This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 并设置回调
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user