chore: 初始开源发布

iotlcm-admin-svr:管理后台服务。

- Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件)
- 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、
  MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位;
  私网 IP → 文档地址段;库名 xiujie_admin → iotlcm
- 真实手机号/邮箱 → 合法 11 位编造号 / example.com
- 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、
  私有依赖 go get 行、明文 registry 登录口令
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- Dockerfile 改用公共基础镜像
- 拼错的 RADEME.md → README.md,README 重写
- 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes

数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
This commit is contained in:
MiFaZhan
2026-10-23 14:46:05 +08:00
commit 59eb34976a
1132 changed files with 138197 additions and 0 deletions
@@ -0,0 +1,212 @@
package simwarning
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"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/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
const (
warnActionDataCut = "data_cut"
warnDataServiceStatusEnabled = 1
warnDataServiceStatusDisabled = 0
)
func (s *sSimWarning) handleAutoAction(ctx context.Context, hit warningHit, warnLogID int64, runTime time.Time) (bool, error) {
if !autoDataCutEnabled(hit) {
return false, nil
}
allowed, reason := autoDataCutEligibility(hit)
if !allowed {
g.Log().Infof(ctx, "SimWarning auto data cut skipped, reason:%s, configId:%d, cardId:%d, warnLogId:%d", reason, hit.Config.Id, hit.Card.Id, warnLogID)
return false, nil
}
acquired, actionKey, err := acquireWarningActionDedupe(ctx, hit, warnActionDataCut, runTime)
if err != nil {
return false, err
}
if !acquired {
g.Log().Infof(ctx, "SimWarning auto data cut duplicated, action:%s, configId:%d, cardId:%d, warnLogId:%d", warnActionDataCut, hit.Config.Id, hit.Card.Id, warnLogID)
return false, nil
}
eligible, reason, err := s.ensureDataCutEligible(ctx, hit)
if err != nil {
releaseWarningDedupe(ctx, actionKey)
return false, err
}
if !eligible {
releaseWarningDedupe(ctx, actionKey)
g.Log().Infof(ctx, "SimWarning auto data cut skipped, reason:%s, configId:%d, cardId:%d, warnLogId:%d", reason, hit.Config.Id, hit.Card.Id, warnLogID)
return false, nil
}
orderNum, err := s.disableCardDataService(ctx, hit)
if err != nil {
releaseWarningDedupe(ctx, actionKey)
return false, err
}
g.Log().Infof(ctx, "SimWarning auto data cut success, action:%s, configId:%d, cardId:%d, warnLogId:%d, orderNum:%s", warnActionDataCut, hit.Config.Id, hit.Card.Id, warnLogID, orderNum)
return true, nil
}
func autoDataCutEnabled(hit warningHit) bool {
return hit.Config.ItemType == warnItemTypeCard &&
(isExpireKind(hit.Kind) || isTrafficKind(hit.Kind)) &&
hit.WarnStage == warnStageThird &&
hit.Config.AutoAction == warnAutoActionDataCut
}
func autoDataCutEligibility(hit warningHit) (bool, string) {
if hit.Card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped {
return false, "\u5361\u5df2\u505c\u673a\uff0c\u81ea\u52a8\u65ad\u7f51\u672a\u6267\u884c"
}
if hit.Card.BossCardStatus != consts.V3ApiSimBossCardStatusActivated {
return false, "\u5f53\u524d\u8fd0\u8425\u5546\u72b6\u6001\u4e3a" + bossCardStatusText(hit.Card.BossCardStatus) + "\uff0c\u4ec5\u5df2\u6fc0\u6d3b\u5361\u53ef\u4ee5\u81ea\u52a8\u65ad\u7f51"
}
if hit.Card.DataServiceStatus == warnDataServiceStatusDisabled {
return false, "\u6570\u636e\u670d\u52a1\u5df2\u505c\u7528\uff0c\u81ea\u52a8\u65ad\u7f51\u672a\u6267\u884c"
}
if hit.Card.DataServiceStatus != warnDataServiceStatusEnabled {
return false, "\u6570\u636e\u670d\u52a1\u72b6\u6001\u65e0\u6548\uff0c\u81ea\u52a8\u65ad\u7f51\u672a\u6267\u884c"
}
if strings.TrimSpace(hit.Card.ChanKey) == "" {
return false, "\u5361\u7f3a\u5c11\u901a\u9053\u6807\u8bc6\uff0c\u81ea\u52a8\u65ad\u7f51\u672a\u6267\u884c"
}
if strings.TrimSpace(hit.Card.Iccid) == "" &&
strings.TrimSpace(hit.Card.Msisdn) == "" &&
strings.TrimSpace(hit.Card.Imsi) == "" {
return false, "\u5361\u7f3a\u5c11 ICCID/MSISDN/IMSI\uff0c\u81ea\u52a8\u65ad\u7f51\u672a\u6267\u884c"
}
return true, "\u5df2\u914d\u7f6e\u7b2c\u4e09\u6863\u65ad\u7f51\uff0c\u7cfb\u7edf\u5c06\u5728\u72b6\u6001\u6821\u9a8c\u901a\u8fc7\u540e\u53d1\u8d77\u6570\u636e\u670d\u52a1\u505c\u7528"
}
func (s *sSimWarning) ensureDataCutEligible(ctx context.Context, hit warningHit) (bool, string, error) {
simKey, err := dataCutSimKey(hit)
if err != nil {
return false, "", err
}
output, err := service.UpstreamChan().QuerySimCardStatus(ctx, &model.SimCardStatusInput{
SimKeys: []*model.SimKey{simKey},
})
if err != nil {
return false, "", err
}
if len(output) == 0 {
return false, "", gerror.New("upstream card status is empty")
}
status := output[0].ResData.CardStatus
if status == consts.V3ApiSimBossCardStatusStopped {
return false, "\u4e0a\u6e38\u5361\u5df2\u505c\u673a\uff0c\u81ea\u52a8\u65ad\u7f51\u672a\u6267\u884c", nil
}
if status != consts.V3ApiSimBossCardStatusActivated {
return false, fmt.Sprintf("\u4e0a\u6e38\u5f53\u524d\u72b6\u6001\u4e3a%s\uff0c\u4ec5\u5df2\u6fc0\u6d3b\u5361\u53ef\u4ee5\u81ea\u52a8\u65ad\u7f51", bossCardStatusText(status)), nil
}
return true, "\u4e0a\u6e38\u72b6\u6001\u5df2\u786e\u8ba4\uff0c\u53ef\u6267\u884c\u6570\u636e\u670d\u52a1\u505c\u7528", nil
}
func bossCardStatusText(status int) string {
if name := consts.V3ApiSimBossCardStatusNameMap[status]; name != "" {
return name
}
return "\u672a\u77e5\u72b6\u6001"
}
func (s *sSimWarning) disableCardDataService(ctx context.Context, hit warningHit) (string, error) {
simKey, err := dataCutSimKey(hit)
if err != nil {
return "", err
}
output, err := service.UpstreamChan().ChangeSimApnFunction(ctx, &model.SimCardOperateChangeApnInput{
SimKeys: []*model.SimKey{simKey},
ReqData: model.SimCardOperateChangeApnInputItem{
OperType: strconv.Itoa(consts.SimCardDataOperTypeStop),
ApnName: consts.SimCardDataApnName,
},
})
if err != nil {
return "", err
}
if len(output) == 0 {
return "", gerror.New("upstream data service stop response is empty")
}
item := output[0]
if item.Code != 0 {
message := strings.TrimSpace(item.Message)
if message == "" {
message = "upstream data service stop failed"
}
return "", gerror.New(message)
}
if err = s.updateLocalDataServiceDisabled(ctx, hit); err != nil {
return item.ResData.OrderNum, err
}
return item.ResData.OrderNum, nil
}
func dataCutSimKey(hit warningHit) (*model.SimKey, error) {
chanKey := strings.TrimSpace(hit.Card.ChanKey)
if chanKey == "" {
return nil, gerror.New("card chan key is empty")
}
if strings.TrimSpace(hit.Card.Iccid) == "" &&
strings.TrimSpace(hit.Card.Msisdn) == "" &&
strings.TrimSpace(hit.Card.Imsi) == "" {
return nil, gerror.New("card identity is empty")
}
return &model.SimKey{
ChanKey: chanKey,
ICCID: strings.TrimSpace(hit.Card.Iccid),
MSISDN: strings.TrimSpace(hit.Card.Msisdn),
IMSI: strings.TrimSpace(hit.Card.Imsi),
}, nil
}
func (s *sSimWarning) updateLocalDataServiceDisabled(ctx context.Context, hit warningHit) error {
columns := dao.TSimCardInfo.Columns()
result, err := dao.TSimCardInfo.Ctx(ctx).
Where(columns.Id, hit.Card.Id).
Where(columns.TenantId, hit.Config.TenantId).
Where(columns.DataServiceStatus, warnDataServiceStatusEnabled).
Data(do.TSimCardInfo{
DataServiceStatus: warnDataServiceStatusDisabled,
UpdatedBy: hit.Config.CreatedBy,
}).
Update()
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 1 {
return nil
}
count, err := dao.TSimCardInfo.Ctx(ctx).
Where(columns.Id, hit.Card.Id).
Where(columns.TenantId, hit.Config.TenantId).
Where(columns.DataServiceStatus, warnDataServiceStatusDisabled).
Count()
if err != nil {
return err
}
if count > 0 {
return nil
}
return gerror.New("local data service status changed before update")
}
+700
View File
@@ -0,0 +1,700 @@
package simwarning
import (
"context"
"database/sql"
"net/mail"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
func (s *sSimWarning) ConfigList(ctx context.Context, in *model.SimWarningConfigListParam) (list []*model.SimWarningConfigModel, totalCount int, err error) {
if in == nil {
in = &model.SimWarningConfigListParam{}
}
if in.Page <= 0 {
in.Page = 1
}
if in.PageSize <= 0 {
in.PageSize = 20
}
mod := s.configModel(ctx)
columns := dao.TWarnConfig.Columns()
if in.Id > 0 {
mod = mod.Where(columns.Id, in.Id)
}
if in.ItemType > 0 {
mod = mod.Where(columns.ItemType, in.ItemType)
}
if in.ItemId > 0 {
mod = mod.Where(columns.ItemId, in.ItemId)
}
if in.WarnUnit > 0 {
if in.WarnUnit == warnUnitDay || in.WarnUnit == warnUnitMonth {
mod = mod.Where("1 = 0")
} else {
mod = mod.Where(columns.WarnUnit, in.WarnUnit)
}
} else if in.WarnType > 0 {
mod = applyWarnTypeCompatibilityFilter(mod, in.WarnType)
}
if in.IsEnabled != nil {
mod = mod.Where(columns.IsEnabled, *in.IsEnabled)
}
if itemCode := strings.TrimSpace(in.ItemCode); itemCode != "" {
mod = mod.WhereLike(columns.ItemCode, "%"+itemCode+"%")
}
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
mod = mod.WhereLike(columns.ItemCode, "%"+keyword+"%")
}
err = mod.Page(in.Page, in.PageSize).
OrderDesc(columns.Id).
ScanAndCount(&list, &totalCount, false)
if err != nil {
return nil, 0, gerror.Wrap(err, "获取预警配置列表失败,请稍后重试")
}
if list == nil {
list = []*model.SimWarningConfigModel{}
}
fillConfigWarnTypes(list)
return list, totalCount, nil
}
func (s *sSimWarning) ConfigView(ctx context.Context, in *model.SimWarningConfigViewParam) (res *model.SimWarningConfigModel, err error) {
if in == nil || (in.Id <= 0 && in.ItemId <= 0) {
return nil, gerror.New("配置ID和对象ID不能同时为空")
}
mod := s.configModel(ctx)
columns := dao.TWarnConfig.Columns()
if in.Id > 0 {
mod = mod.Where(columns.Id, in.Id)
} else {
itemType := in.ItemType
if itemType == 0 {
itemType = warnItemTypeCard
}
if itemType != warnItemTypeCard && itemType != warnItemTypeFlowPool {
return nil, gerror.New("预警对象类型无效")
}
warnUnit, err := warnUnitForConfigLookup(itemType, in.WarnType, in.WarnUnit)
if err != nil {
return nil, err
}
mod = mod.Where(columns.ItemType, itemType).
Where(columns.ItemId, in.ItemId).
Where(columns.WarnUnit, warnUnit)
}
if err = mod.Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取预警配置详情失败,请稍后重试")
}
if res == nil || res.Id == 0 {
return nil, gerror.New("预警配置不存在或无权限访问")
}
if res.WarnUnit == warnUnitDay || res.WarnUnit == warnUnitMonth {
return nil, gerror.New("日期预警由系统默认规则处理,不支持查询用户配置")
}
res.WarnType = deriveWarnType(res.ItemType, res.WarnUnit)
return res, nil
}
func (s *sSimWarning) ConfigSave(ctx context.Context, in *model.SimWarningConfigSaveParam) (res *model.SimWarningConfigSaveModel, err error) {
if in == nil {
return nil, gerror.New("请求参数不能为空")
}
user := contexts.GetUser(ctx)
if user == nil || user.TenantId == "" || user.ID == 0 {
return nil, gerror.New("登录信息已过期")
}
in.ItemCode = strings.TrimSpace(in.ItemCode)
itemType := in.ItemType
if itemType == 0 {
itemType = warnItemTypeCard
}
in.ItemType = itemType
if err = normalizeConfigSaveParam(in); err != nil {
return nil, err
}
var itemCode string
switch in.ItemType {
case warnItemTypeCard:
card, err := s.queryConfigCard(ctx, in.ItemId, in.ItemCode)
if err != nil {
return nil, err
}
in.ItemId = card.Id
itemCode = strings.TrimSpace(in.ItemCode)
if itemCode == "" {
itemCode = strings.TrimSpace(card.Iccid)
}
if itemCode == "" {
itemCode = strings.TrimSpace(card.Msisdn)
}
case warnItemTypeFlowPool:
pool, err := s.queryConfigFlowPool(ctx, in.ItemId, in.ItemCode)
if err != nil {
return nil, err
}
in.ItemId = pool.Id
itemCode = strings.TrimSpace(in.ItemCode)
if itemCode == "" {
itemCode = strings.TrimSpace(pool.FlowPoolCode)
}
default:
return nil, gerror.New("预警对象类型无效")
}
if err = s.ensureConfigWritable(ctx, user.TenantId, in.Id, in.ItemType, in.ItemId, in.WarnUnit); err != nil {
return nil, err
}
now := gtime.Now()
data := buildConfigSaveData(user.TenantId, user.DeptId, user.ID, itemCode, in, now)
columns := dao.TWarnConfig.Columns()
_, err = dao.TWarnConfig.Ctx(ctx).
Data(data).
OnConflict(columns.TenantId, columns.ItemType, columns.ItemId, columns.WarnUnit).
OnDuplicateEx(strings.Join([]string{
columns.Id,
columns.TenantId,
columns.ItemType,
columns.ItemId,
columns.WarnUnit,
columns.CreatedDept,
columns.CreatedBy,
columns.CreatedAt,
}, ",")).
Save()
if err != nil {
return nil, gerror.Wrap(err, "保存预警配置失败,请稍后重试")
}
saved, err := s.findConfigByItem(ctx, user.TenantId, in.ItemType, in.ItemId, in.WarnUnit, true)
if err != nil {
return nil, err
}
if saved == nil || saved.Id == 0 {
return nil, gerror.New("保存预警配置后未查询到配置")
}
return &model.SimWarningConfigSaveModel{Id: saved.Id}, nil
}
func (s *sSimWarning) ConfigStatus(ctx context.Context, in *model.SimWarningConfigStatusParam) (err error) {
if in == nil || in.Id <= 0 {
return gerror.New("配置ID不能为空")
}
if in.IsEnabled != 0 && in.IsEnabled != 1 {
return gerror.New("启停状态无效")
}
if _, err = s.ConfigView(ctx, &model.SimWarningConfigViewParam{Id: in.Id}); err != nil {
return err
}
userId := contexts.GetUserId(ctx)
columns := dao.TWarnConfig.Columns()
result, err := s.configModel(ctx).Where(columns.Id, in.Id).Data(do.TWarnConfig{
IsEnabled: in.IsEnabled,
UpdatedBy: userId,
UpdatedAt: gtime.Now(),
}).Update()
if err != nil {
return gerror.Wrap(err, "启停预警配置失败,请稍后重试")
}
return ensureAffected(result, "预警配置不存在或无权限访问")
}
func (s *sSimWarning) ConfigDelete(ctx context.Context, in *model.SimWarningConfigDeleteParam) (err error) {
if in == nil || in.Id <= 0 {
return gerror.New("配置ID不能为空")
}
if _, err = s.ConfigView(ctx, &model.SimWarningConfigViewParam{Id: in.Id}); err != nil {
return err
}
result, err := s.configModel(ctx).Where(dao.TWarnConfig.Columns().Id, in.Id).Delete()
if err != nil {
return gerror.Wrap(err, "删除预警配置失败,请稍后重试")
}
return ensureAffected(result, "预警配置不存在或无权限访问")
}
func (s *sSimWarning) configModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.TWarnConfig.Ctx(ctx)).
Fields(warningConfigFields()...)
}
func (s *sSimWarning) queryConfigCard(ctx context.Context, itemId int64, itemCode string) (*entity.TSimCardInfo, error) {
itemCode = strings.TrimSpace(itemCode)
if itemId <= 0 && itemCode == "" {
return nil, gerror.New("单卡ID或ICCID/MSISDN/IMSI不能为空")
}
var card *entity.TSimCardInfo
mod := service.IotTSimCardInfo().Model(ctx).Fields(
"id",
"iccid",
"msisdn",
"imsi",
"tenant_id",
)
if itemId > 0 {
mod = mod.Where("id", itemId)
} else {
mod = mod.Where("(iccid = ? OR msisdn = ? OR imsi = ?)", itemCode, itemCode, itemCode)
}
if err := mod.Scan(&card); err != nil {
return nil, gerror.Wrap(err, "查询单卡失败,请稍后重试")
}
if card == nil || card.Id == 0 {
return nil, gerror.New("单卡不存在或无权限访问")
}
return card, nil
}
func (s *sSimWarning) queryConfigFlowPool(ctx context.Context, itemId int64, itemCode string) (*entity.TSimFlowPool, error) {
itemCode = strings.TrimSpace(itemCode)
if itemId <= 0 && itemCode == "" {
return nil, gerror.New("流量池ID或流量池编号不能为空")
}
var pool *entity.TSimFlowPool
mod := service.IotTSimFlowPool().Model(ctx).Fields(
"id",
"flow_pool_code",
"tenant_id",
)
if itemId > 0 {
mod = mod.Where("id", itemId)
} else {
mod = mod.Where("flow_pool_code", itemCode)
}
if err := mod.Scan(&pool); err != nil {
return nil, gerror.Wrap(err, "查询流量池失败,请稍后重试")
}
if pool == nil || pool.Id == 0 {
return nil, gerror.New("流量池不存在或无权限访问")
}
return pool, nil
}
func (s *sSimWarning) ensureConfigWritable(ctx context.Context, tenantId string, id int64, itemType int, itemId int64, warnUnit int) error {
if id > 0 {
existing, err := s.findConfigByID(ctx, tenantId, id, false)
if err != nil {
return err
}
if existing == nil || existing.Id == 0 {
return gerror.New("预警配置不存在")
}
if existing.ItemType != itemType || existing.ItemId != itemId || existing.WarnUnit != warnUnit {
return gerror.New("配置ID与对象或预警维度不匹配")
}
return s.ensureConfigVisible(ctx, existing.Id)
}
existing, err := s.findConfigByItem(ctx, tenantId, itemType, itemId, warnUnit, false)
if err != nil {
return err
}
if existing == nil || existing.Id == 0 {
return nil
}
return s.ensureConfigVisible(ctx, existing.Id)
}
func (s *sSimWarning) ensureConfigVisible(ctx context.Context, id int64) error {
visible, err := s.findConfigByID(ctx, "", id, true)
if err != nil {
return err
}
if visible == nil || visible.Id == 0 {
return gerror.New("预警配置不存在或无权限访问")
}
return nil
}
func (s *sSimWarning) findConfigByID(ctx context.Context, tenantId string, id int64, withAuth bool) (*model.SimWarningConfigModel, error) {
if id <= 0 {
return nil, nil
}
columns := dao.TWarnConfig.Columns()
mod := dao.TWarnConfig.Ctx(ctx).
Fields(warningConfigFields()...).
Where(columns.Id, id)
if tenantId != "" {
mod = mod.Where(columns.TenantId, tenantId)
}
if withAuth {
mod = handler.Model(mod)
}
var res *model.SimWarningConfigModel
if err := mod.Scan(&res); err != nil {
return nil, gerror.Wrap(err, "查询预警配置失败,请稍后重试")
}
if res != nil {
res.WarnType = deriveWarnType(res.ItemType, res.WarnUnit)
}
return res, nil
}
func (s *sSimWarning) findConfigByItem(ctx context.Context, tenantId string, itemType int, itemId int64, warnUnit int, withAuth bool) (*model.SimWarningConfigModel, error) {
if tenantId == "" || itemId <= 0 || warnUnit <= 0 {
return nil, nil
}
if itemType == 0 {
itemType = warnItemTypeCard
}
columns := dao.TWarnConfig.Columns()
mod := dao.TWarnConfig.Ctx(ctx).
Fields(warningConfigFields()...).
Where(columns.TenantId, tenantId).
Where(columns.ItemType, itemType).
Where(columns.ItemId, itemId).
Where(columns.WarnUnit, warnUnit)
if withAuth {
mod = handler.Model(mod)
}
var res *model.SimWarningConfigModel
if err := mod.Scan(&res); err != nil {
return nil, gerror.Wrap(err, "查询预警配置失败,请稍后重试")
}
if res != nil {
res.WarnType = deriveWarnType(res.ItemType, res.WarnUnit)
}
return res, nil
}
func warningConfigFields() []any {
columns := dao.TWarnConfig.Columns()
return []any{
columns.Id,
columns.TenantId,
columns.BatchNo,
columns.ItemType,
columns.ItemId,
columns.ItemCode,
columns.WarnUnit,
columns.FirstWarn,
columns.SecondWarn,
columns.ThirdWarn,
columns.WarnMethods,
columns.WarnPhones,
columns.WarnEmails,
columns.AutoAction,
columns.IsEnabled,
columns.WxTemplateId,
columns.Remark,
columns.CreatedDept,
columns.CreatedBy,
columns.CreatedAt,
columns.UpdatedBy,
columns.UpdatedAt,
columns.AppNotifyEnabled,
}
}
func buildConfigSaveData(tenantId string, deptId int64, userId int64, resolvedItemCode string, in *model.SimWarningConfigSaveParam, now *gtime.Time) do.TWarnConfig {
itemCode := strings.TrimSpace(resolvedItemCode)
if itemCode == "" {
itemCode = strings.TrimSpace(in.ItemCode)
}
isEnabled := 1
if in.IsEnabled != nil {
isEnabled = *in.IsEnabled
}
warnMethods := normalizeConfigWarnMethods(in.WarnMethods)
data := do.TWarnConfig{
TenantId: tenantId,
ItemType: in.ItemType,
ItemId: in.ItemId,
ItemCode: itemCode,
BatchNo: strings.TrimSpace(in.BatchNo),
WarnUnit: in.WarnUnit,
FirstWarn: in.FirstWarn,
SecondWarn: in.SecondWarn,
ThirdWarn: in.ThirdWarn,
WarnMethods: warnMethods,
WarnPhones: strings.TrimSpace(in.WarnPhones),
WarnEmails: strings.TrimSpace(in.WarnEmails),
AutoAction: in.AutoAction,
Remark: strings.TrimSpace(in.Remark),
CreatedDept: deptId,
CreatedBy: userId,
CreatedAt: now,
UpdatedBy: userId,
UpdatedAt: now,
IsEnabled: isEnabled,
}
if in.AppNotifyEnabled != nil {
data.AppNotifyEnabled = *in.AppNotifyEnabled
}
return data
}
func normalizeConfigSaveParam(in *model.SimWarningConfigSaveParam) error {
in.ItemCode = strings.TrimSpace(in.ItemCode)
in.WarnMethods = normalizeConfigWarnMethods(in.WarnMethods)
in.WarnPhones = strings.TrimSpace(in.WarnPhones)
in.WarnEmails = strings.TrimSpace(in.WarnEmails)
in.Remark = strings.TrimSpace(in.Remark)
if in.ItemType == 0 {
in.ItemType = warnItemTypeCard
}
if in.ItemType != warnItemTypeCard && in.ItemType != warnItemTypeFlowPool {
return gerror.New("预警对象类型无效")
}
if in.ItemId <= 0 && in.ItemCode == "" {
if in.ItemType == warnItemTypeFlowPool {
return gerror.New("流量池ID或流量池编号不能为空")
}
return gerror.New("单卡ID或ICCID/MSISDN/IMSI不能为空")
}
if in.WarnUnit == warnUnitDay || in.WarnUnit == warnUnitMonth {
return gerror.New("日期预警由系统默认规则处理,不支持手动配置")
}
if err := normalizeConfigContacts(in); err != nil {
return err
}
if in.IsEnabled != nil && *in.IsEnabled != 0 && *in.IsEnabled != 1 {
return gerror.New("启停状态无效")
}
if in.AppNotifyEnabled != nil && *in.AppNotifyEnabled != 0 && *in.AppNotifyEnabled != 1 {
return gerror.New("小程序订阅通知状态无效")
}
if in.AutoAction != 0 && in.AutoAction != warnAutoActionDataCut {
return gerror.New("第三次是否断网配置无效")
}
if in.ItemType == warnItemTypeFlowPool && in.AutoAction != 0 {
return gerror.New("流量池预警不支持自动断网")
}
in.WarnType = deriveWarnType(in.ItemType, in.WarnUnit)
if err := validateWarnTypeUnit(in.ItemType, in.WarnType, in.WarnUnit); err != nil {
return err
}
if in.ItemType == warnItemTypeCard && in.AutoAction == warnAutoActionDataCut && in.ThirdWarn <= 0 {
return gerror.New("开启第三次断网时必须配置第三次提醒")
}
switch in.WarnType {
case warnTypeCardTraffic, warnTypeFlowPoolPercent:
if in.WarnUnit == warnUnitPercent {
first := normalizePercentThreshold(in.FirstWarn)
second := normalizePercentThreshold(in.SecondWarn)
third := normalizePercentThreshold(in.ThirdWarn)
if first > 100 || second > 100 || third > 100 {
return gerror.New("百分比阈值不能大于100")
}
in.FirstWarn = first
in.SecondWarn = second
in.ThirdWarn = third
}
return validateTrafficThresholds(in.FirstWarn, in.SecondWarn, in.ThirdWarn)
default:
return gerror.New("预警类型无效")
}
}
func normalizeConfigContacts(in *model.SimWarningConfigSaveParam) error {
if hasConfigWarnMethod(in.WarnMethods, "0") {
phones, err := normalizeConfigWarnPhones(in.WarnPhones)
if err != nil {
return err
}
in.WarnPhones = phones
} else {
in.WarnPhones = ""
}
if hasConfigWarnMethod(in.WarnMethods, "1") {
emails, err := normalizeConfigWarnEmails(in.WarnEmails)
if err != nil {
return err
}
in.WarnEmails = emails
} else {
in.WarnEmails = ""
}
return nil
}
func hasConfigWarnMethod(methods string, target string) bool {
for _, method := range splitConfigWarnContactList(methods) {
if method == target {
return true
}
}
return false
}
func normalizeConfigWarnMethods(methods string) string {
selected := map[string]struct{}{
warnMethodPlatform: {},
}
for _, method := range splitConfigWarnContactList(methods) {
switch method {
case warnMethodPlatform, warnMethodEmail, warnMethodSMS, warnMethodPhone:
selected[method] = 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 normalizeConfigWarnEmails(value string) (string, error) {
emails := splitConfigWarnContactList(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 normalizeConfigWarnPhones(value string) (string, error) {
phones := splitConfigWarnContactList(value)
if len(phones) == 0 {
return "", gerror.New("请选择短信提醒时必须填写短信号码")
}
for _, phone := range phones {
if !isValidConfigWarnPhone(phone) {
return "", gerror.Newf("手机号格式不正确:%s", phone)
}
}
return strings.Join(phones, ","), nil
}
func isValidConfigWarnPhone(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 splitConfigWarnContactList(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 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 warnUnitForConfigLookup(itemType, warnType, warnUnit int) (int, error) {
if warnUnit > 0 {
if warnUnit == warnUnitDay || warnUnit == warnUnitMonth {
return 0, gerror.New("日期预警由系统默认规则处理,不支持查询用户配置")
}
if err := validateWarnTypeUnit(itemType, deriveWarnType(itemType, warnUnit), warnUnit); err != nil {
return 0, err
}
return warnUnit, nil
}
switch warnType {
case warnTypeFlowPoolPercent:
return warnUnitPercent, nil
case warnTypeCardTraffic:
return 0, gerror.New("通过对象ID查询单卡流量预警时必须指定warnUnit")
default:
return 0, gerror.New("通过对象ID查询预警配置时必须指定warnUnit")
}
}
func applyWarnTypeCompatibilityFilter(mod *gdb.Model, warnType int) *gdb.Model {
switch warnType {
case warnTypeCardTraffic:
return mod.Where("item_type = ? AND warn_unit IN (?, ?)", warnItemTypeCard, warnUnitMb, warnUnitPercent)
case warnTypeFlowPoolPercent:
return mod.Where("item_type = ? AND warn_unit = ?", warnItemTypeFlowPool, warnUnitPercent)
default:
return mod.Where("1 = 0")
}
}
func fillConfigWarnTypes(list []*model.SimWarningConfigModel) {
for _, item := range list {
if item == nil {
continue
}
item.WarnType = deriveWarnType(item.ItemType, item.WarnUnit)
}
}
func ensureAffected(result sql.Result, message string) error {
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return gerror.New(message)
}
return nil
}
+151
View File
@@ -0,0 +1,151 @@
package simwarning
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/cache"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
)
const (
warnDedupeKeyPrefix = "iotlcm:warning"
warnDedupeMinTTL = 24 * time.Hour
)
func acquireWarningDedupe(ctx context.Context, hit warningHit, runTime time.Time) (bool, string, error) {
key := warningDedupeKey(hit)
if key == "" {
return false, "", gerror.New("warning dedupe key is empty")
}
ok, err := cache.Instance().SetIfNotExist(ctx, key, "1", warningDedupeTTL(hit, runTime))
return ok, key, err
}
func acquireWarningActionDedupe(ctx context.Context, hit warningHit, action string, runTime time.Time) (bool, string, error) {
action = strings.TrimSpace(action)
if action == "" {
return false, "", gerror.New("warning action is empty")
}
key := warningActionDedupeKey(hit, action)
if key == "" {
return false, "", gerror.New("warning action dedupe key is empty")
}
ok, err := cache.Instance().SetIfNotExist(ctx, key, "1", warningDedupeTTL(hit, runTime))
return ok, key, err
}
func releaseWarningDedupe(ctx context.Context, key string) {
key = strings.TrimSpace(key)
if key == "" {
return
}
_, _ = cache.Instance().Remove(ctx, key)
}
func warningDedupeKey(hit warningHit) string {
if hit.Config.TenantId == "" || hit.Config.Id == 0 || hit.Config.ItemId == 0 || hit.WarnStage == 0 {
return ""
}
return fmt.Sprintf(
"%s:hit:%d:%d:%s:%d:%d:%d:%s",
warnDedupeKeyPrefix,
hit.Config.ItemType,
hit.Config.WarnUnit,
hit.Config.TenantId,
hit.Config.Id,
hit.Config.ItemId,
hit.WarnStage,
warningPeriodKey(hit),
)
}
func warningActionDedupeKey(hit warningHit, action string) string {
if hit.Config.TenantId == "" || hit.Config.Id == 0 || hit.Config.ItemId == 0 || hit.WarnStage == 0 {
return ""
}
return fmt.Sprintf(
"%s:act:%s:%d:%d:%s:%d:%d:%d:%s",
warnDedupeKeyPrefix,
action,
hit.Config.ItemType,
hit.Config.WarnUnit,
hit.Config.TenantId,
hit.Config.Id,
hit.Config.ItemId,
hit.WarnStage,
warningPeriodKey(hit),
)
}
func warningDedupeTTL(hit warningHit, runTime time.Time) time.Duration {
if runTime.IsZero() {
runTime = time.Now()
}
expireAt := warningDedupeExpireAt(hit)
if expireAt.IsZero() {
return warnDedupeMinTTL
}
ttl := expireAt.Sub(runTime)
if ttl <= 0 {
return warnDedupeMinTTL
}
return ttl
}
func warningDedupeExpireAt(hit warningHit) time.Time {
if !hit.DedupeExpireAt.IsZero() {
return hit.DedupeExpireAt
}
if !hit.WindowEnd.IsZero() {
return hit.WindowEnd
}
if !hit.PeriodEnd.IsZero() {
return hit.PeriodEnd.AddDate(0, 1, 0)
}
return time.Time{}
}
func warnUnix(value time.Time) int64 {
if value.IsZero() {
return 0
}
return value.Unix()
}
func warningPeriodKey(hit warningHit) string {
begin, end := warningWindow(hit)
return fmt.Sprintf("%d-%d", warnUnix(begin), warnUnix(end))
}
func warningWindow(hit warningHit) (time.Time, time.Time) {
begin := hit.WindowBegin
end := hit.WindowEnd
if begin.IsZero() {
begin = hit.PeriodBegin
}
if end.IsZero() {
end = hit.PeriodEnd
}
return begin, end
}
func isTrafficKind(kind string) bool {
switch kind {
case warnKindTrafficMb, warnKindTrafficPercent, warnKindPoolTrafficPercent:
return true
default:
return false
}
}
func isExpireKind(kind string) bool {
switch kind {
case warnKindExpireDay, warnKindExpireMonth:
return true
default:
return false
}
}
@@ -0,0 +1,622 @@
package simwarning
import (
"context"
"encoding/json"
"fmt"
"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/model/entity"
"net/mail"
"sort"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
const (
expireCandidateKeyPrefix = warnDedupeKeyPrefix + ":sim_card:expire:candidate"
expireCandidateSetPrefix = warnDedupeKeyPrefix + ":sim_card:expire:candidates"
expireCandidateTTLBuffer = 7 * 24 * time.Hour
simTerminalTable = "t_sim_terminal"
terminalTable = "t_terminal"
)
type expireCandidate struct {
TenantID string `json:"tenantId"`
CardID int64 `json:"cardId"`
ICCID string `json:"iccid"`
MSISDN string `json:"msisdn"`
PackageCode string `json:"packageCode"`
ExpireUnix int64 `json:"expireUnix"`
ExpireAt string `json:"expireAt"`
TargetMonth string `json:"targetMonth"`
CreatedDept int64 `json:"createdDept"`
CreatedBy int64 `json:"createdBy"`
LoadedAt string `json:"loadedAt"`
}
type expireCandidateStats struct {
CandidateCount int
CachedCount int
DeletedCount int
scanStats
}
type expireWarningContactRow struct {
UserID int64 `orm:"user_id"`
Email string `orm:"email"`
Phonenumber string `orm:"phonenumber"`
}
func (s *sSimWarning) PreloadExpireCandidates(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
runTime := time.Now()
targetBegin, targetEnd := expirePreloadTargetMonth(runTime)
targetMonth := targetBegin.Format("200601")
stats := expireCandidateStats{}
lastID := int64(0)
g.Log().Infof(ctx, "SimWarning.ExpirePreload begin, runTime:%s, targetMonth:%s", runTime.Format("2006-01-02 15:04:05"), targetMonth)
for {
if err := ctx.Err(); err != nil {
stats.ErrorCount++
g.Log().Warningf(ctx, "SimWarning.ExpirePreload canceled, targetMonth:%s, lastID:%d, err:%+v", targetMonth, lastID, err)
break
}
cards, err := s.queryExpireCandidateCards(ctx, lastID, targetBegin, targetEnd)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.ExpirePreload query candidates failed, targetMonth:%s, lastID:%d, err:%+v", targetMonth, lastID, err)
break
}
if len(cards) == 0 {
break
}
stats.CandidateCount += len(cards)
for _, card := range cards {
if card.Id > lastID {
lastID = card.Id
}
if reason := validExpireCandidateCard(card, runTime); reason != "" {
stats.SkipCount++
g.Log().Debugf(ctx, "SimWarning.ExpirePreload skipped, targetMonth:%s, cardId:%d, iccid:%s, reason:%s", targetMonth, card.Id, card.Iccid, reason)
continue
}
if err := storeExpireCandidate(ctx, card, targetMonth, targetEnd.Add(expireCandidateTTLBuffer), runTime); err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.ExpirePreload store candidate failed, targetMonth:%s, cardId:%d, iccid:%s, err:%+v", targetMonth, card.Id, card.Iccid, err)
continue
}
stats.CachedCount++
}
if len(cards) < scanPageSize {
break
}
}
g.Log().Infof(ctx, "SimWarning.ExpirePreload end, targetMonth:%s, candidates:%d, cached:%d, skipped:%d, errors:%d", targetMonth, stats.CandidateCount, stats.CachedCount, stats.SkipCount, stats.ErrorCount)
}
func (s *sSimWarning) ScanExpireCandidates(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
runTime := time.Now()
stats := expireCandidateStats{}
pendingWarnings := make([]pendingWarning, 0)
g.Log().Infof(ctx, "SimWarning.ExpireCandidateScan begin, runTime:%s, mode:warn_log_only", runTime.Format("2006-01-02 15:04:05"))
for _, targetMonth := range expireCandidateScanMonths(runTime) {
setKey := expireCandidateSetKey(targetMonth)
members, err := g.Redis().SMembers(ctx, setKey)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.ExpireCandidateScan read redis set failed, setKey:%s, err:%+v", setKey, err)
continue
}
keys := members.Strings()
stats.CandidateCount += len(keys)
for _, key := range keys {
if err := ctx.Err(); err != nil {
stats.ErrorCount++
g.Log().Warningf(ctx, "SimWarning.ExpireCandidateScan canceled, setKey:%s, err:%+v", setKey, err)
break
}
pending := s.scanExpireCandidateKey(ctx, setKey, key, runTime, &stats)
if pending != nil {
pendingWarnings = append(pendingWarnings, *pending)
}
}
}
storedWarnings := s.storePendingWarnings(ctx, pendingWarnings, runTime, &stats.scanStats)
g.Log().Infof(
ctx,
"SimWarning.ExpireCandidateScan end, candidates:%d, hits:%d, duplicated:%d, deleted:%d, skipped:%d, errors:%d, mode:warn_log_only",
stats.CandidateCount,
len(storedWarnings),
stats.DupeCount,
stats.DeletedCount,
stats.SkipCount,
stats.ErrorCount,
)
}
func (s *sSimWarning) scanExpireCandidateKey(ctx context.Context, setKey string, key string, runTime time.Time, stats *expireCandidateStats) *pendingWarning {
key = strings.TrimSpace(key)
if key == "" {
stats.SkipCount++
return nil
}
candidate, ok, err := readExpireCandidate(ctx, key)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.ExpireCandidateScan read candidate failed, key:%s, err:%+v", key, err)
return nil
}
if !ok {
stats.DeletedCount++
removeExpireCandidate(ctx, setKey, key)
return nil
}
card, err := s.queryExpireCandidateCard(ctx, candidate.TenantID, candidate.CardID)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.ExpireCandidateScan query card failed, key:%s, tenantId:%s, cardId:%d, err:%+v", key, candidate.TenantID, candidate.CardID, err)
return nil
}
if reason := removeExpireCandidateReason(candidate, card, runTime); reason != "" {
stats.DeletedCount++
removeExpireCandidate(ctx, setKey, key)
g.Log().Infof(ctx, "SimWarning.ExpireCandidateScan candidate removed, key:%s, reason:%s", key, reason)
return nil
}
warnPhones, warnEmails, err := s.resolveExpireWarningContacts(ctx, *card)
if err != nil {
stats.ErrorCount++
g.Log().Warningf(ctx, "SimWarning.ExpireCandidateScan resolve contacts failed, cardId:%d, iccid:%s, err:%+v", card.Id, card.Iccid, err)
}
cfg := systemExpireWarnConfig(ctx, *card, warnPhones, warnEmails)
hit, reason := evaluateSystemExpireDay(cfg, *card, runTime)
if hit == nil {
stats.SkipCount++
g.Log().Debugf(ctx, "SimWarning.ExpireCandidateScan skipped, key:%s, cardId:%d, reason:%s", key, card.Id, reason)
return nil
}
exists, err := s.existsInPeriod(ctx, *hit)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.ExpireCandidateScan idempotent check failed, cardId:%d, stage:%d, err:%+v", card.Id, hit.WarnStage, err)
return nil
}
if exists {
stats.DupeCount++
g.Log().Infof(ctx, "SimWarning.ExpireCandidateScan duplicated by warn log, cardId:%d, stage:%d", card.Id, hit.WarnStage)
return nil
}
acquired, dedupeKey, err := acquireWarningDedupe(ctx, *hit, runTime)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.ExpireCandidateScan acquire dedupe failed, cardId:%d, stage:%d, err:%+v", card.Id, hit.WarnStage, err)
return nil
}
if !acquired {
stats.DupeCount++
g.Log().Infof(ctx, "SimWarning.ExpireCandidateScan duplicated by redis, cardId:%d, stage:%d", card.Id, hit.WarnStage)
return nil
}
return &pendingWarning{Hit: *hit, DedupeKey: dedupeKey}
}
func (s *sSimWarning) queryExpireCandidateCards(ctx context.Context, lastID int64, targetBegin time.Time, targetEnd time.Time) ([]entity.TSimCardInfo, error) {
columns := dao.TSimCardInfo.Columns()
cards := make([]entity.TSimCardInfo, 0, scanPageSize)
err := dao.TSimCardInfo.Ctx(ctx).
Fields(
columns.Id,
columns.Iccid,
columns.Msisdn,
columns.CurrentPackageCode,
columns.PackageStatus,
columns.IsFlowPool,
columns.FlowPkgBeginAt,
columns.FlowPkgEndAt,
columns.TenantId,
columns.DeptId,
columns.CreatedDept,
columns.CreatedBy,
).
WhereGT(columns.Id, lastID).
Where(columns.PackageStatus, consts.PackageStatusEffective).
Where(columns.IsFlowPool, 0).
Where(columns.CurrentPackageCode+" <> ?", "").
Where(columns.FlowPkgEndAt+" >= ?", targetBegin).
Where(columns.FlowPkgEndAt+" < ?", targetEnd).
OrderAsc(columns.Id).
Limit(scanPageSize).
Scan(&cards)
return cards, err
}
func (s *sSimWarning) queryExpireCandidateCard(ctx context.Context, tenantID string, cardID int64) (*entity.TSimCardInfo, error) {
if strings.TrimSpace(tenantID) == "" || cardID <= 0 {
return nil, nil
}
columns := dao.TSimCardInfo.Columns()
var card *entity.TSimCardInfo
err := dao.TSimCardInfo.Ctx(ctx).
Fields(
columns.Id,
columns.Iccid,
columns.Msisdn,
columns.CurrentPackageCode,
columns.PackageStatus,
columns.IsFlowPool,
columns.FlowPkgBeginAt,
columns.FlowPkgEndAt,
columns.TenantId,
columns.DeptId,
columns.CreatedDept,
columns.CreatedBy,
).
Where(columns.Id, cardID).
Where(columns.TenantId, tenantID).
Scan(&card)
return card, err
}
func validExpireCandidateCard(card entity.TSimCardInfo, runTime time.Time) string {
if reason := validCommonCard(card); reason != "" {
return reason
}
if card.FlowPkgEndAt == nil || card.FlowPkgEndAt.IsZero() {
return "package end time is empty"
}
if !runTime.Before(card.FlowPkgEndAt.Time) {
return "package expired"
}
return ""
}
func removeExpireCandidateReason(candidate expireCandidate, card *entity.TSimCardInfo, runTime time.Time) string {
if strings.TrimSpace(candidate.TenantID) == "" || candidate.CardID <= 0 || candidate.ExpireUnix <= 0 {
return "candidate data invalid"
}
if card == nil || card.Id == 0 {
return "card not found"
}
if reason := validExpireCandidateCard(*card, runTime); reason != "" {
return reason
}
if card.FlowPkgEndAt.Time.Unix() != candidate.ExpireUnix {
return "package renewed"
}
return ""
}
func storeExpireCandidate(ctx context.Context, card entity.TSimCardInfo, targetMonth string, expireAt time.Time, runTime time.Time) error {
if card.FlowPkgEndAt == nil || card.FlowPkgEndAt.IsZero() {
return gerror.New("package end time is empty")
}
candidate := expireCandidate{
TenantID: strings.TrimSpace(card.TenantId),
CardID: card.Id,
ICCID: strings.TrimSpace(card.Iccid),
MSISDN: strings.TrimSpace(card.Msisdn),
PackageCode: strings.TrimSpace(card.CurrentPackageCode),
ExpireUnix: card.FlowPkgEndAt.Time.Unix(),
ExpireAt: card.FlowPkgEndAt.Time.Format("2006-01-02 15:04:05"),
TargetMonth: targetMonth,
CreatedDept: expireWarningDeptID(card),
CreatedBy: card.CreatedBy,
LoadedAt: runTime.Format("2006-01-02 15:04:05"),
}
key := expireCandidateKey(candidate.TenantID, candidate.CardID, candidate.ExpireUnix)
if key == "" {
return gerror.New("expire candidate key is empty")
}
payload, err := json.Marshal(candidate)
if err != nil {
return err
}
ttl := expireCandidateTTL(expireAt, runTime)
if _, err = g.Redis().Do(ctx, "SET", key, string(payload), "EX", int(ttl.Seconds())); err != nil {
return err
}
setKey := expireCandidateSetKey(targetMonth)
if _, err = g.Redis().Do(ctx, "SADD", setKey, key); err != nil {
return err
}
_, err = g.Redis().Do(ctx, "EXPIRE", setKey, int(ttl.Seconds()))
return err
}
func readExpireCandidate(ctx context.Context, key string) (expireCandidate, bool, error) {
res, err := g.Redis().Do(ctx, "GET", key)
if err != nil {
return expireCandidate{}, false, err
}
raw := strings.TrimSpace(res.String())
if raw == "" {
return expireCandidate{}, false, nil
}
var candidate expireCandidate
if err = json.Unmarshal([]byte(raw), &candidate); err != nil {
return expireCandidate{}, false, err
}
return candidate, true, nil
}
func removeExpireCandidate(ctx context.Context, setKey string, key string) {
key = strings.TrimSpace(key)
if key == "" {
return
}
if strings.TrimSpace(setKey) != "" {
_, _ = g.Redis().Do(ctx, "SREM", setKey, key)
}
_, _ = g.Redis().Do(ctx, "DEL", key)
}
func systemExpireWarnConfig(ctx context.Context, card entity.TSimCardInfo, phones string, emails string) warnConfig {
thresholds := systemExpireWarnThresholds(ctx)
return warnConfig{
TWarnConfig: entity.TWarnConfig{
Id: systemExpireConfigID,
TenantId: strings.TrimSpace(card.TenantId),
ItemType: warnItemTypeCard,
ItemId: card.Id,
ItemCode: strings.TrimSpace(card.Iccid),
WarnUnit: warnUnitDay,
FirstWarn: thresholds.First,
SecondWarn: thresholds.Second,
ThirdWarn: thresholds.Third,
WarnMethods: systemExpireWarnMethods(phones, emails),
WarnPhones: phones,
WarnEmails: emails,
AppNotifyEnabled: 1,
WxTemplateId: consts.WarningWechatTemplateIDDefault,
IsEnabled: 1,
CreatedDept: expireWarningDeptID(card),
CreatedBy: card.CreatedBy,
},
WarnType: warnTypeExpireDay,
}
}
func systemExpireWarnMethods(phones string, emails string) string {
methods := []string{warnMethodPlatform}
if strings.TrimSpace(emails) != "" {
methods = append(methods, warnMethodEmail)
}
if strings.TrimSpace(phones) != "" {
methods = append(methods, warnMethodSMS)
}
return strings.Join(methods, ",")
}
func (s *sSimWarning) resolveExpireWarningContacts(ctx context.Context, card entity.TSimCardInfo) (string, string, error) {
rows := make([]expireWarningContactRow, 0)
agentRows, err := queryExpireAgentContactRows(ctx, card)
if err != nil {
return "", "", err
}
rows = append(rows, agentRows...)
terminalRows, err := queryExpireTerminalContactRows(ctx, card)
if err != nil {
return "", "", err
}
rows = append(rows, terminalRows...)
return normalizeExpireContactRows(rows)
}
func queryExpireAgentContactRows(ctx context.Context, card entity.TSimCardInfo) ([]expireWarningContactRow, error) {
tenantID := strings.TrimSpace(card.TenantId)
deptID := expireWarningDeptID(card)
if tenantID == "" || (deptID <= 0 && card.CreatedBy <= 0) {
return nil, nil
}
columns := dao.SysUser.Columns()
mod := dao.SysUser.Ctx(ctx).
Fields(columns.UserId, columns.Email, columns.Phonenumber).
Where(columns.TenantId, tenantID).
Where(columns.UserType, consts.SysUserTypeSys).
Where(columns.Status, consts.SysDeptStatusNormal)
switch {
case deptID > 0 && card.CreatedBy > 0:
mod = mod.Where("("+columns.DeptId+" = ? OR "+columns.UserId+" = ?)", deptID, card.CreatedBy)
case deptID > 0:
mod = mod.Where(columns.DeptId, deptID)
default:
mod = mod.Where(columns.UserId, card.CreatedBy)
}
var rows []expireWarningContactRow
err := mod.Scan(&rows)
return rows, err
}
func queryExpireTerminalContactRows(ctx context.Context, card entity.TSimCardInfo) ([]expireWarningContactRow, error) {
rows := make([]expireWarningContactRow, 0)
tenantID := strings.TrimSpace(card.TenantId)
if tenantID == "" {
return rows, nil
}
createdRows, err := queryExpireCreatedTerminalContactRows(ctx, tenantID, card.CreatedBy)
if err != nil {
return nil, err
}
rows = append(rows, createdRows...)
iccid := strings.TrimSpace(card.Iccid)
msisdn := strings.TrimSpace(card.Msisdn)
if iccid == "" && msisdn == "" {
return rows, nil
}
mod := g.DB().Model(simTerminalTable+" st").Ctx(ctx).Safe().
LeftJoin(terminalTable+" t", "t.terminal_id = st.terminal_id AND t.tenant_id = st.tenant_id").
LeftJoin(dao.SysUser.Table()+" u", "(u.user_id = t.leader OR u.user_id = st.terminal_id) AND u.tenant_id = st.tenant_id").
Fields("DISTINCT u.user_id, u.email, u.phonenumber").
Where("st.tenant_id", tenantID).
Where("u.user_type", consts.SysUserTypeAPP).
Where("u.status", consts.SysDeptStatusNormal)
if iccid != "" && msisdn != "" {
mod = mod.Where("(st.iccid = ? OR st.msisdn = ?)", iccid, msisdn)
} else if iccid != "" {
mod = mod.Where("st.iccid", iccid)
} else {
mod = mod.Where("st.msisdn", msisdn)
}
var relatedRows []expireWarningContactRow
if err = mod.Scan(&relatedRows); err != nil {
return nil, err
}
rows = append(rows, relatedRows...)
return rows, nil
}
func queryExpireCreatedTerminalContactRows(ctx context.Context, tenantID string, createdBy int64) ([]expireWarningContactRow, error) {
if strings.TrimSpace(tenantID) == "" || createdBy <= 0 {
return nil, nil
}
columns := dao.SysUser.Columns()
var rows []expireWarningContactRow
err := dao.SysUser.Ctx(ctx).
Fields(columns.UserId, columns.Email, columns.Phonenumber).
Where(columns.TenantId, tenantID).
Where(columns.UserId, createdBy).
Where(columns.UserType, consts.SysUserTypeAPP).
Where(columns.Status, consts.SysDeptStatusNormal).
Scan(&rows)
return rows, err
}
func normalizeExpireContactRows(rows []expireWarningContactRow) (string, string, error) {
phoneSeen := make(map[string]struct{})
emailSeen := make(map[string]struct{})
phones := make([]string, 0)
emails := make([]string, 0)
for _, row := range rows {
phone := strings.TrimSpace(row.Phonenumber)
if phone != "" && isValidConfigWarnPhone(phone) {
phones = appendUniqueString(phones, phoneSeen, phone)
}
email := strings.TrimSpace(row.Email)
if email != "" {
address, err := mail.ParseAddress(email)
if err == nil && address != nil && strings.TrimSpace(address.Address) != "" {
emails = appendUniqueString(emails, emailSeen, strings.ToLower(strings.TrimSpace(address.Address)))
}
}
}
sort.Strings(phones)
sort.Strings(emails)
return strings.Join(phones, ","), strings.Join(emails, ","), nil
}
func appendUniqueString(values []string, seen map[string]struct{}, value string) []string {
value = strings.TrimSpace(value)
if value == "" {
return values
}
key := strings.ToLower(value)
if _, ok := seen[key]; ok {
return values
}
seen[key] = struct{}{}
return append(values, value)
}
func expireWarningDeptID(card entity.TSimCardInfo) int64 {
if card.CreatedDept > 0 {
return card.CreatedDept
}
return card.DeptId
}
func expirePreloadTargetMonth(runTime time.Time) (time.Time, time.Time) {
begin := calendarMonthBegin(runTime)
targetBegin := begin.AddDate(0, 2, 0)
return targetBegin, targetBegin.AddDate(0, 1, 0)
}
func expireCandidateScanMonths(runTime time.Time) []string {
begin := calendarMonthBegin(runTime)
seen := make(map[string]struct{}, 4)
months := make([]string, 0, 4)
for _, offset := range []int{-1, 0, 1, 2} {
month := begin.AddDate(0, offset, 0).Format("200601")
if _, ok := seen[month]; ok {
continue
}
seen[month] = struct{}{}
months = append(months, month)
}
return months
}
func calendarMonthBegin(runTime time.Time) time.Time {
if runTime.IsZero() {
runTime = time.Now()
}
location := runTime.Location()
local := runTime.In(location)
return time.Date(local.Year(), local.Month(), 1, 0, 0, 0, 0, location)
}
func expireCandidateTTL(expireAt time.Time, runTime time.Time) time.Duration {
ttl := expireAt.Sub(runTime)
if ttl < warnDedupeMinTTL {
return warnDedupeMinTTL
}
return ttl
}
func expireCandidateKey(tenantID string, cardID int64, expireUnix int64) string {
tenantID = strings.TrimSpace(tenantID)
if tenantID == "" || cardID <= 0 || expireUnix <= 0 {
return ""
}
return fmt.Sprintf("%s:%s:%d:%d", expireCandidateKeyPrefix, tenantID, cardID, expireUnix)
}
func expireCandidateSetKey(targetMonth string) string {
targetMonth = strings.TrimSpace(targetMonth)
if targetMonth == "" {
targetMonth = "unknown"
}
return expireCandidateSetPrefix + ":" + targetMonth
}
func parseExpireCandidateKey(key string) (tenantID string, cardID int64, expireUnix int64, ok bool) {
key = strings.TrimSpace(key)
prefix := expireCandidateKeyPrefix + ":"
if !strings.HasPrefix(key, prefix) {
return "", 0, 0, false
}
parts := strings.Split(strings.TrimPrefix(key, prefix), ":")
if len(parts) != 3 {
return "", 0, 0, false
}
cardID, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil || cardID <= 0 {
return "", 0, 0, false
}
expireUnix, err = strconv.ParseInt(parts[2], 10, 64)
if err != nil || expireUnix <= 0 {
return "", 0, 0, false
}
return parts[0], cardID, expireUnix, true
}
@@ -0,0 +1,43 @@
package simwarning
import (
"context"
"github.com/gogf/gf/v2/frame/g"
)
const (
defaultExpireFirstWarnDays = 30
defaultExpireSecondWarnDays = 15
defaultExpireThirdWarnDays = 7
)
type expireWarnThresholds struct {
First float64
Second float64
Third float64
}
func systemExpireWarnThresholds(ctx context.Context) expireWarnThresholds {
first := g.Cfg().MustGet(ctx, "warning.expire.firstWarnDays", defaultExpireFirstWarnDays).Int()
second := g.Cfg().MustGet(ctx, "warning.expire.secondWarnDays", defaultExpireSecondWarnDays).Int()
third := g.Cfg().MustGet(ctx, "warning.expire.thirdWarnDays", defaultExpireThirdWarnDays).Int()
if first <= 0 || second <= 0 || third <= 0 || !(first > second && second > third) {
g.Log().Warningf(ctx, "SimWarning expire threshold config invalid, first:%d, second:%d, third:%d, use default:%d/%d/%d",
first,
second,
third,
defaultExpireFirstWarnDays,
defaultExpireSecondWarnDays,
defaultExpireThirdWarnDays,
)
first = defaultExpireFirstWarnDays
second = defaultExpireSecondWarnDays
third = defaultExpireThirdWarnDays
}
return expireWarnThresholds{
First: float64(first),
Second: float64(second),
Third: float64(third),
}
}
@@ -0,0 +1,131 @@
package simwarning
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
func writeWarningRecord(ctx context.Context, hit warningHit, warnTime time.Time) (warningRecord, error) {
stored, err := writeWarningBatch(ctx, []pendingWarning{{Hit: hit}}, warnTime)
if err != nil {
return warningRecord{}, err
}
if len(stored) == 0 {
return warningRecord{}, nil
}
return stored[0].Record, nil
}
func writeWarningBatch(ctx context.Context, pendingWarnings []pendingWarning, warnTime time.Time) ([]storedWarning, error) {
if len(pendingWarnings) == 0 {
return nil, nil
}
storedWarnings := make([]storedWarning, 0, len(pendingWarnings))
err := dao.TWarnLog.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, pending := range pendingWarnings {
warnLogID, err := tx.Ctx(ctx).Model(dao.TWarnLog.Table()).Safe().
Data(buildWarnLogData(pending.Hit, warnTime)).
InsertAndGetId()
if err != nil {
return err
}
storedWarnings = append(storedWarnings, storedWarning{
Hit: pending.Hit,
Record: warningRecord{
WarnLogID: warnLogID,
},
})
}
return nil
})
return storedWarnings, err
}
func buildWarnLogData(hit warningHit, warnTime time.Time) do.TWarnLog {
var remainFlowMb any
if isTrafficKind(hit.Kind) {
remainFlowMb = hit.RemainFlowMb
}
return do.TWarnLog{
TenantId: hit.Config.TenantId,
ConfigId: hit.Config.Id,
ItemType: hit.Config.ItemType,
ItemId: hit.Config.ItemId,
ItemCode: warnItemCodeSnapshot(hit),
WarnStage: hit.WarnStage,
WarnLevel: hit.WarnLevel,
WarnUnit: hit.Config.WarnUnit,
ThresholdValue: hit.ThresholdValue,
ActualValue: hit.ActualValue,
TotalFlowMb: hit.TotalFlowMb,
RemainFlowMb: remainFlowMb,
WarnMethods: warnMethodsSnapshot(hit),
WarnPhones: hit.Config.WarnPhones,
WarnEmails: hit.Config.WarnEmails,
MessageLogId: nil,
WarnTime: gtime.NewFromTime(warnTime),
Remark: hit.Remark,
CreatedDept: hit.Config.CreatedDept,
CreatedBy: hit.Config.CreatedBy,
}
}
func logWarningHit(ctx context.Context, hit warningHit, record warningRecord) {
g.Log().Infof(
ctx,
"SimWarning hit stored, mode:warn_log_only, warnLogId:%d, kind:%s, configId:%d, tenantId:%s, itemType:%d, itemId:%d, itemCode:%s, warnUnit:%d, stage:%d, level:%d, threshold:%.3f, actual:%.3f, totalFlowMb:%.3f, remainFlowMb:%.3f, methods:%s, phones:%s, emails:%s, remark:%s",
record.WarnLogID,
hit.Kind,
hit.Config.Id,
hit.Config.TenantId,
hit.Config.ItemType,
hit.Config.ItemId,
warnItemCodeSnapshot(hit),
hit.Config.WarnUnit,
hit.WarnStage,
hit.WarnLevel,
hit.ThresholdValue,
hit.ActualValue,
hit.TotalFlowMb,
hit.RemainFlowMb,
warnMethodsSnapshot(hit),
hit.Config.WarnPhones,
hit.Config.WarnEmails,
hit.Remark,
)
}
func logWarningBatch(ctx context.Context, storedWarnings []storedWarning) {
if len(storedWarnings) == 0 {
return
}
g.Log().Infof(ctx, "SimWarning warning batch stored, mode:warn_log_only, warnings:%d", len(storedWarnings))
}
func warnItemCodeSnapshot(hit warningHit) string {
if hit.Config.ItemCode != "" {
return hit.Config.ItemCode
}
if hit.Config.ItemType == warnItemTypeFlowPool && hit.Pool.FlowPoolCode != "" {
return hit.Pool.FlowPoolCode
}
if hit.Card.Iccid != "" {
return hit.Card.Iccid
}
return hit.Card.Msisdn
}
func warnMethodsSnapshot(hit warningHit) string {
if hit.Config.WarnMethods != "" {
return hit.Config.WarnMethods
}
return warnMessageDefaultMethod
}
+108
View File
@@ -0,0 +1,108 @@
package simwarning
import (
"context"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/mcache"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/warningnotify"
"github.com/gogf/gf/v2/frame/g"
)
func notifyStoredWarnings(ctx context.Context, storedWarnings []storedWarning, warnTime time.Time) {
if len(storedWarnings) == 0 {
return
}
events := make([]warningnotify.Event, 0, len(storedWarnings))
for _, stored := range storedWarnings {
events = append(events, warningNotifyEvent(ctx, stored, warnTime))
}
if err := warningnotify.Dispatch(ctx, events); err != nil {
g.Log().Errorf(ctx, "SimWarning notify dispatch failed, warnings:%d, err:%+v", len(storedWarnings), err)
}
}
func warningNotifyEvent(ctx context.Context, stored storedWarning, warnTime time.Time) warningnotify.Event {
hit := stored.Hit
operationText := ""
affectOperation := autoDataCutEnabled(hit)
if affectOperation {
_, operationText = autoDataCutEligibility(hit)
}
return warningnotify.Event{
Business: warningnotify.BusinessSimWarning,
EventID: stored.Record.WarnLogID,
TenantID: hit.Config.TenantId,
DeptID: hit.Config.CreatedDept,
DeptName: warningDeptName(ctx, hit.Config.CreatedDept),
CreatedBy: hit.Config.CreatedBy,
Category: warningCategoryText(hit),
Kind: hit.Kind,
ItemType: hit.Config.ItemType,
ItemID: hit.Config.ItemId,
ItemCode: warnItemCodeSnapshot(hit),
MSISDN: hit.Card.Msisdn,
WarnStage: hit.WarnStage,
WarnLevel: hit.WarnLevel,
WarnMethods: warnMethodsSnapshot(hit),
WarnPhones: hit.Config.WarnPhones,
WarnEmails: hit.Config.WarnEmails,
AppNotifyEnabled: hit.Config.AppNotifyEnabled != 0,
WxTemplateID: hit.Config.WxTemplateId,
UnitText: warnUnitText(hit.Config.WarnUnit),
Threshold: hit.ThresholdValue,
Actual: hit.ActualValue,
TotalFlowMB: hit.TotalFlowMb,
RemainFlowMB: hit.RemainFlowMb,
Remark: hit.Remark,
WarnTime: warnTime,
PeriodBegin: hit.PeriodBegin,
PeriodEnd: hit.PeriodEnd,
AffectOperation: affectOperation,
OperationText: operationText,
}
}
func warningDeptName(ctx context.Context, deptID int64) string {
if deptID <= 0 {
return ""
}
deptName, err := mcache.GetDeptName(ctx, deptID)
if err != nil {
g.Log().Warningf(ctx, "SimWarning load dept name failed, deptId:%d, err:%+v", deptID, err)
return ""
}
return strings.TrimSpace(deptName)
}
func warningCategoryText(hit warningHit) string {
switch deriveWarnType(hit.Config.ItemType, hit.Config.WarnUnit) {
case warnTypeCardTraffic:
return "单卡流量预警"
case warnTypeFlowPoolPercent:
return "流量池预警"
case warnTypeExpireDay:
return "到期预警(天)"
case warnTypeExpireMonth:
return "到期预警(月)"
default:
return "预警"
}
}
func warnUnitText(unit int) string {
switch unit {
case warnUnitMb:
return "MB"
case warnUnitPercent:
return "%"
case warnUnitDay:
return "天"
case warnUnitMonth:
return "月"
default:
return ""
}
}
+379
View File
@@ -0,0 +1,379 @@
package simwarning
import (
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"math"
"time"
)
func evaluateConfig(cfg warnConfig, card entity.TSimCardInfo, runTime time.Time) (*warningHit, string) {
cfg.WarnType = normalizeWarnType(cfg.ItemType, cfg.WarnUnit)
switch cfg.WarnType {
case warnTypeCardTraffic:
return evaluateTraffic(cfg, card, runTime)
default:
return nil, "unsupported warn type"
}
}
func evaluateTraffic(cfg warnConfig, card entity.TSimCardInfo, runTime time.Time) (*warningHit, string) {
cfg.WarnType = normalizeWarnType(warnItemTypeCard, cfg.WarnUnit)
if cfg.WarnType != warnTypeCardTraffic {
return nil, "unsupported traffic warn type"
}
if cfg.WarnUnit != warnUnitMb && cfg.WarnUnit != warnUnitPercent {
return nil, "unsupported traffic warn unit"
}
if reason := validCommonCard(card); reason != "" {
return nil, reason
}
if card.PackageTotalFlowMb <= 0 {
return nil, "package total flow is empty"
}
usedFlowMb := card.UsedFlowMb
if usedFlowMb < 0 {
usedFlowMb = 0
}
totalFlowMb := card.PackageTotalFlowMb
remainFlowMb := totalFlowMb - usedFlowMb
if remainFlowMb < 0 {
remainFlowMb = 0
}
usedPercent := usedFlowMb / totalFlowMb * 100
var (
stage int
threshold float64
actual float64
kind string
)
if cfg.WarnUnit == warnUnitPercent {
first := normalizePercentThreshold(cfg.FirstWarn)
second := normalizePercentThreshold(cfg.SecondWarn)
third := normalizePercentThreshold(cfg.ThirdWarn)
stage, threshold = matchGreaterStage(usedPercent, first, second, third)
actual = usedPercent
kind = warnKindTrafficPercent
} else {
stage, threshold = matchGreaterStage(usedFlowMb, cfg.FirstWarn, cfg.SecondWarn, cfg.ThirdWarn)
actual = usedFlowMb
kind = warnKindTrafficMb
}
if stage == 0 {
return nil, "traffic threshold not reached"
}
level := warnLevelByStage(stage)
if cfg.WarnUnit == warnUnitPercent && usedPercent >= 100 {
level = warnLevelUrgent
}
remark := trafficRemark(cfg.WarnUnit, stage, threshold, usedFlowMb, usedPercent, totalFlowMb, remainFlowMb)
hit := baseHit(cfg, card, kind)
hit.WarnStage = stage
hit.WarnLevel = level
hit.ThresholdValue = threshold
hit.ActualValue = round(actual, 3)
hit.TotalFlowMb = round(totalFlowMb, 3)
hit.RemainFlowMb = round(remainFlowMb, 3)
hit.Remark = remark
setTrafficDedupeWindow(&hit, runTime)
return &hit, ""
}
func evaluatePoolTrafficPercent(cfg warnConfig, pool entity.TSimFlowPool, runTime time.Time) (*warningHit, string) {
cfg.WarnType = normalizeWarnType(warnItemTypeFlowPool, cfg.WarnUnit)
if cfg.WarnType != warnTypeFlowPoolPercent {
return nil, "unsupported flow pool warn type"
}
if cfg.WarnUnit != warnUnitPercent {
return nil, "flow pool only supports percent warning"
}
if pool.PoolStatus != 0 {
return nil, "flow pool is not normal"
}
if pool.TotalFlowMb <= 0 {
return nil, "flow pool total flow is empty"
}
usedFlowMb := pool.UsedFlowMb
if usedFlowMb < 0 {
usedFlowMb = 0
}
totalFlowMb := pool.TotalFlowMb
remainFlowMb := totalFlowMb - usedFlowMb
if remainFlowMb < 0 {
remainFlowMb = 0
}
usedPercent := usedFlowMb / totalFlowMb * 100
first := normalizePercentThreshold(cfg.FirstWarn)
second := normalizePercentThreshold(cfg.SecondWarn)
third := normalizePercentThreshold(cfg.ThirdWarn)
stage, threshold := matchGreaterStage(usedPercent, first, second, third)
if stage == 0 {
return nil, "flow pool traffic threshold not reached"
}
level := warnLevelByStage(stage)
if usedPercent >= 100 {
level = warnLevelUrgent
}
remark := fmt.Sprintf(
"流量池流量已达到第%d档阈值,阈值:%.2f%%,实际使用率:%.2f%%,实际已用:%.3f MB,剩余:%.3f MB,总量:%.3f MB",
stage,
threshold,
usedPercent,
usedFlowMb,
remainFlowMb,
totalFlowMb,
)
if usedPercent >= 100 {
remark += ",已超出流量池总量"
}
hit := basePoolHit(cfg, pool, warnKindPoolTrafficPercent)
hit.WarnStage = stage
hit.WarnLevel = level
hit.ThresholdValue = threshold
hit.ActualValue = round(usedPercent, 3)
hit.TotalFlowMb = round(totalFlowMb, 3)
hit.RemainFlowMb = round(remainFlowMb, 3)
hit.Remark = remark
setTrafficDedupeWindow(&hit, runTime)
return &hit, ""
}
func evaluateSystemExpireDay(cfg warnConfig, card entity.TSimCardInfo, runTime time.Time) (*warningHit, string) {
cfg.WarnType = normalizeWarnType(warnItemTypeCard, cfg.WarnUnit)
if cfg.WarnType != warnTypeExpireDay {
return nil, "unsupported expire day warn type"
}
if cfg.WarnUnit != warnUnitDay {
return nil, "unsupported expire day warn unit"
}
if reason := validCommonCard(card); reason != "" {
return nil, reason
}
if card.FlowPkgEndAt == nil || card.FlowPkgEndAt.IsZero() {
return nil, "package end time is empty"
}
endTime := card.FlowPkgEndAt.Time
remainDays := diffDateDays(runTime, endTime)
if remainDays < 0 {
return nil, "package expired"
}
stage, threshold := matchLessStage(float64(remainDays), cfg.FirstWarn, cfg.SecondWarn, cfg.ThirdWarn)
if stage == 0 {
return nil, "expire day threshold not reached"
}
hit := baseHit(cfg, card, warnKindExpireDay)
hit.WarnStage = stage
hit.WarnLevel = warnLevelByStage(stage)
hit.ThresholdValue = threshold
hit.ActualValue = float64(remainDays)
hit.TotalFlowMb = 0
hit.RemainFlowMb = 0
hit.Remark = fmt.Sprintf("套餐将在 %d 天后到期", remainDays)
setExpireDayDedupeWindow(&hit, runTime)
return &hit, ""
}
func validCommonCard(card entity.TSimCardInfo) string {
if card.PackageStatus != consts.PackageStatusEffective {
return "package is not effective"
}
if card.IsFlowPool != 0 {
return "flow pool card is not supported"
}
if card.CurrentPackageCode == "" {
return "current package code is empty"
}
return ""
}
func baseHit(cfg warnConfig, card entity.TSimCardInfo, kind string) warningHit {
hit := warningHit{
Config: cfg,
Card: card,
Kind: kind,
}
if card.FlowPkgBeginAt != nil && !card.FlowPkgBeginAt.IsZero() {
hit.PeriodBegin = card.FlowPkgBeginAt.Time
}
if card.FlowPkgEndAt != nil && !card.FlowPkgEndAt.IsZero() {
hit.PeriodEnd = card.FlowPkgEndAt.Time
}
if hit.PeriodBegin.IsZero() && !hit.PeriodEnd.IsZero() {
hit.PeriodBegin = warnPeriodFallbackBegin(hit.PeriodEnd.Location())
}
return hit
}
func basePoolHit(cfg warnConfig, pool entity.TSimFlowPool, kind string) warningHit {
return warningHit{
Config: cfg,
Pool: pool,
Kind: kind,
}
}
func warnLevelByStage(stage int) int {
switch stage {
case warnStageFirst:
return warnLevelNotice
case warnStageSecond:
return warnLevelWarning
case warnStageThird:
return warnLevelDanger
default:
return warnLevelNotice
}
}
func trafficRemark(warnUnit, stage int, threshold, usedFlowMb, usedPercent, totalFlowMb, remainFlowMb float64) string {
if warnUnit == warnUnitPercent {
remark := fmt.Sprintf(
"单卡流量已达到第%d档阈值,阈值:%.2f%%,实际使用率:%.2f%%,实际已用:%.3f MB,剩余:%.3f MB,总量:%.3f MB",
stage,
threshold,
usedPercent,
usedFlowMb,
remainFlowMb,
totalFlowMb,
)
if usedPercent >= 100 {
return remark + ",已超出套餐流量"
}
return remark
}
return fmt.Sprintf(
"单卡流量已达到第%d档阈值,阈值:%.3f MB,实际已用:%.3f MB,剩余:%.3f MB,总量:%.3f MB",
stage,
threshold,
usedFlowMb,
remainFlowMb,
totalFlowMb,
)
}
func matchGreaterStage(actual, first, second, third float64) (int, float64) {
if third > 0 && actual >= third {
return warnStageThird, third
}
if second > 0 && actual >= second {
return warnStageSecond, second
}
if first > 0 && actual >= first {
return warnStageFirst, first
}
return 0, 0
}
func matchLessStage(actual, first, second, third float64) (int, float64) {
if third > 0 && actual <= third {
return warnStageThird, third
}
if second > 0 && actual <= second {
return warnStageSecond, second
}
if first > 0 && actual <= first {
return warnStageFirst, first
}
return 0, 0
}
func normalizePercentThreshold(value float64) float64 {
if value > 0 && value <= 1 {
return value * 100
}
return value
}
func diffDateDays(runTime, endTime time.Time) int {
location := runTime.Location()
runDate := time.Date(runTime.In(location).Year(), runTime.In(location).Month(), runTime.In(location).Day(), 0, 0, 0, 0, location)
endLocal := endTime.In(location)
endDate := time.Date(endLocal.Year(), endLocal.Month(), endLocal.Day(), 0, 0, 0, 0, location)
return int(endDate.Sub(runDate).Hours() / 24)
}
func round(value float64, places int) float64 {
pow := math.Pow10(places)
return math.Round(value*pow) / pow
}
func warnPeriodFallbackBegin(location *time.Location) time.Time {
if location == nil {
location = time.Local
}
return time.Date(1970, 1, 1, 0, 0, 0, 0, location)
}
func setTrafficDedupeWindow(hit *warningHit, runTime time.Time) {
monthBegin, monthEnd := calendarMonthWindow(runTime)
hit.WindowBegin = monthBegin
hit.WindowEnd = monthEnd
hit.DedupeExpireAt = monthEnd
}
func calendarMonthWindow(runTime time.Time) (time.Time, time.Time) {
if runTime.IsZero() {
runTime = time.Now()
}
location := runTime.Location()
local := runTime.In(location)
begin := time.Date(local.Year(), local.Month(), 1, 0, 0, 0, 0, location)
return begin, begin.AddDate(0, 1, 0)
}
func setExpireDayDedupeWindow(hit *warningHit, runTime time.Time) {
if hit.PeriodEnd.IsZero() {
return
}
stageBegin := expireDayStageBegin(runTime, hit.PeriodEnd, hit.ThresholdValue)
stageEnd := expireDayStageEnd(runTime, *hit)
if !stageEnd.After(stageBegin) {
stageEnd = stageBegin.Add(24 * time.Hour)
}
hit.WindowBegin = stageBegin
hit.WindowEnd = stageEnd
hit.DedupeExpireAt = stageEnd
}
func expireDayStageBegin(runTime, endTime time.Time, threshold float64) time.Time {
location := runTime.Location()
endLocal := endTime.In(location)
endDate := time.Date(endLocal.Year(), endLocal.Month(), endLocal.Day(), 0, 0, 0, 0, location)
return endDate.AddDate(0, 0, -int(threshold))
}
func expireDayStageEnd(runTime time.Time, hit warningHit) time.Time {
if next, ok := nextExpireThreshold(hit); ok {
return expireDayStageBegin(runTime, hit.PeriodEnd, next)
}
location := runTime.Location()
endLocal := hit.PeriodEnd.In(location)
endDate := time.Date(endLocal.Year(), endLocal.Month(), endLocal.Day(), 0, 0, 0, 0, location)
return endDate.AddDate(0, 0, 1)
}
func nextExpireThreshold(hit warningHit) (float64, bool) {
switch hit.WarnStage {
case warnStageFirst:
if hit.Config.SecondWarn > 0 {
return hit.Config.SecondWarn, true
}
case warnStageSecond:
if hit.Config.ThirdWarn > 0 {
return hit.Config.ThirdWarn, true
}
}
return 0, false
}
+472
View File
@@ -0,0 +1,472 @@
package simwarning
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
)
type sSimWarning struct{}
func NewSimWarning() *sSimWarning {
return &sSimWarning{}
}
func init() {
service.RegisterSimWarning(NewSimWarning())
}
func (s *sSimWarning) Scan(ctx context.Context) {
s.scan(ctx, scanScopeAll)
}
func (s *sSimWarning) ScanTraffic(ctx context.Context) {
s.scan(ctx, scanScopeTraffic)
}
func (s *sSimWarning) ScanFlowPool(ctx context.Context) {
s.scan(ctx, scanScopeFlowPool)
}
func (s *sSimWarning) ScanExpire(ctx context.Context) {
s.ScanExpireCandidates(ctx)
}
func (s *sSimWarning) scan(ctx context.Context, scope scanScope) {
if ctx == nil {
ctx = context.Background()
}
scope = normalizeScanScope(scope)
runTime := time.Now()
stats := scanStats{}
pendingWarnings := make([]pendingWarning, 0)
g.Log().Infof(ctx, "SimWarning.Scan begin, runTime:%s, scope:%s, mode:warn_log_only", runTime.Format("2006-01-02 15:04:05"), scope)
for page := 1; ; page++ {
if err := ctx.Err(); err != nil {
stats.ErrorCount++
g.Log().Warningf(ctx, "SimWarning.Scan canceled, page:%d, scope:%s, err:%+v", page, scope, err)
break
}
configs, err := s.queryConfigs(ctx, page, scope)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan query configs failed, page:%d, err:%+v", page, err)
break
}
if len(configs) == 0 {
break
}
stats.ConfigCount += len(configs)
cardMap, err := s.queryCards(ctx, configs)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan query cards failed, page:%d, err:%+v", page, err)
break
}
poolMap, err := s.queryFlowPools(ctx, configs)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan query flow pools failed, page:%d, err:%+v", page, err)
break
}
for _, cfg := range configs {
if pending := s.scanConfig(ctx, cfg, cardMap[itemKey(cfg.TenantId, cfg.ItemId)], poolMap[itemKey(cfg.TenantId, cfg.ItemId)], runTime, &stats); pending != nil {
pendingWarnings = append(pendingWarnings, *pending)
}
}
if len(configs) < scanPageSize {
break
}
}
storedWarnings := s.storePendingWarnings(ctx, pendingWarnings, runTime, &stats)
for _, stored := range storedWarnings {
s.scanAutoAction(ctx, stored.Hit, stored.Record.WarnLogID, runTime, &stats)
}
g.Log().Infof(
ctx,
"SimWarning.Scan end, configs:%d, hits:%d, duplicated:%d, skipped:%d, errors:%d, autoActions:%d, autoActionErrors:%d, scope:%s, mode:warn_log_only",
stats.ConfigCount,
stats.HitCount,
stats.DupeCount,
stats.SkipCount,
stats.ErrorCount,
stats.AutoActionCount,
stats.AutoActionErrors,
scope,
)
}
func (s *sSimWarning) queryConfigs(ctx context.Context, page int, scope scanScope) ([]warnConfig, error) {
scope = normalizeScanScope(scope)
columns := dao.TWarnConfig.Columns()
mod := dao.TWarnConfig.Ctx(ctx).
Fields(
columns.Id,
columns.TenantId,
columns.ItemType,
columns.ItemId,
columns.ItemCode,
columns.WarnUnit,
columns.FirstWarn,
columns.SecondWarn,
columns.ThirdWarn,
columns.WarnMethods,
columns.WarnPhones,
columns.WarnEmails,
columns.AutoAction,
columns.AppNotifyEnabled,
columns.IsEnabled,
columns.WxTemplateId,
columns.CreatedDept,
columns.CreatedBy,
).
Where(columns.IsEnabled, 1)
switch scope {
case scanScopeTraffic:
mod = mod.Where(
columns.ItemType+" = ? AND "+columns.WarnUnit+" IN (?, ?)",
warnItemTypeCard,
warnUnitMb,
warnUnitPercent,
)
case scanScopeFlowPool:
mod = mod.Where(
columns.ItemType+" = ? AND "+columns.WarnUnit+" = ?",
warnItemTypeFlowPool,
warnUnitPercent,
)
case scanScopeExpire:
mod = mod.Where("1 = 0")
default:
mod = mod.Where(
"("+columns.ItemType+" = ? AND "+columns.WarnUnit+" IN (?, ?)) OR ("+
columns.ItemType+" = ? AND "+columns.WarnUnit+" = ?)",
warnItemTypeCard,
warnUnitMb,
warnUnitPercent,
warnItemTypeFlowPool,
warnUnitPercent,
)
}
entities := make([]entity.TWarnConfig, 0)
err := mod.OrderAsc(columns.Id).
Page(page, scanPageSize).
Scan(&entities)
if err != nil {
return nil, err
}
configs := make([]warnConfig, 0, len(entities))
for i := range entities {
configs = append(configs, warnConfig{
TWarnConfig: entities[i],
WarnType: deriveWarnType(entities[i].ItemType, entities[i].WarnUnit),
})
}
return configs, nil
}
func normalizeScanScope(scope scanScope) scanScope {
switch scope {
case scanScopeTraffic, scanScopeFlowPool, scanScopeExpire:
return scope
default:
return scanScopeAll
}
}
func (s *sSimWarning) scanConfig(ctx context.Context, cfg warnConfig, card *entity.TSimCardInfo, pool *entity.TSimFlowPool, runTime time.Time, stats *scanStats) *pendingWarning {
if cfg.ItemType == warnItemTypeFlowPool {
return s.scanFlowPoolConfig(ctx, cfg, pool, runTime, stats)
}
return s.scanCardConfig(ctx, cfg, card, runTime, stats)
}
func (s *sSimWarning) scanCardConfig(ctx context.Context, cfg warnConfig, card *entity.TSimCardInfo, runTime time.Time, stats *scanStats) *pendingWarning {
if card == nil {
stats.SkipCount++
g.Log().Warningf(ctx, "SimWarning.Scan skipped, card not found, configId:%d, tenantId:%s, itemId:%d", cfg.Id, cfg.TenantId, cfg.ItemId)
return nil
}
hit, reason := evaluateConfig(cfg, *card, runTime)
if hit == nil {
stats.SkipCount++
g.Log().Debugf(ctx, "SimWarning.Scan skipped, configId:%d, cardId:%d, reason:%s", cfg.Id, card.Id, reason)
return nil
}
exists, err := s.existsInPeriod(ctx, *hit)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan idempotent check failed, configId:%d, cardId:%d, err:%+v", cfg.Id, card.Id, err)
return nil
}
if exists {
stats.DupeCount++
g.Log().Infof(ctx, "SimWarning.Scan duplicated, configId:%d, cardId:%d, stage:%d", cfg.Id, card.Id, hit.WarnStage)
s.scanAutoAction(ctx, *hit, 0, runTime, stats)
return nil
}
acquired, dedupeKey, err := acquireWarningDedupe(ctx, *hit, runTime)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan acquire dedupe failed, configId:%d, cardId:%d, stage:%d, err:%+v", cfg.Id, card.Id, hit.WarnStage, err)
return nil
}
if !acquired {
stats.DupeCount++
g.Log().Infof(ctx, "SimWarning.Scan duplicated by redis, configId:%d, cardId:%d, stage:%d", cfg.Id, card.Id, hit.WarnStage)
return nil
}
return &pendingWarning{Hit: *hit, DedupeKey: dedupeKey}
}
func (s *sSimWarning) scanFlowPoolConfig(ctx context.Context, cfg warnConfig, pool *entity.TSimFlowPool, runTime time.Time, stats *scanStats) *pendingWarning {
if pool == nil {
stats.SkipCount++
g.Log().Warningf(ctx, "SimWarning.Scan skipped, flow pool not found, configId:%d, tenantId:%s, itemId:%d", cfg.Id, cfg.TenantId, cfg.ItemId)
return nil
}
hit, reason := evaluatePoolTrafficPercent(cfg, *pool, runTime)
if hit == nil {
stats.SkipCount++
g.Log().Debugf(ctx, "SimWarning.Scan skipped, configId:%d, flowPoolId:%d, reason:%s", cfg.Id, pool.Id, reason)
return nil
}
exists, err := s.existsInPeriod(ctx, *hit)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan idempotent check failed, configId:%d, flowPoolId:%d, err:%+v", cfg.Id, pool.Id, err)
return nil
}
if exists {
stats.DupeCount++
g.Log().Infof(ctx, "SimWarning.Scan duplicated, configId:%d, flowPoolId:%d, stage:%d", cfg.Id, pool.Id, hit.WarnStage)
return nil
}
acquired, dedupeKey, err := acquireWarningDedupe(ctx, *hit, runTime)
if err != nil {
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan acquire dedupe failed, configId:%d, flowPoolId:%d, stage:%d, err:%+v", cfg.Id, pool.Id, hit.WarnStage, err)
return nil
}
if !acquired {
stats.DupeCount++
g.Log().Infof(ctx, "SimWarning.Scan duplicated by redis, configId:%d, flowPoolId:%d, stage:%d", cfg.Id, pool.Id, hit.WarnStage)
return nil
}
return &pendingWarning{Hit: *hit, DedupeKey: dedupeKey}
}
func (s *sSimWarning) storePendingWarnings(ctx context.Context, pendingWarnings []pendingWarning, runTime time.Time, stats *scanStats) []storedWarning {
if len(pendingWarnings) == 0 {
return nil
}
storedWarnings, err := writeWarningBatch(ctx, pendingWarnings, runTime)
if err != nil {
for _, pending := range pendingWarnings {
releaseWarningDedupe(ctx, pending.DedupeKey)
}
stats.ErrorCount++
g.Log().Errorf(ctx, "SimWarning.Scan write warning batch failed, warnings:%d, err:%+v", len(pendingWarnings), err)
return nil
}
stats.HitCount += len(storedWarnings)
logWarningBatch(ctx, storedWarnings)
for _, item := range storedWarnings {
logWarningHit(ctx, item.Hit, item.Record)
}
notifyStoredWarnings(ctx, storedWarnings, runTime)
return storedWarnings
}
func (s *sSimWarning) scanAutoAction(ctx context.Context, hit warningHit, warnLogID int64, runTime time.Time, stats *scanStats) {
handled, err := s.handleAutoAction(ctx, hit, warnLogID, runTime)
if err != nil {
stats.ErrorCount++
stats.AutoActionErrors++
g.Log().Errorf(ctx, "SimWarning.Scan auto action failed, configId:%d, cardId:%d, stage:%d, warnLogId:%d, err:%+v", hit.Config.Id, hit.Card.Id, hit.WarnStage, warnLogID, err)
return
}
if handled {
stats.AutoActionCount++
}
}
func (s *sSimWarning) queryCards(ctx context.Context, configs []warnConfig) (map[string]*entity.TSimCardInfo, error) {
result := make(map[string]*entity.TSimCardInfo)
if len(configs) == 0 {
return result, nil
}
itemIds, tenantIds := configKeys(configs, warnItemTypeCard)
if len(itemIds) == 0 || len(tenantIds) == 0 {
return result, nil
}
cards := make([]entity.TSimCardInfo, 0, len(itemIds))
columns := dao.TSimCardInfo.Columns()
err := dao.TSimCardInfo.Ctx(ctx).
Fields(
columns.Id,
columns.Iccid,
columns.Msisdn,
columns.Imsi,
columns.CurrentPackageCode,
columns.PackageStatus,
columns.BossCardStatus,
columns.IsFlowPool,
columns.DataServiceStatus,
columns.FlowPkgBeginAt,
columns.FlowPkgEndAt,
columns.PackageTotalFlowMb,
columns.UsedFlowMb,
columns.ChanKey,
columns.TenantId,
columns.CreatedDept,
columns.CreatedBy,
).
WhereIn(columns.Id, itemIds).
WhereIn(columns.TenantId, tenantIds).
Scan(&cards)
if err != nil {
return nil, err
}
for i := range cards {
card := cards[i]
result[itemKey(card.TenantId, card.Id)] = &cards[i]
}
return result, nil
}
func (s *sSimWarning) queryFlowPools(ctx context.Context, configs []warnConfig) (map[string]*entity.TSimFlowPool, error) {
result := make(map[string]*entity.TSimFlowPool)
if len(configs) == 0 {
return result, nil
}
itemIds, tenantIds := configKeys(configs, warnItemTypeFlowPool)
if len(itemIds) == 0 || len(tenantIds) == 0 {
return result, nil
}
pools := make([]entity.TSimFlowPool, 0, len(itemIds))
columns := dao.TSimFlowPool.Columns()
err := dao.TSimFlowPool.Ctx(ctx).
Fields(
columns.Id,
columns.FlowPoolCode,
columns.TotalFlowMb,
columns.UsedFlowMb,
columns.RemainFlowMb,
columns.PoolStatus,
columns.TenantId,
columns.CreatedDept,
columns.CreatedBy,
).
WhereIn(columns.Id, itemIds).
WhereIn(columns.TenantId, tenantIds).
Scan(&pools)
if err != nil {
return nil, err
}
for i := range pools {
pool := pools[i]
result[itemKey(pool.TenantId, pool.Id)] = &pools[i]
}
return result, nil
}
func (s *sSimWarning) existsInPeriod(ctx context.Context, hit warningHit) (bool, error) {
windowBegin, windowEnd := warningWindow(hit)
if windowBegin.IsZero() || windowEnd.IsZero() {
return false, nil
}
columns := dao.TWarnLog.Columns()
count, err := dao.TWarnLog.Ctx(ctx).
Where(columns.TenantId, hit.Config.TenantId).
Where(columns.ConfigId, hit.Config.Id).
Where(columns.ItemType, hit.Config.ItemType).
Where(columns.ItemId, hit.Config.ItemId).
Where(columns.WarnUnit, hit.Config.WarnUnit).
Where(columns.WarnStage, hit.WarnStage).
Where(columns.WarnTime+" >= ?", windowBegin).
Where(columns.WarnTime+" < ?", windowEnd).
Count()
if err != nil {
if isTableNotExist(err) {
g.Log().Warningf(ctx, "SimWarning.Scan skip idempotent check, warn log table not found: %s", dao.TWarnLog.Table())
return false, nil
}
return false, err
}
return count > 0, nil
}
func isTableNotExist(err error) bool {
if err == nil {
return false
}
for err != nil {
text := strings.ToLower(err.Error())
if strings.Contains(text, "doesn't exist") ||
strings.Contains(text, "does not exist") ||
strings.Contains(text, "not exist") ||
strings.Contains(text, "no such table") ||
strings.Contains(text, "unknown table") {
return true
}
err = errors.Unwrap(err)
}
return false
}
func configKeys(configs []warnConfig, itemType int) ([]int64, []string) {
itemIdSet := make(map[int64]struct{})
tenantIdSet := make(map[string]struct{})
itemIds := make([]int64, 0, len(configs))
tenantIds := make([]string, 0, len(configs))
for _, cfg := range configs {
if itemType > 0 && cfg.ItemType != itemType {
continue
}
if cfg.ItemId > 0 {
if _, ok := itemIdSet[cfg.ItemId]; !ok {
itemIdSet[cfg.ItemId] = struct{}{}
itemIds = append(itemIds, cfg.ItemId)
}
}
if cfg.TenantId != "" {
if _, ok := tenantIdSet[cfg.TenantId]; !ok {
tenantIdSet[cfg.TenantId] = struct{}{}
tenantIds = append(tenantIds, cfg.TenantId)
}
}
}
return itemIds, tenantIds
}
func itemKey(tenantId string, itemId int64) string {
return tenantId + ":" + strconv.FormatInt(itemId, 10)
}
+105
View File
@@ -0,0 +1,105 @@
package simwarning
import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"time"
)
const (
warnItemTypeCard = 1
warnItemTypeFlowPool = 2
warnUnitMb = 1
warnUnitPercent = 2
warnUnitDay = 3
warnUnitMonth = 4
warnTypeCardTraffic = 1
warnTypeExpireDay = 2
warnTypeExpireMonth = 3
warnTypeFlowPoolPercent = 4
warnKindTrafficMb = "traffic_mb"
warnKindTrafficPercent = "traffic_percent"
warnKindPoolTrafficPercent = "pool_traffic_percent"
warnKindExpireDay = "expire_day"
warnKindExpireMonth = "expire_month"
warnStageFirst = 1
warnStageSecond = 2
warnStageThird = 3
warnLevelNotice = 2
warnLevelWarning = 3
warnLevelDanger = 4
warnLevelUrgent = 5
warnAutoActionDataCut = 3
warnMethodSMS = "0"
warnMethodEmail = "1"
warnMethodPhone = "2"
warnMethodPlatform = "3"
warnMessageDefaultMethod = warnMethodPlatform
scanPageSize = 200
systemExpireConfigID = -1
)
type scanScope string
const (
scanScopeAll scanScope = "all"
scanScopeTraffic scanScope = "traffic"
scanScopeFlowPool scanScope = "flow_pool"
scanScopeExpire scanScope = "expire"
)
type warningRecord struct {
WarnLogID int64
}
type pendingWarning struct {
Hit warningHit
DedupeKey string
}
type storedWarning struct {
Hit warningHit
Record warningRecord
}
type warnConfig struct {
entity.TWarnConfig
WarnType int `orm:"-"`
}
type warningHit struct {
Config warnConfig
Card entity.TSimCardInfo
Pool entity.TSimFlowPool
Kind string
WarnStage int
WarnLevel int
ThresholdValue float64
ActualValue float64
TotalFlowMb float64
RemainFlowMb float64
Remark string
PeriodBegin time.Time
PeriodEnd time.Time
WindowBegin time.Time
WindowEnd time.Time
DedupeExpireAt time.Time
}
type scanStats struct {
ConfigCount int
HitCount int
SkipCount int
DupeCount int
ErrorCount int
AutoActionCount int
AutoActionErrors int
}
@@ -0,0 +1,49 @@
package simwarning
import "github.com/gogf/gf/v2/errors/gerror"
func normalizeWarnType(itemType, warnUnit int) int {
return deriveWarnType(itemType, warnUnit)
}
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
}