Files
user-system/internal/service/user_service.go
long-agent a85d822419 fix: 统一API响应格式并修复前端测试
- 所有Handler方法使用标准{code:0,message:"success",data:...}响应格式
- 修复Cursor分页响应包装(GetAllDevices,GetLoginLogs,ListUsers等)
- 修复AuthHandler和SMSHandler认证方法响应格式
- 修复operation_log.go admin用户operation_type前缀问题
- 修复DashboardPage嵌套stats结构
- 修复LoginLogsPage reset功能stale closure问题
- 修复UsersPage批量操作API调用
- 修复多个前端测试(mock格式、按钮选择、断言逻辑)
- 添加OAuth测试域名白名单
- 新增代码审查流程文档
2026-04-08 20:06:54 +08:00

214 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/pagination"
"github.com/user-management-system/internal/repository"
)
// UserService 用户服务
type UserService struct {
userRepo *repository.UserRepository
userRoleRepo *repository.UserRoleRepository
roleRepo *repository.RoleRepository
passwordHistoryRepo *repository.PasswordHistoryRepository
}
const passwordHistoryLimit = 5 // 保留最近5条密码历史
// NewUserService 创建用户服务实例
func NewUserService(
userRepo *repository.UserRepository,
userRoleRepo *repository.UserRoleRepository,
roleRepo *repository.RoleRepository,
passwordHistoryRepo *repository.PasswordHistoryRepository,
) *UserService {
return &UserService{
userRepo: userRepo,
userRoleRepo: userRoleRepo,
roleRepo: roleRepo,
passwordHistoryRepo: passwordHistoryRepo,
}
}
// ChangePassword 修改用户密码(含历史记录检查)
func (s *UserService) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error {
if s.userRepo == nil {
return errors.New("user repository is not configured")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return errors.New("用户不存在")
}
// 验证旧密码
if strings.TrimSpace(oldPassword) == "" {
return errors.New("请输入当前密码")
}
if !auth.VerifyPassword(user.Password, oldPassword) {
return errors.New("当前密码不正确")
}
// 检查新密码强度
if strings.TrimSpace(newPassword) == "" {
return errors.New("新密码不能为空")
}
if err := validatePasswordStrength(newPassword, 8, false); err != nil {
return err
}
// 检查密码历史
if s.passwordHistoryRepo != nil {
histories, err := s.passwordHistoryRepo.GetByUserID(ctx, userID, passwordHistoryLimit)
if err == nil && len(histories) > 0 {
for _, h := range histories {
if auth.VerifyPassword(h.PasswordHash, newPassword) {
return errors.New("新密码不能与最近5次密码相同")
}
}
}
// 保存新密码到历史记录
newHashedPassword, hashErr := auth.HashPassword(newPassword)
if hashErr != nil {
return errors.New("密码哈希失败")
}
go func() {
// 使用带超时的独立 context不能使用请求 ctx该 goroutine 在请求完成后仍可能运行)
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = s.passwordHistoryRepo.Create(bgCtx, &domain.PasswordHistory{
UserID: userID,
PasswordHash: newHashedPassword,
})
_ = s.passwordHistoryRepo.DeleteOldRecords(bgCtx, userID, passwordHistoryLimit)
}()
}
// 更新密码
newHashedPassword, err := auth.HashPassword(newPassword)
if err != nil {
return errors.New("密码哈希失败")
}
user.Password = newHashedPassword
return s.userRepo.Update(ctx, user)
}
// GetByID 根据ID获取用户
func (s *UserService) GetByID(ctx context.Context, id int64) (*domain.User, error) {
return s.userRepo.GetByID(ctx, id)
}
// GetByEmail 根据邮箱获取用户
func (s *UserService) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
return s.userRepo.GetByEmail(ctx, email)
}
// Create 创建用户
func (s *UserService) Create(ctx context.Context, user *domain.User) error {
return s.userRepo.Create(ctx, user)
}
// Update 更新用户
func (s *UserService) Update(ctx context.Context, user *domain.User) error {
return s.userRepo.Update(ctx, user)
}
// Delete 删除用户
func (s *UserService) Delete(ctx context.Context, id int64) error {
return s.userRepo.Delete(ctx, id)
}
// List 获取用户列表
func (s *UserService) List(ctx context.Context, offset, limit int) ([]*domain.User, int64, error) {
return s.userRepo.List(ctx, offset, limit)
}
// ListCursorRequest 用户游标分页请求
type ListCursorRequest struct {
Keyword string `form:"keyword"`
Status int `form:"status"` // -1=全部
RoleIDs []int64
CreatedFrom *time.Time
CreatedTo *time.Time
SortBy string // created_at, last_login_time, username
SortOrder string // asc, desc
Cursor string `form:"cursor"`
Size int `form:"size"`
}
// ListCursor 游标分页获取用户列表(推荐使用)
func (s *UserService) ListCursor(ctx context.Context, req *ListCursorRequest) (*CursorResult, error) {
size := pagination.ClampPageSize(req.Size)
cursor, err := pagination.Decode(req.Cursor)
if err != nil {
return nil, fmt.Errorf("invalid cursor: %w", err)
}
filter := &repository.AdvancedFilter{
Keyword: req.Keyword,
Status: req.Status,
RoleIDs: req.RoleIDs,
CreatedFrom: req.CreatedFrom,
CreatedTo: req.CreatedTo,
SortBy: req.SortBy,
SortOrder: req.SortOrder,
}
users, hasMore, err := s.userRepo.ListCursor(ctx, filter, size, cursor)
if err != nil {
return nil, err
}
nextCursor := ""
if len(users) > 0 {
last := users[len(users)-1]
nextCursor = pagination.BuildNextCursor(last.ID, last.CreatedAt)
}
return &CursorResult{
Items: users,
NextCursor: nextCursor,
HasMore: hasMore,
PageSize: size,
}, nil
}
// UpdateStatus 更新用户状态
func (s *UserService) UpdateStatus(ctx context.Context, id int64, status domain.UserStatus) error {
return s.userRepo.UpdateStatus(ctx, id, status)
}
// BatchUpdateStatusRequest 批量更新状态请求
type BatchUpdateStatusRequest struct {
IDs []int64 `json:"ids" binding:"required,min=1"`
Status domain.UserStatus `json:"status" binding:"required"`
}
// BatchDeleteRequest 批量删除请求
type BatchDeleteRequest struct {
IDs []int64 `json:"ids" binding:"required,min=1"`
}
// BatchUpdateStatus 批量更新用户状态
func (s *UserService) BatchUpdateStatus(ctx context.Context, req *BatchUpdateStatusRequest) (int64, error) {
err := s.userRepo.BatchUpdateStatus(ctx, req.IDs, req.Status)
return int64(len(req.IDs)), err
}
// BatchDelete 批量删除用户
func (s *UserService) BatchDelete(ctx context.Context, req *BatchDeleteRequest) (int64, error) {
err := s.userRepo.BatchDelete(ctx, req.IDs)
return int64(len(req.IDs)), err
}