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,70 @@
# 累计充值追踪
## ADDED Requirements
### Requirement: 按卡/设备按系列累计
系统 SHALL 按照卡/设备在每个套餐系列下独立追踪累计充值金额。不同系列的累计互不影响。
#### Scenario: 同一卡在不同系列的累计
- **WHEN** IoT卡 A 在系列1下充值 100 元
- **AND** IoT卡 A 在系列2下充值 50 元
- **THEN** 系列1的累计金额 SHALL 为 100 元
- **AND** 系列2的累计金额 SHALL 为 50 元
#### Scenario: 同一卡在同一系列的累计
- **WHEN** IoT卡 A 在系列1下第一次充值 100 元
- **AND** IoT卡 A 在系列1下第二次充值 50 元
- **THEN** 系列1的累计金额 SHALL 为 150 元
### Requirement: 只有充值操作累计
系统 SHALL 只累计"充值到钱包"的操作直接购买套餐不经过钱包SHALL NOT 累计。
#### Scenario: 直接充值累计
- **WHEN** 客户选择充值 100 元到钱包
- **AND** 支付成功
- **THEN** 累计金额 SHALL 增加 100 元
#### Scenario: 直接购买不累计
- **WHEN** 客户直接购买 100 元套餐(余额足够)
- **AND** 系统从钱包扣款
- **THEN** 累计金额 SHALL 保持不变
#### Scenario: 强充购买累计
- **WHEN** 客户通过强充购买套餐
- **AND** 强充金额为 200 元
- **AND** 套餐价格为 100 元
- **THEN** 累计金额 SHALL 增加 200 元(充值部分)
- **AND** 钱包余额增加 200 后扣除 100
### Requirement: 首充状态按系列追踪
系统 SHALL 按照卡/设备在每个套餐系列下独立追踪首充状态。一个系列触发首充后,其他系列的首充状态不受影响。
#### Scenario: 首次在系列下充值
- **WHEN** IoT卡 A 从未在系列1下充值过
- **AND** IoT卡 A 进行充值操作
- **THEN** 系统 SHALL 标记该卡在系列1下的首充状态为"已触发"
#### Scenario: 非首次在系列下充值
- **WHEN** IoT卡 A 已在系列1下触发过首充
- **AND** IoT卡 A 再次充值
- **THEN** 系统 SHALL 不触发首充返佣
- **AND** 首充状态保持"已触发"
#### Scenario: 不同系列首充独立
- **WHEN** IoT卡 A 已在系列1下触发过首充
- **AND** IoT卡 A 首次在系列2下充值
- **THEN** 系统 SHALL 触发系列2的首充返佣如果规则启用
### Requirement: 一次性佣金只触发一次
每张卡/设备在每个套餐系列下一次性佣金无论首充还是累计充值SHALL 只触发一次。触发后不再重复触发。
#### Scenario: 累计充值达标后不再触发
- **WHEN** 系列规则为累计充值 200 返 40
- **AND** IoT卡 A 累计充值达到 200 元
- **AND** 系统触发一次性佣金 40 元
- **AND** IoT卡 A 继续充值 100 元(累计 300 元)
- **THEN** 系统 SHALL 不再触发一次性佣金

View File

