Files
junhong_cmp_fiber/internal/task/polling_card_cache.go
邹进 a855742752
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m32s
fix: 关闭轮询的卡不再重新入队
关闭轮询时会移出队列并清除卡缓存,但执行收尾重入队、卡状态检查补齐套餐任务、限流重入队
都不判断轮询开关,执行中途被关闭的卡会被放回队列并互相补齐,关闭轮询无法生效。

- 卡缓存写入 enable_polling,旧缓存缺字段时从库补读并回写
- 重入队、补齐缺失任务、限流重入队前判断轮询开关,已关闭则不入队
2026-10-24 18:10:58 +09:00

187 lines
6.2 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)
_, hasAssetStatus := result["asset_status"]
_, hasEnablePolling := result["enable_polling"]
if !hasAssetStatus || !hasEnablePolling {
b.backfillCardFields(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
}
// backfillCardFields 为旧缓存补齐资产状态与轮询开关:只读取并回写这两个字段,
// 不整条重建缓存,避免覆盖缓存中单独维护的读数等字段。读取失败时保留缓存解析结果(轮询开关按开启)。
func (b *PollingBase) backfillCardFields(ctx context.Context, key string, card *model.IotCard) {
var rows []struct {
AssetStatus int
EnablePolling bool
}
if err := b.db.WithContext(ctx).Model(&model.IotCard{}).Select("asset_status, enable_polling").
Where("id = ?", card.ID).Limit(1).Scan(&rows).Error; err != nil || len(rows) == 0 {
return
}
card.AssetStatus, card.EnablePolling = rows[0].AssetStatus, rows[0].EnablePolling
if err := b.redis.HSet(ctx, key, "asset_status", card.AssetStatus, "enable_polling", boolToStr(card.EnablePolling)).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
}
}
// 轮询开关:所有关闭/开启路径都会清除卡缓存,缓存中的取值与库一致;旧缓存缺字段时按开启并由读取方回填。
card.EnablePolling = true
if v, ok := result["enable_polling"]; ok {
card.EnablePolling = v == "1" || v == "true"
}
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),
"enable_polling": boolToStr(card.EnablePolling),
"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)
}