Files
junhong_cmp_fiber/openspec/specs/agent-series-grant/spec.md
huang b52cb9a078
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
fix: 修复梯度佣金档位字段缺失,补全授权接口响应字段及强充有效状态
- OneTimeCommissionTierDTO 补充 operator 字段映射
- GrantCommissionTierItem 补充 dimension/stat_scope 字段(从全局配置合并)
- 系列授权列表/详情补充强充锁定状态和强充金额的有效值计算
- 同步 OpenSpec 主规范并归档变更文档
2026-03-05 11:23:28 +08:00

225 lines
13 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.
# Capability: 代理系列授权管理
## Purpose
定义"系列授权"Series Grant的 CRUD 操作。系列授权将原本割裂的"系列分配"和"套餐分配"合并为一个原子操作,一次请求完成:授权代理可销售某系列下的指定套餐、设定每个套餐的成本价、配置一次性佣金(固定模式:单值天花板;梯度模式:每档位上限金额列表)和代理自设强充(平台未设时)。
底层仍使用 `tb_shop_series_allocation``tb_shop_package_allocation` 两张表,对外以 `ShopSeriesAllocation.ID` 作为 grant 主键。
---
## Requirements
### Requirement: 创建系列授权(固定模式)
系统 SHALL 提供 `POST /shop-series-grants` 接口,在一次请求中原子性创建系列分配和套餐分配列表。固定模式下 `one_time_commission_amount` MUST 必填,且不得超过分配者自身的天花板。
#### Scenario: 代理成功创建固定模式授权
- **WHEN** 代理A自身天花板=80元为直属下级代理B 创建系列授权commission_type=fixedone_time_commission_amount=500050元packages=[{package_id:1, cost_price:3000}, {package_id:2, cost_price:5000}]
- **THEN** 系统在事务中创建 1 条 ShopSeriesAllocationone_time_commission_amount=5000和 2 条 ShopPackageAllocation响应返回包含 packages 列表的聚合视图
#### Scenario: 代理B 已存在此系列授权,重复创建
- **WHEN** 代理A 为代理B 创建系列授权但代理B 在此系列下已有 active 授权记录
- **THEN** 系统返回错误"该代理已存在此系列授权"
#### Scenario: 分配者自身无此系列授权
- **WHEN** 代理A 自身未被授权此套餐系列尝试为代理B 创建此系列授权
- **THEN** 系统返回错误"当前账号无此系列授权,无法向下分配"
#### Scenario: 平台成功创建固定模式授权
- **WHEN** 平台管理员为一级代理创建系列授权commission_type=fixedone_time_commission_amount=800080元系列总额 commission_amount=10000100元
- **THEN** 系统创建授权,响应中 allocator_shop_id=0allocator_shop_name="平台"
#### Scenario: 固定模式 one_time_commission_amount 为必填
- **WHEN** 请求中不包含 one_time_commission_amount
- **THEN** 系统返回参数错误"固定模式下一次性佣金额度为必填项"
#### Scenario: 金额超过代理自身天花板
- **WHEN** 代理A天花板=8000分为代理B 创建授权one_time_commission_amount=10000
- **THEN** 系统返回错误"一次性佣金额度不能超过上级限额"
#### Scenario: 平台金额超过系列总额
- **WHEN** 平台为代理A 创建授权one_time_commission_amount=12000但 PackageSeries.commission_amount=10000
- **THEN** 系统返回错误"一次性佣金额度不能超过套餐系列设定的总额"
---
### Requirement: 创建系列授权(梯度模式)
系统 SHALL 支持梯度模式的系列授权创建。梯度模式下,`commission_tiers` MUST 为必填,且必须包含与 PackageSeries 完全相同数量和阈值的阶梯(不多不少)。若某档位不希望给下级佣金,应将该档位的 amount 设为 0不可省略该档位。创建成功后的响应中`commission_tiers` 每个档位 MUST 包含 `operator``dimension``stat_scope` 字段(从全局配置合并)。
#### Scenario: 代理成功创建梯度模式授权
- **WHEN** 代理A 的专属阶梯为 `[{operator:">=" , threshold:100, amount:80}, {operator:">=" , threshold:150, amount:120}]`A 为代理B 创建授权,传入 `commission_tiers=[{threshold:100, amount:50}, {threshold:150, amount:100}]`
- **THEN** 系统创建授权,`commission_tiers_json` 存储 `[{threshold:100, amount:50}, {threshold:150, amount:100}]`
- **AND** 响应中 `commission_tiers=[{operator:">=" , dimension:"sales_count", stat_scope:"self", threshold:100, amount:50}, ...]`
#### Scenario: 平台成功创建梯度模式授权
- **WHEN** 平台为顶级代理A 创建授权PackageSeries 阶梯含 `operator`/`dimension`/`stat_scope`
- **THEN** 系统创建授权,响应中 `commission_tiers` 包含 PackageSeries 全局 `operator``dimension``stat_scope`
#### Scenario: 梯度模式某档位金额超过父级
- **WHEN** 代理A 的阶梯第一档 `amount=80`A 为 B 创建授权时传入第一档 `amount=90`
- **THEN** 系统返回错误“某档位佣金金额超过上级天花板”
#### Scenario: 梯度模式传入了不存在的阈值
- **WHEN** PackageSeries 只有 `threshold=100``150` 两档,请求中传入 `threshold=200`
- **THEN** 系统返回错误“梯度阶梯 threshold 与系列配置不匹配”
#### Scenario: 梯度模式 commission_tiers 为必填
- **WHEN** 请求中不包含 `commission_tiers` 或为空数组
- **THEN** 系统返回参数错误“梯度模式必须填写阶梯配置”
---
### Requirement: 强充配置的平台/代理层级
创建系列授权时,系统 SHALL 根据 PackageSeries 的触发类型和强充设置决定代理是否可自设强充。
#### Scenario: 首次充值触发类型,强充不可配置
- **WHEN** PackageSeries.trigger_type=first_recharge代理创建授权时传入任意 enable_force_recharge 值
- **THEN** 系统忽略代理的强充设置,响应中 force_recharge_locked=true首次充值本身即为强充机制无需额外配置
#### Scenario: 累计充值触发类型,平台已设强充,代理配置被忽略
- **WHEN** PackageSeries.trigger_type=accumulated_recharge 且 enable_force_recharge=true代理创建授权时传入 enable_force_recharge=false
- **THEN** 系统忽略代理的强充设置,响应中 force_recharge_locked=true
#### Scenario: 累计充值触发类型,平台未设强充,代理可自设
- **WHEN** PackageSeries.trigger_type=accumulated_recharge 且 enable_force_recharge=false代理创建授权时传入 enable_force_recharge=trueforce_recharge_amount=10000
- **THEN** 系统保存代理的强充配置,响应中 force_recharge_locked=falseforce_recharge_enabled=trueforce_recharge_amount=10000
---
### Requirement: 查询系列授权详情
系统 SHALL 提供 `GET /shop-series-grants/:id` 接口,返回包含套餐列表的聚合视图。梯度模式下,`commission_tiers` 中每个档位 MUST 包含 `dimension`(统计维度)和 `stat_scope`(统计范围)字段,这两个字段从 PackageSeries 全局配置按 `threshold` 合并,对代理只读。
#### Scenario: 固定模式详情
- **WHEN** 查询固定模式系列授权详情
- **THEN** 响应包含 `commission_type="fixed"``one_time_commission_amount=有效值``commission_tiers=[]`
#### Scenario: 梯度模式详情
- **WHEN** 查询梯度模式系列授权详情
- **THEN** 响应包含 `commission_type="tiered"``one_time_commission_amount=0`
- **AND** `commission_tiers` 中每个档位包含 `operator``dimension``stat_scope``threshold``amount` 五个字段
- **AND** `operator``dimension``stat_scope` 的值来自 PackageSeries 全局配置(对应 threshold 的档位),代理的 `amount` 来自 `ShopSeriesAllocation.commission_tiers_json`
#### Scenario: 梯度模式 dimension 为销售量
- **WHEN** 查询梯度模式授权详情PackageSeries 阶梯 `dimension = "sales_count"`
- **THEN** 响应中对应档位 `dimension = "sales_count"`,前端展示“销售量”条件
#### Scenario: 梯度模式 dimension 为销售额
- **WHEN** 查询梯度模式授权详情PackageSeries 阶梯 `dimension = "sales_amount"`
- **THEN** 响应中对应档位 `dimension = "sales_amount"`,前端展示“销售额”条件
#### Scenario: 梯度模式 stat_scope 区分
- **WHEN** 查询梯度模式授权详情
- **THEN** 响应中 `stat_scope` 正确反映 PackageSeries 配置的统计范围(`"self"``"self_and_sub"`
#### Scenario: 查询不存在的授权
- **WHEN** 查询不存在的授权 ID
- **THEN** 系统返回错误“授权记录不存在”
---
### Requirement: 查询系列授权列表
系统 SHALL 提供 `GET /shop-series-grants` 接口,支持分页和多维度筛选,响应内嵌套餐数量摘要(不含完整套餐列表)。
#### Scenario: 列表查询支持按店铺和系列筛选
- **WHEN** 传入 shop_id、series_id、allocator_shop_id 等筛选条件
- **THEN** 仅返回符合条件的授权记录,每条记录包含 package_count
---
### Requirement: 更新系列授权配置
系统 SHALL 提供 `PUT /shop-series-grants/:id` 接口,支持更新一次性佣金配置和强充配置。
#### Scenario: 固定模式更新佣金额度
- **WHEN** 更新 one_time_commission_amount新值不超过分配者天花板
- **THEN** 系统更新成功
#### Scenario: 梯度模式更新阶梯金额
- **WHEN** 更新 commission_tiers每档位金额不超过分配者同档位上限
- **THEN** 系统更新 commission_tiers_json 字段
#### Scenario: 更新代理自设强充(平台未设时)
- **WHEN** 平台未设强充,更新 enable_force_recharge=trueforce_recharge_amount=10000
- **THEN** 系统更新成功,后续该代理渠道下的客户须满足强充要求
---
### Requirement: 管理授权内套餐
系统 SHALL 提供 `PUT /shop-series-grants/:id/packages` 接口,支持添加套餐、移除套餐、更新成本价,操作在事务中完成,成功后返回 HTTP 200无需返回完整授权视图
#### Scenario: 向授权中添加新套餐
- **WHEN** 请求包含新的 package_id 和 cost_price且该套餐属于此系列
- **THEN** 系统创建新的 ShopPackageAllocation
#### Scenario: 更新套餐成本价
- **WHEN** 请求中套餐的 cost_price 与当前值不同
- **THEN** 系统更新 cost_price 并写价格历史记录
#### Scenario: 移除授权中的套餐
- **WHEN** 请求中某套餐标记 remove=true且该套餐在当前授权中存在
- **THEN** 系统软删除对应的 ShopPackageAllocation
#### Scenario: remove=true 但套餐已不在授权中
- **WHEN** 请求中某套餐标记 remove=true但该套餐已被软删除或从未在此授权中
- **THEN** 系统静默忽略该条目,不报错,继续处理其他条目
#### Scenario: 重新添加曾被移除的套餐
- **WHEN** 某套餐曾经被软删除,请求中再次包含该 package_id无 remove 标志)
- **THEN** 系统创建一条新的 ShopPackageAllocation 记录,不恢复旧记录
#### Scenario: 添加不属于该系列的套餐
- **WHEN** 请求中包含不属于该系列的 package_id
- **THEN** 系统返回错误"套餐不属于该系列,无法添加到此授权"
#### Scenario: 添加上级未授权的套餐
- **WHEN** 代理A 尝试添加代理A 自己也未获授权的套餐
- **THEN** 系统返回错误"无权限分配该套餐"
---
### Requirement: 删除系列授权
系统 SHALL 提供 `DELETE /shop-series-grants/:id` 接口,删除时同步软删除所有关联的套餐分配。
#### Scenario: 成功删除无下级依赖的授权
- **WHEN** 删除一个下级代理未基于此授权再分配的记录
- **THEN** 系统软删除 ShopSeriesAllocation 和所有关联的 ShopPackageAllocation
#### Scenario: 有下级依赖时禁止删除
- **WHEN** 删除一个已被下级代理用于创建子授权的记录
- **THEN** 系统返回错误"存在下级依赖,无法删除,请先删除下级授权"
---
### Requirement: 系列授权列表强充状态正确反映
系列授权列表 (`GET /shop-series-grants`) MUST 在每个列表项中返回 `force_recharge_locked`(是否被套餐系列锁定)和 `force_recharge_amount`(强充金额)。`force_recharge_enabled` MUST 反映有效状态:当锁定时为 `true`(无论分配记录自身如何);未锁定时取分配记录的实际设置。
#### Scenario: 套餐系列锁定强充
- **WHEN** 套餐系列配置 `enable_force_recharge=true``trigger_type=first_recharge`,查询列表
- **THEN** 列表项中 `force_recharge_locked=true``force_recharge_enabled=true``force_recharge_amount`=系列配置的 `force_amount`
#### Scenario: 代理自身开启强充(未锁定)
- **WHEN** 套餐系列未锁定强充,分配记录中 `enable_force_recharge=true`,查询列表
- **THEN** 列表项中 `force_recharge_locked=false``force_recharge_enabled=true``force_recharge_amount`=分配记录的实际金额
#### Scenario: 代理未开启强充(未锁定)
- **WHEN** 套餐系列未锁定强充,分配记录中 `enable_force_recharge=false`,查询列表
- **THEN** 列表项中 `force_recharge_locked=false``force_recharge_enabled=false``force_recharge_amount=0`
---
### Requirement: 查询系列授权详情强充有效状态
系列授权详情 (`GET /shop-series-grants/:id`) 中,`force_recharge_enabled` MUST 反映有效状态:当锁定时为 `true``force_recharge_amount` 锁定时应返回系列配置的 `force_amount`
#### Scenario: 锁定强充时详情响应
- **WHEN** 套餐系列锁定强充,查询对应分配记录详情
- **THEN** `force_recharge_locked=true``force_recharge_enabled=true``force_recharge_amount`=系列配置的 `force_amount`