From 67f8d6e7fbc74c839aeaa161ec20b5b8fa495930 Mon Sep 17 00:00:00 2001 From: youpaishidifu <1@ypshidifu.cn> Date: Wed, 28 Oct 2026 19:19:38 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC58=E6=89=B9=E4=BF=AE=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=20-=20=E9=A2=86=E5=9C=B0=E8=AF=AF=E5=88=A0=E8=87=B4=E5=91=BDbu?= =?UTF-8?q?g=EF=BC=9AAreaProtection=20=E6=94=B9=E7=94=A8=20userExistsIgnor?= =?UTF-8?q?eCase=EF=BC=88SQL=E5=BC=82=E5=B8=B8=E8=BF=94=E5=9B=9Etrue?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E8=AF=AF=E5=88=A0=EF=BC=89=20-=20getConnecti?= =?UTF-8?q?on=20=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BF=9E=EF=BC=9A=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=B7=B2=E5=85=B3=E9=97=AD=E6=97=B6=E9=87=8D=E5=BB=BA?= =?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=20SN/=E4=BF=A1=E8=AA=89=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20database=20connection=20closed=20-=20userExists/use?= =?UTF-8?q?rExistsIgnoreCase/userExistsStrict=20=E6=94=B9=E7=94=A8=20getCo?= =?UTF-8?q?nnection()=20=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BF=9E=20-=20AI?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=A7=A3=E6=9E=90=E8=B7=B3=E8=BF=87=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E6=97=A5=E5=BF=97=EF=BC=9AfindProviderUrl=20=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=20null=20=E6=97=B6=E8=BE=93=E5=87=BA=E5=85=B7?= =?UTF-8?q?=E4=BD=93=E4=BE=9B=E5=BA=94=E5=95=86=E5=90=8D=E5=92=8C=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=EF=BC=8C=E4=BE=BF=E4=BA=8E=E6=8E=92=E6=9F=A5=200?= =?UTF-8?q?=E4=B8=AA=E6=A8=A1=E5=9E=8B=20=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Sdf1_login/AIChatManager.java | 10 +- src/main/java/Sdf1_login/AreaProtection.java | 5 +- src/main/java/Sdf1_login/DatabaseManager.java | 6323 +++++++++-------- 3 files changed, 3183 insertions(+), 3155 deletions(-) diff --git a/src/main/java/Sdf1_login/AIChatManager.java b/src/main/java/Sdf1_login/AIChatManager.java index cbd0eb1..6ffce44 100644 --- a/src/main/java/Sdf1_login/AIChatManager.java +++ b/src/main/java/Sdf1_login/AIChatManager.java @@ -274,7 +274,10 @@ public class AIChatManager { boolean isCustom = supplierConfig.has("baseurl"); boolean isBuiltin = supplierConfig.has("供应商名字"); - if (!isCustom && !isBuiltin) continue; + if (!isCustom && !isBuiltin) { + plugin.getLogger().info("[AI聊天] 配置跳过[" + key + "]: 既无baseurl也无供应商名字"); + continue; + } String baseUrl; String apiKey = getStr(supplierConfig, "apikey"); @@ -286,9 +289,14 @@ public class AIChatManager { } else { String providerName = getStr(supplierConfig, "供应商名字"); baseUrl = findProviderUrl(providerName); + if (baseUrl == null || baseUrl.isEmpty()) { + plugin.getLogger().warning("[AI聊天] 配置跳过[" + key + "]: 内置供应商 '" + providerName + "' 在 供应商.json 中找不到URL"); + plugin.getLogger().warning("[AI聊天] 诊断: 请确认 供应商.json 中存在路径 '" + providerName + "',且路径拼写完全正确"); + } } if (baseUrl == null || baseUrl.isEmpty() || apiKey == null || apiKey.isEmpty()) { + plugin.getLogger().warning("[AI聊天] 配置跳过[" + key + "]: baseUrl=" + baseUrl + ", apiKey=" + apiKey); continue; } diff --git a/src/main/java/Sdf1_login/AreaProtection.java b/src/main/java/Sdf1_login/AreaProtection.java index 4138dc8..e5af918 100644 --- a/src/main/java/Sdf1_login/AreaProtection.java +++ b/src/main/java/Sdf1_login/AreaProtection.java @@ -10911,8 +10911,11 @@ public class AreaProtection implements Listener { String reason = (String) o[1]; if (reason == null) { if (dbMgr == null) continue; + // ★ 第58批修复:SQL 异常时 userExists 返回 false, + // 会把 DB 抖动误判为"账户已注销"从而删领地。 + // 改用 userExistsIgnoreCase(异常返回 true,宁可放过)。 try { - if (!dbMgr.userExists(ac.owner)) { + if (!dbMgr.userExistsIgnoreCase(ac.owner)) { reason = "账户已注销"; } } catch (Exception ignored) {} diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index e0ca7c8..9f46cd0 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -1,3153 +1,3170 @@ -package Sdf1_login; - -import java.io.File; -import java.sql.*; -import java.util.*; -import java.util.logging.Logger; - -public class DatabaseManager { - - private Connection db; - private final File dataFolder; - private final Logger logger; - - public DatabaseManager(File dataFolder) { - this.dataFolder = dataFolder; - this.logger = Logger.getLogger("Sdf1_login"); - } - - // ==================== 初始化 ==================== - - public void init() { - try { - Class.forName("org.sqlite.JDBC"); - // ★ 2026-10-08 归位:login.db 创建即放入「登录凭证/」; - // 根目录存量由 Main.migrateRootDataFiles() 启动最早期整组迁移 - // (.db + -shm + -wal 同进同退,严禁人工移动)。 - File dbDir = new File(dataFolder, "登录凭证"); - dbDir.mkdirs(); - File dbFile = new File(dbDir, "login.db"); - db = DriverManager.getConnection( - "jdbc:sqlite:" - + dbFile.getAbsolutePath()); - Statement st = db.createStatement(); - st.execute("PRAGMA journal_mode=WAL"); - // ★ 二十二批:busy_timeout —— WAL 下读写仍会与 - // db-worker/长事务争锁,无超时会让主线程 - // sqlite3_step 无限等待(watchdog 卡点之一), - // 5秒等锁后报 SQLITE_BUSY 快速失败。 - st.execute("PRAGMA busy_timeout=5000"); - - // 用户表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "users (" - + "player_name TEXT PRIMARY KEY," - + "password_hash TEXT NOT NULL," - + "password_salt TEXT NOT NULL," - + "email TEXT DEFAULT ''," - + "register_time INTEGER DEFAULT 0," - + "last_login_time INTEGER DEFAULT 0," - + "total_online_time INTEGER DEFAULT 0," - + "last_online_check INTEGER DEFAULT 0," - + "is_logged_in INTEGER DEFAULT 0," - + "blocks_broken INTEGER DEFAULT 0," - + "blocks_placed INTEGER DEFAULT 0," - + "gift_stage INTEGER DEFAULT 0," - + "gift_claimed TEXT DEFAULT ''," - + "invite_code TEXT DEFAULT ''," - + "invited_by TEXT DEFAULT ''," - + "invited_at INTEGER DEFAULT 0," - + "monthly_invite_count INTEGER DEFAULT 0," - + "invite_month TEXT DEFAULT ''," - + "points INTEGER DEFAULT 0," - + "checkin_streak INTEGER DEFAULT 0," - + "last_checkin_date TEXT DEFAULT ''," - + "total_checkin_days INTEGER DEFAULT 0," - + "temp_password TEXT DEFAULT ''," - + "temp_pw_expire INTEGER DEFAULT 0," - + "temp_pw_used INTEGER DEFAULT 0," - + "ip_address TEXT DEFAULT ''," - + "tasks_completed TEXT DEFAULT ''," - + "premium_uuid TEXT DEFAULT ''," - + "register_ip TEXT DEFAULT ''," - + "register_type TEXT DEFAULT 'manual'," - + "backcheck_dates TEXT DEFAULT ''" - + ")"); - - - // 安全添加 users 缺失列 - safeAdd(st, "tasks_completed", - "TEXT DEFAULT ''"); - safeAdd(st, "temp_password", - "TEXT DEFAULT ''"); - safeAdd(st, "temp_pw_expire", - "INTEGER DEFAULT 0"); - safeAdd(st, "temp_pw_used", - "INTEGER DEFAULT 0"); - safeAdd(st, "ip_address", - "TEXT DEFAULT ''"); - safeAdd(st, "premium_uuid", - "TEXT DEFAULT ''"); - safeAdd(st, "register_ip", - "TEXT DEFAULT ''"); - safeAdd(st, "register_type", - "TEXT DEFAULT 'manual'"); - safeAdd(st, "login_streak", - "INTEGER DEFAULT 0"); - safeAdd(st, "last_login_date", - "TEXT DEFAULT ''"); - safeAdd(st, "last_login_ip", - "TEXT DEFAULT ''"); - // ★ 第九批:邀请绑定时间(毫秒时间戳), - // 供「我的邀请码」GUI 显示邀请成功时间 - safeAdd(st, "invited_at", - "INTEGER DEFAULT 0"); - // UI偏好:1=CLI(默认), 0=GUI - safeAdd(st, "ui_mode", - "INTEGER DEFAULT 1"); - // ===== 风控 / 封禁相关列 ===== - // IP归属(省+市+区),用于异地登录检测 - safeAdd(st, "last_login_location", - "TEXT DEFAULT ''"); - // 账号冻结(玩家主动冻结防被盗):0=正常 1=已冻结 - safeAdd(st, "frozen", - "INTEGER DEFAULT 0"); - // 冻结校验 token(邮件链接防猜测) - safeAdd(st, "freeze_token", - "TEXT DEFAULT ''"); - // 禁言到期时间戳(毫秒),0=未禁言 - safeAdd(st, "muted_until", - "INTEGER DEFAULT 0"); - // 禁言理由 - safeAdd(st, "mute_reason", - "TEXT DEFAULT ''"); - // ★ 正版OAuth登录IP(用于异地登录风控) - safeAdd(st, "last_oauth_ip", - "TEXT DEFAULT ''"); - // ===== 2FA 二次验证(TOTP,仅Java本地校验)===== - safeAdd(st, "twofa_secret", - "TEXT DEFAULT ''"); - safeAdd(st, "twofa_enabled", - "INTEGER DEFAULT 0"); - safeAdd(st, "twofa_pending_secret", - "TEXT DEFAULT ''"); - safeAdd(st, "twofa_pending_at", - "INTEGER DEFAULT 0"); - // ★ 第47批:绑定时间(秒)——与网页端 web_account_2fa.bound_at 对齐, - // 双向同步冲突时"取时间新者"用 - safeAdd(st, "twofa_bound_at", - "INTEGER DEFAULT 0"); - // ===== 2FA 恢复代码(SHA-256 逗号连接存库,明文只发邮件,一次性有效)===== - safeAdd(st, "twofa_recovery_codes", - "TEXT DEFAULT ''"); - - // ★ 一次性自愈:历史版本 setField(null) 曾把 temp_password - // 写成字面量 'null',会让 credentials 指纹永久不一致、 - // 对账每轮补推。启动时统一清回 SQL NULL。 - try { - st.executeUpdate("UPDATE users SET temp_password=NULL " - + "WHERE temp_password='null'"); - } catch (SQLException ignored) { - } - - // ★ 第52批:存量脏凭证一次性清洗——PHP json_encode 把 base64 中的 - // / 转义成 \/,历史解析层漏还原曾把反斜杠写进库(hash 脏 → 密码恒错)。 - // base64 合法字符永不含反斜杠(char92),删除即还原正确值;WHERE 带 - // instr 条件,无脏值时为近似 no-op,可每次启动安全重跑。 - try { - st.executeUpdate("UPDATE users SET password_hash=REPLACE(password_hash, char(92), '') " - + "WHERE instr(password_hash, char(92))>0"); - st.executeUpdate("UPDATE users SET password_salt=REPLACE(password_salt, char(92), '') " - + "WHERE instr(password_salt, char(92))>0"); - st.executeUpdate("UPDATE users SET temp_password=REPLACE(temp_password, char(92), '') " - + "WHERE instr(temp_password, char(92))>0"); - } catch (SQLException ignored) { - } - - // ===== 背包备份表:安全迁移 ===== - // 检查旧表是否存在(用 inventory_data 列判断) - boolean hasOldTable = false; - try { - ResultSet rs = st.executeQuery( - "SELECT inventory_data " - + "FROM inventory_backups " - + "LIMIT 1"); - hasOldTable = true; - rs.close(); - } catch (SQLException e) { - hasOldTable = false; - } - - if (hasOldTable) { - // 旧表存在,迁移数据到新表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "inventory_backups_new (" - + "id INTEGER PRIMARY KEY " - + "AUTOINCREMENT," - + "player_name TEXT NOT NULL," - + "contents_b64 TEXT DEFAULT ''," - + "armor_b64 TEXT DEFAULT ''," - + "extra_b64 TEXT DEFAULT ''," - + "level INTEGER DEFAULT 0," - + "experience REAL DEFAULT 0," - + "save_time INTEGER DEFAULT 0" - + ")"); - // 迁移旧数据 - try { - st.execute("INSERT INTO " - + "inventory_backups_new " - + "(player_name, " - + "contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience) " - + "SELECT player_name, " - + "inventory_data, " - + "armor_data, " - + "extra_data, " - + "player_level, " - + "player_exp " - + "FROM inventory_backups"); - } catch (SQLException ignored) { - } - st.execute("DROP TABLE " - + "inventory_backups"); - st.execute("ALTER TABLE " - + "inventory_backups_new " - + "RENAME TO " - + "inventory_backups"); - } else { - // 无旧表或已迁移,直接创建 - st.execute("CREATE TABLE IF NOT EXISTS " - + "inventory_backups (" - + "id INTEGER PRIMARY KEY " - + "AUTOINCREMENT," - + "player_name TEXT NOT NULL," - + "contents_b64 TEXT DEFAULT ''," - + "armor_b64 TEXT DEFAULT ''," - + "extra_b64 TEXT DEFAULT ''," - + "level INTEGER DEFAULT 0," - + "experience REAL DEFAULT 0," - + "save_time INTEGER DEFAULT 0" - + ")"); - } - - // PVP 背包备份表(独立表:避免与登录背包系统的 inventory_backups 互相覆盖导致原始背包丢失) - st.execute("CREATE TABLE IF NOT EXISTS " - + "pvp_inventory_backup (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "player_name TEXT NOT NULL," - + "contents_b64 TEXT DEFAULT ''," - + "armor_b64 TEXT DEFAULT ''," - + "extra_b64 TEXT DEFAULT ''," - + "level INTEGER DEFAULT 0," - + "experience REAL DEFAULT 0," - + "save_time INTEGER DEFAULT 0" - + ")"); - - // 安全报警表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "security_alerts (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "player_name TEXT NOT NULL," - + "alert_ip TEXT NOT NULL," - + "alert_time INTEGER NOT NULL," - + "alert_date TEXT NOT NULL," - + "notified INTEGER DEFAULT 0," - + "notified_time INTEGER DEFAULT 0)"); - // PVP区域表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "pvp_regions (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "name TEXT NOT NULL UNIQUE," - + "world TEXT NOT NULL," - + "x1 INTEGER NOT NULL," - + "y1 INTEGER NOT NULL," - + "z1 INTEGER NOT NULL," - + "x2 INTEGER NOT NULL," - + "y2 INTEGER NOT NULL," - + "z2 INTEGER NOT NULL," - + "create_time INTEGER DEFAULT 0" - + ")"); - -// PVP击杀总榜表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "pvp_kills (" - + "player_name TEXT NOT NULL," - + "region_name TEXT NOT NULL," - + "kills INTEGER DEFAULT 0," - + "deaths INTEGER DEFAULT 0," - + "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 " - + "menu_icons (" - + "player_name TEXT PRIMARY KEY," - + "icon_b64 TEXT DEFAULT ''," - + "item_name TEXT DEFAULT ''," - + "save_time INTEGER DEFAULT 0" - + ")"); - - // 工单表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "tickets (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "type TEXT NOT NULL," - + "status TEXT DEFAULT 'pending'," - + "priority TEXT DEFAULT 'normal'," - + "requester TEXT NOT NULL," - + "assigned_to TEXT DEFAULT ''," - + "title TEXT DEFAULT ''," - + "description TEXT DEFAULT ''," - + "reward_amount INTEGER DEFAULT 0," - + "score INTEGER DEFAULT 0," - + "target_player TEXT DEFAULT ''," - + "create_time INTEGER NOT NULL," - + "update_time INTEGER DEFAULT 0," - + "complete_time INTEGER DEFAULT 0," - + "admin_confirmed INTEGER DEFAULT 0," - + "reject_reason TEXT DEFAULT ''" - + ")"); - - // 工单回复表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "ticket_replies (" - + "id INTEGER PRIMARY KEY " - + "AUTOINCREMENT," - + "ticket_id INTEGER NOT NULL," - + "sender TEXT NOT NULL," - + "message TEXT NOT NULL," - + "create_time INTEGER NOT NULL" - + ")"); - - // 服务商表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "service_providers (" - + "player_name TEXT PRIMARY KEY," - + "role TEXT DEFAULT 'waiter'," - + "active INTEGER DEFAULT 1," - + "join_time INTEGER DEFAULT 0" - + ")"); - // 垃圾回收站物品表 - st.execute("CREATE TABLE IF NOT EXISTS " - + "items (" - + "id INTEGER PRIMARY KEY " - + "AUTOINCREMENT," - + "player_name TEXT NOT NULL," - + "item_data TEXT DEFAULT ''," - + "amount INTEGER DEFAULT 1," - + "create_time INTEGER DEFAULT 0" - + ")"); - safeAdd(st, "menu_snowball", - "INTEGER DEFAULT 1"); - - // Home传送点表 — 只在不存在时创建,不删除已有数据 - st.execute("CREATE TABLE IF NOT EXISTS homes (" - + "player_name TEXT NOT NULL," - + "home_name TEXT NOT NULL," - + "world TEXT NOT NULL," - + "x REAL NOT NULL," - + "y REAL NOT NULL," - + "z REAL NOT NULL," - + "yaw REAL DEFAULT 0," - + "pitch REAL DEFAULT 0," - + "created_at INTEGER DEFAULT 0," - + "PRIMARY KEY(player_name, home_name)" - + ")"); - safeAdd(st, "menu_snowball", - "INTEGER DEFAULT 1"); - - // 领地方块记录表:记录领地内谁破坏/放置了什么方块(7天滚动) - st.execute("CREATE TABLE IF NOT EXISTS " - + "land_block_log (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "land_name TEXT," - + "land_id INTEGER," - + "world TEXT," - + "x INTEGER," - + "y INTEGER," - + "z INTEGER," - + "player_name TEXT," - + "action TEXT," - + "material TEXT," - + "detail TEXT," - + "time INTEGER" - + ")"); - - // ★ 新增detail列(已有数据库兼容) - try { - st.executeUpdate("ALTER TABLE land_block_log ADD COLUMN detail TEXT"); - } catch (SQLException ignored) {} - - // 领地容器记录表:记录领地内容器的打开/拿取(7天滚动) - st.execute("CREATE TABLE IF NOT EXISTS " - + "land_container_log (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "land_name TEXT," - + "land_id INTEGER," - + "world TEXT," - + "x INTEGER," - + "y INTEGER," - + "z INTEGER," - + "player_name TEXT," - + "action TEXT," - + "container_type TEXT," - + "detail TEXT," - + "amount INTEGER," - + "time INTEGER" - + ")"); - - // 背包自动滚动备份表(每小时 0/15/30/45 分准点定时):每位玩家保留最近 3 份 - st.execute("CREATE TABLE IF NOT EXISTS " - + "inv_backup_auto (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "player_name TEXT NOT NULL," - + "contents_b64 TEXT DEFAULT ''," - + "armor_b64 TEXT DEFAULT ''," - + "extra_b64 TEXT DEFAULT ''," - + "level INTEGER DEFAULT 0," - + "experience REAL DEFAULT 0," - + "save_time INTEGER DEFAULT 0" - + ")"); - - // ★ 充值回执镜像表(2026-10-06,Bug「清空PHP后充值记录回不来」的修复) - // 充值流水一式两份:PHP 持 web_transactions(type=recharge) 一份, - // Java 持这里一份。每轮全量同步时双方点差集、多退少补: - // PHP 缺 → Java 补推重建流水(只补记录,绝不重复加钱) - // Java 缺 → 收回来补齐基线(同样只补记录) - // tx_id = PHP web_transactions.id,作为两边共同的主键。 - st.execute("CREATE TABLE IF NOT EXISTS " - + "web_tx_receipts (" - + "tx_id INTEGER PRIMARY KEY," - + "player_name TEXT NOT NULL DEFAULT ''," - + "type TEXT NOT NULL DEFAULT ''," - + "amount INTEGER NOT NULL DEFAULT 0," - + "detail TEXT NOT NULL DEFAULT ''," - + "created_at INTEGER NOT NULL DEFAULT 0" - + ")"); - - // 注:传送请求不再落库,仅驻留内存(配置项才落库),故不再建 teleport_requests 表 - st.close(); - logger.info("[Sdf1_login] 数据库初始化完成"); - } catch (Exception e) { - throw new RuntimeException( - "[Sdf1_login] DB初始化失败: " - + e.getMessage(), e); - } - } - - - private void safeAdd(Statement st, - String col, String def) { - try { - st.executeUpdate("ALTER TABLE users " - + "ADD COLUMN " + col + " " + def); - } catch (SQLException ignored) { - } - } - - // ==================== 自动续费偏好 ==================== - - /** 获取玩家的自动续费偏好 */ - public boolean getPlayerAutoRenew(String player, String group) { - try { - Statement st = db.createStatement(); - st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))"); - String sql = "SELECT auto_renew FROM player_auto_renew WHERE player_name = ? AND group_name = ?"; - PreparedStatement ps = db.prepareStatement(sql); - ps.setString(1, player); - ps.setString(2, group); - ResultSet rs = ps.executeQuery(); - if (rs.next()) return rs.getInt("auto_renew") == 1; - } catch (Exception ignored) {} - return false; - } - - /** 设置玩家的自动续费偏好 */ - public void setPlayerAutoRenew(String player, String group, boolean value) { - try { - Statement st = db.createStatement(); - st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))"); - String sql = "INSERT OR REPLACE INTO player_auto_renew (player_name, group_name, auto_renew) VALUES (?, ?, ?)"; - PreparedStatement ps = db.prepareStatement(sql); - ps.setString(1, player); - ps.setString(2, group); - ps.setInt(3, value ? 1 : 0); - ps.executeUpdate(); - } catch (Exception ignored) {} - } - - /** 获取玩家所有自动续费开启的组 */ - public List> getAutoRenewGroups(String player) { - List> result = new ArrayList<>(); - try { - Statement st = db.createStatement(); - st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))"); - String sql = "SELECT group_name FROM player_auto_renew WHERE player_name = ? AND auto_renew = 1"; - PreparedStatement ps = db.prepareStatement(sql); - ps.setString(1, player); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = new HashMap<>(); - row.put("group_name", rs.getString("group_name")); - result.add(row); - } - } catch (Exception ignored) {} - return result; - } - - public void close() { - try { - if (db != null && !db.isClosed()) - db.close(); - } catch (Exception ignored) { - } - } - - public Connection getConnection() { - return db; - } - - // ==================== 充值回执镜像(web_tx_receipts,2026-10-06) ==================== - // 充值流水一式两份:PHP 持 web_transactions(type=recharge),Java 持本表。 - // 两边按 tx_id 点差集、多退少补,任何一边被清空都能从另一边补回来。 - - /** 写入/更新一条回执(主键 tx_id = PHP web_transactions.id) */ - public void upsertWebTxReceipt(long txId, String playerName, String type, - int amount, String detail, long createdAt) { - try (java.sql.PreparedStatement ps = db.prepareStatement( - "INSERT OR REPLACE INTO web_tx_receipts " - + "(tx_id, player_name, type, amount, detail, created_at) " - + "VALUES (?, ?, ?, ?, ?, ?)")) { - ps.setLong(1, txId); - ps.setString(2, playerName == null ? "" : playerName); - ps.setString(3, type == null ? "" : type); - ps.setInt(4, amount); - ps.setString(5, detail == null ? "" : detail); - ps.setLong(6, createdAt); - ps.executeUpdate(); - } catch (Exception e) { - logger.warning("[回执镜像] 写入失败 tx_id=" + txId + ": " + e.getMessage()); - } - } - - /** 取全部回执(对账用,字段名与 PHP web_transactions 对齐) */ - public List> getAllWebTxReceipts() { - List> out = new ArrayList<>(); - try (java.sql.PreparedStatement ps = db.prepareStatement( - "SELECT tx_id, player_name, type, amount, detail, created_at " - + "FROM web_tx_receipts ORDER BY tx_id ASC"); - java.sql.ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - Map row = new LinkedHashMap<>(); - row.put("id", rs.getLong("tx_id")); - row.put("player_name", rs.getString("player_name")); - row.put("type", rs.getString("type")); - row.put("amount", rs.getInt("amount")); - row.put("detail", rs.getString("detail")); - row.put("created_at", rs.getLong("created_at")); - out.add(row); - } - } catch (Exception e) { - logger.warning("[回执镜像] 读取失败: " + e.getMessage()); - } - return out; - } - - /** 是否已有某条回执 */ - public boolean webTxReceiptExists(long txId) { - try (java.sql.PreparedStatement ps = db.prepareStatement( - "SELECT 1 FROM web_tx_receipts WHERE tx_id = ? LIMIT 1")) { - ps.setLong(1, txId); - try (java.sql.ResultSet rs = ps.executeQuery()) { - return rs.next(); - } - } catch (Exception e) { - return false; - } - } - - // ==================== UI偏好 ==================== - - /** - * 获取玩家UI偏好 - * @return 1=CLI(默认), 0=GUI - */ - public int getUiMode(String playerName) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT ui_mode FROM users WHERE player_name=?"); - ps.setString(1, playerName); - ResultSet rs = ps.executeQuery(); - int mode = 1; // 默认CLI - if (rs.next()) { - mode = rs.getInt("ui_mode"); - } - rs.close(); - ps.close(); - return mode; - } catch (SQLException e) { - return 1; // 默认CLI - } - } - - /** - * 设置玩家UI偏好 - * @param mode 1=CLI, 0=GUI - */ - public void setUiMode(String playerName, int mode) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE users SET ui_mode=? WHERE player_name=?"); - ps.setInt(1, mode); - ps.setString(2, playerName); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - // 静默 - } - } -// ==================== 菜单图标 ==================== - - public void saveMenuIcon(String name, - String iconB64, String itemName) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT OR REPLACE INTO menu_icons " - + "(player_name, icon_b64, " - + "item_name, save_time) " - + "VALUES (?,?,?,?)"); - ps.setString(1, name); - ps.setString(2, iconB64); - ps.setString(3, itemName); - ps.setLong(4, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public String getMenuIcon(String name) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT icon_b64 FROM menu_icons " - + "WHERE player_name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - String b64 = null; - if (rs.next()) { - b64 = rs.getString("icon_b64"); - } - rs.close(); - ps.close(); - return b64; - } catch (SQLException e) { - return null; - } - } - - public void deleteMenuIcon(String name) { - try { - PreparedStatement ps = db.prepareStatement( - "DELETE FROM menu_icons " - + "WHERE player_name=?"); - ps.setString(1, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public boolean hasMenuIcon(String name) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT 1 FROM menu_icons " - + "WHERE player_name=? " - + "AND icon_b64 IS NOT NULL " - + "AND icon_b64 != '' " - + "LIMIT 1"); - ps.setString(1, name); - boolean r = ps.executeQuery().next(); - ps.close(); - return r; - } catch (SQLException e) { - return false; - } - } - - // ==================== 用户操作 ==================== - - public boolean userExists(String name) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT 1 FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - boolean r = ps.executeQuery().next(); - ps.close(); - return r; - } catch (SQLException e) { - return false; - } - } - - /** - * ★ 封禁防护用:大小写不敏感的注册校验。 - * 返回 false 仅表示「确认未注册」;SQL 异常时返回 true 并告警 - * (宁可放行,也不因 DB 抖动误拒正常的封禁操作)。 - */ - public boolean userExistsIgnoreCase(String name) { - if (name == null || name.isEmpty()) return false; - try { - PreparedStatement ps = db.prepareStatement( - "SELECT 1 FROM users " - + "WHERE player_name=? " - + "COLLATE NOCASE"); - ps.setString(1, name); - boolean r = ps.executeQuery().next(); - ps.close(); - return r; - } catch (SQLException e) { - logger.warning("[封禁防护] 注册校验查询失败(放行): " - + e.getMessage()); - return true; - } - } - - /** - * ★ OP白名单授权校验用(2026-10-11):与 userExistsIgnoreCase 相反的失败语义—— - * SQL 异常一律返回 false(保守拒绝)。opwl add/addtime 宁可让运维改 json 兜底, - * 也绝不因 DB 抖动把临时 OP 发给未注册玩家。 - */ - public boolean userExistsStrict(String name) { - if (name == null || name.isEmpty()) return false; - try { - PreparedStatement ps = db.prepareStatement( - "SELECT 1 FROM users " - + "WHERE player_name=? " - + "COLLATE NOCASE"); - ps.setString(1, name); - boolean r = ps.executeQuery().next(); - ps.close(); - return r; - } catch (SQLException e) { - logger.warning("[OP白名单] 注册校验查询失败(拒绝): " - + e.getMessage()); - return false; - } - } - - /** - * ★ 第52批:base64 凭证白名单清洗。 - * PHP json_encode 默认把 base64 中的 / 转义成 \/,历史解析层漏还原 - * 会把反斜杠写进库(hash 脏值 → 游戏/网页两端密码恒错)。 - * base64 合法字符永不含反斜杠,先还原 \/ 再按白名单清洗即得正确值; - * 写库与比对两侧同款清洗,保证对称。 - */ - public static String b64Clean(String v) { - if (v == null) return null; - return v.replace("\\/", "/").replaceAll("[^A-Za-z0-9+/=]", ""); - } - - public void createUser(String name, String hash, - String salt) { - hash = b64Clean(hash); - salt = b64Clean(salt); - try { - PreparedStatement ps = db.prepareStatement( - "INSERT INTO users " - + "(player_name, " - + "password_hash, " - + "password_salt, " - + "register_time) " - + "VALUES (?,?,?,?)"); - ps.setString(1, name); - ps.setString(2, hash); - ps.setString(3, salt); - ps.setLong(4, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - /** - * 更新玩家密码凭证(salt + hash),用于Web重新注册时同步 - */ - public void updatePassword(String name, String hash, String salt) { - hash = b64Clean(hash); - salt = b64Clean(salt); - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE users SET password_hash = ?, password_salt = ? WHERE player_name = ?"); - ps.setString(1, hash); - ps.setString(2, salt); - ps.setString(3, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - // ========== 临时密码管理 ========== - - public void clearTempPassword(String name) { - setField(name, "temp_password", null); - setField(name, "temp_pw_expire", 0); - setField(name, "temp_pw_used", 1); - } - - public void clearAllTempPasswords() { - try { - Statement st = db.createStatement(); - st.executeUpdate( - "UPDATE users SET " - + "temp_password=NULL," - + "temp_pw_expire=0," - + "temp_pw_used=1 " - + "WHERE temp_password " - + "IS NOT NULL"); - st.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public String getPlayerIPByName( - String name) { - Object ip = getField(name, "ip"); - return ip != null - ? String.valueOf(ip) : null; - } - - /** - * 验证密码(仅主密码) - * @return true if matched - */ - public boolean checkPassword(String name, String hash) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT password_hash " - + "FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String mainHash = - rs.getString("password_hash"); - rs.close(); - ps.close(); - // ★ 第52批:比对前双侧清洗(存量脏 hash 容错,见 b64Clean) - return b64Clean(hash).equals(b64Clean(mainHash)); - } else { - rs.close(); - ps.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - return false; - } - - /** - * 验证密码(主密码优先,不通过再查临时密码) - * 注意: 传入的 hash 必须是主密码的 hash - * @return "main" | "temp" | null - */ - public String checkPasswordWithFallback(String name, String mainHash) { - // 先检查主密码 - if (checkPassword(name, mainHash)) { - return "main"; - } - // 主密码不匹配,检查临时密码 - try { - PreparedStatement ps = db.prepareStatement( - "SELECT temp_password, " - + "temp_pw_expire, " - + "temp_pw_used " - + "FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String tempHash = rs.getString("temp_password"); - long expire = rs.getLong("temp_pw_expire"); - int used = rs.getInt("temp_pw_used"); - rs.close(); - ps.close(); - if (tempHash != null - && !tempHash.isEmpty() - && b64Clean(mainHash).equals(b64Clean(tempHash)) - && System.currentTimeMillis() < expire - && used != 1) { - return "temp"; - } - } else { - rs.close(); - ps.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - /** - * ★ 修改密码专用校验:同时校验【真实密码】与【临时密码】。 - * - * 与登录校验 (checkPasswordWithFallback) 的区别: - * 临时密码的有效期只对"登录"生效,对"改密"不生效。 - * 原因:玩家用临时密码登录成功后,若忘记真实密码, - * 一旦临时密码 5 分钟过期,就再也没有任何途径改密, - * 账号被永久锁死。改密本身就是"持有旧凭据即可换新凭据", - * 因此这里只要求临时密码【存在且未被使用过】, - * 不再要求它仍在登录有效期内。 - * - * @return "main" = 匹配真实密码 | "temp" = 匹配临时密码 | null = 都不匹配 - */ - public String checkPasswordForChange(String name, String hash) { - // 1. 先校验真实密码 - if (checkPassword(name, hash)) { - return "main"; - } - // 2. 再校验临时密码(不要求未过期,但必须未被消耗) - try { - PreparedStatement ps = db.prepareStatement( - "SELECT temp_password, " - + "temp_pw_used " - + "FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String tempHash = rs.getString("temp_password"); - int used = rs.getInt("temp_pw_used"); - rs.close(); - ps.close(); - if (tempHash != null - && !tempHash.isEmpty() - && b64Clean(hash).equals(b64Clean(tempHash)) - && used != 1) { - return "temp"; - } - } else { - rs.close(); - ps.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - public Map getUser(String name) { - Map r = - new LinkedHashMap<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - r.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return r; - } - - public void setField(String name, String field, - Object val) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE users SET " + field - + "=? " - + "WHERE player_name=?"); - if (val instanceof Integer) - ps.setInt(1, (Integer) val); - else if (val instanceof Long) - ps.setLong(1, (Long) val); - // ★ null 必须写 SQL NULL:走 String.valueOf(null) 会把 - // temp_password 写成字面量 'null',两端指纹永不一致, - // 对账每轮恒定补推(2026-10-07 测试服 credentials 补推1 根因) - else if (val == null) - ps.setNull(1, java.sql.Types.VARCHAR); - else - ps.setString(1, - String.valueOf(val)); - ps.setString(2, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - /** 读取当前仍有效的禁言列表(muted_until > now) */ - public List getActiveMutes() { - List list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT player_name, muted_until, " - + "mute_reason FROM users " - + "WHERE muted_until > ?"); - ps.setLong(1, - System.currentTimeMillis()); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Object[] row = new Object[3]; - row[0] = rs.getString( - "player_name"); - row[1] = rs.getLong( - "muted_until"); - row[2] = rs.getString( - "mute_reason"); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - public Object getField(String name, - String field) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT " + field - + " FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - Object val = rs.next() - ? rs.getObject(1) : null; - rs.close(); - ps.close(); - return val; - } catch (SQLException e) { - return null; - } - } - - public boolean isUsingTempPassword(String name) { - String h = (String) getField(name, - "temp_password"); - return h != null && !h.isEmpty(); - } - - public void setLoggedIn(String name, - boolean val) { - setField(name, "is_logged_in", - val ? 1 : 0); - } - - public void addPoints(String name, int amount) { - Object cur = getField(name, "points"); - int curVal = cur instanceof Number - ? ((Number) cur).intValue() : 0; - setField(name, "points", curVal + amount); - } - - public boolean deductPoints(String name, - int amount) { - Object cur = getField(name, "points"); - int curVal = cur instanceof Number - ? ((Number) cur).intValue() : 0; - if (curVal < amount) return false; - setField(name, "points", - curVal - amount); - return true; - } - public List> - getAllUsers() { - List> list = - new ArrayList<>(); - try { - Statement st = db.createStatement(); - ResultSet rs = st.executeQuery( - "SELECT * FROM users"); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - st.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - public List> - getTicketsByProviderAll( - String provider) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM tickets " - + "WHERE assigned_to=? " - + "ORDER BY create_time DESC"); - ps.setString(1, provider); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs - .getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - /** - * ★ 精准大小写匹配:TEST 与 test 是两个独立账号, - * 所有"按名字查/删"的入口都必须区分大小写。 - * @return 完全相等(区分大小写)时返回该玩家名,否则返回 null - */ - public String findExactUserName(String name) { - if (name == null || name.isEmpty()) return null; - try { - PreparedStatement ps = db.prepareStatement( - "SELECT player_name FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - String found = null; - while (rs.next()) { - String row = rs.getString(1); - if (name.equals(row)) { // 精准匹配,拒绝大小写兜底 - found = row; - break; - } - } - rs.close(); - ps.close(); - return found; - } catch (SQLException e) { - return null; - } - } - - /** - * 查找"只差大小写"的同名账号(仅用于提示/告警,绝不用于删除)。 - * @return 找到则返回库里的真实玩家名,否则返回 null - */ - public String findSimilarUserNameIgnoreCase(String name) { - if (name == null || name.isEmpty()) return null; - try { - PreparedStatement ps = db.prepareStatement( - "SELECT player_name FROM users " - + "WHERE LOWER(player_name)=LOWER(?)"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - String found = null; - while (rs.next()) { - String row = rs.getString(1); - if (!name.equals(row)) { // 只要大小写不同 - found = row; - break; - } - } - rs.close(); - ps.close(); - return found; - } catch (SQLException e) { - return null; - } - } - - /** - * 精准删除账号(区分大小写)。 - *

- * TEST 与 test 是两个人:只删 player_name 完全相等的那一条; - * 若只有大小写不同的账号存在,则拒绝删除并记录告警。 - * - * @return 实际删除的行数(0 = 未删除) - */ - public int deleteUser(String name) { - if (name == null || name.isEmpty()) return 0; - if (findExactUserName(name) == null) { - String twin = findSimilarUserNameIgnoreCase(name); - if (twin != null) { - System.out.println("[Sdf1_login] 删除账号拒绝:" - + name + " 不存在,但存在大小写不同的账号 " - + twin + "(已按精准匹配跳过,未误删)"); - } - return 0; - } - int deleted = 0; - try { - PreparedStatement ps = db.prepareStatement( - "DELETE FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - deleted = ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return deleted; - } - - public void recordIP(String name, String ip) { - if (ip != null && !ip.isEmpty()) { - setField(name, "ip_address", ip); - // ★ 同时更新最后登录IP - setField(name, "last_login_ip", ip); - } - } - - // ==================== IP查询 ==================== - - /** - * 统计指定IP注册的账号数量 - */ - public int countIPAccounts(String ip) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT COUNT(*) FROM users " - + "WHERE ip_address=?"); - ps.setString(1, ip); - ResultSet rs = ps.executeQuery(); - int count = 0; - if (rs.next()) count = rs.getInt(1); - rs.close(); - ps.close(); - return count; - } catch (SQLException e) { - return 0; - } - } - - /** - * 获取指定IP下所有账号详情 - */ - public List> - getAccountDetailsByIP(String ip) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT player_name, " - + "register_time " - + "FROM users " - + "WHERE ip_address=? " - + "ORDER BY register_time"); - ps.setString(1, ip); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - row.put("player_name", - rs.getString("player_name")); - row.put("register_time", - rs.getLong("register_time")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - // ==================== 背包备份 ==================== - - /** - * 保存背包主备份 —— 追加写入,每位玩家仅保留最近 3 份(更早的自动清理)。 - * ★ 2026-07-09 改版:原为「DELETE 全部再 INSERT 单份」,会导致 PVP 竞技场/测试场 - * 掉线时用当前(竞技场装备)背包覆盖玩家真实背包。改为滚动保留 3 份后, - * 即便发生误覆盖,玩家仍可从「最近 3 份」里挑出真实背包还原。 - */ - public void saveInventoryBackup(String name, - String contents, String armor, - String extra, int level, - double experience) { - try { - PreparedStatement ps = - db.prepareStatement( - "INSERT INTO " - + "inventory_backups" - + " (player_name, " - + "contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience, " - + "save_time) " - + "VALUES " - + "(?,?,?,?,?,?,?)"); - ps.setString(1, name); - ps.setString(2, contents); - ps.setString(3, armor); - ps.setString(4, extra); - ps.setInt(5, level); - ps.setDouble(6, experience); - ps.setLong(7, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - - // 仅保留最近 3 份,删除该玩家更早的备份 - // ★ 2026-07-10:改为先查后删,避免某些 SQLite 版本/驱动把带 LIMIT 的子查询优化掉, - // 导致只保留最新一份的问题。 - try { - PreparedStatement sel = - db.prepareStatement( - "SELECT id FROM inventory_backups " - + "WHERE player_name=? " - + "ORDER BY save_time DESC LIMIT 3"); - sel.setString(1, name); - ResultSet rs = sel.executeQuery(); - java.util.List keep = new java.util.ArrayList<>(); - while (rs.next()) { - keep.add(rs.getInt("id")); - } - rs.close(); - sel.close(); - - if (keep.size() == 3) { - // 保留 3 份,删除其余 - StringBuilder in = new StringBuilder(); - for (int i = 0; i < keep.size(); i++) { - if (i > 0) in.append(","); - in.append(keep.get(i)); - } - PreparedStatement del = db.prepareStatement( - "DELETE FROM inventory_backups " - + "WHERE player_name=? AND id NOT IN (" - + in.toString() + ")"); - del.setString(1, name); - del.executeUpdate(); - del.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - } - - - public String[] loadInventoryBackup( - String name) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience " - + "FROM " - + "inventory_backups " - + "WHERE player_name=? " - + "ORDER BY save_time " - + "DESC LIMIT 1"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String[] result = - new String[5]; - result[0] = rs.getString( - "contents_b64"); - result[1] = rs.getString( - "armor_b64"); - result[2] = rs.getString( - "extra_b64"); - result[3] = String.valueOf( - rs.getInt("level")); - result[4] = String.valueOf( - rs.getDouble("experience")); - rs.close(); - ps.close(); - return result; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - public void deleteInventoryBackup( - String name) { - try { - PreparedStatement ps = db.prepareStatement( - "DELETE FROM inventory_backups " - + "WHERE player_name=?"); - ps.setString(1, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - // ==================== PVP 背包备份(独立表,避免与登录背包系统互相覆盖)==================== - // 登录背包系统使用 inventory_backups 表;PVP 备份若共用同一表/同一玩家名键, - // 会在换装、断线重连等时机互相覆盖,导致玩家"原始背包丢失"。故 PVP 必须独立存储。 - - /** - * 保存 PVP 背包备份(非覆盖)。 - * ★ 关键保护:只要该玩家已存在 PVP 背包备份(即尚未真正退场),绝不再次写入, - * 确保入场前的原始背包永远不被后续状态(换装/断线重连)覆盖。 - */ - public void savePvpInventoryBackup(String name, - String contents, String armor, - String extra, int level, - double experience) { - try { - if (hasPvpInventoryBackup(name)) { - return; // 已有备份,严禁覆盖 - } - PreparedStatement ps = - db.prepareStatement( - "INSERT INTO " - + "pvp_inventory_backup" - + " (player_name, " - + "contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience, " - + "save_time) " - + "VALUES " - + "(?,?,?,?,?,?,?)"); - ps.setString(1, name); - ps.setString(2, contents); - ps.setString(3, armor); - ps.setString(4, extra); - ps.setInt(5, level); - ps.setDouble(6, experience); - ps.setLong(7, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public String[] getPvpInventoryBackup(String playerName) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience " - + "FROM " - + "pvp_inventory_backup " - + "WHERE player_name=? " - + "ORDER BY save_time DESC " - + "LIMIT 1"); - ps.setString(1, playerName); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String[] result = new String[5]; - result[0] = rs.getString("contents_b64"); - result[1] = rs.getString("armor_b64"); - result[2] = rs.getString("extra_b64"); - result[3] = String.valueOf(rs.getInt("level")); - result[4] = String.valueOf(rs.getDouble("experience")); - rs.close(); - ps.close(); - return result; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - public void deletePvpInventoryBackup(String name) { - try { - PreparedStatement ps = db.prepareStatement( - "DELETE FROM pvp_inventory_backup " - + "WHERE player_name=?"); - ps.setString(1, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public boolean hasPvpInventoryBackup(String name) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT 1 FROM pvp_inventory_backup " - + "WHERE player_name=? LIMIT 1"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - boolean has = rs.next(); - rs.close(); - ps.close(); - return has; - } catch (SQLException e) { - e.printStackTrace(); - } - return false; - } - - public boolean hasInventoryBackup( - String name) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT id FROM " - + "inventory_backups " - + "WHERE player_name=? " - + "LIMIT 1"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - boolean has = rs.next(); - rs.close(); - ps.close(); - return has; - } catch (SQLException e) { - e.printStackTrace(); - return false; - } - } -// ==================== PVP区域 ==================== - - public void savePVPRegion(String name, - String world, - int x1, int y1, int z1, - int x2, int y2, int z2) { - try { - PreparedStatement ps = - db.prepareStatement( - "INSERT OR REPLACE " - + "INTO pvp_regions " - + "(name, world, " - + "x1, y1, z1, " - + "x2, y2, z2, " - + "create_time) " - + "VALUES " - + "(?,?,?,?,?,?,?,?,?)"); - ps.setString(1, name); - ps.setString(2, world); - ps.setInt(3, x1); - ps.setInt(4, y1); - ps.setInt(5, z1); - ps.setInt(6, x2); - ps.setInt(7, y2); - ps.setInt(8, z2); - ps.setLong(9, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public Map - getPVPRegion(String name) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT * FROM " - + "pvp_regions " - + "WHERE name=?"); - ps.setString(1, name); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs - .getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - rs.close(); - ps.close(); - return row; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - public List> - getAllPVPRegions() { - List> list = - new ArrayList<>(); - try { - Statement st = - db.createStatement(); - ResultSet rs = - st.executeQuery( - "SELECT * FROM " - + "pvp_regions"); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs - .getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - st.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - public void deletePVPRegion(String name) { - try { - PreparedStatement ps = - db.prepareStatement( - "DELETE FROM " - + "pvp_regions " - + "WHERE name=?"); - ps.setString(1, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - -// ==================== PVP击杀 ==================== - - public int getPVPKills( - String player, String region) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT kills FROM " - + "pvp_kills " - + "WHERE " - + "player_name=? " - + "AND region_name=?"); - ps.setString(1, player); - ps.setString(2, region); - ResultSet rs = ps.executeQuery(); - int kills = 0; - if (rs.next()) - kills = rs.getInt("kills"); - rs.close(); - ps.close(); - return kills; - } catch (SQLException e) { - return 0; - } - } - - public int getPVPDeaths( - String player, String region) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT deaths FROM " - + "pvp_kills " - + "WHERE " - + "player_name=? " - + "AND region_name=?"); - ps.setString(1, player); - ps.setString(2, region); - ResultSet rs = ps.executeQuery(); - int deaths = 0; - if (rs.next()) - deaths = rs.getInt("deaths"); - rs.close(); - ps.close(); - return deaths; - } catch (SQLException e) { - return 0; - } - } - - public void addPVPKill( - String player, String region) { - try { - PreparedStatement ps = - db.prepareStatement( - "INSERT INTO pvp_kills " - + "(player_name, " - + "region_name, " - + "kills, deaths) " - + "VALUES (?,?,1,0) " - + "ON CONFLICT" - + "(player_name, " - + "region_name) " - + "DO UPDATE SET " - + "kills=kills+1"); - ps.setString(1, player); - ps.setString(2, region); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public void addPVPDeath( - String player, String region) { - try { - PreparedStatement ps = - db.prepareStatement( - "INSERT INTO pvp_kills " - + "(player_name, " - + "region_name, " - + "kills, deaths) " - + "VALUES (?,?,0,1) " - + "ON CONFLICT" - + "(player_name, " - + "region_name) " - + "DO UPDATE SET " - + "deaths=deaths+1"); - ps.setString(1, player); - ps.setString(2, region); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public List> - getPVPKillTop(String region, int limit) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT player_name, " - + "kills, deaths " - + "FROM pvp_kills " - + "WHERE " - + "region_name=? " - + "AND kills>0 " - + "ORDER BY kills " - + "DESC LIMIT ?"); - ps.setString(1, region); - ps.setInt(2, limit); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - row.put("player_name", - rs.getString("player_name")); - row.put("kills", - rs.getInt("kills")); - row.put("deaths", - rs.getInt("deaths")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - 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 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 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> getAllArenaSessions() { - List> 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 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 getArenaTotalStats(String uuid) { - Map 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 getArenaSessionStats(String uuid, int sessionId) { - Map 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; - } - - // ==================== 安全报警 ==================== - - public void recordSecurityAlert( - String playerName, String ip) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT INTO security_alerts " - + "(player_name, alert_ip, " - + "alert_time, alert_date) " - + "VALUES (?,?,?,?)"); - ps.setString(1, playerName); - ps.setString(2, ip); - long now = System.currentTimeMillis(); - ps.setLong(3, now); - ps.setString(4, - new java.text.SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss") - .format(new java.util.Date( - now))); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - - } - - public List> - getUnnotifiedAlerts( - String playerName) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM security_alerts " - + "WHERE player_name=? " - + "AND notified=0 " - + "ORDER BY alert_time ASC"); - ps.setString(1, playerName); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - public void markAlertNotified(int alertId) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE security_alerts " - + "SET notified=1, " - + "notified_time=? " - + "WHERE id=?"); - ps.setLong(1, - System.currentTimeMillis()); - ps.setInt(2, alertId); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - - /** - * 获取玩家最近N条背包备份 - */ - public List> - getInventoryBackups(String name, - int limit) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT id, save_time " - + "FROM inv_backup_auto " - + "WHERE player_name=? " - + "ORDER BY save_time DESC " - + "LIMIT ?"); - ps.setString(1, name); - ps.setInt(2, limit); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - row.put("id", - rs.getInt("id")); - row.put("save_time", - rs.getLong("save_time")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - - /** - * 获取玩家登录备份列表(inventory_backups) - */ - public List> getLoginBackups( - String name, int limit) { - List> list = new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT id, save_time FROM inventory_backups " - + "WHERE player_name=? ORDER BY save_time DESC LIMIT ?"); - ps.setString(1, name); - ps.setInt(2, limit); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = new LinkedHashMap<>(); - row.put("id", rs.getInt("id")); - row.put("save_time", rs.getLong("save_time")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - /** - * 获取指定ID的登录备份数据 - */ - public String[] getLoginBackupById(int backupId) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT contents_b64, armor_b64, extra_b64, level, experience " - + "FROM inventory_backups WHERE id=?"); - ps.setInt(1, backupId); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String[] result = new String[5]; - result[0] = rs.getString("contents_b64"); - result[1] = rs.getString("armor_b64"); - result[2] = rs.getString("extra_b64"); - result[3] = String.valueOf(rs.getInt("level")); - result[4] = String.valueOf(rs.getDouble("experience")); - rs.close(); - ps.close(); - return result; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - /** - * 获取指定ID的备份数据 - */ - public String[] getInventoryBackupById( - int backupId) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience " - + "FROM inv_backup_auto " - + "WHERE id=?"); - ps.setInt(1, backupId); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String[] result = - new String[5]; - result[0] = rs.getString( - "contents_b64"); - result[1] = rs.getString( - "armor_b64"); - result[2] = rs.getString( - "extra_b64"); - result[3] = String.valueOf( - rs.getInt("level")); - result[4] = String.valueOf( - rs.getDouble("experience")); - rs.close(); - ps.close(); - return result; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - - /** - * 获取玩家最新一份自动背包备份(黑名单解除时返还用)。 - * @return String[]{contents_b64, armor_b64, extra_b64, level, experience},无备份返回 null - */ - public String[] getLatestAutoBackup(String playerName) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience " - + "FROM inv_backup_auto " - + "WHERE player_name=? " - + "ORDER BY save_time DESC " - + "LIMIT 1"); - ps.setString(1, playerName); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String[] result = new String[5]; - result[0] = rs.getString("contents_b64"); - result[1] = rs.getString("armor_b64"); - result[2] = rs.getString("extra_b64"); - result[3] = String.valueOf(rs.getInt("level")); - result[4] = String.valueOf(rs.getDouble("experience")); - rs.close(); - ps.close(); - return result; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - /** - * 获取玩家最新背包备份 - */ - public String[] getInventoryBackup(String playerName) { - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT contents_b64, " - + "armor_b64, " - + "extra_b64, " - + "level, " - + "experience " - + "FROM inventory_backups " - + "WHERE player_name=? " - + "ORDER BY save_time DESC " - + "LIMIT 1"); - ps.setString(1, playerName); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String[] result = new String[5]; - result[0] = rs.getString("contents_b64"); - result[1] = rs.getString("armor_b64"); - result[2] = rs.getString("extra_b64"); - result[3] = String.valueOf(rs.getInt("level")); - result[4] = String.valueOf(rs.getDouble("experience")); - rs.close(); - ps.close(); - return result; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - - // ==================== 工单操作 ==================== - - public int createTicket(String type, - String status, String priority, - String requester, String title, - String description, - int rewardAmount, - String targetPlayer) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT INTO tickets " - + "(type, status, priority, " - + "requester, title, " - + "description, " - + "reward_amount, " - + "target_player, " - + "create_time) " - + "VALUES (?,?,?,?,?,?,?,?,?)"); - ps.setString(1, type); - ps.setString(2, status); - ps.setString(3, priority); - ps.setString(4, requester); - ps.setString(5, title); - ps.setString(6, description); - ps.setInt(7, rewardAmount); - ps.setString(8, targetPlayer); - ps.setLong(9, - System.currentTimeMillis()); - ps.executeUpdate(); - ResultSet rs = - ps.getGeneratedKeys(); - int id = -1; - if (rs.next()) id = rs.getInt(1); - rs.close(); - ps.close(); - return id; - } catch (SQLException e) { - e.printStackTrace(); - return -1; - } - } - - public void updateTicketStatus(int ticketId, - String status) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE tickets SET status=?, " - + "update_time=? " - + "WHERE id=?"); - ps.setString(1, status); - ps.setLong(2, - System.currentTimeMillis()); - ps.setInt(3, ticketId); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public void assignTicket(int ticketId, - String provider) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE tickets SET " - + "assigned_to=?, " - + "status='processing', " - + "update_time=? " - + "WHERE id=?"); - ps.setString(1, provider); - ps.setLong(2, - System.currentTimeMillis()); - ps.setInt(3, ticketId); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public void completeTicket(int ticketId) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE tickets SET " - + "status='completed', " - + "complete_time=?, " - + "update_time=? " - + "WHERE id=?"); - long now = System.currentTimeMillis(); - ps.setLong(1, now); - ps.setLong(2, now); - ps.setInt(3, ticketId); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public void confirmTicket(int ticketId, - int score) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE tickets SET " - + "admin_confirmed=1, " - + "score=?, " - + "status='resolved', " - + "update_time=? " - + "WHERE id=?"); - ps.setInt(1, score); - ps.setLong(2, - System.currentTimeMillis()); - ps.setInt(3, ticketId); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - - public void rejectTicketComplete(int ticketId, - String reason) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE tickets SET " - + "status='rejected', " - + "reject_reason=?, " - + "update_time=? " - + "WHERE id=?"); - ps.setString(1, reason); - ps.setLong(2, - System.currentTimeMillis()); - ps.setInt(3, ticketId); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - /** - * 更新工单任意字段 - */ - public void updateTicketField(int ticketId, - String field, Object value) { - try { - PreparedStatement ps = db.prepareStatement( - "UPDATE tickets SET " + field - + "=? WHERE id=?"); - if (value instanceof Integer) - ps.setInt(1, (Integer) value); - else if (value instanceof Long) - ps.setLong(1, (Long) value); - else - ps.setString(1, - String.valueOf(value)); - ps.setInt(2, ticketId); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public Map getTicket( - int ticketId) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM tickets " - + "WHERE id=?"); - ps.setInt(1, ticketId); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - rs.close(); - ps.close(); - return row; - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - public List> - getTicketsByStatus(String status) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM tickets " - + "WHERE status=? " - + "ORDER BY create_time DESC"); - ps.setString(1, status); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - public void addTicketReply(int ticketId, - String sender, String message) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT INTO ticket_replies " - + "(ticket_id, sender, " - + "message, create_time) " - + "VALUES (?,?,?,?)"); - ps.setInt(1, ticketId); - ps.setString(2, sender); - ps.setString(3, message); - ps.setLong(4, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - // 更新工单状态为已回复 - updateTicketStatus(ticketId, - "replied"); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public List> - getTicketReplies(int ticketId) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM ticket_replies " - + "WHERE ticket_id=? " - + "ORDER BY create_time ASC"); - ps.setInt(1, ticketId); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs - .getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - /** - * 获取已回复但超时未互动的工单 - */ - public List> - getRepliedStaleTickets( - long timeoutMs) { - List> list = - new ArrayList<>(); - try { - long cutoff = - System.currentTimeMillis() - - timeoutMs; - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM tickets " - + "WHERE status='replied' " - + "AND update_time < ?"); - ps.setLong(1, cutoff); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs - .getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - public List> - getTicketsByPlayer(String playerName) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM tickets " - + "WHERE requester=? " - + "ORDER BY create_time DESC"); - ps.setString(1, playerName); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - public List> - getTicketsByProvider(String provider) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM tickets " - + "WHERE assigned_to=? " - + "AND (status='processing' " - + "OR status='replied') " - + "ORDER BY create_time DESC"); - ps.setString(1, provider); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs - .getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - public List> - getUnconfirmedCompleted( - long timeoutMs) { - List> list = - new ArrayList<>(); - try { - long cutoff = - System.currentTimeMillis() - - timeoutMs; - PreparedStatement ps = db.prepareStatement( - "SELECT * FROM tickets " - + "WHERE status='completed' " - + "AND admin_confirmed=0 " - + "AND complete_time < ?"); - ps.setLong(1, cutoff); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - // ==================== 服务商操作 ==================== - - public void addServiceProvider(String name, - String role) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT OR REPLACE INTO " - + "service_providers " - + "(player_name, role, " - + "active, join_time) " - + "VALUES (?,?,1,?)"); - ps.setString(1, name); - ps.setString(2, role); - ps.setLong(3, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public void removeServiceProvider( - String name) { - try { - PreparedStatement ps = db.prepareStatement( - "DELETE FROM service_providers " - + "WHERE player_name=?"); - ps.setString(1, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public boolean isServiceProvider( - String name) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT 1 FROM service_providers " - + "WHERE player_name=? " - + "AND active=1"); - ps.setString(1, name); - boolean r = ps.executeQuery().next(); - ps.close(); - return r; - } catch (SQLException e) { - return false; - } - } - - public List> - getAllServiceProviders() { - List> list = - new ArrayList<>(); - try { - Statement st = db.createStatement(); - ResultSet rs = st.executeQuery( - "SELECT * FROM service_providers " - + "WHERE active=1"); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - st.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - /** - * 全量服务商名单(含 active=0 的停用记录)。 - * 对账必须拿到与 PHP 完全相同的集合:只查 active=1 会让停用记录在 PHP 侧永远是 extra, - * 进而被「整表 DELETE」的推送一条条抹掉。 - */ - public List> - getAllServiceProvidersFull() { - List> list = - new ArrayList<>(); - try { - Statement st = db.createStatement(); - ResultSet rs = st.executeQuery( - "SELECT * FROM service_providers"); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - for (int i = 1; i <= rs.getMetaData() - .getColumnCount(); i++) { - row.put(rs.getMetaData() - .getColumnName(i), - rs.getObject(i)); - } - list.add(row); - } - rs.close(); - st.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - // ==================== 领地方块/容器记录 ==================== - - /** 记录领地方块破坏/放置 */ - public void recordLandBlock(String landName, int landId, - String world, int x, int y, int z, - String playerName, String action, - String material) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT INTO land_block_log" - + " (land_name, land_id, world, x, y, z," - + " player_name, action, material, time)" - + " VALUES (?,?,?,?,?,?,?,?,?,?)"); - ps.setString(1, landName); - ps.setInt(2, landId); - ps.setString(3, world); - ps.setInt(4, x); - ps.setInt(5, y); - ps.setInt(6, z); - ps.setString(7, playerName); - ps.setString(8, action); - ps.setString(9, material); - ps.setLong(10, System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - /** ★ 记录领地方块(含detail,用于告示牌内容等) */ - public void recordLandBlockWithDetail(String landName, int landId, - String world, int x, int y, int z, - String playerName, String action, - String material, String detail) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT INTO land_block_log" - + " (land_name, land_id, world, x, y, z," - + " player_name, action, material, detail, time)" - + " VALUES (?,?,?,?,?,?,?,?,?,?,?)"); - ps.setString(1, landName); - ps.setInt(2, landId); - ps.setString(3, world); - ps.setInt(4, x); - ps.setInt(5, y); - ps.setInt(6, z); - ps.setString(7, playerName); - ps.setString(8, action); - ps.setString(9, material); - ps.setString(10, detail); - ps.setLong(11, System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - /** 记录领地容器打开/拿取 */ - public void recordLandContainer(String landName, int landId, - String world, int x, int y, int z, - String playerName, String action, - String containerType, String detail, - int amount) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT INTO land_container_log" - + " (land_name, land_id, world, x, y, z," - + " player_name, action, container_type," - + " detail, amount, time)" - + " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); - ps.setString(1, landName); - ps.setInt(2, landId); - ps.setString(3, world); - ps.setInt(4, x); - ps.setInt(5, y); - ps.setInt(6, z); - ps.setString(7, playerName); - ps.setString(8, action); - ps.setString(9, containerType); - ps.setString(10, detail); - ps.setInt(11, amount); - ps.setLong(12, System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - /** 查询某坐标最近的方块记录(按时间倒序) */ - public List> getLandBlockLogAt( - String world, int x, int y, int z, int limit) { - List> list = new ArrayList<>(); - try { - PreparedStatement ps = db.prepareStatement( - "SELECT time, player_name, action, material, detail" - + " FROM land_block_log" - + " WHERE world=? AND x=? AND y=? AND z=?" - + " ORDER BY time DESC LIMIT ?"); - ps.setString(1, world); - ps.setInt(2, x); - ps.setInt(3, y); - ps.setInt(4, z); - ps.setInt(5, limit > 0 ? limit : 20); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - row.put("time", rs.getLong("time")); - row.put("player_name", - rs.getString("player_name")); - row.put("action", rs.getString("action")); - row.put("material", rs.getString("material")); - row.put("detail", rs.getString("detail")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - /** 查询某坐标指定天数前的方块记录(daysAgo=0今天,1昨天,-1所有) */ - public List> getLandBlockLogAt( - String world, int x, int y, int z, int limit, int daysAgo) { - List> list = new ArrayList<>(); - try { - StringBuilder sql = new StringBuilder( - "SELECT time, player_name, action, material, detail" - + " FROM land_block_log" - + " WHERE world=? AND x=? AND y=? AND z=?"); - if (daysAgo >= 0) { - java.util.Calendar cal = java.util.Calendar.getInstance(); - cal.set(java.util.Calendar.HOUR_OF_DAY, 0); - cal.set(java.util.Calendar.MINUTE, 0); - cal.set(java.util.Calendar.SECOND, 0); - cal.set(java.util.Calendar.MILLISECOND, 0); - cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); - long dayStart = cal.getTimeInMillis(); - cal.add(java.util.Calendar.DAY_OF_YEAR, 1); - long dayEnd = cal.getTimeInMillis(); - sql.append(" AND time >= ? AND time < ?"); - } - sql.append(" ORDER BY time DESC LIMIT ?"); - PreparedStatement ps = db.prepareStatement(sql.toString()); - ps.setString(1, world); - ps.setInt(2, x); - ps.setInt(3, y); - ps.setInt(4, z); - int idx = 5; - if (daysAgo >= 0) { - java.util.Calendar cal = java.util.Calendar.getInstance(); - cal.set(java.util.Calendar.HOUR_OF_DAY, 0); - cal.set(java.util.Calendar.MINUTE, 0); - cal.set(java.util.Calendar.SECOND, 0); - cal.set(java.util.Calendar.MILLISECOND, 0); - cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); - long dayStart = cal.getTimeInMillis(); - cal.add(java.util.Calendar.DAY_OF_YEAR, 1); - long dayEnd = cal.getTimeInMillis(); - ps.setLong(idx++, dayStart); - ps.setLong(idx++, dayEnd); - } - ps.setInt(idx, limit > 0 ? limit : 20); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = new LinkedHashMap<>(); - row.put("time", rs.getLong("time")); - row.put("player_name", rs.getString("player_name")); - row.put("action", rs.getString("action")); - row.put("material", rs.getString("material")); - row.put("detail", rs.getString("detail")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - /** 查询某坐标最近的容器记录(按时间倒序) */ - public List> getLandContainerLogAt( - String world, int x, int y, int z, int limit) { - return getLandContainerLogAt(world, x, y, z, limit, -1); - } - - /** 查询某坐标指定天数前的容器记录(daysAgo=0今天,1昨天,-1所有) */ - public List> getLandContainerLogAt( - String world, int x, int y, int z, int limit, int daysAgo) { - List> list = new ArrayList<>(); - try { - StringBuilder sql = new StringBuilder( - "SELECT time, player_name, action, container_type," - + " detail, amount" - + " FROM land_container_log" - + " WHERE world=? AND x=? AND y=? AND z=?"); - if (daysAgo >= 0) { - // 计算当天起始和结束毫秒时间戳 - java.util.Calendar cal = java.util.Calendar.getInstance(); - cal.set(java.util.Calendar.HOUR_OF_DAY, 0); - cal.set(java.util.Calendar.MINUTE, 0); - cal.set(java.util.Calendar.SECOND, 0); - cal.set(java.util.Calendar.MILLISECOND, 0); - cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); - long dayStart = cal.getTimeInMillis(); - cal.add(java.util.Calendar.DAY_OF_YEAR, 1); - long dayEnd = cal.getTimeInMillis(); - sql.append(" AND time >= ? AND time < ?"); - } - sql.append(" ORDER BY time DESC LIMIT ?"); - PreparedStatement ps = db.prepareStatement(sql.toString()); - ps.setString(1, world); - ps.setInt(2, x); - ps.setInt(3, y); - ps.setInt(4, z); - int idx = 5; - if (daysAgo >= 0) { - java.util.Calendar cal = java.util.Calendar.getInstance(); - cal.set(java.util.Calendar.HOUR_OF_DAY, 0); - cal.set(java.util.Calendar.MINUTE, 0); - cal.set(java.util.Calendar.SECOND, 0); - cal.set(java.util.Calendar.MILLISECOND, 0); - cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); - long dayStart = cal.getTimeInMillis(); - cal.add(java.util.Calendar.DAY_OF_YEAR, 1); - long dayEnd = cal.getTimeInMillis(); - ps.setLong(idx++, dayStart); - ps.setLong(idx++, dayEnd); - } - ps.setInt(idx, limit > 0 ? limit : 20); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - row.put("time", rs.getLong("time")); - row.put("player_name", - rs.getString("player_name")); - row.put("action", rs.getString("action")); - row.put("container_type", - rs.getString("container_type")); - row.put("detail", rs.getString("detail")); - row.put("amount", rs.getInt("amount")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return list; - } - - /** 清理超过 cutoff(毫秒时间戳)的领地记录,返回删除行数 */ - public int pruneLandRecordsOlderThan(long cutoff) { - int deleted = 0; - try { - PreparedStatement ps1 = db.prepareStatement( - "DELETE FROM land_block_log WHERE time < ?"); - ps1.setLong(1, cutoff); - deleted += ps1.executeUpdate(); - ps1.close(); - - PreparedStatement ps2 = db.prepareStatement( - "DELETE FROM land_container_log WHERE time < ?"); - ps2.setLong(1, cutoff); - deleted += ps2.executeUpdate(); - ps2.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return deleted; - } - - // ==================== 背包自动滚动备份(每小时 0/15/30/45 分准点定时)==================== - - /** - * 保存背包自动备份 —— 追加写入,每位玩家仅保留最近 3 份。 - * 跳过 PVP 竞技场/测试场的判断由调用方(InventoryAutoBackup)负责。 - */ - public void saveAutoBackup(String name, - String contents, String armor, - String extra, int level, - double experience) { - try { - try (PreparedStatement ps = db.prepareStatement( - "INSERT INTO inv_backup_auto " - + "(player_name, contents_b64, armor_b64, " - + "extra_b64, level, experience, save_time) " - + "VALUES (?,?,?,?,?,?,?)")) { - ps.setString(1, name); - ps.setString(2, contents); - ps.setString(3, armor); - ps.setString(4, extra); - ps.setInt(5, level); - ps.setDouble(6, experience); - ps.setLong(7, System.currentTimeMillis()); - ps.executeUpdate(); - } - - // 保留最近 3 份 —— 先查最新 3 个 ID,再删其余 - List keep = new ArrayList<>(); - try (PreparedStatement sel = db.prepareStatement( - "SELECT id FROM inv_backup_auto " - + "WHERE player_name=? " - + "ORDER BY save_time DESC LIMIT 3")) { - sel.setString(1, name); - try (ResultSet rs = sel.executeQuery()) { - while (rs.next()) keep.add(rs.getInt("id")); - } - } - - if (keep.size() == 3) { - StringBuilder in = new StringBuilder(); - for (int i = 0; i < keep.size(); i++) { - if (i > 0) in.append(","); - in.append(keep.get(i)); - } - try (PreparedStatement del = db.prepareStatement( - "DELETE FROM inv_backup_auto " - + "WHERE player_name=? AND id NOT IN (" - + in + ")")) { - del.setString(1, name); - int removed = del.executeUpdate(); - if (removed > 0) { - logger.info("[自动备份] " + name + " 滚动清理 " + removed + " 份旧备份"); - } - } - } - } catch (SQLException e) { - logger.warning("[自动备份] 保存/清理失败 (" + name + "): " + e.getMessage()); - } - } - - public Connection getDb() { - return db; - } -} +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.logging.Logger; + +public class DatabaseManager { + + private Connection db; + private final File dataFolder; + private final Logger logger; + + public DatabaseManager(File dataFolder) { + this.dataFolder = dataFolder; + this.logger = Logger.getLogger("Sdf1_login"); + } + + // ==================== 初始化 ==================== + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + // ★ 2026-10-08 归位:login.db 创建即放入「登录凭证/」; + // 根目录存量由 Main.migrateRootDataFiles() 启动最早期整组迁移 + // (.db + -shm + -wal 同进同退,严禁人工移动)。 + File dbDir = new File(dataFolder, "登录凭证"); + dbDir.mkdirs(); + File dbFile = new File(dbDir, "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + // ★ 二十二批:busy_timeout —— WAL 下读写仍会与 + // db-worker/长事务争锁,无超时会让主线程 + // sqlite3_step 无限等待(watchdog 卡点之一), + // 5秒等锁后报 SQLITE_BUSY 快速失败。 + st.execute("PRAGMA busy_timeout=5000"); + + // 用户表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "users (" + + "player_name TEXT PRIMARY KEY," + + "password_hash TEXT NOT NULL," + + "password_salt TEXT NOT NULL," + + "email TEXT DEFAULT ''," + + "register_time INTEGER DEFAULT 0," + + "last_login_time INTEGER DEFAULT 0," + + "total_online_time INTEGER DEFAULT 0," + + "last_online_check INTEGER DEFAULT 0," + + "is_logged_in INTEGER DEFAULT 0," + + "blocks_broken INTEGER DEFAULT 0," + + "blocks_placed INTEGER DEFAULT 0," + + "gift_stage INTEGER DEFAULT 0," + + "gift_claimed TEXT DEFAULT ''," + + "invite_code TEXT DEFAULT ''," + + "invited_by TEXT DEFAULT ''," + + "invited_at INTEGER DEFAULT 0," + + "monthly_invite_count INTEGER DEFAULT 0," + + "invite_month TEXT DEFAULT ''," + + "points INTEGER DEFAULT 0," + + "checkin_streak INTEGER DEFAULT 0," + + "last_checkin_date TEXT DEFAULT ''," + + "total_checkin_days INTEGER DEFAULT 0," + + "temp_password TEXT DEFAULT ''," + + "temp_pw_expire INTEGER DEFAULT 0," + + "temp_pw_used INTEGER DEFAULT 0," + + "ip_address TEXT DEFAULT ''," + + "tasks_completed TEXT DEFAULT ''," + + "premium_uuid TEXT DEFAULT ''," + + "register_ip TEXT DEFAULT ''," + + "register_type TEXT DEFAULT 'manual'," + + "backcheck_dates TEXT DEFAULT ''" + + ")"); + + + // 安全添加 users 缺失列 + safeAdd(st, "tasks_completed", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_password", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_pw_expire", + "INTEGER DEFAULT 0"); + safeAdd(st, "temp_pw_used", + "INTEGER DEFAULT 0"); + safeAdd(st, "ip_address", + "TEXT DEFAULT ''"); + safeAdd(st, "premium_uuid", + "TEXT DEFAULT ''"); + safeAdd(st, "register_ip", + "TEXT DEFAULT ''"); + safeAdd(st, "register_type", + "TEXT DEFAULT 'manual'"); + safeAdd(st, "login_streak", + "INTEGER DEFAULT 0"); + safeAdd(st, "last_login_date", + "TEXT DEFAULT ''"); + safeAdd(st, "last_login_ip", + "TEXT DEFAULT ''"); + // ★ 第九批:邀请绑定时间(毫秒时间戳), + // 供「我的邀请码」GUI 显示邀请成功时间 + safeAdd(st, "invited_at", + "INTEGER DEFAULT 0"); + // UI偏好:1=CLI(默认), 0=GUI + safeAdd(st, "ui_mode", + "INTEGER DEFAULT 1"); + // ===== 风控 / 封禁相关列 ===== + // IP归属(省+市+区),用于异地登录检测 + safeAdd(st, "last_login_location", + "TEXT DEFAULT ''"); + // 账号冻结(玩家主动冻结防被盗):0=正常 1=已冻结 + safeAdd(st, "frozen", + "INTEGER DEFAULT 0"); + // 冻结校验 token(邮件链接防猜测) + safeAdd(st, "freeze_token", + "TEXT DEFAULT ''"); + // 禁言到期时间戳(毫秒),0=未禁言 + safeAdd(st, "muted_until", + "INTEGER DEFAULT 0"); + // 禁言理由 + safeAdd(st, "mute_reason", + "TEXT DEFAULT ''"); + // ★ 正版OAuth登录IP(用于异地登录风控) + safeAdd(st, "last_oauth_ip", + "TEXT DEFAULT ''"); + // ===== 2FA 二次验证(TOTP,仅Java本地校验)===== + safeAdd(st, "twofa_secret", + "TEXT DEFAULT ''"); + safeAdd(st, "twofa_enabled", + "INTEGER DEFAULT 0"); + safeAdd(st, "twofa_pending_secret", + "TEXT DEFAULT ''"); + safeAdd(st, "twofa_pending_at", + "INTEGER DEFAULT 0"); + // ★ 第47批:绑定时间(秒)——与网页端 web_account_2fa.bound_at 对齐, + // 双向同步冲突时"取时间新者"用 + safeAdd(st, "twofa_bound_at", + "INTEGER DEFAULT 0"); + // ===== 2FA 恢复代码(SHA-256 逗号连接存库,明文只发邮件,一次性有效)===== + safeAdd(st, "twofa_recovery_codes", + "TEXT DEFAULT ''"); + + // ★ 一次性自愈:历史版本 setField(null) 曾把 temp_password + // 写成字面量 'null',会让 credentials 指纹永久不一致、 + // 对账每轮补推。启动时统一清回 SQL NULL。 + try { + st.executeUpdate("UPDATE users SET temp_password=NULL " + + "WHERE temp_password='null'"); + } catch (SQLException ignored) { + } + + // ★ 第52批:存量脏凭证一次性清洗——PHP json_encode 把 base64 中的 + // / 转义成 \/,历史解析层漏还原曾把反斜杠写进库(hash 脏 → 密码恒错)。 + // base64 合法字符永不含反斜杠(char92),删除即还原正确值;WHERE 带 + // instr 条件,无脏值时为近似 no-op,可每次启动安全重跑。 + try { + st.executeUpdate("UPDATE users SET password_hash=REPLACE(password_hash, char(92), '') " + + "WHERE instr(password_hash, char(92))>0"); + st.executeUpdate("UPDATE users SET password_salt=REPLACE(password_salt, char(92), '') " + + "WHERE instr(password_salt, char(92))>0"); + st.executeUpdate("UPDATE users SET temp_password=REPLACE(temp_password, char(92), '') " + + "WHERE instr(temp_password, char(92))>0"); + } catch (SQLException ignored) { + } + + // ===== 背包备份表:安全迁移 ===== + // 检查旧表是否存在(用 inventory_data 列判断) + boolean hasOldTable = false; + try { + ResultSet rs = st.executeQuery( + "SELECT inventory_data " + + "FROM inventory_backups " + + "LIMIT 1"); + hasOldTable = true; + rs.close(); + } catch (SQLException e) { + hasOldTable = false; + } + + if (hasOldTable) { + // 旧表存在,迁移数据到新表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups_new (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + // 迁移旧数据 + try { + st.execute("INSERT INTO " + + "inventory_backups_new " + + "(player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience) " + + "SELECT player_name, " + + "inventory_data, " + + "armor_data, " + + "extra_data, " + + "player_level, " + + "player_exp " + + "FROM inventory_backups"); + } catch (SQLException ignored) { + } + st.execute("DROP TABLE " + + "inventory_backups"); + st.execute("ALTER TABLE " + + "inventory_backups_new " + + "RENAME TO " + + "inventory_backups"); + } else { + // 无旧表或已迁移,直接创建 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + } + + // PVP 背包备份表(独立表:避免与登录背包系统的 inventory_backups 互相覆盖导致原始背包丢失) + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_inventory_backup (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + + // 安全报警表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "security_alerts (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "alert_ip TEXT NOT NULL," + + "alert_time INTEGER NOT NULL," + + "alert_date TEXT NOT NULL," + + "notified INTEGER DEFAULT 0," + + "notified_time INTEGER DEFAULT 0)"); + // PVP区域表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT NOT NULL UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER DEFAULT 0" + + ")"); + +// PVP击杀总榜表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT NOT NULL," + + "region_name TEXT NOT NULL," + + "kills INTEGER DEFAULT 0," + + "deaths INTEGER DEFAULT 0," + + "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 " + + "menu_icons (" + + "player_name TEXT PRIMARY KEY," + + "icon_b64 TEXT DEFAULT ''," + + "item_name TEXT DEFAULT ''," + + "save_time INTEGER DEFAULT 0" + + ")"); + + // 工单表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "tickets (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "type TEXT NOT NULL," + + "status TEXT DEFAULT 'pending'," + + "priority TEXT DEFAULT 'normal'," + + "requester TEXT NOT NULL," + + "assigned_to TEXT DEFAULT ''," + + "title TEXT DEFAULT ''," + + "description TEXT DEFAULT ''," + + "reward_amount INTEGER DEFAULT 0," + + "score INTEGER DEFAULT 0," + + "target_player TEXT DEFAULT ''," + + "create_time INTEGER NOT NULL," + + "update_time INTEGER DEFAULT 0," + + "complete_time INTEGER DEFAULT 0," + + "admin_confirmed INTEGER DEFAULT 0," + + "reject_reason TEXT DEFAULT ''" + + ")"); + + // 工单回复表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "ticket_replies (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "ticket_id INTEGER NOT NULL," + + "sender TEXT NOT NULL," + + "message TEXT NOT NULL," + + "create_time INTEGER NOT NULL" + + ")"); + + // 服务商表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "service_providers (" + + "player_name TEXT PRIMARY KEY," + + "role TEXT DEFAULT 'waiter'," + + "active INTEGER DEFAULT 1," + + "join_time INTEGER DEFAULT 0" + + ")"); + // 垃圾回收站物品表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "items (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "item_data TEXT DEFAULT ''," + + "amount INTEGER DEFAULT 1," + + "create_time INTEGER DEFAULT 0" + + ")"); + safeAdd(st, "menu_snowball", + "INTEGER DEFAULT 1"); + + // Home传送点表 — 只在不存在时创建,不删除已有数据 + st.execute("CREATE TABLE IF NOT EXISTS homes (" + + "player_name TEXT NOT NULL," + + "home_name TEXT NOT NULL," + + "world TEXT NOT NULL," + + "x REAL NOT NULL," + + "y REAL NOT NULL," + + "z REAL NOT NULL," + + "yaw REAL DEFAULT 0," + + "pitch REAL DEFAULT 0," + + "created_at INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, home_name)" + + ")"); + safeAdd(st, "menu_snowball", + "INTEGER DEFAULT 1"); + + // 领地方块记录表:记录领地内谁破坏/放置了什么方块(7天滚动) + st.execute("CREATE TABLE IF NOT EXISTS " + + "land_block_log (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "land_name TEXT," + + "land_id INTEGER," + + "world TEXT," + + "x INTEGER," + + "y INTEGER," + + "z INTEGER," + + "player_name TEXT," + + "action TEXT," + + "material TEXT," + + "detail TEXT," + + "time INTEGER" + + ")"); + + // ★ 新增detail列(已有数据库兼容) + try { + st.executeUpdate("ALTER TABLE land_block_log ADD COLUMN detail TEXT"); + } catch (SQLException ignored) {} + + // 领地容器记录表:记录领地内容器的打开/拿取(7天滚动) + st.execute("CREATE TABLE IF NOT EXISTS " + + "land_container_log (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "land_name TEXT," + + "land_id INTEGER," + + "world TEXT," + + "x INTEGER," + + "y INTEGER," + + "z INTEGER," + + "player_name TEXT," + + "action TEXT," + + "container_type TEXT," + + "detail TEXT," + + "amount INTEGER," + + "time INTEGER" + + ")"); + + // 背包自动滚动备份表(每小时 0/15/30/45 分准点定时):每位玩家保留最近 3 份 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inv_backup_auto (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + + // ★ 充值回执镜像表(2026-10-06,Bug「清空PHP后充值记录回不来」的修复) + // 充值流水一式两份:PHP 持 web_transactions(type=recharge) 一份, + // Java 持这里一份。每轮全量同步时双方点差集、多退少补: + // PHP 缺 → Java 补推重建流水(只补记录,绝不重复加钱) + // Java 缺 → 收回来补齐基线(同样只补记录) + // tx_id = PHP web_transactions.id,作为两边共同的主键。 + st.execute("CREATE TABLE IF NOT EXISTS " + + "web_tx_receipts (" + + "tx_id INTEGER PRIMARY KEY," + + "player_name TEXT NOT NULL DEFAULT ''," + + "type TEXT NOT NULL DEFAULT ''," + + "amount INTEGER NOT NULL DEFAULT 0," + + "detail TEXT NOT NULL DEFAULT ''," + + "created_at INTEGER NOT NULL DEFAULT 0" + + ")"); + + // 注:传送请求不再落库,仅驻留内存(配置项才落库),故不再建 teleport_requests 表 + st.close(); + logger.info("[Sdf1_login] 数据库初始化完成"); + } catch (Exception e) { + throw new RuntimeException( + "[Sdf1_login] DB初始化失败: " + + e.getMessage(), e); + } + } + + + private void safeAdd(Statement st, + String col, String def) { + try { + st.executeUpdate("ALTER TABLE users " + + "ADD COLUMN " + col + " " + def); + } catch (SQLException ignored) { + } + } + + // ==================== 自动续费偏好 ==================== + + /** 获取玩家的自动续费偏好 */ + public boolean getPlayerAutoRenew(String player, String group) { + try { + Statement st = db.createStatement(); + st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))"); + String sql = "SELECT auto_renew FROM player_auto_renew WHERE player_name = ? AND group_name = ?"; + PreparedStatement ps = db.prepareStatement(sql); + ps.setString(1, player); + ps.setString(2, group); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getInt("auto_renew") == 1; + } catch (Exception ignored) {} + return false; + } + + /** 设置玩家的自动续费偏好 */ + public void setPlayerAutoRenew(String player, String group, boolean value) { + try { + Statement st = db.createStatement(); + st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))"); + String sql = "INSERT OR REPLACE INTO player_auto_renew (player_name, group_name, auto_renew) VALUES (?, ?, ?)"; + PreparedStatement ps = db.prepareStatement(sql); + ps.setString(1, player); + ps.setString(2, group); + ps.setInt(3, value ? 1 : 0); + ps.executeUpdate(); + } catch (Exception ignored) {} + } + + /** 获取玩家所有自动续费开启的组 */ + public List> getAutoRenewGroups(String player) { + List> result = new ArrayList<>(); + try { + Statement st = db.createStatement(); + st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))"); + String sql = "SELECT group_name FROM player_auto_renew WHERE player_name = ? AND auto_renew = 1"; + PreparedStatement ps = db.prepareStatement(sql); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = new HashMap<>(); + row.put("group_name", rs.getString("group_name")); + result.add(row); + } + } catch (Exception ignored) {} + return result; + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } + + /** 获取数据库连接,连接已关闭时自动重连。★ 第58批修复:避免 SN/信誉同步等模块拿到已关闭连接报 "database connection closed"。 */ + public Connection getConnection() { + try { + if (db == null || db.isClosed()) { + File dbDir = new File(dataFolder, "登录凭证"); + dbDir.mkdirs(); + File dbFile = new File(dbDir, "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("PRAGMA busy_timeout=5000"); + st.close(); + logger.warning("[Sdf1_login] 数据库连接已自动重建"); + } + } catch (Exception e) { + logger.severe("[Sdf1_login] 数据库自动重连失败: " + e.getMessage()); + } + return db; + } + + // ==================== 充值回执镜像(web_tx_receipts,2026-10-06) ==================== + // 充值流水一式两份:PHP 持 web_transactions(type=recharge),Java 持本表。 + // 两边按 tx_id 点差集、多退少补,任何一边被清空都能从另一边补回来。 + + /** 写入/更新一条回执(主键 tx_id = PHP web_transactions.id) */ + public void upsertWebTxReceipt(long txId, String playerName, String type, + int amount, String detail, long createdAt) { + try (java.sql.PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO web_tx_receipts " + + "(tx_id, player_name, type, amount, detail, created_at) " + + "VALUES (?, ?, ?, ?, ?, ?)")) { + ps.setLong(1, txId); + ps.setString(2, playerName == null ? "" : playerName); + ps.setString(3, type == null ? "" : type); + ps.setInt(4, amount); + ps.setString(5, detail == null ? "" : detail); + ps.setLong(6, createdAt); + ps.executeUpdate(); + } catch (Exception e) { + logger.warning("[回执镜像] 写入失败 tx_id=" + txId + ": " + e.getMessage()); + } + } + + /** 取全部回执(对账用,字段名与 PHP web_transactions 对齐) */ + public List> getAllWebTxReceipts() { + List> out = new ArrayList<>(); + try (java.sql.PreparedStatement ps = db.prepareStatement( + "SELECT tx_id, player_name, type, amount, detail, created_at " + + "FROM web_tx_receipts ORDER BY tx_id ASC"); + java.sql.ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("id", rs.getLong("tx_id")); + row.put("player_name", rs.getString("player_name")); + row.put("type", rs.getString("type")); + row.put("amount", rs.getInt("amount")); + row.put("detail", rs.getString("detail")); + row.put("created_at", rs.getLong("created_at")); + out.add(row); + } + } catch (Exception e) { + logger.warning("[回执镜像] 读取失败: " + e.getMessage()); + } + return out; + } + + /** 是否已有某条回执 */ + public boolean webTxReceiptExists(long txId) { + try (java.sql.PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM web_tx_receipts WHERE tx_id = ? LIMIT 1")) { + ps.setLong(1, txId); + try (java.sql.ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } catch (Exception e) { + return false; + } + } + + // ==================== UI偏好 ==================== + + /** + * 获取玩家UI偏好 + * @return 1=CLI(默认), 0=GUI + */ + public int getUiMode(String playerName) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT ui_mode FROM users WHERE player_name=?"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + int mode = 1; // 默认CLI + if (rs.next()) { + mode = rs.getInt("ui_mode"); + } + rs.close(); + ps.close(); + return mode; + } catch (SQLException e) { + return 1; // 默认CLI + } + } + + /** + * 设置玩家UI偏好 + * @param mode 1=CLI, 0=GUI + */ + public void setUiMode(String playerName, int mode) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET ui_mode=? WHERE player_name=?"); + ps.setInt(1, mode); + ps.setString(2, playerName); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + // 静默 + } + } +// ==================== 菜单图标 ==================== + + public void saveMenuIcon(String name, + String iconB64, String itemName) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO menu_icons " + + "(player_name, icon_b64, " + + "item_name, save_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, iconB64); + ps.setString(3, itemName); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT icon_b64 FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + String b64 = null; + if (rs.next()) { + b64 = rs.getString("icon_b64"); + } + rs.close(); + ps.close(); + return b64; + } catch (SQLException e) { + return null; + } + } + + public void deleteMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM menu_icons " + + "WHERE player_name=? " + + "AND icon_b64 IS NOT NULL " + + "AND icon_b64 != '' " + + "LIMIT 1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + // ==================== 用户操作 ==================== + + public boolean userExists(String name) { + try { + PreparedStatement ps = getConnection().prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + /** + * ★ 封禁防护用:大小写不敏感的注册校验。 + * 返回 false 仅表示「确认未注册」;SQL 异常时返回 true 并告警 + * (宁可放行,也不因 DB 抖动误拒正常的封禁操作)。 + */ + public boolean userExistsIgnoreCase(String name) { + if (name == null || name.isEmpty()) return false; + try { + PreparedStatement ps = getConnection().prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=? " + + "COLLATE NOCASE"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + logger.warning("[封禁防护] 注册校验查询失败(放行): " + + e.getMessage()); + return true; + } + } + + /** + * ★ OP白名单授权校验用(2026-10-11):与 userExistsIgnoreCase 相反的失败语义—— + * SQL 异常一律返回 false(保守拒绝)。opwl add/addtime 宁可让运维改 json 兜底, + * 也绝不因 DB 抖动把临时 OP 发给未注册玩家。 + */ + public boolean userExistsStrict(String name) { + if (name == null || name.isEmpty()) return false; + try { + PreparedStatement ps = getConnection().prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=? " + + "COLLATE NOCASE"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + logger.warning("[OP白名单] 注册校验查询失败(拒绝): " + + e.getMessage()); + return false; + } + } + + /** + * ★ 第52批:base64 凭证白名单清洗。 + * PHP json_encode 默认把 base64 中的 / 转义成 \/,历史解析层漏还原 + * 会把反斜杠写进库(hash 脏值 → 游戏/网页两端密码恒错)。 + * base64 合法字符永不含反斜杠,先还原 \/ 再按白名单清洗即得正确值; + * 写库与比对两侧同款清洗,保证对称。 + */ + public static String b64Clean(String v) { + if (v == null) return null; + return v.replace("\\/", "/").replaceAll("[^A-Za-z0-9+/=]", ""); + } + + public void createUser(String name, String hash, + String salt) { + hash = b64Clean(hash); + salt = b64Clean(salt); + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO users " + + "(player_name, " + + "password_hash, " + + "password_salt, " + + "register_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, hash); + ps.setString(3, salt); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 更新玩家密码凭证(salt + hash),用于Web重新注册时同步 + */ + public void updatePassword(String name, String hash, String salt) { + hash = b64Clean(hash); + salt = b64Clean(salt); + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET password_hash = ?, password_salt = ? WHERE player_name = ?"); + ps.setString(1, hash); + ps.setString(2, salt); + ps.setString(3, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ========== 临时密码管理 ========== + + public void clearTempPassword(String name) { + setField(name, "temp_password", null); + setField(name, "temp_pw_expire", 0); + setField(name, "temp_pw_used", 1); + } + + public void clearAllTempPasswords() { + try { + Statement st = db.createStatement(); + st.executeUpdate( + "UPDATE users SET " + + "temp_password=NULL," + + "temp_pw_expire=0," + + "temp_pw_used=1 " + + "WHERE temp_password " + + "IS NOT NULL"); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getPlayerIPByName( + String name) { + Object ip = getField(name, "ip"); + return ip != null + ? String.valueOf(ip) : null; + } + + /** + * 验证密码(仅主密码) + * @return true if matched + */ + public boolean checkPassword(String name, String hash) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String mainHash = + rs.getString("password_hash"); + rs.close(); + ps.close(); + // ★ 第52批:比对前双侧清洗(存量脏 hash 容错,见 b64Clean) + return b64Clean(hash).equals(b64Clean(mainHash)); + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + /** + * 验证密码(主密码优先,不通过再查临时密码) + * 注意: 传入的 hash 必须是主密码的 hash + * @return "main" | "temp" | null + */ + public String checkPasswordWithFallback(String name, String mainHash) { + // 先检查主密码 + if (checkPassword(name, mainHash)) { + return "main"; + } + // 主密码不匹配,检查临时密码 + try { + PreparedStatement ps = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_expire, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String tempHash = rs.getString("temp_password"); + long expire = rs.getLong("temp_pw_expire"); + int used = rs.getInt("temp_pw_used"); + rs.close(); + ps.close(); + if (tempHash != null + && !tempHash.isEmpty() + && b64Clean(mainHash).equals(b64Clean(tempHash)) + && System.currentTimeMillis() < expire + && used != 1) { + return "temp"; + } + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + /** + * ★ 修改密码专用校验:同时校验【真实密码】与【临时密码】。 + * + * 与登录校验 (checkPasswordWithFallback) 的区别: + * 临时密码的有效期只对"登录"生效,对"改密"不生效。 + * 原因:玩家用临时密码登录成功后,若忘记真实密码, + * 一旦临时密码 5 分钟过期,就再也没有任何途径改密, + * 账号被永久锁死。改密本身就是"持有旧凭据即可换新凭据", + * 因此这里只要求临时密码【存在且未被使用过】, + * 不再要求它仍在登录有效期内。 + * + * @return "main" = 匹配真实密码 | "temp" = 匹配临时密码 | null = 都不匹配 + */ + public String checkPasswordForChange(String name, String hash) { + // 1. 先校验真实密码 + if (checkPassword(name, hash)) { + return "main"; + } + // 2. 再校验临时密码(不要求未过期,但必须未被消耗) + try { + PreparedStatement ps = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String tempHash = rs.getString("temp_password"); + int used = rs.getInt("temp_pw_used"); + rs.close(); + ps.close(); + if (tempHash != null + && !tempHash.isEmpty() + && b64Clean(hash).equals(b64Clean(tempHash)) + && used != 1) { + return "temp"; + } + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public Map getUser(String name) { + Map r = + new LinkedHashMap<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + r.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return r; + } + + public void setField(String name, String field, + Object val) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET " + field + + "=? " + + "WHERE player_name=?"); + if (val instanceof Integer) + ps.setInt(1, (Integer) val); + else if (val instanceof Long) + ps.setLong(1, (Long) val); + // ★ null 必须写 SQL NULL:走 String.valueOf(null) 会把 + // temp_password 写成字面量 'null',两端指纹永不一致, + // 对账每轮恒定补推(2026-10-07 测试服 credentials 补推1 根因) + else if (val == null) + ps.setNull(1, java.sql.Types.VARCHAR); + else + ps.setString(1, + String.valueOf(val)); + ps.setString(2, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** 读取当前仍有效的禁言列表(muted_until > now) */ + public List getActiveMutes() { + List list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, muted_until, " + + "mute_reason FROM users " + + "WHERE muted_until > ?"); + ps.setLong(1, + System.currentTimeMillis()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Object[] row = new Object[3]; + row[0] = rs.getString( + "player_name"); + row[1] = rs.getLong( + "muted_until"); + row[2] = rs.getString( + "mute_reason"); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Object getField(String name, + String field) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT " + field + + " FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + Object val = rs.next() + ? rs.getObject(1) : null; + rs.close(); + ps.close(); + return val; + } catch (SQLException e) { + return null; + } + } + + public boolean isUsingTempPassword(String name) { + String h = (String) getField(name, + "temp_password"); + return h != null && !h.isEmpty(); + } + + public void setLoggedIn(String name, + boolean val) { + setField(name, "is_logged_in", + val ? 1 : 0); + } + + public void addPoints(String name, int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + setField(name, "points", curVal + amount); + } + + public boolean deductPoints(String name, + int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + if (curVal < amount) return false; + setField(name, "points", + curVal - amount); + return true; + } + public List> + getAllUsers() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM users"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + public List> + getTicketsByProviderAll( + String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * ★ 精准大小写匹配:TEST 与 test 是两个独立账号, + * 所有"按名字查/删"的入口都必须区分大小写。 + * @return 完全相等(区分大小写)时返回该玩家名,否则返回 null + */ + public String findExactUserName(String name) { + if (name == null || name.isEmpty()) return null; + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + String found = null; + while (rs.next()) { + String row = rs.getString(1); + if (name.equals(row)) { // 精准匹配,拒绝大小写兜底 + found = row; + break; + } + } + rs.close(); + ps.close(); + return found; + } catch (SQLException e) { + return null; + } + } + + /** + * 查找"只差大小写"的同名账号(仅用于提示/告警,绝不用于删除)。 + * @return 找到则返回库里的真实玩家名,否则返回 null + */ + public String findSimilarUserNameIgnoreCase(String name) { + if (name == null || name.isEmpty()) return null; + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name FROM users " + + "WHERE LOWER(player_name)=LOWER(?)"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + String found = null; + while (rs.next()) { + String row = rs.getString(1); + if (!name.equals(row)) { // 只要大小写不同 + found = row; + break; + } + } + rs.close(); + ps.close(); + return found; + } catch (SQLException e) { + return null; + } + } + + /** + * 精准删除账号(区分大小写)。 + *

+ * TEST 与 test 是两个人:只删 player_name 完全相等的那一条; + * 若只有大小写不同的账号存在,则拒绝删除并记录告警。 + * + * @return 实际删除的行数(0 = 未删除) + */ + public int deleteUser(String name) { + if (name == null || name.isEmpty()) return 0; + if (findExactUserName(name) == null) { + String twin = findSimilarUserNameIgnoreCase(name); + if (twin != null) { + System.out.println("[Sdf1_login] 删除账号拒绝:" + + name + " 不存在,但存在大小写不同的账号 " + + twin + "(已按精准匹配跳过,未误删)"); + } + return 0; + } + int deleted = 0; + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + deleted = ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return deleted; + } + + public void recordIP(String name, String ip) { + if (ip != null && !ip.isEmpty()) { + setField(name, "ip_address", ip); + // ★ 同时更新最后登录IP + setField(name, "last_login_ip", ip); + } + } + + // ==================== IP查询 ==================== + + /** + * 统计指定IP注册的账号数量 + */ + public int countIPAccounts(String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address=?"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count; + } catch (SQLException e) { + return 0; + } + } + + /** + * 获取指定IP下所有账号详情 + */ + public List> + getAccountDetailsByIP(String ip) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, " + + "register_time " + + "FROM users " + + "WHERE ip_address=? " + + "ORDER BY register_time"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("register_time", + rs.getLong("register_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 背包备份 ==================== + + /** + * 保存背包主备份 —— 追加写入,每位玩家仅保留最近 3 份(更早的自动清理)。 + * ★ 2026-07-09 改版:原为「DELETE 全部再 INSERT 单份」,会导致 PVP 竞技场/测试场 + * 掉线时用当前(竞技场装备)背包覆盖玩家真实背包。改为滚动保留 3 份后, + * 即便发生误覆盖,玩家仍可从「最近 3 份」里挑出真实背包还原。 + */ + public void saveInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "inventory_backups" + + " (player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience, " + + "save_time) " + + "VALUES " + + "(?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + + // 仅保留最近 3 份,删除该玩家更早的备份 + // ★ 2026-07-10:改为先查后删,避免某些 SQLite 版本/驱动把带 LIMIT 的子查询优化掉, + // 导致只保留最新一份的问题。 + try { + PreparedStatement sel = + db.prepareStatement( + "SELECT id FROM inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time DESC LIMIT 3"); + sel.setString(1, name); + ResultSet rs = sel.executeQuery(); + java.util.List keep = new java.util.ArrayList<>(); + while (rs.next()) { + keep.add(rs.getInt("id")); + } + rs.close(); + sel.close(); + + if (keep.size() == 3) { + // 保留 3 份,删除其余 + StringBuilder in = new StringBuilder(); + for (int i = 0; i < keep.size(); i++) { + if (i > 0) in.append(","); + in.append(keep.get(i)); + } + PreparedStatement del = db.prepareStatement( + "DELETE FROM inventory_backups " + + "WHERE player_name=? AND id NOT IN (" + + in.toString() + ")"); + del.setString(1, name); + del.executeUpdate(); + del.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public String[] loadInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time " + + "DESC LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deleteInventoryBackup( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM inventory_backups " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ==================== PVP 背包备份(独立表,避免与登录背包系统互相覆盖)==================== + // 登录背包系统使用 inventory_backups 表;PVP 备份若共用同一表/同一玩家名键, + // 会在换装、断线重连等时机互相覆盖,导致玩家"原始背包丢失"。故 PVP 必须独立存储。 + + /** + * 保存 PVP 背包备份(非覆盖)。 + * ★ 关键保护:只要该玩家已存在 PVP 背包备份(即尚未真正退场),绝不再次写入, + * 确保入场前的原始背包永远不被后续状态(换装/断线重连)覆盖。 + */ + public void savePvpInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + if (hasPvpInventoryBackup(name)) { + return; // 已有备份,严禁覆盖 + } + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "pvp_inventory_backup" + + " (player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience, " + + "save_time) " + + "VALUES " + + "(?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String[] getPvpInventoryBackup(String playerName) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "pvp_inventory_backup " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT 1"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = new String[5]; + result[0] = rs.getString("contents_b64"); + result[1] = rs.getString("armor_b64"); + result[2] = rs.getString("extra_b64"); + result[3] = String.valueOf(rs.getInt("level")); + result[4] = String.valueOf(rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deletePvpInventoryBackup(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM pvp_inventory_backup " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasPvpInventoryBackup(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM pvp_inventory_backup " + + "WHERE player_name=? LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + boolean has = rs.next(); + rs.close(); + ps.close(); + return has; + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public boolean hasInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + boolean has = rs.next(); + rs.close(); + ps.close(); + return has; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } +// ==================== PVP区域 ==================== + + public void savePVPRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions " + + "(name, world, " + + "x1, y1, z1, " + + "x2, y2, z2, " + + "create_time) " + + "VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map + getPVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getAllPVPRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + db.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deletePVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + +// ==================== PVP击杀 ==================== + + public int getPVPKills( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int kills = 0; + if (rs.next()) + kills = rs.getInt("kills"); + rs.close(); + ps.close(); + return kills; + } catch (SQLException e) { + return 0; + } + } + + public int getPVPDeaths( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int deaths = 0; + if (rs.next()) + deaths = rs.getInt("deaths"); + rs.close(); + ps.close(); + return deaths; + } catch (SQLException e) { + return 0; + } + } + + public void addPVPKill( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,1,0) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "kills=kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void addPVPDeath( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,0,1) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "deaths=deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getPVPKillTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT player_name, " + + "kills, deaths " + + "FROM pvp_kills " + + "WHERE " + + "region_name=? " + + "AND kills>0 " + + "ORDER BY kills " + + "DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + 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 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 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> getAllArenaSessions() { + List> 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 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 getArenaTotalStats(String uuid) { + Map 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 getArenaSessionStats(String uuid, int sessionId) { + Map 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; + } + + // ==================== 安全报警 ==================== + + public void recordSecurityAlert( + String playerName, String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO security_alerts " + + "(player_name, alert_ip, " + + "alert_time, alert_date) " + + "VALUES (?,?,?,?)"); + ps.setString(1, playerName); + ps.setString(2, ip); + long now = System.currentTimeMillis(); + ps.setLong(3, now); + ps.setString(4, + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date( + now))); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + public List> + getUnnotifiedAlerts( + String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM security_alerts " + + "WHERE player_name=? " + + "AND notified=0 " + + "ORDER BY alert_time ASC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void markAlertNotified(int alertId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE security_alerts " + + "SET notified=1, " + + "notified_time=? " + + "WHERE id=?"); + ps.setLong(1, + System.currentTimeMillis()); + ps.setInt(2, alertId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + /** + * 获取玩家最近N条背包备份 + */ + public List> + getInventoryBackups(String name, + int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, save_time " + + "FROM inv_backup_auto " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT ?"); + ps.setString(1, name); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("save_time", + rs.getLong("save_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + /** + * 获取玩家登录备份列表(inventory_backups) + */ + public List> getLoginBackups( + String name, int limit) { + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT id, save_time FROM inventory_backups " + + "WHERE player_name=? ORDER BY save_time DESC LIMIT ?"); + ps.setString(1, name); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("save_time", rs.getLong("save_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 获取指定ID的登录备份数据 + */ + public String[] getLoginBackupById(int backupId) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT contents_b64, armor_b64, extra_b64, level, experience " + + "FROM inventory_backups WHERE id=?"); + ps.setInt(1, backupId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = new String[5]; + result[0] = rs.getString("contents_b64"); + result[1] = rs.getString("armor_b64"); + result[2] = rs.getString("extra_b64"); + result[3] = String.valueOf(rs.getInt("level")); + result[4] = String.valueOf(rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 获取指定ID的备份数据 + */ + public String[] getInventoryBackupById( + int backupId) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inv_backup_auto " + + "WHERE id=?"); + ps.setInt(1, backupId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 获取玩家最新一份自动背包备份(黑名单解除时返还用)。 + * @return String[]{contents_b64, armor_b64, extra_b64, level, experience},无备份返回 null + */ + public String[] getLatestAutoBackup(String playerName) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inv_backup_auto " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT 1"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = new String[5]; + result[0] = rs.getString("contents_b64"); + result[1] = rs.getString("armor_b64"); + result[2] = rs.getString("extra_b64"); + result[3] = String.valueOf(rs.getInt("level")); + result[4] = String.valueOf(rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 获取玩家最新背包备份 + */ + public String[] getInventoryBackup(String playerName) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT 1"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = new String[5]; + result[0] = rs.getString("contents_b64"); + result[1] = rs.getString("armor_b64"); + result[2] = rs.getString("extra_b64"); + result[3] = String.valueOf(rs.getInt("level")); + result[4] = String.valueOf(rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + // ==================== 工单操作 ==================== + + public int createTicket(String type, + String status, String priority, + String requester, String title, + String description, + int rewardAmount, + String targetPlayer) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO tickets " + + "(type, status, priority, " + + "requester, title, " + + "description, " + + "reward_amount, " + + "target_player, " + + "create_time) " + + "VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, type); + ps.setString(2, status); + ps.setString(3, priority); + ps.setString(4, requester); + ps.setString(5, title); + ps.setString(6, description); + ps.setInt(7, rewardAmount); + ps.setString(8, targetPlayer); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ResultSet rs = + ps.getGeneratedKeys(); + int id = -1; + if (rs.next()) id = rs.getInt(1); + rs.close(); + ps.close(); + return id; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + public void updateTicketStatus(int ticketId, + String status) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET status=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, status); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void assignTicket(int ticketId, + String provider) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "assigned_to=?, " + + "status='processing', " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, provider); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void completeTicket(int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='completed', " + + "complete_time=?, " + + "update_time=? " + + "WHERE id=?"); + long now = System.currentTimeMillis(); + ps.setLong(1, now); + ps.setLong(2, now); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void confirmTicket(int ticketId, + int score) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "admin_confirmed=1, " + + "score=?, " + + "status='resolved', " + + "update_time=? " + + "WHERE id=?"); + ps.setInt(1, score); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public void rejectTicketComplete(int ticketId, + String reason) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='rejected', " + + "reject_reason=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, reason); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 更新工单任意字段 + */ + public void updateTicketField(int ticketId, + String field, Object value) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + field + + "=? WHERE id=?"); + if (value instanceof Integer) + ps.setInt(1, (Integer) value); + else if (value instanceof Long) + ps.setLong(1, (Long) value); + else + ps.setString(1, + String.valueOf(value)); + ps.setInt(2, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getTicket( + int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE id=?"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getTicketsByStatus(String status) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status=? " + + "ORDER BY create_time DESC"); + ps.setString(1, status); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void addTicketReply(int ticketId, + String sender, String message) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO ticket_replies " + + "(ticket_id, sender, " + + "message, create_time) " + + "VALUES (?,?,?,?)"); + ps.setInt(1, ticketId); + ps.setString(2, sender); + ps.setString(3, message); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + // 更新工单状态为已回复 + updateTicketStatus(ticketId, + "replied"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getTicketReplies(int ticketId) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM ticket_replies " + + "WHERE ticket_id=? " + + "ORDER BY create_time ASC"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 获取已回复但超时未互动的工单 + */ + public List> + getRepliedStaleTickets( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='replied' " + + "AND update_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByPlayer(String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE requester=? " + + "ORDER BY create_time DESC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByProvider(String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "AND (status='processing' " + + "OR status='replied') " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getUnconfirmedCompleted( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='completed' " + + "AND admin_confirmed=0 " + + "AND complete_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 服务商操作 ==================== + + public void addServiceProvider(String name, + String role) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO " + + "service_providers " + + "(player_name, role, " + + "active, join_time) " + + "VALUES (?,?,1,?)"); + ps.setString(1, name); + ps.setString(2, role); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM service_providers " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean isServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM service_providers " + + "WHERE player_name=? " + + "AND active=1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public List> + getAllServiceProviders() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM service_providers " + + "WHERE active=1"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 全量服务商名单(含 active=0 的停用记录)。 + * 对账必须拿到与 PHP 完全相同的集合:只查 active=1 会让停用记录在 PHP 侧永远是 extra, + * 进而被「整表 DELETE」的推送一条条抹掉。 + */ + public List> + getAllServiceProvidersFull() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM service_providers"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 领地方块/容器记录 ==================== + + /** 记录领地方块破坏/放置 */ + public void recordLandBlock(String landName, int landId, + String world, int x, int y, int z, + String playerName, String action, + String material) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO land_block_log" + + " (land_name, land_id, world, x, y, z," + + " player_name, action, material, time)" + + " VALUES (?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, landName); + ps.setInt(2, landId); + ps.setString(3, world); + ps.setInt(4, x); + ps.setInt(5, y); + ps.setInt(6, z); + ps.setString(7, playerName); + ps.setString(8, action); + ps.setString(9, material); + ps.setLong(10, System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** ★ 记录领地方块(含detail,用于告示牌内容等) */ + public void recordLandBlockWithDetail(String landName, int landId, + String world, int x, int y, int z, + String playerName, String action, + String material, String detail) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO land_block_log" + + " (land_name, land_id, world, x, y, z," + + " player_name, action, material, detail, time)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, landName); + ps.setInt(2, landId); + ps.setString(3, world); + ps.setInt(4, x); + ps.setInt(5, y); + ps.setInt(6, z); + ps.setString(7, playerName); + ps.setString(8, action); + ps.setString(9, material); + ps.setString(10, detail); + ps.setLong(11, System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** 记录领地容器打开/拿取 */ + public void recordLandContainer(String landName, int landId, + String world, int x, int y, int z, + String playerName, String action, + String containerType, String detail, + int amount) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO land_container_log" + + " (land_name, land_id, world, x, y, z," + + " player_name, action, container_type," + + " detail, amount, time)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, landName); + ps.setInt(2, landId); + ps.setString(3, world); + ps.setInt(4, x); + ps.setInt(5, y); + ps.setInt(6, z); + ps.setString(7, playerName); + ps.setString(8, action); + ps.setString(9, containerType); + ps.setString(10, detail); + ps.setInt(11, amount); + ps.setLong(12, System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** 查询某坐标最近的方块记录(按时间倒序) */ + public List> getLandBlockLogAt( + String world, int x, int y, int z, int limit) { + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT time, player_name, action, material, detail" + + " FROM land_block_log" + + " WHERE world=? AND x=? AND y=? AND z=?" + + " ORDER BY time DESC LIMIT ?"); + ps.setString(1, world); + ps.setInt(2, x); + ps.setInt(3, y); + ps.setInt(4, z); + ps.setInt(5, limit > 0 ? limit : 20); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("time", rs.getLong("time")); + row.put("player_name", + rs.getString("player_name")); + row.put("action", rs.getString("action")); + row.put("material", rs.getString("material")); + row.put("detail", rs.getString("detail")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** 查询某坐标指定天数前的方块记录(daysAgo=0今天,1昨天,-1所有) */ + public List> getLandBlockLogAt( + String world, int x, int y, int z, int limit, int daysAgo) { + List> list = new ArrayList<>(); + try { + StringBuilder sql = new StringBuilder( + "SELECT time, player_name, action, material, detail" + + " FROM land_block_log" + + " WHERE world=? AND x=? AND y=? AND z=?"); + if (daysAgo >= 0) { + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.set(java.util.Calendar.HOUR_OF_DAY, 0); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); + long dayStart = cal.getTimeInMillis(); + cal.add(java.util.Calendar.DAY_OF_YEAR, 1); + long dayEnd = cal.getTimeInMillis(); + sql.append(" AND time >= ? AND time < ?"); + } + sql.append(" ORDER BY time DESC LIMIT ?"); + PreparedStatement ps = db.prepareStatement(sql.toString()); + ps.setString(1, world); + ps.setInt(2, x); + ps.setInt(3, y); + ps.setInt(4, z); + int idx = 5; + if (daysAgo >= 0) { + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.set(java.util.Calendar.HOUR_OF_DAY, 0); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); + long dayStart = cal.getTimeInMillis(); + cal.add(java.util.Calendar.DAY_OF_YEAR, 1); + long dayEnd = cal.getTimeInMillis(); + ps.setLong(idx++, dayStart); + ps.setLong(idx++, dayEnd); + } + ps.setInt(idx, limit > 0 ? limit : 20); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("time", rs.getLong("time")); + row.put("player_name", rs.getString("player_name")); + row.put("action", rs.getString("action")); + row.put("material", rs.getString("material")); + row.put("detail", rs.getString("detail")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** 查询某坐标最近的容器记录(按时间倒序) */ + public List> getLandContainerLogAt( + String world, int x, int y, int z, int limit) { + return getLandContainerLogAt(world, x, y, z, limit, -1); + } + + /** 查询某坐标指定天数前的容器记录(daysAgo=0今天,1昨天,-1所有) */ + public List> getLandContainerLogAt( + String world, int x, int y, int z, int limit, int daysAgo) { + List> list = new ArrayList<>(); + try { + StringBuilder sql = new StringBuilder( + "SELECT time, player_name, action, container_type," + + " detail, amount" + + " FROM land_container_log" + + " WHERE world=? AND x=? AND y=? AND z=?"); + if (daysAgo >= 0) { + // 计算当天起始和结束毫秒时间戳 + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.set(java.util.Calendar.HOUR_OF_DAY, 0); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); + long dayStart = cal.getTimeInMillis(); + cal.add(java.util.Calendar.DAY_OF_YEAR, 1); + long dayEnd = cal.getTimeInMillis(); + sql.append(" AND time >= ? AND time < ?"); + } + sql.append(" ORDER BY time DESC LIMIT ?"); + PreparedStatement ps = db.prepareStatement(sql.toString()); + ps.setString(1, world); + ps.setInt(2, x); + ps.setInt(3, y); + ps.setInt(4, z); + int idx = 5; + if (daysAgo >= 0) { + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.set(java.util.Calendar.HOUR_OF_DAY, 0); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo); + long dayStart = cal.getTimeInMillis(); + cal.add(java.util.Calendar.DAY_OF_YEAR, 1); + long dayEnd = cal.getTimeInMillis(); + ps.setLong(idx++, dayStart); + ps.setLong(idx++, dayEnd); + } + ps.setInt(idx, limit > 0 ? limit : 20); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("time", rs.getLong("time")); + row.put("player_name", + rs.getString("player_name")); + row.put("action", rs.getString("action")); + row.put("container_type", + rs.getString("container_type")); + row.put("detail", rs.getString("detail")); + row.put("amount", rs.getInt("amount")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** 清理超过 cutoff(毫秒时间戳)的领地记录,返回删除行数 */ + public int pruneLandRecordsOlderThan(long cutoff) { + int deleted = 0; + try { + PreparedStatement ps1 = db.prepareStatement( + "DELETE FROM land_block_log WHERE time < ?"); + ps1.setLong(1, cutoff); + deleted += ps1.executeUpdate(); + ps1.close(); + + PreparedStatement ps2 = db.prepareStatement( + "DELETE FROM land_container_log WHERE time < ?"); + ps2.setLong(1, cutoff); + deleted += ps2.executeUpdate(); + ps2.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return deleted; + } + + // ==================== 背包自动滚动备份(每小时 0/15/30/45 分准点定时)==================== + + /** + * 保存背包自动备份 —— 追加写入,每位玩家仅保留最近 3 份。 + * 跳过 PVP 竞技场/测试场的判断由调用方(InventoryAutoBackup)负责。 + */ + public void saveAutoBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + try (PreparedStatement ps = db.prepareStatement( + "INSERT INTO inv_backup_auto " + + "(player_name, contents_b64, armor_b64, " + + "extra_b64, level, experience, save_time) " + + "VALUES (?,?,?,?,?,?,?)")) { + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, System.currentTimeMillis()); + ps.executeUpdate(); + } + + // 保留最近 3 份 —— 先查最新 3 个 ID,再删其余 + List keep = new ArrayList<>(); + try (PreparedStatement sel = db.prepareStatement( + "SELECT id FROM inv_backup_auto " + + "WHERE player_name=? " + + "ORDER BY save_time DESC LIMIT 3")) { + sel.setString(1, name); + try (ResultSet rs = sel.executeQuery()) { + while (rs.next()) keep.add(rs.getInt("id")); + } + } + + if (keep.size() == 3) { + StringBuilder in = new StringBuilder(); + for (int i = 0; i < keep.size(); i++) { + if (i > 0) in.append(","); + in.append(keep.get(i)); + } + try (PreparedStatement del = db.prepareStatement( + "DELETE FROM inv_backup_auto " + + "WHERE player_name=? AND id NOT IN (" + + in + ")")) { + del.setString(1, name); + int removed = del.executeUpdate(); + if (removed > 0) { + logger.info("[自动备份] " + name + " 滚动清理 " + removed + " 份旧备份"); + } + } + } + } catch (SQLException e) { + logger.warning("[自动备份] 保存/清理失败 (" + name + "): " + e.getMessage()); + } + } + + public Connection getDb() { + return db; + } +}