feat: PVP战绩持久化+清空队列命令+队列积压丢弃+PVP查询3天限制
Auto Release / build (push) Successful in 31s

1. PVPManager新增showArenaTotalStats/showArenaSessionStats方法
2. DatabaseManager.getArenaSessionByNumber添加3天有效期检查
3. WebManager新增clearQueue()/discardOldestIfOverflow()方法
4. Main.java新增清空队列/cleartake命令处理
5. plugin.yml注册清空队列/cleartake命令
This commit is contained in:
2026-07-22 15:45:25 +08:00
parent b488dfa04f
commit cdce0c0e19
6 changed files with 362 additions and 16 deletions
@@ -228,6 +228,25 @@ public class DatabaseManager {
+ "PRIMARY KEY(player_name, region_name)"
+ ")");
// PVP竞技场会话表(每局PVP世界)
st.execute("CREATE TABLE IF NOT EXISTS "
+ "pvp_arena_sessions ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "session_number INTEGER NOT NULL UNIQUE,"
+ "world_name TEXT NOT NULL,"
+ "created_at INTEGER NOT NULL"
+ ")");
// PVP竞技场战绩表(按会话+玩家)
st.execute("CREATE TABLE IF NOT EXISTS "
+ "pvp_arena_stats ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "session_id INTEGER NOT NULL,"
+ "player_uuid TEXT NOT NULL,"
+ "player_name TEXT NOT NULL,"
+ "kills INTEGER DEFAULT 0,"
+ "deaths INTEGER DEFAULT 0,"
+ "UNIQUE(session_id, player_uuid)"
+ ")");
// 菜单图标表
st.execute("CREATE TABLE IF NOT EXISTS "
@@ -1480,6 +1499,177 @@ public class DatabaseManager {
return list;
}
// ==================== PVP竞技场会话 & 战绩持久化 ====================
/** 创建新PVP竞技场会话,返回生成的id */
public int createArenaSession(String worldName) {
try {
// 先查最大session_number
int maxNum = 0;
Statement st = db.createStatement();
ResultSet rs = st.executeQuery("SELECT COALESCE(MAX(session_number),0) FROM pvp_arena_sessions");
if (rs.next()) maxNum = rs.getInt(1);
rs.close();
st.close();
int newNum = maxNum + 1;
PreparedStatement ps = db.prepareStatement(
"INSERT INTO pvp_arena_sessions (session_number, world_name, created_at) VALUES (?,?,?)",
PreparedStatement.RETURN_GENERATED_KEYS);
ps.setInt(1, newNum);
ps.setString(2, worldName);
ps.setLong(3, System.currentTimeMillis());
ps.executeUpdate();
ResultSet keys = ps.getGeneratedKeys();
int id = keys.next() ? keys.getInt(1) : 1;
keys.close();
ps.close();
return id;
} catch (SQLException e) {
e.printStackTrace();
return -1;
}
}
/** 获取指定会话编号的信息(超过3天返回null) */
public Map<String, Object> getArenaSessionByNumber(int number) {
long threeDaysMs = 3L * 24 * 60 * 60 * 1000;
try {
PreparedStatement ps = db.prepareStatement(
"SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions WHERE session_number=? AND ? - created_at <= ?");
long now = System.currentTimeMillis();
ps.setInt(1, number);
ps.setLong(2, now);
ps.setLong(3, threeDaysMs);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("session_number", rs.getInt("session_number"));
row.put("world_name", rs.getString("world_name"));
row.put("created_at", rs.getLong("created_at"));
rs.close();
ps.close();
return row;
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
/** 获取所有竞技场会话列表(按编号降序) */
public List<Map<String, Object>> getAllArenaSessions() {
List<Map<String, Object>> list = new ArrayList<>();
try {
Statement st = db.createStatement();
ResultSet rs = st.executeQuery(
"SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions ORDER BY session_number DESC");
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("session_number", rs.getInt("session_number"));
row.put("world_name", rs.getString("world_name"));
row.put("created_at", rs.getLong("created_at"));
list.add(row);
}
rs.close();
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
/** 记录竞技场击杀(INSERT OR IGNORE + UPDATE) */
public void recordArenaKill(int sessionId, String uuid, String name) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)");
ps.setInt(1, sessionId);
ps.setString(2, uuid);
ps.setString(3, name);
ps.executeUpdate();
ps.close();
ps = db.prepareStatement(
"UPDATE pvp_arena_stats SET kills = kills + 1, player_name=? WHERE session_id=? AND player_uuid=?");
ps.setString(1, name);
ps.setInt(2, sessionId);
ps.setString(3, uuid);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/** 记录竞技场死亡 */
public void recordArenaDeath(int sessionId, String uuid, String name) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)");
ps.setInt(1, sessionId);
ps.setString(2, uuid);
ps.setString(3, name);
ps.executeUpdate();
ps.close();
ps = db.prepareStatement(
"UPDATE pvp_arena_stats SET deaths = deaths + 1, player_name=? WHERE session_id=? AND player_uuid=?");
ps.setString(1, name);
ps.setInt(2, sessionId);
ps.setString(3, uuid);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/** 获取某玩家所有竞技场会话的总战绩 */
public Map<String, Object> getArenaTotalStats(String uuid) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("kills", 0);
result.put("deaths", 0);
try {
PreparedStatement ps = db.prepareStatement(
"SELECT COALESCE(SUM(kills),0) AS total_kills, COALESCE(SUM(deaths),0) AS total_deaths FROM pvp_arena_stats WHERE player_uuid=?");
ps.setString(1, uuid);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("kills", rs.getInt("total_kills"));
result.put("deaths", rs.getInt("total_deaths"));
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
/** 获取某玩家在指定会话编号中的战绩 */
public Map<String, Object> getArenaSessionStats(String uuid, int sessionId) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("kills", 0);
result.put("deaths", 0);
try {
PreparedStatement ps = db.prepareStatement(
"SELECT kills, deaths FROM pvp_arena_stats WHERE session_id=? AND player_uuid=?");
ps.setInt(1, sessionId);
ps.setString(2, uuid);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("kills", rs.getInt("kills"));
result.put("deaths", rs.getInt("deaths"));
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// ==================== 安全报警 ====================
+14
View File
@@ -5929,6 +5929,20 @@ public class Main extends JavaPlugin
+ count + " 个掉落物");
return true;
}
// ===== 清空队列 / cleartake — 清空DB队列中未回应的web任务 =====
if (cmdName.equals("清空队列") || cmdName.equals("cleartake")) {
if (!isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
if (webManager == null) {
sender.sendMessage("§cWeb通信管理器未初始化");
return true;
}
webManager.clearQueue();
sender.sendMessage("§a已清空DB队列中未回应的web同步任务");
return true;
}
// /menu 开关雪球菜单
if (cmdName.equals("menu")) {
if (!(sender instanceof Player)) {
+59 -14
View File
@@ -110,6 +110,9 @@ public class PVPArenaManager implements Listener {
private final Map<String, Integer> pvpKills = new ConcurrentHashMap<>();
private final Map<String, Integer> pvpDeaths = new ConcurrentHashMap<>();
// ★ 当前竞技场会话ID(持久化到DB)
private int currentSessionId = -1;
// 连杀播报(移植自 PVPManager,全服播报)
private final Map<String, PVPManager.KillSession>
arenaKillSessions =
@@ -332,6 +335,11 @@ public class PVPArenaManager implements Listener {
// 同步分批预生成出生点周边区块(半径2=5×5=25个,每 tick 一行,均摊不冻服)
preGenerateSpawnChunksAsync(pvpWorld, 2);
reapplyWorldRules(pvpWorld);
// ★ 创建DB会话记录(持久化战绩)
currentSessionId = db.createArenaSession(pvpWorldName);
if (currentSessionId > 0) {
plugin.getLogger().info("[PVP] 创建竞技场会话 #" + currentSessionId + " (world=" + pvpWorldName + ")");
}
plugin.getLogger().info("[PVP] PVP竞技场世界就绪 种子=" + seed + " 耗时=" + dt + "ms(NORMAL真实自然地形, 出生点已固化)");
return pvpWorld;
@@ -738,7 +746,7 @@ public class PVPArenaManager implements Listener {
// 兜底:确保世界内无玩家(理论上冷却结束时已无人)
for (Player p : new ArrayList<>(world.getPlayers())) {
World main = Bukkit.getWorlds().get(0);
p.teleport(main.getSpawnLocation());
p.teleport(getBedOrSpawnLocation(p));
p.sendMessage("§e[PVP] 竞技场已关闭,你被传回主世界");
}
Bukkit.unloadWorld(world, false); // false=不保存(即将删除,避免残留破坏的地形)
@@ -916,8 +924,7 @@ public class PVPArenaManager implements Listener {
// 延迟一帧传回主世界(避免与关闭事件冲突)
Bukkit.getScheduler().runTask(plugin, () -> {
if (player.isOnline()) {
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
player.teleport(getBedOrSpawnLocation(player));
player.sendMessage("§c§l[PVP] §c你未确认装备选择,已被遣返回主世界");
player.sendMessage("§7提示:进入PVP后必须选择并确认装备才能开始战斗");
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
@@ -1027,8 +1034,7 @@ public class PVPArenaManager implements Listener {
equipInteracted.remove(playerName);
if (p != null && p.isOnline()) {
p.closeInventory();
World main = Bukkit.getWorlds().get(0);
p.teleport(main.getSpawnLocation());
p.teleport(getBedOrSpawnLocation(p));
p.sendMessage("§c§l[PVP] §c装备选择超时(" + EQUIPMENT_SELECT_TIMEOUT_SECONDS + "秒),已被自动遣返");
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
}
@@ -1156,6 +1162,12 @@ public class PVPArenaManager implements Listener {
// 总榜击杀数
pvpKills.put(kName, pvpKills.getOrDefault(kName, 0) + 1);
// ★ 持久化到DB
if (currentSessionId > 0) {
db.recordArenaKill(currentSessionId, ((Player) killer).getUniqueId().toString(), kName);
db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), vName);
}
// ★ 击杀奖励:PVP世界每完成2次杀敌,发放16个牛排用于补血(背包满则只发能放下的)
int newKills = pvpKills.get(kName);
if (newKills % 2 == 0) {
@@ -1238,6 +1250,10 @@ public class PVPArenaManager implements Listener {
((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + vName + "\u00a7a 当前击杀: " + pvpKills.get(kName));
}
pvpDeaths.put(player.getName(), pvpDeaths.getOrDefault(player.getName(), 0) + 1);
// ★ 非击杀死亡也持久化(如摔死/溺水)
if (currentSessionId > 0) {
db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), player.getName());
}
refreshPVPScoreboard();
// 死亡时保留经验(避免丢失)
@@ -1254,8 +1270,13 @@ public class PVPArenaManager implements Listener {
Player player = event.getPlayer();
if (!inPVPArena.contains(player.getName())) return;
World main = Bukkit.getWorlds().get(0);
event.setRespawnLocation(main.getSpawnLocation());
// ★ 优先床/重生点,兜底主世界出生点
Location bed = player.getRespawnLocation();
if (bed != null && !bed.getWorld().getName().equals(pvpWorldName)) {
event.setRespawnLocation(bed);
} else {
event.setRespawnLocation(Bukkit.getWorlds().get(0).getSpawnLocation());
}
if (!equipmentConfirmed.contains(player.getName())) {
// 未确认装备:身上仍是原背包,只需清状态
@@ -1345,8 +1366,7 @@ public class PVPArenaManager implements Listener {
// 若仍在竞技场世界,送回主世界
if (player.getWorld().getName().equals(pvpWorldName)) {
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
player.teleport(getBedOrSpawnLocation(player));
cleanupPlayerStats(player);
inPVPArena.remove(player.getName());
player.sendMessage("§e[PVP] 已送你回主世界");
@@ -1359,8 +1379,7 @@ public class PVPArenaManager implements Listener {
if (player.getWorld().getName().equals(pvpWorldName)) {
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
if (!player.isOnline()) return;
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
player.teleport(getBedOrSpawnLocation(player));
cleanupPlayerStats(player);
inPVPArena.remove(player.getName());
player.sendMessage("§e[PVP] 检测到你在竞技场非正常断线,已送你回主世界");
@@ -2117,9 +2136,8 @@ public class PVPArenaManager implements Listener {
player.sendMessage("§e你不在PVP竞技场中");
return;
}
// 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena)
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
// 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena),优先床
player.teleport(getBedOrSpawnLocation(player));
player.sendMessage("§a§l正在离开PVP竞技场...");
}
@@ -2152,6 +2170,33 @@ public class PVPArenaManager implements Listener {
saveConfig();
}
/** 获取当前会话ID */
public int getCurrentSessionId() {
return currentSessionId;
}
/** 获取某玩家所有会话的总战绩(从DB) */
public Map<String, Object> getTotalArenaStats(String uuid) {
return db.getArenaTotalStats(uuid);
}
/** 获取指定会话编号的信息 */
public Map<String, Object> getArenaSessionByNumber(int number) {
return db.getArenaSessionByNumber(number);
}
/** 获取某玩家在指定会话中的战绩 */
public Map<String, Object> getArenaSessionStats(String uuid, int sessionId) {
return db.getArenaSessionStats(uuid, sessionId);
}
/** 优先传送玩家到床/重生点,兜底世界出生点 */
public Location getBedOrSpawnLocation(Player player) {
Location bed = player.getRespawnLocation();
if (bed != null) return bed;
return Bukkit.getWorlds().get(0).getSpawnLocation();
}
/** 重置本局公平锁定状态(世界销毁/全量重置时调用,下一局首位玩家重新定死装备) */
private void resetMatchLock() {
lockedEnchantOwner = null;
+48 -2
View File
@@ -1076,8 +1076,15 @@ public class PVPManager implements Listener {
public boolean onCommand(Player p,
String[] args) {
if (args.length == 0) {
return showStats(p,
p.getName());
showStats(p, p.getName());
// ★ 同时展示竞技场总战绩
showArenaTotalStats(p, p.getName());
return true;
}
// ★ 数字参数 → 查询指定会话编号的竞技场战绩
if (args.length == 1 && args[0].matches("\\d+")) {
showArenaSessionStats(p, p.getName(), Integer.parseInt(args[0]));
return true;
}
switch (args[0].toLowerCase()) {
case "create":
@@ -1426,4 +1433,43 @@ public class PVPManager implements Listener {
}
return true;
}
// ★ 展示PVP竞技场总战绩
private void showArenaTotalStats(Player p, String target) {
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
String uuid = off.getUniqueId().toString();
Map<String, Object> stats = plugin.getPVPArenaManager().getTotalArenaStats(uuid);
int kills = (int) stats.get("kills");
int deaths = (int) stats.get("deaths");
if (kills == 0 && deaths == 0) return;
double kd = deaths > 0 ? (double) kills / deaths : kills;
p.sendMessage("§6§l[PVP竞技场] " + target);
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", kd));
}
// ★ 展示指定会话编号的竞技场战绩
private void showArenaSessionStats(Player p, String target, int sessionNumber) {
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
String uuid = off.getUniqueId().toString();
// 先查会话信息(含3天有效期检查)
Map<String, Object> session = plugin.getPVPArenaManager().getArenaSessionByNumber(sessionNumber);
if (session == null) {
p.sendMessage("§c未找到编号 " + sessionNumber + " 的竞技场记录,或已超出3天查询范围");
return;
}
long createdAt = (Long) session.get("created_at");
long threeDaysMs = 3L * 24 * 60 * 60 * 1000;
if (System.currentTimeMillis() - createdAt > threeDaysMs) {
p.sendMessage("§c该记录已超出3天查询范围,不再支持查询");
return;
}
int dbSessionId = (int) session.get("id");
Map<String, Object> stats = plugin.getPVPArenaManager().getArenaSessionStats(uuid, dbSessionId);
int kills = (int) stats.get("kills");
int deaths = (int) stats.get("deaths");
String worldName = (String) session.get("world_name");
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(createdAt));
p.sendMessage("§6§l[PVP竞技场#" + sessionNumber + "] " + target + " §7(" + dateStr + " " + worldName + ")");
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", deaths > 0 ? (double) kills / deaths : kills));
}
}
+45
View File
@@ -310,6 +310,7 @@ public class WebManager {
dbTaskQueue.offer(task);
if (dbTaskQueue.size() > 50) {
plugin.getLogger().warning("[DB队列] 队列积压: " + dbTaskQueue.size() + " 任务等待");
discardOldestIfOverflow();
}
}
@@ -323,6 +324,50 @@ public class WebManager {
}
}
/**
* ★ 清空DB队列中所有未回应的web同步任务
* (清空队列/cleartake 命令调用)
*/
public void clearQueue() {
int count = 0;
Iterator<DbTask> it = dbTaskQueue.iterator();
while (it.hasNext()) {
DbTask t = it.next();
// 只保留登录/商店等关键任务,丢弃所有同步类web任务
if (t.name.contains("TimerA") || t.name.contains("首次")
|| t.name.contains("周期") || t.name.contains("sync")
|| t.name.contains("poll") || t.name.contains("push")
|| t.name.contains("pull") || t.name.contains("cdk")) {
it.remove();
count++;
}
}
plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务");
}
/**
* ★ 队列积压时丢弃最早的任务(优先丢弃同步类低优先级任务)
*/
public void discardOldestIfOverflow() {
if (dbTaskQueue.size() <= 50) return;
int before = dbTaskQueue.size();
// 收集所有普通同步任务(priority >= NORMAL_PRIORITY),按创建时间排序
List<DbTask> syncTasks = new ArrayList<>();
for (DbTask t : dbTaskQueue) {
if (t != null && t.priority >= NORMAL_PRIORITY) {
syncTasks.add(t);
}
}
syncTasks.sort(Comparator.comparingLong(t -> t.createdAt));
// 丢弃最早的一半同步任务(至少丢弃1个)
int toDrop = Math.max(1, syncTasks.size() / 2);
for (int i = 0; i < toDrop && i < syncTasks.size(); i++) {
dbTaskQueue.remove(syncTasks.get(i));
}
int after = dbTaskQueue.size();
plugin.getLogger().warning("[DB队列] 队列积压(" + before + "),自动丢弃" + (before - after) + "个最早的同步请求");
}
// ==================== SSL断路器 + 降级HTTP ====================
/**
+6
View File
@@ -178,3 +178,9 @@ commands:
who:
description: 查看在线玩家列表
usage: /who
清空队列:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /清空队列
cleartake:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /cleartake