暂存一下,防止丢失
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
carddomain "github.com/break/junhong_cmp_fiber/internal/domain/cardobservation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -40,13 +43,6 @@ type PollingCallback interface {
|
||||
OnCardDisabled(ctx context.Context, cardID uint)
|
||||
}
|
||||
|
||||
// DataDeductor 流量扣减回调接口
|
||||
// 用于在手动刷新流量后触发套餐扣减,避免循环依赖
|
||||
type DataDeductor interface {
|
||||
// DeductDataUsage 按优先级扣减套餐流量
|
||||
DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB float64) error
|
||||
}
|
||||
|
||||
// RealnameActivator 实名激活回调接口
|
||||
// 用于在手动实名后触发待实名套餐激活,避免循环依赖
|
||||
type RealnameActivator interface {
|
||||
@@ -65,7 +61,6 @@ type Service struct {
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
pollingCallback PollingCallback
|
||||
dataDeductor DataDeductor
|
||||
realnameActivator RealnameActivator
|
||||
stopResumeService StopResumeServiceInterface
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
@@ -75,6 +70,12 @@ type Service struct {
|
||||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
packageExpiryQuery *packageexpiry.Query
|
||||
cardObservation *cardapp.Service
|
||||
}
|
||||
|
||||
// SetCardObservationService 注入统一卡实名观测写入用例。
|
||||
func (s *Service) SetCardObservationService(service *cardapp.Service) {
|
||||
s.cardObservation = service
|
||||
}
|
||||
|
||||
// SetPackageExpiryQuery 注入套餐最终到期查询,供列表使用批量投影。
|
||||
@@ -129,12 +130,6 @@ func (s *Service) SetEnterpriseStore(store *postgres.EnterpriseStore) {
|
||||
s.enterpriseStore = store
|
||||
}
|
||||
|
||||
// SetDataDeductor 设置流量扣减回调
|
||||
// 在应用启动时由 bootstrap 调用,注入套餐扣减服务
|
||||
func (s *Service) SetDataDeductor(deductor DataDeductor) {
|
||||
s.dataDeductor = deductor
|
||||
}
|
||||
|
||||
// SetRealnameActivator 设置实名激活回调
|
||||
// 在应用启动时由 bootstrap 注入套餐激活服务
|
||||
func (s *Service) SetRealnameActivator(activator RealnameActivator) {
|
||||
@@ -1563,17 +1558,29 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询网络状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else if strings.TrimSpace(statusResp.ICCID) == "" {
|
||||
s.logger.Warn("刷新卡数据:网络状态响应缺少 ICCID,跳过本次网络观测", zap.Uint("card_id", card.ID))
|
||||
} else if s.cardObservation == nil {
|
||||
return errors.New(errors.CodeInternalError, "卡网络观测能力未配置")
|
||||
} else {
|
||||
gatewayExtend := strings.TrimSpace(statusResp.Extend)
|
||||
updates["gateway_extend"] = gatewayExtend
|
||||
networkStatus, ok := gateway.ParseCardNetworkStatus(statusResp.CardStatus, gatewayExtend)
|
||||
if !ok {
|
||||
requestID := requestIDFromContext(ctx)
|
||||
decision, applyErr := s.cardObservation.ApplyNetworkObservation(ctx, carddomain.NetworkObservation{
|
||||
CardID: card.ID, GatewayStatus: statusResp.CardStatus,
|
||||
GatewayExtend: statusResp.Extend, GatewayIMEI: statusResp.IMEI,
|
||||
Metadata: carddomain.ObservationMetadata{
|
||||
ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualSync,
|
||||
Scene: constants.CardObservationSceneManualRefresh, ObservedAt: syncTime,
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
},
|
||||
})
|
||||
if applyErr != nil {
|
||||
return applyErr
|
||||
}
|
||||
if !decision.StatusKnown {
|
||||
s.logger.Warn("刷新卡数据:未知 Gateway 卡状态",
|
||||
zap.String("iccid", iccid),
|
||||
zap.String("card_status", statusResp.CardStatus),
|
||||
zap.String("extend", gatewayExtend))
|
||||
} else {
|
||||
updates["network_status"] = networkStatus
|
||||
zap.String("extend", strings.TrimSpace(statusResp.Extend)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1583,12 +1590,23 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询实名状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else if strings.TrimSpace(realnameResp.ICCID) == "" {
|
||||
s.logger.Warn("刷新卡数据:实名响应缺少 ICCID,跳过本次实名观测", zap.Uint("card_id", card.ID))
|
||||
} else if s.cardObservation == nil {
|
||||
return errors.New(errors.CodeInternalError, "卡实名观测能力未配置")
|
||||
} else {
|
||||
realNameStatus := parseGatewayRealnameStatus(realnameResp.RealStatus)
|
||||
updates["real_name_status"] = realNameStatus
|
||||
// 检测 0→1 变化:旧状态非已实名且新状态为已实名,补写 first_realname_at
|
||||
if card.RealNameStatus != constants.RealNameStatusVerified && realNameStatus == constants.RealNameStatusVerified {
|
||||
updates["first_realname_at"] = syncTime
|
||||
requestID := requestIDFromContext(ctx)
|
||||
_, applyErr := s.cardObservation.ApplyCardObservation(ctx, carddomain.RealnameObservation{
|
||||
CardID: card.ID,
|
||||
Verified: realnameResp.RealStatus,
|
||||
Metadata: carddomain.ObservationMetadata{
|
||||
ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualSync,
|
||||
Scene: constants.CardObservationSceneManualRefresh, ObservedAt: syncTime,
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
},
|
||||
})
|
||||
if applyErr != nil {
|
||||
return applyErr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1598,8 +1616,23 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询流量失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else if s.cardObservation == nil {
|
||||
return errors.New(errors.CodeInternalError, "卡流量观测能力未配置")
|
||||
} else {
|
||||
flowIncrementMB = s.calculateRefreshFlowUpdates(card, float64(flowResp.Used), syncTime, updates)
|
||||
requestID := requestIDFromContext(ctx)
|
||||
resetDay := postgres.NewCarrierStore(s.db).GetDataResetDay(ctx, card.CarrierID)
|
||||
decision, applyErr := s.cardObservation.ApplyTrafficObservation(ctx, carddomain.TrafficObservation{
|
||||
CardID: card.ID, GatewayReadingMB: float64(flowResp.Used), ResetDay: resetDay,
|
||||
Metadata: carddomain.ObservationMetadata{
|
||||
ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualSync,
|
||||
Scene: constants.CardObservationSceneManualRefresh, ObservedAt: syncTime,
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
},
|
||||
})
|
||||
if applyErr != nil {
|
||||
return applyErr
|
||||
}
|
||||
flowIncrementMB = decision.IncrementMB
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1607,16 +1640,6 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新卡数据失败")
|
||||
}
|
||||
|
||||
// 有增量时触发套餐流量扣减
|
||||
if flowIncrementMB > 0 && s.dataDeductor != nil {
|
||||
if err := s.dataDeductor.DeductDataUsage(ctx, constants.AssetTypeIotCard, card.ID, flowIncrementMB); err != nil {
|
||||
s.logger.Warn("手动刷新:套餐流量扣减失败",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Float64("increment_mb", flowIncrementMB),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 失效轮询缓存,确保轮询系统读到最新的 network_status/real_name_status/流量
|
||||
if s.pollingCallback != nil {
|
||||
s.pollingCallback.OnCardStatusChanged(ctx, card.ID)
|
||||
@@ -1629,97 +1652,6 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateRefreshFlowUpdates 计算手动刷新时的流量增量并填充 updates
|
||||
// 算法与轮询 calculateFlowUpdates 一致:增量 = 当前网关读数 - 上次网关读数
|
||||
// 返回本次增量值(MB),用于触发套餐扣减
|
||||
func (s *Service) calculateRefreshFlowUpdates(card *model.IotCard, gatewayFlowMB float64, now time.Time, updates map[string]any) float64 {
|
||||
increment := gatewayFlowMB - card.LastGatewayReadingMB
|
||||
shouldUpdateGatewayReading := true
|
||||
|
||||
if increment < 0 {
|
||||
// 当前值比上次小,检查是否为上游运营商正常重置
|
||||
resetDay := s.getCarrierResetDay(card.CarrierID)
|
||||
if isRefreshResetWindow(now, resetDay) {
|
||||
// 在重置日窗口内,本次原始值即为增量
|
||||
increment = gatewayFlowMB
|
||||
s.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 {
|
||||
// 非重置日出现值下降,异常,不计入
|
||||
s.logger.Warn("手动刷新:流量异常,非重置日出现值下降",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Float64("gateway_flow", gatewayFlowMB),
|
||||
zap.Float64("last_reading", card.LastGatewayReadingMB))
|
||||
increment = 0
|
||||
shouldUpdateGatewayReading = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldUpdateGatewayReading {
|
||||
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 {
|
||||
s.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 {
|
||||
// 同月内,增量累加(使用 gorm.Expr 保证原子性)
|
||||
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 increment
|
||||
}
|
||||
|
||||
// getCarrierResetDay 读取运营商的上游流量重置日
|
||||
func (s *Service) getCarrierResetDay(carrierID uint) int {
|
||||
var carrier model.Carrier
|
||||
if err := s.db.Select("data_reset_day").First(&carrier, carrierID).Error; err != nil {
|
||||
return 1
|
||||
}
|
||||
if carrier.DataResetDay == 0 {
|
||||
return 1
|
||||
}
|
||||
return carrier.DataResetDay
|
||||
}
|
||||
|
||||
// isRefreshResetWindow 判断今天是否在运营商重置日窗口内
|
||||
// 窗口 = 重置日当天 + 前一天(容错网关数据延迟)
|
||||
func isRefreshResetWindow(now time.Time, resetDay int) bool {
|
||||
today := now.Day()
|
||||
if today == resetDay {
|
||||
return true
|
||||
}
|
||||
resetDate := time.Date(now.Year(), now.Month(), resetDay, 0, 0, 0, 0, now.Location())
|
||||
prevDay := resetDate.AddDate(0, 0, -1).Day()
|
||||
return today == prevDay
|
||||
}
|
||||
|
||||
// parseGatewayRealnameStatus 将网关返回的实名状态布尔值转换为 real_name_status 数值
|
||||
// true=已实名(1),false=未实名(0)
|
||||
func parseGatewayRealnameStatus(realStatus bool) int {
|
||||
@@ -2190,22 +2122,20 @@ func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, r
|
||||
}
|
||||
|
||||
oldStatus := card.RealNameStatus
|
||||
statusChanged := oldStatus != realNameStatus
|
||||
now := time.Now()
|
||||
|
||||
fields := map[string]any{
|
||||
"last_real_name_check_at": now,
|
||||
if s.cardObservation == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "卡实名观测能力未配置")
|
||||
}
|
||||
if statusChanged {
|
||||
fields["real_name_status"] = realNameStatus
|
||||
}
|
||||
if statusChanged &&
|
||||
oldStatus != constants.RealNameStatusVerified &&
|
||||
realNameStatus == constants.RealNameStatusVerified {
|
||||
fields["first_realname_at"] = now
|
||||
}
|
||||
|
||||
if err := s.iotCardStore.UpdateFields(ctx, cardID, fields); err != nil {
|
||||
requestID := requestIDFromContext(ctx)
|
||||
if _, err := s.cardObservation.ApplyCardObservation(ctx, carddomain.RealnameObservation{
|
||||
CardID: cardID,
|
||||
Verified: realNameStatus == constants.RealNameStatusVerified,
|
||||
Metadata: carddomain.ObservationMetadata{
|
||||
ObservationID: uuid.NewString(), Source: constants.CardObservationSourceManualOverride,
|
||||
Scene: constants.CardObservationSceneManualRefresh, ObservedAt: now,
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
},
|
||||
}); err != nil {
|
||||
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡实名状态失败")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
@@ -2248,10 +2178,6 @@ func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, r
|
||||
return nil, wrapErr
|
||||
}
|
||||
|
||||
if statusChanged {
|
||||
s.handleManualRealnameStatusChanged(ctx, oldStatus, freshCard)
|
||||
}
|
||||
|
||||
if s.pollingCallback != nil {
|
||||
s.pollingCallback.OnCardStatusChanged(ctx, cardID)
|
||||
}
|
||||
@@ -2281,6 +2207,14 @@ func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, r
|
||||
return freshCard, nil
|
||||
}
|
||||
|
||||
func requestIDFromContext(ctx context.Context) string {
|
||||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||||
if requestID == nil {
|
||||
return ""
|
||||
}
|
||||
return *requestID
|
||||
}
|
||||
|
||||
// handleManualRealnameStatusChanged 处理手动实名状态变更后的联动逻辑
|
||||
func (s *Service) handleManualRealnameStatusChanged(ctx context.Context, oldStatus int, card *model.IotCard) {
|
||||
if card == nil {
|
||||
|
||||
Reference in New Issue
Block a user