更新一下prd
This commit is contained in:
@@ -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`,以及 1~5 个 `{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_status:0=待处理 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=1,status=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_id,status 回到 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=处理失败`,共用同一组中文状态语义,业务页面再结合审批和支付方式解释当前动作。
|
||||
|
||||
### 流程
|
||||
|
||||
|
||||
Reference in New Issue
Block a user