Files
huang df76e33105 feat: 实现 C 端完整认证系统(client-auth-system)
实现面向个人客户的 7 个认证接口(A1-A7),覆盖资产验证、
微信公众号/小程序登录、手机号绑定/换绑、退出登录完整流程。

主要变更:
- 新增 PersonalCustomerOpenID 模型,支持多 AppID 多 OpenID 管理
- 实现有状态 JWT(JWT + Redis 双重校验),支持服务端主动失效
- 扩展微信 SDK:小程序 Code2Session + 3 个 DB 动态工厂函数
- 实现 A1 资产验证 IP 限流(30/min)和 A4 三层验证码限流
- 新增 7 个错误码(1180-1186)和 6 个 Redis Key 函数
- 注册 /api/c/v1/auth/* 下 7 个端点并更新 OpenAPI 文档
- 数据库迁移 000083:新建 tb_personal_customer_openid 表
2026-03-19 11:33:41 +08:00

142 lines
5.5 KiB
Markdown
Raw Permalink 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.
# C 端认证系统功能总结
## 概述
本次实现了面向个人客户C 端)的完整认证体系,替代旧 H5 登录接口。支持微信公众号和小程序两种登录方式,基于「资产标识符验证 → 微信授权 → 自动绑定资产 → 可选绑定手机号」的流程。
## 接口列表
| 接口 | 路径 | 认证 | 说明 |
|------|------|------|------|
| A1 | `POST /api/c/v1/auth/verify-asset` | 否 | 资产标识符验证,返回 asset_token |
| A2 | `POST /api/c/v1/auth/wechat-login` | 否 | 微信公众号登录 |
| A3 | `POST /api/c/v1/auth/miniapp-login` | 否 | 微信小程序登录 |
| A4 | `POST /api/c/v1/auth/send-code` | 否 | 发送手机验证码 |
| A5 | `POST /api/c/v1/auth/bind-phone` | 是 | 首次绑定手机号 |
| A6 | `POST /api/c/v1/auth/change-phone` | 是 | 换绑手机号(双验证码) |
| A7 | `POST /api/c/v1/auth/logout` | 是 | 退出登录 |
## 登录流程
```
用户输入资产标识符SN/IMEI/ICCID
[A1] verify-asset → asset_token5分钟有效
微信授权(前端完成)
├── 公众号 → [A2] wechat-login (code + asset_token)
└── 小程序 → [A3] miniapp-login (code + asset_token)
解析 asset_token → 获取微信 openid
→ 查找/创建客户 → 绑定资产
→ 签发 JWT + Redis 存储
返回 { token, need_bind_phone, is_new_user }
need_bind_phone == true?
YES → [A4] 发送验证码 → [A5] 绑定手机号
NO → 进入主页面
```
## 核心设计
### 有状态 JWTJWT + Redis
- JWT payload 仅含 `customer_id` + `exp`
- 登录时将 token 写入 RedisTTL 与 JWT 一致
- 每次请求在中间件同时校验 JWT 签名和 Redis 有效状态
- 支持服务端主动失效(封禁、强制下线、退出登录)
- 单点登录:新登录覆盖旧 token
### OpenID 多记录管理
- 新增 `tb_personal_customer_openid`
- 同一客户可在多个 AppID公众号/小程序)下拥有不同 OpenID
- 唯一约束:`UNIQUE(app_id, open_id) WHERE deleted_at IS NULL`
- 客户查找逻辑openid 精确匹配 → unionid 回退合并 → 创建新客户
### 资产绑定
- 每次登录创建 `PersonalCustomerDevice` 绑定记录
- 同一资产允许被多个客户绑定(支持转手场景)
- 首次绑定时自动将资产状态从「在库(1)」更新为「已销售(2)」
### 微信配置动态加载
- 登录时从数据库 `tb_wechat_config` 动态读取激活配置
- 优先走 WechatConfigService 的 Redis 缓存
- 小程序登录直接 HTTP 调用微信 `jscode2session`(不依赖 PowerWeChat SDK
## 限流策略
| 接口 | 维度 | 限制 |
|------|------|------|
| A1 | IP | 30 次/分钟 |
| A4 | 手机号 | 60 秒冷却 |
| A4 | IP | 20 次/小时 |
| A4 | 手机号 | 10 次/天 |
## 新增/修改文件
### 新增文件
| 文件 | 说明 |
|------|------|
| `internal/model/personal_customer_openid.go` | OpenID 关联模型 |
| `internal/model/dto/client_auth_dto.go` | A1-A7 请求/响应 DTO |
| `internal/store/postgres/personal_customer_openid_store.go` | OpenID Store |
| `internal/service/client_auth/service.go` | 认证 Service核心业务逻辑 |
| `internal/handler/app/client_auth.go` | 认证 Handler7 个端点) |
| `pkg/wechat/miniapp.go` | 小程序 SDK 封装 |
| `migrations/000083_add_personal_customer_openid.up.sql` | 迁移文件 |
| `migrations/000083_add_personal_customer_openid.down.sql` | 回滚文件 |
### 修改文件
| 文件 | 说明 |
|------|------|
| `internal/middleware/personal_auth.go` | 增加 Redis 双重校验 |
| `pkg/constants/redis.go` | 新增 token 和限流 Redis Key |
| `pkg/errors/codes.go` | 新增错误码 1180-1186 |
| `pkg/config/defaults/config.yaml` | 新增 `client.require_phone_binding` |
| `pkg/wechat/wechat.go` | 新增 MiniAppServiceInterface |
| `pkg/wechat/config.go` | 新增 3 个 DB 动态工厂函数 |
| `internal/bootstrap/types.go` | 新增 ClientAuth Handler 字段 |
| `internal/bootstrap/handlers.go` | 实例化 ClientAuth Handler |
| `internal/bootstrap/services.go` | 初始化 ClientAuth Service |
| `internal/bootstrap/stores.go` | 初始化 OpenID Store |
| `internal/routes/personal.go` | 注册 7 个认证端点 |
| `cmd/api/docs.go` | 注册文档生成器 |
| `cmd/gendocs/main.go` | 注册文档生成器 |
## 错误码
| 码值 | 常量名 | 说明 |
|------|--------|------|
| 1180 | CodeAssetNotFound | 资产不存在 |
| 1181 | CodeWechatConfigUnavailable | 微信配置不可用 |
| 1182 | CodeSmsSendFailed | 短信发送失败 |
| 1183 | CodeVerificationCodeInvalid | 验证码错误或已过期 |
| 1184 | CodePhoneAlreadyBound | 手机号已被其他客户绑定 |
| 1185 | CodeAlreadyBoundPhone | 已绑定手机号不可重复绑定 |
| 1186 | CodeOldPhoneMismatch | 旧手机号与当前绑定不匹配 |
## 数据库变更
- 新建表 `tb_personal_customer_openid`(迁移 000083
- 唯一索引:`idx_pco_app_id_open_id` (app_id, open_id) 软删除条件
- 普通索引:`idx_pco_customer_id` (customer_id)
- 条件索引:`idx_pco_union_id` (union_id) WHERE union_id != ''
## 配置项
| 配置路径 | 环境变量 | 默认值 | 说明 |
|---------|---------|-------|------|
| `client.require_phone_binding` | `JUNHONG_CLIENT_REQUIRE_PHONE_BINDING` | `true` | 是否要求绑定手机号 |