# Phase 03.1: 设备 sync-info 读取链路修复 - Context

**Gathered:** 2026-03-28
**Status:** Ready for planning

<domain>
## Phase Boundary

Phase 3 完成了设备数据的**写入侧**（sync-info 调用 + DB 字段写入 + is_current 更新），但遗漏了**读取侧**的 DTO 映射和设备实时数据的架构对齐。

本 Phase 修复两类缺口：

1. **静态 DTO 映射缺口**（DB 字段已存储但未通过 API 返回）
   - `toDeviceResponse` 5 个字段未映射 → 影响设备管理列表/详情
   - `ListBindings` IsCurrent 未映射 → 影响设备绑卡列表接口
   - `BoundCardInfo` IsCurrent 未映射（2 处）→ 影响资产 Resolve/Refresh 响应
   - `AssetResolveResponse` 缺 5 个 DB 缓存字段 → 影响 admin 资产解析接口

2. **设备实时数据架构对齐**（统一视图的上游属性必须实时查询）
   - 设备上游属性（在线状态/信号/电量/WiFi/当前卡）具有实时特性，存 DB 无意义，必须实时从 Gateway 拉取
   - `GetRealtimeStatus` 对 device 类型实时调用 Gateway sync-info
   - C 端 `GetAsset` 的 `DeviceRealtimeInfo` stub 替换为真实 Gateway 数据

**不在本 Phase 范围：**
- 卡资产的上游属性（走轮询系统，已由 DB 存储，无需改动）
- 设备管理列表（展示 DB 缓存字段即可，不需要实时 Gateway 调用）
- 设备 Refresh 流程（已有 updateDeviceFromSyncInfo，保持现状）

</domain>

<decisions>
## Implementation Decisions

### Plan 分组
- **D-01:** 全部修复合并为 **1 个 Plan**。所有变更同属"DTO 映射缺口 + 实时数据架构对齐"，无依赖关系需拆分。

### 静态 DTO 缺口修复

- **D-02:** `device/service.go:toDeviceResponse()` 补全 5 个 DB 缓存字段：
  - `OnlineStatus`、`LastOnlineTime`、`SoftwareVersion`、`SwitchMode`、`LastGatewaySyncAt`
  - 这些字段对设备管理列表（List）仍有意义（展示 DB 缓存值），保留此映射

- **D-03:** `device/binding.go:ListBindings()` 补全 `IsCurrent` 字段映射
  - `DeviceCardBindingResponse.IsCurrent` 直接从 `binding.IsCurrent` 取值

- **D-04:** `asset/service.go` 中 2 处 `BoundCardInfo` 构建（Resolve path + Refresh path）补全 `IsCurrent`
  - 实现方式：在 `bindings` 遍历中同时构建 `isCurrentMap[b.IotCardID] = b.IsCurrent`（与 `slotMap` 同构）
  - 构建 `BoundCardInfo` 时追加 `IsCurrent: isCurrentMap[c.ID]`

- **D-12:** `asset_dto.go:AssetResolveResponse` 新增 5 个 DB 缓存字段，同步更新 `asset/service.go:buildDeviceResolveResponse()` 映射
  - 新增字段：`OnlineStatus int`、`LastOnlineTime *time.Time`、`SoftwareVersion string`、`SwitchMode string`、`LastGatewaySyncAt *time.Time`
  - 这些是 DB 存储值（Phase 3 写入，上次 sync-info 的快照），不触发 Gateway 调用
  - 适用场景：admin 资产 Resolve 接口（`/api/admin/assets/resolve/:identifier`）快速展示设备上次已知状态
  - 与 D-07（GetRealtimeStatus 实时 Gateway 调用）互补：Resolve = 快速快照，Realtime = 精确实时

### 设备实时数据架构原则

- **D-05:** 设备上游属性（在线/信号/电量/WiFi/当前卡）**不缓存，必须实时查询 Gateway**。原因：设备具有在线/离线实时特性，存 DB 的值在设备离线后立即过时，无法代表真实状态。
  - 对比：卡资产的上游属性通过轮询系统有规律地同步到 DB，查询 DB 有意义。

