Files
90CNServer/server/internal/platform/servergroup/merge_workflow.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

657 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}