package com.ruoyi.app.utils.omg; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; /** * OMG queryTrade 节流(防 OMG 按 MerchantID HTTP 403 自残)。 * *

OMG 的 403 是"短时间内反复查同一笔"触发(频率),不是"下单后 N 分钟内不能查"。故只做 频率 节流、 * 不做时间黑名单——已付款订单可立即查状态(notify 丢失时 /query 立刻补单)。原"40min 首查延迟"是对 OMG 节流的 * 误读(会让已付客户等 40min 才看到状态变更,不可接受),已移除。两道闸,均基于 Redis {@code SET NX EX}(原子,无锁竞争): *

    *
  1. per-MerchantID 令牌桶:同一商店代号每 interval(默认 3s)只放行一次 queryTrade(burst 1), * 非阻塞——拿不到令牌跳过本行本轮。
  2. *
  3. per-ddId 扫描间隔:{@code /query} 默认 60s、定时默认 180s 内不重复查同一订单(挡前端 2s 轮询放大)。
  4. *
* 参数 §9 待 OMG 官方确认后据实调整。 */ @Component public class OmgQueryThrottle { @Value("${omg.query-throttle.merchant-token-seconds:3}") private int merchantTokenSeconds; @Value("${omg.query-throttle.query-dd-seconds:60}") private int queryDdSeconds; @Value("${omg.query-throttle.reconcile-dd-seconds:180}") private int reconcileDdSeconds; private final StringRedisTemplate redis; public OmgQueryThrottle(StringRedisTemplate redis) { this.redis = redis; } /** per-MerchantID 令牌桶(1 token / merchantTokenSeconds,burst 1)。true=已占位可查;false=限流跳过本行本轮。 */ public boolean tryAcquireMerchantToken(String merchantId) { if (merchantId == null || merchantId.isEmpty()) { return true; } String key = "omg:qt:token:" + merchantId; Boolean ok = redis.opsForValue().setIfAbsent(key, "1", merchantTokenSeconds, TimeUnit.SECONDS); return Boolean.TRUE.equals(ok); } /** per-ddId 扫描间隔。source="query"→/query(queryDdSeconds);其他→定时(reconcileDdSeconds)。 * true=可处理;false=窗口内重复,跳过本轮。 */ public boolean acquireDdIdSlot(String ddId, String source) { if (ddId == null || ddId.isEmpty()) { return true; } boolean passive = "query".equals(source); String key = (passive ? "omg:query:dd:" : "omg:reconcile:dd:") + ddId; long ttl = passive ? queryDdSeconds : reconcileDdSeconds; Boolean ok = redis.opsForValue().setIfAbsent(key, "1", ttl, TimeUnit.SECONDS); return Boolean.TRUE.equals(ok); } }