---
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 ./...` 编译通过 | ✅ 通过 |
