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

166 lines
5.3 KiB
Go

package docgov
import (
"context"
"encoding/json"
"log/slog"
)
// GetDocument 获取文档实体 (优先查内存,未命中查 DB)
func (e *GovernanceEngine) GetDocument(ctx context.Context, docID string) (*DocumentRecord, error) {
e.mu.Lock()
defer e.mu.Unlock()
if doc, exists := e.memDocs[docID]; exists {
return doc, nil
}
if e.pool != nil {
if doc, err := e.loadDocumentFromDBLocked(ctx, docID); err == nil && doc != nil {
return doc, nil
}
}
return nil, ErrDocumentNotFound
}
// ListAuditLogs 获取文档审计日志
func (e *GovernanceEngine) ListAuditLogs(ctx context.Context, docID string) ([]*AuditLog, error) {
e.mu.Lock()
defer e.mu.Unlock()
if len(e.memAudits[docID]) == 0 && e.pool != nil {
rows, err := e.pool.Query(ctx, `
SELECT id, doc_id, action, actor_id, actor_name, actor_role, from_state, to_state, details, created_at
FROM document_audit_logs
WHERE doc_id = $1
ORDER BY created_at ASC
`, docID)
if err == nil {
defer rows.Close()
for rows.Next() {
var a AuditLog
var detJSON []byte
var role, fromSt, toSt string
var actID *int64
if err := rows.Scan(&a.ID, &a.DocID, &a.Action, &actID, &a.ActorName, &role, &fromSt, &toSt, &detJSON, &a.CreatedAt); err == nil {
if actID != nil {
a.ActorID = *actID
}
a.ActorRole = UserRole(role)
a.FromState = DocumentState(fromSt)
a.ToState = DocumentState(toSt)
if len(detJSON) > 0 {
_ = json.Unmarshal(detJSON, &a.Details)
}
e.memAudits[docID] = append(e.memAudits[docID], &a)
}
}
if err := rows.Err(); err != nil {
slog.Warn("docgov_audit_scan_incomplete", "error", err)
}
}
}
logs := e.memAudits[docID]
res := make([]*AuditLog, len(logs))
copy(res, logs)
return res, nil
}
// ListComments 列出文档的所有批注
func (e *GovernanceEngine) ListComments(ctx context.Context, docID string) ([]*DocumentComment, error) {
e.mu.Lock()
defer e.mu.Unlock()
if len(e.memComms[docID]) == 0 && e.pool != nil {
rows, err := e.pool.Query(ctx, `
SELECT id, doc_id, user_id, user_name, user_role, target_section, comment_type, content, status, created_at
FROM document_comments
WHERE doc_id = $1
ORDER BY created_at ASC
`, docID)
if err == nil {
defer rows.Close()
for rows.Next() {
var c DocumentComment
var uID *int64
var role, cType string
if err := rows.Scan(&c.ID, &c.DocID, &uID, &c.UserName, &role, &c.TargetSection, &cType, &c.Content, &c.Status, &c.CreatedAt); err == nil {
if uID != nil {
c.UserID = *uID
}
c.UserRole = UserRole(role)
c.CommentType = CommentType(cType)
e.memComms[docID] = append(e.memComms[docID], &c)
}
}
if err := rows.Err(); err != nil {
slog.Warn("docgov_comments_scan_incomplete", "error", err)
}
}
}
comms := e.memComms[docID]
res := make([]*DocumentComment, len(comms))
copy(res, comms)
return res, nil
}
func (e *GovernanceEngine) loadDocumentFromDBLocked(ctx context.Context, docID string) (*DocumentRecord, error) {
if e.pool == nil {
return nil, ErrDocumentNotFound
}
row := e.pool.QueryRow(ctx, `
SELECT id, doc_id, project_id, task_key, title, stage, version,
current_state, is_locked, is_template_only, content_sha256, work_dir,
author_id, author_name, reviewer_id, reviewer_name, approver_id, approver_name,
author_claimed, author_claimed_at, author_last_viewed_at, author_last_edited_at,
reviewer_claimed, reviewer_claimed_at, approver_claimed, approver_claimed_at,
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, COALESCE(lease_fencing_token, ''),
COALESCE(duty_locked, false), COALESCE(duty_policy, '')
FROM document_records
WHERE doc_id = $1
`, docID)
var doc DocumentRecord
var cfgJSON []byte
var currState string
var authorID, reviewerID, approverID *int64
var leaseType, leaseID, leaseWD string
err := row.Scan(
&doc.ID, &doc.DocID, &doc.ProjectID, &doc.TaskKey, &doc.Title, &doc.Stage, &doc.Version,
&currState, &doc.IsLocked, &doc.IsTemplateOnly, &doc.ContentSHA256, &doc.WorkDir,
&authorID, &doc.AuthorName, &reviewerID, &doc.ReviewerName, &approverID, &doc.ApproverName,
&doc.AuthorClaimed, &doc.AuthorClaimedAt, &doc.AuthorLastViewedAt, &doc.AuthorLastEditedAt,
&doc.ReviewerClaimed, &doc.ReviewerClaimedAt, &doc.ApproverClaimed, &doc.ApproverClaimedAt,
&doc.LastRejectedReason, &doc.LastRejectedByRole, &doc.LastRejectedByName, &doc.LastRejectedAt,
&doc.ApprovedAt, &cfgJSON, &doc.CreatedAt, &doc.UpdatedAt,
&leaseType, &leaseID, &leaseWD, &doc.LeaseExpiresAt, &doc.LeaseHeartbeatAt, &doc.LeaseFencingToken,
&doc.DutyLocked, &doc.DutyPolicy,
)
if err != nil {
return nil, err
}
doc.CurrentState = DocumentState(currState)
doc.LeaseHolderType = LeaseHolderType(leaseType)
doc.LeaseHolderID = leaseID
doc.LeaseWorkDir = leaseWD
if authorID != nil {
doc.AuthorID = *authorID
}
if reviewerID != nil {
doc.ReviewerID = *reviewerID
}
if approverID != nil {
doc.ApproverID = *approverID
}
if len(cfgJSON) > 0 {
_ = json.Unmarshal(cfgJSON, &doc.Config)
}
e.memDocs[docID] = &doc
return &doc, nil
}