修复三bug并归位根目录5类文件:①禁言解除后强制重新人机验证(解禁≠解除人机验证,到期/命令解禁/重置均打reverify标记,答对清除) ②OP白名单过期条目不再补授(修复26-10-08到期后每分钟撤销↔授予无限横跳) ③敏感词命中改为明确反馈(控制台日志+玩家提示+下一档处罚预告,修复启用过滤配置解析失效) ④bond.db/chat.txt/garbage.db/maxsell_config.txt/pvp.db启动最早期自动迁入对应子文件夹
This commit is contained in:
@@ -59,7 +59,9 @@ public class BondManager {
|
|||||||
try {
|
try {
|
||||||
Class.forName("org.sqlite.JDBC");
|
Class.forName("org.sqlite.JDBC");
|
||||||
File dbFile = new File(
|
File dbFile = new File(
|
||||||
plugin.getDataFolder(), "bond.db");
|
new File(plugin.getDataFolder(), "财务债券"),
|
||||||
|
"bond.db");
|
||||||
|
dbFile.getParentFile().mkdirs();
|
||||||
db = DriverManager.getConnection(
|
db = DriverManager.getConnection(
|
||||||
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||||
Statement st = db.createStatement();
|
Statement st = db.createStatement();
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ public class CDKManager {
|
|||||||
try {
|
try {
|
||||||
Class.forName("org.sqlite.JDBC");
|
Class.forName("org.sqlite.JDBC");
|
||||||
File dbFile = new File(
|
File dbFile = new File(
|
||||||
plugin.getDataFolder(), "bond.db");
|
new File(plugin.getDataFolder(), "财务债券"),
|
||||||
|
"bond.db");
|
||||||
|
dbFile.getParentFile().mkdirs();
|
||||||
db = DriverManager.getConnection(
|
db = DriverManager.getConnection(
|
||||||
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||||
Statement st = db.createStatement();
|
Statement st = db.createStatement();
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ public class ChatFilterManager {
|
|||||||
|
|
||||||
/** 已验证过的玩家名单(永久,跨session持久化到内存) */
|
/** 已验证过的玩家名单(永久,跨session持久化到内存) */
|
||||||
private final Set<String> verifiedPlayers = ConcurrentHashMap.newKeySet();
|
private final Set<String> verifiedPlayers = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
/** ★ 解禁后强制重新人机验证名单(2026-10-08:解禁≠解除人机验证) */
|
||||||
|
private final Set<String> reverifyRequired = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
/** 验证码数据存储(public供Main.java访问清理) */
|
/** 验证码数据存储(public供Main.java访问清理) */
|
||||||
public final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
|
public final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
|
||||||
@@ -149,6 +152,28 @@ public class ChatFilterManager {
|
|||||||
*/
|
*/
|
||||||
public VerificationResult checkNewPlayerVerification(Player p) {
|
public VerificationResult checkNewPlayerVerification(Player p) {
|
||||||
String name = p.getName();
|
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)) {
|
if (verifiedPlayers.contains(name)) {
|
||||||
@@ -502,6 +527,7 @@ public class ChatFilterManager {
|
|||||||
vd.completed = true;
|
vd.completed = true;
|
||||||
verificationData.remove(playerName);
|
verificationData.remove(playerName);
|
||||||
verifiedPlayers.add(playerName); // 永久记录 - 验证通过
|
verifiedPlayers.add(playerName); // 永久记录 - 验证通过
|
||||||
|
reverifyRequired.remove(playerName); // 解禁重验已通过
|
||||||
return VerificationResult.VERIFIED;
|
return VerificationResult.VERIFIED;
|
||||||
} else {
|
} else {
|
||||||
// ★ 回答错误 → 不清除验证码也不加入verifiedPlayers,让玩家重新答题
|
// ★ 回答错误 → 不清除验证码也不加入verifiedPlayers,让玩家重新答题
|
||||||
@@ -551,6 +577,7 @@ public class ChatFilterManager {
|
|||||||
vd.completed = true;
|
vd.completed = true;
|
||||||
verificationData.remove(name);
|
verificationData.remove(name);
|
||||||
verifiedPlayers.add(name);
|
verifiedPlayers.add(name);
|
||||||
|
reverifyRequired.remove(name); // 解禁重验已通过
|
||||||
return VerificationResult.VERIFIED;
|
return VerificationResult.VERIFIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,7 +733,8 @@ public class ChatFilterManager {
|
|||||||
sensitiveWords.clear(); // 重新加载前先清空
|
sensitiveWords.clear(); // 重新加载前先清空
|
||||||
|
|
||||||
File f = new File(
|
File f = new File(
|
||||||
plugin.getDataFolder(), "chat.txt");
|
new File(plugin.getDataFolder(), "发言规则管控"), "chat.txt");
|
||||||
|
f.getParentFile().mkdirs();
|
||||||
if (!f.exists()) writeDefaultFile(f);
|
if (!f.exists()) writeDefaultFile(f);
|
||||||
boolean hasSensitiveSection = false; // 记录文件是否已有敏感词列表节
|
boolean hasSensitiveSection = false; // 记录文件是否已有敏感词列表节
|
||||||
try (BufferedReader r =
|
try (BufferedReader r =
|
||||||
@@ -749,8 +777,8 @@ public class ChatFilterManager {
|
|||||||
String[] kv = t.split(":", 2);
|
String[] kv = t.split(":", 2);
|
||||||
String k = kv[0].trim();
|
String k = kv[0].trim();
|
||||||
String v = kv[1].trim();
|
String v = kv[1].trim();
|
||||||
if (equals("启用过滤")
|
if (k.equals("启用过滤")
|
||||||
|| equals("enabled")) {
|
|| k.equals("enabled")) {
|
||||||
enabled = parseBool(v);
|
enabled = parseBool(v);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -928,7 +956,8 @@ public class ChatFilterManager {
|
|||||||
|
|
||||||
public void saveConfig() {
|
public void saveConfig() {
|
||||||
File f = new File(
|
File f = new File(
|
||||||
plugin.getDataFolder(), "chat.txt");
|
new File(plugin.getDataFolder(), "发言规则管控"), "chat.txt");
|
||||||
|
f.getParentFile().mkdirs();
|
||||||
try (PrintWriter pw = new PrintWriter(
|
try (PrintWriter pw = new PrintWriter(
|
||||||
new OutputStreamWriter(
|
new OutputStreamWriter(
|
||||||
new FileOutputStream(f),
|
new FileOutputStream(f),
|
||||||
@@ -1094,6 +1123,8 @@ public class ChatFilterManager {
|
|||||||
muteIssuers.remove(name);
|
muteIssuers.remove(name);
|
||||||
muteStarts.remove(name);
|
muteStarts.remove(name);
|
||||||
clearMuteDb(name);
|
clearMuteDb(name);
|
||||||
|
// ★ 禁言到期自动解禁 → 立即要求重新人机验证(解禁≠解除人机验证)
|
||||||
|
markReverify(name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1276,6 +1307,19 @@ public class ChatFilterManager {
|
|||||||
muteIssuers.remove(name);
|
muteIssuers.remove(name);
|
||||||
muteStarts.remove(name);
|
muteStarts.remove(name);
|
||||||
clearMuteDb(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) {
|
public void resetPlayer(String name) {
|
||||||
mutedPlayers.remove(name);
|
boolean wasMuted = mutedPlayers.remove(name) != null;
|
||||||
violationCount.remove(name);
|
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(
|
public boolean isPlayerWhitelisted(
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ public class GarbageManager {
|
|||||||
try {
|
try {
|
||||||
Class.forName("org.sqlite.JDBC");
|
Class.forName("org.sqlite.JDBC");
|
||||||
File dbFile = new File(
|
File dbFile = new File(
|
||||||
plugin.getDataFolder(),
|
new File(plugin.getDataFolder(), "垃圾箱"),
|
||||||
"garbage.db");
|
"garbage.db");
|
||||||
|
dbFile.getParentFile().mkdirs();
|
||||||
db = DriverManager.getConnection(
|
db = DriverManager.getConnection(
|
||||||
"jdbc:sqlite:"
|
"jdbc:sqlite:"
|
||||||
+ dbFile.getAbsolutePath());
|
+ dbFile.getAbsolutePath());
|
||||||
|
|||||||
@@ -386,6 +386,8 @@ public class Main extends JavaPlugin
|
|||||||
saveResource("illegal_domains.txt", false);
|
saveResource("illegal_domains.txt", false);
|
||||||
getLogger().info("[配置] 已自动释放 illegal_domains.txt 到插件目录");
|
getLogger().info("[配置] 已自动释放 illegal_domains.txt 到插件目录");
|
||||||
}
|
}
|
||||||
|
// ★ 任务94:根目录5类文件归位(必须在任何 Manager 打开文件/DB 之前)
|
||||||
|
migrateRootDataFiles();
|
||||||
// 启动时清理.sdf1临时文件
|
// 启动时清理.sdf1临时文件
|
||||||
cleanupSdf1Files();
|
cleanupSdf1Files();
|
||||||
salesStats = new SalesStatsManager(this);
|
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 后缀文件
|
* 清理插件目录下的 .sdf1 后缀文件
|
||||||
* 用于防风控任务准备
|
* 用于防风控任务准备
|
||||||
@@ -5434,6 +5497,12 @@ public class Main extends JavaPlugin
|
|||||||
chatFilter.markPlayerActive(p.getName());
|
chatFilter.markPlayerActive(p.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 先探一次禁言状态:若刚好到期,isMuted() 内部会立即解禁并打上
|
||||||
|
// 「需重新人机验证」标记(2026-10-08:解禁≠解除人机验证)
|
||||||
|
if (chatFilter != null) {
|
||||||
|
chatFilter.isMuted(p.getName());
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 新玩家验证码检查(N+1机制) =====
|
// ===== 新玩家验证码检查(N+1机制) =====
|
||||||
if (chatFilter != null && chatFilter.isEnabled()) {
|
if (chatFilter != null && chatFilter.isEnabled()) {
|
||||||
ChatFilterManager.VerificationResult vr = chatFilter.checkNewPlayerVerification(p);
|
ChatFilterManager.VerificationResult vr = chatFilter.checkNewPlayerVerification(p);
|
||||||
@@ -5649,6 +5718,16 @@ public class Main extends JavaPlugin
|
|||||||
}
|
}
|
||||||
chatFilter.incrementViolation(p.getName());
|
chatFilter.incrementViolation(p.getName());
|
||||||
int sc = chatFilter.getViolationCount(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 = "说出敏感词("
|
String swReason = "说出敏感词("
|
||||||
+ ms.get(0) + ")";
|
+ ms.get(0) + ")";
|
||||||
String ptype = chatFilter.applyPunishment(
|
String ptype = chatFilter.applyPunishment(
|
||||||
|
|||||||
@@ -755,6 +755,12 @@ public class OpWhiteListManager implements Listener {
|
|||||||
// 服主 /op list 也看不到他们)。现改为:在线走 Player.setOp、
|
// 服主 /op list 也看不到他们)。现改为:在线走 Player.setOp、
|
||||||
// 离线走 OfflinePlayer.setOp(无档案的自动跳过,等其上线时再补)。
|
// 离线走 OfflinePlayer.setOp(无档案的自动跳过,等其上线时再补)。
|
||||||
for (String wn : whitelistedNames()) {
|
for (String wn : whitelistedNames()) {
|
||||||
|
// ★ 2026-10-08 修复:过期/时长无法解析的临时条目不再补授。
|
||||||
|
// 否则每轮巡检「第1步撤销过期者 → 第2步又按配置条目原样授予」
|
||||||
|
// 形成无限横跳(截图现象:26-10-08 16:00 到期后每分钟撤销+授予各一次)。
|
||||||
|
if (!isWhitelisted(wn)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Player online = Bukkit.getPlayerExact(wn);
|
Player online = Bukkit.getPlayerExact(wn);
|
||||||
if (online != null) {
|
if (online != null) {
|
||||||
if (online.isOp()) {
|
if (online.isOp()) {
|
||||||
|
|||||||
@@ -108,8 +108,9 @@ public class PVPManager implements Listener {
|
|||||||
Class.forName(
|
Class.forName(
|
||||||
"org.sqlite.JDBC");
|
"org.sqlite.JDBC");
|
||||||
File dbFile = new File(
|
File dbFile = new File(
|
||||||
plugin.getDataFolder(),
|
new File(plugin.getDataFolder(), "pvp"),
|
||||||
"pvp.db");
|
"pvp.db");
|
||||||
|
dbFile.getParentFile().mkdirs();
|
||||||
pvpDb = DriverManager.getConnection(
|
pvpDb = DriverManager.getConnection(
|
||||||
"jdbc:sqlite:"
|
"jdbc:sqlite:"
|
||||||
+ dbFile.getAbsolutePath());
|
+ dbFile.getAbsolutePath());
|
||||||
|
|||||||
@@ -1635,7 +1635,7 @@ public class ShopManager implements Listener {
|
|||||||
// ===== maxSell 配置持久化 =====
|
// ===== maxSell 配置持久化 =====
|
||||||
/** 加载 maxSell 配置从插件数据文件夹下的 maxsell_config.txt */
|
/** 加载 maxSell 配置从插件数据文件夹下的 maxsell_config.txt */
|
||||||
private void loadMaxSellConfig() {
|
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 {
|
try {
|
||||||
if (cfgFile.exists()) {
|
if (cfgFile.exists()) {
|
||||||
BufferedReader br = new BufferedReader(new FileReader(cfgFile));
|
BufferedReader br = new BufferedReader(new FileReader(cfgFile));
|
||||||
@@ -1673,7 +1673,7 @@ public class ShopManager implements Listener {
|
|||||||
try {
|
try {
|
||||||
File dir = new File(plugin.getDataFolder(), "shop");
|
File dir = new File(plugin.getDataFolder(), "shop");
|
||||||
if (!dir.exists()) dir.mkdirs();
|
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));
|
BufferedWriter bw = new BufferedWriter(new FileWriter(cfgFile, StandardCharsets.UTF_8));
|
||||||
bw.write("maxSellLimit=" + maxSellLimit);
|
bw.write("maxSellLimit=" + maxSellLimit);
|
||||||
bw.newLine();
|
bw.newLine();
|
||||||
|
|||||||
Reference in New Issue
Block a user