# 新增需求 05：代理钱包扫码充值

> 状态：已冻结，本文保留为实施明细；如有冲突，以标准评审稿和 UR#34 PRD 为准。
> 评审主文档：`../../7月迭代技术方案-标准评审稿.md`
> 实施 PRD：`../../../../.scratch/ur34-agent-recharge/PRD.md`
> 范围：代理在后台使用微信或支付宝扫码充值代理主钱包。

## 一、已确认决策

1. 代理在后台为自己的店铺主钱包充值。
2. 支付方式支持微信扫码和支付宝扫码。
3. 单笔最低充值金额为 100 元，即 `10000` 分。
4. 代理在线充值不进入企业微信审批；支付成功事实先落库，再由可靠 Worker 幂等增加代理主钱包余额。
5. 平台员工线下代充值仍按 `02-企业微信审批接入.md` 走企微审批，与本方案隔离。
6. 后端返回支付二维码内容，前端使用现有二维码组件渲染，不由后端生成或保存二维码图片文件。
7. 微信使用 Native 支付，支付宝使用 `alipay.trade.precreate` 当面付预创建。
8. 支付回调、钱包入账、钱包流水和审计必须幂等，重复回调不能重复加钱。
9. 当前代理充值复杂写逻辑迁移到 Application/Domain，旧 Service 不再保留另一套在线入账逻辑。

## 二、现状与缺口

现有代码已经具备代理充值记录、微信/富友回调和钱包入账骨架，但还不能满足本需求：

- `CreateAgentRechargeRequest` 只允许 `wechat/offline`，没有支付宝。
- `AgentRechargeMinAmount=1`，当前最低金额是 1 分。
- 创建接口只生成充值记录，没有创建统一 `tb_payment` 支付单，也没有真正向支付渠道预下单获取二维码。
- 微信支付仅保存当前生效支付配置，响应中没有 `code_url`。
- 支付宝回调只分发套餐订单和客户资产钱包充值，没有代理充值订单类型。
- `HandlePaymentCallback` 在旧 Service 中直接更新充值单、钱包和流水，业务状态和幂等边界没有收口为独立用例。
- 前端技术方案只写了“保留收款码和支付状态页面”，没有支付方式选择、最低金额、第三方状态收敛和支付/入账状态细节。

现有评审中“代理在线充值不审批”的结论保持不变，本次补齐的是扫码支付和最低金额的完整技术方案。

## 三、业务流程

```mermaid
sequenceDiagram
    actor Agent as 代理用户
    participant Web as 代理后台
    participant API as Recharge Application
    participant DB as PostgreSQL
    participant Pay as 微信/支付宝
    participant Callback as 支付回调
    participant Wallet as AgentWallet Domain

    Agent->>Web: 输入金额并选择支付方式
    Web->>API: 创建扫码充值单
    API->>DB: 创建充值单和支付单
    API->>Pay: Native/PreCreate 预下单
    Pay-->>API: 二维码内容
    API-->>Web: 原样返回 qr_content
    Web-->>Agent: 展示二维码并轮询支付状态
    Agent->>Pay: 扫码完成支付
    Pay->>Callback: 异步支付通知
    Callback->>API: ConfirmAgentRechargePayment
    API->>DB: 固化支付成功并写入账 Outbox
    API-->>Pay: 返回成功
    DB-->>Wallet: Worker 消费入账任务
    Wallet->>DB: 独立事务增加余额、写流水、完成充值单
    Web->>API: 查询到已完成
    Web-->>Agent: 展示最新钱包余额
```

支付成功不创建 `wecom_approval_instance`，充值详情固定返回：

```json
{
  "approval_source": "none",
  "approval": null
}
```

## 四、DDD 设计

### 4.1 目录

```text
internal/
├── domain/agentwallet/
│   ├── wallet.go                  代理主钱包聚合及余额不变量
│   ├── recharge.go                充值单状态转换
│   ├── events.go                  充值到账领域事件
│   └── repository.go              钱包与充值聚合仓储接口
├── application/agentrecharge/
│   ├── create_qr_recharge.go      创建充值单和扫码支付
│   ├── confirm_payment.go         支付回调或查单确认支付事实
│   ├── post_wallet.go             可靠任务执行钱包入账
│   ├── sync_pending.go            受控查询待支付第三方订单
│   └── get_payment_status.go      轻量支付状态查询
├── infrastructure/adapter/payment/
│   ├── wechat_native.go           微信 Native 预下单
│   └── alipay_precreate.go        支付宝当面付预创建
├── infrastructure/persistence/
│   └── agent_recharge_repository.go
└── query/agentrecharge/
    ├── list.go
    └── detail.go
```

