Files
knowledge-governance/internal/platform/postgres/production_bindings.go
T
姚章浩 6440e3f679 feat(release): 发布目标知识库可选(源库 → 目标库映射)
- 迁移 000044:production_kb_bindings + release_item_executions.target_knowledge_base_id
- 系统设置新增「生产知识库绑定(对内/对外)」:从生产 WeKnora 实际列表选库,
  保存时真校验(不存在拒收;生产端不可达只提示并如实回报),仅管理员可改
- 执行计划按绑定解析目标库并冻结进 release_item_executions;空目标直接拒收;
  已发布过的条目保留原目标(重试/回滚回到同一处)
- DocumentBundle.RemapKnowledgeBase:knowledges/chunks/knowledge_tags/wiki_pages/
  wiki_folders 的 knowledge_base_id 与 ES 文档字段改写为目标库,重算表/文档摘要并重新封装
- 契约比对改为「声明的映射对」(KnowledgeBaseMapping),其余字段继续 fail-closed
- 契约检查器 / 目标身份 / 验证器全部走映射后的目标库;对象通道无需改写
- Compose 拆分:数据面接线 compose.release-sim.yaml + 两道门禁 compose.release-gates.yaml
- 测试:RemapKnowledgeBase 全通道/无操作/fail-closed、映射契约、绑定接口、RBAC、真实 PG 集成
2026-10-08 15:41:13 +08:00

78 lines
2.7 KiB
Go

package postgres
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
)
// ProductionBinding is the UI-managed 源库 → 目标库 mapping row. The left side
// of the release (the tested knowledge base) is fixed by the publication; the
// right side is the production-side base this table chooses. An empty side
// keeps the historical same-id semantics.
type ProductionBinding struct {
InternalKBID string
ExternalKBID string
UpdatedBy string
}
// LoadProductionKBBindings reads the single active mapping row. A missing row
// reads back as unbound rather than an error.
func (s *Store) LoadProductionKBBindings(ctx context.Context) (string, string, string, error) {
var binding ProductionBinding
err := s.db.QueryRow(ctx, `SELECT internal_kb_id,external_kb_id,updated_by FROM production_kb_bindings WHERE id='active'`).
Scan(&binding.InternalKBID, &binding.ExternalKBID, &binding.UpdatedBy)
if errors.Is(err, pgx.ErrNoRows) {
return "", "", "", nil
}
if err != nil {
return "", "", "", errors.New("load production kb bindings")
}
return binding.InternalKBID, binding.ExternalKBID, binding.UpdatedBy, nil
}
// SaveProductionKBBindings upserts the mapping row. The knowledge base ids must
// already have been validated against the live production WeKnora by the caller.
func (s *Store) SaveProductionKBBindings(ctx context.Context, internalID, externalID, updatedBy string, now time.Time) error {
if now.IsZero() {
return errors.New("production binding save requires a timestamp")
}
if _, err := s.db.Exec(ctx, `
INSERT INTO production_kb_bindings(id,internal_kb_id,external_kb_id,updated_by,updated_at)
VALUES('active',$1,$2,$3,$4)
ON CONFLICT(id) DO UPDATE SET
internal_kb_id=EXCLUDED.internal_kb_id,
external_kb_id=EXCLUDED.external_kb_id,
updated_by=EXCLUDED.updated_by,
updated_at=EXCLUDED.updated_at`, internalID, externalID, updatedBy, now); err != nil {
return errors.New("save production kb bindings")
}
return nil
}
// ResolveProductionTargetKB maps a publication scope to the production base the
// release should write into. An unbound scope falls back to the tested base id,
// which is the historical same-id behaviour (and fails closed at the contract
// check when no such base exists on the target).
func (s *Store) ResolveProductionTargetKB(ctx context.Context, scope, testKBID string) (string, error) {
internal, external, _, err := s.LoadProductionKBBindings(ctx)
if err != nil {
return "", err
}
bound := ""
switch scope {
case "internal":
bound = internal
case "external":
bound = external
default:
return "", errors.New("unknown release scope")
}
if bound == "" {
return testKBID, nil
}
return bound, nil
}