Files
junhong_cmp_fiber/openspec/changes/archive/2026-03-19-client-auth-system/design.md
huang b9733c4913
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
fix: 修正零售价架构错误 + 清理旧微信配置 + 归档提案 + 前端接口文档
1. 修正 retail_price 架构:
   - 删除 batch-pricing 接口的 pricing_target 字段和 retail_price 分支
     (上级只能改下级成本价,不能改零售价)
   - 新增 PATCH /api/admin/packages/:id/retail-price 接口
     (代理自己改自己的零售价,校验 retail_price >= cost_price)

2. 清理旧微信 YAML 配置(已全部迁移到数据库 tb_wechat_config):
   - 删除 config.yaml 中 wechat.official_account 配置节
   - 删除 NewOfficialAccountApp() 旧工厂函数
   - 清理 personal_customer service 中的死代码(旧登录/绑定微信方法)
   - 清理 docker-compose.prod.yml 中旧微信环境变量和证书挂载注释

3. 归档四个已完成提案到 openspec/changes/archive/

4. 新增前端接口变更说明文档(docs/前端接口变更说明.md)

5. 修正归档提案和 specs 中关于 pricing_target 的错误描述
2026-03-19 17:39:43 +08:00

8.4 KiB
Raw Blame History

client-auth-system 设计文档

Context

