refactor: 一次性佣金配置从套餐级别提升到系列级别
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s

主要变更:
- 新增 tb_shop_series_allocation 表,存储系列级别的一次性佣金配置
- ShopPackageAllocation 移除 one_time_commission_amount 字段
- PackageSeries 新增 enable_one_time_commission 字段控制是否启用一次性佣金
- 新增 /api/admin/shop-series-allocations CRUD 接口
- 佣金计算逻辑改为从 ShopSeriesAllocation 获取一次性佣金金额
- 删除废弃的 ShopSeriesOneTimeCommissionTier 模型
- OpenAPI Tag '系列分配' 和 '单套餐分配' 合并为 '套餐分配'

迁移脚本:
- 000042: 重构佣金套餐模型
- 000043: 简化佣金分配
- 000044: 一次性佣金分配重构
- 000045: PackageSeries 添加 enable_one_time_commission 字段

测试:
- 新增验收测试 (shop_series_allocation, commission_calculation)
- 新增流程测试 (one_time_commission_chain)
- 删除过时的单元测试(已被验收测试覆盖)
This commit is contained in:
2026-02-04 14:28:44 +08:00
parent fba8e9e76b
commit b18ecfeb55
106 changed files with 9899 additions and 6608 deletions

View File

@@ -0,0 +1,351 @@
# 套餐与佣金业务模型
本文档定义了套餐、套餐系列、佣金的完整业务模型,作为系统改造的规范参考。
---
## 一、核心概念
### 1.1 两种佣金类型
系统只有两种佣金类型:
| 佣金类型 | 触发时机 | 触发次数 | 计算方式 |
|---------|---------|---------|---------|
| **差价佣金** | 每笔订单 | 每单都触发 | 下级成本价 - 自己成本价 |
| **一次性佣金** | 首充/累计充值达标 | 每张卡/设备只触发一次 | 上级给的 - 给下级的 |
### 1.2 实体关系
```
┌─────────────────┐
│ 套餐系列 │
│ PackageSeries │
├─────────────────┤
│ • 系列名称 │
│ • 一次性佣金规则 │ ← 可选配置
└────────┬────────┘
│ 1:N
┌─────────────────┐ ┌─────────────────┐
│ 套餐 │ │ 卡/设备 │
│ Package │ │ IoT/Device │
├─────────────────┤ ├─────────────────┤
│ • 成本价 │ │ • 绑定系列ID │
│ • 建议售价 │ │ • 累计充值金额 │ ← 按系列累计
│ • 真流量(必填) │ │ • 是否已首充 │ ← 按系列记录
│ • 虚流量(可选) │ └────────┬────────┘
│ • 虚流量开关 │ │
└────────┬────────┘ │ 分配
│ ▼
│ 分配 ┌─────────────────┐
▼ │ 店铺 │
┌─────────────────┐ │ Shop │
│ 套餐分配 │◀─────────┤ • 代理层级 │
│ PkgAllocation │ │ • 上级店铺ID │
├─────────────────┤ └─────────────────┘
│ • 店铺ID │
│ • 套餐ID │
│ • 成本价(加价后)│
│ • 一次性佣金额 │ ← 给该代理的金额
└─────────────────┘
```
---
## 二、套餐模型
### 2.1 字段定义
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `cost_price` | int64 | 是 | 成本价(平台设置的基础成本价,分) |
| `suggested_price` | int64 | 是 | 建议售价(给代理参考,分) |
| `real_data_mb` | int64 | 是 | 真实流量额度MB |
| `enable_virtual_data` | bool | 否 | 是否启用虚流量 |
| `virtual_data_mb` | int64 | 否 | 虚流量额度(启用时必填,≤ 真实流量MB |
### 2.2 流量停机判断
```
停机目标值 = enable_virtual_data ? virtual_data_mb : real_data_mb
```
### 2.3 不同用户视角
| 用户类型 | 看到的成本价 | 看到的一次性佣金 |
|---------|-------------|-----------------|
| 平台 | 基础成本价 | 完整规则 |
| 代理A | A的成本价已加价 | A能拿到的金额 |
| 代理A1 | A1的成本价再加价 | A1能拿到的金额 |
---
## 三、差价佣金
### 3.1 计算规则
```
平台设置基础成本价: 100
│ 分配给代理A设置成本价: 120
代理A成本价: 120
│ 分配给代理A1设置成本价: 130
代理A1成本价: 130
│ A1销售给客户售价: 200
结果:
• A1 收入 = 200 - 130 = 70元销售利润不是佣金
• A 佣金 = 130 - 120 = 10元差价佣金
• 平台收入 = 120元
```
### 3.2 关键区分
- **收入/利润**:末端代理的 `售价 - 自己成本价`
- **差价佣金**:上级代理的 `下级成本价 - 自己成本价`
- **平台收入**:一级代理的成本价
---
## 四、一次性佣金
### 4.1 触发条件
| 条件类型 | 说明 | 强充要求 |
|---------|------|---------|
| `first_recharge` | 首充:该卡/设备在该系列下的第一次充值 | 必须强充 |
| `accumulated_recharge` | 累计充值:累计充值金额达到阈值 | 可选强充 |
### 4.2 规则配置(套餐系列层面)
| 配置项 | 类型 | 说明 |
|--------|------|------|
| `enable` | bool | 是否启用一次性佣金 |
| `trigger_type` | string | 触发类型:`first_recharge` / `accumulated_recharge` |
| `threshold` | int64 | 触发阈值(分):首充要求金额 或 累计要求金额 |
| `commission_type` | string | 返佣类型:`fixed`(固定) / `tiered`(梯度) |
| `commission_amount` | int64 | 固定返佣金额fixed类型时 |
| `tiers` | array | 梯度配置tiered类型时 |
| `validity_type` | string | 时效类型:`permanent` / `fixed_date` / `relative` |
| `validity_value` | string | 时效值(到期日期 或 月数) |
| `enable_force_recharge` | bool | 是否启用强充 |
| `force_calc_type` | string | 强充金额计算:`fixed`(固定) / `dynamic`(动态差额) |
| `force_amount` | int64 | 强充金额fixed类型时 |
### 4.3 链式分配
一次性佣金在整条代理链上按约定分配:
```
系列规则首充100返20
分配配置:
平台给A20元
A给A18元
A1给A25元
触发首充时:
A2 获得5元
A1 获得8 - 5 = 3元
A 获得20 - 8 = 12元
─────────────────────
合计20元 ✓
```
### 4.4 首充流程
```
客户购买套餐
预检:系列是否启用一次性佣金且为首充?
否 ───────────────────▶ 正常购买流程
该卡/设备在该系列下是否已首充过?
是 ───────────────────▶ 正常购买流程(不再返佣)
计算强充金额 = max(首充要求, 套餐售价)
返回提示:"需要充值 xxx 元"
用户确认 → 创建充值订单(金额=强充金额)
用户支付
支付成功:
1. 钱进入钱包
2. 标记该卡/设备已首充
3. 自动创建套餐购买订单并完成
4. 扣款(套餐售价)
5. 触发一次性佣金,链式分配
```
### 4.5 累计充值流程
```
客户充值(直接充值到钱包)
累计充值金额 += 本次充值金额
该卡/设备是否已触发过累计充值返佣?
是 ───────────────────▶ 结束(不再返佣)
累计金额 >= 累计要求?
否 ───────────────────▶ 结束(继续累计)
触发一次性佣金,链式分配
标记该卡/设备已触发累计充值返佣
```
**累计规则**
| 操作类型 | 是否累计 |
|---------|---------|
| 直接充值到钱包 | ✅ 累计 |
| 直接购买套餐(不经过钱包) | ❌ 不累计 |
| 强充购买套餐(先充值再扣款) | ✅ 累计(充值部分) |
---
## 五、梯度佣金
梯度佣金是一次性佣金的进阶版,根据代理销量/销售额动态调整返佣金额。
### 5.1 配置项
| 配置项 | 类型 | 说明 |
|--------|------|------|
| `tier_dimension` | string | 梯度维度:`sales_count`(销量) / `sales_amount`(销售额) |
| `stat_scope` | string | 统计范围:`self`(仅自己) / `self_and_sub`(自己+下级) |
| `tiers` | array | 梯度档位列表 |
| `tiers[].threshold` | int64 | 阈值(销量或销售额) |
| `tiers[].amount` | int64 | 返佣金额(分) |
### 5.2 示例
```
梯度规则(销量维度):
┌────────────────┬────────────────────────┐
│ 销量区间 │ 首充100返佣金额 │
├────────────────┼────────────────────────┤
│ >= 0 │ 5元 │
├────────────────┼────────────────────────┤
│ >= 100 │ 10元 │
├────────────────┼────────────────────────┤
│ >= 200 │ 20元 │
└────────────────┴────────────────────────┘
代理A当前销量150单 → 落在 [100, 200) 区间 → 首充返10元
```
### 5.3 梯度升级
```
初始状态:
代理A 销量150适用10元档给A1设置5元
触发时A1得5元A得10-5=5元
升级后A销量达到210
A 适用20元档A1配置仍为5元
触发时A1得5元不变A得20-5=15元增量归上级
```
### 5.4 统计周期
- 统计周期与一次性佣金时效一致
- 只统计该套餐系列下的销量/销售额
---
## 六、约束规则
### 6.1 套餐分配
1. 下级成本价 >= 自己成本价(不能亏本卖)
2. 只能分配自己有权限的套餐给下级
3. 只能分配给直属下级(不能跨级)
### 6.2 一次性佣金分配
4. 给下级的金额 <= 自己能拿到的金额
5. 给下级的金额 >= 0可以设为0独吞全部
### 6.3 流量
6. 虚流量 <= 真实流量
### 6.4 配置修改
7. 修改配置只影响之后的新订单
8. 代理只能修改"给下级多少钱",不能修改触发规则
9. 平台修改系列规则不影响已分配的代理,需收回重新分配
### 6.5 触发限制
10. 一次性佣金每张卡/设备只触发一次
11. "首充"指该卡/设备在该系列下的第一次充值
12. 累计充值只统计"充值"操作,不统计"直接购买"
---
## 七、操作流程
### 7.1 理想的线性流程
```
1. 创建套餐系列
└─▶ 可选:配置一次性佣金规则
2. 创建套餐
└─▶ 归属到系列
└─▶ 设置成本价、建议售价
└─▶ 设置真流量(必填)、虚流量(可选)
3. 分配套餐给代理
└─▶ 设置代理成本价(加价)
└─▶ 如果系列启用一次性佣金:设置给代理的一次性佣金额度
4. 分配资产(卡/设备)给代理
└─▶ 资产绑定的套餐系列自动跟着走
5. 代理销售
└─▶ 客户购买套餐
└─▶ 差价佣金自动计算并入账给上级
└─▶ 满足一次性佣金条件时,按链式分配入账
```
---
## 八、与现有代码的差异
详见改造提案:[refactor-commission-package-model](../openspec/changes/refactor-commission-package-model/)