|
|
@@ -4,28 +4,24 @@ import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
-import java.util.Date;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
/**
|
|
|
- * OMG queryTrade 三道节流闸(T055,防 OMG 按 MerchantID HTTP 403 自残)。
|
|
|
+ * OMG queryTrade 节流(防 OMG 按 MerchantID HTTP 403 自残)。
|
|
|
*
|
|
|
- * <p>设计见 {@code specs/016-omg-payment/payment-attempt-lifecycle.md} §6.4【C】。三闸:
|
|
|
+ * <p>OMG 的 403 是"短时间内反复查同一笔"触发(频率),不是"下单后 N 分钟内不能查"。故只做 <b>频率</b> 节流、
|
|
|
+ * 不做时间黑名单——已付款订单可立即查状态(notify 丢失时 /query 立刻补单)。原"40min 首查延迟"是对 OMG 节流的
|
|
|
+ * 误读(会让已付客户等 40min 才看到状态变更,不可接受),已移除。两道闸,均基于 Redis {@code SET NX EX}(原子,无锁竞争):
|
|
|
* <ol>
|
|
|
- * <li>闸1 首查延迟:下单后 N 分钟(默认 40)内不查 OMG(OMG/ECPay 节流,过早查触发 403 罚 30 分钟)。
|
|
|
- * 跳“新”行(创建距今 < 40min),与 OMG 官方“下单后 40 分钟内别查”一致。</li>
|
|
|
- * <li>闸2 per-MerchantID 令牌桶:同一商店代号每 interval(默认 3s)只放行一次 queryTrade(burst 1),
|
|
|
+ * <li>per-MerchantID 令牌桶:同一商店代号每 interval(默认 3s)只放行一次 queryTrade(burst 1),
|
|
|
* 非阻塞——拿不到令牌跳过本行本轮。</li>
|
|
|
- * <li>闸3 per-ddId 扫描间隔:{@code /query} 默认 60s、定时默认 180s 内不重复查同一订单(挡前端 2s 轮询放大)。</li>
|
|
|
+ * <li>per-ddId 扫描间隔:{@code /query} 默认 60s、定时默认 180s 内不重复查同一订单(挡前端 2s 轮询放大)。</li>
|
|
|
* </ol>
|
|
|
- * 全部基于 Redis {@code SET NX EX}(原子),无锁竞争。参数 §9 待 OMG 官方确认后据实调整。
|
|
|
+ * 参数 §9 待 OMG 官方确认后据实调整。
|
|
|
*/
|
|
|
@Component
|
|
|
public class OmgQueryThrottle {
|
|
|
|
|
|
- @Value("${omg.query-throttle.first-query-minutes:40}")
|
|
|
- private int firstQueryMinutes;
|
|
|
-
|
|
|
@Value("${omg.query-throttle.merchant-token-seconds:3}")
|
|
|
private int merchantTokenSeconds;
|
|
|
|
|
|
@@ -41,16 +37,7 @@ public class OmgQueryThrottle {
|
|
|
this.redis = redis;
|
|
|
}
|
|
|
|
|
|
- /** 闸1:该流水是否仍在首查延迟窗口内(创建距今 < firstQueryMinutes 分钟 → 跳过不查)。未知创建时间不拦。 */
|
|
|
- public boolean isWithinFirstQueryDelay(Date createTime) {
|
|
|
- if (createTime == null) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- long ageMs = System.currentTimeMillis() - createTime.getTime();
|
|
|
- return ageMs < firstQueryMinutes * 60_000L;
|
|
|
- }
|
|
|
-
|
|
|
- /** 闸2:per-MerchantID 令牌桶(1 token / merchantTokenSeconds,burst 1)。true=已占位可查;false=限流跳过本行本轮。 */
|
|
|
+ /** per-MerchantID 令牌桶(1 token / merchantTokenSeconds,burst 1)。true=已占位可查;false=限流跳过本行本轮。 */
|
|
|
public boolean tryAcquireMerchantToken(String merchantId) {
|
|
|
if (merchantId == null || merchantId.isEmpty()) {
|
|
|
return true;
|
|
|
@@ -60,8 +47,8 @@ public class OmgQueryThrottle {
|
|
|
return Boolean.TRUE.equals(ok);
|
|
|
}
|
|
|
|
|
|
- /** 闸3:per-ddId 扫描间隔。source="query"→/query(queryDdSeconds);其他→定时(reconcileDdSeconds)。
|
|
|
- * true=可处理;false=窗口内重复,跳过本轮。 */
|
|
|
+ /** per-ddId 扫描间隔。source="query"→/query(queryDdSeconds);其他→定时(reconcileDdSeconds)。
|
|
|
+ * true=可处理;false=窗口内重复,跳过本轮。 */
|
|
|
public boolean acquireDdIdSlot(String ddId, String source) {
|
|
|
if (ddId == null || ddId.isEmpty()) {
|
|
|
return true;
|