407 lines
11 KiB
Go
407 lines
11 KiB
Go
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 "asset_package_expires_at":
|
||
return map[string]string{
|
||
"package_usage_id": "套餐使用记录ID",
|
||
"expires_at": "套餐过期时间",
|
||
"status": "套餐状态",
|
||
"status_name": "套餐状态名称",
|
||
}
|
||
case "asset_package_usage":
|
||
return map[string]string{
|
||
"package_usage_id": "套餐使用记录ID",
|
||
"data_usage_mb": "套餐真实已用量(MB)",
|
||
"virtual_used_mb": "套餐展示已用量(MB)",
|
||
"real_total_mb": "套餐真实总量(MB)",
|
||
"virtual_total_mb": "套餐业务停机阈值(MB)",
|
||
"status": "套餐状态",
|
||
"status_name": "套餐状态名称",
|
||
"enable_virtual_data": "是否启用虚流量",
|
||
}
|
||
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
|
||
}
|