Files
junhong_cmp_fiber/openspec/specs/iot-package/spec.md
huang 034f00e2e7 实现 IoT SIM 管理模块数据模型和数据库结构
- 添加 IoT 核心业务表:运营商、IoT 卡、设备、号卡、套餐、订单等
- 添加分佣系统表:分佣规则、分佣记录、运营商结算等
- 添加轮询和流量管理表:轮询配置、流量使用记录等
- 添加财务和系统管理表:佣金提现、换卡申请等
- 实现完整的 GORM 模型和常量定义
- 添加数据库迁移脚本和详细文档
- 集成 OpenSpec 工作流工具(opsx 命令和 skills)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 15:44:23 +08:00

227 lines
11 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.
# IoT Package Management
## Purpose
Manage IoT packages (data plans) for IoT cards and devices, including package definitions, real/virtual data coexistence, single-card packages, device-level packages with shared data pools, and agent package allocation.
This capability supports:
- Package entity definition with real and virtual data types
- Formal packages and addon packages (data top-ups)
- Single-card package purchases
- Device-level package purchases with shared data pool across all bound cards
- Agent package allocation with retail pricing
- Commission calculation (counted once for device-level packages regardless of card count)
## Requirements
## ADDED Requirements
### Requirement: 套餐实体定义
系统 SHALL 定义套餐(Package)实体,包含套餐的基本属性、定价、流量配置。
**核心概念**: 套餐只适用于 IoT 卡(ICCID),用户可以为单张 IoT 卡购买套餐,也可以为设备购买套餐(套餐分配到设备绑定的所有 IoT 卡,流量设备级共享)。
**实体字段**:
- `id`: 套餐 ID(主键,BIGINT)
- `package_code`: 套餐编码(VARCHAR(50),唯一)
- `package_name`: 套餐名称(VARCHAR(255))
- `series_id`: 套餐系列 ID(BIGINT,关联 package_series 表,用于组织套餐分组和配置一次性分佣)
- `package_type`: 套餐类型(VARCHAR(20),"formal"-正式套餐 | "addon"-加油包)
- `duration_months`: 套餐时长(INT,月数,1-月套餐 12-年套餐,加油包为 0)
- `real_data_mb`: 真流量额度(BIGINT,MB 为单位,可选)
- `virtual_data_mb`: 虚流量额度(BIGINT,MB 为单位,用于停机判断,可选)
- `data_amount_mb`: 总流量额度(BIGINT,MB 为单位,real_data_mb + virtual_data_mb)
- `price`: 套餐价格(DECIMAL(10,2),元)
- `status`: 套餐状态(INT,1-上架 2-下架)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
**套餐类型说明**:
- **正式套餐(formal)**: 每张 IoT 卡只能有一个有效的正式套餐,购买新的正式套餐会替换旧的
- **加油包(addon)**: 每张 IoT 卡可以购买多个加油包,与正式套餐共存
#### Scenario: 创建月套餐
- **WHEN** 平台创建月套餐,套餐编码为 "PKG-M-001",套餐名称为 "月套餐 10GB",套餐系列 ID 为 1,类型为正式套餐,时长为 1 个月,真流量为 10240 MB,虚流量为 0,价格为 30.00 元
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-M-001",`series_id` 为 1,`package_type` 为 "formal",`duration_months` 为 1,`real_data_mb` 为 10240,`virtual_data_mb` 为 0,`data_amount_mb` 为 10240,`price` 为 30.00
#### Scenario: 创建年套餐
- **WHEN** 平台创建年套餐,套餐编码为 "PKG-Y-001",套餐名称为 "年套餐 120GB",套餐系列 ID 为 1,类型为正式套餐,时长为 12 个月,真流量为 122880 MB,虚流量为 0,价格为 300.00 元
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-Y-001",`series_id` 为 1,`package_type` 为 "formal",`duration_months` 为 12,`real_data_mb` 为 122880,`virtual_data_mb` 为 0,`data_amount_mb` 为 122880,`price` 为 300.00
#### Scenario: 创建流量加油包
- **WHEN** 平台创建加油包,套餐编码为 "PKG-ADD-001",套餐名称为 "流量包 5GB",套餐系列 ID 为 2,类型为加油包,时长为 0,真流量为 5120 MB,虚流量为 0,价格为 10.00 元
- **THEN** 系统创建套餐记录,`package_code` 为 "PKG-ADD-001",`series_id` 为 2,`package_type` 为 "addon",`duration_months` 为 0,`real_data_mb` 为 5120,`virtual_data_mb` 为 0,`data_amount_mb` 为 5120,`price` 为 10.00
---
### Requirement: 套餐流量类型和真虚流量共存
系统 SHALL 支持真流量和虚流量两种流量类型,两者可以共存于同一套餐中。
**流量类型定义**:
- **真流量(real_data_mb)**: 实际可用的流量,可在运营商网络中使用
- **虚流量(virtual_data_mb)**: 虚拟流量,用于停机判断(虚流量用完后停机,即使真流量还有剩余)
- **总流量(data_amount_mb)**: 真流量 + 虚流量的总和
**重要规则**:
- 真流量和虚流量可以同时存在于一个套餐中
- 停机判断基于虚流量(虚流量用完后停机)
- 套餐可以只有真流量、只有虚流量、或两者都有
#### Scenario: 创建真虚流量共存的套餐
- **WHEN** 平台创建套餐,真流量为 8000 MB,虚流量为 2000 MB
- **THEN** 系统创建套餐记录,`real_data_mb` 为 8000,`virtual_data_mb` 为 2000,`data_amount_mb` 为 10000
#### Scenario: 创建纯真流量套餐
- **WHEN** 平台创建套餐,真流量为 10240 MB,虚流量为 0
- **THEN** 系统创建套餐记录,`real_data_mb` 为 10240,`virtual_data_mb` 为 0,`data_amount_mb` 为 10240
#### Scenario: 创建纯虚流量套餐
- **WHEN** 平台创建套餐,真流量为 0,虚流量为 10240 MB
- **THEN** 系统创建套餐记录,`real_data_mb` 为 0,`virtual_data_mb` 为 10240,`data_amount_mb` 为 10240
#### Scenario: 虚流量用完停机
- **WHEN** 套餐的虚流量为 2000 MB,用户已使用 2000 MB 虚流量,但真流量还剩余 5000 MB
- **THEN** 系统判断虚流量已用完,触发停机操作,即使真流量还有剩余
---
### Requirement: 单卡套餐购买
系统 SHALL 支持用户为单张 IoT 卡购买套餐。
**购买规则**:
- 每张 IoT 卡只能有一个有效的正式套餐
- 购买新的正式套餐会替换旧的正式套餐
- 可以同时购买多个加油包
- 套餐购买后创建套餐订单记录
#### Scenario: 为 IoT 卡购买正式套餐
- **WHEN** 用户为 IoT 卡(ICCID 为 "8986...")购买月套餐(套餐 ID 为 1001),价格为 30.00 元
- **THEN** 系统创建套餐订单,`order_type` 为 1(套餐订单),`iot_card_id` 为 IoT 卡 ID,`package_id` 为 1001,`amount` 为 30.00
#### Scenario: 为 IoT 卡购买加油包
- **WHEN** 用户为 IoT 卡购买流量加油包(套餐 ID 为 2001),价格为 10.00 元
- **THEN** 系统创建套餐订单,IoT 卡的正式套餐保持不变,加油包作为额外套餐生效
#### Scenario: 购买新正式套餐替换旧套餐
- **WHEN** 用户为 IoT 卡购买新的月套餐,该 IoT 卡已有月套餐
- **THEN** 系统创建新订单,旧的正式套餐失效,新套餐生效
---
### Requirement: 设备级套餐购买和流量共享
系统 SHALL 支持用户为设备购买套餐,套餐分配到设备绑定的所有 IoT 卡,流量设备级共享。
**设备套餐业务规则**:
- 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张)
- 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡)
- 分佣**只计算一次**(不按卡数倍增)
- 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡
- 设备购买的套餐不受单卡套餐限制(设备套餐和单卡套餐独立管理)
**流量共享机制**:
- 设备绑定的所有 IoT 卡共享套餐流量池
- 任意一张 IoT 卡使用流量都会从共享池扣除
- 流量池耗尽后,所有绑定的 IoT 卡都无法使用
**订单记录**:
- 订单表 `device_id` 字段记录设备 ID(设备级套餐订单)
- 订单表 `iot_card_id` 字段为 NULL(不关联具体 IoT 卡)
- 通过 `device_sim_bindings` 表查询设备绑定的所有 IoT 卡
#### Scenario: 为设备购买套餐
- **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买年套餐,价格为 399.00 元,流量为 3000G/月
- **THEN** 系统创建套餐订单,`order_type` 为 1(套餐订单),`device_id` 为 1001,`iot_card_id` 为 NULL,`amount` 为 399.00,套餐分配到 3 张绑定的 IoT 卡
#### Scenario: 设备流量共享
- **WHEN** 设备(绑定 3 张 IoT 卡)购买套餐 3000G/月,其中一张 IoT 卡使用 1000G 流量
- **THEN** 流量池剩余 2000G,其他两张 IoT 卡可以使用剩余的 2000G
#### Scenario: 设备套餐分佣只计算一次
- **WHEN** 设备(绑定 3 张 IoT 卡)购买套餐,长期佣金为 100.00 元
- **THEN** 系统创建一条分佣记录,金额为 100.00 元(不是 3 × 100.00 元)
---
### Requirement: 套餐分配给代理
系统 SHALL 支持将套餐分配给代理商,代理可以在平台设置的成本价基础上加价销售。
**分配规则**:
- 平台为套餐设置成本价(分配给代理的价格)
- 代理可以在成本价基础上加价,但不能超过成本价的 2 倍
- 分配记录存储在 `agent_package_allocations`
**agent_package_allocations 表**:
- `id`: 分配记录 ID(主键,BIGINT)
- `agent_id`: 代理用户 ID(BIGINT)
- `package_id`: 套餐 ID(BIGINT)
- `cost_price`: 成本价(DECIMAL(10,2),平台给代理的价格)
- `retail_price`: 零售价(DECIMAL(10,2),代理设置的终端销售价格)
- `status`: 分配状态(INT,1-有效 2-无效)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 平台分配套餐给代理
- **WHEN** 平台将套餐(ID 为 1001)分配给代理(用户 ID 为 123),成本价为 25.00 元
- **THEN** 系统创建分配记录,`agent_id` 为 123,`package_id` 为 1001,`cost_price` 为 25.00,状态为 1(有效)
#### Scenario: 代理设置零售价
- **WHEN** 代理(用户 ID 为 123)为套餐(ID 为 1001)设置零售价为 30.00 元
- **THEN** 系统更新分配记录,`retail_price` 为 30.00
#### Scenario: 代理零售价超过 2 倍成本价
- **WHEN** 代理设置零售价为 60.00 元,成本价为 25.00 元(2 倍为 50.00 元)
- **THEN** 系统拒绝设置,返回错误信息"零售价不能超过成本价的 2 倍"
---
### Requirement: 套餐数据校验
系统 SHALL 对套餐数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 套餐编码(package_code):必填,长度 1-50 字符,唯一
- 套餐名称(package_name):必填,长度 1-255 字符
- 套餐系列 ID(series_id):必填,≥ 1,必须是有效的套餐系列 ID
- 套餐类型(package_type):必填,枚举值 "formal" | "addon"
- 套餐时长(duration_months):必填,≥ 0(正式套餐 ≥ 1,加油包为 0)
- 真流量额度(real_data_mb):可选,≥ 0
- 虚流量额度(virtual_data_mb):可选,≥ 0
- 总流量额度(data_amount_mb):必填,≥ 0,必须等于 real_data_mb + virtual_data_mb
- 套餐价格(price):必填,≥ 0,最多 2 位小数
- 状态(status):必填,枚举值 1(上架) | 2(下架)
#### Scenario: 创建套餐时价格为负数
- **WHEN** 平台创建套餐,价格为 -10.00
- **THEN** 系统拒绝创建,返回错误信息"套餐价格必须 ≥ 0"
#### Scenario: 创建套餐时套餐编码重复
- **WHEN** 平台创建套餐,套餐编码为已存在的 "PKG-M-001"
- **THEN** 系统拒绝创建,返回错误信息"套餐编码已存在"
#### Scenario: 创建正式套餐时时长为 0
- **WHEN** 平台创建正式套餐,套餐类型为 "formal",时长为 0
- **THEN** 系统拒绝创建,返回错误信息"正式套餐时长必须 ≥ 1"