- OneTimeCommissionTierDTO 补充 operator 字段映射 - GrantCommissionTierItem 补充 dimension/stat_scope 字段(从全局配置合并) - 系列授权列表/详情补充强充锁定状态和强充金额的有效值计算 - 同步 OpenSpec 主规范并归档变更文档
5.5 KiB
Context
refactor-agent-series-grant 变更在 Model 层(model.OneTimeCommissionTier)已正确新增 Operator 字段,并添加了运算符常量(TierOperatorGT/GTE/LT/LTE),但遗漏了同步更新 DTO 层和 Service 层的映射逻辑。
具体遗漏:
OneTimeCommissionTierDTO(package_series_dto.go)未加Operator字段 → API 无法读写 operatorGrantCommissionTierItem(shop_series_grant_dto.go)未加Dimension、StatScope字段 → grant 响应中梯度档位条件不透明package_series/service.go的dtoToModelConfig()/modelToDTO()未处理Operator映射shop_series_grant/service.go的buildGrantResponse()合并全局 tiers 时未携带Dimension、StatScope- 系列授权列表/详情响应未正确反映
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,不需迁移 |