### 4.2 领域状态

继续使用现有充值状态，不为在线充值增加审批状态：

| 状态 | 含义 | 在线充值使用方式 |
|---:|---|---|
| 1 | 待支付 | 已创建二维码，等待扫码 |
| 2 | 已支付 | 支付已确认，钱包入账事务处理中 |
| 3 | 已完成 | 钱包余额和流水已完成 |
| 4 | 已关闭 | 第三方明确未支付且已关闭/失效，或线下审批撤销/删除 |
| 5 | 已退款 | 历史或后续人工退款结果 |
| 6 | 已驳回 | 仅旧数据或线下审批兼容，在线充值不产生 |

`status=2` 是支付事实或审批通过已经固化、钱包正在处理的业务状态。只有独立钱包入账事务成功后才推进到 `3`；发生可恢复错误时保持 `2`，使用 `processing_status=3`表达失败并由可靠任务继续处理。

### 4.3 钱包入账不变量

`AgentWallet.CreditRecharge` 必须保证：

- 只允许向目标店铺的 `wallet_type=main` 钱包入账。
- 入账金额必须等于充值单金额和支付单金额。
- 同一充值单只能生成一条成功钱包流水。
- Worker 在同一数据库事务中更新钱包余额、`version`、充值单状态和钱包流水；支付事实由更早的独立事务固化，钱包失败不得回滚真实收款。
- 钱包乐观锁冲突时由 Application 重新加载后有限重试，不能重复创建流水。
- 支付渠道成功不等于业务已经完成；只有钱包事务成功后充值单才变为已完成。

## 五、支付预下单

### 5.1 统一支付单

代理在线充值必须创建 `tb_payment` 记录，不能只依赖 `ARCH` 单号前缀判断支付渠道。

新增支付业务类型：

```go
const PaymentOrderTypeAgentRecharge = "agent_recharge"
```

充值单和支付单在同一事务创建：

```text
tb_agent_recharge_record.status = 1
tb_payment.status = 0
tb_payment.order_type = agent_recharge
tb_payment.order_id = recharge_id
tb_payment.payment_method = wechat / alipay
tb_payment.amount = recharge_amount
tb_payment.payment_config_id = 创建时使用的配置ID
```

先完成本地事务，再调用第三方预下单。预下单失败时把支付单标记为失败并关闭本次充值单，代理重新创建，不复用来源不明确的旧二维码。

### 5.2 微信扫码

微信使用 Native 下单：

```text
微信支付 v3 TransactionNative
  -> 返回 code_url
```

现有微信 SDK 已包含 `TransactionNative`，需要在项目支付 Adapter 中封装，不在 Handler 直接调用 SDK。

若当前生效支付配置为：

- `wechat`：使用微信 v3 Native。
- `wechat_v2`：补充 v2 Native 统一下单实现。
- `fuiou`：只有现有富友配置明确支持后台扫码产品时才返回微信可用；不支持时前端隐藏微信扫码入口，不擅自用 JSAPI 代替。

### 5.3 支付宝扫码

支付宝使用当前 SDK 已提供的：

```text
alipay.trade.precreate
  -> 返回 qr_code
```

不复用现有 WAP 支付 URL。创建时校验当前支付配置中的：

```text
ali_app_id
ali_private_key
ali_public_key
ali_notify_url
```

配置不完整时支付宝方式显示为不可用，不能创建只有本地记录而没有有效二维码的充值单。

### 5.4 二维码响应

后端统一返回二维码内容，不返回二维码图片：

```json
{
  "recharge_id": 88,
  "recharge_no": "ARCH20260715143000000001",
  "payment_no": "ARCH20260715143000000001",
  "payment_method": "wechat",
  "amount": 10000,
  "qr_content": "weixin://wxpay/bizpayurl?...",
  "status": 1,
  "status_name": "待支付",
  "payment_status": 0,
  "payment_status_name": "待支付",
  "processing_status": 0,
  "processing_status_name": "未触发"
}
```

微信返回 `code_url`、支付宝返回 `qr_code`，Application 统一映射为 `qr_content`。

本地无法准确知道第三方订单的真实失效时间，因此接口不返回 `expires_at`，前端不展示本地推算的精确倒计时。第三方支付成功或关闭以回调和后端受控查单为准。

## 六、支付确认与可靠入账

### 6.1 回调校验

微信和支付宝回调继续执行现有签名、安全和金额校验，并增加代理充值分发：

