暂存一下,防止丢失

This commit is contained in:
2026-07-24 16:07:18 +08:00
parent 5d6e23f1a5
commit a18ed8bc8d
180 changed files with 13597 additions and 1986 deletions

View File

@@ -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 {