初始化项目
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
target/
|
||||||
|
out/
|
||||||
|
lib/
|
||||||
|
libs/
|
||||||
|
.vscode/
|
||||||
|
.codex/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
222
README.md
Normal file
222
README.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# AuroraContainerLog
|
||||||
|
|
||||||
|
AuroraContainerLog 是一个面向 Spigot 1.12.2 的箱子容器日志插件,用于记录指定世界内玩家对箱子类容器的物品放入和取出行为,适合地皮服排查箱子物品被盗问题。
|
||||||
|
|
||||||
|
当前插件版本:`1.1.5`
|
||||||
|
|
||||||
|
## 1. 功能概览
|
||||||
|
|
||||||
|
- 支持指定世界监听箱子容器。
|
||||||
|
- 支持普通箱子、陷阱箱、大箱子记录与查询。
|
||||||
|
- 支持记录玩家名称和 UUID。
|
||||||
|
- 支持记录容器坐标:`world/x/y/z`。
|
||||||
|
- 支持记录动作:`TAKE`、`PUT`。
|
||||||
|
- 支持记录物品类型、数量、展示名、lore、附魔和完整 ItemStack Base64。
|
||||||
|
- 支持 SQLite 存储。
|
||||||
|
- 支持自动清理超过指定保留天数的历史数据。
|
||||||
|
- 支持按玩家、指定箱子、附近范围分页查询。
|
||||||
|
- 数据库写入和查询通过异步队列执行,避免阻塞主线程。
|
||||||
|
|
||||||
|
## 2. 运行环境
|
||||||
|
|
||||||
|
```text
|
||||||
|
Java: 8
|
||||||
|
服务端: Spigot 1.12.2
|
||||||
|
构建工具: Maven
|
||||||
|
数据库: SQLite
|
||||||
|
```
|
||||||
|
|
||||||
|
运行依赖:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HikariCP
|
||||||
|
sqlite-jdbc
|
||||||
|
```
|
||||||
|
|
||||||
|
插件 JAR 不内置 `HikariCP` 和 `sqlite-jdbc`。运行环境必须通过服务端公共依赖、启动 classpath、依赖插件或统一依赖加载方案提供这两个依赖。
|
||||||
|
|
||||||
|
## 3. 安装方式
|
||||||
|
|
||||||
|
构建插件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn package
|
||||||
|
```
|
||||||
|
|
||||||
|
构建完成后,将生成的插件包放入服务端:
|
||||||
|
|
||||||
|
```text
|
||||||
|
plugins/AuroraContainerLog-1.1.5.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
启动服务端后,插件会生成默认配置文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
plugins/AuroraContainerLog/config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 配置文件
|
||||||
|
|
||||||
|
默认配置:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
listen-worlds:
|
||||||
|
- world
|
||||||
|
|
||||||
|
query:
|
||||||
|
page-size: 8
|
||||||
|
chest-recent-days: 30
|
||||||
|
max-near-radius: 50
|
||||||
|
default-page: 1
|
||||||
|
|
||||||
|
database:
|
||||||
|
file: containerlog.db
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
retention-days: 60
|
||||||
|
interval-hours: 24
|
||||||
|
|
||||||
|
message-prefix: "&8[&bACL&8] &7"
|
||||||
|
```
|
||||||
|
|
||||||
|
配置说明:
|
||||||
|
|
||||||
|
- `listen-worlds`:启用箱子日志监听的世界列表。
|
||||||
|
- `query.page-size`:每页显示的日志数量。
|
||||||
|
- `query.chest-recent-days`:`/acl chest` 默认查询最近多少天的记录。
|
||||||
|
- `query.max-near-radius`:`/acl near` 允许的最大查询半径。
|
||||||
|
- `query.default-page`:未填写页码时使用的默认页。
|
||||||
|
- `database.file`:SQLite 数据库文件名。
|
||||||
|
- `cleanup.enabled`:是否启用过期日志自动清理。
|
||||||
|
- `cleanup.retention-days`:日志保留天数,超过后自动清理。
|
||||||
|
- `cleanup.interval-hours`:自动清理执行间隔。
|
||||||
|
- `message-prefix`:命令消息前缀。
|
||||||
|
|
||||||
|
## 5. 记录内容
|
||||||
|
|
||||||
|
每条日志会记录以下字段:
|
||||||
|
|
||||||
|
```text
|
||||||
|
时间
|
||||||
|
世界
|
||||||
|
容器坐标 x/y/z
|
||||||
|
玩家名称
|
||||||
|
玩家 UUID
|
||||||
|
动作 TAKE / PUT
|
||||||
|
物品 material
|
||||||
|
物品 amount
|
||||||
|
物品 displayName
|
||||||
|
物品 lore
|
||||||
|
物品 enchantments
|
||||||
|
完整 ItemStack Base64
|
||||||
|
```
|
||||||
|
|
||||||
|
插件采用打开容器时快照、关闭容器时对比差异的方式记录变化,只记录箱子容器库存变化,不记录玩家背包自身变化。
|
||||||
|
|
||||||
|
## 6. 命令
|
||||||
|
|
||||||
|
管理命令需要权限:
|
||||||
|
|
||||||
|
```text
|
||||||
|
auroracontainerlog.admin
|
||||||
|
```
|
||||||
|
|
||||||
|
重载配置:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/acl reload
|
||||||
|
```
|
||||||
|
|
||||||
|
查询指定玩家最近记录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/acl player <玩家名> [天数] [页码]
|
||||||
|
```
|
||||||
|
|
||||||
|
查询当前看着的箱子最近记录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/acl chest [页码]
|
||||||
|
```
|
||||||
|
|
||||||
|
查询附近箱子记录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/acl near <半径> [天数] [页码]
|
||||||
|
```
|
||||||
|
|
||||||
|
命令别名:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/aclog
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 权限
|
||||||
|
|
||||||
|
```text
|
||||||
|
auroracontainerlog.admin
|
||||||
|
```
|
||||||
|
|
||||||
|
默认权限:
|
||||||
|
|
||||||
|
```text
|
||||||
|
op
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 数据库说明
|
||||||
|
|
||||||
|
当前版本仅支持 SQLite。
|
||||||
|
|
||||||
|
SQLite 使用 HikariCP 单连接池:
|
||||||
|
|
||||||
|
```text
|
||||||
|
maximumPoolSize: 1
|
||||||
|
minimumIdle: 1
|
||||||
|
connectionTimeout: 10000
|
||||||
|
validationTimeout: 3000
|
||||||
|
idleTimeout: 600000
|
||||||
|
maxLifetime: 1700000
|
||||||
|
```
|
||||||
|
|
||||||
|
连接池大小固定为 1,避免 SQLite 多连接写入导致数据库锁竞争。
|
||||||
|
|
||||||
|
## 9. 自动清理规则
|
||||||
|
|
||||||
|
插件启动后会排队执行一次过期数据清理,之后按 `cleanup.interval-hours` 周期执行。
|
||||||
|
|
||||||
|
默认规则:
|
||||||
|
|
||||||
|
```text
|
||||||
|
保留最近 60 天日志
|
||||||
|
每 24 小时清理一次
|
||||||
|
```
|
||||||
|
|
||||||
|
清理操作进入数据库异步执行队列,不在主线程直接执行 SQL。
|
||||||
|
|
||||||
|
## 10. 查询行为
|
||||||
|
|
||||||
|
- `/acl chest` 会查询玩家准星指向的箱子。
|
||||||
|
- 大箱子查询会同时匹配相邻两侧箱子坐标。
|
||||||
|
- 玩家执行查询时,结果支持点击传送到记录坐标。
|
||||||
|
- 控制台查询会显示完整坐标文本。
|
||||||
|
- 查询结果按时间倒序分页显示。
|
||||||
|
|
||||||
|
## 11. 使用流程
|
||||||
|
|
||||||
|
1. 确认服务端运行环境已提供 `HikariCP` 和 `sqlite-jdbc`。
|
||||||
|
2. 将插件 JAR 放入服务端 `plugins` 目录。
|
||||||
|
3. 启动服务端生成默认配置。
|
||||||
|
4. 修改 `listen-worlds`,填入需要监听的世界。
|
||||||
|
5. 按需调整分页、自动清理和数据库文件配置。
|
||||||
|
6. 执行 `/acl reload` 重载配置。
|
||||||
|
7. 使用 `/acl chest`、`/acl player`、`/acl near` 查询日志。
|
||||||
|
|
||||||
|
## 12. 注意事项
|
||||||
|
|
||||||
|
- 插件包不包含 `sqlite-jdbc` 和 `HikariCP`,缺少运行依赖会导致 SQLite 连接池初始化失败。
|
||||||
|
- 修改监听世界后,只影响后续容器操作记录,不会修改历史数据。
|
||||||
|
- 关闭容器时只记录最终净变化,不记录打开期间每一次中间点击动作。
|
||||||
|
- 若玩家取出物品后又在关闭前放回相同数量,最终差异为 0,不会写入日志。
|
||||||
|
- 发布新版本前应确认 `plugin.yml` 中版本号已递增。
|
||||||
|
- 上线前建议在测试服验证普通点击、shift-click、数字键、拖拽、大箱子和未监听世界场景。
|
||||||
58
pom.xml
Normal file
58
pom.xml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.yaohun</groupId>
|
||||||
|
<artifactId>AuroraContainerLog</artifactId>
|
||||||
|
<version>1.1.5</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>public-rpg</id>
|
||||||
|
<url>https://repo.aurora-pixels.com/repository/public-rpg/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>spigot-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>1.12.2-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
<version>4.0.3</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>AuroraContainerLog-${project.version}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>8</source>
|
||||||
|
<target>8</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
110
src/main/java/com/yaohun/containerlog/CLMain.java
Normal file
110
src/main/java/com/yaohun/containerlog/CLMain.java
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package com.yaohun.containerlog;
|
||||||
|
|
||||||
|
import com.yaohun.containerlog.command.AclCommand;
|
||||||
|
import com.yaohun.containerlog.config.PluginConfig;
|
||||||
|
import com.yaohun.containerlog.database.DatabaseManager;
|
||||||
|
import com.yaohun.containerlog.listener.ContainerListener;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.command.PluginCommand;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
public class CLMain extends JavaPlugin {
|
||||||
|
|
||||||
|
private static CLMain instance;
|
||||||
|
private PluginConfig pluginConfig;
|
||||||
|
private DatabaseManager databaseManager;
|
||||||
|
private ContainerListener containerListener;
|
||||||
|
private BukkitTask cleanupTask;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
instance = this;
|
||||||
|
saveDefaultConfig();
|
||||||
|
|
||||||
|
Bukkit.getConsoleSender().sendMessage("§f[§6!§f] §aAuroraContainerLog §f开始加载");
|
||||||
|
pluginConfig = new PluginConfig(this);
|
||||||
|
pluginConfig.load();
|
||||||
|
|
||||||
|
databaseManager = new DatabaseManager(this);
|
||||||
|
databaseManager.start();
|
||||||
|
startCleanupTask();
|
||||||
|
|
||||||
|
containerListener = new ContainerListener(this);
|
||||||
|
getServer().getPluginManager().registerEvents(containerListener, this);
|
||||||
|
|
||||||
|
AclCommand aclCommand = new AclCommand(this);
|
||||||
|
PluginCommand command = getCommand("acl");
|
||||||
|
if (command != null) {
|
||||||
|
command.setExecutor(aclCommand);
|
||||||
|
command.setTabCompleter(aclCommand);
|
||||||
|
}
|
||||||
|
Bukkit.getConsoleSender().sendMessage("§f[§6!§f] §aAuroraContainerLog §f加载完成,祝你使用愉快!");
|
||||||
|
Bukkit.getConsoleSender().sendMessage("§f[§6!§f] §bBy.极光像素工作室·妖魂");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
HandlerList.unregisterAll(this);
|
||||||
|
if (containerListener != null) {
|
||||||
|
containerListener.clearSessions();
|
||||||
|
}
|
||||||
|
stopCleanupTask();
|
||||||
|
if (databaseManager != null) {
|
||||||
|
databaseManager.stop();
|
||||||
|
}
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CLMain inst() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginConfig getPluginConfig() {
|
||||||
|
return pluginConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseManager getDatabaseManager() {
|
||||||
|
return databaseManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerListener getContainerListener() {
|
||||||
|
return containerListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadPlugin() {
|
||||||
|
reloadConfig();
|
||||||
|
pluginConfig.load();
|
||||||
|
if (containerListener != null) {
|
||||||
|
containerListener.clearSessions();
|
||||||
|
}
|
||||||
|
if (databaseManager != null) {
|
||||||
|
databaseManager.reload();
|
||||||
|
}
|
||||||
|
startCleanupTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCleanupTask() {
|
||||||
|
stopCleanupTask();
|
||||||
|
if (!pluginConfig.isCleanupEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseManager.cleanupExpiredRecords();
|
||||||
|
long intervalTicks = pluginConfig.getCleanupIntervalHours() * 60L * 60L * 20L;
|
||||||
|
cleanupTask = getServer().getScheduler().runTaskTimer(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
databaseManager.cleanupExpiredRecords();
|
||||||
|
}
|
||||||
|
}, intervalTicks, intervalTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopCleanupTask() {
|
||||||
|
if (cleanupTask != null) {
|
||||||
|
cleanupTask.cancel();
|
||||||
|
cleanupTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
283
src/main/java/com/yaohun/containerlog/command/AclCommand.java
Normal file
283
src/main/java/com/yaohun/containerlog/command/AclCommand.java
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package com.yaohun.containerlog.command;
|
||||||
|
|
||||||
|
import com.yaohun.containerlog.CLMain;
|
||||||
|
import com.yaohun.containerlog.database.QueryCallback;
|
||||||
|
import com.yaohun.containerlog.model.LogRecord;
|
||||||
|
import com.yaohun.containerlog.model.PageResult;
|
||||||
|
import com.yaohun.containerlog.util.ChestLocationResolver;
|
||||||
|
import com.yaohun.containerlog.util.TimeUtil;
|
||||||
|
import net.md_5.bungee.api.chat.ClickEvent;
|
||||||
|
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||||
|
import net.md_5.bungee.api.chat.HoverEvent;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
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.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class AclCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
private static final String PERMISSION = "auroracontainerlog.admin";
|
||||||
|
|
||||||
|
private final CLMain plugin;
|
||||||
|
|
||||||
|
public AclCommand(CLMain plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (args.length >= 1 && "chest".equalsIgnoreCase(args[0])) {
|
||||||
|
handleChest(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!sender.hasPermission(PERMISSION)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length == 0) {
|
||||||
|
sendUsage(sender, label);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String subCommand = args[0].toLowerCase();
|
||||||
|
if ("reload".equals(subCommand)) {
|
||||||
|
handleReload(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("player".equals(subCommand)) {
|
||||||
|
handlePlayer(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("near".equals(subCommand)) {
|
||||||
|
handleNear(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendUsage(sender, label);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||||
|
if (!sender.hasPermission(PERMISSION)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
if (args.length == 1) {
|
||||||
|
return filter(Arrays.asList("reload", "player", "chest", "near"), args[0]);
|
||||||
|
}
|
||||||
|
if (args.length == 2) {
|
||||||
|
return Bukkit.getOnlinePlayers().stream()
|
||||||
|
.map(Player::getName)
|
||||||
|
.filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleReload(CommandSender sender) {
|
||||||
|
plugin.reloadPlugin();
|
||||||
|
sender.sendMessage("§f[§c箱子查询§f] §a配置文件已重载.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlayer(final CommandSender sender, String[] args) {
|
||||||
|
String prefix = "§f[§c玩家查询§f] §a";
|
||||||
|
if (args.length < 2) {
|
||||||
|
sender.sendMessage(prefix+"用法: §e/acl player [玩家名] <天数>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String playerName = args[1];
|
||||||
|
Integer days = 1;
|
||||||
|
if(args.length >= 3){
|
||||||
|
days = parsePositiveInt(args[2]);
|
||||||
|
if (days == null) {
|
||||||
|
sender.sendMessage(prefix+"天数必须是整数.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int page = parsePage(args, 3);
|
||||||
|
long since = TimeUtil.daysAgoMillis(days);
|
||||||
|
sender.sendMessage(prefix+"正在查询 §e"+playerName+" §a最近 §e"+days+"天 §a的记录...");
|
||||||
|
plugin.getDatabaseManager().queryByPlayer(playerName, since, page, plugin.getPluginConfig().getPageSize(), new QueryCallback() {
|
||||||
|
@Override
|
||||||
|
public void onComplete(PageResult result) {
|
||||||
|
displayResult(sender, "玩家记录", result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleChest(final CommandSender sender, String[] args) {
|
||||||
|
if (!(sender instanceof Player)) {
|
||||||
|
sender.sendMessage("§c该命令只能由玩家执行");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String prefix = "§f[§c箱子查询§f] §a";
|
||||||
|
Player player = (Player) sender;
|
||||||
|
Block block = getTargetBlock(player);
|
||||||
|
if (!ChestLocationResolver.isChestBlock(block)) {
|
||||||
|
sender.sendMessage(prefix+"请看着一个箱子后执行该命令.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!plugin.getPluginConfig().isWorldEnabled(block.getWorld().getName())) {
|
||||||
|
sender.sendMessage(prefix+"当前世界无法使用该命令查询.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int page = parsePage(args, 1);
|
||||||
|
long since = TimeUtil.daysAgoMillis(plugin.getPluginConfig().getChestRecentDays());
|
||||||
|
List<Location> locations = ChestLocationResolver.resolveLinkedChestLocations(block);
|
||||||
|
|
||||||
|
sender.sendMessage(prefix+"正在查询该箱子最近的记录...");
|
||||||
|
plugin.getDatabaseManager().queryByLocations(locations, since, page, plugin.getPluginConfig().getPageSize(), new QueryCallback() {
|
||||||
|
@Override
|
||||||
|
public void onComplete(PageResult result) {
|
||||||
|
displayResult(sender, "箱子记录", result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleNear(final CommandSender sender, String[] args) {
|
||||||
|
String prefix = "§f[§c箱子查询§f] §a";
|
||||||
|
if (!(sender instanceof Player)) {
|
||||||
|
sender.sendMessage("§c该命令只能由玩家执行.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length < 2) {
|
||||||
|
sender.sendMessage(prefix+"用法: §e/acl near [半径] <天数>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Integer radius = parsePositiveInt(args[1]);
|
||||||
|
if (radius == null) {
|
||||||
|
sender.sendMessage(prefix+"半径必须是正整数.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Integer days = 1;
|
||||||
|
if(args.length >= 3){
|
||||||
|
days = parsePositiveInt(args[2]);
|
||||||
|
if (days == null) {
|
||||||
|
sender.sendMessage(prefix+"天数必须是整数.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int maxRadius = plugin.getPluginConfig().getMaxNearRadius();
|
||||||
|
if (radius > maxRadius) {
|
||||||
|
sender.sendMessage(prefix+"查询半径不能超过 §e" + maxRadius+"格");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = (Player) sender;
|
||||||
|
if (!plugin.getPluginConfig().isWorldEnabled(player.getWorld().getName())) {
|
||||||
|
sender.sendMessage(prefix+"当前世界无法使用该命令查询.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int page = parsePage(args, 3);
|
||||||
|
long since = TimeUtil.daysAgoMillis(days);
|
||||||
|
sender.sendMessage(prefix+"正在查询附近 §e"+radius+"格 §a最近 §e"+days+"天 §a的记录...");
|
||||||
|
plugin.getDatabaseManager().queryNear(player.getLocation(), radius, since, page, plugin.getPluginConfig().getPageSize(), new QueryCallback() {
|
||||||
|
@Override
|
||||||
|
public void onComplete(PageResult result) {
|
||||||
|
displayResult(sender, "附近记录", result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayResult(CommandSender sender, String title, PageResult result) {
|
||||||
|
sender.sendMessage("§8§m-------------------------------------------------------------");
|
||||||
|
sender.sendMessage("§b" + title + " §7共 §f" + result.getTotal() + " §7条,第 §f" + result.getPage() + "§7/§f" + result.getTotalPages() + " §7页");
|
||||||
|
if (result.getRecords().isEmpty()) {
|
||||||
|
sender.sendMessage("§c暂时还没有任何数据产生");
|
||||||
|
} else {
|
||||||
|
if(sender instanceof Player){
|
||||||
|
Player player = (Player) sender;
|
||||||
|
for (LogRecord record : result.getRecords()) {
|
||||||
|
sendClickMessage(player,formatRecord(record,true),"tp "+record.getX()+" "+record.getY()+" "+record.getZ());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (LogRecord record : result.getRecords()) {
|
||||||
|
sender.sendMessage(formatRecord(record,false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sender.sendMessage("§8§m-------------------------------------------------------------");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatRecord(LogRecord record,boolean hide) {
|
||||||
|
String actionColor = "PUT".equals(record.getAction().name()) ? "§a" : "§c";
|
||||||
|
String itemName = record.getDisplayName() == null || record.getDisplayName().isEmpty() ? record.getMaterial() : record.getDisplayName();
|
||||||
|
if(hide){
|
||||||
|
return "§7#" + record.getId() +
|
||||||
|
" §f" + TimeUtil.format(record.getCreatedAt()) +
|
||||||
|
" " + actionColor + record.getAction().getName() +
|
||||||
|
" §e" + record.getPlayerName() +
|
||||||
|
" §7" + itemName + " §fx" + record.getAmount();
|
||||||
|
}
|
||||||
|
return "§7#" + record.getId() +
|
||||||
|
" §f" + TimeUtil.format(record.getCreatedAt()) +
|
||||||
|
" " + actionColor + record.getAction().getName() +
|
||||||
|
" §e" + record.getPlayerName() +
|
||||||
|
" §7" + itemName + " §fx" + record.getAmount() +
|
||||||
|
" §8@ §7" + record.getWorld() + "/" + record.getX() + "/" + record.getY() + "/" + record.getZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendUsage(CommandSender sender, String label) {
|
||||||
|
sender.sendMessage("");
|
||||||
|
sender.sendMessage("§e------- ======= §6箱子数据查询 §e======= -------");
|
||||||
|
sender.sendMessage("§2/"+label+" chest §e[页码] §f- §2查看箱子数据");
|
||||||
|
sender.sendMessage("§2/"+label+" player §e[玩家] §b<天数> §f- §2查询玩家数据");
|
||||||
|
sender.sendMessage("§2/"+label+" near §e[半径] §b<天数> §f- §2查询周围箱子");
|
||||||
|
sender.sendMessage("§2/"+label+" reload §f- §2重载配置文件");
|
||||||
|
sender.sendMessage("§e------- ======= §6箱子数据查询 §e======= -------");
|
||||||
|
sender.sendMessage("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parsePage(String[] args, int index) {
|
||||||
|
if (args.length <= index) {
|
||||||
|
return plugin.getPluginConfig().getDefaultPage();
|
||||||
|
}
|
||||||
|
Integer page = parsePositiveInt(args[index]);
|
||||||
|
return page == null ? plugin.getPluginConfig().getDefaultPage() : page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer parsePositiveInt(String value) {
|
||||||
|
try {
|
||||||
|
int parsed = Integer.parseInt(value);
|
||||||
|
return parsed > 0 ? parsed : null;
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Block getTargetBlock(Player player) {
|
||||||
|
return player.getTargetBlock((Set<Material>) null, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> filter(List<String> source, String prefix) {
|
||||||
|
List<String> result = new ArrayList<String>();
|
||||||
|
String lowerPrefix = prefix == null ? "" : prefix.toLowerCase();
|
||||||
|
for (String value : source) {
|
||||||
|
if (value.startsWith(lowerPrefix)) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendClickMessage(Player player, String message,String command){
|
||||||
|
TextComponent tomessage = new TextComponent(message);
|
||||||
|
tomessage.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/"+command));
|
||||||
|
tomessage.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("/"+command).create()));
|
||||||
|
player.spigot().sendMessage(tomessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.yaohun.containerlog.config;
|
||||||
|
|
||||||
|
import com.yaohun.containerlog.CLMain;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class PluginConfig {
|
||||||
|
|
||||||
|
private final CLMain plugin;
|
||||||
|
private final Set<String> listenWorlds = new HashSet<String>();
|
||||||
|
private int pageSize;
|
||||||
|
private int chestRecentDays;
|
||||||
|
private int maxNearRadius;
|
||||||
|
private int defaultPage;
|
||||||
|
private boolean cleanupEnabled;
|
||||||
|
private int cleanupRetentionDays;
|
||||||
|
private int cleanupIntervalHours;
|
||||||
|
private String databaseFile;
|
||||||
|
|
||||||
|
public PluginConfig(CLMain plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() {
|
||||||
|
plugin.saveDefaultConfig();
|
||||||
|
FileConfiguration config = plugin.getConfig();
|
||||||
|
|
||||||
|
listenWorlds.clear();
|
||||||
|
List<String> worlds = config.getStringList("listen-worlds");
|
||||||
|
for (String world : worlds) {
|
||||||
|
if (world != null && !world.trim().isEmpty()) {
|
||||||
|
listenWorlds.add(world.trim().toLowerCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Bukkit.getConsoleSender().sendMessage("§f[§a!§f] §f监听世界 §8> §6" + listenWorlds.size() + "个");
|
||||||
|
pageSize = Math.max(1, config.getInt("query.page-size", 8));
|
||||||
|
chestRecentDays = Math.max(1, config.getInt("query.chest-recent-days", 30));
|
||||||
|
maxNearRadius = Math.max(1, config.getInt("query.max-near-radius", 50));
|
||||||
|
defaultPage = Math.max(1, config.getInt("query.default-page", 1));
|
||||||
|
databaseFile = config.getString("database.file", "containerlog.db");
|
||||||
|
cleanupEnabled = config.getBoolean("cleanup.enabled", true);
|
||||||
|
cleanupRetentionDays = Math.max(1, config.getInt("cleanup.retention-days", 60));
|
||||||
|
if (cleanupEnabled){
|
||||||
|
Bukkit.getConsoleSender().sendMessage("§f[§a!§f] §f数据缓存 §8> §6" + cleanupRetentionDays + "天");
|
||||||
|
}
|
||||||
|
cleanupIntervalHours = Math.max(1, config.getInt("cleanup.interval-hours", 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWorldEnabled(String worldName) {
|
||||||
|
return worldName != null && listenWorlds.contains(worldName.toLowerCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPageSize() {
|
||||||
|
return pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChestRecentDays() {
|
||||||
|
return chestRecentDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxNearRadius() {
|
||||||
|
return maxNearRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultPage() {
|
||||||
|
return defaultPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCleanupEnabled() {
|
||||||
|
return cleanupEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCleanupRetentionDays() {
|
||||||
|
return cleanupRetentionDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCleanupIntervalHours() {
|
||||||
|
return cleanupIntervalHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getDatabaseFile() {
|
||||||
|
return new File(plugin.getDataFolder(), databaseFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String color(String message) {
|
||||||
|
return ChatColor.translateAlternateColorCodes('&', message == null ? "" : message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
package com.yaohun.containerlog.database;
|
||||||
|
|
||||||
|
import com.yaohun.containerlog.CLMain;
|
||||||
|
import com.yaohun.containerlog.model.ContainerAction;
|
||||||
|
import com.yaohun.containerlog.model.LogRecord;
|
||||||
|
import com.yaohun.containerlog.model.PageResult;
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
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.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class DatabaseManager {
|
||||||
|
|
||||||
|
private final CLMain plugin;
|
||||||
|
private ExecutorService executorService;
|
||||||
|
private HikariDataSource dataSource;
|
||||||
|
private File databaseFile;
|
||||||
|
|
||||||
|
public DatabaseManager(CLMain plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void start() {
|
||||||
|
databaseFile = plugin.getPluginConfig().getDatabaseFile();
|
||||||
|
File parent = databaseFile.getParentFile();
|
||||||
|
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 无法创建数据库目录:" + parent.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataSource = createSQLiteDataSource(databaseFile, "AuroraContainerLog-SQLite");
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 当前环境未检测到 sqlite-jdbc 驱动");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable runnable) {
|
||||||
|
Thread thread = new Thread(runnable, "AuroraContainerLog-SQLite");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
executorService.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
initializeSchema();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void reload() {
|
||||||
|
stop();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void stop() {
|
||||||
|
if (executorService != null) {
|
||||||
|
ExecutorService executor = executorService;
|
||||||
|
executor.shutdown();
|
||||||
|
try {
|
||||||
|
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException exception) {
|
||||||
|
executor.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
executorService = null;
|
||||||
|
}
|
||||||
|
closeDataSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertRecords(final List<LogRecord> records) {
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ExecutorService executor = executorService;
|
||||||
|
if (executor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
insertRecordsInternal(records);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cleanupExpiredRecords() {
|
||||||
|
ExecutorService executor = executorService;
|
||||||
|
if (executor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int retentionDays = plugin.getPluginConfig().getCleanupRetentionDays();
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
cleanupExpiredRecordsInternal(retentionDays);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queryByPlayer(String playerName, long sinceMillis, int page, int pageSize, QueryCallback callback) {
|
||||||
|
String where = "lower(player_name) = lower(?) AND created_at >= ?";
|
||||||
|
List<Object> params = new ArrayList<Object>();
|
||||||
|
params.add(playerName);
|
||||||
|
params.add(sinceMillis);
|
||||||
|
query(where, params, page, pageSize, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queryByLocations(List<Location> locations, long sinceMillis, int page, int pageSize, QueryCallback callback) {
|
||||||
|
if (locations == null || locations.isEmpty()) {
|
||||||
|
complete(callback, new PageResult(new ArrayList<LogRecord>(), Math.max(1, page), 1, 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder where = new StringBuilder("(");
|
||||||
|
List<Object> params = new ArrayList<Object>();
|
||||||
|
for (int index = 0; index < locations.size(); index++) {
|
||||||
|
if (index > 0) {
|
||||||
|
where.append(" OR ");
|
||||||
|
}
|
||||||
|
where.append("(world = ? AND x = ? AND y = ? AND z = ?)");
|
||||||
|
Location location = locations.get(index);
|
||||||
|
params.add(location.getWorld().getName());
|
||||||
|
params.add(location.getBlockX());
|
||||||
|
params.add(location.getBlockY());
|
||||||
|
params.add(location.getBlockZ());
|
||||||
|
}
|
||||||
|
where.append(") AND created_at >= ?");
|
||||||
|
params.add(sinceMillis);
|
||||||
|
query(where.toString(), params, page, pageSize, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queryNear(Location center, int radius, long sinceMillis, int page, int pageSize, QueryCallback callback) {
|
||||||
|
String where = "world = ? AND x BETWEEN ? AND ? AND y BETWEEN ? AND ? AND z BETWEEN ? AND ? AND created_at >= ?";
|
||||||
|
List<Object> params = new ArrayList<Object>();
|
||||||
|
params.add(center.getWorld().getName());
|
||||||
|
params.add(center.getBlockX() - radius);
|
||||||
|
params.add(center.getBlockX() + radius);
|
||||||
|
params.add(center.getBlockY() - radius);
|
||||||
|
params.add(center.getBlockY() + radius);
|
||||||
|
params.add(center.getBlockZ() - radius);
|
||||||
|
params.add(center.getBlockZ() + radius);
|
||||||
|
params.add(sinceMillis);
|
||||||
|
query(where, params, page, pageSize, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HikariDataSource createSQLiteDataSource(File file, String poolName) {
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
config.setJdbcUrl("jdbc:sqlite:" + file.getAbsolutePath());
|
||||||
|
config.setPoolName(poolName);
|
||||||
|
config.setMaximumPoolSize(1);
|
||||||
|
config.setMinimumIdle(1);
|
||||||
|
config.setConnectionTimeout(10000L);
|
||||||
|
config.setValidationTimeout(3000L);
|
||||||
|
config.setIdleTimeout(600000L);
|
||||||
|
config.setMaxLifetime(1700000L);
|
||||||
|
config.setAutoCommit(true);
|
||||||
|
return new HikariDataSource(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void query(final String where, final List<Object> params, final int page, final int pageSize, final QueryCallback callback) {
|
||||||
|
ExecutorService executor = executorService;
|
||||||
|
if (executor == null) {
|
||||||
|
complete(callback, new PageResult(new ArrayList<LogRecord>(), Math.max(1, page), 1, 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
PageResult result;
|
||||||
|
try {
|
||||||
|
result = queryInternal(where, params, page, pageSize);
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 查询容器日志失败");
|
||||||
|
result = new PageResult(new ArrayList<LogRecord>(), Math.max(1, page), 1, 0);
|
||||||
|
}
|
||||||
|
complete(callback, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private PageResult queryInternal(String where, List<Object> params, int page, int pageSize) throws SQLException {
|
||||||
|
int currentPage = Math.max(1, page);
|
||||||
|
int size = Math.max(1, pageSize);
|
||||||
|
int total;
|
||||||
|
|
||||||
|
Connection connection = getConnection();
|
||||||
|
try {
|
||||||
|
PreparedStatement countStatement = connection.prepareStatement("SELECT COUNT(*) FROM acl_records WHERE " + where);
|
||||||
|
try {
|
||||||
|
bindParams(countStatement, params);
|
||||||
|
ResultSet countResult = countStatement.executeQuery();
|
||||||
|
try {
|
||||||
|
total = countResult.next() ? countResult.getInt(1) : 0;
|
||||||
|
} finally {
|
||||||
|
countResult.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
countStatement.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalPages = Math.max(1, (int) Math.ceil(total / (double) size));
|
||||||
|
if (currentPage > totalPages) {
|
||||||
|
currentPage = totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreparedStatement queryStatement = connection.prepareStatement("SELECT * FROM acl_records WHERE " + where + " ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?");
|
||||||
|
try {
|
||||||
|
bindParams(queryStatement, params);
|
||||||
|
queryStatement.setInt(params.size() + 1, size);
|
||||||
|
queryStatement.setInt(params.size() + 2, (currentPage - 1) * size);
|
||||||
|
ResultSet resultSet = queryStatement.executeQuery();
|
||||||
|
try {
|
||||||
|
List<LogRecord> records = new ArrayList<LogRecord>();
|
||||||
|
while (resultSet.next()) {
|
||||||
|
records.add(readRecord(resultSet));
|
||||||
|
}
|
||||||
|
return new PageResult(records, currentPage, totalPages, total);
|
||||||
|
} finally {
|
||||||
|
resultSet.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
queryStatement.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertRecordsInternal(List<LogRecord> records) {
|
||||||
|
Connection connection = null;
|
||||||
|
try {
|
||||||
|
connection = getConnection();
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
PreparedStatement statement = connection.prepareStatement("INSERT INTO acl_records (created_at, world, x, y, z, player_name, player_uuid, action, material, amount, display_name, lore, enchantments, item_stack_base64) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
try {
|
||||||
|
for (LogRecord record : records) {
|
||||||
|
statement.setLong(1, record.getCreatedAt());
|
||||||
|
statement.setString(2, record.getWorld());
|
||||||
|
statement.setInt(3, record.getX());
|
||||||
|
statement.setInt(4, record.getY());
|
||||||
|
statement.setInt(5, record.getZ());
|
||||||
|
statement.setString(6, record.getPlayerName());
|
||||||
|
statement.setString(7, record.getPlayerUuid());
|
||||||
|
statement.setString(8, record.getAction().name());
|
||||||
|
statement.setString(9, record.getMaterial());
|
||||||
|
statement.setInt(10, record.getAmount());
|
||||||
|
statement.setString(11, record.getDisplayName());
|
||||||
|
statement.setString(12, record.getLore());
|
||||||
|
statement.setString(13, record.getEnchantments());
|
||||||
|
statement.setString(14, record.getItemStackBase64());
|
||||||
|
statement.addBatch();
|
||||||
|
}
|
||||||
|
statement.executeBatch();
|
||||||
|
} finally {
|
||||||
|
statement.close();
|
||||||
|
}
|
||||||
|
connection.commit();
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
rollback(connection);
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 写入容器日志失败");
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupExpiredRecordsInternal(int retentionDays) {
|
||||||
|
long expireBefore = System.currentTimeMillis() - (Math.max(1, retentionDays) * 86400000L);
|
||||||
|
Connection connection = null;
|
||||||
|
try {
|
||||||
|
connection = getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("DELETE FROM acl_records WHERE created_at < ?");
|
||||||
|
try {
|
||||||
|
statement.setLong(1, expireBefore);
|
||||||
|
int deleted = statement.executeUpdate();
|
||||||
|
if (deleted > 0) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 已清理 "+deleted+" 条超过 "+retentionDays+" 天的容器缓存。");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
statement.close();
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 清理过期容器日志失败");
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeSchema() {
|
||||||
|
Connection connection = null;
|
||||||
|
try {
|
||||||
|
connection = getConnection();
|
||||||
|
Statement statement = connection.createStatement();
|
||||||
|
try {
|
||||||
|
statement.executeUpdate("CREATE TABLE IF NOT EXISTS acl_records (" +
|
||||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||||
|
"created_at INTEGER NOT NULL," +
|
||||||
|
"world TEXT NOT NULL," +
|
||||||
|
"x INTEGER NOT NULL," +
|
||||||
|
"y INTEGER NOT NULL," +
|
||||||
|
"z INTEGER NOT NULL," +
|
||||||
|
"player_name TEXT NOT NULL," +
|
||||||
|
"player_uuid TEXT NOT NULL," +
|
||||||
|
"action TEXT NOT NULL," +
|
||||||
|
"material TEXT NOT NULL," +
|
||||||
|
"amount INTEGER NOT NULL," +
|
||||||
|
"display_name TEXT," +
|
||||||
|
"lore TEXT," +
|
||||||
|
"enchantments TEXT," +
|
||||||
|
"item_stack_base64 TEXT NOT NULL" +
|
||||||
|
")");
|
||||||
|
statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_acl_records_player_time ON acl_records(player_name, created_at)");
|
||||||
|
statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_acl_records_location_time ON acl_records(world, x, y, z, created_at)");
|
||||||
|
statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_acl_records_time ON acl_records(created_at)");
|
||||||
|
} finally {
|
||||||
|
statement.close();
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] SQLite 初始化失败");
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Connection getConnection() throws SQLException {
|
||||||
|
HikariDataSource currentDataSource = dataSource;
|
||||||
|
if (currentDataSource == null || currentDataSource.isClosed()) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] SQLite 连接池未初始化");
|
||||||
|
throw new SQLException();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection connection = currentDataSource.getConnection();
|
||||||
|
Statement statement = connection.createStatement();
|
||||||
|
try {
|
||||||
|
statement.execute("PRAGMA busy_timeout = 5000");
|
||||||
|
statement.execute("PRAGMA journal_mode = WAL");
|
||||||
|
} finally {
|
||||||
|
statement.close();
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindParams(PreparedStatement statement, List<Object> params) throws SQLException {
|
||||||
|
for (int index = 0; index < params.size(); index++) {
|
||||||
|
Object value = params.get(index);
|
||||||
|
if (value instanceof String) {
|
||||||
|
statement.setString(index + 1, (String) value);
|
||||||
|
} else if (value instanceof Integer) {
|
||||||
|
statement.setInt(index + 1, (Integer) value);
|
||||||
|
} else if (value instanceof Long) {
|
||||||
|
statement.setLong(index + 1, (Long) value);
|
||||||
|
} else {
|
||||||
|
statement.setObject(index + 1, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LogRecord readRecord(ResultSet resultSet) throws SQLException {
|
||||||
|
LogRecord record = new LogRecord();
|
||||||
|
record.setId(resultSet.getLong("id"));
|
||||||
|
record.setCreatedAt(resultSet.getLong("created_at"));
|
||||||
|
record.setWorld(resultSet.getString("world"));
|
||||||
|
record.setX(resultSet.getInt("x"));
|
||||||
|
record.setY(resultSet.getInt("y"));
|
||||||
|
record.setZ(resultSet.getInt("z"));
|
||||||
|
record.setPlayerName(resultSet.getString("player_name"));
|
||||||
|
record.setPlayerUuid(resultSet.getString("player_uuid"));
|
||||||
|
record.setAction(ContainerAction.valueOf(resultSet.getString("action")));
|
||||||
|
record.setMaterial(resultSet.getString("material"));
|
||||||
|
record.setAmount(resultSet.getInt("amount"));
|
||||||
|
record.setDisplayName(resultSet.getString("display_name"));
|
||||||
|
record.setLore(resultSet.getString("lore"));
|
||||||
|
record.setEnchantments(resultSet.getString("enchantments"));
|
||||||
|
record.setItemStackBase64(resultSet.getString("item_stack_base64"));
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void complete(final QueryCallback callback, final PageResult result) {
|
||||||
|
if (callback == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bukkit.getScheduler().runTask(plugin, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
callback.onComplete(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rollback(Connection connection) {
|
||||||
|
if (connection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connection.rollback();
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 回滚容器日志事务失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close(Connection connection) {
|
||||||
|
if (connection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connection.close();
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
Bukkit.getConsoleSender().sendMessage("[日志 - 容器数据] 关闭 SQLite 连接失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeDataSource() {
|
||||||
|
if (dataSource != null && !dataSource.isClosed()) {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
dataSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.yaohun.containerlog.database;
|
||||||
|
|
||||||
|
import com.yaohun.containerlog.model.PageResult;
|
||||||
|
|
||||||
|
public interface QueryCallback {
|
||||||
|
|
||||||
|
void onComplete(PageResult result);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package com.yaohun.containerlog.listener;
|
||||||
|
|
||||||
|
import com.yaohun.containerlog.CLMain;
|
||||||
|
import com.yaohun.containerlog.model.ContainerSession;
|
||||||
|
import com.yaohun.containerlog.model.ItemChange;
|
||||||
|
import com.yaohun.containerlog.model.LogRecord;
|
||||||
|
import com.yaohun.containerlog.util.ChestLocationResolver;
|
||||||
|
import com.yaohun.containerlog.util.ItemDiffUtil;
|
||||||
|
import com.yaohun.containerlog.util.ItemStackUtil;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class ContainerListener implements Listener {
|
||||||
|
|
||||||
|
private static final long CLICKED_LOCATION_EXPIRE_MILLIS = 2500L;
|
||||||
|
|
||||||
|
private final CLMain plugin;
|
||||||
|
private final Map<UUID, ContainerSession> sessions = new HashMap<UUID, ContainerSession>();
|
||||||
|
private final Map<UUID, ClickedChest> clickedChests = new HashMap<UUID, ClickedChest>();
|
||||||
|
|
||||||
|
public ContainerListener(CLMain plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Block block = event.getClickedBlock();
|
||||||
|
if (!ChestLocationResolver.isChestBlock(block)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!plugin.getPluginConfig().isWorldEnabled(block.getWorld().getName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickedChests.put(event.getPlayer().getUniqueId(), new ClickedChest(block.getLocation(), System.currentTimeMillis()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onInventoryOpen(InventoryOpenEvent event) {
|
||||||
|
if (!(event.getPlayer() instanceof Player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = (Player) event.getPlayer();
|
||||||
|
Inventory topInventory = event.getView().getTopInventory();
|
||||||
|
if (!ChestLocationResolver.isChestInventory(topInventory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location clickedLocation = consumeClickedLocation(player.getUniqueId());
|
||||||
|
Location containerLocation = ChestLocationResolver.resolvePrimaryLocation(topInventory, clickedLocation);
|
||||||
|
if (containerLocation == null || containerLocation.getWorld() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!plugin.getPluginConfig().isWorldEnabled(containerLocation.getWorld().getName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack[] snapshot = ItemDiffUtil.cloneContents(topInventory.getContents());
|
||||||
|
sessions.put(player.getUniqueId(), new ContainerSession(player.getUniqueId(), player.getName(), containerLocation, snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
|
if (event.getWhoClicked() instanceof Player) {
|
||||||
|
markTouched((Player) event.getWhoClicked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onInventoryDrag(InventoryDragEvent event) {
|
||||||
|
if (event.getWhoClicked() instanceof Player) {
|
||||||
|
markTouched((Player) event.getWhoClicked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onInventoryClose(InventoryCloseEvent event) {
|
||||||
|
if (!(event.getPlayer() instanceof Player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = (Player) event.getPlayer();
|
||||||
|
ContainerSession session = sessions.remove(player.getUniqueId());
|
||||||
|
if (session == null || !session.isTouched()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Inventory topInventory = event.getView().getTopInventory();
|
||||||
|
if (!ChestLocationResolver.isChestInventory(topInventory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<ItemChange> changes = ItemDiffUtil.diff(session.getSnapshot(), topInventory.getContents());
|
||||||
|
if (changes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.getDatabaseManager().insertRecords(toRecords(session, changes));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "序列化容器物品变化失败", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSessions() {
|
||||||
|
sessions.clear();
|
||||||
|
clickedChests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markTouched(Player player) {
|
||||||
|
ContainerSession session = sessions.get(player.getUniqueId());
|
||||||
|
if (session != null) {
|
||||||
|
session.markTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location consumeClickedLocation(UUID playerUuid) {
|
||||||
|
ClickedChest clickedChest = clickedChests.remove(playerUuid);
|
||||||
|
if (clickedChest == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (System.currentTimeMillis() - clickedChest.clickedAt > CLICKED_LOCATION_EXPIRE_MILLIS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return clickedChest.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LogRecord> toRecords(ContainerSession session, List<ItemChange> changes) throws IOException {
|
||||||
|
List<LogRecord> records = new ArrayList<LogRecord>();
|
||||||
|
Location location = session.getContainerLocation();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (ItemChange change : changes) {
|
||||||
|
ItemStack itemStack = change.getItemStack();
|
||||||
|
LogRecord record = new LogRecord();
|
||||||
|
record.setCreatedAt(now);
|
||||||
|
record.setWorld(location.getWorld().getName());
|
||||||
|
record.setX(location.getBlockX());
|
||||||
|
record.setY(location.getBlockY());
|
||||||
|
record.setZ(location.getBlockZ());
|
||||||
|
record.setPlayerName(session.getPlayerName());
|
||||||
|
record.setPlayerUuid(session.getPlayerUuid().toString());
|
||||||
|
record.setAction(change.getAction());
|
||||||
|
record.setMaterial(itemStack.getType().name());
|
||||||
|
record.setAmount(change.getAmount());
|
||||||
|
record.setDisplayName(ItemStackUtil.getDisplayName(itemStack));
|
||||||
|
record.setLore(ItemStackUtil.getLore(itemStack));
|
||||||
|
record.setEnchantments(ItemStackUtil.getEnchantments(itemStack));
|
||||||
|
record.setItemStackBase64(ItemStackUtil.toBase64(itemStack));
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClickedChest {
|
||||||
|
private final Location location;
|
||||||
|
private final long clickedAt;
|
||||||
|
|
||||||
|
private ClickedChest(Location location, long clickedAt) {
|
||||||
|
this.location = location;
|
||||||
|
this.clickedAt = clickedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.yaohun.containerlog.model;
|
||||||
|
|
||||||
|
public enum ContainerAction {
|
||||||
|
TAKE("取出"),
|
||||||
|
PUT("放入");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
ContainerAction(String name){
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.yaohun.containerlog.model;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ContainerSession {
|
||||||
|
|
||||||
|
private final UUID playerUuid;
|
||||||
|
private final String playerName;
|
||||||
|
private final Location containerLocation;
|
||||||
|
private final ItemStack[] snapshot;
|
||||||
|
private final long openedAt;
|
||||||
|
private boolean touched;
|
||||||
|
|
||||||
|
public ContainerSession(UUID playerUuid, String playerName, Location containerLocation, ItemStack[] snapshot) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.playerName = playerName;
|
||||||
|
this.containerLocation = containerLocation;
|
||||||
|
this.snapshot = snapshot;
|
||||||
|
this.openedAt = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPlayerUuid() {
|
||||||
|
return playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlayerName() {
|
||||||
|
return playerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getContainerLocation() {
|
||||||
|
return containerLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack[] getSnapshot() {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOpenedAt() {
|
||||||
|
return openedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTouched() {
|
||||||
|
return touched;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markTouched() {
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/main/java/com/yaohun/containerlog/model/ItemChange.java
Normal file
28
src/main/java/com/yaohun/containerlog/model/ItemChange.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.yaohun.containerlog.model;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
public class ItemChange {
|
||||||
|
|
||||||
|
private final ContainerAction action;
|
||||||
|
private final ItemStack itemStack;
|
||||||
|
private final int amount;
|
||||||
|
|
||||||
|
public ItemChange(ContainerAction action, ItemStack itemStack, int amount) {
|
||||||
|
this.action = action;
|
||||||
|
this.itemStack = itemStack;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerAction getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack getItemStack() {
|
||||||
|
return itemStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/main/java/com/yaohun/containerlog/model/LogRecord.java
Normal file
140
src/main/java/com/yaohun/containerlog/model/LogRecord.java
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package com.yaohun.containerlog.model;
|
||||||
|
|
||||||
|
public class LogRecord {
|
||||||
|
|
||||||
|
private long id;
|
||||||
|
private long createdAt;
|
||||||
|
private String world;
|
||||||
|
private int x;
|
||||||
|
private int y;
|
||||||
|
private int z;
|
||||||
|
private String playerName;
|
||||||
|
private String playerUuid;
|
||||||
|
private ContainerAction action;
|
||||||
|
private String material;
|
||||||
|
private int amount;
|
||||||
|
private String displayName;
|
||||||
|
private String lore;
|
||||||
|
private String enchantments;
|
||||||
|
private String itemStackBase64;
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(long createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWorld() {
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorld(String world) {
|
||||||
|
this.world = world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setX(int x) {
|
||||||
|
this.x = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setY(int y) {
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getZ() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setZ(int z) {
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlayerName() {
|
||||||
|
return playerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerName(String playerName) {
|
||||||
|
this.playerName = playerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlayerUuid() {
|
||||||
|
return playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerUuid(String playerUuid) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerAction getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAction(ContainerAction action) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMaterial() {
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaterial(String material) {
|
||||||
|
this.material = material;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(int amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayName(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLore() {
|
||||||
|
return lore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLore(String lore) {
|
||||||
|
this.lore = lore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEnchantments() {
|
||||||
|
return enchantments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnchantments(String enchantments) {
|
||||||
|
this.enchantments = enchantments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getItemStackBase64() {
|
||||||
|
return itemStackBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItemStackBase64(String itemStackBase64) {
|
||||||
|
this.itemStackBase64 = itemStackBase64;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/java/com/yaohun/containerlog/model/PageResult.java
Normal file
35
src/main/java/com/yaohun/containerlog/model/PageResult.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yaohun.containerlog.model;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PageResult {
|
||||||
|
|
||||||
|
private final List<LogRecord> records;
|
||||||
|
private final int page;
|
||||||
|
private final int totalPages;
|
||||||
|
private final int total;
|
||||||
|
|
||||||
|
public PageResult(List<LogRecord> records, int page, int totalPages, int total) {
|
||||||
|
this.records = records == null ? Collections.<LogRecord>emptyList() : records;
|
||||||
|
this.page = page;
|
||||||
|
this.totalPages = totalPages;
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LogRecord> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPage() {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalPages() {
|
||||||
|
return totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.yaohun.containerlog.util;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
|
import org.bukkit.block.Chest;
|
||||||
|
import org.bukkit.block.DoubleChest;
|
||||||
|
import org.bukkit.event.inventory.InventoryType;
|
||||||
|
import org.bukkit.inventory.DoubleChestInventory;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class ChestLocationResolver {
|
||||||
|
|
||||||
|
private static final BlockFace[] HORIZONTAL_FACES = new BlockFace[]{
|
||||||
|
BlockFace.NORTH,
|
||||||
|
BlockFace.SOUTH,
|
||||||
|
BlockFace.EAST,
|
||||||
|
BlockFace.WEST
|
||||||
|
};
|
||||||
|
|
||||||
|
private ChestLocationResolver() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isChestInventory(Inventory inventory) {
|
||||||
|
if (inventory == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
InventoryHolder holder = inventory.getHolder();
|
||||||
|
if (holder instanceof Chest || holder instanceof DoubleChest || inventory instanceof DoubleChestInventory) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return inventory.getType() == InventoryType.CHEST && (inventory.getSize() == 27 || inventory.getSize() == 54);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isChestBlock(Block block) {
|
||||||
|
if (block == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String typeName = block.getType().name();
|
||||||
|
return "CHEST".equals(typeName) || "TRAPPED_CHEST".equals(typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Location resolvePrimaryLocation(Inventory inventory, Location clickedLocation) {
|
||||||
|
if (clickedLocation != null && isChestBlock(clickedLocation.getBlock())) {
|
||||||
|
return toBlockLocation(clickedLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryHolder holder = inventory == null ? null : inventory.getHolder();
|
||||||
|
if (holder instanceof Chest) {
|
||||||
|
return ((Chest) holder).getBlock().getLocation();
|
||||||
|
}
|
||||||
|
if (holder instanceof DoubleChest) {
|
||||||
|
DoubleChest doubleChest = (DoubleChest) holder;
|
||||||
|
Location left = resolveHolderLocation(doubleChest.getLeftSide());
|
||||||
|
if (left != null) {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
return resolveHolderLocation(doubleChest.getRightSide());
|
||||||
|
}
|
||||||
|
if (inventory instanceof DoubleChestInventory) {
|
||||||
|
DoubleChestInventory doubleChestInventory = (DoubleChestInventory) inventory;
|
||||||
|
Location left = resolveInventoryLocation(doubleChestInventory.getLeftSide());
|
||||||
|
if (left != null) {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
return resolveInventoryLocation(doubleChestInventory.getRightSide());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Location> resolveLinkedChestLocations(Block block) {
|
||||||
|
Map<String, Location> locations = new LinkedHashMap<String, Location>();
|
||||||
|
if (!isChestBlock(block)) {
|
||||||
|
return new ArrayList<Location>();
|
||||||
|
}
|
||||||
|
|
||||||
|
addLocation(locations, block.getLocation());
|
||||||
|
for (BlockFace face : HORIZONTAL_FACES) {
|
||||||
|
Block relative = block.getRelative(face);
|
||||||
|
if (isChestBlock(relative) && relative.getType() == block.getType()) {
|
||||||
|
addLocation(locations, relative.getLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<Location>(locations.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Location resolveHolderLocation(InventoryHolder holder) {
|
||||||
|
if (holder instanceof Chest) {
|
||||||
|
return ((Chest) holder).getBlock().getLocation();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Location resolveInventoryLocation(Inventory inventory) {
|
||||||
|
return inventory == null ? null : resolveHolderLocation(inventory.getHolder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Location toBlockLocation(Location location) {
|
||||||
|
return new Location(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addLocation(Map<String, Location> locations, Location location) {
|
||||||
|
String key = location.getWorld().getName() + ":" + location.getBlockX() + ":" + location.getBlockY() + ":" + location.getBlockZ();
|
||||||
|
locations.put(key, toBlockLocation(location));
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/main/java/com/yaohun/containerlog/util/ItemDiffUtil.java
Normal file
84
src/main/java/com/yaohun/containerlog/util/ItemDiffUtil.java
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package com.yaohun.containerlog.util;
|
||||||
|
|
||||||
|
import com.yaohun.containerlog.model.ContainerAction;
|
||||||
|
import com.yaohun.containerlog.model.ItemChange;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class ItemDiffUtil {
|
||||||
|
|
||||||
|
private ItemDiffUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ItemChange> diff(ItemStack[] before, ItemStack[] after) throws IOException {
|
||||||
|
Map<String, StackCounter> beforeMap = count(before);
|
||||||
|
Map<String, StackCounter> afterMap = count(after);
|
||||||
|
Set<String> keys = new HashSet<String>();
|
||||||
|
keys.addAll(beforeMap.keySet());
|
||||||
|
keys.addAll(afterMap.keySet());
|
||||||
|
|
||||||
|
List<ItemChange> changes = new ArrayList<ItemChange>();
|
||||||
|
for (String key : keys) {
|
||||||
|
StackCounter beforeCounter = beforeMap.get(key);
|
||||||
|
StackCounter afterCounter = afterMap.get(key);
|
||||||
|
int beforeAmount = beforeCounter == null ? 0 : beforeCounter.amount;
|
||||||
|
int afterAmount = afterCounter == null ? 0 : afterCounter.amount;
|
||||||
|
int delta = afterAmount - beforeAmount;
|
||||||
|
if (delta == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContainerAction action = delta > 0 ? ContainerAction.PUT : ContainerAction.TAKE;
|
||||||
|
int amount = Math.abs(delta);
|
||||||
|
ItemStack sample = delta > 0 ? afterCounter.sample : beforeCounter.sample;
|
||||||
|
changes.add(new ItemChange(action, ItemStackUtil.cloneWithAmount(sample, amount), amount));
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemStack[] cloneContents(ItemStack[] contents) {
|
||||||
|
ItemStack[] clone = new ItemStack[contents.length];
|
||||||
|
for (int index = 0; index < contents.length; index++) {
|
||||||
|
ItemStack itemStack = contents[index];
|
||||||
|
clone[index] = itemStack == null ? null : itemStack.clone();
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, StackCounter> count(ItemStack[] contents) throws IOException {
|
||||||
|
Map<String, StackCounter> counters = new HashMap<String, StackCounter>();
|
||||||
|
if (contents == null) {
|
||||||
|
return counters;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ItemStack itemStack : contents) {
|
||||||
|
if (ItemStackUtil.isEmpty(itemStack)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = ItemStackUtil.toComparableKey(itemStack);
|
||||||
|
StackCounter counter = counters.get(key);
|
||||||
|
if (counter == null) {
|
||||||
|
counter = new StackCounter(itemStack.clone());
|
||||||
|
counters.put(key, counter);
|
||||||
|
}
|
||||||
|
counter.amount += itemStack.getAmount();
|
||||||
|
}
|
||||||
|
return counters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StackCounter {
|
||||||
|
private final ItemStack sample;
|
||||||
|
private int amount;
|
||||||
|
|
||||||
|
private StackCounter(ItemStack sample) {
|
||||||
|
this.sample = sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.yaohun.containerlog.util;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.enchantments.Enchantment;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class ItemStackUtil {
|
||||||
|
|
||||||
|
private ItemStackUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEmpty(ItemStack itemStack) {
|
||||||
|
return itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemStack cloneWithAmount(ItemStack itemStack, int amount) {
|
||||||
|
ItemStack clone = itemStack.clone();
|
||||||
|
clone.setAmount(amount);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toComparableKey(ItemStack itemStack) throws IOException {
|
||||||
|
return toBase64(cloneWithAmount(itemStack, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toBase64(ItemStack itemStack) throws IOException {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
|
||||||
|
try {
|
||||||
|
dataOutput.writeObject(itemStack);
|
||||||
|
} finally {
|
||||||
|
dataOutput.close();
|
||||||
|
}
|
||||||
|
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDisplayName(ItemStack itemStack) {
|
||||||
|
if (isEmpty(itemStack) || !itemStack.hasItemMeta()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ItemMeta meta = itemStack.getItemMeta();
|
||||||
|
return meta != null && meta.hasDisplayName() ? meta.getDisplayName() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getLore(ItemStack itemStack) {
|
||||||
|
if (isEmpty(itemStack) || !itemStack.hasItemMeta()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ItemMeta meta = itemStack.getItemMeta();
|
||||||
|
if (meta == null || !meta.hasLore() || meta.getLore() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return joinLines(meta.getLore());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getEnchantments(ItemStack itemStack) {
|
||||||
|
if (isEmpty(itemStack) || itemStack.getEnchantments().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Map.Entry<Enchantment, Integer>> entries = new ArrayList<Map.Entry<Enchantment, Integer>>(itemStack.getEnchantments().entrySet());
|
||||||
|
Collections.sort(entries, new Comparator<Map.Entry<Enchantment, Integer>>() {
|
||||||
|
@Override
|
||||||
|
public int compare(Map.Entry<Enchantment, Integer> first, Map.Entry<Enchantment, Integer> second) {
|
||||||
|
return first.getKey().getName().compareTo(second.getKey().getName());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> parts = new ArrayList<String>();
|
||||||
|
for (Map.Entry<Enchantment, Integer> entry : entries) {
|
||||||
|
parts.add(entry.getKey().getName() + ":" + entry.getValue());
|
||||||
|
}
|
||||||
|
return joinLines(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String joinLines(List<String> lines) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int index = 0; index < lines.size(); index++) {
|
||||||
|
if (index > 0) {
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
builder.append(lines.get(index));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/com/yaohun/containerlog/util/TimeUtil.java
Normal file
25
src/main/java/com/yaohun/containerlog/util/TimeUtil.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package com.yaohun.containerlog.util;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public final class TimeUtil {
|
||||||
|
|
||||||
|
private static final ThreadLocal<SimpleDateFormat> FORMAT = new ThreadLocal<SimpleDateFormat>() {
|
||||||
|
@Override
|
||||||
|
protected SimpleDateFormat initialValue() {
|
||||||
|
return new SimpleDateFormat("MM-dd HH:mm");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private TimeUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long daysAgoMillis(int days) {
|
||||||
|
return System.currentTimeMillis() - (Math.max(1, days) * 86400000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String format(long millis) {
|
||||||
|
return FORMAT.get().format(new Date(millis));
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/main/resources/config.yml
Normal file
51
src/main/resources/config.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# ==========================================
|
||||||
|
# AuroraContainerLog 配置文件
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 监听的世界列表
|
||||||
|
# 只有这些世界中的容器操作才会被记录
|
||||||
|
# 示例:
|
||||||
|
# - world
|
||||||
|
# - world_nether
|
||||||
|
# - resource
|
||||||
|
listen-worlds:
|
||||||
|
- world
|
||||||
|
|
||||||
|
# 查询功能相关配置
|
||||||
|
query:
|
||||||
|
|
||||||
|
# 每页显示多少条记录
|
||||||
|
# 用于 GUI 或命令查询分页
|
||||||
|
page-size: 8
|
||||||
|
|
||||||
|
# 查询箱子历史时默认显示最近多少天的数据
|
||||||
|
# 超过此时间范围的数据不会被展示
|
||||||
|
chest-recent-days: 30
|
||||||
|
|
||||||
|
# 附近容器查询最大允许半径
|
||||||
|
# 防止一次查询过大范围导致性能问题
|
||||||
|
max-near-radius: 50
|
||||||
|
|
||||||
|
# 默认打开的页码
|
||||||
|
default-page: 1
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
database:
|
||||||
|
|
||||||
|
# SQLite数据库文件名称
|
||||||
|
# 文件将保存在插件数据目录下
|
||||||
|
file: containerlog.db
|
||||||
|
|
||||||
|
# 自动清理配置
|
||||||
|
cleanup:
|
||||||
|
|
||||||
|
# 是否启用自动清理历史记录
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# 数据保留天数
|
||||||
|
# 超过该天数的记录将被删除
|
||||||
|
retention-days: 60
|
||||||
|
|
||||||
|
# 自动清理执行间隔(小时)
|
||||||
|
# 例如24表示每天执行一次
|
||||||
|
interval-hours: 24
|
||||||
16
src/main/resources/plugin.yml
Normal file
16
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: AuroraContainerLog
|
||||||
|
main: com.yaohun.containerlog.CLMain
|
||||||
|
version: 1.1.5
|
||||||
|
author: yaohun
|
||||||
|
description: 记录指定世界箱子容器的物品放入和取出日志
|
||||||
|
commands:
|
||||||
|
acl:
|
||||||
|
description: AuroraContainerLog 管理命令
|
||||||
|
usage: /acl <reload|player|chest|near>
|
||||||
|
permission: auroracontainerlog.admin
|
||||||
|
aliases:
|
||||||
|
- aclog
|
||||||
|
permissions:
|
||||||
|
auroracontainerlog.admin:
|
||||||
|
description: 允许查询和管理箱子日志
|
||||||
|
default: op
|
||||||
Reference in New Issue
Block a user