实现 IoT SIM 管理模块数据模型和数据库结构
- 添加 IoT 核心业务表:运营商、IoT 卡、设备、号卡、套餐、订单等 - 添加分佣系统表:分佣规则、分佣记录、运营商结算等 - 添加轮询和流量管理表:轮询配置、流量使用记录等 - 添加财务和系统管理表:佣金提现、换卡申请等 - 实现完整的 GORM 模型和常量定义 - 添加数据库迁移脚本和详细文档 - 集成 OpenSpec 工作流工具(opsx 命令和 skills) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 之间"
|
||||
@@ -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** 系统拒绝创建,返回错误信息"分销价不能低于成本价"
|
||||
@@ -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** 系统拒绝创建,返回错误信息"设备编号已存在"
|
||||
@@ -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"
|
||||
@@ -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** 系统拒绝创建,返回错误信息"号卡订单必须关联号卡"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user