当前个人客户认证状态如下:

  • 个人客户当前使用纯 JWTpkg/auth/jwt.go),未做 Redis token 状态存储,服务端无法主动失效。
  • 微信配置已在数据库 tb_wechat_config 中存在(WechatConfig),但现有能力中仍存在 YAML 静态配置依赖。
  • 个人客户相关模型已存在:PersonalCustomerPersonalCustomerPhonePersonalCustomerDevice
  • 现有 /api/c/v1 路由将被本次完整认证体系替换为新的 /api/c/v1/auth/* 七个端点。

Goals / Non-Goals

Goals

  • 交付完整 C 端认证系统,覆盖 A1~A7 七个接口:资产验证、微信登录(公众号/小程序)、验证码发送、手机号绑定、手机号换绑、退出登录。
  • 建立有状态 JWTJWT + Redis机制支持服务端主动失效。
  • 建立 OpenID 多记录模型,兼容公众号与小程序不同 AppID 场景。

Non-Goals

  • 不实现业务域 API如充值、套餐、订单等
  • 不包含兑换系统exchange相关设计与实现。

Decisions

1) asset_token 设计

  • 方案:asset_token 使用短时效 JWT5 分钟payload 仅包含 asset_type + asset_id,并使用独立于主登录 JWT 的签名密钥。
  • WhyA1 是无认证接口,若直接暴露内部 asset_id 会造成可枚举风险;短时效 + 独立密钥可降低 token 泄露影响范围。

2) Stateful JWT with Redis

  • 方案:登录成功签发 JWT 后,将 token 状态写入 Redis 并设置 TTL每次请求在中间件同时校验 JWT 与 Redis 状态。
  • Redis KeyRedisPersonalCustomerTokenKey(customerID)
  • Why纯 JWT 无法服务端撤销Redis 状态可支持封禁、强制下线、单点退出等主动失效场景。

3) OpenID multi-record strategy

  • 方案:新增 PersonalCustomerOpenID 表,约束 UNIQUE(app_id, open_id)(软删条件下唯一);客户查找逻辑采用:
    1. 先按 (app_id, open_id) 精确命中;
    2. 未命中时按 unionid 回查并合并;
    3. 仍未命中则创建新客户。
  • Why公众号与小程序可能不在同一开放平台需支持“一客户多 OpenID 记录”。

4) WechatConfig dynamic loading + SDK 实例工厂

  • 方案:登录时动态读取 tb_wechat_config WHERE is_active=true,使用工厂函数按需创建 SDK 实例:
    • OfficialAccount 使用 oa_app_id + oa_app_secret
    • Miniapp 使用 miniapp_app_id + miniapp_app_secret
    • Payment 使用 wx_mch_id + wx_api_v3_key + wx_cert_content + wx_key_content + wx_serial_no
  • Why避免 YAML 静态配置导致多环境切换和配置漂移,支持运营侧动态切换。

现有 SDK 能力盘点(pkg/wechat/

文件 已有能力 客户端接口需要
official_account.go GetUserInfo(code)snsapi_baseGetUserInfoDetailed(code)snsapi_userinfoGetUserInfoByToken() A2 公众号登录 直接复用 GetUserInfoDetailed
payment.go CreateJSAPIOrder()CreateH5Order()QueryOrder()CloseOrder()HandlePaymentNotify() 提案 2 支付
config.go NewOfficialAccountApp(cfg) — 仅从 YAML 创建 需新增 DB 动态工厂
缺失 miniapp.go A3 小程序登录需要

需要新增的 SDK 代码

  1. pkg/wechat/miniapp.go — 小程序服务封装:
// MiniAppService 微信小程序服务实现
type MiniAppService struct {
    appID     string
    appSecret string
    logger    *zap.Logger
}

// MiniAppServiceInterface 微信小程序服务接口
type MiniAppServiceInterface interface {
    Code2Session(ctx context.Context, code string) (openID, unionID, sessionKey string, err error)
}

// Code2Session 通过小程序 login code 换取 openid + session_key
// 调用微信 https://api.weixin.qq.com/sns/jscode2session 接口
// 注意: 小程序无法通过 code 直接获取用户信息(昵称/头像由前端授权后传入)
func (s *MiniAppService) Code2Session(ctx context.Context, code string) (openID, unionID, sessionKey string, err error)
  1. pkg/wechat/config.go — 新增 DB 动态工厂函数:
// NewOfficialAccountAppFromConfig 从 WechatConfig DB 记录创建公众号应用实例
func NewOfficialAccountAppFromConfig(wechatConfig *model.WechatConfig, cache kernel.CacheInterface, logger *zap.Logger) (*officialAccount.OfficialAccount, error)

// NewPaymentAppFromConfig 从 WechatConfig DB 记录创建支付应用实例
// appID 参数决定支付关联的应用:公众号传 oa_app_id小程序传 miniapp_app_id
func NewPaymentAppFromConfig(wechatConfig *model.WechatConfig, appID string, cache kernel.CacheInterface, logger *zap.Logger) (*payment.Payment, error)

// NewMiniAppServiceFromConfig 从 WechatConfig DB 记录创建小程序服务实例
func NewMiniAppServiceFromConfig(wechatConfig *model.WechatConfig, logger *zap.Logger) (*MiniAppService, error)
  1. pkg/wechat/wechat.go — 新增 MiniAppServiceInterface 接口定义和编译时类型检查。

A2/A3 登录时 SDK 调用链路

A2 公众号登录:
  client_auth.Service
    → wechatConfigService.GetActiveConfig()          // 从 DB/Redis 缓存获取配置
    → wechat.NewOfficialAccountAppFromConfig(config)  // 动态创建公众号实例
    → wechat.NewOfficialAccountService(app)           // 包装为 Service
    → officialAccountService.GetUserInfoDetailed(code) // 现有方法,直接复用
    → 返回 openID + unionID + nickname + avatar

A3 小程序登录:
  client_auth.Service
    → wechatConfigService.GetActiveConfig()
    → wechat.NewMiniAppServiceFromConfig(config)      // 新增方法
    → miniAppService.Code2Session(code)                // 新增方法
    → 返回 openID + unionID + sessionKey
    → nickname/avatar 从请求体获取(前端授权后传入)

关键约束:小程序 Code2Session 不调用 PowerWeChat SDK该 SDK 主要封装公众号和支付),而是直接 HTTP 请求微信 jscode2session 接口。这更简单可控。

5) Phone binding config

  • 方案:手机号绑定策略使用 Viper 配置项 client.require_phone_bindingbool在登录时实时读取不新增 DB 配置表。
  • Why该策略属于部署级开关配置中心化更轻量减少数据库复杂度。

6) Asset binding on login

  • 方案:每次登录都创建 PersonalCustomerDevice 绑定记录;同一资产允许被多个客户绑定,不做覆盖写入。
  • Why业务上存在转手、共用、历史归属追踪需求强唯一会丢失使用关系。

7) Rate limiting strategy

  • A1IP 级限频 30/min
  • A4手机号维度 60s 冷却 + IP 维度 20/hour + 手机号维度 10/day
  • WhyA1 主要防资产暴力枚举A4 主要防短信轰炸与资源滥用,采用多维限流降低绕过概率。

Risks / Trade-offs

  1. Redis 强依赖风险

    • 风险Redis 异常会导致 token 校验失败、登录态不可用。
    • 缓解:中间件区分“无效 token”与“Redis 不可用”并记录告警;部署 Redis 高可用;关键路径加入超时与重试上限。
  2. OpenID 合并误关联风险

    • 风险:若第三方返回异常 unionid可能出现错误合并。
    • 缓解:仅在 unionid 非空且满足格式校验时启用回退合并记录合并审计日志customer_id、app_id、openid、unionid
  3. 资产多人绑定带来的业务歧义

    • 风险:后续业务查询若默认“单资产单用户”,可能读取歧义。
    • 缓解:规范下游以“当前登录 customer_id + asset”联合查询在文档中明确“资产可多客户绑定”语义。
  4. 动态微信配置切换风险

    • 风险:运营误切换 is_active 导致登录瞬时失败。
    • 缓解:限制仅单条激活、增加配置健康检查与缓存短 TTL、错误回退到最近一次可用配置。

Migration Plan

  1. 数据库迁移

    • 新增 tb_personal_customer_openid 表(含 customer_id/app_id/open_id/union_id 等字段)。
    • 创建唯一索引:UNIQUE(app_id, open_id) WHERE deleted_at IS NULL
  2. 配置更新

    • pkg/config/defaults/config.yaml 增加:
      • client.require_phone_binding: true|false
  3. 灰度切换顺序

    • 先上线迁移与新配置;
    • 再上线新认证接口与中间件增强;
    • 最后切换前端调用到 /api/c/v1/auth/*