Files
junhong_cmp_fiber/internal/service/asset_audit/operation_content.go
huang c3ae7fcfbc
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m40s
修复设备WiFi上游请求参数
2026-04-27 15:19:23 +08:00

354 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package asset_audit
import (
"strings"
)
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": {},
}
// 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",
"to_shop_id": "目标店铺ID",
"device_ids": "设备ID列表",
"card_ids": "卡ID列表",
"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": "变更后状态",
})
case "device_bind_card":
return map[string]string{
"binding_id": "绑定记录ID",
"slot_position": "插槽位置",
"iot_card_id": "卡ID",
"iccid": "卡ICCID",
}
case "device_unbind_card":
return map[string]string{
"binding_id": "绑定记录ID",
"iot_card_id": "卡ID",
"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{
"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 map[string]string{
"enable_polling": "轮询开关true启用/false禁用",
}
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 map[string]string{
"asset_status": "资产状态",
"deleted": "是否删除",
}
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 common
}
}
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
}