feat: 传送系统增强 - 全局配置化有效期/冷却时间 + 灵活时间输入解析 + 请求过期清理 + HomeManager + UserGroupManager8个新方法 + WebManager/EconomyManager扩展 + 基岩版GUI三级导航

This commit is contained in:
2026-07-01 00:37:11 +08:00
parent c8e8fe88e4
commit 265503be38
9 changed files with 1732 additions and 34 deletions
+147
View File
@@ -55,6 +55,10 @@ public class ConfigManager {
public double areaProtectPricePerSqM = 10.0; // 每平米价格(债券)
public int areaProtectMaxLands = 3; // 每个玩家最大领地数
// ===== 传送配置 =====
public int tpRequestValidSeconds = 90; // 请求有效时间(秒)
public int tpSendIntervalSeconds = 10; // 发送间隔(秒)
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
public ConfigManager(File dataFolder) {
this.dataFolder = dataFolder;
@@ -195,6 +199,9 @@ public class ConfigManager {
areaProtectPricePerSqM = parseDouble(m.getOrDefault("区域防护_每平米价格", "10"), 10.0);
areaProtectMaxLands = parseInt(m.getOrDefault("区域防护_最大领地数", "3"), 3);
tpRequestValidSeconds = parseIntFromString(m.getOrDefault("传送_请求有效秒", "90"));
tpSendIntervalSeconds = parseIntFromString(m.getOrDefault("传送_发送间隔秒", "10"));
// 验证合法值
if (!"economy".equalsIgnoreCase(rewardChannel)
&& !"bonds".equalsIgnoreCase(rewardChannel)) {
@@ -281,6 +288,8 @@ public class ConfigManager {
L.add("签到债券最大=" + checkinBondMax);
L.add("补签积分倍率=" + backCheckPointMultiplier);
L.add("奖励发放方式=债券");
L.add("传送_请求有效秒=" + tpRequestValidSeconds);
L.add("传送_发送间隔秒=" + tpSendIntervalSeconds);
writeLines(file, L);
}
@@ -314,6 +323,8 @@ public class ConfigManager {
L.add("区域防护_管理员Tag=admin");
L.add("区域防护_每平米价格=10");
L.add("区域防护_最大领地数=3");
L.add("传送_请求有效秒=90");
L.add("传送_发送间隔秒=10");
writeLines(f, L);
}
public List<String> getAfkWhitelist() {
@@ -386,4 +397,140 @@ public class ConfigManager {
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; } }
/**
* 将自然语言时间字符串解析为秒数
* 支持:1:30、1分30秒、1.30、90、一分钟三十秒、壹分种叁拾秒、Ninety、Ninety秒、IX〇秒、One minute thirty seconds等
*/
public int parseIntFromString(String s) {
if (s == null) return 90;
s = s.trim().toLowerCase();
// 规则1:纯数字
try { return Integer.parseInt(s); } catch (Exception ignored) {}
// 规则2:冒号分隔 mm:ss
if (s.contains(":")) {
String[] parts = s.split(":");
try {
int min = Integer.parseInt(parts[0]);
int sec = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
return min * 60 + sec;
} catch (Exception ignored) {}
}
// 规则3:点号分隔 mm.ss
if (s.matches("\\d+\\.\\d+")) {
String[] parts = s.split("\\.");
try {
int min = Integer.parseInt(parts[0]);
int sec = Integer.parseInt(parts[1]);
return min * 60 + sec;
} catch (Exception ignored) {}
}
// 规则4:中文/英文混合解析 —— 逐字扫描
// 中文数字映射
java.util.Map<String, Integer> cnNum = new java.util.LinkedHashMap<>();
cnNum.put("零", 0); cnNum.put("一", 1); cnNum.put("壹", 1); cnNum.put("二", 2); cnNum.put("贰", 2); cnNum.put("两", 2);
cnNum.put("三", 3); cnNum.put("叁", 3); cnNum.put("四", 4); cnNum.put("肆", 4); cnNum.put("五", 5); cnNum.put("伍", 5);
cnNum.put("六", 6); cnNum.put("陆", 6); cnNum.put("七", 7); cnNum.put("漆", 7); cnNum.put("八", 8); cnNum.put("捌", 8);
cnNum.put("九", 9); cnNum.put("玖", 9);
// 罗马数字映射(简单支持)
java.util.Map<Character, Integer> romanNum = new java.util.HashMap<>();
romanNum.put('I', 1); romanNum.put('V', 5); romanNum.put('X', 10); romanNum.put('L', 50); romanNum.put('C', 100);
// 单位映射:中文
java.util.regex.Pattern unitPat = java.util.regex.Pattern.compile("(\\d+)(分[钟钟]?|秒|分钟)?");
// 尝试拆分"X分Y秒"格式
java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d+(?:\\.\\d+)?)[分钟]?(?:秒)?(\\d+(?:\\.\\d+)?[秒]?)?").matcher(s);
if (m.find()) {
try {
int minVal = (int) Float.parseFloat(m.group(1));
int secVal = 0;
if (m.group(2) != null) {
secVal = (int) Float.parseFloat(m.group(2).replaceAll("[^\\d\\.]", ""));
}
// 如果没有"分"关键字而是直接数字+秒,整个算秒
if (!s.contains("分") && !s.contains("minute")) {
return (int) Float.parseFloat(s.replaceAll("[^\\d\\.]", ""));
}
return minVal * 60 + secVal;
} catch (Exception ignored) {}
}
// 尝试英文解析:"ninety seconds", "one minute thirty seconds"
java.util.Map<String, Integer> enNum = new java.util.LinkedHashMap<>();
enNum.put("zero", 0); enNum.put("one", 1); enNum.put("two", 2); enNum.put("three", 3); enNum.put("four", 4);
enNum.put("five", 5); enNum.put("six", 6); enNum.put("seven", 7); enNum.put("eight", 8); enNum.put("nine", 9);
enNum.put("ten", 10); enNum.put("eleven", 11); enNum.put("twelve", 12); enNum.put("thirteen", 13);
enNum.put("fourteen", 14); enNum.put("fifteen", 15); enNum.put("sixteen", 16); enNum.put("seventeen", 17);
enNum.put("eighteen", 18); enNum.put("nineteen", 19); enNum.put("twenty", 20);
enNum.put("thirty", 30); enNum.put("forty", 40); enNum.put("fifty", 50); enNum.put("sixty", 60);
enNum.put("minute", 60); enNum.put("minutes", 60); enNum.put("second", 1); enNum.put("seconds", 1); enNum.put("sec", 1);
// 计算罗马数字
if (s.chars().allMatch(c -> "IVXLCDM".indexOf(c) >= 0)) {
int total = 0;
int prev = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int curr = romanNum.getOrDefault(s.charAt(i), 0);
if (curr < prev) total -= curr; else total += curr;
prev = curr;
}
if (total > 0) return total; // 纯罗马数字当作秒
}
// 尝试解析英文短语:"one minute thirty seconds", "ninety seconds", "one fifty"
try {
String[] words = s.split("\\s+");
int totalSeconds = 0;
boolean foundUnit = false;
for (String word : words) {
word = word.toLowerCase().replaceAll("[^a-z]", "");
if (word.isEmpty()) continue;
Integer val = enNum.get(word);
if (val != null) {
if (word.contains("second") || word.equals("sec")) {
totalSeconds += val; foundUnit = true;
} else if (word.equals("minute") || word.equals("minutes")) {
totalSeconds += val; foundUnit = true;
} else if (word.matches("\\d+")) {
totalSeconds += val; // 纯数字(英文单词形式),当作最小单位(秒或分取决于上下文)
} else {
totalSeconds += val * 60; // 当作分钟数
}
} else {
// 尝试阿拉伯数字
try {
int num = Integer.parseInt(word);
if (foundUnit && totalSeconds > 0) {
// 已经有单位了,这应该是个额外的数字
totalSeconds += num;
} else if (num <= 60) {
totalSeconds = num;
} else {
// 如"130" -> 130秒
totalSeconds = num;
}
} catch (Exception ignored) {}
}
}
if (foundUnit || totalSeconds > 0) return totalSeconds;
} catch (Exception ignored) {}
// 兜底:提取所有数字
java.util.regex.Matcher nm = java.util.regex.Pattern.compile("\\d+").matcher(s);
List<Integer> nums = new ArrayList<>();
while (nm.find()) nums.add(Integer.parseInt(nm.group()));
if (nums.size() == 1) return nums.get(0);
if (nums.size() == 2) {
// 第一个当分钟,第二个当秒
return nums.get(0) * 60 + nums.get(1);
}
return 90; // 默认
}
}
@@ -258,6 +258,23 @@ public class DatabaseManager {
+ ")");
safeAdd(st, "menu_snowball",
"INTEGER DEFAULT 1");
// Home传送点表 — 先DROP旧表再重建,确保结构一致
st.execute("DROP TABLE IF EXISTS homes;");
st.execute("CREATE TABLE homes ("
+ "player_name TEXT NOT NULL,"
+ "home_name TEXT NOT NULL,"
+ "world TEXT NOT NULL,"
+ "x REAL NOT NULL,"
+ "y REAL NOT NULL,"
+ "z REAL NOT NULL,"
+ "yaw REAL DEFAULT 0,"
+ "pitch REAL DEFAULT 0,"
+ "created_at INTEGER DEFAULT 0,"
+ "PRIMARY KEY(player_name, home_name)"
+ ")");
safeAdd(st, "menu_snowball",
"INTEGER DEFAULT 1");
st.close();
logger.info("[Sdf1_login] 数据库初始化完成");
} catch (Exception e) {
@@ -285,6 +302,10 @@ public class DatabaseManager {
}
}
public Connection getConnection() {
return db;
}
// ==================== UI偏好 ====================
/**
+481
View File
@@ -0,0 +1,481 @@
package Sdf1_login;
import org.bukkit.*;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerBedEnterEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Home传送点管理器
* 支持: sethome, delhome, home, homes
* 集成用户组系统: home_limit (0=全局默认, >0=组限制, -1=无限)
*/
public class HomeManager implements Listener {
private final Main plugin;
// 缓存: player_name → homes列表
private final Map<String, List<HomeData>> homesCache = new ConcurrentHashMap<>();
// 床位置缓存: player_name → Location (用于自动记录床位置)
private final Map<String, Location> bedLocations = new ConcurrentHashMap<>();
public HomeManager(Main plugin) {
this.plugin = plugin;
initDatabase();
}
// ==================== 数据结构 ====================
public static class HomeData {
public String name;
public String world;
public double x, y, z;
public float yaw, pitch;
public long createdAt;
public Location toLocation(World world) {
return new Location(world, x, y, z, yaw, pitch);
}
}
// ==================== 数据库初始化 ====================
private void initDatabase() {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) {
plugin.getLogger().warning("[Home] 无法获取数据库连接");
return;
}
java.sql.Statement st = db.createStatement();
// homes表已在DatabaseManager中创建,此处确保索引
st.execute("CREATE INDEX IF NOT EXISTS idx_homes_player ON homes(player_name)");
st.close();
plugin.getLogger().info("[Home] 数据库初始化完成");
} catch (Exception e) {
plugin.getLogger().warning("[Home] 数据库初始化失败: " + e.getMessage());
}
}
// ==================== 命令处理 ====================
public boolean handleCommand(Player player, String label, String[] args) {
String lowerLabel = label.toLowerCase();
switch (lowerLabel) {
case "sethome":
return handleSetHome(player, args);
case "delhome":
return handleDelHome(player, args);
case "home":
return handleHome(player, args);
case "homes":
return handleHomes(player, args);
default:
return false;
}
}
/**
* /sethome <名称> - 设置家
*/
private boolean handleSetHome(Player player, String[] args) {
if (args.length == 0) {
player.sendMessage("§c用法: /sethome <名称>");
return true;
}
String homeName = args[0].toLowerCase();
// 校验名称格式
if (!homeName.matches("^[a-zA-Z0-9_]{1,16}$")) {
player.sendMessage("§c家名只能包含字母、数字、下划线,长度1-16");
return true;
}
// 检查家数量限制
int maxHomes = getMaxHomes(player);
if (maxHomes != -1) {
List<HomeData> homes = getHomes(player.getName());
if (homes.size() >= maxHomes) {
player.sendMessage("§c你已达到家数量上限 (" + maxHomes + "个)");
return true;
}
}
// 检查是否已存在同名家
if (homeExists(player.getName(), homeName)) {
player.sendMessage("§c你已有一个名为 §e" + homeName + " §c的家");
return true;
}
// 保存家
Location loc = player.getLocation();
saveHome(player.getName(), homeName, loc);
player.sendMessage("§a家 §e" + homeName + " §a已设置!");
player.sendMessage("§7使用 §e/home " + homeName + " §7传送回家");
return true;
}
/**
* /delhome <名称> - 删除家
*/
private boolean handleDelHome(Player player, String[] args) {
if (args.length == 0) {
player.sendMessage("§c用法: /delhome <名称>");
return true;
}
String homeName = args[0].toLowerCase();
if (!homeExists(player.getName(), homeName)) {
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
return true;
}
deleteHome(player.getName(), homeName);
player.sendMessage("§a家 §e" + homeName + " §a已删除!");
return true;
}
/**
* /home [名称] - 传送到家
*/
private boolean handleHome(Player player, String[] args) {
if (args.length == 0) {
// 无参数: 传送到第一个家
List<HomeData> homes = getHomes(player.getName());
if (homes.isEmpty()) {
player.sendMessage("§c你还没有设置任何家");
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
return true;
}
teleportToHome(player, homes.get(0));
return true;
}
String homeName = args[0].toLowerCase();
HomeData home = getHome(player.getName(), homeName);
if (home == null) {
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
return true;
}
teleportToHome(player, home);
return true;
}
/**
* /homes - 查看所有家
*/
private boolean handleHomes(Player player, String[] args) {
List<HomeData> homes = getHomes(player.getName());
if (homes.isEmpty()) {
player.sendMessage("§e你还没有设置任何家");
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
return true;
}
int maxHomes = getMaxHomes(player);
player.sendMessage("§6§l你的家 §7(" + homes.size() + (maxHomes == -1 ? "/" + "∞" : "/" + maxHomes) + ")");
player.sendMessage("§7────────────────────");
for (HomeData home : homes) {
String worldName = home.world != null ? home.world : "未知";
player.sendMessage("§e • " + home.name + " §7[" + worldName + "]");
}
player.sendMessage("§7────────────────────");
player.sendMessage("§7使用 §e/home <名称> §7传送");
player.sendMessage("§7使用 §e/sethome <名称> §7设置新家");
player.sendMessage("§7使用 §e/delhome <名称> §7删除家");
return true;
}
// ==================== 床事件监听 ====================
/**
* 玩家上床时自动记录床位置
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerBedEnter(PlayerBedEnterEvent event) {
Player player = event.getPlayer();
Location bedLoc = event.getBed().getLocation();
bedLocations.put(player.getName(), bedLoc);
// 自动保存为"bed"家(如果不存在)
if (!homeExists(player.getName(), "bed")) {
saveHome(player.getName(), "bed", bedLoc);
player.sendMessage("§a床位置已自动保存为 §e/home bed");
}
}
/**
* 玩家重生时更新床位置
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
Location respawnLoc = event.getRespawnLocation();
// 如果重生点是床位置,更新bed家
Location bedLoc = bedLocations.get(player.getName());
if (bedLoc != null && bedLoc.getWorld() != null) {
if (respawnLoc.getWorld() != null &&
respawnLoc.getWorld().getName().equals(bedLoc.getWorld().getName()) &&
respawnLoc.distance(bedLoc) < 5) {
// 重生点就是床,更新bed家
updateHome(player.getName(), "bed", respawnLoc);
}
}
}
// ==================== 家数量限制 ====================
/**
* 获取玩家最大家数量
* @return -1=无限, 0=使用全局默认, >0=用户组独立限制
*/
private int getMaxHomes(Player player) {
UserGroupManager userGroupMgr = plugin.getUserGroup();
if (userGroupMgr == null) {
return getGlobalHomeLimit();
}
// 获取玩家最高优先级用户组
UserGroupManager.UserGroupConfig highestGroup = userGroupMgr.getHighestGroup(player.getName());
if (highestGroup == null) {
return getGlobalHomeLimit();
}
// 获取用户组的home_limit配置
int groupLimit = userGroupMgr.getHomeLimit(highestGroup.name);
if (groupLimit == -1) {
// 无限
return -1;
} else if (groupLimit == 0) {
// 跟随全局
return getGlobalHomeLimit();
} else {
// 独立限制
return groupLimit;
}
}
/**
* 获取全局家数量限制
*/
private int getGlobalHomeLimit() {
// 默认5个家,可从配置文件读取
return 5;
}
// ==================== 数据库操作 ====================
private boolean homeExists(String playerName, String homeName) {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return false;
PreparedStatement ps = db.prepareStatement(
"SELECT COUNT(*) FROM homes WHERE player_name=? AND home_name=?");
ps.setString(1, playerName);
ps.setString(2, homeName);
ResultSet rs = ps.executeQuery();
boolean exists = rs.next() && rs.getInt(1) > 0;
rs.close();
ps.close();
return exists;
} catch (SQLException e) {
return false;
}
}
private List<HomeData> getHomes(String playerName) {
// 先查缓存
List<HomeData> cached = homesCache.get(playerName);
if (cached != null) {
return cached;
}
List<HomeData> homes = new ArrayList<>();
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return homes;
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM homes WHERE player_name=? ORDER BY created_at");
ps.setString(1, playerName);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
HomeData home = new HomeData();
home.name = rs.getString("home_name");
home.world = rs.getString("world");
home.x = rs.getDouble("x");
home.y = rs.getDouble("y");
home.z = rs.getDouble("z");
home.yaw = rs.getFloat("yaw");
home.pitch = rs.getFloat("pitch");
home.createdAt = rs.getLong("created_at");
homes.add(home);
}
rs.close();
ps.close();
// 缓存
homesCache.put(playerName, homes);
} catch (SQLException e) {
plugin.getLogger().warning("[Home] 获取家列表失败: " + e.getMessage());
}
return homes;
}
private HomeData getHome(String playerName, String homeName) {
List<HomeData> homes = getHomes(playerName);
for (HomeData home : homes) {
if (home.name.equalsIgnoreCase(homeName)) {
return home;
}
}
return null;
}
private void saveHome(String playerName, String homeName, Location loc) {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return;
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO homes (player_name, home_name, world, x, y, z, yaw, pitch, created_at) "
+ "VALUES (?,?,?,?,?,?,?,?,?)");
ps.setString(1, playerName);
ps.setString(2, homeName);
ps.setString(3, loc.getWorld() != null ? loc.getWorld().getName() : "world");
ps.setDouble(4, loc.getX());
ps.setDouble(5, loc.getY());
ps.setDouble(6, loc.getZ());
ps.setFloat(7, loc.getYaw());
ps.setFloat(8, loc.getPitch());
ps.setLong(9, System.currentTimeMillis());
ps.executeUpdate();
ps.close();
// 清除缓存
homesCache.remove(playerName);
} catch (SQLException e) {
plugin.getLogger().warning("[Home] 保存家失败: " + e.getMessage());
}
}
private void updateHome(String playerName, String homeName, Location loc) {
saveHome(playerName, homeName, loc); // INSERT OR REPLACE 自动更新
}
private void deleteHome(String playerName, String homeName) {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return;
PreparedStatement ps = db.prepareStatement(
"DELETE FROM homes WHERE player_name=? AND home_name=?");
ps.setString(1, playerName);
ps.setString(2, homeName);
ps.executeUpdate();
ps.close();
// 清除缓存
homesCache.remove(playerName);
} catch (SQLException e) {
plugin.getLogger().warning("[Home] 删除家失败: " + e.getMessage());
}
}
/**
* 传送玩家到家
*/
private void teleportToHome(Player player, HomeData home) {
World world = Bukkit.getWorld(home.world);
if (world == null) {
player.sendMessage("§c世界 §e" + home.world + " §c不存在");
return;
}
Location loc = home.toLocation(world);
// 安全检查: 确保位置安全
if (loc.getBlock().getType().isSolid()) {
// 向上寻找安全位置
for (int y = (int) loc.getY(); y < 256; y++) {
Location safeLoc = new Location(world, loc.getX(), y + 1, loc.getZ());
if (!safeLoc.getBlock().getType().isSolid()) {
loc = safeLoc;
break;
}
}
}
player.teleport(loc);
player.sendMessage("§a已传送到家 §e" + home.name);
}
// ==================== Tab补全 ====================
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (!(sender instanceof Player)) {
return Collections.emptyList();
}
Player player = (Player) sender;
String cmdName = command.getName().toLowerCase();
if (cmdName.equals("home") || cmdName.equals("delhome")) {
if (args.length == 1) {
List<String> homes = new ArrayList<>();
for (HomeData home : getHomes(player.getName())) {
if (home.name.toLowerCase().startsWith(args[0].toLowerCase())) {
homes.add(home.name);
}
}
return homes;
}
} else if (cmdName.equals("sethome") || cmdName.equals("homes")) {
return Collections.emptyList();
}
return Collections.emptyList();
}
// ==================== 清理 ====================
public void shutdown() {
homesCache.clear();
bedLocations.clear();
}
}
@@ -104,6 +104,17 @@ public class LoginManager {
}
} catch (Exception ignored) {
}
// ★ 注册后立即同步用户数据到PHP
if (plugin.webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { plugin.webManager.syncUserRegistrations(); }
catch (Exception e) {
plugin.getLogger().warning("[Web通信] 注册后同步用户异常: " + e.getMessage());
}
});
}
return true;
}
+232 -5
View File
@@ -146,6 +146,7 @@ public class Main extends JavaPlugin
public AreaGUIManager areaGUIManager;
public AreaCLIManager areaCLIManager;
private TeleportManager teleportMgr;
private HomeManager homeMgr;
// 钱包流水查看目标(玩家名 → 查看目标)
@@ -545,7 +546,7 @@ public class Main extends JavaPlugin
teleportMgr = new TeleportManager(this);
getServer().getPluginManager().registerEvents(teleportMgr, this);
teleportMgr.initDatabase();
// 注册传送命令执行器和Tab补全
getCommand("tpa").setExecutor(this);
getCommand("tpaccept").setExecutor(this);
@@ -561,6 +562,18 @@ public class Main extends JavaPlugin
getCommand("tpahere").setTabCompleter(this);
getCommand("tpaall").setTabCompleter(this);
getCommand("tpacancel").setTabCompleter(this);
// ===== Home传送点系统 =====
homeMgr = new HomeManager(this);
getServer().getPluginManager().registerEvents(homeMgr, this);
getCommand("sethome").setExecutor(this);
getCommand("delhome").setExecutor(this);
getCommand("home").setExecutor(this);
getCommand("homes").setExecutor(this);
getCommand("sethome").setTabCompleter(this);
getCommand("delhome").setTabCompleter(this);
getCommand("home").setTabCompleter(this);
getCommand("homes").setTabCompleter(this);
// 注册传送命令已在 onCommand 中通过条件分支完成
@@ -673,14 +686,18 @@ public class Main extends JavaPlugin
if (garbage != null) garbage.close();
if (questTracker != null)
questTracker.shutdown();
if (homeMgr != null)
homeMgr.shutdown();
if (areaProtection != null) {
areaProtection.stopEnforceTask();
}
for (BukkitTask task : areaProtection.getPendingTasks()) {
task.cancel();
if (areaProtection != null) {
for (BukkitTask task : areaProtection.getPendingTasks()) {
task.cancel();
}
areaProtection.saveWhitelists();
areaProtection.stopEnforceTask();
}
areaProtection.saveWhitelists();
areaProtection.stopEnforceTask();
// 取消所有延时清理任务,立即清理
if (areaProtection != null) {
for (Map.Entry<UUID, BukkitTask> entry
@@ -1745,6 +1762,36 @@ public class Main extends JavaPlugin
teleportMgr.onPlayerLogin(p);
}
// ★ 用户组到期检查:登录时自动移除过期用户组
if (userGroupManager != null) {
List<String> expiredGroups = userGroupManager.checkAndRemoveExpiredForPlayer(p.getName());
if (!expiredGroups.isEmpty()) {
p.sendMessage("§c以下用户组已过期并自动移除: §e" + String.join("§c, §e", expiredGroups));
p.sendMessage("§7使用 §e/group buy <组名> §7重新购买");
}
// ★ 到期提醒:10分钟内到期的用户组
List<Map<String, Object>> expiring = userGroupManager.getExpiringGroups();
for (Map<String, Object> g : expiring) {
String group = (String) g.get("group");
long expiry = (long) g.get("expiry");
if (p.getName().equalsIgnoreCase((String) g.get("player"))) {
long remaining = expiry - System.currentTimeMillis();
if (remaining > 0) {
UserGroupManager.UserGroupConfig cfg = userGroupManager.getGroupConfig(group);
String displayName = cfg != null && !cfg.displayName.isEmpty() ? cfg.displayName : group;
int minutes = (int) (remaining / 60000);
p.sendMessage("§e§l提醒: §f用户组 §e" + displayName + " §f将在 §c" + minutes + " 分钟 §f后到期!");
if (cfg != null && cfg.autoRenew && cfg.renewPrice > 0) {
p.sendMessage("§7已开启自动续费,到期时将自动扣费 §e" + cfg.renewPrice + " 张债券");
} else {
p.sendMessage("§7使用 §e/group renew " + group + " §7手动续费");
}
}
}
}
}
// ★ 玩家加入时立即允许登录轮询(不等全量同步完成)
if (webManager != null) {
webManager.allowLoginPolling = true;
@@ -4448,6 +4495,176 @@ public class Main extends JavaPlugin
}
}
// ★ Home传送点命令
if (cmd.getName().equalsIgnoreCase("sethome")
|| cmd.getName().equalsIgnoreCase("delhome")
|| cmd.getName().equalsIgnoreCase("home")
|| cmd.getName().equalsIgnoreCase("homes")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可使用");
return true;
}
if (homeMgr == null) {
sender.sendMessage("§cHome系统未初始化");
return true;
}
try {
return homeMgr.handleCommand((Player) sender, cmd.getName(), args);
} catch (Exception e) {
sender.sendMessage("§c执行出错: " + e.getMessage());
e.printStackTrace();
return true;
}
}
// ===== /group buy|renew|info [组名] =====
if (label.equalsIgnoreCase("group")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可使用");
return true;
}
if (userGroupManager == null) {
sender.sendMessage("§c用户组系统未初始化");
return true;
}
Player p = (Player) sender;
// 检查登录状态
if (!loggedIn.contains(p.getName())) {
p.sendMessage("§c请先登录");
return true;
}
if (args.length == 0) {
// /group → 显示帮助
p.sendMessage("§6§l用户组§7 命令列表:");
p.sendMessage("§7────────────────────");
p.sendMessage("§e/group buy <组名> §7- 付费加入用户组");
p.sendMessage("§e/group renew <组名> §7- 续费用户组");
p.sendMessage("§e/group info §7- 查看我的用户组");
p.sendMessage("§7────────────────────");
// 显示可购买的用户组
List<UserGroupManager.UserGroupConfig> allGroups = userGroupManager.getAllGroups();
boolean hasBuyable = false;
for (UserGroupManager.UserGroupConfig cfg : allGroups) {
if (cfg.joinPrice > 0) {
if (!hasBuyable) {
p.sendMessage("§6可购买的用户组:");
hasBuyable = true;
}
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
p.sendMessage("§e • " + displayName + " §7- " + cfg.joinPrice + " 张债券 | 有效期 " + cfg.durationMinutes + " 分钟");
}
}
if (!hasBuyable) {
p.sendMessage("§7暂无可购买的用户组");
}
return true;
}
if (sub.equals("buy")) {
// /group buy <组名>
if (args.length < 2) {
p.sendMessage("§c用法: /group buy <组名>");
return true;
}
String groupName = args[1];
UserGroupManager.UserGroupConfig cfg = userGroupManager.getGroupConfig(groupName);
if (cfg == null) {
p.sendMessage("§c未找到用户组: " + groupName);
return true;
}
p.sendMessage("§7正在处理加入请求...");
String err = userGroupManager.joinGroupByPrice(p.getName(), cfg.name);
if (err != null) {
p.sendMessage("§c" + err);
} else {
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
p.sendMessage("§a§l成功! §7你已加入用户组 §e" + displayName);
p.sendMessage("§7到期时间: §e" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date(System.currentTimeMillis() + cfg.durationMinutes * 60 * 1000)));
}
return true;
} else if (sub.equals("renew")) {
// /group renew <组名>
if (args.length < 2) {
p.sendMessage("§c用法: /group renew <组名>");
return true;
}
String groupName = args[1];
UserGroupManager.UserGroupConfig cfg = userGroupManager.getGroupConfig(groupName);
if (cfg == null) {
p.sendMessage("§c未找到用户组: " + groupName);
return true;
}
p.sendMessage("§7正在处理续费请求...");
String err = userGroupManager.renewGroup(p.getName(), cfg.name);
if (err != null) {
p.sendMessage("§c" + err);
} else {
long newExpiry = userGroupManager.getExpiryTime(p.getName(), cfg.name);
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
p.sendMessage("§a§l续费成功! §7用户组 §e" + displayName + " §7已续期");
if (newExpiry > 0) {
p.sendMessage("§7新到期时间: §e" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date(newExpiry)));
}
}
return true;
} else if (sub.equals("info")) {
// /group info
List<Map<String, Object>> groups = userGroupManager.getPlayerGroups(p.getName());
long now = System.currentTimeMillis();
if (groups.isEmpty()) {
p.sendMessage("§e你还没有加入任何用户组");
p.sendMessage("§7使用 §e/group buy <组名> §7付费加入用户组");
return true;
}
p.sendMessage("§6§l我的用户组§7 (" + groups.size() + "个)");
p.sendMessage("§7────────────────────");
for (Map<String, Object> g : groups) {
String groupName = (String) g.get("group_name");
long expiry = (long) g.get("expiry_time");
boolean expired = (boolean) g.get("expired");
UserGroupManager.UserGroupConfig cfg = userGroupManager.getGroupConfig(groupName);
String displayName = cfg != null && !cfg.displayName.isEmpty() ? cfg.displayName : groupName;
String color = cfg != null ? cfg.displayColor : "§f";
if (expired) {
p.sendMessage(color + " • " + displayName + " §c[已过期]");
} else if (expiry > 0) {
long remaining = expiry - now;
String timeLeft;
if (remaining > 3600000) {
timeLeft = (remaining / 3600000) + "小时";
} else if (remaining > 60000) {
timeLeft = (remaining / 60000) + "分钟";
} else {
timeLeft = (remaining / 1000) + "秒";
}
p.sendMessage(color + " • " + displayName + " §7[剩余 §e" + timeLeft + " §7]");
} else {
p.sendMessage(color + " • " + displayName + " §a[永久]");
}
}
p.sendMessage("§7────────────────────");
return true;
} else {
p.sendMessage("§c未知子命令: " + sub);
p.sendMessage("§7用法: /group <buy|renew|info> [组名]");
return true;
}
}
// ===== /printer [玩家名] =====
if (label.equalsIgnoreCase("printer")) {
@@ -6231,6 +6448,16 @@ public class Main extends JavaPlugin
return cypayCommand.onTabComplete(sender, cmd, label, args);
}
// ★ Home传送点命令 - Tab补全
if (cmd.getName().equalsIgnoreCase("sethome")
|| cmd.getName().equalsIgnoreCase("delhome")
|| cmd.getName().equalsIgnoreCase("home")
|| cmd.getName().equalsIgnoreCase("homes")) {
if (sender instanceof Player && homeMgr != null) {
return homeMgr.onTabComplete(sender, cmd, label, args);
}
}
// ★ 传送系统命令 - Tab补全
if (cmd.getName().equalsIgnoreCase("tpa")
|| cmd.getName().equalsIgnoreCase("tpaccept")
+155 -19
View File
@@ -44,7 +44,10 @@ public class TeleportManager implements Listener {
// 传送冷却时间 (玩家名 → 最后发送时间戳)
private final Map<String, Long> teleportCooldown = new ConcurrentHashMap<>();
private static final long TELEPORT_COOLDOWN_MS = 30_000L; // 30秒冷却
// 请求过期时间 (请求ID/发送者→ 创建时间戳),配置化
private final Map<String, Long> teleportRequestTimes = new ConcurrentHashMap<>();
private volatile long teleportCooldownMs = 30_000L; // 可配置的冷却时间
// Inventory追踪ID集合(基岩版GUI面板)
private final Set<String> teleportPanelIds = ConcurrentHashMap.newKeySet();
@@ -53,6 +56,60 @@ public class TeleportManager implements Listener {
this.plugin = plugin;
}
/**
* 获取传送请求有效时间(秒),从ConfigManager读取
*/
private int getTpRequestValidSeconds() {
if (plugin.getConfigMgr() != null) return plugin.getConfigMgr().tpRequestValidSeconds;
return 90;
}
/**
* 获取传送发送间隔(秒),从ConfigManager读取
*/
private int getTpSendIntervalSeconds() {
if (plugin.getConfigMgr() != null) return plugin.getConfigMgr().tpSendIntervalSeconds;
return 10;
}
/**
* 检查传送请求是否过期(由配置决定)
*/
private boolean isTeleportRequestExpired(String key) {
Long createTime = teleportRequestTimes.get(key);
if (createTime == null) return true;
long elapsed = (System.currentTimeMillis() - createTime) / 1000;
return elapsed > getTpRequestValidSeconds();
}
/**
* 清理已过期的传送请求
*/
private void cleanupExpiredRequests(String playerName) {
int validSec = getTpRequestValidSeconds();
long now = System.currentTimeMillis();
// 清理过期的incoming
Set<String> incoming = incomingRequests.get(playerName);
if (incoming != null) {
incoming.removeIf(sender -> {
String key = playerName + ":" + sender;
Long t = teleportRequestTimes.get(key);
return t != null && (now - t) > (validSec * 1000L);
});
}
// 清理过期的outgoing
Set<String> outgoing = outgoingRequests.get(playerName);
if (outgoing != null) {
outgoing.removeIf(target -> {
String key = playerName + ":" + target + ":out";
Long t = teleportRequestTimes.get(key);
return t != null && (now - t) > (validSec * 1000L);
});
}
}
// ==================== 判断玩家类型 ====================
/**
@@ -101,14 +158,16 @@ public class TeleportManager implements Listener {
String lowerLabel = label.toLowerCase();
boolean bedrock = isBedrockPlayer(player);
// ★ 进入命令时先清理过期请求
cleanupExpiredRequests(player.getName());
// ★ 无参数直接执行的命令(不走面板)
switch (lowerLabel) {
case "tpacancel":
handleTPCancel(player);
return true;
case "tpauto":
handleTPAuto(player);
return true;
return handleTPAuto(player, args);
case "tpaall":
return handleTPAll(player);
}
@@ -380,8 +439,8 @@ public class TeleportManager implements Listener {
String sender = player.getName();
long now = System.currentTimeMillis();
Long lastTime = teleportCooldown.get(sender);
if (lastTime != null && now - lastTime < TELEPORT_COOLDOWN_MS) {
long remaining = (TELEPORT_COOLDOWN_MS - (now - lastTime)) / 1000;
if (lastTime != null && now - lastTime < teleportCooldownMs) {
long remaining = (teleportCooldownMs - (now - lastTime)) / 1000;
player.sendMessage("§c[传送] 请等待 §e" + remaining + " §c秒后再发送传送请求");
return true;
}
@@ -394,10 +453,15 @@ public class TeleportManager implements Listener {
// 添加到内存
outgoingRequests.computeIfAbsent(sender, k -> ConcurrentHashMap.newKeySet()).add(target.getName());
incomingRequests.computeIfAbsent(target.getName(), k -> ConcurrentHashMap.newKeySet()).add(sender);
// 记录请求创建时间
teleportRequestTimes.put(sender + ":" + target.getName(), now);
// 通知发送者(Java版带撤回按钮,基岩版纯文本)
if (isBedrockPlayer(player)) {
player.sendMessage("§a[传送] 已向 §f" + targetName + " §a发送传送请求");
// 显示请求剩余有效时间
int validSec = getTpRequestValidSeconds();
player.sendMessage("§7请求有效 §e" + validSec + " §7秒,间隔 §e" + getTpSendIntervalSeconds() + " §7秒");
player.sendMessage("§7使用 §e/tpacancel §7撤回请求");
} else {
player.sendMessage(Component.empty()
@@ -442,11 +506,17 @@ public class TeleportManager implements Listener {
String targetName = null;
if (args.length > 0) {
String arg = args[0];
try {
int index = Integer.parseInt(arg);
return handleTPAcceptCLI(player, index);
} catch (NumberFormatException e) {
// ★ 优先当玩家名处理,只有在纯数字且作为序号能找到时才当CLI处理
Player possiblePlayer = Bukkit.getServer().getPlayer(arg);
if (possiblePlayer != null && possiblePlayer.isOnline()) {
targetName = arg;
} else {
try {
int index = Integer.parseInt(arg);
return handleTPAcceptCLI(player, index);
} catch (NumberFormatException e) {
targetName = arg;
}
}
}
@@ -504,11 +574,17 @@ public class TeleportManager implements Listener {
String targetName = null;
if (args.length > 0) {
String arg = args[0];
try {
int index = Integer.parseInt(arg);
return handleTPDenyCLI(player, index);
} catch (NumberFormatException e) {
// ★ 优先当玩家名处理,在线则直接走玩家名逻辑
Player possiblePlayer = Bukkit.getServer().getPlayer(arg);
if (possiblePlayer != null && possiblePlayer.isOnline()) {
targetName = arg;
} else {
try {
int index = Integer.parseInt(arg);
return handleTPDenyCLI(player, index);
} catch (NumberFormatException e) {
targetName = arg;
}
}
}
@@ -551,8 +627,30 @@ public class TeleportManager implements Listener {
// ==================== TPAUTO - 自动接受传送 ====================
private void handleTPAuto(Player player) {
private boolean handleTPAuto(Player player, String[] args) {
String name = player.getName();
// ★ 识别插件管理员(检查Tag)
boolean isAdmin = player.getScoreboardTags().contains(plugin.getConfig2().adminTag);
if (args == null || args.length == 0) {
// 无参 = 切换自动接受
handleTPAutoToggle(player, name);
return true;
}
// ★ 管理员可以通过CLI参数配置传送参数
if (isAdmin) {
handleTPAdminConfig(player, args);
return true;
}
// 普通玩家有多个参数但非管理员 → 切换
handleTPAutoToggle(player, name);
return true;
}
private void handleTPAutoToggle(Player player, String name) {
if (autoAcceptPlayers.contains(name)) {
autoAcceptPlayers.remove(name);
removeAutoAcceptFromDB(name);
@@ -564,6 +662,44 @@ public class TeleportManager implements Listener {
}
}
/**
* 管理员通过CLI配置传送参数
* 支持语法:/tpauto valid 90、/tpauto interval 10、/tpauto valid 1:30、/tpauto interval 一分钟三十秒
*/
private boolean handleTPAdminConfig(Player player, String[] args) {
if (args.length != 2) {
player.sendMessage("§6[传送管理] §7用法:");
player.sendMessage("§e /tpauto valid <时间> — 设置请求有效时间");
player.sendMessage("§e /tpauto interval <时间> — 设置发送间隔");
player.sendMessage("§e /tpauto show — 查看当前配置");
player.sendMessage("§7支持语法: 90 | 1:30 | 1.30 | 一分钟三十秒 | Ninety | One minute thirty seconds 等");
return true;
}
String key = args[0].toLowerCase();
String val = args[1];
int seconds = plugin.getConfigMgr().parseIntFromString(val);
if ("valid".equals(key)) {
plugin.getConfigMgr().tpRequestValidSeconds = seconds;
plugin.getConfigMgr().saveSettings();
player.sendMessage("§a[传送管理] 请求有效时间已设置为 §e" + seconds + " 秒");
} else if ("interval".equals(key)) {
plugin.getConfigMgr().tpSendIntervalSeconds = seconds;
teleportCooldownMs = seconds * 1000L;
plugin.getConfigMgr().saveSettings();
player.sendMessage("§a[传送管理] 发送间隔已设置为 §e" + seconds + " 秒");
} else if ("show".equals(key)) {
player.sendMessage("§6[传送管理] §7当前配置:");
player.sendMessage("§e 请求有效时间: §f" + plugin.getConfigMgr().tpRequestValidSeconds + " 秒");
player.sendMessage("§e 发送间隔: §f" + plugin.getConfigMgr().tpSendIntervalSeconds + " 秒");
} else {
player.sendMessage("§c[传送管理] 未知参数: " + key);
player.sendMessage("§e 可用参数: valid, interval, show");
}
return true;
}
// ==================== TPAHERE - 请求传送到自己身边 ====================
private boolean handleTPAH(Player player, String targetName) {
@@ -582,8 +718,8 @@ public class TeleportManager implements Listener {
String sender = player.getName();
long now = System.currentTimeMillis();
Long lastTime = teleportCooldown.get(sender);
if (lastTime != null && now - lastTime < TELEPORT_COOLDOWN_MS) {
long remaining = (TELEPORT_COOLDOWN_MS - (now - lastTime)) / 1000;
if (lastTime != null && now - lastTime < teleportCooldownMs) {
long remaining = (teleportCooldownMs - (now - lastTime)) / 1000;
player.sendMessage("§c[传送] 请等待 §e" + remaining + " §c秒后再发送传送请求");
return true;
}
@@ -630,8 +766,8 @@ public class TeleportManager implements Listener {
// 检查冷却
Long lastTime = teleportCooldown.get(sender);
if (lastTime != null && now - lastTime < TELEPORT_COOLDOWN_MS) {
long remaining = (TELEPORT_COOLDOWN_MS - (now - lastTime)) / 1000;
if (lastTime != null && now - lastTime < teleportCooldownMs) {
long remaining = (teleportCooldownMs - (now - lastTime)) / 1000;
player.sendMessage("§c[传送] 请等待 §e" + remaining + " §c秒后再发送传送请求");
return true;
}
@@ -1096,7 +1232,7 @@ public class TeleportManager implements Listener {
break;
case 3: // 自动接收开关
event.setCancelled(true);
handleTPAuto(player);
handleTPAuto(player, new String[]{});
openTeleportPanel(player);
break;
case 4: // 全服传送
+426 -2
View File
@@ -1,5 +1,7 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import java.io.File;
import java.sql.*;
import java.util.*;
@@ -68,6 +70,13 @@ public class UserGroupManager {
try { st.executeUpdate("ALTER TABLE user_group_config ADD COLUMN default_perms TEXT DEFAULT '{}'"); } catch (Exception ignored) {}
try { st.executeUpdate("ALTER TABLE user_group_config ADD COLUMN is_permanent INTEGER DEFAULT 1"); } catch (Exception ignored) {}
try { st.executeUpdate("ALTER TABLE user_group_config ADD COLUMN duration_minutes INTEGER DEFAULT 0"); } catch (Exception ignored) {}
// Home相关
try { st.executeUpdate("ALTER TABLE user_group_config ADD COLUMN home_limit INTEGER DEFAULT 0"); } catch (Exception ignored) {}
// 付费相关
try { st.executeUpdate("ALTER TABLE user_group_config ADD COLUMN join_price INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { st.executeUpdate("ALTER TABLE user_group_config ADD COLUMN auto_renew INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { st.executeUpdate("ALTER TABLE user_group_config ADD COLUMN renew_price INTEGER DEFAULT 0"); } catch (Exception ignored) {}
// duration_minutes已有(旧列),复用
st.close();
} catch (Exception e) {
@@ -92,6 +101,11 @@ public class UserGroupManager {
cfg.landPricePerSqm = rs.getInt("land_price_per_sqm");
cfg.maxLands = rs.getInt("max_lands");
cfg.defaultPerms = rs.getString("default_perms");
cfg.homeLimit = rs.getInt("home_limit");
cfg.joinPrice = rs.getInt("join_price");
cfg.autoRenew = rs.getInt("auto_renew") == 1;
cfg.renewPrice = rs.getInt("renew_price");
cfg.durationMinutes = rs.getLong("duration_minutes");
if (cfg.name != null && !cfg.name.isEmpty()) {
groupConfigs.put(cfg.name, cfg);
}
@@ -111,8 +125,9 @@ public class UserGroupManager {
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO user_group_config "
+ "(group_name, display_name, display_color,"
+ " priority, land_price_per_sqm, max_lands, default_perms)"
+ " VALUES (?,?,?,?,?,?,?)");
+ " priority, land_price_per_sqm, max_lands, default_perms,"
+ " home_limit, join_price, auto_renew, renew_price, duration_minutes)"
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
ps.setString(1, cfg.name);
ps.setString(2, cfg.displayName);
ps.setString(3, cfg.displayColor);
@@ -120,6 +135,11 @@ public class UserGroupManager {
ps.setInt(5, cfg.landPricePerSqm);
ps.setInt(6, cfg.maxLands);
ps.setString(7, cfg.defaultPerms != null ? cfg.defaultPerms : "{}");
ps.setInt(8, cfg.homeLimit);
ps.setInt(9, cfg.joinPrice);
ps.setInt(10, cfg.autoRenew ? 1 : 0);
ps.setInt(11, cfg.renewPrice);
ps.setLong(12, cfg.durationMinutes);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
@@ -263,6 +283,391 @@ public class UserGroupManager {
return true;
}
/**
* 添加玩家到用户组(带到期时间)
* @param expiryTimeMillis 到期时间戳(ms), 0=永久
* @return null=成功, 非null=错误消息
*/
public String addPlayerWithExpiry(String player, String groupName, String addedBy, long expiryTimeMillis) {
if (!isValidPlayerName(player)) {
return "玩家名格式不正确,仅支持英文字母、数字和下划线(3-16位)";
}
UserGroupConfig cfg = getGroupConfig(groupName);
if (cfg == null) return "未找到用户组: " + groupName;
groupName = cfg.name;
DatabaseManager dbMgr = plugin.getDb();
if (dbMgr != null && !dbMgr.userExists(player)) {
return "玩家 " + player + " 尚未注册,请确认玩家名是否正确";
}
long now = System.currentTimeMillis();
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO user_group_member "
+ "(player_name, group_name, added_by, added_time, expiry_time)"
+ " VALUES (?,?,?,?,?)");
ps.setString(1, player);
ps.setString(2, groupName);
ps.setString(3, addedBy);
ps.setLong(4, now);
ps.setLong(5, expiryTimeMillis);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
plugin.getLogger().warning("[UserGroup] addPlayerWithExpiry failed: " + e.getMessage());
return "数据库错误: " + e.getMessage();
}
pushMemberToPHP(player, groupName, "add");
return null;
}
/**
* 付费加入用户组
* @return null=成功, 非null=错误消息
*/
public String joinGroupByPrice(String player, String groupName) {
UserGroupConfig cfg = getGroupConfig(groupName);
if (cfg == null) return "未找到用户组: " + groupName;
groupName = cfg.name;
if (cfg.joinPrice <= 0) return "该用户组不开放付费加入";
if (cfg.durationMinutes <= 0) return "该用户组配置异常(有效时长未设置)";
// 检查是否已在组内(有效期内)
if (isPlayerInGroup(player, groupName)) {
List<Map<String, Object>> groups = getPlayerGroups(player);
long now = System.currentTimeMillis();
for (Map<String, Object> g : groups) {
if (groupName.equals(g.get("group_name"))) {
long expiry = (long) g.get("expiry_time");
if (expiry <= 0 || expiry > now) {
return "你已经是该用户组的成员" + (expiry > 0 ? "(到期: " + formatExpiry(expiry) + ")" : "(永久)");
}
}
}
}
// 扣费
BondManager bondMgr = plugin.getBondManager();
if (bondMgr == null) return "债券系统未初始化";
int balance = bondMgr.getBonds(player);
if (balance < cfg.joinPrice) {
return "债券不足!需要 " + cfg.joinPrice + " 张,当前 " + balance + " 张";
}
boolean deducted = bondMgr.deductBonds(player, cfg.joinPrice,
"group_join", "", "system",
"付费加入用户组: " + groupName);
if (!deducted) return "扣费失败,请稍后重试";
// 计算到期时间
long now = System.currentTimeMillis();
long expiryTime = now + (long) cfg.durationMinutes * 60 * 1000;
// 加入组
String err = addPlayerWithExpiry(player, groupName, "paid_join", expiryTime);
if (err != null) {
// 回退扣费
bondMgr.addBonds(player, cfg.joinPrice, "group_join_refund", "", "system", "加入失败退款: " + groupName);
return err;
}
plugin.getLogger().info("[UserGroup] " + player + " 付费加入用户组 " + groupName + ",扣费 " + cfg.joinPrice + " 张,到期: " + formatExpiry(expiryTime));
return null;
}
/**
* 续费用户组(延长到期时间)
* @return null=成功, 非null=错误消息
*/
public String renewGroup(String player, String groupName) {
UserGroupConfig cfg = getGroupConfig(groupName);
if (cfg == null) return "未找到用户组: " + groupName;
groupName = cfg.name;
if (cfg.renewPrice <= 0) return "该用户组不支持续费";
// 查找当前成员记录
long currentExpiry = 0;
boolean found = false;
try {
PreparedStatement ps = db.prepareStatement(
"SELECT expiry_time FROM user_group_member WHERE player_name=? AND group_name=?");
ps.setString(1, player);
ps.setString(2, groupName);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
currentExpiry = rs.getLong("expiry_time");
found = true;
}
rs.close();
ps.close();
} catch (SQLException e) {
return "数据库错误: " + e.getMessage();
}
if (!found) return "你不在该用户组中,请先 /group buy " + groupName;
// 如果已过期,不能续费,需要重新购买
long now = System.currentTimeMillis();
if (currentExpiry > 0 && currentExpiry <= now) {
return "用户组已过期,请重新购买";
}
// 扣费
BondManager bondMgr = plugin.getBondManager();
if (bondMgr == null) return "债券系统未初始化";
int balance = bondMgr.getBonds(player);
if (balance < cfg.renewPrice) {
return "债券不足!需要 " + cfg.renewPrice + " 张,当前 " + balance + " 张";
}
boolean deducted = bondMgr.deductBonds(player, cfg.renewPrice,
"group_renew", "", "system",
"续费用户组: " + groupName);
if (!deducted) return "扣费失败,请稍后重试";
// 延长到期时间:从当前到期时间往后延(如果已过期则从当前时间开始)
long baseTime = (currentExpiry > now) ? currentExpiry : now;
long newExpiry = baseTime + (long) cfg.durationMinutes * 60 * 1000;
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE user_group_member SET expiry_time=? WHERE player_name=? AND group_name=?");
ps.setLong(1, newExpiry);
ps.setString(2, player);
ps.setString(3, groupName);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
return "数据库错误: " + e.getMessage();
}
pushMemberToPHP(player, groupName, "add");
plugin.getLogger().info("[UserGroup] " + player + " 续费用户组 " + groupName + ",扣费 " + cfg.renewPrice + " 张,新到期: " + formatExpiry(newExpiry));
return null;
}
/**
* 检查并移除已过期的玩家用户组(用于登录时调用)
* @return 过期被移除的组列表 [{player, group}]
*/
public List<Map<String, Object>> checkAndRemoveExpired() {
List<Map<String, Object>> expired = new ArrayList<>();
long now = System.currentTimeMillis();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT player_name, group_name FROM user_group_member WHERE expiry_time > 0 AND expiry_time <= ?");
ps.setLong(1, now);
ResultSet rs = ps.executeQuery();
List<String[]> toRemove = new ArrayList<>();
while (rs.next()) {
String[] item = new String[]{rs.getString("player_name"), rs.getString("group_name")};
toRemove.add(item);
}
rs.close();
ps.close();
for (String[] item : toRemove) {
String player = item[0];
String group = item[1];
// 删除过期记录
PreparedStatement del = db.prepareStatement(
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
del.setString(1, player);
del.setString(2, group);
del.executeUpdate();
del.close();
pushMemberToPHP(player, group, "remove");
expired.add(Map.of("player", player, "group", group));
}
if (!expired.isEmpty()) {
plugin.getLogger().info("[UserGroup] 自动清理 " + expired.size() + " 个过期用户组成员");
}
} catch (SQLException e) {
plugin.getLogger().warning("[UserGroup] checkAndRemoveExpired failed: " + e.getMessage());
}
return expired;
}
/**
* 检查指定玩家的过期并移除
*/
public List<String> checkAndRemoveExpiredForPlayer(String player) {
List<String> expiredGroups = new ArrayList<>();
long now = System.currentTimeMillis();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT group_name FROM user_group_member WHERE player_name=? AND expiry_time > 0 AND expiry_time <= ?");
ps.setString(1, player);
ps.setLong(2, now);
ResultSet rs = ps.executeQuery();
List<String> toRemove = new ArrayList<>();
while (rs.next()) {
toRemove.add(rs.getString("group_name"));
}
rs.close();
ps.close();
for (String group : toRemove) {
PreparedStatement del = db.prepareStatement(
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
del.setString(1, player);
del.setString(2, group);
del.executeUpdate();
del.close();
pushMemberToPHP(player, group, "remove");
expiredGroups.add(group);
}
if (!expiredGroups.isEmpty()) {
plugin.getLogger().info("[UserGroup] " + player + " 过期用户组已清理: " + String.join(", ", expiredGroups));
}
} catch (SQLException e) {
plugin.getLogger().warning("[UserGroup] checkAndRemoveExpiredForPlayer failed: " + e.getMessage());
}
return expiredGroups;
}
/**
* 自动续费(到期前自动扣费延长)
* @return true=续费成功, false=续费失败或不需要
*/
public boolean autoRenewGroup(String player, String groupName) {
UserGroupConfig cfg = getGroupConfig(groupName);
if (cfg == null || !cfg.autoRenew || cfg.renewPrice <= 0) return false;
// 查当前到期时间
long currentExpiry = 0;
try {
PreparedStatement ps = db.prepareStatement(
"SELECT expiry_time FROM user_group_member WHERE player_name=? AND group_name=?");
ps.setString(1, player);
ps.setString(2, groupName);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
currentExpiry = rs.getLong("expiry_time");
}
rs.close();
ps.close();
} catch (SQLException e) {
return false;
}
// 只在即将到期时续费(10分钟内)
long now = System.currentTimeMillis();
if (currentExpiry <= 0 || currentExpiry > now + 10 * 60 * 1000) return false;
// 扣费
BondManager bondMgr = plugin.getBondManager();
if (bondMgr == null) return false;
int balance = bondMgr.getBonds(player);
if (balance < cfg.renewPrice) {
plugin.getLogger().info("[UserGroup] " + player + " 自动续费失败: 债券不足(" + balance + "/" + cfg.renewPrice + ")");
return false;
}
boolean deducted = bondMgr.deductBonds(player, cfg.renewPrice,
"group_auto_renew", "", "system",
"自动续费用户组: " + groupName);
if (!deducted) return false;
long newExpiry = now + (long) cfg.durationMinutes * 60 * 1000;
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE user_group_member SET expiry_time=? WHERE player_name=? AND group_name=?");
ps.setLong(1, newExpiry);
ps.setString(2, player);
ps.setString(3, groupName);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
return false;
}
pushMemberToPHP(player, groupName, "add");
// 通知在线玩家
Player onlinePlayer = plugin.getServer().getPlayerExact(player);
if (onlinePlayer != null && onlinePlayer.isOnline()) {
onlinePlayer.sendMessage("§a用户组 §e" + (cfg.displayName.isEmpty() ? groupName : cfg.displayName) + " §a已自动续费!");
onlinePlayer.sendMessage("§7新到期时间: §e" + formatExpiry(newExpiry));
}
return true;
}
/**
* 获取玩家用户组到期时间
* @return 到期时间戳(ms), 0=永久, -1=不在组内
*/
public long getExpiryTime(String player, String groupName) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT expiry_time FROM user_group_member WHERE player_name=? AND group_name=?");
ps.setString(1, player);
ps.setString(2, groupName);
ResultSet rs = ps.executeQuery();
long expiry = -1;
if (rs.next()) {
expiry = rs.getLong("expiry_time");
}
rs.close();
ps.close();
return expiry;
} catch (SQLException e) {
return -1;
}
}
/**
* 获取即将到期的玩家列表(10分钟内到期且开启自动续费的)
* 用于定时器轮询提醒
*/
public List<Map<String, Object>> getExpiringGroups() {
List<Map<String, Object>> list = new ArrayList<>();
long now = System.currentTimeMillis();
long tenMinLater = now + 10 * 60 * 1000;
try {
PreparedStatement ps = db.prepareStatement(
"SELECT m.player_name, m.group_name, m.expiry_time, "
+ "c.auto_renew, c.renew_price "
+ "FROM user_group_member m "
+ "LEFT JOIN user_group_config c ON m.group_name = c.group_name "
+ "WHERE m.expiry_time > 0 AND m.expiry_time <= ? AND m.expiry_time > ?");
ps.setLong(1, tenMinLater);
ps.setLong(2, now);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("player", rs.getString("player_name"));
row.put("group", rs.getString("group_name"));
row.put("expiry", rs.getLong("expiry_time"));
row.put("autoRenew", rs.getInt("auto_renew") == 1);
row.put("renewPrice", rs.getInt("renew_price"));
list.add(row);
}
rs.close();
ps.close();
} catch (SQLException e) {
plugin.getLogger().warning("[UserGroup] getExpiringGroups failed: " + e.getMessage());
}
return list;
}
/**
* 格式化到期时间为可读字符串
*/
private String formatExpiry(long expiryTimeMillis) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new java.util.Date(expiryTimeMillis));
}
/** 获取玩家所属的所有有效组(排除过期) */
public List<Map<String, Object>> getPlayerGroups(String player) {
List<Map<String, Object>> list = new ArrayList<>();
@@ -308,6 +713,18 @@ public class UserGroupManager {
return best;
}
/**
* 获取用户组的Home数量限制
* @return 0=跟随全局, >0=独立限制, -1=无限
*/
public int getHomeLimit(String groupName) {
UserGroupConfig cfg = getGroupConfig(groupName);
if (cfg == null) {
return 0; // 默认跟随全局
}
return cfg.homeLimit;
}
public boolean isPlayerInGroup(String player, String groupName) {
try {
PreparedStatement ps = db.prepareStatement(
@@ -445,5 +862,12 @@ public class UserGroupManager {
public int landPricePerSqm = -1; // -1 = 使用全局
public int maxLands = -1; // -1 = 使用全局
public String defaultPerms = "{}"; // JSON
// Home相关
public int homeLimit = 0; // 0=跟随全局, >0=独立限制, -1=无限
// 付费相关
public int joinPrice = 0; // 加入价格(债券), 0=不开放付费
public boolean autoRenew = false; // 是否自动续费
public int renewPrice = 0; // 续费价格(债券)
public long durationMinutes = 0; // 有效时长(分钟), 0=永久
}
}
+244 -8
View File
@@ -65,7 +65,8 @@ public class WebManager {
private static final int TIMER_B = 1; // 交易 0~10秒
private static final int TIMER_C = 2; // 其它 10~20秒
private static final int TIMER_D = 3; // 领地数据同步 15~25秒(独立,防SQL锁)
private final long[] lastRunTimestamps = new long[4];
private static final int TIMER_E = 4; // 用户组续费轮询 20~30秒(独立,防SQL锁)
private final long[] lastRunTimestamps = new long[5];
private final Object scheduleLock = new Object();
// ★ PHP锁库退避:检测到database is locked时暂停所有非关键定时器
@@ -1366,15 +1367,17 @@ public class WebManager {
syncAfterAllOffline = false;
lastSyncDone = false;
// ★ 玩家上线恢复:重启Timer B/C(之前全员下线时已暂停)
// ★ 玩家上线恢复:重启Timer B/C/E(之前全员下线时已暂停)
// ★ Timer D已独立运行,不再需要重启
if (timersBCPaused) {
timersBCPaused = false;
long randB = (long)(Math.random() * 10) * 2;
long randC = (long)(Math.random() * 10) * 2;
scheduleTimerB(40L + randB); // 2秒后重启B
scheduleTimerC(120L + randC); // 6秒后重启C
plugin.getLogger().info("[合并C] ★ 玩家上线,Timer B/C已恢复(随机偏移B=" + (randB/20) + "s C=" + (randC/20) + "s)");
long randE = (long)(Math.random() * 10) * 2;
scheduleTimerB(40L + randB); // 2秒后重启B
scheduleTimerC(120L + randC); // 6秒后重启C
scheduleTimerE(240L + randE); // 12秒后重启E
plugin.getLogger().info("[合并C] ★ 玩家上线,Timer B/C/E已恢复(随机偏移B=" + (randB/20) + "s C=" + (randC/20) + "s E=" + (randE/20) + "s)");
}
// 玩家在线:全量批处理
@@ -1525,11 +1528,13 @@ public class WebManager {
long randB = (long)(Math.random() * 10) * 2;
long randC = (long)(Math.random() * 10) * 2;
long randD = (long)(Math.random() * 10) * 2;
long randE = (long)(Math.random() * 10) * 2;
scheduleTimerA(40L + randA);
scheduleTimerB(140L + randB);
scheduleTimerC(240L + randC);
scheduleTimerD(340L + randD);
plugin.getLogger().info("[Web通信] ★ 合并定时器已启动(随机偏移A=" + (randA/20) + "s B=" + (randB/20) + "s C=" + (randC/20) + "s D=" + (randD/20) + "s)");
scheduleTimerE(440L + randE);
plugin.getLogger().info("[Web通信] ★ 合并定时器已启动(随机偏移A=" + (randA/20) + "s B=" + (randB/20) + "s C=" + (randC/20) + "s D=" + (randD/20) + "s E=" + (randE/20) + "s)");
}
// Timer A 内部计数器:每N轮同步一次在线玩家(保持PHP心跳不断)
@@ -1804,6 +1809,169 @@ public class WebManager {
}.runTaskLaterAsynchronously(plugin, ticks);
}
/**
* 定时器E — 用户组续费轮询(20~30秒,独立防SQL锁)
* ★ 拉取PHP发起的续费请求,随玩家离线自动关停
*/
private void scheduleTimerE(long ticks) {
new BukkitRunnable() {
@Override
public void run() {
long now = System.currentTimeMillis();
synchronized (scheduleLock) { lastRunTimestamps[TIMER_E] = now; }
// ★ 全员下线暂停:不调度下一轮
if (timersBCPaused) {
plugin.getLogger().info("[续费轮询E] ★ 全员下线,Timer E已暂停");
return; // 不调度下一轮,定时器自然停止
}
// ★ PHP锁库退避检查
if (phpBusyUntil > now) {
scheduleTimerE(calcStaggeredDelay(TIMER_E, 20, 30));
return;
}
// 拉取PHP续费请求
try {
pollGroupRenewRequests();
} catch (Exception e) {
plugin.getLogger().warning("[续费轮询E] 异常: " + e.getMessage());
}
// ★ 到期提醒检查(每轮执行)
try {
checkAndNotifyExpiringGroups();
} catch (Exception e) {
plugin.getLogger().warning("[续费轮询E] 到期提醒异常: " + e.getMessage());
}
// 自调度下一轮(20~30秒,错峰)
scheduleTimerE(calcStaggeredDelay(TIMER_E, 20, 30));
}
}.runTaskLaterAsynchronously(plugin, ticks);
}
/**
* 拉取PHP发起的用户组续费请求
* PHP写入web_group_renew表(player_name, group_name, action=pending)
* Java拉取后执行续费扣费,完成后回调PHP
*/
private void pollGroupRenewRequests() {
try {
String endpoint = "api/land_api.php";
java.util.Map<String, String> params = new java.util.LinkedHashMap<>();
params.put("action", "poll_group_renews");
params.put("secret", secretKey);
String resp = httpGet(endpoint, params);
if (resp == null || resp.isEmpty()) return;
detectPhpBusy(resp);
// 解析JSON: {"success":true, "renews":[{player_name, group_name, req_id}]}
if (!resp.contains("\"success\":true") || !resp.contains("\"renews\"")) return;
// 提取renews数组
int arrStart = resp.indexOf("\"renews\":[");
if (arrStart < 0) return;
arrStart += 10;
int arrEnd = resp.indexOf("]", arrStart);
if (arrEnd < 0) return;
String arr = resp.substring(arrStart, arrEnd).trim();
if (arr.isEmpty() || arr.equals("null")) return;
// 逐个处理(简单JSON解析)
String[] items = arr.split("\\},\\s*\\{");
for (String item : items) {
item = item.replaceAll("[\\{\\}]", "").trim();
if (item.isEmpty()) continue;
String playerName = extractJsonString(item, "player_name");
String groupName = extractJsonString(item, "group_name");
String reqId = extractJsonString(item, "req_id");
if (playerName == null || groupName == null || reqId == null) continue;
// 执行续费
UserGroupManager ugm = plugin.getUserGroup();
if (ugm == null) continue;
String err = ugm.renewGroup(playerName, groupName);
// 回调PHP
String callbackAction = (err == null) ? "renew_group_callback" : "renew_group_callback";
String result = (err == null) ? "success" : "failed:" + err;
String callbackBody = "{\"secret\":\"" + escapeJson(secretKey) + "\",\"req_id\":\"" + escapeJson(reqId) + "\",\"result\":\"" + escapeJson(result) + "\"}";
try {
String callbackResp = httpPost("api/land_api.php?action=" + callbackAction, callbackBody);
detectPhpBusy(callbackResp);
} catch (Exception cbEx) {
plugin.getLogger().warning("[续费轮询E] 回调PHP失败: " + cbEx.getMessage());
}
plugin.getLogger().info("[续费轮询E] 处理续费请求: " + playerName + " → " + groupName + " → " + (err == null ? "成功" : "失败: " + err));
}
} catch (Exception e) {
plugin.getLogger().warning("[续费轮询E] pollGroupRenewRequests异常: " + e.getMessage());
}
}
/**
* 检查即将到期的用户组并发送提醒
* 10分钟内到期 → 提醒,5分钟内到期且自动续费 → 尝试扣费
*/
private void checkAndNotifyExpiringGroups() {
UserGroupManager ugm = plugin.getUserGroup();
if (ugm == null) return;
List<Map<String, Object>> expiring = ugm.getExpiringGroups();
long now = System.currentTimeMillis();
for (Map<String, Object> g : expiring) {
String player = (String) g.get("player");
String group = (String) g.get("group");
long expiry = (long) g.get("expiry");
boolean autoRenew = (boolean) g.get("autoRenew");
long remaining = expiry - now;
if (remaining <= 0) continue;
// 在线玩家发送提醒
Player onlinePlayer = plugin.getServer().getPlayerExact(player);
if (onlinePlayer != null && onlinePlayer.isOnline()) {
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(group);
String displayName = cfg != null && !cfg.displayName.isEmpty() ? cfg.displayName : group;
int minutes = (int) (remaining / 60000);
if (minutes <= 1 && autoRenew) {
// 1分钟内到期,尝试自动续费
boolean renewed = ugm.autoRenewGroup(player, group);
if (!renewed) {
onlinePlayer.sendMessage("§c§l自动续费失败! §e用户组 " + displayName + " §c将在 " + minutes + " 分钟后到期");
onlinePlayer.sendMessage("§7请使用 §e/group renew " + group + " §7手动续费");
}
} else if (minutes <= 5) {
// 5分钟内到期
onlinePlayer.sendMessage("§e§l紧急提醒: §f用户组 §e" + displayName + " §f将在 §c" + minutes + " 分钟 §f后到期!");
if (autoRenew && cfg != null && cfg.renewPrice > 0) {
onlinePlayer.sendMessage("§7自动续费将在到期时执行,扣费 §e" + cfg.renewPrice + " 张债券");
} else {
onlinePlayer.sendMessage("§7使用 §e/group renew " + group + " §7手动续费");
}
} else if (minutes <= 10) {
// 10分钟内到期
onlinePlayer.sendMessage("§e提醒: 用户组 §e" + displayName + " §7将在 §c" + minutes + " 分钟 §7后到期");
if (autoRenew && cfg != null && cfg.renewPrice > 0) {
onlinePlayer.sendMessage("§7已开启自动续费");
} else {
onlinePlayer.sendMessage("§7使用 §e/group renew " + group + " §7续费");
}
}
}
}
}
// ==================== 高频轮询器失败计数 ====================
/** 交易轮询连续失败计数 */
@@ -3095,6 +3263,11 @@ public class WebManager {
gs.append("\"priority\":").append(cfg.priority).append(",");
gs.append("\"land_price_per_sqm\":").append(cfg.landPricePerSqm).append(",");
gs.append("\"max_lands\":").append(cfg.maxLands).append(",");
gs.append("\"home_limit\":").append(cfg.homeLimit).append(",");
gs.append("\"join_price\":").append(cfg.joinPrice).append(",");
gs.append("\"auto_renew\":").append(cfg.autoRenew ? 1 : 0).append(",");
gs.append("\"renew_price\":").append(cfg.renewPrice).append(",");
gs.append("\"duration_minutes\":").append(cfg.durationMinutes).append(",");
gs.append("\"default_perms\":\"").append(escapeJson(cfg.defaultPerms != null ? cfg.defaultPerms : "{}")).append("\"");
gs.append("}");
first = false;
@@ -3180,6 +3353,11 @@ public class WebManager {
|| existing.priority != cfg.priority
|| existing.landPricePerSqm != cfg.landPricePerSqm
|| existing.maxLands != cfg.maxLands
|| existing.homeLimit != cfg.homeLimit
|| existing.joinPrice != cfg.joinPrice
|| existing.autoRenew != cfg.autoRenew
|| existing.renewPrice != cfg.renewPrice
|| existing.durationMinutes != cfg.durationMinutes
|| !safeEq(existing.defaultPerms, cfg.defaultPerms);
if (isDifferent) {
ugm.saveGroupConfigToDB(cfg);
@@ -3288,6 +3466,16 @@ public class WebManager {
if (!priceStr.isEmpty()) try { cfg.landPricePerSqm = Integer.parseInt(priceStr); } catch (Exception ignored) {}
String maxStr = extractJsonField(obj, "max_lands");
if (!maxStr.isEmpty()) try { cfg.maxLands = Integer.parseInt(maxStr); } catch (Exception ignored) {}
String homeLimStr = extractJsonField(obj, "home_limit");
if (!homeLimStr.isEmpty()) try { cfg.homeLimit = Integer.parseInt(homeLimStr); } catch (Exception ignored) {}
String joinStr = extractJsonField(obj, "join_price");
if (!joinStr.isEmpty()) try { cfg.joinPrice = Integer.parseInt(joinStr); } catch (Exception ignored) {}
String autoRenStr = extractJsonField(obj, "auto_renew");
if (!autoRenStr.isEmpty()) try { cfg.autoRenew = Integer.parseInt(autoRenStr) == 1; } catch (Exception ignored) {}
String renewStr = extractJsonField(obj, "renew_price");
if (!renewStr.isEmpty()) try { cfg.renewPrice = Integer.parseInt(renewStr); } catch (Exception ignored) {}
String durStr = extractJsonField(obj, "duration_minutes");
if (!durStr.isEmpty()) try { cfg.durationMinutes = Long.parseLong(durStr); } catch (Exception ignored) {}
cfg.defaultPerms = extractJsonStringSafe(obj, "default_perms");
return cfg;
} catch (Exception e) {
@@ -3617,6 +3805,54 @@ public class WebManager {
}
break;
}
case "group_buy": {
// ★ 玩家端付费加入用户组:PHP写入pending → Java拉取执行
String buyGroup = String.valueOf(changeData.getOrDefault("group_name", ""));
String buyPlayer = String.valueOf(changeData.getOrDefault("player", ""));
if (buyGroup.isEmpty() || buyPlayer.isEmpty()) {
plugin.getLogger().warning("[Web通信] group_buy: 参数缺失");
break;
}
// 执行付费加入
UserGroupManager ugm2 = plugin.getUserGroup();
if (ugm2 != null) {
String err = ugm2.joinGroupByPrice(buyPlayer, buyGroup);
if (err != null) {
plugin.getLogger().warning("[Web通信] 付费加入失败: " + err);
} else {
plugin.getLogger().info("[Web通信] 玩家付费加入用户组: " + buyPlayer + " → " + buyGroup);
}
}
applied = true;
break;
}
case "group_renew": {
// ★ 玩家端续费用户组:PHP写入pending → Java拉取执行
String renewGroup = String.valueOf(changeData.getOrDefault("group_name", ""));
String renewPlayer = String.valueOf(changeData.getOrDefault("player", ""));
int renewPrice = ((Number) changeData.getOrDefault("renew_price", 0)).intValue();
int durationMinutes = ((Number) changeData.getOrDefault("duration_minutes", 0)).intValue();
if (renewGroup.isEmpty() || renewPlayer.isEmpty()) {
plugin.getLogger().warning("[Web通信] group_renew: 参数缺失");
break;
}
// 执行续费
UserGroupManager ugm = plugin.getUserGroup();
if (ugm != null) {
String err = ugm.renewGroup(renewPlayer, renewGroup);
if (err != null) {
plugin.getLogger().warning("[Web通信] 续费失败: " + err);
} else {
plugin.getLogger().info("[Web通信] 玩家续费用户组: " + renewPlayer + " → " + renewGroup);
}
}
applied = true;
break;
}
default:
plugin.getLogger().fine("[Web通信] 未知变更类型: " + changeType);
}
@@ -4006,8 +4242,8 @@ public class WebManager {
syncData.add(u);
}
// ★ 无变化静默:对比用户数据hash
String currentHash = syncData.size() + ":" + syncData.hashCode();
// ★ 无变化静默:对比MD5 hash,避免无效网络请求
String currentHash = syncData.toString().hashCode() + "_" + syncData.size();
if (currentHash.equals(lastUserRegistrationHash)) return; // 无变化,跳过
lastUserRegistrationHash = currentHash;
+15
View File
@@ -135,4 +135,19 @@ commands:
tpahere:
description: 请求传送ta到自己身边
usage: /tpahere
sethome:
description: 设置一个家
usage: /sethome <名称>
delhome:
description: 删除一个家
usage: /delhome <名称>
home:
description: 传送到家
usage: /home [名称]
homes:
description: 查看所有家
usage: /homes
group:
description: 用户组管理
usage: /group <buy|renew|info> [组名]