日志记录不全
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s

This commit is contained in:
2026-04-27 15:32:17 +08:00
parent c3ae7fcfbc
commit bb33232b1b
7 changed files with 325 additions and 18 deletions

View File

@@ -57,6 +57,7 @@ func createAdminAuthMiddleware(tokenManager *pkgauth.TokenManager, shopStore pkg
return &pkgmiddleware.UserContextInfo{
UserID: tokenInfo.UserID,
UserType: tokenInfo.UserType,
Username: tokenInfo.Username,
ShopID: tokenInfo.ShopID,
EnterpriseID: tokenInfo.EnterpriseID,
}, nil

View File

@@ -8,8 +8,8 @@ import (
"github.com/break/junhong_cmp_fiber/internal/polling"
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
clientAuthSvc "github.com/break/junhong_cmp_fiber/internal/service/client_auth"
@@ -112,7 +112,7 @@ type services struct {
func initServices(s *stores, deps *Dependencies) *services {
purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package, s.ShopPackageAllocation)
accountAudit := accountAuditSvc.NewService(s.AccountOperationLog)
assetAudit := assetAuditSvc.NewService(s.AssetOperationLog)
assetAudit := assetAuditSvc.NewService(s.AssetOperationLog, deps.DB)
account := accountSvc.New(s.Account, s.Role, s.AccountRole, s.ShopRole, s.Shop, s.Enterprise, accountAudit)
// 创建 IotCard service 并设置回调

View File

@@ -22,7 +22,7 @@ type workerServices struct {
}
func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *queue.WorkerServices {
assetAudit := assetAuditSvc.NewService(stores.AssetOperationLog)
assetAudit := assetAuditSvc.NewService(stores.AssetOperationLog, deps.DB)
commissionStatsService := commission_stats.New(stores.ShopSeriesCommissionStats)
commissionCalculationService := commission_calculation.New(

View File

@@ -16,9 +16,10 @@ type AssetOperationLogItem struct {
ID uint `json:"id" description:"日志ID"`
CreatedAt time.Time `json:"created_at" description:"操作时间"`
OperatorID *uint `json:"operator_id,omitempty" description:"操作人ID系统触发为空"`
OperatorType string `json:"operator_type" description:"操作人类型(system/admin_user/agent_user/enterprise_user/personal_customer)"`
OperatorName string `json:"operator_name" description:"操作人名称快照"`
OperatorID *uint `json:"operator_id,omitempty" description:"操作人ID系统触发为空"`
OperatorType string `json:"operator_type" description:"操作人类型名称 (系统:系统, 平台用户:平台用户, 代理账号:代理账号, 企业账号:企业账号, 个人客户:个人客户)"`
OperatorTypeCode string `json:"operator_type_code" description:"操作人类型编码(system/admin_user/agent_user/enterprise_user/personal_customer)"`
OperatorName string `json:"operator_name" description:"操作人名称(优先返回可读名称)"`
AssetType string `json:"asset_type" description:"资产类型(iot_card/device)"`
AssetID uint `json:"asset_id" description:"资产ID"`

View File

@@ -3,7 +3,6 @@ package asset_audit
import (
"context"
stderrors "errors"
"fmt"
"strconv"
"strings"
@@ -100,6 +99,7 @@ func BuildLog(ctx context.Context, p BuildLogParams) *model.AssetOperationLog {
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("系统任务")
@@ -109,19 +109,19 @@ func OperatorFromContext(ctx context.Context) OperatorInfo {
switch ut {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
op.Type = constants.AssetAuditOperatorTypeAdmin
op.Name = fmt.Sprintf("台用户#%d", uid)
op.Name = fallbackOperatorName(username, "台用户")
case constants.UserTypeAgent:
op.Type = constants.AssetAuditOperatorTypeAgent
op.Name = fmt.Sprintf("代理用户#%d", uid)
op.Name = fallbackOperatorName(username, "代理账号")
case constants.UserTypeEnterprise:
op.Type = constants.AssetAuditOperatorTypeEnterprise
op.Name = fmt.Sprintf("企业用户#%d", uid)
op.Name = fallbackOperatorName(username, "企业账号")
case constants.UserTypePersonalCustomer:
op.Type = constants.AssetAuditOperatorTypePersonal
op.Name = fmt.Sprintf("个人客户#%d", uid)
op.Name = fallbackOperatorName(username, "个人客户")
default:
op.Type = constants.AssetAuditOperatorTypeAdmin
op.Name = fmt.Sprintf("用户#%d", uid)
op.Name = fallbackOperatorName(username, "平台用户")
}
return op
}
@@ -267,3 +267,10 @@ func strPtr(v string) *string {
func uintPtr(v uint) *uint {
return &v
}
func fallbackOperatorName(name string, fallback string) string {
if strings.TrimSpace(name) != "" {
return name
}
return fallback
}

View File

@@ -3,12 +3,15 @@ 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 资产操作日志存储接口。
@@ -33,6 +36,7 @@ type OperationLogger interface {
// Service 资产审计服务。
type Service struct {
store AssetOperationLogStore
db *gorm.DB
}
// ListByAssetParams 按资产查询日志参数。
@@ -46,8 +50,11 @@ type ListByAssetParams struct {
}
// NewService 创建资产审计服务实例。
func NewService(store AssetOperationLogStore) *Service {
return &Service{store: store}
func NewService(store AssetOperationLogStore, db *gorm.DB) *Service {
return &Service{
store: store,
db: db,
}
}
// LogOperation 记录资产操作日志(异步写入,不阻塞主流程)。
@@ -104,9 +111,10 @@ func (s *Service) ListByAsset(ctx context.Context, params ListByAssetParams) (*d
return nil, err
}
operatorViews := s.loadOperatorViews(ctx, logs)
items := make([]*dto.AssetOperationLogItem, 0, len(logs))
for _, item := range logs {
items = append(items, toAssetOperationLogItem(item))
items = append(items, toAssetOperationLogItem(item, operatorViews[item.ID]))
}
return &dto.AssetOperationLogListResponse{
@@ -117,21 +125,24 @@ func (s *Service) ListByAsset(ctx context.Context, params ListByAssetParams) (*d
}, nil
}
func toAssetOperationLogItem(log *model.AssetOperationLog) *dto.AssetOperationLogItem {
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: log.OperatorType,
OperatorName: log.OperatorName,
OperatorType: operatorView.TypeName,
OperatorTypeCode: log.OperatorType,
OperatorName: operatorView.Name,
AssetType: log.AssetType,
AssetID: log.AssetID,
AssetIdentifier: log.AssetIdentifier,
@@ -155,3 +166,269 @@ func toAssetOperationLogItem(log *model.AssetOperationLog) *dto.AssetOperationLo
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
}
}