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

@@ -12,10 +12,12 @@
**API 端点**: `GET /api/admin/assets/resolve/:identifier`
**查找顺序**:
1. 先在 `tb_device` 表查找(匹配 `virtual_no = ? OR imei = ? OR sn = ?`
2. 未命中则在 `tb_iot_card` 表查找(匹配 `virtual_no = ? OR iccid = ? OR msisdn = ?`
3. 两表均未命中 → 返回 HTTP 404
**查找顺序(更新后)**:
1. **主路径**:查 `tb_asset_identifier` WHERE identifier = ? → 命中则得到 asset_type + asset_id直接查对应表取完整记录
2. **Fallback 路径**(注册表未命中时):
- 先查 `tb_device`(匹配 `virtual_no = ? OR imei = ? OR sn = ?`
- 未命中则查 `tb_iot_card`(匹配 `virtual_no = ? OR iccid = ? OR msisdn = ?`
3. 两条路径均未命中 → 返回 HTTP 404
4. 找到后应用数据权限过滤,无权限 → 返回 HTTP 403
**数据权限规则**:
@@ -28,8 +30,11 @@
*通用字段device 和 card 均有)*:
- `asset_type`: 资产类型(`"device"``"card"`
- `asset_id`: 资产主键 ID
- `identifier`: 本次查询所用的标识符(原样回传)
- `virtual_no`: 虚拟号(设备/卡均使用此字段)
- `status`: 资产状态(整型)
- `asset_status`: 业务状态1-在库 2-已销售 3-已换货 4-已停用)
- `generation`: 资产世代编号
- `batch_no`: 批次号
- `shop_id`: 所属店铺 ID平台库存时为空
- `shop_name`: 所属店铺名称
@@ -136,3 +141,20 @@
- **WHEN** 管理员查询一台没有购买任何套餐的设备
- **THEN** `current_package = ""``package_total_mb = 0``package_used_mb = 0``package_remain_mb = 0`
#### Scenario: 通过注册表主路径精确解析
- **WHEN** 管理员输入 identifier 为已存在于 `tb_asset_identifier` 的 VirtualNo 或 ICCID
- **THEN** 系统单次查询注册表命中,直接查对应表返回完整资产信息,响应时间 < 50ms
#### Scenario: Fallback 路径解析 IMEI
- **WHEN** 管理员输入 identifier 为设备 IMEI不在注册表中
- **THEN** 注册表未命中,系统 fallback 查 tb_device 的 imei 字段,找到后返回资产信息
- **THEN** 响应中 `identifier` 字段原样回传该 IMEI 值
#### Scenario: Fallback 路径解析 MSISDN
- **WHEN** 管理员输入 identifier 为 IoT 卡的手机号MSISDN
- **THEN** 注册表未命中fallback 查 tb_iot_card 的 msisdn 字段
- **THEN** 若存在多张卡的 MSISDN 相同返回第一条匹配记录MSISDN 非唯一,存在歧义,记录 warn 日志)