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,317 @@
package config
import (
"fmt"
"os"
"reflect"
"strings"
)
func validateProdSafety(errs *[]error, cfg ServiceConfig) {
// 生产类环境不能只看 environment=prod,k8s/helm/staging 也按上线标准 fail-fast。
validEnvPlaceholders(errs, reflect.ValueOf(cfg), "")
rejectProdDefault(errs, "cluster.id", cfg.Cluster.ID)
rejectProdDefault(errs, "cluster.version", cfg.Cluster.Version)
rejectProdDefault(errs, "cluster.data_version", cfg.Cluster.DataVersion)
rejectKnownSecret(errs, "admin.token", cfg.Admin.Token)
if len(cfg.Admin.Tokens) == 0 {
*errs = append(*errs, fmt.Errorf("admin.tokens requires at least one scoped token for production-like environments"))
}
if !cfg.Metrics.RequireAdminToken {
*errs = append(*errs, fmt.Errorf("metrics.require_admin_token is required for production-like environments"))
}
if cfg.HotUpdate.Enabled && !cfg.HotUpdate.RequireSignature {
*errs = append(*errs, fmt.Errorf("hot_update.require_signature is required for production-like environments"))
}
if cfg.Bus.Kind == "" || cfg.Bus.Kind == "memory" {
*errs = append(*errs, fmt.Errorf("bus.kind memory is not allowed for production-like environments"))
}
if cfg.Cache.Kind == "" || cfg.Cache.Kind == "memory" {
*errs = append(*errs, fmt.Errorf("cache.kind memory is not allowed for production-like environments"))
}
if cfg.Lock.Kind == "" || cfg.Lock.Kind == "memory" {
*errs = append(*errs, fmt.Errorf("lock.kind memory is not allowed for production-like environments"))
}
if cfg.Registry.Kind == "" || cfg.Registry.Kind == "memory" {
*errs = append(*errs, fmt.Errorf("registry.kind memory is not allowed for production-like environments"))
}
if !cfg.Tracing.Enabled && !cfg.OTel.Enabled {
*errs = append(*errs, fmt.Errorf("tracing.enabled or otel.enabled is required for production-like environments"))
}
if cfg.Audit.Kind != "file" {
*errs = append(*errs, fmt.Errorf("audit.kind file is required for production-like environments"))
}
if sdkInsecureEnv() {
*errs = append(*errs, fmt.Errorf("LONGHENG_SDK_INSECURE must not be enabled for production-like environments"))
}
for idx, token := range cfg.Admin.Tokens {
rejectKnownSecret(errs, fmt.Sprintf("admin.tokens[%d].token", idx), token.Token)
}
if isGatewayService(cfg.Service.Name) {
// 多 gateway 必须共享在线状态,否则跨网关推送、踢人和重连恢复都会失真。
if cfg.Presence.Kind != "redis" {
*errs = append(*errs, fmt.Errorf("presence.kind redis is required for production gateway services"))
}
if !cfg.Gateway.ConnectionRateLimitEnabled {
*errs = append(*errs, fmt.Errorf("gateway.connection_rate_limit_enabled is required for production gateway services"))
}
if cfg.Gateway.AuthToken != "" {
rejectKnownSecret(errs, "gateway.auth_token", cfg.Gateway.AuthToken)
}
if cfg.Gateway.LoginToken != "" {
rejectKnownSecret(errs, "gateway.login_token", cfg.Gateway.LoginToken)
}
requireProdSecret(errs, "gateway.session_key_master_secret", cfg.Gateway.SessionKeyMasterSecret)
}
if cfg.Idempotency.Kind == "redis" || cfg.Idempotency.Kind == "mysql_redis" {
requireProdSecret(errs, "idempotency.redis_password", cfg.Idempotency.RedisPassword)
}
if cfg.Registry.Kind == "redis" {
requireProdSecret(errs, "registry.redis_password", cfg.Registry.RedisPassword)
}
if cfg.Bus.Kind == "redis" {
requireProdSecret(errs, "bus.redis_password", cfg.Bus.RedisPassword)
if !cfg.Bus.RedisTLSEnabled {
*errs = append(*errs, fmt.Errorf("bus.redis_tls_enabled is required for production-like redis bus"))
}
}
if cfg.Bus.Kind == "nats" {
if strings.TrimSpace(cfg.Bus.NATSToken) == "" && strings.TrimSpace(cfg.Bus.NATSCredentials) == "" &&
(strings.TrimSpace(cfg.Bus.NATSUsername) == "" || strings.TrimSpace(cfg.Bus.NATSPassword) == "") {
*errs = append(*errs, fmt.Errorf("bus nats requires token, credentials, or username/password for production-like environments"))
}
if strings.TrimSpace(cfg.Bus.NATSToken) != "" {
rejectKnownSecret(errs, "bus.nats_token", cfg.Bus.NATSToken)
}
if strings.TrimSpace(cfg.Bus.NATSPassword) != "" {
rejectKnownSecret(errs, "bus.nats_password", cfg.Bus.NATSPassword)
}
if !cfg.Bus.NATSTLSEnabled && strings.TrimSpace(cfg.Bus.NATSCAFile) == "" && strings.TrimSpace(cfg.Bus.NATSCertFile) == "" {
*errs = append(*errs, fmt.Errorf("bus.nats_tls_enabled or nats CA/client cert is required for production-like nats bus"))
}
}
if cfg.Cache.Kind == "redis" {
requireProdSecret(errs, "cache.redis_password", cfg.Cache.RedisPassword)
}
if cfg.Lock.Kind == "redis" {
requireProdSecret(errs, "lock.redis_password", cfg.Lock.RedisPassword)
}
if cfg.Presence.Kind == "redis" {
requireProdSecret(errs, "presence.redis_password", cfg.Presence.RedisPassword)
}
if cfg.ProfileStore.StoreKind == "redis" || cfg.ProfileStore.StoreKind == "pika" || cfg.ProfileStore.StoreKind == "profiledb" || cfg.ProfileStore.StoreKind == "profile_db" || cfg.ProfileStore.StoreKind == "mysql_redis" {
requireProdSecret(errs, "profile_store.store_password", cfg.ProfileStore.StorePassword)
}
if cfg.ProfileStore.SummaryStoreKind == "redis" || cfg.ProfileStore.SummaryStoreKind == "pika" {
requireProdSecret(errs, "profile_store.summary_store_password", cfg.ProfileStore.SummaryStorePassword)
}
if isProfileCriticalSvc(cfg.Service.Name) {
// logic/playerd 是 Profile 权威写路径,生产不能退回文件、内存或纯 Redis 存档。
switch cfg.ProfileStore.StoreKind {
case "mysql", "mysql_redis":
default:
*errs = append(*errs, fmt.Errorf("profile_store.store_kind mysql or mysql_redis is required for production %s services", cfg.Service.Name))
}
switch cfg.ProfileStore.SummaryStoreKind {
case "redis", "pika":
default:
*errs = append(*errs, fmt.Errorf("profile_store.summary_store_kind redis or pika is required for production %s services", cfg.Service.Name))
}
}
}
func sdkInsecureEnv() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv("LONGHENG_SDK_INSECURE"))) {
case "1", "true", "yes", "y", "on":
return true
default:
return false
}
}
func validEnvPlaceholders(errs *[]error, value reflect.Value, path string) {
if !value.IsValid() {
return
}
if value.Kind() == reflect.Pointer {
if value.IsNil() {
return
}
validEnvPlaceholders(errs, value.Elem(), path)
return
}
switch value.Kind() {
case reflect.Struct:
valueType := value.Type()
for idx := 0; idx < value.NumField(); idx++ {
fieldType := valueType.Field(idx)
if fieldType.PkgPath != "" {
continue
}
fieldPath := joinConfigPath(path, tomlFieldName(fieldType))
validEnvPlaceholders(errs, value.Field(idx), fieldPath)
}
case reflect.Slice, reflect.Array:
for idx := 0; idx < value.Len(); idx++ {
validEnvPlaceholders(errs, value.Index(idx), fmt.Sprintf("%s[%d]", path, idx))
}
case reflect.String:
text := value.String()
if configEnvPattern.MatchString(text) || isShellEnvOnly(text) {
// 未解析占位符比空值更危险:看起来非空,实际会把字面量当密钥或地址启动。
*errs = append(*errs, fmt.Errorf("%s contains unresolved environment variable placeholder", path))
}
}
}
func tomlFieldName(field reflect.StructField) string {
tag := field.Tag.Get("toml")
if tag == "" || tag == "-" {
return strings.ToLower(field.Name)
}
name := strings.Split(tag, ",")[0]
if name == "" {
return strings.ToLower(field.Name)
}
return name
}
func joinConfigPath(prefix, name string) string {
if prefix == "" {
return name
}
return prefix + "." + name
}
func isShellEnvOnly(value string) bool {
value = strings.TrimSpace(value)
if len(value) < 2 || value[0] != '$' {
return false
}
for idx, r := range value[1:] {
if idx == 0 {
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
continue
}
return false
}
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
continue
}
return false
}
return true
}
func requireProdSecret(errs *[]error, field, value string) {
requireString(errs, field, value)
if strings.TrimSpace(value) == "" {
return
}
// 非空还不够,常见本地默认值也要直接拒绝。
rejectKnownSecret(errs, field, value)
}
func rejectKnownSecret(errs *[]error, field, value string) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "local-admin-token",
"local-login-token",
"local-auth-token",
"local-session-key-master-secret",
"local-token",
"admin",
"token",
"password",
"redis",
"mysql",
"changeme",
"change-me",
"change_me":
*errs = append(*errs, fmt.Errorf("%s must not use a local/default secret in production-like environments", field))
}
}
func rejectProdDefault(errs *[]error, field, value string) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "local", "default", "changeme", "change-me", "change_me":
*errs = append(*errs, fmt.Errorf("%s must not use a local/default value in production-like environments", field))
}
}
func IsProdLikeEnv(environment string) bool {
return isProdLikeEnv(environment)
}
// UsesDistributedRuntimeAuthority 判断配置是否已经启用分布式或持久化权威路径,
// 用于承载有状态运行时决策。
//
// 它不会单独把整份配置判定为生产环境:本地开发也可能使用 MySQL/Redis。
// 该信号只服务于运行时装配和预检,避免调用方只依赖 service.environment。
func UsesDistributedRuntimeAuthority(cfg ServiceConfig) bool {
if cfg.Topology.Enabled {
return true
}
if isProfileCriticalSvc(cfg.Service.Name) && ProfileStoreUsesDurableAuthority(cfg.ProfileStore) {
return true
}
return false
}
// ProfileStoreUsesDurableAuthority 判断 profile_store.store_kind 是否由持久化 DB
// 作为权威存储,而不是进程本地或仅文件的开发存储。
func ProfileStoreUsesDurableAuthority(section ProfileStoreSection) bool {
switch strings.ToLower(strings.TrimSpace(section.StoreKind)) {
case "mysql", "mysql_redis":
return true
default:
return false
}
}
func isProdLikeEnv(environment string) bool {
environment = strings.ToLower(strings.TrimSpace(environment))
if environment == "" {
return false
}
for _, token := range []string{"prod", "production", "staging", "stage", "preprod", "pre-production", "k8s", "kubernetes", "helm", "cluster", "live"} {
if strings.Contains(environment, token) {
return true
}
}
return false
}
func isGatewayService(serviceName string) bool {
return strings.EqualFold(strings.TrimSpace(serviceName), "gateway")
}
func isEventLogCritical(serviceName string) bool {
switch strings.ToLower(strings.TrimSpace(serviceName)) {
case "gateway", "logic", "playerd", "payx":
return true
default:
return false
}
}
func isIdemCriticalSvc(serviceName string) bool {
switch strings.ToLower(strings.TrimSpace(serviceName)) {
case "logic", "playerd":
return true
default:
return false
}
}
func isProfileCriticalSvc(serviceName string) bool {
switch strings.ToLower(strings.TrimSpace(serviceName)) {
case "logic", "playerd":
return true
default:
return false
}
}
func isBattleAgentService(serviceName string) bool {
return strings.EqualFold(strings.TrimSpace(serviceName), "battleagent")
}