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>
This commit is contained in:
181
openspec/specs/agent-series-grant/spec.md
Normal file
181
openspec/specs/agent-series-grant/spec.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 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=fixed,one_time_commission_amount=5000(50元),packages=[{package_id:1, cost_price:3000}, {package_id:2, cost_price:5000}]
|
||||
- **THEN** 系统在事务中创建 1 条 ShopSeriesAllocation(one_time_commission_amount=5000)和 2 条 ShopPackageAllocation,响应返回包含 packages 列表的聚合视图
|
||||
|
||||
#### Scenario: 代理B 已存在此系列授权,重复创建
|
||||
- **WHEN** 代理A 为代理B 创建系列授权,但代理B 在此系列下已有 active 授权记录
|
||||
- **THEN** 系统返回错误"该代理已存在此系列授权"
|
||||
|
||||
#### Scenario: 分配者自身无此系列授权
|
||||
- **WHEN** 代理A 自身未被授权此套餐系列,尝试为代理B 创建此系列授权
|
||||
- **THEN** 系统返回错误"当前账号无此系列授权,无法向下分配"
|
||||
|
||||
#### Scenario: 平台成功创建固定模式授权
|
||||
- **WHEN** 平台管理员为一级代理创建系列授权,commission_type=fixed,one_time_commission_amount=8000(80元),系列总额 commission_amount=10000(100元)
|
||||
- **THEN** 系统创建授权,响应中 allocator_shop_id=0,allocator_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=80,A 为 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=true,force_recharge_amount=10000
|
||||
- **THEN** 系统保存代理的强充配置,响应中 force_recharge_locked=false,force_recharge_enabled=true,force_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=0,commission_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=true,force_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** 系统返回错误"存在下级依赖,无法删除,请先删除下级授权"
|
||||
@@ -6,21 +6,47 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 代理层强充层级判断
|
||||
|
||||
系统 SHALL 在强充预检时按层级判断生效的强充配置:平台在 PackageSeries 中设置的强充具有最高优先级;平台未设强充时,读取客户所属销售代理(`order.SellerShopID`)对应的 ShopSeriesAllocation 强充配置。
|
||||
|
||||
#### Scenario: 平台已设强充,代理自设被忽略
|
||||
- **WHEN** PackageSeries.enable_force_recharge=true(平台层),客户在代理A 的渠道下购买,代理A 的 ShopSeriesAllocation.enable_force_recharge=false
|
||||
- **THEN** 系统使用平台强充规则,need_force_recharge=true,force_recharge_amount=平台设定值
|
||||
|
||||
#### Scenario: 平台未设强充,代理自设生效
|
||||
- **WHEN** PackageSeries.enable_force_recharge=false,客户在代理A 的渠道下购买,代理A 的 ShopSeriesAllocation.enable_force_recharge=true,force_recharge_amount=10000
|
||||
- **THEN** 系统使用代理A 的强充配置,need_force_recharge=true,force_recharge_amount=10000
|
||||
|
||||
#### Scenario: 平台未设强充,代理也未设强充
|
||||
- **WHEN** PackageSeries.enable_force_recharge=false,代理A 的 ShopSeriesAllocation.enable_force_recharge=false
|
||||
- **THEN** 系统返回 need_force_recharge=false
|
||||
|
||||
#### Scenario: 平台未设强充,查询不到销售代理分配
|
||||
- **WHEN** PackageSeries.enable_force_recharge=false,系统查询不到 SellerShop 对应的 ShopSeriesAllocation
|
||||
- **THEN** 系统返回 need_force_recharge=false(降级处理,不影响购买流程)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包充值预检
|
||||
|
||||
系统 SHALL 提供钱包充值预检接口,返回强充要求、允许的充值金额等信息。
|
||||
系统 SHALL 提供钱包充值预检接口,返回强充要求、允许的充值金额等信息。强充判断 MUST 按代理层级规则执行:优先使用平台强充,平台未设时使用销售代理自设强充。
|
||||
|
||||
#### Scenario: 无强充要求
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为累计充值触发且未启用强充
|
||||
- **THEN** 系统返回 need_force_recharge = false,min_amount = 100(1元),max_amount = null
|
||||
- **WHEN** 客户查询卡钱包充值预检,PackageSeries.enable_force_recharge=false,销售代理 ShopSeriesAllocation.enable_force_recharge=false
|
||||
- **THEN** 系统返回 need_force_recharge=false
|
||||
|
||||
#### Scenario: 首次充值强充
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为首次充值触发,阈值 10000 分(100元),未发放佣金
|
||||
- **THEN** 系统返回 need_force_recharge = true,force_recharge_amount = 10000,trigger_type = "single_recharge",message = "首次充值需充值100元"
|
||||
#### Scenario: 首次充值强充(平台层)
|
||||
- **WHEN** 客户查询卡钱包充值预检,PackageSeries 配置为首次充值触发,阈值 10000 分,未发放佣金
|
||||
- **THEN** 系统返回 need_force_recharge=true,force_recharge_amount=10000,trigger_type="single_recharge"
|
||||
|
||||
#### Scenario: 累计充值启用强充
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为累计充值触发,启用强充,强充金额 10000 分(100元)
|
||||
- **THEN** 系统返回 need_force_recharge = true,force_recharge_amount = 10000,trigger_type = "accumulated_recharge",message = "每次充值需充值100元"
|
||||
#### Scenario: 累计充值启用强充(平台层)
|
||||
- **WHEN** 客户查询卡钱包充值预检,PackageSeries.enable_force_recharge=true,force_amount=10000
|
||||
- **THEN** 系统返回 need_force_recharge=true,force_recharge_amount=10000,trigger_type="accumulated_recharge"
|
||||
|
||||
#### Scenario: 代理自设累计充值强充(平台未设)
|
||||
- **WHEN** PackageSeries.enable_force_recharge=false,销售代理的 ShopSeriesAllocation.enable_force_recharge=true,force_recharge_amount=8000
|
||||
- **THEN** 系统返回 need_force_recharge=true,force_recharge_amount=8000
|
||||
|
||||
#### Scenario: 一次性佣金已发放
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡的一次性佣金已发放过
|
||||
@@ -34,15 +60,19 @@
|
||||
|
||||
### Requirement: 套餐购买预检
|
||||
|
||||
系统 SHALL 提供套餐购买预检接口,计算实际支付金额、钱包到账金额等信息。
|
||||
系统 SHALL 提供套餐购买预检接口,计算实际支付金额、钱包到账金额等信息。强充判断 MUST 按代理层级规则执行。
|
||||
|
||||
#### Scenario: 无强充要求正常购买
|
||||
- **WHEN** 客户购买 90 元套餐,无强充要求
|
||||
- **THEN** 系统返回 total_package_amount = 9000,need_force_recharge = false,actual_payment = 9000,wallet_credit = 0
|
||||
- **WHEN** 客户购买 90 元套餐,平台和销售代理均未设强充
|
||||
- **THEN** 系统返回 total_package_amount=9000,need_force_recharge=false,actual_payment=9000,wallet_credit=0
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价低于阈值
|
||||
- **WHEN** 客户购买 90 元套餐,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 9000,need_force_recharge = true,force_recharge_amount = 10000,actual_payment = 10000,wallet_credit = 1000,message = "需充值100元,购买套餐后余额10元"
|
||||
#### Scenario: 代理自设强充,套餐价低于强充金额
|
||||
- **WHEN** 客户购买 50 元套餐,平台未设强充,销售代理设置 force_recharge_amount=10000
|
||||
- **THEN** 系统返回 actual_payment=10000,wallet_credit=5000
|
||||
|
||||
#### Scenario: 首次充值强充(平台层),套餐价低于阈值
|
||||
- **WHEN** 客户购买 90 元套餐,首次充值阈值 100 元(平台层)
|
||||
- **THEN** 系统返回 total_package_amount=9000,need_force_recharge=true,force_recharge_amount=10000,actual_payment=10000,wallet_credit=1000
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价高于阈值
|
||||
- **WHEN** 客户购买 150 元套餐,首次充值阈值 100 元
|
||||
|
||||
@@ -172,3 +172,47 @@
|
||||
- 一次性佣金支持按销售数量或销售金额设置多个梯度档位
|
||||
- API 请求中删除 `enable_tier_commission` 和 `tier_config` 字段
|
||||
- API 响应中不再包含 `enable_tier_commission` 字段
|
||||
|
||||
---
|
||||
|
||||
### Requirement: /shop-series-allocations 接口
|
||||
|
||||
**❌ REMOVED** - 此 requirement 已废弃
|
||||
|
||||
**Reason**: 已被 `/shop-series-grants` 完全替代。开发阶段干净重构,不保留兼容接口。
|
||||
|
||||
**删除范围**:
|
||||
- `internal/handler/admin/shop_series_allocation.go`
|
||||
- `internal/routes/shop_series_allocation.go`
|
||||
- `internal/model/dto/shop_series_allocation.go`
|
||||
- `internal/service/shop_series_allocation/`
|
||||
- 从 `bootstrap/types.go`、`bootstrap/handlers.go`、`bootstrap/services.go`、`pkg/openapi/handlers.go`、`routes/admin.go` 移除引用
|
||||
|
||||
**保留**:`internal/store/postgres/shop_series_allocation_store.go`(被佣金计算、订单服务、Grant Service 使用)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: /shop-package-allocations 接口
|
||||
|
||||
**❌ REMOVED** - 此 requirement 已废弃
|
||||
|
||||
**Reason**: 套餐分配已合并进 `/shop-series-grants` 的创建和套餐管理接口。开发阶段干净重构,不保留兼容接口。
|
||||
|
||||
**删除范围**:
|
||||
- `internal/handler/admin/shop_package_allocation.go`
|
||||
- `internal/routes/shop_package_allocation.go`
|
||||
- `internal/model/dto/shop_package_allocation.go`
|
||||
- `internal/service/shop_package_allocation/`
|
||||
- 从 bootstrap、openapi/handlers、routes/admin 移除引用
|
||||
|
||||
**保留**:`internal/store/postgres/shop_package_allocation_store.go`(被多处使用)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 分配时配置 enable_one_time_commission 等字段
|
||||
|
||||
**❌ REMOVED** - 此 requirement 已废弃
|
||||
|
||||
**Reason**: `enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold` 三个字段从未被计算引擎读取,与 PackageSeries 的配置语义完全重复。
|
||||
|
||||
**Migration**:一次性佣金是否启用由 `PackageSeries.enable_one_time_commission` 控制;分配表中仅保留 `one_time_commission_amount`(固定模式天花板)、`commission_tiers_json`(梯度模式专属阶梯)和强充 3 个字段。
|
||||
|
||||
Reference in New Issue
Block a user