## 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`（短时效 JWT，5 分钟过期，payload 含 asset_type + asset_id）。IP 级别限频（30 次/分钟）防暴力枚举。不暴露内部 asset_id
- **A2 微信公众号登录** `POST /wechat-login`：无需认证。用微信 OAuth code + asset_token → 查找/创建客户 → 绑定资产 → 签发有状态 JWT Token（Redis 存储）→ 返回 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_token（5分钟有效）
    │
    ▼
微信授权（前端完成）
    │
    ├─── 公众号 ──→ [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` 配置项
