From bb33232b1bceb9ab76079568d3da975acf98dc3c Mon Sep 17 00:00:00 2001 From: huang Date: Mon, 27 Apr 2026 15:32:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E4=B8=8D?= =?UTF-8?q?=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/bootstrap/middlewares.go | 1 + internal/bootstrap/services.go | 4 +- internal/bootstrap/worker_services.go | 2 +- internal/model/dto/asset_operation_log_dto.go | 7 +- internal/service/asset_audit/builder.go | 19 +- internal/service/asset_audit/service.go | 289 +++++++++++++++++- pkg/middleware/auth.go | 21 ++ 7 files changed, 325 insertions(+), 18 deletions(-) diff --git a/internal/bootstrap/middlewares.go b/internal/bootstrap/middlewares.go index 971d446..ea324d3 100644 --- a/internal/bootstrap/middlewares.go +++ b/internal/bootstrap/middlewares.go @@ -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 diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 3824134..28acb97 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -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 并设置回调 diff --git a/internal/bootstrap/worker_services.go b/internal/bootstrap/worker_services.go index db7276e..aa81fff 100644 --- a/internal/bootstrap/worker_services.go +++ b/internal/bootstrap/worker_services.go @@ -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( diff --git a/internal/model/dto/asset_operation_log_dto.go b/internal/model/dto/asset_operation_log_dto.go index 34ba532..d670997 100644 --- a/internal/model/dto/asset_operation_log_dto.go +++ b/internal/model/dto/asset_operation_log_dto.go @@ -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"` diff --git a/internal/service/asset_audit/builder.go b/internal/service/asset_audit/builder.go index 09a47fe..9081f70 100644 --- a/internal/service/asset_audit/builder.go +++ b/internal/service/asset_audit/builder.go @@ -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 +} diff --git a/internal/service/asset_audit/service.go b/internal/service/asset_audit/service.go index 4e00009..2f81ac3 100644 --- a/internal/service/asset_audit/service.go +++ b/internal/service/asset_audit/service.go @@ -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 + } +} diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 7fa9740..a89d8c6 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -14,6 +14,7 @@ import ( type UserContextInfo struct { UserID uint UserType int + Username string ShopID uint EnterpriseID uint CustomerID uint @@ -23,11 +24,15 @@ type UserContextInfo struct { // SetUserContext 将用户信息设置到 context 中 // 在 Auth 中间件认证成功后调用 func SetUserContext(ctx context.Context, info *UserContextInfo) context.Context { + if info == nil { + return ctx + } ctx = context.WithValue(ctx, constants.ContextKeyUserID, info.UserID) ctx = context.WithValue(ctx, constants.ContextKeyUserType, info.UserType) ctx = context.WithValue(ctx, constants.ContextKeyShopID, info.ShopID) ctx = context.WithValue(ctx, constants.ContextKeyEnterpriseID, info.EnterpriseID) ctx = context.WithValue(ctx, constants.ContextKeyCustomerID, info.CustomerID) + ctx = context.WithValue(ctx, constants.ContextKeyUserInfo, info) // SubordinateShopIDs: nil 表示不限制,空切片表示无权限 if info.SubordinateShopIDs != nil { ctx = context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, info.SubordinateShopIDs) @@ -59,6 +64,18 @@ func GetUserTypeFromContext(ctx context.Context) int { return 0 } +// GetUsernameFromContext 从 context 中提取用户名。 +// 如果未设置,返回空字符串。 +func GetUsernameFromContext(ctx context.Context) string { + if ctx == nil { + return "" + } + if info, ok := ctx.Value(constants.ContextKeyUserInfo).(*UserContextInfo); ok && info != nil { + return info.Username + } + return "" +} + // GetShopIDFromContext 从 context 中提取店铺 ID // 如果未设置,返回 0 func GetShopIDFromContext(ctx context.Context) uint { @@ -157,12 +174,16 @@ func GetRequestMethodFromContext(ctx context.Context) *string { // SetUserToFiberContext 将用户信息设置到 Fiber context 的 Locals 中 // 同时也设置到标准 context 中,便于 GORM 查询使用 func SetUserToFiberContext(c *fiber.Ctx, info *UserContextInfo) { + if info == nil { + return + } // 设置到 Fiber Locals c.Locals(constants.ContextKeyUserID, info.UserID) c.Locals(constants.ContextKeyUserType, info.UserType) c.Locals(constants.ContextKeyShopID, info.ShopID) c.Locals(constants.ContextKeyEnterpriseID, info.EnterpriseID) c.Locals(constants.ContextKeyCustomerID, info.CustomerID) + c.Locals(constants.ContextKeyUserInfo, info) if info.SubordinateShopIDs != nil { c.Locals(constants.ContextKeySubordinateShopIDs, info.SubordinateShopIDs) }