From 2a5db378d32162962c2574a394c38bd8d3fc7827 Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Tue, 23 Jun 2026 21:26:33 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20SSL=E6=96=AD=E8=B7=AF=E5=99=A8=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20-=20=E5=A4=9A=E5=B1=82=E9=98=B2=E6=8A=A4=E9=81=BF?= =?UTF-8?q?=E5=85=8DSSL=E6=8F=A1=E6=89=8B=E9=A3=8E=E6=9A=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:SSL握手失败时,多个轮询定时器(5秒/2秒/3秒)并发提交任务, 导致连续失败计数飙升(58-69次),断路器反复触发但无法有效阻断。 修复: 1. submitDbTask()添加断路器检查 - 断路期间跳过所有DB任务 2. doGet/doPost/doGetStatus/doPostWithRetry添加断路器前置检查 3. 断路期间不再发送任何HTTP请求,避免无意义的SSL握手 4. 60秒冷却期结束后自动恢复 部署:JAR已上传MC服务器 --- src/main/java/Sdf1_login/AreaCLIManager.java | 78 ++++++- src/main/java/Sdf1_login/AreaGUIManager.java | 180 +++++++++++++++- src/main/java/Sdf1_login/AreaProtection.java | 213 ++++++++++++------- src/main/java/Sdf1_login/Main.java | 99 ++++++++- src/main/java/Sdf1_login/WebManager.java | 128 ++++++++++- 5 files changed, 603 insertions(+), 95 deletions(-) diff --git a/src/main/java/Sdf1_login/AreaCLIManager.java b/src/main/java/Sdf1_login/AreaCLIManager.java index 723a63f..9d02e77 100644 --- a/src/main/java/Sdf1_login/AreaCLIManager.java +++ b/src/main/java/Sdf1_login/AreaCLIManager.java @@ -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 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 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 { diff --git a/src/main/java/Sdf1_login/AreaGUIManager.java b/src/main/java/Sdf1_login/AreaGUIManager.java index ee858cc..a651212 100644 --- a/src/main/java/Sdf1_login/AreaGUIManager.java +++ b/src/main/java/Sdf1_login/AreaGUIManager.java @@ -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); + } + } } diff --git a/src/main/java/Sdf1_login/AreaProtection.java b/src/main/java/Sdf1_login/AreaProtection.java index c27cdf2..04ded1b 100644 --- a/src/main/java/Sdf1_login/AreaProtection.java +++ b/src/main/java/Sdf1_login/AreaProtection.java @@ -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); } diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 45d8f82..7fb8698 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -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 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 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 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; diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index d11917d..159e900 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -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 → 写回结果 */