Files
junhong_cmp_fiber/internal/task/polling_card_cache.go
huang 0cba6fd6d5
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
fix: 修复轮询缓存竞态导致流量增量重复计全量的问题
getCardWithCache 在缓存 miss 时将 writeCardToCache 由异步协程改为同步调用。
原实现中协程可能在 updateCardCache 之后才被调度执行,将
last_gateway_reading_mb 覆盖回 DB 旧值(0),导致下次轮询
increment = gatewayFlowMB - 0 = 全量,触发套餐流量重复扣减。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 18:33:46 +08:00

140 lines
4.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 {
return parseCardFromCache(cardID, result), 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
}
// 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))
}
}
}
// 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["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["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,
"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),
"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)
}