package user import ( "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" "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 } // 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) { 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 } return info.UID }