fix: 修复梯度佣金档位字段缺失,补全授权接口响应字段及强充有效状态
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
- OneTimeCommissionTierDTO 补充 operator 字段映射 - GrantCommissionTierItem 补充 dimension/stat_scope 字段(从全局配置合并) - 系列授权列表/详情补充强充锁定状态和强充金额的有效值计算 - 同步 OpenSpec 主规范并归档变更文档
This commit is contained in:
@@ -4,6 +4,7 @@ package dto
|
|||||||
type OneTimeCommissionTierDTO struct {
|
type OneTimeCommissionTierDTO struct {
|
||||||
Dimension string `json:"dimension" validate:"required,oneof=sales_count sales_amount" required:"true" description:"统计维度 (sales_count:销量, sales_amount:销售额)"`
|
Dimension string `json:"dimension" validate:"required,oneof=sales_count sales_amount" required:"true" description:"统计维度 (sales_count:销量, sales_amount:销售额)"`
|
||||||
StatScope string `json:"stat_scope" validate:"omitempty,oneof=self self_and_sub" description:"统计范围 (self:仅自己, self_and_sub:自己+下级)"`
|
StatScope string `json:"stat_scope" validate:"omitempty,oneof=self self_and_sub" description:"统计范围 (self:仅自己, self_and_sub:自己+下级)"`
|
||||||
|
Operator string `json:"operator,omitempty" validate:"omitempty,oneof=> >= < <=" description:"阈值比较运算符(>、>=、<、<=),空值时计算引擎默认 >="`
|
||||||
Threshold int64 `json:"threshold" validate:"required,min=0" required:"true" minimum:"0" description:"达标阈值"`
|
Threshold int64 `json:"threshold" validate:"required,min=0" required:"true" minimum:"0" description:"达标阈值"`
|
||||||
Amount int64 `json:"amount" validate:"required,min=0" required:"true" minimum:"0" description:"佣金金额(分)"`
|
Amount int64 `json:"amount" validate:"required,min=0" required:"true" minimum:"0" description:"佣金金额(分)"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ type ShopSeriesGrantPackageItem struct {
|
|||||||
Status int `json:"status" description:"分配状态 1-启用 2-禁用"`
|
Status int `json:"status" description:"分配状态 1-启用 2-禁用"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GrantCommissionTierItem 梯度佣金档位(operator 仅出现在响应中,来自 PackageSeries 全局配置)
|
// GrantCommissionTierItem 梯度佣金档位(operator/dimension/stat_scope 仅出现在响应中,来自 PackageSeries 全局配置)
|
||||||
type GrantCommissionTierItem struct {
|
type GrantCommissionTierItem struct {
|
||||||
Operator string `json:"operator,omitempty" description:"比较运算符(>、>=、<、<=),响应中从 PackageSeries 合并,请求中不传"`
|
Operator string `json:"operator,omitempty" description:"比较运算符(>、>=、<、<=),响应中从 PackageSeries 合并,请求中不传"`
|
||||||
|
Dimension string `json:"dimension,omitempty" description:"统计维度(sales_count:销售量, sales_amount:销售额),来自 PackageSeries 全局配置,响应中只读"`
|
||||||
|
StatScope string `json:"stat_scope,omitempty" description:"统计范围(self:仅自己, self_and_sub:自己+下级),来自 PackageSeries 全局配置,响应中只读"`
|
||||||
Threshold int64 `json:"threshold" description:"阈值(与 PackageSeries 全局配置对应)"`
|
Threshold int64 `json:"threshold" description:"阈值(与 PackageSeries 全局配置对应)"`
|
||||||
Amount int64 `json:"amount" description:"该代理在此档位的佣金金额(分)"`
|
Amount int64 `json:"amount" description:"该代理在此档位的佣金金额(分)"`
|
||||||
}
|
}
|
||||||
@@ -90,6 +92,8 @@ type ShopSeriesGrantListItem struct {
|
|||||||
CommissionType string `json:"commission_type" description:"佣金类型"`
|
CommissionType string `json:"commission_type" description:"佣金类型"`
|
||||||
OneTimeCommissionAmount int64 `json:"one_time_commission_amount" description:"固定模式佣金金额(分)"`
|
OneTimeCommissionAmount int64 `json:"one_time_commission_amount" description:"固定模式佣金金额(分)"`
|
||||||
ForceRechargeEnabled bool `json:"force_recharge_enabled" description:"是否启用强充"`
|
ForceRechargeEnabled bool `json:"force_recharge_enabled" description:"是否启用强充"`
|
||||||
|
ForceRechargeLocked bool `json:"force_recharge_locked" description:"强充是否被套餐系列锁定(true 时代理不可修改)"`
|
||||||
|
ForceRechargeAmount int64 `json:"force_recharge_amount" description:"强充金额(分)"`
|
||||||
AllocatorShopID uint `json:"allocator_shop_id" description:"分配者店铺ID"`
|
AllocatorShopID uint `json:"allocator_shop_id" description:"分配者店铺ID"`
|
||||||
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
|
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
|
||||||
PackageCount int `json:"package_count" description:"已授权套餐数量"`
|
PackageCount int `json:"package_count" description:"已授权套餐数量"`
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ func (s *Service) dtoToModelConfig(dtoConfig *dto.SeriesOneTimeCommissionConfigD
|
|||||||
tiers = make([]model.OneTimeCommissionTier, len(dtoConfig.Tiers))
|
tiers = make([]model.OneTimeCommissionTier, len(dtoConfig.Tiers))
|
||||||
for i, tier := range dtoConfig.Tiers {
|
for i, tier := range dtoConfig.Tiers {
|
||||||
tiers[i] = model.OneTimeCommissionTier{
|
tiers[i] = model.OneTimeCommissionTier{
|
||||||
|
Operator: tier.Operator,
|
||||||
Dimension: tier.Dimension,
|
Dimension: tier.Dimension,
|
||||||
StatScope: tier.StatScope,
|
StatScope: tier.StatScope,
|
||||||
Threshold: tier.Threshold,
|
Threshold: tier.Threshold,
|
||||||
@@ -296,6 +297,7 @@ func (s *Service) modelToDTO(config *model.OneTimeCommissionConfig) *dto.SeriesO
|
|||||||
tiers = make([]dto.OneTimeCommissionTierDTO, len(config.Tiers))
|
tiers = make([]dto.OneTimeCommissionTierDTO, len(config.Tiers))
|
||||||
for i, tier := range config.Tiers {
|
for i, tier := range config.Tiers {
|
||||||
tiers[i] = dto.OneTimeCommissionTierDTO{
|
tiers[i] = dto.OneTimeCommissionTierDTO{
|
||||||
|
Operator: tier.Operator,
|
||||||
Dimension: tier.Dimension,
|
Dimension: tier.Dimension,
|
||||||
StatScope: tier.StatScope,
|
StatScope: tier.StatScope,
|
||||||
Threshold: tier.Threshold,
|
Threshold: tier.Threshold,
|
||||||
|
|||||||
@@ -128,11 +128,17 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
|
|||||||
resp.AllocatorShopName = "平台"
|
resp.AllocatorShopName = "平台"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强充状态:first_recharge 或平台已启用 accumulated_recharge 强充时,锁定不可改
|
// 强充有效状态:first_recharge 或平台已启用 accumulated_recharge 强充时,锁定不可改
|
||||||
forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge
|
forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge
|
||||||
resp.ForceRechargeLocked = forceRechargeLocked
|
resp.ForceRechargeLocked = forceRechargeLocked
|
||||||
resp.ForceRechargeEnabled = allocation.EnableForceRecharge
|
if forceRechargeLocked {
|
||||||
resp.ForceRechargeAmount = allocation.ForceRechargeAmount
|
// 锁定时强充实际生效,金额取套餐系列配置值(allocation 字段为 0 不能使用)
|
||||||
|
resp.ForceRechargeEnabled = true
|
||||||
|
resp.ForceRechargeAmount = config.ForceAmount
|
||||||
|
} else {
|
||||||
|
resp.ForceRechargeEnabled = allocation.EnableForceRecharge
|
||||||
|
resp.ForceRechargeAmount = allocation.ForceRechargeAmount
|
||||||
|
}
|
||||||
|
|
||||||
// 固定模式
|
// 固定模式
|
||||||
if config.CommissionType == "fixed" {
|
if config.CommissionType == "fixed" {
|
||||||
@@ -153,11 +159,13 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
|
|||||||
// 合并全局 operator 和代理 amount
|
// 合并全局 operator 和代理 amount
|
||||||
tiers := make([]dto.GrantCommissionTierItem, 0, len(config.Tiers))
|
tiers := make([]dto.GrantCommissionTierItem, 0, len(config.Tiers))
|
||||||
for _, globalTier := range config.Tiers {
|
for _, globalTier := range config.Tiers {
|
||||||
tiers = append(tiers, dto.GrantCommissionTierItem{
|
tiers = append(tiers, dto.GrantCommissionTierItem{
|
||||||
Operator: globalTier.Operator,
|
Operator: globalTier.Operator,
|
||||||
Threshold: globalTier.Threshold,
|
Dimension: globalTier.Dimension,
|
||||||
Amount: agentAmountMap[globalTier.Threshold],
|
StatScope: globalTier.StatScope,
|
||||||
})
|
Threshold: globalTier.Threshold,
|
||||||
|
Amount: agentAmountMap[globalTier.Threshold],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
resp.CommissionTiers = tiers
|
resp.CommissionTiers = tiers
|
||||||
}
|
}
|
||||||
@@ -452,9 +460,11 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest)
|
|||||||
SeriesID: a.SeriesID,
|
SeriesID: a.SeriesID,
|
||||||
AllocatorShopID: a.AllocatorShopID,
|
AllocatorShopID: a.AllocatorShopID,
|
||||||
OneTimeCommissionAmount: a.OneTimeCommissionAmount,
|
OneTimeCommissionAmount: a.OneTimeCommissionAmount,
|
||||||
ForceRechargeEnabled: a.EnableForceRecharge,
|
// 强充有效状态在 seriesMap 分支中计算,此处先设默认值
|
||||||
Status: a.Status,
|
ForceRechargeEnabled: a.EnableForceRecharge,
|
||||||
CreatedAt: a.CreatedAt.Format(time.DateTime),
|
ForceRechargeAmount: a.ForceRechargeAmount,
|
||||||
|
Status: a.Status,
|
||||||
|
CreatedAt: a.CreatedAt.Format(time.DateTime),
|
||||||
}
|
}
|
||||||
if a.AllocatorShopID > 0 {
|
if a.AllocatorShopID > 0 {
|
||||||
item.AllocatorShopName = shopMap[a.AllocatorShopID]
|
item.AllocatorShopName = shopMap[a.AllocatorShopID]
|
||||||
@@ -466,6 +476,16 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest)
|
|||||||
config, _ := sr.GetOneTimeCommissionConfig()
|
config, _ := sr.GetOneTimeCommissionConfig()
|
||||||
if config != nil {
|
if config != nil {
|
||||||
item.CommissionType = config.CommissionType
|
item.CommissionType = config.CommissionType
|
||||||
|
// 计算强充有效状态:first_recharge 或平台已启用 accumulated_recharge 强充时锁定
|
||||||
|
forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge
|
||||||
|
item.ForceRechargeLocked = forceRechargeLocked
|
||||||
|
if forceRechargeLocked {
|
||||||
|
item.ForceRechargeEnabled = true
|
||||||
|
item.ForceRechargeAmount = config.ForceAmount
|
||||||
|
} else {
|
||||||
|
item.ForceRechargeEnabled = a.EnableForceRecharge
|
||||||
|
item.ForceRechargeAmount = a.ForceRechargeAmount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 统计套餐数量
|
// 统计套餐数量
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-04
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
`refactor-agent-series-grant` 变更在 Model 层(`model.OneTimeCommissionTier`)已正确新增 `Operator` 字段,并添加了运算符常量(`TierOperatorGT/GTE/LT/LTE`),但遗漏了同步更新 DTO 层和 Service 层的映射逻辑。
|
||||||
|
|
||||||
|
具体遗漏:
|
||||||
|
1. `OneTimeCommissionTierDTO`(package_series_dto.go)未加 `Operator` 字段 → API 无法读写 operator
|
||||||
|
2. `GrantCommissionTierItem`(shop_series_grant_dto.go)未加 `Dimension`、`StatScope` 字段 → grant 响应中梯度档位条件不透明
|
||||||
|
3. `package_series/service.go` 的 `dtoToModelConfig()` / `modelToDTO()` 未处理 `Operator` 映射
|
||||||
|
4. `shop_series_grant/service.go` 的 `buildGrantResponse()` 合并全局 tiers 时未携带 `Dimension`、`StatScope`
|
||||||
|
5. 系列授权列表/详情响应未正确反映 `force_recharge_enabled` 有效状态,且列表缺少 `force_recharge_locked`、`force_recharge_amount` 字段
|
||||||
|
|
||||||
|
无数据库结构变更,纯 DTO + Service 层修复。
|
||||||
|
|
||||||
|
**Bug 3 根因详述**:`forceRechargeLocked = config.TriggerType == FirstRecharge || config.EnableForceRecharge`。当此条件为 `true`,Service 正确跳过写 `allocation.EnableForceRecharge=true`,分配表中该字段保持 `false`。但 List 只返回 `a.EnableForceRecharge`,Detail `buildGrantResponse` 也返回 `allocation.EnableForceRecharge`,两处均未计算有效状态,导致前端看到 `force_recharge_enabled=false`。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- `OneTimeCommissionTierDTO` 支持 `operator` 字段的读写(创建/更新套餐系列时可传,查询时返回)
|
||||||
|
- `GrantCommissionTierItem` 响应中补充展示 `dimension` 和 `stat_scope`(只读,从 PackageSeries 全局配置合并)
|
||||||
|
- 向前兼容:`operator` 字段均使用 `omitempty`,老客户端请求不传时默认 `>=`(沿用 Model 层现有 fallback 逻辑)
|
||||||
|
- `ShopSeriesGrantListItem` 补充 `force_recharge_locked` 和 `force_recharge_amount` 字段
|
||||||
|
- 列表和详情 `force_recharge_enabled` 反映有效状态(`allocation.EnableForceRecharge || forceRechargeLocked`);锁定时 `force_recharge_amount` 取 `config.ForceAmount`
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不修改数据库结构、迁移文件
|
||||||
|
- 不修改 `model.OneTimeCommissionTier`(已正确)
|
||||||
|
- 不修改佣金计算引擎(`commission_calculation/service.go`)
|
||||||
|
- 不修改 `GrantCommissionTierItem` 请求侧(代理创建授权时仍不传 operator/dimension/stat_scope,这三个字段来自全局配置不可修改)
|
||||||
|
- 不修改梯度档位的校验逻辑(threshold 匹配等已正确)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 决策 1:`Operator` 在 DTO 中用 `omitempty`
|
||||||
|
|
||||||
|
`OneTimeCommissionTierDTO.Operator` 和 `GrantCommissionTierItem.Operator` 均使用 `json:"operator,omitempty"`。
|
||||||
|
|
||||||
|
**理由**:现有未设 operator 的套餐系列,JSONB 中该字段为空字符串,`omitempty` 避免返回无意义的 `""` 给前端,且与 Model 层的"Operator 为空时 fallback 到 `>=`"语义一致。
|
||||||
|
|
||||||
|
**备选**:永远返回字符串(空或 `>=`)→ 否决,因为"空"在 JSON 中会被序列化为 `""`,语义不清晰,而写入 JSONB 时可能覆盖原本为空的历史数据。
|
||||||
|
|
||||||
|
### 决策 2:`Dimension`/`StatScope` 仅出现在响应中,请求侧不开放
|
||||||
|
|
||||||
|
`GrantCommissionTierItem` 中 `Dimension` 和 `StatScope` 仅用于 GET/POST/PUT 的响应输出(从 PackageSeries 全局配置按 threshold 合并),请求体中代理仍只传 `threshold` 和 `amount`。
|
||||||
|
|
||||||
|
**理由**:这两个字段是套餐系列级别的全局配置,代理在创建授权时不能修改条件,只能修改金额。与 `Operator` 的处理方式保持一致(已在上次变更中确立该设计原则)。
|
||||||
|
|
||||||
|
### 决策 3:`buildGrantResponse` 按 threshold 索引合并全部条件字段
|
||||||
|
|
||||||
|
现有代码按 threshold 构建 `agentAmountMap`,在遍历 `config.Tiers` 时合并 `Operator`。本次同步合并 `Dimension` 和 `StatScope`,无需额外查询,O(N) 时间复杂度,N 为档位数(通常 ≤ 5),性能无影响。
|
||||||
|
|
||||||
|
### 决策 4:`force_recharge_enabled` 返回有效状态而非存储状态
|
||||||
|
|
||||||
|
列表和详情响应中,`force_recharge_enabled = allocation.EnableForceRecharge || forceRechargeLocked`。
|
||||||
|
|
||||||
|
**理由**:前端关心的是「强充是否实际生效」而非「代理是否主动开启」。当系列锁定强充时,即使 allocation 存储 `false`(Service 正确设计:锁定时不覆盖),对用户而言强充仍然生效。返回 `false` 会让前端误判,需在响应层修正。
|
||||||
|
|
||||||
|
**备选**:返回存储值(`allocation.EnableForceRecharge`)并依靠 `force_recharge_locked` 由前端推导 → 否决,因为历史已有前端对接问题,且两个字段冗余更易出错。统一在后端计算有效状态是更稳健的 API 设计。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
| 风险 | 缓解措施 |
|
||||||
|
|------|----------|
|
||||||
|
| 历史套餐系列 JSONB 中梯度档位无 `operator`/`dimension`/`stat_scope` | 响应用 `omitempty`,缺失字段不出现在响应中而非返回空值;前端应做好字段缺失的降级处理 |
|
||||||
|
| `CreateShopSeriesGrantRequest.CommissionTiers` 中含有 `operator`/`dimension`/`stat_scope` 字段(因共用同一 DTO 结构) | `GrantCommissionTierItem` 在请求侧这三个字段会被忽略(Service 层不读取),无副作用;可在 description 注释中说明仅响应有效 |
|
||||||
|
| 锁定时 `force_recharge_amount` 来源变更(config 而非 allocation) | 列表新增字段,详情原有字段调整逻辑,前端只需使用响应值,无需区分来源;allocation 表 `force_recharge_amount` 在锁定时仍存 0,不需迁移 |
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
`refactor-agent-series-grant` 变更遗留了两处实现漏洞,加上前端对接时发现的第三处 Bug,共三个问题:
|
||||||
|
|
||||||
|
1. 套餐系列管理 API 的梯度档位 DTO 缺少 `operator` 字段,导致无法通过接口设置/查看比较运算符。
|
||||||
|
2. 代理系列授权的梯度响应缺少 `dimension`(销售量/销售额)和 `stat_scope`(统计范围)字段,前端完全无法理解阈值的业务含义。
|
||||||
|
3. 系列授权列表和详情响应中,当套餐系列配置锁定了强充(`enable_force_recharge=true` 或 `trigger_type=first_recharge`)时,`force_recharge_enabled` 仍返回 `false`(分配记录自身的值),未反映有效状态;列表还缺少 `force_recharge_locked` 和 `force_recharge_amount` 字段。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **修复** `OneTimeCommissionTierDTO`(`package_series_dto.go`):新增 `Operator string` 字段,支持创建/更新套餐系列时传入并保存梯度阶梯的比较运算符(`>`、`>=`、`<`、`<=`)
|
||||||
|
- **修复** `package_series/service.go`:`dtoToModelConfig()` 新增 `Operator` 字段映射;`modelToDTO()` 新增 `Operator` 字段回填,使 `PackageSeriesResponse` 能正确返回 `operator`
|
||||||
|
- **修复** `GrantCommissionTierItem`(`shop_series_grant_dto.go`):新增 `Dimension string` 和 `StatScope string` 字段
|
||||||
|
- **修复** `shop_series_grant/service.go`:`buildGrantResponse()` 合并全局 PackageSeries tiers 时,除 `Operator` 外同步合并 `Dimension` 和 `StatScope`
|
||||||
|
|
||||||
|
- **修复** `ShopSeriesGrantListItem`(`shop_series_grant_dto.go`):新增 `ForceRechargeLocked bool` 和 `ForceRechargeAmount int64` 字段
|
||||||
|
- **修复** `shop_series_grant/service.go`:列表构建时计算 `forceRechargeLocked`,当锁定时 `ForceRechargeEnabled=true`、`ForceRechargeAmount=config.ForceAmount`;详情 `buildGrantResponse()` 同步修正 `ForceRechargeEnabled` 和 `ForceRechargeAmount` 有效状态逻辑
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
(无新增 Capability)
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `package-series-management`:梯度档位配置(`one_time_commission_config.tiers`)支持通过 API 读写 `operator` 字段(创建时传入、查询时返回)
|
||||||
|
- `agent-series-grant`:`commission_tiers` 响应中补充展示 `dimension`(`sales_count` / `sales_amount`)和 `stat_scope`(`self` / `self_and_sub`),这两个字段来自 PackageSeries 全局配置,对代理只读
|
||||||
|
- `agent-series-grant`:系列授权列表(`GET /api/admin/shop-series-grants`)新增 `force_recharge_locked` 和 `force_recharge_amount` 字段;列表和详情中 `force_recharge_enabled` 反映有效状态(锁定时为 `true`)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
**受影响的代码**
|
||||||
|
- `internal/model/dto/package_series_dto.go`:`OneTimeCommissionTierDTO` 新增 `Operator` 字段
|
||||||
|
- `internal/service/package_series/service.go`:`dtoToModelConfig()`、`modelToDTO()` 处理 `Operator`
|
||||||
|
- `internal/model/dto/shop_series_grant_dto.go`:`GrantCommissionTierItem` 新增 `Dimension`、`StatScope` 字段
|
||||||
|
- `internal/service/shop_series_grant/service.go`:`buildGrantResponse()` 合并 `Dimension`、`StatScope`
|
||||||
|
- `internal/model/dto/shop_series_grant_dto.go`:`ShopSeriesGrantListItem` 新增 `ForceRechargeLocked`、`ForceRechargeAmount` 字段
|
||||||
|
|
||||||
|
**受影响的 API**
|
||||||
|
- `POST/PUT /api/admin/package-series`:请求体中梯度档位可传 `operator`;响应中梯度档位包含 `operator`
|
||||||
|
- `GET /api/admin/package-series/:id`:同上
|
||||||
|
- `GET /api/admin/shop-series-grants/:id`:响应中 `commission_tiers` 新增 `dimension`、`stat_scope` 字段
|
||||||
|
- `POST /api/admin/shop-series-grants`:同上(Create 响应)
|
||||||
|
- `PUT /api/admin/shop-series-grants/:id`:同上(Update 响应)
|
||||||
|
- `GET /api/admin/shop-series-grants`(列表):新增 `force_recharge_locked`、`force_recharge_amount` 字段;`force_recharge_enabled` 反映有效状态
|
||||||
|
|
||||||
|
**无数据库迁移**:仅涉及 DTO 和 Service 层代码,不改动数据库结构和 Model 层(`OneTimeCommissionTier` model 已在上次变更中添加 `Operator` 字段;`tb_shop_series_allocation` 表结构已有 `enable_force_recharge`/`force_recharge_amount` 字段,无需迁移)
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 查询系列授权详情
|
||||||
|
|
||||||
|
系统 SHALL 提供 `GET /shop-series-grants/:id` 接口,返回包含套餐列表的聚合视图。梯度模式下,`commission_tiers` 中每个档位 MUST 包含 `dimension`(统计维度)和 `stat_scope`(统计范围)字段,这两个字段从 PackageSeries 全局配置按 `threshold` 合并,对代理只读。
|
||||||
|
|
||||||
|
**变更说明**:`GrantCommissionTierItem` 新增 `dimension` 和 `stat_scope` 字段,`buildGrantResponse()` 在合并 `operator` 的同时同步合并这两个字段。
|
||||||
|
|
||||||
|
#### 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 支持梯度模式的系列授权创建。梯度模式下,`commission_tiers` MUST 为必填,且必须包含与 PackageSeries 完全相同数量和阈值的阶梯(不多不少)。若某档位不希望给下级佣金,应将该档位的 amount 设为 0,不可省略该档位。创建成功后的响应中,`commission_tiers` 每个档位 MUST 包含 `operator`、`dimension`、`stat_scope` 字段(从全局配置合并)。
|
||||||
|
|
||||||
|
**变更说明**:Create 响应复用同一 `buildGrantResponse()`,故创建响应也自动包含 `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: 系列授权列表强充状态正确反映
|
||||||
|
|
||||||
|
系列授权列表 (`GET /shop-series-grants`) MUST 在每个列表项中返回 `force_recharge_locked`(是否被套餐系列锁定)和 `force_recharge_amount`(强充金额)。`force_recharge_enabled` MUST 反映有效状态:当锁定时为 `true`(无论分配记录自身如何);未锁定时取分配记录的实际设置。
|
||||||
|
|
||||||
|
**变更说明**:`ShopSeriesGrantListItem` 新增 `force_recharge_locked bool`、`force_recharge_amount int64`。列表构建时从套餐系列配置计算有效强充状态。
|
||||||
|
|
||||||
|
#### 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`。
|
||||||
|
|
||||||
|
**变更说明**:`buildGrantResponse()` 修正强充字段有效状态计算逻辑。
|
||||||
|
|
||||||
|
#### Scenario: 锁定强充时详情响应
|
||||||
|
|
||||||
|
- **WHEN** 套餐系列锁定强充,查询对应分配记录详情
|
||||||
|
- **THEN** `force_recharge_locked=true`,`force_recharge_enabled=true`,`force_recharge_amount`=系列配置的 `force_amount`
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 套餐系列一次性佣金规则配置
|
||||||
|
|
||||||
|
系统 SHALL 在套餐系列层面配置一次性佣金的完整规则,包括触发条件、阈值、金额/梯度、时效、强充配置。梯度配置(`commission_type=tiered`)中每个档位 MUST 支持通过 `operator` 字段设置阈值比较运算符(`>`、`>=`、`<`、`<=`),默认值为 `>=`。
|
||||||
|
|
||||||
|
**变更说明**:梯度档位 `OneTimeCommissionTierDTO` 新增 `operator` 字段,创建/更新套餐系列时可传入并持久化,查询时返回。
|
||||||
|
|
||||||
|
#### 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: 配置梯度规则(含 operator)
|
||||||
|
|
||||||
|
- **WHEN** 创建或更新套餐系列,`commission_type = tiered`
|
||||||
|
- **AND** 梯度配置包含 `operator` 字段:`[{operator: ">=", dimension: "sales_count", stat_scope: "self", threshold: 100, amount: 1000}, {operator: "<", dimension: "sales_count", stat_scope: "self", threshold: 50, amount: 500}]`
|
||||||
|
- **THEN** 系统保存完整梯度配置(含 operator)
|
||||||
|
- **AND** 查询详情时响应中 `tiers` 包含 `operator` 字段
|
||||||
|
|
||||||
|
#### Scenario: 配置梯度规则(不传 operator,向后兼容)
|
||||||
|
|
||||||
|
- **WHEN** 创建或更新套餐系列,`commission_type = tiered`
|
||||||
|
- **AND** 梯度配置未提供 `operator` 字段:`[{dimension: "sales_count", stat_scope: "self", threshold: 100, amount: 1000}]`
|
||||||
|
- **THEN** 系统保存梯度配置,`operator` 存储为空值(计算引擎 fallback 到 `>=`)
|
||||||
|
- **AND** 查询详情时响应中 `tiers` 的 `operator` 字段不出现(omitempty)
|
||||||
|
|
||||||
|
#### Scenario: 查询系列详情包含规则
|
||||||
|
|
||||||
|
- **WHEN** 查询套餐系列详情
|
||||||
|
- **THEN** 返回完整的一次性佣金规则配置,梯度档位包含 `operator`、`dimension`、`stat_scope`、`threshold`、`amount`
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
## 1. 套餐系列 DTO 修复(Operator 字段)
|
||||||
|
|
||||||
|
- [x] 1.1 `internal/model/dto/package_series_dto.go`:`OneTimeCommissionTierDTO` 新增 `Operator string` 字段,tag 为 `json:"operator,omitempty" validate:"omitempty,oneof=> >= < <=" description:"阈值比较运算符(>、>=、<、<=),空值时计算引擎默认 >="`
|
||||||
|
- [x] 1.2 `internal/service/package_series/service.go`:`dtoToModelConfig()` 中 `OneTimeCommissionTier` 赋值新增 `Operator: tier.Operator`
|
||||||
|
- [x] 1.3 `internal/service/package_series/service.go`:`modelToDTO()` 中 `OneTimeCommissionTierDTO` 赋值新增 `Operator: tier.Operator`
|
||||||
|
- [x] 1.4 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 2. 授权分配 DTO 修复(Dimension / StatScope 字段)
|
||||||
|
|
||||||
|
- [x] 2.1 `internal/model/dto/shop_series_grant_dto.go`:`GrantCommissionTierItem` 新增两个字段:
|
||||||
|
- `Dimension string`,tag 为 `json:"dimension,omitempty" description:"统计维度(sales_count:销售量, sales_amount:销售额),来自 PackageSeries 全局配置,响应中只读"`
|
||||||
|
- `StatScope string`,tag 为 `json:"stat_scope,omitempty" description:"统计范围(self:仅自己, self_and_sub:自己+下级),来自 PackageSeries 全局配置,响应中只读"`
|
||||||
|
- [x] 2.2 `internal/service/shop_series_grant/service.go`:`buildGrantResponse()` 梯度模式合并分支,在构造 `GrantCommissionTierItem` 时补充 `Dimension: globalTier.Dimension` 和 `StatScope: globalTier.StatScope`
|
||||||
|
- [x] 2.3 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
|
||||||
|
- [x] 3.1 db-validation:创建含 `operator`/`dimension`/`stat_scope` 的梯度套餐系列,查询详情确认响应中 tiers 包含完整字段
|
||||||
|
- [x] 3.2 db-validation:创建梯度模式系列授权,调用 `GET /shop-series-grants/:id`,确认 `commission_tiers` 中每档位包含 `operator`、`dimension`、`stat_scope`、`threshold`、`amount`
|
||||||
|
- [x] 3.3 db-validation:调用 `POST /shop-series-grants`(Create)和 `PUT /shop-series-grants/:id`(Update),确认响应中同样携带 `dimension`/`stat_scope`
|
||||||
|
- [x] 3.4 db-validation:不传 `operator` 的梯度档位,确认响应中 `operator` 字段缺失(omitempty 生效),不影响佣金计算逻辑
|
||||||
|
|
||||||
|
## 4. 强充状态有效展示修复
|
||||||
|
|
||||||
|
- [x] 4.1 `internal/model/dto/shop_series_grant_dto.go`:`ShopSeriesGrantListItem` 新增两个字段:
|
||||||
|
- `ForceRechargeLocked bool`,tag 为 `json:"force_recharge_locked" description:"强充是否被套餐系列锁定(true 时代理不可修改)"`
|
||||||
|
- `ForceRechargeAmount int64`,tag 为 `json:"force_recharge_amount" description:"强充金额(分)"`
|
||||||
|
- [x] 4.2 `internal/service/shop_series_grant/service.go`:列表构建(for 循环内的 `if sr, ok := seriesMap[a.SeriesID]` 分支)修正强充状态字段:
|
||||||
|
```go
|
||||||
|
forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge
|
||||||
|
item.ForceRechargeLocked = forceRechargeLocked
|
||||||
|
if forceRechargeLocked {
|
||||||
|
item.ForceRechargeEnabled = true
|
||||||
|
item.ForceRechargeAmount = config.ForceAmount
|
||||||
|
} else {
|
||||||
|
item.ForceRechargeEnabled = a.EnableForceRecharge
|
||||||
|
item.ForceRechargeAmount = a.ForceRechargeAmount
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [x] 4.3 `internal/service/shop_series_grant/service.go`:`buildGrantResponse()` 内强充状态计算修正(当前约 L132-L135):
|
||||||
|
```go
|
||||||
|
if forceRechargeLocked {
|
||||||
|
resp.ForceRechargeEnabled = true
|
||||||
|
resp.ForceRechargeAmount = config.ForceAmount
|
||||||
|
} else {
|
||||||
|
resp.ForceRechargeEnabled = allocation.EnableForceRecharge
|
||||||
|
resp.ForceRechargeAmount = allocation.ForceRechargeAmount
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [x] 4.4 运行 `go build ./...` 确认无编译错误
|
||||||
|
|
||||||
|
## 5. 强充 Bug 验证
|
||||||
|
|
||||||
|
- [x] 5.1 db-validation:查询套餐系列中 `enable_force_recharge=true` 的系列(如 series_id=2117,2118)对应的授权分配列表,确认 `force_recharge_locked=true`、`force_recharge_enabled=true`、`force_recharge_amount=系列配置值`
|
||||||
|
- [x] 5.2 db-validation:调用 `GET /api/admin/shop-series-grants/:id`(锁定系列的分配记录),确认详情响应中 `force_recharge_locked=true`、`force_recharge_enabled=true`
|
||||||
@@ -46,27 +46,28 @@
|
|||||||
|
|
||||||
### Requirement: 创建系列授权(梯度模式)
|
### Requirement: 创建系列授权(梯度模式)
|
||||||
|
|
||||||
系统 SHALL 支持梯度模式的系列授权创建。梯度模式下,`commission_tiers` MUST 为必填,且必须包含与 PackageSeries 完全相同数量和阈值的阶梯(不多不少)。若某档位不希望给下级佣金,应将该档位的 amount 设为 0,不可省略该档位。
|
系统 SHALL 支持梯度模式的系列授权创建。梯度模式下,`commission_tiers` MUST 为必填,且必须包含与 PackageSeries 完全相同数量和阈值的阶梯(不多不少)。若某档位不希望给下级佣金,应将该档位的 amount 设为 0,不可省略该档位。创建成功后的响应中,`commission_tiers` 每个档位 MUST 包含 `operator`、`dimension`、`stat_scope` 字段(从全局配置合并)。
|
||||||
|
|
||||||
#### Scenario: 代理成功创建梯度模式授权
|
#### 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}]
|
- **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 读取后合并)
|
- **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: 平台成功创建梯度模式授权
|
#### Scenario: 平台成功创建梯度模式授权
|
||||||
- **WHEN** 平台为顶级代理A 创建授权,PackageSeries 阶梯为 [{operator:">=", threshold:100, amount:100}, {operator:"<", threshold:50, amount:30}],传入 commission_tiers=[{threshold:100, amount:80}, {threshold:50, amount:20}]
|
- **WHEN** 平台为顶级代理A 创建授权,PackageSeries 阶梯含 `operator`/`dimension`/`stat_scope`
|
||||||
- **THEN** 系统创建授权,A 的专属阶梯存入 commission_tiers_json,响应中 commission_tiers 包含对应的 operator
|
- **THEN** 系统创建授权,响应中 `commission_tiers` 包含 PackageSeries 全局 `operator`、`dimension`、`stat_scope`
|
||||||
|
|
||||||
#### Scenario: 梯度模式某档位金额超过父级
|
#### Scenario: 梯度模式某档位金额超过父级
|
||||||
- **WHEN** 代理A 的阶梯第一档 amount=80,A 为 B 创建授权时传入第一档 amount=90
|
- **WHEN** 代理A 的阶梯第一档 `amount=80`,A 为 B 创建授权时传入第一档 `amount=90`
|
||||||
- **THEN** 系统返回错误"梯度佣金档位金额不能超过上级同档位限额"
|
- **THEN** 系统返回错误“某档位佣金金额超过上级天花板”
|
||||||
|
|
||||||
#### Scenario: 梯度模式传入了不存在的阈值
|
#### Scenario: 梯度模式传入了不存在的阈值
|
||||||
- **WHEN** PackageSeries 只有 threshold=100 和 150 两档,请求中传入 threshold=200
|
- **WHEN** PackageSeries 只有 `threshold=100` 和 `150` 两档,请求中传入 `threshold=200`
|
||||||
- **THEN** 系统返回错误"阶梯阈值与系列配置不匹配"
|
- **THEN** 系统返回错误“梯度阶梯 threshold 与系列配置不匹配”
|
||||||
|
|
||||||
#### Scenario: 梯度模式 commission_tiers 为必填
|
#### Scenario: 梯度模式 commission_tiers 为必填
|
||||||
- **WHEN** 请求中不包含 commission_tiers 或为空数组
|
- **WHEN** 请求中不包含 `commission_tiers` 或为空数组
|
||||||
- **THEN** 系统返回参数错误"梯度模式下必须提供阶梯金额配置"
|
- **THEN** 系统返回参数错误“梯度模式必须填写阶梯配置”
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -90,19 +91,33 @@
|
|||||||
|
|
||||||
### Requirement: 查询系列授权详情
|
### Requirement: 查询系列授权详情
|
||||||
|
|
||||||
系统 SHALL 提供 `GET /shop-series-grants/:id` 接口,返回包含套餐列表的聚合视图。
|
系统 SHALL 提供 `GET /shop-series-grants/:id` 接口,返回包含套餐列表的聚合视图。梯度模式下,`commission_tiers` 中每个档位 MUST 包含 `dimension`(统计维度)和 `stat_scope`(统计范围)字段,这两个字段从 PackageSeries 全局配置按 `threshold` 合并,对代理只读。
|
||||||
|
|
||||||
#### Scenario: 固定模式详情
|
#### Scenario: 固定模式详情
|
||||||
- **WHEN** 查询固定模式系列授权详情
|
- **WHEN** 查询固定模式系列授权详情
|
||||||
- **THEN** 响应包含 commission_type="fixed",one_time_commission_amount=有效值,commission_tiers=[]
|
- **THEN** 响应包含 `commission_type="fixed"`,`one_time_commission_amount=有效值`,`commission_tiers=[]`
|
||||||
|
|
||||||
#### Scenario: 梯度模式详情
|
#### Scenario: 梯度模式详情
|
||||||
- **WHEN** 查询梯度模式系列授权详情
|
- **WHEN** 查询梯度模式系列授权详情
|
||||||
- **THEN** 响应包含 commission_type="tiered",one_time_commission_amount=0,commission_tiers=[{threshold, amount}, ...]
|
- **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: 查询不存在的授权
|
#### Scenario: 查询不存在的授权
|
||||||
- **WHEN** 查询不存在的授权 ID
|
- **WHEN** 查询不存在的授权 ID
|
||||||
- **THEN** 系统返回错误"授权记录不存在"
|
- **THEN** 系统返回错误“授权记录不存在”
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -179,3 +194,31 @@
|
|||||||
#### Scenario: 有下级依赖时禁止删除
|
#### Scenario: 有下级依赖时禁止删除
|
||||||
- **WHEN** 删除一个已被下级代理用于创建子授权的记录
|
- **WHEN** 删除一个已被下级代理用于创建子授权的记录
|
||||||
- **THEN** 系统返回错误"存在下级依赖,无法删除,请先删除下级授权"
|
- **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`
|
||||||
|
|||||||
@@ -97,3 +97,40 @@
|
|||||||
#### Scenario: 状态未变化
|
#### Scenario: 状态未变化
|
||||||
- **WHEN** 管理员设置的状态与当前状态相同
|
- **WHEN** 管理员设置的状态与当前状态相同
|
||||||
- **THEN** 系统正常返回成功,不产生错误
|
- **THEN** 系统正常返回成功,不产生错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 套餐系列一次性佣金规则配置
|
||||||
|
|
||||||
|
系统 SHALL 在套餐系列层面配置一次性佣金的完整规则,包括触发条件、阈值、金额/梯度、时效、强充配置。梯度配置(`commission_type=tiered`)中每个档位 MUST 支持通过 `operator` 字段设置阈值比较运算符(`>`、`>=`、`<`、`<=`),默认值为 `>=`。
|
||||||
|
|
||||||
|
#### 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: 配置梯度规则(含 operator)
|
||||||
|
|
||||||
|
- **WHEN** 创建或更新套餐系列,`commission_type = tiered`
|
||||||
|
- **AND** 梯度配置包含 `operator` 字段:`[{operator: ">=" , dimension: "sales_count", stat_scope: "self", threshold: 100, amount: 1000}, {operator: "<", dimension: "sales_count", stat_scope: "self", threshold: 50, amount: 500}]`
|
||||||
|
- **THEN** 系统保存完整梯度配置(含 operator)
|
||||||
|
- **AND** 查询详情时响应中 `tiers` 包含 `operator` 字段
|
||||||
|
|
||||||
|
#### Scenario: 配置梯度规则(不传 operator,向后兼容)
|
||||||
|
|
||||||
|
- **WHEN** 创建或更新套餐系列,`commission_type = tiered`
|
||||||
|
- **AND** 梯度配置未提供 `operator` 字段:`[{dimension: "sales_count", stat_scope: "self", threshold: 100, amount: 1000}]`
|
||||||
|
- **THEN** 系统保存梯度配置,`operator` 存储为空值(计算引擎 fallback 到 `>=`)
|
||||||
|
- **AND** 查询详情时响应中 `tiers` 的 `operator` 字段不出现(omitempty)
|
||||||
|
|
||||||
|
#### Scenario: 查询系列详情包含规则
|
||||||
|
|
||||||
|
- **WHEN** 查询套餐系列详情
|
||||||
|
- **THEN** 返回完整的一次性佣金规则配置,梯度档位包含 `operator`、`dimension`、`stat_scope`、`threshold`、`amount`
|
||||||
|
|||||||
Reference in New Issue
Block a user