349 lines
11 KiB
Markdown
349 lines
11 KiB
Markdown
# 需求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 '发展人ID(shop_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)
|
||
→ 创建 RefundRequest(status=1 待审批)
|
||
→ 调 SubmitApprovalUseCase 创建 ApprovalFlow(biz_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_id,status 回到 1(待审批)
|
||
```
|
||
|
||
---
|
||
|
||
## 需求21:充值审核流程
|
||
|
||
### 充值单现有状态
|
||
|
||
```
|
||
tb_agent_recharge_record:1=待支付 2=已支付 3=已完成 4=已关闭 5=已退款
|
||
```
|
||
|
||
员工线下充值走审批流,需在现有状态基础上**新增"已退回"状态**:
|
||
|
||
```sql
|
||
-- status 说明更新(不改原有值语义,追加新状态)
|
||
-- 6=已退回(审批人退回给提交人修改)
|
||
```
|
||
|
||
充值业务的状态语义:
|
||
- `1=待支付`:创建未支付(线下充值等待审批时也停在这里,由 approval_flow_id 判断是否在审批中)
|
||
- `2=已支付`:支付成功(线下充值审批通过后跳到已完成,不经过此状态)
|
||
- `3=已完成`:充值到账
|
||
- `4=已关闭`:取消/超时
|
||
- `5=已退款`:退款
|
||
- `6=已退回`:审批人退回给提交人修改
|
||
|
||
### 流程
|
||
|
||
**代理自行充值(不走审批)**:
|
||
|
||
```
|
||
代理提交充值申请 → 系统生成收款码 → 代理扫码支付 → 回调自动充值到钱包
|
||
```
|
||
|
||
**员工线下代充值(走审批)**:
|
||
|
||
```
|
||
POST /admin/agent-recharge-records(payment_method=offline)
|
||
→ 创建 AgentRechargeRecord(status=1)
|
||
→ 调 SubmitApprovalUseCase 创建 ApprovalFlow(biz_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_id,status 回到 1(待支付/待审批)
|
||
```
|