Files
90CNServer/server/internal/platform/servergroup/merge_recipe.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

342 lines
17 KiB
Go

package servergroup
import (
"fmt"
"sort"
)
// MergeModuleRecipe 描述项目侧实现一个合服模块时必须准备的输入、阶段动作、证据和验收项。
type MergeModuleRecipe struct {
// Name 是模块名,和 MergeModuleTemplate、MergeModuleSpec 对齐。
Name string `json:"name"`
// Description 是模块职责说明。
Description string `json:"description,omitempty"`
// Features 是模块覆盖的合服 feature。
Features []string `json:"features,omitempty"`
// Required 表示正式项目是否必须实现该模块。
Required bool `json:"required"`
// Inputs 是实现该模块前必须能读取或生成的数据输入。
Inputs []string `json:"inputs,omitempty"`
// Phases 描述 gendb、merge、rollback 三个阶段的实现要求。
Phases []MergePhaseRecipe `json:"phases,omitempty"`
// Acceptance 是上线前验收该模块实现时必须检查的结果。
Acceptance []string `json:"acceptance,omitempty"`
// Notes 是接入时容易漏掉的工程注意事项。
Notes []string `json:"notes,omitempty"`
}
// MergePhaseRecipe 描述单个阶段的目标、步骤、证据和失败策略。
type MergePhaseRecipe struct {
// Phase 是阶段名,取值为 gendb、merge 或 rollback。
Phase string `json:"phase"`
// Goal 是该阶段要达成的目标。
Goal string `json:"goal,omitempty"`
// Steps 是项目侧处理器应该完成的主要步骤。
Steps []string `json:"steps,omitempty"`
// Evidence 是该阶段必须输出到 MergeModuleResult.Evidence 或归档系统的证据。
Evidence []string `json:"evidence,omitempty"`
// FailurePolicy 描述失败时应该返回 error 还是 blocker finding。
FailurePolicy string `json:"failure_policy,omitempty"`
}
type mergeRecipeDetail struct {
Inputs []string
GenDB []string
Merge []string
Rollback []string
Acceptance []string
Notes []string
}
// MergeModuleRecipes 返回从成熟项目合服模块经验抽出的默认实现配方,调用方可以安全修改返回值。
func MergeModuleRecipes() []MergeModuleRecipe {
templates := MergeModuleTemplates()
recipes := make([]MergeModuleRecipe, 0, len(templates))
for _, template := range templates {
recipe, err := MergeRecipeFromTemplate(template)
if err != nil {
continue
}
recipes = append(recipes, recipe)
}
return cloneMergeRecipes(recipes)
}
// FindMergeRecipe 按模块名查找默认合服模块实现配方。
func FindMergeRecipe(name string) (MergeModuleRecipe, bool) {
name = normalizeID(name)
if name == "" {
return MergeModuleRecipe{}, false
}
for _, template := range defModuleTpls {
if template.Name != name {
continue
}
recipe, err := MergeRecipeFromTemplate(template)
if err != nil {
return MergeModuleRecipe{}, false
}
return cloneMergeRecipe(recipe), true
}
return MergeModuleRecipe{}, false
}
// MergeRecipeFromTemplate 把默认模板转换成项目侧实现配方。
func MergeRecipeFromTemplate(template MergeModuleTemplate) (MergeModuleRecipe, error) {
template, err := normModuleTpl(template)
if err != nil {
return MergeModuleRecipe{}, err
}
detail, ok := defaultRecipeDetails[template.Name]
if !ok {
detail = genericRecipeDetail(template)
}
recipe := MergeModuleRecipe{
Name: template.Name,
Description: template.Description,
Features: append([]string(nil), template.Features...),
Required: template.Required,
Inputs: detail.Inputs,
Phases: []MergePhaseRecipe{
{
Phase: MergeModulePhaseGenDB,
Goal: "生成合服前置数据、冲突方案和 dry-run 证据",
Steps: append(cloneMergeStrings(detail.GenDB), template.Checks...),
Evidence: append(cloneMergeStrings(template.Evidence), "gendb 阶段批次号", "抽样校验输入"),
FailurePolicy: "可修复风险返回 blocker finding;读取失败、生成失败或证据落盘失败返回 error",
},
{
Phase: MergeModulePhaseMerge,
Goal: "在审批后的合服窗口内执行幂等业务合并",
Steps: detail.Merge,
Evidence: append(cloneMergeStrings(template.Evidence), "merge 阶段批次号", "写入数量和抽样校验摘要"),
FailurePolicy: "业务写入失败必须返回 error;不能把半完成写入伪装成 warning",
},
{
Phase: MergeModulePhaseRollback,
Goal: "按归档和回滚点执行业务回滚或补偿",
Steps: append(cloneMergeStrings(detail.Rollback), template.Rollback...),
Evidence: append(cloneMergeStrings(template.Rollback), "rollback 阶段批次号", "恢复后抽样校验摘要"),
FailurePolicy: "回滚失败必须返回 error,并保留可人工继续处理的证据引用",
},
},
Acceptance: detail.Acceptance,
Notes: detail.Notes,
}
return normMergeRecipe(recipe)
}
// ValidateMergeModuleRecipe 校验项目侧自定义配方是否包含三段式实现要求。
func ValidateMergeModuleRecipe(recipe MergeModuleRecipe) error {
_, err := normMergeRecipe(recipe)
return err
}
var defaultRecipeDetails = map[string]mergeRecipeDetail{
MergeModuleNameProfile: {
Inputs: []string{"Profile 快照", "角色名唯一索引", "账号绑定关系", "玩家镜像索引", "清理白名单"},
GenDB: []string{"批量读取源服和目标服玩家档案", "生成角色重名改名方案", "计算账号清理候选并套用白名单", "生成玩家镜像重建计划"},
Merge: []string{"按幂等批次写入目标 Profile", "重建角色名唯一索引", "重建账号到玩家镜像", "记录清理或保留结果"},
Rollback: []string{"读取 Profile 回滚快照", "恢复角色名唯一索引", "恢复账号到玩家镜像", "撤销误清理账号"},
Acceptance: []string{"角色名索引全局唯一", "账号绑定能定位到主服玩家", "抽样 Profile 可读取且版本正确", "清理名单和保留名单可审计"},
Notes: []string{"名字冲突必须在 gendb 阶段生成确定方案", "清理策略必须可配置并写入证据"},
},
MergeModuleNameMail: {
Inputs: []string{"个人邮件快照", "全服邮件快照", "附件发放记录", "补偿邮件批次"},
GenDB: []string{"统计未领取邮件和附件", "生成全服邮件保留策略", "生成补偿邮件去重键", "冻结合服窗口新增邮件策略"},
Merge: []string{"按批次迁移个人邮件", "保留或重建全服邮件可见关系", "校验附件发放幂等记录", "写入补偿邮件批次"},
Rollback: []string{"删除合服批次新增邮件", "恢复旧服邮件索引", "冻结异常附件领取"},
Acceptance: []string{"未领取邮件不丢失", "附件不可重复领取", "补偿邮件幂等键可复核", "全服邮件可见范围正确"},
Notes: []string{"邮件附件属于关键写路径,不能绕过幂等和发放 owner"},
},
MergeModuleNameLeaderboard: {
Inputs: []string{"本地榜快照", "跨服榜快照", "赛季窗口", "排行榜补充信息"},
GenDB: []string{"冻结或确认赛季结算窗口", "生成榜单重算计划", "检查同玩家多榜去重策略", "生成榜单补充信息校验集合"},
Merge: []string{"按赛季和榜单维度重算排名", "写入榜单补充信息", "记录榜单重算批次", "抽样校验排序稳定性"},
Rollback: []string{"恢复旧榜快照", "撤销新榜重算批次", "恢复榜单补充信息"},
Acceptance: []string{"榜单排序稳定", "同玩家去重策略生效", "赛季结算窗口无交叉写入", "抽样榜单和补充信息一致"},
Notes: []string{"排行榜重算应输出 before/after 摘要,便于运营复核"},
},
MergeModuleNameGuild: {
Inputs: []string{"公会快照", "公会名唯一索引", "公会成员关系", "公会镜像", "公会榜快照"},
GenDB: []string{"生成公会重名改名方案", "校验会长和成员关系", "生成公会镜像重建计划", "确认公会活动结算窗口"},
Merge: []string{"迁移公会主体和成员关系", "重建公会名唯一索引", "重建公会镜像", "重算公会榜"},
Rollback: []string{"恢复公会主体快照", "恢复公会名索引", "恢复公会镜像", "撤销公会榜重算"},
Acceptance: []string{"公会名索引唯一", "会长和成员关系完整", "公会镜像可查询", "公会榜抽样正确"},
Notes: []string{"公会成员关系和公会榜必须同批次留证,避免只迁主表"},
},
MergeModuleNameFriend: {
Inputs: []string{"好友双向边快照", "黑名单快照", "最近联系人", "好友活动记录"},
GenDB: []string{"检查好友双向边一致性", "确认黑名单优先级", "生成好友活动合并计划", "识别跨服好友引用"},
Merge: []string{"合并好友图谱", "合并黑名单和最近联系人", "迁移好友活动记录", "抽样校验双向边"},
Rollback: []string{"恢复好友图谱快照", "恢复黑名单和最近联系人", "撤销好友活动批次"},
Acceptance: []string{"好友双向边一致", "黑名单优先级正确", "好友活动记录可追踪", "跨服引用有明确处理策略"},
Notes: []string{"好友模块应避免只写单向边"},
},
MergeModuleNameAuction: {
Inputs: []string{"拍卖订单", "竞价记录", "流拍记录", "战区归属"},
GenDB: []string{"冻结拍卖结算窗口", "校验竞价订单幂等键", "生成流拍记录去重计划", "确认战区归属变更"},
Merge: []string{"迁移未结算拍卖", "迁移竞价和流拍记录", "重建战区拍卖归属", "记录结算冻结解除点"},
Rollback: []string{"恢复拍卖快照", "撤销合服后竞价写入", "恢复战区拍卖归属"},
Acceptance: []string{"拍卖结算窗口无并发写入", "竞价订单不重复", "流拍记录去重正确", "战区归属可复核"},
Notes: []string{"拍卖和支付订单交叉时必须明确 owner"},
},
MergeModuleNameActivity: {
Inputs: []string{"活动状态", "活动榜", "奖励领取记录", "活动开放窗口"},
GenDB: []string{"确认活动开放和结算窗口", "生成活动状态去重计划", "生成活动榜重算计划", "检查奖励领取幂等记录"},
Merge: []string{"合并活动状态", "重算活动榜", "合并奖励领取记录", "记录活动批次和窗口"},
Rollback: []string{"恢复活动状态快照", "撤销活动榜重算", "冻结异常奖励领取"},
Acceptance: []string{"活动窗口正确", "活动状态无重复", "奖励领取不重复", "活动榜抽样正确"},
Notes: []string{"活动模块必须区分已结算、结算中和未开启状态"},
},
MergeModuleNamePayment: {
Inputs: []string{"支付订单", "IAP 订单", "发货记录", "补单复核单"},
GenDB: []string{"检查订单唯一键冲突", "统计已发货和未完成订单", "冻结合服窗口补单", "生成补单复核清单"},
Merge: []string{"迁移订单索引", "迁移发货幂等记录", "保留未完成订单冻结状态", "输出补单复核结果"},
Rollback: []string{"恢复订单索引", "冻结合服窗口补单", "恢复发货幂等记录"},
Acceptance: []string{"订单唯一键不冲突", "已发货订单不可重复发货", "未完成订单状态可复核", "补单清单可人工追踪"},
Notes: []string{"支付模块失败必须走 error 和人工复核,不能只给 warning"},
},
MergeModuleNameServerInfo: {
Inputs: []string{"来源区服记录", "目标区服版本", "路由元数据", "合服批次号"},
GenDB: []string{"校验来源区服和主服关系", "生成合服批次号", "检查路由元数据一致性", "生成服务端公告和查询映射"},
Merge: []string{"写入合服批次记录", "写入来源区服映射", "更新路由元数据", "输出合服完成摘要"},
Rollback: []string{"恢复合服信息快照", "恢复旧路由元数据", "标记合服批次回滚"},
Acceptance: []string{"来源区服能映射到主服", "路由元数据版本正确", "合服批次号幂等", "查询面能展示当前合服关系"},
Notes: []string{"合服信息是运营排障入口,必须和平台路由归档互相引用"},
},
}
func genericRecipeDetail(template MergeModuleTemplate) mergeRecipeDetail {
return mergeRecipeDetail{
Inputs: []string{template.Name + " 模块快照", template.Name + " 模块索引", "合服批次号"},
GenDB: []string{"读取模块快照", "生成冲突清单", "生成抽样校验集合"},
Merge: []string{"按幂等批次写入目标模块", "重建模块索引", "输出写入摘要"},
Rollback: []string{"读取模块回滚快照", "恢复模块索引", "输出恢复摘要"},
Acceptance: []string{"模块索引可查询", "抽样数据一致", "批次号可审计"},
Notes: []string{"自定义模块必须声明 feature,并接入 gendb、merge、rollback 三段处理器"},
}
}
func normMergeRecipe(recipe MergeModuleRecipe) (MergeModuleRecipe, error) {
recipe.Name = normalizeID(recipe.Name)
recipe.Description = firstNonEmpty(recipe.Description)
recipe.Features = normalizeIDs(recipe.Features)
recipe.Inputs = cloneMergeStrings(recipe.Inputs)
recipe.Acceptance = cloneMergeStrings(recipe.Acceptance)
recipe.Notes = cloneMergeStrings(recipe.Notes)
if recipe.Name == "" {
return MergeModuleRecipe{}, fmt.Errorf("%w: recipe name is required", ErrMergeModuleInvalid)
}
if len(recipe.Features) == 0 {
return MergeModuleRecipe{}, fmt.Errorf("%w: recipe %s features are required", ErrMergeModuleInvalid, recipe.Name)
}
if len(recipe.Inputs) == 0 {
return MergeModuleRecipe{}, fmt.Errorf("%w: recipe %s inputs are required", ErrMergeModuleInvalid, recipe.Name)
}
if len(recipe.Acceptance) == 0 {
return MergeModuleRecipe{}, fmt.Errorf("%w: recipe %s acceptance items are required", ErrMergeModuleInvalid, recipe.Name)
}
phases, err := normPhaseRecipes(recipe.Phases)
if err != nil {
return MergeModuleRecipe{}, err
}
recipe.Phases = phases
for _, phase := range []string{MergeModulePhaseGenDB, MergeModulePhaseMerge, MergeModulePhaseRollback} {
if !recipeHasPhase(recipe.Phases, phase) {
return MergeModuleRecipe{}, fmt.Errorf("%w: recipe %s missing %s phase", ErrMergeModuleInvalid, recipe.Name, phase)
}
}
return recipe, nil
}
func normPhaseRecipes(phases []MergePhaseRecipe) ([]MergePhaseRecipe, error) {
if len(phases) == 0 {
return nil, fmt.Errorf("%w: recipe phases are required", ErrMergeModuleInvalid)
}
seen := make(map[string]struct{}, len(phases))
out := make([]MergePhaseRecipe, 0, len(phases))
for _, phase := range phases {
phase.Phase = normalizeID(phase.Phase)
phase.Goal = firstNonEmpty(phase.Goal)
phase.Steps = cloneMergeStrings(phase.Steps)
phase.Evidence = cloneMergeStrings(phase.Evidence)
phase.FailurePolicy = firstNonEmpty(phase.FailurePolicy)
if !validMergePhase(phase.Phase) {
return nil, fmt.Errorf("%w: recipe phase %s is invalid", ErrMergeModuleInvalid, phase.Phase)
}
if _, exists := seen[phase.Phase]; exists {
return nil, fmt.Errorf("%w: recipe phase %s duplicated", ErrMergeModuleInvalid, phase.Phase)
}
if phase.Goal == "" || len(phase.Steps) == 0 || len(phase.Evidence) == 0 || phase.FailurePolicy == "" {
return nil, fmt.Errorf("%w: recipe phase %s is incomplete", ErrMergeModuleInvalid, phase.Phase)
}
seen[phase.Phase] = struct{}{}
out = append(out, phase)
}
sort.Slice(out, func(i, j int) bool {
return recipePhaseOrder(out[i].Phase) < recipePhaseOrder(out[j].Phase)
})
return out, nil
}
func recipeHasPhase(phases []MergePhaseRecipe, phase string) bool {
phase = normalizeID(phase)
for _, item := range phases {
if item.Phase == phase {
return true
}
}
return false
}
func recipePhaseOrder(phase string) int {
switch phase {
case MergeModulePhaseGenDB:
return 1
case MergeModulePhaseMerge:
return 2
case MergeModulePhaseRollback:
return 3
default:
return 100
}
}
func cloneMergeRecipes(recipes []MergeModuleRecipe) []MergeModuleRecipe {
if len(recipes) == 0 {
return nil
}
out := make([]MergeModuleRecipe, len(recipes))
for i, recipe := range recipes {
out[i] = cloneMergeRecipe(recipe)
}
return out
}
func cloneMergeRecipe(recipe MergeModuleRecipe) MergeModuleRecipe {
recipe.Features = append([]string(nil), recipe.Features...)
recipe.Inputs = cloneMergeStrings(recipe.Inputs)
recipe.Phases = clonePhaseRecipes(recipe.Phases)
recipe.Acceptance = cloneMergeStrings(recipe.Acceptance)
recipe.Notes = cloneMergeStrings(recipe.Notes)
return recipe
}
func clonePhaseRecipes(phases []MergePhaseRecipe) []MergePhaseRecipe {
if len(phases) == 0 {
return nil
}
out := make([]MergePhaseRecipe, len(phases))
for i, phase := range phases {
out[i] = MergePhaseRecipe{
Phase: phase.Phase,
Goal: phase.Goal,
Steps: cloneMergeStrings(phase.Steps),
Evidence: cloneMergeStrings(phase.Evidence),
FailurePolicy: phase.FailurePolicy,
}
}
return out
}