第32批:垃圾站血条与启发式开关解耦;拉黑信号回响PHP(CDK双链路拦截+syncBlacklist名单同步)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m17s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-21 15:09:06 +08:00
parent 311138b3a6
commit 8311c412a1
5 changed files with 124 additions and 29 deletions
@@ -318,16 +318,26 @@ public class BlacklistManager {
}
// ===== 差异应用 =====
boolean changed = parsed.size() != entries.size();
// 1) 新增 → 施加处罚
for (BlackEntry e : parsed.values()) {
if (!entries.containsKey(e.name)) {
applyBlacklist(e);
changed = true;
} else {
BlackEntry old = entries.get(e.name);
if (old == null || old.expiresMs != e.expiresMs
|| !old.timeText.equals(e.timeText)
|| !old.reason.equals(e.reason)) {
changed = true; // 条目内容改了(理由/时长)
}
}
}
// 2) 移除 → 恢复
for (BlackEntry e : new ArrayList<>(entries.values())) {
if (!parsed.containsKey(e.name)) {
restoreBlacklist(e.name);
changed = true;
}
}
entries.clear();
@@ -338,6 +348,13 @@ public class BlacklistManager {
lastSize = mdFile.length();
}
plugin.getLogger().info("[黑名单] 已重载,当前生效 " + parsed.size() + " 人");
// ★ 第32批(#208):名单变化 → 立即回响PHP(异步;syncBlacklist 内部 hash 门控兜底)
if (changed) {
try {
WebManager wm = plugin.webManager;
if (wm != null) wm.forceSyncBlacklist();
} catch (Exception ignored) {}
}
}
/**
+6 -5
View File
@@ -57,6 +57,12 @@ public class CDKManager {
*/
public String redeem(String code, String player) {
try {
// ★ 第32批:拉黑检查提到查码之前 —— 被拉黑玩家一律拒绝,
// 不管码在本地是否存在,绝不放行进远程验证链
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(player))
return "fail:blacklisted";
PreparedStatement ps = db.prepareStatement(
"SELECT amount,type,used FROM cdk "
+ "WHERE code=?");
@@ -74,11 +80,6 @@ public class CDKManager {
if (used == 1)
return "fail:already_used";
// ★ 第30批:拉黑玩家禁止使用兑换码
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(player))
return "fail:blacklisted";
if (plugin.getBonds()
.isFrozen(player))
return "fail:frozen";
+3 -2
View File
@@ -58,7 +58,8 @@ public class ConfigManager {
public int garbageMaxRounds = 1;
/** ★ 第30批:垃圾站启发式拦截(双击Q抢救窗口 + 倒计时段位提示)。默认开启。 */
/** ★ 第30批:垃圾站启发式拦截 —— 仅控制「红区title警告 + 双击Q抢救窗口」。
* ★ 第32批:与顶部血条解耦,关闭时血条照常显示。默认开启。 */
public boolean garbageHeuristic = true;
@@ -454,7 +455,7 @@ public class ConfigManager {
garbageHeuristic = Boolean.parseBoolean(m.getOrDefault("垃圾站_启发式拦截", "true"));
if (!garbageHeuristic) {
Bukkit.getLogger().warning("[Sdf1配置] 垃圾站启发式拦截已关闭:不再做倒计时段位提示与双击Q抢救拦截。");
Bukkit.getLogger().warning("[Sdf1配置] 垃圾站启发式拦截已关闭:不再做红区title警告与双击Q抢救拦截(顶部血条照常显示)。");
}
+24 -20
View File
@@ -36,19 +36,27 @@ public class GarbageManager implements Listener {
private final Map<String, Integer>
recyclePages = new HashMap<>();
/** ★ 第30批:垃圾站倒计时(秒)。原为 startAutoCleanup 内部局部变量,
* 抢救模式需要外部重置,故提为字段。 */
/**
* ★ 第30批:垃圾站倒计时(秒)。原为 startAutoCleanup 内部局部变量,
* 抢救模式需要外部重置,故提为字段。
*/
private int countdown = 0;
/** ★ 第31批:本轮开始的墙钟时间戳(毫秒)。
* 血条色段按真实时间判定,与 tick 计数解耦,
* 避免“第3分30秒还是绿色”的漂移。0 = 本轮未开始。 */
/**
* ★ 第31批:本轮开始的墙钟时间戳(毫秒)。
* 血条色段按真实时间判定,与 tick 计数解耦,
* 避免“第3分30秒还是绿色”的漂移。0 = 本轮未开始。
*/
private long roundStartMs = 0L;
/** ★ 第30批:顶部血条(玩家 UUID → BossBar) */
/**
* ★ 第30批:顶部血条(玩家 UUID → BossBar)
*/
private final Map<UUID, BossBar> bossBars = new HashMap<>();
/** ★ 第30批:双击 Q 确认丢弃的首次按键时间(玩家 UUID → 毫秒) */
/**
* ★ 第30批:双击 Q 确认丢弃的首次按键时间(玩家 UUID → 毫秒)
*/
private final Map<UUID, Long> pendingDrops = new HashMap<>();
@@ -174,9 +182,6 @@ public class GarbageManager implements Listener {
cleanupInterval =
plugin.getConfig2()
.garbageInterval;
boolean heuristic =
plugin.getConfig2()
.garbageHeuristic;
int remain =
cleanupInterval - countdown;
@@ -211,7 +216,8 @@ public class GarbageManager implements Listener {
countdown++;
// ★ 第30批:顶部血条常驻倒计时(软提醒)
// 用 increment 前的 remain,与广播口径完全一致(避免差 1 秒)
updateBossBars(remain, heuristic);
// ★ 第32批:血条只受 垃圾站_启用 控制,与启发式开关解耦
updateBossBars(remain);
}
}.runTaskTimer(plugin, 20L, 20L);
}
@@ -227,13 +233,11 @@ public class GarbageManager implements Listener {
* <li>4:30 ~ 5:00 → 红 = 危险(另:剩余≤30秒兜底红)</li>
* </ul>
*
* <p>启发式开关(垃圾站_启发式拦截)关闭时:撤掉血条、不提示、不拦截。
* <p>★ 第32批:血条与启发式开关解耦 —— 只要垃圾站启用,血条常驻显示;
* 启发式开关(垃圾站_启发式拦截)关闭时只关掉「红区title警告 + 双击Q拦截」,
* 不再撤血条(否则玩家对清理状态一无所知)。
*/
private void updateBossBars(int remain, boolean heuristic) {
if (!heuristic) {
hideAllBossBars();
return;
}
private void updateBossBars(int remain) {
int interval = Math.max(1, cleanupInterval);
double progress = (double) remain / (double) interval;
if (progress < 0.0D) progress = 0.0D;
@@ -283,7 +287,7 @@ public class GarbageManager implements Listener {
}
}
/** 撤掉所有玩家的顶部血条(启发式关闭时用) */
/** 撤掉所有玩家的顶部血条(插件关闭/停服时用;启发式开关不再触发) */
private void hideAllBossBars() {
for (Player p : Bukkit.getOnlinePlayers()) {
BossBar bar = bossBars.get(p.getUniqueId());
@@ -390,8 +394,8 @@ public class GarbageManager implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e) {
if (!plugin.getConfig2().garbageHeuristic) return;
// 下一轮 tick 会自动建血条,这里只清掉可能残留的按键状态
// 下一轮 tick 会自动建血条(第32批:不再受启发式开关守卫),
// 这里只清掉可能残留的按键状态
pendingDrops.remove(e.getPlayer().getUniqueId());
}
+74 -2
View File
@@ -331,6 +331,7 @@ public class WebManager {
private long lastPushedCutoff = -1; // 上次推送时的播报起点(变了就再强制推一轮)
private String lastAdminsHash = "";
private String lastUserRegistrationHash = "";
private String lastBlacklistHash = ""; // ★ 第32批:拉黑名单同步快照
// 嵌入式HTTP服务器(接收PHP回调)
private java.net.ServerSocket callbackServer;
@@ -3338,7 +3339,14 @@ public class WebManager {
// ★ 直接检查sdf1计分板CDK(bond.db只是钱包,不存CDK)
String status = "not_found";
int amount = 0;
try {
// ★ 第32批:被拉黑玩家的兑换请求 → 直接回 blacklisted,
// 不进验证链、不加债券(PHP cdk.php 据此明确提示「你已被拉黑」)
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(playerName)) {
status = "blacklisted";
plugin.getLogger().info("[CDK远程验证] " + cdkCode
+ " 拉黑拦截 player=" + playerName);
} else try {
Object sdf1Plugin = Bukkit.getPluginManager().getPlugin("sdf1");
if (sdf1Plugin != null) {
plugin.getLogger().info("[CDK远程验证] 调用sdf1检查计分板: " + cdkCode);
@@ -3443,7 +3451,13 @@ public class WebManager {
String status = "not_found";
int amount = 0;
boolean transientFail = false; // 瞬时故障 -> 可重试
try {
// ★ 第32批:被拉黑玩家 → 直接回 blacklisted,不发起远程验证
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(playerName)) {
status = "blacklisted";
plugin.getLogger().info("[CDK-Web验证] " + cdkCode
+ " 拉黑拦截 player=" + playerName);
} else try {
String validateUrl = webBaseUrl + "/api/sync.php?action=check_cdk_exists&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&code=" + java.net.URLEncoder.encode(cdkCode, "UTF-8");
@@ -5292,6 +5306,8 @@ public class WebManager {
// ★ 封禁名单和管理员列表独立于领地hash变化,必须每次都同步
syncBans();
syncAdmins();
// ★ 第32批:拉黑名单同样每轮兜底(PHP重启丢数据时自愈)
syncBlacklist();
// ★ 双向对账(多退少补):PHP 数据齐全就不推 —— 消除「重启必整包推」
// (lastLandDataHash 是内存字段,重启清零,旧逻辑每次开服必整包一次)。
@@ -5561,6 +5577,62 @@ public class WebManager {
syncAdmins();
}
// ★ 第32批(#208):拉黑名单同步到PHP(web_player_blacklist),
// 供 PHP 后台识别被拉黑玩家并给出明确提示(登录/兑换码双链路)。
/**
* 同步拉黑名单到PHP。参照 syncBans 模板:
* hash 门控(无变化静默)→ POST → 回执 success 才提交 hash(防 404 写脏)。
* PHP 侧 sync_blacklist 做全量覆盖,无需对账。
*/
private void syncBlacklist() {
if (!enabled) return;
try {
BlacklistManager blm = plugin.getBlacklist();
java.util.List<BlacklistManager.BlackEntry> list =
blm != null ? blm.getActiveEntries() : null;
StringBuilder sb = new StringBuilder("[");
StringBuilder hb = new StringBuilder();
boolean first = true;
if (list != null) {
for (BlacklistManager.BlackEntry e : list) {
if (!first) sb.append(",");
first = false;
sb.append("{\"name\":\"").append(escapeJson(e.name))
.append("\",\"time\":\"").append(escapeJson(e.timeText))
.append("\",\"reason\":\"").append(escapeJson(e.reason))
.append("\",\"expire\":").append(e.expiresMs)
.append(",\"created\":").append(e.createdMs).append("}");
hb.append(e.name).append(":").append(e.expiresMs)
.append(":").append(e.reason).append("|");
}
}
sb.append("]");
String currentHash = hb.toString();
if (currentHash.equals(lastBlacklistHash)) return; // 无变化静默
Map<String, Object> body = new LinkedHashMap<>();
body.put("secret", secretKey);
body.put("blacks", sb.toString());
String resp = httpPost("api/sync.php?action=sync_blacklist", mapToJson(body));
if (resp != null && resp.contains("\"success\":true")) {
lastBlacklistHash = currentHash;
plugin.getLogger().info("[防护-sync] 拉黑名单同步: "
+ (list == null ? 0 : list.size()) + "人 → " + resp);
} else {
plugin.getLogger().warning("[防护-sync] 拉黑名单同步未确认成功,下轮重试(hash 不提交)");
}
} catch (Exception e) {
plugin.getLogger().warning("[防护-sync] 拉黑名单同步异常: " + e.getMessage());
}
}
/** ★ 第32批:拉黑/解除时立即异步同步(不等定时轮询;内部 hash 门控兜底) */
public void forceSyncBlacklist() {
if (!enabled) return;
Bukkit.getScheduler().runTaskAsynchronously(plugin, this::syncBlacklist);
}
/**
* ★ 同步封禁名单到PHP(供用户管理页面显示封禁状态)
*/