2024年3月

前言

在计算机网络中,我们经常会遇到在不同计算机网络系统之间如何共享和访问文件的场景,并且在实际项目中有这样的需求,在Linux中需要动态的mount文件,需要选择合适的网络文件共享协议以满足并发,吞吐量等需求。这就涉及今天要讲的网络文件共享协议SMB和NFS。

SMB vs NFS

什么是SMB

SMB 即 Server Message Block,最初是由IBM开发的,并被Microsoft进一步发展为CIFS(Common Internet File System)。虽然主要使用于windows,但目前也支持跨平台。该协议还在不断发展,最新的SMB版本是v.3.1.1。有时会将CIFS与SMB混淆,实际上CIFS是微软对SMB的实现。

大家可能也听说过Samba,Samba是SMB在Linux上的实现:

Samba:SMB 协议最初是由 Samba 提供 Unix 支持的。由于微软最初没有公开发布其专有协议的公共规范,Samba 的开发者不得不对其进行逆向工程。未来版本的 Samba 能够使用后来 SMB 协议的公开规范。Samba 包括对 SMB3(3.1.1)的支持。

Linux CIFS utils:这个内核软件充当 SMB 客户端,是在 Linux 上挂载现有 SMB 共享的首选方法。它最初是作为 Samba 软件的一部分包括在内的,但现在可以单独获得。Linux CIFS utils 作为大多数 Linux 发行版中的 cifs_utils 软件包提供

什么是NFS

NFS 即 Network File System,是由Sun Microsystems(现在为Oracle Corporation的一部分)开发的协议。它主要被设计用于UNIX/Linux操作系统的环境中。NFS v4是最新NFS版本。它支持并行文件访问,并且在这个版本中改进了安全性。向后兼容NFS v2和NFS v3。NFS v4支持更多的身份验证。

SMB vs NFS

SMB NFS
认证 User-based Host-based
端口 TCP 445; TCP 139, UDP 137, 138 TCP 2049, UDP 2049, TCP 666666 and UDP 666666; TCP 6666660, UDP 6666660, TCP 4045, UDP 4045.
加密 Kerberos, AES-256 Kerberos and TLS
File Lock 支持 只有高版本的支持
Performance 小文件performance更好,大文件一样。

实现

首先我们的网络文件是使用的Azure服务,我们首先来调查下Azure Blog服务都支持的协议,总结如下:

Azure Storage Service 支持的协议
Azure Blob Storage NFS 3.0
Azure File Storage (Standard) SMB
Azure File Storage (Premium) NFS 4.1, SMB

然后我们使用网络文件是为了读写Sqlite文件,而使用Sqlite文件必须得支持File Lock, 而NFS 3.0并不支持File Lock, 综合考虑,最合适的是SMB。 c#代码如下:

class LinuxMount : IMount
{
    private const String CredFolder = "/tmp";

    private static readonly String MountFileStorageCommand = "mount -t cifs {0} {1} -o credentials={2},dir_mode=0777,file_mode=0777,uid=0,gid=0,cache=strict,mfsymlinks,nobrl";
    private static readonly String UnmountFileStorageCommand = "umount -f {0}";

    public void Mount(DeviceMountOption option)
    {
        var mountPoint = option.MountPoint;
        EnsureFolder(mountPoint);
        EnsureFolder(CredFolder);
        var credFile = Path.Combine(CredFolder, $"credentials_{Guid.NewGuid()}");
        var credContent = $"username={option.Username}{Environment.NewLine}password={option.Password}";
        try
        {
            File.WriteAllText(credFile, credContent);
            ExcuteCommand(String.Format(MountFileStorageCommand, option.FileSharePath, mountPoint, credFile));
        }
        finally
        {
            if (File.Exists(credFile))
            {
                File.Delete(credFile);
            }
        }
    }

    public void Unmount(String mountPoint)
    {
        ExcuteCommand(String.Format(UnmountFileStorageCommand, mountPoint));
    }

    private void EnsureFolder(String folder)
    {
        var dir = new DirectoryInfo(folder);
        if (!dir.Exists)
        {
            dir.Create();
        }
    }

