fix: SSL断路器增强 - 多层防护避免SSL握手风暴

问题:SSL握手失败时,多个轮询定时器(5秒/2秒/3秒)并发提交任务,
导致连续失败计数飙升(58-69次),断路器反复触发但无法有效阻断。

修复:
1. submitDbTask()添加断路器检查 - 断路期间跳过所有DB任务
2. doGet/doPost/doGetStatus/doPostWithRetry添加断路器前置检查
3. 断路期间不再发送任何HTTP请求,避免无意义的SSL握手
4. 60秒冷却期结束后自动恢复

部署:JAR已上传MC服务器
This commit is contained in:
youpaishidifu
2026-06-23 21:26:33 +08:00
parent 05ebd07df6
commit 2a5db378d3
5 changed files with 603 additions and 95 deletions
+69 -9
View File
@@ -6,7 +6,10 @@ import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.*;
import java.util.stream.Collectors;
@@ -21,6 +24,9 @@ public class AreaCLIManager {
private final AreaProtection areaProtect;
private static final int PAGE_SIZE = 10;
// ★ 记录每个玩家当前所在页面(切换权限后刷新用)
private final Map<UUID, String[]> playerPageInfo = new HashMap<>();
public AreaCLIManager(Main plugin, AreaProtection areaProtect) {
this.plugin = plugin;
this.areaProtect = areaProtect;
@@ -28,6 +34,40 @@ public class AreaCLIManager {
// ==================== 辅助方法 ====================
/**
* ★ 记录玩家当前页面位置
* @param type 页面类型(visitorperm / members / manage / lands)
* @param landName 领地名
* @param page 页码
*/
private void savePageInfo(Player p, String type, String landName, int page) {
playerPageInfo.put(p.getUniqueId(), new String[]{type, landName, String.valueOf(page)});
}
/**
* ★ 获取玩家上次所在页面,返回 [type, landName, page] 或 null
*/
private String[] getPageInfo(Player p) {
return playerPageInfo.get(p.getUniqueId());
}
/**
* ★ 切换权限后刷新:回到当前所在访客授权页面
*/
public void refreshCurrentPage(Player p) {
String[] info = getPageInfo(p);
if (info != null && "visitorperm".equals(info[0])) {
showVisitorPerm(p, info[1], Integer.parseInt(info[2]));
} else if (info != null && "members".equals(info[0])) {
showMemberList(p, info[1], Integer.parseInt(info[2]));
} else {
// 默认回到管理页面
if (info != null) {
showLandManage(p, info[1], 1);
}
}
}
private Component coloredText(String text, NamedTextColor color) {
return Component.text(text).color(color);
}
@@ -57,6 +97,8 @@ public class AreaCLIManager {
public void showMainMenu(Player p) {
p.sendMessage(header("领地系统"));
p.sendMessage(clickableAction("⛏", "获取圈地工具",
"/protect 工具"));
p.sendMessage(clickableAction("⚔", "创建领地",
"/protect cli create"));
p.sendMessage(clickableAction("📋", "领地列表",
@@ -69,7 +111,6 @@ public class AreaCLIManager {
// 设置
int uiMode = 1;
try { uiMode = plugin.getDb().getUiMode(p.getName()); } catch (Exception ignored) {}
String modeLabel = uiMode == 0 ? "CLI" : "GUI";
p.sendMessage(clickableAction("⚙", "切换到" + (uiMode == 0 ? "GUI" : "CLI") + "模式",
"/protect uimode " + (uiMode == 0 ? "cli" : "gui")));
@@ -79,6 +120,8 @@ public class AreaCLIManager {
// ==================== 领地列表 ====================
public void showLandList(Player p, int page) {
savePageInfo(p, "lands", "", page);
String playerName = p.getName();
List<AreaProtection.AreaConfig> lands = areaProtect.getLandsByOwner(playerName);
@@ -128,7 +171,6 @@ public class AreaCLIManager {
"/protect tp " + land.name)));
line = line.append(Component.text(" "));
if (areaProtect.hasPendingDelete(land.name)) {
// ★ 有待删除请求时显示取消按钮
line = line.append(Component.text("§e[取消删除]")
.hoverEvent(HoverEvent.showText(
Component.text("§e点击取消删除此领地")))
@@ -175,6 +217,8 @@ public class AreaCLIManager {
// ==================== 领地管理 ====================
public void showLandManage(Player p, String landName, int permPage) {
savePageInfo(p, "manage", landName, 1);
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
@@ -201,6 +245,9 @@ public class AreaCLIManager {
"/protect cli members " + land.name + " 1"));
p.sendMessage(clickableAction("🔑", "访客授权",
"/protect cli visitorperm " + land.name + " 1"));
// ★ 新增:设置传送点
p.sendMessage(clickableAction("📍", "设置传送点(当前位置)",
"/protect settp " + land.name));
p.sendMessage(Component.empty()
.append(Component.text("§a[◀ 返回列表]")
@@ -212,6 +259,8 @@ public class AreaCLIManager {
// ==================== 用户管理 ====================
public void showMemberList(Player p, String landName, int page) {
savePageInfo(p, "members", landName, page);
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
@@ -263,11 +312,13 @@ public class AreaCLIManager {
p.sendMessage(pagination);
}
// ★ 修复占位符:使用 suggest_command 让玩家可以编辑命令输入玩家名
p.sendMessage(Component.empty()
.append(Component.text("§a[添加成员] ")
.clickEvent(ClickEvent.runCommand(
"/protect addvisitor " + land.name + " <玩家名>")))
.append(Component.text("§7§7点击后在聊天框输入玩家名")));
.hoverEvent(HoverEvent.showText(Component.text("§e点击后在聊天框输入玩家名")))
.clickEvent(ClickEvent.suggestCommand(
"/protect addvisitor " + land.name + " ")))
.append(Component.text("§7点击后输入玩家名")));
p.sendMessage(Component.empty()
.append(Component.text("§a[◀ 返回管理]")
.clickEvent(ClickEvent.runCommand(
@@ -278,6 +329,8 @@ public class AreaCLIManager {
// ==================== 访客授权 ====================
public void showVisitorPerm(Player p, String landName, int page) {
savePageInfo(p, "visitorperm", landName, page);
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
@@ -314,7 +367,7 @@ public class AreaCLIManager {
.hoverEvent(HoverEvent.showText(
Component.text("§e点击切换此权限状态")))
.clickEvent(ClickEvent.runCommand(
"/protect cli toggle " + land.name + " " + perm.key)));
"/protect cli toggle " + land.name + " " + perm.key + " " + page)));
p.sendMessage(line);
}
@@ -325,7 +378,7 @@ public class AreaCLIManager {
pagination = pagination.append(Component.text("§a[◀ 上一页]")
.hoverEvent(HoverEvent.showText(Component.text("§e上一页")))
.clickEvent(ClickEvent.runCommand(
"/protect cli manage " + land.name + " " + (page - 1))));
"/protect cli visitorperm " + land.name + " " + (page - 1))));
pagination = pagination.append(Component.text(" "));
}
pagination = pagination.append(Component.text("§7第" + page + "/" + totalPages + "页"));
@@ -334,7 +387,7 @@ public class AreaCLIManager {
pagination = pagination.append(Component.text("§a[下一页 ▶]")
.hoverEvent(HoverEvent.showText(Component.text("§e下一页")))
.clickEvent(ClickEvent.runCommand(
"/protect cli manage " + land.name + " " + (page + 1))));
"/protect cli visitorperm " + land.name + " " + (page + 1))));
}
p.sendMessage(pagination);
}
@@ -342,7 +395,7 @@ public class AreaCLIManager {
// ==================== 切换权限 ====================
public void togglePerm(Player p, String landName, String permKey) {
public void togglePerm(Player p, String landName, String permKey, int page) {
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
@@ -397,6 +450,13 @@ public class AreaCLIManager {
p.sendMessage("§a已切换 §e" + permName + " §7→ " + (newState ? "§a已启用" : "§c已禁用"));
}
/**
* ★ 兼容旧调用(不含page参数)
*/
public void togglePerm(Player p, String landName, String permKey) {
togglePerm(p, landName, permKey, 1);
}
// ==================== 数据结构 ====================
private static class PermItem {
+179 -1
View File
@@ -52,9 +52,18 @@ public class AreaGUIManager implements Listener {
/**
* 打开区域防护主菜单
*/
// ★ 管理员面板标题
private static final String T_ADMIN_PANEL = "§6§l管理员配置面板";
public void openMainMenu(Player p) {
Inventory inv = Bukkit.createInventory(null, 54, T_MAIN);
// ★ 获取圈地工具(位置3)
inv.setItem(3, createItem(Material.STICK, "§a§l获取圈地工具",
"§7获取选择区域的木棍工具",
"",
"§e点击获取"));
// 创建领地(位置12)
inv.setItem(12, createItem(Material.GRASS_BLOCK, "§a§l创建领地",
"§7在你周围创建一个新的领地",
@@ -80,6 +89,15 @@ public class AreaGUIManager implements Listener {
"",
"§e点击查看"));
// ★ 管理员面板:只有tag管理员可见(位置4)
if (areaProtect.isAreaAdmin(p)) {
inv.setItem(4, createItem(Material.NETHER_STAR, "§c§l管理员面板",
"§7调整区域防护全局配置",
"§7每平米价格、最大领地数等",
"",
"§e点击进入"));
}
// ★ 切换到CLI模式(位置42)
inv.setItem(42, createItem(Material.COMMAND_BLOCK, "§b§l切换到CLI模式",
"§7切换到命令行交互模式",
@@ -442,7 +460,11 @@ public class AreaGUIManager implements Listener {
// 区域防护主菜单
if (T_MAIN.equals(title)) {
event.setCancelled(true);
if (raw == 12) {
if (raw == 3) {
// ★ 获取圈地工具
p.closeInventory();
giveWandTool(p);
} else if (raw == 12) {
// 创建领地 - 给予选择工具
p.closeInventory();
p.performCommand("protect 工具");
@@ -454,6 +476,9 @@ public class AreaGUIManager implements Listener {
} else if (raw == 40) {
p.closeInventory();
p.sendMessage("§a文档链接: https://wiki.ypshidifu.cn");
} else if (raw == 4 && areaProtect.isAreaAdmin(p)) {
// ★ 管理员面板
openAdminPanel(p);
} else if (raw == 42) {
// ★ 切换到CLI模式
p.closeInventory();
@@ -671,11 +696,22 @@ public class AreaGUIManager implements Listener {
String modeStr = land.enforceGameMode != null ? land.enforceGameMode : "无";
p.sendMessage("§a强制游戏模式: §f" + modeStr);
openLandSettings(p, landName);
} else if (raw == 31) {
// ★ 设置传送点
p.closeInventory();
p.performCommand("protect settp " + landName);
} else if (raw == 48) {
openLandManage(p, landName);
}
return;
}
// ★ 管理员面板
if (T_ADMIN_PANEL.equals(title)) {
event.setCancelled(true);
handleAdminPanelClick(p, raw, event.isLeftClick(), event.isRightClick());
return;
}
}
/**
@@ -713,9 +749,151 @@ public class AreaGUIManager implements Listener {
"",
"§e点击切换: 生存→创造→冒险→旁观→无"));
// ★ 设置传送点(位置31)
inv.setItem(31, createItem(Material.ENDER_PEARL, "§a§l设置传送点",
"§7将你当前位置设为领地传送点",
"",
"§e点击设置"));
// 返回按钮(位置48)
inv.setItem(48, createItem(Material.ARROW, "§c§l返回管理领地", ""));
p.openInventory(inv);
}
// ==================== 管理员面板 ====================
/**
* ★ 打开管理员配置面板
*/
public void openAdminPanel(Player p) {
Inventory inv = Bukkit.createInventory(null, 54, T_ADMIN_PANEL);
// 读取全局配置
int pricePerSqm = 10;
int maxLands = 5;
int defaultHeight = 255;
int peaceDuration = 3600;
try {
String val = areaProtect.getAreaConfigValue("create_price_per_sqm");
if (val != null) pricePerSqm = Integer.parseInt(val);
val = areaProtect.getAreaConfigValue("max_lands_per_player");
if (val != null) maxLands = Integer.parseInt(val);
val = areaProtect.getAreaConfigValue("default_height");
if (val != null) defaultHeight = Integer.parseInt(val);
val = areaProtect.getAreaConfigValue("peace_mode_max_duration");
if (val != null) peaceDuration = Integer.parseInt(val);
} catch (Exception ignored) {}
// 每平米价格(位置11)
inv.setItem(11, createItem(Material.EMERALD, "§e§l每平米价格",
"§7当前: §f" + pricePerSqm + " 元/㎡",
"",
"§e左键+10 / 右键-10"));
// 最大领地数(位置13)
inv.setItem(13, createItem(Material.NAME_TAG, "§e§l每玩家最大领地数",
"§7当前: §f" + maxLands + " 个",
"",
"§e左键+1 / 右键-1"));
// 默认高度(位置15)
inv.setItem(15, createItem(Material.BARRIER, "§e§l默认领地高度",
"§7当前: §f" + defaultHeight + " 格",
"",
"§e左键+32 / 右键-32"));
// 和平模式最大时间(位置22)
inv.setItem(22, createItem(Material.SHIELD, "§e§l和平模式最长时间",
"§7当前: §f" + peaceDuration + " 秒",
"",
"§e左键+600 / 右键-600"));
// 返回按钮(位置48)
inv.setItem(48, createItem(Material.ARROW, "§c§l返回主菜单", ""));
p.openInventory(inv);
}
/**
* ★ 获取圈地工具(木棍)
*/
private void giveWandTool(Player p) {
ItemStack wand = new ItemStack(Material.STICK);
ItemMeta meta = wand.getItemMeta();
if (meta != null) {
meta.setDisplayName("§a§l区域选择工具");
meta.setLore(Arrays.asList(
"§7左键点击选择第一个点",
"§7右键点击选择第二个点",
"§7手持工具输入 /protect 创建 <名称>",
"",
"§e草原探险 - 区域防护"
));
wand.setItemMeta(meta);
}
// 检查背包是否已有
for (ItemStack item : p.getInventory().getContents()) {
if (item != null && item.getType() == Material.STICK
&& item.hasItemMeta() && item.getItemMeta() != null
&& item.getItemMeta().hasDisplayName()
&& item.getItemMeta().getDisplayName().contains("区域选择工具")) {
p.sendMessage("§e你已拥有圈地工具");
return;
}
}
p.getInventory().addItem(wand);
p.sendMessage("§a已获取圈地工具(木棍),左键/右键选择区域边界");
}
/**
* ★ 处理管理员面板点击
*/
private void handleAdminPanelClick(Player p, int raw, boolean leftClick, boolean rightClick) {
// 读取当前值
int pricePerSqm = 10, maxLands = 5, defaultHeight = 255, peaceDuration = 3600;
try {
String v;
v = areaProtect.getAreaConfigValue("create_price_per_sqm");
if (v != null) pricePerSqm = Integer.parseInt(v);
v = areaProtect.getAreaConfigValue("max_lands_per_player");
if (v != null) maxLands = Integer.parseInt(v);
v = areaProtect.getAreaConfigValue("default_height");
if (v != null) defaultHeight = Integer.parseInt(v);
v = areaProtect.getAreaConfigValue("peace_mode_max_duration");
if (v != null) peaceDuration = Integer.parseInt(v);
} catch (Exception ignored) {}
if (raw == 11) {
// 每平米价格
if (leftClick) pricePerSqm += 10;
else if (rightClick) pricePerSqm = Math.max(0, pricePerSqm - 10);
areaProtect.setAreaConfigValue("create_price_per_sqm", String.valueOf(pricePerSqm));
p.sendMessage("§a每平米价格已更新: §f" + pricePerSqm + " 元/㎡");
openAdminPanel(p);
} else if (raw == 13) {
// 最大领地数
if (leftClick) maxLands += 1;
else if (rightClick) maxLands = Math.max(1, maxLands - 1);
areaProtect.setAreaConfigValue("max_lands_per_player", String.valueOf(maxLands));
p.sendMessage("§a最大领地数已更新: §f" + maxLands + " 个");
openAdminPanel(p);
} else if (raw == 15) {
// 默认高度
if (leftClick) defaultHeight += 32;
else if (rightClick) defaultHeight = Math.max(32, defaultHeight - 32);
areaProtect.setAreaConfigValue("default_height", String.valueOf(defaultHeight));
p.sendMessage("§a默认高度已更新: §f" + defaultHeight + " 格");
openAdminPanel(p);
} else if (raw == 22) {
// 和平模式时间
if (leftClick) peaceDuration += 600;
else if (rightClick) peaceDuration = Math.max(60, peaceDuration - 600);
areaProtect.setAreaConfigValue("peace_mode_max_duration", String.valueOf(peaceDuration));
p.sendMessage("§a和平模式时间已更新: §f" + peaceDuration + " 秒");
openAdminPanel(p);
} else if (raw == 48) {
openMainMenu(p);
}
}
}
+138 -75
View File
@@ -723,6 +723,29 @@ public class AreaProtection implements Listener {
plugin.getLogger().info("========================================");
}
/**
* ★ 从数据库重新加载area_config配置(Web同步用)
*/
public void reloadAreaConfigFromDb() {
if (dbConnection == null) return;
try {
ResultSet cfgRs = dbConnection.createStatement().executeQuery("SELECT key, value FROM area_config");
while (cfgRs.next()) {
String key = cfgRs.getString("key");
String value = cfgRs.getString("value");
switch (key) {
case "create_price_per_sqm": /* 已在area_config表管理 */ break;
case "max_lands_per_player": break;
case "default_height": break;
case "peace_mode_max_duration": break;
}
}
cfgRs.close();
} catch (Exception e) {
plugin.getLogger().warning("[防护] 从DB重载配置失败: " + e.getMessage());
}
}
// ==================== 消息格式化 ====================
/**
@@ -4770,12 +4793,16 @@ public class AreaProtection implements Listener {
break;
case "toggle":
if (args.length < 4) {
p.sendMessage("§c用法: /protect cli toggle <领地名> <权限key>");
p.sendMessage("§c用法: /protect cli toggle <领地名> <权限key> [页码]");
break;
}
plugin.areaCLIManager.togglePerm(p, args[2], args[3]);
// ★ 切换后刷新当前领地管理页面
plugin.areaCLIManager.showLandManage(p, args[2], 1);
int togglePage = 1;
if (args.length >= 5) {
try { togglePage = Integer.parseInt(args[4]); } catch (Exception ignored) {}
}
plugin.areaCLIManager.togglePerm(p, args[2], args[3], togglePage);
// ★ 切换后刷新到访客授权页面(停留当前页面,不退出)
plugin.areaCLIManager.refreshCurrentPage(p);
break;
case "create":
// 跳转到创建命令
@@ -5325,7 +5352,7 @@ public class AreaProtection implements Listener {
return true;
}
// ===== 删除(带1分钟延迟+二次确认)=====
// ===== 删除(带60秒延迟自动删除 + 取消)=====
if (sub.equals("删除") || sub.equals("delete")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可用");
@@ -5348,70 +5375,48 @@ public class AreaProtection implements Listener {
sender.sendMessage("§c领地不存在: " + areaName);
return true;
}
// ★ 二次确认:参数2必须是完整的领地名
if (args.length < 3 || !args[2].equals(areaName)) {
sender.sendMessage("§e⚠ 此操作不可逆!请在60秒内输入: §c/protect 删除 " + areaName + " " + areaName);
// 如果有旧的待删除,取消
PendingDelete old = pendingDeletes.remove(areaName);
if (old != null) old.task.cancel();
// 启动1分钟倒计时
UUID uid = p.getUniqueId();
String finalAreaName = areaName;
long start = System.currentTimeMillis();
BukkitTask task = new BukkitRunnable() {
@Override
public void run() {
pendingDeletes.remove(finalAreaName);
// ★ 检查是否已有待删除请求
PendingDelete existing = pendingDeletes.get(areaName);
if (existing != null) {
// 已有等待期,提示取消方式
long elapsed = System.currentTimeMillis() - existing.startTime;
long remaining = Math.max(0, (60000 - elapsed) / 1000);
sender.sendMessage("§e领地 §c" + areaName + " §e正在等待删除中,还剩 §c" + remaining + " §e秒后自动删除");
sender.sendMessage("§7输入 §e/protect 取消删除 " + areaName + " §7可取消");
return true;
}
// ★ 新建等待期:60秒后自动删除
sender.sendMessage("§e⚠ 此操作不可逆!领地 §c" + areaName + " §e将在 §c60秒 §e后自动删除");
sender.sendMessage("§7输入 §e/protect 取消删除 " + areaName + " §7可取消");
// 也显示到领地列表(CLI/GUI取消按钮会自动刷新)
UUID uid = p.getUniqueId();
String finalAreaName = areaName;
long start = System.currentTimeMillis();
BukkitTask task = new BukkitRunnable() {
@Override
public void run() {
pendingDeletes.remove(finalAreaName);
// ★ 60秒到期后自动删除领地
if (areas.containsKey(finalAreaName)) {
deleteAreaFromDb(finalAreaName);
areas.remove(finalAreaName);
plugin.getLogger().info("[防护] 领地 §e" + finalAreaName + " §7等待期结束,已自动删除");
// 通知发起者
Player online = Bukkit.getPlayer(uid);
if (online != null && online.isOnline()) {
online.sendMessage("§c§l[防护] §f领地 §e" + finalAreaName + " §f的删除请求已超时失效");
online.sendMessage("§a§l[防护] §f领地 §e" + finalAreaName + " §f等待期结束,已自动删除");
}
// 通知所有在线管理员
for (Player admin : Bukkit.getOnlinePlayers()) {
if (admin.getUniqueId().equals(uid)) continue;
if (isAreaAdmin(admin)) {
admin.sendMessage("§7[防护] 领地 §e" + finalAreaName + " §7等待期结束,已自动删除");
}
}
}
}.runTaskLater(plugin, 1200L); // 60秒 = 1200 tick
pendingDeletes.put(areaName, new PendingDelete(areaName, p.getName(), start, task));
return true;
}
// ★ 验证是发起者本人确认(或管理员)
PendingDelete pd = pendingDeletes.get(areaName);
if (pd == null) {
// 没有待删除记录,先创建一个(防止绕过延迟)
sender.sendMessage("§e⚠ 此操作不可逆!请在60秒内输入: §c/protect 删除 " + areaName + " " + areaName);
PendingDelete old = pendingDeletes.remove(areaName);
if (old != null) old.task.cancel();
UUID uid = p.getUniqueId();
String finalAreaName = areaName;
long start = System.currentTimeMillis();
BukkitTask task = new BukkitRunnable() {
@Override
public void run() {
pendingDeletes.remove(finalAreaName);
Player online = Bukkit.getPlayer(uid);
if (online != null && online.isOnline()) {
online.sendMessage("§c§l[防护] §f领地 §e" + finalAreaName + " §f的删除请求已超时失效");
}
}
}.runTaskLater(plugin, 1200L);
pendingDeletes.put(areaName, new PendingDelete(areaName, p.getName(), start, task));
return true;
}
if (!pd.playerName.equals(p.getName()) && !isAreaAdmin(sender)) {
sender.sendMessage("§c只有发起删除的玩家才能确认");
return true;
}
// ★ 检查是否已超时(1分钟)
if (System.currentTimeMillis() - pd.startTime > 60_000) {
pendingDeletes.remove(areaName);
pd.task.cancel();
sender.sendMessage("§c删除请求已超时,请重新发起");
return true;
}
// ★ 冻结期间如果玩家操作了领地,则自动取消
pendingDeletes.remove(areaName);
if (pd != null) pd.task.cancel();
// 执行删除
deleteAreaFromDb(areaName);
areas.remove(areaName);
sender.sendMessage("§a§l[防护] §f已删除领地: §e" + areaName);
}
}.runTaskLater(plugin, 1200L); // 60秒 = 1200 tick
pendingDeletes.put(areaName, new PendingDelete(areaName, p.getName(), start, task));
return true;
}
@@ -6087,15 +6092,28 @@ public class AreaProtection implements Listener {
return true;
}
Player p = (Player) sender;
// 找到玩家所在的领地
AreaConfig ac = getArea(
p.getWorld().getName(),
p.getLocation().getBlockX(),
p.getLocation().getBlockY(),
p.getLocation().getBlockZ());
if (ac == null) {
sender.sendMessage("§c你不在任何领地内");
return true;
AreaConfig ac = null;
// ★ 支持指定领地名:/protect settp <领地名>
if (args.length >= 2) {
String targetName = args[1];
String resolved = resolveAreaName(targetName);
if (resolved != null) targetName = resolved;
ac = areas.get(targetName);
if (ac == null) {
sender.sendMessage("§c领地不存在: " + targetName);
return true;
}
} else {
// 找到玩家所在的领地
ac = getArea(
p.getWorld().getName(),
p.getLocation().getBlockX(),
p.getLocation().getBlockY(),
p.getLocation().getBlockZ());
if (ac == null) {
sender.sendMessage("§c你不在任何领地内,请指定领地名: /protect settp <领地名>");
return true;
}
}
if (!hasPermission(p, ac, PermissionLevel.OWNER)) {
sender.sendMessage("§c需要领地所有者或管理员权限");
@@ -6504,6 +6522,36 @@ public class AreaProtection implements Listener {
}
}
/**
* ★ 获取area_config表中的配置值
*/
public String getAreaConfigValue(String key) {
if (dbConnection == null) return null;
try {
PreparedStatement ps = dbConnection.prepareStatement("SELECT value FROM area_config WHERE key = ?");
ps.setString(1, key);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String val = rs.getString("value");
rs.close();
ps.close();
return val;
}
rs.close();
ps.close();
} catch (Exception e) {
plugin.getLogger().warning("[防护] 读取配置失败: " + e.getMessage());
}
return null;
}
/**
* ★ 设置area_config表中的配置值(公开方法)
*/
public void setAreaConfigValue(String key, String value) {
updateAreaConfig(key, value);
}
/**
* 检查玩家的访客权限是否有效(未过期)
*/
@@ -7129,7 +7177,22 @@ public class AreaProtection implements Listener {
public LandConfig getLandConfig() { return landConfig; }
private void loadAreaConfig() {
File configFile = new File(plugin.getDataFolder(), "区域防护配置.txt");
// ★ 配置文件放在area/子目录,避免插件根目录凌乱
File areaDir = new File(plugin.getDataFolder(), "area");
if (!areaDir.exists()) {
areaDir.mkdirs();
}
// ★ 自动迁移:如果根目录有旧配置文件,移到area/目录
File oldConfigFile = new File(plugin.getDataFolder(), "区域防护配置.txt");
File configFile = new File(areaDir, "区域防护配置.txt");
if (!configFile.exists() && oldConfigFile.exists()) {
try {
oldConfigFile.renameTo(configFile);
plugin.getLogger().info("[防护] 已将区域防护配置迁移到 area/ 目录");
} catch (Exception e) {
// 迁移失败,使用新位置
}
}
if (!configFile.exists()) {
writeDefaultAreaConfig(configFile);
}
+97 -2
View File
@@ -6052,7 +6052,15 @@ public class Main extends JavaPlugin
"expand", "contraction",
"list", "listitem",
"addname", "removename", "listname",
"addwhite", "removewhite", "listwhite");
"addwhite", "removewhite", "listwhite",
"settp", "setwarp", "tp", "warp",
"setowner", "addvisitor", "removevisitor", "listvisitors", "transfer",
"info", "shop",
"cli", "menu", "菜单",
"取消删除", "canceldelete",
"removemember", "addmember",
"config", "配置",
"delete", "create", "reload");
return filterTab(opts, args[0]);
}
@@ -6085,7 +6093,13 @@ public class Main extends JavaPlugin
return Arrays.asList("1", "3", "5", "10", "20", "50");
}
// 创建/删除: 区域名
if (sub.equals("创建") || sub.equals("删除")) {
if (sub.equals("创建") || sub.equals("delete")) {
return filterTab(
new ArrayList<>(areaProtection.getAreaNames()),
args[1]);
}
// ★ 删除也支持中文
if (sub.equals("删除")) {
return filterTab(
new ArrayList<>(areaProtection.getAreaNames()),
args[1]);
@@ -6104,6 +6118,33 @@ public class Main extends JavaPlugin
new ArrayList<>(areaProtection.getAreaNames()),
args[1]);
}
// ★ settp/setwarp/tp/warp: 区域名
if (sub.equals("settp") || sub.equals("setwarp")
|| sub.equals("tp") || sub.equals("warp")) {
return filterTab(
new ArrayList<>(areaProtection.getAreaNames()),
args[1]);
}
// ★ setowner: 区域名
if (sub.equals("setowner") || sub.equals("transfer")) {
return filterTab(
new ArrayList<>(areaProtection.getAreaNames()),
args[1]);
}
// ★ addvisitor/removevisitor/listvisitors/removemember/addmember: 区域名
if (sub.equals("addvisitor") || sub.equals("removevisitor")
|| sub.equals("listvisitors") || sub.equals("removemember")
|| sub.equals("addmember")) {
return filterTab(
new ArrayList<>(areaProtection.getAreaNames()),
args[1]);
}
// ★ cli: 子命令
if (sub.equals("cli")) {
return filterTab(
Arrays.asList("menu", "lands", "manage", "members", "visitorperm", "toggle", "create"),
args[1]);
}
}
// ===== 第三层 =====
@@ -6137,6 +6178,60 @@ public class Main extends JavaPlugin
players.add(pl.getName());
return filterTab(players, args[2]);
}
// ★ setowner: 第三层是玩家名
if (sub.equals("setowner")) {
List<String> players = new ArrayList<>();
for (Player pl : Bukkit.getOnlinePlayers())
players.add(pl.getName());
return filterTab(players, args[2]);
}
// ★ addvisitor/removevisitor: 第三层是玩家名
if (sub.equals("addvisitor") || sub.equals("removevisitor")) {
List<String> players = new ArrayList<>();
for (Player pl : Bukkit.getOnlinePlayers())
players.add(pl.getName());
return filterTab(players, args[2]);
}
// ★ removemember/addmember: 第三层是玩家名
if (sub.equals("removemember") || sub.equals("addmember")) {
List<String> players = new ArrayList<>();
for (Player pl : Bukkit.getOnlinePlayers())
players.add(pl.getName());
return filterTab(players, args[2]);
}
// ★ cli manage/members/visitorperm/toggle: 第三层是领地名
if (sub.equals("cli")) {
String action = args[1].toLowerCase();
if (action.equals("manage") || action.equals("members")
|| action.equals("visitorperm") || action.equals("toggle")) {
return filterTab(
new ArrayList<>(areaProtection.getAreaNames()),
args[2]);
}
}
}
// ★ 第四层:cli toggle需要权限key,cli manage/members/visitorperm需要页码
if (args.length == 4) {
if (sub.equals("cli")) {
String action = args[1].toLowerCase();
if (action.equals("toggle")) {
// toggle: 第四层是权限key
return filterTab(Arrays.asList(
"move", "block_place", "block_break", "pvp", "mount",
"ender_pearl", "thrown_projectiles", "raid", "bow", "potion",
"fire", "fire_spread", "pickup", "drop", "explosion",
"fall_damage", "hunger", "all_damage", "all_effects",
"item_frame", "redstone", "door", "audio", "lead",
"crop_harvest", "wool_shear", "animal_feed", "glowing", "peace_mode"),
args[3]);
}
if (action.equals("manage") || action.equals("members")
|| action.equals("visitorperm")) {
// 页码
return Arrays.asList("1", "2", "3", "4", "5");
}
}
}
return null;
+120 -8
View File
@@ -212,8 +212,35 @@ public class WebManager {
/**
* 提交指定优先级的数据库写入任务
* ★ 限制同种类任务最多3个(防止队列积压)
*/
private void submitDbTask(int priority, String name, Runnable action) {
// ★ SSL断路器:断路期间跳过所有HTTP相关任务
if (isCircuitOpen()) {
plugin.getLogger().warning("[DB队列] SSL断路器开启,跳过任务: " + name);
return;
}
// ★ 同种类任务去重:提取任务类型前缀(如 "周期-syncOnlinePlayers" → "syncOnlinePlayers")
String taskType = name.contains("-") ? name.substring(name.indexOf("-") + 1) : name;
// ★ 统计同类型任务数量
int sameTypeCount = 0;
for (DbTask t : dbTaskQueue) {
if (t != null) {
String tType = t.name.contains("-") ? t.name.substring(t.name.indexOf("-") + 1) : t.name;
if (tType.equals(taskType)) {
sameTypeCount++;
}
}
}
// ★ 同类型任务最多3个,超过则丢弃低优先级的
if (sameTypeCount >= 3) {
plugin.getLogger().warning("[DB队列] 同类型任务已满(3): " + taskType + ",丢弃: " + name);
return;
}
DbTask task = new DbTask(priority, name, action);
dbTaskQueue.offer(task);
if (dbTaskQueue.size() > 50) {
@@ -308,6 +335,10 @@ public class WebManager {
plugin.getLogger().warning("[Web通信] HttpClient未初始化");
return null;
}
// ★ SSL断路器:断路期间直接返回,避免无意义请求
if (isCircuitOpen()) {
return null;
}
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(urlStr))
@@ -345,6 +376,10 @@ public class WebManager {
*/
private int doGetStatus(String urlStr) {
if (cfHttpClient == null) return -1;
// ★ SSL断路器:断路期间直接返回,避免无意义请求
if (isCircuitOpen()) {
return -1;
}
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(urlStr))
@@ -367,6 +402,10 @@ public class WebManager {
plugin.getLogger().warning("[Web通信] HttpClient未初始化");
return null;
}
// ★ SSL断路器:断路期间直接返回,避免无意义请求
if (isCircuitOpen()) {
return null;
}
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(urlStr))
@@ -395,6 +434,10 @@ public class WebManager {
* POST请求带重试(处理SSL/网络异常)
*/
private String doPostWithRetry(String urlStr, String jsonBody, int maxRetries) {
// ★ SSL断路器:断路期间直接返回,避免无意义重试
if (isCircuitOpen()) {
return null;
}
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
HttpRequest req = HttpRequest.newBuilder()
@@ -579,6 +622,9 @@ public class WebManager {
// ★ 启动CDK验证轮询(每2秒检查PHP端CDK验证请求)
startCdkValidationPolling();
// ★ 启动领地数据即时同步(每10秒从PHP拉取最新领地配置)
startLandSyncTimer();
plugin.getLogger().info("[Web通信] 实时同步调度器已启动(60~90秒随机,无人停止,有人恢复)");
plugin.getLogger().info("[Web通信] PHP回调服务器已启动,端口: " + callbackPort);
}
@@ -997,16 +1043,28 @@ public class WebManager {
activeSyncRunning = true;
try {
// 通过DB队列串行化执行周期性同步
submitDbTask("周期-syncOnlinePlayers", () -> syncOnlinePlayers());
// ★ 玩家在线时:全量同步
// ★ 玩家不在线时:只保留注册/登录轮询,其余任务全部跳过
boolean online = !Bukkit.getOnlinePlayers().isEmpty();
// 登录/注册相关(始终执行)
submitDbTask("周期-pushWebLoginCredentials", () -> pushWebLoginCredentials());
submitDbTask("周期-syncUserRegistrations", () -> syncUserRegistrations());
submitDbTask("周期-syncBondTransactions", () -> syncBondTransactions());
submitNormalDbTask("周期-pullPendingTransactions", () -> pullPendingTransactions());
submitNormalDbTask("周期-pullShopStock", () -> pullShopStock());
submitNormalDbTask("周期-pullBondChanges", () -> pullBondChanges());
submitNormalDbTask("周期-syncAllPlayerIps", () -> syncAllPlayerIps());
submitNormalDbTask("周期-syncServiceProviders", () -> syncServiceProviders());
if (online) {
// 在线:执行全量同步
submitDbTask("周期-syncOnlinePlayers", () -> syncOnlinePlayers());
submitDbTask("周期-syncBondTransactions", () -> syncBondTransactions());
submitNormalDbTask("周期-pullPendingTransactions", () -> pullPendingTransactions());
submitNormalDbTask("周期-pullShopStock", () -> pullShopStock());
submitNormalDbTask("周期-pullBondChanges", () -> pullBondChanges());
submitNormalDbTask("周期-syncAllPlayerIps", () -> syncAllPlayerIps());
submitNormalDbTask("周期-syncServiceProviders", () -> syncServiceProviders());
submitNormalDbTask("周期-syncLandData", () -> syncLandData());
} else {
// ★ 不在线:跳过非必要同步任务
plugin.getLogger().info("[Web通信] 全员下线,跳过非登录类同步任务");
}
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 全量同步异常: " + e.getMessage());
} finally {
@@ -1254,6 +1312,60 @@ public class WebManager {
plugin.getLogger().info("[Web通信] CDK远程验证轮询已启动(每2秒)");
}
// ==================== 领地数据即时同步 ====================
/**
* ★ 领地数据即时同步:每10秒从PHP拉取最新领地配置
* 配合60~90秒全量同步作为兜底
*/
private volatile boolean landSyncPulling = false;
private volatile int landSyncFailCount = 0;
private static final int LAND_SYNC_FAIL_THRESHOLD = 10;
private void startLandSyncTimer() {
new BukkitRunnable() {
@Override
public void run() {
try {
// ★ 玩家不在线时跳过
if (Bukkit.getOnlinePlayers().isEmpty()) return;
// ★ 防重入
if (landSyncPulling) return;
// ★ 连续失败过多时降低频率
if (landSyncFailCount >= LAND_SYNC_FAIL_THRESHOLD) {
if (landSyncFailCount % 6 != 0) {
landSyncFailCount++;
return;
}
}
landSyncPulling = true;
try {
String url = webBaseUrl + "/api/land_api.php?action=get_config&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
String json = doGet(url);
if (json != null && json.contains("\"success\":true")) {
landSyncFailCount = 0;
// 配置拉取成功,通知AreaProtection刷新
if (plugin.areaProtection != null) {
plugin.areaProtection.reloadAreaConfigFromDb();
}
} else {
landSyncFailCount++;
}
} finally {
landSyncPulling = false;
}
} catch (Exception e) {
landSyncFailCount++;
}
}
}.runTaskTimerAsynchronously(plugin, 20L * 10, 20L * 10); // 每10秒
plugin.getLogger().info("[Web通信] 领地配置即时同步已启动(每10秒)");
}
/**
* Part 1: Web端写cdk_validate_requests → Sdf1_login拉取 → 本地CDKManager.redeem → 写回结果
*/