fix: apply DIP to UserService with local repository interfaces

- Define userRepository, userRoleRepository, roleRepository, passwordHistoryRepository interfaces
- Update UserService struct to use interface types instead of concrete *repository types
- Update NewUserService constructor to accept interfaces
- Add UserCursorResult type (avoid conflict with login_log.go's CursorResult)
- Fix AssignRoles to use type assertion for WithTx (concrete method not in interface)
- Add GetByEmail, UpdateStatus, BatchUpdateStatus, BatchDelete to userRepository interface
- Add GetByID, GetByIDs to roleRepository interface

This enables dependency injection and mocking at the service layer.
This commit is contained in:
2026-04-11 12:50:28 +08:00
parent 8fe4669b97
commit 73b0d5b8c0

View File

@@ -14,27 +14,66 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// Repository interfaces for dependency inversion (DIP) — service layer depends on these abstractions, not concrete types.
type userRepository interface {
GetByID(ctx context.Context, id int64) (*domain.User, error)
GetByUsername(ctx context.Context, username string) (*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
Create(ctx context.Context, user *domain.User) error
Update(ctx context.Context, user *domain.User) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, offset, limit int) ([]*domain.User, int64, error)
ListCursor(ctx context.Context, filter *repository.AdvancedFilter, limit int, cursor *pagination.Cursor) ([]*domain.User, bool, error)
GetByIDs(ctx context.Context, ids []int64) ([]*domain.User, error)
UpdateStatus(ctx context.Context, id int64, status domain.UserStatus) error
BatchUpdateStatus(ctx context.Context, ids []int64, status domain.UserStatus) error
BatchDelete(ctx context.Context, ids []int64) error
DB() *gorm.DB
}
type userRoleRepository interface {
GetByUserID(ctx context.Context, userID int64) ([]*domain.UserRole, error)
DeleteByUserID(ctx context.Context, userID int64) error
DeleteByUserAndRole(ctx context.Context, userID, roleID int64) error
GetByRoleID(ctx context.Context, roleID int64) ([]*domain.UserRole, error)
GetUserIDByRoleID(ctx context.Context, roleID int64) ([]int64, error)
BatchCreate(ctx context.Context, userRoles []*domain.UserRole) error
DB() *gorm.DB
}
type roleRepository interface {
GetByCode(ctx context.Context, code string) (*domain.Role, error)
GetByID(ctx context.Context, id int64) (*domain.Role, error)
GetByIDs(ctx context.Context, ids []int64) ([]*domain.Role, error)
}
type passwordHistoryRepository interface {
GetByUserID(ctx context.Context, userID int64, limit int) ([]*domain.PasswordHistory, error)
Create(ctx context.Context, history *domain.PasswordHistory) error
DeleteOldRecords(ctx context.Context, userID int64, keep int) error
}
// UserService 用户服务 // UserService 用户服务
type UserService struct { type UserService struct {
userRepo *repository.UserRepository userRepo userRepository
userRoleRepo *repository.UserRoleRepository userRoleRepo userRoleRepository
roleRepo *repository.RoleRepository roleRepo roleRepository
passwordHistoryRepo *repository.PasswordHistoryRepository passwordHistoryRepo passwordHistoryRepository
} }
const passwordHistoryLimit = 5 // 保留最近5条密码历史 const passwordHistoryLimit = 5 // 保留最近5条密码历史
// NewUserService 创建用户服务实例 // NewUserService 创建用户服务实例
func NewUserService( func NewUserService(
userRepo *repository.UserRepository, userRepo userRepository,
userRoleRepo *repository.UserRoleRepository, userRoleRepo userRoleRepository,
roleRepo *repository.RoleRepository, roleRepo roleRepository,
passwordHistoryRepo *repository.PasswordHistoryRepository, passwordHistoryRepo passwordHistoryRepository,
) *UserService { ) *UserService {
return &UserService{ return &UserService{
userRepo: userRepo, userRepo: userRepo,
userRoleRepo: userRoleRepo, userRoleRepo: userRoleRepo,
roleRepo: roleRepo, roleRepo: roleRepo,
passwordHistoryRepo: passwordHistoryRepo, passwordHistoryRepo: passwordHistoryRepo,
} }
} }
@@ -146,8 +185,16 @@ type ListCursorRequest struct {
Size int `form:"size"` Size int `form:"size"`
} }
// UserCursorResult wraps cursor-based pagination response for users
type UserCursorResult struct {
Items []*domain.User `json:"items"`
NextCursor string `json:"next_cursor"`
HasMore bool `json:"has_more"`
PageSize int `json:"page_size"`
}
// ListCursor 游标分页获取用户列表(推荐使用) // ListCursor 游标分页获取用户列表(推荐使用)
func (s *UserService) ListCursor(ctx context.Context, req *ListCursorRequest) (*CursorResult, error) { func (s *UserService) ListCursor(ctx context.Context, req *ListCursorRequest) (*UserCursorResult, error) {
size := pagination.ClampPageSize(req.Size) size := pagination.ClampPageSize(req.Size)
cursor, err := pagination.Decode(req.Cursor) cursor, err := pagination.Decode(req.Cursor)
@@ -176,7 +223,7 @@ func (s *UserService) ListCursor(ctx context.Context, req *ListCursorRequest) (*
nextCursor = pagination.BuildNextCursor(last.ID, last.CreatedAt) nextCursor = pagination.BuildNextCursor(last.ID, last.CreatedAt)
} }
return &CursorResult{ return &UserCursorResult{
Items: users, Items: users,
NextCursor: nextCursor, NextCursor: nextCursor,
HasMore: hasMore, HasMore: hasMore,
@@ -268,11 +315,16 @@ func (s *UserService) AssignRoles(ctx context.Context, userID int64, roleIDs []i
} }
// 使用事务包装删旧建新操作,确保原子性 // 使用事务包装删旧建新操作,确保原子性
// Note: WithTx is on concrete type, requires type assertion
txRepo, ok := s.userRoleRepo.(*repository.UserRoleRepository)
if !ok {
return errors.New("userRoleRepo does not support transactions")
}
return s.userRoleRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.userRoleRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.userRoleRepo.WithTx(tx).DeleteByUserID(ctx, userID); err != nil { if err := txRepo.WithTx(tx).DeleteByUserID(ctx, userID); err != nil {
return err return err
} }
return s.userRoleRepo.WithTx(tx).BatchCreate(ctx, userRoles) return txRepo.WithTx(tx).BatchCreate(ctx, userRoles)
}) })
} }