Files
junhong_cmp_fiber/openspec/specs/agent-series-grant/spec.md
huang f40abaf93c docs: 同步 OpenSpec 主规范,新增系列授权 capability 并更新强充预检规范
三个 capability 同步:
- agent-series-grant(新建):定义系列授权 CRUD,覆盖固定/梯度佣金模式和强充层级场景
- force-recharge-check(更新):新增「代理层强充层级判断」Requirement,更新钱包充值和套餐购买预检场景以反映平台/代理层级规则
- shop-series-allocation(更新):在 REMOVED 区域追加三个已废弃接口的文档说明(/shop-series-allocations、/shop-package-allocations、enable_one_time_commission 等字段)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-04 11:37:46 +08:00

182 lines
9.9 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不可省略该档位。
#### 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}],响应中 commission_tiers=[{operator:">=", threshold:100, amount:50}, {operator:">=", threshold:150, amount:100}]operator 从 PackageSeries 读取后合并)
#### Scenario: 平台成功创建梯度模式授权
- **WHEN** 平台为顶级代理A 创建授权PackageSeries 阶梯为 [{operator:">=", threshold:100, amount:100}, {operator:"<", threshold:50, amount:30}],传入 commission_tiers=[{threshold:100, amount:80}, {threshold:50, amount:20}]
- **THEN** 系统创建授权A 的专属阶梯存入 commission_tiers_json响应中 commission_tiers 包含对应的 operator
#### Scenario: 梯度模式某档位金额超过父级
- **WHEN** 代理A 的阶梯第一档 amount=80A 为 B 创建授权时传入第一档 amount=90
- **THEN** 系统返回错误"梯度佣金档位金额不能超过上级同档位限额"
#### Scenario: 梯度模式传入了不存在的阈值
- **WHEN** PackageSeries 只有 threshold=100 和 150 两档,请求中传入 threshold=200
- **THEN** 系统返回错误"阶梯阈值与系列配置不匹配"
#### 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` 接口,返回包含套餐列表的聚合视图。
#### Scenario: 固定模式详情
- **WHEN** 查询固定模式系列授权详情
- **THEN** 响应包含 commission_type="fixed"one_time_commission_amount=有效值commission_tiers=[]
#### Scenario: 梯度模式详情
- **WHEN** 查询梯度模式系列授权详情
- **THEN** 响应包含 commission_type="tiered"one_time_commission_amount=0commission_tiers=[{threshold, amount}, ...]
#### 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** 系统返回错误"存在下级依赖,无法删除,请先删除下级授权"