package projectinstance import ( "context" "fmt" "chat-agent-system/internal/workflowdef" ) // ReassignDocTaskDutiesInput 激活后受控改派单文档三职。 type ReassignDocTaskDutiesInput struct { TaskKey string `json:"task_key"` Duties DocDuty `json:"duties"` DutyPolicy string `json:"duty_policy,omitempty"` } // DocumentDutyProjector 将实例职责投影到 document_records(含 duty_locked 行)。 type DocumentDutyProjector interface { ProjectDocTaskDuties(ctx context.Context, projectID int64, taskKey string, duties DocDuty, dutyPolicy string, names DutyDisplayNames) error } // DutyDisplayNames 投影用显示名;空则保留原名或写占位。 type DutyDisplayNames struct { AuthorName string ReviewerName string ApproverName string } // DocTaskBusyChecker 改派前检查小队任务是否占用文档。 type DocTaskBusyChecker interface { DocTaskSquadBusy(ctx context.Context, projectID int64, taskKey string) (busy bool, reason string, err error) } // ReassignDocTaskDuties 在 live 实例上改派单个文档任务职责。 // 不放开规划冻结:不修改 lifecycle / selection,仅更新 snapshot 与 assignments。 func (s *Service) ReassignDocTaskDuties( ctx context.Context, projectID, userID int64, allowed bool, in ReassignDocTaskDutiesInput, ) (*ProjectSettings, error) { if !allowed { return nil, ErrForbiddenDutyReassign } if projectID <= 0 || in.TaskKey == "" { return nil, ErrInvalidPlan } if !s.Available() { return nil, ErrNotAvailable } live, err := s.store.GetLiveInstance(ctx, projectID) if err != nil { return nil, err } if live == nil { return nil, ErrInstanceNotFound } plan := cloneWorkflowPlan(live.ConfigSnapshot) dt := findDocTaskPlanMutable(&plan, in.TaskKey) if dt == nil || !dt.Enabled { return nil, ErrDocTaskNotFound } if in.DutyPolicy != "" { dt.DutyPolicy = in.DutyPolicy } dt.Duties = in.Duties *dt = normalizeDocTaskDuties(*dt) version, verErr := s.resolvePublishedVersion(ctx, plan) if verErr != nil { return nil, verErr } td := workflowdef.DocTaskDef{Key: dt.TaskKey, Required: true} if def := findDocTaskDefInVersion(version.Definition, dt.TaskKey); def != nil { td = *def } issues := validateDocTaskDuties(*dt, td) if len(issues) > 0 { return nil, newValidationError(workflowdef.ValidationResult{Valid: false, Issues: issues}) } plan = normalizeWorkflowPlanDuties(plan) if planUsesAuthorOnly(plan) { if _, err := s.requireAuthorOnlyAllowed(ctx); err != nil { return nil, err } } if err := s.checkActiveMembers(ctx, projectID, plan); err != nil { return nil, err } if s.busyChecker != nil { busy, reason, busyErr := s.busyChecker.DocTaskSquadBusy(ctx, projectID, in.TaskKey) if busyErr != nil { return nil, busyErr } if busy { if reason == "" { reason = fmt.Sprintf("文档任务 %s 小队执行中,完成后可更换负责人", in.TaskKey) } return nil, &DocTaskBusyError{TaskKey: in.TaskKey, Reason: reason} } } updated, err := s.store.UpdateLiveDocTaskDuties(ctx, projectID, in.TaskKey, plan, userID) if err != nil { return nil, err } updatedDT := findDocTaskPlan(updated.ConfigSnapshot, in.TaskKey) if updatedDT != nil && s.projector != nil { mode, effective, _ := ResolveDocTaskDuty(*updatedDT) if mode != DutyModeNone { _ = s.projector.ProjectDocTaskDuties(ctx, projectID, in.TaskKey, effective, updatedDT.DutyPolicy, DutyDisplayNames{}) } } return s.GetSettings(ctx, projectID, userID, true) } func (s *Service) SetDocumentDutyProjector(p DocumentDutyProjector) { if s == nil { return } s.projector = p } func (s *Service) SetDocTaskBusyChecker(c DocTaskBusyChecker) { if s == nil { return } s.busyChecker = c } func findDocTaskPlanMutable(plan *WorkflowPlan, taskKey string) *DocTaskPlan { if plan == nil { return nil } for i := range plan.Stages { for j := range plan.Stages[i].DocTasks { if plan.Stages[i].DocTasks[j].TaskKey == taskKey { return &plan.Stages[i].DocTasks[j] } } } return nil } func findDocTaskDefInVersion(def workflowdef.VersionDefinition, taskKey string) *workflowdef.DocTaskDef { for _, st := range def.Stages { for i := range st.DocTasks { if st.DocTasks[i].Key == taskKey { return &st.DocTasks[i] } } } return nil } func cloneWorkflowPlan(in WorkflowPlan) WorkflowPlan { out := in out.Stages = make([]StagePlan, len(in.Stages)) for i, st := range in.Stages { out.Stages[i] = st out.Stages[i].DocTasks = append([]DocTaskPlan(nil), st.DocTasks...) } return out }