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

295 lines
9.8 KiB
Go

package servergroup
import (
"context"
"fmt"
"time"
)
// MergeRequest 是一次合服操作的目标主分片、参与分片和检查范围。
type MergeRequest struct {
ID string `json:"id"`
MainShardID string `json:"main_shard_id"`
Shards []string `json:"shards"`
State string `json:"state,omitempty"`
NextVersion string `json:"next_version,omitempty"`
BlockFeatures []string `json:"block_features,omitempty"`
CheckFeatures []string `json:"check_features,omitempty"`
Reason string `json:"reason,omitempty"`
OperatorID string `json:"operator_id,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// MergeDryRun 是合服预演生成的新计划、回滚点和写路由检查结果。
type MergeDryRun struct {
Request MergeRequest `json:"request"`
Valid bool `json:"valid"`
Next Plan `json:"next"`
Rollback Plan `json:"rollback"`
RollbackPoint RollbackPoint `json:"rollback_point"`
AddedConflicts []Conflict `json:"added_conflicts,omitempty"`
WriteChecks []Target `json:"write_checks,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
// MergeApplyResult 是合服正式应用后的计划变更和执行步骤。
type MergeApplyResult struct {
Request MergeRequest `json:"request"`
Applied bool `json:"applied"`
DryRun MergeDryRun `json:"dry_run"`
Change Change `json:"change"`
Execution MergeExecutionPlan `json:"execution"`
Rollback Plan `json:"rollback"`
RollbackPoint RollbackPoint `json:"rollback_point"`
Warnings []string `json:"warnings,omitempty"`
}
// MergeExecutionPlan 描述合服执行时需要项目侧按阶段完成的步骤。
type MergeExecutionPlan struct {
ID string `json:"id"`
MainShard string `json:"main_shard"`
Shards []string `json:"shards"`
Steps []MergeExecutionStep `json:"steps"`
}
// MergeExecutionStep 是合服执行计划中的单个步骤。
type MergeExecutionStep struct {
Name string `json:"name"`
Phase string `json:"phase"`
Required bool `json:"required"`
Description string `json:"description,omitempty"`
Checks []string `json:"checks,omitempty"`
}
// DryRunMerge 预演合服计划并生成回滚点,不修改当前管理器。
func DryRunMerge(ctx context.Context, current Plan, request MergeRequest) (MergeDryRun, error) {
if err := ctxErr(ctx); err != nil {
return MergeDryRun{}, err
}
request = normMergeReq(request)
if request.ID == "" || request.MainShardID == "" || len(request.Shards) == 0 {
return MergeDryRun{}, fmt.Errorf("%w: merge request id, main shard and shards are required", ErrInvalidPlan)
}
if !containsID(request.Shards, request.MainShardID) {
return MergeDryRun{}, fmt.Errorf("%w: merge request %s does not include main shard %s", ErrInvalidPlan, request.ID, request.MainShardID)
}
normalized, _, err := normalizePlan(current)
if err != nil {
return MergeDryRun{}, err
}
next := clonePlan(normalized)
next.Version = mergeNextVersion(next.Version, request.ID, request.NextVersion)
next.UpdatedAt = firstTime(request.UpdatedAt, time.Now().UTC())
next.MergeGroups = append(next.MergeGroups, MergeGroup{
ID: request.ID,
MainShardID: request.MainShardID,
Shards: append([]string(nil), request.Shards...),
State: request.State,
UpdatedAt: request.UpdatedAt,
Meta: cloneStringMap(request.Meta),
})
addedConflicts := mergeReqConflicts(request)
next.Conflicts = append(next.Conflicts, addedConflicts...)
manager, err := New(next)
if err != nil {
return MergeDryRun{}, err
}
writeChecks, err := mergeWriteChecks(ctx, manager, request)
if err != nil {
return MergeDryRun{}, err
}
normalizedNext := manager.Snapshot()
rollbackPoint := newRollbackPoint(RollbackOperationMerge, normalized.Version, normalizedNext.Version, request.Reason, request.OperatorID, normalized, normalizedNext.UpdatedAt)
return MergeDryRun{
Request: request,
Valid: true,
Next: normalizedNext,
Rollback: normalized,
RollbackPoint: rollbackPoint,
AddedConflicts: addedConflicts,
WriteChecks: writeChecks,
Warnings: mergeWarnings(request, writeChecks),
}, nil
}
// ApplyMerge 校验并应用合服计划到管理器。
func ApplyMerge(ctx context.Context, manager *Manager, request MergeRequest) (MergeApplyResult, error) {
if manager == nil {
return MergeApplyResult{}, ErrNotFound
}
result, err := prepareMergeApply(ctx, manager.Snapshot(), request)
if err != nil {
return MergeApplyResult{}, err
}
if err := manager.Replace(ctx, result.DryRun.Next); err != nil {
return MergeApplyResult{}, err
}
return result, nil
}
func prepareMergeApply(ctx context.Context, current Plan, request MergeRequest) (MergeApplyResult, error) {
previous, _, err := normalizePlan(current)
if err != nil {
return MergeApplyResult{}, err
}
report, err := DryRunMerge(ctx, previous, request)
if err != nil {
return MergeApplyResult{}, err
}
if len(report.Warnings) > 0 {
return MergeApplyResult{}, fmt.Errorf("%w: merge has warnings: %v", ErrInvalidPlan, report.Warnings)
}
change := Change{
PreviousVersion: previous.Version,
CurrentVersion: report.Next.Version,
UpdatedAt: report.Next.UpdatedAt,
}
return MergeApplyResult{
Request: report.Request,
Applied: true,
DryRun: report,
Change: change,
Execution: mergeExecutionPlan(report.Request),
Rollback: report.Rollback,
RollbackPoint: report.RollbackPoint,
Warnings: report.Warnings,
}, nil
}
func normMergeReq(request MergeRequest) MergeRequest {
request.ID = normalizeID(request.ID)
request.MainShardID = normalizeID(request.MainShardID)
request.Shards = normalizeIDs(request.Shards)
request.State = normalizeState(request.State)
request.NextVersion = normalizeID(request.NextVersion)
request.BlockFeatures = normalizeIDs(request.BlockFeatures)
request.CheckFeatures = normalizeIDs(request.CheckFeatures)
request.Reason = firstNonEmpty(request.Reason, "merge_dry_run:"+request.ID)
request.OperatorID = firstNonEmpty(request.OperatorID)
request.Meta = normalizeStringMap(request.Meta)
request.UpdatedAt = normalizeTime(request.UpdatedAt)
return request
}
func mergeReqConflicts(request MergeRequest) []Conflict {
if len(request.BlockFeatures) == 0 {
return nil
}
out := make([]Conflict, 0, len(request.BlockFeatures)*len(request.Shards))
for _, feature := range request.BlockFeatures {
for _, shardID := range request.Shards {
if shardID == request.MainShardID {
continue
}
out = append(out, Conflict{
Feature: feature,
ShardID: shardID,
Reason: request.Reason,
})
}
}
return out
}
func mergeWriteChecks(ctx context.Context, manager *Manager, request MergeRequest) ([]Target, error) {
features := request.CheckFeatures
if len(features) == 0 {
features = request.BlockFeatures
}
if len(features) == 0 {
features = []string{"default"}
}
checks := make([]Target, 0, len(features)*len(request.Shards))
for _, feature := range features {
for _, shardID := range request.Shards {
target, ok, err := manager.ResolveWrite(ctx, feature, shardID)
if err != nil || !ok {
return nil, err
}
checks = append(checks, target)
}
}
return checks, nil
}
func mergeWarnings(request MergeRequest, checks []Target) []string {
var warnings []string
for _, check := range checks {
if check.ShardID == request.MainShardID && !check.Available {
warnings = append(warnings, "main shard write check is unavailable:"+check.Reason)
}
if check.ShardID != request.MainShardID && check.RedirectShardID != request.MainShardID {
warnings = append(warnings, "merged shard does not redirect to main:"+check.ShardID)
}
}
return warnings
}
func mergeNextVersion(currentVersion, mergeID, requested string) string {
if requested = normalizeID(requested); requested != "" {
return requested
}
currentVersion = normalizeID(currentVersion)
mergeID = normalizeID(mergeID)
switch {
case currentVersion == "":
return mergeID
case mergeID == "":
return currentVersion
default:
return currentVersion + "-" + mergeID
}
}
func mergeExecutionPlan(request MergeRequest) MergeExecutionPlan {
return MergeExecutionPlan{
ID: request.ID,
MainShard: request.MainShardID,
Shards: append([]string(nil), request.Shards...),
Steps: []MergeExecutionStep{
{
Name: "freeze_non_main_writes",
Phase: "prepare",
Required: true,
Description: "block critical writes on non-main shards before data movement",
Checks: append([]string(nil), request.BlockFeatures...),
},
{
Name: "publish_merge_group",
Phase: "route",
Required: true,
Description: "publish MergeGroup and route metadata into the server group plan",
},
{
Name: "verify_write_redirects",
Phase: "verify",
Required: true,
Description: "ResolveWrite must keep main shard writable and redirect non-main shards",
Checks: append([]string(nil), request.CheckFeatures...),
},
{
Name: "persist_rollback_snapshot",
Phase: "rollback",
Required: true,
Description: "retain DryRun.Rollback until data verification and customer support window finish",
},
{
Name: "release_blocked_features",
Phase: "finish",
Required: false,
Description: "remove temporary conflicts after data merge jobs and business verification pass",
},
},
}
}
func firstTime(values ...time.Time) time.Time {
for _, value := range values {
if !value.IsZero() {
return value.UTC()
}
}
return time.Time{}
}