# 设计文档：client-core-business-api

## Context

认证系统（提案 1）就绪后，客户端需要一套完整业务接口以覆盖资产查询、钱包充值、套餐购买、实名跳转与设备操作。当前后台接口的 Service 层大部分能力可复用，但客户端场景存在以下关键差异：

1. 个人客户访问资源不应受 shop_id 数据权限过滤影响，需要在调用链中显式绕过。
2. 资产操作必须先做归属校验（绑定关系），避免跨客户操作。
3. 历史数据查询需要按资产当前 generation 过滤，避免展示转手前历史。
4. 支付 OpenID 必须后端查表获取，禁止客户端传入，降低伪造风险。

现有代码参考（复用/改造基线）：

- `asset.Service.Resolve()`：`internal/service/asset/service.go:71`
- `asset.Service.Refresh()`：`internal/service/asset/service.go:295`
- `asset.Service.GetPackages()`：`internal/service/asset/service.go:347`
- `recharge.Service.GetRechargeCheck()`：`internal/service/recharge/service.go:168`
- `recharge.Service.Create()`：`internal/service/recharge/service.go:83`
- `order.Service.CreateH5Order()`：`internal/service/order/service.go:632`
- `order.Service.checkForceRechargeRequirement()`：`internal/service/order/service.go:2216`
- `order.Service.WechatPayJSAPI()`：`internal/service/order/service.go:2095`
- `purchaseValidation.ValidateCardPurchase()`：`internal/service/purchase_validation/service.go:44`
- Gateway 设备能力：`internal/gateway/device.go:41-67`
- Gateway 实名链接：`internal/gateway/flow_card.go:44`

## Goals

本次变更目标是交付 `/api/c/v1/` 下 18 个客户端业务端点，覆盖 5 个模块：

- 模块 B（资产信息）：B1~B4
- 模块 C（钱包与充值）：C1~C5
- 模块 D（套餐购买）：D1~D3
- 模块 E（实名跳转）：E1
- 模块 F（设备能力）：F1~F5

## Non-Goals

- 不改动后台管理端 API 行为与路由。
- 不引入或改造 exchange（交易所）体系。
- 不重做既有支付网关对接协议，仅在客户端入口补齐调用与安全约束。

## Decisions

### 1) Handler 组织

客户端 Handler 按模块拆分为 5 个文件，统一放在 `internal/handler/app/`：

- `client_asset.go`
- `client_wallet.go`
- `client_order.go`
- `client_realname.go`
- `client_device.go`

这样可保持模块边界清晰，减少单文件复杂度，便于后续迭代。

### 2) Service 复用策略

- 直接复用：B1/B3/B4/C1/C2/C3 复用现有 Service 能力（补充客户端上下文约束）。
- 新增逻辑：B2 需新增“渠道价 + 加油包前置校验 + 不可售过滤 + 价格排序”逻辑。
- 新建 `client_order` Service：C4/D1 引入客户端订单编排，复用 `order/recharge` 的底层能力但增加客户端专属流程控制。

### 3) 数据权限绕过

客户端调用 `asset/wallet` 等后台复用 Service 时，统一使用 `gorm.SkipDataPermission(ctx)`，绕过 shop_id 自动过滤，避免个人客户因非店铺主体被误拦截。

### 4) 归属校验方案

所有涉及资产操作接口统一前置：

- 查询 `PersonalCustomerDevice` 条件：`customer_id = 当前登录客户` 且 `virtual_no = 资产虚拟号`
- 未命中即返回 403：`无权限操作该资产或资源不存在`

该规则覆盖 B/C/D/E/F 全模块写操作与敏感读操作。

### 5) Generation 过滤

客户端历史查询统一附加条件：`WHERE generation = 资产当前 generation`，适用于订单、充值、套餐历史。

- 客户端：必须过滤
- 后台：不加该过滤（保留全量视图）

### 6) OpenID 安全规范 + 微信支付 SDK 实例选择

支付相关接口（C4/D1）所需 OpenID 必须由后端按 `customer_id + app_type` 查询 `PersonalCustomerOpenID`。

- 客户端请求体禁止携带 `openid`，仅传 `app_type`（`official_account` 或 `miniapp`）
- 缺失时返回 `OPENID_NOT_FOUND`

**微信支付 SDK 实例选择逻辑**：

支付时需根据 `app_type` 创建不同的 `PaymentService` 实例，因为微信 JSAPI 支付绑定的 AppID 必须与用户 OpenID 所属的应用一致：

