All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m5s
152 lines
4.6 KiB
Go
152 lines
4.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 {
|
||
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))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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["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),
|
||
"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)
|
||
}
|