@@ -0,0 +1,58 @@
# 代理可用套餐变更
## MODIFIED Requirements
### Requirement: 返回代理视角的套餐信息
代理查询套餐列表时,系统 SHALL 返回该代理视角的成本价和一次性佣金金额,而非原始配置。
**变更说明**:成本价和一次性佣金金额需要根据套餐分配关系动态计算。
#### Scenario: 代理查看套餐列表
- **WHEN** 代理A调用套餐列表接口
- **AND** 该套餐的基础成本价100元
- **AND** 平台给A分配时设置成本价120元
- **THEN** 返回的 `cost_price` SHALL 为 120元A的成本价
#### Scenario: 代理查看一次性佣金
- **WHEN** 代理A调用套餐列表接口
- **AND** 系列规则首充100返20元
- **AND** 平台给A设置的一次性佣金金额为15元
- **THEN** 返回的 `one_time_commission_amount` SHALL 为 15元
- **AND** 不返回系列规则的20元
#### Scenario: 平台查看套餐列表
- **WHEN** 平台管理员调用套餐列表接口
- **THEN** 返回基础成本价(不是代理视角)
- **AND** 返回完整的一次性佣金规则
---
### Requirement: 未分配套餐不可见
代理只能看到已分配给自己的套餐。未分配的套餐 SHALL NOT 出现在代理的套餐列表中。
#### Scenario: 只返回已分配套餐
- **WHEN** 代理A调用套餐列表接口
- **AND** 系统共有套餐 P1、P2、P3
- **AND** 只有 P1、P2 分配给了 A
- **THEN** 返回列表只包含 P1、P2
- **AND** 不包含 P3
---
### Requirement: 套餐分配新增一次性佣金金额
ShopPackageAllocation 模型 MUST 新增 `one_time_commission_amount` 字段,记录给该代理的一次性佣金金额。
**变更说明**:一次性佣金金额配置从系列分配移到套餐分配。
#### Scenario: 分配套餐时设置一次性佣金金额
- **WHEN** 上级给下级分配套餐
- **AND** 设置一次性佣金金额为10元
- **THEN** ShopPackageAllocation 记录 `one_time_commission_amount = 1000`(分)
#### Scenario: 一次性佣金金额约束
- **WHEN** 上级给下级设置一次性佣金金额
- **THEN** 该金额 MUST <= 上级自己能拿到的一次性佣金金额
- **AND** 该金额 MUST >= 0

View File

@@ -0,0 +1,72 @@
# 佣金计算变更
## MODIFIED Requirements
### Requirement: 一次性佣金计算
系统 SHALL 按链式分配规则计算一次性佣金。每级代理实际获得的佣金 = 自己能拿到的金额 - 给下级的金额。
**变更说明**:从"直接发放给归属店铺"改为"链式分配给代理链上所有店铺"。
#### Scenario: 计算链式分配金额
- **WHEN** 触发一次性佣金
- **AND** 代理链为 平台 → A → A1 → A2
- **AND** 系列规则返20元
- **AND** A能拿到20元给A1设置8元
- **AND** A1能拿到8元给A2设置5元
- **THEN** A2实际获得 = 5元
- **AND** A1实际获得 = 8 - 5 = 3元
- **AND** A实际获得 = 20 - 8 = 12元
#### Scenario: 末端代理全额获得
- **WHEN** 触发一次性佣金
- **AND** A1是末端代理无下级
- **AND** A1能拿到10元
- **THEN** A1实际获得 = 10元全额
#### Scenario: 独吞场景
- **WHEN** 触发一次性佣金
- **AND** A给A1设置的一次性佣金金额为0元
- **THEN** A1实际获得 = 0元
- **AND** A实际获得 = 自己能拿到的全部金额
---
### Requirement: 梯度佣金计算
系统 SHALL 根据代理当前销量/销售额所在梯度档位计算一次性佣金金额。
**变更说明**:新增统计范围开关(仅自己/自己+下级),梯度升级后上级获得增量。
#### Scenario: 按梯度计算
- **WHEN** 触发一次性佣金
- **AND** 代理A当前销量150适用">=100返10元"档位)
- **AND** A给A1设置5元
- **THEN** A1实际获得 = 5元
- **AND** A实际获得 = 10 - 5 = 5元
#### Scenario: 梯度升级
- **WHEN** 代理A销量从150升到210
- **AND** 适用档位从">=100返10元"变为">=200返20元"
- **AND** A给A1设置仍为5元
- **THEN** A1实际获得 = 5元不变
- **AND** A实际获得 = 20 - 5 = 15元增量归上级
#### Scenario: 统计范围-仅自己
- **WHEN** 梯度配置 `stat_scope = self`
- **THEN** 只统计该代理直接产生的销量/销售额
#### Scenario: 统计范围-自己+下级
- **WHEN** 梯度配置 `stat_scope = self_and_sub`
- **THEN** 统计该代理及所有下级代理的销量/销售额之和
---
### Requirement: 差价佣金计算
差价佣金计算规则不变:上级代理的佣金 = 下级成本价 - 自己成本价。
#### Scenario: 差价佣金计算
- **WHEN** 代理A1销售一单
- **AND** A的成本价120元A1的成本价130元
- **THEN** A的差价佣金 = 130 - 120 = 10元

View File

