DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,656 @@
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)
}