```text
payment.order_type = agent_recharge
  -> ConfirmAgentRechargePayment
```

必须校验：

- 支付单存在且支付方式与回调渠道一致。
- `payment_config_id` 与创建支付单时配置一致。
- 回调金额等于支付单和充值单金额。
- 第三方交易号没有被其他支付单占用。
- 充值单属于支付单中的 `order_id`，不能只依赖订单号前缀。

### 6.2 回调事务

支付事实事务：

1. 按 `payment_no` 加载并核验支付单。
2. 支付单从待支付条件更新为已支付。
3. 充值单从 `1=待支付` 更新为 `2=已支付`，`processing_status=1`。
4. 可靠写入钱包入账 Outbox 后向支付渠道返回成功。

钱包入账 Worker 的独立事务：

1. 加载代理主钱包并执行 `CreditRecharge`。
2. 更新钱包余额和 `version`。
3. 创建唯一钱包流水。
4. 充值单从 `2=已支付` 更新为 `3=已完成`，`processing_status=2`。
5. 写资金 Audit Event 和到账通知事件。

幂等键：

```text
agent_recharge:{recharge_id}:credit
```

数据库还需要保证钱包流水 `reference_type=agent_recharge + reference_id=recharge_id` 唯一。Redis 只用于削减重复并发，不能替代数据库幂等。

### 6.3 回调失败恢复

- 第三方校验失败：拒绝回调，不改变业务状态。
- 支付状态已成功、钱包事务失败：充值单保持已支付，`processing_status=3`，由可靠任务继续入账。
- 钱包流水已存在但充值单未完成：恢复任务只补齐充值单状态。
- 重复回调：查询到支付单或充值单已完成后直接返回渠道成功报文。
- 本功能不引入审批，也不会因为支付金额较大转入审批。

## 七、金额与权限

### 7.1 金额

```go
const AgentRechargeMinAmount int64 = 10000
```

- 单位固定为分。
- DTO 使用 `min=10000`，Service/Application 必须再次校验。
- 前端输入单位为元，提交前转换为分。
- 最大金额固定为 `100000000` 分。
- 禁止前端通过浮点数直接计算金额，元转分使用字符串或十进制定点处理。

### 7.2 权限

- 代理只能为当前登录账号所属店铺的主钱包充值。
- 后端从登录上下文确定当前代理店铺，在线请求不接受 `shop_id`。
- 平台和超级管理员不能替代理发起在线扫码充值，只能为明确的目标 `shop_id` 发起 `offline` 线下代充值。
- 企业账号无权访问代理充值接口。
- 创建权限与查看权限分离。代理在具备充值查看权限时，按既有店铺层级数据范围查看本店及有权管理的下级店铺充值；列表、详情和支付状态使用同一范围。

## 八、API 设计

### 8.1 可用支付方式

```http
GET /api/admin/agent-recharges/payment-methods
```

响应：

```json
{
  "items": [
    {"method": "wechat", "name": "微信支付", "enabled": true},
    {"method": "alipay", "name": "支付宝", "enabled": true}
  ],
  "min_amount": 10000,
  "max_amount": 100000000
}
```

该路由必须先于 `/:id` 动态路由注册。

### 8.2 创建扫码充值

沿用现有接口：

```http
POST /api/admin/agent-recharges
```

```json
{
  "amount": 10000,
  "payment_method": "wechat",
  "request_id": "01J2RECHARGE..."
}
```

`payment_method` 调整为：

```text
wechat / alipay / offline
```

其中 `offline` 仅平台员工线下代充值使用，并继续走企微审批；代理用户只能选择 `wechat/alipay`。

`request_id` 只用于防止同一次提交的重试重复创建。代理每次主动创建或再次拉起支付都必须使用新的 `request_id`，并创建新的充值单和支付单；此前未付款订单继续等待第三方自然收敛，不按金额复用旧单。

### 8.3 查询支付状态

```http
GET /api/admin/agent-recharges/{id}/payment-status
```

```json
{
  "status": 3,
  "status_name": "已完成",
  "payment_status": 1,
  "payment_status_name": "已支付",
  "paid_at": "2026-07-15T14:35:00+08:00",
  "completed_at": "2026-07-15T14:35:01+08:00",
  "processing_status": 2,
  "processing_status_name": "处理成功"
}
```

该接口只返回当前登录代理有权访问的充值单，不返回支付密钥、签名参数或其他店铺余额。

## 九、前端方案

### 9.1 入口