```
客户端传入 app_type
    │
    ├─ "official_account" → 用 WechatConfig.oa_app_id 创建 Payment 实例
    │                       → 查 PersonalCustomerOpenID WHERE app_id=oa_app_id 获取 openid
    │
    └─ "miniapp"          → 用 WechatConfig.miniapp_app_id 创建 Payment 实例
                           → 查 PersonalCustomerOpenID WHERE app_id=miniapp_app_id 获取 openid
```

**使用的现有 SDK 方法**（`pkg/wechat/payment.go`，不需要修改）：

| 方法 | 签名 | 用途 |
|------|------|------|
| `CreateJSAPIOrder` | `(ctx, orderNo, description, openID string, amount int) (*JSAPIPayResult, error)` | 公众号/小程序内拉起支付，返回 `prepay_id` + `PayConfig`（可直接传给前端 `wx.requestPayment`） |
| `HandlePaymentNotify` | `(r *http.Request, callback PaymentNotifyCallback) (*http.Response, error)` | 支付回调验签+解密，回调函数接收 `*PaymentNotifyResult` |
| `QueryOrder` | `(ctx, orderNo string) (*OrderInfo, error)` | 主动查询订单状态 |
| `CloseOrder` | `(ctx, orderNo string) error` | 关闭未支付订单 |

**SDK 实例创建**（使用提案 1 新增的工厂函数）：

```go
// 在 client_order Service 中：
config, _ := s.wechatConfigService.GetActiveConfig(ctx)  // 从 DB/Redis 缓存
appID := config.OaAppID
if req.AppType == "miniapp" {
    appID = config.MiniappAppID
}
paymentApp, _ := wechat.NewPaymentAppFromConfig(config, appID, cache, logger)
paymentService := wechat.NewPaymentService(paymentApp, logger)
// 调用 paymentService.CreateJSAPIOrder(ctx, orderNo, desc, openID, amount)
```

**注意**：`CreateH5Order`（外部浏览器 H5 支付）在客户端场景中**不使用**——客户端始终在微信内（公众号 H5 或小程序），一律走 JSAPI 支付。

### 7) 强充两阶段设计

强充场景采用“同步入账 + 异步自动购买”两阶段：

- 第一阶段（同步事务内）：
  1. 钱入钱包
  2. 更新充值记录状态
  3. 更新累计充值/首充状态
- 第二阶段（异步 Asynq）：
  1. 从钱包扣款
  2. 创建套餐订单
  3. 激活套餐

`AssetRechargeRecord` 新增 `auto_purchase_status` 字段追踪异步状态（pending/success/failed）。

### 8) D1 返回结构分流

`POST /api/c/v1/orders/create` 根据是否触发强充返回不同结构：

- `order_type = "package"`：直接返回 `order + pay_config`
- `order_type = "recharge"`：返回 `recharge + pay_config + linked_package_info`

前端据 `order_type` 决定支付结果页与文案。

### 9) 实名闭环说明

运营商实名为外部流程，无平台回调。用户完成实名后需主动触发 B4 刷新资产状态，再重新发起购买流程。

## Risks / Trade-offs

1. **强充异步失败风险**：第二阶段失败会导致“钱已到账、套餐未生效”的中间态。权衡后采用可重试 + `auto_purchase_status=failed` + 用户可手动购买的降级方案。
2. **Gateway 超时风险**：B4/F2/F3/F4/F5/E1(gateway) 依赖外部网关，网络抖动可能放大请求延迟。需统一超时、重试与可观测日志。
3. **OpenID 缺失风险**：用户未完成公众号授权时无法拉起支付。需明确错误码 `OPENID_NOT_FOUND` 并引导重新授权。
4. **Generation 不一致风险**：资产转手或切换后，若查询未按 generation 过滤会出现历史串数据。客户端侧强制过滤会增加查询条件复杂度，但可换取数据隔离正确性。
5. **服务复用边界风险**：复用后台 Service 可加速交付，但若遗漏客户端前置条件（归属、权限绕过），会造成逻辑缺口。需在 Handler 层统一封装公共校验。

## Migration Plan

数据库迁移新增字段：

- 表：`tb_asset_recharge_record`
- 字段：`auto_purchase_status`（建议 `varchar(20)`，默认 `pending`）
- 用途：记录强充回调后二阶段自动购买状态

迁移步骤：

1. 新增迁移文件（up/down）。
2. 执行迁移并确认版本无 dirty。
3. 更新对应 Model 与常量枚举。
4. 回调逻辑写入状态流转：`pending -> success/failed`。
