fix/feat: 修复 PVP 区域实时榜显示为总榜的问题,新增虚拟货币债券系统(BondManager/BondPrinter/CDKManager/UserGroupManager)

This commit is contained in:
ypsdf1
2026-05-22 14:33:07 +08:00
parent c9da8c8ec2
commit 851375502d
13 changed files with 3363 additions and 427 deletions
+1
View File
@@ -33,6 +33,7 @@
<version>1.21.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
+593
View File
@@ -0,0 +1,593 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.File;
import java.sql.*;
import java.util.*;
public class BondManager {
private final Main plugin;
private Connection db;
// ===== 交易类型常量 =====
public static final String TX_ADMIN_GIVE = "admin_give";
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
public static final String TX_TRANSFER_OUT = "transfer_out";
public static final String TX_TRANSFER_IN = "transfer_in";
public static final String TX_REDEEM = "redeem";
public static final String TX_SHOP_BUY = "shop_buy";
public static final String TX_DAILY_SIGN = "daily_sign";
public static final String TX_FREEZE = "freeze";
public static final String TX_UNFREEZE = "unfreeze";
public static final int MAX_WATER_DAYS = 14;
public static final int ADMIN_MAX_WATER_DAYS = 30;
public BondManager(Main plugin) {
this.plugin = plugin;
initDB();
}
// ======================== 数据库初始化 ========================
private void initDB() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(), "bond.db");
db = DriverManager.getConnection(
"jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
// 债券余额(含冻结状态)
st.execute("CREATE TABLE IF NOT EXISTS bonds ("
+ "player_name TEXT PRIMARY KEY,"
+ "amount INTEGER DEFAULT 0,"
+ "status TEXT DEFAULT 'normal')");
// 兼容旧表
try {
st.execute("ALTER TABLE bonds "
+ "ADD COLUMN status "
+ "TEXT DEFAULT 'normal'");
} catch (SQLException ignored) {}
// 旧流水(保留兼容)
st.execute("CREATE TABLE IF NOT EXISTS bond_log ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "player_name TEXT NOT NULL,"
+ "amount INTEGER NOT NULL,"
+ "reason TEXT DEFAULT '',"
+ "time INTEGER DEFAULT 0)");
// 新交易流水表
st.execute("CREATE TABLE IF NOT EXISTS "
+ "bond_transaction ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "player_name TEXT NOT NULL,"
+ "type TEXT NOT NULL,"
+ "amount INTEGER NOT NULL,"
+ "target_player TEXT DEFAULT '',"
+ "operator TEXT DEFAULT '',"
+ "reason TEXT DEFAULT '',"
+ "balance_after INTEGER DEFAULT 0,"
+ "time INTEGER DEFAULT 0)");
try {
st.execute("CREATE INDEX "
+ "IF NOT EXISTS idx_tx_pt "
+ "ON bond_transaction("
+ "player_name, time)");
} catch (SQLException ignored) {}
try {
st.execute("ALTER TABLE bond_transaction "
+ "ADD COLUMN balance_before "
+ "INTEGER DEFAULT 0");
} catch (SQLException ignored) {}
st.close();
} catch (Exception e) {
throw new RuntimeException(
"[Bond] DB init failed: "
+ e.getMessage(), e);
}
}
// ======================== 账户状态 ========================
public boolean isFrozen(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT status FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String s = rs.getString("status");
rs.close(); ps.close();
return "frozen".equals(s);
}
rs.close(); ps.close();
} catch (SQLException ignored) {}
return false;
}
public String getAccountStatus(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT status FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String s = rs.getString("status");
rs.close(); ps.close();
return s != null ? s : "normal";
}
rs.close(); ps.close();
} catch (SQLException ignored) {}
return "normal";
}
public void freezeAccount(String player) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET status='frozen' "
+ "WHERE player_name=?");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
}
public void unfreezeAccount(String player) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET status='normal' "
+ "WHERE player_name=?");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
}
private void ensureAccount(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR IGNORE INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,0,'normal')");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException ignored) {}
}
// ======================== 余额查询 ========================
public int getBonds(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT amount FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
int v = 0;
if (rs.next()) v = rs.getInt("amount");
rs.close(); ps.close();
return v;
} catch (SQLException e) { return 0; }
}
// ======================== 增加债券 ========================
public int addBonds(String player, int amount) {
return addBondsInternal(player, amount,
TX_ADMIN_GIVE, "", "",
"管理员发放");
}
public int addBonds(String player, int amount,
String type,
String targetPlayer,
String operator,
String reason) {
if (isFrozen(player)) return -1;
return addBondsInternal(player, amount,
type, targetPlayer, operator, reason);
}
private int addBondsInternal(String player,
int amount,
String type,
String targetPlayer,
String operator,
String reason) {
int before = getBonds(player);
int after = before + amount;
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();
}
logTransaction(player, type, amount,
targetPlayer, operator, reason,
before, after);
logLegacy(player, amount);
return after;
}
// ======================== 扣除债券 ========================
private boolean deductBondsInternal(String player,
int amount,
String type,
String targetPlayer,
String operator,
String reason) {
int before = getBonds(player);
if (before < amount) return false;
int after = before - amount;
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET amount=? "
+ "WHERE player_name=?");
ps.setInt(1, after);
ps.setString(2, player);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
logTransaction(player, type, amount,
targetPlayer, operator, reason,
before, after);
logLegacy(player, -amount);
return true;
}
// ======================== 流水写入 ========================
private void logTransaction(String player,
String type,
int amount,
String targetPlayer,
String operator,
String reason,
int balanceBefore,
int balanceAfter) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bond_transaction "
+ "(player_name, type, amount,"
+ " target_player, operator,"
+ " reason, balance_before,"
+ " balance_after, time)"
+ " VALUES (?,?,?,?,?,?,?,?,?)");
ps.setString(1, player);
ps.setString(2, type);
ps.setInt(3, amount);
ps.setString(4,
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.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// ======================== 扣除债券 ========================
/** 完整版:含冻结检查 + 流水记录 */
public boolean deductBonds(String player, int amount,
String type,
String targetPlayer,
String operator,
String reason) {
if (isFrozen(player)) return false;
return deductBondsInternal(player, amount,
type, targetPlayer, operator, reason);
}
public boolean setBonds(String player, int amount) {
ensureAccount(player);
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, amount);
ps.setInt(3, amount);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return true;
}
// ======================== 转账 ========================
public TransferResult transfer(String from,
String to,
int amount) {
if (from.equalsIgnoreCase(to))
return new TransferResult(false,
"不能给自己转账");
if (isFrozen(from))
return new TransferResult(false,
"你的账户已被冻结");
if (isFrozen(to))
return new TransferResult(false,
to + " 的账户已被冻结");
if (isSameIP(from, to))
return new TransferResult(false,
"不能与同IP玩家转账");
if (!deductBondsInternal(from, amount,
TX_TRANSFER_OUT, to, from,
"转账给" + to))
return new TransferResult(false, "债券不足");
addBondsInternal(to, amount,
TX_TRANSFER_IN, from, from,
"收到来自" + from + "的转账");
return new TransferResult(true, "转账成功");
}
// ======================== 同IP检测 ========================
public boolean isSameIP(String p1, String p2) {
String ip1 = getPlayerIPAny(p1);
String ip2 = getPlayerIPAny(p2);
if (ip1 == null || ip2 == null) return false;
return ip1.equals(ip2);
}
/** 获取同IP下的所有其他在线玩家 */
public List<String> getSameIPPlayers(String player) {
String ip = getPlayerIPAny(player);
if (ip == null) return Collections.emptyList();
List<String> list = new ArrayList<>();
for (Player p :
plugin.getServer().getOnlinePlayers()) {
String pIp = plugin.getPlayerIP(p);
if (pIp != null && pIp.equals(ip)
&& !p.getName()
.equalsIgnoreCase(player)) {
list.add(p.getName());
}
}
return list;
}
private String getPlayerIPAny(String player) {
// 在线:直接取
Player p = plugin.getServer()
.getPlayerExact(player);
if (p != null) {
String ip = plugin.getPlayerIP(p);
if (ip != null) return ip;
}
// 离线:查主库
try {
Object v = plugin.getDb()
.getField(player, "last_login_ip");
if (v != null) return v.toString();
} catch (Exception ignored) {}
try {
Object v = plugin.getDb()
.getField(player, "register_ip");
if (v != null) return v.toString();
} catch (Exception ignored) {}
return null;
}
// ======================== 流水查询 ========================
public List<Map<String, Object>> getTransactions(
String player, int days) {
days = Math.min(days, MAX_WATER_DAYS);
long cutoff = System.currentTimeMillis()
- (long) days * 24 * 60 * 60 * 1000;
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND time>=? "
+ "ORDER BY time DESC");
ps.setString(1, player);
ps.setLong(2, cutoff);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("type", rs.getString("type"));
row.put("amount",
rs.getInt("amount"));
row.put("target_player",
rs.getString("target_player"));
row.put("operator",
rs.getString("operator"));
row.put("reason",
rs.getString("reason"));
row.put("balance_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return list;
}
// 放在 getTransactions 方法后面:
/**
* 获取该玩家全部流水(不限时间,供打印使用)
*/
public List<Map<String, Object>>
getAllTransactions(String player) {
List<Map<String, Object>> list =
new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "ORDER BY time ASC");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("player_name",
rs.getString("player_name"));
row.put("type", rs.getString("type"));
row.put("amount",
rs.getInt("amount"));
row.put("target_player",
rs.getString("target_player"));
row.put("operator",
rs.getString("operator"));
row.put("reason",
rs.getString("reason"));
row.put("balance_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
// 放在 getAllTransactions 方法后面:
/**
* 获取所有有流水记录的玩家名(去重)
*/
public List<String> getAllPlayerNames() {
List<String> list = new ArrayList<>();
try {
Statement st = db.createStatement();
ResultSet rs = st.executeQuery(
"SELECT DISTINCT player_name "
+ "FROM bond_transaction "
+ "ORDER BY player_name ASC");
while (rs.next()) {
list.add(rs.getString("player_name"));
}
rs.close(); st.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
public List<Map<String, Object>>
getTransferTransactions(
String player, int days) {
days = Math.min(days, MAX_WATER_DAYS);
long cutoff = System.currentTimeMillis()
- (long) days * 24 * 60 * 60 * 1000;
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND time>=? "
+ "AND (type=? OR type=?) "
+ "ORDER BY time DESC");
ps.setString(1, player);
ps.setLong(2, cutoff);
ps.setString(3, TX_TRANSFER_OUT);
ps.setString(4, TX_TRANSFER_IN);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("type", rs.getString("type"));
row.put("amount",
rs.getInt("amount"));
row.put("target_player",
rs.getString("target_player"));
row.put("reason",
rs.getString("reason"));
row.put("balance_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return list;
}
private void logLegacy(String player, int amount) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bond_log "
+ "(player_name,amount,time) "
+ "VALUES (?,?,?)");
ps.setString(1, player);
ps.setInt(2, amount);
ps.setLong(3,
System.currentTimeMillis());
ps.executeUpdate(); ps.close();
} catch (SQLException ignored) {}
}
public Map<String, Object> getLog(
String player, int limit) {
Map<String, Object> info = new LinkedHashMap<>();
info.put("balance", getBonds(player));
return info;
}
// ======================== 转账结果 ========================
public static class TransferResult {
public final boolean success;
public final String message;
public TransferResult(boolean success,
String message) {
this.success = success;
this.message = message;
}
}
}
+356
View File
@@ -0,0 +1,356 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.io.File;
import java.io.FileOutputStream;
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;
public class BondPrinter {
private final Main plugin;
private final File outputDir;
private static final SimpleDateFormat FILE_SDF =
new SimpleDateFormat(
"yyyy-MM-dd_HH-mm-ss");
private static final SimpleDateFormat CELL_SDF =
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
private static final LinkedHashMap<String, String>
TYPE_NAMES = new LinkedHashMap<>();
static {
TYPE_NAMES.put("admin_give", "管理员发放");
TYPE_NAMES.put("admin_deduct", "管理员扣除");
TYPE_NAMES.put("transfer_out", "转出");
TYPE_NAMES.put("transfer_in", "转入");
TYPE_NAMES.put("redeem", "CDK兑换");
TYPE_NAMES.put("shop_buy", "商城购买");
TYPE_NAMES.put("daily_sign", "签到奖励");
TYPE_NAMES.put("freeze", "冻结");
TYPE_NAMES.put("unfreeze", "解冻");
}
public BondPrinter(Main plugin) {
this.plugin = plugin;
this.outputDir = new File(
plugin.getDataFolder(), "printer");
if (!this.outputDir.exists()) {
this.outputDir.mkdirs();
}
}
// ============ 打印单个玩家 ============
public void printPlayer(String player,
CommandSender sender) {
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() -> doPrintPlayer(
player, sender));
}
private void doPrintPlayer(String player,
CommandSender sender) {
try {
List<Map<String, Object>> txs =
plugin.getBonds()
.getAllTransactions(player);
String fileName = player + "_"
+ FILE_SDF.format(new Date())
+ ".xls";
File file = new File(outputDir, fileName);
deleteOld(player);
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(file),
StandardCharsets.UTF_8));
try {
writeXmlHeader(pw,
escXml(player));
writeXmlRow(pw,
new String[]{
"时间", "类型",
"金额", "对象",
"操作者", "理由",
"操作前余额",
"操作后余额"});
for (Map<String, Object> tx : txs) {
writeXmlRow(pw, new String[]{
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"))
});
}
writeXmlFooter(pw);
pw.flush();
} finally {
pw.close();
}
if (sender != null) {
sender.sendMessage("§a[打印] §f"
+ player + " §a流水已导出");
sender.sendMessage("§7文件: §f"
+ fileName);
sender.sendMessage("§7共 §f"
+ txs.size()
+ " §7条,§724小时后删除");
}
} catch (Exception e) {
if (sender != null) {
sender.sendMessage("§c导出失败: "
+ e.getMessage());
}
plugin.getLogger().severe(
"[Printer] " + e.getMessage());
}
}
// ============ 打印全服 ============
public void printAll(CommandSender sender) {
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() -> doPrintAll(sender));
}
private void doPrintAll(CommandSender sender) {
try {
List<String> players =
plugin.getBonds()
.getAllPlayerNames();
if (players.isEmpty()) {
if (sender != null) {
sender.sendMessage(
"§c没有流水数据");
}
return;
}
String fileName = "全服流水_"
+ FILE_SDF.format(new Date())
+ ".xls";
File file = new File(outputDir, fileName);
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(file),
StandardCharsets.UTF_8));
try {
writeXmlHeader(pw, "全服流水");
writeXmlRow(pw,
new String[]{
"玩家", "时间",
"类型", "金额",
"对象", "操作者",
"理由",
"操作前余额",
"操作后余额"});
Set<String> written = new HashSet<>();
int totalRows = 0;
for (String player : players) {
List<Map<String, Object>> txs =
plugin.getBonds()
.getAllTransactions(
player);
for (Map<String, Object> 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++;
}
}
writeXmlFooter(pw);
pw.flush();
if (sender != null) {
sender.sendMessage(
"§a[打印] §f全服流水"
+ " §a已导出");
sender.sendMessage("§7文件: §f"
+ fileName);
sender.sendMessage("§7共 §f"
+ players.size()
+ " 名玩家,§f"
+ totalRows + " §7条");
sender.sendMessage(
"§724小时后自动删除");
}
} finally {
pw.close();
}
} catch (Exception e) {
if (sender != null) {
sender.sendMessage("§c导出失败: "
+ e.getMessage());
}
plugin.getLogger().severe(
"[Printer] " + e.getMessage());
}
}
// ============ 清理 ============
public void cleanupOldFiles() {
long now = System.currentTimeMillis();
long maxAge = 24L * 60 * 60 * 1000;
File[] files = outputDir.listFiles(
(d, n) -> n.endsWith(".xls"));
if (files == null) {
return;
}
int count = 0;
for (File f : files) {
if (now - f.lastModified() > maxAge) {
if (f.delete()) {
count++;
}
}
}
if (count > 0) {
plugin.getLogger().info(
"[Printer] 清理 "
+ count + " 个过期文件");
}
}
private void deleteOld(String player) {
File[] old = outputDir.listFiles(
(d, n) -> n.startsWith(player + "_")
&& n.endsWith(".xls"));
if (old != null) {
for (File f : old) {
f.delete();
}
}
}
// ============ XML工具 ============
private void writeXmlHeader(PrintWriter pw,
String sheetName) {
pw.println("<?xml version=\"1.0\"?>");
pw.println("<?mso-application "
+ "progid=\"Excel.Sheet\"?>");
pw.println("<Workbook xmlns="
+ "\"urn:schemas-microsoft-com"
+ ":office:spreadsheet\"");
pw.println(" xmlns:ss="
+ "\"urn:schemas-microsoft-com"
+ ":office:spreadsheet\">");
pw.println(" <Worksheet ss:Name=\""
+ sheetName + "\">");
pw.println(" <Table>");
}
private void writeXmlRow(PrintWriter pw,
String[] cells) {
pw.println(" <Row>");
for (String cell : cells) {
pw.println(" <Cell>"
+ "<Data ss:Type=\"String\">"
+ escXml(cell != null ? cell : "")
+ "</Data></Cell>");
}
pw.println(" </Row>");
}
private void writeXmlFooter(PrintWriter pw) {
pw.println(" </Table>");
pw.println(" </Worksheet>");
pw.println("</Workbook>");
}
private String escXml(String val) {
if (val == null) {
return "";
}
return val.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
private String txTime(Map<String, Object> tx) {
Object t = tx.get("time");
if (t == null) return "";
long ms = ((Number) t).longValue();
return CELL_SDF.format(new Date(ms));
}
private String txTypeName(
Map<String, Object> tx) {
String type = (String) tx.get("type");
return TYPE_NAMES.getOrDefault(type, type);
}
private int txInt(Map<String, Object> tx,
String key) {
Object v = tx.get(key);
if (v == null) return 0;
return ((Number) v).intValue();
}
private String txStr(Map<String, Object> tx,
String key) {
Object v = tx.get(key);
return v != null ? v.toString() : "";
}
}
+551
View File
@@ -0,0 +1,551 @@
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;
private Connection db;
private final Map<String, CDKInput> pendingInputs
= new ConcurrentHashMap<>();
public CDKManager(Main plugin) {
this.plugin = plugin;
initDB();
}
private void initDB() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(), "bond.db");
db = DriverManager.getConnection(
"jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
st.execute("CREATE TABLE IF NOT EXISTS cdk ("
+ "code TEXT PRIMARY KEY,"
+ "amount INTEGER DEFAULT 0,"
+ "type TEXT DEFAULT 'bond',"
+ "used INTEGER DEFAULT 0,"
+ "used_by TEXT DEFAULT '',"
+ "created_time INTEGER DEFAULT 0,"
+ "used_time INTEGER DEFAULT 0)");
st.close();
} catch (Exception e) {
throw new RuntimeException(
"[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 + " 条");
}
/** 单文件加载,返回成功数 */
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 方法 =====
public String redeem(String code, String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT amount,type,used FROM cdk "
+ "WHERE code=?");
ps.setString(1, code);
ResultSet rs = ps.executeQuery();
if (!rs.next()) {
rs.close(); ps.close();
return null;
}
int amount = rs.getInt("amount");
String type = rs.getString("type");
int used = rs.getInt("used");
rs.close(); ps.close();
if (used == 1) return "该兑换码已被使用";
// [新增] 冻结检查
if (plugin.getBonds().isFrozen(player)) {
return "账户已被冻结,无法兑换";
}
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.setString(3, code);
up.executeUpdate(); up.close();
if ("bond".equals(type)) {
// [修改] 使用带流水的重载方法
plugin.getBonds().addBonds(player, amount,
BondManager.TX_REDEEM, "",
"system", "CDK兑换: " + code);
return "兑换成功!获得 " + amount + " 债券";
}
return null;
} catch (SQLException e) {
return "兑换失败";
}
}
// ===== 聊天输入系统(主插件转发调用)=====
public void requestInput(Player p, String type, String context) {
CDKInput input = new CDKInput();
input.type = type;
input.context = context;
input.time = System.currentTimeMillis();
pendingInputs.put(p.getName(), input);
}
public boolean isListening(String playerName) {
return pendingInputs.containsKey(playerName);
}
public void onChat(Player p, String msg) {
CDKInput input = pendingInputs.remove(p.getName());
if (input == null) return;
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无效的兑换码");
return;
}
if ("econ".equals(input.type)) {
handleEconInput(p, msg.trim(), input.context);
return;
}
if ("bond".equals(input.type)) {
handleBondInput(p, msg.trim(), input.context);
}
}
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();
} catch (Exception ignored) {}
if (econ == null) {
admin.sendMessage("§c经济系统不可用");
return;
}
if (!msg.startsWith("+") && !msg.startsWith("-")) {
admin.sendMessage("§c格式: §a+100 §7给钱 / §c-50 §7扣钱");
return;
}
try {
String numStr = msg.substring(1).trim();
double amount = Double.parseDouble(numStr);
if (amount <= 0) {
admin.sendMessage("§c金额必须大于0");
return;
}
if (msg.startsWith("-")) {
Player tp = plugin.getServer().getPlayerExact(target);
if (tp == null) {
admin.sendMessage("§c" + target + " 不在线");
return;
}
if (!econ.has(tp, amount)) {
admin.sendMessage("§c" + target + " 余额不足");
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==============");
} 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==============");
}
}
} catch (NumberFormatException e) {
admin.sendMessage("§c格式错误,请输入 §a+数字 §7或 §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 活动奖励");
return;
}
try {
String sign = msg.substring(0, 1);
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");
return;
}
String opName = admin.getName();
String txType = "-".equals(sign)
? BondManager.TX_ADMIN_DEDUCT
: BondManager.TX_ADMIN_GIVE;
if ("-".equals(sign)) {
if (plugin.getBonds()
.isFrozen(target)) {
admin.sendMessage("§c" + target
+ " 账户已被冻结");
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"
+ "=============");
}
} else {
admin.sendMessage("§c" + target
+ " 债券不足(当前: "
+ oldBal + ")");
}
} else {
if (plugin.getBonds()
.isFrozen(target)) {
admin.sendMessage("§c" + target
+ " 账户已被冻结");
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"
+ "=============");
}
}
} catch (NumberFormatException e) {
admin.sendMessage("§c格式错误,请输入 "
+ "§a+数字 §7或 §c-数字");
}
}
public void cancelInput(String player) {
pendingInputs.remove(player);
}
static class CDKInput {
String type;
String context;
long time;
}
}
+37 -47
View File
@@ -35,36 +35,28 @@ public class CheckInManager {
}
Object streakObj = plugin.getDb()
.getField(name,
"checkin_streak");
.getField(name, "checkin_streak");
int streak = streakObj != null
? ((Number) streakObj)
.intValue() : 0;
? ((Number) streakObj).intValue()
: 0;
String lastDate = (String) plugin
.getDb().getField(name,
"last_checkin_date");
String today = new SimpleDateFormat(
"yyyy-MM-dd")
String lastDate = (String) plugin.getDb()
.getField(name, "last_checkin_date");
String today = new SimpleDateFormat("yyyy-MM-dd")
.format(new Date());
if (lastDate != null
&& !lastDate.isEmpty()) {
if (lastDate != null && !lastDate.isEmpty()) {
try {
java.util.Date lastD =
new SimpleDateFormat(
"yyyy-MM-dd")
new SimpleDateFormat("yyyy-MM-dd")
.parse(lastDate);
java.util.Date todayD =
new SimpleDateFormat(
"yyyy-MM-dd")
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) {
} else if (diff > 86400000L) {
streak++;
}
} catch (Exception ignored) {
@@ -79,41 +71,35 @@ public class CheckInManager {
else if (streak >= 14) multi = 1.3;
else if (streak >= 7) multi = 1.1;
int basePoints = plugin.getConfig2()
.checkinPoints;
int points =
(int) (basePoints * multi);
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().addPoints(name, points);
plugin.getDb().setField(name, "checkin_streak", streak);
plugin.getDb().setField(name, "last_checkin_date", today);
Object totalObj = plugin.getDb()
.getField(name,
"total_checkin_days");
.getField(name, "total_checkin_days");
int total = totalObj != null
? ((Number) totalObj)
.intValue() : 0;
plugin.getDb().setField(
name,
"total_checkin_days",
? ((Number) totalObj).intValue()
: 0;
plugin.getDb().setField(name, "total_checkin_days",
total + 1);
return plugin.getConfig2()
.msg("checkin_success")
.replace("{points}",
String.valueOf(points))
.replace("{streak}",
String.valueOf(streak))
.replace("{multi}",
String.valueOf(multi));
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);
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()
@@ -257,8 +243,12 @@ public class CheckInManager {
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) {
+569
View File
@@ -0,0 +1,569 @@
package Sdf1_login;
import org.bukkit.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 java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class CypayCommand
implements CommandExecutor, TabCompleter {
private final Main plugin;
private static final SimpleDateFormat SDF =
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
public CypayCommand(Main plugin) {
this.plugin = plugin;
}
private boolean hasAdminPerm(CommandSender s) {
if (s.isOp()) return true;
if (!(s instanceof Player)) return true;
Player p = (Player) s;
String tag =
plugin.getConfig2().adminTag;
return !tag.isEmpty()
&& p.getScoreboardTags().contains(tag);
}
private int parseAmount(String s) {
return Integer.parseInt(
s.trim().replaceAll("[+]", ""));
}
private List<String> filter(
List<String> list, String prefix) {
String low = prefix.toLowerCase();
return list.stream()
.filter(x -> x.toLowerCase()
.startsWith(low))
.collect(Collectors.toList());
}
@Override
public boolean onCommand(CommandSender sender,
Command cmd,
String label,
String[] args) {
// ===== /cypay — 查自己 =====
if (args.length == 0) {
String name = sender instanceof Player
? sender.getName() : "Console";
int bonds =
plugin.getBonds().getBonds(name);
String status = plugin.getBonds()
.getAccountStatus(name);
sender.sendMessage(
"§e§l[债券] §f" + name
+ " 余额: §a" + bonds);
sender.sendMessage(
"§e§l[债券] §f状态: "
+ ("frozen".equals(status)
? "§c冻结中"
: "§a正常"));
return true;
}
// ===== /cypay import =====
if (args.length == 2
&& "import".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String file = args[1];
if (!file.endsWith(".txt"))
file += ".txt";
int result = plugin.getCDK()
.importFromFile(file);
if (result == -1)
sender.sendMessage("§c文件不存在: "
+ file);
else
sender.sendMessage("§a导入完成: "
+ result + " 条");
return true;
}
// ===== /cypay freeze =====
if (args.length == 2
&& "freeze".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
if (plugin.getBonds().isFrozen(target)) {
sender.sendMessage("§c" + target
+ " 已处于冻结状态");
return true;
}
plugin.getBonds().freezeAccount(target);
sender.sendMessage("§a已冻结 " + target
+ " 的债券账户");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage(
"§c§l[系统] 你的债券账户"
+ "已被管理员冻结");
return true;
}
// ===== /cypay unfreeze =====
if (args.length == 2
&& "unfreeze".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
if (!plugin.getBonds()
.isFrozen(target)) {
sender.sendMessage("§c" + target
+ " 未被冻结");
return true;
}
plugin.getBonds().unfreezeAccount(target);
sender.sendMessage("§a已解冻 " + target
+ " 的债券账户");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage(
"§a§l[系统] 你的债券账户"
+ "已被解冻");
return true;
}
// ===== /cypay water [days] =====
if (args.length >= 2
&& "water".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
int days = 7;
if (args.length >= 3) {
try {
days = Integer.parseInt(args[2]);
} catch (NumberFormatException ignored) {
}
}
days = Math.min(days,
BondManager.ADMIN_MAX_WATER_DAYS);
if (days <= 0) days = 1;
List<Map<String, Object>> txs =
plugin.getBonds()
.getTransactions(
target, days);
sender.sendMessage(
"§e§l===== " + target
+ " 流水记录 (近"
+ days + "天) =====");
sender.sendMessage("§f当前余额: §a"
+ plugin.getBonds()
.getBonds(target)
+ " §f状态: "
+ (plugin.getBonds()
.isFrozen(target)
? "§c冻结" : "§a正常"));
if (txs.isEmpty()) {
sender.sendMessage("§7 (暂无记录)");
} else {
for (Map<String, Object> tx : txs) {
sender.sendMessage(
formatTxLine(tx));
}
}
sender.sendMessage(
"§e§l=========================="
+ "==========");
return true;
}
// ===== /cypay printer [player] =====
if ("printer".equalsIgnoreCase(args[0])) {
if (!hasAdminPerm(sender)
&& !(sender instanceof Player)
== false) {
// 控制台也可以用
if (!(sender instanceof Player))
; // 放行
else if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
}
if (args.length == 1) {
// 打印全服
sender.sendMessage(
"§e正在导出全服流水...");
plugin.getBonds(); // 确保实例就绪
if (plugin.getBondPrinter() != null) {
plugin.getBondPrinter()
.printAll(sender);
} else {
sender.sendMessage("§cPrinter未初始化");
}
return true;
}
if (args.length == 2) {
// 打印指定玩家
String target = args[1];
sender.sendMessage("§e正在导出 "
+ target + " 的流水...");
if (plugin.getBondPrinter() != null) {
plugin.getBondPrinter()
.printPlayer(target, sender);
} else {
sender.sendMessage("§cPrinter未初始化");
}
return true;
}
}
// ===== /cypay give/remove =====
if (args.length == 3) {
String action = args[0].toLowerCase();
if ("give".equals(action)
|| "remove".equals(action)) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
try {
int amount =
parseAmount(args[2]);
if (amount <= 0) {
sender.sendMessage(
"§c金额必须大于0");
return true;
}
String txType =
"give".equals(action)
? BondManager
.TX_ADMIN_GIVE
: BondManager
.TX_ADMIN_DEDUCT;
String opName =
sender instanceof Player
? sender.getName()
: "Console";
if ("give".equals(action)) {
plugin.getBonds().addBonds(
target, amount,
txType, "",
opName, "管理员发放");
sender.sendMessage(
"§a已给 " + target
+ " " + amount
+ " 债券");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage(
"§a管理员给了你 "
+ amount
+ " 债券");
} else {
if (plugin.getBonds()
.deductBonds(target,
amount,
txType, "",
opName,
"管理员扣除")) {
sender.sendMessage(
"§a已扣除 " + target
+ " " + amount
+ " 债券");
Player tp = plugin
.getServer()
.getPlayerExact(
target);
if (tp != null)
tp.sendMessage(
"§c管理员扣除了"
+ "你的 "
+ amount
+ " 债券");
} else {
sender.sendMessage(
"§c" + target
+ " 债券不足"
+ "或账户冻结");
}
}
} catch (NumberFormatException e) {
sender.sendMessage(
"§c金额格式错误");
}
return true;
}
}
// ===== /cypay <player> <amount> — 仅玩家转账 =====
if (args.length == 2) {
try {
int amount = parseAmount(args[1]);
if (amount <= 0) {
sender.sendMessage(
"§c金额必须大于0");
return true;
}
// ★ 控制台禁止转账
if (!(sender instanceof Player)) {
sender.sendMessage(
"§c控制台不能转账,请使用 "
+ "/cypay give <玩家> <金额>");
return true;
}
Player p = (Player) sender;
String target = args[0];
BondManager.TransferResult r =
plugin.getBonds().transfer(
p.getName(),
target, amount);
if (r.success) {
p.sendMessage("§e§l[债券转账] §f向 §a"
+ target + " §f转出 §c"
+ amount + " §f枚债券");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage("§e§l[债券转账] §f收到 §a"
+ p.getName()
+ " §f转入 §a"
+ amount + " §f枚债券");
} else {
p.sendMessage("§c" + r.message
+ ",当前: "
+ plugin.getBonds()
.getBonds(p.getName()));
}
return true;
} catch (NumberFormatException ignored) {
}
}
// ===== /cypay <player> — 查指定玩家 =====
if (args.length == 1) {
String target = args[0];
if (hasAdminPerm(sender)) {
int bonds = plugin.getBonds()
.getBonds(target);
String status = plugin.getBonds()
.getAccountStatus(target);
sender.sendMessage(
"§e§l[债券] §f" + target
+ " 余额: §a"
+ bonds);
sender.sendMessage(
"§e§l[债券] §f状态: "
+ ("frozen".equals(
status)
? "§c冻结中"
: "§a正常"));
} else {
int bonds = plugin.getBonds()
.getBonds(
sender.getName());
sender.sendMessage(
"§e§l[债券] §f你的余额: §a"
+ bonds);
}
return true;
}
showHelp(sender);
return true;
}
// ===== 流水行格式化 =====
private String formatTxLine(
Map<String, Object> tx) {
String type = (String) tx.get("type");
int amount = ((Number) tx.get("amount"))
.intValue();
String tgt = (String) tx.get("target_player");
String reason = (String) tx.get("reason");
int bal = ((Number) tx.get("balance_after"))
.intValue();
long time = ((Number) tx.get("time"))
.longValue();
boolean pos = amount > 0;
String sign = pos ? "+" : "";
String color = pos ? "§a" : "§c";
StringBuilder sb = new StringBuilder();
sb.append("§7[")
.append(SDF.format(new Date(time)))
.append("] ")
.append(formatTxType(type))
.append(" ")
.append(color).append(sign)
.append(amount).append("§7");
if (tgt != null && !tgt.isEmpty())
sb.append(" §e↔ ").append(tgt);
if (reason != null && !reason.isEmpty())
sb.append(" §7(").append(reason)
.append(")");
sb.append(" §7余额:§f").append(bal);
return sb.toString();
}
private String formatTxType(String type) {
if (type == null) return "§7未知";
switch (type) {
case BondManager.TX_ADMIN_GIVE:
return "§a管理员发放";
case BondManager.TX_ADMIN_DEDUCT:
return "§c管理员扣除";
case BondManager.TX_TRANSFER_OUT:
return "§c转出";
case BondManager.TX_TRANSFER_IN:
return "§a转入";
case BondManager.TX_REDEEM:
return "§aCDK兑换";
case BondManager.TX_SHOP_BUY:
return "§e商城购买";
case BondManager.TX_DAILY_SIGN:
return "§a签到奖励";
case BondManager.TX_FREEZE:
return "§c冻结";
case BondManager.TX_UNFREEZE:
return "§a解冻";
default:
return "§7" + type;
}
}
private void showHelp(CommandSender sender) {
sender.sendMessage("§e§l债券命令帮助:");
sender.sendMessage(
"§e/cypay §7- 查询自己余额");
sender.sendMessage(
"§e/cypay <玩家> §7- 查余额(管理员)");
sender.sendMessage(
"§e/cypay <玩家> <金额> §7- 转账");
if (hasAdminPerm(sender)) {
sender.sendMessage(
"§e/cypay give <玩家> <金额> §7- 给债券");
sender.sendMessage(
"§e/cypay remove <玩家> <金额> §7- 扣债券");
sender.sendMessage(
"§e/cypay import <文件> §7- 导入CDK");
sender.sendMessage(
"§e/cypay freeze <玩家> §7- 冻结");
sender.sendMessage(
"§e/cypay unfreeze <玩家> §7- 解冻");
sender.sendMessage(
"§e/cypay water <玩家> [天数] §7- 流水");
sender.sendMessage(
"§e/cypay printer §7- 导出全服流水xlsx");
sender.sendMessage(
"§e/cypay printer <玩家> §7- 导出指定玩家xlsx");
}
}
@Override
public List<String> onTabComplete(
CommandSender sender, Command command,
String label, String[] args) {
List<String> list = new ArrayList<>();
if (args.length == 1) {
String last = args[0].toLowerCase();
if (hasAdminPerm(sender)) {
list.add("give");
list.add("remove");
list.add("import");
list.add("freeze");
list.add("unfreeze");
list.add("water");
list.add("printer");
File cdkDir = new File(
plugin.getDataFolder(),
"cdk");
if (cdkDir.exists()) {
File[] files = cdkDir.listFiles(
(d, n) -> n
.endsWith(".txt"));
if (files != null)
for (File f : files)
list.add(f.getName());
}
}
for (Player o :
Bukkit.getOnlinePlayers())
list.add(o.getName());
return filter(list, last);
}
if (args.length == 2) {
String first = args[0].toLowerCase();
if ("give".equals(first)
|| "remove".equals(first)
|| "freeze".equals(first)
|| "unfreeze".equals(first)
|| "water".equals(first)) {
for (Player o :
Bukkit.getOnlinePlayers())
list.add(o.getName());
return filter(list,
args[1].toLowerCase());
}
list.add("10");
list.add("50");
list.add("100");
return filter(list,
args[1].toLowerCase());
}
if (args.length == 3) {
String first = args[0].toLowerCase();
if ("give".equals(first)
|| "remove".equals(first)) {
list.add("10");
list.add("50");
list.add("100");
list.add("999");
return filter(list,
args[2].toLowerCase());
}
if ("water".equals(first)) {
list.add("1");
list.add("3");
list.add("7");
list.add("14");
return filter(list,
args[2].toLowerCase());
}
}
return list;
}
}
+147 -39
View File
@@ -189,10 +189,10 @@ public class GUIManager implements Listener {
myBal = reg.getProvider()
.getBalance(p);
} catch (Exception ignored) {}
g.setItem(32, mkItem(Material.DIAMOND,
"§a§l余额查询",
"§7我的余额: §a$"
+ String.format("%.2f", myBal)));
int myBonds = plugin.getBonds().getBonds(p.getName());
g.setItem(32, mkItem(Material.EMERALD,
"§b§l我的钱包",
"§7查看债券余额与流水记录"));
// ★ 余额操作 - 仅OP和tag玩家 ★
if (isAdmin(p)) {
@@ -309,50 +309,34 @@ public class GUIManager implements Listener {
}
public void openBalanceOps(Player p) {
Inventory g = Bukkit.createInventory(
null, 54,
"§d§l余额操作");
Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作");
fillBg(g);
net.milkbowl.vault.economy.Economy eco = null;
try {
var reg = plugin.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl
.vault
.economy
.Economy
.class);
var reg = plugin.getServer().getServicesManager()
.getRegistration(net.milkbowl.vault.economy.Economy.class);
if (reg != null) eco = reg.getProvider();
} catch (Exception ignored) {}
int slot = 0;
for (Player op :
Bukkit.getOnlinePlayers()) {
for (Player op : Bukkit.getOnlinePlayers()) {
if (slot >= 45) break;
double bal = 0;
if (eco != null) bal =
eco.getBalance(op);
boolean isSelf =
op.getName().equals(p.getName());
if (eco != null) bal = eco.getBalance(op);
boolean isSelf = op.getName().equals(p.getName());
int bondBal = plugin.getBonds().getBonds(op.getName());
g.setItem(slot, mkItem(
isSelf ? Material.EMERALD_BLOCK
: Material.PLAYER_HEAD,
isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD,
"§e" + op.getName(),
"§7余额: §a$"
+ String.format(
"%.2f", bal),
"§7金币: §a$" + String.format("%.2f", bal),
"§7债券: §e" + bondBal + " §6枚",
"",
"§a左键给钱 §c右键扣钱"));
"§a左键经济(±) §6右键债券(±)"));
slot++;
}
g.setItem(49, mkItem(Material.ARROW,
"§7返回"));
g.setItem(49, mkItem(Material.ARROW, "§7返回"));
p.openInventory(g);
}
@@ -750,7 +734,67 @@ public class GUIManager implements Listener {
e.getOldCursor().clone();
}
}
@EventHandler
public void onInvClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player)) return;
Player p = (Player) event.getWhoClicked();
String title = event.getView().getTitle();
int raw = event.getRawSlot();
// 我的信息 — CDK兑换 (slot 16)
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 (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);
return;
}
if (raw < 0 || raw >= 45) return;
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;
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 (target == null) return;
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);
p.closeInventory();
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);
p.closeInventory();
p.sendMessage("§e§l[债券操作] §f对 §a" + targetName + " §f进行债券操作");
p.sendMessage("§7输入 §a+数字 §7给债券,§c-数字 §7扣债券");
plugin.getCDK().requestInput(p, "bond", targetName);
}
return;
}
}
public void openEditor(
Player p, int idx) {
@@ -842,6 +886,19 @@ public class GUIManager implements Listener {
g.setItem(15, mkItem(Material.EMERALD_BLOCK,
"§a§l积分商城",
"§7当前: " + points + "积分"));
// CDK兑换入口
g.setItem(16, mkItem(Material.BOOK,
"§6§lCDK兑换",
"§7输入兑换码获取债券",
"",
"§e点击输入兑换码"));
// 显示当前债券余额
int bondBal = plugin.getBonds().getBonds(p.getName());
g.setItem(20, mkItem(Material.EMERALD,
"§e§l我的债券: " + bondBal + "枚",
"§7债券可通过签到、CDK、转账获得"));
// 图标说明
@@ -874,6 +931,15 @@ public class GUIManager implements Listener {
g.setItem(32, mkItem(Material.WOODEN_SHOVEL,
"§c清除图标",
"§7点击可清除已保存的图标"));
String ugLabel = "§7(玩家)";
if (plugin.getUserGroup() != null) {
UserGroupManager.UserGroupConfig ugCfg =
plugin.getUserGroup().getHighestGroup(p.getName());
if (ugCfg != null) {
ugLabel = ugCfg.displayColor + ugCfg.displayName;
}
}
g.setItem(22, mkItem(Material.PAPER, "§e§l我的用户组", "§7当前: " + ugLabel));
g.setItem(26, mkItem(Material.ARROW, "§7返回"));
p.openInventory(g);
@@ -956,6 +1022,30 @@ public class GUIManager implements Listener {
}
return true;
}
/**
* 处理"我的信息"GUI中CDK兑换按钮的点击
* 返回 true 表示已处理
*/
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", "");
return true;
}
return false;
}
// ===== 放入自定义图标 =====
private void doPlaceIcon(Player p,
@@ -1557,14 +1647,22 @@ public class GUIManager implements Listener {
"§7左键: 切换开关",
"§7右键: 设置时长",
"§7当前: " + afkMin + "分钟"));
g.setItem(22, mkItem(Material.ARROW, "§7返回"));
// openAdmin 方法中加:
g.setItem(15, mkItem(Material.EMERALD_BLOCK,
"§d§l余额操作",
"§7给钱、扣钱"));
p.openInventory(g);
String ugLabel = "§7(玩家)";
if (plugin.getUserGroup() != null) {
UserGroupManager.UserGroupConfig ugCfg =
plugin.getUserGroup().getHighestGroup(p.getName());
if (ugCfg != null) {
ugLabel = ugCfg.displayColor + ugCfg.displayName;
}
}
g.setItem(22, mkItem(Material.NETHERITE_UPGRADE_SMITHING_TEMPLATE,
"§e§l我的用户组",
"§7当前: " + ugLabel));
g.setItem(24, mkItem(Material.ARROW, "§7返回"));
// openAdmin 方法中加:
}
public void openInvite(Player p) {
@@ -1593,6 +1691,16 @@ public class GUIManager implements Listener {
"§7绑定他人的邀请码",
"",
"§e点击输入"));
String ugLabel = "§7(玩家)";
UserGroupManager.UserGroupConfig ugCfg =
plugin.getUserGroup().getHighestGroup(p.getName());
if (ugCfg != null) {
ugLabel = ugCfg.displayColor + ugCfg.displayName;
}
g.setItem(22, mkItem(Material.ENCHANTED_BOOK,
"§e§l我的用户组",
"§7当前: " + ugLabel));
g.setItem(26, mkItem(Material.ARROW, "§7返回"));
p.openInventory(g);
}
+558 -207
View File
@@ -16,6 +16,7 @@ import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.Date;
@@ -126,6 +127,29 @@ public class Main extends JavaPlugin
// 余额操作待处理
private String balanceTarget = null;
private boolean balanceGiveMode = false;
private BondManager bondManager;
private CDKManager cdkManager;
private UserGroupManager userGroupManager;
private CypayCommand cypayCommand;
public BondManager getBonds() { return bondManager; }
public CDKManager getCDK() { return cdkManager; }
public UserGroupManager getUserGroup() { return userGroupManager; }
public CypayCommand getCypay() { return cypayCommand; }
// 在 Main.java 字段声明区域(bondManager 附近)添加:
// 在 bondManager 字段附近添加:
private BondPrinter bondPrinter;
// 钱包流水查看目标(玩家名 → 查看目标)
private final Map<String, String> walletViewTarget =
new ConcurrentHashMap<>();
// 流水格式化
private static final java.text.SimpleDateFormat
TX_SDF = new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
public CommissionManager getCommission() {
@@ -345,6 +369,10 @@ private PVPManager pvpManager;
getCommand("oa").setExecutor(this);
if (getCommand("menu") != null)
getCommand("menu").setExecutor(this);
if (getCommand("printer") != null) {
getCommand("printer").setExecutor(this);
}
// ===== 10. 注册事件 =====
getServer().getPluginManager()
@@ -368,6 +396,13 @@ private PVPManager pvpManager;
}
}
}.runTaskTimer(this, 600L, 600L);
// [新增] Printer 文件清理(每小时检测一次)
new BukkitRunnable() {
public void run() {
if (bondPrinter != null)
bondPrinter.cleanupOldFiles();
}
}.runTaskTimer(this, 1200L, 72000L);
// 12 ===PVP===
pvpManager = new PVPManager(this);
@@ -380,8 +415,28 @@ private PVPManager pvpManager;
20L, 20L);
setupPVPTab();
// 13 ====cypay债券====
// 债券系统(独立DB)
bondManager = new BondManager(this);
cdkManager = new CDKManager(this);
userGroupManager = new UserGroupManager(this);
cypayCommand = new CypayCommand(this);
// 加载CDK文件
cdkManager.loadCDKsFromDir();
// cypay 命令
if (getCommand("cypay") != null) {
getCommand("cypay").setExecutor(cypayCommand);
getCommand("cypay").setTabCompleter(cypayCommand);
}
bondPrinter = new BondPrinter(this);
cdkManager.loadCDKsFromDir();
getLogger().info("Sdf1_login v1.0 | 就绪");
}
public BondPrinter getBondPrinter() {
return bondPrinter;
}
private void setupPVPTab() {
if (getCommand("pvp") != null) {
@@ -583,6 +638,15 @@ private PVPManager pvpManager;
public boolean isFrozen(Player p) {
return !loggedIn.contains(p.getName());
}
private String debugCodePoints(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (i > 0) sb.append(" ");
sb.append(String.format("U+%04X",
(int) s.charAt(i)));
}
return sb.toString();
}
private boolean isAdmin(CommandSender sender) {
if (sender instanceof Player)
@@ -1323,16 +1387,23 @@ private PVPManager pvpManager;
// 强制重发资源包(清掉客户端拒绝记录)
Bukkit.getScheduler()
.runTaskLater(this, () -> {
if (p.isOnline()
&& radio != null) {
radio.sendResourcePack(p);
// ★ 资源包:只发送一次,延迟3秒确保客户端就绪
if (radio != null) {
final Player jp = p;
Bukkit.getScheduler().runTaskLater(
this, () -> {
if (jp.isOnline()) {
radio.sendResourcePack(jp);
}
}, 60L);
}
}, 20L);
// 重置聊天输入状态(防止上次退出时残留)
chatInput.reset(p);
if (needsPasswordChange.contains(name))
needsPasswordChange.remove(name);
// 改完了直接打包,要打sdf1一起
lastActivity.put(p.getUniqueId(),
System.currentTimeMillis());
if (needsPasswordChange.contains(name))
@@ -1371,6 +1442,7 @@ private PVPManager pvpManager;
}
}
// [ADDED] 资源包(延迟发送,不强制)
// ★ 资源包:只发送一次,延迟3秒确保客户端就绪
if (radio != null) {
final Player jp = p;
Bukkit.getScheduler().runTaskLater(
@@ -1397,20 +1469,32 @@ private PVPManager pvpManager;
afk.recordAction(p.getUniqueId());
hideInventoryDelayed(p, 0);
// [ADDED] 发送资源包
if (radio != null) radio.sendResourcePack(p);
// [ADDED] 发送资源包(延迟5秒确保客户端就绪)
// ★ 资源包:只发送一次,延迟3秒确保客户端就绪
if (radio != null) {
final Player jp = p;
Bukkit.getScheduler().runTaskLater(this,
() -> {
Bukkit.getScheduler().runTaskLater(
this, () -> {
if (jp.isOnline()) {
radio.sendResourcePack(jp);
}
}, 100L);
}, 60L);
}
// [ADDED] 发送资源包(延迟5秒确保客户端就绪)
// ★ 资源包:只发送一次,延迟3秒确保客户端就绪
if (radio != null) {
final Player jp = p;
Bukkit.getScheduler().runTaskLater(
this, () -> {
if (jp.isOnline()) {
radio.sendResourcePack(jp);
}
}, 60L);
}
verification.verifyPremiumAsync(p,
isPremium -> {
if (isPremium) {
@@ -1903,130 +1987,20 @@ private PVPManager pvpManager;
public void onChat(AsyncPlayerChatEvent e) {
Player p = e.getPlayer();
String msg = e.getMessage();
// ★ CDK/经济/债券输入拦截 — 必须在所有聊天处理之前
if (this.getCDK() != null && this.getCDK().isListening(p.getName())) {
e.setCancelled(true);
this.getCDK().onChat(p, msg);
return;
}
// ===== 菜单聊天输入 =====
if (getMenu().isEditing(p.getName())) {
e.setCancelled(true);
getMenu().onChat(p, msg);
return;
}
// ===== 余额操作输入 =====
if (balanceTarget != null) {
e.setCancelled(true);
String input = msg.trim();
String[] parts = input.split("\\s+", 2);
if (parts.length < 2
|| parts[0].isEmpty()
|| parts[1].isEmpty()) {
p.sendMessage("§c格式: 金额 理由");
p.sendMessage("§7例: 100 测试扣款");
balanceTarget = null;
return;
}
double amount;
try {
amount = Double.parseDouble(parts[0]);
} catch (NumberFormatException ex) {
p.sendMessage("§c金额格式错误");
balanceTarget = null;
return;
}
if (amount <= 0) {
p.sendMessage("§c金额必须大于0");
balanceTarget = null;
return;
}
String reason = parts[1];
String target = balanceTarget;
boolean giveMode = balanceGiveMode;
balanceTarget = null;
var reg = getServer().getServicesManager()
.getRegistration(
net.milkbowl.vault.economy
.Economy.class);
if (reg == null) {
p.sendMessage("§c经济系统未加载");
return;
}
net.milkbowl.vault.economy.Economy eco =
reg.getProvider();
Player tp = Bukkit.getPlayer(target);
if (tp == null || !tp.isOnline()) {
p.sendMessage(
"§c玩家不在线: "
+ target);
return;
}
if (giveMode) {
var resp =
eco.depositPlayer(target, amount);
if (resp.transactionSuccess()) {
double oldBal = resp.balance
- amount;
double newBal = resp.balance;
// 操作者看到的
p.sendMessage("");
p.sendMessage("§6【余额转账】正在执行...");
p.sendMessage("§7============== §6余额转账 §7==============");
p.sendMessage("§7目标: §e" + target);
p.sendMessage("§a到账: §e$" + String.format("%.2f", amount));
p.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §a" + String.format("%.2f", newBal));
p.sendMessage("§7理由: §f" + reason);
p.sendMessage("§7============== §6余额转账 §7==============");
p.sendMessage("");
// 被转账者看到的
tp.sendMessage("");
tp.sendMessage("§a【收到转账】一笔新入账...");
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" + p.getName());
tp.sendMessage("§7理由: §f" + reason);
tp.sendMessage("§7============== §a收到转账 §7==============");
tp.sendMessage("");
} else {
p.sendMessage("§c转账失败: "
+ resp.errorMessage);
}
} else {
double bal = eco.getBalance(target);
if (bal < amount) {
p.sendMessage("§c" + target
+ " 余额不足");
p.sendMessage("§7当前余额: §e$"
+ String.format("%.2f", bal));
return;
}
var resp =
eco.withdrawPlayer(target, amount);
if (resp.transactionSuccess()) {
double oldBal = resp.balance + amount;
double newBal = resp.balance;
// 操作者看到的
p.sendMessage("");
p.sendMessage("§c【余额扣款】正在执行...");
p.sendMessage("§7============== §c余额扣款 §7==============");
p.sendMessage("§7目标: §e" + target);
p.sendMessage("§c扣款: §e-$" + String.format("%.2f", amount));
p.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §c" + String.format("%.2f", newBal));
p.sendMessage("§7理由: §f" + reason);
p.sendMessage("§7============== §c余额扣款 §7==============");
p.sendMessage("");
// 被扣款者看到的
tp.sendMessage("");
tp.sendMessage("§c【余额扣款】你的账户收到扣款...");
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" + p.getName());
tp.sendMessage("§7理由: §f" + reason);
tp.sendMessage("§7============== §c余额扣款 §7==============");
tp.sendMessage("");
} else {
p.sendMessage("§c扣款失败: "
+ resp.errorMessage);
}
}
}
// ===== 冻结检查 =====
if (isFrozen(p)) {
@@ -2176,11 +2150,251 @@ private PVPManager pvpManager;
public PVPManager getPVPManager() {
return pvpManager;
}
// ==================== 我的钱包 ====================
public void openMyWallet(Player p) {
openWalletGUI(p, p.getName());
}
private void openWalletGUI(Player p, String target) {
Inventory g = Bukkit.createInventory(
null, 27, "§6§l我的钱包");
ItemStack glass = new ItemStack(
Material.GRAY_STAINED_GLASS_PANE);
ItemMeta gm = glass.getItemMeta();
if (gm != null) {
gm.setDisplayName(" ");
glass.setItemMeta(gm);
}
for (int i = 0; i < 27; i++)
g.setItem(i, glass);
// 债券余额 (slot 4)
int balance = bondManager.getBonds(target);
g.setItem(4, mkItem(Material.GOLD_INGOT,
"§6§l债券余额: §e" + balance + " §6枚",
"§7当前持有的债券数量"));
// 账户状态 (slot 10)
String status = bondManager
.getAccountStatus(target);
if ("frozen".equals(status)) {
g.setItem(10, mkItem(Material.REDSTONE,
"§c§l账户状态: 冻结中",
"§7无法进行任何资金交易"));
} else {
g.setItem(10, mkItem(Material.EMERALD,
"§a§l账户状态: 正常",
"§7账户运行正常"));
}
// 同IP玩家 (slot 11)
if (target.equals(p.getName())) {
List<String> sameIP = bondManager
.getSameIPPlayers(target);
List<String> lore = new ArrayList<>();
lore.add("§7同IP下注册的其他玩家:");
if (sameIP.isEmpty()) {
lore.add("§a 无");
} else {
for (String n : sameIP)
lore.add("§c - " + n);
}
lore.add("§7");
lore.add("§7转账限制:");
lore.add("§7 不能与同IP玩家转账");
g.setItem(11, mkItem(Material.PAPER,
"§e§l同IP玩家", lore.toArray(
new String[0])));
}
// 近7天流水 (slot 12)
g.setItem(12, mkItem(Material.BOOK,
"§e近7天流水",
"§7点击查看最近7天交易记录"));
// 近14天流水 (slot 13)
g.setItem(13, mkItem(Material.BOOK,
"§e近14天流水",
"§7点击查看最近14天交易记录",
"§8(系统最大支持14天)"));
// 转账记录 (slot 14)
g.setItem(14, mkItem(Material.PAPER,
"§e转账记录",
"§7点击查看所有转账明细"));
// 返回 (slot 22)
g.setItem(22, mkItem(Material.ARROW,
"§7← 返回"));
walletViewTarget.put(p.getName(), target);
p.openInventory(g);
}
// ==================== 流水记录GUI ====================
public void openWalletTransactions(
Player p, int days) {
String target = walletViewTarget.getOrDefault(
p.getName(), p.getName());
openWalletTxGUI(p, target, days, false);
}
public void openWalletTransfers(Player p) {
String target = walletViewTarget.getOrDefault(
p.getName(), p.getName());
openWalletTxGUI(p, target, 14, true);
}
private void openWalletTxGUI(Player p,
String target,
int days,
boolean transfersOnly) {
days = Math.min(days, 14); // 普通玩家最大14天
String title = transfersOnly
? "§e§l转账记录 - " + target
: "§e§l流水记录 - 近" + days
+ "天 - " + target;
Inventory g = Bukkit.createInventory(
null, 54, title);
List<Map<String, Object>> txs = transfersOnly
? bondManager.getTransferTransactions(
target, days)
: bondManager.getTransactions(
target, days);
int slot = 0;
for (Map<String, Object> tx : txs) {
if (slot >= 45) break;
String type = (String) tx.get("type");
int amount = ((Number) tx.get("amount"))
.intValue();
String tgt = (String) tx.get("target_player");
String reason = (String) tx.get("reason");
int bal = ((Number) tx.get("balance_after"))
.intValue();
long time = ((Number) tx.get("time"))
.longValue();
boolean pos = amount > 0;
String sign = pos ? "+" : "";
String color = pos ? "§a" : "§c";
String typeName = walletFormatType(type);
// 用 if-else 替代 switch 避免编译问题
Material mat = Material.PAPER;
if (BondManager.TX_TRANSFER_OUT.equals(type)) {
mat = Material.RED_WOOL;
} else if (BondManager.TX_TRANSFER_IN
.equals(type)) {
mat = Material.LIME_WOOL;
} else if (BondManager.TX_ADMIN_GIVE
.equals(type)) {
mat = Material.GOLD_BLOCK;
} else if (BondManager.TX_ADMIN_DEDUCT
.equals(type)) {
mat = Material.REDSTONE_BLOCK;
} else if (BondManager.TX_REDEEM
.equals(type)) {
mat = Material.NAME_TAG;
} else if (BondManager.TX_SHOP_BUY
.equals(type)) {
mat = Material.CHEST;
} else if (BondManager.TX_FREEZE
.equals(type)) {
mat = Material.BARRIER;
} else if (BondManager.TX_UNFREEZE
.equals(type)) {
mat = Material.EMERALD_BLOCK;
} else if (pos) {
mat = Material.LIME_WOOL;
} else {
mat = Material.RED_WOOL;
}
List<String> lore = new ArrayList<>();
lore.add("§7类型: " + typeName);
lore.add("§7金额: " + color + sign + amount
+ " §6枚");
if (tgt != null && !tgt.isEmpty())
lore.add("§7对象: §e" + tgt);
if (reason != null && !reason.isEmpty())
lore.add("§7理由: §7" + reason);
lore.add("§7余额: §f" + bal + " §6枚");
lore.add("§8" + TX_SDF.format(
new Date(time)));
ItemStack item = new ItemStack(mat);
ItemMeta im = item.getItemMeta();
if (im != null) {
im.setDisplayName(color + sign + amount
+ " §6枚 - " + typeName);
im.setLore(lore);
item.setItemMeta(im);
}
g.setItem(slot, item);
slot++;
}
if (txs.isEmpty()) {
ItemStack empty = new ItemStack(
Material.BARRIER);
ItemMeta em = empty.getItemMeta();
if (em != null) {
em.setDisplayName("§7暂无记录");
empty.setItemMeta(em);
}
g.setItem(22, empty);
} else if (txs.size() > 45) {
g.setItem(49, mkItem(Material.PAPER,
"§7还有 " + (txs.size() - 45)
+ " 条未显示"));
}
g.setItem(53, mkItem(Material.ARROW,
"§7← 返回钱包"));
p.openInventory(g);
}
private String walletFormatType(String type) {
if (type == null) return "未知";
switch (type) {
case BondManager.TX_ADMIN_GIVE:
return "管理员发放";
case BondManager.TX_ADMIN_DEDUCT:
return "管理员扣除";
case BondManager.TX_TRANSFER_OUT:
return "转出";
case BondManager.TX_TRANSFER_IN:
return "转入";
case BondManager.TX_REDEEM:
return "CDK兑换";
case BondManager.TX_SHOP_BUY:
return "商城购买";
case BondManager.TX_DAILY_SIGN:
return "签到奖励";
case BondManager.TX_FREEZE:
return "冻结";
case BondManager.TX_UNFREEZE:
return "解冻";
default:
return type;
}
}
// ===== Inventory Click(旧逻辑 + 新主菜单布局 + 新功能) =====
@EventHandler
public void onInvClick(InventoryClickEvent e) {
if (e.isCancelled()) return;
if (!(e.getWhoClicked() instanceof Player))
return;
Player p = (Player) e.getWhoClicked();
@@ -2393,26 +2607,134 @@ private PVPManager pvpManager;
return;
}
// ===== 余额操作 —— 移到前面 =====
// ===== [DEBUG] 标题追踪 =====
{
String dt = title;
if (dt.contains("钱包")
|| dt.contains("余额")
|| dt.contains("流水")
|| dt.contains("转账")) {
getLogger().info(
"[DEBUG-CLICK] "
+ "player=" + p.getName()
+ " title=[" + dt + "]"
+ " slot=" + slot
+ " codePoints="
+ debugCodePoints(dt));
}
}
// ===== 二级菜单(.txt)—— 排除所有固定GUI =====
if (title.startsWith("§6§l")
&& !title.equals(GUIManager.T_MAIN)
&& !title.equals(GUIManager.T_ADMIN)
&& !title.equals(GUIManager.T_MY_INFO)
&& !title.equals(GUIManager.T_INVITE)
&& !title.equals(
GUIManager.T_TASK_CENTER)
&& !title.equals(
GUIManager.T_GIFT_STAGES)
&& !title.equals("§6§l任务面板")
&& !title.equals("§6§l垃圾回收站")
&& !title.equals("§6§l我的钱包")
&& !title.equals("§6§l余额操作")) {
e.setCancelled(true);
if (slot == 49) {
gui.openMain(p);
return;
}
if (slot < 45
&& slot < e.getInventory()
.getSize()
&& e.getCurrentItem() != null) {
ItemMeta subLm = e.getCurrentItem()
.getItemMeta();
if (subLm != null
&& subLm.getLore() != null) {
for (String subLn
: subLm.getLore()) {
if (subLn.contains(
"§7指令: §f")) {
String cmd = subLn
.replace("§7指令: §f", "")
.trim();
if (cmd.isEmpty()
|| cmd.equals("/")
|| cmd.equals("null")) {
p.sendMessage("§c指令为空");
return;
}
p.closeInventory();
if (cmd.startsWith("/")) {
cmd = cmd.substring(1);
}
Bukkit.dispatchCommand(p, cmd);
return;
}
}
}
}
return;
}
// ===== 我的钱包(唯一handler) =====
if (title.equals("§6§l我的钱包")) {
e.setCancelled(true);
if (slot == 22) {
gui.openMain(p);
} else if (slot == 12) {
openWalletTransactions(p, 7);
} else if (slot == 13) {
openWalletTransactions(p, 14);
} else if (slot == 14) {
openWalletTransfers(p);
}
return;
}
// ===== 流水/转账记录(唯一handler) =====
if (title.startsWith("§e§l流水记录")
|| title.startsWith("§e§l转账记录")) {
e.setCancelled(true);
if (slot == 53) {
openMyWallet(p);
}
return;
}
// ===== 余额操作(唯一handler) =====
if (title.equals("§d§l余额操作")) {
e.setCancelled(true);
if (slot == 49) {
gui.openMain(p);
return;
}
// ★ bounds检查 ★
if (slot >= 0
&& slot < e.getInventory()
.getSize()) {
ItemStack item =
e.getInventory().getItem(slot);
&& slot < e.getInventory().getSize()) {
ItemStack item = e.getInventory()
.getItem(slot);
if (item == null
|| !item.hasItemMeta()
|| !item.getItemMeta()
.hasDisplayName())
.hasDisplayName()) {
return;
String targetName =
item.getItemMeta()
}
// 在 slot == 49 的 return 之后、读取 item 之前
// ItemStack item = e.getInventory().getItem(slot);
if (item == null) return;
Material mat = item.getType();
// 以下是常见的装饰性玻璃板材质,按需扩展
if (mat.name().contains("STAINED_GLASS")
|| mat.name().contains("GLASS_PANE")
|| mat == Material.LIGHT_BLUE_STAINED_GLASS_PANE
|| mat == Material.GRAY_STAINED_GLASS_PANE
// ... 其他装饰物
|| mat == Material.BLACK_STAINED_GLASS_PANE) {
e.setCancelled(true);
return; // 装饰物品,不处理
}
String targetName = item.getItemMeta()
.getDisplayName()
.replace("§e", "")
.replace("§a", "");
@@ -2423,18 +2745,37 @@ private PVPManager pvpManager;
if (e.isLeftClick()) {
balanceTarget = targetName;
balanceGiveMode = true;
} else if (e.isRightClick()) {
balanceTarget = targetName;
balanceGiveMode = false;
}
p.closeInventory();
p.sendMessage(
"§e请输入金额和理由");
p.sendMessage(
"§7例: 100 测试扣款");
"§7例: +100 给款 / -100 扣款");
} else if (e.isRightClick()) {
if (this.getCDK() != null) {
this.getCDK().requestInput(
p, "bond",
targetName);
}
p.closeInventory();
p.sendMessage(
"§e【债券操作】对 §a"
+ targetName
+ " §e进行债券操作");
p.sendMessage(
"§e输入 +金额 给债券,"
+ " -金额 扣债券");
p.sendMessage(
"§7可附加理由: §a+100 "
+ "活动奖励 §7不填默认:"
+ "管理员手动调整");
}
}
return;
}
// ===== 菜单管理 =====
if (title.equals("§c§l菜单管理")) {
e.setCancelled(true);
@@ -2562,7 +2903,9 @@ private PVPManager pvpManager;
p.sendMessage("§c已取消");
gui.openMain(p);
break;
}}
}
}
// ==================== 主菜单(新布局) ====================
// ==================== 主菜单(新布局) ====================
if (title.equals(GUIManager.T_MAIN)) {
e.setCancelled(true);
@@ -2573,27 +2916,24 @@ private PVPManager pvpManager;
.getItemMeta();
if (meta != null && meta.getLore() != null) {
for (String ln : meta.getLore()) {
if (ln.contains(
"§7指令: §f")) {
if (ln.contains("§7指令: §f")) {
String cmd = ln.replace(
"§7指令: §f",
"").trim();
"§7指令: §f", "").trim();
if (cmd.isEmpty()
|| cmd.equals("/")
|| cmd.equals("null")) {
p.sendMessage(
"§c指令为空");
p.sendMessage("§c指令为空");
return;
}
p.closeInventory();
if (cmd.startsWith("/"))
if (cmd.startsWith("/")) {
cmd = cmd.substring(1);
}
if (cmd.endsWith(".txt")) {
gui.openSubMenu(p,
cmd.substring(1));
} else {
Bukkit.dispatchCommand(
p, cmd);
Bukkit.dispatchCommand(p, cmd);
}
return;
}
@@ -2602,47 +2942,45 @@ private PVPManager pvpManager;
}
// 固定按钮
switch (slot) {
case 10: gui.openMyInfo(p); break;
case 12: gui.openInvite(p); break;
case 14:
p.openInventory(
points.createShopGUI(p));
break;
case 16: gui.openBiomeMenu(p); break;
case 19: ticket.openMain(p); break;
case 21:
if (slot == 10) {
gui.openMyInfo(p);
} else if (slot == 12) {
gui.openInvite(p);
} else if (slot == 14) {
p.openInventory(points.createShopGUI(p));
} else if (slot == 16) {
gui.openBiomeMenu(p);
} else if (slot == 19) {
ticket.openMain(p);
} else if (slot == 20) {
openMyWallet(p);
} else if (slot == 21) {
dailySignWithReward(p);
gui.openMain(p);
break;
case 23: gui.openTaskCenter(p); break;
case 25:
} else if (slot == 23) {
gui.openTaskCenter(p);
} else if (slot == 25) {
if (p.isOp() || isAdmin(p)) {
garbage.openRecycle(p);
}
break;
case 29:
} else if (slot == 29) {
if (isAdmin(p)) {
p.closeInventory();
chatInput.getState(p).type =
ChatInputManager.InputType
.ADMIN_AUTH;
p.sendMessage(
"请输入管理密码:");
ChatInputManager.InputType.ADMIN_AUTH;
p.sendMessage("请输入管理密码:");
}
break;
case 31:
} else if (slot == 31) {
if (p.isOp()) {
gui.openMenuManager(p);
}
break;
case 34:
} else if (slot == 34) {
if (p.isOp() || isAdmin(p)) {
gui.openBalanceOps(p);
}
break;
}
return;
}
@@ -2723,10 +3061,13 @@ private PVPManager pvpManager;
// ==================== 我的信息 ====================
if (title.equals(GUIManager.T_MY_INFO)) {
e.setCancelled(true);
// 先处理图标槽
if (gui.handleMyInfoIconClick(p, e))
// ★ 只在图标槽(slot 4)调用,不干扰按钮
if (slot == 4
&& gui.handleMyInfoIconClick(p, e)) {
return;
}
if (slot == 26) gui.openMain(p);
else if (slot == 10) {
if (checkIn.isCheckedInToday(
p.getName()))
@@ -3263,8 +3604,41 @@ private PVPManager pvpManager;
Player p = (Player) sender;
return pvpManager.onCommand(p, args);
}
if (cmdName.equals("cypay")) {
return cypayCommand.onCommand(sender, cmd, label, args);
}
// ===== /printer [玩家名] =====
if (label.equalsIgnoreCase("printer")) {
if (bondPrinter == null) {
sender.sendMessage("§cPrinter未初始化");
return true;
}
if (!sender.isOp()
&& !isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
if (args.length == 0) {
// 打印全服
sender.sendMessage(
"§e正在导出全服流水...");
bondPrinter.printAll(sender);
return true;
}
if (args.length == 1) {
// 打印指定玩家
String target = args[0];
sender.sendMessage("§e正在导出 "
+ target + " 的流水...");
bondPrinter.printPlayer(
target, sender);
return true;
}
sender.sendMessage(
"§7用法: /printer [玩家名]");
return true;
}
// ===== /chat 独立命令 =====
if (cmdName.equals("chat")
|| cmdName.equals("聊天")) {
@@ -3610,33 +3984,6 @@ private PVPManager pvpManager;
}
// /sdf1debug
if (cmdName.equals("sdf1debug")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可用");
return true;
}
Player p2 = (Player) sender;
if (!isAdmin(sender)) {
p2.sendMessage("§c权限不足");
return true;
}
if (args.length < 2) {
p2.sendMessage("§e用法: /sdf1debug <玩家> <阶段>");
return true;
}
String tgt = args[0];
try {
int stage = Integer.parseInt(args[1]);
db.setField(tgt, "gift_stage", stage);
p2.sendMessage("§a已将 " + tgt
+ " 的任务阶段设为 " + stage);
} catch (NumberFormatException ex) {
p2.sendMessage("§c请输入有效数字");
}
return true;
}
// /oa
if (cmdName.equals("oa")) {
if (!(sender instanceof Player)) {
@@ -4765,6 +5112,10 @@ private PVPManager pvpManager;
"addplayer", "takeplayer",
"unmute", "reset"));
}
if (cmd.getName().equalsIgnoreCase("cypay")) {
return cypayCommand.onTabComplete(sender, cmd, label, args);
}
return list;
}
}
+87 -50
View File
@@ -13,6 +13,7 @@ import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.scoreboard.*;
import java.io.File;
import java.sql.*;
import java.util.*;
@@ -39,6 +40,10 @@ public class PVPManager implements Listener {
private final Map<String, KillSession>
killSessions =
new ConcurrentHashMap<>();
private final Set<String> tripleAnnounced =
ConcurrentHashMap.newKeySet();
private final Map<String, Long> lastScoreboardRefresh = new ConcurrentHashMap<>();
// 边框显示
private final Set<String>
@@ -48,6 +53,7 @@ public class PVPManager implements Listener {
tempBorders =
ConcurrentHashMap.newKeySet();
public static class KillSession {
public int count = 0;
public long firstKill = 0;
@@ -576,13 +582,10 @@ public class PVPManager implements Listener {
}
String was = inRegion.get(pn);
if (inside != null
&& !inside
.equals(was)) {
if (inside != null && !inside.equals(was)) {
inRegion.put(pn, inside);
if (was == null) {
String reg = inside;
initSession(pn, reg);
final Player pp = p;
Bukkit.getScheduler()
.runTaskLater(plugin,
@@ -591,22 +594,20 @@ public class PVPManager implements Listener {
pp, reg),
1L);
}
} else if (inside == null
&& was != null) {
inRegion.remove(pn);
// 不删 sessionKills,等倒计时结束再清
removeScoreboard(p);
final Player pp = p;
Bukkit.getScheduler()
.runTaskLater(plugin,
() -> plugin
.giveMenuSnowball(
pp),
.giveMenuSnowball(pp),
5L);
} else if (inside != null
&& inside
.equals(was)) {
ensureScoreboard(p, inside);
}
}
// 5分钟超时重置连杀session
@@ -624,19 +625,28 @@ public class PVPManager implements Listener {
}
// 5分钟无击杀 → 计数榜切总榜
// 5分钟无击杀 → 归档并清零
for (String rName :
new ArrayList<>(
lastKillTime
.keySet())) {
long last = lastKillTime
.getOrDefault(rName, 0L);
if (now - last
> 5 * 60 * 1000) {
if (now - last > 5 * 60 * 1000) {
// 清零该区域所有sessionKills
sessionKills.keySet()
.removeIf(k -> k
.startsWith(
rName + ":"));
// 切换到总榜
refreshScoreboard(rName);
lastKillTime.remove(rName);
lastScoreboardRefresh.remove(rName);
}
}
// 边框渲染
for (Player p :
Bukkit.getOnlinePlayers()) {
@@ -653,12 +663,38 @@ public class PVPManager implements Listener {
.contains(pr)) {
drawBorder(p, r);
}
// 每秒刷新活跃区域的计分板倒计时
for (String rName : new ArrayList<>(lastKillTime.keySet())) {
long last = lastScoreboardRefresh.getOrDefault(rName, 0L);
if (now - last >= 1000) {
lastScoreboardRefresh.put(rName, now);
for (Player tp : Bukkit.getOnlinePlayers()) {
String inR = inRegion.get(tp.getName());
if (rName.equals(inR)) {
showCounter(tp, rName);
}
}
}
}
}
}
private void broadcastWithSound(String msg, Sound sound) {
Bukkit.broadcastMessage(msg);
for (Player p : Bukkit.getOnlinePlayers()) {
p.playSound(p.getLocation(), sound, 1.0f, 1.0f);
}
}
private void initSession(String player,
String region) {
killSessions.remove(player);
tripleAnnounced.remove(
region + ":" + player);
KillSession s = new KillSession();
s.region = region;
s.firstKill =
@@ -694,7 +730,7 @@ public class PVPManager implements Listener {
addKill(kName, region);
addDeath(vName, region);
// sessionKills(计数榜用)
// 计数榜
String kKey =
region + ":" + kName;
sessionKills.merge(kKey, 1,
@@ -702,12 +738,12 @@ public class PVPManager implements Listener {
lastKillTime.put(region,
System.currentTimeMillis());
// 终结检测
// 终结检测
KillSession victimSession =
killSessions.get(vName);
if (victimSession != null
&& victimSession.count
>= 3) {
&& victimSession.count >= 3) {
String endMsg =
"§b§l" + kName
+ " §e终结了 "
@@ -716,27 +752,22 @@ public class PVPManager implements Listener {
+ "连杀!§a恭喜终结者 "
+ kName
+ "§e!§7快来挑战!";
Bukkit.broadcastMessage(endMsg);
for (Player pp :
Bukkit.getOnlinePlayers()) {
pp.playSound(
pp.getLocation(),
Sound
.UI_TOAST_CHALLENGE_COMPLETE,
2.5f, 0.8f);
}
killSessions.remove(vName);
broadcastWithSound(endMsg, Sound.ENTITY_ENDER_DRAGON_GROWL);
killSessions.remove(vName);
}
// 击杀者连杀计数
// 击杀者连杀
KillSession ks =
killSessions.get(kName);
if (ks == null
|| !ks.region
.equals(region)
|| !ks.region.equals(region)
|| System.currentTimeMillis()
- ks.lastKill
> 5 * 60 * 1000) {
- ks.lastKill > 5 * 60 * 1000) {
tripleAnnounced.remove(
region + ":" + kName);
KillSession ns =
new KillSession();
ns.region = region;
@@ -756,6 +787,11 @@ public class PVPManager implements Listener {
KillSession killerSession =
killSessions.get(kName);
if (killerSession != null) {
plugin.getLogger().info(
"[PVP] checkMultiKill: "
+ kName + " count="
+ killerSession.count
+ " region=" + region);
checkMultiKill(killer,
region, killerSession);
}
@@ -783,6 +819,7 @@ public class PVPManager implements Listener {
KillSession session) {
int count = session.count;
String kName = killer.getName();
String key = region + ":" + kName;
String msg = null;
if (count >= 5) {
@@ -791,8 +828,7 @@ public class PVPManager implements Listener {
+ "」达成 " + count
+ "连杀!§7§l快来挑战!";
} else if (count == 3) {
if (!session.tripleAnnounced) {
session.tripleAnnounced = true;
if (tripleAnnounced.add(key)) {
msg = "§6§l三杀!"
+ kName
+ " 势不可挡!"
@@ -801,29 +837,28 @@ public class PVPManager implements Listener {
}
if (msg != null) {
Bukkit.broadcastMessage(msg);
for (Player pp :
Bukkit.getOnlinePlayers()) {
pp.playSound(
pp.getLocation(),
Sound
.UI_TOAST_CHALLENGE_COMPLETE,
2.0f, 1.0f);
if (count >= 5) {
broadcastWithSound(msg, Sound.ENTITY_ENDER_DRAGON_GROWL);
} else {
broadcastWithSound(msg, Sound.BLOCK_END_PORTAL_SPAWN);
}
}
}
// ===== 计分板(英文ID) =====
private void ensureScoreboard(
Player p, String region) {
if (lastKillTime
.containsKey(region)) {
// 活跃击杀:每秒强制刷新(更新倒计时)
if (lastKillTime.containsKey(region)) {
showCounter(p, region);
return;
}
Scoreboard sb =
p.getScoreboard();
// 无活跃击杀:只在没有scoreboard时创建总榜
Scoreboard sb = p.getScoreboard();
if (sb == null
|| sb.getObjective(
"sdf1pvp") == null) {
@@ -831,6 +866,7 @@ public class PVPManager implements Listener {
}
}
private void updateScoreboard(
Player p, String region) {
if (lastKillTime
@@ -885,13 +921,11 @@ public class PVPManager implements Listener {
}
p.setScoreboard(sb);
}
private void showCounter(Player p,
String region) {
ScoreboardManager mgr =
Bukkit.getScoreboardManager();
Scoreboard sb =
mgr.getNewScoreboard();
Scoreboard sb = mgr.getNewScoreboard();
Objective obj =
sb.registerNewObjective(
"sdf1pvp",
@@ -940,17 +974,19 @@ public class PVPManager implements Listener {
Long last =
lastKillTime.get(region);
if (last != null) {
long elapsed =
System.currentTimeMillis()
- last;
long remaining =
Math.max(0,
5 * 60 * 1000
- (System
.currentTimeMillis()
- last));
5 * 60 * 1000 - elapsed);
long secs = remaining / 1000;
obj.getScore(
"§7冷却: " + secs + "s")
.setScore(-1);
}
// 关键:强制赋值新scoreboard
p.setScoreboard(sb);
}
@@ -1286,6 +1322,7 @@ public class PVPManager implements Listener {
.startsWith(
name + ":"));
lastKillTime.remove(name);
lastScoreboardRefresh.remove(name);
p.sendMessage(
"§a已删除: " + name);
return true;
@@ -111,34 +111,39 @@ public class RadioDownloadListener
}
private void handleFailure(Player p, UUID uuid) {
int failNum = failCount.getOrDefault(uuid, 0) + 1;
int failNum = failCount.getOrDefault(
uuid, 0) + 1;
failCount.put(uuid, failNum);
log.warning("[Radio] " + p.getName()
+ " 失败次数: " + failNum + "/" + MAX_FAILED);
+ " 失败次数: " + failNum
+ "/" + MAX_FAILED);
if (failNum >= MAX_FAILED) {
blocked.put(uuid, true);
log.severe("[Radio] " + p.getName()
+ " 已达最大失败次数,停止重试,"
+ "玩家继续留在服务器");
// 不踢出,只记录熔断,玩家正常游玩
p.sendMessage("§e§l[Radio] 资源包加载失败"
+ ",跳过资源包,部分画面、音频可能出现问题"
+ "§d§l若想继续下载,请退出重进");
+ " 达到最大失败,停止重试");
p.sendMessage(
"§e§l[Radio] 资源包加载失败"
+ ",部分音频可能异常");
p.sendMessage(
"§d§l若想重试,请退出重进");
return;
}
// 打了包没测试
p.sendMessage("§e§l[Radio] 下载失败,"
+ "重试... (" + failNum + "/" + MAX_FAILED + ")");
// 5秒后重试
p.sendMessage("§e§l[Radio] 下载失败,"
+ "第 " + failNum + " 次重试"
+ " (" + failNum + "/"
+ +MAX_FAILED + ")");
// ★ 3秒后重试(之前是5秒)
Bukkit.getScheduler()
.runTaskLater(plugin, () -> {
if (p.isOnline() && plugin.radio != null) {
plugin.radio.sendResourcePack(p);
if (p.isOnline()
&& plugin.radio != null) {
plugin.radio
.sendResourcePack(p);
}
}, 100L);
}, 60L);
}
}
+40 -41
View File
@@ -515,30 +515,32 @@ public class RadioManager {
// ========== 资源包发送 ==========
/**
* 发送资源包(可选,不强制)
* 优先用HTTP URL发送,兜底用byte[]
* 发送资源包(带限流,不禁重试)
*/
private final Map<UUID, Long> lastSendTime =
new ConcurrentHashMap<>();
public void sendResourcePack(Player p) {
UUID uuid = p.getUniqueId();
if (pendingPack.contains(uuid)) {
// 最短间隔 5秒(不是10秒的全锁死)
Long last = lastSendTime.get(uuid);
if (last != null
&& System.currentTimeMillis() - last
< 5000L) {
plugin.getLogger().info(
"[Radio] 跳过(已等待): "
"[Radio] 跳过(5秒内): "
+ p.getName());
return;
}
pendingPack.add(uuid);
Bukkit.getScheduler()
.runTaskLater(plugin, () -> {
pendingPack.remove(uuid);
}, 200L);
lastSendTime.put(uuid,
System.currentTimeMillis());
String url = getResourcePackUrl();
if (url == null || url.isEmpty()) {
plugin.getLogger().warning(
"[Radio] URL为空,"
+ "跳过发送: "
"[Radio] URL为空,跳过: "
+ p.getName());
pendingPack.remove(uuid);
return;
}
@@ -549,7 +551,6 @@ public class RadioManager {
plugin.getLogger().warning(
"[Radio] radiopack.zip不存在,"
+ "跳过: " + p.getName());
pendingPack.remove(uuid);
return;
}
@@ -560,22 +561,10 @@ public class RadioManager {
long fileSize = zip.length();
plugin.getLogger().info(
"[Radio] === 发送资源包 ===");
plugin.getLogger().info(
"[Radio] 玩家: " + p.getName());
plugin.getLogger().info(
"[Radio] URL: " + url);
plugin.getLogger().info(
"[Radio] Hash: " + hash);
plugin.getLogger().info(
"[Radio] 文件大小: "
+ fileSize + " bytes ("
+ String.format("%.1f",
fileSize / 1024.0)
+ " KB)");
plugin.getLogger().info(
"[Radio] 客户端版本: "
+ p.getProtocolVersion());
"[Radio] 发送: " + p.getName()
+ " url=" + url
+ " size=" + fileSize
+ " hash=" + hash);
sendPackWithHash(p, url, hash);
@@ -584,7 +573,6 @@ public class RadioManager {
"[Radio] 发送异常: "
+ p.getName());
e.printStackTrace();
pendingPack.remove(uuid);
}
}
@@ -594,37 +582,38 @@ public class RadioManager {
Player p,
String url,
String hash) {
UUID uuid = UUID.fromString(
"a1b2c3d4-e5f6-7890-abcd-"
+ "ef1234567890");
// 尝试5参 + Component
try {
Object component =
net.kyori.adventure.text
.Component.text(
"§d§l服务器需要自定义资源包,请您点击'继续'由客户端自动下载即可");
"§d§l请下载自定义资源包");
p.getClass().getMethod(
"setResourcePack",
UUID.class,
String.class,
String.class,
net.kyori.adventure
.text
.Component.class,
.text.Component
.class,
boolean.class)
.invoke(
p, uuid, url,
hash, component,
true);
plugin.getLogger().info(
"[Radio] 发送成功: "
+ p.getName()
+ " url="
+ url);
"[Radio] 成功(5参): "
+ p.getName());
return;
} catch (Exception ignored) {
}
// 尝试5参 + String描述
try {
p.getClass().getMethod(
"setResourcePack",
@@ -639,12 +628,13 @@ public class RadioManager {
"Sdf1 Radio",
true);
plugin.getLogger().info(
"[Radio] 旧版签名成功: "
"[Radio] 成功(5参String): "
+ p.getName());
return;
} catch (Exception ignored) {
}
// 尝试3参
try {
p.getClass().getMethod(
"setResourcePack",
@@ -655,18 +645,27 @@ public class RadioManager {
p, uuid, url,
hash);
plugin.getLogger().info(
"[Radio] 3参签名成功: "
"[Radio] 成功(3参): "
+ p.getName());
return;
} catch (Exception ignored) {
}
// ★ 全部失败 → 通知玩家 + 熔断
plugin.getLogger().severe(
"[Radio] 所有签名失败: "
+ p.getName());
+ p.getName()
+ " 客户端版本: "
+ p.getProtocolVersion());
p.sendMessage(
"§e§l[Radio] §f资源包发送失败,"
+ "§7部分功能可能异常");
p.sendMessage(
"§7可尝试: §e/rejoin §7或重启客户端");
}
private void sendPackFromLocal(
Player p, String url) {
File zip = new File(
@@ -718,7 +717,7 @@ public class RadioManager {
}
public void clearPending(UUID uuid) {
pendingPack.remove(uuid);
lastSendTime.remove(uuid);
}
@@ -0,0 +1,370 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.*;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
public class UserGroupManager {
private final Main plugin;
private Connection db;
private final Map<String, UserGroupConfig> groupConfigs = new ConcurrentHashMap<>();
public UserGroupManager(Main plugin) {
this.plugin = plugin;
initDB();
loadGroupConfigs();
startExpiryChecker();
}
// ===== DB =====
private void initDB() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(plugin.getDataFolder(), "usergroup.db");
db = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
st.execute("CREATE TABLE IF NOT EXISTS user_group_config ("
+ "group_name TEXT PRIMARY KEY,"
+ "display_name TEXT DEFAULT '',"
+ "display_color TEXT DEFAULT '§f',"
+ "display_emoji TEXT DEFAULT '',"
+ "priority INTEGER DEFAULT 0,"
+ "is_permanent INTEGER DEFAULT 1,"
+ "duration_minutes INTEGER DEFAULT 0,"
+ "pack_url TEXT DEFAULT '',"
+ "pack_hash TEXT DEFAULT '',"
+ "pack_prompt TEXT DEFAULT 'Custom Resource Pack',"
+ "pack_size INTEGER DEFAULT 0)");
st.execute("CREATE TABLE IF NOT EXISTS user_group_member ("
+ "player_name TEXT NOT NULL,"
+ "group_name TEXT NOT NULL,"
+ "added_by TEXT DEFAULT 'system',"
+ "added_time INTEGER DEFAULT 0,"
+ "expiry_time INTEGER DEFAULT 0,"
+ "PRIMARY KEY(player_name, group_name))");
st.close();
} catch (Exception e) {
throw new RuntimeException("[UserGroup] DB init failed: " + e.getMessage(), e);
}
}
// ===== 配置加载 =====
public void loadGroupConfigs() {
groupConfigs.clear();
File dir = new File(plugin.getDataFolder(), "用户组");
if (!dir.exists()) dir.mkdirs();
File[] files = dir.listFiles((d, n) -> n.endsWith(".txt"));
if (files == null) return;
for (File f : files) {
try {
List<String> lines = java.nio.file.Files.readAllLines(
f.toPath(), StandardCharsets.UTF_8);
UserGroupConfig cfg = parseGroupConfig(lines);
if (cfg != null) {
groupConfigs.put(cfg.name, cfg);
saveGroupConfigToDB(cfg);
}
} catch (Exception e) {
plugin.getLogger().warning("[UserGroup] Failed to load: " + f.getName());
}
}
}
private UserGroupConfig parseGroupConfig(List<String> lines) {
UserGroupConfig cfg = new UserGroupConfig();
boolean inBlock = false;
for (String raw : lines) {
String l = raw;
// 块注释
if (l.contains("<!--")) { inBlock = true; continue; }
if (l.contains("-->")) { inBlock = false; continue; }
if (l.contains("/*")) { inBlock = true; continue; }
if (l.contains("*/")) { inBlock = false; continue; }
if (inBlock) continue;
// 行注释
l = l.trim();
if (l.isEmpty()) continue;
if (l.startsWith("#") || l.startsWith("//")) continue;
// 行内注释
int h = l.indexOf('#');
int s = l.indexOf("//");
if (h >= 0) l = l.substring(0, h).trim();
if (s >= 0 && (h < 0 || s < h)) l = l.substring(0, s).trim();
if (l.isEmpty()) continue;
// 解析键值对
String key = l;
String val = "";
int idx = l.indexOf('\uff1a');
if (idx < 0) idx = l.indexOf(':');
if (idx >= 0) {
key = l.substring(0, idx).trim();
val = l.substring(idx + 1).trim();
if (val.startsWith("\"") && val.endsWith("\""))
val = val.substring(1, val.length() - 1);
}
switch (key) {
case "组名":
case "name": cfg.name = val; break;
case "显示名":
case "display": cfg.displayName = val; break;
case "颜色":
case "color": cfg.displayColor = val; break;
case "符号":
case "emoji": cfg.displayEmoji = val; break;
case "优先级":
case "priority":
try { cfg.priority = Integer.parseInt(val); } catch (Exception ignored) {}
break;
case "类型":
case "type":
cfg.permanent = val.equalsIgnoreCase("永久") || val.equalsIgnoreCase("permanent");
break;
case "时长":
case "duration":
try { cfg.durationMinutes = Integer.parseInt(val); } catch (Exception ignored) {}
break;
case "资源包URL":
case "pack_url": cfg.packUrl = val; break;
case "资源包Hash":
case "pack_hash": cfg.packHash = val; break;
case "资源包提示":
case "pack_prompt": cfg.packPrompt = val; break;
case "资源包大小":
case "pack_size":
try { cfg.packSize = Integer.parseInt(val); } catch (Exception ignored) {}
break;
case "本地文件":
case "local_file": cfg.localFile = val; break;
}
}
if (cfg.name == null || cfg.name.isEmpty()) return null;
return cfg;
}
private void saveGroupConfigToDB(UserGroupConfig cfg) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO user_group_config "
+ "(group_name, display_name, display_color, display_emoji,"
+ " priority, is_permanent, duration_minutes,"
+ " pack_url, pack_hash, pack_prompt, pack_size)"
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?)");
ps.setString(1, cfg.name);
ps.setString(2, cfg.displayName);
ps.setString(3, cfg.displayColor);
ps.setString(4, cfg.displayEmoji);
ps.setInt(5, cfg.priority);
ps.setInt(6, cfg.permanent ? 1 : 0);
ps.setInt(7, cfg.durationMinutes);
ps.setString(8, cfg.packUrl);
ps.setString(9, cfg.packHash);
ps.setString(10, cfg.packPrompt);
ps.setInt(11, cfg.packSize);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// ===== 玩家操作 =====
public boolean addPlayer(String player, String groupName, String addedBy, long durationMinutes) {
UserGroupConfig cfg = groupConfigs.get(groupName);
if (cfg == null) return false;
long now = System.currentTimeMillis();
long expiry = cfg.permanent ? 0 : now + durationMinutes * 60 * 1000;
if (!cfg.permanent && durationMinutes > 0) expiry = now + durationMinutes * 60 * 1000;
else if (!cfg.permanent) expiry = now + cfg.durationMinutes * 60 * 1000;
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, expiry);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
Player p = plugin.getServer().getPlayerExact(player);
if (p != null) sendResourcePack(p);
return true;
}
public boolean removePlayer(String player, String groupName) {
try {
PreparedStatement ps = db.prepareStatement(
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
ps.setString(1, player);
ps.setString(2, groupName);
int rows = ps.executeUpdate();
ps.close();
return rows > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public List<Map<String, Object>> getPlayerGroups(String player) {
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM user_group_member WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
long now = System.currentTimeMillis();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("group_name", rs.getString("group_name"));
row.put("added_by", rs.getString("added_by"));
long expiry = rs.getLong("expiry_time");
row.put("expiry_time", expiry);
row.put("expired", expiry > 0 && expiry <= now);
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
public UserGroupConfig getHighestGroup(String player) {
List<Map<String, Object>> groups = getPlayerGroups(player);
long now = System.currentTimeMillis();
UserGroupConfig best = null;
for (Map<String, Object> g : groups) {
long expiry = (long) g.get("expiry_time");
if (expiry > 0 && expiry <= now) continue;
String name = (String) g.get("group_name");
UserGroupConfig cfg = groupConfigs.get(name);
if (cfg != null && (best == null || cfg.priority > best.priority)) {
best = cfg;
}
}
return best;
}
public boolean isPlayerInGroup(String player, String groupName) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT 1 FROM user_group_member WHERE player_name=? AND group_name=?");
ps.setString(1, player);
ps.setString(2, groupName);
boolean exists = ps.executeQuery().next();
ps.close();
return exists;
} catch (SQLException e) {
return false;
}
}
// ===== 过期自动移除 =====
private void startExpiryChecker() {
new BukkitRunnable() {
@Override
public void run() {
checkExpiry();
}
}.runTaskTimerAsynchronously(plugin, 1200L, 1200L); // 每分钟检查
}
private void checkExpiry() {
try {
long now = System.currentTimeMillis();
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[]> expired = new ArrayList<>();
while (rs.next()) {
expired.add(new String[]{
rs.getString("player_name"),
rs.getString("group_name")});
}
rs.close(); ps.close();
for (String[] e : expired) {
PreparedStatement del = db.prepareStatement(
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
del.setString(1, e[0]);
del.setString(2, e[1]);
del.executeUpdate();
del.close();
plugin.getLogger().info("[UserGroup] 过期移除: " + e[0] + " -> " + e[1]);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
// ===== 资源包分发 =====
public void sendResourcePack(Player p) {
UserGroupConfig cfg = getHighestGroup(p.getName());
if (cfg == null || cfg.packUrl == null || cfg.packUrl.isEmpty()) return;
String url = cfg.packUrl;
String prompt = cfg.packPrompt != null
? cfg.packPrompt : "Custom Resource Pack";
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
try {
p.setResourcePack(url, new byte[0], prompt, false);
plugin.getLogger().info("[UserGroup] 资源包已发送: "
+ p.getName() + " -> " + cfg.displayName);
} catch (Exception e) {
plugin.getLogger().warning(
"[UserGroup] 资源包发送失败: " + e.getMessage());
}
}, 40L);
}
public Map<String, UserGroupConfig> getGroupConfigs() {
return groupConfigs;
}
// ===== 配置类 =====
public static class UserGroupConfig {
public String name;
public String displayName = "";
public String displayColor = "§f";
public String displayEmoji = "";
public int priority = 0;
public boolean permanent = true;
public int durationMinutes = 0;
public String packUrl = "";
public String packHash = "";
public String packPrompt = "Custom Resource Pack";
public int packSize = 0;
public String localFile = "";
}
}
+9 -3
View File
@@ -60,9 +60,9 @@ commands:
deny:
description: 拒绝OA请求
usage: /deny <编号> [理由]
sdf1debug:
description: 调试指令
usage: /sdf1debug <玩家> take.<阶段>
#sdf1debug:
# description: 调试指令
# usage: /sdf1debug <玩家> take.<阶段>
email:
description: 绑定邮箱
usage: /email <邮箱>
@@ -90,3 +90,9 @@ commands:
pvp :
description: PVP榜单
usage: /pvp
cypay :
description: 草原探险债券
usage: /cypay
printer :
description: 打印债券流水
usage: /printer