fix(docgov): map zero duty ids to NULL on CreateOrGetDocument
Optional reviewer/approver duties leave UserID=0; writing 0 into document_records FK columns failed with SQLSTATE 23503. Align create path with docduty nullIfZero and surface ensureUser failures in logs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -13,6 +14,15 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// nullIfZero maps unset duty holders (0) to SQL NULL so nullable FK columns
|
||||
// (author_id/reviewer_id/approver_id → users.id) accept optional duties.
|
||||
func nullIfZero(id int64) any {
|
||||
if id <= 0 {
|
||||
return nil
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// ModificationCheckResult 智能体或人员发起修改前的认领与授权判定结果
|
||||
type ModificationCheckResult struct {
|
||||
AllowedDirectEdit bool `json:"allowed_direct_edit"` // 是否允许直接执行修改
|
||||
@@ -153,12 +163,15 @@ func (e *GovernanceEngine) CreateOrGetDocument(
|
||||
}
|
||||
|
||||
ensureUser := func(uid int64, uname, urole string) {
|
||||
if uid > 0 {
|
||||
_, _ = 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)
|
||||
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")
|
||||
@@ -185,7 +198,8 @@ func (e *GovernanceEngine) CreateOrGetDocument(
|
||||
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,
|
||||
doc.AuthorID, doc.AuthorName, doc.ReviewerID, doc.ReviewerName, doc.ApproverID, doc.ApproverName,
|
||||
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 {
|
||||
|
||||
@@ -97,3 +97,64 @@ func TestDocGov_DatabaseIntegration(t *testing.T) {
|
||||
t.Fatalf("expected doc locked in reviewing")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK ensures reviewer/approver=0
|
||||
// persist as SQL NULL (optional DutyMode) instead of FK-violating 0.
|
||||
func TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK(t *testing.T) {
|
||||
cfg, err := config.Load(".env")
|
||||
if err != nil || cfg == nil || cfg.ContextDbHost == "" {
|
||||
t.Skip("Skipping DB integration test: context DB not configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pool, err := pgxpool.New(ctx, cfg.ContextDSN())
|
||||
if err != nil || pool.Ping(ctx) != nil {
|
||||
t.Skipf("Skipping DB integration test: cannot connect to %s: %v", cfg.ContextDSN(), err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
engine := docgov.NewGovernanceEngine(pool)
|
||||
testDocID := fmt.Sprintf("doc_opt_duty_%d", time.Now().UnixNano())
|
||||
projectID := time.Now().UnixNano()%900000 + 100000
|
||||
|
||||
doc, err := engine.CreateOrGetDocument(
|
||||
ctx,
|
||||
testDocID,
|
||||
projectID,
|
||||
"intake",
|
||||
"可选职责空审核人",
|
||||
"V0.5_feasibility",
|
||||
"",
|
||||
9, "系统管理员",
|
||||
0, "",
|
||||
0, "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrGetDocument with reviewer/approver=0 failed: %v", err)
|
||||
}
|
||||
if doc.DocID != testDocID {
|
||||
t.Fatalf("unexpected docID: %s", doc.DocID)
|
||||
}
|
||||
|
||||
var reviewerNull, approverNull bool
|
||||
var authorID int64
|
||||
err = pool.QueryRow(ctx, `
|
||||
SELECT author_id,
|
||||
reviewer_id IS NULL,
|
||||
approver_id IS NULL
|
||||
FROM document_records WHERE doc_id = $1
|
||||
`, testDocID).Scan(&authorID, &reviewerNull, &approverNull)
|
||||
if err != nil {
|
||||
t.Fatalf("query document_records: %v", err)
|
||||
}
|
||||
if authorID != 9 {
|
||||
t.Fatalf("author_id=%d want 9", authorID)
|
||||
}
|
||||
if !reviewerNull || !approverNull {
|
||||
t.Fatalf("expected NULL reviewer/approver, got reviewerNull=%v approverNull=%v", reviewerNull, approverNull)
|
||||
}
|
||||
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM document_records WHERE doc_id = $1`, testDocID)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package docgov
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNullIfZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := nullIfZero(0); got != nil {
|
||||
t.Fatalf("nullIfZero(0)=%v want nil", got)
|
||||
}
|
||||
if got := nullIfZero(-1); got != nil {
|
||||
t.Fatalf("nullIfZero(-1)=%v want nil", got)
|
||||
}
|
||||
if got := nullIfZero(9); got != int64(9) {
|
||||
t.Fatalf("nullIfZero(9)=%v want 9", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user