From b52cb9a07890c2ad6aa5417ae96e49753e388249 Mon Sep 17 00:00:00 2001 From: huang Date: Thu, 5 Mar 2026 11:23:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A2=AF=E5=BA=A6?= =?UTF-8?q?=E4=BD=A3=E9=87=91=E6=A1=A3=E4=BD=8D=E5=AD=97=E6=AE=B5=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=EF=BC=8C=E8=A1=A5=E5=85=A8=E6=8E=88=E6=9D=83=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=93=8D=E5=BA=94=E5=AD=97=E6=AE=B5=E5=8F=8A=E5=BC=BA?= =?UTF-8?q?=E5=85=85=E6=9C=89=E6=95=88=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OneTimeCommissionTierDTO 补充 operator 字段映射 - GrantCommissionTierItem 补充 dimension/stat_scope 字段(从全局配置合并) - 系列授权列表/详情补充强充锁定状态和强充金额的有效值计算 - 同步 OpenSpec 主规范并归档变更文档 --- internal/model/dto/package_series_dto.go | 1 + internal/model/dto/shop_series_grant_dto.go | 6 +- internal/service/package_series/service.go | 2 + internal/service/shop_series_grant/service.go | 42 +++++-- .../.openspec.yaml | 2 + .../design.md | 66 +++++++++++ .../proposal.md | 47 ++++++++ .../specs/agent-series-grant/spec.md | 109 ++++++++++++++++++ .../specs/package-series-management/spec.md | 38 ++++++ .../tasks.md | 55 +++++++++ openspec/specs/agent-series-grant/spec.md | 73 +++++++++--- .../specs/package-series-management/spec.md | 37 ++++++ 12 files changed, 451 insertions(+), 27 deletions(-) create mode 100644 openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/.openspec.yaml create mode 100644 openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/design.md create mode 100644 openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/proposal.md create mode 100644 openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/agent-series-grant/spec.md create mode 100644 openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/package-series-management/spec.md create mode 100644 openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/tasks.md diff --git a/internal/model/dto/package_series_dto.go b/internal/model/dto/package_series_dto.go index d4240db..ca1b8dc 100644 --- a/internal/model/dto/package_series_dto.go +++ b/internal/model/dto/package_series_dto.go @@ -4,6 +4,7 @@ package dto type OneTimeCommissionTierDTO struct { 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:自己+下级)"` + Operator string `json:"operator,omitempty" validate:"omitempty,oneof=> >= < <=" 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:"佣金金额(分)"` } diff --git a/internal/model/dto/shop_series_grant_dto.go b/internal/model/dto/shop_series_grant_dto.go index 3ee95b8..aa190b5 100644 --- a/internal/model/dto/shop_series_grant_dto.go +++ b/internal/model/dto/shop_series_grant_dto.go @@ -17,9 +17,11 @@ type ShopSeriesGrantPackageItem struct { Status int `json:"status" description:"分配状态 1-启用 2-禁用"` } -// GrantCommissionTierItem 梯度佣金档位(operator 仅出现在响应中,来自 PackageSeries 全局配置) +// GrantCommissionTierItem 梯度佣金档位(operator/dimension/stat_scope 仅出现在响应中,来自 PackageSeries 全局配置) type GrantCommissionTierItem struct { 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 全局配置对应)"` Amount int64 `json:"amount" description:"该代理在此档位的佣金金额(分)"` } @@ -90,6 +92,8 @@ type ShopSeriesGrantListItem struct { CommissionType string `json:"commission_type" description:"佣金类型"` OneTimeCommissionAmount int64 `json:"one_time_commission_amount" 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"` AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"` PackageCount int `json:"package_count" description:"已授权套餐数量"` diff --git a/internal/service/package_series/service.go b/internal/service/package_series/service.go index 1f676b1..90bc7fd 100644 --- a/internal/service/package_series/service.go +++ b/internal/service/package_series/service.go @@ -263,6 +263,7 @@ func (s *Service) dtoToModelConfig(dtoConfig *dto.SeriesOneTimeCommissionConfigD tiers = make([]model.OneTimeCommissionTier, len(dtoConfig.Tiers)) for i, tier := range dtoConfig.Tiers { tiers[i] = model.OneTimeCommissionTier{ + Operator: tier.Operator, Dimension: tier.Dimension, StatScope: tier.StatScope, Threshold: tier.Threshold, @@ -296,6 +297,7 @@ func (s *Service) modelToDTO(config *model.OneTimeCommissionConfig) *dto.SeriesO tiers = make([]dto.OneTimeCommissionTierDTO, len(config.Tiers)) for i, tier := range config.Tiers { tiers[i] = dto.OneTimeCommissionTierDTO{ + Operator: tier.Operator, Dimension: tier.Dimension, StatScope: tier.StatScope, Threshold: tier.Threshold, diff --git a/internal/service/shop_series_grant/service.go b/internal/service/shop_series_grant/service.go index a5bacdd..ff69180 100644 --- a/internal/service/shop_series_grant/service.go +++ b/internal/service/shop_series_grant/service.go @@ -128,11 +128,17 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop resp.AllocatorShopName = "平台" } - // 强充状态:first_recharge 或平台已启用 accumulated_recharge 强充时,锁定不可改 + // 强充有效状态:first_recharge 或平台已启用 accumulated_recharge 强充时,锁定不可改 forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge resp.ForceRechargeLocked = forceRechargeLocked - resp.ForceRechargeEnabled = allocation.EnableForceRecharge - resp.ForceRechargeAmount = allocation.ForceRechargeAmount + if forceRechargeLocked { + // 锁定时强充实际生效,金额取套餐系列配置值(allocation 字段为 0 不能使用) + resp.ForceRechargeEnabled = true + resp.ForceRechargeAmount = config.ForceAmount + } else { + resp.ForceRechargeEnabled = allocation.EnableForceRecharge + resp.ForceRechargeAmount = allocation.ForceRechargeAmount + } // 固定模式 if config.CommissionType == "fixed" { @@ -153,11 +159,13 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop // 合并全局 operator 和代理 amount tiers := make([]dto.GrantCommissionTierItem, 0, len(config.Tiers)) for _, globalTier := range config.Tiers { - tiers = append(tiers, dto.GrantCommissionTierItem{ - Operator: globalTier.Operator, - Threshold: globalTier.Threshold, - Amount: agentAmountMap[globalTier.Threshold], - }) + tiers = append(tiers, dto.GrantCommissionTierItem{ + Operator: globalTier.Operator, + Dimension: globalTier.Dimension, + StatScope: globalTier.StatScope, + Threshold: globalTier.Threshold, + Amount: agentAmountMap[globalTier.Threshold], + }) } resp.CommissionTiers = tiers } @@ -452,9 +460,11 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest) SeriesID: a.SeriesID, AllocatorShopID: a.AllocatorShopID, OneTimeCommissionAmount: a.OneTimeCommissionAmount, - ForceRechargeEnabled: a.EnableForceRecharge, - Status: a.Status, - CreatedAt: a.CreatedAt.Format(time.DateTime), + // 强充有效状态在 seriesMap 分支中计算,此处先设默认值 + ForceRechargeEnabled: a.EnableForceRecharge, + ForceRechargeAmount: a.ForceRechargeAmount, + Status: a.Status, + CreatedAt: a.CreatedAt.Format(time.DateTime), } if a.AllocatorShopID > 0 { item.AllocatorShopName = shopMap[a.AllocatorShopID] @@ -466,6 +476,16 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest) config, _ := sr.GetOneTimeCommissionConfig() if config != nil { 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 + } } } // 统计套餐数量 diff --git a/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/.openspec.yaml b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/.openspec.yaml new file mode 100644 index 0000000..5aae5cf --- /dev/null +++ b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-04 diff --git a/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/design.md b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/design.md new file mode 100644 index 0000000..7942823 --- /dev/null +++ b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/design.md @@ -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,不需迁移 | diff --git a/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/proposal.md b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/proposal.md new file mode 100644 index 0000000..99a7407 --- /dev/null +++ b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/proposal.md @@ -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` 字段,无需迁移) diff --git a/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/agent-series-grant/spec.md b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/agent-series-grant/spec.md new file mode 100644 index 0000000..33bdd7e --- /dev/null +++ b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/agent-series-grant/spec.md @@ -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` diff --git a/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/package-series-management/spec.md b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/package-series-management/spec.md new file mode 100644 index 0000000..51385c9 --- /dev/null +++ b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/specs/package-series-management/spec.md @@ -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` diff --git a/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/tasks.md b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/tasks.md new file mode 100644 index 0000000..10cda6e --- /dev/null +++ b/openspec/changes/archive/2026-03-05-fix-tiered-commission-tier-fields/tasks.md @@ -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` diff --git a/openspec/specs/agent-series-grant/spec.md b/openspec/specs/agent-series-grant/spec.md index 9432eb2..a41f067 100644 --- a/openspec/specs/agent-series-grant/spec.md +++ b/openspec/specs/agent-series-grant/spec.md @@ -46,27 +46,28 @@ ### Requirement: 创建系列授权(梯度模式) -系统 SHALL 支持梯度模式的系列授权创建。梯度模式下,`commission_tiers` MUST 为必填,且必须包含与 PackageSeries 完全相同数量和阈值的阶梯(不多不少)。若某档位不希望给下级佣金,应将该档位的 amount 设为 0,不可省略该档位。 +系统 SHALL 支持梯度模式的系列授权创建。梯度模式下,`commission_tiers` MUST 为必填,且必须包含与 PackageSeries 完全相同数量和阈值的阶梯(不多不少)。若某档位不希望给下级佣金,应将该档位的 amount 设为 0,不可省略该档位。创建成功后的响应中,`commission_tiers` 每个档位 MUST 包含 `operator`、`dimension`、`stat_scope` 字段(从全局配置合并)。 #### Scenario: 代理成功创建梯度模式授权 -- **WHEN** 代理A 的专属阶梯为 [{operator:">=", threshold:100, amount:80}, {operator:">=", threshold:150, amount:120}],A 为代理B 创建授权,传入 commission_tiers=[{threshold:100, amount:50}, {threshold:150, amount:100}] -- **THEN** 系统创建授权,commission_tiers_json 存储 [{threshold:100, amount:50}, {threshold:150, amount:100}],响应中 commission_tiers=[{operator:">=", threshold:100, amount:50}, {operator:">=", threshold:150, amount:100}](operator 从 PackageSeries 读取后合并) +- **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:">=", 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 +- **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** 系统返回错误"梯度佣金档位金额不能超过上级同档位限额" +- **WHEN** 代理A 的阶梯第一档 `amount=80`,A 为 B 创建授权时传入第一档 `amount=90` +- **THEN** 系统返回错误“某档位佣金金额超过上级天花板” #### Scenario: 梯度模式传入了不存在的阈值 -- **WHEN** PackageSeries 只有 threshold=100 和 150 两档,请求中传入 threshold=200 -- **THEN** 系统返回错误"阶梯阈值与系列配置不匹配" +- **WHEN** PackageSeries 只有 `threshold=100` 和 `150` 两档,请求中传入 `threshold=200` +- **THEN** 系统返回错误“梯度阶梯 threshold 与系列配置不匹配” #### Scenario: 梯度模式 commission_tiers 为必填 -- **WHEN** 请求中不包含 commission_tiers 或为空数组 -- **THEN** 系统返回参数错误"梯度模式下必须提供阶梯金额配置" +- **WHEN** 请求中不包含 `commission_tiers` 或为空数组 +- **THEN** 系统返回参数错误“梯度模式必须填写阶梯配置” --- @@ -90,19 +91,33 @@ ### Requirement: 查询系列授权详情 -系统 SHALL 提供 `GET /shop-series-grants/:id` 接口,返回包含套餐列表的聚合视图。 +系统 SHALL 提供 `GET /shop-series-grants/:id` 接口,返回包含套餐列表的聚合视图。梯度模式下,`commission_tiers` 中每个档位 MUST 包含 `dimension`(统计维度)和 `stat_scope`(统计范围)字段,这两个字段从 PackageSeries 全局配置按 `threshold` 合并,对代理只读。 #### Scenario: 固定模式详情 - **WHEN** 查询固定模式系列授权详情 -- **THEN** 响应包含 commission_type="fixed",one_time_commission_amount=有效值,commission_tiers=[] +- **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}, ...] +- **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** 系统返回错误"授权记录不存在" +- **THEN** 系统返回错误“授权记录不存在” --- @@ -179,3 +194,31 @@ #### Scenario: 有下级依赖时禁止删除 - **WHEN** 删除一个已被下级代理用于创建子授权的记录 - **THEN** 系统返回错误"存在下级依赖,无法删除,请先删除下级授权" + +--- + +### Requirement: 系列授权列表强充状态正确反映 + +系列授权列表 (`GET /shop-series-grants`) MUST 在每个列表项中返回 `force_recharge_locked`(是否被套餐系列锁定)和 `force_recharge_amount`(强充金额)。`force_recharge_enabled` MUST 反映有效状态:当锁定时为 `true`(无论分配记录自身如何);未锁定时取分配记录的实际设置。 + +#### Scenario: 套餐系列锁定强充 +- **WHEN** 套餐系列配置 `enable_force_recharge=true` 或 `trigger_type=first_recharge`,查询列表 +- **THEN** 列表项中 `force_recharge_locked=true`,`force_recharge_enabled=true`,`force_recharge_amount`=系列配置的 `force_amount` + +#### Scenario: 代理自身开启强充(未锁定) +- **WHEN** 套餐系列未锁定强充,分配记录中 `enable_force_recharge=true`,查询列表 +- **THEN** 列表项中 `force_recharge_locked=false`,`force_recharge_enabled=true`,`force_recharge_amount`=分配记录的实际金额 + +#### Scenario: 代理未开启强充(未锁定) +- **WHEN** 套餐系列未锁定强充,分配记录中 `enable_force_recharge=false`,查询列表 +- **THEN** 列表项中 `force_recharge_locked=false`,`force_recharge_enabled=false`,`force_recharge_amount=0` + +--- + +### Requirement: 查询系列授权详情强充有效状态 + +系列授权详情 (`GET /shop-series-grants/:id`) 中,`force_recharge_enabled` MUST 反映有效状态:当锁定时为 `true`;`force_recharge_amount` 锁定时应返回系列配置的 `force_amount`。 + +#### Scenario: 锁定强充时详情响应 +- **WHEN** 套餐系列锁定强充,查询对应分配记录详情 +- **THEN** `force_recharge_locked=true`,`force_recharge_enabled=true`,`force_recharge_amount`=系列配置的 `force_amount` diff --git a/openspec/specs/package-series-management/spec.md b/openspec/specs/package-series-management/spec.md index d335218..b83f968 100644 --- a/openspec/specs/package-series-management/spec.md +++ b/openspec/specs/package-series-management/spec.md @@ -97,3 +97,40 @@ #### Scenario: 状态未变化 - **WHEN** 管理员设置的状态与当前状态相同 - **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`