@@ -0,0 +1,61 @@
# 一次性佣金链式分配
## ADDED Requirements
### Requirement: 分配时设置下级一次性佣金金额
系统 SHALL 允许上级代理在分配套餐时设置给下级的一次性佣金金额。该金额 MUST 小于等于上级自己能拿到的一次性佣金金额,且 MUST 大于等于 0。
#### Scenario: 设置有效的下级佣金金额
- **WHEN** 代理A分配套餐给代理A1设置一次性佣金金额为 10 元
- **AND** 代理A自己能拿到的一次性佣金为 20 元
- **THEN** 系统 SHALL 保存该配置
- **AND** 代理A1的一次性佣金金额记录为 10 元
#### Scenario: 设置超额的下级佣金金额
- **WHEN** 代理A分配套餐给代理A1设置一次性佣金金额为 25 元
- **AND** 代理A自己能拿到的一次性佣金为 20 元
- **THEN** 系统 SHALL 拒绝该配置
- **AND** 返回错误"给下级的一次性佣金不能超过自己能拿到的金额"
#### Scenario: 设置零佣金(独吞)
- **WHEN** 代理A分配套餐给代理A1设置一次性佣金金额为 0 元
- **THEN** 系统 SHALL 保存该配置
- **AND** 代理A1的一次性佣金金额记录为 0 元
### Requirement: 链式佣金分配计算
当一次性佣金触发时,系统 SHALL 沿代理链向上计算并分配佣金。每级代理实际获得的佣金 = 自己能拿到的金额 - 给下级的金额。
#### Scenario: 三级代理链佣金分配
- **WHEN** 代理链为 平台 → A → A1 → A2
- **AND** 系列规则首充100返20元
- **AND** 平台给A设置20元
- **AND** A给A1设置8元
- **AND** A1给A2设置5元
- **AND** A2的客户触发首充
- **THEN** A2 SHALL 获得 5 元
- **AND** A1 SHALL 获得 8 - 5 = 3 元
- **AND** A SHALL 获得 20 - 8 = 12 元
- **AND** 总分配金额 = 20 元
#### Scenario: 末端代理无下级
- **WHEN** 代理A1是末端代理无下级
- **AND** A1的客户触发首充
- **AND** A1能拿到的一次性佣金为 10 元
- **THEN** A1 SHALL 获得完整的 10 元
### Requirement: 代理只能看到自己的一次性佣金金额
代理查看套餐列表时,系统 SHALL 只返回该代理能拿到的一次性佣金金额,不得返回系列规则的总金额或其他代理的配置。
#### Scenario: 代理A查看套餐
- **WHEN** 代理A调用套餐列表接口
- **AND** 该套餐的系列规则为首充100返20元
- **AND** 平台给A设置的一次性佣金为 15 元
- **THEN** 返回的一次性佣金金额 SHALL 为 15 元
- **AND** 不得返回 20 元(总规则)
#### Scenario: 平台查看套餐
- **WHEN** 平台管理员调用套餐列表接口
- **THEN** 返回的一次性佣金 SHALL 显示完整规则首充100返20元

View File

@@ -0,0 +1,72 @@
# 强充检查变更
## MODIFIED Requirements
### Requirement: 首充强充金额计算
当系列启用首充一次性佣金时,强充金额 SHALL 为 max(首充要求, 套餐售价)。
**变更说明**:明确首充强充金额的计算公式。
#### Scenario: 首充要求小于套餐价格
- **WHEN** 首充要求50元套餐售价100元
- **THEN** 强充金额 = 100元取套餐价格
#### Scenario: 首充要求等于套餐价格
- **WHEN** 首充要求100元套餐售价100元
- **THEN** 强充金额 = 100元
#### Scenario: 首充要求大于套餐价格
- **WHEN** 首充要求200元套餐售价100元
- **THEN** 强充金额 = 200元取首充要求
---
### Requirement: 累计充值强充金额计算
当系列启用累计充值一次性佣金且启用强充时,系统 SHALL 支持两种强充金额计算方式:固定金额和动态差额。
**变更说明**:新增强充金额计算方式开关。
#### Scenario: 固定金额模式
- **WHEN** 强充配置 `force_calc_type = fixed`
- **AND** `force_amount = 10000`100元
- **THEN** 强充金额 = 100元固定值
#### Scenario: 动态差额模式
- **WHEN** 强充配置 `force_calc_type = dynamic`
- **AND** 累计要求200元
- **AND** 当前已累计80元
- **THEN** 强充金额 = 200 - 80 = 120元差额
#### Scenario: 动态差额已达标
- **WHEN** 强充配置 `force_calc_type = dynamic`
- **AND** 累计要求200元
- **AND** 当前已累计250元
- **THEN** 强充金额 = 0元已达标无需强充
---
### Requirement: 强充流程
强充流程保持不变:先创建充值订单,支付成功后钱进入钱包,然后自动扣款购买套餐。
#### Scenario: 首充强充流程
- **WHEN** 客户购买套餐触发首充强充
- **AND** 强充金额200元套餐售价100元
- **THEN** 创建充值订单200元
- **AND** 支付成功后钱包余额+200
- **AND** 标记首充状态为"已触发"
- **AND** 自动创建套餐购买订单并扣款100元
- **AND** 触发首充返佣(按链式分配)
- **AND** 钱包剩余100元
#### Scenario: 累计充值强充流程
- **WHEN** 客户购买套餐触发累计充值强充
- **AND** 强充金额120元套餐售价100元
- **THEN** 创建充值订单120元
- **AND** 支付成功后钱包余额+120
- **AND** 累计金额 += 120
- **AND** 自动创建套餐购买订单并扣款100元
- **AND** 如果累计达标则触发返佣
- **AND** 钱包剩余20元

