This commit is contained in:
169
openspec/changes/add-agent-wallet-qr-recharge/design.md
Normal file
169
openspec/changes/add-agent-wallet-qr-recharge/design.md
Normal file
@@ -0,0 +1,169 @@
|
||||
## 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 二维码为当前店铺主钱包充值。
|
||||
- 在线单笔金额限制为 `10000~100000000` 分,并由信任边界和业务用例双重校验。
|
||||
- 每次主动提交创建新业务单,同一次网络重试持久化幂等。
|
||||
- 将第三方收款事实与钱包入账事实分开提交,保证已收款资金可恢复、不重不漏。
|
||||
- 复用当前支付配置、回调、钱包入账、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 元、代理仅充自己主钱包、每次主动提交新单、同请求持久化幂等、第一期不做主动取消和在线退款。
|
||||
Reference in New Issue
Block a user