From c2096ff008f0acf2e08eef14556a801e5cfdc4b9 Mon Sep 17 00:00:00 2001 From: long-agent Date: Sat, 11 Apr 2026 10:32:33 +0800 Subject: [PATCH] fix: wrap AssignRoles in transaction and eliminate N+1 queries - AssignRoles: wrap DeleteByUserID + BatchCreate in DB transaction (P1) - GetUserRoles: use GetByIDs batch query instead of per-role GetByID loop (N+1 fix) - ListAdmins: use GetByIDs batch query instead of per-user GetByID loop (N+1 fix) - Add WithTx/DB methods to UserRoleRepository for transaction support - Add GetByIDs to UserRepository (batch user lookup) - Add .gitattributes to normalize line endings to LF (P2) --- .gitattributes | 32 +++++++++++++++++++++++++ internal/repository/user.go | 18 ++++++++++++++ internal/repository/user_role.go | 10 ++++++++ internal/service/user_service.go | 41 +++++++++++++------------------- 4 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..763fdbc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,32 @@ +# Normalize line endings to LF for all text files +* text=auto eol=lf + +# Enforce LF for source files +*.go text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.css text eol=lf +*.scss text eol=lf +*.html text eol=lf +*.htm text eol=lf +*.json text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.md text eol=lf +*.sh text eol=lf +*.ps1 text eol=lf +*.mjs text eol=lf +*.cjs text eol=lf + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.pdf binary +*.zip binary +*.gz binary +*.tar binary diff --git a/internal/repository/user.go b/internal/repository/user.go index 94e70f5..68051ed 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -31,6 +31,11 @@ func NewUserRepository(db *gorm.DB) *UserRepository { return &UserRepository{db: db} } +// DB returns the underlying GORM DB for transaction support +func (r *UserRepository) DB() *gorm.DB { + return r.db +} + // Create 创建用户 func (r *UserRepository) Create(ctx context.Context, user *domain.User) error { return r.db.WithContext(ctx).Create(user).Error @@ -56,6 +61,19 @@ func (r *UserRepository) GetByID(ctx context.Context, id int64) (*domain.User, e return &user, nil } +// GetByIDs 批量获取用户(消除 N+1 查询) +func (r *UserRepository) GetByIDs(ctx context.Context, ids []int64) ([]*domain.User, error) { + if len(ids) == 0 { + return []*domain.User{}, nil + } + var users []*domain.User + err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&users).Error + if err != nil { + return nil, err + } + return users, nil +} + // GetByUsername 根据用户名获取用户 func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*domain.User, error) { var user domain.User diff --git a/internal/repository/user_role.go b/internal/repository/user_role.go index a526cdb..0ba69c4 100644 --- a/internal/repository/user_role.go +++ b/internal/repository/user_role.go @@ -18,6 +18,16 @@ func NewUserRoleRepository(db *gorm.DB) *UserRoleRepository { return &UserRoleRepository{db: db} } +// DB returns the underlying GORM DB for transaction support +func (r *UserRoleRepository) DB() *gorm.DB { + return r.db +} + +// WithTx returns a new repository instance that uses the given transaction +func (r *UserRoleRepository) WithTx(tx *gorm.DB) *UserRoleRepository { + return &UserRoleRepository{db: tx} +} + // Create 创建用户角色关联 func (r *UserRoleRepository) Create(ctx context.Context, userRole *domain.UserRole) error { return r.db.WithContext(ctx).Create(userRole).Error diff --git a/internal/service/user_service.go b/internal/service/user_service.go index d775253..7c8ae8e 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -235,14 +235,10 @@ func (s *UserService) GetUserRoles(ctx context.Context, userID int64) ([]*domain roleIDs[i] = ur.RoleID } - // 批量获取角色详情 - var roles []*domain.Role - for _, roleID := range roleIDs { - role, err := s.roleRepo.GetByID(ctx, roleID) - if err != nil { - continue // 跳过不存在的角色 - } - roles = append(roles, role) + // 批量获取角色详情(消除 N+1 查询) + roles, err := s.roleRepo.GetByIDs(ctx, roleIDs) + if err != nil { + return nil, fmt.Errorf("failed to fetch roles: %w", err) } return roles, nil @@ -255,19 +251,14 @@ func (s *UserService) AssignRoles(ctx context.Context, userID int64, roleIDs []i return err } - // 验证所有角色存在 + // 验证所有角色存在(预先验证,避免在事务内做不必要的查询) for _, roleID := range roleIDs { if _, err := s.roleRepo.GetByID(ctx, roleID); err != nil { return fmt.Errorf("角色 %d 不存在", roleID) } } - // 删除用户现有角色 - if err := s.userRoleRepo.DeleteByUserID(ctx, userID); err != nil { - return err - } - - // 创建新的用户角色关联 + // 构建新的用户角色关联 var userRoles []*domain.UserRole for _, roleID := range roleIDs { userRoles = append(userRoles, &domain.UserRole{ @@ -276,7 +267,13 @@ func (s *UserService) AssignRoles(ctx context.Context, userID int64, roleIDs []i }) } - return s.userRoleRepo.BatchCreate(ctx, userRoles) + // 使用事务包装删旧建新操作,确保原子性 + return s.userRoleRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := s.userRoleRepo.WithTx(tx).DeleteByUserID(ctx, userID); err != nil { + return err + } + return s.userRoleRepo.WithTx(tx).BatchCreate(ctx, userRoles) + }) } // getAdminRoleID looks up the admin role ID by code to avoid hardcoded magic numbers. @@ -304,14 +301,10 @@ func (s *UserService) ListAdmins(ctx context.Context) ([]*domain.User, error) { return []*domain.User{}, nil } - // 获取所有管理员用户 - var admins []*domain.User - for _, adminID := range adminUserIDs { - user, err := s.userRepo.GetByID(ctx, adminID) - if err != nil { - continue // 跳过不存在的用户 - } - admins = append(admins, user) + // 批量获取所有管理员用户(消除 N+1 查询) + admins, err := s.userRepo.GetByIDs(ctx, adminUserIDs) + if err != nil { + return nil, fmt.Errorf("failed to fetch admin users: %w", err) } return admins, nil