Files
junhong_cmp_fiber/internal/service/asset_audit/builder.go
huang bb33232b1b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
日志记录不全
2026-04-27 15:32:17 +08:00

277 lines
6.7 KiB
Go

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
}