实现 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,2 @@
schema: spec-driven
created: 2026-01-10

View File

@@ -0,0 +1,964 @@
## Context
### 背景
junhong_cmp_fiber 项目需要构建 IoT 卡管理系统,支持三大核心业务:
**核心概念澄清**:
- **IoT 卡** = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)
- **普通卡**: 需要实名认证才能激活使用,遵循运营商实名制要求
- **行业卡**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
- **设备**: 用户的物联网设备(如 GPS 追踪器、智能传感器),可绑定 1-4 张 IoT 卡,主要用于批量管理和设备操作(重启、修改密码等),不在卡管系统中销售
- **号卡**: 完全独立的业务线,从上游平台下单,不走我们平台激活和充值,只接收订单状态更新
**三大核心业务**:
1. **IoT 卡(IotCard)**: 平台自营销售和代理分销,通过购买套餐产生订单,使用 ICCID 作为唯一标识
2. **设备(Device)**: 用户设备管理,可绑定 1-4 张 IoT 卡,支持设备级套餐购买(流量共享),不在卡管系统中销售
3. **号卡(NumberCard)**: 运营商订单回传,使用虚拟商品编码映射,支持代理分销和分佣
### 当前状态
- 已有用户体系:平台用户、代理用户、企业用户、个人用户(`user_organizations`, `users` 等表)
- 已有认证和权限系统(`auth`, `role-permission`, `data-permission`)
- 外部依赖:Gateway 项目提供 IoT 卡状态、实名、流量、停复机等 HTTP 接口
### 约束
- 本阶段只设计数据模型层(域实体、ERD、表结构、Schema、GORM Models)
- 不涉及 API/Handler/Service 层的实现
- 不涉及计费系统、供应管理、事件系统的实现
- 遵循项目规范:无外键约束、无 ORM 关联、手动维护关联关系
### 利益相关方
- 平台用户:自营销售 IoT 卡、管理设备
- 代理商:多级树形结构,分销 IoT 卡和分佣
- 企业客户/个人客户:购买 IoT 卡套餐、管理设备、购买号卡
- 运营商:号卡订单回传和套餐管理
- 运营人员:通过设备维度批量管理投诉和代理要求,查看绑定的所有 IoT 卡
---
## Goals / Non-Goals
### Goals (本阶段目标)
1. **设计完整的数据模型**:
- 定义核心实体:IoT 卡、设备、号卡、套餐、订单、代理分佣
- 绘制 ERD(实体关系图)
- 设计数据库表结构和 Schema
- 实现 GORM 模型定义
2. **支持核心业务流程**:
- 平台自营和代理分销模式(仅 IoT 卡)
- 套餐购买订单流程(单卡套餐、设备级套餐)
- 号卡运营商订单回传和虚拟商品编码映射
- 多级代理分佣计算(组合分佣 OR 条件)
- 设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡)
- 设备级套餐流量共享机制
3. **遵循项目规范**:
- 无数据库外键约束
- 无 GORM ORM 关联标签(`foreignKey`, `references`, `hasMany`, `belongsTo` 等)
- 所有字段显式指定 `column:` 标签
- 字段类型和长度明确定义
- 所有字段添加中文注释
4. **预留扩展能力**:
- 支持未来集成 Gateway 项目(IoT 卡状态查询、停复机操作等)
- 支持未来的计费和供应管理集成
### Non-Goals (明确排除)
- ❌ API 层设计(Handlers、路由、中间件)
- ❌ 业务逻辑层设计(Services、业务规则实现)
- ❌ 计费系统实现(Billing Engine)
- ❌ 供应管理集成(Provisioning)
- ❌ 事件系统集成(Events、消息队列)
- ❌ 单元测试和集成测试
- ❌ API 文档生成
- ❌ Gateway 项目集成的具体实现(只设计数据模型字段预留)
---
## Decisions
### 决策 1: 无外键约束的数据模型设计
**选择**: 所有表之间不使用数据库外键约束,通过存储关联 ID 字段手动维护关系。
**理由**:
- 遵循项目既定规范(参考 `CLAUDE.md` 数据库设计原则)
- 提高灵活性:业务逻辑完全在代码中控制
- 提升性能:无数据库层面的引用完整性检查开销
- 分布式友好:在微服务和分布式数据库场景下更易扩展
- 简化迁移:数据库 schema 更简单,迁移更容易
**替代方案**:
- ❌ 使用外键约束:会引入数据库层面的复杂性,限制灵活性,不符合项目规范
**实施细节**:
- 所有关联关系通过 `{entity}_id` 字段存储(如 `user_id`, `agent_id`, `device_id`)
- GORM 模型不使用 `foreignKey`, `references`, `hasMany`, `belongsTo` 等标签
- 关联数据查询在 Service 层显式执行
---
### 决策 2: 平台自营和代理分销的统一建模
**选择**: 使用 `owner_type``owner_id` 字段统一建模平台自营和代理分销(仅 IoT 卡)。
**理由**:
- IoT 卡既可以平台自营销售,也可以分销给代理
- 设备不在卡管系统中销售,主要用于用户设备管理和运营人员管理投诉
- 使用多态关联字段避免为平台和代理创建两套库存系统
- 简化查询逻辑:通过 `owner_type` 区分所有者类型
**字段设计**:
```
owner_type: VARCHAR(20) -- 值: "platform"-平台 | "agent"-代理 | "user"-用户 | "device"-设备
owner_id: BIGINT -- 平台(0)、代理用户 ID、用户 ID 或设备 ID
```
**替代方案**:
- ❌ 分别设计 `platform_inventory``agent_inventory` 表:重复代码,增加维护成本
- ❌ 只用 `agent_id` 并用 `NULL` 表示平台:语义不清晰,查询复杂
---
### 决策 3: 号卡虚拟商品编码的设计
**选择**: 在 `number_cards` 表中增加 `virtual_product_code` 字段,用于映射运营商回传订单。
**理由**:
- 号卡本身不是系统内真实的库存商品,而是运营商侧的订单
- 需要一个"假的商品编码"来对应上游回调订单的商品标识
- 虚拟编码作为号卡和运营商订单的桥梁
**字段设计**:
```
virtual_product_code: VARCHAR(100) UNIQUE -- 虚拟商品编码,用于对应运营商订单
carrier_order_id: VARCHAR(255) -- 运营商订单 ID
carrier_product_id: VARCHAR(100) -- 运营商商品 ID
```
**替代方案**:
- ❌ 直接使用运营商商品 ID:缺乏系统内部的统一标识
- ❌ 创建独立的商品表:号卡不是真实库存,不应与网卡/设备商品化混淆
---
### 决策 4: 设备与 IoT 卡的多对多绑定关系
**选择**: 使用中间表 `device_sim_bindings` 管理设备与 IoT 卡的绑定关系。
**理由**:
- 一个设备可以绑定 1-4 张 IoT 卡(多对多关系)
- 中间表可以记录绑定时间、绑定状态、插槽位置等元数据
- 支持历史绑定记录查询
- 支持设备级套餐购买(套餐分配到所有绑定的 IoT 卡,流量共享)
**表设计**:
```sql
CREATE TABLE device_sim_bindings (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT NOT NULL, -- 设备 ID
iot_card_id BIGINT NOT NULL, -- IoT 卡 ID
slot_position INT, -- 插槽位置 (1, 2, 3, 4)
bind_status INT DEFAULT 1, -- 绑定状态 1-已绑定 2-已解绑
bind_time TIMESTAMP, -- 绑定时间
unbind_time TIMESTAMP, -- 解绑时间
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**替代方案**:
- ❌ 在设备表存储 `iot_card_ids` JSON 字段:难以查询和维护,不支持元数据
- ❌ 在 IoT 卡表存储 `device_id`:只能支持一对一,不支持多卡绑定
---
### 决策 5: 代理树形结构的设计
**选择**: 在 `agent_hierarchies` 表中使用 `agent_id` + `parent_agent_id` 表示树形关系。
**理由**:
- 每个代理只有一个上级(单亲树)
- 使用递归查询(CTE)可以获取整个代理链
- 支持计算多级分佣
**表设计**:
```sql
CREATE TABLE agent_hierarchies (
id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL UNIQUE, -- 代理用户 ID
parent_agent_id BIGINT, -- 上级代理用户 ID (NULL 表示顶级代理)
level INT NOT NULL, -- 代理层级 (1, 2, 3...)
path VARCHAR(500), -- 代理路径 (如: "1/5/12")
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**替代方案**:
- ❌ 使用闭包表(Closure Table):过度设计,查询性能提升不明显
- ❌ 使用嵌套集合(Nested Set):插入和移动节点复杂,不适合频繁变更
---
### 决策 6: 订单类型的统一建模
**选择**: 使用 `order_type` 字段区分两种订单类型,使用独立字段关联订单来源。
**订单类型**:
1. **套餐订单** (`order_type = 1`): 用户为 IoT 卡或设备购买套餐
- **单卡套餐订单**: `iot_card_id` 有值,`device_id` 为 NULL
- **设备级套餐订单**: `device_id` 有值,`iot_card_id` 为 NULL(套餐分配到所有绑定的 IoT 卡,流量共享)
2. **号卡订单** (`order_type = 2`): 运营商回传订单,`number_card_id` 有值
**字段设计**:
```
order_type: INT -- 值: 1-套餐订单 2-号卡订单
iot_card_id: BIGINT -- IoT 卡 ID(单卡套餐订单时有值)
device_id: BIGINT -- 设备 ID(设备级套餐订单时有值)
number_card_id: BIGINT -- 号卡 ID(号卡订单时有值)
package_id: BIGINT -- 套餐 ID(套餐订单时有值)
```
**理由**:
- 简化订单类型,只保留实际需要的两种订单类型
- 移除 SIM 卡销售订单(IoT 卡不单独销售,只通过套餐订单管理)
- 通过独立字段明确关联不同业务实体,比多态字段更清晰
- 支持设备级套餐订单,流量共享机制
**替代方案**:
- ❌ 创建 `package_orders`, `number_card_orders` 两张表:代码重复,维护成本高
- ❌ 使用 `source_type` + `source_id` 多态字段:不够清晰,查询复杂
---
### 决策 7: IoT 卡状态字段预留 Gateway 集成
**选择**: 在 `iot_cards` 表中增加状态相关字段,但不在本阶段实现 Gateway 集成。
**字段设计**:
```
iccid: VARCHAR(50) UNIQUE -- IoT 卡 ICCID(唯一标识)
activation_status: INT -- 激活状态 (0-未激活 1-已激活)
real_name_status: INT -- 实名状态 (0-未实名 1-已实名)
network_status: INT -- 网络状态 (0-停机 1-开机)
data_usage_mb: BIGINT DEFAULT 0 -- 累计流量使用(MB)
last_sync_time: TIMESTAMP -- 最后一次与 Gateway 同步时间
```
**理由**:
- 本阶段只设计数据模型,不实现具体的 Gateway 集成逻辑
- 预留字段便于后续 Service 层调用 Gateway HTTP 接口并更新这些字段
- 这些字段的数据来源是 Gateway 项目,不由本系统直接管理
- IoT 卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法,统一使用 IoT 卡命名)
**替代方案**:
- ❌ 不预留字段:后续集成需要修改表结构,涉及数据迁移
- ❌ 在独立的 `iot_card_status` 表:过度规范化,增加查询复杂度
---
### 决策 8: 字段命名和类型规范
**选择**: 严格遵循项目规范,所有字段显式指定 `column:` 标签,类型和长度明确定义。
**命名规范**:
- 数据库字段名:snake_case (如 `user_id`, `created_at`)
- Go 结构体字段名:PascalCase (如 `UserID`, `CreatedAt`)
- 必须显式指定 `gorm:"column:字段名"` 标签
**类型规范**:
- ID 字段:BIGINT (对应 Go `uint``int64`)
- 短文本:VARCHAR(50-255)
- 长文本:TEXT
- 货币金额:DECIMAL(18,2) 或 BIGINT(分为单位)
- 时间:TIMESTAMP (对应 Go `time.Time`)
- 枚举:INT 或 VARCHAR,配合常量定义
**示例**:
```go
type IotCard struct {
ID uint `gorm:"column:id;primaryKey;comment:IoT 卡 ID" json:"id"`
ICCID string `gorm:"column:iccid;type:varchar(50);uniqueIndex;not null;comment:ICCID" json:"iccid"`
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-在库 2-已分销 3-已激活" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
}
```
---
### 决策 9: 行业卡无需实名认证的设计
**选择**: 在 IoT 卡实体中增加 `card_category` 字段(枚举值:"normal"-普通卡 | "industry"-行业卡),行业卡可以在实名状态为 0(未实名)的情况下激活和使用。
**业务规则**:
- **普通卡(normal)**: 必须完成实名认证(`real_name_status` 为 1)才能激活使用,遵循运营商实名制要求
- **行业卡(industry)**: 不需要实名认证,可以在 `real_name_status` 为 0 的情况下激活使用,适用于企业/行业客户批量采购场景
**分佣解冻规则调整**:
- **一次性分佣**: 普通卡需要实名认证后才能解冻;行业卡无需实名认证,只需满足激活和充值条件
- **长期分佣**: 普通卡需要实名认证后才能开始长期分佣;行业卡无需实名认证,满足其他条件即可
- **组合分佣**: 行业卡的时间点条件从激活时开始计算(不是实名时)
**轮询控制**:
- 行业卡的实名状态检查轮询应该被禁用或设置为低优先级
- 行业卡的流量检查和套餐检查与普通卡相同
**数据模型变更**:
```go
type IotCard struct {
// ... 其他字段 ...
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;comment:实名状态 0-未实名 1-已实名 (行业卡可以保持 0)" json:"real_name_status"`
// ... 其他字段 ...
}
```
**理由**:
- 符合企业/行业客户批量采购场景的实际需求
- 简化行业卡的激活流程,提高用户体验
- 分佣解冻逻辑需要区分普通卡和行业卡,避免行业卡因未实名而无法解冻
**替代方案**:
- ❌ 为行业卡自动设置实名状态为 1:不真实,会导致数据统计错误
- ❌ 创建独立的行业卡实体:增加系统复杂度,不利于统一管理
---
## Risks / Trade-offs
### 风险 1: 无外键约束导致数据一致性问题
**风险**: 手动维护关联关系可能导致孤儿记录(如删除代理后,其分销的 IoT 卡 `owner_id` 仍然指向已删除的代理)。
**缓解措施**:
- 在 Service 层实现软删除(soft delete),不物理删除关键实体
- 在删除操作前检查关联记录
- 定期运行数据一致性检查脚本
---
### 风险 2: 设备与 IoT 卡的多对多绑定复杂度
**风险**: 中间表 `device_sim_bindings` 的状态管理复杂,可能出现一个 IoT 卡被多个设备绑定的冲突。
**缓解措施**:
- 在 Service 层实现业务规则:一个 IoT 卡同一时间只能绑定一个设备
- 在绑定前查询 IoT 卡的当前绑定状态
- 使用数据库唯一索引:`CREATE UNIQUE INDEX idx_iot_card_active_binding ON device_sim_bindings(iot_card_id) WHERE bind_status = 1`
---
### 风险 3: 号卡虚拟商品编码的唯一性冲突
**风险**: 多个号卡可能误用相同的虚拟商品编码,导致运营商订单映射错误。
**缓解措施**:
-`virtual_product_code` 字段上创建唯一索引
- 在创建号卡时自动生成虚拟商品编码(使用 UUID 或业务规则生成)
- 在 Service 层校验虚拟商品编码的唯一性
---
### 风险 4: 多级代理分佣计算性能
**风险**: 递归查询代理树获取整个分佣链可能影响性能(特别是代理层级深时)。
**缓解措施**:
-`agent_hierarchies` 表中增加 `path` 字段存储代理路径(如 `"1/5/12"`),避免递归查询
- 在 Redis 中缓存代理树结构
- 使用异步任务(Asynq)计算分佣,不阻塞订单创建
---
### 风险 5: Gateway 集成依赖的可用性
**风险**: IoT 卡状态、流量、停复机操作依赖 Gateway 项目 HTTP 接口,如果 Gateway 不可用会影响功能。
**缓解措施**:
- 在数据库中缓存 IoT 卡状态字段,Gateway 不可用时返回缓存数据
- 设置合理的 HTTP 超时和重试机制
- 使用 Asynq 异步任务定期同步 IoT 卡状态,降低实时依赖
---
### Trade-off 1: 单表订单 vs 多表订单
**权衡**: 选择单表存储两种订单类型,使用 `order_type` 区分。
**优点**:
- 统一的订单查询和状态管理
- 代码复用度高
**缺点**:
- 表字段较多,某些字段只对特定订单类型有意义(如 `carrier_order_id` 只对号卡订单有意义)
- 单表数据量大,可能影响查询性能
**选择理由**: 在当前业务规模下,单表方案的代码简洁性优于多表方案的性能优势。如果未来订单量巨大,可以考虑分表或分库。
---
### Trade-off 2: 代理路径字段 vs 纯递归查询
**权衡**: 在 `agent_hierarchies` 表中增加 `path` 字段存储代理路径。
**优点**:
- 避免递归查询,提升查询性能
- 快速获取整个代理链
**缺点**:
- 需要在代理关系变更时维护 `path` 字段
- 增加存储空间
**选择理由**: 分佣计算是高频操作,牺牲少量存储空间换取查询性能提升是值得的。
---
## Migration Plan
### 部署步骤
1. **生成数据库迁移脚本**:
- 使用 `golang-migrate` 创建迁移脚本
- 迁移脚本位置:`migrations/` 目录
- 命名格式:`{timestamp}_create_iot_sim_tables.up.sql``.down.sql`
2. **测试环境验证**:
- 在测试数据库执行 `up` 迁移
- 验证所有表和索引创建成功
- 插入测试数据验证约束和索引
3. **生产环境部署**:
- 在生产数据库执行 `up` 迁移
- 验证表结构和索引
- 监控数据库性能
4. **GORM 模型代码部署**:
- 部署包含新 GORM 模型的代码版本
- 验证 GORM AutoMigrate 不会修改已有表结构(禁用 AutoMigrate 或仅用于开发环境)
### 回滚策略
1. **代码回滚**:
- 如果 GORM 模型有 Bug,回滚到上一个代码版本
2. **数据库回滚**:
- 执行 `.down.sql` 迁移脚本删除新创建的表
- 如果已有数据,需要先备份数据再回滚
### 数据迁移(如果需要)
- 本次为新功能,不涉及旧数据迁移
- 如果需要从旧系统导入数据,使用 ETL 脚本批量导入
---
## Open Questions (已解决)
### ✅ 问题 1: 套餐定价和计费规则 (已解决)
**结论**:
- 套餐基本为月套餐,年套餐通过设置月数实现(如 12 个月)
- 流量单位为 MB
- **流量分为真流量和虚流量两种类型,两者共存**
- **停机判断基于虚流量**(虚流量用完后停机,即使真流量还有剩余)
- 无复杂计费规则,只有固定的套餐价格
**表设计影响**:
```
duration_months: INT -- 套餐时长(月数) 1-月套餐 12-年套餐
data_type: VARCHAR(20) -- 流量类型 "real"(真流量) | "virtual"(虚流量)
data_amount_mb: BIGINT -- 流量额度(MB)
real_data_mb: BIGINT -- 真流量额度(MB,可选)
virtual_data_mb: BIGINT -- 虚流量额度(MB,用于停机判断)
price: DECIMAL(10,2) -- 套餐价格(元)
```
**停机规则**:
- 虚流量用完后自动停机
- 真流量和虚流量独立计算,共存在套餐中
- 前端展示需要同时显示真流量和虚流量余额
---
### ✅ 问题 2: 代理分佣配置方式 (已解决)
**结论**: 分佣体系非常复杂,包含多种类型和触发条件:
**分佣类型**:
1. **一次性分佣**:
- 作用于套餐系列
- 激活(实名) + 达到首次充值金额后产生
- **纯直接给钱**(固定金额,不计算差价)
- 冻结 N 天后解冻
- **一次性佣金订单必须通过钱包付款**
2. **长期分佣**:
- 作用于具体套餐
- 每个计费周期产生
- **佣金 = 实际售价 - 平台成本价**(代理看到的成本价是售价扣掉佣金)
- **号卡**:需要激活 + 充值 + 在网状态 + 三无校验(通过 Excel 导入解冻)
- **物联网卡(流量卡)**:只要用户买了就按佣金返,无需在网状态和三无校验
3. **组合分佣**:
- **物联网卡(流量卡/IoT 卡)**:
- 先产生一次性佣金
- 达到以下**任一条件**(OR 关系)后开始长期分佣:
1. 某个时间点之后(例如:实名后 3 个月)
2. **OR** 该 IoT 卡的套餐使用周期数达到阈值(例如:10 个套餐周期)
- **注意**: 套餐周期阈值是针对单张 IoT 卡的,不是设备级别
- **号卡**:
- 连续在网多少个月后开始长期分佣
**阶梯分佣**:
- **号卡**: 只有激活量作为阶梯条件
- **物联网卡(流量卡)**: 激活量 + 提货量作为阶梯条件
- 达到阶梯条件后变更分佣值
**关键业务规则**:
- 代理销售价格不能超过平台成本价的 2 倍
- **长期分佣**: 佣金 = 实际售价 - 平台成本价(阴阳菜单模式)
- **一次性佣金**: 纯直接给钱,不计算差价
- 一次性佣金订单必须通过钱包付款
**表设计影响**:
- 新增 `commission_templates` 表:分佣模板(常用分佣方案)
- 新增 `commission_rules` 表:代理分佣规则配置(需区分号卡和 IoT 卡)
- 新增 `commission_records` 表:分佣记录(冻结/解冻状态)
- 新增 `commission_ladder` 表:阶梯分佣配置(号卡只支持激活量,IoT 卡支持激活量+提货量)
- 新增 `commission_approvals` 表:分佣解冻审批
- 新增 `commission_combined_conditions` 表:组合分佣条件配置(时间点、套餐周期数、连续在网月数),**OR 关系解冻**
---
### ✅ 问题 3: 号卡运营商订单回传数据格式 (已解决)
**结论**:
- Gateway 项目统一转换各上游订单为 JSON 格式后回传
- 号卡资金流不经过平台,直接支付给运营商
- 平台接收运营商周期性结算的佣金总额,再分配给代理
**表设计影响**:
```
carrier_order_id: VARCHAR(255) -- 运营商订单 ID
carrier_order_data: JSONB -- 运营商订单原始数据(JSON)
settlement_status: INT -- 结算状态 1-待结算 2-已结算
settlement_amount: DECIMAL(18,2) -- 运营商结算佣金金额
```
---
### ✅ 问题 4: IoT 卡绑定设备的插槽数量限制 (已解决)
**结论**: 一个设备最多插 4 张卡
**表设计影响**:
```
max_sim_slots: INT DEFAULT 4 -- 设备最大插槽数量(默认 4)
```
**业务规则**: 在 Service 层校验设备当前绑定的 IoT 卡数量不超过 `max_sim_slots`
---
### ✅ 问题 5: Gateway 集成的认证和授权 (已解决)
**结论**: Gateway 使用统一的加密传输协议
**请求格式**:
```json
{
"appId": "your_app_id",
"data": "AES加密后的Base64字符串",
"sign": "MD5签名(大写)",
"timestamp": 1704067200
}
```
**加密方案**:
- 数据加密:AES-128-ECB + PKCS5Padding,密钥为 `MD5(appSecret)` 的原始字节数组
- 签名算法:MD5(appId + data + timestamp + appSecret),转大写
- 时间戳:Unix 秒级时间戳,允许 ±5 分钟误差
**配置文件影响**:
```yaml
gateway:
base_url: "https://gateway.example.com"
app_id: "your_app_id"
app_secret: "your_app_secret"
timeout: 30s
```
**实现范围**: 本阶段只设计数据模型,不实现 Gateway 集成的具体 HTTP 客户端代码
---
### 决策 9: 佣金提现和财务管理
**选择**: 设计独立的佣金提现申请流程和财务账户管理。
**理由**:
- 代理需要将冻结/已发放的佣金提现到银行卡或支付宝
- 需要审批流程控制提现风险
- 需要记录提现历史和手续费
**表设计**:
```sql
-- 佣金提现申请表
CREATE TABLE commission_withdrawal_requests (
id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL, -- 代理用户 ID
amount DECIMAL(18,2) NOT NULL, -- 提现金额
fee DECIMAL(18,2) DEFAULT 0, -- 手续费
actual_amount DECIMAL(18,2), -- 实际到账金额
withdrawal_method VARCHAR(20), -- 提现方式 "alipay" | "wechat" | "bank"
account_info JSONB, -- 收款账户信息(姓名、账号等)
status INT DEFAULT 1, -- 状态 1-待审核 2-已通过 3-已拒绝 4-已到账
approved_by BIGINT, -- 审批人用户 ID
approved_at TIMESTAMP, -- 审批时间
paid_at TIMESTAMP, -- 到账时间
reject_reason TEXT, -- 拒绝原因
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 佣金提现设置表
CREATE TABLE commission_withdrawal_settings (
id BIGSERIAL PRIMARY KEY,
min_withdrawal_amount DECIMAL(10,2), -- 最低提现金额
fee_rate DECIMAL(5,4), -- 手续费率(如 0.01 表示 1%)
arrival_days INT, -- 到账天数
is_active BOOLEAN DEFAULT TRUE, -- 是否生效(最新一条)
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**替代方案**:
- ❌ 不设计提现流程:代理无法取出佣金,体验差
---
### 决策 10: 商品分配和套餐系列管理
**选择**: 设计套餐系列作为套餐的分组,用于一次性分佣规则配置。
**理由**:
- 一次性分佣作用于套餐系列,而不是单个套餐
- 套餐系列可以包含多个套餐(如"月套餐系列"包含 10GB、20GB、30GB 等月套餐)
- 便于批量管理和分佣规则配置
**表设计**:
```sql
-- 套餐系列表
CREATE TABLE package_series (
id BIGSERIAL PRIMARY KEY,
series_name VARCHAR(255) NOT NULL, -- 系列名称
series_code VARCHAR(100) UNIQUE, -- 系列编码
description TEXT, -- 描述
status INT DEFAULT 1, -- 状态 1-启用 2-禁用
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 套餐表增加 series_id 字段
ALTER TABLE packages ADD COLUMN series_id BIGINT;
```
**说明**: 套餐只适用于 IoT 卡(ICCID),用户可以为单张 IoT 卡购买套餐,也可以为设备购买套餐(套餐分配到设备绑定的所有 IoT 卡,流量设备级共享)
**替代方案**:
- ❌ 不设计套餐系列:需要为每个套餐单独配置分佣规则,维护成本高
---
### 决策 11: 资产分配批量操作
**选择**: 设计批量资产分配接口,支持设备批量分配和 IoT 卡批量分配。
**理由**:
- 代理商提货时通常批量分配大量 IoT 卡或设备
- IoT 卡如果绑定了设备,分配时需要连同设备一起分配
- 批量操作提高效率
**业务规则**:
- **设备批量分配**: 只分配设备,不影响设备绑定的 IoT 卡所有权
- **IoT 卡批量分配**: 分配 IoT 卡,如果 IoT 卡有设备信息(`device_id`),则设备和 IoT 卡一起分配
- 批量分配时需要校验数量和权限
**表设计影响**:
- 复用现有的 `iot_cards``devices` 表的 `owner_type``owner_id` 字段
- 批量操作通过 Service 层事务处理
---
### 决策 12: 换卡申请管理
**选择**: 设计换卡申请表,记录客户的换卡请求和处理流程。
**理由**:
- 客户的 IoT 卡损坏或丢失时需要换卡
- 需要审批流程和旧卡/新卡 ICCID 映射
- 换卡后需要转移套餐和流量余额
**表设计**:
```sql
-- 换卡申请表
CREATE TABLE card_replacement_requests (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, -- 申请用户 ID
old_iccid VARCHAR(50) NOT NULL, -- 旧卡 ICCID
new_iccid VARCHAR(50), -- 新卡 ICCID(审批时填充)
reason TEXT, -- 换卡原因
status INT DEFAULT 1, -- 状态 1-待处理 2-已通过 3-已拒绝 4-已完成
approved_by BIGINT, -- 处理人用户 ID
approved_at TIMESTAMP, -- 处理时间
completed_at TIMESTAMP, -- 完成时间(新卡激活时间)
reject_reason TEXT, -- 拒绝原因
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**替代方案**:
- ❌ 不设计换卡流程:客户无法自助换卡,需要人工处理,效率低
---
### 决策 13: 开发能力管理
**选择**: 设计开发能力管理表,存储 API 对接参数(AppID、AppSecret、回调地址等)。
**理由**:
- 代理或平台需要通过 API 对接系统
- 需要管理 API 凭证和回调配置
- 支持多个应用(多套 AppID/AppSecret)
**表设计**:
```sql
-- 开发能力配置表
CREATE TABLE dev_capability_configs (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, -- 用户 ID(平台或代理)
app_name VARCHAR(255), -- 应用名称
app_id VARCHAR(100) UNIQUE, -- 应用 ID
app_secret VARCHAR(255), -- 应用密钥
callback_url VARCHAR(500), -- 回调地址
ip_whitelist TEXT, -- IP 白名单(多个 IP 用逗号分隔)
status INT DEFAULT 1, -- 状态 1-启用 2-禁用
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**替代方案**:
- ❌ 不设计开发能力管理:无法支持 API 对接,限制系统扩展性
---
### 决策 14: 收款商户设置
**选择**: 设计收款商户设置表,存储代理的收款账户信息。
**理由**:
- 代理提现时需要指定收款账户
- 支持多种收款方式(支付宝、微信、银行卡)
- 需要验证账户信息的真实性
**表设计**:
```sql
-- 收款商户设置表
CREATE TABLE payment_merchant_settings (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, -- 用户 ID
merchant_type VARCHAR(20), -- 商户类型 "alipay" | "wechat" | "bank"
account_name VARCHAR(255), -- 账户名称
account_number VARCHAR(255), -- 账号
bank_name VARCHAR(255), -- 银行名称(仅银行卡)
bank_branch VARCHAR(255), -- 开户行(仅银行卡)
is_verified BOOLEAN DEFAULT FALSE, -- 是否已验证
is_default BOOLEAN DEFAULT FALSE, -- 是否默认账户
status INT DEFAULT 1, -- 状态 1-启用 2-禁用
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**替代方案**:
- ❌ 每次提现时填写账户信息:重复录入,用户体验差
---
### 决策 15: IoT 卡轮询机制和流量管理
**选择**: 设计三个独立的轮询流程和相关的数据表支持卡流量监控和套餐流量管理。
**理由**:
- IoT 卡需要轮询实名状态,实名后降低轮询频率
- IoT 卡需要轮询流量使用情况,防止超额
- 设备级套餐需要汇总设备所有卡的流量,判断是否超过套餐额度
- 卡的流量轮询和套餐流量检查应该是两个独立的逻辑
- 支持细粒度的轮询配置(按运营商、按卡状态配置不同的轮询策略)
- 需要记录流量历史,便于查询和分析
**新增表设计**:
1. **运营商表 (carriers)**:
```sql
CREATE TABLE carriers (
id BIGSERIAL PRIMARY KEY,
carrier_code VARCHAR(50) UNIQUE NOT NULL, -- 运营商编码(CMCC/CUCC/CTCC)
carrier_name VARCHAR(100) NOT NULL, -- 运营商名称(中国移动/中国联通/中国电信)
description VARCHAR(500), -- 运营商描述
status INT NOT NULL DEFAULT 1, -- 状态 1-启用 2-禁用
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 初始数据
INSERT INTO carriers (carrier_code, carrier_name, status) VALUES
('CMCC', '中国移动', 1),
('CUCC', '中国联通', 1),
('CTCC', '中国电信', 1);
```
2. **套餐使用情况表 (package_usages)**:
```sql
CREATE TABLE package_usages (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL, -- 订单 ID
package_id BIGINT NOT NULL, -- 套餐 ID
usage_type VARCHAR(20) NOT NULL, -- 使用类型 single_card-单卡套餐 device-设备级套餐
iot_card_id BIGINT, -- IoT 卡 ID(单卡套餐时有值)
device_id BIGINT, -- 设备 ID(设备级套餐时有值)
data_limit_mb BIGINT NOT NULL, -- 流量限额(MB)
data_usage_mb BIGINT DEFAULT 0, -- 已使用流量(MB)
real_data_usage_mb BIGINT DEFAULT 0, -- 真流量使用(MB)
virtual_data_usage_mb BIGINT DEFAULT 0, -- 虚流量使用(MB)
activated_at TIMESTAMP NOT NULL, -- 套餐生效时间
expires_at TIMESTAMP NOT NULL, -- 套餐过期时间
status INT NOT NULL DEFAULT 1, -- 状态 1-生效中 2-已用完 3-已过期
last_package_check_at TIMESTAMP, -- 最后一次套餐流量检查时间
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_package_usages_order ON package_usages(order_id);
CREATE INDEX idx_package_usages_package ON package_usages(package_id);
CREATE INDEX idx_package_usages_iot_card ON package_usages(iot_card_id);
CREATE INDEX idx_package_usages_device ON package_usages(device_id);
CREATE INDEX idx_package_usages_check ON package_usages(status, expires_at, last_package_check_at);
```
3. **轮询配置表 (polling_configs)**:
```sql
CREATE TABLE polling_configs (
id BIGSERIAL PRIMARY KEY,
config_name VARCHAR(100) UNIQUE NOT NULL, -- 配置名称(如 未实名卡、实名卡)
description VARCHAR(500), -- 配置描述
card_condition VARCHAR(50), -- 卡状态条件(not_real_name | real_name | activated | suspended)
carrier_id BIGINT, -- 运营商 ID(NULL 表示所有运营商)
real_name_check_enabled BOOLEAN DEFAULT false, -- 是否启用实名检查
real_name_check_interval INT DEFAULT 60, -- 实名检查间隔(秒)
card_data_check_enabled BOOLEAN DEFAULT false, -- 是否启用卡流量检查
card_data_check_interval INT DEFAULT 60, -- 卡流量检查间隔(秒)
package_check_enabled BOOLEAN DEFAULT false, -- 是否启用套餐流量检查
package_check_interval INT DEFAULT 60, -- 套餐流量检查间隔(秒)
priority INT NOT NULL DEFAULT 100, -- 优先级(数字越小优先级越高)
status INT NOT NULL DEFAULT 1, -- 状态 1-启用 2-禁用
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_polling_configs_match ON polling_configs(status, card_condition, carrier_id, priority);
```
4. **流量使用记录表 (data_usage_records)**:
```sql
CREATE TABLE data_usage_records (
id BIGSERIAL PRIMARY KEY,
iot_card_id BIGINT NOT NULL, -- IoT 卡 ID
data_usage_mb BIGINT NOT NULL, -- 流量使用量(MB)
data_increase_mb BIGINT DEFAULT 0, -- 相比上次的增量(MB)
check_time TIMESTAMP NOT NULL, -- 检查时间
source VARCHAR(50) DEFAULT 'polling', -- 数据来源(polling-轮询 manual-手动 gateway-回调)
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_data_usage_records_card_time ON data_usage_records(iot_card_id, check_time DESC);
CREATE INDEX idx_data_usage_records_time ON data_usage_records(check_time);
```
**IoT 卡表调整**:
```sql
-- 添加以下字段
carrier_id BIGINT NOT NULL, -- 运营商 ID(关联 carriers 表)
enable_polling BOOLEAN DEFAULT true, -- 是否参与轮询(true-参与 false-不参与)
last_data_check_at TIMESTAMP, -- 最后一次流量检查时间
last_real_name_check_at TIMESTAMP, -- 最后一次实名检查时间
-- 添加索引
CREATE INDEX idx_iot_cards_carrier ON iot_cards(carrier_id);
CREATE INDEX idx_iot_cards_data_check ON iot_cards(enable_polling, activation_status, last_data_check_at);
CREATE INDEX idx_iot_cards_real_name_check ON iot_cards(enable_polling, real_name_status, last_real_name_check_at);
```
**轮询逻辑设计**:
1. **实名状态轮询**:
- 查询需要检查实名的卡(根据 polling_configs 匹配条件)
- 调用 Gateway API 获取卡的实名状态
- 更新 iot_cards.real_name_status 和 last_real_name_check_at
- 实名通过后降低轮询频率(通过配置表实现梯度策略)
2. **卡流量轮询**:
- 只轮询有生效套餐的卡(通过 package_usages 表 JOIN 查询)
- 卡必须 enable_polling = true
- 调用 Gateway API 获取卡的实时流量
- 更新 iot_cards.data_usage_mb 和 last_data_check_at
- 插入流量使用记录到 data_usage_records 表
3. **套餐流量检查**:
- 查询需要检查的套餐使用记录(status = 1 且未过期)
- 单卡套餐:直接读取关联卡的 data_usage_mb
- 设备级套餐:汇总设备所有卡的 data_usage_mb
- 更新 package_usages.data_usage_mb 和 last_package_check_at
- 判断是否超额(data_usage_mb >= data_limit_mb)
- 如果超额:调用 Gateway 停机(单卡停单卡,设备停所有卡)
**配置示例**:
```
┌─────┬──────────────┬───────────────┬─────────────┬──────────┬──────────┬────────────┬────────────┬──────────┬──────────┬────────┐
│ ID │ 配置名称 │ 卡状态 │ 运营商 ID │ 实名检查 │ 实名间隔 │ 卡流量检查 │ 卡流量间隔 │ 套餐检查 │ 套餐间隔 │ 优先级 │
├─────┼──────────────┼───────────────┼─────────────┼──────────┼──────────┼────────────┼────────────┼──────────┼──────────┼────────┤
│ 1 │ 未实名移动卡 │ not_real_name │ 1 (移动) │ ✅ │ 60秒 │ ❌ │ - │ ❌ │ - │ 10 │
├─────┼──────────────┼───────────────┼─────────────┼──────────┼──────────┼────────────┼────────────┼──────────┼──────────┼────────┤
│ 2 │ 未实名联通卡 │ not_real_name │ 2 (联通) │ ✅ │ 120秒 │ ❌ │ - │ ❌ │ - │ 11 │
├─────┼──────────────┼───────────────┼─────────────┼──────────┼──────────┼────────────┼────────────┼──────────┼──────────┼────────┤
│ 3 │ 实名卡-通用 │ real_name │ NULL (所有) │ ✅ │ 3600秒 │ ✅ │ 60秒 │ ✅ │ 60秒 │ 20 │
└─────┴──────────────┴───────────────┴─────────────┴──────────┴──────────┴────────────┴────────────┴──────────┴──────────┴────────┘
```
**业务优势**:
- 套餐为核心:所有流量业务围绕 package_usages 表,清晰明确
- 灵活的轮询配置:通过 polling_configs 表动态配置,不需要改代码
- 梯度配置:未实名卡和实名卡使用不同的轮询策略
- 细粒度控制:支持按运营商配置,支持手动禁用特定卡的轮询
- 流量历史:data_usage_records 表记录所有流量检查历史,便于分析
- 性能优化:只轮询有套餐的卡,通过 enable_polling 避免无效轮询
- 独立流程:实名轮询、卡流量轮询、套餐流量检查三个独立流程,互不干扰
**数据保留策略**:
- 流量使用记录表(data_usage_records)数据量会快速增长
- 建议定期清理 90 天前的记录,或使用 PostgreSQL 分区表
**替代方案**:
- ❌ 在设备表直接跟踪流量:设备和卡的逻辑应该独立,套餐才是业务核心
- ❌ 不区分卡流量轮询和套餐流量检查:混在一起会导致逻辑复杂,难以维护
- ❌ 使用固定的轮询频率:无法支持梯度策略,无法针对不同运营商优化

View File

@@ -0,0 +1,113 @@
## Why
构建 IoT 卡管理系统来支持三大核心业务:IoT 卡(物联网卡/流量卡)、设备(Device)、号卡(NumberCard)的全生命周期管理。系统需要支持平台自营和多级代理商分销模式、套餐订购流程和运营商订单回传处理,实现从产品分销到分佣结算的完整业务闭环。
**核心概念澄清**:
- **IoT 卡** = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)
- **普通卡**: 需要实名认证才能激活使用,遵循运营商实名制要求
- **行业卡**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
- **设备**:用户的物联网设备(如 GPS 追踪器、智能传感器),可绑定 1-4 张 IoT 卡,主要用于批量管理和设备操作(重启、修改密码等),不在卡管系统中销售
- **号卡**:完全独立的业务线,从上游平台下单,不走我们平台激活和充值,只接收订单状态更新
## What Changes
- 新增 IoT 卡(IotCard)业务模型:支持 IoT 卡库存管理、平台自营销售、代理分销(分配)、套餐购买订单生成、集成 Gateway 项目 HTTP 接口获取卡状态/实名状态/流量详情/停复机操作等能力
- 新增设备(Device)业务模型:支持用户设备管理、与 IoT 卡的绑定关系(1设备绑定1-4张IoT卡)、设备批量分配、设备操作(重启、修改密码、重置等)
- 新增号卡(NumberCard)业务模型:支持运营商订单回传、虚拟商品编码映射、号卡代理分销和分佣、运营商侧套餐管理
- 新增套餐(Package)管理:支持 IoT 卡套餐定义、套餐系列、真流量/虚流量共存机制、套餐订购流程、设备级套餐(流量共享)
- 新增订单(Order)管理:支持两种订单类型(套餐订单、号卡订单)、订单状态流转、设备级套餐订单
- 新增多级代理商分佣体系:支持树形代理关系(每个代理只有一个上级)、三种分佣类型(一次性/长期/组合)、分佣计算逻辑、梯度佣金、分佣解冻和审批流程
- 集成现有用户体系:复用已有的平台用户、代理用户、企业用户、个人用户模型
## Capabilities
### New Capabilities
#### 核心数据模型
- `iot-card`: IoT 卡业务模型 - 定义 IoT 卡实体(物联网卡/流量卡)、卡业务类型(普通卡/行业卡,card_category)、状态、库存管理、平台自营和代理分销规则、Gateway 项目集成(状态/实名/流量/停复机)、运营商关联(carrier_id)、轮询控制字段(enable_polling、last_data_check_at、last_real_name_check_at)、行业卡无需实名认证规则
- `iot-device`: 设备业务模型 - 定义设备实体、用户设备管理、IoT 卡绑定关系(1设备绑定1-4张IoT卡)、设备操作接口(重启/修改密码/重置)、设备批量分配
- `iot-number-card`: 号卡业务模型 - 定义号卡实体、虚拟商品编码、运营商订单映射、代理分销和分佣规则(下单即冻结、次月导入Excel解冻)
- `iot-package`: 套餐管理 - 定义套餐实体(只适用于IoT卡)、套餐系列关联(series_id)、真流量/虚流量共存机制(real_data_mb+virtual_data_mb)、停机判断规则(基于虚流量)、设备级套餐(流量共享)
- `iot-order`: 订单管理 - 定义订单实体、订单类型(1-套餐订单 2-号卡订单)、订单状态流转、设备级套餐订单支持
- `iot-agent-commission`: 代理分佣 - 定义代理树形关系、分佣规则(一次性/长期/组合,series_id用于一次性分佣,package_id用于长期分佣)、分佣计算逻辑、梯度佣金(号卡:激活量;IoT卡:激活量+提货量)、分佣解冻条件(行业卡无需实名认证即可解冻),组合佣金:时间点 OR 套餐周期阈值、结算流程
#### 财务和账户管理
- `iot-commission-withdrawal`: 佣金提现管理 - 代理佣金提现申请、审批流程、提现记录查询
- `iot-commission-withdrawal-settings`: 佣金提现设置 - 提现参数配置(最低金额、手续费率、到账时间等)
- `iot-financial-account`: 我的账户 - 查询当前登录账号的佣金数据(可提现余额、冻结金额、累计收入等)
- `iot-payment-merchant-settings`: 收款商户设置 - 配置支付参数(支付宝、微信等收款账户)
- `iot-dev-capability-management`: 开发能力管理 - 管理 API 对接参数(AppID、AppSecret、回调地址等)
- `iot-commission-template-management`: 分佣模板管理 - 创建和管理分佣模板,快速为代理分配产品时设置佣金规则
#### 商品管理
- `iot-number-card-management`: 号卡管理 - 新增和管理号卡商品基础信息(虚拟商品编码、运营商、套餐类型等)
- `iot-number-card-allocation`: 号卡分配 - 为特定代理分配号卡商品,设置佣金模式(一次性/长期/组合)
- `iot-package-series-management`: 套餐系列管理 - 新增和管理套餐系列(用于分组和佣金规则配置)
- `iot-package-management`: 套餐管理 - 新增和管理套餐(只能看到自己的套餐;管理员可以看到全部)
- `iot-package-allocation`: 套餐分配 - 为直属下级代理分配套餐,设置佣金模式
#### 资产管理
- `iot-single-card-info`: 单卡信息查询 - 通过 ICCID 查询单卡详细信息,提供操作入口(套餐充值、停复机、流量详情、更改过期时间、转新卡、停复机记录、往期订单、增减流量、变更钱包余额、充值支付密码、续充、设备操作)
- `iot-card-asset-management`: IoT 卡资产管理 - 查询 IoT 卡信息,提供批量操作入口(批量分配、批量激活、批量停复机等)
- `iot-device-asset-management`: 设备资产管理 - 查看设备信息,提供操作入口,查看和修改设备绑定的 IoT 卡信息,执行设备相关操作(重启、修改密码、重置)
- `iot-asset-allocation`: 资产分配 - 为特定代理批量分配 IoT 卡或设备(支持设备批量分配和 IoT 卡批量分配;设备分配时自动分配绑定的所有 IoT 卡)
- `iot-card-replacement-request`: 换卡申请管理 - 客户提交的换卡申请管理,处理换卡申请,填充新的 ICCID
### Modified Capabilities
无 - 本次变更为新增能力,不修改现有能力的需求。已有的用户体系(`user-organization`, `auth`, `role-permission`)将被复用,但不修改其规范。
## Impact
**新增数据模型**:
- 运营商(Carrier)表及 GORM 模型 - 运营商基础信息(中国移动、中国联通、中国电信)
- IoT 卡(IotCard)表及 GORM 模型 - 物联网卡/流量卡的统一管理
- 设备(Device)表及 GORM 模型 - 用户设备管理
- 设备-IoT卡绑定关系(DeviceSimBinding)表及 GORM 模型
- 号卡(NumberCard)表及 GORM 模型
- 套餐系列(PackageSeries)表及 GORM 模型
- 套餐(Package)表及 GORM 模型
- 代理套餐分配(AgentPackageAllocation)表及 GORM 模型
- 套餐使用情况(PackageUsage)表及 GORM 模型 - 跟踪单卡套餐和设备级套餐的流量使用
- 轮询配置(PollingConfig)表及 GORM 模型 - 支持梯度轮询策略(实名检查、卡流量检查、套餐流量检查)
- 流量使用记录(DataUsageRecord)表及 GORM 模型 - 记录卡的流量历史,支持流量查询和分析
- 订单(Order)表及 GORM 模型
- 代理层级关系(AgentHierarchy)表及 GORM 模型
- 分佣规则(CommissionRule)表及 GORM 模型
- 阶梯分佣配置(CommissionLadder)表及 GORM 模型
- 组合分佣条件(CommissionCombinedCondition)表及 GORM 模型
- 分佣记录(CommissionRecord)表及 GORM 模型
- 分佣审批(CommissionApproval)表及 GORM 模型
- 分佣模板(CommissionTemplate)表及 GORM 模型
- 号卡运营商结算(CarrierSettlement)表及 GORM 模型
- 佣金提现申请(CommissionWithdrawalRequest)表及 GORM 模型
- 佣金提现设置(CommissionWithdrawalSetting)表及 GORM 模型
- 收款商户设置(PaymentMerchantSetting)表及 GORM 模型
- 开发能力配置(DevCapabilityConfig)表及 GORM 模型
- 换卡申请(CardReplacementRequest)表及 GORM 模型
**系统集成**:
- 依赖现有用户体系(`user_organizations`, `users`, `roles`, `permissions` 等表)
- 需要支持三个前端入口:Web 后台(平台+代理)、H5代理/企业端、H5客户端
- 集成 Gateway 项目 HTTP 接口:SIM 卡状态查询、实名状态查询、流量详情查询、停复机操作等
**业务流程**:
- IoT 卡的平台自营销售流程和代理分销流程
- 设备的用户管理流程(添加设备、绑定IoT卡、设备操作)
- 设备的批量分配流程(运营人员分配设备给代理,自动分配绑定的所有IoT卡)
- 套餐购买订单流程(单卡套餐订单、设备级套餐订单)
- 设备级套餐流量共享机制(套餐分配到设备绑定的所有IoT卡,流量共享)
- 号卡的虚拟商品编码映射和运营商订单回传
- 多级代理分佣计算和结算流程:
- IoT 卡分佣:一次性佣金(实名+充值+购买套餐)、长期佣金(购买套餐)、组合佣金(时间点 OR 套餐周期阈值)
- 号卡分佣:下单即冻结,次月导入Excel解冻
- 分佣解冻和审批流程
**明确排除的范围**(本阶段不涉及):
- API 层(Handlers)
- 业务逻辑层(Services)
- 计费系统实现(Billing Engine)
- 供应管理集成(Provisioning)
- 事件系统集成(Events)
- 单元测试和集成测试
- API 文档生成

View File

@@ -0,0 +1,328 @@
## ADDED Requirements
### Requirement: 代理树形关系
系统 SHALL 管理代理的树形层级关系,每个代理只有一个上级代理。
**agent_hierarchies 表**:
- `id`: 代理关系 ID(主键,BIGINT)
- `agent_id`: 代理用户 ID(BIGINT,唯一)
- `parent_agent_id`: 上级代理用户 ID(BIGINT,可空,NULL 表示顶级代理)
- `level`: 代理层级(INT,1-顶级代理 2-二级代理 ...)
- `path`: 代理路径(VARCHAR(500),如 "1/5/12",用于快速获取整个代理链)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建顶级代理
- **WHEN** 平台创建顶级代理(用户 ID 为 101)
- **THEN** 系统创建代理关系记录,`agent_id` 为 101,`parent_agent_id` 为 NULL,`level` 为 1,`path` 为 "101"
#### Scenario: 创建下级代理
- **WHEN** 顶级代理(ID 为 101)创建下级代理(用户 ID 为 102)
- **THEN** 系统创建代理关系记录,`agent_id` 为 102,`parent_agent_id` 为 101,`level` 为 2,`path` 为 "101/102"
#### Scenario: 查询代理的整个上级链
- **WHEN** 查询代理(ID 为 103,路径为 "101/102/103")的上级链
- **THEN** 系统解析 `path` 字段,返回代理 101(顶级)、102(父级)、103(当前代理)
---
### Requirement: 分佣规则配置
系统 SHALL 支持为代理配置分佣规则,包括一次性分佣、长期分佣和组合分佣。
**commission_rules 表**:
- `id`: 分佣规则 ID(主键,BIGINT)
- `agent_id`: 代理用户 ID(BIGINT)
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
- `commission_type`: 分佣类型(VARCHAR(20),"one_time"-一次性 | "long_term"-长期 | "combined"-组合)
- `series_id`: 套餐系列 ID(BIGINT,可空,**仅一次性分佣使用**,关联 package_series 表)
- `package_id`: 套餐 ID(BIGINT,可空,**仅长期分佣使用**,关联 packages 表)
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
- `commission_value`: 分佣值(DECIMAL(10,4),固定金额或百分比值)
- `freeze_days`: 冻结天数(INT,分佣冻结天数,默认 7)
- `is_ladder`: 是否阶梯分佣(BOOLEAN,默认 false)
- `status`: 规则状态(INT,1-有效 2-无效)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
**字段使用规则**:
- **一次性分佣**: 使用 `series_id` 关联套餐系列,`package_id` 为 NULL
- **长期分佣**: 使用 `package_id` 关联具体套餐,`series_id` 为 NULL
- **组合分佣**: 需要创建两条规则记录,一条一次性(使用 `series_id`),一条长期(使用 `package_id`)
- **`series_id``package_id` 互斥**: 不能同时有值
#### Scenario: 配置一次性分佣规则
- **WHEN** 平台为代理(ID 为 123)配置一次性分佣规则,套餐系列 ID 为 1(月套餐系列),固定金额 5.00 元
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL,`commission_mode` 为 "fixed",`commission_value` 为 5.00
#### Scenario: 配置长期分佣规则
- **WHEN** 平台为代理(ID 为 123)配置长期分佣规则,套餐 ID 为 3001,百分比 5%
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,`commission_mode` 为 "percent",`commission_value` 为 0.05
#### Scenario: 配置组合分佣规则
- **WHEN** 平台为代理(ID 为 123)配置组合分佣规则,套餐系列 ID 为 1,先一次性分佣 10.00 元,连续在网 3 个月后开始长期分佣(套餐 ID 为 3001)3.00 元/月
- **THEN** 系统创建两条分佣规则:
- 一条 `commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL
- 另一条 `commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,且关联组合条件
#### Scenario: 字段互斥校验
- **WHEN** 平台尝试创建分佣规则,同时设置 `series_id` 为 1 和 `package_id` 为 3001
- **THEN** 系统拒绝创建,返回错误信息"`series_id``package_id` 不能同时有值"
---
### Requirement: 组合分佣条件配置
系统 SHALL 支持为组合分佣配置解冻条件,包括时间点条件和套餐周期条件。
**commission_combined_conditions 表**:
- `id`: 组合条件 ID(主键,BIGINT)
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT,必须是 commission_type 为 "long_term" 且属于组合分佣的规则)
- `condition_type`: 条件类型(VARCHAR(20),"time_point"-时间点 | "package_cycle"-套餐周期)
- `time_months`: 时间月数(INT,可空,仅当 condition_type 为 "time_point" 时有值,表示实名后多少个月)
- `package_cycle_threshold`: 套餐周期阈值(INT,可空,仅当 condition_type 为 "package_cycle" 时有值,表示使用多少个套餐周期)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
**解冻逻辑**: 组合分佣的长期部分,当满足**任一条件**(OR 关系)时开始产生长期分佣。
#### Scenario: 配置时间点条件
- **WHEN** 平台为组合分佣规则(ID 为 501)配置时间点条件,实名后 3 个月开始长期分佣
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "time_point",`time_months` 为 3
#### Scenario: 配置套餐周期条件
- **WHEN** 平台为组合分佣规则(ID 为 501)配置套餐周期条件,使用 10 个套餐周期后开始长期分佣
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "package_cycle",`package_cycle_threshold` 为 10
#### Scenario: 同时配置两种条件(OR 关系)
- **WHEN** 平台为组合分佣规则(ID 为 501)同时配置时间点条件(6 个月)和套餐周期条件(10 个周期)
- **THEN** 系统创建两条组合条件记录,长期分佣在任一条件满足时开始
---
### Requirement: 阶梯分佣配置
系统 SHALL 支持阶梯分佣,根据激活量/提货量达到阶梯条件后变更分佣值。
**commission_ladder 表**:
- `id`: 阶梯配置 ID(主键,BIGINT)
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT)
- `ladder_type`: 阶梯类型(VARCHAR(20),"activation"-激活量 | "pickup"-提货量 | "deposit"-保证金)
- `ladder_threshold`: 阶梯阈值(INT,如激活 100 张)
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
- `commission_value`: 分佣值(DECIMAL(10,4),达到阶梯后的分佣值)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 配置激活量阶梯
- **WHEN** 平台为代理(ID 为 123)配置阶梯分佣,激活 100 张卡后分佣从 5.00 元提升到 8.00 元
- **THEN** 系统创建阶梯配置,`ladder_type` 为 "activation",`ladder_threshold` 为 100,`commission_value` 为 8.00
#### Scenario: 计算阶梯分佣
- **WHEN** 代理(ID 为 123)当月激活量达到 100 张
- **THEN** 系统根据阶梯配置,从第 101 张卡开始使用新的分佣值 8.00 元
---
### Requirement: 分佣记录管理
系统 SHALL 记录每笔分佣,支持冻结、解冻和发放流程。
**commission_records 表**:
- `id`: 分佣记录 ID(主键,BIGINT)
- `agent_id`: 代理用户 ID(BIGINT)
- `order_id`: 订单 ID(BIGINT)
- `commission_rule_id`: 分佣规则 ID(BIGINT)
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
- `amount`: 分佣金额(DECIMAL(10,2),元)
- `status`: 分佣状态(INT,1-冻结 2-解冻中 3-已发放 4-已失效)
- `freeze_until`: 冻结截止时间(TIMESTAMP,可空)
- `released_at`: 发放时间(TIMESTAMP,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建一次性分佣记录
- **WHEN** 订单(ID 为 10001)完成,触发代理(ID 为 123)的一次性分佣 5.00 元,冻结 7 天
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结),`freeze_until` 为 7 天后
#### Scenario: 分佣自动解冻
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,且满足解冻条件(激活+实名+充值)
- **THEN** 系统将分佣状态从 1(冻结) 变更为 2(解冻中),创建分佣解冻审批记录
#### Scenario: 分佣发放
- **WHEN** 分佣解冻审批通过
- **THEN** 系统将分佣状态从 2(解冻中) 变更为 3(已发放),将分佣金额转入代理钱包,`released_at` 记录发放时间
---
### Requirement: 分佣解冻条件
系统 SHALL 根据分佣类型校验不同的解冻条件。
**一次性分佣解冻条件**:
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
- 达到累计/首次充值金额
- 冻结天数到达
**长期分佣解冻条件**:
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
- 达到累计/首次充值金额
- 在网状态正常
- 三无校验通过(通过 Excel 导入解冻)
**组合分佣解冻条件**:
- **一次性部分**: 立即产生并按一次性分佣条件解冻
- **长期部分**: 当满足以下**任一条件**时开始长期分佣(OR 关系):
- 达到某个时间点之后(例如:实名后 3 个月)
- **OR** 该 IoT 卡的套餐使用周期数达到阈值(例如:10 个周期)
- **注意**: 套餐周期阈值是针对单张 IoT 卡的,不是设备级别
#### Scenario: 一次性分佣满足解冻条件
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,用户已实名且已充值
- **THEN** 系统将分佣状态变更为 2(解冻中),创建审批记录
#### Scenario: 长期分佣等待 Excel 导入解冻
- **WHEN** 长期分佣记录等待三无校验
- **THEN** 系统保持分佣状态为 1(冻结),等待平台通过 Excel 导入解冻数据
#### Scenario: 组合分佣时间点条件满足
- **WHEN** 组合分佣规则配置为实名后 3 个月开始长期分佣,IoT 卡已实名 3 个月
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使套餐周期数未达到阈值
#### Scenario: 组合分佣套餐周期条件满足
- **WHEN** 组合分佣规则配置为套餐使用 10 个周期后开始长期分佣,IoT 卡已使用套餐 10 个周期
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使未达到时间点要求
#### Scenario: 组合分佣任一条件满足即开始
- **WHEN** 组合分佣规则配置为"实名后 6 个月 OR 10 个套餐周期",IoT 卡已使用 10 个周期但只实名 2 个月
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录(因为套餐周期条件已满足)
#### Scenario: 行业卡一次性分佣解冻(无需实名)
- **WHEN** 行业卡(card_category 为 "industry")的一次性分佣记录冻结期到达,卡已激活且已充值,但实名状态为未实名
- **THEN** 系统判定解冻条件满足(行业卡无需实名认证),将分佣状态变更为 2(解冻中),创建审批记录
#### Scenario: 行业卡长期分佣解冻(无需实名)
- **WHEN** 行业卡(card_category 为 "industry")的长期分佣记录满足充值金额和在网状态,但实名状态为未实名
- **THEN** 系统判定行业卡无需实名认证,等待三无校验通过后可解冻
---
### Requirement: 分佣解冻审批
系统 SHALL 支持分佣解冻审批流程,审批通过后发放分佣。
**commission_approvals 表**:
- `id`: 审批记录 ID(主键,BIGINT)
- `commission_record_id`: 分佣记录 ID(BIGINT)
- `approval_type`: 审批类型(VARCHAR(20),"auto"-自动 | "manual"-人工)
- `status`: 审批状态(INT,1-待审批 2-已通过 3-已拒绝)
- `approver_id`: 审批人用户 ID(BIGINT,可空)
- `approval_time`: 审批时间(TIMESTAMP,可空)
- `approval_note`: 审批备注(TEXT,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建审批记录
- **WHEN** 分佣记录(ID 为 1001)状态变更为 2(解冻中)
- **THEN** 系统创建审批记录,`commission_record_id` 为 1001,`approval_type` 为 "auto",状态为 1(待审批)
#### Scenario: 审批通过
- **WHEN** 审批人(用户 ID 为 999)审批通过审批记录(ID 为 2001)
- **THEN** 系统将审批状态变更为 2(已通过),分佣记录状态变更为 3(已发放),将分佣金额转入代理钱包
#### Scenario: 审批拒绝
- **WHEN** 审批人拒绝审批记录(ID 为 2001),备注"用户未满足在网条件"
- **THEN** 系统将审批状态变更为 3(已拒绝),分佣记录状态变更为 4(已失效)
---
### Requirement: 分佣模板
系统 SHALL 支持创建分佣模板,存储常用的分佣方案,便于快速配置。
**commission_templates 表**:
- `id`: 模板 ID(主键,BIGINT)
- `template_name`: 模板名称(VARCHAR(255))
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed" | "percent")
- `commission_value`: 分佣值(DECIMAL(10,4))
- `freeze_days`: 冻结天数(INT)
- `is_ladder`: 是否阶梯分佣(BOOLEAN)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建分佣模板
- **WHEN** 平台创建分佣模板"标准月套餐分佣",业务类型为 IoT 卡,一次性分佣 5.00 元,冻结 7 天
- **THEN** 系统创建模板记录,`template_name` 为 "标准月套餐分佣",`business_type` 为 "iot_card",`commission_type` 为 "one_time",`commission_value` 为 5.00,`freeze_days` 为 7
#### Scenario: 应用分佣模板
- **WHEN** 平台为代理(ID 为 123)应用模板(ID 为 501)
- **THEN** 系统根据模板配置创建分佣规则,`agent_id` 为 123,其他字段从模板复制
---
### Requirement: 多级代理分佣
系统 SHALL 支持多级代理分佣,根据代理路径计算每一级代理的分佣。
**多级分佣规则**:
- 通过代理路径(`path`)获取整个代理链
- 为每一级代理查找对应的分佣规则
- 创建多条分佣记录,每条对应一个代理
#### Scenario: 三级代理分佣
- **WHEN** 订单(ID 为 10001)的代理路径为 "101/102/103",每级代理配置分佣:101(2.00 元)、102(3.00 元)、103(5.00 元)
- **THEN** 系统创建 3 条分佣记录:代理 101 的 2.00 元、代理 102 的 3.00 元、代理 103 的 5.00 元
---
### Requirement: 分佣数据校验
系统 SHALL 对分佣数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 代理 ID(agent_id):必填,≥ 1
- 订单 ID(order_id):必填,≥ 1
- 分佣金额(amount):必填,≥ 0,最多 2 位小数
- 分佣状态(status):必填,枚举值 1-4
- 冻结天数(freeze_days):必填,≥ 0
#### Scenario: 创建分佣记录时金额为负数
- **WHEN** 创建分佣记录,金额为 -5.00
- **THEN** 系统拒绝创建,返回错误信息"分佣金额必须 ≥ 0"
#### Scenario: 创建分佣规则时分佣值无效
- **WHEN** 创建分佣规则,分佣模式为百分比,分佣值为 1.5(超过 100%)
- **THEN** 系统拒绝创建,返回错误信息"百分比分佣值必须在 0-1 之间"

View File

@@ -0,0 +1,291 @@
## ADDED Requirements
### Requirement: IoT 卡实体定义
系统 SHALL 定义 IoT 卡(IotCard)实体,包含 IoT 卡(物联网卡/流量卡/SIM卡)的商品属性、状态属性、所有权信息和 Gateway 集成字段。
**核心概念**: IoT 卡 = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)。系统使用 ICCID 作为 IoT 卡的唯一标识。
**卡业务类型**:
- **普通卡(normal)**: 需要实名认证才能激活使用,遵循运营商实名制要求
- **行业卡(industry)**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
**实体字段**:
**商品属性**:
- `id`: IoT 卡 ID(主键,BIGINT)
- `iccid`: ICCID(VARCHAR(50),唯一,国际移动用户识别码,IoT卡的唯一标识)
- `card_type`: 卡类型(VARCHAR(50),如 "4G"、"5G"、"NB-IoT")
- `card_category`: 卡业务类型(VARCHAR(20),枚举值:"normal"-普通卡 | "industry"-行业卡,默认 "normal")
- `carrier_id`: 运营商 ID(BIGINT,关联 carriers 表,如中国移动、中国联通、中国电信)
- `imsi`: IMSI(VARCHAR(50),可选,国际移动用户识别码)
- `msisdn`: 手机号码(VARCHAR(20),可选)
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
- `supplier`: 供应商名称(VARCHAR(255),可选)
- `cost_price`: 成本价(DECIMAL(10,2),平台进货价)
- `distribute_price`: 分销价(DECIMAL(10,2),分销给代理的价格,仅当 owner_type 为 agent 时有值)
**所有权和状态**:
- `status`: IoT 卡状态(INT,1-在库 2-已分销 3-已激活 4-已停用)
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台自营 | "agent"-代理商 | "user"-用户 | "device"-设备)
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user/device 时为对应的 ID)
- `activated_at`: 激活时间(TIMESTAMP,可空)
**Gateway 集成字段**(从 Gateway 项目同步):
- `activation_status`: 激活状态(INT,0-未激活 1-已激活)
- `real_name_status`: 实名状态(INT,0-未实名 1-已实名)
- `network_status`: 网络状态(INT,0-停机 1-开机)
- `data_usage_mb`: 累计流量使用(BIGINT,MB 为单位,默认 0)
- `last_sync_time`: 最后一次与 Gateway 同步时间(TIMESTAMP,可空)
**轮询控制字段**:
- `enable_polling`: 是否参与轮询(BOOLEAN,默认 true,用于控制是否对该卡进行定时轮询)
- `last_data_check_at`: 最后一次卡流量检查时间(TIMESTAMP,可空,记录上次轮询卡流量的时间)
- `last_real_name_check_at`: 最后一次实名检查时间(TIMESTAMP,可空,记录上次轮询实名状态的时间)
**系统字段**:
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建平台自营 IoT 卡
- **WHEN** 平台批量导入 IoT 卡数据,ICCID 为 "89860123456789012345"
- **THEN** 系统创建 IoT 卡记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(在库),`activation_status` 为 0(未激活)
#### Scenario: 平台分销 IoT 卡给代理
- **WHEN** 平台将在库 IoT 卡分销给代理商(用户 ID 为 123),设置分销价为 50.00 元
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 2(已分销),`owner_type` 变更为 "agent",`owner_id` 设置为 123,`distribute_price` 设置为 50.00
#### Scenario: IoT 卡绑定到设备
- **WHEN** 用户将 IoT 卡(ICCID 为 "8986...")绑定到设备(ID 为 1001)
- **THEN** 系统在 `device_sim_bindings` 表创建绑定记录,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
#### Scenario: IoT 卡直接销售给用户
- **WHEN** 平台或代理将 IoT 卡直接销售给用户(用户 ID 为 2001)
- **THEN** 系统创建套餐订单记录,IoT 卡的 `owner_type` 变更为 "user",`owner_id` 变更为 2001
#### Scenario: 行业卡无需实名认证
- **WHEN** 创建卡业务类型为 "industry"(行业卡)的 IoT 卡
- **THEN** 系统允许该卡在 `real_name_status` 为 0(未实名)的情况下激活使用,不强制要求实名认证
#### Scenario: 普通卡需要实名认证
- **WHEN** 创建卡业务类型为 "normal"(普通卡)的 IoT 卡
- **THEN** 系统要求该卡必须先完成实名认证(`real_name_status` 为 1)才能激活使用
---
### Requirement: IoT 卡状态流转
系统 SHALL 管理 IoT 卡的状态流转,确保状态变更符合业务规则。
**状态定义**:
- **1-在库**: IoT 卡在平台库存中,未分销
- **2-已分销**: IoT 卡已分销给代理商,代理可销售
- **3-已激活**: IoT 卡已被终端用户激活使用
- **4-已停用**: IoT 卡已停用,不可使用
**状态流转规则**:
- 在库(1) → 已分销(2): 平台分销给代理
- 在库(1) → 已激活(3): 平台自营直接销售给用户并激活
- 已分销(2) → 已激活(3): 代理销售给用户并激活
- 已激活(3) → 已停用(4): 用户或平台主动停用
- 已停用(4) → 已激活(3): 用户或平台主动复机(仅在符合业务规则时)
#### Scenario: 代理销售 IoT 卡给用户
- **WHEN** 代理商销售已分销 IoT 卡给终端用户并激活
- **THEN** 系统将 IoT 卡状态从 2(已分销) 变更为 3(已激活),`activated_at` 记录激活时间,`activation_status` 从 Gateway 同步后变更为 1
#### Scenario: 平台自营销售 IoT 卡
- **WHEN** 平台直接销售在库 IoT 卡给终端用户并激活
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 3(已激活),`owner_type` 保持 "platform",`activated_at` 记录激活时间
#### Scenario: 停用已激活 IoT 卡
- **WHEN** 用户或平台停用已激活 IoT 卡
- **THEN** 系统将 IoT 卡状态从 3(已激活) 变更为 4(已停用),通过 Gateway API 执行停机操作
---
### Requirement: IoT 卡平台自营和代理分销
系统 SHALL 支持 IoT 卡的平台自营销售和代理分销两种模式,通过 `owner_type``owner_id` 区分所有者。
**平台自营**:
- `owner_type` 为 "platform"
- `owner_id` 为 0
- 平台直接销售给终端用户
- 销售价格由平台自主定价
**代理分销**:
- `owner_type` 为 "agent"
- `owner_id` 为代理用户 ID
- 代理商可以销售给终端用户或下级代理
- 分销价格由平台设置(`distribute_price`),代理商可在分销价基础上加价(但不能超过 2 倍)
#### Scenario: 查询平台自营 IoT 卡库存
- **WHEN** 查询平台自营 IoT 卡库存
- **THEN** 系统返回 `owner_type` 为 "platform" 且 `status` 为 1(在库) 的 IoT 卡列表
#### Scenario: 查询代理分销 IoT 卡库存
- **WHEN** 代理商(用户 ID 为 123)查询自己的 IoT 卡库存
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
#### Scenario: 代理加价销售 IoT 卡套餐
- **WHEN** 代理商为已分销 IoT 卡设置套餐售价
- **THEN** 系统校验套餐售价不超过分销价的 2 倍,校验通过后允许销售
---
### Requirement: IoT 卡批量导入
系统 SHALL 支持批量导入 IoT 卡数据,用于初始化库存或补充库存。
**导入字段**:
- ICCID(必填)
- 卡类型(必填,如 "4G"、"5G"、"NB-IoT")
- 卡业务类型(可选,枚举值 "normal" | "industry",默认 "normal")
- 运营商 ID(必填,从 carriers 表中选择)
- IMSI(可选)
- 手机号码(可选)
- 供应商(可选)
- 成本价(必填)
- 批次号(必填)
**导入规则**:
- ICCID 必须唯一,重复 ICCID 将被拒绝
- 导入的 IoT 卡默认状态为 1(在库),所有者为平台(`owner_type` 为 "platform",`owner_id` 为 0)
- 导入成功后记录操作日志
#### Scenario: 批量导入 IoT 卡成功
- **WHEN** 平台上传包含 100 条 IoT 卡数据的 CSV 文件
- **THEN** 系统创建 100 条 IoT 卡记录,状态为 1(在库),所有者为平台,返回导入成功消息
#### Scenario: 批量导入包含重复 ICCID
- **WHEN** 平台上传的 CSV 文件中包含已存在的 ICCID
- **THEN** 系统拒绝重复 ICCID 的 IoT 卡,返回错误信息并列出重复 ICCID,其他有效 IoT 卡正常导入
---
### Requirement: IoT 卡查询和筛选
系统 SHALL 支持多维度查询和筛选 IoT 卡,包括状态、所有者、批次号、卡类型等。
**查询条件**:
- ICCID(精确匹配或模糊匹配)
- IoT 卡状态(单选或多选)
- 所有者类型(platform | agent | user | device)
- 所有者 ID(仅当所有者类型为 agent/user/device 时有效)
- 批次号(精确匹配)
- 卡类型(单选或多选)
- 运营商 ID(单选或多选,从 carriers 表选择)
- 激活状态(0-未激活 | 1-已激活)
- 实名状态(0-未实名 | 1-已实名)
- 网络状态(0-停机 | 1-开机)
- 是否参与轮询(true | false)
- 激活时间范围(开始时间 - 结束时间)
- 创建时间范围(开始时间 - 结束时间)
**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数
#### Scenario: 查询特定批次的在库 IoT 卡
- **WHEN** 平台查询批次号为 "BATCH-2025-001" 且状态为 1(在库) 的 IoT 卡
- **THEN** 系统返回符合条件的 IoT 卡列表,包含 ICCID、类型、运营商、成本价等信息
#### Scenario: 代理查询自己的已分销 IoT 卡
- **WHEN** 代理商(用户 ID 为 123)查询自己的已分销 IoT 卡
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
#### Scenario: 分页查询 IoT 卡
- **WHEN** 平台查询在库 IoT 卡,指定每页 50 条,查询第 2 页
- **THEN** 系统返回第 51-100 条 IoT 卡记录,以及总记录数和总页数
---
### Requirement: Gateway 集成
系统 SHALL 预留 IoT 卡状态相关字段,用于后续与 Gateway 项目集成。
**集成字段**:
- `activation_status`: 激活状态(从 Gateway 同步)
- `real_name_status`: 实名状态(从 Gateway 同步)
- `network_status`: 网络状态(从 Gateway 同步)
- `data_usage_mb`: 累计流量使用(从 Gateway 同步)
- `last_sync_time`: 最后同步时间
**集成说明**:
- 本阶段只设计数据模型字段,不实现 Gateway HTTP 客户端代码
- 后续 Service 层将调用 Gateway API 获取 IoT 卡状态并更新这些字段
- Gateway 使用 AES 加密 + MD5 签名的统一传输协议(参考 design.md)
**Gateway API 功能**:
- 查询 IoT 卡状态(激活状态、实名状态、网络状态)
- 查询流量详情(累计流量使用、剩余流量)
- 停复机操作(停机、复机)
- 实名认证操作
#### Scenario: 预留 Gateway 集成字段
- **WHEN** 创建 IoT 卡记录
- **THEN** 系统初始化 Gateway 相关字段为默认值:`activation_status` 为 0,`real_name_status` 为 0,`network_status` 为 0,`data_usage_mb` 为 0,`last_sync_time` 为空
#### Scenario: 从 Gateway 同步 IoT 卡状态
- **WHEN** Service 层调用 Gateway API 查询 IoT 卡状态
- **THEN** 系统更新 IoT 卡的 `activation_status``real_name_status``network_status``data_usage_mb``last_sync_time` 字段
---
### Requirement: IoT 卡数据校验
系统 SHALL 对 IoT 卡数据进行校验,确保数据完整性和一致性。
**校验规则**:
- ICCID(iccid):必填,长度 19-20 字符,唯一
- 卡类型(card_type):必填,长度 1-50 字符
- 卡业务类型(card_category):必填,枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
- 运营商 ID(carrier_id):必填,≥ 1,必须是有效的运营商 ID
- 成本价(cost_price):必填,≥ 0,最多 2 位小数
- 分销价(distribute_price):可选,≥ 0,最多 2 位小数,≥ 成本价
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user" | "device"
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
- 激活状态(activation_status):必填,枚举值 0(未激活) | 1(已激活)
- 实名状态(real_name_status):必填,枚举值 0(未实名) | 1(已实名),当 card_category 为 "industry"(行业卡)时可以保持 0
- 网络状态(network_status):必填,枚举值 0(停机) | 1(开机)
- 轮询开关(enable_polling):必填,布尔值 true | false
#### Scenario: 创建 IoT 卡时 ICCID 格式错误
- **WHEN** 平台创建 IoT 卡,ICCID 长度为 15(小于 19)
- **THEN** 系统拒绝创建,返回错误信息"ICCID 长度必须为 19-20 字符"
#### Scenario: 创建 IoT 卡时 ICCID 重复
- **WHEN** 平台创建 IoT 卡,ICCID 为已存在的 "89860123456789012345"
- **THEN** 系统拒绝创建,返回错误信息"ICCID 已存在"
#### Scenario: 创建 IoT 卡时成本价为负数
- **WHEN** 平台创建 IoT 卡,成本价为 -10.00
- **THEN** 系统拒绝创建,返回错误信息"成本价必须 ≥ 0"
#### Scenario: 创建 IoT 卡时分销价低于成本价
- **WHEN** 平台创建 IoT 卡,成本价为 50.00,分销价为 40.00
- **THEN** 系统拒绝创建,返回错误信息"分销价不能低于成本价"

View File

@@ -0,0 +1,311 @@
## ADDED Requirements
### Requirement: 设备实体定义
系统 SHALL 定义设备(Device)实体,用于管理用户的物联网设备(如 GPS 追踪器、智能传感器等),支持设备与 IoT 卡的绑定关系、设备批量分配和设备操作。
**核心概念**: 设备不在卡管系统中销售,主要用于:
1. 用户设备管理(用户添加自己的设备,绑定 IoT 卡)
2. 方便运营人员管理投诉和代理要求(通过设备维度批量查看绑定的所有 IoT 卡)
3. 设备操作(重启、修改账号密码、重置等)
4. 设备批量分配(运营人员在别的系统报单后发货,把设备和绑定的 IoT 卡一起分配给代理)
**实体字段**:
**基本属性**:
- `id`: 设备 ID(主键,BIGINT)
- `device_no`: 设备编号(唯一,VARCHAR(50))
- `device_name`: 设备名称(VARCHAR(255))
- `device_model`: 设备型号(VARCHAR(100))
- `device_type`: 设备类型(VARCHAR(50),如 "GPS Tracker"、"Camera"、"Sensor")
- `max_sim_slots`: 最大 IoT 卡插槽数量(INT,1-4,默认 4)
- `manufacturer`: 设备制造商(VARCHAR(255),可选)
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
**所有权和状态**:
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台库存(等待分配) | "agent"-代理商 | "user"-用户)
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user 时为对应的 ID)
- `status`: 设备状态(INT,1-未激活 2-已激活 3-已停用)
- `activated_at`: 激活时间(TIMESTAMP,可空)
**设备操作配置**(预留字段,用于后续设备操作功能):
- `device_username`: 设备登录账号(VARCHAR(100),可选)
- `device_password_encrypted`: 设备登录密码(加密存储,TEXT,可选)
- `device_api_endpoint`: 设备 API 接口地址(VARCHAR(500),可选)
**系统字段**:
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 用户添加设备
- **WHEN** 用户添加自己的设备(设备编号为 "GPS-001",设备名称为 "物流车辆追踪器")
- **THEN** 系统创建设备记录,`owner_type` 为 "user",`owner_id` 为用户 ID,状态为 1(未激活)
#### Scenario: 平台导入设备到库存
- **WHEN** 平台批量导入设备数据(准备发货给代理)
- **THEN** 系统创建设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
#### Scenario: 运营人员批量分配设备给代理
- **WHEN** 运营人员将平台库存设备(ID 为 1001)分配给代理商(用户 ID 为 123)
- **THEN** 系统将设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时自动分配该设备绑定的所有 IoT 卡给代理
---
### Requirement: 设备状态流转
系统 SHALL 管理设备的状态流转,确保状态变更符合业务规则。
**状态定义**:
- **1-未激活**: 设备尚未激活使用
- **2-已激活**: 设备已被用户激活使用
- **3-已停用**: 设备已停用,不可使用
**状态流转规则**:
- 未激活(1) → 已激活(2): 用户激活设备
- 已激活(2) → 已停用(3): 用户或平台主动停用设备
- 已停用(3) → 已激活(2): 用户或平台主动恢复设备(仅在符合业务规则时)
#### Scenario: 用户激活设备
- **WHEN** 用户激活自己的设备
- **THEN** 系统将设备状态从 1(未激活) 变更为 2(已激活),`activated_at` 记录激活时间
#### Scenario: 用户停用设备
- **WHEN** 用户停用已激活的设备
- **THEN** 系统将设备状态从 2(已激活) 变更为 3(已停用),同时可选择是否停用该设备绑定的所有 IoT 卡
---
### Requirement: 设备与 IoT 卡绑定关系
系统 SHALL 管理设备与 IoT 卡的绑定关系,一个设备可以绑定 1-4 张 IoT 卡。
**绑定规则**:
- 一个设备最多绑定 4 张 IoT 卡(由 `max_sim_slots` 字段控制)
- 一个 IoT 卡同一时间只能绑定一个设备
- 绑定时记录插槽位置(slot_position: 1, 2, 3, 4)
- 绑定时记录绑定时间和绑定状态(1-已绑定 2-已解绑)
- 设备绑定 IoT 卡后,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为设备 ID
**中间表 device_sim_bindings**:
- `id`: 绑定记录 ID(主键,BIGINT)
- `device_id`: 设备 ID(BIGINT)
- `iot_card_id`: IoT 卡 ID(BIGINT)
- `slot_position`: 插槽位置(INT,1-4)
- `bind_status`: 绑定状态(INT,1-已绑定 2-已解绑)
- `bind_time`: 绑定时间(TIMESTAMP)
- `unbind_time`: 解绑时间(TIMESTAMP,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 绑定 IoT 卡到设备
- **WHEN** 用户将 IoT 卡(ID 为 101)绑定到设备(ID 为 1001)的插槽 1
- **THEN** 系统创建绑定记录,`device_id` 为 1001,`iot_card_id` 为 101,`slot_position` 为 1,`bind_status` 为 1(已绑定),`bind_time` 为当前时间,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
#### Scenario: 绑定超过最大插槽数量
- **WHEN** 用户尝试将第 5 张 IoT 卡绑定到最大插槽数为 4 的设备
- **THEN** 系统拒绝绑定,返回错误信息"设备插槽已满,最多支持 4 张 IoT 卡"
#### Scenario: 绑定已被占用的 IoT 卡
- **WHEN** 用户尝试绑定已被其他设备绑定的 IoT 卡
- **THEN** 系统拒绝绑定,返回错误信息"该 IoT 卡已被其他设备绑定"
#### Scenario: 解绑 IoT 卡
- **WHEN** 用户解绑设备的 IoT 卡(绑定记录 ID 为 10)
- **THEN** 系统将绑定记录的 `bind_status` 从 1(已绑定) 变更为 2(已解绑),`unbind_time` 记录解绑时间,IoT 卡的 `owner_type``owner_id` 重置
#### Scenario: 查询设备当前绑定的 IoT 卡
- **WHEN** 用户查询设备(ID 为 1001)当前绑定的 IoT 卡
- **THEN** 系统返回 `device_id` 为 1001 且 `bind_status` 为 1(已绑定) 的所有绑定记录,包含 IoT 卡信息(ICCID、运营商、激活状态等)和插槽位置
---
### Requirement: 设备套餐购买和流量共享
系统 SHALL 支持用户为设备购买套餐,套餐自动分配到设备绑定的所有 IoT 卡,流量在设备级别共享。
**设备套餐业务规则**:
- 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张)
- 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡)
- 分佣**只计算一次**(不按卡数倍增)
- 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡
**套餐分配示例**:
- 设备绑定 3 张 IoT 卡
- 用户购买套餐:399 元/年,每月 3000G 流量,长期佣金 100 元
- 用户支付:399 元
- 套餐分配:设备的 3 张 IoT 卡都获得该套餐
- 流量使用:3000G/月 在 3 张卡之间共享(不是每张卡 3000G,而是总共 3000G)
- 分佣:代理获得 100 元分佣(只分一次,不是 3 × 100 元)
#### Scenario: 用户为设备购买套餐
- **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买套餐(套餐 ID 为 3001,399 元/年,3000G/月)
- **THEN** 系统创建套餐订单,`device_id` 为 1001,`package_id` 为 3001,订单金额为 399 元,将套餐分配到设备绑定的 3 张 IoT 卡,设置流量共享模式为设备级别
#### Scenario: 设备级流量共享
- **WHEN** 设备(ID 为 1001)的套餐流量为 3000G/月,设备绑定 3 张 IoT 卡
- **THEN** 系统设置流量共享模式,3 张 IoT 卡共享 3000G/月(不是每张卡 3000G),无论使用哪张卡,都从这个流量池扣除
#### Scenario: 设备套餐分佣
- **WHEN** 用户为设备购买套餐,订单金额为 399 元,代理的长期分佣规则为 100 元
- **THEN** 系统为代理创建一条分佣记录,分佣金额为 100 元(只分一次,不按设备绑定的卡数倍增)
---
### Requirement: 设备批量分配
系统 SHALL 支持运营人员批量分配设备给代理,设备分配时自动分配该设备绑定的所有 IoT 卡。
**分配规则**:
- 只能分配 `owner_type` 为 "platform" 的设备(平台库存)
- 分配时,设备的 `owner_type` 变更为 "agent",`owner_id` 设置为代理用户 ID
- 分配时,设备绑定的所有 IoT 卡的 `owner_type` 也变更为 "agent",`owner_id` 设置为代理用户 ID
- 分配操作记录到操作日志
#### Scenario: 运营人员批量分配设备
- **WHEN** 运营人员将 10 台设备(平台库存)分配给代理商(用户 ID 为 123)
- **THEN** 系统将这 10 台设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时将这些设备绑定的所有 IoT 卡也分配给代理 123
#### Scenario: 分配已分配的设备
- **WHEN** 运营人员尝试分配 `owner_type` 为 "agent" 的设备
- **THEN** 系统拒绝分配,返回错误信息"该设备已分配给代理,不能重复分配"
---
### Requirement: 设备操作
系统 SHALL 支持对设备的远程操作(重启、修改账号密码、重置等),用于设备管理和故障排查。
**设备操作类型**:
- **重启设备**: 远程重启设备
- **修改账号密码**: 修改设备的登录账号和密码
- **重置设备**: 将设备恢复到出厂设置
- **查询设备状态**: 查询设备的在线状态、运行状态等
- **设备配置更新**: 更新设备的配置参数
**操作说明**:
- 本阶段只设计数据模型字段和接口定义,不实现设备操作的具体代码
- 后续 Service 层将调用设备厂商提供的 API 或通过 MQTT/HTTP 协议与设备通信
- 设备操作需要记录操作日志(操作类型、操作人、操作时间、操作结果)
#### Scenario: 重启设备
- **WHEN** 用户或运营人员请求重启设备(ID 为 1001)
- **THEN** 系统调用设备 API 发送重启命令,记录操作日志,返回操作结果
#### Scenario: 修改设备密码
- **WHEN** 用户或运营人员修改设备(ID 为 1001)的登录密码
- **THEN** 系统更新设备的 `device_password_encrypted` 字段(加密存储),调用设备 API 同步密码修改,记录操作日志
---
### Requirement: 设备批量导入
系统 SHALL 支持批量导入设备数据,用于平台库存管理。
**导入字段**:
- 设备编号(必填)
- 设备名称(必填)
- 设备型号(必填)
- 设备类型(必填)
- 最大插槽数(可选,默认 4)
- 设备制造商(可选)
- 批次号(必填)
**导入规则**:
- 设备编号必须唯一,重复编号将被拒绝
- 导入的设备默认 `owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
- 导入成功后记录操作日志
#### Scenario: 批量导入设备成功
- **WHEN** 平台上传包含 50 条设备数据的 CSV 文件
- **THEN** 系统创建 50 条设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活),返回导入成功消息
#### Scenario: 批量导入包含重复编号
- **WHEN** 平台上传的 CSV 文件中包含已存在的设备编号
- **THEN** 系统拒绝重复编号的设备,返回错误信息并列出重复编号,其他有效设备正常导入
---
### Requirement: 设备查询和筛选
系统 SHALL 支持多维度查询和筛选设备,包括状态、所有者、批次号、设备类型等。
**查询条件**:
- 设备编号(精确匹配或模糊匹配)
- 设备名称(模糊匹配)
- 设备状态(单选或多选)
- 所有者类型(platform | agent | user)
- 所有者 ID(仅当所有者类型为 agent/user 时有效)
- 批次号(精确匹配)
- 设备类型(单选或多选)
- 设备制造商(模糊匹配)
- 激活时间范围(开始时间 - 结束时间)
- 创建时间范围(开始时间 - 结束时间)
**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数
#### Scenario: 查询平台库存设备
- **WHEN** 运营人员查询平台库存设备
- **THEN** 系统返回 `owner_type` 为 "platform" 的设备列表
#### Scenario: 代理查询自己的设备
- **WHEN** 代理商(用户 ID 为 123)查询自己的设备
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 的设备列表
#### Scenario: 用户查询自己的设备
- **WHEN** 用户(用户 ID 为 2001)查询自己的设备
- **THEN** 系统返回 `owner_type` 为 "user" 且 `owner_id` 为 2001 的设备列表,包含设备绑定的所有 IoT 卡信息
#### Scenario: 运营人员通过设备查看绑定的所有 IoT 卡
- **WHEN** 运营人员需要处理投诉,查询设备(ID 为 1001)绑定的所有 IoT 卡
- **THEN** 系统返回设备信息和绑定的所有 IoT 卡详细信息(ICCID、运营商、激活状态、流量使用等),方便统一查看和管理
---
### Requirement: 设备数据校验
系统 SHALL 对设备数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 设备编号(device_no):必填,长度 1-50 字符,唯一
- 设备名称(device_name):必填,长度 1-255 字符
- 设备型号(device_model):必填,长度 1-100 字符
- 设备类型(device_type):必填,长度 1-50 字符
- 最大插槽数(max_sim_slots):必填,1-4 之间的整数
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user"
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
- 设备状态(status):必填,枚举值 1(未激活) | 2(已激活) | 3(已停用)
#### Scenario: 创建设备时插槽数超出范围
- **WHEN** 用户创建设备,最大插槽数为 5
- **THEN** 系统拒绝创建,返回错误信息"最大插槽数必须在 1-4 之间"
#### Scenario: 创建设备时设备编号重复
- **WHEN** 用户创建设备,设备编号为已存在的 "DEV-001"
- **THEN** 系统拒绝创建,返回错误信息"设备编号已存在"

View File

@@ -0,0 +1,160 @@
## ADDED Requirements
### Requirement: 号卡实体定义
系统 SHALL 定义号卡(NumberCard)实体,作为运营商订单回传的映射,支持代理分销和分佣。
**实体字段**:
- `id`: 号卡 ID(主键,BIGINT)
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),唯一,用于对应运营商订单)
- `product_name`: 商品名称(VARCHAR(255))
- `carrier`: 运营商名称(VARCHAR(100),如 "中国移动"、"中国联通"、"中国电信")
- `carrier_product_id`: 运营商商品 ID(VARCHAR(100))
- `package_type`: 套餐类型(VARCHAR(50),如 "月套餐"、"流量包")
- `data_amount_mb`: 流量额度(BIGINT,MB 为单位,可选)
- `voice_minutes`: 语音分钟数(INT,可选)
- `sms_count`: 短信条数(INT,可选)
- `price`: 固定售价(DECIMAL(10,2),由运营商定价)
- `status`: 号卡状态(INT,1-上架 2-下架)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建号卡商品
- **WHEN** 平台创建号卡商品,虚拟商品编码为 "VC-CMCC-001",运营商为"中国移动",固定售价为 30.00 元
- **THEN** 系统创建号卡记录,`virtual_product_code` 为 "VC-CMCC-001",`carrier` 为 "中国移动",`price` 为 30.00,状态为 1(上架)
#### Scenario: 虚拟商品编码唯一性
- **WHEN** 平台创建号卡商品,虚拟商品编码为已存在的 "VC-CMCC-001"
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码已存在"
---
### Requirement: 号卡运营商订单回传
系统 SHALL 接收 Gateway 项目转换后的运营商订单回传,通过虚拟商品编码匹配号卡,创建订单和分佣记录。
**订单回传字段**:
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),唯一)
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),用于匹配号卡)
- `user_phone`: 用户手机号(VARCHAR(20))
- `amount`: 订单金额(DECIMAL(10,2))
- `order_time`: 订单时间(TIMESTAMP)
- `agent_id`: 代理 ID(BIGINT,可空,如果通过代理推广则有值)
- `carrier_order_data`: 运营商订单原始数据(JSONB)
**回传处理流程**:
1. Gateway 接收运营商订单,统一转换为 JSON 格式
2. Gateway 通过 HTTP POST 回传给 CMP 系统
3. CMP 系统根据 `virtual_product_code` 匹配号卡
4. CMP 系统创建订单记录(`order_type` 为 "number_card")
5. 如果有 `agent_id`,触发代理分佣流程
#### Scenario: 接收运营商订单回传
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为 "VC-CMCC-001",代理 ID 为 123,订单金额为 30.00 元
- **THEN** 系统创建订单记录,`order_type` 为 "number_card",`source_id` 为号卡 ID,`agent_id` 为 123,触发分佣计算
#### Scenario: 虚拟商品编码不存在
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为不存在的 "VC-UNKNOWN"
- **THEN** 系统拒绝创建订单,返回错误信息"虚拟商品编码不存在"并记录到日志
---
### Requirement: 号卡代理分销
系统 SHALL 支持号卡的代理分销,代理通过推广链接或卡板推广号卡给终端用户。
**分销规则**:
- 号卡由运营商定价,平台无权修改价格
- 代理通过推广链接或卡板获取用户激活
- 用户激活充值后,资金直接支付给运营商,不经过平台
- 运营商周期性结算总佣金给平台
- 平台根据代理分佣规则分配佣金给代理
**代理推广方式**:
- **推广链接**: 代理生成带有 `agent_id` 的推广链接,用户点击链接激活
- **卡板**: 代理线下分发印有二维码的卡板,用户扫码激活
#### Scenario: 代理生成推广链接
- **WHEN** 代理商(用户 ID 为 123)为号卡(ID 为 5001)生成推广链接
- **THEN** 系统生成带有 `agent_id=123``product_id=5001` 的推广链接,如 `https://example.com/activate?agent=123&product=5001`
#### Scenario: 用户通过代理链接激活
- **WHEN** 用户通过代理推广链接激活号卡并充值 30.00 元
- **THEN** 运营商接收用户支付,Gateway 回传订单时包含 `agent_id=123`,系统触发代理分佣流程
---
### Requirement: 号卡分佣处理
系统 SHALL 根据号卡分佣规则计算代理佣金,支持冻结和解冻流程。
**分佣规则**:
- 号卡分佣配置在代理分佣规则表(`commission_rules`)中
- 分佣类型:一次性分佣、长期分佣、组合分佣(参考 iot-agent-commission 规范)
- 号卡订单的分佣需要满足条件:激活(实名) + 达到充值金额 + 在网状态 + 三无校验
- 分佣记录创建时状态为"冻结",满足条件后变为"解冻中",审批通过后变为"已发放"
#### Scenario: 号卡订单触发分佣
- **WHEN** 运营商回传订单,代理 ID 为 123,订单金额为 30.00 元,该代理配置了一次性分佣 5.00 元
- **THEN** 系统创建分佣记录,金额为 5.00 元,状态为"冻结",等待满足解冻条件
#### Scenario: 号卡分佣解冻
- **WHEN** 号卡订单满足解冻条件(激活 + 充值 + 在网 + 三无校验)
- **THEN** 系统将分佣记录状态从"冻结"变更为"解冻中",创建分佣解冻审批记录
---
### Requirement: 号卡运营商结算
系统 SHALL 记录运营商周期性结算的佣金总额,用于财务对账和利润计算。
**结算字段**:
- `settlement_id`: 结算记录 ID(主键,BIGINT)
- `carrier`: 运营商名称(VARCHAR(100))
- `settlement_period`: 结算周期(VARCHAR(50),如 "2025-01")
- `total_commission`: 运营商结算的佣金总额(DECIMAL(18,2))
- `settlement_time`: 结算时间(TIMESTAMP)
- `status`: 结算状态(INT,1-待确认 2-已确认)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 记录运营商结算
- **WHEN** 运营商"中国移动"结算 2025 年 1 月的佣金总额 50000.00 元
- **THEN** 系统创建结算记录,`carrier` 为 "中国移动",`settlement_period` 为 "2025-01",`total_commission` 为 50000.00,状态为 1(待确认)
#### Scenario: 确认运营商结算
- **WHEN** 财务确认运营商结算记录(ID 为 1001)
- **THEN** 系统将结算记录状态从 1(待确认) 变更为 2(已确认)
---
### Requirement: 号卡数据校验
系统 SHALL 对号卡数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 虚拟商品编码(virtual_product_code):必填,长度 1-100 字符,唯一
- 商品名称(product_name):必填,长度 1-255 字符
- 运营商名称(carrier):必填,长度 1-100 字符
- 固定售价(price):必填,≥ 0,最多 2 位小数
- 状态(status):必填,枚举值 1(上架) | 2(下架)
#### Scenario: 创建号卡时虚拟商品编码为空
- **WHEN** 平台创建号卡,虚拟商品编码为空
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码不能为空"
#### Scenario: 创建号卡时固定售价为负数
- **WHEN** 平台创建号卡,固定售价为 -10.00
- **THEN** 系统拒绝创建,返回错误信息"固定售价必须 ≥ 0"

