docs(03.1): 创建 Phase 03.1 计划(设备读取链路修复 + DTO 映射补全)
This commit is contained in:
@@ -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: 退款 + 支付 + 运营修复
|
### Phase 4: 退款 + 支付 + 运营修复
|
||||||
**Goal**: 实现完整退款流程(申请→审批→佣金回扣),补全富友支付 JSAPI/小程序接口(替换留桩),修复运营逻辑断点(卡状态 3/4 触发、payment_status 枚举统一)
|
**Goal**: 实现完整退款流程(申请→审批→佣金回扣),补全富友支付 JSAPI/小程序接口(替换留桩),修复运营逻辑断点(卡状态 3/4 触发、payment_status 枚举统一)
|
||||||
**Depends on**: Phase 2(佣金链路稳定后再做退款佣金回扣)
|
**Depends on**: Phase 2(佣金链路稳定后再做退款佣金回扣)
|
||||||
|
|||||||
637
.planning/phases/03.1-sync-info-phase-3-dto/03.1-01-PLAN.md
Normal file
637
.planning/phases/03.1-sync-info-phase-3-dto/03.1-01-PLAN.md
Normal file
@@ -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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user