package docgov import ( "context" "encoding/json" "log/slog" ) // GetDocument 获取文档实体 (优先查内存,未命中查 DB) func (e *GovernanceEngine) GetDocument(ctx context.Context, docID string) (*DocumentRecord, error) { g := e.acquireDocLock(docID) defer g.Release() if doc, exists := e.memDocsGet(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) { g := e.acquireDocLock(docID) defer g.Release() if e.memAuditsEmpty(docID) && 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.memAuditsAppend(docID, &a) } } if err := rows.Err(); err != nil { slog.Warn("docgov_audit_scan_incomplete", "error", err) } } } return e.memAuditsCopy(docID), nil } // ListComments 列出文档的所有批注 func (e *GovernanceEngine) ListComments(ctx context.Context, docID string) ([]*DocumentComment, error) { g := e.acquireDocLock(docID) defer g.Release() if e.memCommsEmpty(docID) && 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.memCommsAppend(docID, &c, false) } } if err := rows.Err(); err != nil { slog.Warn("docgov_comments_scan_incomplete", "error", err) } } } return e.memCommsCopy(docID), 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, ''), COALESCE(review_snapshot_id, 0) 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, &doc.ReviewSnapshotID, ) 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.memDocsPut(docID, &doc) return &doc, nil }