chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package trafficcutoff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/dataswitch"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func submitCutoffBatch(ctx context.Context, key cutoffBatchGroupKey, items []lockedCutoffCard, runTime time.Time) (*batchimport.Result, error) {
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
processor, err := dataswitch.NewProcessor(dataswitch.OperationDataCut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(items))
|
||||
for index, item := range items {
|
||||
rows = append(rows, cutoffDataSwitchRow(index+1, item.Card, runTime))
|
||||
}
|
||||
req := batchimport.SubmitRequest{
|
||||
SourceChannel: batchmetadata.System,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
BizParams: &common.SubmitOptions{Remark: fmt.Sprintf("达量断网自动触发,触发卡数:%d", len(rows))},
|
||||
SourceType: consts.BatchSourceTypeScheduled,
|
||||
Operator: batchimport.OperatorContext{
|
||||
TenantId: key.TenantId,
|
||||
DeptId: key.CreatedDept,
|
||||
UserId: key.CreatedBy,
|
||||
},
|
||||
}
|
||||
result, err := batchimport.CreatePendingRows(ctx, req, processor, rows, batchimport.PendingOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = common.PublishBatchMessageStrict(ctx, result, consts.QueueBatchExecute, "traffic_cutoff"); err != nil {
|
||||
if markErr := markCutoffBatchPublishFailed(ctx, result, err); markErr != nil {
|
||||
return nil, gerror.Wrapf(markErr, "publish cutoff batch failed: %v", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func markCutoffBatchPublishFailed(ctx context.Context, result *batchimport.Result, publishErr error) error {
|
||||
if result == nil || result.BatchNo == "" {
|
||||
return nil
|
||||
}
|
||||
reason := "publish batch message failed"
|
||||
if publishErr != nil {
|
||||
reason += ": " + publishErr.Error()
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(dao.BatchMain.Columns().BatchNo, result.BatchNo).
|
||||
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusPending).
|
||||
Data(do.BatchMain{
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
|
||||
RunResult: consts.BatchRunResultFailed,
|
||||
FailReason: reason,
|
||||
EndTime: gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "mark cutoff batch publish failure")
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, result.BatchNo).
|
||||
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusPending).
|
||||
Data(do.BatchDetail{
|
||||
DetailStatus: consts.BatchDetailStatusFailed,
|
||||
ErrorReason: reason,
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "mark cutoff batch detail publish failure")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func cutoffDataSwitchRow(rowNo int, card cutoffCard, runTime time.Time) *dataswitch.Row {
|
||||
triggerReason := "达量断网:已用流量达到配置阈值,系统自动停用数据服务"
|
||||
if card.isCrossMonthHoldReapply() {
|
||||
triggerReason = "达量断网:跨月保持断网,系统重新停用已恢复的数据服务"
|
||||
}
|
||||
return &dataswitch.Row{
|
||||
RowNo: rowNo,
|
||||
Iccid: card.Iccid,
|
||||
Msisdn: card.Msisdn,
|
||||
Imsi: card.Imsi,
|
||||
Authorized: true,
|
||||
CardID: card.Id,
|
||||
CutoffThreshold: roundCutoffFlow(card.TrafficCutoffThreshold),
|
||||
UsedFlowMb: roundCutoffFlow(card.UsedFlowMb),
|
||||
PackageTotalFlowMb: roundCutoffFlow(cutoffTotalFlowMb(card)),
|
||||
RemainFlowMb: roundCutoffFlow(cutoffRemainFlowMb(card)),
|
||||
DataServiceStatusBefore: card.DataServiceStatus,
|
||||
TrafficCutoffCrossMonthHold: card.TrafficCutoffCrossMonthHold,
|
||||
TriggeredAt: runTime.Format("2006-01-02 15:04:05"),
|
||||
TriggerReason: triggerReason,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package trafficcutoff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/cache"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func acquireCutoffDedupe(ctx context.Context, card cutoffCard, runTime time.Time) (bool, string, error) {
|
||||
key := cutoffDedupeKey(card)
|
||||
if key == "" {
|
||||
return false, "", nil
|
||||
}
|
||||
value := fmt.Sprintf(
|
||||
"threshold=%.3f;used=%.3f;total=%.3f;remain=%.3f;time=%s",
|
||||
card.TrafficCutoffThreshold,
|
||||
card.UsedFlowMb,
|
||||
cutoffTotalFlowMb(card),
|
||||
cutoffRemainFlowMb(card),
|
||||
runTime.Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
ok, err := cache.Instance().SetIfNotExist(ctx, key, value, cutoffDedupeTTL)
|
||||
return ok, key, err
|
||||
}
|
||||
|
||||
func releaseCutoffDedupe(ctx context.Context, key string) {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
_, _ = cache.Instance().Remove(ctx, key)
|
||||
}
|
||||
|
||||
func releaseCutoffDedupeBatch(ctx context.Context, items []lockedCutoffCard) {
|
||||
for _, item := range items {
|
||||
releaseCutoffDedupe(ctx, item.DedupeKey)
|
||||
}
|
||||
}
|
||||
|
||||
func cutoffDedupeKey(card cutoffCard) string {
|
||||
tenantId := strings.TrimSpace(card.TenantId)
|
||||
if tenantId == "" || card.Id <= 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s:card:%s:%d", cutoffDedupeKeyPrefix, tenantId, card.Id)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package trafficcutoff
|
||||
|
||||
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 notifyCutoffBatch(ctx context.Context, key cutoffBatchGroupKey, items []lockedCutoffCard, batchNo string, runTime time.Time) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
deptNames := make(map[int64]string)
|
||||
events := make([]warningnotify.Event, 0, len(items))
|
||||
for _, item := range items {
|
||||
card := item.Card
|
||||
deptID := cutoffNotificationDeptID(card, key.CreatedDept)
|
||||
if card.DeptId <= 0 {
|
||||
g.Log().Warningf(ctx, "TrafficCutoff notify fallback to created dept because card dept is empty, cardId:%d, iccid:%s, createdDept:%d", card.Id, card.Iccid, key.CreatedDept)
|
||||
}
|
||||
deptName, ok := deptNames[deptID]
|
||||
if !ok {
|
||||
deptName = cutoffDeptName(ctx, deptID)
|
||||
deptNames[deptID] = deptName
|
||||
}
|
||||
events = append(events, warningnotify.Event{
|
||||
Business: warningnotify.BusinessTrafficCutoff,
|
||||
TenantID: key.TenantId,
|
||||
DeptID: deptID,
|
||||
DeptName: deptName,
|
||||
CreatedBy: key.CreatedBy,
|
||||
Category: "达量断网",
|
||||
Kind: "traffic_cutoff",
|
||||
ItemType: 1,
|
||||
ItemID: card.Id,
|
||||
ItemCode: card.Iccid,
|
||||
MSISDN: card.Msisdn,
|
||||
WarnLevel: 5,
|
||||
WarnMethods: warningnotify.MethodPlatform,
|
||||
UnitText: "MB",
|
||||
Threshold: roundCutoffFlow(card.TrafficCutoffThreshold),
|
||||
Actual: roundCutoffFlow(card.UsedFlowMb),
|
||||
TotalFlowMB: roundCutoffFlow(cutoffTotalFlowMb(card)),
|
||||
RemainFlowMB: roundCutoffFlow(cutoffRemainFlowMb(card)),
|
||||
Remark: "已用流量达到达量断网阈值,系统已生成停用数据服务批次",
|
||||
WarnTime: runTime,
|
||||
AffectOperation: true,
|
||||
OperationText: "已生成达量断网批次,worker 将执行停用数据服务",
|
||||
BatchNo: batchNo,
|
||||
})
|
||||
}
|
||||
if err := warningnotify.Dispatch(ctx, events); err != nil {
|
||||
g.Log().Errorf(ctx, "TrafficCutoff.Check notify dispatch failed, batchNo:%s, cards:%d, err:%+v", batchNo, len(items), err)
|
||||
}
|
||||
}
|
||||
|
||||
func cutoffNotificationDeptID(card cutoffCard, fallbackDeptID int64) int64 {
|
||||
if card.DeptId > 0 {
|
||||
return card.DeptId
|
||||
}
|
||||
return fallbackDeptID
|
||||
}
|
||||
|
||||
func cutoffDeptName(ctx context.Context, deptID int64) string {
|
||||
if deptID <= 0 {
|
||||
return ""
|
||||
}
|
||||
deptName, err := mcache.GetDeptName(ctx, deptID)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "TrafficCutoff load dept name failed, deptId:%d, err:%+v", deptID, err)
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(deptName)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package trafficcutoff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/logic/cardflow"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sTrafficCutoff struct{}
|
||||
|
||||
func NewTrafficCutoff() *sTrafficCutoff {
|
||||
return &sTrafficCutoff{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterTrafficCutoff(NewTrafficCutoff())
|
||||
}
|
||||
|
||||
func (s *sTrafficCutoff) Check(ctx context.Context) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
runTime := time.Now()
|
||||
stats := cutoffStats{}
|
||||
lastID := int64(0)
|
||||
|
||||
g.Log().Infof(ctx, "TrafficCutoff.Check begin, runTime:%s", runTime.Format("2006-01-02 15:04:05"))
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
stats.ErrorCount++
|
||||
g.Log().Warningf(ctx, "TrafficCutoff.Check canceled, lastID:%d, err:%+v", lastID, err)
|
||||
break
|
||||
}
|
||||
cards, err := s.queryCandidates(ctx, lastID, runTime)
|
||||
if err != nil {
|
||||
stats.ErrorCount++
|
||||
g.Log().Errorf(ctx, "TrafficCutoff.Check query candidates failed, lastID:%d, err:%+v", lastID, err)
|
||||
break
|
||||
}
|
||||
if len(cards) == 0 {
|
||||
break
|
||||
}
|
||||
stats.CandidateCount += len(cards)
|
||||
for _, card := range cards {
|
||||
if card.Id > lastID {
|
||||
lastID = card.Id
|
||||
}
|
||||
}
|
||||
s.handleCandidates(ctx, cards, runTime, &stats)
|
||||
if len(cards) < cutoffPageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Infof(
|
||||
ctx,
|
||||
"TrafficCutoff.Check end, candidates:%d, hits:%d, duplicated:%d, skipped:%d, errors:%d, batches:%d",
|
||||
stats.CandidateCount,
|
||||
stats.HitCount,
|
||||
stats.DupeCount,
|
||||
stats.SkipCount,
|
||||
stats.ErrorCount,
|
||||
stats.BatchCount,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sTrafficCutoff) queryCandidates(ctx context.Context, lastID int64, runTime time.Time) ([]cutoffCard, error) {
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
packageTable := dao.TSimFlowPackageInfo.Table()
|
||||
packageColumns := dao.TSimFlowPackageInfo.Columns()
|
||||
packageAlias := "current_package"
|
||||
crossMonthHoldField := fieldName(cardTable, "traffic_cutoff_cross_month_hold")
|
||||
triggeredAtField := fieldName(cardTable, "traffic_cutoff_triggered_at")
|
||||
field := func(name string) string {
|
||||
return cardTable + "." + name
|
||||
}
|
||||
cards := make([]cutoffCard, 0, cutoffPageSize)
|
||||
err := dao.TSimCardInfo.Ctx(ctx).
|
||||
LeftJoin(packageTable+" as "+packageAlias, packageAlias+"."+packageColumns.PackageCode+" = "+field(columns.CurrentPackageCode)+" AND "+packageAlias+"."+packageColumns.TenantId+" = "+field(columns.TenantId)).
|
||||
Fields(
|
||||
field(columns.Id),
|
||||
field(columns.Iccid),
|
||||
field(columns.Msisdn),
|
||||
field(columns.Imsi),
|
||||
field(columns.CurrentPackageCode),
|
||||
field(columns.PackageStatus),
|
||||
field(columns.BossCardStatus),
|
||||
field(columns.IsFlowPool),
|
||||
field(columns.PackageTotalFlowMb),
|
||||
field(columns.UsedFlowMb),
|
||||
field(columns.DataServiceStatus),
|
||||
field(columns.TrafficCutoffStatus),
|
||||
field(columns.TrafficCutoffThreshold),
|
||||
crossMonthHoldField,
|
||||
triggeredAtField,
|
||||
field(columns.ChanKey),
|
||||
field(columns.TenantId),
|
||||
field(columns.DeptId),
|
||||
field(columns.CreatedDept),
|
||||
field(columns.CreatedBy),
|
||||
cardflow.TotalFlowExpr(cardTable, packageAlias)+" AS total_flow_mb",
|
||||
cardflow.RemainFlowExpr(cardTable, packageAlias)+" AS remain_flow_mb",
|
||||
).
|
||||
WhereGT(field(columns.Id), lastID).
|
||||
Where(field(columns.TrafficCutoffStatus), cutoffStatusEnabled).
|
||||
Where("(("+field(columns.TrafficCutoffThreshold)+" > ? AND "+field(columns.UsedFlowMb)+" >= "+field(columns.TrafficCutoffThreshold)+") OR ("+crossMonthHoldField+" = ? AND "+triggeredAtField+" IS NOT NULL AND "+triggeredAtField+" < ?))", 0, 1, monthStart(runTime)).
|
||||
Where(field(columns.DataServiceStatus), cutoffDataServiceStatusEnabled).
|
||||
Where(field(columns.PackageStatus), consts.PackageStatusEffective).
|
||||
Where(field(columns.BossCardStatus), consts.V3ApiSimBossCardStatusActivated).
|
||||
Where(field(columns.IsFlowPool), 0).
|
||||
Where(field(columns.Iccid)+" <> ?", "").
|
||||
Where(field(columns.ChanKey)+" <> ?", "").
|
||||
OrderAsc(field(columns.Id)).
|
||||
Limit(cutoffPageSize).
|
||||
Scan(&cards)
|
||||
return cards, err
|
||||
}
|
||||
|
||||
func monthStart(value time.Time) time.Time {
|
||||
location := value.Location()
|
||||
return time.Date(value.Year(), value.Month(), 1, 0, 0, 0, 0, location)
|
||||
}
|
||||
|
||||
func (s *sTrafficCutoff) handleCandidates(ctx context.Context, cards []cutoffCard, runTime time.Time, stats *cutoffStats) {
|
||||
groups := make(map[cutoffBatchGroupKey][]lockedCutoffCard)
|
||||
for _, card := range cards {
|
||||
if reason := validateCutoffCard(card, runTime); reason != "" {
|
||||
stats.SkipCount++
|
||||
g.Log().Infof(ctx, "TrafficCutoff.Check skipped, cardId:%d, iccid:%s, reason:%s", card.Id, card.Iccid, reason)
|
||||
continue
|
||||
}
|
||||
acquired, dedupeKey, err := acquireCutoffDedupe(ctx, card, runTime)
|
||||
if err != nil {
|
||||
stats.ErrorCount++
|
||||
g.Log().Errorf(ctx, "TrafficCutoff.Check acquire dedupe failed, cardId:%d, iccid:%s, err:%+v", card.Id, card.Iccid, err)
|
||||
continue
|
||||
}
|
||||
if !acquired {
|
||||
stats.DupeCount++
|
||||
g.Log().Infof(ctx, "TrafficCutoff.Check duplicated by redis, cardId:%d, iccid:%s", card.Id, card.Iccid)
|
||||
continue
|
||||
}
|
||||
key := cutoffGroupKey(card)
|
||||
groups[key] = append(groups[key], lockedCutoffCard{Card: card, DedupeKey: dedupeKey})
|
||||
}
|
||||
|
||||
for key, items := range groups {
|
||||
if err := ctx.Err(); err != nil {
|
||||
stats.ErrorCount++
|
||||
releaseCutoffDedupeBatch(ctx, items)
|
||||
g.Log().Warningf(ctx, "TrafficCutoff.Check canceled before submit batch, tenantId:%s, cards:%d, err:%+v", key.TenantId, len(items), err)
|
||||
continue
|
||||
}
|
||||
result, err := submitCutoffBatch(ctx, key, items, runTime)
|
||||
if err != nil {
|
||||
stats.ErrorCount++
|
||||
releaseCutoffDedupeBatch(ctx, items)
|
||||
g.Log().Errorf(ctx, "TrafficCutoff.Check submit cutoff batch failed, tenantId:%s, dept:%d, user:%d, cards:%d, err:%+v", key.TenantId, key.CreatedDept, key.CreatedBy, len(items), err)
|
||||
continue
|
||||
}
|
||||
stats.HitCount += len(items)
|
||||
stats.BatchCount++
|
||||
g.Log().Infof(ctx, "TrafficCutoff.Check batch submitted, batchNo:%s, batchType:%d, tenantId:%s, dept:%d, user:%d, cards:%d", result.BatchNo, result.BatchType, key.TenantId, key.CreatedDept, key.CreatedBy, len(items))
|
||||
notifyCutoffBatch(ctx, key, items, result.BatchNo, runTime)
|
||||
}
|
||||
}
|
||||
|
||||
func validateCutoffCard(card cutoffCard, runTime time.Time) string {
|
||||
if card.Id <= 0 {
|
||||
return "card id is empty"
|
||||
}
|
||||
if strings.TrimSpace(card.TenantId) == "" {
|
||||
return "tenant id is empty"
|
||||
}
|
||||
if card.CreatedDept <= 0 || card.CreatedBy <= 0 {
|
||||
return "created dept or created by is empty"
|
||||
}
|
||||
if strings.TrimSpace(card.Iccid) == "" {
|
||||
return "iccid is empty"
|
||||
}
|
||||
if strings.TrimSpace(card.ChanKey) == "" {
|
||||
return "chan key is empty"
|
||||
}
|
||||
if card.TrafficCutoffStatus != cutoffStatusEnabled {
|
||||
return "traffic cutoff is not enabled"
|
||||
}
|
||||
if !card.isThresholdReached() && !card.isCrossMonthHoldReapplyAt(runTime) {
|
||||
return "traffic cutoff threshold is not reached"
|
||||
}
|
||||
if card.DataServiceStatus != cutoffDataServiceStatusEnabled {
|
||||
return "data service is not enabled"
|
||||
}
|
||||
if card.PackageStatus != consts.PackageStatusEffective {
|
||||
return "package is not effective"
|
||||
}
|
||||
if card.BossCardStatus != consts.V3ApiSimBossCardStatusActivated {
|
||||
return "boss card status is not activated"
|
||||
}
|
||||
if card.IsFlowPool != 0 {
|
||||
return "flow pool card is not supported"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fieldName(table, column string) string {
|
||||
return table + "." + column
|
||||
}
|
||||
|
||||
func cutoffGroupKey(card cutoffCard) cutoffBatchGroupKey {
|
||||
return cutoffBatchGroupKey{
|
||||
TenantId: strings.TrimSpace(card.TenantId),
|
||||
CreatedDept: card.CreatedDept,
|
||||
CreatedBy: card.CreatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
func cutoffTotalFlowMb(card cutoffCard) float64 {
|
||||
if card.TotalFlowMb > 0 {
|
||||
return card.TotalFlowMb
|
||||
}
|
||||
return card.PackageTotalFlowMb
|
||||
}
|
||||
|
||||
func cutoffRemainFlowMb(card cutoffCard) float64 {
|
||||
remain := card.RemainFlowMb
|
||||
if remain == 0 && cutoffTotalFlowMb(card) > 0 {
|
||||
remain = cutoffTotalFlowMb(card) - card.UsedFlowMb
|
||||
}
|
||||
if remain < 0 {
|
||||
return 0
|
||||
}
|
||||
return remain
|
||||
}
|
||||
|
||||
func roundCutoffFlow(value float64) float64 {
|
||||
return math.Round(value*1000) / 1000
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package trafficcutoff
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
cutoffPageSize = 200
|
||||
cutoffStatusEnabled = 1
|
||||
cutoffDataServiceStatusEnabled = 1
|
||||
cutoffDedupeKeyPrefix = "iotlcm:warning:traffic_cutoff"
|
||||
cutoffDedupeTTL = 6 * time.Hour
|
||||
)
|
||||
|
||||
type cutoffBatchGroupKey struct {
|
||||
TenantId string
|
||||
CreatedDept int64
|
||||
CreatedBy int64
|
||||
}
|
||||
|
||||
type cutoffCard struct {
|
||||
entity.TSimCardInfo
|
||||
TrafficCutoffCrossMonthHold int `json:"trafficCutoffCrossMonthHold" orm:"traffic_cutoff_cross_month_hold"`
|
||||
TrafficCutoffTriggeredAt *gtime.Time `json:"trafficCutoffTriggeredAt" orm:"traffic_cutoff_triggered_at"`
|
||||
TotalFlowMb float64 `json:"totalFlowMb" orm:"total_flow_mb"`
|
||||
RemainFlowMb float64 `json:"remainFlowMb" orm:"remain_flow_mb"`
|
||||
}
|
||||
|
||||
func (c cutoffCard) isCrossMonthHoldReapply() bool {
|
||||
return c.TrafficCutoffCrossMonthHold == 1 && c.TrafficCutoffTriggeredAt != nil
|
||||
}
|
||||
|
||||
func (c cutoffCard) isCrossMonthHoldReapplyAt(runTime time.Time) bool {
|
||||
return c.isCrossMonthHoldReapply() && c.TrafficCutoffTriggeredAt.Before(gtime.New(monthStart(runTime)))
|
||||
}
|
||||
|
||||
func (c cutoffCard) isThresholdReached() bool {
|
||||
return c.TrafficCutoffThreshold > 0 && c.UsedFlowMb >= c.TrafficCutoffThreshold
|
||||
}
|
||||
|
||||
type lockedCutoffCard struct {
|
||||
Card cutoffCard
|
||||
DedupeKey string
|
||||
}
|
||||
|
||||
type cutoffStats struct {
|
||||
CandidateCount int
|
||||
HitCount int
|
||||
DupeCount int
|
||||
SkipCount int
|
||||
ErrorCount int
|
||||
BatchCount int
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package trafficcutoff
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/warningnotify"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestCutoffCardIsCrossMonthHoldReapply(t *testing.T) {
|
||||
triggeredAt := gtime.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
card cutoffCard
|
||||
want bool
|
||||
}{
|
||||
{name: "enabled and triggered", card: cutoffCard{TrafficCutoffCrossMonthHold: 1, TrafficCutoffTriggeredAt: triggeredAt}, want: true},
|
||||
{name: "enabled but not triggered", card: cutoffCard{TrafficCutoffCrossMonthHold: 1}, want: false},
|
||||
{name: "disabled but triggered", card: cutoffCard{TrafficCutoffTriggeredAt: triggeredAt}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.card.isCrossMonthHoldReapply(); got != tt.want {
|
||||
t.Fatalf("isCrossMonthHoldReapply() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCutoffCardIsCrossMonthHoldReapplyAt(t *testing.T) {
|
||||
runTime := time.Date(2026, 9, 22, 10, 0, 0, 0, time.Local)
|
||||
previousMonth := gtime.New(time.Date(2026, 8, 31, 23, 59, 59, 0, time.Local))
|
||||
currentMonth := gtime.New(time.Date(2026, 9, 1, 0, 0, 0, 0, time.Local))
|
||||
if !(cutoffCard{TrafficCutoffCrossMonthHold: 1, TrafficCutoffTriggeredAt: previousMonth}).isCrossMonthHoldReapplyAt(runTime) {
|
||||
t.Fatal("previous-month successful cutoff should be reapplied")
|
||||
}
|
||||
if (cutoffCard{TrafficCutoffCrossMonthHold: 1, TrafficCutoffTriggeredAt: currentMonth}).isCrossMonthHoldReapplyAt(runTime) {
|
||||
t.Fatal("current-month cutoff must not be reapplied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCutoffCardIsThresholdReached(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
card cutoffCard
|
||||
want bool
|
||||
}{
|
||||
{name: "equal", card: cutoffCard{TSimCardInfo: entity.TSimCardInfo{TrafficCutoffThreshold: 1024, UsedFlowMb: 1024}}, want: true},
|
||||
{name: "above", card: cutoffCard{TSimCardInfo: entity.TSimCardInfo{TrafficCutoffThreshold: 1024, UsedFlowMb: 1024.001}}, want: true},
|
||||
{name: "below", card: cutoffCard{TSimCardInfo: entity.TSimCardInfo{TrafficCutoffThreshold: 1024, UsedFlowMb: 1023.999}}, want: false},
|
||||
{name: "disabled threshold", card: cutoffCard{TSimCardInfo: entity.TSimCardInfo{TrafficCutoffThreshold: 0, UsedFlowMb: 2048}}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.card.isThresholdReached(); got != tt.want {
|
||||
t.Fatalf("isThresholdReached() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCutoffCardUsesConfiguredThreshold(t *testing.T) {
|
||||
runTime := time.Date(2026, 9, 22, 10, 0, 0, 0, time.Local)
|
||||
card := cutoffCard{TSimCardInfo: entity.TSimCardInfo{
|
||||
Id: 1,
|
||||
Iccid: "8986000000000000001",
|
||||
ChanKey: "channel-001",
|
||||
TenantId: "000000",
|
||||
CreatedDept: 1,
|
||||
CreatedBy: 1,
|
||||
TrafficCutoffStatus: cutoffStatusEnabled,
|
||||
TrafficCutoffThreshold: 1024,
|
||||
UsedFlowMb: 1024,
|
||||
DataServiceStatus: cutoffDataServiceStatusEnabled,
|
||||
PackageStatus: consts.PackageStatusEffective,
|
||||
BossCardStatus: consts.V3ApiSimBossCardStatusActivated,
|
||||
}}
|
||||
if reason := validateCutoffCard(card, runTime); reason != "" {
|
||||
t.Fatalf("validateCutoffCard() reason = %q", reason)
|
||||
}
|
||||
card.UsedFlowMb = 1023.999
|
||||
if reason := validateCutoffCard(card, runTime); reason != "traffic cutoff threshold is not reached" {
|
||||
t.Fatalf("validateCutoffCard() reason = %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonthStart(t *testing.T) {
|
||||
value := time.Date(2026, 9, 21, 14, 35, 12, 0, time.FixedZone("CST", 8*60*60))
|
||||
want := time.Date(2026, 9, 1, 0, 0, 0, 0, value.Location())
|
||||
if got := monthStart(value); !got.Equal(want) {
|
||||
t.Fatalf("monthStart() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCutoffNotificationDeptIDUsesCardCurrentDept(t *testing.T) {
|
||||
card := cutoffCard{TSimCardInfo: entity.TSimCardInfo{DeptId: 172, CreatedDept: 1, CreatedBy: 1}}
|
||||
if got := cutoffNotificationDeptID(card, card.CreatedDept); got != 172 {
|
||||
t.Fatalf("cutoffNotificationDeptID() = %d, want current card dept 172", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCutoffNotificationDeptIDFallsBackAndDoesNotSilentlyDrop(t *testing.T) {
|
||||
card := cutoffCard{TSimCardInfo: entity.TSimCardInfo{CreatedDept: 1, CreatedBy: 1}}
|
||||
if got := cutoffNotificationDeptID(card, card.CreatedDept); got != 1 {
|
||||
t.Fatalf("cutoffNotificationDeptID() = %d, want fallback created dept 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCutoffNotificationsSplitByCurrentCardDept(t *testing.T) {
|
||||
events := []warningnotify.Event{
|
||||
{
|
||||
Business: warningnotify.BusinessTrafficCutoff,
|
||||
TenantID: "000000",
|
||||
DeptID: 172,
|
||||
CreatedBy: 1,
|
||||
Category: "达量断网",
|
||||
ItemType: 1,
|
||||
ItemCode: "card-for-172",
|
||||
},
|
||||
{
|
||||
Business: warningnotify.BusinessTrafficCutoff,
|
||||
TenantID: "000000",
|
||||
DeptID: 206,
|
||||
CreatedBy: 1,
|
||||
Category: "达量断网",
|
||||
ItemType: 1,
|
||||
ItemCode: "card-for-206",
|
||||
},
|
||||
}
|
||||
|
||||
batches := warningnotify.BuildBatches(events)
|
||||
if len(batches) != 2 {
|
||||
t.Fatalf("BuildBatches() returned %d batches, want 2", len(batches))
|
||||
}
|
||||
|
||||
want := map[int64]string{172: "card-for-172", 206: "card-for-206"}
|
||||
for _, batch := range batches {
|
||||
if batch.Method != warningnotify.MethodPlatform {
|
||||
t.Fatalf("batch method = %s, want platform", batch.Method)
|
||||
}
|
||||
if len(batch.Items) != 1 {
|
||||
t.Fatalf("dept %d has %d items, want 1", batch.DeptID, len(batch.Items))
|
||||
}
|
||||
if wantCode, ok := want[batch.DeptID]; !ok || batch.Items[0].ItemCode != wantCode {
|
||||
t.Fatalf("batch dept=%d item=%s, want one matching current-dept card", batch.DeptID, batch.Items[0].ItemCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user