实现 IoT SIM 管理模块数据模型和数据库结构

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 15:44:23 +08:00
parent 743db126f7
commit 034f00e2e7
48 changed files with 11675 additions and 1 deletions

View File

@@ -0,0 +1,211 @@
## 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"