DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,624 @@
|
||||
package servergroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMergeModuleInvalid 表示合服模块缺少名称、阶段或执行函数。
|
||||
ErrMergeModuleInvalid = errors.New("server group merge module is invalid")
|
||||
// ErrMergeModuleExists 表示合服模块名称已经注册。
|
||||
ErrMergeModuleExists = errors.New("server group merge module already exists")
|
||||
// ErrMergeModuleFailed 表示合服模块执行失败。
|
||||
ErrMergeModuleFailed = errors.New("server group merge module failed")
|
||||
)
|
||||
|
||||
const (
|
||||
// MergeModulePhaseGenDB 表示合服模块处于预生成数据库阶段。
|
||||
MergeModulePhaseGenDB = "gendb"
|
||||
// MergeModulePhaseMerge 表示合服模块处于正式合并阶段。
|
||||
MergeModulePhaseMerge = "merge"
|
||||
// MergeModulePhaseRollback 表示合服模块处于回滚阶段。
|
||||
MergeModulePhaseRollback = "rollback"
|
||||
)
|
||||
|
||||
// MergeModuleFunc 是项目侧合服模块的统一执行函数。
|
||||
type MergeModuleFunc func(context.Context, MergeModuleInput) (MergeModuleResult, error)
|
||||
|
||||
// MergeGenDBFunc 对应合服前的数据生成和冲突预检阶段。
|
||||
type MergeGenDBFunc = MergeModuleFunc
|
||||
|
||||
// MergeApplyFunc 对应业务数据真正迁移或合并阶段。
|
||||
type MergeApplyFunc = MergeModuleFunc
|
||||
|
||||
// MergeRollbackFunc 对应业务数据回滚或补偿阶段。
|
||||
type MergeRollbackFunc = MergeModuleFunc
|
||||
|
||||
// MergeModuleSpec 描述一个项目侧合服模块,平台只保存通用契约,不保存具体业务表结构。
|
||||
type MergeModuleSpec struct {
|
||||
// Name 是模块唯一名,建议使用 profile、mail、leaderboard 这类稳定业务域名。
|
||||
Name string `json:"name"`
|
||||
// Description 描述模块职责,主要用于报告和运营后台展示。
|
||||
Description string `json:"description,omitempty"`
|
||||
// Features 是模块影响的功能域,用于和合服请求里的 feature 对齐。
|
||||
Features []string `json:"features,omitempty"`
|
||||
// Required 表示该模块是项目合服的必跑模块,必须同时提供三段处理器。
|
||||
Required bool `json:"required"`
|
||||
// GenDB 负责合服前生成预检数据、冲突方案和 dry-run 证据。
|
||||
GenDB MergeGenDBFunc `json:"-"`
|
||||
// Merge 负责合服窗口内的真实业务数据合并。
|
||||
Merge MergeApplyFunc `json:"-"`
|
||||
// Rollback 负责合服失败后的业务回滚或补偿。
|
||||
Rollback MergeRollbackFunc `json:"-"`
|
||||
}
|
||||
|
||||
// MergeModuleSnapshot 是注册器对外暴露的只读模块摘要。
|
||||
type MergeModuleSnapshot struct {
|
||||
// Name 是模块唯一名。
|
||||
Name string `json:"name"`
|
||||
// Description 是模块职责说明。
|
||||
Description string `json:"description,omitempty"`
|
||||
// Features 是模块影响的功能域。
|
||||
Features []string `json:"features,omitempty"`
|
||||
// Required 表示模块是否为必跑模块。
|
||||
Required bool `json:"required"`
|
||||
// HasGenDB 表示是否注册了预检处理器。
|
||||
HasGenDB bool `json:"has_gendb"`
|
||||
// HasMerge 表示是否注册了合并处理器。
|
||||
HasMerge bool `json:"has_merge"`
|
||||
// HasRollback 表示是否注册了回滚处理器。
|
||||
HasRollback bool `json:"has_rollback"`
|
||||
}
|
||||
|
||||
// MergeModuleInput 是传给项目侧合服模块的通用上下文快照。
|
||||
type MergeModuleInput struct {
|
||||
// Request 是当前合服请求。
|
||||
Request MergeRequest `json:"request"`
|
||||
// Current 是执行前的平台路由计划快照。
|
||||
Current Plan `json:"current"`
|
||||
// DryRun 是平台 dry-run 结果,模块侧可以复用其中的回滚点和写检查。
|
||||
DryRun MergeDryRun `json:"dry_run"`
|
||||
// Steps 是平台迁移步骤快照。
|
||||
Steps []MigrationStep `json:"steps,omitempty"`
|
||||
// Findings 是前置计划已有的风险项,供后续阶段生成证据时参考。
|
||||
Findings []ConflictFinding `json:"findings,omitempty"`
|
||||
// Meta 是项目编排器传入的通用元数据。
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// MergeModuleResult 是项目侧模块执行后的通用回执。
|
||||
type MergeModuleResult struct {
|
||||
// Module 是执行结果所属模块,空值会回填注册器里的模块名。
|
||||
Module string `json:"module"`
|
||||
// Phase 是执行阶段,取值为 gendb、merge 或 rollback。
|
||||
Phase string `json:"phase"`
|
||||
// OK 表示该模块阶段没有 blocker finding。
|
||||
OK bool `json:"ok"`
|
||||
// Skipped 表示模块与当前请求无关,不进入阶段报告。
|
||||
Skipped bool `json:"skipped,omitempty"`
|
||||
// Findings 是模块返回的冲突、风险或阻断项。
|
||||
Findings []ConflictFinding `json:"findings,omitempty"`
|
||||
// Evidence 是模块返回的证据路径、批次号或校验摘要。
|
||||
Evidence []string `json:"evidence,omitempty"`
|
||||
// Rollback 是模块建议的回滚动作或补偿步骤。
|
||||
Rollback []string `json:"rollback,omitempty"`
|
||||
// Meta 是模块返回的通用元数据。
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// MergeModuleRunReport 汇总一次模块级合服阶段执行结果,用于运营审计和上线证据归档。
|
||||
type MergeModuleRunReport struct {
|
||||
// Phase 是本次报告对应的阶段,取值为 gendb、merge 或 rollback。
|
||||
Phase string `json:"phase"`
|
||||
// Request 是合服请求快照,便于证据脱离执行进程后仍能复核。
|
||||
Request MergeRequest `json:"request"`
|
||||
// GeneratedAt 是报告生成时间,统一保存为 UTC。
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
// OK 表示本阶段没有 blocker finding,业务错误应直接返回 error。
|
||||
OK bool `json:"ok"`
|
||||
// Results 保存每个模块的原始阶段回执。
|
||||
Results []MergeModuleResult `json:"results,omitempty"`
|
||||
// Findings 汇总所有模块返回的冲突或风险项。
|
||||
Findings []ConflictFinding `json:"findings,omitempty"`
|
||||
// Evidence 汇总所有模块返回的证据路径、批次号或校验摘要。
|
||||
Evidence []string `json:"evidence,omitempty"`
|
||||
// Rollback 汇总所有模块声明的回滚动作或补偿步骤。
|
||||
Rollback []string `json:"rollback,omitempty"`
|
||||
// Meta 保存项目编排器附加的通用元数据。
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// MergeModuleCoverageReport 描述合服请求 feature 与模块处理器之间的覆盖关系。
|
||||
type MergeModuleCoverageReport struct {
|
||||
// Phase 是本次覆盖检查对应的阶段。
|
||||
Phase string `json:"phase"`
|
||||
// Request 是被检查的合服请求快照。
|
||||
Request MergeRequest `json:"request"`
|
||||
// OK 表示所有声明的 feature 都有对应阶段处理器覆盖。
|
||||
OK bool `json:"ok"`
|
||||
// Features 是本次请求声明需要检查或冻结的功能域。
|
||||
Features []string `json:"features,omitempty"`
|
||||
// Missing 是缺少模块处理器覆盖的功能域。
|
||||
Missing []string `json:"missing,omitempty"`
|
||||
// Modules 是注册器当前模块快照。
|
||||
Modules []MergeModuleSnapshot `json:"modules,omitempty"`
|
||||
// Findings 是缺失覆盖转换出的 blocker 风险项。
|
||||
Findings []ConflictFinding `json:"findings,omitempty"`
|
||||
}
|
||||
|
||||
// MergeOpRegistry 把成熟项目里按模块注册 GenDB/Merge/Rollback 的经验抽成龙恒通用扩展点。
|
||||
type MergeOpRegistry struct {
|
||||
mu sync.RWMutex
|
||||
modules map[string]MergeModuleSpec
|
||||
}
|
||||
|
||||
// NewMergeOpRegistry 创建模块注册器,并按注册顺序校验重复和必填处理器。
|
||||
func NewMergeOpRegistry(specs ...MergeModuleSpec) (*MergeOpRegistry, error) {
|
||||
registry := &MergeOpRegistry{modules: make(map[string]MergeModuleSpec, len(specs))}
|
||||
for _, spec := range specs {
|
||||
if err := registry.Register(spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
// Register 注册一个项目侧合服模块,重复模块名会被拒绝。
|
||||
func (r *MergeOpRegistry) Register(spec MergeModuleSpec) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("%w: registry is nil", ErrMergeModuleInvalid)
|
||||
}
|
||||
normalized, err := normMergeSpec(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if _, exists := r.modules[normalized.Name]; exists {
|
||||
return fmt.Errorf("%w: %s", ErrMergeModuleExists, normalized.Name)
|
||||
}
|
||||
r.modules[normalized.Name] = normalized
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot 返回按模块名排序的注册器快照,调用方可以安全修改返回值。
|
||||
func (r *MergeOpRegistry) Snapshot() []MergeModuleSnapshot {
|
||||
specs := r.snapshotSpecs()
|
||||
if len(specs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]MergeModuleSnapshot, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
out = append(out, MergeModuleSnapshot{
|
||||
Name: spec.Name,
|
||||
Description: spec.Description,
|
||||
Features: append([]string(nil), spec.Features...),
|
||||
Required: spec.Required,
|
||||
HasGenDB: spec.GenDB != nil,
|
||||
HasMerge: spec.Merge != nil,
|
||||
HasRollback: spec.Rollback != nil,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildChecker 把注册器转换为合服迁移计划可用的冲突检查器。
|
||||
func (r *MergeOpRegistry) BuildChecker() ConflictChecker {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return ConflictCheckFunc(func(ctx context.Context, request ConflictCheckRequest) ([]ConflictFinding, error) {
|
||||
coverage, err := r.CheckCoverage(request.DryRun.Request, MergeModulePhaseGenDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results, err := r.RunGenDB(ctx, MergeModuleInput{
|
||||
Request: request.DryRun.Request,
|
||||
Current: request.Current,
|
||||
DryRun: request.DryRun,
|
||||
Steps: request.Steps,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
findings := cloneClashFindings(coverage.Findings)
|
||||
for _, result := range results {
|
||||
findings = append(findings, result.Findings...)
|
||||
}
|
||||
return findings, nil
|
||||
})
|
||||
}
|
||||
|
||||
// CheckCoverage 检查合服请求声明的 feature 是否有对应阶段模块处理器覆盖。
|
||||
func (r *MergeOpRegistry) CheckCoverage(request MergeRequest, phase string) (MergeModuleCoverageReport, error) {
|
||||
phase = normalizeID(phase)
|
||||
if !validMergePhase(phase) {
|
||||
return MergeModuleCoverageReport{}, fmt.Errorf("%w: invalid coverage phase %s", ErrMergeModuleInvalid, phase)
|
||||
}
|
||||
request = normMergeReq(request)
|
||||
specs := r.snapshotSpecs()
|
||||
features := requestedFeatures(request)
|
||||
report := MergeModuleCoverageReport{
|
||||
Phase: phase,
|
||||
Request: request,
|
||||
OK: true,
|
||||
Features: features,
|
||||
Modules: snapshotsFromSpecs(specs),
|
||||
}
|
||||
if len(features) == 0 || !hasFeatureDecls(specs) {
|
||||
return report, nil
|
||||
}
|
||||
for _, feature := range features {
|
||||
if mergeFeatureCovered(specs, phase, feature) {
|
||||
continue
|
||||
}
|
||||
report.Missing = append(report.Missing, feature)
|
||||
report.Findings = append(report.Findings, ConflictFinding{
|
||||
Code: "merge_module_feature_missing",
|
||||
Severity: MigrationSeverityBlocker,
|
||||
Subject: feature,
|
||||
Detail: fmt.Sprintf("合服 feature %s 缺少 %s 阶段模块处理器", feature, phase),
|
||||
})
|
||||
}
|
||||
report.Missing = normalizeIDs(report.Missing)
|
||||
report.Findings = cloneClashFindings(report.Findings)
|
||||
report.OK = len(report.Missing) == 0
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// CoverageFindings 返回覆盖缺口对应的 blocker findings,便于接入自定义计划检查器。
|
||||
func (r *MergeOpRegistry) CoverageFindings(request MergeRequest, phase string) ([]ConflictFinding, error) {
|
||||
report, err := r.CheckCoverage(request, phase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloneClashFindings(report.Findings), nil
|
||||
}
|
||||
|
||||
// RunGenDB 执行所有已注册的合服前数据生成和冲突预检模块。
|
||||
func (r *MergeOpRegistry) RunGenDB(ctx context.Context, input MergeModuleInput) ([]MergeModuleResult, error) {
|
||||
return r.runPhase(ctx, MergeModulePhaseGenDB, input)
|
||||
}
|
||||
|
||||
// RunGenDBReport 执行合服前数据生成和冲突预检,并生成可归档报告。
|
||||
func (r *MergeOpRegistry) RunGenDBReport(ctx context.Context, input MergeModuleInput) (MergeModuleRunReport, error) {
|
||||
return r.runPhaseReport(ctx, MergeModulePhaseGenDB, input)
|
||||
}
|
||||
|
||||
// RunMerge 执行所有已注册的业务合并模块。
|
||||
func (r *MergeOpRegistry) RunMerge(ctx context.Context, input MergeModuleInput) ([]MergeModuleResult, error) {
|
||||
return r.runPhase(ctx, MergeModulePhaseMerge, input)
|
||||
}
|
||||
|
||||
// RunMergeReport 执行业务合并模块,并生成可归档报告。
|
||||
func (r *MergeOpRegistry) RunMergeReport(ctx context.Context, input MergeModuleInput) (MergeModuleRunReport, error) {
|
||||
return r.runPhaseReport(ctx, MergeModulePhaseMerge, input)
|
||||
}
|
||||
|
||||
// RunRollback 执行所有已注册的业务回滚模块。
|
||||
func (r *MergeOpRegistry) RunRollback(ctx context.Context, input MergeModuleInput) ([]MergeModuleResult, error) {
|
||||
return r.runPhase(ctx, MergeModulePhaseRollback, input)
|
||||
}
|
||||
|
||||
// RunRollbackReport 执行业务回滚模块,并生成可归档报告。
|
||||
func (r *MergeOpRegistry) RunRollbackReport(ctx context.Context, input MergeModuleInput) (MergeModuleRunReport, error) {
|
||||
return r.runPhaseReport(ctx, MergeModulePhaseRollback, input)
|
||||
}
|
||||
|
||||
// MergeModuleInputFromPlan 根据平台迁移计划生成项目侧模块输入,便于业务编排器复用同一份 dry-run 证据。
|
||||
func MergeModuleInputFromPlan(current Plan, plan MigrationPlan) MergeModuleInput {
|
||||
return MergeModuleInput{
|
||||
Request: plan.Request,
|
||||
Current: current,
|
||||
DryRun: plan.DryRun,
|
||||
Steps: plan.Steps,
|
||||
Findings: plan.Findings,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MergeOpRegistry) runPhase(ctx context.Context, phase string, input MergeModuleInput) ([]MergeModuleResult, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
phase = normalizeID(phase)
|
||||
if !validMergePhase(phase) {
|
||||
return nil, fmt.Errorf("%w: invalid phase %s", ErrMergeModuleInvalid, phase)
|
||||
}
|
||||
specs := r.snapshotSpecs()
|
||||
if len(specs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
input = cloneMergeInput(input)
|
||||
results := make([]MergeModuleResult, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return results, err
|
||||
}
|
||||
fn := mergeFuncForPhase(spec, phase)
|
||||
if fn == nil {
|
||||
continue
|
||||
}
|
||||
result, err := fn(ctx, cloneMergeInput(input))
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("%w: %s %s: %w", ErrMergeModuleFailed, spec.Name, phase, err)
|
||||
}
|
||||
normalized, err := normMergeResult(spec.Name, phase, result)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
if normalized.Skipped {
|
||||
continue
|
||||
}
|
||||
results = append(results, normalized)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *MergeOpRegistry) runPhaseReport(ctx context.Context, phase string, input MergeModuleInput) (MergeModuleRunReport, error) {
|
||||
coverage, err := r.CheckCoverage(mergeInputReq(input), phase)
|
||||
if err != nil {
|
||||
return MergeModuleRunReport{}, err
|
||||
}
|
||||
results, err := r.runPhase(ctx, phase, input)
|
||||
if err != nil {
|
||||
return MergeModuleRunReport{}, err
|
||||
}
|
||||
report, err := newMergeRunReport(phase, input, results, time.Now().UTC())
|
||||
if err != nil {
|
||||
return MergeModuleRunReport{}, err
|
||||
}
|
||||
report.Findings = append(cloneClashFindings(coverage.Findings), report.Findings...)
|
||||
if !coverage.OK {
|
||||
report.OK = false
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (r *MergeOpRegistry) snapshotSpecs() []MergeModuleSpec {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if len(r.modules) == 0 {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(r.modules))
|
||||
for name := range r.modules {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
out := make([]MergeModuleSpec, 0, len(names))
|
||||
for _, name := range names {
|
||||
out = append(out, cloneMergeModuleSpec(r.modules[name]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normMergeSpec(spec MergeModuleSpec) (MergeModuleSpec, error) {
|
||||
spec.Name = normalizeID(spec.Name)
|
||||
spec.Description = firstNonEmpty(spec.Description)
|
||||
spec.Features = normalizeIDs(spec.Features)
|
||||
if spec.Name == "" {
|
||||
return MergeModuleSpec{}, fmt.Errorf("%w: module name is required", ErrMergeModuleInvalid)
|
||||
}
|
||||
if spec.GenDB == nil && spec.Merge == nil && spec.Rollback == nil {
|
||||
return MergeModuleSpec{}, fmt.Errorf("%w: module %s has no handlers", ErrMergeModuleInvalid, spec.Name)
|
||||
}
|
||||
if spec.Required && (spec.GenDB == nil || spec.Merge == nil || spec.Rollback == nil) {
|
||||
return MergeModuleSpec{}, fmt.Errorf("%w: required module %s must provide gendb, merge and rollback handlers", ErrMergeModuleInvalid, spec.Name)
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func snapshotsFromSpecs(specs []MergeModuleSpec) []MergeModuleSnapshot {
|
||||
if len(specs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]MergeModuleSnapshot, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
out = append(out, MergeModuleSnapshot{
|
||||
Name: spec.Name,
|
||||
Description: spec.Description,
|
||||
Features: append([]string(nil), spec.Features...),
|
||||
Required: spec.Required,
|
||||
HasGenDB: spec.GenDB != nil,
|
||||
HasMerge: spec.Merge != nil,
|
||||
HasRollback: spec.Rollback != nil,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneMergeModuleSpec(spec MergeModuleSpec) MergeModuleSpec {
|
||||
spec.Features = append([]string(nil), spec.Features...)
|
||||
return spec
|
||||
}
|
||||
|
||||
func mergeFuncForPhase(spec MergeModuleSpec, phase string) MergeModuleFunc {
|
||||
switch phase {
|
||||
case MergeModulePhaseGenDB:
|
||||
return spec.GenDB
|
||||
case MergeModulePhaseMerge:
|
||||
return spec.Merge
|
||||
case MergeModulePhaseRollback:
|
||||
return spec.Rollback
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validMergePhase(phase string) bool {
|
||||
switch phase {
|
||||
case MergeModulePhaseGenDB, MergeModulePhaseMerge, MergeModulePhaseRollback:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func requestedFeatures(request MergeRequest) []string {
|
||||
request = normMergeReq(request)
|
||||
features := append([]string(nil), request.CheckFeatures...)
|
||||
features = append(features, request.BlockFeatures...)
|
||||
return normalizeIDs(features)
|
||||
}
|
||||
|
||||
func hasFeatureDecls(specs []MergeModuleSpec) bool {
|
||||
for _, spec := range specs {
|
||||
if len(spec.Features) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeFeatureCovered(specs []MergeModuleSpec, phase, feature string) bool {
|
||||
feature = normalizeID(feature)
|
||||
for _, spec := range specs {
|
||||
if len(spec.Features) == 0 || !containsID(spec.Features, feature) {
|
||||
continue
|
||||
}
|
||||
if mergeFuncForPhase(spec, phase) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeInputReq(input MergeModuleInput) MergeRequest {
|
||||
request := normMergeReq(input.Request)
|
||||
if request.ID != "" {
|
||||
return request
|
||||
}
|
||||
return normMergeReq(input.DryRun.Request)
|
||||
}
|
||||
|
||||
func cloneMergeInput(input MergeModuleInput) MergeModuleInput {
|
||||
return MergeModuleInput{
|
||||
Request: mergeInputReq(input),
|
||||
Current: clonePlan(input.Current),
|
||||
DryRun: cloneMergeDryRun(input.DryRun),
|
||||
Steps: cloneMigrationSteps(input.Steps),
|
||||
Findings: cloneClashFindings(input.Findings),
|
||||
Meta: normalizeStringMap(input.Meta),
|
||||
}
|
||||
}
|
||||
|
||||
func normMergeResult(module, phase string, result MergeModuleResult) (MergeModuleResult, error) {
|
||||
result.Module = firstNonEmpty(result.Module, module)
|
||||
result.Phase = firstNonEmpty(result.Phase, phase)
|
||||
result.Evidence = cloneMergeStrings(result.Evidence)
|
||||
result.Rollback = cloneMergeStrings(result.Rollback)
|
||||
result.Meta = normalizeStringMap(result.Meta)
|
||||
if result.Module == "" || result.Phase == "" {
|
||||
return MergeModuleResult{}, fmt.Errorf("%w: result module and phase are required", ErrMergeModuleInvalid)
|
||||
}
|
||||
if !validMergePhase(result.Phase) {
|
||||
return MergeModuleResult{}, fmt.Errorf("%w: result %s has invalid phase %s", ErrMergeModuleInvalid, result.Module, result.Phase)
|
||||
}
|
||||
findings, err := normalizeFindings(result.Module, result.Findings)
|
||||
if err != nil {
|
||||
return MergeModuleResult{}, err
|
||||
}
|
||||
result.Findings = findings
|
||||
result.OK = !hasBlockerFinding(result.Findings)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newMergeRunReport(phase string, input MergeModuleInput, results []MergeModuleResult, generatedAt time.Time) (MergeModuleRunReport, error) {
|
||||
phase = normalizeID(phase)
|
||||
if !validMergePhase(phase) {
|
||||
return MergeModuleRunReport{}, fmt.Errorf("%w: invalid report phase %s", ErrMergeModuleInvalid, phase)
|
||||
}
|
||||
if generatedAt.IsZero() {
|
||||
generatedAt = time.Now().UTC()
|
||||
} else {
|
||||
generatedAt = generatedAt.UTC()
|
||||
}
|
||||
input = cloneMergeInput(input)
|
||||
results = cloneMergeResults(results)
|
||||
report := MergeModuleRunReport{
|
||||
Phase: phase,
|
||||
Request: input.Request,
|
||||
GeneratedAt: generatedAt,
|
||||
OK: true,
|
||||
Results: results,
|
||||
Meta: cloneStringMap(input.Meta),
|
||||
}
|
||||
for _, result := range results {
|
||||
report.Findings = append(report.Findings, result.Findings...)
|
||||
report.Evidence = append(report.Evidence, result.Evidence...)
|
||||
report.Rollback = append(report.Rollback, result.Rollback...)
|
||||
if !result.OK {
|
||||
report.OK = false
|
||||
}
|
||||
}
|
||||
report.Findings = cloneClashFindings(report.Findings)
|
||||
report.Evidence = cloneMergeStrings(report.Evidence)
|
||||
report.Rollback = cloneMergeStrings(report.Rollback)
|
||||
if hasBlockerFinding(report.Findings) {
|
||||
report.OK = false
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func normalizeFindings(module string, findings []ConflictFinding) ([]ConflictFinding, error) {
|
||||
if len(findings) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]ConflictFinding, 0, len(findings))
|
||||
for _, finding := range findings {
|
||||
if finding.Subject == "" {
|
||||
finding.Subject = module
|
||||
}
|
||||
normalized, err := normConflict(finding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, normalized)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func cloneMergeResults(results []MergeModuleResult) []MergeModuleResult {
|
||||
if len(results) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]MergeModuleResult, len(results))
|
||||
for i, result := range results {
|
||||
out[i] = MergeModuleResult{
|
||||
Module: result.Module,
|
||||
Phase: result.Phase,
|
||||
OK: result.OK,
|
||||
Skipped: result.Skipped,
|
||||
Findings: cloneClashFindings(result.Findings),
|
||||
Evidence: cloneMergeStrings(result.Evidence),
|
||||
Rollback: cloneMergeStrings(result.Rollback),
|
||||
Meta: cloneStringMap(result.Meta),
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneMergeStrings(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = firstNonEmpty(value)
|
||||
if value != "" {
|
||||
out = append(out, value)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user