biz-member-gateway/internal/logic/member/forget_password_code_logic.go

193 lines
5.9 KiB
Go

package member
import (
"biz-member-gateway/internal/domain"
"biz-member-gateway/internal/svc"
"biz-member-gateway/internal/types"
"bytes"
"code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member"
"code.30cm.net/digimon/library-go/errs/code"
"context"
"fmt"
"html/template"
"code.30cm.net/digimon/library-go/errs"
ntpl "code.30cm.net/digimon/app-cloudep-notification-service/pkg/domain/template"
memberProto "code.30cm.net/digimon/proto-all/pkg/member"
notificationProto "code.30cm.net/digimon/proto-all/pkg/notification"
"github.com/zeromicro/go-zero/core/logx"
)
type ForgetPasswordCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewForgetPasswordCodeLogic 發送忘記密碼驗證
func NewForgetPasswordCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ForgetPasswordCodeLogic {
return &ForgetPasswordCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ForgetPasswordCodeLogic) ForgetPasswordCode(req *types.ForgetPasswordCodeReq) (resp *types.RespOK, err error) {
// 1. 驗證並標準化帳號
acc, err := normalizeAccount(req.AccountType, req.Account)
if err != nil {
return nil, err
}
// 2. 統一檢查冷卻時間與平台正確性
if err = l.checkAccountValidity(acc); err != nil {
return nil, err
}
// 3. 生成驗證碼
vcode, err := l.svcCtx.MemberRPC.GenerateRefreshCode(l.ctx, &memberProto.GenerateRefreshCodeReq{
Account: acc,
CodeType: int32(member.GenerateCodeTypeForgetPassword),
})
if err != nil {
return nil, err
}
// 4. 取得用戶資訊
uidInfo, err := l.svcCtx.MemberRPC.GetUIDByAccount(l.ctx, &memberProto.GetUIDByAccountReq{Account: acc})
if err != nil {
return nil, err
}
userInfo, err := l.svcCtx.MemberRPC.GetUserInfo(l.ctx, &memberProto.GetUserInfoReq{Uid: uidInfo.GetUid()})
if err != nil {
return nil, err
}
// 5. 發送驗證碼
nickname := getEmailShowName(userInfo)
if err = l.sendVerificationCode(req.AccountType, acc, userInfo.Data, vcode.Data.VerifyCode, nickname); err != nil {
return nil, err
}
// 6. 設置 Redis 驗證碼鍵值
rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, member.GenerateCodeTypeForgetPassword))
l.setRedisKeyWithExpiry(rk, vcode.Data.VerifyCode, int(l.svcCtx.Config.Member.ForgetTimeOutInSec))
return &types.RespOK{}, nil
}
// normalizeAccount 驗證並標準化帳號
func normalizeAccount(accountType, account string) (string, error) {
at := member.GetAccountTypeByCode(accountType)
switch at {
case member.AccountTypePhone:
phone, ok := normalizeTaiwanMobile(account)
if !ok {
return "", errs.InvalidFormat("phone number is invalid")
}
return phone, nil
case member.AccountTypeMail:
if !isValidEmail(account) {
return "", errs.InvalidFormat("email is invalid")
}
return account, nil
default:
return "", errs.InvalidFormat("unsupported account type")
}
}
// checkAccountValidity 同時檢查發送冷卻與平台正確性
func (l *ForgetPasswordCodeLogic) checkAccountValidity(acc string) error {
// 檢查發送冷卻
rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, member.GenerateCodeTypeForgetPassword))
if cached, err := l.svcCtx.Redis.GetCtx(l.ctx, rk); err != nil || cached != "" {
return errs.InvalidRange("verification code already sent, please wait for system to send again")
}
// 檢查平台是否正確(只允許平台帳號進行忘記密碼操作)
accountInfo, err := l.svcCtx.MemberRPC.GetUserAccountInfo(l.ctx, &memberProto.GetUIDByAccountReq{Account: acc})
if err != nil {
return err
}
if accountInfo.Data.Platform != member.Digimon.ToInt64() {
return errs.InvalidResourceState("failed to send verify code since platform not correct")
}
return nil
}
// sendVerificationCode 根據帳號類型發送驗證碼
func (l *ForgetPasswordCodeLogic) sendVerificationCode(accountType, acc string, info *memberProto.UserInfo, verifyCode, nickname string) error {
switch member.GetAccountTypeByCode(accountType) {
case member.AccountTypePhone:
// TODO: 傳送簡訊
fmt.Printf("SMS Template: %s\n", verifyCode)
case member.AccountTypeMail:
return l.sendVerificationEmail(acc, info, verifyCode, nickname)
default:
return errs.InvalidResourceState("unsupported account type")
}
return nil
}
type EmailTmpInfo struct {
Username string
VerifyCode string
}
// sendVerificationEmail 發送驗證郵件
func (l *ForgetPasswordCodeLogic) sendVerificationEmail(recipientEmail string, info *memberProto.UserInfo, verifyCode, nickname string) error {
tpl, err := l.svcCtx.NotificationRPC.GetStaticTemplate(l.ctx, &notificationProto.TemplateReq{
Language: info.Language,
TemplateId: string(ntpl.ForgetPasswordVerify),
})
if err != nil {
return err
}
tmpl, err := template.New("ForgetPasswordEmail").Parse(tpl.GetBody())
if err != nil {
return err
}
emailParams := EmailTmpInfo{Username: nickname, VerifyCode: verifyCode}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, emailParams); err != nil {
return err
}
_, err = l.svcCtx.NotificationRPC.SendMail(l.ctx, &notificationProto.SendMailReq{
To: recipientEmail,
Subject: tpl.Title,
Body: buf.String(),
From: l.svcCtx.Config.MailSender,
})
return err
}
// getEmailShowName 取得寄信用的顯示名稱
func getEmailShowName(info *memberProto.GetUserInfoResp) string {
if info.Data.FullName != nil {
return *info.Data.FullName
}
if info.Data.NickName != nil {
return *info.Data.NickName
}
return info.Data.Uid
}
// setRedisKeyWithExpiry 設置 Redis 鍵與過期時間
func (l *ForgetPasswordCodeLogic) setRedisKeyWithExpiry(rk, verifyCode string, expiry int) {
if status, err := l.svcCtx.Redis.SetnxExCtx(l.ctx, rk, verifyCode, expiry); err != nil || !status {
_ = errs.DatabaseErrorWithScopeL(code.CloudEPMember, domain.FailedToSetVerifyCodeErrorCode,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "redisKey", Value: rk},
{Key: "error", Value: err.Error()},
}, "failed to set redis expire").Wrap(err)
}
}