## 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,不需迁移 |