docs: 归档 refactor-agent-series-grant 变更文档
将已完成的变更(proposal、design、tasks、delta specs)归档至 openspec/changes/archive/2026-03-04-refactor-agent-series-grant/。变更内容:合并系列分配和套餐分配为系列授权(Grant)、新增梯度佣金模式、新增代理层强充层级规则。50/50 任务全部完成。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-03
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
当前系统中,"给代理分配套餐"需要两步独立操作:先调用 `POST /shop-series-allocations` 创建系列分配,再多次调用 `POST /shop-package-allocations` 逐一分配套餐。
|
||||||
|
|
||||||
|
`ShopSeriesAllocation` 有 6 个字段存在问题:
|
||||||
|
- 前 3 个死字段(`enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold`)从未被计算引擎读取,与 PackageSeries 配置完全重复
|
||||||
|
- 强充 3 个字段(`enable_force_recharge`、`force_recharge_amount`、`force_recharge_trigger_type`)语义正确,但 `checkForceRechargeRequirement` 只读 PackageSeries,代理配置完全无效
|
||||||
|
|
||||||
|
梯度模式下,`calculateChainOneTimeCommission` 直接把 PackageSeries 全局 tiers 传给 `matchOneTimeCommissionTier`,即所有代理用同一套阶梯金额。但业务模型要求每个代理有自己的专属阶梯金额(上级可以把金额压低给下级,但阈值不变),数据库中完全没有存储这个信息的字段。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 新增统一的"系列授权" API,一次请求原子性完成系列分配 + 套餐列表分配
|
||||||
|
- 修复强充层级:平台未设强充时,销售代理自设的强充配置真正生效
|
||||||
|
- 修复梯度模式数据模型:新增 `commission_tiers_json` 字段存储每代理专属阶梯金额
|
||||||
|
- 修复梯度模式计算引擎:读取各代理自己的阶梯金额,而非全局 tiers
|
||||||
|
- 新增金额上限校验:固定模式单值天花板;梯度模式每档位天花板
|
||||||
|
- 清理 3 个语义重复的死字段(数据库迁移删除)
|
||||||
|
- 删除旧的 `/shop-series-allocations` 和 `/shop-package-allocations` 接口,干净重构
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不重写现有的佣金计算链式逻辑(数学结构正确,只改数据读取来源)
|
||||||
|
- 不修改 ShopPackageBatchAllocation、ShopPackageBatchPricing 这两个 Service(批量操作不在本次范围)
|
||||||
|
- 不修改差价佣金(`CalculateCostDiffCommission`)逻辑
|
||||||
|
- 不改变数据存储底层结构(仍是两张表)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 决策 1:新接口策略——外观层
|
||||||
|
|
||||||
|
**选择**:新增 `ShopSeriesGrantService`,内部事务性地调用已有 `ShopSeriesAllocationStore` 和 `ShopPackageAllocationStore`。
|
||||||
|
|
||||||
|
**理由**:底层两张表的数据结构不变,对外以"授权"概念聚合呈现。
|
||||||
|
|
||||||
|
### 决策 2:梯度模式数据模型——JSONB 字段
|
||||||
|
|
||||||
|
**选择**:在 `ShopSeriesAllocation` 新增 `commission_tiers_json JSONB` 字段,存储该代理的专属阶梯金额列表。
|
||||||
|
|
||||||
|
数据格式:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"threshold": 100, "amount": 80},
|
||||||
|
{"threshold": 150, "amount": 120}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `threshold` 必须与 PackageSeries 全局 tiers 的阈值一一对应(不允许创建不存在的阈值)
|
||||||
|
- `amount` 由上级在授权时设定,不得超过上级同阈值的 amount
|
||||||
|
- 固定模式下此字段为空数组 `[]`
|
||||||
|
|
||||||
|
**理由**:阶梯金额是每代理独立的配置,与分配记录 1:1 关联,JSONB 存储简洁无需额外联表;阈值条件(dimension、stat_scope、threshold 数值)全局一致,无需在分配层重复存储。
|
||||||
|
|
||||||
|
**备选方案**:新建 `tb_shop_series_allocation_tier` 子表 → 被否决,阈值不可修改且数量固定(跟随系列配置),子表增加了查询复杂度。
|
||||||
|
|
||||||
|
### 决策 3:梯度模式计算引擎改写
|
||||||
|
|
||||||
|
**原逻辑**(错误):
|
||||||
|
```
|
||||||
|
matchOneTimeCommissionTier(ctx, shopID, seriesID, allocationID, config.Tiers)
|
||||||
|
↑ PackageSeries 全局阶梯金额
|
||||||
|
```
|
||||||
|
|
||||||
|
**新逻辑**:
|
||||||
|
```
|
||||||
|
1. 从 PackageSeries 取 tiers(dimension、stat_scope、threshold 列表)
|
||||||
|
2. 查询当前代理的 ShopSeriesAllocation.commission_tiers_json(专属金额)
|
||||||
|
3. 根据该代理的销售统计匹配命中的 threshold
|
||||||
|
4. 从步骤 2 的专属金额列表中取出该 threshold 对应的 amount → 即 myAmount
|
||||||
|
```
|
||||||
|
|
||||||
|
阶梯金额的来源由全局改为各代理自己的记录,dimension/stat_scope/threshold 仍从全局读取。
|
||||||
|
|
||||||
|
### 决策 4:统一授权响应格式
|
||||||
|
|
||||||
|
`GET /shop-series-grants/:id` 返回聚合视图:
|
||||||
|
|
||||||
|
```
|
||||||
|
ShopSeriesGrantResponse {
|
||||||
|
id // ShopSeriesAllocation.ID
|
||||||
|
shop_id / shop_name
|
||||||
|
series_id / series_name / series_code
|
||||||
|
commission_type // "fixed" 或 "tiered",从 PackageSeries 读取
|
||||||
|
one_time_commission_amount // 固定模式:该代理的天花板;梯度模式:返回 0
|
||||||
|
commission_tiers // 梯度模式:专属阶梯列表;固定模式:空数组
|
||||||
|
force_recharge_locked // true=平台已设强充,前端只读
|
||||||
|
force_recharge_enabled
|
||||||
|
force_recharge_amount
|
||||||
|
allocator_shop_id / allocator_shop_name
|
||||||
|
status
|
||||||
|
packages: [{
|
||||||
|
package_id, package_name, package_code, cost_price, shelf_status, status
|
||||||
|
}]
|
||||||
|
created_at / updated_at
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 决策 5:金额上限校验规则
|
||||||
|
|
||||||
|
**固定模式**:
|
||||||
|
```
|
||||||
|
allocator_shop_id == 0(平台)→ one_time_commission_amount ≤ PackageSeries.commission_amount
|
||||||
|
allocator_shop_id > 0(代理)→ one_time_commission_amount ≤ 分配者自身的 one_time_commission_amount
|
||||||
|
```
|
||||||
|
|
||||||
|
**梯度模式**:
|
||||||
|
```
|
||||||
|
对请求中每一个 {threshold, amount}:
|
||||||
|
allocator_shop_id == 0(平台)→ amount ≤ PackageSeries 同 threshold 的 amount
|
||||||
|
allocator_shop_id > 0(代理)→ amount ≤ 分配者的 commission_tiers_json 中同 threshold 的 amount
|
||||||
|
```
|
||||||
|
|
||||||
|
### 决策 6:强充层级的实现位置
|
||||||
|
|
||||||
|
在 `order/service.go` 的 `checkForceRechargeRequirement` 中增加:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 从 PackageSeries 获取配置,config.Enable == false → 无强充
|
||||||
|
|
||||||
|
2. firstCommissionPaid == true → 无强充
|
||||||
|
|
||||||
|
3. config.TriggerType == "first_recharge":
|
||||||
|
→ 直接返回需要强充(amount = config.Threshold)
|
||||||
|
→ 首次充值本身即为强充机制,代理无法修改此行为
|
||||||
|
→ 创建授权时忽略代理传入的 enable_force_recharge,响应 force_recharge_locked=true
|
||||||
|
|
||||||
|
4. config.TriggerType == "accumulated_recharge":
|
||||||
|
a. config.EnableForceRecharge == true
|
||||||
|
→ 使用平台强充(amount = config.ForceAmount),force_recharge_locked=true
|
||||||
|
b. config.EnableForceRecharge == false
|
||||||
|
→ 查询 result.Card.ShopID(或 result.Device.ShopID)的 ShopSeriesAllocation
|
||||||
|
→ 若 allocation.EnableForceRecharge == true → 使用代理强充配置
|
||||||
|
→ 否则 → 无强充
|
||||||
|
```
|
||||||
|
|
||||||
|
**Grant API 强充字段的可写条件**:
|
||||||
|
- `TriggerType == first_recharge`:`enable_force_recharge` 字段无效,创建/更新时忽略,响应 `force_recharge_locked=true`
|
||||||
|
- `TriggerType == accumulated_recharge` + 平台已设:同上,`force_recharge_locked=true`
|
||||||
|
- `TriggerType == accumulated_recharge` + 平台未设:`enable_force_recharge` 和 `force_recharge_amount` 生效,`force_recharge_locked=false`
|
||||||
|
|
||||||
|
### 决策 7:删除旧接口范围
|
||||||
|
|
||||||
|
删除以下内容(开发阶段干净重构):
|
||||||
|
- `internal/handler/admin/shop_series_allocation.go`
|
||||||
|
- `internal/handler/admin/shop_package_allocation.go`
|
||||||
|
- `internal/routes/shop_series_allocation.go`
|
||||||
|
- `internal/routes/shop_package_allocation.go`
|
||||||
|
- `internal/model/dto/shop_series_allocation.go`
|
||||||
|
- `internal/model/dto/shop_package_allocation.go`
|
||||||
|
- `internal/service/shop_series_allocation/`
|
||||||
|
- `internal/service/shop_package_allocation/`
|
||||||
|
- 从 `bootstrap/types.go`、`bootstrap/handlers.go`、`bootstrap/services.go`、`pkg/openapi/handlers.go`、`routes/admin.go` 中移除相关引用
|
||||||
|
|
||||||
|
保留(不删除):
|
||||||
|
- 两个 Store(`ShopSeriesAllocationStore`、`ShopPackageAllocationStore`)——被佣金计算、订单服务、新 Grant Service 使用
|
||||||
|
- `ShopPackageBatchAllocation`、`ShopPackageBatchPricing` Service + Handler + routes(批量操作不在本次范围)
|
||||||
|
|
||||||
|
### 决策 8:数据库迁移策略
|
||||||
|
|
||||||
|
单次迁移文件完成:
|
||||||
|
- 删除 3 列:`enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold`
|
||||||
|
- 新增 1 列:`commission_tiers_json JSONB NOT NULL DEFAULT '[]'`
|
||||||
|
|
||||||
|
迁移顺序:先部署代码(新代码不再读写 3 个死字段,新字段有默认值),再执行迁移,规避滚动部署期间的字段缺失问题。DOWN 脚本不可逆(不恢复数据)。
|
||||||
|
|
||||||
|
### 决策 9:梯度阶梯运算符支持
|
||||||
|
|
||||||
|
**背景**:当前 `matchOneTimeCommissionTier` 对所有阶梯固定使用 `>=` 做阈值比较,无法支持不同运算符(如"销量 < 100 给 X 元")。
|
||||||
|
|
||||||
|
**选择**:在 `OneTimeCommissionTier` 结构体新增 `Operator string` 字段,支持 `>`、`>=`、`<`、`<=`,默认值 `>=`(向前兼容)。
|
||||||
|
|
||||||
|
**存储位置**:
|
||||||
|
- `Operator` 仅存于 `PackageSeries.config.Tiers`(全局,由套餐系列配置决定)
|
||||||
|
- `ShopSeriesAllocation.commission_tiers_json` 不存 `Operator`(代理不能修改条件,只能修改金额)
|
||||||
|
- Grant 响应的 `commission_tiers` 展示时,将 `Operator` 从 PackageSeries 全局 tiers 按 threshold 合并进来
|
||||||
|
|
||||||
|
**计算引擎**:`matchOneTimeCommissionTier` 根据 `Operator` 选择对应比较函数;`Operator` 为空时 fallback 到 `>=`。
|
||||||
|
|
||||||
|
**不修改**:PackageSeries 的创建/编辑 API 不在本次范围(通过直接写 JSONB 设置 operator)。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
| 风险 | 缓解措施 |
|
||||||
|
|------|----------|
|
||||||
|
| 旧接口删除后前端需同步更新 | 开发阶段删除无历史数据顾虑,与前端对齐后统一上线 |
|
||||||
|
| 现有梯度模式历史分配记录的 commission_tiers_json 为空 | 新字段 DEFAULT '[]',计算引擎遇到空数组时 fallback 到 one_time_commission_amount(存 0)→ 不发佣金,需在上线前批量补填历史数据或确认历史记录均为固定模式 |
|
||||||
|
| checkForceRechargeRequirement 新增 DB 查询 | 函数已有 2 次查询,增加 1 次;量级可接受,后续可加缓存 |
|
||||||
|
| 梯度模式计算引擎改写影响正在运行的佣金计算 | 新逻辑仅在 commission_tiers_json 有值时生效;历史空记录走 fallback,不影响已发放佣金 |
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
代理套餐授权体系存在四类问题:
|
||||||
|
|
||||||
|
1. **接口割裂**:给代理授权套餐需要两步独立 API(先创建系列分配,再多次创建套餐分配),但这本质是一个业务动作
|
||||||
|
2. **死字段**:`ShopSeriesAllocation` 有 3 个字段(`enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold`)从未被计算引擎读取,与 PackageSeries 配置语义完全重复
|
||||||
|
3. **强充逻辑未接入**:代理自设强充的 3 个字段虽然存储,但 `checkForceRechargeRequirement` 从不读取,导致代理无论如何设置都不生效
|
||||||
|
4. **梯度模式实现错误**:梯度佣金计算引擎用 PackageSeries 全局阶梯表(所有代理同一套金额),但业务模型要求每个代理有自己的专属阶梯金额(可压缩、不超父级同档位上限);且数据库中没有存储每代理专属阶梯金额的字段
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **新增** 统一"系列授权"接口(`/shop-series-grants`),一次操作原子性完成系列分配 + 套餐列表分配,底层仍为两张表
|
||||||
|
- **删除** 旧的 `/shop-series-allocations` 和 `/shop-package-allocations` 接口及其全部 Handler、routes、DTO、Service(开发阶段,干净重构)
|
||||||
|
- **修复** 强充层级:订单服务接入销售代理的 `ShopSeriesAllocation` 强充配置,实现"平台未设强充 → 代理自设生效"
|
||||||
|
- **修复** 梯度模式数据模型:`ShopSeriesAllocation` 新增 `commission_tiers_json` JSONB 字段,存储每代理专属阶梯金额
|
||||||
|
- **修复** 梯度模式计算引擎:读取各代理自己的阶梯金额,而不是 PackageSeries 全局阶梯
|
||||||
|
- **新增** 金额上限校验:固定模式——子级 `one_time_commission_amount` 不得超过父级;梯度模式——子级每档位金额不得超过父级同档位金额
|
||||||
|
- **清理** 删除 3 个死字段(`enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold`)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `agent-series-grant`:统一代理系列授权 CRUD,单次操作包含套餐系列 + 套餐列表 + 成本价 + 一次性佣金配置(固定模式:单个上限金额;梯度模式:每档位上限金额列表)+ 强充配置(平台未设时)
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `force-recharge-check`:强充检查增加层级判断——平台系列已设强充时沿用,未设时改为读取销售代理的 `ShopSeriesAllocation` 强充配置
|
||||||
|
|
||||||
|
### Removed Capabilities
|
||||||
|
|
||||||
|
- `shop-series-allocation`:旧系列分配接口全部删除(Handler、routes、DTO、Service)
|
||||||
|
- `shop-package-allocation`:旧套餐分配接口全部删除(Handler、routes、DTO、Service)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
**受影响的数据库表**
|
||||||
|
- `tb_shop_series_allocation`:
|
||||||
|
- 删除 3 列(enable_one_time_commission、one_time_commission_trigger、one_time_commission_threshold)
|
||||||
|
- 新增 1 列(commission_tiers_json JSONB,梯度模式专用)
|
||||||
|
|
||||||
|
**受影响的 API**
|
||||||
|
- 新增:`POST/GET/PUT/DELETE /api/admin/shop-series-grants` 系列
|
||||||
|
- 新增:`PUT /api/admin/shop-series-grants/:id/packages`
|
||||||
|
- 删除:`/api/admin/shop-series-allocations` 全部接口
|
||||||
|
- 删除:`/api/admin/shop-package-allocations` 全部接口
|
||||||
|
- 变更:强充预检接口(`/purchase-check`、`/wallet-recharge-check`)返回结果受新逻辑影响
|
||||||
|
|
||||||
|
**受影响的服务**
|
||||||
|
- `internal/service/order/service.go`:`checkForceRechargeRequirement` 增加代理分配查询
|
||||||
|
- `internal/service/commission_calculation/service.go`:梯度模式改为读取各代理专属阶梯金额
|
||||||
|
- 删除:`internal/service/shop_series_allocation/`
|
||||||
|
- 删除:`internal/service/shop_package_allocation/`
|
||||||
|
- 新增:`internal/service/shop_series_grant/`
|
||||||
@@ -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** 系统返回错误"存在下级依赖,无法删除,请先删除下级授权"
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
## ADDED 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(降级处理,不影响购买流程)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 钱包充值预检
|
||||||
|
|
||||||
|
系统 SHALL 提供钱包充值预检接口,返回强充要求、允许的充值金额等信息。强充判断 MUST 按代理层级规则执行:优先使用平台强充,平台未设时使用销售代理自设强充。
|
||||||
|
|
||||||
|
#### Scenario: 无强充要求
|
||||||
|
- **WHEN** 客户查询卡钱包充值预检,PackageSeries.enable_force_recharge=false,销售代理 ShopSeriesAllocation.enable_force_recharge=false
|
||||||
|
- **THEN** 系统返回 need_force_recharge=false
|
||||||
|
|
||||||
|
#### Scenario: 首次充值强充(平台层)
|
||||||
|
- **WHEN** 客户查询卡钱包充值预检,PackageSeries 配置为首次充值触发,阈值 10000 分,未发放佣金
|
||||||
|
- **THEN** 系统返回 need_force_recharge=true,force_recharge_amount=10000,trigger_type="single_recharge"
|
||||||
|
|
||||||
|
#### 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** 客户查询卡钱包充值预检,卡的一次性佣金已发放过
|
||||||
|
- **THEN** 系统返回 need_force_recharge=false(不再强充)
|
||||||
|
|
||||||
|
#### Scenario: 未启用一次性佣金
|
||||||
|
- **WHEN** 客户查询卡钱包充值预检,卡关联系列未启用一次性佣金
|
||||||
|
- **THEN** 系统返回 need_force_recharge=false
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐购买预检
|
||||||
|
|
||||||
|
系统 SHALL 提供套餐购买预检接口,计算实际支付金额、钱包到账金额等信息。强充判断 MUST 按代理层级规则执行。
|
||||||
|
|
||||||
|
#### Scenario: 无强充要求正常购买
|
||||||
|
- **WHEN** 客户购买 90 元套餐,平台和销售代理均未设强充
|
||||||
|
- **THEN** 系统返回 total_package_amount=9000,need_force_recharge=false,actual_payment=9000,wallet_credit=0
|
||||||
|
|
||||||
|
#### 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** 客户购买 3 个套餐,总价 120 元,首次充值阈值 100 元
|
||||||
|
- **THEN** 系统返回 total_package_amount=12000,actual_payment=12000,wallet_credit=0
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: /shop-series-allocations 接口
|
||||||
|
|
||||||
|
**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 接口
|
||||||
|
|
||||||
|
**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 等字段
|
||||||
|
|
||||||
|
**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 个字段。
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
## 1. 数据库迁移文件准备
|
||||||
|
|
||||||
|
- [x] 1.1 使用 db-migration 规范创建迁移文件:
|
||||||
|
- **删除** `tb_shop_series_allocation` 的 3 列:`enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold`
|
||||||
|
- **新增** `commission_tiers_json JSONB NOT NULL DEFAULT '[]'`(梯度模式专属阶梯金额)
|
||||||
|
- DOWN 脚本添加说明注释(不恢复数据)
|
||||||
|
|
||||||
|
## 2. 删除旧接口(Handler / routes / DTO / Service)
|
||||||
|
|
||||||
|
- [x] 2.1 删除文件:
|
||||||
|
- `internal/handler/admin/shop_series_allocation.go`
|
||||||
|
- `internal/handler/admin/shop_package_allocation.go`
|
||||||
|
- `internal/routes/shop_series_allocation.go`
|
||||||
|
- `internal/routes/shop_package_allocation.go`
|
||||||
|
- `internal/model/dto/shop_series_allocation.go`
|
||||||
|
- `internal/model/dto/shop_package_allocation.go`
|
||||||
|
- `internal/service/shop_series_allocation/`(整个目录)
|
||||||
|
- `internal/service/shop_package_allocation/`(整个目录)
|
||||||
|
- [x] 2.2 `internal/bootstrap/types.go`:删除 `ShopSeriesAllocation`、`ShopPackageAllocation` 两个 Handler 字段
|
||||||
|
- [x] 2.3 `internal/bootstrap/handlers.go`:删除对应 Handler 初始化行;删除对应 service 引用
|
||||||
|
- [x] 2.4 `internal/bootstrap/services.go`:删除 `ShopSeriesAllocation`、`ShopPackageAllocation` 两个 Service 字段及初始化;移除对应 import
|
||||||
|
- [x] 2.5 `pkg/openapi/handlers.go`:删除 `ShopSeriesAllocation`、`ShopPackageAllocation` 两行
|
||||||
|
- [x] 2.6 `internal/routes/admin.go`:删除 `registerShopSeriesAllocationRoutes`、`registerShopPackageAllocationRoutes` 两处调用
|
||||||
|
- [x] 2.7 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 3. Model 更新
|
||||||
|
|
||||||
|
- [x] 3.1 `internal/model/shop_series_allocation.go`:
|
||||||
|
- 删除 `EnableOneTimeCommission`、`OneTimeCommissionTrigger`、`OneTimeCommissionThreshold` 三个字段
|
||||||
|
- 新增 `CommissionTiersJSON string` 字段(JSONB,默认 `'[]'`)
|
||||||
|
- 新增辅助类型 `AllocationCommissionTier struct { Threshold int64; Amount int64 }`
|
||||||
|
- 新增 `GetCommissionTiers() ([]AllocationCommissionTier, error)` 方法
|
||||||
|
- 新增 `SetCommissionTiers(tiers []AllocationCommissionTier) error` 方法
|
||||||
|
- [x] 3.2 `internal/model/package.go`:
|
||||||
|
- `OneTimeCommissionTier` 新增 `Operator string` 字段(json:"operator")
|
||||||
|
- 新增运算符常量:`TierOperatorGT = ">"` / `TierOperatorGTE = ">="` / `TierOperatorLT = "<"` / `TierOperatorLTE = "<="`
|
||||||
|
- [x] 3.3 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 4. 修复梯度模式计算引擎
|
||||||
|
|
||||||
|
- [x] 4.1 `internal/service/commission_calculation/service.go`:
|
||||||
|
修改 `calculateChainOneTimeCommission` 中梯度模式分支:
|
||||||
|
- 原来:直接把 `config.Tiers`(全局)传给 `matchOneTimeCommissionTier`
|
||||||
|
- 新的:从 `currentSeriesAllocation.GetCommissionTiers()` 取专属金额列表,结合 `config.Tiers`(取 operator/dimension/stat_scope/threshold)做匹配
|
||||||
|
- 匹配逻辑:根据代理销售统计和 tier.Operator 判断是否命中 threshold → 查专属列表同 threshold 的 amount → 即 myAmount;未命中任何阶梯时 myAmount = 0
|
||||||
|
- commission_tiers_json 为空时(历史数据)fallback 到 `currentSeriesAllocation.OneTimeCommissionAmount`
|
||||||
|
- [x] 4.2 修改 `matchOneTimeCommissionTier`:接受 agentTiers `[]AllocationCommissionTier` 参数作为金额来源;根据 `tier.Operator` 选择对应比较逻辑(>、>=、<、<=),Operator 为空时默认 `>=`
|
||||||
|
- [x] 4.3 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 5. 修复强充层级
|
||||||
|
|
||||||
|
- [x] 5.1 `internal/service/order/service.go` `checkForceRechargeRequirement()`:
|
||||||
|
- `config.TriggerType == "first_recharge"` 时:直接返回需要强充(不变),不查代理配置
|
||||||
|
- `config.TriggerType == "accumulated_recharge"` 且 `config.EnableForceRecharge == false` 时:
|
||||||
|
从 `result.Card.ShopID`(或 `result.Device.ShopID`)查询该代理的 `ShopSeriesAllocation`,
|
||||||
|
若该分配 `EnableForceRecharge=true` 则返回代理强充配置,查询不到时降级返回 `need_force_recharge=false`
|
||||||
|
- [x] 5.2 验证 `GetPurchaseCheck` 调用路径已覆盖新逻辑(复用同一函数,无需额外修改)
|
||||||
|
- [x] 5.3 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 6. 新系列授权 DTO
|
||||||
|
|
||||||
|
- [x] 6.1 创建 `internal/model/dto/shop_series_grant_dto.go`,定义:
|
||||||
|
- `GrantPackageItem`(package_id、cost_price、remove *bool)
|
||||||
|
- `ShopSeriesGrantPackageItem`(package_id、package_name、package_code、cost_price、shelf_status、status)
|
||||||
|
- `GrantCommissionTierItem`(operator string、threshold int64、amount int64)
|
||||||
|
—— operator 仅出现在响应中(从 PackageSeries 合并),请求中不传 operator
|
||||||
|
- [x] 6.2 定义 `ShopSeriesGrantResponse`:
|
||||||
|
- id、shop_id/name、series_id/name/code、commission_type
|
||||||
|
- one_time_commission_amount(固定模式有效,梯度模式返回 0)
|
||||||
|
- commission_tiers []GrantCommissionTierItem(梯度模式有值,固定模式为空)
|
||||||
|
- force_recharge_locked、force_recharge_enabled、force_recharge_amount
|
||||||
|
- allocator_shop_id/name、status、packages、created_at、updated_at
|
||||||
|
- [x] 6.3 定义 `CreateShopSeriesGrantRequest`:
|
||||||
|
- shop_id、series_id
|
||||||
|
- one_time_commission_amount *int64(固定模式必填)
|
||||||
|
- commission_tiers []GrantCommissionTierItem(梯度模式必填)
|
||||||
|
- enable_force_recharge *bool、force_recharge_amount *int64
|
||||||
|
- packages []GrantPackageItem
|
||||||
|
- [x] 6.4 定义 `UpdateShopSeriesGrantRequest`:
|
||||||
|
- one_time_commission_amount *int64
|
||||||
|
- commission_tiers []GrantCommissionTierItem
|
||||||
|
- enable_force_recharge *bool、force_recharge_amount *int64
|
||||||
|
- [x] 6.5 定义 `ManageGrantPackagesRequest`(packages []GrantPackageItem)
|
||||||
|
- [x] 6.6 定义 `ShopSeriesGrantListRequest`(page、page_size、shop_id *uint、series_id *uint、allocator_shop_id *uint、status *int)及列表 DTO(`ShopSeriesGrantListItem` 含 package_count、`ShopSeriesGrantPageResult`)
|
||||||
|
|
||||||
|
## 7. 新系列授权 Service
|
||||||
|
|
||||||
|
- [x] 7.1 创建 `internal/service/shop_series_grant/service.go`,定义 Service 结构及 New() 构造函数
|
||||||
|
(依赖:db、shopSeriesAllocationStore、shopPackageAllocationStore、shopPackageAllocationPriceHistoryStore、shopStore、packageStore、packageSeriesStore)
|
||||||
|
|
||||||
|
- [x] 7.2 实现私有方法 `getParentCeilingFixed()`:固定模式天花板查询
|
||||||
|
- allocatorShopID=0 → 读 PackageSeries.commission_amount
|
||||||
|
- allocatorShopID>0 → 读分配者自身的 ShopSeriesAllocation.one_time_commission_amount
|
||||||
|
|
||||||
|
- [x] 7.3 实现私有方法 `getParentCeilingTiered()`:梯度模式天花板查询
|
||||||
|
- allocatorShopID=0 → 读 PackageSeries.config.Tiers 中各 threshold 的 amount
|
||||||
|
- allocatorShopID>0 → 读分配者自身 ShopSeriesAllocation.commission_tiers_json
|
||||||
|
|
||||||
|
- [x] 7.4 实现 `Create()`:
|
||||||
|
- 查询 PackageSeries 确认 commission_type
|
||||||
|
- 检查重复授权:shop_id + series_id 已有 active 记录 → 错误"该代理已存在此系列授权"
|
||||||
|
- allocator 是代理时:查分配者自身的 ShopSeriesAllocation,无记录 → 错误"当前账号无此系列授权,无法向下分配"
|
||||||
|
- 固定模式:one_time_commission_amount 必填 + 天花板校验
|
||||||
|
- 梯度模式:commission_tiers 必填 + 阶梯数量和 threshold 必须与 PackageSeries 完全一致(不多不少,amount 可为 0)+ 每档位天花板校验
|
||||||
|
- 强充层级判断(TriggerType=first_recharge 或平台已设强充 → locked=true 忽略代理传入;仅 accumulated_recharge 且平台未设时接受代理强充配置)
|
||||||
|
- 事务中创建 ShopSeriesAllocation + N 条 ShopPackageAllocation
|
||||||
|
- 返回聚合响应
|
||||||
|
|
||||||
|
- [x] 7.5 实现 `Get()`:查询 ShopSeriesAllocation → 查 PackageSeries 取全局 tiers(含 operator)→ 关联套餐分配 → 拼装 ShopSeriesGrantResponse
|
||||||
|
(梯度模式下,commission_tiers 响应需将 agent 的 amount 与 PackageSeries tiers 的 operator 按 threshold 合并)
|
||||||
|
|
||||||
|
- [x] 7.6 实现 `List()`:分页查询 → 统计 package_count → 返回 ShopSeriesGrantPageResult
|
||||||
|
|
||||||
|
- [x] 7.7 实现 `Update()`:
|
||||||
|
- 固定模式:含 one_time_commission_amount 时做天花板校验
|
||||||
|
- 梯度模式:含 commission_tiers 时做每档位天花板校验
|
||||||
|
- 平台已设强充时忽略强充变更
|
||||||
|
- 保存更新
|
||||||
|
|
||||||
|
- [x] 7.8 实现 `ManagePackages()`:事务中处理 packages 列表:
|
||||||
|
- remove=true:查找 active 的 ShopPackageAllocation,找到则软删除,找不到则静默忽略
|
||||||
|
- 无 remove 标志:校验套餐归属和分配权限,查现有 active 记录(有则更新 cost_price+写历史,无则新建)
|
||||||
|
|
||||||
|
- [x] 7.9 实现 `Delete()`:检查子级依赖 → 事务软删除 ShopSeriesAllocation + 所有关联 ShopPackageAllocation
|
||||||
|
|
||||||
|
- [x] 7.10 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 8. Handler、路由及文档生成器
|
||||||
|
|
||||||
|
- [x] 8.1 创建 `internal/handler/admin/shop_series_grant.go`,实现 Create、Get、List、Update、ManagePackages、Delete 六个 Handler 方法
|
||||||
|
- [x] 8.2 创建 `internal/routes/shop_series_grant.go`,注册路由(Tag: "代理系列授权"):
|
||||||
|
- `GET /shop-series-grants`
|
||||||
|
- `POST /shop-series-grants`
|
||||||
|
- `GET /shop-series-grants/:id`
|
||||||
|
- `PUT /shop-series-grants/:id`
|
||||||
|
- `DELETE /shop-series-grants/:id`
|
||||||
|
- `PUT /shop-series-grants/:id/packages`
|
||||||
|
- [x] 8.3 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 9. 依赖注入 & Bootstrap
|
||||||
|
|
||||||
|
- [x] 9.1 `internal/bootstrap/types.go`:添加 `ShopSeriesGrant *admin.ShopSeriesGrantHandler` 字段
|
||||||
|
- [x] 9.2 `internal/bootstrap/services.go`:import shop_series_grant service 包,添加字段并在 `initServices()` 中初始化
|
||||||
|
- [x] 9.3 `internal/bootstrap/handlers.go`:添加 ShopSeriesGrant Handler 初始化
|
||||||
|
- [x] 9.4 `pkg/openapi/handlers.go`:添加 `ShopSeriesGrant: admin.NewShopSeriesGrantHandler(nil)`
|
||||||
|
- [x] 9.5 `internal/routes/admin.go`:添加 `registerShopSeriesGrantRoutes()` 调用
|
||||||
|
- [x] 9.6 运行 `go build ./...` 确认完整构建通过
|
||||||
|
|
||||||
|
## 10. 执行迁移 & 数据验证
|
||||||
|
|
||||||
|
- [x] 10.1 执行迁移(`make migrate-up`),用 db-migration 规范验证:3 列已删除,commission_tiers_json 列已添加
|
||||||
|
- [x] 10.2 db-validation:创建固定模式授权(正常路径),确认 ShopSeriesAllocation 和 ShopPackageAllocation 均创建成功
|
||||||
|
- [x] 10.3 db-validation:固定模式金额超过父级天花板时接口返回错误
|
||||||
|
- [x] 10.4 db-validation:梯度模式创建授权,commission_tiers_json 正确写入
|
||||||
|
- [x] 10.5 db-validation:梯度模式某档位金额超过父级同档位时接口返回错误
|
||||||
|
- [x] 10.6 db-validation:代理自设强充后,购买预检接口返回 need_force_recharge=true,金额与代理设置一致
|
||||||
|
- [x] 10.7 db-validation:平台系列已设强充时,代理自设强充被锁定,购买预检使用平台强充金额
|
||||||
|
- [x] 10.8 db-validation:梯度模式阶梯含 `operator="<"` 时,销售统计低于阈值的代理命中该档位,高于阈值的代理不命中
|
||||||
Reference in New Issue
Block a user