package asset_audit import ( "context" stderrors "errors" "strconv" "strings" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" apperrors "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/bytedance/sonic" ) // OperatorInfo 审计操作者信息。 type OperatorInfo struct { ID *uint Type string Name string } // BuildLogParams 构建资产审计日志参数。 type BuildLogParams struct { Operator OperatorInfo AssetType string AssetID uint AssetIdentifier string OperationType string OperationDesc string BeforeData map[string]any AfterData map[string]any ResultStatus string ErrorCode string ErrorMsg string BatchTotal int SuccessCount int FailCount int } // BuildLog 统一构建资产审计日志。 func BuildLog(ctx context.Context, p BuildLogParams) *model.AssetOperationLog { resultStatus := strings.TrimSpace(p.ResultStatus) if resultStatus == "" { resultStatus = constants.AssetAuditResultSuccess } operationDesc := strings.TrimSpace(p.OperationDesc) if operationDesc == "" { operationDesc = p.OperationType } log := &model.AssetOperationLog{ OperatorID: p.Operator.ID, OperatorType: strings.TrimSpace(p.Operator.Type), OperatorName: strings.TrimSpace(p.Operator.Name), AssetType: NormalizeAssetType(p.AssetType), AssetID: p.AssetID, AssetIdentifier: strings.TrimSpace(p.AssetIdentifier), OperationType: strings.TrimSpace(p.OperationType), OperationDesc: operationDesc, BeforeData: sanitizeAndTrimJSONB(p.BeforeData), AfterData: sanitizeAndTrimJSONB(p.AfterData), RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx), RequestPath: middleware.GetRequestPathFromContext(ctx), RequestMethod: middleware.GetRequestMethodFromContext(ctx), ResultStatus: resultStatus, BatchTotal: nonNegative(p.BatchTotal), SuccessCount: nonNegative(p.SuccessCount), FailCount: nonNegative(p.FailCount), } if log.OperatorType == "" { log.OperatorType = constants.AssetAuditOperatorTypeSystem } if log.OperatorName == "" { log.OperatorName = "系统任务" } if code := strings.TrimSpace(p.ErrorCode); code != "" { log.ErrorCode = strPtr(code) } if msg := strings.TrimSpace(p.ErrorMsg); msg != "" { msg = limitString(msg, 1000) log.ErrorMsg = strPtr(msg) } return log } // OperatorFromContext 从 context 解析操作者信息。 func OperatorFromContext(ctx context.Context) OperatorInfo { uid := middleware.GetUserIDFromContext(ctx) ut := middleware.GetUserTypeFromContext(ctx) username := strings.TrimSpace(middleware.GetUsernameFromContext(ctx)) if uid == 0 { return SystemOperator("系统任务") } op := OperatorInfo{ID: uintPtr(uid)} switch ut { case constants.UserTypeSuperAdmin, constants.UserTypePlatform: op.Type = constants.AssetAuditOperatorTypeAdmin op.Name = fallbackOperatorName(username, "平台用户") case constants.UserTypeAgent: op.Type = constants.AssetAuditOperatorTypeAgent op.Name = fallbackOperatorName(username, "代理账号") case constants.UserTypeEnterprise: op.Type = constants.AssetAuditOperatorTypeEnterprise op.Name = fallbackOperatorName(username, "企业账号") case constants.UserTypePersonalCustomer: op.Type = constants.AssetAuditOperatorTypePersonal op.Name = fallbackOperatorName(username, "个人客户") default: op.Type = constants.AssetAuditOperatorTypeAdmin op.Name = fallbackOperatorName(username, "平台用户") } return op } // SystemOperator 构建系统操作者信息。 func SystemOperator(name string) OperatorInfo { n := strings.TrimSpace(name) if n == "" { n = "系统任务" } return OperatorInfo{ ID: nil, Type: constants.AssetAuditOperatorTypeSystem, Name: n, } } // BuildErrorInfo 从统一错误提取审计错误码与错误摘要。 func BuildErrorInfo(err error) (string, string) { if err == nil { return "", "" } var appErr *apperrors.AppError if strings.TrimSpace(err.Error()) == "" { return "", "未知错误" } if stderrors.As(err, &appErr) && appErr != nil { return strconv.Itoa(appErr.Code), appErr.Message } return "", err.Error() } // NormalizeAssetType 统一资产类型值。 func NormalizeAssetType(assetType string) string { switch strings.TrimSpace(assetType) { case "card": return constants.AssetTypeIotCard case "device": return constants.AssetTypeDevice default: return strings.TrimSpace(assetType) } } func sanitizeAndTrimJSONB(data map[string]any) model.JSONB { if len(data) == 0 { return nil } sanitized := sanitizeMap(data) raw, err := sonic.Marshal(sanitized) if err != nil { return model.JSONB{"_marshal_error": "序列化失败"} } if len(raw) <= constants.AssetAuditDataMaxBytes { return model.JSONB(sanitized) } preview := string(raw) if len(preview) > constants.AssetAuditPreviewMaxBytes { preview = preview[:constants.AssetAuditPreviewMaxBytes] } return model.JSONB{ "_truncated": true, "_original_bytes": len(raw), "_preview": preview, "_message": "数据超过大小限制,已裁剪", } } func sanitizeMap(source map[string]any) map[string]any { out := make(map[string]any, len(source)) for k, v := range source { if isSensitiveKey(k) { out[k] = "******" continue } out[k] = sanitizeValue(v) } return out } func sanitizeValue(v any) any { switch vv := v.(type) { case map[string]any: return sanitizeMap(vv) case model.JSONB: return sanitizeMap(map[string]any(vv)) case []map[string]any: arr := make([]any, 0, len(vv)) for _, item := range vv { arr = append(arr, sanitizeMap(item)) } return arr case []any: arr := make([]any, 0, len(vv)) for _, item := range vv { arr = append(arr, sanitizeValue(item)) } return arr case string: return limitString(vv, 2000) default: return v } } func isSensitiveKey(key string) bool { lower := strings.ToLower(strings.TrimSpace(key)) for _, kw := range []string{"password", "passwd", "pwd", "secret", "token", "wifi_password", "wifipwd"} { if strings.Contains(lower, kw) { return true } } return false } func nonNegative(v int) int { if v < 0 { return 0 } return v } func limitString(value string, max int) string { if max <= 0 || len(value) <= max { return value } return value[:max] } func strPtr(v string) *string { if strings.TrimSpace(v) == "" { return nil } return &v } func uintPtr(v uint) *uint { return &v } func fallbackOperatorName(name string, fallback string) string { if strings.TrimSpace(name) != "" { return name } return fallback }