动态字段关系管理
# 前言
在平台前期设计中,三方 IM 的组织架构以及用户信息往平台同步时,都是将两侧的字段做成硬编码映射的,在后来群里一些小伙伴沟通中,慢慢才发现,三方 IM 中的一些字段属性很灵活,每家公司用的也各不一样,于是,动态字段关联就很自然地提上了日程。
举个例子直白点说明这个问题:
比如钉钉的用户属性中,关于邮箱的有两个字段,参考获取部门用户详情 (opens new window)。
email:
员工邮箱org_email:
员工的企业邮箱
而事实上,不同的公司可能会选择 email,也可能选择 org_email 作为员工的邮箱字段,这个时候,对于平台要把用户信息同步到平台上来说,转化在代码中,就不知道该怎么处理了 (本地平台以及 ldap 中只有一个 email 邮箱字段来标识用户的邮箱)。
动态字段关联旨在给用户提供创建这种连接关系的自由,用户配置 email 字段,则同步的时候就映射 email 字段,用户配置 org_email 字段,则同步的时候就会将 org_email 字段对应的值同步到本地。示意图如下:
# 实现原理
事实上一开始我并没有特别具体的思路来实现这块儿的功能,只大概想了一个方向,毕竟后端的经验还不够,这时候人脉资源就很重要了,请教了之前的后端同学,给我指导了具体的思路实现,在此感谢周同学。
核心设计在于:用户基于类似连线的方式添加一份三方 IM 字段与本地字段的映射关系,通过 map 存放到 MySQL,当获取到远程数据之后,通过遍历映射关系,将远程数据挂载到本地结构体中。
# 动态字段
动态字段结构体定义如下:
package model
import (
"gorm.io/datatypes"
"gorm.io/gorm"
)
type FieldRelation struct {
gorm.Model
Flag string `gorm:"type:varchar(20);comment:'数据标志'" json:"flag"`
Attributes datatypes.JSON `gorm:"comment:'字段关系'" json:"attributes"`
}
2
3
4
5
6
7
8
9
10
11
12
这里引入了 https://github.com/go-gorm/datatypes (opens new window) ,以基于 gorm 官方封装的能力,进行 JSON 字段的管理。
最开始我想着把字段横着展开,只是字段将要存储分组与用户两种关系,两种关系的基础字段不一致,因此展开之后并不美观,因此最后选择了将数据直接以 JSON 格式存入 MySQL。
datatypes.JSON
已经内置了 Value()
和 Scan(value interface{})
两个方法,使得我们在与 MySQL 交互的时候,可以像普通字段一样对待 JSON 数据,而不必再进行其他封装。
# 构建数据
这里就只拿 Group 进行举例,在 Group 结构体下定义分组中需要灵活映射的字段方法:
type Group struct {
gorm.Model
GroupName string `gorm:"type:varchar(20);comment:'分组名称'" json:"groupName"`
Remark string `gorm:"type:varchar(100);comment:'分组中文说明'" json:"remark"`
Creator string `gorm:"type:varchar(20);comment:'创建人'" json:"creator"`
GroupType string `gorm:"type:varchar(20);comment:'分组类型:cn、ou'" json:"groupType"`
Users []*User `gorm:"many2many:group_users" json:"users"`
ParentId uint `gorm:"default:0;comment:'父组编号(编号为0时表示根组)'" json:"parentId"`
SourceDeptId string `gorm:"type:varchar(100);comment:'部门编号'" json:"sourceDeptId"`
Source string `gorm:"type:varchar(20);comment:'来源:dingTalk、weCom、ldap、platform'" json:"source"`
SourceDeptParentId string `gorm:"type:varchar(100);comment:'父部门编号'" json:"sourceDeptParentId"`
SourceUserNum int `gorm:"default:0;comment:'部门下的用户数量,从第三方获取的数据'" json:"source_user_num"`
Children []*Group `gorm:"-" json:"children"`
GroupDN string `gorm:"type:varchar(255);not null;comment:'分组dn'" json:"groupDn"` // 分组在ldap的dn
}
func (g *Group) SetGroupName(groupName string) {
g.GroupName = groupName
}
func (g *Group) SetRemark(remark string) {
g.Remark = remark
}
func (g *Group) SetSourceDeptId(sourceDeptId string) {
g.SourceDeptId = sourceDeptId
}
func (g *Group) SetSourceDeptParentId(sourceDeptParentId string) {
g.SourceDeptParentId = sourceDeptParentId
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
然后通过 BuildGroupData
方法,将远程数据进行挂载:
// BuildGroupData 根据数据与动态字段组装成分组数据
func BuildGroupData(flag string, remoteData map[string]interface{}) (*model.Group, error) {
output, err := sonic.Marshal(&remoteData)
if err != nil {
return nil, err
}
oldData := new(model.FieldRelation)
err = isql.FieldRelation.Find(tools.H{"flag": flag + "_group"}, oldData)
if err != nil {
return nil, tools.NewMySqlError(err)
}
frs, err := tools.JsonToMap(string(oldData.Attributes))
if err != nil {
return nil, tools.NewOperationError(err)
}
g := &model.Group{}
for system, remote := range frs {
switch system {
case "groupName":
g.SetGroupName(gjson.Get(string(output), remote).String())
case "remark":
g.SetRemark(gjson.Get(string(output), remote).String())
case "sourceDeptId":
g.SetSourceDeptId(fmt.Sprintf("%s_%s", flag, gjson.Get(string(output), remote).String()))
case "sourceDeptParentId":
g.SetSourceDeptParentId(fmt.Sprintf("%s_%s", flag, gjson.Get(string(output), remote).String()))
}
}
return g, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
当我们拿到远程数据之后,就可以调用此方法将远程数据,根据字段转化成本地 Group 的结构体字段了。
# 预留字段
其实说是动态字段,也不能完全没有任何约束的动态化,三方 IM 与本地平台的字段都不能超出已有给定的字段之外。
这里先陈列出三方 IM 对应的字段属性,你只能在如下陈列的属性中进行关系映射的选择,如果还有重要的字段没有出现在如下列表,请提交 issue。
注意:
有些场景中,三方 IM 提供的字段未必直接适合本地使用,因此后台也提供了一些自定义的字段,以供用户选择,本平台自定义字段,将以custom_
前缀作为自定义字段的标识,请注意区分。
# 三方字段
# 钉钉字段
Group:字段详情参考获取部门列表 (opens new window)
官方字段
id:
部门 IDname:
部门名称parentid:
父部门 ID
自定义字段
custom_name_pinyin:
部门名称拼音
User:字段详情参考获取部门用户详情 (opens new window)
官方字段
userid:
用户的 userIdunionid:
用户在当前开发者企业帐号范围内的唯一标识name:
用户姓名avatar:
头像mobile:
手机号码job_number:
工号title:
职位work_place:
工位remark:
备注leader:
是否是部门的主管org_email:
员工的企业邮箱email:
员工邮箱department_ids:
所属部门 id 列表
自定义字段
custom_name_pinyin:
用户姓名拼音,可能会在多音字方面出现问题。custom_nickname_org_email:
企业邮箱前缀,如邮箱为:liql@eryajf.net
,则该字段值为:liql
。custom_nickname_email:
员工邮箱前缀,如邮箱为:liql@eryajf.net
,则该字段值为:liql
。
如上三个字段都是为了提取出本地用户登录时使用的名字,各个公司情况不一,这里就尽可能把情况都兼容了。
# 企业微信字段
Group:字段详情参考获取部门列表 (opens new window)
- 官方字段
id:
部门 IDname:
部门名称name_en:
部门英文名称parentid:
父部门 ID
- 自定义字段
custom_name_pinyin:
部门名称拼音
- 官方字段
User:字段详情参考获取部门用户详情 (opens new window)
官方字段
name:
用户姓名userid:
用户的 useridmobile:
手机号码position:
职位gender:
性别email:
邮箱biz_email:
企业邮箱avatar:
头像telephone:
座机alias:
别名external_position:
对外职务address:
地址open_userid:
用户的 openidmain_department:
主部门english_name:
英文名department_ids:
所属部门 id 列表
自定义字段
custom_name_pinyin:
用户姓名拼音,可能会在多音字方面出现问题。custom_nickname_biz_email:
企业邮箱前缀,如邮箱为:liql@eryajf.net
,则该字段值为:liql
。custom_nickname_email:
员工邮箱前缀,如邮箱为:liql@eryajf.net
,则该字段值为:liql
。
如上三个字段都是为了提取出本地用户登录时使用的名字,各个公司情况不一,这里就尽可能把情况都兼容了。
# 飞书字段
Group:字段详情参考获取部门列表 (opens new window)
- 官方字段
name:
部门名称parent_department_id:
父部门 IDdepartment_id:
部门 IDopen_department_id:
部门的 open_idleader_user_id:
部门的主管 IDunit_ids:
部门单位的自定义 ID 列表
- 自定义字段
custom_name_pinyin:
部门名称拼音
- 官方字段
User:字段详情参考获取部门用户详情 (opens new window)
官方字段
name:
用户姓名union_id:
用户的 union_iduser_id:
用户的 user_idopen_id:
用户的 open_iden_name:
英文名nickname:
别名email:
邮箱mobile:
手机号码gender:
性别avatar:
头像city:
城市country:
国家work_station:
工位join_time:
入职时间employee_no:
工号enterprise_email:
企业邮箱job_title:
职位department_ids:
所属部门 ID 列表
自定义字段
custom_name_pinyin:
用户姓名拼音,可能会在多音字方面出现问题。custom_nickname_enterprise_email:
企业邮箱前缀,如邮箱为:liql@eryajf.net
,则该字段值为:liql
。custom_nickname_email:
员工邮箱前缀,如邮箱为:liql@eryajf.net
,则该字段值为:liql
。
如上三个字段都是为了提取出本地用户登录时使用的名字,各个公司情况不一,这里就尽可能把情况都兼容了。
# 本地字段
平台自身的数据属性字段,是可以完全确定的,这里陈列说明如下:
- Group
groupName:
分组名称,建议用name_pinyin
字段映射。remark:
分组说明,建议用name
字段映射。sourceDeptId:
部门 ID,建议用id
字段映射。sourceDeptParentId:
父部门 ID,建议用parentid
字段映射。
- User
username:
用户名,建议用name_pinyin
或者custom_nickname_org_email
字段映射。nickname:
中文名字,建议用name
字段映射。givenName:
花名,建议用name
或者nickname
字段映射。mail:
邮箱,根据实际情况映射用户邮箱。jobNumber:
工号,根据对应字段映射。mobile:
手机号码,根据对应字段映射。avatar:
头像,根据对应字段映射。postalAddress:
地址,根据对应字段映射。position:
职位,根据对应字段映射。introduction:
备注,根据对应字段映射。sourceUserId:
用户 user_id,根据对应字段映射。sourceUnionId:
用户 union_id,根据对应字段映射。
# 实践操练
下边是我在本地测试时添加的字段映射关系,仅做参考,各位根据自己的实际情况进行调整。
# 钉钉
创建分组的动态关系:
{
"flag": "dingtalk_group", // 字段标识
"attributes": {
// 字段属性
"groupName": "custom_name_pinyin", // 分组名称(通常为分组名的拼音)
"remark": "name", // 分组描述
"sourceDeptId": "id", // 部门ID
"sourceDeptParentId": "parentid" // 父部门ID
}
}
2
3
4
5
6
7
8
9
10
创建用户的动态关系:
{
"flag": "dingtalk_user", // 字段标识
"attributes": {
// 字段属性
"username": "custom_name_pinyin", // 用户名(通常为用户名拼音)
"nickname": "name", // 中文名字
"givenName": "name", // 花名
"mail": "email", // 邮箱
"jobNumber": "job_number", // 工号
"mobile": "mobile", // 手机号
"avatar": "avatar", // 头像
"postalAddress": "work_place", // 地址
"position": "title", // 职位
"introduction": "remark", // 说明
"sourceUserId": "userid", // 源用户ID
"sourceUnionId": "unionid" // 源用户唯一ID
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 飞书
创建分组的动态关系
{
"flag": "feishu_group",
"attributes": {
"groupName": "department_id",
"remark": "name",
"sourceDeptId": "open_department_id",
"sourceDeptParentId": "parent_department_id"
}
}
2
3
4
5
6
7
8
9
创建用户的动态关系:
{
"flag": "feishu_user",
"attributes": {
"username": "custom_name_pinyin",
"nickname": "name",
"givenName": "name",
"mail": "email",
"jobNumber": "employee_no",
"mobile": "mobile",
"avatar": "avatar",
"postalAddress": "work_station",
"position": "job_title",
"introduction": "name",
"sourceUserId": "user_id",
"sourceUnionId": "union_id"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 企业微信
创建分组的动态关系
{
"flag": "wecom_group",
"attributes": {
"groupName": "custom_name_pinyin",
"remark": "name",
"sourceDeptId": "id",
"sourceDeptParentId": "parentid"
}
}
2
3
4
5
6
7
8
9
创建用户的动态关系:
{
"flag": "wecom_user",
"attributes": {
"username": "custom_name_pinyin",
"nickname": "name",
"givenName": "alias",
"mail": "email",
"jobNumber": "mobile",
"mobile": "mobile",
"avatar": "avatar",
"postalAddress": "address",
"position": "external_position",
"introduction": "name",
"sourceUserId": "userid",
"sourceUnionId": "userid"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
你可以直接在平台上对这块儿内容就进行维护:
通过页面对字段关系进行维护。