Files
junhong_cmp_fiber/openspec/specs/asset-historical-orders/spec.md
huang 80c6f6c756
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
主要变更:
- 新增 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>
2026-04-07 17:39:36 +08:00

4.4 KiB
Raw Blame History

Requirement: 查询资产本代历史订单

系统 SHALL 提供接口,让管理员查询某资产本代(当前世代)的全部历史订单,支持分页。

API 端点GET /api/admin/assets/:identifier/orders

请求参数

  • :identifier路径资产标识符ICCID 或 VirtualNo必填
  • pagequery页码默认 1
  • page_sizequery每页数量默认 20最大 100
  • include_previousquery是否包含前代订单布尔值默认 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

{
  "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=trueDEV-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 或不返回,节省带宽