feat: add transaction logs and custom bank inputs
Adds configurable fees, transaction history, chat amount input, and v1.1.0 release documentation.
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# CraftBank — 项目约定
|
||||||
|
|
||||||
|
适用于 **Folia 1.21.8** 的经济与银行核心插件(Java 21+,Gradle 构建)。
|
||||||
|
|
||||||
|
## 仓库
|
||||||
|
|
||||||
|
- 远端:私有仓库 `dmz7p4xmqh-coder/CraftBank`(默认分支 `main`)。
|
||||||
|
- `gh` 与 `git` 已安装并登录,可直接用于推送与发行。
|
||||||
|
|
||||||
|
## 每次改动后的固定流程
|
||||||
|
|
||||||
|
完成任何代码改动后,**主动执行**以下步骤(无需等用户再次要求):
|
||||||
|
|
||||||
|
1. **构建验证**:运行 `./gradlew shadowJar`,确认编译通过并产出 `build/libs/CraftBank-<version>.jar`。
|
||||||
|
2. **更新 README**:若改动影响功能、指令、权限、配置项或 PlaceholderAPI 变量,同步更新 `README.md`。
|
||||||
|
3. **提交并推送**到 `main`。
|
||||||
|
4. **发布 Release**:用 `gh release create` 创建新发行版,附上编译好的 jar。
|
||||||
|
|
||||||
|
## 版本号约定(语义化版本)
|
||||||
|
|
||||||
|
- 新功能 → 递增次版本号:`v1.x.0`
|
||||||
|
- Bug 修复 → 递增修订号:`v1.0.x`
|
||||||
|
- 不兼容的重大改动 → 递增主版本号:`v2.0.0`
|
||||||
|
|
||||||
|
发行版 tag 与 `build.gradle.kts` 里的 `version`、`plugin.yml` 的版本需保持一致(`plugin.yml` 通过 `${version}` 占位符在构建时自动填充,改 `build.gradle.kts` 的 `version` 即可)。
|
||||||
|
|
||||||
|
## 构建注意事项
|
||||||
|
|
||||||
|
- 用当前 JDK(已验证 JDK 25)编译,通过 `--release 21` 产出 Java 21 字节码。
|
||||||
|
- **不要**在 shadowJar 中启用包重定位(`relocate`):当前 shadow 8.3.5 的字节码重映射器在新 JDK 产出的 `invokedynamic` class 上会崩溃。如确需重定位,改用 JDK 21 编译或升级 shadow 插件。
|
||||||
|
- MySQL 驱动默认不打包;需要时在 `build.gradle.kts` 取消相应依赖注释。
|
||||||
|
- 不要提交 `build/`、`.gradle/`、`.Codex/`(已在 `.gitignore` 中排除)。
|
||||||
|
|
||||||
|
## 架构速览
|
||||||
|
|
||||||
|
- 主类 `CraftBank`:初始化各子系统、注册 Vault(Highest)、封装 Folia 调度(`runAsync` / `runForPlayer` / `runGlobal`)。
|
||||||
|
- `economy/`:`EconomyManager`(`ConcurrentHashMap` 缓存 + `synchronized` 账户)、`VaultEconomyProvider`(Vault 接口实现)。
|
||||||
|
- `bank/BankManager`:活期复利结算、定期存款创建 / 锁定 / 到期结算。
|
||||||
|
- `database/DatabaseManager`:SQLite/MySQL,定期领取用 `UPDATE ... WHERE claimed = 0` 原子置位防重复。
|
||||||
|
- `items/`:银行卡与支票的 PDC 创建与防伪校验。
|
||||||
|
- `commands/`、`listeners/`、`gui/`、`hooks/`:指令、事件、GUI、PlaceholderAPI。
|
||||||
|
|
||||||
|
## Folia 线程纪律
|
||||||
|
|
||||||
|
- 严禁 `Bukkit.getScheduler()`。
|
||||||
|
- 数据库 / 排序 / 利息扫描等后台任务走 `AsyncScheduler`。
|
||||||
|
- 操作玩家物品、开 GUI、发消息从异步切回时调度到 `player.getScheduler()`(实体区域线程)。
|
||||||
|
- 经济数据操作必须线程安全(Vault 接口可能在其它插件的异步线程被调用)。
|
||||||
@@ -19,6 +19,9 @@ CraftBank 内置完善的「个人钱包 / 现金」系统,并引入深度模
|
|||||||
| 📜 **支票** | `/cheque <金额>` 具现化为可流通实体物品;右键兑现入钱包;PDC 防伪 + 防刷 |
|
| 📜 **支票** | `/cheque <金额>` 具现化为可流通实体物品;右键兑现入钱包;PDC 防伪 + 防刷 |
|
||||||
| 💰 **活期储蓄** | 存取自由,按真实时间戳逐日复利结算(含离线补偿,带封顶)|
|
| 💰 **活期储蓄** | 存取自由,按真实时间戳逐日复利结算(含离线补偿,带封顶)|
|
||||||
| ⏳ **定期存款** | 7/15/30 天可选,到期前**任何情况不可提前支取**;到期本息自动入活期 |
|
| ⏳ **定期存款** | 7/15/30 天可选,到期前**任何情况不可提前支取**;到期本息自动入活期 |
|
||||||
|
| 📒 **交易流水** | 记录转账、存取、支票、定期、利息与管理操作;管理员可分页查询 |
|
||||||
|
| 🧾 **手续费** | 转账与活期取现可按配置收取固定 / 百分比手续费 |
|
||||||
|
| 💬 **自定义金额输入** | 银行 GUI 左键进入聊天输入自定义金额,右键保留「全部」快捷操作 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ CraftBank 内置完善的「个人钱包 / 现金」系统,并引入深度模
|
|||||||
| `/bankadmin reload` | 重载配置 |
|
| `/bankadmin reload` | 重载配置 |
|
||||||
| `/bankadmin give/take/set <玩家> <金额>` | 增加 / 扣除 / 设置现金 |
|
| `/bankadmin give/take/set <玩家> <金额>` | 增加 / 扣除 / 设置现金 |
|
||||||
| `/bankadmin setinterest <savings\|term_7d\|term_15d\|term_30d> <利率>` | 动态修改利率 |
|
| `/bankadmin setinterest <savings\|term_7d\|term_15d\|term_30d> <利率>` | 动态修改利率 |
|
||||||
|
| `/bankadmin log <玩家> [页码]` | 分页查看玩家交易流水 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -67,12 +71,16 @@ CraftBank 内置完善的「个人钱包 / 现金」系统,并引入深度模
|
|||||||
| `%craftbank_cash_formatted%` | 现金(含货币符号)|
|
| `%craftbank_cash_formatted%` | 现金(含货币符号)|
|
||||||
| `%craftbank_bank_saving%` | 活期(纯数字)|
|
| `%craftbank_bank_saving%` | 活期(纯数字)|
|
||||||
| `%craftbank_bank_saving_formatted%` | 活期(含货币符号)|
|
| `%craftbank_bank_saving_formatted%` | 活期(含货币符号)|
|
||||||
|
| `%craftbank_total%` | 现金 + 活期总额(纯数字)|
|
||||||
|
| `%craftbank_total_formatted%` | 现金 + 活期总额(含货币符号)|
|
||||||
|
| `%craftbank_has_card%` | 是否持有有效银行卡 |
|
||||||
|
| `%craftbank_savings_rate%` | 当前活期日利率百分比 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚙️ 配置文件
|
## ⚙️ 配置文件
|
||||||
|
|
||||||
详见生成的 `config.yml`,涵盖:数据库(SQLite/MySQL)、货币设置、利率(活期 / 各定期)、银行交互方块、银行卡与支票的自定义材质 / CustomModelData / 名称 / Lore、以及全部消息文案。
|
详见生成的 `config.yml`,涵盖:数据库(SQLite/MySQL)、货币设置、利率(活期 / 各定期)、银行交互方块、转账 / 取现手续费、交易流水开关、银行卡与支票的自定义材质 / CustomModelData / 名称 / Lore、以及全部消息文案。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "com.craftbank"
|
group = "com.craftbank"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
|
|
||||||
// 用当前 JDK(25)编译,通过 --release 21 产出兼容 Java 21 的字节码,
|
// 用当前 JDK(25)编译,通过 --release 21 产出兼容 Java 21 的字节码,
|
||||||
// 避免 toolchain 触发额外的 JDK 下载。Folia 要求运行时为 Java 21+。
|
// 避免 toolchain 触发额外的 JDK 下载。Folia 要求运行时为 Java 21+。
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.craftbank;
|
package com.craftbank;
|
||||||
|
|
||||||
import com.craftbank.bank.BankManager;
|
import com.craftbank.bank.BankManager;
|
||||||
|
import com.craftbank.bank.FeeCalculator;
|
||||||
|
import com.craftbank.bank.TransactionLog;
|
||||||
import com.craftbank.commands.BankAdminCommand;
|
import com.craftbank.commands.BankAdminCommand;
|
||||||
import com.craftbank.commands.BankCommand;
|
import com.craftbank.commands.BankCommand;
|
||||||
import com.craftbank.commands.BaltopCommand;
|
import com.craftbank.commands.BaltopCommand;
|
||||||
@@ -11,6 +13,7 @@ import com.craftbank.database.DatabaseManager;
|
|||||||
import com.craftbank.economy.EconomyManager;
|
import com.craftbank.economy.EconomyManager;
|
||||||
import com.craftbank.economy.VaultEconomyProvider;
|
import com.craftbank.economy.VaultEconomyProvider;
|
||||||
import com.craftbank.gui.BankGUI;
|
import com.craftbank.gui.BankGUI;
|
||||||
|
import com.craftbank.gui.PendingInput;
|
||||||
import com.craftbank.hooks.CraftBankExpansion;
|
import com.craftbank.hooks.CraftBankExpansion;
|
||||||
import com.craftbank.items.BankCardManager;
|
import com.craftbank.items.BankCardManager;
|
||||||
import com.craftbank.items.ChequeManager;
|
import com.craftbank.items.ChequeManager;
|
||||||
@@ -40,6 +43,9 @@ public final class CraftBank extends JavaPlugin {
|
|||||||
private DatabaseManager databaseManager;
|
private DatabaseManager databaseManager;
|
||||||
private EconomyManager economyManager;
|
private EconomyManager economyManager;
|
||||||
private BankManager bankManager;
|
private BankManager bankManager;
|
||||||
|
private TransactionLog transactionLog;
|
||||||
|
private FeeCalculator feeCalculator;
|
||||||
|
private PendingInput pendingInput;
|
||||||
private BankCardManager bankCardManager;
|
private BankCardManager bankCardManager;
|
||||||
private ChequeManager chequeManager;
|
private ChequeManager chequeManager;
|
||||||
private BankGUI bankGUI;
|
private BankGUI bankGUI;
|
||||||
@@ -70,6 +76,9 @@ public final class CraftBank extends JavaPlugin {
|
|||||||
|
|
||||||
this.economyManager = new EconomyManager(this);
|
this.economyManager = new EconomyManager(this);
|
||||||
this.bankManager = new BankManager(this);
|
this.bankManager = new BankManager(this);
|
||||||
|
this.transactionLog = new TransactionLog(this);
|
||||||
|
this.feeCalculator = new FeeCalculator(this);
|
||||||
|
this.pendingInput = new PendingInput();
|
||||||
this.bankCardManager = new BankCardManager(this);
|
this.bankCardManager = new BankCardManager(this);
|
||||||
this.chequeManager = new ChequeManager(this);
|
this.chequeManager = new ChequeManager(this);
|
||||||
this.bankGUI = new BankGUI(this);
|
this.bankGUI = new BankGUI(this);
|
||||||
@@ -138,6 +147,7 @@ public final class CraftBank extends JavaPlugin {
|
|||||||
getServer().getPluginManager().registerEvents(new PlayerConnectionListener(this), this);
|
getServer().getPluginManager().registerEvents(new PlayerConnectionListener(this), this);
|
||||||
getServer().getPluginManager().registerEvents(new InteractListener(this), this);
|
getServer().getPluginManager().registerEvents(new InteractListener(this), this);
|
||||||
getServer().getPluginManager().registerEvents(new InventoryListener(this), this);
|
getServer().getPluginManager().registerEvents(new InventoryListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new com.craftbank.listeners.ChatInputListener(this), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerPlaceholders() {
|
private void registerPlaceholders() {
|
||||||
@@ -219,6 +229,18 @@ public final class CraftBank extends JavaPlugin {
|
|||||||
return bankManager;
|
return bankManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TransactionLog getTransactionLog() {
|
||||||
|
return transactionLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeeCalculator getFeeCalculator() {
|
||||||
|
return feeCalculator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PendingInput getPendingInput() {
|
||||||
|
return pendingInput;
|
||||||
|
}
|
||||||
|
|
||||||
public BankCardManager getBankCardManager() {
|
public BankCardManager getBankCardManager() {
|
||||||
return bankCardManager;
|
return bankCardManager;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.craftbank.bank;
|
||||||
|
|
||||||
|
import com.craftbank.CraftBank;
|
||||||
|
import com.craftbank.util.Amounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手续费计算。手续费 = 固定费 + 金额 × 百分比,并受最小/最大值约束。
|
||||||
|
*
|
||||||
|
* <p>配置位于 {@code Fees.<场景>},目前支持场景:{@code pay}(转账)、{@code withdraw}(取现)。
|
||||||
|
* 各场景独立配置,未配置或全为 0 表示不收费。</p>
|
||||||
|
*/
|
||||||
|
public final class FeeCalculator {
|
||||||
|
|
||||||
|
private final CraftBank plugin;
|
||||||
|
|
||||||
|
public FeeCalculator(CraftBank plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算指定场景下、给定金额应收的手续费(已规整到 2 位小数)。
|
||||||
|
*
|
||||||
|
* @param scene 配置场景键,如 "pay" / "withdraw"
|
||||||
|
* @param amount 交易金额
|
||||||
|
*/
|
||||||
|
public double calculate(String scene, double amount) {
|
||||||
|
String base = "Fees." + scene + ".";
|
||||||
|
double percent = plugin.getConfig().getDouble(base + "percent", 0.0);
|
||||||
|
double fixed = plugin.getConfig().getDouble(base + "fixed", 0.0);
|
||||||
|
double min = plugin.getConfig().getDouble(base + "min", 0.0);
|
||||||
|
double max = plugin.getConfig().getDouble(base + "max", Double.MAX_VALUE);
|
||||||
|
|
||||||
|
double fee = fixed + amount * percent;
|
||||||
|
if (fee < min) fee = min;
|
||||||
|
if (fee > max) fee = max;
|
||||||
|
if (fee < 0) fee = 0;
|
||||||
|
// 手续费不应超过交易金额本身。
|
||||||
|
if (fee > amount) fee = amount;
|
||||||
|
return Amounts.normalize(fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 该场景是否配置了任何手续费。 */
|
||||||
|
public boolean hasFee(String scene) {
|
||||||
|
String base = "Fees." + scene + ".";
|
||||||
|
return plugin.getConfig().getDouble(base + "percent", 0.0) > 0
|
||||||
|
|| plugin.getConfig().getDouble(base + "fixed", 0.0) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.craftbank.bank;
|
||||||
|
|
||||||
|
import com.craftbank.CraftBank;
|
||||||
|
import com.craftbank.model.Transaction;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易流水服务。统一封装异步写入与查询,并受配置开关控制是否记录。
|
||||||
|
*/
|
||||||
|
public final class TransactionLog {
|
||||||
|
|
||||||
|
private final CraftBank plugin;
|
||||||
|
|
||||||
|
public TransactionLog(CraftBank plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean enabled() {
|
||||||
|
return plugin.getConfig().getBoolean("Transaction_Log.enabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 异步记录一条流水(无手续费、无对手方的简化重载)。 */
|
||||||
|
public void record(UUID player, Transaction.Type type, double amount, String description) {
|
||||||
|
record(player, type, amount, 0.0, null, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 异步记录一条流水。本方法可在任意线程调用,内部自行切换到异步线程写库。 */
|
||||||
|
public void record(UUID player, Transaction.Type type, double amount, double fee,
|
||||||
|
UUID counterparty, String description) {
|
||||||
|
if (!enabled() || player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Transaction tx = new Transaction(0, player, type, amount, fee, counterparty,
|
||||||
|
description, System.currentTimeMillis());
|
||||||
|
plugin.runAsync(() -> plugin.getDatabaseManager().insertTransaction(tx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询玩家流水(阻塞,须在异步线程调用)。 */
|
||||||
|
public List<Transaction> query(UUID player, int page, int pageSize) {
|
||||||
|
return plugin.getDatabaseManager().getTransactions(player, page, pageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ public final class BankAdminCommand implements TabExecutor {
|
|||||||
case "take" -> modify(sender, args, ModifyType.TAKE);
|
case "take" -> modify(sender, args, ModifyType.TAKE);
|
||||||
case "set" -> modify(sender, args, ModifyType.SET);
|
case "set" -> modify(sender, args, ModifyType.SET);
|
||||||
case "setinterest" -> setInterest(sender, args);
|
case "setinterest" -> setInterest(sender, args);
|
||||||
|
case "log" -> queryLog(sender, args);
|
||||||
default -> sendHelp(sender);
|
default -> sendHelp(sender);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -90,6 +91,13 @@ public final class BankAdminCommand implements TabExecutor {
|
|||||||
msg.raw(sender, "&c操作失败 (可能是余额不足)。");
|
msg.raw(sender, "&c操作失败 (可能是余额不足)。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
com.craftbank.model.Transaction.Type txType = switch (type) {
|
||||||
|
case GIVE -> com.craftbank.model.Transaction.Type.ADMIN_GIVE;
|
||||||
|
case TAKE -> com.craftbank.model.Transaction.Type.ADMIN_TAKE;
|
||||||
|
case SET -> com.craftbank.model.Transaction.Type.ADMIN_SET;
|
||||||
|
};
|
||||||
|
plugin.getTransactionLog().record(uuid, txType, amount,
|
||||||
|
"管理员 " + sender.getName() + " 操作");
|
||||||
msg.raw(sender, "&a已" + label(type) + " &f" + targetName + " &a现金 " + econ.format(amount)
|
msg.raw(sender, "&a已" + label(type) + " &f" + targetName + " &a现金 " + econ.format(amount)
|
||||||
+ " &7(当前: " + econ.format(econ.getCash(uuid)) + ")");
|
+ " &7(当前: " + econ.format(econ.getCash(uuid)) + ")");
|
||||||
});
|
});
|
||||||
@@ -130,6 +138,66 @@ public final class BankAdminCommand implements TabExecutor {
|
|||||||
msg.raw(sender, "&a已将 &f" + key + " &a的日利率设置为 &e" + rate + " &7(" + (rate * 100) + "%)");
|
msg.raw(sender, "&a已将 &f" + key + " &a的日利率设置为 &e" + rate + " &7(" + (rate * 100) + "%)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void queryLog(CommandSender sender, String[] args) {
|
||||||
|
if (args.length < 2) {
|
||||||
|
msg.raw(sender, "&c用法: /bankadmin log <玩家> [页码]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String targetName = args[1];
|
||||||
|
int page = 1;
|
||||||
|
if (args.length >= 3) {
|
||||||
|
try {
|
||||||
|
page = Math.max(1, Integer.parseInt(args[2]));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int finalPage = page;
|
||||||
|
final int pageSize = 10;
|
||||||
|
|
||||||
|
plugin.runAsync(() -> {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
|
||||||
|
UUID uuid = target.getUniqueId();
|
||||||
|
if (uuid == null || (!target.hasPlayedBefore() && !target.isOnline())) {
|
||||||
|
msg.key(sender, "player-not-found", "&c找不到目标玩家。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var list = plugin.getTransactionLog().query(uuid, finalPage, pageSize);
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
msg.raw(sender, "&7" + targetName + " 在第 " + finalPage + " 页没有交易记录。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EconomyManager econ = plugin.getEconomyManager();
|
||||||
|
java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat("MM-dd HH:mm");
|
||||||
|
msg.raw(sender, "&b&l" + targetName + " 的交易流水 &7- 第 " + finalPage + " 页");
|
||||||
|
for (var tx : list) {
|
||||||
|
String feePart = tx.getFee() > 0 ? " &8(费 " + econ.format(tx.getFee()) + ")" : "";
|
||||||
|
sender.sendMessage(com.craftbank.util.Text.color(
|
||||||
|
"&7[" + fmt.format(new java.util.Date(tx.getTimestamp())) + "] &f"
|
||||||
|
+ typeLabel(tx.getType()) + " &a" + econ.format(tx.getAmount())
|
||||||
|
+ feePart + " &8" + (tx.getDescription() == null ? "" : tx.getDescription())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String typeLabel(com.craftbank.model.Transaction.Type type) {
|
||||||
|
return switch (type) {
|
||||||
|
case PAY_OUT -> "转出";
|
||||||
|
case PAY_IN -> "转入";
|
||||||
|
case DEPOSIT -> "存活期";
|
||||||
|
case WITHDRAW -> "取活期";
|
||||||
|
case CHEQUE_ISSUE -> "开支票";
|
||||||
|
case CHEQUE_REDEEM -> "兑支票";
|
||||||
|
case TERM_CREATE -> "存定期";
|
||||||
|
case TERM_CLAIM -> "领定期";
|
||||||
|
case INTEREST -> "利息";
|
||||||
|
case ADMIN_GIVE -> "管理给予";
|
||||||
|
case ADMIN_TAKE -> "管理扣除";
|
||||||
|
case ADMIN_SET -> "管理设置";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private void sendHelp(CommandSender sender) {
|
private void sendHelp(CommandSender sender) {
|
||||||
msg.raw(sender, "&b&l工艺银行管理 &7指令帮助:");
|
msg.raw(sender, "&b&l工艺银行管理 &7指令帮助:");
|
||||||
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin reload"));
|
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin reload"));
|
||||||
@@ -137,6 +205,7 @@ public final class BankAdminCommand implements TabExecutor {
|
|||||||
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin take <玩家> <金额>"));
|
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin take <玩家> <金额>"));
|
||||||
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin set <玩家> <金额>"));
|
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin set <玩家> <金额>"));
|
||||||
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin setinterest <类型> <利率>"));
|
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin setinterest <类型> <利率>"));
|
||||||
|
sender.sendMessage(com.craftbank.util.Text.color("&7/bankadmin log <玩家> [页码]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -145,14 +214,14 @@ public final class BankAdminCommand implements TabExecutor {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
if (args.length == 1) {
|
if (args.length == 1) {
|
||||||
List<String> base = new ArrayList<>(List.of("reload", "give", "take", "set", "setinterest"));
|
List<String> base = new ArrayList<>(List.of("reload", "give", "take", "set", "setinterest", "log"));
|
||||||
base.removeIf(s -> !s.startsWith(args[0].toLowerCase()));
|
base.removeIf(s -> !s.startsWith(args[0].toLowerCase()));
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
if (args.length == 2 && args[0].equalsIgnoreCase("setinterest")) {
|
if (args.length == 2 && args[0].equalsIgnoreCase("setinterest")) {
|
||||||
return new ArrayList<>(List.of("savings", "term_7d", "term_15d", "term_30d"));
|
return new ArrayList<>(List.of("savings", "term_7d", "term_15d", "term_30d"));
|
||||||
}
|
}
|
||||||
if (args.length == 2 && List.of("give", "take", "set").contains(args[0].toLowerCase())) {
|
if (args.length == 2 && List.of("give", "take", "set", "log").contains(args[0].toLowerCase())) {
|
||||||
return null; // 在线玩家名
|
return null; // 在线玩家名
|
||||||
}
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ public final class ChequeCommand implements TabExecutor {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(),
|
||||||
|
com.craftbank.model.Transaction.Type.CHEQUE_ISSUE, amount, "开具支票");
|
||||||
msg.raw(player, "&a已开具一张面额 &f" + econ.format(amount) + " &a的支票。");
|
msg.raw(player, "&a已开具一张面额 &f" + econ.format(amount) + " &a的支票。");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,16 +71,34 @@ public final class PayCommand implements TabExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!econ.has(player.getUniqueId(), amount)) {
|
// 手续费由付款方承担:需同时覆盖转账金额与手续费。
|
||||||
msg.key(sender, "insufficient-funds", "&c余额不足。");
|
double fee = plugin.getFeeCalculator().calculate("pay", amount);
|
||||||
|
if (!econ.has(player.getUniqueId(), amount + fee)) {
|
||||||
|
if (fee > 0) {
|
||||||
|
msg.raw(sender, "&c余额不足 (需 " + econ.format(amount) + " + 手续费 " + econ.format(fee) + ")。");
|
||||||
|
} else {
|
||||||
|
msg.key(sender, "insufficient-funds", "&c余额不足。");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (econ.transfer(player.getUniqueId(), targetUuid, amount)) {
|
if (econ.transfer(player.getUniqueId(), targetUuid, amount)) {
|
||||||
msg.raw(sender, "&a成功向 &f" + targetName + " &a转账 " + econ.format(amount));
|
// 转账成功后再扣手续费(金额已校验充足)。
|
||||||
|
if (fee > 0) {
|
||||||
|
econ.withdraw(player.getUniqueId(), fee);
|
||||||
|
}
|
||||||
|
String feeNote = fee > 0 ? " &7(手续费 " + econ.format(fee) + ")" : "";
|
||||||
|
msg.raw(sender, "&a成功向 &f" + targetName + " &a转账 " + econ.format(amount) + feeNote);
|
||||||
Player onlineTarget = Bukkit.getPlayer(targetUuid);
|
Player onlineTarget = Bukkit.getPlayer(targetUuid);
|
||||||
if (onlineTarget != null) {
|
if (onlineTarget != null) {
|
||||||
msg.raw(onlineTarget, "&a你收到来自 &f" + player.getName() + " &a的转账 " + econ.format(amount));
|
msg.raw(onlineTarget, "&a你收到来自 &f" + player.getName() + " &a的转账 " + econ.format(amount));
|
||||||
}
|
}
|
||||||
|
// 记录双方流水。
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(),
|
||||||
|
com.craftbank.model.Transaction.Type.PAY_OUT, amount, fee, targetUuid,
|
||||||
|
"转账给 " + targetName);
|
||||||
|
plugin.getTransactionLog().record(targetUuid,
|
||||||
|
com.craftbank.model.Transaction.Type.PAY_IN, amount, 0, player.getUniqueId(),
|
||||||
|
"收到 " + player.getName() + " 的转账");
|
||||||
} else {
|
} else {
|
||||||
msg.key(sender, "insufficient-funds", "&c余额不足。");
|
msg.key(sender, "insufficient-funds", "&c余额不足。");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,9 +90,23 @@ public class DatabaseManager {
|
|||||||
"end_time BIGINT NOT NULL," +
|
"end_time BIGINT NOT NULL," +
|
||||||
"claimed INT NOT NULL DEFAULT 0" +
|
"claimed INT NOT NULL DEFAULT 0" +
|
||||||
")";
|
")";
|
||||||
|
String transactions = "CREATE TABLE IF NOT EXISTS craftbank_transactions (" +
|
||||||
|
"id " + autoInc + "," +
|
||||||
|
"player VARCHAR(36) NOT NULL," +
|
||||||
|
"type VARCHAR(24) NOT NULL," +
|
||||||
|
"amount DOUBLE NOT NULL," +
|
||||||
|
"fee DOUBLE NOT NULL DEFAULT 0," +
|
||||||
|
"counterparty VARCHAR(36)," +
|
||||||
|
"description VARCHAR(128)," +
|
||||||
|
"timestamp BIGINT NOT NULL" +
|
||||||
|
")";
|
||||||
try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
|
try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
|
||||||
st.executeUpdate(players);
|
st.executeUpdate(players);
|
||||||
st.executeUpdate(deposits);
|
st.executeUpdate(deposits);
|
||||||
|
st.executeUpdate(transactions);
|
||||||
|
// 按玩家 + 时间查询的索引(日志翻页常用)。
|
||||||
|
st.executeUpdate("CREATE INDEX IF NOT EXISTS idx_tx_player_time " +
|
||||||
|
"ON craftbank_transactions(player, timestamp)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,4 +283,69 @@ public class DatabaseManager {
|
|||||||
rs.getLong("end_time"),
|
rs.getLong("end_time"),
|
||||||
rs.getInt("claimed") != 0);
|
rs.getInt("claimed") != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// 交易流水
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** 写入一条交易流水。 */
|
||||||
|
public void insertTransaction(com.craftbank.model.Transaction tx) {
|
||||||
|
String sql = "INSERT INTO craftbank_transactions " +
|
||||||
|
"(player, type, amount, fee, counterparty, description, timestamp) VALUES (?,?,?,?,?,?,?)";
|
||||||
|
try (Connection conn = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, tx.getPlayer().toString());
|
||||||
|
ps.setString(2, tx.getType().name());
|
||||||
|
ps.setDouble(3, tx.getAmount());
|
||||||
|
ps.setDouble(4, tx.getFee());
|
||||||
|
ps.setString(5, tx.getCounterparty() == null ? null : tx.getCounterparty().toString());
|
||||||
|
ps.setString(6, tx.getDescription());
|
||||||
|
ps.setLong(7, tx.getTimestamp());
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "写入交易流水失败: " + tx.getPlayer(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询某玩家的交易流水(按时间倒序分页)。
|
||||||
|
*
|
||||||
|
* @param page 从 1 开始的页码
|
||||||
|
* @param pageSize 每页条数
|
||||||
|
*/
|
||||||
|
public List<com.craftbank.model.Transaction> getTransactions(UUID player, int page, int pageSize) {
|
||||||
|
List<com.craftbank.model.Transaction> list = new ArrayList<>();
|
||||||
|
int offset = Math.max(0, (page - 1) * pageSize);
|
||||||
|
String sql = "SELECT * FROM craftbank_transactions WHERE player = ? " +
|
||||||
|
"ORDER BY timestamp DESC LIMIT ? OFFSET ?";
|
||||||
|
try (Connection conn = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, player.toString());
|
||||||
|
ps.setInt(2, pageSize);
|
||||||
|
ps.setInt(3, offset);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
String cp = rs.getString("counterparty");
|
||||||
|
com.craftbank.model.Transaction.Type t;
|
||||||
|
try {
|
||||||
|
t = com.craftbank.model.Transaction.Type.valueOf(rs.getString("type"));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
continue; // 跳过未知类型(向后兼容)
|
||||||
|
}
|
||||||
|
list.add(new com.craftbank.model.Transaction(
|
||||||
|
rs.getLong("id"),
|
||||||
|
UUID.fromString(rs.getString("player")),
|
||||||
|
t,
|
||||||
|
rs.getDouble("amount"),
|
||||||
|
rs.getDouble("fee"),
|
||||||
|
cp == null ? null : UUID.fromString(cp),
|
||||||
|
rs.getString("description"),
|
||||||
|
rs.getLong("timestamp")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "读取交易流水失败: " + player, ex);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,13 +61,15 @@ public class BankGUI {
|
|||||||
inv.setItem(SLOT_TERM_LIST, icon(Material.BOOK, "&b我的定期存款",
|
inv.setItem(SLOT_TERM_LIST, icon(Material.BOOK, "&b我的定期存款",
|
||||||
List.of("&7点击查看 / 领取到期定期存款")));
|
List.of("&7点击查看 / 领取到期定期存款")));
|
||||||
|
|
||||||
inv.setItem(SLOT_DEPOSIT_ALL, icon(Material.HOPPER, "&a存入全部现金",
|
inv.setItem(SLOT_DEPOSIT_ALL, icon(Material.HOPPER, "&a存入活期",
|
||||||
List.of("&7将钱包中全部现金存入活期",
|
List.of("&7当前现金: &f" + eco.format(account.getCash()),
|
||||||
"&7当前现金: &f" + eco.format(account.getCash()))));
|
"&e左键: &f输入自定义金额",
|
||||||
|
"&e右键: &f存入全部现金")));
|
||||||
|
|
||||||
inv.setItem(SLOT_WITHDRAW_ALL, icon(Material.DROPPER, "&c取出全部活期",
|
inv.setItem(SLOT_WITHDRAW_ALL, icon(Material.DROPPER, "&c取出活期",
|
||||||
List.of("&7将活期余额全部取回钱包",
|
List.of("&7活期余额: &f" + eco.format(account.getBankSaving()),
|
||||||
"&7活期余额: &f" + eco.format(account.getBankSaving()))));
|
"&e左键: &f输入自定义金额",
|
||||||
|
"&e右键: &f取出全部活期")));
|
||||||
|
|
||||||
Map<Integer, Double> terms = plugin.getBankManager().getTermOptions();
|
Map<Integer, Double> terms = plugin.getBankManager().getTermOptions();
|
||||||
inv.setItem(SLOT_TERM_7, termIcon(eco, 7, terms.getOrDefault(7, 0.0), account.getBankSaving()));
|
inv.setItem(SLOT_TERM_7, termIcon(eco, 7, terms.getOrDefault(7, 0.0), account.getBankSaving()));
|
||||||
@@ -81,8 +83,9 @@ public class BankGUI {
|
|||||||
return icon(Material.DIAMOND, "&b定期存款 · " + days + "天",
|
return icon(Material.DIAMOND, "&b定期存款 · " + days + "天",
|
||||||
List.of("&7日利率: &a" + percent(rate),
|
List.of("&7日利率: &a" + percent(rate),
|
||||||
"&7到期利息: &a" + percent(rate * days) + " (单利)",
|
"&7到期利息: &a" + percent(rate * days) + " (单利)",
|
||||||
"&8点击将<活期全部余额>存入该定期",
|
"&7可存活期: &f" + eco.format(saving),
|
||||||
"&8可存金额: &f" + eco.format(saving),
|
"&e左键: &f输入自定义金额",
|
||||||
|
"&e右键: &f存入全部活期",
|
||||||
"&c一旦存入到期前不可取出!"));
|
"&c一旦存入到期前不可取出!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.craftbank.gui;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录玩家在 GUI 中触发的「待聊天输入金额」状态。
|
||||||
|
*
|
||||||
|
* <p>玩家点击「存入/取出/办定期」后进入对应 {@link Action} 状态,下一条聊天消息被
|
||||||
|
* 当作金额处理。状态存活有上限,由聊天监听器消费后清除。</p>
|
||||||
|
*/
|
||||||
|
public final class PendingInput {
|
||||||
|
|
||||||
|
public enum Action {
|
||||||
|
DEPOSIT, // 现金 -> 活期
|
||||||
|
WITHDRAW, // 活期 -> 现金
|
||||||
|
TERM // 办理定期(extra = 天数)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单条待输入记录。 */
|
||||||
|
public static final class Entry {
|
||||||
|
public final Action action;
|
||||||
|
public final int extra; // TERM 时为天数,其它为 0
|
||||||
|
public final long createdAt;
|
||||||
|
|
||||||
|
Entry(Action action, int extra) {
|
||||||
|
this.action = action;
|
||||||
|
this.extra = extra;
|
||||||
|
this.createdAt = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待输入状态有效期:超时后视为放弃。 */
|
||||||
|
private static final long EXPIRE_MS = 60_000L;
|
||||||
|
|
||||||
|
private final Map<UUID, Entry> pending = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public void await(UUID player, Action action, int extra) {
|
||||||
|
pending.put(player, new Entry(action, extra));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取出并移除待输入状态;不存在或已过期返回 null。 */
|
||||||
|
public Entry consume(UUID player) {
|
||||||
|
Entry e = pending.remove(player);
|
||||||
|
if (e == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (System.currentTimeMillis() - e.createdAt > EXPIRE_MS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAwaiting(UUID player) {
|
||||||
|
return pending.containsKey(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel(UUID player) {
|
||||||
|
pending.remove(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,16 +48,25 @@ public class CraftBankExpansion extends PlaceholderExpansion {
|
|||||||
if (player == null) {
|
if (player == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
// 不依赖账户的变量先处理。
|
||||||
|
if (params.equalsIgnoreCase("savings_rate")) {
|
||||||
|
return String.format("%.3f%%", plugin.getBankManager().getSavingsRate() * 100);
|
||||||
|
}
|
||||||
|
|
||||||
// 占位符在主线程被解析: 仅读取已缓存数据, 不触发阻塞式数据库查询。
|
// 占位符在主线程被解析: 仅读取已缓存数据, 不触发阻塞式数据库查询。
|
||||||
PlayerAccount account = plugin.getEconomyManager().getCached(player.getUniqueId());
|
PlayerAccount account = plugin.getEconomyManager().getCached(player.getUniqueId());
|
||||||
double cash = account != null ? account.getCash() : 0.0;
|
double cash = account != null ? account.getCash() : 0.0;
|
||||||
double saving = account != null ? account.getBankSaving() : 0.0;
|
double saving = account != null ? account.getBankSaving() : 0.0;
|
||||||
|
double total = cash + saving;
|
||||||
|
|
||||||
return switch (params.toLowerCase()) {
|
return switch (params.toLowerCase()) {
|
||||||
case "cash" -> plugin.getEconomyManager().formatNumber(cash);
|
case "cash" -> plugin.getEconomyManager().formatNumber(cash);
|
||||||
case "cash_formatted" -> plugin.getEconomyManager().format(cash);
|
case "cash_formatted" -> plugin.getEconomyManager().format(cash);
|
||||||
case "bank_saving" -> plugin.getEconomyManager().formatNumber(saving);
|
case "bank_saving" -> plugin.getEconomyManager().formatNumber(saving);
|
||||||
case "bank_saving_formatted" -> plugin.getEconomyManager().format(saving);
|
case "bank_saving_formatted" -> plugin.getEconomyManager().format(saving);
|
||||||
|
case "total" -> plugin.getEconomyManager().formatNumber(total);
|
||||||
|
case "total_formatted" -> plugin.getEconomyManager().format(total);
|
||||||
|
case "has_card" -> account != null && account.hasValidCard() ? "是" : "否";
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.craftbank.listeners;
|
||||||
|
|
||||||
|
import com.craftbank.CraftBank;
|
||||||
|
import com.craftbank.economy.EconomyManager;
|
||||||
|
import com.craftbank.gui.PendingInput;
|
||||||
|
import com.craftbank.model.PlayerAccount;
|
||||||
|
import com.craftbank.model.TermDeposit;
|
||||||
|
import com.craftbank.model.Transaction;
|
||||||
|
import com.craftbank.util.Amounts;
|
||||||
|
import io.papermc.paper.event.player.AsyncChatEvent;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.TextComponent;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 捕获处于「待输入金额」状态玩家的下一条聊天消息,将其解析为金额并执行对应的
|
||||||
|
* 存入 / 取出 / 办理定期操作(自定义金额 GUI 的输入端)。
|
||||||
|
*
|
||||||
|
* <p>聊天事件为异步触发;账户余额操作切回玩家区域线程,定期写库走异步线程。</p>
|
||||||
|
*/
|
||||||
|
public final class ChatInputListener implements Listener {
|
||||||
|
|
||||||
|
private final CraftBank plugin;
|
||||||
|
|
||||||
|
public ChatInputListener(CraftBank plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||||
|
public void onChat(AsyncChatEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
if (!plugin.getPendingInput().isAwaiting(uuid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PendingInput.Entry entry = plugin.getPendingInput().consume(uuid);
|
||||||
|
// 拦截这条聊天,不广播给其他玩家。
|
||||||
|
event.setCancelled(true);
|
||||||
|
if (entry == null) {
|
||||||
|
plugin.msg().raw(player, "&c输入已超时, 请重新在银行界面操作。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String raw = plainText(event.message()).trim();
|
||||||
|
if (raw.equalsIgnoreCase("cancel") || raw.equalsIgnoreCase("取消")) {
|
||||||
|
plugin.msg().raw(player, "&7已取消本次操作。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double amount = Amounts.parse(raw);
|
||||||
|
if (amount <= 0) {
|
||||||
|
plugin.msg().raw(player, "&c无效金额, 操作已取消。请输入正数, 例如 &f1000");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerAccount account = plugin.getEconomyManager().getCached(uuid);
|
||||||
|
if (account == null) {
|
||||||
|
plugin.msg().raw(player, "&c账户尚未加载完成, 请稍后再试。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (entry.action) {
|
||||||
|
case DEPOSIT -> plugin.runForPlayer(player, () -> doDeposit(player, account, amount));
|
||||||
|
case WITHDRAW -> plugin.runForPlayer(player, () -> doWithdraw(player, account, amount));
|
||||||
|
case TERM -> doTerm(player, account, amount, entry.extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将聊天消息 Component 递归提取为纯文本。folia-api 不打包 Adventure 的
|
||||||
|
* PlainTextComponentSerializer, 故这里手动遍历 TextComponent 内容与子节点,
|
||||||
|
* 对聊天输入 (单行数字/关键字) 已足够。
|
||||||
|
*/
|
||||||
|
private String plainText(Component component) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (component instanceof TextComponent tc) {
|
||||||
|
sb.append(tc.content());
|
||||||
|
}
|
||||||
|
for (Component child : component.children()) {
|
||||||
|
sb.append(plainText(child));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doDeposit(Player player, PlayerAccount account, double amount) {
|
||||||
|
EconomyManager econ = plugin.getEconomyManager();
|
||||||
|
synchronized (account) {
|
||||||
|
if (!account.withdrawCash(amount)) {
|
||||||
|
plugin.msg().key(player, "insufficient-funds", "&c余额不足。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
account.depositSaving(amount);
|
||||||
|
}
|
||||||
|
plugin.persistAsync(account);
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(), Transaction.Type.DEPOSIT, amount, "存入活期");
|
||||||
|
plugin.msg().raw(player, "&a已将 &e" + econ.format(amount) + " &a存入活期账户。");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doWithdraw(Player player, PlayerAccount account, double amount) {
|
||||||
|
EconomyManager econ = plugin.getEconomyManager();
|
||||||
|
// 取现手续费(若配置)。
|
||||||
|
double fee = plugin.getFeeCalculator().calculate("withdraw", amount);
|
||||||
|
synchronized (account) {
|
||||||
|
if (!account.withdrawSaving(amount)) {
|
||||||
|
plugin.msg().raw(player, "&c活期余额不足。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 实际到手 = 金额 - 手续费。
|
||||||
|
account.depositCash(amount - fee);
|
||||||
|
}
|
||||||
|
plugin.persistAsync(account);
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(), Transaction.Type.WITHDRAW, amount, fee, null, "取出活期");
|
||||||
|
String feeNote = fee > 0 ? " &7(手续费 " + econ.format(fee) + ", 到手 " + econ.format(amount - fee) + ")" : "";
|
||||||
|
plugin.msg().raw(player, "&a已从活期取回 &e" + econ.format(amount) + " &a到钱包。" + feeNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doTerm(Player player, PlayerAccount account, double amount, int days) {
|
||||||
|
EconomyManager econ = plugin.getEconomyManager();
|
||||||
|
plugin.runAsync(() -> {
|
||||||
|
TermDeposit deposit = plugin.getBankManager().createTermDeposit(account, amount, days);
|
||||||
|
plugin.runForPlayer(player, () -> {
|
||||||
|
if (deposit == null) {
|
||||||
|
plugin.msg().raw(player, "&c办理失败: 活期余额不足或期限无效。");
|
||||||
|
} else {
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(), Transaction.Type.TERM_CREATE,
|
||||||
|
deposit.getPrincipal(), days + "天定期");
|
||||||
|
plugin.msg().raw(player, "&a成功存入 " + days + "天定期: &e"
|
||||||
|
+ econ.format(deposit.getPrincipal())
|
||||||
|
+ "&a, 到期本息 &e" + econ.format(deposit.calculateTotalPayout()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,6 +104,8 @@ public class InteractListener implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugin.getEconomyManager().deposit(player.getUniqueId(), amount);
|
plugin.getEconomyManager().deposit(player.getUniqueId(), amount);
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(),
|
||||||
|
com.craftbank.model.Transaction.Type.CHEQUE_REDEEM, amount, "兑现支票");
|
||||||
plugin.msg().raw(player, "&a成功兑现支票 &e" + plugin.getEconomyManager().format(amount)
|
plugin.msg().raw(player, "&a成功兑现支票 &e" + plugin.getEconomyManager().format(amount)
|
||||||
+ " &a到你的钱包! (开票人: " + plugin.getChequeManager().getIssuerName(current) + ")");
|
+ " &a到你的钱包! (开票人: " + plugin.getChequeManager().getIssuerName(current) + ")");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,50 +60,87 @@ public class InventoryListener implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (gui.getType() == BankGuiHolder.Type.MAIN) {
|
if (gui.getType() == BankGuiHolder.Type.MAIN) {
|
||||||
handleMain(player, account, event.getRawSlot());
|
handleMain(player, account, event.getRawSlot(), event.isRightClick());
|
||||||
} else if (gui.getType() == BankGuiHolder.Type.TERM_LIST) {
|
} else if (gui.getType() == BankGuiHolder.Type.TERM_LIST) {
|
||||||
handleTermList(player, account, event.getCurrentItem(), event.getRawSlot(), top.getSize());
|
handleTermList(player, account, event.getCurrentItem(), event.getRawSlot(), top.getSize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMain(Player player, PlayerAccount account, int slot) {
|
private void handleMain(Player player, PlayerAccount account, int slot, boolean rightClick) {
|
||||||
switch (slot) {
|
switch (slot) {
|
||||||
case BankGUI.SLOT_DEPOSIT_ALL -> {
|
case BankGUI.SLOT_DEPOSIT_ALL -> {
|
||||||
double cash = account.getCash();
|
if (rightClick) {
|
||||||
if (cash <= 0) {
|
depositAll(player, account);
|
||||||
plugin.msg().raw(player, "&c你的钱包里没有现金可以存入。");
|
} else {
|
||||||
return;
|
promptInput(player, com.craftbank.gui.PendingInput.Action.DEPOSIT, 0, "存入活期");
|
||||||
}
|
}
|
||||||
if (account.withdrawCash(cash)) {
|
|
||||||
account.depositSaving(cash);
|
|
||||||
plugin.persistAsync(account);
|
|
||||||
plugin.msg().raw(player, "&a已将 &e" + plugin.getEconomyManager().format(cash) + " &a存入活期。");
|
|
||||||
}
|
|
||||||
plugin.getBankGUI().openMain(player, account);
|
|
||||||
}
|
}
|
||||||
case BankGUI.SLOT_WITHDRAW_ALL -> {
|
case BankGUI.SLOT_WITHDRAW_ALL -> {
|
||||||
double saving = account.getBankSaving();
|
if (rightClick) {
|
||||||
if (saving <= 0) {
|
withdrawAll(player, account);
|
||||||
plugin.msg().raw(player, "&c你的活期账户里没有余额可以取出。");
|
} else {
|
||||||
return;
|
promptInput(player, com.craftbank.gui.PendingInput.Action.WITHDRAW, 0, "取出活期");
|
||||||
}
|
}
|
||||||
if (account.withdrawSaving(saving)) {
|
|
||||||
account.depositCash(saving);
|
|
||||||
plugin.persistAsync(account);
|
|
||||||
plugin.msg().raw(player, "&a已从活期取回 &e" + plugin.getEconomyManager().format(saving) + " &a到钱包。");
|
|
||||||
}
|
|
||||||
plugin.getBankGUI().openMain(player, account);
|
|
||||||
}
|
}
|
||||||
case BankGUI.SLOT_TERM_LIST -> openTermList(player, account);
|
case BankGUI.SLOT_TERM_LIST -> openTermList(player, account);
|
||||||
case BankGUI.SLOT_TERM_7 -> createTerm(player, account, 7);
|
case BankGUI.SLOT_TERM_7 -> termSlot(player, account, 7, rightClick);
|
||||||
case BankGUI.SLOT_TERM_15 -> createTerm(player, account, 15);
|
case BankGUI.SLOT_TERM_15 -> termSlot(player, account, 15, rightClick);
|
||||||
case BankGUI.SLOT_TERM_30 -> createTerm(player, account, 30);
|
case BankGUI.SLOT_TERM_30 -> termSlot(player, account, 30, rightClick);
|
||||||
default -> {
|
default -> {
|
||||||
// 其它槽位 (信息/背景) 无操作。
|
// 其它槽位 (信息/背景) 无操作。
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 进入聊天输入态,让玩家自定义金额。 */
|
||||||
|
private void promptInput(Player player, com.craftbank.gui.PendingInput.Action action, int extra, String label) {
|
||||||
|
player.closeInventory();
|
||||||
|
plugin.getPendingInput().await(player.getUniqueId(), action, extra);
|
||||||
|
plugin.msg().raw(player, "&e请在聊天框输入要" + label + "的金额 &7(输入 &fcancel &7取消, 60 秒内有效)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void termSlot(Player player, PlayerAccount account, int days, boolean rightClick) {
|
||||||
|
if (rightClick) {
|
||||||
|
createTerm(player, account, days); // 右键:用全部活期办定期(旧行为)
|
||||||
|
} else {
|
||||||
|
promptInput(player, com.craftbank.gui.PendingInput.Action.TERM, days, days + "天定期存入");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void depositAll(Player player, PlayerAccount account) {
|
||||||
|
double cash = account.getCash();
|
||||||
|
if (cash <= 0) {
|
||||||
|
plugin.msg().raw(player, "&c你的钱包里没有现金可以存入。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (account.withdrawCash(cash)) {
|
||||||
|
account.depositSaving(cash);
|
||||||
|
plugin.persistAsync(account);
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(),
|
||||||
|
com.craftbank.model.Transaction.Type.DEPOSIT, cash, "存入活期(全部)");
|
||||||
|
plugin.msg().raw(player, "&a已将 &e" + plugin.getEconomyManager().format(cash) + " &a存入活期。");
|
||||||
|
}
|
||||||
|
plugin.getBankGUI().openMain(player, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withdrawAll(Player player, PlayerAccount account) {
|
||||||
|
double saving = account.getBankSaving();
|
||||||
|
if (saving <= 0) {
|
||||||
|
plugin.msg().raw(player, "&c你的活期账户里没有余额可以取出。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double fee = plugin.getFeeCalculator().calculate("withdraw", saving);
|
||||||
|
if (account.withdrawSaving(saving)) {
|
||||||
|
account.depositCash(saving - fee);
|
||||||
|
plugin.persistAsync(account);
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(),
|
||||||
|
com.craftbank.model.Transaction.Type.WITHDRAW, saving, fee, null, "取出活期(全部)");
|
||||||
|
String feeNote = fee > 0 ? " &7(手续费 " + plugin.getEconomyManager().format(fee) + ")" : "";
|
||||||
|
plugin.msg().raw(player, "&a已从活期取回 &e" + plugin.getEconomyManager().format(saving) + " &a到钱包。" + feeNote);
|
||||||
|
}
|
||||||
|
plugin.getBankGUI().openMain(player, account);
|
||||||
|
}
|
||||||
|
|
||||||
private void createTerm(Player player, PlayerAccount account, int days) {
|
private void createTerm(Player player, PlayerAccount account, int days) {
|
||||||
double amount = account.getBankSaving();
|
double amount = account.getBankSaving();
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
@@ -165,6 +202,8 @@ public class InventoryListener implements Listener {
|
|||||||
if (payout < 0) {
|
if (payout < 0) {
|
||||||
plugin.msg().raw(player, "&c该定期存款尚未到期, 无法提前支取!");
|
plugin.msg().raw(player, "&c该定期存款尚未到期, 无法提前支取!");
|
||||||
} else {
|
} else {
|
||||||
|
plugin.getTransactionLog().record(player.getUniqueId(),
|
||||||
|
com.craftbank.model.Transaction.Type.TERM_CLAIM, payout, "领取定期本息");
|
||||||
plugin.msg().raw(player, "&a已领取定期本息 &e" + plugin.getEconomyManager().format(payout) + " &a到活期账户。");
|
plugin.msg().raw(player, "&a已领取定期本息 &e" + plugin.getEconomyManager().format(payout) + " &a到活期账户。");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.craftbank.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一条交易流水记录,用于审计与防刷追溯。
|
||||||
|
*
|
||||||
|
* <p>{@link #counterparty} 为对手方(转账的另一方、支票开票人等),无对手方时为 null。
|
||||||
|
* {@link #fee} 为本次交易产生的手续费(无则为 0)。</p>
|
||||||
|
*/
|
||||||
|
public class Transaction {
|
||||||
|
|
||||||
|
/** 交易类型。 */
|
||||||
|
public enum Type {
|
||||||
|
PAY_OUT, // 转出(/pay 付款方)
|
||||||
|
PAY_IN, // 转入(/pay 收款方)
|
||||||
|
DEPOSIT, // 现金 -> 活期
|
||||||
|
WITHDRAW, // 活期 -> 现金
|
||||||
|
CHEQUE_ISSUE, // 开具支票
|
||||||
|
CHEQUE_REDEEM, // 兑现支票
|
||||||
|
TERM_CREATE, // 创建定期
|
||||||
|
TERM_CLAIM, // 领取定期本息
|
||||||
|
INTEREST, // 活期利息结算
|
||||||
|
ADMIN_GIVE, // 管理员给予
|
||||||
|
ADMIN_TAKE, // 管理员扣除
|
||||||
|
ADMIN_SET // 管理员设置
|
||||||
|
}
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
private final UUID player;
|
||||||
|
private final Type type;
|
||||||
|
private final double amount;
|
||||||
|
private final double fee;
|
||||||
|
private final UUID counterparty;
|
||||||
|
private final String description;
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
public Transaction(long id, UUID player, Type type, double amount, double fee,
|
||||||
|
UUID counterparty, String description, long timestamp) {
|
||||||
|
this.id = id;
|
||||||
|
this.player = player;
|
||||||
|
this.type = type;
|
||||||
|
this.amount = amount;
|
||||||
|
this.fee = fee;
|
||||||
|
this.counterparty = counterparty;
|
||||||
|
this.description = description;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() { return id; }
|
||||||
|
public UUID getPlayer() { return player; }
|
||||||
|
public Type getType() { return type; }
|
||||||
|
public double getAmount() { return amount; }
|
||||||
|
public double getFee() { return fee; }
|
||||||
|
public UUID getCounterparty() { return counterparty; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public long getTimestamp() { return timestamp; }
|
||||||
|
}
|
||||||
@@ -51,6 +51,26 @@ Bank_Settings:
|
|||||||
# 利息结算最大补偿天数 (防止离线过久产生天文数字利息)
|
# 利息结算最大补偿天数 (防止离线过久产生天文数字利息)
|
||||||
max_offline_interest_days: 30
|
max_offline_interest_days: 30
|
||||||
|
|
||||||
|
# 手续费设置 (手续费 = fixed + 金额 × percent, 受 min/max 约束; 全为 0 即不收费)
|
||||||
|
Fees:
|
||||||
|
# 转账手续费 (由付款方承担)
|
||||||
|
pay:
|
||||||
|
percent: 0.0 # 百分比, 如 0.01 = 1%
|
||||||
|
fixed: 0.0 # 固定费用
|
||||||
|
min: 0.0 # 最低手续费
|
||||||
|
max: 1000000.0 # 最高手续费
|
||||||
|
# 活期取现手续费 (从取出金额中扣除)
|
||||||
|
withdraw:
|
||||||
|
percent: 0.0
|
||||||
|
fixed: 0.0
|
||||||
|
min: 0.0
|
||||||
|
max: 1000000.0
|
||||||
|
|
||||||
|
# 交易流水记录
|
||||||
|
Transaction_Log:
|
||||||
|
# 是否记录交易流水 (转账/存取/支票/定期/管理操作)
|
||||||
|
enabled: true
|
||||||
|
|
||||||
# 自定义物品设置
|
# 自定义物品设置
|
||||||
Items:
|
Items:
|
||||||
# 银行卡
|
# 银行卡
|
||||||
|
|||||||
Reference in New Issue
Block a user