动态字段关系管理
# 前言
在平台前期设计中,三方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
创建用户的动态关系:
{
"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
# 飞书
创建分组的动态关系
{
"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
你可以直接在平台上对这块儿内容就进行维护:
通过页面对字段关系进行维护。