View File

@@ -0,0 +1,233 @@
## ADDED Requirements
### Requirement: 订单实体定义
系统 SHALL 定义订单(Order)实体,统一管理两种订单类型:套餐订单、号卡订单。
**核心概念**:
- **套餐订单**: 用户为 IoT 卡或设备购买套餐的订单,包括单卡套餐订单和设备级套餐订单
- **号卡订单**: 运营商回传的号卡订单,用户直接在上游平台下单,系统只接收订单状态更新
**实体字段**:
- `id`: 订单 ID(主键,BIGINT)
- `order_no`: 订单编号(VARCHAR(50),唯一)
- `order_type`: 订单类型(INT,1-套餐订单 2-号卡订单)
- `iot_card_id`: IoT 卡 ID(BIGINT,可空,单卡套餐订单时有值)
- `device_id`: 设备 ID(BIGINT,可空,设备级套餐订单时有值)
- `number_card_id`: 号卡 ID(BIGINT,可空,号卡订单时有值)
- `package_id`: 套餐 ID(BIGINT,可空,仅当 order_type 为 1 时有值)
- `user_id`: 用户 ID(BIGINT,购买用户)
- `agent_id`: 代理 ID(BIGINT,可空,通过代理购买时有值)
- `amount`: 订单金额(DECIMAL(10,2),元)
- `payment_method`: 支付方式(VARCHAR(20),"wallet"-钱包 | "online"-在线支付 | "carrier"-运营商直付)
- `status`: 订单状态(INT,1-待支付 2-已支付 3-已完成 4-已取消 5-已退款)
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),可空,仅号卡订单有值)
- `carrier_order_data`: 运营商订单原始数据(JSONB,可空)
- `paid_at`: 支付时间(TIMESTAMP,可空)
- `completed_at`: 完成时间(TIMESTAMP,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
**订单类型说明**:
- **单卡套餐订单**: `order_type` 为 1,`iot_card_id` 有值,`device_id` 为 NULL
- **设备级套餐订单**: `order_type` 为 1,`device_id` 有值,`iot_card_id` 为 NULL
- **号卡订单**: `order_type` 为 2,`number_card_id` 有值,`iot_card_id``device_id` 为 NULL
#### Scenario: 创建单卡套餐购买订单
- **WHEN** 用户(ID 为 2001)为 IoT 卡(ID 为 1001)购买套餐(ID 为 3001),金额为 30.00 元
- **THEN** 系统创建订单记录,`order_type` 为 1,`iot_card_id` 为 1001,`device_id` 为 NULL,`package_id` 为 3001,`user_id` 为 2001,`amount` 为 30.00,状态为 1(待支付)
#### Scenario: 创建设备级套餐购买订单
- **WHEN** 用户(ID 为 2001)为设备(ID 为 5001,绑定 3 张 IoT 卡)购买套餐(ID 为 3002),金额为 399.00 元
- **THEN** 系统创建订单记录,`order_type` 为 1,`device_id` 为 5001,`iot_card_id` 为 NULL,`package_id` 为 3002,`user_id` 为 2001,`amount` 为 399.00,状态为 1(待支付)
#### Scenario: 创建号卡订单(运营商回传)
- **WHEN** Gateway 回传运营商订单,虚拟商品编码对应号卡 ID 为 6001,代理 ID 为 123,订单金额为 30.00 元
- **THEN** 系统创建订单记录,`order_type` 为 2,`number_card_id` 为 6001,`iot_card_id` 为 NULL,`device_id` 为 NULL,`agent_id` 为 123,`amount` 为 30.00,`payment_method` 为 "carrier",状态为 2(已支付)
---
### Requirement: 订单状态流转
系统 SHALL 管理订单的状态流转,确保状态变更符合业务规则。
**状态定义**:
- **1-待支付**: 订单已创建,等待用户支付
- **2-已支付**: 用户已支付,等待系统处理
- **3-已完成**: 订单已完成(激活/发货等)
- **4-已取消**: 订单已取消
- **5-已退款**: 订单已退款
**状态流转规则**:
- 待支付(1) → 已支付(2): 用户完成支付
- 待支付(1) → 已取消(4): 用户取消订单或订单超时
- 已支付(2) → 已完成(3): 系统完成订单处理(激活/发货)
- 已支付(2) → 已退款(5): 用户申请退款且审核通过
- 已完成(3) → 已退款(5): 用户申请退款且审核通过(特殊情况)
#### Scenario: 用户支付订单
- **WHEN** 用户支付待支付订单(ID 为 10001),支付金额为 30.00 元
- **THEN** 系统将订单状态从 1(待支付) 变更为 2(已支付),`paid_at` 记录支付时间
#### Scenario: 单卡套餐订单完成
- **WHEN** 系统处理完单卡套餐订单(ID 为 10001),激活 IoT 卡并分配套餐
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
#### Scenario: 设备级套餐订单完成
- **WHEN** 系统处理完设备级套餐订单(ID 为 10002),为设备绑定的所有 IoT 卡分配套餐
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
---
### Requirement: 订单支付方式
系统 SHALL 支持三种支付方式:钱包支付、在线支付、运营商直付。
**支付方式**:
- **钱包支付(wallet)**: 从用户钱包余额扣款
- **在线支付(online)**: 通过第三方支付(微信/支付宝等)
- **运营商直付(carrier)**: 用户直接支付给运营商(仅号卡订单)
**支付规则**:
- 一次性分佣订单必须使用钱包支付
- 套餐购买订单可以使用钱包或在线支付
- 号卡订单必须使用运营商直付
#### Scenario: 钱包支付订单
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 50.00 元
- **THEN** 系统从钱包扣除 30.00 元,订单状态变更为 2(已支付),`payment_method` 为 "wallet"
#### Scenario: 钱包余额不足
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 20.00 元
- **THEN** 系统拒绝支付,返回错误信息"钱包余额不足"
#### Scenario: 一次性分佣订单强制钱包支付
- **WHEN** 用户购买配置了一次性分佣的套餐,尝试使用在线支付
- **THEN** 系统拒绝支付,返回错误信息"一次性分佣订单必须使用钱包支付"
---
### Requirement: 订单分佣触发
系统 SHALL 在订单完成时触发分佣计算,根据代理分佣规则创建分佣记录。
**触发条件**:
- 订单状态变更为 3(已完成)
- 订单有 `agent_id`(通过代理销售)
- 代理配置了分佣规则
**分佣计算规则**:
- **单卡套餐订单**: 根据 IoT 卡关联的代理分佣规则计算分佣
- **设备级套餐订单**: 分佣只计算一次(不按设备绑定的 IoT 卡数量倍增)
- **号卡订单**: 下单即冻结分佣,次月通过 Excel 导入解冻
#### Scenario: 单卡套餐购买订单触发分佣
- **WHEN** 代理(ID 为 123)的单卡套餐订单(ID 为 10001)完成,订单金额为 30.00 元,代理配置了 5.00 元一次性分佣
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结)
#### Scenario: 设备级套餐订单触发分佣(只计算一次)
- **WHEN** 代理(ID 为 123)的设备级套餐订单(ID 为 10002)完成,设备绑定 3 张 IoT 卡,订单金额为 399.00 元,代理配置了 100.00 元长期分佣
- **THEN** 系统创建一条分佣记录,`agent_id` 为 123,`order_id` 为 10002,`amount` 为 100.00,状态为 1(冻结),不是 3 × 100.00
#### Scenario: 号卡订单触发分佣
- **WHEN** 代理(ID 为 123)的号卡订单(ID 为 10003)创建,订单金额为 30.00 元,代理配置了长期分佣
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10003,状态为 1(冻结),等待次月通过 Excel 导入解冻
---
### Requirement: 订单查询和筛选
系统 SHALL 支持多维度查询和筛选订单。
**查询条件**:
- 订单编号(精确匹配)
- 订单类型(1-套餐订单 2-号卡订单)
- 订单状态(单选或多选)
- IoT 卡 ID(精确匹配)
- 设备 ID(精确匹配)
- 号卡 ID(精确匹配)
- 用户 ID(精确匹配)
- 代理 ID(精确匹配)
- 支付方式(单选或多选)
- 创建时间范围(开始时间 - 结束时间)
- 支付时间范围(开始时间 - 结束时间)
- 完成时间范围(开始时间 - 结束时间)
**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数
#### Scenario: 查询用户的所有订单
- **WHEN** 用户(ID 为 2001)查询自己的所有订单
- **THEN** 系统返回 `user_id` 为 2001 的所有订单列表,按创建时间倒序排列
#### Scenario: 查询代理的订单
- **WHEN** 代理(ID 为 123)查询自己的订单,筛选已完成的套餐订单
- **THEN** 系统返回 `agent_id` 为 123 且 `order_type` 为 1 且 `status` 为 3(已完成) 的订单列表
#### Scenario: 查询 IoT 卡的订单历史
- **WHEN** 运营人员查询 IoT 卡(ID 为 1001)的所有订单
- **THEN** 系统返回 `iot_card_id` 为 1001 的所有订单列表,包含套餐购买记录
#### Scenario: 查询设备的订单历史
- **WHEN** 运营人员查询设备(ID 为 5001)的所有订单
- **THEN** 系统返回 `device_id` 为 5001 的所有设备级套餐订单列表
---
### Requirement: 订单数据校验
系统 SHALL 对订单数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 订单编号(order_no):必填,长度 1-50 字符,唯一
- 订单类型(order_type):必填,枚举值 1(套餐订单) | 2(号卡订单)
- IoT 卡 ID(iot_card_id):套餐订单时 iot_card_id 和 device_id 二选一
- 设备 ID(device_id):套餐订单时 iot_card_id 和 device_id 二选一
- 号卡 ID(number_card_id):号卡订单时必填
- 套餐 ID(package_id):套餐订单时必填
- 用户 ID(user_id):必填,≥ 1
- 订单金额(amount):必填,≥ 0,最多 2 位小数
- 支付方式(payment_method):必填,枚举值 "wallet" | "online" | "carrier"
- 状态(status):必填,枚举值 1-5
#### Scenario: 创建订单时金额为负数
- **WHEN** 创建订单,金额为 -10.00
- **THEN** 系统拒绝创建,返回错误信息"订单金额必须 ≥ 0"
#### Scenario: 创建订单时订单编号重复
- **WHEN** 创建订单,订单编号为已存在的 "ORD-2025-001"
- **THEN** 系统拒绝创建,返回错误信息"订单编号已存在"
#### Scenario: 创建套餐订单时未关联 IoT 卡或设备
- **WHEN** 创建套餐订单,`iot_card_id``device_id` 都为 NULL
- **THEN** 系统拒绝创建,返回错误信息"套餐订单必须关联 IoT 卡或设备"
#### Scenario: 创建套餐订单时同时关联 IoT 卡和设备
- **WHEN** 创建套餐订单,`iot_card_id` 为 1001,`device_id` 为 5001
- **THEN** 系统拒绝创建,返回错误信息"套餐订单不能同时关联 IoT 卡和设备"
#### Scenario: 创建号卡订单时未关联号卡
- **WHEN** 创建号卡订单,`number_card_id` 为 NULL
- **THEN** 系统拒绝创建,返回错误信息"号卡订单必须关联号卡"

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"

