From 3450c202362cbac7ae17844606b3db79b7cec3ae Mon Sep 17 00:00:00 2001 From: huang Date: Sat, 28 Mar 2026 13:12:42 +0800 Subject: [PATCH] =?UTF-8?q?docs(03.1):=20=E5=88=9B=E5=BB=BA=20Phase=2003.1?= =?UTF-8?q?=20=E8=AE=A1=E5=88=92=EF=BC=88=E8=AE=BE=E5=A4=87=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E9=93=BE=E8=B7=AF=E4=BF=AE=E5=A4=8D=20+=20DTO=20?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E8=A1=A5=E5=85=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/ROADMAP.md | 10 + .../03.1-01-PLAN.md | 637 ++++++++++++++++++ 2 files changed, 647 insertions(+) create mode 100644 .planning/phases/03.1-sync-info-phase-3-dto/03.1-01-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4ab9d52..542be16 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -78,6 +78,16 @@ Plans: --- +### Phase 03.1: 设备 sync-info 读取链路修复:补全 Phase 3 新增字段在查询接口的 DTO 映射缺口 (INSERTED) + +**Goal:** 补全 Phase 3 完成的设备数据写入链路在读取侧的缺口:修复 DTO 字段映射遗漏(5个 DB 缓存字段 + 2处 IsCurrent)+ 建立设备实时状态的 Gateway 查询链路(替换 mock → 真实 Gateway 数据) +**Requirements**: DEVICE-02, DEVICE-03, DEVICE-04 +**Depends on:** Phase 3 +**Plans:** 1 plans + +Plans: +- [ ] 03.1-01-PLAN.md — 补全静态 DTO 映射缺口 + 建立 GetRealtimeStatus Gateway 实时调用 + C 端 stub 替换(Wave 1) + ### Phase 4: 退款 + 支付 + 运营修复 **Goal**: 实现完整退款流程(申请→审批→佣金回扣),补全富友支付 JSAPI/小程序接口(替换留桩),修复运营逻辑断点(卡状态 3/4 触发、payment_status 枚举统一) **Depends on**: Phase 2(佣金链路稳定后再做退款佣金回扣) diff --git a/.planning/phases/03.1-sync-info-phase-3-dto/03.1-01-PLAN.md b/.planning/phases/03.1-sync-info-phase-3-dto/03.1-01-PLAN.md new file mode 100644 index 0000000..2c5b519 --- /dev/null +++ b/.planning/phases/03.1-sync-info-phase-3-dto/03.1-01-PLAN.md @@ -0,0 +1,637 @@ +--- +phase: 03.1-sync-info-phase-3-dto +plan: "01" +type: execute +wave: 1 +depends_on: [] +files_modified: + - internal/gateway/models.go + - internal/model/dto/asset_dto.go + - internal/service/device/service.go + - internal/service/device/binding.go + - internal/service/asset/service.go + - internal/handler/app/client_asset.go +autonomous: true +requirements: + - DEVICE-02 + - DEVICE-03 + - DEVICE-04 + +must_haves: + truths: + - "设备管理列表/详情接口返回 online_status、last_online_time、software_version、switch_mode、last_gateway_sync_at 字段(Phase 3 已写入 DB,现在需要映射到 DTO)" + - "设备绑卡列表接口(ListBindings)返回 is_current 字段,标识当前使用的卡" + - "资产 Resolve/Refresh 接口的 BoundCardInfo 包含 is_current 字段" + - "admin 资产解析接口(AssetResolveResponse)返回 5 个 DB 缓存字段:online_status/last_online_time/software_version/switch_mode/last_gateway_sync_at" + - "资产实时状态接口(GetRealtimeStatus)对 device 类型实时调用 Gateway sync-info,返回 DeviceRealtime 字段" + - "C 端 GetAsset 接口的 device_realtime 字段从真实 Gateway 数据填充,不再是 mock 数据" + - "Gateway 调用失败时(IMEI 为空/超时/离线)device_realtime 返回 null,资产基础信息正常返回" + artifacts: + - path: "internal/gateway/models.go" + provides: "SyncDeviceInfoResp 补全缺失字段(Rsrp/Rsrq/Sinr/WifiPassword/IMSI/LANIP/WANIP/MaxClients/ConnectTime/ULStats/DLStats/LimitSpeed/SyncInterval/MACAddress/Status)" + - path: "internal/model/dto/asset_dto.go" + provides: "DeviceGatewayInfo 新 B 端实时状态结构体;AssetRealtimeStatusResponse.DeviceRealtime 字段;AssetResolveResponse 5个 DB 缓存字段" + - path: "internal/service/device/service.go" + provides: "toDeviceResponse() 补全 OnlineStatus/LastOnlineTime/SoftwareVersion/SwitchMode/LastGatewaySyncAt 5个字段映射" + - path: "internal/service/device/binding.go" + provides: "ListBindings() 补全 IsCurrent 字段映射" + - path: "internal/service/asset/service.go" + provides: "buildDeviceResolveResponse() 补全 IsCurrent + 5个DB缓存字段;GetRealtimeStatus() device case 追加 Gateway 实时调用" + - path: "internal/handler/app/client_asset.go" + provides: "GetAsset() 删除 buildMockDeviceRealtime(),改为调用 assetService.GetRealtimeStatus() 并映射到 DeviceRealtimeInfo" + key_links: + - from: "internal/service/asset/service.go:GetRealtimeStatus()" + to: "internal/gateway/device.go:SyncDeviceInfo()" + via: "s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{CardNo: device.IMEI})" + pattern: "gatewayClient.*SyncDeviceInfo" + - from: "internal/handler/app/client_asset.go:GetAsset()" + to: "internal/service/asset/service.go:GetRealtimeStatus()" + via: "h.assetService.GetRealtimeStatus(\"device\", deviceID)" + pattern: "assetService.GetRealtimeStatus" + - from: "internal/service/asset/service.go:buildDeviceResolveResponse()" + to: "dto.BoundCardInfo.IsCurrent" + via: "isCurrentMap[b.IotCardID] = b.IsCurrent" + pattern: "isCurrentMap" +--- + + +补全 Phase 3 完成的设备数据写入链路在读取侧的缺口:修复 DTO 字段映射遗漏(静态缺口)+ 建立设备实时状态的 Gateway 查询链路(架构对齐)。 + +Purpose: Phase 3 已实现 sync-info 调用和 DB 字段写入,但 API 接口未返回这些字段,且 C 端设备实时状态仍是 mock 数据。本 Phase 完成"读取侧"闭环。 +Output: 6 个文件修改,设备管理/资产解析/C 端资产接口全部返回真实数据。 + + + +@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md +@$HOME/.config/opencode/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/03.1-sync-info-phase-3-dto/03.1-CONTEXT.md + +@internal/gateway/models.go +@internal/model/dto/asset_dto.go +@internal/model/dto/device_dto.go +@internal/model/dto/client_asset_dto.go +@internal/service/asset/service.go +@internal/service/device/service.go +@internal/service/device/binding.go +@internal/handler/app/client_asset.go + + + + + +```go +type SyncDeviceInfoResp struct { + DeviceID string `json:"device_id"` + DeviceName string `json:"device_name"` + IMEI string `json:"imei"` + CurrentIccid string `json:"current_iccid"` + DeviceType string `json:"device_type"` + SoftwareVersion string `json:"software_version"` + MacAddress string `json:"mac_address"` + SSID string `json:"ssid"` + WifiEnabled bool `json:"wifi_enabled"` + SwitchMode string `json:"switch_mode"` + RSSI string `json:"rssi"` + BatteryLevel *int `json:"battery_level"` + OnlineStatus int `json:"online_status"` + LastUpdateTime string `json:"last_update_time"` + LastOnlineTime string `json:"last_online_time"` + RunTime string `json:"run_time"` + DailyUsage string `json:"daily_usage"` + // 以下字段 D-10 要求补全: + // 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(已有 MacAddress,需确认 json tag) +} +``` + + +```go +type BoundCardInfo struct { + CardID uint `json:"card_id"` + ICCID string `json:"iccid"` + MSISDN string `json:"msisdn,omitempty"` + NetworkStatus int `json:"network_status"` + RealNameStatus int `json:"real_name_status"` + SlotPosition int `json:"slot_position,omitempty"` + IsCurrent bool `json:"is_current"` // ← 已定义,但 buildDeviceResolveResponse 未赋值 +} +``` + + +```go +type AssetRealtimeStatusResponse struct { + AssetType string `json:"asset_type"` + AssetID uint `json:"asset_id"` + NetworkStatus int `json:"network_status,omitempty"` + RealNameStatus int `json:"real_name_status,omitempty"` + CurrentMonthUsageMB float64 `json:"current_month_usage_mb,omitempty"` + LastSyncTime *time.Time `json:"last_sync_time,omitempty"` + DeviceProtectStatus string `json:"device_protect_status,omitempty"` + Cards []BoundCardInfo `json:"cards,omitempty"` + // 需新增:DeviceRealtime *DeviceGatewayInfo `json:"device_realtime,omitempty"` +} +``` + + +```go +type DeviceResponse struct { + // ... 其他字段已有 + OnlineStatus int `json:"online_status"` + LastOnlineTime *time.Time `json:"last_online_time,omitempty"` + SoftwareVersion string `json:"software_version"` + SwitchMode string `json:"switch_mode"` + LastGatewaySyncAt *time.Time `json:"last_gateway_sync_at,omitempty"` + // ↑ 以上字段已在 device_dto.go 中定义(Phase 3 完成),但 toDeviceResponse() 未赋值 +} +``` + + +```go +type DeviceCardBindingResponse struct { + ID uint + SlotPosition int + IotCardID uint + ICCID string + MSISDN string + CarrierName string + Status int + BindTime *time.Time + IsCurrent bool // ← 已定义,但 ListBindings() 未赋值 +} +``` + + +```go +// buildDeviceResolveResponse 中现有 slotMap 构建模式,isCurrentMap 同构: +slotMap := make(map[uint]int, len(bindings)) +for _, b := range bindings { + cardIDs = append(cardIDs, b.IotCardID) + slotMap[b.IotCardID] = b.SlotPosition +} +// 新增同构的 isCurrentMap: +// isCurrentMap := make(map[uint]bool, len(bindings)) +// isCurrentMap[b.IotCardID] = b.IsCurrent + +// GetRealtimeStatus device case 中现有 slotMap 构建模式,isCurrentMap 同构补充 +``` + + +```go +type DeviceRealtimeInfo struct { + OnlineStatus *int64; BatteryLevel *int64; Status *int64 + RunTime *string; ConnectTime *string; LastOnlineTime *string; LastUpdateTime *string + Rsrp *int64; Rsrq *int64; Rssi *string; Sinr *int64 + SSID *string; WifiEnabled *bool; WifiPassword *string + IPAddress *string; WANIP *string; LANIP *string; MACAddress *string + DailyUsage *string; DLStats *string; ULStats *string; LimitSpeed *int64 + CurrentIccid *string; MaxClients *int64; SoftwareVersion *string + SwitchMode *int; SyncInterval *int64 + DeviceID *string; DeviceName *string; DeviceType *string + Imei *string; Imsi *string; CreatedAt *int64; UpdatedAt *int64 +} +``` + + + + + + + Task 1: 补全 Gateway 模型和 B 端 DTO 契约 + internal/gateway/models.go, internal/model/dto/asset_dto.go + +**A. 补全 `internal/gateway/models.go` 的 `SyncDeviceInfoResp`(per D-10):** + +在现有 `DailyUsage string` 字段之后,追加以下缺失字段(参考 Gateway 接口文档 `docs/第三方文档/gateway设备详情同步接口.md`): + +```go +// 信号相关(补全) +Rsrp int `json:"rsrp" description:"参考信号接收功率(dBm)"` +Rsrq int `json:"rsrq" description:"参考信号接收质量(dB)"` +Sinr int `json:"sinr" description:"信噪比(dB)"` + +// WiFi 相关(补全) +WifiPassword string `json:"wifi_password" description:"WiFi密码"` + +// 网络相关(补全) +IPAddress string `json:"ip_address" description:"IP地址"` +WANIP string `json:"wan_ip" description:"基站分配IPv4地址"` +LANIP string `json:"lan_ip" description:"局域网网关IP地址"` + +// 连接相关(补全) +MaxClients int `json:"max_clients" description:"最大连接客户端数"` +ConnectTime string `json:"connect_time" description:"设备本次联网时间(秒)"` + +// 设备属性(补全) +Status int `json:"status" description:"设备状态:1=正常,0=禁用"` +IMSI string `json:"imsi" description:"IMSI用户标识码"` +ULStats string `json:"ul_stats" description:"本次开机上传流量(字节)"` +DLStats string `json:"dl_stats" description:"本次开机下载流量(字节)"` +LimitSpeed int `json:"limit_speed" description:"限速速率(KB/s)"` +SyncInterval int `json:"sync_interval" description:"信息上报周期(秒)"` +``` + +注意:`MacAddress` 字段已存在(json tag 为 `mac_address`),**不要重复添加**。 + +**B. 在 `internal/model/dto/asset_dto.go` 中新增 `DeviceGatewayInfo` 结构体(per D-08/D-09):** + +在文件末尾追加(在 `CardICCIDRequest` 之后),B 端 DTO 独立结构,全字段带 `description` tag: + +```go +// DeviceGatewayInfo B端设备实时状态信息(来自 Gateway sync-info) +// 与 C 端 DeviceRealtimeInfo 独立,未来可展示更多技术字段 +// 所有字段均为可选,Gateway 调用失败时整体为 null +type DeviceGatewayInfo struct { + // 设备状态 + OnlineStatus *int `json:"online_status,omitempty" description:"在线状态:1=在线,2=离线"` + BatteryLevel *int `json:"battery_level,omitempty" description:"电池电量百分比(无电池时为null)"` + Status *int `json:"status,omitempty" description:"设备状态:1=正常,0=禁用"` + RunTime *string `json:"run_time,omitempty" description:"本次开机运行时间(秒)"` + ConnectTime *string `json:"connect_time,omitempty" description:"本次联网时间(秒)"` + LastOnlineTime *string `json:"last_online_time,omitempty" description:"设备最后在线时间"` + LastUpdateTime *string `json:"last_update_time,omitempty" description:"设备信息最后更新时间"` + + // 信号相关 + Rsrp *int `json:"rsrp,omitempty" description:"参考信号接收功率(dBm)"` + Rsrq *int `json:"rsrq,omitempty" description:"参考信号接收质量(dB)"` + Rssi *string `json:"rssi,omitempty" description:"接收信号强度"` + Sinr *int `json:"sinr,omitempty" description:"信噪比(dB)"` + + // WiFi 相关 + SSID *string `json:"ssid,omitempty" description:"WiFi热点名称"` + WifiEnabled *bool `json:"wifi_enabled,omitempty" description:"WiFi开关状态"` + WifiPassword *string `json:"wifi_password,omitempty" description:"WiFi密码"` + + // 网络相关 + IPAddress *string `json:"ip_address,omitempty" description:"IP地址"` + WANIP *string `json:"wan_ip,omitempty" description:"基站分配IPv4地址"` + LANIP *string `json:"lan_ip,omitempty" description:"局域网网关IP地址"` + MACAddress *string `json:"mac_address,omitempty" description:"MAC地址"` + + // 流量与速率 + DailyUsage *string `json:"daily_usage,omitempty" description:"日使用流量(字节)"` + DLStats *string `json:"dl_stats,omitempty" description:"本次开机下载流量(字节)"` + ULStats *string `json:"ul_stats,omitempty" description:"本次开机上传流量(字节)"` + LimitSpeed *int `json:"limit_speed,omitempty" description:"限速速率(KB/s)"` + + // 设备属性 + CurrentIccid *string `json:"current_iccid,omitempty" description:"当前使用的ICCID"` + MaxClients *int `json:"max_clients,omitempty" description:"最大连接客户端数"` + SoftwareVersion *string `json:"software_version,omitempty" description:"软件版本号"` + SwitchMode *string `json:"switch_mode,omitempty" description:"切卡模式:0=自动,1=手动"` + SyncInterval *int `json:"sync_interval,omitempty" description:"信息上报周期(秒)"` + + // Gateway 标识字段 + DeviceID *string `json:"device_id,omitempty" description:"Gateway设备ID(IMEI/SN)"` + DeviceName *string `json:"device_name,omitempty" description:"Gateway返回的设备名称"` + DeviceType *string `json:"device_type,omitempty" description:"Gateway返回的设备类型"` + IMEI *string `json:"imei,omitempty" description:"IMEI号"` + IMSI *string `json:"imsi,omitempty" description:"IMSI用户标识码"` +} +``` + +**C. 修改 `AssetRealtimeStatusResponse`(per D-07/D-08):** + +在 `Cards []BoundCardInfo` 字段之后追加: +```go +DeviceRealtime *DeviceGatewayInfo `json:"device_realtime,omitempty" description:"设备实时状态(来自 Gateway sync-info,Gateway 失败时为 null)"` +``` + +**D. 修改 `AssetResolveResponse`(per D-12):** + +在 `Manufacturer string` 字段之后、`// 卡专属字段` 注释之前,追加 5 个 DB 缓存字段: +```go +// 设备 DB 缓存字段(Phase 3 sync-info 写入,展示上次同步快照) +OnlineStatus int `json:"online_status,omitempty" description:"在线状态:0未知 1在线 2离线(设备类型时有效)"` +LastOnlineTime *time.Time `json:"last_online_time,omitempty" description:"最后在线时间(设备类型时有效)"` +SoftwareVersion string `json:"software_version,omitempty" description:"固件版本号(设备类型时有效)"` +SwitchMode string `json:"switch_mode,omitempty" description:"切卡模式:0=自动,1=手动(设备类型时有效)"` +LastGatewaySyncAt *time.Time `json:"last_gateway_sync_at,omitempty" description:"最后 sync-info 同步时间(设备类型时有效)"` +``` + + go build ./internal/gateway/... && go build ./internal/model/dto/... + +- `SyncDeviceInfoResp` 包含所有 Gateway 文档字段(包括 Rsrp/Rsrq/Sinr/WifiPassword/IMSI/LANIP/WANIP/MaxClients/ConnectTime/Status/ULStats/DLStats/LimitSpeed/SyncInterval) +- `asset_dto.go` 中 `DeviceGatewayInfo` 结构体存在,字段带 `description` tag +- `AssetRealtimeStatusResponse` 含 `DeviceRealtime *DeviceGatewayInfo` +- `AssetResolveResponse` 含 5 个 DB 缓存设备字段 +- `go build` 编译通过 + + + + + Task 2: 修复静态 DTO 映射缺口(5 个文件映射补全) + internal/service/device/service.go, internal/service/device/binding.go, internal/service/asset/service.go + +**A. `internal/service/device/service.go:toDeviceResponse()`(per D-02):** + +在现有 `resp := &dto.DeviceResponse{...}` 初始化块中,追加 5 个 DB 缓存字段映射: +```go +OnlineStatus: device.OnlineStatus, +LastOnlineTime: device.LastOnlineTime, +SoftwareVersion: device.SoftwareVersion, +SwitchMode: device.SwitchMode, +LastGatewaySyncAt: device.LastGatewaySyncAt, +``` + +**B. `internal/service/device/binding.go:ListBindings()`(per D-03):** + +在构建 `resp := &dto.DeviceCardBindingResponse{...}` 时,追加: +```go +IsCurrent: binding.IsCurrent, +``` + +**C. `internal/service/asset/service.go:buildDeviceResolveResponse()`(per D-04 + D-12):** + +1. **补全 `BoundCardInfo.IsCurrent`(per D-04):** + + 在现有 `slotMap` 构建循环中,同构添加 `isCurrentMap`: + ```go + slotMap := make(map[uint]int, len(bindings)) + isCurrentMap := make(map[uint]bool, len(bindings)) // 新增 + for _, b := range bindings { + cardIDs = append(cardIDs, b.IotCardID) + slotMap[b.IotCardID] = b.SlotPosition + isCurrentMap[b.IotCardID] = b.IsCurrent // 新增 + } + ``` + + 在构建 `dto.BoundCardInfo{...}` 时追加: + ```go + IsCurrent: isCurrentMap[c.ID], // 新增 + ``` + +2. **补全 AssetResolveResponse 的 5 个 DB 缓存字段(per D-12):** + + 在 `resp := &dto.AssetResolveResponse{...}` 初始化块中追加: + ```go + OnlineStatus: device.OnlineStatus, + LastOnlineTime: device.LastOnlineTime, + SoftwareVersion: device.SoftwareVersion, + SwitchMode: device.SwitchMode, + LastGatewaySyncAt: device.LastGatewaySyncAt, + ``` + +**D. `internal/service/asset/service.go:GetRealtimeStatus()` device case 中 `BoundCardInfo`(per D-04):** + +同 C 步骤,在 `GetRealtimeStatus` device case 中也需补全 `isCurrentMap`: + +在 `slotMap` 构建循环中追加 `isCurrentMap`,在 `dto.BoundCardInfo{...}` 中追加 `IsCurrent: isCurrentMap[c.ID]`。 + +注意:`Refresh()` 内部会调用 `GetRealtimeStatus()`,`buildDeviceResolveResponse()` 是独立的两处代码路径,都需要修复。 + + go build ./internal/service/device/... && go build ./internal/service/asset/... + +- `toDeviceResponse()` 初始化块中含 5 个新字段赋值 +- `ListBindings()` 的 `DeviceCardBindingResponse` 含 `IsCurrent: binding.IsCurrent` +- `buildDeviceResolveResponse()` 中含 `isCurrentMap` + `BoundCardInfo.IsCurrent` + 5 个 DB 缓存字段 +- `GetRealtimeStatus()` device case 中含 `isCurrentMap` + `BoundCardInfo.IsCurrent` +- `go build` 编译通过 + + + + + Task 3: 建立设备实时 Gateway 调用链路(GetRealtimeStatus + C 端 stub 替换) + internal/service/asset/service.go, internal/handler/app/client_asset.go + +**A. `internal/service/asset/service.go:GetRealtimeStatus()` device case 追加 Gateway 实时调用(per D-07):** + +在现有 `resp.DeviceProtectStatus = s.getDeviceProtectStatus(ctx, id)` 之后追加: + +```go +// 实时查询 Gateway 设备状态(in-line,不缓存,per D-05) +// Gateway 失败不阻断主流程,DeviceRealtime 返回 null(per D-06) +if s.gatewayClient != nil { + device, devErr := s.deviceStore.GetByID(ctx, id) + if devErr == nil { + // IMEI 优先,无则用 SN(与 Refresh 中 updateDeviceFromSyncInfo 保持一致) + cardNo := device.IMEI + if cardNo == "" { + cardNo = device.SN + } + if cardNo != "" { + if syncResp, syncErr := s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{ + CardNo: cardNo, + }); syncErr == nil { + resp.DeviceRealtime = mapSyncRespToDeviceGatewayInfo(syncResp) + } else { + logger.GetAppLogger().Warn("GetRealtimeStatus: sync-info 调用失败,DeviceRealtime 返回 null", + zap.Uint("device_id", id), + zap.Error(syncErr)) + } + } + } +} +``` + +在 `GetRealtimeStatus()` 函数之后(同一文件,`service.go` 中),新增私有转换函数: + +```go +// mapSyncRespToDeviceGatewayInfo 将 Gateway SyncDeviceInfoResp 映射为 B 端 DeviceGatewayInfo DTO +// 使用指针字段,未赋值的字段保持 nil(omitempty) +func mapSyncRespToDeviceGatewayInfo(r *gateway.SyncDeviceInfoResp) *dto.DeviceGatewayInfo { + info := &dto.DeviceGatewayInfo{ + OnlineStatus: &r.OnlineStatus, + RunTime: strPtr(r.RunTime), + ConnectTime: strPtr(r.ConnectTime), + LastOnlineTime: strPtr(r.LastOnlineTime), + LastUpdateTime: strPtr(r.LastUpdateTime), + Rsrp: &r.Rsrp, + Rsrq: &r.Rsrq, + Rssi: strPtr(r.RSSI), + Sinr: &r.Sinr, + SSID: strPtr(r.SSID), + WifiEnabled: &r.WifiEnabled, + WifiPassword: strPtr(r.WifiPassword), + IPAddress: strPtr(r.IPAddress), + WANIP: strPtr(r.WANIP), + LANIP: strPtr(r.LANIP), + MACAddress: strPtr(r.MacAddress), + DailyUsage: strPtr(r.DailyUsage), + DLStats: strPtr(r.DLStats), + ULStats: strPtr(r.ULStats), + LimitSpeed: &r.LimitSpeed, + CurrentIccid: strPtr(r.CurrentIccid), + MaxClients: &r.MaxClients, + SoftwareVersion: strPtr(r.SoftwareVersion), + SwitchMode: strPtr(r.SwitchMode), + SyncInterval: &r.SyncInterval, + DeviceID: strPtr(r.DeviceID), + DeviceName: strPtr(r.DeviceName), + DeviceType: strPtr(r.DeviceType), + IMEI: strPtr(r.IMEI), + IMSI: strPtr(r.IMSI), + Status: &r.Status, + } + if r.BatteryLevel != nil { + info.BatteryLevel = r.BatteryLevel + } + return info +} + +// strPtr 将空字符串转为 nil,非空字符串返回指针 +// 避免 JSON 响应中出现大量空字符串字段 +func strPtr(s string) *string { + if s == "" { + return nil + } + return &s +} +``` + +注意:若 `strPtr` 函数已在其他地方定义,复用现有实现,不要重复定义。 + +**B. `internal/handler/app/client_asset.go:GetAsset()` 替换 mock(per D-11):** + +定位 `handler/app/client_asset.go` 约 187-191 行的 mock 调用: +```go +// 旧代码(删除): +if resp.AssetType == "device" { + resp.DeviceRealtime = buildMockDeviceRealtime() +} +``` + +替换为: +```go +// 调用服务层获取设备实时 Gateway 数据(per D-11) +// Gateway 失败时 DeviceRealtime 为 null,不阻断主流程(per D-06) +if resp.AssetType == "device" { + realtimeResp, realtimeErr := h.assetService.GetRealtimeStatus( + c.UserContext(), "device", resp.AssetID, + ) + if realtimeErr == nil && realtimeResp.DeviceRealtime != nil { + // 将 B 端 DeviceGatewayInfo 映射到 C 端 DeviceRealtimeInfo + resp.DeviceRealtime = mapDeviceGatewayInfoToClientInfo(realtimeResp.DeviceRealtime) + } +} +``` + +在同一文件中,新增私有映射函数(替换被删除的 `buildMockDeviceRealtime()`): + +```go +// mapDeviceGatewayInfoToClientInfo 将 B 端 DeviceGatewayInfo 映射为 C 端 DeviceRealtimeInfo +// B 端和 C 端结构独立,通过映射函数转换(per D-08) +func mapDeviceGatewayInfoToClientInfo(g *dto.DeviceGatewayInfo) *dto.DeviceRealtimeInfo { + if g == nil { + return nil + } + info := &dto.DeviceRealtimeInfo{} + if g.OnlineStatus != nil { + v := int64(*g.OnlineStatus) + info.OnlineStatus = &v + } + if g.BatteryLevel != nil { + v := int64(*g.BatteryLevel) + info.BatteryLevel = &v + } + if g.Status != nil { + v := int64(*g.Status) + info.Status = &v + } + info.RunTime = g.RunTime + info.ConnectTime = g.ConnectTime + info.LastOnlineTime = g.LastOnlineTime + info.LastUpdateTime = g.LastUpdateTime + if g.Rsrp != nil { + v := int64(*g.Rsrp) + info.Rsrp = &v + } + if g.Rsrq != nil { + v := int64(*g.Rsrq) + info.Rsrq = &v + } + info.Rssi = g.Rssi + if g.Sinr != nil { + v := int64(*g.Sinr) + info.Sinr = &v + } + info.SSID = g.SSID + info.WifiEnabled = g.WifiEnabled + info.WifiPassword = g.WifiPassword + info.IPAddress = g.IPAddress + info.WANIP = g.WANIP + info.LANIP = g.LANIP + info.MACAddress = g.MACAddress + info.DailyUsage = g.DailyUsage + info.DLStats = g.DLStats + info.ULStats = g.ULStats + if g.LimitSpeed != nil { + v := int64(*g.LimitSpeed) + info.LimitSpeed = &v + } + info.CurrentIccid = g.CurrentIccid + if g.MaxClients != nil { + v := int64(*g.MaxClients) + info.MaxClients = &v + } + info.SoftwareVersion = g.SoftwareVersion + if g.SwitchMode != nil { + v := int(*g.SwitchMode) // C端 SwitchMode 是 *int + info.SwitchMode = &v + } + if g.SyncInterval != nil { + v := int64(*g.SyncInterval) + info.SyncInterval = &v + } + info.DeviceID = g.DeviceID + info.DeviceName = g.DeviceName + info.DeviceType = g.DeviceType + info.Imei = g.IMEI + info.Imsi = g.IMSI + return info +} +``` + +最后,**删除** `buildMockDeviceRealtime()` 函数(约 595-640 行),该函数不再需要。 + + go build ./internal/service/asset/... && go build ./internal/handler/app/... + +- `GetRealtimeStatus()` device case 末尾有 Gateway 调用 + `mapSyncRespToDeviceGatewayInfo` 函数存在 +- `GetAsset()` 中 `buildMockDeviceRealtime()` 调用已删除,改为调用 `h.assetService.GetRealtimeStatus()` + `mapDeviceGatewayInfoToClientInfo()` +- `buildMockDeviceRealtime()` 函数已删除 +- `go build ./...` 全量编译通过(无编译错误) + + + + + + +```bash +# 全量编译验证(必须通过) +go build ./... + +# 静态检查(无新增错误) +go vet ./internal/gateway/... ./internal/model/dto/... ./internal/service/asset/... ./internal/service/device/... ./internal/handler/app/... +``` + +**手动验证清单(通过 DB 和 API 确认):** + +1. 设备管理列表接口(GET /api/admin/devices)— 响应中含 `online_status`、`software_version` 等字段 +2. 设备绑卡列表接口(GET /api/admin/devices/:id/cards)— 响应中 `bindings[].is_current` 有值 +3. 资产解析接口(GET /api/admin/assets/resolve/:identifier,标识符为设备号)— 响应中含 `online_status`、`last_gateway_sync_at` 等字段,`cards[].is_current` 有值 +4. 资产实时状态接口(GET /api/admin/assets/:type/:id/realtime,type=device)— 响应中含 `device_realtime`(若 Gateway 可达则有值,不可达则为 null);`cards[].is_current` 有值 +5. C 端资产信息接口(GET /api/c/v1/asset,identifier=设备IMEI/SN)— 响应中 `device_realtime` 非 mock 数据(含真实 `online_status`/`battery_level` 等字段) + + + +- `go build ./...` 编译通过,无错误无 warning +- 所有 DTO 静态映射缺口已补全(5 处字段 + 2 处 IsCurrent + 5 处 DB 缓存字段) +- `GetRealtimeStatus("device", id)` 调用 Gateway sync-info,失败时 `DeviceRealtime` 为 null +- C 端 `GetAsset` device 类型不再返回 mock 数据 +- 代码符合项目规范:中文注释、分层架构、错误只记 Warn 不阻断 + + + +完成后创建 `.planning/phases/03.1-sync-info-phase-3-dto/03.1-01-SUMMARY.md`,记录: +- 实际修改的文件和行号 +- `mapSyncRespToDeviceGatewayInfo` 和 `mapDeviceGatewayInfoToClientInfo` 的实现位置 +- 编译通过的确认 +- 任何与计划不符的实现调整(并说明原因) +