Files
knowledge-governance/internal/platform/postgres/operation_requests.go
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

181 lines
6.8 KiB
Go

package postgres
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/jobs"
)
var ErrOperationRequestConflict = errors.New("operation request conflicts with an existing request")
type operationExecutor func(pgx.Tx) (json.RawMessage, error)
func (s *Store) RouteCatalogFilesWithOperation(ctx context.Context, request governance.CatalogRouteRequest, operationID string) (governance.CatalogRouteResult, bool, error) {
if err := validateCatalogRouteRequest(request); err != nil {
return governance.CatalogRouteResult{}, false, err
}
fingerprint, err := catalogRouteFingerprint(request)
if err != nil {
return governance.CatalogRouteResult{}, false, err
}
resultJSON, replayed, err := s.executeOperation(ctx, "catalog.route", operationID, fingerprint, request.Now, func(tx pgx.Tx) (json.RawMessage, error) {
result, err := routeCatalogFilesTx(ctx, tx, request)
if err != nil {
return nil, err
}
encoded, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("encode catalog route result: %w", err)
}
return encoded, nil
})
if err != nil {
return governance.CatalogRouteResult{}, false, err
}
var result governance.CatalogRouteResult
if err := json.Unmarshal(resultJSON, &result); err != nil {
return governance.CatalogRouteResult{}, false, fmt.Errorf("decode stored catalog route result: %w", err)
}
return result, replayed, nil
}
func catalogRouteFingerprint(request governance.CatalogRouteRequest) (string, error) {
targets := request.TargetKnowledgeBaseIDs
if len(targets) == 0 && request.TargetKnowledgeBaseID != "" {
targets = []string{request.TargetKnowledgeBaseID}
}
payload, err := json.Marshal(struct {
ConnectionID string `json:"connection_id"`
NodeIDs []string `json:"node_ids"`
Visibility governance.Visibility `json:"visibility"`
Targets []string `json:"target_knowledge_base_ids"`
OperatorRef string `json:"operator_ref"`
Reason string `json:"reason"`
}{
ConnectionID: request.ConnectionID,
NodeIDs: request.NodeIDs,
Visibility: request.Visibility,
Targets: targets,
OperatorRef: strings.TrimSpace(request.OperatorRef),
Reason: strings.TrimSpace(request.Reason),
})
if err != nil {
return "", fmt.Errorf("encode catalog route fingerprint: %w", err)
}
digest := sha256.Sum256(payload)
return hex.EncodeToString(digest[:]), nil
}
func (s *Store) EnqueueCatalogRefreshWithOperation(ctx context.Context, source governance.CatalogSource, operationID string, now time.Time) (jobs.Job, bool, error) {
if !validRouteID(source.ConnectionID) || strings.TrimSpace(source.Name) == "" || now.IsZero() {
return jobs.Job{}, false, errors.New("invalid catalog refresh request")
}
fingerprint, err := operationFingerprint(struct {
ConnectionID string `json:"connection_id"`
}{ConnectionID: source.ConnectionID})
if err != nil {
return jobs.Job{}, false, err
}
resultJSON, replayed, err := s.executeOperation(ctx, "catalog.refresh", operationID, fingerprint, now, func(tx pgx.Tx) (json.RawMessage, error) {
jobID, err := newOpaqueID("job-")
if err != nil {
return nil, err
}
payload, err := json.Marshal(map[string]any{"connection_id": source.ConnectionID, "source_type": source.SourceType, "source_name": source.Name, "stage": "queued", "progress": 0, "message": "等待后台刷新", "processed": 0, "total": 0})
if err != nil {
return nil, fmt.Errorf("encode catalog refresh job: %w", err)
}
job, err := scanJob(tx.QueryRow(ctx, enqueueJobSQL, jobID, "source.catalog.refresh", payload, 20, now, 3, "catalog-refresh:"+jobID, now))
if err != nil {
return nil, fmt.Errorf("enqueue catalog refresh job: %w", err)
}
encoded, err := json.Marshal(job)
if err != nil {
return nil, fmt.Errorf("encode catalog refresh result: %w", err)
}
return encoded, nil
})
if err != nil {
return jobs.Job{}, false, err
}
var result jobs.Job
if err := json.Unmarshal(resultJSON, &result); err != nil {
return jobs.Job{}, false, fmt.Errorf("decode stored catalog refresh result: %w", err)
}
return result, replayed, nil
}
func operationFingerprint(value any) (string, error) {
payload, err := json.Marshal(value)
if err != nil {
return "", fmt.Errorf("encode operation fingerprint: %w", err)
}
digest := sha256.Sum256(payload)
return hex.EncodeToString(digest[:]), nil
}
func (s *Store) executeOperation(ctx context.Context, operationType, operationID, fingerprint string, now time.Time, execute operationExecutor) (json.RawMessage, bool, error) {
if strings.TrimSpace(operationType) == "" || len(operationType) > 100 || strings.TrimSpace(operationID) == "" || len(operationID) > 128 || len(fingerprint) != 64 || now.IsZero() || execute == nil {
return nil, false, errors.New("invalid operation request")
}
beginner, ok := s.db.(interface {
Begin(context.Context) (pgx.Tx, error)
})
if !ok {
return nil, false, errors.New("operation request transactions are unavailable")
}
tx, err := beginner.Begin(ctx)
if err != nil {
return nil, false, fmt.Errorf("begin operation request: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
tag, err := tx.Exec(ctx, `INSERT INTO operation_requests(operation_type,operation_id,request_fingerprint_sha256,state,result,created_at,completed_at)
VALUES($1,$2,$3,'completed','{}'::jsonb,$4,$4)
ON CONFLICT(operation_type,operation_id) DO NOTHING`, operationType, operationID, fingerprint, now)
if err != nil {
return nil, false, fmt.Errorf("create operation request: %w", err)
}
if tag.RowsAffected() == 0 {
var storedFingerprint string
var storedResult json.RawMessage
if err := tx.QueryRow(ctx, `SELECT request_fingerprint_sha256,result
FROM operation_requests
WHERE operation_type=$1 AND operation_id=$2
FOR UPDATE`, operationType, operationID).Scan(&storedFingerprint, &storedResult); err != nil {
return nil, false, fmt.Errorf("load operation request: %w", err)
}
if storedFingerprint != fingerprint {
return nil, false, ErrOperationRequestConflict
}
if err := tx.Commit(ctx); err != nil {
return nil, false, fmt.Errorf("commit replay operation request: %w", err)
}
return storedResult, true, nil
}
result, err := execute(tx)
if err != nil {
return nil, false, err
}
if !json.Valid(result) {
return nil, false, errors.New("operation result must be valid JSON")
}
if _, err := tx.Exec(ctx, `UPDATE operation_requests SET result=$3::jsonb,completed_at=$4
WHERE operation_type=$1 AND operation_id=$2`, operationType, operationID, result, now); err != nil {
return nil, false, fmt.Errorf("complete operation request: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return nil, false, fmt.Errorf("commit operation request: %w", err)
}
return result, false, nil
}