批次B3异步化 + 任务48改判系统/注册校验 + 任务50无感切换
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m6s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

批次B3(混合型定时器拆分):
- Main: quest PlayTime 计时器 DB 下沉异步
- Main: 工单自动处理定时器改 runTaskTimerAsynchronously
- TicketManager: 新增 sync() 辅助,autoProcessCompleted/settleTicket/processDeleteAccount Bukkit 操作回主线程
- GarbageManager: doCleanup 两段式(DB删旧异步+收集回主线程),collectItems 落库异步
- SnManager: startAutoSweepTask 分片(每 tick 只清点 1 人)
- AreaProtection: sweepBannedOwnerLands 两阶段(Bukkit主线程判定 + DB/HTTP异步删除)

任务48(改判系统 + 注册校验):
- DatabaseManager: 新增 userExistsIgnoreCase(COLLATE NOCASE)
- Main: 新增 ensureRegistered 辅助,doTempBan/doBanIp 入口加注册校验
- Main: 新增 guardVanillaBan 守卫(HIGH优先级双事件),未注册拒绝+临时封禁改判永久

任务50(无感切换):
- Main: 新增 onSeamlessCommandSwitch(MONITOR),命中指令直接执行/大小写不敏感/未命中转公屏
- Main: 新增 getCommandMapSafe + forwardAsChat
This commit is contained in:
youpaishidifu
2026-10-03 15:36:31 +08:00
parent efe00d92af
commit 96aef279cf
10 changed files with 669 additions and 197 deletions
+53 -21
View File
@@ -9940,9 +9940,17 @@ public class AreaProtection implements Listener {
} }
} }
/**
* ★ 两阶段执行:
* 阶段1(主线程):只做 Bukkit 侧判定(封禁表 / 在线状态),不碰 DB;
* 阶段2(异步):userExists 判定 + deleteAreaFromDb 落库 + 触发 PHP 同步(含 HTTP)。
* 上报要遍历在线玩家,回主线程。
*/
private void sweepBannedOwnerLands() { private void sweepBannedOwnerLands() {
if (areas.isEmpty()) return; if (areas.isEmpty()) return;
DatabaseManager dbMgr = plugin.getDb();
// ---- 阶段1(主线程,纯 Bukkit 只读判定)----
final List<Object[]> candidates = new ArrayList<>();
for (AreaConfig ac : new ArrayList<>(areas.values())) { for (AreaConfig ac : new ArrayList<>(areas.values())) {
if (ac == null || ac.owner == null || ac.owner.isEmpty()) continue; if (ac == null || ac.owner == null || ac.owner.isEmpty()) continue;
// 快照遍历期间已被删除/重载的跳过 // 快照遍历期间已被删除/重载的跳过
@@ -9964,31 +9972,55 @@ public class AreaProtection implements Listener {
} }
} catch (Exception ignored) {} } catch (Exception ignored) {}
// 3) 账户已注销(在线领主一律跳过,避免误删)
if (reason == null) { if (reason == null) {
if (dbMgr == null) continue; // 3) 账户已注销(在线领主一律跳过,避免误删)—— 在线判定是 Bukkit API
Player ownerOnline = Bukkit.getPlayerExact(ac.owner); Player ownerOnline = Bukkit.getPlayerExact(ac.owner);
if (ownerOnline != null && ownerOnline.isOnline()) continue; if (ownerOnline != null && ownerOnline.isOnline()) continue;
try {
if (!dbMgr.userExists(ac.owner)) {
reason = "账户已注销";
}
} catch (Exception ignored) {}
} }
// reason 为 null = 还需异步阶段用 DB 判定是否注销
if (reason == null) continue; candidates.add(new Object[]{ac, reason});
// 不退款、不注销账号,直接强制删除
cancelPendingDelete(ac.name);
deleteAreaFromDb(ac.name);
areas.remove(ac.name);
String coord = ac.world + " " + ac.x1 + "," + ac.z1 + " ~ " + ac.x2 + "," + ac.z2;
plugin.getLogger().warning("[防护] 自动清理领地 " + ac.name + ":领主 " + ac.owner
+ " 因" + reason + ",已强制删除(不退款)坐标 " + coord);
reportLandRemovalToAdmins("§6§l[防护-自动清理] §f领主 §e" + ac.owner
+ " §f因§c" + reason + " §f已自动删除其领地 §e" + ac.name
+ " §7(不退款) [" + coord + "]");
} }
if (candidates.isEmpty()) return;
final DatabaseManager dbMgr = plugin.getDb();
// ---- 阶段2(异步:DB 判定 + 删除落库 + PHP 同步)----
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
final List<String> reports = new ArrayList<>();
try {
for (Object[] o : candidates) {
AreaConfig ac = (AreaConfig) o[0];
String reason = (String) o[1];
if (reason == null) {
if (dbMgr == null) continue;
try {
if (!dbMgr.userExists(ac.owner)) {
reason = "账户已注销";
}
} catch (Exception ignored) {}
if (reason == null) continue;
}
if (areas.get(ac.name) != ac) continue;
// 不退款、不注销账号,直接强制删除
cancelPendingDelete(ac.name);
deleteAreaFromDb(ac.name);
areas.remove(ac.name);
String coord = ac.world + " " + ac.x1 + "," + ac.z1 + " ~ " + ac.x2 + "," + ac.z2;
plugin.getLogger().warning("[防护] 自动清理领地 " + ac.name + ":领主 " + ac.owner
+ " 因" + reason + ",已强制删除(不退款)坐标 " + coord);
reports.add("§6§l[防护-自动清理] §f领主 §e" + ac.owner
+ " §f因§c" + reason + " §f已自动删除其领地 §e" + ac.name
+ " §7(不退款) [" + coord + "]");
}
} catch (Exception e) {
plugin.getLogger().warning("[防护] 领地自动清理(异步阶段)异常: " + e.getMessage());
}
if (reports.isEmpty()) return;
// ---- 阶段3(回主线程上报:遍历在线玩家 + OP/管理员判定)----
Bukkit.getScheduler().runTask(plugin, () -> {
for (String m : reports) reportLandRemovalToAdmins(m);
});
});
} }
/** /**
@@ -600,6 +600,29 @@ public class DatabaseManager {
} }
} }
/**
* ★ 封禁防护用:大小写不敏感的注册校验。
* 返回 false 仅表示「确认未注册」;SQL 异常时返回 true 并告警
* (宁可放行,也不因 DB 抖动误拒正常的封禁操作)。
*/
public boolean userExistsIgnoreCase(String name) {
if (name == null || name.isEmpty()) return false;
try {
PreparedStatement ps = db.prepareStatement(
"SELECT 1 FROM users "
+ "WHERE player_name=? "
+ "COLLATE NOCASE");
ps.setString(1, name);
boolean r = ps.executeQuery().next();
ps.close();
return r;
} catch (SQLException e) {
logger.warning("[封禁防护] 注册校验查询失败(放行): "
+ e.getMessage());
return true;
}
}
public void createUser(String name, String hash, public void createUser(String name, String hash,
String salt) { String salt) {
try { try {
+53 -28
View File
@@ -173,41 +173,56 @@ public class GarbageManager {
} }
private void doCleanup() { private void doCleanup() {
// 删除过期轮次的物品 // ★ 过期轮次删除是阻塞 DB -> 先异步执行;
int deleteRound = // 轮次递增 / 实体收集 / 广播 必须主线程 -> 异步完成后回主线程
final int deleteRound =
currentRound - maxRounds; currentRound - maxRounds;
if (deleteRound >= 0) { Bukkit.getScheduler().runTaskAsynchronously(
int deleted = deleteOldItems( plugin, () -> {
deleteRound); final int deleted =
if (deleted > 0) { deleteRound >= 0
broadcast("§7[垃圾站] §f永久清理了 §e" ? deleteOldItems(deleteRound)
+ deleted + " §f个过期物品"); : 0;
} Bukkit.getScheduler().runTask(
} plugin, () -> {
if (deleted > 0) {
broadcast("§7[垃圾站] §f永久清理了 §e"
+ deleted + " §f个过期物品");
}
// 递增轮次 // 递增轮次
currentRound++; currentRound++;
setConfigValue("round", final int roundNow =
String.valueOf(currentRound)); currentRound;
// ★ 轮次落库是 DB -> 异步
Bukkit.getScheduler()
.runTaskAsynchronously(
plugin, () ->
setConfigValue("round",
String.valueOf(roundNow)));
// 收集当前掉落物 // 收集当前掉落物
int count = collectItems(); // (实体移除必须主线程,落库已在内部转异步)
broadcast("§a§l[垃圾站] §f清理完成," int count = collectItems();
+ "共清理 §e" + count broadcast("§a§l[垃圾站] §f清理完成,"
+ " §f个掉落物,存入回收站"); + "共清理 §e" + count
+ " §f个掉落物,存入回收站");
// 打印下次清理时间 // 打印下次清理时间
long nextMs = System.currentTimeMillis() long nextMs = System.currentTimeMillis()
+ (long) cleanupInterval * 1000L; + (long) cleanupInterval * 1000L;
String nextTime = String nextTime =
new SimpleDateFormat("HH:mm:ss") new SimpleDateFormat("HH:mm:ss")
.format(new Date(nextMs)); .format(new Date(nextMs));
broadcast("§e§l[垃圾站] §f下次清理时间: §a" broadcast("§e§l[垃圾站] §f下次清理时间: §a"
+ nextTime); + nextTime);
});
});
} }
public int collectItems() { public int collectItems() {
int count = 0; int count = 0;
List<ItemStack> picked = new ArrayList<>();
for (org.bukkit.World w : for (org.bukkit.World w :
Bukkit.getWorlds()) { Bukkit.getWorlds()) {
Collection<Item> items = Collection<Item> items =
@@ -219,11 +234,21 @@ public class GarbageManager {
|| stack.getType() || stack.getType()
== Material.AIR) == Material.AIR)
continue; continue;
saveItem(stack); // ★ 先取快照再移除实体(实体移除必须主线程)
picked.add(stack.clone());
item.remove(); item.remove();
count++; count++;
} }
} }
// ★ 落库是阻塞 DB(含 SN 脱管登记)-> 丢异步批量写
if (!picked.isEmpty()) {
final List<ItemStack> toSave = picked;
Bukkit.getScheduler()
.runTaskAsynchronously(plugin, () -> {
for (ItemStack s : toSave)
saveItem(s);
});
}
return count; return count;
} }
@@ -70,11 +70,18 @@ public class InventoryAutoBackup {
return; return;
} }
int level = p.getLevel(); final int level = p.getLevel();
double experience = p.getExp(); // 当前等级进度 0.0~1.0 final double experience = p.getExp(); // 当前等级进度 0.0~1.0
plugin.getDb().saveAutoBackup( // ★ 读背包/等级必须主线程;落库是阻塞 DB 写 -> 投递异步执行
name, contentsB64, armorB64, extraB64, Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
level, experience); try {
plugin.getLogger().info("[自动备份] " + name + " 背包已备份"); plugin.getDb().saveAutoBackup(
name, contentsB64, armorB64, extraB64,
level, experience);
plugin.getLogger().info("[自动备份] " + name + " 背包已备份");
} catch (Throwable t) {
plugin.getLogger().warning("[自动备份] 落库失败 (" + name + "): " + t.getMessage());
}
});
} }
} }
+275 -15
View File
@@ -532,13 +532,22 @@ public class Main extends JavaPlugin
radio.startMainRadio(); radio.startMainRadio();
new BukkitRunnable() { new BukkitRunnable() {
public void run() { public void run() {
if (questTracker == null) return; final QuestTracker qt = questTracker;
if (qt == null) return;
// ★ QuestStorage.getCondition 是同步 SELECT(SQLITE_BUSY 时
// 会在调用线程 sleep 重试)-> 主线程只攒名单,DB 全部下沉异步
final List<String> names = new ArrayList<>();
for (Player p : for (Player p :
Bukkit.getOnlinePlayers()) { Bukkit.getOnlinePlayers()) {
if (!isFrozen(p)) if (!isFrozen(p))
questTracker.onPlayTime( names.add(p.getName());
p.getName(), 30000L);
} }
if (names.isEmpty()) return;
Bukkit.getScheduler()
.runTaskAsynchronously(Main.this, () -> {
for (String n : names)
qt.onPlayTime(n, 30000L);
});
} }
}.runTaskTimer(this, 600L, 600L); }.runTaskTimer(this, 600L, 600L);
// [新增] Printer 文件清理(每小时检测一次) // [新增] Printer 文件清理(每小时检测一次)
@@ -547,7 +556,8 @@ public class Main extends JavaPlugin
if (bondPrinter != null) if (bondPrinter != null)
bondPrinter.cleanupOldFiles(); bondPrinter.cleanupOldFiles();
} }
}.runTaskTimer(this, 1200L, 72000L); // ★ 纯文件 listFiles/delete -> 异步,不占主线程
}.runTaskTimerAsynchronously(this, 1200L, 72000L);
// 12 ===PVP=== // 12 ===PVP===
pvpManager = new PVPManager(this); pvpManager = new PVPManager(this);
@@ -670,7 +680,8 @@ public class Main extends JavaPlugin
// 这里简单采用「每小时检测一次,命中当天 0 点窗口即清理」过于繁琐, // 这里简单采用「每小时检测一次,命中当天 0 点窗口即清理」过于繁琐,
// 故改为每日定时:延迟 1 小时后启动,之后每 24 小时执行一次。 // 故改为每日定时:延迟 1 小时后启动,之后每 24 小时执行一次。
long dayTicks = 24L * 60L * 60L * 20L; long dayTicks = 24L * 60L * 60L * 20L;
getServer().getScheduler().runTaskTimer(this, // ★ 纯 DB 清理 + 日志,主线程跑会随保留数据量增长而卡顿 -> 异步
getServer().getScheduler().runTaskTimerAsynchronously(this,
() -> { () -> {
try { try {
long cutoff = System.currentTimeMillis() long cutoff = System.currentTimeMillis()
@@ -1009,8 +1020,11 @@ public class Main extends JavaPlugin
private void startLoginReminder() { private void startLoginReminder() {
new BukkitRunnable() { new BukkitRunnable() {
public void run() { public void run() {
// ★ 主线程只做 Bukkit 操作(踢出/自动登录/在线判定),
// db.userExists 是同步 DB → 攒名单异步查,回主线程发提示
Iterator<UUID> it = Iterator<UUID> it =
joinTime.keySet().iterator(); joinTime.keySet().iterator();
final List<String> needCheck = new ArrayList<>();
while (it.hasNext()) { while (it.hasNext()) {
UUID uuid = it.next(); UUID uuid = it.next();
Player p = Bukkit.getPlayer(uuid); Player p = Bukkit.getPlayer(uuid);
@@ -1033,13 +1047,28 @@ public class Main extends JavaPlugin
continue; continue;
} }
// 已注册→提示登录,未注册→提示注册 // 已注册→提示登录,未注册→提示注册(DB 查询下沉到异步)
if (db.userExists(p.getName())) needCheck.add(p.getName());
p.sendMessage(config.msg( }
"not_logged_in"));
else if (!needCheck.isEmpty()) {
p.sendMessage(config.msg( Bukkit.getScheduler().runTaskAsynchronously(Main.this, () -> {
"not_registered")); final Map<String, Boolean> existsMap = new LinkedHashMap<>();
for (String n : needCheck) {
existsMap.put(n, db.userExists(n));
}
Bukkit.getScheduler().runTask(Main.this, () -> {
for (Map.Entry<String, Boolean> e : existsMap.entrySet()) {
Player pl = Bukkit.getPlayerExact(e.getKey());
if (pl == null || !pl.isOnline()) continue;
if (loggedIn.contains(pl.getName())) continue;
pl.sendMessage(config.msg(
Boolean.TRUE.equals(e.getValue())
? "not_logged_in"
: "not_registered"));
}
});
});
} }
} }
}.runTaskTimer(this, 200L, 200L); }.runTaskTimer(this, 200L, 200L);
@@ -1067,10 +1096,13 @@ public class Main extends JavaPlugin
private void startTicketAutoProcess() { private void startTicketAutoProcess() {
new BukkitRunnable() { new BukkitRunnable() {
public void run() { public void run() {
if (ticket != null) // ★ autoProcessCompleted 是「多轮同步 DB 查询 + 写回」
ticket.autoProcessCompleted(); // -> 整体异步;内部所有 Bukkit 操作自行回主线程
final TicketManager tk = ticket;
if (tk != null)
tk.autoProcessCompleted();
} }
}.runTaskTimer(this, 6000L, 6000L); }.runTaskTimerAsynchronously(this, 6000L, 6000L);
} }
// ===== Utils ===== // ===== Utils =====
@@ -1281,6 +1313,33 @@ public class Main extends JavaPlugin
return t.contains(":"); return t.contains(":");
} }
/**
* ★ 注册校验:封禁目标必须在 login.db 的 users 表中已注册,
* 未注册直接中止并告知执行者「玩家不存在」。
* IP 目标(isIpTarget)不参与校验;DB 未就绪或查询异常时放行(只告警)。
* 返回 true = 放行;false = 已提示,调用方应直接 return。
*/
private boolean ensureRegistered(CommandSender sender, String target) {
if (target == null || target.isEmpty()) return true;
if (isIpTarget(target)) return true; // 纯 IP 不是玩家名
DatabaseManager d = getDb();
if (d == null) return true; // DB 未就绪不阻断封禁
boolean exists;
try {
exists = d.userExistsIgnoreCase(target);
} catch (Exception ex) {
getLogger().warning("[封禁防护] 注册校验异常(放行): "
+ ex.getMessage());
return true;
}
if (!exists) {
sender.sendMessage(
"§c§l[封禁] §f玩家不存在: §e" + target);
return false;
}
return true;
}
/** 统一封禁入口:根据目标自动选择 NAME / IP 封禁名单 */ /** 统一封禁入口:根据目标自动选择 NAME / IP 封禁名单 */
private void applyBan(CommandSender sender, String target, private void applyBan(CommandSender sender, String target,
String reason, Long expireMs) { String reason, Long expireMs) {
@@ -1416,6 +1475,8 @@ public class Main extends JavaPlugin
return; return;
} }
String target = args[0]; String target = args[0];
// ★ 安全防护:目标必须已注册,否则直接返回「玩家不存在」
if (!ensureRegistered(sender, target)) return;
String durStr = args[1]; String durStr = args[1];
StringBuilder reason = new StringBuilder(); StringBuilder reason = new StringBuilder();
for (int i = 2; i < args.length; i++) { for (int i = 2; i < args.length; i++) {
@@ -1473,6 +1534,8 @@ public class Main extends JavaPlugin
return; return;
} }
String target = args[0]; String target = args[0];
// ★ 安全防护:目标必须已注册,否则直接返回「玩家不存在」
if (!ensureRegistered(sender, target)) return;
int reasonStart = 1; int reasonStart = 1;
if (args.length > 1 if (args.length > 1
&& (args[1].equals("永久") && (args[1].equals("永久")
@@ -3396,6 +3459,203 @@ public class Main extends JavaPlugin
handleBanCommand(e.getCommand().trim()); handleBanCommand(e.getCommand().trim());
} }
// =========================================================
// ★ 任务50:无感切换 —— 服务端接管聊天栏 "/指令"
// · 命中服务器指令(含原版 + 插件)→ 直接执行
// · 指令名大小写不敏感(/SDF1_login 与 /sdf1_login 等价)
// · 未命中 → 取消事件(压制原生 "Unknown command"),
// 按普通聊天经插件聊天管道转发公屏
// 优先级 MONITOR:晚于 HIGHEST 的冻结守卫,
// 未登录玩家的未知命令已先被拦下,不会绕过登录变成聊天。
// =========================================================
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onSeamlessCommandSwitch(
org.bukkit.event.player.PlayerCommandPreprocessEvent e) {
if (e.isCancelled()) return;
Player p = e.getPlayer();
String msg = e.getMessage().trim();
if (msg.isEmpty() || msg.charAt(0) != '/') return;
String body = msg.substring(1).trim();
if (body.isEmpty()) return;
org.bukkit.command.CommandMap cm = getCommandMapSafe();
if (cm == null) return; // 拿不到命令映射 -> 维持原行为
String label = body.split(" ", 2)[0];
String lowerLabel = label.toLowerCase(java.util.Locale.ROOT);
// 命中判定统一用小写:SimpleCommandMap 的 key 本身就是小写
org.bukkit.command.Command hit = cm.getCommand(lowerLabel);
if (hit == null) hit = cm.getCommand("minecraft:" + lowerLabel);
if (hit != null) {
// ★ 指令大小写不敏感:原样 label 查不到但小写查得到
// -> 取消原事件,用小写 label 重新分发一次
if (!label.equals(lowerLabel)
&& cm.getCommand(label) == null) {
e.setCancelled(true);
String rest = body.substring(label.length());
try {
Bukkit.dispatchCommand(p, lowerLabel + rest);
} catch (Throwable t) {
getLogger().warning("[无感切换] 指令分发失败: "
+ t.getMessage());
}
}
return; // 命中 -> 直接执行,交还原版流程
}
// ★ 未命中:压制原生 "Unknown command",转普通聊天
e.setCancelled(true);
forwardAsChat(p, body);
}
/**
* 反射取命令映射(Bukkit.getCommandMap 为 Paper API,
* 反射可同时兼容 Spigot / CraftServer 实现)。拿不到返回 null。
*/
private org.bukkit.command.CommandMap getCommandMapSafe() {
try {
java.lang.reflect.Method m =
Bukkit.class.getMethod("getCommandMap");
Object cm = m.invoke(null);
if (cm instanceof org.bukkit.command.CommandMap)
return (org.bukkit.command.CommandMap) cm;
} catch (Throwable ignored) {
}
try {
java.lang.reflect.Method m = Bukkit.getServer().getClass()
.getMethod("getCommandMap");
Object cm = m.invoke(Bukkit.getServer());
if (cm instanceof org.bukkit.command.CommandMap)
return (org.bukkit.command.CommandMap) cm;
} catch (Throwable ignored) {
}
return null;
}
/**
* 把「未命中指令」的文本当作玩家聊天,走插件既有聊天管道
* (验证码 / 敏感词过滤 / 自定义格式 / 其它插件监听)。
*/
private void forwardAsChat(Player p, String text) {
if (text == null || text.isEmpty()) return;
try {
java.util.Set<Player> rcpt =
new java.util.HashSet<>(Bukkit.getOnlinePlayers());
org.bukkit.event.player.AsyncPlayerChatEvent ev =
new org.bukkit.event.player.AsyncPlayerChatEvent(
false, p, text, rcpt);
Bukkit.getPluginManager().callEvent(ev);
if (ev.isCancelled()) return;
String fmt = ev.getFormat();
// 新版 API 中 recipients 可能为空 -> 兜底全服
java.util.Set<Player> rs = ev.getRecipients();
if (rs == null || rs.isEmpty())
rs = new java.util.HashSet<>(Bukkit.getOnlinePlayers());
for (Player r : rs) {
r.sendMessage(String.format(fmt,
p.getDisplayName(), ev.getMessage()));
}
} catch (Throwable t) {
getLogger().warning("[无感切换] 转发聊天失败: "
+ t.getMessage());
Bukkit.broadcastMessage(p.getDisplayName() + "§r: " + text);
}
}
// =========================================================
// ★ 任务48:改判系统 + 注册校验(原版 /ban 执行前拦截)
// 优先级 HIGH —— 早于 MONITOR 的 handleBanCommand,
// 取消后原版命令根本不执行 => 天然压制 MC 原生提示。
// =========================================================
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerBanGuard(
org.bukkit.event.player.PlayerCommandPreprocessEvent e) {
if (e.isCancelled()) return;
String msg = e.getMessage().trim();
if (msg.isEmpty() || msg.charAt(0) != '/') return;
if (!guardVanillaBan(e.getPlayer(), msg.substring(1)))
e.setCancelled(true);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onConsoleBanGuard(
org.bukkit.event.server.ServerCommandEvent e) {
if (e.isCancelled()) return;
if (!guardVanillaBan(e.getSender(), e.getCommand().trim()))
e.setCancelled(true);
}
/**
* 原版 /ban、/ban-ip 前置守卫。
* 返回 true = 放行交回原版;false = 已处理/已拒绝(调用方取消事件)。
*/
private boolean guardVanillaBan(CommandSender sender,
String rawCommand) {
if (rawCommand == null || rawCommand.isEmpty()) return true;
String lower = rawCommand.toLowerCase();
// 兼容 minecraft:ban / minecraft:ban-ip 形式
if (lower.startsWith("minecraft:")) {
rawCommand = rawCommand.substring(10);
lower = lower.substring(10);
}
boolean isNameBan = lower.equals("ban")
|| lower.startsWith("ban ");
boolean isIpBan = lower.equals("ban-ip")
|| lower.startsWith("ban-ip ")
|| lower.equals("banip")
|| lower.startsWith("banip ");
if (!isNameBan && !isIpBan) return true; // 非封禁命令放行
String[] parts = rawCommand.split("\\s+");
if (parts.length < 2) return true; // 参数不足交回原版提示
String target = parts[1];
if (isIpTarget(target)) return true; // 纯 IP 目标不校验注册
// 无权限玩家直接交回原版(原版自行拒绝),不泄露注册信息
if (sender instanceof Player && !isPrivileged(sender)
&& !sender.hasPermission("minecraft:ban")
&& !sender.hasPermission("minecraft:op")) {
return true;
}
// ---- 1) 注册校验:未注册 -> 拒绝并提示 ----
if (!ensureRegistered(sender, target)) return false;
// ---- 2) 改判:目标处于「生效中的临时封禁」时,
// 原版 /ban 直接改判为永久封禁 ----
if (isNameBan) {
try {
org.bukkit.BanEntry<?> old =
Bukkit.getBanList(org.bukkit.BanList.Type.NAME)
.getBanEntry(target);
java.util.Date exp = old == null
? null : old.getExpiration();
if (exp != null
&& exp.getTime() > System.currentTimeMillis()) {
StringBuilder rb = new StringBuilder();
for (int i = 2; i < parts.length; i++) {
if (rb.length() > 0) rb.append(' ');
rb.append(parts[i]);
}
String reason = rb.length() > 0 ? rb.toString()
: "违反服务器规则(临时封禁改判为永久)";
// 取消原版执行 -> 原生 "Banned player ..." 不再出现
applyBan(sender, target, reason, null);
sender.sendMessage(
"§6§l[改判] §f目标 §e" + target
+ " §f正处于临时封禁中,"
+ "§c已直接改判为永久封禁");
return false;
}
} catch (Exception ex) {
getLogger().warning("[改判] 读取封禁条目异常: "
+ ex.getMessage());
}
}
return true;
}
private void handleBanCommand(String rawCommand) { private void handleBanCommand(String rawCommand) {
if (rawCommand.isEmpty()) return; if (rawCommand.isEmpty()) return;
String lower = rawCommand.toLowerCase(); String lower = rawCommand.toLowerCase();
+16 -2
View File
@@ -1340,10 +1340,22 @@ public class OrderManager implements Listener {
} if (cur != null && cur.uuid != null) orders.add(cur); br.close(); } if (cur != null && cur.uuid != null) orders.add(cur); br.close();
} catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); } } catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); }
} }
/** 写盘互斥锁:与 orders 锁分离,避免持 orders 锁做文件 IO(异步清理写盘时主线程 add 会被卡) */
private final Object saveLock = new Object();
private void saveOrders() { private void saveOrders() {
// ★ 先在 orders 锁内取快照(极快),再持 saveLock 写盘
// ① 主线程 recordOrder 的 add 不会被慢速写盘阻塞
// ② 主线程与异步清理任务不会同时写 orders.dat 互相截断
final List<OrderRecord> snap;
synchronized (orders) { snap = new ArrayList<>(orders); }
synchronized (saveLock) { saveOrdersTo(snap); }
}
private void saveOrdersTo(List<OrderRecord> snap) {
File f = new File(plugin.getDataFolder(), "orders.dat"); File f = new File(plugin.getDataFolder(), "orders.dat");
try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8));
synchronized (orders) { for (OrderRecord r : orders) { pw.println("=="); { for (OrderRecord r : snap) { pw.println("==");
pw.println("id=" + r.orderId); pw.println("uuid=" + r.uuid); pw.println("player=" + r.player); pw.println("id=" + r.orderId); pw.println("uuid=" + r.uuid); pw.println("player=" + r.player);
pw.println("totalOriginal=" + r.totalOriginal); pw.println("totalPaid=" + r.totalPaid); pw.println("totalOriginal=" + r.totalOriginal); pw.println("totalPaid=" + r.totalPaid);
pw.println("discount=" + r.discount); pw.println("discountType=" + nn(r.discountType)); pw.println("discount=" + r.discount); pw.println("discountType=" + nn(r.discountType));
@@ -1355,7 +1367,9 @@ public class OrderManager implements Listener {
} } pw.flush(); pw.close(); } } pw.flush(); pw.close();
} catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); } } catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); }
} }
private void startCleanupTask() { new BukkitRunnable() { public void run() { synchronized (orders) { if (orders.removeIf(OrderManager.this::isExpired)) saveOrders(); } } }.runTaskTimer(plugin, 36000L, 36000L); } // ★ 过期清理 = 遍历 + 全量落盘(文件 IO)→ 异步定时器;
// removeIf 在 orders 锁内完成即释放,写盘走 saveLock,不长期占着 orders 锁
private void startCleanupTask() { new BukkitRunnable() { public void run() { boolean changed; synchronized (orders) { changed = orders.removeIf(OrderManager.this::isExpired); } if (changed) saveOrders(); } }.runTaskTimerAsynchronously(plugin, 36000L, 36000L); }
/** /**
* 将小票书塞入潜影盒 * 将小票书塞入潜影盒
*/ */
+9 -1
View File
@@ -125,7 +125,15 @@ public class QuickBack implements Listener, CommandExecutor, TabCompleter {
private void recordLocation(Player player) { private void recordLocation(Player player) {
Location loc = player.getLocation(); Location loc = player.getLocation();
lastKnownLocations.put(player.getName(), loc); lastKnownLocations.put(player.getName(), loc);
saveToDB(player.getName(), loc); // ★ 读坐标/更新内存缓存留主线程;写库是阻塞 DB 操作 -> 异步
final String name = player.getName();
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
saveToDB(name, loc);
} catch (Throwable t) {
plugin.getLogger().warning("[快捷返回] 坐标落库失败 (" + name + "): " + t.getMessage());
}
});
} }
public Location getLastKnownLocation(Player player) { public Location getLastKnownLocation(Player player) {
+24 -3
View File
@@ -1416,14 +1416,33 @@ public class SnManager implements Listener {
*/ */
private void startAutoSweepTask() { private void startAutoSweepTask() {
try { try {
// ★ autoSweep 必须主线程(读写玩家背包),且内部含多次 SQLite 点查。
// 原实现一 tick 内清点「全部」在线玩家,人数一多就是主线程尖峰 ->
// 改为「每 tick 只清点 1 名玩家」的轮转分片,仍保持约 60 秒一轮:
// 单 tick 成本 = 1 名玩家,而非全服。
final int[] cursor = {0};
final long[] roundStart =
{System.currentTimeMillis()};
Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() { Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() {
@Override @Override
public void run() { public void run() {
long now = System.currentTimeMillis();
// 每 60 秒重开一轮
if (now - roundStart[0] >= 60_000L) {
cursor[0] = 0;
roundStart[0] = now;
}
List<Player> online = List<Player> online =
new ArrayList<>(Bukkit.getOnlinePlayers()); new ArrayList<>(Bukkit.getOnlinePlayers());
for (Player p : online) autoSweep(p, false); if (online.isEmpty()) {
cursor[0] = 0;
return;
}
if (cursor[0] >= online.size()) return; // 本轮已清点完
Player p = online.get(cursor[0]++);
autoSweep(p, false);
} }
}, 200L, 1200L); }, 200L, 1L);
} catch (Throwable t) { } catch (Throwable t) {
throttleErr("sweeptimer:" + t.getMessage()); throttleErr("sweeptimer:" + t.getMessage());
} }
@@ -1438,7 +1457,9 @@ public class SnManager implements Listener {
*/ */
private void startCustodySweepTask() { private void startCustodySweepTask() {
try { try {
Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() { // ★ sweepCustody 是纯 DB(UPDATE + SELECT),改异步定时器
// 内部 lostReported 是 ConcurrentHashMap、日志线程安全
Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() {
@Override @Override
public void run() { public void run() {
sweepCustody(); sweepCustody();
+91 -65
View File
@@ -704,6 +704,11 @@ public class TicketManager {
"§e§l==================="); "§e§l===================");
} }
/**
* ★ 由异步定时器调用(Main.startTicketAutoProcess)。
* DB 查询/写回留在当前(异步)线程;
* 取玩家 / 发消息 / 备份背包 / 踢人 一律经 sync() 回主线程。
*/
public void autoProcessCompleted() { public void autoProcessCompleted() {
long timeoutMs = 10 * 60 * 1000L; long timeoutMs = 10 * 60 * 1000L;
@@ -737,25 +742,26 @@ public class TicketManager {
.getUnconfirmedCompleted( .getUnconfirmedCompleted(
timeoutMs); timeoutMs);
for (Map<String, Object> t : list) { for (Map<String, Object> t : list) {
int id = nv(t, "id"); final int id = nv(t, "id");
plugin.getDb().confirmTicket(id, 3); plugin.getDb().confirmTicket(id, 3);
Player rp = Bukkit.getPlayer( final String req = sv(t, "requester");
sv(t, "requester")); final String asg = sv(t, "assigned_to");
if (rp != null && rp.isOnline()) sync(() -> {
rp.sendMessage( Player rp = Bukkit.getPlayer(req);
"§e§l[工单] §f工单 #" if (rp != null && rp.isOnline())
+ id + " 已自动完结"); rp.sendMessage(
String assigned =
sv(t, "assigned_to");
if (!assigned.isEmpty()) {
Player pp =
Bukkit.getPlayer(assigned);
if (pp != null && pp.isOnline())
pp.sendMessage(
"§e§l[工单] §f工单 #" "§e§l[工单] §f工单 #"
+ id + id + " 已自动完结");
+ " 已自动完结"); if (!asg.isEmpty()) {
} Player pp =
Bukkit.getPlayer(asg);
if (pp != null && pp.isOnline())
pp.sendMessage(
"§e§l[工单] §f工单 #"
+ id
+ " 已自动完结");
}
});
settleTicket(id); settleTicket(id);
} }
@@ -765,14 +771,16 @@ public class TicketManager {
.getRepliedStaleTickets( .getRepliedStaleTickets(
timeoutMs); timeoutMs);
for (Map<String, Object> t : stale) { for (Map<String, Object> t : stale) {
int id = nv(t, "id"); final int id = nv(t, "id");
plugin.getDb().confirmTicket(id, 3); plugin.getDb().confirmTicket(id, 3);
Player rp = Bukkit.getPlayer( final String req = sv(t, "requester");
sv(t, "requester")); sync(() -> {
if (rp != null && rp.isOnline()) Player rp = Bukkit.getPlayer(req);
rp.sendMessage( if (rp != null && rp.isOnline())
"§e§l[工单] §f工单 #" rp.sendMessage(
+ id + " 超时自动完结"); "§e§l[工单] §f工单 #"
+ id + " 超时自动完结");
});
settleTicket(id); settleTicket(id);
} }
} }
@@ -1266,13 +1274,23 @@ public class TicketManager {
// ========== 私有 ========== // ========== 私有 ==========
/**
* 把 Bukkit 操作切回主线程执行。
* 已在主线程时立即执行(保持原调用方的同步语义)。
* 供异步定时器路径(autoProcessCompleted / settleTicket / processDeleteAccount)使用。
*/
private void sync(Runnable r) {
if (Bukkit.isPrimaryThread()) r.run();
else Bukkit.getScheduler().runTask(plugin, r);
}
private void settleTicket(int tid) { private void settleTicket(int tid) {
Map<String, Object> t = Map<String, Object> t =
plugin.getDb().getTicket(tid); plugin.getDb().getTicket(tid);
if (t == null) return; if (t == null) return;
String requester = final String requester =
sv(t, "requester"); sv(t, "requester");
String assigned = final String assigned =
sv(t, "assigned_to"); sv(t, "assigned_to");
int reward = nv(t, "reward_amount"); int reward = nv(t, "reward_amount");
int score = nv(t, "score"); int score = nv(t, "score");
@@ -1285,14 +1303,16 @@ public class TicketManager {
requester, reward, requester, reward,
BondManager.TX_TICKET_REWARD, "", BondManager.TX_TICKET_REWARD, "",
"System", "工单 #" + tid + " 奖励"); "System", "工单 #" + tid + " 奖励");
Player rp = Bukkit.getPlayer( sync(() -> {
requester); Player rp = Bukkit.getPlayer(
if (rp != null && rp.isOnline()) { requester);
rp.sendMessage( if (rp != null && rp.isOnline()) {
"§e§l[工单] §f获得 " rp.sendMessage(
+ reward "§e§l[工单] §f获得 "
+ " 债券奖励"); + reward
} + " 债券奖励");
}
});
} }
// ★ 服务商获得债券奖励 // ★ 服务商获得债券奖励
@@ -1307,17 +1327,20 @@ public class TicketManager {
+ " 服务奖励(评分" + " 服务奖励(评分"
+ score + ")"); + score + ")");
} }
Player pp = final int br = bondReward;
Bukkit.getPlayer(assigned); sync(() -> {
if (pp != null && pp.isOnline()) { Player pp =
pp.sendMessage( Bukkit.getPlayer(assigned);
"§a§l[工单] §f工单 #" if (pp != null && pp.isOnline()) {
+ tid pp.sendMessage(
+ " 确认完结"); "§a§l[工单] §f工单 #"
if (bondReward > 0) + tid
pp.sendMessage("§7债券: §e" + " 确认完结");
+ bondReward + " 枚"); if (br > 0)
} pp.sendMessage("§7债券: §e"
+ br + " 枚");
}
});
} }
} }
@@ -1405,31 +1428,34 @@ public class TicketManager {
*/ */
public void processDeleteAccount(int tid, public void processDeleteAccount(int tid,
Map<String, Object> t) { Map<String, Object> t) {
String requester = sv(t, "requester"); final String requester = sv(t, "requester");
plugin.getDb().updateTicketStatus( plugin.getDb().updateTicketStatus(
tid, "resolved"); tid, "resolved");
plugin.getDb().updateTicketField(tid, plugin.getDb().updateTicketField(tid,
"admin_confirmed", 1); "admin_confirmed", 1);
Player target = // ★ 备份背包 / 发消息 / 踢人 必须主线程
Bukkit.getPlayerExact(requester); sync(() -> {
if (target != null && target.isOnline()) { Player target =
plugin.backupInventory(target); Bukkit.getPlayerExact(requester);
target.sendMessage( if (target != null && target.isOnline()) {
"§a§l[工单] §f您的删号请求已通过"); plugin.backupInventory(target);
target.sendMessage( target.sendMessage(
"§7物品已备份,下次登录可取回"); "§a§l[工单] §f您的删号请求已通过");
target.kickPlayer( target.sendMessage(
"§a删号成功,物品已备份。\n" "§7物品已备份,下次登录可取回");
+ "请等待5分钟后重新注册取回物品。"); target.kickPlayer(
} else { "§a删号成功,物品已备份。\n"
// 离线:仅标记,不操作数据库 + "请等待5分钟后重新注册取回物品。");
plugin.getLogger().info( } else {
"[Sdf1_login] " + requester // 离线:仅标记,不操作数据库
+ " 删号工单已通过,等待上线执行"); plugin.getLogger().info(
} "[Sdf1_login] " + requester
// 延迟执行删号 + " 删号工单已通过,等待上线执行");
}
});
// 延迟执行删号(DB 写 -> 异步,不占主线程)
final String name = requester; final String name = requester;
Bukkit.getScheduler().runTaskLater( Bukkit.getScheduler().runTaskLaterAsynchronously(
plugin, () -> { plugin, () -> {
int deleted = int deleted =
plugin.getDb().deleteUser(name); plugin.getDb().deleteUser(name);
+112 -56
View File
@@ -3460,33 +3460,44 @@ public class WebManager {
/** /**
* 4. 注册账号到Web端 * 4. 注册账号到Web端
* ★ 内部异步:调用点在主线程(Web注册回调 runTask 里),
* HTTP 同步(token + register.php POST,10s 超时)会卡死 tick。
*/ */
public void syncRegistration(String playerName, String passwordHash, String salt, String email, String ip) { public void syncRegistration(String playerName, String passwordHash, String salt, String email, String ip) {
if (!enabled) return; if (!enabled) return;
try { // 参数在 lambda 内只读,effectively final;email/ip 可能为 null,先落地
String token = generateAndSyncToken(playerName, "register"); final String fPlayer = playerName;
final String fHash = passwordHash;
final String fSalt = salt;
final String fEmail = email != null ? email : "";
final String fIp = ip != null ? ip : "";
Map<String, Object> body = new LinkedHashMap<>(); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
body.put("player", playerName); try {
body.put("password_hash", passwordHash); String token = generateAndSyncToken(fPlayer, "register");
body.put("salt", salt);
body.put("email", email != null ? email : "");
body.put("ip", ip != null ? ip : "");
String response = httpPostWithToken("api/register.php?action=register", token, body); Map<String, Object> body = new LinkedHashMap<>();
if (response != null) { body.put("player", fPlayer);
Map<String, Object> result = parseJson(response); body.put("password_hash", fHash);
Boolean success = (Boolean) result.get("success"); body.put("salt", fSalt);
if (Boolean.TRUE.equals(success)) { body.put("email", fEmail);
plugin.getLogger().info("[Web通信] 注册同步成功: " + playerName); body.put("ip", fIp);
} else {
plugin.getLogger().warning("[Web通信] 注册同步失败: " + result.get("message")); String response = httpPostWithToken("api/register.php?action=register", token, body);
if (response != null) {
Map<String, Object> result = parseJson(response);
Boolean success = (Boolean) result.get("success");
if (Boolean.TRUE.equals(success)) {
plugin.getLogger().info("[Web通信] 注册同步成功: " + fPlayer);
} else {
plugin.getLogger().warning("[Web通信] 注册同步失败: " + result.get("message"));
}
} }
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 注册同步异常: " + e.getMessage());
} }
} catch (Exception e) { });
plugin.getLogger().warning("[Web通信] 注册同步异常: " + e.getMessage());
}
} }
/** /**
@@ -6608,11 +6619,12 @@ public class WebManager {
// 标记为已处理 // 标记为已处理
processedWebLoginRequests.put(reqId, System.currentTimeMillis()); processedWebLoginRequests.put(reqId, System.currentTimeMillis());
// 在主线程验证密码 // ★ 异步验证密码:handleWebPasswordVerify 是纯 DB(login.db),
// 原来 runTask 到主线程跑同步查询,数据量涨起来会顶 tick
final String fReqId = reqId; final String fReqId = reqId;
final String fName = playerName; final String fName = playerName;
final String fPwd = password; final String fPwd = password;
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { try {
String result = plugin.handleWebPasswordVerify(fName, fPwd); String result = plugin.handleWebPasswordVerify(fName, fPwd);
plugin.getLogger().info("[Web密码验证] 结果: player=" + fName + " result=" + result); plugin.getLogger().info("[Web密码验证] 结果: player=" + fName + " result=" + result);
@@ -6926,7 +6938,9 @@ public class WebManager {
// 现在:拉取→立即ack(写confirmed表)→处理→confirm(保险) // 现在:拉取→立即ack(写confirmed表)→处理→confirm(保险)
ackTransaction(txId, type); ackTransaction(txId, type);
// 在主线程处理交易 // 在主线程处理交易(Bukkit 发货/背包操作必须主线程)
// ★ 交易内的 HTTP(confirm/refund)已由 confirmTransaction/
// writeRefundTransaction 改为异步投递,主线程不再等待网络。
final String fTxId = txId; final String fTxId = txId;
final String fName = playerName; final String fName = playerName;
final String fType = type; final String fType = type;
@@ -7645,7 +7659,15 @@ public class WebManager {
} }
/** /**
* 确认交易已处理(同步调用,确保PHP收到确认后才返回,防止重启后重复发货) * 确认交易已处理。
*
* <p>★ 2026-10-03 主线程零HTTP改造:本地去重标记仍然同步写入,
* 但上报 PHP 的 doPost(HttpClient.send,10 秒超时)改丢异步线程。
* 原实现被 processWebTransaction 调用,而后者是被
* Bukkit.getScheduler().runTask 派回主线程执行的 —— 后端一慢,
* 主线程就被 cfHttpClient.send 卡满 10 秒,触发
* "The server has not responded for 10 seconds" 线程转储(线上报错根因)。
* 重启防重不受影响:confirmedTxIds 同步写入 + 拉取阶段 ackTransaction 已先行落库。
*/ */
private void confirmTransaction(String txId) { private void confirmTransaction(String txId) {
try { try {
@@ -7653,27 +7675,53 @@ public class WebManager {
String bodyJson = "{\"tx_id\":\"" + txId + "\"}"; String bodyJson = "{\"tx_id\":\"" + txId + "\"}";
String postUrl = webBaseUrl + "/api/sync.php?action=confirm_transaction&secret=" String postUrl = webBaseUrl + "/api/sync.php?action=confirm_transaction&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8"); + java.net.URLEncoder.encode(secretKey, "UTF-8");
doPost(postUrl, bodyJson); postAsyncFireAndForget("[Web交易] 确认交易 " + txId, postUrl, bodyJson);
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[Web交易] 确认交易 " + txId + " 失败(已标记本地已确认,不会重复处理): " + e.getMessage()); plugin.getLogger().warning("[Web交易] 确认交易 " + txId + " 失败(已标记本地已确认,不会重复处理): " + e.getMessage());
} }
} }
/**
* 异步 POST 上报(fire-and-forget):结果只用于记日志的场合,
* 绝不在主线程同步等待 HTTP。
*/
private void postAsyncFireAndForget(final String tag, final String url, final String body) {
try {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
doPost(url, body);
} catch (Exception e) {
plugin.getLogger().warning(tag + " 异步上报失败: " + e.getMessage());
}
});
} catch (Throwable t) {
plugin.getLogger().warning(tag + " 无法投递异步上报: " + t.getMessage());
}
}
/** /**
* 写入退款记录到 PHP 端(通过 sync.php),让 PHP 自动退款+恢复库存。 * 写入退款记录到 PHP 端(通过 sync.php),让 PHP 自动退款+恢复库存。
* 用于购物车发货部分/全部失败时的整单退款。 * 用于购物车发货部分/全部失败时的整单退款。
*/ */
private void writeRefundTransaction(String playerName, int amount, String origTxId, String reason, String payMode) { private void writeRefundTransaction(final String playerName, final int amount, final String origTxId,
final String reason, final String payMode) {
try { try {
String bodyJson = "{\"player_name\":\"" + playerName final String bodyJson = "{\"player_name\":\"" + playerName
+ "\",\"amount\":" + amount + "\",\"amount\":" + amount
+ ",\"orig_tx_id\":\"" + origTxId + ",\"orig_tx_id\":\"" + origTxId
+ "\",\"reason\":\"" + reason.replace("\"", "'") + "\",\"reason\":\"" + reason.replace("\"", "'")
+ "\",\"pay_mode\":\"" + (payMode != null ? payMode : "bond") + "\"}"; + "\",\"pay_mode\":\"" + (payMode != null ? payMode : "bond") + "\"}";
String postUrl = webBaseUrl + "/api/sync.php?action=write_shop_refund&secret=" final String postUrl = webBaseUrl + "/api/sync.php?action=write_shop_refund&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8"); + java.net.URLEncoder.encode(secretKey, "UTF-8");
String resp = doPost(postUrl, bodyJson); // ★ 主线程零HTTP:退款上报同样丢异步(结果仅记日志,不影响发货流程)
plugin.getLogger().info("[Web交易] 已写入退款记录: 玩家=" + playerName + " 金额=" + amount + " 原交易#" + origTxId + " 响应=" + resp); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
String resp = doPost(postUrl, bodyJson);
plugin.getLogger().info("[Web交易] 已写入退款记录: 玩家=" + playerName + " 金额=" + amount + " 原交易#" + origTxId + " 响应=" + resp);
} catch (Exception e) {
plugin.getLogger().warning("[Web交易] 写入退款记录失败: " + e.getMessage());
}
});
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[Web交易] 写入退款记录失败: " + e.getMessage()); plugin.getLogger().warning("[Web交易] 写入退款记录失败: " + e.getMessage());
} }
@@ -7919,15 +7967,21 @@ public class WebManager {
* 直接写入 PHP shop_config 表(secret 认证),随后由 pullShopConfig 定时器刷新本地缓存 * 直接写入 PHP shop_config 表(secret 认证),随后由 pullShopConfig 定时器刷新本地缓存
*/ */
public void pushShopConfig(String key, String value) { public void pushShopConfig(String key, String value) {
try { // ★ 调用点在命令处理(主线程):doGet 有 10s 超时,同步执行会卡死 tick
String urlStr = webBaseUrl + "/api/sync.php?action=set_shop_config&secret=" // 沿用项目惯例走 submitWebTask(webExecutor),熔断打开时静默跳过
+ java.net.URLEncoder.encode(secretKey, "UTF-8") final String fKey = key;
+ "&key=" + java.net.URLEncoder.encode(key, "UTF-8") final String fValue = value;
+ "&value=" + java.net.URLEncoder.encode(value, "UTF-8"); submitWebTask("pushShopConfig-" + fKey, () -> {
doGet(urlStr); try {
} catch (Exception e) { String urlStr = webBaseUrl + "/api/sync.php?action=set_shop_config&secret="
plugin.getLogger().warning("[配置推送] 保存商店配置失败: " + e.getMessage()); + java.net.URLEncoder.encode(secretKey, "UTF-8")
} + "&key=" + java.net.URLEncoder.encode(fKey, "UTF-8")
+ "&value=" + java.net.URLEncoder.encode(fValue, "UTF-8");
doGet(urlStr);
} catch (Exception e) {
plugin.getLogger().warning("[配置推送] 保存商店配置失败: " + e.getMessage());
}
});
} }
/** /**
@@ -8442,7 +8496,8 @@ public class WebManager {
plugin.getLogger().info("[Web注册] 处理Web注册请求: " + playerName + " (ID:" + reqId + ")"); plugin.getLogger().info("[Web注册] 处理Web注册请求: " + playerName + " (ID:" + reqId + ")");
// 在主线程创建游戏账号 // ★ 异步创建游戏账号:主体是 DB 写 + HTTP 同步,
// 只有"在线玩家自动登录"必须回主线程(Bukkit API)
final String fReqId = reqId; final String fReqId = reqId;
final String fName = playerName; final String fName = playerName;
final String fHash = passwordHash; final String fHash = passwordHash;
@@ -8450,7 +8505,7 @@ public class WebManager {
final String fEmail = email != null ? email : ""; final String fEmail = email != null ? email : "";
final String fIp = ipAddress != null ? ipAddress : ""; final String fIp = ipAddress != null ? ipAddress : "";
Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { try {
// 检查玩家是否已注册 // 检查玩家是否已注册
DatabaseManager dbMgr = plugin.getDb(); DatabaseManager dbMgr = plugin.getDb();
@@ -8485,19 +8540,19 @@ public class WebManager {
dbMgr.setField(fName, "register_ip", fIp); dbMgr.setField(fName, "register_ip", fIp);
} }
// ★ 如果玩家当前在线,自动登录 // ★ 如果玩家当前在线,自动登录 —— Bukkit API 必须主线程
Player onlinePlayer = Bukkit.getPlayer(fName); Bukkit.getScheduler().runTask(plugin, () -> {
if (onlinePlayer != null && onlinePlayer.isOnline()) { Player onlinePlayer = Bukkit.getPlayer(fName);
plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前在线,执行自动登录"); if (onlinePlayer != null && onlinePlayer.isOnline()) {
Bukkit.getScheduler().runTask(plugin, () -> { plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前在线,执行自动登录");
plugin.autoLogin(onlinePlayer, "web_register"); plugin.autoLogin(onlinePlayer, "web_register");
onlinePlayer.sendMessage("§a[Sdf1_login] §fWeb注册成功,已自动登录!"); onlinePlayer.sendMessage("§a[Sdf1_login] §fWeb注册成功,已自动登录!");
}); } else {
} else { plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前不在线");
plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前不在线"); }
} });
// 同步注册到Web端 // 同步注册到Web端(方法内部已异步)
syncRegistration(fName, fHash, fSalt, fIp, fEmail); syncRegistration(fName, fHash, fSalt, fIp, fEmail);
// 同步密码凭证到Web端(通过DB队列) // 同步密码凭证到Web端(通过DB队列)
@@ -8591,7 +8646,8 @@ public class WebManager {
plugin.getLogger().info("[Web注册] 发现未同步用户: " + fName + ",正在补全到本地数据库"); plugin.getLogger().info("[Web注册] 发现未同步用户: " + fName + ",正在补全到本地数据库");
Bukkit.getScheduler().runTask(plugin, () -> { // ★ 异步补全:DB 写 + HTTP 同步;自动登录回主线程
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { try {
dbMgr.createUser(fName, fHash, fSalt); dbMgr.createUser(fName, fHash, fSalt);
plugin.getLogger().info("[Web注册] 补全成功: " + fName); plugin.getLogger().info("[Web注册] 补全成功: " + fName);
@@ -8602,13 +8658,13 @@ public class WebManager {
dbMgr.setField(fName, "register_ip", fIp); dbMgr.setField(fName, "register_ip", fIp);
} }
Player onlinePlayer = Bukkit.getPlayer(fName); Bukkit.getScheduler().runTask(plugin, () -> {
if (onlinePlayer != null && onlinePlayer.isOnline()) { Player onlinePlayer = Bukkit.getPlayer(fName);
Bukkit.getScheduler().runTask(plugin, () -> { if (onlinePlayer != null && onlinePlayer.isOnline()) {
plugin.autoLogin(onlinePlayer, "web_register"); plugin.autoLogin(onlinePlayer, "web_register");
onlinePlayer.sendMessage("§a[Sdf1_login] §fWeb注册成功,已自动登录!"); onlinePlayer.sendMessage("§a[Sdf1_login] §fWeb注册成功,已自动登录!");
}); }
} });
syncRegistration(fName, fHash, fSalt, fIp, fEmail); syncRegistration(fName, fHash, fSalt, fIp, fEmail);
submitDbTask("补全-pushWebLoginCredentials", () -> pushWebLoginCredentials()); submitDbTask("补全-pushWebLoginCredentials", () -> pushWebLoginCredentials());