From 41d8ffce3d02183620b99c760dfe9bb4fa226d5b Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Thu, 9 Jul 2026 17:09:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E/tempban=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E5=B0=81=E7=A6=81=E3=80=81/chat=20mute=E7=A6=81=E8=A8=80?= =?UTF-8?q?=E3=80=81=E5=BC=82=E5=9C=B0=E7=99=BB=E5=BD=95=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E4=B8=8E=E8=B4=A6=E5=8F=B7=E5=86=BB=E7=BB=93=E8=A7=A3=E5=86=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DurationParser: 通用时长解析(时间戳/日期/相对时长) - /tempban: Bukkit原生封禁(1秒~10年) - ChatFilterManager: 持久化禁言(muted_until)+loadMutes启动加载 - 异地登录检测: onJoin异步通知PHP比对IP归属(ip9.com.cn) - freeze/unfreeze: web_admin_changes通道执行/解除Bukkit原生封禁 - PVPArenaManager: 移植PVP全服连杀/终结播报逻辑 - DatabaseManager/plugin.yml: 风控列 + tempban命令注册 --- .../java/Sdf1_login/ChatFilterManager.java | 76 ++++++++ src/main/java/Sdf1_login/DatabaseManager.java | 46 +++++ src/main/java/Sdf1_login/DurationParser.java | 184 ++++++++++++++++++ src/main/java/Sdf1_login/Main.java | 128 +++++++++++- src/main/java/Sdf1_login/PVPArenaManager.java | 93 ++++++++- src/main/java/Sdf1_login/WebManager.java | 56 ++++++ src/main/resources/plugin.yml | 4 + 7 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 src/main/java/Sdf1_login/DurationParser.java diff --git a/src/main/java/Sdf1_login/ChatFilterManager.java b/src/main/java/Sdf1_login/ChatFilterManager.java index 0d443d9..b7a091e 100644 --- a/src/main/java/Sdf1_login/ChatFilterManager.java +++ b/src/main/java/Sdf1_login/ChatFilterManager.java @@ -36,6 +36,10 @@ public class ChatFilterManager { private final Map mutedPlayers = new ConcurrentHashMap<>(); + // ★ 禁言理由(内存镜像,持久化到 users.muted_until / mute_reason) + private final Map + muteReasons = + new ConcurrentHashMap<>(); private final Map messages = new LinkedHashMap<>(); // ★ 广告机检测:记录每个玩家发送链接的频率 @@ -889,6 +893,8 @@ public class ChatFilterManager { if (System.currentTimeMillis() >= expiry) { mutedPlayers.remove(name); + muteReasons.remove(name); + clearMuteDb(name); return false; } return true; @@ -981,6 +987,76 @@ public class ChatFilterManager { public void unmutePlayer(String name) { mutedPlayers.remove(name); + muteReasons.remove(name); + clearMuteDb(name); + } + + // ========== 禁言持久化(重启后仍生效) ========== + + /** 禁言指定玩家,到期时间戳为 expireMs(绝对毫秒) */ + public void mutePlayer(String name, long expireMs, + String reason) { + mutedPlayers.put(name, expireMs); + if (reason != null && !reason.isEmpty()) + muteReasons.put(name, reason); + persistMute(name, expireMs, reason); + plugin.getLogger().info( + "[聊天] 已禁言 " + name + " 至 " + + new java.util.Date(expireMs)); + } + + private void persistMute(String name, long expireMs, + String reason) { + try { + Main mainPlugin = (Main) plugin; + DatabaseManager db = mainPlugin.getDb(); + if (db == null) return; + db.setField(name, "muted_until", expireMs); + db.setField(name, "mute_reason", + reason == null ? "" : reason); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void clearMuteDb(String name) { + try { + Main mainPlugin = (Main) plugin; + DatabaseManager db = mainPlugin.getDb(); + if (db == null) return; + db.setField(name, "muted_until", 0); + db.setField(name, "mute_reason", ""); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** 插件启动时从数据库加载仍有效的禁言 */ + public void loadMutes() { + try { + Main mainPlugin = (Main) plugin; + DatabaseManager db = mainPlugin.getDb(); + if (db == null) return; + for (Object[] row : db.getActiveMutes()) { + String name = (String) row[0]; + long expire = (Long) row[1]; + String reason = (String) row[2]; + mutedPlayers.put(name, expire); + if (reason != null + && !reason.isEmpty()) + muteReasons.put(name, reason); + } + if (!mutedPlayers.isEmpty()) + plugin.getLogger().info( + "[聊天] 已加载 " + mutedPlayers.size() + + " 条有效禁言"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getMuteReason(String name) { + return muteReasons.get(name); } public void resetPlayer(String name) { diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index 6a392b0..5256f8d 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -91,6 +91,22 @@ public class DatabaseManager { // UI偏好:1=CLI(默认), 0=GUI safeAdd(st, "ui_mode", "INTEGER DEFAULT 1"); + // ===== 风控 / 封禁相关列 ===== + // IP归属(省+市+区),用于异地登录检测 + safeAdd(st, "last_login_location", + "TEXT DEFAULT ''"); + // 账号冻结(玩家主动冻结防被盗):0=正常 1=已冻结 + safeAdd(st, "frozen", + "INTEGER DEFAULT 0"); + // 冻结校验 token(邮件链接防猜测) + safeAdd(st, "freeze_token", + "TEXT DEFAULT ''"); + // 禁言到期时间戳(毫秒),0=未禁言 + safeAdd(st, "muted_until", + "INTEGER DEFAULT 0"); + // 禁言理由 + safeAdd(st, "mute_reason", + "TEXT DEFAULT ''"); // ===== 背包备份表:安全迁移 ===== // 检查旧表是否存在(用 inventory_data 列判断) @@ -684,6 +700,36 @@ public class DatabaseManager { } } + /** 读取当前仍有效的禁言列表(muted_until > now) */ + public List getActiveMutes() { + List list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, muted_until, " + + "mute_reason FROM users " + + "WHERE muted_until > ?"); + ps.setLong(1, + System.currentTimeMillis()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Object[] row = new Object[3]; + row[0] = rs.getString( + "player_name"); + row[1] = rs.getLong( + "muted_until"); + row[2] = rs.getString( + "mute_reason"); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + public Object getField(String name, String field) { try { diff --git a/src/main/java/Sdf1_login/DurationParser.java b/src/main/java/Sdf1_login/DurationParser.java new file mode 100644 index 0000000..afcde16 --- /dev/null +++ b/src/main/java/Sdf1_login/DurationParser.java @@ -0,0 +1,184 @@ +package Sdf1_login; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 统一时长解析器(供 /tempban 与 /chat mute 共用) + * + * parseToExpireMs(String) 返回「绝对到期时间戳(毫秒)」,调用方自行校验最小/最大区间。 + * + * 支持的写法: + * 1) 纯数字(Unix 秒时间戳): 253402271999 + * 2) 中文日期(绝对): 2026年7月9日 + * 3) ISO 日期(绝对): 2025-07-09 + * 4) 相对时长(中文/英文单位): 1秒 / 1s / 1年 / 1年2月3天 / 2天12小时 + * + * 解析规则优先级:纯数字 → 时间戳;含「年/月/日」且为完整日期格式 → 绝对日期; + * 否则按「数字+单位」逐段累加为相对时长。 + */ +public class DurationParser { + + private static final Map UNIT_MS = new HashMap<>(); + static { + // 秒 + UNIT_MS.put("秒", 1000L); + UNIT_MS.put("秒钟", 1000L); + UNIT_MS.put("s", 1000L); + UNIT_MS.put("sec", 1000L); + // 分 + UNIT_MS.put("分", 60000L); + UNIT_MS.put("分钟", 60000L); + UNIT_MS.put("mins", 60000L); + UNIT_MS.put("min", 60000L); + UNIT_MS.put("m", 60000L); + // 时 + UNIT_MS.put("时", 3600000L); + UNIT_MS.put("小时", 3600000L); + UNIT_MS.put("钟头", 3600000L); + UNIT_MS.put("h", 3600000L); + UNIT_MS.put("hr", 3600000L); + // 天 + UNIT_MS.put("天", 86400000L); + UNIT_MS.put("日", 86400000L); + UNIT_MS.put("d", 86400000L); + // 周 + UNIT_MS.put("周", 604800000L); + UNIT_MS.put("星期", 604800000L); + UNIT_MS.put("礼拜", 604800000L); + UNIT_MS.put("w", 604800000L); + // 月(按30天近似) + UNIT_MS.put("个月", 2592000000L); + UNIT_MS.put("月", 2592000000L); + UNIT_MS.put("mo", 2592000000L); + // 年(按365天近似) + UNIT_MS.put("年", 31536000000L); + UNIT_MS.put("y", 31536000000L); + } + + /** 解析为绝对到期毫秒;无法解析抛出 IllegalArgumentException */ + public static long parseToExpireMs(String input) + throws IllegalArgumentException { + if (input == null || input.trim().isEmpty()) + throw new IllegalArgumentException("时长为空"); + String s = input.trim().replace(" ", ""); + + // 1) 纯数字 → Unix 秒时间戳 + if (s.matches("\\d+")) { + long ts = Long.parseLong(s); + return ts * 1000L; + } + + // 2) 中文日期 YYYY年M月D日 + Matcher mc = Pattern.compile( + "(\\d{4})年(\\d{1,2})月(\\d{1,2})日").matcher(s); + if (mc.find()) { + return buildDateMillis(Integer.parseInt(mc.group(1)), + Integer.parseInt(mc.group(2)), + Integer.parseInt(mc.group(3))); + } + + // 3) ISO 日期 YYYY-MM-DD / YYYY/MM/DD + Matcher mi = Pattern.compile( + "(\\d{4})[-/](\\d{1,2})[-/](\\d{1,2})").matcher(s); + if (mi.find()) { + return buildDateMillis(Integer.parseInt(mi.group(1)), + Integer.parseInt(mi.group(2)), + Integer.parseInt(mi.group(3))); + } + + // 4) 相对时长(可多段组合) + long rel = parseRelative(s); + if (rel >= 0) { + return System.currentTimeMillis() + rel; + } + + throw new IllegalArgumentException("无法解析时长: " + input); + } + + private static long parseRelative(String s) { + // 按「数字 + 单位」逐段匹配并累加 + StringBuilder units = new StringBuilder(); + for (String u : UNIT_MS.keySet()) { + if (units.length() > 0) units.append("|"); + // 转义,避免正则特殊字符 + units.append(Pattern.quote(u)); + } + Pattern p = Pattern.compile( + "([0-9零一二两三四五六七八九十百千万]+)(" + units + ")"); + Matcher m = p.matcher(s); + long total = 0; + int found = 0; + while (m.find()) { + found++; + long num = parseNumber(m.group(1)); + Long u = UNIT_MS.get(m.group(2)); + if (u == null) continue; + total += num * u; + } + return found > 0 ? total : -1; + } + + private static long buildDateMillis(int y, int mo, int d) { + Calendar c = Calendar.getInstance(); + c.set(y, mo - 1, d, 0, 0, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTimeInMillis(); + } + + /** 解析数字(阿拉伯数字优先,其次中文数字 零-九/十百千万/两/壹贰叁…) */ + private static long parseNumber(String s) { + if (s.matches("\\d+")) return Long.parseLong(s); + + Map cn = new HashMap<>(); + cn.put('零', 0L); cn.put('〇', 0L); + cn.put('一', 1L); cn.put('壹', 1L); + cn.put('二', 2L); cn.put('贰', 2L); cn.put('两', 2L); + cn.put('三', 3L); cn.put('叁', 3L); + cn.put('四', 4L); cn.put('肆', 4L); + cn.put('五', 5L); cn.put('伍', 5L); + cn.put('六', 6L); cn.put('陆', 6L); + cn.put('七', 7L); cn.put('柒', 7L); + cn.put('八', 8L); cn.put('捌', 8L); + cn.put('九', 9L); cn.put('玖', 9L); + + long section = 0, num = 0; + boolean hasNum = false; + for (char ch : s.toCharArray()) { + if (cn.containsKey(ch)) { + num = cn.get(ch); + hasNum = true; + } else if (ch == '十') { + if (!hasNum) num = 1; + section += num * 10; hasNum = false; num = 0; + } else if (ch == '百') { + if (!hasNum) num = 1; + section += num * 100; hasNum = false; num = 0; + } else if (ch == '千') { + if (!hasNum) num = 1; + section += num * 1000; hasNum = false; num = 0; + } else if (ch == '万') { + if (!hasNum) num = 1; + section = (section + num) * 10000; hasNum = false; num = 0; + } else { + num = 0; + } + } + return section + (hasNum ? num : 0); + } + + /** 格式化绝对到期时间戳为「yyyy年M月d日 HH:mm」 */ + public static String formatExpire(long ms) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(ms); + return String.format("%d年%d月%d日 %02d:%02d", + c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH), + c.get(Calendar.HOUR_OF_DAY), + c.get(Calendar.MINUTE)); + } +} diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index e028013..b83b2fc 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -382,6 +382,7 @@ public class Main extends JavaPlugin chatFilter = new ChatFilterManager(this); chatFilter.loadConfig(); chatFilter.loadIllegalDomains(); // 加载非法域名后缀 + chatFilter.loadMutes(); // 启动加载持久化禁言(muted_until 字段) welcome = new WelcomeManager(this); // ===== 5.5 Web通信管理器 ===== @@ -1835,6 +1836,20 @@ public class Main extends JavaPlugin } } + // ===== 异地登录检测:异步通知PHP比较IP归属并发提醒邮件 ===== + if (webManager != null && ip != null + && !ip.isEmpty()) { + final String alertName = name; + final String alertIp = ip; + new BukkitRunnable() { + @Override + public void run() { + webManager.reportLoginLocation( + alertName, alertIp); + } + }.runTaskAsynchronously(this); + } + // ★ 传送联动:登录阶段自动开启接受传送(基岩版玩家跳过) if (teleportMgr != null) { teleportMgr.onPlayerLogin(p); @@ -4656,6 +4671,67 @@ public class Main extends JavaPlugin return true; } + // ===== /tempban 临时封禁 ===== + if (cmd.getName().equalsIgnoreCase("tempban")) { + if (sender instanceof Player + && !isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e/tempban <玩家名> <时长> [理由]"); + sender.sendMessage( + "§7时长示例: 1秒 / 1s / 1年 / 2026年7月9日 / 253402271999"); + return true; + } + String target = args[0]; + String durStr = args[1]; + StringBuilder reason = new StringBuilder(); + for (int i = 2; i < args.length; i++) { + if (reason.length() > 0) + reason.append(" "); + reason.append(args[i]); + } + long expireMs; + try { + expireMs = DurationParser + .parseToExpireMs(durStr); + } catch (IllegalArgumentException ex) { + sender.sendMessage("§c时长格式无法解析: " + + ex.getMessage()); + return true; + } + long durMs = expireMs + - System.currentTimeMillis(); + long MIN = 1000L; + long MAX = 10L * 365 * 24 * 3600 * 1000; // 约10年 + if (durMs < MIN) { + sender.sendMessage("§c封禁时长至少 1 秒"); + return true; + } + if (durMs > MAX) { + sender.sendMessage("§c封禁时长最长 10 年"); + return true; + } + String r = reason.length() > 0 + ? reason.toString() + : "您已被服务器封禁,效期至: " + + DurationParser.formatExpire(expireMs); + Bukkit.getBanList( + org.bukkit.BanList.Type.NAME).addBan( + target, r, + new java.util.Date(expireMs), + sender.getName()); + sender.sendMessage("§a已封禁 " + target + + " 至 " + DurationParser.formatExpire(expireMs)); + org.bukkit.entity.Player tp = + Bukkit.getPlayerExact(target); + if (tp != null) + tp.kickPlayer("§c§l" + r); + return true; + } + String sub = (args.length > 0) ? args[0].toLowerCase() : ""; // ===== 在 onCommand 方法体内,最开头加: ===== @@ -5128,6 +5204,56 @@ public class Main extends JavaPlugin return true; } String cs = args[0].toLowerCase(); + if (cs.equals("mute")) { + if (args.length < 3) { + sender.sendMessage( + "§e/chat mute <玩家名> <时长> [理由]"); + sender.sendMessage( + "§7时长示例: 1秒 / 1s / 1年 / 2026年7月9日 / 253402271999"); + return true; + } + String target = args[1]; + String durStr = args[2]; + StringBuilder reason = new StringBuilder(); + for (int i = 3; i < args.length; i++) { + if (reason.length() > 0) + reason.append(" "); + reason.append(args[i]); + } + long expireMs; + try { + expireMs = DurationParser + .parseToExpireMs(durStr); + } catch (IllegalArgumentException ex) { + sender.sendMessage("§c时长格式无法解析: " + + ex.getMessage()); + return true; + } + long durMs = expireMs + - System.currentTimeMillis(); + long MIN = 1000L; + long MAX = 30L * 24 * 3600 * 1000; // 1个月 + if (durMs < MIN) { + sender.sendMessage("§c禁言时长至少 1 秒"); + return true; + } + if (durMs > MAX) { + sender.sendMessage("§c禁言时长最长 1 个月"); + return true; + } + String r = reason.length() > 0 + ? reason.toString() + : "您已被管理员禁言,效期至:" + + DurationParser.formatExpire(expireMs); + chatFilter.mutePlayer(target, expireMs, r); + sender.sendMessage("§a已禁言 " + target + + " 至 " + DurationParser.formatExpire(expireMs)); + org.bukkit.entity.Player tp = + Bukkit.getPlayerExact(target); + if (tp != null) + tp.sendMessage("§c§l" + r); + return true; + } if (cs.equals("reload")) { chatFilter.loadConfig(); sender.sendMessage("§a聊天配置已重载"); @@ -6856,7 +6982,7 @@ public class Main extends JavaPlugin list.addAll(Arrays.asList( "reload", "add", "remove", "addplayer", "takeplayer", - "unmute", "reset")); + "mute", "unmute", "reset")); } if (cmd.getName().equalsIgnoreCase("cypay")) { return cypayCommand.onTabComplete(sender, cmd, label, args); diff --git a/src/main/java/Sdf1_login/PVPArenaManager.java b/src/main/java/Sdf1_login/PVPArenaManager.java index a963749..4add142 100644 --- a/src/main/java/Sdf1_login/PVPArenaManager.java +++ b/src/main/java/Sdf1_login/PVPArenaManager.java @@ -110,6 +110,14 @@ public class PVPArenaManager implements Listener { private final Map pvpKills = new ConcurrentHashMap<>(); private final Map pvpDeaths = new ConcurrentHashMap<>(); + // 连杀播报(移植自 PVPManager,全服播报) + private final Map + arenaKillSessions = + new ConcurrentHashMap<>(); + private final Set arenaTripleAnnounced = + ConcurrentHashMap.newKeySet(); + private static final String ARENA_LABEL = "PVP竞技场"; + // 记分板条目缓存:玩家名 -> 侧边栏条目字符串(用于精确清除) private final Map scoreEntries = new ConcurrentHashMap<>(); @@ -1094,8 +1102,54 @@ public class PVPArenaManager implements Listener { // paper-api 1.21.4: getKiller() 已移除,改用 DamageSource.getCausingEntity() org.bukkit.entity.Entity killer = event.getDamageSource().getCausingEntity(); if (killer instanceof Player && inPVPArena.contains(killer.getName())) { - pvpKills.put(killer.getName(), pvpKills.getOrDefault(killer.getName(), 0) + 1); - ((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + player.getName() + "\u00a7a 当前击杀: " + pvpKills.get(killer.getName())); + String kName = killer.getName(); + String vName = player.getName(); + // 总榜击杀数 + pvpKills.put(kName, pvpKills.getOrDefault(kName, 0) + 1); + + // 击杀者连杀会话 + PVPManager.KillSession ks = arenaKillSessions.get(kName); + if (ks == null + || System.currentTimeMillis() - ks.lastKill + > 5 * 60 * 1000) { + arenaTripleAnnounced.remove(kName); + PVPManager.KillSession ns = + new PVPManager.KillSession(); + ns.region = ARENA_LABEL; + ns.count = 1; + ns.firstKill = System.currentTimeMillis(); + ns.lastKill = System.currentTimeMillis(); + arenaKillSessions.put(kName, ns); + } else { + ks.count++; + ks.lastKill = System.currentTimeMillis(); + } + + // 终结检测:受害者连杀 >= 3 被终结 + PVPManager.KillSession victimSession = + arenaKillSessions.get(vName); + if (victimSession != null + && victimSession.count >= 3) { + String endMsg = "§b§l" + kName + " §e终结了 " + + vName + " §e的 " + victimSession.count + + "连杀!§a恭喜终结者 " + kName + + "§e!§7快来挑战!"; + arenaKillSessions.remove(vName); + broadcastWithSound(endMsg, + org.bukkit.Sound.ENTITY_ENDER_DRAGON_GROWL); + } + // 无论如何,死者连杀清零 + arenaKillSessions.remove(vName); + + // 多杀通报(全服播报) + PVPManager.KillSession killerSession = + arenaKillSessions.get(kName); + if (killerSession != null) { + checkArenaMultiKill((Player) killer, + killerSession); + } + + ((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + vName + "\u00a7a 当前击杀: " + pvpKills.get(kName)); } pvpDeaths.put(player.getName(), pvpDeaths.getOrDefault(player.getName(), 0) + 1); refreshPVPScoreboard(); @@ -1119,6 +1173,41 @@ public class PVPArenaManager implements Listener { }.runTaskLater(plugin, 20L); // 1秒后 } + // ===== 全服击杀播报(移植自 PVPManager) ===== + private void broadcastWithSound(String msg, + org.bukkit.Sound sound) { + Bukkit.broadcastMessage(msg); + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), sound, + 1.0f, 1.0f); + } + } + + private void checkArenaMultiKill(Player killer, + PVPManager.KillSession session) { + int count = session.count; + String kName = killer.getName(); + String msg = null; + if (count >= 5) { + msg = "§c§l" + kName + " §e在「" + ARENA_LABEL + + "」达成 " + count + "连杀!§7§l快来挑战!"; + } else if (count == 3) { + if (arenaTripleAnnounced.add(kName)) { + msg = "§6§l三杀!" + kName + + " 势不可挡!§7快来挑战!"; + } + } + if (msg != null) { + if (count >= 5) { + broadcastWithSound(msg, + org.bukkit.Sound.ENTITY_ENDER_DRAGON_GROWL); + } else { + broadcastWithSound(msg, + org.bukkit.Sound.BLOCK_END_PORTAL_SPAWN); + } + } + } + /** * 玩家加入事件 — 如果上次在PVP中断线,还原背包 */ diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index 897b5dd..b118911 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -3908,6 +3908,39 @@ public class WebManager { } break; } + case "freeze": { + // ★ 玩家主动冻结账号(异地登录邮件触发):Bukkit 原生封禁 + String target = targetName; + if (!target.isEmpty()) { + String reason = "§c§l您的账号已被临时冻结(疑似被盗)\n§7请到网页修改密码后自动解冻"; + Bukkit.getBanList( + org.bukkit.BanList.Type.NAME) + .addBan(target, reason, + null, "security"); + org.bukkit.entity.Player fp = + Bukkit.getPlayerExact(target); + if (fp != null) + fp.kickPlayer(reason); + plugin.getLogger().info( + "[安全] 已按玩家请求冻结账号: " + + target); + applied = true; + } + break; + } + case "unfreeze": { + // ★ 玩家改密后解冻:解除 Bukkit 原生封禁 + String target = targetName; + if (!target.isEmpty()) { + Bukkit.getBanList( + org.bukkit.BanList.Type.NAME) + .pardon(target); + plugin.getLogger().info( + "[安全] 已解冻账号: " + target); + applied = true; + } + break; + } case "group_buy": { // ★ 玩家端付费加入用户组:PHP写入pending → Java拉取执行 String buyGroup = String.valueOf(changeData.getOrDefault("group_name", "")); @@ -5289,6 +5322,29 @@ public class WebManager { return secretKey; } + /** + * 异步通知 PHP:玩家加入服务器,触发异地登录检测与提醒邮件。 + * PHP 端比较 IP 归属并决定是否发邮件(含冻结/改密链接)。 + */ + public void reportLoginLocation(String name, String ip) { + if (!enabled) return; + try { + String url = webBaseUrl + "/api/security_alert.php" + + "?action=login_location_alert" + + "&secret=" + java.net.URLEncoder + .encode(secretKey, "UTF-8") + + "&name=" + java.net.URLEncoder + .encode(name, "UTF-8") + + "&ip=" + java.net.URLEncoder + .encode(ip, "UTF-8"); + doGet(url); + } catch (Exception e) { + plugin.getLogger().warning( + "[安全] 通知PHP异地登录检测失败: " + + e.getMessage()); + } + } + // ==================== Web登录轮询 ==================== /** diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 15dcdd4..012cd3e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -153,3 +153,7 @@ commands: back: description: 快速返回最近记录点 usage: /back + + tempban: + description: 临时封禁玩家 + usage: /tempban <玩家名> <时长> [理由]