package warningconfig import ( "context" "crypto/rand" "encoding/json" "fmt" "math/big" "mime" "net" "net/mail" "net/smtp" "strings" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) const ( warnMethodSMS = "0" warnMethodEmail = "1" warnMethodPhone = "2" warnMethodPlatform = "3" warnEmailCodeTTLSeconds = 300 warnEmailCodeRedisKeyScope = "iotlcm:warning:email-code" ) func SendEmailCode(ctx context.Context, in *model.TSimWarningConfigEmailCodeParam) error { if in == nil { return gerror.New("邮箱验证码参数不能为空") } email, err := normalizeSingleWarnEmail(in.Email) if err != nil { return err } if strings.TrimSpace(in.CaptchaID) == "" || strings.TrimSpace(in.CaptchaValue) == "" { return gerror.New("图片验证码不能为空") } if err = service.SysCaptcha().VerifyCaptcha(ctx, in.CaptchaID, in.CaptchaValue); err != nil { return err } userID := contexts.GetUserId(ctx) tenantID := contexts.GetTenantId(ctx) if userID <= 0 || strings.TrimSpace(tenantID) == "" { return gerror.New("登录信息已过期") } code, err := generateWarnEmailCode() if err != nil { return gerror.Wrap(err, "生成邮箱验证码失败") } if err = setWarnEmailCode(ctx, tenantID, userID, email, code); err != nil { return gerror.Wrap(err, "保存邮箱验证码失败,请稍后重试") } return sendWarnEmailCode(ctx, email, code) } func ValidateEmailCode(ctx context.Context, options Options) error { return validateEmailCode(ctx, options, false) } func ConsumeEmailCode(ctx context.Context, options Options) error { return validateEmailCode(ctx, options, true) } func validateEmailCode(ctx context.Context, options Options, consume bool) error { if !hasEmailMethod(options.WarnMethods) { return nil } email, err := normalizeSingleWarnEmail(options.WarnEmails) if err != nil { return err } code := strings.TrimSpace(options.WarnEmailCode) if code == "" { return gerror.New("请选择邮箱提醒时必须填写邮箱验证码") } userID := contexts.GetUserId(ctx) tenantID := contexts.GetTenantId(ctx) if userID <= 0 || strings.TrimSpace(tenantID) == "" { return gerror.New("登录信息已过期") } cacheCode, err := getWarnEmailCode(ctx, tenantID, userID, email) if err != nil { return gerror.Wrap(err, "获取邮箱验证码失败,请稍后重试") } if cacheCode == "" { return gerror.New("邮箱验证码已失效") } if cacheCode != code { return gerror.New("邮箱验证码错误") } if consume { return deleteWarnEmailCode(ctx, tenantID, userID, email) } return nil } func validateNotificationOptions(options *Options) error { if options == nil { return gerror.New("预警配置参数不能为空") } if hasSMSMethod(options.WarnMethods) { phones, err := normalizeWarnPhones(options.WarnPhones) if err != nil { return err } options.WarnPhones = phones } if hasEmailMethod(options.WarnMethods) { emails, err := normalizeWarnEmails(options.WarnEmails) if err != nil { return err } options.WarnEmails = emails } return nil } func hasSMSMethod(methods string) bool { for _, method := range splitWarnList(methods) { if method == warnMethodSMS { return true } } return false } func hasEmailMethod(methods string) bool { for _, method := range splitWarnList(methods) { if method == warnMethodEmail { return true } } return false } func normalizeSingleWarnEmail(value string) (string, error) { emails := splitWarnList(value) if len(emails) == 0 { return "", gerror.New("请选择邮箱提醒时必须填写邮箱地址") } if len(emails) > 1 { return "", gerror.New("邮箱提醒验证码第一版仅支持一个邮箱地址") } address, err := mail.ParseAddress(emails[0]) if err != nil || address == nil || strings.TrimSpace(address.Address) == "" { return "", gerror.New("邮箱格式不正确") } return strings.ToLower(strings.TrimSpace(address.Address)), nil } func normalizeWarnEmails(value string) (string, error) { emails := splitWarnList(value) if len(emails) == 0 { return "", gerror.New("请选择邮箱提醒时必须填写邮箱地址") } result := make([]string, 0, len(emails)) for _, email := range emails { address, err := mail.ParseAddress(email) if err != nil || address == nil || strings.TrimSpace(address.Address) == "" { return "", gerror.Newf("邮箱格式不正确:%s", email) } result = append(result, strings.ToLower(strings.TrimSpace(address.Address))) } return strings.Join(result, ","), nil } func normalizeWarnPhones(value string) (string, error) { phones := splitWarnList(value) if len(phones) == 0 { return "", gerror.New("请选择短信提醒时必须填写短信号码") } for _, phone := range phones { if !isValidWarnPhone(phone) { return "", gerror.Newf("手机号格式不正确:%s", phone) } } return strings.Join(phones, ","), nil } func isValidWarnPhone(phone string) bool { if len(phone) != 11 || phone[0] != '1' { return false } for _, r := range phone { if r < '0' || r > '9' { return false } } return phone[1] >= '3' && phone[1] <= '9' } func splitWarnList(value string) []string { parts := strings.FieldsFunc(value, func(r rune) bool { switch r { case ',', ';', ',', ';', '|', '\n', '\r', '\t', ' ': return true default: return false } }) seen := make(map[string]struct{}, len(parts)) result := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } key := strings.ToLower(part) if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} result = append(result, part) } return result } func warnEmailCodeKey(tenantID string, userID int64, email string) string { return fmt.Sprintf("%s:%s:%d:%s", warnEmailCodeRedisKeyScope, strings.TrimSpace(tenantID), userID, strings.ToLower(strings.TrimSpace(email))) } func setWarnEmailCode(ctx context.Context, tenantID string, userID int64, email string, code string) error { _, err := g.Redis().Do(ctx, "SETEX", warnEmailCodeKey(tenantID, userID, email), warnEmailCodeTTLSeconds, code) return err } func getWarnEmailCode(ctx context.Context, tenantID string, userID int64, email string) (string, error) { value, err := g.Redis().Get(ctx, warnEmailCodeKey(tenantID, userID, email)) if err != nil { return "", err } if value.IsNil() { return "", nil } return value.String(), nil } func deleteWarnEmailCode(ctx context.Context, tenantID string, userID int64, email string) error { _, err := g.Redis().Del(ctx, warnEmailCodeKey(tenantID, userID, email)) return err } func generateWarnEmailCode() (string, error) { n, err := rand.Int(rand.Reader, big.NewInt(1000000)) if err != nil { return "", err } return fmt.Sprintf("%06d", n.Int64()), nil } func sendWarnEmailCode(ctx context.Context, email string, code string) error { cfg, ok := loadWarnEmailCodeConfig(ctx) if !ok || strings.TrimSpace(cfg.Host) == "" { if strings.TrimSpace(cfg.Url) != "" { return callWarnEmailCodeHTTP(ctx, cfg.UserCodeHttpSenderConfig, email, code) } g.Log().Infof(ctx, "warning email config code, email:%s, code:%s", email, code) return nil } if cfg.Port <= 0 { cfg.Port = 25 } if strings.TrimSpace(cfg.From) == "" { cfg.From = cfg.Username } if strings.TrimSpace(cfg.From) == "" { return gerror.New("预警邮箱验证码发件人为空") } addr := net.JoinHostPort(cfg.Host, gconv.String(cfg.Port)) auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) msg := buildWarnEmailCodeMessage(cfg, email, code) err := utility.SendSMTP(ctx, utility.SMTPOptions{ Address: addr, Host: cfg.Host, Auth: auth, From: cfg.From, To: []string{email}, Data: msg, SSL: cfg.SSL, Timeout: utility.ExternalRequestTimeout(cfg.Timeout), }) if err != nil { g.Log().Errorf(ctx, "send warning email config code failed, email:%s, err:%+v", email, err) return gerror.New("发送邮箱验证码失败,请稍后重试") } return nil } func loadWarnEmailCodeConfig(ctx context.Context) (model.UserCodeEmailSenderConfig, bool) { var cfg model.UserCodeEmailSenderConfig for _, key := range []string{"warning.email", "register.email"} { v, err := g.Cfg().Get(ctx, key) if err != nil || v.IsNil() { continue } _ = v.Scan(&cfg) if strings.TrimSpace(cfg.Url) != "" || strings.TrimSpace(cfg.Host) != "" { return cfg, true } } return cfg, false } func buildWarnEmailCodeMessage(cfg model.UserCodeEmailSenderConfig, email string, code string) []byte { subject := strings.TrimSpace(cfg.Subject) if subject == "" || subject == "注册验证码" { subject = "预警邮箱验证码" } body := renderWarnEmailCodeTemplate(cfg.BodyTemplate, email, code) if body == "" || strings.Contains(body, "注册验证码") { body = fmt.Sprintf("您的预警邮箱验证码是:%s,5分钟内有效。", code) } from := strings.TrimSpace(cfg.From) if strings.TrimSpace(cfg.FromName) != "" { from = mime.QEncoding.Encode("UTF-8", cfg.FromName) + " <" + from + ">" } message := strings.Builder{} message.WriteString("From: " + from + "\r\n") message.WriteString("To: " + email + "\r\n") message.WriteString("Subject: " + mime.QEncoding.Encode("UTF-8", subject) + "\r\n") message.WriteString("MIME-Version: 1.0\r\n") message.WriteString("Content-Type: text/html; charset=UTF-8\r\n") message.WriteString("\r\n") message.WriteString(body) return []byte(message.String()) } func renderWarnEmailCodeTemplate(template string, contact string, code string) string { if strings.TrimSpace(template) == "" { return "" } replacer := strings.NewReplacer( "{contact}", contact, "{{contact}}", contact, "{email}", contact, "{{email}}", contact, "{code}", code, "{{code}}", code, ) return replacer.Replace(template) } func callWarnEmailCodeHTTP(ctx context.Context, cfg model.UserCodeHttpSenderConfig, email string, code string) error { method := strings.ToUpper(strings.TrimSpace(cfg.Method)) if method == "" { method = "POST" } successStatus := cfg.SuccessStatus if successStatus <= 0 { successStatus = 200 } body := renderWarnEmailCodeTemplate(cfg.BodyTemplate, email, code) if body == "" { bodyBytes, _ := json.Marshal(g.Map{ "contact": email, "email": email, "code": code, }) body = string(bodyBytes) } headers := cfg.Headers if headers == nil { headers = map[string]string{} } if _, ok := headers["Content-Type"]; !ok { headers["Content-Type"] = "application/json" } resp, err := g.Client().Timeout(utility.ExternalRequestTimeout(cfg.Timeout)).SetHeaderMap(headers).DoRequest(ctx, method, cfg.Url, body) if err != nil { return gerror.Wrap(err, "发送邮箱验证码失败,请稍后重试") } defer resp.Close() if resp.StatusCode != successStatus { return gerror.Newf("发送邮箱验证码失败,服务响应状态码:%d", resp.StatusCode) } return nil }