All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
- 轮询配置新增套餐状态、资产状态、重置日前窗口与窗口内流量间隔(迁移 248),间隔三态:正数按间隔、0 不检查、空继承 - 配置按 priority, id 排序合并;在库与已销售无套餐未匹配时告警不入队 - 新增优先级整体重排、匹配预览、立即重算到期时间(异步只提前)、分层统计与重置日前加密统计接口 - 已切换日快照计量的卡流量按观测时间归属,激活前用量只记已结算 - 订单购买与实名激活补齐资产级激活锁,激活前对已切换卡结算一次流量,失败只告警不阻塞,结算不触发通道阈值判定
175 lines
5.6 KiB
Go
175 lines
5.6 KiB
Go
package task
|
||
|
||
import (
|
||
"context"
|
||
"strconv"
|
||
"time"
|
||
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
)
|
||
|
||
// getCardWithCache 优先从 Redis 缓存获取卡信息,缓存 miss 时查 DB 并异步写缓存
|
||
func (b *PollingBase) getCardWithCache(ctx context.Context, cardID uint) (*model.IotCard, error) {
|
||
key := constants.RedisPollingCardInfoKey(cardID)
|
||
|
||
result, err := b.redis.HGetAll(ctx, key).Result()
|
||
if err == nil && len(result) > 0 {
|
||
card := parseCardFromCache(cardID, result)
|
||
if _, ok := result["asset_status"]; !ok {
|
||
b.backfillAssetStatus(ctx, key, card)
|
||
}
|
||
return card, nil
|
||
}
|
||
|
||
card, err := b.iotCardStore.GetByID(ctx, cardID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 同步写缓存,避免与后续 updateCardCache 产生竞态:
|
||
// 若异步协程在 updateCardCache 之后才执行,会将 last_gateway_reading_mb
|
||
// 覆盖回 DB 旧值(0),导致下次轮询增量重复计全量。
|
||
writeCardToCache(b, key, card)
|
||
return card, nil
|
||
}
|
||
|
||
// backfillAssetStatus 为分层轮询补齐旧缓存缺少的资产状态:只读取并回写这一个字段,
|
||
// 不整条重建缓存,避免覆盖缓存中单独维护的读数等字段。
|
||
func (b *PollingBase) backfillAssetStatus(ctx context.Context, key string, card *model.IotCard) {
|
||
var statuses []int
|
||
if err := b.db.WithContext(ctx).Model(&model.IotCard{}).Where("id = ?", card.ID).Limit(1).Pluck("asset_status", &statuses).Error; err != nil || len(statuses) == 0 {
|
||
return
|
||
}
|
||
card.AssetStatus = statuses[0]
|
||
if err := b.redis.HSet(ctx, key, "asset_status", card.AssetStatus).Err(); err != nil {
|
||
b.logger.Debug("回写卡缓存资产状态失败", zap.Uint("card_id", card.ID), zap.Error(err))
|
||
}
|
||
}
|
||
|
||
// updateCardCache 更新 Redis 卡信息缓存中的指定字段
|
||
func (b *PollingBase) updateCardCache(ctx context.Context, cardID uint, updates map[string]any) {
|
||
key := constants.RedisPollingCardInfoKey(cardID)
|
||
args := make([]any, 0, len(updates)*2)
|
||
for k, v := range updates {
|
||
args = append(args, k, v)
|
||
}
|
||
if len(args) > 0 {
|
||
if err := b.redis.HSet(ctx, key, args...).Err(); err != nil {
|
||
b.logger.Warn("更新卡缓存失败", zap.Uint("card_id", cardID), zap.Error(err))
|
||
}
|
||
}
|
||
}
|
||
|
||
// invalidateCardCache 删除轮询卡缓存,确保后续从数据库重建
|
||
func (b *PollingBase) invalidateCardCache(ctx context.Context, cardID uint) {
|
||
key := constants.RedisPollingCardInfoKey(cardID)
|
||
if err := b.redis.Del(ctx, key).Err(); err != nil {
|
||
b.logger.Warn("删除卡缓存失败", zap.Uint("card_id", cardID), zap.Error(err))
|
||
}
|
||
}
|
||
|
||
// parseCardFromCache 从 Redis Hash 反序列化卡信息
|
||
func parseCardFromCache(cardID uint, result map[string]string) *model.IotCard {
|
||
card := &model.IotCard{}
|
||
card.ID = cardID
|
||
if v, ok := result["iccid"]; ok {
|
||
card.ICCID = v
|
||
}
|
||
if v, ok := result["card_category"]; ok {
|
||
card.CardCategory = v
|
||
}
|
||
if v, ok := result["real_name_status"]; ok {
|
||
if status, err := strconv.Atoi(v); err == nil {
|
||
card.RealNameStatus = status
|
||
}
|
||
}
|
||
if v, ok := result["network_status"]; ok {
|
||
if status, err := strconv.Atoi(v); err == nil {
|
||
card.NetworkStatus = status
|
||
}
|
||
}
|
||
if v, ok := result["gateway_extend"]; ok {
|
||
card.GatewayExtend = v
|
||
}
|
||
if v, ok := result["carrier_id"]; ok {
|
||
if id, err := strconv.ParseUint(v, 10, 64); err == nil {
|
||
card.CarrierID = uint(id)
|
||
}
|
||
}
|
||
if v, ok := result["current_month_usage_mb"]; ok {
|
||
if usage, err := strconv.ParseFloat(v, 64); err == nil {
|
||
card.CurrentMonthUsageMB = usage
|
||
}
|
||
}
|
||
if v, ok := result["last_gateway_reading_mb"]; ok {
|
||
if reading, err := strconv.ParseFloat(v, 64); err == nil {
|
||
card.LastGatewayReadingMB = reading
|
||
}
|
||
}
|
||
if v, ok := result["data_usage_mb"]; ok {
|
||
if usage, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||
card.DataUsageMB = usage
|
||
}
|
||
}
|
||
if v, ok := result["stop_reason"]; ok {
|
||
card.StopReason = v
|
||
}
|
||
if v, ok := result["asset_status"]; ok {
|
||
if status, err := strconv.Atoi(v); err == nil {
|
||
card.AssetStatus = status
|
||
}
|
||
}
|
||
if v, ok := result["is_standalone"]; ok {
|
||
card.IsStandalone = v == "1" || v == "true"
|
||
} else {
|
||
card.IsStandalone = true
|
||
}
|
||
if v, ok := result["series_id"]; ok {
|
||
if id, err := strconv.ParseUint(v, 10, 64); err == nil {
|
||
seriesID := uint(id)
|
||
card.SeriesID = &seriesID
|
||
}
|
||
}
|
||
if v, ok := result["current_month_start_date"]; ok {
|
||
if ts, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||
t := time.Unix(ts, 0)
|
||
card.CurrentMonthStartDate = &t
|
||
}
|
||
}
|
||
return card
|
||
}
|
||
|
||
// writeCardToCache 将卡信息异步写入 Redis 缓存(7 天 TTL)
|
||
func writeCardToCache(b *PollingBase, key string, card *model.IotCard) {
|
||
cacheCtx := context.Background()
|
||
cacheData := map[string]any{
|
||
"id": card.ID,
|
||
"iccid": card.ICCID,
|
||
"card_category": card.CardCategory,
|
||
"real_name_status": card.RealNameStatus,
|
||
"network_status": card.NetworkStatus,
|
||
"gateway_extend": card.GatewayExtend,
|
||
"carrier_id": card.CarrierID,
|
||
"current_month_usage_mb": card.CurrentMonthUsageMB,
|
||
"last_gateway_reading_mb": card.LastGatewayReadingMB,
|
||
"data_usage_mb": card.DataUsageMB,
|
||
"stop_reason": card.StopReason,
|
||
"is_standalone": boolToStr(card.IsStandalone),
|
||
"asset_status": card.AssetStatus,
|
||
"cached_at": time.Now().Unix(),
|
||
}
|
||
if card.SeriesID != nil {
|
||
cacheData["series_id"] = *card.SeriesID
|
||
}
|
||
if card.CurrentMonthStartDate != nil {
|
||
cacheData["current_month_start_date"] = card.CurrentMonthStartDate.Unix()
|
||
}
|
||
pipe := b.redis.Pipeline()
|
||
pipe.HSet(cacheCtx, key, cacheData)
|
||
pipe.Expire(cacheCtx, key, 7*24*time.Hour)
|
||
_, _ = pipe.Exec(cacheCtx)
|
||
}
|