From 3dc66fb396ca7d5a11ed1c2e4a6291891f2ca71c Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Wed, 23 Sep 2026 23:36:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=EF=BC=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E7=99=BB=E5=BD=95=E6=94=AF=E6=8C=81=E7=83=AD=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=EF=BC=8CAppSecret=20=E4=B8=BA=E7=A9=BA=E6=97=B6?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=A6=82=E5=AE=9E=E6=8A=A5=E6=9C=AA=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 注册 wechat_auth 的热重载:后台改了 AppSecret 不用重启就生效 (正式服补填 AppSecret 后一直报 41004 appsecret missing,直到重启) - AppID / AppSecret 为空时 OnInit 返回 error,启动日志降级为「未配置」,不再报 success - 后台粘贴带的首尾空白自动去掉;defsys 读写加锁 Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/services/services/home/configreload.go | 6 ++- apps/services/sys/auth/wechat/core.go | 50 ++++++++++++++++++-- apps/services/sys/auth/wechat/options.go | 19 ++++++++ apps/services/sys/auth/wechat/reload_test.go | 44 +++++++++++++++++ 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 apps/services/sys/auth/wechat/reload_test.go diff --git a/apps/services/services/home/configreload.go b/apps/services/services/home/configreload.go index a4c67ed1..ca49cdc5 100644 --- a/apps/services/services/home/configreload.go +++ b/apps/services/services/home/configreload.go @@ -5,6 +5,7 @@ import ( "yunyan/comm" "yunyan/lego/sys/mysql" + wechat_auth "yunyan/sys/auth/wechat" "yunyan/sys/email" "yunyan/sys/pay/alipay" "yunyan/sys/pay/appleiap" @@ -22,7 +23,7 @@ import ( // registerModuleReloaders 注册可热重载的业务模块(键 = ModuleDef.SysKey)。 // -// 只有在此登记的模块才会真的热更;其余(各 auth)重载时如实回 restart_required—— +// 只有在此登记的模块才会真的热更;其余(google_auth 等)重载时如实回 restart_required—— // 它们的 sys 尚未提供「校验失败不 panic 且保留旧实例」的 Reload,贸然复用 OnInit 会在 // 配置填错时把线上进程打挂。要扩展就照 sys/email 的 Reload 补齐后再加一行。 // @@ -35,6 +36,9 @@ func registerModuleReloaders() { comm.RegisterModuleReloader("paypal", func(cfg map[string]interface{}) error { return paypal.Reload(cfg) }) comm.RegisterModuleReloader("appleiap", func(cfg map[string]interface{}) error { return appleiap.Reload(cfg) }) comm.RegisterModuleReloader("googleiap", func(cfg map[string]interface{}) error { return googleiap.Reload(cfg) }) + // 微信登录:2026-09-23 正式服在后台补填 AppSecret 后因没有这一行而不生效, + // 登录持续报 41004 appsecret missing,直到重启。 + comm.RegisterModuleReloader("wechat_auth", func(cfg map[string]interface{}) error { return wechat_auth.Reload(cfg) }) } // setupModuleReload 登记重载上下文。必须在各 sys OnInit 与 mysql.OnInit 之后调用—— diff --git a/apps/services/sys/auth/wechat/core.go b/apps/services/sys/auth/wechat/core.go index 9dc85a6b..0cc0d84b 100644 --- a/apps/services/sys/auth/wechat/core.go +++ b/apps/services/sys/auth/wechat/core.go @@ -3,6 +3,7 @@ package wechat_auth import ( "context" "errors" + "sync" ) type ( @@ -32,18 +33,56 @@ type ( } ) -var defsys ISys +// defsys 由 OnInit(启动) 与 Reload(后台改配置后热更) 写、Auth 读,故必须加锁: +// 无锁并发读写接口值是 data race。 +var ( + mu sync.RWMutex + defsys ISys +) + +func get() ISys { + mu.RLock() + defer mu.RUnlock() + return defsys +} + +func set(sys ISys) { + mu.Lock() + defsys = sys + mu.Unlock() +} // ErrNotInited 微信登录未初始化。OnInit 失败时调用方可选择降级;此时包级函数返回本错误,而不是 panic。 var ErrNotInited = errors.New("wechat_auth 未初始化:配置缺失") +// OnInit 配置缺失时返回 error(调用方降级为「微信登录不可用」),不再像以前那样空着 AppSecret +// 也报 success——那会让启动日志说一切正常、实际每次登录都被微信回 41004 appsecret missing。 func OnInit(config map[string]interface{}, option ...Option) error { - sys, err := newSys(newOptions(config, option...)) + options, err := newOptionsChecked(config, option...) + if err != nil { + return err + } + sys, err := newSys(options) if err != nil { // 出错时不要给 defsys 赋值,避免「非 nil 接口 + nil 指针」让守卫失效。 return err } - defsys = sys + set(sys) + return nil +} + +// Reload 后台「业务功能配置 → 微信登录」保存后热更,无需重启。 +// 校验不过直接返回 error、保留旧实例继续服务(与 sys/sms 的 Reload 同口径)。 +func Reload(config map[string]interface{}, option ...Option) error { + options, err := newOptionsChecked(config, option...) + if err != nil { + return err + } + sys, err := newSys(options) + if err != nil { + return err + } + set(sys) return nil } @@ -53,8 +92,9 @@ func NewSys(option ...Option) (sys ISys, err error) { } func Auth(ctx context.Context, idToken string) (info *WeChatUserInfoResponse, err error) { - if defsys == nil { + sys := get() + if sys == nil { return nil, ErrNotInited } - return defsys.Auth(ctx, idToken) + return sys.Auth(ctx, idToken) } diff --git a/apps/services/sys/auth/wechat/options.go b/apps/services/sys/auth/wechat/options.go index e4a3a028..15c30857 100644 --- a/apps/services/sys/auth/wechat/options.go +++ b/apps/services/sys/auth/wechat/options.go @@ -1,6 +1,9 @@ package wechat_auth import ( + "errors" + "strings" + "yunyan/lego/sys/log" "yunyan/lego/utils/mapstructure" ) @@ -47,3 +50,19 @@ func newOptionsByOption(opts ...Option) Options { } return options } + +// newOptionsChecked 解析 map 配置并校验,不合法时返回 error 而非 panic。 +// 启动(OnInit)与热重载(Reload)共用:配置缺失只应让微信登录降级 / 本次重载失败,不该打挂进程。 +func newOptionsChecked(config map[string]interface{}, opts ...Option) (Options, error) { + options := newOptions(config, opts...) + // 后台表单粘贴时常带首尾空白,原样拼进 URL 微信会判 invalid appsecret。 + options.AppID = strings.TrimSpace(options.AppID) + options.AppSecret = strings.TrimSpace(options.AppSecret) + if options.AppID == "" { + return options, errors.New("wechat_auth 配置缺失: AppID 为空") + } + if options.AppSecret == "" { + return options, errors.New("wechat_auth 配置缺失: AppSecret 为空") + } + return options, nil +} diff --git a/apps/services/sys/auth/wechat/reload_test.go b/apps/services/sys/auth/wechat/reload_test.go new file mode 100644 index 00000000..fc22723f --- /dev/null +++ b/apps/services/sys/auth/wechat/reload_test.go @@ -0,0 +1,44 @@ +package wechat_auth + +import "testing" + +// 空 AppSecret 必须判失败:以前 OnInit 照单全收、日志报 success,线上每次登录都被微信回 41004。 +func TestReloadRejectsMissingAndKeepsOld(t *testing.T) { + set(nil) + defer set(nil) + + if err := OnInit(map[string]interface{}{"AppID": "wx1", "AppSecret": ""}); err == nil { + t.Fatal("AppSecret 为空时 OnInit 应返回 error") + } + if get() != nil { + t.Fatal("OnInit 失败不应留下实例") + } + + if err := Reload(map[string]interface{}{"AppID": " wx1 ", "AppSecret": " s1\n"}); err != nil { + t.Fatalf("合法配置 Reload 失败: %v", err) + } + old := get().(*WeChat) + if old.options.AppID != "wx1" || old.options.AppSecret != "s1" { + t.Fatalf("首尾空白未去掉: %+v", old.options) + } + + for _, cfg := range []map[string]interface{}{ + {"AppID": "wx2", "AppSecret": ""}, + {"AppID": "", "AppSecret": "s2"}, + {"AppID": "wx2", "AppSecret": " "}, + } { + if err := Reload(cfg); err == nil { + t.Fatalf("非法配置 %v 应返回 error", cfg) + } + if get() != old { + t.Fatalf("非法配置 %v 不应替换旧实例", cfg) + } + } + + if err := Reload(map[string]interface{}{"AppID": "wx3", "AppSecret": "s3"}); err != nil { + t.Fatal(err) + } + if get().(*WeChat).options.AppID != "wx3" { + t.Fatal("合法配置应替换实例") + } +}