    private void ExcuteCommand(String command)
    {
        using var proc = new Process();
        proc.StartInfo.FileName = "sh";
        proc.StartInfo.Arguments = $"-c \"{command}\"";
        proc.StartInfo.UseShellExecute = false;
        proc.StartInfo.CreateNoWindow = true;
        proc.StartInfo.RedirectStandardOutput = true;
        proc.StartInfo.RedirectStandardError = true;
        proc.Start();

        var result = proc.StandardOutput.ReadToEnd();
        result += proc.StandardError.ReadToEnd();

        proc.WaitForExit();
        if (proc.ExitCode != 0)
        {
            throw new Exception($"Command failed, {result}");
        }
    }
}

今天和大家分享一下:大模型提示工程之Prompt框架和示例:

TAG框架

  • 任务(Task): 开发一个新的手机应用,旨在帮助用户更好地管理他们的日常健康。
  • 行动(Action): 进行市场调研,设计用户友好的界面,开发核心健康跟踪功能,测试应用并收集用户反馈。
  • 目标(Goal): 在六个月内发布应用,并在发布后的第一个月内达到10万次下载。

SPAR框架

  • 情境(Scenario): 用户对现有的健康管理应用感到不满,因为它们通常操作复杂,功能繁多而不实用。
  • 问题(Problem): 用户需要一个简单直观的应用来跟踪日常健康数据,如饮食、运动和睡眠。
  • 行动(Action): 设计并开发一个以用户体验为中心,功能集中于核心健康管理的应用。
  • 结果(Result): 用户能够轻松管理自己的健康,应用的用户满意度和市场占有率提高。

TRACE框架

  • 任务(Task): 提升应用的用户参与度和日活跃用户数(DAU)。
  • 请求(Request): 需要一个功能,能让用户定制个性化的健康计划。
  • 行动(Action): 开发一个算法,根据用户的行为和偏好提供定制化健康建议。
  • 背景(Context): 用户参与度低,因为缺乏个性化和互动性。
  • 示例(Example): 竞争对手的应用通过引入个性化健康计划,使其DAU增加了25%。

SCOPE框架

  • 情境(Scenario): 应用市场上健康管理类应用众多,但用户粘性不高。
  • 复杂情况(Complications): 用户往往下载后短期内使用,但很快就弃用。
  • 目标(Objective): 创建一个具有高用户粘性的健康管理应用。
  • 计划(Plan): 引入社交功能,让用户能够分享进度,并与朋友一起参与健康挑战。
  • 评估(Evaluation): 通过跟踪用户的留存率和社交互动数据来评估应用的粘性。

APE框架

  • 行动(Action): 在应用内引入基于位置的健康食品推荐功能。
  • 目的(Purpose): 使用户能够根据自己的位置找到健康的饮食选项,从而促进健康饮食习惯。
  • 期望(Expectation): 用户能够频繁使用此功能,提升整体应用的使用频率和用户满意度。

SAGE框架

  • 情况(Situation): 用户反映应用中缺乏互动和个性化体验。
  • 行动(Action): 添加一个AI健康助手,为用户提供个性化建议和互动体验。
  • 目标(Goal): 增强用户体验,提高用户的日常活跃度。
  • 预期(Expectation): 通过用户反馈和活跃度数据,期望AI健康助手能提升用户满意度至少20%。

RTF框架

  • 角色(Role): 作为用户的健康管理顾问。
  • 任务(Task): 提供根据用户生活习惯定制的健康建议。
  • 格式(Format): 以每日提醒和周报的形式提供健康建议。

ROSES模型

  • 角色(Role): 作为健康管理应用的开发者。
  • 目标(Objective): 提供一款能够帮助用户实现健康目标的应用。
  • 情境(Scenario): 用户希望通过应用来跟踪和改善他们的日常健康习惯。
  • 解决方案(Solution): 开发包含饮食、运动和睡眠跟踪的综合性健康应用。
  • 步骤(Steps): 进行市场调研,设计UI/UX,开发应用功能,进行测试,收集反馈,发布应用。

CARE框架

  • 背景(Context): 健康管理应用市场竞争激烈,用户对新应用的期望日益增高。
  • 行动(Action): 开发一个集成了最新技术(如AI和AR)的创新健康管理应用。
  • 结果(Result): 应用因其创新特性和卓越的用户体验而脱颖而出,获得高用户评价。
  • 示例(Example): 一款采用AI个性化推荐系统的健康应用在三个月内下载量翻倍。

