package member import ( "biz-member-gateway/internal/domain" "bytes" "context" "fmt" "html/template" "code.30cm.net/digimon/app-cloudep-member-server/pkg/domain/member" ntpl "code.30cm.net/digimon/app-cloudep-notification-service/pkg/domain/template" "code.30cm.net/digimon/library-go/errs" "code.30cm.net/digimon/library-go/errs/code" memberProto "code.30cm.net/digimon/proto-all/pkg/member" notificationProto "code.30cm.net/digimon/proto-all/pkg/notification" "biz-member-gateway/internal/svc" "biz-member-gateway/internal/types" "github.com/zeromicro/go-zero/core/logx" ) type SendVerifyCodeLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } // NewSendVerifyCodeLogic 發送邀請 - 綁定會員 func NewSendVerifyCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendVerifyCodeLogic { return &SendVerifyCodeLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *SendVerifyCodeLogic) SendVerifyCode(req *types.VerificationCodeRequest) (resp *types.RespOK, err error) { at := member.GetGetCodeNameByCode(req.CodeType) // 1. 驗證並標準化帳號 acc, err := normalizeAccount(req.CodeType, req.Identifier) if err != nil { return nil, err } // 2. 統一檢查冷卻時間與平台正確性 if err = l.checkAccountValidity(acc, at); err != nil { return nil, err } // 3. 生成驗證碼 vcode, err := l.svcCtx.MemberRPC.GenerateRefreshCode(l.ctx, &memberProto.GenerateRefreshCodeReq{ Account: acc, CodeType: int32(at), }) 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.CodeType, acc, userInfo.Data, vcode.Data.VerifyCode, nickname); err != nil { return nil, err } // 6. 設置 Redis 驗證碼鍵值 rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, at)) l.setRedisKeyWithExpiry(rk, vcode.Data.VerifyCode, int(l.svcCtx.Config.Member.ForgetTimeOutInSec)) return &types.RespOK{}, nil } // checkAccountValidity 同時檢查發送冷卻與平台正確性 func (l *SendVerifyCodeLogic) checkAccountValidity(acc string, gc member.GenerateCodeType) error { // 檢查發送冷卻 rk := domain.GetSendCodeRedisKey(fmt.Sprintf("%s:%d", acc, gc)) 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 } // sendVerificationEmail 發送驗證郵件 func (l *SendVerifyCodeLogic) sendVerificationEmail(recipientEmail string, info *memberProto.UserInfo, verifyCode, nickname string) error { tpl, err := l.svcCtx.NotificationRPC.GetStaticTemplate(l.ctx, ¬ificationProto.TemplateReq{ Language: info.Language, TemplateId: string(ntpl.BindingEmail), }) if err != nil { return err } tmpl, err := template.New("SendVerificationEmail").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, ¬ificationProto.SendMailReq{ To: recipientEmail, Subject: tpl.Title, Body: buf.String(), From: l.svcCtx.Config.MailSender, }) return err } // sendVerificationCode 根據帳號類型發送驗證碼 func (l *SendVerifyCodeLogic) 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 } // setRedisKeyWithExpiry 設置 Redis 鍵與過期時間 func (l *SendVerifyCodeLogic) 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: "func", Value: "SendVerifyCodeLogic.setRedisKeyWithExpiry"}, {Key: "redisKey", Value: rk}, {Key: "error", Value: err.Error()}, }, "failed to set redis expire").Wrap(err) } }