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
}
@@ -0,0 +1,99 @@
package warningconfig
import (
"context"
"errors"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text"
"github.com/gogf/gf/v2/errors/gerror"
)
func ParseInputRows(ctx context.Context, input string, itemType, warnUnit int) ([]batchimport.Row, error) {
lines, err := text.ParseNonEmptyLines(ctx, input)
if err != nil {
if errors.Is(err, text.ErrEmptyInput) {
return nil, gerror.New("批量输入预警配置数据为空")
}
return nil, err
}
rows := make([]batchimport.Row, 0, len(lines))
for _, line := range lines {
row := &Row{RowNo: line.RowNo, ItemType: itemType}
fields := parseInputFields(line.Text)
if isCompleteInputRow(itemType, fields) {
row.ConfigSet = true
row.Config, err = parseInputConfig(itemType, warnUnit, fields[inputConfigOffset(itemType):])
if err != nil {
row.AddErrorReason(err.Error())
}
if itemType == warnItemTypeFlowPool {
row.FlowPoolCode = strings.TrimSpace(fields[0])
} else {
row.Iccid = strings.TrimSpace(fields[0])
row.Msisdn = strings.TrimSpace(fields[1])
row.Imsi = strings.TrimSpace(fields[2])
}
rows = append(rows, row)
continue
}
if itemType == warnItemTypeFlowPool {
fields = strings.Fields(line.Text)
if len(fields) == 1 {
row.FlowPoolCode = fields[0]
} else {
row.AddErrorReason("格式错误,应为流量池编号")
}
rows = append(rows, row)
continue
}
fields = strings.Fields(line.Text)
switch len(fields) {
case 1:
row.Iccid = fields[0]
case 3:
row.Iccid = fields[0]
row.Msisdn = fields[1]
row.Imsi = fields[2]
default:
row.AddErrorReason("格式错误,应为 ICCID 或 ICCID MSISDN IMSI")
}
rows = append(rows, row)
}
return rows, nil
}
func parseInputFields(value string) []string {
value = strings.TrimSpace(value)
if strings.Contains(value, "|") {
parts := strings.Split(value, "|")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
}
if strings.Contains(value, "\t") {
parts := strings.Split(value, "\t")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
}
return nil
}
func inputConfigOffset(itemType int) int {
if itemType == warnItemTypeFlowPool {
return 1
}
return 3
}
func isCompleteInputRow(itemType int, fields []string) bool {
if itemType == warnItemTypeFlowPool {
return len(fields) == 9
}
return len(fields) == 12
}
@@ -0,0 +1,62 @@
package warningconfig
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
)
func ParseExcelFileRows(ctx context.Context, filePath string, itemType int) ([]batchimport.Row, error) {
parsedRows, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{
RequiredHeaders: requiredHeaders(itemType),
SingleSheet: true,
})
if err != nil {
return nil, err
}
baseOptions, err := NormalizeContextOptions(itemType, defaultWarnUnit(itemType))
if err != nil {
return nil, err
}
rows := make([]batchimport.Row, 0, len(parsedRows))
for _, parsed := range parsedRows {
if itemType == warnItemTypeFlowPool {
row := &Row{
RowNo: parsed.RowNo,
ItemType: warnItemTypeFlowPool,
FlowPoolCode: excel.Field(parsed, poolHeaderCode),
Config: baseOptions,
ConfigSet: true,
}
row.Config, err = parseExcelRowOptions(parsed, itemType, baseOptions.WarnUnit)
if err != nil {
row.AddErrorReason(err.Error())
}
rows = append(rows, row)
continue
}
row := &Row{
RowNo: parsed.RowNo,
ItemType: warnItemTypeCard,
Iccid: excel.Field(parsed, cardHeaderIccid),
Msisdn: excel.Field(parsed, cardHeaderMsisdn),
Imsi: excel.Field(parsed, cardHeaderImsi),
Config: baseOptions,
ConfigSet: true,
}
row.Config, err = parseExcelRowOptions(parsed, itemType, baseOptions.WarnUnit)
if err != nil {
row.AddErrorReason(err.Error())
}
rows = append(rows, row)
}
return rows, nil
}
func defaultWarnUnit(itemType int) int {
if itemType == warnItemTypeFlowPool {
return warnUnitPercent
}
return warnUnitMb
}
@@ -0,0 +1,433 @@
package warningconfig
import (
"context"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
const (
warnItemTypeCard = 1
warnItemTypeFlowPool = 2
warnUnitMb = 1
warnUnitPercent = 2
warnUnitDay = 3
warnUnitMonth = 4
warnTypeCardTraffic = 1
warnTypeExpireDay = 2
warnTypeExpireMonth = 3
warnTypeFlowPoolPercent = 4
warnAutoActionDataCut = 3
warnMessageDefaultType = warnMethodPlatform
)
type Processor struct {
config batchimport.ProcessorConfig
itemType int
}
func NewProcessor(itemTypeInput ...int) *Processor {
itemType := warnItemTypeCard
if len(itemTypeInput) > 0 {
itemType = normalizeItemType(itemTypeInput[0])
}
return &Processor{
itemType: itemType,
config: batchimport.ProcessorConfig{
Code: "sim_warning_config",
BatchType: consts.BatchTypeSimWarningConfig,
BatchNoPrefix: consts.BatchNoPrefixSimWarningConfig,
TaskName: consts.ResourceBatchDownloadTaskSimWarningConfig,
ExportScene: consts.ResourceExportSceneSimWarningConfig,
ImportDir: consts.SimWarningConfigSubDirImportExcel,
ResultDir: consts.SimWarningConfigSubDirResultExcel,
MaxFileSize: consts.SimWarningConfigMaxFileSize,
TemplateHeaders: templateHeaders(itemType),
RequiredHeaders: requiredHeaders(itemType),
},
}
}
func (p *Processor) Config() batchimport.ProcessorConfig {
return p.config
}
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
return ParseExcelFileRows(ctx, filePath, p.itemType)
}
func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
if p.itemType == warnItemTypeFlowPool {
return p.validateFlowPoolRows(rows)
}
return p.validateCardRows(rows)
}
func (p *Processor) validateCardRows(rows []batchimport.Row) error {
firstByIccid := make(map[string]*Row, len(rows))
for _, item := range rows {
if item == nil {
continue
}
row, ok := item.(*Row)
if !ok || row == nil {
item.AddErrorReason("预警配置行类型错误")
continue
}
row.ItemType = warnItemTypeCard
row.Iccid = strings.TrimSpace(row.Iccid)
row.Msisdn = strings.TrimSpace(row.Msisdn)
row.Imsi = strings.TrimSpace(row.Imsi)
if row.Iccid == "" {
row.AddErrorReason("ICCID不能为空")
continue
}
if first, exists := firstByIccid[row.Iccid]; exists {
row.AddErrorReason("ICCID重复")
first.AddErrorReason("ICCID重复")
continue
}
firstByIccid[row.Iccid] = row
if row.ConfigSet && row.Reason == "" {
if err := validateOptions(&row.Config); err != nil {
row.AddErrorReason(err.Error())
}
}
}
return nil
}
func (p *Processor) validateFlowPoolRows(rows []batchimport.Row) error {
firstByCode := make(map[string]*Row, len(rows))
for _, item := range rows {
if item == nil {
continue
}
row, ok := item.(*Row)
if !ok || row == nil {
item.AddErrorReason("预警配置行类型错误")
continue
}
row.ItemType = warnItemTypeFlowPool
row.FlowPoolCode = strings.TrimSpace(row.FlowPoolCode)
if row.FlowPoolCode == "" {
row.AddErrorReason("流量池编号不能为空")
continue
}
if first, exists := firstByCode[row.FlowPoolCode]; exists {
row.AddErrorReason("流量池编号重复")
first.AddErrorReason("流量池编号重复")
continue
}
firstByCode[row.FlowPoolCode] = row
if row.ConfigSet && row.Reason == "" {
if err := validateOptions(&row.Config); err != nil {
row.AddErrorReason(err.Error())
}
}
}
return nil
}
func (p *Processor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
options, _ := batchInfoOptions(req.BizParams)
scene := submitScene(req.BizParams)
info := g.Map{
"operation": "sim_warning_config",
"configName": warningConfigName(options.ItemType, options.WarnUnit),
"submitScene": scene,
"itemType": options.ItemType,
"warnUnit": options.WarnUnit,
"firstWarn": options.FirstWarn,
"secondWarn": options.SecondWarn,
"thirdWarn": options.ThirdWarn,
"warnMethods": options.WarnMethods,
"warnPhones": options.WarnPhones,
"warnEmails": options.WarnEmails,
"autoAction": options.AutoAction,
"isEnabled": options.IsEnabled,
"remark": options.Remark,
"rowConfig": scene == "excel" || scene == "input",
"resultFile": g.Map{"status": "pending"},
}
if result != nil && result.ImportFileOssId > 0 {
info["importFileOssId"] = result.ImportFileOssId
} else if importOss != nil && importOss.OssId > 0 {
info["importFileOssId"] = importOss.OssId
}
if resultOss != nil && resultOss.OssId > 0 {
info["resultFileOssId"] = resultOss.OssId
}
if downloadId > 0 {
info["resultDownloadId"] = downloadId
}
return gjson.New(info)
}
func OptionsFromParams(params any) (Options, error) {
switch in := params.(type) {
case *model.TSimWarningConfigExcelSubmitParam:
if in == nil {
return Options{}, gerror.New("预警配置参数不能为空")
}
return NormalizeContextOptions(in.ItemType, in.WarnUnit)
case *model.TSimWarningConfigInputSubmitParam:
if in == nil {
return Options{}, gerror.New("预警配置参数不能为空")
}
return NormalizeOptions(&in.TSimWarningConfigApplyOptions)
case *model.TSimWarningConfigRangeSubmitParam:
if in == nil {
return Options{}, gerror.New("预警配置参数不能为空")
}
return NormalizeOptions(&in.TSimWarningConfigApplyOptions)
default:
return Options{}, gerror.New("预警配置参数不能为空")
}
}
func batchInfoOptions(params any) (Options, error) {
if in, ok := params.(*model.TSimWarningConfigInputSubmitParam); ok && in != nil {
contextOptions, err := NormalizeContextOptions(in.ItemType, in.WarnUnit)
if err != nil {
return Options{}, err
}
if in.FirstWarn <= 0 {
return contextOptions, nil
}
}
return OptionsFromParams(params)
}
func ValidateSubmitOptions(params any) error {
_, err := OptionsFromParams(params)
return err
}
func NormalizeOptions(in *model.TSimWarningConfigApplyOptions) (Options, error) {
if in == nil {
return Options{}, gerror.New("预警配置参数不能为空")
}
itemType := normalizeItemType(in.ItemType)
options := Options{
ItemType: itemType,
WarnType: deriveWarnType(itemType, in.WarnUnit),
WarnUnit: in.WarnUnit,
FirstWarn: in.FirstWarn,
SecondWarn: in.SecondWarn,
ThirdWarn: in.ThirdWarn,
WarnMethods: normalizeWarnMethods(in.WarnMethods),
WarnPhones: strings.TrimSpace(in.WarnPhones),
WarnEmails: strings.TrimSpace(in.WarnEmails),
WarnEmailCode: strings.TrimSpace(in.WarnEmailCode),
AutoAction: in.AutoAction,
IsEnabled: 1,
Remark: strings.TrimSpace(in.Remark),
}
if in.IsEnabled != nil {
options.IsEnabled = *in.IsEnabled
}
if err := validateOptions(&options); err != nil {
return Options{}, err
}
return options, nil
}
func validateOptions(options *Options) error {
if options == nil {
return gerror.New("预警配置参数不能为空")
}
if options.ItemType != warnItemTypeCard && options.ItemType != warnItemTypeFlowPool {
return gerror.New("预警对象类型无效")
}
if options.WarnUnit == warnUnitDay || options.WarnUnit == warnUnitMonth {
return gerror.New("日期预警由系统默认规则处理,不支持手动配置")
}
if options.IsEnabled != 0 && options.IsEnabled != 1 {
return gerror.New("启停状态无效")
}
if options.AutoAction != 0 && options.AutoAction != warnAutoActionDataCut {
return gerror.New("第三次是否断网配置无效")
}
if err := validateNotificationOptions(options); err != nil {
return err
}
if err := validateWarnTypeUnit(options.ItemType, options.WarnType, options.WarnUnit); err != nil {
return err
}
if options.ItemType == warnItemTypeFlowPool && options.AutoAction != 0 {
return gerror.New("流量池预警不支持第三次断网")
}
if options.AutoAction == warnAutoActionDataCut && options.ThirdWarn <= 0 {
return gerror.New("开启第三次断网时必须配置第三次提醒")
}
switch options.WarnType {
case warnTypeCardTraffic:
if options.WarnUnit == warnUnitPercent {
options.FirstWarn = normalizePercentThreshold(options.FirstWarn)
options.SecondWarn = normalizePercentThreshold(options.SecondWarn)
options.ThirdWarn = normalizePercentThreshold(options.ThirdWarn)
if options.FirstWarn > 100 || options.SecondWarn > 100 || options.ThirdWarn > 100 {
return gerror.New("百分比阈值不能大于100")
}
}
return validateTrafficThresholds(options.FirstWarn, options.SecondWarn, options.ThirdWarn)
case warnTypeFlowPoolPercent:
options.FirstWarn = normalizePercentThreshold(options.FirstWarn)
options.SecondWarn = normalizePercentThreshold(options.SecondWarn)
options.ThirdWarn = normalizePercentThreshold(options.ThirdWarn)
if options.FirstWarn > 100 || options.SecondWarn > 100 || options.ThirdWarn > 100 {
return gerror.New("百分比阈值不能大于100")
}
return validateTrafficThresholds(options.FirstWarn, options.SecondWarn, options.ThirdWarn)
default:
return gerror.New("预警类型无效")
}
}
func validateTrafficThresholds(first, second, third float64) error {
if first <= 0 {
return gerror.New("第一档阈值必须大于0")
}
if second < 0 || third < 0 {
return gerror.New("预警阈值不能小于0")
}
if second > 0 && second <= first {
return gerror.New("流量预警第二档阈值必须大于第一档")
}
if third > 0 {
if second <= 0 {
return gerror.New("配置第三档时必须先配置第二档")
}
if third <= second {
return gerror.New("流量预警第三档阈值必须大于第二档")
}
}
return nil
}
func normalizePercentThreshold(value float64) float64 {
if value > 0 && value <= 1 {
return value * 100
}
return value
}
func normalizeWarnMethods(methods string) string {
selected := map[string]struct{}{
warnMethodPlatform: {},
}
for _, method := range splitWarnList(methods) {
switch strings.ToLower(method) {
case warnMethodPlatform, "平台消息", "站内提醒", "站内消息":
selected[warnMethodPlatform] = struct{}{}
case warnMethodEmail, "邮箱":
selected[warnMethodEmail] = struct{}{}
case warnMethodSMS, "短信":
selected[warnMethodSMS] = struct{}{}
case warnMethodPhone, "电话":
selected[warnMethodPhone] = struct{}{}
}
}
ordered := []string{
warnMethodPlatform,
warnMethodEmail,
warnMethodSMS,
warnMethodPhone,
}
result := make([]string, 0, len(ordered))
for _, method := range ordered {
if _, ok := selected[method]; ok {
result = append(result, method)
}
}
return strings.Join(result, ",")
}
func submitScene(params any) string {
switch params.(type) {
case *model.TSimWarningConfigExcelSubmitParam:
return "excel"
case *model.TSimWarningConfigInputSubmitParam:
return "input"
case *model.TSimWarningConfigRangeSubmitParam:
return "range"
default:
return ""
}
}
func warningConfigName(itemType, warnUnit int) string {
warnType := deriveWarnType(itemType, warnUnit)
switch warnType {
case warnTypeFlowPoolPercent:
return "流量池预警配置"
case warnTypeCardTraffic:
return "流量预警配置"
case warnTypeExpireDay:
return "日期预警(天)配置"
case warnTypeExpireMonth:
return "日期预警(月)配置"
default:
return "SIM卡预警配置"
}
}
func normalizeItemType(itemType int) int {
if itemType == 0 {
return warnItemTypeCard
}
return itemType
}
func deriveWarnType(itemType, warnUnit int) int {
if itemType == warnItemTypeFlowPool {
return warnTypeFlowPoolPercent
}
switch warnUnit {
case warnUnitMb, warnUnitPercent:
return warnTypeCardTraffic
case warnUnitDay:
return warnTypeExpireDay
case warnUnitMonth:
return warnTypeExpireMonth
default:
return 0
}
}
func validateWarnTypeUnit(itemType, warnType, warnUnit int) error {
switch warnType {
case warnTypeCardTraffic:
if itemType != warnItemTypeCard {
return gerror.New("单卡流量预警只能配置单卡对象")
}
if warnUnit != warnUnitMb && warnUnit != warnUnitPercent {
return gerror.New("单卡流量预警仅支持MB或百分比单位")
}
case warnTypeExpireDay:
return gerror.New("日期预警由系统默认规则处理,不支持手动配置")
case warnTypeExpireMonth:
return gerror.New("日期预警由系统默认规则处理,不支持手动配置")
case warnTypeFlowPoolPercent:
if itemType != warnItemTypeFlowPool {
return gerror.New("流量池预警只能配置流量池对象")
}
if warnUnit != warnUnitPercent {
return gerror.New("流量池预警仅支持百分比单位")
}
default:
return gerror.New("预警类型无效")
}
return nil
}
@@ -0,0 +1,32 @@
package warningconfig
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common/cardsource"
)
const maxRangeRows = 100000
func BuildRangeRows(ctx context.Context, startIccid, endIccid, startMsisdn, endMsisdn, startImsi, endImsi string) ([]batchimport.Row, error) {
return cardsource.BuildRangeRows(ctx, cardsource.RangeConfig{
StartIccid: startIccid,
EndIccid: endIccid,
StartMsisdn: startMsisdn,
EndMsisdn: endMsisdn,
StartImsi: startImsi,
EndImsi: endImsi,
MaxRows: maxRangeRows,
EmptyMessage: "连号预警配置数据为空",
OverLimitMessage: "连号预警配置数量不能超过%d条",
CountMismatchMessage: "ICCID号段数量(%d)、MSISDN号段数量(%d)、IMSI号段数量(%d)必须一致",
}, func(rowNo int, iccid, msisdn, imsi string) *Row {
return &Row{
RowNo: rowNo,
Iccid: iccid,
Msisdn: msisdn,
Imsi: imsi,
}
})
}
@@ -0,0 +1,219 @@
package warningconfig
import (
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
"github.com/gogf/gf/v2/errors/gerror"
)
const (
cardHeaderIccid = "ICCID"
cardHeaderMsisdn = "MSISDN"
cardHeaderImsi = "IMSI"
cardHeaderFirstWarn = "第一档阈值(MB)"
cardHeaderSecondWarn = "第二档阈值(MB)"
cardHeaderThirdWarn = "第三档阈值(MB)"
cardHeaderAutoAction = "第三次是否断网"
poolHeaderCode = "流量池编号"
poolHeaderFirstWarn = "第一档阈值(%)"
poolHeaderSecondWarn = "第二档阈值(%)"
poolHeaderThirdWarn = "第三档阈值(%)"
headerWarnMethods = "提醒方式"
headerWarnPhones = "短信号码"
headerWarnEmails = "邮箱地址"
headerIsEnabled = "是否启用"
headerRemark = "备注"
)
func templateHeaders(itemType int) []string {
if itemType == warnItemTypeFlowPool {
return []string{
poolHeaderCode,
poolHeaderFirstWarn,
poolHeaderSecondWarn,
poolHeaderThirdWarn,
headerWarnMethods,
headerWarnPhones,
headerWarnEmails,
headerIsEnabled,
headerRemark,
}
}
return []string{
cardHeaderIccid,
cardHeaderMsisdn,
cardHeaderImsi,
cardHeaderFirstWarn,
cardHeaderSecondWarn,
cardHeaderThirdWarn,
cardHeaderAutoAction,
headerWarnMethods,
headerWarnPhones,
headerWarnEmails,
headerIsEnabled,
headerRemark,
}
}
func requiredHeaders(itemType int) []string {
return templateHeaders(itemType)
}
func NormalizeContextOptions(itemType, warnUnit int) (Options, error) {
itemType = normalizeItemType(itemType)
options := Options{
ItemType: itemType,
WarnType: deriveWarnType(itemType, warnUnit),
WarnUnit: warnUnit,
WarnMethods: warnMethodPlatform,
IsEnabled: 1,
}
if err := validateWarnTypeUnit(itemType, options.WarnType, warnUnit); err != nil {
return Options{}, err
}
return options, nil
}
func parseExcelRowOptions(parsed excel.ParsedRow, itemType, warnUnit int) (Options, error) {
if itemType == warnItemTypeFlowPool {
return parseRowOptions(itemType, warnUnit, []string{
excel.Field(parsed, poolHeaderFirstWarn),
excel.Field(parsed, poolHeaderSecondWarn),
excel.Field(parsed, poolHeaderThirdWarn),
excel.Field(parsed, headerWarnMethods),
excel.Field(parsed, headerWarnPhones),
excel.Field(parsed, headerWarnEmails),
excel.Field(parsed, headerIsEnabled),
excel.Field(parsed, headerRemark),
})
}
return parseRowOptions(itemType, warnUnit, []string{
excel.Field(parsed, cardHeaderFirstWarn),
excel.Field(parsed, cardHeaderSecondWarn),
excel.Field(parsed, cardHeaderThirdWarn),
excel.Field(parsed, cardHeaderAutoAction),
excel.Field(parsed, headerWarnMethods),
excel.Field(parsed, headerWarnPhones),
excel.Field(parsed, headerWarnEmails),
excel.Field(parsed, headerIsEnabled),
excel.Field(parsed, headerRemark),
})
}
func parseRowOptions(itemType, warnUnit int, fields []string) (Options, error) {
expected := 8
if itemType == warnItemTypeCard {
expected = 9
}
if len(fields) != expected {
return Options{}, gerror.New("预警配置字段数量不正确")
}
first, err := parseRequiredAmount(fields[0], "第一档阈值")
if err != nil {
return Options{}, err
}
second, err := parseOptionalAmount(fields[1], "第二档阈值")
if err != nil {
return Options{}, err
}
third, err := parseOptionalAmount(fields[2], "第三档阈值")
if err != nil {
return Options{}, err
}
offset := 3
autoAction := 0
if itemType == warnItemTypeCard {
if strings.TrimSpace(fields[offset]) != "" {
enabled, parseErr := excel.ParseBool(fields[offset])
if parseErr != nil {
return Options{}, gerror.New("第三次是否断网必须填写是或否")
}
if enabled {
autoAction = warnAutoActionDataCut
}
}
offset++
}
methods, err := parseWarnMethods(fields[offset])
if err != nil {
return Options{}, err
}
phones := strings.TrimSpace(fields[offset+1])
emails := strings.TrimSpace(fields[offset+2])
isEnabled := 1
if strings.TrimSpace(fields[offset+3]) != "" {
enabled, parseErr := excel.ParseBool(fields[offset+3])
if parseErr != nil {
return Options{}, gerror.New("是否启用必须填写是或否")
}
if !enabled {
isEnabled = 0
}
}
options := Options{
ItemType: itemType,
WarnType: deriveWarnType(itemType, warnUnit),
WarnUnit: warnUnit,
FirstWarn: first,
SecondWarn: second,
ThirdWarn: third,
WarnMethods: methods,
WarnPhones: phones,
WarnEmails: emails,
AutoAction: autoAction,
IsEnabled: isEnabled,
Remark: strings.TrimSpace(fields[offset+4]),
}
if err = validateOptions(&options); err != nil {
return Options{}, err
}
return options, nil
}
func parseRequiredAmount(value, name string) (float64, error) {
if strings.TrimSpace(value) == "" {
return 0, gerror.Newf("%s不能为空", name)
}
parsed, err := excel.ParseAmount(value)
if err != nil {
return 0, gerror.Newf("%s必须是非负数字", name)
}
return parsed, nil
}
func parseOptionalAmount(value, name string) (float64, error) {
if strings.TrimSpace(value) == "" {
return 0, nil
}
parsed, err := excel.ParseAmount(value)
if err != nil {
return 0, gerror.Newf("%s必须是非负数字", name)
}
return parsed, nil
}
func parseWarnMethods(value string) (string, error) {
if strings.TrimSpace(value) == "" {
return "", gerror.New("提醒方式不能为空,至少填写站内提醒")
}
for _, method := range splitWarnList(value) {
switch strings.ToLower(method) {
case "0", "短信", "1", "邮箱", "2", "电话", "3", "平台消息", "站内提醒", "站内消息":
default:
return "", gerror.Newf("提醒方式无效:%s", method)
}
}
return normalizeWarnMethods(value), nil
}
func parseInputConfig(itemType, warnUnit int, fields []string) (Options, error) {
return parseRowOptions(itemType, warnUnit, fields)
}
@@ -0,0 +1,221 @@
package warningconfig
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/ghttp"
)
func CreatePendingExcel(ctx context.Context, file *ghttp.UploadFile, in *model.TSimWarningConfigExcelSubmitParam) (*PendingResult, error) {
if in == nil {
in = &model.TSimWarningConfigExcelSubmitParam{}
}
options, err := NormalizeContextOptions(in.ItemType, in.WarnUnit)
if err != nil {
return nil, err
}
processor := NewProcessor(options.ItemType)
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
File: file,
BizParams: in,
SourceType: consts.BatchSourceTypeFile,
Operator: common.CurrentOperator(ctx),
}
result, err := batchimport.CreatePendingUpload(ctx, req, processor, prepareOptions(options))
if err != nil {
return nil, err
}
common.PublishBatchMessage(ctx, result, consts.QueueBatchExecute, "sim_warning_config")
return pendingResultFromBatch(result), nil
}
func CreatePendingInput(ctx context.Context, in *model.TSimWarningConfigInputSubmitParam) (*PendingResult, error) {
if in == nil {
in = &model.TSimWarningConfigInputSubmitParam{}
}
contextOptions, err := NormalizeContextOptions(in.ItemType, in.WarnUnit)
if err != nil {
return nil, err
}
rows, err := ParseInputRows(ctx, in.Input, contextOptions.ItemType, contextOptions.WarnUnit)
if err != nil {
return nil, err
}
options := contextOptions
if rowsNeedSharedOptions(rows) {
options, err = OptionsFromParams(in)
if err != nil {
return nil, err
}
}
processor := NewProcessor(options.ItemType)
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
BizParams: in,
SourceType: consts.BatchSourceTypeBatchInput,
Operator: common.CurrentOperator(ctx),
}
result, err := batchimport.CreatePendingRows(ctx, req, processor, rows, prepareOptions(options))
if err != nil {
return nil, err
}
common.PublishBatchMessage(ctx, result, consts.QueueBatchExecute, "sim_warning_config")
return pendingResultFromBatch(result), nil
}
func CreatePendingRange(ctx context.Context, in *model.TSimWarningConfigRangeSubmitParam) (*PendingResult, error) {
if in == nil {
in = &model.TSimWarningConfigRangeSubmitParam{}
}
options, err := OptionsFromParams(in)
if err != nil {
return nil, err
}
if options.ItemType == warnItemTypeFlowPool {
return nil, gerror.New("流量池预警配置不支持连号")
}
rows, err := BuildRangeRows(ctx, in.StartIccid, in.EndIccid, in.StartMsisdn, in.EndMsisdn, in.StartImsi, in.EndImsi)
if err != nil {
return nil, err
}
processor := NewProcessor(options.ItemType)
req := batchimport.SubmitRequest{
SourceChannel: batchmetadata.Console,
ExecutionMode: batchmetadata.Bulk,
BizParams: in,
SourceType: consts.BatchSourceTypeRange,
Operator: common.CurrentOperator(ctx),
}
result, err := batchimport.CreatePendingRows(ctx, req, processor, rows, prepareOptions(options))
if err != nil {
return nil, err
}
common.PublishBatchMessage(ctx, result, consts.QueueBatchExecute, "sim_warning_config")
return pendingResultFromBatch(result), nil
}
func prepareOptions(options Options) batchimport.PendingOptions {
return batchimport.PendingOptions{
PrepareRows: func(ctx context.Context, rows []batchimport.Row) error {
if options.ItemType == warnItemTypeFlowPool {
return prepareAuthorizedFlowPoolRows(ctx, rows, options)
}
if err := common.PrepareAuthorizedRows(ctx, rows, func(item batchimport.Row) (*Row, error) {
row, ok := item.(*Row)
if !ok || row == nil {
return nil, gerror.New("预警配置行类型错误")
}
return row, nil
}, "预警配置行类型错误"); err != nil {
return err
}
applyConfigSnapshot(rows, options)
return nil
},
}
}
func prepareAuthorizedFlowPoolRows(ctx context.Context, rows []batchimport.Row, options Options) error {
flowPoolRows := make([]*Row, 0, len(rows))
codes := make([]string, 0, len(rows))
for _, item := range rows {
row, ok := item.(*Row)
if !ok || row == nil {
return gerror.New("预警配置行类型错误")
}
flowPoolRows = append(flowPoolRows, row)
if code := strings.TrimSpace(row.FlowPoolCode); code != "" {
codes = append(codes, code)
}
}
if len(codes) == 0 {
applyConfigSnapshot(rows, options)
return nil
}
var visible []struct {
FlowPoolCode string `orm:"flow_pool_code"`
}
columns := dao.TSimFlowPool.Columns()
err := handler.Model(dao.TSimFlowPool.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: true}).
Fields(columns.FlowPoolCode, columns.TenantId, columns.DeptId, columns.CreatedDept, columns.CreatedBy).
WhereIn(columns.FlowPoolCode, codes).
Scan(&visible)
if err != nil {
return gerror.Wrap(err, "计算流量池数据权限失败")
}
authorized := make(map[string]struct{}, len(visible))
for _, pool := range visible {
authorized[strings.TrimSpace(pool.FlowPoolCode)] = struct{}{}
}
for _, row := range flowPoolRows {
_, ok := authorized[strings.TrimSpace(row.FlowPoolCode)]
row.SetAuthorized(ok)
}
applyConfigSnapshot(rows, options)
return nil
}
func applyConfigSnapshot(rows []batchimport.Row, options Options) {
for _, item := range rows {
row, ok := item.(*Row)
if !ok || row == nil {
continue
}
if row.ConfigSet {
continue
}
row.Config = options
row.ConfigSet = true
if row.ItemType == 0 {
row.ItemType = options.ItemType
}
}
}
func rowsNeedSharedOptions(rows []batchimport.Row) bool {
for _, item := range rows {
row, ok := item.(*Row)
if ok && row != nil && !row.ConfigSet && row.Reason == "" {
return true
}
}
return false
}
func pendingResultFromBatch(result *batchimport.Result) *PendingResult {
res := &PendingResult{
BatchType: consts.BatchTypeSimWarningConfig,
BatchAsyncStatus: consts.BatchAsyncStatusPending,
}
if result == nil {
return res
}
valid := result.ValidRows
if result.TotalRows > 0 && valid == 0 {
valid = result.TotalRows - result.ErrorRows
if valid < 0 {
valid = 0
}
}
res.BatchNo = result.BatchNo
res.BatchType = result.BatchType
res.Total = result.TotalRows
res.ValidCount = valid
res.FailCount = result.ErrorRows
return res
}
@@ -0,0 +1,117 @@
package warningconfig
import (
"strings"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
)
type Row struct {
RowNo int
ItemType int
Iccid string
Msisdn string
Imsi string
FlowPoolCode string
Authorized bool
Reason string
Config Options
ConfigSet bool
}
func (r *Row) GetRowNo() int {
if r == nil {
return 0
}
return r.RowNo
}
func (r *Row) GetItemID() string {
if r == nil {
return ""
}
if r.ItemType == warnItemTypeFlowPool {
return r.FlowPoolCode
}
return r.Iccid
}
func (r *Row) GetErrorReasons() []string {
if r == nil || r.Reason == "" {
return nil
}
return []string{r.Reason}
}
func (r *Row) AddErrorReason(reason string) {
if r == nil || r.Reason != "" || strings.TrimSpace(reason) == "" {
return
}
r.Reason = strings.TrimSpace(reason)
}
func (r *Row) GetIccid() string {
if r == nil {
return ""
}
return r.Iccid
}
func (r *Row) SetAuthorized(v bool) {
if r == nil {
return
}
r.Authorized = v
}
func (r *Row) Snapshot() *gjson.Json {
if r == nil {
return gjson.New(g.Map{})
}
return gjson.New(g.Map{
"itemType": r.ItemType,
"ICCID": r.Iccid,
"MSISDN": r.Msisdn,
"IMSI": r.Imsi,
"flowPoolCode": r.FlowPoolCode,
"authorized": r.Authorized,
"configItemType": r.Config.ItemType,
"configSet": r.ConfigSet,
"warnUnit": r.Config.WarnUnit,
"firstWarn": r.Config.FirstWarn,
"secondWarn": r.Config.SecondWarn,
"thirdWarn": r.Config.ThirdWarn,
"warnMethods": r.Config.WarnMethods,
"warnPhones": r.Config.WarnPhones,
"warnEmails": r.Config.WarnEmails,
"autoAction": r.Config.AutoAction,
"isEnabled": r.Config.IsEnabled,
"remark": r.Config.Remark,
})
}
type Options struct {
ItemType int
WarnType int
WarnUnit int
FirstWarn float64
SecondWarn float64
ThirdWarn float64
WarnMethods string
WarnPhones string
WarnEmails string
WarnEmailCode string
AutoAction int
IsEnabled int
Remark string
}
type PendingResult struct {
BatchNo string
BatchType int
BatchAsyncStatus int
Total int
ValidCount int
FailCount int
}