Files
junhong_cmp_fiber/openspec/changes/archive/2026-03-19-client-auth-system/proposal.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

136 lines
8.2 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.
## Why
系统需要一套面向个人客户C 端)的完整认证体系,替代已删除的旧 H5 登录接口。客户端(微信公众号 H5 / 微信小程序)的登录流程与 B 端完全不同:基于**资产标识符**而非用户账号密码,先验证资产 → 再微信授权 → 自动绑定资产 → 可选绑定手机号。同时,公众号和小程序可能使用不同 AppID 且不一定绑定同一微信开放平台,需要支持多 OpenID 管理。
**前置依赖**:提案 0`client-api-data-model-fixes`)已完成 PersonalCustomer.wx_open_id 索引变更和旧接口删除。
## What Changes
### 新增模型
- **PersonalCustomerOpenID**:个人客户 OpenID 关联表,支持同一客户在不同 AppID公众号/小程序)下的多 OpenID 记录。唯一索引 `UNIQUE(app_id, open_id) WHERE deleted_at IS NULL`
### 认证接口(`/api/c/v1/auth/`
- **A1 验证资产标识符** `POST /verify-asset`:无需认证。输入 SN/IMEI/虚拟号/ICCID/MSISDN → 返回 `asset_token`(短时效 JWT5 分钟过期payload 含 asset_type + asset_id。IP 级别限频30 次/分钟)防暴力枚举。不暴露内部 asset_id
- **A2 微信公众号登录** `POST /wechat-login`:无需认证。用微信 OAuth code + asset_token → 查找/创建客户 → 绑定资产 → 签发有状态 JWT TokenRedis 存储)→ 返回 token + 是否需要绑定手机号
- **A3 微信小程序登录** `POST /miniapp-login`:无需认证。用小程序 jscode2session + asset_token → 同 A2 后续流程
- **A4 发送验证码** `POST /send-code`:无需认证。限频:同手机号 60s、同 IP 20 次/小时、每手机号 10 次/天
- **A5 绑定手机号** `POST /bind-phone`:需 JWT。首次绑定检查重复
- **A6 换绑手机号** `POST /change-phone`:需 JWT。双重验证码旧+新手机)
- **A7 退出登录** `POST /logout`:需 JWT。删除 Redis token 记录
### 基础设施
- **有状态 JWT Token 管理**JWT payload 仅含 `customer_id` + `exp`Redis 存储 token 有效状态,支持服务端主动失效(封禁/强制下线)
- **PersonalAuthMiddleware 增强**:增加 Redis 有效性检查token 不在 Redis 中则拒绝
- **统一资产解析公共方法** `resolveAssetFromIdentifier()`:个人客户调用不走 shop_id 数据权限过滤
- **OpenID 安全规范**:所有需要 OpenID 的接口支付、充值OpenID 由后端根据 `customer_id` + `app_type` 查 PersonalCustomerOpenID 表获取,禁止客户端传入
- **手机号绑定配置**:通过 Viper 配置 `client.require_phone_binding`boolean登录时检查并返回 `need_bind_phone` 标识
### 登录完整流程
```
用户打开客户端
输入资产标识符SN/IMEI/虚拟号/ICCID
[A1] POST /verify-asset ──→ 返回 asset_token5分钟有效
微信授权(前端完成)
├─── 公众号 ──→ [A2] POST /wechat-login (code + asset_token)
└─── 小程序 ──→ [A3] POST /miniapp-login (code + asset_token)
┌──────────────────┐
│ 解析 asset_token │
│ 获取微信 openid │
│ 查找/创建客户 │
│ 绑定资产 │
│ 签发 JWT + Redis │
└──────┬───────────┘
返回 { token, need_bind_phone, is_new_user }
need_bind_phone == true?
│ │
YES NO
│ │
▼ ▼
[A4] 发送验证码 进入主页面
[A5] 绑定手机号
进入主页面
```
### 客户查找/创建逻辑A2/A3 共享)
```
收到 openid + (可选)unionid
查 PersonalCustomerOpenID WHERE app_id=当前AppID AND open_id=openid
├── 找到 → 获取 customer_id → 已有客户
└── 没找到
有 unionid
├── YES → 查 PersonalCustomerOpenID WHERE union_id=unionid
│ │
│ ├── 找到 → 获取 customer_id → 新增当前 AppID 的 openid 记录
│ │
│ └── 没找到 → 创建新客户 + openid 记录
└── NO → 创建新客户 + openid 记录
```
## Capabilities
### New Capabilities
- `client-asset-token`资产验证令牌机制。A1 接口、asset_token JWT 生成/验证、IP 限频、安全规范(不暴露 asset_id
- `client-wechat-login`:微信登录(公众号+小程序。A2/A3 接口、OAuth/jscode2session 对接、客户查找/创建/合并逻辑、资产绑定(**首次绑定时触发 `asset_status` 从 1→2**、OpenID 多记录管理
- `client-phone-bindng`:手机号绑定/换绑。A4/A5/A6 接口、验证码发送/校验、限频规则、绑定/换绑逻辑
- `client-token-management`:有状态 JWT Token 管理。签发、Redis 存储、有效性检查、退出登录A7、服务端主动失效
- `personal-customer-openid`PersonalCustomerOpenID 模型定义、唯一索引、与 PersonalCustomer 的关系
### Modified Capabilities
- `personal-customer`PersonalCustomer 模型行为变化——登录逻辑从手机号+验证码改为微信授权wx_open_id 字段保留但逻辑迁移到 PersonalCustomerOpenID 表
- `asset-lifecycle-status`:首次客户绑定资产时,`asset_status` 从 1在库自动更新为 2已销售使用条件更新确保幂等
- `wechat-official-account`OAuth 配置来源变化——从 YAML 静态配置改为从 WechatConfig 表动态读取公众号/小程序 AppID+AppSecret
### 微信 SDK 使用说明
本提案使用项目中已有的微信 SDK`pkg/wechat/`,基于 PowerWeChat v3同时需要扩展小程序能力
| 场景 | SDK 方法 | 文件 | 状态 |
|------|---------|------|------|
| A2 公众号登录 | `OfficialAccountService.GetUserInfoDetailed(code)` | `pkg/wechat/official_account.go:69` | ✅ 已有,直接复用 |
| A3 小程序登录 | `MiniAppService.Code2Session(code)` | `pkg/wechat/miniapp.go` | ❌ **需新建**,直接 HTTP 调用微信 jscode2session |
| SDK 实例创建 | `NewOfficialAccountAppFromConfig(wechatConfig)` | `pkg/wechat/config.go` | ❌ **需新增**,从 DB 动态创建 |
| SDK 实例创建 | `NewMiniAppServiceFromConfig(wechatConfig)` | `pkg/wechat/config.go` | ❌ **需新增** |
| SDK 实例创建 | `NewPaymentAppFromConfig(wechatConfig, appID)` | `pkg/wechat/config.go` | ❌ **需新增**,供提案 2 支付使用 |
**现有 `NewOfficialAccountApp(cfg)` 从 YAML 创建实例,客户端场景需要从 `tb_wechat_config` DB 动态加载。**
## Impact
- **新增文件**`internal/model/personal_customer_openid.go`(模型)、`internal/handler/app/client_auth.go`(认证 Handler`internal/service/client_auth/service.go`(认证 Service`internal/store/postgres/personal_customer_openid_store.go`Store、**`pkg/wechat/miniapp.go`(小程序 SDK 封装)**、DTO 文件、迁移文件、常量定义
- **修改文件**`internal/middleware/personal_auth.go`(增加 Redis 检查)、`internal/routes/personal.go`(新增路由)、`internal/bootstrap/`(注册新模块)、`cmd/api/docs.go` + `cmd/gendocs/main.go`(文档生成器)、`pkg/config/defaults/config.yaml`(新增 client 配置节)、`internal/model/system.go`AutoMigrate 注册新模型)、**`pkg/wechat/config.go`(新增 3 个 DB 动态工厂函数)**、**`pkg/wechat/wechat.go`(新增 MiniAppServiceInterface**
- **新增 API 路由**`/api/c/v1/auth/` 下 7 个端点
- **数据库变更**:新建 `tb_personal_customer_openid`
- **新增依赖**:无(微信 SDK 已有 PowerWeChat v3小程序 jscode2session 为纯 HTTP 调用)
- **配置变更**config.yaml 新增 `client.require_phone_binding` 配置项