All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m20s
277 lines
6.7 KiB
Go
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.Error()
|
|
}
|
|
|
|
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
|
|
}
|