package utils import ( "context" "fmt" "git.hlsq.asia/alex/service-common/db/redis" "git.hlsq.asia/alex/service-common/net/http/httpresp" "google.golang.org/grpc/metadata" "time" ) const ( replayTTL = 2 * time.Minute // 防重放缓存有效期 lockTTL = 10 * time.Second // 分布式锁有效期 ) // WrapPreventReplay 防止重放请求 func WrapPreventReplay[T any](ctx context.Context, usn int64, fun func() (T, error)) (T, error) { var zero T key := "" if md, ok := metadata.FromIncomingContext(ctx); ok { ridArr := md.Get("Request-Id") if len(ridArr) == 0 || ridArr[0] == "" { return fun() } key = fmt.Sprintf("cache:replay:%v:%v", usn, ridArr[0]) } else { return fun() } // 先查缓存,命中直接返回 if val := getReplayCache[T](ctx, key); val != nil { return *val, nil } // 尝试抢分布式锁,抢到就执行,没抢到报错 lockKey := fmt.Sprintf("%s:lock", key) locked, err := redis.GetClient().SetNX(ctx, lockKey, "1", lockTTL).Result() if err == nil && locked { defer redis.GetClient().Del(ctx, lockKey) result, err := fun() // 这里报错了是没有缓存的 if err != nil { return zero, err } redis.GetCacheClient().Set(ctx, key, result, replayTTL) return result, nil } return zero, httpresp.RequestTooFast } // getReplayCache 从 Redis 读取重放缓存 func getReplayCache[T any](ctx context.Context, key string) *T { var dst T if redis.GetCacheClient().Get(ctx, key, &dst) { return &dst } return nil }