588 lines
28 KiB
Markdown
588 lines
28 KiB
Markdown
# 需求15/16/18/19/20/21 技术方案
|
||
|
||
> 状态:原需求来源稿;其中本地审批流、审批页面和操作密码方案已废弃,最终以企微审批方案和标准评审稿为准。
|
||
> 评审建议:需求 15/16、需求 18/20/21、需求 19 分三组评审,不在一次会议中混合确认。
|
||
|
||
---
|
||
|
||
## 需求15:套餐下架后允许续费
|
||
|
||
### 业务规则
|
||
|
||
- 下架套餐(`shelf_status=2`)不可被**新购**
|
||
- 下架套餐**可以续费**(已在使用该套餐的客户)
|
||
- 续费仅支持**客户自己购买**(不允许代理代购下架套餐给新客户)
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start[请求购买套餐] --> Enabled{套餐是否启用?}
|
||
Enabled -->|否| RejectDisabled[拒绝:套餐已禁用]
|
||
Enabled -->|是| Shelf{是否已下架?}
|
||
Shelf -->|否| Allow[允许继续下单]
|
||
Shelf -->|是| Renewal{当前客户资产是否有该套餐历史使用记录?}
|
||
Renewal -->|否| RejectNew[拒绝:下架套餐不可新购]
|
||
Renewal -->|是| Actor{是否客户本人续费?}
|
||
Actor -->|是| Allow
|
||
Actor -->|否| RejectProxy[拒绝:下架套餐不可代购]
|
||
```
|
||
|
||
“客户本人”必须由后端登录主体与资产归属关系判断,不能信任前端传入 `is_renewal=true`。
|
||
|
||
### 后端
|
||
|
||
**当前逻辑**:下架套餐在购买时被拦截。
|
||
|
||
修改:在订单创建校验中由后端根据资产、历史使用记录和登录主体判定“新购/续费”,不能接收或信任前端 `is_renewal`:
|
||
|
||
```go
|
||
// internal/service/order/service.go 或 client_order/service.go
|
||
func (s *Service) validatePackageAvailability(
|
||
ctx context.Context,
|
||
pkg *model.Package,
|
||
asset *ResolvedAsset,
|
||
actor *PurchaseActor,
|
||
) error {
|
||
if pkg.Status == constants.StatusDisabled { // 0=禁用,定义在 pkg/constants/constants.go
|
||
return errors.New(errors.CodeForbidden, "套餐已禁用")
|
||
}
|
||
if pkg.ShelfStatus == constants.ShelfStatusOff { // 2=下架
|
||
if !s.hasRenewalEligibility(ctx, asset, actor, pkg.ID) {
|
||
return errors.New(errors.CodeForbidden, "套餐已下架,仅支持资产所有人续费")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
```
|
||
|
||
**续费资格判断**:查当前资产是否有该套餐的有效历史使用记录,并确认当前登录主体就是资产所有人。已失效、已退款或仅创建未生效的记录不能作为续费资格:
|
||
|
||
```go
|
||
func (s *Service) hasRenewalEligibility(ctx context.Context, asset *ResolvedAsset, actor *PurchaseActor, packageID uint) bool {
|
||
return actor.OwnsAsset(asset) &&
|
||
s.packageUsageStore.HasValidHistory(ctx, asset.Type, asset.ID, packageID)
|
||
}
|
||
```
|
||
|
||
### 前端
|
||
|
||
C端资产详情/当前套餐卡片:在当前套餐旁提供“续费”按钮。点击后复用现有购买套餐流程,并携带资产和当前套餐上下文;后端重新判定资格,前端传入的上下文不构成授权依据。下架套餐不出现在普通“新购套餐”列表,也不额外建设第二个续费套餐列表。
|
||
|
||
后台代购时:下架套餐的"代购"按钮禁用,tooltip 提示"套餐已下架,不可代购"。
|
||
|
||
---
|
||
|
||
## 需求16:代理分销码与佣金提现(已移出7月迭代)
|
||
|
||
> 状态:已移出本期
|
||
> 决策日期:2026-07-14
|
||
> 后续处理:作为独立需求重新评审和排期,不纳入本次开发、迁移和发布
|
||
|
||
本次范围调整包含整个需求 16:
|
||
|
||
- 代理/员工分销码和推广二维码。
|
||
- H5 代理申请、进度查询和退回重提。
|
||
- 代理申请接入通用审批及审批后自动开店。
|
||
- 店铺发展人关系和代理申请来源字段。
|
||
- 佣金提现合同、营业执照、法人身份证、门头照、发票及主体一致性校验。
|
||
|
||
因此 7 月迭代不创建 `tb_distribution_code`、`tb_agent_application`,不修改 `tb_shop` 发展人字段和提现材料字段,不注册代理申请相关 API,不发布 `agent_application_approval`,也不建设对应前端页面。
|
||
|
||
通用审批流本期只接入退款和平台员工线下充值。需求 16 后续重新立项时,可以复用本期审批流、站内消息、对象存储和 Outbox 能力,但必须重新评审其数据模型、H5 安全、开店幂等、提现材料及工时。
|
||
|
||
---
|
||
|
||
## 需求18:多人审批(APR-001~009)
|
||
|
||
### 依赖
|
||
|
||
历史设计基于 [已废弃的本地审批流](../历史方案/本地通用审批流-已废弃方案.md) 和 [站内消息初版](../历史方案/站内消息-初版.md)。
|
||
|
||
APR-009(企微审批对接)= Phase 2。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor Applicant as 提交人
|
||
participant Biz as 退款/充值业务
|
||
participant Approval as 审批流
|
||
participant Notice as 站内消息
|
||
actor Approver1 as 当前节点审批人
|
||
actor Approver2 as 下一节点审批人
|
||
|
||
Applicant->>Biz: 提交申请
|
||
Biz->>Approval: 同事务创建流程实例和首任务
|
||
Approval->>Notice: TaskCreated
|
||
Notice-->>Approver1: 待审批提醒
|
||
Approver1->>Approval: 审批通过
|
||
Approval->>Notice: 下一节点 TaskCreated
|
||
Notice-->>Approver2: 待审批提醒
|
||
Approver2->>Approval: 通过/驳回/退回
|
||
Approval->>Notice: 流程结果事件
|
||
Notice-->>Applicant: 结果和原因
|
||
```
|
||
|
||
### 实现要点
|
||
|
||
| 编号 | 需求 | 实现 |
|
||
|------|------|------|
|
||
| APR-001~003 | 充值/退款多级审核 | 见需求20/21;需求16已移出本期 |
|
||
| APR-004 | 审核环节:部门领导→财务 | 作为默认流程定义的两个串行节点;系统无部门模型,节点审批人由角色或指定账号配置,禁止按步骤编号或角色名称写死 |
|
||
| APR-005 | 待审核有消息提示 | 站内消息 `NotifyTypeApprovalPending` |
|
||
| APR-006 | 上一级完成后才提示下一级 | `ProcessInstance` 完成当前任务并激活下一节点,写入 `TaskCreated` Outbox 事件 |
|
||
| APR-007 | 通过后通知申请人 | `ProcessApproved` 事件处理器 |
|
||
| APR-008 | 驳回/退回后通知申请人含原因 | `ProcessRejected` / `ProcessReturned` 事件处理器 |
|
||
| APR-009 | 对接企微 | **Phase 2** |
|
||
|
||
### 默认流程与可配置边界
|
||
|
||
- 本迭代可以预置“业务审核 → 财务审核”两个节点,但这只是初始流程定义,不是引擎固定规则。
|
||
- 每个节点可配置 `role` 或 `user` 审批人来源,以及 `any` 或 `all` 完成方式。
|
||
- 节点可配置是否要求操作密码;仅触发该节点完成的审批人输入,不能按“财务节点”等名称写死。
|
||
- 角色审批在节点激活时解析当前启用账号,并将候选审批人快照到任务审批人表。
|
||
- 流程定义发布后不可修改;调整节点或审批人配置时创建新版本。
|
||
- 已发起实例始终使用发起时保存的流程定义快照。
|
||
|
||
### 业务表与审批流的关联
|
||
|
||
充值单和退款单接入审批流,各自新增 `approval_instance_id` 字段。具体增量 DDL 与业务处理状态字段分别在需求 20、需求 21 中定义,迁移文件只能创建一次。
|
||
|
||
本段是历史设计。旧接入协议见 [本地通用审批流 - 业务接入协议](../历史方案/本地通用审批流-已废弃方案.md#十一业务接入协议)。
|
||
|
||
退款和充值详情统一返回 `approval_source=none|workflow|legacy`。代理在线充值等无需审批的记录返回 `none`;发布前已经结束且没有流程实例的历史审批记录返回 `legacy` 并只读展示;发布时仍待审批的退款和员工线下充值必须在维护窗口回填流程实例。
|
||
|
||
### 前端技术方案
|
||
|
||
- 历史前端交互见 [前端共性方案历史稿](../历史方案/前端共性方案-历史稿.md#六统一审批交互);当前已改为企微审批只读页面。
|
||
- 退款、充值列表只展示审批摘要;审批详情首屏展示发起时固化的业务关键字段和业务资料,随后展示完整审批人、意见和历史实例。
|
||
- 每次通过、驳回、退回分别保存审批意见和可选附件;驳回、退回意见必填,附件不与退款/充值业务凭证混用。
|
||
- 时间线必须能查看此前审批人的动作、时间、意见和审批附件;业务资料与审批附件分区展示。
|
||
- 所有节点名称和审批人来自接口,不保留“部门领导审批人”“财务审批人”固定字段。
|
||
- APR-009 不在 Phase 1 前端中展示不可用入口;企业微信接入完成后再增加来源标识和跳转。
|
||
|
||
---
|
||
|
||
## 需求19:批量订购套餐(BPO-001~008)
|
||
|
||
### 支付方式粒度
|
||
|
||
评审结论:支付方式按**整批统一**设计:
|
||
|
||
- 页面选择 `offline` 或 `agent_wallet`,Excel 不再重复填写支付方式。
|
||
- 混合支付拆成两个批次,避免一份凭证对应多种支付语义。
|
||
- Excel 不包含支付方式列,后端拒绝同一批次混合支付。
|
||
|
||
### 业务流程
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start[员工选择代理和支付方式] --> Upload[上传 Excel]
|
||
Upload --> Parse[解析并持久化逐行明细]
|
||
Parse --> Validate[校验资产、套餐、归属和重复行]
|
||
Validate --> Item{处理下一条有效明细}
|
||
Item -->|代理钱包| Lock[锁定钱包并校验可用余额]
|
||
Item -->|线下支付| Voucher[校验整批支付凭证]
|
||
Lock --> Order[单条事务创建订单并扣款]
|
||
Voucher --> OrderOffline[单条事务创建已支付订单]
|
||
Order --> Result[记录订单 ID 和成功状态]
|
||
OrderOffline --> Result
|
||
Result --> More{还有待处理明细?}
|
||
More -->|是| Item
|
||
More -->|否| Summary[汇总任务结果]
|
||
Validate -->|校验失败| Failed[记录结构化失败原因]
|
||
Failed --> More
|
||
```
|
||
|
||
任务允许部分成功。每一行是独立、可重试、可审计的业务单元,不能只保存一段失败 JSON。
|
||
|
||
### 数据库变更
|
||
|
||
```sql
|
||
CREATE TABLE tb_bulk_purchase_task (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ,
|
||
creator BIGINT NOT NULL DEFAULT 0,
|
||
updater BIGINT NOT NULL DEFAULT 0,
|
||
task_no VARCHAR(30) NOT NULL,
|
||
source_file_key VARCHAR(500) NOT NULL,
|
||
shop_id BIGINT NOT NULL,
|
||
operator_id BIGINT NOT NULL,
|
||
payment_method VARCHAR(20) NOT NULL,
|
||
voucher_keys JSONB NOT NULL DEFAULT '[]',
|
||
total_amount BIGINT NOT NULL DEFAULT 0,
|
||
total_count INT NOT NULL DEFAULT 0,
|
||
success_count INT NOT NULL DEFAULT 0,
|
||
fail_count INT NOT NULL DEFAULT 0,
|
||
status INT NOT NULL DEFAULT 1,
|
||
error_message TEXT NOT NULL DEFAULT '',
|
||
started_at TIMESTAMPTZ,
|
||
completed_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_bulk_purchase_task_no
|
||
ON tb_bulk_purchase_task(task_no)
|
||
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,
|
||
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,
|
||
failure_code VARCHAR(50) NOT NULL DEFAULT '',
|
||
failure_reason VARCHAR(500) NOT NULL DEFAULT '',
|
||
idempotency_key VARCHAR(100) NOT NULL,
|
||
processed_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_bulk_purchase_item_task_row
|
||
ON tb_bulk_purchase_item(task_id, row_no);
|
||
|
||
CREATE UNIQUE INDEX idx_bulk_purchase_item_idempotency
|
||
ON tb_bulk_purchase_item(idempotency_key);
|
||
|
||
CREATE INDEX idx_bulk_purchase_item_task_status
|
||
ON tb_bulk_purchase_item(task_id, status);
|
||
```
|
||
|
||
状态建议:
|
||
|
||
- 任务:`1=待处理, 2=处理中, 3=已完成, 4=部分成功, 5=失败`。
|
||
- 明细:`1=待处理, 2=处理中, 3=成功, 4=失败`。
|
||
|
||
### 处理与幂等
|
||
|
||
1. API 校验文件、代理、支付方式和凭证,将 Excel 保存到对象存储并创建任务,返回 `task_id`。
|
||
2. Worker 根据 `source_file_key` 下载并解析 Excel,将每一行先写入明细表,再开始业务处理。
|
||
3. 同一任务按行顺序处理,避免对同一代理钱包制造不必要的乐观锁冲突。
|
||
4. 每行使用独立事务。代理钱包支付时锁定钱包记录,校验 `balance - frozen_balance + credit_limit` 后,在同一事务扣款、创建订单、资金流水并更新明细。
|
||
5. `idempotency_key` 使用 `bulk_purchase:{task_id}:{row_no}`。Worker 重试时,已存在成功订单的明细直接跳过。
|
||
6. 单行失败不回滚已成功行;失败原因写结构化错误码和用户可见中文原因。
|
||
7. 任务汇总从明细表计算,不信任内存计数。
|
||
|
||
Asynq 载荷只传任务 ID,不传文件字节或临时路径。源文件保留周期按对象存储统一策略处理,确保 Worker 重试期间仍可读取。
|
||
|
||
建议单文件上限 1000 行;超过上限在 API 层拒绝,避免长事务和过长处理时间。
|
||
|
||
### API 设计
|
||
|
||
**前端静态 Excel 模板**:
|
||
|
||
模板由前端项目随版本发布,后端不提供下载接口。按已确认的“整批统一支付方式”,模板字段为:
|
||
|
||
```text
|
||
资产类型 | 资产标识 | 套餐编码 | 套餐名称
|
||
```
|
||
|
||
`套餐名称`用于人工核对,实际匹配以稳定的 `套餐编码` 为准。后端必须校验表头并对未知列给出明确错误,不能依赖模板一定来自当前前端版本。
|
||
|
||
**上传并提交**:
|
||
|
||
```text
|
||
POST /api/admin/bulk-purchases
|
||
Content-Type: multipart/form-data
|
||
|
||
shop_id: 123
|
||
payment_method: offline | agent_wallet
|
||
voucher_keys: ["key1","key2"]
|
||
file: <Excel文件>
|
||
```
|
||
|
||
`offline` 时 `voucher_keys` 必填,`agent_wallet` 时忽略该字段。
|
||
|
||
**查询任务状态**:
|
||
|
||
```text
|
||
GET /api/admin/bulk-purchases/{task_id}
|
||
```
|
||
|
||
**分页查询任务明细**:
|
||
|
||
```text
|
||
GET /api/admin/bulk-purchases/{task_id}/items?status=4&page=1&page_size=50
|
||
```
|
||
|
||
### 前端技术方案
|
||
|
||
- 页面分为“参数确认 → 文件上传 → 处理中 → 结果”四个稳定步骤,刷新页面后可根据任务 ID 恢复进度。
|
||
- 提交前展示代理、支付方式、凭证数量和文件名的二次确认;钱包支付额外展示当前可用余额,但最终以 Worker 扣款时校验为准。
|
||
- 任务处理中展示总数、已处理数、成功数和失败数,轮询规则复用统一异步任务方案。
|
||
- 结果页默认显示失败明细,可切换全部/成功/失败,并可按资产标识搜索。
|
||
- 部分成功使用明确状态,不弹“全部成功”提示;再次上传失败行会创建新任务,不修改旧任务历史。
|
||
- 操作员、任务号和处理时间在页面固定展示,便于财务和运营追溯。
|
||
|
||
---
|
||
|
||
## 需求20:退款审批
|
||
|
||
### 依赖
|
||
|
||
本段历史设计基于 [已废弃的本地审批流](../历史方案/本地通用审批流-已废弃方案.md)。
|
||
|
||
### 退款单现有状态(不变)
|
||
|
||
```
|
||
1=待审批 2=已通过 3=已拒绝 4=已退回
|
||
```
|
||
|
||
退款单状态值的原有语义不改;审批进度由 `tb_approval_process_instance` 管理,两者通过 `approval_instance_id` 关联。审批通过后,代理钱包退款自动回退到原扣款代理主钱包;微信、支付宝和线下退款由财务人工完成。业务表增加独立处理状态:
|
||
|
||
```text
|
||
processing_status:0=待处理 1=处理中 2=已完成 3=处理失败
|
||
```
|
||
|
||
`status=2` 表示审批结论已通过,`processing_status` 表示实际退款动作是否完成。接口和前端必须同时展示两者。
|
||
|
||
### 数据库变更
|
||
|
||
```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 '[]';
|
||
|
||
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` 仍是申请业务资料,不能混用为财务完成凭证。
|
||
|
||
现有退款审批允许确认 `approved_refund_amount`,切换到通用审批后必须保留:配置为最终决策的节点在完成时返回金额动作字段,审批人可确认实际退款金额;省略时使用申请金额。退款动作适配器在审批事务内校验金额大于 0,且不超过申请退款金额和订单实收金额,并写入退款单和审批操作日志。会签需要指定金额决策人时,流程定义增加其专属最终节点,禁止第一位会签人预先锁定金额。
|
||
|
||
### 流程
|
||
|
||
```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` 首节点创建流程实例和任务;历史终态退款不伪造流程实例。
|
||
|
||
---
|
||
|
||
## 需求21:充值审核流程
|
||
|
||
### 充值单现有状态
|
||
|
||
```
|
||
tb_agent_recharge_record:1=待支付 2=已支付 3=已完成 4=已关闭 5=已退款
|
||
```
|
||
|
||
代码中已经存在 `6=已驳回`,不能改写其含义。员工线下充值走审批流时,在现有状态基础上追加 `7=已退回`:
|
||
|
||
```sql
|
||
-- 现有:1=待支付 2=已支付 3=已完成 4=已关闭 5=已退款 6=已驳回
|
||
-- 新增:7=已退回
|
||
|
||
ALTER TABLE tb_agent_recharge_record
|
||
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 return_reason VARCHAR(500) NOT NULL DEFAULT '';
|
||
|
||
CREATE INDEX idx_agent_recharge_approval_instance
|
||
ON tb_agent_recharge_record(approval_instance_id)
|
||
WHERE approval_instance_id IS NOT NULL;
|
||
```
|
||
|
||
充值业务的状态语义:
|
||
- `1=待支付`:创建未支付(线下充值等待审批时也停在这里,由 `approval_instance_id` 查询审批状态)
|
||
- `2=已支付`:在线支付已确认,或线下充值审批通过后正在执行钱包入账
|
||
- `3=已完成`:充值到账
|
||
- `4=已关闭`:取消/超时
|
||
- `5=已退款`:退款
|
||
- `6=已驳回`:审批流程拒绝
|
||
- `7=已退回`:审批人退回给提交人修改
|
||
|
||
`rejection_reason` 只保存驳回原因,新增 `return_reason` 保存退回修改原因,禁止复用一个字段导致前端无法区分终止和可重提。
|
||
|
||
充值处理状态保持:`0=未触发, 1=处理中, 2=处理成功, 3=处理失败`。退款的 `0` 已收口为“待处理”,两者不要共用中文状态名称常量。
|
||
|
||
### 流程
|
||
|
||
**代理自行充值(不走审批)**:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A[代理提交充值申请] --> B[系统生成收款码]
|
||
B --> C[代理扫码支付]
|
||
C --> D[支付回调幂等入账]
|
||
```
|
||
|
||
**员工线下代充值(走审批)**:
|
||
|
||
现有 `offline-pay` 的全局操作密码校验必须保留。通用审批详情在最后一个审批节点返回 `operation_password` 动作字段;审批动作适配器调用现有 `OperationPasswordService` 校验通过后才允许流程完成。密码只在内存中参与本次校验,不落库、不写审批日志、不进入 Outbox。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
actor Staff as 平台员工
|
||
actor Approver as 审批人
|
||
participant Recharge as Recharge Application
|
||
participant Approval as Approval Application
|
||
participant DB as PostgreSQL
|
||
participant Worker as RechargeApprovalHandler
|
||
|
||
Staff->>Recharge: POST /api/admin/agent-recharges(payment_method=offline)
|
||
Recharge->>DB: 同事务创建充值单(status=1)
|
||
Recharge->>Approval: StartProcess(recharge, recharge_id)
|
||
Approval->>DB: 创建实例、首任务、审批人、Outbox
|
||
Recharge->>DB: 回写 approval_instance_id
|
||
Approver->>Approval: 按 task_id 审批
|
||
DB-->>Worker: 投递流程结果事件
|
||
alt 审批通过
|
||
Worker->>DB: status 从 1 更新为 2,processing_status=1
|
||
Worker->>DB: 幂等增加钱包余额并写流水
|
||
Worker->>DB: status 从 2 更新为 3,processing_status=2
|
||
else 审批拒绝
|
||
Worker->>DB: status 从 1 更新为 6,写 rejection_reason
|
||
else 退回修改
|
||
Worker->>DB: status 从 1 更新为 7,写 return_reason
|
||
end
|
||
```
|
||
|
||
充值接口独立返回审批状态和业务处理状态。`RechargeApprovalHandler` 使用 `recharge:{recharge_no}` 作为幂等键;钱包余额、版本、充值单和交易流水必须在同一事务更新。
|
||
|
||
充值处理同样使用 `processing_started_at` 作为可恢复租约。重复事件不能再次增加余额;若钱包流水已经存在而充值单状态未完成,重试只补齐充值单状态。
|
||
|
||
### 退回后重新提交
|
||
|
||
```
|
||
POST /api/admin/agent-recharges/{id}/resubmit
|
||
→ 校验 status=7
|
||
→ 请求体可修改 amount、payment_voucher_key、remark
|
||
→ 在同一事务新建 ProcessInstance
|
||
→ 更新 approval_instance_id,status 回到 1(待支付/待审批)
|
||
→ processing_status 重置为 0,清空处理错误和 return_reason
|
||
→ 旧审批实例保留为历史记录
|
||
```
|
||
|
||
新增 `resubmit` 路由时沿用现有 `/api/admin/agent-recharges` 资源名,不另建 `/agent-recharge-records` 路径。店铺、支付方式和提交人不可修改;编辑与新流程创建必须同事务完成。
|
||
|
||
### 停机切换
|
||
|
||
现有 `POST /api/admin/agent-recharges/{id}/offline-pay` 和 `POST /api/admin/agent-recharges/{id}/reject` 都会绕过通用审批任务,本次不保留兼容窗口:
|
||
|
||
1. 发布前进入维护模式,停止创建和处理线下充值。
|
||
2. 执行审批关联字段迁移,同时发布新 API、Worker 和前端。
|
||
3. 初始化并启用 `recharge → recharge_approval` 绑定。
|
||
4. 为存量“平台员工创建 + 线下支付 + 尚未入账”的充值记录幂等创建流程实例,代理在线充值不回填审批。
|
||
5. 新前端创建线下充值后直接进入审批详情,不再展示“确认线下充值”按钮。
|
||
6. 新版本不注册 `offline-pay` 和业务单级 `reject` 路由;线下充值只能由 `ProcessApproved` 事件触发幂等入账,驳回统一由任务级审批接口产生 `ProcessRejected`。
|
||
7. 验证审批通过、驳回、退回、处理失败重试和钱包流水后再解除维护模式。
|
||
|
||
### 前端技术方案
|
||
|
||
- 代理自行充值保留现有收款码和支付状态页面,不显示审批信息。
|
||
- 平台员工选择 `offline` 时,提交成功进入充值详情并展示审批时间线。
|
||
- `status=6` 展示“已驳回”,`status=7` 展示“已退回”;两者按钮不同,只有已退回可编辑和重新提交。
|
||
- 审批通过但 `processing_status=1` 时显示“充值处理中”;状态为 3 且处理成功后才显示最新钱包余额。
|
||
- `processing_status=3` 时不允许前端再次点击入账,只展示系统重试状态和管理员排查入口。
|