package mapping // 部门组的拍平命名与 DN 构造。 // // LLDAP 组不能嵌套(组里只能有用户,没有父/子组概念),因此部门树必须拍平: // 组名 = 父链拼音点分拼接(如 cn=yanfa.pingtai)。拼音为空时回落到部门 ID 保证 // 非空稳定。组名天然唯一(部门树路径唯一),重名时追加部门 ID 消歧。 // // 这些 DN 在快照落库阶段一次性算好并持久化到 mirror_departments,供物化阶段 // 直接复用,也让 WebUI 能展示每个部门的预期 DN。 import ( "strings" "idsync/internal/feishu" ) // FlatDept 是一个部门的拍平命名结果。 type FlatDept struct { DeptID string // Chain 是拍平后的拼音链,如 "yanfa.pingtai"。 Chain string // DN 是完整组 DN,如 "cn=yanfa.pingtai,ou=groups,"。 DN string // Level 是部门在树中的层级,根为 0。 Level int } // GroupRootDN 返回部门组根 OU 的完整 DN。LLDAP 分组固定挂在 ou=groups 下。 func GroupRootDN(baseDN string) string { return "ou=groups," + strings.TrimSpace(baseDN) } // FlatDeptInfo 把部门树一次性拍平为拼音点分命名并计算每股的 DN。 // // 输入是快照里的全量部门;返回 dept_id → FlatDept。父先于子顺序处理, // 因此计算子部门时可复用父部门的 Chain。纯函数,不触碰 IO。 func FlatDeptInfo(depts []*feishu.Department, baseDN string) map[string]*FlatDept { byID := make(map[string]*feishu.Department, len(depts)) for _, d := range depts { byID[d.ID] = d } ordered := orderByDepth(depts, byID) root := GroupRootDN(baseDN) out := make(map[string]*FlatDept, len(depts)) used := make(map[string]string, len(depts)) // chain -> 占用它的 dept_id for _, d := range ordered { prefix, parentLevel := "", -1 if p, ok := out[d.ParentID]; ok { prefix = p.Chain parentLevel = p.Level } level := parentLevel + 1 // 单个部门的拼音段;拼音为空时回落部门 ID(cnName 保证非空)。 seg := cnName(d.Name, d.ID) chain := seg if prefix != "" { chain = prefix + "." + seg } // 重名消歧:同一层出现相同拼音(如两个「开发组」)会让 Chain 撞车, // 组名必须唯一,因此在链尾追加部门 ID 片段。 if other, taken := used[chain]; taken && other != d.ID { chain += "." + d.ID } used[chain] = d.ID out[d.ID] = &FlatDept{ DeptID: d.ID, Chain: chain, DN: "cn=" + chain + "," + root, Level: level, } } return out }