Files
idsync/internal/mapping/mirror.go
T
caowangrenbo 8fd10f134b refactor: mapping 落库化(期望状态改从镜像视图构造 + 拼音拍平组名)
- mapping.BuildFromMirror:由镜像 active 用户/部门构造期望 LLDAP 状态(纯函数),
  组名/DN 直接复用快照落库时算好的拍平拼音,用户 DN 固定 ou=people、去掉离职 OU
- Feat: FlatDeptInfo 已在本阶段完成测试覆盖(拍平拼音父子/空拼音回落/重名消歧/DN 稳定)
- 移除已被目标架构取代的旧「嵌套 CN」测试断言
- 旧 Build(OpenLDAP 单阶段)暂留保证过渡期编译,阶段6退役
2026-09-29 23:25:50 +08:00

122 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mapping
// 镜像 → 期望 LLDAP 状态 的落库化映射。
//
// 与旧 `Build(feishu.Snapshot)`(OpenLDAP 单阶段时代的嵌套 CN + inactive OU 模型)
// 不同,这里是目标架构的映射:输入是 SQLite 镜像的活动用户与活动部门,输出是
// 期望写入 LLDAP 的纯函数状态。LLDAP 组不能嵌套,因此组用拍平拼音点分命名;
// 用户 DN 固定挂在 ou=people 下,离职不靠 DN 移动而是物理删除(见 syncer 阶段 6)。
import (
"sort"
"strings"
"idsync/internal/ldaplib"
"idsync/internal/store"
)
// DesiredGroup 是期望的分组(拍平命名)。
type DesiredGroup struct {
DeptID string
// DN 是完整组 DN(来自 mirror_departments,形式 cn=<父.子>,ou=groups,<base>)。
DN string
// Chain 是拍平拼音链,仅展示用。
Chain string
Name string
// Level 是部门在树中的层级,用于保证父组先于子组创建。
Level int
// Members 是期望成员的用户 DN 集合。
Members map[string]struct{}
}
// DesiredUser 是期望的用户。
//
// 不再有 InactiveDN:离职走物理删除,用户 DN 固定 ou=people/<base>。
type DesiredUser struct {
UID string
DN string
CN string
SN string
GivenName string
DisplayName string
Mail string
Mobile string
EmployeeNumber string
DepartmentNumber string
PostalAddress string
BusinessCategory string
Description string
DeptIDs []string
}
// DesiredState 是期望写入 LLDAP 的完整状态。
type DesiredState struct {
Groups []*DesiredGroup
Users []*DesiredUser
GroupByDeptID map[string]*DesiredGroup
}
// BuildFromMirror 由镜像的活动用户与活动部门构造期望状态。纯函数,不触碰 IO。
//
// 组名与 DN 直接复用镜像落库时算好的 PinyinName/DN(见 FlatDeptInfo),保证
// 物化与预览看到的是同一份命名,不会出现「写入了但预览里看不到」的分叉。
func BuildFromMirror(users []*store.MirrorUser, depts []*store.MirrorDepartment, baseDN string) *DesiredState {
st := &DesiredState{
Groups: make([]*DesiredGroup, 0, len(depts)),
Users: make([]*DesiredUser, 0, len(users)),
GroupByDeptID: make(map[string]*DesiredGroup, len(depts)),
}
peopleOU := "ou=people," + strings.TrimSpace(baseDN)
for _, d := range depts {
g := &DesiredGroup{
DeptID: d.DeptID,
DN: d.DN,
Chain: d.PinyinName,
Name: d.Name,
Level: d.Level,
Members: make(map[string]struct{}),
}
st.Groups = append(st.Groups, g)
st.GroupByDeptID[d.DeptID] = g
}
for _, u := range users {
uid := u.UID
du := &DesiredUser{
UID: uid,
DN: ldaplib.UserDN(uid, peopleOU),
CN: uid,
SN: u.SN,
GivenName: u.GivenName,
DisplayName: u.DisplayName,
Mail: u.Email,
Mobile: u.Mobile,
EmployeeNumber: u.EmployeeNumber,
DepartmentNumber: u.DepartmentNumber,
PostalAddress: u.PostalAddress,
BusinessCategory: u.BusinessCategory,
Description: u.Description,
DeptIDs: u.DepartmentIDs,
}
st.Users = append(st.Users, du)
// 维护成员关系:用户只加入其实际所属且在本轮同步范围内的部门组。
for _, deptID := range u.DepartmentIDs {
if g, ok := st.GroupByDeptID[deptID]; ok {
g.Members[du.DN] = struct{}{}
}
}
}
// 分组按层级排序,父组必须排在子组之前(LLDAP 组虽不嵌套,但物化创建
// 顺序仍要保持父先于子,避免依赖隐含顺序造成问题)。
sort.SliceStable(st.Groups, func(i, j int) bool {
if st.Groups[i].Level != st.Groups[j].Level {
return st.Groups[i].Level < st.Groups[j].Level
}
return st.Groups[i].DeptID < st.Groups[j].DeptID
})
sort.SliceStable(st.Users, func(i, j int) bool { return st.Users[i].UID < st.Users[j].UID })
return st
}