新增3个功能:领地改名冷却、封禁名单联控、封禁广播警告

1. 领地改名冷却机制:1小时限5次,自然滚动窗口
   - AreaProtection: renameLand/renameCooldownMap/getRenameCooldown/getRemainingRenames
   - CLI: /protect cli rename <领地名> 交互式输入
   - GUI: Name Tag按钮(slot 33)显示冷却状态
   - 改名更新DB+内存+触发PHP同步

2. PHP用户管理显示封禁名单(联控Java BanList)
   - WebManager.syncBans(): 收集Bukkit名字/IP封禁→推送到PHP
   - PHP: web_player_bans表 + handleSyncBans + list_bans API
   - admin.php: 用户行红色+禁图标标注被封禁玩家

3. 封禁事件公屏广播警告
   - Main.applyBan(): 封禁后自动广播
   - broadcastBanWarning(): 玩家名+IP(如IP封禁)+理由+到期时间
   - loadBanWarningMessage(): 从释放警告消息.txt随机读取标语
   - 支持永久封禁/限时封禁/IP封禁3种场景
This commit is contained in:
2026-07-12 22:08:38 +08:00
parent 61e4ec967f
commit 569611c40f
5 changed files with 373 additions and 2 deletions
+14 -1
View File
@@ -70,7 +70,7 @@ public class AreaCLIManager {
return Component.text(text).color(color);
}
private Component header(String title) {
public static Component header(String title) {
return Component.text("\n§6§l═══════ " + title + " ═══════\n");
}
@@ -336,6 +336,19 @@ public class AreaCLIManager {
// ★ 新增:领地公告管理
p.sendMessage(clickableAction("📢", "领地公告(进出提示消息)",
"/protect cli announcement " + land.name + " 1"));
// ★ 新增:领地改名
long[] cooldown = areaProtect.getRenameCooldown(p);
String renameHint;
if (cooldown != null) {
long remainMin = cooldown[0] / 60;
long remainSecR = cooldown[0] % 60;
renameHint = "§c冷却中(已用" + cooldown[1] + "/" + 5 + "次,"
+ remainMin + "分" + remainSecR + "秒后解锁)";
} else {
renameHint = "§a点击输入新名字(1小时限5次)";
}
p.sendMessage(clickableAction("✏️", "领地改名",
"/protect cli rename " + land.name));
p.sendMessage(Component.empty()
.append(Component.text("§a[◀ 返回列表]")
@@ -245,6 +245,19 @@ public class AreaGUIManager implements Listener {
"§7将领地所有权转移给他人",
"§7需要有领地所有者权限"));
// 领地改名(位置33)
long[] cd = areaProtect.getRenameCooldown(p);
String renameLore;
if (cd != null) {
long rm = cd[0] / 60;
long rs = cd[0] % 60;
renameLore = "§c冷却中(已用" + cd[1] + "/5次)\n§c" + rm + "分" + rs + "秒后解锁";
} else {
renameLore = "§7剩余次数: §a" + areaProtect.getRemainingRenames(p) + "/5\n§71小时限5次,自然滚动窗口";
}
inv.setItem(33, createItem(Material.NAME_TAG, "§e§l领地改名",
renameLore, "", "§e点击输入新名字"));
// 返回按钮(位置48)
inv.setItem(48, createItem(Material.ARROW, "§c§l返回领地列表", ""));
@@ -941,6 +954,22 @@ public class AreaGUIManager implements Listener {
} else if (raw == 32) {
// 过户领地
openTransferLandPanel(p, landName);
} else if (raw == 33) {
// ★ 领地改名
p.closeInventory();
areaProtect.pendingEffectInput.put(p.getUniqueId(),
new String[]{landName, "rename_land"});
p.sendMessage(AreaCLIManager.header("领地改名: " + landName));
long[] cooldownInfo = areaProtect.getRenameCooldown(p);
if (cooldownInfo != null) {
long rm = cooldownInfo[0] / 60;
long rs = cooldownInfo[0] % 60;
p.sendMessage("§c冷却中!已用" + cooldownInfo[1] + "/5次," + rm + "分" + rs + "秒后解锁");
} else {
p.sendMessage("§7剩余次数: §a" + areaProtect.getRemainingRenames(p) + "/5");
}
p.sendMessage("§e请在聊天栏输入新的领地名:");
p.sendMessage("§7输入 §c取消 §7放弃修改");
} else if (raw == 31) {
// 传送 - 优先传送点,否则安全搜索中心
p.closeInventory();
+173 -1
View File
@@ -101,11 +101,15 @@ public class AreaProtection implements Listener {
// ★ 添加成员输入等待:UUID → 领地名
private final Map<java.util.UUID, String> pendingAddMemberInput = new ConcurrentHashMap<>();
// ★ 效果管理待处理输入:UUID → [landName, inputType]
private final Map<java.util.UUID, String[]> pendingEffectInput = new ConcurrentHashMap<>();
public final Map<java.util.UUID, String[]> pendingEffectInput = new ConcurrentHashMap<>();
// ★ 过户领地待输入新所有者:UUID → [landName]
public final Map<java.util.UUID, String[]> pendingTransferInput = new ConcurrentHashMap<>();
// ★ 用户组配置编辑待输入:UUID → [groupName, field("price"|"maxlands"|"priority")]
public final Map<java.util.UUID, String[]> pendingGroupEditInput = new ConcurrentHashMap<>();
// ★ 领地改名冷却:UUID → 改名时间戳列表(自然滚动窗口,1小时限5次)
private final Map<java.util.UUID, java.util.List<Long>> renameCooldownMap = new ConcurrentHashMap<>();
private static final int RENAME_LIMIT = 5;
private static final long RENAME_WINDOW_MS = 3600_000L; // 1小时
// ★ 全局配置默认值
private int globalCreatePricePerSqm = 10; // 每㎡创建价格
@@ -6271,6 +6275,42 @@ public class AreaProtection implements Listener {
}
plugin.areaCLIManager.showAnnouncementManagement(p, args[2], annPage);
break;
case "rename":
// ★ 领地改名(交互式输入)
if (args.length < 3) {
p.sendMessage("§c用法: /protect cli rename <领地名>");
break;
}
{
String rnLandName = args[2];
AreaConfig rnAc = getLand(rnLandName);
if (rnAc == null) {
p.sendMessage("§c领地不存在: " + rnLandName);
break;
}
boolean rnOwner = p.getName().equalsIgnoreCase(rnAc.owner);
boolean rnAdmin = isAreaAdmin(p);
if (!rnOwner && !rnAdmin) {
p.sendMessage("§c需要领地所有者或管理员权限");
break;
}
// 设置等待输入状态
pendingEffectInput.put(p.getUniqueId(),
new String[]{rnLandName, "rename_land"});
p.sendMessage(AreaCLIManager.header("领地改名: " + rnLandName));
long[] cd = getRenameCooldown(p);
if (cd != null) {
long rm = cd[0] / 60;
long rs = cd[0] % 60;
p.sendMessage(Component.text("§c冷却中!已用" + cd[1] + "/" + RENAME_LIMIT + "次,"
+ rm + "分" + rs + "秒后解锁"));
} else {
p.sendMessage(Component.text("§7剩余次数: §a" + getRemainingRenames(p) + "/" + RENAME_LIMIT));
}
p.sendMessage(Component.text("§e请在聊天栏输入新的领地名:"));
p.sendMessage(Component.text("§7输入 §c取消 §7放弃修改"));
}
break;
case "create":
// 跳转到创建命令
// 直接转发
@@ -8896,6 +8936,120 @@ public class AreaProtection implements Listener {
}
}
/**
* ★ 领地改名(带冷却检查)
* @return null=成功, 非null=错误/冷却信息
*/
public String renameLand(Player p, String oldName, String newName) {
// 1. 基本校验
if (newName == null || newName.trim().isEmpty()) return "新名字不能为空";
newName = newName.trim();
if (newName.length() > 20) return "名字不能超过20个字符";
if (newName.contains(" ") || newName.contains("§")) return "名字不能包含空格或颜色代码";
if (newName.equalsIgnoreCase(oldName)) return "新名字和旧名字相同";
// 2. 重名检查
if (getLand(newName) != null) return "已存在同名领地: " + newName;
// 3. 冷却检查
long now = System.currentTimeMillis();
java.util.List<Long> timestamps = renameCooldownMap.computeIfAbsent(
p.getUniqueId(), k -> new java.util.ArrayList<>());
// 清除过期记录(超出1小时窗口的)
timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS);
if (timestamps.size() >= RENAME_LIMIT) {
long firstInWindow = timestamps.get(0);
long unlockTime = firstInWindow + RENAME_WINDOW_MS;
long remainSec = (unlockTime - now) / 1000;
long remainMin = remainSec / 60;
long remainSecR = remainSec % 60;
return "改名冷却中!本小时已改名" + RENAME_LIMIT + "次,"
+ remainMin + "分" + remainSecR + "秒后解锁";
}
// 4. 执行改名
try {
if (dbConnection == null || dbConnection.isClosed()) return "数据库未连接";
// 4a. 更新 area_lands 表的 name
PreparedStatement ps = dbConnection.prepareStatement(
"UPDATE area_lands SET name = ? WHERE name = ?");
ps.setString(1, newName);
ps.setString(2, oldName);
int affected = ps.executeUpdate();
ps.close();
if (affected <= 0) return "数据库中未找到领地: " + oldName;
// 4b. 更新 area_land_permissions 表的 land_id(如果用name关联)
// land_id 是 area_lands 的自增 id,改名不影响 id,所以无需更新
// 4c. 更新内存
AreaConfig ac = areas.remove(oldName);
if (ac != null) {
ac.name = newName;
areas.put(newName, ac);
}
// 4d. 更新内存中的成员权限缓存等(如果有以旧名字为key的缓存)
// areaPlayerWhitelist 也以 landName 为 key,需同步
java.util.Set<String> whitelist = areaPlayerWhitelist.remove(oldName);
if (whitelist != null) {
areaPlayerWhitelist.put(newName, whitelist);
}
// 4e. 记录冷却
timestamps.add(now);
// 4f. 触发PHP同步
try {
plugin.webManager.syncLandData();
} catch (Exception ignored) {}
plugin.getLogger().info("[防护] 领地改名: " + oldName + " → " + newName + " (by " + p.getName() + ")");
return null; // 成功
} catch (SQLException e) {
plugin.getLogger().warning("[防护] 领地改名失败: " + e.getMessage());
return "数据库错误: " + e.getMessage();
}
}
/**
* ★ 获取改名冷却信息(用于显示在菜单中)
* @return [剩余秒数, 已用次数] 或 null(无冷却)
*/
public long[] getRenameCooldown(Player p) {
long now = System.currentTimeMillis();
java.util.List<Long> timestamps = renameCooldownMap.get(p.getUniqueId());
if (timestamps == null || timestamps.isEmpty()) return null;
// 清除过期
timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS);
int used = timestamps.size();
if (used == 0) return null;
long firstInWindow = timestamps.get(0);
long unlockTime = firstInWindow + RENAME_WINDOW_MS;
if (now >= unlockTime) return null; // 已解锁
long remainSec = (unlockTime - now) / 1000;
return new long[]{remainSec, used};
}
/**
* 获取剩余改名次数
*/
public int getRemainingRenames(Player p) {
long now = System.currentTimeMillis();
java.util.List<Long> timestamps = renameCooldownMap.get(p.getUniqueId());
if (timestamps == null) return RENAME_LIMIT;
timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS);
return Math.max(0, RENAME_LIMIT - timestamps.size());
}
/**
* 将 PHP 端同步过来的单个字段值直接写入内存 AreaConfig 对象
*/
@@ -9467,6 +9621,24 @@ public class AreaProtection implements Listener {
}
saveAreaToDb(land);
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 3);
} else if ("rename_land".equals(inputType)) {
// ★ 领地改名输入处理
if (message.trim().equals("取消") || message.trim().equals("0")) {
p.sendMessage(Component.text("§e已取消改名,保持原样"));
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, landName, 1);
return true;
}
String newName = message.trim();
String error = renameLand(p, landName, newName);
if (error != null) {
p.sendMessage(Component.text("§c改名失败: " + error));
p.sendMessage(Component.text("§7请重新输入(或输入 §c取消§7)"));
pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "rename_land"});
return true;
}
p.sendMessage(Component.text("§a✓ 领地已改名: §f" + landName + " §a→ §f" + newName));
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, newName, 1);
return true;
}
// 刷新GUI,回到原来的subPage(必须切主线程)
+97
View File
@@ -1033,6 +1033,103 @@ public class Main extends JavaPlugin
Bukkit.getBanList(type).addBan(target, reason,
expireMs == null ? null : new java.util.Date(expireMs),
sender.getName());
// ★ 公屏广播封禁警告
broadcastBanWarning(target, type, reason, expireMs, sender.getName());
}
/**
* ★ 封禁事件公屏广播警告
* 从"释放警告消息.txt"读取随机标语,结合封禁信息发送给全服
*/
private void broadcastBanWarning(String target, org.bukkit.BanList.Type type,
String reason, Long expireMs, String adminName) {
try {
// 确定封禁类型描述
boolean isIpBan = (type == org.bukkit.BanList.Type.IP);
boolean isPerm = (expireMs == null);
// 如果是IP封禁,获取被封IP对应的在线玩家名
String displayTarget = target;
String ipInfo = "";
if (isIpBan) {
// IP封禁:target是IP,尝试查找对应玩家
for (org.bukkit.entity.Player p : Bukkit.getOnlinePlayers()) {
if (p.getAddress() != null && p.getAddress().getAddress() != null) {
String playerIp = p.getAddress().getAddress().getHostAddress();
if (target.equals(playerIp)) {
displayTarget = p.getName();
ipInfo = ",IP: " + target;
break;
}
}
}
if (displayTarget.equals(target)) {
// 没找到在线玩家,直接用IP
ipInfo = ",IP: " + target;
}
}
// 构建封禁描述
String banDesc;
if (isIpBan) {
banDesc = "被永久封禁IP";
} else if (isPerm) {
banDesc = "被终身封禁";
} else {
banDesc = "被封禁至" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date(expireMs));
}
// 读取警告标语
String warningLine = loadBanWarningMessage();
// 构建消息
String msg = "§c§l⚠ " + displayTarget + ipInfo + "因违反服务器规则" + banDesc + "。";
if (reason != null && !reason.isEmpty() && !reason.startsWith("§")) {
msg += "§r§7理由: " + reason + "。";
}
msg += "§r§c" + warningLine;
// 广播
for (org.bukkit.entity.Player online : Bukkit.getOnlinePlayers()) {
online.sendMessage(msg);
}
// 控制台也打印
getLogger().info("[封禁广播] " + msg.replaceAll("§[0-9a-fk-orA-FK-OR]", ""));
} catch (Exception e) {
getLogger().warning("[封禁广播] 异常: " + e.getMessage());
}
}
/**
* 从"释放警告消息.txt"读取随机标语
*/
private String loadBanWarningMessage() {
try {
java.io.File file = new java.io.File(getDataFolder(), "释放警告消息.txt");
if (!file.exists()) {
// 创建默认文件
getDataFolder().mkdirs();
java.io.FileWriter fw = new java.io.FileWriter(file);
fw.write("请大家引以为戒,务必遵守服务器规则。\n");
fw.write("请大家务必遵守服务器规则,切勿以身试险。\n");
fw.write("请大家务必遵守规则,注意一言一行。\n");
fw.close();
}
java.util.List<String> lines = new java.util.ArrayList<>();
java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(new java.io.FileInputStream(file), java.nio.charset.StandardCharsets.UTF_8));
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()) lines.add(line);
}
br.close();
if (lines.isEmpty()) return "请大家遵守服务器规则。";
return lines.get(new java.util.Random().nextInt(lines.size()));
} catch (Exception e) {
return "请大家遵守服务器规则。";
}
}
/** 临时封禁(名字或IP),由 /tempban 与 /tempbanip 共用 */
+60
View File
@@ -3343,6 +3343,9 @@ public class WebManager {
// 4. 同步成员权限数据(area_land_permissions → web_area_permissions)
syncPermissions(areaProtect);
// 5. 同步封禁名单到PHP
syncBans();
// 5. ★ 同步插件管理员列表到PHP
syncAdmins();
@@ -3488,6 +3491,63 @@ public class WebManager {
}
}
/**
* ★ 同步封禁名单到PHP(供用户管理页面显示封禁状态)
*/
private void syncBans() {
if (!enabled) return;
try {
org.bukkit.BanList nameBanList = Bukkit.getBanList(org.bukkit.BanList.Type.NAME);
org.bukkit.BanList ipBanList = Bukkit.getBanList(org.bukkit.BanList.Type.IP);
StringBuilder sb = new StringBuilder("[");
boolean first = true;
// 名字封禁
@SuppressWarnings("unchecked")
Set<org.bukkit.BanEntry<?>> nameEntries = (Set<org.bukkit.BanEntry<?>>)(Set<?>) nameBanList.getEntries();
for (org.bukkit.BanEntry<?> entry : nameEntries) {
if (!first) sb.append(",");
first = false;
String target = entry.getTarget();
String reason = entry.getReason() != null ? entry.getReason() : "";
String source = entry.getSource() != null ? entry.getSource() : "";
long expireDate = entry.getExpiration() != null ? entry.getExpiration().getTime() : 0;
sb.append("{\"type\":\"name\",\"target\":\"").append(escapeJson(target))
.append("\",\"reason\":\"").append(escapeJson(reason))
.append("\",\"source\":\"").append(escapeJson(source))
.append("\",\"expire\":").append(expireDate).append("}");
}
// IP封禁
@SuppressWarnings("unchecked")
Set<org.bukkit.BanEntry<?>> ipEntries = (Set<org.bukkit.BanEntry<?>>)(Set<?>) ipBanList.getEntries();
for (org.bukkit.BanEntry<?> entry : ipEntries) {
if (!first) sb.append(",");
first = false;
String target = entry.getTarget();
String reason = entry.getReason() != null ? entry.getReason() : "";
String source = entry.getSource() != null ? entry.getSource() : "";
long expireDate = entry.getExpiration() != null ? entry.getExpiration().getTime() : 0;
sb.append("{\"type\":\"ip\",\"target\":\"").append(escapeJson(target))
.append("\",\"reason\":\"").append(escapeJson(reason))
.append("\",\"source\":\"").append(escapeJson(source))
.append("\",\"expire\":").append(expireDate).append("}");
}
sb.append("]");
Map<String, String> params = new LinkedHashMap<>();
params.put("action", "sync_bans");
params.put("secret", secretKey);
params.put("bans", sb.toString());
String resp = httpGet("api/sync.php", params);
plugin.getLogger().fine("[防护-sync] 封禁名单同步: " + resp);
} catch (Exception e) {
plugin.getLogger().warning("[防护-sync] 封禁名单同步异常: " + e.getMessage());
}
}
/**
* ★ 从PHP拉取用户组配置到Java本地
* PHP管理后台创建的用户组通过此方法同步到Java