6440e3f679
- 迁移 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 集成
78 lines
2.7 KiB
Go
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
|
|
}
|