服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
274 lines
8.5 KiB
Go
274 lines
8.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/platform/config"
|
|
"longheng.io/server/internal/platform/datatable"
|
|
"longheng.io/server/internal/platform/discovery"
|
|
"longheng.io/server/internal/platform/reload"
|
|
rpckit "longheng.io/server/internal/platform/rpc"
|
|
)
|
|
|
|
func (a *Application) applyRuntimeReload(ctx context.Context, trigger reload.Trigger, reason string) (reload.Result, error) {
|
|
result := reload.Result{
|
|
Trigger: string(trigger),
|
|
Reason: reason,
|
|
At: time.Now().UTC(),
|
|
}
|
|
if a == nil {
|
|
return result, errors.New("application is nil")
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
oldCfg := a.configSnapshot()
|
|
newCfg, err := config.Load(a.configPath, oldCfg.Service.Name)
|
|
if err != nil {
|
|
result.Error = err.Error()
|
|
return result, err
|
|
}
|
|
result.Changed = !reflect.DeepEqual(oldCfg, newCfg)
|
|
if trigger == reload.TriggerWatch && !result.Changed {
|
|
result.Skipped = append(result.Skipped, "config unchanged")
|
|
return result, nil
|
|
}
|
|
|
|
effective := oldCfg
|
|
var applyErr error
|
|
apply := func(name string, changed bool, fn func() error) {
|
|
if applyErr != nil || (!changed && trigger != reload.TriggerManual) {
|
|
return
|
|
}
|
|
if err := fn(); err != nil {
|
|
applyErr = fmt.Errorf("reload %s: %w", name, err)
|
|
result.Error = applyErr.Error()
|
|
return
|
|
}
|
|
result.Applied = append(result.Applied, name)
|
|
}
|
|
|
|
apply("rate_limit", !reflect.DeepEqual(oldCfg.RateLimit, newCfg.RateLimit), func() error {
|
|
cfg, err := rateLimitConfig(newCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.rateLimiter.Configure(cfg)
|
|
effective.RateLimit = newCfg.RateLimit
|
|
return nil
|
|
})
|
|
apply("discovery", !reflect.DeepEqual(oldCfg.Discovery, newCfg.Discovery), func() error {
|
|
a.discovery.Configure(
|
|
time.Duration(newCfg.Discovery.CacheTTLSeconds)*time.Second,
|
|
time.Duration(newCfg.Discovery.FailureTTLSeconds)*time.Second,
|
|
newCfg.Discovery.FailureThreshold,
|
|
!newCfg.Discovery.AllowMaintaining,
|
|
discovery.Strategy(newCfg.Discovery.Strategy),
|
|
)
|
|
effective.Discovery = newCfg.Discovery
|
|
return nil
|
|
})
|
|
apply("rpc", !reflect.DeepEqual(oldCfg.RPC, newCfg.RPC), func() error {
|
|
a.rpc.SetOptionsExact(rpckit.Options{
|
|
CallTimeout: time.Duration(newCfg.RPC.CallTimeoutSeconds) * time.Second,
|
|
MaxPending: newCfg.RPC.MaxPending,
|
|
MaxPayloadBytes: newCfg.RPC.MaxPayloadBytes,
|
|
})
|
|
effective.RPC = newCfg.RPC
|
|
return nil
|
|
})
|
|
apply("topology", !reflect.DeepEqual(oldCfg.Topology, newCfg.Topology), func() error {
|
|
a.topology.Configure(topologyConfig(newCfg))
|
|
if newCfg.Topology.Enabled {
|
|
if err := a.topology.Refresh(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
effective.Topology = newCfg.Topology
|
|
return nil
|
|
})
|
|
apply("tracing", !reflect.DeepEqual(oldCfg.Tracing, newCfg.Tracing), func() error {
|
|
a.tracer.Configure(newCfg.Tracing.Enabled || oldCfg.OTel.Enabled, newCfg.Tracing.MaxSpans)
|
|
effective.Tracing = newCfg.Tracing
|
|
return nil
|
|
})
|
|
apply("data_tables", !reflect.DeepEqual(oldCfg.DataTables, newCfg.DataTables), func() error {
|
|
if newCfg.DataTables.Enabled {
|
|
options := datatable.ReloadOptions{}
|
|
if a.dataTableViews != nil {
|
|
options.Validators = append(options.Validators, a.dataTableViews)
|
|
}
|
|
loadResult, err := a.dataTables.ReloadDir(ctx, newCfg.DataTables.Directory, newCfg.DataTables.Version, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
effective.DataTables = newCfg.DataTables
|
|
if a.dataTableViews != nil {
|
|
viewResult, err := a.dataTableViews.Refresh(context.WithoutCancel(ctx), a.dataTables, loadResult)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addReloadDetail(&result, "data_table_views", viewResult)
|
|
}
|
|
addReloadDetail(&result, "data_tables", loadResult)
|
|
} else {
|
|
a.dataTables.Configure(newCfg.DataTables.Directory, newCfg.DataTables.Version)
|
|
a.dataTables.Clear()
|
|
if a.dataTableViews != nil {
|
|
addReloadDetail(&result, "data_table_views", a.dataTableViews.Clear())
|
|
}
|
|
addReloadDetail(&result, "data_tables", map[string]any{
|
|
"enabled": false,
|
|
"manifest": a.dataTables.Manifest(),
|
|
})
|
|
}
|
|
effective.DataTables = newCfg.DataTables
|
|
return nil
|
|
})
|
|
apply("i18n", !reflect.DeepEqual(oldCfg.I18N, newCfg.I18N), func() error {
|
|
if a.i18nCatalog == nil {
|
|
effective.I18N = newCfg.I18N
|
|
return nil
|
|
}
|
|
if newCfg.I18N.Enabled {
|
|
if err := a.i18nCatalog.LoadDirWithConfig(ctx, newCfg.I18N.Directory, newCfg.I18N.DefaultLanguage, newCfg.I18N.Version); err != nil {
|
|
return err
|
|
}
|
|
if !a.i18nCatalog.HasLanguage(newCfg.I18N.DefaultLanguage) {
|
|
return fmt.Errorf("default language %q is not loaded", newCfg.I18N.DefaultLanguage)
|
|
}
|
|
a.i18nCatalog.Configure(newCfg.I18N.DefaultLanguage, newCfg.I18N.Version)
|
|
} else {
|
|
a.i18nCatalog.Configure(newCfg.I18N.DefaultLanguage, newCfg.I18N.Version)
|
|
a.i18nCatalog.Clear()
|
|
}
|
|
effective.I18N = newCfg.I18N
|
|
return nil
|
|
})
|
|
apply("reload", !reflect.DeepEqual(oldCfg.Reload, newCfg.Reload), func() error {
|
|
if a.reloader != nil {
|
|
a.reloader.Configure(reloadConfig(newCfg))
|
|
}
|
|
effective.Reload = newCfg.Reload
|
|
return nil
|
|
})
|
|
apply("admin_security", adminSecurityChanged(oldCfg.Admin, newCfg.Admin), func() error {
|
|
effective.Admin.Token = newCfg.Admin.Token
|
|
effective.Admin.Tokens = cloneAdminTokens(newCfg.Admin.Tokens)
|
|
effective.Admin.RBACEnabled = newCfg.Admin.RBACEnabled
|
|
effective.Admin.DangerousConfirm = newCfg.Admin.DangerousConfirm
|
|
return nil
|
|
})
|
|
apply("metrics_admin_policy", oldCfg.Metrics.RequireAdminToken != newCfg.Metrics.RequireAdminToken, func() error {
|
|
effective.Metrics.RequireAdminToken = newCfg.Metrics.RequireAdminToken
|
|
return nil
|
|
})
|
|
|
|
result.RestartRequired = restartReqSections(oldCfg, newCfg)
|
|
if applyErr != nil {
|
|
a.setConfig(effective)
|
|
return result, applyErr
|
|
}
|
|
a.setConfig(effective)
|
|
if len(result.Applied) == 0 && len(result.RestartRequired) == 0 {
|
|
result.Skipped = append(result.Skipped, "no runtime-applicable changes")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func adminSecurityChanged(oldAdmin, newAdmin config.AdminSection) bool {
|
|
return oldAdmin.Token != newAdmin.Token ||
|
|
oldAdmin.RBACEnabled != newAdmin.RBACEnabled ||
|
|
oldAdmin.DangerousConfirm != newAdmin.DangerousConfirm ||
|
|
!reflect.DeepEqual(oldAdmin.Tokens, newAdmin.Tokens)
|
|
}
|
|
|
|
func cloneAdminTokens(tokens []config.AdminTokenSection) []config.AdminTokenSection {
|
|
if len(tokens) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]config.AdminTokenSection, len(tokens))
|
|
copy(out, tokens)
|
|
for idx := range out {
|
|
if len(out[idx].Scopes) > 0 {
|
|
out[idx].Scopes = append([]string(nil), out[idx].Scopes...)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func restartReqSections(oldCfg, newCfg config.ServiceConfig) []string {
|
|
var out []string
|
|
if !reflect.DeepEqual(oldCfg.Service, newCfg.Service) {
|
|
out = append(out, "service")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.Cluster, newCfg.Cluster) {
|
|
out = append(out, "cluster")
|
|
}
|
|
if oldCfg.Admin.Listen != newCfg.Admin.Listen ||
|
|
oldCfg.Admin.ReadTimeoutSeconds != newCfg.Admin.ReadTimeoutSeconds ||
|
|
oldCfg.Admin.ReadHeaderTimeoutSeconds != newCfg.Admin.ReadHeaderTimeoutSeconds ||
|
|
oldCfg.Admin.WriteTimeoutSeconds != newCfg.Admin.WriteTimeoutSeconds ||
|
|
oldCfg.Admin.IdleTimeoutSeconds != newCfg.Admin.IdleTimeoutSeconds {
|
|
out = append(out, "admin_http")
|
|
}
|
|
if oldCfg.Metrics.PrometheusEnabled != newCfg.Metrics.PrometheusEnabled {
|
|
out = append(out, "metrics_endpoint")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.OTel, newCfg.OTel) {
|
|
out = append(out, "otel")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.Debug, newCfg.Debug) {
|
|
out = append(out, "debug_routes")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.PVF, newCfg.PVF) {
|
|
out = append(out, "pvf")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.Audit, newCfg.Audit) {
|
|
out = append(out, "audit_sink")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.Registry, newCfg.Registry) {
|
|
out = append(out, "registry")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.Bus, newCfg.Bus) {
|
|
out = append(out, "bus")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.Worker, newCfg.Worker) {
|
|
out = append(out, "worker_pool")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.ProfileStore, newCfg.ProfileStore) {
|
|
out = append(out, "profile_store")
|
|
}
|
|
if !reflect.DeepEqual(oldCfg.Gateway, newCfg.Gateway) {
|
|
out = append(out, "gateway")
|
|
}
|
|
return out
|
|
}
|
|
|
|
func addReloadDetail(result *reload.Result, key string, value any) {
|
|
if result == nil {
|
|
return
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return
|
|
}
|
|
if result.Details == nil {
|
|
result.Details = make(map[string]any)
|
|
}
|
|
result.Details[key] = value
|
|
}
|
|
|
|
func reloadConfig(cfg config.ServiceConfig) reload.Config {
|
|
return reload.Config{
|
|
Enabled: cfg.Reload.Enabled,
|
|
PollInterval: time.Duration(cfg.Reload.PollIntervalSeconds) * time.Second,
|
|
ApplyTimeout: time.Duration(cfg.Reload.ApplyTimeoutSeconds) * time.Second,
|
|
}
|
|
}
|