fix: 修复全员下线后SN同步定时器永久停摆与冷静期提示误导
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 1m53s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s

1. WebManager: Timer B/C/E 全员下线时改为保活调度(不再 return 停死),
   Timer C 兼任看门狗在检测到玩家上线时恢复;doActiveSyncBatch 移除
   重复 schedule 调用避免产生双链。
2. SnManager.setStatus 写入 cancel_time,新增 cooldownMinutesLeft 辅助方法。
3. AreaProtection/AreaGUIManager/LandRecordManager/PVPManager 申领入口按
   真实原因分流提示(冷静期剩余分钟数 vs 旧SN未注销),副作用与 return 不变。
This commit is contained in:
youpaishidifu
2026-09-29 16:38:31 +08:00
parent 68bbb52ae6
commit ccb67a0bf5
6 changed files with 56 additions and 51 deletions
+7 -2
View File
@@ -1769,8 +1769,13 @@ public class AreaGUIManager implements Listener {
if (plugin.getSnManager() != null
&& plugin.getSnManager().isBlocked(
p, SnManager.TYPE_WAND)) {
p.sendMessage("§e你已有区域选择工具的有效登记,"
+ "旧SN未注销前不能重新申领");
long cdMin = plugin.getSnManager().cooldownMinutesLeft(p.getName());
if (cdMin > 0) {
p.sendMessage("§e注销冷静期未结束,还剩约 §f" + cdMin + " §e分钟,期间无法申领区域选择工具");
} else {
p.sendMessage("§e你已有区域选择工具的有效登记,"
+ "旧SN未注销前不能重新申领");
}
areaProtect.wandCooldownMap.put(p.getUniqueId(), now);
return;
}
@@ -6632,8 +6632,13 @@ public class AreaProtection implements Listener {
if (plugin.getSnManager() != null
&& plugin.getSnManager().isBlocked(
p, SnManager.TYPE_WAND)) {
long cdMin = plugin.getSnManager().cooldownMinutesLeft(p.getName());
if (cdMin > 0) {
p.sendMessage("§c§l[防护] §f注销冷静期未结束,还剩约 §e" + cdMin + " §f分钟,期间无法申领区域选择工具");
} else {
p.sendMessage("§c§l[防护] §f你已有区域选择工具的有效登记,"
+ "旧SN未注销前不能重新申领");
}
wandCooldownMap.put(p.getUniqueId(), now);
return true;
}
@@ -114,8 +114,13 @@ public class LandRecordManager implements Listener {
if (plugin.getSnManager() != null
&& plugin.getSnManager().isBlocked(
p, SnManager.TYPE_ECHO)) {
p.sendMessage("§c§l[回声碎片] §f你已有回声碎片的有效登记,"
+ "旧SN未注销前不能重新申领");
long cdMin = plugin.getSnManager().cooldownMinutesLeft(p.getName());
if (cdMin > 0) {
p.sendMessage("§c§l[回声碎片] §f注销冷静期未结束,还剩约 §e" + cdMin + " §f分钟,期间无法申领新设备");
} else {
p.sendMessage("§c§l[回声碎片] §f你已有回声碎片的有效登记,"
+ "旧SN未注销前不能重新申领");
}
return;
}
ItemStack shard = createEchoShard();
+7 -2
View File
@@ -490,8 +490,13 @@ public class PVPManager implements Listener {
if (plugin.getSnManager() != null
&& plugin.getSnManager().isBlocked(
p, SnManager.TYPE_PVP)) {
p.sendMessage("§c[PVP] 你已有PVP圈地棒的有效登记,"
+ "旧SN未注销前不能重新申领");
long cdMin = plugin.getSnManager().cooldownMinutesLeft(p.getName());
if (cdMin > 0) {
p.sendMessage("§c[PVP] 注销冷静期未结束,还剩约 " + cdMin + " 分钟,期间无法申领新设备");
} else {
p.sendMessage("§c[PVP] 你已有PVP圈地棒的有效登记,"
+ "旧SN未注销前不能重新申领");
}
return;
}
ItemStack tool = createTool();
+12 -2
View File
@@ -437,6 +437,13 @@ public class SnManager implements Listener {
}
}
/** 注销冷静期剩余分钟数,0 = 不在冷静期 */
public long cooldownMinutesLeft(String player) {
long cd = getCooldownUntil(player);
long now = System.currentTimeMillis();
return cd > now ? (cd - now + 59999) / 60000 : 0L;
}
/** 启动 1 小时冷静期 */
public void startCooldown(String player) {
try {
@@ -697,7 +704,8 @@ public class SnManager implements Listener {
+ " bind_reason = CASE WHEN ? = 'illegal'"
+ " THEN ? ELSE bind_reason END,"
+ " bind_time = CASE WHEN ? = 'illegal'"
+ " THEN ? ELSE bind_time END"
+ " THEN ? ELSE bind_time END,"
+ " cancel_time = CASE WHEN ? = 'cancelled' THEN ? ELSE cancel_time END"
+ " WHERE sn = ?");
ps.setString(1, status);
ps.setString(2, remark);
@@ -705,7 +713,9 @@ public class SnManager implements Listener {
ps.setString(4, remark);
ps.setString(5, status);
ps.setLong(6, time);
ps.setString(7, sn);
ps.setString(7, status);
ps.setLong(8, time);
ps.setString(9, sn);
int n = ps.executeUpdate();
ps.close();
return n;
+18 -43
View File
@@ -1605,13 +1605,7 @@ public class WebManager {
// ★ Timer D已独立运行,不再需要重启
if (timersBCPaused) {
timersBCPaused = false;
long randB = (long)(Math.random() * 10) * 2;
long randC = (long)(Math.random() * 10) * 2;
long randE = (long)(Math.random() * 10) * 2;
scheduleTimerB(40L + randB); // 2秒后重启B
scheduleTimerC(120L + randC); // 6秒后重启C
scheduleTimerE(240L + randE); // 12秒后重启E
plugin.getLogger().info("[合并C] ★ 玩家上线,Timer B/C/E已恢复(随机偏移B=" + (randB/20) + "s C=" + (randC/20) + "s E=" + (randE/20) + "s)");
plugin.getLogger().info("[合并C] ★ 玩家上线,Timer B/C/E 已恢复");
}
// 玩家在线:全量批处理
@@ -1932,10 +1926,12 @@ public class WebManager {
return;
}
// ★ 全员下线暂停:不调度下一轮
// ★ 全员下线暂停:跳过本轮工作但保持链存活
// ★ 关键:恢复逻辑在 doActiveSyncBatch 里,只由 Timer C 调用,
// ★ 如果在这里直接 return 停死,就再也没人能唤醒它(生产事故根因)
if (timersBCPaused) {
plugin.getLogger().info("[合并B] ★ 全员下线,Timer B已暂停");
return; // 不调度下一轮,定时器自然停止
scheduleTimerB(calcStaggeredDelay(TIMER_B, 0, 10));
return;
}
// ★ PHP锁库退避检查
@@ -1994,10 +1990,16 @@ public class WebManager {
return;
}
// ★ 全员下线暂停:不调度下一轮
// ★ 全员下线暂停:Timer C 兼任看门狗,不能停死
if (timersBCPaused) {
plugin.getLogger().info("[合并C] ★ 全员下线,Timer C已暂停");
return; // 不调度下一轮,定时器自然停止
if (!Bukkit.getOnlinePlayers().isEmpty()) {
timersBCPaused = false;
plugin.getLogger().info("[合并C] ★ 检测到玩家上线,Timer B/C/E 恢复运行");
// 三条链本身一直存活(下方都改为保活调度),这里无需重新 schedule B/E,继续本轮正常流程
} else {
scheduleTimerC(calcStaggeredDelay(TIMER_C, 10, 20));
return;
}
}
// ★ PHP锁库退避检查
@@ -2095,37 +2097,10 @@ public class WebManager {
return;
}
// ★ 全员下线暂停:不调度下一轮
// ★ 全员下线暂停:跳过本轮工作但保持链存活(原实现会停死,且每轮打印 ASCII 噪音)
if (timersBCPaused) {
plugin.getLogger().info("[续费轮询E] ★ 全员下线,Timer E已暂停");
log.error("\n" +
" __ __ _ \n" +
" \\ \\ / / | | \n" +
" \\ \\ /\\ / /__ ___ ___ _ __ ___ ___ | |_ ___ \n" +
" \\ \\/ \\/ / _ \\/ __/ _ \\| '_ ` _ \\ / _ \\ | __/ _ \\ \n" +
" \\ /\\ / __/ (_| (_) | | | | | | __/ | || (_) | \n" +
" \\/ \\/ \\___|\\___\\___/|_| |_| |_|\\___| \\__\\___/ _ \n" +
" | | (_) \n" +
" ___ __ _ ___ _ _ _ _ __ _ _ __ | |_ __ _ _ __ __ ___ __ _ _ __ ___ ___ _ ____ _____ _ __ \n" +
" / __/ _` |/ _ \\ | | | | | | |/ _` | '_ \\ | __/ _` | '_ \\ \\ \\/ / |/ _` | '_ \\ / __|/ _ \\ '__\\ \\ / / _ \\ '__|\n" +
" | (_| (_| | (_) | | |_| | |_| | (_| | | | | | || (_| | | | | > <| | (_| | | | | \\__ \\ __/ | \\ V / __/ | \n" +
" \\___\\__,_|\\___/ \\__, |\\__,_|\\__,_|_| |_| \\__\\__,_|_| |_| /_/\\_\\_|\\__,_|_| |_| |___/\\___|_| \\_/ \\___|_| \n" +
" _ __/ | ___ _ _ _ _ __ \n" +
" (_) _ |___/ |__ \\ | | (_) | (_)/ _| \n" +
" _ _ __(_) _ __ ___ ___ ) | _ _ _ __ ___| |__ _ __| |_| |_ _ _ ___ _ __ \n" +
" | | '_ \\ | '_ ` _ \\ / __| / / | | | | '_ \\/ __| '_ \\| |/ _` | | _| | | | / __| '_ \\ \n" +
" | | |_) | | | | | | | (__ / /_ | |_| | |_) \\__ \\ | | | | (_| | | | | |_| || (__| | | | \n" +
" |_| .__(_) |_| |_| |_|\\___|____(_)__, | .__/|___/_| |_|_|\\__,_|_|_| \\__,_(_)___|_| |_| \n" +
" | | __/ | | \n" +
" |_| _ ____ _|___/|_|______ ___ \n" +
" | | _ |___ \\ / _ \\ / /____ / _ \\ \n" +
" _ __ ___ ___| |_(_) __) | | | |/ /_ / / (_) | \n" +
" | '_ \\ / _ \\/ __| __| |__ <| | | | '_ \\ / / \\__, | \n" +
" | |_) | (_) \\__ \\ |_ _ ___) | |_| | (_) / / / / \n" +
" | .__/ \\___/|___/\\__(_) |____/ \\___/ \\___/_/ /_/ \n" +
" | | \n" +
" |_| ");
return; // 不调度下一轮,定时器自然停止
scheduleTimerE(calcStaggeredDelay(TIMER_E, 20, 30));
return;
}
// ★ PHP锁库退避检查