更新一下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

@@ -336,7 +336,7 @@ sequenceDiagram
→ 原子发布新版本并恢复场景
```
退款至少映射店铺、退款单号、申请金额、原因、附件和提交人;线下充值至少映射店铺、充值单号、金额、备注、附件和提交人。后台每 10 分钟验证启用版本,模板不可访问或 fingerprint 变化时暂停场景并发送系统告警。
退款至少映射店铺、退款单号、订单实收金额、可退款区间、固定申请金额、原因、备注、附件和真实业务提交人;线下充值至少映射店铺、充值单号、金额、备注、附件和提交人。退款金额在提交后只读,企微审批人只能同意或拒绝。后台每 10 分钟验证启用版本,模板不可访问或 fingerprint 变化时暂停场景并发送系统告警。
场景处于暂停中、已暂停,或当前模板版本失效时,新的退款/线下充值创建请求必须在写入任何业务单、审批实例或 Outbox 之前失败,并返回明确的“审批场景当前不可用”。前端保留用户已经填写的表单,待场景恢复后由用户重新提交;后端不为失败请求保留待补提的孤儿业务单。暂停前已经成功创建的审批实例继续接收回调、执行 2 分钟兜底同步和终态业务处理,不受场景暂停影响。
@@ -585,7 +585,7 @@ stateDiagram-v2
Submitting --> Processing: 创建任务成功
Submitting --> Editing: 参数或上传失败
Processing --> Processing: 轮询进度
Processing --> Completed: 全部或部分完成
Processing --> Completed: 业务处理完成
Processing --> Failed: 任务失败
Completed --> [*]
Failed --> Editing: 修正后创建新任务
@@ -594,7 +594,8 @@ stateDiagram-v2
- 创建成功后立即请求一次详情,再按 2、3、5 秒退避,最大间隔 10 秒。
- 页面不可见时暂停轮询,恢复可见时立即刷新。
- 网络错误不等于业务失败,展示“状态获取失败,点击重试”。
- 必须展示总数、成功数、失败数和部分成功状态。
- 全局异步任务状态固定为 `1=待处理, 2=处理中, 3=已完成, 4=已失败, 5=已取消`;各业务不得另占状态码或定义“部分成功状态。
- 必须展示总数、成功数和失败数;全部成功、部分成功和全部业务项失败是完成任务的结果摘要,由计数推导。
- 页面刷新后根据任务 ID 恢复进度。
- 批量文件模板由前端静态资源提供;设备批量分配和批量订购均只接受 CSV但各自使用独立表头契约。后端仍严格校验表头、编码、文件大小和内容。
@@ -1217,18 +1218,19 @@ GET /api/admin/shops/fund-summary 返回信用和可用金额
#### 规则和流程
- 支付方式整批选择 `offline` `agent_wallet`CSV 不包含支付方式
- 创建批次时选择单个 `package_id` 和整批支付方式 `offline|wallet`CSV 不包含套餐或支付方式。`wallet` 复用现有后台订单支付枚举,在本批次中表示逐行扣结算代理主钱包,不新增同义枚举 `agent_wallet`
- 混合支付必须拆成不同批次。
- 批次不选择代理、不接收 `shop_id`;同一 CSV 可以包含不同代理的资产。Worker 逐行以资产当前归属解析结算代理,套餐授权、成本价、钱包和数据权限均以该行解析结果为准。
- 创建、查询批次和查看明细只允许超级管理员,或具备独立“批量订购套餐”权限的平台账号;代理和企业账号无页面及 API 权限。平台身份只授予发起批次的能力,不跳过任何逐行资产、结算代理、套餐授权、成本价或钱包校验。
- CSV 固定 UTF-8允许 BOM模板字段:资产类型、资产标识、套餐编码、套餐名称;实际匹配使用套餐编码,套餐名称只供人工核对。不接受 Excel
- 同一 CSV 内按“资产类型 + 标准化资产标识 + 套餐编码”识别重复业务行:首次出现的行正常处理,后续重复行记为失败并返回首次出现的行号;同一资产订购不同套餐不视为重复。本期不通过复制相同行表达购买多份,未来需要多份时增加明确数量字段
- 不新增批量订购后端权限码、账号类型拦截或任务创建人隔离。页面是否展示入口沿用前端现有可见性规则;能够通过现有后台认证调用接口的主体视为可以使用。此决定不跳过逐行资产、结算代理、套餐授权、成本价或钱包校验。
- CSV 固定 UTF-8允许 BOM只有一列表头 `资产标识`,不接受 Excel。Worker 复用系统统一资产解析能力识别资产类型和资产 ID不在批量用例维护标识白名单当前统一能力支持 ICCID、卡 `virtual_no`、MSISDN、设备 `virtual_no`、IMEI 和 SN
- Worker 按统一解析得到的“资产类型 + 资产 ID”识别重复行。一个批次只有一个套餐因此重复键不包含套餐同一资产即使使用不同受支持标识仍属于重复首次出现的行正常处理,后续重复行失败并返回首次行号。本期不通过复制相同行表达购买多份。
- 资产标识未命中或无法唯一解析时只失败对应行,禁止任意选择资产。明细保留用户原始资产标识、解析后的资产类型/ID和规范标识快照便于审计和跨标识判重。
- 任务允许部分成功,每行是独立、可重试、可审计的业务单元。
- 文件最大 10MB、最多 1000 行;失败明细最多保存 1000 条。
- 文件最大 10MB、最多 1000 行;失败明细最多保存 1000 条。创建接口只校验对象存在、属于当前上传主体、扩展名/Content-Type 和 10MB 大小不同步解析文件Worker 校验 UTF-8允许 BOM、固定表头、未知列、CSV 语法、空文件和 1000 行上限。任一文件级校验失败时任务整体失败且不创建订单;资产、套餐、归属、钱包和重复行等业务错误才进入逐行失败并允许部分成功。
```mermaid
flowchart TD
Submit[选择支付方式CSV及凭证直传私有对象存储] --> Task[提交file_key和voucher_keys创建任务]
Submit[选择单个套餐和支付方式CSV及凭证直传私有对象存储] --> Task[提交package_id、file_key和voucher_keys创建任务]
Task --> Parse[解析并持久化逐行明细]
Parse --> Item{处理下一行}
Item -->|钱包| Wallet[锁钱包并校验有效可用金额]
@@ -1245,11 +1247,10 @@ flowchart TD
| 表 | 关键字段 |
|----|----------|
| `tb_bulk_purchase_task` | 任务号、`request_id`、文件 Key、支付方式、凭证快照、涉及代理数、金额和数量汇总、状态、处理租约 |
| `tb_bulk_purchase_item` | 行号、资产、结算代理快照、套餐快照、金额、状态、订单 ID、错误码、错误原因、幂等键 |
| `tb_bulk_purchase_task` | 任务号、`request_id`、文件 Key、所选套餐 ID/编码/名称快照、支付方式、凭证快照、涉及代理数、金额和数量汇总、状态、处理租约 |
| `tb_bulk_purchase_item` | 行号、解析后的资产类型、用户原始资产标识、解析后资产 ID、规范标识快照、结算代理快照、金额、状态、订单 ID、错误码、错误原因、幂等键 |
任务状态:`1=待处理, 2=处理中, 3=完成, 4=部分成功, 5=失败`
明细状态:`1=待处理, 2=处理中, 3=成功, 4=失败`
任务复用全局异步任务状态:`1=待处理, 2=处理中, 3=完成, 4=已失败, 5=已取消`。文件有效且逐行处理结束时任务为已完成,部分成功只通过 `success_count``fail_count` 表达;本期没有取消入口,但保留状态码 5。逐行明细不是独立任务使用 `1=待处理, 2=处理中, 3=成功, 4=失败`
必须具备:
@@ -1259,9 +1260,10 @@ flowchart TD
- 钱包可用金额统一使用 `credit_enabled` 后的有效信用额度,禁止直接无条件加 `credit_limit`
- 钱包余额、版本、订单、资金流水和明细成功状态在同一事务。
- 钱包支付逐行扣该资产当前所属代理的主钱包;资产无代理归属、归属异常或该代理没有有效主钱包时只将该行记为失败。同一批次可依次锁定不同代理钱包,不存在整批共享钱包。
- `wallet` 批次严格按 CSV 行号顺序逐行结算不预占整批或某一代理全部行的金额。当前行余额不足只失败该行并继续处理后续行后续金额较小的行若当时余额足够仍可成功。因此同一代理资金不足时CSV 行序就是订购优先级。
- 单行失败不回滚其他成功行,任务统计从明细表重新聚合。
- 批量订购必须复用需求 15 的套餐可售策略,不能绕过下架续费限制。
- 任务详情和明细只能由上述有权平台主体读取;不得通过猜测 `task_id` 向代理或企业账号暴露跨代理资产、钱包或失败原因
- 任务详情和明细复用现有后台认证及公共数据范围,不新增基于账号类型、权限码或任务创建人的过滤
API
@@ -1272,16 +1274,33 @@ GET /api/admin/bulk-purchases/{task_id}
GET /api/admin/bulk-purchases/{task_id}/items
```
CSV 先使用 `purpose=bulk_purchase` 获取预签名地址并直传私有对象存储;线下凭证使用附件用途直传。`POST /api/admin/bulk-purchases` 只接收 JSON`request_id``payment_method``file_key``voucher_keys`,不接收 `shop_id`、multipart 或文件字节。创建任务前校验对象存在、归属当前上传主体、扩展名/Content-Type 和大小;Worker 按稳定 `file_key` 下载解析
CSV 先使用 `purpose=bulk_purchase` 获取预签名地址并直传私有对象存储;线下凭证使用附件用途直传。`POST /api/admin/bulk-purchases` 只接收 JSON`request_id`单个 `package_id``payment_method``file_key``voucher_keys`,不接收 `shop_id`、multipart 或文件字节。创建任务前校验套餐存在以及对象归属、类型和 10MB 大小并立即返回任务;逐行按结算代理的当前套餐授权、价格和可售规则再次校验。Worker 按稳定 `file_key` 下载并执行文件级解析校验
线下凭证仅作为本批次业务资料和审计快照,本期不校验跨批次唯一性或建立财务核销规则。
#### 自动化测试环境隔离
- 已部署测试环境的 API/Worker 使用 Redis DB 6本地开发和 Agent 自动化测试统一使用 Redis DB 7。自动化测试禁止向 DB 6 写入普通 Redis Key 或 Asynq 任务。
- 测试入口必须在启动前校验实际生效的 Redis DB只有 DB 7 才允许继续;不能只依赖调用方记得覆盖环境变量。当前 Redis 客户端、Asynq Client 和 Worker Server均从同一 `Redis.DB` 配置取值,测试必须保持三者一致。
- DB 7 也可能被多个本地进程共用,测试不得执行 `FLUSHDB`;测试数据、业务键和清理由每次运行的唯一标识及实际创建 ID 限定。
- 自动化测试直接使用当前可调用的真实 S3不建立内存对象存储替身每次运行使用唯一对象 Key并只删除本次创建的测试对象。真实上传、下载和删除失败都必须作为测试失败暴露。
- PostgreSQL 继续使用现有测试库,不新增专用数据库。测试夹具必须带唯一运行标识并按实际创建记录 ID 精确清理,禁止 `TRUNCATE`、清整表、模糊条件删除或修改既有业务数据;并发钱包测试也在该测试库内使用隔离夹具执行。
- 自动化以 Go HTTP 集成测试为主:通过 Fiber `app.Test` 穿过真实路由、认证、Handler、Application/Domain、GORM 和统一响应;测试接缝捕获待投递任务后直接调用公开 Worker Handler不依赖 `sleep` 等待后台 Worker生产仍使用真实 Asynq。
- 本需求实现时必须沉淀可供后续 Agent 复用的集成测试规范、环境守卫与资源清理 Harness、典型 CSV `testdata` 和完整示例;另提供真实部署环境的 `curl` 冒烟模板,但 `curl` 不替代 Go 自动化测试。
### 6.5 需求 20退款审批
退款金额在申请时完成合法性校验并固定,企微审批不允许修改金额。退款凭证先保存本地对象存储,再上传企微副本;审批人可在企微看到业务字段、提交备注和附件
退款创建只接收 `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 <= 订单实收金额`,提交后固定。企微展示订单实收金额、可退款区间和申请金额,审批人只能同意或拒绝,不能修改金额。退款凭证先保存本地对象存储,再上传企微副本;本地对象 Key 是权威资料,企微 `media_id` 只是临时审批副本。
退款状态在历史枚举基础上追加 `5=已撤销/审批已删除`,保留历史 `4=已退回` 语义但新企微审批不再产生退回状态。独立保存业务处理结果,避免“企微已通过但代理钱包回溯或资产处理失败”被展示为全部完成。
```text
退款状态1=待审批 2=已通过 3=已拒绝 4=已退回(仅历史) 5=已撤销/审批已删除
处理状态0=未触发 1=处理中 2=处理成功 3=处理失败
```
```mermaid
sequenceDiagram
actor User as 平台员工/财务
@@ -1294,25 +1313,34 @@ sequenceDiagram
User->>Refund: 创建退款申请
Refund->>DB: 保存退款单、业务快照和企微实例
Refund-->>WeCom: 异步提交申请和附件
User->>WeCom: 审批;非代理钱包先完成人工退款
User->>WeCom: 非代理钱包先人工退款;审批只能同意或拒绝
WeCom-->>Sync: 回调/轮询同步终态
Sync->>DB: 保存审批详情并写终态Outbox
DB-->>Worker: 首次通过时处理业务终态
Worker->>DB: 代理钱包回溯、订单/佣金/资产处理
Worker->>DB: 资金确认、订单/佣金/套餐整单终结
```
关键规则:
- 微信、支付宝、线下等非代理钱包支付由财务在系统外人工退款后再通过企微;企微通过即表示人工退款已确认,本系统不调用渠道退款 API也不再提供本地 `manual-complete` 二次确认
- 代理钱包支付订单在企微通过后按原扣款流水定位原代理主钱包,幂等回溯并写退款流水;可自然冲减负余额
- 个人客户资产钱包不自动回款,现有 `BuyerTypePersonal` 自动资产钱包退款分支在迁移时删除或隔离。
- 无论申请金额是否等于订单实收金额,企微同意后的业务处理都按整张订单终结:订单标记已退款、该订单产生的全部有效套餐失效、全部佣金失效,且该订单不能再申请剩余差额。实际向客户退款的金额仍为本次申请金额
- 微信、支付宝、线下及其他非代理钱包支付由财务在系统外人工退款后再通过企微;企微通过即表示人工退款已确认。本系统不调用渠道退款 API也不提供本地 `manual-complete` 二次确认
- 个人客户资产钱包支付同样由财务系统外退款,本系统不自动回充资产钱包、不写资产钱包退款流水;现有 `BuyerTypePersonal` 自动资产钱包退款分支在迁移时删除或隔离。
- 代理主钱包支付订单在企微通过后必须按原扣款流水定位原代理主钱包,幂等回溯并写唯一退款流水;可自然冲减负余额。缺失原扣款流水时处理失败,禁止按当前店铺关系或买卖方猜测钱包。
- `actual_refund_amount` 只表示资金已经完成:非代理钱包在企微同意时写申请金额;代理主钱包在余额和退款流水事务成功时写申请金额。驳回、撤销、删除及代理钱包尚未回溯时为空;资金完成但佣金或套餐失败时保留。旧 `approved_refund_amount` 仅作历史兼容,不再是新业务概念。
- 已冻结、解冻中、未发放或待人工修正的佣金直接失效;已发放佣金先从对应佣金钱包全额扣回,再失效,钱包允许为负。佣金保存结构化 `invalid_reason``invalid_refund_id``invalidated_at`,每条已发放佣金以退款单和佣金记录组成防重键;状态、钱包、回扣流水和 Audit Event 同事务。
- 该订单产生的全部有效套餐失效;主套餐失效级联加油包,随后尝试激活下一条待生效主套餐,没有下一条时通过公共卡/设备状态能力停止资产。
- 驳回时本地状态改为已拒绝,审批和该退款单同时终结;原退款单不可编辑、不可再次提交。业务人员纠正驳回原因后仍需退款时,重新走 `POST /api/admin/refunds` 创建新退款单。撤销/删除时进入异常状态和人工处置,同样不开放原单重新提交。
- 通过后撤销且资金已执行时不自动冲正,记录 `critical` 审计、站内告警并人工处理;资金尚未执行时终止后续任务。
- 原退款业务单级 `approve/reject/return` 路由下线,不存在本地审批动作 API。
- 原进程内佣金和套餐 Goroutine 改为 Outbox + 可靠 Worker。资金、佣金、套餐、资产状态各自保存持久化幂等事实局部失败只重试未完成步骤全部完成后才把处理状态置为成功。
一张退款单只对应一条企微审批申请。企微同意或拒绝后,该审批申请和退款单均形成不可变终态;本期下线既有 `POST /api/admin/refunds/{id}/resubmit`,不提供任何原退款单编辑或重提接口。拒绝后再次退款属于新的业务事实:前端重新进入退款创建流程,用户根据拒绝原因重新填写金额、凭证和原因,后端生成新的退款 ID、退款单号、业务快照、提交人快照和企微审批申请。新旧退款单只因指向同一订单或资产而具有关联不继承审批节点、意见、附件、状态或企微发起身份已拒绝退款不计入该订单或资产的活跃退款但仍须阻止与其他活跃退款并存。企微意外返回撤销、删除或通过后撤销时只进入异常处置不作为创建新退款的自动放行依据。
前端只读展示企微审批状态、意见/附件和业务处理结果,不显示本地审批或人工退款确认按钮
代理退款查询不再按创建账号隔离,改为既有店铺层级和退款业务权限范围;代理可看业务凭证、真实提交人、审批状态和处理结果,但看不到审批人、内部意见和审批人附件。平台和超级管理员也必须具备退款业务查看权限才可读取完整审批详情。列表、详情、附件下载和导出复用同一权限投影
前端只读展示退款状态、企微审批状态和业务处理结果,不显示本地审批、金额修改、重提或人工退款确认按钮。详情固定分为退款业务信息、企微审批信息和业务处理结果;处理失败展示脱敏错误摘要及系统重试状态。
本需求实现完成必须同时通过两类门禁:可编程 WeCom Adapter 的可重复自动化,以及真实企微验收。真实企微至少完成“代理固定成员代提交并同意”和“平台本人绑定提交并拒绝”两张独立退款,覆盖真实附件、`applyevent`、加密回调、轮询兜底及本地资金/佣金/套餐/审计核对。回调按“企业微信 → 用户提供的中转应用 → 本地服务”原样转发,后端仍完整验签解密;真实参数只通过安全配置提供。撤销、删除和通过后撤销由 Adapter 自动化稳定覆盖,不强制每次真实企微人工制造。
### 6.6 需求 21代理在线充值与员工线下充值审批

View File

@@ -57,7 +57,7 @@ FE/BE 研发需求开发完成
| #38 不同渠道额度处理 | `[FE][UR#38] 角色默认信用和店铺实际额度管理` | `[BE][UR#38] 代理主钱包信用额度与并发资金不变量` | INT-05 钱包支付 |
| #37 审核流转 | `[FE][UR#37] 企微审批状态、意见附件和结果通知展示` | `[BE][UR#37] 企微审批模板、账号绑定、回调和轮询补偿` | INT-06 企微审批 |
| #36 批量订购套餐 | `[FE][UR#36] 批量订购上传、支付方式、进度和失败明细` | `[BE][UR#36] 批量订购任务、逐行幂等下单和钱包扣款` | INT-04 批量与导出、INT-05 钱包支付 |
| #35 退款审核 | `[FE][UR#35] 退款企微审批详情与业务处理状态` | `[BE][UR#35] 退款企微终态、人工退款和代理钱包回溯` | INT-06 企微审批 |
| #35 退款审核 | `[FE][UR#35] 退款企微审批详情与业务处理状态` | `[BE][UR#35] 退款企微终态与整单退款终结` | INT-06 企微审批 |
| #34 充值审核流程 | `[FE][UR#34] 代理扫码充值与员工线下审批状态页面` | `[BE][UR#34] 微信/支付宝充值入账和线下充值企微终态` | INT-05 钱包支付、INT-06 企微审批 |
| #33 套餐临期提醒 | `[FE][UR#33] 临期列表、各端高亮、3天置顶和续费入口` | `[BE][UR#33] 最终到期临期Query与15/7/3站内通知` | INT-03 套餐生命周期 |
@@ -112,8 +112,8 @@ FE/BE 研发需求开发完成
| #40 下架套餐续费 | **标题:**C端当前套餐续费入口。<br>**页面:**当前套餐旁显示续费;下架套餐不出现在新购列表。 | **标题:**下架套餐续费资格。<br>**接口:**`GET /api/c/v1/asset/packages` 返回 `can_purchase/purchase_mode/disabled_reason``POST /api/c/v1/orders/create` 强校验资产所有人和历史使用记录。 |
| #38 代理信用额度 | **标题:**角色默认信用和店铺实际额度管理。<br>**页面:**客户角色配置新建默认额度并提示不影响存量;店铺资金页单独调整实际额度。 | **标题:**代理主钱包信用额度。<br>**接口:**`PUT /api/admin/roles/{id}/default-credit``PUT /api/admin/shops/{id}/credit-limit``GET /api/admin/shops/fund-summary`。<br>**入参:**开关、额度、钱包版本。<br>**返回:**额度、可用金额、欠款和版本。 |
| #37 企微审核流转 | **标题:**企微配置、账号绑定和审批详情。<br>**页面:**企微配置页、个人扫码绑定、审批运行列表;业务详情只读展示意见和附件。 | **标题:**企业微信审批接入。<br>**接口:**`GET /api/admin/wecom/status``POST /api/admin/wecom/account-binding/sessions``GET /api/admin/wecom/approvals``POST /api/admin/wecom/approvals/{id}/sync`。<br>**业务详情:**统一返回 `approval` 对象。 |
| #36 批量订购套餐 | **标题:**批量订购上传、支付、进度和失败明细。<br>**页面:**仅超管/具备独立权限的平台账号可用;选择整批支付方式CSV及线下凭证先直传对象存储不选择代理同一CSV可含不同代理资产不接受Excel。 | **标题:**批量订购任务。<br>**接口:**`POST /api/admin/storage/upload-url``POST /api/admin/bulk-purchases``GET /api/admin/bulk-purchases/{task_id}``GET /api/admin/bulk-purchases/{task_id}/items`。<br>**权限:**超管或独立“批量订购套餐”权限;代理/企业拒绝。<br>**入参:**支付方式`file_key``voucher_keys`,无`shop_id`。<br>**判重**同文件相同资产类型、标准化标识和套餐编码仅首行处理,后续行失败。<br>**返回:**任务和逐行结算代理结果。 |
| #35 退款审核 | **标题:**退款企微审批和退款处理状态。<br>**页面:**创建时上传备注附件;详情展示审批、人工退款说明和业务处理结果;拒绝后重新退款必须新建退款单。 | **标题:**退款企微终态处理。<br>**接口:**`POST /api/admin/refunds``GET /api/admin/refunds/{id}`;下线原 `approve/reject/return/resubmit`。<br>**返回**退款数据、`approval``processing_status`。一单一审批,拒绝终结原单;代理钱包通过后幂等回溯。 |
| #36 批量订购套餐 | **标题:**批量订购上传、支付、进度和失败明细。<br>**页面:**沿用现有前端入口可见性;选择单个套餐和整批支付方式CSV及线下凭证先直传对象存储不选择代理同一CSV可含不同代理资产不接受Excel。 | **标题:**批量订购任务。<br>**接口:**`POST /api/admin/storage/upload-url``POST /api/admin/bulk-purchases``GET /api/admin/bulk-purchases/{task_id}``GET /api/admin/bulk-purchases/{task_id}/items`。<br>**权限:**后端仅复用现有后台认证,不新增权限码、账号类型拦截或任务创建人隔离。<br>**入参:**单个`package_id``payment_method=wallet|offline``file_key``voucher_keys`,无`shop_id``wallet`按CSV行号逐行扣结算代理主钱包余额不足只失败当前行并继续。<br>**文件**CSV仅一列“资产标识”创建接口校验套餐和对象/类型/10MBWorker校验编码、表头、语法、空文件及1000行文件级错误整批失败且不下单。<br>**资产:**复用统一资产解析能力识别类型和ID当前支持ICCID、卡`virtual_no`、MSISDN、设备`virtual_no`、IMEI和SN。<br>**判重:**按解析后的资产类型和资产ID判重同一资产不同标识也仅首行处理。<br>**状态:**统一五态,部分成功由计数表达。<br>**返回:**任务和逐行结算代理结果。 |
| #35 退款审核 | **标题:**退款企微审批和整单处理状态。<br>**页面:**创建只提交订单、固定申请金额、必填原因、可选备注及15个结构化附件不提交实收金额或套餐使用记录详情分区展示退款、审批和处理结果;拒绝后重新退款必须新建退款单;代理隐藏审批人、内部意见和审批附件。 | **标题:**退款企微终态与整单终结。<br>**接口:**`POST /api/admin/refunds``GET /api/admin/refunds``GET /api/admin/refunds/{id}`;下线原 `approve/reject/return/resubmit` 及任何 `manual-complete`。<br>**规则**后端读取实收并校验固定申请金额;无论是否全额,通过后订单、该订单套餐和佣金均整单终结且不可再退差额。只有代理钱包按原扣款流水自动回溯;其他方式系统外退款,资产钱包不回充。佣金结构化失效并可靠回扣,处理状态独立且可重试。代理按店铺层级权限查看。<br>**门禁:**可编程Adapter自动化和真实企微代理同意/平台拒绝两条链路均必过。 |
| #34 充值审核流程 | **标题:**代理扫码充值与员工线下充值审批。<br>**页面:**在线充值展示支付方式、二维码和支付状态;线下充值展示只读企微审批状态。 | **标题:**代理在线充值和员工线下审批。<br>**接口:**`GET /api/admin/agent-recharges/payment-methods``POST /api/admin/agent-recharges``GET /api/admin/agent-recharges/{id}/payment-status``GET /api/admin/agent-recharges/{id}`。<br>**返回:**二维码、过期时间、支付/审批/入账状态。 |
| #33 套餐临期提醒 | **标题:**临期列表、各端高亮和续费入口。<br>**页面:**临期页3天内置顶普通资产列表只高亮代理首页显示数量C端显示续费按钮。 | **标题:**预计最终到期临期Query和站内通知。<br>**接口:**`GET /api/admin/expiring-assets`、资产列表/详情增加临期字段、`GET /api/c/v1/asset/info`、通知接口。<br>**返回:**最终到期、剩余天数、颜色节点15/7/3天通知防重。 |

View File

@@ -1345,21 +1345,23 @@
页面入口:批量订购套餐页或现有订单页批量入口。
权限:仅超级管理员或具备独立“批量订购套餐”权限的平台账号展示入口并调用创建、任务详情和明细接口;代理和企业账号不展示入口,后端仍必须独立拒绝
入口:页面是否展示沿用前端现有可见性规则;后端不新增批量订购权限码或账号类型拦截,调用复用现有后台认证
页面结构:
1. 选择整批支付方式线下或代理钱包并上传CSV不选择代理线下支付时上传整批凭证不接受Excel。
2. CSV模板下载使用前端静态文件编码为UTF-8并允许BOM。
1. 选择一个套餐和整批支付方式线下或代理钱包并上传CSV不选择代理线下支付时上传整批凭证不接受Excel。
2. CSV模板下载使用前端静态文件编码为UTF-8并允许BOM,唯一表头为“资产标识”
3. 创建后展示任务号、状态、总数、成功数、失败数、金额汇总和失败明细表。
4. 失败明细包含行号、资产、套餐编码、错误原因支持按任务ID恢复页面。
4. 失败明细包含行号、原始/规范资产标识和错误原因支持按任务ID恢复页面。资产类型由统一资产解析能力识别当前支持ICCID、卡virtual_no、MSISDN、设备virtual_no、IMEI和SN前端不要求用户填写资产类型。
接口约定:
- CSV先调用POST /api/admin/storage/upload-urlpurpose=bulk_purchase使用返回的upload_url直传后取得file_key线下凭证按附件用途直传取得voucher_keys。
- POST /api/admin/bulk-purchases只接收JSONrequest_id、payment_method、file_key、voucher_keys不接收shop_id、multipart或文件字节。
- POST /api/admin/bulk-purchases只接收JSONrequest_id、单个package_id、payment_method、file_key、voucher_keys不接收shop_id、multipart或文件字节。
- GET /api/admin/bulk-purchases/{task_id} 返回任务汇总。
- GET /api/admin/bulk-purchases/{task_id}/items?page=&size=&status= 返回逐行结果。
- GET /api/admin/bulk-purchases/{task_id}/items?page=&page_size=&status= 返回逐行结果。
交互规则一个批次不能混合支付方式同一CSV允许不同代理资产代理归属由后端逐行解析前端不提交或猜测部分成功视为任务终态;某代理钱包余额不足只影响使用该钱包的相应行,不回滚已成功行。
交互规则:一个批次固定一个套餐且不能混合支付方式同一CSV允许不同代理资产代理归属由后端逐行解析前端不提交或猜测任务状态统一为1待处理、2处理中、3已完成、4已失败、5已取消部分成功只由成功数/失败数表达wallet批次严格按CSV行号逐行结算行序就是同一代理余额不足时的订购优先级当前行余额不足只失败该行并继续尝试后续行不预占或回滚该代理全部行。
文件级错误异步展示创建接口通过后不代表CSV内容有效Worker发现编码、表头、未知列、CSV语法、空文件或超过1000行时任务整体失败且不会创建任何订单。资产、套餐、归属、钱包和重复行错误才展示为逐行失败。MSISDN未命中或命中多张卡时只失败该行前端展示后端原因。
完成标准:上传、进度恢复、部分成功、失败筛选和凭证展示完整。
```
@@ -1381,11 +1383,13 @@
接口POST /api/admin/bulk-purchasesGET /api/admin/bulk-purchases/{task_id}GET /api/admin/bulk-purchases/{task_id}/items。
权限仅超级管理员或具备独立“批量订购套餐”权限的平台账号可创建和读取任务代理、企业账号一律拒绝。规则整批选择offline或agent_wallet不接收shop_id同一CSV可以包含不同代理资产逐行以资产当前归属解析结算代理CSV和凭证先直传私有对象存储业务接口只接收稳定file_key/voucher_keys仅接受UTF-8 CSV允许BOM不接受Excel模板按套餐编码匹配文件最大10MB、最多1000行;同一文件按“资产类型+标准化资产标识+套餐编码”判重,首行正常处理、后续重复行失败并指出首行号,同一资产的不同套餐不算重复request_id唯一返回原任务行幂等键为bulk_purchase:{task_id}:{row_no}。
入口与规则后端不新增批量订购权限码、账号类型拦截或任务创建人隔离复用现有后台认证创建任务选择单个package_id和整批offline|walletwallet表示逐行扣结算代理主钱包不新增agent_wallet不接收shop_id同一CSV可以包含不同代理资产逐行以资产当前归属解析结算代理CSV只有“资产标识”一列资产类型和ID由系统统一资产解析能力识别当前支持ICCID、卡virtual_no、MSISDN、设备virtual_no、IMEI和SN批量模块不另写识别规则CSV和凭证先直传私有对象存储业务接口只接收稳定file_key/voucher_keys创建接口校验套餐存在以及对象、上传归属、类型和10MB大小Worker校验UTF-8允许BOM、单列表头、未知列、CSV语法、空文件和1000行上限文件级错误使任务失败且不创建订单不接受Excel标识未命中或无法唯一解析只失败该行;同一文件按解析后的“资产类型+资产ID”判重同一资产使用不同标识仍只处理首行后续失败并指出首行号request_id唯一返回原任务行幂等键为bulk_purchase:{task_id}:{row_no}。任务统一状态为1待处理、2处理中、3已完成、4已失败、5已取消部分成功只由success_count/fail_count表达逐行明细状态为1待处理、2处理中、3成功、4失败。
钱包行事务:锁该行资产所属代理的主钱包,按信用不变量校验,订单、扣款、流水、结算代理快照和明细成功状态同事务;无代理归属、无有效主钱包或余额不足只失败对应行,单行失败不回滚其他行;任务统计从明细重新聚合。线下凭证只做本批资料,不校验跨批唯一。
钱包行事务:严格按CSV行号逐行处理并锁该行资产所属代理的主钱包,按信用不变量校验,订单、扣款、流水、结算代理快照和明细成功状态同事务;不预占整批或某一代理全部行金额,无代理归属、无有效主钱包或余额不足只失败当前行并继续后续行,后续较小金额若余额足够仍可成功;任务统计从明细重新聚合。线下凭证只做本批资料,不校验跨批唯一。
完成标准Worker处理租约、重复消费、进程中断恢复和部分成功均不产生重复订单或重复扣款。
测试环境约定已部署测试环境使用Redis DB 6本地开发和Agent自动化测试强制使用DB 7测试启动时必须校验实际生效DB并在不是7时失败禁止向DB 6投递任务也禁止对DB 7执行FLUSHDB。自动化测试直连当前真实S3并按唯一Key精确清理不做内存替身PostgreSQL沿用现有测试库夹具按唯一运行标识和实际记录ID精确清理禁止TRUNCATE、清表、模糊删除或修改既有业务数据。Go HTTP集成测试使用Fiber app.Test穿过真实认证和业务链路捕获任务后直接调用公开Worker Handler不通过sleep等待后台Worker实现时沉淀可复用的测试规范、环境守卫/清理Harness、CSV testdata和完整示例并提供只用于部署冒烟的curl模板。
```
## UR#35 退款审核
@@ -1408,21 +1412,21 @@
页面入口:退款创建、退款列表、退款详情。
页面结构:
1. 创建表单包含退款金额、原因、备注和附件;金额提交后企微审批不可修改
1. 创建表单包含订单、退款金额、必填原因、可选备注和15个附件附件提交file_key、file_name、file_size金额提交后企微审批不可修改前端不提交实收金额或套餐使用记录
2. 详情分为退款业务信息、企微审批信息、业务处理结果三个区域。
3. 审批区展示sp_no、状态、申请人、审批人、意见、附件和时间线。
3. 审批区按主体权限展示:代理只见真实申请人、审批状态和业务处理结果;平台/超级管理员具备退款查看权限时才展示sp_no、审批人、意见、审批附件和时间线。
4. 处理区展示processing_status、失败摘要和“系统重试中/联系管理员”。
5. 不显示本地通过、驳回、退回或人工退款确认按钮。
接口约定:
- POST /api/admin/refunds 创建退款。
- POST /api/admin/refunds 创建退款请求只包含order_id、requested_refund_amount、refund_reason、remark和attachments
- GET /api/admin/refunds/{id} 返回退款数据、approval对象和processing_status。
- attachments使用现有对象存储上传结果提交结构为 {file_key,file_name,file_size}[]。
- approval结构至少包含 source、sp_no、status、status_name、template_version、applicant、approvers、comments、attachments、timeline、business_process_result。
交互规则非代理钱包由财务在系统外人工退款后再在企微通过。企微拒绝后当前退款单终结详情只读且不提供编辑或重提业务人员处理拒绝原因后仍需退款时重新进入创建退款流程并填写金额、凭证和原因成功后展示新的退款ID、退款单号和审批信息。
完成标准:审批中、通过处理中、处理成功、驳回、撤销和通过后撤销异常状态均展示明确。
完成标准:审批中、通过处理中、处理成功、处理失败、驳回、撤销和通过后撤销异常状态均展示明确;申请金额小于实收金额时明确提示通过后仍按整单终结
```
### 后端研发需求
@@ -1430,26 +1434,30 @@
**标题**
```text
[BE][UR#35] 退款企微终态与人工退款处理
[BE][UR#35] 退款企微终态与整单退款终结
```
**描述**
```markdown
目标:退款通过企微审批驱动业务终态,系统只自动回溯代理主钱包支付。
目标:退款通过企微审批驱动整单业务终态,系统只自动回溯代理主钱包支付。
预计工时后端45小时。
接口POST /api/admin/refundsGET /api/admin/refunds/{id}下线原approve/reject/return/resubmit路由。
接口POST /api/admin/refundsGET /api/admin/refundsGET /api/admin/refunds/{id}下线原approve/reject/return/resubmit及任何manual-complete路由。
规则:
1. 非代理钱包支付由财务人工退款企微通过代表人工退款已确认不调用渠道退款API
2. 代理钱包订单通过后按原扣款流水幂等回溯原代理主钱包并写退款流水
3. 个人客户或资产钱包不自动回款
4. 驳回更新为已拒绝;撤销/删除更新为已撤销通过后撤销且资金已执行不自动冲正记录critical审计
5. 一张退款单只创建一条企微审批申请业务表保存唯一approval_instance_id并由(biz_type,biz_id)唯一约束防重。正常结果只有同意或拒绝任一结果产生后审批与退款单同时完结拒绝后原退款单不可修改、不可重提。若仍需退款必须重新调用创建接口生成新的退款ID、退款单号、业务快照、提交人快照和企微审批旧单只保留为历史事实已拒绝退款不阻止新建但仍需阻止存在其他活跃退款时重复创建。撤销、删除或通过后撤销只作外部异常处置不作为自动放行新退款的依据
1. 创建请求只接受order_id、requested_refund_amount、必填refund_reason、可选remark和15个结构化attachments后端读取订单实收金额并校验0<申请金额<=实收金额不接受actual_received_amount或package_usage_id
2. 申请金额在企微只读,审批人只能同意或拒绝。无论申请金额是否等于实收金额,通过后都把订单标记已退款、该订单全部有效套餐失效、全部佣金失效,并禁止再退剩余差额
3. 非代理主钱包支付由财务系统外退款企微通过代表人工退款已确认不调用渠道退款API、不回充个人资产钱包也不再二次人工确认actual_refund_amount在资金已完成时写申请金额
4. 代理钱包订单通过后只按原扣款流水幂等回溯原代理主钱包并写唯一退款流水;缺少原流水则失败,不按当前关系猜测钱包
5. 已发放佣金全额从佣金钱包扣回并允许负余额,其他未发放佣金直接失效;佣金记录保存结构化失效原因、退款引用和失效时间。订单套餐按整单失效,主套餐级联加油包并尝试下一排队主套餐
6. 驳回更新为已拒绝;撤销/删除更新为已撤销通过后撤销且资金已执行不自动冲正记录critical审计和站内告警。
7. 一张退款单只创建一条企微审批申请业务表保存唯一approval_instance_id并由(biz_type,biz_id)唯一约束防重。正常结果只有同意或拒绝任一结果产生后审批与退款单同时完结拒绝后原退款单不可修改、不可重提。若仍需退款必须重新调用创建接口生成新的退款ID、退款单号、业务快照、提交人快照和企微审批旧单只保留为历史事实已拒绝退款不阻止新建但仍需阻止存在其他活跃退款时重复创建。撤销、删除或通过后撤销只作外部异常处置不作为自动放行新退款的依据。
8. 代理按既有店铺层级和退款业务权限查看本店及可管理下级退款不再按creator隔离代理不见审批人、内部意见或审批人附件平台/超级管理员也须有退款业务查看权限。
9. 企微终态通过Outbox和可靠Worker处理processing_status固定0未触发、1处理中、2处理成功、3处理失败局部失败可安全重试不使用进程内Goroutine。
完成标准:审批状态与业务处理状态分离,重复终态不重复回款,失败任务可可靠重试。
完成标准:审批状态与业务处理状态分离,重复终态不重复回款/扣佣/失效套餐,失败任务可可靠重试实现期必须通过可编程Adapter自动化和真实企微两张独立退款验收代理代提交同意、平台本人提交拒绝INT-06不能替代
```
## UR#34 充值审核流程
@@ -1596,7 +1604,7 @@
交付内容:
1. 统一加载、空数据、权限不足、接口失败和重试状态。
2. 统一异步任务进度结构:任务状态、总数、成功数、失败数、部分成功和失败明细
2. 统一异步任务进度结构:状态固定为1待处理、2处理中、3已完成、4已失败、5已取消总数、成功数、失败数和失败明细独立返回部分成功只由计数表达不占状态码
3. 创建任务后按2秒、3秒、5秒递增轮询最大间隔10秒页面不可见暂停恢复后立即刷新。
4. 页面刷新后通过task_id恢复任务详情。

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=处理失败`,共用同一组中文状态语义,业务页面再结合审批和支付方式解释当前动作
### 流程