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/comm"
"yunyan/lego/sys/mysql" "yunyan/lego/sys/mysql"
wechat_auth "yunyan/sys/auth/wechat"
"yunyan/sys/email" "yunyan/sys/email"
"yunyan/sys/pay/alipay" "yunyan/sys/pay/alipay"
"yunyan/sys/pay/appleiap" "yunyan/sys/pay/appleiap"
@ -22,7 +23,7 @@ import (
// registerModuleReloaders 注册可热重载的业务模块(键 = ModuleDef.SysKey)。 // registerModuleReloaders 注册可热重载的业务模块(键 = ModuleDef.SysKey)。
// //
// 只有在此登记的模块才会真的热更;其余(各 auth)重载时如实回 restart_required—— // 只有在此登记的模块才会真的热更;其余(google_auth 等)重载时如实回 restart_required——
// 它们的 sys 尚未提供「校验失败不 panic 且保留旧实例」的 Reload,贸然复用 OnInit 会在 // 它们的 sys 尚未提供「校验失败不 panic 且保留旧实例」的 Reload,贸然复用 OnInit 会在
// 配置填错时把线上进程打挂。要扩展就照 sys/email 的 Reload 补齐后再加一行。 // 配置填错时把线上进程打挂。要扩展就照 sys/email 的 Reload 补齐后再加一行。
// //
@ -35,6 +36,9 @@ func registerModuleReloaders() {
comm.RegisterModuleReloader("paypal", func(cfg map[string]interface{}) error { return paypal.Reload(cfg) }) 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("appleiap", func(cfg map[string]interface{}) error { return appleiap.Reload(cfg) })
comm.RegisterModuleReloader("googleiap", func(cfg map[string]interface{}) error { return googleiap.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 之后调用—— // setupModuleReload 登记重载上下文。必须在各 sys OnInit 与 mysql.OnInit 之后调用——

50
apps/services/sys/auth/wechat/core.go

@ -3,6 +3,7 @@ package wechat_auth
import ( import (
"context" "context"
"errors" "errors"
"sync"
) )
type ( 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。 // ErrNotInited 微信登录未初始化。OnInit 失败时调用方可选择降级;此时包级函数返回本错误,而不是 panic。
var ErrNotInited = errors.New("wechat_auth 未初始化:配置缺失") var ErrNotInited = errors.New("wechat_auth 未初始化:配置缺失")
// OnInit 配置缺失时返回 error(调用方降级为「微信登录不可用」),不再像以前那样空着 AppSecret
// 也报 success——那会让启动日志说一切正常、实际每次登录都被微信回 41004 appsecret missing。
func OnInit(config map[string]interface{}, option ...Option) error { 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 { if err != nil {
// 出错时不要给 defsys 赋值,避免「非 nil 接口 + nil 指针」让守卫失效。 // 出错时不要给 defsys 赋值,避免「非 nil 接口 + nil 指针」让守卫失效。
return err 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 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) { func Auth(ctx context.Context, idToken string) (info *WeChatUserInfoResponse, err error) {
if defsys == nil { sys := get()
if sys == nil {
return nil, ErrNotInited 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 package wechat_auth
import ( import (
"errors"
"strings"
"yunyan/lego/sys/log" "yunyan/lego/sys/log"
"yunyan/lego/utils/mapstructure" "yunyan/lego/utils/mapstructure"
) )
@ -47,3 +50,19 @@ func newOptionsByOption(opts ...Option) Options {
} }
return 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