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,)。 DN string // Chain 是拍平拼音链,仅展示用。 Chain string Name string // Level 是部门在树中的层级,用于保证父组先于子组创建。 Level int // Members 是期望成员的用户 DN 集合。 Members map[string]struct{} } // DesiredUser 是期望的用户。 // // 不再有 InactiveDN:离职走物理删除,用户 DN 固定 ou=people/。 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 }