- 添加 IoT 核心业务表:运营商、IoT 卡、设备、号卡、套餐、订单等 - 添加分佣系统表:分佣规则、分佣记录、运营商结算等 - 添加轮询和流量管理表:轮询配置、流量使用记录等 - 添加财务和系统管理表:佣金提现、换卡申请等 - 实现完整的 GORM 模型和常量定义 - 添加数据库迁移脚本和详细文档 - 集成 OpenSpec 工作流工具(opsx 命令和 skills) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
249 lines
11 KiB
Markdown
249 lines
11 KiB
Markdown
# 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** 系统拒绝创建,返回错误信息"号卡订单必须关联号卡"
|