本文分享自华为云社区《
突破语言golang中的类型限制
》,作者:码乐。

1 简介

在使用c语言编程时,常常因为类型的问题大伤脑筋,而其他语言比如java,python默认类型又是难以改变的,golang提供了一些方式用于喜欢hack的用户。

2 标准库unsafe的简单介绍

官方说明标准库 unsafe 包含绕过 Go 程序的类型安全的操作。

导入unsafe包可能是不可移植的,并且不受 Go 1 兼容性指南的保护。

在1.20中,标准库的unsafe包很小, 二个结构体类型,八个函数,在一个文件中。

package unsage

type ArbitraryType
inttype IntegerTypeinttype Pointer*ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

func Add(ptr Pointer, len IntegerType) Pointer
func Slice(ptr
*ArbitraryType, len IntegerType) []ArbitraryType
func SliceData(slice []ArbitraryType)
*ArbitraryType
func String(ptr
*byte, len IntegerType) stringfunc StringData(strstring) *byte

unsafe包定义了 二个类型和 八个函数,二个类型 ArbitraryType 和 IntegerType 不真正属于unsafe包,我们在Go代码中并不能使用它们定义变量。

它表示一个任意表达式的类型,仅用于文档目的,Go编译器会对其做特殊处理。

虽然位于 unsafe,但是 Alignof,Offsetof,Sizeof,这三个函数的使用是绝对安全的。 以至于Go设计者Rob pike提议移走它们。

这三个函数的共同点是 都返回 uintptr 类型。

之所以使用 uintptr 类型而不是 uint64 整型,因为这三个函数更多应用于 有 unsafe.Pointer和 uintptr类型参数的指针运算。

采用uintptr做为返回值类型可以减少指针运算表达式的显式类型转换。

2.1 获取大小 Sizeof

Sizeof 用于获取一个表达式的大小。 该函数获取一个任意类型的表达式 x,并返回 按bytes计算 的大小,假设变量v,并且v通过 v =x声明。

Sizeof 接收任何类型的表达式x,并返回以bytes字节为单位的大小, 并且假设变量v是通过var v = x声明的。该大小不包括任何可能被x引用的内存。

例如,如果x是一个切片,Sizeof返回切片描述符的大小,而不是该片所引用的内存的大小。
对于一个结构体,其大小包括由字段对齐引入的任何填充。

如果参数x的类型没有变化,不具有可变的大小,Sizeof的返回值是一个Go常数不可变值 。
(如果一个类型是一个类型参数,或者是一个数组,则该类型具有可变的大小或结构类型中的元素大小可变)。

示例:

    var(
i
int = 5a= [10]int{}
ss
=a[:]
f FuncFoo

preValue
= map[string]uintptr{"i": 8,"a": 80,"ss": 24,"f": 48,"f.c": 10,"int_nil": 8,
}
)

