chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user