diff --git a/.gitattributes b/.gitattributes index 94a8bdd..d49c874 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,5 @@ +*.md text=auto encoding=UTF-8 *.png binary *.jpg binary *.jpeg binary -*.gif binary -*.ico binary -*.bmp binary \ No newline at end of file +*.gif binary \ No newline at end of file diff --git a/src/main/java/Sdf1_login/ConfigManager.java b/src/main/java/Sdf1_login/ConfigManager.java index 29fb6d7..a22ac57 100644 --- a/src/main/java/Sdf1_login/ConfigManager.java +++ b/src/main/java/Sdf1_login/ConfigManager.java @@ -1,451 +1,462 @@ -package Sdf1_login; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static java.lang.Double.parseDouble; - -public class ConfigManager { - - private final File dataFolder; - private final Map messages = new LinkedHashMap<>(); - private final Map smtpSettings = new LinkedHashMap<>(); - - public String adminTag = "admin"; - public String adminPassword = "qweasd"; - public int maxAccountsPerIP = 3; - public String approvalMode = "manual"; - public int autoApproveDelayMinutes = 30; - public int loginTimeout = 60; - public boolean afkEnabled = false; - public int afkTimeout = 300; - public boolean smtpSsl = true; - public String smtpVerifyCode = ""; - public long smtpVerifyExpire = 0; - public long smtpLastSendTime = 0; - public String smtpConfiguringPlayer = ""; - - - public ConfigManager(File dataFolder) { - this.dataFolder = dataFolder; - } - - // ==================== 消息 ==================== - - public void loadMessages() { - messages.clear(); - File file = new File(dataFolder, "消息.txt"); - if (!file.exists()) createDefaultMessages(file); - loadMap(file, messages); - } - - public String msg(String key) { - String val = messages.getOrDefault(key, key); - val = val.replace("", "&l"); - val = val.replace("", "&r"); - val = val.replace("
", "\n"); - val = val.replace("\\n", "\n"); - // &颜色码 转换为 §颜色码 - val = val.replaceAll( - "&([0-9a-fk-orA-FK-OR])", "§$1"); - return val; - } - - - private void createDefaultMessages(File f) { - List L = new ArrayList<>(); - L.add("# =========================================="); - L.add("# Sdf1_login 消息配置文件"); - L.add("# =========================================="); - L.add("# 颜色代码使用 § 符号"); - L.add("# §0黑色 §1深蓝 §2深绿 §3深青"); - L.add("# §4深红 §5深紫 §6金色 §7灰色"); - L.add("# §8深灰 §9蓝色 §a绿色 §b浅蓝"); - L.add("# §c红色 §d粉色 §e黄色 §f白色"); - L.add("# §l粗体 §n下划线 §o斜体 §r重置"); - L.add("#"); - L.add("# 占位符用 {xxx} 表示,运行时自动替换"); - L.add("# 支持 粗体 标签"); - L.add("# 支持 \\n 或
换行"); - L.add("# 修改后执行 /sdf1_login reload 重载"); - L.add("# =========================================="); - L.add(""); - - // --- 登录/注册 --- - L.add("# ---- 登录注册相关 ----"); - L.add("# login_timeout: 登录超时被踢出时的提示"); - L.add("login_timeout=§c登录超时,请重新登录"); - L.add("# not_registered: 未注册玩家收到的提示"); - L.add("not_registered=§e您尚未注册,请使用 /reg <密码>"); - L.add("# not_logged_in: 已注册但未登录的提示"); - L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>"); - L.add("# already_registered: 已注册玩家再次注册时的提示"); - L.add("already_registered=§c您已注册过账号"); - L.add("# already_logged_in: 已登录玩家再次登录时的提示"); - L.add("already_logged_in=§c您已登录"); - L.add("# reg_success: 注册成功提示"); - L.add("reg_success=§a注册成功!欢迎来到服务器"); - L.add("# login_success: 登录成功提示"); - L.add("login_success=§a登录成功!"); - L.add("# login_failed: 密码错误提示"); - L.add("login_failed=§c密码错误,请重试"); - L.add(""); - - // --- 密码 --- - L.add("# ---- 密码相关 ----"); - L.add("# password_wrong: 旧密码验证失败"); - L.add("password_wrong=§c密码错误"); - L.add("# password_changed: 密码修改成功"); - L.add("password_changed=§a密码修改成功!"); - L.add("# password_format_error: 密码格式不符合要求"); - L.add("password_format_error=§c密码需6位以上,含大小写字母和数字"); - L.add("# password_same: 新密码与旧密码相同"); - L.add("password_same=§c新密码不能与旧密码相同"); - L.add("# reset_no_email: 找回密码时未绑定邮箱"); - L.add("reset_no_email=§c您未绑定邮箱,无法找回密码"); - L.add("# reset_sent: 临时密码已发送到邮箱"); - L.add("reset_sent=§a临时密码已发送到您的邮箱,请查收"); - L.add(""); - - // --- 签到积分 --- - L.add("# ---- 签到积分相关 ----"); - L.add("# checkin_already: 今日已签到"); - L.add("checkin_already=§c您今日已签到"); - L.add("# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率"); - L.add("checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}"); - L.add("# points_insufficient: 积分不足"); - L.add("points_insufficient=§c积分不足,当前: {points}"); - L.add("# points_purchase_success: 积分购买成功"); - L.add("points_purchase_success=§a购买成功!消耗 {count} 积分"); - L.add(""); - - // --- 挂机 --- - L.add("# ---- 挂机检测相关 ----"); - L.add("# afk_set_enabled: 挂机检测开启"); - L.add("afk_set_enabled=§a挂机检测已开启"); - L.add("# afk_set_disabled: 挂机检测关闭"); - L.add("afk_set_disabled=§c挂机检测已关闭"); - L.add("# afk_kick: 因挂机被踢出"); - L.add("afk_kick=§c因挂机过久被踢出"); - L.add(""); - - // --- 邮箱 --- - L.add("# ---- 邮箱相关 ----"); - L.add("# email_set: 邮箱设置成功"); - L.add("email_set=§a邮箱设置成功:{email}"); - L.add("# email_format_error: 邮箱格式错误"); - L.add("email_format_error=§c邮箱格式不正确"); - L.add(""); - - // --- 邀请 --- - L.add("# ---- 邀请相关 ----"); - L.add("# invite_code_generated: 邀请码生成"); - L.add("invite_code_generated=§a邀请码:{code}"); - L.add("# invite_used: 使用邀请码成功"); - L.add("invite_used=§a邀请成功,邀请人:{player}"); - L.add("# invite_self: 使用自己的邀请码"); - L.add("invite_self=§c不能使用自己的邀请码"); - L.add("# invite_not_found: 邀请码不存在"); - L.add("invite_not_found=§c邀请码不存在"); - L.add("# invite_referral_bonus: 被邀请人签到后邀请人获得奖励"); - L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分"); - L.add(""); - - // --- 礼包 --- - L.add("# ---- 新人礼包相关 ----"); - L.add("# gift_not_ready: 礼包条件未满足"); - L.add("gift_not_ready=§c礼包条件未满足"); - L.add("# gift_claimed: 礼包领取成功"); - L.add("gift_claimed=§a礼包领取成功!"); - L.add(""); - - // --- 审批/IP --- - L.add("# ---- 审批与IP限制 ----"); - L.add("# need_approval: 注册需审批"); - L.add("need_approval=§e注册需审批 (工单#{id})"); - L.add("# ip_max_accounts: IP注册已达上限"); - L.add("ip_max_accounts=§cIP注册已达上限"); - L.add(""); - - // --- 管理员 --- - L.add("# ---- 管理员操作 ----"); - L.add("# admin_delete_confirm: 管理员删除账号确认提示"); - L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除"); - L.add(""); - - // --- 聊天过滤 --- - L.add("# ---- 聊天过滤 ----"); - L.add("# chat_muted: 禁言玩家尝试聊天"); - L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天"); - L.add("# chat_url_blocked: 发送违规链接被拦截"); - L.add("# {url}=检测到的链接地址"); - L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}"); - L.add("# chat_url_violation: 当前违规次数"); - L.add("# {count}=当前违规次数"); - L.add("chat_url_violation=§7当前违规次数: §e{count}"); - L.add("# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数"); - L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)"); - L.add("# chat_url_broadcast: 全服通报,{player}=玩家名"); - L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚"); - L.add(""); - - // --- 通用 --- - L.add("# ---- 通用 ----"); - L.add("# 未知参数: 未知命令或权限不足时的提示"); - L.add("未知参数=§c未知参数或权限不足"); - L.add("# only_player: 仅玩家可用的命令"); - L.add("# only_player=§c此命令仅玩家可用"); - - writeLines(f, L); - } - - - // ==================== SMTP ==================== - - public void loadSmtp() { - smtpSettings.clear(); - File file = new File(dataFolder, "SMTP设置.txt"); - if (!file.exists()) createDefaultSmtp(file); - loadMap(file, smtpSettings); - } - private double parseDouble(String s, double def) { - try { return Double.parseDouble(s.trim()); } - catch (Exception e) { return def; } - } - - public String getSmtp(String key) { - return smtpSettings.getOrDefault(key, ""); - } - /** 获取挂机白名单列表 */ - public List getAfkWhitelist() { - List list = new ArrayList<>(); - File file = new File(dataFolder, "挂机白名单.txt"); - if (!file.exists()) { - try { - file.createNewFile(); - try (BufferedWriter w = new BufferedWriter( - new OutputStreamWriter( - new FileOutputStream(file), - StandardCharsets.UTF_8))) { - w.write("# 每行一个玩家名,前面加 # 为注释"); - w.newLine(); - w.write("# 以下玩家不会被挂机踢出"); - w.newLine(); - } - } catch (IOException ignored) {} - } - try (BufferedReader r = new BufferedReader( - new InputStreamReader( - new FileInputStream(file), - StandardCharsets.UTF_8))) { - String line; - while ((line = r.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && !line.startsWith("#")) { - list.add(line); - } - } - } catch (IOException ignored) {} - return list; - } - - /** 设置SMTP单项(聊天输入用) */ - public void setSmtp(String key, String value) { - smtpSettings.put(key, value); - } - - /** 将当前SMTP设置写回文件 */ - public void saveSmtp() { - File file = new File(dataFolder, "SMTP设置.txt"); - List L = new ArrayList<>(); - L.add("# ===== SMTP 邮件服务器配置 ====="); - for (Map.Entry e : smtpSettings.entrySet()) { - L.add(e.getKey() + "=" + e.getValue()); - } - writeLines(file, L); - } - - private void createDefaultSmtp(File f) { - List L = new ArrayList<>(); - L.add("# ===== SMTP 邮件服务器配置 ====="); - L.add("smtp地址=smtp.example.com"); - L.add("smtp端口=465"); - L.add("smtp账号=your_email@example.com"); - L.add("smtp密码="); - L.add("发件人名称=Sdf1_login"); - L.add("smtp加密=true"); - writeLines(f, L); - } - - // ==================== 插件设置 ==================== - - public void loadSettings() { - File file = new File(dataFolder, "插件设置.txt"); - if (!file.exists()) createDefaultSettings(file); - Map m = new LinkedHashMap<>(); - loadMap(file, m); - adminTag = m.getOrDefault("管理标签", adminTag); - adminPassword = m.getOrDefault("管理密码", adminPassword); - maxAccountsPerIP = parseInt( - m.getOrDefault("每IP最大账号数", "3"), 3); - approvalMode = m.getOrDefault("审批模式", approvalMode); - autoApproveDelayMinutes = parseInt( - m.getOrDefault("自动审批延迟分钟", "30"), 30); - loginTimeout = parseInt( - m.getOrDefault("登录超时秒数", "60"), 60); - afkEnabled = Boolean.parseBoolean( - m.getOrDefault("挂机踢出", "false")); - afkTimeout = parseInt( - m.getOrDefault("挂机超时秒数", "300"), 300); - smtpSsl = Boolean.parseBoolean( - m.getOrDefault("smtp加密", "true")); - providerPointsPerScore = parseDouble( - m.getOrDefault("服务商积分倍率", "2.0"), - 2.0); - baseEconomyReward = parseInt( - m.getOrDefault("基础经济奖励", "100"), - 100); - providerEconomyPerScore = parseDouble( - m.getOrDefault("经济奖励每评分", "50.0"), - 50.0); - requesterPointsReward = parseInt( - m.getOrDefault("报单人积分奖励", "10"), - 10); - - - } - - - public void saveSettings() { - File file = new File(dataFolder, "插件设置.txt"); - List L = new ArrayList<>(); - L.add("# ===== Sdf1_login 插件设置 ====="); - L.add("管理标签=" + adminTag); - L.add("管理密码=" + adminPassword); - L.add("每IP最大账号数=" + maxAccountsPerIP); - L.add("审批模式=" + approvalMode); - L.add("自动审批延迟分钟=" + autoApproveDelayMinutes); - L.add("登录超时秒数=" + loginTimeout); - L.add("挂机踢出=" + afkEnabled); - L.add("挂机超时秒数=" + afkTimeout); - // 垃圾站设置 - L.add("# ---- 垃圾站 ----"); - L.add("垃圾站_启用: " - + garbageEnabled); - L.add("垃圾站_清理间隔秒: " - + garbageInterval); - L.add("垃圾站_保留轮数: " - + garbageMaxRounds); - - writeLines(file, L); - } - public double providerPointsPerScore = 2.0; - public int baseEconomyReward = 100; - public double providerEconomyPerScore = 50.0; - public int requesterPointsReward = 10; - public boolean garbageEnabled = true; - public int garbageInterval = 300; - public int garbageMaxRounds = 1; - - - private void createDefaultSettings(File f) { - List L = new ArrayList<>(); - L.add("# ===== Sdf1_login 插件设置 ====="); - L.add("管理标签=admin"); - L.add("管理密码=qweasd"); - L.add("每IP最大账号数=3"); - L.add("审批模式=manual"); - L.add("自动审批延迟分钟=30"); - L.add("登录超时秒数=60"); - L.add("挂机踢出=false"); - L.add("挂机超时秒数=300"); - L.add("服务商积分倍率=" + providerPointsPerScore); - L.add("基础经济奖励=" + baseEconomyReward); - L.add("经济奖励每评分=" + providerEconomyPerScore); - L.add("报单人积分奖励=" + requesterPointsReward); - - writeLines(f, L); - } - - // ==================== 通用读写 ==================== - - private void loadMap(File file, Map map) { - try (BufferedReader r = new BufferedReader( - new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { - String line; - while ((line = r.readLine()) != null) { - line = line.trim(); - if (line.isEmpty() || line.startsWith("#")) continue; - int eq = line.indexOf('='); - if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); - } - } catch (IOException e) { - System.err.println("[Sdf1_login] 读取失败: " + file.getName()); - } - } - - private void writeLines(File file, List lines) { - try (BufferedWriter w = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { - for (String l : lines) { w.write(l); w.newLine(); } - } catch (IOException e) { - System.err.println("[Sdf1_login] 写入失败: " + file.getName()); - } - } - - private int parseInt(String s, int def) { - try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } - } - /** - * 生成6位验证码(字母+数字) - */ - public String generateSmtpVerifyCode() { - String chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - + "abcdefghijklmnopqrstuvwxyz" - + "0123456789"; - StringBuilder sb = new StringBuilder(); - java.util.Random r = new java.util.Random(); - for (int i = 0; i < 6; i++) { - sb.append(chars.charAt(r.nextInt( - chars.length()))); - } - smtpVerifyCode = sb.toString(); - smtpVerifyExpire = - System.currentTimeMillis() + 300000L; - return smtpVerifyCode; - } - - /** - * 检查验证码是否有效 - */ - public boolean checkSmtpVerifyCode(String code) { - if (System.currentTimeMillis() > smtpVerifyExpire) { - smtpVerifyCode = ""; - return false; - } - return smtpVerifyCode.equalsIgnoreCase(code); - } - - /** - * 清除验证码 - */ - public void clearSmtpVerifyCode() { - smtpVerifyCode = ""; - smtpVerifyExpire = 0; - } - - /** - * 检查发送间隔是否>=1分钟 - */ - public boolean canSendSmtpVerify() { - return System.currentTimeMillis() - - smtpLastSendTime >= 60000L; - } - - public void recordSmtpSendTime() { - smtpLastSendTime = System.currentTimeMillis(); - } - -} +package Sdf1_login; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static java.lang.Double.parseDouble; + +public class ConfigManager { + + private final File dataFolder; + private final Map messages = new LinkedHashMap<>(); + private final Map smtpSettings = new LinkedHashMap<>(); + + public String adminTag = "admin"; + public String adminPassword = "qweasd"; + public int maxAccountsPerIP = 3; + public String approvalMode = "manual"; + public int autoApproveDelayMinutes = 30; + public int loginTimeout = 60; + public boolean afkEnabled = false; + public int afkTimeout = 300; + public boolean smtpSsl = true; + public String smtpVerifyCode = ""; + public long smtpVerifyExpire = 0; + public long smtpLastSendTime = 0; + public String smtpConfiguringPlayer = ""; + + + public ConfigManager(File dataFolder) { + this.dataFolder = dataFolder; + } + + // ==================== 消息 ==================== + + public void loadMessages() { + messages.clear(); + File file = new File(dataFolder, "消息.txt"); + if (!file.exists()) createDefaultMessages(file); + loadMap(file, messages); + } + + public String msg(String key) { + String val = messages.getOrDefault(key, key); + val = val.replace("", "&l"); + val = val.replace("", "&r"); + val = val.replace("
", "\n"); + val = val.replace("\\n", "\n"); + // &颜色码 转换为 §颜色码 + val = val.replaceAll( + "&([0-9a-fk-orA-FK-OR])", "§$1"); + return val; + } + + + private void createDefaultMessages(File f) { + List L = new ArrayList<>(); + L.add("# =========================================="); + L.add("# Sdf1_login 消息配置文件"); + L.add("# =========================================="); + L.add("# 颜色代码使用 § 符号"); + L.add("# §0黑色 §1深蓝 §2深绿 §3深青"); + L.add("# §4深红 §5深紫 §6金色 §7灰色"); + L.add("# §8深灰 §9蓝色 §a绿色 §b浅蓝"); + L.add("# §c红色 §d粉色 §e黄色 §f白色"); + L.add("# §l粗体 §n下划线 §o斜体 §r重置"); + L.add("#"); + L.add("# 占位符用 {xxx} 表示,运行时自动替换"); + L.add("# 支持 粗体 标签"); + L.add("# 支持 \\n 或
换行"); + L.add("# 修改后执行 /sdf1_login reload 重载"); + L.add("# =========================================="); + L.add(""); + + // --- 登录/注册 --- + L.add("# ---- 登录注册相关 ----"); + L.add("# login_timeout: 登录超时被踢出时的提示"); + L.add("login_timeout=§c登录超时,请重新登录"); + L.add("# not_registered: 未注册玩家收到的提示"); + L.add("not_registered=§e您尚未注册,请使用 /reg <密码>"); + L.add("# not_logged_in: 已注册但未登录的提示"); + L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>"); + L.add("# already_registered: 已注册玩家再次注册时的提示"); + L.add("already_registered=§c您已注册过账号"); + L.add("# already_logged_in: 已登录玩家再次登录时的提示"); + L.add("already_logged_in=§c您已登录"); + L.add("# reg_success: 注册成功提示"); + L.add("reg_success=§a注册成功!欢迎来到服务器"); + L.add("# login_success: 登录成功提示"); + L.add("login_success=§a登录成功!"); + L.add("# login_failed: 密码错误提示"); + L.add("login_failed=§c密码错误,请重试"); + L.add(""); + + // --- 密码 --- + L.add("# ---- 密码相关 ----"); + L.add("# password_wrong: 旧密码验证失败"); + L.add("password_wrong=§c密码错误"); + L.add("# password_changed: 密码修改成功"); + L.add("password_changed=§a密码修改成功!"); + L.add("# password_format_error: 密码格式不符合要求"); + L.add("password_format_error=§c密码需6位以上,含大小写字母和数字"); + L.add("# password_same: 新密码与旧密码相同"); + L.add("password_same=§c新密码不能与旧密码相同"); + L.add("# reset_no_email: 找回密码时未绑定邮箱"); + L.add("reset_no_email=§c您未绑定邮箱,无法找回密码"); + L.add("# reset_sent: 临时密码已发送到邮箱"); + L.add("reset_sent=§a临时密码已发送到您的邮箱,请查收"); + L.add(""); + + // --- 签到积分 --- + L.add("# ---- 签到积分相关 ----"); + L.add("# checkin_already: 今日已签到"); + L.add("checkin_already=§c您今日已签到"); + L.add("# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率"); + L.add("checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}"); + L.add("# points_insufficient: 积分不足"); + L.add("points_insufficient=§c积分不足,当前: {points}"); + L.add("# points_purchase_success: 积分购买成功"); + L.add("points_purchase_success=§a购买成功!消耗 {count} 积分"); + L.add(""); + + // --- 挂机 --- + L.add("# ---- 挂机检测相关 ----"); + L.add("# afk_set_enabled: 挂机检测开启"); + L.add("afk_set_enabled=§a挂机检测已开启"); + L.add("# afk_set_disabled: 挂机检测关闭"); + L.add("afk_set_disabled=§c挂机检测已关闭"); + L.add("# afk_kick: 因挂机被踢出"); + L.add("afk_kick=§c因挂机过久被踢出"); + L.add(""); + + // --- 邮箱 --- + L.add("# ---- 邮箱相关 ----"); + L.add("# email_set: 邮箱设置成功"); + L.add("email_set=§a邮箱设置成功:{email}"); + L.add("# email_format_error: 邮箱格式错误"); + L.add("email_format_error=§c邮箱格式不正确"); + L.add(""); + + // --- 邀请 --- + L.add("# ---- 邀请相关 ----"); + L.add("# invite_code_generated: 邀请码生成"); + L.add("invite_code_generated=§a邀请码:{code}"); + L.add("# invite_used: 使用邀请码成功"); + L.add("invite_used=§a邀请成功,邀请人:{player}"); + L.add("# invite_self: 使用自己的邀请码"); + L.add("invite_self=§c不能使用自己的邀请码"); + L.add("# invite_not_found: 邀请码不存在"); + L.add("invite_not_found=§c邀请码不存在"); + L.add("# invite_referral_bonus: 被邀请人签到后邀请人获得奖励"); + L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分"); + L.add(""); + + // --- 礼包 --- + L.add("# ---- 新人礼包相关 ----"); + L.add("# gift_not_ready: 礼包条件未满足"); + L.add("gift_not_ready=§c礼包条件未满足"); + L.add("# gift_claimed: 礼包领取成功"); + L.add("gift_claimed=§a礼包领取成功!"); + L.add(""); + + // --- 审批/IP --- + L.add("# ---- 审批与IP限制 ----"); + L.add("# need_approval: 注册需审批"); + L.add("need_approval=§e注册需审批 (工单#{id})"); + L.add("# ip_max_accounts: IP注册已达上限"); + L.add("ip_max_accounts=§cIP注册已达上限"); + L.add(""); + + // --- 管理员 --- + L.add("# ---- 管理员操作 ----"); + L.add("# admin_delete_confirm: 管理员删除账号确认提示"); + L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除"); + L.add(""); + + // --- 聊天过滤 --- + L.add("# ---- 聊天过滤 ----"); + L.add("# chat_muted: 禁言玩家尝试聊天"); + L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天"); + L.add("# chat_url_blocked: 发送违规链接被拦截"); + L.add("# {url}=检测到的链接地址"); + L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}"); + L.add("# chat_url_violation: 当前违规次数"); + L.add("# {count}=当前违规次数"); + L.add("chat_url_violation=§7当前违规次数: §e{count}"); + L.add("# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数"); + L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)"); + L.add("# chat_url_broadcast: 全服通报,{player}=玩家名"); + L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚"); + L.add(""); + + // --- 通用 --- + L.add("# ---- 通用 ----"); + L.add("# 未知参数: 未知命令或权限不足时的提示"); + L.add("未知参数=§c未知参数或权限不足"); + L.add("# only_player: 仅玩家可用的命令"); + L.add("# only_player=§c此命令仅玩家可用"); + + writeLines(f, L); + } + + + // ==================== SMTP ==================== + + public void loadSmtp() { + smtpSettings.clear(); + File file = new File(dataFolder, "SMTP设置.txt"); + if (!file.exists()) createDefaultSmtp(file); + loadMap(file, smtpSettings); + } + private double parseDouble(String s, double def) { + try { return Double.parseDouble(s.trim()); } + catch (Exception e) { return def; } + } + + public String getSmtp(String key) { + return smtpSettings.getOrDefault(key, ""); + } + /** 获取挂机白名单列表 */ + public List getAfkWhitelist() { + List list = new ArrayList<>(); + File file = new File(dataFolder, "挂机白名单.txt"); + if (!file.exists()) { + try { + file.createNewFile(); + try (BufferedWriter w = new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8))) { + w.write("# 每行一个玩家名,前面加 # 为注释"); + w.newLine(); + w.write("# 以下玩家不会被挂机踢出"); + w.newLine(); + } + } catch (IOException ignored) {} + } + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + list.add(line); + } + } + } catch (IOException ignored) {} + return list; + } + + /** 设置SMTP单项(聊天输入用) */ + public void setSmtp(String key, String value) { + smtpSettings.put(key, value); + } + + /** 将当前SMTP设置写回文件 */ + public void saveSmtp() { + File file = new File(dataFolder, "SMTP设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== SMTP 邮件服务器配置 ====="); + for (Map.Entry e : smtpSettings.entrySet()) { + L.add(e.getKey() + "=" + e.getValue()); + } + writeLines(file, L); + } + + private void createDefaultSmtp(File f) { + List L = new ArrayList<>(); + L.add("# ===== SMTP 邮件服务器配置 ====="); + L.add("smtp地址=smtp.example.com"); + L.add("smtp端口=465"); + L.add("smtp账号=your_email@example.com"); + L.add("smtp密码="); + L.add("发件人名称=Sdf1_login"); + L.add("smtp加密=true"); + writeLines(f, L); + } + + // ==================== 插件设置 ==================== + + public void loadSettings() { + File file = new File(dataFolder, "插件设置.txt"); + if (!file.exists()) createDefaultSettings(file); + Map m = new LinkedHashMap<>(); + loadMap(file, m); + adminTag = m.getOrDefault("管理标签", adminTag); + adminPassword = m.getOrDefault("管理密码", adminPassword); + maxAccountsPerIP = parseInt( + m.getOrDefault("每IP最大账号数", "3"), 3); + approvalMode = m.getOrDefault("审批模式", approvalMode); + autoApproveDelayMinutes = parseInt( + m.getOrDefault("自动审批延迟分钟", "30"), 30); + loginTimeout = parseInt( + m.getOrDefault("登录超时秒数", "60"), 60); + afkEnabled = Boolean.parseBoolean( + m.getOrDefault("挂机踢出", "false")); + afkTimeout = parseInt( + m.getOrDefault("挂机超时秒数", "300"), 300); + smtpSsl = Boolean.parseBoolean( + m.getOrDefault("smtp加密", "true")); + providerPointsPerScore = parseDouble( + m.getOrDefault("服务商积分倍率", "2.0"), + 2.0); + baseEconomyReward = parseInt( + m.getOrDefault("基础经济奖励", "100"), + 100); + providerEconomyPerScore = parseDouble( + m.getOrDefault("经济奖励每评分", "50.0"), + 50.0); + requesterPointsReward = parseInt( + m.getOrDefault("报单人积分奖励", "10"), + 10); + garbageEnabled = Boolean.parseBoolean( + m.getOrDefault("垃圾站_启用", "true")); + garbageInterval = parseInt( + m.getOrDefault("垃圾站_清理间隔秒", + "300"), 300); + garbageMaxRounds = parseInt( + m.getOrDefault("垃圾站_保留轮数", + "1"), 1); + + + } + + + public void saveSettings() { + File file = new File(dataFolder, "插件设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=" + adminTag); + L.add("管理密码=" + adminPassword); + L.add("每IP最大账号数=" + maxAccountsPerIP); + L.add("审批模式=" + approvalMode); + L.add("自动审批延迟分钟=" + autoApproveDelayMinutes); + L.add("登录超时秒数=" + loginTimeout); + L.add("挂机踢出=" + afkEnabled); + L.add("挂机超时秒数=" + afkTimeout); + // 垃圾站设置 + L.add("# ---- 垃圾站 ----"); + L.add("垃圾站_启用=" + garbageEnabled); + L.add("垃圾站_清理间隔秒=" + garbageInterval); + L.add("垃圾站_保留轮数=" + garbageMaxRounds); + + + writeLines(file, L); + } + public double providerPointsPerScore = 2.0; + public int baseEconomyReward = 100; + public double providerEconomyPerScore = 50.0; + public int requesterPointsReward = 10; + public boolean garbageEnabled = true; + public int garbageInterval = 300; + public int garbageMaxRounds = 1; + + + private void createDefaultSettings(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=admin"); + L.add("管理密码=qweasd"); + L.add("每IP最大账号数=3"); + L.add("审批模式=manual"); + L.add("自动审批延迟分钟=30"); + L.add("登录超时秒数=60"); + L.add("挂机踢出=false"); + L.add("挂机超时秒数=300"); + L.add("服务商积分倍率=" + providerPointsPerScore); + L.add("基础经济奖励=" + baseEconomyReward); + L.add("经济奖励每评分=" + providerEconomyPerScore); + L.add("报单人积分奖励=" + requesterPointsReward); + L.add("# ---- 垃圾站 ----"); + L.add("垃圾站_启用=true"); + L.add("垃圾站_清理间隔秒=300"); + L.add("垃圾站_保留轮数=1"); + + + writeLines(f, L); + } + + // ==================== 通用读写 ==================== + + private void loadMap(File file, Map map) { + try (BufferedReader r = new BufferedReader( + new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); + } + } catch (IOException e) { + System.err.println("[Sdf1_login] 读取失败: " + file.getName()); + } + } + + private void writeLines(File file, List lines) { + try (BufferedWriter w = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { + for (String l : lines) { w.write(l); w.newLine(); } + } catch (IOException e) { + System.err.println("[Sdf1_login] 写入失败: " + file.getName()); + } + } + + private int parseInt(String s, int def) { + try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } + } + /** + * 生成6位验证码(字母+数字) + */ + public String generateSmtpVerifyCode() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789"; + StringBuilder sb = new StringBuilder(); + java.util.Random r = new java.util.Random(); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt(r.nextInt( + chars.length()))); + } + smtpVerifyCode = sb.toString(); + smtpVerifyExpire = + System.currentTimeMillis() + 300000L; + return smtpVerifyCode; + } + + /** + * 检查验证码是否有效 + */ + public boolean checkSmtpVerifyCode(String code) { + if (System.currentTimeMillis() > smtpVerifyExpire) { + smtpVerifyCode = ""; + return false; + } + return smtpVerifyCode.equalsIgnoreCase(code); + } + + /** + * 清除验证码 + */ + public void clearSmtpVerifyCode() { + smtpVerifyCode = ""; + smtpVerifyExpire = 0; + } + + /** + * 检查发送间隔是否>=1分钟 + */ + public boolean canSendSmtpVerify() { + return System.currentTimeMillis() + - smtpLastSendTime >= 60000L; + } + + public void recordSmtpSendTime() { + smtpLastSendTime = System.currentTimeMillis(); + } + +} diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index 63c8cfb..95102f5 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -1,1394 +1,1405 @@ -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"); - File dbFile = new File(dataFolder, - "login.db"); - db = DriverManager.getConnection( - "jdbc:sqlite:" - + dbFile.getAbsolutePath()); - Statement st = db.createStatement(); - st.execute("PRAGMA journal_mode=WAL"); - - // 用户表 - 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 ''," - + "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'" - + ")"); - - // 安全添加 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'"); - - // ===== 背包备份表:安全迁移 ===== - // 检查旧表是否存在(用 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" - + ")"); - } - - // 安全报警表 - 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)"); - - // 工单表 - 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.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 void close() { - try { - if (db != null && !db.isClosed()) - db.close(); - } catch (Exception ignored) { - } - } - - // ==================== 用户操作 ==================== - - 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; - } - } - - public void createUser(String name, String hash, - String 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(); - } - } - - // ========== 临时密码管理 ========== - - 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; - } - - public boolean checkPasswordOrTemp( - 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(); - if (hash.equals(mainHash)) - return true; - } else { - rs.close(); - ps.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - // 再检查临时密码 - try { - PreparedStatement ps2 = db.prepareStatement( - "SELECT temp_password, " - + "temp_pw_expire, " - + "temp_pw_used " - + "FROM users " - + "WHERE player_name=?"); - ps2.setString(1, name); - ResultSet rs2 = ps2.executeQuery(); - if (rs2.next()) { - String tempHash = - rs2.getString( - "temp_password"); - long expire = - rs2.getLong( - "temp_pw_expire"); - int used = - rs2.getInt( - "temp_pw_used"); - rs2.close(); - ps2.close(); - if (tempHash != null - && !tempHash.isEmpty() - && hash.equals(tempHash) - && System.currentTimeMillis() - < expire - && used != 1) { - return true; - } - } else { - rs2.close(); - ps2.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - return false; - } - - - 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); - else - ps.setString(1, - String.valueOf(val)); - ps.setString(2, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - 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 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 stored = - rs.getString("password_hash"); - rs.close(); - ps.close(); - return hash.equals(stored); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - return false; - } - - - - 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; - } - - public void deleteUser(String name) { - try { - PreparedStatement ps = db.prepareStatement( - "DELETE FROM users " - + "WHERE player_name=?"); - ps.setString(1, name); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public void recordIP(String name, String ip) { - if (ip != null && !ip.isEmpty()) { - setField(name, "ip_address", 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; - } - - // ==================== 背包备份 ==================== - - public void saveInventoryBackup(String name, - String contents, String armor, - String extra, int level, - double experience) { - try { - PreparedStatement del = - db.prepareStatement( - "DELETE FROM " - + "inventory_backups " - + "WHERE player_name=?"); - del.setString(1, name); - del.executeUpdate(); - del.close(); - 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(); - } 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(); - } - } - - 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; - } - } - - - // ==================== 安全报警 ==================== - - 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 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[] getInventoryBackupById( - 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; - } - - - // ==================== 工单操作 ==================== - - 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; - } - - 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"); + File dbFile = new File(dataFolder, + "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 用户表 + 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 ''," + + "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'" + + ")"); + + // 安全添加 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'"); + + // ===== 背包备份表:安全迁移 ===== + // 检查旧表是否存在(用 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" + + ")"); + } + + // 安全报警表 + 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)"); + + // 工单表 + 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"); + 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 void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } + + // ==================== 用户操作 ==================== + + 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; + } + } + + public void createUser(String name, String hash, + String 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(); + } + } + + // ========== 临时密码管理 ========== + + 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; + } + + public boolean checkPasswordOrTemp( + 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(); + if (hash.equals(mainHash)) + return true; + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + // 再检查临时密码 + try { + PreparedStatement ps2 = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_expire, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps2.setString(1, name); + ResultSet rs2 = ps2.executeQuery(); + if (rs2.next()) { + String tempHash = + rs2.getString( + "temp_password"); + long expire = + rs2.getLong( + "temp_pw_expire"); + int used = + rs2.getInt( + "temp_pw_used"); + rs2.close(); + ps2.close(); + if (tempHash != null + && !tempHash.isEmpty() + && hash.equals(tempHash) + && System.currentTimeMillis() + < expire + && used != 1) { + return true; + } + } else { + rs2.close(); + ps2.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + 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); + else + ps.setString(1, + String.valueOf(val)); + ps.setString(2, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + 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 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 stored = + rs.getString("password_hash"); + rs.close(); + ps.close(); + return hash.equals(stored); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + + 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; + } + + public void deleteUser(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void recordIP(String name, String ip) { + if (ip != null && !ip.isEmpty()) { + setField(name, "ip_address", 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; + } + + // ==================== 背包备份 ==================== + + public void saveInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + PreparedStatement del = + db.prepareStatement( + "DELETE FROM " + + "inventory_backups " + + "WHERE player_name=?"); + del.setString(1, name); + del.executeUpdate(); + del.close(); + 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(); + } 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(); + } + } + + 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; + } + } + + + // ==================== 安全报警 ==================== + + 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 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[] getInventoryBackupById( + 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; + } + + + // ==================== 工单操作 ==================== + + 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; + } + + public Connection getDb() { + return db; + } +} diff --git a/src/main/java/Sdf1_login/GUIManager.java b/src/main/java/Sdf1_login/GUIManager.java index 114aad8..02230d6 100644 --- a/src/main/java/Sdf1_login/GUIManager.java +++ b/src/main/java/Sdf1_login/GUIManager.java @@ -1,596 +1,602 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.*; -import org.bukkit.inventory.meta.ItemMeta; -import java.io.File; - -import java.util.*; - -public class GUIManager { - - private final Main plugin; - - public static final String T_MAIN = - "§6§l登录面板"; - public static final String T_MY_INFO = - "§a§l我的信息"; - public static final String T_ADMIN = - "§c§l管理员面板"; - public static final String T_USER_MGMT = - "§e§l用户管理"; - public static final String T_INVITE = - "§b§l邀请数据"; - public static final String T_TASK_CENTER = "§d§l任务中心"; - public static final String T_GIFT_STAGES = "§d§l新人任务"; - - - public GUIManager(Main plugin) { - this.plugin = plugin; - } - - public void openMain(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_MAIN); - fillBg(g); - g.setItem(10, mkItem(Material.PLAYER_HEAD, - "§a§l我的信息")); - g.setItem(12, mkItem(Material.PAPER, - "§b§l邀请数据")); - g.setItem(14, mkItem(Material.EMERALD, - "§d§l积分商城")); - g.setItem(16, mkItem(Material.CHEST, - "§e§l新人礼包")); - g.setItem(18, mkItem(Material.BOOK, - "§6工单系统", - "§7提交bug、求助、举报")); - if (p.isOp() || isAdmin(p)) { - g.setItem(20, mkItem(Material.CHEST, - "§6垃圾回收站", - "§7查看和取回清理的物品")); - } - if (isAdmin(p)) { - g.setItem(22, mkItem(Material.REDSTONE_BLOCK, - "§c§l管理员面板")); - } - p.openInventory(g); - } - - public void openMyInfo(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_MY_INFO); - fillBg(g); - Map user = plugin.getDb() - .getUser(p.getName()); - boolean checkedIn = plugin.getCheckIn() - .isCheckedInToday(p.getName()); - Material checkMat = checkedIn - ? Material.EMERALD : Material.COAL; - String checkName = checkedIn - ? "§a已签到" : "§7点击签到"; - int streak = ((Number) user.getOrDefault( - "checkin_streak", 0)).intValue(); - int points = ((Number) user.getOrDefault( - "points", 0)).intValue(); - int totalDays = ((Number) user.getOrDefault( - "total_checkin_days", 0)).intValue(); - String email = (String) user.getOrDefault( - "email", ""); - g.setItem(4, mkItem(Material.NAME_TAG, - "§e" + p.getName())); - g.setItem(10, mkItem(checkMat, checkName, - "§7连续签到: " + streak + "天", - "§7累计签到: " + totalDays + "天", - "§7当前积分: " + points)); - g.setItem(11, mkItem(Material.PAPER, - "§e修改密码", "§7双击修改密码")); - g.setItem(12, mkItem(Material.CLOCK, - "§e补签", "§7消耗10积分补签3天内")); - g.setItem(13, mkItem(Material.ENDER_PEARL, - "§e邀请码", - "§7你的邀请码: " - + user.getOrDefault( - "invite_code", "无"), - "§7点击生成/查看")); - g.setItem(14, mkItem(Material.BOOK, - "§e邮箱: " - + (email.isEmpty() - ? "未绑定" : email), - "§7点击绑定邮箱")); - g.setItem(15, mkItem(Material.EMERALD_BLOCK, - "§a§l积分商城", - "§7当前: " + points + "积分")); - g.setItem(26, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - - /** - * 打开账号删除选择面板 - * 展示该玩家IP下所有账号,供选择删除 - */ - public void openAccountDelete(Player p) { - String ip = plugin.getPlayerIP(p); - if (ip == null) { - p.sendMessage("§c无法获取你的IP地址"); - return; - } - - List> accounts = - plugin.getDb().getAccountDetailsByIP(ip); - - // 54格大箱子 - org.bukkit.inventory.Inventory g = - org.bukkit.Bukkit.createInventory( - null, 54, - "§c§l选择要删除的账号"); - - // 填充玻璃面板 - org.bukkit.inventory.ItemStack glass = - new org.bukkit.inventory.ItemStack( - org.bukkit.Material - .RED_STAINED_GLASS_PANE); - org.bukkit.inventory.meta.ItemMeta gm = - glass.getItemMeta(); - if (gm != null) { - gm.setDisplayName(" "); - glass.setItemMeta(gm); - } - for (int i = 0; i < 54; i++) { - g.setItem(i, glass); - } - - // 展示账号列表(从0号位开始放) - java.text.SimpleDateFormat sdf = - new java.text.SimpleDateFormat( - "yyyy-MM-dd HH:mm"); - int slot = 0; - for (Map acc : accounts) { - if (slot >= 45) break; - String accName = (String) acc - .get("player_name"); - long regTime = ((Number) acc - .get("register_time")).longValue(); - String dateStr = regTime > 0 - ? sdf.format( - new java.util.Date(regTime)) - : "未知"; - - // 当前登录的账号用不同颜色标记 - boolean isCurrent = - accName.equalsIgnoreCase( - p.getName()); - - org.bukkit.inventory.ItemStack item = - new org.bukkit.inventory.ItemStack( - isCurrent - ? org.bukkit.Material - .RED_WOOL - : org.bukkit.Material - .ORANGE_WOOL); - org.bukkit.inventory.meta.ItemMeta im = - item.getItemMeta(); - if (im != null) { - im.setDisplayName( - (isCurrent ? "§c" : "§e") - + accName); - im.setLore(java.util.Arrays.asList( - "§7注册时间: " + dateStr, - "§7IP: " + ip, - isCurrent - ? "§c§l(当前账号)" - : "§a点击选择删除此账号")); - item.setItemMeta(im); - } - g.setItem(slot, item); - slot++; - } - - // 49号位:返回 - org.bukkit.inventory.ItemStack back = - new org.bukkit.inventory.ItemStack( - org.bukkit.Material.ARROW); - org.bukkit.inventory.meta.ItemMeta backMeta = - back.getItemMeta(); - if (backMeta != null) { - backMeta.setDisplayName("§7返回"); - back.setItemMeta(backMeta); - } - g.setItem(49, back); - - p.openInventory(g); - } - - /** - * 打开删除确认面板 - */ - public void openDeleteConfirm(Player p, - String targetName) { - org.bukkit.inventory.Inventory g = - org.bukkit.Bukkit.createInventory( - null, 27, - "§c§l确认删除: " + targetName); - - org.bukkit.inventory.ItemStack glass = - new org.bukkit.inventory.ItemStack( - org.bukkit.Material - .GRAY_STAINED_GLASS_PANE); - org.bukkit.inventory.meta.ItemMeta gm = - glass.getItemMeta(); - if (gm != null) { - gm.setDisplayName(" "); - glass.setItemMeta(gm); - } - for (int i = 0; i < 27; i++) { - g.setItem(i, glass); - } - - // 11号位:确认删除 - org.bukkit.inventory.ItemStack confirm = - new org.bukkit.inventory.ItemStack( - org.bukkit.Material.LIME_WOOL); - org.bukkit.inventory.meta.ItemMeta cm = - confirm.getItemMeta(); - if (cm != null) { - cm.setDisplayName("§a§l确认删除"); - cm.setLore(java.util.Arrays.asList( - "§7删除账号: §c" + targetName, - "§c此操作不可撤销!")); - confirm.setItemMeta(cm); - } - g.setItem(11, confirm); - - // 15号位:取消 - org.bukkit.inventory.ItemStack cancel = - new org.bukkit.inventory.ItemStack( - org.bukkit.Material.RED_WOOL); - org.bukkit.inventory.meta.ItemMeta cml = - cancel.getItemMeta(); - if (cml != null) { - cml.setDisplayName("§c§l取消"); - cancel.setItemMeta(cml); - } - g.setItem(15, cancel); - - p.openInventory(g); - } - - public void openShopAdmin(Player p) { - Inventory g = Bukkit.createInventory( - null, 54, "§c§l商城管理"); - ItemStack glass = new ItemStack( - Material.GRAY_STAINED_GLASS_PANE); - ItemMeta glm = glass.getItemMeta(); - if (glm != null) { - glm.setDisplayName(" "); - glass.setItemMeta(glm); - } - for (int i = 0; i < 54; i++) - g.setItem(i, glass); - g.setItem(49, mkItem(Material.ARROW, - "§7返回商城")); - g.setItem(50, mkItem(Material.EMERALD, - "§a添加商品", - "§7格式: /sdf1_login shopadd <名称> <价格> <命令>")); - g.setItem(51, mkItem(Material.BARRIER, - "§c删除商品", - "§7格式: /sdf1_login shopdel <序号>")); - p.openInventory(g); - } - - - public void openAdmin(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_ADMIN); - fillBg(g); - g.setItem(4, mkItem(Material.NAME_TAG, - "§e§l管理员面板")); - g.setItem(10, mkItem(Material.REDSTONE, - "§6SMTP配置", - "§7配置邮件服务器")); - g.setItem(11, mkItem(Material.ARMOR_STAND, - "§a用户管理", - "§7管理玩家数据")); - g.setItem(12, mkItem(Material.PLAYER_HEAD, - "§a§l我的信息")); - g.setItem(14, mkItem(Material.EMERALD, - "§d§l积分商城")); - boolean afkOn = plugin.getConfig2().afkEnabled; - int afkMin = plugin.getConfig2().afkTimeout / 60; - g.setItem(16, mkItem( - afkOn ? Material.REDSTONE_TORCH - : Material.TORCH, - afkOn ? "§a§l挂机踢出: 开启" - : "§c§l挂机踢出: 关闭", - "§7左键: 切换开关", - "§7右键: 设置时长", - "§7当前: " + afkMin + "分钟")); - g.setItem(22, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - - - public void openInvite(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_INVITE); - fillBg(g); - String code = (String) plugin.getDb() - .getField(p.getName(), "invite_code"); - if (code == null || code.isEmpty()) { - code = plugin.getInvite().generateCode(p); - } - int monthly = ((Number) plugin.getDb() - .getField(p.getName(), - "monthly_invite_count")) - .intValue(); - g.setItem(10, mkItem(Material.PAPER, - "§b§l我的邀请码", - "§7" + code, - "", - "§e点击刷新邀请码")); - g.setItem(12, mkItem(Material.PLAYER_HEAD, - "§a本月邀请", - "§7已邀请 §e" + monthly + " §7人")); - g.setItem(16, mkItem(Material.BOOK, - "§e输入邀请码", - "§7绑定他人的邀请码", - "", - "§e点击输入")); - g.setItem(26, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - - - private String getInviteRate(int count) { - if (count > 15) return "60%"; - if (count > 10) return "35%"; - if (count > 5) return "15%"; - if (count > 0) return "10%"; - return "0%"; - } - - public void openUserManagement(Player p) { - Inventory g = Bukkit.createInventory( - null, 54, T_USER_MGMT); - fillBg(g); - List> users = plugin.getDb() - .getAllUsers(); - int slot = 0; - for (Map user : users) { - if (slot >= 45) break; - String name = (String) user.get("player_name"); - int pts = ((Number) user.getOrDefault( - "points", 0)).intValue(); - int stage = ((Number) user.getOrDefault( - "gift_stage", 0)).intValue(); - g.setItem(slot, mkItem(Material.PLAYER_HEAD, - "§e" + name, - "§7积分: " + pts - + " 礼包阶段: " + stage, - "§7双击管理")); - slot++; - } - g.setItem(53, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - - public void openTaskCenter(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_TASK_CENTER); - fillBg(g); - g.setItem(10, mkItem(Material.BOOK, - "§e§l新人任务", - "§7完成新手阶段获取奖励")); - g.setItem(12, mkItem(Material.COMPASS, - "§b§l主线任务", - "§7查看主线任务列表")); - g.setItem(14, mkItem(Material.PAPER, - "§a§l支线任务", - "§7查看支线任务列表")); - g.setItem(16, mkItem(Material.EMERALD, - "§d§l每日签到", - "§7签到获取积分")); - g.setItem(22, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - public void openGiftStages(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_GIFT_STAGES); - fillBg(g); - int max = plugin.getGift().getMaxStage(); - for (int i = 0; i < max && i < 9; i++) { - int stage = i + 1; - boolean claimed = plugin.getDb() - .getField(p.getName(), - "gift_claimed") != null - && ((String) plugin.getDb() - .getField(p.getName(), - "gift_claimed")) - .contains("[" + stage + "]"); - boolean can = plugin.getGift() - .canClaim(p, stage); - Material mat = claimed ? Material.LIME_WOOL - : (can ? Material.YELLOW_WOOL - : Material.RED_WOOL); - String status = claimed ? "§a已领取" - : (can ? "§e可领取" : "§c未达标"); - g.setItem(10 + i, mkItem(mat, - "§e第" + stage + "阶段", - "§7" + status)); - } - g.setItem(26, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - - public void openTaskList(Player p, String folderName) { - File folder = new File( - plugin.getDataFolder(), folderName); - List> tasks = - plugin.getGift() - .loadTasksFromFolder(folder); - int size = Math.max(54, - ((tasks.size() / 9) + 1) * 9); - if (size > 54) size = 54; - Inventory g = Bukkit.createInventory(null, size, - "§d§l" + folderName); - fillBg(g); - for (int i = 0; i < tasks.size() - && i < 45; i++) { - Map task = tasks.get(i); - String name = (String) task.get("name"); - List conds = - (List) task.get("conditions"); - List rewards = - (List) task.get("rewards"); - List lore = new ArrayList<>(); - lore.add("§7───── 条件 ─────"); - if (conds != null) - for (String c : conds) - lore.add("§e" + c); - lore.add(""); - lore.add("§7───── 奖励 ─────"); - if (rewards != null) - for (String r2 : rewards) - lore.add("§a" + r2); - g.setItem(i, mkItem(Material.BOOK, - "§e" + name, - lore.toArray(new String[0]))); - } - if (tasks.isEmpty()) { - g.setItem(22, mkItem(Material.BARRIER, - "§7暂无任务")); - } - g.setItem(size - 1, - mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - - - public void openUserDetail(Player p, String target) { - Inventory g = Bukkit.createInventory( - null, 27, "§e§l管理: " + target); - fillBg(g); - Map user = plugin.getDb() - .getUser(target); - int pts = ((Number) user.getOrDefault( - "points", 0)).intValue(); - String email = (String) user.getOrDefault( - "email", ""); - g.setItem(4, mkItem(Material.NAME_TAG, - "§e" + target)); - g.setItem(10, mkItem(Material.EMERALD, - "§a积分: " + pts, - "§7点击设置积分")); - g.setItem(11, mkItem(Material.PAPER, - "§e邮箱: " - + (email.isEmpty() ? "无" : email))); - g.setItem(12, mkItem(Material.BOOK, - "§e设置密码")); - g.setItem(16, mkItem(Material.BARRIER, - "§c删除用户", "§7二次验证删除")); - g.setItem(22, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - - /** - * 打开账号请求面板 - * 玩家通过 /oa 命令触发 - */ - public void openAccountRequest(Player p) { - org.bukkit.inventory.Inventory g = - org.bukkit.Bukkit.createInventory( - null, 27, - "§e§l账号请求"); - - // 填充玻璃面板 - org.bukkit.inventory.ItemStack glass = - new org.bukkit.inventory.ItemStack( - org.bukkit.Material - .YELLOW_STAINED_GLASS_PANE); - org.bukkit.inventory.meta.ItemMeta gm = - glass.getItemMeta(); - if (gm != null) { - gm.setDisplayName(" "); - glass.setItemMeta(gm); - } - for (int i = 0; i < 27; i++) { - g.setItem(i, glass); - } - - // 10号位:申请删除账号 - g.setItem(10, makeItem( - org.bukkit.Material.BARRIER, - "§c§l申请删除账号", - "§7点击后确认删除您的账号", - "§7此操作不可撤销")); - - // 13号位:找回密码 - g.setItem(13, makeItem( - org.bukkit.Material.TRIPWIRE_HOOK, - "§e§l找回密码", - "§7通过绑定邮箱获取临时密码", - "§7需要先绑定邮箱")); - - // 16号位:联系管理员 - g.setItem(16, makeItem( - org.bukkit.Material.EMERALD, - "§a§l联系管理员", - "§7遇到问题?向管理员提交工单")); - - // 22号位:返回主菜单 - g.setItem(22, makeItem( - org.bukkit.Material.ARROW, - "§7返回")); - - p.openInventory(g); - } - - /** - * 创建物品的工具方法 - */ - private org.bukkit.inventory.ItemStack makeItem( - org.bukkit.Material mat, - String displayName, - String... lore) { - org.bukkit.inventory.ItemStack item = - new org.bukkit.inventory.ItemStack(mat); - org.bukkit.inventory.meta.ItemMeta meta = - item.getItemMeta(); - if (meta != null) { - meta.setDisplayName(displayName); - if (lore.length > 0) { - meta.setLore( - java.util.Arrays.asList(lore)); - } - item.setItemMeta(meta); - } - return item; - } - - - private boolean isAdmin(Player p) { - return p.getScoreboardTags().contains( - plugin.getConfig2().adminTag); - } - - private void fillBg(Inventory inv) { - ItemStack gl = mkItem( - Material.GRAY_STAINED_GLASS_PANE, " "); - for (int i = 0; i < inv.getSize(); i++) - inv.setItem(i, gl); - } - - private ItemStack mkItem(Material mat, String name, - String... lore) { - ItemStack it = new ItemStack(mat); - ItemMeta im = it.getItemMeta(); - if (im != null) { - im.setDisplayName(name); - if (lore.length > 0) - im.setLore(Arrays.asList(lore)); - it.setItemMeta(im); - } - return it; - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.*; +import org.bukkit.inventory.meta.ItemMeta; +import java.io.File; + +import java.util.*; + +public class GUIManager { + + private final Main plugin; + + public static final String T_MAIN = + "§6§l登录面板"; + public static final String T_MY_INFO = + "§a§l我的信息"; + public static final String T_ADMIN = + "§c§l管理员面板"; + public static final String T_USER_MGMT = + "§e§l用户管理"; + public static final String T_INVITE = + "§b§l邀请数据"; + public static final String T_TASK_CENTER = "§d§l任务中心"; + public static final String T_GIFT_STAGES = "§d§l新人任务"; + + + public GUIManager(Main plugin) { + this.plugin = plugin; + } + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_MAIN); + fillBg(g); + g.setItem(10, mkItem(Material.PLAYER_HEAD, + "§a§l我的信息")); + g.setItem(12, mkItem(Material.PAPER, + "§b§l邀请数据")); + g.setItem(14, mkItem(Material.EMERALD, + "§d§l积分商城")); + g.setItem(16, mkItem(Material.CHEST, + "§e§l新人礼包")); + g.setItem(18, mkItem(Material.BOOK, + "§6工单系统", + "§7提交bug、求助、举报")); + if (p.isOp() || isAdmin(p)) { + g.setItem(20, mkItem(Material.CHEST, + "§6垃圾回收站", + "§7查看和取回清理的物品")); + } + if (isAdmin(p)) { + g.setItem(22, mkItem(Material.REDSTONE_BLOCK, + "§c§l管理员面板")); + } + p.openInventory(g); + } + + public void openMyInfo(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_MY_INFO); + fillBg(g); + Map user = plugin.getDb() + .getUser(p.getName()); + boolean checkedIn = plugin.getCheckIn() + .isCheckedInToday(p.getName()); + Material checkMat = checkedIn + ? Material.EMERALD : Material.COAL; + String checkName = checkedIn + ? "§a已签到" : "§7点击签到"; + int streak = ((Number) user.getOrDefault( + "checkin_streak", 0)).intValue(); + int points = ((Number) user.getOrDefault( + "points", 0)).intValue(); + int totalDays = ((Number) user.getOrDefault( + "total_checkin_days", 0)).intValue(); + String email = (String) user.getOrDefault( + "email", ""); + g.setItem(4, mkItem(Material.NAME_TAG, + "§e" + p.getName())); + g.setItem(10, mkItem(checkMat, checkName, + "§7连续签到: " + streak + "天", + "§7累计签到: " + totalDays + "天", + "§7当前积分: " + points)); + g.setItem(11, mkItem(Material.PAPER, + "§e修改密码", "§7双击修改密码")); + g.setItem(12, mkItem(Material.CLOCK, + "§e补签", "§7消耗10积分补签3天内")); + g.setItem(13, mkItem(Material.ENDER_PEARL, + "§e邀请码", + "§7你的邀请码: " + + user.getOrDefault( + "invite_code", "无"), + "§7点击生成/查看")); + g.setItem(14, mkItem(Material.BOOK, + "§e邮箱: " + + (email.isEmpty() + ? "未绑定" : email), + "§7点击绑定邮箱")); + g.setItem(15, mkItem(Material.EMERALD_BLOCK, + "§a§l积分商城", + "§7当前: " + points + "积分")); + g.setItem(26, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + /** + * 打开账号删除选择面板 + * 展示该玩家IP下所有账号,供选择删除 + */ + public void openAccountDelete(Player p) { + String ip = plugin.getPlayerIP(p); + if (ip == null) { + p.sendMessage("§c无法获取你的IP地址"); + return; + } + + List> accounts = + plugin.getDb().getAccountDetailsByIP(ip); + + // 54格大箱子 + org.bukkit.inventory.Inventory g = + org.bukkit.Bukkit.createInventory( + null, 54, + "§c§l选择要删除的账号"); + + // 填充玻璃面板 + org.bukkit.inventory.ItemStack glass = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material + .RED_STAINED_GLASS_PANE); + org.bukkit.inventory.meta.ItemMeta gm = + glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 54; i++) { + g.setItem(i, glass); + } + + // 展示账号列表(从0号位开始放) + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm"); + int slot = 0; + for (Map acc : accounts) { + if (slot >= 45) break; + String accName = (String) acc + .get("player_name"); + long regTime = ((Number) acc + .get("register_time")).longValue(); + String dateStr = regTime > 0 + ? sdf.format( + new java.util.Date(regTime)) + : "未知"; + + // 当前登录的账号用不同颜色标记 + boolean isCurrent = + accName.equalsIgnoreCase( + p.getName()); + + org.bukkit.inventory.ItemStack item = + new org.bukkit.inventory.ItemStack( + isCurrent + ? org.bukkit.Material + .RED_WOOL + : org.bukkit.Material + .ORANGE_WOOL); + org.bukkit.inventory.meta.ItemMeta im = + item.getItemMeta(); + if (im != null) { + im.setDisplayName( + (isCurrent ? "§c" : "§e") + + accName); + im.setLore(java.util.Arrays.asList( + "§7注册时间: " + dateStr, + "§7IP: " + ip, + isCurrent + ? "§c§l(当前账号)" + : "§a点击选择删除此账号")); + item.setItemMeta(im); + } + g.setItem(slot, item); + slot++; + } + + // 49号位:返回 + org.bukkit.inventory.ItemStack back = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material.ARROW); + org.bukkit.inventory.meta.ItemMeta backMeta = + back.getItemMeta(); + if (backMeta != null) { + backMeta.setDisplayName("§7返回"); + back.setItemMeta(backMeta); + } + g.setItem(49, back); + + p.openInventory(g); + } + + /** + * 打开删除确认面板 + */ + public void openDeleteConfirm(Player p, + String targetName) { + org.bukkit.inventory.Inventory g = + org.bukkit.Bukkit.createInventory( + null, 27, + "§c§l确认删除: " + targetName); + + org.bukkit.inventory.ItemStack glass = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material + .GRAY_STAINED_GLASS_PANE); + org.bukkit.inventory.meta.ItemMeta gm = + glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 27; i++) { + g.setItem(i, glass); + } + + // 11号位:确认删除 + org.bukkit.inventory.ItemStack confirm = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material.LIME_WOOL); + org.bukkit.inventory.meta.ItemMeta cm = + confirm.getItemMeta(); + if (cm != null) { + cm.setDisplayName("§a§l确认删除"); + cm.setLore(java.util.Arrays.asList( + "§7删除账号: §c" + targetName, + "§c此操作不可撤销!")); + confirm.setItemMeta(cm); + } + g.setItem(11, confirm); + + // 15号位:取消 + org.bukkit.inventory.ItemStack cancel = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material.RED_WOOL); + org.bukkit.inventory.meta.ItemMeta cml = + cancel.getItemMeta(); + if (cml != null) { + cml.setDisplayName("§c§l取消"); + cancel.setItemMeta(cml); + } + g.setItem(15, cancel); + + p.openInventory(g); + } + + public void openShopAdmin(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§c§l商城管理"); + ItemStack glass = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = glass.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + glass.setItemMeta(glm); + } + for (int i = 0; i < 54; i++) + g.setItem(i, glass); + g.setItem(49, mkItem(Material.ARROW, + "§7返回商城")); + g.setItem(50, mkItem(Material.EMERALD, + "§a添加商品", + "§7格式: /sdf1_login shopadd <名称> <价格> <命令>")); + g.setItem(51, mkItem(Material.BARRIER, + "§c删除商品", + "§7格式: /sdf1_login shopdel <序号>")); + p.openInventory(g); + } + + + public void openAdmin(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_ADMIN); + fillBg(g); + g.setItem(4, mkItem(Material.NAME_TAG, + "§e§l管理员面板")); + g.setItem(10, mkItem(Material.REDSTONE, + "§6SMTP配置", + "§7配置邮件服务器")); + g.setItem(11, mkItem(Material.ARMOR_STAND, + "§a用户管理", + "§7管理玩家数据")); + g.setItem(12, mkItem(Material.PLAYER_HEAD, + "§a§l我的信息")); + g.setItem(14, mkItem(Material.EMERALD, + "§d§l积分商城")); + boolean afkOn = plugin.getConfig2().afkEnabled; + int afkMin = plugin.getConfig2().afkTimeout / 60; + g.setItem(16, mkItem( + afkOn ? Material.REDSTONE_TORCH + : Material.TORCH, + afkOn ? "§a§l挂机踢出: 开启" + : "§c§l挂机踢出: 关闭", + "§7左键: 切换开关", + "§7右键: 设置时长", + "§7当前: " + afkMin + "分钟")); + g.setItem(22, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + public void openInvite(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_INVITE); + fillBg(g); + String code = (String) plugin.getDb() + .getField(p.getName(), "invite_code"); + if (code == null || code.isEmpty()) { + code = plugin.getInvite().generateCode(p); + } + int monthly = ((Number) plugin.getDb() + .getField(p.getName(), + "monthly_invite_count")) + .intValue(); + g.setItem(10, mkItem(Material.PAPER, + "§b§l我的邀请码", + "§7" + code, + "", + "§e点击刷新邀请码")); + g.setItem(12, mkItem(Material.PLAYER_HEAD, + "§a本月邀请", + "§7已邀请 §e" + monthly + " §7人")); + g.setItem(16, mkItem(Material.BOOK, + "§e输入邀请码", + "§7绑定他人的邀请码", + "", + "§e点击输入")); + g.setItem(26, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + private String getInviteRate(int count) { + if (count > 15) return "60%"; + if (count > 10) return "35%"; + if (count > 5) return "15%"; + if (count > 0) return "10%"; + return "0%"; + } + + public void openUserManagement(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_USER_MGMT); + fillBg(g); + List> users = plugin.getDb() + .getAllUsers(); + int slot = 0; + for (Map user : users) { + if (slot >= 45) break; + String name = (String) user.get("player_name"); + int pts = ((Number) user.getOrDefault( + "points", 0)).intValue(); + int stage = ((Number) user.getOrDefault( + "gift_stage", 0)).intValue(); + g.setItem(slot, mkItem(Material.PLAYER_HEAD, + "§e" + name, + "§7积分: " + pts + + " 礼包阶段: " + stage, + "§7双击管理")); + slot++; + } + g.setItem(53, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + public void openTaskCenter(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_TASK_CENTER); + fillBg(g); + g.setItem(10, mkItem(Material.BOOK, + "§e§l新人任务", + "§7完成新手阶段获取奖励")); + g.setItem(12, mkItem(Material.COMPASS, + "§b§l主线任务", + "§7查看主线任务列表")); + g.setItem(14, mkItem(Material.PAPER, + "§a§l支线任务", + "§7查看支线任务列表")); + g.setItem(16, mkItem(Material.EMERALD, + "§d§l每日签到", + "§7签到获取积分")); + g.setItem(22, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + public void openGiftStages(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_GIFT_STAGES); + fillBg(g); + int max = plugin.getGift().getMaxStage(); + for (int i = 0; i < max && i < 9; i++) { + int stage = i + 1; + boolean claimed = plugin.getDb() + .getField(p.getName(), + "gift_claimed") != null + && ((String) plugin.getDb() + .getField(p.getName(), + "gift_claimed")) + .contains("[" + stage + "]"); + boolean can = plugin.getGift() + .canClaim(p, stage); + Material mat = claimed ? Material.LIME_WOOL + : (can ? Material.YELLOW_WOOL + : Material.RED_WOOL); + String status = claimed ? "§a已领取" + : (can ? "§e可领取" : "§c未达标"); + g.setItem(10 + i, mkItem(mat, + "§e第" + stage + "阶段", + "§7" + status)); + } + g.setItem(26, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + public void openTaskList(Player p, String folderName) { + File folder = new File( + plugin.getDataFolder(), folderName); + List> tasks = + plugin.getGift() + .loadTasksFromFolder(folder); + int size = Math.max(54, + ((tasks.size() / 9) + 1) * 9); + if (size > 54) size = 54; + Inventory g = Bukkit.createInventory(null, size, + "§d§l" + folderName); + fillBg(g); + for (int i = 0; i < tasks.size() + && i < 45; i++) { + Map task = tasks.get(i); + String name = (String) task.get("name"); + List conds = + (List) task.get("conditions"); + List rewards = + (List) task.get("rewards"); + List lore = new ArrayList<>(); + lore.add("§7───── 条件 ─────"); + if (conds != null) + for (String c : conds) + lore.add("§e" + c); + lore.add(""); + lore.add("§7───── 奖励 ─────"); + if (rewards != null) + for (String r2 : rewards) + lore.add("§a" + r2); + g.setItem(i, mkItem(Material.BOOK, + "§e" + name, + lore.toArray(new String[0]))); + } + if (tasks.isEmpty()) { + g.setItem(22, mkItem(Material.BARRIER, + "§7暂无任务")); + } + g.setItem(size - 1, + mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + public void openUserDetail(Player p, String target) { + Inventory g = Bukkit.createInventory( + null, 27, "§e§l管理: " + target); + fillBg(g); + Map user = plugin.getDb() + .getUser(target); + int pts = ((Number) user.getOrDefault( + "points", 0)).intValue(); + String email = (String) user.getOrDefault( + "email", ""); + g.setItem(4, mkItem(Material.NAME_TAG, + "§e" + target)); + g.setItem(10, mkItem(Material.EMERALD, + "§a积分: " + pts, + "§7点击设置积分")); + g.setItem(11, mkItem(Material.PAPER, + "§e邮箱: " + + (email.isEmpty() ? "无" : email))); + g.setItem(12, mkItem(Material.BOOK, + "§e设置密码")); + g.setItem(16, mkItem(Material.BARRIER, + "§c删除用户", "§7二次验证删除")); + // 雪球菜单检测(slot 14) + g.setItem(14, mkItem( + Material.SNOWBALL, + "§e§l雪球菜单", + "§7点击为该玩家发放菜单雪球")); + + g.setItem(22, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + /** + * 打开账号请求面板 + * 玩家通过 /oa 命令触发 + */ + public void openAccountRequest(Player p) { + org.bukkit.inventory.Inventory g = + org.bukkit.Bukkit.createInventory( + null, 27, + "§e§l账号请求"); + + // 填充玻璃面板 + org.bukkit.inventory.ItemStack glass = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material + .YELLOW_STAINED_GLASS_PANE); + org.bukkit.inventory.meta.ItemMeta gm = + glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 27; i++) { + g.setItem(i, glass); + } + + // 10号位:申请删除账号 + g.setItem(10, makeItem( + org.bukkit.Material.BARRIER, + "§c§l申请删除账号", + "§7点击后确认删除您的账号", + "§7此操作不可撤销")); + + // 13号位:找回密码 + g.setItem(13, makeItem( + org.bukkit.Material.TRIPWIRE_HOOK, + "§e§l找回密码", + "§7通过绑定邮箱获取临时密码", + "§7需要先绑定邮箱")); + + // 16号位:联系管理员 + g.setItem(16, makeItem( + org.bukkit.Material.EMERALD, + "§a§l联系管理员", + "§7遇到问题?向管理员提交工单")); + + // 22号位:返回主菜单 + g.setItem(22, makeItem( + org.bukkit.Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + /** + * 创建物品的工具方法 + */ + private org.bukkit.inventory.ItemStack makeItem( + org.bukkit.Material mat, + String displayName, + String... lore) { + org.bukkit.inventory.ItemStack item = + new org.bukkit.inventory.ItemStack(mat); + org.bukkit.inventory.meta.ItemMeta meta = + item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(displayName); + if (lore.length > 0) { + meta.setLore( + java.util.Arrays.asList(lore)); + } + item.setItemMeta(meta); + } + return item; + } + + + private boolean isAdmin(Player p) { + return p.getScoreboardTags().contains( + plugin.getConfig2().adminTag); + } + + private void fillBg(Inventory inv) { + ItemStack gl = mkItem( + Material.GRAY_STAINED_GLASS_PANE, " "); + for (int i = 0; i < inv.getSize(); i++) + inv.setItem(i, gl); + } + + private ItemStack mkItem(Material mat, String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/src/main/java/Sdf1_login/GarbageManager.java b/src/main/java/Sdf1_login/GarbageManager.java index b4da32c..26c379d 100644 --- a/src/main/java/Sdf1_login/GarbageManager.java +++ b/src/main/java/Sdf1_login/GarbageManager.java @@ -1,568 +1,610 @@ -package Sdf1_login; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Item; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.scheduler.BukkitRunnable; - -import java.io.*; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.sql.*; -import java.sql.Date; -import java.text.SimpleDateFormat; -import java.util.*; - -public class GarbageManager { - - private final Main plugin; - private Connection db; - private int cleanupInterval = 1800; - private int maxRounds = 1; - private boolean enabled = true; - private int currentRound = 0; - private final Map - recyclePages = new HashMap<>(); - - - public GarbageManager(Main plugin) { - this.plugin = plugin; - } - - public void init() { - try { - Class.forName("org.sqlite.JDBC"); - File dbFile = new File( - plugin.getDataFolder(), - "garbage.db"); - db = DriverManager.getConnection( - "jdbc:sqlite:" - + dbFile.getAbsolutePath()); - Statement st = db.createStatement(); - st.execute("PRAGMA journal_mode=WAL"); - st.execute("CREATE TABLE IF NOT EXISTS " - + "items (" - + "id INTEGER PRIMARY KEY " - + "AUTOINCREMENT," - + "item_data TEXT NOT NULL," - + "amount INTEGER DEFAULT 1," - + "round INTEGER DEFAULT 0," - + "create_time INTEGER NOT NULL" - + ")"); - st.execute("CREATE TABLE IF NOT EXISTS " - + "config (" - + "key TEXT PRIMARY KEY," - + "value TEXT NOT NULL" - + ")"); - st.close(); - } catch (Exception e) { - plugin.getLogger().severe( - "[Garbage] DB失败: " - + e.getMessage()); - } - } - - public void loadConfig() { - // 从 ConfigManager 读取 - cleanupInterval = - plugin.getConfig2() - .garbageInterval; - enabled = - plugin.getConfig2() - .garbageEnabled; - maxRounds = - plugin.getConfig2() - .garbageMaxRounds; - - // 从数据库读取当前轮次 - try { - String val = getConfigValue("round"); - if (val != null) - currentRound = - Integer.parseInt(val); - } catch (Exception ignored) { - } - - plugin.getLogger().info( - "[Garbage] 间隔: " - + cleanupInterval + "秒, " - + "轮次保留: " - + maxRounds + "轮, " - + "启用: " + enabled); - } - - - private String getConfigValue(String key) { - try { - PreparedStatement ps = db.prepareStatement( - "SELECT value FROM config " - + "WHERE key=?"); - ps.setString(1, key); - ResultSet rs = ps.executeQuery(); - String val = rs.next() - ? rs.getString("value") : null; - rs.close(); - ps.close(); - return val; - } catch (Exception e) { - return null; - } - } - - private void setConfigValue(String key, - String value) { - try { - PreparedStatement ps = db.prepareStatement( - "INSERT OR REPLACE INTO config " - + "(key, value) " - + "VALUES (?, ?)"); - ps.setString(1, key); - ps.setString(2, value); - ps.executeUpdate(); - ps.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void startAutoCleanup() { - if (!enabled) return; - new BukkitRunnable() { - int countdown = 0; - - @Override - public void run() { - // 在线人数≤0时暂停 - if (Bukkit.getOnlinePlayers() - .isEmpty()) { - countdown = 0; - return; - } - - // 重新读取配置(支持热更) - cleanupInterval = - plugin.getConfig2() - .garbageInterval; - - int remain = - cleanupInterval - countdown; - if (remain == 300) - broadcast("§e§l[垃圾站] §f距离清理还有 §e5分钟"); - else if (remain == 180) - broadcast("§e§l[垃圾站] §f距离清理还有 §e3分钟"); - else if (remain == 60) - broadcast("§e§l[垃圾站] §f距离清理还有 §e1分钟"); - else if (remain == 30) - broadcast("§e§l[垃圾站] §f距离清理还有 §e30秒"); - else if (remain == 10) - broadcast("§e§l[垃圾站] §f距离清理还有 §e10秒"); - else if (remain <= 5 && remain > 0) - broadcast("§e§l[垃圾站] §c" + remain + "§f..."); - else if (remain == 0) { - doCleanup(); - countdown = 0; - return; - } - countdown++; - } - }.runTaskTimer(plugin, 20L, 20L); - } - - private void doCleanup() { - // 删除过期轮次的物品 - int deleteRound = - currentRound - maxRounds; - if (deleteRound >= 0) { - int deleted = deleteOldItems( - deleteRound); - if (deleted > 0) { - broadcast("§7[垃圾站] §f永久清理了 §e" - + deleted + " §f个过期物品"); - } - } - - // 递增轮次 - currentRound++; - setConfigValue("round", - String.valueOf(currentRound)); - - // 收集当前掉落物 - int count = collectItems(); - broadcast("§a§l[垃圾站] §f清理完成," - + "共清理 §e" + count - + " §f个掉落物,存入回收站"); - - // 打印下次清理时间 - long nextMs = System.currentTimeMillis() - + (long) cleanupInterval * 1000L; - String nextTime = - new SimpleDateFormat("HH:mm:ss") - .format(new Date(nextMs)); - broadcast("§e§l[垃圾站] §f下次清理时间: §a" - + nextTime); - } - - public int collectItems() { - int count = 0; - for (org.bukkit.World w : - Bukkit.getWorlds()) { - Collection items = - w.getEntitiesByClass(Item.class); - for (Item item : items) { - ItemStack stack = - item.getItemStack(); - if (stack == null - || stack.getType() - == Material.AIR) - continue; - saveItem(stack); - item.remove(); - count++; - } - } - return count; - } - - public void saveItem(ItemStack stack) { - { - try { - String data = - stackToBase64(stack); - if (data.isEmpty()) return; - PreparedStatement ps = db.prepareStatement( - "INSERT INTO items " - + "(item_data, amount, " - + "round, create_time) " - + "VALUES (?,?,?,?)"); - ps.setString(1, data); - ps.setInt(2, stack.getAmount()); - ps.setInt(3, currentRound); - ps.setLong(4, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (Exception e) { - e.printStackTrace(); - } - }} - - private int deleteOldItems(int beforeRound) { - try { - PreparedStatement ps = db.prepareStatement( - "DELETE FROM items " - + "WHERE round <= ?"); - ps.setInt(1, beforeRound); - int rows = ps.executeUpdate(); - ps.close(); - return rows; - } catch (Exception e) { - return 0; - } - } - - // ========== 序列化 ========== - - private String stackToBase64(ItemStack stack) { - try { - Map map = - stack.serialize(); - String json = new Gson().toJson(map); - return Base64.getEncoder() - .encodeToString( - json.getBytes( - StandardCharsets.UTF_8)); - } catch (Exception e) { - return ""; - } - } - - private ItemStack base64ToStack(String base64) { - try { - byte[] bytes = Base64.getDecoder() - .decode(base64); - String json = new String(bytes, - StandardCharsets.UTF_8); - Type type = new TypeToken< - Map>() {} - .getType(); - Map map = - new Gson().fromJson(json, type); - map = fixNumberTypes(map); - return ItemStack.deserialize(map); - } catch (Exception e) { - return null; - } - } - - @SuppressWarnings("unchecked") - private Map fixNumberTypes( - Map map) { - Map result = - new LinkedHashMap<>(); - for (Map.Entry e - : map.entrySet()) { - Object v = e.getValue(); - if (v instanceof Double) { - double d = (Double) v; - if (d == (long) d) { - result.put(e.getKey(), (long) d); - } else { - result.put(e.getKey(), d); - } - } else if (v instanceof Map) { - result.put(e.getKey(), - fixNumberTypes( - (Map) v)); - } else if (v instanceof List) { - List list = - new ArrayList<>(); - for (Object item : (List) v) { - if (item instanceof Map) { - list.add(fixNumberTypes( - (Map) item)); - } else { - list.add(item); - } - } - result.put(e.getKey(), list); - } else { - result.put(e.getKey(), v); - } - } - return result; - } - - // ========== 回收站GUI ========== - public void openRecycle(Player p) { - openRecyclePage(p, 0); - } - - public void openRecyclePage(Player p, int page) { - - Inventory g = Bukkit.createInventory( - null, 54, "§6§l垃圾回收站"); - - // 获取全部存储物品 - List> allItems = - getStoredItems(999); - int total = allItems.size(); - int pageSize = 40; - int totalPages = Math.max(1, - (int) Math.ceil( - (double) total / pageSize)); - - // 修正页码 - if (page < 0) page = 0; - if (page >= totalPages) - page = totalPages - 1; - - recyclePages.put(p.getName(), page); - - // 填充物品(0-39 格 = 第1页,40-79 = 第2页...) - int start = page * pageSize; - int end = Math.min(start + pageSize, - total); - int slot = 0; - for (int i = start; i < end; i++) { - if (slot >= 40) break; - Map item = - allItems.get(i); - int id = ((Number) item.get("id")) - .intValue(); - String data = (String) item - .get("item_data"); - int amount = ((Number) item - .get("amount")).intValue(); - ItemStack stack = base64ToStack(data); - if (stack != null) { - stack.setAmount(amount); - ItemMeta im = - stack.getItemMeta(); - if (im != null) { - List lore = - new ArrayList<>(); - lore.add("§7ID: #" + id); - lore.add("§a点击取出"); - im.setLore(lore); - stack.setItemMeta(im); - } - g.setItem(slot, stack); - } - slot++; - } - - // 45-48 格:额外投放区(空位) - // 52-53 格:额外投放区(空位) - - // 49 格:翻页-上一页 - if (page > 0) { - g.setItem(49, mk(Material.ARROW, - "§e上一页 §7(" - + page + "/" - + totalPages + ")")); - } else { - g.setItem(49, mk(Material.ARROW, - "§e第一页 §7(" - + "1/" + totalPages + ")")); - } - - // 50 格:统计信息 - g.setItem(50, mk(Material.CHEST, - "§e回收站统计", - "§7总存量: §f" + total + " 个", - "§7当前页: §f" + (page + 1) - + "/" + totalPages)); - - // 51 格:翻页-下一页 - boolean canNext = total >= pageSize - && page < totalPages - 1; - if (canNext) { - g.setItem(51, mk(Material.ARROW, - "§e下一页 §7(" - + (page + 2) + "/" - + totalPages + ")")); - } else { - g.setItem(51, mk(Material.ARROW, - "§7已是最后一页")); - } - - p.openInventory(g); - } - public int getRecyclePage(String name) { - return recyclePages - .getOrDefault(name, 0); - } - - - public boolean handleClick(Player p, - String title, int slot) { - if (!title.equals("§6§l垃圾回收站")) - return false; - // 49:上一页 - if (slot == 49) { - int page = recyclePages - .getOrDefault(p.getName(), 0); - if (page > 0) - openRecyclePage(p, page - 1); - return true; - } - // 51:下一页 - if (slot == 51) { - int page = recyclePages - .getOrDefault(p.getName(), 0); - openRecyclePage(p, page + 1); - return true; - } - // 50/52/53:不处理 - if (slot >= 49 && slot <= 53) - return true; - return true; - } - - private List> - getStoredItems(int limit) { - List> list = - new ArrayList<>(); - try { - PreparedStatement ps = - db.prepareStatement( - "SELECT id, item_data, " - + "amount " - + "FROM items " - + "ORDER BY " - + "create_time " - + "DESC LIMIT ?"); - ps.setInt(1, limit); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - Map row = - new LinkedHashMap<>(); - row.put("id", - rs.getInt("id")); - row.put("item_data", - rs.getString("item_data")); - row.put("amount", - rs.getInt("amount")); - list.add(row); - } - rs.close(); - ps.close(); - } catch (Exception e) { - e.printStackTrace(); - } - return list; - } - - public boolean removeItem(int id) { - try { - PreparedStatement ps = - db.prepareStatement( - "DELETE FROM items " - + "WHERE id=?"); - ps.setInt(1, id); - int rows = ps.executeUpdate(); - ps.close(); - return rows > 0; - } catch (Exception e) { - return false; - } - } - - public int getItemCount() { - try { - Statement st = db.createStatement(); - ResultSet rs = st.executeQuery( - "SELECT COUNT(*) FROM items"); - int count = 0; - if (rs.next()) - count = rs.getInt(1); - rs.close(); - st.close(); - return count; - } catch (Exception e) { - return 0; - } - } - - private void broadcast(String msg) { - // 同步打印到控制台 - plugin.getLogger().info( - msg.replaceAll("§[0-9a-fk-orA-FK-OR]", "")); - // 同步打印到聊天栏 - Bukkit.broadcastMessage(msg); - } - - - private ItemStack mk(Material mat, - String name, String... lore) { - ItemStack it = new ItemStack(mat); - ItemMeta im = it.getItemMeta(); - if (im != null) { - im.setDisplayName(name); - if (lore.length > 0) { - List list = - new ArrayList<>(); - for (String l : lore) - if (!l.isEmpty()) list.add(l); - if (!list.isEmpty()) - im.setLore(list); - } - it.setItemMeta(im); - } - return it; - } - - public void close() { - try { - if (db != null && !db.isClosed()) - db.close(); - } catch (Exception ignored) { - } - } -} +package Sdf1_login; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.*; + +public class GarbageManager { + + private final Main plugin; + private Connection db; + private int cleanupInterval = 1800; + private int maxRounds = 1; + private boolean enabled = true; + private int currentRound = 0; + private final Map + recyclePages = new HashMap<>(); + + + public GarbageManager(Main plugin) { + this.plugin = plugin; + } + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), + "garbage.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS " + + "items (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "item_data TEXT NOT NULL," + + "amount INTEGER DEFAULT 1," + + "round INTEGER DEFAULT 0," + + "create_time INTEGER NOT NULL" + + ")"); + st.execute("CREATE TABLE IF NOT EXISTS " + + "config (" + + "key TEXT PRIMARY KEY," + + "value TEXT NOT NULL" + + ")"); + st.close(); + } catch (Exception e) { + plugin.getLogger().severe( + "[Garbage] DB失败: " + + e.getMessage()); + } + } + + public void loadConfig() { + // 从 ConfigManager 读取 + cleanupInterval = + plugin.getConfig2() + .garbageInterval; + enabled = + plugin.getConfig2() + .garbageEnabled; + maxRounds = + plugin.getConfig2() + .garbageMaxRounds; + + // 从数据库读取当前轮次 + try { + String val = getConfigValue("round"); + if (val != null) + currentRound = + Integer.parseInt(val); + } catch (Exception ignored) { + } + + plugin.getLogger().info( + "[Garbage] 间隔: " + + cleanupInterval + "秒, " + + "轮次保留: " + + maxRounds + "轮, " + + "启用: " + enabled); + } + + + private String getConfigValue(String key) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT value FROM config " + + "WHERE key=?"); + ps.setString(1, key); + ResultSet rs = ps.executeQuery(); + String val = rs.next() + ? rs.getString("value") : null; + rs.close(); + ps.close(); + return val; + } catch (Exception e) { + return null; + } + } + + private void setConfigValue(String key, + String value) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO config " + + "(key, value) " + + "VALUES (?, ?)"); + ps.setString(1, key); + ps.setString(2, value); + ps.executeUpdate(); + ps.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void startAutoCleanup() { + if (!enabled) return; + new BukkitRunnable() { + int countdown = 0; + + @Override + public void run() { + // 在线人数≤0时暂停 + if (Bukkit.getOnlinePlayers() + .isEmpty()) { + countdown = 0; + return; + } + + // 重新读取配置(支持热更) + cleanupInterval = + plugin.getConfig2() + .garbageInterval; + + int remain = + cleanupInterval - countdown; + if (remain == 300) + broadcast("§e§l[垃圾站] §f距离清理还有 §e5分钟"); + else if (remain == 180) + broadcast("§e§l[垃圾站] §f距离清理还有 §e3分钟"); + else if (remain == 60) + broadcast("§e§l[垃圾站] §f距离清理还有 §e1分钟"); + else if (remain == 30) + broadcast("§e§l[垃圾站] §f距离清理还有 §e30秒"); + else if (remain == 10) + broadcast("§e§l[垃圾站] §f距离清理还有 §e10秒"); + else if (remain <= 5 && remain > 0) + broadcast("§e§l[垃圾站] §c" + remain + "§f..."); + else if (remain == 0) { + doCleanup(); + countdown = 0; + return; + } + countdown++; + } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void doCleanup() { + // 删除过期轮次的物品 + int deleteRound = + currentRound - maxRounds; + if (deleteRound >= 0) { + int deleted = deleteOldItems( + deleteRound); + if (deleted > 0) { + broadcast("§7[垃圾站] §f永久清理了 §e" + + deleted + " §f个过期物品"); + } + } + + // 递增轮次 + currentRound++; + setConfigValue("round", + String.valueOf(currentRound)); + + // 收集当前掉落物 + int count = collectItems(); + broadcast("§a§l[垃圾站] §f清理完成," + + "共清理 §e" + count + + " §f个掉落物,存入回收站"); + + // 打印下次清理时间 + long nextMs = System.currentTimeMillis() + + (long) cleanupInterval * 1000L; + String nextTime = + new SimpleDateFormat("HH:mm:ss") + .format(new Date(nextMs)); + broadcast("§e§l[垃圾站] §f下次清理时间: §a" + + nextTime); + } + + public int collectItems() { + int count = 0; + for (org.bukkit.World w : + Bukkit.getWorlds()) { + Collection items = + w.getEntitiesByClass(Item.class); + for (Item item : items) { + ItemStack stack = + item.getItemStack(); + if (stack == null + || stack.getType() + == Material.AIR) + continue; + saveItem(stack); + item.remove(); + count++; + } + } + return count; + } + + public void saveItem(ItemStack stack) { + { + try { + String data = + stackToBase64(stack); + if (data.isEmpty()) return; + PreparedStatement ps = db.prepareStatement( + "INSERT INTO items " + + "(item_data, amount, " + + "round, create_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, data); + ps.setInt(2, stack.getAmount()); + ps.setInt(3, currentRound); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }} + + private int deleteOldItems(int beforeRound) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM items " + + "WHERE round <= ?"); + ps.setInt(1, beforeRound); + int rows = ps.executeUpdate(); + ps.close(); + return rows; + } catch (Exception e) { + return 0; + } + } + + // ========== 序列化 ========== + + private String stackToBase64(ItemStack stack) { + try { + Map map = + stack.serialize(); + String json = new Gson().toJson(map); + return Base64.getEncoder() + .encodeToString( + json.getBytes( + StandardCharsets.UTF_8)); + } catch (Exception e) { + return ""; + } + } + + private ItemStack base64ToStack(String base64) { + try { + byte[] bytes = Base64.getDecoder() + .decode(base64); + String json = new String(bytes, + StandardCharsets.UTF_8); + Type type = new TypeToken< + Map>() {} + .getType(); + Map map = + new Gson().fromJson(json, type); + map = fixNumberTypes(map); + return ItemStack.deserialize(map); + } catch (Exception e) { + return null; + } + } + + @SuppressWarnings("unchecked") + private Map fixNumberTypes( + Map map) { + Map result = + new LinkedHashMap<>(); + for (Map.Entry e + : map.entrySet()) { + Object v = e.getValue(); + if (v instanceof Double) { + double d = (Double) v; + if (d == (long) d) { + result.put(e.getKey(), (long) d); + } else { + result.put(e.getKey(), d); + } + } else if (v instanceof Map) { + result.put(e.getKey(), + fixNumberTypes( + (Map) v)); + } else if (v instanceof List) { + List list = + new ArrayList<>(); + for (Object item : (List) v) { + if (item instanceof Map) { + list.add(fixNumberTypes( + (Map) item)); + } else { + list.add(item); + } + } + result.put(e.getKey(), list); + } else { + result.put(e.getKey(), v); + } + } + return result; + } + + // ========== 回收站GUI ========== + public void openRecycle(Player p) { + openRecyclePage(p, 0); + } + + public void openRecyclePage(Player p, int page) { + + Inventory g = Bukkit.createInventory( + null, 54, "§6§l垃圾回收站"); + + // 获取全部存储物品 + List> allItems = + getStoredItems(999); + int total = allItems.size(); + int pageSize = 40; + int totalPages = Math.max(1, + (int) Math.ceil( + (double) total / pageSize)); + + // 修正页码 + if (page < 0) page = 0; + if (page >= totalPages) + page = totalPages - 1; + + recyclePages.put(p.getName(), page); + + // 填充物品(0-39 格 = 第1页,40-79 = 第2页...) + int start = page * pageSize; + int end = Math.min(start + pageSize, + total); + int slot = 0; + for (int i = start; i < end; i++) { + if (slot >= 40) break; + Map item = + allItems.get(i); + int id = ((Number) item.get("id")) + .intValue(); + String data = (String) item + .get("item_data"); + int amount = ((Number) item + .get("amount")).intValue(); + ItemStack stack = base64ToStack(data); + if (stack != null) { + try { + byte[] bytes = + stack.serializeAsBytes(); + String b64 = java.util.Base64 + .getEncoder() + .encodeToString(bytes); + int len = b64.length(); + String f = len > 15 + ? b64.substring(10, 15) : "短"; + String m = len >= 5 + ? b64.substring(len / 2, + Math.min(len / 2 + 5, len)) + : "短"; + String b = len > 10 + ? b64.substring(len - 10, len - 5) + : "短"; + plugin.getLogger().info( + "[垃圾站加载] ID#" + id + + " " + stack.getType() + + " x" + amount + + " base64[" + len + "]" + + " 前=" + f + + " 中=" + m + + " 后=" + b); + } catch (Exception ex) { + plugin.getLogger().warning( + "[垃圾站加载] 序列化异常: " + + ex.getMessage()); + } + } + + if (stack != null) { + stack.setAmount(amount); + ItemMeta im = + stack.getItemMeta(); + if (im != null) { + List lore = + new ArrayList<>(); + lore.add("§7ID: #" + id); + lore.add("§a点击取出"); + im.setLore(lore); + stack.setItemMeta(im); + } + g.setItem(slot, stack); + } + slot++; + } + + // 45-48 格:额外投放区(空位) + // 52-53 格:额外投放区(空位) + + // 49 格:翻页-上一页 + if (page > 0) { + g.setItem(49, mk(Material.ARROW, + "§e上一页 §7(" + + page + "/" + + totalPages + ")")); + } else { + g.setItem(49, mk(Material.ARROW, + "§e第一页 §7(" + + "1/" + totalPages + ")")); + } + + // 50 格:统计信息 + g.setItem(50, mk(Material.CHEST, + "§e回收站统计", + "§7总存量: §f" + total + " 个", + "§7当前页: §f" + (page + 1) + + "/" + totalPages)); + + // 51 格:翻页-下一页 + boolean canNext = total >= pageSize + && page < totalPages - 1; + if (canNext) { + g.setItem(51, mk(Material.ARROW, + "§e下一页 §7(" + + (page + 2) + "/" + + totalPages + ")")); + } else { + g.setItem(51, mk(Material.ARROW, + "§7已是最后一页")); + } + // 52格:返回按钮 + g.setItem(52, mk(Material.ARROW, + "§7返回", + "§7返回主菜单")); + + p.openInventory(g); + } + public int getRecyclePage(String name) { + return recyclePages + .getOrDefault(name, 0); + } + + + public boolean handleClick(Player p, + String title, int slot) { + if (!title.equals("§6§l垃圾回收站")) + return false; + // 49:上一页 + if (slot == 49) { + int page = recyclePages + .getOrDefault(p.getName(), 0); + if (page > 0) + openRecyclePage(p, page - 1); + return true; + } + // 51:下一页 + if (slot == 51) { + int page = recyclePages + .getOrDefault(p.getName(), 0); + openRecyclePage(p, page + 1); + return true; + } + // 52:返回 + if (slot == 52) { + plugin.getGui().openMain(p); + return true; + } + + // 50/52/53:不处理 + if (slot >= 49 && slot <= 53) + return true; + return true; + } + + private List> + getStoredItems(int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, item_data, " + + "amount " + + "FROM items " + + "ORDER BY " + + "create_time " + + "DESC LIMIT ?"); + ps.setInt(1, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("item_data", + rs.getString("item_data")); + row.put("amount", + rs.getInt("amount")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + public boolean removeItem(int id) { + try { + PreparedStatement ps = + db.prepareStatement( + "DELETE FROM items " + + "WHERE id=?"); + ps.setInt(1, id); + int rows = ps.executeUpdate(); + ps.close(); + return rows > 0; + } catch (Exception e) { + return false; + } + } + + public int getItemCount() { + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT COUNT(*) FROM items"); + int count = 0; + if (rs.next()) + count = rs.getInt(1); + rs.close(); + st.close(); + return count; + } catch (Exception e) { + return 0; + } + } + + private void broadcast(String msg) { + // 同步打印到控制台 + plugin.getLogger().info( + msg.replaceAll("§[0-9a-fk-orA-FK-OR]", "")); + // 同步打印到聊天栏 + Bukkit.broadcastMessage(msg); + } + + + private ItemStack mk(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) + if (!l.isEmpty()) list.add(l); + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } +} diff --git a/src/main/java/Sdf1_login/GiftManager.java b/src/main/java/Sdf1_login/GiftManager.java index ddda2b3..1e4f9a7 100644 --- a/src/main/java/Sdf1_login/GiftManager.java +++ b/src/main/java/Sdf1_login/GiftManager.java @@ -1,296 +1,259 @@ -package Sdf1_login; - -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; - - -public class GiftManager { - - private final File giftFolder; - private final Main plugin; - private final List> stages = - new ArrayList<>(); - - public GiftManager(Main plugin) { - this.plugin = plugin; - this.giftFolder = new File( - plugin.getDataFolder(), "新人礼包"); - } - - public void loadStages() { - stages.clear(); - if (!giftFolder.exists()) { - giftFolder.mkdirs(); - writeDefaultStages(); - } - File[] files = giftFolder.listFiles( - (dir, name) -> name.endsWith(".md")); - if (files == null) return; - Arrays.sort(files); - for (File f : files) { - stages.add(parseMarkdown(f)); - } - } - - private Map parseMarkdown(File f) { - Map stage = new LinkedHashMap<>(); - stage.put("stage", stages.size() + 1); - stage.put("name", - f.getName().replace(".md", "")); - stage.put("rewards", new ArrayList()); - try { - BufferedReader r = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8)); - String line; - List rewards = new ArrayList<>(); - while ((line = r.readLine()) != null) { - if (line.contains("礼包内容") - || line.contains("奖励") - || line.contains("任务内容")) { - int colon = line.indexOf(':'); - if (colon < 0) - colon = line.indexOf('\uff1a'); - if (colon >= 0) { - rewards.add( - line.substring(colon + 1) - .trim()); - } - } - } - stage.put("rewards", rewards); - r.close(); - } catch (Exception e) { - e.printStackTrace(); - } - return stage; - } - private void writeStage(String name, String content) { - File f = new File(giftFolder, name); - if (f.exists()) return; - try (PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream(f), - StandardCharsets.UTF_8))) { - pw.print(content); - } catch (IOException ignored) {} - } - - private void writeDefaultStages() { - writeStage("第一阶段.md", - "# 第一阶段\n\n## 领取条件\n\n- 无前级条件\n\n## 礼包内容\n\n- 皮革套装 × 1\n- 木制武器工具套装 × 1\n"); - writeStage("第二阶段.md", - "# 第二阶段\n\n## 领取条件\n\n- 在线时长>1小时\n\n## 礼包内容\n\n- 500 游戏币\n"); - writeStage("第三阶段.md", - "# 第三阶段\n\n## 领取条件\n\n- 破坏100个方块\n- 在线>1天\n\n## 礼包内容\n\n- 1500 游戏币\n- 铁质工具套装 × 1\n- 铁质防具 × 1\n"); - writeStage("第四阶段.md", - "# 第四阶段\n\n## 领取条件\n\n- 破坏/放置300方块\n- 在线≥7天\n\n## 礼包内容\n\n- 钻石套装 × 1\n- 2000 游戏币\n"); - } - - /** 从任意文件夹加载任务列表 */ - public List> loadTasksFromFolder( - File folder) { - List> tasks = - new ArrayList<>(); - if (!folder.exists() || !folder.isDirectory()) - return tasks; - File[] files = folder.listFiles( - (dir, name) -> name.endsWith(".md")); - if (files == null) return tasks; - Arrays.sort(files); - for (File f : files) { - Map task = - new LinkedHashMap<>(); - task.put("name", - f.getName().replace(".md", "")); - task.put("conditions", - new ArrayList()); - task.put("rewards", - new ArrayList()); - try { - BufferedReader r = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8)); - String line; - List current = null; - while ((line = r.readLine()) != null) { - if (line.startsWith("# ")) { - task.put("name", - line.substring(2) - .trim()); - } else if (line.contains("条件") - || line.contains("领取条件") - || line.contains("完成条件")) { - current = (List) task - .get("conditions"); - } else if (line.contains("内容") - || line.contains("奖励") - || line.contains("任务内容") - || line.contains("礼包内容")) { - current = (List) task - .get("rewards"); - } else if (line.startsWith("- ") - && current != null) { - current.add(line.substring(2) - .trim()); - } - } - r.close(); - } catch (Exception e) { - e.printStackTrace(); - } - tasks.add(task); - } - return tasks; - } - - public boolean canClaim(Player p, int stage) { - Map user = plugin.getDb() - .getUser(p.getName()); - if (user.isEmpty()) return false; - int curStage = ((Number) user.getOrDefault( - "gift_stage", 0)).intValue(); - if (stage > curStage + 1) return false; - if (stage < 1 || stage > stages.size()) - return false; - String claimed = (String) user.getOrDefault( - "gift_claimed", ""); - if (claimed.contains("[" + stage + "]")) - return false; - - // debug 绕过:检查 tasks_completed - String completed = (String) user.getOrDefault( - "tasks_completed", ""); - if (completed.contains("[" + stage + "]")) - return true; - - // 正常条件检查 - long dbTime = ((Number) user.getOrDefault( - "total_online_time", 0L)).longValue(); - long sessionTime = 0; - Object lastObj = plugin.getDb().getField( - p.getName(), "last_online_check"); - if (lastObj != null && plugin.getLoggedIn() - .contains(p.getName())) { - long last = ((Number) lastObj).longValue(); - if (last > 0) { - sessionTime = - System.currentTimeMillis() - last; - } - } - long onlineTime = dbTime + sessionTime; - int broken = ((Number) user.getOrDefault( - "blocks_broken", 0)).intValue(); - int placed = ((Number) user.getOrDefault( - "blocks_placed", 0)).intValue(); - - switch (stage) { - case 1: return true; - case 2: return onlineTime >= 3600000; - case 3: return broken >= 100 - && onlineTime >= 86400000L; - case 4: return (broken + placed) >= 300 - && onlineTime >= 604800000L; - default: return false; - } - } - - public void claimReward(Player p, int stage) { - if (!canClaim(p, stage)) return; - Map user = plugin.getDb() - .getUser(p.getName()); - String claimed = (String) user.getOrDefault( - "gift_claimed", ""); - claimed += "[" + stage + "]"; - plugin.getDb().setField( - p.getName(), "gift_claimed", claimed); - plugin.getDb().setField( - p.getName(), "gift_stage", - Math.max(stage, - ((Number) user.getOrDefault( - "gift_stage", 0)) - .intValue())); - - if (stage == 1) { - giveItem(p, Material.LEATHER_HELMET); - giveItem(p, Material.LEATHER_CHESTPLATE); - giveItem(p, Material.LEATHER_LEGGINGS); - giveItem(p, Material.LEATHER_BOOTS); - giveItem(p, Material.WOODEN_SWORD); - giveItem(p, Material.WOODEN_PICKAXE); - giveItem(p, Material.WOODEN_AXE); - giveItem(p, Material.WOODEN_SHOVEL); - } else if (stage == 2) { - if (plugin.getEconomy() != null) { - double before = plugin.getEconomy() - .getBalance(p); - plugin.getEconomy() - .depositPlayer(p, 500); - double after = plugin.getEconomy() - .getBalance(p); - p.sendMessage("§a§l[礼包] §f游戏币: §e" - + (long) before - + "§f → §a" + (long) after); - } - } else if (stage == 3) { - if (plugin.getEconomy() != null) { - double before = plugin.getEconomy() - .getBalance(p); - plugin.getEconomy() - .depositPlayer(p, 1500); - double after = plugin.getEconomy() - .getBalance(p); - p.sendMessage("§a§l[礼包] §f游戏币: §e" - + (long) before - + "§f → §a" + (long) after); - } - giveItem(p, Material.IRON_PICKAXE); - giveItem(p, Material.IRON_AXE); - giveItem(p, Material.IRON_SWORD); - giveItem(p, Material.IRON_HELMET); - giveItem(p, Material.IRON_CHESTPLATE); - giveItem(p, Material.IRON_LEGGINGS); - giveItem(p, Material.IRON_BOOTS); - } else if (stage == 4) { - if (plugin.getEconomy() != null) { - double before = plugin.getEconomy() - .getBalance(p); - plugin.getEconomy() - .depositPlayer(p, 2000); - double after = plugin.getEconomy() - .getBalance(p); - p.sendMessage("§a§l[礼包] §f游戏币: §e" - + (long) before - + "§f → §a" + (long) after); - } - giveItem(p, Material.DIAMOND_HELMET); - giveItem(p, Material.DIAMOND_CHESTPLATE); - giveItem(p, Material.DIAMOND_LEGGINGS); - giveItem(p, Material.DIAMOND_BOOTS); - } - p.sendMessage(plugin.getConfig2().msg( - "gift_claimed")); - } - - private void giveItem(Player p, Material mat) { - p.getInventory().addItem(new ItemStack(mat)); - } - - public List> getStages() { - return stages; - } - - public int getMaxStage() { - return stages.size(); - } -} +package Sdf1_login; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; + + +public class GiftManager { + + private final File giftFolder; + private final Main plugin; + private final List> stages = + new ArrayList<>(); + + public GiftManager(Main plugin) { + this.plugin = plugin; + this.giftFolder = new File( + plugin.getDataFolder(), "新人礼包"); + } + + public void loadStages() { + stages.clear(); + if (!giftFolder.exists()) { + giftFolder.mkdirs(); + writeDefaultStages(); + } + File[] files = giftFolder.listFiles( + (dir, name) -> name.endsWith(".md")); + if (files == null) return; + Arrays.sort(files); + for (File f : files) { + stages.add(parseMarkdown(f)); + } + } + + private Map parseMarkdown(File f) { + Map stage = new LinkedHashMap<>(); + stage.put("stage", stages.size() + 1); + stage.put("name", + f.getName().replace(".md", "")); + stage.put("rewards", new ArrayList()); + try { + BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + List rewards = new ArrayList<>(); + while ((line = r.readLine()) != null) { + if (line.contains("礼包内容") + || line.contains("奖励") + || line.contains("任务内容")) { + int colon = line.indexOf(':'); + if (colon < 0) + colon = line.indexOf('\uff1a'); + if (colon >= 0) { + rewards.add( + line.substring(colon + 1) + .trim()); + } + } + } + stage.put("rewards", rewards); + r.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return stage; + } + private void writeStage(String name, String content) { + File f = new File(giftFolder, name); + if (f.exists()) return; + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.print(content); + } catch (IOException ignored) {} + } + + private void writeDefaultStages() { + writeStage("第一阶段.md", + "# 第一阶段\n\n## 领取条件\n\n- 无前级条件\n\n## 礼包内容\n\n- 皮革套装 × 1\n- 木制武器工具套装 × 1\n"); + writeStage("第二阶段.md", + "# 第二阶段\n\n## 领取条件\n\n- 在线时长>1小时\n\n## 礼包内容\n\n- 500 游戏币\n"); + writeStage("第三阶段.md", + "# 第三阶段\n\n## 领取条件\n\n- 破坏100个方块\n- 在线>1天\n\n## 礼包内容\n\n- 1500 游戏币\n- 铁质工具套装 × 1\n- 铁质防具 × 1\n"); + writeStage("第四阶段.md", + "# 第四阶段\n\n## 领取条件\n\n- 破坏/放置300方块\n- 在线≥7天\n\n## 礼包内容\n\n- 钻石套装 × 1\n- 2000 游戏币\n"); + } + + /** 从任意文件夹加载任务列表 */ + public List> loadTasksFromFolder( + File folder) { + List> tasks = + new ArrayList<>(); + if (!folder.exists() || !folder.isDirectory()) + return tasks; + File[] files = folder.listFiles( + (dir, name) -> name.endsWith(".md")); + if (files == null) return tasks; + Arrays.sort(files); + for (File f : files) { + Map task = + new LinkedHashMap<>(); + task.put("name", + f.getName().replace(".md", "")); + task.put("conditions", + new ArrayList()); + task.put("rewards", + new ArrayList()); + try { + BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + List current = null; + while ((line = r.readLine()) != null) { + if (line.startsWith("# ")) { + task.put("name", + line.substring(2) + .trim()); + } else if (line.contains("条件") + || line.contains("领取条件") + || line.contains("完成条件")) { + current = (List) task + .get("conditions"); + } else if (line.contains("内容") + || line.contains("奖励") + || line.contains("任务内容") + || line.contains("礼包内容")) { + current = (List) task + .get("rewards"); + } else if (line.startsWith("- ") + && current != null) { + current.add(line.substring(2) + .trim()); + } + } + r.close(); + } catch (Exception e) { + e.printStackTrace(); + } + tasks.add(task); + } + return tasks; + } + + public boolean canClaim(Player p, int stage) { + String name = p.getName(); + int curStage = ((Number) plugin.getDb() + .getField(name, "gift_stage")) + .intValue(); + String claimed = (String) plugin.getDb() + .getField(name, "gift_claimed"); + if (claimed == null) claimed = ""; + return curStage >= stage + && !claimed.contains( + String.valueOf(stage)); + } + + + public void claimReward(Player p, int stage) { + if (!canClaim(p, stage)) return; + Map user = plugin.getDb() + .getUser(p.getName()); + String claimed = (String) user.getOrDefault( + "gift_claimed", ""); + claimed += "[" + stage + "]"; + plugin.getDb().setField( + p.getName(), "gift_claimed", claimed); + plugin.getDb().setField( + p.getName(), "gift_stage", + Math.max(stage, + ((Number) user.getOrDefault( + "gift_stage", 0)) + .intValue())); + + if (stage == 1) { + giveItem(p, Material.LEATHER_HELMET); + giveItem(p, Material.LEATHER_CHESTPLATE); + giveItem(p, Material.LEATHER_LEGGINGS); + giveItem(p, Material.LEATHER_BOOTS); + giveItem(p, Material.WOODEN_SWORD); + giveItem(p, Material.WOODEN_PICKAXE); + giveItem(p, Material.WOODEN_AXE); + giveItem(p, Material.WOODEN_SHOVEL); + } else if (stage == 2) { + if (plugin.getEconomy() != null) { + double before = plugin.getEconomy() + .getBalance(p); + plugin.getEconomy() + .depositPlayer(p, 500); + double after = plugin.getEconomy() + .getBalance(p); + p.sendMessage("§a§l[礼包] §f游戏币: §e" + + (long) before + + "§f → §a" + (long) after); + } + } else if (stage == 3) { + if (plugin.getEconomy() != null) { + double before = plugin.getEconomy() + .getBalance(p); + plugin.getEconomy() + .depositPlayer(p, 1500); + double after = plugin.getEconomy() + .getBalance(p); + p.sendMessage("§a§l[礼包] §f游戏币: §e" + + (long) before + + "§f → §a" + (long) after); + } + giveItem(p, Material.IRON_PICKAXE); + giveItem(p, Material.IRON_AXE); + giveItem(p, Material.IRON_SWORD); + giveItem(p, Material.IRON_HELMET); + giveItem(p, Material.IRON_CHESTPLATE); + giveItem(p, Material.IRON_LEGGINGS); + giveItem(p, Material.IRON_BOOTS); + } else if (stage == 4) { + if (plugin.getEconomy() != null) { + double before = plugin.getEconomy() + .getBalance(p); + plugin.getEconomy() + .depositPlayer(p, 2000); + double after = plugin.getEconomy() + .getBalance(p); + p.sendMessage("§a§l[礼包] §f游戏币: §e" + + (long) before + + "§f → §a" + (long) after); + } + giveItem(p, Material.DIAMOND_HELMET); + giveItem(p, Material.DIAMOND_CHESTPLATE); + giveItem(p, Material.DIAMOND_LEGGINGS); + giveItem(p, Material.DIAMOND_BOOTS); + } + p.sendMessage(plugin.getConfig2().msg( + "gift_claimed")); + } + + private void giveItem(Player p, Material mat) { + p.getInventory().addItem(new ItemStack(mat)); + } + + public List> getStages() { + return stages; + } + + public int getMaxStage() { + return stages.size(); + } +} diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 6e30696..bb4e5b5 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -1,3055 +1,3186 @@ -package Sdf1_login; - -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import org.bukkit.event.player.PlayerInteractEvent; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; -import java.util.LinkedHashMap; -import java.util.concurrent.ConcurrentHashMap; - -import net.milkbowl.vault.economy.Economy; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.PlayerDropItemEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerMoveEvent; -import org.bukkit.event.player.PlayerPickupItemEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.plugin.RegisteredServiceProvider; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.scheduler.BukkitRunnable; - -public class Main extends JavaPlugin - implements CommandExecutor, Listener, - TabCompleter { - - private DatabaseManager db; - private ConfigManager config; - private EmailManager email; - private LoginManager loginMgr; - private AFKManager afk; - private GiftManager gift; - private CheckInManager checkIn; - private InviteManager invite; - private PointsManager points; - private GUIManager gui; - private ChatInputManager chatInput; - private Economy economy; - private VerificationManager verification; - private IPGroupManager ipGroup; - private AccountRequestManager accountRequest; - private TicketManager ticket; - private ChatFilterManager chatFilter; - private GarbageManager garbage; - - private final Set loggedIn = - new TreeSet<>( - String.CASE_INSENSITIVE_ORDER); - private final Set needsPasswordChange = - new TreeSet<>( - String.CASE_INSENSITIVE_ORDER); - private final Map joinTime = - new ConcurrentHashMap<>(); - private final Map joinLoc = - new HashMap<>(); - private final Map ipAutoLogin = - new HashMap<>(); - private static final long IP_SKIP_MS = 300000L; - - private final Map lastActivity = - new ConcurrentHashMap<>(); - - private static class PwdRollback { - final String hash; - final String salt; - final long time; - PwdRollback(String h, String s, long t) { - hash = h; - salt = s; - time = t; - } - } - - private final Map - pwdRollback = - new ConcurrentHashMap<>(); - - public void recordPasswordChange(String name, - String oldHash, String oldSalt) { - pwdRollback.put(name, - new PwdRollback(oldHash, oldSalt, - System.currentTimeMillis())); - } - - // ===== Getters ===== - public DatabaseManager getDb() { return db; } - public ConfigManager getConfig2() { return config; } - public EmailManager getEmail() { return email; } - public Economy getEconomy() { return economy; } - public LoginManager getLoginMgr() { return loginMgr; } - public AFKManager getAfk() { return afk; } - public GiftManager getGift() { return gift; } - public CheckInManager getCheckIn() { return checkIn; } - public InviteManager getInvite() { return invite; } - public PointsManager getPoints() { return points; } - public GUIManager getGui() { return gui; } - public ChatInputManager getChatInput() { return chatInput; } - public VerificationManager getVerification() { return verification; } - public IPGroupManager getIPGroup() { return ipGroup; } - public AccountRequestManager getAccountRequest() { return accountRequest; } - public TicketManager getTicket() { return ticket; } - public ChatFilterManager getChatFilter() { return chatFilter; } - public GarbageManager getGarbage() { return garbage; } - public Set getLoggedIn() { return loggedIn; } - public Set getNeedsPasswordChange() { return needsPasswordChange; } - - // ===== Enable/Disable ===== - @Override - public void onEnable() { - getDataFolder().mkdirs(); - config = new ConfigManager(getDataFolder()); - config.loadMessages(); - config.loadSmtp(); - config.loadSettings(); - getCommand("oa").setExecutor(this); - - db = new DatabaseManager(getDataFolder()); - db.init(); - email = new EmailManager(config); - loginMgr = new LoginManager(this); - afk = new AFKManager(config, this); - gift = new GiftManager(this); - gift.loadStages(); - checkIn = new CheckInManager(this); - invite = new InviteManager(this); - points = new PointsManager(this); - gui = new GUIManager(this); - chatInput = new ChatInputManager(); - verification = new VerificationManager(this); - ipGroup = new IPGroupManager(this, - config.maxAccountsPerIP); - accountRequest = - new AccountRequestManager(this); - accountRequest.setMode(config.approvalMode); - accountRequest.setAutoDelayMinutes( - config.autoApproveDelayMinutes); - ticket = new TicketManager(this); - garbage = new GarbageManager(this); - garbage.init(); - garbage.loadConfig(); - garbage.startAutoCleanup(); - chatFilter = new ChatFilterManager(this); - chatFilter.loadConfig(); - - setupEconomy(); - - if (getCommand("sdf1_login") != null) { - getCommand("sdf1_login").setExecutor(this); - getCommand("sdf1_login").setTabCompleter(this); - } - if (getCommand("reg") != null) - getCommand("reg").setExecutor(this); - if (getCommand("login") != null) - getCommand("login").setExecutor(this); - if (getCommand("l") != null) - getCommand("l").setExecutor(this); - if (getCommand("签到") != null) - getCommand("签到").setExecutor(this); - if (getCommand("recycle") != null) - getCommand("recycle").setExecutor(this); - if (getCommand("垃圾清理") != null) - getCommand("垃圾清理").setExecutor(this); - if (getCommand("绑定邮箱") != null) - getCommand("绑定邮箱").setExecutor(this); - if (getCommand("找回密码") != null) - getCommand("找回密码").setExecutor(this); - if (getCommand("玩家信息") != null) - getCommand("玩家信息").setExecutor(this); - if (getCommand("sdf1debug") != null) - getCommand("sdf1debug").setExecutor(this); - if (getCommand("oa") != null) - getCommand("oa").setExecutor(this); - - - getServer().getPluginManager() - .registerEvents(this, this); - - afk.startCheck(); - startLoginReminder(); - startTimeoutCheck(); - startPasswordReminder(); - startTicketAutoProcess(); - - getLogger().info( - "Sdf1_login v1.0 | 就绪"); - } - - @Override - public void onDisable() { - if (db != null) db.close(); - if (garbage != null) garbage.close(); - } - - private void setupEconomy() { - if (getServer().getPluginManager() - .getPlugin("Vault") == null) return; - RegisteredServiceProvider rsp = - getServer().getServicesManager() - .getRegistration( - Economy.class); - if (rsp != null) - economy = rsp.getProvider(); - } - - // ===== Timers ===== - private void startTimeoutCheck() { - new BukkitRunnable() { - public void run() { - long now = System.currentTimeMillis(); - Iterator> it = - joinTime.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = - it.next(); - if (now - entry.getValue() - > config.loginTimeout - * 1000L) { - Player p = Bukkit.getPlayer( - entry.getKey()); - if (p != null && p.isOnline() - && !loggedIn.contains( - p.getName())) - p.kickPlayer(config.msg( - "login_timeout")); - it.remove(); - } - } - } - }.runTaskTimer(this, 80L, 80L); - } - - private void startLoginReminder() { - new BukkitRunnable() { - public void run() { - Iterator it = - joinTime.keySet().iterator(); - while (it.hasNext()) { - UUID uuid = it.next(); - Player p = Bukkit.getPlayer(uuid); - if (p == null || !p.isOnline()) { - it.remove(); - continue; - } - if (loggedIn.contains( - p.getName())) { - it.remove(); - continue; - } - // 已注册→提示登录,未注册→提示注册 - if (db.userExists(p.getName())) - p.sendMessage(config.msg( - "not_logged_in")); - else - p.sendMessage(config.msg( - "not_registered")); - } - } - }.runTaskTimer(this, 200L, 200L); - } - - private void startPasswordReminder() { - new BukkitRunnable() { - public void run() { - for (Player p : - Bukkit.getOnlinePlayers()) { - if (needsPasswordChange.contains( - p.getName())) { - p.sendTitle("§c§l请修改密码", - "§f使用 /sdf1_login pw 修改密码", - 10, 40, 10); - p.playSound(p.getLocation(), - Sound.BLOCK_NOTE_BLOCK_PLING, - 1.0f, 1.0f); - } - } - } - }.runTaskTimer(this, 200L, 200L); - } - - private void startTicketAutoProcess() { - new BukkitRunnable() { - public void run() { - if (ticket != null) - ticket.autoProcessCompleted(); - } - }.runTaskTimer(this, 6000L, 6000L); - } - - // ===== Utils ===== - public String getPlayerIP(Player p) { - InetSocketAddress addr = p.getAddress(); - if (addr == null) return null; - return addr.getAddress().getHostAddress(); - } - - - public void resetActivity(Player p) { - lastActivity.put(p.getUniqueId(), - System.currentTimeMillis()); - } - - public boolean canAutoLogin(Player p) { - String ip = getPlayerIP(p); - if (ip == null) return false; - Long last = ipAutoLogin.get(ip); - return last != null - && System.currentTimeMillis() - last - < IP_SKIP_MS; - } - - public void recordIPLogin(Player p) { - String ip = getPlayerIP(p); - if (ip != null) - ipAutoLogin.put(ip, - System.currentTimeMillis()); - } - - public boolean isFrozen(Player p) { - return !loggedIn.contains(p.getName()); - } - - private boolean isAdmin(CommandSender sender) { - if (sender instanceof Player) - return ((Player) sender) - .getScoreboardTags() - .contains(config.adminTag); - return true; - } - - private void clearPlayer(Player p) { - p.getInventory().clear(); - p.getInventory().setArmorContents( - new ItemStack[4]); - p.getInventory().setExtraContents( - new ItemStack[1]); - p.setLevel(0); - p.setExp(0f); - } - - public void restoreInventory(Player p) { - String name = p.getName(); - try { - String[] backup = - db.loadInventoryBackup(name); - if (backup == null - || !isValidBackup(backup)) { - return; - } - - debugPrintBase64("恢复前", name, - backup[0]); - - ItemStack[] contents = - InventorySerializer - .fromBase64(backup[0]); - if (contents != null) - p.getInventory() - .setContents(contents); - - if (backup[1] != null - && !backup[1].isEmpty()) { - ItemStack[] armor = - InventorySerializer - .fromBase64( - backup[1]); - if (armor != null) - p.getInventory() - .setArmorContents( - armor); - } - - if (backup[2] != null - && !backup[2].isEmpty()) { - ItemStack[] extra = - InventorySerializer - .fromBase64( - backup[2]); - if (extra != null) - p.getInventory() - .setExtraContents( - extra); - } - - int lv = Integer.parseInt( - backup[3]); - if (lv > 0) p.setLevel(lv); - double xp = Double.parseDouble( - backup[4]); - if (xp > 0) - p.setExp((float) xp); - - debugPrintBase64("恢复后", name, - InventorySerializer.toBase64( - p.getInventory() - .getContents())); - - getLogger().info( - "[Sdf1_login] " + name - + " 背包已恢复"); - } catch (Exception e) { - getLogger().severe( - "[Sdf1_login] 还原背包失败: " - + name); - e.printStackTrace(); - } - } - - - - - public void backupInventory(Player p) { - String name = p.getName(); - int cnt = countItems(p) - + countArmor(p) - + countExtra(p); - if (cnt == 0) { - getLogger().warning( - "[Sdf1_login] " + name - + " 背包为空,跳过备份"); - return; - } - try { - String invB64 = - InventorySerializer.toBase64( - p.getInventory() - .getContents()); - String armorB64 = - InventorySerializer.toBase64( - p.getInventory() - .getArmorContents()); - String extraB64 = - InventorySerializer.toBase64( - p.getInventory() - .getExtraContents()); - db.saveInventoryBackup(name, invB64, - armorB64, extraB64, - p.getLevel(), p.getExp()); - getLogger().info( - "[Sdf1_login] " + name - + " 背包已备份 (物品:" - + cnt + ")"); - } catch (Exception e) { - getLogger().severe( - "[Sdf1_login] 背包备份失败: " - + name); - e.printStackTrace(); - } - } - - private void hideInventory(Player p) { - String name = p.getName(); - try { - debugPrintBase64("清空前", name, - InventorySerializer.toBase64( - p.getInventory() - .getContents())); - - // 清空背包(备份已在上次退出时保存) - p.getInventory().clear(); - p.getInventory() - .setArmorContents(null); - p.getInventory() - .setExtraContents(null); - p.setLevel(0); - p.setExp(0); - - debugPrintBase64("清空后", name, - InventorySerializer.toBase64( - p.getInventory() - .getContents())); - } catch (Exception e) { - getLogger().severe( - "[Sdf1_login] hideInventory失败: " - + name); - e.printStackTrace(); - } - } - - private void hideInventoryDelayed(Player p, - int attempt) { - String name = p.getName(); - if (!p.isOnline()) return; - if (!isFrozen(p)) return; - - // 从数据库加载已有备份 - String[] existing = - db.loadInventoryBackup(name); - - // 调试打印 - debugPrintBase64("加入-加载DB", name, - existing != null - ? existing[0] : null); - - // 无有效备份 → 跳过清空背包 - if (existing == null - || !isValidBackup(existing)) { - getLogger().info( - "[Sdf1_login] " + name - + " 无有效备份," - + "跳过清空背包"); - return; - } - - // 有有效备份 → 清空当前背包 - hideInventory(p); - } - - - public void autoLogin(Player p, - String registerType) { - String name = p.getName(); - String ip = getPlayerIP(p); - String uuid = - p.getUniqueId().toString(); - String defaultPwd = "123456aA"; - - if (!db.userExists(name)) { - if (config.maxAccountsPerIP > 0 - && ip != null - && !ipGroup.canRegister(ip)) { - p.kickPlayer( - buildIPKickMessage(p, ip)); - return; - } - if (!"manual".equals( - config.approvalMode)) { - AccountRequestManager.Request req = - accountRequest.createRequest( - name, name, ip); - if (req != null) { - p.kickPlayer( - config.msg("need_approval") - .replace("{id}", - String.valueOf( - req.id))); - return; - } - } - String salt = - PasswordUtils.generateSalt(); - String hash = PasswordUtils.hash( - defaultPwd, salt); - db.createUser(name, hash, salt); - db.setField(name, - "premium_uuid", uuid); - db.setField(name, - "register_ip", ip); - db.setField(name, - "register_type", registerType); - db.recordIP(name, ip); - } - - loggedIn.add(name); - db.setLoggedIn(name, true); - db.setField(name, - "last_login_time", - System.currentTimeMillis()); - db.setField(name, - "last_online_check", - System.currentTimeMillis()); - db.recordIP(name, ip); - joinTime.remove(p.getUniqueId()); - recordIPLogin(p); - - // 只在有有效备份时恢复 - restoreInventory(p); - - giveMenuSnowball(p); - p.sendMessage("§a[Sdf1_login] §f您已自动登录!"); - activateBeibao(p); - pushPendingAlerts(p); - - - } - - private void activateBeibao(Player p) { - try { - org.bukkit.plugin.Plugin cy = - Bukkit.getPluginManager() - .getPlugin("CY_beibao"); - if (cy != null && cy.isEnabled()) - cy.getClass().getMethod( - "onSdf1Activation", - String.class, - int.class, int.class) - .invoke(cy, p.getName(), - 0, 0); - } catch (Exception ignored) { - } - } - - public void triggerSecurityAlert(Player player, - String suspiciousIP) { - String name = player.getName(); - db.recordSecurityAlert(name, suspiciousIP); - Bukkit.broadcastMessage( - "§c§l[安全警告] §e" + name - + " §f账号疑似遭到 §c" - + suspiciousIP - + " §f的盗号尝试"); - String emailAddr = - (String) db.getField(name, "email"); - if (emailAddr != null - && !emailAddr.isEmpty()) { - String body = "玩家 " + name - + ":\n您的账号在 " - + new java.text.SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss") - .format(new java.util.Date()) - + " 疑似遭到 IP: " - + suspiciousIP - + " 的盗号尝试。\n" - + "建议立即修改密码。"; - final String to = emailAddr; - final String finalBody = body; - Bukkit.getScheduler() - .runTaskAsynchronously(this, - () -> email - .sendTempPassword( - to, name, - finalBody)); - } - } - - public void pushPendingAlerts(Player p) { - String name = p.getName(); - List> alerts = - db.getUnnotifiedAlerts(name); - if (alerts.isEmpty()) return; - p.sendMessage("§c§l========== 安全报警 =========="); - for (Map alert : alerts) { - p.sendMessage("§e时间: §f" - + alert.get("alert_date")); - p.sendMessage("§e可疑IP: §c" - + alert.get("alert_ip")); - db.markAlertNotified( - ((Number) alert.get("id")) - .intValue()); - } - p.sendMessage( - "§c建议使用 §e/sdf1_login pw §c修改密码"); - p.sendMessage( - "§c§l=================================="); - } - - public void handleReset(Player p) { - loginMgr.handleReset(p); - } - - public boolean dailySign(Player p) { - if (checkIn.isCheckedInToday(p.getName())) - return false; - p.sendMessage(checkIn.checkIn(p)); - invite.onInviteeCheckIn(p.getName()); - return true; - } - - public void openTaskPanel(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, "§6§l任务面板"); - ItemStack gl = new ItemStack( - Material.GRAY_STAINED_GLASS_PANE); - ItemMeta glm = gl.getItemMeta(); - if (glm != null) { - glm.setDisplayName(" "); - gl.setItemMeta(glm); - } - for (int i = 0; i < 27; i++) - g.setItem(i, gl); - Map user = - db.getUser(p.getName()); - int stage = ((Number) user.getOrDefault( - "gift_stage", 0)).intValue(); - String claimed = (String) user.getOrDefault( - "gift_claimed", ""); - g.setItem(11, mkItem(Material.BOOK, - "§e新人礼包进度", - "§7当前阶段: §e" + stage, - "§7已领取: §a" + (claimed.isEmpty() - ? "无" : claimed))); - g.setItem(15, mkItem(Material.CLOCK, - "§b签到", "§7今日签到获取积分")); - g.setItem(22, mkItem(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - public void openAccountRequestPanel(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, "§e§l账号请求"); - ItemStack gl = new ItemStack( - Material.GRAY_STAINED_GLASS_PANE); - ItemMeta glm = gl.getItemMeta(); - if (glm != null) { - glm.setDisplayName(" "); - gl.setItemMeta(glm); - } - for (int i = 0; i < 27; i++) - g.setItem(i, gl); - g.setItem(10, mkItem(Material.BARRIER, - "§c申请删除账号", - "§7申请删除自己名下的账号")); - g.setItem(13, mkItem(Material.PAPER, - "§6提交工单", - "§7提交bug反馈、求助、举报等")); - g.setItem(16, mkItem(Material.REDSTONE, - "§e找回密码", - "§7通过邮箱找回密码")); - g.setItem(22, mkItem(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - private void playSuccessSound(Player p) { - p.playSound(p.getLocation(), - Sound.BLOCK_NOTE_BLOCK_CHIME, - 1.0f, 0.79f); - p.playSound(p.getLocation(), - Sound.BLOCK_NOTE_BLOCK_BASS, - 1.0f, 0.79f); - p.playSound(p.getLocation(), - Sound.BLOCK_NOTE_BLOCK_PLING, - 1.0f, 0.79f); - } - - private int countItems(Player p) { - int c = 0; - for (ItemStack it : p.getInventory() - .getContents()) - if (it != null && it.getType() - != Material.AIR) c++; - return c; - } - - private int countArmor(Player p) { - int c = 0; - for (ItemStack it : p.getInventory() - .getArmorContents()) - if (it != null && it.getType() - != Material.AIR) c++; - return c; - } - - private int countExtra(Player p) { - int c = 0; - for (ItemStack it : p.getInventory() - .getExtraContents()) - if (it != null && it.getType() - != Material.AIR) c++; - return c; - } - - /** - * 检查备份是否有效(包含非雪球的真实物品) - */ - public boolean isValidBackup(String[] backup) { - if (backup == null - || backup[0] == null - || backup[0].isEmpty()) { - return false; - } - try { - ItemStack[] items = - InventorySerializer - .fromBase64(backup[0]); - if (items == null) return false; - for (ItemStack item : items) { - if (item == null - || item.getType() - == Material.AIR) - continue; - if (isMenuSnowball(item)) - continue; - return true; - } - } catch (Exception e) { - return false; - } - return false; - } - - /** - * 控制台打印 base64 前10位 - */ - private void debugPrintBase64(String label, - String name, String base64) { - if (base64 == null || base64.isEmpty()) { - getLogger().info( - "[BACKUP_DEBUG] " + label - + " " + name - + ": "); - return; - } - String preview = - base64.length() > 10 - ? base64.substring(0, 10) - : base64; - getLogger().info( - "[BACKUP_DEBUG] " + label + " " - + name + ": " + preview); - } - - private ItemStack mkItem(Material mat, - String name, String... lore) { - ItemStack it = new ItemStack(mat); - ItemMeta im = it.getItemMeta(); - if (im != null) { - im.setDisplayName(name); - if (lore.length > 0) { - List list = - new ArrayList<>(); - for (String l : lore) - if (!l.isEmpty()) list.add(l); - if (!list.isEmpty()) - im.setLore(list); - } - it.setItemMeta(im); - } - return it; - } - - private String buildIPKickMessage(Player p, - String ip) { - List> accounts = - db.getAccountDetailsByIP(ip); - StringBuilder sb = new StringBuilder(); - sb.append("§c§l======== IP注册已满 ========\n"); - sb.append("§e您的IP §f").append(ip) - .append(" §e下已注册 §f") - .append(accounts.size()) - .append(" §e个账号:\n"); - java.text.SimpleDateFormat sdf = - new java.text.SimpleDateFormat( - "yyyy-MM-dd HH:mm"); - int idx = 1; - for (Map acc : accounts) { - long regTime = ((Number) acc.get( - "register_time")).longValue(); - String dateStr = regTime > 0 - ? sdf.format(new java.util.Date( - regTime)) - : "未知"; - sb.append("§7").append(idx) - .append(". §f") - .append(acc.get("player_name")) - .append(" §7注册于: §e") - .append(dateStr).append("\n"); - idx++; - } - sb.append("§7-----------------------------\n"); - sb.append("§e请使用已有账号登录\n"); - sb.append("§e登录后输入 §f/oa §e提交工单\n"); - sb.append("§c§l============================"); - return sb.toString(); - } - /** - * 读取备份,返回物品名预览 - */ - private String getBackupItemPreview( - int backupId) { - try { - String[] backup = - db.getInventoryBackupById( - backupId); - if (backup == null - || backup[0] == null - || backup[0].isEmpty()) - return "§7空"; - ItemStack[] items = - InventorySerializer - .fromBase64( - backup[0]); - if (items == null) - return "§7空"; - Map counts = - new LinkedHashMap<>(); - for (ItemStack it : items) { - if (it == null - || it.getType() - == Material.AIR) - continue; - String name = translateMaterial( - it.getType().name()); - counts.merge(name, - it.getAmount(), - Integer::sum); - } - if (counts.isEmpty()) - return "§7空"; - StringBuilder sb = - new StringBuilder(); - int shown = 0; - for (Map.Entry en - : counts.entrySet()) { - if (shown >= 5) { - sb.append(" §7..."); - break; - } - if (shown > 0) - sb.append("§7, "); - sb.append("§f").append(en.getKey()) - .append("§7x") - .append(en.getValue()); - shown++; - } - return sb.toString(); - } catch (Exception e) { - return "§7解析失败"; - } - } - - private String translateMaterial( - String name) { - switch (name) { - case "GRASS_BLOCK": - return "草方块"; - case "OAK_LOG": - return "橡木原木"; - case "OAK_PLANKS": - return "橡木木板"; - case "STONE": - return "石头"; - case "COBBLESTONE": - return "圆石"; - case "DIRT": - return "泥土"; - case "SAND": - return "沙子"; - case "GRAVEL": - return "砂砾"; - case "DIAMOND": - return "钻石"; - case "DIAMOND_ORE": - return "钻石矿石"; - case "IRON_INGOT": - return "铁锭"; - case "GOLD_INGOT": - return "金锭"; - case "COAL": - return "煤炭"; - case "COAL_ORE": - return "煤矿石"; - case "RAW_IRON": - return "粗铁"; - case "RAW_GOLD": - return "粗金"; - case "OAK_LEAVES": - return "橡树树叶"; - case "OAK_SAPLING": - return "橡树树苗"; - case "STICK": - return "木棍"; - case "CRAFTING_TABLE": - return "工作台"; - case "FURNACE": - return "熔炉"; - case "CHEST": - return "箱子"; - case "TORCH": - return "火把"; - case "BEDROCK": - return "基岩"; - case "TNT": - return "TNT"; - case "REDSTONE": - return "红石"; - case "REDSTONE_ORE": - return "红石矿石"; - case "LAPIS_LAZULI": - return "青金石"; - case "EMERALD": - return "绿宝石"; - case "EMERALD_ORE": - return "绿宝石矿石"; - case "OBSIDIAN": - return "黑曜石"; - case "GLOWSTONE": - return "萤石"; - case "ARROW": - return "箭"; - case "BOW": - return "弓"; - case "IRON_SWORD": - return "铁剑"; - case "DIAMOND_SWORD": - return "钻石剑"; - case "IRON_PICKAXE": - return "铁镐"; - case "DIAMOND_PICKAXE": - return "钻石镐"; - case "IRON_AXE": - return "铁斧"; - case "DIAMOND_AXE": - return "钻石斧"; - case "IRON_HELMET": - return "铁头盔"; - case "IRON_CHESTPLATE": - return "铁胸甲"; - case "IRON_LEGGINGS": - return "铁护腿"; - case "IRON_BOOTS": - return "铁靴子"; - case "DIAMOND_HELMET": - return "钻石头盔"; - case "DIAMOND_CHESTPLATE": - return "钻石胸甲"; - case "DIAMOND_LEGGINGS": - return "钻石护腿"; - case "DIAMOND_BOOTS": - return "钻石靴子"; - case "BREAD": - return "面包"; - case "COOKED_BEEF": - return "牛排"; - case "COOKED_PORKCHOP": - return "熟猪排"; - case "COOKED_CHICKEN": - return "熟鸡肉"; - case "GOLDEN_APPLE": - return "金苹果"; - case "ENDER_PEARL": - return "末影珍珠"; - case "BLAZE_ROD": - return "烈焰棒"; - case "GHAST_TEAR": - return "恶魂之泪"; - case "NETHER_STAR": - return "下界之星"; - case "SHULKER_SHELL": - return "潜影壳"; - case "ELYTRA": - return "鞘翅"; - case "TOTEM_OF_UNDYING": - return "不死图腾"; - case "TRIDENT": - return "三叉戟"; - case "ENCHANTED_GOLDEN_APPLE": - return "附魔金苹果"; - default: - // 转中文名格式:SANDSTONE -> 沙石 - return name.toLowerCase() - .replace("_", " "); - } - } - - // ===== Events ===== - @EventHandler - public void onJoin(PlayerJoinEvent e) { - Player p = e.getPlayer(); - String name = p.getName(); - String ip = getPlayerIP(p); - // 重置聊天输入状态(防止上次退出时残留) - chatInput.reset(p); - if (needsPasswordChange.contains(name)) - needsPasswordChange.remove(name); - - lastActivity.put(p.getUniqueId(), - System.currentTimeMillis()); - if (needsPasswordChange.contains(name)) - needsPasswordChange.remove(name); - - boolean isBedrock = - verification.isBedrockPlayer(p); - boolean isOnlineMode = - verification.isOnlineMode(); - - if (!db.userExists(name)) { - if (config.maxAccountsPerIP > 0 - && ip != null - && !ipGroup.canRegister(ip)) { - p.kickPlayer( - buildIPKickMessage(p, ip)); - return; - } - } - - if (!"manual".equals(config.approvalMode) - && !db.userExists(name)) { - AccountRequestManager.Request req = - accountRequest.createRequest( - name, name, ip); - if (req != null) { - p.kickPlayer( - config.msg("need_approval") - .replace("{id}", - String.valueOf( - req.id))); - return; - } - } - - if (isOnlineMode) { - autoLogin(p, "premium"); - return; - } - if (isBedrock) { - autoLogin(p, "bedrock"); - return; - } - if (canAutoLogin(p)) { - autoLogin(p, "ip_reconnect"); - return; - } - afk.remove(p.getUniqueId()); - afk.recordAction(p.getUniqueId()); - hideInventoryDelayed(p, 0); - - - verification.verifyPremiumAsync(p, - isPremium -> { - if (isPremium) { - Bukkit.getScheduler() - .runTask(this, - () -> autoLogin(p, - "premium")); - } else { - Bukkit.getScheduler() - .runTask(this, () -> { - joinTime.put( - p.getUniqueId(), - System.currentTimeMillis()); - if (!db.userExists(name)) - p.sendMessage(config.msg( - "not_registered")); - else - p.sendMessage(config.msg( - "not_logged_in")); - }); - } - }); - } - - @EventHandler - public void onQuit(PlayerQuitEvent e) { - Player p = e.getPlayer(); - String name = p.getName(); - chatInput.reset(p); - - boolean wasLoggedIn = - loggedIn.contains(name); - - // 判断是否需要保存: - // 1. 已登录 → 保存当前背包 - // 2. 未登录但有真实物品 → 保存 - // (插件安装前的老玩家) - // 3. 未登录且背包被插件清空 → 不保存 - // (避免覆盖有效备份) - boolean hasRealItems = false; - for (ItemStack it : p.getInventory() - .getContents()) { - if (it != null - && it.getType() - != Material.AIR - && !isMenuSnowball(it)) { - hasRealItems = true; - break; - } - } - - if (wasLoggedIn || hasRealItems) { - try { - String contents = - InventorySerializer - .toBase64( - p.getInventory() - .getContents()); - String armor = - InventorySerializer - .toBase64( - p.getInventory() - .getArmorContents()); - String extra = - InventorySerializer - .toBase64( - p.getInventory() - .getExtraContents()); - int level = p.getLevel(); - double exp = p.getExp(); - - debugPrintBase64( - "退出保存", name, - contents); - db.saveInventoryBackup(name, - contents, armor, extra, - level, exp); - } catch (Exception ex) { - getLogger().severe( - "[Sdf1_login] " - + "退出保存背包失败: " - + name); - } - } else { - getLogger().info( - "[BACKUP_DEBUG] 退出跳过 " - + name - + ": 背包为空" - + "(插件已清空)"); - } - - if (wasLoggedIn) { - loggedIn.remove(name); - db.setLoggedIn(name, false); - recordIPLogin(p); - } - - lastActivity.remove(p.getUniqueId()); - joinTime.remove(p.getUniqueId()); - joinLoc.remove(p.getUniqueId()); - needsPasswordChange.remove(name); - } - - private static final String MENU_SNOWBALL_TAG = - "sdf1_menu"; - - public void giveMenuSnowball(Player p) { - // 检查背包里是否已有菜单雪球 - for (ItemStack it : p.getInventory() - .getContents()) { - if (isMenuSnowball(it)) { - return; // 已有,不重复发 - } - } - // 没有则发放 - ItemStack snow = new ItemStack( - Material.SNOWBALL); - ItemMeta im = snow.getItemMeta(); - if (im != null) { - im.setDisplayName( - "§e§l[菜单] §f右键打开主菜单"); - List lore = new ArrayList<>(); - lore.add("§7右键点击打开功能菜单"); - lore.add("§8" + MENU_SNOWBALL_TAG); - im.setLore(lore); - snow.setItemMeta(im); - } - // 放到最后一个空格,避免覆盖 - int emptySlot = - p.getInventory() - .firstEmpty(); - if (emptySlot >= 0) { - p.getInventory().setItem( - emptySlot, snow); - } else { - // 背包满,丢在地上 - p.getWorld().dropItemNaturally( - p.getLocation(), snow); - } - } - - private boolean isMenuSnowball(ItemStack item) { - if (item == null - || item.getType() - != Material.SNOWBALL) - return false; - if (!item.hasItemMeta()) - return false; - ItemMeta im = item.getItemMeta(); - if (im == null) return false; - if (!im.hasLore()) return false; - List lore = im.getLore(); - if (lore == null) return false; - for (String line : lore) { - if (line.contains(MENU_SNOWBALL_TAG)) - return true; - } - return false; - } - - - @EventHandler - public void onMove(PlayerMoveEvent e) { - Player p = e.getPlayer(); - Location from = e.getFrom(); - Location to = e.getTo(); - if (isFrozen(p)) { - if (to != null && (from.getX() != to.getX() - || from.getY() != to.getY() - || from.getZ() != to.getZ())) { - e.setTo(e.getFrom()); - } - return; - } - if (to != null && (from.getX() != to.getX() - || from.getY() != to.getY() - || from.getZ() != to.getZ())) { - afk.recordAction(p.getUniqueId()); - } - } - - @EventHandler - public void onInteract(PlayerInteractEvent e) { - Player p = e.getPlayer(); - if (isFrozen(p)) { - e.setCancelled(true); - return; - } - // 右键菜单雪球 - if (e.getAction().name() - .contains("RIGHT") - || e.getAction().name() - .contains("LEFT")) { - ItemStack hand = - p.getInventory() - .getItemInMainHand(); - if (isMenuSnowball(hand)) { - e.setCancelled(true); - gui.openMain(p); - return; - } - } - } - - - @EventHandler - public void onBlockBreak(BlockBreakEvent e) { - Player p = e.getPlayer(); - if (isFrozen(p)) { - e.setCancelled(true); - return; - } - afk.recordAction(p.getUniqueId()); - } - - @EventHandler - public void onBlockPlace(BlockPlaceEvent e) { - if (isFrozen(e.getPlayer())) - e.setCancelled(true); - } - - @EventHandler - public void onDrop(PlayerDropItemEvent e) { - if (isFrozen(e.getPlayer())) { - e.setCancelled(true); - return; - } - // 禁止丢弃菜单雪球 - ItemStack drop = - e.getItemDrop().getItemStack(); - if (isMenuSnowball(drop)) { - e.setCancelled(true); - } - } - - @EventHandler - public void onPickup(PlayerPickupItemEvent e) { - if (isFrozen(e.getPlayer())) - e.setCancelled(true); - } - - @EventHandler - public void onDamage(EntityDamageEvent e) { - if (e.getEntity() instanceof Player) { - if (isFrozen((Player) e.getEntity())) - e.setCancelled(true); - } - } - - @EventHandler - public void onEntityAttack( - EntityDamageByEntityEvent e) { - if (e.getDamager() instanceof Player - && isFrozen( - (Player) e.getDamager())) - e.setCancelled(true); - if (e.getEntity() instanceof Player - && isFrozen( - (Player) e.getEntity())) - e.setCancelled(true); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onChat(AsyncPlayerChatEvent e) { - Player p = e.getPlayer(); - String msg = e.getMessage(); - - if (chatInput.getState(p).type - != ChatInputManager.InputType.NONE) { - e.setCancelled(true); - ChatInputManager.InputType inputType = - chatInput.getState(p).type; - if (ticket != null - && ticket.handleChatInput( - p, msg, inputType)) { - return; - } - chatInput.handleInput(p, msg, this); - return; - } - - if (isFrozen(p)) { - e.setCancelled(true); - return; - } - - if (chatFilter == null) return; - if (chatFilter.isPlayerWhitelisted( - p.getName())) return; - if (chatFilter.isMuted(p.getName())) { - e.setCancelled(true); - p.sendMessage( - chatFilter.msg("chat_muted")); - return; - } - - List urls = - chatFilter.extractUrls(msg); - if (urls.isEmpty()) return; - - for (String u : urls) { - if (chatFilter.isWhitelisted(u)) - continue; - if (!chatFilter.isLikelyDomain(u)) - continue; - - e.setCancelled(true); - chatFilter.incrementViolation( - p.getName()); - int count = chatFilter - .getViolationCount(p.getName()); - - p.sendMessage(chatFilter - .msg("chat_url_blocked") - .replace("{url}", u)); - p.sendMessage(chatFilter - .msg("chat_url_violation") - .replace("{count}", - String.valueOf(count))); - - chatFilter.applyPunishment(p, count); - - if (chatFilter.isNotifyAdmin()) { - String tag = config.adminTag; - for (Player op : - Bukkit.getOnlinePlayers()) { - if (op.getScoreboardTags() - .contains(tag) - && !op.equals(p)) { - op.sendMessage(chatFilter - .msg("chat_url_admin_notify") - .replace("{player}", - p.getName()) - .replace("{url}", u) - .replace("{count}", - String.valueOf( - count))); - } - } - } - - if (chatFilter.isNotifyAll()) { - Bukkit.broadcastMessage(chatFilter - .msg("chat_url_broadcast") - .replace("{player}", - p.getName())); - } - } - } - - - - // ===== Inventory Click ===== - @EventHandler - public void onInvClick(InventoryClickEvent e) { - if (!(e.getWhoClicked() instanceof Player)) - return; - Player p = (Player) e.getWhoClicked(); - if (isFrozen(p)) { - e.setCancelled(true); - return; - } - String title = - e.getView().getTitle(); - int slot = e.getRawSlot(); - - if (needsPasswordChange.contains(p.getName()) - && !title.equals("§6§l任务面板")) { - e.setCancelled(true); - p.sendMessage("§c请先修改密码!"); - return; - } - - // 工单GUI - if (title.equals( - TicketManager.T_TICKET_MAIN) - || title.equals( - TicketManager.T_TICKET_CREATE) - || title.equals( - TicketManager.T_TICKET_MY) - || title.equals( - TicketManager.T_TICKET_ADMIN) - || title.equals( - TicketManager.T_TICKET_DETAIL) - || title.equals("§d§l服务商面板") - || title.equals("§6§l抢单大厅") - || title.equals( - "§b§l我处理中的工单")) { - e.setCancelled(true); - ticket.handleClick(p, title, slot); - return; - } - - // 垃圾回收站 - if (title.equals("§6§l垃圾回收站")) { - if (slot < 0 || slot >= 54) return; - - // 功能格 49-53 - if (slot >= 49 && slot <= 53) { - e.setCancelled(true); - garbage.handleClick( - p, title, slot); - return; - } - - ItemStack inSlot = - e.getInventory().getItem(slot); - ItemStack onCursor = e.getCursor(); - - // 1. 格子里是垃圾站物品 → 取出 - if (isTrashDisplayItem(inSlot)) { - e.setCancelled(true); - try { - List lore = - inSlot.getItemMeta() - .getLore(); - String numStr = - lore.get(0) - .replace( - "§7ID: #", "") - .trim(); - int dbId = Integer - .parseInt(numStr); - // 先从DB删除 - if (garbage.removeItem( - dbId)) { - // 再给玩家(去掉ID lore) - ItemStack give = - inSlot.clone(); - ItemMeta gm = - give.getItemMeta(); - if (gm != null) { - List newLore = - new ArrayList<>(); - // 保留原始lore(跳过ID和数量行) - if (lore.size() > 2) { - for (int li = 2; - li < lore - .size(); - li++) { - newLore.add( - lore.get( - li)); - } - } - gm.setLore( - newLore.isEmpty() - ? null - : newLore); - give.setItemMeta(gm); - } - p.getInventory() - .addItem(give); - // 清空格子 - e.getInventory() - .setItem(slot, - null); - } - } catch (Exception ex) { - p.sendMessage("§c取出失败"); - } - return; - } - - // 2. 格子空 + 手上有物品 → 放入 - if ((inSlot == null - || inSlot.getType() - == Material.AIR) - && onCursor != null - && onCursor.getType() - != Material.AIR - && !isMenuSnowball( - onCursor)) { - // 不取消!让物品自然进入格子 - Bukkit.getScheduler() - .runTaskLater(this, - () -> { - ItemStack after = - e.getInventory() - .getItem( - slot); - if (after != null - && after.getType() - != Material.AIR) { - garbage.saveItem( - after - .clone()); - e.getInventory() - .setItem( - slot, - null); - } - }, 1L); - return; - } - - // 3. 其他情况 - e.setCancelled(true); - return; - } - - // 主菜单 - if (title.equals(GUIManager.T_MAIN)) { - e.setCancelled(true); - if (slot == 10) gui.openMyInfo(p); - else if (slot == 12) gui.openInvite(p); - else if (slot == 14) - p.openInventory( - points.createShopGUI(p)); - else if (slot == 16) - gui.openTaskCenter(p); - else if (slot == 18) ticket.openMain(p); - else if (slot == 20) { - if (p.isOp() || p.getScoreboardTags() - .contains(config.adminTag)) { - garbage.openRecycle(p); - } - } - - else if (slot == 22 && isAdmin(p)) { - p.closeInventory(); - chatInput.getState(p).type = - ChatInputManager.InputType - .ADMIN_AUTH; - p.sendMessage("请输入管理密码:"); - } - return; - } - - - // 我的信息 - if (title.equals(GUIManager.T_MY_INFO)) { - e.setCancelled(true); - if (slot == 26) gui.openMain(p); - else if (slot == 10) { - if (checkIn.isCheckedInToday( - p.getName())) - p.sendMessage(config.msg( - "checkin_already")); - else { - p.sendMessage( - checkIn.checkIn(p)); - invite.onInviteeCheckIn( - p.getName()); - playSuccessSound(p); - gui.openMyInfo(p); - } - } else if (slot == 11) { - chatInput.getState(p).type = - ChatInputManager.InputType - .CHANGE_PWD_STEP1; - p.closeInventory(); - p.sendMessage("§e请输入当前密码:"); - } else if (slot == 12) { - chatInput.getState(p).type = - ChatInputManager.InputType - .BACK_CHECKIN; - p.closeInventory(); - p.sendMessage("§e请输入补签日期:"); - } else if (slot == 13) { - String code = (String) db.getField( - p.getName(), "invite_code"); - if (code == null || code.isEmpty()) - code = invite.generateCode(p); - p.sendMessage("§e邀请码: " + code); - } else if (slot == 14) { - chatInput.getState(p).type = - ChatInputManager.InputType - .SET_EMAIL; - p.closeInventory(); - p.sendMessage("§e请输入邮箱:"); - } else if (slot == 15) - p.openInventory( - points.createShopGUI(p)); - return; - } - - // 管理面板 - if (title.equals(GUIManager.T_ADMIN)) { - e.setCancelled(true); - if (slot == 22) gui.openMain(p); - else if (slot == 10) { - chatInput.getState(p).type = - ChatInputManager.InputType - .SMTP_CONFIG; - p.closeInventory(); - p.sendMessage("§e当前SMTP: " - + config.getSmtp("smtp地址")); - p.sendMessage("§e输入新地址(0跳过):"); - } else if (slot == 11) - gui.openUserManagement(p); - else if (slot == 12) gui.openMyInfo(p); - else if (slot == 14) - p.openInventory( - points.createShopGUI(p)); - else if (slot == 16) { - if (e.getClick().isLeftClick()) { - config.afkEnabled = - !config.afkEnabled; - p.sendMessage(config.msg( - config.afkEnabled - ? "afk_set_enabled" - : "afk_set_disabled")); - config.saveSettings(); - gui.openAdmin(p); - } else if (e.getClick() - .isRightClick()) { - p.closeInventory(); - chatInput.getState(p).type = - ChatInputManager.InputType - .ADMIN_SET_AFK_TIME; - p.sendMessage( - "§e输入挂机时长(分钟):"); - } - } - return; - } - - // 任务中心 - if (title.equals( - GUIManager.T_TASK_CENTER)) { - e.setCancelled(true); - if (slot == 22) gui.openMain(p); - else if (slot == 10) - gui.openGiftStages(p); - else if (slot == 12) - gui.openTaskList(p, "主线任务"); - else if (slot == 14) - gui.openTaskList(p, "支线任务"); - else if (slot == 16) { - if (dailySign(p)) { - gui.openTaskCenter(p); - playSuccessSound(p); - } else - p.sendMessage(config.msg( - "checkin_already")); - } - return; - } - - // 新人礼包 - if (title.equals( - GUIManager.T_GIFT_STAGES)) { - e.setCancelled(true); - if (slot == 26) - gui.openTaskCenter(p); - else if (slot >= 10 && slot <= 18) { - int stage = slot - 9; - if (gift.canClaim(p, stage)) { - gift.claimReward(p, stage); - gui.openGiftStages(p); - } else - p.sendMessage(config.msg( - "gift_not_ready")); - } - return; - } - - // 邀请 - if (title.equals(GUIManager.T_INVITE)) { - e.setCancelled(true); - if (slot == 26) gui.openMain(p); - else if (slot == 10) { - String code = - invite.generateCode(p); - p.sendMessage("§e邀请码: " + code); - gui.openInvite(p); - } else if (slot == 16) { - chatInput.getState(p).type = - ChatInputManager.InputType - .INVITE_INPUT_CODE; - p.closeInventory(); - p.sendMessage("§e请输入邀请码:"); - } - return; - } - - // 用户管理 - if (title.equals( - GUIManager.T_USER_MGMT)) { - e.setCancelled(true); - if (slot == 53) - gui.openAdmin(p); - else if (slot >= 0 && slot < 45) { - ItemStack item = - e.getInventory().getItem(slot); - if (item != null - && item.hasItemMeta() - && item.getItemMeta() - .hasDisplayName()) - gui.openUserDetail(p, - item.getItemMeta() - .getDisplayName() - .replace("§e", "") - .replace("§a", "")); - } - return; - } - - // 用户详情 - if (title.startsWith("§e§l管理: ")) { - e.setCancelled(true); - String tgt = title.substring( - "§e§l管理: ".length()); - if (slot == 22) - gui.openUserManagement(p); - else if (slot == 10) { - chatInput.getState(p).type = - ChatInputManager.InputType - .ADMIN_SET_POINTS; - chatInput.getState(p) - .targetPlayer = tgt; - p.closeInventory(); - p.sendMessage("§e输入" + tgt - + "的新积分:"); - } else if (slot == 12) { - chatInput.getState(p).type = - ChatInputManager.InputType - .ADMIN_SET_PWD; - chatInput.getState(p) - .targetPlayer = tgt; - p.closeInventory(); - p.sendMessage("§e输入" + tgt - + "的临时密码:"); - } else if (slot == 16) { - chatInput.getState(p).type = - ChatInputManager.InputType - .ADMIN_DELETE_CONFIRM; - chatInput.getState(p) - .targetPlayer = tgt; - p.closeInventory(); - p.sendMessage(config.msg( - "admin_delete_confirm")); - } - return; - } - - // 积分商城 - if (title.startsWith("§d§l积分商城")) { - e.setCancelled(true); - if (slot == 49) gui.openMain(p); - else if (slot == 50 && isAdmin(p)) { - gui.openShopAdmin(p); - } else - points.handleClick(p, slot); - return; - } - - // 商城管理 - if (title.equals("§c§l商城管理")) { - e.setCancelled(true); - if (slot == 49) { - p.openInventory( - points.createShopGUI(p)); - return; - } - return; - } - - // CY背包商城 - if (title.startsWith("§d§lCY背包商城")) { - e.setCancelled(true); - int size = e.getInventory().getSize(); - if (slot == size - 1) - p.openInventory( - points.createShopGUI(p)); - else points.handleCYClick(p, slot); - return; - } - - // 任务面板 - if (title.equals("§6§l任务面板")) { - e.setCancelled(true); - if (slot == 22) gui.openMain(p); - else if (slot == 15) { - if (dailySign(p)) { - openTaskPanel(p); - playSuccessSound(p); - } else - p.sendMessage(config.msg( - "checkin_already")); - } - return; - } - - // 账号请求面板 - if (title.equals("§e§l账号请求")) { - e.setCancelled(true); - if (slot == 22) gui.openMain(p); - else if (slot == 10) { - p.closeInventory(); - p.sendMessage("§c§l[确认] §f请输入:"); - p.sendMessage("§e/sdf1_login del " - + p.getName()); - } else if (slot == 13) { - ticket.openCreate(p); - } else if (slot == 16) { - p.closeInventory(); - loginMgr.handleReset(p); - } - return; - } - - // 主线/支线任务 - if (title.equals("§d§l主线任务") - || title.equals("§d§l支线任务")) { - e.setCancelled(true); - if (slot == e.getInventory().getSize() - 1) - gui.openTaskCenter(p); - return; - } - } - - @EventHandler - public void onInvDrag(InventoryDragEvent e) { - if (!(e.getWhoClicked() instanceof Player)) - return; - Player p = (Player) e.getWhoClicked(); - if (isFrozen(p)) { - e.setCancelled(true); - return; - } - String t = e.getView().getTitle(); - - // 回收站拖拽 - if (t.equals("§6§l垃圾回收站")) { - for (int rawSlot : e.getRawSlots()) { - if (rawSlot >= 49 - && rawSlot <= 53) { - e.setCancelled(true); - return; - } - } - // 0-48:不取消,延迟存DB - Bukkit.getScheduler() - .runTaskLater(this, () -> { - for (int rawSlot : - e.getRawSlots()) { - if (rawSlot < 0 - || rawSlot >= 49) - continue; - ItemStack item = - e.getInventory() - .getItem( - rawSlot); - if (item != null - && item.getType() - != Material.AIR - && !isMenuSnowball( - item)) { - garbage.saveItem( - item.clone()); - e.getInventory() - .setItem( - rawSlot, - null); - } - } - }, 1L); - return; - } - - - if (t.equals(TicketManager.T_TICKET_MAIN) - || t.equals( - TicketManager.T_TICKET_CREATE) - || t.equals( - TicketManager.T_TICKET_MY) - || t.equals( - TicketManager.T_TICKET_ADMIN) - || t.equals( - TicketManager.T_TICKET_DETAIL) - || t.equals("§d§l服务商面板") - || t.equals("§6§l抢单大厅") - || t.equals( - "§b§l我处理中的工单") - || t.equals("§6§l我的历史工单") - || t.equals("§c§l商城管理")) { - e.setCancelled(true); - } - } - - // ===== Commands ===== - @Override - public boolean onCommand(CommandSender sender, - Command cmd, String label, - String[] args) { - - String cmdName = cmd.getName().toLowerCase(); - - if (cmdName.equals("recycle")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player pr = (Player) sender; - if (!pr.isOp() - && !pr.getScoreboardTags() - .contains(config.adminTag)) { - pr.sendMessage("§c权限不足"); - return true; - } - garbage.openRecycle(pr); - return true; - } - - - // /垃圾清理 - if (cmdName.equals("垃圾清理")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - int count = garbage.collectItems(); - sender.sendMessage("§a已清理 " - + count + " 个掉落物"); - return true; - } - - if (cmdName.equals("reg") - || cmdName.equals("注册")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - chatInput.reset(p); - if (db.userExists(p.getName())) { - p.sendMessage( - "§c您已注册过,请使用 /login"); - return true; - } - try { - loginMgr.handleRegister(p, args); - } catch (Exception e) { - getLogger().severe( - "[Sdf1_login] reg命令异常: " - + e.getMessage()); - e.printStackTrace(); - p.sendMessage("§c注册出错,请联系管理员"); - } - return true; - } - - - - if (cmdName.equals("login") - || cmdName.equals("l") - || cmdName.equals("登录")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - chatInput.reset(p); - if (loggedIn.contains(p.getName())) { - p.sendMessage("§c您已登录"); - return true; - } - try { - loginMgr.handleLogin(p, args); - } catch (Exception e) { - getLogger().severe( - "[Sdf1_login] login命令异常: " - + e.getMessage()); - e.printStackTrace(); - p.sendMessage("§c登录出错,请联系管理员"); - } - return true; - } - - // /找回密码 - if (cmdName.equals("找回密码")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - handleReset((Player) sender); - return true; - } - - // /签到 - if (cmdName.equals("签到")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - if (dailySign((Player) sender)) - playSuccessSound((Player) sender); - else - ((Player) sender).sendMessage( - config.msg("checkin_already")); - return true; - } - - // 绑定邮箱 - if (cmdName.equals("绑定邮箱")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p2 = (Player) sender; - if (args.length >= 1) { - String emailAddr = args[0]; - if (!emailAddr.contains("@") - || !emailAddr.contains(".")) { - p2.sendMessage("§c邮箱格式不正确"); - return true; - } - String oldEmail = (String) db.getField( - p2.getName(), "email"); - if (oldEmail != null - && !oldEmail.isEmpty()) { - chatInput.getState(p2).type = - ChatInputManager.InputType - .SET_EMAIL_VERIFY; - chatInput.getState(p2) - .targetPlayer = emailAddr; - chatInput.getState(p2) - .ticketTitle = "need_pwd"; - p2.sendMessage("§e您已绑定邮箱: " - + oldEmail); - p2.sendMessage( - "§e请输入登录密码以确认修改:"); - } else { - String code = String.valueOf( - (int) (Math.random() - * 900000 + 100000)); - chatInput.getState(p2).type = - ChatInputManager.InputType - .SET_EMAIL_VERIFY; - chatInput.getState(p2) - .targetPlayer = emailAddr; - chatInput.getState(p2) - .ticketTitle = code; - final String to = emailAddr; - final String c = code; - final Player fp = p2; - Bukkit.getScheduler() - .runTaskAsynchronously(this, - () -> { - boolean sent = email - .sendVerifyCode(to, c); - Bukkit.getScheduler().runTask( - this, () -> { - if (sent) { - fp.sendMessage( - "§a验证码已发送到 " - + to); - fp.sendMessage( - "§e输入6位验证码(0取消):"); - } else { - fp.sendMessage( - "§c发送失败"); - } - }); - }); - } - } else { - chatInput.getState(p2).type = - ChatInputManager.InputType - .SET_EMAIL; - p2.sendMessage("§e请输入邮箱:"); - } - return true; - } - - - // /sdf1debug - if (cmdName.equals("sdf1debug")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p2 = (Player) sender; - if (!isAdmin(sender)) { - p2.sendMessage("§c权限不足"); - return true; - } - if (args.length < 2) { - p2.sendMessage("§e用法: /sdf1debug <玩家> <阶段>"); - return true; - } - String tgt = args[0]; - try { - int stage = Integer.parseInt(args[1]); - db.setField(tgt, "gift_stage", stage); - p2.sendMessage("§a已将 " + tgt - + " 的任务阶段设为 " + stage); - } catch (NumberFormatException ex) { - p2.sendMessage("§c请输入有效数字"); - } - return true; - } - - // /oa - if (cmdName.equals("oa")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - openAccountRequestPanel( - (Player) sender); - return true; - } - - // /玩家信息 - if (cmdName.equals("玩家信息")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - gui.openMain((Player) sender); - return true; - } - - // ===== /sdf1_login ===== - if (!cmdName.equals("sdf1_login")) - return false; - - if (args.length == 0) { - if (!(sender instanceof Player)) { - sender.sendMessage( - "§e用法: /sdf1_login <子命令>"); - return true; - } - gui.openMain((Player) sender); - return true; - } - - String sub = args[0].toLowerCase(); - - // stop - if (sub.equals("stop")) { - if (!(sender instanceof Player)) { - // 控制台 - if (args.length >= 2) { - db.clearTempPassword(args[1]); - sender.sendMessage("§a已报废 " - + args[1] + " 的临时密码"); - } else { - db.clearAllTempPasswords(); - sender.sendMessage( - "§a已报废所有临时密码"); - } - return true; - } - Player p2 = (Player) sender; - if (args.length >= 2) { - String tgt = args[1]; - String myIP = - getPlayerIP(p2); - String targetIP = - db.getPlayerIPByName(tgt); - if (targetIP == null) { - Player tp = - Bukkit.getPlayer(tgt); - if (tp != null && tp.isOnline()) - targetIP = - getPlayerIP(tp); - } - if (targetIP == null - || !targetIP.equals(myIP)) { - p2.sendMessage( - "§c只能操作同IP下的玩家"); - return true; - } - db.clearTempPassword(tgt); - p2.sendMessage("§a已报废 " + tgt - + " 的临时密码"); - } else { - db.clearTempPassword(p2.getName()); - p2.sendMessage( - "§a已报废您的临时密码"); - } - return true; - } - - // reload - if (sub.equals("reload")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - config.loadMessages(); - config.loadSmtp(); - config.loadSettings(); - chatFilter.loadConfig(); - sender.sendMessage("§a配置已重载!"); - return true; - } - // back - 恢复背包备份 - if (sub.equals("back")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 2) { - sender.sendMessage( - "§e用法: /sdf1_login back <玩家> [#编号]"); - return true; - } - String backTarget = args[1]; - if (args.length >= 3 - && args[2].startsWith("#")) { - // /sdf1_login back Aplayer #1 - int backupId; - try { - backupId = Integer.parseInt( - args[2].replace("#", "")); - } catch (NumberFormatException ex) { - sender.sendMessage( - "§c无效编号"); - return true; - } - String[] backup = - db.getInventoryBackupById( - backupId); - if (backup == null) { - sender.sendMessage( - "§c备份 #" + backupId - + " 不存在"); - return true; - } - // 如果玩家在线,直接恢复 - Player tp = Bukkit.getPlayer( - backTarget); - if (tp != null && tp.isOnline()) { - try { - tp.getInventory() - .setContents( - InventorySerializer - .fromBase64( - backup[0])); - if (backup[1] != null - && !backup[1] - .isEmpty()) - tp.getInventory() - .setArmorContents( - InventorySerializer - .fromBase64( - backup[1])); - if (backup[2] != null - && !backup[2] - .isEmpty()) - tp.getInventory() - .setExtraContents( - InventorySerializer - .fromBase64( - backup[2])); - int lv = Integer.parseInt( - backup[3]); - if (lv > 0) - tp.setLevel(lv); - tp.sendMessage( - "§a管理员已恢复您的背包"); - sender.sendMessage( - "§a已恢复 " + backTarget - + " 的背包 (备份#" - + backupId + ")"); - } catch (Exception e) { - sender.sendMessage( - "§c恢复失败: " - + e.getMessage()); - } - } else { - // 玩家离线:保存到数据库供下次登录恢复 - db.saveInventoryBackup( - backTarget, - backup[0], - backup[1], - backup[2], - Integer.parseInt( - backup[3]), - Double.parseDouble( - backup[4])); - sender.sendMessage( - "§a" + backTarget - + " 离线,备份已写入数据库," - + "下次登录时恢复"); - } - } else { - // /sdf1_login back Aplayer - // 罗列最近5条备份 - List> backups = - db.getInventoryBackups( - backTarget, 5); - if (backups.isEmpty()) { - sender.sendMessage( - "§c" + backTarget - + " 没有背包备份"); - return true; - } - sender.sendMessage( - "§e§l=== " + backTarget - + " 背包备份 ==="); - java.text.SimpleDateFormat sdf = - new java.text.SimpleDateFormat( - "MM-dd HH:mm:ss"); - for (Map b : - backups) { - int bid = ((Number) - b.get("id")) - .intValue(); - Object stObj = - b.get("save_time"); - long st = stObj != null - ? ((Number) stObj) - .longValue() : 0; - String time = st > 0 - ? sdf.format( - new java.util.Date(st)) - : "未知时间"; - String itemNames = - getBackupItemPreview( - bid); - sender.sendMessage( - "§7#" + bid - + " §f" + time - + " §7: " - + itemNames); - } - sender.sendMessage( - "§7使用 §e/sdf1_login back " - + backTarget - + " #编号 §7恢复"); - sender.sendMessage( - "§e§l================================"); - } - } - - // pw / password - if (sub.equals("pw") - || sub.equals("password")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p2 = (Player) sender; - - if (args.length >= 3) { - // /sdf1_login pw 旧密码 新密码 - String oldPwd = args[1]; - String newPwd = args[2]; - if (!PasswordUtils.validate( - newPwd)) { - p2.sendMessage(config.msg( - "password_format_error")); - return true; - } - String salt = (String) db.getField( - p2.getName(), - "password_salt"); - String hash = - PasswordUtils.hash( - oldPwd, salt); - boolean mainOk = - db.checkPassword( - p2.getName(), hash); - boolean tempOk = - db.checkPasswordOrTemp( - p2.getName(), hash); - if (!mainOk && !tempOk) { - p2.sendMessage(config.msg( - "password_wrong")); - return true; - } - String oldHash = - (String) db.getField( - p2.getName(), - "password_hash"); - String newSalt = - PasswordUtils.generateSalt(); - String newHash = - PasswordUtils.hash( - newPwd, newSalt); - if (newHash.equals(oldHash)) { - p2.sendMessage(config.msg( - "password_same")); - return true; - } - recordPasswordChange( - p2.getName(), - oldHash, salt); - db.setField(p2.getName(), - "password_hash", newHash); - db.setField(p2.getName(), - "password_salt", newSalt); - if (!mainOk && tempOk) { - db.clearTempPassword( - p2.getName()); - } - needsPasswordChange.remove( - p2.getName()); - p2.sendMessage(config.msg( - "password_changed")); - return true; - } - - if (args.length == 2) { - // /sdf1_login pw 旧密码 - // 验证旧密码,然后聊天输入新密码 - String oldPwd = args[1]; - String salt = (String) db.getField( - p2.getName(), - "password_salt"); - String hash = - PasswordUtils.hash( - oldPwd, salt); - boolean mainOk = - db.checkPassword( - p2.getName(), hash); - boolean tempOk = - db.checkPasswordOrTemp( - p2.getName(), hash); - if (!mainOk && !tempOk) { - p2.sendMessage(config.msg( - "password_wrong")); - return true; - } - chatInput.getState(p2).type = - ChatInputManager.InputType - .CHANGE_PWD_STEP2; - chatInput.getState(p2) - .ticketTitle = - mainOk ? "main" : "temp"; - p2.sendMessage( - "§e请输入新密码:"); - return true; - } - - // /sdf1_login pw (无参数) - chatInput.getState(p2).type = - ChatInputManager.InputType - .CHANGE_PWD_STEP1; - p2.sendMessage( - "§e请输入当前密码:"); - return true; - } - - // email - if (sub.equals("email")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p2 = (Player) sender; - if (args.length >= 2) { - String emailAddr = args[1]; - if (!emailAddr.contains("@") - || !emailAddr.contains(".")) { - p2.sendMessage("§c邮箱格式不正确"); - return true; - } - String oldEmail = (String) db.getField( - p2.getName(), "email"); - if (oldEmail != null - && !oldEmail.isEmpty()) { - // 已绑定:先验证密码 - chatInput.getState(p2).type = - ChatInputManager.InputType - .SET_EMAIL_VERIFY; - chatInput.getState(p2) - .targetPlayer = emailAddr; - chatInput.getState(p2) - .ticketTitle = "need_pwd"; - p2.sendMessage("§e您已绑定邮箱: " - + oldEmail); - p2.sendMessage( - "§e请输入登录密码以确认修改:"); - } else { - // 未绑定:直接发验证码 - String code = String.valueOf( - (int) (Math.random() - * 900000 + 100000)); - chatInput.getState(p2).type = - ChatInputManager.InputType - .SET_EMAIL_VERIFY; - chatInput.getState(p2) - .targetPlayer = emailAddr; - chatInput.getState(p2) - .ticketTitle = code; - final String to = emailAddr; - final String c = code; - final Player fp = p2; - Bukkit.getScheduler() - .runTaskAsynchronously(this, - () -> { - boolean sent = email - .sendVerifyCode(to, c); - Bukkit.getScheduler().runTask( - this, () -> { - if (sent) { - fp.sendMessage( - "§a验证码已发送到 " - + to); - fp.sendMessage( - "§e输入6位验证码(0取消):"); - } else { - fp.sendMessage( - "§c发送失败"); - } - }); - }); - } - } else { - chatInput.getState(p2).type = - ChatInputManager.InputType - .SET_EMAIL; - p2.sendMessage("§e请输入邮箱:"); - } - return true; - } - - - // sign - if (sub.equals("sign")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - if (dailySign((Player) sender)) - playSuccessSound((Player) sender); - else - ((Player) sender).sendMessage( - config.msg("checkin_already")); - return true; - } - - // reset - if (sub.equals("reset")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - loginMgr.handleReset( - (Player) sender); - return true; - } - - // undo - if (sub.equals("undo")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p2 = (Player) sender; - PwdRollback rb = - pwdRollback.get(p2.getName()); - if (rb == null) { - p2.sendMessage("§c无可撤销记录"); - return true; - } - if (System.currentTimeMillis() - rb.time - > 600000L) { - pwdRollback.remove(p2.getName()); - p2.sendMessage("§c撤销已过期"); - return true; - } - db.setField(p2.getName(), - "password_hash", rb.hash); - db.setField(p2.getName(), - "password_salt", rb.salt); - pwdRollback.remove(p2.getName()); - p2.sendMessage("§a密码已撤销!"); - return true; - } - - // take - if (sub.equals("take")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 3) { - sender.sendMessage( - "§e用法: /sdf1_login take <玩家> <积分>"); - return true; - } - int amount; - try { - amount = Integer.parseInt(args[2]); - } catch (NumberFormatException ex) { - sender.sendMessage("§c无效数字"); - return true; - } - if (!db.deductPoints(args[1], amount)) { - sender.sendMessage("§c积分不足"); - return true; - } - sender.sendMessage("§a已扣除 " + args[1] - + " " + amount + " 积分"); - return true; - } - - // set - if (sub.equals("set")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 3) { - sender.sendMessage( - "§e用法: /sdf1_login set <玩家> <临时密码>"); - return true; - } - String tgt = args[1]; - String tempPwd = args[2]; - if (!PasswordUtils.validate(tempPwd)) { - sender.sendMessage("§c密码格式不符合要求"); - return true; - } - if (!db.userExists(tgt)) { - sender.sendMessage("§c玩家不存在"); - return true; - } - String salt = (String) db.getField( - tgt, "password_salt"); - String hash = PasswordUtils.hash( - tempPwd, salt); - db.setField(tgt, "temp_password", hash); - db.setField(tgt, "temp_pw_expire", - System.currentTimeMillis() - + 300000L); - - db.setField(tgt, "temp_pw_used", 0); - sender.sendMessage("§a已为 " + tgt - + " 设置临时密码"); - Player tp = Bukkit.getPlayer(tgt); - if (tp != null && tp.isOnline()) - tp.kickPlayer( - "§c管理员已设置临时密码"); - // 邮件通知 - String emailAddr = (String) db.getField( - tgt, "email"); - if (emailAddr != null - && !emailAddr.isEmpty()) { - String time = - new java.text.SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss") - .format(new java.util.Date()); - String body = "玩家 " + tgt - + ":\n您的密码已于 " + time - + " 被管理员重置为临时密码。\n" - + "如果是本人操作请忽略。\n" - + "如非本人操作请立即联系管理员。"; - final String to = emailAddr; - final String fName = tgt; - final String bodyF = body; - Bukkit.getScheduler() - .runTaskAsynchronously(this, - () -> email - .sendTempPassword( - to, fName, - bodyF)); - } - return true; - } - - // shopadd - if (sub.equals("shopadd")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 4) { - sender.sendMessage( - "§e用法: /sdf1_login shopadd <名称> <价格> <命令>"); - return true; - } - String shopName = args[1]; - int price; - try { - price = Integer.parseInt(args[2]); - } catch (NumberFormatException ex) { - sender.sendMessage("§c无效数字"); - return true; - } - String shopCmd = args[3]; - points.addItem(shopName, price, shopCmd); - sender.sendMessage("§a已添加商品: " - + shopName + " (" + price - + "积分)"); - return true; - } - - // shopdel - if (sub.equals("shopdel")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 2) { - sender.sendMessage( - "§e用法: /sdf1_login shopdel <序号>"); - return true; - } - try { - int idx = Integer.parseInt(args[1]); - points.removeItem(idx); - sender.sendMessage("§a已删除商品 #" + idx); - } catch (NumberFormatException ex) { - sender.sendMessage("§c无效数字"); - } - return true; - } - - // get - if (sub.equals("get")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 2) { - sender.sendMessage( - "§e用法: /sdf1_login get <玩家>"); - return true; - } - Map user = - db.getUser(args[1]); - if (user.isEmpty()) { - sender.sendMessage("§c玩家不存在"); - return true; - } - sender.sendMessage( - "§e=== " + args[1] + " ==="); - sender.sendMessage("§7积分: §e" - + user.getOrDefault("points", 0)); - sender.sendMessage("§7邮箱: §e" - + user.getOrDefault("email", "无")); - sender.sendMessage("§7签到天数: §e" - + user.getOrDefault( - "total_checkin_days", 0)); - sender.sendMessage("§7邀请码: §e" - + user.getOrDefault( - "invite_code", "无")); - return true; - } - - // del - if (sub.equals("del")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 2) { - sender.sendMessage( - "§e用法: /sdf1_login del <玩家>"); - return true; - } - db.deleteUser(args[1]); - sender.sendMessage("§a已删除 " + args[1]); - Player tp = Bukkit.getPlayer(args[1]); - if (tp != null && tp.isOnline()) - tp.kickPlayer("§c账号已被管理员删除"); - return true; - } - - // kick (挂机) - if (sub.equals("kick")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 2) { - sender.sendMessage( - "§e用法: /sdf1_login kick <开关> [分钟]"); - return true; - } - String mode = args[1].toLowerCase(); - if (mode.equals("on") - || mode.equals("开") - || mode.equals("启用")) { - config.afkEnabled = true; - if (args.length >= 3) { - try { - config.afkTimeout = - Integer.parseInt( - args[2]) * 60; - } catch (NumberFormatException ignored) { - } - } - config.saveSettings(); - int minutes = - config.afkTimeout / 60; - sender.sendMessage( - "§a挂机踢出已开启,超时: " - + minutes + " 分钟"); - } else if (mode.equals("off") - || mode.equals("关") - || mode.equals("停用")) { - config.afkEnabled = false; - config.saveSettings(); - sender.sendMessage( - "§c挂机踢出已关闭"); - } else { - sender.sendMessage( - "§c无效参数 on/off"); - } - return true; - } - - // add - if (sub.equals("add")) { - if (!isAdmin(sender)) { - sender.sendMessage("§c权限不足"); - return true; - } - if (args.length < 3) { - sender.sendMessage( - "§e用法: /sdf1_login add <玩家> <积分>"); - return true; - } - int amount; - try { - amount = Integer.parseInt(args[2]); - } catch (NumberFormatException ex) { - sender.sendMessage("§c无效数字"); - return true; - } - db.addPoints(args[1], amount); - sender.sendMessage("§a已为 " + args[1] - + " 增加 " + amount + " 积分"); - return true; - } - - // ticket - if (sub.equals("ticket") - || sub.equals("工单")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - ticket.openMain((Player) sender); - return true; - } - - sender.sendMessage("§c未知参数"); - return true; - } - /** - * 是否是垃圾站展示物品(带ID lore) - */ - private boolean isTrashDisplayItem( - ItemStack item) { - if (item == null - || item.getType() - == Material.AIR) - return false; - if (!item.hasItemMeta()) - return false; - ItemMeta im = item.getItemMeta(); - if (im == null || !im.hasLore()) - return false; - List lore = im.getLore(); - if (lore == null || lore.isEmpty()) - return false; - return lore.get(0) - .startsWith("§7ID: #"); - } - - private void sendChatHelp(Player p) { - p.sendMessage( - "§e§l======== 聊天过滤帮助 =========="); - p.sendMessage("§e/chat reload §7- 重载配置"); - p.sendMessage("§e/chat add §7- 添加白名单URL"); - p.sendMessage("§e/chat remove §7- 移除白名单URL"); - p.sendMessage("§e/chat addplayer <玩家> §7- 添加白名单玩家"); - p.sendMessage("§e/chat takeplayer <玩家> §7- 移除白名单玩家"); - p.sendMessage("§e/chat unmute <玩家> §7- 解禁玩家"); - p.sendMessage("§e/chat reset <玩家> §7- 重置违规记录"); - p.sendMessage( - "§e§l=================================="); - } - private int findEmptySlot(Inventory inv) { - for (int i = 0; i < 49; i++) { - ItemStack item = inv.getItem(i); - if (item == null - || item.getType() - == Material.AIR) - return i; - } - return -1; - } - - // ===== TabComplete ===== - @Override - public List onTabComplete( - CommandSender sender, Command cmd, - String label, String[] args) { - List list = new ArrayList<>(); - if (cmd.getName().equalsIgnoreCase( - "sdf1_login") - && args.length == 1) { - if (isAdmin(sender)) { - list.addAll(Arrays.asList( - "reload", "pw", "email", - "sign", "reset", "undo", - "get", "del", "set", - "take", "add", "kick", - "ticket", "oa", "stop", - "shopadd", "shopdel","back")); - } else { - list.addAll(Arrays.asList( - "pw", "email", "sign", - "reset", "undo", - "ticket", "oa", "stop")); - } - } - if (cmd.getName().equalsIgnoreCase("chat") - && args.length == 1 - && isAdmin(sender)) { - list.addAll(Arrays.asList( - "reload", "add", "remove", - "addplayer", "takeplayer", - "unmute", "reset")); - } - return list; - } - -} +package Sdf1_login; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import org.bukkit.event.player.PlayerInteractEvent; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.LinkedHashMap; +import java.util.concurrent.ConcurrentHashMap; + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerPickupItemEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.scheduler.BukkitRunnable; + +public class Main extends JavaPlugin + implements CommandExecutor, Listener, + TabCompleter { + + private DatabaseManager db; + private ConfigManager config; + private EmailManager email; + private LoginManager loginMgr; + private AFKManager afk; + private GiftManager gift; + private CheckInManager checkIn; + private InviteManager invite; + private PointsManager points; + private GUIManager gui; + private ChatInputManager chatInput; + private Economy economy; + private VerificationManager verification; + private IPGroupManager ipGroup; + private AccountRequestManager accountRequest; + private TicketManager ticket; + private ChatFilterManager chatFilter; + private GarbageManager garbage; + + private final Set loggedIn = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Set needsPasswordChange = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map joinTime = + new ConcurrentHashMap<>(); + private final Map joinLoc = + new HashMap<>(); + private final Map ipAutoLogin = + new HashMap<>(); + private static final long IP_SKIP_MS = 300000L; + + private final Map lastActivity = + new ConcurrentHashMap<>(); + + private static class PwdRollback { + final String hash; + final String salt; + final long time; + PwdRollback(String h, String s, long t) { + hash = h; + salt = s; + time = t; + } + } + + private final Map + pwdRollback = + new ConcurrentHashMap<>(); + + public void recordPasswordChange(String name, + String oldHash, String oldSalt) { + pwdRollback.put(name, + new PwdRollback(oldHash, oldSalt, + System.currentTimeMillis())); + } + + // ===== Getters ===== + public DatabaseManager getDb() { return db; } + public ConfigManager getConfig2() { return config; } + public EmailManager getEmail() { return email; } + public Economy getEconomy() { return economy; } + public LoginManager getLoginMgr() { return loginMgr; } + public AFKManager getAfk() { return afk; } + public GiftManager getGift() { return gift; } + public CheckInManager getCheckIn() { return checkIn; } + public InviteManager getInvite() { return invite; } + public PointsManager getPoints() { return points; } + public GUIManager getGui() { return gui; } + public ChatInputManager getChatInput() { return chatInput; } + public VerificationManager getVerification() { return verification; } + public IPGroupManager getIPGroup() { return ipGroup; } + public AccountRequestManager getAccountRequest() { return accountRequest; } + public TicketManager getTicket() { return ticket; } + public ChatFilterManager getChatFilter() { return chatFilter; } + public GarbageManager getGarbage() { return garbage; } + public Set getLoggedIn() { return loggedIn; } + public Set getNeedsPasswordChange() { return needsPasswordChange; } + + // ===== Enable/Disable ===== + @Override + public void onEnable() { + getDataFolder().mkdirs(); + config = new ConfigManager(getDataFolder()); + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + getCommand("oa").setExecutor(this); + + db = new DatabaseManager(getDataFolder()); + db.init(); + email = new EmailManager(config); + loginMgr = new LoginManager(this); + afk = new AFKManager(config, this); + gift = new GiftManager(this); + gift.loadStages(); + checkIn = new CheckInManager(this); + invite = new InviteManager(this); + points = new PointsManager(this); + gui = new GUIManager(this); + chatInput = new ChatInputManager(); + verification = new VerificationManager(this); + ipGroup = new IPGroupManager(this, + config.maxAccountsPerIP); + accountRequest = + new AccountRequestManager(this); + accountRequest.setMode(config.approvalMode); + accountRequest.setAutoDelayMinutes( + config.autoApproveDelayMinutes); + ticket = new TicketManager(this); + garbage = new GarbageManager(this); + garbage.init(); + garbage.loadConfig(); + garbage.startAutoCleanup(); + chatFilter = new ChatFilterManager(this); + chatFilter.loadConfig(); + + setupEconomy(); + + if (getCommand("sdf1_login") != null) { + getCommand("sdf1_login").setExecutor(this); + getCommand("sdf1_login").setTabCompleter(this); + } + if (getCommand("reg") != null) + getCommand("reg").setExecutor(this); + if (getCommand("login") != null) + getCommand("login").setExecutor(this); + if (getCommand("l") != null) + getCommand("l").setExecutor(this); + if (getCommand("签到") != null) + getCommand("签到").setExecutor(this); + if (getCommand("recycle") != null) + getCommand("recycle").setExecutor(this); + if (getCommand("垃圾清理") != null) + getCommand("垃圾清理").setExecutor(this); + if (getCommand("绑定邮箱") != null) + getCommand("绑定邮箱").setExecutor(this); + if (getCommand("找回密码") != null) + getCommand("找回密码").setExecutor(this); + if (getCommand("玩家信息") != null) + getCommand("玩家信息").setExecutor(this); + if (getCommand("sdf1debug") != null) + getCommand("sdf1debug").setExecutor(this); + if (getCommand("oa") != null) + getCommand("oa").setExecutor(this); + if (getCommand("menu") != null) + getCommand("menu").setExecutor(this); + + + getServer().getPluginManager() + .registerEvents(this, this); + + afk.startCheck(); + startLoginReminder(); + startTimeoutCheck(); + startPasswordReminder(); + startTicketAutoProcess(); + + getLogger().info( + "Sdf1_login v1.0 | 就绪"); + } + + @Override + public void onDisable() { + if (db != null) db.close(); + if (garbage != null) garbage.close(); + } + + private void setupEconomy() { + if (getServer().getPluginManager() + .getPlugin("Vault") == null) return; + RegisteredServiceProvider rsp = + getServer().getServicesManager() + .getRegistration( + Economy.class); + if (rsp != null) + economy = rsp.getProvider(); + } + + // ===== Timers ===== + private void startTimeoutCheck() { + new BukkitRunnable() { + public void run() { + long now = System.currentTimeMillis(); + Iterator> it = + joinTime.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = + it.next(); + if (now - entry.getValue() + > config.loginTimeout + * 1000L) { + Player p = Bukkit.getPlayer( + entry.getKey()); + if (p != null && p.isOnline() + && !loggedIn.contains( + p.getName())) + p.kickPlayer(config.msg( + "login_timeout")); + it.remove(); + } + } + } + }.runTaskTimer(this, 80L, 80L); + } + + private void startLoginReminder() { + new BukkitRunnable() { + public void run() { + Iterator it = + joinTime.keySet().iterator(); + while (it.hasNext()) { + UUID uuid = it.next(); + Player p = Bukkit.getPlayer(uuid); + if (p == null || !p.isOnline()) { + it.remove(); + continue; + } + if (loggedIn.contains( + p.getName())) { + it.remove(); + continue; + } + // 已注册→提示登录,未注册→提示注册 + if (db.userExists(p.getName())) + p.sendMessage(config.msg( + "not_logged_in")); + else + p.sendMessage(config.msg( + "not_registered")); + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startPasswordReminder() { + new BukkitRunnable() { + public void run() { + for (Player p : + Bukkit.getOnlinePlayers()) { + if (needsPasswordChange.contains( + p.getName())) { + p.sendTitle("§c§l请修改密码", + "§f使用 /sdf1_login pw 修改密码", + 10, 40, 10); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 1.0f); + } + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startTicketAutoProcess() { + new BukkitRunnable() { + public void run() { + if (ticket != null) + ticket.autoProcessCompleted(); + } + }.runTaskTimer(this, 6000L, 6000L); + } + + // ===== Utils ===== + public String getPlayerIP(Player p) { + InetSocketAddress addr = p.getAddress(); + if (addr == null) return null; + return addr.getAddress().getHostAddress(); + } + + + public void resetActivity(Player p) { + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + } + + public boolean canAutoLogin(Player p) { + String ip = getPlayerIP(p); + if (ip == null) return false; + Long last = ipAutoLogin.get(ip); + return last != null + && System.currentTimeMillis() - last + < IP_SKIP_MS; + } + + public void recordIPLogin(Player p) { + String ip = getPlayerIP(p); + if (ip != null) + ipAutoLogin.put(ip, + System.currentTimeMillis()); + } + + public boolean isFrozen(Player p) { + return !loggedIn.contains(p.getName()); + } + + private boolean isAdmin(CommandSender sender) { + if (sender instanceof Player) + return ((Player) sender) + .getScoreboardTags() + .contains(config.adminTag); + return true; + } + + private void clearPlayer(Player p) { + p.getInventory().clear(); + p.getInventory().setArmorContents( + new ItemStack[4]); + p.getInventory().setExtraContents( + new ItemStack[1]); + p.setLevel(0); + p.setExp(0f); + } + + public void restoreInventory(Player p) { + String name = p.getName(); + try { + String[] backup = + db.loadInventoryBackup(name); + if (backup == null + || !isValidBackup(backup)) { + return; + } + + debugPrintBase64("恢复前", name, + backup[0]); + + ItemStack[] contents = + InventorySerializer + .fromBase64(backup[0]); + if (contents != null) + p.getInventory() + .setContents(contents); + + if (backup[1] != null + && !backup[1].isEmpty()) { + ItemStack[] armor = + InventorySerializer + .fromBase64( + backup[1]); + if (armor != null) + p.getInventory() + .setArmorContents( + armor); + } + + if (backup[2] != null + && !backup[2].isEmpty()) { + ItemStack[] extra = + InventorySerializer + .fromBase64( + backup[2]); + if (extra != null) + p.getInventory() + .setExtraContents( + extra); + } + + int lv = Integer.parseInt( + backup[3]); + if (lv > 0) p.setLevel(lv); + double xp = Double.parseDouble( + backup[4]); + if (xp > 0) + p.setExp((float) xp); + + debugPrintBase64("恢复后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + getLogger().info( + "[Sdf1_login] " + name + + " 背包已恢复"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 还原背包失败: " + + name); + e.printStackTrace(); + } + } + + + + + public void backupInventory(Player p) { + String name = p.getName(); + int cnt = countItems(p) + + countArmor(p) + + countExtra(p); + if (cnt == 0) { + getLogger().warning( + "[Sdf1_login] " + name + + " 背包为空,跳过备份"); + return; + } + try { + String invB64 = + InventorySerializer.toBase64( + p.getInventory() + .getContents()); + String armorB64 = + InventorySerializer.toBase64( + p.getInventory() + .getArmorContents()); + String extraB64 = + InventorySerializer.toBase64( + p.getInventory() + .getExtraContents()); + db.saveInventoryBackup(name, invB64, + armorB64, extraB64, + p.getLevel(), p.getExp()); + getLogger().info( + "[Sdf1_login] " + name + + " 背包已备份 (物品:" + + cnt + ")"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 背包备份失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventory(Player p) { + String name = p.getName(); + try { + debugPrintBase64("清空前", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + // 清空背包(备份已在上次退出时保存) + p.getInventory().clear(); + p.getInventory() + .setArmorContents(null); + p.getInventory() + .setExtraContents(null); + p.setLevel(0); + p.setExp(0); + + debugPrintBase64("清空后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] hideInventory失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventoryDelayed(Player p, + int attempt) { + String name = p.getName(); + if (!p.isOnline()) return; + if (!isFrozen(p)) return; + + // 从数据库加载已有备份 + String[] existing = + db.loadInventoryBackup(name); + + // 调试打印 + debugPrintBase64("加入-加载DB", name, + existing != null + ? existing[0] : null); + + // 无有效备份 → 跳过清空背包 + if (existing == null + || !isValidBackup(existing)) { + getLogger().info( + "[Sdf1_login] " + name + + " 无有效备份," + + "跳过清空背包"); + return; + } + + // 有有效备份 → 清空当前背包 + hideInventory(p); + } + + + public void autoLogin(Player p, + String registerType) { + String name = p.getName(); + String ip = getPlayerIP(p); + String uuid = + p.getUniqueId().toString(); + String defaultPwd = "123456aA"; + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + if (!"manual".equals( + config.approvalMode)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + } + String salt = + PasswordUtils.generateSalt(); + String hash = PasswordUtils.hash( + defaultPwd, salt); + db.createUser(name, hash, salt); + db.setField(name, + "premium_uuid", uuid); + db.setField(name, + "register_ip", ip); + db.setField(name, + "register_type", registerType); + db.recordIP(name, ip); + } + + loggedIn.add(name); + db.setLoggedIn(name, true); + db.setField(name, + "last_login_time", + System.currentTimeMillis()); + db.setField(name, + "last_online_check", + System.currentTimeMillis()); + db.recordIP(name, ip); + joinTime.remove(p.getUniqueId()); + recordIPLogin(p); + + // 只在有有效备份时恢复 + restoreInventory(p); + + giveMenuSnowball(p); + p.sendMessage("§a[Sdf1_login] §f您已自动登录!"); + activateBeibao(p); + pushPendingAlerts(p); + + + } + + private void activateBeibao(Player p) { + try { + org.bukkit.plugin.Plugin cy = + Bukkit.getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class) + .invoke(cy, p.getName(), + 0, 0); + } catch (Exception ignored) { + } + } + + public void triggerSecurityAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + db.recordSecurityAlert(name, suspiciousIP); + Bukkit.broadcastMessage( + "§c§l[安全警告] §e" + name + + " §f账号疑似遭到 §c" + + suspiciousIP + + " §f的盗号尝试"); + String emailAddr = + (String) db.getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String body = "玩家 " + name + + ":\n您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + + suspiciousIP + + " 的盗号尝试。\n" + + "建议立即修改密码。"; + final String to = emailAddr; + final String finalBody = body; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> email + .sendTempPassword( + to, name, + finalBody)); + } + } + + public void pushPendingAlerts(Player p) { + String name = p.getName(); + List> alerts = + db.getUnnotifiedAlerts(name); + if (alerts.isEmpty()) return; + p.sendMessage("§c§l========== 安全报警 =========="); + for (Map alert : alerts) { + p.sendMessage("§e时间: §f" + + alert.get("alert_date")); + p.sendMessage("§e可疑IP: §c" + + alert.get("alert_ip")); + db.markAlertNotified( + ((Number) alert.get("id")) + .intValue()); + } + p.sendMessage( + "§c建议使用 §e/sdf1_login pw §c修改密码"); + p.sendMessage( + "§c§l=================================="); + } + + public void handleReset(Player p) { + loginMgr.handleReset(p); + } + + public boolean dailySign(Player p) { + if (checkIn.isCheckedInToday(p.getName())) + return false; + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn(p.getName()); + return true; + } + + public void openTaskPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l任务面板"); + ItemStack gl = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = gl.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + gl.setItemMeta(glm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, gl); + Map user = + db.getUser(p.getName()); + int stage = ((Number) user.getOrDefault( + "gift_stage", 0)).intValue(); + String claimed = (String) user.getOrDefault( + "gift_claimed", ""); + g.setItem(11, mkItem(Material.BOOK, + "§e新人礼包进度", + "§7当前阶段: §e" + stage, + "§7已领取: §a" + (claimed.isEmpty() + ? "无" : claimed))); + g.setItem(15, mkItem(Material.CLOCK, + "§b签到", "§7今日签到获取积分")); + g.setItem(22, mkItem(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openAccountRequestPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§e§l账号请求"); + ItemStack gl = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = gl.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + gl.setItemMeta(glm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, gl); + g.setItem(10, mkItem(Material.BARRIER, + "§c申请删除账号", + "§7申请删除自己名下的账号")); + g.setItem(13, mkItem(Material.PAPER, + "§6提交工单", + "§7提交bug反馈、求助、举报等")); + g.setItem(16, mkItem(Material.REDSTONE, + "§e找回密码", + "§7通过邮箱找回密码")); + g.setItem(22, mkItem(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private void playSuccessSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + private int countItems(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countArmor(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getArmorContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countExtra(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getExtraContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + /** + * 检查备份是否有效(包含非雪球的真实物品) + */ + public boolean isValidBackup(String[] backup) { + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) { + return false; + } + try { + ItemStack[] items = + InventorySerializer + .fromBase64(backup[0]); + if (items == null) return false; + for (ItemStack item : items) { + if (item == null + || item.getType() + == Material.AIR) + continue; + if (isMenuSnowball(item)) + continue; + return true; + } + } catch (Exception e) { + return false; + } + return false; + } + + /** + * 控制台打印 base64 前10位 + */ + private void debugPrintBase64(String label, + String name, String base64) { + if (base64 == null || base64.isEmpty()) { + getLogger().info( + "[BACKUP_DEBUG] " + label + + " " + name + + ": "); + return; + } + String preview = + base64.length() > 10 + ? base64.substring(0, 10) + : base64; + getLogger().info( + "[BACKUP_DEBUG] " + label + " " + + name + ": " + preview); + } + + private ItemStack mkItem(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) + if (!l.isEmpty()) list.add(l); + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } + + private String buildIPKickMessage(Player p, + String ip) { + List> accounts = + db.getAccountDetailsByIP(ip); + StringBuilder sb = new StringBuilder(); + sb.append("§c§l======== IP注册已满 ========\n"); + sb.append("§e您的IP §f").append(ip) + .append(" §e下已注册 §f") + .append(accounts.size()) + .append(" §e个账号:\n"); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm"); + int idx = 1; + for (Map acc : accounts) { + long regTime = ((Number) acc.get( + "register_time")).longValue(); + String dateStr = regTime > 0 + ? sdf.format(new java.util.Date( + regTime)) + : "未知"; + sb.append("§7").append(idx) + .append(". §f") + .append(acc.get("player_name")) + .append(" §7注册于: §e") + .append(dateStr).append("\n"); + idx++; + } + sb.append("§7-----------------------------\n"); + sb.append("§e请使用已有账号登录\n"); + sb.append("§e登录后输入 §f/oa §e提交工单\n"); + sb.append("§c§l============================"); + return sb.toString(); + } + /** + * 读取备份,返回物品名预览 + */ + private String getBackupItemPreview( + int backupId) { + try { + String[] backup = + db.getInventoryBackupById( + backupId); + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) + return "§7空"; + ItemStack[] items = + InventorySerializer + .fromBase64( + backup[0]); + if (items == null) + return "§7空"; + Map counts = + new LinkedHashMap<>(); + for (ItemStack it : items) { + if (it == null + || it.getType() + == Material.AIR) + continue; + String name = translateMaterial( + it.getType().name()); + counts.merge(name, + it.getAmount(), + Integer::sum); + } + if (counts.isEmpty()) + return "§7空"; + StringBuilder sb = + new StringBuilder(); + int shown = 0; + for (Map.Entry en + : counts.entrySet()) { + if (shown >= 5) { + sb.append(" §7..."); + break; + } + if (shown > 0) + sb.append("§7, "); + sb.append("§f").append(en.getKey()) + .append("§7x") + .append(en.getValue()); + shown++; + } + return sb.toString(); + } catch (Exception e) { + return "§7解析失败"; + } + } + + private String translateMaterial( + String name) { + switch (name) { + case "GRASS_BLOCK": + return "草方块"; + case "OAK_LOG": + return "橡木原木"; + case "OAK_PLANKS": + return "橡木木板"; + case "STONE": + return "石头"; + case "COBBLESTONE": + return "圆石"; + case "DIRT": + return "泥土"; + case "SAND": + return "沙子"; + case "GRAVEL": + return "砂砾"; + case "DIAMOND": + return "钻石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "IRON_INGOT": + return "铁锭"; + case "GOLD_INGOT": + return "金锭"; + case "COAL": + return "煤炭"; + case "COAL_ORE": + return "煤矿石"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "OAK_LEAVES": + return "橡树树叶"; + case "OAK_SAPLING": + return "橡树树苗"; + case "STICK": + return "木棍"; + case "CRAFTING_TABLE": + return "工作台"; + case "FURNACE": + return "熔炉"; + case "CHEST": + return "箱子"; + case "TORCH": + return "火把"; + case "BEDROCK": + return "基岩"; + case "TNT": + return "TNT"; + case "REDSTONE": + return "红石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "LAPIS_LAZULI": + return "青金石"; + case "EMERALD": + return "绿宝石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "OBSIDIAN": + return "黑曜石"; + case "GLOWSTONE": + return "萤石"; + case "ARROW": + return "箭"; + case "BOW": + return "弓"; + case "IRON_SWORD": + return "铁剑"; + case "DIAMOND_SWORD": + return "钻石剑"; + case "IRON_PICKAXE": + return "铁镐"; + case "DIAMOND_PICKAXE": + return "钻石镐"; + case "IRON_AXE": + return "铁斧"; + case "DIAMOND_AXE": + return "钻石斧"; + case "IRON_HELMET": + return "铁头盔"; + case "IRON_CHESTPLATE": + return "铁胸甲"; + case "IRON_LEGGINGS": + return "铁护腿"; + case "IRON_BOOTS": + return "铁靴子"; + case "DIAMOND_HELMET": + return "钻石头盔"; + case "DIAMOND_CHESTPLATE": + return "钻石胸甲"; + case "DIAMOND_LEGGINGS": + return "钻石护腿"; + case "DIAMOND_BOOTS": + return "钻石靴子"; + case "BREAD": + return "面包"; + case "COOKED_BEEF": + return "牛排"; + case "COOKED_PORKCHOP": + return "熟猪排"; + case "COOKED_CHICKEN": + return "熟鸡肉"; + case "GOLDEN_APPLE": + return "金苹果"; + case "ENDER_PEARL": + return "末影珍珠"; + case "BLAZE_ROD": + return "烈焰棒"; + case "GHAST_TEAR": + return "恶魂之泪"; + case "NETHER_STAR": + return "下界之星"; + case "SHULKER_SHELL": + return "潜影壳"; + case "ELYTRA": + return "鞘翅"; + case "TOTEM_OF_UNDYING": + return "不死图腾"; + case "TRIDENT": + return "三叉戟"; + case "ENCHANTED_GOLDEN_APPLE": + return "附魔金苹果"; + default: + // 转中文名格式:SANDSTONE -> 沙石 + return name.toLowerCase() + .replace("_", " "); + } + } + + // ===== Events ===== + @EventHandler + public void onJoin(PlayerJoinEvent e) { + Player p = e.getPlayer(); + String name = p.getName(); + String ip = getPlayerIP(p); + // 重置聊天输入状态(防止上次退出时残留) + chatInput.reset(p); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); + + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); + + boolean isBedrock = + verification.isBedrockPlayer(p); + boolean isOnlineMode = + verification.isOnlineMode(); + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + } + if (isFrozen(p)) { + p.setAllowFlight(true); + } + + if (!"manual".equals(config.approvalMode) + && !db.userExists(name)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + } + + if (isOnlineMode) { + autoLogin(p, "premium"); + return; + } + if (isBedrock) { + autoLogin(p, "bedrock"); + return; + } + if (canAutoLogin(p)) { + autoLogin(p, "ip_reconnect"); + return; + } + afk.remove(p.getUniqueId()); + afk.recordAction(p.getUniqueId()); + hideInventoryDelayed(p, 0); + + + verification.verifyPremiumAsync(p, + isPremium -> { + if (isPremium) { + Bukkit.getScheduler() + .runTask(this, + () -> autoLogin(p, + "premium")); + } else { + Bukkit.getScheduler() + .runTask(this, () -> { + joinTime.put( + p.getUniqueId(), + System.currentTimeMillis()); + if (!db.userExists(name)) + p.sendMessage(config.msg( + "not_registered")); + else + p.sendMessage(config.msg( + "not_logged_in")); + }); + } + }); + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + Player p = e.getPlayer(); + String name = p.getName(); + chatInput.reset(p); + + boolean wasLoggedIn = + loggedIn.contains(name); + + // 判断是否需要保存: + // 1. 已登录 → 保存当前背包 + // 2. 未登录但有真实物品 → 保存 + // (插件安装前的老玩家) + // 3. 未登录且背包被插件清空 → 不保存 + // (避免覆盖有效备份) + boolean hasRealItems = false; + for (ItemStack it : p.getInventory() + .getContents()) { + if (it != null + && it.getType() + != Material.AIR + && !isMenuSnowball(it)) { + hasRealItems = true; + break; + } + } + + if (wasLoggedIn || hasRealItems) { + try { + String contents = + InventorySerializer + .toBase64( + p.getInventory() + .getContents()); + String armor = + InventorySerializer + .toBase64( + p.getInventory() + .getArmorContents()); + String extra = + InventorySerializer + .toBase64( + p.getInventory() + .getExtraContents()); + int level = p.getLevel(); + double exp = p.getExp(); + + debugPrintBase64( + "退出保存", name, + contents); + db.saveInventoryBackup(name, + contents, armor, extra, + level, exp); + } catch (Exception ex) { + getLogger().severe( + "[Sdf1_login] " + + "退出保存背包失败: " + + name); + } + } else { + getLogger().info( + "[BACKUP_DEBUG] 退出跳过 " + + name + + ": 背包为空" + + "(插件已清空)"); + } + + if (wasLoggedIn) { + loggedIn.remove(name); + db.setLoggedIn(name, false); + recordIPLogin(p); + } + + lastActivity.remove(p.getUniqueId()); + joinTime.remove(p.getUniqueId()); + joinLoc.remove(p.getUniqueId()); + needsPasswordChange.remove(name); + } + + private static final String MENU_SNOWBALL_TAG = + "sdf1_menu"; + + // 雪球菜单 + public void giveMenuSnowball(Player p) { + // 检查开关 + Object val = db.getField( + p.getName(), "menu_snowball"); + if (val != null) { + int on = val instanceof Number + ? ((Number) val).intValue() + : 1; + if (on == 0) { + return; // 开关关了,不发放 + } + } + // 检查是否已有 + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) { + return; + } + } + // 发放 + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l[菜单] §f右键打开主菜单"); + List lore = + new ArrayList<>(); + lore.add("§7右键点击打开功能菜单"); + lore.add("§8" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + /** + * 手动发放雪球菜单(绕过开关检查) + */ + public void forceGiveMenuSnowball( + Player p) { + // 只检查是否已有 + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) { + return; + } + } + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l[菜单] §f右键打开主菜单"); + List lore = + new ArrayList<>(); + lore.add("§7右键点击打开功能菜单"); + lore.add("§8" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + private boolean isMenuSnowball(ItemStack item) { + if (item == null + || item.getType() + != Material.SNOWBALL) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null) return false; + if (!im.hasLore()) return false; + List lore = im.getLore(); + if (lore == null) return false; + for (String line : lore) { + if (line.contains(MENU_SNOWBALL_TAG)) + return true; + } + return false; + } + + + @EventHandler + public void onMove(PlayerMoveEvent e) { + Player p = e.getPlayer(); + if (!isFrozen(p)) return; + + Location from = e.getFrom(); + Location to = e.getTo(); + if (to == null) return; + + // 允许转头(无位移) + if (from.getBlockX() == to.getBlockX() + && from.getBlockY() == to.getBlockY() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + + // 有位移 → 取消 + e.setCancelled(true); + + // 防止"服务器未启用飞行"踢出 + p.setAllowFlight(true); + } + + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + // 右键菜单雪球 + if (e.getAction().name() + .contains("RIGHT") + || e.getAction().name() + .contains("LEFT")) { + ItemStack hand = + p.getInventory() + .getItemInMainHand(); + if (isMenuSnowball(hand)) { + e.setCancelled(true); + gui.openMain(p); + return; + } + } + } + + + @EventHandler + public void onBlockBreak(BlockBreakEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + afk.recordAction(p.getUniqueId()); + } + + @EventHandler + public void onBlockPlace(BlockPlaceEvent e) { + if (isFrozen(e.getPlayer())) + e.setCancelled(true); + } + + @EventHandler + public void onDrop(PlayerDropItemEvent e) { + if (isFrozen(e.getPlayer())) { + e.setCancelled(true); + return; + } + // 禁止丢弃菜单雪球 + ItemStack drop = + e.getItemDrop().getItemStack(); + if (isMenuSnowball(drop)) { + e.setCancelled(true); + } + } + + @EventHandler + public void onPickup(PlayerPickupItemEvent e) { + if (isFrozen(e.getPlayer())) + e.setCancelled(true); + } + + @EventHandler + public void onDamage(EntityDamageEvent e) { + if (e.getEntity() instanceof Player) { + if (isFrozen((Player) e.getEntity())) + e.setCancelled(true); + } + } + + @EventHandler + public void onEntityAttack( + EntityDamageByEntityEvent e) { + if (e.getDamager() instanceof Player + && isFrozen( + (Player) e.getDamager())) + e.setCancelled(true); + if (e.getEntity() instanceof Player + && isFrozen( + (Player) e.getEntity())) + e.setCancelled(true); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + String msg = e.getMessage(); + + if (chatInput.getState(p).type + != ChatInputManager.InputType.NONE) { + e.setCancelled(true); + ChatInputManager.InputType inputType = + chatInput.getState(p).type; + if (ticket != null + && ticket.handleChatInput( + p, msg, inputType)) { + return; + } + chatInput.handleInput(p, msg, this); + return; + } + + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + if (chatFilter == null) return; + if (chatFilter.isPlayerWhitelisted( + p.getName())) return; + if (chatFilter.isMuted(p.getName())) { + e.setCancelled(true); + p.sendMessage( + chatFilter.msg("chat_muted")); + return; + } + + List urls = + chatFilter.extractUrls(msg); + if (urls.isEmpty()) return; + + for (String u : urls) { + if (chatFilter.isWhitelisted(u)) + continue; + if (!chatFilter.isLikelyDomain(u)) + continue; + + e.setCancelled(true); + chatFilter.incrementViolation( + p.getName()); + int count = chatFilter + .getViolationCount(p.getName()); + + p.sendMessage(chatFilter + .msg("chat_url_blocked") + .replace("{url}", u)); + p.sendMessage(chatFilter + .msg("chat_url_violation") + .replace("{count}", + String.valueOf(count))); + + chatFilter.applyPunishment(p, count); + + if (chatFilter.isNotifyAdmin()) { + String tag = config.adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag) + && !op.equals(p)) { + op.sendMessage(chatFilter + .msg("chat_url_admin_notify") + .replace("{player}", + p.getName()) + .replace("{url}", u) + .replace("{count}", + String.valueOf( + count))); + } + } + } + + if (chatFilter.isNotifyAll()) { + Bukkit.broadcastMessage(chatFilter + .msg("chat_url_broadcast") + .replace("{player}", + p.getName())); + } + } + } + + + + // ===== Inventory Click ===== + // ===== Inventory Click ===== + @EventHandler + public void onInvClick(InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + String title = e.getView().getTitle(); + int slot = e.getRawSlot(); + + // Shift+点击背包物品 → 存入垃圾箱 + if (e.isShiftClick() + && e.getClickedInventory() != null + && e.getClickedInventory() + == p.getInventory() + && e.getCurrentItem() != null + && e.getCurrentItem().getType() + != Material.AIR + && !isMenuSnowball( + e.getCurrentItem()) + && title.equals( + "§6§l垃圾回收站")) { + e.setCancelled(true); + ItemStack clicked = + e.getCurrentItem().clone(); + garbage.saveItem(clicked); + e.getInventory().setItem( + e.getSlot(), null); + Bukkit.getScheduler() + .runTaskLater(this, + () -> { + garbage.openRecyclePage(p, + garbage.getRecyclePage( + p.getName())); + }, 1L); + return; + } + + if (needsPasswordChange + .contains(p.getName()) + && !title.equals( + "§6§l任务面板")) { + e.setCancelled(true); + p.sendMessage("§c请先修改密码!"); + return; + } + + // 工单GUI + if (title.equals( + TicketManager.T_TICKET_MAIN) + || title.equals( + TicketManager.T_TICKET_CREATE) + || title.equals( + TicketManager.T_TICKET_MY) + || title.equals( + TicketManager.T_TICKET_ADMIN) + || title.equals( + TicketManager.T_TICKET_DETAIL) + || title.equals( + "§d§l服务商面板") + || title.equals( + "§6§l抢单大厅") + || title.equals( + "§b§l我处理中的工单")) { + e.setCancelled(true); + ticket.handleClick( + p, title, slot); + return; + } + + // 垃圾回收站 + if (title.equals("§6§l垃圾回收站")) { + if (slot < 0 || slot >= 54) return; + + // 功能格 49-53 + if (slot >= 49 && slot <= 53) { + e.setCancelled(true); + garbage.handleClick( + p, title, slot); + return; + } + + ItemStack inSlot = + e.getInventory().getItem(slot); + ItemStack cursor = e.getCursor(); + + // ===== 取出 ===== + if (inSlot != null + && inSlot.hasItemMeta() + && inSlot.getItemMeta().hasLore()) { + List lore = + inSlot.getItemMeta().getLore(); + if (lore != null && !lore.isEmpty() + && lore.get(0) + .startsWith("§7ID: #")) { + e.setCancelled(true); + try { + String numStr = lore.get(0) + .replace("§7ID: #", "") + .trim(); + int dbId = + Integer.parseInt(numStr); + if (garbage.removeItem(dbId)) { + ItemStack give = + inSlot.clone(); + ItemMeta gm = + give.getItemMeta(); + gm.setLore(null); + give.setItemMeta(gm); + p.getInventory() + .addItem(give); + e.getInventory() + .setItem(slot, null); + } + } catch (Exception ex) { + p.sendMessage("§c取出失败"); + } + return; + } + } + + // ===== 光标存入 ===== + if (cursor != null + && cursor.getType() + != Material.AIR + && !isMenuSnowball(cursor)) { + ItemStack toSave = cursor.clone(); + getLogger().info( + "[垃圾站] 存入: " + + toSave.getType() + + " x" + toSave.getAmount()); + garbage.saveItem(toSave); + e.setCancelled(true); + Bukkit.getScheduler().runTaskLater( + this, () -> { + p.getOpenInventory() + .setCursor(null); + garbage.openRecyclePage(p, + garbage.getRecyclePage( + p.getName())); + }, 1L); + return; + } + + // 默认取消 + e.setCancelled(true); + return; + } + + // 主菜单 + if (title.equals(GUIManager.T_MAIN)) { + e.setCancelled(true); + if (slot == 10) gui.openMyInfo(p); + else if (slot == 12) gui.openInvite(p); + else if (slot == 14) + p.openInventory( + points.createShopGUI(p)); + else if (slot == 16) + gui.openTaskCenter(p); + else if (slot == 18) ticket.openMain(p); + else if (slot == 20) { + if (p.isOp() || p.getScoreboardTags() + .contains(config.adminTag)) { + garbage.openRecycle(p); + } + } + + else if (slot == 22 && isAdmin(p)) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_AUTH; + p.sendMessage("请输入管理密码:"); + } + return; + } + + + // 我的信息 + if (title.equals(GUIManager.T_MY_INFO)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) { + if (checkIn.isCheckedInToday( + p.getName())) + p.sendMessage(config.msg( + "checkin_already")); + else { + p.sendMessage( + checkIn.checkIn(p)); + invite.onInviteeCheckIn( + p.getName()); + playSuccessSound(p); + gui.openMyInfo(p); + } + } else if (slot == 11) { + chatInput.getState(p).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p.closeInventory(); + p.sendMessage("§e请输入当前密码:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .BACK_CHECKIN; + p.closeInventory(); + p.sendMessage("§e请输入补签日期:"); + } else if (slot == 13) { + String code = (String) db.getField( + p.getName(), "invite_code"); + if (code == null || code.isEmpty()) + code = invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + } else if (slot == 14) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SET_EMAIL; + p.closeInventory(); + p.sendMessage("§e请输入邮箱:"); + } else if (slot == 15) + p.openInventory( + points.createShopGUI(p)); + return; + } + + // 管理面板 + if (title.equals(GUIManager.T_ADMIN)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SMTP_CONFIG; + p.closeInventory(); + p.sendMessage("§e当前SMTP: " + + config.getSmtp("smtp地址")); + p.sendMessage("§e输入新地址(0跳过):"); + } else if (slot == 11) + gui.openUserManagement(p); + else if (slot == 12) gui.openMyInfo(p); + else if (slot == 14) + p.openInventory( + points.createShopGUI(p)); + else if (slot == 16) { + if (e.getClick().isLeftClick()) { + config.afkEnabled = + !config.afkEnabled; + p.sendMessage(config.msg( + config.afkEnabled + ? "afk_set_enabled" + : "afk_set_disabled")); + config.saveSettings(); + gui.openAdmin(p); + } else if (e.getClick() + .isRightClick()) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_AFK_TIME; + p.sendMessage( + "§e输入挂机时长(分钟):"); + } + } + return; + } + + // 任务中心 + if (title.equals( + GUIManager.T_TASK_CENTER)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) + gui.openGiftStages(p); + else if (slot == 12) + gui.openTaskList(p, "主线任务"); + else if (slot == 14) + gui.openTaskList(p, "支线任务"); + else if (slot == 16) { + if (dailySign(p)) { + gui.openTaskCenter(p); + playSuccessSound(p); + } else + p.sendMessage(config.msg( + "checkin_already")); + } + return; + } + + // 新人礼包 + if (title.equals( + GUIManager.T_GIFT_STAGES)) { + e.setCancelled(true); + if (slot == 26) + gui.openTaskCenter(p); + else if (slot >= 10 && slot <= 18) { + int stage = slot - 9; + if (gift.canClaim(p, stage)) { + gift.claimReward(p, stage); + gui.openGiftStages(p); + } else + p.sendMessage(config.msg( + "gift_not_ready")); + } + return; + } + + // 邀请 + if (title.equals(GUIManager.T_INVITE)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) { + String code = + invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + gui.openInvite(p); + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .INVITE_INPUT_CODE; + p.closeInventory(); + p.sendMessage("§e请输入邀请码:"); + } + + return; + } + + // 用户管理 + if (title.equals( + GUIManager.T_USER_MGMT)) { + e.setCancelled(true); + if (slot == 53) + gui.openAdmin(p); + else if (slot >= 0 && slot < 45) { + ItemStack item = + e.getInventory().getItem(slot); + if (item != null + && item.hasItemMeta() + && item.getItemMeta() + .hasDisplayName()) + gui.openUserDetail(p, + item.getItemMeta() + .getDisplayName() + .replace("§e", "") + .replace("§a", "")); + } + + return; + } + + // 用户详情 + if (title.startsWith("§e§l管理: ")) { + e.setCancelled(true); + String tgt = title.substring( + "§e§l管理: ".length()); + if (slot == 22) + gui.openUserManagement(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_POINTS; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage("§e输入" + tgt + + "的新积分:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_PWD; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage("§e输入" + tgt + + "的临时密码:"); + } else if (slot == 14) { + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) { + forceGiveMenuSnowball(tp); + p.sendMessage("§a已发放雪球菜单给 " + + tgt); + } else { + p.sendMessage("§c玩家不在线"); + } + + + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_DELETE_CONFIRM; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage(config.msg( + "admin_delete_confirm")); + } + + return; + + } + + + // 积分商城 + if (title.startsWith("§d§l积分商城")) { + e.setCancelled(true); + if (slot == 49) gui.openMain(p); + else if (slot == 50 && isAdmin(p)) { + gui.openShopAdmin(p); + } else + points.handleClick(p, slot); + return; + } + + // 商城管理 + if (title.equals("§c§l商城管理")) { + e.setCancelled(true); + if (slot == 49) { + p.openInventory( + points.createShopGUI(p)); + return; + } + return; + } + + // CY背包商城 + if (title.startsWith("§d§lCY背包商城")) { + e.setCancelled(true); + int size = e.getInventory().getSize(); + if (slot == size - 1) + p.openInventory( + points.createShopGUI(p)); + else points.handleCYClick(p, slot); + return; + } + + // 任务面板 + if (title.equals("§6§l任务面板")) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 15) { + if (dailySign(p)) { + openTaskPanel(p); + playSuccessSound(p); + } else + p.sendMessage(config.msg( + "checkin_already")); + } + return; + } + + // 账号请求面板 + if (title.equals("§e§l账号请求")) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + p.closeInventory(); + p.sendMessage("§c§l[确认] §f请输入:"); + p.sendMessage("§e/sdf1_login del " + + p.getName()); + } else if (slot == 13) { + ticket.openCreate(p); + } else if (slot == 16) { + p.closeInventory(); + loginMgr.handleReset(p); + } + return; + } + + // 主线/支线任务 + if (title.equals("§d§l主线任务") + || title.equals("§d§l支线任务")) { + e.setCancelled(true); + if (slot == e.getInventory().getSize() - 1) + gui.openTaskCenter(p); + return; + } + } + + @EventHandler + public void onInvDrag(InventoryDragEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + String t = e.getView().getTitle(); + + // 回收站拖拽 + if (t.equals("§6§l垃圾回收站")) { + for (int rawSlot : + e.getRawSlots()) { + if (rawSlot >= 49) { + e.setCancelled(true); + return; + } + } + // 0-48允许拖入,不取消 + // 延迟2tick后扫描格子 + final Player fp = p; + Bukkit.getScheduler() + .runTaskLater(this, + () -> { + Inventory top = fp + .getOpenInventory() + .getTopInventory(); + for (int rawSlot : + e.getRawSlots()) { + if (rawSlot < 0 + || rawSlot >= 49) + continue; + ItemStack item = + top.getItem(rawSlot); + if (item != null + && item.getType() + != Material.AIR + && !isMenuSnowball( + item)) { + garbage.saveItem( + item.clone()); + top.setItem( + rawSlot, null); + } + } + }, 2L); + return; + } + + //服务商工单系统 + if (t.equals(TicketManager.T_TICKET_MAIN) + || t.equals( + TicketManager.T_TICKET_CREATE) + || t.equals( + TicketManager.T_TICKET_MY) + || t.equals( + TicketManager.T_TICKET_ADMIN) + || t.equals( + TicketManager.T_TICKET_DETAIL) + || t.equals("§d§l服务商面板") + || t.equals("§6§l抢单大厅") + || t.equals( + "§b§l我处理中的工单") + || t.equals("§6§l我的历史工单") + || t.equals("§c§l商城管理")) { + e.setCancelled(true); + } + } + + // ===== Commands ===== + @Override + public boolean onCommand(CommandSender sender, + Command cmd, String label, + String[] args) { + + String cmdName = cmd.getName().toLowerCase(); + + if (cmdName.equals("recycle")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player pr = (Player) sender; + if (!pr.isOp() + && !pr.getScoreboardTags() + .contains(config.adminTag)) { + pr.sendMessage("§c权限不足"); + return true; + } + garbage.openRecycle(pr); + return true; + } + + + // /垃圾清理 + if (cmdName.equals("垃圾清理")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + int count = garbage.collectItems(); + sender.sendMessage("§a已清理 " + + count + " 个掉落物"); + return true; + } + // /menu 开关雪球菜单 + if (cmdName.equals("menu")) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length == 0) { + p2.sendMessage( + "§e用法: /menu <开/关>"); + return true; + } + String arg = args[0]; + if (arg.equals("开")) { + db.setField(p2.getName(), + "menu_snowball", 1); + p2.sendMessage( + "§a雪球菜单已开启," + + "下次进入将发放"); + } else if (arg.equals("关")) { + db.setField(p2.getName(), + "menu_snowball", 0); + p2.sendMessage( + "§c雪球菜单已关闭," + + "下次进入不再发放"); + } else { + p2.sendMessage( + "§e用法: /menu <开/关>"); + } + return true; + } + + if (cmdName.equals("reg") + || cmdName.equals("注册")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (db.userExists(p.getName())) { + p.sendMessage( + "§c您已注册过,请使用 /login"); + return true; + } + try { + loginMgr.handleRegister(p, args); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] reg命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c注册出错,请联系管理员"); + } + return true; + } + + + + if (cmdName.equals("login") + || cmdName.equals("l") + || cmdName.equals("登录")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (loggedIn.contains(p.getName())) { + p.sendMessage("§c您已登录"); + return true; + } + try { + loginMgr.handleLogin(p, args); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] login命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c登录出错,请联系管理员"); + } + return true; + } + + // /找回密码 + if (cmdName.equals("找回密码")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + handleReset((Player) sender); + return true; + } + + // /签到 + if (cmdName.equals("签到")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // 绑定邮箱 + if (cmdName.equals("绑定邮箱")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 1) { + String emailAddr = args[0]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // /sdf1debug + if (cmdName.equals("sdf1debug")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (!isAdmin(sender)) { + p2.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + p2.sendMessage("§e用法: /sdf1debug <玩家> <阶段>"); + return true; + } + String tgt = args[0]; + try { + int stage = Integer.parseInt(args[1]); + db.setField(tgt, "gift_stage", stage); + p2.sendMessage("§a已将 " + tgt + + " 的任务阶段设为 " + stage); + } catch (NumberFormatException ex) { + p2.sendMessage("§c请输入有效数字"); + } + return true; + } + + // /oa + if (cmdName.equals("oa")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + openAccountRequestPanel( + (Player) sender); + return true; + } + + // /玩家信息 + if (cmdName.equals("玩家信息")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + gui.openMain((Player) sender); + return true; + } + + // ===== /sdf1_login ===== + if (!cmdName.equals("sdf1_login")) + return false; + + if (args.length == 0) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§e用法: /sdf1_login <子命令>"); + return true; + } + gui.openMain((Player) sender); + return true; + } + + String sub = args[0].toLowerCase(); + + // stop + if (sub.equals("stop")) { + if (!(sender instanceof Player)) { + // 控制台 + if (args.length >= 2) { + db.clearTempPassword(args[1]); + sender.sendMessage("§a已报废 " + + args[1] + " 的临时密码"); + } else { + db.clearAllTempPasswords(); + sender.sendMessage( + "§a已报废所有临时密码"); + } + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String tgt = args[1]; + String myIP = + getPlayerIP(p2); + String targetIP = + db.getPlayerIPByName(tgt); + if (targetIP == null) { + Player tp = + Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + targetIP = + getPlayerIP(tp); + } + if (targetIP == null + || !targetIP.equals(myIP)) { + p2.sendMessage( + "§c只能操作同IP下的玩家"); + return true; + } + db.clearTempPassword(tgt); + p2.sendMessage("§a已报废 " + tgt + + " 的临时密码"); + } else { + db.clearTempPassword(p2.getName()); + p2.sendMessage( + "§a已报废您的临时密码"); + } + return true; + } + + // reload + if (sub.equals("reload")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + chatFilter.loadConfig(); + sender.sendMessage("§a配置已重载!"); + return true; + } + // back - 恢复背包备份 + if (sub.equals("back")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login back <玩家> [#编号]"); + return true; + } + String backTarget = args[1]; + if (args.length >= 3 + && args[2].startsWith("#")) { + // /sdf1_login back Aplayer #1 + int backupId; + try { + backupId = Integer.parseInt( + args[2].replace("#", "")); + } catch (NumberFormatException ex) { + sender.sendMessage( + "§c无效编号"); + return true; + } + String[] backup = + db.getInventoryBackupById( + backupId); + if (backup == null) { + sender.sendMessage( + "§c备份 #" + backupId + + " 不存在"); + return true; + } + // 如果玩家在线,直接恢复 + Player tp = Bukkit.getPlayer( + backTarget); + if (tp != null && tp.isOnline()) { + try { + tp.getInventory() + .setContents( + InventorySerializer + .fromBase64( + backup[0])); + if (backup[1] != null + && !backup[1] + .isEmpty()) + tp.getInventory() + .setArmorContents( + InventorySerializer + .fromBase64( + backup[1])); + if (backup[2] != null + && !backup[2] + .isEmpty()) + tp.getInventory() + .setExtraContents( + InventorySerializer + .fromBase64( + backup[2])); + int lv = Integer.parseInt( + backup[3]); + if (lv > 0) + tp.setLevel(lv); + tp.sendMessage( + "§a管理员已恢复您的背包"); + sender.sendMessage( + "§a已恢复 " + backTarget + + " 的背包 (备份#" + + backupId + ")"); + } catch (Exception e) { + sender.sendMessage( + "§c恢复失败: " + + e.getMessage()); + } + } else { + // 玩家离线:保存到数据库供下次登录恢复 + db.saveInventoryBackup( + backTarget, + backup[0], + backup[1], + backup[2], + Integer.parseInt( + backup[3]), + Double.parseDouble( + backup[4])); + sender.sendMessage( + "§a" + backTarget + + " 离线,备份已写入数据库," + + "下次登录时恢复"); + } + } else { + // /sdf1_login back Aplayer + // 罗列最近5条备份 + List> backups = + db.getInventoryBackups( + backTarget, 5); + if (backups.isEmpty()) { + sender.sendMessage( + "§c" + backTarget + + " 没有背包备份"); + return true; + } + sender.sendMessage( + "§e§l=== " + backTarget + + " 背包备份 ==="); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "MM-dd HH:mm:ss"); + for (Map b : + backups) { + int bid = ((Number) + b.get("id")) + .intValue(); + Object stObj = + b.get("save_time"); + long st = stObj != null + ? ((Number) stObj) + .longValue() : 0; + String time = st > 0 + ? sdf.format( + new java.util.Date(st)) + : "未知时间"; + String itemNames = + getBackupItemPreview( + bid); + sender.sendMessage( + "§7#" + bid + + " §f" + time + + " §7: " + + itemNames); + } + sender.sendMessage( + "§7使用 §e/sdf1_login back " + + backTarget + + " #编号 §7恢复"); + sender.sendMessage( + "§e§l================================"); + } + } + + // pw / password + if (sub.equals("pw") + || sub.equals("password")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + + if (args.length >= 3) { + // /sdf1_login pw 旧密码 新密码 + String oldPwd = args[1]; + String newPwd = args[2]; + if (!PasswordUtils.validate( + newPwd)) { + p2.sendMessage(config.msg( + "password_format_error")); + return true; + } + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + String oldHash = + (String) db.getField( + p2.getName(), + "password_hash"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + newPwd, newSalt); + if (newHash.equals(oldHash)) { + p2.sendMessage(config.msg( + "password_same")); + return true; + } + recordPasswordChange( + p2.getName(), + oldHash, salt); + db.setField(p2.getName(), + "password_hash", newHash); + db.setField(p2.getName(), + "password_salt", newSalt); + if (!mainOk && tempOk) { + db.clearTempPassword( + p2.getName()); + } + needsPasswordChange.remove( + p2.getName()); + p2.sendMessage(config.msg( + "password_changed")); + return true; + } + + if (args.length == 2) { + // /sdf1_login pw 旧密码 + // 验证旧密码,然后聊天输入新密码 + String oldPwd = args[1]; + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP2; + chatInput.getState(p2) + .ticketTitle = + mainOk ? "main" : "temp"; + p2.sendMessage( + "§e请输入新密码:"); + return true; + } + + // /sdf1_login pw (无参数) + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p2.sendMessage( + "§e请输入当前密码:"); + return true; + } + + // email + if (sub.equals("email")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String emailAddr = args[1]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + // 已绑定:先验证密码 + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + // 未绑定:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // sign + if (sub.equals("sign")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // reset + if (sub.equals("reset")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + loginMgr.handleReset( + (Player) sender); + return true; + } + + // undo + if (sub.equals("undo")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + PwdRollback rb = + pwdRollback.get(p2.getName()); + if (rb == null) { + p2.sendMessage("§c无可撤销记录"); + return true; + } + if (System.currentTimeMillis() - rb.time + > 600000L) { + pwdRollback.remove(p2.getName()); + p2.sendMessage("§c撤销已过期"); + return true; + } + db.setField(p2.getName(), + "password_hash", rb.hash); + db.setField(p2.getName(), + "password_salt", rb.salt); + pwdRollback.remove(p2.getName()); + p2.sendMessage("§a密码已撤销!"); + return true; + } + + // take + if (sub.equals("take")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login take <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + if (!db.deductPoints(args[1], amount)) { + sender.sendMessage("§c积分不足"); + return true; + } + sender.sendMessage("§a已扣除 " + args[1] + + " " + amount + " 积分"); + return true; + } + + // set + if (sub.equals("set")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login set <玩家> <临时密码>"); + return true; + } + String tgt = args[1]; + String tempPwd = args[2]; + if (!PasswordUtils.validate(tempPwd)) { + sender.sendMessage("§c密码格式不符合要求"); + return true; + } + if (!db.userExists(tgt)) { + sender.sendMessage("§c玩家不存在"); + return true; + } + String salt = (String) db.getField( + tgt, "password_salt"); + String hash = PasswordUtils.hash( + tempPwd, salt); + db.setField(tgt, "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + + db.setField(tgt, "temp_pw_used", 0); + sender.sendMessage("§a已为 " + tgt + + " 设置临时密码"); + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c管理员已设置临时密码"); + // 邮件通知 + String emailAddr = (String) db.getField( + tgt, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String time = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()); + String body = "玩家 " + tgt + + ":\n您的密码已于 " + time + + " 被管理员重置为临时密码。\n" + + "如果是本人操作请忽略。\n" + + "如非本人操作请立即联系管理员。"; + final String to = emailAddr; + final String fName = tgt; + final String bodyF = body; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> email + .sendTempPassword( + to, fName, + bodyF)); + } + return true; + } + // give - 手动发放雪球菜单 + if (sub.equals("give")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login give <玩家名>"); + return true; + } + Player tp = Bukkit.getPlayer( + args[1]); + if (tp == null || !tp.isOnline()) { + sender.sendMessage( + "§c玩家不在线: " + + args[1]); + return true; + } + forceGiveMenuSnowball(tp); + tp.sendMessage( + "§a管理员已为您发放菜单雪球"); + sender.sendMessage( + "§a已发放雪球菜单给 " + + args[1]); + return true; + } + + // shopadd + if (sub.equals("shopadd")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 4) { + sender.sendMessage( + "§e用法: /sdf1_login shopadd <名称> <价格> <命令>"); + return true; + } + String shopName = args[1]; + int price; + try { + price = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + String shopCmd = args[3]; + points.addItem(shopName, price, shopCmd); + sender.sendMessage("§a已添加商品: " + + shopName + " (" + price + + "积分)"); + return true; + } + + // shopdel + if (sub.equals("shopdel")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login shopdel <序号>"); + return true; + } + try { + int idx = Integer.parseInt(args[1]); + points.removeItem(idx); + sender.sendMessage("§a已删除商品 #" + idx); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + } + return true; + } + + // get + if (sub.equals("get")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login get <玩家>"); + return true; + } + Map user = + db.getUser(args[1]); + if (user.isEmpty()) { + sender.sendMessage("§c玩家不存在"); + return true; + } + sender.sendMessage( + "§e=== " + args[1] + " ==="); + sender.sendMessage("§7积分: §e" + + user.getOrDefault("points", 0)); + sender.sendMessage("§7邮箱: §e" + + user.getOrDefault("email", "无")); + sender.sendMessage("§7签到天数: §e" + + user.getOrDefault( + "total_checkin_days", 0)); + sender.sendMessage("§7邀请码: §e" + + user.getOrDefault( + "invite_code", "无")); + return true; + } + + // del + if (sub.equals("del")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login del <玩家>"); + return true; + } + db.deleteUser(args[1]); + sender.sendMessage("§a已删除 " + args[1]); + Player tp = Bukkit.getPlayer(args[1]); + if (tp != null && tp.isOnline()) + tp.kickPlayer("§c账号已被管理员删除"); + return true; + } + + // kick (挂机) + if (sub.equals("kick")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login kick <开关> [分钟]"); + return true; + } + String mode = args[1].toLowerCase(); + if (mode.equals("on") + || mode.equals("开") + || mode.equals("启用")) { + config.afkEnabled = true; + if (args.length >= 3) { + try { + config.afkTimeout = + Integer.parseInt( + args[2]) * 60; + } catch (NumberFormatException ignored) { + } + } + config.saveSettings(); + int minutes = + config.afkTimeout / 60; + sender.sendMessage( + "§a挂机踢出已开启,超时: " + + minutes + " 分钟"); + } else if (mode.equals("off") + || mode.equals("关") + || mode.equals("停用")) { + config.afkEnabled = false; + config.saveSettings(); + sender.sendMessage( + "§c挂机踢出已关闭"); + } else { + sender.sendMessage( + "§c无效参数 on/off"); + } + return true; + } + + // add + if (sub.equals("add")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login add <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + db.addPoints(args[1], amount); + sender.sendMessage("§a已为 " + args[1] + + " 增加 " + amount + " 积分"); + return true; + } + + // ticket + if (sub.equals("ticket") + || sub.equals("工单")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + ticket.openMain((Player) sender); + return true; + } + + sender.sendMessage("§c未知参数"); + return true; + } + /** + * 是否是垃圾站展示物品(带ID lore) + */ + private boolean isTrashDisplayItem( + ItemStack item) { + if (item == null + || item.getType() + == Material.AIR) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null || !im.hasLore()) + return false; + List lore = im.getLore(); + if (lore == null || lore.isEmpty()) + return false; + return lore.get(0) + .startsWith("§7ID: #"); + } + + private void sendChatHelp(Player p) { + p.sendMessage( + "§e§l======== 聊天过滤帮助 =========="); + p.sendMessage("§e/chat reload §7- 重载配置"); + p.sendMessage("§e/chat add §7- 添加白名单URL"); + p.sendMessage("§e/chat remove §7- 移除白名单URL"); + p.sendMessage("§e/chat addplayer <玩家> §7- 添加白名单玩家"); + p.sendMessage("§e/chat takeplayer <玩家> §7- 移除白名单玩家"); + p.sendMessage("§e/chat unmute <玩家> §7- 解禁玩家"); + p.sendMessage("§e/chat reset <玩家> §7- 重置违规记录"); + p.sendMessage( + "§e§l=================================="); + } + private int findEmptySlot(Inventory inv) { + for (int i = 0; i < 49; i++) { + ItemStack item = inv.getItem(i); + if (item == null + || item.getType() + == Material.AIR) + return i; + } + return -1; + } + + // ===== TabComplete ===== + @Override + public List onTabComplete( + CommandSender sender, Command cmd, + String label, String[] args) { + List list = new ArrayList<>(); + if (cmd.getName().equalsIgnoreCase( + "sdf1_login") + && args.length == 1) { + if (isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "pw", "email", + "sign", "reset", "undo", + "get", "del", "set", + "take", "add", "kick", + "ticket", "oa", "stop", + "shopadd", "shopdel","back")); + } else { + list.addAll(Arrays.asList( + "pw", "email", "sign", + "reset", "undo", + "ticket", "oa", "stop")); + } + } + if (cmd.getName().equalsIgnoreCase("chat") + && args.length == 1 + && isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "add", "remove", + "addplayer", "takeplayer", + "unmute", "reset")); + } + return list; + } + +} \ No newline at end of file diff --git a/src/main/java/Sdf1_login/TicketManager.java b/src/main/java/Sdf1_login/TicketManager.java index 3d2f9c5..c21edc2 100644 --- a/src/main/java/Sdf1_login/TicketManager.java +++ b/src/main/java/Sdf1_login/TicketManager.java @@ -1,1656 +1,1644 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -public class TicketManager { - - private final Main plugin; - - public static final String T_TICKET_MAIN = - "§6§l工单中心"; - public static final String T_TICKET_CREATE = - "§e§l创建工单"; - public static final String T_TICKET_MY = - "§b§l我的工单"; - public static final String T_TICKET_ADMIN = - "§c§l工单管理"; - public static final String T_TICKET_DETAIL = - "§a§l工单详情"; - - public static final String TYPE_BUG = - "bug反馈"; - public static final String TYPE_HELP = - "求助"; - public static final String TYPE_REPORT = - "举报"; - public static final String TYPE_APPLY = - "申请"; - public static final String TYPE_OTHER = - "其他"; - public static final String TYPE_DELETE = - "删号"; - public static final String TYPE_PASSWORD = - "修改密码"; - - - - private static final Map - PRICES = new LinkedHashMap<>(); - static { - PRICES.put(TYPE_BUG, 10); - PRICES.put(TYPE_HELP, 5); - PRICES.put(TYPE_REPORT, 8); - PRICES.put(TYPE_APPLY, 15); - PRICES.put(TYPE_OTHER, 5); - PRICES.put(TYPE_DELETE, 0); - PRICES.put(TYPE_PASSWORD, 0); - } - - - public TicketManager(Main plugin) { - this.plugin = plugin; - } - - // ========== GUI ========== - - public void openMain(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_TICKET_MAIN); - fillBg(g); - g.setItem(10, mk(Material.PAPER, - "§e创建工单", - "§7提交bug、求助、举报等")); - g.setItem(12, mk(Material.BOOK, - "§b我的工单", - "§7查看已提交的工单")); - g.setItem(14, mk(Material.CHEST, - "§6工单记录", - "§7查看历史工单")); - if (adm(p)) { - g.setItem(16, mk(Material.REDSTONE, - "§c工单管理", - "§7管理员专用")); - } - if (plugin.getDb() - .isServiceProvider( - p.getName())) { - g.setItem(18, mk(Material.EMERALD, - "§a服务商面板", - "§7接单、处理工单")); - } - g.setItem(22, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - public void openCreate(Player p) { - Inventory g = Bukkit.createInventory( - null, 36, T_TICKET_CREATE); - fillBg(g); - g.setItem(10, mk(Material.BOOK, - "§cbug反馈")); - g.setItem(11, mk(Material.EMERALD, - "§a求助")); - g.setItem(12, mk(Material.BLAZE_POWDER, - "§6举报")); - g.setItem(13, mk(Material.NAME_TAG, - "§e申请")); - g.setItem(14, mk(Material.PAPER, - "§7其他")); - g.setItem(20, mk(Material.BARRIER, - "§c申请删号", - "§7申请删除账号")); - g.setItem(21, mk(Material.NETHER_WART, - "§d申请改密", - "§7请求管理员协助改密")); - g.setItem(31, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - public void openMyTickets(Player p) { - Inventory g = Bukkit.createInventory( - null, 54, T_TICKET_MY); - fillBg(g); - List> list = - plugin.getDb() - .getTicketsByPlayer( - p.getName()); - fillList(g, list); - g.setItem(49, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - public void openDetail(Player p, int tid) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) { - p.sendMessage("§c工单不存在"); - return; - } - Inventory g = Bukkit.createInventory( - null, 27, T_TICKET_DETAIL); - fillBg(g); - - String type = sv(t, "type"); - String status = sv(t, "status"); - String requester = sv(t, "requester"); - String title = sv(t, "title"); - String desc = sv(t, "description"); - String assigned = sv(t, "assigned_to"); - int reward = nv(t, "reward_amount"); - int score = nv(t, "score"); - int confirmed = nv(t, "admin_confirmed"); - - g.setItem(10, mk(Material.PAPER, - "§e§l" + (title.isEmpty() - ? type : title), - "§7类型: §f" + type, - "§7状态: " + stxt(status), - "§7提交者: §f" + requester, - "§7奖励: §e" + reward + " 积分")); - - g.setItem(12, mk(Material.BOOK, - "§7详细描述", - desc.isEmpty() ? "§7无描述" - : "§7" + (desc.length() > 40 - ? desc.substring(0, 40) - + "..." : desc))); - - if (!assigned.isEmpty()) { - g.setItem(14, mk(Material.PLAYER_HEAD, - "§7处理人: §f" + assigned, - confirmed == 1 - ? "§7评分: §e" + score - : "§7处理中")); - } else { - g.setItem(14, mk(Material.BARRIER, - "§7未分配")); - } - - // 驳回原因 - if ("closed".equals(status)) { - String reason = sv(t, "reject_reason"); - if (!reason.isEmpty()) { - g.setItem(13, mk(Material.BARRIER, - "§c驳回原因", - "§7" + reason)); - } - } - - // 回复按钮(非关闭/已完结状态) - if (!"closed".equals(status) - && !"withdrawn".equals(status) - && !"resolved".equals(status)) { - g.setItem(15, mk( - Material.WRITABLE_BOOK, - "§b回复工单", - "§7向对方发送消息")); - g.setItem(16, mk(Material.BOOK, - "§7查看回复记录")); - } - - // ===== 管理员按钮 ===== - if (adm(p)) { - if ("pending".equals(status)) { - // 待分配:可分配给服务商 - g.setItem(19, mk(Material.EMERALD, - "§a分配工单", - "§7指定服务商处理")); - // 也可自己处理 - g.setItem(20, mk(Material.LIME_DYE, - "§a我来处理", - "§7管理员直接处理此工单")); - } - if ("processing".equals(status) - && !assigned.equals("admin") - && !assigned.isEmpty()) { - // 服务商在处理:可取消指派收回 - g.setItem(19, mk(Material.BARRIER, - "§c取消指派", - "§7取消服务商指派,收回工单")); - // 也可自己接手处理 - g.setItem(20, mk(Material.LIME_DYE, - "§a我来处理", - "§7管理员直接处理此工单")); - } - if ("processing".equals(status) - && assigned.equals("admin")) { - // 管理员自己在处理:可标记完结 - g.setItem(19, mk(Material.LIME_DYE, - "§a标记完结", - "§7确认工单处理完成")); - } - if ("replied".equals(status) - && assigned.equals("admin")) { - g.setItem(19, mk(Material.LIME_DYE, - "§a标记完结", - "§7确认工单处理完成")); - } - // 驳回(非已关闭/已完结状态) - if (!"closed".equals(status) - && !"resolved".equals(status) - && !"withdrawn".equals(status)) { - g.setItem(21, mk(Material.RED_DYE, - "§c驳回工单", - "§7驳回并关闭此工单")); - } - } - - // ===== 服务商按钮 ===== - if (plugin.getDb() - .isServiceProvider(p.getName()) - && !TYPE_DELETE.equals(type) - && !TYPE_PASSWORD.equals(type)) { - if ("pending".equals(status) - && assigned.isEmpty()) { - // 待分配无人:抢单 - g.setItem(19, mk( - Material.GOLD_INGOT, - "§6抢单", - "§7接取此工单")); - } - if (assigned.equals(p.getName()) - && ("processing".equals(status) - || "replied".equals(status))) { - // 我在处理:标记完结/上报异常 - g.setItem(19, mk( - Material.LIME_DYE, - "§a标记完结", - "§7上报处理完成")); - g.setItem(20, mk( - Material.REDSTONE, - "§c上报异常", - "§7报告处理中的问题")); - } - } - - g.setItem(22, mk(Material.ARROW, - "§7返回")); - - ItemStack info = g.getItem(10); - if (info != null) { - ItemMeta im = info.getItemMeta(); - if (im != null) { - im.setCustomModelData(tid); - info.setItemMeta(im); - } - } - p.openInventory(g); - } - - - public void openAdminList(Player p) { - Inventory g = Bukkit.createInventory( - null, 54, T_TICKET_ADMIN); - fillBg(g); - g.setItem(45, mk(Material.EMERALD, - "§a邀请服务商")); - g.setItem(46, mk(Material.BARRIER, - "§c移除服务商")); - g.setItem(47, mk(Material.CLOCK, - "§e服务商列表")); - g.setItem(48, mk(Material.NETHER_STAR, - "§e直接派单", - "§7管理员直接创建并派单")); - List> all = - new ArrayList<>(); - all.addAll(plugin.getDb() - .getTicketsByStatus("pending")); - all.addAll(plugin.getDb() - .getTicketsByStatus("processing")); - all.addAll(plugin.getDb() - .getTicketsByStatus("replied")); - all.addAll(plugin.getDb() - .getTicketsByStatus("completed")); - all.addAll(plugin.getDb() - .getTicketsByStatus("resolved")); - all.addAll(plugin.getDb() - .getTicketsByStatus("closed")); - fillList(g, all); - g.setItem(49, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - - public void openProviderPanel(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, "§d§l服务商面板"); - fillBg(g); - g.setItem(10, mk(Material.CHEST, - "§e抢单大厅", - "§7查看可接工单")); - g.setItem(12, mk(Material.CLOCK, - "§b我的工单", - "§7我正在处理的工单")); - g.setItem(14, mk(Material.PAPER, - "§6历史工单", - "§7已完成的工单")); - g.setItem(22, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - public void openGrabList(Player p) { - Inventory g = Bukkit.createInventory( - null, 54, "§6§l抢单大厅"); - fillBg(g); - List> pending = - plugin.getDb() - .getTicketsByStatus( - "pending"); - List> list = - new ArrayList<>(); - for (Map t : pending) { - if (sv(t, "assigned_to").isEmpty()) { - list.add(t); - } - } - fillList(g, list); - g.setItem(49, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - public void openMyAssigned(Player p) { - Inventory g = Bukkit.createInventory( - null, 54, - "§b§l我处理中的工单"); - fillBg(g); - List> list = - plugin.getDb() - .getTicketsByProvider( - p.getName()); - fillList(g, list); - g.setItem(49, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - // ========== 业务 ========== - - public int submitTicket(Player p, - String type, String title, - String desc) { - int price = PRICES - .getOrDefault(type, 5); - int id = plugin.getDb().createTicket( - type, "pending", "normal", - p.getName(), title, desc, - price, ""); - if (id > 0) { - if (TYPE_DELETE.equals(type) - || TYPE_PASSWORD.equals(type)) { - plugin.getDb().updateTicketField( - id, "assigned_to", "admin"); - plugin.getDb().updateTicketStatus( - id, "processing"); - p.sendMessage( - "§a§l[工单] §f工单 #" - + id + " 已提交," - + "等待管理员处理"); - } else { - p.sendMessage( - "§a§l[工单] §f创建成功!" - + " #§e" + id - + " 奖励: §e" - + price + "积分"); - } - notifyAdmins(p.getName(), id); - } else { - p.sendMessage("§c创建失败"); - } - return id; - } - - - public void adminCreateAndAssign( - Player p, String type, - String title, String desc, - String provider, int price) { - if (!plugin.getDb() - .isServiceProvider(provider)) { - p.sendMessage("§c" + provider - + " 不是服务商"); - return; - } - int id = plugin.getDb().createTicket( - type, "processing", "normal", - p.getName(), title, desc, - price, ""); - if (id <= 0) { - p.sendMessage("§c创建失败"); - return; - } - plugin.getDb().assignTicket( - id, provider); - if (price > 0) { - plugin.getDb().updateTicketField( - id, "reward_amount", price); - } - p.sendMessage("§a工单 #" + id - + " 已创建并分配给 " + provider); - Player tp = Bukkit.getPlayer(provider); - if (tp != null && tp.isOnline()) { - tp.sendMessage( - "§e§l[工单] §f管理员指派工单 #" - + id + " 给您"); - playAssignedSound(tp); - } - } - - public void grabTicket(Player p, int tid) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) { - p.sendMessage("§c工单不存在"); - return; - } - if (!"pending".equals( - sv(t, "status"))) { - p.sendMessage("§c该工单已被接单"); - return; - } - if (!sv(t, "assigned_to").isEmpty()) { - p.sendMessage("§c该工单已被分配"); - return; - } - plugin.getDb().assignTicket( - tid, p.getName()); - p.sendMessage("§a抢单成功!工单 #" + tid); - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) { - rp.sendMessage( - "§e§l[工单] §f您的工单 #" - + tid + " 已被 " - + p.getName() - + " 接单"); - } - } - - public void assignToProvider(Player p, - int tid, String provider, - int price) { - if (!plugin.getDb() - .isServiceProvider(provider)) { - p.sendMessage("§c" + provider - + " 不是服务商"); - return; - } - plugin.getDb().assignTicket( - tid, provider); - if (price > 0) { - plugin.getDb().updateTicketStatus( - tid, "processing"); - plugin.getDb().updateTicketField( - tid, "reward_amount", price); - } - p.sendMessage("§a工单 #" + tid - + " 已分配给 " + provider); - Player tp = Bukkit.getPlayer(provider); - if (tp != null && tp.isOnline()) { - tp.sendMessage( - "§e§l[工单] §f您有新工单: #" - + tid); - playAssignedSound(tp); - } - } - - public void confirmAndScore(Player p, - int tid, int score) { - if (score < 1 || score > 5) { - p.sendMessage("§c评分1-5"); - return; - } - plugin.getDb().confirmTicket( - tid, score); - p.sendMessage("§a工单 #" + tid - + " 已确认,评分: " + score); - Map t = - plugin.getDb().getTicket(tid); - if (t != null) { - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) { - rp.sendMessage( - "§a§l[工单] §f工单 #" - + tid - + " 已确认完结"); - } - } - settleTicket(tid); - } - - public void completeAsProvider(Player p, - int tid) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) { - p.sendMessage("§c工单不存在"); - return; - } - if (!sv(t, "assigned_to") - .equals(p.getName())) { - p.sendMessage("§c这不是你的工单"); - return; - } - plugin.getDb().completeTicket(tid); - p.sendMessage("§a工单 #" + tid - + " 已标记完结,等待确认"); - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) { - rp.sendMessage( - "§a§l[工单] §f工单 #" - + tid - + " 已处理完成"); - } - } - - public void rejectTicket(Player p, - int tid, String reason) { - plugin.getDb().updateTicketStatus( - tid, "closed"); - plugin.getDb().updateTicketField( - tid, "reject_reason", reason); - p.sendMessage("§c工单 #" + tid - + " 已驳回并关闭"); - Map t = - plugin.getDb().getTicket(tid); - if (t != null) { - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) { - rp.sendMessage( - "§c§l[工单] §f工单 #" - + tid - + " 被驳回: " - + reason); - } - } - } - - public void replyTicket(Player p, - int tid, String message) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) { - p.sendMessage("§c工单不存在"); - return; - } - String status = sv(t, "status"); - if ("closed".equals(status) - || "withdrawn".equals(status) - || "resolved".equals(status)) { - p.sendMessage("§c该工单已关闭"); - return; - } - plugin.getDb().addTicketReply( - tid, p.getName(), message); - p.sendMessage("§a回复成功"); - String requester = - sv(t, "requester"); - String assigned = - sv(t, "assigned_to"); - String sender = p.getName(); - if (!requester.equals(sender)) { - Player rp = Bukkit.getPlayer( - requester); - if (rp != null && rp.isOnline()) { - rp.sendMessage( - "§e§l[工单] §f工单 #" - + tid + " 新回复"); - } - } - if (!assigned.isEmpty() - && !assigned.equals(sender)) { - Player ap = Bukkit.getPlayer( - assigned); - if (ap != null && ap.isOnline()) { - ap.sendMessage( - "§e§l[工单] §f工单 #" - + tid + " 新回复"); - } - } - } - - public void showReplies(Player p, - int tid) { - List> replies = - plugin.getDb() - .getTicketReplies(tid); - if (replies.isEmpty()) { - p.sendMessage("§7暂无回复"); - return; - } - p.sendMessage("§e§l=== 回复记录 ==="); - SimpleDateFormat sdf = - new SimpleDateFormat( - "MM-dd HH:mm"); - for (Map r : replies) { - String sender = sv(r, "sender"); - String msg2 = sv(r, "message"); - long time = ((Number) r.get( - "create_time")).longValue(); - p.sendMessage("§7[" + sdf.format( - new Date(time)) + "] §f" - + sender + "§7: " + msg2); - } - p.sendMessage( - "§e§l================="); - } - - public void withdrawTicket(Player p, - int tid) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) { - p.sendMessage("§c工单不存在"); - return; - } - if (!sv(t, "requester") - .equals(p.getName()) - && !adm(p)) { - p.sendMessage("§c无权操作"); - return; - } - plugin.getDb().updateTicketStatus( - tid, "withdrawn"); - p.sendMessage("§a工单 #" + tid - + " 已撤回"); - } - - public void showProviderList(Player p) { - List> list = - plugin.getDb() - .getAllServiceProviders(); - if (list.isEmpty()) { - p.sendMessage("§7暂无服务商"); - return; - } - p.sendMessage( - "§e§l=== 服务商列表 ==="); - for (Map pro : list) { - p.sendMessage("§7- §f" - + pro.get("player_name") - + " §7角色: §e" - + pro.get("role")); - } - p.sendMessage( - "§e§l==================="); - } - - public void autoProcessCompleted() { - long timeoutMs = 10 * 60 * 1000L; - - // 自动处理删号工单(5分钟自动通过) - List> deleteTickets = - plugin.getDb() - .getTicketsByStatus( - "processing"); - for (Map t : deleteTickets) { - String dtype = sv(t, "type"); - if (!TYPE_DELETE.equals(dtype)) continue; - int did = nv(t, "id"); - int dconf = nv(t, "admin_confirmed"); - // 已经处理过了跳过 - if (dconf != 0) continue; - String dasg = sv(t, "assigned_to"); - // 只处理分配给admin的 - if (!"admin".equals(dasg)) continue; - long createTime = ((Number) t.get( - "create_time")).longValue(); - if (System.currentTimeMillis() - - createTime >= 5 * 60 * 1000L) { - processDeleteAccount(did, t); - } - } - - - // 已完结未确认→10分钟自动完结 - List> list = - plugin.getDb() - .getUnconfirmedCompleted( - timeoutMs); - for (Map t : list) { - int id = nv(t, "id"); - plugin.getDb().confirmTicket(id, 3); - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) - rp.sendMessage( - "§e§l[工单] §f工单 #" - + id + " 已自动完结"); - String assigned = - sv(t, "assigned_to"); - if (!assigned.isEmpty()) { - Player pp = - Bukkit.getPlayer(assigned); - if (pp != null && pp.isOnline()) - pp.sendMessage( - "§e§l[工单] §f工单 #" - + id - + " 已自动完结"); - } - settleTicket(id); - } - - // 已回复但10分钟无互动→自动完结 - List> stale = - plugin.getDb() - .getRepliedStaleTickets( - timeoutMs); - for (Map t : stale) { - int id = nv(t, "id"); - plugin.getDb().confirmTicket(id, 3); - Player rp = Bukkit.getPlayer( - sv(t, "requester")); - if (rp != null && rp.isOnline()) - rp.sendMessage( - "§e§l[工单] §f工单 #" - + id + " 超时自动完结"); - settleTicket(id); - } - } - - - - // ========== GUI点击 ========== - public boolean handleClick(Player p, - String title, int slot) { - - if (title.equals(T_TICKET_MAIN)) { - if (slot == 10) openCreate(p); - else if (slot == 12) - openMyTickets(p); - else if (slot == 14) - openMyTickets(p); - else if (slot == 16) { - if (adm(p)) openAdminList(p); - else p.sendMessage("§c权限不足"); - } - else if (slot == 18) { - if (plugin.getDb() - .isServiceProvider(p.getName())) { - openProviderPanel(p); - } else { - p.sendMessage("§c权限不足"); - } - } - else if (slot == 22) - plugin.getGui().openMain(p); - return true; - } - - if (title.equals(T_TICKET_CREATE)) { - if (slot == 31) { - openMain(p); - return true; - } - String type = null; - if (slot == 10) type = TYPE_BUG; - else if (slot == 11) type = TYPE_HELP; - else if (slot == 12) type = TYPE_REPORT; - else if (slot == 13) type = TYPE_APPLY; - else if (slot == 14) type = TYPE_OTHER; - else if (slot == 20) type = TYPE_DELETE; - else if (slot == 21) type = TYPE_PASSWORD; - if (type != null) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_TITLE; - plugin.getChatInput() - .getState(p) - .ticketType = type; - if (TYPE_DELETE.equals(type)) { - p.sendMessage("§e请描述删号原因:"); - } else if (TYPE_PASSWORD.equals(type)) { - p.sendMessage("§e请描述需要协助的内容:"); - } else { - p.sendMessage("§e请输入工单标题:"); - } - } - return true; - } - - - if (title.equals(T_TICKET_MY)) { - if (slot == 49) { - openMain(p); - return true; - } - if (slot >= 0 && slot < 45) { - ItemStack it = p.getOpenInventory() - .getTopInventory().getItem(slot); - int id = gt(it); - if (id > 0) openDetail(p, id); - } - return true; - } - - if (title.equals(T_TICKET_DETAIL)) { - return handleClickDetail(p, slot); - } - - if (title.equals(T_TICKET_ADMIN)) { - if (slot == 49) { - openMain(p); - return true; - } - if (slot == 45) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_INVITE_PROVIDER; - p.sendMessage("§e输入服务商玩家名:"); - return true; - } - if (slot == 46) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_REMOVE_PROVIDER; - p.sendMessage("§e输入要移除的服务商:"); - return true; - } - if (slot == 47) { - showProviderList(p); - return true; - } - if (slot == 48) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_ADMIN_TYPE; - p.sendMessage("§e选择工单类型:"); - p.sendMessage("§7bug/求助/举报/申请/其他"); - return true; - } - if (slot >= 0 && slot < 45) { - ItemStack it = p.getOpenInventory() - .getTopInventory().getItem(slot); - int id = gt(it); - if (id > 0) openDetail(p, id); - } - return true; - } - - if (title.equals("§d§l服务商面板")) { - if (slot == 22) { - plugin.getGui().openMain(p); - return true; - } - if (slot == 10) openGrabList(p); - else if (slot == 12) openMyAssigned(p); - else if (slot == 14) openMyTickets(p); - return true; - } - - if (title.equals("§6§l抢单大厅")) { - if (slot == 49) { - openProviderPanel(p); - return true; - } - if (slot >= 0 && slot < 45) { - ItemStack it = p.getOpenInventory() - .getTopInventory().getItem(slot); - int id = gt(it); - if (id > 0) openDetail(p, id); - } - return true; - } - - if (title.equals("§b§l我处理中的工单")) { - if (slot == 49) { - openProviderPanel(p); - return true; - } - if (slot >= 0 && slot < 45) { - ItemStack it = p.getOpenInventory() - .getTopInventory().getItem(slot); - int id = gt(it); - if (id > 0) openDetail(p, id); - } - return true; - } - - return false; - } - - private boolean handleClickDetail(Player p, - int slot) { - if (slot == 22) { - openMyTickets(p); - return true; - } - int id = gtFromDetail(p); - if (id <= 0) return true; - Map t = - plugin.getDb().getTicket(id); - if (t == null) return true; - - String st = sv(t, "status"); - int conf = nv(t, "admin_confirmed"); - String asg = sv(t, "assigned_to"); - - // 回复按钮 - if (slot == 15) { - if (!"closed".equals(st) - && !"withdrawn".equals(st) - && !"resolved".equals(st)) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_REPLY; - plugin.getChatInput() - .getState(p).ticketId = id; - p.sendMessage("§e输入回复内容:"); - } - return true; - } - - // 查看回复记录 - if (slot == 16) { - showReplies(p, id); - return true; - } - - // slot 19 - if (slot == 19) { - // 管理员:分配工单 - if (adm(p) && "pending".equals(st) - && asg.isEmpty()) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_ASSIGN; - plugin.getChatInput() - .getState(p).ticketId = id; - p.sendMessage( - "§e输入: 服务商名 价格"); - return true; - } - // 管理员:取消指派(服务商在处理中) - if (adm(p) && "processing".equals(st) - && !asg.equals("admin") - && !asg.isEmpty()) { - cancelTicket(p, id); - openDetail(p, id); - return true; - } - // 管理员:自己处理(pending) - if (adm(p) && "pending".equals(st) - && asg.isEmpty()) { - plugin.getDb().assignTicket( - id, "admin"); - plugin.getDb().updateTicketStatus( - id, "processing"); - p.sendMessage("§a工单 #" + id - + " 已由您接手处理"); - openDetail(p, id); - return true; - } - // 管理员:标记完结(自己在处理) - if (adm(p) && "processing".equals(st) - && asg.equals("admin")) { - Map tk = - plugin.getDb().getTicket(id); - String tkType = sv(tk, "type"); - if (TYPE_DELETE.equals(tkType)) { - approveDeleteTicket(p, id); - return true; - } - plugin.getDb().completeTicket(id); - p.sendMessage("§a工单 #" + id - + " 已标记完结"); - openDetail(p, id); - return true; - } - // 管理员:标记完结(已回复状态) - if (adm(p) && "replied".equals(st) - && asg.equals("admin")) { - Map tk = - plugin.getDb().getTicket(id); - String tkType = sv(tk, "type"); - if (TYPE_DELETE.equals(tkType)) { - approveDeleteTicket(p, id); - return true; - } - plugin.getDb().completeTicket(id); - p.sendMessage("§a工单 #" + id - + " 已标记完结"); - openDetail(p, id); - return true; - } - - // 服务商:抢单 - if (plugin.getDb() - .isServiceProvider(p.getName()) - && "pending".equals(st) - && asg.isEmpty()) { - grabTicket(p, id); - openDetail(p, id); - return true; - } - // 服务商:标记完结 - if (plugin.getDb() - .isServiceProvider(p.getName()) - && asg.equals(p.getName()) - && ("processing".equals(st) - || "replied".equals(st))) { - completeAsProvider(p, id); - openDetail(p, id); - return true; - } - return true; - } - - // slot 20 - if (slot == 20) { - // 管理员:自己处理(pending) - if (adm(p) && "pending".equals(st) - && asg.isEmpty()) { - plugin.getDb().assignTicket( - id, "admin"); - plugin.getDb().updateTicketStatus( - id, "processing"); - p.sendMessage("§a工单 #" + id - + " 已由您接手处理"); - openDetail(p, id); - return true; - } - // 管理员:自己接手(服务商在处理中) - if (adm(p) && "processing".equals(st) - && !asg.equals("admin") - && !asg.isEmpty()) { - plugin.getDb().assignTicket( - id, "admin"); - p.sendMessage("§a工单 #" + id - + " 已由您接手处理"); - openDetail(p, id); - return true; - } - // 服务商:上报异常 - if (plugin.getDb() - .isServiceProvider(p.getName()) - && asg.equals(p.getName()) - && ("processing".equals(st) - || "replied".equals(st))) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_REPORT_ISSUE; - plugin.getChatInput() - .getState(p).ticketId = id; - p.sendMessage("§e输入异常描述:"); - return true; - } - return true; - } - - // slot 21:驳回 - if (slot == 21) { - if (adm(p) && !"closed".equals(st) - && !"resolved".equals(st) - && !"withdrawn".equals(st)) { - p.closeInventory(); - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_REJECT; - plugin.getChatInput() - .getState(p).ticketId = id; - p.sendMessage("§e输入驳回原因:"); - } - return true; - } - - return true; - } - - // ========== 聊天输入 ========== - - public boolean handleChatInput(Player p, - String msg, - ChatInputManager.InputType type) { - switch (type) { - case TICKET_TITLE: { - plugin.getChatInput() - .getState(p) - .ticketTitle = msg; - plugin.getChatInput() - .getState(p).type = - ChatInputManager.InputType - .TICKET_DESC; - p.sendMessage( - "§e请输入工单详细描述:"); - return true; - } - case TICKET_DESC: { - String tt = plugin.getChatInput() - .getState(p).ticketType; - String tl = plugin.getChatInput() - .getState(p).ticketTitle; - plugin.getChatInput().reset(p); - submitTicket(p, tt, tl, msg); - return true; - } - case TICKET_SCORE: { - int tid2 = plugin.getChatInput() - .getState(p).ticketId; - try { - int sc = Integer.parseInt( - msg.trim()); - Map tk = - plugin.getDb() - .getTicket(tid2); - String st = tk != null - ? sv(tk, "status") - : ""; - if ("completed".equals(st)) { - plugin.getChatInput() - .reset(p); - confirmAndScore( - p, tid2, sc); - } else { - plugin.getChatInput() - .reset(p); - p.sendMessage( - "§c工单尚未处理完成"); - } - } catch ( - NumberFormatException e) { - plugin.getChatInput() - .reset(p); - p.sendMessage( - "§c请输入数字1-5"); - } - return true; - } - case TICKET_REJECT: { - int tid2 = plugin.getChatInput() - .getState(p).ticketId; - plugin.getChatInput().reset(p); - rejectTicket(p, tid2, msg); - return true; - } - case TICKET_ASSIGN: { - int tid2 = plugin.getChatInput() - .getState(p).ticketId; - String[] parts = msg.trim() - .split("\\s+"); - String prov = parts[0]; - int price = 0; - if (parts.length >= 2) { - try { - price = Integer.parseInt( - parts[1]); - } catch ( - NumberFormatException e) { - p.sendMessage( - "§c价格格式错误"); - } - } - plugin.getChatInput().reset(p); - assignToProvider(p, tid2, - prov, price); - runSync(() -> - openDetail(p, tid2)); - return true; - } - case TICKET_INVITE_PROVIDER: { - String tgt = msg.trim(); - plugin.getChatInput().reset(p); - if (!plugin.getDb() - .userExists(tgt)) { - p.sendMessage( - "§c玩家不存在"); - return true; - } - plugin.getDb() - .addServiceProvider( - tgt, "waiter"); - p.sendMessage("§a已邀请 " + tgt - + " 成为服务商"); - return true; - } - case TICKET_REMOVE_PROVIDER: { - String tgt = msg.trim(); - plugin.getChatInput().reset(p); - if (!plugin.getDb() - .isServiceProvider( - tgt)) { - p.sendMessage("§c" + tgt - + " 不是服务商"); - return true; - } - plugin.getDb() - .removeServiceProvider( - tgt); - p.sendMessage( - "§a已移除 " + tgt); - return true; - } - default: - return false; - } - } - - // ========== 私有 ========== - - private void settleTicket(int tid) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) return; - String requester = - sv(t, "requester"); - String assigned = - sv(t, "assigned_to"); - int reward = nv(t, "reward_amount"); - int score = nv(t, "score"); - ConfigManager cfg = - plugin.getConfig2(); - if (reward > 0) { - plugin.getDb().addPoints( - requester, reward); - Player rp = Bukkit.getPlayer( - requester); - if (rp != null && rp.isOnline()) { - rp.sendMessage( - "§e§l[工单] §f获得 " - + reward - + " 积分奖励"); - } - } - if (!assigned.isEmpty()) { - int pts = (int) (score - * cfg.providerPointsPerScore); - if (pts > 0) { - plugin.getDb().addPoints( - assigned, pts); - } - double money = - cfg.baseEconomyReward - + score - * cfg.providerEconomyPerScore; - boolean ok = false; - try { - if (plugin.getEconomy() != null) { - OfflinePlayer op = - Bukkit.getOfflinePlayer( - assigned); - plugin.getEconomy() - .depositPlayer( - op, money); - ok = true; - } - } catch (Exception e) { - plugin.getLogger().warning( - "[Sdf1_login] 经济失败: " - + e.getMessage()); - } - Player pp = - Bukkit.getPlayer(assigned); - if (pp != null && pp.isOnline()) { - pp.sendMessage( - "§a§l[工单] §f工单 #" - + tid - + " 确认完结"); - if (pts > 0) - pp.sendMessage("§7积分: §e" - + pts); - if (ok) - pp.sendMessage("§7经济: §a$" - + String.format( - "%.0f", money)); - } - } - } - - private void notifyAdmins(String creator, - int tid) { - String tag = - plugin.getConfig2().adminTag; - for (Player op : - Bukkit.getOnlinePlayers()) { - if (op.getScoreboardTags() - .contains(tag)) { - op.sendMessage( - "§e§l[工单] §f" + creator - + " 提交了新工单 #" - + tid); - } - } - } - - private void playAssignedSound(Player p) { - for (int i = 0; i < 3; i++) { - final int d = i * 15; - Bukkit.getScheduler() - .runTaskLater(plugin, () -> { - if (p.isOnline()) { - p.playSound(p.getLocation(), - Sound - .BLOCK_NOTE_BLOCK_CHIME, - 1.0f, 0.79f); - p.playSound(p.getLocation(), - Sound - .BLOCK_NOTE_BLOCK_BASS, - 1.0f, 0.79f); - p.playSound(p.getLocation(), - Sound - .BLOCK_NOTE_BLOCK_PLING, - 1.0f, 0.79f); - } - }, d); - } - } - - private void runSync(Runnable task) { - Bukkit.getScheduler() - .runTask(plugin, task); - } - - private boolean adm(Player p) { - return p.getScoreboardTags().contains( - plugin.getConfig2().adminTag); - } - - private void fillBg(Inventory g) { - ItemStack glass = mk( - Material.GRAY_STAINED_GLASS_PANE, - " "); - for (int i = 0; i < g.getSize(); i++) { - if (g.getItem(i) == null - || g.getItem(i).getType() - == Material.AIR) { - g.setItem(i, glass); - } - } - } - - private void fillList(Inventory g, - List> list) { - if (list.isEmpty()) { - g.setItem(22, mk(Material.BARRIER, - "§7暂无工单")); - return; - } - int slot = 0; - for (Map t : list) { - if (slot >= 45) break; - g.setItem(slot, makeItem(t)); - slot++; - } - } - /** - * 执行删号流程 - */ - /** - * 执行删号流程 - */ - public void processDeleteAccount(int tid, - Map t) { - String requester = sv(t, "requester"); - plugin.getDb().updateTicketStatus( - tid, "resolved"); - plugin.getDb().updateTicketField(tid, - "admin_confirmed", 1); - Player target = - Bukkit.getPlayer(requester); - if (target != null && target.isOnline()) { - plugin.backupInventory(target); - target.sendMessage( - "§a§l[工单] §f您的删号请求已通过"); - target.sendMessage( - "§7物品已备份,下次登录可取回"); - target.kickPlayer( - "§a删号成功,物品已备份。\n" - + "请等待5分钟后重新注册取回物品。"); - } else { - // 离线:仅标记,不操作数据库 - plugin.getLogger().info( - "[Sdf1_login] " + requester - + " 删号工单已通过,等待上线执行"); - } - // 延迟执行删号 - final String name = requester; - Bukkit.getScheduler().runTaskLater( - plugin, () -> { - plugin.getDb().deleteUser(name); - plugin.getLogger().info( - "[Sdf1_login] " + name - + " 删号工单已执行"); - }, 100L); - } - - /** - * 管理员手动批准删号 - */ - public void approveDeleteTicket(Player p, - int tid) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) { - p.sendMessage("§c工单不存在"); - return; - } - if (!TYPE_DELETE.equals(sv(t, "type"))) { - p.sendMessage("§c这不是删号工单"); - return; - } - p.sendMessage("§a工单 #" + tid - + " 已批准,正在执行删号..."); - processDeleteAccount(tid, t); - } - - /** - * 管理员取消工单(服务商异常后) - */ - /** - * 管理员取消工单(解除服务商绑定,管理员可自行处理) - */ - public void cancelTicket(Player p, int tid) { - Map t = - plugin.getDb().getTicket(tid); - if (t == null) { - p.sendMessage("§c工单不存在"); - return; - } - // 解除服务商绑定,状态回退到pending - plugin.getDb().updateTicketField( - tid, "assigned_to", ""); - plugin.getDb().updateTicketStatus( - tid, "pending"); - p.sendMessage("§a工单 #" + tid - + " 已取消指派,回到待分配状态"); - // 通知原服务商 - String assigned = sv(t, "assigned_to"); - if (!assigned.isEmpty()) { - Player ap = Bukkit.getPlayer(assigned); - if (ap != null && ap.isOnline()) { - ap.sendMessage( - "§c§l[工单] §f工单 #" - + tid - + " 已被管理员取消指派"); - } - } - } - - - /** - * 服务商查看历史工单 - */ - public void openProviderHistory(Player p) { - Inventory g = Bukkit.createInventory( - null, 54, - "§6§l我的历史工单"); - fillBg(g); - List> all = - plugin.getDb() - .getTicketsByProviderAll( - p.getName()); - fillList(g, all); - g.setItem(49, mk(Material.ARROW, - "§7返回")); - p.openInventory(g); - } - - private ItemStack makeItem( - Map t) { - int id = nv(t, "id"); - String type = sv(t, "type"); - String status = sv(t, "status"); - String requester = - sv(t, "requester"); - String title = sv(t, "title"); - int reward = nv(t, "reward_amount"); - Material mat; - switch (status) { - case "processing": - mat = Material.YELLOW_DYE; - break; - case "replied": - mat = Material.CYAN_DYE; - break; - case "completed": - mat = Material.LIME_DYE; - break; - case "resolved": - mat = Material.LIME_DYE; - break; - case "closed": - mat = Material.GRAY_DYE; - break; - case "rejected": - mat = Material.RED_DYE; - break; - case "withdrawn": - mat = Material.GRAY_DYE; - break; - default: - mat = Material.ORANGE_DYE; - break; - } - ItemStack item = new ItemStack(mat); - ItemMeta im = item.getItemMeta(); - if (im != null) { - im.setDisplayName("§e#" + id - + " " + (title.isEmpty() - ? type : title)); - List lore = - new ArrayList<>(); - lore.add("§7类型: §f" + type); - lore.add("§7状态: " - + stxt(status)); - lore.add("§7提交者: §f" - + requester); - lore.add("§7奖励: §e" + reward - + " 积分"); - lore.add(""); - lore.add("§7点击查看详情"); - im.setLore(lore); - im.setCustomModelData(id); - item.setItemMeta(im); - } - return item; - } - - private int gt(ItemStack item) { - if (item == null) return -1; - ItemMeta im = item.getItemMeta(); - if (im == null) return -1; - if (im.hasCustomModelData()) - return im.getCustomModelData(); - return -1; - } - - private int gtFromDetail(Player p) { - ItemStack it = p.getOpenInventory() - .getTopInventory().getItem(10); - return gt(it); - } - - private String stxt(String status) { - switch (status) { - case "pending": - return "§e待处理"; - case "processing": - return "§b处理中"; - case "replied": - return "§9已回复"; - case "completed": - return "§a待确认"; - case "resolved": - return "§a已完结"; - case "closed": - return "§7已关闭"; - case "rejected": - return "§c已驳回"; - case "withdrawn": - return "§7已撤回"; - default: - return "§7" + status; - } - } - - private String sv(Map m, - String key) { - Object v = m.get(key); - return v != null - ? String.valueOf(v) : ""; - } - - private int nv(Map m, - String key) { - Object v = m.get(key); - return v instanceof Number - ? ((Number) v).intValue() : 0; - } - - private ItemStack mk(Material mat, - String name, String... lore) { - ItemStack it = new ItemStack(mat); - ItemMeta im = it.getItemMeta(); - if (im != null) { - im.setDisplayName(name); - if (lore.length > 0) { - List list = - new ArrayList<>(); - for (String l : lore) { - if (!l.isEmpty()) - list.add(l); - } - if (!list.isEmpty()) - im.setLore(list); - } - it.setItemMeta(im); - } - return it; - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TicketManager { + + private final Main plugin; + + public static final String T_TICKET_MAIN = + "§6§l工单中心"; + public static final String T_TICKET_CREATE = + "§e§l创建工单"; + public static final String T_TICKET_MY = + "§b§l我的工单"; + public static final String T_TICKET_ADMIN = + "§c§l工单管理"; + public static final String T_TICKET_DETAIL = + "§a§l工单详情"; + + public static final String TYPE_BUG = + "bug反馈"; + public static final String TYPE_HELP = + "求助"; + public static final String TYPE_REPORT = + "举报"; + public static final String TYPE_APPLY = + "申请"; + public static final String TYPE_OTHER = + "其他"; + public static final String TYPE_DELETE = + "删号"; + public static final String TYPE_PASSWORD = + "修改密码"; + + + + private static final Map + PRICES = new LinkedHashMap<>(); + static { + PRICES.put(TYPE_BUG, 10); + PRICES.put(TYPE_HELP, 5); + PRICES.put(TYPE_REPORT, 8); + PRICES.put(TYPE_APPLY, 15); + PRICES.put(TYPE_OTHER, 5); + PRICES.put(TYPE_DELETE, 0); + PRICES.put(TYPE_PASSWORD, 0); + } + + + public TicketManager(Main plugin) { + this.plugin = plugin; + } + + // ========== GUI ========== + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_MAIN); + fillBg(g); + g.setItem(10, mk(Material.PAPER, + "§e创建工单", + "§7提交bug、求助、举报等")); + g.setItem(12, mk(Material.BOOK, + "§b我的工单", + "§7查看已提交的工单")); + g.setItem(14, mk(Material.CHEST, + "§6工单记录", + "§7查看历史工单")); + if (adm(p)) { + g.setItem(16, mk(Material.REDSTONE, + "§c工单管理", + "§7管理员专用")); + } + if (plugin.getDb() + .isServiceProvider( + p.getName())) { + g.setItem(18, mk(Material.EMERALD, + "§a服务商面板", + "§7接单、处理工单")); + } + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openCreate(Player p) { + Inventory g = Bukkit.createInventory( + null, 36, T_TICKET_CREATE); + fillBg(g); + g.setItem(10, mk(Material.BOOK, + "§cbug反馈")); + g.setItem(11, mk(Material.EMERALD, + "§a求助")); + g.setItem(12, mk(Material.BLAZE_POWDER, + "§6举报")); + g.setItem(13, mk(Material.NAME_TAG, + "§e申请")); + g.setItem(14, mk(Material.PAPER, + "§7其他")); + g.setItem(20, mk(Material.BARRIER, + "§c申请删号", + "§7申请删除账号")); + g.setItem(21, mk(Material.NETHER_WART, + "§d申请改密", + "§7请求管理员协助改密")); + g.setItem(31, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyTickets(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_MY); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByPlayer( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openDetail(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_DETAIL); + fillBg(g); + + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = sv(t, "requester"); + String title = sv(t, "title"); + String desc = sv(t, "description"); + String assigned = sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + int confirmed = nv(t, "admin_confirmed"); + + g.setItem(10, mk(Material.PAPER, + "§e§l" + (title.isEmpty() + ? type : title), + "§7类型: §f" + type, + "§7状态: " + stxt(status), + "§7提交者: §f" + requester, + "§7奖励: §e" + reward + " 积分")); + + g.setItem(12, mk(Material.BOOK, + "§7详细描述", + desc.isEmpty() ? "§7无描述" + : "§7" + (desc.length() > 40 + ? desc.substring(0, 40) + + "..." : desc))); + + if (!assigned.isEmpty()) { + g.setItem(14, mk(Material.PLAYER_HEAD, + "§7处理人: §f" + assigned, + confirmed == 1 + ? "§7评分: §e" + score + : "§7处理中")); + } else { + g.setItem(14, mk(Material.BARRIER, + "§7未分配")); + } + + // 驳回原因 + if ("closed".equals(status)) { + String reason = sv(t, "reject_reason"); + if (!reason.isEmpty()) { + g.setItem(13, mk(Material.BARRIER, + "§c驳回原因", + "§7" + reason)); + } + } + + // 回复按钮(非关闭/已完结状态) + if (!"closed".equals(status) + && !"withdrawn".equals(status) + && !"resolved".equals(status)) { + g.setItem(15, mk( + Material.WRITABLE_BOOK, + "§b回复工单", + "§7向对方发送消息")); + g.setItem(16, mk(Material.BOOK, + "§7查看回复记录")); + } + + // ===== 管理员按钮 ===== + if (adm(p)) { + if ("pending".equals(status)) { + // 待分配:可分配给服务商 + g.setItem(19, mk(Material.EMERALD, + "§a分配工单", + "§7指定服务商处理")); + // 也可自己处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && !assigned.equals("admin") + && !assigned.isEmpty()) { + // 服务商在处理:可取消指派收回 + g.setItem(19, mk(Material.BARRIER, + "§c取消指派", + "§7取消服务商指派,收回工单")); + // 也可自己接手处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && assigned.equals("admin")) { + // 管理员自己在处理:可标记完结 + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + if ("replied".equals(status) + && assigned.equals("admin")) { + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + // 驳回(非已关闭/已完结状态) + if (!"closed".equals(status) + && !"resolved".equals(status) + && !"withdrawn".equals(status)) { + g.setItem(21, mk(Material.RED_DYE, + "§c驳回工单", + "§7驳回并关闭此工单")); + } + } + + // ===== 服务商按钮 ===== + if (plugin.getDb() + .isServiceProvider(p.getName()) + && !TYPE_DELETE.equals(type) + && !TYPE_PASSWORD.equals(type)) { + if ("pending".equals(status) + && assigned.isEmpty()) { + // 待分配无人:抢单 + g.setItem(19, mk( + Material.GOLD_INGOT, + "§6抢单", + "§7接取此工单")); + } + if (assigned.equals(p.getName()) + && ("processing".equals(status) + || "replied".equals(status))) { + // 我在处理:标记完结/上报异常 + g.setItem(19, mk( + Material.LIME_DYE, + "§a标记完结", + "§7上报处理完成")); + g.setItem(20, mk( + Material.REDSTONE, + "§c上报异常", + "§7报告处理中的问题")); + } + } + + g.setItem(22, mk(Material.ARROW, + "§7返回")); + + ItemStack info = g.getItem(10); + if (info != null) { + ItemMeta im = info.getItemMeta(); + if (im != null) { + im.setCustomModelData(tid); + info.setItemMeta(im); + } + } + p.openInventory(g); + } + + + public void openAdminList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_ADMIN); + fillBg(g); + g.setItem(45, mk(Material.EMERALD, + "§a邀请服务商")); + g.setItem(46, mk(Material.BARRIER, + "§c移除服务商")); + g.setItem(47, mk(Material.CLOCK, + "§e服务商列表")); + g.setItem(48, mk(Material.NETHER_STAR, + "§e直接派单", + "§7管理员直接创建并派单")); + List> all = + new ArrayList<>(); + all.addAll(plugin.getDb() + .getTicketsByStatus("pending")); + all.addAll(plugin.getDb() + .getTicketsByStatus("processing")); + all.addAll(plugin.getDb() + .getTicketsByStatus("replied")); + all.addAll(plugin.getDb() + .getTicketsByStatus("completed")); + all.addAll(plugin.getDb() + .getTicketsByStatus("resolved")); + all.addAll(plugin.getDb() + .getTicketsByStatus("closed")); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + + public void openProviderPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§d§l服务商面板"); + fillBg(g); + g.setItem(10, mk(Material.CHEST, + "§e抢单大厅", + "§7查看可接工单")); + g.setItem(12, mk(Material.CLOCK, + "§b我的工单", + "§7我正在处理的工单")); + g.setItem(14, mk(Material.PAPER, + "§6历史工单", + "§7已完成的工单")); + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openGrabList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l抢单大厅"); + fillBg(g); + List> pending = + plugin.getDb() + .getTicketsByStatus( + "pending"); + List> list = + new ArrayList<>(); + for (Map t : pending) { + if (sv(t, "assigned_to").isEmpty()) { + list.add(t); + } + } + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyAssigned(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§b§l我处理中的工单"); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByProvider( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + // ========== 业务 ========== + + public int submitTicket(Player p, + String type, String title, + String desc) { + int price = PRICES + .getOrDefault(type, 5); + int id = plugin.getDb().createTicket( + type, "pending", "normal", + p.getName(), title, desc, + price, ""); + if (id > 0) { + if (TYPE_DELETE.equals(type) + || TYPE_PASSWORD.equals(type)) { + plugin.getDb().updateTicketField( + id, "assigned_to", "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage( + "§a§l[工单] §f工单 #" + + id + " 已提交," + + "等待管理员处理"); + } else { + p.sendMessage( + "§a§l[工单] §f创建成功!" + + " #§e" + id + + " 奖励: §e" + + price + "积分"); + } + notifyAdmins(p.getName(), id); + } else { + p.sendMessage("§c创建失败"); + } + return id; + } + + + public void adminCreateAndAssign( + Player p, String type, + String title, String desc, + String provider, int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + int id = plugin.getDb().createTicket( + type, "processing", "normal", + p.getName(), title, desc, + price, ""); + if (id <= 0) { + p.sendMessage("§c创建失败"); + return; + } + plugin.getDb().assignTicket( + id, provider); + if (price > 0) { + plugin.getDb().updateTicketField( + id, "reward_amount", price); + } + p.sendMessage("§a工单 #" + id + + " 已创建并分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f管理员指派工单 #" + + id + " 给您"); + playAssignedSound(tp); + } + } + + public void grabTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!"pending".equals( + sv(t, "status"))) { + p.sendMessage("§c该工单已被接单"); + return; + } + if (!sv(t, "assigned_to").isEmpty()) { + p.sendMessage("§c该工单已被分配"); + return; + } + plugin.getDb().assignTicket( + tid, p.getName()); + p.sendMessage("§a抢单成功!工单 #" + tid); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f您的工单 #" + + tid + " 已被 " + + p.getName() + + " 接单"); + } + } + + public void assignToProvider(Player p, + int tid, String provider, + int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + plugin.getDb().assignTicket( + tid, provider); + if (price > 0) { + plugin.getDb().updateTicketStatus( + tid, "processing"); + plugin.getDb().updateTicketField( + tid, "reward_amount", price); + } + p.sendMessage("§a工单 #" + tid + + " 已分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f您有新工单: #" + + tid); + playAssignedSound(tp); + } + } + + public void confirmAndScore(Player p, + int tid, int score) { + if (score < 1 || score > 5) { + p.sendMessage("§c评分1-5"); + return; + } + plugin.getDb().confirmTicket( + tid, score); + p.sendMessage("§a工单 #" + tid + + " 已确认,评分: " + score); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已确认完结"); + } + } + settleTicket(tid); + } + + public void completeAsProvider(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "assigned_to") + .equals(p.getName())) { + p.sendMessage("§c这不是你的工单"); + return; + } + plugin.getDb().completeTicket(tid); + p.sendMessage("§a工单 #" + tid + + " 已标记完结,等待确认"); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已处理完成"); + } + } + + public void rejectTicket(Player p, + int tid, String reason) { + plugin.getDb().updateTicketStatus( + tid, "closed"); + plugin.getDb().updateTicketField( + tid, "reject_reason", reason); + p.sendMessage("§c工单 #" + tid + + " 已驳回并关闭"); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 被驳回: " + + reason); + } + } + } + + public void replyTicket(Player p, + int tid, String message) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + String status = sv(t, "status"); + if ("closed".equals(status) + || "withdrawn".equals(status) + || "resolved".equals(status)) { + p.sendMessage("§c该工单已关闭"); + return; + } + plugin.getDb().addTicketReply( + tid, p.getName(), message); + p.sendMessage("§a回复成功"); + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + String sender = p.getName(); + if (!requester.equals(sender)) { + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + if (!assigned.isEmpty() + && !assigned.equals(sender)) { + Player ap = Bukkit.getPlayer( + assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + } + + public void showReplies(Player p, + int tid) { + List> replies = + plugin.getDb() + .getTicketReplies(tid); + if (replies.isEmpty()) { + p.sendMessage("§7暂无回复"); + return; + } + p.sendMessage("§e§l=== 回复记录 ==="); + SimpleDateFormat sdf = + new SimpleDateFormat( + "MM-dd HH:mm"); + for (Map r : replies) { + String sender = sv(r, "sender"); + String msg2 = sv(r, "message"); + long time = ((Number) r.get( + "create_time")).longValue(); + p.sendMessage("§7[" + sdf.format( + new Date(time)) + "] §f" + + sender + "§7: " + msg2); + } + p.sendMessage( + "§e§l================="); + } + + public void withdrawTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "requester") + .equals(p.getName()) + && !adm(p)) { + p.sendMessage("§c无权操作"); + return; + } + plugin.getDb().updateTicketStatus( + tid, "withdrawn"); + p.sendMessage("§a工单 #" + tid + + " 已撤回"); + } + + public void showProviderList(Player p) { + List> list = + plugin.getDb() + .getAllServiceProviders(); + if (list.isEmpty()) { + p.sendMessage("§7暂无服务商"); + return; + } + p.sendMessage( + "§e§l=== 服务商列表 ==="); + for (Map pro : list) { + p.sendMessage("§7- §f" + + pro.get("player_name") + + " §7角色: §e" + + pro.get("role")); + } + p.sendMessage( + "§e§l==================="); + } + + public void autoProcessCompleted() { + long timeoutMs = 10 * 60 * 1000L; + + // 自动处理删号工单(5分钟自动通过) + List> deleteTickets = + plugin.getDb() + .getTicketsByStatus( + "processing"); + for (Map t : deleteTickets) { + String dtype = sv(t, "type"); + if (!TYPE_DELETE.equals(dtype)) continue; + int did = nv(t, "id"); + int dconf = nv(t, "admin_confirmed"); + // 已经处理过了跳过 + if (dconf != 0) continue; + String dasg = sv(t, "assigned_to"); + // 只处理分配给admin的 + if (!"admin".equals(dasg)) continue; + long createTime = ((Number) t.get( + "create_time")).longValue(); + if (System.currentTimeMillis() + - createTime >= 5 * 60 * 1000L) { + processDeleteAccount(did, t); + } + } + + + // 已完结未确认→10分钟自动完结 + List> list = + plugin.getDb() + .getUnconfirmedCompleted( + timeoutMs); + for (Map t : list) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 已自动完结"); + String assigned = + sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) + pp.sendMessage( + "§e§l[工单] §f工单 #" + + id + + " 已自动完结"); + } + settleTicket(id); + } + + // 已回复但10分钟无互动→自动完结 + List> stale = + plugin.getDb() + .getRepliedStaleTickets( + timeoutMs); + for (Map t : stale) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 超时自动完结"); + settleTicket(id); + } + } + + + + // ========== GUI点击 ========== + public boolean handleClick(Player p, + String title, int slot) { + + if (title.equals(T_TICKET_MAIN)) { + if (slot == 10) openCreate(p); + else if (slot == 12) + openMyTickets(p); + else if (slot == 14) + openMyTickets(p); + else if (slot == 16) { + if (adm(p)) openAdminList(p); + else p.sendMessage("§c权限不足"); + } + else if (slot == 18) { + if (plugin.getDb() + .isServiceProvider(p.getName())) { + openProviderPanel(p); + } else { + p.sendMessage("§c权限不足"); + } + } + else if (slot == 22) + plugin.getGui().openMain(p); + return true; + } + + if (title.equals(T_TICKET_CREATE)) { + if (slot == 31) { + openMain(p); + return true; + } + String type = null; + if (slot == 10) type = TYPE_BUG; + else if (slot == 11) type = TYPE_HELP; + else if (slot == 12) type = TYPE_REPORT; + else if (slot == 13) type = TYPE_APPLY; + else if (slot == 14) type = TYPE_OTHER; + else if (slot == 20) type = TYPE_DELETE; + else if (slot == 21) type = TYPE_PASSWORD; + if (type != null) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_TITLE; + plugin.getChatInput() + .getState(p) + .ticketType = type; + if (TYPE_DELETE.equals(type)) { + p.sendMessage("§e请描述删号原因:"); + } else if (TYPE_PASSWORD.equals(type)) { + p.sendMessage("§e请描述需要协助的内容:"); + } else { + p.sendMessage("§e请输入工单标题:"); + } + } + return true; + } + + + if (title.equals(T_TICKET_MY)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals(T_TICKET_DETAIL)) { + return handleClickDetail(p, slot); + } + + if (title.equals(T_TICKET_ADMIN)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot == 45) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_INVITE_PROVIDER; + p.sendMessage("§e输入服务商玩家名:"); + return true; + } + if (slot == 46) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REMOVE_PROVIDER; + p.sendMessage("§e输入要移除的服务商:"); + return true; + } + if (slot == 47) { + showProviderList(p); + return true; + } + if (slot == 48) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ADMIN_TYPE; + p.sendMessage("§e选择工单类型:"); + p.sendMessage("§7bug/求助/举报/申请/其他"); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§d§l服务商面板")) { + if (slot == 22) { + plugin.getGui().openMain(p); + return true; + } + if (slot == 10) openGrabList(p); + else if (slot == 12) openMyAssigned(p); + else if (slot == 14) openMyTickets(p); + return true; + } + + if (title.equals("§6§l抢单大厅")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§b§l我处理中的工单")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + return false; + } + + private boolean handleClickDetail(Player p, + int slot) { + if (slot == 22) { + openMyTickets(p); + return true; + } + int id = gtFromDetail(p); + if (id <= 0) return true; + Map t = + plugin.getDb().getTicket(id); + if (t == null) return true; + + String st = sv(t, "status"); + int conf = nv(t, "admin_confirmed"); + String asg = sv(t, "assigned_to"); + + // 回复按钮 + if (slot == 15) { + if (!"closed".equals(st) + && !"withdrawn".equals(st) + && !"resolved".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPLY; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入回复内容:"); + } + return true; + } + + // 查看回复记录 + if (slot == 16) { + showReplies(p, id); + return true; + } + + // slot 19 + if (slot == 19) { + // 管理员:分配工单 + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ASSIGN; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage( + "§e输入: 服务商名 价格"); + return true; + } + // 管理员:取消指派(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + cancelTicket(p, id); + openDetail(p, id); + return true; + } + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + Map tk = + plugin.getDb().getTicket(id); + String tkType = sv(tk, "type"); + if (TYPE_DELETE.equals(tkType)) { + approveDeleteTicket(p, id); + return true; + } + plugin.getDb().completeTicket(id); + p.sendMessage("§a工单 #" + id + + " 已标记完结"); + openDetail(p, id); + return true; + } + } + // 服务商:抢单 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && "pending".equals(st) + && asg.isEmpty()) { + grabTicket(p, id); + openDetail(p, id); + return true; + } + // 服务商:标记完结 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + completeAsProvider(p, id); + openDetail(p, id); + return true; + } + return true; + } + + // slot 20 + if (slot == 20) { + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:自己接手(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 服务商:上报异常 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPORT_ISSUE; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入异常描述:"); + return true; + } + return true; + } + + // slot 21:驳回 + if (slot == 21) { + if (adm(p) && !"closed".equals(st) + && !"resolved".equals(st) + && !"withdrawn".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REJECT; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入驳回原因:"); + } + return true; + } + + return true; + } + + // ========== 聊天输入 ========== + + public boolean handleChatInput(Player p, + String msg, + ChatInputManager.InputType type) { + switch (type) { + case TICKET_TITLE: { + plugin.getChatInput() + .getState(p) + .ticketTitle = msg; + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return true; + } + case TICKET_DESC: { + String tt = plugin.getChatInput() + .getState(p).ticketType; + String tl = plugin.getChatInput() + .getState(p).ticketTitle; + plugin.getChatInput().reset(p); + submitTicket(p, tt, tl, msg); + return true; + } + case TICKET_SCORE: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + Map tk = + plugin.getDb() + .getTicket(tid2); + String st = tk != null + ? sv(tk, "status") + : ""; + if ("completed".equals(st)) { + plugin.getChatInput() + .reset(p); + confirmAndScore( + p, tid2, sc); + } else { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c工单尚未处理完成"); + } + } catch ( + NumberFormatException e) { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return true; + } + case TICKET_REJECT: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + plugin.getChatInput().reset(p); + rejectTicket(p, tid2, msg); + return true; + } + case TICKET_ASSIGN: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + plugin.getChatInput().reset(p); + assignToProvider(p, tid2, + prov, price); + runSync(() -> + openDetail(p, tid2)); + return true; + } + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return true; + } + plugin.getDb() + .addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return true; + } + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .isServiceProvider( + tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return true; + } + plugin.getDb() + .removeServiceProvider( + tgt); + p.sendMessage( + "§a已移除 " + tgt); + return true; + } + default: + return false; + } + } + + // ========== 私有 ========== + + private void settleTicket(int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) return; + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + ConfigManager cfg = + plugin.getConfig2(); + if (reward > 0) { + plugin.getDb().addPoints( + requester, reward); + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f获得 " + + reward + + " 积分奖励"); + } + } + if (!assigned.isEmpty()) { + int pts = (int) (score + * cfg.providerPointsPerScore); + if (pts > 0) { + plugin.getDb().addPoints( + assigned, pts); + } + double money = + cfg.baseEconomyReward + + score + * cfg.providerEconomyPerScore; + boolean ok = false; + try { + if (plugin.getEconomy() != null) { + OfflinePlayer op = + Bukkit.getOfflinePlayer( + assigned); + plugin.getEconomy() + .depositPlayer( + op, money); + ok = true; + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 经济失败: " + + e.getMessage()); + } + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) { + pp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 确认完结"); + if (pts > 0) + pp.sendMessage("§7积分: §e" + + pts); + if (ok) + pp.sendMessage("§7经济: §a$" + + String.format( + "%.0f", money)); + } + } + } + + private void notifyAdmins(String creator, + int tid) { + String tag = + plugin.getConfig2().adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§e§l[工单] §f" + creator + + " 提交了新工单 #" + + tid); + } + } + } + + private void playAssignedSound(Player p) { + for (int i = 0; i < 3; i++) { + final int d = i * 15; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline()) { + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + }, d); + } + } + + private void runSync(Runnable task) { + Bukkit.getScheduler() + .runTask(plugin, task); + } + + private boolean adm(Player p) { + return p.getScoreboardTags().contains( + plugin.getConfig2().adminTag); + } + + private void fillBg(Inventory g) { + ItemStack glass = mk( + Material.GRAY_STAINED_GLASS_PANE, + " "); + for (int i = 0; i < g.getSize(); i++) { + if (g.getItem(i) == null + || g.getItem(i).getType() + == Material.AIR) { + g.setItem(i, glass); + } + } + } + + private void fillList(Inventory g, + List> list) { + if (list.isEmpty()) { + g.setItem(22, mk(Material.BARRIER, + "§7暂无工单")); + return; + } + int slot = 0; + for (Map t : list) { + if (slot >= 45) break; + g.setItem(slot, makeItem(t)); + slot++; + } + } + /** + * 执行删号流程 + */ + /** + * 执行删号流程 + */ + public void processDeleteAccount(int tid, + Map t) { + String requester = sv(t, "requester"); + plugin.getDb().updateTicketStatus( + tid, "resolved"); + plugin.getDb().updateTicketField(tid, + "admin_confirmed", 1); + Player target = + Bukkit.getPlayer(requester); + if (target != null && target.isOnline()) { + plugin.backupInventory(target); + target.sendMessage( + "§a§l[工单] §f您的删号请求已通过"); + target.sendMessage( + "§7物品已备份,下次登录可取回"); + target.kickPlayer( + "§a删号成功,物品已备份。\n" + + "请等待5分钟后重新注册取回物品。"); + } else { + // 离线:仅标记,不操作数据库 + plugin.getLogger().info( + "[Sdf1_login] " + requester + + " 删号工单已通过,等待上线执行"); + } + // 延迟执行删号 + final String name = requester; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + plugin.getDb().deleteUser(name); + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 删号工单已执行"); + }, 100L); + } + + /** + * 管理员手动批准删号 + */ + public void approveDeleteTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!TYPE_DELETE.equals(sv(t, "type"))) { + p.sendMessage("§c这不是删号工单"); + return; + } + p.sendMessage("§a工单 #" + tid + + " 已批准,正在执行删号..."); + processDeleteAccount(tid, t); + } + + /** + * 管理员取消工单(服务商异常后) + */ + /** + * 管理员取消工单(解除服务商绑定,管理员可自行处理) + */ + public void cancelTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + // 解除服务商绑定,状态回退到pending + plugin.getDb().updateTicketField( + tid, "assigned_to", ""); + plugin.getDb().updateTicketStatus( + tid, "pending"); + p.sendMessage("§a工单 #" + tid + + " 已取消指派,回到待分配状态"); + // 通知原服务商 + String assigned = sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player ap = Bukkit.getPlayer(assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 已被管理员取消指派"); + } + } + } + + + /** + * 服务商查看历史工单 + */ + public void openProviderHistory(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l我的历史工单"); + fillBg(g); + List> all = + plugin.getDb() + .getTicketsByProviderAll( + p.getName()); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private ItemStack makeItem( + Map t) { + int id = nv(t, "id"); + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = + sv(t, "requester"); + String title = sv(t, "title"); + int reward = nv(t, "reward_amount"); + Material mat; + switch (status) { + case "processing": + mat = Material.YELLOW_DYE; + break; + case "replied": + mat = Material.CYAN_DYE; + break; + case "completed": + mat = Material.LIME_DYE; + break; + case "resolved": + mat = Material.LIME_DYE; + break; + case "closed": + mat = Material.GRAY_DYE; + break; + case "rejected": + mat = Material.RED_DYE; + break; + case "withdrawn": + mat = Material.GRAY_DYE; + break; + default: + mat = Material.ORANGE_DYE; + break; + } + ItemStack item = new ItemStack(mat); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.setDisplayName("§e#" + id + + " " + (title.isEmpty() + ? type : title)); + List lore = + new ArrayList<>(); + lore.add("§7类型: §f" + type); + lore.add("§7状态: " + + stxt(status)); + lore.add("§7提交者: §f" + + requester); + lore.add("§7奖励: §e" + reward + + " 积分"); + lore.add(""); + lore.add("§7点击查看详情"); + im.setLore(lore); + im.setCustomModelData(id); + item.setItemMeta(im); + } + return item; + } + + private int gt(ItemStack item) { + if (item == null) return -1; + ItemMeta im = item.getItemMeta(); + if (im == null) return -1; + if (im.hasCustomModelData()) + return im.getCustomModelData(); + return -1; + } + + private int gtFromDetail(Player p) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(10); + return gt(it); + } + + private String stxt(String status) { + switch (status) { + case "pending": + return "§e待处理"; + case "processing": + return "§b处理中"; + case "replied": + return "§9已回复"; + case "completed": + return "§a待确认"; + case "resolved": + return "§a已完结"; + case "closed": + return "§7已关闭"; + case "rejected": + return "§c已驳回"; + case "withdrawn": + return "§7已撤回"; + default: + return "§7" + status; + } + } + + private String sv(Map m, + String key) { + Object v = m.get(key); + return v != null + ? String.valueOf(v) : ""; + } + + private int nv(Map m, + String key) { + Object v = m.get(key); + return v instanceof Number + ? ((Number) v).intValue() : 0; + } + + private ItemStack mk(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) { + if (!l.isEmpty()) + list.add(l); + } + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 73d274a..32ad554 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,86 +1,89 @@ -name: Sdf1_login -version: 1.0 -main: Sdf1_login.Main -api-version: '1.21' -commands: - sdf1_login: - description: Sdf1_login主命令 - usage: /sdf1_login - reg: - description: 注册 - usage: /reg <密码> - login: - description: 登录 - usage: /login <密码> - l: - description: 登录快捷版 - usage: /l <密码> - 注册: - description: 注册(中文) - usage: /注册 <密码> - 登录: - description: 登录(中文) - usage: /登录 <密码> - 玩家信息: - description: 打开面板(中文) - usage: /玩家信息 - 改密码: - description: 修改密码(中文) - usage: /改密码 <旧密码> <新密码> <确认新密码> - 设置邮箱: - description: 设置邮箱(中文) - usage: /设置邮箱 <邮箱地址> - 撤销: - description: 撤销操作(中文) - usage: /撤销 - 删除: - description: 删除玩家(中文) - usage: /删除 <玩家> <密码> - 找回密码: - description: 找回密码(中文) - usage: /找回密码 - 签到: - description: 每日签到(中文) - usage: /签到 - oa: - description: 申请删除账号 - usage: /oa [玩家名] - 删除账号: - description: 申请删除账号(中文) - usage: /删除账号 [玩家名] - 通过: - description: 批准OA请求(中文) - usage: /通过 <编号> - 拒绝: - description: 拒绝OA请求(中文) - usage: /拒绝 <编号> [理由] - accept: - description: 批准OA请求 - usage: /accept <编号> - deny: - description: 拒绝OA请求 - usage: /deny <编号> [理由] - sdf1debug: - description: 调试指令 - usage: /sdf1debug <玩家> take.<阶段> - email: - description: 绑定邮箱 - usage: /email <邮箱> - 绑定: - description: 绑定邮箱(中文) - usage: /绑定 <邮箱> - 调试: - description: 调试模式 - usage: /调试 - 绑定邮箱: - description: 绑定邮箱 - usage: /绑定邮箱 - chat: - description: 聊天过滤管理 - usage: /chat - recycle: - description: 打开垃圾回收站 - usage: /recycle - 垃圾清理: - description: 手动清理掉落物 - usage: /垃圾清理 +name: Sdf1_login +version: 1.0 +main: Sdf1_login.Main +api-version: '1.21' +commands: + sdf1_login: + description: Sdf1_login主命令 + usage: /sdf1_login + reg: + description: 注册 + usage: /reg <密码> + login: + description: 登录 + usage: /login <密码> + l: + description: 登录快捷版 + usage: /l <密码> + 注册: + description: 注册(中文) + usage: /注册 <密码> + 登录: + description: 登录(中文) + usage: /登录 <密码> + 玩家信息: + description: 打开面板(中文) + usage: /玩家信息 + 改密码: + description: 修改密码(中文) + usage: /改密码 <旧密码> <新密码> <确认新密码> + 设置邮箱: + description: 设置邮箱(中文) + usage: /设置邮箱 <邮箱地址> + 撤销: + description: 撤销操作(中文) + usage: /撤销 + 删除: + description: 删除玩家(中文) + usage: /删除 <玩家> <密码> + 找回密码: + description: 找回密码(中文) + usage: /找回密码 + 签到: + description: 每日签到(中文) + usage: /签到 + oa: + description: 申请删除账号 + usage: /oa [玩家名] + 删除账号: + description: 申请删除账号(中文) + usage: /删除账号 [玩家名] + 通过: + description: 批准OA请求(中文) + usage: /通过 <编号> + 拒绝: + description: 拒绝OA请求(中文) + usage: /拒绝 <编号> [理由] + accept: + description: 批准OA请求 + usage: /accept <编号> + deny: + description: 拒绝OA请求 + usage: /deny <编号> [理由] + sdf1debug: + description: 调试指令 + usage: /sdf1debug <玩家> take.<阶段> + email: + description: 绑定邮箱 + usage: /email <邮箱> + 绑定: + description: 绑定邮箱(中文) + usage: /绑定 <邮箱> + 调试: + description: 调试模式 + usage: /调试 + 绑定邮箱: + description: 绑定邮箱 + usage: /绑定邮箱 + chat: + description: 聊天过滤管理 + usage: /chat + recycle: + description: 打开垃圾回收站 + usage: /recycle + 垃圾清理: + description: 手动清理掉落物 + usage: /垃圾清理 + menu: + description: 雪球菜单开关控制 + usage: /menu <开/关> \ No newline at end of file