diff --git a/.idea/.gitignore b/.idea/.gitignore index f6906f2..7f050be 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,10 +1,10 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# 已忽略包含查询文件的默认文件夹 -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml index aa00ffa..a156f52 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,7 +1,7 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index fdc35ea..62e7bdb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,14 +1,14 @@ - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..9661ac7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..f1b0660 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,48 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "21" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm:2026.1 diff --git a/src/main/java/Sdf1_login/AFKManager.java b/src/main/java/Sdf1_login/AFKManager.java index 2f5fdd6..f714c1b 100644 --- a/src/main/java/Sdf1_login/AFKManager.java +++ b/src/main/java/Sdf1_login/AFKManager.java @@ -1,197 +1,197 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitRunnable; - -import java.util.*; - -public class AFKManager { - - // 玩家最后活动时间(空闲才开始计时) - private final Map lastAction = - new HashMap<>(); - // 每个玩家当前的警告阶段 - private final Map lastStage = - new HashMap<>(); - // 是否已进入挂机检测(false=还在活动,不计时) - private final Map afkTracking = - new HashMap<>(); - - private final ConfigManager config; - private final JavaPlugin plugin; - - public AFKManager(ConfigManager config, - JavaPlugin plugin) { - this.config = config; - this.plugin = plugin; - } - - public void startCheck() { - new BukkitRunnable() { - public void run() { - tick(); - } - }.runTaskTimer(plugin, 20L, 20L); - } - - private void tick() { - if (!config.afkEnabled) return; - long now = System.currentTimeMillis(); - - for (Player p : Bukkit.getOnlinePlayers()) { - String name = p.getName(); - UUID uuid = p.getUniqueId(); - - if (!isLoggedIn(name)) continue; - if (config.getAfkWhitelist().contains(name)) - continue; - - // 确保有记录 - if (!lastAction.containsKey(uuid)) { - lastAction.put(uuid, now); - afkTracking.put(uuid, false); - lastStage.put(uuid, 0); - continue; - } - - long last = lastAction.get(uuid); - long idle = (now - last) / 1000; - int timeout = config.afkTimeout; - - // 还在活动范围内:不进入挂机检测 - if (idle < 60) { - afkTracking.put(uuid, false); - lastStage.put(uuid, 0); - continue; - } - - // 超过60秒无活动:开始挂机检测 - afkTracking.put(uuid, true); - long remain = timeout - idle; - - // 已超时:踢出 - if (remain <= 0) { - p.kickPlayer("§c§l你已挂机 " - + fmtSec(idle) + ",被踢出"); - lastAction.remove(uuid); - lastStage.remove(uuid); - afkTracking.remove(uuid); - continue; - } - - int stage = lastStage.getOrDefault(uuid, 0); - - // 阶段3:最后10秒 - if (remain <= 10) { - if (stage < 3) { - lastStage.put(uuid, 3); - } - p.sendTitle( - "§e§l挂机了 " + fmtSec(idle), - "§c还剩 " + remain + " 秒踢出", - 0, 22, 3); - playSound(p); - continue; - } - - // 阶段2:1分钟~10秒 - if (remain <= 60) { - if (stage < 2) { - lastStage.put(uuid, 2); - p.sendMessage( - "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" - + fmtSec(idle) - + "§f,还剩 §c" - + fmtSec(remain) - + "§f踢出"); - } - p.sendTitle( - "§e§l挂机了 " + fmtSec(idle), - "§c还剩 " + fmtSec(remain) + " 踢出", - 5, 25, 5); - playSound(p); - continue; - } - - // 阶段1:3分钟提醒 - if (remain <= 180 && stage < 1) { - lastStage.put(uuid, 1); - p.sendMessage( - "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" - + fmtSec(idle) - + "§f,还剩 §c" - + fmtSec(remain) - + "§f踢出"); - p.sendTitle( - "§e§l挂机了 " + fmtSec(idle), - "§f还剩 " + fmtSec(remain) + " 踢出", - 10, 60, 20); - for (int i = 0; i < 3; i++) { - final int delay = i * 10; - Bukkit.getScheduler().runTaskLater( - plugin, () -> { - if (p.isOnline()) playSound(p); - }, delay); - } - } - - if (remain > 180) { - lastStage.remove(uuid); - } - } - } - - /** - * 玩家有活动时调用(移动/交互/放置/挖掘等) - * 重置计时,退出挂机检测 - */ - public void recordAction(UUID uuid) { - lastAction.put(uuid, System.currentTimeMillis()); - afkTracking.put(uuid, false); - lastStage.put(uuid, 0); - } - - /** - * 玩家下线时调用 - * 彻底停止计时,清理所有数据 - */ - public void remove(UUID uuid) { - lastAction.remove(uuid); - lastStage.remove(uuid); - afkTracking.remove(uuid); - } - - /** - * 获取玩家当前是否在挂机检测中 - */ - public boolean isAfkTracking(UUID uuid) { - return afkTracking.getOrDefault(uuid, false); - } - - private boolean isLoggedIn(String name) { - return ((Main) plugin).getLoggedIn() - .contains(name); - } - - private void playSound(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 String fmtSec(long sec) { - if (sec < 60) return sec + "秒"; - long m = sec / 60; - long s = sec % 60; - return m + "分" + (s > 0 ? s + "秒" : ""); - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; + +public class AFKManager { + + // 玩家最后活动时间(空闲才开始计时) + private final Map lastAction = + new HashMap<>(); + // 每个玩家当前的警告阶段 + private final Map lastStage = + new HashMap<>(); + // 是否已进入挂机检测(false=还在活动,不计时) + private final Map afkTracking = + new HashMap<>(); + + private final ConfigManager config; + private final JavaPlugin plugin; + + public AFKManager(ConfigManager config, + JavaPlugin plugin) { + this.config = config; + this.plugin = plugin; + } + + public void startCheck() { + new BukkitRunnable() { + public void run() { + tick(); + } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void tick() { + if (!config.afkEnabled) return; + long now = System.currentTimeMillis(); + + for (Player p : Bukkit.getOnlinePlayers()) { + String name = p.getName(); + UUID uuid = p.getUniqueId(); + + if (!isLoggedIn(name)) continue; + if (config.getAfkWhitelist().contains(name)) + continue; + + // 确保有记录 + if (!lastAction.containsKey(uuid)) { + lastAction.put(uuid, now); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + long last = lastAction.get(uuid); + long idle = (now - last) / 1000; + int timeout = config.afkTimeout; + + // 还在活动范围内:不进入挂机检测 + if (idle < 60) { + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + // 超过60秒无活动:开始挂机检测 + afkTracking.put(uuid, true); + long remain = timeout - idle; + + // 已超时:踢出 + if (remain <= 0) { + p.kickPlayer("§c§l你已挂机 " + + fmtSec(idle) + ",被踢出"); + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + continue; + } + + int stage = lastStage.getOrDefault(uuid, 0); + + // 阶段3:最后10秒 + if (remain <= 10) { + if (stage < 3) { + lastStage.put(uuid, 3); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + remain + " 秒踢出", + 0, 22, 3); + playSound(p); + continue; + } + + // 阶段2:1分钟~10秒 + if (remain <= 60) { + if (stage < 2) { + lastStage.put(uuid, 2); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + fmtSec(remain) + " 踢出", + 5, 25, 5); + playSound(p); + continue; + } + + // 阶段1:3分钟提醒 + if (remain <= 180 && stage < 1) { + lastStage.put(uuid, 1); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§f还剩 " + fmtSec(remain) + " 踢出", + 10, 60, 20); + for (int i = 0; i < 3; i++) { + final int delay = i * 10; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + if (p.isOnline()) playSound(p); + }, delay); + } + } + + if (remain > 180) { + lastStage.remove(uuid); + } + } + } + + /** + * 玩家有活动时调用(移动/交互/放置/挖掘等) + * 重置计时,退出挂机检测 + */ + public void recordAction(UUID uuid) { + lastAction.put(uuid, System.currentTimeMillis()); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + } + + /** + * 玩家下线时调用 + * 彻底停止计时,清理所有数据 + */ + public void remove(UUID uuid) { + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + } + + /** + * 获取玩家当前是否在挂机检测中 + */ + public boolean isAfkTracking(UUID uuid) { + return afkTracking.getOrDefault(uuid, false); + } + + private boolean isLoggedIn(String name) { + return ((Main) plugin).getLoggedIn() + .contains(name); + } + + private void playSound(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 String fmtSec(long sec) { + if (sec < 60) return sec + "秒"; + long m = sec / 60; + long s = sec % 60; + return m + "分" + (s > 0 ? s + "秒" : ""); + } +} diff --git a/src/main/java/Sdf1_login/AccountRequestManager.java b/src/main/java/Sdf1_login/AccountRequestManager.java index bf54ac1..ae5eaad 100644 --- a/src/main/java/Sdf1_login/AccountRequestManager.java +++ b/src/main/java/Sdf1_login/AccountRequestManager.java @@ -1,146 +1,146 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class AccountRequestManager { - - public enum ApprovalMode { AUTO, MANUAL, HYBRID } - - public static class Request { - public final int id; - public final String requester; - public final String target; - public final String ip; - public final long time; - public String status; - public String denyReason; - - public Request(int id, String requester, String target, String ip) { - this.id = id; - this.requester = requester; - this.target = target; - this.ip = ip; - this.time = System.currentTimeMillis(); - this.status = "PENDING"; - this.denyReason = ""; - } - } - - private final Main plugin; - private final Map requests = new ConcurrentHashMap<>(); - private final Map pendingByPlayer = new ConcurrentHashMap<>(); - private int nextId = 1; - private ApprovalMode mode = ApprovalMode.MANUAL; - private int autoDelayMinutes = 60; - - public AccountRequestManager(Main plugin) { - this.plugin = plugin; - } - - public ApprovalMode getMode() { return mode; } - - public void setMode(String m) { - try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); } - catch (Exception e) { this.mode = ApprovalMode.MANUAL; } - } - - public void setAutoDelayMinutes(int min) { - this.autoDelayMinutes = min; - } - - public Request createRequest(String requester, String target, String ip) { - int id = nextId++; - Request req = new Request(id, requester, target, ip); - requests.put(id, req); - pendingByPlayer.put(requester, id); - - if (mode == ApprovalMode.AUTO) { - scheduleAutoApprove(id); - } else if (mode == ApprovalMode.HYBRID) { - String targetIp = (String) plugin.getDb().getField(target, "ip_address"); - if (ip != null && ip.equals(targetIp)) { - scheduleAutoApprove(id); - } - } - return req; - } - - private void scheduleAutoApprove(int id) { - long delayTicks = (long) autoDelayMinutes * 60L * 20L; - Request req = requests.get(id); - if (req == null) return; - String requesterName = req.requester; - String targetName = req.target; - plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过"); - - Bukkit.getScheduler().runTaskLater(plugin, () -> { - Request r = requests.get(id); - if (r != null && "PENDING".equals(r.status)) { - approveRequest(id); - plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过"); - Player requester = Bukkit.getPlayer(requesterName); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!"); - } - } - }, delayTicks); - } - - public boolean approveRequest(int id) { - Request req = requests.get(id); - if (req == null || !"PENDING".equals(req.status)) return false; - req.status = "APPROVED"; - plugin.getDb().deleteUser(req.target); - pendingByPlayer.remove(req.requester); - // 清背包+传送出生点+踢出 - Player target = Bukkit.getPlayer(req.target); - if (target != null && target.isOnline()) { - target.getInventory().clear(); - target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]); - target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]); - target.teleport(target.getWorld().getSpawnLocation()); - target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册"); - } - return true; - } - - - public boolean denyRequest(int id, String reason) { - Request req = requests.get(id); - if (req == null || !"PENDING".equals(req.status)) return false; - req.status = "DENIED"; - req.denyReason = reason; - pendingByPlayer.remove(req.requester); - return true; - } - - public List getPendingRequests() { - List list = new ArrayList<>(); - for (Request r : requests.values()) { - if ("PENDING".equals(r.status)) list.add(r); - } - return list; - } - - public Request getRequest(int id) { - return requests.get(id); - } - - public boolean hasPending(String player) { - return pendingByPlayer.containsKey(player); - } - - public void cancelPending(String player) { - Integer id = pendingByPlayer.remove(player); - if (id != null) { - Request r = requests.get(id); - if (r != null) r.status = "CANCELLED"; - } - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AccountRequestManager { + + public enum ApprovalMode { AUTO, MANUAL, HYBRID } + + public static class Request { + public final int id; + public final String requester; + public final String target; + public final String ip; + public final long time; + public String status; + public String denyReason; + + public Request(int id, String requester, String target, String ip) { + this.id = id; + this.requester = requester; + this.target = target; + this.ip = ip; + this.time = System.currentTimeMillis(); + this.status = "PENDING"; + this.denyReason = ""; + } + } + + private final Main plugin; + private final Map requests = new ConcurrentHashMap<>(); + private final Map pendingByPlayer = new ConcurrentHashMap<>(); + private int nextId = 1; + private ApprovalMode mode = ApprovalMode.MANUAL; + private int autoDelayMinutes = 60; + + public AccountRequestManager(Main plugin) { + this.plugin = plugin; + } + + public ApprovalMode getMode() { return mode; } + + public void setMode(String m) { + try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); } + catch (Exception e) { this.mode = ApprovalMode.MANUAL; } + } + + public void setAutoDelayMinutes(int min) { + this.autoDelayMinutes = min; + } + + public Request createRequest(String requester, String target, String ip) { + int id = nextId++; + Request req = new Request(id, requester, target, ip); + requests.put(id, req); + pendingByPlayer.put(requester, id); + + if (mode == ApprovalMode.AUTO) { + scheduleAutoApprove(id); + } else if (mode == ApprovalMode.HYBRID) { + String targetIp = (String) plugin.getDb().getField(target, "ip_address"); + if (ip != null && ip.equals(targetIp)) { + scheduleAutoApprove(id); + } + } + return req; + } + + private void scheduleAutoApprove(int id) { + long delayTicks = (long) autoDelayMinutes * 60L * 20L; + Request req = requests.get(id); + if (req == null) return; + String requesterName = req.requester; + String targetName = req.target; + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过"); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Request r = requests.get(id); + if (r != null && "PENDING".equals(r.status)) { + approveRequest(id); + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过"); + Player requester = Bukkit.getPlayer(requesterName); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!"); + } + } + }, delayTicks); + } + + public boolean approveRequest(int id) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "APPROVED"; + plugin.getDb().deleteUser(req.target); + pendingByPlayer.remove(req.requester); + // 清背包+传送出生点+踢出 + Player target = Bukkit.getPlayer(req.target); + if (target != null && target.isOnline()) { + target.getInventory().clear(); + target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]); + target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]); + target.teleport(target.getWorld().getSpawnLocation()); + target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册"); + } + return true; + } + + + public boolean denyRequest(int id, String reason) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "DENIED"; + req.denyReason = reason; + pendingByPlayer.remove(req.requester); + return true; + } + + public List getPendingRequests() { + List list = new ArrayList<>(); + for (Request r : requests.values()) { + if ("PENDING".equals(r.status)) list.add(r); + } + return list; + } + + public Request getRequest(int id) { + return requests.get(id); + } + + public boolean hasPending(String player) { + return pendingByPlayer.containsKey(player); + } + + public void cancelPending(String player) { + Integer id = pendingByPlayer.remove(player); + if (id != null) { + Request r = requests.get(id); + if (r != null) r.status = "CANCELLED"; + } + } +} diff --git a/src/main/java/Sdf1_login/BondManager.java b/src/main/java/Sdf1_login/BondManager.java index cb8ec93..bba0344 100644 --- a/src/main/java/Sdf1_login/BondManager.java +++ b/src/main/java/Sdf1_login/BondManager.java @@ -182,6 +182,23 @@ public class BondManager { } catch (SQLException e) { return 0; } } + /** 该玩家是否在 bond 表中有真实记录 */ + public boolean hasBondAccount(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM bonds " + + "WHERE player_name=? " + + "AND amount > 0"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + boolean exists = rs.next(); + rs.close(); ps.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + // ======================== 增加债券 ======================== public int addBonds(String player, int amount) { @@ -272,6 +289,53 @@ public class BondManager { int balanceBefore, int balanceAfter) { try { + // 合并模式:shop_buy 30秒内同商品合并 + if ("shop_buy".equals(type) + && reason != null + && !reason.isEmpty()) { + long mergeWindow = + System.currentTimeMillis() - 30000L; + PreparedStatement chk = + db.prepareStatement( + "SELECT id, amount " + + "FROM bond_transaction " + + "WHERE player_name=? " + + "AND type=? " + + "AND reason=? " + + "AND time > ? " + + "ORDER BY time DESC " + + "LIMIT 1"); + chk.setString(1, player); + chk.setString(2, type); + chk.setString(3, reason); + chk.setLong(4, mergeWindow); + ResultSet rs = chk.executeQuery(); + if (rs.next()) { + int existId = rs.getInt("id"); + int existAmt = rs.getInt("amount"); + rs.close(); + chk.close(); + PreparedStatement upd = + db.prepareStatement( + "UPDATE bond_transaction " + + "SET amount=?, " + + "balance_after=?, " + + "time=? " + + "WHERE id=?"); + upd.setInt(1, existAmt + amount); + upd.setInt(2, balanceAfter); + upd.setLong(3, + System.currentTimeMillis()); + upd.setInt(4, existId); + upd.executeUpdate(); + upd.close(); + return; + } + rs.close(); + chk.close(); + } + + // 原始插入 PreparedStatement ps = db.prepareStatement( "INSERT INTO bond_transaction " + "(player_name, type, amount," @@ -283,16 +347,14 @@ public class BondManager { ps.setString(2, type); ps.setInt(3, amount); ps.setString(4, - targetPlayer != null - ? targetPlayer : ""); + targetPlayer != null ? targetPlayer : ""); ps.setString(5, operator != null ? operator : ""); ps.setString(6, reason != null ? reason : ""); ps.setInt(7, balanceBefore); ps.setInt(8, balanceAfter); - ps.setLong(9, - System.currentTimeMillis()); + ps.setLong(9, System.currentTimeMillis()); ps.executeUpdate(); ps.close(); } catch (SQLException e) { @@ -314,6 +376,47 @@ public class BondManager { type, targetPlayer, operator, reason); } + public boolean checkoutCart(String player, + String[] itemNames, + int[] subtotals) { + int total = 0; + for (int s : subtotals) total += s; + if (isFrozen(player)) return false; + + int before = getBonds(player); + if (before < total) return false; + int after = before - total; + + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,?,'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, after); + ps.setInt(3, after); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + + int running = before; + for (int i = 0; i < itemNames.length; i++) { + int next = running - subtotals[i]; + logTransaction(player, "shop_buy", subtotals[i], + "", "商店系统", + "购物车购买-" + itemNames[i], + running, next); + running = next; + } + + logLegacy(player, -total); + return true; + } public boolean setBonds(String player, int amount) { ensureAccount(player); diff --git a/src/main/java/Sdf1_login/BondPrinter.java b/src/main/java/Sdf1_login/BondPrinter.java index a430ede..674ec3c 100644 --- a/src/main/java/Sdf1_login/BondPrinter.java +++ b/src/main/java/Sdf1_login/BondPrinter.java @@ -9,12 +9,7 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; public class BondPrinter { @@ -38,9 +33,11 @@ public class BondPrinter { TYPE_NAMES.put("transfer_in", "转入"); TYPE_NAMES.put("redeem", "CDK兑换"); TYPE_NAMES.put("shop_buy", "商城购买"); + TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上 TYPE_NAMES.put("daily_sign", "签到奖励"); TYPE_NAMES.put("freeze", "冻结"); TYPE_NAMES.put("unfreeze", "解冻"); + } public BondPrinter(Main plugin) { @@ -151,8 +148,7 @@ public class BondPrinter { .getAllPlayerNames(); if (players.isEmpty()) { if (sender != null) { - sender.sendMessage( - "§c没有流水数据"); + sender.sendMessage("§c没有流水数据"); } return; } @@ -162,59 +158,84 @@ public class BondPrinter { + ".xls"; File file = new File(outputDir, fileName); + // ===== 收集所有流水 ===== + // key = 时间戳+玩家+金额,用于去重 + // value = 一行数据 + List allRows = new ArrayList<>(); + Set seenKeys = new HashSet<>(); + + for (String player : players) { + List> txs = + plugin.getBonds() + .getAllTransactions(player); + for (Map tx : txs) { + int txId = ((Number) tx.get("id")) + .intValue(); + String dedupKey = + String.valueOf(txId); + if (!seenKeys.add(dedupKey)) { + continue; + } + + String timeStr = txTime(tx); + long timeMs = 0; + Object t = tx.get("time"); + if (t != null) { + timeMs = + ((Number) t).longValue(); + } + + allRows.add(new String[]{ + player, + timeStr, + txTypeName(tx), + String.valueOf( + txInt(tx, "amount")), + txStr(tx, "target_player"), + txStr(tx, "operator"), + txStr(tx, "reason"), + String.valueOf( + txInt(tx, + "balance_before")), + String.valueOf( + txInt(tx, + "balance_after")), + // 额外存时间戳用于排序 + String.valueOf(timeMs) + }); + } + } + + // ===== 按时间戳排序 ===== + allRows.sort((a, b) -> { + long ta = Long.parseLong( + a[a.length - 1]); + long tb = Long.parseLong( + b[b.length - 1]); + return Long.compare(ta, tb); + }); + + // ===== 写入文件 ===== PrintWriter pw = new PrintWriter( new OutputStreamWriter( new FileOutputStream(file), StandardCharsets.UTF_8)); try { writeXmlHeader(pw, "全服流水"); - writeXmlRow(pw, - new String[]{ - "玩家", "时间", - "类型", "金额", - "对象", "操作者", - "理由", - "操作前余额", - "操作后余额"}); + writeXmlRow(pw, new String[]{ + "玩家", "时间", "类型", + "金额", "对象", "操作者", + "理由", "操作前余额", + "操作后余额" + }); - Set written = new HashSet<>(); - int totalRows = 0; - - for (String player : players) { - List> txs = - plugin.getBonds() - .getAllTransactions( - player); - for (Map tx : txs) { - int txId = ((Number) tx - .get("id")) - .intValue(); - if (!written.add( - String.valueOf( - txId))) { - continue; - } - - writeXmlRow(pw, new String[]{ - player, - txTime(tx), - txTypeName(tx), - String.valueOf( - txInt(tx, - "amount")), - txStr(tx, - "target_player"), - txStr(tx, "operator"), - txStr(tx, "reason"), - String.valueOf( - txInt(tx, - "balance_before")), - String.valueOf( - txInt(tx, - "balance_after")) - }); - totalRows++; - } + for (String[] row : allRows) { + // 去掉最后的时间戳列 + String[] cells = + new String[row.length - 1]; + System.arraycopy(row, 0, + cells, 0, cells.length); + writeXmlRow(pw, cells); } writeXmlFooter(pw); @@ -222,14 +243,15 @@ public class BondPrinter { if (sender != null) { sender.sendMessage( - "§a[打印] §f全服流水" - + " §a已导出"); + "§a[打印] §f全服流水 §a已导出"); sender.sendMessage("§7文件: §f" + fileName); sender.sendMessage("§7共 §f" + players.size() + " 名玩家,§f" - + totalRows + " §7条"); + + allRows.size() + " §7条"); + sender.sendMessage( + "§7按时间从早到晚排序"); sender.sendMessage( "§724小时后自动删除"); } diff --git a/src/main/java/Sdf1_login/CDKManager.java b/src/main/java/Sdf1_login/CDKManager.java index 595bef2..e07569f 100644 --- a/src/main/java/Sdf1_login/CDKManager.java +++ b/src/main/java/Sdf1_login/CDKManager.java @@ -2,13 +2,13 @@ package Sdf1_login; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.scoreboard.Scoreboard; import java.io.*; import java.sql.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.nio.charset.StandardCharsets; + public class CDKManager { private final Main plugin; @@ -41,198 +41,18 @@ public class CDKManager { st.close(); } catch (Exception e) { throw new RuntimeException( - "[CDK] DB init failed: " + e.getMessage(), e); + "[CDK] DB init failed: " + + e.getMessage(), e); } } - public void loadCDKsFromDir() { - File dir = new File( - plugin.getDataFolder(), "cdk"); - if (!dir.exists()) { - dir.mkdirs(); - plugin.getLogger().info( - "[CDK] 已创建 cdk/ 目录"); - return; - } - File[] files = dir.listFiles( - (d, n) -> n.endsWith(".txt")); - if (files == null || files.length == 0) { - plugin.getLogger().warning( - "[CDK] cdk/ 目录下" - + "无 .txt 文件"); - return; - } - int count = 0; - for (File f : files) { - int loaded = loadCDKFile(f); - plugin.getLogger().info( - "[CDK] 文件: " + f.getName() - + " 加载 " + loaded + " 条"); - count += loaded; - } - plugin.getLogger().info( - "[CDK] 共导入 " + count + " 条"); - } + // ===== CDK兑换(供SDF1调用) ===== - /** 单文件加载,返回成功数 */ - private int loadCDKFile(File f) { - int count = 0; - try { - BufferedReader br = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8)); - String line; - int lineNum = 0; - while ((line = br.readLine()) != null) { - lineNum++; - line = line.trim(); - if (line.isEmpty()) continue; - if (line.startsWith("#")) continue; - if (line.startsWith("//")) continue; - - // 支持格式: - // CODE 100 - // CODE,100 - // CODE 100 bond - // CODE,100,bond - String[] parts = line - .split("[,\\s]+"); - if (parts.length < 2) { - plugin.getLogger().warning( - "[CDK] " + f.getName() - + " L" + lineNum - + " 格式错误: " - + line); - continue; - } - - String code = parts[0].trim(); - if (code.isEmpty()) continue; - - int amount; - try { - amount = Integer.parseInt( - parts[1].trim()); - } catch (NumberFormatException e) { - plugin.getLogger().warning( - "[CDK] " + f.getName() - + " L" + lineNum - + " 金额格式错误: " - + parts[1]); - continue; - } - - String type = parts.length >= 3 - ? parts[2].trim() : "bond"; - - boolean ok = addCDK(code, amount, - type); - if (ok) { - count++; - } else { - plugin.getLogger().warning( - "[CDK] " + f.getName() - + " L" + lineNum - + " 重复跳过: " - + code); - } - } - br.close(); - } catch (Exception e) { - plugin.getLogger().warning( - "[CDK] 读取失败: " + f.getName() - + " " + e.getMessage()); - } - return count; - } - - - public int importFromFile(String fileName) { - File dir = new File( - plugin.getDataFolder(), "cdk"); - if (!dir.exists()) { - dir.mkdirs(); - } - - // 先在 cdk/ 目录下找 - File f = new File(dir, fileName); - if (!f.exists()) { - // 再在插件根目录找 - f = new File( - plugin.getDataFolder(), fileName); - } - if (!f.exists()) { - plugin.getLogger().warning( - "[CDK] 文件不存在: " - + fileName); - return -1; - } - - plugin.getLogger().info( - "[CDK] 开始导入: " - + f.getAbsolutePath() - + " 大小:" + f.length() - + " bytes"); - - int count = loadCDKFile(f); - plugin.getLogger().info( - "[CDK] 导入完成: " + count + " 条"); - return count; - } - - public int importFromSdf1(String fileName) { - int count = 0; - Scoreboard board = Bukkit.getScoreboardManager() - .getMainScoreboard(); - // 尝试从sdf1的计分板读取口令 - // 这部分依赖sdf1插件的配置 - return count; - } - - - private boolean addCDK(String code, int amount, - String type) { - try { - // 先检查是否已存在 - PreparedStatement check = - db.prepareStatement( - "SELECT code FROM cdk " - + "WHERE code=?"); - check.setString(1, code); - ResultSet rs = check.executeQuery(); - if (rs.next()) { - rs.close(); - check.close(); - return false; - } - rs.close(); - check.close(); - - PreparedStatement ps = - db.prepareStatement( - "INSERT INTO cdk " - + "(code, amount, type," - + " created_time)" - + " VALUES (?,?,?,?)"); - ps.setString(1, code); - ps.setInt(2, amount); - ps.setString(3, - type != null ? type : "bond"); - ps.setLong(4, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - return true; - } catch (SQLException e) { - plugin.getLogger().warning( - "[CDK] addCDK失败: " + code - + " " + e.getMessage()); - return false; - } - } - - // ===== 替换整个 redeem 方法 ===== + /** + * 兑换CDK,返回结果字符串 + * @return "success:金额:余额前:余额后" + * 或 "fail:原因" + */ public String redeem(String code, String player) { try { PreparedStatement ps = db.prepareStatement( @@ -242,45 +62,54 @@ public class CDKManager { ResultSet rs = ps.executeQuery(); if (!rs.next()) { rs.close(); ps.close(); - return null; + return "fail:not_found"; } int amount = rs.getInt("amount"); String type = rs.getString("type"); int used = rs.getInt("used"); rs.close(); ps.close(); - if (used == 1) return "该兑换码已被使用"; + if (used == 1) + return "fail:already_used"; - // [新增] 冻结检查 - if (plugin.getBonds().isFrozen(player)) { - return "账户已被冻结,无法兑换"; - } + if (plugin.getBonds() + .isFrozen(player)) + return "fail:frozen"; PreparedStatement up = db.prepareStatement( "UPDATE cdk SET used=1," + "used_by=?,used_time=? " + "WHERE code=?"); up.setString(1, player); - up.setLong(2, System.currentTimeMillis()); + up.setLong(2, + System.currentTimeMillis()); up.setString(3, code); up.executeUpdate(); up.close(); if ("bond".equals(type)) { - // [修改] 使用带流水的重载方法 - plugin.getBonds().addBonds(player, amount, - BondManager.TX_REDEEM, "", - "system", "CDK兑换: " + code); - return "兑换成功!获得 " + amount + " 债券"; + int bef = plugin.getBonds() + .getBonds(player); + plugin.getBonds().addBonds(player, + amount, + BondManager.TX_REDEEM, + "", "CDK系统", + "CDK兑换: " + code); + int aft = plugin.getBonds() + .getBonds(player); + return "success:" + amount + + ":" + bef + ":" + aft; } - return null; + return "fail:unknown_type"; } catch (SQLException e) { - return "兑换失败"; + return "fail:" + e.getMessage(); } } - // ===== 聊天输入系统(主插件转发调用)===== + // ===== 聊天输入系统 ===== - public void requestInput(Player p, String type, String context) { + public void requestInput(Player p, + String type, + String context) { CDKInput input = new CDKInput(); input.type = type; input.context = context; @@ -289,129 +118,133 @@ public class CDKManager { } public boolean isListening(String playerName) { - return pendingInputs.containsKey(playerName); + return pendingInputs + .containsKey(playerName); } public void onChat(Player p, String msg) { - CDKInput input = pendingInputs.remove(p.getName()); + CDKInput input = pendingInputs + .remove(p.getName()); if (input == null) return; - if (System.currentTimeMillis() - input.time > 60000) { - p.sendMessage("§c输入超时,请重新操作"); + if (System.currentTimeMillis() + - input.time > 60000) { + p.sendMessage("§c输入超时"); return; } if ("cdk".equals(input.type)) { - String result = redeem(msg.trim(), p.getName()); - p.sendMessage(result != null - ? "§e§l[CDK] §f" + result - : "§c§l[CDK] §f无效的兑换码"); + String result = redeem( + msg.trim(), p.getName()); + if (result != null + && result.startsWith("success:")) { + String[] parts = result.split(":"); + int amt = Integer.parseInt(parts[1]); + int bef = Integer.parseInt(parts[2]); + int aft = Integer.parseInt(parts[3]); + p.sendMessage("§e§l[CDK] §f兑换成功!" + + "获得 §c" + amt + "§f债券"); + p.sendMessage("§7余额: §e" + bef + + " §7-> §a" + aft); + } else { + p.sendMessage("§c§l[CDK] §f" + + (result != null + ? result.replace("fail:", "") + : "无效的兑换码")); + } return; } if ("econ".equals(input.type)) { - handleEconInput(p, msg.trim(), input.context); + handleEconInput(p, msg.trim(), + input.context); return; } if ("bond".equals(input.type)) { - handleBondInput(p, msg.trim(), input.context); + handleBondInput(p, msg.trim(), + input.context); } } - private void handleEconInput(Player admin, String msg, String target) { - net.milkbowl.vault.economy.Economy econ = null; + private void handleEconInput(Player admin, + String msg, + String target) { + net.milkbowl.vault.economy.Economy econ + = null; try { - var reg = plugin.getServer().getServicesManager() - .getRegistration(net.milkbowl.vault.economy.Economy.class); - if (reg != null) econ = reg.getProvider(); + var reg = plugin.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy + .Economy.class); + if (reg != null) + econ = reg.getProvider(); } catch (Exception ignored) {} if (econ == null) { admin.sendMessage("§c经济系统不可用"); return; } - if (!msg.startsWith("+") && !msg.startsWith("-")) { - admin.sendMessage("§c格式: §a+100 §7给钱 / §c-50 §7扣钱"); + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); return; } try { - String numStr = msg.substring(1).trim(); - double amount = Double.parseDouble(numStr); + String numStr = + msg.substring(1).trim(); + double amount = + Double.parseDouble(numStr); if (amount <= 0) { - admin.sendMessage("§c金额必须大于0"); + admin.sendMessage("§c金额必须>0"); return; } if (msg.startsWith("-")) { - Player tp = plugin.getServer().getPlayerExact(target); + Player tp = plugin.getServer() + .getPlayerExact(target); if (tp == null) { - admin.sendMessage("§c" + target + " 不在线"); + admin.sendMessage("§c" + target + + " 不在线"); return; } if (!econ.has(tp, amount)) { - admin.sendMessage("§c" + target + " 余额不足"); + admin.sendMessage("§c余额不足"); return; } - double oldBal = econ.getBalance(tp); econ.withdrawPlayer(tp, amount); - double newBal = econ.getBalance(tp); - admin.sendMessage("§7============== §c余额扣款 §7=============="); - admin.sendMessage("§7目标: §e" + target); - admin.sendMessage("§c扣款: §e-$" + String.format("%.2f", amount)); - admin.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §c" + String.format("%.2f", newBal)); - admin.sendMessage("§7============== §c余额扣款 §7=============="); - tp.sendMessage("§7============== §c余额扣款 §7=============="); - tp.sendMessage("§c扣款: §e-$" + String.format("%.2f", amount)); - tp.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §c" + String.format("%.2f", newBal)); - tp.sendMessage("§7操作者: §e" + admin.getName()); - tp.sendMessage("§7============== §c余额扣款 §7=============="); + admin.sendMessage("§a扣款成功"); } else { - double oldBal = econ.getBalance(target); econ.depositPlayer(target, amount); - double newBal = econ.getBalance(target); - Player tp = plugin.getServer().getPlayerExact(target); - admin.sendMessage("§7============== §6余额转账 §7=============="); - admin.sendMessage("§7目标: §e" + target); - admin.sendMessage("§a到账: §e$" + String.format("%.2f", amount)); - admin.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §a" + String.format("%.2f", newBal)); - admin.sendMessage("§7============== §6余额转账 §7=============="); - if (tp != null) { - tp.sendMessage("§7============== §a收到转账 §7=============="); - tp.sendMessage("§a到账: §e$" + String.format("%.2f", amount)); - tp.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §a" + String.format("%.2f", newBal)); - tp.sendMessage("§7来自: §e" + admin.getName()); - tp.sendMessage("§7============== §a收到转账 §7=============="); - } + admin.sendMessage("§a到账成功"); } } catch (NumberFormatException e) { - admin.sendMessage("§c格式错误,请输入 §a+数字 §7或 §c-数字"); + admin.sendMessage("§c格式错误"); } } - // ===== 替换整个 handleBondInput 方法 ===== + private void handleBondInput(Player admin, String msg, String target) { if (!msg.startsWith("+") && !msg.startsWith("-")) { - admin.sendMessage( - "§c格式: §a+100 §7给债券" - + " / §c-50 §7扣债券"); - admin.sendMessage( - "§7可附加理由: §a+100 活动奖励"); + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); return; } try { String sign = msg.substring(0, 1); - String rest = msg.substring(1).trim(); - - // 拆分金额和理由 - String[] parts = rest.split("\\s+", 2); + String rest = + msg.substring(1).trim(); + String[] parts = + rest.split("\\s+", 2); int amount = Integer.parseInt(parts[0]); String reason = parts.length >= 2 ? parts[1].trim() : "管理员手动调整"; - if (amount <= 0) { - admin.sendMessage("§c金额必须大于0"); + admin.sendMessage("§c金额必须>0"); return; } String opName = admin.getName(); @@ -422,123 +255,32 @@ public class CDKManager { if ("-".equals(sign)) { if (plugin.getBonds() .isFrozen(target)) { - admin.sendMessage("§c" + target - + " 账户已被冻结"); + admin.sendMessage("§c账户冻结"); return; } - int oldBal = plugin.getBonds() - .getBonds(target); if (plugin.getBonds().deductBonds( target, amount, txType, "", opName, reason)) { - int newBal = plugin.getBonds() - .getBonds(target); - admin.sendMessage( - "§7============== §c" - + "债券扣款 §7" - + "============="); - admin.sendMessage( - "§7目标: §e" + target); - admin.sendMessage("§c扣款: §c-" - + amount + " §6枚"); - admin.sendMessage("§7理由: §f" - + reason); - admin.sendMessage("§7债券: §e" - + oldBal + " → §c" - + newBal); - admin.sendMessage( - "§7============== §c" - + "债券扣款 §7" - + "============="); - Player tp = plugin.getServer() - .getPlayerExact(target); - if (tp != null) { - tp.sendMessage( - "§7============== §c" - + "债券扣款 §7" - + "============="); - tp.sendMessage("§c扣款: §c-" - + amount + " §6枚"); - tp.sendMessage("§7理由: §f" - + reason); - tp.sendMessage("§7债券: §e" - + oldBal + " → §c" - + newBal); - tp.sendMessage("§7操作者: §e" - + opName); - tp.sendMessage( - "§7============== §c" - + "债券扣款 §7" - + "============="); - } + admin.sendMessage("§a扣除成功"); } else { - admin.sendMessage("§c" + target - + " 债券不足(当前: " - + oldBal + ")"); + admin.sendMessage("§c债券不足"); } } else { if (plugin.getBonds() .isFrozen(target)) { - admin.sendMessage("§c" + target - + " 账户已被冻结"); + admin.sendMessage("§c账户冻结"); return; } - int oldBal = plugin.getBonds() - .getBonds(target); plugin.getBonds().addBonds(target, amount, txType, "", opName, reason); - int newBal = plugin.getBonds() - .getBonds(target); - admin.sendMessage( - "§7============== §6" - + "债券转账 §7" - + "============="); - admin.sendMessage( - "§7目标: §e" + target); - admin.sendMessage("§a到账: §a+" - + amount + " §6枚"); - admin.sendMessage("§7理由: §f" - + reason); - admin.sendMessage("§7债券: §e" - + oldBal + " → §a" - + newBal); - admin.sendMessage( - "§7============== §6" - + "债券转账 §7" - + "============="); - Player tp = plugin.getServer() - .getPlayerExact(target); - if (tp != null) { - tp.sendMessage( - "§7============== §a" - + "收到债券 §7" - + "============="); - tp.sendMessage("§a到账: §a+" - + amount + " §6枚"); - tp.sendMessage("§7理由: §f" - + reason); - tp.sendMessage("§7债券: §e" - + oldBal + " → §a" - + newBal); - tp.sendMessage("§7来自: §e" - + opName); - tp.sendMessage( - "§7============== §a" - + "收到债券 §7" - + "============="); - } + admin.sendMessage("§a发放成功"); } } catch (NumberFormatException e) { - admin.sendMessage("§c格式错误,请输入 " - + "§a+数字 §7或 §c-数字"); + admin.sendMessage("§c格式错误"); } } - - - - public void cancelInput(String player) { pendingInputs.remove(player); } diff --git a/src/main/java/Sdf1_login/CheckInManager.java b/src/main/java/Sdf1_login/CheckInManager.java index e9e3d79..b594b0f 100644 --- a/src/main/java/Sdf1_login/CheckInManager.java +++ b/src/main/java/Sdf1_login/CheckInManager.java @@ -26,6 +26,7 @@ public class CheckInManager { return today.equals(last); } + // ===== 签到 ===== // ===== 签到 ===== public String checkIn(Player p) { String name = p.getName(); @@ -53,7 +54,8 @@ public class CheckInManager { java.util.Date todayD = new SimpleDateFormat("yyyy-MM-dd") .parse(today); - long diff = todayD.getTime() - lastD.getTime(); + long diff = todayD.getTime() + - lastD.getTime(); if (diff > 86400000L * 2) { streak = 0; } else if (diff > 86400000L) { @@ -71,35 +73,49 @@ public class CheckInManager { else if (streak >= 14) multi = 1.3; else if (streak >= 7) multi = 1.1; - int basePoints = plugin.getConfig2().checkinPoints; + int basePoints = + plugin.getConfig2().checkinPoints; int points = (int) (basePoints * multi); plugin.getDb().addPoints(name, points); - plugin.getDb().setField(name, "checkin_streak", streak); - plugin.getDb().setField(name, "last_checkin_date", today); + plugin.getDb().setField(name, + "checkin_streak", streak); + plugin.getDb().setField(name, + "last_checkin_date", today); Object totalObj = plugin.getDb() .getField(name, "total_checkin_days"); int total = totalObj != null ? ((Number) totalObj).intValue() : 0; - plugin.getDb().setField(name, "total_checkin_days", - total + 1); + plugin.getDb().setField(name, + "total_checkin_days", total + 1); - int bondReward = 1; - if (streak >= 21) bondReward = 3; - else if (streak >= 14) bondReward = 2; - else if (streak >= 7) bondReward = 2; - plugin.getBonds().addBonds(name, bondReward); + // ★ 债券盲盒奖励(6参数版,写入流水)★ + int bondMin = + plugin.getConfig2().checkinBondMin; + int bondMax = + plugin.getConfig2().checkinBondMax; + if (bondMin <= 0) bondMin = 1; + if (bondMax < bondMin) bondMax = bondMin; + int bondReward = bondMin + + new java.util.Random() + .nextInt(bondMax - bondMin + 1); + plugin.getBonds().addBonds(name, bondReward, + "daily_sign", "", "签到系统", + "每日签到连续" + streak + "天"); - return plugin.getConfig2().msg("checkin_success") - .replace("{points}", String.valueOf(points)) - .replace("{streak}", String.valueOf(streak)) - .replace("{multi}", String.valueOf(multi)) + return plugin.getConfig2() + .msg("checkin_success") + .replace("{points}", + String.valueOf(points)) + .replace("{streak}", + String.valueOf(streak)) + .replace("{multi}", + String.valueOf(multi)) + " §e+" + bondReward + "债券"; } - // ===== 获取连续天数 ===== public int getStreak(String name) { Object val = plugin.getDb() @@ -111,7 +127,16 @@ public class CheckInManager { } return 0; } - + // 标准化日期 + private String normalizeDate(String input) { + String s = input.trim().replace("/", "-"); + if (s.matches("\\d{8}")) { + return s.substring(0, 4) + "-" + + s.substring(4, 6) + "-" + + s.substring(6, 8); + } + return s; + } // ===== 补签入口 ===== public String backCheckIn( Player p, String dateStr) { @@ -130,14 +155,12 @@ public class CheckInManager { } catch (Exception ex) { return "§c无效日期: " + dateStr; } - String reformat = - sdf.format(target); + String reformat = sdf.format(target); if (!reformat.equals(normalized)) { return "§c无效日期: " + dateStr; } String today = sdf.format(new Date()); - if (normalized.compareTo(today) >= 0) { return "§c今天不需要补签"; } @@ -146,19 +169,16 @@ public class CheckInManager { if (normalized.compareTo( threeDaysAgo) < 0) { return "§c只能补签3天内" - + "(最多到 " - + threeDaysAgo + ")"; + + "(最多到 " + threeDaysAgo + ")"; } Object val = plugin.getDb() - .getField(name, - "backcheck_dates"); + .getField(name, "backcheck_dates"); String existing = val != null ? val.toString() : ""; boolean alreadyBacked = false; if (!existing.isEmpty()) { - for (String d - : existing.split(",")) { + for (String d : existing.split(",")) { if (normalizeDate(d.trim()) .equals(normalized)) { alreadyBacked = true; @@ -167,71 +187,94 @@ public class CheckInManager { } } if (alreadyBacked) { - return "§c该日期已补签: " - + normalized; + return "§c该日期已补签: " + normalized; } Object normalCheck = plugin.getDb().getField( - name, - "last_checkin_date"); - String lastCheckin = normalCheck - != null - ? normalCheck.toString() - : ""; + name, "last_checkin_date"); + String lastCheckin = normalCheck != null + ? normalCheck.toString() : ""; if (lastCheckin.equals(normalized)) { - return "§c该日期已签到: " - + normalized; + return "§c该日期已签到: " + normalized; } - if (!plugin.getDb() - .deductPoints(name, 10)) { - return "§c积分不足,补签需 10 积分"; - } + // ★ 补签消耗:3债券 + 10积分 + // 积分不够时:用 9债券(3倍)代替 ★ + int bondCost = 3; + int pointCost = 10; + int bondAltCost = 9; // 3倍债券替代 - net.milkbowl.vault.economy - .Economy econ = - plugin.getEconomy(); - if (econ != null) { - double bal = econ.getBalance(p); - if (bal < 10.0) { - plugin.getDb() - .addPoints(name, 10); - int mult = plugin.getConfig2() - .backCheckPointMultiplier; - int needed = 10 * mult; - p.sendMessage("§e§l[补签] " - + "§f经济不足($" - + String.format( - "%.2f", bal) - + " < $10)"); - p.sendMessage("§7是否用 §a§l" - + needed + "积分 " - + "§7代替 10经济?"); - p.sendMessage("§7输入 §a1 " - + "§7同意," - + "§c其他 §7取消"); - return "NEED_CONFIRM:" - + normalized - + ":" + needed; + int myBonds = plugin.getBonds() + .getBonds(name); + Object ptsObj = plugin.getDb() + .getField(name, "points"); + int myPoints = ptsObj != null + ? ((Number) ptsObj).intValue() + : 0; + + + // 方案A:3债券 + 10积分 + boolean canA = myBonds >= bondCost + && myPoints >= pointCost; + // 方案B:9债券(积分不够时) + boolean canB = myBonds >= bondAltCost + && myPoints < pointCost; + + if (!canA && !canB) { + if (myPoints < pointCost + && myBonds < bondAltCost) { + return "§c补签费用不足" + + "(需3债券+10积分," + + "或9债券代替)" + + " 当前: " + + myBonds + "债券 " + + myPoints + "积分"; } - econ.withdrawPlayer(p, 10); + if (myBonds < bondCost) { + return "§c债券不足(需" + + bondCost + "枚)"; + } + return "§c积分不足(需" + + pointCost + "分)"; + } + + // 执行扣款 + if (canA) { + // 方案A:3债券 + 10积分 + plugin.getBonds().deductBonds( + name, bondCost, + "admin_deduct", "", + "补签系统", "补签扣款"); + plugin.getDb().deductPoints( + name, pointCost); + } else { + // 方案B:9债券代替 + plugin.getBonds().deductBonds( + name, bondAltCost, + "admin_deduct", "", + "补签系统", "补签扣款(债券替代)"); } doBackCheckIn(p, normalized); + + String costMsg = canA + ? bondCost + "债券+" + pointCost + "积分" + : bondAltCost + "债券(替代)"; return "§a补签成功!日期: " + normalized - + " §7(扣除10积分+10经济)"; + + " §7消耗: " + costMsg; } + + // ===== 执行补签 ===== // ===== 执行补签 ===== public void doBackCheckIn( Player p, String dateStr) { String name = p.getName(); Object val = plugin.getDb() - .getField(name, - "backcheck_dates"); + .getField(name, "backcheck_dates"); String existing = val != null ? val.toString() : ""; String updated = existing.isEmpty() @@ -241,25 +284,8 @@ public class CheckInManager { "backcheck_dates", updated); int streak = getStreak(name); plugin.getDb().setField(name, - "checkin_streak", - streak + 1); - int bondReward = 1; - if (streak >= 21) bondReward = 3; - else if (streak >= 14) bondReward = 2; - else if (streak >= 7) bondReward = 2; - plugin.getBonds().addBonds(name, bondReward); - } - // ===== 标准化日期 ===== - private String normalizeDate( - String input) { - String s = input.trim() - .replace("/", "-"); - if (s.matches("\\d{8}")) { - return s.substring(0, 4) + "-" - + s.substring(4, 6) + "-" - + s.substring(6, 8); - } - return s; + "checkin_streak", streak + 1); + // ★ 补签不发放奖励,只记录 } // ===== 获取N天前日期 ===== diff --git a/src/main/java/Sdf1_login/ConfigManager.java b/src/main/java/Sdf1_login/ConfigManager.java index 13b7cfa..a15ada0 100644 --- a/src/main/java/Sdf1_login/ConfigManager.java +++ b/src/main/java/Sdf1_login/ConfigManager.java @@ -7,10 +7,10 @@ import java.util.*; public class ConfigManager { private final File dataFolder; - private final Map messages = - new LinkedHashMap<>(); - private final Map smtpSettings = - new LinkedHashMap<>(); + private final Map messages + = new LinkedHashMap<>(); + private final Map smtpSettings + = new LinkedHashMap<>(); // ===== 公共字段 ===== public String adminTag = "admin"; @@ -30,7 +30,6 @@ public class ConfigManager { public int httpPort = 8080; public String garbageTag = "admin"; - // 签到经济奖励 public double checkinRewardFixed = 0; public double checkinRewardMin = 0; public double checkinRewardMax = 0; @@ -38,35 +37,36 @@ public class ConfigManager { public int checkinPoints = 10; public String checkinRewardType = "none"; public int backCheckPointMultiplier = 5; - // 提成/工单 + public int checkinBondMin = 1; + public int checkinBondMax = 3; + 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; + // ★ 构造函数接收 File,和 Main.java 调用一致 ★ public ConfigManager(File dataFolder) { this.dataFolder = dataFolder; } + public String rewardChannel = "债券"; // bonds = 债券,economy = Vault经济 + // ==================== 消息 ==================== public void loadMessages() { messages.clear(); - File file = - new File(dataFolder, "消息.txt"); - if (!file.exists()) - createDefaultMessages(file); + 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); + String val = messages.getOrDefault(key, key); val = val.replace("", "&l"); val = val.replace("", "&r"); val = val.replace("
", "\n"); @@ -79,81 +79,22 @@ public class ConfigManager { private void createDefaultMessages(File f) { List L = new ArrayList<>(); L.add("# ===== Sdf1_login 消息配置 ====="); - L.add("# 占位符用 {xxx} 表示"); - L.add("# 修改后执行 /sdf1_login reload"); - L.add(""); - - L.add("# ---- 登录注册 ----"); - L.add("login_timeout=§c登录超时,请重新登录"); - L.add("not_registered=§e您尚未注册,请使用 /reg <密码>"); - L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>"); - L.add("already_registered=§c您已注册过账号"); + L.add("login_timeout=§c登录超时"); + L.add("not_registered=§e您尚未注册"); + L.add("not_logged_in=§e您尚未登录"); + L.add("already_registered=§c您已注册过"); L.add("already_logged_in=§c您已登录"); - L.add("reg_success=§a注册成功!欢迎来到服务器"); + L.add("reg_success=§a注册成功!"); L.add("login_success=§a登录成功!"); - L.add("login_failed=§c密码错误,请重试"); - L.add(""); - - L.add("# ---- 密码 ----"); + L.add("login_failed=§c密码错误"); L.add("password_wrong=§c密码错误"); L.add("password_changed=§a密码修改成功!"); - L.add("password_format_error=§c密码需6位以上,含大小写字母和数字"); + L.add("password_format_error=§c密码需6位以上含大小写和数字"); L.add("password_same=§c新密码不能与旧密码相同"); - L.add("reset_no_email=§c您未绑定邮箱,无法找回密码"); - L.add("reset_sent=§a临时密码已发送到您的邮箱"); - L.add(""); - - L.add("# ---- 签到积分 ----"); L.add("checkin_already=§c您今日已签到"); L.add("checkin_success=§a签到成功!获得 {points} 积分"); - L.add("points_insufficient=§c积分不足,当前: {points}"); - L.add("points_purchase_success=§a购买成功!消耗 {count} 积分"); - L.add(""); - - L.add("# ---- 挂机检测 ----"); - L.add("afk_set_enabled=§a挂机检测已开启"); - L.add("afk_set_disabled=§c挂机检测已关闭"); - L.add("afk_kick=§c因挂机过久被踢出"); - L.add(""); - - L.add("# ---- 邮箱 ----"); - L.add("email_set=§a邮箱设置成功:{email}"); - L.add("email_format_error=§c邮箱格式不正确"); - L.add(""); - - L.add("# ---- 邀请 ----"); - L.add("invite_code_generated=§a邀请码:{code}"); - L.add("invite_used=§a邀请成功,邀请人:{player}"); - L.add("invite_self=§c不能使用自己的邀请码"); - L.add("invite_not_found=§c邀请码不存在"); - L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分"); - L.add(""); - - L.add("# ---- 礼包 ----"); - L.add("gift_not_ready=§c礼包条件未满足"); - L.add("gift_claimed=§a礼包领取成功!"); - L.add(""); - - L.add("# ---- 审批与IP ----"); - L.add("need_approval=§e注册需审批 (工单#{id})"); - L.add("ip_max_accounts=§cIP注册已达上限"); - L.add(""); - - L.add("# ---- 管理员 ----"); - L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除"); - L.add(""); - - L.add("# ---- 聊天过滤 ----"); - L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言"); - L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规链接: §e{url}"); - L.add("chat_url_violation=§7当前违规次数: §e{count}"); - L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url}"); - L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚"); - L.add(""); - - L.add("# ---- 通用 ----"); + L.add("chat_muted=§c你已被禁言"); L.add("未知参数=§c未知参数或权限不足"); - writeLines(f, L); } @@ -161,10 +102,8 @@ public class ConfigManager { public void loadSmtp() { smtpSettings.clear(); - File file = - new File(dataFolder, "SMTP设置.txt"); - if (!file.exists()) - createDefaultSmtp(file); + File file = new File(dataFolder, "SMTP设置.txt"); + if (!file.exists()) createDefaultSmtp(file); loadMap(file, smtpSettings); } @@ -177,10 +116,9 @@ public class ConfigManager { } public void saveSmtp() { - File file = - new File(dataFolder, "SMTP设置.txt"); + File file = new File(dataFolder, "SMTP设置.txt"); List L = new ArrayList<>(); - L.add("# ===== SMTP 邮件服务器配置 ====="); + L.add("# ===== SMTP 配置 ====="); for (Map.Entry e : smtpSettings.entrySet()) { L.add(e.getKey() + "=" + e.getValue()); @@ -190,10 +128,10 @@ public class ConfigManager { private void createDefaultSmtp(File f) { List L = new ArrayList<>(); - L.add("# ===== SMTP 邮件服务器配置 ====="); + 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("smtp密码="); L.add("发件人名称=Sdf1_login"); L.add("smtp加密=true"); @@ -203,131 +141,134 @@ public class ConfigManager { // ==================== 插件设置 ==================== public void loadSettings() { - File file = - new File(dataFolder, "插件设置.txt"); - if (!file.exists()) - createDefaultSettings(file); - Map m = - new LinkedHashMap<>(); + 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")); - httpPort = parseInt( - m.getOrDefault("http-port", "8080"), - 8080); - broadcastServerUrl = m.getOrDefault( - "resource-pack-url", ""); + 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")); + httpPort = parseInt(m.getOrDefault("http-port", "8080"), 8080); + broadcastServerUrl = m.getOrDefault("resource-pack-url", ""); - // 提成 - 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); + 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); + garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true")); + garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300); + garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1); - // 签到 - checkinRewardFixed = parseDouble( - m.getOrDefault( - "签到固定金额", "0"), 0); - checkinRewardMin = parseDouble( - m.getOrDefault( - "签到最小金额", "0"), 0); - checkinRewardMax = parseDouble( - m.getOrDefault( - "签到最大金额", "0"), 0); - checkinGivePoints = Boolean.parseBoolean( - m.getOrDefault( - "签到给积分", "true")); - checkinPoints = parseInt( - m.getOrDefault( - "签到积分数量", "10"), 10); - backCheckPointMultiplier = parseInt( - m.getOrDefault( - "补签积分倍率", "5"), 5); + checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0); + checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0); + checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0); + checkinGivePoints = Boolean.parseBoolean(m.getOrDefault("签到给积分", "true")); + checkinPoints = parseInt(m.getOrDefault("签到积分数量", "10"), 10); + backCheckPointMultiplier = parseInt(m.getOrDefault("补签积分倍率", "5"), 5); + checkinBondMin = parseInt(m.getOrDefault("签到债券最小", "1"), 1); + checkinBondMax = parseInt(m.getOrDefault("签到债券最大", "3"), 3); + if (checkinBondMax < checkinBondMin) checkinBondMax = checkinBondMin; + if (checkinRewardFixed > 0) checkinRewardType = "fixed"; + else if (checkinRewardMin > 0 && checkinRewardMax > checkinRewardMin) checkinRewardType = "range"; + else checkinRewardType = "none"; + rewardChannel = m.getOrDefault("奖励发放方式", "债券"); + rewardChannel = normalizeRewardChannel(rewardChannel); - // 自动判断模式 - if (checkinRewardFixed > 0) { - checkinRewardType = "fixed"; - } else if (checkinRewardMin > 0 - && checkinRewardMax > 0 - && checkinRewardMax - > checkinRewardMin) { - checkinRewardType = "range"; - } else { - checkinRewardType = "none"; +// 验证合法值 + if (!"economy".equalsIgnoreCase(rewardChannel) + && !"bonds".equalsIgnoreCase(rewardChannel)) { + rewardChannel = "bonds"; } + + + + // ★ 自动补充新配置项 ★ + boolean needAppend = false; + List newKeys = new ArrayList<>(); + if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); } + if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); } + if (!m.containsKey("奖励发放方式")) { + needAppend = true; + newKeys.add("奖励发放方式=债券"); + } + if (needAppend) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + for (String key : newKeys) { bw.write(key); bw.newLine(); } + bw.flush(); bw.close(); + System.out.println("[Config] 已补充签到债券配置"); + + } catch (IOException ignored) {} + if (!m.containsKey("奖励发放方式")) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + bw.write("奖励发放方式=bonds"); + bw.newLine(); + bw.flush(); bw.close(); + } catch (IOException ignored) {} + } + + } + } + /** + * 奖励方式标准化:中英文全兼容 + * "债券" "bonds" "Bond" "BONDS" → "bonds" + * "经济" "economy" "Economy" "ECONOMY" → "economy" + * 其他任何值 → "bonds"(默认债券) + */ + private String normalizeRewardChannel(String raw) { + if (raw == null) return "bonds"; + String s = raw.trim().toLowerCase(); + // 中文 + if (s.contains("债券")) return "bonds"; + if (s.contains("经济")) return "economy"; + // 英文 + if (s.contains("bond")) return "bonds"; + if (s.contains("econ")) return "economy"; + if (s.contains("Vault")) return "economy"; + // 兜底 + return "bonds"; } public void saveSettings() { - File file = - new File(dataFolder, "插件设置.txt"); + 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("自动审批延迟分钟=" + autoApproveDelayMinutes); L.add("登录超时秒数=" + loginTimeout); L.add("挂机踢出=" + afkEnabled); L.add("挂机超时秒数=" + afkTimeout); - L.add("# ---- 资源包 ----"); - L.add("resource-pack-url=" - + broadcastServerUrl); + L.add("resource-pack-url=" + broadcastServerUrl); L.add("http-port=" + httpPort); - L.add("# ---- 垃圾站 ----"); L.add("垃圾站_启用=" + garbageEnabled); - L.add("垃圾站_清理间隔秒=" - + garbageInterval); + L.add("垃圾站_清理间隔秒=" + garbageInterval); L.add("垃圾站_保留轮数=" + garbageMaxRounds); - L.add("# ---- 签到盲盒奖励 ----"); L.add("签到固定金额=" + checkinRewardFixed); L.add("签到最小金额=" + checkinRewardMin); L.add("签到最大金额=" + checkinRewardMax); L.add("签到给积分=" + checkinGivePoints); L.add("签到积分数量=" + checkinPoints); - L.add("补签积分倍率=" - + backCheckPointMultiplier); - - + L.add("签到债券最小=" + checkinBondMin); + L.add("签到债券最大=" + checkinBondMax); + L.add("补签积分倍率=" + backCheckPointMultiplier); + L.add("奖励发放方式=债券"); writeLines(file, L); } @@ -342,177 +283,90 @@ public class ConfigManager { L.add("登录超时秒数=60"); L.add("挂机踢出=false"); L.add("挂机超时秒数=300"); - L.add("# ---- 资源包 ----"); L.add("resource-pack-url="); L.add("http-port=8080"); - L.add("# ---- 垃圾站 ----"); L.add("垃圾站_启用=true"); L.add("垃圾站_清理间隔秒=300"); L.add("垃圾站_保留轮数=1"); - L.add("# ---- 签到盲盒奖励 ----"); L.add("签到固定金额=0"); L.add("签到最小金额=50"); L.add("签到最大金额=500"); L.add("签到给积分=true"); L.add("签到积分数量=10"); + L.add("签到债券最小=1"); + L.add("签到债券最大=3"); L.add("补签积分倍率=5"); - - + L.add("奖励发放方式=债券"); writeLines(f, L); } - - // ==================== 通用工具 ==================== - public List getAfkWhitelist() { List list = new ArrayList<>(); - File file = new File( - dataFolder, "挂机白名单.txt"); + 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(); - } + FileWriter fw = new FileWriter(file); + fw.write("# 每行一个玩家名\n"); + fw.flush(); + fw.close(); } catch (IOException ignored) {} + return list; } - try (BufferedReader r = - new BufferedReader( - new InputStreamReader( - new FileInputStream( - file), - StandardCharsets - .UTF_8))) { + 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("#")) { + if (!line.isEmpty() && !line.startsWith("#")) list.add(line); - } } } catch (IOException ignored) {} return list; } + // ==================== 其他 ==================== + public String generateSmtpVerifyCode() { - String chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - + "abcdefghijklmnopqrstuvwxyz" - + "0123456789"; + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 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()))); - } + 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; + smtpVerifyExpire = System.currentTimeMillis() + 300000L; return smtpVerifyCode; } - public boolean checkSmtpVerifyCode( - String code) { - if (System.currentTimeMillis() - > smtpVerifyExpire) { - smtpVerifyCode = ""; - return false; - } - return smtpVerifyCode - .equalsIgnoreCase(code); + public boolean checkSmtpVerifyCode(String code) { + if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; } + return smtpVerifyCode.equalsIgnoreCase(code); } - public void clearSmtpVerifyCode() { - smtpVerifyCode = ""; - smtpVerifyExpire = 0; - } + public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; } + public boolean canSendSmtpVerify() { return System.currentTimeMillis() - smtpLastSendTime >= 60000L; } + public void recordSmtpSendTime() { smtpLastSendTime = System.currentTimeMillis(); } - public boolean canSendSmtpVerify() { - return System.currentTimeMillis() - - smtpLastSendTime >= 60000L; - } + // ==================== 文件工具 ==================== - public void recordSmtpSendTime() { - smtpLastSendTime = - System.currentTimeMillis(); - } - - // ==================== 文件读写 ==================== - - private void loadMap(File file, - Map map) { - try (BufferedReader r = - new BufferedReader( - new InputStreamReader( - new FileInputStream( - file), - StandardCharsets - .UTF_8))) { + 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; + 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()); - } + 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()); - } + } catch (IOException ignored) {} } - 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 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 ignored) {} } - private double parseDouble( - String s, double def) { - try { - return Double.parseDouble(s.trim()); - } catch (Exception e) { - return def; - } - } - - private int parseInt(String s, int def) { - try { - return Integer.parseInt(s.trim()); - } catch (Exception e) { - return def; - } - } + private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } } + private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } } } diff --git a/src/main/java/Sdf1_login/CypayCommand.java b/src/main/java/Sdf1_login/CypayCommand.java index 468a7ec..7fd9067 100644 --- a/src/main/java/Sdf1_login/CypayCommand.java +++ b/src/main/java/Sdf1_login/CypayCommand.java @@ -74,7 +74,7 @@ public class CypayCommand } // ===== /cypay import ===== - if (args.length == 2 + /* if (args.length == 2 && "import".equalsIgnoreCase( args[0])) { if (!hasAdminPerm(sender)) { @@ -93,7 +93,7 @@ public class CypayCommand sender.sendMessage("§a导入完成: " + result + " 条"); return true; - } + }*/ // ===== /cypay freeze ===== if (args.length == 2 @@ -368,6 +368,14 @@ public class CypayCommand // ===== /cypay — 查指定玩家 ===== if (args.length == 1) { String target = args[0]; + // ★ 验证玩家在 login.db 中存在 ★ + if (!plugin.getDb().userExists(target)) { + sender.sendMessage("§c玩家 " + target + + " 不存在于服务器"); + return true; + } + + // String target = args[0]; if (hasAdminPerm(sender)) { int bonds = plugin.getBonds() .getBonds(target); diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index 9f18ca2..cb2b8f7 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -60,9 +60,11 @@ public class DatabaseManager { + "tasks_completed TEXT DEFAULT ''," + "premium_uuid TEXT DEFAULT ''," + "register_ip TEXT DEFAULT ''," - + "register_type TEXT DEFAULT 'manual'" + + "register_type TEXT DEFAULT 'manual'," + + "backcheck_dates TEXT DEFAULT ''" + ")"); + // 安全添加 users 缺失列 safeAdd(st, "tasks_completed", "TEXT DEFAULT ''"); diff --git a/src/main/java/Sdf1_login/EmailManager.java b/src/main/java/Sdf1_login/EmailManager.java index 8e2b3c0..b6906e5 100644 --- a/src/main/java/Sdf1_login/EmailManager.java +++ b/src/main/java/Sdf1_login/EmailManager.java @@ -1,275 +1,284 @@ -package Sdf1_login; - -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.net.Socket; -import java.util.Base64; -import java.util.logging.Logger; - -public class EmailManager { - - private final ConfigManager config; - private final Logger logger; - - public EmailManager(ConfigManager config) { - this.config = config; - this.logger = Logger.getLogger("Sdf1_login"); - } - - public boolean isConfigured() { - String host = config.getSmtp("smtp地址"); - String user = config.getSmtp("smtp账号"); - String pass = config.getSmtp("smtp密码"); - return host != null && !host.isEmpty() - && user != null && !user.isEmpty() - && pass != null && !pass.isEmpty(); - } - - public boolean sendVerifyCode(String to, - String code) { - if (!isConfigured()) { - logger.warning( - "[Sdf1_login] SMTP未配置," - + "无法发送验证码"); - return false; - } - logger.info("[Sdf1_login] 发送验证码到 " + to); - return sendEmailSync(to, - "[Sdf1_login] 验证码", - "您的验证码: " + code - + "\n5分钟内有效。\n" - + "如非本人操作请忽略。"); - } - - public boolean sendTempPassword(String to, - String player, String tempPwd) { - if (!isConfigured()) { - logger.warning( - "[Sdf1_login] SMTP未配置"); - return false; - } - return sendEmailSync(to, - "[Sdf1_login] 临时密码", - "玩家 " + player - + " 的临时密码: " + tempPwd - + "\n请尽快登录并修改密码!"); - } - - private boolean sendEmailSync(String to, - String subject, String body) { - final boolean[] result = {false}; - Thread t = new Thread(() -> { - try { - result[0] = sendEmail( - to, subject, body); - } catch (Exception e) { - logger.severe( - "[Sdf1_login] 邮件线程异常: " - + e.getMessage()); - } - }, "Sdf1_login-SMTP"); - t.setDaemon(true); - t.start(); - try { - t.join(15000); - } catch (InterruptedException ignored) { - } - if (t.isAlive()) { - t.interrupt(); - logger.severe( - "[Sdf1_login] SMTP超时(15秒)"); - return false; - } - return result[0]; - } - - private boolean sendEmail(String to, - String subject, String body) { - String host = config.getSmtp("smtp地址"); - String portStr = config.getSmtp("smtp端口"); - String user = config.getSmtp("smtp账号"); - String pass = config.getSmtp("smtp密码"); - String fromName = config.getSmtp( - "发件人名称"); - - int port; - try { - port = Integer.parseInt(portStr); - } catch (Exception e) { - port = 465; - } - - try { - String encSubject = "=?UTF-8?B?" - + Base64.getEncoder() - .encodeToString( - subject.getBytes("UTF-8")) - + "?="; - String encBody = Base64.getEncoder() - .encodeToString( - body.getBytes("UTF-8")); - - if (port == 465) { - SSLSocketFactory factory = - (SSLSocketFactory) - SSLSocketFactory - .getDefault(); - SSLSocket socket = (SSLSocket) - factory.createSocket( - host, port); - socket.setSoTimeout(10000); - doSmtp(socket, host, user, pass, - to, encSubject, encBody, - fromName); - socket.close(); - } else { - Socket plainSocket = - new Socket(host, port); - plainSocket.setSoTimeout(10000); - BufferedReader preReader = - new BufferedReader( - new InputStreamReader( - plainSocket - .getInputStream(), - "UTF-8")); - PrintWriter preWriter = - new PrintWriter( - plainSocket - .getOutputStream(), - true); - checkResponse(preReader, "220"); - sendLine(preWriter, - "EHLO " + host); - checkResponse(preReader, "250"); - sendLine(preWriter, "STARTTLS"); - checkResponse(preReader, "220"); - SSLSocketFactory factory = - (SSLSocketFactory) - SSLSocketFactory - .getDefault(); - SSLSocket sslSocket = (SSLSocket) - factory.createSocket( - plainSocket, host, - port, true); - sslSocket.startHandshake(); - doSmtp(sslSocket, host, user, pass, - to, encSubject, encBody, - fromName); - sslSocket.close(); - } - logger.info( - "[Sdf1_login] 邮件发送成功 -> " - + to); - return true; - } catch (Exception e) { - logger.severe( - "[Sdf1_login] 邮件异常: " - + e.getClass() - .getSimpleName() - + ": " + e.getMessage()); - if (e.getCause() != null) { - logger.severe( - "[Sdf1_login] 根因: " - + e.getCause() - .getMessage()); - } - return false; - } - } - - private void doSmtp(SSLSocket socket, - String host, String user, String pass, - String to, String subject, String body, - String fromName) throws Exception { - BufferedReader reader = - new BufferedReader( - new InputStreamReader( - socket.getInputStream(), - "UTF-8")); - OutputStream rawOut = - socket.getOutputStream(); - PrintWriter writer = - new PrintWriter( - new java.io.BufferedOutputStream( - rawOut), true); - - checkResponse(reader, "220"); - sendLine(writer, "EHLO " + host); - checkResponse(reader, "250"); - sendLine(writer, "AUTH LOGIN"); - checkResponse(reader, "334"); - sendLine(writer, Base64.getEncoder() - .encodeToString( - user.getBytes("UTF-8"))); - checkResponse(reader, "334"); - sendLine(writer, Base64.getEncoder() - .encodeToString( - pass.getBytes("UTF-8"))); - checkResponse(reader, "235"); - sendLine(writer, - "MAIL FROM:<" + user + ">"); - checkResponse(reader, "250"); - sendLine(writer, - "RCPT TO:<" + to + ">"); - checkResponse(reader, "250"); - sendLine(writer, "DATA"); - checkResponse(reader, "354"); - sendLine(writer, - "From: " + fromName - + " <" + user + ">"); - sendLine(writer, - "To: <" + to + ">"); - sendLine(writer, - "Reply-To: <" + user + ">"); - sendLine(writer, - "Subject: " + subject); - sendLine(writer, "MIME-Version: 1.0"); - sendLine(writer, - "Content-Type: text/plain; " - + "charset=UTF-8"); - sendLine(writer, - "Content-Transfer-Encoding: " - + "base64"); - sendLine(writer, ""); - sendLine(writer, body); - sendLine(writer, "."); - checkResponse(reader, "250"); - sendLine(writer, "QUIT"); - } - - private void sendLine(PrintWriter writer, - String line) { - writer.print(line + "\r\n"); - writer.flush(); - } - - private void checkResponse( - BufferedReader reader, - String expected) throws Exception { - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - if (line.length() >= 4 - && Character.isDigit( - line.charAt(0)) - && line.charAt(3) == ' ') - break; - } - String resp = sb.toString().trim(); - if (!resp.startsWith(expected)) { - if (resp.startsWith("535")) - logger.severe( - "[Sdf1_login] SMTP 535 " - + "认证失败"); - throw new Exception( - "SMTP响应异常: 期望 " - + expected - + ",实际: " + resp); - } - } -} +package Sdf1_login; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Base64; +import java.util.logging.Logger; + +public class EmailManager { + + private final ConfigManager config; + private final Logger logger; + + public EmailManager(ConfigManager config) { + this.config = config; + this.logger = Logger.getLogger("Sdf1_login"); + } + + public boolean isConfigured() { + String host = config.getSmtp("smtp地址"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + return host != null && !host.isEmpty() + && user != null && !user.isEmpty() + && pass != null && !pass.isEmpty(); + } + + public boolean sendVerifyCode(String to, + String code) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置," + + "无法发送验证码"); + return false; + } + logger.info("[Sdf1_login] 发送验证码到 " + to); + return sendEmailSync(to, + "[Sdf1_login] 验证码", + "您的验证码: " + code + + "\n5分钟内有效。\n" + + "如非本人操作请忽略。"); + } + + public boolean sendTempPassword(String to, + String player, String tempPwd) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, + "[Sdf1_login] 临时密码", + "玩家 " + player + + " 的临时密码: " + tempPwd + + "\n请尽快登录并修改密码!"); + } + + public boolean sendBody(String to, String subject, String body) { + if (!isConfigured()) { + logger.warning("[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, subject, body); + } + + + private boolean sendEmailSync(String to, + String subject, String body) { + final boolean[] result = {false}; + Thread t = new Thread(() -> { + try { + result[0] = sendEmail( + to, subject, body); + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件线程异常: " + + e.getMessage()); + } + }, "Sdf1_login-SMTP"); + t.setDaemon(true); + t.start(); + try { + t.join(15000); + } catch (InterruptedException ignored) { + } + if (t.isAlive()) { + t.interrupt(); + logger.severe( + "[Sdf1_login] SMTP超时(15秒)"); + return false; + } + return result[0]; + } + + private boolean sendEmail(String to, + String subject, String body) { + String host = config.getSmtp("smtp地址"); + String portStr = config.getSmtp("smtp端口"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + String fromName = config.getSmtp( + "发件人名称"); + + int port; + try { + port = Integer.parseInt(portStr); + } catch (Exception e) { + port = 465; + } + + try { + String encSubject = "=?UTF-8?B?" + + Base64.getEncoder() + .encodeToString( + subject.getBytes("UTF-8")) + + "?="; + String encBody = Base64.getEncoder() + .encodeToString( + body.getBytes("UTF-8")); + + if (port == 465) { + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket socket = (SSLSocket) + factory.createSocket( + host, port); + socket.setSoTimeout(10000); + doSmtp(socket, host, user, pass, + to, encSubject, encBody, + fromName); + socket.close(); + } else { + Socket plainSocket = + new Socket(host, port); + plainSocket.setSoTimeout(10000); + BufferedReader preReader = + new BufferedReader( + new InputStreamReader( + plainSocket + .getInputStream(), + "UTF-8")); + PrintWriter preWriter = + new PrintWriter( + plainSocket + .getOutputStream(), + true); + checkResponse(preReader, "220"); + sendLine(preWriter, + "EHLO " + host); + checkResponse(preReader, "250"); + sendLine(preWriter, "STARTTLS"); + checkResponse(preReader, "220"); + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket sslSocket = (SSLSocket) + factory.createSocket( + plainSocket, host, + port, true); + sslSocket.startHandshake(); + doSmtp(sslSocket, host, user, pass, + to, encSubject, encBody, + fromName); + sslSocket.close(); + } + logger.info( + "[Sdf1_login] 邮件发送成功 -> " + + to); + return true; + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件异常: " + + e.getClass() + .getSimpleName() + + ": " + e.getMessage()); + if (e.getCause() != null) { + logger.severe( + "[Sdf1_login] 根因: " + + e.getCause() + .getMessage()); + } + return false; + } + } + + private void doSmtp(SSLSocket socket, + String host, String user, String pass, + String to, String subject, String body, + String fromName) throws Exception { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + socket.getInputStream(), + "UTF-8")); + OutputStream rawOut = + socket.getOutputStream(); + PrintWriter writer = + new PrintWriter( + new java.io.BufferedOutputStream( + rawOut), true); + + checkResponse(reader, "220"); + sendLine(writer, "EHLO " + host); + checkResponse(reader, "250"); + sendLine(writer, "AUTH LOGIN"); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + user.getBytes("UTF-8"))); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + pass.getBytes("UTF-8"))); + checkResponse(reader, "235"); + sendLine(writer, + "MAIL FROM:<" + user + ">"); + checkResponse(reader, "250"); + sendLine(writer, + "RCPT TO:<" + to + ">"); + checkResponse(reader, "250"); + sendLine(writer, "DATA"); + checkResponse(reader, "354"); + sendLine(writer, + "From: " + fromName + + " <" + user + ">"); + sendLine(writer, + "To: <" + to + ">"); + sendLine(writer, + "Reply-To: <" + user + ">"); + sendLine(writer, + "Subject: " + subject); + sendLine(writer, "MIME-Version: 1.0"); + sendLine(writer, + "Content-Type: text/plain; " + + "charset=UTF-8"); + sendLine(writer, + "Content-Transfer-Encoding: " + + "base64"); + sendLine(writer, ""); + sendLine(writer, body); + sendLine(writer, "."); + checkResponse(reader, "250"); + sendLine(writer, "QUIT"); + } + + private void sendLine(PrintWriter writer, + String line) { + writer.print(line + "\r\n"); + writer.flush(); + } + + private void checkResponse( + BufferedReader reader, + String expected) throws Exception { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + if (line.length() >= 4 + && Character.isDigit( + line.charAt(0)) + && line.charAt(3) == ' ') + break; + } + String resp = sb.toString().trim(); + if (!resp.startsWith(expected)) { + if (resp.startsWith("535")) + logger.severe( + "[Sdf1_login] SMTP 535 " + + "认证失败"); + throw new Exception( + "SMTP响应异常: 期望 " + + expected + + ",实际: " + resp); + } + } +} diff --git a/src/main/java/Sdf1_login/GUIManager.java b/src/main/java/Sdf1_login/GUIManager.java index 34c54c2..1d586d2 100644 --- a/src/main/java/Sdf1_login/GUIManager.java +++ b/src/main/java/Sdf1_login/GUIManager.java @@ -16,7 +16,7 @@ import org.bukkit.util.io.BukkitObjectInputStream; import org.bukkit.util.io.BukkitObjectOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.util.Base64; + @@ -163,8 +163,15 @@ public class GUIManager implements Listener { "§6§l垃圾回收站", "§7查看和取回清理的物品")); + } // ===== Row3(27-35): 管理功能 ===== + // ★ 商店 - 所有人都能看 ★ + g.setItem(27, mkItem(Material.CHEST, + "§6§l商店", + "§7浏览、购买、出售物品", + "§7货币: 债券")); + if (isAdmin(p)) { g.setItem(29, mkItem( Material.REDSTONE_BLOCK, @@ -301,6 +308,10 @@ public class GUIManager implements Listener { g.setItem(22, mkItem(Material.LIME_WOOL, "§a§l添加菜单项")); + g.setItem(27, mkItem(Material.ENDER_CHEST, + "§6§l商店", + "§7浏览、购买、出售物品", + "§7货币: 债券")); g.setItem(26, mkItem(Material.ARROW, "§7返回")); @@ -342,7 +353,7 @@ public class GUIManager implements Listener { public void openShop(Player p) { p.closeInventory(); - p.performCommand("sdf1_login shop"); + plugin.getShopManager().openShopMain(p); } public void openTicket(Player p) { @@ -351,11 +362,16 @@ public class GUIManager implements Listener { } @EventHandler public void onInvClose( + InventoryCloseEvent e) { if (!(e.getPlayer() instanceof Player)) return; + if (!(e.getPlayer() instanceof Player)) return; Player p = (Player) e.getPlayer(); + Player p3 = (Player) e.getPlayer(); String t = e.getView().getTitle(); + if (!(e.getPlayer() instanceof Player)) return; + Player p2 = (Player) e.getPlayer(); if (!t.equals(T_EDITOR)) return; @@ -718,13 +734,39 @@ public class GUIManager implements Listener { @EventHandler - public void onInvDrag( - InventoryDragEvent e) { - if (!(e.getWhoClicked() - instanceof Player)) return; - Player p = (Player) e.getWhoClicked(); - if (!T_EDITOR.equals( - e.getView().getTitle())) return; + public void onInvDrag(InventoryDragEvent e) { + if (!(e.getWhoClicked() instanceof Player)) return; + String title = e.getView().getTitle(); + plugin.getLogger().info("[GUI] title检查点1: [" + title + "]"); + + // 购物车/退款/商店/流水记录页面:禁止一切拖拽 + if ("§a§l购物车".equals(title) + || "§c§l退款中心".equals(title) + || "§6§l商店".equals(title) + || title.startsWith("§e§l流水记录") + || title.startsWith("§e§l转账记录") + || title.startsWith("§d§l余额操作")) { + e.setCancelled(true); + return; + } + + // 商店分类页:禁止拖拽 + if (title.startsWith("§6§l") && !title.equals(T_MAIN) + && !title.equals(T_MY_INFO) + && !title.equals(T_ADMIN) + && !title.equals(T_MORE) + && !title.equals(T_EDITOR) + && !title.equals(T_BIOME) + && !title.equals(T_GIFT_STAGES) + && !title.equals(T_INVITE) + && !title.equals(T_USER_MGMT) + && !title.equals(T_TASK_CENTER)) { + e.setCancelled(true); + return; + } + + // 编辑器:图标拖拽替换 + if (!T_EDITOR.equals(title)) return; if (e.getRawSlots().contains(12)) { List list = plugin.getMenu().getItems(); @@ -734,69 +776,195 @@ public class GUIManager implements Listener { e.getOldCursor().clone(); } } + + @EventHandler public void onInvClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player)) return; + if (!(event.getWhoClicked() instanceof Player)) + return; Player p = (Player) event.getWhoClicked(); String title = event.getView().getTitle(); int raw = event.getRawSlot(); - // 我的信息 — CDK兑换 (slot 16) + // ★ 模糊匹配所有订单相关 + if (title.contains("订单") + || title.contains("购买记录")) { + event.setCancelled(true); + if (title.contains("小票配置")) { + plugin.getOrderManager() + .handleReceiptConfigClick(p, raw); + } else if (title.contains("编辑:")) { + plugin.getOrderManager() + .handleReceiptBuilderClick(p, raw); + } else if (title.contains("购买记录")) { + plugin.getOrderManager() + .handleMyOrdersClick( + p, raw, event.isLeftClick(), + event.isShiftClick()); + } else if (title.contains("管理员订单面板")) { + plugin.getOrderManager() + .handleAdminClick(p, raw); + } else if (title.contains("订单管理")) { + plugin.getOrderManager() + .handleAdminOrdersClick( + p, raw, event.isLeftClick(), + event.isShiftClick()); + } else if (title.contains("订单中心")) { + plugin.getOrderManager() + .handleCenterClick(p, raw); + } + return; + } + + // ===== 我的信息 — CDK兑换 ===== if (T_MY_INFO.equals(title) && raw == 16) { - event.setCancelled(true); - p.closeInventory(); - p.sendMessage("§e§l[CDK] §f请输入兑换码(聊天输入):"); - plugin.getCDK().requestInput(p, "cdk", ""); - return; + if (handleCDKClick(p, event)) return; } - if (T_MAIN.equals(title) && raw == 32) { - event.setCancelled(true); - plugin.openMyWallet(p); - return; - } - // 余额操作界面 - // 余额操作界面 - if ("§d§l余额操作".equals(title)) { - event.setCancelled(true); - if (raw == 49) { - openAdmin(p); + + // ===== 主菜单 ===== + if (T_MAIN.equals(title)) { + if (raw == 32) { + event.setCancelled(true); + plugin.openMyWallet(p); return; } + if (raw == 27) { + event.setCancelled(true); + plugin.getShopManager().openShopMain(p); + return; + } + return; + } + + // ===== 我的钱包 ===== + if ("§6§l我的钱包".equals(title)) { + event.setCancelled(true); + if (raw == 22) { openMain(p); return; } + if (raw == 12) { plugin.openWalletTransactions(p, 7); return; } + if (raw == 13) { plugin.openWalletTransactions(p, 14); return; } + if (raw == 14) { plugin.openWalletTransfers(p); return; } + return; + } + + // ===== 流水/转账记录 ===== + if (title.startsWith("§e§l流水记录") + || title.startsWith("§e§l转账记录")) { + event.setCancelled(true); + if (raw == 53) { plugin.openMyWallet(p); return; } + return; + } + + // ===== 余额操作 ===== + if ("§d§l余额操作".equals(title)) { + event.setCancelled(true); + if (raw == 49) { openAdmin(p); return; } if (raw < 0 || raw >= 45) return; - ItemStack item = event.getView().getTopInventory().getItem(raw); + ItemStack item = event.getView() + .getTopInventory().getItem(raw); if (item == null) return; Material mat = item.getType(); - if (mat != Material.PLAYER_HEAD && mat != Material.EMERALD_BLOCK) return; + if (mat != Material.PLAYER_HEAD + && mat != Material.EMERALD_BLOCK) return; ItemMeta im = item.getItemMeta(); - if (im == null || im.getDisplayName() == null) return; - String targetName = im.getDisplayName().replaceAll("§[0-9a-fk-orA-FK-OR]", ""); - Player target = plugin.getServer().getPlayerExact(targetName); + if (im == null + || im.getDisplayName() == null) return; + String targetName = im.getDisplayName() + .replaceAll("§[0-9a-fk-orA-FK-OR]", ""); + Player target = plugin.getServer() + .getPlayerExact(targetName); if (target == null) return; - String adminTag = plugin.getConfig2().adminTag; - boolean isAdmin = p.isOp() || (!adminTag.isEmpty() && p.getScoreboardTags().contains(adminTag)); + String adminTag = + plugin.getConfig2().adminTag; + boolean isAdmin = p.isOp() + || (!adminTag.isEmpty() + && p.getScoreboardTags() + .contains(adminTag)); if (!isAdmin) return; - - boolean leftOnly = event.isLeftClick() && !event.isRightClick(); - boolean rightOnly = event.isRightClick() && !event.isLeftClick(); - - if (leftOnly) { - event.setCancelled(true); + if (event.isLeftClick() + && !event.isRightClick()) { p.closeInventory(); - p.sendMessage("§e§l[经济操作] §f对 §a" + targetName + " §f进行经济操作"); + p.sendMessage("§e§l[经济操作] §f对 §a" + + targetName + " §f进行经济操作"); p.sendMessage("§7输入 §a+数字 §7给钱,§c-数字 §7扣钱"); plugin.getCDK().requestInput(p, "econ", targetName); - } else if (rightOnly) { - event.setCancelled(true); + } else if (event.isRightClick() + && !event.isLeftClick()) { p.closeInventory(); - p.sendMessage("§e§l[债券操作] §f对 §a" + targetName + " §f进行债券操作"); + p.sendMessage("§e§l[债券操作] §f对 §a" + + targetName + " §f进行债券操作"); p.sendMessage("§7输入 §a+数字 §7给债券,§c-数字 §7扣债券"); plugin.getCDK().requestInput(p, "bond", targetName); } return; } + + // ===== 商店主界面 ===== + if ("§6§l商店".equals(title)) { + event.setCancelled(true); + plugin.getShopManager().handleMainShopClick(p, raw); + return; + } + + // ===== 退款中心 → 重定向订单中心 ===== + if ("§c§l退款中心".equals(title)) { + event.setCancelled(true); + plugin.getOrderManager().openOrderCenter(p); + return; + } + + // ===== 购物车页面 ===== + if ("§a§l购物车".equals(title)) { + event.setCancelled(true); + plugin.getShopManager().handleCartClick( + p, raw, event.isLeftClick(), + event.isRightClick()); + return; + } + + // ===== 打包确认 ===== + if ("§6§l选择包装方式".equals(title)) { + event.setCancelled(true); + plugin.getShopManager().handlePackingConfirm(p, raw); + return; + } + + // ===== 自选颜色 ===== + if ("§b§l选择潜影盒颜色".equals(title)) { + event.setCancelled(true); + plugin.getShopManager().handleColorSelect(p, raw); + return; + } + + // ===== 我的订单 ===== + if ("§6§l我的订单".equals(title)) { + event.setCancelled(true); + plugin.getOrderManager().handleMyOrdersClick( + p, raw, event.isLeftClick(), + event.isShiftClick()); + return; + } + + // ===== 管理员订单面板 ===== + if ("§c§l管理员订单面板".equals(title)) { + event.setCancelled(true); + plugin.getOrderManager().handleAdminClick(p, raw); + return; + } + + // ===== 商店分类页 ===== + ShopManager sm = plugin.getShopManager(); + boolean handled = sm.handleCategoryClick( + p, title, raw, + event.isLeftClick(), + event.isShiftClick()); + if (handled) { + event.setCancelled(true); + return; + } } - public void openEditor( + + public void openEditor( Player p, int idx) { List list = plugin.getMenu().getItems(); @@ -1026,27 +1194,51 @@ public class GUIManager implements Listener { * 处理"我的信息"GUI中CDK兑换按钮的点击 * 返回 true 表示已处理 */ - public boolean handleCDKClick(Player p, - InventoryClickEvent event) { + public boolean handleCDKClick(Player p, InventoryClickEvent event) { String title = event.getView().getTitle(); int raw = event.getRawSlot(); if (!T_MY_INFO.equals(title)) return false; - // CDK兑换按钮 (slot 16) if (raw == 16) { event.setCancelled(true); p.closeInventory(); - p.sendMessage( - "§e§l[CDK] §f请输入兑换码" - + "(直接输入聊天即可):"); - plugin.getCDK().requestInput( - p, "cdk", ""); + + org.bukkit.plugin.Plugin sdf1 = + org.bukkit.Bukkit.getPluginManager().getPlugin("Sdf1"); + + // 情况1:插件未安装 + if (sdf1 == null) { + p.sendMessage("§c§l[CDK] §f口令兑换系统尚未安装,请联系管理员"); + return true; + } + + // 情况2:插件已安装但未启用 + if (!sdf1.isEnabled()) { + p.sendMessage("§c§l[CDK] §f口令兑换系统未启用,请联系管理员"); + return true; + } + + // 情况3:正常接入 — 反射调用SDF1的startListening + try { + Class mainClass = sdf1.getClass(); + java.lang.reflect.Method method = + mainClass.getDeclaredMethod("startListening", Player.class); + method.setAccessible(true); + method.invoke(sdf1, p); + // startListening内部已发送 "已开启口令监听 (15秒)" 提示 + } catch (NoSuchMethodException e) { + p.sendMessage("§c§l[CDK] §f口令系统版本过低,请联系管理员更新"); + } catch (Exception e) { + p.sendMessage("§c§l[CDK] §f兑换系统异常,请稍后再试"); + } + return true; } return false; } + // ===== 放入自定义图标 ===== private void doPlaceIcon(Player p, Inventory topInv, diff --git a/src/main/java/Sdf1_login/IPGroupManager.java b/src/main/java/Sdf1_login/IPGroupManager.java index 34c2e00..cfeb699 100644 --- a/src/main/java/Sdf1_login/IPGroupManager.java +++ b/src/main/java/Sdf1_login/IPGroupManager.java @@ -1,67 +1,110 @@ -package Sdf1_login; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.util.ArrayList; -import java.util.List; - -public class IPGroupManager { - - private final Main plugin; - private final int maxAccounts; - - public IPGroupManager(Main plugin, int maxAccounts) { - this.plugin = plugin; - this.maxAccounts = maxAccounts; - } - - /** - * 检查该IP是否还能注册新账号 - */ - public boolean canRegister(String ip) { - if (maxAccounts <= 0) return true; - if (ip == null || ip.isEmpty()) return true; - return queryAccountsByIP(ip).size() < maxAccounts; - } - - /** - * 获取该IP下已注册的账号数量 - */ - public int getAccountCount(String ip) { - if (ip == null || ip.isEmpty()) return 0; - return queryAccountsByIP(ip).size(); - } - - /** - * 获取该IP下所有账号名 - */ - public List getAccounts(String ip) { - return queryAccountsByIP(ip); - } - - /** - * 直接通过数据库连接查询指定IP下的所有账号 - */ - private List queryAccountsByIP(String ip) { - List accounts = new ArrayList<>(); - Connection conn = plugin.getDb().getDb(); - if (conn == null) return accounts; - try { - PreparedStatement ps = conn.prepareStatement( - "SELECT player_name FROM users " - + "WHERE ip_address = ?"); - ps.setString(1, ip); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - accounts.add( - rs.getString("player_name")); - } - rs.close(); - ps.close(); - } catch (Exception e) { - e.printStackTrace(); - } - return accounts; - } -} +package Sdf1_login; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; + +public class IPGroupManager { + + private final Main plugin; + private final int maxAccounts; + + public IPGroupManager(Main plugin, int maxAccounts) { + this.plugin = plugin; + this.maxAccounts = maxAccounts; + } + + public boolean canRegister(String ip) { + if (maxAccounts <= 0) return true; + if (ip == null || ip.isEmpty()) return true; + int count = getAccountCount(ip); + boolean can = count < maxAccounts; + plugin.getLogger().info( + "[IP限制] IP=" + ip + + " 已注册=" + count + + " 上限=" + maxAccounts + + " 允许=" + can); + return can; + } + + public int getAccountCount(String ip) { + if (ip == null || ip.isEmpty()) return 0; + return queryAccountsByIP(ip).size(); + } + + public List getAccounts(String ip) { + return queryAccountsByIP(ip); + } + + private List queryAccountsByIP(String ip) { + List accounts = new ArrayList<>(); + Connection conn = plugin.getDb().getDb(); + if (conn == null) return accounts; + try { + // 主查询:ip_address 列 + PreparedStatement ps = conn.prepareStatement( + "SELECT player_name FROM users " + + "WHERE ip_address = ?"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + accounts.add( + rs.getString("player_name")); + } + rs.close(); + ps.close(); + + // 兜底:如果主列没数据,查 last_login_ip + if (accounts.isEmpty()) { + PreparedStatement ps2 = conn.prepareStatement( + "SELECT player_name FROM users " + + "WHERE last_login_ip = ? " + + "AND (ip_address IS NULL " + + "OR ip_address = '' " + + "OR ip_address != ?)"); + ps2.setString(1, ip); + ps2.setString(2, ip); + ResultSet rs2 = ps2.executeQuery(); + while (rs2.next()) { + String name = rs2.getString("player_name"); + if (!accounts.contains(name)) + accounts.add(name); + } + rs2.close(); + ps2.close(); + } + + // 兜底2:查 register_ip + if (accounts.isEmpty()) { + PreparedStatement ps3 = conn.prepareStatement( + "SELECT player_name FROM users " + + "WHERE register_ip = ? " + + "AND (ip_address IS NULL " + + "OR ip_address = '') " + + "AND (last_login_ip IS NULL " + + "OR last_login_ip = '' " + + "OR last_login_ip != ?)"); + ps3.setString(1, ip); + ps3.setString(2, ip); + ResultSet rs3 = ps3.executeQuery(); + while (rs3.next()) { + String name = rs3.getString("player_name"); + if (!accounts.contains(name)) + accounts.add(name); + } + rs3.close(); + ps3.close(); + } + + plugin.getLogger().info( + "[IP限制] 查询IP=" + ip + + " 结果数=" + accounts.size() + + " 账号=" + accounts); + } catch (Exception e) { + e.printStackTrace(); + } + return accounts; + } +} diff --git a/src/main/java/Sdf1_login/InventorySerializer.java b/src/main/java/Sdf1_login/InventorySerializer.java index 800272e..32bc632 100644 --- a/src/main/java/Sdf1_login/InventorySerializer.java +++ b/src/main/java/Sdf1_login/InventorySerializer.java @@ -1,46 +1,46 @@ -package Sdf1_login; - -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Base64; - -public class InventorySerializer { - - public static String toBase64(ItemStack[] items) throws IOException { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); - dataOutput.writeInt(items.length); - for (ItemStack item : items) { - dataOutput.writeObject(item); - } - dataOutput.close(); - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); - } catch (Exception e) { - throw new IOException("Unable to serialize item stacks.", e); - } - } - - public static ItemStack[] fromBase64(String data) throws IOException { - if (data == null || data.isEmpty()) { - return new ItemStack[0]; - } - try { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); - ItemStack[] items = new ItemStack[dataInput.readInt()]; - for (int i = 0; i < items.length; i++) { - items[i] = (ItemStack) dataInput.readObject(); - } - dataInput.close(); - return items; - } catch (ClassNotFoundException e) { - throw new IOException("Unable to deserialize item stacks.", e); - } - } -} +package Sdf1_login; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; + +public class InventorySerializer { + + public static String toBase64(ItemStack[] items) throws IOException { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); + dataOutput.writeInt(items.length); + for (ItemStack item : items) { + dataOutput.writeObject(item); + } + dataOutput.close(); + return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + } catch (Exception e) { + throw new IOException("Unable to serialize item stacks.", e); + } + } + + public static ItemStack[] fromBase64(String data) throws IOException { + if (data == null || data.isEmpty()) { + return new ItemStack[0]; + } + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); + ItemStack[] items = new ItemStack[dataInput.readInt()]; + for (int i = 0; i < items.length; i++) { + items[i] = (ItemStack) dataInput.readObject(); + } + dataInput.close(); + return items; + } catch (ClassNotFoundException e) { + throw new IOException("Unable to deserialize item stacks.", e); + } + } +} diff --git a/src/main/java/Sdf1_login/LoginManager.java b/src/main/java/Sdf1_login/LoginManager.java index 5b3db8c..760ce78 100644 --- a/src/main/java/Sdf1_login/LoginManager.java +++ b/src/main/java/Sdf1_login/LoginManager.java @@ -118,7 +118,16 @@ public class LoginManager { public boolean handleLogin(Player p, String[] args) { + String name = p.getName(); + // ★ 在 handleLogin 方法开头 + if (plugin.getNeedsPasswordChange().contains(name) + && plugin.getLoggedIn().contains(name)) { + // 已登录但还没改密码,只允许执行 /sdf1_login pw + p.sendMessage("§c请先修改密码: /sdf1_login pw <旧密码> <新密码>"); + return true; + } + try { if (plugin.getLoggedIn().contains(name)) { p.sendMessage(plugin.getConfig2() @@ -179,27 +188,21 @@ public class LoginManager { name, hash); if (matchTemp) { plugin.getLoggedIn().add(name); - plugin.getDb().setLoggedIn( - name, true); + plugin.getDb().setLoggedIn(name, true); p.setAllowFlight(false); p.setFlying(false); - plugin.getDb().setField(name, - "last_login_time", + plugin.getDb().setField(name, "last_login_time", System.currentTimeMillis()); - plugin.getDb().setField(name, - "last_online_check", + plugin.getDb().setField(name, "last_online_check", System.currentTimeMillis()); plugin.restoreInventory(p); plugin.recordIPLogin(p); plugin.giveMenuSnowball(p); - plugin.getNeedsPasswordChange() - .add(name); - p.sendMessage(plugin.getConfig2() - .msg("login_success")); - p.sendMessage( - "§c§l[警告] §f您使用的是临时密码,请尽快修改密码!"); - p.sendMessage( - "§7用法: /sdf1_login pw"); + // ★ 确保这行存在 + plugin.getNeedsPasswordChange().add(name); + p.sendMessage(plugin.getConfig2().msg("login_success")); + p.sendMessage("§c§l[警告] §f您使用的是临时密码,请尽快修改密码!"); + p.sendMessage("§7用法: /sdf1_login pw"); activateCY(p, name); return true; } diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 33a412f..3e2343a 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -5,6 +5,8 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; + +import org.bukkit.event.inventory.*; import org.bukkit.event.player.PlayerInteractEvent; import java.util.Iterator; import java.util.List; @@ -17,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.bukkit.NamespacedKey; import org.bukkit.persistence.PersistentDataType; import java.util.Date; - +import org.bukkit.command.CommandSender; import net.milkbowl.vault.economy.Economy; @@ -33,8 +35,6 @@ 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; @@ -52,7 +52,6 @@ import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.inventory.CraftItemEvent; import org.bukkit.event.player.PlayerFishEvent; import java.util.HashMap; @@ -120,6 +119,7 @@ public class Main extends JavaPlugin private final Map ipAutoLogin = new HashMap<>(); private static final long IP_SKIP_MS = 300000L; + private SalesStatsManager salesStats; private final Map lastActivity = new ConcurrentHashMap<>(); @@ -138,6 +138,11 @@ public class Main extends JavaPlugin // 在 Main.java 字段声明区域(bondManager 附近)添加: // 在 bondManager 字段附近添加: private BondPrinter bondPrinter; + private OrderManager orderManager; + + public OrderManager getOrderManager() { + return orderManager; + } // 钱包流水查看目标(玩家名 → 查看目标) private final Map walletViewTarget = @@ -148,7 +153,7 @@ public class Main extends JavaPlugin TX_SDF = new java.text.SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); - + private ShopManager shopManager; //商店 @@ -275,17 +280,23 @@ private PVPManager pvpManager; public Set getNeedsPasswordChange() { return needsPasswordChange; } + + // public RadioManager radio; @Override public void onEnable() { getDataFolder().mkdirs(); + salesStats = new SalesStatsManager(this); // ===== 1. 基础配置 ===== config = new ConfigManager(getDataFolder()); config.loadMessages(); config.loadSmtp(); config.loadSettings(); - + configMgr = new ConfigManager(getDataFolder()); + configMgr.loadSettings(); + configMgr.loadMessages(); + orderManager = new OrderManager(this); // ===== 2. 数据库 ===== db = new DatabaseManager(getDataFolder()); db.init(); @@ -299,6 +310,10 @@ private PVPManager pvpManager; // ===== 4. GUI(依赖 menu) ===== gui = new GUIManager(this); + shopManager = new ShopManager(this); + Bukkit.getPluginManager().registerEvents(shopManager, this); + shopManager.loadCategories(); + getServer().getPluginManager() .registerEvents(gui, this); @@ -371,6 +386,10 @@ private PVPManager pvpManager; getCommand("menu").setExecutor(this); if (getCommand("printer") != null) { getCommand("printer").setExecutor(this); + getCommand("shop").setExecutor(this); + getCommand("商店").setExecutor(this); + // getCommand("testorder").setExecutor(this); + } @@ -421,22 +440,46 @@ private PVPManager pvpManager; cdkManager = new CDKManager(this); userGroupManager = new UserGroupManager(this); cypayCommand = new CypayCommand(this); -// 加载CDK文件 - cdkManager.loadCDKsFromDir(); +/*加载CDK文件 + cdkManager.loadCDKsFromDir();*/ // cypay 命令 if (getCommand("cypay") != null) { getCommand("cypay").setExecutor(cypayCommand); getCommand("cypay").setTabCompleter(cypayCommand); } bondPrinter = new BondPrinter(this); - cdkManager.loadCDKsFromDir(); + // cdkManager.loadCDKsFromDir(); +//14 ====商店==== + // ★ 只重载数据,不创建新实例 ★ + if (shopManager != null) { + shopManager.loadCategories(); + } + + configMgr = new ConfigManager(getDataFolder()); + configMgr.loadSettings(); + orderManager = new OrderManager(this); + + + getLogger().info("Sdf1_login v1.0 | 就绪"); + getLogger().info("[SDF1] orderManager=" + (orderManager != null ? "OK" : "NULL")); + } public BondPrinter getBondPrinter() { return bondPrinter; } + public ShopManager getShopManager() { + return shopManager; + } + + private ConfigManager configMgr; + + public ConfigManager getConfigMgr() { + return configMgr; + } + private void setupPVPTab() { if (getCommand("pvp") != null) { @@ -494,6 +537,7 @@ private PVPManager pvpManager; @Override public void onDisable() { + if (shopManager != null) shopManager.saveAll(); // 取消未执行的删除任务 if (pendingDeleteTask != null) { pendingDeleteTask.cancel(); @@ -2282,10 +2326,14 @@ private PVPManager pvpManager; long time = ((Number) tx.get("time")) .longValue(); - boolean pos = amount > 0; - String sign = pos ? "+" : ""; - String color = pos ? "§a" : "§c"; - String typeName = walletFormatType(type); + boolean isExpense = "shop_buy".equals(type) + || "shop_packing".equals(type); + boolean pos = isExpense ? false : (amount > 0); + String sign = pos ? "+" : (isExpense ? "-" : ""); + String color = pos ? "§a" : (isExpense ? "§c" : (amount < 0 ? "§c" : "")); +// 金额取绝对值显示 + int displayAmount = Math.abs(amount); + // 用 if-else 替代 switch 避免编译问题 Material mat = Material.PAPER; @@ -2317,11 +2365,13 @@ private PVPManager pvpManager; } else { mat = Material.RED_WOOL; } + String typeName = walletFormatType(type); List lore = new ArrayList<>(); lore.add("§7类型: " + typeName); - lore.add("§7金额: " + color + sign + amount + lore.add("§7金额: " + color + sign + displayAmount + " §6枚"); + if (tgt != null && !tgt.isEmpty()) lore.add("§7对象: §e" + tgt); if (reason != null && !reason.isEmpty()) @@ -2333,8 +2383,9 @@ private PVPManager pvpManager; ItemStack item = new ItemStack(mat); ItemMeta im = item.getItemMeta(); if (im != null) { - im.setDisplayName(color + sign + amount + im.setDisplayName(color + sign + displayAmount + " §6枚 - " + typeName); + im.setLore(lore); item.setItemMeta(im); } @@ -3587,6 +3638,18 @@ private PVPManager pvpManager; e.setCancelled(true); } } + /** 验证玩家是否在主库中真实存在 */ + public boolean isRealPlayer(String name) { + if (name == null || name.isEmpty()) return false; + try { + Map user = + getDb().getUser(name); + return user != null && !user.isEmpty() + && user.containsKey("player_name"); + } catch (Exception e) { + return false; + } + } // ===== Commands ===== @Override @@ -3595,6 +3658,136 @@ private PVPManager pvpManager; String[] args) { String cmdName = cmd.getName().toLowerCase(); + if (cmd.getName().equalsIgnoreCase("testorder") + && sender instanceof Player) { + Player p = (Player) sender; + p.sendMessage("§e=== 测试订单中心 ==="); + p.sendMessage("§7当前界面: §f" + + p.getOpenInventory().getTitle()); + this.getOrderManager().openOrderCenter(p); + // 延迟1秒检查实际打开了什么 + Bukkit.getScheduler().runTaskLater(this, () -> { + if (p.isOnline()) { + String opened = p.getOpenInventory() + .getTitle(); + p.sendMessage("§7打开后界面: §f" + opened); + this.getLogger().info("[TEST] 打开后: " + + opened); + } + }, 20L); + return true; + } + if (cmd.getName().equalsIgnoreCase("shop") + || cmd.getName().equals("商店")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可使用"); + return true; + } + if (shopManager != null) { + shopManager.openShopMain((Player) sender); + } else { + sender.sendMessage("§c商店系统未加载"); + } + return true; + } + +// ===== 在 onCommand 方法体内,最开头加: ===== + if (cmd.getName().equalsIgnoreCase("sdf1_login") + && args.length > 0 + && args[0].equalsIgnoreCase("shop")) { + if (shopManager == null) { + sender.sendMessage("§c商店未加载"); + return true; + } + return shopManager.handleCommand(sender, args); + } + // set + String sub = args[0].toLowerCase(); + 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 (!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); + + // ★ 立即发邮件(踢人之前) + String emailAddr = (String) db.getField(tgt, "email"); + if (emailAddr != null && !emailAddr.isEmpty()) { + // 获取最近登录信息 + Object lastLoginObj = db.getField(tgt, "last_login_time"); + long lastLogin = lastLoginObj != null + ? ((Number) lastLoginObj).longValue() : 0; + String lastIP = (String) db.getField(tgt, "last_ip"); + if (lastIP == null || lastIP.isEmpty()) + lastIP = "未知"; + + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String loginTimeStr = lastLogin > 0 + ? sdf.format(new java.util.Date(lastLogin)) + : "未知时间"; + + final String to = emailAddr; + final String fName = tgt; + final String pwd = tempPwd; + final String ipInfo = lastIP; + final String timeInfo = loginTimeStr; + final Main self = this; + + Bukkit.getScheduler().runTaskAsynchronously(this, + () -> { + // ★ 发送临时密码邮件(带登录信息) + boolean sent = email.sendTempPassword( + to, fName, pwd); + // ★ 额外发送安全通知邮件 + if (sent) { + String body = "玩家 " + fName + + ":\n\n" + + "管理员已为您的账号设置了临时密码。\n\n" + + "临时密码: " + pwd + "\n" + + "过期时间: 5分钟内\n\n" + + "最近登录记录:\n" + + " 时间: " + timeInfo + "\n" + + " IP地址: " + ipInfo + "\n\n" + + "如果您本人未操作,请忽略此邮件。\n" + + "如果您怀疑账号被盗,请立即修改密码。"; + email.sendBody(to, + "【安全通知】账号临时密码已设置", body); + } + Bukkit.getScheduler().runTask(self, () -> { + if (sent) { + sender.sendMessage("§a临时密码已发送至 " + + tgt + " 的邮箱"); + } + }); + }); + } else { + sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知"); + } + + // 踢人 + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer("§c管理员已设置了临时密码,请用新密码登录"); + sender.sendMessage("§a已为 " + tgt + " 设置临时密码"); + return true; + } + if (label.equalsIgnoreCase("pvp")) { if (!(sender instanceof Player)) { @@ -3604,10 +3797,22 @@ private PVPManager pvpManager; Player p = (Player) sender; return pvpManager.onCommand(p, args); } + if (cmdName.equals("cypay")) { - return cypayCommand.onCommand(sender, cmd, label, args); + if (args.length > 0) { + String target = args[0]; + if (!getDb().userExists(target)) { + sender.sendMessage("§c玩家 " + target + + " 不存在于服务器"); + return true; + } + } + return cypayCommand.onCommand( + sender, cmd, label, args); } + + // ===== /printer [玩家名] ===== if (label.equalsIgnoreCase("printer")) { if (bondPrinter == null) { @@ -4018,19 +4223,6 @@ private PVPManager pvpManager; gui.openMain((Player) sender); return true; } - // String cmdName = cmd.getName().toLowerCase(); - if (!cmdName.equals("sdf1_login")) return false; - - if (args.length == 0) { - if (sender instanceof Player) { - gui.openMain((Player) sender); - } else { - sender.sendMessage("§e用法: /sdf1_login <子命令>"); - } - return true; - } - - String sub = args[0].toLowerCase(); // ===== del / confirm / cancel(最先处理) ===== if (sub.equals("del")) { @@ -4179,7 +4371,10 @@ private PVPManager pvpManager; if (getMenu() != null) { getMenu().loadMenu(); } - + if (shopManager != null) { + shopManager.loadCategories(); + } + if (shopManager != null) shopManager.loadCategories(); sender.sendMessage("§a配置已重载!"); return true; } @@ -4303,8 +4498,12 @@ private PVPManager pvpManager; db.setField(p2.getName(), "password_salt", newSalt); if (!mainOk && tempOk) db.clearTempPassword(p2.getName()); + // pw 命令改密码成功后 needsPasswordChange.remove(p2.getName()); - p2.sendMessage(config.msg("password_changed")); +// 清除临时密码 + if (!mainOk && tempOk) + db.clearTempPassword(p2.getName()); + return true; } @@ -4785,62 +4984,6 @@ private PVPManager pvpManager; 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 body = "玩家 " + tgt - + ":\n您的临时密码已由管理员设置。\n" - + "临时密码将在5分钟后过期,请尽快登录并修改密码。"; - final String to = emailAddr; - final String finalBody = body; - Bukkit.getScheduler() - .runTaskAsynchronously(this, - () -> email - .sendTempPassword( - to, tgt, - finalBody)); - } - return true; - } // give - 手动发放雪球菜单 if (sub.equals("give")) { @@ -5032,6 +5175,21 @@ private PVPManager pvpManager; return true; } + /** + * 供SDF1反射调用的CDK兑换 + * 返回: "success:金额:余额前:余额后" + * 或 "fail:原因" + */ + public String redeemBondForExternal( + String playerName, String code) { + if (cdkManager == null) + return "fail:CDK未初始化"; + return cdkManager.redeem(code, playerName); + } + // 旧的(可能缺失或拼写错误) + public SalesStatsManager getSalesStats() { + return salesStats; + } /** * 是否是垃圾站展示物品(带ID lore) @@ -5096,7 +5254,7 @@ private PVPManager pvpManager; "get", "del", "set", "take", "add", "kick", "ticket", "oa", "stop", - "shopadd", "shopdel", "back", "radio", "back")); + "shopadd", "shopdel", "back", "radio", "back", "shop")); } else { list.addAll(Arrays.asList( "pw", "email", "sign", diff --git a/src/main/java/Sdf1_login/OrderManager.java b/src/main/java/Sdf1_login/OrderManager.java new file mode 100644 index 0000000..9dcb625 --- /dev/null +++ b/src/main/java/Sdf1_login/OrderManager.java @@ -0,0 +1,1345 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; + +public class OrderManager implements Listener { + private final Main plugin; + private final List orders = + Collections.synchronizedList(new ArrayList<>()); + private final Map userViewPage = new HashMap<>(); + private final Map adminViewPage = new HashMap<>(); + private final Map adminViewTarget = new HashMap<>(); + private final Set partialRefundListening = new HashSet<>(); + private final Map partialRefundTarget = new HashMap<>(); + private final Map receiptBuilderSection = new HashMap<>(); + private String tplHeader = ""; + private String tplItemLine = "{品名} x{数量} {原价}→{执行价} 小计{小计}枚"; + private String tplDiscountLine = "优惠: -{优惠额}枚 ({优惠类型})"; + private String tplPackLine = "打包: +{打包费}枚 ({打包颜色})"; + private String tplSummary = "原价{原价合计} → 实付{实付}"; + private String tplFooter = "感谢惠顾,欢迎再来!"; + private static final long RETENTION_30D = 30L * 24 * 60 * 60 * 1000; + private static final long SELF_REFUND_WINDOW = 5 * 60 * 1000; + private static final int PAGE_SIZE = 21; + private static final int[] SLOTS = { + 10,11,12,13,14,15,16,19,20,21,22,23,24,25,28,29,30,31,32,33,34}; + // ===== 字段 ===== + private final Set shulkerUsedOrders = + Collections.synchronizedSet(new HashSet<>()); + // ===== 待审批的退货退款 ===== + private final Map pendingReturns = + new HashMap<>(); + // ===== 退货申请确认 ===== + private final Map returnConfirmWaiting = + new HashMap<>(); // uuid -> orderId + private static final int RETURN_CONFIRM_TIMEOUT = 600; // 30秒 + + + private static class PendingReturn { + long orderId; + UUID playerUUID; + String playerName; + int totalPaid; + long createTime; + String packColor; + String packType; + List items; + + PendingReturn(OrderRecord r) { + this.orderId = r.orderId; + this.playerUUID = r.uuid; + this.playerName = r.player; + this.totalPaid = r.totalPaid; + this.createTime = System.currentTimeMillis(); + this.packColor = r.packColor; + this.packType = r.packType; + this.items = new ArrayList<>(r.items); + } + } + + + public static class OrderItem { + public String name; public String mat; + public int originalPrice; public int finalPrice; public int qty; + public OrderItem(String n,String m,int op,int fp,int q){ + name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;} + public int subtotal(){return finalPrice*qty;} + public int originalSubtotal(){return originalPrice*qty;} + } + public static class OrderRecord { + public long orderId; public UUID uuid; public String player; + public List items; public int totalOriginal; public int totalPaid; + public int discount; public String discountType; public String couponCode; + public int packFee; public String packType; public String packColor; + public long timestamp; public long printTime; public int status; + public int refundedAmount; public String refundType; + } + + public OrderManager(Main plugin) { + this.plugin = plugin; + loadReceiptConfig(); + loadOrders(); + startCleanupTask(); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + public List getOrders() { return orders; } + + // ===== 小票模板 ===== + private void loadReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + if (!f.exists()) createDefaultReceiptConfig(f); + Map m = loadMap(f); + tplHeader = m.getOrDefault("头部", tplHeader); + tplItemLine = m.getOrDefault("商品行", tplItemLine); + tplDiscountLine = m.getOrDefault("优惠行", tplDiscountLine); + tplPackLine = m.getOrDefault("打包行", tplPackLine); + tplSummary = m.getOrDefault("汇总", tplSummary); + tplFooter = m.getOrDefault("页脚", tplFooter); + } + private void saveReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=" + tplHeader); + L.add("商品行=" + tplItemLine); + L.add("优惠行=" + tplDiscountLine); + L.add("打包行=" + tplPackLine); + L.add("汇总=" + tplSummary); + L.add("页脚=" + tplFooter); + writeLines(f, L); + } + private void createDefaultReceiptConfig(File f) { + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}"); + L.add("商品行=§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚"); + L.add("优惠行=优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})"); + L.add("打包行=打包: §c+{打包费}枚§r ({打包颜色})"); + L.add("汇总=原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚"); + L.add("页脚=感谢惠顾,欢迎再来!"); + writeLines(f, L); + } + + // ===== 订单记录 ===== + public void recordOrder(Player p, List items, + int totalOriginal, int totalPaid, int discount, + String discountType, String couponCode, int packFee, + String packType, String packColor) { + OrderRecord r = new OrderRecord(); + r.orderId = System.currentTimeMillis(); + r.uuid = p.getUniqueId(); r.player = p.getName(); + r.items = items; r.totalOriginal = totalOriginal; + r.totalPaid = totalPaid; r.discount = discount; + r.discountType = discountType; r.couponCode = couponCode; + r.packFee = packFee; r.packType = packType; + r.packColor = packColor; r.timestamp = r.orderId; + r.printTime = 0; r.status = 0; r.refundedAmount = 0; + r.refundType = null; + orders.add(r); saveOrders(); + } + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + // ===== 小票聊天栏 ===== + /** + * 聊天栏打印小票(给定参数 Player + OrderRecord) + * 不要改动方法签名 + */ + public void sendReceiptChat(Player p, OrderRecord r) { + if (p == null || r == null) return; + + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + p.sendMessage("§7§m "); + p.sendMessage("§6§l======== 商店购物小票 ========"); + + // 退款标记 + String statusTag = getStatusTag(r); + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§7订单号: §f#" + r.orderId); + p.sendMessage("§7下单时间: §f" + + df.format(new Date(r.timestamp))); + p.sendMessage("§7玩家: §f" + r.player); + p.sendMessage("§7------------------------------"); + + for (OrderItem it : r.items) { + String price; + if (it.originalPrice != it.finalPrice) { + price = "§f" + it.originalPrice + + "§7→§6" + it.finalPrice; + } else { + price = "§6" + it.finalPrice; + } + p.sendMessage("§7 §e" + it.name + + " §7x" + it.qty + + " " + price + "枚" + + " §6小计" + it.subtotal() + "枚"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7原价: §f" + r.totalOriginal + "枚"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "§7(优惠券 -" + pct + "%)" + : "eco".equals(r.discountType) + ? "§7(环保 -" + pct + "%)" + : "§7(-" + pct + "%)"; + p.sendMessage("§7优惠: §a-" + r.discount + + "枚 " + dt); + } + + if (r.packFee > 0) { + String cn = r.packColor != null + ? r.packColor + .replace("_SHULKER_BOX", "") + .replace("SHULKER_BOX", "原色") + : "默认"; + p.sendMessage("§7打包: §c+" + r.packFee + + "枚 §7(" + cn + "潜影盒)"); + } else { + p.sendMessage("§7包装: 直接放入背包"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7实付: §6" + r.totalPaid + "枚"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + p.sendMessage("§7余额: §a" + bal + "枚"); + + // 退款标记 + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§6§l=============================="); + r.printTime = System.currentTimeMillis(); + } + + /** + * 生成书本小票内容 + */ + private String buildReceiptText(OrderRecord r) { + String statusTag = getStatusTag(r); + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + StringBuilder sb = new StringBuilder(); + + sb.append("§6§l===== 商店小票 =====\n"); + + // 退款标记 + String tag = getStatusTag(r); + if (!tag.isEmpty()) sb.append(tag).append("\n"); + + sb.append("订单号: #").append(r.orderId).append("\n"); + sb.append("时间: ").append( + df.format(new Date(r.timestamp))).append("\n"); + sb.append("玩家: ").append(r.player).append("\n"); + sb.append("------------------------------\n"); + + for (OrderItem it : r.items) { + String pr = (it.originalPrice != it.finalPrice) + ? it.originalPrice + "->" + it.finalPrice + : String.valueOf(it.finalPrice); + sb.append(it.name).append(" x").append(it.qty) + .append(" ").append(pr).append("枚") + .append(" 小计").append(it.subtotal()) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("原价: ").append(r.totalOriginal) + .append("枚\n"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "优惠券(-" + pct + "%)" + : "环保(-" + pct + "%)"; + sb.append("优惠: -").append(r.discount) + .append("枚 ").append(dt).append("\n"); + } + + if (r.packFee > 0) { + sb.append("打包: +").append(r.packFee) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("实付: ").append(r.totalPaid).append("枚\n"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + sb.append("余额: ").append(bal).append("枚\n"); + + // 退款标记 + if (!statusTag.isEmpty()) sb.append("\n").append(tag).append("\n"); + + return sb.toString(); + } + + /** + * 退款状态标记文本 + */ + private String getStatusTag(OrderRecord r) { + switch (r.status) { + case 2: + return "§c§l<<<<<<< 已全额退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 3: + return "§c§l<<<<<<< 已部分退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 4: + return "§c§l<<<<<<< 退款申请已拒绝 >>>>>>>"; + default: + return ""; + } + } + + /** + * 创建小票书 + */ + public ItemStack createReceiptBook(OrderRecord r) { + String text = buildReceiptText(r); + String[] lines = text.split("\n"); + + ItemStack book = new ItemStack(Material.WRITTEN_BOOK); + BookMeta bm = (BookMeta) book.getItemMeta(); + bm.setTitle("购物小票 #" + r.orderId); + bm.setAuthor("商店"); + + StringBuilder page = new StringBuilder(); + int lineCount = 0; + for (String line : lines) { + // ★ 保留颜色代码 + page.append(line).append("\n"); + lineCount++; + if (lineCount >= 13) { + bm.addPage(page.toString().trim()); + page = new StringBuilder(); + lineCount = 0; + } + } + if (page.length() > 0) { + bm.addPage(page.toString().trim()); + } + + book.setItemMeta(bm); + return book; + } + + // ===== 退款工具 ===== + private boolean withinSelfRefundWindow(OrderRecord r) { return System.currentTimeMillis() - r.timestamp < SELF_REFUND_WINDOW; } + private int countDirectItems(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private int countShulkers(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private void removeItems(Player p, String matName, int amt) { + Material m = Material.matchMaterial(matName); if (m != null) p.getInventory().removeItem(new ItemStack(m, amt)); + } + private boolean removeOneShulker(Player p, + String materialName) { + // 先尝试精确匹配 + Material mat = Material.matchMaterial(materialName); + if (mat != null) { + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + return true; + } + } + } + // 匹配不到,移除任意一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .endsWith("_SHULKER_BOX")) { + p.getInventory().clear(i); + return true; + } + } + return false; + } + public String validateRefund(OrderRecord r, Player p) { + if (r.status != 0) + return "订单状态: " + getStatusName(r.status); + + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // ★ 检查:潜影盒是否被放置过 + if (shulkerUsedOrders.contains(r.orderId)) { + return "§c潜影盒已被使用过,无法自助退款"; + } + + // ★ 检查:背包里是否有同一订单的潜影盒 + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im == null || im.getLore() == null) + continue; + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + return null; // 找到了,可退 + } + } + } + return "§c对应的潜影盒已不在背包中"; + } else { + // ★ 散装:检测物品数量 + for (OrderItem it : r.items) { + int have = countDirectItems(p, it.mat); + if (have < it.qty) + return "§c" + it.name + "不足(有" + + have + "/" + it.qty + ")"; + } + } + return null; + } + + + + private void notifyPlayer(UUID uuid, String msg) { Player p = Bukkit.getPlayer(uuid); if (p != null && p.isOnline()) p.sendMessage(msg); } + private void notifyAdmins(Player p, OrderRecord r) { + String m = "§e[订单] " + p.getName() + " 申请退款 #" + r.orderId + " §6" + r.totalPaid + "枚"; + for (Player a : Bukkit.getOnlinePlayers()) if (isAdmin(a) && !a.equals(p)) a.sendMessage(m); + } + private String getStatusName(int s) { switch(s){case 0:return "正常";case 1:return "申请中";case 2:return "已退";case 3:return "部分退";case 4:return "已拒绝";default:return "未知";} } + private boolean isAdmin(Player p) { + if (p.isOp()) return true; + try { ConfigManager cm = plugin.getConfigMgr(); if (cm == null) return false; return p.hasPermission("sdf1.admin") || p.hasPermission("sdf1." + cm.adminTag); } catch (Exception e) { return p.hasPermission("sdf1.admin"); } + } + + // ===== 订单中心 ===== + public void openOrderCenter(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l订单中心"); + g.setItem(13, mkItem(Material.BOOK, "§e§l购买记录", "§7查看历史订单", "", "§a点击进入")); + if (isAdmin(p)) { g.setItem(31, mkItem(Material.NETHER_STAR, "§c§l管理员面板", "§7在线玩家购物记录", "§7支持退款操作", "", "§a点击进入")); + g.setItem(33, mkItem(Material.CRAFTING_TABLE, "§e§l小票配置", "§7配置小票模板样式", "", "§a点击进入")); } + g.setItem(49, mkItem(Material.ARROW, "§7返回商店")); + p.openInventory(g); + } + + // ===== 购买记录 ===== + public void openMyOrders(Player p, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l购买记录"); + List list = getPlayerAllOrders(p.getUniqueId()); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + userViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon; String st; String act; + if (r.status != 0) { icon = r.status == 1 ? Material.PAPER : Material.BARRIER; st = "§7状态: " + getStatusName(r.status); act = "§b左键: 补打小票"; } + else if (withinSelfRefundWindow(r)) { icon = Material.EMERALD; st = "§7状态: §a可退款(5分钟内)"; act = "§b左键: 补打小票\n§a右键: 退款退货\n§eShift+右键: 仅退款"; } + else { icon = Material.BOOK; st = "§7状态: §7已过自助退款期"; act = "§b左键: 补打小票\n§e右键: 申请退款退货\n§6Shift+右键: 申请仅退款"; } + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7原价: §f" + r.totalOriginal + "枚"); + if (r.discount > 0) lore.add("§7优惠: §a-" + r.discount + "枚"); + lore.add("§7实付: §6" + r.totalPaid + "枚"); lore.add(st); lore.add(""); + for (String line : act.split("\n")) lore.add(line); + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7暂无订单记录")); + addPageButtons(g, page, total); + p.openInventory(g); + } + + // ===== 管理员面板 ===== + public void openAdminPanel(Player p, int page) { + if (!isAdmin(p)) { p.sendMessage("§c权限不足"); return; } + Inventory g = Bukkit.createInventory(null, 54, "§c§l管理员订单面板"); + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int total = Math.max(1, (int) Math.ceil((double) online.length / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); adminViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, online.length); + // ★ 替换 openAdminPanel 中的物品创建部分 + for (int i = start; i < end; i++) { + int si = i - start; + if (si >= SLOTS.length) break; + Player t = online[i]; + List tl = getPlayerOrdersWithin( + t.getUniqueId(), RETENTION_30D); + int ts = 0; + for (OrderRecord r : tl) ts += r.totalPaid; + + ItemStack is = new ItemStack(Material.PLAYER_HEAD); + org.bukkit.inventory.meta.SkullMeta sm = + (org.bukkit.inventory.meta.SkullMeta) + is.getItemMeta(); + if (sm != null) { + sm.setOwner(t.getName()); // ★ 设置头颅皮肤 + sm.setDisplayName("§e" + t.getName()); + sm.setLore(Arrays.asList( + "§7订单: §f" + tl.size() + "笔", + "§7消费: §6" + ts + "枚", + "", "§a点击进入")); + is.setItemMeta(sm); // ★ 这行之前缺失了 + } + g.setItem(SLOTS[si], is); + } + + if (online.length == 0) g.setItem(31, mkItem(Material.BARRIER, "§7无在线玩家")); + addPageButtons(g, page, total); p.openInventory(g); + } + + // ===== 管理员订单 ===== + public void openAdminOrders(Player p, UUID targetUUID, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§c§l订单管理"); + String name = Bukkit.getOfflinePlayer(targetUUID).getName(); + List list = getPlayerOrdersWithin(targetUUID, RETENTION_30D); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + adminViewPage.put(p.getUniqueId(), page); adminViewTarget.put(p.getUniqueId(), targetUUID); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon = r.status == 2 || r.status == 4 ? Material.BARRIER : r.status == 1 ? Material.PAPER : Material.BOOK; + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7实付: §6" + r.totalPaid + "枚"); + if (r.status == 1) { lore.add("§7状态: §e申请中(" + nn(r.refundType) + ")"); lore.add(""); lore.add("§a右键: 批准"); lore.add("§c左键: 拒绝"); } + else if (r.status == 2) lore.add("§7已退: §a" + r.refundedAmount + "枚"); + else if (r.status == 3) lore.add("§7部分退: §a" + r.refundedAmount + "枚"); + else if (r.status == 4) lore.add("§7已拒绝"); + else { lore.add(""); lore.add("§a右键: 整单退款"); lore.add("§e左键: 部分退款(输入%)"); } + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7该玩家暂无订单")); + addPageButtons(g, page, total); + g.setItem(49, mkItem(org.bukkit.Material.PAPER, name + " §e" + (page + 1) + "/" + total)); + p.openInventory(g); + } + private void addPageButtons(Inventory g, int page, int total) { + g.setItem(48, page > 0 ? mkItem(Material.ARROW, "§7上一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + if (g.getItem(49) == null) g.setItem(49, mkItem(Material.PAPER, "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 ? mkItem(Material.ARROW, "§7下一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, "§7返回")); + } + + // ===== 小票配置 ===== + public void openReceiptConfig(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l小票配置"); + g.setItem(10, mkItem(Material.PAPER, "§e§l头部", "§7" + brief(tplHeader), "", "§a点击编辑")); + g.setItem(11, mkItem(Material.PAPER, "§e§l商品行", "§7" + brief(tplItemLine), "", "§a点击编辑")); + g.setItem(12, mkItem(Material.PAPER, "§e§l优惠行", "§7" + brief(tplDiscountLine), "", "§a点击编辑")); + g.setItem(14, mkItem(Material.PAPER, "§e§l打包行", "§7" + brief(tplPackLine), "", "§a点击编辑")); + g.setItem(15, mkItem(Material.PAPER, "§e§l汇总", "§7" + brief(tplSummary), "", "§a点击编辑")); + g.setItem(16, mkItem(Material.PAPER, "§e§l页脚", "§7" + brief(tplFooter), "", "§a点击编辑")); + g.setItem(22, mkItem(Material.BOOK, "§6§l预览(随机价格)", "§7预览小票效果")); + g.setItem(48, mkItem(Material.LIME_WOOL, "§a§l保存")); g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(50, mkItem(Material.RED_WOOL, "§c§l重置")); + p.openInventory(g); + } + public void openReceiptBuilder(Player p, String section) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l编辑: " + sectionName(section)); + g.setItem(10, mkItem(Material.EMERALD, "§a{品名}")); g.setItem(11, mkItem(Material.EMERALD, "§a{原价}")); + g.setItem(12, mkItem(Material.EMERALD, "§a{执行价}")); g.setItem(13, mkItem(Material.EMERALD, "§a{数量}")); + g.setItem(14, mkItem(Material.EMERALD, "§a{小计}")); g.setItem(15, mkItem(Material.EMERALD, "§a{商店名}")); + g.setItem(16, mkItem(Material.EMERALD, "§a{玩家}")); g.setItem(19, mkItem(Material.DIAMOND, "§b{订单号}")); + g.setItem(20, mkItem(Material.DIAMOND, "§b{订单时间}")); g.setItem(21, mkItem(Material.DIAMOND, "§b{打印时间}")); + g.setItem(22, mkItem(Material.DIAMOND, "§b{日期}")); g.setItem(23, mkItem(Material.DIAMOND, "§b{时间}")); + g.setItem(24, mkItem(Material.DIAMOND, "§b{原价合计}")); g.setItem(25, mkItem(Material.DIAMOND, "§b{实付}")); + g.setItem(28, mkItem(Material.GOLD_INGOT, "§6{优惠额}")); g.setItem(29, mkItem(Material.GOLD_INGOT, "§6{优惠类型}")); + g.setItem(30, mkItem(Material.GOLD_INGOT, "§6{优惠券码}")); g.setItem(31, mkItem(Material.GOLD_INGOT, "§6{打包费}")); + g.setItem(32, mkItem(Material.GOLD_INGOT, "§6{打包颜色}")); g.setItem(33, mkItem(Material.GOLD_INGOT, "§6{余额}")); + g.setItem(37, mkItem(Material.STONE, "§f═══")); g.setItem(38, mkItem(Material.STONE, "§f───")); + g.setItem(39, mkItem(Material.STONE, "§f═════════════")); g.setItem(40, mkItem(Material.PAPER, "§e换行(\\n)")); + g.setItem(47, mkItem(Material.RED_STAINED_GLASS_PANE, "§c删末尾")); g.setItem(48, mkItem(Material.RED_WOOL, "§c清空")); + g.setItem(49, mkItem(Material.ARROW, "§7返回")); g.setItem(50, mkItem(Material.LIME_WOOL, "§a保存")); + g.setItem(51, mkItem(Material.BLUE_WOOL, "§9重置")); + p.openInventory(g); + } + // ===== 点击处理 ===== + public boolean handleCenterClick(Player p, int raw) { + if (raw == 13) { openMyOrders(p, 0); return true; } + if (raw == 31 && isAdmin(p)) { openAdminPanel(p, 0); return true; } + if (raw == 33 && isAdmin(p)) { openReceiptConfig(p); return true; } + if (raw == 49) { plugin.getShopManager().openShopMain(p); return true; } + return true; + } + public boolean handleMyOrdersClick(Player p, int raw, boolean left, boolean shift) { + List list = getPlayerAllOrders(p.getUniqueId()); + int page = userViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openMyOrders(p, page - 1); return true; } + if (raw == 50) { openMyOrders(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (left) { + try { ItemStack book = createReceiptBook(r); + HashMap ov = p.getInventory().addItem(book); + for (ItemStack d : ov.values()) p.getWorld().dropItemNaturally(p.getLocation(), d); + r.printTime = System.currentTimeMillis(); p.sendMessage("§a已补打小票书: #" + r.orderId); + } catch (Exception e) { p.sendMessage("§c补打失败"); } + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // ★ Shift+右键:仅退款申请(需管理员审批) + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请,等待管理员审批"); + notifyAdmins(p, r); + } else { + // ★ 普通右键:5分钟内直接退(自动通过) + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason + + ",Shift+右键可申请仅退款"); + } else { + // 扣货 + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + if (isShulker) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory() + .getItem(i); + if (is != null && is.getType() + .name().contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + // 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + } + + + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // 仅退款申请 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } else { + // 5分钟内退货退款 + String reason = validateRefund(r, p); + if (reason != null) { + // ★ 不满足条件:询问是否申请 + p.sendMessage("§c" + reason); + askReturnConfirm(p, r); + } else { + // 满足条件:自动退 + doAutoReturn(p, r); + } + } + } else { + if (shift) { + // 申请仅退款 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请"); + notifyAdmins(p, r); + } else { + // ★ 申请退货退款:先检查条件 + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason); + } + // ★ 无论是否满足条件都询问 + askReturnConfirm(p, r); + } + } + } + } + if (left) { + // 已退款订单仍然允许补打小票,但小票内会显示退款标记 + try { + ItemStack book = createReceiptBook(r); + HashMap overflow = + p.getInventory().addItem(book); + for (ItemStack drop : overflow.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + r.printTime = System.currentTimeMillis(); + if (r.status != 0) { + p.sendMessage("§e已补打小票(含退款标记): #" + + r.orderId); + } else { + p.sendMessage("§a已补打小票书: #" + + r.orderId); + } + } catch (Exception e) { + p.sendMessage("§c补打失败"); + } + } + + final int fp = page; Bukkit.getScheduler().runTask(plugin, () -> openMyOrders(p, fp)); + return true; + } + public boolean handleAdminClick(Player p, int raw) { + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openAdminPanel(p, page - 1); return true; } + if (raw == 50) { openAdminPanel(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= online.length) return true; + openAdminOrders(p, online[actual].getUniqueId(), 0); return true; + } + public boolean handleAdminOrdersClick(Player p, int raw, boolean left, boolean shift) { + UUID tu = adminViewTarget.get(p.getUniqueId()); + if (tu == null) return false; + List list = getPlayerOrdersWithin(tu, RETENTION_30D); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { + openAdminOrders(p, tu, page - 1); + return true; + } + if (raw == 50) { + openAdminOrders(p, tu, page + 1); + return true; + } + if (raw == 53) { + openAdminPanel(p, 0); + return true; + } + int si = slotIndex(raw); + if (si < 0) return true; + int actual = si + page * PAGE_SIZE; + if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (r.status == 2 || r.status == 3 || r.status == 4) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + return true; + } + if (left) { + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款被拒绝"); + openAdminOrders(p, tu, page); + } else if (r.status == 0) { + partialRefundListening.add(p.getUniqueId()); + partialRefundTarget.put(p.getUniqueId(), r); + p.closeInventory(); + p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚"); + } + } else { + if (r.status == 0 || r.status == 1) { + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a全额退款 §e" + r.player + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, "§a退款 §6" + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + } + } + // ★ 替换 handleAdminOrdersClick 中的审批逻辑 + if (left) { + // 左键 = 拒绝 + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款申请已被拒绝"); + openAdminOrders(p, tu, page); + } + } else { + // 右键 = 批准 + if (r.status == 1) { + String rt = r.refundType != null + ? r.refundType : ""; + + // ★ 退款退货申请 → 检查潜影盒 + if (rt.equals("refund_return_apply")) { + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 检查玩家背包里有没有带订单标签的盒子 + Player target = Bukkit.getPlayer(r.uuid); + if (target == null || !target.isOnline()) { + p.sendMessage("§c玩家不在线,无法执行退货退款"); + openAdminOrders(p, tu, page); + return true; + } + + boolean foundTagged = false; + boolean foundAny = false; + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is == null) continue; + if (!is.getType().name() + .contains("SHULKER")) continue; + foundAny = true; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + foundTagged = true; + // 收走盒子 + target.getInventory().clear(i); + break; + } + } + } + if (foundTagged) break; + } + + if (foundTagged) { + // ★ 有带标记的盒子 → 收走 + 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + + r.player + " §6" + + r.totalPaid + "枚(盒子已收走)"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + return true; + } else if (foundAny) { + // ★ 有盒子但没标记 → 等管理员决定 + pendingReturns.put(r.orderId, + new PendingReturn(r)); + p.closeInventory(); + p.sendMessage("§e§l[退货审批] 订单 #" + + r.orderId); + p.sendMessage("§7玩家背包有潜影盒但" + + "无订单标记,可能已被替换"); + p.sendMessage("§a输入 §e1 §a= 收走盒子并退款"); + p.sendMessage("§e输入 §e2 §e= 仅退款(不收盒子)"); + p.sendMessage("§c输入 §c3 §c= 拒绝退款"); + p.sendMessage("§72分钟内未操作自动拒绝"); + // 启动2分钟超时 + startReturnTimeout(r.orderId); + return true; + } else { + // ★ 背包里没有任何盒子 + p.sendMessage("§c玩家背包里没有潜影盒"); + p.sendMessage("§7可拒绝并通知玩家" + + "申请仅退款或把货拿回来"); + openAdminOrders(p, tu, page); + return true; + } + } + // ★ 散装退货退款:扣货+退钱 + Player target = Bukkit.getPlayer(r.uuid); + if (target != null && target.isOnline()) { + for (OrderItem it : r.items) + removeItems(target, it.mat, it.qty); + } + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + return true; + } + + // ★ 仅退款申请 / 5分钟内自动退款 + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已批准退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退款已批准,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + } + } + return true; + } + + /** + * 询问用户是否提交退货退款申请 + */ + private void askReturnConfirm(Player p, OrderRecord r) { + returnConfirmWaiting.put(p.getUniqueId(), r.orderId); + p.sendMessage(""); + p.sendMessage("§e§l[退货退款申请]"); + p.sendMessage("§7订单 #" + r.orderId + + " 实付" + r.totalPaid + "枚"); + p.sendMessage("§a输入 §e1 §a= 提交退货退款申请"); + p.sendMessage("§c输入 §e2 §c= 取消"); + p.sendMessage("§730秒内未操作自动取消"); + p.sendMessage(""); + + // 30秒超时 + final UUID uuid = p.getUniqueId(); + final long oid = r.orderId; + new BukkitRunnable() { + @Override + public void run() { + Long waiting = returnConfirmWaiting.get(uuid); + if (waiting != null && waiting == oid) { + returnConfirmWaiting.remove(uuid); + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§7退货退款申请已超时取消"); + } + } + } + }.runTaskLater(plugin, RETURN_CONFIRM_TIMEOUT); + } + + /** + * 执行5分钟内自动退货退款 + */ + private void doAutoReturn(Player p, OrderRecord r) { + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 找并收走带标签的盒子 + boolean found = false; + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + p.getInventory().clear(i); + found = true; + break; + } + } + } + if (found) break; + } + // 没找到标签盒子,收任意潜影盒 + if (!found) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + + + // ===== 2分钟超时 ===== + private void startReturnTimeout(long orderId) { + new BukkitRunnable() { + @Override + public void run() { + PendingReturn pr = pendingReturns + .remove(orderId); + if (pr == null) return; // 已处理 + Player admin = Bukkit.getPlayer(pr.playerUUID); + if (admin != null && admin.isOnline()) { + admin.sendMessage("§c[退货审批] 订单 #" + + orderId + " 超时,自动拒绝"); + } + // 通知玩家 + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款申请已超时拒绝"); + // 恢复订单状态 + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 0; + r.refundType = null; + saveOrders(); + } + } + }.runTaskLater(plugin, 2400L); // 2分钟 + } + + // ===== 查找订单 ===== + private OrderRecord findOrder(long orderId) { + synchronized (orders) { + for (OrderRecord r : orders) { + if (r.orderId == orderId) return r; + } + } + return null; + } + + + public boolean handleReceiptConfigClick(Player p, int raw) { + if (raw == 49) { openOrderCenter(p); return true; } + if (raw == 48) { saveReceiptConfig(); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 50) { createDefaultReceiptConfig(new File(plugin.getDataFolder(), "小票设置.txt")); loadReceiptConfig(); p.sendMessage("§e已重置"); openReceiptConfig(p); return true; } + if (raw == 22) { sendRandomPreview(p); return true; } + String sec = null; + if (raw == 10) sec = "header"; else if (raw == 11) sec = "itemLine"; else if (raw == 12) sec = "discountLine"; + else if (raw == 14) sec = "packLine"; else if (raw == 15) sec = "summary"; else if (raw == 16) sec = "footer"; + if (sec != null) { receiptBuilderSection.put(p.getUniqueId(), sec); openReceiptBuilder(p, sec); } + return true; + } + public boolean handleReceiptBuilderClick(Player p, int raw) { + String sec = receiptBuilderSection.get(p.getUniqueId()); if (sec == null) return false; + String token = getTokenBySlot(raw); + if (token != null) { String cur = getSection(sec); setSection(sec, cur.isEmpty() ? token : cur + token); openReceiptBuilder(p, sec); return true; } + if (raw == 47) { String c = getSection(sec); int lb = c.lastIndexOf('}'); if (lb >= 0) { int ls = c.lastIndexOf('{', lb); if (ls >= 0) setSection(sec, c.substring(0, ls)); } else setSection(sec, c.substring(0, Math.max(0, c.length() - 8))); openReceiptBuilder(p, sec); return true; } + if (raw == 48) { setSection(sec, ""); openReceiptBuilder(p, sec); return true; } + if (raw == 49) { receiptBuilderSection.remove(p.getUniqueId()); openReceiptConfig(p); return true; } + if (raw == 50) { saveReceiptConfig(); receiptBuilderSection.remove(p.getUniqueId()); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 51) { setSection(sec, getDefaultVal(sec)); openReceiptBuilder(p, sec); return true; } + return true; + } + + // ===== 聊天事件 ===== + @EventHandler + public void onAdminChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); UUID uuid = p.getUniqueId(); + // ★ 在 onAdminChat 方法开头添加 +// ===== 退货审批输入 ===== + for (Map.Entry entry : + new HashMap<>(pendingReturns).entrySet()) { + Long pendingOrderId = entry.getKey(); + PendingReturn pr = entry.getValue(); + if (pr.playerUUID.equals(p.getUniqueId()) + || isAdmin(p)) { + try { + int choice = Integer.parseInt( + e.getMessage().trim()); + pendingReturns.remove(pendingOrderId); + OrderRecord r = findOrder(pendingOrderId); + if (r == null) { + p.sendMessage("§c订单不存在"); + return; + } + + if (choice == 1) { + // ★ 收走盒子并退款 + Player target = Bukkit.getPlayer( + pr.playerUUID); + if (target != null && target.isOnline()) { + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + pendingOrderId)) { + target.getInventory().clear(i); + break; + } + } + } + break; + } + } + } + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(盒子已收走)"); + notifyPlayer(pr.playerUUID, + "§a[订单] 退货退款成功,§6" + + pr.totalPaid + "枚已退还"); + + } else if (choice == 2) { + // ★ 仅退款(不收盒子) + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§e已仅退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(未收盒子)"); + notifyPlayer(pr.playerUUID, + "§e[订单] 仅退款成功,§6" + + pr.totalPaid + "枚已退还" + + ",盒子无需退回"); + + } else if (choice == 3) { + // ★ 拒绝 + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款 #" + pendingOrderId); + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款已被拒绝" + + ",您可以申请仅退款" + + "或把货拿回来后重新申请"); + } else { + p.sendMessage("§c请输入 1、2 或 3"); + pendingReturns.put(pendingOrderId, pr); + } + return; + + } catch (NumberFormatException ignored) { + // 不是数字,跳过 + } + } + } +// ★ 在 onAdminChat 方法开头,处理用户退货确认 + if (returnConfirmWaiting.containsKey(uuid)) { + String msg = e.getMessage().trim(); + if (msg.equals("1")) { + // ★ 确认提交 + Long orderId = returnConfirmWaiting.remove(uuid); + if (orderId != null) { + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 1; + r.refundType = "refund_return_apply"; + saveOrders(); + p.sendMessage("§e已提交退货退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } + } + e.setCancelled(true); + return; + } else if (msg.equals("2")) { + // ★ 取消 + returnConfirmWaiting.remove(uuid); + p.sendMessage("§7已取消退货退款申请"); + e.setCancelled(true); + return; + } else { + // 输入其他内容:取消等待 + returnConfirmWaiting.remove(uuid); + // 不 return,让消息正常发送 + } + } + + if (partialRefundListening.contains(uuid)) { + e.setCancelled(true); partialRefundListening.remove(uuid); + OrderRecord t = partialRefundTarget.remove(uuid); if (t == null) return; + try { int pct = Integer.parseInt(e.getMessage().trim()); if (pct < 1 || pct > 100) { p.sendMessage("§c1-100"); return; } + int ref = t.totalPaid * pct / 100; if (ref <= 0) ref = 1; + plugin.getBonds().addBonds(t.player, ref); t.status = 3; t.refundedAmount = ref; saveOrders(); + p.sendMessage("§a部分退款 §e" + t.player + " §6" + ref + "枚(" + pct + "%)"); + notifyPlayer(t.uuid, "§a退款 §6" + ref + "枚"); + final UUID tu = adminViewTarget.get(uuid); final int pg = adminViewPage.getOrDefault(uuid, 0); + Bukkit.getScheduler().runTask(plugin, () -> openAdminOrders(p, tu, pg)); + } catch (NumberFormatException ex) { p.sendMessage("§c请输入数字"); } catch (Exception ex) { plugin.getLogger().log(Level.WARNING, "[Order]退款异常", ex); } + return; + } + if (receiptBuilderSection.containsKey(uuid)) { + e.setCancelled(true); String sec = receiptBuilderSection.remove(uuid); if (sec == null) return; + String msg = e.getMessage().trim(); + if ("cancel".equalsIgnoreCase(msg)) { Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); return; } + setSection(sec, msg.replace("\\n", "\n")); p.sendMessage("§a已更新 §e" + sec); + Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); + } + } + + // ===== 数据查询 ===== + private List getPlayerAllOrders(UUID uuid) { + List r = new ArrayList<>(); + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && System.currentTimeMillis() - o.timestamp < RETENTION_30D) r.add(o); } + Collections.reverse(r); return r; + } + private List getPlayerOrdersWithin(UUID uuid, long ms) { + List r = new ArrayList<>(); long cut = System.currentTimeMillis() - ms; + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && o.timestamp >= cut) r.add(o); } + Collections.reverse(r); return r; + } + + // ===== 持久化 ===== + private void loadOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); if (!f.exists()) return; + try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)); + OrderRecord cur = null; String line; + while ((line = br.readLine()) != null) { line = line.trim(); + if (line.equals("==")) { cur = new OrderRecord(); cur.items = new ArrayList<>(); } + else if (cur != null && line.contains("=")) { int eq = line.indexOf('='); String k = line.substring(0, eq); String v = line.substring(eq + 1); + switch (k) { case "id": cur.orderId = Long.parseLong(v); cur.timestamp = cur.orderId; break; case "uuid": cur.uuid = UUID.fromString(v); break; + case "player": cur.player = v; break; case "totalOriginal": cur.totalOriginal = Integer.parseInt(v); break; case "totalPaid": cur.totalPaid = Integer.parseInt(v); break; + case "discount": cur.discount = Integer.parseInt(v); break; case "discountType": cur.discountType = v; break; case "couponCode": cur.couponCode = v; break; + case "packFee": cur.packFee = Integer.parseInt(v); break; case "packType": cur.packType = v; break; case "packColor": cur.packColor = v; break; + case "status": cur.status = Integer.parseInt(v); break; case "refundedAmount": cur.refundedAmount = Integer.parseInt(v); break; + case "printTime": cur.printTime = Long.parseLong(v); break; case "refundType": cur.refundType = v; break; + case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) cur.items.add(new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4]))); break; } + } else if (line.isEmpty() && cur != null && cur.uuid != null) { orders.add(cur); cur = null; } + } if (cur != null && cur.uuid != null) orders.add(cur); br.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); } + } + private void saveOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); + try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); + synchronized (orders) { for (OrderRecord r : orders) { pw.println("=="); + pw.println("id=" + r.orderId); pw.println("uuid=" + r.uuid); pw.println("player=" + r.player); + pw.println("totalOriginal=" + r.totalOriginal); pw.println("totalPaid=" + r.totalPaid); + pw.println("discount=" + r.discount); pw.println("discountType=" + nn(r.discountType)); + pw.println("couponCode=" + nn(r.couponCode)); pw.println("packFee=" + r.packFee); + pw.println("packType=" + nn(r.packType)); pw.println("packColor=" + nn(r.packColor)); + pw.println("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount); + pw.println("printTime=" + r.printTime); pw.println("refundType=" + nn(r.refundType)); + for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty); + } } pw.flush(); pw.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); } + } + private void startCleanupTask() { new BukkitRunnable() { public void run() { synchronized (orders) { if (orders.removeIf(OrderManager.this::isExpired)) saveOrders(); } } }.runTaskTimer(plugin, 36000L, 36000L); } + /** + * 将小票书塞入潜影盒 + */ + public void addBookToShulker(ItemStack shulker, ItemStack book) { + try { + org.bukkit.inventory.meta.BlockStateMeta bm = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (bm == null) return; + org.bukkit.block.BlockState st = bm.getBlockState(); + if (st instanceof org.bukkit.block.Container) { + ((org.bukkit.block.Container) st) + .getInventory().addItem(book); + bm.setBlockState(st); + shulker.setItemMeta(bm); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Order] 塞书失败: " + e.getMessage()); + } + } + + // ===== 工具 ===== + private String nn(String s) { return s != null ? s : ""; } + private boolean isExpired(OrderRecord r) { return System.currentTimeMillis() - r.timestamp > RETENTION_30D; } + private String fmtDate(long ts) { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ts)); } + private int slotIndex(int raw) { for (int i = 0; i < SLOTS.length; i++) if (SLOTS[i] == raw) return i; return -1; } + private String brief(String s) { if (s == null || s.isEmpty()) return "(空)"; String c = sc(s); return c.length() > 30 ? c.substring(0, 30) + "..." : c; } + private String sectionName(String s) { switch(s){case "header":return "头部";case "itemLine":return "商品行";case "discountLine":return "优惠行";case "packLine":return "打包行";case "summary":return "汇总";case "footer":return "页脚";default:return s;} } + private String sc(String s) { + if (s == null) return ""; + return s.replaceAll("§[0-9a-fk-orA-FK-OR]", ""); + } + private String getSection(String s) { switch(s){case "header":return tplHeader;case "itemLine":return tplItemLine;case "discountLine":return tplDiscountLine;case "packLine":return tplPackLine;case "summary":return tplSummary;case "footer":return tplFooter;default:return "";} } + private void setSection(String s, String v) { switch(s){case "header":tplHeader=v;break;case "itemLine":tplItemLine=v;break;case "discountLine":tplDiscountLine=v;break;case "packLine":tplPackLine=v;break;case "summary":tplSummary=v;break;case "footer":tplFooter=v;break;} } + private String getDefaultVal(String s) { switch(s){case "header":return "§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}";case "itemLine":return "§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚";case "discountLine":return "优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})";case "packLine":return "打包: §c+{打包费}枚§r ({打包颜色})";case "summary":return "原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚";case "footer":return "感谢惠顾,欢迎再来!";default:return "";} } + private String getTokenBySlot(int raw) { switch(raw){case 10:return "{品名}";case 11:return "{原价}";case 12:return "{执行价}";case 13:return "{数量}";case 14:return "{小计}";case 15:return "{商店名}";case 16:return "{玩家}";case 19:return "{订单号}";case 20:return "{订单时间}";case 21:return "{打印时间}";case 22:return "{日期}";case 23:return "{时间}";case 24:return "{原价合计}";case 25:return "{实付}";case 28:return "{优惠额}";case 29:return "{优惠类型}";case 30:return "{优惠券码}";case 31:return "{打包费}";case 32:return "{打包颜色}";case 33:return "{余额}";case 37:return "═══";case 38:return "───";case 39:return "═════════════";case 40:return "\\n";default:return null;} } + 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; } + private Map loadMap(File f) { Map m = new LinkedHashMap<>(); try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)); String line; while ((line = br.readLine()) != null) { line = line.trim(); if (line.isEmpty() || line.startsWith("#")) continue; int eq = line.indexOf('='); if (eq > 0) m.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); } br.close(); } catch (Exception ignored) {} return m; } + private void writeLines(File f, List lines) { try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); for (String l : lines) pw.println(l); pw.flush(); pw.close(); } catch (Exception ignored) {} } + private void sendRandomPreview(Player p) { String[] n = {"钻石剑","钻石镐","铁剑","金苹果"}; String[] m = {"DIAMOND_SWORD","DIAMOND_PICKAXE","IRON_SWORD","GOLDEN_APPLE"}; Random r = new Random(); List items = new ArrayList<>(); for (int i = 0; i < 2 + r.nextInt(3); i++) { int idx = r.nextInt(n.length); items.add(new OrderItem(n[idx], m[idx], 100 + r.nextInt(5000), 100 + r.nextInt(5000), 1 + r.nextInt(10))); } OrderRecord o = new OrderRecord(); o.orderId = System.currentTimeMillis(); o.uuid = p.getUniqueId(); o.player = p.getName(); o.items = items; o.totalOriginal = 0; for (OrderItem it : items) o.totalOriginal += it.originalSubtotal(); o.totalPaid = o.totalOriginal; o.timestamp = o.orderId; o.status = 0; p.sendMessage("§6§l===== 小票预览(随机) ====="); sendReceiptChat(p, o); p.sendMessage("§6§l===== 预览结束 ====="); } +} diff --git a/src/main/java/Sdf1_login/PasswordUtils.java b/src/main/java/Sdf1_login/PasswordUtils.java index 910a63f..4029ed4 100644 --- a/src/main/java/Sdf1_login/PasswordUtils.java +++ b/src/main/java/Sdf1_login/PasswordUtils.java @@ -1,72 +1,72 @@ -package Sdf1_login; - -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Base64; - -public class PasswordUtils { - - private static final SecureRandom RANDOM = - new SecureRandom(); - - public static String generateSalt() { - byte[] salt = new byte[16]; - RANDOM.nextBytes(salt); - return Base64.getEncoder().encodeToString(salt); - } - - public static String hash(String password, String salt) { - try { - MessageDigest md = - MessageDigest.getInstance("SHA-256"); - md.update(Base64.getDecoder().decode(salt)); - byte[] hash = - md.digest(password.getBytes("UTF-8")); - return Base64.getEncoder().encodeToString(hash); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static boolean validate(String password) { - if (password == null) return false; - if (password.length() < 6 || password.length() > 20) - return false; - boolean hasUpper = false; - boolean hasLower = false; - boolean hasDigit = false; - for (char c : password.toCharArray()) { - if (Character.isUpperCase(c)) hasUpper = true; - if (Character.isLowerCase(c)) hasLower = true; - if (Character.isDigit(c)) hasDigit = true; - } - return hasUpper && hasLower && hasDigit; - } - - public static String generateTempPassword() { - String chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - StringBuilder sb = new StringBuilder(8); - for (int i = 0; i < 8; i++) { - sb.append(chars.charAt( - RANDOM.nextInt(chars.length()))); - } - return sb.toString(); - } - - public static String generateInviteCode() { - String chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - StringBuilder sb = new StringBuilder(6); - for (int i = 0; i < 6; i++) { - sb.append(chars.charAt( - RANDOM.nextInt(chars.length()))); - } - return sb.toString(); - } - - public static String generateVerifyCode() { - int code = 100000 + RANDOM.nextInt(900000); - return String.valueOf(code); - } -} +package Sdf1_login; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordUtils { + + private static final SecureRandom RANDOM = + new SecureRandom(); + + public static String generateSalt() { + byte[] salt = new byte[16]; + RANDOM.nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + public static String hash(String password, String salt) { + try { + MessageDigest md = + MessageDigest.getInstance("SHA-256"); + md.update(Base64.getDecoder().decode(salt)); + byte[] hash = + md.digest(password.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean validate(String password) { + if (password == null) return false; + if (password.length() < 6 || password.length() > 20) + return false; + boolean hasUpper = false; + boolean hasLower = false; + boolean hasDigit = false; + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpper = true; + if (Character.isLowerCase(c)) hasLower = true; + if (Character.isDigit(c)) hasDigit = true; + } + return hasUpper && hasLower && hasDigit; + } + + public static String generateTempPassword() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(8); + for (int i = 0; i < 8; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateInviteCode() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + StringBuilder sb = new StringBuilder(6); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateVerifyCode() { + int code = 100000 + RANDOM.nextInt(900000); + return String.valueOf(code); + } +} diff --git a/src/main/java/Sdf1_login/PointsManager.java b/src/main/java/Sdf1_login/PointsManager.java index d7dd0ca..77fc195 100644 --- a/src/main/java/Sdf1_login/PointsManager.java +++ b/src/main/java/Sdf1_login/PointsManager.java @@ -1,374 +1,374 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - - -import java.lang.reflect.Method; -import java.util.*; - -public class PointsManager { - - private final Main plugin; - private Object cyInstance = null; - private Method cyActivationMethod = null; - private Method cyGetShopItemsMethod = null; - - private final List> cyItems = - new ArrayList<>(); - - public PointsManager(Main plugin) { - this.plugin = plugin; - discoverCY(); - } - - private void discoverCY() { - try { - org.bukkit.plugin.Plugin p = Bukkit - .getPluginManager() - .getPlugin("CY_beibao"); - if (p != null && p.isEnabled()) { - cyInstance = p; - cyGetShopItemsMethod = p.getClass() - .getMethod("onSdf1GetShopItems"); - cyActivationMethod = p.getClass() - .getMethod("onSdf1Activation", - String.class, - int.class, int.class); - plugin.getLogger().info( - "[Sdf1_login] CY_beibao 联控已连接"); - } - } catch (Exception ignored) { - } - } - - private void refreshCYItems() { - cyItems.clear(); - if (cyInstance == null - || cyGetShopItemsMethod == null) return; - try { - Object result = - cyGetShopItemsMethod.invoke(cyInstance); - if (result == null) return; - String data = result.toString(); - if (data.isEmpty()) return; - String[] items = data.split(";"); - for (String itemStr : items) { - String[] p = itemStr.split("\\|"); - if (p.length < 8) continue; - Map item = - new LinkedHashMap<>(); - item.put("id", p[0]); - item.put("name", p[1]); - item.put("days", - Integer.parseInt(p[2])); - item.put("slots", - Integer.parseInt(p[3])); - item.put("price", - Integer.parseInt(p[4])); - item.put("stock", - Integer.parseInt(p[5])); - item.put("icon", p[6]); - item.put("lifetime", - "1".equals(p[7])); - cyItems.add(item); - } - } catch (Exception e) { - plugin.getLogger().warning( - "[Sdf1_login] 拉取CY商品失败: " - + e.getMessage()); - } - } - - private void activateCY(String name, - int slots, int days) { - try { - if (cyInstance == null - || cyActivationMethod == null) return; - cyActivationMethod.invoke( - cyInstance, name, slots, days); - } catch (Exception e) { - plugin.getLogger().warning( - "[Sdf1_login] CY激活失败: " - + e.getMessage()); - } - } - - public int getPoints(String name) { - return ((Number) plugin.getDb() - .getField(name, "points")).intValue(); - } - - public Inventory createShopGUI(Player p) { - int points = getPoints(p.getName()); - Inventory inv = Bukkit.createInventory( - null, 54, - "§d§l积分商城 | 当前积分: " + points); - fillBg(inv); - inv.setItem(10, mkItem(Material.EMERALD, - "§a游戏币充值", - "§7消耗积分兑换游戏币", - "§7100积分 = $1000", - "", - "§e点击兑换")); - inv.setItem(12, mkItem(Material.ENDER_CHEST, - "§b背包空间", - "§7消耗积分兑换背包格子", - "§750积分 = 9格永久空间", - "", - "§e点击兑换")); - inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE, - "§e经验瓶", - "§7消耗积分兑换经验", - "§720积分 = 10级经验", - "", - "§e点击兑换")); - if (cyInstance != null) { - inv.setItem(31, mkItem(Material.CHEST, - "§6背包商城", - "§7来自CY_beibao的商品", - "", - "§e点击查看")); - } - inv.setItem(49, mkItem(Material.ARROW, "§7返回")); - return inv; - } - - public Inventory createCYShopGUI(Player p) { - refreshCYItems(); - int points = getPoints(p.getName()); - int size = Math.max(54, - ((cyItems.size() / 9) + 1) * 9); - if (size > 54) size = 54; - Inventory inv = Bukkit.createInventory( - null, size, - "§d§lCY背包商城 | 当前积分: " + points); - fillBg(inv); - for (int i = 0; i < cyItems.size() - && i < 45; i++) { - Map item = cyItems.get(i); - String name = (String) item.get("name"); - int price = ((Number) item.get("price")) - .intValue(); - int stock = ((Number) item.get("stock")) - .intValue(); - int days = ((Number) item.get("days")) - .intValue(); - int slots = ((Number) item.get("slots")) - .intValue(); - boolean lifetime = (boolean) item - .get("lifetime"); - - List lore = new ArrayList<>(); - lore.add("§7价格: §e" + price + " 积分"); - if (lifetime) { - lore.add("§7格子: §e终身 +" + slots); - } else { - lore.add("§7格子: §e+" + slots); - lore.add("§7时间: §e" + days + "天"); - } - if (stock == 0) { - lore.add("§7库存: §a无限"); - } else if (stock == -1) { - lore.add("§7库存: §c已下架"); - } else { - lore.add("§7库存: §e" + stock); - } - lore.add(""); - if (points >= price && stock != -1) { - lore.add("§e点击购买"); - } else if (stock == -1) { - lore.add("§c已下架"); - } else { - lore.add("§c积分不足"); - } - inv.setItem(i, mkItem(Material.CHEST, - "§a" + name, - lore.toArray(new String[0]))); - } - inv.setItem(size - 1, - mkItem(Material.ARROW, "§7返回")); - return inv; - } - - public void addItem(String name, - int price, String cmd2) { - // 追加到商品配置文件 - File file = new File( - plugin.getDataFolder(), - "ShopItems.txt"); - try { - java.io.FileWriter fw = - new java.io.FileWriter(file, true); - fw.write(name + ":" + price - + ":" + cmd2 + "\n"); - fw.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void removeItem(int index) { - File file = new File( - plugin.getDataFolder(), - "ShopItems.txt"); - try { - java.util.List lines = - new java.util.ArrayList<>(); - java.io.BufferedReader br = - new java.io.BufferedReader( - new java.io.FileReader( - file)); - String line; - int i = 0; - while ((line = br.readLine()) != null) { - if (line.trim().isEmpty() - || line.startsWith("#")) { - lines.add(line); - continue; - } - if (i != index) { - lines.add(line); - } - i++; - } - br.close(); - java.io.PrintWriter pw = - new java.io.PrintWriter(file); - for (String l : lines) pw.println(l); - pw.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - public void handleClick(Player p, int slot) { - int points = getPoints(p.getName()); - switch (slot) { - case 10: - if (points >= 100) { - plugin.getDb().deductPoints( - p.getName(), 100); - if (plugin.getEconomy() != null) { - plugin.getEconomy() - .depositPlayer(p, 1000); - } - p.sendMessage(plugin.getConfig2() - .msg("points_purchase_success") - .replace("{count}", "100")); - } else { - p.sendMessage(plugin.getConfig2() - .msg("points_insufficient") - .replace("{points}", - String.valueOf(points))); - } - break; - case 12: - if (points >= 50) { - plugin.getDb().deductPoints( - p.getName(), 50); - activateCY(p.getName(), 9, 0); - p.sendMessage(plugin.getConfig2() - .msg("points_purchase_success") - .replace("{count}", "50")); - } else { - p.sendMessage(plugin.getConfig2() - .msg("points_insufficient") - .replace("{points}", - String.valueOf(points))); - } - break; - case 14: - if (points >= 20) { - plugin.getDb().deductPoints( - p.getName(), 20); - p.setLevel(p.getLevel() + 10); - p.sendMessage(plugin.getConfig2() - .msg("points_purchase_success") - .replace("{count}", "20")); - } else { - p.sendMessage(plugin.getConfig2() - .msg("points_insufficient") - .replace("{points}", - String.valueOf(points))); - } - break; - case 31: - if (cyInstance != null) { - p.openInventory( - createCYShopGUI(p)); - } - break; - } - } - - public void handleCYClick(Player p, int slot) { - if (slot < 0 || slot >= cyItems.size() - || slot >= 45) return; - Map item = cyItems.get(slot); - int price = ((Number) item.get("price")) - .intValue(); - int stock = ((Number) item.get("stock")) - .intValue(); - int days = ((Number) item.get("days")) - .intValue(); - int slots = ((Number) item.get("slots")) - .intValue(); - boolean lifetime = (boolean) item - .get("lifetime"); - - if (stock == -1) { - p.sendMessage("§c该商品已下架"); - return; - } - int points = getPoints(p.getName()); - if (points < price) { - p.sendMessage(plugin.getConfig2() - .msg("points_insufficient") - .replace("{points}", - String.valueOf(points))); - return; - } - - plugin.getDb().deductPoints(p.getName(), price); - int actDays = lifetime ? 0 : days; - activateCY(p.getName(), slots, actDays); - - String tl = lifetime - ? "终身" : (days + "天"); - p.sendMessage("§a购买成功!获得 " - + tl + " +" + slots + "格 背包空间"); - } - - 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.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + + +import java.lang.reflect.Method; +import java.util.*; + +public class PointsManager { + + private final Main plugin; + private Object cyInstance = null; + private Method cyActivationMethod = null; + private Method cyGetShopItemsMethod = null; + + private final List> cyItems = + new ArrayList<>(); + + public PointsManager(Main plugin) { + this.plugin = plugin; + discoverCY(); + } + + private void discoverCY() { + try { + org.bukkit.plugin.Plugin p = Bukkit + .getPluginManager() + .getPlugin("CY_beibao"); + if (p != null && p.isEnabled()) { + cyInstance = p; + cyGetShopItemsMethod = p.getClass() + .getMethod("onSdf1GetShopItems"); + cyActivationMethod = p.getClass() + .getMethod("onSdf1Activation", + String.class, + int.class, int.class); + plugin.getLogger().info( + "[Sdf1_login] CY_beibao 联控已连接"); + } + } catch (Exception ignored) { + } + } + + private void refreshCYItems() { + cyItems.clear(); + if (cyInstance == null + || cyGetShopItemsMethod == null) return; + try { + Object result = + cyGetShopItemsMethod.invoke(cyInstance); + if (result == null) return; + String data = result.toString(); + if (data.isEmpty()) return; + String[] items = data.split(";"); + for (String itemStr : items) { + String[] p = itemStr.split("\\|"); + if (p.length < 8) continue; + Map item = + new LinkedHashMap<>(); + item.put("id", p[0]); + item.put("name", p[1]); + item.put("days", + Integer.parseInt(p[2])); + item.put("slots", + Integer.parseInt(p[3])); + item.put("price", + Integer.parseInt(p[4])); + item.put("stock", + Integer.parseInt(p[5])); + item.put("icon", p[6]); + item.put("lifetime", + "1".equals(p[7])); + cyItems.add(item); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 拉取CY商品失败: " + + e.getMessage()); + } + } + + private void activateCY(String name, + int slots, int days) { + try { + if (cyInstance == null + || cyActivationMethod == null) return; + cyActivationMethod.invoke( + cyInstance, name, slots, days); + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] CY激活失败: " + + e.getMessage()); + } + } + + public int getPoints(String name) { + return ((Number) plugin.getDb() + .getField(name, "points")).intValue(); + } + + public Inventory createShopGUI(Player p) { + int points = getPoints(p.getName()); + Inventory inv = Bukkit.createInventory( + null, 54, + "§d§l积分商城 | 当前积分: " + points); + fillBg(inv); + inv.setItem(10, mkItem(Material.EMERALD, + "§a游戏币充值", + "§7消耗积分兑换游戏币", + "§7100积分 = $1000", + "", + "§e点击兑换")); + inv.setItem(12, mkItem(Material.ENDER_CHEST, + "§b背包空间", + "§7消耗积分兑换背包格子", + "§750积分 = 9格永久空间", + "", + "§e点击兑换")); + inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE, + "§e经验瓶", + "§7消耗积分兑换经验", + "§720积分 = 10级经验", + "", + "§e点击兑换")); + if (cyInstance != null) { + inv.setItem(31, mkItem(Material.CHEST, + "§6背包商城", + "§7来自CY_beibao的商品", + "", + "§e点击查看")); + } + inv.setItem(49, mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public Inventory createCYShopGUI(Player p) { + refreshCYItems(); + int points = getPoints(p.getName()); + int size = Math.max(54, + ((cyItems.size() / 9) + 1) * 9); + if (size > 54) size = 54; + Inventory inv = Bukkit.createInventory( + null, size, + "§d§lCY背包商城 | 当前积分: " + points); + fillBg(inv); + for (int i = 0; i < cyItems.size() + && i < 45; i++) { + Map item = cyItems.get(i); + String name = (String) item.get("name"); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + List lore = new ArrayList<>(); + lore.add("§7价格: §e" + price + " 积分"); + if (lifetime) { + lore.add("§7格子: §e终身 +" + slots); + } else { + lore.add("§7格子: §e+" + slots); + lore.add("§7时间: §e" + days + "天"); + } + if (stock == 0) { + lore.add("§7库存: §a无限"); + } else if (stock == -1) { + lore.add("§7库存: §c已下架"); + } else { + lore.add("§7库存: §e" + stock); + } + lore.add(""); + if (points >= price && stock != -1) { + lore.add("§e点击购买"); + } else if (stock == -1) { + lore.add("§c已下架"); + } else { + lore.add("§c积分不足"); + } + inv.setItem(i, mkItem(Material.CHEST, + "§a" + name, + lore.toArray(new String[0]))); + } + inv.setItem(size - 1, + mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public void addItem(String name, + int price, String cmd2) { + // 追加到商品配置文件 + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.io.FileWriter fw = + new java.io.FileWriter(file, true); + fw.write(name + ":" + price + + ":" + cmd2 + "\n"); + fw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void removeItem(int index) { + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.util.List lines = + new java.util.ArrayList<>(); + java.io.BufferedReader br = + new java.io.BufferedReader( + new java.io.FileReader( + file)); + String line; + int i = 0; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty() + || line.startsWith("#")) { + lines.add(line); + continue; + } + if (i != index) { + lines.add(line); + } + i++; + } + br.close(); + java.io.PrintWriter pw = + new java.io.PrintWriter(file); + for (String l : lines) pw.println(l); + pw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void handleClick(Player p, int slot) { + int points = getPoints(p.getName()); + switch (slot) { + case 10: + if (points >= 100) { + plugin.getDb().deductPoints( + p.getName(), 100); + if (plugin.getEconomy() != null) { + plugin.getEconomy() + .depositPlayer(p, 1000); + } + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "100")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 12: + if (points >= 50) { + plugin.getDb().deductPoints( + p.getName(), 50); + activateCY(p.getName(), 9, 0); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "50")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 14: + if (points >= 20) { + plugin.getDb().deductPoints( + p.getName(), 20); + p.setLevel(p.getLevel() + 10); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "20")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 31: + if (cyInstance != null) { + p.openInventory( + createCYShopGUI(p)); + } + break; + } + } + + public void handleCYClick(Player p, int slot) { + if (slot < 0 || slot >= cyItems.size() + || slot >= 45) return; + Map item = cyItems.get(slot); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + if (stock == -1) { + p.sendMessage("§c该商品已下架"); + return; + } + int points = getPoints(p.getName()); + if (points < price) { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + return; + } + + plugin.getDb().deductPoints(p.getName(), price); + int actDays = lifetime ? 0 : days; + activateCY(p.getName(), slots, actDays); + + String tl = lifetime + ? "终身" : (days + "天"); + p.sendMessage("§a购买成功!获得 " + + tl + " +" + slots + "格 背包空间"); + } + + 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/QuestTracker.java b/src/main/java/Sdf1_login/QuestTracker.java index ea422d2..228c1ac 100644 --- a/src/main/java/Sdf1_login/QuestTracker.java +++ b/src/main/java/Sdf1_login/QuestTracker.java @@ -15,6 +15,8 @@ import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; +// 如果 Bonds 在 Sdf1_login 包下: +import Sdf1_login.BondManager; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -892,7 +894,7 @@ public class QuestTracker { /** * 发放阶段奖励 - * 含经济内容的走 Vault API + * 含经济内容的走 Vault API或债券 */ public void claimRewards( Player player, @@ -900,13 +902,20 @@ public class QuestTracker { if (qf == null || qf.rewards == null || qf.rewards.isEmpty()) { - player.sendMessage( - "§c无可领取的奖励"); + player.sendMessage("§c无可领取的奖励"); return; } - net.milkbowl.vault.economy.Economy econ = - ((Main) plugin).getEconomy(); + Main main = (Main) plugin; + // ★ 读取配置:economy = Vault,bonds = 债券(默认) + boolean useEconomy = "economy".equalsIgnoreCase( + main.getConfigMgr().rewardChannel); + + // Vault 经济引用(仅 useEconomy 时使用) + net.milkbowl.vault.economy.Economy econ = null; + if (useEconomy) { + econ = main.getEconomy(); + } int sentCount = 0; @@ -916,52 +925,70 @@ public class QuestTracker { // 检查可选奖励标记 if (hasOptionalMarker(rt)) { - if (!areAllConditionsMet(player.getName(), qf)) + if (!areAllConditionsMet(player.getName(), qf)) { player.sendMessage( - "§7[可选奖励] " - + "需完成所有可选条件: " - + rt); + "§7[可选奖励] 需完成所有可选条件: " + rt); continue; } - + } // 按 + 分割子项 - String[] parts = - rt.split("[+,]"); + String[] parts = rt.split("[+,]"); for (String part : parts) { part = part.trim(); if (part.isEmpty()) continue; - // 尝试发经济 - double amt = - extractEconomyAmount(part); - if (amt > 0 && econ != null) { - double before = - econ.getBalance(player); - econ.depositPlayer( - player, amt); - player.sendMessage( - "§a§l[任务奖励] §f" - + part - + " §7(余额: §e" - + String.format( - "%.2f", before) - + " §7→ §a" - + String.format( - "%.2f", - econ.getBalance( - player)) - + "§7)"); - // 通知提成系统 - if (plugin instanceof Main) { - ((Main) plugin) - .getCommission() - .payCommission( - player - .getName(), - amt); + // 尝试识别经济金额 + double amt = extractEconomyAmount(part); + if (amt > 0) { + // ★ 根据配置分发 + if (useEconomy && econ != null) { + // —— Vault 经济 —— + double before = + econ.getBalance(player); + econ.depositPlayer(player, amt); + double after = + econ.getBalance(player); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(余额: §e" + + String.format("%.2f", before) + + " §7→ §a" + + String.format("%.2f", after) + + "§7)"); + if (plugin instanceof Main) { + ((Main) plugin).getCommission() + .payCommission( + player.getName(), amt); + } + } else { + // —— 债券(默认) —— + int bondAmt = + (int) Math.round(amt); + BondManager bonds = main.getBonds(); + if (bonds != null) { + int before = bonds.getBonds( + player.getName()); + bonds.addBonds( + player.getName(), + bondAmt, + "quest_reward", + qf.displayName, + "任务系统", + "任务奖励:" + qf.displayName); + int after = bonds.getBonds( + player.getName()); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(债券: §e" + + before + + " §7→ §a" + + after + "§7)"); + } else { + player.sendMessage( + "§c债券系统不可用"); + } } - sentCount++; continue; } @@ -971,32 +998,27 @@ public class QuestTracker { parseRewardItems(part); if (!items.isEmpty()) { for (ItemStack is : items) { - player.getInventory() - .addItem(is); + player.getInventory().addItem(is); } player.sendMessage( - "§a§l[任务奖励] §f" - + part); + "§a§l[任务奖励] §f" + part); sentCount++; continue; } // 非经济非物品 → 仅消息 player.sendMessage( - "§a§l[任务奖励] §f" - + part); + "§a§l[任务奖励] §f" + part); sentCount++; } } plugin.getLogger().info( - "[QuestTracker] " - + player.getName() - + " 领取了 \"" - + qf.displayName - + "\" 共 " + sentCount - + " 项奖励"); + "[QuestTracker] " + player.getName() + + " 预取了 \"" + qf.displayName + + "\" 共 " + sentCount + "项奖励"); } + /** * 检查奖励文本是否含可选标记 */ @@ -1043,7 +1065,9 @@ public class QuestTracker { || t.contains("$") || t.contains("经济") || t.contains("余额") - || t.contains("发钱"); + || t.contains("发钱") + || t.contains("债券"); // ★ 新增 + if (!hasEcon) return 0; diff --git a/src/main/java/Sdf1_login/RefundRecord.java b/src/main/java/Sdf1_login/RefundRecord.java new file mode 100644 index 0000000..c0a3070 --- /dev/null +++ b/src/main/java/Sdf1_login/RefundRecord.java @@ -0,0 +1,62 @@ +package Sdf1_login; + +import org.bukkit.Material; +import java.util.UUID; + +public class RefundRecord { + public final UUID playerUUID; + public final String playerName; + public final String itemId; + public final String itemName; + public final Material material; + public final Material shulkerMaterial; + public int amount; + public int totalPaid; + public final long timestamp; + + // 普通购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, int amount, + int totalPaid, long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = null; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + // 盒装购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, + Material shulkerMat, + int amount, int totalPaid, + long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = shulkerMat; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + public boolean isExpired() { + return System.currentTimeMillis() + - timestamp > 300000L; + } + + public int getSecondsLeft() { + long left = 300000L + - (System.currentTimeMillis() + - timestamp); + return (int) Math.max(0, left / 1000); + } +} diff --git a/src/main/java/Sdf1_login/RiskControlManager.java b/src/main/java/Sdf1_login/RiskControlManager.java index bb38775..4ddedcc 100644 --- a/src/main/java/Sdf1_login/RiskControlManager.java +++ b/src/main/java/Sdf1_login/RiskControlManager.java @@ -1,248 +1,248 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警 - */ -public class RiskControlManager { - - private final Main plugin; - - // 每个玩家名 -> 连续失败次数 - private final Map failAttempts = - new ConcurrentHashMap<>(); - - // 每个IP -> 关联的可疑玩家列表 - private final Map> ipSuspicious = - new ConcurrentHashMap<>(); - - // 每个玩家 -> 上次验证时间(用于冷却) - private final Map verifyCooldown = - new ConcurrentHashMap<>(); - - // 配置项 - private static final int MAX_FAIL_ATTEMPTS = 5; - private static final int MAX_ACCOUNTS_PER_IP = 5; - private static final long COOLDOWN_MS = 60000L; - - public RiskControlManager(Main plugin) { - this.plugin = plugin; - } - - /** - * 记录一次失败的登录尝试 - * - * @param playerName 玩家名 - * @return 剩余尝试次数 - */ - public int recordFailAttempt(String playerName) { - int current = failAttempts - .getOrDefault(playerName, 0) + 1; - failAttempts.put(playerName, current); - return Math.max(0, MAX_FAIL_ATTEMPTS - current); - } - - /** - * 重置指定玩家的失败计数(登录成功时调用) - */ - public void resetFailAttempts(String playerName) { - failAttempts.remove(playerName); - } - - /** - * 获取指定玩家的失败次数 - */ - public int getFailAttempts(String playerName) { - return failAttempts - .getOrDefault(playerName, 0); - } - - /** - * 判断玩家是否已被锁定(失败次数达到上限) - */ - public boolean isLocked(String playerName) { - return failAttempts - .getOrDefault(playerName, 0) - >= MAX_FAIL_ATTEMPTS; - } - - /** - * 检查IP是否达到注册上限 - * 直接查数据库,不依赖 DatabaseManager 的方法 - */ - public boolean canRegisterFromIP(String ip) { - if (ip == null || ip.isEmpty()) return true; - try { - java.sql.Connection conn = - plugin.getDb().getDb(); - if (conn == null) return true; - java.sql.PreparedStatement ps = - conn.prepareStatement( - "SELECT COUNT(*) FROM users " - + "WHERE ip_address = ?"); - ps.setString(1, ip); - java.sql.ResultSet rs = ps.executeQuery(); - int count = 0; - if (rs.next()) count = rs.getInt(1); - rs.close(); - ps.close(); - return count < 5; - } catch (Exception e) { - e.printStackTrace(); - return true; - } - } - - - /** - * 记录IP关联的可疑行为 - */ - public void recordSuspiciousIP(String ip, - String playerName) { - if (ip == null || ip.isEmpty()) return; - ipSuspicious - .computeIfAbsent(ip, - k -> ConcurrentHashMap.newKeySet()) - .add(playerName); - } - - /** - * 获取某IP关联的所有可疑玩家 - */ - public Set getSuspiciousAccounts(String ip) { - return ipSuspicious - .getOrDefault(ip, Collections.emptySet()); - } - - /** - * 检查是否在验证冷却期内 - */ - public boolean isOnCooldown(String playerName) { - Long last = verifyCooldown.get(playerName); - if (last == null) return false; - return System.currentTimeMillis() - last - < COOLDOWN_MS; - } - - /** - * 设置验证冷却时间 - */ - public void setCooldown(String playerName) { - verifyCooldown.put(playerName, - System.currentTimeMillis()); - } - - /** - * 当检测到可疑登录时触发报警 - * - 全服广播 - * - 有邮箱则发邮件 - * - 记录到数据库 - */ - public void triggerAlert(Player player, - String suspiciousIP) { - String name = player.getName(); - - // 记录到数据库 - plugin.getDb().recordSecurityAlert( - name, suspiciousIP); - - // 记录IP可疑行为 - recordSuspiciousIP(suspiciousIP, name); - - // 全服广播 - String serverMsg = "§c§l[安全警告] §e" + name - + " §f的账号疑似遭到 §c" + suspiciousIP - + " §f的盗号尝试, 请各位玩家注意账号安全!"; - Bukkit.broadcastMessage(serverMsg); - - // 有邮箱则发邮件 - String emailAddr = (String) plugin.getDb() - .getField(name, "email"); - if (emailAddr != null - && !emailAddr.isEmpty()) { - String subject = - "[安全警告] 您的账号疑似遭到盗号尝试"; - String body = "尊敬的玩家 " + name - + ":\n\n" - + "您的账号在 " - + new java.text.SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss") - .format(new java.util.Date()) - + " 疑似遭到 IP: " + suspiciousIP - + " 的盗号尝试。\n\n" - + "建议您:\n" - + "1. 立即修改密码" - + "(使用 /sdf1_login pw)\n" - + "2. 如非本人操作," - + "请联系管理员处理\n\n" - + "--Sdf1_login 安全系统"; - plugin.getEmail().sendTempPassword( - emailAddr, name, body); - } - } - - /** - * 处理登录失败时的风控逻辑 - * - * @param player 玩家 - * @param suspiciousIP 可疑来源IP - * @return true=已被锁定(应踢出), false=仍可继续尝试 - */ - public boolean handleLoginFailure(Player player, - String suspiciousIP) { - String name = player.getName(); - int remaining = recordFailAttempt(name); - - if (remaining <= 0) { - // 达到上限,触发报警 - triggerAlert(player, suspiciousIP); - player.kickPlayer( - "§c§l[风控] §f登录失败次数过多," - + " 账号已被临时锁定, 请联系管理员"); - return true; - } - - // 未达上限,提示剩余次数 - player.sendMessage( - "§c§l[风控] §f密码错误! " - + "剩余" + remaining + "次机会"); - return false; - } - - /** - * 登录成功后重置风控状态 - */ - public void onLoginSuccess(String playerName) { - resetFailAttempts(playerName); - } - - /** - * 清理过期的冷却记录(可由定时任务调用) - */ - public void cleanup() { - long now = System.currentTimeMillis(); - verifyCooldown.entrySet().removeIf( - e -> now - e.getValue() > COOLDOWN_MS * 10); - } - - /** - * 获取所有玩家的失败尝试记录(调试用) - */ - public Map getAllFailAttempts() { - return Collections.unmodifiableMap(failAttempts); - } - - /** - * 清除所有风控数据(管理员调试用) - */ - public void clearAll() { - failAttempts.clear(); - ipSuspicious.clear(); - verifyCooldown.clear(); - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警 + */ +public class RiskControlManager { + + private final Main plugin; + + // 每个玩家名 -> 连续失败次数 + private final Map failAttempts = + new ConcurrentHashMap<>(); + + // 每个IP -> 关联的可疑玩家列表 + private final Map> ipSuspicious = + new ConcurrentHashMap<>(); + + // 每个玩家 -> 上次验证时间(用于冷却) + private final Map verifyCooldown = + new ConcurrentHashMap<>(); + + // 配置项 + private static final int MAX_FAIL_ATTEMPTS = 5; + private static final int MAX_ACCOUNTS_PER_IP = 5; + private static final long COOLDOWN_MS = 60000L; + + public RiskControlManager(Main plugin) { + this.plugin = plugin; + } + + /** + * 记录一次失败的登录尝试 + * + * @param playerName 玩家名 + * @return 剩余尝试次数 + */ + public int recordFailAttempt(String playerName) { + int current = failAttempts + .getOrDefault(playerName, 0) + 1; + failAttempts.put(playerName, current); + return Math.max(0, MAX_FAIL_ATTEMPTS - current); + } + + /** + * 重置指定玩家的失败计数(登录成功时调用) + */ + public void resetFailAttempts(String playerName) { + failAttempts.remove(playerName); + } + + /** + * 获取指定玩家的失败次数 + */ + public int getFailAttempts(String playerName) { + return failAttempts + .getOrDefault(playerName, 0); + } + + /** + * 判断玩家是否已被锁定(失败次数达到上限) + */ + public boolean isLocked(String playerName) { + return failAttempts + .getOrDefault(playerName, 0) + >= MAX_FAIL_ATTEMPTS; + } + + /** + * 检查IP是否达到注册上限 + * 直接查数据库,不依赖 DatabaseManager 的方法 + */ + public boolean canRegisterFromIP(String ip) { + if (ip == null || ip.isEmpty()) return true; + try { + java.sql.Connection conn = + plugin.getDb().getDb(); + if (conn == null) return true; + java.sql.PreparedStatement ps = + conn.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address = ?"); + ps.setString(1, ip); + java.sql.ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count < 5; + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + + /** + * 记录IP关联的可疑行为 + */ + public void recordSuspiciousIP(String ip, + String playerName) { + if (ip == null || ip.isEmpty()) return; + ipSuspicious + .computeIfAbsent(ip, + k -> ConcurrentHashMap.newKeySet()) + .add(playerName); + } + + /** + * 获取某IP关联的所有可疑玩家 + */ + public Set getSuspiciousAccounts(String ip) { + return ipSuspicious + .getOrDefault(ip, Collections.emptySet()); + } + + /** + * 检查是否在验证冷却期内 + */ + public boolean isOnCooldown(String playerName) { + Long last = verifyCooldown.get(playerName); + if (last == null) return false; + return System.currentTimeMillis() - last + < COOLDOWN_MS; + } + + /** + * 设置验证冷却时间 + */ + public void setCooldown(String playerName) { + verifyCooldown.put(playerName, + System.currentTimeMillis()); + } + + /** + * 当检测到可疑登录时触发报警 + * - 全服广播 + * - 有邮箱则发邮件 + * - 记录到数据库 + */ + public void triggerAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + + // 记录到数据库 + plugin.getDb().recordSecurityAlert( + name, suspiciousIP); + + // 记录IP可疑行为 + recordSuspiciousIP(suspiciousIP, name); + + // 全服广播 + String serverMsg = "§c§l[安全警告] §e" + name + + " §f的账号疑似遭到 §c" + suspiciousIP + + " §f的盗号尝试, 请各位玩家注意账号安全!"; + Bukkit.broadcastMessage(serverMsg); + + // 有邮箱则发邮件 + String emailAddr = (String) plugin.getDb() + .getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String subject = + "[安全警告] 您的账号疑似遭到盗号尝试"; + String body = "尊敬的玩家 " + name + + ":\n\n" + + "您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + suspiciousIP + + " 的盗号尝试。\n\n" + + "建议您:\n" + + "1. 立即修改密码" + + "(使用 /sdf1_login pw)\n" + + "2. 如非本人操作," + + "请联系管理员处理\n\n" + + "--Sdf1_login 安全系统"; + plugin.getEmail().sendTempPassword( + emailAddr, name, body); + } + } + + /** + * 处理登录失败时的风控逻辑 + * + * @param player 玩家 + * @param suspiciousIP 可疑来源IP + * @return true=已被锁定(应踢出), false=仍可继续尝试 + */ + public boolean handleLoginFailure(Player player, + String suspiciousIP) { + String name = player.getName(); + int remaining = recordFailAttempt(name); + + if (remaining <= 0) { + // 达到上限,触发报警 + triggerAlert(player, suspiciousIP); + player.kickPlayer( + "§c§l[风控] §f登录失败次数过多," + + " 账号已被临时锁定, 请联系管理员"); + return true; + } + + // 未达上限,提示剩余次数 + player.sendMessage( + "§c§l[风控] §f密码错误! " + + "剩余" + remaining + "次机会"); + return false; + } + + /** + * 登录成功后重置风控状态 + */ + public void onLoginSuccess(String playerName) { + resetFailAttempts(playerName); + } + + /** + * 清理过期的冷却记录(可由定时任务调用) + */ + public void cleanup() { + long now = System.currentTimeMillis(); + verifyCooldown.entrySet().removeIf( + e -> now - e.getValue() > COOLDOWN_MS * 10); + } + + /** + * 获取所有玩家的失败尝试记录(调试用) + */ + public Map getAllFailAttempts() { + return Collections.unmodifiableMap(failAttempts); + } + + /** + * 清除所有风控数据(管理员调试用) + */ + public void clearAll() { + failAttempts.clear(); + ipSuspicious.clear(); + verifyCooldown.clear(); + } +} diff --git a/src/main/java/Sdf1_login/SalesStatsManager.java b/src/main/java/Sdf1_login/SalesStatsManager.java new file mode 100644 index 0000000..2f58b5d --- /dev/null +++ b/src/main/java/Sdf1_login/SalesStatsManager.java @@ -0,0 +1,170 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class SalesStatsManager { + + private final Main plugin; + + private final Map sessionSales = + new ConcurrentHashMap<>(); + private int sessionRevenue = 0; + private int sessionOrders = 0; + private int sessionItems = 0; + private int sessionRefunds = 0; + private int sessionRefundAmount = 0; + private long sessionStart = System.currentTimeMillis(); + + // 状态 + private boolean waitingForEmpty = false; + private boolean hasReported = false; + private BukkitRunnable waitTask = null; + + public SalesStatsManager(Main plugin) { + this.plugin = plugin; + startMonitor(); + } + + // ★ 每10秒检测一次 + private void startMonitor() { + new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 有玩家在线 → 重置等待状态 + if (waitingForEmpty) { + // 刚有人进来了,清除已汇报标记 + // 下次没人时会重新汇报 + } + waitingForEmpty = false; + hasReported = false; + cancelWaitTask(); + } else if (!waitingForEmpty && !hasReported) { + // 没人了,且还没汇报过 → 开始1分钟倒计时 + waitingForEmpty = true; + startWaitCountdown(); + } + } + }.runTaskTimer(plugin, 200L, 200L); + } + + // ★ 1分钟等待 + private void startWaitCountdown() { + cancelWaitTask(); + waitTask = new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 等待期间有人进来 → 取消,等下次没人 + waitingForEmpty = false; + hasReported = false; + waitTask = null; + cancel(); + return; + } + + // 1分钟没人 → 汇报一次 + if (!hasReported) { + hasReported = true; + report(); + } + waitingForEmpty = false; + waitTask = null; + cancel(); + } + }; + waitTask.runTaskLater(plugin, 1200L); // 60秒 + } + + private void cancelWaitTask() { + if (waitTask != null) { + waitTask.cancel(); + waitTask = null; + } + } + + // ===== 记录 ===== + + public void recordSale(String itemName, int price) { + sessionSales.merge(itemName, 1, Integer::sum); + sessionRevenue += price; + sessionOrders++; + sessionItems++; + } + + public void recordRefund(int amount) { + sessionRefunds++; + sessionRefundAmount += amount; + } + + // ===== 汇报 ===== + + public void report() { + if (sessionItems == 0 && sessionRefunds == 0) { + plugin.getLogger().info("[Sales] 无数据,跳过"); + return; + } + + long minutes = + (System.currentTimeMillis() - sessionStart) + / 60000; + if (minutes < 1) minutes = 1; + + StringBuilder sb = new StringBuilder(); + sb.append("§6§l========== 销量汇总 =========="); + sb.append("\n§7时长: §f").append(minutes).append("分钟"); + sb.append("\n§7订单: §f").append(sessionOrders).append("笔"); + sb.append("\n§7商品: §f").append(sessionItems).append("件"); + sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚"); + sb.append("\n§7退款: §c").append(sessionRefunds) + .append("次(§c").append(sessionRefundAmount) + .append("枚§7)"); + sb.append("\n§6§l------------------------------"); + + if (!sessionSales.isEmpty()) { + sb.append("\n§7销量排行:"); + List> sorted = + new ArrayList<>(sessionSales.entrySet()); + sorted.sort((a, b) -> + b.getValue() - a.getValue()); + int rank = 1; + for (Map.Entry e : sorted) { + if (rank > 10) break; + sb.append("\n§e").append(rank).append(". ") + .append(e.getKey()) + .append(" §7x").append(e.getValue()); + rank++; + } + } + sb.append("\n§6§l=============================="); + + String msg = sb.toString(); + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.isOp()) { + for (String line : msg.split("\n")) + p.sendMessage(line); + } + } + plugin.getLogger().info("[Sales] 汇报完成"); + resetSession(); + } + + public void resetSession() { + sessionSales.clear(); + sessionRevenue = 0; + sessionOrders = 0; + sessionItems = 0; + sessionRefunds = 0; + sessionRefundAmount = 0; + sessionStart = System.currentTimeMillis(); + } +} diff --git a/src/main/java/Sdf1_login/ShopCategory.java b/src/main/java/Sdf1_login/ShopCategory.java new file mode 100644 index 0000000..715e9ad --- /dev/null +++ b/src/main/java/Sdf1_login/ShopCategory.java @@ -0,0 +1,32 @@ +package Sdf1_login; + +import java.util.ArrayList; +import java.util.List; + +public class ShopCategory { + private final String name; + private final String fileName; + private final List items; + + public ShopCategory(String name, String fileName) { + this.name = name; + this.fileName = fileName; + this.items = new ArrayList<>(); + } + + public String getName() { return name; } + public String getFileName() { return fileName; } + public List getItems() { return items; } + + public ShopItem getItem(String id) { + for (ShopItem item : items) { + if (item.getId().equalsIgnoreCase(id)) + return item; + } + return null; + } + + public void addItem(ShopItem item) { + items.add(item); + } +} diff --git a/src/main/java/Sdf1_login/ShopItem.java b/src/main/java/Sdf1_login/ShopItem.java new file mode 100644 index 0000000..422c083 --- /dev/null +++ b/src/main/java/Sdf1_login/ShopItem.java @@ -0,0 +1,50 @@ +package Sdf1_login; + +import org.bukkit.Material; + +public class ShopItem { + private final String id; + private final String displayName; + private final Material material; + private final int buyPrice; + private final int sellPrice; + private int stock; + private int hourlySales; + private int totalSales; +//打包了 直接测试替换就好了。 + public ShopItem(String id, String displayName, + Material material, int buyPrice, + int sellPrice, int stock, + int hourlySales, int totalSales) { + this.id = id; + this.displayName = displayName; + this.material = material; + this.buyPrice = buyPrice; + this.sellPrice = sellPrice; + this.stock = stock; + this.hourlySales = hourlySales; + this.totalSales = totalSales; + } + + public int getEffectiveSellPrice() { + if (sellPrice <= 0) return 0; + return sellPrice; + } + + public String getId() { return id; } + public String getDisplayName() { return displayName; } + public Material getMaterial() { return material; } + public int getBuyPrice() { return buyPrice; } + public int getSellPrice() { return sellPrice; } + public int getStock() { return stock; } + public int getHourlySales() { return hourlySales; } + public int getTotalSales() { return totalSales; } + + public void setStock(int s) { this.stock = s; } + public void setHourlySales(int s) { this.hourlySales = s; } + public void setTotalSales(int s) { this.totalSales = s; } + public void addSales(int n) { + this.hourlySales += n; + this.totalSales += n; + } +} diff --git a/src/main/java/Sdf1_login/ShopManager.java b/src/main/java/Sdf1_login/ShopManager.java new file mode 100644 index 0000000..e18b388 --- /dev/null +++ b/src/main/java/Sdf1_login/ShopManager.java @@ -0,0 +1,2612 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.Base64; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.inventory.meta.PotionMeta; + + + + +public class ShopManager implements Listener { + + private final Main plugin; + private final List categories = + new ArrayList<>(); + private final List refundRecords = + new ArrayList<>(); + private final Map activeDiscount = + new HashMap<>(); + private final Map viewCat = + new HashMap<>(); + private final Map viewPage = + new HashMap<>(); + private final Map> shoppingCarts = + new HashMap<>(); + private final Set cartModeEnabled = + new HashSet<>(); + private final java.util.Random rng = + new java.util.Random(); + private final Map> pendingCheckout = + new HashMap<>(); + private final Set couponListening = + new HashSet<>(); + private final Map pendingCouponCode = + new HashMap<>(); + // ===== 物品NBT数据 ===== + private final Map itemNbtData = + new HashMap<>(); + private final Map itemStackCache = + new HashMap<>(); + // 替换字段 + private final Map couponApplyTime = new HashMap<>(); + + + + private static final Material[] SHULKER_COLORS = { + Material.SHULKER_BOX, + Material.WHITE_SHULKER_BOX, + Material.ORANGE_SHULKER_BOX, + Material.MAGENTA_SHULKER_BOX, + Material.LIGHT_BLUE_SHULKER_BOX, + Material.YELLOW_SHULKER_BOX, + Material.LIME_SHULKER_BOX, + Material.PINK_SHULKER_BOX, + Material.GRAY_SHULKER_BOX, + Material.LIGHT_GRAY_SHULKER_BOX, + Material.CYAN_SHULKER_BOX, + Material.PURPLE_SHULKER_BOX, + Material.BLUE_SHULKER_BOX, + Material.BROWN_SHULKER_BOX, + Material.GREEN_SHULKER_BOX, + Material.RED_SHULKER_BOX, + Material.BLACK_SHULKER_BOX + }; + private static final Map EFFECT_MAP = new HashMap<>(); + static { + // ===== 基础效果 ===== + EFFECT_MAP.put("夜视", PotionEffectType.NIGHT_VISION); + EFFECT_MAP.put("隐身", PotionEffectType.INVISIBILITY); + EFFECT_MAP.put("迅捷", PotionEffectType.SPEED); + EFFECT_MAP.put("速度", PotionEffectType.SPEED); + EFFECT_MAP.put("力量", PotionEffectType.STRENGTH); // 1.21+ 改名 + EFFECT_MAP.put("治疗", PotionEffectType.INSTANT_HEALTH); // 1.21+ 改名 + EFFECT_MAP.put("治愈", PotionEffectType.INSTANT_HEALTH); + EFFECT_MAP.put("伤害", PotionEffectType.INSTANT_DAMAGE); // 1.21+ 改名 + EFFECT_MAP.put("剧毒", PotionEffectType.POISON); + EFFECT_MAP.put("中毒", PotionEffectType.POISON); + EFFECT_MAP.put("回复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("再生", PotionEffectType.REGENERATION); + EFFECT_MAP.put("生命恢复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("抗性", PotionEffectType.RESISTANCE); // 1.21+ 改名 + EFFECT_MAP.put("抗火", PotionEffectType.FIRE_RESISTANCE); + EFFECT_MAP.put("水下呼吸", PotionEffectType.WATER_BREATHING); + EFFECT_MAP.put("急迫", PotionEffectType.HASTE); // 1.21+ 改名 + EFFECT_MAP.put("挖掘疲劳", PotionEffectType.MINING_FATIGUE); // 1.21+ 改名 + EFFECT_MAP.put("虚弱", PotionEffectType.WEAKNESS); + EFFECT_MAP.put("失明", PotionEffectType.BLINDNESS); + EFFECT_MAP.put("飘浮", PotionEffectType.LEVITATION); + EFFECT_MAP.put("缓降", PotionEffectType.SLOW_FALLING); // ★ 补充 + EFFECT_MAP.put("迟缓", PotionEffectType.SLOWNESS); + EFFECT_MAP.put("饱和", PotionEffectType.SATURATION); + EFFECT_MAP.put("伤害吸收", PotionEffectType.ABSORPTION); + EFFECT_MAP.put("幸运", PotionEffectType.LUCK); + EFFECT_MAP.put("不祥", PotionEffectType.UNLUCK); + EFFECT_MAP.put("发光", PotionEffectType.GLOWING); + EFFECT_MAP.put("黑暗", PotionEffectType.DARKNESS); + EFFECT_MAP.put("生命加成", PotionEffectType.HEALTH_BOOST); + EFFECT_MAP.put("饥饿", PotionEffectType.HUNGER); + EFFECT_MAP.put("反胃", PotionEffectType.NAUSEA); + EFFECT_MAP.put("海豚关怀", PotionEffectType.DOLPHINS_GRACE); + EFFECT_MAP.put("跳跃", PotionEffectType.JUMP_BOOST); + // ★ 神龟药水是双效果,特殊处理,这里只做名称匹配入口 + EFFECT_MAP.put("神龟", PotionEffectType.RESISTANCE); + } + + public static class CartItem { + String itemId; + String displayName; + Material material; + int unitPrice; + int quantity; + } + + + private static final int PAGE_SIZE = 28; + private static final int[] ITEM_SLOTS = { + 10,11,12,13,14,15,16, + 19,20,21,22,23,24,25, + 28,29,30,31,32,33,34, + 37,38,39,40,41,42,43 + }; + + public ShopManager(Main plugin) { + this.plugin = plugin; + loadCategories(); + startHourlyTask(); + loadItemNbtData(); + startCleanupTask(); + } + // ===== 切换购物车模式 ===== + // ===== 切换购物车模式 ===== + public boolean toggleCartMode(Player p) { + UUID uuid = p.getUniqueId(); + if (cartModeEnabled.contains(uuid)) { + cartModeEnabled.remove(uuid); + p.sendMessage("§7购物车模式已关闭,恢复正常购买"); + return false; + } else { + cartModeEnabled.add(uuid); + p.sendMessage("§a购物车模式已开启,点击商品加入购物车"); + return true; + } + } + + public boolean isCartMode(Player p) { + return cartModeEnabled.contains(p.getUniqueId()); + } + private String itemToBase64(ItemStack item) { + try { + ByteArrayOutputStream os = + new ByteArrayOutputStream(); + BukkitObjectOutputStream bos = + new BukkitObjectOutputStream(os); + bos.writeObject(item); + bos.close(); + return Base64.getEncoder() + .encodeToString(os.toByteArray()); + } catch (Exception e) { + return ""; + } + } + + private ItemStack base64ToItem(String b64) { + try { + byte[] data = + Base64.getDecoder().decode(b64); + ByteArrayInputStream is = + new ByteArrayInputStream(data); + BukkitObjectInputStream bis = + new BukkitObjectInputStream(is); + ItemStack item = + (ItemStack) bis.readObject(); + bis.close(); + return item; + } catch (Exception e) { + return null; + } + } + + private void loadItemNbtData() { + itemNbtData.clear(); + itemStackCache.clear(); + File f = new File(plugin.getDataFolder(), + "shop/item_nbt.properties"); + if (!f.exists()) return; + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq < 0) continue; + String id = + line.substring(0, eq).trim(); + String b64 = + line.substring(eq + 1).trim(); + itemNbtData.put(id, b64); + ItemStack stack = base64ToItem(b64); + if (stack != null) { + itemStackCache.put(id, stack); + } + } + br.close(); + plugin.getLogger().info("[Shop] NBT数据: " + + itemStackCache.size() + "条"); + } catch (Exception ignored) {} + } + + private void saveItemNbtData() { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, "item_nbt.properties"); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 商品完整数据(自动生成)"); + for (Map.Entry e + : itemNbtData.entrySet()) { + pw.println(e.getKey() + "=" + + e.getValue()); + } + pw.flush(); + pw.close(); + } catch (Exception ignored) {} + } + + // ===== 加入/移除购物车 ===== + public boolean addToCart(Player p, ShopItem item) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts + .computeIfAbsent( + uuid, k -> new ArrayList<>()); + // ★ 始终用原价,折扣在结算时统一应用 + int price = item.getBuyPrice(); + for (CartItem ci : cart) { + if (ci.itemId.equals(item.getId())) { + ci.quantity += 1; + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + } + CartItem ci = new CartItem(); + ci.itemId = item.getId(); + ci.displayName = item.getDisplayName(); + ci.material = item.getMaterial(); + ci.unitPrice = price; + ci.quantity = 1; + cart.add(ci); + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + + + public int getCartCount(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int c = 0; + for (CartItem ci : cart) c += ci.quantity; + return c; + } + + public int getCartTotal(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int t = 0; + for (CartItem ci : cart) + t += ci.unitPrice * ci.quantity; + return t; + } + + public void cartAddOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + cart.get(index).quantity += 1; + } + + public void cartRemoveOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + CartItem ci = cart.get(index); + ci.quantity -= 1; + if (ci.quantity <= 0) cart.remove(index); + } + + public void clearCart(Player p) { + shoppingCarts.remove(p.getUniqueId()); + } + + // ===== 打开购物车页面 ===== + public void openCart(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§a§l购物车"); + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + for (int i = 0; i < cart.size() && i < 25; i++) { + CartItem ci = cart.get(i); + ItemStack is = new ItemStack(ci.material); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + ci.displayName + + " §7x" + ci.quantity); + im.setLore(java.util.Arrays.asList( + "§7单价: §f" + ci.unitPrice + "枚", + "§7小计: §6" + + (ci.unitPrice * ci.quantity) + + "枚", + "", + "§a左键+1 §c右键-1")); + is.setItemMeta(im); + } + g.setItem(10 + i, is); + } + if (cart.isEmpty()) { + g.setItem(22, mkItem(Material.BARRIER, + "§7购物车为空")); + } + int total = getCartTotal(p); + int bal = plugin.getBonds().getBonds(p.getName()); + g.setItem(49, mkItem(Material.EMERALD, + "§6§l结算: §e" + total + " §6枚债券", + "§7余额: §f" + bal + "枚", + bal >= total ? "§a余额充足" : "§c余额不足", + "", "§a点击结算")); + g.setItem(48, mkItem( + Material.LIME_STAINED_GLASS_PANE, + "§7清空购物车")); + g.setItem(53, mkItem(Material.ARROW, "§7返回商店")); + + p.openInventory(g); + } + + // ===== 购物车点击处理 ===== + public boolean handleCartClick(Player p, int raw, + boolean left, + boolean right) { + if (raw == 53) { openShopMain(p); return true; } + if (raw == 48) { + clearCart(p); + p.sendMessage("§7购物车已清空"); + openCart(p); + return true; + } + if (raw == 49) { + checkoutCartInit(p); + return true; + } + if (raw >= 10 && raw < 35) { + int idx = raw - 10; + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + if (idx < cart.size()) { + if (right) cartRemoveOne(p, idx); + else cartAddOne(p, idx); + openCart(p); + } + return true; + } + return true; + } + + // ===== 结算入口:判断打包方式 ===== + private void checkoutCartInit(Player p) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts.get(uuid); + if (cart == null || cart.isEmpty()) { + p.sendMessage("§c购物车为空"); + return; + } + int total = getCartTotal(p); + int bal = plugin.getBonds() + .getBonds(p.getName()); + if (bal < total) { + p.sendMessage("§c余额不足,需要 §e" + + total + "§c枚"); + return; + } + int totalQty = 0; + for (CartItem ci : cart) totalQty += ci.quantity; + if (cart.size() == 1 && totalQty <= 64) { + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + deductAndGive(p, cart, + (int) Math.round(total * factor), + false, null, "", 0, + ecoPct, couponPct); + return; + } + pendingCheckout.put(uuid, + new ArrayList<>(cart)); + openPackingConfirm(p, total); + } + + + + // ===== 执行扣款+发货 ===== + private boolean deductAndGive(Player p, + List cart, int baseTotal, + boolean useShulker, Material color, + String packNote, int packFee, + int ecoPct, int couponPct) { + + UUID uuid = p.getUniqueId(); + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + String[] names = new String[cart.size()]; + int[] subs = new int[cart.size()]; + for (int i = 0; i < cart.size(); i++) { + CartItem ci = cart.get(i); + names[i] = ci.displayName; + subs[i] = ci.unitPrice * ci.quantity; + } + + if (baseTotal < originalTotal) { + double ratio = + (double) baseTotal / originalTotal; + int sum = 0; + for (int i = 0; i < subs.length; i++) { + if (i == subs.length - 1) { + subs[i] = baseTotal - sum; + } else { + subs[i] = (int) (subs[i] * ratio); + sum += subs[i]; + } + } + } + + boolean ok = plugin.getBonds().checkoutCart( + p.getName(), names, subs); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + if (packFee > 0) { + plugin.getBonds().deductBonds( + p.getName(), packFee, + "shop_packing", "", + "商店系统", "打包费"); + } + + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + if (si != null) { + if (si.getStock() >= 0) + si.setStock(si.getStock() + - ci.quantity); + si.addSales(ci.quantity); + } + } + + int newBal = plugin.getBonds() + .getBonds(p.getName()); + + + // ===== 优惠券码 ===== + String usedCouponCode = + pendingCouponCode.get(uuid); + + // ===== 记录订单 ===== + OrderManager.OrderRecord latestOrder = null; + try { + List orderItems = + new ArrayList<>(); + for (CartItem ci : cart) { + orderItems.add( + new OrderManager.OrderItem( + ci.displayName, + ci.material.name(), + ci.unitPrice, + ci.unitPrice, + ci.quantity)); + } + String dType = "none"; + String cCode = ""; + if (usedCouponCode != null) { + dType = "coupon"; + cCode = usedCouponCode; + } else if (ecoPct > 0) { + dType = "eco"; + } + plugin.getOrderManager().recordOrder( + p, orderItems, originalTotal, + baseTotal + packFee, + originalTotal - baseTotal, + dType, cCode, packFee, + useShulker ? (packFee > 0 + ? "custom" : "default") + : "none", + color != null ? color.name() : ""); + // 获取最新订单 + synchronized (plugin.getOrderManager() + .getOrders()) { + List all = + plugin.getOrderManager() + .getOrders(); + if (!all.isEmpty()) + latestOrder = + all.get(all.size() - 1); + } + // 散装打印小票 + if (!useShulker && latestOrder != null) { + plugin.getOrderManager() + .sendReceiptChat(p, latestOrder); + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[Shop] 记录订单失败: " + + ex.getMessage()); + } + +// ===== 发货 ===== + if (!useShulker) { + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + ItemStack give = (si != null) + ? getShopStack(si, ci.quantity) + : new ItemStack(ci.material, ci.quantity); + HashMap left2 = + p.getInventory().addItem(give); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } else { + List shulkers = + packShulkers(cart, color); + + // ★ 给潜影盒打订单ID标签 + if (latestOrder != null) { + for (ItemStack sh : shulkers) { + ItemMeta shm = sh.getItemMeta(); + if (shm != null) { + List lore = shm.getLore() != null + ? new ArrayList<>(shm.getLore()) + : new ArrayList<>(); + lore.add("§8#" + latestOrder.orderId); + shm.setLore(lore); + sh.setItemMeta(shm); + } + } + } + + // 小票书塞入 + if (!shulkers.isEmpty() + && latestOrder != null) { + ItemStack book = plugin.getOrderManager() + .createReceiptBook(latestOrder); + plugin.getOrderManager() + .addBookToShulker(shulkers.get(0), book); + } + // 发货 + for (ItemStack sh : shulkers) { + HashMap left2 = + p.getInventory().addItem(sh); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } + + + // ===== 退款记录 ===== + long now = System.currentTimeMillis(); + if (!useShulker) { + for (CartItem ci : cart) { + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + ci.itemId, ci.displayName, + ci.material, null, + ci.quantity, + ci.unitPrice * ci.quantity, + now)); + } + } else { + String summary = ""; + for (CartItem ci : cart) { + if (!summary.isEmpty()) summary += "+"; + summary += ci.displayName + + "x" + ci.quantity; + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + "cart_" + now, summary, + Material.SHULKER_BOX, color, + 1, baseTotal, now)); + } + + // ===== 核销优惠券 ===== + if (usedCouponCode != null) { + activeDiscount.remove(uuid); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCouponCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCouponCode + + " §7已核销"); + } + + shoppingCarts.remove(uuid); + pendingCheckout.remove(uuid); + return true; + } + + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + markShulkerUsed(item); + } + + // ★ 公共方法:标记潜影盒为已使用 + private void markShulkerUsed(ItemStack item) { + if (item == null) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + // shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + @EventHandler + public void onPlayerDrop(org.bukkit.event.player.PlayerDropItemEvent e) { + ItemStack item = e.getItemDrop().getItemStack(); + if (item != null && item.getType().name().contains("SHULKER")) { + markShulkerUsed(item); + } + } + + + // ===== 潠影盒打包(指定颜色)===== + private List packShulkers( + List items, Material color) { + List result = new ArrayList<>(); + List all = new ArrayList<>(); + for (CartItem ci : items) { + int rem = ci.quantity; + while (rem > 0) { + int sz = Math.min(rem, 64); + all.add(new ItemStack(ci.material, sz)); + rem -= sz; + } + } + while (!all.isEmpty()) { + ItemStack shulker = new ItemStack(color); + org.bukkit.inventory.meta.BlockStateMeta meta = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (meta != null) { + org.bukkit.block.BlockState state = + meta.getBlockState(); + if (state instanceof org.bukkit.block.Container) { + org.bukkit.block.Container c = + (org.bukkit.block.Container) state; + List remain = new ArrayList<>(); + for (ItemStack stack : all) { + HashMap left = + c.getInventory().addItem(stack); + for (ItemStack l : left.values()) + remain.add(l); + } + all = remain; + meta.setBlockState(c); + shulker.setItemMeta(meta); + } + } + result.add(shulker); + } + + return result; + } + + // ===== 打包确认GUI ===== + public void openPackingConfirm(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l选择包装方式"); + g.setItem(10, mkItem(Material.CHEST, + "§a§l不打包", + "§7直接放入背包", + "§7适合少量物品")); + g.setItem(13, mkItem(Material.SHULKER_BOX, + "§6§l默认打包", + "§7随机颜色潜影盒", + "§7加收 §e5 §6枚债券")); + g.setItem(16, mkItem(Material.ENDER_CHEST, + "§b§l自选颜色", + "§7选择你喜欢的潜影盒颜色", + "§7加收 §e5 §6枚债券")); + g.setItem(22, mkItem(Material.PAPER, + "§7消费金额: §e" + baseTotal + "枚")); + + p.openInventory(g); + } + + public boolean handlePackingConfirm( + Player p, int raw) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + + if (raw == 10) { + ecoPct = 2; + } + + double factor = 1.0; + if (ecoPct > 0) + factor *= (100 - ecoPct) / 100.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + originalTotal * factor); + + int totalQty = 0; + for (CartItem ci : cart) + totalQty += ci.quantity; + boolean multiAndBig = (cart.size() > 1 + && originalTotal >= 100); + + if (raw == 10) { + deductAndGive(p, cart, discounted, + false, null, "", 0, + ecoPct, couponPct); + return true; + } + if (raw == 13) { + Material c = Material.SHULKER_BOX; + if (multiAndBig) { + deductAndGive(p, cart, discounted, + true, c, "", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, + true, c, "默认打包", 5, + ecoPct, couponPct); + } + return true; + } + if (raw == 16) { + pendingCheckout.put(uuid, cart); + openColorSelect(p, discounted); + return true; + } + return true; + } + + + // ===== 自选颜色GUI ===== + public void openColorSelect(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 54, "§b§l选择潜影盒颜色"); + String[] colorNames = { + "白色","橙色","品红","淡蓝", + "黄色","淡绿","粉色","灰色", + "淡灰","青色","紫色","蓝色", + "棕色","绿色","红色","黑色","原色" + }; + for (int i = 0; i < SHULKER_COLORS.length; i++) { + ItemStack is = new ItemStack(SHULKER_COLORS[i]); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + colorNames[i] + + "§6潜影盒"); + im.setLore(java.util.Arrays.asList( + "§7加收 §e5 §6枚债券", + "", + "§a点击选择")); + is.setItemMeta(im); + } + g.setItem(i, is); + } + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(45, mkItem(Material.PAPER, + "§7消费: §e" + baseTotal + + "枚 + §e5§6打包费")); + + p.openInventory(g); + } + + public boolean handleColorSelect( + Player p, int raw) { + if (raw == 49) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.get(uuid); + if (cart != null) { + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + openPackingConfirm(p, total); + } + return true; + } + if (raw < 0 || raw >= SHULKER_COLORS.length) + return false; + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + total * factor); + + Material selectedColor = SHULKER_COLORS[raw]; + if (selectedColor == Material.SHULKER_BOX) { + deductAndGive(p, cart, discounted, true, + Material.SHULKER_BOX, + "原色打包", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, true, + selectedColor, "自选颜色", 5, + ecoPct, couponPct); + } + return true; + } + + + + // ===== 加载分类 ===== + + public void loadCategories() { + plugin.getLogger().info("[Shop] === loadCategories 被调用 ==="); + categories.clear(); + viewCat.clear(); // ★ 清缓存 + viewPage.clear(); + + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + + convertTxtToMd(dir); + + File[] allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + if (allMd == null || allMd.length == 0) { + createDefaultCategories(dir); + allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + } + if (allMd == null) { + plugin.getLogger().warning( + "[Shop] shop目录为空"); + return; + } + + plugin.getLogger().info("[Shop] === 开始加载 ==="); + plugin.getLogger().info("[Shop] 扫描到 " + + allMd.length + " 个md文件"); + + for (File f : allMd) { + plugin.getLogger().info("[Shop] 读取: " + + f.getName()); + ShopCategory cat = parseFile(f); + if (cat != null) { + plugin.getLogger().info("[Shop] 分类: " + + cat.getName() + + " 商品数: " + + cat.getItems().size()); + if (!cat.getItems().isEmpty()) { + categories.add(cat); + } + } + } + + int totalItems = 0; + for (ShopCategory c : categories) + totalItems += c.getItems().size(); + + plugin.getLogger().info("[Shop] === 加载完成 ===" + + " 分类=" + categories.size() + + " 商品=" + totalItems); + } + + +// ===== txt → md 自动迁移 ===== + + private void convertTxtToMd(File dir) { + File[] txtFiles = dir.listFiles( + (d, n) -> n.endsWith(".txt") + && !n.equals("coupons.txt")); + if (txtFiles == null) return; + for (File txt : txtFiles) { + String mdName = txt.getName() + .replace(".txt", ".md"); + File mdFile = new File(dir, mdName); + if (mdFile.exists()) continue; + + try { + List lines = new ArrayList<>(); + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(txt), + StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) + lines.add(line); + br.close(); + + String catName = txt.getName() + .replace(".txt", ""); + List out = new ArrayList<>(); + out.add("# " + catName); + out.add(""); + out.add("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + out.add("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + + for (String l : lines) { + l = l.trim(); + if (l.isEmpty() || l.startsWith("#")) + continue; + // 旧格式: ID | 名称 | 材质 | ... + // 保持原样转换为 md 表格行 + out.add("| " + l.replace("|", "|") + + " |"); + } + + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(mdFile), + StandardCharsets.UTF_8)); + for (String o : out) + pw.println(o); + pw.flush(); + pw.close(); + + txt.delete(); + plugin.getLogger().info( + "[Shop] 已迁移: " + + txt.getName() + + " → " + mdName); + } catch (Exception e) { + plugin.getLogger().warning( + "[Shop] 迁移失败: " + + txt.getName()); + } + } + } + +// ===== 解析 md 文件 ===== + + private ShopCategory parseFile(File f) { + String name = f.getName() + .replace(".md", ""); + ShopCategory cat = + new ShopCategory(name, f.getName()); + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + boolean headerPassed = false; + boolean separatorPassed = false; + + while ((line = br.readLine()) != null) { + line = line.trim(); + + // 空行跳过 + if (line.isEmpty()) continue; + + // 标题行:# 分类名 + if (line.startsWith("#")) { + String t = line.replaceFirst( + "^#+\\s*", "").trim(); + if (!t.isEmpty()) { + // 更新分类名(用文件内标题) + // cat.name 是 final,不改 + } + continue; + } + + // 表格分隔行:| --- | --- | + if (line.contains("---")) { + separatorPassed = true; + continue; + } + + // 表头行:| ID | 品名 | ... + if (line.startsWith("|")) { + if (!separatorPassed) { + headerPassed = true; + continue; + } + // 数据行 + ShopItem item = + parseTableRow(line); + if (item != null) + cat.addItem(item); + } + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[Shop] 解析失败: " + f.getName() + + " 原因: " + e.getMessage()); + return null; + } + return cat; + } + +// ===== 解析表格行 ===== + + private ShopItem parseTableRow(String row) { + plugin.getLogger().info("[Shop] 行: " + row); + + String trimmed = row.trim(); + if (trimmed.startsWith("|")) + trimmed = trimmed.substring(1); + if (trimmed.endsWith("|")) + trimmed = trimmed.substring(0, + trimmed.length() - 1); + + String[] p = trimmed.split("\\|"); + plugin.getLogger().info("[Shop] 列数=" + + p.length); + + if (p.length < 6) { + plugin.getLogger().warning( + "[Shop] ✗ 列数不足, 跳过"); + return null; + } + + String rawId = p[0].trim(); + String rawName = p[1].trim(); + String rawMat = p[2].trim().toUpperCase(); + String rawBuy = p[3].trim(); + String rawSell = p[4].trim(); + String rawStock = p[5].trim(); + + plugin.getLogger().info("[Shop] ID=" + + rawId + " 名=" + rawName + + " 材质=" + rawMat + + " 购=" + rawBuy + " 售=" + rawSell + + " 库=" + rawStock); + + // 检查材质是否存在 + Material mat = Material.matchMaterial(rawMat); + if (mat == null) { + plugin.getLogger().warning( + "[Shop] ✗ 无效材质: " + rawMat + + " (行: " + row + ")"); + return null; + } + + try { + ShopItem item = new ShopItem( + rawId, rawName, mat, + Integer.parseInt(rawBuy), + Integer.parseInt(rawSell), + Integer.parseInt(rawStock), + p.length > 6 + ? Integer.parseInt( + p[6].trim()) : 0, + p.length > 7 + ? Integer.parseInt( + p[7].trim()) : 0); + plugin.getLogger().info( + "[Shop] ✓ 解析成功: " + rawId); + return item; + } catch (Exception e) { + plugin.getLogger().warning( + "[Shop] ✗ 数字解析失败: " + + e.getMessage()); + return null; + } + } + + +// ===== 保存为 md ===== + + public void saveCategory(ShopCategory cat) { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, cat.getFileName()); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# " + cat.getName()); + pw.println(""); + pw.println("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + pw.println("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + for (ShopItem item : cat.getItems()) { + pw.println("| " + + item.getId() + + " | " + item.getDisplayName() + + " | " + item.getMaterial().name() + + " | " + item.getBuyPrice() + + " | " + item.getSellPrice() + + " | " + item.getStock() + + " | " + item.getHourlySales() + + " | " + item.getTotalSales() + + " |"); + } + pw.flush(); + pw.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[Shop] 保存失败: " + + cat.getFileName()); + } + } + +// ===== 创建示例分类 ===== + + private void createDefaultCategories(File dir) { + writeMd(dir, "建筑材料.md", + "# 建筑材料\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| STONE | 石头 | STONE" + + " | 10 | -1 | -1 | 0 | 0 |\n" + + "| COBBLESTONE | 圆石 | COBBLESTONE" + + " | 5 | -1 | -1 | 0 | 0 |\n" + + "| OAK_PLANKS | 橡木木板 | OAK_PLANKS" + + " | 8 | 3 | -1 | 0 | 0 |\n" + + "| BRICKS | 砖块 | BRICKS" + + " | 12 | -1 | -1 | 0 | 0 |\n" + + "| GLASS | 玻璃 | GLASS" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK" + + " | 20 | 8 | -1 | 0 | 0 |\n"); + + writeMd(dir, "装备工具.md", + "# 装备工具\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| IRON_PICKAXE | 铁镐 | IRON_PICKAXE" + + " | 50 | 20 | -1 | 0 | 0 |\n" + + "| DIAMOND_PICKAXE | 钻石镐 | DIAMOND_PICKAXE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| IRON_SWORD | 铁剑 | IRON_SWORD" + + " | 40 | 16 | -1 | 0 | 0 |\n" + + "| DIAMOND_SWORD | 钻石剑 | DIAMOND_SWORD" + + " | 400 | 160 | -1 | 0 | 0 |\n" + + "| ELYTRA | 鞘翅 | ELYTRA" + + " | 2000 | 800 | -1 | 0 | 0 |\n"); + + writeMd(dir, "食物补给.md", + "# 食物补给\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| COOKED_BEEF | 熟牛排 | COOKED_BEEF" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| GOLDEN_APPLE | 金苹果 | GOLDEN_APPLE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| BREAD | 面包 | BREAD" + + " | 8 | 3 | -1 | 0 | 0 |\n"); + + plugin.getLogger().info( + "[Shop] 已创建3个示例分类(md)"); + } + + private void writeMd(File dir, String name, + String content) { + File f = new File(dir, name); + if (f.exists()) return; + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.print(content); + pw.flush(); + pw.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[Shop] 创建失败: " + name); + } + } + + /** + * 从商品名称+材质智能推断完整物品 + * + * "夜视药水" + POTION + * → PotionEffect(NIGHT_VISION, 600, 0) + * + * "喷溅型治疗药水" + SPLASH_POTION + * → PotionEffect(HEALING, 600, 0) + * + * "长效夜视之箭" + TIPPED_ARROW + * → PotionEffect(NIGHT_VISION, 1800, 0) + * + * "强效力量喷溅药水" + SPLASH_POTION + * → PotionEffect(STRENGTH, 600, 1) + */ + private ItemStack inferFromName( + String name, Material mat) { + // 只处理药水和箭 + if (mat != Material.POTION + && mat != Material.SPLASH_POTION + && mat != Material.LINGERING_POTION + && mat != Material.TIPPED_ARROW) { + return null; + } + + // 1. 找效果类型 + PotionEffectType effectType = null; + for (Map.Entry e + : EFFECT_MAP.entrySet()) { + if (name.contains(e.getKey())) { + effectType = e.getValue(); + break; + } + } + if (effectType == null) return null; + + // 2. 找时长 + int duration = 600; + if (name.contains("长效") || name.contains("长时") + || name.contains("持久") || name.contains("长命")) { + duration = 1800; + } +// ★ 瞬间效果药水:duration = 1 + if (name.contains("治疗") || name.contains("治愈") + || name.contains("伤害")) { + duration = 1; + } + + // 3. 找等级 + int amplifier = 0; + if (name.contains("强效") + || name.contains("强化") + || name.contains("加强")) { + amplifier = 1; + } + + // 4. 构造完整物品 + try { + ItemStack item = new ItemStack(mat); + PotionMeta meta = + (PotionMeta) item.getItemMeta(); + + // ★ 神龟药水:双效果 + if (name.contains("神龟")) { + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.RESISTANCE, + duration, amplifier + 2), + true); + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.SLOWNESS, + duration, amplifier + 3), + true); + } else { + // 普通药水:单效果 + meta.addCustomEffect( + new PotionEffect(effectType, + duration, amplifier), + true); + } + + item.setItemMeta(meta); + return item; + } catch (Exception e) { + return null; + } + } + + public void saveAll() { + for (ShopCategory cat : categories) + saveCategory(cat); + } + private ItemStack getShopStack( + ShopItem item, int amount) { + // 优先级1: 手动捕获的NBT数据 + ItemStack cached = + itemStackCache.get(item.getId()); + if (cached != null) { + ItemStack copy = cached.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级2: 根据名称智能推断 + ItemStack inferred = inferFromName( + item.getDisplayName(), + item.getMaterial()); + if (inferred != null) { + ItemStack copy = inferred.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级3: 纯材质(默认) + return new ItemStack( + item.getMaterial(), amount); + } + + // ===== 购买 ===== + + public boolean buyItem(Player p, ShopItem item, + int amount) { + plugin.getLogger().info("[Shop] ★ buyItem item=" + (item != null ? item.getId() : "null") + " amount=" + amount); + plugin.getLogger().info("buyItem被调用"); + if (item == null) return false; + if (item.getStock() >= 0 + && item.getStock() < amount) { + p.sendMessage("§c库存不足,剩余: " + + item.getStock()); + return false; + } + if (p.getInventory().firstEmpty() == -1 + && !p.getInventory().contains( + item.getMaterial(), 1)) { + p.sendMessage("§c背包已满"); + return false; + } + + int unit = item.getBuyPrice(); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int total = Math.max(0, + unit * amount * (100 - disc) / 100); + + int balance = plugin.getBonds() + .getBonds(p.getName()); + if (balance < total) { + p.sendMessage("§c债券不足,需要 §e" + + total + "§c枚,当前 §e" + + balance + "§c枚"); + return false; + } + + boolean ok = plugin.getBonds().deductBonds( + p.getName(), total, + "shop_buy", item.getId(), + "商店系统", "购买" + + item.getDisplayName()); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + ItemStack give = getShopStack(item, amount); + HashMap left = + p.getInventory().addItem(give); + for (ItemStack drop : left.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + + if (item.getStock() >= 0) + item.setStock( + item.getStock() - amount); + item.addSales(amount); + + mergeOrAddRefund(p, item, amount, total); + Long applyTime = couponApplyTime.get(p.getUniqueId()); + if (applyTime != null + && System.currentTimeMillis() - applyTime > 300000L) { + // 超过5分钟,自动失效 + pendingCouponCode.remove(p.getUniqueId()); + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + p.sendMessage("§7优惠券已过期,请重新输入"); + disc = 0; // 重置折扣 + } + + + String discMsg = disc > 0 + ? " §7(优惠" + disc + "%)" : ""; + p.sendMessage("§a购买成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a-" + total + "枚债券" + + discMsg); + if (disc > 0) + activeDiscount.remove( + p.getUniqueId()); + // ★ 直购成功后核销优惠券 + String usedCode = pendingCouponCode + .remove(p.getUniqueId()); + if (usedCode != null) { + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCode + + " §7已核销"); + couponApplyTime.remove(p.getUniqueId()); + } + // ★ 记录订单(包裹try-catch,失败不影响购买) + try { + String usedCouponCode = + pendingCouponCode.get(p.getUniqueId()); + List orderItems = + new ArrayList<>(); + orderItems.add(new OrderManager.OrderItem( + item.getDisplayName(), + item.getMaterial().name(), + item.getBuyPrice(), + item.getBuyPrice(), amount)); + plugin.getOrderManager().recordOrder( + p, orderItems, + item.getBuyPrice() * amount, total, + item.getBuyPrice() * amount - total, + usedCouponCode != null ? "coupon" : "none", + usedCouponCode != null ? usedCouponCode : "", + 0, "none", ""); + // 打印小票 + List all = + plugin.getOrderManager().getOrders(); + if (!all.isEmpty()) { + plugin.getOrderManager().sendReceiptChat( + p, all.get(all.size() - 1)); + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[Shop] 记录订单失败: " + ex.getMessage()); + } + + return true; + } + + + public void handleRefundItemClick(Player p, int raw) { + if (raw < 10 || raw >= 45) return; + int target = raw - 10; + int cnt = 0; + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { it.remove(); continue; } + if (cnt == target) { + requestRefund(p, r); + return; + } + cnt++; + } + } + } + + + // ===== 出售 ===== + + public boolean sellItem(Player p, ShopItem item, + int amount) { + if (item == null) return false; + + // ★ 先判断售价,禁止出售的不收物品 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return false; + } + + if (!p.getInventory().containsAtLeast( + getShopStack(item, 1), amount)) { + p.sendMessage("§c你没有足够的 §e" + + item.getDisplayName()); + return false; + } + + int total = item.getEffectiveSellPrice() * amount; + + p.getInventory().removeItem( + getShopStack(item, amount)); + plugin.getBonds().addBonds( + p.getName(), total, "shop_sell", + item.getId(), "商店系统", + "出售" + item.getDisplayName()); + if (item.getStock() >= 0) + item.setStock(item.getStock() + amount); + p.sendMessage("§a出售成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a+" + total + "枚债券"); + return true; + } + + // ===== 退款合并:30秒内同商品合并为一个订单 ===== + private boolean mergeOrAddRefund(Player p, ShopItem item, + int amount, int total) { + long now = System.currentTimeMillis(); + synchronized (refundRecords) { + for (int i = refundRecords.size() - 1; i >= 0; i--) { + RefundRecord r = refundRecords.get(i); + boolean same = r.playerUUID.equals(p.getUniqueId()) + && r.itemId.equals(item.getId()) + && (now - r.timestamp) < 30000L; + if (same) { + // final 字段不能改,只能删旧建新 + int newAmount = r.amount + amount; + int newTotal = r.totalPaid + total; + refundRecords.remove(i); + refundRecords.add(new RefundRecord( + r.playerUUID, r.playerName, + r.itemId, r.itemName, + r.material, newAmount, + newTotal, now)); + return true; + } + } + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + item.getId(), item.getDisplayName(), + item.getMaterial(), amount, total, now)); + return false; + } + + + + // ===== 退款 ===== + + public boolean requestRefund(Player p, RefundRecord rec) { + if (rec == null) return false; + if (!rec.playerUUID.equals(p.getUniqueId())) { + p.sendMessage("§c这不是你的购买记录"); + return false; + } + if (rec.isExpired()) { + p.sendMessage("§c已超过5分钟退款期限"); + synchronized (refundRecords) { + refundRecords.remove(rec); + } + return false; + } + + // 检测物品/潜影盒是否还在 + synchronized (refundRecords) { + if (rec.shulkerMaterial != null) { + int boxCount = countShulkers(p, rec.shulkerMaterial); + if (boxCount < 1) { + p.sendMessage("§c潜影盒已移出背包,无法退款"); + refundRecords.remove(rec); + return false; + } + } else { + int have = countDirect(p, rec.material); + if (have < rec.amount) { + p.sendMessage("§c物品已转移或消耗,无法退款"); + refundRecords.remove(rec); + return false; + } + } + } + + // 退款:扣物品/潜影盒 + if (rec.shulkerMaterial != null) { + // 盒装:扣一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == rec.shulkerMaterial) { + p.getInventory().clear(i); + break; + } + } + } else { + // 普通:扣商品 + p.getInventory().removeItem( + new ItemStack(rec.material, rec.amount)); + } + + // 退款:加债券 + plugin.getBonds().addBonds( + p.getName(), rec.totalPaid, + "refund", rec.itemId, + "商店系统", "退款" + rec.itemName); + + // 恢复库存 + ShopItem si = findItemById(rec.itemId); + if (si != null && si.getStock() >= 0) + si.setStock(si.getStock() + rec.amount); + + synchronized (refundRecords) { + refundRecords.remove(rec); + } + + p.sendMessage("§a退款成功: §e" + rec.itemName + + " §a+" + rec.totalPaid + "枚债券"); + return true; + } + + + + + public void setPlayerDiscount(Player p, + int pct) { + activeDiscount.put( + p.getUniqueId(), pct); + p.sendMessage("§a已设置 " + pct + + "% 折扣,下次购买生效"); + } + + // ===== 管理员操作 ===== + + public boolean addItemToCategory( + String catName, String id, + String displayName, Material mat, + int buyPrice, int sellPrice, + int stock) { + ShopCategory cat = + findCategory(catName); + if (cat == null) return false; + if (cat.getItem(id) != null) return false; + cat.addItem(new ShopItem(id, displayName, + mat, buyPrice, sellPrice, + stock, 0, 0)); + saveCategory(cat); + return true; + } + + public boolean removeItem(String id) { + for (ShopCategory cat : categories) { + ShopItem item = cat.getItem(id); + if (item != null) { + cat.getItems().remove(item); + saveCategory(cat); + return true; + } + } + return false; + } + + public boolean setStock(String id, int stock) { + ShopItem item = findItemById(id); + if (item == null) return false; + item.setStock(stock); + for (ShopCategory cat : categories) { + if (cat.getItem(id) != null) { + saveCategory(cat); + return true; + } + } + return false; + } + + public ShopCategory findCategory(String n) { + for (ShopCategory c : categories) + if (c.getName().equals(n)) return c; + return null; + } + + public ShopItem findItemById(String id) { + for (ShopCategory c : categories) { + ShopItem item = c.getItem(id); + if (item != null) return item; + } + return null; + } + + public List getCategories() { + return categories; + } + + // ===== 命令处理 ===== + + public boolean handleCommand( + CommandSender s, String[] a) { + if (a.length <= 1) { + if (s instanceof Player) { + openShopMain((Player) s); + } else { + s.sendMessage("§c仅玩家可使用"); + } + return true; + } + + String sub = a[1].toLowerCase(); + + if (sub.equals("reload")) { + loadCategories(); + s.sendMessage("§a商店已重载,共 " + + categories.size() + + " 个分类"); + return true; + } + + if (sub.equals("add")) { + if (a.length < 7) { + s.sendMessage( + "§e用法: shop add <分类>" + + " <名称> <材质>" + + " <购入价>" + + " [售出价-1自动]" + + " [库存-1无限]"); + return true; + } + Material mat; + try { + mat = Material.valueOf( + a[5].toUpperCase()); + } catch (Exception e) { + s.sendMessage( + "§c无效材质: " + a[5]); + return true; + } + int price = Integer.parseInt(a[6]); + int sell = a.length > 7 + ? Integer.parseInt(a[7]) : -1; + int stock = a.length > 8 + ? Integer.parseInt(a[8]) : -1; + if (addItemToCategory(a[2], a[3], + a[4], mat, price, sell, stock)) { + s.sendMessage("§a已添加: " + + a[4] + " → " + a[2]); + } else { + s.sendMessage("§c添加失败" + + "(分类不存在或ID重复)"); + } + return true; + } + + if (sub.equals("remove")) { + if (a.length < 3) { + s.sendMessage( + "§e用法: shop remove "); + return true; + } + if (removeItem(a[2])) { + s.sendMessage( + "§a已移除: " + a[2]); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("set")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop set " + + " <库存数(-1无限)>"); + return true; + } + int stock = Integer.parseInt(a[3]); + if (setStock(a[2], stock)) { + s.sendMessage("§a已设置 " + + a[2] + " 库存为 " + + stock); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("pro")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop pro" + + " <玩家名> <折扣%>"); + return true; + } + Player t = + Bukkit.getPlayerExact(a[2]); + if (t == null) { + s.sendMessage("§c玩家不在线"); + return true; + } + int pct = Integer.parseInt(a[3]); + setPlayerDiscount(t, pct); + s.sendMessage("§a已对 " + a[2] + + " 设置 " + pct + "% 折扣"); + return true; + } + + + s.sendMessage("§e商店子命令:" + + " reload / add / remove" + + " / set / pro "); + return true; + } + @EventHandler(priority = EventPriority.LOW) + public void onCouponChat( + org.bukkit.event.player.AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + if (!couponListening.contains(uuid)) return; + e.setCancelled(true); + couponListening.remove(uuid); + String code = e.getMessage().trim(); + if (code.isEmpty()) { + p.sendMessage("§c已取消输入"); + return; + } + // ★ 预处理:标准化输入 + String normalized = normalizeCouponInput(code); + if (normalized.isEmpty()) { + p.sendMessage("§c优惠券码无效(输入为空或仅含特殊字符)"); + couponListening.remove(uuid); + return; + } + + p.sendMessage("§7正在验证优惠券: §e" + code); + + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "validateCoupon", + String.class); + Object result = m.invoke(sdf1, normalized); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception ex) { + p.sendMessage("§c验证异常: " + + ex.getMessage()); + return; + } + if (discount <= 0) { + p.sendMessage("§c优惠券无效或已使用"); + return; + } + int offPct = 100 - discount; + int current = activeDiscount + .getOrDefault(uuid, 0); + if (offPct > current) { + pendingCouponCode.put(uuid, normalized); + activeDiscount.put(uuid, offPct); + couponApplyTime.put(uuid, System.currentTimeMillis()); + String zhe; + if (discount % 10 == 0) + zhe = (discount / 10) + "折"; + else + zhe = (discount / 10.0) + "折"; + p.sendMessage("§6§l[优惠券] §a验证通过!"); + p.sendMessage("§7折扣: §e" + zhe + + " §7(减" + offPct + "%)"); + p.sendMessage("§7结算时自动核销"); + final UUID u = uuid; + Bukkit.getScheduler().runTask(plugin, () -> { + Player pl = Bukkit.getPlayer(u); + if (pl != null && pl.isOnline()) { + openShopMain(pl); + } + }); + } else { + String curZhe; + if ((100 - current) % 10 == 0) + curZhe = ((100 - current) / 10) + "折"; + else + curZhe = ((100 - current) / 10.0) + "折"; + p.sendMessage("§c当前已有更优折扣: " + + curZhe + ",本次不生效"); + } + } + + /** + * 优惠券输入标准化 + * 全角→半角、去分隔符、转小写 + * "AAA-BBB-CCCC" → "aaaabbbbcccc" + * "AAA-BBB" → "aaabbb" + * "aaa_bbb_cccc" → "aaabbbcccc" + */ + private String normalizeCouponInput(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // 全角数字→半角 + if (c >= '\uFF10' && c <= '\uFF19') { + sb.append((char)(c - 0xFF10 + '0')); + } + // 全角字母→半角 + else if (c >= '\uFF21' && c <= '\uFF3A') { + sb.append((char)(c - 0xFF21 + 'A')); + } + else if (c >= '\uFF41' && c <= '\uFF5A') { + sb.append((char)(c - 0xFF41 + 'a')); + } + // ★ 保留中文字符(Unicode CJK统一汉字范围) + else if ((c >= '\u4E00' && c <= '\u9FFF') + || (c >= '\u3400' && c <= '\u4DBF')) { + sb.append(c); + } + // 保留半角字母和数字 + else if ((c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9')) { + sb.append(c); + } + // 分隔符(- _ 空格等)丢弃 + } + return sb.toString().toLowerCase(); + } + + + + + + private String formatZhe(int discount) { + if (discount % 10 == 0) { + return (discount / 10) + "折"; + } + return (discount / 10.0) + "折"; + } + + public boolean redeemCoupon(Player p, String code) { + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + Object result = m.invoke(sdf1, code); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception e) { + return false; + } + if (discount <= 0) return false; + int offPct = 100 - discount; + int cur = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (offPct > cur) { + activeDiscount.put(p.getUniqueId(), offPct); + } + return true; + } + + /** + * 商店主界面点击"优惠券"按钮后调用 + * 开启聊天监听,等待玩家输入优惠券码 + */ + public void openCouponInput(Player p) { + UUID uuid = p.getUniqueId(); + // 先取消已有的待核销优惠券 + pendingCouponCode.remove(uuid); + couponListening.add(uuid); + // 关闭UI + Bukkit.getScheduler().runTask(plugin, () -> { + if (p.isOnline()) p.closeInventory(); + }); + p.sendMessage("§6§l[优惠券] §e请在聊天栏输入优惠券码"); + p.sendMessage("§715秒内输入,过期自动取消"); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (couponListening.remove(uuid)) { + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§c优惠券输入已超时取消"); + } + } + }, 300L); + } + + + // ===== GUI: 主界面 ===== + + public void openShopMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l商店"); + int si = 0; + for (ShopCategory cat : categories) { + if (si >= ITEM_SLOTS.length) break; + ItemStack is = new ItemStack( + Material.CHEST); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l" + cat.getName()); + im.setLore(Arrays.asList( + "§7商品: §f" + + cat.getItems().size() + + " 种", + "", + "§a点击进入")); + is.setItemMeta(im); + } + g.setItem(ITEM_SLOTS[si], is); + si++; + } + // ★ 改为跳转订单中心 + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看订单记录、申请退款", + "", "§a点击进入")); + + // 在 openShopMain 方法中,原来的 slot 49/53 附近加: + + g.setItem(48, mkItem( + isCartMode(p) + ? Material.LIME_WOOL + : Material.GRAY_WOOL, + isCartMode(p) + ? "§a§l购物车模式 §7[开]" + : "§7§l购物车模式 §7[关]", + isCartMode(p) + ? "§a当前: 点击商品加入购物车" + : "§7当前: 直接购买", + "§7点击切换")); + g.setItem(50, mkItem(Material.CHEST, + "§6§l购物车 §7[" + getCartCount(p) + "件]", + "§7总价: §6" + getCartTotal(p) + "枚", + "", + "§a点击打开购物车")); + + // 在现有按钮之后,p.openInventory(g) 之前加: + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看购物订单记录", + "", "§a点击进入")); + + // 优惠券按钮 - slot 47 + String discStatus = "§7"; + int curDisc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (curDisc > 0) { + discStatus = "§a当前: " + formatZhe(100 - curDisc); + } + g.setItem(47, mkItem(Material.NAME_TAG, + "§6§l优惠券", + discStatus, + "", + "§a点击输入优惠券码", + "§7一次性使用,核销后失效")); + + g.setItem(53, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + // ===== GUI: 分类 ===== + public void openCategory(Player p, + ShopCategory cat, + int page) { + // ★ 每次重新获取最新对象 ★ + ShopCategory fresh = null; + for (ShopCategory c : categories) { + if (c.getName().equals(cat.getName())) { + fresh = c; + break; + } + } + if (fresh == null) { + openShopMain(p); + return; + } + cat = fresh; + viewCat.put(p.getUniqueId(), cat); + + int total = (int) Math.ceil( + (double) cat.getItems().size() + / PAGE_SIZE); + if (total < 1) total = 1; + if (page < 0) page = 0; + if (page >= total) page = total - 1; + viewPage.put(p.getUniqueId(), page); + + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l" + cat.getName()); + + + int start = page * PAGE_SIZE; + int end = Math.min(start + PAGE_SIZE, + cat.getItems().size()); + + for (int i = start; i < end; i++) { + int idx = i - start; + if (idx >= ITEM_SLOTS.length) break; + + // ★ item 保留原名,代表 ShopItem 数据 + ShopItem item = cat.getItems().get(i); + + // ★ is 是带完整元数据的 ItemStack(用于GUI图标) + ItemStack is = getShopStack(item, 1); + ItemMeta im = is.getItemMeta(); + + if (im != null) { + im.setDisplayName("§e§l" + + item.getDisplayName()); + List lore = new ArrayList<>(); + lore.add("§7购入: §a" + + item.getBuyPrice() + + "§7枚债券"); + lore.add("§7售出: §e" + + item.getEffectiveSellPrice() + + "§7枚债券"); + if (item.getStock() >= 0) + lore.add("§7库存: §f" + + item.getStock()); + else + lore.add("§7库存: §a无限"); + lore.add(""); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (disc > 0) { + int dis = item.getBuyPrice() + * (100 - disc) / 100; + lore.add("§7优惠价: §6" + + dis + "枚" + + " §7(-" + disc + "%)"); + } + lore.add("§a左键买1 §6右键卖1"); + lore.add("§aShift+左键买64" + + " §6Shift+右键卖64"); + im.setLore(lore); + is.setItemMeta(im); + } + + // ★ 用 is(ItemStack)放入GUI格子 + g.setItem(ITEM_SLOTS[idx], is); + } + + + g.setItem(4, mkItem(Material.BOOK, + "§e§l" + cat.getName(), + "§7共 " + cat.getItems().size() + + " 种商品")); + g.setItem(48, page > 0 + ? mkItem(Material.ARROW, + "§7上一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(49, mkItem(Material.PAPER, + "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 + ? mkItem(Material.ARROW, + "§7下一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, + "§7返回商店")); + + p.openInventory(g); + } + + // ===== GUI: 退款 ===== + + public void openRefundPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§c§l退款中心"); + + // 位移检测:比对上次记录的背包内容 + validateRefunds(p); + + // 过期清理 + synchronized (refundRecords) { + refundRecords.removeIf(RefundRecord::isExpired); + } + + int idx = 0; + synchronized (refundRecords) { + for (RefundRecord r : refundRecords) { + if (idx >= 45) break; + if (!r.playerUUID.equals(p.getUniqueId())) continue; + if (r.isExpired()) continue; + + // 盒装用潜影盒图标,普通用商品图标 + Material iconMat = (r.shulkerMaterial != null) + ? r.shulkerMaterial : r.material; + + ItemStack is = new ItemStack(iconMat); + ItemMeta im = is.getItemMeta(); + if (im != null) { + int secs = r.getSecondsLeft(); + im.setDisplayName("§e" + r.itemName + + (r.shulkerMaterial != null + ? " §7[盒装]" : " x" + r.amount)); + + List lore = new ArrayList<>(); + lore.add("§7订单: §f#" + r.timestamp); + lore.add("§7支付: §a" + r.totalPaid + + "§7枚债券"); + + if (r.shulkerMaterial != null) { + int boxCount = countShulkers(p, + r.shulkerMaterial); + lore.add("§7潜影盒: §f" + + (boxCount >= 1 + ? "§a在背包中" + : "§c已移出")); + } else { + int have = countDirect(p, r.material); + lore.add("§7物品: §f" + have + + "/" + r.amount); + } + + lore.add("§7剩余: §e" + secs + "秒"); + lore.add(""); + lore.add("§c点击退款"); + im.setLore(lore); + } + is.setItemMeta(im); + g.setItem(10 + idx, is); + idx++; + } + } + + if (idx == 0) { + g.setItem(22, mkItem(Material.BARRIER, + "§7暂无可退款记录")); + } + + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + + p.openInventory(g); + } + // 统计背包中某物品数量 + private int countInInventory(Player p, Material mat) { + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == mat) { + count += is.getAmount(); + } + } + // 潜影盒也要检查里面 + for (ItemStack is : p.getInventory().getContents()) { + if (is != null + && is.getType().name() + .endsWith("_SHULKER_BOX")) { + count += is.getAmount(); + } + } + return count; + } + + // 只统计背包中直接可见的数量(不含盒内) + // 新代码:增加按完整物品匹配的方法 + private int countDirect(Player p, Material mat) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.getType() == mat) + count += is.getAmount(); + } + return count; + } + + private int countDirectByStack(Player p, + ItemStack target) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.isSimilar(target)) + count += is.getAmount(); + } + return count; + } + // 统计背包中潜影盒数量 + private int countShulkers(Player p, Material shulker) { + if (shulker == null) return Integer.MAX_VALUE; + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == shulker) + count += is.getAmount(); + } + return count; + } + + // 退款检测:位移 OR 消耗 → 不能退 + private void validateRefunds(Player p) { + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { + it.remove(); + continue; + } + + if (r.shulkerMaterial != null) { + // 盒装:检测潜影盒是否还在背包 + int now = countShulkers( + p, r.shulkerMaterial); + if (now < 1) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 潜影盒已移出背包,无法退款"); + } + } else { + // 普通:检测商品是否还在背包 + int now = countDirect(p, r.material); + if (now < r.amount) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 物品已被使用或移出,无法退款"); + } + } + } + } + } + + + public int getPlayerPage(java.util.UUID uuid) { + return viewPage.getOrDefault(uuid, 0); + } + + public ShopCategory getPlayerCategory(java.util.UUID uuid) { + return viewCat.get(uuid); + } + + // ===== 供GUIManager调用:商店主界面点击 ===== + public boolean handleMainShopClick(Player p, int raw) { + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true; } + + if (raw == 48) { + toggleCartMode(p); + openShopMain(p); + return true; + } + if (raw == 50) { + openCart(p); + return true; + } + if (raw == 53) { + plugin.getGui().openMain(p); + return true; + } + + // ★ 新增:优惠券按钮 + if (raw == 47) { + openCouponInput(p); + return true; + } + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true;} + int idx = slotToIndex(raw); + if (idx < 0 || idx >= categories.size()) + return true; + openCategory(p, categories.get(idx), 0); + return true; + } + + // ===== 供GUIManager调用:退款中心点击 ===== + public boolean handleRefundClick(Player p, int raw) { + if (raw == 49) { openShopMain(p); return true; } + return true; + } + + // ===== 供GUIManager调用:分类页点击 ===== + public boolean handleCategoryClick(Player p, String title, + int raw, boolean left, + boolean shift) { + // plugin.getLogger().info("[Shop] ★ handleCategoryClick raw=" + raw); + + ShopManager sm = this; + ShopCategory match = null; + for (ShopCategory c : categories) { + if (title.equals("§6§l" + c.getName())) { + match = c; + break; + } + } + if (match == null) { + // plugin.getLogger().info("[Shop] ★ 匹配失败, title=[" + title + "]"); + return false; + } + // plugin.getLogger().info("[Shop] ★ 匹配到: " + match.getName()); + + viewCat.put(p.getUniqueId(), match); + + if (raw == 53) { openShopMain(p); return true; } + int pg = getPlayerPage(p.getUniqueId()); + if (raw == 48) { openCategory(p, match, pg - 1); return true; } + if (raw == 50) { openCategory(p, match, pg + 1); return true; } + + int itemIdx = slotToIndex(raw); + if (itemIdx < 0) { + plugin.getLogger().info("[Shop] ★ slot=" + raw + " 不是商品格"); + return true; + } + int actual = itemIdx + pg * PAGE_SIZE; + if (actual < 0 || actual >= match.getItems().size()) { + // plugin.getLogger().info("[Shop] ★ 超出范围 actual=" + actual + " size=" + match.getItems().size()); + return true; + } + ShopItem si = match.getItems().get(actual); + // plugin.getLogger().info("[Shop] ★ 准备购买: " + si.getId() + " left=" + left); + + int amount = shift ? 64 : 1; + if (left) { + if (shift && isCartMode(p)) { addToCart(p, si); } + else if (shift) { buyItem(p, si, 64); } + else if (isCartMode(p)) { addToCart(p, si); } + else { buyItem(p, si, 1); } + } else { + if (shift) { sellAllOf(p, si); } + else { sellItem(p, si, 1); } + } + openCategory(p, match, pg); + return true; + } + + // ===== 批量出售某商品全部数量 ===== + private void sellAllOf(Player p, ShopItem item) { + if (item == null) return; + // ★ 补上禁止出售检查 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return; + } + if (item == null) return; + Material mat = item.getMaterial(); + int total = 0; + int count = 0; + // 遍历背包清点 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + total += is.getAmount(); + count++; + } + } + if (total <= 0) { + p.sendMessage("§c你没有可出售的 §e" + + item.getDisplayName()); + return; + } + // 清除背包中该物品 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + } + } + int money = item.getEffectiveSellPrice() * total; + plugin.getBonds().addBonds( + p.getName(), money, "shop_sell", + item.getId(), "商店系统", + "批量出售" + item.getDisplayName() + + " x" + total); + if (item.getStock() >= 0) + item.setStock(item.getStock() + total); + p.sendMessage("§a批量出售: §e" + + item.getDisplayName() + + " x" + total + + " §a+" + money + "枚债券"); + } + + + // ===== 定时 ===== + + private void startHourlyTask() { + new BukkitRunnable() { + @Override + public void run() { + // ★ 汇报交给 SalesStatsManager + if (plugin.getSalesStats() != null) { + plugin.getSalesStats().report(); + } + // 重置小时销量 + for (ShopCategory cat : categories) + for (ShopItem item : cat.getItems()) + item.setHourlySales(0); + saveAll(); + } + }.runTaskTimer(plugin, 72000L, 72000L); + } + + + private void startCleanupTask() { + new BukkitRunnable() { + @Override + public void run() { + synchronized (refundRecords) { + refundRecords.removeIf( + RefundRecord::isExpired); + } + } + }.runTaskTimer(plugin, 1200L, 1200L); + } + + // ===== 工具 ===== + + private int slotToIndex(int slot) { + for (int i = 0; i < ITEM_SLOTS.length; i++) + if (ITEM_SLOTS[i] == slot) return i; + return -1; + } + + 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/VerificationManager.java b/src/main/java/Sdf1_login/VerificationManager.java index 0343ec1..3fc4ceb 100644 --- a/src/main/java/Sdf1_login/VerificationManager.java +++ b/src/main/java/Sdf1_login/VerificationManager.java @@ -1,158 +1,158 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.UUID; -import java.util.function.Consumer; - -public class VerificationManager { - - private final Main plugin; - - public VerificationManager(Main plugin) { - this.plugin = plugin; - } - - public boolean isBedrockPlayer(Player p) { - String uuid = p.getUniqueId().toString(); - return uuid.contains("f851ba4") - || uuid.startsWith("00000000-0000-0000"); - } - - public boolean isOnlineMode() { - return Bukkit.getServer().getOnlineMode(); - } - - public void verifyPremiumAsync(Player player, - Consumer callback) { - Thread t = new Thread(() -> doVerify(player, callback), - "Sdf1_login-PremiumVerify"); - t.setDaemon(true); - t.start(); - } - - private void doVerify(Player player, - Consumer callback) { - String name = player.getName(); - String uuid = player.getUniqueId().toString(); - - plugin.getLogger().info( - "[Sdf1_login] ====== 验证开始 ======"); - plugin.getLogger().info( - "[Sdf1_login] 玩家: " + name); - plugin.getLogger().info( - "[Sdf1_login] 当前UUID: " + uuid); - - try { - UUID offlineUuid = UUID.nameUUIDFromBytes( - ("OfflinePlayer:" + name) - .getBytes(StandardCharsets.UTF_8)); - String offlineStr = offlineUuid.toString(); - - plugin.getLogger().info( - "[Sdf1_login] 离线UUID: " + offlineStr); - - boolean isOfflineUuid = - player.getUniqueId().equals(offlineUuid); - - plugin.getLogger().info( - "[Sdf1_login] UUID匹配离线格式: " - + isOfflineUuid); - - if (!isOfflineUuid) { - plugin.getLogger().info( - "[Sdf1_login] 结论: 正版玩家"); - plugin.getLogger().info( - "[Sdf1_login] ====== 验证结束 ======"); - callback.accept(true); - return; - } - - plugin.getLogger().info( - "[Sdf1_login] UUID为离线格式," - + "查询Mojang API..."); - UUID mojangUuid = fetchMojangUuid(name); - - if (mojangUuid != null) { - plugin.getLogger().info( - "[Sdf1_login] Mojang返回UUID: " - + mojangUuid); - plugin.getLogger().info( - "[Sdf1_login] 结论: 盗版客户端" - + "使用了正版名字"); - } else { - plugin.getLogger().info( - "[Sdf1_login] Mojang无此名字"); - plugin.getLogger().info( - "[Sdf1_login] 结论: 纯离线玩家"); - } - - plugin.getLogger().info( - "[Sdf1_login] ====== 验证结束 ======"); - callback.accept(false); - - } catch (Exception e) { - plugin.getLogger().warning( - "[Sdf1_login] 验证异常: " - + e.getMessage()); - callback.accept(false); - } - } - - private UUID fetchMojangUuid(String name) { - HttpURLConnection conn = null; - try { - URL url = new URL( - "https://api.mojang.com/users/profiles/minecraft/" - + name); - conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); - conn.setRequestMethod("GET"); - - int code = conn.getResponseCode(); - plugin.getLogger().info( - "[Sdf1_login] Mojang API响应: " + code); - - if (code == 200) { - BufferedReader reader = - new BufferedReader( - new InputStreamReader( - conn.getInputStream(), - StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - } - reader.close(); - - String json = sb.toString(); - String uuidStr = json.replaceAll( - ".*\"id\"\\s*:\\s*\"([^\"]+)\".*", - "$1"); - plugin.getLogger().info( - "[Sdf1_login] Mojang原始UUID: " - + uuidStr); - - String formatted = uuidStr.replaceFirst( - "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", - "$1-$2-$3-$4-$5"); - return UUID.fromString(formatted); - } - } catch (Exception e) { - plugin.getLogger().warning( - "[Sdf1_login] Mojang查询异常: " - + e.getMessage()); - } finally { - if (conn != null) conn.disconnect(); - } - return null; - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.function.Consumer; + +public class VerificationManager { + + private final Main plugin; + + public VerificationManager(Main plugin) { + this.plugin = plugin; + } + + public boolean isBedrockPlayer(Player p) { + String uuid = p.getUniqueId().toString(); + return uuid.contains("f851ba4") + || uuid.startsWith("00000000-0000-0000"); + } + + public boolean isOnlineMode() { + return Bukkit.getServer().getOnlineMode(); + } + + public void verifyPremiumAsync(Player player, + Consumer callback) { + Thread t = new Thread(() -> doVerify(player, callback), + "Sdf1_login-PremiumVerify"); + t.setDaemon(true); + t.start(); + } + + private void doVerify(Player player, + Consumer callback) { + String name = player.getName(); + String uuid = player.getUniqueId().toString(); + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证开始 ======"); + plugin.getLogger().info( + "[Sdf1_login] 玩家: " + name); + plugin.getLogger().info( + "[Sdf1_login] 当前UUID: " + uuid); + + try { + UUID offlineUuid = UUID.nameUUIDFromBytes( + ("OfflinePlayer:" + name) + .getBytes(StandardCharsets.UTF_8)); + String offlineStr = offlineUuid.toString(); + + plugin.getLogger().info( + "[Sdf1_login] 离线UUID: " + offlineStr); + + boolean isOfflineUuid = + player.getUniqueId().equals(offlineUuid); + + plugin.getLogger().info( + "[Sdf1_login] UUID匹配离线格式: " + + isOfflineUuid); + + if (!isOfflineUuid) { + plugin.getLogger().info( + "[Sdf1_login] 结论: 正版玩家"); + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(true); + return; + } + + plugin.getLogger().info( + "[Sdf1_login] UUID为离线格式," + + "查询Mojang API..."); + UUID mojangUuid = fetchMojangUuid(name); + + if (mojangUuid != null) { + plugin.getLogger().info( + "[Sdf1_login] Mojang返回UUID: " + + mojangUuid); + plugin.getLogger().info( + "[Sdf1_login] 结论: 盗版客户端" + + "使用了正版名字"); + } else { + plugin.getLogger().info( + "[Sdf1_login] Mojang无此名字"); + plugin.getLogger().info( + "[Sdf1_login] 结论: 纯离线玩家"); + } + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(false); + + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 验证异常: " + + e.getMessage()); + callback.accept(false); + } + } + + private UUID fetchMojangUuid(String name) { + HttpURLConnection conn = null; + try { + URL url = new URL( + "https://api.mojang.com/users/profiles/minecraft/" + + name); + conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.setRequestMethod("GET"); + + int code = conn.getResponseCode(); + plugin.getLogger().info( + "[Sdf1_login] Mojang API响应: " + code); + + if (code == 200) { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn.getInputStream(), + StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + + String json = sb.toString(); + String uuidStr = json.replaceAll( + ".*\"id\"\\s*:\\s*\"([^\"]+)\".*", + "$1"); + plugin.getLogger().info( + "[Sdf1_login] Mojang原始UUID: " + + uuidStr); + + String formatted = uuidStr.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", + "$1-$2-$3-$4-$5"); + return UUID.fromString(formatted); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] Mojang查询异常: " + + e.getMessage()); + } finally { + if (conn != null) conn.disconnect(); + } + return null; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7a97953..e3a5009 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -60,18 +60,12 @@ commands: deny: description: 拒绝OA请求 usage: /deny <编号> [理由] - #sdf1debug: - # description: 调试指令 - # usage: /sdf1debug <玩家> take.<阶段> email: description: 绑定邮箱 usage: /email <邮箱> 绑定: description: 绑定邮箱(中文) usage: /绑定 <邮箱> - 调试: - description: 调试模式 - usage: /调试 绑定邮箱: description: 绑定邮箱 usage: /绑定邮箱 @@ -95,4 +89,13 @@ commands: usage: /cypay printer : description: 打印债券流水 - usage: /printer \ No newline at end of file + usage: /printer + shop : + description: 游戏商人 + usage: /shop + 商店: + description: 打开商店 + usage: /商店 + #testorder: + # description: 测试订单中心 + # usage: /testorder