更新一下prd

This commit is contained in:
2026-07-22 11:08:04 +09:00
parent 4902a02c87
commit da9c805d89
10 changed files with 1026 additions and 200 deletions

View File

@@ -166,7 +166,7 @@ sequenceDiagram
评审结论:支付方式按**整批统一**设计:
- 页面选择 `offline``agent_wallet`CSV 不再重复填写支付方式。
- 页面选择 `offline``wallet`CSV 不再重复填写支付方式。`wallet` 复用现有后台订单支付枚举,在批量任务中表示逐行扣结算代理主钱包,不新增同义枚举 `agent_wallet`
- 混合支付拆成两个批次,避免一份凭证对应多种支付语义。
- CSV 不包含支付方式列,后端拒绝同一批次混合支付。
@@ -193,9 +193,11 @@ flowchart TD
任务允许部分成功。每一行是独立、可重试、可审计的业务单元,不能只保存一段失败 JSON。
同一 CSV 内按“资产类型 + 标准化资产标识 + 套餐编码”判断重复:首次出现的行正常处理,后续重复行失败并记录首次出现的行号;同一资产订购不同套餐不算重复。本期不把重复行解释为购买多份,未来如有多份订购需求,应增加明确数量字段。
Worker 复用系统统一资产解析能力,把资产标识解析为唯一资产类型和资产 ID再按“资产类型 + 资产 ID”判断重复。一个批次在创建时只选择一个套餐因此重复键不包含套餐同一资产使用不同受支持标识仍是重复首次出现的行正常处理,后续重复行失败并记录首次出现的行号。本期不把重复行解释为购买多份,未来如有多份订购需求,应增加明确数量字段。
本页面和全部批量订购 API 只允许超级管理员,或具备独立“批量订购套餐”权限的平台账号访问;代理和企业账号一律禁止。平台权限不能替代逐行的资产归属、套餐授权、成本价和钱包业务校验
统一资产解析当前支持 ICCID、卡 `virtual_no`、MSISDN、设备 `virtual_no`、IMEI 和 SN未命中或无法唯一解析时只失败该行禁止任意选择资产。明细保存用户原始资产标识、解析后的资产类型/ID和规范标识快照便于审计和跨标识判重
本页面入口沿用前端现有可见性规则;后端不新增批量订购权限码、账号类型拦截或任务创建人隔离,能够通过现有后台认证调用接口的主体视为可以使用。该入口决定不能替代逐行的资产归属、套餐授权、成本价和钱包业务校验。
### 数据库变更
@@ -208,10 +210,15 @@ CREATE TABLE tb_bulk_purchase_task (
creator BIGINT NOT NULL DEFAULT 0,
updater BIGINT NOT NULL DEFAULT 0,
task_no VARCHAR(30) NOT NULL,
request_id VARCHAR(64) NOT NULL,
source_file_key VARCHAR(500) NOT NULL,
operator_id BIGINT NOT NULL,
package_id BIGINT NOT NULL,
package_code_snapshot VARCHAR(50) NOT NULL DEFAULT '',
package_name_snapshot VARCHAR(200) NOT NULL DEFAULT '',
payment_method VARCHAR(20) NOT NULL,
voucher_keys JSONB NOT NULL DEFAULT '[]',
involved_shop_count INT NOT NULL DEFAULT 0,
total_amount BIGINT NOT NULL DEFAULT 0,
total_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
@@ -226,19 +233,22 @@ CREATE UNIQUE INDEX idx_bulk_purchase_task_no
ON tb_bulk_purchase_task(task_no)
WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX idx_bulk_purchase_task_request_id
ON tb_bulk_purchase_task(request_id)
WHERE deleted_at IS NULL;
CREATE TABLE tb_bulk_purchase_item (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
task_id BIGINT NOT NULL,
row_no INT NOT NULL,
asset_type VARCHAR(20) NOT NULL,
asset_identifier VARCHAR(100) NOT NULL,
package_code VARCHAR(50) NOT NULL,
asset_type VARCHAR(20),
input_asset_identifier VARCHAR(100) NOT NULL,
resolved_asset_id BIGINT,
canonical_identifier VARCHAR(100) NOT NULL DEFAULT '',
resolved_shop_id BIGINT,
resolved_shop_name VARCHAR(200) NOT NULL DEFAULT '',
package_name_snapshot VARCHAR(200) NOT NULL DEFAULT '',
package_id BIGINT,
amount BIGINT NOT NULL DEFAULT 0,
status INT NOT NULL DEFAULT 1,
order_id BIGINT,
@@ -260,14 +270,14 @@ CREATE INDEX idx_bulk_purchase_item_task_status
状态建议:
- 任务:`1=待处理, 2=处理中, 3=已完成, 4=部分成功, 5=失败`
- 明细:`1=待处理, 2=处理中, 3=成功, 4=失败`
- 任务统一复用全局异步任务状态`1=待处理, 2=处理中, 3=已完成, 4=已失败, 5=已取消`;逐行明细使用 `1=待处理, 2=处理中, 3=成功, 4=失败`
- 文件有效并完成全部行业务处理后任务为“已完成”;全部成功、部分成功和全部行业务失败由成功数/失败数表达,不建立“部分成功”状态。本期没有取消入口,但保留全局取消状态码
### 处理与幂等
1. 前端通过对象存储预签名地址直传 CSV 和线下凭证,再由 API 校验 `file_key`、支付方式和 `voucher_keys` 并创建任务,返回 `task_id`;业务接口不接收 `shop_id` 或文件字节。
2. Worker 根据 `source_file_key` 下载并解析 UTF-8 CSV允许 BOM将每一行先写入明细表再开始业务处理不接受 Excel。
3. 同一任务按行顺序处理;每行按资产当前归属解析结算代理,同一任务可以依次处理不同代理的钱包。
1. 前端选择单个套餐和整批支付方式,通过对象存储预签名地址直传 CSV 和线下凭证,再由 API 校验 `package_id``file_key`、支付方式和 `voucher_keys` 并创建任务,返回 `task_id`;业务接口不接收 `shop_id` 或文件字节。创建接口校验套餐存在以及对象归属、扩展名/Content-Type 和 10MB 大小,不同步解析 CSV逐行处理时再校验结算代理当前套餐授权、价格和可售规则。
2. Worker 根据 `source_file_key` 下载并解析 UTF-8 CSV允许 BOM校验固定表头、未知列、CSV 语法、空文件和 1000 行上限;任一文件级校验失败时任务整体失败且不创建明细订单。文件有效后将每一行先写入明细表,再开始业务处理;不接受 Excel。
3. 同一任务严格按 CSV 行号顺序处理;每行按资产当前归属解析结算代理,同一任务可以依次处理不同代理的钱包。`wallet` 不预占整批或某一代理全部行的金额,当前行余额不足只失败该行并继续后续行;后续金额较小且当时余额足够的行仍可成功,因此行序就是同一代理资金不足时的订购优先级。
4. 每行使用独立事务。代理钱包支付时锁定该行结算代理的主钱包,按有效信用额度校验总可用金额后,在同一事务扣款、创建订单、资金流水并更新明细;无归属、归属异常、无主钱包或余额不足只失败该行。
5. `idempotency_key` 使用 `bulk_purchase:{task_id}:{row_no}`。Worker 重试时,已存在成功订单的明细直接跳过。
6. 单行失败不回滚已成功行;失败原因写结构化错误码和用户可见中文原因。
@@ -275,19 +285,19 @@ CREATE INDEX idx_bulk_purchase_item_task_status
Asynq 载荷只传任务 ID不传文件字节或临时路径。源文件保留周期按对象存储统一策略处理确保 Worker 重试期间仍可读取。
建议单文件上限 1000 行;超过上限在 API 层拒绝,避免长事务和过长处理时间
单文件上限 1000 行;超过上限由 Worker 将任务标记为整体失败,不创建任何订单
### API 设计
**前端静态 CSV 模板**
模板由前端项目随版本发布,后端不提供下载接口。按已确认的“整批统一支付方式”,模板字段为
模板由前端项目随版本发布,后端不提供下载接口。模板只有一列
```text
资产类型 | 资产标识 | 套餐编码 | 套餐名称
资产标识
```
`套餐名称`用于人工核对,实际匹配以稳定的 `套餐编码` 为准。后端必须校验表头并对未知列给出明确错误,不能依赖模板一定来自当前前端版本。
套餐由创建任务请求中的单个 `package_id` 决定。资产标识统一交给系统资产解析能力识别资产类型和 ID批量模块不维护自己的标识白名单。后端必须校验单列表头并对未知列给出明确错误,不能依赖模板一定来自当前前端版本。
**上传并提交**
@@ -297,13 +307,14 @@ Content-Type: application/json
{
"request_id": "01J...",
"package_id": 1001,
"payment_method": "offline",
"file_key": "bulk-purchase/2026/07/xxx.csv",
"voucher_keys": ["attachment/2026/07/key1"]
}
```
CSV 先调用 `POST /api/admin/storage/upload-url` 并使用独立 `purpose=bulk_purchase` 获取预签名地址后直传;线下凭证使用附件用途直传。`offline``voucher_keys` 必填,`agent_wallet` 时必须为空。创建任务前校验对象存在、归属当前上传主体、CSV 类型和 10MB 大小限制。
CSV 先调用 `POST /api/admin/storage/upload-url` 并使用独立 `purpose=bulk_purchase` 获取预签名地址后直传;线下凭证使用附件用途直传。`package_id` 为大于 0 的单个套餐 ID`offline``voucher_keys` 必填,`wallet` 时必须为空。创建任务前校验套餐存在、对象归属、CSV 类型和 10MB 大小限制;编码、表头、语法、空文件和行数由 Worker 校验
**查询任务状态**
@@ -320,158 +331,84 @@ GET /api/admin/bulk-purchases/{task_id}/items?status=4&page=1&page_size=50
### 前端技术方案
- 页面分为“参数确认 → 文件上传 → 处理中 → 结果”四个稳定步骤,刷新页面后可根据任务 ID 恢复进度。
- 提交前展示支付方式、凭证数量和文件名的二次确认;不展示整批目标代理或单一钱包余额。任务结果按行展示后端解析的结算代理及金额。
- 提交前展示所选单个套餐、支付方式、凭证数量和文件名;不展示整批目标代理或单一钱包余额。任务结果按行展示后端解析的结算代理及金额。
- 任务处理中展示总数、已处理数、成功数和失败数,轮询规则复用统一异步任务方案。
- 结果页默认显示失败明细,可切换全部/成功/失败,并可按资产标识搜索。
- 部分成功使用明确状态,不弹“全部成功”提示;再次上传失败行会创建新任务,不修改旧任务历史。
- 任务状态为已完成后,根据成功数和失败数显示全部成功、部分成功或全部业务行失败摘要;再次上传失败行会创建新任务,不修改旧任务历史。
- 操作员、任务号和处理时间在页面固定展示,便于财务和运营追溯。
### 自动化测试环境隔离
- 已部署测试环境继续使用 Redis DB 6本地开发和 Agent 自动化测试统一使用 Redis DB 7禁止自动化测试向 DB 6 投递 Asynq 任务。
- 测试入口必须读取并校验实际 Redis Client 配置DB 不是 7 时立即失败。普通 Redis Client、Asynq Client 和 Worker Handler 使用同一 DB 配置,不能只隔离普通 Key 而把任务送到 DB 6。
- 不允许对 DB 7 执行 `FLUSHDB`;每次运行使用唯一测试标识,并只清理本次运行实际创建的 Key 和数据。
- 自动化测试直接连接当前真实 S3使用唯一对象 Key 上传 CSV 和凭证,结束时只删除本次创建的对象;不提供内存对象存储替身。
- PostgreSQL 继续使用现有测试库,不新增 Agent 专用库。测试只创建带唯一运行标识的夹具,并按实际记录 ID 精确清理;禁止清表、`TRUNCATE`、模糊删除和修改既有业务数据,并发钱包场景也遵守相同隔离规则。
- Go HTTP 集成测试是主要自动化入口:使用 Fiber `app.Test` 穿过真实认证和业务链路,捕获待投递任务后直接驱动公开 Worker Handler不通过 `sleep` 等待测试环境 Worker 抢取任务。
- 实现时同步保留可复用的 Agent 集成测试规范、环境守卫与精确清理 Harness、典型 CSV `testdata` 和完整示例;真实部署环境另提供 `curl` 冒烟模板,但不以 `curl` 代替自动化断言。
---
## 需求20退款审批
### 依赖
段历史设计基于 [已废弃的本地审批流](../历史方案/本地通用审批流-已废弃方案.md)
需求依赖七月统一企业微信审批公共能力,以稳定场景码 `refund_approval` 复用真实提交人、平台本人绑定、代理固定成员代提交、模板版本、附件上传、加密回调、轮询和终态 Outbox。退款模块不再依赖已废弃的本地审批流也不自行实现审批节点、候选审批人或审批按钮
### 退款单现有状态(不变)
### 创建契约与固定金额
```
1=待审批 2=已通过 3=已拒绝 4=已退回
```
- 保留 `POST /api/admin/refunds`,允许超级管理员、平台和代理发起;企业账号不允许发起。
- 请求只接受 `order_id``requested_refund_amount`、必填且最多 1000 字的 `refund_reason`、可选且最多 500 字的 `remark`,以及 15 个 `{file_key,file_name,file_size}` 附件。
- 不再接受前端提交的 `actual_received_amount``package_usage_id`。后端读取订单实收金额、支付方式、买卖方、资产和店铺事实并固化快照。
- 申请金额校验 `0 < requested_refund_amount <= 订单实收金额`,提交后固定。企微表单只读展示订单实收金额、可退款区间和本次申请金额,审批人只能同意或拒绝,不能修改金额。
- 退款单、唯一企微审批实例和提交 Outbox 同一 PostgreSQL 事务创建;远程企微异步提交,业务接口返回本地退款和“企微提交中”。场景、模板、平台本人绑定或代理固定成员不可用时,在任何业务事实落库前失败。
- 同一订单存在提交中、审批中、已通过但处理未成功或异常人工处置未完成的退款时禁止新建。已拒绝退款不占活跃名额;撤销、删除和通过后撤销不自动放行。
退款单状态值的原有语义不改;审批进度由 `tb_approval_process_instance` 管理,两者通过 `approval_instance_id` 关联。审批通过后,代理钱包退款自动回退到原扣款代理主钱包;微信、支付宝和线下退款由财务人工完成。业务表增加独立处理状态:
### 独立状态与数据事实
```text
processing_status0=待处理 1=处理中 2=已完成 3=处理失败
退款状态1=待审批 2=已通过 3=已拒绝 4=已退回(仅历史) 5=已撤销/审批已删除
处理状态0=未触发 1=处理中 2=处理成功 3=处理失败
```
`status=2` 表示审批结论已通过,`processing_status` 表示实际退款动作是否完成。接口和前端必须同时展示两者
- 企微审批状态独立复用公共枚举:`0=提交中, 1=审批中, 2=已通过, 3=已驳回, 4=已撤销, 5=通过后撤销, 6=已删除, 7=提交失败, 8=提交结果未知`。退款、审批和业务处理状态不得互相覆盖
- 退款记录保存唯一 `approval_instance_id`、真实提交人显示快照、结构化申请附件、申请备注、处理状态/错误/开始与完成时间,以及可靠 Worker 所需的租约或版本事实。
- `actual_received_amount` 继续作为后端生成的订单实收快照;`actual_refund_amount` 只表示资金已经实际完成的金额。旧 `approved_refund_amount``package_usage_id` 和字符串 Key 列表只作历史兼容,新退款不再依赖。
- 非代理钱包在财务已系统外退款并同意企微时写 `actual_refund_amount=申请金额`;代理主钱包只在原钱包回溯与唯一退款流水事务成功后写。驳回、撤销、删除及尚未完成代理回溯时为空;资金完成后即使佣金或套餐失败也保留。
- 佣金记录增加结构化 `invalid_reason``invalid_refund_id``invalidated_at` 和必要的人工操作者事实;已发放佣金回扣以“退款单 + 佣金记录”唯一防重。
### 数据库变更
### 企微终态和资金处理
```sql
ALTER TABLE tb_refund_request
ADD COLUMN approval_instance_id BIGINT,
ADD COLUMN processing_status INT NOT NULL DEFAULT 0,
ADD COLUMN processing_error TEXT NOT NULL DEFAULT '',
ADD COLUMN processing_started_at TIMESTAMPTZ,
ADD COLUMN processing_completed_at TIMESTAMPTZ,
ADD COLUMN manual_refund_operator_id BIGINT,
ADD COLUMN manual_refund_completed_at TIMESTAMPTZ,
ADD COLUMN manual_refund_remark TEXT NOT NULL DEFAULT '',
ADD COLUMN manual_refund_voucher_key JSONB NOT NULL DEFAULT '[]';
- 企微拒绝把退款状态置为已拒绝,处理状态保持未触发;该审批和退款单均终结。若修正问题后仍需退款,重新调用创建接口生成新退款 ID、退款单号、快照、附件和企微实例原单不可编辑或重提。
- 微信、支付宝、线下及其他非代理主钱包支付由财务先在系统外退款,再同意企微;企微同意即代表退款完成。本系统不调用渠道退款 API也不提供 `manual-complete` 二次确认。
- 个人客户资产钱包支付同样走系统外人工退款。本系统不自动回充资产钱包、不创建资产钱包退款流水;现有个人钱包自动回充分支必须从新终态用例移除或隔离。
- 代理主钱包支付在企微同意后只按原订单扣款流水定位原主钱包,增加本次申请金额并写唯一退款流水。余额允许为负,退款自然冲减欠款;缺少可核验原扣款流水时处理失败,禁止根据当前代理关系、买卖方或当前钱包猜测。
- 企微撤销或删除进入异常人工处置,不执行资金动作也不自动放行新退款。通过后撤销且资金尚未执行时阻止后续资金步骤;资金已执行时不自动冲正,保留实际退款金额,记录 `critical` Audit Event 和站内告警。
CREATE INDEX idx_refund_request_approval_instance
ON tb_refund_request(approval_instance_id)
WHERE approval_instance_id IS NOT NULL;
```
### 整单终结、可靠性和审计
`processing_error` 只保存可运维排查的摘要。`manual_refund_*` 只在人工退款确认时写入,退款申请时提交的 `refund_voucher_key` 仍是申请业务资料,不能混用为财务完成凭证
- 无论申请金额是否等于订单实收金额,企微同意后都以整张订单为边界:订单标记已退款,该订单全部有效套餐和佣金资格终结,该订单不允许再申请未退差额。向客户实际退款的金额仍是本次申请金额
- 已冻结、解冻中、尚未发放或待人工修正的佣金不移动钱包,直接改为已失效;已发放佣金先从对应佣金钱包全额扣回,再改为已失效,钱包允许为负。佣金状态、钱包余额/版本、回扣流水和统一 Audit Event 同事务。
- 该订单生成的全部有效套餐失效,不按单个套餐使用记录缩小范围。主套餐失效级联其加油包,然后尝试激活下一条待生效主套餐;没有下一条时通过公共卡/设备状态能力停止资产。
- 企微首次终态通过公共业务 Outbox 触发可靠 Asynq Worker不使用进程内 Goroutine。Worker 通过状态条件和租约领取任务;资金、订单、每条佣金和套餐分别保存持久化幂等事实,局部失败只重试未完成步骤,全部完成后才置处理成功。
- 代理主钱包余额/版本/退款流水与实际退款金额、已发放佣金回扣、订单状态和关键套餐状态分别按最小一致性边界写统一 Audit Event。重复回调、轮询和 Worker 命中已完成事实时不产生第二笔资金、第二次失效或第二条等价审计。
现有退款审批允许确认 `approved_refund_amount`,切换到通用审批后必须保留:配置为最终决策的节点在完成时返回金额动作字段,审批人可确认实际退款金额;省略时使用申请金额。退款动作适配器在审批事务内校验金额大于 0且不超过申请退款金额和订单实收金额并写入退款单和审批操作日志。会签需要指定金额决策人时流程定义增加其专属最终节点禁止第一位会签人预先锁定金额。
### API、权限与前端
### 流程
- 保留 `POST /api/admin/refunds``GET /api/admin/refunds``GET /api/admin/refunds/{id}`。下线并不再注册原 `approve/reject/return/resubmit` 及任何 `manual-complete` 路由。
- 列表默认每页 20、最大 100按创建时间倒序并以 ID 作并列排序键;筛选参数按 AND 组合。详情和列表同时返回退款状态、`approval` 对象和独立处理状态,历史本地审批使用 `approval.source=legacy`,新企微退款使用 `wecom`
- 代理退款查询不再按创建账号隔离,改为既有店铺层级数据范围和退款业务权限。代理可见申请资料、业务凭证、真实提交人、审批状态和处理结果,但看不到审批人、内部意见和审批人附件。平台/超级管理员也必须有退款业务查看权限,才可读取完整企微详情。
- 列表、详情、附件下载和导出复用同一权限投影;企微运营权限不能绕过退款业务权限。
- 创建页不提交实收金额或套餐使用记录,明确提示“申请金额可以小于实收金额,但通过后仍按整单终结”。详情固定分为退款业务信息、企微审批信息和业务处理结果,不显示本地审批、金额修改、重提或人工确认按钮。
- 非代理钱包提示财务先系统外退款再同意企微;代理钱包提示同意后系统自动回溯。处理失败只展示脱敏摘要和系统重试状态;通过后撤销使用高风险告警并说明不会自动冲正。
```mermaid
sequenceDiagram
actor Applicant as 提交人
actor Approver as 审批人
participant Refund as Refund Application
participant Approval as Approval Application
participant DB as PostgreSQL
participant Worker as AgentWalletRefundHandler
actor Finance as 财务人员
### 测试与发布门禁
Applicant->>Refund: POST /api/admin/refunds
Refund->>DB: 同事务创建退款单(status=1)
Refund->>Approval: StartProcess(refund, refund_id)
Approval->>DB: 创建实例、首任务、审批人、Outbox
Refund->>DB: 回写 approval_instance_id
Approver->>Approval: 按 task_id 审批,决策节点可提交实际退款金额
Approval->>DB: 提交 ProcessApproved/Rejected/Returned
alt 代理钱包支付且审批通过
DB-->>Worker: Outbox + Asynq 至少一次投递
Worker->>DB: claim processing_status=1status=2
Worker->>DB: 幂等回退原扣款代理主钱包并写资金流水
Worker->>DB: processing_status=2
else 非代理钱包支付且审批通过
Refund->>DB: status=2, processing_status=0待人工退款
Finance->>Refund: POST manual-complete
Refund->>DB: 条件更新处理状态并记录确认信息
else 审批拒绝
Worker->>DB: status 从 1 更新为 3
else 退回修改
Worker->>DB: status 从 1 更新为 4
end
```
`AgentWalletRefundHandler` 仅处理代理钱包订单,使用 `refund:{refund_id}` 作为业务幂等键,并读取审批事务已经持久化的 `approved_refund_amount`。它必须按原扣款资金流水定位原代理主钱包,余额、版本、钱包退款流水和处理状态在同一事务更新。处理失败时单独更新 `processing_status=3` 和错误摘要后返回可重试错误;不得回滚已经完成的审批实例,也不得重复回退。
处理器通过条件更新领取任务:`processing_status IN (0,3)`,或状态为处理中但 `processing_started_at` 已超过约定租约。重复消费者看到未过期的处理中状态时不重复执行;进程在副作用完成后崩溃时,下一次重试依靠业务幂等键恢复并补写成功状态。
本期不调用第三方退款 API也不增加商户退款号、渠道退款号或渠道结果字段。个人/客户资产钱包退款不属于本期自动回退范围,现有对应分支必须在实施时隔离或拒绝进入本流程。
人工退款确认接口:
```text
POST /api/admin/refunds/{id}/manual-complete
```
请求包含 `request_id`、可选 `remark` 和最多 5 个完成凭证。后端仅允许具备财务确认权限的账号对 `status=2 AND processing_status IN (0,3)` 的非代理钱包退款操作;实际金额沿用审批金额,不允许在确认时再次改价。确认记录操作人、时间、备注和凭证后将处理状态置为已完成。
### 退回后重新提交(已废弃,禁止实施)
> 本节是旧本地审批方案的历史记录。七月最终方案下线该接口;企微拒绝会终结原退款单,后续仍需退款时重新创建新退款单。
```
POST /api/admin/refunds/{id}/resubmit
→ 校验 status=4
→ 请求体可修改 actual_received_amount、requested_refund_amount、refund_voucher_key、refund_reason
→ 在同一事务新建 ProcessInstance
→ 更新 approval_instance_idstatus 回到 1待审批
→ processing_status 重置为 0清空本次处理错误和人工确认记录
→ 旧审批实例保留为历史记录
```
复用当前真实路由 `POST /api/admin/refunds/{id}/resubmit`,不新增单独 `PUT`。重提命令沿用现有 `ResubmitRefundRequest` 字段范围;禁止修改订单 ID、资产快照、提交人或已形成的历史审批记录。
### API 响应与前端
退款列表和详情增加:
```json
{
"approval_instance_id": 1001,
"approval_source": "workflow",
"approval_status": 2,
"approval_status_name": "已通过",
"current_node_name": "",
"processing_status": 0,
"processing_status_name": "待人工退款",
"processing_error": ""
}
```
前端展示规则:
| 审批状态 | 处理状态 | 展示 |
|----------|----------|------|
| 审批中 | 待处理 | 待审批 + 当前节点 |
| 已通过 + 代理钱包 | 处理中 | 审批已通过,代理钱包回退处理中 |
| 已通过 + 非代理钱包 | 待处理 | 审批已通过,待人工退款;财务可确认完成 |
| 已通过 | 已完成 | 退款已完成 |
| 已通过 + 代理钱包 | 处理失败 | 系统重试中;管理员可查看错误摘要 |
| 已拒绝 | 待处理 | 已拒绝 + 原因 |
| 已退回 | 待处理 | 已退回,可编辑并重新提交 |
列表页不直接放固定审批按钮。点击进入详情后,根据审批接口返回的 `available_actions` 渲染通过、驳回和退回操作。
决策节点根据 `action_form` 展示“实际退款金额”输入,默认等于申请金额。审批通过后的非代理钱包退款展示“确认人工退款”入口,仅具备财务确认权限时显示;完成凭证与审批附件分区展示。前端只负责元/分转换和基础格式校验,金额上限以后端在审批事务中的校验为准。
停机发布后,现有按退款业务单 ID 直接通过、驳回或退回的路由不再注册;所有退款审批动作统一操作 `task_id`,避免绕过审批人快照、并发控制和操作日志。
维护窗口内需要为 `status=1 AND approval_instance_id IS NULL` 的存量退款单执行幂等回填,从 `refund_approval` 首节点创建流程实例和任务;历史终态退款不伪造流程实例。
- 自动化最高接缝为 Fiber HTTP 与真实认证,经退款 Application/Domain/Query、GORM、现有测试 PostgreSQL、Outbox 和公开 Worker Handler最终核对钱包、佣金、套餐和统一审计企微网络边界使用可编程 Adapter 覆盖超时、重复、乱序、撤销、删除、通过后撤销和并发重试。
- 本地/Agent 测试强制 Redis DB 7已部署测试环境继续使用 DB 6测试启动前校验普通 Redis、Asynq Client 和 Worker 的实际 DB禁止向 DB 6 投递、禁止 `FLUSHDB`。PostgreSQL 沿用现有测试库并按唯一运行标识精确清理;对象存储使用真实 S3 和唯一 Key不做内存替身。
- 真实企微验收是实现完成门禁,不得推迟到 INT-06。至少执行两张独立退款代理使用固定成员代提交并同意验证真实附件、加密回调及代理钱包/佣金/套餐/审计;平台账号使用本人绑定发起并拒绝,验证轮询兜底且不触发资金处理。
- 回调按“企业微信 → 用户提供的中转应用 → 本地服务”原样转发查询参数和请求体,后端仍完整验签、解密并核对 CorpID。真实参数只通过安全配置提供。撤销、删除和通过后撤销由可编程 Adapter 稳定覆盖,不强制每次真实企微人工制造。
- 停机发布时先具备真实企微公共能力、模板映射、平台绑定和代理固定成员,再迁移待审批退款并启用终态 Worker历史终态保留 `legacy`,历史已退回只读。旧状态不一致记录进入对账/人工处置,迁移脚本不得猜测资金、佣金或套餐已完成。
---
@@ -513,7 +450,7 @@ CREATE INDEX idx_agent_recharge_approval_instance
`rejection_reason` 只保存驳回原因,新增 `return_reason` 保存退回修改原因,禁止复用一个字段导致前端无法区分终止和可重提。
充值处理状态保持`0=未触发, 1=处理中, 2=处理成功, 3=处理失败`。退款的 `0` 已收口为“待处理”,两者不要共用中文状态名称常量
充值与退款处理状态统一为`0=未触发, 1=处理中, 2=处理成功, 3=处理失败`,共用同一组中文状态语义,业务页面再结合审批和支付方式解释当前动作
### 流程