dcdc8d1ddd
全栈知识治理平台首次入库(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 干净
118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
package testweknora
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"knowledge-governance/internal/jobs"
|
|
postgresstore "knowledge-governance/internal/platform/postgres"
|
|
"knowledge-governance/internal/platform/weknora"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidRemoveJobPayload = errors.New("test_remove_invalid_payload")
|
|
ErrRemoveRemoteFailed = errors.New("test_remove_remote_failed")
|
|
ErrRemoveRemoteIdentity = errors.New("test_remove_remote_identity_invalid")
|
|
ErrRemovePending = errors.New("test_remove_pending")
|
|
ErrRemoveStateConflict = errors.New("test_remove_state_conflict")
|
|
)
|
|
|
|
type RemoveStore interface {
|
|
LoadPublicationForRemoval(context.Context, string) (postgresstore.PublicationForRemoval, error)
|
|
RecordRemovalCompleted(context.Context, string, time.Time) error
|
|
RecordRemovalFailure(context.Context, string, string, time.Time) error
|
|
}
|
|
|
|
type RemoveClient interface {
|
|
GetKnowledge(context.Context, string) (weknora.Knowledge, error)
|
|
DeleteKnowledge(context.Context, string) error
|
|
}
|
|
|
|
type RemoveHandlerConfig struct {
|
|
Store RemoveStore
|
|
Client RemoveClient
|
|
Now func() time.Time
|
|
}
|
|
|
|
type RemoveHandler struct {
|
|
store RemoveStore
|
|
client RemoveClient
|
|
now func() time.Time
|
|
}
|
|
|
|
type removePayload struct {
|
|
PublicationID string `json:"publication_id"`
|
|
}
|
|
|
|
func NewRemoveHandler(config RemoveHandlerConfig) (*RemoveHandler, error) {
|
|
if config.Store == nil || config.Client == nil {
|
|
return nil, errors.New("invalid test removal handler configuration")
|
|
}
|
|
if config.Now == nil {
|
|
config.Now = time.Now
|
|
}
|
|
return &RemoveHandler{store: config.Store, client: config.Client, now: config.Now}, nil
|
|
}
|
|
|
|
func (handler *RemoveHandler) Handle(ctx context.Context, job jobs.Job) error {
|
|
var payload removePayload
|
|
if err := decodeStrictJSON(job.Payload, &payload); err != nil || payload.PublicationID == "" {
|
|
return jobs.Permanent(jobs.ErrCodeHandlerFallback, ErrInvalidRemoveJobPayload)
|
|
}
|
|
candidate, err := handler.store.LoadPublicationForRemoval(ctx, payload.PublicationID)
|
|
if err != nil {
|
|
return ErrRemoveStateConflict
|
|
}
|
|
knowledge, err := handler.client.GetKnowledge(ctx, candidate.KnowledgeID)
|
|
if isNotFound(err) {
|
|
return handler.complete(ctx, candidate.ID)
|
|
}
|
|
if err != nil {
|
|
return ErrRemoveRemoteFailed
|
|
}
|
|
if knowledge.ID != candidate.KnowledgeID || knowledge.KnowledgeBaseID != candidate.TestKBID {
|
|
return handler.failClosed(ctx, candidate.ID, "removal_identity_invalid")
|
|
}
|
|
if err := handler.client.DeleteKnowledge(ctx, candidate.KnowledgeID); err != nil {
|
|
if isNotFound(err) {
|
|
return handler.complete(ctx, candidate.ID)
|
|
}
|
|
if !weknora.IsRetryable(err) {
|
|
return handler.failClosed(ctx, candidate.ID, "removal_remote_rejected")
|
|
}
|
|
return ErrRemoveRemoteFailed
|
|
}
|
|
knowledge, err = handler.client.GetKnowledge(ctx, candidate.KnowledgeID)
|
|
if isNotFound(err) {
|
|
return handler.complete(ctx, candidate.ID)
|
|
}
|
|
if err != nil {
|
|
return ErrRemoveRemoteFailed
|
|
}
|
|
if knowledge.ID != candidate.KnowledgeID || knowledge.KnowledgeBaseID != candidate.TestKBID {
|
|
return handler.failClosed(ctx, candidate.ID, "removal_identity_invalid")
|
|
}
|
|
return ErrRemovePending
|
|
}
|
|
|
|
func (handler *RemoveHandler) complete(ctx context.Context, publicationID string) error {
|
|
if err := handler.store.RecordRemovalCompleted(ctx, publicationID, handler.now()); err != nil {
|
|
return ErrRemoveStateConflict
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (handler *RemoveHandler) failClosed(ctx context.Context, publicationID, code string) error {
|
|
if err := handler.store.RecordRemovalFailure(ctx, publicationID, code, handler.now()); err != nil {
|
|
return ErrRemoveStateConflict
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isNotFound(err error) bool {
|
|
var httpErr *weknora.HTTPError
|
|
return errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound
|
|
}
|