Files
nl-im-service/internal/service/auth_service.go
2026-08-24 15:29:53 +08:00

271 lines
7.3 KiB
Go
Raw Permalink 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
* 作用:认证服务,处理登录、注册、验证码等功能
*/
package service
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"log"
"math/big"
"strings"
"time"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/utils"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
// AuthService 认证服务结构体
type AuthService struct {
DB *gorm.DB
Redis *redis.Client
}
// AuthSvc 全局单例
var AuthSvc *AuthService
/**
* InitAuthService
* 功能:初始化认证服务
*/
func InitAuthService(db *gorm.DB, rdb *redis.Client) {
AuthSvc = &AuthService{DB: db, Redis: rdb}
}
/**
* Login
* 功能:用户登录验证
* @param account 账号(邮箱或手机号)
* @param password 密码
* @returns 用户信息和错误
*/
func (s *AuthService) Login(account, password string) (*model.User, error) {
var user model.User
// 根据邮箱或手机号查询用户
result := s.DB.Where("email = ? OR phone = ?", account, account).First(&user)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, result.Error
}
// 验证密码
if !utils.CheckPassword(password, user.Password) {
return nil, errors.New("密码错误")
}
return &user, nil
}
/**
* Register
* 功能:用户注册
* @param req 注册请求
* @returns 用户信息和错误
*/
func (s *AuthService) Register(req *model.RegisterReq) (*model.User, error) {
// 验证密码一致性
if req.Password != req.ConfirmPassword {
return nil, errors.New("两次密码输入不一致")
}
// 检查邮箱是否已存在
var existingUser model.User
if err := s.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
return nil, errors.New("邮箱已被注册")
}
// 检查手机号是否已存在
if err := s.DB.Where("phone = ?", req.Phone).First(&existingUser).Error; err == nil {
return nil, errors.New("手机号已被注册")
}
// 加密密码
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
return nil, fmt.Errorf("密码加密失败: %v", err)
}
// 生成用户ID(使用时间戳+随机数)
userID := fmt.Sprintf("%d%s", time.Now().UnixNano(), generateRandomString(6))
// 生成默认名称(使用邮箱前缀)
defaultName := req.Email
if atIndex := strings.Index(req.Email, "@"); atIndex > 0 {
defaultName = req.Email[:atIndex]
}
if len(defaultName) > 20 {
defaultName = defaultName[:20]
}
// 创建用户
user := model.User{
ID: userID,
Email: req.Email,
Phone: req.Phone,
Password: hashedPassword,
Name: defaultName,
Avatar: generateAvatar(userID),
Desc: "",
Region: "",
}
if err := s.DB.Create(&user).Error; err != nil {
return nil, fmt.Errorf("创建用户失败: %v", err)
}
// 清除密码字段
user.Password = ""
return &user, nil
}
/**
* SendEmailCode
* 功能:发送邮箱验证码(模拟)
* @param email 邮箱
* @returns 验证码和错误
*/
func (s *AuthService) SendEmailCode(email string) (string, error) {
// 生成6位验证码
code := generateCode(6)
// 保存验证码到数据库(5分钟过期)
vc := model.VerificationCode{
Target: email,
Code: code,
Type: "email",
ExpiresAt: time.Now().Add(5 * time.Minute),
}
if err := s.DB.Create(&vc).Error; err != nil {
return "", fmt.Errorf("保存验证码失败: %v", err)
}
// 模拟发送(实际应调用邮件服务)
// 这里直接返回验证码,生产环境应通过邮件发送
return code, nil
}
/**
* SendSmsCode
* 功能:发送短信验证码(模拟)
* @param phone 手机号
* @returns 验证码和错误
*/
func (s *AuthService) SendSmsCode(phone string) (string, error) {
// 生成6位验证码
code := generateCode(6)
// 保存验证码到数据库(5分钟过期)
vc := model.VerificationCode{
Target: phone,
Code: code,
Type: "sms",
ExpiresAt: time.Now().Add(5 * time.Minute),
}
if err := s.DB.Create(&vc).Error; err != nil {
return "", fmt.Errorf("保存验证码失败: %v", err)
}
// 模拟发送(实际应调用短信服务)
// 这里直接返回验证码,生产环境应通过短信发送
return code, nil
}
/**
* VerifyCode
* 功能:验证验证码(只读校验,不消费)。
* 为什么不在这里删除:调用方(注册)在校验之后还有可失败的步骤(落库、
* 邮箱/手机号重复等),校验即删会导致注册失败后同一验证码无法重试,
* 用户被迫重新获取。消费动作由 ConsumeCode 在业务成功后显式执行——
* 与扫码登录"窥视→动作→原子消费"同一模式
* @param target 邮箱或手机号
* @param code 验证码
* @param codeType 类型(email/sms)
* @returns 是否有效
*/
func (s *AuthService) VerifyCode(target, code, codeType string) (bool, error) {
var vc model.VerificationCode
// 查询验证码
result := s.DB.Where("target = ? AND code = ? AND type = ? AND expires_at > ?",
target, code, codeType, time.Now()).
Order("created_at DESC").
First(&vc)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, errors.New("验证码无效或已过期")
}
return false, result.Error
}
return true, nil
}
/**
* ConsumeCode
* 功能:消费(删除)验证码,业务成功后调用,防止同一验证码被重放使用。
* 说明:按 (target, code, type) 删除全部匹配记录,重复调用幂等;
* 校验到消费之间的并发重放窗口由注册本身的邮箱/手机唯一约束兜底,无实际危害
*/
func (s *AuthService) ConsumeCode(target, code, codeType string) {
if err := s.DB.Where("target = ? AND code = ? AND type = ?", target, code, codeType).
Delete(&model.VerificationCode{}).Error; err != nil {
// 删除失败只记日志不阻断业务:验证码有 5 分钟 TTL 兜底过期
log.Printf("⚠️ [Auth] 消费验证码失败 target=%s: %v", target, err)
}
}
// 辅助函数:生成随机字符串
// rand.Read 错误必须处理:crypto/rand 失败时缓冲区是全零字节,
// 生成的"随机"串完全可预测(用于用户ID后缀等场景会产生碰撞/被猜测风险),
// 此时降级为纳秒时间戳兜底,保证仍有基本的唯一性
func generateRandomString(length int) string {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
log.Printf("⚠️ crypto/rand 读取失败,降级为时间戳随机: %v", err)
ts := fmt.Sprintf("%x", time.Now().UnixNano())
for len(ts) < length {
ts += ts
}
return ts[:length]
}
return hex.EncodeToString(b)[:length]
}
// 辅助函数:生成验证码
// rand.Int 错误必须处理:忽略错误时 n 为 nil 会直接 panic(n.String() 空指针),
// 失败位降级为纳秒时间戳取模,保证验证码仍是完整位数的数字
func generateCode(length int) string {
code := ""
for i := 0; i < length; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
log.Printf("⚠️ crypto/rand 生成验证码位失败,降级为时间戳随机: %v", err)
code += fmt.Sprintf("%d", time.Now().UnixNano()%10)
continue
}
code += n.String()
}
return code
}
// 辅助函数:生成默认头像
func generateAvatar(userID string) string {
// 简单实现:使用用户ID的第一个字符
if len(userID) > 0 {
return string(userID[0])
}
return "U"
}