修复:Web通信关闭后仍在出网(热重载暴露的可见性 bug)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m4s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

现象(用户日志实证):
  16:41:36 配置已热重载: 启用=false 密钥长度=30 [开关已关闭]
  16:41:45 syncOnlinePlayers: enabled=false, 跳过     ← 有守卫,正常
  16:42:12 推送游戏内商品目录 ... 已同步 975 项         ← ★ 无守卫,仍在出网
  16:42:46 服务商数据变更,已同步: 1人                 ← ★ 无守卫,仍在出网

根因两层:
1) 约 22 个同步方法(pushShopCatalog/syncServiceProviders/syncPermissions/
   pullShopStock/pullShopPrices/pullBondChanges 等)压根没有 !enabled 守卫;
2) 即使有守卫也拦不住 —— 任务是在「开关还是 true 时」就已 submitDbTask 入队的,
   db-worker 单线程排队轮到它时开关早已关闭,方法开头的守卫已经执行完了。

修法:出网最后一公里统一加闸,三道防线
1) 7 个 HTTP 收口方法开头加 isEnabled() 闸。doGet 是 46 处调用的唯一总出口,
   一处改动覆盖全部 GET 路径,零遗漏;两个 HTTP 降级方法一并覆盖。
2) submitDbTask / submitWebTask 入队前拦一道(省掉入队开销)。
3) startDbWorker 执行 task.run() 之前再查一次(治标兜底,拦存量任务),
   静默 continue 不打 WARN —— 关闭后每轮几十个任务全打日志会刷爆控制台,
   真正的开关变化已由 hotReloadIfChanged 打了一行「[开关已关闭]」,信息不丢。
另加 noteWebDisabledOnce():关闭状态下每 60 秒一条 INFO,说明「为什么没发」。

