backend/internal/logic/user/request_verification_code_l...

212 lines
5.9 KiB
Go
Raw Normal View History

package user
import (
2025-11-08 06:37:41 +00:00
"backend/internal/domain"
"backend/internal/utils/email_template"
errs "backend/pkg/library/errors"
mbr "backend/pkg/member/domain/member"
member "backend/pkg/member/domain/usecase"
"backend/pkg/notification/domain/usecase"
"backend/pkg/permission/domain/token"
"bytes"
"context"
2025-11-08 06:37:41 +00:00
"fmt"
"html/template"
"regexp"
"strings"
"backend/internal/svc"
"backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RequestVerificationCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
2025-11-07 07:44:23 +00:00
// NewRequestVerificationCodeLogic 請求發送驗證碼 (用於驗證信箱/手機)
func NewRequestVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestVerificationCodeLogic {
return &RequestVerificationCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RequestVerificationCodeLogic) RequestVerificationCode(req *types.RequestVerificationCodeReq) (resp *types.RespOK, err error) {
2025-11-08 06:37:41 +00:00
acc := ""
ct := mbr.GenerateCodeTypeEmail
switch req.Purpose {
case "email_verification":
if !isValidEmail(req.Account) {
return nil, errs.InputInvalidFormatError("email is invalid")
}
acc = req.Account
// 1. TODO 討論 email 不可以再被使用
// 2. TODO 討論 email 跟我帳號是不是一樣(如果是用自己的信箱註冊的話)
case "phone_verification":
phone, isPhone := normalizeTaiwanMobile(req.Account)
if !isPhone {
return nil, errs.InputInvalidFormatError("phone number is invalid")
}
acc = phone
// TODO 討論號碼有被用過就不可以再被使用了
ct = mbr.GenerateCodeTypePhone
default:
return &types.RespOK{}, errs.InputInvalidRangeError("")
}
uid := token.UID(l.ctx)
// 限制三分鐘內只可以發送一次
rk := domain.GenerateVerifyCodeRedisKey.With(
fmt.Sprintf("%s-%s", uid, req.Purpose),
).ToString()
// 拿不到不會出錯DB 壞掉才會
get, err := l.svcCtx.Redis.GetCtx(l.ctx, rk)
if err != nil {
return nil, errs.DBErrorError("failed to connect to redis").Wrap(err)
}
if get != "" {
// 已經發送過驗證碼,返回提示
return nil, errs.SysTooManyRequestError("code already sent, please wait 3min for system to send again")
}
// 生成驗證碼
vcode, err := l.svcCtx.AccountUC.GenerateRefreshCode(l.ctx, member.GenerateRefreshCodeRequest{
LoginID: acc,
CodeType: ct,
})
if err != nil {
return nil, err
}
// 取得用戶資訊
info, err := l.svcCtx.AccountUC.GetUserInfo(l.ctx, member.GetUserInfoRequest{
UID: uid,
})
if err != nil {
return nil, err
}
nickname := generateMsgName(&info)
switch ct {
case mbr.GenerateCodeTypeEmail:
body, title, err := email_template.GetEmailTemplate(email_template.Language(info.PreferredLanguage), email_template.BindingEmail)
if err != nil {
e := errs.ResNotFoundError("failed to get correct email template")
return nil, e
}
tmpl, err := template.New("BindEmailBody").Parse(body)
if err != nil {
e := errs.ResInvalidFormatError("failed to get correct email template")
return nil, e
}
emailParams := email_template.ForgetPasswordEmailReq{
Username: nickname,
VerifyCode: vcode.Data.VerifyCode,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, emailParams); err != nil {
e := errs.ResInvalidFormatError("failed to build data")
return nil, e
}
err = l.svcCtx.DeliveryUC.SendEmail(l.ctx, usecase.MailReq{
To: []string{req.Account},
From: l.svcCtx.Config.SMTPConfig.Sender,
SenderName: l.svcCtx.Config.SMTPConfig.SenderName,
Subject: title,
Body: buf.String(),
})
if err != nil {
e := errs.SvcThirdPartyError("failed to send email").Wrap(err)
return nil, e
}
case mbr.GenerateCodeTypePhone:
//// 送出手機號碼
//templateResp, err := l.svcCtx.NotificationUseCase.GetSMSTemplateByTypeID(
// l.ctx, notificationModule.Language(info.PreferredLanguage), notificationModule.BindingPhone)
//if err != nil {
// return nil, err
//}
//
//fmt.Println(fmt.Sprintf("%s:%s", templateResp.Body, vcode.Data.VerifyCode))
////err = l.svcCtx.NotificationUseCase.SendMessage(l.ctx, notificationModule.SMSMessageRequest{
//// PhoneNumber: acc,
//// RecipientName: nickname,
//// MessageContent: fmt.Sprintf("%s:%s", templateResp.Body, vcode),
////})
////if err != nil {
//// return nil, err
////}
case mbr.GenerateCodeTypeNone:
case mbr.GenerateCodeTypeForgetPassword:
default:
return &types.RespOK{}, errs.InputInvalidRangeError("")
}
// 設置 Redis 鍵,並設置 3 分鐘的過期時間
status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, vcode.Data.VerifyCode, 60*3)
if err != nil || !status {
// 純記錄,前面都已經成功,就不報錯了
_ = errs.DBErrorErrorL(l.svcCtx.Logger,
[]errs.LogField{
{Key: "req", Val: req},
{Key: "func", Val: "Redis.SetnxExCtx"},
{Key: "err", Val: err.Error()},
}, "failed to set redis expire").Wrap(err)
}
return &types.RespOK{}, nil
}
// 標準化號碼並驗證是否為合法台灣手機號碼
func normalizeTaiwanMobile(phone string) (string, bool) {
// 移除空格
phone = strings.ReplaceAll(phone, " ", "")
// 移除 "+886" 並將剩餘部分標準化
if strings.HasPrefix(phone, "+886") {
phone = strings.TrimPrefix(phone, "+886")
if !strings.HasPrefix(phone, "0") {
phone = "0" + phone
}
}
// 正則表達式驗證標準化後的號碼
regex := regexp.MustCompile(`^(09\d{8})$`)
if regex.MatchString(phone) {
return phone, true
}
return "", false
}
// 驗證 Email 格式的函數
func isValidEmail(email string) bool {
// 定義正則表達式
regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return regex.MatchString(email)
}
// generateMsgName 取得寄信用的名稱
func generateMsgName(info *member.UserInfo) string {
if info.FullName != nil {
return *info.FullName
}
if info.Nickname != nil {
return *info.Nickname
}
2025-11-08 06:37:41 +00:00
return info.UID
}