View File

@@ -0,0 +1,441 @@
# IoT SIM 管理 - 数据模型与数据库表结构实现任务
本任务清单聚焦于 IoT SIM 管理模块的数据模型定义和数据库表结构实现,不包含业务逻辑代码。
---
## 1. 数据库迁移脚本
### 1.1 核心业务表
- [x] 1.1.1 创建迁移脚本文件:`migrations/YYYYMMDDHHMMSS_create_iot_sim_management_tables.up.sql``*.down.sql`
- [x] 1.1.2 创建运营商表(carriers)及其索引
- 主键索引
- `carrier_code` 唯一索引
- 初始数据:中国移动(CMCC)、中国联通(CUCC)、中国电信(CTCC)
- [x] 1.1.3 创建 IoT 卡表(iot_cards)及其索引
- 主键索引
- `iccid` 唯一索引
- `card_category` 字段:枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
- `carrier_id` 索引(关联运营商表)
- `owner_type` + `owner_id` + `status` 组合索引
- `batch_no` 索引
- `activated_at` 索引
- `card_category` 索引(用于区分普通卡和行业卡)
- `enable_polling` + `activation_status` + `last_data_check_at` 组合索引(卡流量轮询查询优化)
- `enable_polling` + `real_name_status` + `last_real_name_check_at` 组合索引(实名轮询查询优化)
- [x] 1.1.4 创建设备表(devices)及其索引
- 主键索引
- `device_no` 唯一索引
- `owner_type` + `owner_id` + `status` 组合索引
- [x] 1.1.5 创建号卡表(number_cards)及其索引
- 主键索引
- `virtual_product_code` 唯一索引
- `agent_id` + `status` 组合索引
- [x] 1.1.6 创建套餐系列表(package_series)及其索引
- 主键索引
- `series_code` 唯一索引
- [x] 1.1.7 创建套餐表(packages)及其索引
- 主键索引
- `package_code` 唯一索引
- `series_id` + `status` 组合索引
- [x] 1.1.8 创建代理套餐分配表(agent_package_allocations)及其索引
- 主键索引
- `agent_id` + `package_id` 唯一组合索引
- [x] 1.1.9 创建设备-IoT卡绑定关系表(device_sim_bindings)及其索引
- 主键索引
- `device_id` + `bind_status` 组合索引
- `iot_card_id` + `bind_status` 组合索引
- `iot_card_id` 部分唯一索引(WHERE bind_status = 1)
- [x] 1.1.10 创建订单表(orders)及其索引
- 主键索引
- `order_no` 唯一索引
- `user_id` + `status` 组合索引
- `agent_id` + `status` 组合索引
- `iot_card_id` 索引
- `device_id` 索引
- `number_card_id` 索引
### 1.2 套餐和轮询相关表
- [x] 1.2.1 创建套餐使用情况表(package_usages)及其索引
- 主键索引
- `order_id` 索引
- `package_id` 索引
- `iot_card_id` 索引
- `device_id` 索引
- `status` + `expires_at` + `last_package_check_at` 组合索引(套餐流量检查优化)
- [x] 1.2.2 创建轮询配置表(polling_configs)及其索引
- 主键索引
- `config_name` 唯一索引
- `status` + `card_condition` + `carrier_id` + `priority` 组合索引(配置匹配优化)
- [x] 1.2.3 创建流量使用记录表(data_usage_records)及其索引
- 主键索引
- `iot_card_id` + `check_time` 组合索引(按卡和时间查询)
- `check_time` 索引(按时间范围查询)
- 注意:此表数据量会快速增长,建议定期清理 90 天前的记录或使用分区表
### 1.3 分佣相关表
- [x] 1.3.1 创建代理层级关系表(agent_hierarchies)及其索引
- 主键索引
- `agent_id` 唯一索引
- `parent_agent_id` 索引
- [x] 1.3.2 创建分佣规则表(commission_rules)及其索引
- 主键索引
- `agent_id` + `business_type` + `card_type` 组合索引
- [x] 1.3.3 创建阶梯分佣配置表(commission_ladder)及其索引
- 主键索引
- `rule_id` 索引
- [x] 1.3.4 创建组合分佣条件表(commission_combined_conditions)及其索引
- 主键索引
- `rule_id` 唯一索引
- [x] 1.3.5 创建分佣记录表(commission_records)及其索引
- 主键索引
- `agent_id` + `status` 组合索引
- `order_id` 索引
- `rule_id` 索引
- [x] 1.3.6 创建分佣审批表(commission_approvals)及其索引
- 主键索引
- `commission_record_id` 索引
- `status` 索引
- [x] 1.3.7 创建分佣模板表(commission_templates)及其索引
- 主键索引
- `template_name` 唯一索引
- [x] 1.3.8 创建号卡运营商结算表(carrier_settlements)及其索引
- 主键索引
- `commission_record_id` 唯一索引
- `agent_id` + `status` 组合索引
### 1.4 财务管理表
- [x] 1.4.1 创建佣金提现申请表(commission_withdrawal_requests)及其索引
- 主键索引
- `agent_id` + `status` 组合索引
- `created_at` 索引
- [x] 1.4.2 创建佣金提现设置表(commission_withdrawal_settings)及其索引
- 主键索引
- `status` 索引
- [x] 1.4.3 创建收款商户设置表(payment_merchant_settings)及其索引
- 主键索引
- `user_id` + `is_default` 组合索引
- `merchant_type` + `status` 组合索引
### 1.5 系统管理表
- [x] 1.5.1 创建开发能力配置表(dev_capability_configs)及其索引
- 主键索引
- `app_id` 唯一索引
- `user_id` + `status` 组合索引
- [x] 1.5.2 创建换卡申请表(card_replacement_requests)及其索引
- 主键索引
- `user_id` + `status` 组合索引
- `old_iccid` 索引
- `new_iccid` 索引
### 1.6 迁移脚本验证
- [x] 1.6.1 编写迁移脚本的 down 部分(删除所有表)
- [x] 1.6.2 在本地测试数据库执行 up 迁移
- [x] 1.6.3 验证所有表和索引创建成功
- [x] 1.6.4 执行 down 迁移验证回滚成功
- [x] 1.6.5 编写迁移脚本的 README 说明(执行步骤、注意事项)
---
## 2. GORM 模型定义
### 2.1 目录结构
- [x] 2.1.1 创建 `internal/iot/model` 目录
- [x] 2.1.2 创建模型文件结构:
- `carrier.go` - 运营商模型
- `iot_card.go` - IoT 卡模型
- `device.go` - 设备模型
- `number_card.go` - 号卡模型
- `package.go` - 套餐、套餐系列、套餐使用情况模型
- `order.go` - 订单模型
- `polling.go` - 轮询配置模型
- `data_usage.go` - 流量使用记录模型
- `commission.go` - 分佣相关模型
- `financial.go` - 财务管理模型
- `system.go` - 系统管理模型
### 2.2 核心业务模型
- [x] 2.2.1 定义运营商(Carrier)模型
- 字段包括id, carrier_code, carrier_name, description, status, created_at, updated_at
- 初始数据:中国移动(CMCC)、中国联通(CUCC)、中国电信(CTCC)
- [x] 2.2.2 定义 IoT 卡(IotCard)模型
- 所有字段必须显式指定 `gorm:"column:字段名"`
- 添加中文字段注释(comment 标签)
- 字段包括id, iccid, card_type, card_category, carrier_id, imsi, msisdn, batch_no, supplier, cost_price, distribute_price, status, owner_type, owner_id, activated_at, activation_status, real_name_status, network_status, data_usage_mb, enable_polling, last_data_check_at, last_real_name_check_at, last_sync_time, created_at, updated_at
- **关键调整**
- `card_category` 字段:枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
- `carrier_id` 关联运营商表(替代原来的 carrier 字符串字段)
- `enable_polling` 控制是否参与轮询(默认 true
- `last_data_check_at` 卡流量检查时间
- `last_real_name_check_at` 实名检查时间
- 行业卡可以在 `real_name_status` 为 0 的情况下激活使用
- [x] 2.2.3 定义设备(Device)模型
- 字段包括id, device_no, device_name, device_model, device_type, max_sim_slots, manufacturer, batch_no, owner_type, owner_id, status, activated_at, device_username, device_password_encrypted, device_api_endpoint, created_at, updated_at
- [x] 2.2.4 定义号卡(NumberCard)模型
- 字段包括id, virtual_product_code, card_name, card_type, carrier, data_amount_mb, price, agent_id, status, created_at, updated_at
- [x] 2.2.5 定义套餐系列(PackageSeries)模型
- 字段包括id, series_code, series_name, description, status, created_at, updated_at
- [x] 2.2.6 定义套餐(Package)模型
- 字段包括id, package_code, package_name, series_id, package_type, duration_months, data_type, real_data_mb, virtual_data_mb, data_amount_mb, price, status, created_at, updated_at
- [x] 2.2.7 定义代理套餐分配(AgentPackageAllocation)模型
- 字段包括id, agent_id, package_id, cost_price, retail_price, status, created_at, updated_at
- [x] 2.2.8 定义设备-IoT卡绑定关系(DeviceSimBinding)模型
- 字段包括id, device_id, iot_card_id, slot_number, bind_status, bound_at, unbound_at, created_at, updated_at
- [x] 2.2.9 定义订单(Order)模型
- 字段包括id, order_no, order_type, iot_card_id, device_id, number_card_id, package_id, user_id, agent_id, amount, payment_method, status, carrier_order_id, carrier_order_data, paid_at, completed_at, created_at, updated_at
### 2.3 套餐和轮询相关模型
- [x] 2.3.1 定义套餐使用情况(PackageUsage)模型
- 字段包括id, order_id, package_id, usage_type, iot_card_id, device_id, data_limit_mb, data_usage_mb, real_data_usage_mb, virtual_data_usage_mb, activated_at, expires_at, status, last_package_check_at, created_at, updated_at
- **业务逻辑**
- `usage_type` = "single_card" 时,`iot_card_id` 有值,`device_id` 为 NULL
- `usage_type` = "device" 时,`device_id` 有值,`iot_card_id` 为 NULL
- `data_usage_mb` 通过汇总卡的流量计算(单卡套餐直接读卡流量,设备级套餐汇总所有卡流量)
- [x] 2.3.2 定义轮询配置(PollingConfig)模型
- 字段包括id, config_name, description, card_condition, carrier_id, real_name_check_enabled, real_name_check_interval, card_data_check_enabled, card_data_check_interval, package_check_enabled, package_check_interval, priority, status, created_at, updated_at
- **配置说明**
- `carrier_id` 为 NULL 表示匹配所有运营商
- `priority` 数字越小优先级越高
- 支持独立配置实名检查、卡流量检查、套餐流量检查
- [x] 2.3.3 定义流量使用记录(DataUsageRecord)模型
- 字段包括id, iot_card_id, data_usage_mb, data_increase_mb, check_time, source, created_at
- **业务逻辑**
- Worker 每次轮询卡流量后插入一条记录
- `data_increase_mb` = 本次流量 - 上次流量
- `source` 数据来源polling-轮询 manual-手动 gateway-回调)
### 2.4 分佣相关模型
- [x] 2.4.1 定义代理层级关系(AgentHierarchy)模型
- 字段包括id, agent_id, parent_agent_id, agent_path, level, created_at, updated_at
- [x] 2.4.2 定义分佣规则(CommissionRule)模型
- 字段包括id, agent_id, business_type, card_type, commission_type, commission_mode, commission_value, unfreeze_days, min_activation_for_unfreeze, approval_type, status, created_at, updated_at
- [x] 2.4.3 定义阶梯分佣配置(CommissionLadder)模型
- 字段包括id, rule_id, ladder_type, threshold_value, commission_mode, commission_value, created_at, updated_at
- [x] 2.4.4 定义组合分佣条件(CommissionCombinedCondition)模型
- 字段包括id, rule_id, one_time_commission_mode, one_time_commission_value, long_term_commission_mode, long_term_commission_value, long_term_unfreeze_days, long_term_min_activation, created_at, updated_at
- [x] 2.4.5 定义分佣记录(CommissionRecord)模型
- 字段包括id, agent_id, order_id, rule_id, commission_type, amount, status, unfrozen_at, released_at, created_at, updated_at
- [x] 2.4.6 定义分佣审批(CommissionApproval)模型
- 字段包括id, commission_record_id, approver_id, status, reason, created_at, updated_at
- [x] 2.4.7 定义分佣模板(CommissionTemplate)模型
- 字段包括id, template_name, business_type, card_type, commission_type, commission_mode, commission_value, unfreeze_days, min_activation_for_unfreeze, approval_type, created_at, updated_at
- [x] 2.4.8 定义号卡运营商结算(CarrierSettlement)模型
- 字段包括id, commission_record_id, agent_id, settlement_month, settlement_amount, status, created_at, updated_at
### 2.5 财务管理模型
- [x] 2.5.1 定义佣金提现申请(CommissionWithdrawalRequest)模型
- 字段包括id, agent_id, amount, withdrawal_method, merchant_id, account_info, status, approved_by, approved_at, rejected_reason, paid_at, created_at, updated_at
- [x] 2.5.2 定义佣金提现设置(CommissionWithdrawalSetting)模型
- 字段包括id, min_withdrawal_amount, max_withdrawal_amount, daily_withdrawal_limit, fee_rate, status, created_at, updated_at
- [x] 2.5.3 定义收款商户设置(PaymentMerchantSetting)模型
- 字段包括id, user_id, merchant_type, account_name, account_number, bank_name, is_verified, is_default, status, created_at, updated_at
### 2.6 系统管理模型
- [x] 2.6.1 定义开发能力配置(DevCapabilityConfig)模型
- 字段包括id, user_id, app_id, app_secret, callback_url, status, created_at, updated_at
- [x] 2.6.2 定义换卡申请(CardReplacementRequest)模型
- 字段包括id, user_id, old_iccid, new_iccid, reason, status, processed_by, processed_at, created_at, updated_at
---
## 3. 常量定义
### 3.1 核心业务常量
- [x] 3.1.1 在 `pkg/constants/iot.go` 中定义以下常量:
- IoT 卡状态IotCardStatusInStock(1), IotCardStatusDistributed(2), IotCardStatusActivated(3), IotCardStatusSuspended(4)
- 设备状态DeviceStatusInStock(1), DeviceStatusDistributed(2), DeviceStatusActivated(3), DeviceStatusSuspended(4)
- 号卡状态NumberCardStatusOnSale(1), NumberCardStatusOffSale(2)
- IoT 卡激活状态ActivationStatusInactive(0), ActivationStatusActive(1)
- IoT 卡实名状态RealNameStatusNotVerified(0), RealNameStatusVerified(1)
- IoT 卡网络状态NetworkStatusOffline(0), NetworkStatusOnline(1)
- 套餐流量类型DataTypeReal("real"), DataTypeVirtual("virtual")
- 套餐类型PackageTypeFormal("formal"), PackageTypeAddon("addon")
- 订单类型OrderTypePackage(1), OrderTypeNumberCard(2)
- 订单状态OrderStatusPending(1), OrderStatusPaid(2), OrderStatusCompleted(3), OrderStatusCancelled(4), OrderStatusRefunded(5)
- 支付方式PaymentMethodWallet("wallet"), PaymentMethodOnline("online"), PaymentMethodCarrier("carrier")
- 所有者类型OwnerTypePlatform("platform"), OwnerTypeAgent("agent"), OwnerTypeUser("user"), OwnerTypeDevice("device")
- 绑定状态BindStatusBound(1), BindStatusUnbound(2)
### 3.2 套餐和轮询相关常量
- [x] 3.2.1 定义套餐使用类型常量:
- PackageUsageTypeSingleCard("single_card") - 单卡套餐
- PackageUsageTypeDevice("device") - 设备级套餐
- [x] 3.2.2 定义套餐使用状态常量:
- PackageUsageStatusActive(1) - 生效中
- PackageUsageStatusExhausted(2) - 已用完
- PackageUsageStatusExpired(3) - 已过期
- [x] 3.2.3 定义轮询配置卡条件常量:
- CardConditionNotRealName("not_real_name") - 未实名
- CardConditionRealName("real_name") - 已实名
- CardConditionActivated("activated") - 已激活
- CardConditionSuspended("suspended") - 已停用
- [x] 3.2.4 定义流量使用记录来源常量:
- DataUsageSourcePolling("polling") - 轮询
- DataUsageSourceManual("manual") - 手动
- DataUsageSourceGateway("gateway") - Gateway 回调
### 3.3 分佣相关常量
- [x] 3.3.1 定义分佣相关常量:
- 分佣类型CommissionTypeOneTime("one_time"), CommissionTypeLongTerm("long_term"), CommissionTypeCombined("combined")
- 分佣模式CommissionModeFixed("fixed"), CommissionModePercent("percent")
- 分佣状态CommissionStatusFrozen(1), CommissionStatusUnfreezing(2), CommissionStatusReleased(3), CommissionStatusInvalid(4)
- 阶梯类型LadderTypeActivation("activation"), LadderTypePickup("pickup"), LadderTypeDeposit("deposit")
- 卡类型CardTypeNumberCard("number_card"), CardTypeIotCard("iot_card")
- 审批类型ApprovalTypeAuto("auto"), ApprovalTypeManual("manual")
- 审批状态ApprovalStatusPending(1), ApprovalStatusApproved(2), ApprovalStatusRejected(3)
### 3.4 财务管理常量
- [x] 3.4.1 定义财务相关常量:
- 提现状态WithdrawalStatusPending(1), WithdrawalStatusApproved(2), WithdrawalStatusRejected(3), WithdrawalStatusPaid(4)
- 提现方式WithdrawalMethodAlipay("alipay"), WithdrawalMethodWechat("wechat"), WithdrawalMethodBank("bank")
- 商户类型MerchantTypeAlipay("alipay"), MerchantTypeWechat("wechat"), MerchantTypeBank("bank")
### 3.5 系统管理常量
- [x] 3.5.1 定义系统管理常量:
- 换卡申请状态ReplacementStatusPending(1), ReplacementStatusApproved(2), ReplacementStatusRejected(3), ReplacementStatusCompleted(4)
- 开发能力配置状态DevCapabilityStatusEnabled(1), DevCapabilityStatusDisabled(2)
---
## 4. 模型和表结构文档
### 4.1 代码注释
- [x] 4.1.1 为所有 GORM 模型添加中文结构体注释(描述表的业务用途)
- [x] 4.1.2 为所有模型字段添加清晰的中文注释
- [x] 4.1.3 为所有常量添加中文注释(说明枚举值含义)
- [x] 4.1.4 在迁移脚本中为所有表和字段添加 SQL COMMENT
### 4.2 数据库设计文档
- [x] 4.2.1 在 `docs/iot-sim-management/` 目录下创建 `数据库设计.md`
- [x] 4.2.2 使用 Markdown 表格描述所有表结构(字段名、类型、约束、说明)
- [x] 4.2.3 使用 dbdiagram.io 或 draw.io 创建数据库 ERD 图
- [x] 4.2.4 导出 ERD 图并保存到 `docs/iot-sim-management/erd.png`
- [x] 4.2.5 在 `数据库设计.md` 中嵌入 ERD 图
### 4.3 模型使用说明
- [x] 4.3.1 创建 `docs/iot-sim-management/模型说明.md`
- [x] 4.3.2 说明每个模型的用途和关键字段含义
- [x] 4.3.3 说明表之间的关联关系(虽然没有外键,但逻辑关联需要说明)
- [x] 4.3.4 说明关键枚举字段的取值和含义
- [x] 4.3.5 说明特殊设计决策(如无外键约束、owner_type/owner_id 模式等)
### 4.4 轮询机制说明文档
- [x] 4.4.1 创建 `docs/iot-sim-management/轮询机制说明.md`
- [x] 4.4.2 说明三个独立轮询流程:实名状态轮询、卡流量轮询、套餐流量检查
- [x] 4.4.3 说明轮询配置的匹配规则和优先级
- [x] 4.4.4 说明 `enable_polling` 字段的使用场景
- [x] 4.4.5 说明流量使用记录表的数据保留策略
### 4.5 项目文档更新
- [x] 4.5.1 更新 `README.md`,添加 IoT SIM 管理模块描述
- [x] 4.5.2 在 README 中添加数据库设计文档链接
- [x] 4.5.3 在 README 中添加模型说明文档链接
- [x] 4.5.4 在 README 中添加轮询机制说明文档链接
---
## 5. 数据迁移验证
### 5.1 本地验证
- [x] 5.1.1 在本地 PostgreSQL 测试数据库执行迁移脚本 up
- [x] 5.1.2 使用 `\dt``\d table_name` 验证所有表创建成功
- [x] 5.1.3 验证所有字段类型、默认值、NOT NULL 约束正确
- [x] 5.1.4 使用 `\di` 验证所有索引创建成功
- [x] 5.1.5 验证唯一索引和组合索引的正确性
### 5.2 数据完整性验证
- [x] 5.2.1 插入测试数据验证唯一索引生效(尝试插入重复 ICCID 应失败)
- [x] 5.2.2 插入测试数据验证 NOT NULL 约束生效
- [x] 5.2.3 插入测试数据验证 CHECK 约束生效(如金额 >= 0)
- [x] 5.2.4 查询测试数据验证组合索引生效(使用 EXPLAIN ANALYZE)
- [x] 5.2.5 验证运营商初始数据插入成功
### 5.3 回滚验证
- [x] 5.3.1 执行迁移脚本 down
- [x] 5.3.2 验证所有表和索引删除成功
- [x] 5.3.3 重新执行 up 验证迁移脚本可重复执行
---
## 6. 代码质量检查
### 6.1 代码格式化
- [x] 6.1.1 使用 `go fmt` 格式化所有模型代码
- [x] 6.1.2 使用 `goimports` 整理导入语句
- [x] 6.1.3 使用 `golangci-lint` 检查代码质量
### 6.2 命名规范检查
- [x] 6.2.1 验证所有 Go 字段名遵循驼峰命名法(PascalCase)
- [x] 6.2.2 验证所有数据库字段名遵循下划线命名法(snake_case)
- [x] 6.2.3 验证所有常量命名遵循 Go 规范(如 IotCardStatusInStock)
- [x] 6.2.4 验证所有模型文件名遵循 Go 规范(snake_case如 iot_card.go)
### 6.3 GORM 标签检查
- [x] 6.3.1 验证所有字段都有 `gorm:"column:字段名"` 标签
- [x] 6.3.2 验证所有字段都有 `json:"字段名"` 标签
- [x] 6.3.3 验证所有字段的 `comment` 标签包含中文说明
- [x] 6.3.4 验证字符串字段的 `type` 标签指定了长度(如 `type:varchar(100)`)
- [x] 6.3.5 验证数值字段的 `type` 标签指定了精度(如 `type:decimal(10,2)`)
---
## 完成标准
本阶段任务完成后,应该具备:
1. ✅ 完整的数据库迁移脚本(up 和 down)
2. ✅ 完整的 GORM 模型定义(所有表对应的 Go 结构体)
3. ✅ 完整的常量定义(所有枚举值)
4. ✅ 完整的数据库设计文档(ERD 图 + 表结构说明)
5. ✅ 完整的模型使用说明文档
6. ✅ 完整的轮询机制说明文档
7. ✅ 数据库迁移在本地测试通过
8. ✅ 所有代码遵循项目开发规范(命名、注释、格式)
**不包含**业务逻辑实现、API 接口、Service 层、Store 层、DTO、错误码、Redis Key 等。
---
## 关键设计说明
### 运营商表 (carriers)
- 存储运营商基础信息(中国移动、中国联通、中国电信)
- IoT 卡表通过 `carrier_id` 关联运营商表
### IoT 卡表 (iot_cards)
- `card_category` 字段:枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
- **普通卡**: 需要实名认证才能激活使用
- **行业卡**: 不需要实名认证,可以在 `real_name_status` 为 0 的情况下激活使用
- `carrier_id` 关联运营商表(替代原来的 carrier 字符串字段)
- `enable_polling` 控制是否参与轮询(默认 true可手动禁用
- `last_data_check_at` 记录卡流量检查时间
- `last_real_name_check_at` 记录实名检查时间
### 套餐使用情况表 (package_usages)
- 核心业务表,跟踪套餐的激活、使用、过期情况
- 单卡套餐:`usage_type` = "single_card"`iot_card_id` 有值
- 设备级套餐:`usage_type` = "device"`device_id` 有值
- `data_usage_mb` 通过汇总卡的流量计算(不是实时轮询,而是定期统计)
### 轮询配置表 (polling_configs)
- 支持梯度配置(未实名卡、实名卡使用不同的轮询策略)
- 支持按运营商配置不同的轮询频率
- 独立配置三种轮询:实名检查、卡流量检查、套餐流量检查
- `priority` 数字越小优先级越高
### 流量使用记录表 (data_usage_records)
- 记录每次卡流量检查的结果
- 支持按卡、按时间范围查询流量历史
- 数据量会快速增长,建议定期清理 90 天前的记录或使用分区表
### 轮询逻辑(概念说明)
1. **卡流量轮询**:只轮询有生效套餐的卡,`enable_polling = true`
2. **套餐流量检查**:定期汇总卡的流量,判断套餐是否超额
3. **实名状态轮询**:定期检查卡的实名状态,实名后降低轮询频率
- **行业卡特殊处理**: 行业卡的实名状态检查应该被禁用或设置为低优先级
4. **三个流程独立运行**:互不干扰,通过轮询配置表动态控制
### 分佣解冻逻辑(概念说明)
1. **一次性分佣**: 普通卡需要实名认证后才能解冻;行业卡无需实名认证,只需满足激活和充值条件
2. **长期分佣**: 普通卡需要实名认证后才能开始长期分佣;行业卡无需实名认证,满足其他条件即可
3. **组合分佣**: 行业卡的时间点条件从激活时开始计算(不是实名时)

View File

@@ -0,0 +1,346 @@
# IoT Agent Commission Management
## Purpose
Manage commission rules and records for IoT agents, supporting three commission types (one-time, long-term, combined), ladder commissions, commission freeze/unfreeze logic, approval workflows, and multi-level agent commission distribution.
This capability supports:
- Agent hierarchy (tree structure) management
- Three commission types: one-time, long-term, combined
- Commission rule configuration (series-based for one-time, package-based for long-term)
- Combined commission with OR-condition unfreezing (time point OR package cycle)
- Ladder commission based on activation/pickup/deposit thresholds
- Commission record lifecycle (frozen → unfreezing → released → invalid)
- Commission unfreeze conditions (activation + real-name + recharge for normal cards; no real-name required for industry cards)
- Commission approval workflow (auto or manual)
- Multi-level agent commission distribution
## Requirements
## ADDED Requirements
### Requirement: 代理树形关系
系统 SHALL 管理代理的树形层级关系,每个代理只有一个上级代理。
**agent_hierarchies 表**:
- `id`: 代理关系 ID(主键,BIGINT)
- `agent_id`: 代理用户 ID(BIGINT,唯一)
- `parent_agent_id`: 上级代理用户 ID(BIGINT,可空,NULL 表示顶级代理)
- `level`: 代理层级(INT,1-顶级代理 2-二级代理 ...)
- `path`: 代理路径(VARCHAR(500),如 "1/5/12",用于快速获取整个代理链)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建顶级代理
- **WHEN** 平台创建顶级代理(用户 ID 为 101)
- **THEN** 系统创建代理关系记录,`agent_id` 为 101,`parent_agent_id` 为 NULL,`level` 为 1,`path` 为 "101"
#### Scenario: 创建下级代理
- **WHEN** 顶级代理(ID 为 101)创建下级代理(用户 ID 为 102)
- **THEN** 系统创建代理关系记录,`agent_id` 为 102,`parent_agent_id` 为 101,`level` 为 2,`path` 为 "101/102"
#### Scenario: 查询代理的整个上级链
- **WHEN** 查询代理(ID 为 103,路径为 "101/102/103")的上级链
- **THEN** 系统解析 `path` 字段,返回代理 101(顶级)、102(父级)、103(当前代理)
---
### Requirement: 分佣规则配置
系统 SHALL 支持为代理配置分佣规则,包括一次性分佣、长期分佣和组合分佣。
**commission_rules 表**:
- `id`: 分佣规则 ID(主键,BIGINT)
- `agent_id`: 代理用户 ID(BIGINT)
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
- `commission_type`: 分佣类型(VARCHAR(20),"one_time"-一次性 | "long_term"-长期 | "combined"-组合)
- `series_id`: 套餐系列 ID(BIGINT,可空,**仅一次性分佣使用**,关联 package_series 表)
- `package_id`: 套餐 ID(BIGINT,可空,**仅长期分佣使用**,关联 packages 表)
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
- `commission_value`: 分佣值(DECIMAL(10,4),固定金额或百分比值)
- `freeze_days`: 冻结天数(INT,分佣冻结天数,默认 7)
- `is_ladder`: 是否阶梯分佣(BOOLEAN,默认 false)
- `status`: 规则状态(INT,1-有效 2-无效)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
**字段使用规则**:
- **一次性分佣**: 使用 `series_id` 关联套餐系列,`package_id` 为 NULL
- **长期分佣**: 使用 `package_id` 关联具体套餐,`series_id` 为 NULL
- **组合分佣**: 需要创建两条规则记录,一条一次性(使用 `series_id`),一条长期(使用 `package_id`)
- **`series_id``package_id` 互斥**: 不能同时有值
#### Scenario: 配置一次性分佣规则
- **WHEN** 平台为代理(ID 为 123)配置一次性分佣规则,套餐系列 ID 为 1(月套餐系列),固定金额 5.00 元
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL,`commission_mode` 为 "fixed",`commission_value` 为 5.00
#### Scenario: 配置长期分佣规则
- **WHEN** 平台为代理(ID 为 123)配置长期分佣规则,套餐 ID 为 3001,百分比 5%
- **THEN** 系统创建分佣规则,`agent_id` 为 123,`commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,`commission_mode` 为 "percent",`commission_value` 为 0.05
#### Scenario: 配置组合分佣规则
- **WHEN** 平台为代理(ID 为 123)配置组合分佣规则,套餐系列 ID 为 1,先一次性分佣 10.00 元,连续在网 3 个月后开始长期分佣(套餐 ID 为 3001)3.00 元/月
- **THEN** 系统创建两条分佣规则:
- 一条 `commission_type` 为 "one_time",`series_id` 为 1,`package_id` 为 NULL
- 另一条 `commission_type` 为 "long_term",`series_id` 为 NULL,`package_id` 为 3001,且关联组合条件
#### Scenario: 字段互斥校验
- **WHEN** 平台尝试创建分佣规则,同时设置 `series_id` 为 1 和 `package_id` 为 3001
- **THEN** 系统拒绝创建,返回错误信息"`series_id``package_id` 不能同时有值"
---
### Requirement: 组合分佣条件配置
系统 SHALL 支持为组合分佣配置解冻条件,包括时间点条件和套餐周期条件。
**commission_combined_conditions 表**:
- `id`: 组合条件 ID(主键,BIGINT)
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT,必须是 commission_type 为 "long_term" 且属于组合分佣的规则)
- `condition_type`: 条件类型(VARCHAR(20),"time_point"-时间点 | "package_cycle"-套餐周期)
- `time_months`: 时间月数(INT,可空,仅当 condition_type 为 "time_point" 时有值,表示实名后多少个月)
- `package_cycle_threshold`: 套餐周期阈值(INT,可空,仅当 condition_type 为 "package_cycle" 时有值,表示使用多少个套餐周期)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
**解冻逻辑**: 组合分佣的长期部分,当满足**任一条件**(OR 关系)时开始产生长期分佣。
#### Scenario: 配置时间点条件
- **WHEN** 平台为组合分佣规则(ID 为 501)配置时间点条件,实名后 3 个月开始长期分佣
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "time_point",`time_months` 为 3
#### Scenario: 配置套餐周期条件
- **WHEN** 平台为组合分佣规则(ID 为 501)配置套餐周期条件,使用 10 个套餐周期后开始长期分佣
- **THEN** 系统创建组合条件记录,`commission_rule_id` 为 501,`condition_type` 为 "package_cycle",`package_cycle_threshold` 为 10
#### Scenario: 同时配置两种条件(OR 关系)
- **WHEN** 平台为组合分佣规则(ID 为 501)同时配置时间点条件(6 个月)和套餐周期条件(10 个周期)
- **THEN** 系统创建两条组合条件记录,长期分佣在任一条件满足时开始
---
### Requirement: 阶梯分佣配置
系统 SHALL 支持阶梯分佣,根据激活量/提货量达到阶梯条件后变更分佣值。
**commission_ladder 表**:
- `id`: 阶梯配置 ID(主键,BIGINT)
- `commission_rule_id`: 关联的分佣规则 ID(BIGINT)
- `ladder_type`: 阶梯类型(VARCHAR(20),"activation"-激活量 | "pickup"-提货量 | "deposit"-保证金)
- `ladder_threshold`: 阶梯阈值(INT,如激活 100 张)
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed"-固定金额 | "percent"-百分比)
- `commission_value`: 分佣值(DECIMAL(10,4),达到阶梯后的分佣值)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 配置激活量阶梯
- **WHEN** 平台为代理(ID 为 123)配置阶梯分佣,激活 100 张卡后分佣从 5.00 元提升到 8.00 元
- **THEN** 系统创建阶梯配置,`ladder_type` 为 "activation",`ladder_threshold` 为 100,`commission_value` 为 8.00
#### Scenario: 计算阶梯分佣
- **WHEN** 代理(ID 为 123)当月激活量达到 100 张
- **THEN** 系统根据阶梯配置,从第 101 张卡开始使用新的分佣值 8.00 元
---
### Requirement: 分佣记录管理
系统 SHALL 记录每笔分佣,支持冻结、解冻和发放流程。
**commission_records 表**:
- `id`: 分佣记录 ID(主键,BIGINT)
- `agent_id`: 代理用户 ID(BIGINT)
- `order_id`: 订单 ID(BIGINT)
- `commission_rule_id`: 分佣规则 ID(BIGINT)
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
- `amount`: 分佣金额(DECIMAL(10,2),元)
- `status`: 分佣状态(INT,1-冻结 2-解冻中 3-已发放 4-已失效)
- `freeze_until`: 冻结截止时间(TIMESTAMP,可空)
- `released_at`: 发放时间(TIMESTAMP,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建一次性分佣记录
- **WHEN** 订单(ID 为 10001)完成,触发代理(ID 为 123)的一次性分佣 5.00 元,冻结 7 天
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结),`freeze_until` 为 7 天后
#### Scenario: 分佣自动解冻
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,且满足解冻条件(激活+实名+充值)
- **THEN** 系统将分佣状态从 1(冻结) 变更为 2(解冻中),创建分佣解冻审批记录
#### Scenario: 分佣发放
- **WHEN** 分佣解冻审批通过
- **THEN** 系统将分佣状态从 2(解冻中) 变更为 3(已发放),将分佣金额转入代理钱包,`released_at` 记录发放时间
---
### Requirement: 分佣解冻条件
系统 SHALL 根据分佣类型校验不同的解冻条件。
**一次性分佣解冻条件**:
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
- 达到累计/首次充值金额
- 冻结天数到达
**长期分佣解冻条件**:
- 激活(实名状态为已实名;对于行业卡,实名状态可以为未实名)
- 达到累计/首次充值金额
- 在网状态正常
- 三无校验通过(通过 Excel 导入解冻)
**组合分佣解冻条件**:
- **一次性部分**: 立即产生并按一次性分佣条件解冻
- **长期部分**: 当满足以下**任一条件**时开始长期分佣(OR 关系):
- 达到某个时间点之后(例如:实名后 3 个月)
- **OR** 该 IoT 卡的套餐使用周期数达到阈值(例如:10 个周期)
- **注意**: 套餐周期阈值是针对单张 IoT 卡的,不是设备级别
#### Scenario: 一次性分佣满足解冻条件
- **WHEN** 分佣记录(ID 为 1001)的冻结截止时间到达,用户已实名且已充值
- **THEN** 系统将分佣状态变更为 2(解冻中),创建审批记录
#### Scenario: 长期分佣等待 Excel 导入解冻
- **WHEN** 长期分佣记录等待三无校验
- **THEN** 系统保持分佣状态为 1(冻结),等待平台通过 Excel 导入解冻数据
#### Scenario: 组合分佣时间点条件满足
- **WHEN** 组合分佣规则配置为实名后 3 个月开始长期分佣,IoT 卡已实名 3 个月
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使套餐周期数未达到阈值
#### Scenario: 组合分佣套餐周期条件满足
- **WHEN** 组合分佣规则配置为套餐使用 10 个周期后开始长期分佣,IoT 卡已使用套餐 10 个周期
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录,即使未达到时间点要求
#### Scenario: 组合分佣任一条件满足即开始
- **WHEN** 组合分佣规则配置为"实名后 6 个月 OR 10 个套餐周期",IoT 卡已使用 10 个周期但只实名 2 个月
- **THEN** 系统开始为该 IoT 卡创建长期分佣记录(因为套餐周期条件已满足)
#### Scenario: 行业卡一次性分佣解冻(无需实名)
- **WHEN** 行业卡(card_category 为 "industry")的一次性分佣记录冻结期到达,卡已激活且已充值,但实名状态为未实名
- **THEN** 系统判定解冻条件满足(行业卡无需实名认证),将分佣状态变更为 2(解冻中),创建审批记录
#### Scenario: 行业卡长期分佣解冻(无需实名)
- **WHEN** 行业卡(card_category 为 "industry")的长期分佣记录满足充值金额和在网状态,但实名状态为未实名
- **THEN** 系统判定行业卡无需实名认证,等待三无校验通过后可解冻
---
### Requirement: 分佣解冻审批
系统 SHALL 支持分佣解冻审批流程,审批通过后发放分佣。
**commission_approvals 表**:
- `id`: 审批记录 ID(主键,BIGINT)
- `commission_record_id`: 分佣记录 ID(BIGINT)
- `approval_type`: 审批类型(VARCHAR(20),"auto"-自动 | "manual"-人工)
- `status`: 审批状态(INT,1-待审批 2-已通过 3-已拒绝)
- `approver_id`: 审批人用户 ID(BIGINT,可空)
- `approval_time`: 审批时间(TIMESTAMP,可空)
- `approval_note`: 审批备注(TEXT,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建审批记录
- **WHEN** 分佣记录(ID 为 1001)状态变更为 2(解冻中)
- **THEN** 系统创建审批记录,`commission_record_id` 为 1001,`approval_type` 为 "auto",状态为 1(待审批)
#### Scenario: 审批通过
- **WHEN** 审批人(用户 ID 为 999)审批通过审批记录(ID 为 2001)
- **THEN** 系统将审批状态变更为 2(已通过),分佣记录状态变更为 3(已发放),将分佣金额转入代理钱包
#### Scenario: 审批拒绝
- **WHEN** 审批人拒绝审批记录(ID 为 2001),备注"用户未满足在网条件"
- **THEN** 系统将审批状态变更为 3(已拒绝),分佣记录状态变更为 4(已失效)
---
### Requirement: 分佣模板
系统 SHALL 支持创建分佣模板,存储常用的分佣方案,便于快速配置。
**commission_templates 表**:
- `id`: 模板 ID(主键,BIGINT)
- `template_name`: 模板名称(VARCHAR(255))
- `business_type`: 业务类型(VARCHAR(20),"iot_card"-IoT卡 | "number_card"-号卡)
- `commission_type`: 分佣类型(VARCHAR(20),"one_time" | "long_term" | "combined")
- `commission_mode`: 分佣模式(VARCHAR(20),"fixed" | "percent")
- `commission_value`: 分佣值(DECIMAL(10,4))
- `freeze_days`: 冻结天数(INT)
- `is_ladder`: 是否阶梯分佣(BOOLEAN)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建分佣模板
- **WHEN** 平台创建分佣模板"标准月套餐分佣",业务类型为 IoT 卡,一次性分佣 5.00 元,冻结 7 天
- **THEN** 系统创建模板记录,`template_name` 为 "标准月套餐分佣",`business_type` 为 "iot_card",`commission_type` 为 "one_time",`commission_value` 为 5.00,`freeze_days` 为 7
#### Scenario: 应用分佣模板
- **WHEN** 平台为代理(ID 为 123)应用模板(ID 为 501)
- **THEN** 系统根据模板配置创建分佣规则,`agent_id` 为 123,其他字段从模板复制
---
### Requirement: 多级代理分佣
系统 SHALL 支持多级代理分佣,根据代理路径计算每一级代理的分佣。
**多级分佣规则**:
- 通过代理路径(`path`)获取整个代理链
- 为每一级代理查找对应的分佣规则
- 创建多条分佣记录,每条对应一个代理
#### Scenario: 三级代理分佣
- **WHEN** 订单(ID 为 10001)的代理路径为 "101/102/103",每级代理配置分佣:101(2.00 元)、102(3.00 元)、103(5.00 元)
- **THEN** 系统创建 3 条分佣记录:代理 101 的 2.00 元、代理 102 的 3.00 元、代理 103 的 5.00 元
---
### Requirement: 分佣数据校验
系统 SHALL 对分佣数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 代理 ID(agent_id):必填,≥ 1
- 订单 ID(order_id):必填,≥ 1
- 分佣金额(amount):必填,≥ 0,最多 2 位小数
- 分佣状态(status):必填,枚举值 1-4
- 冻结天数(freeze_days):必填,≥ 0
#### Scenario: 创建分佣记录时金额为负数
- **WHEN** 创建分佣记录,金额为 -5.00
- **THEN** 系统拒绝创建,返回错误信息"分佣金额必须 ≥ 0"
#### Scenario: 创建分佣规则时分佣值无效
- **WHEN** 创建分佣规则,分佣模式为百分比,分佣值为 1.5(超过 100%)
- **THEN** 系统拒绝创建,返回错误信息"百分比分佣值必须在 0-1 之间"

View File

@@ -0,0 +1,304 @@
# IoT Card Management
## Purpose
Manage IoT cards (SIM cards) for the IoT management system, including inventory management, distribution, activation, status tracking, and Gateway integration.
This capability supports:
- IoT card entity definition and lifecycle management
- Platform self-operation and agent distribution models
- Integration with Gateway project for real-time status synchronization
- Batch import and multi-dimensional querying
- Support for normal cards (require real-name verification) and industry cards (no real-name required)
## Requirements
### Requirement: IoT 卡实体定义
系统 SHALL 定义 IoT 卡(IotCard)实体,包含 IoT 卡(物联网卡/流量卡/SIM卡)的商品属性、状态属性、所有权信息和 Gateway 集成字段。
**核心概念**: IoT 卡 = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)。系统使用 ICCID 作为 IoT 卡的唯一标识。
**卡业务类型**:
- **普通卡(normal)**: 需要实名认证才能激活使用,遵循运营商实名制要求
- **行业卡(industry)**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景
**实体字段**:
**商品属性**:
- `id`: IoT 卡 ID(主键,BIGINT)
- `iccid`: ICCID(VARCHAR(50),唯一,国际移动用户识别码,IoT卡的唯一标识)
- `card_type`: 卡类型(VARCHAR(50),如 "4G"、"5G"、"NB-IoT")
- `card_category`: 卡业务类型(VARCHAR(20),枚举值:"normal"-普通卡 | "industry"-行业卡,默认 "normal")
- `carrier_id`: 运营商 ID(BIGINT,关联 carriers 表,如中国移动、中国联通、中国电信)
- `imsi`: IMSI(VARCHAR(50),可选,国际移动用户识别码)
- `msisdn`: 手机号码(VARCHAR(20),可选)
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
- `supplier`: 供应商名称(VARCHAR(255),可选)
- `cost_price`: 成本价(DECIMAL(10,2),平台进货价)
- `distribute_price`: 分销价(DECIMAL(10,2),分销给代理的价格,仅当 owner_type 为 agent 时有值)
**所有权和状态**:
- `status`: IoT 卡状态(INT,1-在库 2-已分销 3-已激活 4-已停用)
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台自营 | "agent"-代理商 | "user"-用户 | "device"-设备)
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user/device 时为对应的 ID)
- `activated_at`: 激活时间(TIMESTAMP,可空)
**Gateway 集成字段**(从 Gateway 项目同步):
- `activation_status`: 激活状态(INT,0-未激活 1-已激活)
- `real_name_status`: 实名状态(INT,0-未实名 1-已实名)
- `network_status`: 网络状态(INT,0-停机 1-开机)
- `data_usage_mb`: 累计流量使用(BIGINT,MB 为单位,默认 0)
- `last_sync_time`: 最后一次与 Gateway 同步时间(TIMESTAMP,可空)
**轮询控制字段**:
- `enable_polling`: 是否参与轮询(BOOLEAN,默认 true,用于控制是否对该卡进行定时轮询)
- `last_data_check_at`: 最后一次卡流量检查时间(TIMESTAMP,可空,记录上次轮询卡流量的时间)
- `last_real_name_check_at`: 最后一次实名检查时间(TIMESTAMP,可空,记录上次轮询实名状态的时间)
**系统字段**:
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建平台自营 IoT 卡
- **WHEN** 平台批量导入 IoT 卡数据,ICCID 为 "89860123456789012345"
- **THEN** 系统创建 IoT 卡记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(在库),`activation_status` 为 0(未激活)
#### Scenario: 平台分销 IoT 卡给代理
- **WHEN** 平台将在库 IoT 卡分销给代理商(用户 ID 为 123),设置分销价为 50.00 元
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 2(已分销),`owner_type` 变更为 "agent",`owner_id` 设置为 123,`distribute_price` 设置为 50.00
#### Scenario: IoT 卡绑定到设备
- **WHEN** 用户将 IoT 卡(ICCID 为 "8986...")绑定到设备(ID 为 1001)
- **THEN** 系统在 `device_sim_bindings` 表创建绑定记录,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
#### Scenario: IoT 卡直接销售给用户
- **WHEN** 平台或代理将 IoT 卡直接销售给用户(用户 ID 为 2001)
- **THEN** 系统创建套餐订单记录,IoT 卡的 `owner_type` 变更为 "user",`owner_id` 变更为 2001
#### Scenario: 行业卡无需实名认证
- **WHEN** 创建卡业务类型为 "industry"(行业卡)的 IoT 卡
- **THEN** 系统允许该卡在 `real_name_status` 为 0(未实名)的情况下激活使用,不强制要求实名认证
#### Scenario: 普通卡需要实名认证
- **WHEN** 创建卡业务类型为 "normal"(普通卡)的 IoT 卡
- **THEN** 系统要求该卡必须先完成实名认证(`real_name_status` 为 1)才能激活使用
---
### Requirement: IoT 卡状态流转
系统 SHALL 管理 IoT 卡的状态流转,确保状态变更符合业务规则。
**状态定义**:
- **1-在库**: IoT 卡在平台库存中,未分销
- **2-已分销**: IoT 卡已分销给代理商,代理可销售
- **3-已激活**: IoT 卡已被终端用户激活使用
- **4-已停用**: IoT 卡已停用,不可使用
**状态流转规则**:
- 在库(1) → 已分销(2): 平台分销给代理
- 在库(1) → 已激活(3): 平台自营直接销售给用户并激活
- 已分销(2) → 已激活(3): 代理销售给用户并激活
- 已激活(3) → 已停用(4): 用户或平台主动停用
- 已停用(4) → 已激活(3): 用户或平台主动复机(仅在符合业务规则时)
#### Scenario: 代理销售 IoT 卡给用户
- **WHEN** 代理商销售已分销 IoT 卡给终端用户并激活
- **THEN** 系统将 IoT 卡状态从 2(已分销) 变更为 3(已激活),`activated_at` 记录激活时间,`activation_status` 从 Gateway 同步后变更为 1
#### Scenario: 平台自营销售 IoT 卡
- **WHEN** 平台直接销售在库 IoT 卡给终端用户并激活
- **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 3(已激活),`owner_type` 保持 "platform",`activated_at` 记录激活时间
#### Scenario: 停用已激活 IoT 卡
- **WHEN** 用户或平台停用已激活 IoT 卡
- **THEN** 系统将 IoT 卡状态从 3(已激活) 变更为 4(已停用),通过 Gateway API 执行停机操作
---
### Requirement: IoT 卡平台自营和代理分销
系统 SHALL 支持 IoT 卡的平台自营销售和代理分销两种模式,通过 `owner_type``owner_id` 区分所有者。
**平台自营**:
- `owner_type` 为 "platform"
- `owner_id` 为 0
- 平台直接销售给终端用户
- 销售价格由平台自主定价
**代理分销**:
- `owner_type` 为 "agent"
- `owner_id` 为代理用户 ID
- 代理商可以销售给终端用户或下级代理
- 分销价格由平台设置(`distribute_price`),代理商可在分销价基础上加价(但不能超过 2 倍)
#### Scenario: 查询平台自营 IoT 卡库存
- **WHEN** 查询平台自营 IoT 卡库存
- **THEN** 系统返回 `owner_type` 为 "platform" 且 `status` 为 1(在库) 的 IoT 卡列表
#### Scenario: 查询代理分销 IoT 卡库存
- **WHEN** 代理商(用户 ID 为 123)查询自己的 IoT 卡库存
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
#### Scenario: 代理加价销售 IoT 卡套餐
- **WHEN** 代理商为已分销 IoT 卡设置套餐售价
- **THEN** 系统校验套餐售价不超过分销价的 2 倍,校验通过后允许销售
---
### Requirement: IoT 卡批量导入
系统 SHALL 支持批量导入 IoT 卡数据,用于初始化库存或补充库存。
**导入字段**:
- ICCID(必填)
- 卡类型(必填,如 "4G"、"5G"、"NB-IoT")
- 卡业务类型(可选,枚举值 "normal" | "industry",默认 "normal")
- 运营商 ID(必填,从 carriers 表中选择)
- IMSI(可选)
- 手机号码(可选)
- 供应商(可选)
- 成本价(必填)
- 批次号(必填)
**导入规则**:
- ICCID 必须唯一,重复 ICCID 将被拒绝
- 导入的 IoT 卡默认状态为 1(在库),所有者为平台(`owner_type` 为 "platform",`owner_id` 为 0)
- 导入成功后记录操作日志
#### Scenario: 批量导入 IoT 卡成功
- **WHEN** 平台上传包含 100 条 IoT 卡数据的 CSV 文件
- **THEN** 系统创建 100 条 IoT 卡记录,状态为 1(在库),所有者为平台,返回导入成功消息
#### Scenario: 批量导入包含重复 ICCID
- **WHEN** 平台上传的 CSV 文件中包含已存在的 ICCID
- **THEN** 系统拒绝重复 ICCID 的 IoT 卡,返回错误信息并列出重复 ICCID,其他有效 IoT 卡正常导入
---
### Requirement: IoT 卡查询和筛选
系统 SHALL 支持多维度查询和筛选 IoT 卡,包括状态、所有者、批次号、卡类型等。
**查询条件**:
- ICCID(精确匹配或模糊匹配)
- IoT 卡状态(单选或多选)
- 所有者类型(platform | agent | user | device)
- 所有者 ID(仅当所有者类型为 agent/user/device 时有效)
- 批次号(精确匹配)
- 卡类型(单选或多选)
- 运营商 ID(单选或多选,从 carriers 表选择)
- 激活状态(0-未激活 | 1-已激活)
- 实名状态(0-未实名 | 1-已实名)
- 网络状态(0-停机 | 1-开机)
- 是否参与轮询(true | false)
- 激活时间范围(开始时间 - 结束时间)
- 创建时间范围(开始时间 - 结束时间)
**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数
#### Scenario: 查询特定批次的在库 IoT 卡
- **WHEN** 平台查询批次号为 "BATCH-2025-001" 且状态为 1(在库) 的 IoT 卡
- **THEN** 系统返回符合条件的 IoT 卡列表,包含 ICCID、类型、运营商、成本价等信息
#### Scenario: 代理查询自己的已分销 IoT 卡
- **WHEN** 代理商(用户 ID 为 123)查询自己的已分销 IoT 卡
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 且 `status` 为 2(已分销) 的 IoT 卡列表
#### Scenario: 分页查询 IoT 卡
- **WHEN** 平台查询在库 IoT 卡,指定每页 50 条,查询第 2 页
- **THEN** 系统返回第 51-100 条 IoT 卡记录,以及总记录数和总页数
---
### Requirement: Gateway 集成
系统 SHALL 预留 IoT 卡状态相关字段,用于后续与 Gateway 项目集成。
**集成字段**:
- `activation_status`: 激活状态(从 Gateway 同步)
- `real_name_status`: 实名状态(从 Gateway 同步)
- `network_status`: 网络状态(从 Gateway 同步)
- `data_usage_mb`: 累计流量使用(从 Gateway 同步)
- `last_sync_time`: 最后同步时间
**集成说明**:
- 本阶段只设计数据模型字段,不实现 Gateway HTTP 客户端代码
- 后续 Service 层将调用 Gateway API 获取 IoT 卡状态并更新这些字段
- Gateway 使用 AES 加密 + MD5 签名的统一传输协议(参考 design.md)
**Gateway API 功能**:
- 查询 IoT 卡状态(激活状态、实名状态、网络状态)
- 查询流量详情(累计流量使用、剩余流量)
- 停复机操作(停机、复机)
- 实名认证操作
#### Scenario: 预留 Gateway 集成字段
- **WHEN** 创建 IoT 卡记录
- **THEN** 系统初始化 Gateway 相关字段为默认值:`activation_status` 为 0,`real_name_status` 为 0,`network_status` 为 0,`data_usage_mb` 为 0,`last_sync_time` 为空
#### Scenario: 从 Gateway 同步 IoT 卡状态
- **WHEN** Service 层调用 Gateway API 查询 IoT 卡状态
- **THEN** 系统更新 IoT 卡的 `activation_status``real_name_status``network_status``data_usage_mb``last_sync_time` 字段
---
### Requirement: IoT 卡数据校验
系统 SHALL 对 IoT 卡数据进行校验,确保数据完整性和一致性。
**校验规则**:
- ICCID(iccid):必填,长度 19-20 字符,唯一
- 卡类型(card_type):必填,长度 1-50 字符
- 卡业务类型(card_category):必填,枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal"
- 运营商 ID(carrier_id):必填,≥ 1,必须是有效的运营商 ID
- 成本价(cost_price):必填,≥ 0,最多 2 位小数
- 分销价(distribute_price):可选,≥ 0,最多 2 位小数,≥ 成本价
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user" | "device"
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
- 激活状态(activation_status):必填,枚举值 0(未激活) | 1(已激活)
- 实名状态(real_name_status):必填,枚举值 0(未实名) | 1(已实名),当 card_category 为 "industry"(行业卡)时可以保持 0
- 网络状态(network_status):必填,枚举值 0(停机) | 1(开机)
- 轮询开关(enable_polling):必填,布尔值 true | false
#### Scenario: 创建 IoT 卡时 ICCID 格式错误
- **WHEN** 平台创建 IoT 卡,ICCID 长度为 15(小于 19)
- **THEN** 系统拒绝创建,返回错误信息"ICCID 长度必须为 19-20 字符"
#### Scenario: 创建 IoT 卡时 ICCID 重复
- **WHEN** 平台创建 IoT 卡,ICCID 为已存在的 "89860123456789012345"
- **THEN** 系统拒绝创建,返回错误信息"ICCID 已存在"
#### Scenario: 创建 IoT 卡时成本价为负数
- **WHEN** 平台创建 IoT 卡,成本价为 -10.00
- **THEN** 系统拒绝创建,返回错误信息"成本价必须 ≥ 0"
#### Scenario: 创建 IoT 卡时分销价低于成本价
- **WHEN** 平台创建 IoT 卡,成本价为 50.00,分销价为 40.00
- **THEN** 系统拒绝创建,返回错误信息"分销价不能低于成本价"

View File

@@ -0,0 +1,325 @@
# IoT Device Management
## Purpose
Manage IoT devices and their bindings with IoT cards (SIM cards), supporting device lifecycle management, device-card binding relationships, device-level package purchases, batch allocation, and remote device operations.
This capability supports:
- Device entity definition and lifecycle management
- Device-IoT card binding relationships (1-4 cards per device)
- Device-level package purchases with shared data pool
- Batch device allocation to agents
- Remote device operations (reboot, password change, reset)
## Requirements
## ADDED Requirements
### Requirement: 设备实体定义
系统 SHALL 定义设备(Device)实体,用于管理用户的物联网设备(如 GPS 追踪器、智能传感器等),支持设备与 IoT 卡的绑定关系、设备批量分配和设备操作。
**核心概念**: 设备不在卡管系统中销售,主要用于:
1. 用户设备管理(用户添加自己的设备,绑定 IoT 卡)
2. 方便运营人员管理投诉和代理要求(通过设备维度批量查看绑定的所有 IoT 卡)
3. 设备操作(重启、修改账号密码、重置等)
4. 设备批量分配(运营人员在别的系统报单后发货,把设备和绑定的 IoT 卡一起分配给代理)
**实体字段**:
**基本属性**:
- `id`: 设备 ID(主键,BIGINT)
- `device_no`: 设备编号(唯一,VARCHAR(50))
- `device_name`: 设备名称(VARCHAR(255))
- `device_model`: 设备型号(VARCHAR(100))
- `device_type`: 设备类型(VARCHAR(50),如 "GPS Tracker"、"Camera"、"Sensor")
- `max_sim_slots`: 最大 IoT 卡插槽数量(INT,1-4,默认 4)
- `manufacturer`: 设备制造商(VARCHAR(255),可选)
- `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯)
**所有权和状态**:
- `owner_type`: 所有者类型(VARCHAR(20),"platform"-平台库存(等待分配) | "agent"-代理商 | "user"-用户)
- `owner_id`: 所有者 ID(BIGINT,platform 时为 0,agent/user 时为对应的 ID)
- `status`: 设备状态(INT,1-未激活 2-已激活 3-已停用)
- `activated_at`: 激活时间(TIMESTAMP,可空)
**设备操作配置**(预留字段,用于后续设备操作功能):
- `device_username`: 设备登录账号(VARCHAR(100),可选)
- `device_password_encrypted`: 设备登录密码(加密存储,TEXT,可选)
- `device_api_endpoint`: 设备 API 接口地址(VARCHAR(500),可选)
**系统字段**:
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 用户添加设备
- **WHEN** 用户添加自己的设备(设备编号为 "GPS-001",设备名称为 "物流车辆追踪器")
- **THEN** 系统创建设备记录,`owner_type` 为 "user",`owner_id` 为用户 ID,状态为 1(未激活)
#### Scenario: 平台导入设备到库存
- **WHEN** 平台批量导入设备数据(准备发货给代理)
- **THEN** 系统创建设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
#### Scenario: 运营人员批量分配设备给代理
- **WHEN** 运营人员将平台库存设备(ID 为 1001)分配给代理商(用户 ID 为 123)
- **THEN** 系统将设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时自动分配该设备绑定的所有 IoT 卡给代理
---
### Requirement: 设备状态流转
系统 SHALL 管理设备的状态流转,确保状态变更符合业务规则。
**状态定义**:
- **1-未激活**: 设备尚未激活使用
- **2-已激活**: 设备已被用户激活使用
- **3-已停用**: 设备已停用,不可使用
**状态流转规则**:
- 未激活(1) → 已激活(2): 用户激活设备
- 已激活(2) → 已停用(3): 用户或平台主动停用设备
- 已停用(3) → 已激活(2): 用户或平台主动恢复设备(仅在符合业务规则时)
#### Scenario: 用户激活设备
- **WHEN** 用户激活自己的设备
- **THEN** 系统将设备状态从 1(未激活) 变更为 2(已激活),`activated_at` 记录激活时间
#### Scenario: 用户停用设备
- **WHEN** 用户停用已激活的设备
- **THEN** 系统将设备状态从 2(已激活) 变更为 3(已停用),同时可选择是否停用该设备绑定的所有 IoT 卡
---
### Requirement: 设备与 IoT 卡绑定关系
系统 SHALL 管理设备与 IoT 卡的绑定关系,一个设备可以绑定 1-4 张 IoT 卡。
**绑定规则**:
- 一个设备最多绑定 4 张 IoT 卡(由 `max_sim_slots` 字段控制)
- 一个 IoT 卡同一时间只能绑定一个设备
- 绑定时记录插槽位置(slot_position: 1, 2, 3, 4)
- 绑定时记录绑定时间和绑定状态(1-已绑定 2-已解绑)
- 设备绑定 IoT 卡后,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为设备 ID
**中间表 device_sim_bindings**:
- `id`: 绑定记录 ID(主键,BIGINT)
- `device_id`: 设备 ID(BIGINT)
- `iot_card_id`: IoT 卡 ID(BIGINT)
- `slot_position`: 插槽位置(INT,1-4)
- `bind_status`: 绑定状态(INT,1-已绑定 2-已解绑)
- `bind_time`: 绑定时间(TIMESTAMP)
- `unbind_time`: 解绑时间(TIMESTAMP,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 绑定 IoT 卡到设备
- **WHEN** 用户将 IoT 卡(ID 为 101)绑定到设备(ID 为 1001)的插槽 1
- **THEN** 系统创建绑定记录,`device_id` 为 1001,`iot_card_id` 为 101,`slot_position` 为 1,`bind_status` 为 1(已绑定),`bind_time` 为当前时间,IoT 卡的 `owner_type` 变更为 "device",`owner_id` 变更为 1001
#### Scenario: 绑定超过最大插槽数量
- **WHEN** 用户尝试将第 5 张 IoT 卡绑定到最大插槽数为 4 的设备
- **THEN** 系统拒绝绑定,返回错误信息"设备插槽已满,最多支持 4 张 IoT 卡"
#### Scenario: 绑定已被占用的 IoT 卡
- **WHEN** 用户尝试绑定已被其他设备绑定的 IoT 卡
- **THEN** 系统拒绝绑定,返回错误信息"该 IoT 卡已被其他设备绑定"
#### Scenario: 解绑 IoT 卡
- **WHEN** 用户解绑设备的 IoT 卡(绑定记录 ID 为 10)
- **THEN** 系统将绑定记录的 `bind_status` 从 1(已绑定) 变更为 2(已解绑),`unbind_time` 记录解绑时间,IoT 卡的 `owner_type``owner_id` 重置
#### Scenario: 查询设备当前绑定的 IoT 卡
- **WHEN** 用户查询设备(ID 为 1001)当前绑定的 IoT 卡
- **THEN** 系统返回 `device_id` 为 1001 且 `bind_status` 为 1(已绑定) 的所有绑定记录,包含 IoT 卡信息(ICCID、运营商、激活状态等)和插槽位置
---
### Requirement: 设备套餐购买和流量共享
系统 SHALL 支持用户为设备购买套餐,套餐自动分配到设备绑定的所有 IoT 卡,流量在设备级别共享。
**设备套餐业务规则**:
- 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张)
- 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡)
- 分佣**只计算一次**(不按卡数倍增)
- 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡
**套餐分配示例**:
- 设备绑定 3 张 IoT 卡
- 用户购买套餐:399 元/年,每月 3000G 流量,长期佣金 100 元
- 用户支付:399 元
- 套餐分配:设备的 3 张 IoT 卡都获得该套餐
- 流量使用:3000G/月 在 3 张卡之间共享(不是每张卡 3000G,而是总共 3000G)
- 分佣:代理获得 100 元分佣(只分一次,不是 3 × 100 元)
#### Scenario: 用户为设备购买套餐
- **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买套餐(套餐 ID 为 3001,399 元/年,3000G/月)
- **THEN** 系统创建套餐订单,`device_id` 为 1001,`package_id` 为 3001,订单金额为 399 元,将套餐分配到设备绑定的 3 张 IoT 卡,设置流量共享模式为设备级别
#### Scenario: 设备级流量共享
- **WHEN** 设备(ID 为 1001)的套餐流量为 3000G/月,设备绑定 3 张 IoT 卡
- **THEN** 系统设置流量共享模式,3 张 IoT 卡共享 3000G/月(不是每张卡 3000G),无论使用哪张卡,都从这个流量池扣除
#### Scenario: 设备套餐分佣
- **WHEN** 用户为设备购买套餐,订单金额为 399 元,代理的长期分佣规则为 100 元
- **THEN** 系统为代理创建一条分佣记录,分佣金额为 100 元(只分一次,不按设备绑定的卡数倍增)
---
### Requirement: 设备批量分配
系统 SHALL 支持运营人员批量分配设备给代理,设备分配时自动分配该设备绑定的所有 IoT 卡。
**分配规则**:
- 只能分配 `owner_type` 为 "platform" 的设备(平台库存)
- 分配时,设备的 `owner_type` 变更为 "agent",`owner_id` 设置为代理用户 ID
- 分配时,设备绑定的所有 IoT 卡的 `owner_type` 也变更为 "agent",`owner_id` 设置为代理用户 ID
- 分配操作记录到操作日志
#### Scenario: 运营人员批量分配设备
- **WHEN** 运营人员将 10 台设备(平台库存)分配给代理商(用户 ID 为 123)
- **THEN** 系统将这 10 台设备的 `owner_type` 变更为 "agent",`owner_id` 设置为 123,同时将这些设备绑定的所有 IoT 卡也分配给代理 123
#### Scenario: 分配已分配的设备
- **WHEN** 运营人员尝试分配 `owner_type` 为 "agent" 的设备
- **THEN** 系统拒绝分配,返回错误信息"该设备已分配给代理,不能重复分配"
---
### Requirement: 设备操作
系统 SHALL 支持对设备的远程操作(重启、修改账号密码、重置等),用于设备管理和故障排查。
**设备操作类型**:
- **重启设备**: 远程重启设备
- **修改账号密码**: 修改设备的登录账号和密码
- **重置设备**: 将设备恢复到出厂设置
- **查询设备状态**: 查询设备的在线状态、运行状态等
- **设备配置更新**: 更新设备的配置参数
**操作说明**:
- 本阶段只设计数据模型字段和接口定义,不实现设备操作的具体代码
- 后续 Service 层将调用设备厂商提供的 API 或通过 MQTT/HTTP 协议与设备通信
- 设备操作需要记录操作日志(操作类型、操作人、操作时间、操作结果)
#### Scenario: 重启设备
- **WHEN** 用户或运营人员请求重启设备(ID 为 1001)
- **THEN** 系统调用设备 API 发送重启命令,记录操作日志,返回操作结果
#### Scenario: 修改设备密码
- **WHEN** 用户或运营人员修改设备(ID 为 1001)的登录密码
- **THEN** 系统更新设备的 `device_password_encrypted` 字段(加密存储),调用设备 API 同步密码修改,记录操作日志
---
### Requirement: 设备批量导入
系统 SHALL 支持批量导入设备数据,用于平台库存管理。
**导入字段**:
- 设备编号(必填)
- 设备名称(必填)
- 设备型号(必填)
- 设备类型(必填)
- 最大插槽数(可选,默认 4)
- 设备制造商(可选)
- 批次号(必填)
**导入规则**:
- 设备编号必须唯一,重复编号将被拒绝
- 导入的设备默认 `owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
- 导入成功后记录操作日志
#### Scenario: 批量导入设备成功
- **WHEN** 平台上传包含 50 条设备数据的 CSV 文件
- **THEN** 系统创建 50 条设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活),返回导入成功消息
#### Scenario: 批量导入包含重复编号
- **WHEN** 平台上传的 CSV 文件中包含已存在的设备编号
- **THEN** 系统拒绝重复编号的设备,返回错误信息并列出重复编号,其他有效设备正常导入
---
### Requirement: 设备查询和筛选
系统 SHALL 支持多维度查询和筛选设备,包括状态、所有者、批次号、设备类型等。
**查询条件**:
- 设备编号(精确匹配或模糊匹配)
- 设备名称(模糊匹配)
- 设备状态(单选或多选)
- 所有者类型(platform | agent | user)
- 所有者 ID(仅当所有者类型为 agent/user 时有效)
- 批次号(精确匹配)
- 设备类型(单选或多选)
- 设备制造商(模糊匹配)
- 激活时间范围(开始时间 - 结束时间)
- 创建时间范围(开始时间 - 结束时间)
**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数
#### Scenario: 查询平台库存设备
- **WHEN** 运营人员查询平台库存设备
- **THEN** 系统返回 `owner_type` 为 "platform" 的设备列表
#### Scenario: 代理查询自己的设备
- **WHEN** 代理商(用户 ID 为 123)查询自己的设备
- **THEN** 系统返回 `owner_type` 为 "agent" 且 `owner_id` 为 123 的设备列表
#### Scenario: 用户查询自己的设备
- **WHEN** 用户(用户 ID 为 2001)查询自己的设备
- **THEN** 系统返回 `owner_type` 为 "user" 且 `owner_id` 为 2001 的设备列表,包含设备绑定的所有 IoT 卡信息
#### Scenario: 运营人员通过设备查看绑定的所有 IoT 卡
- **WHEN** 运营人员需要处理投诉,查询设备(ID 为 1001)绑定的所有 IoT 卡
- **THEN** 系统返回设备信息和绑定的所有 IoT 卡详细信息(ICCID、运营商、激活状态、流量使用等),方便统一查看和管理
---
### Requirement: 设备数据校验
系统 SHALL 对设备数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 设备编号(device_no):必填,长度 1-50 字符,唯一
- 设备名称(device_name):必填,长度 1-255 字符
- 设备型号(device_model):必填,长度 1-100 字符
- 设备类型(device_type):必填,长度 1-50 字符
- 最大插槽数(max_sim_slots):必填,1-4 之间的整数
- 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user"
- 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
- 设备状态(status):必填,枚举值 1(未激活) | 2(已激活) | 3(已停用)
#### Scenario: 创建设备时插槽数超出范围
- **WHEN** 用户创建设备,最大插槽数为 5
- **THEN** 系统拒绝创建,返回错误信息"最大插槽数必须在 1-4 之间"
#### Scenario: 创建设备时设备编号重复
- **WHEN** 用户创建设备,设备编号为已存在的 "DEV-001"
- **THEN** 系统拒绝创建,返回错误信息"设备编号已存在"

View File

@@ -0,0 +1,175 @@
# Number Card Management
## Purpose
Manage number cards (virtual products) for carrier order callbacks, supporting carrier order passthrough, agent promotion, commission processing, and carrier settlement tracking.
This capability supports:
- Number card entity definition as virtual product mapping
- Carrier order callbacks from Gateway project
- Agent promotion via links or offline cards
- Commission processing for number card orders
- Carrier settlement tracking for financial reconciliation
- Integration with existing commission rules (one-time, long-term, combined)
## Requirements
## ADDED Requirements
### Requirement: 号卡实体定义
系统 SHALL 定义号卡(NumberCard)实体,作为运营商订单回传的映射,支持代理分销和分佣。
**实体字段**:
- `id`: 号卡 ID(主键,BIGINT)
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),唯一,用于对应运营商订单)
- `product_name`: 商品名称(VARCHAR(255))
- `carrier`: 运营商名称(VARCHAR(100),如 "中国移动"、"中国联通"、"中国电信")
- `carrier_product_id`: 运营商商品 ID(VARCHAR(100))
- `package_type`: 套餐类型(VARCHAR(50),如 "月套餐"、"流量包")
- `data_amount_mb`: 流量额度(BIGINT,MB 为单位,可选)
- `voice_minutes`: 语音分钟数(INT,可选)
- `sms_count`: 短信条数(INT,可选)
- `price`: 固定售价(DECIMAL(10,2),由运营商定价)
- `status`: 号卡状态(INT,1-上架 2-下架)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 创建号卡商品
- **WHEN** 平台创建号卡商品,虚拟商品编码为 "VC-CMCC-001",运营商为"中国移动",固定售价为 30.00 元
- **THEN** 系统创建号卡记录,`virtual_product_code` 为 "VC-CMCC-001",`carrier` 为 "中国移动",`price` 为 30.00,状态为 1(上架)
#### Scenario: 虚拟商品编码唯一性
- **WHEN** 平台创建号卡商品,虚拟商品编码为已存在的 "VC-CMCC-001"
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码已存在"
---
### Requirement: 号卡运营商订单回传
系统 SHALL 接收 Gateway 项目转换后的运营商订单回传,通过虚拟商品编码匹配号卡,创建订单和分佣记录。
**订单回传字段**:
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),唯一)
- `virtual_product_code`: 虚拟商品编码(VARCHAR(100),用于匹配号卡)
- `user_phone`: 用户手机号(VARCHAR(20))
- `amount`: 订单金额(DECIMAL(10,2))
- `order_time`: 订单时间(TIMESTAMP)
- `agent_id`: 代理 ID(BIGINT,可空,如果通过代理推广则有值)
- `carrier_order_data`: 运营商订单原始数据(JSONB)
**回传处理流程**:
1. Gateway 接收运营商订单,统一转换为 JSON 格式
2. Gateway 通过 HTTP POST 回传给 CMP 系统
3. CMP 系统根据 `virtual_product_code` 匹配号卡
4. CMP 系统创建订单记录(`order_type` 为 "number_card")
5. 如果有 `agent_id`,触发代理分佣流程
#### Scenario: 接收运营商订单回传
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为 "VC-CMCC-001",代理 ID 为 123,订单金额为 30.00 元
- **THEN** 系统创建订单记录,`order_type` 为 "number_card",`source_id` 为号卡 ID,`agent_id` 为 123,触发分佣计算
#### Scenario: 虚拟商品编码不存在
- **WHEN** Gateway 回传运营商订单,虚拟商品编码为不存在的 "VC-UNKNOWN"
- **THEN** 系统拒绝创建订单,返回错误信息"虚拟商品编码不存在"并记录到日志
---
### Requirement: 号卡代理分销
系统 SHALL 支持号卡的代理分销,代理通过推广链接或卡板推广号卡给终端用户。
**分销规则**:
- 号卡由运营商定价,平台无权修改价格
- 代理通过推广链接或卡板获取用户激活
- 用户激活充值后,资金直接支付给运营商,不经过平台
- 运营商周期性结算总佣金给平台
- 平台根据代理分佣规则分配佣金给代理
**代理推广方式**:
- **推广链接**: 代理生成带有 `agent_id` 的推广链接,用户点击链接激活
- **卡板**: 代理线下分发印有二维码的卡板,用户扫码激活
#### Scenario: 代理生成推广链接
- **WHEN** 代理商(用户 ID 为 123)为号卡(ID 为 5001)生成推广链接
- **THEN** 系统生成带有 `agent_id=123``product_id=5001` 的推广链接,如 `https://example.com/activate?agent=123&product=5001`
#### Scenario: 用户通过代理链接激活
- **WHEN** 用户通过代理推广链接激活号卡并充值 30.00 元
- **THEN** 运营商接收用户支付,Gateway 回传订单时包含 `agent_id=123`,系统触发代理分佣流程
---
### Requirement: 号卡分佣处理
系统 SHALL 根据号卡分佣规则计算代理佣金,支持冻结和解冻流程。
**分佣规则**:
- 号卡分佣配置在代理分佣规则表(`commission_rules`)中
- 分佣类型:一次性分佣、长期分佣、组合分佣(参考 iot-agent-commission 规范)
- 号卡订单的分佣需要满足条件:激活(实名) + 达到充值金额 + 在网状态 + 三无校验
- 分佣记录创建时状态为"冻结",满足条件后变为"解冻中",审批通过后变为"已发放"
#### Scenario: 号卡订单触发分佣
- **WHEN** 运营商回传订单,代理 ID 为 123,订单金额为 30.00 元,该代理配置了一次性分佣 5.00 元
- **THEN** 系统创建分佣记录,金额为 5.00 元,状态为"冻结",等待满足解冻条件
#### Scenario: 号卡分佣解冻
- **WHEN** 号卡订单满足解冻条件(激活 + 充值 + 在网 + 三无校验)
- **THEN** 系统将分佣记录状态从"冻结"变更为"解冻中",创建分佣解冻审批记录
---
### Requirement: 号卡运营商结算
系统 SHALL 记录运营商周期性结算的佣金总额,用于财务对账和利润计算。
**结算字段**:
- `settlement_id`: 结算记录 ID(主键,BIGINT)
- `carrier`: 运营商名称(VARCHAR(100))
- `settlement_period`: 结算周期(VARCHAR(50),如 "2025-01")
- `total_commission`: 运营商结算的佣金总额(DECIMAL(18,2))
- `settlement_time`: 结算时间(TIMESTAMP)
- `status`: 结算状态(INT,1-待确认 2-已确认)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
#### Scenario: 记录运营商结算
- **WHEN** 运营商"中国移动"结算 2025 年 1 月的佣金总额 50000.00 元
- **THEN** 系统创建结算记录,`carrier` 为 "中国移动",`settlement_period` 为 "2025-01",`total_commission` 为 50000.00,状态为 1(待确认)
#### Scenario: 确认运营商结算
- **WHEN** 财务确认运营商结算记录(ID 为 1001)
- **THEN** 系统将结算记录状态从 1(待确认) 变更为 2(已确认)
---
### Requirement: 号卡数据校验
系统 SHALL 对号卡数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 虚拟商品编码(virtual_product_code):必填,长度 1-100 字符,唯一
- 商品名称(product_name):必填,长度 1-255 字符
- 运营商名称(carrier):必填,长度 1-100 字符
- 固定售价(price):必填,≥ 0,最多 2 位小数
- 状态(status):必填,枚举值 1(上架) | 2(下架)
#### Scenario: 创建号卡时虚拟商品编码为空
- **WHEN** 平台创建号卡,虚拟商品编码为空
- **THEN** 系统拒绝创建,返回错误信息"虚拟商品编码不能为空"
#### Scenario: 创建号卡时固定售价为负数
- **WHEN** 平台创建号卡,固定售价为 -10.00
- **THEN** 系统拒绝创建,返回错误信息"固定售价必须 ≥ 0"

