批次B3异步化 + 任务48改判系统/注册校验 + 任务50无感切换
批次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:
@@ -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 {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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); }
|
||||||
/**
|
/**
|
||||||
* 将小票书塞入潜影盒
|
* 将小票书塞入潜影盒
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user