- **D-06:** Gateway 调用失败时（设备离线/超时/IMEI 为空）**返回部分数据**：`DeviceRealtime` 字段为 null，资产基础信息正常返回。不抛错误，与卡资产处理保持一致。

### GetRealtimeStatus 改造

- **D-07:** `asset/service.go:GetRealtimeStatus()` 对 device 类型新增 Gateway 实时调用：
  - 调用 `s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{IMEI: device.IMEI})`（gatewayClient 已注入）
  - 成功：将响应映射到新增的 `AssetRealtimeStatusResponse.DeviceRealtime *DeviceGatewayInfo` 字段
  - 失败：仅记录 Warn 日志，`DeviceRealtime` 返回 null

### DTO 结构

- **D-08:** B 端和 C 端使用**独立结构体**，不共用。原因：未来管理端可能展示 C 端不展示的字段（如 IMSI、DL/UL stats、LAN/WAN IP 等技术字段）。
  - **B 端**：新建 `DeviceGatewayInfo` struct，位于 `internal/model/dto/asset_dto.go`
  - **C 端**：保留现有 `DeviceRealtimeInfo` struct，位于 `internal/model/dto/client_asset_dto.go`
  - `AssetRealtimeStatusResponse` 新增 `DeviceRealtime *DeviceGatewayInfo` 字段

- **D-09:** `DeviceGatewayInfo`（B 端）至少包含以下字段（参考 Gateway sync-info 文档完整映射）：
  - 关键：`OnlineStatus`、`BatteryLevel`、`Rsrp/Rsrq/Rssi/Sinr`（信号）、`MaxClients`（连接数）
  - WiFi：`SSID`、`WifiPassword`
  - 当前卡：`CurrentIccid`
  - 附加：`SoftwareVersion`、`SwitchMode`、`RunTime`、`DailyUsage`、`LastOnlineTime`

- **D-10:** `gateway.SyncDeviceInfoResp` 当前缺少字段，需补全：
  - 补充：`Rsrp int`、`Rsrq int`、`Sinr int`、`WifiPassword string`、`IPAddress string`
  - 补充：`WANIP string`、`LANIP string`、`MaxClients int`、`ConnectTime string`
  - 补充：`Status int`（设备状态）、`IMSI string`、`ULStats string`、`DLStats string`
  - 补充：`LimitSpeed int`、`SyncInterval int`、`MACAddress string`

### C 端 DeviceRealtime stub 替换

- **D-11:** C 端 `client_asset.go:GetAsset()` 中对 device 类型的处理：
  - 删除 `buildMockDeviceRealtime()` 调用
  - 改为调用 `h.assetService.GetRealtimeStatus("device", deviceID)` 获取实时数据
  - 将返回的 `AssetRealtimeStatusResponse.DeviceRealtime (*DeviceGatewayInfo)` 映射到 `ClientAssetResponse.DeviceRealtime (*DeviceRealtimeInfo)`
  - Gateway 失败时 `resp.DeviceRealtime = nil`（已处理，不额外报错）

### Agent's Discretion

- `DeviceGatewayInfo` B 端结构体的字段是否包含所有 Gateway 字段或只包含关键字段，agent 可参考 `DeviceRealtimeInfo`（C 端）的完整字段集合决定
- `toDeviceResponse` 中 `GetRealtimeStatus` 调用的冷却 Key 策略（是否需要防止高频调用）由 agent 参考现有 Refresh 冷却 Key 逻辑自行决定

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Gateway 接口文档
- `docs/第三方文档/gateway设备详情同步接口.md` — sync-info 完整字段说明（data 字段映射表，包含 rsrp/rsrq/sinr/wifi_password 等当前 SyncDeviceInfoResp 缺失的字段）

### Phase 3 上下文（继承决策）
- `.planning/phases/03-device-system/03-CONTEXT.md` — Phase 3 所有决策（D-06：结构体位置；D-13/14/15：updateDeviceFromSyncInfo 实现细节；D-08/09：存储字段范围）

