Files
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

381 lines
19 KiB
Go

package readiness
import (
"fmt"
"sort"
"strings"
)
type Plan struct {
SchemaVersion int `json:"schema_version"`
Posture string `json:"posture"`
Scenarios []Scenario `json:"scenarios"`
Modules []Module `json:"modules"`
Framework []Control `json:"framework"`
Operations []Control `json:"operations"`
Security []Control `json:"security"`
Ecosystem []Control `json:"ecosystem"`
Evidence []Evidence `json:"evidence"`
ReleaseGates []Gate `json:"release_gates"`
Gaps []Gap `json:"gaps,omitempty"`
}
type Scenario struct {
Name string `json:"name"`
Tier string `json:"tier"`
Status string `json:"status"`
Signals []string `json:"signals"`
Commands []string `json:"commands,omitempty"`
ExitGate string `json:"exit_gate"`
Owner string `json:"owner"`
NextAction string `json:"next_action,omitempty"`
}
type Module struct {
Name string `json:"name"`
Depth string `json:"depth"`
MustProve []string `json:"must_prove"`
NextAction string `json:"next_action"`
}
type Control struct {
Name string `json:"name"`
Status string `json:"status"`
Boundary string `json:"boundary"`
NextAction string `json:"next_action"`
}
type Evidence struct {
Name string `json:"name"`
Status string `json:"status"`
Reports []string `json:"reports,omitempty"`
Commands []string `json:"commands,omitempty"`
Owner string `json:"owner"`
}
type Gate struct {
Name string `json:"name"`
Tier string `json:"tier"`
Requires []string `json:"requires"`
Decision string `json:"decision"`
}
type Gap struct {
Area string `json:"area"`
Name string `json:"name"`
Reason string `json:"reason"`
}
func DefaultPlan() Plan {
plan := Plan{
SchemaVersion: 1,
Posture: "engineering-ready; production maturity requires evidence-backed soak, chaos, and operator drills",
Scenarios: []Scenario{
{
Name: "single-node functional acceptance",
Tier: "P0",
Status: "automated",
Signals: []string{"go test ./...", "go vet ./...", "check -roadmap", "admin_snapshot"},
Commands: []string{
"python scripts/release_gate.py --profile local",
"go test -count=1 ./...",
"go vet ./...",
"go run ./cmd/check -roadmap",
"python scripts/admin_snapshot.py --strict --json-out reports/admin-snapshot.json --markdown-out reports/admin-snapshot.md",
},
ExitGate: "all commands pass on every release candidate",
Owner: "platform",
},
{
Name: "multi-node gateway logic replay",
Tier: "P0",
Status: "scripted",
Signals: []string{"same-account session replacement", "idempotent response replay", "gateway backend failover"},
Commands: []string{
"python scripts/sdk_gateway_smoke.py",
"python scripts/redis_mysql_failover_smoke.py",
},
ExitGate: "no duplicated handler execution and no dropped idempotent response",
Owner: "platform",
NextAction: "run nightly against Redis/MySQL and at least two gateway nodes",
},
{
Name: "weak network TCP/WebSocket/KCP/QUIC",
Tier: "P0",
Status: "evidence-backed",
Signals: []string{"TCP latency", "WebSocket latency", "KCP latency", "QUIC latency", "packet loss", "reconnect behavior"},
Commands: []string{
"python scripts/run_gateway_weaknet.py",
},
ExitGate: "summary.md includes TCP, WebSocket, KCP, and QUIC data under loss/jitter profiles",
Owner: "network",
},
{
Name: "long soak and leak detection",
Tier: "P1",
Status: "scripted",
Signals: []string{"goroutine count", "heap", "session count", "pending rpc", "player loop lag"},
Commands: []string{
"python scripts/run_soak.py --duration 6h --interval 30s --load-command \"go run ./cmd/gwbot -duration 6h -clients 5000\"",
"python scripts/check_rollout_report.py reports/soak-*/summary.json --kind soak --min-samples 10",
},
ExitGate: "no unbounded goroutine, heap, pending rpc, or session growth",
Owner: "performance",
NextAction: "run nightly against staging and attach reports/soak-*/summary.json to release candidates",
},
{
Name: "chaos and rolling upgrade",
Tier: "P1",
Status: "scripted",
Signals: []string{"gateway kill", "logic kill", "Redis restart", "MySQL restart", "registry lease expiration", "config reload"},
Commands: []string{"python scripts/run_chaos_drill.py --action restart-logic=\"systemctl restart game-logic\"", "python scripts/check_rollout_report.py reports/chaos-*/summary.json --kind chaos"},
ExitGate: "clients reconnect, stateful requests replay, and admin readiness returns to ok",
Owner: "sre",
NextAction: "add Redis/MySQL and rolling gateway actions to the staging drill matrix",
},
{
Name: "operator audit drill",
Tier: "P1",
Status: "scripted",
Signals: []string{"admin token", "audit log", "OpenAPI", "snapshot diff"},
Commands: []string{
"python scripts/admin_snapshot.py --strict",
},
ExitGate: "every mutating admin endpoint requires auth and emits audit evidence",
Owner: "ops",
NextAction: "add UI workflow for drain, kick, deadletter requeue, and rollback",
},
},
Modules: []Module{
{Name: "matchmaker", Depth: "foundation", MustProve: []string{"queue fairness", "timeout", "cancel", "multi-node ownership"}, NextAction: "add multi-node fairness and timeout report"},
{Name: "party", Depth: "foundation", MustProve: []string{"leader transfer", "invite expiry", "disconnect recovery"}, NextAction: "add reconnect and leader migration smoke"},
{Name: "leaderboard", Depth: "usable sample", MustProve: []string{"season rollover", "tie break", "rank query scale"}, NextAction: "add Redis sorted-set scale smoke"},
{Name: "tournament", Depth: "foundation", MustProve: []string{"bracket lifecycle", "reward idempotency", "late join policy"}, NextAction: "add lifecycle and reward replay tests"},
{Name: "presence", Depth: "distributed path", MustProve: []string{"Lua atomic update", "stale index cleanup", "cross-gateway push"}, NextAction: "add stale-index pressure report"},
},
Framework: []Control{
{Name: "Gate / Node / Mesh", Status: "implemented", Boundary: "topology roles + route-aware discovery", NextAction: "add multi-node chaos evidence for mesh route changes"},
{Name: "Node route options", Status: "implemented", Boundary: "route_policy internal/stateful/authorized/idempotent", NextAction: "generate route matrix docs from handler schema"},
{Name: "Actor model", Status: "foundation", Boundary: "internal/platform/actor mailbox and pid lifecycle", NextAction: "add supervision and remote actor placement smoke"},
{Name: "Config center", Status: "foundation", Boundary: "config reload + typed ServiceConfig", NextAction: "add remote config backend adapter acceptance"},
{Name: "Registry", Status: "implemented", Boundary: "memory/Redis/etcd registry adapters", NextAction: "run etcd lease expiry smoke in server acceptance"},
{Name: "Event bus", Status: "implemented", Boundary: "memory/Redis/NATS bus adapters", NextAction: "run NATS reconnect smoke in server acceptance"},
{Name: "Distributed lock", Status: "implemented", Boundary: "memory/Redis lock manager", NextAction: "run Redis failover and lock expiry smoke"},
{Name: "Cache", Status: "implemented", Boundary: "memory/Redis cache store", NextAction: "run cache TTL and reconnect smoke"},
{Name: "Network transport plugins", Status: "implemented", Boundary: "TCP/WebSocket/KCP/QUIC gateway transports", NextAction: "keep weaknet report in every release candidate"},
},
Operations: []Control{
{Name: "admin console", Status: "minimal-ops", Boundary: "HTML admin + OpenAPI + player/profile/event/leaderboard/ban workflows", NextAction: "add visual topology, session, audit and readiness dashboards"},
{Name: "admin snapshot", Status: "scripted", Boundary: "scripts/admin_snapshot.py JSON/Markdown reports", NextAction: "run in deployment acceptance and attach reports"},
{Name: "audit trail", Status: "rbac-enabled", Boundary: "RBAC admin wrapper + audit logger + trace id + dangerous confirmation", NextAction: "require audit evidence for every mutating admin action in release gate reports"},
{Name: "alerts", Status: "planned", Boundary: "metrics/OpenAPI snapshot data", NextAction: "add Prometheus alert rules for session growth, rpc pending, deadletters, idempotency errors"},
{Name: "operator rollback", Status: "planned", Boundary: "deployment runbook", NextAction: "script drain, canary rollback, and config rollback drills"},
},
Security: []Control{
{Name: "account identity", Status: "extension-boundary", Boundary: "gateway auth_mode external/disabled + project account adapter", NextAction: "integrate real platform auth/OIDC/device binding in project layer"},
{Name: "payments", Status: "extension-boundary", Boundary: "external platform receipt adapter + idempotent DB/EventLog/Outbox grant", NextAction: "add project payment receipt smoke and audit report"},
{Name: "admin auth", Status: "rbac-enabled", Boundary: "X-Admin-Token + role scopes + X-Admin-Confirm + audit trace", NextAction: "add mTLS/reverse proxy pattern and secret rotation runbook"},
{Name: "abuse/risk", Status: "foundation", Boundary: "moderation + rate limit", NextAction: "add risk scoring extension points and sanction audit reports"},
},
Ecosystem: []Control{
{Name: "CLI", Status: "basic", Boundary: "ctl + scripts", NextAction: "turn common admin actions into stable JSON CLI commands"},
{Name: "SDK", Status: "multi-language foundation", Boundary: "Go/TypeScript/Python/Lua/C#/Unity", NextAction: "generate typed DTO clients from sdk contract for C# and Unity"},
{Name: "public docs", Status: "engineering docs", Boundary: "docs/", NextAction: "add tutorial, runbook, and production case-study tracks"},
{Name: "operator console", Status: "minimal-ops", Boundary: "admin HTML + OpenAPI + ops endpoints", NextAction: "build visual workflows for topology, sessions, audit, readiness and rollback"},
},
Evidence: []Evidence{
{Name: "local full verification", Status: "automated", Reports: []string{"reports/release-gate-*/summary.json", "go test", "go vet", "check"}, Commands: []string{"python scripts/release_gate.py --profile local", "go test -count=1 ./...", "go vet ./...", "go run ./cmd/check -roadmap"}, Owner: "platform"},
{Name: "server acceptance", Status: "captured", Reports: []string{"reports/server-acceptance-*/summary.json", "reports/server-acceptance-*/deploy_smoke/admin_snapshot.md"}, Owner: "sre"},
{Name: "TCP/WebSocket/KCP/QUIC weaknet", Status: "captured", Reports: []string{"reports/kcp-quic-weaknet-*/summary.md", "reports/server-acceptance-*/kcp_quic_weaknet*/summary.md", "reports/server-staging-*/kcp_quic_weaknet*/summary.md", "reports/release-gate-server-staging-*/kcp_quic_weaknet*/summary.md"}, Commands: []string{"python scripts/run_gateway_weaknet.py"}, Owner: "network"},
{Name: "Redis/MySQL failover", Status: "scripted", Reports: []string{"reports/server-acceptance-*/mysql_redis_idempotency*.log"}, Commands: []string{"python scripts/redis_mysql_failover_smoke.py"}, Owner: "platform"},
{Name: "multi-node stress", Status: "scripted", Reports: []string{"reports/multi-node-stress-*/summary.json", "reports/multi-node-stress-*/summary.md"}, Commands: []string{"python scripts/run_multi_node_stress.py", "python scripts/check_stress_report.py reports/multi-node-stress-*/summary.json"}, Owner: "performance"},
{Name: "Redis/MySQL/NATS fault injection", Status: "scripted", Reports: []string{"reports/dependency-faults-*/summary.json", "reports/dependency-faults-*/summary.md"}, Commands: []string{"python scripts/run_dependency_faults.py", "python scripts/check_dep_fault_report.py reports/dependency-faults-*/summary.json"}, Owner: "sre"},
{Name: "long soak", Status: "scripted", Reports: []string{"reports/soak-*/summary.json", "reports/soak-*/summary.md"}, Commands: []string{"python scripts/run_soak.py --duration 6h --interval 30s", "python scripts/check_rollout_report.py reports/soak-*/summary.json --kind soak"}, Owner: "performance"},
{Name: "chaos drill", Status: "scripted", Reports: []string{"reports/chaos-*/summary.json", "reports/chaos-*/summary.md"}, Commands: []string{"python scripts/run_chaos_drill.py --action restart-logic=\"systemctl restart game-logic\"", "python scripts/check_rollout_report.py reports/chaos-*/summary.json --kind chaos"}, Owner: "sre"},
},
ReleaseGates: []Gate{
{Name: "engineering merge", Tier: "P0", Requires: []string{"local full verification", "boundary checks", "roadmap checks"}, Decision: "must pass before merge"},
{Name: "staging deployment", Tier: "P0", Requires: []string{"admin snapshot", "sdk smoke", "Redis/MySQL failover", "TCP/WebSocket/KCP/QUIC weaknet", "multi-node stress", "Redis/MySQL/NATS fault injection"}, Decision: "must attach reports before release candidate"},
{Name: "production rollout", Tier: "P1", Requires: []string{"long soak", "chaos drill", "operator audit drill", "rollback drill"}, Decision: "blocked until evidence exists for LongHeng production launch"},
},
}
return Normalize(plan)
}
func Normalize(plan Plan) Plan {
for i := range plan.Scenarios {
plan.Scenarios[i].Name = strings.TrimSpace(plan.Scenarios[i].Name)
plan.Scenarios[i].Signals = cleanSorted(plan.Scenarios[i].Signals)
plan.Scenarios[i].Commands = cleanSorted(plan.Scenarios[i].Commands)
}
sort.Slice(plan.Scenarios, func(i, j int) bool {
if plan.Scenarios[i].Tier != plan.Scenarios[j].Tier {
return plan.Scenarios[i].Tier < plan.Scenarios[j].Tier
}
return plan.Scenarios[i].Name < plan.Scenarios[j].Name
})
for i := range plan.Modules {
plan.Modules[i].MustProve = cleanSorted(plan.Modules[i].MustProve)
}
sort.Slice(plan.Modules, func(i, j int) bool { return plan.Modules[i].Name < plan.Modules[j].Name })
sort.Slice(plan.Framework, func(i, j int) bool { return plan.Framework[i].Name < plan.Framework[j].Name })
sort.Slice(plan.Operations, func(i, j int) bool { return plan.Operations[i].Name < plan.Operations[j].Name })
sort.Slice(plan.Security, func(i, j int) bool { return plan.Security[i].Name < plan.Security[j].Name })
sort.Slice(plan.Ecosystem, func(i, j int) bool { return plan.Ecosystem[i].Name < plan.Ecosystem[j].Name })
for i := range plan.Evidence {
plan.Evidence[i].Reports = cleanSorted(plan.Evidence[i].Reports)
plan.Evidence[i].Commands = cleanSorted(plan.Evidence[i].Commands)
}
sort.Slice(plan.Evidence, func(i, j int) bool { return plan.Evidence[i].Name < plan.Evidence[j].Name })
for i := range plan.ReleaseGates {
plan.ReleaseGates[i].Requires = cleanSorted(plan.ReleaseGates[i].Requires)
}
sort.Slice(plan.ReleaseGates, func(i, j int) bool {
if plan.ReleaseGates[i].Tier != plan.ReleaseGates[j].Tier {
return plan.ReleaseGates[i].Tier < plan.ReleaseGates[j].Tier
}
return plan.ReleaseGates[i].Name < plan.ReleaseGates[j].Name
})
plan.Gaps = gaps(plan)
return plan
}
func Validate(plan Plan) error {
plan = Normalize(plan)
var missing []string
if plan.SchemaVersion <= 0 {
missing = append(missing, "schema_version")
}
requiredScenarios := []string{"single-node functional acceptance", "multi-node gateway logic replay", "weak network TCP/WebSocket/KCP/QUIC", "long soak and leak detection", "chaos and rolling upgrade"}
for _, name := range requiredScenarios {
if !hasScenario(plan, name) {
missing = append(missing, "scenario "+name)
}
}
for _, name := range []string{"matchmaker", "party", "leaderboard", "tournament", "presence"} {
if !hasModule(plan, name) {
missing = append(missing, "module "+name)
}
}
for _, name := range []string{"Gate / Node / Mesh", "Actor model", "Config center", "Registry", "Event bus", "Distributed lock", "Cache", "Network transport plugins"} {
if !hasControl(plan.Framework, name) {
missing = append(missing, "framework "+name)
}
}
for _, name := range []string{"admin console", "admin snapshot", "audit trail", "alerts"} {
if !hasControl(plan.Operations, name) {
missing = append(missing, "operations "+name)
}
}
for _, name := range []string{"account identity", "payments", "admin auth"} {
if !hasControl(plan.Security, name) {
missing = append(missing, "security "+name)
}
}
for _, name := range []string{"local full verification", "server acceptance", "TCP/WebSocket/KCP/QUIC weaknet", "Redis/MySQL failover", "multi-node stress", "Redis/MySQL/NATS fault injection", "long soak", "chaos drill"} {
if !hasEvidence(plan, name) {
missing = append(missing, "evidence "+name)
}
}
for _, name := range []string{"engineering merge", "staging deployment", "production rollout"} {
if !hasGate(plan, name) {
missing = append(missing, "release gate "+name)
}
}
if len(missing) > 0 {
return fmt.Errorf("readiness plan incomplete: %s", strings.Join(missing, "; "))
}
return nil
}
func gaps(plan Plan) []Gap {
var out []Gap
for _, scenario := range plan.Scenarios {
if scenario.Status == "planned" {
out = append(out, Gap{Area: "scenario", Name: scenario.Name, Reason: "planned validation evidence is not yet automated"})
}
}
controls := append([]Control{}, plan.Framework...)
controls = append(controls, plan.Operations...)
controls = append(controls, plan.Security...)
controls = append(controls, plan.Ecosystem...)
for _, control := range controls {
if control.Status == "placeholder" ||
control.Status == "not-core" ||
control.Status == "minimal" ||
control.Status == "minimal-ops" ||
control.Status == "extension-boundary" {
out = append(out, Gap{Area: "control", Name: control.Name, Reason: control.Status})
}
}
for _, evidence := range plan.Evidence {
if evidence.Status == "planned" {
out = append(out, Gap{Area: "evidence", Name: evidence.Name, Reason: "missing report evidence"})
}
}
return out
}
func hasScenario(plan Plan, name string) bool {
for _, scenario := range plan.Scenarios {
if scenario.Name == name {
return true
}
}
return false
}
func hasModule(plan Plan, name string) bool {
for _, module := range plan.Modules {
if module.Name == name {
return true
}
}
return false
}
func hasControl(controls []Control, name string) bool {
for _, control := range controls {
if control.Name == name {
return true
}
}
return false
}
func hasEvidence(plan Plan, name string) bool {
for _, evidence := range plan.Evidence {
if evidence.Name == name {
return true
}
}
return false
}
func hasGate(plan Plan, name string) bool {
for _, gate := range plan.ReleaseGates {
if gate.Name == name {
return true
}
}
return false
}
func cleanSorted(values []string) []string {
out := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
sort.Strings(out)
return out
}