# 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_token（5分钟有效）
    │
    ▼
微信授权（前端完成）
    │
    ├── 公众号 → [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  → 进入主页面
```

## 核心设计

### 有状态 JWT（JWT + Redis）

- JWT payload 仅含 `customer_id` + `exp`
- 登录时将 token 写入 Redis，TTL 与 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` | 认证 Handler（7 个端点） |
| `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` | 是否要求绑定手机号 |