View File

@@ -0,0 +1,248 @@
# IoT Order Management
## Purpose
Manage orders for IoT card packages and number card products, including order creation, payment processing, status tracking, commission triggering, and support for single-card orders, device-level orders, and carrier number card orders.
This capability supports:
- Unified order entity for package orders and number card orders
- Order status lifecycle management
- Multiple payment methods (wallet, online payment, carrier direct payment)
- Commission triggering on order completion
- Device-level order commission (counted once regardless of bound card count)
- Multi-dimensional order querying and filtering
## Requirements
## ADDED Requirements
### Requirement: 订单实体定义
系统 SHALL 定义订单(Order)实体,统一管理两种订单类型:套餐订单、号卡订单。
**核心概念**:
- **套餐订单**: 用户为 IoT 卡或设备购买套餐的订单,包括单卡套餐订单和设备级套餐订单
- **号卡订单**: 运营商回传的号卡订单,用户直接在上游平台下单,系统只接收订单状态更新
**实体字段**:
- `id`: 订单 ID(主键,BIGINT)
- `order_no`: 订单编号(VARCHAR(50),唯一)
- `order_type`: 订单类型(INT,1-套餐订单 2-号卡订单)
- `iot_card_id`: IoT 卡 ID(BIGINT,可空,单卡套餐订单时有值)
- `device_id`: 设备 ID(BIGINT,可空,设备级套餐订单时有值)
- `number_card_id`: 号卡 ID(BIGINT,可空,号卡订单时有值)
- `package_id`: 套餐 ID(BIGINT,可空,仅当 order_type 为 1 时有值)
- `user_id`: 用户 ID(BIGINT,购买用户)
- `agent_id`: 代理 ID(BIGINT,可空,通过代理购买时有值)
- `amount`: 订单金额(DECIMAL(10,2),元)
- `payment_method`: 支付方式(VARCHAR(20),"wallet"-钱包 | "online"-在线支付 | "carrier"-运营商直付)
- `status`: 订单状态(INT,1-待支付 2-已支付 3-已完成 4-已取消 5-已退款)
- `carrier_order_id`: 运营商订单 ID(VARCHAR(255),可空,仅号卡订单有值)
- `carrier_order_data`: 运营商订单原始数据(JSONB,可空)
- `paid_at`: 支付时间(TIMESTAMP,可空)
- `completed_at`: 完成时间(TIMESTAMP,可空)
- `created_at`: 创建时间(TIMESTAMP,自动填充)
- `updated_at`: 更新时间(TIMESTAMP,自动填充)
**订单类型说明**:
- **单卡套餐订单**: `order_type` 为 1,`iot_card_id` 有值,`device_id` 为 NULL
- **设备级套餐订单**: `order_type` 为 1,`device_id` 有值,`iot_card_id` 为 NULL
- **号卡订单**: `order_type` 为 2,`number_card_id` 有值,`iot_card_id``device_id` 为 NULL
#### Scenario: 创建单卡套餐购买订单
- **WHEN** 用户(ID 为 2001)为 IoT 卡(ID 为 1001)购买套餐(ID 为 3001),金额为 30.00 元
- **THEN** 系统创建订单记录,`order_type` 为 1,`iot_card_id` 为 1001,`device_id` 为 NULL,`package_id` 为 3001,`user_id` 为 2001,`amount` 为 30.00,状态为 1(待支付)
#### Scenario: 创建设备级套餐购买订单
- **WHEN** 用户(ID 为 2001)为设备(ID 为 5001,绑定 3 张 IoT 卡)购买套餐(ID 为 3002),金额为 399.00 元
- **THEN** 系统创建订单记录,`order_type` 为 1,`device_id` 为 5001,`iot_card_id` 为 NULL,`package_id` 为 3002,`user_id` 为 2001,`amount` 为 399.00,状态为 1(待支付)
#### Scenario: 创建号卡订单(运营商回传)
- **WHEN** Gateway 回传运营商订单,虚拟商品编码对应号卡 ID 为 6001,代理 ID 为 123,订单金额为 30.00 元
- **THEN** 系统创建订单记录,`order_type` 为 2,`number_card_id` 为 6001,`iot_card_id` 为 NULL,`device_id` 为 NULL,`agent_id` 为 123,`amount` 为 30.00,`payment_method` 为 "carrier",状态为 2(已支付)
---
### Requirement: 订单状态流转
系统 SHALL 管理订单的状态流转,确保状态变更符合业务规则。
**状态定义**:
- **1-待支付**: 订单已创建,等待用户支付
- **2-已支付**: 用户已支付,等待系统处理
- **3-已完成**: 订单已完成(激活/发货等)
- **4-已取消**: 订单已取消
- **5-已退款**: 订单已退款
**状态流转规则**:
- 待支付(1) → 已支付(2): 用户完成支付
- 待支付(1) → 已取消(4): 用户取消订单或订单超时
- 已支付(2) → 已完成(3): 系统完成订单处理(激活/发货)
- 已支付(2) → 已退款(5): 用户申请退款且审核通过
- 已完成(3) → 已退款(5): 用户申请退款且审核通过(特殊情况)
#### Scenario: 用户支付订单
- **WHEN** 用户支付待支付订单(ID 为 10001),支付金额为 30.00 元
- **THEN** 系统将订单状态从 1(待支付) 变更为 2(已支付),`paid_at` 记录支付时间
#### Scenario: 单卡套餐订单完成
- **WHEN** 系统处理完单卡套餐订单(ID 为 10001),激活 IoT 卡并分配套餐
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
#### Scenario: 设备级套餐订单完成
- **WHEN** 系统处理完设备级套餐订单(ID 为 10002),为设备绑定的所有 IoT 卡分配套餐
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
---
### Requirement: 订单支付方式
系统 SHALL 支持三种支付方式:钱包支付、在线支付、运营商直付。
**支付方式**:
- **钱包支付(wallet)**: 从用户钱包余额扣款
- **在线支付(online)**: 通过第三方支付(微信/支付宝等)
- **运营商直付(carrier)**: 用户直接支付给运营商(仅号卡订单)
**支付规则**:
- 一次性分佣订单必须使用钱包支付
- 套餐购买订单可以使用钱包或在线支付
- 号卡订单必须使用运营商直付
#### Scenario: 钱包支付订单
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 50.00 元
- **THEN** 系统从钱包扣除 30.00 元,订单状态变更为 2(已支付),`payment_method` 为 "wallet"
#### Scenario: 钱包余额不足
- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 20.00 元
- **THEN** 系统拒绝支付,返回错误信息"钱包余额不足"
#### Scenario: 一次性分佣订单强制钱包支付
- **WHEN** 用户购买配置了一次性分佣的套餐,尝试使用在线支付
- **THEN** 系统拒绝支付,返回错误信息"一次性分佣订单必须使用钱包支付"
---
### Requirement: 订单分佣触发
系统 SHALL 在订单完成时触发分佣计算,根据代理分佣规则创建分佣记录。
**触发条件**:
- 订单状态变更为 3(已完成)
- 订单有 `agent_id`(通过代理销售)
- 代理配置了分佣规则
**分佣计算规则**:
- **单卡套餐订单**: 根据 IoT 卡关联的代理分佣规则计算分佣
- **设备级套餐订单**: 分佣只计算一次(不按设备绑定的 IoT 卡数量倍增)
- **号卡订单**: 下单即冻结分佣,次月通过 Excel 导入解冻
#### Scenario: 单卡套餐购买订单触发分佣
- **WHEN** 代理(ID 为 123)的单卡套餐订单(ID 为 10001)完成,订单金额为 30.00 元,代理配置了 5.00 元一次性分佣
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结)
#### Scenario: 设备级套餐订单触发分佣(只计算一次)
- **WHEN** 代理(ID 为 123)的设备级套餐订单(ID 为 10002)完成,设备绑定 3 张 IoT 卡,订单金额为 399.00 元,代理配置了 100.00 元长期分佣
- **THEN** 系统创建一条分佣记录,`agent_id` 为 123,`order_id` 为 10002,`amount` 为 100.00,状态为 1(冻结),不是 3 × 100.00
#### Scenario: 号卡订单触发分佣
- **WHEN** 代理(ID 为 123)的号卡订单(ID 为 10003)创建,订单金额为 30.00 元,代理配置了长期分佣
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10003,状态为 1(冻结),等待次月通过 Excel 导入解冻
---
### Requirement: 订单查询和筛选
系统 SHALL 支持多维度查询和筛选订单。
**查询条件**:
- 订单编号(精确匹配)
- 订单类型(1-套餐订单 2-号卡订单)
- 订单状态(单选或多选)
- IoT 卡 ID(精确匹配)
- 设备 ID(精确匹配)
- 号卡 ID(精确匹配)
- 用户 ID(精确匹配)
- 代理 ID(精确匹配)
- 支付方式(单选或多选)
- 创建时间范围(开始时间 - 结束时间)
- 支付时间范围(开始时间 - 结束时间)
- 完成时间范围(开始时间 - 结束时间)
**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数
#### Scenario: 查询用户的所有订单
- **WHEN** 用户(ID 为 2001)查询自己的所有订单
- **THEN** 系统返回 `user_id` 为 2001 的所有订单列表,按创建时间倒序排列
#### Scenario: 查询代理的订单
- **WHEN** 代理(ID 为 123)查询自己的订单,筛选已完成的套餐订单
- **THEN** 系统返回 `agent_id` 为 123 且 `order_type` 为 1 且 `status` 为 3(已完成) 的订单列表
#### Scenario: 查询 IoT 卡的订单历史
- **WHEN** 运营人员查询 IoT 卡(ID 为 1001)的所有订单
- **THEN** 系统返回 `iot_card_id` 为 1001 的所有订单列表,包含套餐购买记录
#### Scenario: 查询设备的订单历史
- **WHEN** 运营人员查询设备(ID 为 5001)的所有订单
- **THEN** 系统返回 `device_id` 为 5001 的所有设备级套餐订单列表
---
### Requirement: 订单数据校验
系统 SHALL 对订单数据进行校验,确保数据完整性和一致性。
**校验规则**:
- 订单编号(order_no):必填,长度 1-50 字符,唯一
- 订单类型(order_type):必填,枚举值 1(套餐订单) | 2(号卡订单)
- IoT 卡 ID(iot_card_id):套餐订单时 iot_card_id 和 device_id 二选一
- 设备 ID(device_id):套餐订单时 iot_card_id 和 device_id 二选一
- 号卡 ID(number_card_id):号卡订单时必填
- 套餐 ID(package_id):套餐订单时必填
- 用户 ID(user_id):必填,≥ 1
- 订单金额(amount):必填,≥ 0,最多 2 位小数
- 支付方式(payment_method):必填,枚举值 "wallet" | "online" | "carrier"
- 状态(status):必填,枚举值 1-5
#### Scenario: 创建订单时金额为负数
- **WHEN** 创建订单,金额为 -10.00
- **THEN** 系统拒绝创建,返回错误信息"订单金额必须 ≥ 0"
#### Scenario: 创建订单时订单编号重复
- **WHEN** 创建订单,订单编号为已存在的 "ORD-2025-001"
- **THEN** 系统拒绝创建,返回错误信息"订单编号已存在"
#### Scenario: 创建套餐订单时未关联 IoT 卡或设备
- **WHEN** 创建套餐订单,`iot_card_id``device_id` 都为 NULL
- **THEN** 系统拒绝创建,返回错误信息"套餐订单必须关联 IoT 卡或设备"
#### Scenario: 创建套餐订单时同时关联 IoT 卡和设备
- **WHEN** 创建套餐订单,`iot_card_id` 为 1001,`device_id` 为 5001
- **THEN** 系统拒绝创建,返回错误信息"套餐订单不能同时关联 IoT 卡和设备"
#### Scenario: 创建号卡订单时未关联号卡
- **WHEN** 创建号卡订单,`number_card_id` 为 NULL
- **THEN** 系统拒绝创建,返回错误信息"号卡订单必须关联号卡"

View File

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