修复三bug并归位根目录5类文件:①禁言解除后强制重新人机验证(解禁≠解除人机验证,到期/命令解禁/重置均打reverify标记,答对清除) ②OP白名单过期条目不再补授(修复26-10-08到期后每分钟撤销↔授予无限横跳) ③敏感词命中改为明确反馈(控制台日志+玩家提示+下一档处罚预告,修复启用过滤配置解析失效) ④bond.db/chat.txt/garbage.db/maxsell_config.txt/pvp.db启动最早期自动迁入对应子文件夹
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m31s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-08 17:02:02 +08:00
parent 58f9a756e9
commit 19e03c5fb4
8 changed files with 170 additions and 11 deletions
+3 -1
View File
@@ -59,7 +59,9 @@ public class BondManager {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(), "bond.db");
new File(plugin.getDataFolder(), "财务债券"),
"bond.db");
dbFile.getParentFile().mkdirs();
db = DriverManager.getConnection(
"jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
+3 -1
View File
@@ -25,7 +25,9 @@ public class CDKManager {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(), "bond.db");
new File(plugin.getDataFolder(), "财务债券"),
"bond.db");
dbFile.getParentFile().mkdirs();
db = DriverManager.getConnection(
"jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
@@ -97,6 +97,9 @@ public class ChatFilterManager {
/** 已验证过的玩家名单(永久,跨session持久化到内存) */
private final Set<String> verifiedPlayers = ConcurrentHashMap.newKeySet();
/** ★ 解禁后强制重新人机验证名单(2026-10-08:解禁≠解除人机验证) */
private final Set<String> reverifyRequired = ConcurrentHashMap.newKeySet();
/** 验证码数据存储(public供Main.java访问清理) */
public final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
@@ -149,6 +152,28 @@ public class ChatFilterManager {
*/
public VerificationResult checkNewPlayerVerification(Player p) {
String name = p.getName();
// ★ 解禁 ≠ 解除人机验证(2026-10-08):解禁后必须重新过人机验证才能发言。
// 该分支优先级最高——连 verifiedPlayers 与正版OAuth豁免也压过。
if (reverifyRequired.contains(name)) {
VerificationData vd0 = verificationData.get(name);
if (vd0 != null && vd0.completed) {
verifiedPlayers.add(name);
verificationData.remove(name);
reverifyRequired.remove(name);
return VerificationResult.VERIFIED;
}
if (vd0 != null && vd0.isExpired()) {
verificationData.remove(name);
p.sendMessage("§c§l[验证码] §c上一题超时,已为你换新题");
generateMathVerification(name);
return VerificationResult.TIMEOUT;
}
if (vd0 != null) {
return VerificationResult.PENDING;
}
return VerificationResult.NEED_VERIFICATION;
}
// ★ 永久已验证的玩家 → 直接放行
if (verifiedPlayers.contains(name)) {
@@ -502,6 +527,7 @@ public class ChatFilterManager {
vd.completed = true;
verificationData.remove(playerName);
verifiedPlayers.add(playerName); // 永久记录 - 验证通过
reverifyRequired.remove(playerName); // 解禁重验已通过
return VerificationResult.VERIFIED;
} else {
// ★ 回答错误 → 不清除验证码也不加入verifiedPlayers,让玩家重新答题
@@ -551,6 +577,7 @@ public class ChatFilterManager {
vd.completed = true;
verificationData.remove(name);
verifiedPlayers.add(name);
reverifyRequired.remove(name); // 解禁重验已通过
return VerificationResult.VERIFIED;
}
@@ -706,7 +733,8 @@ public class ChatFilterManager {
sensitiveWords.clear(); // 重新加载前先清空
File f = new File(
plugin.getDataFolder(), "chat.txt");
new File(plugin.getDataFolder(), "发言规则管控"), "chat.txt");
f.getParentFile().mkdirs();
if (!f.exists()) writeDefaultFile(f);
boolean hasSensitiveSection = false; // 记录文件是否已有敏感词列表节
try (BufferedReader r =
@@ -749,8 +777,8 @@ public class ChatFilterManager {
String[] kv = t.split(":", 2);
String k = kv[0].trim();
String v = kv[1].trim();
if (equals("启用过滤")
|| equals("enabled")) {
if (k.equals("启用过滤")
|| k.equals("enabled")) {
enabled = parseBool(v);
continue;
}
@@ -928,7 +956,8 @@ public class ChatFilterManager {
public void saveConfig() {
File f = new File(
plugin.getDataFolder(), "chat.txt");
new File(plugin.getDataFolder(), "发言规则管控"), "chat.txt");
f.getParentFile().mkdirs();
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
@@ -1094,6 +1123,8 @@ public class ChatFilterManager {
muteIssuers.remove(name);
muteStarts.remove(name);
clearMuteDb(name);
// ★ 禁言到期自动解禁 → 立即要求重新人机验证(解禁≠解除人机验证)
markReverify(name);
return false;
}
return true;
@@ -1276,6 +1307,19 @@ public class ChatFilterManager {
muteIssuers.remove(name);
muteStarts.remove(name);
clearMuteDb(name);
// ★ 解禁≠解除人机验证(2026-10-08)
markReverify(name);
}
/**
* ★ 解禁(命令解禁/禁言到期/重置)→ 吊销人机验证:
* 移出已验证名单、清掉挂起的题与缓存消息,下次发言必须重新过人机验证。
*/
public void markReverify(String name) {
reverifyRequired.add(name);
verifiedPlayers.remove(name);
verificationData.remove(name);
pendingMessages.remove(name);
}
// ========== 禁言持久化(重启后仍生效) ==========
@@ -1365,8 +1409,32 @@ public class ChatFilterManager {
}
public void resetPlayer(String name) {
mutedPlayers.remove(name);
boolean wasMuted = mutedPlayers.remove(name) != null;
violationCount.remove(name);
// ★ 清禁言=解禁 → 必须重新人机验证;从未被禁言的只清计数,不打扰
if (wasMuted) {
markReverify(name);
}
}
/** 下一档违规将触发的处罚描述(命中时给玩家预告),无更高档返回 null */
public String nextPunishHint(int violation) {
int nextKey = Integer.MAX_VALUE;
for (Integer k : punishmentRules.keySet()) {
if (k > violation && k < nextKey) {
nextKey = k;
}
}
if (nextKey == Integer.MAX_VALUE) {
return null;
}
String type = punishmentRules.get(nextKey);
int dur = punishmentDurations.getOrDefault(nextKey, muteDuration);
String cn = switchPunishType(type);
if ("mute".equals(type)) {
cn += " " + fmtDuration(dur);
}
return "第 " + nextKey + " 次违规: " + cn;
}
public boolean isPlayerWhitelisted(
+2 -1
View File
@@ -37,8 +37,9 @@ public class GarbageManager {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(),
new File(plugin.getDataFolder(), "垃圾箱"),
"garbage.db");
dbFile.getParentFile().mkdirs();
db = DriverManager.getConnection(
"jdbc:sqlite:"
+ dbFile.getAbsolutePath());
+79
View File
@@ -386,6 +386,8 @@ public class Main extends JavaPlugin
saveResource("illegal_domains.txt", false);
getLogger().info("[配置] 已自动释放 illegal_domains.txt 到插件目录");
}
// ★ 任务94:根目录5类文件归位(必须在任何 Manager 打开文件/DB 之前)
migrateRootDataFiles();
// 启动时清理.sdf1临时文件
cleanupSdf1Files();
salesStats = new SalesStatsManager(this);
@@ -986,6 +988,67 @@ public class Main extends JavaPlugin
}
}
// ===== 任务94:根目录5类文件归位(2026-10-08) =====
/**
* 启动最早期把根目录杂项移入对应子文件夹(幂等)。
* 目标已存在则跳过不覆盖;迁移失败只警告不阻断启动。
* 必须在任何 Manager 打开文件/数据库之前调用。
*/
private void migrateRootDataFiles() {
File root = getDataFolder();
String[][] moves = {
{"bond.db", "财务债券"},
{"bond.db-shm", "财务债券"},
{"bond.db-wal", "财务债券"},
{"chat.txt", "发言规则管控"},
{"garbage.db", "垃圾箱"},
{"garbage.db-shm", "垃圾箱"},
{"garbage.db-wal", "垃圾箱"},
{"pvp.db", "pvp"},
{"pvp.db-shm", "pvp"},
{"pvp.db-wal", "pvp"},
{"maxsell_config.txt", "shop"},
};
for (String[] m : moves) {
File src = new File(root, m[0]);
if (!src.exists()) {
continue;
}
File dir = new File(root, m[1]);
dir.mkdirs();
File dst = new File(dir, m[0]);
if (dst.exists()) {
getLogger().warning("[归位] " + m[0]
+ " 目标已存在,跳过(根目录保留副本): "
+ m[1] + "/" + m[0]);
continue;
}
if (src.renameTo(dst)) {
getLogger().info("[归位] " + m[0] + " → " + m[1] + "/");
} else {
getLogger().warning("[归位] " + m[0] + " → " + m[1]
+ "/ 迁移失败(保留原位,请手动移动)");
}
}
// 用户点名的 maxshell_*.txt 也归入 shop/
File[] extras = root.listFiles((d, n) ->
n.toLowerCase().startsWith("maxshell_")
&& n.toLowerCase().endsWith(".txt"));
if (extras != null) {
File dir = new File(root, "shop");
for (File src : extras) {
File dst = new File(dir, src.getName());
if (dst.exists()) {
continue;
}
dir.mkdirs();
if (src.renameTo(dst)) {
getLogger().info("[归位] " + src.getName() + " → shop/");
}
}
}
}
/**
* 清理插件目录下的 .sdf1 后缀文件
* 用于防风控任务准备
@@ -5434,6 +5497,12 @@ public class Main extends JavaPlugin
chatFilter.markPlayerActive(p.getName());
}
// ★ 先探一次禁言状态:若刚好到期,isMuted() 内部会立即解禁并打上
// 「需重新人机验证」标记(2026-10-08:解禁≠解除人机验证)
if (chatFilter != null) {
chatFilter.isMuted(p.getName());
}
// ===== 新玩家验证码检查(N+1机制) =====
if (chatFilter != null && chatFilter.isEnabled()) {
ChatFilterManager.VerificationResult vr = chatFilter.checkNewPlayerVerification(p);
@@ -5649,6 +5718,16 @@ public class Main extends JavaPlugin
}
chatFilter.incrementViolation(p.getName());
int sc = chatFilter.getViolationCount(p.getName());
// ★ 2026-10-08:命中必须给发送者明确反馈——此前 warn 档完全静默、
// 自回显又与正常聊天一模一样 →观感「中文敏感词没过滤」
getLogger().info("[Sdf1_chat] " + p.getName() + " 命中敏感词("
+ ms.get(0) + "),累计违规 " + sc + " 次,消息已拦截");
p.sendMessage("§c§l[聊天过滤] §7消息包含违规词 §f" + ms.get(0)
+ " §7已被拦截 §8(第 " + sc + " 次违规)");
String punishHint = chatFilter.nextPunishHint(sc);
if (punishHint != null) {
p.sendMessage("§8下次违规将触发: §e" + punishHint);
}
String swReason = "说出敏感词("
+ ms.get(0) + ")";
String ptype = chatFilter.applyPunishment(
@@ -755,6 +755,12 @@ public class OpWhiteListManager implements Listener {
// 服主 /op list 也看不到他们)。现改为:在线走 Player.setOp、
// 离线走 OfflinePlayer.setOp(无档案的自动跳过,等其上线时再补)。
for (String wn : whitelistedNames()) {
// ★ 2026-10-08 修复:过期/时长无法解析的临时条目不再补授。
// 否则每轮巡检「第1步撤销过期者 → 第2步又按配置条目原样授予」
// 形成无限横跳(截图现象:26-10-08 16:00 到期后每分钟撤销+授予各一次)。
if (!isWhitelisted(wn)) {
continue;
}
Player online = Bukkit.getPlayerExact(wn);
if (online != null) {
if (online.isOp()) {
+2 -1
View File
@@ -108,8 +108,9 @@ public class PVPManager implements Listener {
Class.forName(
"org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(),
new File(plugin.getDataFolder(), "pvp"),
"pvp.db");
dbFile.getParentFile().mkdirs();
pvpDb = DriverManager.getConnection(
"jdbc:sqlite:"
+ dbFile.getAbsolutePath());
+2 -2
View File
@@ -1635,7 +1635,7 @@ public class ShopManager implements Listener {
// ===== maxSell 配置持久化 =====
/** 加载 maxSell 配置从插件数据文件夹下的 maxsell_config.txt */
private void loadMaxSellConfig() {
File cfgFile = new File(plugin.getDataFolder(), "maxsell_config.txt");
File cfgFile = new File(new File(plugin.getDataFolder(), "shop"), "maxsell_config.txt");
try {
if (cfgFile.exists()) {
BufferedReader br = new BufferedReader(new FileReader(cfgFile));
@@ -1673,7 +1673,7 @@ public class ShopManager implements Listener {
try {
File dir = new File(plugin.getDataFolder(), "shop");
if (!dir.exists()) dir.mkdirs();
File cfgFile = new File(dir.getParentFile(), "maxsell_config.txt");
File cfgFile = new File(dir, "maxsell_config.txt");
BufferedWriter bw = new BufferedWriter(new FileWriter(cfgFile, StandardCharsets.UTF_8));
bw.write("maxSellLimit=" + maxSellLimit);
bw.newLine();