Files
junhong_cmp_fiber/internal/task/polling_card_cache.go
邹进 98017ac053
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
feat: 分层轮询、重置日前加密与激活前流量结算
- 轮询配置新增套餐状态、资产状态、重置日前窗口与窗口内流量间隔(迁移 248),间隔三态:正数按间隔、0 不检查、空继承
- 配置按 priority, id 排序合并;在库与已销售无套餐未匹配时告警不入队
- 新增优先级整体重排、匹配预览、立即重算到期时间(异步只提前)、分层统计与重置日前加密统计接口
- 已切换日快照计量的卡流量按观测时间归属,激活前用量只记已结算
- 订单购买与实名激活补齐资产级激活锁,激活前对已切换卡结算一次流量,失败只告警不阻塞,结算不触发通道阈值判定
2026-10-24 17:06:38 +09:00

175 lines
5.6 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"
"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)
}