Files
WeiCHEN 39e115b4d3 refactor(docgov): split engine.go into use-case files
Move claim/document/persist/proposal/read/review_flow into ≤800-line
siblings without behavior change; short tests green.
2026-09-23 09:54:19 +08:00

237 lines
6.6 KiB
Go

package docgov
import (
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"strings"
"time"
)
// CreateOrGetDocument 创建或获取受控文档记录
func (e *GovernanceEngine) CreateOrGetDocument(
ctx context.Context,
docID string,
projectID int64,
taskKey string,
title string,
stage string,
workDir string,
authorID int64, authorName string,
reviewerID int64, reviewerName string,
approverID int64, approverName string,
) (*DocumentRecord, error) {
e.mu.Lock()
defer e.mu.Unlock()
now := time.Now()
// 先查内存
if doc, exists := e.memDocs[docID]; exists {
return doc, nil
}
// 内存没有查 DB
if e.pool != nil {
if docFromDB, err := e.loadDocumentFromDBLocked(ctx, docID); err == nil && docFromDB != nil {
return docFromDB, nil
}
}
doc := &DocumentRecord{
DocID: docID,
ProjectID: projectID,
TaskKey: taskKey,
Title: title,
Stage: stage,
Version: 1,
CurrentState: StateDrafting,
IsLocked: false,
IsTemplateOnly: true,
WorkDir: workDir,
AuthorID: authorID,
AuthorName: authorName,
ReviewerID: reviewerID,
ReviewerName: reviewerName,
ApproverID: approverID,
ApproverName: approverName,
Config: DefaultDocumentConfig(),
CreatedAt: now,
UpdatedAt: now,
}
// 若有 DB 连接,写 DB 并同步
if e.pool != nil {
if projectID > 0 {
_, _ = e.pool.Exec(ctx, `
INSERT INTO projects (id, code, name)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
`, projectID, fmt.Sprintf("PROJ_%d", projectID), "受控工程研发项目")
}
ensureUser := func(uid int64, uname, urole string) {
if uid <= 0 {
return
}
if _, err := e.pool.Exec(ctx, `
INSERT INTO users (id, account_id, name, role)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO NOTHING
`, uid, fmt.Sprintf("acc_%d", uid), uname, urole); err != nil {
log.Printf("[docgov] ensureUser id=%d role=%s failed: %v", uid, urole, err)
}
}
ensureUser(doc.AuthorID, doc.AuthorName, "engineer")
ensureUser(doc.ReviewerID, doc.ReviewerName, "regulatory")
ensureUser(doc.ApproverID, doc.ApproverName, "director")
cfgJSON, _ := json.Marshal(doc.Config)
query := `
INSERT INTO document_records (
doc_id, project_id, task_key, title, stage, version,
current_state, is_locked, is_template_only, work_dir,
author_id, author_name, reviewer_id, reviewer_name, approver_id, approver_name,
config, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
ON CONFLICT (doc_id) DO UPDATE SET
title = EXCLUDED.title,
stage = EXCLUDED.stage,
updated_at = EXCLUDED.updated_at
RETURNING id, current_state, is_locked, is_template_only, author_claimed, reviewer_claimed, approver_claimed;
`
var id int64
var currState string
var isLocked, isTmpl, authClaim, revClaim, appClaim bool
err := e.pool.QueryRow(ctx, query,
doc.DocID, doc.ProjectID, doc.TaskKey, doc.Title, doc.Stage, doc.Version,
string(doc.CurrentState), doc.IsLocked, doc.IsTemplateOnly, doc.WorkDir,
nullIfZero(doc.AuthorID), doc.AuthorName, nullIfZero(doc.ReviewerID), doc.ReviewerName,
nullIfZero(doc.ApproverID), doc.ApproverName,
cfgJSON, doc.CreatedAt, doc.UpdatedAt,
).Scan(&id, &currState, &isLocked, &isTmpl, &authClaim, &revClaim, &appClaim)
if err != nil {
return nil, fmt.Errorf("persist document ledger: %w", err)
}
doc.ID = id
doc.CurrentState = DocumentState(currState)
doc.IsLocked = isLocked
doc.IsTemplateOnly = isTmpl
doc.AuthorClaimed = authClaim
doc.ReviewerClaimed = revClaim
doc.ApproverClaimed = appClaim
}
e.memDocs[docID] = doc
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 {
e.mu.Lock()
defer e.mu.Unlock()
doc, err := e.ensureDocumentLocked(ctx, docID)
if err != nil {
return err
}
doc.ContentSHA256 = contentSHA256
doc.IsTemplateOnly = isTemplateOnly
doc.UpdatedAt = time.Now()
if e.pool != nil {
tag, execErr := e.pool.Exec(ctx, `
UPDATE document_records
SET content_sha256 = $1, is_template_only = $2, work_dir = $3, updated_at = $4
WHERE doc_id = $5
`, doc.ContentSHA256, doc.IsTemplateOnly, doc.WorkDir, doc.UpdatedAt, docID)
if execErr != nil {
return fmt.Errorf("docgov: mark content updated: %w", execErr)
}
if tag.RowsAffected() == 0 {
return ErrDocumentNotFound
}
}
return nil
}
func (e *GovernanceEngine) ensureDocumentLocked(ctx context.Context, docID string) (*DocumentRecord, error) {
if doc, exists := e.memDocs[docID]; exists {
return doc, nil
}
if e.pool != nil {
doc, err := e.loadDocumentFromDBLocked(ctx, docID)
if err == nil && doc != nil {
return doc, nil
}
}
return nil, ErrDocumentNotFound
}
// SetDocumentWorkDir 更新文档的工作区路径
func (e *GovernanceEngine) SetDocumentWorkDir(ctx context.Context, docID string, workDir string) error {
e.mu.Lock()
defer e.mu.Unlock()
doc, exists := e.memDocs[docID]
if !exists {
return ErrDocumentNotFound
}
doc.WorkDir = workDir
doc.UpdatedAt = time.Now()
if e.pool != nil {
if _, err := e.pool.Exec(ctx, `
UPDATE document_records
SET work_dir = $1, updated_at = $2
WHERE doc_id = $3
`, doc.WorkDir, doc.UpdatedAt, docID); err != nil {
slog.Error("docgov_workdir_persist_failed", "doc_id", docID, "error", err)
}
}
return nil
}