From 79797cab7cb63d7c39498d6af4e732a7f2cc1cdf Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 13 Sep 2026 17:02:23 +0000 Subject: [PATCH] feat(docgov): project document summary returns authoritative duties ListWorkflowDocumentsByProject now projects author/reviewer/approver id+name so clients cannot invent the current user as author. Co-authored-by: Cursor --- internal/docgov/project_docs.go | 19 ++++++++++++- internal/docgov/project_docs_test.go | 41 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/internal/docgov/project_docs.go b/internal/docgov/project_docs.go index aca0a205..b27810ce 100644 --- a/internal/docgov/project_docs.go +++ b/internal/docgov/project_docs.go @@ -27,6 +27,12 @@ type WorkflowDocumentSummary struct { LeaseHolderID string `json:"lease_holder_id,omitempty"` LeaseExpiresAt *time.Time `json:"lease_expires_at,omitempty"` LeaseHeartbeatAt *time.Time `json:"lease_heartbeat_at,omitempty"` + AuthorID int64 `json:"author_id"` + AuthorName string `json:"author_name"` + ReviewerID int64 `json:"reviewer_id"` + ReviewerName string `json:"reviewer_name"` + ApproverID int64 `json:"approver_id"` + ApproverName string `json:"approver_name"` } // DocumentContent 受控文档正文读取结果。 @@ -49,7 +55,10 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p SELECT doc_id, project_id, task_key, title, current_state, is_template_only, COALESCE(content_sha256, ''), COALESCE(work_dir, ''), version, updated_at, COALESCE(lease_holder_type, ''), COALESCE(lease_holder_id, ''), - lease_expires_at, lease_heartbeat_at + 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, '') FROM document_records WHERE project_id = $1 AND task_key <> '' ORDER BY task_key ASC, doc_id ASC @@ -67,6 +76,8 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p &item.DocID, &item.ProjectID, &item.TaskKey, &item.Title, &state, &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, ); err != nil { return nil, err } @@ -98,6 +109,12 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p LeaseHolderID: doc.LeaseHolderID, LeaseExpiresAt: doc.LeaseExpiresAt, LeaseHeartbeatAt: doc.LeaseHeartbeatAt, + AuthorID: doc.AuthorID, + AuthorName: doc.AuthorName, + ReviewerID: doc.ReviewerID, + ReviewerName: doc.ReviewerName, + ApproverID: doc.ApproverID, + ApproverName: doc.ApproverName, }) } } diff --git a/internal/docgov/project_docs_test.go b/internal/docgov/project_docs_test.go index 7d521de4..560559d7 100644 --- a/internal/docgov/project_docs_test.go +++ b/internal/docgov/project_docs_test.go @@ -38,3 +38,44 @@ func TestListWorkflowDocumentsIncludesActiveLease(t *testing.T) { t.Fatalf("lease timestamps missing: %+v", docs[0]) } } + +func TestListWorkflowDocumentsIncludesAuthoritativeDuties(t *testing.T) { + engine := docgov.NewGovernanceEngine(nil) + ctx := context.Background() + const ( + docID = "doc_proj143_intake" + authorID = int64(9) + reviewerID = int64(2) + approverID = int64(3) + ) + if _, err := engine.CreateOrGetDocument( + ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1", "", + authorID, "编制人A", reviewerID, "审核人B", approverID, "批准人C", + ); err != nil { + t.Fatal(err) + } + + docs, err := engine.ListWorkflowDocumentsByProject(ctx, 143) + if err != nil { + t.Fatal(err) + } + if len(docs) != 1 { + t.Fatalf("documents=%d", len(docs)) + } + got := docs[0] + if got.AuthorID != authorID || got.AuthorName != "编制人A" { + t.Fatalf("author not projected: id=%d name=%q", got.AuthorID, got.AuthorName) + } + if got.ReviewerID != reviewerID || got.ReviewerName != "审核人B" { + t.Fatalf("reviewer not projected: id=%d name=%q", got.ReviewerID, got.ReviewerName) + } + if got.ApproverID != approverID || got.ApproverName != "批准人C" { + t.Fatalf("approver not projected: id=%d name=%q", got.ApproverID, got.ApproverName) + } + if got.AuthorID == reviewerID { + t.Fatalf("reviewer B must not become author: %+v", got) + } + if got.AuthorID == 99 { + t.Fatalf("stranger must not become author: %+v", got) + } +}