feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
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:
106
openspec/specs/asset-historical-orders/spec.md
Normal file
106
openspec/specs/asset-historical-orders/spec.md
Normal file
@@ -0,0 +1,106 @@
|
||||
### Requirement: 查询资产本代历史订单
|
||||
|
||||
系统 SHALL 提供接口,让管理员查询某资产本代(当前世代)的全部历史订单,支持分页。
|
||||
|
||||
**API 端点**:`GET /api/admin/assets/:identifier/orders`
|
||||
|
||||
**请求参数**:
|
||||
- `:identifier`(路径):资产标识符(ICCID 或 VirtualNo),必填
|
||||
- `page`(query):页码,默认 1
|
||||
- `page_size`(query):每页数量,默认 20,最大 100
|
||||
- `include_previous`(query):是否包含前代订单,布尔值,默认 false
|
||||
|
||||
**权限规则**:
|
||||
- 代理用户:只能查看数据权限范围内资产的订单
|
||||
- 平台/超管:可查看所有资产订单
|
||||
- 企业账号:不支持此接口(返回 403)
|
||||
|
||||
#### Scenario: 查询本代订单(默认)
|
||||
- **WHEN** 管理员请求 `GET /api/admin/assets/DEV-001/orders`
|
||||
- **THEN** 返回该资产(当前世代)的订单列表,按创建时间倒序,支持分页
|
||||
- **THEN** 响应中每条订单包含 `generation` 字段,值为资产当前世代编号
|
||||
|
||||
#### Scenario: 资产无订单
|
||||
- **WHEN** 管理员查询一个从未购买过套餐的资产
|
||||
- **THEN** 返回 `{ items: [], total: 0, page: 1 }`,不返回错误
|
||||
|
||||
#### Scenario: identifier 不存在
|
||||
- **WHEN** 请求的 identifier 无法解析到任何资产
|
||||
- **THEN** 返回 HTTP 404,错误消息"资产不存在"
|
||||
|
||||
#### Scenario: 代理查询无权限资产
|
||||
- **WHEN** 代理用户请求不属于其数据权限范围的资产订单
|
||||
- **THEN** 返回 HTTP 403,错误消息"无权限操作该资源或资源不存在"
|
||||
|
||||
### Requirement: 查询资产跨代历史订单(含前代)
|
||||
|
||||
当 `include_previous=true` 时,系统 SHALL 通过换货链追溯前代资产,返回前代订单,并在响应中区分世代来源。
|
||||
|
||||
**追溯逻辑**:
|
||||
1. 通过 `ExchangeOrder.new_asset_id` 逆向查找当前资产的换货来源
|
||||
2. 得到前代的 `old_asset_identifier`,查询该标识符的订单
|
||||
3. 递归追溯,最多向前 10 代(安全上限)
|
||||
|
||||
**响应结构(AssetOrdersResponse)**:
|
||||
```json
|
||||
{
|
||||
"current_generation": {
|
||||
"generation": 2,
|
||||
"identifier": "DEV-001",
|
||||
"asset_type": "device",
|
||||
"total": 5,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"items": [ ...订单列表... ]
|
||||
},
|
||||
"previous_generations": [
|
||||
{
|
||||
"generation": 1,
|
||||
"identifier": "DEV-OLD-001",
|
||||
"asset_type": "device",
|
||||
"exchange_no": "EXC20260101XXXXXX",
|
||||
"exchanged_at": "2026-01-01T00:00:00Z",
|
||||
"total": 3,
|
||||
"items": [ ...前代订单列表(最近20条)... ]
|
||||
}
|
||||
],
|
||||
"truncated": false
|
||||
}
|
||||
```
|
||||
|
||||
**每条订单项(AssetOrderItem)包含**:
|
||||
- `order_no`:订单号
|
||||
- `order_type`:订单类型(single_card / device)
|
||||
- `payment_status`:支付状态
|
||||
- `payment_status_text`:支付状态文本
|
||||
- `total_amount`:订单金额(分)
|
||||
- `payment_method`:支付方式
|
||||
- `paid_at`:支付时间(可空)
|
||||
- `generation`:订单所属资产世代
|
||||
- `items`:套餐明细列表
|
||||
- `created_at`:订单创建时间
|
||||
|
||||
#### Scenario: 查询换货后资产的全代际订单
|
||||
- **WHEN** 管理员请求 `GET /api/admin/assets/DEV-001/orders?include_previous=true`,DEV-001 是换货后的新设备(第2代),原设备为 DEV-OLD-001(第1代)
|
||||
- **THEN** `current_generation` 包含 DEV-001 本代的订单(generation=2)
|
||||
- **THEN** `previous_generations[0]` 包含 DEV-OLD-001 的订单(generation=1),附带换货单号和换货时间
|
||||
- **THEN** `truncated=false`(未超出追溯上限)
|
||||
|
||||
#### Scenario: 资产本身就是第一代(无前代)
|
||||
- **WHEN** 管理员请求带 `include_previous=true`,但该资产从未经过换货
|
||||
- **THEN** `previous_generations` 为空数组 `[]`
|
||||
- **THEN** `current_generation` 正常返回本代订单
|
||||
|
||||
#### Scenario: 换货链超过追溯上限
|
||||
- **WHEN** 换货链深度超过 10 代
|
||||
- **THEN** 追溯在第 10 代截断,`truncated=true`
|
||||
- **THEN** 已追溯到的前代数据正常返回
|
||||
|
||||
#### Scenario: 前代订单分页
|
||||
- **WHEN** 请求带 `include_previous=true`
|
||||
- **THEN** 分页参数(page/page_size)只对 `current_generation` 的订单生效
|
||||
- **THEN** 前代订单每代最多返回 20 条(不支持前代内分页)
|
||||
|
||||
#### Scenario: 无 include_previous 时响应不含前代字段
|
||||
- **WHEN** 管理员请求不带 `include_previous=true`(或传 false)
|
||||
- **THEN** 响应结构中 `previous_generations` 字段为 null 或不返回,节省带宽
|
||||
Reference in New Issue
Block a user