---
phase: 03-device-system
plan: "02"
subsystem: device
tags: [service, gateway, sync-info, store, bootstrap]
dependency_graph:
  requires:
    - Plan 03-01（tb_device 5字段 + tb_device_sim_binding.is_current + Gateway SyncDeviceInfo 方法）
  provides:
    - DeviceSimBindingStore.UpdateIsCurrentByDeviceID（事务原子更新 is_current）
    - asset.Service.updateDeviceFromSyncInfo（私有函数，同步 5 个设备字段 + is_current）
    - asset.Service.Refresh device 分支接入 SyncDeviceInfo（nil guard + Warn 日志）
    - asset.Service 注入 gatewayClient
  affects:
    - DEVICE-04 需求（设备 Refresh + 详情接入 sync-info）
tech_stack:
  added: []
  patterns:
    - GORM Transaction 两步原子更新模式
    - Service nil guard 可选依赖注入模式
    - ISO 8601 字符串 → time.Time（time.RFC3339）
key_files:
  created: []
  modified:
    - internal/store/postgres/device_sim_binding_store.go
    - internal/service/asset/service.go
    - internal/bootstrap/services.go
decisions:
  - "gatewayClient 使用 nil guard（而非断言非 nil），允许 bootstrap 传入 nil 依然安全运行"
  - "sync-info 失败只记 Warn 日志，不阻断 Refresh 主流程（非关键步骤，D-15 决策）"
  - "UpdateIsCurrentByDeviceID 事务两步：先全清 false，再按 ICCID 设 true（原子保证唯一 is_current）"
metrics:
  duration: "10min"
  completed_date: "2026-03-28"
  tasks: 2
  files: 3
---

# Phase 03 Plan 02: Gateway sync-info 接入 Refresh 流程 Summary

**一句话总结：** 在设备 Refresh 流程末尾接入 Gateway SyncDeviceInfo，通过 updateDeviceFromSyncInfo 原子更新在线状态、固件版本等 5 个设备字段及 is_current 当前卡标识（DEVICE-04）。

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | DeviceSimBindingStore 新增 UpdateIsCurrentByDeviceID 方法 | `ba1886c` | device_sim_binding_store.go |
| 2 | Asset Service 注入 gatewayClient + 实现 updateDeviceFromSyncInfo + Refresh 接入 sync-info | `15dbf8d` | service.go, services.go |

## Artifacts Delivered

### Store 层

- **UpdateIsCurrentByDeviceID**：事务两步原子更新。第一步将设备所有活跃绑定的 `is_current` 清为 false；第二步通过子查询定位 `current_iccid` 对应的 `iot_card_id`，将该绑定设为 `is_current = true`。`currentIccid` 为空时仅清空。

### Service 层

- **Service 结构体**：新增 `gatewayClient *gateway.Client` 字段（允许 nil）
- **New() 构造函数**：新增 `gatewayClient *gateway.Client` 参数
- **Refresh device 分支**：在设置冷却 Key 之后、`GetRealtimeStatus` 返回之前，增加 sync-info 调用逻辑（nil guard 保护，失败只记 Warn 日志）
- **updateDeviceFromSyncInfo 私有函数**：
  1. 解析 `last_online_time`（ISO 8601 → `*time.Time`，`time.RFC3339`）
  2. GORM Updates 更新 5 个字段：`online_status`、`software_version`、`switch_mode`、`last_gateway_sync_at`、`last_online_time`（非空时）
  3. 调用 `UpdateIsCurrentByDeviceID` 原子更新 `is_current`

### Bootstrap 层

- **services.go**：`asset.New()` 调用末尾追加 `deps.GatewayClient` 参数

## Deviations from Plan

None — 计划完全按预期执行。

## Verification

- ✅ `go build ./...` 零新增编译错误
- ✅ `updateDeviceFromSyncInfo` 方法存在于 `internal/service/asset/service.go`
- ✅ `Refresh` device 分支含 `gatewayClient.SyncDeviceInfo()` 调用（nil guard + Warn 日志）
- ✅ `UpdateIsCurrentByDeviceID` 存在于 `internal/store/postgres/device_sim_binding_store.go`
- ✅ `internal/bootstrap/services.go` asset.New() 传入了 `deps.GatewayClient`
- ✅ sync-info 失败时只记录 Warn，不影响 Refresh 主流程

## Self-Check: PASSED
