220 lines
36 KiB
Markdown
220 lines
36 KiB
Markdown
# PRD:UR#35 退款企微审批与整单终结
|
||
|
||
Status: ready-for-agent
|
||
|
||
---
|
||
|
||
## Problem Statement
|
||
|
||
当前退款流程把退款申请、平台内审批、退款金额修改、钱包回款、佣金回扣和套餐失效混在同一个旧 Service 中。系统仍提供本地通过、拒绝、退回和重提接口,审批人能够修改实际退款金额;审批通过后又用进程内 Goroutine 分别处理佣金与套餐,失败后既不能可靠恢复,也可能错误地把未完成的处理标记为完成。
|
||
|
||
现有退款创建接口还要求调用方提交订单实收金额,并允许用单条套餐使用记录限定退款范围。这样会把本应由订单事实决定的数据交给前端,也与本期确认的“金额可以小于实收,但一旦通过就按整张订单终结”相冲突。代理退款查询当前按创建账号隔离,上级代理无法在既有店铺层级数据范围内管理下级退款;平台内部审批资料又缺少按主体投影,存在向代理泄漏审批人、意见或审批附件的风险。
|
||
|
||
退款涉及真实资金、负余额、已发放佣金、套餐队列、资产状态和外部企微终态。系统必须明确区分申请金额、实际完成金额、企微审批状态和本地业务处理状态,并保证重复回调、重复 Worker、进程中断和局部失败不会造成重复回款、重复扣佣或伪造成功。
|
||
|
||
## Solution
|
||
|
||
保留现有退款创建、列表和详情资源,但把新退款申请接入 UR#37 提供的企业微信审批公共能力。创建时后端从订单读取实收金额及相关快照,只接收申请金额、原因、备注和 1~5 个本地对象存储附件;金额提交后固定,企微审批人只能同意或拒绝。退款单、唯一企微审批实例和提交 Outbox 在同一 PostgreSQL 事务创建,接口立即返回本地退款及“企微提交中”状态。
|
||
|
||
企微同意后,无论申请金额是否等于订单实收金额,都启动整单退款终结:订单变为已退款,该订单产生的有效套餐全部失效,相关佣金全部失效;已发放佣金从对应佣金钱包全额扣回,余额允许为负。只有代理主钱包支付订单自动按原扣款流水回溯原钱包;微信、支付宝、线下及个人资产钱包订单均由财务先在系统外完成退款,再在企微同意,系统不调用支付渠道退款,也不回充资产钱包。
|
||
|
||
审批结论和本地业务处理分开保存与展示。企微已经同意后,即使资金、佣金或套餐处理失败,审批状态和退款状态仍保持已通过,失败由独立处理状态、错误摘要和可靠 Worker 重试表达。企微拒绝即终结当前退款;业务人员修正问题后若仍需退款,必须新建退款单,不能编辑或重提原单。
|
||
|
||
## User Stories
|
||
|
||
1. 作为代理退款发起人,我希望使用订单事实创建退款,而不需要自行填写订单实收金额或选择某条套餐使用记录。
|
||
2. 作为平台退款发起人,我希望填写固定的申请退款金额、原因、备注和业务凭证,并在提交后立即获得本地退款单号。
|
||
3. 作为退款发起人,我希望系统在申请金额不大于订单实收金额时允许提交,并明确告诉我本次通过后会整单终结。
|
||
4. 作为退款发起人,我希望同一订单存在审批中或业务处理未完成的退款时不能再次创建,避免并发退款。
|
||
5. 作为平台账号,我希望通过本人绑定的企微成员发起审批,确保企微中的实际发起身份可追溯。
|
||
6. 作为代理账号,我希望无需绑定企微,由配置的固定成员代提交,同时审批表单仍展示我才是真实业务提交人。
|
||
7. 作为审批人,我希望在企微看到退款单号、订单实收金额、可退款区间、本次申请金额、原因、备注、附件和真实提交人。
|
||
8. 作为审批人,我只能同意或拒绝固定金额,不能在审批时修改退款金额。
|
||
9. 作为业务人员,我希望金额填写错误时拒绝当前单并重新创建,而不是改写已经提交的业务事实。
|
||
10. 作为财务人员,我希望微信、支付宝、线下及资产钱包订单先在线下完成人工退款,再通过企微表达已确认完成,无需回到系统点击第二次确认。
|
||
11. 作为代理主钱包所有者,我希望企微同意后资金准确退回当时真实扣款的钱包,而不是按当前代理关系猜测退款钱包。
|
||
12. 作为欠款代理,我希望退款可以自然冲减主钱包负余额,而不是因余额为负而拒绝入账。
|
||
13. 作为个人客户,我希望资产钱包支付订单仍可申请退款,但系统不会错误地把资金自动充回资产钱包。
|
||
14. 作为佣金归属代理,我希望订单退款时未发放佣金停止发放,已发放佣金被完整扣回,且每笔变化可追溯。
|
||
15. 作为佣金归属代理,我接受退款回扣后佣金钱包余额为负,以真实反映已经支取但现应追回的佣金。
|
||
16. 作为套餐使用者,我希望退款通过后该订单生成的有效套餐全部失效,主套餐失效时其加油包也一并失效。
|
||
17. 作为资产使用者,我希望退款套餐失效后系统尝试激活下一条排队主套餐;没有可激活套餐时停止资产使用。
|
||
18. 作为退款发起人,我希望看到申请金额与实际退款金额的区别,避免把审批通过误认为所有本地处理都已完成。
|
||
19. 作为退款发起人,我希望企微拒绝后原退款单保持只读,并能从订单重新进入创建流程发起一张新退款。
|
||
20. 作为代理管理者,我希望按既有店铺层级和退款查看权限看到本店及有权管理的下级店铺退款,而不是只能看到自己创建的记录。
|
||
21. 作为代理查看者,我希望查看退款凭证、真实业务提交人、审批状态和业务处理结果,但看不到平台内部审批人、意见或审批人附件。
|
||
22. 作为平台查看者,我希望只有在具备退款业务查看权限时才能读取完整企微审批时间线和附件,企微运营权限不能绕过业务权限。
|
||
23. 作为运维人员,我希望审批状态、退款状态和业务处理状态分别展示,能准确识别“企微已通过但本地处理失败”。
|
||
24. 作为运维人员,我希望资金、佣金、订单和套餐步骤失败后可以可靠重试,且已经完成的步骤不会重复执行。
|
||
25. 作为审计人员,我希望退款、订单、钱包、资金流水、佣金、套餐和企微实例之间可以完整关联,并保留金额与状态的前后事实。
|
||
26. 作为审计人员,我希望自动退款、佣金失效、通过后撤销和人工异常处置都有明确原因与操作者,而不是只依赖自由文本备注。
|
||
27. 作为系统维护人员,我希望企微重复回调、回调与轮询并发、Worker 重投和进程中断都不会重复回款或重复扣佣。
|
||
28. 作为系统维护人员,我希望企微撤销、删除和通过后撤销进入明确异常处置,不会自动放行新的退款或自动冲正已经完成的资金。
|
||
29. 作为前端开发者,我希望创建、列表和详情返回稳定的退款、审批及处理契约,不需要在页面推断资金是否已经完成。
|
||
30. 作为验收人员,我希望实现阶段同时通过可重复的企微 Adapter 自动化和真实企微同意/拒绝链路,而不是把真实企微验证推迟到后续联调。
|
||
|
||
## Implementation Decisions
|
||
|
||
### 范围、依赖与领域口径
|
||
|
||
- UR#35 负责退款申请、退款终态消费、整单退款终结、退款 Query 和前端业务页面;企微连接、身份绑定、模板版本、附件上传、提交、加密回调、轮询和审批详情由 UR#37 的公共能力提供,退款模块不得自行实现第二套企微客户端或审批状态机。
|
||
- “申请退款金额”是提交企微的固定金额;“实际退款金额”只表示资金已经实际完成。两者不因为整单终结而自动改成订单实收金额。
|
||
- “整单退款终结”表示订单、该订单生成的套餐和该订单佣金资格全部终结,不表示必须按订单全额向客户退款。即使申请金额小于订单实收金额,也不保留差额的第二次退款权利。
|
||
- 一张退款单只对应一条企微审批实例。企微拒绝后的再次退款是新的业务事实,必须有新的退款 ID、退款单号、申请资料、提交人快照和企微审批实例。
|
||
- 退款创建、企微终态消费、代理钱包回溯、佣金失效和套餐失效属于复杂写用例,采用 Application UseCase、Domain、Repository 与 Infrastructure Adapter 分层;退款列表和详情采用 Query 通道。只迁移完成本需求所需的最小完整退款用例,不主动迁移未触碰的订单、钱包或套餐模块。
|
||
|
||
### 创建退款契约
|
||
|
||
- 保留 `POST /api/admin/refunds`。允许超级管理员、平台账号和代理账号发起;企业账号不允许发起。所有主体仍受既有认证、退款业务权限、订单数据范围和越权防护约束。
|
||
- 请求只包含:`order_id`;`requested_refund_amount`;必填 `refund_reason`(最多 1000 字);可选 `remark`(最多 500 字);必填 `attachments`(1~5 项)。
|
||
- 每个附件项固定包含 `file_key`、`file_name` 和 `file_size`。附件必须来自当前本地私有对象存储授权范围,后端按现有存储规则校验对象存在、上传归属、文件类型和实际大小;请求中的名称与大小作为申请快照,不能替代对象元数据校验。
|
||
- 新请求不再接受 `actual_received_amount` 或 `package_usage_id`。订单实收金额、订单号、资产、店铺、买卖方、支付方式和必要的资金关联事实由后端从有权限访问的订单读取并固化快照。
|
||
- 申请金额必须满足 `0 < requested_refund_amount <= 订单实收金额`。只允许对已支付且尚未整单退款的订单创建。
|
||
- 创建前必须确认 `refund_approval` 场景、当前模板和本次企微发起身份可用。平台/超级管理员使用本人有效企微绑定;代理使用配置的固定代提交成员。任一前置不可用时,在写入退款、审批实例或 Outbox 前失败,前端保留当前表单。
|
||
- 同一订单存在提交中、审批中、已通过但业务处理未成功,或尚未完成异常人工处置的退款时拒绝创建。已拒绝退款不再占用活跃名额;已撤销、已删除或通过后撤销不能自动放行新建。
|
||
- 退款单、唯一审批实例和 `WeComApprovalSubmissionRequested` Outbox 必须在同一 PostgreSQL 事务创建。创建接口不等待远程企微,成功响应返回退款详情以及 `approval.status=0` 的“提交中”状态。
|
||
- 创建并发通过数据库唯一约束、状态条件或等价的持久化业务约束保证同一订单不会产生两张活跃退款;Redis 只能作为快速防重,不能成为唯一正确性依据。
|
||
|
||
### 固定金额与企微表单
|
||
|
||
- 退款企微模板至少展示:退款单号、订单号、店铺、真实业务提交人、订单实收金额、可退款区间、本次申请退款金额、退款原因、申请备注和申请附件。
|
||
- 可退款区间在提交时固定为大于 0 且不超过订单实收金额;本次申请金额只读。企微审批人只能使用模板配置的同意或拒绝动作,不提供金额修改控件。
|
||
- 系统不提供本地审批按钮,也不向企微回写或伪造审批结论。认为金额错误时必须拒绝当前退款,再创建新退款。
|
||
- 申请附件的本地对象 Key 是权威资料;上传企微的 `media_id` 只是审批副本。审批人后续上传的附件属于企微审批资料,不能替代退款申请附件。
|
||
|
||
### 三套独立状态
|
||
|
||
- 退款业务状态为生命周期 `int`:`1=待审批`、`2=已通过`、`3=已拒绝`、`4=已退回`、`5=已撤销/审批已删除`。状态 4 只保留历史兼容,新企微审批不再产生“退回”。
|
||
- 本地业务处理状态为生命周期 `int`:`0=未触发`、`1=处理中`、`2=处理成功`、`3=处理失败`。各响应同时返回对应的 `processing_status_name`。
|
||
- 企微审批状态完全复用 UR#37 公共常量:`0=提交中`、`1=审批中`、`2=已通过`、`3=已驳回`、`4=已撤销`、`5=通过后撤销`、`6=已删除`、`7=提交失败`、`8=提交结果未知`。退款模块不得复制或重新编号。
|
||
- 三套状态分别返回,禁止用退款状态或处理状态覆盖企微状态。所有状态 DTO 的 description 必须从公共 constants 原文复制,并提供对应中文名称字段。
|
||
- 企微驳回把退款状态从待审批条件更新为已拒绝,处理状态保持未触发;拒绝原因和企微时间线来自公共审批快照。
|
||
- 企微同意把退款状态条件更新为已通过,并触发可靠的退款终态 Outbox。即使后续本地处理失败,退款状态和企微状态也不得降级或回改。
|
||
- 企微撤销或删除把退款状态置为 5,记录异常原因并进入人工处置;不自动执行资金动作,也不自动允许新退款。
|
||
- 通过后撤销时,资金尚未执行则阻止后续资金任务;资金已执行则不自动冲正,保留实际退款金额,写 `critical` Audit Event 和站内告警,交由人工处理。
|
||
|
||
### 资金处理与实际退款金额
|
||
|
||
- `requested_refund_amount` 始终是申请金额。新增 `actual_refund_amount`,只在资金已实际完成时写入本次申请金额;未完成时为 `null`。
|
||
- 微信、支付宝、线下及其他非代理主钱包支付订单,由财务在系统外完成退款后再同意企微。企微同意被视为财务已经确认完成,终态 Worker 写入 `actual_refund_amount=requested_refund_amount`;系统不调用支付渠道退款 API,也不提供本地 `manual-complete` 二次确认。
|
||
- 个人客户使用资产钱包支付的订单同样由财务在系统外退款。系统不得调用现有资产钱包自动回充逻辑,不创建资产钱包退款流水;企微同意时记录实际退款金额并继续整单终结。
|
||
- 代理主钱包支付订单只按原订单扣款流水定位原主钱包和原资金关系,退回本次申请金额并写唯一退款流水。禁止根据当前店铺上下级关系、当前买卖方或当前钱包猜测退款目标。
|
||
- 如果代理订单缺少可核验的原扣款流水,资金步骤失败,`actual_refund_amount` 保持为空,处理状态为失败并记录可运维错误摘要;不得走历史兼容猜测分支。
|
||
- 代理主钱包余额增加与钱包版本、唯一退款流水、`actual_refund_amount` 和资金 Audit Event 在同一事务提交。钱包原余额为负时允许正常增加,结果自然冲减欠款。
|
||
- 资金已经完成但后续佣金或套餐处理失败时,`actual_refund_amount` 必须保留,不能因整单处理未成功而清空或重复退款。
|
||
- 旧 `approved_refund_amount` 只保留历史读取和迁移兼容,不再是新创建、企微表单或新公共响应中的业务概念。
|
||
|
||
### 订单、佣金与套餐整单终结
|
||
|
||
- 一旦企微同意,本次处理以整张订单为边界。资金步骤完成后,订单支付状态条件更新为已退款;申请金额小于实收金额时也执行同样更新,并禁止该订单再申请差额退款。
|
||
- 查找该订单产生的全部佣金记录。处于已冻结、解冻中、尚未发放或待人工修正的记录不移动钱包,直接改为已失效;处于已发放的记录先从对应佣金钱包全额扣回,再改为已失效,钱包允许变为负数。
|
||
- 佣金记录增加结构化失效事实:`invalid_reason` 至少支持 `order_refund` 和 `manual_resolution`;退款自动失效时保存 `invalid_refund_id`、`invalidated_at`,人工失效时另存人工操作者。不能只在 `remark` 中描述失效。
|
||
- 每条已发放佣金以“退款单 + 佣金记录”为业务防重键。佣金状态、佣金钱包余额和版本、回扣流水及统一 Audit Event 在同一事务提交;命中已完成防重事实时直接返回成功,不重复扣款或重复审计。
|
||
- 退款失效后的佣金不得被原有解冻、发放或补算任务重新发放。订单佣金流程的派生结论必须与全部佣金已失效保持一致。
|
||
- 失效该订单生成的所有仍有效套餐,不接受 `package_usage_id` 精确失效。命中主套餐时级联失效其加油包,并为套餐保存退款 ID、退款单号及失效时间等退款快照。
|
||
- 套餐失效完成后,按现有套餐队列规则尝试激活下一条待生效主套餐;没有下一条主套餐时按公共卡/设备状态写入能力停止资产。该过程复用套餐与卡状态领域能力,不在退款模块复制状态判断。
|
||
- 资金、订单、每条佣金和套餐步骤均需可单独识别已完成事实。任何步骤失败都把处理状态置为失败并由可靠 Worker 重试;只有全部必要步骤成功后才把处理状态置为处理成功。
|
||
|
||
### 可靠执行、幂等与审计
|
||
|
||
- 不再使用进程内 Goroutine 处理佣金或套餐。企微首次进入终态时由公共审批能力写业务 Outbox,退款终态 Worker 使用 Asynq 执行;载荷只传结构化退款标识,不传预序列化字节、附件内容或密钥。
|
||
- Worker 通过处理状态、条件更新和有期限租约领取任务。未过期的处理中任务不能被第二个消费者重复执行;租约过期可恢复。失败摘要不得包含数据库连接、对象存储签名 URL、企微密钥或原始第三方响应。
|
||
- 退款终态事件、资金回款、佣金回扣、套餐失效和处理成功都必须各自具备持久化业务幂等事实。Redis 锁可以减少并发,但不能代替数据库条件更新、唯一约束和钱包乐观锁。
|
||
- 审计使用全局统一 Audit Event,不新建退款私有审计表。至少关联退款、订单、审批实例、钱包、钱包流水、佣金记录和套餐使用记录,并记录操作来源、真实业务提交人、系统执行身份、失效原因、金额和状态前后值。
|
||
- 钱包余额/版本/流水与其资金 Audit Event 同事务;佣金状态/钱包/回扣流水与其 Audit Event 同事务;订单及关键退款状态变更与对应 Audit Event 同事务。重复任务命中已完成事实时不新增第二条等价审计。
|
||
- 外部企微调用、附件上传、回调和详情同步使用公共 Integration Log;Integration Log 只记录接口、耗时、企微错误码/摘要和关联标识,不记录 Token、Secret、EncodingAESKey、完整对象 Key、临时 `media_id` 或文件内容。
|
||
|
||
### 权限、数据范围与信息投影
|
||
|
||
- 移除当前代理按 `creator` 隔离退款的专属规则。代理主账号及店铺内具备退款查看权限的账号按既有店铺层级数据范围读取本店及可管理下级店铺退款。
|
||
- 列表、详情、业务附件下载和导出必须复用同一主体权限投影。资源不存在与无权访问统一返回禁止访问语义,不能借错误差异探测其他店铺退款。
|
||
- 代理可见:退款业务资料、申请附件、订单及金额快照、真实业务提交人、企微审批状态和时间、业务处理状态及面向业务的失败提示。
|
||
- 代理不可见:企微审批人、内部意见、审批人上传附件、企微内部成员标识、模板内部映射及运维错误详情。
|
||
- 平台账号和超级管理员仍必须具备退款业务查看权限,才可读取完整审批人、意见、时间线和审批附件。企微审批运营或异常恢复权限本身不能绕过退款业务数据权限。
|
||
- 附件返回受保护的业务附件引用及下载能力,不把对象存储签名 URL 或企微临时 `media_id` 作为永久字段。历史导出或已获得的附件引用也不能绕过当前权限。
|
||
|
||
### API 与查询契约
|
||
|
||
- 保留 `POST /api/admin/refunds`、`GET /api/admin/refunds` 和 `GET /api/admin/refunds/{id}`。
|
||
- 下线并不再注册:`POST /api/admin/refunds/{id}/approve`、`/reject`、`/return`、`/resubmit`,以及任何本地人工退款确认接口。不能保留隐藏兼容入口。
|
||
- 列表继续使用 `page`、`page_size`、退款状态、订单、店铺和资产标识等既有筛选;默认第 1 页、每页 20、最大 100,默认按创建时间倒序并以 ID 作为并列排序键。所有筛选按 AND 组合,空参数不改变原查询。
|
||
- 创建成功的 `data` 与详情使用同一退款详情结构。列表项固定返回退款 ID/单号、订单与资产快照、店铺、真实业务提交人、订单实收金额、申请金额、实际退款金额、退款状态及名称、企微来源/状态及名称、当前审批人摘要和处理状态及名称。代理投影中的当前审批人摘要固定为空。
|
||
- 详情返回完整退款业务资料、申请附件、订单/支付快照、`approval` 对象以及根级处理状态。申请附件项返回 `file_key`、`file_name`、`file_size` 和受保护下载能力;不能返回对象存储永久地址或企微 `media_id`。
|
||
- `approval` 固定包含 `source`、`approval_instance_id`、可空 `sp_no`、`status`、`status_name`、`template_version`、真实业务提交人、状态更新时间和 `business_process_result`;平台完整投影另包含审批人、意见、审批附件和时间线,代理投影不返回这些内部字段。
|
||
- 详情根级处理字段至少包含 `processing_status`、`processing_status_name`、面向当前主体脱敏后的 `processing_error`、开始时间和完成时间。审批状态、退款状态和处理状态不得合并成单个前端状态。
|
||
- 历史本地审批返回 `approval.source=legacy`,不伪造 `sp_no`、审批节点或企微时间线。新企微退款固定返回 `approval.source=wecom`。
|
||
- 所有接口使用统一 `{code,msg,data,timestamp}` 响应和项目分页结构。错误码固定复用当前公共语义:请求字段、金额或附件元数据非法使用 `CodeInvalidParam=1001`;未登录使用 `CodeUnauthorized=1004`;订单或退款不存在与越权统一使用 `CodeForbidden=1005`;订单并非已支付、已经整单退款或退款状态不允许使用 `CodeInvalidStatus=1050`;活跃退款及并发重复创建使用 `CodeConflict=1007`;对象不存在/类型非法分别使用 `CodeStorageFileNotFound=1093`、`CodeStorageInvalidFileType=1095`;场景或模板不可用、代理固定成员不可用使用 `CodeServiceUnavailable=2004`。平台本人未绑定使用 `CodeInvalidStatus=1050` 和稳定消息“请先绑定企业微信”,前端结合本人绑定查询显示绑定入口。任何错误均不得透传底层企微、数据库、Redis、对象存储或验证器信息。
|
||
|
||
### 前端页面与交互
|
||
|
||
- 退款创建表单只展示订单、申请退款金额、必填原因、可选备注和 1~5 个附件。订单实收金额及“通过后整单终结”的提示由后端订单/退款契约展示,不允许前端提交或覆盖实收金额。
|
||
- 提交中禁用重复提交;创建成功后进入退款详情,显示企微“提交中”。场景暂停、平台账号未绑定、代理固定成员不可用或附件校验失败时保留全部表单内容,并展示后端明确原因;平台账号未绑定时可原地进入 UR#37 的本人绑定流程。
|
||
- 退款详情分为“退款业务信息”“企微审批信息”“业务处理结果”三个稳定区域。审批与处理状态各自有加载、空、失败和刷新表现。
|
||
- 页面不显示本地通过、拒绝、退回、重提、审批金额修改或人工退款确认按钮。企微拒绝后只读展示原因;若仍需退款,从订单重新进入创建页,不在旧退款详情中编辑。
|
||
- 对非代理钱包订单明确提示财务必须先在系统外完成退款再同意企微;对代理钱包订单提示同意后系统自动回溯原主钱包。
|
||
- 处理失败时代理只看到可行动的业务提示,平台按权限看到脱敏错误摘要和“系统重试中/联系管理员”;不得提供会重复执行资金动作的前端按钮。
|
||
- 通过后撤销等高风险异常使用明显告警,并说明资金不会自动冲正。代理与平台页面严格遵守各自审批资料投影。
|
||
|
||
### 数据迁移、发布与回滚
|
||
|
||
- 退款记录补充唯一审批实例引用、真实提交人显示快照、结构化申请附件、申请备注、实际退款金额、处理状态、错误摘要、处理开始/完成时间及处理租约/版本等可靠执行字段。数据库不建立外键,也不使用 GORM 关联标签。
|
||
- 现有 `actual_received_amount` 继续作为后端生成的订单实收快照;新接口不再接受客户端值。现有 `package_usage_id` 和 `approved_refund_amount` 仅保留历史兼容,新退款不写入且整单处理不读取。
|
||
- 现有 `remark` 若包含历史审批备注,不直接改写语义;新申请备注使用明确的申请备注字段。新附件使用结构化元数据保存,旧 `refund_voucher_key` 只读兼容,并在对象仍存在时投影为历史业务附件。
|
||
- 佣金记录补充结构化失效原因、关联退款、失效时间和必要的人工操作者字段;已发放佣金回扣流水建立退款与佣金记录级唯一业务约束。
|
||
- 停机发布前盘点待审批、已通过但旧异步标记未完整、已拒绝、已退回及历史终态退款。历史终态保留为 `legacy`;待审批记录按 UR#37 迁移规则创建真实企微审批,缺少平台绑定、代理固定身份、附件或真实提交人事实的记录进入明确的迁移待处理清单。
|
||
- 历史状态 4 保持“已退回”只读,不自动创建新审批或改为拒绝。历史已通过但资金、佣金或套餐事实不一致的记录先进入对账与人工处置,不允许迁移脚本猜测已完成。
|
||
- 发布顺序必须先具备 UR#37 公共企微能力、真实模板映射、平台绑定和代理固定成员,再启用退款创建与终态 Worker;旧本地审批路由在同一停机窗口移除。
|
||
- 应用回滚必须保留已形成的退款、企微实例、Outbox、Integration Log、Audit Event、资金流水和失效事实。已经进入真实企微的退款不得恢复旧本地审批按钮,只能继续同步、重试安全的本地步骤或人工处置。
|
||
|
||
## Testing Decisions
|
||
|
||
- 最高公共自动化接缝为:Fiber HTTP 路由与真实认证 → Refund Application/Domain/Query → GORM → 现有测试 PostgreSQL → Outbox/公开 Worker Handler → 钱包、佣金、套餐和统一审计;企业微信网络边界使用可编程 WeCom Adapter。测试只断言公开响应、数据库业务事实、资金流水、状态和审计,不断言私有函数或目录结构。
|
||
- 本地开发和 Agent 自动化测试必须使用 Redis DB 7;已部署测试环境继续使用 DB 6。测试入口在启动前读取并验证 Redis Client、Asynq Client 与 Worker Server 的实际 DB,任一不是 7 就立即失败。禁止向 DB 6 投递任务,禁止对 DB 7 执行 `FLUSHDB`。
|
||
- PostgreSQL 沿用现有测试库,不新建数据库。每次运行使用唯一标识创建隔离订单、店铺、钱包、佣金、套餐和退款夹具,只按实际创建 ID 精确清理;禁止 `TRUNCATE`、清表、模糊删除或修改既有业务数据。
|
||
- 对象存储直接使用现有真实 S3,不建立内存替身。测试使用唯一 Key 上传申请附件,验证元数据、下载及企微附件提交,结束时只删除本次创建对象;真实上传、读取或删除失败均使测试失败。
|
||
- 自动化测试捕获待投递任务后直接调用公开 Worker Handler,不通过 `sleep` 等待后台 Worker。实现需沉淀可复用的环境守卫、唯一夹具、精确清理、真实 S3 和 Worker 驱动 Harness,并提供真实 HTTP/curl 冒烟模板;curl 不替代 Go 自动化断言。
|
||
- 创建 HTTP 测试覆盖三类允许账号、企业账号拒绝、订单越权、订单不存在、未支付/已退款订单、金额为零/负数/超过实收、缺少原因、备注超长、附件数量/元数据/归属错误、场景不可用、平台未绑定、代理固定成员失效和并发重复创建。
|
||
- 创建事务测试证明退款、唯一企微实例和提交 Outbox 要么全部成功,要么全部不存在;远程企微尚未响应时接口仍只产生一组本地事实。
|
||
- 固定金额测试证明申请金额写入后不会被企微详情、重复回调或终态 Worker 修改;企微表单包含实收金额、可退款区间和申请金额,审批动作不接受金额字段。
|
||
- 状态测试覆盖企微提交中、审批中、通过、拒绝、撤销、删除、通过后撤销、提交失败和结果未知,并证明退款状态、企微状态与处理状态独立。历史已退回只读且旧重提路由不存在。
|
||
- 非代理钱包测试覆盖微信、支付宝、线下和个人资产钱包:企微通过后记录实际退款金额,不调用任何渠道退款或资产钱包回充,不生成对应自动退款流水,但仍执行订单、佣金和套餐整单终结。
|
||
- 代理钱包测试使用真实钱包与版本字段,覆盖按原扣款流水退款、负余额冲减、缺失原扣款流水、重复 Worker、并发 Worker、余额更新后崩溃恢复和唯一退款流水;证明系统不按当前代理关系猜测钱包。
|
||
- 整单语义测试至少包含“申请金额小于订单实收金额”,验证只退申请金额但订单仍标记已退款、该订单全部有效套餐及佣金均终结,且不能再申请剩余差额。
|
||
- 佣金测试覆盖已冻结、解冻中、已发放、已失效和待人工修正;已发放记录全额扣佣金钱包并允许负数,其他未发放状态不动钱包;验证结构化失效字段、唯一回扣流水、同事务 Audit Event 和原发放任务不能复活记录。
|
||
- 套餐测试覆盖订单生成的待生效/生效中等有效主套餐、加油包级联、下一主套餐激活、无下一套餐时停止资产、重复处理、处理中崩溃和状态写入失败恢复;不得使用单个 `package_usage_id` 缩小范围。
|
||
- 局部失败测试依次制造资金失败、佣金中途失败、套餐失效失败和资产状态失败。资金未完成时实际退款金额为空;资金完成后的下游失败保留实际退款金额;修复后重试只补未完成步骤,最终不产生重复资金或审计。
|
||
- 权限测试对同一退款使用本店代理、上级代理、无管理关系代理、具备业务权限平台、仅具备企微运营权限平台和超级管理员读取,验证店铺层级范围及平台业务权限。列表、详情、附件下载和导出必须给出一致投影。
|
||
- 信息投影测试证明代理可见业务凭证、真实提交人、审批状态和处理结果,但看不到审批人、内部意见或审批人附件;有退款业务权限的平台可以看到完整审批详情,无业务权限的平台即使有企微运营权限也不能读取。
|
||
- 可靠性测试覆盖 Outbox 重投、Asynq 重投、租约过期、回调与轮询并发、重复终态、乱序状态、乐观锁冲突和处理成功后再次消费。每个外部终态只触发一次业务处理,完成步骤不重复。
|
||
- 可编程 WeCom Adapter 自动化必须覆盖真实企微难以稳定制造的超时、明确失败、响应丢失、重复/乱序回调、撤销、删除、通过后撤销、并发轮询和限流。加密回调自动化仍从真实 HTTP 回调入口进入,使用测试 Token/AES Key 生成协议密文并验证签名、解密和 CorpID,不能绕过协议直接调用内部同步函数。
|
||
- 真实企微验收是 UR#35 实现完成门禁,不得推迟到 INT-06。真实参数、模板和密钥只通过环境变量或安全配置提供,不写入 Spec、源码、日志或报告;回调按“企业微信 → 用户提供的中转应用 → 本地服务”进入,中转应用原样转发企微查询参数与请求体,后端仍执行完整验签、解密和 CorpID 校验。
|
||
- 真实企微至少执行两张独立退款:其一由代理创建,使用固定企微成员代提交并人工同意,建议选择代理主钱包订单,同时验证真实附件上传、`applyevent`、加密回调、钱包回溯、佣金、套餐、订单和审计;其二由平台账号创建,使用本人绑定企微身份并人工拒绝,验证轮询兜底、退款终结且不触发资金或套餐处理。
|
||
- 真实企微验收采用可复用两阶段流程:阶段一创建唯一隔离夹具并通过真实 HTTP 创建退款,输出运行 ID、退款号与 `sp_no`;人工在企微同意或拒绝;阶段二等待或主动驱动公共同步/轮询和退款 Worker,再自动核对数据库、钱包流水、佣金、套餐、审批实例、Audit Event 与幂等结果,并生成不含敏感信息的通过/失败报告。
|
||
- 真实企微每次不强制人工制造撤销、删除或通过后撤销,这些异常由可编程 Adapter 自动化覆盖。真实验收必须证明同意、拒绝、真实附件、两类发起身份、真实加密回调和回调缺失时的轮询兜底。
|
||
- 前端人工验收覆盖创建表单保留、本人绑定入口、代理代提交、列表和详情三状态分区、代理/平台投影、拒绝后新建、处理失败、通过后撤销高风险告警以及所有加载、空、失败和无权限状态。
|
||
- 完成门禁同时要求:相关 Go 自动化、数据库和真实 S3 测试通过;可编程 Adapter 异常矩阵通过;两条真实企微验收通过;迁移演练、旧路由不存在、生成的 OpenAPI、前端接入和数据核对均通过。INT-06 只做跨需求与前后端复验,不能替代本需求真实企微门禁。
|
||
|
||
## Out of Scope
|
||
|
||
- 不建设本地审批流、审批任务、审批节点配置、审批按钮或本地“待我审批”。
|
||
- 不允许企微审批人修改退款金额,也不保留 `approved_refund_amount` 作为新业务字段。
|
||
- 不提供原退款单编辑、退回修改、重提或拒绝后的复用;再次退款必须新建。
|
||
- 不调用微信、支付宝或其他支付渠道的自动退款 API。
|
||
- 不自动回充个人资产钱包,不把资产钱包退款扩展为资金渠道能力。
|
||
- 不提供非代理钱包的本地人工退款确认按钮或 `manual-complete` 接口。
|
||
- 不自动冲正通过后撤销前已经完成的资金、佣金或套餐动作。
|
||
- 不允许一张订单通过多张退款单拆分退款,也不保留申请金额之外差额的后续退款权利。
|
||
- 不向代理公开审批人、内部意见、审批人附件或企微内部标识。
|
||
- 不在 UR#35 重复实现 UR#37 的连接配置、账号绑定、模板发布、回调协议、轮询调度或异常恢复后台。
|
||
- 不在本需求迁移无关订单、钱包、佣金、套餐或卡状态模块的全部旧代码。
|
||
|
||
## Further Notes
|
||
|
||
- 当前代码仍要求前端提交实收金额和可选套餐使用记录,仍注册本地通过、拒绝、退回与重提接口;实现必须以本 Spec 为准删除这些新流程入口,不能把当前行为当成兼容要求。
|
||
- 当前代理退款查询按创建账号过滤,与已确认的店铺层级查看范围冲突;Query 改造必须覆盖列表、详情、附件和导出,不能只改列表。
|
||
- 当前代理钱包退款在找不到原扣款流水时会按关系猜测钱包,个人资产钱包会自动回充;两条兼容分支都与本 Spec 冲突,必须从新退款终态用例中移除或隔离。
|
||
- 当前佣金和套餐后处理使用进程内 Goroutine,并可能在部分失败后写完成布尔值;这些布尔值只能用于历史对账,不能作为新处理链路的可靠幂等事实。
|
||
- 当前套餐失效能力在不传 `package_usage_id` 时已经能够按订单查找目标并级联主套餐加油包,可作为迁移时的行为参考,但必须纳入可靠 Worker、状态条件、审计和下一套餐/资产状态闭环。
|
||
- UR#37 必须先提供可调用的公共企微契约;UR#44 列表摘要、UR#42 退款附件导出和 UR#57 退款中禁止换货应消费本 Spec 的状态与权限投影,不得自行定义另一套“活跃退款”或审批信息。
|
||
- 本需求同时触及企微、资金、佣金、套餐、权限、迁移和真实环境验收,预计超过一个高质量实现上下文。进入实现前应基于本 Spec 评估并拆分窄的端到端 tracer-bullet tickets;拆分不能按 Model、Service、Handler 和测试做水平切层。
|