162 lines
4.0 KiB
Go
162 lines
4.0 KiB
Go
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"
|
|
)
|
|
|
|
// ClassifyDocDuty 供运行时/门禁复用,禁止平行再写一套三职判定。
|
|
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
|
|
}
|
|
}
|
|
|
|
func docTaskRequiresDuty(td workflowdef.DocTaskDef) bool {
|
|
return td.Required || td.Blocking || !td.IsCreatorConfigurable()
|
|
}
|
|
|
|
func normalizeDocTaskDuties(dt DocTaskPlan) DocTaskPlan {
|
|
if !dt.Enabled || 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
|
|
}
|
|
mode := classifyDocDuty(dt.Duties)
|
|
field := dt.TaskKey + ".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 && classifyDocDuty(dt.Duties) == 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 || classifyDocDuty(dt.Duties) != DutyModeFull {
|
|
continue
|
|
}
|
|
for _, item := range []struct {
|
|
id int64
|
|
role string
|
|
label string
|
|
}{
|
|
{dt.Duties.AuthorUserID, "author", "编制"},
|
|
{dt.Duties.ReviewerUserID, "reviewer", "审核"},
|
|
{dt.Duties.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
|
|
}
|