From 948243b13d7cc102705e2a2c64fa5089e8697039 Mon Sep 17 00:00:00 2001 From: huang Date: Thu, 30 Apr 2026 15:49:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/admin-openapi.yaml | 22 ++- internal/handler/app/client_asset.go | 2 + internal/model/dto/asset_dto.go | 76 +++++---- internal/model/dto/client_asset_dto.go | 10 +- internal/service/asset/service.go | 91 +++++----- internal/service/asset/signal_summary.go | 159 ++++++++++++++++++ .../service/asset_audit/operation_content.go | 121 ++++++++----- internal/service/device/audit.go | 117 ++++++++++++- internal/service/device/service.go | 75 ++++++--- internal/service/iot_card/audit.go | 143 ++++++++++++++-- internal/service/iot_card/service.go | 65 +++++-- 11 files changed, 703 insertions(+), 178 deletions(-) create mode 100644 internal/service/asset/signal_summary.go diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index 2164dad..8ba30ac 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -3610,6 +3610,14 @@ components: description: 本次开机运行时间(秒) nullable: true type: string + signal_bad_reason: + description: 信号较弱时的怀疑原因提示 + nullable: true + type: string + signal_quality: + description: 信号综合判断(信号很好/信号正常/信号较弱/信号很差/暂无数据) + nullable: true + type: string sinr: description: 信噪比(dB) nullable: true @@ -3906,6 +3914,14 @@ components: description: 设备本次开机运行时间(秒) nullable: true type: string + signal_bad_reason: + description: 信号较弱时的怀疑原因提示 + nullable: true + type: string + signal_quality: + description: 信号综合判断(信号很好/信号正常/信号较弱/信号很差/暂无数据) + nullable: true + type: string sinr: description: 信噪比(dB) nullable: true @@ -13578,9 +13594,11 @@ paths: - `device_name`: 设备名称 - `device_model`: 设备型号 - `device_type`: 设备类型 - - `max_sim_slots`: 最大插槽数(默认4) + - `imei`: 设备 IMEI - `manufacturer`: 制造商 - - `iccid_1` ~ `iccid_4`: 绑定的卡 ICCID(卡必须已存在且未绑定) + - `max_sim_slots`: 最大插槽数(默认4,范围1-4) + - `iccid_1` ~ `iccid_4`: 固定槽位的卡 ICCID(`iccid_1`=槽1,`iccid_2`=槽2,`iccid_3`=槽3,`iccid_4`=槽4;卡必须已存在且未绑定) + - 槽位规则:中间留空不会触发前移补位;如果 `max_sim_slots=2`,则 `iccid_3`、`iccid_4` 必须留空,否则该行失败 - 列格式:设置为文本格式(避免长数字被转为科学记数法) requestBody: content: diff --git a/internal/handler/app/client_asset.go b/internal/handler/app/client_asset.go index dec1331..a3e1fd2 100644 --- a/internal/handler/app/client_asset.go +++ b/internal/handler/app/client_asset.go @@ -641,6 +641,8 @@ func mapDeviceGatewayInfoToClientInfo(g *dto.DeviceGatewayInfo) *dto.DeviceRealt v := int64(*g.Sinr) info.Sinr = &v } + info.SignalQuality = g.SignalQuality + info.SignalBadReason = g.SignalBadReason info.SSID = g.SSID info.WifiEnabled = g.WifiEnabled info.WifiPassword = g.WifiPassword diff --git a/internal/model/dto/asset_dto.go b/internal/model/dto/asset_dto.go index 1a538d3..edcbe5a 100644 --- a/internal/model/dto/asset_dto.go +++ b/internal/model/dto/asset_dto.go @@ -19,17 +19,17 @@ type AssetResolveResponse struct { CreatedAt time.Time `json:"created_at" description:"创建时间"` UpdatedAt time.Time `json:"updated_at" description:"更新时间"` // 状态聚合字段 - RealNameStatus int `json:"real_name_status" description:"实名状态:0未实名 1已实名"` - RealNameAt *time.Time `json:"real_name_at" description:"最近一次完成实名的时间,未实名时为 null"` - RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"` - CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"` - CurrentPackageUsageID *uint `json:"current_package_usage_id" description:"当前主套餐的套餐使用记录ID,无主套餐时为 null"` - RealTotalMB int64 `json:"real_total_mb" description:"当前主套餐真实总量(MB)"` - RealUsedMB int64 `json:"real_used_mb" description:"当前主套餐真实已用量(MB)"` - VirtualTotalMB int64 `json:"virtual_total_mb" description:"当前主套餐业务停机阈值(MB)"` - VirtualUsedMB float64 `json:"virtual_used_mb" description:"当前主套餐展示已用量(MB)"` - ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"` - DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态:none/stop/start(仅asset_type=device时有效)"` + RealNameStatus int `json:"real_name_status" description:"实名状态:0未实名 1已实名"` + RealNameAt *time.Time `json:"real_name_at" description:"最近一次完成实名的时间,未实名时为 null"` + RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"` + CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"` + CurrentPackageUsageID *uint `json:"current_package_usage_id" description:"当前主套餐的套餐使用记录ID,无主套餐时为 null"` + RealTotalMB int64 `json:"real_total_mb" description:"当前主套餐真实总量(MB)"` + RealUsedMB int64 `json:"real_used_mb" description:"当前主套餐真实已用量(MB)"` + VirtualTotalMB int64 `json:"virtual_total_mb" description:"当前主套餐业务停机阈值(MB)"` + VirtualUsedMB float64 `json:"virtual_used_mb" description:"当前主套餐展示已用量(MB)"` + ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"` + DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态:none/stop/start(仅asset_type=device时有效)"` // 绑定关系字段 ICCID string `json:"iccid,omitempty" description:"卡ICCID(asset_type=card时有效)"` BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID(asset_type=card时有效)"` @@ -97,28 +97,28 @@ type AssetRealtimeStatusResponse struct { // AssetPackageResponse 资产套餐信息 type AssetPackageResponse struct { - PackageUsageID uint `json:"package_usage_id" description:"套餐使用记录ID"` - PackageID uint `json:"package_id" description:"套餐ID"` - PackageName string `json:"package_name" description:"套餐名称"` - PackageType string `json:"package_type" description:"套餐类型:formal/addon"` - OrderID uint `json:"order_id" description:"关联订单ID(无订单分配时为0)"` - OrderNo string `json:"order_no,omitempty" description:"订单号快照(无订单分配时为空)"` - RefundID *uint `json:"refund_id,omitempty" description:"退款主键ID快照(未退款时为空)"` - RefundNo string `json:"refund_no,omitempty" description:"退款单号快照(未退款时为空)"` - UsageType string `json:"usage_type" description:"使用类型:single_card/device"` - Status int `json:"status" description:"状态:0待生效 1生效中 2已用完 3已过期 4已失效"` - StatusName string `json:"status_name" description:"状态名称"` - RealTotalMB int64 `json:"real_total_mb" description:"套餐真实总量(MB)"` - RealUsedMB int64 `json:"real_used_mb" description:"套餐真实已用量(MB)"` - VirtualTotalMB int64 `json:"virtual_total_mb" description:"套餐业务停机阈值(MB)"` - VirtualUsedMB float64 `json:"virtual_used_mb" description:"套餐展示已用量(MB)"` - ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"` - ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间(待生效套餐为空)"` - ExpiresAt *time.Time `json:"expires_at,omitempty" description:"到期时间(待生效套餐为空)"` - MasterUsageID *uint `json:"master_usage_id,omitempty" description:"主套餐ID(加油包时有值)"` - Priority int `json:"priority" description:"优先级"` - PaidAmount *int64 `json:"paid_amount,omitempty" description:"购买实付金额(分),线下支付或无订单分配时为空"` - CreatedAt time.Time `json:"created_at" description:"创建时间"` + PackageUsageID uint `json:"package_usage_id" description:"套餐使用记录ID"` + PackageID uint `json:"package_id" description:"套餐ID"` + PackageName string `json:"package_name" description:"套餐名称"` + PackageType string `json:"package_type" description:"套餐类型:formal/addon"` + OrderID uint `json:"order_id" description:"关联订单ID(无订单分配时为0)"` + OrderNo string `json:"order_no,omitempty" description:"订单号快照(无订单分配时为空)"` + RefundID *uint `json:"refund_id,omitempty" description:"退款主键ID快照(未退款时为空)"` + RefundNo string `json:"refund_no,omitempty" description:"退款单号快照(未退款时为空)"` + UsageType string `json:"usage_type" description:"使用类型:single_card/device"` + Status int `json:"status" description:"状态:0待生效 1生效中 2已用完 3已过期 4已失效"` + StatusName string `json:"status_name" description:"状态名称"` + RealTotalMB int64 `json:"real_total_mb" description:"套餐真实总量(MB)"` + RealUsedMB int64 `json:"real_used_mb" description:"套餐真实已用量(MB)"` + VirtualTotalMB int64 `json:"virtual_total_mb" description:"套餐业务停机阈值(MB)"` + VirtualUsedMB float64 `json:"virtual_used_mb" description:"套餐展示已用量(MB)"` + ReductionPct float64 `json:"reduction_pct" description:"展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1"` + ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间(待生效套餐为空)"` + ExpiresAt *time.Time `json:"expires_at,omitempty" description:"到期时间(待生效套餐为空)"` + MasterUsageID *uint `json:"master_usage_id,omitempty" description:"主套餐ID(加油包时有值)"` + Priority int `json:"priority" description:"优先级"` + PaidAmount *int64 `json:"paid_amount,omitempty" description:"购买实付金额(分),线下支付或无订单分配时为空"` + CreatedAt time.Time `json:"created_at" description:"创建时间"` } // AssetPackagesResult 套餐列表分页结果 @@ -180,10 +180,12 @@ type DeviceGatewayInfo struct { 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)"` + 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)"` + SignalQuality *string `json:"signal_quality,omitempty" description:"信号综合判断(信号很好/信号正常/信号较弱/信号很差/暂无数据)"` + SignalBadReason *string `json:"signal_bad_reason,omitempty" description:"信号较弱时的怀疑原因提示"` // WiFi 相关 SSID *string `json:"ssid,omitempty" description:"WiFi热点名称"` diff --git a/internal/model/dto/client_asset_dto.go b/internal/model/dto/client_asset_dto.go index 25d6671..15a54a1 100644 --- a/internal/model/dto/client_asset_dto.go +++ b/internal/model/dto/client_asset_dto.go @@ -89,10 +89,12 @@ type DeviceRealtimeInfo struct { LastUpdateTime *string `json:"last_update_time,omitempty" description:"设备信息最后更新时间"` // === 信号相关 === - Rsrp *int64 `json:"rsrp,omitempty" description:"参考信号接收功率(dBm)"` - Rsrq *int64 `json:"rsrq,omitempty" description:"参考信号接收质量(dB)"` - Rssi *string `json:"rssi,omitempty" description:"接收信号强度"` - Sinr *int64 `json:"sinr,omitempty" description:"信噪比(dB)"` + Rsrp *int64 `json:"rsrp,omitempty" description:"参考信号接收功率(dBm)"` + Rsrq *int64 `json:"rsrq,omitempty" description:"参考信号接收质量(dB)"` + Rssi *string `json:"rssi,omitempty" description:"接收信号强度"` + Sinr *int64 `json:"sinr,omitempty" description:"信噪比(dB)"` + SignalQuality *string `json:"signal_quality,omitempty" description:"信号综合判断(信号很好/信号正常/信号较弱/信号很差/暂无数据)"` + SignalBadReason *string `json:"signal_bad_reason,omitempty" description:"信号较弱时的怀疑原因提示"` // === WiFi 相关 === SSID *string `json:"ssid,omitempty" description:"WiFi热点名称"` diff --git a/internal/service/asset/service.go b/internal/service/asset/service.go index 5b33a5d..20222bc 100644 --- a/internal/service/asset/service.go +++ b/internal/service/asset/service.go @@ -481,6 +481,9 @@ func mapSyncRespToDeviceGatewayInfo(r *gateway.SyncDeviceInfoResp) *dto.DeviceGa IMSI: flexStrPtr(r.IMSI), Status: flexIntPtr(r.Status), } + signalQuality, signalBadReason := buildSignalSummary(info.Rsrp, info.Rsrq, info.Rssi, info.Sinr) + info.SignalQuality = strPtr(signalQuality) + info.SignalBadReason = strPtr(signalBadReason) if r.BatteryLevel != nil { n := int(*r.BatteryLevel) info.BatteryLevel = &n @@ -713,28 +716,28 @@ func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, pa pkgType = pkg.PackageType } item := &dto.AssetPackageResponse{ - PackageUsageID: u.ID, - PackageID: u.PackageID, - PackageName: pkgName, - PackageType: pkgType, - OrderID: u.OrderID, - OrderNo: u.OrderNo, - RefundID: u.RefundID, - RefundNo: u.RefundNo, - UsageType: u.UsageType, - Status: u.Status, - StatusName: packageStatusName(u.Status), - RealTotalMB: metrics.RealTotalMB, - RealUsedMB: metrics.RealUsedMB, - VirtualTotalMB: metrics.VirtualTotalMB, - VirtualUsedMB: metrics.VirtualUsedMB, - ReductionPct: metrics.ReductionPct, - ActivatedAt: u.ActivatedAt, - ExpiresAt: u.ExpiresAt, - MasterUsageID: u.MasterUsageID, - Priority: u.Priority, - PaidAmount: u.PaidAmount, - CreatedAt: u.CreatedAt, + PackageUsageID: u.ID, + PackageID: u.PackageID, + PackageName: pkgName, + PackageType: pkgType, + OrderID: u.OrderID, + OrderNo: u.OrderNo, + RefundID: u.RefundID, + RefundNo: u.RefundNo, + UsageType: u.UsageType, + Status: u.Status, + StatusName: packageStatusName(u.Status), + RealTotalMB: metrics.RealTotalMB, + RealUsedMB: metrics.RealUsedMB, + VirtualTotalMB: metrics.VirtualTotalMB, + VirtualUsedMB: metrics.VirtualUsedMB, + ReductionPct: metrics.ReductionPct, + ActivatedAt: u.ActivatedAt, + ExpiresAt: u.ExpiresAt, + MasterUsageID: u.MasterUsageID, + Priority: u.Priority, + PaidAmount: u.PaidAmount, + CreatedAt: u.CreatedAt, } all = append(all, item) } @@ -791,28 +794,28 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui pkgType = pkg.PackageType } return &dto.AssetPackageResponse{ - PackageUsageID: usage.ID, - PackageID: usage.PackageID, - PackageName: pkgName, - PackageType: pkgType, - OrderID: usage.OrderID, - OrderNo: usage.OrderNo, - RefundID: usage.RefundID, - RefundNo: usage.RefundNo, - UsageType: usage.UsageType, - Status: usage.Status, - StatusName: packageStatusName(usage.Status), - RealTotalMB: metrics.RealTotalMB, - RealUsedMB: metrics.RealUsedMB, - VirtualTotalMB: metrics.VirtualTotalMB, - VirtualUsedMB: metrics.VirtualUsedMB, - ReductionPct: metrics.ReductionPct, - ActivatedAt: usage.ActivatedAt, - ExpiresAt: usage.ExpiresAt, - MasterUsageID: usage.MasterUsageID, - Priority: usage.Priority, - PaidAmount: usage.PaidAmount, - CreatedAt: usage.CreatedAt, + PackageUsageID: usage.ID, + PackageID: usage.PackageID, + PackageName: pkgName, + PackageType: pkgType, + OrderID: usage.OrderID, + OrderNo: usage.OrderNo, + RefundID: usage.RefundID, + RefundNo: usage.RefundNo, + UsageType: usage.UsageType, + Status: usage.Status, + StatusName: packageStatusName(usage.Status), + RealTotalMB: metrics.RealTotalMB, + RealUsedMB: metrics.RealUsedMB, + VirtualTotalMB: metrics.VirtualTotalMB, + VirtualUsedMB: metrics.VirtualUsedMB, + ReductionPct: metrics.ReductionPct, + ActivatedAt: usage.ActivatedAt, + ExpiresAt: usage.ExpiresAt, + MasterUsageID: usage.MasterUsageID, + Priority: usage.Priority, + PaidAmount: usage.PaidAmount, + CreatedAt: usage.CreatedAt, }, nil } diff --git a/internal/service/asset/signal_summary.go b/internal/service/asset/signal_summary.go new file mode 100644 index 0000000..4c7e0ef --- /dev/null +++ b/internal/service/asset/signal_summary.go @@ -0,0 +1,159 @@ +package asset + +import "strings" + +const ( + signalQualityExcellent = "信号很好" + signalQualityNormal = "信号正常" + signalQualityWeak = "信号较弱" + signalQualityPoor = "信号很差" + signalQualityNoData = "暂无数据" + + signalReasonWeakCoverage = "怀疑当前位置信号覆盖较弱" + signalReasonInterference = "怀疑周围干扰较多" + signalReasonBlocked = "怀疑设备所处位置遮挡较强" + signalReasonUnstable = "怀疑网络环境不稳定" + signalReasonUnknown = "暂时无法判断" +) + +// buildSignalSummary 根据四个原始信号指标输出统一的信号摘要。 +func buildSignalSummary(rsrp, rsrq *int, rssi *string, sinr *int) (string, string) { + score := 0 + seen := 0 + coverageBad := false + interferenceBad := false + blockedBad := false + unstableBad := false + + if rsrp != nil { + seen++ + switch { + case *rsrp >= -90: + score += 2 + case *rsrp >= -105: + score += 1 + case *rsrp >= -115: + score -= 1 + coverageBad = true + default: + score -= 2 + coverageBad = true + blockedBad = true + } + } + + if rsrq != nil { + seen++ + switch { + case *rsrq >= -10: + score += 2 + case *rsrq >= -15: + score += 1 + case *rsrq >= -18: + score -= 1 + interferenceBad = true + default: + score -= 2 + interferenceBad = true + unstableBad = true + } + } + + if sinr != nil { + seen++ + switch { + case *sinr >= 20: + score += 2 + case *sinr >= 13: + score += 1 + case *sinr >= 5: + score -= 1 + interferenceBad = true + default: + score -= 2 + interferenceBad = true + unstableBad = true + } + } + + if rssiValue, ok := parseRSSI(rssi); ok { + seen++ + switch { + case rssiValue >= -65: + score += 2 + case rssiValue >= -75: + score += 1 + case rssiValue >= -85: + score -= 1 + coverageBad = true + default: + score -= 2 + coverageBad = true + blockedBad = true + } + } + + if seen == 0 { + return signalQualityNoData, signalReasonUnknown + } + + quality := signalQualityNormal + switch { + case score >= 6: + quality = signalQualityExcellent + case score >= 2: + quality = signalQualityNormal + case score >= -2: + quality = signalQualityWeak + default: + quality = signalQualityPoor + } + + if quality == signalQualityExcellent || quality == signalQualityNormal { + return quality, signalReasonUnknown + } + + switch { + case blockedBad: + return quality, signalReasonBlocked + case interferenceBad: + return quality, signalReasonInterference + case coverageBad: + return quality, signalReasonWeakCoverage + case unstableBad: + return quality, signalReasonUnstable + default: + return quality, signalReasonUnknown + } +} + +func parseRSSI(rssi *string) (int, bool) { + if rssi == nil { + return 0, false + } + value := strings.TrimSpace(*rssi) + if value == "" { + return 0, false + } + sign := 1 + number := 0 + started := false + for i, ch := range value { + if ch == '-' && i == 0 { + sign = -1 + continue + } + if ch >= '0' && ch <= '9' { + started = true + number = number*10 + int(ch-'0') + continue + } + if started { + break + } + } + if !started { + return 0, false + } + return sign * number, true +} diff --git a/internal/service/asset_audit/operation_content.go b/internal/service/asset_audit/operation_content.go index 2baf3f2..cb90577 100644 --- a/internal/service/asset_audit/operation_content.go +++ b/internal/service/asset_audit/operation_content.go @@ -5,22 +5,23 @@ import ( ) var snapshotFieldSet = map[string]struct{}{ - "id": {}, - "virtual_no": {}, - "iccid": {}, - "imei": {}, - "sn": {}, - "msisdn": {}, - "shop_id": {}, - "status": {}, - "series_id": {}, - "enable_polling": {}, - "realname_policy": {}, - "real_name_status": {}, - "network_status": {}, - "stop_reason": {}, - "deleted": {}, - "asset_status": {}, + "id": {}, + "virtual_no": {}, + "device_virtual_no": {}, + "iccid": {}, + "imei": {}, + "sn": {}, + "msisdn": {}, + "shop_id": {}, + "status": {}, + "series_id": {}, + "enable_polling": {}, + "realname_policy": {}, + "real_name_status": {}, + "network_status": {}, + "stop_reason": {}, + "deleted": {}, + "asset_status": {}, } // normalizeOperationContent 将原始 before/after 数据归一化为“操作内容”。 @@ -250,34 +251,49 @@ func getOperationFieldDesc(operationType string) map[string]string { switch strings.TrimSpace(operationType) { case "card_allocate", "device_allocate": return mergeFieldDesc(common, map[string]string{ - "target_shop_id": "目标店铺ID", - "to_shop_id": "目标店铺ID", - "device_ids": "设备ID列表", - "card_ids": "卡ID列表", - "selection_type": "选择方式", - "new_status": "变更后状态", + "target_shop_id": "目标店铺ID", + "target_shop_name": "目标店铺名称", + "to_shop_id": "目标店铺ID", + "to_shop_name": "目标店铺名称", + "device_ids": "设备ID列表", + "device_virtual_nos": "设备虚拟号列表", + "card_ids": "卡ID列表", + "iccids": "卡ICCID列表", + "selection_type": "选择方式", + "new_status": "变更后状态", }) case "card_recall", "device_recall": return mergeFieldDesc(common, map[string]string{ - "source_shop_id": "来源店铺ID", - "target_shop": "回收后归属店铺(空表示平台库存)", - "device_ids": "设备ID列表", - "card_ids": "卡ID列表", - "selection_type": "选择方式", - "new_status": "变更后状态", + "source_shop_id": "来源店铺ID", + "source_shop_name": "来源店铺名称", + "target_shop": "回收后归属店铺ID(空表示平台库存)", + "target_shop_name": "回收后归属店铺名称", + "device_ids": "设备ID列表", + "device_virtual_nos": "设备虚拟号列表", + "card_ids": "卡ID列表", + "iccids": "卡ICCID列表", + "selection_type": "选择方式", + "new_status": "变更后状态", }) case "device_bind_card": return map[string]string{ - "binding_id": "绑定记录ID", - "slot_position": "插槽位置", - "iot_card_id": "卡ID", - "iccid": "卡ICCID", + "binding_id": "绑定记录ID", + "slot_position": "插槽位置", + "device_id": "设备ID", + "device_virtual_no": "设备虚拟号", + "device_imei": "设备IMEI", + "iot_card_id": "卡ID", + "iccid": "卡ICCID", } case "device_unbind_card": return map[string]string{ - "binding_id": "绑定记录ID", - "iot_card_id": "卡ID", - "unbind": "是否执行解绑", + "binding_id": "绑定记录ID", + "device_id": "设备ID", + "device_virtual_no": "设备虚拟号", + "device_imei": "设备IMEI", + "iot_card_id": "卡ID", + "iccid": "卡ICCID", + "unbind": "是否执行解绑", } case "device_stop", "device_start": return mergeFieldDesc(common, map[string]string{ @@ -297,7 +313,9 @@ func getOperationFieldDesc(operationType string) map[string]string { } case "device_switch_card": return map[string]string{ - "target_iccid": "目标ICCID", + "device_virtual_no": "设备虚拟号", + "device_imei": "设备IMEI", + "target_iccid": "目标ICCID", } case "device_switch_mode": return map[string]string{ @@ -312,9 +330,11 @@ func getOperationFieldDesc(operationType string) map[string]string { "action": "设备恢复出厂动作", } case "asset_polling_status", "card_polling_status": - return map[string]string{ + return mergeFieldDesc(common, map[string]string{ "enable_polling": "轮询开关(true启用/false禁用)", - } + "card_ids": "卡ID列表", + "iccids": "卡ICCID列表", + }) case "asset_realname_policy", "card_realname_policy": return map[string]string{ "realname_policy": "实名策略(none/before_order/after_order)", @@ -324,10 +344,13 @@ func getOperationFieldDesc(operationType string) map[string]string { "real_name_status": "实名状态(0未实名/1已实名)", } case "asset_deactivate", "card_delete", "card_batch_delete", "device_delete": - return map[string]string{ - "asset_status": "资产状态", - "deleted": "是否删除", - } + return mergeFieldDesc(common, map[string]string{ + "asset_status": "资产状态", + "deleted": "是否删除", + "iccids": "卡ICCID列表", + "device_virtual_nos": "设备虚拟号列表", + "device_imei": "设备IMEI", + }) case "device_import_task_create", "iot_card_import_task_create": return map[string]string{ "batch_no": "批次号", @@ -337,7 +360,19 @@ func getOperationFieldDesc(operationType string) map[string]string { "card_category": "卡类型(仅卡导入)", } default: - return common + return mergeFieldDesc(common, map[string]string{ + "card_id": "卡ID", + "iccid": "卡ICCID", + "iccids": "卡ICCID列表", + "device_id": "设备ID", + "device_virtual_no": "设备虚拟号", + "device_virtual_nos": "设备虚拟号列表", + "device_imei": "设备IMEI", + "device_sn": "设备SN", + "shop_name": "店铺名称", + "target_shop_name": "目标店铺名称", + "source_shop_name": "来源店铺名称", + }) } } diff --git a/internal/service/device/audit.go b/internal/service/device/audit.go index e4b3b0b..6af200e 100644 --- a/internal/service/device/audit.go +++ b/internal/service/device/audit.go @@ -2,6 +2,7 @@ package device import ( "context" + "fmt" "strings" "github.com/break/junhong_cmp_fiber/internal/model" @@ -62,6 +63,8 @@ func (s *Service) logDeviceOperation( snapshot := deviceSnapshot(device) beforeContent := stripDeviceOperationMeta(beforeData) afterContent := stripDeviceOperationMeta(afterData) + enrichDeviceOperationContent(beforeContent, beforeData) + enrichDeviceOperationContent(afterContent, afterData) params.BeforeData, params.AfterData = assetAuditSvc.WrapOperationContent(beforeContent, afterContent, snapshot) if device != nil { @@ -81,7 +84,7 @@ func stripDeviceOperationMeta(data map[string]any) map[string]any { } out := make(map[string]any, len(data)) for k, v := range data { - if k == "device" || k == "card" || k == "cards" || k == "asset_snapshot" || k == "operation_content" { + if k == "device" || k == "devices" || k == "card" || k == "cards" || k == "asset_snapshot" || k == "operation_content" { continue } out[k] = v @@ -92,6 +95,116 @@ func stripDeviceOperationMeta(data map[string]any) map[string]any { return out } +func enrichDeviceOperationContent(content map[string]any, raw map[string]any) { + if len(raw) == 0 { + return + } + + if deviceRaw, ok := raw["device"].(map[string]any); ok { + mergeReadableDeviceFields(content, deviceRaw) + } + if cardRaw, ok := raw["card"].(map[string]any); ok { + mergeReadableCardFields(content, cardRaw) + } + + switch devicesRaw := raw["devices"].(type) { + case []map[string]any: + deviceIDs, virtualNos := collectDeviceReadableLists(devicesRaw) + if len(deviceIDs) > 0 && content["device_ids"] == nil { + content["device_ids"] = deviceIDs + } + if len(virtualNos) > 0 && content["device_virtual_nos"] == nil { + content["device_virtual_nos"] = virtualNos + } + case []any: + devices := make([]map[string]any, 0, len(devicesRaw)) + for _, item := range devicesRaw { + deviceMap, ok := item.(map[string]any) + if !ok { + continue + } + devices = append(devices, deviceMap) + } + deviceIDs, virtualNos := collectDeviceReadableLists(devices) + if len(deviceIDs) > 0 && content["device_ids"] == nil { + content["device_ids"] = deviceIDs + } + if len(virtualNos) > 0 && content["device_virtual_nos"] == nil { + content["device_virtual_nos"] = virtualNos + } + } +} + +func mergeReadableDeviceFields(content map[string]any, device map[string]any) { + if len(device) == 0 { + return + } + if _, ok := content["device_id"]; !ok { + if v, exists := device["id"]; exists { + content["device_id"] = v + } + } + if _, ok := content["device_virtual_no"]; !ok { + if v := stringifyDeviceAuditValue(device["virtual_no"]); v != "" { + content["device_virtual_no"] = v + } + } + if _, ok := content["device_imei"]; !ok { + if v := stringifyDeviceAuditValue(device["imei"]); v != "" { + content["device_imei"] = v + } + } + if _, ok := content["device_sn"]; !ok { + if v := stringifyDeviceAuditValue(device["sn"]); v != "" { + content["device_sn"] = v + } + } +} + +func mergeReadableCardFields(content map[string]any, card map[string]any) { + if len(card) == 0 { + return + } + if _, ok := content["iot_card_id"]; !ok { + if v, exists := card["id"]; exists { + content["iot_card_id"] = v + } + } + if _, ok := content["iccid"]; !ok { + if v := stringifyDeviceAuditValue(card["iccid"]); v != "" { + content["iccid"] = v + } + } +} + +func collectDeviceReadableLists(devices []map[string]any) ([]any, []string) { + deviceIDs := make([]any, 0, len(devices)) + virtualNos := make([]string, 0, len(devices)) + for _, device := range devices { + if id, ok := device["id"]; ok { + deviceIDs = append(deviceIDs, id) + } + if virtualNo := stringifyDeviceAuditValue(device["virtual_no"]); virtualNo != "" { + virtualNos = append(virtualNos, virtualNo) + } + } + return deviceIDs, virtualNos +} + +func stringifyDeviceAuditValue(v any) string { + switch vv := v.(type) { + case string: + return vv + case fmt.Stringer: + return vv.String() + default: + if vv == nil { + return "" + } + return fmt.Sprint(vv) + } +} + func deviceSnapshot(device *model.Device) map[string]any { if device == nil { return nil @@ -99,6 +212,8 @@ func deviceSnapshot(device *model.Device) map[string]any { return map[string]any{ "id": device.ID, "virtual_no": device.VirtualNo, + "imei": device.IMEI, + "sn": device.SN, "shop_id": device.ShopID, "status": device.Status, "series_id": device.SeriesID, diff --git a/internal/service/device/service.go b/internal/service/device/service.go index 121218e..4f6b70c 100644 --- a/internal/service/device/service.go +++ b/internal/service/device/service.go @@ -450,13 +450,16 @@ func (s *Service) AllocateDevices(ctx context.Context, req *dto.AllocateDevicesR }, nil } - beforeData := make([]map[string]any, 0, len(devices)) - for _, device := range devices { - beforeData = append(beforeData, deviceSnapshot(device)) - } - newStatus := constants.DeviceStatusDistributed targetShopID := req.TargetShopID + targetShopName := s.getAuditShopName(ctx, &targetShopID) + shopMap := s.loadShopData(ctx, devices) + beforeData := make([]map[string]any, 0, len(devices)) + for _, device := range devices { + snapshot := deviceSnapshot(device) + snapshot["shop_name"] = deviceShopMapValue(shopMap, device.ShopID) + beforeData = append(beforeData, snapshot) + } err = s.db.Transaction(func(tx *gorm.DB) error { txDeviceStore := postgres.NewDeviceStore(tx, nil) @@ -492,8 +495,9 @@ func (s *Service) AllocateDevices(ctx context.Context, req *dto.AllocateDevicesR nil, map[string]any{"devices": beforeData}, map[string]any{ - "target_shop_id": req.TargetShopID, - "failed_items": failedItems, + "target_shop_id": req.TargetShopID, + "target_shop_name": targetShopName, + "failed_items": failedItems, }, len(devices), len(deviceIDs), @@ -511,9 +515,10 @@ func (s *Service) AllocateDevices(ctx context.Context, req *dto.AllocateDevicesR nil, map[string]any{"devices": beforeData}, map[string]any{ - "target_shop_id": req.TargetShopID, - "device_ids": deviceIDs, - "failed_items": failedItems, + "target_shop_id": req.TargetShopID, + "target_shop_name": targetShopName, + "device_ids": deviceIDs, + "failed_items": failedItems, }, len(devices), len(deviceIDs), @@ -629,11 +634,6 @@ func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesReque }, nil } - beforeData := make([]map[string]any, 0, len(devices)) - for _, device := range devices { - beforeData = append(beforeData, deviceSnapshot(device)) - } - var newShopID *uint var newStatus int if isPlatform { @@ -643,6 +643,14 @@ func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesReque newShopID = operatorShopID newStatus = constants.DeviceStatusDistributed } + targetShopName := s.getAuditRecallTargetShopName(ctx, newShopID) + shopMap := s.loadShopData(ctx, devices) + beforeData := make([]map[string]any, 0, len(devices)) + for _, device := range devices { + snapshot := deviceSnapshot(device) + snapshot["shop_name"] = deviceShopMapValue(shopMap, device.ShopID) + beforeData = append(beforeData, snapshot) + } err = s.db.Transaction(func(tx *gorm.DB) error { txDeviceStore := postgres.NewDeviceStore(tx, nil) @@ -684,8 +692,9 @@ func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesReque nil, map[string]any{"devices": beforeData}, map[string]any{ - "device_ids": deviceIDs, - "failed_items": failedItems, + "device_ids": deviceIDs, + "failed_items": failedItems, + "target_shop_name": targetShopName, }, len(devices), len(deviceIDs), @@ -703,9 +712,10 @@ func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesReque nil, map[string]any{"devices": beforeData}, map[string]any{ - "device_ids": deviceIDs, - "failed_items": failedItems, - "target_shop": newShopID, + "device_ids": deviceIDs, + "failed_items": failedItems, + "target_shop": newShopID, + "target_shop_name": targetShopName, }, len(devices), len(deviceIDs), @@ -748,6 +758,31 @@ func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID return nil } +func (s *Service) getAuditShopName(ctx context.Context, shopID *uint) string { + if shopID == nil || *shopID == 0 { + return "" + } + var shop model.Shop + if err := s.db.WithContext(ctx).Unscoped().First(&shop, *shopID).Error; err != nil { + return "" + } + return shop.ShopName +} + +func (s *Service) getAuditRecallTargetShopName(ctx context.Context, shopID *uint) string { + if shopID == nil { + return "平台库存" + } + return s.getAuditShopName(ctx, shopID) +} + +func deviceShopMapValue(shopMap map[uint]string, shopID *uint) string { + if shopID == nil { + return "" + } + return shopMap[*shopID] +} + func (s *Service) loadShopData(ctx context.Context, devices []*model.Device) map[uint]string { shopIDs := make([]uint, 0) shopIDSet := make(map[uint]bool) diff --git a/internal/service/iot_card/audit.go b/internal/service/iot_card/audit.go index 8d9292f..59e3d90 100644 --- a/internal/service/iot_card/audit.go +++ b/internal/service/iot_card/audit.go @@ -2,6 +2,7 @@ package iot_card import ( "context" + "fmt" "github.com/break/junhong_cmp_fiber/internal/model" assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" @@ -55,7 +56,11 @@ func normalizeCardAuditPayload(beforeData, afterData map[string]any) (map[string } } } - return assetAuditSvc.WrapOperationContent(stripCardOperationMeta(beforeData), stripCardOperationMeta(afterData), snapshot) + beforeContent := stripCardOperationMeta(beforeData) + afterContent := stripCardOperationMeta(afterData) + enrichCardOperationContent(beforeContent, beforeData) + enrichCardOperationContent(afterContent, afterData) + return assetAuditSvc.WrapOperationContent(beforeContent, afterContent, snapshot) } func stripCardOperationMeta(data map[string]any) map[string]any { @@ -75,20 +80,136 @@ func stripCardOperationMeta(data map[string]any) map[string]any { return out } +func enrichCardOperationContent(content map[string]any, raw map[string]any) { + if len(raw) == 0 { + return + } + + if cardRaw, ok := raw["card"].(map[string]any); ok { + mergeReadableCardFields(content, cardRaw) + } + if deviceRaw, ok := raw["device"].(map[string]any); ok { + mergeReadableDeviceFields(content, deviceRaw) + } + + switch cardsRaw := raw["cards"].(type) { + case []map[string]any: + cardIDs, iccids := collectCardReadableLists(cardsRaw) + if len(cardIDs) > 0 && content["card_ids"] == nil { + content["card_ids"] = cardIDs + } + if len(iccids) > 0 && content["iccids"] == nil { + content["iccids"] = iccids + } + case []any: + cards := make([]map[string]any, 0, len(cardsRaw)) + for _, item := range cardsRaw { + cardMap, ok := item.(map[string]any) + if !ok { + continue + } + cards = append(cards, cardMap) + } + cardIDs, iccids := collectCardReadableLists(cards) + if len(cardIDs) > 0 && content["card_ids"] == nil { + content["card_ids"] = cardIDs + } + if len(iccids) > 0 && content["iccids"] == nil { + content["iccids"] = iccids + } + } +} + +func mergeReadableCardFields(content map[string]any, card map[string]any) { + if len(card) == 0 { + return + } + if _, ok := content["card_id"]; !ok { + if v, exists := card["id"]; exists { + content["card_id"] = v + } + } + if _, ok := content["iccid"]; !ok { + if v := stringifyCardAuditValue(card["iccid"]); v != "" { + content["iccid"] = v + } + } + if _, ok := content["device_virtual_no"]; !ok { + if v := stringifyCardAuditValue(card["device_virtual_no"]); v != "" { + content["device_virtual_no"] = v + } + } +} + +func mergeReadableDeviceFields(content map[string]any, device map[string]any) { + if len(device) == 0 { + return + } + if _, ok := content["device_id"]; !ok { + if v, exists := device["id"]; exists { + content["device_id"] = v + } + } + if _, ok := content["device_virtual_no"]; !ok { + if v := stringifyCardAuditValue(device["virtual_no"]); v != "" { + content["device_virtual_no"] = v + } + } + if _, ok := content["device_imei"]; !ok { + if v := stringifyCardAuditValue(device["imei"]); v != "" { + content["device_imei"] = v + } + } + if _, ok := content["device_sn"]; !ok { + if v := stringifyCardAuditValue(device["sn"]); v != "" { + content["device_sn"] = v + } + } +} + +func collectCardReadableLists(cards []map[string]any) ([]any, []string) { + cardIDs := make([]any, 0, len(cards)) + iccids := make([]string, 0, len(cards)) + for _, card := range cards { + if id, ok := card["id"]; ok { + cardIDs = append(cardIDs, id) + } + if iccid := stringifyCardAuditValue(card["iccid"]); iccid != "" { + iccids = append(iccids, iccid) + } + } + return cardIDs, iccids +} + +func stringifyCardAuditValue(v any) string { + switch vv := v.(type) { + case string: + return vv + case fmt.Stringer: + return vv.String() + default: + if vv == nil { + return "" + } + return fmt.Sprint(vv) + } +} + func cardSnapshot(card *model.IotCard) map[string]any { if card == nil { return nil } return map[string]any{ - "id": card.ID, - "iccid": card.ICCID, - "shop_id": card.ShopID, - "status": card.Status, - "series_id": card.SeriesID, - "enable_polling": card.EnablePolling, - "realname_policy": card.RealnamePolicy, - "real_name_status": card.RealNameStatus, - "network_status": card.NetworkStatus, - "stop_reason": card.StopReason, + "id": card.ID, + "iccid": card.ICCID, + "device_virtual_no": card.DeviceVirtualNo, + "shop_id": card.ShopID, + "status": card.Status, + "series_id": card.SeriesID, + "enable_polling": card.EnablePolling, + "realname_policy": card.RealnamePolicy, + "real_name_status": card.RealNameStatus, + "network_status": card.NetworkStatus, + "stop_reason": card.StopReason, } } diff --git a/internal/service/iot_card/service.go b/internal/service/iot_card/service.go index bb2a7e3..7de259b 100644 --- a/internal/service/iot_card/service.go +++ b/internal/service/iot_card/service.go @@ -494,13 +494,16 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone } } + shopMap := s.loadShopNames(ctx, cards) + targetShopName := s.getAuditShopName(ctx, &req.ToShopID) beforeData := make([]map[string]any, 0, len(cards)) for _, card := range cards { beforeData = append(beforeData, map[string]any{ - "id": card.ID, - "iccid": card.ICCID, - "shop_id": card.ShopID, - "status": card.Status, + "id": card.ID, + "iccid": card.ICCID, + "shop_id": card.ShopID, + "shop_name": shopMapValue(shopMap, card.ShopID), + "status": card.Status, }) } s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ @@ -515,6 +518,7 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone }, AfterData: map[string]any{ "to_shop_id": req.ToShopID, + "to_shop_name": targetShopName, "new_status": newStatus, "failed_items": failedItems, }, @@ -702,13 +706,16 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard } } + shopMap := s.loadShopNames(ctx, successCards) + targetShopName := s.getAuditRecallTargetShopName(ctx, newShopID) beforeData := make([]map[string]any, 0, len(successCards)) for _, card := range successCards { beforeData = append(beforeData, map[string]any{ - "id": card.ID, - "iccid": card.ICCID, - "shop_id": card.ShopID, - "status": card.Status, + "id": card.ID, + "iccid": card.ICCID, + "shop_id": card.ShopID, + "shop_name": shopMapValue(shopMap, card.ShopID), + "status": card.Status, }) } s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ @@ -722,9 +729,10 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard "cards": beforeData, }, AfterData: map[string]any{ - "to_shop_id": newShopID, - "new_status": newStatus, - "failed_items": failedItems, + "to_shop_id": newShopID, + "target_shop_name": targetShopName, + "new_status": newStatus, + "failed_items": failedItems, }, }) @@ -774,6 +782,31 @@ func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID return nil } +func (s *Service) getAuditShopName(ctx context.Context, shopID *uint) string { + if shopID == nil || *shopID == 0 { + return "" + } + var shop model.Shop + if err := s.db.WithContext(ctx).Unscoped().First(&shop, *shopID).Error; err != nil { + return "" + } + return shop.ShopName +} + +func (s *Service) getAuditRecallTargetShopName(ctx context.Context, shopID *uint) string { + if shopID == nil { + return "平台库存" + } + return s.getAuditShopName(ctx, shopID) +} + +func shopMapValue(shopMap map[uint]string, shopID *uint) string { + if shopID == nil { + return "" + } + return shopMap[*shopID] +} + func (s *Service) getCardsForAllocation(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorShopID *uint) ([]*model.IotCard, error) { switch req.SelectionType { case dto.SelectionTypeList: @@ -1443,9 +1476,9 @@ func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint, BatchTotal: len(cardIDs), FailCount: len(cardIDs), AfterData: map[string]any{ - "card_ids": cardIDs, - "enable_polling": enablePolling, - "trigger_source": "batch", + "card_ids": cardIDs, + "enable_polling": enablePolling, + "trigger_source": "batch", }, }) return err @@ -1857,11 +1890,11 @@ func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, r AssetID: freshCard.ID, AssetIdentifier: freshCard.ICCID, BeforeData: map[string]any{ - "real_name_status": oldStatus, + "real_name_status": oldStatus, "first_realname_at": card.FirstRealnameAt, }, AfterData: map[string]any{ - "real_name_status": freshCard.RealNameStatus, + "real_name_status": freshCard.RealNameStatus, "first_realname_at": freshCard.FirstRealnameAt, }, })