--- 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