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
}