Files
junhong_cmp_fiber/internal/task/polling_carddata_handler.go
2026-05-18 10:47:07 +08:00

201 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package task
import (
"context"
"time"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/model"
iot_card_svc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// PollingCarddataHandler 流量检查任务处理器
// 职责:调 Gateway 查流量 → 写 DB → 扣减套餐流量 → 调 EvaluateAndAct 判断停复机
// 不做:直接停复机决策,委托给 StopResumeService.EvaluateAndAct
type PollingCarddataHandler struct {
base *PollingBase
gateway *gateway.Client
iotCardStore *postgres.IotCardStore
carrierStore *postgres.CarrierStore
usageService *packagepkg.UsageService
stopResumeSvc iot_card_svc.StopResumeServiceInterface
}
// NewPollingCarddataHandler 创建流量检查任务处理器
func NewPollingCarddataHandler(
base *PollingBase,
gw *gateway.Client,
iotCardStore *postgres.IotCardStore,
carrierStore *postgres.CarrierStore,
usageService *packagepkg.UsageService,
stopResumeSvc iot_card_svc.StopResumeServiceInterface,
) *PollingCarddataHandler {
return &PollingCarddataHandler{
base: base,
gateway: gw,
iotCardStore: iotCardStore,
carrierStore: carrierStore,
usageService: usageService,
stopResumeSvc: stopResumeSvc,
}
}
// Handle 处理流量检查任务
func (h *PollingCarddataHandler) Handle(ctx context.Context, t *asynq.Task) error {
startTime := time.Now()
cardID, ok := parseTaskPayload(t.Payload(), h.base.logger)
if !ok {
return nil
}
if !h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata) {
h.base.logger.Debug("并发已满,重新入队", zap.Uint("card_id", cardID))
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
}
defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)
card, err := h.base.getCardWithCache(ctx, cardID)
if err != nil {
if isNotFound(err) {
return nil
}
h.base.logger.Error("获取卡信息失败", zap.Uint("card_id", cardID), zap.Error(err))
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, false, time.Since(startTime))
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
}
var gatewayFlowMB float64
if h.gateway != nil {
result, gwErr := h.gateway.QueryFlow(ctx, &gateway.FlowQueryReq{CardNo: card.ICCID})
if gwErr != nil {
h.base.logger.Warn("查询流量失败",
zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID), zap.Error(gwErr))
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, false, time.Since(startTime))
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
}
gatewayFlowMB = float64(result.Used)
} else {
gatewayFlowMB = card.CurrentMonthUsageMB
}
now := time.Now()
resetDay := h.carrierStore.GetDataResetDay(ctx, card.CarrierID)
updates, flowIncrementMB, isCrossMonth := h.calculateFlowUpdates(card, gatewayFlowMB, now, resetDay)
updates["last_data_check_at"] = now
if h.gateway != nil {
updates["last_sync_time"] = now
}
if h.base.verboseLog && h.gateway != nil {
h.base.logger.Info("流量轮询详情",
zap.Uint("card_id", cardID),
zap.String("iccid", card.ICCID),
zap.Float64("gateway_flow_mb", gatewayFlowMB),
zap.Float64("increment_mb", flowIncrementMB),
zap.Bool("is_cross_month", isCrossMonth))
}
if updateErr := h.iotCardStore.UpdateFields(ctx, cardID, updates); updateErr != nil {
h.base.logger.Error("更新卡流量信息失败", zap.Uint("card_id", cardID), zap.Error(updateErr))
}
go h.base.insertDataUsageRecord(context.Background(), cardID, flowIncrementMB, now)
cacheUpdates := map[string]any{"last_gateway_reading_mb": gatewayFlowMB}
if isCrossMonth {
// 跨月时同步更新缓存中的本月开始日期和本月用量,避免下次从缓存读取时再次误判跨月
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
cacheUpdates["current_month_start_date"] = currentMonthStart.Unix()
if flowIncrementMB > 0 {
cacheUpdates["current_month_usage_mb"] = flowIncrementMB
} else {
cacheUpdates["current_month_usage_mb"] = float64(0)
}
}
h.base.updateCardCache(ctx, cardID, cacheUpdates)
if flowIncrementMB > 0 && h.usageService != nil {
if deductErr := h.usageService.DeductDataUsage(ctx, constants.AssetTypeIotCard, cardID, flowIncrementMB); deductErr != nil {
h.base.logger.Warn("套餐流量扣减失败",
zap.Uint("card_id", cardID), zap.Float64("increment_mb", flowIncrementMB), zap.Error(deductErr))
}
}
if h.stopResumeSvc != nil {
freshCard, loadErr := h.iotCardStore.GetByID(ctx, cardID)
if loadErr == nil {
if evalErr := h.stopResumeSvc.EvaluateAndAct(ctx, freshCard); evalErr != nil {
h.base.logger.Warn("流量检查后停复机评估失败",
zap.Uint("card_id", cardID), zap.Error(evalErr))
}
}
}
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, true, time.Since(startTime))
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
}
// calculateFlowUpdates 计算流量更新字段、增量和是否跨月
// 决策13完整迁移跨月流量边界检测逻辑月份切换检测、上月总量保存、当月计数器重置
// 第三个返回值 isCrossMonth 供调用方同步更新 Redis 缓存,避免下次误判跨月
func (h *PollingCarddataHandler) calculateFlowUpdates(card *model.IotCard, gatewayFlowMB float64, now time.Time, resetDay int) (map[string]any, float64, bool) {
updates := make(map[string]any)
increment := gatewayFlowMB - card.LastGatewayReadingMB
if increment < 0 {
if isResetWindow(now, resetDay) {
increment = gatewayFlowMB
h.base.logger.Info("检测到上游运营商重置",
zap.Uint("card_id", card.ID),
zap.Float64("gateway_flow", gatewayFlowMB),
zap.Float64("last_reading", card.LastGatewayReadingMB),
zap.Int("reset_day", resetDay))
} else {
h.base.logger.Warn("流量异常:非重置日出现值下降",
zap.Uint("card_id", card.ID),
zap.Float64("gateway_flow", gatewayFlowMB),
zap.Float64("last_reading", card.LastGatewayReadingMB))
increment = 0
}
}
updates["last_gateway_reading_mb"] = gatewayFlowMB
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
isCrossMonth := card.CurrentMonthStartDate == nil ||
card.CurrentMonthStartDate.Before(currentMonthStart)
if isCrossMonth {
h.base.logger.Info("检测到跨月,重置流量计数",
zap.Uint("card_id", card.ID),
zap.Float64("last_month_total", card.CurrentMonthUsageMB))
updates["last_month_total_mb"] = card.CurrentMonthUsageMB
updates["current_month_start_date"] = currentMonthStart
if increment > 0 {
updates["current_month_usage_mb"] = increment
} else {
updates["current_month_usage_mb"] = float64(0)
}
} else if increment > 0 {
updates["current_month_usage_mb"] = gorm.Expr("current_month_usage_mb + ?", increment)
}
if increment > 0 {
updates["data_usage_mb"] = gorm.Expr("data_usage_mb + ?", int64(increment))
}
if card.CurrentMonthStartDate == nil {
updates["current_month_start_date"] = currentMonthStart
}
return updates, increment, isCrossMonth
}