fix: PVP观察者改纯命令切换+双向免伤、清空队列联动PHP清理+回写lambda化、封禁IP推送Web黑名单
This commit is contained in:
@@ -780,7 +780,8 @@ public class Main extends JavaPlugin
|
||||
"tempban", "arena",
|
||||
"join", "leave",
|
||||
"exit", "test",
|
||||
"equip", "adjust"};
|
||||
"equip", "adjust",
|
||||
"see"};
|
||||
for (String s : sub) {
|
||||
if (s.startsWith(
|
||||
args[0]
|
||||
@@ -814,6 +815,16 @@ public class Main extends JavaPlugin
|
||||
list.add(rn);
|
||||
}
|
||||
}
|
||||
} else if (sub.equals("join")
|
||||
|| sub.equals("arena")
|
||||
|| sub.equals("leave")
|
||||
|| sub.equals("exit")) {
|
||||
// ★ /pvp join see、/pvp leave see —— 观赛模式
|
||||
if ("see".startsWith(
|
||||
args[1]
|
||||
.toLowerCase())) {
|
||||
list.add("see");
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
@@ -6205,8 +6216,8 @@ public class Main extends JavaPlugin
|
||||
sender.sendMessage("§cWeb通信管理器未初始化");
|
||||
return true;
|
||||
}
|
||||
webManager.clearQueue();
|
||||
sender.sendMessage("§a已清空DB队列中未回应的web同步任务");
|
||||
String clearSummary = webManager.clearQueue();
|
||||
sender.sendMessage("§a已清空DB队列: §f" + clearSummary);
|
||||
return true;
|
||||
}
|
||||
// /menu 开关雪球菜单
|
||||
|
||||
@@ -2777,8 +2777,6 @@ public class PVPArenaManager implements Listener {
|
||||
|
||||
// ==================== PVP 观赛(/pvp see 观察者模式)====================
|
||||
|
||||
/** 观赛雪球 lore 标记(与菜单雪球标签 sdf1_menu 区分,避免被误判为菜单物品) */
|
||||
private static final String SPECTATE_TAG = "PVP观赛|";
|
||||
|
||||
/** 玩家名 → 进入观察者前的游戏模式(离场/重连时还原,避免把观察者模式带到主世界) */
|
||||
private final Map<String, GameMode> spectatorBackups = new ConcurrentHashMap<>();
|
||||
@@ -2791,40 +2789,74 @@ public class PVPArenaManager implements Listener {
|
||||
|
||||
/**
|
||||
* /pvp see —— PVP 观赛模式(仅 PVP 世界生效)
|
||||
* ① 已在观察者 → 切回普通生存模式
|
||||
* ② 否则:回收 PVP 装备(★ 不发还玩家自己的装备,自己的装备仍在备份里、离场时统一还原)
|
||||
* → 发放 2 个不同的雪球 → 自动进入 MC 原版观察者模式
|
||||
*
|
||||
* 三种调用形式:
|
||||
* /pvp see → 切换(在观察者⇄普通模式之间来回切,mode=null)
|
||||
* /pvp join see → 强制进入观察者(mode="enter")
|
||||
* /pvp leave see → 强制退出观察者(mode="exit")
|
||||
*
|
||||
* ★ v2 设计变更:原版【观察者模式下物品栏会被客户端自动隐藏替换】,
|
||||
* 雪球发了也看不见、无法右键,故已彻底移除雪球方案,只保留命令切换。
|
||||
* 观察者不能伤害玩家,玩家也不能伤害观察者(见 onSpectatorCombat)。
|
||||
*/
|
||||
public void handleSeeCommand(Player p) {
|
||||
handleSeeCommand(p, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode null=切换 "enter"=强制进观察者 "exit"=强制退观察者
|
||||
*/
|
||||
public void handleSeeCommand(Player p, String mode) {
|
||||
boolean already = p.getGameMode() == GameMode.SPECTATOR;
|
||||
|
||||
// ★ 退出观察者在任何世界都放行(防止玩家被带离PVP世界后卡在观察者回不来)
|
||||
if ("exit".equals(mode)) {
|
||||
if (!already) {
|
||||
p.sendMessage("§e§l[PVP观赛] §f你当前不在观察者模式");
|
||||
return;
|
||||
}
|
||||
exitSpectator(p);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPVPWorld(p.getWorld())) {
|
||||
p.sendMessage("§c§l[PVP观赛] §f仅在PVP世界可用(/pvp join 进入竞技场后再使用)");
|
||||
return;
|
||||
}
|
||||
if (p.getGameMode() == GameMode.SPECTATOR) {
|
||||
exitSpectator(p);
|
||||
|
||||
if ("enter".equals(mode)) {
|
||||
if (already) {
|
||||
p.sendMessage("§e§l[PVP观赛] §f你已经在观察者模式了");
|
||||
return;
|
||||
}
|
||||
enterSpectator(p);
|
||||
return;
|
||||
}
|
||||
enterSpectator(p);
|
||||
|
||||
// mode == null → 切换
|
||||
if (already) {
|
||||
exitSpectator(p);
|
||||
} else {
|
||||
enterSpectator(p);
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入观察者观赛:回收PVP装备(不退自己装备) → 发2个雪球 → 进入原版观察者 */
|
||||
/** 进入观察者观赛:回收PVP装备(不退自己装备) → 记录原模式 → 进入原版观察者 */
|
||||
private void enterSpectator(Player p) {
|
||||
if (p.getGameMode() == GameMode.SPECTATOR) return;
|
||||
|
||||
// 1) 回收PVP专属装备;★ 玩家自己的装备【不发还】,仍留在备份中,离场时统一还原
|
||||
回收PVPEquipment(p);
|
||||
|
||||
// 2) 发放2个不同的雪球(必须在进入观察者之前发放:观察者收不到 /give,会掉在地上)
|
||||
giveSpectatorSnowballs(p);
|
||||
|
||||
// 3) 记录原模式并进入 MC 原版观察者
|
||||
// 2) 记录原模式并进入 MC 原版观察者
|
||||
// ★ 必须【先记再切】:观察者模式下 addItem 会掉地上,故不再发放任何物品
|
||||
spectatorBackups.put(p.getName(), p.getGameMode());
|
||||
p.setGameMode(GameMode.SPECTATOR);
|
||||
|
||||
p.sendMessage("§a§l[PVP观赛] §f已进入观察者模式,可穿墙自由观赛");
|
||||
p.sendMessage("§7 §b右键「观赛·观察者视角」§7→ 原版观察者(穿墙飞行)");
|
||||
p.sendMessage("§7 §a右键「观赛·生存视角」§7→ 普通生存(落到地面观赛)");
|
||||
p.sendMessage("§7 也可再次输入 §f/pvp see §7在两种模式间切换");
|
||||
p.sendMessage("§7 §f/pvp see §7→ 切换回普通模式");
|
||||
p.sendMessage("§7 §f/pvp leave see §7→ 退出观察者");
|
||||
p.sendMessage("§7 旁观状态下你与他人互不伤害,可放心观战");
|
||||
p.playSound(p.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f);
|
||||
plugin.getLogger().info("[PVP] 玩家 " + p.getName() + " 进入观赛观察者模式");
|
||||
}
|
||||
@@ -2874,81 +2906,54 @@ public class PVPArenaManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/** 发放2个不同的雪球:观察者视角 / 生存视角(右键触发切换) */
|
||||
private void giveSpectatorSnowballs(Player p) {
|
||||
ItemStack spec = makeSpectateSnowball("§b观赛·观察者视角",
|
||||
"§7右键 → 切换到 MC 原版观察者模式", "观察者");
|
||||
ItemStack surv = makeSpectateSnowball("§a观赛·生存视角",
|
||||
"§7右键 → 切换到普通生存模式(落地观赛)", "生存");
|
||||
Map<Integer, ItemStack> left = p.getInventory().addItem(spec, surv);
|
||||
if (left != null && !left.isEmpty()) {
|
||||
for (ItemStack it : left.values()) {
|
||||
p.getWorld().dropItemNaturally(p.getLocation(), it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack makeSpectateSnowball(String name, String desc, String key) {
|
||||
ItemStack it = new ItemStack(Material.SNOWBALL);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
im.setLore(Arrays.asList(desc, "§8[" + SPECTATE_TAG + key + "]"));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
/** 判定雪球类型:0=非观赛雪球 1=观察者视角 2=生存视角 */
|
||||
private int spectateSnowballType(ItemStack item) {
|
||||
if (item == null || item.getType() != Material.SNOWBALL) return 0;
|
||||
ItemMeta im = item.getItemMeta();
|
||||
if (im == null || !im.hasLore()) return 0;
|
||||
List<String> lore = im.getLore();
|
||||
if (lore == null) return 0;
|
||||
for (String line : lore) {
|
||||
if (line == null || !line.contains(SPECTATE_TAG)) continue;
|
||||
if (line.contains("观察者")) return 1;
|
||||
if (line.contains("生存")) return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 观赛雪球:右键切换 观察者 ⇄ 生存(仅PVP世界生效)。
|
||||
* ★ 必须 setCancelled(true),否则雪球会被当成投掷物消耗掉。
|
||||
* ★ 原版限制:观察者模式下客户端只在【对着方块右键】时才发包,对空气右键不发包;
|
||||
* 若当时视线没对准方块,可改用 /pvp see 切换(两种方式等价)。
|
||||
* ★ 观察者战斗保护(仅 PVP 世界):
|
||||
* ① 观察者不能伤害任何人(旁观状态下客户端本不发攻击包,此处兜底拦截投掷物/残留判定)
|
||||
* ② 任何人不能伤害观察者(含玩家、生物、箭、TNT、摔落等一切伤害来源)
|
||||
* 观察者视角进入的EntityDamageByEntityEvent中 damager 为观察者本人;
|
||||
* 保护观察者则需同时拦掉 e.getEntity() 是观察者的情形。
|
||||
*/
|
||||
@EventHandler
|
||||
public void onSpectateSnowball(PlayerInteractEvent e) {
|
||||
Action a = e.getAction();
|
||||
if (a != Action.RIGHT_CLICK_AIR && a != Action.RIGHT_CLICK_BLOCK) return;
|
||||
if (e.getHand() == org.bukkit.inventory.EquipmentSlot.OFF_HAND) return;
|
||||
Player p = e.getPlayer();
|
||||
if (!isPVPWorld(p.getWorld())) return;
|
||||
@EventHandler(priority = org.bukkit.event.EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onSpectatorCombat(org.bukkit.event.entity.EntityDamageByEntityEvent e) {
|
||||
if (!(e.getEntity() instanceof Player)) return;
|
||||
Player victim = (Player) e.getEntity();
|
||||
|
||||
int type = spectateSnowballType(e.getItem());
|
||||
if (type == 0) return;
|
||||
|
||||
e.setCancelled(true); // 阻止雪球被投掷消耗
|
||||
|
||||
if (type == 1) { // 观察者视角
|
||||
if (p.getGameMode() == GameMode.SPECTATOR) {
|
||||
p.sendMessage("§e[PVP观赛] 你已经在观察者模式了");
|
||||
return;
|
||||
// ② 别人打观察者 → 直接取消
|
||||
if (victim.getGameMode() == GameMode.SPECTATOR) {
|
||||
if (isPVPWorld(victim.getWorld())) {
|
||||
e.setCancelled(true);
|
||||
}
|
||||
enterSpectator(p);
|
||||
} else { // 生存视角
|
||||
if (p.getGameMode() != GameMode.SPECTATOR) {
|
||||
p.sendMessage("§e[PVP观赛] 你已经在生存模式了");
|
||||
return;
|
||||
}
|
||||
exitSpectator(p);
|
||||
return;
|
||||
}
|
||||
|
||||
// ① 观察者打别人 → 取消(观察者投出的雪球/药水等弹射物伤害)
|
||||
org.bukkit.entity.Entity damager = e.getDamager();
|
||||
Player attacker = null;
|
||||
if (damager instanceof Player) {
|
||||
attacker = (Player) damager;
|
||||
} else if (damager instanceof org.bukkit.entity.Projectile) {
|
||||
org.bukkit.projectiles.ProjectileSource src =
|
||||
((org.bukkit.entity.Projectile) damager).getShooter();
|
||||
if (src instanceof Player) attacker = (Player) src;
|
||||
}
|
||||
if (attacker != null
|
||||
&& attacker.getGameMode() == GameMode.SPECTATOR
|
||||
&& isPVPWorld(attacker.getWorld())) {
|
||||
e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公共查询 ====================
|
||||
/** 观察者被其他实体(非EntityDamageByEntity路径)直接伤害时的兜底:取消 PVP 世界内对观察者的伤害 */
|
||||
@EventHandler(priority = org.bukkit.event.EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onSpectatorHurt(org.bukkit.event.entity.EntityDamageEvent e) {
|
||||
if (!(e.getEntity() instanceof Player)) return;
|
||||
Player victim = (Player) e.getEntity();
|
||||
if (victim.getGameMode() != GameMode.SPECTATOR) return;
|
||||
if (!isPVPWorld(victim.getWorld())) return;
|
||||
e.setCancelled(true);
|
||||
}
|
||||
|
||||
// ==================== 公共查询 ====================
|
||||
|
||||
public boolean isInPVPArena(String playerName) {
|
||||
return inPVPArena.contains(playerName);
|
||||
|
||||
@@ -1123,6 +1123,11 @@ public class PVPManager implements Listener {
|
||||
p, args);
|
||||
case "join":
|
||||
case "arena":
|
||||
// ★ /pvp join see → 直接进入观察者观赛模式
|
||||
if (args.length >= 2 && args[1].equalsIgnoreCase("see")) {
|
||||
plugin.getPVPArenaManager().handleSeeCommand(p, "enter");
|
||||
return true;
|
||||
}
|
||||
plugin.getPVPArenaManager().joinArena(p);
|
||||
return true;
|
||||
case "equip":
|
||||
@@ -1134,11 +1139,16 @@ public class PVPManager implements Listener {
|
||||
plugin.getPVPArenaManager().openEquipmentSelection(p);
|
||||
return true;
|
||||
case "see":
|
||||
// PVP观赛:进入/退出观察者模式(仅PVP世界生效,右键雪球可随时切换)
|
||||
// PVP观赛:/pvp see=切换观察者⇄普通(仅PVP世界,权限对所有人开放)
|
||||
plugin.getPVPArenaManager().handleSeeCommand(p);
|
||||
return true;
|
||||
case "leave":
|
||||
case "exit":
|
||||
// ★ /pvp leave see → 退出观察者模式
|
||||
if (args.length >= 2 && args[1].equalsIgnoreCase("see")) {
|
||||
plugin.getPVPArenaManager().handleSeeCommand(p, "exit");
|
||||
return true;
|
||||
}
|
||||
plugin.leavePVP(p);
|
||||
return true;
|
||||
default:
|
||||
@@ -1146,7 +1156,9 @@ public class PVPManager implements Listener {
|
||||
p.sendMessage("§7——— PVP指令 ———");
|
||||
p.sendMessage("§e/pvp join §7进入PVP竞技场");
|
||||
p.sendMessage("§e/pvp equip §7调整装备(食物/盾牌/弓弩)");
|
||||
p.sendMessage("§e/pvp see §7观赛:进入/退出观察者模式(仅PVP世界)");
|
||||
p.sendMessage("§e/pvp see §7观赛:切换观察者⇄普通模式(仅PVP世界)");
|
||||
p.sendMessage("§e/pvp join see §7加入观赛:进入观察者模式");
|
||||
p.sendMessage("§e/pvp leave see §7退出观赛:离开观察者模式");
|
||||
p.sendMessage("§e/pvp leave §7离开PVP竞技场");
|
||||
p.sendMessage("§e/pvp stats [玩家] §7查看战绩");
|
||||
p.sendMessage("§e/pvp list §7查看PVP区域列表");
|
||||
|
||||
@@ -327,8 +327,14 @@ public class WebManager {
|
||||
/**
|
||||
* ★ 清空DB队列中所有未回应的web同步任务
|
||||
* (清空队列/cleartake 命令调用)
|
||||
*
|
||||
* v2:以前只清 Java 内存队列,PHP 侧 web_login_requests 表里的 pending 记录
|
||||
* 原封不动 → 下一轮轮询又原样取回来,看起来"打了几十次都没清掉"。
|
||||
* 现在增加两件事:
|
||||
* ① 复位本地"已处理"标记与TimerA在途计数(避免清完后仍被跳过/卡死)
|
||||
* ② 异步调用 PHP clear_pending_web_logins,把服务端 pending 一并清理
|
||||
*/
|
||||
public void clearQueue() {
|
||||
public String clearQueue() {
|
||||
int count = 0;
|
||||
Iterator<DbTask> it = dbTaskQueue.iterator();
|
||||
while (it.hasNext()) {
|
||||
@@ -342,7 +348,33 @@ public class WebManager {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务");
|
||||
// ★ 复位轮询在途标记:队列里的任务被丢弃后,finally不会执行,必须手工复位
|
||||
processedWebLoginRequests.clear();
|
||||
plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务,联动清理PHP待处理请求...");
|
||||
|
||||
// ★ 联动清理 PHP 侧 pending(异步,不阻塞命令线程)
|
||||
purgePhpPendingWebLogins(count);
|
||||
return "丢弃 " + count + " 个未回应web任务 + 联动清理PHP待处理登录请求";
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 联动清理 PHP 的 web_login_requests pending 记录
|
||||
* 必须使用 lambda 而非匿名内部类:jar 热替换后匿名内部类会抛
|
||||
* NoClassDefFoundError(WebManager$N 找不到),导致回写/清理永远失败。
|
||||
*/
|
||||
private void purgePhpPendingWebLogins(final int localDropped) {
|
||||
if (!enabled) return;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=clear_pending_web_logins&secret="
|
||||
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
|
||||
String resp = doGet(urlStr);
|
||||
plugin.getLogger().info("[DB队列] 清空队列联动: 本地丢弃=" + localDropped
|
||||
+ " PHP响应=" + (resp == null ? "null" : resp.substring(0, Math.min(200, resp.length()))));
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("[DB队列] 联动清理PHP待处理登录请求失败: " + t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1847,38 +1879,34 @@ public class WebManager {
|
||||
|
||||
// ★ CDK离线兑付:Timer A永不暂停,定期拉取CDK交易
|
||||
// 解决全员下线时Timer B暂停导致CDK兑换不到账的bug
|
||||
//
|
||||
// ★★★ 队列保护(v2.93):原来这一轮会一口气投出【5个】子任务,
|
||||
// 而 db-worker 是单线程、每个任务又含10~30秒级HTTP请求,
|
||||
// Timer A 却每3~5秒就再投一轮 → 队列越堆越长
|
||||
// (日志表现为「等待过久 10s+」「清空队列丢弃17个」依然慢)。
|
||||
// 现在合并为【1个】队列任务串行执行:执行顺序与原来一致
|
||||
// (单线程本来就是串行),但队列条目数降为 1/5。
|
||||
if (timerACycleCount % 5 == 0) { // 每5轮(~15-25秒)检查一次
|
||||
submitNormalDbTask("TimerA-cdkPull", () -> {
|
||||
submitNormalDbTask("TimerA-周期批处理", () -> {
|
||||
// 本地CDK(web_transactions pending)
|
||||
try {
|
||||
// 本地CDK(web_transactions pending)
|
||||
doTransactionPollCheck();
|
||||
} catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
// 远程CDK(cdk_validate_requests,sdf1计分板CDK验证)
|
||||
if (initialSyncComplete) {
|
||||
submitNormalDbTask("TimerA-cdkRemotePull", () -> {
|
||||
try {
|
||||
pullWebCdkRequestsAndValidate();
|
||||
pullSdf1PendingAndValidateWeb();
|
||||
} catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
// ★ PHP→Java变更轮询(管理员在PHP改了配置/领地,Java及时拉取)
|
||||
if (initialSyncComplete) {
|
||||
submitNormalDbTask("TimerA-pollAdminChanges", () -> {
|
||||
try { pollAdminChanges(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
if (!initialSyncComplete) return;
|
||||
|
||||
// 远程CDK(cdk_validate_requests,sdf1计分板CDK验证)
|
||||
try {
|
||||
pullWebCdkRequestsAndValidate();
|
||||
pullSdf1PendingAndValidateWeb();
|
||||
} catch (Exception e) { /* 静默 */ }
|
||||
|
||||
// ★ PHP→Java变更轮询(管理员在PHP改了配置/领地,Java及时拉取)
|
||||
try { pollAdminChanges(); } catch (Exception e) { /* 静默 */ }
|
||||
// ★ 过户cooldown检测:权限变更则取消过户
|
||||
submitNormalDbTask("TimerA-transferCancellations", () -> {
|
||||
try { handlePendingTransferCancellations(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
// ★ 异步玩家验证轮询:拉取PHP的pending_player_validations,验证后推回结果
|
||||
if (initialSyncComplete) {
|
||||
submitNormalDbTask("TimerA-pollPlayerValidations", () -> {
|
||||
try { pullPendingPlayerValidations(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
try { handlePendingTransferCancellations(); } catch (Exception e) { /* 静默 */ }
|
||||
// ★ 异步玩家验证轮询:拉取PHP的pending_player_validations,验证后推回结果
|
||||
try { pullPendingPlayerValidations(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
|
||||
// 自调度下一轮(3~5秒,快速响应登录请求,错峰避免锁库)
|
||||
@@ -3722,12 +3750,31 @@ public class WebManager {
|
||||
}
|
||||
lastBansHash = currentHash;
|
||||
|
||||
// ★ 封禁IP → 顺手推给Web黑名单(web_ip_blacklist)
|
||||
// Web端据此拦截被封IP的访问,除非该IP持有游戏内 /web 签发的token
|
||||
StringBuilder ipsb = new StringBuilder("[");
|
||||
boolean ipFirst = true;
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<org.bukkit.BanEntry<?>> ipOnlyEntries = (Set<org.bukkit.BanEntry<?>>)(Set<?>) ipBanList.getEntries();
|
||||
for (org.bukkit.BanEntry<?> entry : ipOnlyEntries) {
|
||||
if (!ipFirst) ipsb.append(",");
|
||||
ipFirst = false;
|
||||
long expireDate = entry.getExpiration() != null ? entry.getExpiration().getTime() : 0;
|
||||
ipsb.append("{\"ip\":\"").append(escapeJson(entry.getTarget()))
|
||||
.append("\",\"reason\":\"").append(escapeJson(entry.getReason() != null ? entry.getReason() : ""))
|
||||
.append("\",\"source\":\"").append(escapeJson(entry.getSource() != null ? entry.getSource() : ""))
|
||||
.append("\",\"expire\":").append(expireDate).append("}");
|
||||
}
|
||||
ipsb.append("]");
|
||||
|
||||
// ★ 改用POST避免GET URL过长(55条封禁URL可达数KB)
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("secret", secretKey);
|
||||
body.put("bans", sb.toString());
|
||||
body.put("ips", ipsb.toString());
|
||||
String resp = httpPost("api/sync.php?action=sync_bans", mapToJson(body));
|
||||
plugin.getLogger().info("[防护-sync] 封禁名单同步: " + (nameEntries.size() + ipEntries.size()) + "条 → " + resp);
|
||||
plugin.getLogger().info("[防护-sync] 封禁名单同步: " + (nameEntries.size() + ipEntries.size())
|
||||
+ "条(IP黑名单 " + ipOnlyEntries.size() + "个) → " + resp);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[防护-sync] 封禁名单同步异常: " + e.getMessage());
|
||||
}
|
||||
@@ -6328,7 +6375,9 @@ public class WebManager {
|
||||
if (arrStr.trim().equals("[]")) {
|
||||
return;
|
||||
}
|
||||
plugin.getLogger().info("[Web密码验证轮询] ★ 发现待处理请求,数组长度=" + arrStr.length());
|
||||
int pendingCount = countTopLevelJsonObjects(arrStr);
|
||||
plugin.getLogger().info("[Web密码验证轮询] ★ 发现待处理请求,条数=" + pendingCount
|
||||
+ "(JSON长度=" + arrStr.length() + ")");
|
||||
int idx = 0;
|
||||
while (true) {
|
||||
int objStart = arrStr.indexOf("{", idx);
|
||||
@@ -6376,7 +6425,15 @@ public class WebManager {
|
||||
}
|
||||
|
||||
// 异步将结果写回PHP(供Web端查询)
|
||||
sendWebLoginResult(fReqId, fName, result);
|
||||
try {
|
||||
sendWebLoginResult(fReqId, fName, result);
|
||||
} catch (Throwable t) {
|
||||
// ★ 热替换jar后类加载器可能找不到匿名/内部类(Error不是Exception,
|
||||
// 外层catch接不住)→ 复位处理标记,下一轮重新验证重试
|
||||
plugin.getLogger().warning("[Web密码验证] 回写调度失败: " + t
|
||||
+ " → 已复位处理标记,稍后重试");
|
||||
processedWebLoginRequests.remove(fReqId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web密码验证] 处理异常: player=" + fName + " error=" + e.getMessage());
|
||||
}
|
||||
@@ -6394,62 +6451,67 @@ public class WebManager {
|
||||
/**
|
||||
* 将密码验证结果写回PHP后端
|
||||
* 使用GET请求传所有参数(避免POST body解析问题),同时增加重试机制
|
||||
*
|
||||
* ★ 用 lambda 而非匿名内部类(原 WebManager$15):
|
||||
* jar 被热替换后 PluginClassLoader 找不到旧的匿名内部类,
|
||||
* 抛 NoClassDefFoundError → 回写永远失败 → PHP pending 永不清空。
|
||||
* lambda 编译为 WebManager 自身的合成方法,类已加载,不受热替换影响。
|
||||
*/
|
||||
private void sendWebLoginResult(String reqId, String playerName, String result) {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 将result转换为JSON对象格式 {"success": true/false, "status": "..."}
|
||||
String resultJson;
|
||||
if ("\"success\"".equals(result)) {
|
||||
resultJson = "{\"success\":true,\"status\":\"success\"}";
|
||||
} else if ("\"not_registered\"".equals(result)) {
|
||||
resultJson = "{\"success\":false,\"status\":\"not_registered\"}";
|
||||
} else {
|
||||
resultJson = "{\"success\":false,\"status\":\"failed\"}";
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[Web密码验证回写] ★ 开始回写PHP player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result);
|
||||
|
||||
// 使用GET请求,所有参数通过URL传递,避免POST body解析问题
|
||||
// 最多重试3次,每次间隔2秒
|
||||
boolean sent = false;
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
String resultEncoded = java.net.URLEncoder.encode(resultJson, "UTF-8");
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=complete_web_login_request"
|
||||
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
|
||||
+ "&request_id=" + reqId
|
||||
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
|
||||
+ "&result=" + resultEncoded;
|
||||
|
||||
String resp = doGet(urlStr);
|
||||
|
||||
if (resp != null) {
|
||||
sent = true;
|
||||
plugin.getLogger().info("[Web密码验证回写] ✓ 成功: player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result
|
||||
+ " PHP响应=" + resp.substring(0, Math.min(200, resp.length()))
|
||||
+ " (第" + (attempt + 1) + "次)");
|
||||
break;
|
||||
} else {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ GET失败: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次)");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 异常: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次) " + e.getMessage());
|
||||
}
|
||||
// 重试前等待
|
||||
try { Thread.sleep(2000); } catch (InterruptedException ie) { break; }
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 最终失败,已重试3次: player=" + playerName + " reqId=" + reqId);
|
||||
}
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
// 将result转换为JSON对象格式 {"success": true/false, "status": "..."}
|
||||
String resultJson;
|
||||
if ("\"success\"".equals(result)) {
|
||||
resultJson = "{\"success\":true,\"status\":\"success\"}";
|
||||
} else if ("\"not_registered\"".equals(result)) {
|
||||
resultJson = "{\"success\":false,\"status\":\"not_registered\"}";
|
||||
} else {
|
||||
resultJson = "{\"success\":false,\"status\":\"failed\"}";
|
||||
}
|
||||
}.runTaskAsynchronously(plugin);
|
||||
|
||||
plugin.getLogger().info("[Web密码验证回写] ★ 开始回写PHP player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result);
|
||||
|
||||
// 使用GET请求,所有参数通过URL传递,避免POST body解析问题
|
||||
// 最多重试3次,每次间隔2秒
|
||||
boolean sent = false;
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
String resultEncoded = java.net.URLEncoder.encode(resultJson, "UTF-8");
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=complete_web_login_request"
|
||||
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
|
||||
+ "&request_id=" + reqId
|
||||
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
|
||||
+ "&result=" + resultEncoded;
|
||||
|
||||
String resp = doGet(urlStr);
|
||||
|
||||
if (resp != null) {
|
||||
sent = true;
|
||||
plugin.getLogger().info("[Web密码验证回写] ✓ 成功: player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result
|
||||
+ " PHP响应=" + resp.substring(0, Math.min(200, resp.length()))
|
||||
+ " (第" + (attempt + 1) + "次)");
|
||||
break;
|
||||
} else {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ GET失败: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次)");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 异常: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次) " + e.getMessage());
|
||||
}
|
||||
// 重试前等待
|
||||
try { Thread.sleep(2000); } catch (InterruptedException ie) { break; }
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 最终失败,已重试3次: player=" + playerName + " reqId=" + reqId);
|
||||
// ★ 回写失败 → 复位"已处理"标记,下一轮轮询重新验证并重试写回,
|
||||
// 否则该请求会在PHP里挂满10分钟过期,Web端一直转圈
|
||||
processedWebLoginRequests.remove(reqId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6491,6 +6553,28 @@ public class WebManager {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计JSON数组中顶层对象 {…} 的个数(用于日志显示真实待处理条数,
|
||||
* 以前打印的是字符串长度,容易被误读成条数)
|
||||
*/
|
||||
private static int countTopLevelJsonObjects(String arr) {
|
||||
if (arr == null || arr.isEmpty()) return 0;
|
||||
int count = 0, depth = 0;
|
||||
boolean inString = false;
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
char c = arr.charAt(i);
|
||||
if (inString) {
|
||||
if (c == '\\' && i + 1 < arr.length()) { i++; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '{') { if (depth == 0) count++; depth++; }
|
||||
else if (c == '}') { if (depth > 0) depth--; }
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ==================== 新增功能 ====================
|
||||
|
||||
// 日志控制:避免频繁打印
|
||||
|
||||
Reference in New Issue
Block a user