Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c66ec5eaaa | ||
|
|
aa719dc2da | ||
|
|
32371a6e30 | ||
|
|
7953e9de13 | ||
|
|
038e2f3e87 | ||
|
|
a35ddb8418 | ||
|
|
3c65360f9c | ||
|
|
7b0722b039 | ||
|
|
49fb264378 | ||
|
|
7dca1a71e1 | ||
|
|
704b8a03d9 | ||
|
|
387fad8bdd | ||
|
|
acc4085509 | ||
|
|
a75be0ebae | ||
|
|
325ec9f877 | ||
|
|
851375502d | ||
|
|
c9da8c8ec2 | ||
|
|
cf246d4076 | ||
|
|
c747dbcc76 | ||
|
|
66f1e33d5e | ||
|
|
717a7a9266 |
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug 反馈
|
||||
about: 报告一个 Bug 以帮助改进插件
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Bug 描述
|
||||
请简明扼要地描述 Bug 是什么。
|
||||
|
||||
## 复现步骤
|
||||
1. 进入 ...
|
||||
2. 点击 ...
|
||||
3. 出现错误 ...
|
||||
|
||||
## 期望行为
|
||||
描述你期望发生的事情。
|
||||
|
||||
## 实际行为
|
||||
描述实际发生了什么。
|
||||
|
||||
## 服务端信息
|
||||
- 服务器类型和版本(如 Paper 1.21.4):
|
||||
- 插件版本:
|
||||
|
||||
## 截图 / 日志
|
||||
如有相关截图或报错日志,请粘贴在此处。
|
||||
|
||||
## 其他信息
|
||||
补充任何你认为有助于解决问题的信息。
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 问题排查指南
|
||||
url: https://github.com/ypsdf1/sdf1_login
|
||||
about: 在提交 Bug 前,请先查阅文档确认是否已有相关说明。
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 功能新增
|
||||
about: 提出一个新功能的需求建议
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 功能描述
|
||||
请描述你希望添加的新功能。
|
||||
|
||||
## 动机
|
||||
为什么需要这个功能?它能解决什么问题?
|
||||
|
||||
## 建议的实现方式
|
||||
如果对实现方式有想法,请在此描述。
|
||||
|
||||
## 额外信息
|
||||
补充任何你认为有助于理解需求的上下文。
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: 建议提交
|
||||
about: 对插件提出改进建议或反馈
|
||||
title: "[Suggestion] "
|
||||
labels: suggestion
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 建议内容
|
||||
请描述你的建议或想法。
|
||||
|
||||
## 背景
|
||||
什么场景下产生的这个建议?
|
||||
|
||||
## 预期效果
|
||||
你认为采用这个建议后能达到什么效果?
|
||||
Generated
+10
-10
@@ -1,10 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
||||
Generated
+6
-6
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+13
-13
@@ -1,14 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+5
-5
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -4,30 +4,35 @@
|
||||
|
||||
## 1. 功能介绍
|
||||
|
||||
| 名字 | 描述/作用 | 施工状态
|
||||
| :--- | :--- | :---
|
||||
| 登录 | 用户登录/注册 | 已完成
|
||||
| 世界垃圾箱 | 清理服务器凋落物 | 已完成
|
||||
| 工单系统 | 用户提交工单 | 已完成
|
||||
| 签到系统 | 签到积分换奖励 | 已完成
|
||||
| 新人礼包 | 新玩家7日礼包 | 已完成
|
||||
| 挂机检测 | 挂机检测踢出 | 已完成
|
||||
| 自定义菜单 | 可以自己diy指令 | 已完成
|
||||
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 待施工
|
||||
| 名字 | 描述/作用 | 施工状态 |
|
||||
| :--- | :--- | :--- |
|
||||
| 登录 | 用户登录/注册 | 已完成 |
|
||||
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
||||
| 工单系统 | 用户提交工单 | 已完成 |
|
||||
| 签到系统 | 签到积分换奖励 | 已完成 |
|
||||
| 新人礼包 | 新玩家7日礼包 | 已完成 |
|
||||
| 挂机检测 | 挂机检测踢出 | 已完成 |
|
||||
| 自定义菜单 | 可以自己diy指令 | 已完成 |
|
||||
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
|
||||
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
|
||||
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
|
||||
|
||||
|
||||
## 2.安装说明:
|
||||
|
||||
| 前置插件 | 选装类型 |
|
||||
| :--- | :---
|
||||
| [CY_beibao](https://pan.ypshidifu.cn/s/Ygc0) | 可选
|
||||
| EssentialsX | 可选
|
||||
| 前置插件 | 选装类型 | 功能作用 |
|
||||
| :--- | :--- | :--- |
|
||||
| [CY_beibao](https://github.com/ypsdf1/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
|
||||
| EssentialsX | 可选 | 主流经济系统
|
||||
| [sdf1](https://github.com/ypsdf1/sdf1_plugn) | 可选,推荐 | CDK兑换系统
|
||||
|
||||
|
||||
### 2.1 下载
|
||||
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||
|
||||
### 2.2.2 配置文件
|
||||
### 2.2. 配置文件
|
||||
|
||||
#### 2.2.2.1 消息.txt
|
||||
#### 2.2.1 消息.txt
|
||||
此文件控制着整个插件的所有提示消息,您可以根据自己的需求修改,修改后需要执行 /sdf1_login reload 重载插件。支持[颜色代码](https://zh.minecraft.wiki/w/%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%BB%A3%E7%A0%81)<br>
|
||||
同时兼容`<b> </b>`标签,对段落加粗,`\n 和 <br>`进行段落换行
|
||||
|
||||
@@ -254,15 +259,16 @@ http-port=443
|
||||
|
||||
### 2.2.7 菜单.txt
|
||||
- 自定义菜单配置文件
|
||||
|
||||
```json
|
||||
# 服务器菜单
|
||||
# 使用类json格式
|
||||
// 支持使用://Java单行注释、#yml单行注释,/* */和<!--- --->块注释
|
||||
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
|
||||
|
||||
{
|
||||
标题:主城传送
|
||||
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
|
||||
权限类型:玩家
|
||||
权限类型:玩家
|
||||
图标:COMPASS
|
||||
}
|
||||
|
||||
@@ -332,6 +338,40 @@ http-port=443
|
||||
格子:28
|
||||
}
|
||||
```
|
||||
### 2.2.8 商品配置
|
||||
- 配置此处的文件,负责展示游戏内所销售的商品。<br>
|
||||
- 一个文件名,对应一个商品分类,使用md文件格式
|
||||
```
|
||||
# 建筑材料
|
||||
|
||||
| ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| STONE | 石头 | STONE | 10 | -1 | -1 | 0 | 0 |
|
||||
| COBBLESTONE | 圆石 | COBBLESTONE | 5 | -1 | -1 | 0 | 26 |
|
||||
| OAK_PLANKS | 橡木木板 | OAK_PLANKS | 8 | 3 | -1 | 0 | 0 |
|
||||
| BRICKS | 砖块 | BRICKS | 12 | -1 | -1 | 0 | 0 |
|
||||
| GLASS | 玻璃 | GLASS | 15 | 6 | -1 | 0 | 0 |
|
||||
| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK | 20 | 8 | -1 | 0 | 0 |
|
||||
| AMETHYST_BLOCK | 紫水晶 | AMETHYST_BLOCK | 10 | 0 | -1 | 0 | 999 |
|
||||
| OCHRE_FROGLIGHT | 赭黄蛙鸣灯 | OCHRE_FROGLIGHT | 100 | 0 | -1 | 0 | 1000 |
|
||||
|
||||
```
|
||||
|
||||
## 2.3 商城部分介绍
|
||||
1. 自助退款,买多买错,不用慌,一件退单很安心<br>
|
||||
2. 购物车,买的多怕买错,先加购物车,后续统一结算<br>
|
||||
3. 害怕订单找不到?一键查看历史订单<br>
|
||||
4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
|
||||
5. 想查看玩家消费记录?小手一点,明明白白<br>
|
||||
|
||||
|
||||
|
||||
|
||||
## 2.4 打印流水部分介绍
|
||||
1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天<br>
|
||||
2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删
|
||||
|
||||
|
||||
|
||||
## 3. 指令参数及介绍
|
||||
|
||||
@@ -342,7 +382,98 @@ http-port=443
|
||||
- l 登录账号<br>
|
||||
- sdf1_login del 删除玩家账号<br>
|
||||
- sdf1_login radio 停止播放广播<br>
|
||||
- sdf1_login radio reload 重载广播并重新打包资源包
|
||||
- sdf1_login radio reload 重载广播并重新打包资源包<br>
|
||||
- menu 开/关 是否开启菜单<br>
|
||||
- printer 打印流水数据,支持不带参数&带玩家名为参数<br>
|
||||
- sdf1_login printer 作用同printer<br>
|
||||
- sdf1_login shop 商城
|
||||
- protect 区域防护
|
||||
|
||||
|
||||
|
||||
## 4. 自定义菜单物品
|
||||
- 打开主界面,选择"我的"进入<br>
|
||||
- 将你的自定义物品与雪球交换<br>
|
||||
- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了
|
||||
|
||||
## 5. pvp战神榜
|
||||
- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域<br>
|
||||
- 并且有着网游同款的击杀通报<br>
|
||||
- 播报逻辑是:<br>
|
||||
- 3杀<br>
|
||||
- 5杀<br>
|
||||
- 5杀以上每杀一次都是一条播报<br>
|
||||
- 终结者通报,触发连杀的玩家被终结也会有全服通报
|
||||
|
||||
## 6. 区域防护
|
||||
- 通过此功能,您可在服务器里需要防护的地方(比如展示框不想被玩家随便破坏、主城不准玩家袭击等)加以防护。<br>
|
||||
- 还能够保护指定区域的玩家不受任何伤害。<br>
|
||||
|
||||
| 参数 | 效果 |
|
||||
| :---: | :---: |
|
||||
| 和平模式 | 所有敌对生物全部进虚空 |
|
||||
| 禁止袭击 | 立即清理玩家身上的袭击效果,传灾厄村民进虚空 |
|
||||
| 清除XX效果 | 立即清除玩家身上的效果
|
||||
| 给与xx效果 | 根据配置文件,给予玩家指定等级、时长的效果
|
||||
|
||||
## 生物属性对照表
|
||||
| 中文名称 | 类型 | 附加属性 |
|
||||
| :---: | :----: | :----: |
|
||||
| 溺尸 | 近战/远程 | 亡灵、水生 |
|
||||
| 骷髅 | 远程 | 亡灵 |
|
||||
| 流浪者 | 远程 | 亡灵、寒冷群系变种 |
|
||||
| 沼骸 | 远程 | 亡灵、沼泽变种 |
|
||||
| 焦骸 | 远程 | 亡灵、沙漠变种、免疫阳光 |
|
||||
| 苦力怕 | 近战(爆炸) | 无 |
|
||||
| 幻翼 | 远程(俯冲) | 亡灵、夜间生成 |
|
||||
| 掠夺者 | 远程 | 灾厄村民、袭击成员 |
|
||||
| 卫道士 | 近战 | 灾厄村民、袭击成员 |
|
||||
| 幻术师 | 远程 | 灾厄村民、Java专属、隐身分身 |
|
||||
| 旋风人 | 远程 | 试炼密室生成、免疫弹射物、强击退 |
|
||||
| 监守者 | 近战/声波远程 | 深暗之域、附带盲视效果 |
|
||||
| 远古守卫者 | 近战/激光远程 | 海底神殿、水下、附加挖掘疲劳 |
|
||||
| 守卫者 | 近战/激光远程 | 海底神殿、水下 |
|
||||
|
||||
|
||||
配置文件解析:<br>
|
||||
- 支持注释格式:`Java的//单行注释`、`yml的#单行注释`、`HTML的<!--- --->注释`和`Java/* */块注释`
|
||||
```txt
|
||||
# 区域: 主城区
|
||||
起点: world,-130,-713
|
||||
终点: world,-427,-560
|
||||
高度范围: 0-255
|
||||
|
||||
没收物品: 蘑菇,毒蘑菇 #没收哪些物品(末地推荐这两个)
|
||||
// 禁止放置方块 #保护区内,非区白/全白玩家禁止放置方块
|
||||
// 禁止破坏方块 # 同上
|
||||
禁止PVP # 玩家之间不能PVP
|
||||
禁止摔伤 # 摔落无法造成任何伤害
|
||||
禁止饥饿 # 不会消耗饱食度
|
||||
禁止一切伤害 # 任何怪物都无法造成伤害
|
||||
# 禁止丢弃物品
|
||||
# 禁止末影珍珠
|
||||
# 禁止使用弓箭
|
||||
# 禁止骑乘
|
||||
// 和平模式 // 所有的敌对生物全部传送进虚空
|
||||
禁止袭击
|
||||
禁止交互展示框 # 核心,任何玩家不能触碰展示框。区白/全白玩家除外
|
||||
// 清除所有效果 # 清理所有负面效果
|
||||
效果 村庄英雄 255 999999 # 给予玩家效果。格式为: 效果 效果名字 等级(最高255) 时间
|
||||
效果 生命提升 255 999999
|
||||
效果 抗性提升 255 999999
|
||||
效果 抗火 255 999999
|
||||
# 禁止药水
|
||||
禁止爆炸
|
||||
禁止使用物品: TNT
|
||||
# 效果: 夜视 1 999
|
||||
进入提示: 欢迎来到保护区 # 是否开启进入提示,即自定义提示
|
||||
离开提示: 已离开保护区
|
||||
没收提示: 你的违禁品已被没收
|
||||
通报批评: §c§l{player} §f在{area}违规携带违禁品,被没收 §e{count}§f件! # 通报违禁品。{player} 玩家名 {count}数量 {area}区域
|
||||
# 惩罚命令: ban {player} 60s 违规
|
||||
|
||||
|
||||
```
|
||||
|
||||
## 运行截图
|
||||
|
||||
@@ -393,6 +524,16 @@ http-port=443
|
||||
管理员-工单详情页面
|
||||

|
||||
|
||||
商城主界面
|
||||

|
||||
|
||||
商城购物车
|
||||

|
||||
|
||||
商城退单
|
||||

|
||||
|
||||
|
||||
## 4. 下载地址
|
||||
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login)
|
||||
- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -33,6 +33,7 @@
|
||||
<version>1.21.4-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.MilkBowl</groupId>
|
||||
<artifactId>VaultAPI</artifactId>
|
||||
|
||||
@@ -1,197 +1,197 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AFKManager {
|
||||
|
||||
// 玩家最后活动时间(空闲才开始计时)
|
||||
private final Map<UUID, Long> lastAction =
|
||||
new HashMap<>();
|
||||
// 每个玩家当前的警告阶段
|
||||
private final Map<UUID, Integer> lastStage =
|
||||
new HashMap<>();
|
||||
// 是否已进入挂机检测(false=还在活动,不计时)
|
||||
private final Map<UUID, Boolean> afkTracking =
|
||||
new HashMap<>();
|
||||
|
||||
private final ConfigManager config;
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public AFKManager(ConfigManager config,
|
||||
JavaPlugin plugin) {
|
||||
this.config = config;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void startCheck() {
|
||||
new BukkitRunnable() {
|
||||
public void run() {
|
||||
tick();
|
||||
}
|
||||
}.runTaskTimer(plugin, 20L, 20L);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (!config.afkEnabled) return;
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
String name = p.getName();
|
||||
UUID uuid = p.getUniqueId();
|
||||
|
||||
if (!isLoggedIn(name)) continue;
|
||||
if (config.getAfkWhitelist().contains(name))
|
||||
continue;
|
||||
|
||||
// 确保有记录
|
||||
if (!lastAction.containsKey(uuid)) {
|
||||
lastAction.put(uuid, now);
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
long last = lastAction.get(uuid);
|
||||
long idle = (now - last) / 1000;
|
||||
int timeout = config.afkTimeout;
|
||||
|
||||
// 还在活动范围内:不进入挂机检测
|
||||
if (idle < 60) {
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 超过60秒无活动:开始挂机检测
|
||||
afkTracking.put(uuid, true);
|
||||
long remain = timeout - idle;
|
||||
|
||||
// 已超时:踢出
|
||||
if (remain <= 0) {
|
||||
p.kickPlayer("§c§l你已挂机 "
|
||||
+ fmtSec(idle) + ",被踢出");
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int stage = lastStage.getOrDefault(uuid, 0);
|
||||
|
||||
// 阶段3:最后10秒
|
||||
if (remain <= 10) {
|
||||
if (stage < 3) {
|
||||
lastStage.put(uuid, 3);
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + remain + " 秒踢出",
|
||||
0, 22, 3);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段2:1分钟~10秒
|
||||
if (remain <= 60) {
|
||||
if (stage < 2) {
|
||||
lastStage.put(uuid, 2);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + fmtSec(remain) + " 踢出",
|
||||
5, 25, 5);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段1:3分钟提醒
|
||||
if (remain <= 180 && stage < 1) {
|
||||
lastStage.put(uuid, 1);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§f还剩 " + fmtSec(remain) + " 踢出",
|
||||
10, 60, 20);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int delay = i * 10;
|
||||
Bukkit.getScheduler().runTaskLater(
|
||||
plugin, () -> {
|
||||
if (p.isOnline()) playSound(p);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (remain > 180) {
|
||||
lastStage.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家有活动时调用(移动/交互/放置/挖掘等)
|
||||
* 重置计时,退出挂机检测
|
||||
*/
|
||||
public void recordAction(UUID uuid) {
|
||||
lastAction.put(uuid, System.currentTimeMillis());
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家下线时调用
|
||||
* 彻底停止计时,清理所有数据
|
||||
*/
|
||||
public void remove(UUID uuid) {
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家当前是否在挂机检测中
|
||||
*/
|
||||
public boolean isAfkTracking(UUID uuid) {
|
||||
return afkTracking.getOrDefault(uuid, false);
|
||||
}
|
||||
|
||||
private boolean isLoggedIn(String name) {
|
||||
return ((Main) plugin).getLoggedIn()
|
||||
.contains(name);
|
||||
}
|
||||
|
||||
private void playSound(Player p) {
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_CHIME,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_BASS,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_PLING,
|
||||
1.0f, 0.79f);
|
||||
}
|
||||
|
||||
private String fmtSec(long sec) {
|
||||
if (sec < 60) return sec + "秒";
|
||||
long m = sec / 60;
|
||||
long s = sec % 60;
|
||||
return m + "分" + (s > 0 ? s + "秒" : "");
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AFKManager {
|
||||
|
||||
// 玩家最后活动时间(空闲才开始计时)
|
||||
private final Map<UUID, Long> lastAction =
|
||||
new HashMap<>();
|
||||
// 每个玩家当前的警告阶段
|
||||
private final Map<UUID, Integer> lastStage =
|
||||
new HashMap<>();
|
||||
// 是否已进入挂机检测(false=还在活动,不计时)
|
||||
private final Map<UUID, Boolean> afkTracking =
|
||||
new HashMap<>();
|
||||
|
||||
private final ConfigManager config;
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public AFKManager(ConfigManager config,
|
||||
JavaPlugin plugin) {
|
||||
this.config = config;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void startCheck() {
|
||||
new BukkitRunnable() {
|
||||
public void run() {
|
||||
tick();
|
||||
}
|
||||
}.runTaskTimer(plugin, 20L, 20L);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (!config.afkEnabled) return;
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
String name = p.getName();
|
||||
UUID uuid = p.getUniqueId();
|
||||
|
||||
if (!isLoggedIn(name)) continue;
|
||||
if (config.getAfkWhitelist().contains(name))
|
||||
continue;
|
||||
|
||||
// 确保有记录
|
||||
if (!lastAction.containsKey(uuid)) {
|
||||
lastAction.put(uuid, now);
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
long last = lastAction.get(uuid);
|
||||
long idle = (now - last) / 1000;
|
||||
int timeout = config.afkTimeout;
|
||||
|
||||
// 还在活动范围内:不进入挂机检测
|
||||
if (idle < 60) {
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 超过60秒无活动:开始挂机检测
|
||||
afkTracking.put(uuid, true);
|
||||
long remain = timeout - idle;
|
||||
|
||||
// 已超时:踢出
|
||||
if (remain <= 0) {
|
||||
p.kickPlayer("§c§l你已挂机 "
|
||||
+ fmtSec(idle) + ",被踢出");
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int stage = lastStage.getOrDefault(uuid, 0);
|
||||
|
||||
// 阶段3:最后10秒
|
||||
if (remain <= 10) {
|
||||
if (stage < 3) {
|
||||
lastStage.put(uuid, 3);
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + remain + " 秒踢出",
|
||||
0, 22, 3);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段2:1分钟~10秒
|
||||
if (remain <= 60) {
|
||||
if (stage < 2) {
|
||||
lastStage.put(uuid, 2);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + fmtSec(remain) + " 踢出",
|
||||
5, 25, 5);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段1:3分钟提醒
|
||||
if (remain <= 180 && stage < 1) {
|
||||
lastStage.put(uuid, 1);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§f还剩 " + fmtSec(remain) + " 踢出",
|
||||
10, 60, 20);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int delay = i * 10;
|
||||
Bukkit.getScheduler().runTaskLater(
|
||||
plugin, () -> {
|
||||
if (p.isOnline()) playSound(p);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (remain > 180) {
|
||||
lastStage.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家有活动时调用(移动/交互/放置/挖掘等)
|
||||
* 重置计时,退出挂机检测
|
||||
*/
|
||||
public void recordAction(UUID uuid) {
|
||||
lastAction.put(uuid, System.currentTimeMillis());
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家下线时调用
|
||||
* 彻底停止计时,清理所有数据
|
||||
*/
|
||||
public void remove(UUID uuid) {
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家当前是否在挂机检测中
|
||||
*/
|
||||
public boolean isAfkTracking(UUID uuid) {
|
||||
return afkTracking.getOrDefault(uuid, false);
|
||||
}
|
||||
|
||||
private boolean isLoggedIn(String name) {
|
||||
return ((Main) plugin).getLoggedIn()
|
||||
.contains(name);
|
||||
}
|
||||
|
||||
private void playSound(Player p) {
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_CHIME,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_BASS,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_PLING,
|
||||
1.0f, 0.79f);
|
||||
}
|
||||
|
||||
private String fmtSec(long sec) {
|
||||
if (sec < 60) return sec + "秒";
|
||||
long m = sec / 60;
|
||||
long s = sec % 60;
|
||||
return m + "分" + (s > 0 ? s + "秒" : "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +1,146 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AccountRequestManager {
|
||||
|
||||
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
|
||||
|
||||
public static class Request {
|
||||
public final int id;
|
||||
public final String requester;
|
||||
public final String target;
|
||||
public final String ip;
|
||||
public final long time;
|
||||
public String status;
|
||||
public String denyReason;
|
||||
|
||||
public Request(int id, String requester, String target, String ip) {
|
||||
this.id = id;
|
||||
this.requester = requester;
|
||||
this.target = target;
|
||||
this.ip = ip;
|
||||
this.time = System.currentTimeMillis();
|
||||
this.status = "PENDING";
|
||||
this.denyReason = "";
|
||||
}
|
||||
}
|
||||
|
||||
private final Main plugin;
|
||||
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
|
||||
private int nextId = 1;
|
||||
private ApprovalMode mode = ApprovalMode.MANUAL;
|
||||
private int autoDelayMinutes = 60;
|
||||
|
||||
public AccountRequestManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public ApprovalMode getMode() { return mode; }
|
||||
|
||||
public void setMode(String m) {
|
||||
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
|
||||
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
|
||||
}
|
||||
|
||||
public void setAutoDelayMinutes(int min) {
|
||||
this.autoDelayMinutes = min;
|
||||
}
|
||||
|
||||
public Request createRequest(String requester, String target, String ip) {
|
||||
int id = nextId++;
|
||||
Request req = new Request(id, requester, target, ip);
|
||||
requests.put(id, req);
|
||||
pendingByPlayer.put(requester, id);
|
||||
|
||||
if (mode == ApprovalMode.AUTO) {
|
||||
scheduleAutoApprove(id);
|
||||
} else if (mode == ApprovalMode.HYBRID) {
|
||||
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
|
||||
if (ip != null && ip.equals(targetIp)) {
|
||||
scheduleAutoApprove(id);
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
private void scheduleAutoApprove(int id) {
|
||||
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
|
||||
Request req = requests.get(id);
|
||||
if (req == null) return;
|
||||
String requesterName = req.requester;
|
||||
String targetName = req.target;
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
Request r = requests.get(id);
|
||||
if (r != null && "PENDING".equals(r.status)) {
|
||||
approveRequest(id);
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
|
||||
Player requester = Bukkit.getPlayer(requesterName);
|
||||
if (requester != null && requester.isOnline()) {
|
||||
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
|
||||
}
|
||||
}
|
||||
}, delayTicks);
|
||||
}
|
||||
|
||||
public boolean approveRequest(int id) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "APPROVED";
|
||||
plugin.getDb().deleteUser(req.target);
|
||||
pendingByPlayer.remove(req.requester);
|
||||
// 清背包+传送出生点+踢出
|
||||
Player target = Bukkit.getPlayer(req.target);
|
||||
if (target != null && target.isOnline()) {
|
||||
target.getInventory().clear();
|
||||
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
|
||||
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
|
||||
target.teleport(target.getWorld().getSpawnLocation());
|
||||
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean denyRequest(int id, String reason) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "DENIED";
|
||||
req.denyReason = reason;
|
||||
pendingByPlayer.remove(req.requester);
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<Request> getPendingRequests() {
|
||||
List<Request> list = new ArrayList<>();
|
||||
for (Request r : requests.values()) {
|
||||
if ("PENDING".equals(r.status)) list.add(r);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Request getRequest(int id) {
|
||||
return requests.get(id);
|
||||
}
|
||||
|
||||
public boolean hasPending(String player) {
|
||||
return pendingByPlayer.containsKey(player);
|
||||
}
|
||||
|
||||
public void cancelPending(String player) {
|
||||
Integer id = pendingByPlayer.remove(player);
|
||||
if (id != null) {
|
||||
Request r = requests.get(id);
|
||||
if (r != null) r.status = "CANCELLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AccountRequestManager {
|
||||
|
||||
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
|
||||
|
||||
public static class Request {
|
||||
public final int id;
|
||||
public final String requester;
|
||||
public final String target;
|
||||
public final String ip;
|
||||
public final long time;
|
||||
public String status;
|
||||
public String denyReason;
|
||||
|
||||
public Request(int id, String requester, String target, String ip) {
|
||||
this.id = id;
|
||||
this.requester = requester;
|
||||
this.target = target;
|
||||
this.ip = ip;
|
||||
this.time = System.currentTimeMillis();
|
||||
this.status = "PENDING";
|
||||
this.denyReason = "";
|
||||
}
|
||||
}
|
||||
|
||||
private final Main plugin;
|
||||
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
|
||||
private int nextId = 1;
|
||||
private ApprovalMode mode = ApprovalMode.MANUAL;
|
||||
private int autoDelayMinutes = 60;
|
||||
|
||||
public AccountRequestManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public ApprovalMode getMode() { return mode; }
|
||||
|
||||
public void setMode(String m) {
|
||||
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
|
||||
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
|
||||
}
|
||||
|
||||
public void setAutoDelayMinutes(int min) {
|
||||
this.autoDelayMinutes = min;
|
||||
}
|
||||
|
||||
public Request createRequest(String requester, String target, String ip) {
|
||||
int id = nextId++;
|
||||
Request req = new Request(id, requester, target, ip);
|
||||
requests.put(id, req);
|
||||
pendingByPlayer.put(requester, id);
|
||||
|
||||
if (mode == ApprovalMode.AUTO) {
|
||||
scheduleAutoApprove(id);
|
||||
} else if (mode == ApprovalMode.HYBRID) {
|
||||
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
|
||||
if (ip != null && ip.equals(targetIp)) {
|
||||
scheduleAutoApprove(id);
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
private void scheduleAutoApprove(int id) {
|
||||
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
|
||||
Request req = requests.get(id);
|
||||
if (req == null) return;
|
||||
String requesterName = req.requester;
|
||||
String targetName = req.target;
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
Request r = requests.get(id);
|
||||
if (r != null && "PENDING".equals(r.status)) {
|
||||
approveRequest(id);
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
|
||||
Player requester = Bukkit.getPlayer(requesterName);
|
||||
if (requester != null && requester.isOnline()) {
|
||||
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
|
||||
}
|
||||
}
|
||||
}, delayTicks);
|
||||
}
|
||||
|
||||
public boolean approveRequest(int id) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "APPROVED";
|
||||
plugin.getDb().deleteUser(req.target);
|
||||
pendingByPlayer.remove(req.requester);
|
||||
// 清背包+传送出生点+踢出
|
||||
Player target = Bukkit.getPlayer(req.target);
|
||||
if (target != null && target.isOnline()) {
|
||||
target.getInventory().clear();
|
||||
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
|
||||
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
|
||||
target.teleport(target.getWorld().getSpawnLocation());
|
||||
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean denyRequest(int id, String reason) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "DENIED";
|
||||
req.denyReason = reason;
|
||||
pendingByPlayer.remove(req.requester);
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<Request> getPendingRequests() {
|
||||
List<Request> list = new ArrayList<>();
|
||||
for (Request r : requests.values()) {
|
||||
if ("PENDING".equals(r.status)) list.add(r);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Request getRequest(int id) {
|
||||
return requests.get(id);
|
||||
}
|
||||
|
||||
public boolean hasPending(String player) {
|
||||
return pendingByPlayer.containsKey(player);
|
||||
}
|
||||
|
||||
public void cancelPending(String player) {
|
||||
Integer id = pendingByPlayer.remove(player);
|
||||
if (id != null) {
|
||||
Request r = requests.get(id);
|
||||
if (r != null) r.status = "CANCELLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,697 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
|
||||
public class BondManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Connection db;
|
||||
|
||||
// ===== 交易类型常量 =====
|
||||
public static final String TX_ADMIN_GIVE = "admin_give";
|
||||
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
|
||||
public static final String TX_TRANSFER_OUT = "transfer_out";
|
||||
public static final String TX_TRANSFER_IN = "transfer_in";
|
||||
public static final String TX_REDEEM = "redeem";
|
||||
public static final String TX_SHOP_BUY = "shop_buy";
|
||||
public static final String TX_DAILY_SIGN = "daily_sign";
|
||||
public static final String TX_FREEZE = "freeze";
|
||||
public static final String TX_UNFREEZE = "unfreeze";
|
||||
|
||||
|
||||
public static final int MAX_WATER_DAYS = 14;
|
||||
public static final int ADMIN_MAX_WATER_DAYS = 30;
|
||||
|
||||
public BondManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initDB();
|
||||
}
|
||||
|
||||
// ======================== 数据库初始化 ========================
|
||||
|
||||
private void initDB() {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
File dbFile = new File(
|
||||
plugin.getDataFolder(), "bond.db");
|
||||
db = DriverManager.getConnection(
|
||||
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
Statement st = db.createStatement();
|
||||
st.execute("PRAGMA journal_mode=WAL");
|
||||
|
||||
// 债券余额(含冻结状态)
|
||||
st.execute("CREATE TABLE IF NOT EXISTS bonds ("
|
||||
+ "player_name TEXT PRIMARY KEY,"
|
||||
+ "amount INTEGER DEFAULT 0,"
|
||||
+ "status TEXT DEFAULT 'normal')");
|
||||
|
||||
// 兼容旧表
|
||||
try {
|
||||
st.execute("ALTER TABLE bonds "
|
||||
+ "ADD COLUMN status "
|
||||
+ "TEXT DEFAULT 'normal'");
|
||||
} catch (SQLException ignored) {}
|
||||
|
||||
// 旧流水(保留兼容)
|
||||
st.execute("CREATE TABLE IF NOT EXISTS bond_log ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "amount INTEGER NOT NULL,"
|
||||
+ "reason TEXT DEFAULT '',"
|
||||
+ "time INTEGER DEFAULT 0)");
|
||||
|
||||
// 新交易流水表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "bond_transaction ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "type TEXT NOT NULL,"
|
||||
+ "amount INTEGER NOT NULL,"
|
||||
+ "target_player TEXT DEFAULT '',"
|
||||
+ "operator TEXT DEFAULT '',"
|
||||
+ "reason TEXT DEFAULT '',"
|
||||
+ "balance_after INTEGER DEFAULT 0,"
|
||||
+ "time INTEGER DEFAULT 0)");
|
||||
|
||||
try {
|
||||
st.execute("CREATE INDEX "
|
||||
+ "IF NOT EXISTS idx_tx_pt "
|
||||
+ "ON bond_transaction("
|
||||
+ "player_name, time)");
|
||||
} catch (SQLException ignored) {}
|
||||
try {
|
||||
st.execute("ALTER TABLE bond_transaction "
|
||||
+ "ADD COLUMN balance_before "
|
||||
+ "INTEGER DEFAULT 0");
|
||||
} catch (SQLException ignored) {}
|
||||
|
||||
|
||||
st.close();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"[Bond] DB init failed: "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 账户状态 ========================
|
||||
|
||||
public boolean isFrozen(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT status FROM bonds "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
String s = rs.getString("status");
|
||||
rs.close(); ps.close();
|
||||
return "frozen".equals(s);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getAccountStatus(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT status FROM bonds "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
String s = rs.getString("status");
|
||||
rs.close(); ps.close();
|
||||
return s != null ? s : "normal";
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
return "normal";
|
||||
}
|
||||
|
||||
public void freezeAccount(String player) {
|
||||
ensureAccount(player);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"UPDATE bonds SET status='frozen' "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
}
|
||||
|
||||
public void unfreezeAccount(String player) {
|
||||
ensureAccount(player);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"UPDATE bonds SET status='normal' "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
}
|
||||
|
||||
private void ensureAccount(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR IGNORE INTO bonds "
|
||||
+ "(player_name,amount,status) "
|
||||
+ "VALUES (?,0,'normal')");
|
||||
ps.setString(1, player);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
}
|
||||
|
||||
// ======================== 余额查询 ========================
|
||||
|
||||
public int getBonds(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT amount FROM bonds "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
int v = 0;
|
||||
if (rs.next()) v = rs.getInt("amount");
|
||||
rs.close(); ps.close();
|
||||
return v;
|
||||
} catch (SQLException e) { return 0; }
|
||||
}
|
||||
|
||||
/** 该玩家是否在 bond 表中有真实记录 */
|
||||
public boolean hasBondAccount(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT 1 FROM bonds "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND amount > 0");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
boolean exists = rs.next();
|
||||
rs.close(); ps.close();
|
||||
return exists;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 增加债券 ========================
|
||||
|
||||
public int addBonds(String player, int amount) {
|
||||
return addBondsInternal(player, amount,
|
||||
TX_ADMIN_GIVE, "", "",
|
||||
"管理员发放");
|
||||
}
|
||||
|
||||
public int addBonds(String player, int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
if (isFrozen(player)) return -1;
|
||||
return addBondsInternal(player, amount,
|
||||
type, targetPlayer, operator, reason);
|
||||
}
|
||||
|
||||
private int addBondsInternal(String player,
|
||||
int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
int before = getBonds(player);
|
||||
int after = before + amount;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bonds "
|
||||
+ "(player_name,amount,status)"
|
||||
+ " VALUES (?,?, 'normal') "
|
||||
+ "ON CONFLICT(player_name) "
|
||||
+ "DO UPDATE SET amount=?");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, after);
|
||||
ps.setInt(3, after);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
logTransaction(player, type, amount,
|
||||
targetPlayer, operator, reason,
|
||||
before, after);
|
||||
logLegacy(player, amount);
|
||||
return after;
|
||||
}
|
||||
|
||||
// ======================== 扣除债券 ========================
|
||||
|
||||
|
||||
|
||||
private boolean deductBondsInternal(String player,
|
||||
int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
int before = getBonds(player);
|
||||
if (before < amount) return false;
|
||||
int after = before - amount;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"UPDATE bonds SET amount=? "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setInt(1, after);
|
||||
ps.setString(2, player);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
logTransaction(player, type, amount,
|
||||
targetPlayer, operator, reason,
|
||||
before, after);
|
||||
logLegacy(player, -amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======================== 流水写入 ========================
|
||||
|
||||
private void logTransaction(String player,
|
||||
String type,
|
||||
int amount,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason,
|
||||
int balanceBefore,
|
||||
int balanceAfter) {
|
||||
try {
|
||||
// 合并模式:shop_buy 30秒内同商品合并
|
||||
if ("shop_buy".equals(type)
|
||||
&& reason != null
|
||||
&& !reason.isEmpty()) {
|
||||
long mergeWindow =
|
||||
System.currentTimeMillis() - 30000L;
|
||||
PreparedStatement chk =
|
||||
db.prepareStatement(
|
||||
"SELECT id, amount "
|
||||
+ "FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND type=? "
|
||||
+ "AND reason=? "
|
||||
+ "AND time > ? "
|
||||
+ "ORDER BY time DESC "
|
||||
+ "LIMIT 1");
|
||||
chk.setString(1, player);
|
||||
chk.setString(2, type);
|
||||
chk.setString(3, reason);
|
||||
chk.setLong(4, mergeWindow);
|
||||
ResultSet rs = chk.executeQuery();
|
||||
if (rs.next()) {
|
||||
int existId = rs.getInt("id");
|
||||
int existAmt = rs.getInt("amount");
|
||||
rs.close();
|
||||
chk.close();
|
||||
PreparedStatement upd =
|
||||
db.prepareStatement(
|
||||
"UPDATE bond_transaction "
|
||||
+ "SET amount=?, "
|
||||
+ "balance_after=?, "
|
||||
+ "time=? "
|
||||
+ "WHERE id=?");
|
||||
upd.setInt(1, existAmt + amount);
|
||||
upd.setInt(2, balanceAfter);
|
||||
upd.setLong(3,
|
||||
System.currentTimeMillis());
|
||||
upd.setInt(4, existId);
|
||||
upd.executeUpdate();
|
||||
upd.close();
|
||||
return;
|
||||
}
|
||||
rs.close();
|
||||
chk.close();
|
||||
}
|
||||
|
||||
// 原始插入
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bond_transaction "
|
||||
+ "(player_name, type, amount,"
|
||||
+ " target_player, operator,"
|
||||
+ " reason, balance_before,"
|
||||
+ " balance_after, time)"
|
||||
+ " VALUES (?,?,?,?,?,?,?,?,?)");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, type);
|
||||
ps.setInt(3, amount);
|
||||
ps.setString(4,
|
||||
targetPlayer != null ? targetPlayer : "");
|
||||
ps.setString(5,
|
||||
operator != null ? operator : "");
|
||||
ps.setString(6,
|
||||
reason != null ? reason : "");
|
||||
ps.setInt(7, balanceBefore);
|
||||
ps.setInt(8, balanceAfter);
|
||||
ps.setLong(9, System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ======================== 扣除债券 ========================
|
||||
|
||||
/** 完整版:含冻结检查 + 流水记录 */
|
||||
public boolean deductBonds(String player, int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
if (isFrozen(player)) return false;
|
||||
return deductBondsInternal(player, amount,
|
||||
type, targetPlayer, operator, reason);
|
||||
}
|
||||
|
||||
public boolean checkoutCart(String player,
|
||||
String[] itemNames,
|
||||
int[] subtotals) {
|
||||
int total = 0;
|
||||
for (int s : subtotals) total += s;
|
||||
if (isFrozen(player)) return false;
|
||||
|
||||
int before = getBonds(player);
|
||||
if (before < total) return false;
|
||||
int after = before - total;
|
||||
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bonds "
|
||||
+ "(player_name,amount,status) "
|
||||
+ "VALUES (?,?,'normal') "
|
||||
+ "ON CONFLICT(player_name) "
|
||||
+ "DO UPDATE SET amount=?");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, after);
|
||||
ps.setInt(3, after);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
int running = before;
|
||||
for (int i = 0; i < itemNames.length; i++) {
|
||||
int next = running - subtotals[i];
|
||||
logTransaction(player, "shop_buy", subtotals[i],
|
||||
"", "商店系统",
|
||||
"购物车购买-" + itemNames[i],
|
||||
running, next);
|
||||
running = next;
|
||||
}
|
||||
|
||||
logLegacy(player, -total);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean setBonds(String player, int amount) {
|
||||
ensureAccount(player);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bonds "
|
||||
+ "(player_name,amount,status) "
|
||||
+ "VALUES (?,?, 'normal') "
|
||||
+ "ON CONFLICT(player_name) "
|
||||
+ "DO UPDATE SET amount=?");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, amount);
|
||||
ps.setInt(3, amount);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======================== 转账 ========================
|
||||
|
||||
public TransferResult transfer(String from,
|
||||
String to,
|
||||
int amount) {
|
||||
if (from.equalsIgnoreCase(to))
|
||||
return new TransferResult(false,
|
||||
"不能给自己转账");
|
||||
if (isFrozen(from))
|
||||
return new TransferResult(false,
|
||||
"你的账户已被冻结");
|
||||
if (isFrozen(to))
|
||||
return new TransferResult(false,
|
||||
to + " 的账户已被冻结");
|
||||
if (isSameIP(from, to))
|
||||
return new TransferResult(false,
|
||||
"不能与同IP玩家转账");
|
||||
|
||||
if (!deductBondsInternal(from, amount,
|
||||
TX_TRANSFER_OUT, to, from,
|
||||
"转账给" + to))
|
||||
return new TransferResult(false, "债券不足");
|
||||
|
||||
addBondsInternal(to, amount,
|
||||
TX_TRANSFER_IN, from, from,
|
||||
"收到来自" + from + "的转账");
|
||||
|
||||
return new TransferResult(true, "转账成功");
|
||||
}
|
||||
|
||||
// ======================== 同IP检测 ========================
|
||||
|
||||
public boolean isSameIP(String p1, String p2) {
|
||||
String ip1 = getPlayerIPAny(p1);
|
||||
String ip2 = getPlayerIPAny(p2);
|
||||
if (ip1 == null || ip2 == null) return false;
|
||||
return ip1.equals(ip2);
|
||||
}
|
||||
|
||||
/** 获取同IP下的所有其他在线玩家 */
|
||||
public List<String> getSameIPPlayers(String player) {
|
||||
String ip = getPlayerIPAny(player);
|
||||
if (ip == null) return Collections.emptyList();
|
||||
List<String> list = new ArrayList<>();
|
||||
for (Player p :
|
||||
plugin.getServer().getOnlinePlayers()) {
|
||||
String pIp = plugin.getPlayerIP(p);
|
||||
if (pIp != null && pIp.equals(ip)
|
||||
&& !p.getName()
|
||||
.equalsIgnoreCase(player)) {
|
||||
list.add(p.getName());
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String getPlayerIPAny(String player) {
|
||||
// 在线:直接取
|
||||
Player p = plugin.getServer()
|
||||
.getPlayerExact(player);
|
||||
if (p != null) {
|
||||
String ip = plugin.getPlayerIP(p);
|
||||
if (ip != null) return ip;
|
||||
}
|
||||
// 离线:查主库
|
||||
try {
|
||||
Object v = plugin.getDb()
|
||||
.getField(player, "last_login_ip");
|
||||
if (v != null) return v.toString();
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
Object v = plugin.getDb()
|
||||
.getField(player, "register_ip");
|
||||
if (v != null) return v.toString();
|
||||
} catch (Exception ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ======================== 流水查询 ========================
|
||||
|
||||
public List<Map<String, Object>> getTransactions(
|
||||
String player, int days) {
|
||||
days = Math.min(days, MAX_WATER_DAYS);
|
||||
long cutoff = System.currentTimeMillis()
|
||||
- (long) days * 24 * 60 * 60 * 1000;
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND time>=? "
|
||||
+ "ORDER BY time DESC");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, cutoff);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("amount",
|
||||
rs.getInt("amount"));
|
||||
row.put("target_player",
|
||||
rs.getString("target_player"));
|
||||
row.put("operator",
|
||||
rs.getString("operator"));
|
||||
row.put("reason",
|
||||
rs.getString("reason"));
|
||||
row.put("balance_after",
|
||||
rs.getInt("balance_after"));
|
||||
row.put("balance_before", rs.getInt("balance_before"));
|
||||
row.put("time", rs.getLong("time"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
return list;
|
||||
}
|
||||
// 放在 getTransactions 方法后面:
|
||||
|
||||
/**
|
||||
* 获取该玩家全部流水(不限时间,供打印使用)
|
||||
*/
|
||||
public List<Map<String, Object>>
|
||||
getAllTransactions(String player) {
|
||||
List<Map<String, Object>> list =
|
||||
new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "ORDER BY time ASC");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("player_name",
|
||||
rs.getString("player_name"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("amount",
|
||||
rs.getInt("amount"));
|
||||
row.put("target_player",
|
||||
rs.getString("target_player"));
|
||||
row.put("operator",
|
||||
rs.getString("operator"));
|
||||
row.put("reason",
|
||||
rs.getString("reason"));
|
||||
row.put("balance_after",
|
||||
rs.getInt("balance_after"));
|
||||
row.put("balance_before", rs.getInt("balance_before"));
|
||||
row.put("time", rs.getLong("time"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
// 放在 getAllTransactions 方法后面:
|
||||
|
||||
/**
|
||||
* 获取所有有流水记录的玩家名(去重)
|
||||
*/
|
||||
public List<String> getAllPlayerNames() {
|
||||
List<String> list = new ArrayList<>();
|
||||
try {
|
||||
Statement st = db.createStatement();
|
||||
ResultSet rs = st.executeQuery(
|
||||
"SELECT DISTINCT player_name "
|
||||
+ "FROM bond_transaction "
|
||||
+ "ORDER BY player_name ASC");
|
||||
while (rs.next()) {
|
||||
list.add(rs.getString("player_name"));
|
||||
}
|
||||
rs.close(); st.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>>
|
||||
getTransferTransactions(
|
||||
String player, int days) {
|
||||
days = Math.min(days, MAX_WATER_DAYS);
|
||||
long cutoff = System.currentTimeMillis()
|
||||
- (long) days * 24 * 60 * 60 * 1000;
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND time>=? "
|
||||
+ "AND (type=? OR type=?) "
|
||||
+ "ORDER BY time DESC");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, cutoff);
|
||||
ps.setString(3, TX_TRANSFER_OUT);
|
||||
ps.setString(4, TX_TRANSFER_IN);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("amount",
|
||||
rs.getInt("amount"));
|
||||
row.put("target_player",
|
||||
rs.getString("target_player"));
|
||||
row.put("reason",
|
||||
rs.getString("reason"));
|
||||
row.put("balance_after",
|
||||
rs.getInt("balance_after"));
|
||||
row.put("balance_before", rs.getInt("balance_before"));
|
||||
row.put("time", rs.getLong("time"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
private void logLegacy(String player, int amount) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bond_log "
|
||||
+ "(player_name,amount,time) "
|
||||
+ "VALUES (?,?,?)");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, amount);
|
||||
ps.setLong(3,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
}
|
||||
|
||||
public Map<String, Object> getLog(
|
||||
String player, int limit) {
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("balance", getBonds(player));
|
||||
return info;
|
||||
}
|
||||
|
||||
// ======================== 转账结果 ========================
|
||||
|
||||
public static class TransferResult {
|
||||
public final boolean success;
|
||||
public final String message;
|
||||
public TransferResult(boolean success,
|
||||
String message) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class BondPrinter {
|
||||
|
||||
private final Main plugin;
|
||||
private final File outputDir;
|
||||
|
||||
private static final SimpleDateFormat FILE_SDF =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd_HH-mm-ss");
|
||||
private static final SimpleDateFormat CELL_SDF =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private static final LinkedHashMap<String, String>
|
||||
TYPE_NAMES = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
TYPE_NAMES.put("admin_give", "管理员发放");
|
||||
TYPE_NAMES.put("admin_deduct", "管理员扣除");
|
||||
TYPE_NAMES.put("transfer_out", "转出");
|
||||
TYPE_NAMES.put("transfer_in", "转入");
|
||||
TYPE_NAMES.put("redeem", "CDK兑换");
|
||||
TYPE_NAMES.put("shop_buy", "商城购买");
|
||||
TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上
|
||||
TYPE_NAMES.put("daily_sign", "签到奖励");
|
||||
TYPE_NAMES.put("freeze", "冻结");
|
||||
TYPE_NAMES.put("unfreeze", "解冻");
|
||||
|
||||
}
|
||||
|
||||
public BondPrinter(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.outputDir = new File(
|
||||
plugin.getDataFolder(), "printer");
|
||||
if (!this.outputDir.exists()) {
|
||||
this.outputDir.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 打印单个玩家 ============
|
||||
|
||||
public void printPlayer(String player,
|
||||
CommandSender sender) {
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(plugin,
|
||||
() -> doPrintPlayer(
|
||||
player, sender));
|
||||
}
|
||||
|
||||
private void doPrintPlayer(String player,
|
||||
CommandSender sender) {
|
||||
try {
|
||||
List<Map<String, Object>> txs =
|
||||
plugin.getBonds()
|
||||
.getAllTransactions(player);
|
||||
|
||||
String fileName = player + "_"
|
||||
+ FILE_SDF.format(new Date())
|
||||
+ ".xls";
|
||||
File file = new File(outputDir, fileName);
|
||||
|
||||
deleteOld(player);
|
||||
|
||||
PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(file),
|
||||
StandardCharsets.UTF_8));
|
||||
try {
|
||||
writeXmlHeader(pw,
|
||||
escXml(player));
|
||||
writeXmlRow(pw,
|
||||
new String[]{
|
||||
"时间", "类型",
|
||||
"金额", "对象",
|
||||
"操作者", "理由",
|
||||
"操作前余额",
|
||||
"操作后余额"});
|
||||
|
||||
for (Map<String, Object> tx : txs) {
|
||||
writeXmlRow(pw, new String[]{
|
||||
txTime(tx),
|
||||
txTypeName(tx),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"amount")),
|
||||
txStr(tx,
|
||||
"target_player"),
|
||||
txStr(tx, "operator"),
|
||||
txStr(tx, "reason"),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_before")),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_after"))
|
||||
});
|
||||
}
|
||||
|
||||
writeXmlFooter(pw);
|
||||
pw.flush();
|
||||
} finally {
|
||||
pw.close();
|
||||
}
|
||||
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§a[打印] §f"
|
||||
+ player + " §a流水已导出");
|
||||
sender.sendMessage("§7文件: §f"
|
||||
+ fileName);
|
||||
sender.sendMessage("§7共 §f"
|
||||
+ txs.size()
|
||||
+ " §7条,§724小时后删除");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§c导出失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
plugin.getLogger().severe(
|
||||
"[Printer] " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 打印全服 ============
|
||||
|
||||
public void printAll(CommandSender sender) {
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(plugin,
|
||||
() -> doPrintAll(sender));
|
||||
}
|
||||
|
||||
private void doPrintAll(CommandSender sender) {
|
||||
try {
|
||||
List<String> players =
|
||||
plugin.getBonds()
|
||||
.getAllPlayerNames();
|
||||
if (players.isEmpty()) {
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§c没有流水数据");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String fileName = "全服流水_"
|
||||
+ FILE_SDF.format(new Date())
|
||||
+ ".xls";
|
||||
File file = new File(outputDir, fileName);
|
||||
|
||||
// ===== 收集所有流水 =====
|
||||
// key = 时间戳+玩家+金额,用于去重
|
||||
// value = 一行数据
|
||||
List<String[]> allRows = new ArrayList<>();
|
||||
Set<String> seenKeys = new HashSet<>();
|
||||
|
||||
for (String player : players) {
|
||||
List<Map<String, Object>> txs =
|
||||
plugin.getBonds()
|
||||
.getAllTransactions(player);
|
||||
for (Map<String, Object> tx : txs) {
|
||||
int txId = ((Number) tx.get("id"))
|
||||
.intValue();
|
||||
String dedupKey =
|
||||
String.valueOf(txId);
|
||||
if (!seenKeys.add(dedupKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String timeStr = txTime(tx);
|
||||
long timeMs = 0;
|
||||
Object t = tx.get("time");
|
||||
if (t != null) {
|
||||
timeMs =
|
||||
((Number) t).longValue();
|
||||
}
|
||||
|
||||
allRows.add(new String[]{
|
||||
player,
|
||||
timeStr,
|
||||
txTypeName(tx),
|
||||
String.valueOf(
|
||||
txInt(tx, "amount")),
|
||||
txStr(tx, "target_player"),
|
||||
txStr(tx, "operator"),
|
||||
txStr(tx, "reason"),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_before")),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_after")),
|
||||
// 额外存时间戳用于排序
|
||||
String.valueOf(timeMs)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 按时间戳排序 =====
|
||||
allRows.sort((a, b) -> {
|
||||
long ta = Long.parseLong(
|
||||
a[a.length - 1]);
|
||||
long tb = Long.parseLong(
|
||||
b[b.length - 1]);
|
||||
return Long.compare(ta, tb);
|
||||
});
|
||||
|
||||
// ===== 写入文件 =====
|
||||
PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(file),
|
||||
StandardCharsets.UTF_8));
|
||||
try {
|
||||
writeXmlHeader(pw, "全服流水");
|
||||
writeXmlRow(pw, new String[]{
|
||||
"玩家", "时间", "类型",
|
||||
"金额", "对象", "操作者",
|
||||
"理由", "操作前余额",
|
||||
"操作后余额"
|
||||
});
|
||||
|
||||
for (String[] row : allRows) {
|
||||
// 去掉最后的时间戳列
|
||||
String[] cells =
|
||||
new String[row.length - 1];
|
||||
System.arraycopy(row, 0,
|
||||
cells, 0, cells.length);
|
||||
writeXmlRow(pw, cells);
|
||||
}
|
||||
|
||||
writeXmlFooter(pw);
|
||||
pw.flush();
|
||||
|
||||
if (sender != null) {
|
||||
sender.sendMessage(
|
||||
"§a[打印] §f全服流水 §a已导出");
|
||||
sender.sendMessage("§7文件: §f"
|
||||
+ fileName);
|
||||
sender.sendMessage("§7共 §f"
|
||||
+ players.size()
|
||||
+ " 名玩家,§f"
|
||||
+ allRows.size() + " §7条");
|
||||
sender.sendMessage(
|
||||
"§7按时间从早到晚排序");
|
||||
sender.sendMessage(
|
||||
"§724小时后自动删除");
|
||||
}
|
||||
} finally {
|
||||
pw.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§c导出失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
plugin.getLogger().severe(
|
||||
"[Printer] " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 清理 ============
|
||||
|
||||
public void cleanupOldFiles() {
|
||||
long now = System.currentTimeMillis();
|
||||
long maxAge = 24L * 60 * 60 * 1000;
|
||||
File[] files = outputDir.listFiles(
|
||||
(d, n) -> n.endsWith(".xls"));
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
int count = 0;
|
||||
for (File f : files) {
|
||||
if (now - f.lastModified() > maxAge) {
|
||||
if (f.delete()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Printer] 清理 "
|
||||
+ count + " 个过期文件");
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteOld(String player) {
|
||||
File[] old = outputDir.listFiles(
|
||||
(d, n) -> n.startsWith(player + "_")
|
||||
&& n.endsWith(".xls"));
|
||||
if (old != null) {
|
||||
for (File f : old) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ XML工具 ============
|
||||
|
||||
private void writeXmlHeader(PrintWriter pw,
|
||||
String sheetName) {
|
||||
pw.println("<?xml version=\"1.0\"?>");
|
||||
pw.println("<?mso-application "
|
||||
+ "progid=\"Excel.Sheet\"?>");
|
||||
pw.println("<Workbook xmlns="
|
||||
+ "\"urn:schemas-microsoft-com"
|
||||
+ ":office:spreadsheet\"");
|
||||
pw.println(" xmlns:ss="
|
||||
+ "\"urn:schemas-microsoft-com"
|
||||
+ ":office:spreadsheet\">");
|
||||
pw.println(" <Worksheet ss:Name=\""
|
||||
+ sheetName + "\">");
|
||||
pw.println(" <Table>");
|
||||
}
|
||||
|
||||
private void writeXmlRow(PrintWriter pw,
|
||||
String[] cells) {
|
||||
pw.println(" <Row>");
|
||||
for (String cell : cells) {
|
||||
pw.println(" <Cell>"
|
||||
+ "<Data ss:Type=\"String\">"
|
||||
+ escXml(cell != null ? cell : "")
|
||||
+ "</Data></Cell>");
|
||||
}
|
||||
pw.println(" </Row>");
|
||||
}
|
||||
|
||||
private void writeXmlFooter(PrintWriter pw) {
|
||||
pw.println(" </Table>");
|
||||
pw.println(" </Worksheet>");
|
||||
pw.println("</Workbook>");
|
||||
}
|
||||
|
||||
private String escXml(String val) {
|
||||
if (val == null) {
|
||||
return "";
|
||||
}
|
||||
return val.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """);
|
||||
}
|
||||
|
||||
private String txTime(Map<String, Object> tx) {
|
||||
Object t = tx.get("time");
|
||||
if (t == null) return "";
|
||||
long ms = ((Number) t).longValue();
|
||||
return CELL_SDF.format(new Date(ms));
|
||||
}
|
||||
|
||||
private String txTypeName(
|
||||
Map<String, Object> tx) {
|
||||
String type = (String) tx.get("type");
|
||||
return TYPE_NAMES.getOrDefault(type, type);
|
||||
}
|
||||
|
||||
private int txInt(Map<String, Object> tx,
|
||||
String key) {
|
||||
Object v = tx.get(key);
|
||||
if (v == null) return 0;
|
||||
return ((Number) v).intValue();
|
||||
}
|
||||
|
||||
private String txStr(Map<String, Object> tx,
|
||||
String key) {
|
||||
Object v = tx.get(key);
|
||||
return v != null ? v.toString() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.*;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class CDKManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Connection db;
|
||||
private final Map<String, CDKInput> pendingInputs
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
public CDKManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initDB();
|
||||
}
|
||||
|
||||
private void initDB() {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
File dbFile = new File(
|
||||
plugin.getDataFolder(), "bond.db");
|
||||
db = DriverManager.getConnection(
|
||||
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
Statement st = db.createStatement();
|
||||
st.execute("PRAGMA journal_mode=WAL");
|
||||
st.execute("CREATE TABLE IF NOT EXISTS cdk ("
|
||||
+ "code TEXT PRIMARY KEY,"
|
||||
+ "amount INTEGER DEFAULT 0,"
|
||||
+ "type TEXT DEFAULT 'bond',"
|
||||
+ "used INTEGER DEFAULT 0,"
|
||||
+ "used_by TEXT DEFAULT '',"
|
||||
+ "created_time INTEGER DEFAULT 0,"
|
||||
+ "used_time INTEGER DEFAULT 0)");
|
||||
st.close();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"[CDK] DB init failed: "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CDK兑换(供SDF1调用) =====
|
||||
|
||||
/**
|
||||
* 兑换CDK,返回结果字符串
|
||||
* @return "success:金额:余额前:余额后"
|
||||
* 或 "fail:原因"
|
||||
*/
|
||||
public String redeem(String code, String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT amount,type,used FROM cdk "
|
||||
+ "WHERE code=?");
|
||||
ps.setString(1, code);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (!rs.next()) {
|
||||
rs.close(); ps.close();
|
||||
return "fail:not_found";
|
||||
}
|
||||
int amount = rs.getInt("amount");
|
||||
String type = rs.getString("type");
|
||||
int used = rs.getInt("used");
|
||||
rs.close(); ps.close();
|
||||
|
||||
if (used == 1)
|
||||
return "fail:already_used";
|
||||
|
||||
if (plugin.getBonds()
|
||||
.isFrozen(player))
|
||||
return "fail:frozen";
|
||||
|
||||
PreparedStatement up = db.prepareStatement(
|
||||
"UPDATE cdk SET used=1,"
|
||||
+ "used_by=?,used_time=? "
|
||||
+ "WHERE code=?");
|
||||
up.setString(1, player);
|
||||
up.setLong(2,
|
||||
System.currentTimeMillis());
|
||||
up.setString(3, code);
|
||||
up.executeUpdate(); up.close();
|
||||
|
||||
if ("bond".equals(type)) {
|
||||
int bef = plugin.getBonds()
|
||||
.getBonds(player);
|
||||
plugin.getBonds().addBonds(player,
|
||||
amount,
|
||||
BondManager.TX_REDEEM,
|
||||
"", "CDK系统",
|
||||
"CDK兑换: " + code);
|
||||
int aft = plugin.getBonds()
|
||||
.getBonds(player);
|
||||
return "success:" + amount
|
||||
+ ":" + bef + ":" + aft;
|
||||
}
|
||||
return "fail:unknown_type";
|
||||
} catch (SQLException e) {
|
||||
return "fail:" + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 聊天输入系统 =====
|
||||
|
||||
public void requestInput(Player p,
|
||||
String type,
|
||||
String context) {
|
||||
CDKInput input = new CDKInput();
|
||||
input.type = type;
|
||||
input.context = context;
|
||||
input.time = System.currentTimeMillis();
|
||||
pendingInputs.put(p.getName(), input);
|
||||
}
|
||||
|
||||
public boolean isListening(String playerName) {
|
||||
return pendingInputs
|
||||
.containsKey(playerName);
|
||||
}
|
||||
|
||||
public void onChat(Player p, String msg) {
|
||||
CDKInput input = pendingInputs
|
||||
.remove(p.getName());
|
||||
if (input == null) return;
|
||||
|
||||
if (System.currentTimeMillis()
|
||||
- input.time > 60000) {
|
||||
p.sendMessage("§c输入超时");
|
||||
return;
|
||||
}
|
||||
|
||||
if ("cdk".equals(input.type)) {
|
||||
String result = redeem(
|
||||
msg.trim(), p.getName());
|
||||
if (result != null
|
||||
&& result.startsWith("success:")) {
|
||||
String[] parts = result.split(":");
|
||||
int amt = Integer.parseInt(parts[1]);
|
||||
int bef = Integer.parseInt(parts[2]);
|
||||
int aft = Integer.parseInt(parts[3]);
|
||||
p.sendMessage("§e§l[CDK] §f兑换成功!"
|
||||
+ "获得 §c" + amt + "§f债券");
|
||||
p.sendMessage("§7余额: §e" + bef
|
||||
+ " §7-> §a" + aft);
|
||||
} else {
|
||||
p.sendMessage("§c§l[CDK] §f"
|
||||
+ (result != null
|
||||
? result.replace("fail:", "")
|
||||
: "无效的兑换码"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ("econ".equals(input.type)) {
|
||||
handleEconInput(p, msg.trim(),
|
||||
input.context);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("bond".equals(input.type)) {
|
||||
handleBondInput(p, msg.trim(),
|
||||
input.context);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEconInput(Player admin,
|
||||
String msg,
|
||||
String target) {
|
||||
net.milkbowl.vault.economy.Economy econ
|
||||
= null;
|
||||
try {
|
||||
var reg = plugin.getServer()
|
||||
.getServicesManager()
|
||||
.getRegistration(
|
||||
net.milkbowl.vault
|
||||
.economy
|
||||
.Economy.class);
|
||||
if (reg != null)
|
||||
econ = reg.getProvider();
|
||||
} catch (Exception ignored) {}
|
||||
if (econ == null) {
|
||||
admin.sendMessage("§c经济系统不可用");
|
||||
return;
|
||||
}
|
||||
if (!msg.startsWith("+")
|
||||
&& !msg.startsWith("-")) {
|
||||
admin.sendMessage("§c格式: §a+100"
|
||||
+ " §7或 §c-50");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String numStr =
|
||||
msg.substring(1).trim();
|
||||
double amount =
|
||||
Double.parseDouble(numStr);
|
||||
if (amount <= 0) {
|
||||
admin.sendMessage("§c金额必须>0");
|
||||
return;
|
||||
}
|
||||
if (msg.startsWith("-")) {
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp == null) {
|
||||
admin.sendMessage("§c" + target
|
||||
+ " 不在线");
|
||||
return;
|
||||
}
|
||||
if (!econ.has(tp, amount)) {
|
||||
admin.sendMessage("§c余额不足");
|
||||
return;
|
||||
}
|
||||
econ.withdrawPlayer(tp, amount);
|
||||
admin.sendMessage("§a扣款成功");
|
||||
} else {
|
||||
econ.depositPlayer(target, amount);
|
||||
admin.sendMessage("§a到账成功");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
admin.sendMessage("§c格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBondInput(Player admin,
|
||||
String msg,
|
||||
String target) {
|
||||
if (!msg.startsWith("+")
|
||||
&& !msg.startsWith("-")) {
|
||||
admin.sendMessage("§c格式: §a+100"
|
||||
+ " §7或 §c-50");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String sign = msg.substring(0, 1);
|
||||
String rest =
|
||||
msg.substring(1).trim();
|
||||
String[] parts =
|
||||
rest.split("\\s+", 2);
|
||||
int amount = Integer.parseInt(parts[0]);
|
||||
String reason = parts.length >= 2
|
||||
? parts[1].trim()
|
||||
: "管理员手动调整";
|
||||
if (amount <= 0) {
|
||||
admin.sendMessage("§c金额必须>0");
|
||||
return;
|
||||
}
|
||||
String opName = admin.getName();
|
||||
String txType = "-".equals(sign)
|
||||
? BondManager.TX_ADMIN_DEDUCT
|
||||
: BondManager.TX_ADMIN_GIVE;
|
||||
|
||||
if ("-".equals(sign)) {
|
||||
if (plugin.getBonds()
|
||||
.isFrozen(target)) {
|
||||
admin.sendMessage("§c账户冻结");
|
||||
return;
|
||||
}
|
||||
if (plugin.getBonds().deductBonds(
|
||||
target, amount, txType,
|
||||
"", opName, reason)) {
|
||||
admin.sendMessage("§a扣除成功");
|
||||
} else {
|
||||
admin.sendMessage("§c债券不足");
|
||||
}
|
||||
} else {
|
||||
if (plugin.getBonds()
|
||||
.isFrozen(target)) {
|
||||
admin.sendMessage("§c账户冻结");
|
||||
return;
|
||||
}
|
||||
plugin.getBonds().addBonds(target,
|
||||
amount, txType,
|
||||
"", opName, reason);
|
||||
admin.sendMessage("§a发放成功");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
admin.sendMessage("§c格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelInput(String player) {
|
||||
pendingInputs.remove(player);
|
||||
}
|
||||
|
||||
static class CDKInput {
|
||||
String type;
|
||||
String context;
|
||||
long time;
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,9 @@ public class ChatInputManager {
|
||||
TICKET_ADMIN_DESC,
|
||||
TICKET_ADMIN_PROVIDER,
|
||||
BACK_CHECK_ECONOMY_CONFIRM,
|
||||
}
|
||||
USER_MGMT_SEARCH,
|
||||
|
||||
}
|
||||
|
||||
public static class PlayerState {
|
||||
public InputType type = InputType.NONE;
|
||||
@@ -49,6 +51,7 @@ public class ChatInputManager {
|
||||
public int ticketId = 0;
|
||||
public int smtpStep = 0;
|
||||
public int scoreValue = 0;
|
||||
|
||||
}
|
||||
|
||||
private final Map<String, PlayerState> states =
|
||||
|
||||
@@ -26,6 +26,7 @@ public class CheckInManager {
|
||||
return today.equals(last);
|
||||
}
|
||||
|
||||
// ===== 签到 =====
|
||||
// ===== 签到 =====
|
||||
public String checkIn(Player p) {
|
||||
String name = p.getName();
|
||||
@@ -35,36 +36,29 @@ public class CheckInManager {
|
||||
}
|
||||
|
||||
Object streakObj = plugin.getDb()
|
||||
.getField(name,
|
||||
"checkin_streak");
|
||||
.getField(name, "checkin_streak");
|
||||
int streak = streakObj != null
|
||||
? ((Number) streakObj)
|
||||
.intValue() : 0;
|
||||
? ((Number) streakObj).intValue()
|
||||
: 0;
|
||||
|
||||
String lastDate = (String) plugin
|
||||
.getDb().getField(name,
|
||||
"last_checkin_date");
|
||||
String today = new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
String lastDate = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
|
||||
if (lastDate != null
|
||||
&& !lastDate.isEmpty()) {
|
||||
if (lastDate != null && !lastDate.isEmpty()) {
|
||||
try {
|
||||
java.util.Date lastD =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
new SimpleDateFormat("yyyy-MM-dd")
|
||||
.parse(lastDate);
|
||||
java.util.Date todayD =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
new SimpleDateFormat("yyyy-MM-dd")
|
||||
.parse(today);
|
||||
long diff = todayD.getTime()
|
||||
- lastD.getTime();
|
||||
if (diff > 86400000L * 2) {
|
||||
streak = 0;
|
||||
} else if (diff
|
||||
> 86400000L) {
|
||||
} else if (diff > 86400000L) {
|
||||
streak++;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
@@ -79,30 +73,37 @@ public class CheckInManager {
|
||||
else if (streak >= 14) multi = 1.3;
|
||||
else if (streak >= 7) multi = 1.1;
|
||||
|
||||
int basePoints = plugin.getConfig2()
|
||||
.checkinPoints;
|
||||
int points =
|
||||
(int) (basePoints * multi);
|
||||
int basePoints =
|
||||
plugin.getConfig2().checkinPoints;
|
||||
int points = (int) (basePoints * multi);
|
||||
|
||||
plugin.getDb().addPoints(
|
||||
name, points);
|
||||
plugin.getDb().setField(
|
||||
name, "checkin_streak",
|
||||
streak);
|
||||
plugin.getDb().setField(
|
||||
name,
|
||||
plugin.getDb().addPoints(name, points);
|
||||
plugin.getDb().setField(name,
|
||||
"checkin_streak", streak);
|
||||
plugin.getDb().setField(name,
|
||||
"last_checkin_date", today);
|
||||
|
||||
Object totalObj = plugin.getDb()
|
||||
.getField(name,
|
||||
"total_checkin_days");
|
||||
.getField(name, "total_checkin_days");
|
||||
int total = totalObj != null
|
||||
? ((Number) totalObj)
|
||||
.intValue() : 0;
|
||||
plugin.getDb().setField(
|
||||
name,
|
||||
"total_checkin_days",
|
||||
total + 1);
|
||||
? ((Number) totalObj).intValue()
|
||||
: 0;
|
||||
plugin.getDb().setField(name,
|
||||
"total_checkin_days", total + 1);
|
||||
|
||||
// ★ 债券盲盒奖励(6参数版,写入流水)★
|
||||
int bondMin =
|
||||
plugin.getConfig2().checkinBondMin;
|
||||
int bondMax =
|
||||
plugin.getConfig2().checkinBondMax;
|
||||
if (bondMin <= 0) bondMin = 1;
|
||||
if (bondMax < bondMin) bondMax = bondMin;
|
||||
int bondReward = bondMin
|
||||
+ new java.util.Random()
|
||||
.nextInt(bondMax - bondMin + 1);
|
||||
plugin.getBonds().addBonds(name, bondReward,
|
||||
"daily_sign", "", "签到系统",
|
||||
"每日签到连续" + streak + "天");
|
||||
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_success")
|
||||
@@ -111,7 +112,8 @@ public class CheckInManager {
|
||||
.replace("{streak}",
|
||||
String.valueOf(streak))
|
||||
.replace("{multi}",
|
||||
String.valueOf(multi));
|
||||
String.valueOf(multi))
|
||||
+ " §e+" + bondReward + "债券";
|
||||
}
|
||||
|
||||
// ===== 获取连续天数 =====
|
||||
@@ -125,7 +127,16 @@ public class CheckInManager {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 标准化日期
|
||||
private String normalizeDate(String input) {
|
||||
String s = input.trim().replace("/", "-");
|
||||
if (s.matches("\\d{8}")) {
|
||||
return s.substring(0, 4) + "-"
|
||||
+ s.substring(4, 6) + "-"
|
||||
+ s.substring(6, 8);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// ===== 补签入口 =====
|
||||
public String backCheckIn(
|
||||
Player p, String dateStr) {
|
||||
@@ -144,14 +155,12 @@ public class CheckInManager {
|
||||
} catch (Exception ex) {
|
||||
return "§c无效日期: " + dateStr;
|
||||
}
|
||||
String reformat =
|
||||
sdf.format(target);
|
||||
String reformat = sdf.format(target);
|
||||
if (!reformat.equals(normalized)) {
|
||||
return "§c无效日期: " + dateStr;
|
||||
}
|
||||
|
||||
String today = sdf.format(new Date());
|
||||
|
||||
if (normalized.compareTo(today) >= 0) {
|
||||
return "§c今天不需要补签";
|
||||
}
|
||||
@@ -160,19 +169,16 @@ public class CheckInManager {
|
||||
if (normalized.compareTo(
|
||||
threeDaysAgo) < 0) {
|
||||
return "§c只能补签3天内"
|
||||
+ "(最多到 "
|
||||
+ threeDaysAgo + ")";
|
||||
+ "(最多到 " + threeDaysAgo + ")";
|
||||
}
|
||||
|
||||
Object val = plugin.getDb()
|
||||
.getField(name,
|
||||
"backcheck_dates");
|
||||
.getField(name, "backcheck_dates");
|
||||
String existing = val != null
|
||||
? val.toString() : "";
|
||||
boolean alreadyBacked = false;
|
||||
if (!existing.isEmpty()) {
|
||||
for (String d
|
||||
: existing.split(",")) {
|
||||
for (String d : existing.split(",")) {
|
||||
if (normalizeDate(d.trim())
|
||||
.equals(normalized)) {
|
||||
alreadyBacked = true;
|
||||
@@ -181,71 +187,94 @@ public class CheckInManager {
|
||||
}
|
||||
}
|
||||
if (alreadyBacked) {
|
||||
return "§c该日期已补签: "
|
||||
+ normalized;
|
||||
return "§c该日期已补签: " + normalized;
|
||||
}
|
||||
|
||||
Object normalCheck =
|
||||
plugin.getDb().getField(
|
||||
name,
|
||||
"last_checkin_date");
|
||||
String lastCheckin = normalCheck
|
||||
!= null
|
||||
? normalCheck.toString()
|
||||
: "";
|
||||
name, "last_checkin_date");
|
||||
String lastCheckin = normalCheck != null
|
||||
? normalCheck.toString() : "";
|
||||
if (lastCheckin.equals(normalized)) {
|
||||
return "§c该日期已签到: "
|
||||
+ normalized;
|
||||
return "§c该日期已签到: " + normalized;
|
||||
}
|
||||
|
||||
if (!plugin.getDb()
|
||||
.deductPoints(name, 10)) {
|
||||
return "§c积分不足,补签需 10 积分";
|
||||
}
|
||||
// ★ 补签消耗:3债券 + 10积分
|
||||
// 积分不够时:用 9债券(3倍)代替 ★
|
||||
int bondCost = 3;
|
||||
int pointCost = 10;
|
||||
int bondAltCost = 9; // 3倍债券替代
|
||||
|
||||
net.milkbowl.vault.economy
|
||||
.Economy econ =
|
||||
plugin.getEconomy();
|
||||
if (econ != null) {
|
||||
double bal = econ.getBalance(p);
|
||||
if (bal < 10.0) {
|
||||
plugin.getDb()
|
||||
.addPoints(name, 10);
|
||||
int mult = plugin.getConfig2()
|
||||
.backCheckPointMultiplier;
|
||||
int needed = 10 * mult;
|
||||
p.sendMessage("§e§l[补签] "
|
||||
+ "§f经济不足($"
|
||||
+ String.format(
|
||||
"%.2f", bal)
|
||||
+ " < $10)");
|
||||
p.sendMessage("§7是否用 §a§l"
|
||||
+ needed + "积分 "
|
||||
+ "§7代替 10经济?");
|
||||
p.sendMessage("§7输入 §a1 "
|
||||
+ "§7同意,"
|
||||
+ "§c其他 §7取消");
|
||||
return "NEED_CONFIRM:"
|
||||
+ normalized
|
||||
+ ":" + needed;
|
||||
int myBonds = plugin.getBonds()
|
||||
.getBonds(name);
|
||||
Object ptsObj = plugin.getDb()
|
||||
.getField(name, "points");
|
||||
int myPoints = ptsObj != null
|
||||
? ((Number) ptsObj).intValue()
|
||||
: 0;
|
||||
|
||||
|
||||
// 方案A:3债券 + 10积分
|
||||
boolean canA = myBonds >= bondCost
|
||||
&& myPoints >= pointCost;
|
||||
// 方案B:9债券(积分不够时)
|
||||
boolean canB = myBonds >= bondAltCost
|
||||
&& myPoints < pointCost;
|
||||
|
||||
if (!canA && !canB) {
|
||||
if (myPoints < pointCost
|
||||
&& myBonds < bondAltCost) {
|
||||
return "§c补签费用不足"
|
||||
+ "(需3债券+10积分,"
|
||||
+ "或9债券代替)"
|
||||
+ " 当前: "
|
||||
+ myBonds + "债券 "
|
||||
+ myPoints + "积分";
|
||||
}
|
||||
econ.withdrawPlayer(p, 10);
|
||||
if (myBonds < bondCost) {
|
||||
return "§c债券不足(需"
|
||||
+ bondCost + "枚)";
|
||||
}
|
||||
return "§c积分不足(需"
|
||||
+ pointCost + "分)";
|
||||
}
|
||||
|
||||
// 执行扣款
|
||||
if (canA) {
|
||||
// 方案A:3债券 + 10积分
|
||||
plugin.getBonds().deductBonds(
|
||||
name, bondCost,
|
||||
"admin_deduct", "",
|
||||
"补签系统", "补签扣款");
|
||||
plugin.getDb().deductPoints(
|
||||
name, pointCost);
|
||||
} else {
|
||||
// 方案B:9债券代替
|
||||
plugin.getBonds().deductBonds(
|
||||
name, bondAltCost,
|
||||
"admin_deduct", "",
|
||||
"补签系统", "补签扣款(债券替代)");
|
||||
}
|
||||
|
||||
doBackCheckIn(p, normalized);
|
||||
|
||||
String costMsg = canA
|
||||
? bondCost + "债券+" + pointCost + "积分"
|
||||
: bondAltCost + "债券(替代)";
|
||||
return "§a补签成功!日期: "
|
||||
+ normalized
|
||||
+ " §7(扣除10积分+10经济)";
|
||||
+ " §7消耗: " + costMsg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===== 执行补签 =====
|
||||
// ===== 执行补签 =====
|
||||
public void doBackCheckIn(
|
||||
Player p, String dateStr) {
|
||||
String name = p.getName();
|
||||
Object val = plugin.getDb()
|
||||
.getField(name,
|
||||
"backcheck_dates");
|
||||
.getField(name, "backcheck_dates");
|
||||
String existing = val != null
|
||||
? val.toString() : "";
|
||||
String updated = existing.isEmpty()
|
||||
@@ -255,21 +284,8 @@ public class CheckInManager {
|
||||
"backcheck_dates", updated);
|
||||
int streak = getStreak(name);
|
||||
plugin.getDb().setField(name,
|
||||
"checkin_streak",
|
||||
streak + 1);
|
||||
}
|
||||
|
||||
// ===== 标准化日期 =====
|
||||
private String normalizeDate(
|
||||
String input) {
|
||||
String s = input.trim()
|
||||
.replace("/", "-");
|
||||
if (s.matches("\\d{8}")) {
|
||||
return s.substring(0, 4) + "-"
|
||||
+ s.substring(4, 6) + "-"
|
||||
+ s.substring(6, 8);
|
||||
}
|
||||
return s;
|
||||
"checkin_streak", streak + 1);
|
||||
// ★ 补签不发放奖励,只记录
|
||||
}
|
||||
|
||||
// ===== 获取N天前日期 =====
|
||||
@@ -283,4 +299,4 @@ public class CheckInManager {
|
||||
"yyyy-MM-dd")
|
||||
.format(cal.getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@ import java.util.*;
|
||||
public class ConfigManager {
|
||||
|
||||
private final File dataFolder;
|
||||
private final Map<String, String> messages =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<String, String> smtpSettings =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<String, String> messages
|
||||
= new LinkedHashMap<>();
|
||||
private final Map<String, String> smtpSettings
|
||||
= new LinkedHashMap<>();
|
||||
|
||||
// ===== 公共字段 =====
|
||||
public String adminTag = "admin";
|
||||
@@ -30,7 +30,6 @@ public class ConfigManager {
|
||||
public int httpPort = 8080;
|
||||
public String garbageTag = "admin";
|
||||
|
||||
// 签到经济奖励
|
||||
public double checkinRewardFixed = 0;
|
||||
public double checkinRewardMin = 0;
|
||||
public double checkinRewardMax = 0;
|
||||
@@ -38,35 +37,36 @@ public class ConfigManager {
|
||||
public int checkinPoints = 10;
|
||||
public String checkinRewardType = "none";
|
||||
public int backCheckPointMultiplier = 5;
|
||||
// 提成/工单
|
||||
public int checkinBondMin = 1;
|
||||
public int checkinBondMax = 3;
|
||||
|
||||
public double providerPointsPerScore = 2.0;
|
||||
public int baseEconomyReward = 100;
|
||||
public double providerEconomyPerScore = 50.0;
|
||||
public int requesterPointsReward = 10;
|
||||
|
||||
// 垃圾站
|
||||
public boolean garbageEnabled = true;
|
||||
public int garbageInterval = 300;
|
||||
public int garbageMaxRounds = 1;
|
||||
|
||||
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
|
||||
public ConfigManager(File dataFolder) {
|
||||
this.dataFolder = dataFolder;
|
||||
}
|
||||
public String rewardChannel = "债券"; // bonds = 债券,economy = Vault经济
|
||||
|
||||
|
||||
// ==================== 消息 ====================
|
||||
|
||||
public void loadMessages() {
|
||||
messages.clear();
|
||||
File file =
|
||||
new File(dataFolder, "消息.txt");
|
||||
if (!file.exists())
|
||||
createDefaultMessages(file);
|
||||
File file = new File(dataFolder, "消息.txt");
|
||||
if (!file.exists()) createDefaultMessages(file);
|
||||
loadMap(file, messages);
|
||||
}
|
||||
|
||||
public String msg(String key) {
|
||||
String val =
|
||||
messages.getOrDefault(key, key);
|
||||
String val = messages.getOrDefault(key, key);
|
||||
val = val.replace("<b>", "&l");
|
||||
val = val.replace("</b>", "&r");
|
||||
val = val.replace("<br>", "\n");
|
||||
@@ -79,81 +79,22 @@ public class ConfigManager {
|
||||
private void createDefaultMessages(File f) {
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== Sdf1_login 消息配置 =====");
|
||||
L.add("# 占位符用 {xxx} 表示");
|
||||
L.add("# 修改后执行 /sdf1_login reload");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 登录注册 ----");
|
||||
L.add("login_timeout=§c登录超时,请重新登录");
|
||||
L.add("not_registered=§e您尚未注册,请使用 /reg <密码>");
|
||||
L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>");
|
||||
L.add("already_registered=§c您已注册过账号");
|
||||
L.add("login_timeout=§c登录超时");
|
||||
L.add("not_registered=§e您尚未注册");
|
||||
L.add("not_logged_in=§e您尚未登录");
|
||||
L.add("already_registered=§c您已注册过");
|
||||
L.add("already_logged_in=§c您已登录");
|
||||
L.add("reg_success=§a注册成功!欢迎来到服务器");
|
||||
L.add("reg_success=§a注册成功!");
|
||||
L.add("login_success=§a登录成功!");
|
||||
L.add("login_failed=§c密码错误,请重试");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 密码 ----");
|
||||
L.add("login_failed=§c密码错误");
|
||||
L.add("password_wrong=§c密码错误");
|
||||
L.add("password_changed=§a密码修改成功!");
|
||||
L.add("password_format_error=§c密码需6位以上,含大小写字母和数字");
|
||||
L.add("password_format_error=§c密码需6位以上含大小写和数字");
|
||||
L.add("password_same=§c新密码不能与旧密码相同");
|
||||
L.add("reset_no_email=§c您未绑定邮箱,无法找回密码");
|
||||
L.add("reset_sent=§a临时密码已发送到您的邮箱");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 签到积分 ----");
|
||||
L.add("checkin_already=§c您今日已签到");
|
||||
L.add("checkin_success=§a签到成功!获得 {points} 积分");
|
||||
L.add("points_insufficient=§c积分不足,当前: {points}");
|
||||
L.add("points_purchase_success=§a购买成功!消耗 {count} 积分");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 挂机检测 ----");
|
||||
L.add("afk_set_enabled=§a挂机检测已开启");
|
||||
L.add("afk_set_disabled=§c挂机检测已关闭");
|
||||
L.add("afk_kick=§c因挂机过久被踢出");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 邮箱 ----");
|
||||
L.add("email_set=§a邮箱设置成功:{email}");
|
||||
L.add("email_format_error=§c邮箱格式不正确");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 邀请 ----");
|
||||
L.add("invite_code_generated=§a邀请码:{code}");
|
||||
L.add("invite_used=§a邀请成功,邀请人:{player}");
|
||||
L.add("invite_self=§c不能使用自己的邀请码");
|
||||
L.add("invite_not_found=§c邀请码不存在");
|
||||
L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 礼包 ----");
|
||||
L.add("gift_not_ready=§c礼包条件未满足");
|
||||
L.add("gift_claimed=§a礼包领取成功!");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 审批与IP ----");
|
||||
L.add("need_approval=§e注册需审批 (工单#{id})");
|
||||
L.add("ip_max_accounts=§cIP注册已达上限");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 管理员 ----");
|
||||
L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 聊天过滤 ----");
|
||||
L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言");
|
||||
L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规链接: §e{url}");
|
||||
L.add("chat_url_violation=§7当前违规次数: §e{count}");
|
||||
L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url}");
|
||||
L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚");
|
||||
L.add("");
|
||||
|
||||
L.add("# ---- 通用 ----");
|
||||
L.add("chat_muted=§c你已被禁言");
|
||||
L.add("未知参数=§c未知参数或权限不足");
|
||||
|
||||
writeLines(f, L);
|
||||
}
|
||||
|
||||
@@ -161,10 +102,8 @@ public class ConfigManager {
|
||||
|
||||
public void loadSmtp() {
|
||||
smtpSettings.clear();
|
||||
File file =
|
||||
new File(dataFolder, "SMTP设置.txt");
|
||||
if (!file.exists())
|
||||
createDefaultSmtp(file);
|
||||
File file = new File(dataFolder, "SMTP设置.txt");
|
||||
if (!file.exists()) createDefaultSmtp(file);
|
||||
loadMap(file, smtpSettings);
|
||||
}
|
||||
|
||||
@@ -177,10 +116,9 @@ public class ConfigManager {
|
||||
}
|
||||
|
||||
public void saveSmtp() {
|
||||
File file =
|
||||
new File(dataFolder, "SMTP设置.txt");
|
||||
File file = new File(dataFolder, "SMTP设置.txt");
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== SMTP 邮件服务器配置 =====");
|
||||
L.add("# ===== SMTP 配置 =====");
|
||||
for (Map.Entry<String, String> e
|
||||
: smtpSettings.entrySet()) {
|
||||
L.add(e.getKey() + "=" + e.getValue());
|
||||
@@ -190,10 +128,10 @@ public class ConfigManager {
|
||||
|
||||
private void createDefaultSmtp(File f) {
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== SMTP 邮件服务器配置 =====");
|
||||
L.add("# ===== SMTP 配置 =====");
|
||||
L.add("smtp地址=smtp.example.com");
|
||||
L.add("smtp端口=465");
|
||||
L.add("smtp账号=your_email@example.com");
|
||||
L.add("smtp账号=");
|
||||
L.add("smtp密码=");
|
||||
L.add("发件人名称=Sdf1_login");
|
||||
L.add("smtp加密=true");
|
||||
@@ -203,131 +141,134 @@ public class ConfigManager {
|
||||
// ==================== 插件设置 ====================
|
||||
|
||||
public void loadSettings() {
|
||||
File file =
|
||||
new File(dataFolder, "插件设置.txt");
|
||||
if (!file.exists())
|
||||
createDefaultSettings(file);
|
||||
Map<String, String> m =
|
||||
new LinkedHashMap<>();
|
||||
File file = new File(dataFolder, "插件设置.txt");
|
||||
if (!file.exists()) createDefaultSettings(file);
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
loadMap(file, m);
|
||||
|
||||
adminTag = m.getOrDefault(
|
||||
"管理标签", adminTag);
|
||||
adminPassword = m.getOrDefault(
|
||||
"管理密码", adminPassword);
|
||||
maxAccountsPerIP = parseInt(
|
||||
m.getOrDefault("每IP最大账号数", "3"), 3);
|
||||
approvalMode = m.getOrDefault(
|
||||
"审批模式", approvalMode);
|
||||
autoApproveDelayMinutes = parseInt(
|
||||
m.getOrDefault("自动审批延迟分钟", "30"),
|
||||
30);
|
||||
loginTimeout = parseInt(
|
||||
m.getOrDefault("登录超时秒数", "60"), 60);
|
||||
afkEnabled = Boolean.parseBoolean(
|
||||
m.getOrDefault("挂机踢出", "false"));
|
||||
afkTimeout = parseInt(
|
||||
m.getOrDefault("挂机超时秒数", "300"), 300);
|
||||
smtpSsl = Boolean.parseBoolean(
|
||||
m.getOrDefault("smtp加密", "true"));
|
||||
httpPort = parseInt(
|
||||
m.getOrDefault("http-port", "8080"),
|
||||
8080);
|
||||
broadcastServerUrl = m.getOrDefault(
|
||||
"resource-pack-url", "");
|
||||
adminTag = m.getOrDefault("管理标签", adminTag);
|
||||
adminPassword = m.getOrDefault("管理密码", adminPassword);
|
||||
maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3);
|
||||
approvalMode = m.getOrDefault("审批模式", approvalMode);
|
||||
autoApproveDelayMinutes = parseInt(m.getOrDefault("自动审批延迟分钟", "30"), 30);
|
||||
loginTimeout = parseInt(m.getOrDefault("登录超时秒数", "60"), 60);
|
||||
afkEnabled = Boolean.parseBoolean(m.getOrDefault("挂机踢出", "false"));
|
||||
afkTimeout = parseInt(m.getOrDefault("挂机超时秒数", "300"), 300);
|
||||
smtpSsl = Boolean.parseBoolean(m.getOrDefault("smtp加密", "true"));
|
||||
httpPort = parseInt(m.getOrDefault("http-port", "8080"), 8080);
|
||||
broadcastServerUrl = m.getOrDefault("resource-pack-url", "");
|
||||
|
||||
// 提成
|
||||
providerPointsPerScore = parseDouble(
|
||||
m.getOrDefault(
|
||||
"服务商积分倍率", "2.0"), 2.0);
|
||||
baseEconomyReward = parseInt(
|
||||
m.getOrDefault(
|
||||
"基础经济奖励", "100"), 100);
|
||||
providerEconomyPerScore = parseDouble(
|
||||
m.getOrDefault(
|
||||
"经济奖励每评分", "50.0"), 50.0);
|
||||
requesterPointsReward = parseInt(
|
||||
m.getOrDefault(
|
||||
"报单人积分奖励", "10"), 10);
|
||||
providerPointsPerScore = parseDouble(m.getOrDefault("服务商积分倍率", "2.0"), 2.0);
|
||||
baseEconomyReward = parseInt(m.getOrDefault("基础经济奖励", "100"), 100);
|
||||
providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0);
|
||||
requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10);
|
||||
|
||||
// 垃圾站
|
||||
garbageEnabled = Boolean.parseBoolean(
|
||||
m.getOrDefault(
|
||||
"垃圾站_启用", "true"));
|
||||
garbageInterval = parseInt(
|
||||
m.getOrDefault(
|
||||
"垃圾站_清理间隔秒", "300"), 300);
|
||||
garbageMaxRounds = parseInt(
|
||||
m.getOrDefault(
|
||||
"垃圾站_保留轮数", "1"), 1);
|
||||
garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true"));
|
||||
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
|
||||
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
|
||||
|
||||
// 签到
|
||||
checkinRewardFixed = parseDouble(
|
||||
m.getOrDefault(
|
||||
"签到固定金额", "0"), 0);
|
||||
checkinRewardMin = parseDouble(
|
||||
m.getOrDefault(
|
||||
"签到最小金额", "0"), 0);
|
||||
checkinRewardMax = parseDouble(
|
||||
m.getOrDefault(
|
||||
"签到最大金额", "0"), 0);
|
||||
checkinGivePoints = Boolean.parseBoolean(
|
||||
m.getOrDefault(
|
||||
"签到给积分", "true"));
|
||||
checkinPoints = parseInt(
|
||||
m.getOrDefault(
|
||||
"签到积分数量", "10"), 10);
|
||||
backCheckPointMultiplier = parseInt(
|
||||
m.getOrDefault(
|
||||
"补签积分倍率", "5"), 5);
|
||||
checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0);
|
||||
checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0);
|
||||
checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0);
|
||||
checkinGivePoints = Boolean.parseBoolean(m.getOrDefault("签到给积分", "true"));
|
||||
checkinPoints = parseInt(m.getOrDefault("签到积分数量", "10"), 10);
|
||||
backCheckPointMultiplier = parseInt(m.getOrDefault("补签积分倍率", "5"), 5);
|
||||
checkinBondMin = parseInt(m.getOrDefault("签到债券最小", "1"), 1);
|
||||
checkinBondMax = parseInt(m.getOrDefault("签到债券最大", "3"), 3);
|
||||
if (checkinBondMax < checkinBondMin) checkinBondMax = checkinBondMin;
|
||||
|
||||
if (checkinRewardFixed > 0) checkinRewardType = "fixed";
|
||||
else if (checkinRewardMin > 0 && checkinRewardMax > checkinRewardMin) checkinRewardType = "range";
|
||||
else checkinRewardType = "none";
|
||||
rewardChannel = m.getOrDefault("奖励发放方式", "债券");
|
||||
rewardChannel = normalizeRewardChannel(rewardChannel);
|
||||
|
||||
// 自动判断模式
|
||||
if (checkinRewardFixed > 0) {
|
||||
checkinRewardType = "fixed";
|
||||
} else if (checkinRewardMin > 0
|
||||
&& checkinRewardMax > 0
|
||||
&& checkinRewardMax
|
||||
> checkinRewardMin) {
|
||||
checkinRewardType = "range";
|
||||
} else {
|
||||
checkinRewardType = "none";
|
||||
// 验证合法值
|
||||
if (!"economy".equalsIgnoreCase(rewardChannel)
|
||||
&& !"bonds".equalsIgnoreCase(rewardChannel)) {
|
||||
rewardChannel = "bonds";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ★ 自动补充新配置项 ★
|
||||
boolean needAppend = false;
|
||||
List<String> newKeys = new ArrayList<>();
|
||||
if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); }
|
||||
if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); }
|
||||
if (!m.containsKey("奖励发放方式")) {
|
||||
needAppend = true;
|
||||
newKeys.add("奖励发放方式=债券");
|
||||
}
|
||||
if (needAppend) {
|
||||
try {
|
||||
FileWriter fw = new FileWriter(file, true);
|
||||
BufferedWriter bw = new BufferedWriter(fw);
|
||||
bw.newLine();
|
||||
for (String key : newKeys) { bw.write(key); bw.newLine(); }
|
||||
bw.flush(); bw.close();
|
||||
System.out.println("[Config] 已补充签到债券配置");
|
||||
|
||||
} catch (IOException ignored) {}
|
||||
if (!m.containsKey("奖励发放方式")) {
|
||||
try {
|
||||
FileWriter fw = new FileWriter(file, true);
|
||||
BufferedWriter bw = new BufferedWriter(fw);
|
||||
bw.newLine();
|
||||
bw.write("奖励发放方式=bonds");
|
||||
bw.newLine();
|
||||
bw.flush(); bw.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 奖励方式标准化:中英文全兼容
|
||||
* "债券" "bonds" "Bond" "BONDS" → "bonds"
|
||||
* "经济" "economy" "Economy" "ECONOMY" → "economy"
|
||||
* 其他任何值 → "bonds"(默认债券)
|
||||
*/
|
||||
private String normalizeRewardChannel(String raw) {
|
||||
if (raw == null) return "bonds";
|
||||
String s = raw.trim().toLowerCase();
|
||||
// 中文
|
||||
if (s.contains("债券")) return "bonds";
|
||||
if (s.contains("经济")) return "economy";
|
||||
// 英文
|
||||
if (s.contains("bond")) return "bonds";
|
||||
if (s.contains("econ")) return "economy";
|
||||
if (s.contains("Vault")) return "economy";
|
||||
// 兜底
|
||||
return "bonds";
|
||||
}
|
||||
|
||||
public void saveSettings() {
|
||||
File file =
|
||||
new File(dataFolder, "插件设置.txt");
|
||||
File file = new File(dataFolder, "插件设置.txt");
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== Sdf1_login 插件设置 =====");
|
||||
L.add("管理标签=" + adminTag);
|
||||
L.add("管理密码=" + adminPassword);
|
||||
L.add("每IP最大账号数=" + maxAccountsPerIP);
|
||||
L.add("审批模式=" + approvalMode);
|
||||
L.add("自动审批延迟分钟="
|
||||
+ autoApproveDelayMinutes);
|
||||
L.add("自动审批延迟分钟=" + autoApproveDelayMinutes);
|
||||
L.add("登录超时秒数=" + loginTimeout);
|
||||
L.add("挂机踢出=" + afkEnabled);
|
||||
L.add("挂机超时秒数=" + afkTimeout);
|
||||
L.add("# ---- 资源包 ----");
|
||||
L.add("resource-pack-url="
|
||||
+ broadcastServerUrl);
|
||||
L.add("resource-pack-url=" + broadcastServerUrl);
|
||||
L.add("http-port=" + httpPort);
|
||||
L.add("# ---- 垃圾站 ----");
|
||||
L.add("垃圾站_启用=" + garbageEnabled);
|
||||
L.add("垃圾站_清理间隔秒="
|
||||
+ garbageInterval);
|
||||
L.add("垃圾站_清理间隔秒=" + garbageInterval);
|
||||
L.add("垃圾站_保留轮数=" + garbageMaxRounds);
|
||||
L.add("# ---- 签到盲盒奖励 ----");
|
||||
L.add("签到固定金额=" + checkinRewardFixed);
|
||||
L.add("签到最小金额=" + checkinRewardMin);
|
||||
L.add("签到最大金额=" + checkinRewardMax);
|
||||
L.add("签到给积分=" + checkinGivePoints);
|
||||
L.add("签到积分数量=" + checkinPoints);
|
||||
L.add("补签积分倍率="
|
||||
+ backCheckPointMultiplier);
|
||||
|
||||
|
||||
L.add("签到债券最小=" + checkinBondMin);
|
||||
L.add("签到债券最大=" + checkinBondMax);
|
||||
L.add("补签积分倍率=" + backCheckPointMultiplier);
|
||||
L.add("奖励发放方式=债券");
|
||||
writeLines(file, L);
|
||||
}
|
||||
|
||||
@@ -342,177 +283,90 @@ public class ConfigManager {
|
||||
L.add("登录超时秒数=60");
|
||||
L.add("挂机踢出=false");
|
||||
L.add("挂机超时秒数=300");
|
||||
L.add("# ---- 资源包 ----");
|
||||
L.add("resource-pack-url=");
|
||||
L.add("http-port=8080");
|
||||
L.add("# ---- 垃圾站 ----");
|
||||
L.add("垃圾站_启用=true");
|
||||
L.add("垃圾站_清理间隔秒=300");
|
||||
L.add("垃圾站_保留轮数=1");
|
||||
L.add("# ---- 签到盲盒奖励 ----");
|
||||
L.add("签到固定金额=0");
|
||||
L.add("签到最小金额=50");
|
||||
L.add("签到最大金额=500");
|
||||
L.add("签到给积分=true");
|
||||
L.add("签到积分数量=10");
|
||||
L.add("签到债券最小=1");
|
||||
L.add("签到债券最大=3");
|
||||
L.add("补签积分倍率=5");
|
||||
|
||||
|
||||
L.add("奖励发放方式=债券");
|
||||
writeLines(f, L);
|
||||
}
|
||||
|
||||
// ==================== 通用工具 ====================
|
||||
|
||||
public List<String> getAfkWhitelist() {
|
||||
List<String> list = new ArrayList<>();
|
||||
File file = new File(
|
||||
dataFolder, "挂机白名单.txt");
|
||||
File file = new File(dataFolder, "挂机白名单.txt");
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
try (BufferedWriter w =
|
||||
new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
w.write("# 每行一个玩家名");
|
||||
w.newLine();
|
||||
}
|
||||
FileWriter fw = new FileWriter(file);
|
||||
fw.write("# 每行一个玩家名\n");
|
||||
fw.flush();
|
||||
fw.close();
|
||||
} catch (IOException ignored) {}
|
||||
return list;
|
||||
}
|
||||
try (BufferedReader r =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
try (BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(file),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()
|
||||
&& !line.startsWith("#")) {
|
||||
if (!line.isEmpty() && !line.startsWith("#"))
|
||||
list.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ==================== 其他 ====================
|
||||
|
||||
public String generateSmtpVerifyCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "abcdefghijklmnopqrstuvwxyz"
|
||||
+ "0123456789";
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
java.util.Random r =
|
||||
new java.util.Random();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(
|
||||
r.nextInt(chars.length())));
|
||||
}
|
||||
java.util.Random r = new java.util.Random();
|
||||
for (int i = 0; i < 6; i++) sb.append(chars.charAt(r.nextInt(chars.length())));
|
||||
smtpVerifyCode = sb.toString();
|
||||
smtpVerifyExpire =
|
||||
System.currentTimeMillis()
|
||||
+ 300000L;
|
||||
smtpVerifyExpire = System.currentTimeMillis() + 300000L;
|
||||
return smtpVerifyCode;
|
||||
}
|
||||
|
||||
public boolean checkSmtpVerifyCode(
|
||||
String code) {
|
||||
if (System.currentTimeMillis()
|
||||
> smtpVerifyExpire) {
|
||||
smtpVerifyCode = "";
|
||||
return false;
|
||||
}
|
||||
return smtpVerifyCode
|
||||
.equalsIgnoreCase(code);
|
||||
public boolean checkSmtpVerifyCode(String code) {
|
||||
if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; }
|
||||
return smtpVerifyCode.equalsIgnoreCase(code);
|
||||
}
|
||||
|
||||
public void clearSmtpVerifyCode() {
|
||||
smtpVerifyCode = "";
|
||||
smtpVerifyExpire = 0;
|
||||
}
|
||||
public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; }
|
||||
public boolean canSendSmtpVerify() { return System.currentTimeMillis() - smtpLastSendTime >= 60000L; }
|
||||
public void recordSmtpSendTime() { smtpLastSendTime = System.currentTimeMillis(); }
|
||||
|
||||
public boolean canSendSmtpVerify() {
|
||||
return System.currentTimeMillis()
|
||||
- smtpLastSendTime >= 60000L;
|
||||
}
|
||||
// ==================== 文件工具 ====================
|
||||
|
||||
public void recordSmtpSendTime() {
|
||||
smtpLastSendTime =
|
||||
System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// ==================== 文件读写 ====================
|
||||
|
||||
private void loadMap(File file,
|
||||
Map<String, String> map) {
|
||||
try (BufferedReader r =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
private void loadMap(File file, Map<String, String> map) {
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()
|
||||
|| line.startsWith("#"))
|
||||
continue;
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
int eq = line.indexOf('=');
|
||||
if (eq > 0) {
|
||||
map.put(
|
||||
line.substring(0, eq)
|
||||
.trim(),
|
||||
line.substring(eq + 1)
|
||||
.trim());
|
||||
}
|
||||
if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println(
|
||||
"[Sdf1_login] 读取失败: "
|
||||
+ file.getName());
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
private void writeLines(File file,
|
||||
List<String> lines) {
|
||||
try (BufferedWriter w =
|
||||
new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
for (String l : lines) {
|
||||
w.write(l);
|
||||
w.newLine();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println(
|
||||
"[Sdf1_login] 写入失败: "
|
||||
+ file.getName());
|
||||
}
|
||||
private void writeLines(File file, List<String> lines) {
|
||||
try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||
for (String l : lines) { w.write(l); w.newLine(); }
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
private double parseDouble(
|
||||
String s, double def) {
|
||||
try {
|
||||
return Double.parseDouble(s.trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(String s, int def) {
|
||||
try {
|
||||
return Integer.parseInt(s.trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } }
|
||||
private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CypayCommand
|
||||
implements CommandExecutor, TabCompleter {
|
||||
|
||||
private final Main plugin;
|
||||
private static final SimpleDateFormat SDF =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public CypayCommand(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private boolean hasAdminPerm(CommandSender s) {
|
||||
if (s.isOp()) return true;
|
||||
if (!(s instanceof Player)) return true;
|
||||
Player p = (Player) s;
|
||||
String tag =
|
||||
plugin.getConfig2().adminTag;
|
||||
return !tag.isEmpty()
|
||||
&& p.getScoreboardTags().contains(tag);
|
||||
}
|
||||
|
||||
private int parseAmount(String s) {
|
||||
return Integer.parseInt(
|
||||
s.trim().replaceAll("[+]", ""));
|
||||
}
|
||||
|
||||
private List<String> filter(
|
||||
List<String> list, String prefix) {
|
||||
String low = prefix.toLowerCase();
|
||||
return list.stream()
|
||||
.filter(x -> x.toLowerCase()
|
||||
.startsWith(low))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender,
|
||||
Command cmd,
|
||||
String label,
|
||||
String[] args) {
|
||||
|
||||
// ===== /cypay — 查自己 =====
|
||||
if (args.length == 0) {
|
||||
String name = sender instanceof Player
|
||||
? sender.getName() : "Console";
|
||||
int bonds =
|
||||
plugin.getBonds().getBonds(name);
|
||||
String status = plugin.getBonds()
|
||||
.getAccountStatus(name);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f" + name
|
||||
+ " 余额: §a" + bonds);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f状态: "
|
||||
+ ("frozen".equals(status)
|
||||
? "§c冻结中"
|
||||
: "§a正常"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /cypay import =====
|
||||
/* if (args.length == 2
|
||||
&& "import".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String file = args[1];
|
||||
if (!file.endsWith(".txt"))
|
||||
file += ".txt";
|
||||
int result = plugin.getCDK()
|
||||
.importFromFile(file);
|
||||
if (result == -1)
|
||||
sender.sendMessage("§c文件不存在: "
|
||||
+ file);
|
||||
else
|
||||
sender.sendMessage("§a导入完成: "
|
||||
+ result + " 条");
|
||||
return true;
|
||||
}*/
|
||||
|
||||
// ===== /cypay freeze =====
|
||||
if (args.length == 2
|
||||
&& "freeze".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
if (plugin.getBonds().isFrozen(target)) {
|
||||
sender.sendMessage("§c" + target
|
||||
+ " 已处于冻结状态");
|
||||
return true;
|
||||
}
|
||||
plugin.getBonds().freezeAccount(target);
|
||||
sender.sendMessage("§a已冻结 " + target
|
||||
+ " 的债券账户");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§c§l[系统] 你的债券账户"
|
||||
+ "已被管理员冻结");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /cypay unfreeze =====
|
||||
if (args.length == 2
|
||||
&& "unfreeze".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
if (!plugin.getBonds()
|
||||
.isFrozen(target)) {
|
||||
sender.sendMessage("§c" + target
|
||||
+ " 未被冻结");
|
||||
return true;
|
||||
}
|
||||
plugin.getBonds().unfreezeAccount(target);
|
||||
sender.sendMessage("§a已解冻 " + target
|
||||
+ " 的债券账户");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§a§l[系统] 你的债券账户"
|
||||
+ "已被解冻");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /cypay water [days] =====
|
||||
if (args.length >= 2
|
||||
&& "water".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
int days = 7;
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
days = Integer.parseInt(args[2]);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
days = Math.min(days,
|
||||
BondManager.ADMIN_MAX_WATER_DAYS);
|
||||
|
||||
if (days <= 0) days = 1;
|
||||
|
||||
List<Map<String, Object>> txs =
|
||||
plugin.getBonds()
|
||||
.getTransactions(
|
||||
target, days);
|
||||
|
||||
sender.sendMessage(
|
||||
"§e§l===== " + target
|
||||
+ " 流水记录 (近"
|
||||
+ days + "天) =====");
|
||||
sender.sendMessage("§f当前余额: §a"
|
||||
+ plugin.getBonds()
|
||||
.getBonds(target)
|
||||
+ " §f状态: "
|
||||
+ (plugin.getBonds()
|
||||
.isFrozen(target)
|
||||
? "§c冻结" : "§a正常"));
|
||||
|
||||
if (txs.isEmpty()) {
|
||||
sender.sendMessage("§7 (暂无记录)");
|
||||
} else {
|
||||
for (Map<String, Object> tx : txs) {
|
||||
sender.sendMessage(
|
||||
formatTxLine(tx));
|
||||
}
|
||||
}
|
||||
sender.sendMessage(
|
||||
"§e§l=========================="
|
||||
+ "==========");
|
||||
return true;
|
||||
}
|
||||
// ===== /cypay printer [player] =====
|
||||
if ("printer".equalsIgnoreCase(args[0])) {
|
||||
if (!hasAdminPerm(sender)
|
||||
&& !(sender instanceof Player)
|
||||
== false) {
|
||||
// 控制台也可以用
|
||||
if (!(sender instanceof Player))
|
||||
; // 放行
|
||||
else if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (args.length == 1) {
|
||||
// 打印全服
|
||||
sender.sendMessage(
|
||||
"§e正在导出全服流水...");
|
||||
plugin.getBonds(); // 确保实例就绪
|
||||
if (plugin.getBondPrinter() != null) {
|
||||
plugin.getBondPrinter()
|
||||
.printAll(sender);
|
||||
} else {
|
||||
sender.sendMessage("§cPrinter未初始化");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (args.length == 2) {
|
||||
// 打印指定玩家
|
||||
String target = args[1];
|
||||
sender.sendMessage("§e正在导出 "
|
||||
+ target + " 的流水...");
|
||||
if (plugin.getBondPrinter() != null) {
|
||||
plugin.getBondPrinter()
|
||||
.printPlayer(target, sender);
|
||||
} else {
|
||||
sender.sendMessage("§cPrinter未初始化");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== /cypay give/remove =====
|
||||
if (args.length == 3) {
|
||||
String action = args[0].toLowerCase();
|
||||
if ("give".equals(action)
|
||||
|| "remove".equals(action)) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
try {
|
||||
int amount =
|
||||
parseAmount(args[2]);
|
||||
if (amount <= 0) {
|
||||
sender.sendMessage(
|
||||
"§c金额必须大于0");
|
||||
return true;
|
||||
}
|
||||
String txType =
|
||||
"give".equals(action)
|
||||
? BondManager
|
||||
.TX_ADMIN_GIVE
|
||||
: BondManager
|
||||
.TX_ADMIN_DEDUCT;
|
||||
String opName =
|
||||
sender instanceof Player
|
||||
? sender.getName()
|
||||
: "Console";
|
||||
|
||||
if ("give".equals(action)) {
|
||||
plugin.getBonds().addBonds(
|
||||
target, amount,
|
||||
txType, "",
|
||||
opName, "管理员发放");
|
||||
sender.sendMessage(
|
||||
"§a已给 " + target
|
||||
+ " " + amount
|
||||
+ " 债券");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§a管理员给了你 "
|
||||
+ amount
|
||||
+ " 债券");
|
||||
} else {
|
||||
if (plugin.getBonds()
|
||||
.deductBonds(target,
|
||||
amount,
|
||||
txType, "",
|
||||
opName,
|
||||
"管理员扣除")) {
|
||||
sender.sendMessage(
|
||||
"§a已扣除 " + target
|
||||
+ " " + amount
|
||||
+ " 债券");
|
||||
Player tp = plugin
|
||||
.getServer()
|
||||
.getPlayerExact(
|
||||
target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§c管理员扣除了"
|
||||
+ "你的 "
|
||||
+ amount
|
||||
+ " 债券");
|
||||
} else {
|
||||
sender.sendMessage(
|
||||
"§c" + target
|
||||
+ " 债券不足"
|
||||
+ "或账户冻结");
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage(
|
||||
"§c金额格式错误");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== /cypay <player> <amount> — 仅玩家转账 =====
|
||||
if (args.length == 2) {
|
||||
try {
|
||||
int amount = parseAmount(args[1]);
|
||||
if (amount <= 0) {
|
||||
sender.sendMessage(
|
||||
"§c金额必须大于0");
|
||||
return true;
|
||||
}
|
||||
// ★ 控制台禁止转账
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage(
|
||||
"§c控制台不能转账,请使用 "
|
||||
+ "/cypay give <玩家> <金额>");
|
||||
return true;
|
||||
}
|
||||
Player p = (Player) sender;
|
||||
String target = args[0];
|
||||
BondManager.TransferResult r =
|
||||
plugin.getBonds().transfer(
|
||||
p.getName(),
|
||||
target, amount);
|
||||
if (r.success) {
|
||||
p.sendMessage("§e§l[债券转账] §f向 §a"
|
||||
+ target + " §f转出 §c"
|
||||
+ amount + " §f枚债券");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage("§e§l[债券转账] §f收到 §a"
|
||||
+ p.getName()
|
||||
+ " §f转入 §a"
|
||||
+ amount + " §f枚债券");
|
||||
} else {
|
||||
p.sendMessage("§c" + r.message
|
||||
+ ",当前: "
|
||||
+ plugin.getBonds()
|
||||
.getBonds(p.getName()));
|
||||
}
|
||||
return true;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== /cypay <player> — 查指定玩家 =====
|
||||
if (args.length == 1) {
|
||||
String target = args[0];
|
||||
// ★ 验证玩家在 login.db 中存在 ★
|
||||
if (!plugin.getDb().userExists(target)) {
|
||||
sender.sendMessage("§c玩家 " + target
|
||||
+ " 不存在于服务器");
|
||||
return true;
|
||||
}
|
||||
|
||||
// String target = args[0];
|
||||
if (hasAdminPerm(sender)) {
|
||||
int bonds = plugin.getBonds()
|
||||
.getBonds(target);
|
||||
String status = plugin.getBonds()
|
||||
.getAccountStatus(target);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f" + target
|
||||
+ " 余额: §a"
|
||||
+ bonds);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f状态: "
|
||||
+ ("frozen".equals(
|
||||
status)
|
||||
? "§c冻结中"
|
||||
: "§a正常"));
|
||||
} else {
|
||||
int bonds = plugin.getBonds()
|
||||
.getBonds(
|
||||
sender.getName());
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f你的余额: §a"
|
||||
+ bonds);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
showHelp(sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== 流水行格式化 =====
|
||||
|
||||
private String formatTxLine(
|
||||
Map<String, Object> tx) {
|
||||
String type = (String) tx.get("type");
|
||||
int amount = ((Number) tx.get("amount"))
|
||||
.intValue();
|
||||
String tgt = (String) tx.get("target_player");
|
||||
String reason = (String) tx.get("reason");
|
||||
int bal = ((Number) tx.get("balance_after"))
|
||||
.intValue();
|
||||
long time = ((Number) tx.get("time"))
|
||||
.longValue();
|
||||
|
||||
boolean pos = amount > 0;
|
||||
String sign = pos ? "+" : "";
|
||||
String color = pos ? "§a" : "§c";
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("§7[")
|
||||
.append(SDF.format(new Date(time)))
|
||||
.append("] ")
|
||||
.append(formatTxType(type))
|
||||
.append(" ")
|
||||
.append(color).append(sign)
|
||||
.append(amount).append("§7");
|
||||
if (tgt != null && !tgt.isEmpty())
|
||||
sb.append(" §e↔ ").append(tgt);
|
||||
if (reason != null && !reason.isEmpty())
|
||||
sb.append(" §7(").append(reason)
|
||||
.append(")");
|
||||
sb.append(" §7余额:§f").append(bal);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatTxType(String type) {
|
||||
if (type == null) return "§7未知";
|
||||
switch (type) {
|
||||
case BondManager.TX_ADMIN_GIVE:
|
||||
return "§a管理员发放";
|
||||
case BondManager.TX_ADMIN_DEDUCT:
|
||||
return "§c管理员扣除";
|
||||
case BondManager.TX_TRANSFER_OUT:
|
||||
return "§c转出";
|
||||
case BondManager.TX_TRANSFER_IN:
|
||||
return "§a转入";
|
||||
case BondManager.TX_REDEEM:
|
||||
return "§aCDK兑换";
|
||||
case BondManager.TX_SHOP_BUY:
|
||||
return "§e商城购买";
|
||||
case BondManager.TX_DAILY_SIGN:
|
||||
return "§a签到奖励";
|
||||
case BondManager.TX_FREEZE:
|
||||
return "§c冻结";
|
||||
case BondManager.TX_UNFREEZE:
|
||||
return "§a解冻";
|
||||
default:
|
||||
return "§7" + type;
|
||||
}
|
||||
}
|
||||
|
||||
private void showHelp(CommandSender sender) {
|
||||
sender.sendMessage("§e§l债券命令帮助:");
|
||||
sender.sendMessage(
|
||||
"§e/cypay §7- 查询自己余额");
|
||||
sender.sendMessage(
|
||||
"§e/cypay <玩家> §7- 查余额(管理员)");
|
||||
sender.sendMessage(
|
||||
"§e/cypay <玩家> <金额> §7- 转账");
|
||||
if (hasAdminPerm(sender)) {
|
||||
sender.sendMessage(
|
||||
"§e/cypay give <玩家> <金额> §7- 给债券");
|
||||
sender.sendMessage(
|
||||
"§e/cypay remove <玩家> <金额> §7- 扣债券");
|
||||
sender.sendMessage(
|
||||
"§e/cypay import <文件> §7- 导入CDK");
|
||||
sender.sendMessage(
|
||||
"§e/cypay freeze <玩家> §7- 冻结");
|
||||
sender.sendMessage(
|
||||
"§e/cypay unfreeze <玩家> §7- 解冻");
|
||||
sender.sendMessage(
|
||||
"§e/cypay water <玩家> [天数] §7- 流水");
|
||||
sender.sendMessage(
|
||||
"§e/cypay printer §7- 导出全服流水xlsx");
|
||||
sender.sendMessage(
|
||||
"§e/cypay printer <玩家> §7- 导出指定玩家xlsx");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender, Command command,
|
||||
String label, String[] args) {
|
||||
List<String> list = new ArrayList<>();
|
||||
|
||||
if (args.length == 1) {
|
||||
String last = args[0].toLowerCase();
|
||||
if (hasAdminPerm(sender)) {
|
||||
list.add("give");
|
||||
list.add("remove");
|
||||
list.add("import");
|
||||
list.add("freeze");
|
||||
list.add("unfreeze");
|
||||
list.add("water");
|
||||
list.add("printer");
|
||||
|
||||
File cdkDir = new File(
|
||||
plugin.getDataFolder(),
|
||||
"cdk");
|
||||
if (cdkDir.exists()) {
|
||||
File[] files = cdkDir.listFiles(
|
||||
(d, n) -> n
|
||||
.endsWith(".txt"));
|
||||
if (files != null)
|
||||
for (File f : files)
|
||||
list.add(f.getName());
|
||||
}
|
||||
}
|
||||
for (Player o :
|
||||
Bukkit.getOnlinePlayers())
|
||||
list.add(o.getName());
|
||||
return filter(list, last);
|
||||
}
|
||||
|
||||
if (args.length == 2) {
|
||||
String first = args[0].toLowerCase();
|
||||
if ("give".equals(first)
|
||||
|| "remove".equals(first)
|
||||
|| "freeze".equals(first)
|
||||
|| "unfreeze".equals(first)
|
||||
|| "water".equals(first)) {
|
||||
for (Player o :
|
||||
Bukkit.getOnlinePlayers())
|
||||
list.add(o.getName());
|
||||
return filter(list,
|
||||
args[1].toLowerCase());
|
||||
}
|
||||
list.add("10");
|
||||
list.add("50");
|
||||
list.add("100");
|
||||
return filter(list,
|
||||
args[1].toLowerCase());
|
||||
}
|
||||
|
||||
if (args.length == 3) {
|
||||
String first = args[0].toLowerCase();
|
||||
if ("give".equals(first)
|
||||
|| "remove".equals(first)) {
|
||||
list.add("10");
|
||||
list.add("50");
|
||||
list.add("100");
|
||||
list.add("999");
|
||||
return filter(list,
|
||||
args[2].toLowerCase());
|
||||
}
|
||||
if ("water".equals(first)) {
|
||||
list.add("1");
|
||||
list.add("3");
|
||||
list.add("7");
|
||||
list.add("14");
|
||||
return filter(list,
|
||||
args[2].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -60,9 +60,11 @@ public class DatabaseManager {
|
||||
+ "tasks_completed TEXT DEFAULT '',"
|
||||
+ "premium_uuid TEXT DEFAULT '',"
|
||||
+ "register_ip TEXT DEFAULT '',"
|
||||
+ "register_type TEXT DEFAULT 'manual'"
|
||||
+ "register_type TEXT DEFAULT 'manual',"
|
||||
+ "backcheck_dates TEXT DEFAULT ''"
|
||||
+ ")");
|
||||
|
||||
|
||||
// 安全添加 users 缺失列
|
||||
safeAdd(st, "tasks_completed",
|
||||
"TEXT DEFAULT ''");
|
||||
@@ -160,6 +162,40 @@ public class DatabaseManager {
|
||||
+ "alert_date TEXT NOT NULL,"
|
||||
+ "notified INTEGER DEFAULT 0,"
|
||||
+ "notified_time INTEGER DEFAULT 0)");
|
||||
// PVP区域表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "pvp_regions ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "name TEXT NOT NULL UNIQUE,"
|
||||
+ "world TEXT NOT NULL,"
|
||||
+ "x1 INTEGER NOT NULL,"
|
||||
+ "y1 INTEGER NOT NULL,"
|
||||
+ "z1 INTEGER NOT NULL,"
|
||||
+ "x2 INTEGER NOT NULL,"
|
||||
+ "y2 INTEGER NOT NULL,"
|
||||
+ "z2 INTEGER NOT NULL,"
|
||||
+ "create_time INTEGER DEFAULT 0"
|
||||
+ ")");
|
||||
|
||||
// PVP击杀总榜表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "pvp_kills ("
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "region_name TEXT NOT NULL,"
|
||||
+ "kills INTEGER DEFAULT 0,"
|
||||
+ "deaths INTEGER DEFAULT 0,"
|
||||
+ "PRIMARY KEY(player_name, region_name)"
|
||||
+ ")");
|
||||
|
||||
|
||||
// 菜单图标表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "menu_icons ("
|
||||
+ "player_name TEXT PRIMARY KEY,"
|
||||
+ "icon_b64 TEXT DEFAULT '',"
|
||||
+ "item_name TEXT DEFAULT '',"
|
||||
+ "save_time INTEGER DEFAULT 0"
|
||||
+ ")");
|
||||
|
||||
// 工单表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
@@ -239,6 +275,76 @@ public class DatabaseManager {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
// ==================== 菜单图标 ====================
|
||||
|
||||
public void saveMenuIcon(String name,
|
||||
String iconB64, String itemName) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR REPLACE INTO menu_icons "
|
||||
+ "(player_name, icon_b64, "
|
||||
+ "item_name, save_time) "
|
||||
+ "VALUES (?,?,?,?)");
|
||||
ps.setString(1, name);
|
||||
ps.setString(2, iconB64);
|
||||
ps.setString(3, itemName);
|
||||
ps.setLong(4,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public String getMenuIcon(String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT icon_b64 FROM menu_icons "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, name);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
String b64 = null;
|
||||
if (rs.next()) {
|
||||
b64 = rs.getString("icon_b64");
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return b64;
|
||||
} catch (SQLException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteMenuIcon(String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"DELETE FROM menu_icons "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, name);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasMenuIcon(String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT 1 FROM menu_icons "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND icon_b64 IS NOT NULL "
|
||||
+ "AND icon_b64 != '' "
|
||||
+ "LIMIT 1");
|
||||
ps.setString(1, name);
|
||||
boolean r = ps.executeQuery().next();
|
||||
ps.close();
|
||||
return r;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 用户操作 ====================
|
||||
|
||||
@@ -747,6 +853,252 @@ public class DatabaseManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ==================== PVP区域 ====================
|
||||
|
||||
public void savePVPRegion(String name,
|
||||
String world,
|
||||
int x1, int y1, int z1,
|
||||
int x2, int y2, int z2) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"INSERT OR REPLACE "
|
||||
+ "INTO pvp_regions "
|
||||
+ "(name, world, "
|
||||
+ "x1, y1, z1, "
|
||||
+ "x2, y2, z2, "
|
||||
+ "create_time) "
|
||||
+ "VALUES "
|
||||
+ "(?,?,?,?,?,?,?,?,?)");
|
||||
ps.setString(1, name);
|
||||
ps.setString(2, world);
|
||||
ps.setInt(3, x1);
|
||||
ps.setInt(4, y1);
|
||||
ps.setInt(5, z1);
|
||||
ps.setInt(6, x2);
|
||||
ps.setInt(7, y2);
|
||||
ps.setInt(8, z2);
|
||||
ps.setLong(9,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object>
|
||||
getPVPRegion(String name) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"SELECT * FROM "
|
||||
+ "pvp_regions "
|
||||
+ "WHERE name=?");
|
||||
ps.setString(1, name);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
for (int i = 1; i <= rs
|
||||
.getMetaData()
|
||||
.getColumnCount(); i++) {
|
||||
row.put(rs.getMetaData()
|
||||
.getColumnName(i),
|
||||
rs.getObject(i));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return row;
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>>
|
||||
getAllPVPRegions() {
|
||||
List<Map<String, Object>> list =
|
||||
new ArrayList<>();
|
||||
try {
|
||||
Statement st =
|
||||
db.createStatement();
|
||||
ResultSet rs =
|
||||
st.executeQuery(
|
||||
"SELECT * FROM "
|
||||
+ "pvp_regions");
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
for (int i = 1; i <= rs
|
||||
.getMetaData()
|
||||
.getColumnCount(); i++) {
|
||||
row.put(rs.getMetaData()
|
||||
.getColumnName(i),
|
||||
rs.getObject(i));
|
||||
}
|
||||
list.add(row);
|
||||
}
|
||||
rs.close();
|
||||
st.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public void deletePVPRegion(String name) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"DELETE FROM "
|
||||
+ "pvp_regions "
|
||||
+ "WHERE name=?");
|
||||
ps.setString(1, name);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PVP击杀 ====================
|
||||
|
||||
public int getPVPKills(
|
||||
String player, String region) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"SELECT kills FROM "
|
||||
+ "pvp_kills "
|
||||
+ "WHERE "
|
||||
+ "player_name=? "
|
||||
+ "AND region_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, region);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
int kills = 0;
|
||||
if (rs.next())
|
||||
kills = rs.getInt("kills");
|
||||
rs.close();
|
||||
ps.close();
|
||||
return kills;
|
||||
} catch (SQLException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int getPVPDeaths(
|
||||
String player, String region) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"SELECT deaths FROM "
|
||||
+ "pvp_kills "
|
||||
+ "WHERE "
|
||||
+ "player_name=? "
|
||||
+ "AND region_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, region);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
int deaths = 0;
|
||||
if (rs.next())
|
||||
deaths = rs.getInt("deaths");
|
||||
rs.close();
|
||||
ps.close();
|
||||
return deaths;
|
||||
} catch (SQLException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void addPVPKill(
|
||||
String player, String region) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"INSERT INTO pvp_kills "
|
||||
+ "(player_name, "
|
||||
+ "region_name, "
|
||||
+ "kills, deaths) "
|
||||
+ "VALUES (?,?,1,0) "
|
||||
+ "ON CONFLICT"
|
||||
+ "(player_name, "
|
||||
+ "region_name) "
|
||||
+ "DO UPDATE SET "
|
||||
+ "kills=kills+1");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, region);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void addPVPDeath(
|
||||
String player, String region) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"INSERT INTO pvp_kills "
|
||||
+ "(player_name, "
|
||||
+ "region_name, "
|
||||
+ "kills, deaths) "
|
||||
+ "VALUES (?,?,0,1) "
|
||||
+ "ON CONFLICT"
|
||||
+ "(player_name, "
|
||||
+ "region_name) "
|
||||
+ "DO UPDATE SET "
|
||||
+ "deaths=deaths+1");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, region);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String, Object>>
|
||||
getPVPKillTop(String region, int limit) {
|
||||
List<Map<String, Object>> list =
|
||||
new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"SELECT player_name, "
|
||||
+ "kills, deaths "
|
||||
+ "FROM pvp_kills "
|
||||
+ "WHERE "
|
||||
+ "region_name=? "
|
||||
+ "AND kills>0 "
|
||||
+ "ORDER BY kills "
|
||||
+ "DESC LIMIT ?");
|
||||
ps.setString(1, region);
|
||||
ps.setInt(2, limit);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("player_name",
|
||||
rs.getString("player_name"));
|
||||
row.put("kills",
|
||||
rs.getInt("kills"));
|
||||
row.put("deaths",
|
||||
rs.getInt("deaths"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
// ==================== 安全报警 ====================
|
||||
@@ -773,6 +1125,7 @@ public class DatabaseManager {
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public List<Map<String, Object>>
|
||||
|
||||
@@ -1,275 +1,284 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.util.Base64;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class EmailManager {
|
||||
|
||||
private final ConfigManager config;
|
||||
private final Logger logger;
|
||||
|
||||
public EmailManager(ConfigManager config) {
|
||||
this.config = config;
|
||||
this.logger = Logger.getLogger("Sdf1_login");
|
||||
}
|
||||
|
||||
public boolean isConfigured() {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
return host != null && !host.isEmpty()
|
||||
&& user != null && !user.isEmpty()
|
||||
&& pass != null && !pass.isEmpty();
|
||||
}
|
||||
|
||||
public boolean sendVerifyCode(String to,
|
||||
String code) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置,"
|
||||
+ "无法发送验证码");
|
||||
return false;
|
||||
}
|
||||
logger.info("[Sdf1_login] 发送验证码到 " + to);
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 验证码",
|
||||
"您的验证码: " + code
|
||||
+ "\n5分钟内有效。\n"
|
||||
+ "如非本人操作请忽略。");
|
||||
}
|
||||
|
||||
public boolean sendTempPassword(String to,
|
||||
String player, String tempPwd) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置");
|
||||
return false;
|
||||
}
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 临时密码",
|
||||
"玩家 " + player
|
||||
+ " 的临时密码: " + tempPwd
|
||||
+ "\n请尽快登录并修改密码!");
|
||||
}
|
||||
|
||||
private boolean sendEmailSync(String to,
|
||||
String subject, String body) {
|
||||
final boolean[] result = {false};
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
result[0] = sendEmail(
|
||||
to, subject, body);
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件线程异常: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}, "Sdf1_login-SMTP");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
try {
|
||||
t.join(15000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
if (t.isAlive()) {
|
||||
t.interrupt();
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP超时(15秒)");
|
||||
return false;
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private boolean sendEmail(String to,
|
||||
String subject, String body) {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String portStr = config.getSmtp("smtp端口");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
String fromName = config.getSmtp(
|
||||
"发件人名称");
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} catch (Exception e) {
|
||||
port = 465;
|
||||
}
|
||||
|
||||
try {
|
||||
String encSubject = "=?UTF-8?B?"
|
||||
+ Base64.getEncoder()
|
||||
.encodeToString(
|
||||
subject.getBytes("UTF-8"))
|
||||
+ "?=";
|
||||
String encBody = Base64.getEncoder()
|
||||
.encodeToString(
|
||||
body.getBytes("UTF-8"));
|
||||
|
||||
if (port == 465) {
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket socket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
host, port);
|
||||
socket.setSoTimeout(10000);
|
||||
doSmtp(socket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
socket.close();
|
||||
} else {
|
||||
Socket plainSocket =
|
||||
new Socket(host, port);
|
||||
plainSocket.setSoTimeout(10000);
|
||||
BufferedReader preReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
plainSocket
|
||||
.getInputStream(),
|
||||
"UTF-8"));
|
||||
PrintWriter preWriter =
|
||||
new PrintWriter(
|
||||
plainSocket
|
||||
.getOutputStream(),
|
||||
true);
|
||||
checkResponse(preReader, "220");
|
||||
sendLine(preWriter,
|
||||
"EHLO " + host);
|
||||
checkResponse(preReader, "250");
|
||||
sendLine(preWriter, "STARTTLS");
|
||||
checkResponse(preReader, "220");
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket sslSocket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
plainSocket, host,
|
||||
port, true);
|
||||
sslSocket.startHandshake();
|
||||
doSmtp(sslSocket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
sslSocket.close();
|
||||
}
|
||||
logger.info(
|
||||
"[Sdf1_login] 邮件发送成功 -> "
|
||||
+ to);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件异常: "
|
||||
+ e.getClass()
|
||||
.getSimpleName()
|
||||
+ ": " + e.getMessage());
|
||||
if (e.getCause() != null) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 根因: "
|
||||
+ e.getCause()
|
||||
.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void doSmtp(SSLSocket socket,
|
||||
String host, String user, String pass,
|
||||
String to, String subject, String body,
|
||||
String fromName) throws Exception {
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
socket.getInputStream(),
|
||||
"UTF-8"));
|
||||
OutputStream rawOut =
|
||||
socket.getOutputStream();
|
||||
PrintWriter writer =
|
||||
new PrintWriter(
|
||||
new java.io.BufferedOutputStream(
|
||||
rawOut), true);
|
||||
|
||||
checkResponse(reader, "220");
|
||||
sendLine(writer, "EHLO " + host);
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "AUTH LOGIN");
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
user.getBytes("UTF-8")));
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
pass.getBytes("UTF-8")));
|
||||
checkResponse(reader, "235");
|
||||
sendLine(writer,
|
||||
"MAIL FROM:<" + user + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer,
|
||||
"RCPT TO:<" + to + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "DATA");
|
||||
checkResponse(reader, "354");
|
||||
sendLine(writer,
|
||||
"From: " + fromName
|
||||
+ " <" + user + ">");
|
||||
sendLine(writer,
|
||||
"To: <" + to + ">");
|
||||
sendLine(writer,
|
||||
"Reply-To: <" + user + ">");
|
||||
sendLine(writer,
|
||||
"Subject: " + subject);
|
||||
sendLine(writer, "MIME-Version: 1.0");
|
||||
sendLine(writer,
|
||||
"Content-Type: text/plain; "
|
||||
+ "charset=UTF-8");
|
||||
sendLine(writer,
|
||||
"Content-Transfer-Encoding: "
|
||||
+ "base64");
|
||||
sendLine(writer, "");
|
||||
sendLine(writer, body);
|
||||
sendLine(writer, ".");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "QUIT");
|
||||
}
|
||||
|
||||
private void sendLine(PrintWriter writer,
|
||||
String line) {
|
||||
writer.print(line + "\r\n");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
private void checkResponse(
|
||||
BufferedReader reader,
|
||||
String expected) throws Exception {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
if (line.length() >= 4
|
||||
&& Character.isDigit(
|
||||
line.charAt(0))
|
||||
&& line.charAt(3) == ' ')
|
||||
break;
|
||||
}
|
||||
String resp = sb.toString().trim();
|
||||
if (!resp.startsWith(expected)) {
|
||||
if (resp.startsWith("535"))
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP 535 "
|
||||
+ "认证失败");
|
||||
throw new Exception(
|
||||
"SMTP响应异常: 期望 "
|
||||
+ expected
|
||||
+ ",实际: " + resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.util.Base64;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class EmailManager {
|
||||
|
||||
private final ConfigManager config;
|
||||
private final Logger logger;
|
||||
|
||||
public EmailManager(ConfigManager config) {
|
||||
this.config = config;
|
||||
this.logger = Logger.getLogger("Sdf1_login");
|
||||
}
|
||||
|
||||
public boolean isConfigured() {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
return host != null && !host.isEmpty()
|
||||
&& user != null && !user.isEmpty()
|
||||
&& pass != null && !pass.isEmpty();
|
||||
}
|
||||
|
||||
public boolean sendVerifyCode(String to,
|
||||
String code) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置,"
|
||||
+ "无法发送验证码");
|
||||
return false;
|
||||
}
|
||||
logger.info("[Sdf1_login] 发送验证码到 " + to);
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 验证码",
|
||||
"您的验证码: " + code
|
||||
+ "\n5分钟内有效。\n"
|
||||
+ "如非本人操作请忽略。");
|
||||
}
|
||||
|
||||
public boolean sendTempPassword(String to,
|
||||
String player, String tempPwd) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置");
|
||||
return false;
|
||||
}
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 临时密码",
|
||||
"玩家 " + player
|
||||
+ " 的临时密码: " + tempPwd
|
||||
+ "\n请尽快登录并修改密码!");
|
||||
}
|
||||
|
||||
public boolean sendBody(String to, String subject, String body) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning("[Sdf1_login] SMTP未配置");
|
||||
return false;
|
||||
}
|
||||
return sendEmailSync(to, subject, body);
|
||||
}
|
||||
|
||||
|
||||
private boolean sendEmailSync(String to,
|
||||
String subject, String body) {
|
||||
final boolean[] result = {false};
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
result[0] = sendEmail(
|
||||
to, subject, body);
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件线程异常: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}, "Sdf1_login-SMTP");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
try {
|
||||
t.join(15000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
if (t.isAlive()) {
|
||||
t.interrupt();
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP超时(15秒)");
|
||||
return false;
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private boolean sendEmail(String to,
|
||||
String subject, String body) {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String portStr = config.getSmtp("smtp端口");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
String fromName = config.getSmtp(
|
||||
"发件人名称");
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} catch (Exception e) {
|
||||
port = 465;
|
||||
}
|
||||
|
||||
try {
|
||||
String encSubject = "=?UTF-8?B?"
|
||||
+ Base64.getEncoder()
|
||||
.encodeToString(
|
||||
subject.getBytes("UTF-8"))
|
||||
+ "?=";
|
||||
String encBody = Base64.getEncoder()
|
||||
.encodeToString(
|
||||
body.getBytes("UTF-8"));
|
||||
|
||||
if (port == 465) {
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket socket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
host, port);
|
||||
socket.setSoTimeout(10000);
|
||||
doSmtp(socket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
socket.close();
|
||||
} else {
|
||||
Socket plainSocket =
|
||||
new Socket(host, port);
|
||||
plainSocket.setSoTimeout(10000);
|
||||
BufferedReader preReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
plainSocket
|
||||
.getInputStream(),
|
||||
"UTF-8"));
|
||||
PrintWriter preWriter =
|
||||
new PrintWriter(
|
||||
plainSocket
|
||||
.getOutputStream(),
|
||||
true);
|
||||
checkResponse(preReader, "220");
|
||||
sendLine(preWriter,
|
||||
"EHLO " + host);
|
||||
checkResponse(preReader, "250");
|
||||
sendLine(preWriter, "STARTTLS");
|
||||
checkResponse(preReader, "220");
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket sslSocket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
plainSocket, host,
|
||||
port, true);
|
||||
sslSocket.startHandshake();
|
||||
doSmtp(sslSocket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
sslSocket.close();
|
||||
}
|
||||
logger.info(
|
||||
"[Sdf1_login] 邮件发送成功 -> "
|
||||
+ to);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件异常: "
|
||||
+ e.getClass()
|
||||
.getSimpleName()
|
||||
+ ": " + e.getMessage());
|
||||
if (e.getCause() != null) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 根因: "
|
||||
+ e.getCause()
|
||||
.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void doSmtp(SSLSocket socket,
|
||||
String host, String user, String pass,
|
||||
String to, String subject, String body,
|
||||
String fromName) throws Exception {
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
socket.getInputStream(),
|
||||
"UTF-8"));
|
||||
OutputStream rawOut =
|
||||
socket.getOutputStream();
|
||||
PrintWriter writer =
|
||||
new PrintWriter(
|
||||
new java.io.BufferedOutputStream(
|
||||
rawOut), true);
|
||||
|
||||
checkResponse(reader, "220");
|
||||
sendLine(writer, "EHLO " + host);
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "AUTH LOGIN");
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
user.getBytes("UTF-8")));
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
pass.getBytes("UTF-8")));
|
||||
checkResponse(reader, "235");
|
||||
sendLine(writer,
|
||||
"MAIL FROM:<" + user + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer,
|
||||
"RCPT TO:<" + to + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "DATA");
|
||||
checkResponse(reader, "354");
|
||||
sendLine(writer,
|
||||
"From: " + fromName
|
||||
+ " <" + user + ">");
|
||||
sendLine(writer,
|
||||
"To: <" + to + ">");
|
||||
sendLine(writer,
|
||||
"Reply-To: <" + user + ">");
|
||||
sendLine(writer,
|
||||
"Subject: " + subject);
|
||||
sendLine(writer, "MIME-Version: 1.0");
|
||||
sendLine(writer,
|
||||
"Content-Type: text/plain; "
|
||||
+ "charset=UTF-8");
|
||||
sendLine(writer,
|
||||
"Content-Transfer-Encoding: "
|
||||
+ "base64");
|
||||
sendLine(writer, "");
|
||||
sendLine(writer, body);
|
||||
sendLine(writer, ".");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "QUIT");
|
||||
}
|
||||
|
||||
private void sendLine(PrintWriter writer,
|
||||
String line) {
|
||||
writer.print(line + "\r\n");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
private void checkResponse(
|
||||
BufferedReader reader,
|
||||
String expected) throws Exception {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
if (line.length() >= 4
|
||||
&& Character.isDigit(
|
||||
line.charAt(0))
|
||||
&& line.charAt(3) == ' ')
|
||||
break;
|
||||
}
|
||||
String resp = sb.toString().trim();
|
||||
if (!resp.startsWith(expected)) {
|
||||
if (resp.startsWith("535"))
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP 535 "
|
||||
+ "认证失败");
|
||||
throw new Exception(
|
||||
"SMTP响应异常: 期望 "
|
||||
+ expected
|
||||
+ ",实际: " + resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,110 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IPGroupManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final int maxAccounts;
|
||||
|
||||
public IPGroupManager(Main plugin, int maxAccounts) {
|
||||
this.plugin = plugin;
|
||||
this.maxAccounts = maxAccounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查该IP是否还能注册新账号
|
||||
*/
|
||||
public boolean canRegister(String ip) {
|
||||
if (maxAccounts <= 0) return true;
|
||||
if (ip == null || ip.isEmpty()) return true;
|
||||
return queryAccountsByIP(ip).size() < maxAccounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该IP下已注册的账号数量
|
||||
*/
|
||||
public int getAccountCount(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return 0;
|
||||
return queryAccountsByIP(ip).size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该IP下所有账号名
|
||||
*/
|
||||
public List<String> getAccounts(String ip) {
|
||||
return queryAccountsByIP(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接通过数据库连接查询指定IP下的所有账号
|
||||
*/
|
||||
private List<String> queryAccountsByIP(String ip) {
|
||||
List<String> accounts = new ArrayList<>();
|
||||
Connection conn = plugin.getDb().getDb();
|
||||
if (conn == null) return accounts;
|
||||
try {
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE ip_address = ?");
|
||||
ps.setString(1, ip);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
accounts.add(
|
||||
rs.getString("player_name"));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IPGroupManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final int maxAccounts;
|
||||
|
||||
public IPGroupManager(Main plugin, int maxAccounts) {
|
||||
this.plugin = plugin;
|
||||
this.maxAccounts = maxAccounts;
|
||||
}
|
||||
|
||||
public boolean canRegister(String ip) {
|
||||
if (maxAccounts <= 0) return true;
|
||||
if (ip == null || ip.isEmpty()) return true;
|
||||
int count = getAccountCount(ip);
|
||||
boolean can = count < maxAccounts;
|
||||
plugin.getLogger().info(
|
||||
"[IP限制] IP=" + ip
|
||||
+ " 已注册=" + count
|
||||
+ " 上限=" + maxAccounts
|
||||
+ " 允许=" + can);
|
||||
return can;
|
||||
}
|
||||
|
||||
public int getAccountCount(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return 0;
|
||||
return queryAccountsByIP(ip).size();
|
||||
}
|
||||
|
||||
public List<String> getAccounts(String ip) {
|
||||
return queryAccountsByIP(ip);
|
||||
}
|
||||
|
||||
private List<String> queryAccountsByIP(String ip) {
|
||||
List<String> accounts = new ArrayList<>();
|
||||
Connection conn = plugin.getDb().getDb();
|
||||
if (conn == null) return accounts;
|
||||
try {
|
||||
// 主查询:ip_address 列
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE ip_address = ?");
|
||||
ps.setString(1, ip);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
accounts.add(
|
||||
rs.getString("player_name"));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
|
||||
// 兜底:如果主列没数据,查 last_login_ip
|
||||
if (accounts.isEmpty()) {
|
||||
PreparedStatement ps2 = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE last_login_ip = ? "
|
||||
+ "AND (ip_address IS NULL "
|
||||
+ "OR ip_address = '' "
|
||||
+ "OR ip_address != ?)");
|
||||
ps2.setString(1, ip);
|
||||
ps2.setString(2, ip);
|
||||
ResultSet rs2 = ps2.executeQuery();
|
||||
while (rs2.next()) {
|
||||
String name = rs2.getString("player_name");
|
||||
if (!accounts.contains(name))
|
||||
accounts.add(name);
|
||||
}
|
||||
rs2.close();
|
||||
ps2.close();
|
||||
}
|
||||
|
||||
// 兜底2:查 register_ip
|
||||
if (accounts.isEmpty()) {
|
||||
PreparedStatement ps3 = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE register_ip = ? "
|
||||
+ "AND (ip_address IS NULL "
|
||||
+ "OR ip_address = '') "
|
||||
+ "AND (last_login_ip IS NULL "
|
||||
+ "OR last_login_ip = '' "
|
||||
+ "OR last_login_ip != ?)");
|
||||
ps3.setString(1, ip);
|
||||
ps3.setString(2, ip);
|
||||
ResultSet rs3 = ps3.executeQuery();
|
||||
while (rs3.next()) {
|
||||
String name = rs3.getString("player_name");
|
||||
if (!accounts.contains(name))
|
||||
accounts.add(name);
|
||||
}
|
||||
rs3.close();
|
||||
ps3.close();
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[IP限制] 查询IP=" + ip
|
||||
+ " 结果数=" + accounts.size()
|
||||
+ " 账号=" + accounts);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class InventorySerializer {
|
||||
|
||||
public static String toBase64(ItemStack[] items) throws IOException {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
|
||||
dataOutput.writeInt(items.length);
|
||||
for (ItemStack item : items) {
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
dataOutput.close();
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to serialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemStack[] fromBase64(String data) throws IOException {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
try {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
|
||||
ItemStack[] items = new ItemStack[dataInput.readInt()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = (ItemStack) dataInput.readObject();
|
||||
}
|
||||
dataInput.close();
|
||||
return items;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to deserialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class InventorySerializer {
|
||||
|
||||
public static String toBase64(ItemStack[] items) throws IOException {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
|
||||
dataOutput.writeInt(items.length);
|
||||
for (ItemStack item : items) {
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
dataOutput.close();
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to serialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemStack[] fromBase64(String data) throws IOException {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
try {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
|
||||
ItemStack[] items = new ItemStack[dataInput.readInt()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = (ItemStack) dataInput.readObject();
|
||||
}
|
||||
dataInput.close();
|
||||
return items;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to deserialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,16 @@ public class LoginManager {
|
||||
|
||||
public boolean handleLogin(Player p,
|
||||
String[] args) {
|
||||
|
||||
String name = p.getName();
|
||||
// ★ 在 handleLogin 方法开头
|
||||
if (plugin.getNeedsPasswordChange().contains(name)
|
||||
&& plugin.getLoggedIn().contains(name)) {
|
||||
// 已登录但还没改密码,只允许执行 /sdf1_login pw
|
||||
p.sendMessage("§c请先修改密码: /sdf1_login pw <旧密码> <新密码>");
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (plugin.getLoggedIn().contains(name)) {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
@@ -179,27 +188,21 @@ public class LoginManager {
|
||||
name, hash);
|
||||
if (matchTemp) {
|
||||
plugin.getLoggedIn().add(name);
|
||||
plugin.getDb().setLoggedIn(
|
||||
name, true);
|
||||
plugin.getDb().setLoggedIn(name, true);
|
||||
p.setAllowFlight(false);
|
||||
p.setFlying(false);
|
||||
plugin.getDb().setField(name,
|
||||
"last_login_time",
|
||||
plugin.getDb().setField(name, "last_login_time",
|
||||
System.currentTimeMillis());
|
||||
plugin.getDb().setField(name,
|
||||
"last_online_check",
|
||||
plugin.getDb().setField(name, "last_online_check",
|
||||
System.currentTimeMillis());
|
||||
plugin.restoreInventory(p);
|
||||
plugin.recordIPLogin(p);
|
||||
plugin.giveMenuSnowball(p);
|
||||
plugin.getNeedsPasswordChange()
|
||||
.add(name);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("login_success"));
|
||||
p.sendMessage(
|
||||
"§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
|
||||
p.sendMessage(
|
||||
"§7用法: /sdf1_login pw");
|
||||
// ★ 确保这行存在
|
||||
plugin.getNeedsPasswordChange().add(name);
|
||||
p.sendMessage(plugin.getConfig2().msg("login_success"));
|
||||
p.sendMessage("§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
|
||||
p.sendMessage("§7用法: /sdf1_login pw");
|
||||
activateCY(p, name);
|
||||
return true;
|
||||
}
|
||||
|
||||
+1213
-411
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Base64;
|
||||
|
||||
public class MenuIconManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final NamespacedKey iconKey;
|
||||
|
||||
public MenuIconManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.iconKey = new NamespacedKey(
|
||||
plugin, "custom_menu_icon");
|
||||
}
|
||||
|
||||
// ========== GUIManager 调用的方法 ==========
|
||||
|
||||
/**
|
||||
* Base64 → ItemStack
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().deserializeItem(...)
|
||||
*/
|
||||
public ItemStack deserializeItem(String b64) {
|
||||
if (b64 == null || b64.isEmpty())
|
||||
return null;
|
||||
try {
|
||||
byte[] data =
|
||||
Base64.getDecoder().decode(b64);
|
||||
ByteArrayInputStream bais =
|
||||
new ByteArrayInputStream(data);
|
||||
BukkitObjectInputStream is =
|
||||
new BukkitObjectInputStream(bais);
|
||||
Object obj = is.readObject();
|
||||
is.close();
|
||||
if (obj instanceof ItemStack)
|
||||
return (ItemStack) obj;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ItemStack → Base64
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().serializeItem(...)
|
||||
*/
|
||||
public String serializeItem(ItemStack item) {
|
||||
if (item == null) return null;
|
||||
try {
|
||||
ByteArrayOutputStream baos =
|
||||
new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream os =
|
||||
new BukkitObjectOutputStream(baos);
|
||||
os.writeObject(item);
|
||||
os.close();
|
||||
return Base64.getEncoder()
|
||||
.encodeToString(
|
||||
baos.toByteArray());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给物品打上菜单图标标记
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().ensureMenuIconTag(...)
|
||||
*/
|
||||
public void ensureMenuIconTag(ItemStack item) {
|
||||
if (item == null) return;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return;
|
||||
meta.getPersistentDataContainer().set(
|
||||
iconKey,
|
||||
PersistentDataType.STRING,
|
||||
"menu");
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标Key
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().getIconKey()
|
||||
*/
|
||||
public NamespacedKey getIconKey() {
|
||||
return iconKey;
|
||||
}
|
||||
|
||||
// ========== LoginManager / Main 调用的方法 ==========
|
||||
|
||||
/**
|
||||
* 获取玩家自定义图标物品
|
||||
* 有返回物品,无返回null
|
||||
*/
|
||||
public ItemStack getIcon(Player player) {
|
||||
String b64 = plugin.getDb()
|
||||
.getMenuIcon(player.getName());
|
||||
if (b64 == null || b64.isEmpty())
|
||||
return null;
|
||||
ItemStack item = deserializeItem(b64);
|
||||
if (item == null) return null;
|
||||
ensureMenuIconTag(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图标到DB
|
||||
*/
|
||||
public void saveIcon(Player player,
|
||||
ItemStack item) {
|
||||
String b64 = serializeItem(item);
|
||||
if (b64 != null) {
|
||||
plugin.getDb().saveMenuIcon(
|
||||
player.getName(), b64,
|
||||
item.getType().name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图标
|
||||
*/
|
||||
public void removeIcon(Player player) {
|
||||
plugin.getDb().deleteMenuIcon(
|
||||
player.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查物品是否是自定义菜单图标
|
||||
*/
|
||||
public boolean isCustomMenuIcon(
|
||||
ItemStack item) {
|
||||
if (item == null) return false;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return false;
|
||||
return meta.getPersistentDataContainer()
|
||||
.has(iconKey,
|
||||
PersistentDataType.STRING);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,72 +1,72 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PasswordUtils {
|
||||
|
||||
private static final SecureRandom RANDOM =
|
||||
new SecureRandom();
|
||||
|
||||
public static String generateSalt() {
|
||||
byte[] salt = new byte[16];
|
||||
RANDOM.nextBytes(salt);
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
public static String hash(String password, String salt) {
|
||||
try {
|
||||
MessageDigest md =
|
||||
MessageDigest.getInstance("SHA-256");
|
||||
md.update(Base64.getDecoder().decode(salt));
|
||||
byte[] hash =
|
||||
md.digest(password.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(String password) {
|
||||
if (password == null) return false;
|
||||
if (password.length() < 6 || password.length() > 20)
|
||||
return false;
|
||||
boolean hasUpper = false;
|
||||
boolean hasLower = false;
|
||||
boolean hasDigit = false;
|
||||
for (char c : password.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) hasUpper = true;
|
||||
if (Character.isLowerCase(c)) hasLower = true;
|
||||
if (Character.isDigit(c)) hasDigit = true;
|
||||
}
|
||||
return hasUpper && hasLower && hasDigit;
|
||||
}
|
||||
|
||||
public static String generateTempPassword() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder(8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateInviteCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateVerifyCode() {
|
||||
int code = 100000 + RANDOM.nextInt(900000);
|
||||
return String.valueOf(code);
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PasswordUtils {
|
||||
|
||||
private static final SecureRandom RANDOM =
|
||||
new SecureRandom();
|
||||
|
||||
public static String generateSalt() {
|
||||
byte[] salt = new byte[16];
|
||||
RANDOM.nextBytes(salt);
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
public static String hash(String password, String salt) {
|
||||
try {
|
||||
MessageDigest md =
|
||||
MessageDigest.getInstance("SHA-256");
|
||||
md.update(Base64.getDecoder().decode(salt));
|
||||
byte[] hash =
|
||||
md.digest(password.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(String password) {
|
||||
if (password == null) return false;
|
||||
if (password.length() < 6 || password.length() > 20)
|
||||
return false;
|
||||
boolean hasUpper = false;
|
||||
boolean hasLower = false;
|
||||
boolean hasDigit = false;
|
||||
for (char c : password.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) hasUpper = true;
|
||||
if (Character.isLowerCase(c)) hasLower = true;
|
||||
if (Character.isDigit(c)) hasDigit = true;
|
||||
}
|
||||
return hasUpper && hasLower && hasDigit;
|
||||
}
|
||||
|
||||
public static String generateTempPassword() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder(8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateInviteCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateVerifyCode() {
|
||||
int code = 100000 + RANDOM.nextInt(900000);
|
||||
return String.valueOf(code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,374 +1,374 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
public class PointsManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Object cyInstance = null;
|
||||
private Method cyActivationMethod = null;
|
||||
private Method cyGetShopItemsMethod = null;
|
||||
|
||||
private final List<Map<String, Object>> cyItems =
|
||||
new ArrayList<>();
|
||||
|
||||
public PointsManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
discoverCY();
|
||||
}
|
||||
|
||||
private void discoverCY() {
|
||||
try {
|
||||
org.bukkit.plugin.Plugin p = Bukkit
|
||||
.getPluginManager()
|
||||
.getPlugin("CY_beibao");
|
||||
if (p != null && p.isEnabled()) {
|
||||
cyInstance = p;
|
||||
cyGetShopItemsMethod = p.getClass()
|
||||
.getMethod("onSdf1GetShopItems");
|
||||
cyActivationMethod = p.getClass()
|
||||
.getMethod("onSdf1Activation",
|
||||
String.class,
|
||||
int.class, int.class);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] CY_beibao 联控已连接");
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshCYItems() {
|
||||
cyItems.clear();
|
||||
if (cyInstance == null
|
||||
|| cyGetShopItemsMethod == null) return;
|
||||
try {
|
||||
Object result =
|
||||
cyGetShopItemsMethod.invoke(cyInstance);
|
||||
if (result == null) return;
|
||||
String data = result.toString();
|
||||
if (data.isEmpty()) return;
|
||||
String[] items = data.split(";");
|
||||
for (String itemStr : items) {
|
||||
String[] p = itemStr.split("\\|");
|
||||
if (p.length < 8) continue;
|
||||
Map<String, Object> item =
|
||||
new LinkedHashMap<>();
|
||||
item.put("id", p[0]);
|
||||
item.put("name", p[1]);
|
||||
item.put("days",
|
||||
Integer.parseInt(p[2]));
|
||||
item.put("slots",
|
||||
Integer.parseInt(p[3]));
|
||||
item.put("price",
|
||||
Integer.parseInt(p[4]));
|
||||
item.put("stock",
|
||||
Integer.parseInt(p[5]));
|
||||
item.put("icon", p[6]);
|
||||
item.put("lifetime",
|
||||
"1".equals(p[7]));
|
||||
cyItems.add(item);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 拉取CY商品失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void activateCY(String name,
|
||||
int slots, int days) {
|
||||
try {
|
||||
if (cyInstance == null
|
||||
|| cyActivationMethod == null) return;
|
||||
cyActivationMethod.invoke(
|
||||
cyInstance, name, slots, days);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] CY激活失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public int getPoints(String name) {
|
||||
return ((Number) plugin.getDb()
|
||||
.getField(name, "points")).intValue();
|
||||
}
|
||||
|
||||
public Inventory createShopGUI(Player p) {
|
||||
int points = getPoints(p.getName());
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, 54,
|
||||
"§d§l积分商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
inv.setItem(10, mkItem(Material.EMERALD,
|
||||
"§a游戏币充值",
|
||||
"§7消耗积分兑换游戏币",
|
||||
"§7100积分 = $1000",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(12, mkItem(Material.ENDER_CHEST,
|
||||
"§b背包空间",
|
||||
"§7消耗积分兑换背包格子",
|
||||
"§750积分 = 9格永久空间",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
|
||||
"§e经验瓶",
|
||||
"§7消耗积分兑换经验",
|
||||
"§720积分 = 10级经验",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
if (cyInstance != null) {
|
||||
inv.setItem(31, mkItem(Material.CHEST,
|
||||
"§6背包商城",
|
||||
"§7来自CY_beibao的商品",
|
||||
"",
|
||||
"§e点击查看"));
|
||||
}
|
||||
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public Inventory createCYShopGUI(Player p) {
|
||||
refreshCYItems();
|
||||
int points = getPoints(p.getName());
|
||||
int size = Math.max(54,
|
||||
((cyItems.size() / 9) + 1) * 9);
|
||||
if (size > 54) size = 54;
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, size,
|
||||
"§d§lCY背包商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
for (int i = 0; i < cyItems.size()
|
||||
&& i < 45; i++) {
|
||||
Map<String, Object> item = cyItems.get(i);
|
||||
String name = (String) item.get("name");
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7价格: §e" + price + " 积分");
|
||||
if (lifetime) {
|
||||
lore.add("§7格子: §e终身 +" + slots);
|
||||
} else {
|
||||
lore.add("§7格子: §e+" + slots);
|
||||
lore.add("§7时间: §e" + days + "天");
|
||||
}
|
||||
if (stock == 0) {
|
||||
lore.add("§7库存: §a无限");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§7库存: §c已下架");
|
||||
} else {
|
||||
lore.add("§7库存: §e" + stock);
|
||||
}
|
||||
lore.add("");
|
||||
if (points >= price && stock != -1) {
|
||||
lore.add("§e点击购买");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§c已下架");
|
||||
} else {
|
||||
lore.add("§c积分不足");
|
||||
}
|
||||
inv.setItem(i, mkItem(Material.CHEST,
|
||||
"§a" + name,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
inv.setItem(size - 1,
|
||||
mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public void addItem(String name,
|
||||
int price, String cmd2) {
|
||||
// 追加到商品配置文件
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.io.FileWriter fw =
|
||||
new java.io.FileWriter(file, true);
|
||||
fw.write(name + ":" + price
|
||||
+ ":" + cmd2 + "\n");
|
||||
fw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(int index) {
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.util.List<String> lines =
|
||||
new java.util.ArrayList<>();
|
||||
java.io.BufferedReader br =
|
||||
new java.io.BufferedReader(
|
||||
new java.io.FileReader(
|
||||
file));
|
||||
String line;
|
||||
int i = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.trim().isEmpty()
|
||||
|| line.startsWith("#")) {
|
||||
lines.add(line);
|
||||
continue;
|
||||
}
|
||||
if (i != index) {
|
||||
lines.add(line);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
br.close();
|
||||
java.io.PrintWriter pw =
|
||||
new java.io.PrintWriter(file);
|
||||
for (String l : lines) pw.println(l);
|
||||
pw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void handleClick(Player p, int slot) {
|
||||
int points = getPoints(p.getName());
|
||||
switch (slot) {
|
||||
case 10:
|
||||
if (points >= 100) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 100);
|
||||
if (plugin.getEconomy() != null) {
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1000);
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "100"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
if (points >= 50) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 50);
|
||||
activateCY(p.getName(), 9, 0);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "50"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 14:
|
||||
if (points >= 20) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 20);
|
||||
p.setLevel(p.getLevel() + 10);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "20"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 31:
|
||||
if (cyInstance != null) {
|
||||
p.openInventory(
|
||||
createCYShopGUI(p));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleCYClick(Player p, int slot) {
|
||||
if (slot < 0 || slot >= cyItems.size()
|
||||
|| slot >= 45) return;
|
||||
Map<String, Object> item = cyItems.get(slot);
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
if (stock == -1) {
|
||||
p.sendMessage("§c该商品已下架");
|
||||
return;
|
||||
}
|
||||
int points = getPoints(p.getName());
|
||||
if (points < price) {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getDb().deductPoints(p.getName(), price);
|
||||
int actDays = lifetime ? 0 : days;
|
||||
activateCY(p.getName(), slots, actDays);
|
||||
|
||||
String tl = lifetime
|
||||
? "终身" : (days + "天");
|
||||
p.sendMessage("§a购买成功!获得 "
|
||||
+ tl + " +" + slots + "格 背包空间");
|
||||
}
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack gl = mkItem(
|
||||
Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||
for (int i = 0; i < inv.getSize(); i++)
|
||||
inv.setItem(i, gl);
|
||||
}
|
||||
|
||||
private ItemStack mkItem(Material mat, String name,
|
||||
String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0)
|
||||
im.setLore(Arrays.asList(lore));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
public class PointsManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Object cyInstance = null;
|
||||
private Method cyActivationMethod = null;
|
||||
private Method cyGetShopItemsMethod = null;
|
||||
|
||||
private final List<Map<String, Object>> cyItems =
|
||||
new ArrayList<>();
|
||||
|
||||
public PointsManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
discoverCY();
|
||||
}
|
||||
|
||||
private void discoverCY() {
|
||||
try {
|
||||
org.bukkit.plugin.Plugin p = Bukkit
|
||||
.getPluginManager()
|
||||
.getPlugin("CY_beibao");
|
||||
if (p != null && p.isEnabled()) {
|
||||
cyInstance = p;
|
||||
cyGetShopItemsMethod = p.getClass()
|
||||
.getMethod("onSdf1GetShopItems");
|
||||
cyActivationMethod = p.getClass()
|
||||
.getMethod("onSdf1Activation",
|
||||
String.class,
|
||||
int.class, int.class);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] CY_beibao 联控已连接");
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshCYItems() {
|
||||
cyItems.clear();
|
||||
if (cyInstance == null
|
||||
|| cyGetShopItemsMethod == null) return;
|
||||
try {
|
||||
Object result =
|
||||
cyGetShopItemsMethod.invoke(cyInstance);
|
||||
if (result == null) return;
|
||||
String data = result.toString();
|
||||
if (data.isEmpty()) return;
|
||||
String[] items = data.split(";");
|
||||
for (String itemStr : items) {
|
||||
String[] p = itemStr.split("\\|");
|
||||
if (p.length < 8) continue;
|
||||
Map<String, Object> item =
|
||||
new LinkedHashMap<>();
|
||||
item.put("id", p[0]);
|
||||
item.put("name", p[1]);
|
||||
item.put("days",
|
||||
Integer.parseInt(p[2]));
|
||||
item.put("slots",
|
||||
Integer.parseInt(p[3]));
|
||||
item.put("price",
|
||||
Integer.parseInt(p[4]));
|
||||
item.put("stock",
|
||||
Integer.parseInt(p[5]));
|
||||
item.put("icon", p[6]);
|
||||
item.put("lifetime",
|
||||
"1".equals(p[7]));
|
||||
cyItems.add(item);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 拉取CY商品失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void activateCY(String name,
|
||||
int slots, int days) {
|
||||
try {
|
||||
if (cyInstance == null
|
||||
|| cyActivationMethod == null) return;
|
||||
cyActivationMethod.invoke(
|
||||
cyInstance, name, slots, days);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] CY激活失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public int getPoints(String name) {
|
||||
return ((Number) plugin.getDb()
|
||||
.getField(name, "points")).intValue();
|
||||
}
|
||||
|
||||
public Inventory createShopGUI(Player p) {
|
||||
int points = getPoints(p.getName());
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, 54,
|
||||
"§d§l积分商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
inv.setItem(10, mkItem(Material.EMERALD,
|
||||
"§a游戏币充值",
|
||||
"§7消耗积分兑换游戏币",
|
||||
"§7100积分 = $1000",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(12, mkItem(Material.ENDER_CHEST,
|
||||
"§b背包空间",
|
||||
"§7消耗积分兑换背包格子",
|
||||
"§750积分 = 9格永久空间",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
|
||||
"§e经验瓶",
|
||||
"§7消耗积分兑换经验",
|
||||
"§720积分 = 10级经验",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
if (cyInstance != null) {
|
||||
inv.setItem(31, mkItem(Material.CHEST,
|
||||
"§6背包商城",
|
||||
"§7来自CY_beibao的商品",
|
||||
"",
|
||||
"§e点击查看"));
|
||||
}
|
||||
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public Inventory createCYShopGUI(Player p) {
|
||||
refreshCYItems();
|
||||
int points = getPoints(p.getName());
|
||||
int size = Math.max(54,
|
||||
((cyItems.size() / 9) + 1) * 9);
|
||||
if (size > 54) size = 54;
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, size,
|
||||
"§d§lCY背包商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
for (int i = 0; i < cyItems.size()
|
||||
&& i < 45; i++) {
|
||||
Map<String, Object> item = cyItems.get(i);
|
||||
String name = (String) item.get("name");
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7价格: §e" + price + " 积分");
|
||||
if (lifetime) {
|
||||
lore.add("§7格子: §e终身 +" + slots);
|
||||
} else {
|
||||
lore.add("§7格子: §e+" + slots);
|
||||
lore.add("§7时间: §e" + days + "天");
|
||||
}
|
||||
if (stock == 0) {
|
||||
lore.add("§7库存: §a无限");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§7库存: §c已下架");
|
||||
} else {
|
||||
lore.add("§7库存: §e" + stock);
|
||||
}
|
||||
lore.add("");
|
||||
if (points >= price && stock != -1) {
|
||||
lore.add("§e点击购买");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§c已下架");
|
||||
} else {
|
||||
lore.add("§c积分不足");
|
||||
}
|
||||
inv.setItem(i, mkItem(Material.CHEST,
|
||||
"§a" + name,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
inv.setItem(size - 1,
|
||||
mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public void addItem(String name,
|
||||
int price, String cmd2) {
|
||||
// 追加到商品配置文件
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.io.FileWriter fw =
|
||||
new java.io.FileWriter(file, true);
|
||||
fw.write(name + ":" + price
|
||||
+ ":" + cmd2 + "\n");
|
||||
fw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(int index) {
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.util.List<String> lines =
|
||||
new java.util.ArrayList<>();
|
||||
java.io.BufferedReader br =
|
||||
new java.io.BufferedReader(
|
||||
new java.io.FileReader(
|
||||
file));
|
||||
String line;
|
||||
int i = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.trim().isEmpty()
|
||||
|| line.startsWith("#")) {
|
||||
lines.add(line);
|
||||
continue;
|
||||
}
|
||||
if (i != index) {
|
||||
lines.add(line);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
br.close();
|
||||
java.io.PrintWriter pw =
|
||||
new java.io.PrintWriter(file);
|
||||
for (String l : lines) pw.println(l);
|
||||
pw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void handleClick(Player p, int slot) {
|
||||
int points = getPoints(p.getName());
|
||||
switch (slot) {
|
||||
case 10:
|
||||
if (points >= 100) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 100);
|
||||
if (plugin.getEconomy() != null) {
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1000);
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "100"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
if (points >= 50) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 50);
|
||||
activateCY(p.getName(), 9, 0);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "50"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 14:
|
||||
if (points >= 20) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 20);
|
||||
p.setLevel(p.getLevel() + 10);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "20"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 31:
|
||||
if (cyInstance != null) {
|
||||
p.openInventory(
|
||||
createCYShopGUI(p));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleCYClick(Player p, int slot) {
|
||||
if (slot < 0 || slot >= cyItems.size()
|
||||
|| slot >= 45) return;
|
||||
Map<String, Object> item = cyItems.get(slot);
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
if (stock == -1) {
|
||||
p.sendMessage("§c该商品已下架");
|
||||
return;
|
||||
}
|
||||
int points = getPoints(p.getName());
|
||||
if (points < price) {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getDb().deductPoints(p.getName(), price);
|
||||
int actDays = lifetime ? 0 : days;
|
||||
activateCY(p.getName(), slots, actDays);
|
||||
|
||||
String tl = lifetime
|
||||
? "终身" : (days + "天");
|
||||
p.sendMessage("§a购买成功!获得 "
|
||||
+ tl + " +" + slots + "格 背包空间");
|
||||
}
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack gl = mkItem(
|
||||
Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||
for (int i = 0; i < inv.getSize(); i++)
|
||||
inv.setItem(i, gl);
|
||||
}
|
||||
|
||||
private ItemStack mkItem(Material mat, String name,
|
||||
String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0)
|
||||
im.setLore(Arrays.asList(lore));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
// 如果 Bonds 在 Sdf1_login 包下:
|
||||
import Sdf1_login.BondManager;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -892,7 +894,7 @@ public class QuestTracker {
|
||||
|
||||
/**
|
||||
* 发放阶段奖励
|
||||
* 含经济内容的走 Vault API
|
||||
* 含经济内容的走 Vault API或债券
|
||||
*/
|
||||
public void claimRewards(
|
||||
Player player,
|
||||
@@ -900,13 +902,20 @@ public class QuestTracker {
|
||||
if (qf == null
|
||||
|| qf.rewards == null
|
||||
|| qf.rewards.isEmpty()) {
|
||||
player.sendMessage(
|
||||
"§c无可领取的奖励");
|
||||
player.sendMessage("§c无可领取的奖励");
|
||||
return;
|
||||
}
|
||||
|
||||
net.milkbowl.vault.economy.Economy econ =
|
||||
((Main) plugin).getEconomy();
|
||||
Main main = (Main) plugin;
|
||||
// ★ 读取配置:economy = Vault,bonds = 债券(默认)
|
||||
boolean useEconomy = "economy".equalsIgnoreCase(
|
||||
main.getConfigMgr().rewardChannel);
|
||||
|
||||
// Vault 经济引用(仅 useEconomy 时使用)
|
||||
net.milkbowl.vault.economy.Economy econ = null;
|
||||
if (useEconomy) {
|
||||
econ = main.getEconomy();
|
||||
}
|
||||
|
||||
int sentCount = 0;
|
||||
|
||||
@@ -916,52 +925,70 @@ public class QuestTracker {
|
||||
|
||||
// 检查可选奖励标记
|
||||
if (hasOptionalMarker(rt)) {
|
||||
if (!areAllConditionsMet(player.getName(), qf))
|
||||
if (!areAllConditionsMet(player.getName(), qf)) {
|
||||
player.sendMessage(
|
||||
"§7[可选奖励] "
|
||||
+ "需完成所有可选条件: "
|
||||
+ rt);
|
||||
"§7[可选奖励] 需完成所有可选条件: " + rt);
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 按 + 分割子项
|
||||
String[] parts =
|
||||
rt.split("[+,]");
|
||||
String[] parts = rt.split("[+,]");
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
if (part.isEmpty()) continue;
|
||||
|
||||
// 尝试发经济
|
||||
double amt =
|
||||
extractEconomyAmount(part);
|
||||
if (amt > 0 && econ != null) {
|
||||
double before =
|
||||
econ.getBalance(player);
|
||||
econ.depositPlayer(
|
||||
player, amt);
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f"
|
||||
+ part
|
||||
+ " §7(余额: §e"
|
||||
+ String.format(
|
||||
"%.2f", before)
|
||||
+ " §7→ §a"
|
||||
+ String.format(
|
||||
"%.2f",
|
||||
econ.getBalance(
|
||||
player))
|
||||
+ "§7)");
|
||||
// 通知提成系统
|
||||
if (plugin instanceof Main) {
|
||||
((Main) plugin)
|
||||
.getCommission()
|
||||
.payCommission(
|
||||
player
|
||||
.getName(),
|
||||
amt);
|
||||
// 尝试识别经济金额
|
||||
double amt = extractEconomyAmount(part);
|
||||
if (amt > 0) {
|
||||
// ★ 根据配置分发
|
||||
if (useEconomy && econ != null) {
|
||||
// —— Vault 经济 ——
|
||||
double before =
|
||||
econ.getBalance(player);
|
||||
econ.depositPlayer(player, amt);
|
||||
double after =
|
||||
econ.getBalance(player);
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f" + part
|
||||
+ " §7(余额: §e"
|
||||
+ String.format("%.2f", before)
|
||||
+ " §7→ §a"
|
||||
+ String.format("%.2f", after)
|
||||
+ "§7)");
|
||||
if (plugin instanceof Main) {
|
||||
((Main) plugin).getCommission()
|
||||
.payCommission(
|
||||
player.getName(), amt);
|
||||
}
|
||||
} else {
|
||||
// —— 债券(默认) ——
|
||||
int bondAmt =
|
||||
(int) Math.round(amt);
|
||||
BondManager bonds = main.getBonds();
|
||||
if (bonds != null) {
|
||||
int before = bonds.getBonds(
|
||||
player.getName());
|
||||
bonds.addBonds(
|
||||
player.getName(),
|
||||
bondAmt,
|
||||
"quest_reward",
|
||||
qf.displayName,
|
||||
"任务系统",
|
||||
"任务奖励:" + qf.displayName);
|
||||
int after = bonds.getBonds(
|
||||
player.getName());
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f" + part
|
||||
+ " §7(债券: §e"
|
||||
+ before
|
||||
+ " §7→ §a"
|
||||
+ after + "§7)");
|
||||
} else {
|
||||
player.sendMessage(
|
||||
"§c债券系统不可用");
|
||||
}
|
||||
}
|
||||
|
||||
sentCount++;
|
||||
continue;
|
||||
}
|
||||
@@ -971,32 +998,27 @@ public class QuestTracker {
|
||||
parseRewardItems(part);
|
||||
if (!items.isEmpty()) {
|
||||
for (ItemStack is : items) {
|
||||
player.getInventory()
|
||||
.addItem(is);
|
||||
player.getInventory().addItem(is);
|
||||
}
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f"
|
||||
+ part);
|
||||
"§a§l[任务奖励] §f" + part);
|
||||
sentCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 非经济非物品 → 仅消息
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f"
|
||||
+ part);
|
||||
"§a§l[任务奖励] §f" + part);
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[QuestTracker] "
|
||||
+ player.getName()
|
||||
+ " 领取了 \""
|
||||
+ qf.displayName
|
||||
+ "\" 共 " + sentCount
|
||||
+ " 项奖励");
|
||||
"[QuestTracker] " + player.getName()
|
||||
+ " 预取了 \"" + qf.displayName
|
||||
+ "\" 共 " + sentCount + "项奖励");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查奖励文本是否含可选标记
|
||||
*/
|
||||
@@ -1043,7 +1065,9 @@ public class QuestTracker {
|
||||
|| t.contains("$")
|
||||
|| t.contains("经济")
|
||||
|| t.contains("余额")
|
||||
|| t.contains("发钱");
|
||||
|| t.contains("发钱")
|
||||
|| t.contains("债券"); // ★ 新增
|
||||
|
||||
|
||||
if (!hasEcon) return 0;
|
||||
|
||||
|
||||
@@ -125,7 +125,8 @@ public class RadioDownloadListener
|
||||
// 不踢出,只记录熔断,玩家正常游玩
|
||||
p.sendMessage("§e§l[Radio] 资源包加载失败"
|
||||
+ ",跳过资源包,部分画面、音频可能出现问题"
|
||||
+ "§d§l若想继续下载,请退出重进");
|
||||
+ "\n§d§l若想继续下载,请退出重进" +
|
||||
"\n§b§l若持续失败,请重启客户端");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,6 +250,95 @@ public class RadioManager {
|
||||
Arrays.sort(files);
|
||||
for (File f : files) list.add(f);
|
||||
}
|
||||
/**
|
||||
* 解析 pack.png:优先读取插件目录,
|
||||
* 没有则搜索图片并用ffmpeg转码
|
||||
*/
|
||||
private File resolvePackIcon() {
|
||||
File dataDir = plugin.getDataFolder();
|
||||
File packPng = new File(dataDir, "pack.png");
|
||||
|
||||
if (packPng.exists()
|
||||
&& packPng.length() > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 找到 pack.png ("
|
||||
+ packPng.length()
|
||||
+ " bytes)");
|
||||
return packPng;
|
||||
}
|
||||
|
||||
// 搜索可转换的图片文件
|
||||
String[] exts = {"png", "jpg", "jpeg",
|
||||
"bmp", "webp", "tiff", "gif"};
|
||||
File[] found = dataDir.listFiles(
|
||||
(dir, name) -> {
|
||||
String low = name.toLowerCase();
|
||||
for (String ext : exts) {
|
||||
if (low.endsWith("." + ext)
|
||||
&& !name.equals(
|
||||
"pack.png")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (found != null && found.length > 0) {
|
||||
File src = found[0];
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 找到图片: "
|
||||
+ src.getName()
|
||||
+ " → 转码为 pack.png");
|
||||
if (convertToPackPng(src, packPng)) {
|
||||
return packPng;
|
||||
}
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 转码失败");
|
||||
return null;
|
||||
}
|
||||
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 未找到 pack.png 或可转换图片");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ffmpeg 将任意图片转为 256x256 PNG
|
||||
*/
|
||||
private boolean convertToPackPng(
|
||||
File input, File output) {
|
||||
try {
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"ffmpeg", "-y",
|
||||
"-i", input.getAbsolutePath(),
|
||||
"-vf",
|
||||
"scale='min(256,iw)'"
|
||||
+ ":'min(256,ih)':"
|
||||
+ "force_original_aspect_ratio"
|
||||
+ "=decrease,"
|
||||
+ "pad=256:256:"
|
||||
+ "(ow-iw)/2:"
|
||||
+ "(oh-ih)/2:"
|
||||
+ "color=0x00000000",
|
||||
"-c:v", "png",
|
||||
output.getAbsolutePath());
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
boolean ok = p.waitFor(
|
||||
30, TimeUnit.SECONDS);
|
||||
if (!ok) {
|
||||
p.destroyForcibly();
|
||||
return false;
|
||||
}
|
||||
return output.exists()
|
||||
&& output.length() > 0;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] ffmpeg转码失败: "
|
||||
+ e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 资源包构建 ==========
|
||||
private void buildResourcePack() {
|
||||
@@ -320,6 +409,25 @@ public class RadioManager {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] sounds.json写入失败");
|
||||
}
|
||||
// sounds.json 写入后...
|
||||
|
||||
// ===== 新增:打包 pack.png 作为资源包图标 =====
|
||||
File iconPng = resolvePackIcon();
|
||||
if (iconPng != null && iconPng.exists()) {
|
||||
try {
|
||||
Files.copy(iconPng.toPath(),
|
||||
new File(packDir, "pack.png")
|
||||
.toPath(),
|
||||
StandardCopyOption
|
||||
.REPLACE_EXISTING);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] pack.png 已加入资源包");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] pack.png复制失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// pack.mcmeta(pack_format=54)
|
||||
try {
|
||||
@@ -407,30 +515,32 @@ public class RadioManager {
|
||||
// ========== 资源包发送 ==========
|
||||
|
||||
/**
|
||||
* 发送资源包(可选,不强制)
|
||||
* 优先用HTTP URL发送,兜底用byte[]
|
||||
* 发送资源包(带限流,不禁重试)
|
||||
*/
|
||||
private final Map<UUID, Long> lastSendTime =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public void sendResourcePack(Player p) {
|
||||
UUID uuid = p.getUniqueId();
|
||||
if (pendingPack.contains(uuid)) {
|
||||
|
||||
// 最短间隔 5秒(不是10秒的全锁死)
|
||||
Long last = lastSendTime.get(uuid);
|
||||
if (last != null
|
||||
&& System.currentTimeMillis() - last
|
||||
< 5000L) {
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 跳过(已等待): "
|
||||
"[Radio] 跳过(5秒内): "
|
||||
+ p.getName());
|
||||
return;
|
||||
}
|
||||
pendingPack.add(uuid);
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin, () -> {
|
||||
pendingPack.remove(uuid);
|
||||
}, 200L);
|
||||
lastSendTime.put(uuid,
|
||||
System.currentTimeMillis());
|
||||
|
||||
String url = getResourcePackUrl();
|
||||
if (url == null || url.isEmpty()) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] URL为空,"
|
||||
+ "跳过发送: "
|
||||
"[Radio] URL为空,跳过: "
|
||||
+ p.getName());
|
||||
pendingPack.remove(uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -441,7 +551,6 @@ public class RadioManager {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] radiopack.zip不存在,"
|
||||
+ "跳过: " + p.getName());
|
||||
pendingPack.remove(uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -452,22 +561,10 @@ public class RadioManager {
|
||||
long fileSize = zip.length();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Radio] === 发送资源包 ===");
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 玩家: " + p.getName());
|
||||
plugin.getLogger().info(
|
||||
"[Radio] URL: " + url);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] Hash: " + hash);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 文件大小: "
|
||||
+ fileSize + " bytes ("
|
||||
+ String.format("%.1f",
|
||||
fileSize / 1024.0)
|
||||
+ " KB)");
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 客户端版本: "
|
||||
+ p.getProtocolVersion());
|
||||
"[Radio] 发送: " + p.getName()
|
||||
+ " url=" + url
|
||||
+ " size=" + fileSize
|
||||
+ " hash=" + hash);
|
||||
|
||||
sendPackWithHash(p, url, hash);
|
||||
|
||||
@@ -476,7 +573,6 @@ public class RadioManager {
|
||||
"[Radio] 发送异常: "
|
||||
+ p.getName());
|
||||
e.printStackTrace();
|
||||
pendingPack.remove(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,37 +582,38 @@ public class RadioManager {
|
||||
Player p,
|
||||
String url,
|
||||
String hash) {
|
||||
|
||||
UUID uuid = UUID.fromString(
|
||||
"a1b2c3d4-e5f6-7890-abcd-"
|
||||
+ "ef1234567890");
|
||||
|
||||
// 尝试5参 + Component
|
||||
try {
|
||||
Object component =
|
||||
net.kyori.adventure.text
|
||||
.Component.text(
|
||||
"§d§l服务器需要自定义资源包,请您点击'继续'由客户端自动下载即可");
|
||||
"§d§l请下载自定义资源包");
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
UUID.class,
|
||||
String.class,
|
||||
String.class,
|
||||
net.kyori.adventure
|
||||
.text
|
||||
.Component.class,
|
||||
.text.Component
|
||||
.class,
|
||||
boolean.class)
|
||||
.invoke(
|
||||
p, uuid, url,
|
||||
hash, component,
|
||||
true);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 发送成功: "
|
||||
+ p.getName()
|
||||
+ " url="
|
||||
+ url);
|
||||
"[Radio] 成功(5参): "
|
||||
+ p.getName());
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 尝试5参 + String描述
|
||||
try {
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
@@ -531,12 +628,13 @@ public class RadioManager {
|
||||
"Sdf1 Radio",
|
||||
true);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 旧版签名成功: "
|
||||
"[Radio] 成功(5参String): "
|
||||
+ p.getName());
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 尝试3参
|
||||
try {
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
@@ -547,18 +645,27 @@ public class RadioManager {
|
||||
p, uuid, url,
|
||||
hash);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 3参签名成功: "
|
||||
"[Radio] 成功(3参): "
|
||||
+ p.getName());
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// ★ 全部失败 → 通知玩家 + 熔断
|
||||
plugin.getLogger().severe(
|
||||
"[Radio] 所有签名失败: "
|
||||
+ p.getName());
|
||||
+ p.getName()
|
||||
+ " 客户端版本: "
|
||||
+ p.getProtocolVersion());
|
||||
p.sendMessage(
|
||||
"§e§l[Radio] §f资源包发送失败,"
|
||||
+ "§7部分功能可能异常");
|
||||
p.sendMessage(
|
||||
"§7可尝试: §e/rejoin §7或重启客户端");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void sendPackFromLocal(
|
||||
Player p, String url) {
|
||||
File zip = new File(
|
||||
@@ -610,7 +717,7 @@ public class RadioManager {
|
||||
}
|
||||
|
||||
public void clearPending(UUID uuid) {
|
||||
pendingPack.remove(uuid);
|
||||
lastSendTime.remove(uuid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RefundRecord {
|
||||
public final UUID playerUUID;
|
||||
public final String playerName;
|
||||
public final String itemId;
|
||||
public final String itemName;
|
||||
public final Material material;
|
||||
public final Material shulkerMaterial;
|
||||
public int amount;
|
||||
public int totalPaid;
|
||||
public final long timestamp;
|
||||
|
||||
// 普通购买
|
||||
public RefundRecord(UUID uuid, String name,
|
||||
String itemId, String itemName,
|
||||
Material material, int amount,
|
||||
int totalPaid, long timestamp) {
|
||||
this.playerUUID = uuid;
|
||||
this.playerName = name;
|
||||
this.itemId = itemId;
|
||||
this.itemName = itemName;
|
||||
this.material = material;
|
||||
this.shulkerMaterial = null;
|
||||
this.amount = amount;
|
||||
this.totalPaid = totalPaid;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
// 盒装购买
|
||||
public RefundRecord(UUID uuid, String name,
|
||||
String itemId, String itemName,
|
||||
Material material,
|
||||
Material shulkerMat,
|
||||
int amount, int totalPaid,
|
||||
long timestamp) {
|
||||
this.playerUUID = uuid;
|
||||
this.playerName = name;
|
||||
this.itemId = itemId;
|
||||
this.itemName = itemName;
|
||||
this.material = material;
|
||||
this.shulkerMaterial = shulkerMat;
|
||||
this.amount = amount;
|
||||
this.totalPaid = totalPaid;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis()
|
||||
- timestamp > 300000L;
|
||||
}
|
||||
|
||||
public int getSecondsLeft() {
|
||||
long left = 300000L
|
||||
- (System.currentTimeMillis()
|
||||
- timestamp);
|
||||
return (int) Math.max(0, left / 1000);
|
||||
}
|
||||
}
|
||||
@@ -1,248 +1,248 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
|
||||
*/
|
||||
public class RiskControlManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
// 每个玩家名 -> 连续失败次数
|
||||
private final Map<String, Integer> failAttempts =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个IP -> 关联的可疑玩家列表
|
||||
private final Map<String, Set<String>> ipSuspicious =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个玩家 -> 上次验证时间(用于冷却)
|
||||
private final Map<String, Long> verifyCooldown =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 配置项
|
||||
private static final int MAX_FAIL_ATTEMPTS = 5;
|
||||
private static final int MAX_ACCOUNTS_PER_IP = 5;
|
||||
private static final long COOLDOWN_MS = 60000L;
|
||||
|
||||
public RiskControlManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一次失败的登录尝试
|
||||
*
|
||||
* @param playerName 玩家名
|
||||
* @return 剩余尝试次数
|
||||
*/
|
||||
public int recordFailAttempt(String playerName) {
|
||||
int current = failAttempts
|
||||
.getOrDefault(playerName, 0) + 1;
|
||||
failAttempts.put(playerName, current);
|
||||
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定玩家的失败计数(登录成功时调用)
|
||||
*/
|
||||
public void resetFailAttempts(String playerName) {
|
||||
failAttempts.remove(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定玩家的失败次数
|
||||
*/
|
||||
public int getFailAttempts(String playerName) {
|
||||
return failAttempts
|
||||
.getOrDefault(playerName, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断玩家是否已被锁定(失败次数达到上限)
|
||||
*/
|
||||
public boolean isLocked(String playerName) {
|
||||
return failAttempts
|
||||
.getOrDefault(playerName, 0)
|
||||
>= MAX_FAIL_ATTEMPTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查IP是否达到注册上限
|
||||
* 直接查数据库,不依赖 DatabaseManager 的方法
|
||||
*/
|
||||
public boolean canRegisterFromIP(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return true;
|
||||
try {
|
||||
java.sql.Connection conn =
|
||||
plugin.getDb().getDb();
|
||||
if (conn == null) return true;
|
||||
java.sql.PreparedStatement ps =
|
||||
conn.prepareStatement(
|
||||
"SELECT COUNT(*) FROM users "
|
||||
+ "WHERE ip_address = ?");
|
||||
ps.setString(1, ip);
|
||||
java.sql.ResultSet rs = ps.executeQuery();
|
||||
int count = 0;
|
||||
if (rs.next()) count = rs.getInt(1);
|
||||
rs.close();
|
||||
ps.close();
|
||||
return count < 5;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 记录IP关联的可疑行为
|
||||
*/
|
||||
public void recordSuspiciousIP(String ip,
|
||||
String playerName) {
|
||||
if (ip == null || ip.isEmpty()) return;
|
||||
ipSuspicious
|
||||
.computeIfAbsent(ip,
|
||||
k -> ConcurrentHashMap.newKeySet())
|
||||
.add(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某IP关联的所有可疑玩家
|
||||
*/
|
||||
public Set<String> getSuspiciousAccounts(String ip) {
|
||||
return ipSuspicious
|
||||
.getOrDefault(ip, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在验证冷却期内
|
||||
*/
|
||||
public boolean isOnCooldown(String playerName) {
|
||||
Long last = verifyCooldown.get(playerName);
|
||||
if (last == null) return false;
|
||||
return System.currentTimeMillis() - last
|
||||
< COOLDOWN_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置验证冷却时间
|
||||
*/
|
||||
public void setCooldown(String playerName) {
|
||||
verifyCooldown.put(playerName,
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* 当检测到可疑登录时触发报警
|
||||
* - 全服广播
|
||||
* - 有邮箱则发邮件
|
||||
* - 记录到数据库
|
||||
*/
|
||||
public void triggerAlert(Player player,
|
||||
String suspiciousIP) {
|
||||
String name = player.getName();
|
||||
|
||||
// 记录到数据库
|
||||
plugin.getDb().recordSecurityAlert(
|
||||
name, suspiciousIP);
|
||||
|
||||
// 记录IP可疑行为
|
||||
recordSuspiciousIP(suspiciousIP, name);
|
||||
|
||||
// 全服广播
|
||||
String serverMsg = "§c§l[安全警告] §e" + name
|
||||
+ " §f的账号疑似遭到 §c" + suspiciousIP
|
||||
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
|
||||
Bukkit.broadcastMessage(serverMsg);
|
||||
|
||||
// 有邮箱则发邮件
|
||||
String emailAddr = (String) plugin.getDb()
|
||||
.getField(name, "email");
|
||||
if (emailAddr != null
|
||||
&& !emailAddr.isEmpty()) {
|
||||
String subject =
|
||||
"[安全警告] 您的账号疑似遭到盗号尝试";
|
||||
String body = "尊敬的玩家 " + name
|
||||
+ ":\n\n"
|
||||
+ "您的账号在 "
|
||||
+ new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date())
|
||||
+ " 疑似遭到 IP: " + suspiciousIP
|
||||
+ " 的盗号尝试。\n\n"
|
||||
+ "建议您:\n"
|
||||
+ "1. 立即修改密码"
|
||||
+ "(使用 /sdf1_login pw)\n"
|
||||
+ "2. 如非本人操作,"
|
||||
+ "请联系管理员处理\n\n"
|
||||
+ "--Sdf1_login 安全系统";
|
||||
plugin.getEmail().sendTempPassword(
|
||||
emailAddr, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录失败时的风控逻辑
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param suspiciousIP 可疑来源IP
|
||||
* @return true=已被锁定(应踢出), false=仍可继续尝试
|
||||
*/
|
||||
public boolean handleLoginFailure(Player player,
|
||||
String suspiciousIP) {
|
||||
String name = player.getName();
|
||||
int remaining = recordFailAttempt(name);
|
||||
|
||||
if (remaining <= 0) {
|
||||
// 达到上限,触发报警
|
||||
triggerAlert(player, suspiciousIP);
|
||||
player.kickPlayer(
|
||||
"§c§l[风控] §f登录失败次数过多,"
|
||||
+ " 账号已被临时锁定, 请联系管理员");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未达上限,提示剩余次数
|
||||
player.sendMessage(
|
||||
"§c§l[风控] §f密码错误! "
|
||||
+ "剩余" + remaining + "次机会");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后重置风控状态
|
||||
*/
|
||||
public void onLoginSuccess(String playerName) {
|
||||
resetFailAttempts(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的冷却记录(可由定时任务调用)
|
||||
*/
|
||||
public void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
verifyCooldown.entrySet().removeIf(
|
||||
e -> now - e.getValue() > COOLDOWN_MS * 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家的失败尝试记录(调试用)
|
||||
*/
|
||||
public Map<String, Integer> getAllFailAttempts() {
|
||||
return Collections.unmodifiableMap(failAttempts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有风控数据(管理员调试用)
|
||||
*/
|
||||
public void clearAll() {
|
||||
failAttempts.clear();
|
||||
ipSuspicious.clear();
|
||||
verifyCooldown.clear();
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
|
||||
*/
|
||||
public class RiskControlManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
// 每个玩家名 -> 连续失败次数
|
||||
private final Map<String, Integer> failAttempts =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个IP -> 关联的可疑玩家列表
|
||||
private final Map<String, Set<String>> ipSuspicious =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个玩家 -> 上次验证时间(用于冷却)
|
||||
private final Map<String, Long> verifyCooldown =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 配置项
|
||||
private static final int MAX_FAIL_ATTEMPTS = 5;
|
||||
private static final int MAX_ACCOUNTS_PER_IP = 5;
|
||||
private static final long COOLDOWN_MS = 60000L;
|
||||
|
||||
public RiskControlManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一次失败的登录尝试
|
||||
*
|
||||
* @param playerName 玩家名
|
||||
* @return 剩余尝试次数
|
||||
*/
|
||||
public int recordFailAttempt(String playerName) {
|
||||
int current = failAttempts
|
||||
.getOrDefault(playerName, 0) + 1;
|
||||
failAttempts.put(playerName, current);
|
||||
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定玩家的失败计数(登录成功时调用)
|
||||
*/
|
||||
public void resetFailAttempts(String playerName) {
|
||||
failAttempts.remove(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定玩家的失败次数
|
||||
*/
|
||||
public int getFailAttempts(String playerName) {
|
||||
return failAttempts
|
||||
.getOrDefault(playerName, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断玩家是否已被锁定(失败次数达到上限)
|
||||
*/
|
||||
public boolean isLocked(String playerName) {
|
||||
return failAttempts
|
||||
.getOrDefault(playerName, 0)
|
||||
>= MAX_FAIL_ATTEMPTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查IP是否达到注册上限
|
||||
* 直接查数据库,不依赖 DatabaseManager 的方法
|
||||
*/
|
||||
public boolean canRegisterFromIP(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return true;
|
||||
try {
|
||||
java.sql.Connection conn =
|
||||
plugin.getDb().getDb();
|
||||
if (conn == null) return true;
|
||||
java.sql.PreparedStatement ps =
|
||||
conn.prepareStatement(
|
||||
"SELECT COUNT(*) FROM users "
|
||||
+ "WHERE ip_address = ?");
|
||||
ps.setString(1, ip);
|
||||
java.sql.ResultSet rs = ps.executeQuery();
|
||||
int count = 0;
|
||||
if (rs.next()) count = rs.getInt(1);
|
||||
rs.close();
|
||||
ps.close();
|
||||
return count < 5;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 记录IP关联的可疑行为
|
||||
*/
|
||||
public void recordSuspiciousIP(String ip,
|
||||
String playerName) {
|
||||
if (ip == null || ip.isEmpty()) return;
|
||||
ipSuspicious
|
||||
.computeIfAbsent(ip,
|
||||
k -> ConcurrentHashMap.newKeySet())
|
||||
.add(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某IP关联的所有可疑玩家
|
||||
*/
|
||||
public Set<String> getSuspiciousAccounts(String ip) {
|
||||
return ipSuspicious
|
||||
.getOrDefault(ip, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在验证冷却期内
|
||||
*/
|
||||
public boolean isOnCooldown(String playerName) {
|
||||
Long last = verifyCooldown.get(playerName);
|
||||
if (last == null) return false;
|
||||
return System.currentTimeMillis() - last
|
||||
< COOLDOWN_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置验证冷却时间
|
||||
*/
|
||||
public void setCooldown(String playerName) {
|
||||
verifyCooldown.put(playerName,
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* 当检测到可疑登录时触发报警
|
||||
* - 全服广播
|
||||
* - 有邮箱则发邮件
|
||||
* - 记录到数据库
|
||||
*/
|
||||
public void triggerAlert(Player player,
|
||||
String suspiciousIP) {
|
||||
String name = player.getName();
|
||||
|
||||
// 记录到数据库
|
||||
plugin.getDb().recordSecurityAlert(
|
||||
name, suspiciousIP);
|
||||
|
||||
// 记录IP可疑行为
|
||||
recordSuspiciousIP(suspiciousIP, name);
|
||||
|
||||
// 全服广播
|
||||
String serverMsg = "§c§l[安全警告] §e" + name
|
||||
+ " §f的账号疑似遭到 §c" + suspiciousIP
|
||||
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
|
||||
Bukkit.broadcastMessage(serverMsg);
|
||||
|
||||
// 有邮箱则发邮件
|
||||
String emailAddr = (String) plugin.getDb()
|
||||
.getField(name, "email");
|
||||
if (emailAddr != null
|
||||
&& !emailAddr.isEmpty()) {
|
||||
String subject =
|
||||
"[安全警告] 您的账号疑似遭到盗号尝试";
|
||||
String body = "尊敬的玩家 " + name
|
||||
+ ":\n\n"
|
||||
+ "您的账号在 "
|
||||
+ new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date())
|
||||
+ " 疑似遭到 IP: " + suspiciousIP
|
||||
+ " 的盗号尝试。\n\n"
|
||||
+ "建议您:\n"
|
||||
+ "1. 立即修改密码"
|
||||
+ "(使用 /sdf1_login pw)\n"
|
||||
+ "2. 如非本人操作,"
|
||||
+ "请联系管理员处理\n\n"
|
||||
+ "--Sdf1_login 安全系统";
|
||||
plugin.getEmail().sendTempPassword(
|
||||
emailAddr, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录失败时的风控逻辑
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param suspiciousIP 可疑来源IP
|
||||
* @return true=已被锁定(应踢出), false=仍可继续尝试
|
||||
*/
|
||||
public boolean handleLoginFailure(Player player,
|
||||
String suspiciousIP) {
|
||||
String name = player.getName();
|
||||
int remaining = recordFailAttempt(name);
|
||||
|
||||
if (remaining <= 0) {
|
||||
// 达到上限,触发报警
|
||||
triggerAlert(player, suspiciousIP);
|
||||
player.kickPlayer(
|
||||
"§c§l[风控] §f登录失败次数过多,"
|
||||
+ " 账号已被临时锁定, 请联系管理员");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未达上限,提示剩余次数
|
||||
player.sendMessage(
|
||||
"§c§l[风控] §f密码错误! "
|
||||
+ "剩余" + remaining + "次机会");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后重置风控状态
|
||||
*/
|
||||
public void onLoginSuccess(String playerName) {
|
||||
resetFailAttempts(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的冷却记录(可由定时任务调用)
|
||||
*/
|
||||
public void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
verifyCooldown.entrySet().removeIf(
|
||||
e -> now - e.getValue() > COOLDOWN_MS * 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家的失败尝试记录(调试用)
|
||||
*/
|
||||
public Map<String, Integer> getAllFailAttempts() {
|
||||
return Collections.unmodifiableMap(failAttempts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有风控数据(管理员调试用)
|
||||
*/
|
||||
public void clearAll() {
|
||||
failAttempts.clear();
|
||||
ipSuspicious.clear();
|
||||
verifyCooldown.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class SalesStatsManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
private final Map<String, Integer> sessionSales =
|
||||
new ConcurrentHashMap<>();
|
||||
private int sessionRevenue = 0;
|
||||
private int sessionOrders = 0;
|
||||
private int sessionItems = 0;
|
||||
private int sessionRefunds = 0;
|
||||
private int sessionRefundAmount = 0;
|
||||
private long sessionStart = System.currentTimeMillis();
|
||||
|
||||
// 状态
|
||||
private boolean waitingForEmpty = false;
|
||||
private boolean hasReported = false;
|
||||
private BukkitRunnable waitTask = null;
|
||||
|
||||
public SalesStatsManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
startMonitor();
|
||||
}
|
||||
|
||||
// ★ 每10秒检测一次
|
||||
private void startMonitor() {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int online = Bukkit.getOnlinePlayers().size();
|
||||
|
||||
if (online > 0) {
|
||||
// 有玩家在线 → 重置等待状态
|
||||
if (waitingForEmpty) {
|
||||
// 刚有人进来了,清除已汇报标记
|
||||
// 下次没人时会重新汇报
|
||||
}
|
||||
waitingForEmpty = false;
|
||||
hasReported = false;
|
||||
cancelWaitTask();
|
||||
} else if (!waitingForEmpty && !hasReported) {
|
||||
// 没人了,且还没汇报过 → 开始1分钟倒计时
|
||||
waitingForEmpty = true;
|
||||
startWaitCountdown();
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 200L, 200L);
|
||||
}
|
||||
|
||||
// ★ 1分钟等待
|
||||
private void startWaitCountdown() {
|
||||
cancelWaitTask();
|
||||
waitTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int online = Bukkit.getOnlinePlayers().size();
|
||||
|
||||
if (online > 0) {
|
||||
// 等待期间有人进来 → 取消,等下次没人
|
||||
waitingForEmpty = false;
|
||||
hasReported = false;
|
||||
waitTask = null;
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// 1分钟没人 → 汇报一次
|
||||
if (!hasReported) {
|
||||
hasReported = true;
|
||||
report();
|
||||
}
|
||||
waitingForEmpty = false;
|
||||
waitTask = null;
|
||||
cancel();
|
||||
}
|
||||
};
|
||||
waitTask.runTaskLater(plugin, 1200L); // 60秒
|
||||
}
|
||||
|
||||
private void cancelWaitTask() {
|
||||
if (waitTask != null) {
|
||||
waitTask.cancel();
|
||||
waitTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 记录 =====
|
||||
|
||||
public void recordSale(String itemName, int price) {
|
||||
sessionSales.merge(itemName, 1, Integer::sum);
|
||||
sessionRevenue += price;
|
||||
sessionOrders++;
|
||||
sessionItems++;
|
||||
}
|
||||
|
||||
public void recordRefund(int amount) {
|
||||
sessionRefunds++;
|
||||
sessionRefundAmount += amount;
|
||||
}
|
||||
|
||||
// ===== 汇报 =====
|
||||
|
||||
public void report() {
|
||||
if (sessionItems == 0 && sessionRefunds == 0) {
|
||||
// plugin.getLogger().info("[Sales] 无数据,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
long minutes =
|
||||
(System.currentTimeMillis() - sessionStart)
|
||||
/ 60000;
|
||||
if (minutes < 1) minutes = 1;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("§6§l========== 销量汇总 ==========");
|
||||
sb.append("\n§7时长: §f").append(minutes).append("分钟");
|
||||
sb.append("\n§7订单: §f").append(sessionOrders).append("笔");
|
||||
sb.append("\n§7商品: §f").append(sessionItems).append("件");
|
||||
sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚");
|
||||
sb.append("\n§7退款: §c").append(sessionRefunds)
|
||||
.append("次(§c").append(sessionRefundAmount)
|
||||
.append("枚§7)");
|
||||
sb.append("\n§6§l------------------------------");
|
||||
|
||||
if (!sessionSales.isEmpty()) {
|
||||
sb.append("\n§7销量排行:");
|
||||
List<Map.Entry<String, Integer>> sorted =
|
||||
new ArrayList<>(sessionSales.entrySet());
|
||||
sorted.sort((a, b) ->
|
||||
b.getValue() - a.getValue());
|
||||
int rank = 1;
|
||||
for (Map.Entry<String, Integer> e : sorted) {
|
||||
if (rank > 10) break;
|
||||
sb.append("\n§e").append(rank).append(". ")
|
||||
.append(e.getKey())
|
||||
.append(" §7x").append(e.getValue());
|
||||
rank++;
|
||||
}
|
||||
}
|
||||
sb.append("\n§6§l==============================");
|
||||
|
||||
String msg = sb.toString();
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.isOp()) {
|
||||
for (String line : msg.split("\n"))
|
||||
p.sendMessage(line);
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[Sales] 汇报完成");
|
||||
resetSession();
|
||||
}
|
||||
|
||||
public void resetSession() {
|
||||
sessionSales.clear();
|
||||
sessionRevenue = 0;
|
||||
sessionOrders = 0;
|
||||
sessionItems = 0;
|
||||
sessionRefunds = 0;
|
||||
sessionRefundAmount = 0;
|
||||
sessionStart = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ShopCategory {
|
||||
private final String name;
|
||||
private final String fileName;
|
||||
private final List<ShopItem> items;
|
||||
|
||||
public ShopCategory(String name, String fileName) {
|
||||
this.name = name;
|
||||
this.fileName = fileName;
|
||||
this.items = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getFileName() { return fileName; }
|
||||
public List<ShopItem> getItems() { return items; }
|
||||
|
||||
public ShopItem getItem(String id) {
|
||||
for (ShopItem item : items) {
|
||||
if (item.getId().equalsIgnoreCase(id))
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addItem(ShopItem item) {
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class ShopItem {
|
||||
private final String id;
|
||||
private final String displayName;
|
||||
private final Material material;
|
||||
private final int buyPrice;
|
||||
private final int sellPrice;
|
||||
private int stock;
|
||||
private int hourlySales;
|
||||
private int totalSales;
|
||||
//打包了 直接测试替换就好了。
|
||||
public ShopItem(String id, String displayName,
|
||||
Material material, int buyPrice,
|
||||
int sellPrice, int stock,
|
||||
int hourlySales, int totalSales) {
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
this.material = material;
|
||||
this.buyPrice = buyPrice;
|
||||
this.sellPrice = sellPrice;
|
||||
this.stock = stock;
|
||||
this.hourlySales = hourlySales;
|
||||
this.totalSales = totalSales;
|
||||
}
|
||||
|
||||
public int getEffectiveSellPrice() {
|
||||
if (sellPrice <= 0) return 0;
|
||||
return sellPrice;
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public Material getMaterial() { return material; }
|
||||
public int getBuyPrice() { return buyPrice; }
|
||||
public int getSellPrice() { return sellPrice; }
|
||||
public int getStock() { return stock; }
|
||||
public int getHourlySales() { return hourlySales; }
|
||||
public int getTotalSales() { return totalSales; }
|
||||
|
||||
public void setStock(int s) { this.stock = s; }
|
||||
public void setHourlySales(int s) { this.hourlySales = s; }
|
||||
public void setTotalSales(int s) { this.totalSales = s; }
|
||||
public void addSales(int n) {
|
||||
this.hourlySales += n;
|
||||
this.totalSales += n;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,370 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class UserGroupManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Connection db;
|
||||
private final Map<String, UserGroupConfig> groupConfigs = new ConcurrentHashMap<>();
|
||||
|
||||
public UserGroupManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initDB();
|
||||
loadGroupConfigs();
|
||||
startExpiryChecker();
|
||||
}
|
||||
|
||||
// ===== DB =====
|
||||
|
||||
private void initDB() {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
File dbFile = new File(plugin.getDataFolder(), "usergroup.db");
|
||||
db = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
Statement st = db.createStatement();
|
||||
st.execute("PRAGMA journal_mode=WAL");
|
||||
st.execute("CREATE TABLE IF NOT EXISTS user_group_config ("
|
||||
+ "group_name TEXT PRIMARY KEY,"
|
||||
+ "display_name TEXT DEFAULT '',"
|
||||
+ "display_color TEXT DEFAULT '§f',"
|
||||
+ "display_emoji TEXT DEFAULT '',"
|
||||
+ "priority INTEGER DEFAULT 0,"
|
||||
+ "is_permanent INTEGER DEFAULT 1,"
|
||||
+ "duration_minutes INTEGER DEFAULT 0,"
|
||||
+ "pack_url TEXT DEFAULT '',"
|
||||
+ "pack_hash TEXT DEFAULT '',"
|
||||
+ "pack_prompt TEXT DEFAULT 'Custom Resource Pack',"
|
||||
+ "pack_size INTEGER DEFAULT 0)");
|
||||
st.execute("CREATE TABLE IF NOT EXISTS user_group_member ("
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "group_name TEXT NOT NULL,"
|
||||
+ "added_by TEXT DEFAULT 'system',"
|
||||
+ "added_time INTEGER DEFAULT 0,"
|
||||
+ "expiry_time INTEGER DEFAULT 0,"
|
||||
+ "PRIMARY KEY(player_name, group_name))");
|
||||
st.close();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("[UserGroup] DB init failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 配置加载 =====
|
||||
|
||||
public void loadGroupConfigs() {
|
||||
groupConfigs.clear();
|
||||
File dir = new File(plugin.getDataFolder(), "用户组");
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
File[] files = dir.listFiles((d, n) -> n.endsWith(".txt"));
|
||||
if (files == null) return;
|
||||
|
||||
for (File f : files) {
|
||||
try {
|
||||
List<String> lines = java.nio.file.Files.readAllLines(
|
||||
f.toPath(), StandardCharsets.UTF_8);
|
||||
UserGroupConfig cfg = parseGroupConfig(lines);
|
||||
if (cfg != null) {
|
||||
groupConfigs.put(cfg.name, cfg);
|
||||
saveGroupConfigToDB(cfg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[UserGroup] Failed to load: " + f.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UserGroupConfig parseGroupConfig(List<String> lines) {
|
||||
UserGroupConfig cfg = new UserGroupConfig();
|
||||
boolean inBlock = false;
|
||||
|
||||
for (String raw : lines) {
|
||||
String l = raw;
|
||||
|
||||
// 块注释
|
||||
if (l.contains("<!--")) { inBlock = true; continue; }
|
||||
if (l.contains("-->")) { inBlock = false; continue; }
|
||||
if (l.contains("/*")) { inBlock = true; continue; }
|
||||
if (l.contains("*/")) { inBlock = false; continue; }
|
||||
if (inBlock) continue;
|
||||
|
||||
// 行注释
|
||||
l = l.trim();
|
||||
if (l.isEmpty()) continue;
|
||||
if (l.startsWith("#") || l.startsWith("//")) continue;
|
||||
|
||||
// 行内注释
|
||||
int h = l.indexOf('#');
|
||||
int s = l.indexOf("//");
|
||||
if (h >= 0) l = l.substring(0, h).trim();
|
||||
if (s >= 0 && (h < 0 || s < h)) l = l.substring(0, s).trim();
|
||||
if (l.isEmpty()) continue;
|
||||
|
||||
// 解析键值对
|
||||
String key = l;
|
||||
String val = "";
|
||||
int idx = l.indexOf('\uff1a');
|
||||
if (idx < 0) idx = l.indexOf(':');
|
||||
if (idx >= 0) {
|
||||
key = l.substring(0, idx).trim();
|
||||
val = l.substring(idx + 1).trim();
|
||||
if (val.startsWith("\"") && val.endsWith("\""))
|
||||
val = val.substring(1, val.length() - 1);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "组名":
|
||||
case "name": cfg.name = val; break;
|
||||
case "显示名":
|
||||
case "display": cfg.displayName = val; break;
|
||||
case "颜色":
|
||||
case "color": cfg.displayColor = val; break;
|
||||
case "符号":
|
||||
case "emoji": cfg.displayEmoji = val; break;
|
||||
case "优先级":
|
||||
case "priority":
|
||||
try { cfg.priority = Integer.parseInt(val); } catch (Exception ignored) {}
|
||||
break;
|
||||
case "类型":
|
||||
case "type":
|
||||
cfg.permanent = val.equalsIgnoreCase("永久") || val.equalsIgnoreCase("permanent");
|
||||
break;
|
||||
case "时长":
|
||||
case "duration":
|
||||
try { cfg.durationMinutes = Integer.parseInt(val); } catch (Exception ignored) {}
|
||||
break;
|
||||
case "资源包URL":
|
||||
case "pack_url": cfg.packUrl = val; break;
|
||||
case "资源包Hash":
|
||||
case "pack_hash": cfg.packHash = val; break;
|
||||
case "资源包提示":
|
||||
case "pack_prompt": cfg.packPrompt = val; break;
|
||||
case "资源包大小":
|
||||
case "pack_size":
|
||||
try { cfg.packSize = Integer.parseInt(val); } catch (Exception ignored) {}
|
||||
break;
|
||||
case "本地文件":
|
||||
case "local_file": cfg.localFile = val; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.name == null || cfg.name.isEmpty()) return null;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private void saveGroupConfigToDB(UserGroupConfig cfg) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR REPLACE INTO user_group_config "
|
||||
+ "(group_name, display_name, display_color, display_emoji,"
|
||||
+ " priority, is_permanent, duration_minutes,"
|
||||
+ " pack_url, pack_hash, pack_prompt, pack_size)"
|
||||
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
||||
ps.setString(1, cfg.name);
|
||||
ps.setString(2, cfg.displayName);
|
||||
ps.setString(3, cfg.displayColor);
|
||||
ps.setString(4, cfg.displayEmoji);
|
||||
ps.setInt(5, cfg.priority);
|
||||
ps.setInt(6, cfg.permanent ? 1 : 0);
|
||||
ps.setInt(7, cfg.durationMinutes);
|
||||
ps.setString(8, cfg.packUrl);
|
||||
ps.setString(9, cfg.packHash);
|
||||
ps.setString(10, cfg.packPrompt);
|
||||
ps.setInt(11, cfg.packSize);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 玩家操作 =====
|
||||
|
||||
public boolean addPlayer(String player, String groupName, String addedBy, long durationMinutes) {
|
||||
UserGroupConfig cfg = groupConfigs.get(groupName);
|
||||
if (cfg == null) return false;
|
||||
long now = System.currentTimeMillis();
|
||||
long expiry = cfg.permanent ? 0 : now + durationMinutes * 60 * 1000;
|
||||
if (!cfg.permanent && durationMinutes > 0) expiry = now + durationMinutes * 60 * 1000;
|
||||
else if (!cfg.permanent) expiry = now + cfg.durationMinutes * 60 * 1000;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR REPLACE INTO user_group_member "
|
||||
+ "(player_name, group_name, added_by, added_time, expiry_time)"
|
||||
+ " VALUES (?,?,?,?,?)");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, groupName);
|
||||
ps.setString(3, addedBy);
|
||||
ps.setLong(4, now);
|
||||
ps.setLong(5, expiry);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
Player p = plugin.getServer().getPlayerExact(player);
|
||||
if (p != null) sendResourcePack(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removePlayer(String player, String groupName) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, groupName);
|
||||
int rows = ps.executeUpdate();
|
||||
ps.close();
|
||||
return rows > 0;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getPlayerGroups(String player) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM user_group_member WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
long now = System.currentTimeMillis();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("group_name", rs.getString("group_name"));
|
||||
row.put("added_by", rs.getString("added_by"));
|
||||
long expiry = rs.getLong("expiry_time");
|
||||
row.put("expiry_time", expiry);
|
||||
row.put("expired", expiry > 0 && expiry <= now);
|
||||
list.add(row);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public UserGroupConfig getHighestGroup(String player) {
|
||||
List<Map<String, Object>> groups = getPlayerGroups(player);
|
||||
long now = System.currentTimeMillis();
|
||||
UserGroupConfig best = null;
|
||||
for (Map<String, Object> g : groups) {
|
||||
long expiry = (long) g.get("expiry_time");
|
||||
if (expiry > 0 && expiry <= now) continue;
|
||||
String name = (String) g.get("group_name");
|
||||
UserGroupConfig cfg = groupConfigs.get(name);
|
||||
if (cfg != null && (best == null || cfg.priority > best.priority)) {
|
||||
best = cfg;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
public boolean isPlayerInGroup(String player, String groupName) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT 1 FROM user_group_member WHERE player_name=? AND group_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, groupName);
|
||||
boolean exists = ps.executeQuery().next();
|
||||
ps.close();
|
||||
return exists;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 过期自动移除 =====
|
||||
|
||||
private void startExpiryChecker() {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
checkExpiry();
|
||||
}
|
||||
}.runTaskTimerAsynchronously(plugin, 1200L, 1200L); // 每分钟检查
|
||||
}
|
||||
|
||||
private void checkExpiry() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT player_name, group_name FROM user_group_member "
|
||||
+ "WHERE expiry_time > 0 AND expiry_time <= ?");
|
||||
ps.setLong(1, now);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
List<String[]> expired = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
expired.add(new String[]{
|
||||
rs.getString("player_name"),
|
||||
rs.getString("group_name")});
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
|
||||
for (String[] e : expired) {
|
||||
PreparedStatement del = db.prepareStatement(
|
||||
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
|
||||
del.setString(1, e[0]);
|
||||
del.setString(2, e[1]);
|
||||
del.executeUpdate();
|
||||
del.close();
|
||||
plugin.getLogger().info("[UserGroup] 过期移除: " + e[0] + " -> " + e[1]);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 资源包分发 =====
|
||||
|
||||
public void sendResourcePack(Player p) {
|
||||
UserGroupConfig cfg = getHighestGroup(p.getName());
|
||||
if (cfg == null || cfg.packUrl == null || cfg.packUrl.isEmpty()) return;
|
||||
|
||||
String url = cfg.packUrl;
|
||||
String prompt = cfg.packPrompt != null
|
||||
? cfg.packPrompt : "Custom Resource Pack";
|
||||
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
try {
|
||||
p.setResourcePack(url, new byte[0], prompt, false);
|
||||
plugin.getLogger().info("[UserGroup] 资源包已发送: "
|
||||
+ p.getName() + " -> " + cfg.displayName);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[UserGroup] 资源包发送失败: " + e.getMessage());
|
||||
}
|
||||
}, 40L);
|
||||
}
|
||||
|
||||
public Map<String, UserGroupConfig> getGroupConfigs() {
|
||||
return groupConfigs;
|
||||
}
|
||||
|
||||
// ===== 配置类 =====
|
||||
|
||||
public static class UserGroupConfig {
|
||||
public String name;
|
||||
public String displayName = "";
|
||||
public String displayColor = "§f";
|
||||
public String displayEmoji = "";
|
||||
public int priority = 0;
|
||||
public boolean permanent = true;
|
||||
public int durationMinutes = 0;
|
||||
public String packUrl = "";
|
||||
public String packHash = "";
|
||||
public String packPrompt = "Custom Resource Pack";
|
||||
public int packSize = 0;
|
||||
public String localFile = "";
|
||||
}
|
||||
}
|
||||
@@ -1,158 +1,158 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class VerificationManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public VerificationManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isBedrockPlayer(Player p) {
|
||||
String uuid = p.getUniqueId().toString();
|
||||
return uuid.contains("f851ba4")
|
||||
|| uuid.startsWith("00000000-0000-0000");
|
||||
}
|
||||
|
||||
public boolean isOnlineMode() {
|
||||
return Bukkit.getServer().getOnlineMode();
|
||||
}
|
||||
|
||||
public void verifyPremiumAsync(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
Thread t = new Thread(() -> doVerify(player, callback),
|
||||
"Sdf1_login-PremiumVerify");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void doVerify(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
String name = player.getName();
|
||||
String uuid = player.getUniqueId().toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证开始 ======");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 玩家: " + name);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 当前UUID: " + uuid);
|
||||
|
||||
try {
|
||||
UUID offlineUuid = UUID.nameUUIDFromBytes(
|
||||
("OfflinePlayer:" + name)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
String offlineStr = offlineUuid.toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 离线UUID: " + offlineStr);
|
||||
|
||||
boolean isOfflineUuid =
|
||||
player.getUniqueId().equals(offlineUuid);
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] UUID匹配离线格式: "
|
||||
+ isOfflineUuid);
|
||||
|
||||
if (!isOfflineUuid) {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 正版玩家");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(true);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] UUID为离线格式,"
|
||||
+ "查询Mojang API...");
|
||||
UUID mojangUuid = fetchMojangUuid(name);
|
||||
|
||||
if (mojangUuid != null) {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang返回UUID: "
|
||||
+ mojangUuid);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 盗版客户端"
|
||||
+ "使用了正版名字");
|
||||
} else {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang无此名字");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 纯离线玩家");
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(false);
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 验证异常: "
|
||||
+ e.getMessage());
|
||||
callback.accept(false);
|
||||
}
|
||||
}
|
||||
|
||||
private UUID fetchMojangUuid(String name) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(
|
||||
"https://api.mojang.com/users/profiles/minecraft/"
|
||||
+ name);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang API响应: " + code);
|
||||
|
||||
if (code == 200) {
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
conn.getInputStream(),
|
||||
StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
reader.close();
|
||||
|
||||
String json = sb.toString();
|
||||
String uuidStr = json.replaceAll(
|
||||
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
|
||||
"$1");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang原始UUID: "
|
||||
+ uuidStr);
|
||||
|
||||
String formatted = uuidStr.replaceFirst(
|
||||
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
|
||||
"$1-$2-$3-$4-$5");
|
||||
return UUID.fromString(formatted);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] Mojang查询异常: "
|
||||
+ e.getMessage());
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class VerificationManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public VerificationManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isBedrockPlayer(Player p) {
|
||||
String uuid = p.getUniqueId().toString();
|
||||
return uuid.contains("f851ba4")
|
||||
|| uuid.startsWith("00000000-0000-0000");
|
||||
}
|
||||
|
||||
public boolean isOnlineMode() {
|
||||
return Bukkit.getServer().getOnlineMode();
|
||||
}
|
||||
|
||||
public void verifyPremiumAsync(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
Thread t = new Thread(() -> doVerify(player, callback),
|
||||
"Sdf1_login-PremiumVerify");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void doVerify(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
String name = player.getName();
|
||||
String uuid = player.getUniqueId().toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证开始 ======");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 玩家: " + name);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 当前UUID: " + uuid);
|
||||
|
||||
try {
|
||||
UUID offlineUuid = UUID.nameUUIDFromBytes(
|
||||
("OfflinePlayer:" + name)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
String offlineStr = offlineUuid.toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 离线UUID: " + offlineStr);
|
||||
|
||||
boolean isOfflineUuid =
|
||||
player.getUniqueId().equals(offlineUuid);
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] UUID匹配离线格式: "
|
||||
+ isOfflineUuid);
|
||||
|
||||
if (!isOfflineUuid) {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 正版玩家");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(true);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] UUID为离线格式,"
|
||||
+ "查询Mojang API...");
|
||||
UUID mojangUuid = fetchMojangUuid(name);
|
||||
|
||||
if (mojangUuid != null) {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang返回UUID: "
|
||||
+ mojangUuid);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 盗版客户端"
|
||||
+ "使用了正版名字");
|
||||
} else {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang无此名字");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 纯离线玩家");
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(false);
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 验证异常: "
|
||||
+ e.getMessage());
|
||||
callback.accept(false);
|
||||
}
|
||||
}
|
||||
|
||||
private UUID fetchMojangUuid(String name) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(
|
||||
"https://api.mojang.com/users/profiles/minecraft/"
|
||||
+ name);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang API响应: " + code);
|
||||
|
||||
if (code == 200) {
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
conn.getInputStream(),
|
||||
StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
reader.close();
|
||||
|
||||
String json = sb.toString();
|
||||
String uuidStr = json.replaceAll(
|
||||
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
|
||||
"$1");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] Mojang原始UUID: "
|
||||
+ uuidStr);
|
||||
|
||||
String formatted = uuidStr.replaceFirst(
|
||||
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
|
||||
"$1-$2-$3-$4-$5");
|
||||
return UUID.fromString(formatted);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] Mojang查询异常: "
|
||||
+ e.getMessage());
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ commands:
|
||||
签到:
|
||||
description: 每日签到(中文)
|
||||
usage: /签到
|
||||
oa:
|
||||
description: 申请删除账号
|
||||
usage: /oa [玩家名]
|
||||
#oa:
|
||||
# description: 申请删除账号
|
||||
# usage: /oa [玩家名]
|
||||
删除账号:
|
||||
description: 申请删除账号(中文)
|
||||
usage: /删除账号 [玩家名]
|
||||
@@ -60,18 +60,12 @@ commands:
|
||||
deny:
|
||||
description: 拒绝OA请求
|
||||
usage: /deny <编号> [理由]
|
||||
sdf1debug:
|
||||
description: 调试指令
|
||||
usage: /sdf1debug <玩家> take.<阶段>
|
||||
email:
|
||||
description: 绑定邮箱
|
||||
usage: /email <邮箱>
|
||||
绑定:
|
||||
description: 绑定邮箱(中文)
|
||||
usage: /绑定 <邮箱>
|
||||
调试:
|
||||
description: 调试模式
|
||||
usage: /调试
|
||||
绑定邮箱:
|
||||
description: 绑定邮箱
|
||||
usage: /绑定邮箱
|
||||
@@ -86,4 +80,25 @@ commands:
|
||||
usage: /垃圾清理
|
||||
menu:
|
||||
description: 雪球菜单开关控制
|
||||
usage: /menu <开/关>
|
||||
usage: /menu <开/关>
|
||||
pvp :
|
||||
description: PVP榜单
|
||||
usage: /pvp
|
||||
cypay :
|
||||
description: 草原探险债券
|
||||
usage: /cypay
|
||||
printer :
|
||||
description: 打印债券流水
|
||||
usage: /printer
|
||||
shop :
|
||||
description: 游戏商人
|
||||
usage: /shop
|
||||
商店:
|
||||
description: 打开商店
|
||||
usage: /商店
|
||||
#testorder:
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
protect:
|
||||
description: 区域保护
|
||||
usage: /protect
|
||||
Reference in New Issue
Block a user