Files
junhong_cmp_fiber/docs/commission-package-model.md
huang b18ecfeb55
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
refactor: 一次性佣金配置从套餐级别提升到系列级别
主要变更:
- 新增 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)
- 删除过时的单元测试(已被验收测试覆盖)
2026-02-04 14:28:44 +08:00

352 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.
# 套餐与佣金业务模型
本文档定义了套餐、套餐系列、佣金的完整业务模型,作为系统改造的规范参考。
---
## 一、核心概念
### 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/)