diff --git a/internal/docgov/engine.go b/internal/docgov/engine.go index 9e650e44..7363b47b 100644 --- a/internal/docgov/engine.go +++ b/internal/docgov/engine.go @@ -218,6 +218,43 @@ func (e *GovernanceEngine) CreateOrGetDocument( return doc, nil } +// LockDocumentDuty 将文档职责钉住为实例治理结论;heal/revoke 跳过 duty_locked 行。 +// 不改写 author/reviewer/approver 列——那些在 CreateOrGetDocument 时已由调用方写入。 +func (e *GovernanceEngine) LockDocumentDuty(ctx context.Context, docID string, dutyPolicy string) error { + docID = strings.TrimSpace(docID) + if docID == "" { + return fmt.Errorf("docgov: empty doc_id") + } + dutyPolicy = strings.TrimSpace(dutyPolicy) + + e.mu.Lock() + defer e.mu.Unlock() + + doc, err := e.ensureDocumentLocked(ctx, docID) + if err != nil { + return err + } + now := time.Now() + doc.DutyLocked = true + doc.DutyPolicy = dutyPolicy + doc.UpdatedAt = now + + if e.pool != nil { + tag, execErr := e.pool.Exec(ctx, ` + UPDATE document_records + SET duty_locked = true, duty_policy = $2, updated_at = $3 + WHERE doc_id = $1 + `, docID, dutyPolicy, now) + if execErr != nil { + return fmt.Errorf("docgov: lock document duty: %w", execErr) + } + if tag.RowsAffected() == 0 { + return ErrDocumentNotFound + } + } + return nil +} + // MarkDocumentContentUpdated 当正文生成或更新时标记正文哈希及是否脱离模板状态。 // 未预加载时从 DB 补偿装入;已是目标哈希则幂等成功,保证 repair 重试也能同步记录。 func (e *GovernanceEngine) MarkDocumentContentUpdated(ctx context.Context, docID string, contentSHA256 string, isTemplateOnly bool) error { @@ -1043,7 +1080,8 @@ func (e *GovernanceEngine) loadDocumentFromDBLocked(ctx context.Context, docID s last_rejected_reason, last_rejected_by_role, last_rejected_by_name, last_rejected_at, approved_at, config, created_at, updated_at, COALESCE(lease_holder_type, ''), COALESCE(lease_holder_id, ''), COALESCE(lease_work_dir, ''), - lease_expires_at, lease_heartbeat_at + lease_expires_at, lease_heartbeat_at, + COALESCE(duty_locked, false), COALESCE(duty_policy, '') FROM document_records WHERE doc_id = $1 `, docID) @@ -1062,6 +1100,7 @@ func (e *GovernanceEngine) loadDocumentFromDBLocked(ctx context.Context, docID s &doc.LastRejectedReason, &doc.LastRejectedByRole, &doc.LastRejectedByName, &doc.LastRejectedAt, &doc.ApprovedAt, &cfgJSON, &doc.CreatedAt, &doc.UpdatedAt, &leaseType, &leaseID, &leaseWD, &doc.LeaseExpiresAt, &doc.LeaseHeartbeatAt, + &doc.DutyLocked, &doc.DutyPolicy, ) if err != nil { return nil, err diff --git a/internal/docgov/project_docs.go b/internal/docgov/project_docs.go index 30e2cb38..96294223 100644 --- a/internal/docgov/project_docs.go +++ b/internal/docgov/project_docs.go @@ -36,6 +36,7 @@ type WorkflowDocumentSummary struct { ReviewerName string `json:"reviewer_name"` ApproverID int64 `json:"approver_id"` ApproverName string `json:"approver_name"` + DutyPolicy string `json:"duty_policy,omitempty"` } // DocumentContent 受控文档正文读取结果。 @@ -67,7 +68,8 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p lease_expires_at, lease_heartbeat_at, COALESCE(author_id, 0), COALESCE(author_name, ''), COALESCE(reviewer_id, 0), COALESCE(reviewer_name, ''), - COALESCE(approver_id, 0), COALESCE(approver_name, '') + COALESCE(approver_id, 0), COALESCE(approver_name, ''), + COALESCE(duty_policy, '') FROM document_records WHERE project_id = $1 AND task_key <> '' ORDER BY task_key ASC, doc_id ASC @@ -86,7 +88,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p &item.IsTemplateOnly, &item.ContentSHA256, &item.WorkDir, &item.Version, &updatedAt, &item.LeaseHolderType, &item.LeaseHolderID, &item.LeaseExpiresAt, &item.LeaseHeartbeatAt, &item.AuthorID, &item.AuthorName, &item.ReviewerID, &item.ReviewerName, - &item.ApproverID, &item.ApproverName, + &item.ApproverID, &item.ApproverName, &item.DutyPolicy, ); err != nil { return nil, err } @@ -124,6 +126,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p ReviewerName: doc.ReviewerName, ApproverID: doc.ApproverID, ApproverName: doc.ApproverName, + DutyPolicy: doc.DutyPolicy, }) } } diff --git a/internal/docgov/types.go b/internal/docgov/types.go index 01ea18aa..5a1dd710 100644 --- a/internal/docgov/types.go +++ b/internal/docgov/types.go @@ -166,6 +166,10 @@ type DocumentRecord struct { ApproverID int64 `json:"approver_id"` ApproverName string `json:"approver_name"` + // DutyLocked 为 true 时,职责权威来自工作流实例规划,heal/revoke 不得覆盖。 + DutyLocked bool `json:"duty_locked"` + DutyPolicy string `json:"duty_policy,omitempty"` + // 认领状态 (Claiming State) AuthorClaimed bool `json:"author_claimed"` // 编制人是否已认领 (非模板状态查看/编辑后为 true) AuthorClaimedAt *time.Time `json:"author_claimed_at"`