--- phase: 03.1-sync-info-phase-3-dto plan: "01" subsystem: asset/device tags: - gateway - dto - device - realtime dependency_graph: requires: - Phase 3 DEVICE-01~04(DB 字段写入已完成) provides: - 设备管理列表/详情返回 5 个 DB 缓存字段 - 设备绑卡列表返回 is_current 字段 - 资产 Resolve/Refresh BoundCardInfo 含 is_current - admin 资产解析返回 5 个 DB 缓存字段 - GetRealtimeStatus device 类型实时调用 Gateway - C 端 GetAssetInfo device_realtime 为真实 Gateway 数据 affects: - GET /api/admin/devices(新增 5 字段) - GET /api/admin/devices/:id/cards(新增 is_current) - GET /api/admin/assets/resolve/:identifier(新增 5 字段 + is_current) - GET /api/admin/assets/:type/:id/realtime(新增 device_realtime) - GET /api/c/v1/asset/info(device_realtime 从 mock 改为真实数据) tech_stack: added: [] patterns: - isCurrentMap 与 slotMap 同构模式(asset/service.go) - strPtr() 辅助函数(空字符串→nil,防止 JSON 冗余字段) - mapSyncRespToDeviceGatewayInfo() Gateway DTO → B端 DTO 映射 - mapDeviceGatewayInfoToClientInfo() B端 DTO → C端 DTO 映射 key_files: created: [] modified: - internal/gateway/models.go(+16 行:SyncDeviceInfoResp 补全 15 个字段) - internal/model/dto/asset_dto.go(+77 行:DeviceGatewayInfo 新增;AssetRealtimeStatusResponse.DeviceRealtime 新增;AssetResolveResponse 5 字段新增) - internal/service/device/service.go(+5 行:toDeviceResponse 补全 5 字段) - internal/service/device/binding.go(+1 行:ListBindings IsCurrent 补全) - internal/service/asset/service.go(+84 行:isCurrentMap 两处补全;Gateway 实时调用;mapSyncRespToDeviceGatewayInfo;strPtr) - internal/handler/app/client_asset.go(+113/-41 行:mock 替换为真实 Gateway 调用;mapDeviceGatewayInfoToClientInfo) decisions: - DeviceGatewayInfo(B端)与 DeviceRealtimeInfo(C端)独立结构,通过映射函数转换(per D-08) - SwitchMode 在 B端为 *string(Gateway 原始),C端为 *int,映射时使用 strconv.Atoi 转换 - strPtr() 在 asset/service.go 包内独立定义(order/service.go 已有同名函数但不同包) metrics: duration: 5min completed_date: "2026-03-28" tasks: 3 files_modified: 6 --- # Phase 03.1 Plan 01: 设备读取链路修复 + DTO 映射补全 Summary **一句话总结:** 补全 Phase 3 设备数据写入链路的读取侧缺口——6 个字段映射缺口修复 + Gateway sync-info 实时调用链路建立 + C端 mock 数据替换。 --- ## 完成的任务 ### Task 1:补全 Gateway 模型和 B 端 DTO 契约 **`internal/gateway/models.go`(第 162-183 行):** `SyncDeviceInfoResp` 新增 15 个缺失字段: - 信号:`Rsrp`、`Rsrq`、`Sinr` - WiFi:`WifiPassword` - 网络:`IPAddress`、`WANIP`、`LANIP` - 连接:`MaxClients`、`ConnectTime` - 设备:`Status`、`IMSI`、`ULStats`、`DLStats`、`LimitSpeed`、`SyncInterval` **`internal/model/dto/asset_dto.go`:** 1. 新增 `DeviceGatewayInfo` 结构体(第 138-188 行),49 字段,全部含 `description` tag(per D-08/D-09) 2. `AssetRealtimeStatusResponse` 新增 `DeviceRealtime *DeviceGatewayInfo` 字段(第 77 行)(per D-07/D-08) 3. `AssetResolveResponse` 新增 5 个设备 DB 缓存字段(第 44-49 行)(per D-12): - `OnlineStatus`、`LastOnlineTime`、`SoftwareVersion`、`SwitchMode`、`LastGatewaySyncAt` --- ### Task 2:修复静态 DTO 映射缺口 **`internal/service/device/service.go`(`toDeviceResponse()`,第 559-563 行):** 补全 5 个 DB 缓存字段赋值: ```go OnlineStatus: device.OnlineStatus, LastOnlineTime: device.LastOnlineTime, SoftwareVersion: device.SoftwareVersion, SwitchMode: device.SwitchMode, LastGatewaySyncAt: device.LastGatewaySyncAt, ``` **`internal/service/device/binding.go`(`ListBindings()`,第 60 行):** ```go IsCurrent: binding.IsCurrent, ``` **`internal/service/asset/service.go`(两处修复):** 1. `buildDeviceResolveResponse()`(第 124-128 行):新增 `isCurrentMap` + `IsCurrent` + 5 个 DB 缓存字段 2. `GetRealtimeStatus()` device case(第 274-279 行):新增 `isCurrentMap` + `IsCurrent` 两处均采用与现有 `slotMap` 同构的 `isCurrentMap` 模式: ```go isCurrentMap := make(map[uint]bool, len(bindings)) // ... isCurrentMap[b.IotCardID] = b.IsCurrent // ... IsCurrent: isCurrentMap[c.ID], ``` --- ### Task 3:建立设备实时 Gateway 调用链路 **`internal/service/asset/service.go`:** 1. `GetRealtimeStatus()` device case 末尾(第 291-312 行)新增 Gateway 实时调用: - `gatewayClient != nil` 安全检查 - IMEI 优先,无则用 SN(与 Refresh 逻辑一致) - 调用 `s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{CardNo: cardNo})` - 成功时调用 `mapSyncRespToDeviceGatewayInfo(syncResp)` 填充 `resp.DeviceRealtime` - 失败时仅记录 Warn 日志,`DeviceRealtime` 保持 nil(per D-06) 2. 新增 `mapSyncRespToDeviceGatewayInfo()`(第 330-369 行):Gateway resp → B端 DTO 映射 3. 新增 `strPtr()`(第 371-378 行):辅助函数,将空字符串转为 nil 指针 **`internal/handler/app/client_asset.go`:** 1. `GetAssetInfo()`(第 188-197 行):删除 `buildMockDeviceRealtime()` 调用,替换为: ```go realtimeResp, realtimeErr := h.assetService.GetRealtimeStatus( c.UserContext(), "device", resp.AssetID, ) if realtimeErr == nil && realtimeResp.DeviceRealtime != nil { resp.DeviceRealtime = mapDeviceGatewayInfoToClientInfo(realtimeResp.DeviceRealtime) } ``` 2. 删除 `buildMockDeviceRealtime()` 函数(原第 595-635 行) 3. 新增 `mapDeviceGatewayInfoToClientInfo()`(第 595-660 行):B端 DeviceGatewayInfo → C端 DeviceRealtimeInfo 映射 --- ## 编译验证 ``` go build ./... → 全量编译通过 go vet ./internal/gateway/... ./internal/model/dto/... ./internal/service/asset/... ./internal/service/device/... ./internal/handler/app/... → 无新增错误 ``` --- ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] SwitchMode 类型不匹配** - **Found during:** Task 3 - **Issue:** 计划中 `mapDeviceGatewayInfoToClientInfo` 的 `SwitchMode` 转换代码为 `v := int(*g.SwitchMode)`,但 `g.SwitchMode` 是 `*string`(B端 Gateway DTO 定义),`DeviceRealtimeInfo.SwitchMode` 是 `*int`(C端 DTO),直接转换会编译报错 - **Fix:** 使用 `strconv.Atoi(*g.SwitchMode)` 进行字符串→整型安全转换,解析失败时忽略(`SwitchMode` 保持 nil) - **Files modified:** `internal/handler/app/client_asset.go` - **Commit:** `a202b2d` --- ## Self-Check: PASSED | 检查项 | 结果 | |--------|------| | `internal/gateway/models.go` 存在 | ✅ FOUND | | `internal/model/dto/asset_dto.go` 存在 | ✅ FOUND | | `internal/service/device/service.go` 存在 | ✅ FOUND | | `internal/service/device/binding.go` 存在 | ✅ FOUND | | `internal/service/asset/service.go` 存在 | ✅ FOUND | | `internal/handler/app/client_asset.go` 存在 | ✅ FOUND | | 提交 `00dba42` 存在 | ✅ FOUND | | 提交 `3b54850` 存在 | ✅ FOUND | | 提交 `a202b2d` 存在 | ✅ FOUND | | `go build ./...` 编译通过 | ✅ 通过 |