Browse Source

服务端:微信登录支持热更新,AppSecret 为空时启动如实报未配置

- 注册 wechat_auth 的热重载:后台改了 AppSecret 不用重启就生效
  (正式服补填 AppSecret 后一直报 41004 appsecret missing,直到重启)
- AppID / AppSecret 为空时 OnInit 返回 error,启动日志降级为「未配置」,不再报 success
- 后台粘贴带的首尾空白自动去掉;defsys 读写加锁

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main
Rodger-Wang 3 weeks ago
parent
commit
3dc66fb396
  1. 6
      apps/services/services/home/configreload.go
  2. 50
      apps/services/sys/auth/wechat/core.go
  3. 19
      apps/services/sys/auth/wechat/options.go
  4. 44
      apps/services/sys/auth/wechat/reload_test.go

6
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 之后调用——

50
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)
}

19
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
}

44
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("合法配置应替换实例")
}
}
Loading…
Cancel
Save