517d5f862d
目标非空又没有生效版本时,首次发布以前只能停在"权威版本未知"。现在有了 真实入口:管理员发起 baseline 作业,worker 在维护隔离下把目标整库捕获成 归档快照,再用同一套 Verify 证明目标与快照逐类一致,最后登记 production_effective(证据写明 origin=target_baseline、快照 ID、逐表行数、 目标索引映射)。不写目标内容、不清空目标、不手写版本。 同时修两处会误导人的判定: - productionScopeEmpty 原来只数 knowledges,"文档删了但分块/Wiki/资源还在" 的目标会被当成空库;改用适配层同一份表清单与范围过滤逐表计数。 - 概览里的"测试封存候选"原来取该 scope 最新一份归档,而归档里还有生产 预备份与目标基线快照;改为只认版本登记里 kind=test_sealed 的那一份。 迁移 075 放开 kind='baseline' 并把它并入 sync/restore 的写互斥索引; worker 重启时用同一作业 id 接管未完成的基线窗口。维护接口补 capability, 页面才能区分"未运行维护"与"维护能力没接线"。 验证:go build ./... / go vet ./... 通过;新增用例覆盖管理员校验、已有生效 版本拒绝、目标读不到时零作业零版本、候选来自登记、缺项文案指向基线入口、 capability 逐项缺失;反向自检(把候选改回按最新归档取、把文案改回旧句、 去掉管理员判断)三项均按预期变红后复原。
194 lines
8.5 KiB
Go
194 lines
8.5 KiB
Go
package governance
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"errors"
|
||
"time"
|
||
)
|
||
|
||
// 整库版本与作业的领域类型:与队列(jobs)分开——队列负责租约与重试,
|
||
// 这里负责业务身份、阶段与结果,重启后仍能如实展示。
|
||
|
||
type LibraryVersionKind string
|
||
|
||
const (
|
||
// LibraryVersionTestSealed 是测试库封存产物:只证明一份完整测试版本被封存,
|
||
// 不代表生产生效,也不代表做过生产恢复演练。
|
||
LibraryVersionTestSealed LibraryVersionKind = "test_sealed"
|
||
// LibraryVersionProductionEffective 是生产当前权威版本:只有生产验证通过并
|
||
// 记录证据后才能写入,测试快照不能顶替它。
|
||
LibraryVersionProductionEffective LibraryVersionKind = "production_effective"
|
||
// LibraryVersionProductionPrebackup 是每次生产写入前的独立预备份。
|
||
LibraryVersionProductionPrebackup LibraryVersionKind = "production_prebackup"
|
||
)
|
||
|
||
type LibraryJobKind string
|
||
|
||
const (
|
||
LibraryJobSeal LibraryJobKind = "seal"
|
||
LibraryJobSync LibraryJobKind = "sync"
|
||
// LibraryJobRestore 是按指定整库版本恢复:与同步分开,授权与阶段也独立。
|
||
LibraryJobRestore LibraryJobKind = "restore"
|
||
// LibraryJobBaseline 建立首次发布目标的权威基线:在维护隔离下把目标整库
|
||
// 读出来封存成快照并逐类校验,再登记成 production_effective。
|
||
// 它不写目标内容、不清空目标、不猜版本——基线只能来自真实捕获与校验。
|
||
LibraryJobBaseline LibraryJobKind = "baseline"
|
||
)
|
||
|
||
type LibraryJobState string
|
||
|
||
const (
|
||
LibraryJobPending LibraryJobState = "pending"
|
||
LibraryJobRunning LibraryJobState = "running"
|
||
LibraryJobSucceeded LibraryJobState = "succeeded"
|
||
LibraryJobFailed LibraryJobState = "failed"
|
||
LibraryJobCancelled LibraryJobState = "cancelled"
|
||
)
|
||
|
||
// ErrLibraryJobActive:同一目标同一类作业已有在飞作业(数据库级互斥)。
|
||
var ErrLibraryJobActive = errors.New("library job already active for this target")
|
||
|
||
// ErrLibraryJobIdempotencyConflict:同一幂等键对应了不同的请求体。
|
||
var ErrLibraryJobIdempotencyConflict = errors.New("library job idempotency key reused with a different request")
|
||
|
||
type LibraryStage struct {
|
||
Stage string `json:"stage"`
|
||
State string `json:"state"`
|
||
// Total/Completed/BytesTotal/BytesDone 为 nil 表示该维度没有可靠数据,
|
||
// 界面必须显示"未知",不得当 0 或据此算百分比。
|
||
Total *int64 `json:"total"`
|
||
Completed *int64 `json:"completed"`
|
||
BytesTotal *int64 `json:"bytes_total"`
|
||
BytesDone *int64 `json:"bytes_done"`
|
||
Detail string `json:"detail"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
type LibraryJob struct {
|
||
ID string `json:"id"`
|
||
Scope string `json:"scope"`
|
||
Kind LibraryJobKind `json:"kind"`
|
||
State LibraryJobState `json:"state"`
|
||
Stage string `json:"stage"`
|
||
IdempotencyKey string `json:"-"`
|
||
RequestDigest string `json:"request_digest"`
|
||
QueueJobID string `json:"queue_job_id"`
|
||
SnapshotID string `json:"snapshot_id"`
|
||
TargetSnapshotID string `json:"target_snapshot_id"`
|
||
Actor string `json:"actor"`
|
||
Error string `json:"error"`
|
||
Recovery string `json:"recovery"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
FinishedAt *time.Time `json:"finished_at"`
|
||
Stages []LibraryStage `json:"stages"`
|
||
}
|
||
|
||
type LibraryVersion struct {
|
||
ID string `json:"id"`
|
||
Scope string `json:"scope"`
|
||
Kind LibraryVersionKind `json:"kind"`
|
||
SnapshotID string `json:"snapshot_id"`
|
||
VersionLabel string `json:"version_label"`
|
||
SourceJobID string `json:"source_job_id"`
|
||
Evidence map[string]any `json:"evidence"`
|
||
CreatedBy string `json:"created_by"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
SupersededAt *time.Time `json:"superseded_at"`
|
||
}
|
||
|
||
// LibraryRequestDigest 绑定一次请求的确切含义:同键不同体必须被拒绝,而不是
|
||
// 复用旧作业。只覆盖会影响执行结果的字段;actor 不参与摘要。
|
||
// LibraryVersionID 生成版本记录 ID:library_versions.id 是 varchar(64),
|
||
// 用完整快照 ID 拼会超长(真实踩过),所以用稳定短摘要——同样的输入永远得到同一个 ID,
|
||
// 收尾重试因此是幂等的。
|
||
func LibraryVersionID(kind LibraryVersionKind, scope, snapshotID string) string {
|
||
sum := sha256.Sum256([]byte(string(kind) + "|" + scope + "|" + snapshotID))
|
||
return "libver-" + string(kind)[:0] + hex.EncodeToString(sum[:])[:16]
|
||
}
|
||
|
||
// QueueJobIDFor 生成队列作业 ID:jobs.id 是 varchar(36),
|
||
// 直接用业务作业 ID 加后缀会超长(真实踩过),所以用固定长度摘要。
|
||
func QueueJobIDFor(libraryJobID string) string {
|
||
sum := sha256.Sum256([]byte(libraryJobID))
|
||
return "q" + hex.EncodeToString(sum[:])[:32]
|
||
}
|
||
|
||
func LibraryRequestDigest(scope string, kind LibraryJobKind, payload map[string]any) (string, error) {
|
||
canonical := map[string]any{"scope": scope, "kind": string(kind), "payload": payload}
|
||
raw, err := json.Marshal(canonical)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
sum := sha256.Sum256(raw)
|
||
return hex.EncodeToString(sum[:]), nil
|
||
}
|
||
|
||
// SyncStages 是生产同步作业的固定阶段顺序:先备份并核验恢复条件,再写生产,
|
||
// 最后验证;失败时走恢复而不是把作业标成功。
|
||
func SyncStages() []string {
|
||
// finalize 是收尾:登记生产生效版本。登记失败不得报成功,所以它是独立阶段。
|
||
return []string{"prebackup", "database", "resources", "index", "wiki", "verify", "finalize", "rollback"}
|
||
}
|
||
|
||
// SealStages 是封存作业的阶段:导出是原子操作,没有分项进度。
|
||
func SealStages() []string {
|
||
return []string{"export"}
|
||
}
|
||
|
||
// BaselineStages 是"建立目标权威基线"的阶段:捕获目标整库 → 与快照逐类校验
|
||
// → 登记生效版本。没有 rollback 阶段:基线过程不写目标内容。
|
||
func BaselineStages() []string {
|
||
return []string{"capture", "verify", "finalize"}
|
||
}
|
||
|
||
// LibraryReleaseAuthorization 是生产发布授权:绑定目标、快照、参数摘要与有效期。
|
||
type LibraryReleaseAuthorization struct {
|
||
ID string `json:"id"`
|
||
Scope string `json:"scope"`
|
||
SnapshotID string `json:"snapshot_id"`
|
||
ParamsDigest string `json:"params_digest"`
|
||
AuthorizedBy string `json:"authorized_by"`
|
||
ExpiresAt time.Time `json:"expires_at"`
|
||
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// ValidAt 只在未撤销且未过期时为真;参数摘要由调用方显式比对。
|
||
func (a LibraryReleaseAuthorization) ValidAt(now time.Time) bool {
|
||
return a.RevokedAt == nil && now.Before(a.ExpiresAt)
|
||
}
|
||
|
||
// LibraryDeadLetterRelease 是治理平台对一条上游死信的精确解除记录。上游行保持原样,
|
||
// 这条记录只说明"这一行已被审计、重放并结清";指纹一变就不再命中。
|
||
type LibraryDeadLetterRelease struct {
|
||
ID string `json:"id"`
|
||
Instance string `json:"instance"`
|
||
TenantID uint64 `json:"tenant_id"`
|
||
KnowledgeBaseID string `json:"kb_id"`
|
||
DeadLetterID int64 `json:"dead_letter_id"`
|
||
Fingerprint string `json:"fingerprint"`
|
||
TaskType string `json:"task_type"`
|
||
Scope string `json:"scope"`
|
||
ScopeID string `json:"scope_id"`
|
||
RelatedID string `json:"related_id"`
|
||
ReplayQueue string `json:"replay_queue"`
|
||
ReplayTaskID string `json:"replay_task_id"`
|
||
ReplayedAt time.Time `json:"replayed_at"`
|
||
ReplayEvidence string `json:"replay_evidence"`
|
||
ObservationDigest string `json:"observation_digest"`
|
||
Observation json.RawMessage `json:"observation"`
|
||
Note string `json:"note"`
|
||
Actor string `json:"actor"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// LibraryDeadLetterReleaseID 生成解除记录 ID。同一实例同一指纹永远得到同一个 ID,
|
||
// 重复提交返回已存记录而不是造第二条审计。
|
||
func LibraryDeadLetterReleaseID(instance, fingerprint string) string {
|
||
sum := sha256.Sum256([]byte(instance + "|" + fingerprint))
|
||
return "dldr-" + hex.EncodeToString(sum[:])[:24]
|
||
}
|