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,379 @@
package adminworkflow
import (
"errors"
"fmt"
"sort"
"longheng.io/server/internal/platform/admincmd"
)
var ErrOperationRecipeInvalid = errors.New("admin operation recipe is invalid")
// OperationRecipe 描述一个可复用运营命令从参数、预览、执行到回滚证据的接入配方。
type OperationRecipe struct {
// Name 是运营配方名,和 admincmd 模板、adminworkflow 工作流名对齐。
Name string `json:"name"`
// Description 是配方用途说明。
Description string `json:"description,omitempty"`
// Source 是抽取来源类别,通常标记为成熟项目GM 或龙恒平台能力。
Source string `json:"source,omitempty"`
// Command 是对应的 admincmd 模板。
Command admincmd.Template `json:"command"`
// Workflow 是对应的 Admin 工作流控制要求。
Workflow WorkflowTemplate `json:"workflow"`
// DryRunSteps 是执行前必须产出的预览或检查步骤。
DryRunSteps []string `json:"dry_run_steps,omitempty"`
// ExecuteSteps 是审批后真正执行的步骤。
ExecuteSteps []string `json:"execute_steps,omitempty"`
// RollbackSteps 是失败或误操作后的回滚、补偿或人工修复步骤。
RollbackSteps []string `json:"rollback_steps,omitempty"`
// Notes 是项目接入时容易遗漏的工程注意事项。
Notes []string `json:"notes,omitempty"`
}
// DefaultOperationRecipes 返回从成熟项目GM 操作经验抽出的默认运营命令配方。
func DefaultOperationRecipes() []OperationRecipe {
return cloneOpRecipes(defOpRecipes)
}
// FindDefaultOperationRecipe 按名称查找默认运营命令配方。
func FindDefaultOperationRecipe(name string) (OperationRecipe, bool) {
name = normalizeWorkflowID(name)
for _, recipe := range defOpRecipes {
if recipe.Name == name {
return cloneOperationRecipe(recipe), true
}
}
return OperationRecipe{}, false
}
// ValidateOperationRecipe 校验运营命令配方是否具备命令模板、工作流控制、步骤和证据。
func ValidateOperationRecipe(recipe OperationRecipe) error {
_, err := normOpRecipe(recipe)
return err
}
// DefaultOperationCommandTemplates 返回默认运营命令配方中的 admincmd 模板。
func DefaultOperationCommandTemplates() ([]admincmd.Template, error) {
recipes := DefaultOperationRecipes()
out := make([]admincmd.Template, 0, len(recipes))
for _, recipe := range recipes {
normalized, err := normOpRecipe(recipe)
if err != nil {
return nil, err
}
out = append(out, normalized.Command)
}
return out, nil
}
// DefaultOperationWorkflowTemplates 返回默认运营命令配方中的 Admin 工作流模板。
func DefaultOperationWorkflowTemplates() ([]WorkflowTemplate, error) {
recipes := DefaultOperationRecipes()
out := make([]WorkflowTemplate, 0, len(recipes))
for _, recipe := range recipes {
normalized, err := normOpRecipe(recipe)
if err != nil {
return nil, err
}
out = append(out, normalized.Workflow)
}
return out, nil
}
var defOpRecipes = []OperationRecipe{
{
Name: "player_query",
Description: "玩家基础信息、会话和 Profile 版本查询",
Source: "成熟项目GM player/profile 查询经验",
Command: admincmd.Template{
Name: "player_query",
Operation: "player.query",
Scope: "logic",
Target: "player",
Params: []admincmd.ParamSpec{
{Name: "account_id", Type: admincmd.ParamString, Required: true},
{Name: "shard_id", Type: admincmd.ParamString},
},
},
Workflow: WorkflowTemplate{
Name: "player_query",
RequiredControls: []string{"RBAC", "read_only", "audit", "trace_id"},
Evidence: []string{"player overview snapshot", "session snapshot", "profile version"},
},
DryRunSteps: []string{"读取玩家在线状态", "读取 Profile 版本", "读取最近会话摘要"},
ExecuteSteps: []string{"返回只读查询结果", "记录审计查询"},
RollbackSteps: []string{"只读操作不需要回滚"},
Notes: []string{"查询也要审计,避免后台成为隐形数据出口"},
},
{
Name: "ban_or_unban",
Description: "封禁、解封、禁言和解除禁言",
Source: "成熟项目GM ban 操作经验",
Command: admincmd.Template{
Name: "ban_or_unban",
Operation: "moderation.sanction",
Scope: "moderation",
Target: "account",
Dangerous: true,
Params: []admincmd.ParamSpec{
{Name: "account_id", Type: admincmd.ParamString, Required: true},
{Name: "action", Type: admincmd.ParamString, Required: true, Allowed: []string{"ban", "unban", "mute", "unmute"}},
{Name: "duration_seconds", Type: admincmd.ParamInt},
{Name: "reason_code", Type: admincmd.ParamString, Required: true},
},
},
Workflow: WorkflowTemplate{
Name: "ban_or_unban",
RequiredControls: []string{"RBAC", "reason", "duration_or_policy", "dangerous_confirmation", "idempotency_key", "audit", "command_receipt"},
Evidence: []string{"moderation record", "gateway refresh", "audit query"},
},
DryRunSteps: []string{"查询当前处罚状态", "校验处罚策略", "生成影响范围预览"},
ExecuteSteps: []string{"写入处罚记录", "刷新网关会话状态", "返回 command receipt"},
RollbackSteps: []string{"按 receipt 撤销本次处罚变更", "刷新网关会话状态"},
Notes: []string{"封禁类操作必须有原因码和审计记录"},
},
{
Name: "mail_publish",
Description: "单人邮件、全服邮件和运营公告发布",
Source: "成熟项目GM mail/public_notice 操作经验",
Command: admincmd.Template{
Name: "mail_publish",
Operation: "notice.publish",
Scope: "logic",
Target: "notice",
Dangerous: true,
Params: []admincmd.ParamSpec{
{Name: "notice_id", Type: admincmd.ParamString, Required: true},
{Name: "audience", Type: admincmd.ParamString, Required: true, Allowed: []string{"account", "shard", "all"}},
{Name: "account_id", Type: admincmd.ParamString},
{Name: "shard_id", Type: admincmd.ParamString},
},
},
Workflow: WorkflowTemplate{
Name: "mail_publish",
RequiredControls: []string{"dry_run", "RBAC", "dangerous_confirmation", "idempotency_key", "audit", "command_receipt", "rollback_note"},
Evidence: []string{"audience preview", "notice publish receipt", "delivery sample"},
},
DryRunSteps: []string{"校验收件范围", "抽样展示邮件内容", "检查附件幂等键"},
ExecuteSteps: []string{"写入公告或邮件批次", "触发在线推送", "记录发放证据"},
RollbackSteps: []string{"冻结未领取附件", "撤销未生效公告", "生成客服补偿清单"},
Notes: []string{"邮件附件必须接入可靠发放 owner,不能只写后台记录"},
},
{
Name: "profile_repair",
Description: "玩家 Profile 检查、字段修复和批量修复",
Source: "成熟项目GM profile 修复经验",
Command: admincmd.Template{
Name: "profile_repair",
Operation: "profile.repair",
Scope: "logic",
Target: "player",
Dangerous: true,
Params: []admincmd.ParamSpec{
{Name: "account_id", Type: admincmd.ParamString, Required: true},
{Name: "field", Type: admincmd.ParamString, Required: true},
{Name: "mode", Type: admincmd.ParamString, Required: true, Allowed: []string{"check", "repair"}},
{Name: "expected_version", Type: admincmd.ParamString},
},
},
Workflow: WorkflowTemplate{
Name: "profile_repair",
RequiredControls: []string{"read_only_preview", "RBAC", "dangerous_confirmation", "idempotency_key", "audit", "command_receipt", "rollback_plan"},
Evidence: []string{"before profile snapshot", "admin command receipt", "EventLog entry", "after profile snapshot"},
},
DryRunSteps: []string{"读取修复前 Profile 快照", "校验字段 owner", "生成字段差异预览"},
ExecuteSteps: []string{"通过 owner 写路径修复字段", "写入 EventLog", "返回修复 receipt"},
RollbackSteps: []string{"按修复前快照生成回滚命令", "校验 Profile 版本后恢复"},
Notes: []string{"Profile 修复不能绕过 owner、幂等和 EventLog"},
},
{
Name: "hotdata_publish",
Description: "热数据发布、检查和回滚",
Source: "成熟项目GM hotdata 操作经验",
Command: admincmd.Template{
Name: "hotdata_publish",
Operation: "hotdata.publish",
Scope: "config",
Target: "hotdata",
Dangerous: true,
Params: []admincmd.ParamSpec{
{Name: "bundle_id", Type: admincmd.ParamString, Required: true},
{Name: "version", Type: admincmd.ParamString, Required: true},
{Name: "mode", Type: admincmd.ParamString, Required: true, Allowed: []string{"dry_run", "apply", "rollback"}},
},
},
Workflow: WorkflowTemplate{
Name: "hotdata_publish",
RequiredControls: []string{"dry_run", "RBAC", "dangerous_confirmation", "idempotency_key", "audit", "command_receipt", "rollback_note"},
Evidence: []string{"hotdata diff", "checksum report", "reload receipt"},
},
DryRunSteps: []string{"校验 bundle checksum", "生成配置差异", "检查目标服务 reload 能力"},
ExecuteSteps: []string{"发布热数据版本", "触发受控 reload", "采集服务确认回执"},
RollbackSteps: []string{"切回上一版本", "再次触发 reload", "验证配置 checksum"},
Notes: []string{"热数据发布必须有差异报告和回滚 intent"},
},
{
Name: "hotpatch_rollout",
Description: "代码热补丁发布、灰度和回滚",
Source: "成熟项目GM hotpatch 操作经验",
Command: admincmd.Template{
Name: "hotpatch_rollout",
Operation: "hotpatch.rollout",
Scope: "runtime",
Target: "hotpatch",
Dangerous: true,
Params: []admincmd.ParamSpec{
{Name: "patch_id", Type: admincmd.ParamString, Required: true},
{Name: "action", Type: admincmd.ParamString, Required: true, Allowed: []string{"dry_run", "rollout", "rollback"}},
{Name: "target_nodes", Type: admincmd.ParamString},
},
},
Workflow: WorkflowTemplate{
Name: "hotpatch_rollout",
RequiredControls: []string{"dry_run", "RBAC", "dangerous_confirmation", "idempotency_key", "audit", "command_receipt", "rollback_note", "health verification"},
Evidence: []string{"patch manifest", "target node list", "rollout receipt", "health after rollout"},
},
DryRunSteps: []string{"校验补丁签名和目标节点", "生成影响范围", "确认回滚补丁存在"},
ExecuteSteps: []string{"按节点灰度发布补丁", "采集健康检查", "记录 rollout receipt"},
RollbackSteps: []string{"按补丁 ID 回滚", "恢复节点健康", "输出回滚证据"},
Notes: []string{"热补丁必须先小流量灰度,不能直接全量覆盖"},
},
{
Name: "payment_replay",
Description: "支付订单查询、补单、重发和发货回放",
Source: "成熟项目GM pay_opt 操作经验",
Command: admincmd.Template{
Name: "payment_replay",
Operation: "commerce.fulfillment.replay",
Scope: "commerce",
Target: "order",
Dangerous: true,
Params: []admincmd.ParamSpec{
{Name: "order_id", Type: admincmd.ParamString, Required: true},
{Name: "channel", Type: admincmd.ParamString, Required: true},
{Name: "mode", Type: admincmd.ParamString, Required: true, Allowed: []string{"query", "replay", "compensate"}},
},
},
Workflow: WorkflowTemplate{
Name: "payment_replay",
RequiredControls: []string{"order inspect", "dry_run", "RBAC", "dangerous_confirmation", "idempotency_key", "audit", "command_receipt", "rollback_note"},
Evidence: []string{"payment order snapshot", "fulfillment idempotency record", "delivery receipt"},
},
DryRunSteps: []string{"查询订单状态", "查询发货幂等记录", "判断是否允许补偿"},
ExecuteSteps: []string{"通过发货 owner 重放", "写入补单复核记录", "返回发货 receipt"},
RollbackSteps: []string{"冻结异常发货", "生成客服人工处理单", "保留支付复核证据"},
Notes: []string{"支付补单不能绕过订单唯一键和发货幂等记录"},
},
{
Name: "servergroup_merge",
Description: "区服合服 dry-run、执行、回滚和证据归档",
Source: "成熟项目GM merge 操作经验",
Command: admincmd.Template{
Name: "servergroup_merge",
Operation: "servergroup.merge.workflow",
Scope: "servergroup",
Target: "merge",
Dangerous: true,
Params: []admincmd.ParamSpec{
{Name: "merge_id", Type: admincmd.ParamString, Required: true},
{Name: "main_shard_id", Type: admincmd.ParamString, Required: true},
{Name: "shards", Type: admincmd.ParamString, Required: true},
{Name: "mode", Type: admincmd.ParamString, Required: true, Allowed: []string{"dry_run", "apply", "rollback_dry_run", "rollback"}},
{Name: "check_features", Type: admincmd.ParamString},
{Name: "block_features", Type: admincmd.ParamString},
{Name: "rollback_archive_id", Type: admincmd.ParamString},
},
},
Workflow: WorkflowTemplate{
Name: "servergroup_merge",
RequiredControls: []string{"merge dry_run", "module report", "RBAC", "dangerous_confirmation", "idempotency_key", "audit", "command_receipt", "adminworkflow_adapter", "merge_workflow_runner", "merge_workflow_archive", "archive_store", "archive_query_api", "workflow_record_query_api", "rollback_note"},
Evidence: []string{"adminworkflow.DryRun", "adminworkflow.Record", "adminworkflow.RollbackNote", "MigrationPlan", "MergeArchive", "MergeModuleRunReport", "RollbackPoint"},
},
DryRunSteps: []string{"生成平台 MigrationPlan", "执行模块 GenDB 报告", "保存 dry-run 归档"},
ExecuteSteps: []string{"校验审批和幂等键", "执行模块 Merge 报告", "应用平台路由并保存归档"},
RollbackSteps: []string{"读取 apply 归档", "执行模块 Rollback 报告", "恢复平台 RollbackPoint"},
Notes: []string{"合服必须保留模块报告、平台回滚点和后台可查询归档"},
},
}
func normOpRecipe(recipe OperationRecipe) (OperationRecipe, error) {
recipe.Name = normalizeWorkflowID(recipe.Name)
recipe.Command = normCmdTemplate(recipe.Command)
recipe.Workflow = NormalizeWorkflowTemplate(recipe.Workflow)
recipe.DryRunSteps = normWorkflowList(recipe.DryRunSteps)
recipe.ExecuteSteps = normWorkflowList(recipe.ExecuteSteps)
recipe.RollbackSteps = normWorkflowList(recipe.RollbackSteps)
recipe.Notes = normWorkflowList(recipe.Notes)
if recipe.Name == "" {
return OperationRecipe{}, fmt.Errorf("%w: name is required", ErrOperationRecipeInvalid)
}
if recipe.Command.Name != recipe.Name {
return OperationRecipe{}, fmt.Errorf("%w: command template name must match %s", ErrOperationRecipeInvalid, recipe.Name)
}
if recipe.Workflow.Name != recipe.Name {
return OperationRecipe{}, fmt.Errorf("%w: workflow template name must match %s", ErrOperationRecipeInvalid, recipe.Name)
}
if len(recipe.DryRunSteps) == 0 || len(recipe.ExecuteSteps) == 0 || len(recipe.RollbackSteps) == 0 {
return OperationRecipe{}, fmt.Errorf("%w: %s steps are required", ErrOperationRecipeInvalid, recipe.Name)
}
if err := ValidateWorkflowTemplate(recipe.Workflow); err != nil {
return OperationRecipe{}, fmt.Errorf("%w: %w", ErrOperationRecipeInvalid, err)
}
if recipe.Command.Operation == "" || recipe.Command.Target == "" {
return OperationRecipe{}, fmt.Errorf("%w: %s command operation and target are required", ErrOperationRecipeInvalid, recipe.Name)
}
if recipe.Command.Dangerous {
controls := workflowSet(recipe.Workflow.RequiredControls)
for _, required := range []string{"dangerous_confirmation", "idempotency_key", "command_receipt"} {
if !hasWorkflowItem(controls, required) {
return OperationRecipe{}, fmt.Errorf("%w: dangerous recipe %s missing %s", ErrOperationRecipeInvalid, recipe.Name, required)
}
}
}
return recipe, nil
}
func normCmdTemplate(template admincmd.Template) admincmd.Template {
registry, err := admincmd.NewTemplateRegistry(template)
if err != nil {
return template
}
normalized, ok := registry.Get(template.Name)
if !ok {
return template
}
return normalized
}
func cloneOpRecipes(recipes []OperationRecipe) []OperationRecipe {
if len(recipes) == 0 {
return nil
}
out := make([]OperationRecipe, len(recipes))
for index, recipe := range recipes {
out[index] = cloneOperationRecipe(recipe)
}
return out
}
func cloneOperationRecipe(recipe OperationRecipe) OperationRecipe {
recipe.Command.Params = append([]admincmd.ParamSpec(nil), recipe.Command.Params...)
recipe.Workflow.RequiredControls = append([]string(nil), recipe.Workflow.RequiredControls...)
recipe.Workflow.Evidence = append([]string(nil), recipe.Workflow.Evidence...)
recipe.DryRunSteps = append([]string(nil), recipe.DryRunSteps...)
recipe.ExecuteSteps = append([]string(nil), recipe.ExecuteSteps...)
recipe.RollbackSteps = append([]string(nil), recipe.RollbackSteps...)
recipe.Notes = append([]string(nil), recipe.Notes...)
return recipe
}
func sortedOpNames(recipes []OperationRecipe) []string {
names := make([]string, 0, len(recipes))
for _, recipe := range recipes {
names = append(names, recipe.Name)
}
sort.Strings(names)
return names
}