// // Copyright (C) 2024 veypi // 2025-03-04 16:08:06 // Distributed under terms of the MIT license. // package verification import ( "fmt" "math/rand" "time" "github.com/veypi/vbase/cfg" "github.com/veypi/vbase/libs/email" "github.com/veypi/vbase/libs/sms" "github.com/veypi/vbase/models" "github.com/veypi/vigo" ) // SendRequest 发送验证码请求 type SendRequest struct { Type string `json:"type" src:"json" desc:"类型: email/sms"` Target string `json:"target" src:"json" desc:"邮箱或手机号"` Purpose string `json:"purpose" src:"json" desc:"用途: register/login/reset_password/bind"` } // SendResponse 发送响应 type SendResponse struct { Message string `json:"message"` } // sendCode 统一发送验证码 func sendCode(x *vigo.X, req *SendRequest) (*SendResponse, error) { // 参数校验 if req.Type != "email" && req.Type != "sms" { return nil, vigo.ErrInvalidArg.WithString("invalid type, must be email or sms") } if req.Target == "" { return nil, vigo.ErrInvalidArg.WithString("target is required") } if req.Purpose == "" { req.Purpose = models.CodePurposeLogin } db := cfg.DB() // 检查发送频率限制 interval, _ := models.GetSettingInt(models.SettingCodeSendInterval) if interval == 0 { interval = 60 } var lastCode models.VerificationCode if err := db.Where("target = ? AND type = ?", req.Target, req.Type). Order("created_at DESC").First(&lastCode).Error; err == nil { // 检查是否在间隔期内 if time.Since(lastCode.CreatedAt) < time.Duration(interval)*time.Second { return nil, vigo.ErrTooManyRequests.WithString(fmt.Sprintf("please wait %d seconds", interval)) } } // 检查每日发送次数限制 // 配置说明: 0=禁用验证码功能, -1=不限制, >0=限制次数 maxDaily, _ := models.GetSettingInt(models.SettingCodeMaxDailyCount) if maxDaily == 0 { return nil, vigo.ErrForbidden.WithString("verification code service is disabled") } if maxDaily > 0 { startOfDay := time.Now().Truncate(24 * time.Hour) var dailyCount int64 db.Model(&models.VerificationCode{}). Where("target = ? AND type = ? AND created_at >= ?", req.Target, req.Type, startOfDay). Count(&dailyCount) if int(dailyCount) >= maxDaily { return nil, vigo.ErrTooManyRequests.WithString("daily limit exceeded") } } // 生成验证码 codeLength, _ := models.GetSettingInt(models.SettingCodeLength) if codeLength == 0 { codeLength = 6 } code := generateCode(codeLength) // 过期时间 expiryMinutes, _ := models.GetSettingInt(models.SettingCodeExpiry) if expiryMinutes == 0 { expiryMinutes = 5 } expiresAt := time.Now().Add(time.Duration(expiryMinutes) * time.Minute) // 保存验证码到数据库 verification := &models.VerificationCode{ Target: req.Target, Type: req.Type, Code: code, Purpose: req.Purpose, Status: models.CodeStatusPending, ExpiresAt: expiresAt, RemoteIP: x.GetRemoteIP(), } if err := db.Create(verification).Error; err != nil { return nil, vigo.ErrInternalServer.WithError(err) } // 发送验证码 var sendErr error switch req.Type { case "email": sendErr = sendEmailCode(req.Target, code) case "sms": sendErr = sendSMSCode(req.Target, code) } if sendErr != nil { // 更新状态为失败 db.Model(verification).Update("status", models.CodeStatusFailed) return nil, vigo.ErrInternalServer.WithError(sendErr) } return &SendResponse{Message: "verification code sent"}, nil } // sendEmailCode 发送邮件验证码 func sendEmailCode(target, code string) error { // 检查邮件服务是否启用 enabled, err := models.GetSettingBool(models.SettingEmailEnabled) if err != nil || !enabled { return fmt.Errorf("email service not enabled") } // 发送邮件 if err := email.SendVerificationCode(target, code); err != nil { return err } // 记录日志 log := &models.EmailLog{ To: target, Subject: "验证码", Provider: "smtp", Status: "sent", } cfg.DB().Create(log) return nil } // sendSMSCode 发送短信验证码 func sendSMSCode(phone, code string) error { // 检查短信服务是否启用 enabled, err := models.GetSettingBool(models.SettingSMSEnabled) if err != nil || !enabled { return fmt.Errorf("sms service not enabled") } // 获取短信配置 provider, _ := models.GetSetting(models.SettingSMSProvider) accessKey, _ := models.GetSetting(models.SettingSMSAccessKey) accessSecret, _ := models.GetSetting(models.SettingSMSAccessSecret) signName, _ := models.GetSetting(models.SettingSMSSignName) templateCode, _ := models.GetSetting(models.SettingSMSTemplateCode) if accessKey == "" || accessSecret == "" { return fmt.Errorf("sms not configured") } // 创建短信提供商 smsProvider, err := sms.NewProvider(provider, accessKey, accessSecret, signName, templateCode) if err != nil { return err } // 发送短信 if err := smsProvider.Send(phone, code); err != nil { return err } return nil } // generateCode 生成随机验证码 func generateCode(length int) string { const digits = "0123456789" code := make([]byte, length) for i := range code { code[i] = digits[rand.Intn(len(digits))] } return string(code) }