This commit is contained in:
269
internal/service/asset_audit/builder.go
Normal file
269
internal/service/asset_audit/builder.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package asset_audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"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)
|
||||
|
||||
if uid == 0 {
|
||||
return SystemOperator("系统任务")
|
||||
}
|
||||
|
||||
op := OperatorInfo{ID: uintPtr(uid)}
|
||||
switch ut {
|
||||
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
|
||||
op.Type = constants.AssetAuditOperatorTypeAdmin
|
||||
op.Name = fmt.Sprintf("后台用户#%d", uid)
|
||||
case constants.UserTypeAgent:
|
||||
op.Type = constants.AssetAuditOperatorTypeAgent
|
||||
op.Name = fmt.Sprintf("代理用户#%d", uid)
|
||||
case constants.UserTypeEnterprise:
|
||||
op.Type = constants.AssetAuditOperatorTypeEnterprise
|
||||
op.Name = fmt.Sprintf("企业用户#%d", uid)
|
||||
case constants.UserTypePersonalCustomer:
|
||||
op.Type = constants.AssetAuditOperatorTypePersonal
|
||||
op.Name = fmt.Sprintf("个人客户#%d", uid)
|
||||
default:
|
||||
op.Type = constants.AssetAuditOperatorTypeAdmin
|
||||
op.Name = fmt.Sprintf("用户#%d", uid)
|
||||
}
|
||||
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
|
||||
}
|
||||
149
internal/service/asset_audit/service.go
Normal file
149
internal/service/asset_audit/service.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Package asset_audit 提供资产操作审计日志服务。
|
||||
package asset_audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AssetOperationLogStore 资产操作日志存储接口。
|
||||
type AssetOperationLogStore interface {
|
||||
Create(ctx context.Context, log *model.AssetOperationLog) error
|
||||
ListByAssetPaged(
|
||||
ctx context.Context,
|
||||
assetType string,
|
||||
assetID uint,
|
||||
page int,
|
||||
pageSize int,
|
||||
operationType string,
|
||||
resultStatus string,
|
||||
) ([]*model.AssetOperationLog, int64, error)
|
||||
}
|
||||
|
||||
// OperationLogger 资产审计记录接口。
|
||||
type OperationLogger interface {
|
||||
LogOperation(ctx context.Context, log *model.AssetOperationLog)
|
||||
}
|
||||
|
||||
// Service 资产审计服务。
|
||||
type Service struct {
|
||||
store AssetOperationLogStore
|
||||
}
|
||||
|
||||
// ListByAssetParams 按资产查询日志参数。
|
||||
type ListByAssetParams struct {
|
||||
AssetType string
|
||||
AssetID uint
|
||||
Page int
|
||||
PageSize int
|
||||
OperationType string
|
||||
ResultStatus string
|
||||
}
|
||||
|
||||
// NewService 创建资产审计服务实例。
|
||||
func NewService(store AssetOperationLogStore) *Service {
|
||||
return &Service{store: store}
|
||||
}
|
||||
|
||||
// LogOperation 记录资产操作日志(异步写入,不阻塞主流程)。
|
||||
func (s *Service) LogOperation(ctx context.Context, log *model.AssetOperationLog) {
|
||||
if s == nil || s.store == nil || log == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := s.store.Create(context.Background(), log); err != nil {
|
||||
logger.GetAppLogger().Error("写入资产操作日志失败",
|
||||
zap.String("asset_type", log.AssetType),
|
||||
zap.Uint("asset_id", log.AssetID),
|
||||
zap.String("operation_type", log.OperationType),
|
||||
zap.String("result_status", log.ResultStatus),
|
||||
zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ListByAsset 按资产分页查询操作日志。
|
||||
func (s *Service) ListByAsset(ctx context.Context, params ListByAssetParams) (*dto.AssetOperationLogListResponse, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return &dto.AssetOperationLogListResponse{
|
||||
Total: 0,
|
||||
Page: 1,
|
||||
PageSize: constants.DefaultPageSize,
|
||||
Items: []*dto.AssetOperationLogItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
page := params.Page
|
||||
pageSize := params.PageSize
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = constants.DefaultPageSize
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
logs, total, err := s.store.ListByAssetPaged(
|
||||
ctx,
|
||||
NormalizeAssetType(params.AssetType),
|
||||
params.AssetID,
|
||||
page,
|
||||
pageSize,
|
||||
params.OperationType,
|
||||
params.ResultStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]*dto.AssetOperationLogItem, 0, len(logs))
|
||||
for _, item := range logs {
|
||||
items = append(items, toAssetOperationLogItem(item))
|
||||
}
|
||||
|
||||
return &dto.AssetOperationLogListResponse{
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toAssetOperationLogItem(log *model.AssetOperationLog) *dto.AssetOperationLogItem {
|
||||
if log == nil {
|
||||
return nil
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user