Add ResolveDocTaskDuty, require the system policy on save/preview, lock the policy row during activation, and derive A/A/A assignments. Co-authored-by: Cursor <cursoragent@cursor.com>
275 lines
8.0 KiB
Go
275 lines
8.0 KiB
Go
package projectinstance
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"chat-agent-system/internal/workflowdef"
|
|
)
|
|
|
|
func validatePlan(plan WorkflowPlan, version *workflowdef.Version) workflowdef.ValidationResult {
|
|
issues := make([]workflowdef.ValidationIssue, 0)
|
|
if version == nil {
|
|
return workflowdef.ValidationResult{
|
|
Valid: false,
|
|
Issues: []workflowdef.ValidationIssue{{
|
|
Code: "WORKFLOW_VERSION_NOT_FOUND", Message: "工作流版本不存在或未发布",
|
|
}},
|
|
}
|
|
}
|
|
if version.Status != workflowdef.StatusPublished {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "WORKFLOW_NOT_PUBLISHED", Message: "只能绑定已发布的工作流版本",
|
|
})
|
|
}
|
|
if !workflowdef.IsOfficialWorkflowKey(plan.WorkflowDefinitionKey) {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "WORKFLOW_NOT_SELECTABLE", Field: "workflow_definition_key",
|
|
Message: "仅允许选择正式工作流(医疗三或软件自举/交付)",
|
|
})
|
|
}
|
|
if plan.WorkflowDefinitionKey != version.WorkflowKey {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "WORKFLOW_KEY_MISMATCH", Field: "workflow_definition_key",
|
|
Message: fmt.Sprintf("规划 key %s 与版本 %s 不一致", plan.WorkflowDefinitionKey, version.WorkflowKey),
|
|
})
|
|
}
|
|
if plan.WorkflowVersion != version.Version {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "WORKFLOW_VERSION_MISMATCH", Field: "workflow_version",
|
|
Message: fmt.Sprintf("规划版本 %d 与已发布版本 %d 不一致", plan.WorkflowVersion, version.Version),
|
|
})
|
|
}
|
|
|
|
stageDefMap := make(map[string]workflowdef.StageDef, len(version.Definition.Stages))
|
|
for _, st := range version.Definition.Stages {
|
|
stageDefMap[st.Key] = st
|
|
}
|
|
seenStage := map[string]bool{}
|
|
for _, st := range plan.Stages {
|
|
if seenStage[st.StageKey] {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "DUPLICATE_STAGE", Field: st.StageKey, Message: "阶段 key 重复",
|
|
})
|
|
continue
|
|
}
|
|
seenStage[st.StageKey] = true
|
|
def, ok := stageDefMap[st.StageKey]
|
|
if !ok {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "UNKNOWN_STAGE", Field: st.StageKey, Message: "阶段不在工作流定义中",
|
|
})
|
|
continue
|
|
}
|
|
if !def.Optional && !st.Enabled {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "REQUIRED_STAGE_DISABLED", Field: st.StageKey, Message: "必需阶段不能关闭",
|
|
})
|
|
}
|
|
taskDefMap := make(map[string]workflowdef.DocTaskDef, len(def.DocTasks))
|
|
for _, dt := range def.DocTasks {
|
|
taskDefMap[dt.Key] = dt
|
|
}
|
|
seenTask := map[string]bool{}
|
|
for _, dt := range st.DocTasks {
|
|
if seenTask[dt.TaskKey] {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "DUPLICATE_DOC_TASK", Field: dt.TaskKey, Message: "文档任务 key 重复",
|
|
})
|
|
continue
|
|
}
|
|
seenTask[dt.TaskKey] = true
|
|
td, ok := taskDefMap[dt.TaskKey]
|
|
if !ok {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "UNKNOWN_DOC_TASK", Field: dt.TaskKey, Message: "文档任务不在阶段定义中",
|
|
})
|
|
continue
|
|
}
|
|
if !dt.Enabled && !td.IsCreatorConfigurable() {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "REQUIRED_DOC_DISABLED", Field: dt.TaskKey, Message: "该文档任务不可由创建者关闭",
|
|
})
|
|
}
|
|
if dt.Enabled {
|
|
issues = append(issues, validateDocTaskDuties(dt, td)...)
|
|
}
|
|
}
|
|
}
|
|
|
|
enabledStages := 0
|
|
for _, st := range plan.Stages {
|
|
if st.Enabled {
|
|
enabledStages++
|
|
}
|
|
}
|
|
if enabledStages == 0 {
|
|
issues = append(issues, workflowdef.ValidationIssue{
|
|
Code: "NO_ENABLED_STAGE", Message: "至少启用一个阶段",
|
|
})
|
|
}
|
|
|
|
return workflowdef.ValidationResult{Valid: len(issues) == 0, Issues: issues}
|
|
}
|
|
|
|
func validateActivationPlan(plan WorkflowPlan, version *workflowdef.Version) workflowdef.ValidationResult {
|
|
base := validatePlan(plan, version)
|
|
if !base.Valid {
|
|
return base
|
|
}
|
|
if countEnabledDocTasks(plan) == 0 {
|
|
base.Valid = false
|
|
base.Issues = append(base.Issues, workflowdef.ValidationIssue{
|
|
Code: "EMPTY_WORKFLOW", Message: "至少启用一个文档任务才能激活工作流",
|
|
})
|
|
}
|
|
if countFullDutyDocs(plan) == 0 {
|
|
base.Valid = false
|
|
base.Issues = append(base.Issues, workflowdef.ValidationIssue{
|
|
Code: issueNoGovernedDocument,
|
|
Message: "至少一份文档必须纳入人工审批才能激活工作流",
|
|
})
|
|
}
|
|
return base
|
|
}
|
|
|
|
func countEnabledDocTasks(plan WorkflowPlan) int {
|
|
n := 0
|
|
for _, st := range plan.Stages {
|
|
if !st.Enabled {
|
|
continue
|
|
}
|
|
for _, dt := range st.DocTasks {
|
|
if dt.Enabled {
|
|
n++
|
|
}
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func validateDuties(d DocDuty, taskKey string) []workflowdef.ValidationIssue {
|
|
return validateDocTaskDuties(DocTaskPlan{TaskKey: taskKey, Enabled: true, Duties: d}, workflowdef.DocTaskDef{Key: taskKey, Required: true})
|
|
}
|
|
|
|
func buildDefaultPlan(version *workflowdef.Version) WorkflowPlan {
|
|
plan := WorkflowPlan{
|
|
WorkflowDefinitionKey: version.WorkflowKey,
|
|
WorkflowVersion: version.Version,
|
|
WorkflowVersionID: version.ID,
|
|
Stages: make([]StagePlan, 0, len(version.Definition.Stages)),
|
|
}
|
|
for _, st := range version.Definition.Stages {
|
|
sp := StagePlan{
|
|
StageKey: st.Key,
|
|
StageName: st.Name,
|
|
Enabled: !st.Optional,
|
|
DocTasks: make([]DocTaskPlan, 0, len(st.DocTasks)),
|
|
}
|
|
for _, dt := range st.DocTasks {
|
|
sp.DocTasks = append(sp.DocTasks, DocTaskPlan{
|
|
TaskKey: dt.Key,
|
|
TaskTitle: dt.Title,
|
|
Enabled: dt.DefaultEnabled,
|
|
ApprovalConfigurable: !docTaskRequiresDuty(dt),
|
|
})
|
|
}
|
|
plan.Stages = append(plan.Stages, sp)
|
|
}
|
|
return plan
|
|
}
|
|
|
|
// applyDefinitionLabels 使用已发布定义覆盖展示名称,避免客户端把自定义文本写入冻结快照。
|
|
func applyDefinitionLabels(plan *WorkflowPlan, version *workflowdef.Version) {
|
|
if plan == nil || version == nil {
|
|
return
|
|
}
|
|
stageDefs := make(map[string]workflowdef.StageDef, len(version.Definition.Stages))
|
|
for _, stage := range version.Definition.Stages {
|
|
stageDefs[stage.Key] = stage
|
|
}
|
|
for stageIdx := range plan.Stages {
|
|
stage := &plan.Stages[stageIdx]
|
|
stageDef, ok := stageDefs[stage.StageKey]
|
|
if !ok {
|
|
continue
|
|
}
|
|
stage.StageName = stageDef.Name
|
|
taskDefs := make(map[string]workflowdef.DocTaskDef, len(stageDef.DocTasks))
|
|
for _, task := range stageDef.DocTasks {
|
|
taskDefs[task.Key] = task
|
|
}
|
|
for taskIdx := range stage.DocTasks {
|
|
task := &stage.DocTasks[taskIdx]
|
|
if taskDef, exists := taskDefs[task.TaskKey]; exists {
|
|
task.TaskTitle = taskDef.Title
|
|
task.ApprovalConfigurable = !docTaskRequiresDuty(taskDef)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func previewSummary(plan WorkflowPlan, version *workflowdef.Version) PreviewSummary {
|
|
sum := PreviewSummary{
|
|
WorkflowVersion: plan.WorkflowVersion,
|
|
}
|
|
if version != nil {
|
|
sum.WorkflowName = version.WorkflowName
|
|
if sum.WorkflowName == "" {
|
|
sum.WorkflowName = version.WorkflowKey
|
|
}
|
|
}
|
|
for _, st := range plan.Stages {
|
|
if !st.Enabled {
|
|
continue
|
|
}
|
|
sum.EnabledStages++
|
|
if sum.FirstStageKey == "" {
|
|
sum.FirstStageKey = st.StageKey
|
|
if version != nil {
|
|
for _, sd := range version.Definition.Stages {
|
|
if sd.Key == st.StageKey {
|
|
sum.FirstStageName = sd.Name
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, dt := range st.DocTasks {
|
|
if !dt.Enabled {
|
|
continue
|
|
}
|
|
sum.EnabledDocTasks++
|
|
mode, effective, isAuthorOnly := ResolveDocTaskDuty(dt)
|
|
switch mode {
|
|
case DutyModeFull:
|
|
sum.GovernedDocTasks++
|
|
if isAuthorOnly {
|
|
sum.AuthorOnlyDocTasks++
|
|
sum.AuthorOnlyDocuments = append(sum.AuthorOnlyDocuments, PreviewDocRef{
|
|
TaskKey: dt.TaskKey,
|
|
Title: dutyTaskLabel(dt),
|
|
AuthorUserID: effective.AuthorUserID,
|
|
})
|
|
} else {
|
|
if effective.AuthorUserID > 0 {
|
|
sum.AssignedDuties++
|
|
}
|
|
if effective.ReviewerUserID > 0 {
|
|
sum.AssignedDuties++
|
|
}
|
|
if effective.ApproverUserID > 0 {
|
|
sum.AssignedDuties++
|
|
}
|
|
}
|
|
case DutyModeNone:
|
|
sum.UnmanagedDocTasks++
|
|
sum.UnmanagedDocuments = append(sum.UnmanagedDocuments, PreviewDocRef{
|
|
TaskKey: dt.TaskKey,
|
|
Title: dutyTaskLabel(dt),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return sum
|
|
}
|