补单提速第二轮+控台指令最小化信息透露
问题1 补单10秒才抓到单(根因两条): - 线程错位: TimerA-周期批处理跑在sdf1-db-worker上, triggerFastPoller在该线程同步doGet, 撞sendHard的10秒硬超时 -> 每次补单固定10秒且返回null, 还吃光DB队列整轮预算; - 节奏不符设计: 定时器是固定5秒而非0~5秒随机, 4秒限流又把随机节奏里<4秒的轮次整轮吞掉。 修法: 定时器改0~100tick随机(nextPollerDelayTicks); 限流4000ms->500ms; HTTP挪到补单专用单线程pollerExecutor(fire-and-forget, 队列容量1+丢弃); 补单改8秒短超时POLLER_TIMEOUT_S; 本轮processed>0立刻pullPendingTransactions入账; shutdown补关pollerExecutor。 问题2 conmsg/opwl对玩家泄露"仅控制台可用/需要控台权限": 从plugin.yml摘掉注册 -> 玩家敲它必然落进UnknownCommandEvent, 与"不存在的指令" 走完全相同路径(压制原生unknown报错+相似建议候选里永远没有这俩); 控制台由onUnknownCommand里的dispatchConsoleOnlyCommand就地接管执行, onCommand里的opwl/conmsg分支改为兜底且玩家侧一律静默。
This commit is contained in:
@@ -782,9 +782,9 @@ public class Main extends JavaPlugin
|
||||
// ===== OP白名单(opwl,仅控制台;15秒文件热重载 + 整点(:15/:30/:45)定点扫描)=====
|
||||
opWhiteListMgr = new OpWhiteListManager(this);
|
||||
getServer().getPluginManager().registerEvents(opWhiteListMgr, this);
|
||||
if (getCommand("opwl") != null) {
|
||||
getCommand("opwl").setExecutor(this);
|
||||
}
|
||||
// ★ 2026-10-08 最小化信息透露:opwl 不再在 plugin.yml 注册,
|
||||
// 改由 onUnknownCommand 的 dispatchConsoleOnlyCommand 接管(仅控制台),
|
||||
// 玩家侧表现为「这指令不存在」(不进 /help、不进 Tab、不进相似建议)。
|
||||
getCommand("back").setTabCompleter(quickBack);
|
||||
|
||||
// ===== 药水类成就防作弊 =====
|
||||
@@ -3904,6 +3904,18 @@ public class Main extends JavaPlugin
|
||||
? ("玩家/" + ((Player) sender).getName())
|
||||
: sender.getClass().getSimpleName();
|
||||
|
||||
// ---- 0.5) 控制台专属指令(conmsg / opwl)----
|
||||
// ★ 2026-10-08 最小化信息透露:这俩指令已从 plugin.yml 摘除,
|
||||
// 玩家敲它 = 与「不存在的指令」完全一样(走下面第 3 步:压制原生 unknown
|
||||
// 报错、相似建议里也永远不含它们),绝不再回「仅控制台可用 / 需要控台权限」
|
||||
// —— 那等于告诉玩家「有指令、只是你没权限」,反而会勾起攻打控制台抢权限。
|
||||
// 控制台侧则在这里就地接管执行,功能不变。
|
||||
if (!isPlayer && dispatchConsoleOnlyCommand(sender, body)) {
|
||||
suppressUnknown(e);
|
||||
getLogger().info("[无感切换] 控制台专属指令已执行: \"" + body + "\"");
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 1) 命中我们 4 插件的指令 -> 大小写不敏感直接执行 ----
|
||||
if (dispatchOwnCommand(sender, body)) {
|
||||
getLogger().info("[无感切换] 已接管并执行: \"" + body
|
||||
@@ -4985,6 +4997,105 @@ public class Main extends JavaPlugin
|
||||
*/
|
||||
|
||||
|
||||
// ===== 控制台专属指令(conmsg / opwl):2026-10-08 最小化信息透露 =====
|
||||
//
|
||||
// 【为什么从 plugin.yml 摘掉注册】只要插件注册了指令,Bukkit 就认识它:
|
||||
// 玩家敲 /opwl 会命中 onCommand,我们只能回一句「仅控制台可用 / 需要控台权限」——
|
||||
// 这等于亲口告诉玩家「有这么个指令,只是你没权限」,反而可能勾起攻打控制台
|
||||
// 抢权限的念头;而且它还会出现在 /help、Tab 补全和相似指令建议里。
|
||||
// 摘掉注册后,玩家敲它会落到 UnknownCommandEvent,走与「不存在的指令」完全
|
||||
// 相同的路径:suppressUnknown 压掉原生报错 + sendSimilarHint 的候选里永远
|
||||
// 没有 conmsg/opwl(filterByPermission 双保险)=> 玩家的体感就是
|
||||
// 「这指令不存在」,一个字都不多说。
|
||||
// 控制台则由下面的 dispatchConsoleOnlyCommand 就地接管执行,功能一点不减。
|
||||
|
||||
/**
|
||||
* 控制台专属指令(conmsg / opwl)就地分发。
|
||||
*
|
||||
* <p><b>★ 认领了就一定返回 true</b>(哪怕执行时抛异常)——否则会漏到
|
||||
* {@code handleConsoleMiss},被当成「运维手打的话」广播给全服。
|
||||
*
|
||||
* @return true = 已接管执行(调用方负责 suppressUnknown);
|
||||
* false = 不归我管,交回 UnknownCommandEvent 的通用流程
|
||||
*/
|
||||
private boolean dispatchConsoleOnlyCommand(
|
||||
org.bukkit.command.CommandSender sender, String body) {
|
||||
// 玩家一律不接管:让他们走「指令不存在」的通用路径,一个字都不提示
|
||||
if (sender instanceof Player) return false;
|
||||
|
||||
String label;
|
||||
String rest;
|
||||
try {
|
||||
int sp = body.indexOf(' ');
|
||||
label = (sp < 0 ? body : body.substring(0, sp)).trim();
|
||||
rest = sp < 0 ? "" : body.substring(sp + 1);
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
if (label.startsWith("/")) label = label.substring(1);
|
||||
label = label.toLowerCase(java.util.Locale.ROOT);
|
||||
int colon = label.lastIndexOf(':');
|
||||
if (colon >= 0 && colon < label.length() - 1) label = label.substring(colon + 1);
|
||||
|
||||
boolean ours = "opwl".equals(label) || "conmsg".equals(label);
|
||||
if (!ours) return false;
|
||||
|
||||
try {
|
||||
String[] args = splitArgs(rest);
|
||||
if ("opwl".equals(label)) {
|
||||
if (opWhiteListMgr == null) {
|
||||
sender.sendMessage("§c[OP白名单] 模块未加载,指令暂不可用。");
|
||||
} else {
|
||||
opWhiteListMgr.onCommand(sender, args);
|
||||
}
|
||||
} else {
|
||||
runConmsg(sender, args);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
getLogger().warning("[无感切换] 控制台专属指令执行失败: "
|
||||
+ t.getClass().getSimpleName() + " - " + t.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 按空白切参(连续空格自动合并;空串返回空数组) */
|
||||
private static String[] splitArgs(String rest) {
|
||||
rest = rest == null ? "" : rest.trim();
|
||||
return rest.isEmpty() ? new String[0] : rest.split("\\s+");
|
||||
}
|
||||
|
||||
/**
|
||||
* /conmsg 执行体(设置控制台发言署名)。
|
||||
* ★ 2026-10-08:原来玩家分支的「这个指令只有控制台能用哦 / 这是控制台用的指令」
|
||||
* 提示已全部删除 —— 指令不在 plugin.yml 里,玩家根本走不到这里,也不该走到。
|
||||
*/
|
||||
private boolean runConmsg(org.bukkit.command.CommandSender sender, String[] args) {
|
||||
String nm = args.length > 0 ? args[0].trim() : "";
|
||||
// ★ 用户名只做 & 色码兼容,**不支持换行**:
|
||||
// 换行/制表一律压成空格(署名占一行,多行会破聊天框版式);
|
||||
// 色码保留,由 parseLegacyOneLine 统一按 & 解析。
|
||||
nm = nm.replaceAll("[\\r\\n\\t]+", " ").trim();
|
||||
// ★ "clear" 是 conmsg 的保留子参数 = 恢复出厂设置(署名回到默认"管理员")。
|
||||
// 只在本方法拦:控制台直接敲 /clear 仍走原版清背包,插件不接管。
|
||||
if ("clear".equalsIgnoreCase(nm)) {
|
||||
consoleName = null;
|
||||
sender.sendMessage("§a已恢复控制台发言署名为默认:§f" + getConsoleName());
|
||||
sender.sendMessage("§7(想清背包请直接敲 §f/clear §7,那是原版指令)");
|
||||
getLogger().info("[无感切换] 控制台署名已恢复默认");
|
||||
return true;
|
||||
}
|
||||
if (nm.isEmpty()) {
|
||||
sender.sendMessage("§e用法: §f/conmsg <名字> §e或 §f/conmsg clear §8(恢复默认)");
|
||||
sender.sendMessage("§7当前署名: §f" + getConsoleName());
|
||||
} else {
|
||||
if (nm.length() > 16) nm = nm.substring(0, 16);
|
||||
consoleName = nm;
|
||||
sender.sendMessage("§a已设置控制台发言署名为: §f" + nm);
|
||||
getLogger().info("[无感切换] 控制台署名已改为: " + nm);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleBanCommand(String rawCommand) {
|
||||
if (rawCommand.isEmpty()) return;
|
||||
String lower = rawCommand.toLowerCase();
|
||||
@@ -7301,55 +7412,25 @@ public class Main extends JavaPlugin
|
||||
|
||||
String cmdName = cmd.getName().toLowerCase();
|
||||
|
||||
// ===== /opwl:OP白名单(仅控制台;玩家调用给提示)=====
|
||||
// ===== /opwl:OP白名单(仅控制台)=====
|
||||
// ★ 2026-10-08 最小化信息透露:opwl 已从 plugin.yml 摘除,正常走不到这里;
|
||||
// 此处只做兜底,玩家侧一律静默 —— 绝不再回「OP白名单仅控制台可用」,
|
||||
// 那等于告诉玩家「有这个指令、只是你没权限」,反而会勾起攻打控制台的念头。
|
||||
if (cmdName.equals("opwl")) {
|
||||
if (!(sender instanceof Player) && opWhiteListMgr != null) {
|
||||
opWhiteListMgr.onCommand(sender, args);
|
||||
} else if (sender instanceof Player) {
|
||||
sender.sendMessage("§7OP白名单仅控制台可用;玩家 §f/op §7已被插件接管,"
|
||||
+ "需要授权请联系管理员在控制台执行 §f/opwl add <玩家>§7,"
|
||||
+ "或直接编辑 §fOP白名单/opwl.json §7(强制手段,不查注册)");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /conmsg <名字>:控制台发言署名(仅控制台可用)=====
|
||||
// ===== /conmsg <名字>:控制台发言署名(仅控制台)=====
|
||||
// ★ 2026-10-08 最小化信息透露:conmsg 已从 plugin.yml 摘除,正常走不到这里;
|
||||
// 此处只做兜底(万一有人把注册加回来),玩家侧一律静默,不给任何提示。
|
||||
if (cmdName.equals("conmsg")) {
|
||||
if (!(sender instanceof Player)) {
|
||||
String nm = args.length > 0 ? args[0].trim() : "";
|
||||
// ★ 用户名只做 & 色码兼容,**不支持换行**:
|
||||
// 换行/制表一律压成空格(署名占一行,多行会破聊天框版式);
|
||||
// 色码保留,由 parseLegacyOneLine 统一按 & 解析。
|
||||
nm = nm.replaceAll("[\\r\\n\\t]+", " ").trim();
|
||||
// ★ "clear" 是 conmsg 的保留子参数 = 恢复出厂设置(署名回到默认"管理员")。
|
||||
// 只在本分支拦:控制台/玩家直接敲 /clear 仍走原版清背包,插件不接管。
|
||||
if ("clear".equalsIgnoreCase(nm)) {
|
||||
consoleName = null;
|
||||
sender.sendMessage("§a已恢复控制台发言署名为默认:§f"
|
||||
+ getConsoleName());
|
||||
sender.sendMessage("§7(想清背包请直接敲 §f/clear §7,那是原版指令)");
|
||||
getLogger().info("[无感切换] 控制台署名已恢复默认");
|
||||
return true;
|
||||
}
|
||||
if (nm.isEmpty()) {
|
||||
sender.sendMessage("§e用法: §f/conmsg <名字> §e或 §f/conmsg clear §8(恢复默认)");
|
||||
sender.sendMessage("§7当前署名: §f" + getConsoleName());
|
||||
} else {
|
||||
if (nm.length() > 16) nm = nm.substring(0, 16);
|
||||
consoleName = nm;
|
||||
sender.sendMessage("§a已设置控制台发言署名为: §f" + nm);
|
||||
getLogger().info("[无感切换] 控制台署名已改为: " + nm);
|
||||
}
|
||||
if (sender instanceof Player) {
|
||||
return true;
|
||||
}
|
||||
Player cp = (Player) sender;
|
||||
if (!isPrivileged(cp)) {
|
||||
cp.sendMessage("§7这个指令只有控制台能用哦");
|
||||
return true;
|
||||
}
|
||||
cp.sendMessage("§7这是控制台用的指令,"
|
||||
+ "直接在服务器控制台输入即可");
|
||||
return true;
|
||||
return runConmsg(sender, args);
|
||||
}
|
||||
|
||||
// ===== /2fa 二次验证绑定(TOTP,全部校验在Java本地)=====
|
||||
|
||||
@@ -1068,6 +1068,15 @@ public class WebManager {
|
||||
* ★ 包含SSL降级逻辑:连续失败3次后降级到HTTP
|
||||
*/
|
||||
private String doGet(String urlStr) {
|
||||
return doGet(urlStr, GET_REQUEST_TIMEOUT_S);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求 - 返回响应体,失败返回null(可指定响应超时秒数)。
|
||||
* ★ 2026-10-08 补单提速:补单链路传 POLLER_TIMEOUT_S(8秒),快判死快重来;
|
||||
* 其余调用仍走 doGet(urlStr) = GET_REQUEST_TIMEOUT_S(15秒),行为完全不变。
|
||||
*/
|
||||
private String doGet(String urlStr, int timeoutSeconds) {
|
||||
// ★ Web通信关闭闸:开关关闭后一律不出网(doGet 是全部 GET 路径的唯一出口)
|
||||
if (!isEnabled()) {
|
||||
return null;
|
||||
@@ -1084,7 +1093,7 @@ public class WebManager {
|
||||
try {
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(urlStr))
|
||||
.timeout(Duration.ofSeconds(GET_REQUEST_TIMEOUT_S)) // ★ 2026-10-06 30→15秒:链路挂起时白等30秒,既慢又刷屏
|
||||
.timeout(Duration.ofSeconds(timeoutSeconds)) // ★ 2026-10-08 按入参超时:普通 15 秒,补单 8 秒(POLLER_TIMEOUT_S)
|
||||
.header("User-Agent", "Sdf1-WebManager/2.9")
|
||||
.header("Accept", "application/json")
|
||||
.GET()
|
||||
@@ -1105,7 +1114,7 @@ public class WebManager {
|
||||
plugin.getLogger().warning("[Web通信] GET HTTP " + resp.statusCode() + ": " + shortUrl + " | 响应: " + shortBody);
|
||||
if (isSecretAuthFailure(body)) {
|
||||
// ★ 密钥错配是唯一值得立即自愈 + 重试的 4xx(其余 4xx 重试无意义)
|
||||
return withSecretAutoHeal(() -> doGet(urlStr));
|
||||
return withSecretAutoHeal(() -> doGet(urlStr, timeoutSeconds));
|
||||
}
|
||||
// 返回body让调用方可以解析PHP错误信息
|
||||
if (body != null && !body.isEmpty()) {
|
||||
@@ -1724,6 +1733,7 @@ public class WebManager {
|
||||
// ★ 关闭Web线程池
|
||||
webExecutor.shutdownNow();
|
||||
httpSideExecutor.shutdownNow(); // 旁路线程池一并关停(daemon线程,不关也能退出)
|
||||
pollerExecutor.shutdownNow(); // 补单专用线程一并关停(daemon线程)
|
||||
// 关闭回调服务器
|
||||
if (callbackServer != null) {
|
||||
try {
|
||||
@@ -2297,24 +2307,39 @@ public class WebManager {
|
||||
* 支持MC服务器和Web服务器不在同一台机器的场景
|
||||
*/
|
||||
/**
|
||||
* ★ 2026-10-08 补单提速:独立补单定时器(固定 5 秒一轮)。
|
||||
* ★ 2026-10-08 补单提速(第二轮):独立补单定时器,节奏 = 0~5 秒随机。
|
||||
*
|
||||
* 补单原来只挂在合并定时器B / TimerA 周期批处理里,而这两条路各自都有「早退守卫」:
|
||||
* - Timer B:PHP 锁库退避、全员下线暂停 → 直接 return,补单被连带跳过;
|
||||
* - Timer A:每 5 轮才投一次,且投在单线程 db-worker 队列上(任务含 10~30 秒级 HTTP);
|
||||
* - 两路再共用一次 15 秒限流。
|
||||
* 三者叠加把实测触发节奏拖到 25~41 秒 → 平均要等约 15 秒才补到单(与用户实测吻合)。
|
||||
* 这里拆出一条独立链路:不受锁库 / 下线 / 队列积压影响,固定节奏触发。
|
||||
* 第一轮把补单从「合并定时器B / TimerA 周期批处理」里拆出来单独跑,是因为那两条路
|
||||
* 各自都有「早退守卫」:Timer B 撞 PHP 锁库 / 全员下线就 return;Timer A 每 5 轮才投
|
||||
* 一次且投在单线程 db-worker 队列上;两路还共用一次 15 秒限流 —— 三者叠加把实测触发
|
||||
* 节奏拖到 25~41 秒,平均要等约 15 秒才补到单。
|
||||
*
|
||||
* 第一轮改成了「固定 5 秒」,用户实测仍然 10 秒才抓到单,根因有两条:
|
||||
* ① 线程错位:TimerA-周期批处理跑在 sdf1-db-worker 上,triggerFastPoller 在该线程里
|
||||
* 同步 doGet,撞上 sendHard 的 10 秒硬超时 —— 每次补单固定耗时 10 秒且返回 null
|
||||
* (日志:[快速补单] 补单无响应(null),耗时: 10004ms;
|
||||
* [DB队列] TimerA-周期批处理 耗时=10002ms(HTTP已按预算让位)),
|
||||
* 还顺带把整轮 DB 队列预算吃光。
|
||||
* ② 4 秒限流会把「0~5 秒随机」节奏里间隔小于 4 秒的轮次整轮吞掉,随机形同虚设。
|
||||
*
|
||||
* 第二轮修复:
|
||||
* - 定时器 0~100 tick(0~5 秒)随机自调度(nextPollerDelayTicks);
|
||||
* - 限流 4 秒 → 500 毫秒(POLLER_MIN_INTERVAL_MS);
|
||||
* - HTTP 挪到补单专用单线程 pollerExecutor,fire-and-forget:调用方永不阻塞,
|
||||
* 不吃 DB 队列预算,也不再受 10 秒硬超时摆布;
|
||||
* - 补单用 8 秒短超时(POLLER_TIMEOUT_S),卡住就判死,下一轮随机节奏立刻重来;
|
||||
* - 这轮真补到新订单(processed>0)就立刻拉单入账,不再等周期批处理来捞。
|
||||
* poller 侧有 flock 进程锁 + 幂等去重,重复触发安全;security.php 对带正确
|
||||
* SECRET_KEY 的 Java 调用不计数,5 秒一轮不会撞全局限流。
|
||||
* SECRET_KEY 的 Java 调用不计数,随机节奏不会撞全局限流。
|
||||
*/
|
||||
private void schedulePollerTick(long ticks) {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 先自调度下一轮再执行本轮:即便 doGet 卡到超时,
|
||||
// 补单节奏也不会被单次慢请求拖垮(服务端有 flock 防并发)
|
||||
schedulePollerTick(POLLER_TICK_TICKS);
|
||||
// 先按 0~5 秒随机自调度下一轮,再执行本轮:
|
||||
// 即便 doGet 卡到超时,补单节奏也不会被单次慢请求拖垮
|
||||
//(服务端有 flock 防并发,补单线程队列容量 1 防堆积)
|
||||
schedulePollerTick(nextPollerDelayTicks());
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
@@ -2323,9 +2348,22 @@ public class WebManager {
|
||||
}.runTaskLaterAsynchronously(plugin, ticks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一轮补单的随机延迟:0~100 tick = 0~5 秒(2026-10-08 用户定案的节奏)。
|
||||
* 随机到 0 时 Bukkit 最快也要等下一个 tick(50ms);即便连续随机到 0,
|
||||
* POLLER_MIN_INTERVAL_MS = 500ms 也会把 HTTP 频次兜在每秒最多 2 次。
|
||||
*/
|
||||
private static long nextPollerDelayTicks() {
|
||||
return (long) (Math.random() * 101);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发一次补单。多个触发源(独立定时器 / 合并B / TimerA 批处理)共用 lastPollerTriggerTime
|
||||
* 统一限流,保证任意时刻都不会并发打满 poller。
|
||||
*
|
||||
* ★ 第二轮:HTTP 移到补单专用线程,本方法立刻返回(fire-and-forget)。
|
||||
* 原来在调用线程里同步 doGet —— TimerA-周期批处理的调用线程是 sdf1-db-worker,
|
||||
* 撞 10 秒硬超时后补单固定 10 秒才返回 null,这就是「10 秒才抓到单」的直接根因。
|
||||
* @param from 触发来源,仅用于日志区分
|
||||
*/
|
||||
private void triggerFastPoller(String from) {
|
||||
@@ -2334,33 +2372,71 @@ public class WebManager {
|
||||
return;
|
||||
}
|
||||
lastPollerTriggerTime = now;
|
||||
try {
|
||||
pollerExecutor.execute(() -> doPollerRequest(from));
|
||||
} catch (java.util.concurrent.RejectedExecutionException ignored) {
|
||||
// 补单线程正忙且队列已满:本轮丢弃,0~5 秒后的随机节奏会再来(flock 保证不并发)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 真正发一次 poller_online.php(只在补单专用线程 sdf1-poller 里跑)。
|
||||
* 捕获到新订单(processed>0)就立刻拉单入账,不再等 TimerA/TimerB 的周期批处理。
|
||||
*/
|
||||
private void doPollerRequest(String from) {
|
||||
try {
|
||||
String pollerUrl = webBaseUrl + "/api/poller_online.php?secret="
|
||||
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
|
||||
long pollerStart = System.currentTimeMillis();
|
||||
String pollerResp = doGet(pollerUrl);
|
||||
String pollerResp = doGet(pollerUrl, POLLER_TIMEOUT_S);
|
||||
long pollerElapsed = System.currentTimeMillis() - pollerStart;
|
||||
|
||||
if (pollerResp != null) {
|
||||
if (pollerResp.contains("\"result\":\"ok\"")) {
|
||||
plugin.getLogger().info("[快速补单] 补单成功 (" + pollerElapsed + "ms, 来源="
|
||||
+ from + "): " + pollerResp.substring(0, Math.min(150, pollerResp.length())));
|
||||
}
|
||||
} else {
|
||||
if (pollerResp == null) {
|
||||
warnSlow("快速补单", "[快速补单] 补单无响应(null),耗时: " + pollerElapsed + "ms, 来源=" + from);
|
||||
return;
|
||||
}
|
||||
if (pollerResp.contains("\"result\":\"ok\"")) {
|
||||
plugin.getLogger().info("[快速补单] 补单成功 (" + pollerElapsed + "ms, 来源="
|
||||
+ from + "): " + pollerResp.substring(0, Math.min(150, pollerResp.length())));
|
||||
}
|
||||
// ★ 快准狠:这轮真补到新订单就立刻拉单入账,
|
||||
// 不用再等 TimerA(15~25秒) / TimerB(0~10秒) 的周期批处理来捞。
|
||||
int processed = jsonInt(pollerResp, "processed");
|
||||
if (processed > 0) {
|
||||
plugin.getLogger().info("[快速补单] 新补 " + processed
|
||||
+ " 笔,立即拉取入账 (来源=" + from + ", " + pollerElapsed + "ms)");
|
||||
pullPendingTransactions();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 补单失败不影响交易拉取,静默忽略
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 JSON 文本里取一个整数字段(取不到返回 -1)。例:{"result":"ok","processed":3} */
|
||||
private static int jsonInt(String json, String key) {
|
||||
int i = json.indexOf("\"" + key + "\":");
|
||||
if (i < 0) return -1;
|
||||
i += key.length() + 3; // 跳过 "key":
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (i < json.length() && json.charAt(i) >= '0' && json.charAt(i) <= '9') {
|
||||
sb.append(json.charAt(i));
|
||||
i++;
|
||||
}
|
||||
if (sb.length() == 0 || sb.length() > 9) return -1;
|
||||
try {
|
||||
return Integer.parseInt(sb.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查PHP端是否有待处理交易(由合并定时器B调用)
|
||||
*/
|
||||
private void doTransactionPollCheck() {
|
||||
// ★ 2026-07-15 高频补单触发:顺带触发 poller 快速检测已支付订单
|
||||
// poller 内部有 flock 锁 + 幂等去重,多次调用安全
|
||||
// ★ 2026-10-08 补单提速:限流 15s → 4s,并由独立 5 秒定时器兜底触发
|
||||
// ★ 2026-10-08 补单提速:限流 15s → 500ms,并由 0~5 秒随机的独立定时器兜底触发
|
||||
triggerFastPoller("合并B-交易");
|
||||
|
||||
try {
|
||||
@@ -2475,11 +2551,11 @@ public class WebManager {
|
||||
scheduleTimerC(240L + randC);
|
||||
scheduleTimerD(340L + randD);
|
||||
scheduleTimerE(440L + randE);
|
||||
// ★ 2026-10-08 补单提速:独立补单定时器(5 秒一轮),
|
||||
// ★ 2026-10-08 补单提速:独立补单定时器(0~5 秒随机一轮),
|
||||
// 不受 Timer B 的锁库/下线早退、也不受 Timer A 的 db-worker 队列积压影响
|
||||
if (!pollerTimerStarted) {
|
||||
pollerTimerStarted = true;
|
||||
schedulePollerTick(POLLER_TICK_TICKS);
|
||||
schedulePollerTick(nextPollerDelayTicks());
|
||||
}
|
||||
plugin.getLogger().info("[Web通信] ★ 合并定时器已启动(随机偏移A=" + (randA/20) + "s B=" + (randB/20) + "s C=" + (randC/20) + "s D=" + (randD/20) + "s E=" + (randE/20) + "s)");
|
||||
}
|
||||
@@ -9190,12 +9266,33 @@ public class WebManager {
|
||||
private long lastPollRegisterRequestsLog = 0;
|
||||
private long lastPollWebLoginExceptionLog = 0;
|
||||
private long lastPollerTriggerTime = 0; // 补单触发限流(多触发源共用)
|
||||
/** ★ 2026-10-08 补单提速:两次补单触发的最小间隔(原 15 秒,实测把到账拖到 15~30 秒) */
|
||||
private static final long POLLER_MIN_INTERVAL_MS = 4000L;
|
||||
/** ★ 独立补单定时器周期:100 tick = 5 秒 */
|
||||
private static final long POLLER_TICK_TICKS = 100L;
|
||||
/**
|
||||
* ★ 2026-10-08 补单提速(第二轮):两次补单触发的最小间隔。
|
||||
* 15 秒 → 4 秒 → 500 毫秒。4 秒会把「0~5 秒随机定时器」里间隔小于 4 秒的轮次
|
||||
* 整轮吞掉(定时器触发了却不发请求),随机节奏形同虚设;500 毫秒只用来挡
|
||||
* 「同一瞬间多个触发源齐发」,真正的并发保护在服务端 flock 进程锁。
|
||||
*/
|
||||
private static final long POLLER_MIN_INTERVAL_MS = 500L;
|
||||
/** 补单专用响应超时(秒):快判死、快重来,绝不占着补单线程干等 15 秒 */
|
||||
private static final int POLLER_TIMEOUT_S = 8;
|
||||
/** 独立补单定时器只允许启动一条链(startMergedPolling 可能被多次调用) */
|
||||
private boolean pollerTimerStarted = false;
|
||||
/**
|
||||
* ★ 补单专用单线程(第二轮核心改动):HTTP 从调用方线程挪到这里,fire-and-forget。
|
||||
* 队列容量 1 + 丢弃策略:正在补单时后来的触发直接丢(0~5 秒后随机节奏会再来),
|
||||
* 既不堆积任务也不并发打 poller;守护线程,随 JVM 退出。
|
||||
* 这条线程不是 sdf1-db-worker,因此不再吃 sendHard 的 10 秒硬超时,
|
||||
* 也不会把 DB 队列的 10 秒任务预算耗光。
|
||||
*/
|
||||
private final java.util.concurrent.ExecutorService pollerExecutor =
|
||||
new java.util.concurrent.ThreadPoolExecutor(1, 1, 30L, java.util.concurrent.TimeUnit.SECONDS,
|
||||
new java.util.concurrent.LinkedBlockingQueue<>(1),
|
||||
r -> {
|
||||
Thread t = new Thread(r, "sdf1-poller");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
},
|
||||
new java.util.concurrent.ThreadPoolExecutor.DiscardPolicy());
|
||||
private static final long LOG_INTERVAL = 60000; // 1分钟内不重复打印相同日志
|
||||
|
||||
/**
|
||||
@@ -9213,7 +9310,7 @@ public class WebManager {
|
||||
String pollerUrl = webBaseUrl + "/api/poller_online.php?secret="
|
||||
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
|
||||
long pollerStart = System.currentTimeMillis();
|
||||
String pollerResp = doGet(pollerUrl);
|
||||
String pollerResp = doGet(pollerUrl, POLLER_TIMEOUT_S);
|
||||
long pollerElapsed = System.currentTimeMillis() - pollerStart;
|
||||
|
||||
if (pollerResp != null) {
|
||||
|
||||
@@ -117,9 +117,6 @@ commands:
|
||||
web:
|
||||
description: 生成Web端登录令牌
|
||||
usage: /web
|
||||
conmsg:
|
||||
description: 控制台发言署名(仅控制台可用,替换转发聊天时的 Console 字样)—— conmsg clear 恢复默认"管理员"
|
||||
usage: /conmsg <名字> | /conmsg clear
|
||||
控制台:
|
||||
description: 生成Web端登录令牌(中文版)
|
||||
usage: /控制台
|
||||
@@ -190,9 +187,3 @@ commands:
|
||||
cleartake:
|
||||
description: 清空DB队列中未回应的web同步任务(管理员)
|
||||
usage: /cleartake
|
||||
opwl:
|
||||
description: OP白名单管理(仅控制台可用,15秒自动热重载)
|
||||
usage: /opwl <add|addtime|removetime|remove|list|reload|help>
|
||||
opwl:
|
||||
description: op白名单
|
||||
usage: /opwl
|
||||
|
||||
Reference in New Issue
Block a user