diff --git a/internal/gateway/models.go b/internal/gateway/models.go index a3df6ff..45bff14 100644 --- a/internal/gateway/models.go +++ b/internal/gateway/models.go @@ -46,6 +46,33 @@ func (i *FlexInt) UnmarshalJSON(data []byte) error { return nil } +// FlexString 灵活字符串类型 +// Gateway 部分字段会在 string / number / bool 之间漂移,此类型统一转为字符串避免解析失败。 +type FlexString string + +// UnmarshalJSON 实现 json.Unmarshaler 接口 +// 支持:"text", 123, true, null +func (s *FlexString) UnmarshalJSON(data []byte) error { + raw := strings.TrimSpace(string(data)) + if raw == "" || raw == "null" { + *s = "" + return nil + } + + // 标准 JSON 字符串,优先做反转义,保持原始内容语义 + if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") { + unquoted, err := strconv.Unquote(raw) + if err == nil { + *s = FlexString(unquoted) + return nil + } + } + + // number/bool/object/array 等类型直接保留原始文本 + *s = FlexString(raw) + return nil +} + // GatewayResponse 是 Gateway API 的通用响应结构 type GatewayResponse struct { Code int `json:"code" description:"业务状态码(200 = 成功)"` @@ -191,23 +218,23 @@ type SyncDeviceInfoReq struct { // SyncDeviceInfoResp sync-info 同步查询设备信息响应(对应 Gateway data 字段) // 注意:所有字段均可能为 null/零值,表示暂无数据 type SyncDeviceInfoResp struct { - DeviceID string `json:"device_id" description:"设备ID(IMEI/SN)"` - DeviceName string `json:"device_name" description:"设备名称"` - IMEI string `json:"imei" description:"IMEI号"` - CurrentIccid string `json:"current_iccid" description:"当前使用的ICCID"` - DeviceType string `json:"device_type" description:"设备类型:1=诺行,2=玺龙,3=迎势达"` - SoftwareVersion string `json:"software_version" description:"软件版本号"` - MacAddress string `json:"mac_address" description:"MAC地址"` - SSID string `json:"ssid" description:"WiFi热点名称"` - WifiEnabled FlexBool `json:"wifi_enabled" description:"WiFi开关状态"` - SwitchMode string `json:"switch_mode" description:"切卡模式:0=自动,1=手动"` - RSSI string `json:"rssi" description:"接收信号强度"` - BatteryLevel *FlexInt `json:"battery_level" description:"电池电量百分比(无电池时为null)"` - OnlineStatus FlexInt `json:"online_status" description:"在线状态:1=在线,2=离线"` - LastUpdateTime string `json:"last_update_time" description:"设备信息最后更新时间(ISO 8601)"` - LastOnlineTime string `json:"last_online_time" description:"设备最后在线时间(ISO 8601)"` - RunTime string `json:"run_time" description:"本次开机运行时间(秒)"` - DailyUsage string `json:"daily_usage" description:"日使用流量(字节)"` + DeviceID FlexString `json:"device_id" description:"设备ID(IMEI/SN)"` + DeviceName FlexString `json:"device_name" description:"设备名称"` + IMEI FlexString `json:"imei" description:"IMEI号"` + CurrentIccid FlexString `json:"current_iccid" description:"当前使用的ICCID"` + DeviceType FlexString `json:"device_type" description:"设备类型:1=诺行,2=玺龙,3=迎势达"` + SoftwareVersion FlexString `json:"software_version" description:"软件版本号"` + MacAddress FlexString `json:"mac_address" description:"MAC地址"` + SSID FlexString `json:"ssid" description:"WiFi热点名称"` + WifiEnabled FlexBool `json:"wifi_enabled" description:"WiFi开关状态"` + SwitchMode FlexString `json:"switch_mode" description:"切卡模式:0=自动,1=手动"` + RSSI FlexString `json:"rssi" description:"接收信号强度"` + BatteryLevel *FlexInt `json:"battery_level" description:"电池电量百分比(无电池时为null)"` + OnlineStatus FlexInt `json:"online_status" description:"在线状态:1=在线,2=离线"` + LastUpdateTime FlexString `json:"last_update_time" description:"设备信息最后更新时间(ISO 8601 或 Unix 时间戳)"` + LastOnlineTime FlexString `json:"last_online_time" description:"设备最后在线时间(ISO 8601 或 Unix 时间戳)"` + RunTime FlexString `json:"run_time" description:"本次开机运行时间(秒)"` + DailyUsage FlexString `json:"daily_usage" description:"日使用流量(字节)"` // 信号相关(D-10 补全) Rsrp FlexInt `json:"rsrp" description:"参考信号接收功率(dBm)"` @@ -215,23 +242,23 @@ type SyncDeviceInfoResp struct { Sinr FlexInt `json:"sinr" description:"信噪比(dB)"` // WiFi 相关(D-10 补全) - WifiPassword string `json:"wifi_password" description:"WiFi密码"` + WifiPassword FlexString `json:"wifi_password" description:"WiFi密码"` // 网络相关(D-10 补全) - IPAddress string `json:"ip_address" description:"IP地址"` - WANIP string `json:"wan_ip" description:"基站分配IPv4地址"` - LANIP string `json:"lan_ip" description:"局域网网关IP地址"` + IPAddress FlexString `json:"ip_address" description:"IP地址"` + WANIP FlexString `json:"wan_ip" description:"基站分配IPv4地址"` + LANIP FlexString `json:"lan_ip" description:"局域网网关IP地址"` // 连接相关(D-10 补全) - MaxClients FlexInt `json:"max_clients" description:"最大连接客户端数"` - ClientNumber FlexInt `json:"client_number" description:"当前已连接客户端数"` - ConnectTime string `json:"connect_time" description:"设备本次联网时间(秒)"` + MaxClients FlexInt `json:"max_clients" description:"最大连接客户端数"` + ClientNumber FlexInt `json:"client_number" description:"当前已连接客户端数"` + ConnectTime FlexString `json:"connect_time" description:"设备本次联网时间(秒)"` // 设备属性(D-10 补全) - Status FlexInt `json:"status" description:"设备状态:1=正常,0=禁用"` - IMSI string `json:"imsi" description:"IMSI用户标识码"` - ULStats string `json:"ul_stats" description:"本次开机上传流量(字节)"` - DLStats string `json:"dl_stats" description:"本次开机下载流量(字节)"` - LimitSpeed FlexInt `json:"limit_speed" description:"限速速率(KB/s)"` - SyncInterval FlexInt `json:"sync_interval" description:"信息上报周期(秒)"` + Status FlexInt `json:"status" description:"设备状态:1=正常,0=禁用"` + IMSI FlexString `json:"imsi" description:"IMSI用户标识码"` + ULStats FlexString `json:"ul_stats" description:"本次开机上传流量(字节)"` + DLStats FlexString `json:"dl_stats" description:"本次开机下载流量(字节)"` + LimitSpeed FlexInt `json:"limit_speed" description:"限速速率(KB/s)"` + SyncInterval FlexInt `json:"sync_interval" description:"信息上报周期(秒)"` } diff --git a/internal/service/asset/service.go b/internal/service/asset/service.go index 80f38d2..a25b654 100644 --- a/internal/service/asset/service.go +++ b/internal/service/asset/service.go @@ -7,6 +7,7 @@ import ( "context" stderrors "errors" "sort" + "strconv" "time" "github.com/break/junhong_cmp_fiber/internal/gateway" @@ -442,36 +443,36 @@ func (s *Service) fetchDeviceGatewayInfo(ctx context.Context, deviceID uint) *dt func mapSyncRespToDeviceGatewayInfo(r *gateway.SyncDeviceInfoResp) *dto.DeviceGatewayInfo { info := &dto.DeviceGatewayInfo{ OnlineStatus: flexIntPtr(r.OnlineStatus), - RunTime: strPtr(r.RunTime), - ConnectTime: strPtr(r.ConnectTime), - LastOnlineTime: strPtr(r.LastOnlineTime), - LastUpdateTime: strPtr(r.LastUpdateTime), + RunTime: flexStrPtr(r.RunTime), + ConnectTime: flexStrPtr(r.ConnectTime), + LastOnlineTime: flexStrPtr(r.LastOnlineTime), + LastUpdateTime: flexStrPtr(r.LastUpdateTime), Rsrp: flexIntPtr(r.Rsrp), Rsrq: flexIntPtr(r.Rsrq), - Rssi: strPtr(r.RSSI), + Rssi: flexStrPtr(r.RSSI), Sinr: flexIntPtr(r.Sinr), - SSID: strPtr(r.SSID), + SSID: flexStrPtr(r.SSID), WifiEnabled: flexBoolPtr(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), + WifiPassword: flexStrPtr(r.WifiPassword), + IPAddress: flexStrPtr(r.IPAddress), + WANIP: flexStrPtr(r.WANIP), + LANIP: flexStrPtr(r.LANIP), + MACAddress: flexStrPtr(r.MacAddress), + DailyUsage: flexStrPtr(r.DailyUsage), + DLStats: flexStrPtr(r.DLStats), + ULStats: flexStrPtr(r.ULStats), LimitSpeed: flexIntPtr(r.LimitSpeed), - CurrentIccid: strPtr(r.CurrentIccid), + CurrentIccid: flexStrPtr(r.CurrentIccid), MaxClients: flexIntPtr(r.MaxClients), ClientNumber: flexIntPtr(r.ClientNumber), - SoftwareVersion: strPtr(r.SoftwareVersion), - SwitchMode: strPtr(r.SwitchMode), + SoftwareVersion: flexStrPtr(r.SoftwareVersion), + SwitchMode: flexStrPtr(r.SwitchMode), SyncInterval: flexIntPtr(r.SyncInterval), - DeviceID: strPtr(r.DeviceID), - DeviceName: strPtr(r.DeviceName), - DeviceType: strPtr(r.DeviceType), - IMEI: strPtr(r.IMEI), - IMSI: strPtr(r.IMSI), + DeviceID: flexStrPtr(r.DeviceID), + DeviceName: flexStrPtr(r.DeviceName), + DeviceType: flexStrPtr(r.DeviceType), + IMEI: flexStrPtr(r.IMEI), + IMSI: flexStrPtr(r.IMSI), Status: flexIntPtr(r.Status), } if r.BatteryLevel != nil { @@ -490,6 +491,11 @@ func strPtr(s string) *string { return &s } +// flexStrPtr 将 FlexString 转为字符串指针 +func flexStrPtr(s gateway.FlexString) *string { + return strPtr(string(s)) +} + func flexBoolPtr(fb gateway.FlexBool) *bool { b := bool(fb) return &b @@ -580,19 +586,14 @@ func (s *Service) Refresh(ctx context.Context, assetType string, id uint) (*dto. func (s *Service) updateDeviceFromSyncInfo(ctx context.Context, deviceID uint, resp *gateway.SyncDeviceInfoResp) { now := time.Now() - // 解析 LastOnlineTime(ISO 8601 字符串 → *time.Time) - var lastOnlineTime *time.Time - if resp.LastOnlineTime != "" { - if t, err := time.Parse(time.RFC3339, resp.LastOnlineTime); err == nil { - lastOnlineTime = &t - } - } + // Gateway 时间字段可能返回 RFC3339 或 Unix 时间戳,统一解析后再回写数据库 + lastOnlineTime := parseGatewayTime(resp.LastOnlineTime) // 更新设备 5 个存储字段 updates := map[string]any{ "online_status": int(resp.OnlineStatus), - "software_version": resp.SoftwareVersion, - "switch_mode": resp.SwitchMode, + "software_version": string(resp.SoftwareVersion), + "switch_mode": string(resp.SwitchMode), "last_gateway_sync_at": now, } if lastOnlineTime != nil { @@ -609,9 +610,9 @@ func (s *Service) updateDeviceFromSyncInfo(ctx context.Context, deviceID uint, r return } - if resp.CurrentIccid != "" { + if string(resp.CurrentIccid) != "" { // 更新 is_current 标识(事务原子操作,per D-14 决策) - if err := s.deviceSimBindingStore.UpdateIsCurrentByDeviceID(ctx, deviceID, resp.CurrentIccid); err != nil { + if err := s.deviceSimBindingStore.UpdateIsCurrentByDeviceID(ctx, deviceID, string(resp.CurrentIccid)); err != nil { logger.GetAppLogger().Warn("sync-info 更新 is_current 失败", zap.Uint("device_id", deviceID), zap.Error(err)) @@ -619,6 +620,33 @@ func (s *Service) updateDeviceFromSyncInfo(ctx context.Context, deviceID uint, r } } +// parseGatewayTime 兼容解析 Gateway 时间字段 +// 支持 RFC3339 字符串、Unix 秒时间戳、Unix 毫秒时间戳。 +func parseGatewayTime(raw gateway.FlexString) *time.Time { + value := string(raw) + if value == "" { + return nil + } + + if t, err := time.Parse(time.RFC3339, value); err == nil { + return &t + } + + // 兼容网关返回的纯数字时间戳(秒或毫秒) + ts, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil + } + + var t time.Time + if len(value) >= 13 { + t = time.UnixMilli(ts) + } else { + t = time.Unix(ts, 0) + } + return &t +} + // GetPackages 获取资产的所有套餐列表(支持分页) // page 默认 1,pageSize 默认 50,pageSize 最大 100 func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, page, pageSize int) (*dto.AssetPackagesResult, error) {