This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-27
|
||||
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 元、代理仅充自己主钱包、每次主动提交新单、同请求持久化幂等、第一期不做主动取消和在线退款。
|
||||
39
openspec/changes/add-agent-wallet-qr-recharge/proposal.md
Normal file
39
openspec/changes/add-agent-wallet-qr-recharge/proposal.md
Normal file
@@ -0,0 +1,39 @@
|
||||
## Why
|
||||
|
||||
当前代理充值只能由平台员工通过线下代充完成;现有代理在线充值入口仅创建本地记录,不能生成微信或支付宝扫码支付内容,也无法在真实收款后可靠、幂等地增加代理主钱包余额。需要补齐桌面端扫码充值闭环,让代理能够安全地为当前店铺主钱包自主充值,同时保持平台线下代充审批链路不变。
|
||||
|
||||
功能 ID:`feature-034-agent-wallet-qr-recharge`。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 代理可在后台选择微信或支付宝,为当前所属店铺的主钱包创建在线充值单;请求不接受目标 `shop_id`。
|
||||
- 微信使用 Native 支付、支付宝使用 `alipay.trade.precreate`,后端统一返回原始 `qr_content`,由前端渲染二维码。
|
||||
- 代理在线充值金额范围固定为 `10000~100000000` 分,即最低 100 元、最高 100 万元;平台线下代充金额规则不随之改变。
|
||||
- 每次主动创建生成新的充值单与支付单;同一次请求重试使用提交账号与 `request_id` 持久化幂等。
|
||||
- 支付成功先固化支付事实,再通过可靠 Outbox/Worker 幂等增加代理主钱包余额、写唯一钱包流水并完成充值单。
|
||||
- 增加本地支付与入账状态查询,供桌面端页面轮询;不提供 JSAPI、H5、WAP、手机唤起、二维码图片、主动取消、支付方式切换或在线退款。
|
||||
- 微信、支付宝回调按 `tb_payment.order_type` 分发代理充值,校验支付配置、渠道、金额、第三方交易号及业务关联,重复回调不得重复入账。
|
||||
- 支付单保存创建时的收款身份快照:微信商户号或支付宝应用 ID,联合支付方式与配置 ID 支持后续导出对账;该字段不在在线创建响应中暴露。
|
||||
- 平台员工线下代充继续走现有企业微信审批,不与代理在线充值互相复用创建权限或审批状态。
|
||||
- 充值创建、列表、详情和在线支付状态响应统一返回稳定来源字段:`platform_offline` 表示平台线下代充,`agent_online` 表示代理在线自充;列表支持按该字段筛选。
|
||||
- 复用现有 Fiber、GORM、Asynq、支付 SDK、统一钱包入账、公共 Outbox 与 Integration Log,不新增依赖;本 Change 不建设或接入 Audit Event。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
无。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `agent-recharge`: 将现有代理充值 Stub 扩展为微信/支付宝桌面扫码支付、支付状态收敛和可靠钱包入账完整能力,并收紧代理自主充值权限与在线金额规则。
|
||||
|
||||
## Impact
|
||||
|
||||
- API:调整 `POST /api/admin/agent-recharges` 的角色判别请求契约与在线响应;新增可用支付方式和轻量支付状态读取能力。
|
||||
- 复杂写主通道:`Handler → Application UseCase → Domain → Repository/Infrastructure`,收口在线创建、支付确认和钱包入账状态机;旧 Service 仅保留未触碰查询或作为迁移门面。
|
||||
- 读取辅助通道:`Handler → Query → GORM/DTO`,用于充值列表、详情和支付状态轮询。
|
||||
- Infrastructure:扩展微信 Native、支付宝 PreCreate Adapter,复用 `tb_payment`、公共 Outbox、Integration Log 和现有支付回调入口。
|
||||
- 数据:增加代理充值支付业务类型、请求幂等、收款身份快照和处理状态所需字段/约束;禁止外键和 GORM 关联标签。
|
||||
- 验收:按用户明确要求不新增或运行自动化测试;通过编译、静态检查、迁移演练、接口/数据库人工核对和联调环境真实扫码验证角色权限、100 元边界、请求幂等、回调校验、异步入账与唯一流水。
|
||||
- 性能:创建接口只做必要数据库写入与一次渠道预下单;轮询接口走轻量本地查询,避免每次请求第三方,维持 API P95 小于 200ms(第三方预下单耗时单独观测)。
|
||||
@@ -0,0 +1,181 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 创建代理充值订单
|
||||
|
||||
系统 SHALL 通过 `POST /api/admin/agent-recharges` 保留同一代理充值资源,并按登录账号类型与 `payment_method` 执行严格分流。
|
||||
|
||||
代理在线请求 MUST 仅接受 `amount`、`payment_method` 与 `request_id`:
|
||||
|
||||
- `payment_method` MUST 为 `wechat` 或 `alipay`。
|
||||
- `amount` MUST 使用分为单位,范围 MUST 为 `10000~100000000`。
|
||||
- 目标店铺与主钱包 MUST 从当前认证上下文确定;请求 MUST NOT 接受或信任 `shop_id`、支付凭证或运营备注。
|
||||
- 每次新的主动提交 MUST 创建新的代理充值单和 `tb_payment` 支付单,不得按金额或待支付记录复用旧单。
|
||||
- 同一提交账号与 `request_id` MUST 形成持久化幂等作用域;相同指纹重放 MUST 返回原业务结果,不同指纹重放 MUST 返回统一冲突错误。
|
||||
|
||||
平台或超级管理员的 `offline` 线下代充 MUST 继续使用现有目标 `shop_id`、付款凭证和企业微信审批契约,在线充值 100 元最低金额 MUST NOT 改变线下代充金额规则。平台、超级管理员和企业账号 MUST NOT 创建代理在线扫码充值单。
|
||||
|
||||
在线创建 MUST 在同一 GORM 事务中保存充值单、支付单及请求幂等事实,再调用创建时选定的支付 Adapter:微信 MUST 使用 Native 支付,支付宝 MUST 使用 `alipay.trade.precreate`。成功响应 MUST 使用统一 `{code,msg,data,timestamp}` 格式,并在 `data` 中至少返回 `recharge_id`、`recharge_no`、`payment_no`、`payment_method`、`recharge_source`、`recharge_source_name`、`amount`、`qr_content`、`status` 与 `status_name`。后端 MUST 返回支付渠道原始付款字符串,不得生成或保存二维码图片。
|
||||
|
||||
充值订单创建、列表、详情和在线支付状态响应 MUST 使用稳定来源枚举区分创建路径:`platform_offline` 表示平台线下代充,`agent_online` 表示代理在线自充。列表 MUST 支持使用 `recharge_source` 筛选;来源可由受控创建方式推导,不要求新增重复数据库字段。
|
||||
|
||||
支付单 MUST 同时保存创建时的收款身份快照:微信记录商户号,支付宝记录应用 ID,并保留支付方式与 `payment_config_id`,供后续导出对账。创建响应 MUST NOT 返回该内部收款身份快照。
|
||||
|
||||
第三方预下单失败时,系统 MUST 记录 Integration Log,并将本次支付单标记为失败、充值单标记为已关闭;不得返回缺少有效 `qr_content` 的成功响应。
|
||||
|
||||
#### Scenario: 代理创建微信 Native 扫码充值
|
||||
- **WHEN** 代理提交 `amount=10000`、`payment_method=wechat` 和新的 `request_id`
|
||||
- **THEN** 系统从登录上下文确定当前店铺主钱包,创建充值单和支付单,调用微信 Native 预下单,并将 `code_url` 映射为 `qr_content`
|
||||
- **THEN** 响应不得包含支付配置 ID、商户密钥或其他店铺信息
|
||||
|
||||
#### Scenario: 代理创建支付宝当面付扫码充值
|
||||
- **WHEN** 代理提交有效金额、`payment_method=alipay` 和新的 `request_id`
|
||||
- **THEN** 系统创建充值单和支付单,调用 `alipay.trade.precreate`,并将 `qr_code` 映射为 `qr_content`
|
||||
|
||||
#### Scenario: 区分平台代充与代理自充
|
||||
- **WHEN** 调用方查询充值订单列表、详情或在线支付状态
|
||||
- **THEN** 平台线下代充 MUST 返回 `recharge_source=platform_offline`,代理微信或支付宝在线自充 MUST 返回 `recharge_source=agent_online`
|
||||
|
||||
#### Scenario: 在线充值金额边界
|
||||
- **WHEN** 代理提交的在线充值金额小于 `10000` 分或大于 `100000000` 分
|
||||
- **THEN** 系统 MUST 返回 `CodeInvalidParam`,且不得创建充值单、支付单或调用第三方支付
|
||||
|
||||
#### Scenario: 代理请求携带目标店铺
|
||||
- **WHEN** 代理在线请求携带 `shop_id` 或其他仅线下代充允许的字段
|
||||
- **THEN** 系统 MUST 拒绝请求,不得允许代理选择本店、下级店铺或其他店铺作为受益方
|
||||
|
||||
#### Scenario: 相同请求重放
|
||||
- **WHEN** 同一提交账号使用相同 `request_id` 和相同业务字段重试
|
||||
- **THEN** 系统 MUST 返回首次创建的充值单、支付单和付款内容,不得再次创建业务单或再次向第三方预下单
|
||||
|
||||
#### Scenario: 幂等请求载荷冲突
|
||||
- **WHEN** 同一提交账号使用已有 `request_id` 但改变金额或支付方式
|
||||
- **THEN** 系统 MUST 返回 `CodeConflict`,不得改变原充值单或创建新单
|
||||
|
||||
#### Scenario: 支付方式不可用
|
||||
- **WHEN** 所选支付方式缺少完整配置、扫码预下单能力或回调验签能力
|
||||
- **THEN** 系统 MUST 返回统一支付配置不可用错误,且不得创建只有本地记录而无法付款的待支付订单
|
||||
|
||||
### Requirement: 代理充值回调处理
|
||||
|
||||
现有微信和支付宝异步回调入口 MUST 在完成渠道验签后,按 `tb_payment.payment_no` 和 `order_type=agent_recharge` 分发到代理充值支付确认用例,不得仅依赖 `ARCH` 单号前缀判断业务类型。旧代理充值支付单可在迁移期保留受控兼容分支,但新支付单 MUST 走统一支付记录分发。
|
||||
|
||||
支付确认 MUST 校验支付方式、创建时的 `payment_config_id`、回调商户或应用身份、支付单金额、充值单金额、支付单与充值单关联以及第三方交易号唯一性。校验失败 MUST 不改变支付单、充值单或钱包事实,并写入中文安全日志和 Integration Log;日志不得记录密钥或完整敏感正文。
|
||||
|
||||
成功确认 MUST 在同一 GORM 事务中:
|
||||
|
||||
1. 条件更新支付单为已支付并保存第三方交易号与支付时间;
|
||||
2. 条件更新代理充值单从 `1=待支付` 为 `2=已支付`;
|
||||
3. 写入稳定版本的代理充值入账 Outbox。
|
||||
|
||||
支付渠道成功响应 MUST 在上述事务提交后返回。钱包入账不得继续作为支付回调事务中的同步步骤。
|
||||
|
||||
Outbox 消费者 MUST 在独立事务中复用统一代理主钱包入账能力,锁定目标主钱包、增加余额、创建唯一成功流水、将充值单从 `2=已支付` 更新为 `3=已完成`,并写入现有钱包入账事件。消费者 MUST 以充值记录 ID 作为业务幂等引用;重复回调、重复 Outbox 或 Worker 重试不得重复增加余额。
|
||||
|
||||
#### Scenario: 微信支付成功回调
|
||||
- **WHEN** 微信回调验签通过,支付单类型为 `agent_recharge`,且金额、配置、商户身份和业务关联全部一致
|
||||
- **THEN** 系统 MUST 固化支付成功事实和入账 Outbox,并向微信返回渠道成功响应
|
||||
- **THEN** 钱包余额由独立消费者完成,不得在回调事务中同步增加
|
||||
|
||||
#### Scenario: 支付宝支付成功回调
|
||||
- **WHEN** 支付宝通知验签通过,交易状态为成功,支付单类型为 `agent_recharge`,且金额、配置、应用身份和业务关联全部一致
|
||||
- **THEN** 系统 MUST 固化支付成功事实和入账 Outbox,并向支付宝返回 `success`
|
||||
|
||||
#### Scenario: 回调金额或关联不一致
|
||||
- **WHEN** 回调金额与支付单或充值单不一致,或支付单未关联该代理充值记录
|
||||
- **THEN** 系统 MUST 拒绝处理并保留原状态,钱包余额和流水 MUST NOT 改变
|
||||
|
||||
#### Scenario: 重复支付回调
|
||||
- **WHEN** 同一第三方交易号对同一已支付或已完成充值单重复回调
|
||||
- **THEN** 系统 MUST 幂等返回渠道成功响应,不得重复写支付事实、入账 Outbox或钱包流水
|
||||
|
||||
#### Scenario: 第三方交易号被其他支付单占用
|
||||
- **WHEN** 回调中的第三方交易号已绑定另一张支付单或代理充值单
|
||||
- **THEN** 系统 MUST 返回冲突并记录不含敏感信息的严重错误,任何钱包 MUST NOT 入账
|
||||
|
||||
#### Scenario: 钱包入账暂时失败
|
||||
- **WHEN** 支付事实已提交但钱包入账消费者因锁冲突或暂时性基础设施错误失败
|
||||
- **THEN** 充值单 MUST 保持 `2=已支付`,Outbox/Worker MUST 重试,且支付成功事实不得回滚或要求代理再次付款
|
||||
|
||||
#### Scenario: 重复执行钱包入账
|
||||
- **WHEN** 同一代理充值入账任务被重复消费
|
||||
- **THEN** 数据库唯一流水约束和状态条件更新 MUST 保证余额最多增加一次,并最终将充值单收敛为 `3=已完成`
|
||||
|
||||
### Requirement: 权限控制
|
||||
|
||||
系统 MUST 使用以下权限边界:
|
||||
|
||||
| 操作 | 平台/超级管理员 | 代理账号 | 企业账号 |
|
||||
|------|-----------------|----------|----------|
|
||||
| 创建在线扫码充值 | 禁止 | 仅当前所属店铺主钱包 | 禁止 |
|
||||
| 创建线下代充 | 按现有权限指定目标店铺 | 禁止 | 禁止 |
|
||||
| 查询可用在线支付方式 | 禁止 | 允许 | 禁止 |
|
||||
| 查询充值列表与详情 | 按既有数据范围 | 按既有店铺层级与查看权限 | 禁止 |
|
||||
| 查询在线支付状态 | 按既有数据范围 | 按既有店铺层级与查看权限 | 禁止 |
|
||||
|
||||
路由层 MUST 对企业账号执行粗粒度拦截;Application/Query MUST 执行创建角色、当前店铺、资源归属和查看权限校验;GORM 数据范围过滤保持启用。越权与资源不存在 MUST 统一返回 `CodeForbidden` 和“无权限操作该资源或资源不存在”,不得泄露资源是否存在。
|
||||
|
||||
#### Scenario: 代理为当前店铺充值
|
||||
- **WHEN** 代理账号创建在线扫码充值且当前店铺主钱包可用
|
||||
- **THEN** 系统 MUST 以认证上下文中的店铺和主钱包作为唯一受益方
|
||||
|
||||
#### Scenario: 平台尝试创建在线扫码充值
|
||||
- **WHEN** 平台或超级管理员提交 `wechat` 或 `alipay` 代理充值请求
|
||||
- **THEN** 系统 MUST 返回 `CodeForbidden`,并引导其使用既有线下代充审批路径
|
||||
|
||||
#### Scenario: 无权读取支付状态
|
||||
- **WHEN** 登录账号查询其数据范围外充值单的支付状态
|
||||
- **THEN** 系统 MUST 返回统一禁止访问错误,不得返回支付状态、付款内容或钱包余额
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 查询代理在线充值可用支付方式
|
||||
|
||||
系统 SHALL 提供 `GET /api/admin/agent-recharges/payment-methods`,仅根据当前生效支付配置返回真正具备扫码预下单、回调验签和查单能力的在线支付方式。路由 MUST 注册在 `/:id` 动态路由之前。
|
||||
|
||||
成功响应 MUST 使用统一 `{code,msg,data,timestamp}` 格式;`data.methods` MUST 为按 `wechat`、`alipay` 固定顺序排列的字符串数组,并同时返回 `min_amount=10000` 与 `max_amount=100000000`。接口不得返回支付配置 ID、商户号、应用 ID、密钥或具体缺失的敏感配置。
|
||||
|
||||
#### Scenario: 微信与支付宝均可用
|
||||
- **WHEN** 当前支付配置完整支持微信 Native 和支付宝 PreCreate
|
||||
- **THEN** 接口 MUST 返回 `methods=["wechat","alipay"]` 及在线金额上下限
|
||||
|
||||
#### Scenario: 没有可用扫码支付方式
|
||||
- **WHEN** 当前配置不支持任何已约定的扫码支付方式
|
||||
- **THEN** 接口 MUST 成功返回空数组,不得伪造可用渠道
|
||||
|
||||
### Requirement: 查询代理在线充值支付状态
|
||||
|
||||
系统 SHALL 提供 `GET /api/admin/agent-recharges/:id/payment-status` 供桌面端轮询本地事实。接口 MUST 仅查询 PostgreSQL 本地支付单、充值单及必要的钱包入账结果,不得在每次轮询时调用第三方支付渠道。
|
||||
|
||||
响应 MUST 使用统一 `{code,msg,data,timestamp}` 格式,并至少返回 `status`、`status_name`、`payment_status`、`payment_status_name`、`paid_at` 与 `completed_at`。`1=待支付` MUST 表示尚未确认收款,`2=已支付` MUST 表示第三方收款已确认但钱包仍在入账,`3=已完成` MUST 表示钱包余额和唯一流水已经提交。
|
||||
|
||||
接口 MUST NOT 返回 `qr_content`、支付密钥、签名参数、内部重试错误或其他店铺余额。第一期 MUST NOT 提供主动取消、支付方式切换、在线退款或本地推算的精确二维码倒计时。
|
||||
|
||||
#### Scenario: 等待扫码支付
|
||||
- **WHEN** 充值单仍为待支付且支付单未确认成功
|
||||
- **THEN** 接口 MUST 返回待支付状态,不得调用第三方查单
|
||||
|
||||
#### Scenario: 已支付等待钱包入账
|
||||
- **WHEN** 支付单已支付且充值单状态为 `2=已支付`
|
||||
- **THEN** 接口 MUST 明确返回支付成功、入账处理中,不得显示支付失败
|
||||
|
||||
#### Scenario: 钱包已经到账
|
||||
- **WHEN** 充值单状态为 `3=已完成` 且唯一钱包流水存在
|
||||
- **THEN** 接口 MUST 返回已完成和完成时间
|
||||
|
||||
### Requirement: 待支付订单受控收敛
|
||||
|
||||
系统 MUST 通过后台受控任务查询长期待支付的代理在线充值支付单,以弥补第三方回调丢失。任务 MUST 使用创建支付单时记录的支付方式和 `payment_config_id` 调用对应查单 Adapter,并为每次外部尝试记录 Integration Log。
|
||||
|
||||
查单确认成功 MUST 复用与回调相同的支付确认用例;查单确认关闭或失效 MUST 条件关闭仍为待支付的支付单与充值单;未知、超时或渠道异常 MUST 保持原业务状态并按任务策略重试。迟到的真实成功通知经完整校验后 MUST 仍能固化支付事实并入账,不得因本地曾判断待支付或关闭而吞掉已收款资金。
|
||||
|
||||
#### Scenario: 回调丢失但查单成功
|
||||
- **WHEN** 待支付收敛任务从第三方查询到交易成功且金额与配置校验通过
|
||||
- **THEN** 系统 MUST 调用统一支付确认用例,写入支付事实和钱包入账 Outbox
|
||||
|
||||
#### Scenario: 第三方明确订单关闭
|
||||
- **WHEN** 查单结果明确表示订单关闭或失效,且本地支付单仍为待支付
|
||||
- **THEN** 系统 MUST 条件更新支付单为失败并将充值单关闭,不得增加钱包余额
|
||||
|
||||
#### Scenario: 查单结果未知
|
||||
- **WHEN** 第三方超时、返回未知状态或暂时不可用
|
||||
- **THEN** 系统 MUST 保持本地待支付状态并重试,不得猜测支付失败
|
||||
26
openspec/changes/add-agent-wallet-qr-recharge/tasks.md
Normal file
26
openspec/changes/add-agent-wallet-qr-recharge/tasks.md
Normal file
@@ -0,0 +1,26 @@
|
||||
## 1. 代理扫码创建纵向切片
|
||||
|
||||
- [x] 1.1 【主通道:复杂写;辅助:Infrastructure;完整边界:在线充值单、支付单、幂等事实、付款内容和收款身份快照;不迁移:线下代充、客户充值】添加 golang-migrate 上下迁移与 Model/常量:代理充值 `request_id/request_fingerprint`、支付单 `qr_content/merchant_identity`、提交账号幂等唯一索引、第三方交易号唯一索引、收款身份对账索引、待处理查询索引及 `PaymentOrderTypeAgentRecharge`;迁移前检测冲突资金事实,以 up/down/up 演练和 SQL 查询核对字段、索引及回滚结果,不编写或运行自动化测试。
|
||||
- [x] 1.2 【主通道:Application + Port/Adapter;完整边界:微信 Native 与支付宝 PreCreate/Query;不迁移:通用支付框架和 SDK 升级】用现有 PowerWeChat、smartwalle/alipay 与 Integration Log 实现两个薄 Adapter,通过结构体字段注入在线创建用例;使用编译、静态诊断和联调环境渠道请求核对返回与错误转换,不编写或运行自动化测试。
|
||||
- [x] 1.3 【主通道:复杂写;辅助:Domain、Infrastructure;完整边界:代理只为当前店铺创建全新扫码充值;不迁移:旧查询和线下审批】实现纯领域金额/状态规则与在线创建 Application:双重金额校验、认证上下文店铺、主钱包/配置校验、本地短事务、第三方预下单、成功保存付款内容、失败条件关闭以及持久化请求幂等;复用 `pkg/idempotency`,通过构建、接口调用和数据库前后状态人工核对 100 元边界、角色权限与同请求重放。
|
||||
- [x] 1.4 【主通道:Handler → Application;辅助:Query;完整边界:创建与可用方式 API;不迁移:其他代理充值 Handler 方法】更新 DTO、现有 AgentRecharge Handler 和路由,静态 `/payment-methods` 注册在 `/:id` 前,统一返回 `{code,msg,data,timestamp}` 且不接受代理 `shop_id`;同步 RouteSpec/OpenAPI,若实际新增 Handler 类型则更新 `cmd/api/docs.go` 与 `cmd/gendocs/main.go`;通过 OpenAPI 生成、构建、静态诊断和接口响应人工核对完成验收。
|
||||
|
||||
## 2. 支付确认与可靠钱包入账纵向切片
|
||||
|
||||
- [x] 2.1 【主通道:复杂写;辅助:Domain、Infrastructure;完整边界:第三方支付事实确认;不迁移:其他支付业务确认用例】实现代理充值支付确认领域转换与 Application,用支付单类型、渠道、创建配置身份、金额、业务关联和第三方交易号唯一性完成校验;同事务条件更新支付单/充值单并写 `agent_recharge.payment_confirmed.v1` Outbox;通过编译和静态诊断核对金额篡改、配置错配、交易号冲突与重复确认分支,不连接数据库或支付渠道。
|
||||
- [x] 2.2 【主通道:Application + Port/Adapter;完整边界:微信/支付宝回调分发;不迁移:既有验签算法和其他订单回调】扩展现有微信、支付宝回调按 `tb_payment.order_type=agent_recharge` 调用统一确认用例,保留有界存量 `ARCH` 兼容分支,复用渠道成功报文并记录入站 Integration Log;通过编译和静态诊断核对分发、配置身份、金额与幂等分支,不发送真实回调。
|
||||
- [x] 2.3 【主通道:复杂写;辅助:现有 Wallet Domain/Outbox;完整边界:支付成功后的代理主钱包一次性入账;不迁移:钱包扣款、退款和线下审批终态】实现并装配代理充值入账 Outbox 消费者,在独立事务复用 `wallet.PostingService`,以充值 ID 唯一引用增加余额、写流水、更新 `2→3` 并写既有钱包事件;注册 Worker,通过构建和静态诊断核对幂等分支,不连接数据库或运行 Worker 重放。
|
||||
- [x] 2.4 【主通道:复杂写;完整边界:支付回调不再同步入账;不迁移:旧线下 Service 入口】将新在线支付调用方切到统一确认/异步入账用例,封闭旧 `agent_recharge.Service.HandlePaymentCallback` 对新支付单的同步钱包写路径,确保业务不变量不在旧 Service 与 Application 两处并存;通过代码搜索、静态诊断和构建确认新单只走 `order_type=agent_recharge` 的统一确认与异步入账,旧方法仅保留无支付记录的 `ARCH` 存量兼容。
|
||||
|
||||
## 3. 状态读取与回调丢失收敛纵向切片
|
||||
|
||||
- [x] 3.1 【主通道:Query;完整边界:代理在线充值本地支付/到账状态;不迁移:既有列表、详情和导出】实现 `GET /api/admin/agent-recharges/:id/payment-status` Query/DTO/路由,复用店铺层级数据范围,仅返回来源、状态名称与支付/完成时间,不返回 `qr_content`、内部错误或其他钱包余额;按用户要求仅通过构建、OpenAPI 和静态代码检查验收,不连接数据库或调用接口。
|
||||
- [x] 3.2 【主通道:Infrastructure;辅助:Application + Port/Adapter;完整边界:长期待支付和待预下单收敛;不迁移:通用轮询框架】实现固定批次的受控查单/恢复任务,按创建方式和 `payment_config_id` 调用对应 Adapter,成功复用统一确认用例、明确关闭条件关单、未知状态保持并重试,每次外呼记录 Integration Log;按用户要求仅通过构建和静态代码检查验收,不运行 Worker、不调用支付渠道或查询数据库。
|
||||
- [x] 3.3 【主通道:Query + Infrastructure;完整边界:桌面轮询性能与终态收敛;不迁移:Redis 支付状态缓存】静态确认支付状态查询命中既有业务关联索引且无 N+1/外部请求,批量查单有固定批次和时间窗口,第三方预下单/查单耗时写入 Integration Log;按用户要求不执行 `EXPLAIN ANALYZE`、访问日志或数据库性能实测,并记录该验证边界。
|
||||
|
||||
## 4. 文档与发布验收
|
||||
|
||||
- [x] 4.1 【主通道:Infrastructure;完整边界:在线充值运行事实分类;不迁移:全仓审计治理】按用户决定将本 Change 的 Audit Event 明确为 N/A,不新增审计表、Writer、Adapter 或发布门禁;充值/支付/钱包/流水继续作为 Domain Ledger,预下单/回调/查单使用 Integration Log,支付确认/钱包事件使用 Outbox。
|
||||
- [x] 4.2 【主通道:文档;完整边界:代理桌面扫码充值交付说明;不迁移:手机支付与在线退款】在 `docs/feature-034-agent-wallet-qr-recharge/` 编写中文功能总结、API/前端对接、异常恢复、部署回滚与人工扫码验收说明,同步 README 和 OpenAPI;核对所有 DTO 枚举说明来自 `pkg/constants` 原文、导出符号/日志/错误为中文,运行文档生成、构建和静态检查。
|
||||
- [x] 4.3 【主通道:静态验收;完整边界:已确认扫码充值范围;不迁移:真实生产支付】不新增或运行自动化测试,不启动服务、不连接数据库/Redis、不调用支付渠道或发送回调;执行 `go build ./...`、`go vet ./...` 和 OpenSpec 严格校验,静态核对微信 Native、支付宝 PreCreate、100 元边界、权限隔离、幂等、回调分发、恢复查单与 Worker 注册,并记录未联调边界。
|
||||
- [x] 4.4 【主通道:发布门禁;完整边界:停机切换与资金单收敛;不迁移:自动退款】静态核对发布顺序、必要支付配置、回调地址、Outbox/Worker、待支付/已支付监控和回滚步骤;确认回滚只关闭新入口且继续处理已收款订单,不删除待处理支付事实,形成上线检查记录,不执行生产或联调环境操作。
|
||||
Reference in New Issue
Block a user