Files
junhong_cmp_fiber/.scratch/ur34-agent-recharge/PRD.md
2026-07-22 11:08:04 +09:00

286 lines
40 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.
# PRDUR#34 代理在线扫码充值与平台线下代充值
Status: ready-for-agent
---
## Problem Statement
当前代理充值接口只创建本地充值记录,没有真正创建微信 Native 或支付宝 PreCreate 支付单并返回可供前端渲染的付款内容。在线回调直接在一次事务中尝试完成钱包入账,支付事实与钱包处理结果无法独立表达;回调也没有完整校验支付配置、金额、第三方交易号和业务关联。网络超时、回调丢失、重复回调、钱包事务失败和第三方迟到成功都缺少可靠恢复边界。
当前同一个创建接口还允许代理或平台提交目标 `shop_id`,平台可以替任意代理创建在线支付,容易混淆真实付款人和受益钱包。线下代充值仍通过本地 `offline-pay``reject` 和全局操作密码完成人工入账,没有接入已经冻结的企业微信审批公共能力;附件仍是字符串 Key 列表,审批结论、充值状态和钱包入账状态也未独立建模。
本需求必须把代理自主在线充值与平台线下代充值分成两条不可混用的路径,复用现有支付和钱包基础能力,保证第三方真实收款、企微审批结论、钱包实际入账和通知都可独立追踪,并在重复回调、重复 Worker、进程中断、查单未知和审批异常下保持资金不重不漏。
## Solution
保留代理充值资源,建立两条严格隔离的创建路径:代理只为当前店铺主钱包选择 `wechat``alipay` 发起在线充值,不提交目标店铺,也不进入审批;平台或超级管理员只为指定代理店铺发起 `offline` 线下代充值,提交固定金额、备注和 15 个结构化付款凭证,并接入 UR#37 的真实企业微信审批。
在线充值为每次用户主动创建生成新的充值单和支付单。微信 Native 或支付宝 PreCreate 返回的字符串或 HTTPS URL 通过 `qr_content` 原样返回,前端自行渲染二维码,后端不生成二维码图片。前端支付状态轮询只读取本地状态;后端通过支付回调和受控查单任务同步第三方真实状态。支付成功先可靠固化收款事实,再由独立 Worker 幂等增加代理主钱包并写唯一流水,钱包失败不回滚支付成功事实。
线下充值创建时把充值单、唯一企微审批实例和提交 Outbox 原子落库。企微只能同意或拒绝固定金额;同意后复用与在线充值相同的钱包入账 Worker拒绝后原单终结。无论在线还是线下只要主钱包实际入账成功都向目标代理发送防重的站内到账通知。
## User Stories
1. 作为代理账号,我希望只为当前所属店铺的主钱包充值,不需要也不能选择目标店铺。
2. 作为代理付款人,我希望在创建时选择微信或支付宝,并在订单创建后不能切换支付方式。
3. 作为代理付款人,我希望每次主动点击创建支付都得到一张新的充值单和支付单,而不是复用相同金额的旧单。
4. 作为代理付款人,我希望网络重试不会为同一次提交创建多张充值单。
5. 作为代理付款人,我希望后端返回支付平台给出的付款字符串,由前端稳定渲染二维码。
6. 作为代理付款人,我希望页面不展示一个本地猜测的精确失效倒计时,以免把第三方实际已经失效的码显示为有效。
7. 作为代理付款人,我希望关闭页面后旧支付单继续等待回调或查单收敛,不因页面关闭而被取消。
8. 作为代理付款人,我希望即使创建了多张支付单,每张真实付款的订单都能分别准确入账。
9. 作为代理付款人,我希望付款完成后页面先显示第三方已收款,再准确展示钱包是否已经到账。
10. 作为代理付款人,我希望第三方已经收款但钱包暂时失败时,系统明确显示“支付成功、入账重试中”,而不是显示支付失败。
11. 作为代理付款人,我希望第三方成功回调晚于本地失败或关闭状态时,系统仍能核实并补充入账,不吞掉已付资金。
12. 作为代理付款人,我希望回调丢失时系统能够通过第三方查单补偿,而不要求我再次付款。
13. 作为代理付款人,我希望微信或支付宝不可用时页面只展示真实可用的支付方式。
14. 作为代理付款人,我希望钱包实际到账后收到一条站内到账通知。
15. 作为非主账号的在线充值提交人,我希望自己和目标店铺主账号都能收到到账通知。
16. 作为平台员工,我希望选择目标代理店铺、金额、备注和付款凭证发起线下代充值。
17. 作为平台员工,我希望线下代充值金额大于 0 即可,不受代理在线充值 100 元起付限制。
18. 作为平台员工,我希望使用本人绑定的企微身份提交线下充值审批,业务系统同时保存我是真实提交人。
19. 作为审批人,我希望企微表单展示充值单号、目标代理、固定金额、真实提交人、备注和付款凭证。
20. 作为审批人,我只需要同意或拒绝线下代充值,不能修改金额,也不需要回业务系统输入操作密码。
21. 作为平台提交人,我希望企微拒绝后原充值单只读终结;修正资料后通过创建入口提交一张新单。
22. 作为平台提交人,我希望审批通过和钱包实际到账是两个独立状态,不会把审批通过误认为余额已增加。
23. 作为目标代理主账号,我希望平台线下代充值实际到账后收到站内到账通知。
24. 作为真实业务提交人,我希望线下审批通过或拒绝继续收到 UR#37 的审批结果通知。
25. 作为代理管理者,我希望按既有店铺层级和充值查看权限查看本店及可管理下级店铺的充值记录,而不是只看自己创建的记录。
26. 作为代理查看者,我希望看到充值金额、付款方式、业务凭证、真实提交人、审批状态和入账结果,但看不到平台内部审批人、意见、审批附件或支付配置。
27. 作为平台财务,我希望钱包入账持续失败或审批通过后撤销时收到内部异常通知,以便人工处理。
28. 作为财务人员,我希望已经入账后发生企微通过后撤销时系统不自动扣减代理余额,避免未经核实的反向资金动作。
29. 作为系统维护人员,我希望支付回调、第三方查单、企微回调、企微轮询和重复 Worker 都进入统一幂等用例,不重复加钱。
30. 作为审计人员,我希望充值单、支付单、企微审批实例、代理主钱包、钱包流水、通知和外部交互能够完整关联。
31. 作为前端开发者,我希望在线支付、线下审批和钱包处理状态分别返回,不需要通过单个状态猜测业务阶段。
32. 作为验收人员,我希望本地自动化覆盖完整后台业务和异常恢复,真实微信/支付宝留到部署测试环境人工扫码,真实企微仍在实现阶段参与线下充值验收。
## Implementation Decisions
### 范围、依赖与架构边界
- UR#34 只负责代理在线充值、平台线下代充值、支付状态同步、企微终态消费、钱包入账、充值 Query 和前端业务页面。企微连接、平台账号绑定、模板版本、附件上传企微、提交、加密回调、轮询和审批详情复用 UR#37,不实现第二套企微客户端或审批状态机。
- 站内到账通知复用 TECH 公共站内通知;支付和钱包关键事实使用全局 Audit Event第三方支付与企微调用使用 Integration Log不新建充值私有审计或通知体系。
- 在线创建、支付确认、线下创建、企微终态消费和钱包入账均涉及远程调用、状态机、金额、并发与可靠事件,采用 `Handler → Application UseCase → Domain → Repository/Infrastructure` 的复杂写通道。充值列表、详情和轻量支付状态采用 Query 通道。
- 只迁移完成 UR#34 所需的最小完整充值用例。现有旧 Service 可以暂时作为迁移门面,但支付确认和钱包入账不变量不得一半留在旧 Service、一半放在新 Domain。
- 当前支付配置通常只有一套微信参数和一套支付宝参数。本期不建设多通道池、优先级、权重、自动切换或故障转移;代理选择的是支付方式,不选择也不感知具体支付通道。
### 角色与两条创建路径
- 代理账号只能创建 `payment_method=wechat|alipay` 的在线充值。目标店铺和主钱包从当前认证上下文确定;请求不接受 `shop_id`,代理不能给本店下级或其他店铺创建在线支付。
- 平台账号和超级管理员只能创建 `payment_method=offline` 的线下代充值,必须提交目标 `shop_id`,并受既有目标店铺数据范围和管理权限约束。平台与超级管理员不能替代理创建微信或支付宝支付码。
- 代理账号不能创建线下代充值;企业账号不能创建或访问任一代理充值路径。
- 创建权限和查看权限分开处理。创建路径使用上述严格角色规则;列表、详情和支付状态继续使用既有充值业务权限与店铺层级数据范围。
- 新创建不接受历史 `bank` 支付方式;历史 `bank` 记录只读兼容,不改变原事实。
### 可用支付方式接口
- `GET /api/admin/agent-recharges/payment-methods` 只供代理在线充值页面使用,返回当前真正配置完整且能够创建支付的 `wechat` 和/或 `alipay`
- `data` 固定包含:`methods:string[]``min_amount:int64=10000``max_amount:int64=100000000`。没有可用方式时 `methods=[]`,接口本身仍成功。
- 接口不返回商户号、应用私钥、支付通道、支付配置 ID或具体缺失的敏感配置。支付方式顺序固定为 `wechat``alipay`,仅保留可用项。
- 创建接口必须再次校验所选支付方式。配置在查询列表后失效时拒绝创建,不能只信任前端先前取得的列表。
- 微信只有在当前微信支付配置支持本需求所需的扫码预下单、回调验签和查单能力时才视为可用;支付宝只有在 PreCreate 所需参数完整时才视为可用。
### 创建接口与请求幂等
- 保留 `POST /api/admin/agent-recharges`,根据 `payment_method` 使用严格的判别请求契约。
- 在线请求只接受:`amount``payment_method=wechat|alipay``request_id``amount` 为分,范围 `10000100000000`;不接受 `shop_id`、付款凭证或备注。
- 线下请求只接受:`shop_id``amount``payment_method=offline``attachments`、可选 `remark``request_id``amount` 为分,范围 `1100000000``remark` 最多 500 字。
- 线下 `attachments` 必传 15 项,每项固定包含 `file_key``file_name``file_size`。后端使用现有真实对象存储元数据能力校验对象存在、当前上传主体、允许的文件类型和实际大小;请求元数据作为业务快照,不能替代对象事实。
- `request_id` 只防止同一次用户提交因超时、重试或重复点击产生重复业务单。幂等范围为真实提交账号与 `request_id`,并保存请求指纹;同账号、同 ID、同载荷返回原业务结果载荷变化返回冲突。
- 用户每次主动点击创建或拉起支付必须生成新的 `request_id`。后端每次创建全新的充值单、支付单和 `qr_content`,不按相同金额、支付方式或既有待支付记录复用旧单。
- 在线和线下创建都必须通过数据库唯一约束和请求指纹保证持久化幂等Redis 只能减少并发,不能成为唯一正确性依据。
### 在线充值创建与付款内容
- 在线创建先在同一 PostgreSQL 事务写充值单和 `tb_payment`。支付单增加稳定 `order_type=agent_recharge`,关联充值 ID、支付方式、金额、创建时支付配置 ID和支付单号充值单初始 `status=1`,支付单初始 `status=0`,处理状态为 0。
- 微信使用 Native 扫码预下单;支付宝使用 `alipay.trade.precreate`。支付宝不再返回 WAP 支付链接作为本需求的扫码实现。
- 微信或支付宝返回的二维码码串、协议字符串或 HTTPS URL 原样保存为 `qr_content` 并返回。前端使用二维码组件渲染;后端不生成、上传或返回二维码图片,也不新增“生成二维码”“重新生成二维码”接口。
- 创建成功的在线 `data` 至少包含:`recharge_id``recharge_no``amount``payment_method``qr_content``status/status_name``payment_status/payment_status_name``processing_status/processing_status_name``approval_source=none`。不返回 `expires_at``payment_channel` 或支付配置 ID。
- 本地计算时间不能代表第三方支付订单真实有效期。后台在线充值接口不返回 `expires_at`,前端不展示精确倒计时,也不根据本地时间判定二维码仍然有效。
- 用户关闭页面只停止当前页面展示,不调用支付取消或关单。此前支付单继续等待支付回调或后端查单自然收敛;用户再次主动创建得到独立新单。
- 多张在线支付单若都被第三方确认真实付款,每张分别进入幂等钱包入账,不能因为存在更新的支付单而吞掉旧单资金。
### 预下单失败和结果未知
- 第三方明确返回预下单失败时,支付单改为 `2=已失败`并保存稳定失败码/脱敏摘要,充值单改为 `4=已关闭`;创建接口返回统一支付错误。用户再次主动尝试使用新的 `request_id` 创建新单。
- 网络超时、连接中断或响应丢失导致预下单结果未知时,不能直接把原支付单判失败,也不能以同一个 `request_id` 创建另一张支付单。必须保留原业务事实,并使用创建时支付配置和原支付单号执行安全恢复。
- 结果未知恢复先向第三方查单:明确已支付则进入统一支付确认;明确不存在时可用同一支付单号重新预下单;明确未支付且第三方支持幂等恢复付款内容时保存并返回原支付单的 `qr_content`;仍无法判断时保持未知并继续可靠恢复,不伪造失败或成功。
- 同一 `request_id` 重试只能返回原充值/支付事实或当前稳定错误,不能创建新单。用户主动使用新 `request_id` 创建的新单不取消未知旧单,旧单仍需查单收敛。
- 预下单结果未知、恢复尝试和最终结论写 Integration Log外部原始响应、商户密钥和完整付款内容不得进入普通日志或审计详情。
### 支付状态、充值状态与处理状态
- `tb_payment` 状态统一沿用当前 Go 常量:`0=待支付``1=已支付``2=已失败``3=已退款`。本需求不新增“已关闭”支付状态;第三方明确关闭或失效时使用 `2=已失败`并保存结构化失败原因。
- 充值业务状态沿用 `16``1=待支付/线下待审批``2=已支付或审批通过后正在入账``3=已完成``4=已关闭``5=已退款``6=已驳回`。不新增 `7=已退回`
- 钱包处理状态统一为:`0=未触发``1=处理中``2=处理成功``3=处理失败`。所有响应返回对应 `processing_status_name`,不再提供同义字段 `wallet_posting_status`
- 在线记录另返回支付状态及 `payment_status_name`;线下记录没有第三方支付单,支付状态字段为不适用,不用伪造“已支付”。审批状态完全复用 UR#37 的公共企微状态。
- 状态类字段使用 `int`,方式/来源类使用 `string`。DTO description 必须从公共 constants 原文复制,不能按迁移注释或旧文档重新编号。
- 历史 `tb_payment` 建表迁移的注释和默认值与当前 Go `03` 常量不一致。实施前必须按状态、业务类型和创建入口盘点存量值;新迁移统一默认值、注释和约束到 `03`,遇到无法证明含义的存量值必须中止并输出异常清单,禁止盲目整体加减 1。
### 支付回调、查单补偿与迟到成功
- 微信、支付宝回调先完成渠道协议验签/解密,再按支付单号和 `order_type=agent_recharge` 分发到统一支付确认用例,不再只依赖充值单号前缀猜测业务类型。
- 支付确认必须核验:原支付方式、创建时支付配置、商户订单号、业务关联、第三方交易号、支付金额和第三方成功状态。金额或关联不一致时拒绝入账并记录严重 Integration Log/Audit Event不能把异常回调当成功。
- 第三方交易号必须具备持久化唯一性或等价防重;同一支付单重复回调、回调与查单并发、第三方重复通知都只固化一次支付成功事实。
- 前端每 3 秒调用轻量支付状态接口时只查询本地数据库,不直接请求微信或支付宝。页面不可见时暂停,恢复时立即刷新;到达本地终态或离开页面时停止。
- 后端可靠任务按受控、可运维调整的频率查询仍待支付或预下单结果未知的支付单。具体频率是实现配置,不成为前端或业务契约;任务必须有限流、租约和批次上限,不能形成无限并发查单。
- 查单与回调进入同一支付确认用例。只有第三方明确返回已支付、已关闭/失效或订单不存在时才推进对应本地结论;超时、限流和未知状态继续保持原状态并重试。
- 第三方明确关闭或失效且未支付时,支付单改为已失败,充值单改为已关闭。不得仅根据本地 `expire_at` 或页面停留时间关闭第三方订单。
- 本地曾因第三方明确关闭而标记失败后,如果又收到可验证的迟到成功回调,以真实收款事实为准:支付单改为已支付,原充值单进入入账流程。已关闭业务状态不能吞掉已付资金。
### 两阶段钱包入账与资金幂等
- 在线支付确认采用两阶段处理。第一阶段在一个事务中把支付单条件更新为已支付,保存第三方交易号和支付时间,把充值单更新为 `2=已支付``processing_status=1`,并写钱包入账 Outbox。事务成功后即可向支付渠道返回成功不等待钱包余额更新。
- 线下企微首次同意同样只把充值单推进到 `2``processing_status=1`并写钱包入账 Outbox企微同步线程不直接修改钱包。
- 钱包入账 Worker 在独立事务中锁定目标代理主钱包,使用钱包版本/条件更新增加余额,创建唯一充值流水,把充值单改为 `3=已完成``processing_status=2`并写资金 Audit Event。
- 钱包流水以充值单号或等价稳定业务键建立唯一约束。若流水已经存在而充值单尚未完成,重试只核验并补齐充值单状态和审计关联,不再次增加余额。
- Worker 通过处理状态、条件更新和有期限租约领取。失败时支付或审批事实保持不变,充值单 `processing_status=3`并保存脱敏错误摘要,由可靠任务重试;前端没有人工“再次入账”按钮。
- 重复支付回调、重复企微终态、重复 Outbox、Asynq 至少一次投递、租约过期和进程中断均不能重复加钱、重复流水或重复到账通知。
- 充值增加主钱包账面余额;钱包原余额为负时允许正常入账并自然冲减欠款,不因欠款状态拒绝充值。
### 线下代充值与企微终态
- 创建线下充值前必须确认 `offline_recharge_approval` 场景、当前模板和当前平台/超级管理员本人的企微绑定可用。任一前置不可用时,在充值单、审批实例和 Outbox 落库前失败,前端保留表单。
- 线下充值单、真实业务提交人快照、结构化申请附件、唯一企微审批实例和提交 Outbox 在同一 PostgreSQL 事务创建。远程企微异步提交;创建成功返回充值详情及 `approval_source=wecom`、审批“提交中”。
- 企微表单至少展示充值单号、目标代理店铺、充值金额、真实业务提交人、申请备注和 15 个付款凭证。金额提交后固定;企微只允许同意或拒绝,不提供金额编辑、退回修改或本地审批动作。
- 企微拒绝把原充值单条件更新为 `6=已驳回`,处理状态保持未触发,保存拒绝原因和审批快照。原单、凭证和拒绝原因只读保留;修正后必须通过创建接口生成新的充值 ID、单号、资料和企微审批。
- 系统不新增 `7=已退回`,不提供 `resubmit`,也不注册旧 `offline-pay` 或业务单级 `reject`。企微同意后自动入账,不校验全局操作密码。
- 企微在通过前撤销或删除时,充值单改为 `4=已关闭`且不入账,可以重新创建。企微通过后、钱包尚未入账时发生撤销,终止后续入账并关闭充值单。
- 钱包已经入账后再收到通过后撤销时,不自动扣减代理余额,充值单保持已完成;系统记录 `critical` Audit Event并向平台财务/运维发送异常通知,交由人工处理。
### 到账通知与异常通知
- 无论在线扫码还是平台线下代充值,只有钱包入账事务成功后才产生“充值到账”可靠事件;支付成功或企微同意本身不提前发送到账通知。
- 目标代理店铺主账号必须收到到账通知。在线充值的真实提交人若不是店铺主账号,也收到到账通知;同一账号同时符合多种身份时按“充值单 + 接收人”唯一键只生成一条。
- 平台线下充值提交人不因为提交身份收到代理到账通知,但继续通过 UR#37 收到审批通过/拒绝结果通知。若平台提交人同时也是目标代理接收账号,则仍按接收人防重。
- 通知至少展示充值单号、金额、支付方式/线下代充来源、到账时间和受控充值详情引用,不保存任意 URL。通知投递失败不回滚资金事务并由公共通知 Worker可靠重试。
- 入账持续失败、金额异常、重复第三方交易号和通过后撤销只通知有权的平台财务/运维,代理页面显示业务化处理状态,不暴露数据库、支付配置或第三方原始错误。
### 查询、权限与信息投影
- 保留 `GET /api/admin/agent-recharges``GET /api/admin/agent-recharges/{id}`,新增 `GET /api/admin/agent-recharges/{id}/payment-status`。列表、详情和支付状态必须使用相同认证、充值业务权限和店铺层级数据范围。
- 充值记录不按创建账号隔离。代理在具备充值查看权限时,可查看本店及有权管理的下级店铺记录;平台和超级管理员沿用既有数据范围;企业账号统一拒绝。
- 代理可见充值金额、支付方式、业务付款凭证、真实提交人、充值状态、企微状态和钱包处理结果;不可见企微审批人、内部意见、审批人附件、企微成员 ID、支付配置 ID、商户号、支付通道和内部失败详情。
- 平台和超级管理员只有同时具备充值业务查看权限时,才能读取 UR#37 提供的完整审批详情。企微运营权限不能绕过充值业务权限。
- 资源不存在与无权访问统一返回禁止访问语义,不能借列表、详情、支付状态或附件下载的差异探测其他店铺充值。
- 列表沿用 `page/page_size`,默认第 1 页、每页 20、最大 100默认 `created_at DESC, id DESC`。店铺、充值状态、支付方式、提交人和起止时间等筛选按 AND 组合,空参数不改变查询。
- 列表项与 UR#44 对齐,至少返回充值 ID/单号、店铺、金额、支付方式、真实提交人、充值状态及名称、`approval_source`、审批状态及名称、代理投影为空的当前审批人摘要、处理状态及名称、创建和完成时间。
- 详情返回完整充值业务资料、结构化申请附件、支付摘要、根级处理状态以及按主体投影的 `approval` 对象。在线固定 `approval_source=none``approval=null`;新线下固定 `approval_source=wecom`;历史本地审批使用 `legacy`,不伪造企微时间线。
- 轻量支付状态接口只适用于在线充值,至少返回:充值 ID/单号、充值 `status/status_name``payment_status/payment_status_name``processing_status/processing_status_name`、可空 `paid_at/completed_at`。不返回 `qr_content`、钱包余额、审批详情、`expires_at`、支付通道或配置。
### 数据模型、索引与迁移
- `tb_agent_recharge_record` 保留现有业务主表,并补充:`request_id`、请求指纹、结构化申请附件、真实提交人显示快照、`approval_instance_id`、处理状态/错误/开始/完成时间、处理租约、乐观锁版本及可靠执行所需字段。数据库不建立外键,也不使用 GORM 关联标签。
- 新线下附件使用结构化 `{file_key,file_name,file_size}` 列表保存。历史 `payment_voucher_key` 字符串或字符串数组只读兼容;迁移不能丢失旧凭证,也不能把短期预签名 URL 写成业务事实。
- `tb_payment` 增加 `order_type=agent_recharge` 常量及本需求需要的 `qr_content`、稳定失败码/摘要和预下单恢复事实。付款内容不得写普通访问日志API 仅在在线创建成功响应中返回。
- 充值记录对 `(user_id,request_id)` 建有效记录唯一约束并保存请求指纹;`approval_instance_id` 对新线下充值保持一单一审批唯一性;为待处理状态/租约、店铺+创建时间和支付单业务关联建立必要索引。
- 支付单号、第三方交易号和钱包充值流水分别建立符合其语义的唯一约束。软删除不能允许相同第三方资金事实再次入账。
- 数据库金额约束继续允许历史及线下 `amount>=1`;在线 `amount>=10000` 和两条路径的 `amount<=100000000` 必须在 Domain 中按支付方式重校验。不得用一个全局最低 100 元约束破坏线下小额代充。
- 发布前盘点充值状态、支付状态、重复第三方交易号、重复充值流水、孤立支付单、待处理线下单和历史附件格式。任何不确定资金记录进入异常清单,不由迁移脚本猜测完成状态。
### 停机发布、存量迁移与回滚
- 采用已确认的停机发布同时切换迁移、API、Worker、支付回调分发、企微终态消费和前端。新版本不保留旧本地审批兼容窗口。
- 历史已完成、已关闭、已退款和已驳回的线下充值只读保留并标记/投影为 `approval_source=legacy`,不补企微审批,也不伪造审批人或时间线。
- 发布时仍待处理的线下充值,只有在真实提交人可确认、平台账号已绑定企微且金额/付款凭证等资料完整时,才幂等创建真实企微审批继续处理;其余进入明确迁移异常清单,不自动入账,也不能再调用旧 `offline-pay`
- 上线前必须确认 UR#37 的线下充值场景和模板已发布、平台绑定可用、回调/轮询可达、钱包入账 Worker 和通知 Worker 已部署;条件不满足时不得开放线下创建入口。
- 回滚应用时保留已经创建的充值单、支付单、真实支付事实、企微实例、Outbox、Integration Log、Audit Event、钱包流水和通知。已进入第三方支付或真实企微的业务不能恢复旧本地按钮只能继续安全同步、重试或人工处置。
- 已经实际入账的资金不通过迁移或应用回滚自动反向删除;任何资金冲正必须是后续独立、受审计的业务流程。
### 统一错误与安全响应
- 所有接口使用统一 `{code,msg,data,timestamp}`;分页使用项目公共结构。金额始终使用分,时间使用项目统一时区/RFC3339格式。
- 请求字段、在线/线下金额、支付方式、备注或附件元数据非法使用 `CodeInvalidParam=1001`;未登录使用 `CodeUnauthorized=1004`;企业账号、错误创建角色、越权店铺及无权资源统一使用 `CodeForbidden=1005`
- 相同 `request_id` 的请求指纹不一致使用 `CodeConflict=1007`;状态不允许的业务动作使用 `CodeInvalidStatus=1050`;无可用支付配置使用 `CodeNoPaymentConfig=1175`;企微场景/模板/本人绑定不可用复用 UR#37 的稳定错误语义。
- 微信/支付宝明确预下单失败或恢复失败使用统一、可区分支付方式的支付错误;若现有错误码不足,新增公共错误码而不是在 Handler 拼接底层错误。结果未知返回稳定“支付创建结果确认中/请稍后重试”语义,不透传 SDK、HTTP、数据库或 Redis 错误。
- Handler 参数验证统一返回公共参数错误Application/Domain/Query 不向调用方返回 `fmt.Errorf`。日志、审计和响应不得包含私钥、Secret、商户签名、完整回调报文、对象存储永久凭证或企微敏感标识。
### 前端页面与交互
- 代理资金页保留充值入口。打开时获取可用支付方式,金额输入单位为元并明确最低 100 元、最高 100 万元;只展示后端返回的微信/支付宝选项,无可用方式时禁止提交并显示统一业务提示。
- 用户选择支付方式并点击创建时生成新的 `request_id`。同一次请求超时重试复用原 ID用户再次主动点击“创建支付”生成新 ID和新订单不搜索或复用旧待支付单。
- 创建成功后用 `qr_content` 渲染二维码,展示充值单号、金额、支付方式和本地支付/入账状态。不展示精确过期倒计时,不请求二维码图片,也不显示“取消支付”或调用“重新生成二维码”接口。
- 在线页面可提供“再次创建支付”用户动作,但它本质上重新调用创建接口并形成独立业务单;旧单在充值记录中继续自然收敛。
- 页面可见时每 3 秒读取本地轻量状态,隐藏时暂停、恢复时立即刷新。支付成功但入账处理中/失败时保留明确提示;入账完成后停止轮询、刷新现有钱包资金概况并展示成功。
- 平台线下创建页只展示目标店铺、金额、可选备注和 15 个付款凭证。提交中禁止重复操作;场景、绑定、附件或企微提交前置失败时保留表单内容。
- 线下创建成功进入充值详情,分为业务资料、企微审批和钱包处理结果三个区域。不展示本地确认、拒绝、退回、重提或操作密码输入框。
- 列表和详情按 `approval_source` 展示:`none` 审批列为“-”;`wecom` 展示只读企微状态;`legacy` 展示“历史审批”且无操作按钮。代理界面不展示审批人列或内部意见。
- 加载、空、网络失败、支付方式为空、支付创建失败、支付已成功但入账失败、企微提交中/失败、审批拒绝和异常撤销均有独立展示,不由前端自行推导或改写服务端状态。
## Testing Decisions
### 最高公共测试接缝
- 主要自动化接缝为Fiber 真实路由与认证 → Recharge Application/Domain/Query → GORM → 现有测试 PostgreSQL → Outbox/公开 Worker Handler → 代理主钱包、唯一流水、通知和统一审计。测试只断言公开响应和持久化业务事实,不断言私有函数或目录结构。
- 微信/支付宝网络边界使用可编程 Payment Adapter覆盖预下单、查单、回调确认和异常不在本地或 Agent 自动化中调用真实微信、支付宝。支付协议验签与基础能力优先复用 C 端已经验证的实现,本需求重点验证后台充值分发和业务闭环。
- 企微网络边界复用 UR#37 的可编程 WeCom Adapter并保留真实企微线下充值验收对象存储直接使用现有真实 S3 Provider不建立内存替身。
- 实现时沉淀可复用测试 Harness环境守卫、唯一运行标识、真实认证/Fiber 请求、受控支付与企微 Adapter、Outbox 捕获、公开 Worker 驱动、真实 S3、精确资源台账和失败清理并提供不含敏感信息的 HTTP/curl 冒烟模板。
### 环境隔离
- 本地开发和 Agent 自动化测试固定使用 Redis DB 7已部署测试环境继续使用 Redis DB 6。测试启动前必须核验普通 Redis Client、Asynq Client和 Worker Server 实际 DB任一不是 7 时立即失败。
- 自动化不得向 DB 6 投递任务,不执行 `FLUSHDB`、全前缀删除或清空队列。每次运行生成唯一 `run_id`,只清理本次实际创建的精确 Key和任务事实。
- PostgreSQL 继续使用现有测试数据库,不新建 Agent 专用数据库。所有账号、店铺、钱包、充值、支付、审批、流水和通知夹具使用唯一运行标识,只按实际主键精确清理;禁止 `TRUNCATE`、模糊删除或改写既有业务数据。
- S3 测试对象 Key包含唯一运行标识覆盖真实上传、Head/元数据、读取及精确删除。网络、鉴权或删除失败使测试失败;禁止按目录前缀或 Bucket 级清理。
- 自动化捕获待投递任务后直接调用公开 Worker Handler不通过 `sleep` 等待测试环境 Worker 抢任务。生产仍使用真实 Asynq 投递。
### 在线创建、支付和查单场景
- HTTP 创建覆盖:代理成功、平台/超管在线拒绝、企业拒绝、请求携带 `shop_id` 拒绝、微信/支付宝可用性、配置在列表后失效、金额 9999/10000/100000000/100000001、支付方式非法和支付方式创建后不可切换。
- 幂等覆盖:同账号同 `request_id` 同载荷返回原单,不同载荷冲突,并发重复只有一张充值单和支付单;新 `request_id` 在金额相同且旧单待支付时仍创建新单。
- Adapter 预下单覆盖微信 Native 与支付宝 PreCreate 成功、明确失败、超时但未创建、超时后第三方已创建、响应丢失、恢复仍未知和重复恢复;断言不会因同一请求再建支付单。
- 响应覆盖原样 `qr_content`、前端可渲染字符串/HTTPS URL、不返回 `expires_at`、支付通道或配置,不创建二维码图片文件。
- 回调覆盖签名/验签失败、订单不存在、业务类型错误、支付方式不符、配置不符、金额不符、第三方交易号冲突、业务关联错误、成功、重复成功和乱序通知。
- 查单覆盖明确待支付、已支付、已关闭/失效、不存在、未知、超时和限流;证明前端状态接口不调用 Adapter查单与回调并发只固化一次支付成功。
- 迟到成功覆盖支付单已失败、充值单已关闭后收到有效成功回调,最终仍只入账一次;多张独立支付单均成功时分别入账。
### 钱包、通知和审计场景
- 两阶段测试证明支付成功事务完成后即使钱包事务失败,支付单仍已支付、充值单为已支付、处理状态失败/重试,不能回到待支付或向渠道返回失败。
- 钱包覆盖正常余额、负余额自然冲减、乐观锁冲突、数据库失败、流水已存在但状态未完成、Worker 崩溃前/后、租约过期、重复 Outbox和两个 Worker 并发;余额、版本、唯一流水和完成审计只变化一次。
- 通知覆盖在线主账号、在线非主账号提交人、线下目标代理主账号、同一接收人身份重合、重复 Worker和通知投递失败每个充值单与接收人最多一条到账通知通知失败不回滚钱包。
- 审计覆盖在线创建、线下创建、明确预下单失败、结果未知、支付确认、第三方状态异常、钱包成功/失败、审批拒绝、撤销和通过后撤销;验证金额、状态前后值和关联 ID完整且不泄漏敏感配置。
### 线下企微场景
- 创建覆盖平台/超级管理员本人绑定成功、未绑定、场景暂停、模板失效、企业/代理拒绝、目标店铺越权、金额 0/1/100000000/100000001、备注边界、附件 0/1/5/6、对象不存在、其他主体对象和元数据不符。
- 事务测试证明充值单、结构化附件、唯一企微实例和提交 Outbox要么全部存在要么全部不存在相同 `request_id` 不产生第二条审批。
- 状态自动化覆盖提交中、审批中、同意、拒绝、撤销、删除、通过后撤销、重复/乱序回调和轮询并发;拒绝后原单不可编辑或重提,新申请产生全新业务事实。
- 入账前撤销验证任务被阻止且充值关闭;入账后撤销验证余额不自动扣回、充值保持完成,并产生严重审计和平台异常通知。
- 真实企微至少使用平台本人绑定身份创建一笔独立线下代充值,上传真实 S3付款凭证完成 `applyevent`、人工同意、加密回调或轮询同步、钱包入账、唯一流水、到账通知和审计核对。回调沿用“企业微信 → 用户中转应用 → 本地服务”原样转发,后端仍完成验签解密。
- 真实企微不要求每次人工制造撤销、删除或通过后撤销,这些异常由可编程 WeCom Adapter 自动化稳定覆盖。
### 查询、权限、迁移和前端验收
- 查询覆盖默认分页、最大页大小、稳定排序、多个筛选 AND组合、代理本店/下级范围、平台范围、企业拒绝,以及列表/详情/支付状态/附件使用同一权限投影。
- 信息投影分别以平台和代理读取同一线下充值:平台在具备业务权限时可见完整审批资料,代理只能看到业务资料和最小审批摘要;任何响应不返回支付配置、通道或内部错误。
- 迁移演练覆盖支付状态盘点、默认值/注释/约束不一致、历史终态 legacy、可迁移待处理线下单、缺少平台绑定或附件的异常清单、重复执行幂等以及旧 `offline-pay``reject``resubmit` 路由确实不存在。
- 前端验收覆盖可用方式加载、每次主动新建、二维码渲染、无倒计时、3 秒本地轮询、页面隐藏暂停、支付成功/入账处理中/失败、余额刷新、线下表单、企微只读详情、审批与到账两类通知、加载/空/失败状态。
- 本地自动化不要求真实微信或支付宝。部署到测试环境后由用户手工完成一笔微信 Native 和一笔支付宝 PreCreate 最低金额扫码,验证真实付款内容、回调/查单、本地支付状态、钱包入账和到账通知;该人工联调不阻塞本地实现完成门禁,但属于上线前验收。
- 完成门禁至少包括目标单元/集成测试、相关包测试、全量 Go 测试、并发/竞态专项、静态检查、迁移演练、OpenAPI 生成校验、真实 S3和真实企微线下充值。新增 Handler 时同步两个接口文档生成入口。
## Out of Scope
- 不建设多支付通道池、通道优先级、权重轮询、自动切换或故障转移。
- 不让前端选择或提交支付通道、支付配置 ID、商户号或目标在线充值店铺。
- 不生成二维码图片,不新增二维码生成、重新生成、主动取消支付或手工关单接口。
- 不向后台在线充值返回 `expires_at`,不展示本地推算的精确倒计时,也不以本地时间直接关闭第三方支付单。
- 不复用相同金额或未过期支付单;也不因新单创建而自动取消旧单。
- 不在本地或 Agent 自动化中调用真实微信、支付宝;真实扫码由部署测试环境人工验收。
- 不接入微信/支付宝自动退款,不因充值状态 5 扩展本期退款流程。
- 不允许代理线下代充值,不允许平台/超级管理员替代理创建在线支付,不允许企业账号访问。
- 不为新充值支持 `bank`,不允许创建后修改金额、目标店铺或支付方式。
- 不保留本地确认入账、拒绝、退回、重提、操作密码或人工重复入账按钮。
- 不新增 `7=已退回`、支付“已关闭”状态或 `wallet_posting_status` 同义字段。
- 不向代理公开企微审批人、内部意见、审批人附件、支付配置或内部异常详情。
- 不借 UR#34 重构 C 端全部支付、全仓钱包、全局支付配置或未触碰的历史订单模块。
## Further Notes
- 当前代码已经有代理充值表、主钱包和流水、统一 `tb_payment`、微信回调基础、支付宝 WAP 支付基础及支付配置,但代理充值创建只支持 `wechat/offline`,未创建支付单或返回付款内容;支付宝 PreCreate、微信 Native 后台充值、支付查单补偿和两阶段入账需要补齐。
- 当前支付配置模型把微信提供方和支付宝参数放在同一条全局生效配置中。UR#34 按现状复用,不把它扩成多通道路由系统。
- 当前微信 v3 已有查单/关单能力,微信 v2适配器缺少查单支付宝 SDK具备 PreCreate、TradeQuery和TradeClose。支付方式可用性必须以本需求实际需要的 Adapter能力为准不能只判断某个字段非空。
- 当前 `tb_payment` 迁移写的是 `14`,运行代码和 DTO使用 `03`;这是实施前必须通过存量核查解决的历史一致性问题,不是新 Agent可以忽略的文档差异。
- 当前旧线下充值使用字符串 Key列表、操作密码和本地 `offline-pay/reject`。新实现以本 PRD 和 UR#37公共企微契约为准,旧实现仅用于迁移事实核对,不代表目标行为。