第十八批:cfg.json语法有效值空改继承不判损毁(回退表格/默认、真损毁才退md、每轮重查json、jar无模板日志友好化) + 新增/banget点名全量封禁查询(user/ip自动识别、遍历getEntries防NAME型API失效)
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m28s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-15 12:36:55 +08:00
parent c52d235972
commit c83eaad23f
3 changed files with 172 additions and 3 deletions
@@ -59,6 +59,7 @@ public class BanBroadcastManager {
private long startCutoffMs = 0; // 播报起点(毫秒):只播该时刻之后创建的封禁
private boolean startCutoffAuto = true; // 起点未配置 → 自动取插件数据目录创建时间(=插件安装时间)
private long autoCutoffMs = -1; // 自动起点缓存(-1=未取)
private boolean jsonSyntaxOk = false; // json 文本语法是否有效(区分「语法坏」与「语法好但值空」)
public BanBroadcastManager(Main plugin) {
this.plugin = plugin;
@@ -92,6 +93,7 @@ public class BanBroadcastManager {
/** 配置文件丢失就补一份(自愈) */
private void ensureConfigExists() {
jsonFile = findJsonConfig(); // ★ 每轮重查:运行中新增/删除的 json 都要跟上
try {
if (jsonFile != null) {
if (!banDir.exists()) banDir.mkdirs();
@@ -122,13 +124,28 @@ public class BanBroadcastManager {
mode = 1;
startCutoffAuto = true;
startCutoffMs = 0;
if (jsonFile == null) jsonFile = findJsonConfig();
jsonFile = findJsonConfig(); // ★ 每轮重查(不再只在 null 时查一次)
if (jsonFile != null && jsonFile.exists()) {
String txt = readFile(jsonFile);
if (parseJsonText(txt)) { log("已加载 json 配置: " + jsonFile.getName()); return; }
backupAndReset(jsonFile, jsonFile.getName());
if (jsonSyntaxOk) {
// ★ 第十八批:语法有效但间隔值为空(文档语义「表格/json 二者选一填」)
// → 继承 json 里已解析的播报模式/播报起点,间隔回退 md 表格
// (只取间隔,绝不覆盖已继承的模式/起点)或出厂默认,不判损毁。
boolean iv = false;
if (mdFile != null && mdFile.exists()) {
String md0 = readFile(mdFile);
if (md0 != null) iv = parseIntervalTable(md0);
}
normalize();
log("json 配置语法有效,已继承模式/起点(间隔回退"
+ (iv ? "md 表格" : "出厂默认") + "): " + jsonFile.getName());
return;
}
// 语法真损坏 → 备份+移除,继续回退 md 分支(不再就此 return)
backupAndReset(jsonFile, jsonFile.getName());
jsonFile = null;
}
if (!mdFile.exists()) { ensureConfigExists(); }
String md = readFile(mdFile);
if (md != null) {
@@ -160,8 +177,10 @@ public class BanBroadcastManager {
* @return true = json 配置生效;false = 视为未配置,调用方回退表格
*/
private boolean parseJsonText(String txt) {
jsonSyntaxOk = false;
try {
JsonObject o = JsonParser.parseString(txt).getAsJsonObject();
jsonSyntaxOk = true; // 语法解析成功(值是否为空另行判定)
boolean hasIntervalKey = o.has("最小间隔") || o.has("最大间隔")
|| o.has("轮播最低间隔") || o.has("轮播最大间隔");
boolean anyInterval = false;
@@ -219,6 +238,27 @@ public class BanBroadcastManager {
return any;
}
/** 只从 md 表格取间隔值(不碰播报模式/播报起点 —— 继承 json 时仅回退间隔用) */
private boolean parseIntervalTable(String md) {
boolean any = false;
for (String line : md.split("\n")) {
String t = line.trim();
if (!t.startsWith("|") || t.startsWith("| ---") || t.startsWith("|---")) continue;
String[] cells = t.split("\\|");
if (cells.length < 4) continue;
String key = cells[1].trim();
String val = cells[cells.length - 2].trim();
if (key.equals("轮播最低间隔")) { minInterval = toSec(val); any = true; }
else if (key.equals("轮播最大间隔")) { maxInterval = toSec(val); any = true; }
}
if (any) {
if (minInterval < 0) minInterval = 0;
if (maxInterval < 0) maxInterval = 0;
if (maxInterval > 0 && minInterval > maxInterval) maxInterval = minInterval;
}
return any;
}
/** 播报模式文本/布尔/数字 → 0/1/2 */
private int parseMode(String s) {
if (s == null) return 1;
@@ -457,6 +497,11 @@ public class BanBroadcastManager {
if (f != null && f.exists()) f.delete();
plugin.saveResource("ban/" + resourceName, false);
log("已释放一份新配置: " + resourceName);
} catch (IllegalArgumentException e) {
// jar 里没有同名模板(如用户自建 cfg.json):原文件已备份并移除,
// 不算失败 —— 调用方会继续回退 md / 出厂默认
warn("jar 内无模板资源 ban/" + resourceName
+ ",原文件已备份并移除,回退其它配置源");
} catch (Exception e) {
warn("备份/重置失败: " + e.getMessage());
}
+122 -1
View File
@@ -1942,6 +1942,114 @@ public class Main extends JavaPlugin
sender.sendMessage(sb.toString());
}
/**
* /banget 点名查询封禁:全量原生 banlist(不应用播报起点过滤,
* 插件安装之前的历史封禁也能查到),替代原版 /banlist 一屏全量打印。
* 用法:/banget <玩家名|IP>(自动识别)| /banget user <名> | /banget ip <IP>
*/
@SuppressWarnings("unchecked")
private void doBanGet(CommandSender sender, String[] args) {
if (!isPrivileged(sender)) {
sender.sendMessage("§c权限不足");
return;
}
String kind = null; // user / ip / null=自动识别
String target = null;
if (args.length == 1) {
String a = args[0].toLowerCase();
if (a.equals("user") || a.equals("ip")) {
sender.sendMessage("§c请指定查询目标: /banget " + a + " <玩家名或IP>");
return;
}
target = args[0];
} else if (args.length >= 2) {
String a = args[0].toLowerCase();
if (!a.equals("user") && !a.equals("ip")) {
sender.sendMessage("§c用法: /banget [user|ip] <玩家名或IP>");
return;
}
kind = a;
target = args[1];
} else {
sender.sendMessage("§c用法: /banget [user|ip] <玩家名或IP>");
sender.sendMessage("§7示例: §f/banget Notch §7| §f/banget user Notch"
+ " §7| §f/banget ip 127.0.0.1");
return;
}
boolean ipMode = kind != null ? kind.equals("ip") : isIpTarget(target);
sender.sendMessage("§8========== §e封禁查询: §f" + target
+ (ipMode ? " §7[IP]" : " §7[玩家名]") + " §8==========");
int hits = 0;
int expiredHits = 0;
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
try {
org.bukkit.BanList bl = Bukkit.getBanList(ipMode
? org.bukkit.BanList.Type.IP : org.bukkit.BanList.Type.NAME);
// ★ 全量查询:不应用播报起点,历史封禁照查;
// getEntries() 实际返回 Set,按 Collection 中转强转(防 ClassCastException)
for (Object oo : (java.util.Collection<?>) bl.getEntries()) {
org.bukkit.BanEntry e = (org.bukkit.BanEntry) oo;
if (!banEntryMatches(e, target)) continue;
hits++;
Date created = e.getCreated();
Date exp = e.getExpiration();
boolean expired = exp != null
&& exp.getTime() <= System.currentTimeMillis();
if (expired) expiredHits++;
String reason = e.getReason();
if (reason == null || reason.trim().isEmpty()) {
reason = "违反服务器规则";
}
String src;
try { src = e.getSource(); } catch (Throwable t) { src = null; }
if (src == null || src.trim().isEmpty()) src = "未知";
sender.sendMessage((expired ? "§8⌛ §f" : "§c☠ §f")
+ target + " §7- " + reason);
sender.sendMessage("§7 封于§f"
+ (created != null ? df.format(created) : "未知时间")
+ " §8| " + (expired ? "§a已过期"
: (exp != null ? "§7至§f" + df.format(exp) : "§c永久"))
+ " §8| §7来源§f" + src);
}
} catch (Exception ex) {
sender.sendMessage("§c查询封禁名单失败: " + ex.getMessage());
return;
}
if (hits == 0) {
sender.sendMessage("§7未查到相关封禁(全量查询,含插件安装前的历史数据)");
} else {
sender.sendMessage("§7共§f" + hits + "§7条记录"
+ (expiredHits > 0
? " §8(其中§f" + expiredHits + "§8条已过期)" : ""));
}
}
/**
* banget 目标匹配:getTarget() 字符串直比 + GameProfile[id=..,name=..] 兜底。
* (NAME 型 getBanEntry(String) 新 API 已不可靠,故遍历 getEntries 自行匹配)
*/
private boolean banEntryMatches(org.bukkit.BanEntry e, String target) {
try {
String t = e.getTarget();
if (t != null && t.equalsIgnoreCase(target)) return true;
} catch (Throwable ignored) { }
try {
Object bt = e.getBanTarget();
if (bt == null) return false;
String bs = String.valueOf(bt);
if (bs.equalsIgnoreCase(target)) return true;
int ni = bs.indexOf("name=");
if (ni >= 0) {
String nm = bs.substring(ni + 5);
int end = nm.indexOf(']');
if (end >= 0) nm = nm.substring(0, end);
if (nm.equalsIgnoreCase(target)) return true;
}
} catch (Throwable ignored) { }
return false;
}
private long asLong(Object o) {
if (o == null) return 0;
if (o instanceof Number) return ((Number) o).longValue();
@@ -4243,7 +4351,7 @@ public class Main extends JavaPlugin
private static final Set<String> ADMIN_ONLY = new HashSet<>(
java.util.Arrays.asList(
// 封禁 / 改判 / 注销
"tempban", "tempbanip", "banip", "whois", "clear",
"tempban", "tempbanip", "banip", "banget", "whois", "clear",
"cleartake", "清空队列",
// 账号管理
"删除", "删除账号", "通过", "拒绝", "accept", "deny",
@@ -8026,6 +8134,12 @@ public class Main extends JavaPlugin
return true;
}
// ===== /banget 点名查询封禁(全量 banlist,含插件安装前的历史封禁) =====
if (cmd.getName().equalsIgnoreCase("banget")) {
doBanGet(sender, args);
return true;
}
String sub = (args.length > 0) ? args[0].toLowerCase() : "";
// ===== 在 onCommand 方法体内,最开头加: =====
@@ -10316,6 +10430,13 @@ public class Main extends JavaPlugin
}
return list;
}
// ★ /banget 点名查询封禁:第一层 user/ip 提示
if (cmd.getName().equalsIgnoreCase("banget")) {
if (args.length == 1) {
return filterTab(Arrays.asList("user", "ip"), args[0]);
}
return list;
}
if (cmd.getName().equalsIgnoreCase(
"sdf1_login")
&& args.length == 1) {
+3
View File
@@ -184,6 +184,9 @@ commands:
who:
description: 查看在线玩家列表
usage: /who
banget:
description: 点名查询封禁(全量,含插件安装前的历史封禁)
usage: /banget [user|ip] <玩家名或IP>
清空队列:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /清空队列