Files
junhong_cmp_fiber/openspec/specs/package-purchase-validation/spec.md
huang 80c6f6c756
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
主要变更:
- 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等)
- 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束
- 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析
- 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合
- 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝
- Excel 工具函数优化,前端导入指引文档同步更新
- 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no
- 更新 OpenAPI 文档及相关 DTO

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-07 17:39:36 +08:00

140 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# package-purchase-validation Specification
## Purpose
套餐购买验证 - 定义客户端购买套餐前的权限、状态、价格及设备卡验证规则。
## Requirements
### Requirement: 验证卡/设备的套餐购买权限
创建订单前系统 MUST 验证卡/设备是否有权购买指定套餐。
#### Scenario: 卡有套餐系列关联
- **WHEN** 卡的 series_allocation_id 有值,且套餐属于该系列
- **THEN** 验证通过
#### Scenario: 卡无套餐系列关联
- **WHEN** 卡的 series_allocation_id 为空
- **THEN** 验证失败,返回 "该卡未关联套餐系列"
#### Scenario: 套餐不属于关联系列
- **WHEN** 套餐的 series_id 与卡关联的分配系列不匹配
- **THEN** 验证失败,返回 "该套餐不在可购买范围内"
#### Scenario: 系列分配已禁用
- **WHEN** 卡关联的系列分配状态为禁用
- **THEN** 验证失败,返回 "套餐系列已禁用"
---
### Requirement: 验证套餐状态
创建订单前系统 MUST 验证套餐处于可购买状态。**校验逻辑因购买场景而不同**:通过代理渠道购买时检查代理分配记录的 shelf_status通过平台自营渠道购买时检查套餐全局 shelf_status。`Package.status`(启用/禁用)为全局开关,任何场景下都必须检查。
#### Scenario: 代理渠道 - 套餐启用且代理上架
- **GIVEN** `Package.status=1`(启用),卖家代理的 `allocation.shelf_status=1`(上架)
- **WHEN** 客户通过该代理下单购买套餐
- **THEN** 套餐状态校验通过
#### Scenario: 代理渠道 - 套餐已禁用
- **GIVEN** `Package.status=2`(禁用)
- **WHEN** 客户通过任意代理下单购买套餐
- **THEN** 验证失败,返回 "套餐已禁用"
#### Scenario: 代理渠道 - 代理已下架套餐
- **GIVEN** `Package.status=1`(启用),卖家代理的 `allocation.shelf_status=2`(代理下架)
- **WHEN** 客户通过该代理下单购买套餐
- **THEN** 验证失败,返回 "套餐已下架"
#### Scenario: 代理渠道 - 平台下架不影响代理销售
- **GIVEN** `Package.status=1`(启用),`Package.shelf_status=2`(平台下架),卖家代理的 `allocation.shelf_status=1`(代理上架)
- **WHEN** 客户通过该代理下单购买套餐
- **THEN** 套餐状态校验通过(平台 shelf_status 不参与代理渠道校验)
#### Scenario: 平台自营渠道 - 套餐启用且平台上架
- **GIVEN** `Package.status=1`(启用),`Package.shelf_status=1`(平台上架)
- **WHEN** 客户通过平台自营渠道下单购买套餐
- **THEN** 套餐状态校验通过
#### Scenario: 平台自营渠道 - 套餐已下架
- **GIVEN** `Package.status=1`(启用),`Package.shelf_status=2`(平台下架)
- **WHEN** 客户通过平台自营渠道下单购买套餐
- **THEN** 验证失败,返回 "套餐已下架"
---
### Requirement: 获取购买价格
系统 MUST 根据买家身份返回正确的购买价格。
#### Scenario: 个人客户购买
- **WHEN** 个人客户购买套餐
- **THEN** 使用 Package.suggested_retail_price 作为支付金额
#### Scenario: 代理为店铺购买
- **WHEN** 代理为自己店铺购买套餐(囤货/测试)
- **THEN** 使用代理的成本价作为支付金额
---
### Requirement: 设备购买时的卡验证
设备购买套餐时 MUST 使用设备的 series_allocation_id 验证,不使用设备下单卡的关联。
#### Scenario: 设备有系列关联
- **WHEN** 设备的 series_allocation_id 有值
- **THEN** 使用设备的关联验证购买权限
#### Scenario: 设备无系列关联
- **WHEN** 设备的 series_allocation_id 为空
- **THEN** 验证失败,返回 "该设备未关联套餐系列"
### Requirement: 代理渠道购买价格规则
系统 MUST 根据购买渠道返回正确的购买价格:代理渠道使用 `allocation.retail_price`,平台渠道使用 `Package.SuggestedRetailPrice`
#### Scenario: 代理渠道使用分配零售价
- **WHEN** 客户通过代理渠道购买套餐
- **THEN** 系统 MUST 使用 `allocation.retail_price` 作为支付金额
#### Scenario: 平台渠道使用套餐建议零售价
- **WHEN** 客户通过平台自营渠道购买套餐
- **THEN** 系统 MUST 使用 `Package.SuggestedRetailPrice` 作为支付金额
---
### Requirement: validatePackages 价格累加与展示校验
系统 MUST 在 `validatePackages()` 中按渠道来源使用一致的价格来源进行累加计算,并在代理渠道增加价格展示可见性校验。
#### Scenario: 代理渠道累加使用 retail_price
- **WHEN** `validatePackages()` 处理代理渠道的多套餐下单
- **THEN** 总价累加 MUST 基于各套餐的 `allocation.retail_price`
#### Scenario: 平台渠道累加使用 SuggestedRetailPrice
- **WHEN** `validatePackages()` 处理平台渠道的多套餐下单
- **THEN** 总价累加 MUST 基于各套餐的 `Package.SuggestedRetailPrice`
#### Scenario: 代理渠道过滤异常零售价
- **WHEN** 代理渠道某套餐存在 `retail_price < cost_price`
- **THEN** 系统 MUST 不展示该套餐,且不允许该套餐进入下单校验
---
### Requirement: 已绑定设备的卡不允许单独购买套餐
购买套餐验证时,系统 MUST 检查目标 IoT 卡是否为独立卡(`is_standalone = true`)。若卡已绑定设备(`is_standalone = false`),必须拒绝购买并引导用户至对应设备页面操作。
此规则适用于所有套餐购买入口C 端个人客户、B 端代理/平台)。
#### Scenario: 独立卡正常购买
- **WHEN** 买家为 IoT 卡购买套餐,该卡的 `is_standalone = true`(未绑定任何设备)
- **THEN** 验证通过,继续后续购买流程
#### Scenario: 已绑定设备的卡被单独购买
- **WHEN** 买家使用 IoT 卡的 ICCID 或 VirtualNo 购买套餐,该卡的 `is_standalone = false`
- **THEN** 系统拒绝购买,返回错误码 `CodeInvalidParam`,错误消息"该卡已绑定设备,请前往设备页面购买套餐"
#### Scenario: B 端代理通过卡标识符购买套餐(绑定了设备的卡)
- **WHEN** 代理使用已绑定设备的卡的 ICCID 创建订单
- **THEN** 系统在 `ValidateCardPurchase()` 中检查 `is_standalone`,返回错误,订单不创建