代理后台钱包页面保留“充值”按钮，点击后打开充值弹窗或抽屉，不新增营销页面。

控件：

- 金额使用数字输入框，单位为元，明确最低 100 元。
- 支付方式使用微信/支付宝分段控件，带对应图标。
- 不可用渠道禁用并显示简短原因。
- 主按钮为“立即充值”。按钮提交创建充值请求，后端不提供独立的二维码生成接口。

### 9.2 二维码状态

创建成功后展示：

```text
充值金额
支付方式
二维码
支付状态
钱包入账状态
```

- 前端使用 `qr_content` 生成二维码，不请求后端图片文件。
- 页面可见时每 3 秒查询一次轻量支付状态。
- 页面隐藏时暂停轮询，恢复可见时立即查询。
- 状态变为已完成、已关闭或离开页面时停止轮询。
- 不显示本地推算的精确过期时间，也不提供后端“取消/重新生成二维码”接口；用户再次主动拉起时按一次全新的充值创建处理。
- 支付完成后关闭二维码区域，刷新钱包余额并展示充值成功结果。
- 全流程不展示审批状态或企微审批区块。

### 9.3 列表和详情

代理充值列表增加支付方式和支付状态：

```text
充值单号 | 金额 | 支付方式 | 支付状态 | 充值状态 | 创建时间 | 完成时间
```

在线充值详情返回 `approval_source=none`。平台员工线下代充值详情继续展示企微审批信息，两类记录按 `payment_method` 区分。

## 十、审计与通知

统一审计至少记录：

```text
代理创建充值单
支付预下单成功/失败
微信/支付宝支付回调成功/失败
钱包入账成功/失败
重复回调被幂等忽略
第三方查单确认支付关闭或失效
```

支付渠道交互写 `tb_integration_log`，钱包余额变化写关键 `Audit Event`，并关联充值单、支付单、代理钱包和钱包流水。

在线充值不产生审批通知。目标代理主钱包实际入账后必须生成“充值到账”站内通知；在线实际提交账号与目标代理主账号不同时，两者分别通知并按充值单与接收人防重。平台线下代充值的真实提交人只接收 UR#37 的审批结果通知，除非其本身也是到账通知接收人。

## 十一、代码迁移范围

### 11.1 必须修改

- `AgentRechargeMinAmount` 从 `1` 调整为 `10000`。
- `CreateAgentRechargeRequest.payment_method` 增加 `alipay`，金额校验改为 `min=10000`。
- 创建代理在线充值时同时创建 `tb_payment` 记录。
- 新增 `PaymentOrderTypeAgentRecharge`。
- 微信支付 Adapter 增加 Native 预下单。
- 支付宝 Adapter 增加 `TradePreCreate`。
- 支付宝回调增加代理充值分发。
- 微信/富友代理充值回调统一改为按支付单分发，不只依赖 `ARCH` 前缀。
- 原 `agent_recharge.Service.HandlePaymentCallback` 迁入 `ConfirmAgentRechargePayment` 用例。
- 前端增加支付方式查询、二维码展示和支付状态轮询。

### 11.2 保持不变

- 代理在线充值不进入企微审批。
- 平台员工线下代充值继续走企微审批。
- 代理只能充值自己的店铺主钱包。
- 钱包流水仍是资金变化权威记录。
- 现有支付回调验签和金额校验原则保持不变。

## 十二、发布与人工验证

停机发布，API 与回调服务同时切换：

1. 验证 99.99 元被后端拒绝，100 元可以创建充值单。
2. 验证代理只能为自己的店铺创建微信或支付宝充值。
3. 验证微信 Native 返回有效 `code_url`，前端能够扫码支付。
4. 验证支付宝 PreCreate 返回有效 `qr_code`，前端能够扫码支付。
5. 验证支付回调通过支付单类型分发到代理充值用例。
6. 验证微信、支付宝回调金额不一致时不会增加钱包余额。
7. 验证支付成功后不创建企微审批实例，先固化支付事实，再由可靠 Worker 完成钱包入账。
8. 验证重复回调只产生一条钱包流水，余额只增加一次。
9. 验证支付成功但钱包事务暂时失败时能够恢复完成，不需要代理重复支付。
10. 验证接口不返回 `expires_at`，前端不展示本地倒计时；第三方明确关闭后充值单才关闭，迟到成功回调仍能幂等入账。
11. 验证代理充值详情固定返回 `approval_source=none`，不展示审批区域。
12. 验证平台员工线下代充值仍按原企微审批方案执行，不受在线充值改造影响。
