diff --git a/src/main/java/Sdf1_login/AreaProtection.java b/src/main/java/Sdf1_login/AreaProtection.java index 9de1a2d..41bdd23 100644 --- a/src/main/java/Sdf1_login/AreaProtection.java +++ b/src/main/java/Sdf1_login/AreaProtection.java @@ -9940,9 +9940,17 @@ public class AreaProtection implements Listener { } } + /** + * ★ 两阶段执行: + * 阶段1(主线程):只做 Bukkit 侧判定(封禁表 / 在线状态),不碰 DB; + * 阶段2(异步):userExists 判定 + deleteAreaFromDb 落库 + 触发 PHP 同步(含 HTTP)。 + * 上报要遍历在线玩家,回主线程。 + */ private void sweepBannedOwnerLands() { if (areas.isEmpty()) return; - DatabaseManager dbMgr = plugin.getDb(); + + // ---- 阶段1(主线程,纯 Bukkit 只读判定)---- + final List candidates = new ArrayList<>(); for (AreaConfig ac : new ArrayList<>(areas.values())) { if (ac == null || ac.owner == null || ac.owner.isEmpty()) continue; // 快照遍历期间已被删除/重载的跳过 @@ -9964,31 +9972,55 @@ public class AreaProtection implements Listener { } } catch (Exception ignored) {} - // 3) 账户已注销(在线领主一律跳过,避免误删) if (reason == null) { - if (dbMgr == null) continue; + // 3) 账户已注销(在线领主一律跳过,避免误删)—— 在线判定是 Bukkit API Player ownerOnline = Bukkit.getPlayerExact(ac.owner); if (ownerOnline != null && ownerOnline.isOnline()) continue; - try { - if (!dbMgr.userExists(ac.owner)) { - reason = "账户已注销"; - } - } catch (Exception ignored) {} } - - if (reason == null) 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); - reportLandRemovalToAdmins("§6§l[防护-自动清理] §f领主 §e" + ac.owner - + " §f因§c" + reason + " §f已自动删除其领地 §e" + ac.name - + " §7(不退款) [" + coord + "]"); + // reason 为 null = 还需异步阶段用 DB 判定是否注销 + candidates.add(new Object[]{ac, reason}); } + if (candidates.isEmpty()) return; + + final DatabaseManager dbMgr = plugin.getDb(); + // ---- 阶段2(异步:DB 判定 + 删除落库 + PHP 同步)---- + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + final List 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); + }); + }); } /** diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index f473609..2416a6e 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -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, String salt) { try { diff --git a/src/main/java/Sdf1_login/GarbageManager.java b/src/main/java/Sdf1_login/GarbageManager.java index b710208..c8adf54 100644 --- a/src/main/java/Sdf1_login/GarbageManager.java +++ b/src/main/java/Sdf1_login/GarbageManager.java @@ -173,41 +173,56 @@ public class GarbageManager { } private void doCleanup() { - // 删除过期轮次的物品 - int deleteRound = + // ★ 过期轮次删除是阻塞 DB -> 先异步执行; + // 轮次递增 / 实体收集 / 广播 必须主线程 -> 异步完成后回主线程 + final int deleteRound = currentRound - maxRounds; - if (deleteRound >= 0) { - int deleted = deleteOldItems( - deleteRound); - if (deleted > 0) { - broadcast("§7[垃圾站] §f永久清理了 §e" - + deleted + " §f个过期物品"); - } - } + Bukkit.getScheduler().runTaskAsynchronously( + plugin, () -> { + final int deleted = + deleteRound >= 0 + ? deleteOldItems(deleteRound) + : 0; + Bukkit.getScheduler().runTask( + plugin, () -> { + if (deleted > 0) { + broadcast("§7[垃圾站] §f永久清理了 §e" + + deleted + " §f个过期物品"); + } - // 递增轮次 - currentRound++; - setConfigValue("round", - String.valueOf(currentRound)); + // 递增轮次 + currentRound++; + final int roundNow = + currentRound; + // ★ 轮次落库是 DB -> 异步 + Bukkit.getScheduler() + .runTaskAsynchronously( + plugin, () -> + setConfigValue("round", + String.valueOf(roundNow))); - // 收集当前掉落物 - int count = collectItems(); - broadcast("§a§l[垃圾站] §f清理完成," - + "共清理 §e" + count - + " §f个掉落物,存入回收站"); + // 收集当前掉落物 + // (实体移除必须主线程,落库已在内部转异步) + int count = collectItems(); + broadcast("§a§l[垃圾站] §f清理完成," + + "共清理 §e" + count + + " §f个掉落物,存入回收站"); - // 打印下次清理时间 - long nextMs = System.currentTimeMillis() - + (long) cleanupInterval * 1000L; - String nextTime = - new SimpleDateFormat("HH:mm:ss") - .format(new Date(nextMs)); - broadcast("§e§l[垃圾站] §f下次清理时间: §a" - + nextTime); + // 打印下次清理时间 + long nextMs = System.currentTimeMillis() + + (long) cleanupInterval * 1000L; + String nextTime = + new SimpleDateFormat("HH:mm:ss") + .format(new Date(nextMs)); + broadcast("§e§l[垃圾站] §f下次清理时间: §a" + + nextTime); + }); + }); } public int collectItems() { int count = 0; + List picked = new ArrayList<>(); for (org.bukkit.World w : Bukkit.getWorlds()) { Collection items = @@ -219,11 +234,21 @@ public class GarbageManager { || stack.getType() == Material.AIR) continue; - saveItem(stack); + // ★ 先取快照再移除实体(实体移除必须主线程) + picked.add(stack.clone()); item.remove(); count++; } } + // ★ 落库是阻塞 DB(含 SN 脱管登记)-> 丢异步批量写 + if (!picked.isEmpty()) { + final List toSave = picked; + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, () -> { + for (ItemStack s : toSave) + saveItem(s); + }); + } return count; } diff --git a/src/main/java/Sdf1_login/InventoryAutoBackup.java b/src/main/java/Sdf1_login/InventoryAutoBackup.java index 55bdbff..394bce2 100644 --- a/src/main/java/Sdf1_login/InventoryAutoBackup.java +++ b/src/main/java/Sdf1_login/InventoryAutoBackup.java @@ -70,11 +70,18 @@ public class InventoryAutoBackup { return; } - int level = p.getLevel(); - double experience = p.getExp(); // 当前等级进度 0.0~1.0 - plugin.getDb().saveAutoBackup( - name, contentsB64, armorB64, extraB64, - level, experience); - plugin.getLogger().info("[自动备份] " + name + " 背包已备份"); + final int level = p.getLevel(); + final double experience = p.getExp(); // 当前等级进度 0.0~1.0 + // ★ 读背包/等级必须主线程;落库是阻塞 DB 写 -> 投递异步执行 + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + plugin.getDb().saveAutoBackup( + name, contentsB64, armorB64, extraB64, + level, experience); + plugin.getLogger().info("[自动备份] " + name + " 背包已备份"); + } catch (Throwable t) { + plugin.getLogger().warning("[自动备份] 落库失败 (" + name + "): " + t.getMessage()); + } + }); } } diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 745e516..a12c4c7 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -532,13 +532,22 @@ public class Main extends JavaPlugin radio.startMainRadio(); new BukkitRunnable() { public void run() { - if (questTracker == null) return; + final QuestTracker qt = questTracker; + if (qt == null) return; + // ★ QuestStorage.getCondition 是同步 SELECT(SQLITE_BUSY 时 + // 会在调用线程 sleep 重试)-> 主线程只攒名单,DB 全部下沉异步 + final List names = new ArrayList<>(); for (Player p : Bukkit.getOnlinePlayers()) { if (!isFrozen(p)) - questTracker.onPlayTime( - p.getName(), 30000L); + names.add(p.getName()); } + if (names.isEmpty()) return; + Bukkit.getScheduler() + .runTaskAsynchronously(Main.this, () -> { + for (String n : names) + qt.onPlayTime(n, 30000L); + }); } }.runTaskTimer(this, 600L, 600L); // [新增] Printer 文件清理(每小时检测一次) @@ -547,7 +556,8 @@ public class Main extends JavaPlugin if (bondPrinter != null) bondPrinter.cleanupOldFiles(); } - }.runTaskTimer(this, 1200L, 72000L); + // ★ 纯文件 listFiles/delete -> 异步,不占主线程 + }.runTaskTimerAsynchronously(this, 1200L, 72000L); // 12 ===PVP=== pvpManager = new PVPManager(this); @@ -670,7 +680,8 @@ public class Main extends JavaPlugin // 这里简单采用「每小时检测一次,命中当天 0 点窗口即清理」过于繁琐, // 故改为每日定时:延迟 1 小时后启动,之后每 24 小时执行一次。 long dayTicks = 24L * 60L * 60L * 20L; - getServer().getScheduler().runTaskTimer(this, + // ★ 纯 DB 清理 + 日志,主线程跑会随保留数据量增长而卡顿 -> 异步 + getServer().getScheduler().runTaskTimerAsynchronously(this, () -> { try { long cutoff = System.currentTimeMillis() @@ -1009,8 +1020,11 @@ public class Main extends JavaPlugin private void startLoginReminder() { new BukkitRunnable() { public void run() { + // ★ 主线程只做 Bukkit 操作(踢出/自动登录/在线判定), + // db.userExists 是同步 DB → 攒名单异步查,回主线程发提示 Iterator it = joinTime.keySet().iterator(); + final List needCheck = new ArrayList<>(); while (it.hasNext()) { UUID uuid = it.next(); Player p = Bukkit.getPlayer(uuid); @@ -1033,13 +1047,28 @@ public class Main extends JavaPlugin continue; } - // 已注册→提示登录,未注册→提示注册 - if (db.userExists(p.getName())) - p.sendMessage(config.msg( - "not_logged_in")); - else - p.sendMessage(config.msg( - "not_registered")); + // 已注册→提示登录,未注册→提示注册(DB 查询下沉到异步) + needCheck.add(p.getName()); + } + + if (!needCheck.isEmpty()) { + Bukkit.getScheduler().runTaskAsynchronously(Main.this, () -> { + final Map existsMap = new LinkedHashMap<>(); + for (String n : needCheck) { + existsMap.put(n, db.userExists(n)); + } + Bukkit.getScheduler().runTask(Main.this, () -> { + for (Map.Entry 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); @@ -1067,10 +1096,13 @@ public class Main extends JavaPlugin private void startTicketAutoProcess() { new BukkitRunnable() { public void run() { - if (ticket != null) - ticket.autoProcessCompleted(); + // ★ autoProcessCompleted 是「多轮同步 DB 查询 + 写回」 + // -> 整体异步;内部所有 Bukkit 操作自行回主线程 + final TicketManager tk = ticket; + if (tk != null) + tk.autoProcessCompleted(); } - }.runTaskTimer(this, 6000L, 6000L); + }.runTaskTimerAsynchronously(this, 6000L, 6000L); } // ===== Utils ===== @@ -1281,6 +1313,33 @@ public class Main extends JavaPlugin 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 封禁名单 */ private void applyBan(CommandSender sender, String target, String reason, Long expireMs) { @@ -1416,6 +1475,8 @@ public class Main extends JavaPlugin return; } String target = args[0]; + // ★ 安全防护:目标必须已注册,否则直接返回「玩家不存在」 + if (!ensureRegistered(sender, target)) return; String durStr = args[1]; StringBuilder reason = new StringBuilder(); for (int i = 2; i < args.length; i++) { @@ -1473,6 +1534,8 @@ public class Main extends JavaPlugin return; } String target = args[0]; + // ★ 安全防护:目标必须已注册,否则直接返回「玩家不存在」 + if (!ensureRegistered(sender, target)) return; int reasonStart = 1; if (args.length > 1 && (args[1].equals("永久") @@ -3396,6 +3459,203 @@ public class Main extends JavaPlugin 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 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 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) { if (rawCommand.isEmpty()) return; String lower = rawCommand.toLowerCase(); diff --git a/src/main/java/Sdf1_login/OrderManager.java b/src/main/java/Sdf1_login/OrderManager.java index c284985..3686bfa 100644 --- a/src/main/java/Sdf1_login/OrderManager.java +++ b/src/main/java/Sdf1_login/OrderManager.java @@ -1340,10 +1340,22 @@ public class OrderManager implements Listener { } if (cur != null && cur.uuid != null) orders.add(cur); br.close(); } catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); } } + /** 写盘互斥锁:与 orders 锁分离,避免持 orders 锁做文件 IO(异步清理写盘时主线程 add 会被卡) */ + private final Object saveLock = new Object(); + private void saveOrders() { + // ★ 先在 orders 锁内取快照(极快),再持 saveLock 写盘 + // ① 主线程 recordOrder 的 add 不会被慢速写盘阻塞 + // ② 主线程与异步清理任务不会同时写 orders.dat 互相截断 + final List snap; + synchronized (orders) { snap = new ArrayList<>(orders); } + synchronized (saveLock) { saveOrdersTo(snap); } + } + + private void saveOrdersTo(List snap) { File f = new File(plugin.getDataFolder(), "orders.dat"); 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("totalOriginal=" + r.totalOriginal); pw.println("totalPaid=" + r.totalPaid); pw.println("discount=" + r.discount); pw.println("discountType=" + nn(r.discountType)); @@ -1355,7 +1367,9 @@ public class OrderManager implements Listener { } } pw.flush(); pw.close(); } 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); } /** * 将小票书塞入潜影盒 */ diff --git a/src/main/java/Sdf1_login/QuickBack.java b/src/main/java/Sdf1_login/QuickBack.java index c379cac..bd9497f 100644 --- a/src/main/java/Sdf1_login/QuickBack.java +++ b/src/main/java/Sdf1_login/QuickBack.java @@ -125,7 +125,15 @@ public class QuickBack implements Listener, CommandExecutor, TabCompleter { private void recordLocation(Player player) { Location loc = player.getLocation(); 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) { diff --git a/src/main/java/Sdf1_login/SnManager.java b/src/main/java/Sdf1_login/SnManager.java index 53f1a0e..89d0030 100644 --- a/src/main/java/Sdf1_login/SnManager.java +++ b/src/main/java/Sdf1_login/SnManager.java @@ -1416,14 +1416,33 @@ public class SnManager implements Listener { */ private void startAutoSweepTask() { try { + // ★ autoSweep 必须主线程(读写玩家背包),且内部含多次 SQLite 点查。 + // 原实现一 tick 内清点「全部」在线玩家,人数一多就是主线程尖峰 -> + // 改为「每 tick 只清点 1 名玩家」的轮转分片,仍保持约 60 秒一轮: + // 单 tick 成本 = 1 名玩家,而非全服。 + final int[] cursor = {0}; + final long[] roundStart = + {System.currentTimeMillis()}; Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() { @Override public void run() { + long now = System.currentTimeMillis(); + // 每 60 秒重开一轮 + if (now - roundStart[0] >= 60_000L) { + cursor[0] = 0; + roundStart[0] = now; + } List online = 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) { throttleErr("sweeptimer:" + t.getMessage()); } @@ -1438,7 +1457,9 @@ public class SnManager implements Listener { */ private void startCustodySweepTask() { try { - Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() { + // ★ sweepCustody 是纯 DB(UPDATE + SELECT),改异步定时器 + // 内部 lostReported 是 ConcurrentHashMap、日志线程安全 + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() { @Override public void run() { sweepCustody(); diff --git a/src/main/java/Sdf1_login/TicketManager.java b/src/main/java/Sdf1_login/TicketManager.java index 16ea4d7..e8ab50d 100644 --- a/src/main/java/Sdf1_login/TicketManager.java +++ b/src/main/java/Sdf1_login/TicketManager.java @@ -704,6 +704,11 @@ public class TicketManager { "§e§l==================="); } + /** + * ★ 由异步定时器调用(Main.startTicketAutoProcess)。 + * DB 查询/写回留在当前(异步)线程; + * 取玩家 / 发消息 / 备份背包 / 踢人 一律经 sync() 回主线程。 + */ public void autoProcessCompleted() { long timeoutMs = 10 * 60 * 1000L; @@ -737,25 +742,26 @@ public class TicketManager { .getUnconfirmedCompleted( timeoutMs); for (Map t : list) { - int id = nv(t, "id"); + final int id = nv(t, "id"); plugin.getDb().confirmTicket(id, 3); - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) - rp.sendMessage( - "§e§l[工单] §f工单 #" - + id + " 已自动完结"); - String assigned = - sv(t, "assigned_to"); - if (!assigned.isEmpty()) { - Player pp = - Bukkit.getPlayer(assigned); - if (pp != null && pp.isOnline()) - pp.sendMessage( + final String req = sv(t, "requester"); + final String asg = sv(t, "assigned_to"); + sync(() -> { + Player rp = Bukkit.getPlayer(req); + if (rp != null && rp.isOnline()) + rp.sendMessage( "§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); } @@ -765,14 +771,16 @@ public class TicketManager { .getRepliedStaleTickets( timeoutMs); for (Map t : stale) { - int id = nv(t, "id"); + final int id = nv(t, "id"); plugin.getDb().confirmTicket(id, 3); - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) - rp.sendMessage( - "§e§l[工单] §f工单 #" - + id + " 超时自动完结"); + final String req = sv(t, "requester"); + sync(() -> { + Player rp = Bukkit.getPlayer(req); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + 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) { Map t = plugin.getDb().getTicket(tid); if (t == null) return; - String requester = + final String requester = sv(t, "requester"); - String assigned = + final String assigned = sv(t, "assigned_to"); int reward = nv(t, "reward_amount"); int score = nv(t, "score"); @@ -1285,14 +1303,16 @@ public class TicketManager { requester, reward, BondManager.TX_TICKET_REWARD, "", "System", "工单 #" + tid + " 奖励"); - Player rp = Bukkit.getPlayer( - requester); - if (rp != null && rp.isOnline()) { - rp.sendMessage( - "§e§l[工单] §f获得 " - + reward - + " 债券奖励"); - } + sync(() -> { + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f获得 " + + reward + + " 债券奖励"); + } + }); } // ★ 服务商获得债券奖励 @@ -1307,17 +1327,20 @@ public class TicketManager { + " 服务奖励(评分" + score + ")"); } - Player pp = - Bukkit.getPlayer(assigned); - if (pp != null && pp.isOnline()) { - pp.sendMessage( - "§a§l[工单] §f工单 #" - + tid - + " 确认完结"); - if (bondReward > 0) - pp.sendMessage("§7债券: §e" - + bondReward + " 枚"); - } + final int br = bondReward; + sync(() -> { + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) { + pp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 确认完结"); + if (br > 0) + pp.sendMessage("§7债券: §e" + + br + " 枚"); + } + }); } } @@ -1405,31 +1428,34 @@ public class TicketManager { */ public void processDeleteAccount(int tid, Map t) { - String requester = sv(t, "requester"); + final String requester = sv(t, "requester"); plugin.getDb().updateTicketStatus( tid, "resolved"); plugin.getDb().updateTicketField(tid, "admin_confirmed", 1); - Player target = - Bukkit.getPlayerExact(requester); - if (target != null && target.isOnline()) { - plugin.backupInventory(target); - target.sendMessage( - "§a§l[工单] §f您的删号请求已通过"); - target.sendMessage( - "§7物品已备份,下次登录可取回"); - target.kickPlayer( - "§a删号成功,物品已备份。\n" - + "请等待5分钟后重新注册取回物品。"); - } else { - // 离线:仅标记,不操作数据库 - plugin.getLogger().info( - "[Sdf1_login] " + requester - + " 删号工单已通过,等待上线执行"); - } - // 延迟执行删号 + // ★ 备份背包 / 发消息 / 踢人 必须主线程 + sync(() -> { + Player target = + Bukkit.getPlayerExact(requester); + if (target != null && target.isOnline()) { + plugin.backupInventory(target); + target.sendMessage( + "§a§l[工单] §f您的删号请求已通过"); + target.sendMessage( + "§7物品已备份,下次登录可取回"); + target.kickPlayer( + "§a删号成功,物品已备份。\n" + + "请等待5分钟后重新注册取回物品。"); + } else { + // 离线:仅标记,不操作数据库 + plugin.getLogger().info( + "[Sdf1_login] " + requester + + " 删号工单已通过,等待上线执行"); + } + }); + // 延迟执行删号(DB 写 -> 异步,不占主线程) final String name = requester; - Bukkit.getScheduler().runTaskLater( + Bukkit.getScheduler().runTaskLaterAsynchronously( plugin, () -> { int deleted = plugin.getDb().deleteUser(name); diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index 22b6920..71a64ef 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -3460,33 +3460,44 @@ public class WebManager { /** * 4. 注册账号到Web端 + * ★ 内部异步:调用点在主线程(Web注册回调 runTask 里), + * HTTP 同步(token + register.php POST,10s 超时)会卡死 tick。 */ public void syncRegistration(String playerName, String passwordHash, String salt, String email, String ip) { if (!enabled) return; - try { - String token = generateAndSyncToken(playerName, "register"); + // 参数在 lambda 内只读,effectively final;email/ip 可能为 null,先落地 + 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 body = new LinkedHashMap<>(); - body.put("player", playerName); - body.put("password_hash", passwordHash); - body.put("salt", salt); - body.put("email", email != null ? email : ""); - body.put("ip", ip != null ? ip : ""); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + String token = generateAndSyncToken(fPlayer, "register"); - String response = httpPostWithToken("api/register.php?action=register", token, body); - if (response != null) { - Map result = parseJson(response); - Boolean success = (Boolean) result.get("success"); - if (Boolean.TRUE.equals(success)) { - plugin.getLogger().info("[Web通信] 注册同步成功: " + playerName); - } else { - plugin.getLogger().warning("[Web通信] 注册同步失败: " + result.get("message")); + Map body = new LinkedHashMap<>(); + body.put("player", fPlayer); + body.put("password_hash", fHash); + body.put("salt", fSalt); + body.put("email", fEmail); + body.put("ip", fIp); + + String response = httpPostWithToken("api/register.php?action=register", token, body); + if (response != null) { + Map 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()); - // 在主线程验证密码 + // ★ 异步验证密码:handleWebPasswordVerify 是纯 DB(login.db), + // 原来 runTask 到主线程跑同步查询,数据量涨起来会顶 tick final String fReqId = reqId; final String fName = playerName; final String fPwd = password; - Bukkit.getScheduler().runTask(plugin, () -> { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { String result = plugin.handleWebPasswordVerify(fName, fPwd); plugin.getLogger().info("[Web密码验证] 结果: player=" + fName + " result=" + result); @@ -6926,7 +6938,9 @@ public class WebManager { // 现在:拉取→立即ack(写confirmed表)→处理→confirm(保险) ackTransaction(txId, type); - // 在主线程处理交易 + // 在主线程处理交易(Bukkit 发货/背包操作必须主线程) + // ★ 交易内的 HTTP(confirm/refund)已由 confirmTransaction/ + // writeRefundTransaction 改为异步投递,主线程不再等待网络。 final String fTxId = txId; final String fName = playerName; final String fType = type; @@ -7645,7 +7659,15 @@ public class WebManager { } /** - * 确认交易已处理(同步调用,确保PHP收到确认后才返回,防止重启后重复发货) + * 确认交易已处理。 + * + *

★ 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) { try { @@ -7653,27 +7675,53 @@ public class WebManager { String bodyJson = "{\"tx_id\":\"" + txId + "\"}"; String postUrl = webBaseUrl + "/api/sync.php?action=confirm_transaction&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8"); - doPost(postUrl, bodyJson); + postAsyncFireAndForget("[Web交易] 确认交易 " + txId, postUrl, bodyJson); } catch (Exception e) { 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 自动退款+恢复库存。 * 用于购物车发货部分/全部失败时的整单退款。 */ - 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 { - String bodyJson = "{\"player_name\":\"" + playerName + final String bodyJson = "{\"player_name\":\"" + playerName + "\",\"amount\":" + amount + ",\"orig_tx_id\":\"" + origTxId + "\",\"reason\":\"" + reason.replace("\"", "'") + "\",\"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"); - String resp = doPost(postUrl, bodyJson); - plugin.getLogger().info("[Web交易] 已写入退款记录: 玩家=" + playerName + " 金额=" + amount + " 原交易#" + origTxId + " 响应=" + resp); + // ★ 主线程零HTTP:退款上报同样丢异步(结果仅记日志,不影响发货流程) + 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) { plugin.getLogger().warning("[Web交易] 写入退款记录失败: " + e.getMessage()); } @@ -7919,15 +7967,21 @@ public class WebManager { * 直接写入 PHP shop_config 表(secret 认证),随后由 pullShopConfig 定时器刷新本地缓存 */ public void pushShopConfig(String key, String value) { - try { - String urlStr = webBaseUrl + "/api/sync.php?action=set_shop_config&secret=" - + java.net.URLEncoder.encode(secretKey, "UTF-8") - + "&key=" + java.net.URLEncoder.encode(key, "UTF-8") - + "&value=" + java.net.URLEncoder.encode(value, "UTF-8"); - doGet(urlStr); - } catch (Exception e) { - plugin.getLogger().warning("[配置推送] 保存商店配置失败: " + e.getMessage()); - } + // ★ 调用点在命令处理(主线程):doGet 有 10s 超时,同步执行会卡死 tick + // 沿用项目惯例走 submitWebTask(webExecutor),熔断打开时静默跳过 + final String fKey = key; + final String fValue = value; + submitWebTask("pushShopConfig-" + fKey, () -> { + try { + String urlStr = webBaseUrl + "/api/sync.php?action=set_shop_config&secret=" + + 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 + ")"); - // 在主线程创建游戏账号 + // ★ 异步创建游戏账号:主体是 DB 写 + HTTP 同步, + // 只有"在线玩家自动登录"必须回主线程(Bukkit API) final String fReqId = reqId; final String fName = playerName; final String fHash = passwordHash; @@ -8450,7 +8505,7 @@ public class WebManager { final String fEmail = email != null ? email : ""; final String fIp = ipAddress != null ? ipAddress : ""; - Bukkit.getScheduler().runTask(plugin, () -> { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { // 检查玩家是否已注册 DatabaseManager dbMgr = plugin.getDb(); @@ -8485,19 +8540,19 @@ public class WebManager { dbMgr.setField(fName, "register_ip", fIp); } - // ★ 如果玩家当前在线,自动登录 - Player onlinePlayer = Bukkit.getPlayer(fName); - if (onlinePlayer != null && onlinePlayer.isOnline()) { - plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前在线,执行自动登录"); - Bukkit.getScheduler().runTask(plugin, () -> { + // ★ 如果玩家当前在线,自动登录 —— Bukkit API 必须主线程 + Bukkit.getScheduler().runTask(plugin, () -> { + Player onlinePlayer = Bukkit.getPlayer(fName); + if (onlinePlayer != null && onlinePlayer.isOnline()) { + plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前在线,执行自动登录"); plugin.autoLogin(onlinePlayer, "web_register"); onlinePlayer.sendMessage("§a[Sdf1_login] §fWeb注册成功,已自动登录!"); - }); - } else { - plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前不在线"); - } + } else { + plugin.getLogger().info("[Web注册] 玩家 " + fName + " 当前不在线"); + } + }); - // 同步注册到Web端 + // 同步注册到Web端(方法内部已异步) syncRegistration(fName, fHash, fSalt, fIp, fEmail); // 同步密码凭证到Web端(通过DB队列) @@ -8591,7 +8646,8 @@ public class WebManager { plugin.getLogger().info("[Web注册] 发现未同步用户: " + fName + ",正在补全到本地数据库"); - Bukkit.getScheduler().runTask(plugin, () -> { + // ★ 异步补全:DB 写 + HTTP 同步;自动登录回主线程 + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { dbMgr.createUser(fName, fHash, fSalt); plugin.getLogger().info("[Web注册] 补全成功: " + fName); @@ -8602,13 +8658,13 @@ public class WebManager { dbMgr.setField(fName, "register_ip", fIp); } - Player onlinePlayer = Bukkit.getPlayer(fName); - if (onlinePlayer != null && onlinePlayer.isOnline()) { - Bukkit.getScheduler().runTask(plugin, () -> { + Bukkit.getScheduler().runTask(plugin, () -> { + Player onlinePlayer = Bukkit.getPlayer(fName); + if (onlinePlayer != null && onlinePlayer.isOnline()) { plugin.autoLogin(onlinePlayer, "web_register"); onlinePlayer.sendMessage("§a[Sdf1_login] §fWeb注册成功,已自动登录!"); - }); - } + } + }); syncRegistration(fName, fHash, fSalt, fIp, fEmail); submitDbTask("补全-pushWebLoginCredentials", () -> pushWebLoginCredentials());