type FuncFoo
struct{
a
intbstringc [10]byted float64
}
func TestFuncSizeof(t
*testing.T) {
defer setUp(t.Name())()
fmt.Printf(
"\tExecute test:%v\n", t.Name())if unsafe.Sizeof(i) != preValue["i"] {
ErrorHandler(fmt.Sprintf(
"size: %v not equal %v", unsafe.Sizeof(i), preValue["i"]), t)
}
if unsafe.Sizeof(a) != preValue["a"] {
ErrorHandler(fmt.Sprintf(
"size: %v not equal %v", unsafe.Sizeof(i), preValue["a"]), t)

}
if unsafe.Sizeof(ss) != preValue["ss"] {
ErrorHandler(fmt.Sprintf(
"size: %v not equal %v", unsafe.Sizeof(i), preValue["ss"]), t)

}
if unsafe.Sizeof(f) != preValue["f"] {
ErrorHandler(fmt.Sprintf(
"size: %v not equal %v", unsafe.Sizeof(i), preValue["f"]), t)

}
if unsafe.Sizeof(f.c) != preValue["f.c"] {
ErrorHandler(fmt.Sprintf(
"size: %v not equal %v", unsafe.Sizeof(i), preValue["f.c"]), t)

}
if unsafe.Sizeof(unsafe.Sizeof((*int)(nil))) != preValue["int_nil"] {
ErrorHandler(fmt.Sprintf(
"size: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

}
}

Sizeof 函数不支持之间传入无类型信息的nil值,如下错误

    unsafe.Sizeof(nil)  

我们必须显式告知 Sizeof 传入的nil究竟是那个类型,

    unsafe.Sizeof(unsafe.Sizeof((*int)(nil))) 

必须显式告知nil是哪个类型的nil,这就是传入一个值 nil 但是类型明确的变量。

对齐系数 Alignof 用于获取一个表达式的内地地址对齐系数,对齐系数 alignment factor 是一个计算机体系架构 computer architecture 层面的术语。

在不同计算机体系中,处理器对变量地址都有对齐要求,即变量的地址必须可被该变量的对齐系数整除。

它接收一个任何类型的表达式x,并返回所需的排列方式 假设变量v是通过var v = x声明的。
它是m一个最大的值。

例1,

        a      = [10]int{}

reflect.TypeOf(x).Align()
//8 unsafe.Alignof(a) //8

它与reflect.TypeOf(x).Align()返回的值相同。

作为一个特例,如果一个变量s是结构类型,f是一个字段,那么Alignof(s.f)将返回所需的对齐方式。

该类型的字段在结构中的位置。这种情况与reeflect.TypeOf(s.f).FieldAlign()返回的值。

Alignof的返回值是一个Go常数,如果参数的类型不具有可变大小。
(关于可变大小类型的定义,请参见[Sizeof]的描述)。

继上 例2:

      var(
i
int = 5a= [10]int{}
ss
=a[:]
f FuncFoo
zhs
= ""preValue= map[string]uintptr{"i": 8,"a": 80,"ss": 24,"f": 48,"f.c": 10,"int_nil": 8,
}
)

func TestAlignof(t
*testing.T) {

defer setUp(t.Name())()
fmt.Printf(
"\tExecute test:%v\n", t.Name())var x intb := uintptr(unsafe.Pointer(&x))%unsafe.Alignof(x) == 0t.Log("alignof:", b)if unsafe.Alignof(i) != preValue["i"] {
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

}
if unsafe.Alignof(a) != preValue["i"] {
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

}
if unsafe.Alignof(ss) != preValue["i"] {
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

}
if unsafe.Alignof(f.a) != preValue["i"] {
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

}
if unsafe.Alignof(f) != preValue["i"] {
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["int_nil"]), t)

}

中文对齐系数 为 8

        if unsafe.Alignof(zhs) != preValue["i"] {
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), preValue["i"]), t)
}

空结构体对齐系数 1

        if unsafe.Alignof(struct{}{}) != 1{
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), 1), t)
}

byte 数组对齐系数为 1

        if unsafe.Alignof(sbyte) != 1{
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), 1), t)
}

长度为0 的数组,与其元素的对齐系数相同

        if unsafe.Alignof([0]int{}) != 8{
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), 8), t)
}

长度为0 的数组,与其元素的对齐系数相同

        if unsafe.Alignof([0]struct{}{}) != 1{
ErrorHandler(fmt.Sprintf(
"Alignof: %v not equal %v", unsafe.Sizeof(i), 1), t)
}

}

执行它:

    go test -timeout 30s -run ^TestAlignof$ ./unsafe_case.go

对齐系数 alignment factor,变量的地址必须可被该变量的对齐系数整除。

2.2 使用对齐的例子

