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

435 lines
12 KiB
Go

// Package asset_audit 提供资产操作审计日志服务。
package asset_audit
import (
"context"
"fmt"
"strings"
"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"
"gorm.io/gorm"
)
// 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
db *gorm.DB
}
// ListByAssetParams 按资产查询日志参数。
type ListByAssetParams struct {
AssetType string
AssetID uint
Page int
PageSize int
OperationType string
ResultStatus string
}
// NewService 创建资产审计服务实例。
func NewService(store AssetOperationLogStore, db *gorm.DB) *Service {
return &Service{
store: store,
db: db,
}
}
// 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
}
operatorViews := s.loadOperatorViews(ctx, logs)
items := make([]*dto.AssetOperationLogItem, 0, len(logs))
for _, item := range logs {
items = append(items, toAssetOperationLogItem(item, operatorViews[item.ID]))
}
return &dto.AssetOperationLogListResponse{
Total: total,
Page: page,
PageSize: pageSize,
Items: items,
}, nil
}
func toAssetOperationLogItem(log *model.AssetOperationLog, operatorView operatorView) *dto.AssetOperationLogItem {
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: operatorView.TypeName,
OperatorTypeCode: log.OperatorType,
OperatorName: operatorView.Name,
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,
}
}
type operatorView struct {
TypeName string
Name string
}
func (s *Service) loadOperatorViews(ctx context.Context, logs []*model.AssetOperationLog) map[uint]operatorView {
result := make(map[uint]operatorView, len(logs))
if len(logs) == 0 {
return result
}
accountIDs := make([]uint, 0)
customerIDs := make([]uint, 0)
accountIDSet := make(map[uint]struct{})
customerIDSet := make(map[uint]struct{})
for _, log := range logs {
if log == nil {
continue
}
typeName := operatorTypeDisplayName(log.OperatorType)
result[log.ID] = operatorView{
TypeName: typeName,
Name: fallbackResolvedOperatorName(log.OperatorName, typeName, log.OperatorID),
}
if log.OperatorID == nil {
continue
}
switch log.OperatorType {
case constants.AssetAuditOperatorTypeAdmin, constants.AssetAuditOperatorTypeAgent, constants.AssetAuditOperatorTypeEnterprise:
if _, exists := accountIDSet[*log.OperatorID]; !exists {
accountIDSet[*log.OperatorID] = struct{}{}
accountIDs = append(accountIDs, *log.OperatorID)
}
case constants.AssetAuditOperatorTypePersonal:
if _, exists := customerIDSet[*log.OperatorID]; !exists {
customerIDSet[*log.OperatorID] = struct{}{}
customerIDs = append(customerIDs, *log.OperatorID)
}
}
}
if s.db == nil {
return result
}
accountMap, shopMap, enterpriseMap := s.loadAccountRelatedMaps(ctx, accountIDs)
customerMap := s.loadPersonalCustomerMap(ctx, customerIDs)
for _, log := range logs {
if log == nil {
continue
}
view := result[log.ID]
if log.OperatorID == nil {
result[log.ID] = finalizeOperatorView(view, log.OperatorName, log.OperatorType, log.OperatorID)
continue
}
switch log.OperatorType {
case constants.AssetAuditOperatorTypeAdmin, constants.AssetAuditOperatorTypeAgent, constants.AssetAuditOperatorTypeEnterprise:
if account, ok := accountMap[*log.OperatorID]; ok {
view.Name = buildAccountOperatorName(account, shopMap, enterpriseMap)
}
case constants.AssetAuditOperatorTypePersonal:
if customer, ok := customerMap[*log.OperatorID]; ok {
if nickname := strings.TrimSpace(customer.Nickname); nickname != "" {
view.Name = nickname
}
}
}
result[log.ID] = finalizeOperatorView(view, log.OperatorName, log.OperatorType, log.OperatorID)
}
return result
}
func (s *Service) loadAccountRelatedMaps(ctx context.Context, accountIDs []uint) (map[uint]*model.Account, map[uint]string, map[uint]string) {
accountMap := make(map[uint]*model.Account)
shopMap := make(map[uint]string)
enterpriseMap := make(map[uint]string)
if len(accountIDs) == 0 || s.db == nil {
return accountMap, shopMap, enterpriseMap
}
var accounts []*model.Account
if err := s.db.WithContext(ctx).Unscoped().Where("id IN ?", accountIDs).Find(&accounts).Error; err != nil {
return accountMap, shopMap, enterpriseMap
}
shopIDs := make([]uint, 0)
shopIDSet := make(map[uint]struct{})
enterpriseIDs := make([]uint, 0)
enterpriseIDSet := make(map[uint]struct{})
for _, account := range accounts {
if account == nil {
continue
}
accountMap[account.ID] = account
if account.ShopID != nil {
if _, exists := shopIDSet[*account.ShopID]; !exists {
shopIDSet[*account.ShopID] = struct{}{}
shopIDs = append(shopIDs, *account.ShopID)
}
}
if account.EnterpriseID != nil {
if _, exists := enterpriseIDSet[*account.EnterpriseID]; !exists {
enterpriseIDSet[*account.EnterpriseID] = struct{}{}
enterpriseIDs = append(enterpriseIDs, *account.EnterpriseID)
}
}
}
if len(shopIDs) > 0 {
var shops []*model.Shop
if err := s.db.WithContext(ctx).Unscoped().Where("id IN ?", shopIDs).Find(&shops).Error; err == nil {
for _, shop := range shops {
if shop == nil {
continue
}
shopMap[shop.ID] = strings.TrimSpace(shop.ShopName)
}
}
}
if len(enterpriseIDs) > 0 {
var enterprises []*model.Enterprise
if err := s.db.WithContext(ctx).Unscoped().Where("id IN ?", enterpriseIDs).Find(&enterprises).Error; err == nil {
for _, enterprise := range enterprises {
if enterprise == nil {
continue
}
enterpriseMap[enterprise.ID] = strings.TrimSpace(enterprise.EnterpriseName)
}
}
}
return accountMap, shopMap, enterpriseMap
}
func (s *Service) loadPersonalCustomerMap(ctx context.Context, customerIDs []uint) map[uint]*model.PersonalCustomer {
result := make(map[uint]*model.PersonalCustomer)
if len(customerIDs) == 0 || s.db == nil {
return result
}
var customers []*model.PersonalCustomer
if err := s.db.WithContext(ctx).Unscoped().Where("id IN ?", customerIDs).Find(&customers).Error; err != nil {
return result
}
for _, customer := range customers {
if customer == nil {
continue
}
result[customer.ID] = customer
}
return result
}
func finalizeOperatorView(view operatorView, storedName string, typeCode string, operatorID *uint) operatorView {
if strings.TrimSpace(view.TypeName) == "" {
view.TypeName = operatorTypeDisplayName(typeCode)
}
view.Name = resolveOperatorName(view.Name, storedName, view.TypeName, operatorID)
return view
}
func operatorTypeDisplayName(typeCode string) string {
switch strings.TrimSpace(typeCode) {
case constants.AssetAuditOperatorTypeSystem:
return "系统"
case constants.AssetAuditOperatorTypeAdmin:
return "平台用户"
case constants.AssetAuditOperatorTypeAgent:
return "代理账号"
case constants.AssetAuditOperatorTypeEnterprise:
return "企业账号"
case constants.AssetAuditOperatorTypePersonal:
return "个人客户"
default:
if strings.TrimSpace(typeCode) == "" {
return "未知"
}
return strings.TrimSpace(typeCode)
}
}
func fallbackResolvedOperatorName(storedName string, typeName string, operatorID *uint) string {
storedName = strings.TrimSpace(storedName)
if storedName == "" || isPlaceholderOperatorName(storedName) {
return typeNameWithOptionalID(typeName, operatorID)
}
return storedName
}
func resolveOperatorName(resolvedName string, storedName string, typeName string, operatorID *uint) string {
if strings.TrimSpace(resolvedName) != "" && !isPlaceholderOperatorName(resolvedName) {
return strings.TrimSpace(resolvedName)
}
return fallbackResolvedOperatorName(storedName, typeName, operatorID)
}
func isPlaceholderOperatorName(name string) bool {
name = strings.TrimSpace(name)
if name == "" {
return true
}
for _, prefix := range []string{"后台用户#", "代理用户#", "企业用户#", "个人客户#", "用户#"} {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
func typeNameWithOptionalID(typeName string, operatorID *uint) string {
if operatorID == nil || *operatorID == 0 {
return typeName
}
return fmt.Sprintf("%s(ID:%d)", typeName, *operatorID)
}
func buildAccountOperatorName(account *model.Account, shopMap map[uint]string, enterpriseMap map[uint]string) string {
if account == nil {
return ""
}
username := strings.TrimSpace(account.Username)
switch account.UserType {
case constants.UserTypeAgent:
return composeReadableName(lookupNameByPtr(account.ShopID, shopMap), username)
case constants.UserTypeEnterprise:
return composeReadableName(lookupNameByPtr(account.EnterpriseID, enterpriseMap), username)
default:
return username
}
}
func lookupNameByPtr(id *uint, nameMap map[uint]string) string {
if id == nil {
return ""
}
return strings.TrimSpace(nameMap[*id])
}
func composeReadableName(primary string, secondary string) string {
primary = strings.TrimSpace(primary)
secondary = strings.TrimSpace(secondary)
switch {
case primary != "" && secondary != "" && primary != secondary:
return fmt.Sprintf("%s(%s)", primary, secondary)
case primary != "":
return primary
default:
return secondary
}
}