Files
junhong_cmp_fiber/docs/7月迭代/需求15-16-18-19-20-21-复杂需求.md
2026-07-13 12:01:18 +09:00

349 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 需求15/16/18/19/20/21 技术方案
---
## 需求15套餐下架后允许续费
### 业务规则
- 下架套餐(`shelf_status=2`)不可被**新购**
- 下架套餐**可以续费**(已在使用该套餐的客户)
- 续费仅支持**客户自己购买**(不允许代理代购下架套餐给新客户)
### 后端
**当前逻辑**:下架套餐在购买时被拦截。
修改:在订单创建校验中,区分"新购"和"续费"场景:
```go
// internal/service/order/service.go 或 client_order/service.go
func (s *Service) validatePackageAvailability(
ctx context.Context,
pkg *model.Package,
assetID uint,
isRenewal bool,
) error {
if pkg.Status == constants.StatusDisabled { // 0=禁用,定义在 pkg/constants/constants.go
return errors.New(errors.CodeForbidden, "套餐已禁用")
}
if pkg.ShelfStatus == constants.ShelfStatusOff && !isRenewal { // 2=下架
return errors.New(errors.CodeForbidden, "套餐已下架,不可新购")
}
return nil
}
```
**isRenewal 判断**:查当前资产是否有该套餐的历史生效记录:
```go
func (s *Service) isRenewal(ctx context.Context, assetType string, assetID uint, packageID uint) bool {
count, _ := s.packageUsageStore.CountByAssetAndPackage(ctx, assetType, assetID, packageID)
return count > 0
}
```
### 前端
C端续费页面下架套餐不在"新购"列表中展示,但在"续费"入口中仍可展示。
后台代购时:下架套餐的"代购"按钮禁用tooltip 提示"套餐已下架,不可代购"。
---
## 需求16代理分销码与佣金提现
### 业务规则
**DST-001**:新建代理时自动建立分销归属关系(指定发展人)
**DST-002**:员工可作为代理发展人进行标识
**二维码**:代理或员工生成推广二维码 → 扫码进入H5填写信息 → 提交成为代理申请 → 平台审批
### 数据库变更
```sql
-- 1. Shop 表新增发展人字段
ALTER TABLE tb_shop
ADD COLUMN referrer_type VARCHAR(20) DEFAULT NULL
COMMENT '发展人类型 shop=代理介绍 admin=员工介绍',
ADD COLUMN referrer_id BIGINT DEFAULT NULL
COMMENT '发展人IDshop_id 或 tb_account.id',
ADD COLUMN referrer_name VARCHAR(50) DEFAULT NULL
COMMENT '发展人姓名快照';
-- 2. 分销码表
CREATE TABLE tb_distribution_code (
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,
code VARCHAR(32) NOT NULL,
owner_type VARCHAR(20) NOT NULL, -- shop=代理 | admin=员工
owner_id BIGINT NOT NULL,
owner_name VARCHAR(50) NOT NULL DEFAULT '',
qr_code_url TEXT,
use_count INT NOT NULL DEFAULT 0,
status INT NOT NULL DEFAULT 1, -- 1=有效 0=禁用
expires_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_distribution_code ON tb_distribution_code(code) WHERE deleted_at IS NULL;
-- 3. 代理申请表H5扫码提交
CREATE TABLE tb_agent_application (
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,
apply_no VARCHAR(30) NOT NULL,
distribution_code VARCHAR(32) NOT NULL,
referrer_type VARCHAR(20) NOT NULL,
referrer_id BIGINT NOT NULL,
applicant_name VARCHAR(50) NOT NULL,
applicant_phone VARCHAR(20) NOT NULL,
shop_name VARCHAR(100) NOT NULL,
province VARCHAR(50),
city VARCHAR(50),
district VARCHAR(50),
address VARCHAR(255),
status INT NOT NULL DEFAULT 1, -- 1=待审批 2=已通过 3=已拒绝
reject_reason TEXT,
reviewed_by BIGINT,
reviewed_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_agent_application_no ON tb_agent_application(apply_no) WHERE deleted_at IS NULL;
```
### API 设计
**生成分销码**
```
POST /admin/distribution-codes
body: { owner_type: "admin"|"shop", owner_id: 123 }
```
**H5 扫码获取分销码信息**
```
GET /app/distribution-codes/{code}
```
**H5 提交代理申请**
```
POST /app/agent-applications
body: { distribution_code, applicant_name, applicant_phone, shop_name, ... }
```
**后台代理申请列表/审批**
```
GET /admin/agent-applications?status=1
POST /admin/agent-applications/{id}/approve
POST /admin/agent-applications/{id}/reject
body: { reject_reason: "..." }
```
审批通过时:自动创建 `tb_shop` + `tb_account`,设置 `referrer_type``referrer_id`
**佣金提现DST-003~007**
现有 commission 框架基础上新增材料上传字段,提现申请时一次性上传所有材料(对象存储 Key 列表)。
---
## 需求18多人审批APR-001~009
### 依赖
基于 [审批流基础设施](./基础设施/审批流.md) 和 [站内消息](./基础设施/站内消息.md)。
APR-009企微审批对接= Phase 2。
### 实现要点
| 编号 | 需求 | 实现 |
|------|------|------|
| APR-001~003 | 充值/退款多级审核 | 见需求20/21 |
| APR-004 | 审核环节:部门领导→财务 | 审批流固定两步step=1 部门领导step=2 财务 |
| APR-005 | 待审核有消息提示 | 站内消息 `NotifyTypeApprovalPending` |
| APR-006 | 上一级完成后才提示下一级 | `ApprovalFlowAggregate.Approve()` 触发 `ApprovalStepAdvancedEvent` |
| APR-007 | 通过后通知申请人 | `ApprovalCompletedEvent` handler |
| APR-008 | 驳回/退回后通知申请人含原因 | `ApprovalRejectedEvent` / `ApprovalReturnedEvent` handler |
| APR-009 | 对接企微 | **Phase 2** |
### 业务表与审批流的关联
充值单、退款单接入审批流,各自新增 `approval_flow_id` 字段:
```sql
ALTER TABLE tb_refund_request ADD COLUMN approval_flow_id BIGINT DEFAULT NULL
COMMENT '当前审批流ID已退回后重新提交会更新为新流程ID';
ALTER TABLE tb_agent_recharge_record ADD COLUMN approval_flow_id BIGINT DEFAULT NULL
COMMENT '当前审批流ID已退回后重新提交会更新为新流程ID';
```
接入协议详见 [审批流文档 - 接入协议](./基础设施/审批流.md#六接入协议业务层如何接入审批流)。
---
## 需求19批量订购套餐BPO-001~008
### 业务流程
1. 员工进入批量订购页面
2. 选择代理 + 上传 Excel字段资产类型/资产标识/套餐编码/套餐名称/支付方式)
3. 系统解析并校验每一行
4. 支付方式:`offline=线下支付(默认成功)` / `agent_wallet=代理钱包支付`
5. 代理钱包支付时从代理余额扣款(含信用额度)
6. 线下支付时标记为已支付(不走钱包)
7. 校验失败的行展示在页面,带失败原因
8. **线下支付需上传整批次的支付凭证**
### 数据库变更
```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,
shop_id BIGINT NOT NULL,
operator_id BIGINT NOT NULL,
payment_method VARCHAR(20) NOT NULL, -- offline | agent_wallet
voucher_keys JSONB, -- 线下支付凭证列表
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, -- 1=处理中 2=已完成
failed_items JSONB -- 失败明细
);
CREATE UNIQUE INDEX idx_bulk_purchase_task_no ON tb_bulk_purchase_task(task_no) WHERE deleted_at IS NULL;
```
> 失败明细存 JSONB`failed_items`),与现有 DeviceImportTask 模式一致,无需单独明细表。
### API 设计
**下载 Excel 模板**
```
GET /admin/bulk-purchases/template
```
**上传并提交**
```
POST /admin/bulk-purchases
Content-Type: multipart/form-data
字段:
shop_id: 123
payment_method: offline | agent_wallet
voucher_keys: ["key1","key2"] (offline 时必填)
file: <Excel文件>
```
**查询任务状态**
```
GET /admin/bulk-purchases/{task_id}
```
---
## 需求20退款审批
### 依赖
基于 [审批流基础设施](./基础设施/审批流.md)。
### 退款单现有状态(不变)
```
1=待审批 2=已通过 3=已拒绝 4=已退回
```
退款单本身的状态含义不变,审批进度由 `tb_approval_flow` 管理,两者通过 `approval_flow_id` 关联。
### 流程
```
提交退款申请POST /admin/refund-requests
→ 创建 RefundRequeststatus=1 待审批)
→ 调 SubmitApprovalUseCase 创建 ApprovalFlowbiz_type=refund
→ 把 approval_flow_id 写入 RefundRequest
审批人操作POST /admin/approvals/{flow_id}/approve|reject|return
→ ApprovalCompletedEvent → RefundRequest.status=2已通过→ 触发实际退款
→ ApprovalRejectedEvent → RefundRequest.status=3已拒绝
→ ApprovalReturnedEvent → RefundRequest.status=4已退回→ 通知提交人可修改
```
### 退回后重新提交
```
PUT /admin/refund-requests/{id} 仅 status=4 时可编辑退款单内容
POST /admin/refund-requests/{id}/resubmit
→ 校验 status=4
→ 新建 ApprovalFlow
→ 更新 approval_flow_idstatus 回到 1待审批
```
---
## 需求21充值审核流程
### 充值单现有状态
```
tb_agent_recharge_record1=待支付 2=已支付 3=已完成 4=已关闭 5=已退款
```
员工线下充值走审批流,需在现有状态基础上**新增"已退回"状态**
```sql
-- status 说明更新(不改原有值语义,追加新状态)
-- 6=已退回(审批人退回给提交人修改)
```
充值业务的状态语义:
- `1=待支付`:创建未支付(线下充值等待审批时也停在这里,由 approval_flow_id 判断是否在审批中)
- `2=已支付`:支付成功(线下充值审批通过后跳到已完成,不经过此状态)
- `3=已完成`:充值到账
- `4=已关闭`:取消/超时
- `5=已退款`:退款
- `6=已退回`:审批人退回给提交人修改
### 流程
**代理自行充值(不走审批)**
```
代理提交充值申请 → 系统生成收款码 → 代理扫码支付 → 回调自动充值到钱包
```
**员工线下代充值(走审批)**
```
POST /admin/agent-recharge-recordspayment_method=offline
→ 创建 AgentRechargeRecordstatus=1
→ 调 SubmitApprovalUseCase 创建 ApprovalFlowbiz_type=recharge
→ 把 approval_flow_id 写入记录
审批通过 → AgentRechargeRecord.status=3已完成→ 实际充值到钱包
审批驳回 → AgentRechargeRecord.status=4已关闭
审批退回 → AgentRechargeRecord.status=6已退回→ 通知提交人可修改
```
### 退回后重新提交
```
PUT /admin/agent-recharge-records/{id} 仅 status=6 时可编辑
POST /admin/agent-recharge-records/{id}/resubmit
→ 校验 status=6
→ 新建 ApprovalFlow
→ 更新 approval_flow_idstatus 回到 1待支付/待审批)
```