我们使用相同字段,分别创建两个结构体属性分别为对齐或不对齐,帮助 go 更好地分配内存和 使用cpu读取,查看效果

    type RandomResource struct{
Cloud
string //16 bytes Name string //16 bytes HaveDSL bool //1 byte PluginVersion string //16 bytes IsVersionControlled bool //1 byte TerraformVersion string //16 bytes ModuleVersionMajor int32 //4 bytes }

type OrderResource
struct{
ModuleVersionMajor int32
//4 bytes HaveDSL bool //1 byte IsVersionControlled bool //1 byte Cloud string //16 bytes Name string //16 bytes PluginVersion string //16 bytes TerraformVersion string //16 bytes }

字段 存储使用的空间与 字段值没有关系

         vard RandomResource
d.Cloud
= "aws-singapore"...

InfoHandler(fmt.Sprintf(
"随机顺序属性的结构体内存 总共占用 StructType: %T => [%d]\n", d, unsafe.Sizeof(d)), m)var te =OrderResource{}
te.Cloud
= "aws-singapore"...
m.Logf(
"属性对齐的结构体内存 总共占用 StructType:d %T => [%d]\n", te, unsafe.Sizeof(te))

然后复制结构体,并改变其属性值,查看存储空间和值的长度变化

        te2 :=te
te2.Cloud
= "ali2"m.Logf("结构体2 te2:%#v\n", &te2)
m.Logf(
"结构体1 te:%#v\n", &te)

m.Log(
"改变 te3 将同时改变 te,te3 指向了 te的地址")
m.Log(
"复制了对齐结构体,并重新赋值,用于查看字段长度。")
m.Log(
"(*te).Cloud:", (te).Cloud, "*te.Cloud", te.Cloud, "te size:", unsafe.Sizeof(te.Cloud), "te value len:", len(te.Cloud))

te3 :
= &te
te3.Cloud
= "HWCloud2"m.Log("(*te3).Cloud:", (*te3).Cloud, "*te3.Cloud", te3.Cloud, "te3 size:", unsafe.Sizeof(te3.Cloud), "te3 value len:", len(te3.Cloud))
m.Logf(
"字段 Cloud:%v te3:%p\n", (*te3).Cloud, te3)
m.Logf(
"字段 Cloud:%v order:%v te:%v, addr:%p\n", te.Cloud, (te).Cloud, te, &te)

执行它,

    go test -v .\case_test.go

得到以下输出:

随机顺序属性的结构体内存 总共占用 StructType: main.Raesource => [88]

    ...

属性对齐的结构体内存 总共占用 StructType:d main.OrderResource => [72]

改变 te3 将同时改变 te,te3 指向了 te的地址

    case_test.go:186: 复制了对齐结构体,并重新赋值,用于查看字段长度。

case_test.go:
188: (*te).Cloud: aws-singapore *te.Cloud aws-singapore te size: 16 te Alignof: 8 te value len: 13 reflect Align len and field Align len: 8 8case_test.go:190: (*te2).Cloud: ali2 *te2.Cloud aws-singapore te2 size: 16 te2 Alignof: 8 te2 value len: 4 reflect Align len and field Align len: 8 8case_test.go:196: (*te3).Cloud: HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company *te3.Cloud HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company te3
size:
16 te3 Alignof: 8 te3 value len: 105 reflect Align len and field Align len: 8 8case_test.go: 结构体1字段 Cloud:HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company te2:0xc0000621e0case_test.go:198: 结构体2字段 Cloud:ali2 te2:0xc000062280case_test.go:199: 结构体3字段 Cloud:HWCloud2-asia-southeast-from-big-plant-place-air-local-video-service-picture-merge-from-other-all-company te3:0xc0000621e0

小结

我们介绍了unsafe包的检查功能,在初始化时,go结构体已经分配了对于的内存空间,

一个结构体而言,结构体属性为随机顺序的,go将分配更多内存空间。 即使是复制后。

比如 结构体的Cloud 字段。

Sizeof表达式大小总是16,
而对齐系数 Alignof 大小总是8,
而在不同的结构体实例中值长度可以为 4,13, 105.

本节源码地址:

https://github.com/hahamx/examples/tree/main/alg_practice/2_sys_io

点击关注,第一时间了解华为云新鲜技术~

客户管理系统的应用架构设计

应用层定义了软件系统的应用功能,负责接收用户的请求,协调领域层能力来执行任务,并将结果返回给用户,功能模块包括:

  • 客户管理:核心功能模块,负责收集和更新客户信息,包括个人资料、联系方式、消费习惯、会员卡、归属信息(比如销售或顾问)和备注。这个模块是CRM系统的基础,支撑其他模块的运作,提供详细的客户信息,帮助企业更好的理解和服务客户。
  • 客户标签:通过对客户进行标签化管理,实现客户的细分和个性化服务。支持创建新标签、删除标签、批量打标签和自动打标签等功能,以及同步到企业微信等三方平台的标签,让客户信息的管理和应用更加灵活、精准。
  • 人群运营:针对不同的客户群体,执行有针对性的营销和服务策略。包括人群圈选(根据特定标准选择目标客户群)、场景营销(根据客户所处的具体场景设计营销活动)、互动营销(通过互动提高客户参与度)、促销工具(如限时折扣、买赠等),实现精准营销,并提升客户参与度。
  • 触达渠道:定义了企业与客户沟通的多种渠道,包括电话外呼、短信、小程序订阅消息、微信群发等。这个模块使企业能够通过多种渠道与客户进行有效沟通,提供信息、促销和服务,增强客户体验。
  • 数据分析:对客户数据进行深入分析,包括会员业绩、会员画像、RFM模型分析(基于客户最近一次购买时间、购买频率、购买金额的分析模型)、消费分析(包括消费习惯、复购率等)、积分和储值分析。通过这些分析,企业可以获得关键洞察,以改善营销策略和提升客户服务。
  • 客户资产:管理客户的权益价值,包括储值(预存款)、积分(奖励计划)、权益卡(如会员卡提供的特权)、优惠券和自定义权益等。这个模块帮助企业建立和维护客户忠诚度计划,通过提供价值和优惠,来鼓励客户消费和再次消费。

领域层是业务逻辑的核心,专注于表示业务概念、业务状态流转和业务规则,沉淀可复用的系统能力。

  • 客户基础
    • 客户基本信息:维护客户的基础数据,如姓名、联系方式、地址等。这是识别和联系客户的核心信息。
    • 自定义资料项:允许企业根据业务需要,添加客户的额外信息,提供灵活性以适应各种业务场景。
    • 客户变更记录:记录客户信息的变更记录,提供历史数据追踪,用于审计和服务质量检查。
    • 客户归属:明确客户与公司内部人员(例如,销售团队、客户经理)的关系,以便明确客户管理的职责。
    • 客户授权:管理客户授权给企业的权限,如数据访问和处理的权限,确保数据处理的合法性和合规性。
    • 客户合并处理:解决客户记录重复的问题,通过合并相似或重复的客户记录来维护数据的准确性和一致性。
    • 行为明细:采集并记录客户的具体行为数据,如页面访问、产品浏览和购买行为等。
    • 交易行为统计:对客户的交易行为(如购买频次、金额等)进行汇总和统计,支持业务分析和决策。
  • 客户标签
    • 标签元数据:管理标签的定义,包括标签名称、类型和适用范围等,是标签管理的基础。
    • 标签模板管理:提供标签模板的创建、编辑和删除功能,支持标签的快速应用和复用。
    • 自动打标签:根据预定义的规则自动为客户打标,如根据购买行为自动标记为“高价值客户”。
    • 手动打标签:允许用户手动为客户添加或修改标签,提供灵活的客户细分和管理能力。
    • 批量打标签:允许用户一次性为多个客户添加相同的标签,相比单个操作,大大提高了工作效率。
    • 标签同步:标签同步功能可以保持在不同系统和平台间的客户标签一致。比如,可以同步到其他CRM系统、营销自动化平台或企业微信等第三方系统的标签。
  • 客户资产
    • 积分:管理客户通过购买行为或参与活动获得的积分,以及积分的使用和过期规则。
    • 权益:定义和管理客户拥有的各种权益。
    • 权益卡:管理客户的会员卡或权益卡,及其对应的权益和条件。
    • 等级:根据客户的消费行为划分客户等级,管理等级升降规则和相应的权益。
    • 权益核销:处理客户使用权益(如优惠券使用、积分兑换)的操作和记录,确保权益的正确核销。
    • 储值:管理客户的预付款项或账户余额,支持储值的使用、充值和退款操作。

前言

最近我在可视化课程中学习了如何在Canvas中利用像素处理来实现滤镜效果,在这节课程的结尾留了一道局部放大镜的题目,提示我们用像素处理的方式去实现这个效果,最终实现随着鼠标移动将图片局部放大,本着把学到的内容落地实践的想法,我就去思考了一番,但很不幸,我思考了好几天也没思考出结果,因为刚开始我想的一直是在一个Canvas上来操作,但是一来我对Canvas API还并不是很熟悉,二来我对像素处理还不够熟练,然后第三是如果原图的部分像素被处理了,那下一次放大就会有问题,因此我最终放弃了这个思路,选择了再增加一个Canvas来完成最终的效果,以下就是利用这种方式实现图片局部放大的效果。

像素处理

在实现这个效果之前,我们先来了解一下如何处理像素,有些小伙伴可能不太清楚,所以这里简单说一下,在屏幕上我们知道所有显示的内容都是由像素点组成的,那么在处理像素之前,我们需要先获取到像素信息,那么Canvas就是提供了一个API叫做getImageData让我们可以获取到画布上的像素信息,最终这个API返回的是一个
ImageData类型
的值,关于这个API的具体描述可以参考对应的
MDN页面

ImageData类型的数据包含三个属性,包括data、width、height。width和height简单来说,就是被提取像素信息的区域的宽高,最主要的像素信息是在这个data属性中。data属性指向一个数组类型的值,准确来说是Uint8ClampedArray的实例,Uint8ClampedArray表示
8 位无符号整型固定数组
,也就是说其中的元素是0到255之间的整数,我们知道一个像素的颜色信息可以使用rgba四个分量表示,那么我们就得出在data数组中每四个元素就能表示一个像素点的信息,因此data数组的长度就是
width * height * 4

了解完像素处理,我们就可以开始进行具体的实现了。

具体实现

<canvas ref="canvasRef" width="0" height="0"></canvas>
<canvas ref="magnifier" width="0" height="0"></canvas><!-- 放大镜 -->

1. 准备工作

在实现放大效果之前,我们需要先把图片加载到Canvas上:

(async function() {
  const img = await loadImage('src/assets/girl1.jpg');
  canvasRef.value.width = img.width;
  canvasRef.value.height = img.height;
  context.drawImage(img, 0, 0);
}());

这里
loadImage
方法是通过Image对象来异步加载图片,然后通过drawImage方法将图片绘制到画布上。

接着设置一个要放大的区域,也就是以鼠标坐标为中心,多少半径以内的内容要被放大,这里我设置一个变量originSize用于存储原图大小,并设置一个5倍的放大倍数。

let originSize = 40; // 原图大小
let zoom = 5; // 放大倍数

(async function() {
  // ...
  magnifier.value.width = originSize * zoom;
  magnifier.value.height = originSize * zoom;
}());

用作于放大镜的magnifier,我们使用
originSize * zoom
来设置它的宽高。

2. 鼠标移动事件监听

接下来就是主要的代码实现。

首先是添加鼠标移动事件的监听:

const addEvent = () => {
  canvasRef.value.addEventListener('mousemove', mouseDownHandler);
};

addEvent();

然后我们就来实现
mouseDownHandler
函数。

  • 首先我们获取鼠标坐标在Canvas中的相对坐标,并通过
    Math.floor
    取整

    const mouseDownHandler = e => {
      // 相对于画布的坐标
      const center = {
        x: Math.floor(e.pageX - left),
        y: Math.floor(e.pageY - top)
      };
    };
    
  • 然后利用
    getImageData
    方法获取指定区域的像素信息,这里我们用到了
    OffscreenCanvas
    ,它提供了一个可以脱离屏幕渲染的 canvas 对象,可以提升渲染性能;这样我们就得到了待放大区域的像素信息。

    const mouseDownHandler = e => {
      // 相对于画布的坐标
      // ...
      // 待放大区域的imageData
      const originImageData = getImageData(img, [center.x - originSize / 2, center.y - originSize / 2, originSize, originSize]);
    };
    
  • 现在我们需要一个ImageData类型的变量,用于存储放大后的像素信息,因为最终要渲染到magnifier这个Canvas上,我们就使用magnifier的2d上下文对象调用
    createImageData
    方法来创建一个ImageData对象,关于这个方法的使用具体可查看
    MDN文档

    const mouseDownHandler = e => {
      // 相对于画布的坐标
      // ...
      // 待放大区域的imageData
      // ...
      // 构建一个imageData
      const areaImageData = mContext.createImageData(magnifier.value.width, magnifier.value.height);
    };
    
  • 接下来就是具体的像素遍历和处理,按照areaImageData的宽高来进行遍历,这里迭代的增量使用
    +zoom
    是因为,当我们放大zoom倍数之后,原图1个像素的信息在magnifier使用
    zoom*zoom
    个像素来放大,也就是
    zoom*zoom
    个像素点的色值和原图中对应的那个像素的色值是一样的。在我们这段代码中设置zoom为5,也就是放大后使用5*5=25个像素点表示之前的一个像素点。

    const mouseDownHandler = e => {
      // 相对于画布的坐标
      // ...
      // 待放大区域的imageData
      // ...
      // 构建一个imageData
      // ...
      let count = 0;
      for (let j = 0; j < originSize * zoom; j += zoom) {
        for (let i = 0; i < originSize * zoom; i += zoom) {
    
          // ...
    
        }
      }
    };
    
  • 所以我们继续使用两个for循环k和m,把areaImageData的data数组中的对应元素赋值为原图对应像素的色值,完成赋值后我们就可以通过putImageData方法将像素信息渲染到magnifier画布上。

    const mouseDownHandler = e => {
      // 相对于画布的坐标
      // ...
      // 待放大区域的imageData
      // ...
      // 构建一个imageData
      // ...
      let count = 0;
      for (let j = 0; j < originSize * zoom; j += zoom) {
        for (let i = 0; i < originSize * zoom; i += zoom) {
    
          for (let k = j; k < j + zoom; k ++) {
            for (let m = i; m < i + zoom; m ++) {
              const index = (k * originSize * zoom + m) * 4;
              areaImageData.data[index] = originImageData.data[count];
              areaImageData.data[index + 1] = originImageData.data[count + 1];
              areaImageData.data[index + 2] = originImageData.data[count + 2];
              areaImageData.data[index + 3] = originImageData.data[count + 3];
    
            }
          }
          count += 4;
    
        }
      }
      mContext.putImageData(areaImageData, 0, 0);
    };
    

至此我们就实现了基本的局部放大,但现在放大镜不在原图Canvas的上方,并且放大镜是一个正方形,我们继续简单优化一下。

3. 简单优化

  • 首先因为我对Canvas API还不太熟悉,所以我现在通过css把放大镜改为圆形,并加上一个阴影
    box-shadow
    来优化视觉效果。

    #magnifier {
      position: absolute;
      box-shadow: 0 0 10px 4px rgba(12, 12, 12, .5);
      border-radius: 50%;
    }
    
  • 然后给两个Canvas外层加一个div容器,把放大镜设置绝对定位,把它放到鼠标坐标的位置,在鼠标移动过程中更新放大镜的位置。

    <div class="canvas-container" ref="containerRef" :style="{width: containerWidth + 'px'}">
      <canvas ref="canvasRef" width="0" height="0"></canvas>
      <canvas ref="magnifier" width="0" height="0" id="magnifier" :style="position"></canvas>
    </div>
    

    const position = reactive({
      left: 0,
      top: 0
    });
    const containerWidth = ref(0);
    
    containerWidth.value = img.width;
    // 在鼠标移动过程中更新放大镜的位置
    position.top = (center.y - originSize * zoom / 2) + 'px';
    position.left = (center.x - originSize * zoom / 2) + 'px';
    

    .canvas-container {
      position: relative;
      overflow: hidden;
    }
    
  • 这个时候放大镜的位置就和我们预想的一致了,但是现在还有一个问题,就是放大镜在原图的上方,在移动的过程中会看到放大镜的渲染有点卡顿,这是因为鼠标移动事件是加在原图Canvas上的,当鼠标悬浮在放大镜上时,这个移动事件的监听就不连贯了,此时我们可以考虑把鼠标移动监听加改为加在外层容器上,这样就能看到移动过程中放大镜的渲染是比较流畅了。

    const addEvent = () => {
      containerRef.value.addEventListener('mousemove', mouseDownHandler);
    };
    

至此就完成了简单的局部放大效果,虽然还存在一些问题吧。

总结

以上的实现比较简单粗暴,就是遍历imageData然后赋值,不算什么很高明的思路,就当作是抛砖引玉吧。

最终效果

完整代码