package service import ( "errors" "time" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "laic-backend/cache" "laic-backend/common" "laic-backend/logger" "laic-backend/model" "laic-backend/token" "laic-backend/tool" "laic-backend/vo" ) type UserService struct{} var DefaultUserService = &UserService{} // Register 注册用户(默认角色 user),成功后直接返回登录态 func (s *UserService) Register(name, phone, email, password string) (*vo.LoginResp, *common.BusiError) { // 手机号唯一性预检(兼顾并发,唯一键兜底) var count int64 if err := common.DB.Model(&model.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil { return nil, common.ErrInternal } if count > 0 { return nil, common.ErrUserPhoneExists } id, err := tool.NextID() if err != nil { return nil, common.ErrInternal } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, common.ErrInternal } user := &model.User{ ID: id, Name: name, Phone: phone, Email: email, Password: string(hash), Role: common.RoleUser, Status: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := common.DB.Create(user).Error; err != nil { if code, _ := common.ParseError(err); code == 1062 { return nil, common.ErrUserPhoneExists } logger.ERROR("注册用户失败", err) return nil, common.ErrInternal } _ = common.SetUserRole(id, common.RoleUser) return s.buildLoginResp(user) } // Login 手机号 + 密码登录 func (s *UserService) Login(phone, password string) (*vo.LoginResp, *common.BusiError) { var user model.User if err := common.DB.Where("phone = ? AND status = 1", phone).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrPasswordError // 统一返回"用户名或密码错误",避免暴露账号是否存在 } logger.ERROR("登录查询用户失败", err) return nil, common.ErrInternal } if !checkPassword(password, user.Password) { return nil, common.ErrPasswordError } // 更新最后登录时间(失败不阻断登录) common.DB.Model(&user).Update("last_login", time.Now()) return s.buildLoginResp(&user) } // Refresh 用 refresh token 换取新的 access + refresh token(刷新令牌轮换) func (s *UserService) Refresh(refreshToken string) (*vo.LoginResp, *common.BusiError) { userID, err := token.ParseRefreshToken(refreshToken) if err != nil { return nil, common.ErrTokenInvalid } // 校验 refresh token 是否仍存证(登出会删除) refreshKey := cache.RefreshTokenKeyOf(userID, token.GenerateShortID(refreshToken)) stored, err := common.GetString(refreshKey) if err != nil || stored != refreshToken { return nil, common.ErrTokenInvalid } var user model.User if err := common.DB.Where("id = ? AND status = 1", userID).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrUserNotFound } return nil, common.ErrInternal } // 轮换:删除旧 refresh 存证 _ = common.Delete(refreshKey) return s.buildLoginResp(&user) } // Logout 登出:删除 access 存证(及可选 refresh 存证),使 token 立即失效 func (s *UserService) Logout(userID int64, accessToken, refreshToken string) *common.BusiError { accessKey := cache.UserTokenKeyOf(userID, token.GenerateShortID(accessToken)) _ = common.Delete(accessKey) if refreshToken != "" { refreshKey := cache.RefreshTokenKeyOf(userID, token.GenerateShortID(refreshToken)) _ = common.Delete(refreshKey) } return nil } // SendSmsCode 生成并"发送"短信验证码(暂存 Redis,短信通道未接入前记日志) func (s *UserService) SendSmsCode(phone string) (string, *common.BusiError) { code := tool.RandDigit(6) key := cache.SmsCodeKeyOf(phone) if err := common.SetValueWithExpired(key, code, 5*60); err != nil { logger.ERROR("短信验证码写 Redis 失败", err) return "", common.ErrInternal } // TODO: 接入短信服务商后,此处调用真实发送接口;当前仅在日志输出验证码便于联调 logger.INFO("send sms code to", phone, "code:", code) return code, nil } // ResetPassword 通过短信验证码重置密码 func (s *UserService) ResetPassword(phone, smsCode, newPassword string) *common.BusiError { key := cache.SmsCodeKeyOf(phone) stored, err := common.GetString(key) if err != nil || stored == "" || stored != smsCode { return common.ErrSmsCodeError } var user model.User if err := common.DB.Where("phone = ?", phone).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return common.ErrUserNotFound } return common.ErrInternal } hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { return common.ErrInternal } if err := common.DB.Model(&user).Updates(map[string]any{ "password": string(hash), "updated_at": time.Now(), }).Error; err != nil { return common.ErrInternal } _ = common.Delete(key) return nil } // GetByID 按 ID 查询用户 func (s *UserService) GetByID(id int64) (*model.User, *common.BusiError) { var user model.User if err := common.DB.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrUserNotFound } return nil, common.ErrInternal } return &user, nil } // buildLoginResp 生成 token 并存证 Redis,返回登录响应 func (s *UserService) buildLoginResp(user *model.User) (*vo.LoginResp, *common.BusiError) { roles := []string{user.Role} accessToken, refreshToken, err := token.GenerateToken(user.ID, user.Phone, roles) if err != nil { logger.ERROR("生成 token 失败", err) return nil, common.ErrInternal } accessKey := cache.UserTokenKeyOf(user.ID, token.GenerateShortID(accessToken)) refreshKey := cache.RefreshTokenKeyOf(user.ID, token.GenerateShortID(refreshToken)) if err := common.SetValueWithExpired(accessKey, "1", token.AccessExpireSeconds()); err != nil { return nil, common.ErrInternal } if err := common.SetValueWithExpired(refreshKey, refreshToken, token.RefreshExpireSeconds()); err != nil { return nil, common.ErrInternal } return &vo.LoginResp{ AccessToken: accessToken, RefreshToken: refreshToken, User: vo.NewUserVO(user), }, nil } func checkPassword(password, hash string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil }