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,402 @@
package servergroup
import (
"context"
"errors"
"fmt"
)
// ErrInvalidMigration 表示合服迁移计划缺少步骤、阶段或回滚信息。
var ErrInvalidMigration = errors.New("server group migration plan is invalid")
const (
// MigrationSeverityInfo 表示迁移检查发现普通提示。
MigrationSeverityInfo = "info"
// MigrationSeverityWarning 表示迁移检查发现警告。
MigrationSeverityWarning = "warning"
// MigrationSeverityBlocker 表示迁移检查发现阻断项。
MigrationSeverityBlocker = "blocker"
)
const (
// MigrationPhasePrepare 表示合服迁移准备阶段。
MigrationPhasePrepare = "prepare"
// MigrationPhaseMigrate 表示业务数据迁移阶段。
MigrationPhaseMigrate = "migrate"
// MigrationPhaseRoute 表示路由发布阶段。
MigrationPhaseRoute = "route"
// MigrationPhaseVerify 表示合服结果验证阶段。
MigrationPhaseVerify = "verify"
// MigrationPhaseRollback 表示回滚阶段。
MigrationPhaseRollback = "rollback"
// MigrationPhaseFinish 表示收尾阶段。
MigrationPhaseFinish = "finish"
)
// MigrationStep 只描述通用迁移控制动作,不承载具体项目表结构或业务数据。
type MigrationStep struct {
Name string `json:"name"`
Phase string `json:"phase"`
Target string `json:"target,omitempty"`
Owner string `json:"owner,omitempty"`
Required bool `json:"required"`
Description string `json:"description,omitempty"`
Checks []string `json:"checks,omitempty"`
Rollback []string `json:"rollback,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
// ConflictCheckRequest 是合服冲突检查器的输入快照。
type ConflictCheckRequest struct {
Current Plan `json:"current"`
DryRun MergeDryRun `json:"dry_run"`
Steps []MigrationStep `json:"steps"`
}
// ConflictFinding 是合服冲突检查器返回的单条发现。
type ConflictFinding struct {
Code string `json:"code"`
Severity string `json:"severity"`
Subject string `json:"subject,omitempty"`
Detail string `json:"detail,omitempty"`
}
// ConflictChecker 定义合服迁移计划的冲突检查接口。
type ConflictChecker interface {
CheckMerge(context.Context, ConflictCheckRequest) ([]ConflictFinding, error)
}
// ConflictCheckFunc 将函数适配为 ConflictChecker。
type ConflictCheckFunc func(context.Context, ConflictCheckRequest) ([]ConflictFinding, error)
// CheckMerge 执行函数型合服冲突检查器。
func (f ConflictCheckFunc) CheckMerge(ctx context.Context, request ConflictCheckRequest) ([]ConflictFinding, error) {
if f == nil {
return nil, nil
}
return f(ctx, request)
}
// MigrationPlan 是合服迁移预演、步骤、发现和回滚点的组合计划。
type MigrationPlan struct {
Request MergeRequest `json:"request"`
DryRun MergeDryRun `json:"dry_run"`
Steps []MigrationStep `json:"steps"`
Findings []ConflictFinding `json:"findings,omitempty"`
Ready bool `json:"ready"`
Rollback Plan `json:"rollback"`
RollbackPoint RollbackPoint `json:"rollback_point"`
}
// DefaultMergeMigrationSteps 返回框架内置的合服迁移步骤模板。
func DefaultMergeMigrationSteps() []MigrationStep {
return []MigrationStep{
{
Name: "freeze_non_main_writes",
Phase: MigrationPhasePrepare,
Target: "shards",
Required: true,
Description: "冻结非主分片关键写入,避免迁移窗口产生新冲突",
Checks: []string{"ResolveWrite", "critical_write_blocked"},
Rollback: []string{"release_write_block"},
},
{
Name: "capture_rollback_snapshot",
Phase: MigrationPhasePrepare,
Target: "servergroup_plan",
Required: true,
Description: "保存 dry-run rollback 快照,供验证失败或回滚演练使用",
Checks: []string{"rollback_snapshot_persisted"},
Rollback: []string{"restore_rollback_snapshot"},
},
{
Name: "migrate_generic_state",
Phase: MigrationPhaseMigrate,
Target: "runtime_state",
Required: true,
Description: "按项目提供的迁移器搬迁通用状态,核心只约束幂等和校验点",
Checks: []string{"idempotent_migration_job", "conflict_report_empty"},
Rollback: []string{"run_project_rollback_job"},
},
{
Name: "publish_merge_routes",
Phase: MigrationPhaseRoute,
Target: "servergroup_plan",
Required: true,
Description: "发布 MergeGroup 和路由计划,让非主分片写入重定向到主分片",
Checks: []string{"ResolveWrite_redirects"},
Rollback: []string{"restore_previous_routes"},
},
{
Name: "verify_reads_and_writes",
Phase: MigrationPhaseVerify,
Target: "runtime_state",
Required: true,
Description: "校验读写目标、冲突报告、审计回执和抽样数据",
Checks: []string{"read_sample_passed", "write_redirect_passed", "audit_receipt_saved"},
Rollback: []string{"restore_rollback_snapshot"},
},
{
Name: "release_temporary_blocks",
Phase: MigrationPhaseFinish,
Target: "shards",
Required: false,
Description: "验证窗口结束后释放临时冲突和冻结项",
Checks: []string{"operator_approved"},
},
}
}
// BuildMergeMigrationPlan 构建合服迁移计划并运行冲突检查。
func BuildMergeMigrationPlan(ctx context.Context, current Plan, request MergeRequest, steps []MigrationStep, checkers ...ConflictChecker) (MigrationPlan, error) {
if err := ctxErr(ctx); err != nil {
return MigrationPlan{}, err
}
normalized, _, err := normalizePlan(current)
if err != nil {
return MigrationPlan{}, err
}
dryRun, err := DryRunMerge(ctx, normalized, request)
if err != nil {
return MigrationPlan{}, err
}
if len(steps) == 0 {
steps = DefaultMergeMigrationSteps()
}
steps = cloneMigrationSteps(steps)
if err := ValidateMigrationSteps(steps); err != nil {
return MigrationPlan{}, err
}
checkRequest := ConflictCheckRequest{
Current: clonePlan(normalized),
DryRun: cloneMergeDryRun(dryRun),
Steps: cloneMigrationSteps(steps),
}
checkers = append([]ConflictChecker{DefaultConflictChecker()}, checkers...)
var findings []ConflictFinding
for _, checker := range checkers {
if checker == nil {
continue
}
found, err := checker.CheckMerge(ctx, cloneConflictReq(checkRequest))
if err != nil {
return MigrationPlan{}, err
}
for _, finding := range found {
normalizedFinding, err := normConflict(finding)
if err != nil {
return MigrationPlan{}, err
}
findings = append(findings, normalizedFinding)
}
}
return MigrationPlan{
Request: dryRun.Request,
DryRun: cloneMergeDryRun(dryRun),
Steps: cloneMigrationSteps(steps),
Findings: cloneClashFindings(findings),
Ready: dryRun.Valid && len(dryRun.Warnings) == 0 && !hasBlockerFinding(findings),
Rollback: clonePlan(dryRun.Rollback),
RollbackPoint: cloneRollbackPoint(dryRun.RollbackPoint),
}, nil
}
// DefaultConflictChecker 返回框架内置的合服冲突检查器。
func DefaultConflictChecker() ConflictChecker {
return ConflictCheckFunc(func(_ context.Context, request ConflictCheckRequest) ([]ConflictFinding, error) {
var findings []ConflictFinding
if request.DryRun.RollbackPoint.ID == "" || request.DryRun.RollbackPoint.Snapshot.Version != request.DryRun.Rollback.Version {
findings = append(findings, ConflictFinding{
Code: "rollback_point_missing",
Severity: MigrationSeverityBlocker,
Subject: request.DryRun.Request.ID,
Detail: "merge dry-run must carry a rollback point matching the rollback snapshot",
})
}
for _, warning := range request.DryRun.Warnings {
findings = append(findings, ConflictFinding{
Code: "write_check_warning",
Severity: MigrationSeverityBlocker,
Subject: request.DryRun.Request.ID,
Detail: warning,
})
}
for _, check := range request.DryRun.WriteChecks {
switch {
case check.ShardID == request.DryRun.Request.MainShardID && !check.Available:
findings = append(findings, ConflictFinding{
Code: "main_shard_unavailable",
Severity: MigrationSeverityBlocker,
Subject: check.ShardID,
Detail: check.Reason,
})
case check.ShardID != request.DryRun.Request.MainShardID && check.RedirectShardID != request.DryRun.Request.MainShardID:
findings = append(findings, ConflictFinding{
Code: "merged_shard_not_redirected",
Severity: MigrationSeverityBlocker,
Subject: check.ShardID,
Detail: "merged shard writes must redirect to the main shard",
})
}
}
if !hasMigrationPhase(request.Steps, MigrationPhaseRollback) && !hasRollbackActions(request.Steps) {
findings = append(findings, ConflictFinding{
Code: "rollback_phase_missing",
Severity: MigrationSeverityWarning,
Subject: request.DryRun.Request.ID,
Detail: "migration steps should include an explicit rollback phase or rollback actions on required steps",
})
}
return findings, nil
})
}
// ValidateMigrationSteps 校验迁移步骤名称、阶段和必填检查。
func ValidateMigrationSteps(steps []MigrationStep) error {
if len(steps) == 0 {
return fmt.Errorf("%w: at least one step is required", ErrInvalidMigration)
}
seen := make(map[string]struct{}, len(steps))
for _, step := range steps {
step = normMigrationStep(step)
if step.Name == "" || step.Phase == "" {
return fmt.Errorf("%w: step name and phase are required", ErrInvalidMigration)
}
if _, exists := seen[step.Name]; exists {
return fmt.Errorf("%w: duplicate step %s", ErrInvalidMigration, step.Name)
}
seen[step.Name] = struct{}{}
if !validMigrationPhase(step.Phase) {
return fmt.Errorf("%w: step %s has invalid phase %s", ErrInvalidMigration, step.Name, step.Phase)
}
if step.Required && step.Description == "" && len(step.Checks) == 0 && len(step.Rollback) == 0 {
return fmt.Errorf("%w: required step %s must describe checks or rollback", ErrInvalidMigration, step.Name)
}
}
return nil
}
func normMigrationStep(step MigrationStep) MigrationStep {
step.Name = normalizeID(step.Name)
step.Phase = normalizeID(step.Phase)
step.Target = normalizeID(step.Target)
step.Owner = normalizeID(step.Owner)
step.Description = firstNonEmpty(step.Description)
step.Checks = normalizeIDs(step.Checks)
step.Rollback = normalizeIDs(step.Rollback)
step.Meta = normalizeStringMap(step.Meta)
return step
}
func validMigrationPhase(phase string) bool {
switch phase {
case MigrationPhasePrepare, MigrationPhaseMigrate, MigrationPhaseRoute, MigrationPhaseVerify, MigrationPhaseRollback, MigrationPhaseFinish:
return true
default:
return false
}
}
func normConflict(finding ConflictFinding) (ConflictFinding, error) {
finding.Code = normalizeID(finding.Code)
finding.Severity = normalizeID(finding.Severity)
finding.Subject = normalizeID(finding.Subject)
finding.Detail = firstNonEmpty(finding.Detail)
if finding.Code == "" {
return ConflictFinding{}, fmt.Errorf("%w: finding code is required", ErrInvalidMigration)
}
if finding.Severity == "" {
finding.Severity = MigrationSeverityWarning
}
if !validFindingSeverity(finding.Severity) {
return ConflictFinding{}, fmt.Errorf("%w: finding %s has invalid severity %s", ErrInvalidMigration, finding.Code, finding.Severity)
}
return finding, nil
}
func validFindingSeverity(severity string) bool {
switch severity {
case MigrationSeverityInfo, MigrationSeverityWarning, MigrationSeverityBlocker:
return true
default:
return false
}
}
func hasBlockerFinding(findings []ConflictFinding) bool {
for _, finding := range findings {
if finding.Severity == MigrationSeverityBlocker {
return true
}
}
return false
}
func hasMigrationPhase(steps []MigrationStep, phase string) bool {
phase = normalizeID(phase)
for _, step := range steps {
if normalizeID(step.Phase) == phase {
return true
}
}
return false
}
func hasRollbackActions(steps []MigrationStep) bool {
for _, step := range steps {
if len(step.Rollback) > 0 {
return true
}
}
return false
}
func cloneMigrationSteps(steps []MigrationStep) []MigrationStep {
if len(steps) == 0 {
return nil
}
out := make([]MigrationStep, len(steps))
for i, step := range steps {
out[i] = normMigrationStep(step)
}
return out
}
func cloneClashFindings(findings []ConflictFinding) []ConflictFinding {
if len(findings) == 0 {
return nil
}
return append([]ConflictFinding(nil), findings...)
}
func cloneConflictReq(request ConflictCheckRequest) ConflictCheckRequest {
return ConflictCheckRequest{
Current: clonePlan(request.Current),
DryRun: cloneMergeDryRun(request.DryRun),
Steps: cloneMigrationSteps(request.Steps),
}
}
func cloneMergeDryRun(dryRun MergeDryRun) MergeDryRun {
dryRun.Request = normMergeReq(dryRun.Request)
dryRun.Next = clonePlan(dryRun.Next)
dryRun.Rollback = clonePlan(dryRun.Rollback)
dryRun.RollbackPoint = cloneRollbackPoint(dryRun.RollbackPoint)
dryRun.AddedConflicts = append([]Conflict(nil), dryRun.AddedConflicts...)
dryRun.WriteChecks = cloneTargets(dryRun.WriteChecks)
dryRun.Warnings = append([]string(nil), dryRun.Warnings...)
return dryRun
}
func cloneTargets(targets []Target) []Target {
if len(targets) == 0 {
return nil
}
out := make([]Target, len(targets))
for i, target := range targets {
target.Shards = append([]string(nil), target.Shards...)
target.Meta = cloneStringMap(target.Meta)
out[i] = target
}
return out
}