Files
junhong_cmp_fiber/openspec/changes/archive/2026-04-07-asset-historical-orders/design.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

3.7 KiB
Raw Blame History

Context

系统中换货流程已有完整的数据记录:

  • ExchangeOrder 表存有 old_asset_idold_asset_identifiernew_asset_idnew_asset_identifier,形成资产的"世代链"
  • Order 表通过提案一新增的 asset_identifier 快照字段,可直接按标识符查询订单
  • Device/IotCard 表有 generation 字段记录当前世代编号

当前缺失的是:把这三张表的信息串联起来,给管理员呈现"这个资产在各世代的订单全貌"。

Goals / Non-Goals

Goals:

  • 新增 GET /api/admin/assets/:identifier/orders 接口,默认返回本代订单
  • 支持 include_previous=true 参数,追溯换货链返回前代订单(明确标注世代)
  • 分页支持

Non-Goals:

  • C 端不暴露跨代订单C 端用户视角只关心当前资产本代)
  • 修改订单数据本身(只读接口)
  • 跨代订单的聚合统计(如历代总充值额),留给未来迭代

Decisions

决策 1通过 ExchangeOrder 逆向追溯前代资产 ID

查询逻辑

给定 identifier当前资产
  1. 通过注册表/Resolve 得到 asset_type + asset_id当前代
  2. 查当前资产的订单WHERE asset_identifier = identifier直接走快照字段
  3. 若 include_previous=true
     a. 查 ExchangeOrder WHERE new_asset_id = asset_id找到换货记录
     b. 得到 old_asset_id + old_asset_identifier前代标识符
     c. 查前代订单WHERE asset_identifier = old_asset_identifier
     d. 递归至无更多前代(链式追溯,最多向前追溯 N 代,防止死循环)
  4. 合并结果,按世代分组,每条订单附加 generation 字段

理由:直接使用 asset_identifier 快照字段查询,无需 JOIN换货链通过 ExchangeOrder 逆向遍历,逻辑清晰。

决策 2响应结构按世代分组不跨代混合排序

选择:响应分为 current_generation(本代)和 previous_generations(前代数组)两个区块

备选方案:全部打平按时间排序。问题:管理员看到时间线上跳跃的世代可能产生困惑(订单时间早于资产创建时间)

理由:分区块展示逻辑清晰——管理员一眼能看出"这是这台设备自己的订单"vs"这是上一台的历史"

决策 3限制追溯深度防止换货链过长

最大追溯世代数为 10 代,超出则截断并在响应中说明(truncated: true)。实际业务中换货链极少超过 3 代,此限制仅为安全保障。

决策 4本接口依赖提案一的 asset_identifier 快照字段

若提案一未完成Order 表无 asset_identifier 字段),本接口降级为通过 iot_card_id/device_id 查询(只支持本代,不支持 include_previous。实际按提案一完成后实现。

Risks / Trade-offs

风险 缓解措施
换货链查询产生多次 DB 往返 每代一次 ExchangeOrder 查询 + 一次 Order 查询链深度有限≤10总查询次数可控可加 Redis 缓存换货链
旧订单 asset_identifier 为空(提案一之前创建的订单) 本接口限定只查有 asset_identifier 快照的订单;旧订单通过 iot_card_id/device_id fallback 查询补充,明确标注"旧格式订单"
include_previous 导致响应体过大 分页仅对本代订单生效;前代订单默认返回最近 20 条,不支持前代分页(前代是历史归档,数量有限)

Migration Plan

  1. 依赖提案一完成(Order.asset_identifier 字段存在)
  2. 实现 exchange_order_store.FindChainByNewAssetID()
  3. 实现 asset_service.GetOrders()
  4. 注册路由和 Handler
  5. 更新文档生成器

Open Questions