View File

@@ -0,0 +1,99 @@
# 一次性佣金触发变更
## MODIFIED Requirements
### Requirement: 一次性充值触发佣金
系统 SHALL 支持"首充"触发条件:当该卡/设备在该套餐系列下首次充值且金额 ≥ 配置阈值时触发一次性佣金。
**变更说明**:将 `single_recharge` 重命名为 `first_recharge`(首充),强调是"第一次"充值而非"单次"充值。
#### Scenario: 首充达到阈值
- **WHEN** IoT卡在该系列下首次充值 500 元
- **AND** 配置阈值 300 元
- **AND** 该卡在该系列下未触发过首充返佣
- **THEN** 系统按链式分配规则发放一次性佣金
- **AND** 标记该卡在该系列下的首充状态为"已触发"
#### Scenario: 首充未达到阈值
- **WHEN** IoT卡在该系列下首次充值 200 元
- **AND** 配置阈值 300 元
- **THEN** 系统不发放一次性佣金
- **AND** 首充状态保持"未触发"
#### Scenario: 非首次充值
- **WHEN** IoT卡在该系列下已触发过首充
- **AND** 再次充值 500 元(≥阈值)
- **THEN** 系统不发放一次性佣金
---
### Requirement: 累计充值触发佣金
系统 SHALL 支持"累计充值"触发条件:当卡/设备在该套餐系列下的累计充值金额 ≥ 配置阈值时触发一次性佣金。
**变更说明**:累计范围改为按"套餐系列"累计,而非全局累计。只有充值操作累计,直接购买套餐不累计。
#### Scenario: 累计达到阈值
- **WHEN** IoT卡在该系列下之前累计充值 200 元
- **AND** 本次充值 150 元
- **AND** 配置阈值 300 元
- **THEN** 系统更新该系列的累计充值为 350 元
- **AND** 累计 350 元 ≥ 300 元,系统按链式分配规则发放一次性佣金
- **AND** 标记该卡在该系列下已触发累计充值返佣
#### Scenario: 累计未达到阈值
- **WHEN** IoT卡在该系列下累计充值为 100 元
- **AND** 本次充值 100 元
- **AND** 配置阈值 300 元
- **THEN** 系统更新累计充值为 200 元
- **AND** 累计 200 元 < 300 元,系统不发放一次性佣金
#### Scenario: 直接购买不累计
- **WHEN** IoT卡直接购买套餐不经过充值
- **THEN** 该系列的累计充值金额不变
---
### Requirement: 一次性佣金只发放一次
每张卡/设备在每个套餐系列下,一次性佣金 SHALL 只发放一次,无论是首充还是累计充值触发。通过按系列追踪的状态字段控制。
**变更说明**:首充状态和累计充值触发状态改为按套餐系列追踪,不同系列互不影响。
#### Scenario: 首次触发
- **WHEN** 首次满足触发条件(首充或累计充值)
- **THEN** 按链式分配规则发放佣金
- **AND** 设置该系列的触发状态为 true
#### Scenario: 再次满足条件
- **WHEN** 再次满足触发条件
- **AND** 该系列的触发状态已为 true
- **THEN** 不发放佣金
#### Scenario: 不同系列独立
- **WHEN** IoT卡在系列1已触发一次性佣金
- **AND** IoT卡首次满足系列2的触发条件
- **THEN** 系统发放系列2的一次性佣金如果规则启用
---
### Requirement: 一次性佣金发放对象
一次性佣金 SHALL 按链式分配规则发放给代理链上的所有相关店铺。
**变更说明**:从"发放给直接归属店铺"改为"链式分配给代理链上所有店铺"。
#### Scenario: 链式发放
- **WHEN** IoT卡归属代理A1
- **AND** 代理链为 平台 → A → A1
- **AND** 触发一次性佣金系列规则返20元
- **AND** A能拿到20元给A1设置10元
- **THEN** A1获得10元
- **AND** A获得20-10=10元
## RENAMED Requirements
### Requirement: single_recharge 触发类型
- **FROM**: `single_recharge`(单次充值)
- **TO**: `first_recharge`(首充)

