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

13 KiB
Raw Blame History

MODIFIED Requirements

Requirement: 创建代理充值订单

系统 SHALL 通过 POST /api/admin/agent-recharges 保留同一代理充值资源,并按登录账号类型与 payment_method 执行严格分流。

代理在线请求 MUST 仅接受 amountpayment_methodrequest_id

  • payment_method MUST 为 wechatalipay
  • amount MUST 使用分为单位,范围 MUST 为 10000100000000
  • 目标店铺与主钱包 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_idrecharge_nopayment_nopayment_methodrecharge_sourcerecharge_source_nameamountqr_contentstatusstatus_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=10000payment_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_noorder_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 平台或超级管理员提交 wechatalipay 代理充值请求
  • 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 为按 wechatalipay 固定顺序排列的字符串数组,并同时返回 min_amount=10000max_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} 格式,并至少返回 statusstatus_namepayment_statuspayment_status_namepaid_atcompleted_at1=待支付 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 保持本地待支付状态并重试,不得猜测支付失败