feat: CraftBank Folia 1.21.8 经济与银行核心插件初始实现
实现完整的现金/钱包、银行卡、支票、活期储蓄与定期存款系统, 实现并以 Highest 优先级注册 Vault Economy 接口,接入 PlaceholderAPI。 全程遵循 Folia 调度模型(AsyncScheduler / 实体区域线程), 数据缓存线程安全,支票兑现与定期领取做防刷处理。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
package com.craftbank.database;
|
||||
|
||||
import com.craftbank.CraftBank;
|
||||
import com.craftbank.model.PlayerAccount;
|
||||
import com.craftbank.model.TermDeposit;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* 数据库管理器。支持 SQLite (默认) 与 MySQL, 通过 HikariCP 管理连接池。
|
||||
*
|
||||
* <p>所有方法均为阻塞式 JDBC 调用, <b>必须</b> 在 Folia 的 AsyncScheduler
|
||||
* 线程上调用, 严禁在主区域线程同步调用。</p>
|
||||
*/
|
||||
public class DatabaseManager {
|
||||
|
||||
public enum Type {
|
||||
SQLITE, MYSQL
|
||||
}
|
||||
|
||||
private final CraftBank plugin;
|
||||
private HikariDataSource dataSource;
|
||||
private Type type;
|
||||
|
||||
public DatabaseManager(CraftBank plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void init() throws SQLException {
|
||||
FileConfiguration cfg = plugin.getConfig();
|
||||
String typeName = cfg.getString("Database.type", "SQLITE").toUpperCase();
|
||||
this.type = "MYSQL".equals(typeName) ? Type.MYSQL : Type.SQLITE;
|
||||
|
||||
HikariConfig hikari = new HikariConfig();
|
||||
hikari.setPoolName("CraftBank-Pool");
|
||||
|
||||
if (type == Type.MYSQL) {
|
||||
String host = cfg.getString("Database.mysql.host", "localhost");
|
||||
int port = cfg.getInt("Database.mysql.port", 3306);
|
||||
String db = cfg.getString("Database.mysql.database", "craftbank");
|
||||
boolean ssl = cfg.getBoolean("Database.mysql.useSSL", false);
|
||||
hikari.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + db
|
||||
+ "?useSSL=" + ssl + "&useUnicode=true&characterEncoding=utf8&autoReconnect=true");
|
||||
hikari.setUsername(cfg.getString("Database.mysql.username", "root"));
|
||||
hikari.setPassword(cfg.getString("Database.mysql.password", ""));
|
||||
hikari.setMaximumPoolSize(cfg.getInt("Database.mysql.pool-size", 10));
|
||||
} else {
|
||||
File file = new File(plugin.getDataFolder(), cfg.getString("Database.sqlite-file", "craftbank.db"));
|
||||
hikari.setDriverClassName("org.sqlite.JDBC");
|
||||
hikari.setJdbcUrl("jdbc:sqlite:" + file.getAbsolutePath());
|
||||
// SQLite 单写入者, 限制为 1 条连接以避免 "database is locked"。
|
||||
hikari.setMaximumPoolSize(1);
|
||||
}
|
||||
hikari.setConnectionTimeout(10000);
|
||||
|
||||
this.dataSource = new HikariDataSource(hikari);
|
||||
createTables();
|
||||
}
|
||||
|
||||
private void createTables() throws SQLException {
|
||||
String autoInc = type == Type.MYSQL ? "INT AUTO_INCREMENT PRIMARY KEY" : "INTEGER PRIMARY KEY AUTOINCREMENT";
|
||||
String players = "CREATE TABLE IF NOT EXISTS craftbank_players (" +
|
||||
"uuid VARCHAR(36) PRIMARY KEY," +
|
||||
"player_name VARCHAR(32)," +
|
||||
"cash DOUBLE NOT NULL DEFAULT 0," +
|
||||
"bank_saving DOUBLE NOT NULL DEFAULT 0," +
|
||||
"last_interest BIGINT NOT NULL DEFAULT 0," +
|
||||
"card_serial BIGINT NOT NULL DEFAULT 0" +
|
||||
")";
|
||||
String deposits = "CREATE TABLE IF NOT EXISTS craftbank_term_deposits (" +
|
||||
"id " + autoInc + "," +
|
||||
"owner VARCHAR(36) NOT NULL," +
|
||||
"principal DOUBLE NOT NULL," +
|
||||
"daily_rate DOUBLE NOT NULL," +
|
||||
"duration_days INT NOT NULL," +
|
||||
"start_time BIGINT NOT NULL," +
|
||||
"end_time BIGINT NOT NULL," +
|
||||
"claimed INT NOT NULL DEFAULT 0" +
|
||||
")";
|
||||
try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
|
||||
st.executeUpdate(players);
|
||||
st.executeUpdate(deposits);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 玩家账户
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/** 读取账户, 不存在返回 null。 */
|
||||
public PlayerAccount loadAccount(UUID uuid) {
|
||||
String sql = "SELECT player_name, cash, bank_saving, last_interest, card_serial FROM craftbank_players WHERE uuid = ?";
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, uuid.toString());
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return new PlayerAccount(uuid,
|
||||
rs.getString("player_name"),
|
||||
rs.getDouble("cash"),
|
||||
rs.getDouble("bank_saving"),
|
||||
rs.getLong("last_interest"),
|
||||
rs.getLong("card_serial"));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "读取账户失败: " + uuid, ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void saveAccount(PlayerAccount account) {
|
||||
String sql = type == Type.MYSQL
|
||||
? "INSERT INTO craftbank_players (uuid, player_name, cash, bank_saving, last_interest, card_serial) " +
|
||||
"VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE player_name=VALUES(player_name)," +
|
||||
"cash=VALUES(cash), bank_saving=VALUES(bank_saving), last_interest=VALUES(last_interest), card_serial=VALUES(card_serial)"
|
||||
: "INSERT INTO craftbank_players (uuid, player_name, cash, bank_saving, last_interest, card_serial) " +
|
||||
"VALUES (?,?,?,?,?,?) ON CONFLICT(uuid) DO UPDATE SET player_name=excluded.player_name," +
|
||||
"cash=excluded.cash, bank_saving=excluded.bank_saving, last_interest=excluded.last_interest, card_serial=excluded.card_serial";
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
// 在持有快照期间数据可能被其他线程修改, 直接读取同步后的字段。
|
||||
synchronized (account) {
|
||||
ps.setString(1, account.getUuid().toString());
|
||||
ps.setString(2, account.getName());
|
||||
ps.setDouble(3, account.getCash());
|
||||
ps.setDouble(4, account.getBankSaving());
|
||||
ps.setLong(5, account.getLastInterestSettle());
|
||||
ps.setLong(6, account.getCardSerial());
|
||||
}
|
||||
ps.executeUpdate();
|
||||
account.setDirty(false);
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "保存账户失败: " + account.getUuid(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** 现金排行榜。 */
|
||||
public List<Map.Entry<String, Double>> getTopCash(int limit) {
|
||||
List<Map.Entry<String, Double>> result = new ArrayList<>();
|
||||
String sql = "SELECT player_name, cash FROM craftbank_players ORDER BY cash DESC LIMIT ?";
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setInt(1, limit);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String name = rs.getString("player_name");
|
||||
result.add(Map.entry(name == null ? "?" : name, rs.getDouble("cash")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "读取排行榜失败", ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 定期存款
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/** 插入一条新的定期存款, 返回生成的记录 (含自增 id)。 */
|
||||
public TermDeposit insertTermDeposit(UUID owner, double principal, double dailyRate,
|
||||
int durationDays, long startTime, long endTime) {
|
||||
String sql = "INSERT INTO craftbank_term_deposits (owner, principal, daily_rate, duration_days, start_time, end_time, claimed) " +
|
||||
"VALUES (?,?,?,?,?,?,0)";
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
ps.setString(1, owner.toString());
|
||||
ps.setDouble(2, principal);
|
||||
ps.setDouble(3, dailyRate);
|
||||
ps.setInt(4, durationDays);
|
||||
ps.setLong(5, startTime);
|
||||
ps.setLong(6, endTime);
|
||||
ps.executeUpdate();
|
||||
int id = -1;
|
||||
try (ResultSet keys = ps.getGeneratedKeys()) {
|
||||
if (keys.next()) {
|
||||
id = keys.getInt(1);
|
||||
}
|
||||
}
|
||||
return new TermDeposit(id, owner, principal, dailyRate, durationDays, startTime, endTime, false);
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "创建定期存款失败: " + owner, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<TermDeposit> getTermDeposits(UUID owner, boolean includeClaimed) {
|
||||
List<TermDeposit> list = new ArrayList<>();
|
||||
String sql = "SELECT * FROM craftbank_term_deposits WHERE owner = ?"
|
||||
+ (includeClaimed ? "" : " AND claimed = 0") + " ORDER BY end_time ASC";
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, owner.toString());
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
list.add(readDeposit(rs));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "读取定期存款失败: " + owner, ex);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 所有已到期但尚未领取的定期存款 (供定时扫描自动结算)。 */
|
||||
public List<TermDeposit> getMaturedUnclaimed() {
|
||||
List<TermDeposit> list = new ArrayList<>();
|
||||
String sql = "SELECT * FROM craftbank_term_deposits WHERE claimed = 0 AND end_time <= ?";
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setLong(1, System.currentTimeMillis());
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
list.add(readDeposit(rs));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "读取到期定期存款失败", ex);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子地将一笔定期存款标记为已领取。
|
||||
*
|
||||
* @return true 表示本次调用成功完成了标记 (claimed 由 0 改为 1);
|
||||
* false 表示该笔存款已被其他线程领取, 调用方不应重复发放本息。
|
||||
*/
|
||||
public boolean markClaimed(int id) {
|
||||
String sql = "UPDATE craftbank_term_deposits SET claimed = 1 WHERE id = ? AND claimed = 0";
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setInt(1, id);
|
||||
return ps.executeUpdate() > 0;
|
||||
} catch (SQLException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "标记定期存款失败: id=" + id, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private TermDeposit readDeposit(ResultSet rs) throws SQLException {
|
||||
return new TermDeposit(
|
||||
rs.getInt("id"),
|
||||
UUID.fromString(rs.getString("owner")),
|
||||
rs.getDouble("principal"),
|
||||
rs.getDouble("daily_rate"),
|
||||
rs.getInt("duration_days"),
|
||||
rs.getLong("start_time"),
|
||||
rs.getLong("end_time"),
|
||||
rs.getInt("claimed") != 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user