OmgQueryThrottle.java 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. package com.ruoyi.app.utils.omg;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.data.redis.core.StringRedisTemplate;
  4. import org.springframework.stereotype.Component;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7. * OMG queryTrade 节流(防 OMG 按 MerchantID HTTP 403 自残)。
  8. *
  9. * <p>OMG 的 403 是"短时间内反复查同一笔"触发(频率),不是"下单后 N 分钟内不能查"。故只做 <b>频率</b> 节流、
  10. * 不做时间黑名单——已付款订单可立即查状态(notify 丢失时 /query 立刻补单)。原"40min 首查延迟"是对 OMG 节流的
  11. * 误读(会让已付客户等 40min 才看到状态变更,不可接受),已移除。两道闸,均基于 Redis {@code SET NX EX}(原子,无锁竞争):
  12. * <ol>
  13. * <li>per-MerchantID 令牌桶:同一商店代号每 interval(默认 3s)只放行一次 queryTrade(burst 1),
  14. * 非阻塞——拿不到令牌跳过本行本轮。</li>
  15. * <li>per-ddId 扫描间隔:{@code /query} 默认 60s、定时默认 180s 内不重复查同一订单(挡前端 2s 轮询放大)。</li>
  16. * </ol>
  17. * 参数 §9 待 OMG 官方确认后据实调整。
  18. */
  19. @Component
  20. public class OmgQueryThrottle {
  21. @Value("${omg.query-throttle.merchant-token-seconds:3}")
  22. private int merchantTokenSeconds;
  23. @Value("${omg.query-throttle.query-dd-seconds:60}")
  24. private int queryDdSeconds;
  25. @Value("${omg.query-throttle.reconcile-dd-seconds:180}")
  26. private int reconcileDdSeconds;
  27. private final StringRedisTemplate redis;
  28. public OmgQueryThrottle(StringRedisTemplate redis) {
  29. this.redis = redis;
  30. }
  31. /** per-MerchantID 令牌桶(1 token / merchantTokenSeconds,burst 1)。true=已占位可查;false=限流跳过本行本轮。 */
  32. public boolean tryAcquireMerchantToken(String merchantId) {
  33. if (merchantId == null || merchantId.isEmpty()) {
  34. return true;
  35. }
  36. String key = "omg:qt:token:" + merchantId;
  37. Boolean ok = redis.opsForValue().setIfAbsent(key, "1", merchantTokenSeconds, TimeUnit.SECONDS);
  38. return Boolean.TRUE.equals(ok);
  39. }
  40. /** per-ddId 扫描间隔。source="query"→/query(queryDdSeconds);其他→定时(reconcileDdSeconds)。
  41. * true=可处理;false=窗口内重复,跳过本轮。 */
  42. public boolean acquireDdIdSlot(String ddId, String source) {
  43. if (ddId == null || ddId.isEmpty()) {
  44. return true;
  45. }
  46. boolean passive = "query".equals(source);
  47. String key = (passive ? "omg:query:dd:" : "omg:reconcile:dd:") + ddId;
  48. long ttl = passive ? queryDdSeconds : reconcileDdSeconds;
  49. Boolean ok = redis.opsForValue().setIfAbsent(key, "1", ttl, TimeUnit.SECONDS);
  50. return Boolean.TRUE.equals(ok);
  51. }
  52. }