package common import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/limit" "github.com/perfect-panel/server/pkg/random" "github.com/pkg/errors" "gorm.io/gorm" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" queue "github.com/perfect-panel/server/queue/types" ) type SendEmailCodeLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } const ( IntervalTime = 60 ) type VerifyTemplate struct { Type uint8 SiteLogo string SiteName string Expire uint8 Code string } type CacheKeyPayload struct { Code string `json:"code"` LastAt int64 `json:"lastAt"` } // NewSendEmailCodeLogic Get verification code func NewSendEmailCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendEmailCodeLogic { return &SendEmailCodeLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *types.SendCodeResponse, err error) { req.Email = strings.ToLower(strings.TrimSpace(req.Email)) // Check if there is Redis in the code cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.ParseVerifyType(req.Type), req.Email) // Check if the limit is exceeded of current request limiter := limit.NewPeriodLimit(60, 1, l.svcCtx.Redis, fmt.Sprintf("%s:%s:%s", config.SendIntervalKeyPrefix, "email", constant.ParseVerifyType(req.Type))) permit, err := limiter.Take(req.Email) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit") } if !limiter.ParsePermitState(permit) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "send email too many requests") } // Check if the limit is exceeded of today permit, err = l.svcCtx.AuthLimiter.Take(fmt.Sprintf("%s:%s:%s", "email", constant.ParseVerifyType(req.Type), req.Email)) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit") } if !l.svcCtx.AuthLimiter.ParsePermitState(permit) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.TodaySendCountExceedsLimit), "send email too many requests") } m, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error") } if constant.ParseVerifyType(req.Type) == constant.Register && m.Id > 0 { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "mobile already bind") } var payload CacheKeyPayload var taskPayload queue.SendEmailPayload // Generate verification code code := random.Key(6, 0) taskPayload.Type = queue.EmailTypeVerify taskPayload.Email = req.Email taskPayload.Subject = "Verification code" expireTime := l.svcCtx.Config.VerifyCode.ExpireTime if expireTime == 0 { expireTime = 900 } fmt.Printf("expireTime: %v\n", expireTime) expireMinutes := expireTime / 60 taskPayload.Content = map[string]interface{}{ "Type": req.Type, "SiteLogo": l.svcCtx.Config.Site.SiteLogo, "SiteName": l.svcCtx.Config.Site.SiteName, "Expire": expireMinutes, "Code": code, } // Override for account deletion if constant.ParseVerifyType(req.Type) == constant.DeleteAccount { taskPayload.Subject = "注销账号验证" taskPayload.Content["Content"] = fmt.Sprintf("您正在申请注销账号,验证码为:%s,有效期 %d 分钟。如非本人操作,请忽略。", code, expireMinutes) } // Save to Redis payload = CacheKeyPayload{ Code: code, LastAt: time.Now().Unix(), } // Marshal the payload val, _ := json.Marshal(payload) if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*time.Duration(l.svcCtx.Config.VerifyCode.ExpireTime)).Err(); err != nil { l.Errorw("[SendEmailCode]: Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code") } // Marshal the task payload payloadBuy, err := json.Marshal(taskPayload) if err != nil { l.Errorw("[SendEmailCode]: Marshal Error", logger.Field("error", err.Error())) return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") } // Create a queue task task := asynq.NewTask(queue.ForthwithSendEmail, payloadBuy, asynq.MaxRetry(3)) // Enqueue the task taskInfo, err := l.svcCtx.Queue.Enqueue(task) if err != nil { l.Errorw("[SendEmailCode]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payloadBuy))) return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") } l.Infow("[SendEmailCode]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payloadBuy))) if l.svcCtx.Config.Model == constant.DevMode { return &types.SendCodeResponse{ Code: payload.Code, Status: true, }, nil } else { return &types.SendCodeResponse{ Status: true, }, nil } }