修复缓存没有更新的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m13s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m13s
This commit is contained in:
@@ -18,7 +18,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const listCountCacheTTL = 30 * time.Minute
|
||||
const listCountCacheTTL = 30 * time.Second
|
||||
|
||||
type IotCardStore struct {
|
||||
db *gorm.DB
|
||||
@@ -33,14 +33,22 @@ func NewIotCardStore(db *gorm.DB, redis *redis.Client) *IotCardStore {
|
||||
}
|
||||
|
||||
func (s *IotCardStore) Create(ctx context.Context, card *model.IotCard) error {
|
||||
return s.db.WithContext(ctx).Create(card).Error
|
||||
if err := s.db.WithContext(ctx).Create(card).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) CreateBatch(ctx context.Context, cards []*model.IotCard) error {
|
||||
if len(cards) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.db.WithContext(ctx).CreateInBatches(cards, 100).Error
|
||||
if err := s.db.WithContext(ctx).CreateInBatches(cards, 100).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) GetByID(ctx context.Context, id uint) (*model.IotCard, error) {
|
||||
@@ -162,7 +170,11 @@ func (s *IotCardStore) ExistsByICCIDBatch(ctx context.Context, iccids []string)
|
||||
}
|
||||
|
||||
func (s *IotCardStore) Update(ctx context.Context, card *model.IotCard) error {
|
||||
return s.db.WithContext(ctx).Save(card).Error
|
||||
if err := s.db.WithContext(ctx).Save(card).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateFields 按 ID 部分更新卡字段
|
||||
@@ -185,9 +197,13 @@ func (s *IotCardStore) UpdateFields(ctx context.Context, cardID uint, fields map
|
||||
}
|
||||
}
|
||||
|
||||
return s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ?", cardID).
|
||||
Updates(updates).Error
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldSyncActivationStatus(fields map[string]any) bool {
|
||||
@@ -299,7 +315,11 @@ func (s *IotCardStore) UpdateRealnamePolicy(ctx context.Context, cardID uint, re
|
||||
}
|
||||
|
||||
func (s *IotCardStore) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).Delete(&model.IotCard{}, id).Error
|
||||
if err := s.db.WithContext(ctx).Delete(&model.IotCard{}, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
|
||||
@@ -362,13 +382,13 @@ func (s *IotCardStore) List(ctx context.Context, opts *store.QueryOptions, filte
|
||||
query = query.Where("series_id = ?", seriesID)
|
||||
}
|
||||
|
||||
if cachedTotal, ok := s.getCachedCount(ctx, "iot_card", filters); ok {
|
||||
if cachedTotal, ok, cacheVersion := s.getCachedCount(ctx, "iot_card", filters); ok {
|
||||
total = cachedTotal
|
||||
} else {
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
s.cacheCount(ctx, "iot_card", filters, total)
|
||||
s.cacheCount(ctx, "iot_card", filters, total, cacheVersion)
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
@@ -450,13 +470,13 @@ func (s *IotCardStore) listStandaloneTwoPhase(ctx context.Context, opts *store.Q
|
||||
query = middleware.ApplyShopFilter(ctx, query)
|
||||
query = s.applyStandaloneFilters(ctx, query, filters)
|
||||
|
||||
if cachedTotal, ok := s.getCachedCount(ctx, "standalone", filters); ok {
|
||||
if cachedTotal, ok, cacheVersion := s.getCachedCount(ctx, "standalone", filters); ok {
|
||||
total = cachedTotal
|
||||
} else {
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
s.cacheCount(ctx, "standalone", filters, total)
|
||||
s.cacheCount(ctx, "standalone", filters, total, cacheVersion)
|
||||
}
|
||||
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
@@ -511,13 +531,13 @@ func (s *IotCardStore) listStandaloneDefault(ctx context.Context, opts *store.Qu
|
||||
query = middleware.ApplyShopFilter(ctx, query)
|
||||
query = s.applyStandaloneFilters(ctx, query, filters)
|
||||
|
||||
if cachedTotal, ok := s.getCachedCount(ctx, "standalone", filters); ok {
|
||||
if cachedTotal, ok, cacheVersion := s.getCachedCount(ctx, "standalone", filters); ok {
|
||||
total = cachedTotal
|
||||
} else {
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
s.cacheCount(ctx, "standalone", filters, total)
|
||||
s.cacheCount(ctx, "standalone", filters, total, cacheVersion)
|
||||
}
|
||||
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
@@ -559,7 +579,7 @@ func (s *IotCardStore) listStandaloneParallel(ctx context.Context, opts *store.Q
|
||||
results := make([]shopResult, len(shopIDs))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cachedTotal, hasCachedTotal := s.getCachedCount(ctx, "standalone", filters)
|
||||
cachedTotal, hasCachedTotal, cacheVersion := s.getCachedCount(ctx, "standalone", filters)
|
||||
|
||||
for i, shopID := range shopIDs {
|
||||
wg.Add(1)
|
||||
@@ -613,7 +633,7 @@ func (s *IotCardStore) listStandaloneParallel(ctx context.Context, opts *store.Q
|
||||
}
|
||||
|
||||
if !hasCachedTotal && totalCount > 0 {
|
||||
s.cacheCount(ctx, "standalone", filters, totalCount)
|
||||
s.cacheCount(ctx, "standalone", filters, totalCount, cacheVersion)
|
||||
}
|
||||
|
||||
sort.Slice(allCards, func(i, j int) bool {
|
||||
@@ -668,7 +688,7 @@ func (s *IotCardStore) listStandaloneParallelTwoPhase(ctx context.Context, opts
|
||||
results := make([]shopResult, len(shopIDs))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cachedTotal, hasCachedTotal := s.getCachedCount(ctx, "standalone", filters)
|
||||
cachedTotal, hasCachedTotal, cacheVersion := s.getCachedCount(ctx, "standalone", filters)
|
||||
|
||||
// Phase 1: 并行获取每个 shop 的 (id, created_at),走覆盖索引
|
||||
for i, shopID := range shopIDs {
|
||||
@@ -723,7 +743,7 @@ func (s *IotCardStore) listStandaloneParallelTwoPhase(ctx context.Context, opts
|
||||
}
|
||||
|
||||
if !hasCachedTotal && totalCount > 0 {
|
||||
s.cacheCount(ctx, "standalone", filters, totalCount)
|
||||
s.cacheCount(ctx, "standalone", filters, totalCount, cacheVersion)
|
||||
}
|
||||
|
||||
sort.Slice(allIDs, func(i, j int) bool {
|
||||
@@ -1006,9 +1026,13 @@ func (s *IotCardStore) BatchUpdateShopIDAndStatus(ctx context.Context, cardIDs [
|
||||
"shop_id": shopID,
|
||||
"status": status,
|
||||
}
|
||||
return s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id IN ?", cardIDs).
|
||||
Updates(updates).Error
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) GetBoundCardIDs(ctx context.Context, cardIDs []uint) ([]uint, error) {
|
||||
@@ -1157,9 +1181,13 @@ func (s *IotCardStore) BatchDelete(ctx context.Context, cardIDs []uint) error {
|
||||
if len(cardIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.db.WithContext(ctx).
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("id IN ?", cardIDs).
|
||||
Delete(&model.IotCard{}).Error
|
||||
Delete(&model.IotCard{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExistsByVirtualNoBatch 批量检查 virtual_no 是否已存在
|
||||
@@ -1243,24 +1271,73 @@ func (s *IotCardStore) ListForPollingBatch(ctx context.Context, lastID uint, lim
|
||||
|
||||
// ==================== 列表计数缓存 ====================
|
||||
|
||||
func (s *IotCardStore) getCachedCount(ctx context.Context, table string, filters map[string]any) (int64, bool) {
|
||||
func (s *IotCardStore) getCachedCount(ctx context.Context, table string, filters map[string]any) (int64, bool, int64) {
|
||||
if s.redis == nil {
|
||||
return 0, false
|
||||
return 0, false, 0
|
||||
}
|
||||
key := constants.RedisListCountKey(table, middleware.GetUserIDFromContext(ctx), hashFilters(filters))
|
||||
version := s.listCountCacheVersion(ctx, table)
|
||||
key := s.listCountCacheKey(ctx, table, filters, version)
|
||||
val, err := s.redis.Get(ctx, key).Int64()
|
||||
if err != nil {
|
||||
return 0, false
|
||||
return 0, false, version
|
||||
}
|
||||
return val, true
|
||||
return val, true, version
|
||||
}
|
||||
|
||||
func (s *IotCardStore) cacheCount(ctx context.Context, table string, filters map[string]any, total int64) {
|
||||
func (s *IotCardStore) cacheCount(ctx context.Context, table string, filters map[string]any, total int64, version int64) {
|
||||
if s.redis == nil {
|
||||
return
|
||||
}
|
||||
key := constants.RedisListCountKey(table, middleware.GetUserIDFromContext(ctx), hashFilters(filters))
|
||||
s.redis.Set(ctx, key, total, listCountCacheTTL)
|
||||
if s.listCountCacheVersion(ctx, table) != version {
|
||||
return
|
||||
}
|
||||
key := s.listCountCacheKey(ctx, table, filters, version)
|
||||
_ = s.redis.Set(ctx, key, total, listCountCacheTTL).Err()
|
||||
}
|
||||
|
||||
func (s *IotCardStore) listCountCacheKey(ctx context.Context, table string, filters map[string]any, version int64) string {
|
||||
return constants.RedisListCountKey(
|
||||
table,
|
||||
version,
|
||||
middleware.GetUserIDFromContext(ctx),
|
||||
hashDataScope(ctx),
|
||||
hashFilters(filters),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *IotCardStore) listCountCacheVersion(ctx context.Context, table string) int64 {
|
||||
version, err := s.redis.Get(ctx, constants.RedisListCountVersionKey(table)).Int64()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// InvalidateListCountCache 失效 IoT 卡相关列表计数缓存。
|
||||
func (s *IotCardStore) InvalidateListCountCache(ctx context.Context) {
|
||||
s.invalidateIotCardListCountCache(ctx)
|
||||
}
|
||||
|
||||
func (s *IotCardStore) invalidateIotCardListCountCache(ctx context.Context) {
|
||||
s.invalidateListCountCache(ctx, "iot_card", "standalone")
|
||||
}
|
||||
|
||||
func (s *IotCardStore) invalidateListCountCache(ctx context.Context, tables ...string) {
|
||||
if s.redis == nil {
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(tables))
|
||||
for _, table := range tables {
|
||||
if table == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[table]; ok {
|
||||
continue
|
||||
}
|
||||
seen[table] = struct{}{}
|
||||
_ = s.redis.Incr(ctx, constants.RedisListCountVersionKey(table)).Err()
|
||||
}
|
||||
}
|
||||
|
||||
func hashFilters(filters map[string]any) string {
|
||||
@@ -1280,3 +1357,27 @@ func hashFilters(filters map[string]any) string {
|
||||
}
|
||||
return fmt.Sprintf("%08x", h.Sum32())
|
||||
}
|
||||
|
||||
func hashDataScope(ctx context.Context) string {
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
|
||||
subordinateIDs := middleware.GetSubordinateShopIDs(ctx)
|
||||
|
||||
h := fnv.New32a()
|
||||
_, _ = fmt.Fprintf(h, "user_type=%d;shop_id=%d;enterprise_id=%d;", userType, shopID, enterpriseID)
|
||||
if subordinateIDs == nil {
|
||||
_, _ = h.Write([]byte("subordinate=nil"))
|
||||
return fmt.Sprintf("%08x", h.Sum32())
|
||||
}
|
||||
|
||||
ids := append([]uint(nil), subordinateIDs...)
|
||||
sort.Slice(ids, func(i, j int) bool {
|
||||
return ids[i] < ids[j]
|
||||
})
|
||||
_, _ = fmt.Fprint(h, "subordinate=")
|
||||
for _, id := range ids {
|
||||
_, _ = fmt.Fprintf(h, "%d,", id)
|
||||
}
|
||||
return fmt.Sprintf("%08x", h.Sum32())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user