All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
- OneTimeCommissionTierDTO 补充 operator 字段映射 - GrantCommissionTierItem 补充 dimension/stat_scope 字段(从全局配置合并) - 系列授权列表/详情补充强充锁定状态和强充金额的有效值计算 - 同步 OpenSpec 主规范并归档变更文档
67 lines
5.5 KiB
Markdown
67 lines
5.5 KiB
Markdown
## 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,不需迁移 |
|