246 lines
7.9 KiB
Go
246 lines
7.9 KiB
Go
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
|
|
}
|