chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)

This commit is contained in:
MiFaZhan
2026-11-02 13:27:59 +08:00
1250 changed files with 154111 additions and 0 deletions
@@ -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
}