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

@@ -0,0 +1,81 @@
## 1. 数据库迁移:全局标识符注册表
- [x] 1.1 创建迁移文件:`tb_asset_identifier`identifier UNIQUE、asset_type、asset_id、created_at建立 `idx_asset_identifier_asset(asset_type, asset_id)` 索引
- [x] 1.2 执行迁移,验证表结构和唯一约束正确
## 2. 数据层AssetIdentifier 模型与 Store
- [x] 2.1 创建 `internal/model/asset_identifier.go`:定义 `AssetIdentifier` 结构体(含 GORM 标签、TableName
- [x] 2.2 创建 `internal/store/postgres/asset_identifier_store.go`:实现 `Register(ctx, identifier, assetType, assetID)`(写入注册表)、`FindByIdentifier(ctx, identifier)`(精确查询)、`DeleteByAsset(ctx, assetType, assetID)`(删除资产时清理)
- [x] 2.3 在 `internal/bootstrap/stores.go` 注入 `AssetIdentifierStore`
## 3. 数据层Order 模型新增 asset_identifier 字段
- [x] 3.1 创建迁移文件:`tb_order` 表增加 `asset_identifier VARCHAR(100)` 字段
- [x] 3.2 更新 `internal/model/order.go`:新增 `AssetIdentifier string` 字段(含 GORM 标签和注释)
- [x] 3.3 更新 `internal/store/postgres/order_store.go`:列表查询新增 `identifier` 过滤条件(精确匹配 `asset_identifier`
## 4. 服务层Resolve 改走注册表
- [x] 4.1 更新 `internal/service/asset/service.go``Resolve` 方法优先查 `AssetIdentifierStore.FindByIdentifier()`,命中则按 asset_type 查对应表;未命中 fallback 原有逻辑
- [x] 4.2 在 `AssetService` 中注入 `AssetIdentifierStore` 依赖
## 5. 服务层purchase_validation 增加 is_standalone 校验
- [x] 5.1 更新 `internal/service/purchase_validation/service.go`:在 `ValidateCardPurchase()` 中增加 `if !card.IsStandalone { return error "该卡已绑定设备,请前往设备页面购买套餐" }` 校验(在套餐校验之前执行)
## 6. 服务层:资产创建/删除时同步注册表
- [x] 6.1 更新设备导入任务 `internal/task/device_import.go`:创建设备成功后在同一事务内调用 `AssetIdentifierStore.Register()` 注册 VirtualNo若冲突则拒绝该行
- [x] 6.2 更新 IoT 卡导入任务 `internal/task/iot_card_import.go`:创建卡成功后注册 ICCID必填和 VirtualNo如有到注册表冲突则拒绝该行
- [x] 6.3 更新设备/IoT 卡的软删除逻辑:删除时调用 `AssetIdentifierStore.DeleteByAsset()` 清理注册表
## 7. 服务层:订单创建改用 identifier
- [x] 7.1 更新 `internal/service/order/service.go`B 端):`Create()` 方法接收 `identifier` 字符串,内部调用 `AssetService.Resolve()` 解析资产,不再接受 `iot_card_id`/`device_id`;写入订单时填入 `asset_identifier` 快照字段
- [x] 7.2 更新 `internal/service/client_order/service.go`C 端):同上,接收 `identifier`,解析后写入 `asset_identifier` 快照
## 8. DTO 层:更新请求/响应结构
- [x] 8.1 更新 `internal/model/dto/order_dto.go``CreateAdminOrderRequest` 去掉 `IotCardID`/`DeviceID`/`OrderType`,新增 `Identifier string``OrderResponse` 新增 `AssetIdentifier string``AssetType string``OrderListRequest` 新增 `Identifier string`
- [x] 8.2 更新 `internal/model/dto/client_order_dto.go``CreateOrderRequest`):同上,去掉 `IotCardID`/`DeviceID`/`OrderType`,新增 `Identifier string`
- [x] 8.3 更新 `internal/model/dto/asset_dto.go``AssetTypeIDRequest` 废弃,`AssetResolveRequest` 确认 `Identifier` 字段已有;新增 `AssetIdentifierRequest`(路径参数,供操作接口使用)
- [x] 8.4 确认 `AssetResolveResponse` 新增 `Identifier` 字段(回传请求中使用的标识符)
## 9. Handler 层B 端资产操作接口更新
- [x] 9.1 更新 `internal/handler/admin/asset.go``RealtimeStatus`/`Refresh`/`Packages`/`CurrentPackage`/`UpdatePollingStatus`/`StopDevice`/`StartDevice`/`StopCard`/`StartCard` 均改为从路径参数 `:identifier` 读取标识符,调用 Resolve 解析后操作;合并 Stop/Start 卡和设备为统一的 `Stop`/`Start` handler内部按资产类型分支
- [x] 9.2 更新 `internal/handler/admin/asset.go`:新增 `Deactivate` handler合并 IoT 卡和设备停用,原 `/iot-cards/:id/deactivate``/devices/:id/deactivate`
- [x] 9.3 更新 `internal/handler/admin/device.go``Delete`/`ListCards`/`BindCard`/`UnbindCard` 改为从路径参数 `:virtual_no` 读取设备标识;`UnbindCard` 第二段路径参数改为 `:iccid`
## 10. Handler 层B 端订单 Handler 更新
- [x] 10.1 更新 `internal/handler/admin/order.go``Create` handler 解析 `CreateAdminOrderRequest.Identifier` 字段,移除 `IotCardID`/`DeviceID` 解析逻辑;`List` handler 支持 `identifier` query 参数传入 Store 过滤
## 11. Handler 层C 端订单 Handler 更新
- [x] 11.1 更新 `internal/handler/app/client_order.go``CreateOrder` handler 解析 `CreateOrderRequest.Identifier`,移除旧字段解析
## 12. 路由层:重注册所有受影响路由
- [x] 12.1 更新 `internal/routes/asset.go`:将所有 `:asset_type/:id` 路由改为 `:identifier`;合并卡/设备的 stop/start/deactivate 路由
- [x] 12.2 更新 `internal/routes/device.go``/:id` 改为 `/:virtual_no`;解绑路由 `/:id/cards/:cardId` 改为 `/:virtual_no/cards/:iccid`
- [x] 12.3 更新 `internal/routes/iot_card.go`:移除 `/:id/deactivate`(已并入 asset.go 的统一路由)
- [x] 12.4 更新 `internal/routes/order.go`:无路由变更,仅确认 Handler 已更新
## 13. 文档生成器更新
- [x] 13.1 更新 `cmd/api/docs.go``cmd/gendocs/main.go`:确保新 Handler 方法已注册Deactivate 等新合并 handler 需加入)
- [x] 13.2 执行 `go run cmd/gendocs/main.go` 重新生成 OpenAPI 文档,验证新路由和 DTO 正确体现
## 14. 验证
- [x] 14.1 构建验证:`go build ./...` 无编译错误
- [x] 14.2 LSP 诊断:对所有修改文件运行 lsp_diagnostics无错误/警告
- [ ] 14.3 接口验证(使用 PostgreSQL MCP 和 curl
- 通过 ICCID 调用 `GET /api/admin/assets/:identifier/packages` 返回正确套餐列表
- 通过 VirtualNo 调用 `POST /api/admin/assets/:identifier/stop` 执行停机
- 使用绑定设备的卡 identifier 创建订单,验证返回"该卡已绑定设备"错误
- 使用独立卡 identifier 创建订单成功OrderResponse 包含 `asset_identifier` 字段
- 并发写入同一 VirtualNo 到注册表,验证只有一条成功
- [ ] 14.4 订单列表验证:`GET /api/admin/orders?identifier=CARD-001` 返回该资产的订单