// Package service 提供 auth 模块的核心业务逻辑 package service import ( "context" "errors" "github.com/echochat/backend/app/auth/dao" "github.com/echochat/backend/app/auth/model" "github.com/echochat/backend/app/constants" "github.com/echochat/backend/app/dto" "github.com/echochat/backend/config" "github.com/echochat/backend/pkg/logs" "github.com/echochat/backend/pkg/utils" "go.uber.org/zap" "gorm.io/gorm" ) var ( ErrUserAlreadyExists = errors.New("用户名或邮箱已被注册") ErrUserNotFound = errors.New("用户不存在") ErrPasswordWrong = errors.New("密码错误") ErrUserDisabled = errors.New("账号已被禁用") ErrUserDeleted = errors.New("账号已注销") ErrNotAdmin = errors.New("该账号无管理员权限") ErrRefreshTokenType = errors.New("无效的 Refresh Token 类型") ) // AuthService 认证服务,处理注册、登录、Token 管理、个人信息等业务逻辑 type AuthService struct { userDAO *dao.UserDAO roleDAO *dao.RoleDAO jwtCfg *config.JWTConfig tokenStore *TokenStore } // NewAuthService 创建认证服务实例 func NewAuthService(userDAO *dao.UserDAO, roleDAO *dao.RoleDAO, jwtCfg *config.JWTConfig, tokenStore *TokenStore) *AuthService { return &AuthService{ userDAO: userDAO, roleDAO: roleDAO, jwtCfg: jwtCfg, tokenStore: tokenStore, } } // Register 用户注册(前台用户端专用,clientType 固定为 frontend) // 流程:检查用户名/邮箱是否重复 → 加密密码 → 创建用户 → 分配默认角色 → 生成 Token func (s *AuthService) Register(ctx context.Context, req *dto.RegisterRequest) (*dto.LoginResponse, error) { funcName := "service.auth_service.Register" logs.Info(ctx, funcName, "开始处理注册", zap.String("username", req.Username), zap.String("email", logs.MaskEmail(req.Email)), ) var err error defer func() { if err != nil { logs.Error(ctx, funcName, "注册处理失败", zap.String("username", req.Username), zap.Error(err), ) } }() // 检查用户名是否已存在 existing, _ := s.userDAO.FindByUsername(ctx, req.Username) if existing != nil { err = ErrUserAlreadyExists return nil, err } // 检查邮箱是否已存在 existing, _ = s.userDAO.FindByEmail(ctx, req.Email) if existing != nil { err = ErrUserAlreadyExists return nil, err } // 加密密码 hashedPassword, hashErr := utils.HashPassword(req.Password) if hashErr != nil { err = hashErr return nil, err } // 创建用户 nickname := req.Nickname if nickname == "" { nickname = req.Username } user := &model.User{ Username: req.Username, Email: req.Email, PasswordHash: hashedPassword, Nickname: nickname, Status: constants.UserStatusActive, } if err = s.userDAO.Create(ctx, user); err != nil { return nil, err } // 分配默认角色(普通用户) defaultRole, roleErr := s.roleDAO.FindByCode(ctx, constants.RoleUser) if roleErr != nil { logs.Warn(ctx, funcName, "查找默认角色失败,跳过角色分配", zap.Error(roleErr)) } else { if assignErr := s.roleDAO.AssignRole(ctx, user.ID, defaultRole.ID); assignErr != nil { logs.Warn(ctx, funcName, "分配默认角色失败", zap.Error(assignErr)) } } // 获取角色列表并生成 Token(注册只有前台,clientType 为 frontend) roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID) resp, err := s.buildLoginResponse(ctx, user, roles, constants.ClientTypeFrontend) if err != nil { return nil, err } logs.Info(ctx, funcName, "注册成功", zap.Int64("user_id", user.ID)) return resp, nil } // Login 用户登录 // 流程:查找用户 → 校验密码 → 检查账号状态 → 更新登录信息 → 生成 Token // clientType 区分前台(frontend)和管理端(admin),Token 在 Redis 中隔离存储 func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, clientIP, clientType string) (*dto.LoginResponse, error) { funcName := "service.auth_service.Login" logs.Info(ctx, funcName, "开始处理登录", zap.String("account", req.Account), ) var err error defer func() { if err != nil { logs.Error(ctx, funcName, "登录处理失败", zap.String("account", req.Account), zap.Error(err), ) } }() // 按用户名或邮箱查找用户(用户不存在时统一返回密码错误,防止枚举攻击) user, findErr := s.userDAO.FindByAccount(ctx, req.Account) if findErr != nil { if errors.Is(findErr, gorm.ErrRecordNotFound) { err = ErrPasswordWrong } else { err = findErr } return nil, err } // 校验密码 if !utils.CheckPassword(req.Password, user.PasswordHash) { err = ErrPasswordWrong return nil, err } // 检查账号状态 if err = s.checkUserStatus(user.Status); err != nil { return nil, err } // 更新最后登录信息 _ = s.userDAO.UpdateLastLogin(ctx, user.ID, clientIP) // 获取角色列表并生成 Token(同时存入 Redis,按 clientType 隔离) roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID) resp, err := s.buildLoginResponse(ctx, user, roles, clientType) if err != nil { return nil, err } logs.Info(ctx, funcName, "登录成功", zap.Int64("user_id", user.ID), zap.String("username", user.Username), zap.String("client_type", clientType), ) return resp, nil } func (s *AuthService) BuildLoginResponseForUser(ctx context.Context, user *model.User, clientIP, clientType string) (*dto.LoginResponse, error) { if user == nil { return nil, ErrUserNotFound } if err := s.checkUserStatus(user.Status); err != nil { return nil, err } _ = s.userDAO.UpdateLastLogin(ctx, user.ID, clientIP) roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID) return s.buildLoginResponse(ctx, user, roles, clientType) } // AdminLogin 管理后台登录 // 与普通登录相同,但额外检查用户是否拥有 admin 或 super_admin 角色 // clientType 固定为 constants.ClientTypeAdmin func (s *AuthService) AdminLogin(ctx context.Context, req *dto.LoginRequest, clientIP string) (*dto.LoginResponse, error) { funcName := "service.auth_service.AdminLogin" logs.Info(ctx, funcName, "开始处理管理员登录", zap.String("account", req.Account), ) resp, err := s.Login(ctx, req, clientIP, constants.ClientTypeAdmin) if err != nil { return nil, err } // 检查是否拥有管理员角色 hasAdmin := false for _, role := range resp.User.Roles { if role == constants.RoleAdmin || role == constants.RoleSuperAdmin { hasAdmin = true break } } if !hasAdmin { logs.Warn(ctx, funcName, "非管理员尝试后台登录", zap.String("account", req.Account), ) return nil, ErrNotAdmin } logs.Info(ctx, funcName, "管理员登录成功", zap.String("account", req.Account), ) return resp, nil } // RefreshToken 刷新 Access Token // 验证 Refresh Token 有效性后,重新生成一对新的 Token func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*dto.LoginResponse, error) { funcName := "service.auth_service.RefreshToken" // 解析 Refresh Token claims, err := utils.ParseToken(s.jwtCfg, refreshToken) if err != nil { logs.Warn(ctx, funcName, "Refresh Token 解析失败", zap.Error(err)) return nil, err } // 验证 Token 类型 if claims.Subject != "refresh" { return nil, ErrRefreshTokenType } // 从 claims 中获取 clientType(前台 / 管理端) clientType := claims.ClientType if clientType == "" { clientType = constants.ClientTypeFrontend } // 验证 Refresh Token 是否与 Redis 中存储的一致 if !s.tokenStore.ValidateRefreshToken(ctx, claims.UserID, clientType, refreshToken) { logs.Warn(ctx, funcName, "Refresh Token 已失效(不在 Redis 中)", zap.Int64("user_id", claims.UserID), zap.String("client_type", clientType), ) return nil, ErrRefreshTokenType } // 查找用户(确保用户仍然有效) user, err := s.userDAO.FindByID(ctx, claims.UserID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, err } if err = s.checkUserStatus(user.Status); err != nil { return nil, err } // 获取角色并生成新 Token(同时存入 Redis 覆盖旧 Token,保持 clientType 不变) roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID) resp, err := s.buildLoginResponse(ctx, user, roles, clientType) if err != nil { return nil, err } logs.Info(ctx, funcName, "Token 刷新成功", zap.Int64("user_id", user.ID), zap.String("client_type", clientType), ) return resp, nil } // Logout 用户登出 // 从 Redis 中删除指定 clientType 下该用户的 Token(前台登出不影响管理端) func (s *AuthService) Logout(ctx context.Context, userID int64, clientType string) error { funcName := "service.auth_service.Logout" logs.Info(ctx, funcName, "用户登出", zap.Int64("user_id", userID), zap.String("client_type", clientType), ) return s.tokenStore.RemoveTokens(ctx, userID, clientType) } // ValidateAccessToken 校验 Access Token 是否在 Redis 中有效 // 供 JWT 中间件 / WS 握手调用,实现有状态 JWT 验证,按 clientType 隔离校验; // username 用于识别云律临时会议账号并放宽「单点登录挤号」校验 func (s *AuthService) ValidateAccessToken(ctx context.Context, userID int64, clientType, token, username string) bool { return s.tokenStore.ValidateAccessToken(ctx, userID, clientType, token, username) } // GetProfile 获取用户个人信息 func (s *AuthService) GetProfile(ctx context.Context, userID int64) (*dto.UserInfo, error) { funcName := "service.auth_service.GetProfile" logs.Debug(ctx, funcName, "获取用户信息", zap.Int64("user_id", userID)) user, err := s.userDAO.FindByID(ctx, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, err } roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID) return s.buildUserInfo(user, roles), nil } // UpdateProfile 更新用户个人资料 func (s *AuthService) UpdateProfile(ctx context.Context, userID int64, req *dto.UpdateProfileRequest) (*dto.UserInfo, error) { funcName := "service.auth_service.UpdateProfile" logs.Info(ctx, funcName, "更新用户资料", zap.Int64("user_id", userID)) user, err := s.userDAO.FindByID(ctx, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUserNotFound } return nil, err } if req.Nickname != "" { user.Nickname = req.Nickname } if req.Avatar != "" { user.Avatar = req.Avatar } if req.Gender != nil { user.Gender = *req.Gender } if req.Phone != "" { user.Phone = &req.Phone } if err = s.userDAO.Update(ctx, user); err != nil { return nil, err } roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID) logs.Info(ctx, funcName, "用户资料更新成功", zap.Int64("user_id", userID)) return s.buildUserInfo(user, roles), nil } // ChangePassword 修改密码 func (s *AuthService) ChangePassword(ctx context.Context, userID int64, req *dto.ChangePasswordRequest) error { funcName := "service.auth_service.ChangePassword" logs.Info(ctx, funcName, "修改密码", zap.Int64("user_id", userID)) user, err := s.userDAO.FindByID(ctx, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrUserNotFound } return err } // 校验旧密码 if !utils.CheckPassword(req.OldPassword, user.PasswordHash) { return ErrPasswordWrong } // 加密新密码 hashedPassword, err := utils.HashPassword(req.NewPassword) if err != nil { return err } user.PasswordHash = hashedPassword if err = s.userDAO.Update(ctx, user); err != nil { return err } logs.Info(ctx, funcName, "密码修改成功", zap.Int64("user_id", userID)) return nil } // checkUserStatus 检查用户账号状态 func (s *AuthService) checkUserStatus(status int) error { switch status { case constants.UserStatusDisabled: return ErrUserDisabled case constants.UserStatusDeleted: return ErrUserDeleted default: return nil } } // buildLoginResponse 构建登录响应(生成 Token + 存入 Redis + 用户信息) // clientType 决定 Token 存入 Redis 的 key 前缀和 JWT Claims 中的 client_type 字段 func (s *AuthService) buildLoginResponse(ctx context.Context, user *model.User, roles []string, clientType string) (*dto.LoginResponse, error) { if roles == nil { roles = []string{} } token, err := utils.GenerateToken(s.jwtCfg, user.ID, user.Username, roles, clientType) if err != nil { return nil, err } refreshToken, err := utils.GenerateRefreshToken(s.jwtCfg, user.ID, clientType) if err != nil { return nil, err } // 将 Token 存入 Redis(按 clientType 隔离,前台和管理端互不影响) if err = s.tokenStore.SaveTokens(ctx, user.ID, clientType, token, refreshToken); err != nil { return nil, err } return &dto.LoginResponse{ Token: token, RefreshToken: refreshToken, ExpiresIn: int64(s.jwtCfg.AccessExpireMin) * 60, User: *s.buildUserInfo(user, roles), }, nil } // buildUserInfo 从 model.User 构建 dto.UserInfo func (s *AuthService) buildUserInfo(user *model.User, roles []string) *dto.UserInfo { if roles == nil { roles = []string{} } info := &dto.UserInfo{ ID: user.ID, Username: user.Username, Email: user.Email, Nickname: user.Nickname, Avatar: user.Avatar, Gender: user.Gender, Roles: roles, CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), } if user.Phone != nil { info.Phone = *user.Phone } return info }