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

<objective>
补全 Phase 3 完成的设备数据写入链路在读取侧的缺口：修复 DTO 字段映射遗漏（静态缺口）+ 建立设备实时状态的 Gateway 查询链路（架构对齐）。

Purpose: Phase 3 已实现 sync-info 调用和 DB 字段写入，但 API 接口未返回这些字段，且 C 端设备实时状态仍是 mock 数据。本 Phase 完成"读取侧"闭环。
Output: 6 个文件修改，设备管理/资产解析/C 端资产接口全部返回真实数据。
</objective>

<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
<!-- 执行本 Plan 前必须读取以下接口定义，无需探索代码库 -->

<!-- gateway/models.go 当前 SyncDeviceInfoResp（缺失字段需补全）-->
```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）
}
```

<!-- asset_dto.go 当前 BoundCardInfo（已有 IsCurrent，但 buildDeviceResolveResponse 未填充）-->
```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 未赋值
}
```

<!-- asset_dto.go 当前 AssetRealtimeStatusResponse（需新增 DeviceRealtime 字段）-->
```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"`
}
```

<!-- device_dto.go 当前 DeviceResponse（已有字段，但 toDeviceResponse 未映射）-->
```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() 未赋值
}
```

<!-- device/binding.go DeviceCardBindingResponse（已有 IsCurrent，但 ListBindings 未填充）-->
```go
type DeviceCardBindingResponse struct {
    ID           uint
    SlotPosition int
    IotCardID    uint
    ICCID        string
    MSISDN       string
    CarrierName  string
    Status       int
    BindTime     *time.Time
    IsCurrent    bool  // ← 已定义，但 ListBindings() 未赋值
}
```

<!-- asset/service.go 关键模式（isCurrentMap 构建方式）-->
```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 同构补充
```

<!-- client_asset_dto.go DeviceRealtimeInfo 完整结构（B 端 DeviceGatewayInfo 的字段参考）-->
```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
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: 补全 Gateway 模型和 B 端 DTO 契约</name>
  <files>internal/gateway/models.go, internal/model/dto/asset_dto.go</files>
  <action>
**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 同步时间（设备类型时有效）"`
```
  </action>
  <verify>go build ./internal/gateway/... && go build ./internal/model/dto/...</verify>
  <done>
- `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` 编译通过
  </done>
</task>

<task type="auto">
  <name>Task 2: 修复静态 DTO 映射缺口（5 个文件映射补全）</name>
  <files>internal/service/device/service.go, internal/service/device/binding.go, internal/service/asset/service.go</files>
  <action>
**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()` 是独立的两处代码路径，都需要修复。
  </action>
  <verify>go build ./internal/service/device/... && go build ./internal/service/asset/...</verify>
  <done>
- `toDeviceResponse()` 初始化块中含 5 个新字段赋值
- `ListBindings()` 的 `DeviceCardBindingResponse` 含 `IsCurrent: binding.IsCurrent`
- `buildDeviceResolveResponse()` 中含 `isCurrentMap` + `BoundCardInfo.IsCurrent` + 5 个 DB 缓存字段
- `GetRealtimeStatus()` device case 中含 `isCurrentMap` + `BoundCardInfo.IsCurrent`
- `go build` 编译通过
  </done>
</task>

<task type="auto">
  <name>Task 3: 建立设备实时 Gateway 调用链路（GetRealtimeStatus + C 端 stub 替换）</name>
  <files>internal/service/asset/service.go, internal/handler/app/client_asset.go</files>
  <action>
**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 行），该函数不再需要。
  </action>
  <verify>go build ./internal/service/asset/... && go build ./internal/handler/app/...</verify>
  <done>
- `GetRealtimeStatus()` device case 末尾有 Gateway 调用 + `mapSyncRespToDeviceGatewayInfo` 函数存在
- `GetAsset()` 中 `buildMockDeviceRealtime()` 调用已删除，改为调用 `h.assetService.GetRealtimeStatus()` + `mapDeviceGatewayInfoToClientInfo()`
- `buildMockDeviceRealtime()` 函数已删除
- `go build ./...` 全量编译通过（无编译错误）
  </done>
</task>

</tasks>

<verification>
```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` 等字段）
</verification>

<success_criteria>
- `go build ./...` 编译通过，无错误无 warning
- 所有 DTO 静态映射缺口已补全（5 处字段 + 2 处 IsCurrent + 5 处 DB 缓存字段）
- `GetRealtimeStatus("device", id)` 调用 Gateway sync-info，失败时 `DeviceRealtime` 为 null
- C 端 `GetAsset` device 类型不再返回 mock 数据
- 代码符合项目规范：中文注释、分层架构、错误只记 Warn 不阻断
</success_criteria>

<output>
完成后创建 `.planning/phases/03.1-sync-info-phase-3-dto/03.1-01-SUMMARY.md`，记录：
- 实际修改的文件和行号
- `mapSyncRespToDeviceGatewayInfo` 和 `mapDeviceGatewayInfoToClientInfo` 的实现位置
- 编译通过的确认
- 任何与计划不符的实现调整（并说明原因）
</output>
