package docduty import ( "fmt" "strings" ) const ( DutyNone = "" DutyAuthor = "author" DutyReviewer = "reviewer" DutyApprover = "approver" StatusActive = "active" ) // Holder 某一文档职责的用户。 type Holder struct { UserID int64 Name string } // Binding 一份受控文档应采用的编制/审核/批准职责。 type Binding struct { Author Holder Reviewer Holder Approver Holder } // Member 项目成员快照(访问角色与文档职责分离)。 type Member struct { UserID int64 Name string Status string ProjectRole string DocumentDuty string } // RequestDuties 任务请求里客户端声称的职责;仅当该用户是在任成员时才可采用。 type RequestDuties struct { AuthorID int64 AuthorName string ReviewerID int64 ReviewerName string ApproverID int64 ApproverName string } // NormalizeDocumentDuty 校验 document_duty。空串表示无文档职责。 func NormalizeDocumentDuty(raw string) (string, error) { duty := strings.TrimSpace(strings.ToLower(raw)) switch duty { case DutyNone, DutyAuthor, DutyReviewer, DutyApprover: return duty, nil default: return "", fmt.Errorf("docduty: invalid document_duty %q", raw) } } // ActiveMemberIDs 在任成员(含创建者)。 func ActiveMemberIDs(members []Member, creator Holder) map[int64]struct{} { ids := make(map[int64]struct{}, len(members)+1) if creator.UserID > 0 { ids[creator.UserID] = struct{}{} } for _, m := range members { if m.UserID > 0 && (m.Status == "" || m.Status == StatusActive) { ids[m.UserID] = struct{}{} } } return ids } func isActiveMember(m Member) bool { return m.UserID > 0 && (m.Status == "" || m.Status == StatusActive) } func dutyHolder(members []Member, duty string) Holder { var found Holder for _, m := range members { if !isActiveMember(m) || m.DocumentDuty != duty { continue } found = Holder{UserID: m.UserID, Name: strings.TrimSpace(m.Name)} } return found } func pickMember(reqID int64, reqName string, memberIDs map[int64]struct{}, members []Member) Holder { if reqID <= 0 { return Holder{} } if _, ok := memberIDs[reqID]; !ok { return Holder{} } name := strings.TrimSpace(reqName) for _, m := range members { if m.UserID == reqID && isActiveMember(m) && strings.TrimSpace(m.Name) != "" { name = strings.TrimSpace(m.Name) break } } return Holder{UserID: reqID, Name: name} } // Resolve 以项目 document_duty 为权威。禁止回落到硬编码 2/3。 // author:指定编制人 → 创建者 → 请求 id(须为成员)。 // reviewer/approver:指定职责 → 请求 id(须为成员)→ 空。 func Resolve(members []Member, creator Holder, req RequestDuties) Binding { memberIDs := ActiveMemberIDs(members, creator) out := Binding{} if h := dutyHolder(members, DutyAuthor); h.UserID > 0 { out.Author = h } else if creator.UserID > 0 { out.Author = creator } else { out.Author = pickMember(req.AuthorID, req.AuthorName, memberIDs, members) } if h := dutyHolder(members, DutyReviewer); h.UserID > 0 { out.Reviewer = h } else { out.Reviewer = pickMember(req.ReviewerID, req.ReviewerName, memberIDs, members) } if h := dutyHolder(members, DutyApprover); h.UserID > 0 { out.Approver = h } else { out.Approver = pickMember(req.ApproverID, req.ApproverName, memberIDs, members) } return out } // DutyMissing 当前职责缺失或指向非成员(含历史硬编码占位 id)。 func DutyMissing(currentID int64, memberIDs map[int64]struct{}) bool { if currentID <= 0 { return true } _, ok := memberIDs[currentID] return !ok } func healSlot(current, assigned Holder, memberIDs map[int64]struct{}, clearInactive bool) (Holder, bool) { if assigned.UserID > 0 { if current.UserID == assigned.UserID && current.Name == assigned.Name { return current, false } return assigned, true } if current.UserID <= 0 { return current, false } if _, ok := memberIDs[current.UserID]; ok { return current, false } if !clearInactive { return current, false } return Holder{}, true } // HealBinding 以项目 document_duty 为权威。assigned 非零则对齐(错 ID 或同 ID 旧名称)。 // assigned 为空时:审核/批准仅清空已退出或非成员的失效 holder;在任成员保留以兼容未初始化 duty 的旧项目。 // 编制槽位即使 assigned 为空也不清空,避免无意去掉 owner/author。 func HealBinding(current, assigned Binding, memberIDs map[int64]struct{}) (Binding, bool) { out := current changed := false if next, ok := healSlot(current.Author, assigned.Author, memberIDs, false); ok { out.Author = next changed = true } if next, ok := healSlot(current.Reviewer, assigned.Reviewer, memberIDs, true); ok { out.Reviewer = next changed = true } if next, ok := healSlot(current.Approver, assigned.Approver, memberIDs, true); ok { out.Approver = next changed = true } return out, changed }