### 核心修改文件
- `internal/service/device/service.go` — `toDeviceResponse()`：补全 5 个字段
- `internal/service/device/binding.go` — `ListBindings()`：补全 IsCurrent
- `internal/service/asset/service.go` — `GetRealtimeStatus()`：新增 Gateway 调用；Resolve/Refresh 两处 BoundCardInfo：补全 IsCurrent
- `internal/model/dto/asset_dto.go` — 新增 `DeviceGatewayInfo`；`AssetRealtimeStatusResponse` 新增 `DeviceRealtime` 字段；`AssetResolveResponse` 新增 5 个 DB 缓存字段（D-12）
- `internal/model/dto/device_dto.go` — 已有字段，无需新增（确认映射即可）
- `internal/gateway/models.go` — `SyncDeviceInfoResp` 补全缺失字段
- `internal/handler/app/client_asset.go` — 删除 `buildMockDeviceRealtime()` stub，改为调用服务 + 映射

### 当前 SyncDeviceInfoResp 位置
- `internal/gateway/models.go:147` — 当前结构（缺 Rsrp/Rsrq/Sinr/WifiPassword/LANIP/MaxClients 等）

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `internal/service/asset/service.go:gatewayClient` — 已注入，`GetRealtimeStatus` 可直接使用
- `internal/service/asset/service.go:updateDeviceFromSyncInfo()` — Phase 3 已实现，展示了 SyncDeviceInfoResp → DB 的映射模式，可参考作为 Gateway 响应 → DTO 映射的参考
- `internal/gateway/device.go:SyncDeviceInfo()` — 已实现（Phase 3），直接复用
- `internal/model/dto/client_asset_dto.go:DeviceRealtimeInfo` — 完整的 C 端实时信息结构（46 字段），对应所有 Gateway 字段，作为 `DeviceGatewayInfo` 的参考

### Established Patterns
- isCurrentMap 模式：参考 `slotMap` 在 `asset/service.go` 的构建方式，`isCurrentMap[b.IotCardID] = b.IsCurrent`
- Gateway 失败不阻断主流程：`logger.Warn("...")`，继续返回部分数据
- B 端 DTO 字段均需 `description` tag（参考 `asset_dto.go` 现有字段）

### Integration Points
- `asset/service.go:GetRealtimeStatus()` — device case 追加 Gateway 调用入口（现只有 `getDeviceProtectStatus` 和 bindings 查询）
- `handler/app/client_asset.go:GetAsset()` — 约第 187-191 行，device 类型的 `resp.DeviceRealtime = buildMockDeviceRealtime()` 替换入口
- `internal/model/dto/asset_dto.go:AssetRealtimeStatusResponse` — 新增 `DeviceRealtime *DeviceGatewayInfo` 字段位置

</code_context>

<specifics>
## Specific Ideas

- 用户明确：设备上游属性（在线/信号/电量/WiFi/当前卡）**不应存 DB**，必须实时查询。现有的 `online_status` 等 DB 字段对于设备管理列表展示仍有价值（缓存上次同步值）。
- 用户明确：关键实时字段优先级 — 在线状态 > 信号强度 > 连接数 > 电量 > WiFi 名称 + WiFi 密码 > 当前使用卡 ICCID
- `DeviceGatewayInfo`（B 端）与 `DeviceRealtimeInfo`（C 端）结构独立，为未来管理端展示更多技术字段（IMSI、LAN/WAN IP、DL/UL stats 等）预留空间

</specifics>

<deferred>
## Deferred Ideas

- 设备 Refresh 接口的语义重新评估（用户提到"刷新接口对设备而言意义不大"）——当前 Refresh 仍保留（写回 DB 供管理列表使用），但未来可能弃用 —— Phase 4+ 评估
- `GetRealtimeStatus` 的防高频调用冷却 Key（若 C 端每次进入详情页都触发 Gateway 调用，需评估频率）—— 留待 Phase 4 运营优化

</deferred>

---

*Phase: 03.1-sync-info-phase-3-dto*
*Context gathered: 2026-03-28 (updated: 2026-03-28, added D-12)*
