package projectinstance import ( "fmt" "chat-agent-system/internal/workflowdef" ) type DutyMode string const ( DutyModeNone DutyMode = "none" DutyModePartial DutyMode = "partial" DutyModeFull DutyMode = "full" issueDutyPartialAssignment = "DUTY_PARTIAL_ASSIGNMENT" issueDutyAssignmentRequired = "DUTY_ASSIGNMENT_REQUIRED" issueNoGovernedDocument = "NO_GOVERNED_DOCUMENT" issueDutyAssigneeNotActive = "DUTY_ASSIGNEE_NOT_ACTIVE_MEMBER" issueSeparationOfDutyViolation = "SEPARATION_OF_DUTY_VIOLATION" issueUnknownDutyPolicy = "UNKNOWN_DUTY_POLICY" issueAuthorOnlyAuthorRequired = "AUTHOR_ONLY_AUTHOR_REQUIRED" issueAuthorOnlyDutyConflict = "AUTHOR_ONLY_DUTY_CONFLICT" ) // ClassifyDocDuty 供运行时/门禁复用,禁止平行再写一套三职判定。 // 仅按三职 ID 完整度分类;治理调用应改用 ResolveDocTaskDuty。 func ClassifyDocDuty(d DocDuty) DutyMode { return classifyDocDuty(d) } func classifyDocDuty(d DocDuty) DutyMode { ids := []int64{d.AuthorUserID, d.ReviewerUserID, d.ApproverUserID} assigned := 0 for _, id := range ids { if id < 0 { return DutyModePartial } if id > 0 { assigned++ } } switch assigned { case 0: return DutyModeNone case 3: return DutyModeFull default: return DutyModePartial } } // ResolveDocTaskDuty 按逐文档 duty_policy 解析完整度、有效三职与是否兼任。 // author_only 的规范存储可只含 author;有效三职派生为 A/A/A。 func ResolveDocTaskDuty(dt DocTaskPlan) (mode DutyMode, effective DocDuty, isAuthorOnly bool) { if !dt.Enabled { return DutyModeNone, DocDuty{}, false } switch dt.DutyPolicy { case "", DutyPolicySeparated: return classifyDocDuty(dt.Duties), dt.Duties, false case DutyPolicyAuthorOnly: if dt.Duties.AuthorUserID <= 0 { return DutyModePartial, DocDuty{}, true } a := dt.Duties.AuthorUserID return DutyModeFull, DocDuty{AuthorUserID: a, ReviewerUserID: a, ApproverUserID: a}, true default: return DutyModePartial, DocDuty{}, false } } func docTaskRequiresDuty(td workflowdef.DocTaskDef) bool { return td.Required || td.Blocking || !td.IsCreatorConfigurable() } func normalizeDocTaskDuties(dt DocTaskPlan) DocTaskPlan { if !dt.Enabled { dt.Duties = DocDuty{} dt.DutyPolicy = "" return dt } if dt.DutyPolicy == DutyPolicyAuthorOnly { a := dt.Duties.AuthorUserID if a > 0 { if dt.Duties.ReviewerUserID == a { dt.Duties.ReviewerUserID = 0 } if dt.Duties.ApproverUserID == a { dt.Duties.ApproverUserID = 0 } } return dt } if classifyDocDuty(dt.Duties) == DutyModeNone { dt.Duties = DocDuty{} } return dt } func normalizeWorkflowPlanDuties(plan WorkflowPlan) WorkflowPlan { for i := range plan.Stages { for j := range plan.Stages[i].DocTasks { plan.Stages[i].DocTasks[j] = normalizeDocTaskDuties(plan.Stages[i].DocTasks[j]) } } return plan } func validateDocTaskDuties(dt DocTaskPlan, td workflowdef.DocTaskDef) []workflowdef.ValidationIssue { if !dt.Enabled { return nil } field := dt.TaskKey + ".duties" switch dt.DutyPolicy { case "", DutyPolicySeparated: // fall through to separated rules below case DutyPolicyAuthorOnly: if dt.Duties.AuthorUserID <= 0 { return []workflowdef.ValidationIssue{{ Code: issueAuthorOnlyAuthorRequired, Field: field, Message: fmt.Sprintf("文档「%s」兼任模式必须指定编制人", dutyTaskLabel(dt)), }} } a := dt.Duties.AuthorUserID if (dt.Duties.ReviewerUserID > 0 && dt.Duties.ReviewerUserID != a) || (dt.Duties.ApproverUserID > 0 && dt.Duties.ApproverUserID != a) { return []workflowdef.ValidationIssue{{ Code: issueAuthorOnlyDutyConflict, Field: field, Message: fmt.Sprintf("文档「%s」兼任模式不能指定与编制人不同的审核人或批准人", dutyTaskLabel(dt)), }} } return nil default: return []workflowdef.ValidationIssue{{ Code: issueUnknownDutyPolicy, Field: dt.TaskKey + ".duty_policy", Message: fmt.Sprintf("文档「%s」的职责模式无效", dutyTaskLabel(dt)), }} } mode := classifyDocDuty(dt.Duties) switch mode { case DutyModePartial: return []workflowdef.ValidationIssue{{ Code: issueDutyPartialAssignment, Field: field, Message: fmt.Sprintf("文档「%s」不能只指派部分职责", dutyTaskLabel(dt)), }} case DutyModeNone: if docTaskRequiresDuty(td) { return []workflowdef.ValidationIssue{{ Code: issueDutyAssignmentRequired, Field: field, Message: fmt.Sprintf("文档「%s」必须完整指派编制、审核、批准", dutyTaskLabel(dt)), }} } return nil default: if dt.Duties.AuthorUserID == dt.Duties.ReviewerUserID || dt.Duties.AuthorUserID == dt.Duties.ApproverUserID || dt.Duties.ReviewerUserID == dt.Duties.ApproverUserID { return []workflowdef.ValidationIssue{{ Code: issueSeparationOfDutyViolation, Field: dt.TaskKey, Message: "作者、审核人、批准人必须分离", }} } return nil } } func countFullDutyDocs(plan WorkflowPlan) int { n := 0 for _, st := range plan.Stages { if !st.Enabled { continue } for _, dt := range st.DocTasks { if !dt.Enabled { continue } mode, _, _ := ResolveDocTaskDuty(dt) if mode == DutyModeFull { n++ } } } return n } func validateActiveMemberDuties(plan WorkflowPlan, active map[int64]struct{}) []workflowdef.ValidationIssue { if active == nil { return nil } issues := make([]workflowdef.ValidationIssue, 0) for _, st := range plan.Stages { if !st.Enabled { continue } for _, dt := range st.DocTasks { if !dt.Enabled { continue } mode, effective, _ := ResolveDocTaskDuty(dt) if mode != DutyModeFull { continue } for _, item := range []struct { id int64 role string label string }{ {effective.AuthorUserID, "author", "编制"}, {effective.ReviewerUserID, "reviewer", "审核"}, {effective.ApproverUserID, "approver", "批准"}, } { if _, ok := active[item.id]; ok { continue } issues = append(issues, workflowdef.ValidationIssue{ Code: issueDutyAssigneeNotActive, Field: dt.TaskKey + "." + item.role, Message: fmt.Sprintf("文档「%s」的%s负责人不是在任项目成员", dutyTaskLabel(dt), item.label), }) } } } return issues } func dutyTaskLabel(dt DocTaskPlan) string { if dt.TaskTitle != "" { return dt.TaskTitle } return dt.TaskKey }