View File

@@ -0,0 +1,54 @@
# 一次性佣金时效管理
## ADDED Requirements
### Requirement: 时效类型配置
系统 SHALL 支持三种一次性佣金时效类型永久permanent、固定日期fixed_date、相对时长relative
#### Scenario: 配置永久有效
- **WHEN** 配置一次性佣金规则时设置 `validity_type = permanent`
- **THEN** 系统 SHALL 保存该配置
- **AND** 该规则永久有效,不会过期
#### Scenario: 配置固定到期日期
- **WHEN** 配置一次性佣金规则时设置 `validity_type = fixed_date`
- **AND** `validity_value = "2025-12-31"`
- **THEN** 系统 SHALL 保存该配置
- **AND** 该规则在 2025-12-31 23:59:59 后失效
#### Scenario: 配置相对时长
- **WHEN** 配置一次性佣金规则时设置 `validity_type = relative`
- **AND** `validity_value = "3"` (表示 3 个月)
- **THEN** 系统 SHALL 保存该配置
- **AND** 该规则从创建时间起 3 个月后失效
### Requirement: 过期规则不触发返佣
当一次性佣金规则过期时,系统 SHALL 不再触发返佣,即使满足触发条件(首充/累计充值)。
#### Scenario: 规则过期后首充
- **WHEN** 一次性佣金规则已过期
- **AND** 客户首充达到阈值
- **THEN** 系统 SHALL 不触发一次性佣金
- **AND** 正常完成充值和套餐购买
#### Scenario: 规则有效期内首充
- **WHEN** 一次性佣金规则在有效期内
- **AND** 客户首充达到阈值
- **THEN** 系统 SHALL 触发一次性佣金
- **AND** 按链式分配规则分配佣金
### Requirement: 梯度统计周期与时效一致
当一次性佣金使用梯度模式时,梯度统计周期(销量/销售额SHALL 与一次性佣金时效一致。时效结束后统计归零。
#### Scenario: 时效内统计
- **WHEN** 一次性佣金时效为 3 个月
- **AND** 使用销量梯度
- **THEN** 销量统计 SHALL 只计算这 3 个月内的销量
#### Scenario: 时效结束后统计重置
- **WHEN** 一次性佣金时效到期
- **AND** 配置了新的一次性佣金时效
- **THEN** 销量/销售额统计 SHALL 从零开始

View File

@@ -0,0 +1,74 @@
# 套餐管理变更
## MODIFIED Requirements
### Requirement: 创建套餐
系统 SHALL 允许平台管理员创建套餐,包含套餐编码、套餐名称、所属系列、套餐类型、时长、流量配置(真流量必填、虚流量可选)、成本价和建议售价。套餐编码 MUST 全局唯一(排除已删除记录)。新创建的套餐默认为启用状态(1)和下架状态(2)。
**变更说明**:移除 `price``data_type``data_amount_mb` 参数,新增 `enable_virtual_data` 参数。
#### Scenario: 成功创建套餐
- **WHEN** 管理员提交有效的套餐信息
- **AND** 包含 `cost_price``suggested_retail_price``real_data_mb`
- **THEN** 系统创建套餐记录,状态为启用(1),上架状态为下架(2),返回创建的套餐详情
#### Scenario: 创建带虚流量的套餐
- **WHEN** 管理员提交套餐信息
- **AND** `enable_virtual_data = true`
- **AND** `virtual_data_mb = 800`
- **AND** `real_data_mb = 1000`
- **THEN** 系统创建套餐记录,虚流量配置正确保存
#### Scenario: 套餐编码重复
- **WHEN** 管理员提交的套餐编码已存在(未删除)
- **THEN** 系统返回错误 "套餐编码已存在"
#### Scenario: 关联不存在的套餐系列
- **WHEN** 管理员指定的系列 ID 不存在
- **THEN** 系统返回错误 "套餐系列不存在"
#### Scenario: 缺少必填字段
- **WHEN** 管理员未提供必填字段(套餐编码、套餐名称、套餐类型、时长、成本价、真流量)
- **THEN** 系统返回参数验证错误
---
### Requirement: Package 模型新增字段
系统 MUST 在 Package 模型中调整以下字段:
- 移除 `price` 字段
- 移除 `data_type` 字段
- 移除 `data_amount_mb` 字段
- 保留 `suggested_cost_price` 并重命名为 `cost_price`:成本价(分为单位)
- 保留 `suggested_retail_price`:建议售价(分为单位)
- 新增 `enable_virtual_data`:是否启用虚流量,布尔值,默认 false
- 保留 `real_data_mb`:真实流量(必填)
- 保留 `virtual_data_mb`:虚流量(启用时必填)
- 保留 `shelf_status`上架状态1-上架 2-下架,默认 2
#### Scenario: 创建套餐时设置价格
- **WHEN** 管理员创建套餐并设置成本价和建议售价
- **THEN** 系统保存 `cost_price``suggested_retail_price`
#### Scenario: 查询套餐时返回价格
- **WHEN** 管理员查询套餐详情或列表
- **THEN** 响应中包含 `cost_price``suggested_retail_price``shelf_status``enable_virtual_data` 字段
- **AND** 不再返回 `price``data_type``data_amount_mb` 字段
## REMOVED Requirements
### Requirement: price 字段
**Reason**: `price` 字段语义不清,与 `suggested_cost_price``suggested_retail_price` 混淆
**Migration**: 使用 `cost_price`(成本价)和 `suggested_retail_price`(建议售价)替代
### Requirement: data_type 字段
**Reason**: `data_type` 暗示真流量/虚流量二选一,但业务需求是共存
**Migration**: 使用 `enable_virtual_data` 开关控制是否启用虚流量
### Requirement: data_amount_mb 字段
**Reason**: 语义不清,不知道是真流量还是虚流量
**Migration**: 使用 `real_data_mb`(真流量)和 `virtual_data_mb`(虚流量)明确区分

View File

@@ -0,0 +1,55 @@
# 套餐系列管理变更
## MODIFIED Requirements
### Requirement: 套餐系列一次性佣金规则配置
系统 SHALL 在套餐系列层面配置一次性佣金的完整规则,包括触发条件、阈值、金额/梯度、时效、强充配置。
**变更说明**:一次性佣金规则从分配时配置改为在系列层面统一定义。分配时只设置"给下级多少"。
#### Scenario: 配置首充规则
- **WHEN** 创建或更新套餐系列
- **AND** 设置一次性佣金规则:`trigger_type = first_recharge``threshold = 10000`100元`commission_amount = 2000`20元
- **THEN** 系统保存该规则配置
#### Scenario: 配置累计充值规则
- **WHEN** 创建或更新套餐系列
- **AND** 设置一次性佣金规则:`trigger_type = accumulated_recharge``threshold = 20000`200元`commission_amount = 4000`40元
- **THEN** 系统保存该规则配置
#### Scenario: 配置梯度规则
- **WHEN** 创建或更新套餐系列
- **AND** 设置一次性佣金规则:`commission_type = tiered`
- **AND** 梯度配置:销量>=0返5元>=100返10元>=200返20元
- **THEN** 系统保存梯度配置
#### Scenario: 配置时效
- **WHEN** 创建或更新套餐系列
- **AND** 设置时效:`validity_type = relative``validity_value = 3`3个月
- **THEN** 系统保存时效配置
- **AND** 该规则在3个月后失效
---
### Requirement: PackageSeries 模型新增字段
系统 MUST 在 PackageSeries 模型中新增一次性佣金规则配置字段:
**新增字段**(使用 JSONB 存储):
- `one_time_commission_config`一次性佣金规则配置JSON
- `enable`:是否启用
- `trigger_type`触发类型first_recharge / accumulated_recharge
- `threshold`:触发阈值(分)
- `commission_type`返佣类型fixed / tiered
- `commission_amount`:固定返佣金额(分)
- `tiers`:梯度配置数组
- `validity_type`时效类型permanent / fixed_date / relative
- `validity_value`:时效值
- `enable_force_recharge`:是否启用强充
- `force_calc_type`强充金额计算方式fixed / dynamic
- `force_amount`强充金额fixed类型时
#### Scenario: 查询系列详情包含规则
- **WHEN** 查询套餐系列详情
- **THEN** 返回完整的一次性佣金规则配置

View File

@@ -0,0 +1,62 @@
# 套餐真流量/虚流量共存机制
## ADDED Requirements
### Requirement: 真流量必填
创建或更新套餐时,系统 SHALL 要求 `real_data_mb`(真实流量额度)为必填字段,且 MUST 大于 0。
#### Scenario: 创建套餐时提供真流量
- **WHEN** 创建套餐请求包含 `real_data_mb = 1000`
- **THEN** 系统 SHALL 保存该套餐
- **AND** `real_data_mb` 记录为 1000 MB
#### Scenario: 创建套餐时缺少真流量
- **WHEN** 创建套餐请求未提供 `real_data_mb` 字段
- **THEN** 系统 SHALL 拒绝请求
- **AND** 返回参数验证失败错误
### Requirement: 虚流量可选开关
系统 SHALL 提供 `enable_virtual_data` 开关控制是否启用虚流量。启用时 MUST 提供 `virtual_data_mb`,且该值 MUST 小于等于 `real_data_mb`
#### Scenario: 启用虚流量
- **WHEN** 创建套餐请求包含 `enable_virtual_data = true`
- **AND** `virtual_data_mb = 800`
- **AND** `real_data_mb = 1000`
- **THEN** 系统 SHALL 保存该配置
- **AND** `enable_virtual_data` 记录为 true
- **AND** `virtual_data_mb` 记录为 800 MB
#### Scenario: 启用虚流量但未提供额度
- **WHEN** 创建套餐请求包含 `enable_virtual_data = true`
- **AND** 未提供 `virtual_data_mb`
- **THEN** 系统 SHALL 拒绝请求
- **AND** 返回"启用虚流量时必须提供虚流量额度"错误
#### Scenario: 虚流量超过真流量
- **WHEN** 创建套餐请求包含 `enable_virtual_data = true`
- **AND** `virtual_data_mb = 1200`
- **AND** `real_data_mb = 1000`
- **THEN** 系统 SHALL 拒绝请求
- **AND** 返回"虚流量不能超过真实流量"错误
#### Scenario: 不启用虚流量
- **WHEN** 创建套餐请求包含 `enable_virtual_data = false`
- **THEN** 系统 SHALL 保存该配置
- **AND** `virtual_data_mb` 可为空或忽略
### Requirement: 停机判断目标值
轮询停机模块在判断是否停机时,系统 SHALL 根据 `enable_virtual_data` 选择目标值:启用虚流量时使用 `virtual_data_mb`,否则使用 `real_data_mb`
#### Scenario: 启用虚流量的停机判断
- **WHEN** 套餐配置 `enable_virtual_data = true`
- **AND** `virtual_data_mb = 800`
- **AND** `real_data_mb = 1000`
- **THEN** 停机判断的目标流量值 SHALL 为 800 MB
#### Scenario: 未启用虚流量的停机判断
- **WHEN** 套餐配置 `enable_virtual_data = false`
- **AND** `real_data_mb = 1000`
- **THEN** 停机判断的目标流量值 SHALL 为 1000 MB

View File

@@ -0,0 +1,53 @@
# 店铺佣金梯度变更
## MODIFIED Requirements
### Requirement: 梯度统计范围开关
系统 SHALL 支持配置梯度佣金的统计范围仅自己self或自己+下级self_and_sub
**变更说明**:新增 `stat_scope` 字段,允许配置统计是否包含下级代理的销量/销售额。
#### Scenario: 配置统计范围-仅自己
- **WHEN** 配置梯度规则时设置 `stat_scope = self`
- **THEN** 系统保存该配置
- **AND** 计算梯度时只统计该代理直接产生的销量/销售额
#### Scenario: 配置统计范围-自己+下级
- **WHEN** 配置梯度规则时设置 `stat_scope = self_and_sub`
- **THEN** 系统保存该配置
- **AND** 计算梯度时统计该代理及所有下级代理的销量/销售额之和
---
### Requirement: 梯度统计周期
梯度佣金的统计周期 SHALL 与一次性佣金时效一致。时效结束后统计归零。
**变更说明**:统计周期从独立配置改为与一次性佣金时效绑定。
#### Scenario: 时效内统计
- **WHEN** 一次性佣金时效为3个月relative = 3
- **THEN** 销量/销售额统计只计算这3个月内的数据
#### Scenario: 时效结束统计重置
- **WHEN** 一次性佣金时效到期
- **AND** 配置了新的时效周期
- **THEN** 销量/销售额统计从零开始
#### Scenario: 永久时效
- **WHEN** 一次性佣金时效为永久permanent
- **THEN** 销量/销售额永久累计,不会重置
---
### Requirement: ShopSeriesOneTimeCommissionTier 模型新增字段
系统 MUST 在 ShopSeriesOneTimeCommissionTier 模型中新增统计范围字段:
**新增字段**
- `stat_scope`统计范围varchar(20),可选值 `self`/`self_and_sub`,默认 `self`
#### Scenario: 查询梯度配置包含统计范围
- **WHEN** 查询梯度配置详情
- **THEN** 返回包含 `stat_scope` 字段

View File

@@ -0,0 +1,71 @@
# 店铺系列分配变更
## MODIFIED Requirements
### Requirement: 创建店铺系列分配
系统 SHALL 允许上级店铺为下级店铺分配套餐系列。分配时配置基础返佣和一次性佣金金额(给下级的金额)。
**变更说明**移除完整的一次性佣金配置字段type、trigger、threshold、mode、value 等),改为只配置"给被分配店铺的一次性佣金金额"。一次性佣金规则从套餐系列获取。
#### Scenario: 创建分配并设置一次性佣金金额
- **WHEN** 平台给代理A分配系列
- **AND** 系列启用一次性佣金规则为首充100返20元
- **AND** 设置给A的一次性佣金金额为20元
- **THEN** 系统创建分配记录
- **AND** `one_time_commission_amount` 记录为 2000
#### Scenario: 设置超额的一次性佣金金额
- **WHEN** 代理A给代理A1分配系列
- **AND** A自己能拿到的一次性佣金为15元
- **AND** 设置给A1的一次性佣金金额为20元
- **THEN** 系统拒绝该配置
- **AND** 返回错误"给下级的一次性佣金不能超过自己能拿到的金额"
#### Scenario: 系列未启用一次性佣金
- **WHEN** 分配的系列未启用一次性佣金
- **THEN** 不需要设置 `one_time_commission_amount`
- **AND** 该字段默认为 0
---
### Requirement: ShopSeriesAllocation 模型字段调整
系统 MUST 调整 ShopSeriesAllocation 模型字段:
**保留字段**
- `shop_id`被分配的店铺ID
- `series_id`套餐系列ID
- `allocator_shop_id`分配者店铺ID
- `base_commission_mode`:基础返佣模式
- `base_commission_value`:基础返佣值
- `status`:状态
**新增字段**
- `one_time_commission_amount`给被分配店铺的一次性佣金金额默认0
**移除字段**
- `enable_one_time_commission`
- `one_time_commission_type`
- `one_time_commission_trigger`
- `one_time_commission_threshold`
- `one_time_commission_mode`
- `one_time_commission_value`
- `enable_force_recharge`
- `force_recharge_amount`
- `force_recharge_trigger_type`
#### Scenario: 查询分配详情
- **WHEN** 查询店铺系列分配详情
- **THEN** 返回 `one_time_commission_amount`(给该店铺的一次性佣金金额)
- **AND** 不再返回完整的一次性佣金配置字段
## REMOVED Requirements
### Requirement: 分配时配置完整一次性佣金规则
**Reason**: 一次性佣金规则应在套餐系列层面定义,分配时只需设置"给下级多少"
**Migration**:
1. 一次性佣金规则(触发条件、阈值、金额/梯度)移到套餐系列的配置中
2. 分配时只配置 `one_time_commission_amount`(给该代理的金额)
3. 迁移脚本将现有 `one_time_commission_value` 迁移到新字段