服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
657 lines
20 KiB
Go
657 lines
20 KiB
Go
package servergroup
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// MergeRunnerOptions 组合合服 worker、模块注册器和归档存储。
|
||
type MergeRunnerOptions struct {
|
||
Worker *MergeWorker
|
||
MergeOps *MergeOpRegistry
|
||
Archives MergeArchiveStore
|
||
Now func() time.Time
|
||
}
|
||
|
||
// MergeRunner 串起 dry-run、业务模块报告、平台 apply、回滚和归档。
|
||
type MergeRunner struct {
|
||
worker *MergeWorker
|
||
mergeOps *MergeOpRegistry
|
||
archives MergeArchiveStore
|
||
now func() time.Time
|
||
}
|
||
|
||
// MergeCmd 是后台、CLI 或流水线传给合服工作流的通用命令。
|
||
type MergeCmd struct {
|
||
Mode string `json:"mode"`
|
||
ArchiveID string `json:"archive_id,omitempty"`
|
||
RollbackArchiveID string `json:"rollback_archive_id,omitempty"`
|
||
WorkflowID string `json:"workflow_id,omitempty"`
|
||
ApprovalID string `json:"approval_id,omitempty"`
|
||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||
OperatorID string `json:"operator_id,omitempty"`
|
||
Reason string `json:"reason,omitempty"`
|
||
Request MergeRequest `json:"request"`
|
||
RollbackPoint RollbackPoint `json:"rollback_point,omitempty"`
|
||
RestoreVersion string `json:"restore_version,omitempty"`
|
||
AllowVersionMismatch bool `json:"allow_version_mismatch,omitempty"`
|
||
Meta map[string]string `json:"meta,omitempty"`
|
||
}
|
||
|
||
// MergeRunResult 是一次合服工作流预检或执行后的结构化结果。
|
||
type MergeRunResult struct {
|
||
Mode string `json:"mode"`
|
||
Applied bool `json:"applied"`
|
||
GeneratedAt time.Time `json:"generated_at"`
|
||
Archive MergeArchive `json:"archive"`
|
||
Migration *MigrationPlan `json:"migration,omitempty"`
|
||
Apply *MergeApplyResult `json:"apply,omitempty"`
|
||
RollbackApply *RollbackApplyResult `json:"rollback_apply,omitempty"`
|
||
ModuleReports []MergeModuleRunReport `json:"module_reports,omitempty"`
|
||
}
|
||
|
||
// NewMergeRunner 创建合服工作流 runner。
|
||
func NewMergeRunner(options MergeRunnerOptions) (*MergeRunner, error) {
|
||
if options.Worker == nil {
|
||
return nil, ErrNotFound
|
||
}
|
||
now := options.Now
|
||
if now == nil {
|
||
now = time.Now
|
||
}
|
||
return &MergeRunner{
|
||
worker: options.Worker,
|
||
mergeOps: options.MergeOps,
|
||
archives: options.Archives,
|
||
now: now,
|
||
}, nil
|
||
}
|
||
|
||
// MergeCmdFromParams 把 admincmd 模板参数或 HTTP 参数转换为合服工作流命令。
|
||
func MergeCmdFromParams(params map[string]any) (MergeCmd, error) {
|
||
mode, err := mergeStrParam(params, "mode")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
mergeID, err := mergeStrParam(params, "merge_id")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
mainShardID, err := mergeStrParam(params, "main_shard_id")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
shards, err := stringListParam(params, "shards")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
checkFeatures, err := stringListParam(params, "check_features")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
blockFeatures, err := stringListParam(params, "block_features")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
nextVersion, err := mergeStrParam(params, "next_version")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
archiveID, err := mergeStrParam(params, "archive_id")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
rollbackArchiveID, err := mergeStrParam(params, "rollback_archive_id")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
workflowID, err := mergeStrParam(params, "workflow_id")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
approvalID, err := mergeStrParam(params, "approval_id")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
restoreVersion, err := mergeStrParam(params, "restore_version")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
allowMismatch, err := mergeBoolParam(params, "allow_version_mismatch")
|
||
if err != nil {
|
||
return MergeCmd{}, err
|
||
}
|
||
return normalizeMergeCmd(MergeCmd{
|
||
Mode: mode,
|
||
ArchiveID: archiveID,
|
||
RollbackArchiveID: rollbackArchiveID,
|
||
WorkflowID: workflowID,
|
||
ApprovalID: approvalID,
|
||
Request: MergeRequest{
|
||
ID: mergeID,
|
||
MainShardID: mainShardID,
|
||
Shards: shards,
|
||
NextVersion: nextVersion,
|
||
CheckFeatures: checkFeatures,
|
||
BlockFeatures: blockFeatures,
|
||
},
|
||
RestoreVersion: restoreVersion,
|
||
AllowVersionMismatch: allowMismatch,
|
||
})
|
||
}
|
||
|
||
// Preview 只生成计划、模块预检报告和归档,不修改平台路由计划。
|
||
func (r *MergeRunner) Preview(ctx context.Context, command MergeCmd) (MergeRunResult, error) {
|
||
if r == nil || r.worker == nil {
|
||
return MergeRunResult{}, ErrNotFound
|
||
}
|
||
if err := ctxErr(ctx); err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
command, err := normalizeMergeCmd(command)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
switch command.Mode {
|
||
case MergeStageDryRun, MergeStageApply:
|
||
return r.previewMerge(ctx, command)
|
||
case MergeStageRollbackDry, MergeStageRollback:
|
||
return r.previewRollback(ctx, command)
|
||
default:
|
||
return MergeRunResult{}, ErrInvalidMigration
|
||
}
|
||
}
|
||
|
||
// Execute 按命令模式执行 apply 或 rollback;dry-run 模式仍只做预检。
|
||
func (r *MergeRunner) Execute(ctx context.Context, command MergeCmd) (MergeRunResult, error) {
|
||
if r == nil || r.worker == nil {
|
||
return MergeRunResult{}, ErrNotFound
|
||
}
|
||
if err := ctxErr(ctx); err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
command, err := normalizeMergeCmd(command)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
switch command.Mode {
|
||
case MergeStageDryRun:
|
||
return r.previewMerge(ctx, command)
|
||
case MergeStageApply:
|
||
return r.executeMerge(ctx, command)
|
||
case MergeStageRollbackDry:
|
||
return r.previewRollback(ctx, command)
|
||
case MergeStageRollback:
|
||
return r.executeRollback(ctx, command)
|
||
default:
|
||
return MergeRunResult{}, ErrInvalidMigration
|
||
}
|
||
}
|
||
|
||
func (r *MergeRunner) previewMerge(ctx context.Context, command MergeCmd) (MergeRunResult, error) {
|
||
current := r.worker.Snapshot()
|
||
plan, err := r.worker.Plan(ctx, command.Request)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
reports, err := r.runGenDBReport(ctx, current, plan, command.Meta)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
archive, err := NewMergeArchive(MergeArchiveOptions{
|
||
Stage: MergeStageDryRun,
|
||
ArchiveID: command.ArchiveID,
|
||
WorkflowID: command.WorkflowID,
|
||
ApprovalID: command.ApprovalID,
|
||
IdempotencyKey: command.IdempotencyKey,
|
||
OperatorID: command.OperatorID,
|
||
Reason: command.Reason,
|
||
Request: command.Request,
|
||
GeneratedAt: r.now().UTC(),
|
||
Migration: &plan,
|
||
ModuleReports: reports,
|
||
Meta: mergeArchiveMeta(command, map[string]string{"command_mode": command.Mode}),
|
||
})
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
if err := r.saveArchive(ctx, archive); err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
return MergeRunResult{
|
||
Mode: command.Mode,
|
||
Applied: false,
|
||
GeneratedAt: archive.GeneratedAt,
|
||
Archive: archive,
|
||
Migration: cloneMigrationPtr(plan),
|
||
ModuleReports: cloneMergeReports(reports),
|
||
}, nil
|
||
}
|
||
|
||
func (r *MergeRunner) executeMerge(ctx context.Context, command MergeCmd) (MergeRunResult, error) {
|
||
current := r.worker.Snapshot()
|
||
plan, err := r.worker.Plan(ctx, command.Request)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
gendbReports, err := r.runGenDBReport(ctx, current, plan, command.Meta)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
if !plan.Ready || !mergeReportsOK(gendbReports) {
|
||
return MergeRunResult{}, fmt.Errorf("%w: merge dry-run is not ready", ErrInvalidMigration)
|
||
}
|
||
moduleInput := MergeModuleInputFromPlan(current, plan)
|
||
moduleInput.Meta = mergeArchiveMeta(command, map[string]string{"command_mode": command.Mode})
|
||
mergeReports, err := r.runModuleReport(ctx, MergeModulePhaseMerge, moduleInput)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
reports := append(cloneMergeReports(gendbReports), mergeReports...)
|
||
if !mergeReportsOK(mergeReports) {
|
||
return MergeRunResult{}, fmt.Errorf("%w: merge module report has blockers", ErrInvalidMigration)
|
||
}
|
||
workerReport, err := r.worker.Run(ctx, command.Request, true)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
if workerReport.Apply == nil {
|
||
return MergeRunResult{}, fmt.Errorf("%w: merge apply result is required", ErrInvalidMigration)
|
||
}
|
||
archive, err := NewMergeArchive(MergeArchiveOptions{
|
||
Stage: MergeStageApply,
|
||
ArchiveID: command.ArchiveID,
|
||
WorkflowID: command.WorkflowID,
|
||
ApprovalID: command.ApprovalID,
|
||
IdempotencyKey: command.IdempotencyKey,
|
||
OperatorID: command.OperatorID,
|
||
Reason: command.Reason,
|
||
Request: command.Request,
|
||
GeneratedAt: r.now().UTC(),
|
||
Migration: &plan,
|
||
Apply: workerReport.Apply,
|
||
ModuleReports: reports,
|
||
Meta: mergeArchiveMeta(command, nil),
|
||
})
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
if err := r.saveArchive(ctx, archive); err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
return MergeRunResult{
|
||
Mode: command.Mode,
|
||
Applied: true,
|
||
GeneratedAt: archive.GeneratedAt,
|
||
Archive: archive,
|
||
Migration: cloneMigrationPtr(plan),
|
||
Apply: cloneMergeApplyRef(*workerReport.Apply),
|
||
ModuleReports: cloneMergeReports(reports),
|
||
}, nil
|
||
}
|
||
|
||
func (r *MergeRunner) previewRollback(ctx context.Context, command MergeCmd) (MergeRunResult, error) {
|
||
source, point, err := r.resolveRollbackPoint(ctx, command)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
request := rollbackRequest(command, point, false)
|
||
workerReport, err := r.worker.Rollback(ctx, request, false)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
archiveID := rollbackArchiveID(command, MergeStageRollbackDry, point)
|
||
archive, err := NewMergeArchive(MergeArchiveOptions{
|
||
Stage: MergeStageRollbackDry,
|
||
ArchiveID: archiveID,
|
||
WorkflowID: command.WorkflowID,
|
||
ApprovalID: command.ApprovalID,
|
||
IdempotencyKey: command.IdempotencyKey,
|
||
OperatorID: command.OperatorID,
|
||
Reason: command.Reason,
|
||
Request: rollbackCmdReq(command, source),
|
||
GeneratedAt: r.now().UTC(),
|
||
RollbackApply: &workerReport.Rollback,
|
||
ModuleReports: nil,
|
||
Meta: mergeArchiveMeta(command, map[string]string{"rollback_archive_id": source.ArchiveID}),
|
||
})
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
if err := r.saveArchive(ctx, archive); err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
return MergeRunResult{
|
||
Mode: command.Mode,
|
||
Applied: false,
|
||
GeneratedAt: archive.GeneratedAt,
|
||
Archive: archive,
|
||
RollbackApply: cloneApplyResultPtr(workerReport.Rollback),
|
||
}, nil
|
||
}
|
||
|
||
func (r *MergeRunner) executeRollback(ctx context.Context, command MergeCmd) (MergeRunResult, error) {
|
||
source, point, err := r.resolveRollbackPoint(ctx, command)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
current := r.worker.Snapshot()
|
||
moduleInput := rollbackModuleInput(current, source, command)
|
||
rollbackReports, err := r.runModuleReport(ctx, MergeModulePhaseRollback, moduleInput)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
if !mergeReportsOK(rollbackReports) {
|
||
return MergeRunResult{}, fmt.Errorf("%w: rollback module report has blockers", ErrInvalidMigration)
|
||
}
|
||
request := rollbackRequest(command, point, true)
|
||
workerReport, err := r.worker.Rollback(ctx, request, true)
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
archiveID := rollbackArchiveID(command, MergeStageRollback, point)
|
||
archive, err := NewMergeArchive(MergeArchiveOptions{
|
||
Stage: MergeStageRollback,
|
||
ArchiveID: archiveID,
|
||
WorkflowID: command.WorkflowID,
|
||
ApprovalID: command.ApprovalID,
|
||
IdempotencyKey: command.IdempotencyKey,
|
||
OperatorID: command.OperatorID,
|
||
Reason: command.Reason,
|
||
Request: rollbackCmdReq(command, source),
|
||
GeneratedAt: r.now().UTC(),
|
||
RollbackApply: &workerReport.Rollback,
|
||
ModuleReports: rollbackReports,
|
||
Meta: mergeArchiveMeta(command, map[string]string{"rollback_archive_id": source.ArchiveID}),
|
||
})
|
||
if err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
if err := r.saveArchive(ctx, archive); err != nil {
|
||
return MergeRunResult{}, err
|
||
}
|
||
return MergeRunResult{
|
||
Mode: command.Mode,
|
||
Applied: true,
|
||
GeneratedAt: archive.GeneratedAt,
|
||
Archive: archive,
|
||
RollbackApply: cloneApplyResultPtr(workerReport.Rollback),
|
||
ModuleReports: cloneMergeReports(rollbackReports),
|
||
}, nil
|
||
}
|
||
|
||
func (r *MergeRunner) runGenDBReport(ctx context.Context, current Plan, plan MigrationPlan, meta map[string]string) ([]MergeModuleRunReport, error) {
|
||
input := MergeModuleInputFromPlan(current, plan)
|
||
input.Meta = cloneStringMap(meta)
|
||
return r.runModuleReport(ctx, MergeModulePhaseGenDB, input)
|
||
}
|
||
|
||
func (r *MergeRunner) runModuleReport(ctx context.Context, phase string, input MergeModuleInput) ([]MergeModuleRunReport, error) {
|
||
if r.mergeOps == nil {
|
||
return nil, nil
|
||
}
|
||
report, err := r.mergeOps.runPhaseReport(ctx, phase, input)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return []MergeModuleRunReport{report}, nil
|
||
}
|
||
|
||
func (r *MergeRunner) saveArchive(ctx context.Context, archive MergeArchive) error {
|
||
if r.archives == nil {
|
||
return nil
|
||
}
|
||
return r.archives.SaveMergeArchive(ctx, archive)
|
||
}
|
||
|
||
func (r *MergeRunner) resolveRollbackPoint(ctx context.Context, command MergeCmd) (MergeArchive, RollbackPoint, error) {
|
||
if command.RollbackPoint.ID != "" {
|
||
source := MergeArchive{ArchiveID: firstNonEmpty(command.RollbackArchiveID, command.ArchiveID), Request: normMergeReq(command.Request)}
|
||
return source, cloneRollbackPoint(command.RollbackPoint), nil
|
||
}
|
||
if r.archives == nil {
|
||
return MergeArchive{}, RollbackPoint{}, ErrMergeArchiveNotFound
|
||
}
|
||
archiveID := firstNonEmpty(command.RollbackArchiveID, command.ArchiveID)
|
||
if archiveID == "" {
|
||
return MergeArchive{}, RollbackPoint{}, ErrMergeArchiveNotFound
|
||
}
|
||
archive, ok, err := r.archives.GetMergeArchive(ctx, archiveID)
|
||
if err != nil {
|
||
return MergeArchive{}, RollbackPoint{}, err
|
||
}
|
||
if !ok {
|
||
return MergeArchive{}, RollbackPoint{}, ErrMergeArchiveNotFound
|
||
}
|
||
if archive.RollbackPoint.ID == "" {
|
||
return MergeArchive{}, RollbackPoint{}, fmt.Errorf("%w: rollback point is required", ErrInvalidMigration)
|
||
}
|
||
return archive, cloneRollbackPoint(archive.RollbackPoint), nil
|
||
}
|
||
|
||
func normalizeMergeCmd(command MergeCmd) (MergeCmd, error) {
|
||
command.Mode = normalizeID(command.Mode)
|
||
if command.Mode == "" {
|
||
command.Mode = MergeStageDryRun
|
||
}
|
||
if !validMergeStage(command.Mode) {
|
||
return MergeCmd{}, ErrInvalidMigration
|
||
}
|
||
command.ArchiveID = normalizeID(command.ArchiveID)
|
||
command.RollbackArchiveID = normalizeID(command.RollbackArchiveID)
|
||
command.WorkflowID = firstNonEmpty(command.WorkflowID)
|
||
command.ApprovalID = firstNonEmpty(command.ApprovalID)
|
||
command.IdempotencyKey = firstNonEmpty(command.IdempotencyKey)
|
||
command.OperatorID = firstNonEmpty(command.OperatorID)
|
||
command.Reason = firstNonEmpty(command.Reason)
|
||
command.Request = normMergeReq(command.Request)
|
||
if command.Request.OperatorID == "" {
|
||
command.Request.OperatorID = command.OperatorID
|
||
}
|
||
if command.Request.Reason == "" {
|
||
command.Request.Reason = command.Reason
|
||
}
|
||
command.RollbackPoint = normRollbackPoint(command.RollbackPoint)
|
||
command.RestoreVersion = normalizeID(command.RestoreVersion)
|
||
command.Meta = normalizeStringMap(command.Meta)
|
||
return command, nil
|
||
}
|
||
|
||
func mergeArchiveMeta(command MergeCmd, extra map[string]string) map[string]string {
|
||
meta := cloneStringMap(command.Meta)
|
||
if len(meta) == 0 {
|
||
meta = make(map[string]string, len(extra)+3)
|
||
}
|
||
if command.IdempotencyKey != "" {
|
||
meta["idempotency_key"] = command.IdempotencyKey
|
||
}
|
||
if command.WorkflowID != "" {
|
||
meta["workflow_id"] = command.WorkflowID
|
||
}
|
||
if command.ApprovalID != "" {
|
||
meta["approval_id"] = command.ApprovalID
|
||
}
|
||
if command.OperatorID != "" {
|
||
meta["operator_id"] = command.OperatorID
|
||
}
|
||
if command.Reason != "" {
|
||
meta["reason"] = command.Reason
|
||
}
|
||
for key, value := range extra {
|
||
key = strings.TrimSpace(key)
|
||
value = strings.TrimSpace(value)
|
||
if key != "" && value != "" {
|
||
meta[key] = value
|
||
}
|
||
}
|
||
return normalizeStringMap(meta)
|
||
}
|
||
|
||
func rollbackRequest(command MergeCmd, point RollbackPoint, apply bool) RollbackApplyRequest {
|
||
reason := firstNonEmpty(command.Reason, point.Reason)
|
||
if apply {
|
||
reason = firstNonEmpty(reason, "merge rollback:"+point.ID)
|
||
}
|
||
return RollbackApplyRequest{
|
||
RollbackPoint: cloneRollbackPoint(point),
|
||
RestoreVersion: command.RestoreVersion,
|
||
Reason: reason,
|
||
OperatorID: firstNonEmpty(command.OperatorID, point.OperatorID),
|
||
AllowVersionMismatch: command.AllowVersionMismatch,
|
||
}
|
||
}
|
||
|
||
func rollbackArchiveID(command MergeCmd, stage string, point RollbackPoint) string {
|
||
if command.ArchiveID != "" && command.RollbackArchiveID != "" {
|
||
return command.ArchiveID
|
||
}
|
||
if command.IdempotencyKey != "" {
|
||
return command.IdempotencyKey
|
||
}
|
||
seed := firstNonEmpty(point.ID, command.RollbackArchiveID, command.ArchiveID, command.Request.ID)
|
||
if seed == "" {
|
||
return ""
|
||
}
|
||
return normalizeID(seed + "-" + stage)
|
||
}
|
||
|
||
func rollbackCmdReq(command MergeCmd, source MergeArchive) MergeRequest {
|
||
request := normMergeReq(command.Request)
|
||
if request.ID != "" {
|
||
return request
|
||
}
|
||
return normMergeReq(source.Request)
|
||
}
|
||
|
||
func rollbackModuleInput(current Plan, source MergeArchive, command MergeCmd) MergeModuleInput {
|
||
if source.Migration != nil {
|
||
input := MergeModuleInputFromPlan(current, *source.Migration)
|
||
input.Meta = mergeArchiveMeta(command, map[string]string{"rollback_archive_id": source.ArchiveID})
|
||
return input
|
||
}
|
||
return MergeModuleInput{
|
||
Request: rollbackCmdReq(command, source),
|
||
Current: clonePlan(current),
|
||
Findings: cloneClashFindings(source.Findings),
|
||
Meta: mergeArchiveMeta(command, map[string]string{"rollback_archive_id": source.ArchiveID}),
|
||
}
|
||
}
|
||
|
||
func mergeReportsOK(reports []MergeModuleRunReport) bool {
|
||
for _, report := range reports {
|
||
if !report.OK {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func cloneMigrationPtr(plan MigrationPlan) *MigrationPlan {
|
||
cloned := cloneMigrationPlan(plan)
|
||
return &cloned
|
||
}
|
||
|
||
func cloneMergeApplyRef(result MergeApplyResult) *MergeApplyResult {
|
||
cloned := cloneMergeResult(result)
|
||
return &cloned
|
||
}
|
||
|
||
func cloneApplyResultPtr(result RollbackApplyResult) *RollbackApplyResult {
|
||
cloned := cloneRollbackApply(result)
|
||
return &cloned
|
||
}
|
||
|
||
func mergeStrParam(params map[string]any, name string) (string, error) {
|
||
value, ok := params[name]
|
||
if !ok || value == nil {
|
||
return "", nil
|
||
}
|
||
switch typed := value.(type) {
|
||
case string:
|
||
return strings.TrimSpace(typed), nil
|
||
case fmt.Stringer:
|
||
return strings.TrimSpace(typed.String()), nil
|
||
case int:
|
||
return strconv.Itoa(typed), nil
|
||
case int64:
|
||
return strconv.FormatInt(typed, 10), nil
|
||
case float64:
|
||
if typed == float64(int64(typed)) {
|
||
return strconv.FormatInt(int64(typed), 10), nil
|
||
}
|
||
return "", fmt.Errorf("%w: %s must be string", ErrInvalidMigration, name)
|
||
default:
|
||
return "", fmt.Errorf("%w: %s must be string", ErrInvalidMigration, name)
|
||
}
|
||
}
|
||
|
||
func stringListParam(params map[string]any, name string) ([]string, error) {
|
||
value, ok := params[name]
|
||
if !ok || value == nil {
|
||
return nil, nil
|
||
}
|
||
switch typed := value.(type) {
|
||
case string:
|
||
if strings.TrimSpace(typed) == "" {
|
||
return nil, nil
|
||
}
|
||
return normalizeIDs(strings.Split(typed, ",")), nil
|
||
case []string:
|
||
return normalizeIDs(typed), nil
|
||
case []any:
|
||
out := make([]string, 0, len(typed))
|
||
for _, item := range typed {
|
||
text, err := mergeStrValue(item, name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, text)
|
||
}
|
||
return normalizeIDs(out), nil
|
||
default:
|
||
return nil, fmt.Errorf("%w: %s must be string list", ErrInvalidMigration, name)
|
||
}
|
||
}
|
||
|
||
func mergeBoolParam(params map[string]any, name string) (bool, error) {
|
||
value, ok := params[name]
|
||
if !ok || value == nil {
|
||
return false, nil
|
||
}
|
||
switch typed := value.(type) {
|
||
case bool:
|
||
return typed, nil
|
||
case string:
|
||
text := strings.TrimSpace(typed)
|
||
if text == "" {
|
||
return false, nil
|
||
}
|
||
parsed, err := strconv.ParseBool(text)
|
||
if err != nil {
|
||
return false, fmt.Errorf("%w: %s must be bool", ErrInvalidMigration, name)
|
||
}
|
||
return parsed, nil
|
||
default:
|
||
return false, fmt.Errorf("%w: %s must be bool", ErrInvalidMigration, name)
|
||
}
|
||
}
|
||
|
||
func mergeStrValue(value any, name string) (string, error) {
|
||
switch typed := value.(type) {
|
||
case string:
|
||
return strings.TrimSpace(typed), nil
|
||
case int:
|
||
return strconv.Itoa(typed), nil
|
||
case int64:
|
||
return strconv.FormatInt(typed, 10), nil
|
||
case float64:
|
||
if typed == float64(int64(typed)) {
|
||
return strconv.FormatInt(int64(typed), 10), nil
|
||
}
|
||
}
|
||
return "", fmt.Errorf("%w: %s must contain strings", ErrInvalidMigration, name)
|
||
}
|