package asset_audit import ( "strings" ) var snapshotFieldSet = map[string]struct{}{ "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 数据归一化为“操作内容”。 // 兼容历史数据: // 1. 优先读取 operation_content; // 2. 若 before 仅有资产快照而 after 是操作内容,则按字段名回填 before; // 3. 若 after 为空则按 before 补齐,保证前端有稳定结构。 func normalizeOperationContent(operationType string, beforeData, afterData map[string]any) (map[string]any, map[string]any) { beforeRaw := extractOperationContent(beforeData) afterRaw := extractOperationContent(afterData) before := trimNonOperationKeys(beforeRaw) after := trimNonOperationKeys(afterRaw) snapshot := extractAssetSnapshot(beforeData, afterData) // 历史数据可能把扁平资产快照直接写到 before_data。 // 这种情况下 before 不是“操作内容”,需要按 after 字段从快照回填。 if isLikelySnapshotInsteadOfOperation(before, after, operationType) { if len(snapshot) == 0 { snapshot = cloneMap(beforeRaw) } before = deriveBeforeFromSnapshot(after, snapshot) } if len(before) == 0 && len(after) > 0 { before = deriveBeforeFromSnapshot(after, snapshot) } if len(after) == 0 && len(before) > 0 { after = cloneMap(before) } return before, after } // WrapOperationContent 将操作内容包装为统一结构写入 before_data/after_data。 // 写入格式: // // { // "operation_content": {...}, // "asset_snapshot": {...} // 可选 // } func WrapOperationContent(beforeOperation, afterOperation, snapshot map[string]any) (map[string]any, map[string]any) { before := cloneMap(beforeOperation) after := cloneMap(afterOperation) if len(before) == 0 && len(after) > 0 { before = deriveBeforeFromSnapshot(after, snapshot) } if len(after) == 0 && len(before) > 0 { after = cloneMap(before) } beforeWrapped := map[string]any{ "operation_content": before, } afterWrapped := map[string]any{ "operation_content": after, } if len(snapshot) > 0 { beforeWrapped["asset_snapshot"] = cloneMap(snapshot) afterWrapped["asset_snapshot"] = cloneMap(snapshot) } return beforeWrapped, afterWrapped } func extractOperationContent(data map[string]any) map[string]any { if len(data) == 0 { return nil } if v, ok := data["operation_content"]; ok { if m, ok := toMap(v); ok { return cloneMap(m) } } return cloneMap(data) } func extractAssetSnapshot(beforeData, afterData map[string]any) map[string]any { candidates := []map[string]any{beforeData, afterData} for _, data := range candidates { if len(data) == 0 { continue } if v, ok := data["asset_snapshot"]; ok { if m, ok := toMap(v); ok { return cloneMap(m) } } if v, ok := data["device"]; ok { if m, ok := toMap(v); ok { return cloneMap(m) } } if v, ok := data["card"]; ok { if m, ok := toMap(v); ok { return cloneMap(m) } } if isLikelyAssetSnapshot(data) { return cloneMap(data) } } return nil } func trimNonOperationKeys(in map[string]any) map[string]any { if len(in) == 0 { return nil } out := cloneMap(in) delete(out, "operation_content") delete(out, "asset_snapshot") delete(out, "operation_context") delete(out, "device") delete(out, "card") delete(out, "cards") if len(out) == 0 { return nil } return out } func deriveBeforeFromSnapshot(after map[string]any, snapshot map[string]any) map[string]any { if len(after) == 0 { return nil } out := make(map[string]any, len(after)) for key := range after { if val, ok := snapshot[key]; ok { out[key] = val continue } if alias := snapshotAlias(key); alias != "" { if val, ok := snapshot[alias]; ok { out[key] = val continue } } out[key] = nil } return out } func snapshotAlias(operationField string) string { switch operationField { case "target_shop_id", "to_shop_id", "source_shop_id", "from_shop_id": return "shop_id" case "new_status": return "status" case "new_series_id": return "series_id" case "new_realname_policy": return "realname_policy" case "new_enable_polling": return "enable_polling" case "new_switch_mode": return "switch_mode" default: return "" } } func cloneMap(in map[string]any) map[string]any { if len(in) == 0 { return nil } out := make(map[string]any, len(in)) for k, v := range in { out[k] = v } return out } func toMap(v any) (map[string]any, bool) { switch val := v.(type) { case map[string]any: return val, true default: return nil, false } } func isLikelyAssetSnapshot(data map[string]any) bool { if len(data) == 0 { return false } snapshotCount := 0 for key := range data { if _, ok := snapshotFieldSet[key]; !ok { return false } snapshotCount++ } return snapshotCount >= 2 } func isLikelySnapshotInsteadOfOperation(before, after map[string]any, operationType string) bool { if len(before) == 0 || len(after) == 0 { return false } if !isLikelyAssetSnapshot(before) { return false } fieldDesc := getOperationFieldDesc(operationType) for key := range after { if _, ok := fieldDesc[key]; ok { return true } if _, ok := snapshotFieldSet[key]; !ok { return true } } return false } func getOperationFieldDesc(operationType string) map[string]string { common := map[string]string{ "batch_total": "批量总数", "success_count": "成功数量", "fail_count": "失败数量", "failed_items": "失败明细", "reason": "原因说明", } switch strings.TrimSpace(operationType) { case "card_allocate", "device_allocate": return mergeFieldDesc(common, map[string]string{ "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", "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": "插槽位置", "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", "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{ "skip_count": "跳过数量", "failed_items": "失败明细(含ICCID与原因)", }) case "device_speed_limit": return map[string]string{ "speed_limit": "限速值(KB/s)", } case "device_set_wifi": return map[string]string{ "imei": "设备 IMEI", "ssid": "WiFi 名称", "password": "WiFi 密码(脱敏)", "enabled": "WiFi 开关状态", } case "device_switch_card": return map[string]string{ "device_virtual_no": "设备虚拟号", "device_imei": "设备IMEI", "target_iccid": "目标ICCID", } case "device_switch_mode": return map[string]string{ "switch_mode": "切卡模式(0自动/1手动)", } case "device_reboot": return map[string]string{ "action": "设备重启动作", } case "device_reset": return map[string]string{ "action": "设备恢复出厂动作", } case "asset_polling_status", "card_polling_status": 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)", } case "card_realname_status": return map[string]string{ "real_name_status": "实名状态(0未实名/1已实名)", } case "asset_deactivate", "card_delete", "card_batch_delete", "device_delete": 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": "批次号", "file_key": "文件存储键", "realname_policy": "实名策略", "carrier_id": "运营商ID(仅卡导入)", "card_category": "卡类型(仅卡导入)", } default: 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": "来源店铺名称", }) } } func mergeFieldDesc(base, extra map[string]string) map[string]string { out := make(map[string]string, len(base)+len(extra)) for k, v := range base { out[k] = v } for k, v := range extra { out[k] = v } return out }