All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m32s
关闭轮询时会移出队列并清除卡缓存,但执行收尾重入队、卡状态检查补齐套餐任务、限流重入队 都不判断轮询开关,执行中途被关闭的卡会被放回队列并互相补齐,关闭轮询无法生效。 - 卡缓存写入 enable_polling,旧缓存缺字段时从库补读并回写 - 重入队、补齐缺失任务、限流重入队前判断轮询开关,已关闭则不入队
187 lines
6.2 KiB
Go
187 lines
6.2 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)
|
||
_, 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)
|
||
}
|