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

172 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# client-auth-system 设计文档
## Context
当前个人客户认证状态如下:
- 个人客户当前使用纯 JWT`pkg/auth/jwt.go`),未做 Redis token 状态存储,服务端无法主动失效。
- 微信配置已在数据库 `tb_wechat_config` 中存在(`WechatConfig`),但现有能力中仍存在 YAML 静态配置依赖。
- 个人客户相关模型已存在:`PersonalCustomer``PersonalCustomerPhone``PersonalCustomerDevice`
- 现有 `/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 Key`RedisPersonalCustomerTokenKey(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_base`GetUserInfoDetailed(code)`snsapi_userinfo`GetUserInfoByToken()` | 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`** — 小程序服务封装:
```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)
```
2. **`pkg/wechat/config.go`** — 新增 DB 动态工厂函数:
```go
// 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)
```
3. **`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_binding`bool在登录时实时读取不新增 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/*`