This commit is contained in:
@@ -27,6 +27,10 @@ type AssetOperationLogItem struct {
|
||||
OperationType string `json:"operation_type" description:"操作类型"`
|
||||
OperationDesc string `json:"operation_desc" description:"操作描述(中文)"`
|
||||
|
||||
OperationContentBefore map[string]any `json:"operation_content_before,omitempty" description:"操作内容(变更前)"`
|
||||
OperationContentAfter map[string]any `json:"operation_content_after,omitempty" description:"操作内容(变更后)"`
|
||||
OperationFieldsDesc map[string]string `json:"operation_fields_desc,omitempty" description:"操作内容字段说明(key 为字段名,value 为中文说明)"`
|
||||
|
||||
BeforeData map[string]any `json:"before_data,omitempty" description:"变更前数据"`
|
||||
AfterData map[string]any `json:"after_data,omitempty" description:"变更后数据"`
|
||||
|
||||
|
||||
353
internal/service/asset_audit/operation_content.go
Normal file
353
internal/service/asset_audit/operation_content.go
Normal file
@@ -0,0 +1,353 @@
|
||||
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{
|
||||
"card_no": "卡槽/卡序号",
|
||||
"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
|
||||
}
|
||||
@@ -121,29 +121,37 @@ func toAssetOperationLogItem(log *model.AssetOperationLog) *dto.AssetOperationLo
|
||||
if log == nil {
|
||||
return nil
|
||||
}
|
||||
operationBefore, operationAfter := normalizeOperationContent(
|
||||
log.OperationType,
|
||||
map[string]any(log.BeforeData),
|
||||
map[string]any(log.AfterData),
|
||||
)
|
||||
return &dto.AssetOperationLogItem{
|
||||
ID: log.ID,
|
||||
CreatedAt: log.CreatedAt,
|
||||
OperatorID: log.OperatorID,
|
||||
OperatorType: log.OperatorType,
|
||||
OperatorName: log.OperatorName,
|
||||
AssetType: log.AssetType,
|
||||
AssetID: log.AssetID,
|
||||
AssetIdentifier: log.AssetIdentifier,
|
||||
OperationType: log.OperationType,
|
||||
OperationDesc: log.OperationDesc,
|
||||
BeforeData: map[string]any(log.BeforeData),
|
||||
AfterData: map[string]any(log.AfterData),
|
||||
RequestID: log.RequestID,
|
||||
IPAddress: log.IPAddress,
|
||||
UserAgent: log.UserAgent,
|
||||
RequestPath: log.RequestPath,
|
||||
RequestMethod: log.RequestMethod,
|
||||
ResultStatus: log.ResultStatus,
|
||||
ErrorCode: log.ErrorCode,
|
||||
ErrorMsg: log.ErrorMsg,
|
||||
BatchTotal: log.BatchTotal,
|
||||
SuccessCount: log.SuccessCount,
|
||||
FailCount: log.FailCount,
|
||||
ID: log.ID,
|
||||
CreatedAt: log.CreatedAt,
|
||||
OperatorID: log.OperatorID,
|
||||
OperatorType: log.OperatorType,
|
||||
OperatorName: log.OperatorName,
|
||||
AssetType: log.AssetType,
|
||||
AssetID: log.AssetID,
|
||||
AssetIdentifier: log.AssetIdentifier,
|
||||
OperationType: log.OperationType,
|
||||
OperationDesc: log.OperationDesc,
|
||||
OperationContentBefore: operationBefore,
|
||||
OperationContentAfter: operationAfter,
|
||||
OperationFieldsDesc: getOperationFieldDesc(log.OperationType),
|
||||
BeforeData: map[string]any(log.BeforeData),
|
||||
AfterData: map[string]any(log.AfterData),
|
||||
RequestID: log.RequestID,
|
||||
IPAddress: log.IPAddress,
|
||||
UserAgent: log.UserAgent,
|
||||
RequestPath: log.RequestPath,
|
||||
RequestMethod: log.RequestMethod,
|
||||
ResultStatus: log.ResultStatus,
|
||||
ErrorCode: log.ErrorCode,
|
||||
ErrorMsg: log.ErrorMsg,
|
||||
BatchTotal: log.BatchTotal,
|
||||
SuccessCount: log.SuccessCount,
|
||||
FailCount: log.FailCount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +55,15 @@ func (s *Service) logDeviceOperation(
|
||||
OperationType: operationType,
|
||||
OperationDesc: operationDesc,
|
||||
ResultStatus: resultStatus,
|
||||
BeforeData: beforeData,
|
||||
AfterData: afterData,
|
||||
BatchTotal: batchTotal,
|
||||
SuccessCount: successCount,
|
||||
FailCount: failCount,
|
||||
}
|
||||
snapshot := deviceSnapshot(device)
|
||||
beforeContent := stripDeviceOperationMeta(beforeData)
|
||||
afterContent := stripDeviceOperationMeta(afterData)
|
||||
params.BeforeData, params.AfterData = assetAuditSvc.WrapOperationContent(beforeContent, afterContent, snapshot)
|
||||
|
||||
if device != nil {
|
||||
params.AssetID = device.ID
|
||||
params.AssetIdentifier = device.VirtualNo
|
||||
@@ -72,6 +75,23 @@ func (s *Service) logDeviceOperation(
|
||||
s.logDeviceAudit(ctx, params)
|
||||
}
|
||||
|
||||
func stripDeviceOperationMeta(data map[string]any) map[string]any {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
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" {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func deviceSnapshot(device *model.Device) map[string]any {
|
||||
if device == nil {
|
||||
return nil
|
||||
|
||||
@@ -27,6 +27,7 @@ func (s *Service) logDeviceImportAudit(ctx context.Context, p assetAuditSvc.Buil
|
||||
if p.AssetType == "" {
|
||||
p.AssetType = constants.AssetTypeDevice
|
||||
}
|
||||
p.BeforeData, p.AfterData = assetAuditSvc.WrapOperationContent(p.BeforeData, p.AfterData, nil)
|
||||
s.assetAudit.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package iot_card
|
||||
import (
|
||||
"context"
|
||||
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ func (s *Service) logCardAudit(ctx context.Context, p assetAuditSvc.BuildLogPara
|
||||
if p.AssetType == "" {
|
||||
p.AssetType = constants.AssetTypeIotCard
|
||||
}
|
||||
p.BeforeData, p.AfterData = normalizeCardAuditPayload(p.BeforeData, p.AfterData)
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
@@ -36,23 +37,58 @@ func (s *StopResumeService) logCardAudit(ctx context.Context, p assetAuditSvc.Bu
|
||||
if p.AssetType == "" {
|
||||
p.AssetType = constants.AssetTypeIotCard
|
||||
}
|
||||
p.BeforeData, p.AfterData = normalizeCardAuditPayload(p.BeforeData, p.AfterData)
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
func normalizeCardAuditPayload(beforeData, afterData map[string]any) (map[string]any, map[string]any) {
|
||||
snapshot := map[string]any(nil)
|
||||
if raw, ok := beforeData["card"]; ok {
|
||||
if m, ok := raw.(map[string]any); ok {
|
||||
snapshot = m
|
||||
}
|
||||
}
|
||||
if snapshot == nil {
|
||||
if raw, ok := afterData["card"]; ok {
|
||||
if m, ok := raw.(map[string]any); ok {
|
||||
snapshot = m
|
||||
}
|
||||
}
|
||||
}
|
||||
return assetAuditSvc.WrapOperationContent(stripCardOperationMeta(beforeData), stripCardOperationMeta(afterData), snapshot)
|
||||
}
|
||||
|
||||
func stripCardOperationMeta(data map[string]any) map[string]any {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
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" {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
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,
|
||||
"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,
|
||||
"network_status": card.NetworkStatus,
|
||||
"stop_reason": card.StopReason,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func (s *Service) logIotCardImportAudit(ctx context.Context, p assetAuditSvc.Bui
|
||||
if p.AssetType == "" {
|
||||
p.AssetType = constants.AssetTypeIotCard
|
||||
}
|
||||
p.BeforeData, p.AfterData = assetAuditSvc.WrapOperationContent(p.BeforeData, p.AfterData, nil)
|
||||
s.assetAudit.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user