1db21fb899
- 生产/测试 WeKnora 地址、租户 ID、知识库 ID 由静态 env 收敛为版本化 UI 设置,保留修订号与操作人审计 - 新增 serviceverify:保存前真调目标端点探测,端点无效才拒绝;网络抖动或无凭据仅告警 - 迁移 000042(生产 WeKnora 端点)/000043(服务目标);补齐设置候选与校验契约用例
158 lines
5.6 KiB
Go
158 lines
5.6 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
var ErrInvalidUISettings = errors.New("invalid UI settings")
|
|
|
|
const uiSettingsColumns = `id,state,document_refresh_seconds,resource_refresh_seconds,stalled_after_seconds,finalizing_after_seconds,ingestion_concurrency,test_weknora_base_url,production_weknora_base_url,production_tenant_id,production_knowledge_base_id,updated_by,created_at,activated_at,retired_at,error_code`
|
|
|
|
const getActiveUISettingsSQL = `
|
|
SELECT ` + uiSettingsColumns + `
|
|
FROM ui_setting_revisions
|
|
WHERE state = 'active'
|
|
`
|
|
|
|
const createUISettingsCandidateSQL = `
|
|
INSERT INTO ui_setting_revisions(
|
|
id,state,document_refresh_seconds,resource_refresh_seconds,
|
|
stalled_after_seconds,finalizing_after_seconds,ingestion_concurrency,
|
|
test_weknora_base_url,production_weknora_base_url,production_tenant_id,
|
|
production_knowledge_base_id,updated_by,created_at
|
|
) VALUES($1,'validated',$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
|
RETURNING ` + uiSettingsColumns + `
|
|
`
|
|
|
|
const getUISettingsRevisionSQL = `
|
|
SELECT ` + uiSettingsColumns + `
|
|
FROM ui_setting_revisions
|
|
WHERE id=$1
|
|
`
|
|
|
|
const lockUISettingsRevisionSQL = `
|
|
SELECT ` + uiSettingsColumns + `
|
|
FROM ui_setting_revisions
|
|
WHERE id=$1
|
|
FOR UPDATE
|
|
`
|
|
|
|
const retireActiveUISettingsSQL = `
|
|
UPDATE ui_setting_revisions
|
|
SET state='retired',retired_at=$2
|
|
WHERE state='active' AND id<>$1
|
|
`
|
|
|
|
const activateUISettingsSQL = `
|
|
UPDATE ui_setting_revisions
|
|
SET state='active',activated_at=$2,retired_at=NULL,error_code=''
|
|
WHERE id=$1 AND state='validated'
|
|
RETURNING ` + uiSettingsColumns + `
|
|
`
|
|
|
|
func (s *Store) GetActiveUISettings(ctx context.Context) (governance.UISettings, error) {
|
|
return scanUISettings(s.db.QueryRow(ctx, getActiveUISettingsSQL))
|
|
}
|
|
|
|
func (s *Store) CreateUISettingsCandidate(ctx context.Context, request governance.CreateUISettingsCandidateRequest) (governance.UISettings, error) {
|
|
if !request.Valid() {
|
|
return governance.UISettings{}, ErrInvalidUISettings
|
|
}
|
|
id, err := newOpaqueID("uis-")
|
|
if err != nil {
|
|
return governance.UISettings{}, fmt.Errorf("create UI settings id: %w", err)
|
|
}
|
|
return scanUISettings(s.db.QueryRow(ctx, createUISettingsCandidateSQL,
|
|
id, request.DocumentRefreshSeconds, request.ResourceRefreshSeconds,
|
|
request.StalledAfterSeconds, request.FinalizingAfterSeconds, request.IngestionConcurrency,
|
|
request.TestWeKnoraBaseURL, request.ProductionWeKnoraBaseURL,
|
|
request.ProductionTenantID, request.ProductionKnowledgeBaseID,
|
|
request.UpdatedBy, request.Now.UTC()))
|
|
}
|
|
|
|
// GetUISettingsRevision reads one revision in any state. Activation uses it to
|
|
// compare the incoming endpoint against the active one before letting an
|
|
// engineer apply a change only an admin may author.
|
|
func (s *Store) GetUISettingsRevision(ctx context.Context, revisionID string) (governance.UISettings, error) {
|
|
if !validShortID(revisionID) {
|
|
return governance.UISettings{}, ErrInvalidUISettings
|
|
}
|
|
return scanUISettings(s.db.QueryRow(ctx, getUISettingsRevisionSQL, revisionID))
|
|
}
|
|
|
|
func (s *Store) ActivateUISettings(ctx context.Context, revisionID string, now time.Time) (governance.UISettings, error) {
|
|
if !validShortID(revisionID) || now.IsZero() {
|
|
return governance.UISettings{}, ErrInvalidUISettings
|
|
}
|
|
var tx pgx.Tx
|
|
var err error
|
|
ownsTransaction := false
|
|
if existing, ok := s.db.(pgx.Tx); ok {
|
|
tx = existing
|
|
} else {
|
|
beginner, ok := s.db.(interface {
|
|
Begin(context.Context) (pgx.Tx, error)
|
|
})
|
|
if !ok {
|
|
return governance.UISettings{}, errors.New("store does not support transactions")
|
|
}
|
|
tx, err = beginner.Begin(ctx)
|
|
if err != nil {
|
|
return governance.UISettings{}, fmt.Errorf("begin UI settings activation: %w", err)
|
|
}
|
|
ownsTransaction = true
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
}
|
|
candidate, err := scanUISettings(tx.QueryRow(ctx, lockUISettingsRevisionSQL, revisionID))
|
|
if err != nil {
|
|
return governance.UISettings{}, err
|
|
}
|
|
if candidate.State == "active" {
|
|
if ownsTransaction {
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return governance.UISettings{}, fmt.Errorf("commit UI settings activation: %w", err)
|
|
}
|
|
}
|
|
return candidate, nil
|
|
}
|
|
if candidate.State != "validated" {
|
|
return governance.UISettings{}, ErrInvalidUISettings
|
|
}
|
|
activatedAt := now.UTC()
|
|
if _, err := tx.Exec(ctx, retireActiveUISettingsSQL, revisionID, activatedAt); err != nil {
|
|
return governance.UISettings{}, fmt.Errorf("retire active UI settings: %w", err)
|
|
}
|
|
active, err := scanUISettings(tx.QueryRow(ctx, activateUISettingsSQL, revisionID, activatedAt))
|
|
if err != nil {
|
|
return governance.UISettings{}, err
|
|
}
|
|
if ownsTransaction {
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return governance.UISettings{}, fmt.Errorf("commit UI settings activation: %w", err)
|
|
}
|
|
}
|
|
return active, nil
|
|
}
|
|
|
|
func scanUISettings(row interface{ Scan(...any) error }) (governance.UISettings, error) {
|
|
var settings governance.UISettings
|
|
if err := row.Scan(&settings.ID, &settings.State, &settings.DocumentRefreshSeconds,
|
|
&settings.ResourceRefreshSeconds, &settings.StalledAfterSeconds, &settings.FinalizingAfterSeconds,
|
|
&settings.IngestionConcurrency, &settings.TestWeKnoraBaseURL, &settings.ProductionWeKnoraBaseURL,
|
|
&settings.ProductionTenantID, &settings.ProductionKnowledgeBaseID,
|
|
&settings.UpdatedBy, &settings.CreatedAt, &settings.ActivatedAt, &settings.RetiredAt, &settings.ErrorCode); err != nil {
|
|
return governance.UISettings{}, fmt.Errorf("scan UI settings: %w", err)
|
|
}
|
|
if settings.State != "validated" && settings.State != "active" && settings.State != "retired" && settings.State != "failed" {
|
|
return governance.UISettings{}, errors.New("invalid UI settings state")
|
|
}
|
|
return settings, nil
|
|
}
|