更新一下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代理在线充值与员工线下充值审批