Files
sundynix-agentix/sundynix-gateway/internal/store/redis_memfallback.go
T
Blizzard 07955ddf07 feat(auth): 微信扫码登录后端 —— 网页授权 + ticket 轮询
服务号「植趣 ZeeQ」已认证,走网页授权(snsapi_base,只拿 openid、用户无感),
不接管消息推送,副作用最小。

流程:PC 建 ticket → 二维码指向 /wx/mp?t= → 用户微信扫码 → 302 到微信授权页 →
回调 /api/v1/wx/mp/callback 用 code 换 openid → 找/建用户 → ticket 置 authorized →
PC 轮询 /wx/mp/poll 拿到 authorized → 签发 JWT。ticket 一次性消费防重放。

- 配置(appid/secret/base_url)后台可改,secret AES 加密入库,与微信支付同一套 secrets;
- ticket 存 Redis(短 TTL),无 Redis 时回退进程内内存(本地单实例可用,生产必须有 Redis);
- User 加 wechat_openid。**部分唯一索引**(WHERE openid <> '')而非普通唯一:
  存量邮箱用户该列是空串,普通唯一索引会让多个空串互撞、AutoMigrate 直接失败
  —— 与之前 NULL 余额同类的坑,这次提前避开。

单测覆盖:授权 URL 拼接(含 #wechat_redirect 锚点必须在末尾)、secret 加密往返、
建号/查号、空 openid 不误命中存量用户。微信 API 调用依赖公网回调,本地测不了,
留待部署后真机扫码。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:44:00 +08:00

41 lines
871 B
Go

package store
import (
"sync"
"time"
)
// Redis 降级时的进程内 ticket 兜底:仅供本地开发/单实例。带惰性过期,避免泄漏。
var (
memTicketMu sync.Mutex
memTickets = map[string]memTicketEntry{}
)
type memTicketEntry struct {
value string
expireAt time.Time
}
func memTicketSet(ticket, value string, ttl time.Duration) {
memTicketMu.Lock()
defer memTicketMu.Unlock()
memTickets[ticket] = memTicketEntry{value: value, expireAt: time.Now().Add(ttl)}
// 顺带清理已过期项(登录频率低,全表扫无压力)
now := time.Now()
for k, e := range memTickets {
if now.After(e.expireAt) {
delete(memTickets, k)
}
}
}
func memTicketGet(ticket string) string {
memTicketMu.Lock()
defer memTicketMu.Unlock()
e, ok := memTickets[ticket]
if !ok || time.Now().After(e.expireAt) {
return ""
}
return e.value
}