Files
junhong_cmp_fiber/openspec/changes/add-agent-wallet-qr-recharge/design.md
break cbf909b878
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s
代理在线充值
2026-07-27 16:02:55 +08:00

170 lines
13 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.
## Context
当前 `POST /api/admin/agent-recharges` 已能创建代理充值记录,平台线下代充已经接入企业微信审批;代理在线分支仍是 Stub只保存 `tb_agent_recharge_record` 和当前支付配置,不创建 `tb_payment`,也不向微信或支付宝预下单。现有回调对新支付单可按 `tb_payment.order_type` 分发,但代理充值仍依赖 `ARCH` 前缀回退到旧 Service并在回调事务内同步完成钱包入账。
项目已有可复用基础:
- `tb_payment``PaymentStore` 和按创建配置加载回调验签能力;
- 微信 v3 支付封装及查单能力、支付宝 SDK 客户端和安全回调验签;
- `internal/application/wallet.PostingService`、代理主钱包领域规则和钱包流水唯一索引;
- 公共 Outbox Relay/Consumer、Integration Log、请求指纹工具和支付配置管理
- 代理充值状态 `1=待支付、2=已支付、3=已完成、4=已关闭、5=已退款、6=已驳回`
本变更涉及金额、支付状态机、外部调用、并发幂等和可靠事件,主通道采用 `Handler → Application UseCase → Domain → Repository/Infrastructure`。充值支付状态读取采用 `Handler → Query → GORM/DTO`。未触碰的线下审批、旧列表/详情和其他支付业务不迁移。
利益相关方包括代理付款人、目标店铺主账号、平台财务、支付回调服务和钱包 Worker。
## Goals / Non-Goals
**Goals:**
- 让代理在桌面后台使用微信 Native 或支付宝 PreCreate 二维码为当前店铺主钱包充值。
- 在线单笔金额限制为 `10000100000000` 分,并由信任边界和业务用例双重校验。
- 每次主动提交创建新业务单,同一次网络重试持久化幂等。
- 将第三方收款事实与钱包入账事实分开提交,保证已收款资金可恢复、不重不漏。
- 复用当前支付配置、回调、钱包入账、Outbox 和外部交互日志,不引入新依赖。
- 为前端提供可用支付方式和轻量本地状态轮询接口。
**Non-Goals:**
- 不支持手机浏览器、JSAPI、H5、WAP 或同设备拉起支付。
- 不生成或保存二维码图片,只保存并返回渠道原始付款字符串。
- 不允许代理选择受益店铺或替下级代理充值。
- 不允许平台或超级管理员替代理创建在线支付码。
- 不提供主动取消、支付方式切换、待支付单复用、在线退款或自动反向扣款。
- 不建设多商户通道池、权重路由、自动故障切换或新的支付框架。
- 不迁移未触碰的线下审批、客户资产充值、套餐支付或代理充值查询实现。
## Decisions
### 1. 沿用代理充值资源,按账号类型分流创建用例
继续使用 `POST /api/admin/agent-recharges`Handler 只解析判别请求并转为稳定 Command
- 代理:只允许 `wechat|alipay`,从认证上下文取得 `account_id` 与当前 `shop_id`,在线 Command 不包含可由调用方指定的 `shop_id`
- 平台/超级管理员:只允许现有 `offline` Command继续调用 `OfflineCreationService`
- 企业账号:路由层直接拒绝。
在线创建进入新的 `internal/application/agentrecharge` 用例;旧 Service 可临时作为 Handler 装配门面,但不得继续保存另一套在线创建或支付确认规则。
选择理由:保留前端已有资源路径并限制迁移范围。备选方案是新增 `/online-recharges` 资源,但会复制权限、列表和详情语义,当前没有收益。
### 2. 使用现有状态区分收款与到账,不增加处理状态列
状态语义固定为:
```text
1 待支付 ──支付确认──▶ 2 已支付 ──钱包入账──▶ 3 已完成
└──第三方明确关闭/预下单失败──▶ 4 已关闭
```
`2=已支付` 本身表达“第三方已收款、钱包入账处理中”;失败重试状态由 Outbox/Worker 权威记录承载API 不暴露内部错误。这样无需新增 `processing_status`,也避免两个状态字段组合出非法状态。
迟到的真实成功通知可以从 `1` 或受控关闭状态恢复为支付成功,但必须重新执行完整金额、配置、渠道和交易号校验;不得恢复已退款或已完成且交易号冲突的记录。
选择理由:现有状态已经足够表达用户需要看到的阶段。备选方案是增加独立处理状态列,但第一期只会重复 Outbox 状态并增加组合复杂度。
### 3. 本地事务先建单,渠道预下单后保存付款内容
在线创建分为三个短边界:
1. GORM 事务内锁定幂等作用域,校验当前店铺主钱包和可用支付配置,创建充值单与 `tb_payment`
2. 事务提交后调用对应扫码 Adapter
3. 成功时保存 `qr_content` 并返回,失败时短事务条件关闭支付单和充值单。
不得在数据库事务中持有锁等待第三方网络。若进程在步骤 1 与步骤 2 之间退出,恢复任务按“本地待预下单”事实重试相同 `payment_no`;如果渠道返回订单已存在,则通过查单或渠道等价能力收敛,不能盲目创建另一支付单。
新增最小持久化字段:
- `tb_agent_recharge_record.request_id``request_fingerprint`
- `(user_id, request_id)` 条件唯一索引,保证同一提交账号持久化幂等;
- `tb_payment.qr_content`,仅用于同请求重放和当前所有者首次支付页面恢复;
- `tb_payment.merchant_identity`,创建时固化微信 `wx_mch_id` 或支付宝 `ali_app_id`,与 `payment_method/payment_config_id` 共同支持后续导出对账;
- `(payment_method, third_party_trade_no)` 非空条件唯一索引,禁止第三方交易号跨支付单复用;
- `PaymentOrderTypeAgentRecharge = "agent_recharge"`
`qr_content` 视为敏感支付材料:不写日志和审计正文,不出现在列表、详情或支付状态接口,只在在线创建的首次/幂等响应中返回,并复用现有 Sanitizer 规则。
`merchant_identity` 是支付事实快照,不随配置后续修改而变化;当前支付宝配置未保存商户 PID因此以实际签约应用 ID 作为可核对身份。该快照不属于创建接口的用户可见响应字段。
选择理由:请求重放必须返回原付款内容,数据库事实比 Redis 可靠。备选方案是只将付款内容放 Redis但缓存丢失后无法满足持久化幂等。
### 4. 两个薄 Adapter共用一个最小扫码支付端口
Application 通过结构体字段注入两个明确 Adapter不增加工厂或通道注册框架
```text
WechatNativeAdapter → 现有 PowerWeChat TransactionNative / QueryOrder
AlipayPreCreateAdapter → 现有 smartwalle/alipay TradePreCreate / TradeQuery
```
端口仅覆盖本用例需要的 `PreCreate``Query`,返回统一的付款内容和渠道状态。微信 Adapter 返回 `code_url`,支付宝 Adapter 返回 `qr_code`Application 映射为 `qr_content`。配置缺失和渠道错误转换为 `pkg/errors` 统一错误码Adapter 的每次真实外呼通过现有 Integration Log Repository 记录开始和完成。
选择理由:已有 SDK 已能覆盖需求,无需新增依赖。备选方案是建设通用支付编排框架,但当前只有两个固定分支,属于无请求抽象。
### 5. 支付确认统一收口,钱包入账异步可靠执行
新增 `ConfirmOnlinePayment` 用例作为回调与查单的唯一写入口。输入包含支付单号、渠道、配置身份、第三方交易号、实付金额与支付时间。用例在事务中锁定支付单和充值单,调用纯领域状态规则校验转换,然后:
- 条件更新 `tb_payment` 为已支付并保存第三方交易号;
- 条件更新充值单为 `2=已支付`
- 同事务写 `agent_recharge.payment_confirmed.v1` Outbox。
回调 Handler 保留现有渠道验签与成功报文格式,只把已验证事实传给该用例。新支付单按 `order_type=agent_recharge` 分发;旧 `ARCH` 前缀分支仅兼容迁移前存量单。
消费者以 `agent-recharge:{recharge_id}:credit` 为业务键,在独立 GORM 事务中调用现有 `wallet.PostingService.PostInTx`,复用 `reference_type=topup` 与充值记录 ID 的数据库唯一流水约束,再条件更新充值单为 `3=已完成`。钱包、唯一流水、充值完成状态与已有 `wallet.agent_main.credited` 事件在同一事务提交。
选择理由:支付渠道不能因为钱包短暂故障反复认为回调失败,已收款事实也不能随钱包事务回滚。备选方案是继续在回调事务同步入账,故障耦合和恢复风险不可接受。
### 6. 本地轮询与后台查单职责分离
`GET /api/admin/agent-recharges/:id/payment-status` 走 Query 通道,只读取本地 `tb_agent_recharge_record``tb_payment` 和必要的成功流水事实,不调用第三方,避免桌面端每 3 秒轮询放大外部流量。
后台受控任务批量领取长时间待支付或待预下单记录,通过创建时的 `payment_config_id` 和支付方式调用 Adapter 查单。成功结果复用 `ConfirmOnlinePayment`;明确关闭才条件关闭本地单;未知或超时保持原状态并重试。查询按状态、创建时间和 ID 建立组合索引并分页领取,单批数量固定为项目常量,避免全表扫描。
不为支付状态增加 Redis 缓存:读取是按主键/支付单索引的轻量查询,缓存会引入资金状态短暂不一致,收益不足。
### 7. 领域账本、外部日志与 Outbox 分工
- **Audit Event**:按用户决定为 N/A本 Change 不新增表、Writer、Adapter 或发布门禁。
- **Domain Ledger**`tb_agent_recharge_record``tb_payment``tb_agent_wallet``tb_agent_wallet_transaction` 是支付、充值和资金权威事实。
- **Integration Log**:微信/支付宝预下单、查单和入站回调每次真实尝试均记录,正文经过 Sanitizer禁止保存密钥和完整 `qr_content`
- **Outbox**:支付确认事务写 `agent_recharge.payment_confirmed.v1`;钱包入账继续写现有 `wallet.agent_main.credited`。消费者以稳定业务键幂等。
现有全局审计覆盖基线不作为本 Change 的实现或发布门禁。
### 8. API 与文档装配
新增静态路由 `/payment-methods` 必须注册在 `/:id` 之前;`/:id/payment-status` 使用现有认证和数据范围。DTO 枚举说明从 `pkg/constants` 原文复制int 状态响应同时返回 `status_name`
充值来源不新增数据库列,直接由现有创建方式稳定映射:`offline` 映射为 `platform_offline``wechat|alipay` 映射为 `agent_online`。创建、列表、详情和支付状态响应返回 `recharge_source/recharge_source_name`,列表可按 `recharge_source` 筛选;支付商户对账仍以支付单创建时保存的 `merchant_identity` 为准。
Handler 继续通过 Fiber、Validator、统一 `pkg/response` 和全局 ErrorHandler 工作,不拼接底层错误。新增 Handler 方法时同步更新 OpenAPI RouteSpec若新增 Handler 类型,则同时更新 `cmd/api/docs.go``cmd/gendocs/main.go`。所有导出符号、日志、错误和文档使用中文。
## Risks / Trade-offs
- [本地建单后进程在预下单前退出] → 记录明确待预下单事实,由恢复任务使用原 `payment_no` 重试,禁止创建替代单。
- [第三方回调丢失] → 后台任务受控查单,成功复用统一确认用例。
- [支付成功但钱包 Worker 失败] → 保持 `2=已支付`Outbox/Worker 重试;唯一流水约束保证不重入账。
- [重复或伪造回调] → 渠道验签、创建配置身份、金额、业务关联和第三方交易号唯一约束共同防护。
- [保存付款内容扩大敏感面] → 仅保存原始字符串用于幂等重放,禁止日志/审计输出,读取只限创建者且其他查询不返回。
- [轮询流量增加] → 使用本地主键/组合索引查询,前端页面可见时每 3 秒轮询,终态立即停止;不引入缓存。
- [旧代理充值单仍依赖前缀回调] → 保留有界兼容分支;所有新单必须有 `tb_payment.order_type=agent_recharge`,后续存量清零后再独立删除兼容逻辑。
- [支付 SDK Native/PreCreate 方法签名与当前版本不同] → 实施前以已锁定依赖版本的实际 API 做最小适配,不升级或替换 SDK。
## Migration Plan
1. 增加可回滚迁移:请求幂等字段、`qr_content`、必要的唯一/查询索引和注释;迁移前检查重复第三方交易号,禁止静默清洗资金事实。
2. 增加常量、DTO、领域状态规则、扫码 Adapter、在线创建/确认/入账 Application 与 Query并完成结构体字段注入。
3. 注册 API 与回调分发,更新 OpenAPI 生成装配、README 和功能总结。
4. 先运行迁移与后端服务,再同时切换回调/Worker 和代理后台入口;未启用前前端不展示扫码充值。
5. 在联调环境分别完成微信和支付宝真实扫码人工核对重复回调、回调丢失查单、Worker 重试和唯一流水。
6. 观察支付成功未完成数量、Outbox 重试、Integration Log 失败率和钱包入账延迟后再开放生产入口。
回滚时先关闭前端入口和在线支付方式,再停止创建新单;回调、查单和钱包 Worker MUST 保持运行直至已收款订单全部收敛。应用代码可回滚到兼容读旧字段版本,新增列和索引最后单独回滚;不得通过删除待处理支付单完成回滚。
## Open Questions
无。业务范围已经确认:仅桌面扫码、最低 100 元、代理仅充自己主钱包、每次主动提交新单、同请求持久化幂等、第一期不做主动取消和在线退款。