This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-28
|
||||
@@ -0,0 +1,89 @@
|
||||
## Context
|
||||
|
||||
当前套餐价格在多个链路里被直接读取,后台配置、C 端展示、订单创建、自动购包和代理授权都在用同一批字段,但它们需要的其实不是同一种值。与此同时,赠送套餐已经被业务明确为独立语义,既不是普通可售套餐的别名,也不能靠 `package_type` 或数值 0 去推断。这个变更会同时影响 Handler、Service、Store、Model 和迁移脚本,必须先把语义拆开,再改各条链路。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 建立普通可售套餐的原始价与生效价分层。
|
||||
- 在建议零售价未配置时,统一回退到成本价。
|
||||
- 普通可售套餐不能显式配置 0 作为售卖价,0 只保留给赠送语义。
|
||||
- 把赠送套餐做成独立语义,平台专属,禁止代理分配和 C 端自购。
|
||||
- 保留原始配置值,避免把回退后的值写回配置页。
|
||||
- 完成历史数据与存量记录的迁移回填,并输出人工复核清单。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不重做套餐类型体系,也不把 gift 语义塞进 `package_type`。
|
||||
- 不新增外部依赖,不改用新的队列或存储组件。
|
||||
- 不在本次变更里重构整个套餐管理域,只处理价格与赠送政策相关路径。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 价格状态与价格数值分开存
|
||||
|
||||
采用独立的价格配置状态来区分“未配置”“赠送 0”“已配置为非 0”,原始建议零售价继续保留,生效售价由服务层计算。
|
||||
|
||||
**原因**:普通可售套餐不能显式卖 0,但赠送套餐仍可能使用 0,不能再用数值 0 当哨兵值。
|
||||
|
||||
**备选方案**:
|
||||
- 继续用 0 代表未配置,最简单,但会把合法 0 价和未配置混在一起,不能接受。
|
||||
- 直接覆盖原始字段为生效价,查询简单,但配置页会丢失真实原值。
|
||||
|
||||
### 2. 赠送语义独立于价格和套餐类型
|
||||
|
||||
新增独立的赠送语义标记,平台专属限制和价格规则彼此独立。
|
||||
|
||||
**原因**:平台-only、不可分配给代理、不可自购,这些都是业务权限问题,不是价格问题。
|
||||
|
||||
**备选方案**:
|
||||
- 仅靠 `package_type=formal/addon` 推断,已经被明确否决。
|
||||
- 仅靠价格为 0 推断,无法区分合法 0 价与未配置。
|
||||
|
||||
### 3. 生效价格在 Service 层统一计算
|
||||
|
||||
Handler 只接收和返回值,不做价格推导。Store 只取原始数据,Service 统一计算生效价并下发给下游。
|
||||
|
||||
**原因**:价格规则会同时影响 C 端列表、下单、自动购包和授权链路,放在 Service 层更容易复用,也更符合分层职责。
|
||||
|
||||
**备选方案**:
|
||||
- 在 Handler 里临时计算,容易散落和重复。
|
||||
- 在 Store 里拼接业务规则,会把数据访问和业务逻辑混在一起。
|
||||
|
||||
### 4. 迁移采用“先加字段,再回填,再切流”的顺序
|
||||
|
||||
先把新语义字段和状态字段上线,再用迁移脚本回填历史数据,最后让读链路切到生效价。
|
||||
|
||||
**原因**:这样可以避免一次性改动过大导致历史数据无法解释,也方便回滚。
|
||||
|
||||
**备选方案**:
|
||||
- 直接替换旧字段,风险太高,历史记录会失真。
|
||||
- 先切读后补数据,会出现短时间内的口径不一致。
|
||||
|
||||
### 5. 不引入价格缓存
|
||||
|
||||
价格计算只依赖单条套餐和分配记录,属于 O(1) 读取,不新增 Redis 缓存层。
|
||||
|
||||
**原因**:这次变更的核心是语义和一致性,不是性能瓶颈。现有分页与查询边界已经足够。
|
||||
|
||||
**备选方案**:
|
||||
- 引入缓存可以做,但会增加失效和回填复杂度,不适合作为首版方案。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [历史数据无法准确判断是否为赠送 0 价] → 迁移脚本优先读取历史发放记录和配置快照,只有能明确识别为赠送 0 的记录才保留为赠送 0,其余旧 0 一律转为未配置并输出待人工核验清单。
|
||||
- [赠送语义改动会影响多条购买链路] → 先在 Service 层统一封装判定函数,再逐步替换各入口的直接读取。
|
||||
- [价格展示口径切换后前端可能误解返回值] → 接口文档明确区分原始值、生效值和赠送语义,前后端一起切换。
|
||||
- [存量分配记录字段回填出错] → 迁移前后保留回滚脚本,按批次执行并校验记录数。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 增加价格状态字段、赠送语义字段和平台专属标记,保持原始价格字段不变。
|
||||
2. 回填存量套餐的价格状态和生效价,优先使用历史配置与发放记录判断。只有能明确识别为赠送 0 的记录才保留为赠送 0,其余旧 0 一律转为未配置,并输出人工复核清单。
|
||||
3. 回填分配记录、历史订单和自动购包相关快照,让展示链路能读取生效价。
|
||||
4. 切换 Service 层统一价格解析与赠送校验,再切换 Handler 输出。
|
||||
5. 验证完成后,清理旧的直接读取路径,保留原始值用于配置页和审计。
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- 人工复核清单只负责承接无法正向识别的旧 0 数据,不回写为赠送语义。
|
||||
- 赠送套餐历史识别优先以后台发放记录为准,补录清单只作为迁移输入,不改变判定标准。
|
||||
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
当前套餐价格在后台采购、C 端展示、自动购包和分配记录里口径不一致,`suggested_retail_price` 被直接当成展示价和成交价使用,空配置、旧 0 价和赠送语义也混在一起。现在业务已经明确两件事,普通可售套餐不能显式卖 0,赠送套餐则是独立语义,必须平台专属、仅后端发放,不能再继续靠价格字段临时凑语义。
|
||||
|
||||
## What Changes
|
||||
|
||||
- feature-001-package-effective-price-fallback:普通可售套餐在建议零售价未配置时,生效售价回退到成本价,覆盖后台采购、C 端展示与下单、自动购包及相关链路。
|
||||
- feature-002-package-raw-vs-effective-price-view:配置页和编辑页继续展示原始值,销售页和购买页切换为展示生效值,避免把“配置值”和“成交值”混为一谈。
|
||||
- feature-003-gift-package-independent-semantic:新增赠送套餐独立语义,不再依赖套餐类型去硬解释赠送行为,且必须与价格语义分离。
|
||||
- feature-004-platform-only-gift-package-policy:赠送套餐仅允许平台侧创建和发放,不能分配给代理,也不能出现在 C 端可购买套餐列表中。
|
||||
- feature-005-backend-grant-not-self-purchase:赠送加油包只能通过后台发放进入用户资产,不允许用户自购或前台下单获得。
|
||||
- feature-006-history-and-migration:补齐历史数据与迁移规则,保证存量数据只保留可明确识别的“赠送 0 价”,其余旧 0 一律转为未配置并进入人工复核清单。
|
||||
- **BREAKING** 价格与赠送语义不再共享同一含义,任何依赖旧口径的列表、下单、分配和展示链路都需要按新规则调整。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `package-price-policy`:定义普通可售套餐的原始价、生效价、回退规则、展示口径和历史迁移规则。
|
||||
- `gift-package-policy`:定义赠送套餐的独立语义、平台专属限制、后端发放规则和 C 端可见性规则。
|
||||
|
||||
### Modified Capabilities
|
||||
- `package-management`:套餐配置、编辑和详情页要按原始价与生效价分层展示。
|
||||
- `package-purchase-validation`:购买校验要使用生效价,并排除赠送套餐的自购路径。
|
||||
- `client-asset-info`:C 端可购买套餐列表要按生效价展示,并隐藏不可购买的赠送套餐。
|
||||
- `client-order-purchase`:C 端列表、下单和自动购包要使用生效价,并隐藏不可购买的赠送套餐。
|
||||
- `agent-series-grant`:套餐授权和发放链路要识别赠送套餐的平台专属属性,禁止向代理分配。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响模块:`internal/service/purchase_validation/service.go`、`internal/handler/app/client_asset.go`、`internal/service/client_order/service.go`、`internal/service/order/service.go`、`internal/task/auto_purchase.go`、`internal/service/shop_package_batch_allocation/service.go`、`internal/service/shop_series_grant/service.go`。
|
||||
- 影响数据:套餐模型、分配记录、历史回填字段和迁移脚本都需要支持原始值、生效值与赠送语义的并存。
|
||||
- 影响 API:后台套餐管理接口、C 端套餐列表与下单接口、代理授权与平台发放接口都会出现返回值和校验规则变化。
|
||||
- 影响文档:需要同步更新 OpenSpec、接口文档和字段说明,确保“未配置价格”“赠送 0 价”“平台专属不可分配”三种语义不会再混用。
|
||||
- 验证方式:实现阶段按项目约束使用手工接口验证和 PostgreSQL 数据核验,不引入新的自动化测试要求。
|
||||
@@ -0,0 +1,17 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 赠送套餐不可纳入代理系列授权
|
||||
|
||||
系统 SHALL 将赠送套餐视为平台专属资产,禁止将其加入代理系列授权、套餐分配或任何下级可见的授权列表。赠送套餐的发放 MUST 走后台发放链路,而不是代理授权链路。
|
||||
|
||||
#### Scenario: 代理创建授权时包含赠送套餐
|
||||
- **WHEN** 代理在系列授权中尝试加入一个赠送套餐
|
||||
- **THEN** 系统返回错误并拒绝创建
|
||||
|
||||
#### Scenario: 平台后台发放赠送套餐
|
||||
- **WHEN** 平台管理员通过后台发放流程授予赠送套餐
|
||||
- **THEN** 系统允许创建发放记录,但不生成代理可分配权限
|
||||
|
||||
#### Scenario: 授权列表隐藏赠送套餐
|
||||
- **WHEN** 系统查询任意代理系列授权的可选套餐列表
|
||||
- **THEN** 赠送套餐 MUST 不出现在列表中
|
||||
@@ -0,0 +1,13 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: B2 可购买套餐列表接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/c/v1/asset/packages?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 在归属校验通过后返回可购买套餐列表。价格规则 MUST 为:代理渠道取分配记录上的生效售价,平台渠道取套餐生效售价。若建议零售价未配置,系统 SHALL 使用成本价作为生效售价。过滤规则 MUST 同时满足:`Package.status=1`、`shelf_status` 可售、加油包前置主套餐条件成立,并且 MUST 排除赠送套餐。结果 MUST 按展示价格升序。响应体 SHALL 包含 `packages[]`,每项至少含 `package_id`、`package_name`、`package_type`、`retail_price`、`cost_price`、`validity`、`is_addon`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误`、`FORBIDDEN/无权限操作该资产或资源不存在`、`PACKAGE_NOT_AVAILABLE/当前无可购买套餐`。
|
||||
|
||||
#### Scenario: 代理渠道价格与过滤生效
|
||||
- **WHEN** 客户查询可购套餐且其销售链路为代理渠道,部分套餐建议零售价未配置
|
||||
- **THEN** 系统仅返回可售且满足价格约束的套餐,并按生效价格升序输出
|
||||
|
||||
#### Scenario: 赠送套餐不出现在可购列表
|
||||
- **WHEN** 赠送套餐已存在于后台数据中
|
||||
- **THEN** C 端可购买套餐列表 MUST 不返回该套餐
|
||||
@@ -0,0 +1,17 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 订单与自动购包价格规则
|
||||
|
||||
系统 SHALL 在既有订单创建和自动购包流程中统一使用生效售价。普通可售套餐若建议零售价未配置,则生效售价 MUST 回退到成本价;赠送套餐 MUST NOT 通过这些自购或自动购包路径创建。
|
||||
|
||||
#### Scenario: 普通可售套餐未配置建议零售价
|
||||
- **WHEN** 订单创建或自动购包处理一个普通可售套餐,且其建议零售价未配置
|
||||
- **THEN** 系统 SHALL 使用成本价作为生效售价
|
||||
|
||||
#### Scenario: 普通可售套餐已配置建议零售价
|
||||
- **WHEN** 订单创建或自动购包处理一个普通可售套餐,且其建议零售价已配置
|
||||
- **THEN** 系统 SHALL 使用已配置的建议零售价作为生效售价
|
||||
|
||||
#### Scenario: 赠送套餐进入购买路径
|
||||
- **WHEN** 任一自购或自动购包路径命中赠送套餐
|
||||
- **THEN** 系统 MUST 拒绝创建订单
|
||||
@@ -0,0 +1,49 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 赠送套餐语义必须显式标记,且 0 价仅用于赠送语义
|
||||
|
||||
系统 MUST 将赠送套餐视为独立业务语义,不能仅通过 `package_type` 推导赠送含义。若套餐显式配置为 0 价,则该 0 价 MUST 仅用于赠送语义。
|
||||
|
||||
#### Scenario: 赠送语义不能仅靠 package_type 推导
|
||||
- **WHEN** 套餐类型为 formal 或 addon,但未显式标记为赠送
|
||||
- **THEN** 系统 MUST NOT 仅凭套餐类型判断其是否为赠送套餐
|
||||
|
||||
#### Scenario: 赠送套餐显式使用 0 价
|
||||
- **WHEN** 平台创建赠送套餐并将建议零售价显式配置为 0
|
||||
- **THEN** 系统 MUST 保留该 0 价配置并将其识别为赠送语义
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 赠送套餐仅限平台侧且不可分配给代理
|
||||
|
||||
系统 SHALL 将赠送套餐限定为平台侧能力。赠送套餐 MUST NOT 被分配给代理,也 MUST NOT 出现在任何代理可操作的套餐授权列表中。
|
||||
|
||||
#### Scenario: 平台可创建赠送套餐
|
||||
- **WHEN** 平台管理员创建赠送套餐
|
||||
- **THEN** 系统允许保存该赠送语义
|
||||
|
||||
#### Scenario: 代理不能分配赠送套餐
|
||||
- **WHEN** 代理尝试将赠送套餐加入自己的授权或分配给下级
|
||||
- **THEN** 系统返回错误并拒绝操作
|
||||
|
||||
#### Scenario: 赠送套餐不进入代理可售列表
|
||||
- **WHEN** 系统生成代理可见的套餐授权列表
|
||||
- **THEN** 赠送套餐 MUST 被过滤掉
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 赠送套餐仅能后台发放,不能前台自购
|
||||
|
||||
系统 SHALL 仅允许通过后台发放流程创建赠送套餐资产。C 端与前台购买路径 MUST NOT 直接购买赠送套餐,且赠送套餐 MUST NOT 出现在 C 端可购买套餐列表中。
|
||||
|
||||
#### Scenario: 后台发放赠送加油包
|
||||
- **WHEN** 平台通过后台发放赠送加油包
|
||||
- **THEN** 系统创建发放记录并授予目标资产
|
||||
|
||||
#### Scenario: C 端不能自购赠送套餐
|
||||
- **WHEN** 个人客户在 C 端尝试购买赠送套餐
|
||||
- **THEN** 系统返回不可购买错误
|
||||
|
||||
#### Scenario: 赠送套餐不出现在 C 端可购列表
|
||||
- **WHEN** C 端请求可购买套餐列表
|
||||
- **THEN** 系统 MUST 不返回任何赠送套餐
|
||||
@@ -0,0 +1,29 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 套餐配置页展示原始价格与价格状态
|
||||
|
||||
系统 SHALL 在套餐创建、编辑、详情接口中返回原始建议成本价、原始建议零售价和价格配置状态。配置页 MUST 以原始值为准展示,不得用生效价覆盖原始值。
|
||||
|
||||
普通可售套餐 MUST NOT 把显式配置的建议零售价 0 作为正常销售路径;该 0 价仅应保留给赠送语义使用。
|
||||
|
||||
#### Scenario: 编辑页展示未配置价格
|
||||
- **WHEN** 管理员打开一个建议零售价未配置的套餐编辑页
|
||||
- **THEN** 页面 SHALL 显示原始建议零售价为空或未配置状态
|
||||
|
||||
#### Scenario: 编辑页展示赠送 0 价
|
||||
- **WHEN** 管理员打开一个已标记为赠送语义且建议零售价显式配置为 0 的套餐编辑页
|
||||
- **THEN** 页面 SHALL 显示原始建议零售价为 0,并同时展示赠送语义与已配置状态
|
||||
|
||||
#### Scenario: 普通可售套餐创建时填写 0 被拒绝
|
||||
- **WHEN** 管理员创建一个普通可售套餐,并将建议零售价填写为 0
|
||||
- **THEN** 系统 SHALL 拒绝保存
|
||||
- **THEN** 系统 SHALL 提示普通可售套餐不允许显式 0
|
||||
|
||||
#### Scenario: 普通可售套餐编辑时改成 0 被拒绝
|
||||
- **WHEN** 管理员编辑一个普通可售套餐,并将原建议零售价修改为 0
|
||||
- **THEN** 系统 SHALL 拒绝保存
|
||||
- **THEN** 系统 SHALL 保持原有价格状态不变
|
||||
|
||||
#### Scenario: 详情页不回填生效价
|
||||
- **WHEN** 管理员查看套餐详情
|
||||
- **THEN** 系统 SHALL 返回原始价格字段,而不是把回退后的生效价写回原始字段
|
||||
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 价格配置状态与原始值分离
|
||||
|
||||
系统 MUST 同时保存套餐价格的原始配置值和价格配置状态,且 MUST 区分以下三种语义:未配置、赠送 0、已配置为非 0。普通可售套餐 MUST NOT 显式配置 0 作为售卖价,0 仅可用于赠送语义。配置页与编辑页 SHALL 展示原始值,不能用生效价覆盖原始值。
|
||||
|
||||
#### Scenario: 未配置价格与 0 价必须可区分
|
||||
- **WHEN** 管理员创建两个套餐,一个建议零售价未配置,一个已被标记为赠送且建议零售价显式配置为 0
|
||||
- **THEN** 系统 MUST 为两者保存不同的价格配置状态
|
||||
- **THEN** 系统 MUST NOT 仅依赖数值 0 判断价格是否已配置
|
||||
|
||||
#### Scenario: 普通可售套餐显式 0 被拒绝
|
||||
- **WHEN** 管理员创建或编辑一个普通可售套餐,并将建议零售价填写为 0
|
||||
- **THEN** 系统 MUST 拒绝保存
|
||||
- **THEN** 系统 MUST 返回普通可售套餐不允许显式 0 的错误
|
||||
|
||||
#### Scenario: 配置页展示原始值
|
||||
- **WHEN** 管理员进入套餐详情或编辑页
|
||||
- **THEN** 页面 SHALL 展示原始建议成本价和原始建议零售价
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 普通可售套餐的生效售价回退
|
||||
|
||||
系统 SHALL 对普通可售套餐计算生效售价。若建议零售价未配置,生效售价 MUST 回退为成本价。该规则 MUST 适用于后台采购、C 端展示与下单、自动购包以及所有依赖套餐成交价的链路。
|
||||
|
||||
#### Scenario: 建议零售价未配置时回退到成本价
|
||||
- **WHEN** 普通可售套餐的建议零售价未配置,成本价为 8000 分
|
||||
- **THEN** 系统返回的生效售价 MUST 为 8000 分
|
||||
|
||||
#### Scenario: 建议零售价已配置时保持原值
|
||||
- **WHEN** 普通可售套餐的建议零售价已配置为 12000 分,成本价为 8000 分
|
||||
- **THEN** 系统返回的生效售价 MUST 为 12000 分
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 历史数据回填与迁移
|
||||
|
||||
系统 MUST 对存量套餐、分配记录和订单相关价格快照执行迁移回填,补齐价格配置状态和生效价字段。迁移后,只有能被正向识别为赠送 0 的记录才保留为赠送 0,其余历史 0 数据 MUST 转为未配置并进入人工复核清单。
|
||||
|
||||
#### Scenario: 存量未配置价格被回填为未配置状态
|
||||
- **WHEN** 迁移执行前某套餐历史上从未配置过建议零售价
|
||||
- **THEN** 迁移后该套餐 MUST 保持未配置状态
|
||||
- **THEN** 其生效售价 MUST 按成本价回填
|
||||
|
||||
#### Scenario: 正向识别的赠送 0 被保留
|
||||
- **WHEN** 迁移执行前某套餐的建议零售价为 0,且历史记录能明确证明其为赠送语义
|
||||
- **THEN** 迁移后系统 MUST 保留赠送 0 状态
|
||||
|
||||
#### Scenario: 模糊旧 0 转为未配置并进入人工复核
|
||||
- **WHEN** 迁移执行前某套餐的建议零售价为 0,但历史记录无法明确证明其为赠送语义
|
||||
- **THEN** 迁移后系统 MUST 将其转为未配置状态
|
||||
- **THEN** 系统 MUST 将该记录加入人工复核清单
|
||||
|
||||
#### Scenario: 历史快照按生效价展示
|
||||
- **WHEN** 用户查询历史订单或历史分配记录
|
||||
- **THEN** 系统 SHALL 使用迁移后的生效价字段进行展示
|
||||
@@ -0,0 +1,48 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 获取购买价格
|
||||
|
||||
系统 MUST 根据买家身份返回正确的购买价格,并且 MUST 使用生效售价,不得直接把未配置的建议零售价当成交价返回。个人客户、平台自营和自动购包路径 SHALL 以套餐生效售价为准;代理为店铺购买时 SHALL 以分配记录上的生效售价为准。
|
||||
|
||||
#### Scenario: 个人客户购买
|
||||
- **WHEN** 个人客户购买一个普通可售套餐,且建议零售价未配置
|
||||
- **THEN** 系统 SHALL 返回回退后的生效售价,金额等于成本价
|
||||
|
||||
#### Scenario: 代理为店铺购买
|
||||
- **WHEN** 代理为自己店铺购买套餐
|
||||
- **THEN** 系统 SHALL 返回该分配记录对应的生效售价
|
||||
|
||||
---
|
||||
|
||||
### Requirement: validatePackages() 价格累加与展示校验
|
||||
|
||||
系统 MUST 在 `validatePackages()` 中按渠道来源使用一致的生效售价进行累加计算,并在代理渠道保持价格展示可见性校验。未配置建议零售价时,累计金额 MUST 使用回退后的生效售价,而不是原始空值。
|
||||
|
||||
#### Scenario: 代理渠道累加使用生效售价
|
||||
- **WHEN** `validatePackages()` 处理代理渠道的多套餐下单
|
||||
- **THEN** 总价累加 MUST 基于各套餐的生效售价
|
||||
|
||||
#### Scenario: 平台渠道累加使用生效售价
|
||||
- **WHEN** `validatePackages()` 处理平台渠道的多套餐下单
|
||||
- **THEN** 总价累加 MUST 基于各套餐的生效售价
|
||||
|
||||
#### Scenario: 未配置价格不再触发错误展示
|
||||
- **WHEN** 某个普通可售套餐的建议零售价未配置
|
||||
- **THEN** 系统 SHALL 使用回退后的生效售价继续校验
|
||||
- **THEN** 系统 MUST NOT 因原始建议零售价为空而把该套餐当成异常价格直接拦截
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 赠送套餐禁止进入自购下单
|
||||
|
||||
系统 SHALL 拒绝任何用户通过自购路径购买赠送套餐。赠送加油包只能通过后台发放进入资产,不能通过 C 端或后台自助下单购买。
|
||||
|
||||
赠送场景 SHALL 仅允许使用显式配置的建议零售价 0,但该 0 价仅表示赠送语义,不能作为普通可售套餐的正常销售路径。
|
||||
|
||||
#### Scenario: C 端自购赠送套餐被拦截
|
||||
- **WHEN** 个人客户尝试购买一个启用赠送语义的套餐
|
||||
- **THEN** 系统返回不可购买错误,订单不创建
|
||||
|
||||
#### Scenario: 后台自助下单赠送套餐被拦截
|
||||
- **WHEN** 后台操作者尝试通过普通购买接口创建赠送套餐订单
|
||||
- **THEN** 系统 MUST 拒绝该订单
|
||||
@@ -0,0 +1,33 @@
|
||||
## 1. 验证准备
|
||||
|
||||
- [x] 1.1 梳理现有价格读取、赠送发放和分配链路,整理一份手工验证清单,覆盖后台配置、C 端列表、下单、自动购包和授权发放。
|
||||
- [x] 1.2 记录当前会受影响的接口与数据表,确认回滚点和历史数据来源,避免后续迁移时把未配置价格、赠送 0 价和旧的模糊 0 数据混在一起。
|
||||
- [x] 1.3 对涉及的现有实现文件先跑一次 `lsp_diagnostics`,保留基线结果,作为改动前对比。
|
||||
|
||||
## 2. 数据模型与迁移
|
||||
|
||||
- [x] 2.1 为套餐及相关价格快照补充价格状态、赠送语义和平台专属标记的迁移设计,保持原始价格字段不变,并明确普通可售套餐不能显式配置 0。
|
||||
- [x] 2.2 编写 golang-migrate 迁移文件,先加字段,再回填存量数据,确保历史记录能区分未配置价格、可明确识别的赠送 0 价和需要人工复核的旧 0 数据。
|
||||
- [x] 2.3 补齐历史回填逻辑和回滚脚本,完成后用数据库核验确认字段状态、默认值、人工复核清单和历史记录数量一致。
|
||||
- [x] 2.4 对迁移相关文件和模型定义运行 `lsp_diagnostics`,确认没有语法或引用错误。
|
||||
|
||||
## 3. 价格回退与展示链路
|
||||
|
||||
- [x] 3.1 提炼统一的生效价解析方法,确保普通可售套餐在建议零售价未配置时回退到成本价,同时保留原始值供配置页使用。
|
||||
- [x] 3.2 更新后台套餐配置、编辑和详情返回值,只展示原始价格与价格状态,不把生效价写回配置字段。
|
||||
- [x] 3.3 更新 C 端可购套餐列表、订单创建和自动购包链路,统一使用生效价累加、展示和落单。
|
||||
- [ ] 3.4 对价格回退链路做接口回放和手工验收,确认未配置价格回退正确、普通可售套餐显式 0 被拒绝、赠送 0 保持赠送语义。
|
||||
- [x] 3.5 对改动后的价格相关服务、处理器和任务文件运行 `lsp_diagnostics`。
|
||||
|
||||
## 4. 赠送套餐政策与授权发放
|
||||
|
||||
- [x] 4.1 在套餐授权与分配链路加入赠送语义校验,禁止代理拿到、转发或分配赠送套餐。
|
||||
- [x] 4.2 在 C 端可购列表和购买校验里隐藏并拦截赠送套餐,确保用户无法通过自购路径获取。
|
||||
- [x] 4.3 保留后台发放入口的赠送套餐授予能力,确保赠送加油包只能通过后台 grant 进入资产,同时在后台创建、编辑时拒绝非赠送套餐的显式 0。
|
||||
- [ ] 4.4 对赠送政策相关的服务、接口和任务文件运行 `lsp_diagnostics`,并用正反例手工核验平台专属与不可自购规则。
|
||||
|
||||
## 5. 文档与最终验收
|
||||
|
||||
- [x] 5.1 更新接口文档、字段说明和变更说明,明确原始值、生效值、未配置价格、赠送 0 价和普通套餐显式 0 被拒绝的区别。
|
||||
- [x] 5.2 重新整理一份最终验收清单,覆盖价格回退、赠送拦截、平台发放和历史迁移四类核心场景。
|
||||
- [ ] 5.3 对所有改动文件做最终 `lsp_diagnostics`,再执行一次完整手工回归,确认没有遗漏的旧口径入口。
|
||||
@@ -0,0 +1,98 @@
|
||||
# 实施基线与手工验证清单
|
||||
|
||||
## 一、当前受影响接口与链路
|
||||
|
||||
### 1. 后台套餐管理
|
||||
- `GET /api/admin/packages`
|
||||
- `GET /api/admin/packages/:id`
|
||||
- `POST /api/admin/packages`
|
||||
- `PUT /api/admin/packages/:id`
|
||||
- `PUT /api/admin/packages/:id/shelf-status`
|
||||
- `PUT /api/admin/packages/:id/retail-price`
|
||||
|
||||
### 2. C 端可购套餐与下单
|
||||
- `GET /api/c/v1/asset/packages?identifier=xxx`
|
||||
- C 端创建订单链路(`internal/service/client_order/service.go`)
|
||||
- 充值后自动购包链路(`internal/task/auto_purchase.go`)
|
||||
|
||||
### 3. 后台订单与发放链路
|
||||
- 后台创建订单(`internal/service/order/service.go`)
|
||||
- 套餐激活链路(`internal/service/order/service.go`、`internal/service/package/activation_service.go`)
|
||||
|
||||
### 4. 代理授权与分配
|
||||
- 系列授权创建/维护(`internal/service/shop_series_grant/service.go`)
|
||||
- 批量分配(`internal/service/shop_package_batch_allocation/service.go`)
|
||||
|
||||
## 二、当前受影响数据表
|
||||
|
||||
- `tb_package`:套餐主数据,当前仅有 `cost_price`、`suggested_retail_price`
|
||||
- `tb_shop_package_allocation`:代理分配记录,当前仅有 `cost_price`、`retail_price`
|
||||
- `tb_order`:订单主表
|
||||
- `tb_order_item`:订单价格快照
|
||||
- `tb_package_usage`:套餐使用记录快照
|
||||
- `tb_shop_package_allocation_price_history`:当前仅记录成本价变更历史
|
||||
|
||||
## 三、回滚点与历史数据来源
|
||||
|
||||
### 1. 回滚点
|
||||
- 迁移文件回滚:撤销新增字段、清理人工复核清单表
|
||||
- Service 层回滚:恢复到直接读取 `suggested_retail_price` / `retail_price` 的旧逻辑
|
||||
- 发放策略回滚:撤销后台赠送订单专属分支,恢复统一后台订单逻辑
|
||||
|
||||
### 2. 历史数据判定来源
|
||||
- `tb_package.suggested_retail_price`
|
||||
- `tb_shop_package_allocation.retail_price`
|
||||
- `tb_order_item.unit_price`
|
||||
- `tb_order.total_amount` / `tb_order.actual_paid_amount`
|
||||
- `tb_package_usage.paid_amount`
|
||||
- 后台创建订单记录(用于识别“发放=后台创建订单”的赠送语义)
|
||||
|
||||
## 四、改动前诊断基线
|
||||
|
||||
### 1. 无阻塞诊断错误的核心文件
|
||||
- `internal/model/package.go`
|
||||
- `internal/service/purchase_validation/service.go`
|
||||
- `internal/service/client_order/service.go`
|
||||
- `internal/task/auto_purchase.go`
|
||||
- `internal/service/shop_package_batch_allocation/service.go`
|
||||
- `internal/service/shop_series_grant/service.go`
|
||||
|
||||
### 2. 已知基线提示(非本次改动引入)
|
||||
- `internal/service/order/service.go` 存在若干 `QF1003` 提示、`unusedparams` 提示和既有 `nilness` warning,作为改动前基线保留
|
||||
- `internal/handler/app/client_asset.go` 存在 `unusedfunc` 基线提示
|
||||
|
||||
## 五、手工验证清单
|
||||
|
||||
### 1. 后台套餐配置
|
||||
- 创建普通可售套餐,建议售价留空,保存成功
|
||||
- 创建普通可售套餐,建议售价填 `0`,保存失败
|
||||
- 创建赠送套餐,建议售价填 `0`,保存成功
|
||||
- 编辑普通可售套餐将建议售价改成 `0`,保存失败
|
||||
- 详情页/编辑页能区分“未配置价格”和“赠送 0 价”
|
||||
|
||||
### 2. C 端可购列表
|
||||
- 平台渠道普通套餐未配置建议售价时,列表显示成本价
|
||||
- 代理渠道分配记录未配置零售价时,列表显示成本价
|
||||
- 赠送套餐不出现在可购列表
|
||||
- 无主套餐时,加油包仍然不可见
|
||||
|
||||
### 3. 下单与自动购包
|
||||
- C 端普通套餐下单按生效价落单
|
||||
- H5/后台普通购买接口无法购买赠送套餐
|
||||
- 自动购包命中赠送套餐时被拦截
|
||||
- 同一订单仍禁止正式套餐和加油包混买
|
||||
|
||||
### 4. 后台发放(走创建订单)
|
||||
- 平台后台可为目标资产创建赠送订单并激活套餐
|
||||
- 赠送订单无需支付凭证,不产生正常购买语义
|
||||
- 赠送加油包必须已有主套餐,否则发放失败
|
||||
|
||||
### 5. 代理授权与分配
|
||||
- 代理授权列表不展示赠送套餐
|
||||
- 系列授权新增赠送套餐失败
|
||||
- 批量分配不会把赠送套餐分配给代理
|
||||
|
||||
### 6. 迁移与历史数据
|
||||
- 迁移后能区分未配置价格、赠送 0 价、已配置非 0
|
||||
- 历史模糊 0 数据进入人工复核清单
|
||||
- 历史快照查询不再把普通未配置价格当成显式 0 售卖价
|
||||
Reference in New Issue
Block a user