Files
junhong_cmp_fiber/docs/7月迭代/独立方案/新增需求/05-代理钱包扫码充值.md
2026-07-22 11:34:20 +09:00

457 lines
17 KiB
Markdown
Raw 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.
# 新增需求 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. 验证平台员工线下代充值仍按原企微审批方案执行,不受在线充值改造影响。