验证:mvn package 零错误;离线单测 18/18(闸的位置与结构均从真实源码抽取),
含端到端复现「关闭前入队、关闭后执行」这一用户日志场景。
This commit is contained in:
youpaishidifu
2026-10-04 17:01:21 +08:00
parent 932623ed88
commit ba613ff953
+58
View File
@@ -216,6 +216,33 @@ public class WebManager {
return true;
}
// ★★★ Web通信关闭闸(2026-10-04 热重载 bug 修复)★★★
// 现象:开关已关闭后,日志里仍出现「已同步」「推送成功」——
// 原因一:约 22 个同步方法(pushShopCatalog/syncServiceProviders/syncPermissions 等)
// 压根没有 !enabled 守卫;
// 原因二:有守卫的方法也拦不住「开关还是 true 时就已 submitDbTask 入队」的任务,
// db-worker 单线程排队轮到它时开关早已关闭。
//
// 修法:在【出网最后一公里】统一拦。doGet 是 46 处调用的唯一总出口,
// 所以只要在这几个 HTTP 收口方法开头加闸,所有 GET/POST 路径一次覆盖,零遗漏。
// 逻辑复用 isEnabled()(它现在实时读配置文件,改开关下一毫秒生效)。
private static final String WEB_DISABLED_HINT =
"[Web通信] 已关闭(web通信-启用=false),请求已跳过(改回 true 会自动恢复,无需 reload)";
// ★ 「已关闭」提示的节流时间戳:关闭状态下每 60 秒最多打一条,避免刷爆控制台
private volatile long lastWebDisabledLogAt = 0;
/**
* 关闭状态下按节流打一条提示(60 秒一条)。
* 用途:出网闸/队列闸都是静默 return,运维可能以为插件卡死;
* 这里保证「偶尔能看到一句『因为开关关着所以没发』」,且不会刷屏。
*/
private void noteWebDisabledOnce(String where) {
long now = System.currentTimeMillis();
if (now - lastWebDisabledLogAt < 60000L) return;
lastWebDisabledLogAt = now;
plugin.getLogger().info(WEB_DISABLED_HINT + "(最近触发点: " + where + ")");
}
/**
* 判断响应体是否为「密钥验证失败」。
* PHP 端 error('密钥验证失败', 403) → {"success":false,"message":"密钥验证失败"}
@@ -394,6 +421,13 @@ public class WebManager {
if (waitMs > 10000) {
plugin.getLogger().warning("[DB队列] 等待过久: " + task.name + " 等待=" + waitMs + "ms");
}
// ★ Web通信关闭闸(执行时点复查):任务可能是在「开关还是 true 时」入队的,
// 排队期间运维把 web通信-启用 改成 false,轮到执行时必须再拦一次。
// 这里【静默跳过】而不打 WARN —— 关闭开关后每轮几十个任务全打日志会刷爆控制台;
// 真正的开关变化已由 hotReloadIfChanged 打了一行「[开关已关闭]」,信息不会丢。
if (!isEnabled()) {
continue;
}
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -454,6 +488,12 @@ public class WebManager {
* 提交任务(支持bypassSslCheck:关键任务如syncOnlinePlayers不应被SSL断路器阻断)
*/
private void submitDbTask(int priority, String name, Runnable action, boolean bypassSslCheck) {
// ★ Web通信关闭闸(入队前):开关关闭时不再往队列里塞任务,避免队列堆积。
// 注意不能只靠这一处 —— 已入队的存量任务由 startDbWorker 执行时的复查兜底。
if (!isEnabled()) {
noteWebDisabledOnce(name);
return;
}
// ★ SSL断路器:断路期间跳过所有HTTP相关任务(除非指定绕过)
if (!bypassSslCheck && isCircuitOpen()) {
plugin.getLogger().warning("[DB队列] SSL断路器开启,跳过任务: " + name);
@@ -638,6 +678,8 @@ public class WebManager {
* 降级后的HTTP GET请求(使用不带SSL的HttpClient)
*/
private String doGetHttpFallback(String urlStr) {
// ★ Web通信关闭闸(降级路径同样不能出网)
if (!isEnabled()) return null;
if (plainHttpClient == null) {
plugin.getLogger().warning("[Web通信] HTTP降级客户端未初始化");
return null;
@@ -677,6 +719,8 @@ public class WebManager {
*/
private void submitWebTask(String name, Runnable action) {
if (isCircuitOpen()) return;
// ★ Web通信关闭闸(入队前拦一道,webExecutor 队列同样可能有存量任务)
if (!isEnabled()) return;
webExecutor.submit(() -> {
try {
action.run();
@@ -781,6 +825,10 @@ public class WebManager {
* ★ 包含SSL降级逻辑:连续失败3次后降级到HTTP
*/
private String doGet(String urlStr) {
// ★ Web通信关闭闸:开关关闭后一律不出网(doGet 是全部 GET 路径的唯一出口)
if (!isEnabled()) {
return null;
}
if (cfHttpClient == null) {
plugin.getLogger().warning("[Web通信] HttpClient未初始化");
return null;
@@ -840,6 +888,8 @@ public class WebManager {
* GET请求 - 返回状态码
*/
private int doGetStatus(String urlStr) {
// ★ Web通信关闭闸
if (!isEnabled()) return -1;
if (cfHttpClient == null) return -1;
// ★ SSL断路器:断路期间直接返回,避免无意义请求
if (isCircuitOpen()) {
@@ -872,6 +922,8 @@ public class WebManager {
* 供不需要降级的特殊POST使用
*/
private String doPostWithoutFallback(String urlStr, String jsonBody) {
// ★ Web通信关闭闸
if (!isEnabled()) return null;
if (cfHttpClient == null) {
plugin.getLogger().warning("[Web通信] HttpClient未初始化");
return null;
@@ -917,6 +969,8 @@ public class WebManager {
* POST请求 - 返回响应体,失败返回null(含SSL降级逻辑)
*/
private String doPostWithSslFallback(String urlStr, String jsonBody) {
// ★ Web通信关闭闸
if (!isEnabled()) return null;
if (cfHttpClient == null) {
plugin.getLogger().warning("[Web通信] HttpClient未初始化");
return null;
@@ -973,6 +1027,8 @@ public class WebManager {
* 降级后的HTTP POST请求(使用不带SSL的HttpClient)
*/
private String doPostHttpFallback(String urlStr, String jsonBody) {
// ★ Web通信关闭闸(降级路径同样不能出网)
if (!isEnabled()) return null;
if (plainHttpClient == null) {
plugin.getLogger().warning("[Web通信] HTTP降级客户端未初始化");
return null;
@@ -1009,6 +1065,8 @@ public class WebManager {
* POST请求带重试(处理SSL/网络异常)
*/
private String doPostWithRetry(String urlStr, String jsonBody, int maxRetries) {
// ★ Web通信关闭闸
if (!isEnabled()) return null;
// ★ SSL断路器:断路期间直接返回,避免无意义重试
if (isCircuitOpen()) {
return null;