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
+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)
}