feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s

主要变更:
- 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等)
- 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束
- 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析
- 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合
- 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝
- Excel 工具函数优化,前端导入指引文档同步更新
- 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no
- 更新 OpenAPI 文档及相关 DTO

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-07 17:39:36 +08:00
parent 7e489a19fb
commit 80c6f6c756
69 changed files with 3039 additions and 851 deletions

View File

@@ -26,7 +26,21 @@
### Requirement: 创建套餐购买订单
系统 SHALL 允许买家创建套餐购买订单。订单类型分为单卡购买和设备购买。创建前 MUST 验证购买权限和强充要求。**后台订单接口 MUST 支持 `payment_method` 字段wallet/offline根据支付方式自动设置 `is_purchase_on_behalf` 标识**。
系统 SHALL 允许买家创建套餐购买订单。**[BREAKING]** 请求体改为使用资产标识符identifier废弃 `iot_card_id`/`device_id` 主键字段;`order_type` 字段由系统根据 identifier 解析结果自动填入,请求体不再接受。创建前 MUST 验证购买权限和强充要求。**后台订单接口 MUST 支持 `payment_method` 字段wallet/offline根据支付方式自动设置 `is_purchase_on_behalf` 标识**。
**后台订单创建请求CreateAdminOrderRequest**
```json
{
"identifier": "string资产标识符ICCID 或 VirtualNo必填",
"package_ids": "[uint](套餐 ID 列表必填1-10 个)",
"payment_method": "stringwallet|offline必填"
}
```
**废弃字段**
- `iot_card_id`(原用于单卡购买)
- `device_id`(原用于设备购买)
- `order_type`(改为系统自动推断)
**支付方式和订单类型映射**
- `payment_method = "wallet"`:扣买家钱包,`is_purchase_on_behalf = false`(普通订单)
@@ -72,6 +86,22 @@
- **WHEN** 买家尝试购买已下架的套餐
- **THEN** 系统返回错误 "该套餐已下架"
#### Scenario: 使用 VirtualNo 创建设备订单
- **WHEN** 管理员请求体携带 `{ identifier: "DEV-001", package_ids: [5], payment_method: "wallet" }`
- **THEN** 系统解析 identifier 为设备,自动设置 `order_type = device`,创建设备购买订单
#### Scenario: 使用 ICCID 创建单卡订单
- **WHEN** 管理员请求体携带 `{ identifier: "898600XXXXX", package_ids: [5], payment_method: "wallet" }`
- **THEN** 系统解析 identifier 为独立 IoT 卡(`is_standalone = true`),自动设置 `order_type = single_card`,创建单卡购买订单
#### Scenario: 使用绑定设备的卡 ICCID 创建订单被拒
- **WHEN** 管理员请求体携带 `{ identifier: "898600XXXXX" }` 但该卡 `is_standalone = false`
- **THEN** 系统返回错误"该卡已绑定设备,请前往设备页面购买套餐",订单不创建
#### Scenario: identifier 不存在
- **WHEN** 请求的 identifier 无法解析到任何资产
- **THEN** 返回错误"资产不存在",订单不创建
---
### Requirement: 查询订单列表
@@ -224,3 +254,36 @@
#### Scenario: 后台管理端可为未实名载体购买
- **WHEN** 管理员通过后台为未实名载体购买套餐
- **THEN** 系统创建订单成功PackageUsage status=0, pending_realname_activation=true
---
### Requirement: 订单响应包含资产标识符
订单响应OrderResponseSHALL 包含资产标识符字段,前端无需额外请求即可展示资产信息。
**新增响应字段**
- `asset_identifier`下单时资产的标识符快照ICCID 或 VirtualNo
- `asset_type`:资产类型(`single_card` 对应 `iot_card``device` 对应 `device`
#### Scenario: 查看订单详情时展示资产标识符
- **WHEN** 管理员查询订单详情 `GET /api/admin/orders/:id`
- **THEN** 响应中包含 `asset_identifier`(如 "DEV-001")和 `asset_type`(如 "device"
- **THEN** 即使原资产已被删除,`asset_identifier` 仍可读(快照字段)
---
### Requirement: 订单列表支持按资产标识符过滤
系统 SHALL 在订单列表查询OrderListRequest中支持 `identifier` 过滤参数,按照资产标识符精确匹配订单(匹配 `asset_identifier` 快照字段)。
#### Scenario: 按 ICCID 查询该卡的历史订单
- **WHEN** 管理员查询 `GET /api/admin/orders?identifier=898600XXXXX`
- **THEN** 返回 `asset_identifier = "898600XXXXX"` 的所有订单(分页)
#### Scenario: 按设备 VirtualNo 查询该设备的历史订单
- **WHEN** 管理员查询 `GET /api/admin/orders?identifier=DEV-001`
- **THEN** 返回 `asset_identifier = "DEV-001"` 的所有订单(分页)
#### Scenario: 标识符无匹配订单
- **WHEN** 管理员查询不存在订单的 identifier
- **THEN** 返回空列表(`items: []``total: 0`),不返回错误