Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcca065931 | ||
|
|
a34cc92327 | ||
|
|
44302d8eff | ||
|
|
7624b7cb7d | ||
|
|
a4b09254f4 | ||
|
|
68a6d2e7d4 | ||
|
|
7d70f8f92e | ||
|
|
c66ec5eaaa | ||
|
|
aa719dc2da | ||
|
|
32371a6e30 | ||
|
|
7953e9de13 | ||
|
|
038e2f3e87 | ||
|
|
a35ddb8418 | ||
|
|
3c65360f9c | ||
|
|
7b0722b039 | ||
|
|
49fb264378 | ||
|
|
7dca1a71e1 | ||
|
|
704b8a03d9 | ||
|
|
387fad8bdd | ||
|
|
acc4085509 | ||
|
|
a75be0ebae | ||
|
|
325ec9f877 | ||
|
|
851375502d | ||
|
|
c9da8c8ec2 | ||
|
|
cf246d4076 | ||
|
|
c747dbcc76 | ||
|
|
66f1e33d5e | ||
|
|
717a7a9266 | ||
|
|
34d46a7d12 | ||
|
|
1f12d3ec30 | ||
|
|
1a58caec96 | ||
|
|
ea93ccc087 | ||
|
|
302328bc8e |
@@ -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: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 建议内容
|
||||||
|
请描述你的建议或想法。
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
什么场景下产生的这个建议?
|
||||||
|
|
||||||
|
## 预期效果
|
||||||
|
你认为采用这个建议后能达到什么效果?
|
||||||
@@ -36,4 +36,11 @@ build/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# 开发辅助文件(本地保留,不推送到仓库)
|
||||||
|
txt/
|
||||||
|
*.xlsx
|
||||||
|
*.patch
|
||||||
|
新建*.txt
|
||||||
|
群戏归类.txt
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# 默认忽略的文件
|
# 默认忽略的文件
|
||||||
/shelf/
|
/shelf/
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
# 基于编辑器的 HTTP 客户端请求
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
/httpRequests/
|
/httpRequests/
|
||||||
# 已忽略包含查询文件的默认文件夹
|
# 已忽略包含查询文件的默认文件夹
|
||||||
/queries/
|
/queries/
|
||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="Encoding">
|
<component name="Encoding">
|
||||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="MavenProjectsManager">
|
<component name="MavenProjectsManager">
|
||||||
<option name="originalFiles">
|
<option name="originalFiles">
|
||||||
<list>
|
<list>
|
||||||
<option value="$PROJECT_DIR$/pom.xml" />
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,112 +1,569 @@
|
|||||||
# Sdf1系列登录插件使用说明
|
# Sdf1系列登录插件使用说明
|
||||||
|
|
||||||
|
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
|
||||||
|
|
||||||
## 1. 功能介绍
|
## 1. 功能介绍
|
||||||
|
|
||||||
| 名字 | 描述/作用 | 施工状态 |
|
| 名字 | 描述/作用 | 施工状态 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| 登录 | 用户登录/注册 | 已完成 |
|
| 登录 | 用户登录/注册 | 已完成 |
|
||||||
| 世界垃圾箱 | 清理服务器凋落物 | 半成品 |
|
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
||||||
| 工单系统 | 用户提交工单 | 已完成 |
|
| 工单系统 | 用户提交工单 | 已完成 |
|
||||||
| 签到系统 | 签到积分换奖励 | 已完成 |
|
| 签到系统 | 签到积分换奖励 | 已完成 |
|
||||||
|
| 新人礼包 | 新玩家7日礼包 | 已完成 |
|
||||||
|
| 挂机检测 | 挂机检测踢出 | 已完成 |
|
||||||
|
| 自定义菜单 | 可以自己diy指令 | 已完成 |
|
||||||
|
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
|
||||||
|
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
|
||||||
|
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
|
||||||
|
|
||||||
## 2. 安装说明
|
|
||||||
|
|
||||||
| 前置插件 | 选装类型 |
|
## 2.安装说明:
|
||||||
| :--- | :--- |
|
|
||||||
| CY_beibao | 可选 |
|
| 前置插件 | 选装类型 | 功能作用 |
|
||||||
| EssentialsX | 可选 |
|
| :--- | :--- | :--- |
|
||||||
|
| [CY_beibao](https://github.com/ypsdf1/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
|
||||||
|
| EssentialsX | 可选 | 主流经济系统
|
||||||
|
| [sdf1](https://github.com/ypsdf1/sdf1_plugn) | 可选,推荐 | CDK兑换系统
|
||||||
|
|
||||||
|
|
||||||
### 2.1 下载
|
### 2.1 下载
|
||||||
您可以从GitHub和Gitee下载插件,将其放入服务器plugin目录,重启服务器即可。
|
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||||
|
|
||||||
### 2.2 配置文件
|
### 2.2. 配置文件
|
||||||
|
|
||||||
#### 2.2.1 消息.txt
|
#### 2.2.1 消息.txt
|
||||||
此文件控制整个插件的所有提示消息,修改后执行 /sdf1_login reload 重载插件。
|
此文件控制着整个插件的所有提示消息,您可以根据自己的需求修改,修改后需要执行 /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>`进行段落换行
|
||||||
|
|
||||||
#### 2.2.2 挂机白名单.txt
|
|
||||||
挂机踢出开启时,该配置下的玩家不受影响。
|
|
||||||
|
|
||||||
#### 2.2.3 SMTP设置.txt
|
```txt
|
||||||
控制SMTP邮件服务器配置,用于找回密码时发送邮件。
|
# ==========================================
|
||||||
|
# Sdf1_login 消息配置文件
|
||||||
|
# ==========================================
|
||||||
|
# 颜色代码使用 § 符号
|
||||||
|
# §0黑色 §1深蓝 §2深绿 §3深青
|
||||||
|
# §4深红 §5深紫 §6金色 §7灰色
|
||||||
|
# §8深灰 §9蓝色 §a绿色 §b浅蓝
|
||||||
|
# §c红色 §d粉色 §e黄色 §f白色
|
||||||
|
# §l粗体 §n下划线 §o斜体 §r重置
|
||||||
|
#
|
||||||
|
# 占位符用 {xxx} 表示,运行时自动替换
|
||||||
|
# 支持 <b>粗体</b> 标签
|
||||||
|
# 支持 \n 或 <br> 换行
|
||||||
|
# 修改后执行 /sdf1_login reload 重载
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
#### 2.2.4 插件设置.txt
|
# ---- 登录注册相关 ----
|
||||||
管控整个插件,包括账号注册限制、管理员密码等。
|
# login_timeout: 登录超时被踢出时的提示
|
||||||
|
login_timeout=§c登录超时,请重新登录
|
||||||
|
# not_registered: 未注册玩家收到的提示
|
||||||
|
not_registered=§e您尚未注册,请使用 /reg <密码>
|
||||||
|
# not_logged_in: 已注册但未登录的提示
|
||||||
|
not_logged_in=§e您尚未登录,请使用 /login <密码>
|
||||||
|
# already_registered: 已注册玩家再次注册时的提示
|
||||||
|
already_registered=§c您已注册过账号
|
||||||
|
# already_logged_in: 已登录玩家再次登录时的提示
|
||||||
|
already_logged_in=§c您已登录
|
||||||
|
# reg_success: 注册成功提示
|
||||||
|
reg_success=§a注册成功!欢迎来到服务器
|
||||||
|
# login_success: 登录成功提示
|
||||||
|
login_success=§a登录成功!
|
||||||
|
# login_failed: 密码错误提示
|
||||||
|
login_failed=§c密码错误,请重试
|
||||||
|
|
||||||
#### 2.2.5 chat.txt
|
# ---- 密码相关 ----
|
||||||
控制聊天过滤,处理违规链接。
|
# password_wrong: 旧密码验证失败
|
||||||
|
password_wrong=§c密码错误
|
||||||
|
# password_changed: 密码修改成功
|
||||||
|
password_changed=§a密码修改成功!
|
||||||
|
# password_format_error: 密码格式不符合要求
|
||||||
|
password_format_error=§c密码需6位以上,含大小写字母和数字
|
||||||
|
# password_same: 新密码与旧密码相同
|
||||||
|
password_same=§c新密码不能与旧密码相同
|
||||||
|
# reset_no_email: 找回密码时未绑定邮箱
|
||||||
|
reset_no_email=§c您未绑定邮箱,无法找回密码
|
||||||
|
# reset_sent: 临时密码已发送到邮箱
|
||||||
|
reset_sent=§a临时密码已发送到您的邮箱,请查收
|
||||||
|
|
||||||
## 3. 指令
|
# ---- 签到积分相关 ----
|
||||||
|
# checkin_already: 今日已签到
|
||||||
|
checkin_already=§c您今日已签到
|
||||||
|
# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率
|
||||||
|
checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}
|
||||||
|
# points_insufficient: 积分不足
|
||||||
|
points_insufficient=§c积分不足,当前: {points}
|
||||||
|
# points_purchase_success: 积分购买成功
|
||||||
|
points_purchase_success=§a购买成功!消耗 {count} 积分
|
||||||
|
|
||||||
- Sdf1_login 主指令
|
# ---- 挂机检测相关 ----
|
||||||
- Sdf1_login reload 重载配置
|
# afk_set_enabled: 挂机检测开启
|
||||||
- reg 注册账号
|
afk_set_enabled=§a挂机检测已开启
|
||||||
- login 登录账号
|
# afk_set_disabled: 挂机检测关闭
|
||||||
- l 登录账号
|
afk_set_disabled=§c挂机检测已关闭
|
||||||
|
# afk_kick: 因挂机被踢出
|
||||||
|
afk_kick=§c因挂机过久被踢出
|
||||||
|
|
||||||
## 4. 运行截图
|
# ---- 邮箱相关 ----
|
||||||
|
# email_set: 邮箱设置成功
|
||||||
|
email_set=§a邮箱设置成功:{email}
|
||||||
|
# email_format_error: 邮箱格式错误
|
||||||
|
email_format_error=§c邮箱格式不正确
|
||||||
|
|
||||||
|
# ---- 邀请相关 ----
|
||||||
|
# invite_code_generated: 邀请码生成
|
||||||
|
invite_code_generated=§a邀请码:{code}
|
||||||
|
# invite_used: 使用邀请码成功
|
||||||
|
invite_used=§a邀请成功,邀请人:{player}
|
||||||
|
# invite_self: 使用自己的邀请码
|
||||||
|
invite_self=§c不能使用自己的邀请码
|
||||||
|
# invite_not_found: 邀请码不存在
|
||||||
|
invite_not_found=§c邀请码不存在
|
||||||
|
# invite_referral_bonus: 被邀请人签到后邀请人获得奖励
|
||||||
|
invite_referral_bonus=§a邀请奖励!获得 {points} 积分
|
||||||
|
|
||||||
|
# ---- 新人礼包相关 ----
|
||||||
|
# gift_not_ready: 礼包条件未满足
|
||||||
|
gift_not_ready=§c礼包条件未满足
|
||||||
|
# gift_claimed: 礼包领取成功
|
||||||
|
gift_claimed=§a礼包领取成功!
|
||||||
|
|
||||||
|
# ---- 审批与IP限制 ----
|
||||||
|
# need_approval: 注册需审批
|
||||||
|
need_approval=§e注册需审批 (工单#{id})
|
||||||
|
# ip_max_accounts: IP注册已达上限
|
||||||
|
ip_max_accounts=§cIP注册已达上限
|
||||||
|
|
||||||
|
# ---- 管理员操作 ----
|
||||||
|
# admin_delete_confirm: 管理员删除账号确认提示
|
||||||
|
admin_delete_confirm=§c请在聊天中输入玩家名确认删除
|
||||||
|
|
||||||
|
# ---- 聊天过滤 ----
|
||||||
|
# chat_muted: 禁言玩家尝试聊天
|
||||||
|
chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天
|
||||||
|
# chat_url_blocked: 发送违规链接被拦截
|
||||||
|
# {url}=检测到的链接地址
|
||||||
|
chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}
|
||||||
|
# chat_url_violation: 当前违规次数
|
||||||
|
# {count}=当前违规次数
|
||||||
|
chat_url_violation=§7当前违规次数: §e{count}
|
||||||
|
# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数
|
||||||
|
chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)
|
||||||
|
# chat_url_broadcast: 全服通报,{player}=玩家名
|
||||||
|
chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚
|
||||||
|
|
||||||
|
# ---- 通用 ----
|
||||||
|
# 未知参数: 未知命令或权限不足时的提示
|
||||||
|
未知参数=§c未知参数或权限不足
|
||||||
|
# only_player: 仅玩家可用的命令
|
||||||
|
# only_player=§c此命令仅玩家可用
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.2 挂机白名单.txt
|
||||||
|
当挂机踢出开启时,该配置下的玩家不受挂机提出影响
|
||||||
|
|
||||||
|
### 2.2.3 SMTP设置.txt
|
||||||
|
|
||||||
|
此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。<br>可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ===== SMTP 邮件服务器配置 =====
|
||||||
|
smtp地址=smtp.example.com
|
||||||
|
smtp端口=465
|
||||||
|
smtp账号=mail@example.com
|
||||||
|
smtp密码=example.com
|
||||||
|
发件人名称=Sdf1_login
|
||||||
|
smtp加密=true
|
||||||
|
验证码接收邮箱=example@example.com
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.4 插件设置.txt
|
||||||
|
|
||||||
|
此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ===== Sdf1_login 插件设置 =====
|
||||||
|
管理标签=admin
|
||||||
|
管理密码=qweasd
|
||||||
|
每IP最大账号数=3
|
||||||
|
审批模式=manual
|
||||||
|
自动审批延迟分钟=30
|
||||||
|
登录超时秒数=60
|
||||||
|
挂机踢出=false
|
||||||
|
挂机超时秒数=300
|
||||||
|
服务商积分倍率=2.0
|
||||||
|
基础经济奖励=100
|
||||||
|
经济奖励每评分=50.0
|
||||||
|
报单人积分奖励=10
|
||||||
|
# ---- 签到盲盒奖励 ----
|
||||||
|
签到固定金额=0
|
||||||
|
签到最小金额=10
|
||||||
|
签到最大金额=100
|
||||||
|
签到给积分=true
|
||||||
|
签到积分数量=10
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.5 chat.txt
|
||||||
|
该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。<br>
|
||||||
|
可用的处罚规则:
|
||||||
|
- warn: 警告
|
||||||
|
- mute: 禁言
|
||||||
|
- kick: 踢出
|
||||||
|
- ban: 封禁
|
||||||
|
- banip:封禁IP
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Sdf1_chat 聊天过滤配置
|
||||||
|
通知管理员: true
|
||||||
|
全服通报: true
|
||||||
|
禁言时长: 300
|
||||||
|
|
||||||
|
白名单:
|
||||||
|
*.minecraft.net
|
||||||
|
baidu.com
|
||||||
|
github.com
|
||||||
|
|
||||||
|
处罚规则:
|
||||||
|
1:warn:0
|
||||||
|
2:warn:0
|
||||||
|
3:mute:300
|
||||||
|
5:mute:600
|
||||||
|
8:kick:0
|
||||||
|
10:ban:0
|
||||||
|
|
||||||
|
白名单玩家:
|
||||||
|
|
||||||
|
```
|
||||||
|
### 2.2.6 radio_config.txt
|
||||||
|
```txt
|
||||||
|
# ============ Sdf1 Radio Config ============
|
||||||
|
#
|
||||||
|
# resource-pack-url
|
||||||
|
# 填外部地址(HTTP或HTTPS均可)
|
||||||
|
# 例: https://example.com/radiopack.zip
|
||||||
|
# 留空 = 使用内置HTTP服务
|
||||||
|
#
|
||||||
|
# http-port
|
||||||
|
# 内置HTTP端口(仅resource-pack-url为空时生效)
|
||||||
|
#
|
||||||
|
# ============ Settings ===============
|
||||||
|
|
||||||
|
resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc
|
||||||
|
http-port=443
|
||||||
|
```
|
||||||
|
- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了
|
||||||
|
|
||||||
|
### 2.2.7 菜单.txt
|
||||||
|
- 自定义菜单配置文件
|
||||||
|
|
||||||
|
```json
|
||||||
|
# 服务器菜单
|
||||||
|
# 使用类json格式
|
||||||
|
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:主城传送
|
||||||
|
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
|
||||||
|
权限类型:玩家
|
||||||
|
图标:COMPASS
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:随机传送
|
||||||
|
指令:/rtp
|
||||||
|
权限类型:玩家
|
||||||
|
图标:ENDER_PEARL
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:擂台比武
|
||||||
|
指令:/dom tp PVP
|
||||||
|
权限类型:玩家
|
||||||
|
图标:netherite_spear
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:领地操作
|
||||||
|
指令:/dom
|
||||||
|
权限类型:玩家
|
||||||
|
图标:paper
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:逛商店
|
||||||
|
指令:/dom tp shop
|
||||||
|
权限类型:玩家
|
||||||
|
图标:honeycomb
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:传送开关
|
||||||
|
指令:/tptoggle
|
||||||
|
权限类型:玩家
|
||||||
|
图标:redstone_torch
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:返回上一个位置
|
||||||
|
指令:/back
|
||||||
|
权限类型:玩家
|
||||||
|
图标:red_bed
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:前往小黑塔
|
||||||
|
指令:/dom tp 小黑塔
|
||||||
|
权限类型:玩家
|
||||||
|
图标:dragon_head
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:云背包
|
||||||
|
指令:/cy
|
||||||
|
权限类型:玩家
|
||||||
|
图标:potato
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:自杀
|
||||||
|
指令:/suicide
|
||||||
|
权限类型:玩家
|
||||||
|
图标:wooden_axe
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:游戏商人
|
||||||
|
指令:/shop
|
||||||
|
权限类型:玩家
|
||||||
|
图标:shield
|
||||||
|
格子: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. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
|
||||||
|
<br>
|
||||||
|

|
||||||
|
5. 想查看玩家消费记录?小手一点,明明白白<br>
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2.4 打印流水部分介绍
|
||||||
|
1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天<br>
|
||||||
|
2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 3. 指令参数及介绍
|
||||||
|
|
||||||
|
- Sdf1_login 主指令<br>
|
||||||
|
- Sdf1_login reload 重载配置文件<br>
|
||||||
|
- reg 注册账号<br>
|
||||||
|
- login 登录账号<br>
|
||||||
|
- l 登录账号<br>
|
||||||
|
- sdf1_login del 删除玩家账号<br>
|
||||||
|
- sdf1_login radio 停止播放广播<br>
|
||||||
|
- 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>
|
||||||
|
|
||||||
|
| 指令 | 参数1| 参数2 | 参数3 | 结果 |
|
||||||
|
| :---: | :---: | :---: | :---: | :---: |
|
||||||
|
| protect | 无 | 无 | 无 | 帮助信息 |
|
||||||
|
| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 |
|
||||||
|
| protect | add | 玩家名 | 无| 加白指定玩家为全局 |
|
||||||
|
| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 |
|
||||||
|
| protect | additem | 物品名 | 无 | 加黑指定物品到全局 |
|
||||||
|
| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 |
|
||||||
|
| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 |
|
||||||
|
| protect | remove | 玩家名字 | 无 | 移除玩家为全白 |
|
||||||
|
| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 |
|
||||||
|
| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑
|
||||||
|
| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑
|
||||||
|
| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 |
|
||||||
|
| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 |
|
||||||
|
| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 |
|
||||||
|
|
||||||
|
示范:
|
||||||
|
- `/protect add 末地保护区 players` 添加players玩家到末地保护区<br>
|
||||||
|
- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品<br>
|
||||||
|
- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。<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 违规
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行截图
|
||||||
|
|
||||||
主界面
|
主界面
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
邀请数据
|
邀请数据
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
积分商店
|
积分商店
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
任务中心
|
任务中心
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
垃圾箱
|
垃圾箱
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
管理员面板
|
管理员面板
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
管理面板-用户管理
|
管理面板-用户管理
|
||||||
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
工单中心
|
工单中心
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
工单管理
|
工单管理
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
我的工单
|
我的工单
|
||||||
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
提交工单
|
提交工单
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
服务商列表
|
服务商列表
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
服务商抢单大厅
|
服务商抢单大厅
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
服务商-接单界面
|
服务商-接单界面
|
||||||
|

|
||||||
|
|
||||||

|
管理员-工单详情页面
|
||||||
|

|
||||||
|
|
||||||
管理员-工单详情
|
商城主界面
|
||||||
|

|
||||||
|
|
||||||

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

|
||||||
|
|
||||||
## 5. 下载地址
|
商城退单
|
||||||
|

|
||||||
|
|
||||||
- GitHub: https://github.com/ypsdf1/sdf1_login
|
|
||||||
- Gitee: https://gitee.com/nihaoshidifu/sdf1_login
|
## 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)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
*.md text=auto encoding=UTF-8
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
.kotlin
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# 开发辅助文件(本地保留,不推送到仓库)
|
||||||
|
txt/
|
||||||
|
*.xlsx
|
||||||
|
*.patch
|
||||||
|
新建*.txt
|
||||||
|
群戏归类.txt
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
# Sdf1系列登录插件使用说明
|
||||||
|
|
||||||
|
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
|
||||||
|
|
||||||
|
## 1. 功能介绍
|
||||||
|
|
||||||
|
| 名字 | 描述/作用 | 施工状态 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| 登录 | 用户登录/注册 | 已完成 |
|
||||||
|
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
||||||
|
| 工单系统 | 用户提交工单 | 已完成 |
|
||||||
|
| 签到系统 | 签到积分换奖励 | 已完成 |
|
||||||
|
| 新人礼包 | 新玩家7日礼包 | 已完成 |
|
||||||
|
| 挂机检测 | 挂机检测踢出 | 已完成 |
|
||||||
|
| 自定义菜单 | 可以自己diy指令 | 已完成 |
|
||||||
|
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
|
||||||
|
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
|
||||||
|
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
|
||||||
|
|
||||||
|
|
||||||
|
## 2.安装说明:
|
||||||
|
|
||||||
|
| 前置插件 | 选装类型 | 功能作用 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| [CY_beibao](https://gitee.com/nihaoshidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
|
||||||
|
| EssentialsX | 可选 | 主流经济系统
|
||||||
|
| [sdf1](https://gitee.com/nihaoshidifu/sdf1_plugn) | 可选,推荐 | CDK兑换系统
|
||||||
|
|
||||||
|
|
||||||
|
### 2.1 下载
|
||||||
|
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||||
|
|
||||||
|
### 2.2. 配置文件
|
||||||
|
|
||||||
|
#### 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>`进行段落换行
|
||||||
|
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ==========================================
|
||||||
|
# Sdf1_login 消息配置文件
|
||||||
|
# ==========================================
|
||||||
|
# 颜色代码使用 § 符号
|
||||||
|
# §0黑色 §1深蓝 §2深绿 §3深青
|
||||||
|
# §4深红 §5深紫 §6金色 §7灰色
|
||||||
|
# §8深灰 §9蓝色 §a绿色 §b浅蓝
|
||||||
|
# §c红色 §d粉色 §e黄色 §f白色
|
||||||
|
# §l粗体 §n下划线 §o斜体 §r重置
|
||||||
|
#
|
||||||
|
# 占位符用 {xxx} 表示,运行时自动替换
|
||||||
|
# 支持 <b>粗体</b> 标签
|
||||||
|
# 支持 \n 或 <br> 换行
|
||||||
|
# 修改后执行 /sdf1_login reload 重载
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# ---- 登录注册相关 ----
|
||||||
|
# login_timeout: 登录超时被踢出时的提示
|
||||||
|
login_timeout=§c登录超时,请重新登录
|
||||||
|
# not_registered: 未注册玩家收到的提示
|
||||||
|
not_registered=§e您尚未注册,请使用 /reg <密码>
|
||||||
|
# not_logged_in: 已注册但未登录的提示
|
||||||
|
not_logged_in=§e您尚未登录,请使用 /login <密码>
|
||||||
|
# already_registered: 已注册玩家再次注册时的提示
|
||||||
|
already_registered=§c您已注册过账号
|
||||||
|
# already_logged_in: 已登录玩家再次登录时的提示
|
||||||
|
already_logged_in=§c您已登录
|
||||||
|
# reg_success: 注册成功提示
|
||||||
|
reg_success=§a注册成功!欢迎来到服务器
|
||||||
|
# login_success: 登录成功提示
|
||||||
|
login_success=§a登录成功!
|
||||||
|
# login_failed: 密码错误提示
|
||||||
|
login_failed=§c密码错误,请重试
|
||||||
|
|
||||||
|
# ---- 密码相关 ----
|
||||||
|
# password_wrong: 旧密码验证失败
|
||||||
|
password_wrong=§c密码错误
|
||||||
|
# password_changed: 密码修改成功
|
||||||
|
password_changed=§a密码修改成功!
|
||||||
|
# password_format_error: 密码格式不符合要求
|
||||||
|
password_format_error=§c密码需6位以上,含大小写字母和数字
|
||||||
|
# password_same: 新密码与旧密码相同
|
||||||
|
password_same=§c新密码不能与旧密码相同
|
||||||
|
# reset_no_email: 找回密码时未绑定邮箱
|
||||||
|
reset_no_email=§c您未绑定邮箱,无法找回密码
|
||||||
|
# reset_sent: 临时密码已发送到邮箱
|
||||||
|
reset_sent=§a临时密码已发送到您的邮箱,请查收
|
||||||
|
|
||||||
|
# ---- 签到积分相关 ----
|
||||||
|
# checkin_already: 今日已签到
|
||||||
|
checkin_already=§c您今日已签到
|
||||||
|
# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率
|
||||||
|
checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}
|
||||||
|
# points_insufficient: 积分不足
|
||||||
|
points_insufficient=§c积分不足,当前: {points}
|
||||||
|
# points_purchase_success: 积分购买成功
|
||||||
|
points_purchase_success=§a购买成功!消耗 {count} 积分
|
||||||
|
|
||||||
|
# ---- 挂机检测相关 ----
|
||||||
|
# afk_set_enabled: 挂机检测开启
|
||||||
|
afk_set_enabled=§a挂机检测已开启
|
||||||
|
# afk_set_disabled: 挂机检测关闭
|
||||||
|
afk_set_disabled=§c挂机检测已关闭
|
||||||
|
# afk_kick: 因挂机被踢出
|
||||||
|
afk_kick=§c因挂机过久被踢出
|
||||||
|
|
||||||
|
# ---- 邮箱相关 ----
|
||||||
|
# email_set: 邮箱设置成功
|
||||||
|
email_set=§a邮箱设置成功:{email}
|
||||||
|
# email_format_error: 邮箱格式错误
|
||||||
|
email_format_error=§c邮箱格式不正确
|
||||||
|
|
||||||
|
# ---- 邀请相关 ----
|
||||||
|
# invite_code_generated: 邀请码生成
|
||||||
|
invite_code_generated=§a邀请码:{code}
|
||||||
|
# invite_used: 使用邀请码成功
|
||||||
|
invite_used=§a邀请成功,邀请人:{player}
|
||||||
|
# invite_self: 使用自己的邀请码
|
||||||
|
invite_self=§c不能使用自己的邀请码
|
||||||
|
# invite_not_found: 邀请码不存在
|
||||||
|
invite_not_found=§c邀请码不存在
|
||||||
|
# invite_referral_bonus: 被邀请人签到后邀请人获得奖励
|
||||||
|
invite_referral_bonus=§a邀请奖励!获得 {points} 积分
|
||||||
|
|
||||||
|
# ---- 新人礼包相关 ----
|
||||||
|
# gift_not_ready: 礼包条件未满足
|
||||||
|
gift_not_ready=§c礼包条件未满足
|
||||||
|
# gift_claimed: 礼包领取成功
|
||||||
|
gift_claimed=§a礼包领取成功!
|
||||||
|
|
||||||
|
# ---- 审批与IP限制 ----
|
||||||
|
# need_approval: 注册需审批
|
||||||
|
need_approval=§e注册需审批 (工单#{id})
|
||||||
|
# ip_max_accounts: IP注册已达上限
|
||||||
|
ip_max_accounts=§cIP注册已达上限
|
||||||
|
|
||||||
|
# ---- 管理员操作 ----
|
||||||
|
# admin_delete_confirm: 管理员删除账号确认提示
|
||||||
|
admin_delete_confirm=§c请在聊天中输入玩家名确认删除
|
||||||
|
|
||||||
|
# ---- 聊天过滤 ----
|
||||||
|
# chat_muted: 禁言玩家尝试聊天
|
||||||
|
chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天
|
||||||
|
# chat_url_blocked: 发送违规链接被拦截
|
||||||
|
# {url}=检测到的链接地址
|
||||||
|
chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}
|
||||||
|
# chat_url_violation: 当前违规次数
|
||||||
|
# {count}=当前违规次数
|
||||||
|
chat_url_violation=§7当前违规次数: §e{count}
|
||||||
|
# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数
|
||||||
|
chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)
|
||||||
|
# chat_url_broadcast: 全服通报,{player}=玩家名
|
||||||
|
chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚
|
||||||
|
|
||||||
|
# ---- 通用 ----
|
||||||
|
# 未知参数: 未知命令或权限不足时的提示
|
||||||
|
未知参数=§c未知参数或权限不足
|
||||||
|
# only_player: 仅玩家可用的命令
|
||||||
|
# only_player=§c此命令仅玩家可用
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.2 挂机白名单.txt
|
||||||
|
当挂机踢出开启时,该配置下的玩家不受挂机提出影响
|
||||||
|
|
||||||
|
### 2.2.3 SMTP设置.txt
|
||||||
|
|
||||||
|
此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。<br>可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ===== SMTP 邮件服务器配置 =====
|
||||||
|
smtp地址=smtp.example.com
|
||||||
|
smtp端口=465
|
||||||
|
smtp账号=mail@example.com
|
||||||
|
smtp密码=example.com
|
||||||
|
发件人名称=Sdf1_login
|
||||||
|
smtp加密=true
|
||||||
|
验证码接收邮箱=example@example.com
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.4 插件设置.txt
|
||||||
|
|
||||||
|
此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ===== Sdf1_login 插件设置 =====
|
||||||
|
管理标签=admin
|
||||||
|
管理密码=qweasd
|
||||||
|
每IP最大账号数=3
|
||||||
|
审批模式=manual
|
||||||
|
自动审批延迟分钟=30
|
||||||
|
登录超时秒数=60
|
||||||
|
挂机踢出=false
|
||||||
|
挂机超时秒数=300
|
||||||
|
服务商积分倍率=2.0
|
||||||
|
基础经济奖励=100
|
||||||
|
经济奖励每评分=50.0
|
||||||
|
报单人积分奖励=10
|
||||||
|
# ---- 签到盲盒奖励 ----
|
||||||
|
签到固定金额=0
|
||||||
|
签到最小金额=10
|
||||||
|
签到最大金额=100
|
||||||
|
签到给积分=true
|
||||||
|
签到积分数量=10
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.5 chat.txt
|
||||||
|
该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。<br>
|
||||||
|
可用的处罚规则:
|
||||||
|
- warn: 警告
|
||||||
|
- mute: 禁言
|
||||||
|
- kick: 踢出
|
||||||
|
- ban: 封禁
|
||||||
|
- banip:封禁IP
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Sdf1_chat 聊天过滤配置
|
||||||
|
通知管理员: true
|
||||||
|
全服通报: true
|
||||||
|
禁言时长: 300
|
||||||
|
|
||||||
|
白名单:
|
||||||
|
*.minecraft.net
|
||||||
|
baidu.com
|
||||||
|
github.com
|
||||||
|
|
||||||
|
处罚规则:
|
||||||
|
1:warn:0
|
||||||
|
2:warn:0
|
||||||
|
3:mute:300
|
||||||
|
5:mute:600
|
||||||
|
8:kick:0
|
||||||
|
10:ban:0
|
||||||
|
|
||||||
|
白名单玩家:
|
||||||
|
|
||||||
|
```
|
||||||
|
### 2.2.6 radio_config.txt
|
||||||
|
```txt
|
||||||
|
# ============ Sdf1 Radio Config ============
|
||||||
|
#
|
||||||
|
# resource-pack-url
|
||||||
|
# 填外部地址(HTTP或HTTPS均可)
|
||||||
|
# 例: https://example.com/radiopack.zip
|
||||||
|
# 留空 = 使用内置HTTP服务
|
||||||
|
#
|
||||||
|
# http-port
|
||||||
|
# 内置HTTP端口(仅resource-pack-url为空时生效)
|
||||||
|
#
|
||||||
|
# ============ Settings ===============
|
||||||
|
|
||||||
|
resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc
|
||||||
|
http-port=443
|
||||||
|
```
|
||||||
|
- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了
|
||||||
|
|
||||||
|
### 2.2.7 菜单.txt
|
||||||
|
- 自定义菜单配置文件
|
||||||
|
|
||||||
|
```json
|
||||||
|
# 服务器菜单
|
||||||
|
# 使用类json格式
|
||||||
|
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:主城传送
|
||||||
|
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
|
||||||
|
权限类型:玩家
|
||||||
|
图标:COMPASS
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:随机传送
|
||||||
|
指令:/rtp
|
||||||
|
权限类型:玩家
|
||||||
|
图标:ENDER_PEARL
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:擂台比武
|
||||||
|
指令:/dom tp PVP
|
||||||
|
权限类型:玩家
|
||||||
|
图标:netherite_spear
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:领地操作
|
||||||
|
指令:/dom
|
||||||
|
权限类型:玩家
|
||||||
|
图标:paper
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:逛商店
|
||||||
|
指令:/dom tp shop
|
||||||
|
权限类型:玩家
|
||||||
|
图标:honeycomb
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:传送开关
|
||||||
|
指令:/tptoggle
|
||||||
|
权限类型:玩家
|
||||||
|
图标:redstone_torch
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:返回上一个位置
|
||||||
|
指令:/back
|
||||||
|
权限类型:玩家
|
||||||
|
图标:red_bed
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:前往小黑塔
|
||||||
|
指令:/dom tp 小黑塔
|
||||||
|
权限类型:玩家
|
||||||
|
图标:dragon_head
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:云背包
|
||||||
|
指令:/cy
|
||||||
|
权限类型:玩家
|
||||||
|
图标:potato
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:自杀
|
||||||
|
指令:/suicide
|
||||||
|
权限类型:玩家
|
||||||
|
图标:wooden_axe
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:游戏商人
|
||||||
|
指令:/shop
|
||||||
|
权限类型:玩家
|
||||||
|
图标:shield
|
||||||
|
格子: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. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
|
||||||
|
<br>
|
||||||
|

|
||||||
|
5. 想查看玩家消费记录?小手一点,明明白白<br>
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2.4 打印流水部分介绍
|
||||||
|
1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天<br>
|
||||||
|
2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 3. 指令参数及介绍
|
||||||
|
|
||||||
|
- Sdf1_login 主指令<br>
|
||||||
|
- Sdf1_login reload 重载配置文件<br>
|
||||||
|
- reg 注册账号<br>
|
||||||
|
- login 登录账号<br>
|
||||||
|
- l 登录账号<br>
|
||||||
|
- sdf1_login del 删除玩家账号<br>
|
||||||
|
- sdf1_login radio 停止播放广播<br>
|
||||||
|
- 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>
|
||||||
|
|
||||||
|
| 指令 | 参数1| 参数2 | 参数3 | 结果 |
|
||||||
|
| :---: | :---: | :---: | :---: | :---: |
|
||||||
|
| protect | 无 | 无 | 无 | 帮助信息 |
|
||||||
|
| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 |
|
||||||
|
| protect | add | 玩家名 | 无| 加白指定玩家为全局 |
|
||||||
|
| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 |
|
||||||
|
| protect | additem | 物品名 | 无 | 加黑指定物品到全局 |
|
||||||
|
| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 |
|
||||||
|
| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 |
|
||||||
|
| protect | remove | 玩家名字 | 无 | 移除玩家为全白 |
|
||||||
|
| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 |
|
||||||
|
| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑
|
||||||
|
| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑
|
||||||
|
| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 |
|
||||||
|
| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 |
|
||||||
|
| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 |
|
||||||
|
|
||||||
|
示范:
|
||||||
|
- `/protect add 末地保护区 players` 添加players玩家到末地保护区<br>
|
||||||
|
- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品<br>
|
||||||
|
- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。<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 违规
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行截图
|
||||||
|
|
||||||
|
主界面
|
||||||
|

|
||||||
|
|
||||||
|
邀请数据
|
||||||
|

|
||||||
|
|
||||||
|
积分商店
|
||||||
|

|
||||||
|
|
||||||
|
任务中心
|
||||||
|

|
||||||
|
|
||||||
|
垃圾箱
|
||||||
|

|
||||||
|
|
||||||
|
管理员面板
|
||||||
|

|
||||||
|
|
||||||
|
管理面板-用户管理
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
工单中心
|
||||||
|

|
||||||
|
|
||||||
|
工单管理
|
||||||
|

|
||||||
|
|
||||||
|
我的工单
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
提交工单
|
||||||
|

|
||||||
|
|
||||||
|
服务商列表
|
||||||
|

|
||||||
|
|
||||||
|
服务商抢单大厅
|
||||||
|

|
||||||
|
|
||||||
|
服务商-接单界面
|
||||||
|

|
||||||
|
|
||||||
|
管理员-工单详情页面
|
||||||
|

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

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

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

|
||||||
|
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>sdf1</groupId>
|
||||||
|
<artifactId>Sdf1_login</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>1.21.4-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.MilkBowl</groupId>
|
||||||
|
<artifactId>VaultAPI</artifactId>
|
||||||
|
<version>1.7.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.46.0.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>Sdf1_login</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +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 + "秒" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class CheckInManager {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
public CheckInManager(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 今日是否已签到 =====
|
||||||
|
public boolean isCheckedInToday(
|
||||||
|
String name) {
|
||||||
|
String last = (String) plugin.getDb()
|
||||||
|
.getField(name,
|
||||||
|
"last_checkin_date");
|
||||||
|
String today = new SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd")
|
||||||
|
.format(new Date());
|
||||||
|
return today.equals(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 签到 =====
|
||||||
|
// ===== 签到 =====
|
||||||
|
public String checkIn(Player p) {
|
||||||
|
String name = p.getName();
|
||||||
|
if (isCheckedInToday(name)) {
|
||||||
|
return plugin.getConfig2()
|
||||||
|
.msg("checkin_already");
|
||||||
|
}
|
||||||
|
|
||||||
|
Object streakObj = plugin.getDb()
|
||||||
|
.getField(name, "checkin_streak");
|
||||||
|
int streak = streakObj != null
|
||||||
|
? ((Number) streakObj).intValue()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
try {
|
||||||
|
java.util.Date lastD =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd")
|
||||||
|
.parse(lastDate);
|
||||||
|
java.util.Date todayD =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd")
|
||||||
|
.parse(today);
|
||||||
|
long diff = todayD.getTime()
|
||||||
|
- lastD.getTime();
|
||||||
|
if (diff > 86400000L * 2) {
|
||||||
|
streak = 0;
|
||||||
|
} else if (diff > 86400000L) {
|
||||||
|
streak++;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streak == 0) streak = 1;
|
||||||
|
else streak++;
|
||||||
|
|
||||||
|
double multi = 1.0;
|
||||||
|
if (streak >= 21) multi = 1.5;
|
||||||
|
else if (streak >= 14) multi = 1.3;
|
||||||
|
else if (streak >= 7) multi = 1.1;
|
||||||
|
|
||||||
|
int basePoints =
|
||||||
|
plugin.getConfig2().checkinPoints;
|
||||||
|
int points = (int) (basePoints * multi);
|
||||||
|
|
||||||
|
plugin.getDb().addPoints(name, points);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"checkin_streak", streak);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"last_checkin_date", today);
|
||||||
|
|
||||||
|
Object totalObj = plugin.getDb()
|
||||||
|
.getField(name, "total_checkin_days");
|
||||||
|
int total = totalObj != null
|
||||||
|
? ((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")
|
||||||
|
.replace("{points}",
|
||||||
|
String.valueOf(points))
|
||||||
|
.replace("{streak}",
|
||||||
|
String.valueOf(streak))
|
||||||
|
.replace("{multi}",
|
||||||
|
String.valueOf(multi))
|
||||||
|
+ " §e+" + bondReward + "债券";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 获取连续天数 =====
|
||||||
|
public int getStreak(String name) {
|
||||||
|
Object val = plugin.getDb()
|
||||||
|
.getField(name,
|
||||||
|
"checkin_streak");
|
||||||
|
if (val instanceof Number) {
|
||||||
|
return ((Number) val)
|
||||||
|
.intValue();
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
String name = p.getName();
|
||||||
|
String normalized =
|
||||||
|
normalizeDate(dateStr);
|
||||||
|
|
||||||
|
// 严格日期验证
|
||||||
|
java.text.SimpleDateFormat sdf =
|
||||||
|
new java.text.SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd");
|
||||||
|
sdf.setLenient(false);
|
||||||
|
java.util.Date target;
|
||||||
|
try {
|
||||||
|
target = sdf.parse(normalized);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return "§c无效日期: " + dateStr;
|
||||||
|
}
|
||||||
|
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今天不需要补签";
|
||||||
|
}
|
||||||
|
|
||||||
|
String threeDaysAgo = getPastDate(3);
|
||||||
|
if (normalized.compareTo(
|
||||||
|
threeDaysAgo) < 0) {
|
||||||
|
return "§c只能补签3天内"
|
||||||
|
+ "(最多到 " + threeDaysAgo + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object val = plugin.getDb()
|
||||||
|
.getField(name, "backcheck_dates");
|
||||||
|
String existing = val != null
|
||||||
|
? val.toString() : "";
|
||||||
|
boolean alreadyBacked = false;
|
||||||
|
if (!existing.isEmpty()) {
|
||||||
|
for (String d : existing.split(",")) {
|
||||||
|
if (normalizeDate(d.trim())
|
||||||
|
.equals(normalized)) {
|
||||||
|
alreadyBacked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alreadyBacked) {
|
||||||
|
return "§c该日期已补签: " + normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object normalCheck =
|
||||||
|
plugin.getDb().getField(
|
||||||
|
name, "last_checkin_date");
|
||||||
|
String lastCheckin = normalCheck != null
|
||||||
|
? normalCheck.toString() : "";
|
||||||
|
if (lastCheckin.equals(normalized)) {
|
||||||
|
return "§c该日期已签到: " + normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 补签消耗:3债券 + 10积分
|
||||||
|
// 积分不够时:用 9债券(3倍)代替 ★
|
||||||
|
int bondCost = 3;
|
||||||
|
int pointCost = 10;
|
||||||
|
int bondAltCost = 9; // 3倍债券替代
|
||||||
|
|
||||||
|
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 + "积分";
|
||||||
|
}
|
||||||
|
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消耗: " + costMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===== 执行补签 =====
|
||||||
|
// ===== 执行补签 =====
|
||||||
|
public void doBackCheckIn(
|
||||||
|
Player p, String dateStr) {
|
||||||
|
String name = p.getName();
|
||||||
|
Object val = plugin.getDb()
|
||||||
|
.getField(name, "backcheck_dates");
|
||||||
|
String existing = val != null
|
||||||
|
? val.toString() : "";
|
||||||
|
String updated = existing.isEmpty()
|
||||||
|
? dateStr
|
||||||
|
: existing + "," + dateStr;
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"backcheck_dates", updated);
|
||||||
|
int streak = getStreak(name);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"checkin_streak", streak + 1);
|
||||||
|
// ★ 补签不发放奖励,只记录
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 获取N天前日期 =====
|
||||||
|
private String getPastDate(
|
||||||
|
int daysAgo) {
|
||||||
|
Calendar cal =
|
||||||
|
Calendar.getInstance();
|
||||||
|
cal.add(Calendar.DAY_OF_MONTH,
|
||||||
|
-daysAgo);
|
||||||
|
return new SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd")
|
||||||
|
.format(cal.getTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class CommissionManager {
|
||||||
|
|
||||||
|
private final Main main;
|
||||||
|
private final Logger logger;
|
||||||
|
private final List<double[]> rules =
|
||||||
|
new ArrayList<>();
|
||||||
|
|
||||||
|
public CommissionManager(Main main) {
|
||||||
|
this.main = main;
|
||||||
|
this.logger = main.getLogger();
|
||||||
|
loadRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadRules() {
|
||||||
|
rules.clear();
|
||||||
|
File f = new File(
|
||||||
|
main.getDataFolder(),
|
||||||
|
"提成规则.txt");
|
||||||
|
if (!f.exists()) {
|
||||||
|
writeDefault(f);
|
||||||
|
}
|
||||||
|
try (BufferedReader br =
|
||||||
|
new BufferedReader(
|
||||||
|
new InputStreamReader(
|
||||||
|
new FileInputStream(f),
|
||||||
|
StandardCharsets
|
||||||
|
.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()
|
||||||
|
|| line.startsWith("#"))
|
||||||
|
continue;
|
||||||
|
// 格式: 邀请人数:提成百分比
|
||||||
|
// 如 "5:15" = 邀请5人提成15%
|
||||||
|
String[] parts =
|
||||||
|
line.split(":");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
try {
|
||||||
|
int count = Integer.parseInt(
|
||||||
|
parts[0].trim());
|
||||||
|
double rate = Double
|
||||||
|
.parseDouble(
|
||||||
|
parts[1]
|
||||||
|
.trim()) / 100.0;
|
||||||
|
rules.add(new double[]{
|
||||||
|
count, rate});
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按人数排序(从大到小)
|
||||||
|
rules.sort((a, b) ->
|
||||||
|
Double.compare(b[0], a[0]));
|
||||||
|
logger.info(
|
||||||
|
"[Commission] 加载 "
|
||||||
|
+ rules.size() + " 条提成规则");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warning(
|
||||||
|
"[Commission] 加载失败: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDefault(File f) {
|
||||||
|
try (PrintWriter pw = new PrintWriter(
|
||||||
|
new OutputStreamWriter(
|
||||||
|
new FileOutputStream(f),
|
||||||
|
StandardCharsets.UTF_8))) {
|
||||||
|
pw.println("# 邀请提成规则");
|
||||||
|
pw.println("# 格式: 邀请人数:提成百分比");
|
||||||
|
pw.println("# 邀请10人提成20%");
|
||||||
|
pw.println("10:20");
|
||||||
|
pw.println("# 邀请5人提成15%");
|
||||||
|
pw.println("5:15");
|
||||||
|
pw.println("# 邀请1人提成10%");
|
||||||
|
pw.println("1:10");
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邀请人的提成比例
|
||||||
|
* 根据已邀请人数匹配最高规则
|
||||||
|
*/
|
||||||
|
public double getCommissionRate(
|
||||||
|
String inviter) {
|
||||||
|
int inviteCount = getInviteCount(
|
||||||
|
inviter);
|
||||||
|
for (double[] rule : rules) {
|
||||||
|
if (inviteCount >= (int) rule[0]) {
|
||||||
|
return rule[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邀请人数
|
||||||
|
*/
|
||||||
|
public int getInviteCount(
|
||||||
|
String inviter) {
|
||||||
|
try {
|
||||||
|
Object val =
|
||||||
|
main.getDb().getField(
|
||||||
|
inviter,
|
||||||
|
"monthly_invite_count");
|
||||||
|
if (val instanceof Number) {
|
||||||
|
return ((Number) val).intValue();
|
||||||
|
}
|
||||||
|
Object total =
|
||||||
|
main.getDb().getField(
|
||||||
|
inviter,
|
||||||
|
"total_invite_count");
|
||||||
|
if (total instanceof Number) {
|
||||||
|
return ((Number) total).intValue();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发放提成
|
||||||
|
* 被邀请人获得奖励时,邀请人获得提成
|
||||||
|
*/
|
||||||
|
public void payCommission(
|
||||||
|
String invitee,
|
||||||
|
double originalAmount) {
|
||||||
|
// 查找邀请人
|
||||||
|
String inviter = getInviter(invitee);
|
||||||
|
if (inviter == null
|
||||||
|
|| inviter.isEmpty()) return;
|
||||||
|
|
||||||
|
double rate = getCommissionRate(
|
||||||
|
inviter);
|
||||||
|
if (rate <= 0) return;
|
||||||
|
|
||||||
|
double commission =
|
||||||
|
originalAmount * rate;
|
||||||
|
if (commission <= 0) return;
|
||||||
|
|
||||||
|
// 通过 Vault 发放
|
||||||
|
var rsp = main.getServer()
|
||||||
|
.getServicesManager()
|
||||||
|
.getRegistration(
|
||||||
|
net.milkbowl.vault
|
||||||
|
.economy.Economy.class);
|
||||||
|
if (rsp == null) return;
|
||||||
|
net.milkbowl.vault.economy.Economy econ =
|
||||||
|
rsp.getProvider();
|
||||||
|
if (econ == null) return;
|
||||||
|
|
||||||
|
econ.depositPlayer(inviter, commission);
|
||||||
|
|
||||||
|
// 通知邀请人
|
||||||
|
Player inviterP =
|
||||||
|
main.getServer()
|
||||||
|
.getPlayer(inviter);
|
||||||
|
if (inviterP != null
|
||||||
|
&& inviterP.isOnline()) {
|
||||||
|
inviterP.sendMessage(
|
||||||
|
"§a§l[邀请提成] §f"
|
||||||
|
+ invitee + " 完成任务,"
|
||||||
|
+ "你获得 §e"
|
||||||
|
+ String.format("%.2f",
|
||||||
|
commission)
|
||||||
|
+ " §f游戏币提成 ("
|
||||||
|
+ (int)(rate * 100) + "%)");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[Commission] 提成: "
|
||||||
|
+ inviter + " ← "
|
||||||
|
+ invitee + " | "
|
||||||
|
+ String.format("%.2f",
|
||||||
|
originalAmount)
|
||||||
|
+ " × "
|
||||||
|
+ (int)(rate * 100) + "% = "
|
||||||
|
+ String.format("%.2f",
|
||||||
|
commission));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邀请人
|
||||||
|
*/
|
||||||
|
private String getInviter(String player) {
|
||||||
|
try {
|
||||||
|
Object val =
|
||||||
|
main.getDb().getField(
|
||||||
|
player,
|
||||||
|
"invited_by");
|
||||||
|
if (val != null
|
||||||
|
&& !val.toString()
|
||||||
|
.isEmpty()) {
|
||||||
|
return val.toString();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断金额是否包含经济内容
|
||||||
|
*/
|
||||||
|
public boolean hasEconomyContent(
|
||||||
|
String text) {
|
||||||
|
String t = text.toLowerCase();
|
||||||
|
return t.contains("游戏币")
|
||||||
|
|| t.contains("金币")
|
||||||
|
|| t.contains("积分")
|
||||||
|
|| t.contains("块钱")
|
||||||
|
|| t.contains("元")
|
||||||
|
|| t.contains("$")
|
||||||
|
|| t.contains("经济")
|
||||||
|
|| t.contains("余额");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
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<>();
|
||||||
|
|
||||||
|
// ===== 公共字段 =====
|
||||||
|
public String adminTag = "admin";
|
||||||
|
public String adminPassword = "qweasd";
|
||||||
|
public int maxAccountsPerIP = 3;
|
||||||
|
public String approvalMode = "manual";
|
||||||
|
public int autoApproveDelayMinutes = 30;
|
||||||
|
public int loginTimeout = 60;
|
||||||
|
public boolean afkEnabled = false;
|
||||||
|
public int afkTimeout = 300;
|
||||||
|
public boolean smtpSsl = true;
|
||||||
|
public String smtpVerifyCode = "";
|
||||||
|
public long smtpVerifyExpire = 0;
|
||||||
|
public long smtpLastSendTime = 0;
|
||||||
|
public String smtpConfiguringPlayer = "";
|
||||||
|
public String broadcastServerUrl = "";
|
||||||
|
public int httpPort = 8080;
|
||||||
|
public String garbageTag = "admin";
|
||||||
|
|
||||||
|
public double checkinRewardFixed = 0;
|
||||||
|
public double checkinRewardMin = 0;
|
||||||
|
public double checkinRewardMax = 0;
|
||||||
|
public boolean checkinGivePoints = true;
|
||||||
|
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);
|
||||||
|
loadMap(file, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String msg(String key) {
|
||||||
|
String val = messages.getOrDefault(key, key);
|
||||||
|
val = val.replace("<b>", "&l");
|
||||||
|
val = val.replace("</b>", "&r");
|
||||||
|
val = val.replace("<br>", "\n");
|
||||||
|
val = val.replace("\\n", "\n");
|
||||||
|
val = val.replaceAll(
|
||||||
|
"&([0-9a-fk-orA-FK-OR])", "§$1");
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDefaultMessages(File f) {
|
||||||
|
List<String> L = new ArrayList<>();
|
||||||
|
L.add("# ===== Sdf1_login 消息配置 =====");
|
||||||
|
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("login_success=§a登录成功!");
|
||||||
|
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_same=§c新密码不能与旧密码相同");
|
||||||
|
L.add("checkin_already=§c您今日已签到");
|
||||||
|
L.add("checkin_success=§a签到成功!获得 {points} 积分");
|
||||||
|
L.add("chat_muted=§c你已被禁言");
|
||||||
|
L.add("未知参数=§c未知参数或权限不足");
|
||||||
|
writeLines(f, L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SMTP ====================
|
||||||
|
|
||||||
|
public void loadSmtp() {
|
||||||
|
smtpSettings.clear();
|
||||||
|
File file = new File(dataFolder, "SMTP设置.txt");
|
||||||
|
if (!file.exists()) createDefaultSmtp(file);
|
||||||
|
loadMap(file, smtpSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSmtp(String key) {
|
||||||
|
return smtpSettings.getOrDefault(key, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSmtp(String key, String value) {
|
||||||
|
smtpSettings.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveSmtp() {
|
||||||
|
File file = new File(dataFolder, "SMTP设置.txt");
|
||||||
|
List<String> L = new ArrayList<>();
|
||||||
|
L.add("# ===== SMTP 配置 =====");
|
||||||
|
for (Map.Entry<String, String> e
|
||||||
|
: smtpSettings.entrySet()) {
|
||||||
|
L.add(e.getKey() + "=" + e.getValue());
|
||||||
|
}
|
||||||
|
writeLines(file, L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDefaultSmtp(File f) {
|
||||||
|
List<String> L = new ArrayList<>();
|
||||||
|
L.add("# ===== SMTP 配置 =====");
|
||||||
|
L.add("smtp地址=smtp.example.com");
|
||||||
|
L.add("smtp端口=465");
|
||||||
|
L.add("smtp账号=");
|
||||||
|
L.add("smtp密码=");
|
||||||
|
L.add("发件人名称=Sdf1_login");
|
||||||
|
L.add("smtp加密=true");
|
||||||
|
writeLines(f, L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 插件设置 ====================
|
||||||
|
|
||||||
|
public void loadSettings() {
|
||||||
|
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", "");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 (!"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");
|
||||||
|
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("登录超时秒数=" + loginTimeout);
|
||||||
|
L.add("挂机踢出=" + afkEnabled);
|
||||||
|
L.add("挂机超时秒数=" + afkTimeout);
|
||||||
|
L.add("resource-pack-url=" + broadcastServerUrl);
|
||||||
|
L.add("http-port=" + httpPort);
|
||||||
|
L.add("垃圾站_启用=" + garbageEnabled);
|
||||||
|
L.add("垃圾站_清理间隔秒=" + garbageInterval);
|
||||||
|
L.add("垃圾站_保留轮数=" + garbageMaxRounds);
|
||||||
|
L.add("签到固定金额=" + checkinRewardFixed);
|
||||||
|
L.add("签到最小金额=" + checkinRewardMin);
|
||||||
|
L.add("签到最大金额=" + checkinRewardMax);
|
||||||
|
L.add("签到给积分=" + checkinGivePoints);
|
||||||
|
L.add("签到积分数量=" + checkinPoints);
|
||||||
|
L.add("签到债券最小=" + checkinBondMin);
|
||||||
|
L.add("签到债券最大=" + checkinBondMax);
|
||||||
|
L.add("补签积分倍率=" + backCheckPointMultiplier);
|
||||||
|
L.add("奖励发放方式=债券");
|
||||||
|
writeLines(file, L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDefaultSettings(File f) {
|
||||||
|
List<String> L = new ArrayList<>();
|
||||||
|
L.add("# ===== Sdf1_login 插件设置 =====");
|
||||||
|
L.add("管理标签=admin");
|
||||||
|
L.add("管理密码=qweasd");
|
||||||
|
L.add("每IP最大账号数=3");
|
||||||
|
L.add("审批模式=manual");
|
||||||
|
L.add("自动审批延迟分钟=30");
|
||||||
|
L.add("登录超时秒数=60");
|
||||||
|
L.add("挂机踢出=false");
|
||||||
|
L.add("挂机超时秒数=300");
|
||||||
|
L.add("resource-pack-url=");
|
||||||
|
L.add("http-port=8080");
|
||||||
|
L.add("垃圾站_启用=true");
|
||||||
|
L.add("垃圾站_清理间隔秒=300");
|
||||||
|
L.add("垃圾站_保留轮数=1");
|
||||||
|
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");
|
||||||
|
if (!file.exists()) {
|
||||||
|
try {
|
||||||
|
file.createNewFile();
|
||||||
|
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))) {
|
||||||
|
String line;
|
||||||
|
while ((line = r.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (!line.isEmpty() && !line.startsWith("#"))
|
||||||
|
list.add(line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 其他 ====================
|
||||||
|
|
||||||
|
public String generateSmtpVerifyCode() {
|
||||||
|
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())));
|
||||||
|
smtpVerifyCode = sb.toString();
|
||||||
|
smtpVerifyExpire = System.currentTimeMillis() + 300000L;
|
||||||
|
return smtpVerifyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkSmtpVerifyCode(String code) {
|
||||||
|
if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; }
|
||||||
|
return smtpVerifyCode.equalsIgnoreCase(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; }
|
||||||
|
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))) {
|
||||||
|
String line;
|
||||||
|
while ((line = r.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
} 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 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; } }
|
||||||
|
}
|
||||||
@@ -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,10 +60,12 @@ public class DatabaseManager {
|
|||||||
+ "tasks_completed TEXT DEFAULT '',"
|
+ "tasks_completed TEXT DEFAULT '',"
|
||||||
+ "premium_uuid TEXT DEFAULT '',"
|
+ "premium_uuid TEXT DEFAULT '',"
|
||||||
+ "register_ip 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",
|
safeAdd(st, "tasks_completed",
|
||||||
"TEXT DEFAULT ''");
|
"TEXT DEFAULT ''");
|
||||||
safeAdd(st, "temp_password",
|
safeAdd(st, "temp_password",
|
||||||
@@ -80,39 +82,76 @@ public class DatabaseManager {
|
|||||||
"TEXT DEFAULT ''");
|
"TEXT DEFAULT ''");
|
||||||
safeAdd(st, "register_type",
|
safeAdd(st, "register_type",
|
||||||
"TEXT DEFAULT 'manual'");
|
"TEXT DEFAULT 'manual'");
|
||||||
// 在 init 方法的 try 中添加
|
|
||||||
// 如果表存在但没有save_time列,添加它
|
// ===== 背包备份表:安全迁移 =====
|
||||||
|
// 检查旧表是否存在(用 inventory_data 列判断)
|
||||||
|
boolean hasOldTable = false;
|
||||||
try {
|
try {
|
||||||
|
ResultSet rs = st.executeQuery(
|
||||||
|
"SELECT inventory_data "
|
||||||
|
+ "FROM inventory_backups "
|
||||||
|
+ "LIMIT 1");
|
||||||
|
hasOldTable = true;
|
||||||
|
rs.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
hasOldTable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOldTable) {
|
||||||
|
// 旧表存在,迁移数据到新表
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "inventory_backups_new ("
|
||||||
|
+ "id INTEGER PRIMARY KEY "
|
||||||
|
+ "AUTOINCREMENT,"
|
||||||
|
+ "player_name TEXT NOT NULL,"
|
||||||
|
+ "contents_b64 TEXT DEFAULT '',"
|
||||||
|
+ "armor_b64 TEXT DEFAULT '',"
|
||||||
|
+ "extra_b64 TEXT DEFAULT '',"
|
||||||
|
+ "level INTEGER DEFAULT 0,"
|
||||||
|
+ "experience REAL DEFAULT 0,"
|
||||||
|
+ "save_time INTEGER DEFAULT 0"
|
||||||
|
+ ")");
|
||||||
|
// 迁移旧数据
|
||||||
|
try {
|
||||||
|
st.execute("INSERT INTO "
|
||||||
|
+ "inventory_backups_new "
|
||||||
|
+ "(player_name, "
|
||||||
|
+ "contents_b64, "
|
||||||
|
+ "armor_b64, "
|
||||||
|
+ "extra_b64, "
|
||||||
|
+ "level, "
|
||||||
|
+ "experience) "
|
||||||
|
+ "SELECT player_name, "
|
||||||
|
+ "inventory_data, "
|
||||||
|
+ "armor_data, "
|
||||||
|
+ "extra_data, "
|
||||||
|
+ "player_level, "
|
||||||
|
+ "player_exp "
|
||||||
|
+ "FROM inventory_backups");
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
}
|
||||||
|
st.execute("DROP TABLE "
|
||||||
|
+ "inventory_backups");
|
||||||
st.execute("ALTER TABLE "
|
st.execute("ALTER TABLE "
|
||||||
+ "inventory_backups "
|
+ "inventory_backups_new "
|
||||||
+ "ADD COLUMN save_time "
|
+ "RENAME TO "
|
||||||
+ "INTEGER DEFAULT 0");
|
+ "inventory_backups");
|
||||||
} catch (SQLException ignored) {
|
} else {
|
||||||
// 列已存在
|
// 无旧表或已迁移,直接创建
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "inventory_backups ("
|
||||||
|
+ "id INTEGER PRIMARY KEY "
|
||||||
|
+ "AUTOINCREMENT,"
|
||||||
|
+ "player_name TEXT NOT NULL,"
|
||||||
|
+ "contents_b64 TEXT DEFAULT '',"
|
||||||
|
+ "armor_b64 TEXT DEFAULT '',"
|
||||||
|
+ "extra_b64 TEXT DEFAULT '',"
|
||||||
|
+ "level INTEGER DEFAULT 0,"
|
||||||
|
+ "experience REAL DEFAULT 0,"
|
||||||
|
+ "save_time INTEGER DEFAULT 0"
|
||||||
|
+ ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 背包备份表
|
|
||||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
|
||||||
+ "inventory_backups ("
|
|
||||||
+ "player_name TEXT PRIMARY KEY,"
|
|
||||||
+ "inventory_data TEXT DEFAULT '',"
|
|
||||||
+ "armor_data TEXT DEFAULT '',"
|
|
||||||
+ "extra_data TEXT DEFAULT '',"
|
|
||||||
+ "player_level INTEGER DEFAULT 0,"
|
|
||||||
+ "player_exp REAL DEFAULT 0.0)");
|
|
||||||
// 确保 inventory_backups 表结构正确
|
|
||||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
|
||||||
+ "inventory_backups ("
|
|
||||||
+ "id INTEGER PRIMARY KEY "
|
|
||||||
+ "AUTOINCREMENT,"
|
|
||||||
+ "player_name TEXT NOT NULL,"
|
|
||||||
+ "contents_b64 TEXT,"
|
|
||||||
+ "armor_b64 TEXT,"
|
|
||||||
+ "extra_b64 TEXT,"
|
|
||||||
+ "level INTEGER DEFAULT 0,"
|
|
||||||
+ "experience REAL DEFAULT 0,"
|
|
||||||
+ "save_time INTEGER DEFAULT 0"
|
|
||||||
+ ")");
|
|
||||||
// 安全报警表
|
// 安全报警表
|
||||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
+ "security_alerts ("
|
+ "security_alerts ("
|
||||||
@@ -123,6 +162,40 @@ public class DatabaseManager {
|
|||||||
+ "alert_date TEXT NOT NULL,"
|
+ "alert_date TEXT NOT NULL,"
|
||||||
+ "notified INTEGER DEFAULT 0,"
|
+ "notified INTEGER DEFAULT 0,"
|
||||||
+ "notified_time 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 "
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
@@ -144,6 +217,7 @@ public class DatabaseManager {
|
|||||||
+ "admin_confirmed INTEGER DEFAULT 0,"
|
+ "admin_confirmed INTEGER DEFAULT 0,"
|
||||||
+ "reject_reason TEXT DEFAULT ''"
|
+ "reject_reason TEXT DEFAULT ''"
|
||||||
+ ")");
|
+ ")");
|
||||||
|
|
||||||
// 工单回复表
|
// 工单回复表
|
||||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
+ "ticket_replies ("
|
+ "ticket_replies ("
|
||||||
@@ -163,58 +237,18 @@ public class DatabaseManager {
|
|||||||
+ "active INTEGER DEFAULT 1,"
|
+ "active INTEGER DEFAULT 1,"
|
||||||
+ "join_time INTEGER DEFAULT 0"
|
+ "join_time INTEGER DEFAULT 0"
|
||||||
+ ")");
|
+ ")");
|
||||||
// 安全迁移 inventory_backups 表
|
// 垃圾回收站物品表
|
||||||
// 不删除旧数据,只补缺失列
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
try {
|
+ "items ("
|
||||||
st.execute("ALTER TABLE "
|
+ "id INTEGER PRIMARY KEY "
|
||||||
+ "inventory_backups "
|
+ "AUTOINCREMENT,"
|
||||||
+ "ADD COLUMN "
|
+ "player_name TEXT NOT NULL,"
|
||||||
+ "save_time INTEGER "
|
+ "item_data TEXT DEFAULT '',"
|
||||||
+ "DEFAULT 0");
|
+ "amount INTEGER DEFAULT 1,"
|
||||||
} catch (SQLException ignored) {
|
+ "create_time INTEGER DEFAULT 0"
|
||||||
}
|
+ ")");
|
||||||
try {
|
safeAdd(st, "menu_snowball",
|
||||||
st.execute("ALTER TABLE "
|
"INTEGER DEFAULT 1");
|
||||||
+ "inventory_backups "
|
|
||||||
+ "ADD COLUMN "
|
|
||||||
+ "level INTEGER "
|
|
||||||
+ "DEFAULT 0");
|
|
||||||
} catch (SQLException ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
st.execute("ALTER TABLE "
|
|
||||||
+ "inventory_backups "
|
|
||||||
+ "ADD COLUMN "
|
|
||||||
+ "experience REAL "
|
|
||||||
+ "DEFAULT 0");
|
|
||||||
} catch (SQLException ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
st.execute("ALTER TABLE "
|
|
||||||
+ "inventory_backups "
|
|
||||||
+ "ADD COLUMN "
|
|
||||||
+ "armor_b64 TEXT "
|
|
||||||
+ "DEFAULT ''");
|
|
||||||
} catch (SQLException ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
st.execute("ALTER TABLE "
|
|
||||||
+ "inventory_backups "
|
|
||||||
+ "ADD COLUMN "
|
|
||||||
+ "extra_b64 TEXT "
|
|
||||||
+ "DEFAULT ''");
|
|
||||||
} catch (SQLException ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
st.execute("ALTER TABLE "
|
|
||||||
+ "inventory_backups "
|
|
||||||
+ "ADD COLUMN "
|
|
||||||
+ "contents_b64 TEXT "
|
|
||||||
+ "DEFAULT ''");
|
|
||||||
} catch (SQLException ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
st.close();
|
st.close();
|
||||||
logger.info("[Sdf1_login] 数据库初始化完成");
|
logger.info("[Sdf1_login] 数据库初始化完成");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -224,6 +258,7 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void safeAdd(Statement st,
|
private void safeAdd(Statement st,
|
||||||
String col, String def) {
|
String col, String def) {
|
||||||
try {
|
try {
|
||||||
@@ -240,6 +275,76 @@ public class DatabaseManager {
|
|||||||
} catch (Exception ignored) {
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 用户操作 ====================
|
// ==================== 用户操作 ====================
|
||||||
|
|
||||||
@@ -748,6 +853,252 @@ public class DatabaseManager {
|
|||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==================== 安全报警 ====================
|
// ==================== 安全报警 ====================
|
||||||
@@ -774,6 +1125,7 @@ public class DatabaseManager {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Map<String, Object>>
|
public List<Map<String, Object>>
|
||||||
@@ -0,0 +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请尽快登录并修改密码!");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public class GiftManager {
|
|||||||
public GiftManager(Main plugin) {
|
public GiftManager(Main plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.giftFolder = new File(
|
this.giftFolder = new File(
|
||||||
plugin.getDataFolder(), "新人礼包");
|
plugin.getDataFolder(), "任务面板");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadStages() {
|
public void loadStages() {
|
||||||
@@ -155,56 +155,19 @@ public class GiftManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean canClaim(Player p, int stage) {
|
public boolean canClaim(Player p, int stage) {
|
||||||
Map<String, Object> user = plugin.getDb()
|
String name = p.getName();
|
||||||
.getUser(p.getName());
|
int curStage = ((Number) plugin.getDb()
|
||||||
if (user.isEmpty()) return false;
|
.getField(name, "gift_stage"))
|
||||||
int curStage = ((Number) user.getOrDefault(
|
.intValue();
|
||||||
"gift_stage", 0)).intValue();
|
String claimed = (String) plugin.getDb()
|
||||||
if (stage > curStage + 1) return false;
|
.getField(name, "gift_claimed");
|
||||||
if (stage < 1 || stage > stages.size())
|
if (claimed == null) claimed = "";
|
||||||
return false;
|
return curStage >= stage
|
||||||
String claimed = (String) user.getOrDefault(
|
&& !claimed.contains(
|
||||||
"gift_claimed", "");
|
String.valueOf(stage));
|
||||||
if (claimed.contains("[" + stage + "]"))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// debug 绕过:检查 tasks_completed
|
|
||||||
String completed = (String) user.getOrDefault(
|
|
||||||
"tasks_completed", "");
|
|
||||||
if (completed.contains("[" + stage + "]"))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// 正常条件检查
|
|
||||||
long dbTime = ((Number) user.getOrDefault(
|
|
||||||
"total_online_time", 0L)).longValue();
|
|
||||||
long sessionTime = 0;
|
|
||||||
Object lastObj = plugin.getDb().getField(
|
|
||||||
p.getName(), "last_online_check");
|
|
||||||
if (lastObj != null && plugin.getLoggedIn()
|
|
||||||
.contains(p.getName())) {
|
|
||||||
long last = ((Number) lastObj).longValue();
|
|
||||||
if (last > 0) {
|
|
||||||
sessionTime =
|
|
||||||
System.currentTimeMillis() - last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
long onlineTime = dbTime + sessionTime;
|
|
||||||
int broken = ((Number) user.getOrDefault(
|
|
||||||
"blocks_broken", 0)).intValue();
|
|
||||||
int placed = ((Number) user.getOrDefault(
|
|
||||||
"blocks_placed", 0)).intValue();
|
|
||||||
|
|
||||||
switch (stage) {
|
|
||||||
case 1: return true;
|
|
||||||
case 2: return onlineTime >= 3600000;
|
|
||||||
case 3: return broken >= 100
|
|
||||||
&& onlineTime >= 86400000L;
|
|
||||||
case 4: return (broken + placed) >= 300
|
|
||||||
&& onlineTime >= 604800000L;
|
|
||||||
default: return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void claimReward(Player p, int stage) {
|
public void claimReward(Player p, int stage) {
|
||||||
if (!canClaim(p, stage)) return;
|
if (!canClaim(p, stage)) return;
|
||||||
Map<String, Object> user = plugin.getDb()
|
Map<String, Object> user = plugin.getDb()
|
||||||
@@ -0,0 +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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,91 +1,96 @@
|
|||||||
package Sdf1_login;
|
package Sdf1_login;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class InviteManager {
|
public class InviteManager {
|
||||||
|
|
||||||
private final Main plugin;
|
private final Main plugin;
|
||||||
|
|
||||||
public InviteManager(Main plugin) {
|
public InviteManager(Main plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generateCode(Player p) {
|
public String generateCode(Player p) {
|
||||||
String code = PasswordUtils.generateInviteCode();
|
String code = PasswordUtils.generateInviteCode();
|
||||||
plugin.getDb().setField(
|
plugin.getDb().setField(
|
||||||
p.getName(), "invite_code", code);
|
p.getName(), "invite_code", code);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onInviteeCheckIn(String inviteeName) {
|
public void onInviteeCheckIn(String inviteeName) {
|
||||||
String invitedBy = (String) plugin.getDb()
|
String invitedBy = (String) plugin.getDb()
|
||||||
.getField(inviteeName, "invited_by");
|
.getField(inviteeName, "invited_by");
|
||||||
if (invitedBy == null || invitedBy.isEmpty()) return;
|
if (invitedBy == null || invitedBy.isEmpty())
|
||||||
|
return;
|
||||||
String month = new SimpleDateFormat("yyyy-MM")
|
|
||||||
.format(new Date());
|
String month = new SimpleDateFormat("yyyy-MM")
|
||||||
String lastMonth = (String) plugin.getDb()
|
.format(new Date());
|
||||||
.getField(invitedBy, "invite_month");
|
String lastMonth = (String) plugin.getDb()
|
||||||
|
.getField(invitedBy, "invite_month");
|
||||||
if (!month.equals(lastMonth)) {
|
|
||||||
plugin.getDb().setField(
|
if (!month.equals(lastMonth)) {
|
||||||
invitedBy, "invite_month", month);
|
plugin.getDb().setField(
|
||||||
plugin.getDb().setField(
|
invitedBy, "invite_month", month);
|
||||||
invitedBy, "monthly_invite_count", 0);
|
plugin.getDb().setField(
|
||||||
}
|
invitedBy, "monthly_invite_count", 0);
|
||||||
|
}
|
||||||
int count = ((Number) plugin.getDb()
|
|
||||||
.getField(invitedBy,
|
int count = ((Number) plugin.getDb()
|
||||||
"monthly_invite_count")).intValue();
|
.getField(invitedBy,
|
||||||
plugin.getDb().setField(
|
"monthly_invite_count"))
|
||||||
invitedBy, "monthly_invite_count", count + 1);
|
.intValue();
|
||||||
|
plugin.getDb().setField(
|
||||||
double rate = 0.10;
|
invitedBy, "monthly_invite_count",
|
||||||
if (count + 1 > 15) {
|
count + 1);
|
||||||
rate = 0.60;
|
|
||||||
} else if (count + 1 > 10) {
|
double rate = 0.10;
|
||||||
rate = 0.35;
|
if (count + 1 > 15) {
|
||||||
} else if (count + 1 > 5) {
|
rate = 0.60;
|
||||||
rate = 0.15;
|
} else if (count + 1 > 10) {
|
||||||
}
|
rate = 0.35;
|
||||||
|
} else if (count + 1 > 5) {
|
||||||
int bonus = (int) (1 * rate);
|
rate = 0.15;
|
||||||
if (bonus > 0) {
|
}
|
||||||
plugin.getDb().addPoints(invitedBy, bonus);
|
|
||||||
Player inviterPlayer = Bukkit
|
int bonus = (int) (1 * rate);
|
||||||
.getPlayerExact(invitedBy);
|
if (bonus > 0) {
|
||||||
if (inviterPlayer != null
|
plugin.getDb().addPoints(invitedBy, bonus);
|
||||||
&& inviterPlayer.isOnline()) {
|
Player inviterPlayer = Bukkit
|
||||||
inviterPlayer.sendMessage(
|
.getPlayerExact(invitedBy);
|
||||||
plugin.getConfig2().msg(
|
if (inviterPlayer != null
|
||||||
"invite_referral_bonus",
|
&& inviterPlayer.isOnline()) {
|
||||||
"points",
|
inviterPlayer.sendMessage(
|
||||||
String.valueOf(bonus)));
|
plugin.getConfig2()
|
||||||
}
|
.msg("invite_referral_bonus")
|
||||||
}
|
.replace("{points}",
|
||||||
}
|
String.valueOf(bonus)));
|
||||||
|
}
|
||||||
public List<Map<String, Object>> getAllInviteData() {
|
}
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
}
|
||||||
for (Map<String, Object> user
|
|
||||||
: plugin.getDb().getAllUsers()) {
|
public List<Map<String, Object>> getAllInviteData() {
|
||||||
String code = (String) user.getOrDefault(
|
List<Map<String, Object>> result =
|
||||||
"invite_code", "");
|
new ArrayList<>();
|
||||||
if (!code.isEmpty()) {
|
for (Map<String, Object> user
|
||||||
Map<String, Object> row = new LinkedHashMap<>();
|
: plugin.getDb().getAllUsers()) {
|
||||||
row.put("player",
|
String code = (String) user.getOrDefault(
|
||||||
user.get("player_name"));
|
"invite_code", "");
|
||||||
row.put("code", code);
|
if (!code.isEmpty()) {
|
||||||
row.put("monthly",
|
Map<String, Object> row =
|
||||||
user.getOrDefault(
|
new LinkedHashMap<>();
|
||||||
"monthly_invite_count", 0));
|
row.put("player",
|
||||||
result.add(row);
|
user.get("player_name"));
|
||||||
}
|
row.put("code", code);
|
||||||
}
|
row.put("monthly",
|
||||||
return result;
|
user.getOrDefault(
|
||||||
}
|
"monthly_invite_count", 0));
|
||||||
}
|
result.add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
|
import org.bukkit.event.player.PlayerToggleSneakEvent;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class IronBlockElevator implements Listener {
|
||||||
|
|
||||||
|
private static final int MAX_RANGE = 64;
|
||||||
|
private static final int MIN_RANGE = 5; // 屏蔽±5格以内的铁块
|
||||||
|
private final Set<UUID> cooldown = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定位置脚下是否是铁块
|
||||||
|
*/
|
||||||
|
private boolean isIronBlockBelow(Location loc) {
|
||||||
|
Block block = loc.getWorld().getBlockAt(
|
||||||
|
loc.getBlockX(), loc.getBlockY() - 1,
|
||||||
|
loc.getBlockZ());
|
||||||
|
return block.getType() == Material.IRON_BLOCK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从起始位置向上或向下搜索铁块并传送
|
||||||
|
* @param dir 1=向上 -1=向下
|
||||||
|
*/
|
||||||
|
private boolean tryTeleport(Player p, int dir) {
|
||||||
|
Location loc = p.getLocation();
|
||||||
|
int baseY = loc.getBlockY();
|
||||||
|
|
||||||
|
for (int i = MIN_RANGE; i < MAX_RANGE; i++) {
|
||||||
|
int y = baseY + i * dir;
|
||||||
|
Block block = loc.getWorld().getBlockAt(
|
||||||
|
loc.getBlockX(), y, loc.getBlockZ());
|
||||||
|
if (block.getType() == Material.IRON_BLOCK) {
|
||||||
|
cooldown.add(p.getUniqueId());
|
||||||
|
Location dest = new Location(
|
||||||
|
loc.getWorld(),
|
||||||
|
loc.getX(),
|
||||||
|
block.getY() + 1,
|
||||||
|
loc.getZ());
|
||||||
|
p.teleport(dest);
|
||||||
|
org.bukkit.Bukkit.getScheduler()
|
||||||
|
.runTaskLater(
|
||||||
|
org.bukkit.Bukkit.getPluginManager()
|
||||||
|
.getPlugin("Sdf1_login"),
|
||||||
|
() -> cooldown.remove(
|
||||||
|
p.getUniqueId()),
|
||||||
|
15L);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 下蹲 → 向下传送 =====
|
||||||
|
@EventHandler
|
||||||
|
public void onSneak(PlayerToggleSneakEvent e) {
|
||||||
|
if (!e.isSneaking()) return;
|
||||||
|
Player p = e.getPlayer();
|
||||||
|
if (cooldown.contains(p.getUniqueId())) return;
|
||||||
|
// 必须站在铁块上
|
||||||
|
if (!isIronBlockBelow(p.getLocation())) return;
|
||||||
|
tryTeleport(p, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 跳跃 → 向上传送 =====
|
||||||
|
@EventHandler
|
||||||
|
public void onMove(PlayerMoveEvent e) {
|
||||||
|
Player p = e.getPlayer();
|
||||||
|
if (cooldown.contains(p.getUniqueId())) return;
|
||||||
|
if (p.isSneaking()) return;
|
||||||
|
|
||||||
|
Location from = e.getFrom();
|
||||||
|
Location to = e.getTo();
|
||||||
|
if (to == null) return;
|
||||||
|
if (to.getY() <= from.getY()) return;
|
||||||
|
if (p.getVelocity().getY() <= 0) return;
|
||||||
|
|
||||||
|
// 关键:检查跳跃前的位置(from)脚下是否是铁块
|
||||||
|
// 跳跃后玩家已离开铁块上方,检查 to 会失败
|
||||||
|
if (!isIronBlockBelow(from)) return;
|
||||||
|
|
||||||
|
tryTeleport(p, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
public class LoginManager {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
public LoginManager(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleRegister(Player p,
|
||||||
|
String[] args) {
|
||||||
|
String name = p.getName();
|
||||||
|
|
||||||
|
// 已注册玩家不能再注册
|
||||||
|
if (plugin.getDb().userExists(name)) {
|
||||||
|
p.sendMessage(plugin.getConfig2()
|
||||||
|
.msg("already_registered"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP注册限制检查
|
||||||
|
String ip = plugin.getPlayerIP(p);
|
||||||
|
if (ip != null && plugin.getConfig2()
|
||||||
|
.maxAccountsPerIP > 0) {
|
||||||
|
if (!plugin.getIPGroup()
|
||||||
|
.canRegister(ip)) {
|
||||||
|
int count = plugin.getIPGroup()
|
||||||
|
.getAccountCount(ip);
|
||||||
|
int max = plugin.getConfig2()
|
||||||
|
.maxAccountsPerIP;
|
||||||
|
p.sendMessage("§c该IP注册已达上限("
|
||||||
|
+ count + "/" + max + ")");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length < 1) {
|
||||||
|
p.sendMessage("§c用法: /reg <密码>");
|
||||||
|
p.sendMessage("§7密码要求: 6位以上,"
|
||||||
|
+ "包含大小写字母和数字");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String pwd = args[0];
|
||||||
|
if (pwd.length() < 6) {
|
||||||
|
p.sendMessage("§c密码长度不足6位");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!pwd.matches(".*[a-z].*")
|
||||||
|
|| !pwd.matches(".*[A-Z].*")
|
||||||
|
|| !pwd.matches(".*[0-9].*")) {
|
||||||
|
p.sendMessage("§c密码必须包含大写字母、"
|
||||||
|
+ "小写字母和数字");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String salt =
|
||||||
|
PasswordUtils.generateSalt();
|
||||||
|
String hash =
|
||||||
|
PasswordUtils.hash(pwd, salt);
|
||||||
|
plugin.getDb().createUser(
|
||||||
|
name, hash, salt);
|
||||||
|
plugin.getLoggedIn().add(name);
|
||||||
|
p.setAllowFlight(false);
|
||||||
|
p.setFlying(false);
|
||||||
|
plugin.getDb().setLoggedIn(name, true);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"last_login_time",
|
||||||
|
System.currentTimeMillis());
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"last_online_check",
|
||||||
|
System.currentTimeMillis());
|
||||||
|
plugin.getDb().recordIP(name, ip);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"register_time",
|
||||||
|
System.currentTimeMillis());
|
||||||
|
plugin.restoreInventory(p);
|
||||||
|
plugin.recordIPLogin(p);
|
||||||
|
plugin.giveMenuSnowball(p);
|
||||||
|
|
||||||
|
p.sendMessage(plugin.getConfig2()
|
||||||
|
.msg("reg_success"));
|
||||||
|
playRegisterSound(p);
|
||||||
|
|
||||||
|
try {
|
||||||
|
org.bukkit.plugin.Plugin cy =
|
||||||
|
Bukkit.getPluginManager()
|
||||||
|
.getPlugin("CY_beibao");
|
||||||
|
if (cy != null && cy.isEnabled()) {
|
||||||
|
cy.getClass().getMethod(
|
||||||
|
"onSdf1Activation",
|
||||||
|
String.class,
|
||||||
|
int.class, int.class)
|
||||||
|
.invoke(cy, name, 9, 0);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playRegisterSound(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
.msg("already_logged_in"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!plugin.getDb().userExists(name)) {
|
||||||
|
p.sendMessage(plugin.getConfig2()
|
||||||
|
.msg("not_registered"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length < 1) {
|
||||||
|
p.sendMessage("§c用法: /login <密码>");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String pwd = args[0];
|
||||||
|
String salt = (String) plugin.getDb()
|
||||||
|
.getField(name, "password_salt");
|
||||||
|
if (salt == null) {
|
||||||
|
p.sendMessage(
|
||||||
|
"§c数据异常,请联系管理员");
|
||||||
|
plugin.getLogger().severe(
|
||||||
|
"[Sdf1_login] 玩家 " + name
|
||||||
|
+ " 盐值为null");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String hash =
|
||||||
|
PasswordUtils.hash(pwd, salt);
|
||||||
|
|
||||||
|
boolean matchMain =
|
||||||
|
plugin.getDb().checkPassword(
|
||||||
|
name, hash);
|
||||||
|
if (matchMain) {
|
||||||
|
plugin.getLoggedIn().add(name);
|
||||||
|
plugin.getDb().setLoggedIn(
|
||||||
|
name, true);
|
||||||
|
plugin.getDb().setLoggedIn(
|
||||||
|
name, true);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"last_login_time",
|
||||||
|
System.currentTimeMillis());
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"last_online_check",
|
||||||
|
System.currentTimeMillis());
|
||||||
|
// 只在有有效备份时恢复
|
||||||
|
plugin.restoreInventory(p);
|
||||||
|
plugin.recordIPLogin(p);
|
||||||
|
plugin.giveMenuSnowball(p);
|
||||||
|
p.sendMessage(plugin.getConfig2()
|
||||||
|
.msg("login_success"));
|
||||||
|
activateCY(p, name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchTemp =
|
||||||
|
plugin.getDb()
|
||||||
|
.checkPasswordOrTemp(
|
||||||
|
name, hash);
|
||||||
|
if (matchTemp) {
|
||||||
|
plugin.getLoggedIn().add(name);
|
||||||
|
plugin.getDb().setLoggedIn(name, true);
|
||||||
|
p.setAllowFlight(false);
|
||||||
|
p.setFlying(false);
|
||||||
|
plugin.getDb().setField(name, "last_login_time",
|
||||||
|
System.currentTimeMillis());
|
||||||
|
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");
|
||||||
|
activateCY(p, name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
p.sendMessage(plugin.getConfig2()
|
||||||
|
.msg("login_failed"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe(
|
||||||
|
"[Sdf1_login] handleLogin异常: "
|
||||||
|
+ e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
p.sendMessage(
|
||||||
|
"§c登录过程出错,请联系管理员");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void handleReset(Player p) {
|
||||||
|
String name = p.getName();
|
||||||
|
String emailAddr = (String)
|
||||||
|
plugin.getDb().getField(
|
||||||
|
name, "email");
|
||||||
|
if (emailAddr == null
|
||||||
|
|| emailAddr.isEmpty()) {
|
||||||
|
p.sendMessage(
|
||||||
|
"§c未设置邮箱,无法重置密码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String tempPwd = PasswordUtils
|
||||||
|
.generateTempPassword();
|
||||||
|
String salt = (String)
|
||||||
|
plugin.getDb().getField(
|
||||||
|
name, "password_salt");
|
||||||
|
String hash = PasswordUtils.hash(
|
||||||
|
tempPwd, salt);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"temp_password", hash);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"temp_pw_expire",
|
||||||
|
System.currentTimeMillis()
|
||||||
|
+ 300000L);
|
||||||
|
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"temp_pw_used", 0);
|
||||||
|
p.sendMessage("§e正在发送临时密码...");
|
||||||
|
final String to = emailAddr;
|
||||||
|
final String fName = name;
|
||||||
|
final String pwd = tempPwd;
|
||||||
|
final Main self = plugin;
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTaskAsynchronously(self,
|
||||||
|
() -> {
|
||||||
|
final boolean[] sent = {false};
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
|
sent[0] = self.getEmail()
|
||||||
|
.sendTempPassword(
|
||||||
|
to, fName, pwd);
|
||||||
|
}, "Sdf1_login-reset");
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
try {
|
||||||
|
t.join(15000);
|
||||||
|
} catch (
|
||||||
|
InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
if (t.isAlive()) {
|
||||||
|
t.interrupt();
|
||||||
|
Bukkit.getScheduler().runTask(
|
||||||
|
self, () ->
|
||||||
|
p.sendMessage(
|
||||||
|
"§c邮件发送超时"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bukkit.getScheduler().runTask(
|
||||||
|
self, () -> {
|
||||||
|
if (sent[0]) {
|
||||||
|
p.sendMessage(
|
||||||
|
"§a临时密码已发送到 "
|
||||||
|
+ to);
|
||||||
|
} else {
|
||||||
|
p.sendMessage(
|
||||||
|
"§c邮件发送失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void activateCY(Player p,
|
||||||
|
String name) {
|
||||||
|
try {
|
||||||
|
org.bukkit.plugin.Plugin cy =
|
||||||
|
Bukkit.getPluginManager()
|
||||||
|
.getPlugin("CY_beibao");
|
||||||
|
if (cy != null && cy.isEnabled()) {
|
||||||
|
cy.getClass().getMethod(
|
||||||
|
"onSdf1Activation",
|
||||||
|
String.class,
|
||||||
|
int.class, int.class)
|
||||||
|
.invoke(cy, name, 0, 0);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.*;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.*;
|
||||||
|
import org.bukkit.event.inventory.*;
|
||||||
|
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
public class MenuManager implements Listener {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
private final List<MenuItem> items =
|
||||||
|
new ArrayList<>();
|
||||||
|
|
||||||
|
private final Set<String> chatWait =
|
||||||
|
ConcurrentHashMap.newKeySet();
|
||||||
|
private final Map<String, String> chatField =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Integer> chatIdx =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
// ===== 菜单项 =====
|
||||||
|
public static class MenuItem {
|
||||||
|
public String title = "新菜单项";
|
||||||
|
public String command = "";
|
||||||
|
public String permType = "玩家";
|
||||||
|
public Integer slot = null;
|
||||||
|
public ItemStack icon;
|
||||||
|
public MenuItem() {
|
||||||
|
icon = randomIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MenuItem(String t,
|
||||||
|
String c, String p) {
|
||||||
|
title = t;
|
||||||
|
command = c;
|
||||||
|
permType = p;
|
||||||
|
icon = randomIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ItemStack randomIcon() {
|
||||||
|
Material[] pool = {
|
||||||
|
Material.COMPASS,
|
||||||
|
Material.ENDER_PEARL,
|
||||||
|
Material.DIAMOND,
|
||||||
|
Material.GOLD_INGOT,
|
||||||
|
Material.EMERALD,
|
||||||
|
Material.NETHER_STAR,
|
||||||
|
Material.BEACON,
|
||||||
|
Material.ENCHANTING_TABLE,
|
||||||
|
Material.CHEST,
|
||||||
|
Material.FIREWORK_ROCKET,
|
||||||
|
Material.BOOK,
|
||||||
|
Material.ELYTRA,
|
||||||
|
Material.TOTEM_OF_UNDYING
|
||||||
|
};
|
||||||
|
Material m = pool[
|
||||||
|
(int) (Math.random()
|
||||||
|
* pool.length)];
|
||||||
|
ItemStack it =
|
||||||
|
new ItemStack(m);
|
||||||
|
ItemMeta im = it.getItemMeta();
|
||||||
|
im.setDisplayName("§e默认图标");
|
||||||
|
it.setItemMeta(im);
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MenuManager(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MenuItem> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 读取菜单.txt =====
|
||||||
|
public void loadMenu() {
|
||||||
|
items.clear();
|
||||||
|
File f = new File(
|
||||||
|
plugin.getDataFolder(),
|
||||||
|
"菜单.txt");
|
||||||
|
if (!f.exists()) createDefault(f);
|
||||||
|
parseFile(f);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 加载 "
|
||||||
|
+ items.size()
|
||||||
|
+ " 个菜单项");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseFile(File f) {
|
||||||
|
plugin.getLogger().info("[Menu] ===== 扫描 =====");
|
||||||
|
plugin.getLogger().info("[Menu] 路径: "
|
||||||
|
+ f.getAbsolutePath());
|
||||||
|
plugin.getLogger().info("[Menu] 大小: "
|
||||||
|
+ f.length());
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<String> lines = Files.readAllLines(
|
||||||
|
f.toPath(), StandardCharsets.UTF_8);
|
||||||
|
plugin.getLogger().info("[Menu] 总行数: "
|
||||||
|
+ lines.size());
|
||||||
|
|
||||||
|
boolean inBlock = false;
|
||||||
|
boolean inHtml = false;
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
boolean inItem = false;
|
||||||
|
int parsed = 0;
|
||||||
|
|
||||||
|
for (int lineNum = 1;
|
||||||
|
lineNum <= lines.size();
|
||||||
|
lineNum++) {
|
||||||
|
String raw = lines.get(lineNum - 1);
|
||||||
|
String l = raw.trim();
|
||||||
|
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": 原文[" + l.length()
|
||||||
|
+ "字符] " + l);
|
||||||
|
|
||||||
|
// ===== 第1优先:跨行块注释 =====
|
||||||
|
if (inBlock) {
|
||||||
|
if (l.contains("*/")) {
|
||||||
|
inBlock = false;
|
||||||
|
l = l.substring(
|
||||||
|
l.indexOf("*/") + 2)
|
||||||
|
.trim();
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": 块注释结束");
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": [块注释中]");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 第2优先:跨行HTML注释 =====
|
||||||
|
if (inHtml) {
|
||||||
|
if (l.contains("-->")) {
|
||||||
|
inHtml = false;
|
||||||
|
l = l.substring(
|
||||||
|
l.indexOf("-->") + 3)
|
||||||
|
.trim();
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": [HTML注释中]");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 第3优先:整行注释(最高裁决) =====
|
||||||
|
// 以 # 或 // 开头的行,直接跳过
|
||||||
|
// 不管后面包含什么 /* <!-- 等
|
||||||
|
if (l.startsWith("#")
|
||||||
|
|| l.startsWith("//")) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": [整行注释,跳过]");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 第4优先:块注释开始 =====
|
||||||
|
if (l.contains("/*")) {
|
||||||
|
l = l.substring(0, l.indexOf("/*"))
|
||||||
|
.trim();
|
||||||
|
inBlock = true;
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": 进入/*块注释"
|
||||||
|
+ " 剩余[" + l + "]");
|
||||||
|
if (l.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 第5优先:HTML注释开始 =====
|
||||||
|
if (l.contains("<!--")) {
|
||||||
|
l = l.substring(0,
|
||||||
|
l.indexOf("<!--")).trim();
|
||||||
|
inHtml = true;
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": 进入<!--注释");
|
||||||
|
if (l.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 第6优先:行内注释 =====
|
||||||
|
int hashPos = l.indexOf("#");
|
||||||
|
int slashPos = l.indexOf("//");
|
||||||
|
if (hashPos >= 0) {
|
||||||
|
l = l.substring(0, hashPos).trim();
|
||||||
|
}
|
||||||
|
if (slashPos >= 0) {
|
||||||
|
if (hashPos < 0
|
||||||
|
|| slashPos < hashPos) {
|
||||||
|
l = l.substring(0, slashPos)
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l.isEmpty()) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": 空");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + lineNum
|
||||||
|
+ ": [保留] " + l);
|
||||||
|
|
||||||
|
if (l.equals("{")) {
|
||||||
|
inItem = true;
|
||||||
|
buf.setLength(0);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] === 块开始 ===");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l.equals("}")) {
|
||||||
|
if (inItem) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] === 块结束 ===");
|
||||||
|
parseBlock(buf.toString());
|
||||||
|
parsed++;
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 当前菜单项: "
|
||||||
|
+ items.size());
|
||||||
|
}
|
||||||
|
inItem = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inItem) {
|
||||||
|
buf.append(l).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] ===== 完毕 =====");
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 块数: " + parsed);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 菜单项: "
|
||||||
|
+ items.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Menu] 异常: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void parseBlock(String block) {
|
||||||
|
String title = "";
|
||||||
|
String cmd = "";
|
||||||
|
String perm = "玩家";
|
||||||
|
String iconMat = "";
|
||||||
|
int slotNum = -1;
|
||||||
|
|
||||||
|
for (String line : block.split("\n")) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
|
||||||
|
String v = "";
|
||||||
|
int idx = line.indexOf(":");
|
||||||
|
if (idx < 0) idx = line.indexOf(":");
|
||||||
|
if (idx >= 0) {
|
||||||
|
v = line.substring(idx + 1).trim();
|
||||||
|
if (v.startsWith("\"")
|
||||||
|
&& v.endsWith("\"")
|
||||||
|
&& v.length() >= 2) {
|
||||||
|
v = v.substring(1, v.length() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("标题")) {
|
||||||
|
title = v;
|
||||||
|
} else if (line.startsWith("指令")) {
|
||||||
|
cmd = v;
|
||||||
|
if (!cmd.isEmpty()
|
||||||
|
&& !cmd.startsWith("/")) {
|
||||||
|
cmd = "/" + cmd;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("权限")) {
|
||||||
|
perm = v;
|
||||||
|
} else if (line.startsWith("图标")) {
|
||||||
|
iconMat = v;
|
||||||
|
} else if (line.startsWith("格子")) {
|
||||||
|
try {
|
||||||
|
int s = Integer.parseInt(v);
|
||||||
|
if (s >= 1 && s <= 54) {
|
||||||
|
slotNum = s;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.isEmpty()) return;
|
||||||
|
|
||||||
|
MenuItem mi = new MenuItem(title, cmd, perm);
|
||||||
|
if (slotNum >= 1 && slotNum <= 54) {
|
||||||
|
mi.slot = slotNum;
|
||||||
|
}
|
||||||
|
if (!iconMat.isEmpty()) {
|
||||||
|
try {
|
||||||
|
String matName = iconMat
|
||||||
|
.replace("minecraft:", "")
|
||||||
|
.replace("bukkit:", "")
|
||||||
|
.trim().toUpperCase();
|
||||||
|
Material m =
|
||||||
|
Material.valueOf(matName);
|
||||||
|
mi.icon = new ItemStack(m);
|
||||||
|
ItemMeta im =
|
||||||
|
mi.icon.getItemMeta();
|
||||||
|
im.setDisplayName(
|
||||||
|
"§e" + title);
|
||||||
|
mi.icon.setItemMeta(im);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Menu] 无法解析: "
|
||||||
|
+ iconMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.add(mi);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void createDefault(File f) {
|
||||||
|
try (PrintWriter pw = new PrintWriter(
|
||||||
|
new OutputStreamWriter(
|
||||||
|
new FileOutputStream(f),
|
||||||
|
StandardCharsets
|
||||||
|
.UTF_8))) {
|
||||||
|
pw.println("# 服务器菜单");
|
||||||
|
pw.println("# {}定义菜单项");
|
||||||
|
pw.println("# 支持"
|
||||||
|
+ " # // /* */ <!-- -->");
|
||||||
|
pw.println();
|
||||||
|
pw.println("{");
|
||||||
|
pw.println(" 标题:主城传送");
|
||||||
|
pw.println(" 指令:/spawn");
|
||||||
|
pw.println(" 权限类型:玩家");
|
||||||
|
pw.println(" 图标:COMPASS");
|
||||||
|
pw.println("}");
|
||||||
|
pw.println();
|
||||||
|
pw.println("{");
|
||||||
|
pw.println(" 标题:随机传送");
|
||||||
|
pw.println(" 指令:/rtp");
|
||||||
|
pw.println(" 权限类型:玩家");
|
||||||
|
pw.println(" 图标:ENDER_PEARL");
|
||||||
|
pw.println("}");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 保存 =====
|
||||||
|
public void saveMenu() {
|
||||||
|
File f = new File(
|
||||||
|
plugin.getDataFolder(),
|
||||||
|
"菜单.txt");
|
||||||
|
try (PrintWriter pw = new PrintWriter(
|
||||||
|
new OutputStreamWriter(
|
||||||
|
new FileOutputStream(f),
|
||||||
|
StandardCharsets.UTF_8))) {
|
||||||
|
pw.println("# 服务器菜单");
|
||||||
|
pw.println();
|
||||||
|
for (MenuItem mi : items) {
|
||||||
|
pw.println("{");
|
||||||
|
pw.println(" 标题:"
|
||||||
|
+ mi.title);
|
||||||
|
pw.println(" 指令:"
|
||||||
|
+ mi.command);
|
||||||
|
pw.println(" 权限类型:"
|
||||||
|
+ mi.permType);
|
||||||
|
// ★ 保存实际材质名 ★
|
||||||
|
pw.println(" 图标:"
|
||||||
|
+ mi.icon.getType().name());
|
||||||
|
pw.println("}");
|
||||||
|
pw.println();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Menu] 保存失败: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 聊天编辑 =====
|
||||||
|
public boolean isEditing(String name) {
|
||||||
|
return chatWait.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void editTitle(
|
||||||
|
Player p, int idx) {
|
||||||
|
chatIdx.put(p.getName(), idx);
|
||||||
|
chatWait.add(p.getName());
|
||||||
|
chatField.put(p.getName(), "title");
|
||||||
|
p.closeInventory();
|
||||||
|
p.sendMessage("§e请输入菜单标题:");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void editCommand(
|
||||||
|
Player p, int idx) {
|
||||||
|
chatIdx.put(p.getName(), idx);
|
||||||
|
chatWait.add(p.getName());
|
||||||
|
chatField.put(p.getName(), "command");
|
||||||
|
p.closeInventory();
|
||||||
|
p.sendMessage("§e请输入指令"
|
||||||
|
+ "(自动补全/):");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onChat(Player p, String input) {
|
||||||
|
String name = p.getName();
|
||||||
|
if (!chatWait.remove(name)) return;
|
||||||
|
|
||||||
|
String field = chatField.remove(name);
|
||||||
|
Integer idx = chatIdx.get(name);
|
||||||
|
if (idx == null || idx >= items.size()) return;
|
||||||
|
|
||||||
|
MenuItem mi = items.get(idx);
|
||||||
|
|
||||||
|
if ("title".equals(field)) {
|
||||||
|
mi.title = input;
|
||||||
|
p.sendMessage("§a标题: " + input);
|
||||||
|
} else if ("command".equals(field)) {
|
||||||
|
if (!input.startsWith("/"))
|
||||||
|
input = "/" + input;
|
||||||
|
mi.command = input;
|
||||||
|
p.sendMessage("§a指令: " + input);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
|
plugin.gui.openEditor(p, idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class PlayerStorage {
|
||||||
|
private Connection conn;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
public PlayerStorage(File dataFolder,
|
||||||
|
Logger logger) {
|
||||||
|
this.logger = logger;
|
||||||
|
File dbFile = new File(
|
||||||
|
dataFolder,
|
||||||
|
"quest_data.db");
|
||||||
|
try {
|
||||||
|
Class.forName(
|
||||||
|
"org.sqlite.JDBC");
|
||||||
|
conn = DriverManager
|
||||||
|
.getConnection(
|
||||||
|
"jdbc:sqlite:"
|
||||||
|
+ dbFile
|
||||||
|
.getAbsolutePath());
|
||||||
|
conn.setAutoCommit(true);
|
||||||
|
Statement s =
|
||||||
|
conn.createStatement();
|
||||||
|
s.executeUpdate(
|
||||||
|
"CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "quest_kv ("
|
||||||
|
+ "player TEXT NOT NULL,"
|
||||||
|
+ "key TEXT NOT NULL,"
|
||||||
|
+ "value TEXT,"
|
||||||
|
+ "PRIMARY KEY"
|
||||||
|
+ "(player, key))");
|
||||||
|
s.close();
|
||||||
|
logger.info(
|
||||||
|
"[QuestStorage] 初始化: "
|
||||||
|
+ dbFile.getAbsolutePath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe(
|
||||||
|
"[QuestStorage] 失败: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getCondition(
|
||||||
|
String player, String key) {
|
||||||
|
if (conn == null) return null;
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
conn.prepareStatement(
|
||||||
|
"SELECT value "
|
||||||
|
+ "FROM quest_kv "
|
||||||
|
+ "WHERE player=? "
|
||||||
|
+ "AND key=?");
|
||||||
|
ps.setString(1,
|
||||||
|
player.toLowerCase());
|
||||||
|
ps.setString(2, key);
|
||||||
|
ResultSet rs =
|
||||||
|
ps.executeQuery();
|
||||||
|
String val = rs.next()
|
||||||
|
? rs.getString("value")
|
||||||
|
: null;
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return parseValue(val);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCondition(
|
||||||
|
String player, String key) {
|
||||||
|
setConditionRaw(
|
||||||
|
player, key, "true");
|
||||||
|
}
|
||||||
|
public void debugPlayer(String player) {
|
||||||
|
|
||||||
|
}
|
||||||
|
public void setConditionRaw(
|
||||||
|
String player, String key,
|
||||||
|
String value) {
|
||||||
|
if (conn == null) return;
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
conn.prepareStatement(
|
||||||
|
"INSERT OR REPLACE "
|
||||||
|
+ "INTO quest_kv"
|
||||||
|
+ "(player,key,value)"
|
||||||
|
+ " VALUES(?,?,?)");
|
||||||
|
ps.setString(1,
|
||||||
|
player.toLowerCase());
|
||||||
|
ps.setString(2, key);
|
||||||
|
ps.setString(3, value);
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warning(
|
||||||
|
"[QuestStorage] 写入失败: "
|
||||||
|
+ key + "=" + value
|
||||||
|
+ " | " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (conn != null
|
||||||
|
&& !conn.isClosed())
|
||||||
|
conn.close();
|
||||||
|
} catch (Exception e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object parseValue(String v) {
|
||||||
|
if (v == null) return null;
|
||||||
|
try {
|
||||||
|
return Long.parseLong(v);
|
||||||
|
} catch (Exception e1) {
|
||||||
|
try {
|
||||||
|
return Double.parseDouble(v);
|
||||||
|
} catch (Exception e2) {
|
||||||
|
if ("true".equalsIgnoreCase(v))
|
||||||
|
return true;
|
||||||
|
if ("false".equalsIgnoreCase(v))
|
||||||
|
return false;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,14 @@ import org.bukkit.entity.Player;
|
|||||||
import org.bukkit.inventory.Inventory;
|
import org.bukkit.inventory.Inventory;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.ItemMeta;
|
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.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -41,11 +49,10 @@ public class PointsManager {
|
|||||||
plugin.getLogger().info(
|
plugin.getLogger().info(
|
||||||
"[Sdf1_login] CY_beibao 联控已连接");
|
"[Sdf1_login] CY_beibao 联控已连接");
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CY 商品拉取 =====
|
|
||||||
|
|
||||||
private void refreshCYItems() {
|
private void refreshCYItems() {
|
||||||
cyItems.clear();
|
cyItems.clear();
|
||||||
if (cyInstance == null
|
if (cyInstance == null
|
||||||
@@ -98,15 +105,11 @@ public class PointsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 积分查询 =====
|
|
||||||
|
|
||||||
public int getPoints(String name) {
|
public int getPoints(String name) {
|
||||||
return ((Number) plugin.getDb()
|
return ((Number) plugin.getDb()
|
||||||
.getField(name, "points")).intValue();
|
.getField(name, "points")).intValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 主面板 =====
|
|
||||||
|
|
||||||
public Inventory createShopGUI(Player p) {
|
public Inventory createShopGUI(Player p) {
|
||||||
int points = getPoints(p.getName());
|
int points = getPoints(p.getName());
|
||||||
Inventory inv = Bukkit.createInventory(
|
Inventory inv = Bukkit.createInventory(
|
||||||
@@ -142,8 +145,6 @@ public class PointsManager {
|
|||||||
return inv;
|
return inv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CY 商品面板 =====
|
|
||||||
|
|
||||||
public Inventory createCYShopGUI(Player p) {
|
public Inventory createCYShopGUI(Player p) {
|
||||||
refreshCYItems();
|
refreshCYItems();
|
||||||
int points = getPoints(p.getName());
|
int points = getPoints(p.getName());
|
||||||
@@ -201,7 +202,57 @@ public class PointsManager {
|
|||||||
return inv;
|
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) {
|
public void handleClick(Player p, int slot) {
|
||||||
int points = getPoints(p.getName());
|
int points = getPoints(p.getName());
|
||||||
@@ -214,14 +265,14 @@ public class PointsManager {
|
|||||||
plugin.getEconomy()
|
plugin.getEconomy()
|
||||||
.depositPlayer(p, 1000);
|
.depositPlayer(p, 1000);
|
||||||
}
|
}
|
||||||
p.sendMessage(plugin.getConfig2().msg(
|
p.sendMessage(plugin.getConfig2()
|
||||||
"points_purchase_success",
|
.msg("points_purchase_success")
|
||||||
"count", "100"));
|
.replace("{count}", "100"));
|
||||||
} else {
|
} else {
|
||||||
p.sendMessage(plugin.getConfig2().msg(
|
p.sendMessage(plugin.getConfig2()
|
||||||
"points_insufficient",
|
.msg("points_insufficient")
|
||||||
"points",
|
.replace("{points}",
|
||||||
String.valueOf(points)));
|
String.valueOf(points)));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 12:
|
case 12:
|
||||||
@@ -229,14 +280,14 @@ public class PointsManager {
|
|||||||
plugin.getDb().deductPoints(
|
plugin.getDb().deductPoints(
|
||||||
p.getName(), 50);
|
p.getName(), 50);
|
||||||
activateCY(p.getName(), 9, 0);
|
activateCY(p.getName(), 9, 0);
|
||||||
p.sendMessage(plugin.getConfig2().msg(
|
p.sendMessage(plugin.getConfig2()
|
||||||
"points_purchase_success",
|
.msg("points_purchase_success")
|
||||||
"count", "50"));
|
.replace("{count}", "50"));
|
||||||
} else {
|
} else {
|
||||||
p.sendMessage(plugin.getConfig2().msg(
|
p.sendMessage(plugin.getConfig2()
|
||||||
"points_insufficient",
|
.msg("points_insufficient")
|
||||||
"points",
|
.replace("{points}",
|
||||||
String.valueOf(points)));
|
String.valueOf(points)));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 14:
|
case 14:
|
||||||
@@ -244,14 +295,14 @@ public class PointsManager {
|
|||||||
plugin.getDb().deductPoints(
|
plugin.getDb().deductPoints(
|
||||||
p.getName(), 20);
|
p.getName(), 20);
|
||||||
p.setLevel(p.getLevel() + 10);
|
p.setLevel(p.getLevel() + 10);
|
||||||
p.sendMessage(plugin.getConfig2().msg(
|
p.sendMessage(plugin.getConfig2()
|
||||||
"points_purchase_success",
|
.msg("points_purchase_success")
|
||||||
"count", "20"));
|
.replace("{count}", "20"));
|
||||||
} else {
|
} else {
|
||||||
p.sendMessage(plugin.getConfig2().msg(
|
p.sendMessage(plugin.getConfig2()
|
||||||
"points_insufficient",
|
.msg("points_insufficient")
|
||||||
"points",
|
.replace("{points}",
|
||||||
String.valueOf(points)));
|
String.valueOf(points)));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 31:
|
case 31:
|
||||||
@@ -263,8 +314,6 @@ public class PointsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CY 商品面板点击 =====
|
|
||||||
|
|
||||||
public void handleCYClick(Player p, int slot) {
|
public void handleCYClick(Player p, int slot) {
|
||||||
if (slot < 0 || slot >= cyItems.size()
|
if (slot < 0 || slot >= cyItems.size()
|
||||||
|| slot >= 45) return;
|
|| slot >= 45) return;
|
||||||
@@ -281,16 +330,15 @@ public class PointsManager {
|
|||||||
.get("lifetime");
|
.get("lifetime");
|
||||||
|
|
||||||
if (stock == -1) {
|
if (stock == -1) {
|
||||||
p.sendMessage(
|
p.sendMessage("§c该商品已下架");
|
||||||
"§c[Sdf1_login] §f该商品已下架");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int points = getPoints(p.getName());
|
int points = getPoints(p.getName());
|
||||||
if (points < price) {
|
if (points < price) {
|
||||||
p.sendMessage(plugin.getConfig2().msg(
|
p.sendMessage(plugin.getConfig2()
|
||||||
"points_insufficient",
|
.msg("points_insufficient")
|
||||||
"points",
|
.replace("{points}",
|
||||||
String.valueOf(points)));
|
String.valueOf(points)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,12 +348,10 @@ public class PointsManager {
|
|||||||
|
|
||||||
String tl = lifetime
|
String tl = lifetime
|
||||||
? "终身" : (days + "天");
|
? "终身" : (days + "天");
|
||||||
p.sendMessage("§a[Sdf1_login] §f购买成功!获得 "
|
p.sendMessage("§a购买成功!获得 "
|
||||||
+ tl + " +" + slots + "格 背包空间");
|
+ tl + " +" + slots + "格 背包空间");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 工具 =====
|
|
||||||
|
|
||||||
private void fillBg(Inventory inv) {
|
private void fillBg(Inventory inv) {
|
||||||
ItemStack gl = mkItem(
|
ItemStack gl = mkItem(
|
||||||
Material.GRAY_STAINED_GLASS_PANE, " ");
|
Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
public class QuestStorage {
|
||||||
|
|
||||||
|
private Connection conn;
|
||||||
|
|
||||||
|
public QuestStorage(File dataFolder) {
|
||||||
|
try {
|
||||||
|
Class.forName("org.sqlite.JDBC");
|
||||||
|
File dbFile = new File(
|
||||||
|
dataFolder,
|
||||||
|
"quest_data.db");
|
||||||
|
conn = DriverManager.getConnection(
|
||||||
|
"jdbc:sqlite:"
|
||||||
|
+ dbFile.getPath());
|
||||||
|
initTable();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTable()
|
||||||
|
throws SQLException {
|
||||||
|
Statement stmt =
|
||||||
|
conn.createStatement();
|
||||||
|
stmt.executeUpdate(
|
||||||
|
"CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "quest_conditions ("
|
||||||
|
+ "player TEXT NOT NULL, "
|
||||||
|
+ "condition_key TEXT NOT NULL, "
|
||||||
|
+ "value TEXT DEFAULT '1', "
|
||||||
|
+ "timestamp INTEGER, "
|
||||||
|
+ "PRIMARY KEY "
|
||||||
|
+ "(player, condition_key))");
|
||||||
|
stmt.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记条件已完成(value = "1")
|
||||||
|
*/
|
||||||
|
public void setCondition(
|
||||||
|
String player,
|
||||||
|
String condition) {
|
||||||
|
setConditionRaw(
|
||||||
|
player, condition, "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入任意值(计数器等)
|
||||||
|
*/
|
||||||
|
public void setConditionRaw(
|
||||||
|
String player,
|
||||||
|
String condition,
|
||||||
|
String value) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
conn.prepareStatement(
|
||||||
|
"INSERT OR REPLACE "
|
||||||
|
+ "INTO "
|
||||||
|
+ "quest_conditions "
|
||||||
|
+ "(player, "
|
||||||
|
+ "condition_key, "
|
||||||
|
+ "value, timestamp) "
|
||||||
|
+ "VALUES "
|
||||||
|
+ "(?, ?, ?, ?)");
|
||||||
|
ps.setString(1, player);
|
||||||
|
ps.setString(2, condition);
|
||||||
|
ps.setString(3, value);
|
||||||
|
ps.setLong(4,
|
||||||
|
System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询条件值
|
||||||
|
*/
|
||||||
|
public Object getCondition(
|
||||||
|
String player,
|
||||||
|
String condition) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
conn.prepareStatement(
|
||||||
|
"SELECT value FROM "
|
||||||
|
+ "quest_conditions "
|
||||||
|
+ "WHERE player = ? "
|
||||||
|
+ "AND "
|
||||||
|
+ "condition_key = ?");
|
||||||
|
ps.setString(1, player);
|
||||||
|
ps.setString(2, condition);
|
||||||
|
ResultSet rs =
|
||||||
|
ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
String val =
|
||||||
|
rs.getString("value");
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
// 尝试解析为数字
|
||||||
|
try {
|
||||||
|
if (val.contains(".")) {
|
||||||
|
return Double.parseDouble(
|
||||||
|
val);
|
||||||
|
}
|
||||||
|
return Long.parseLong(val);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (conn != null
|
||||||
|
&& !conn.isClosed()) {
|
||||||
|
conn.close();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player
|
||||||
|
.PlayerResourcePackStatusEvent;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent
|
||||||
|
.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class RadioDownloadListener
|
||||||
|
implements Listener {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
private final java.util.logging.Logger log;
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<UUID, Integer>
|
||||||
|
failCount = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<UUID, Boolean>
|
||||||
|
blocked = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final int MAX_FAILED = 3;
|
||||||
|
|
||||||
|
public RadioDownloadListener(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.log = plugin.getLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPlayerQuit(UUID uuid) {
|
||||||
|
failCount.remove(uuid);
|
||||||
|
blocked.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onStatus(
|
||||||
|
PlayerResourcePackStatusEvent e) {
|
||||||
|
Player p = e.getPlayer();
|
||||||
|
UUID uuid = p.getUniqueId();
|
||||||
|
String status =
|
||||||
|
e.getStatus().name();
|
||||||
|
|
||||||
|
log.info("[Radio] === 资源包状态 ===");
|
||||||
|
log.info("[Radio] 玩家: "
|
||||||
|
+ p.getName());
|
||||||
|
log.info("[Radio] UUID: " + uuid);
|
||||||
|
log.info("[Radio] 状态: "
|
||||||
|
+ status);
|
||||||
|
log.info("[Radio] 原始状态枚举: "
|
||||||
|
+ e.getStatus().getClass()
|
||||||
|
.getSimpleName()
|
||||||
|
+ "."
|
||||||
|
+ e.getStatus().name());
|
||||||
|
|
||||||
|
if (blocked.containsKey(uuid)) {
|
||||||
|
log.info("[Radio] 已熔断,跳过: "
|
||||||
|
+ p.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "SUCCESS":
|
||||||
|
log.info("[Radio] "
|
||||||
|
+ p.getName()
|
||||||
|
+ " ✓ 资源包加载成功");
|
||||||
|
failCount.remove(uuid);
|
||||||
|
blocked.remove(uuid);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ACCEPTED":
|
||||||
|
log.info("[Radio] "
|
||||||
|
+ p.getName()
|
||||||
|
+ " 客户端已接受,"
|
||||||
|
+ "等待下载...");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "DECLINED":
|
||||||
|
log.warning("[Radio] "
|
||||||
|
+ p.getName()
|
||||||
|
+ " ✗ 拒绝了资源包");
|
||||||
|
log.warning("[Radio] "
|
||||||
|
+ "可能原因: "
|
||||||
|
+ "1.客户端设置禁用资源包"
|
||||||
|
+ " 2.已缓存拒绝记录"
|
||||||
|
+ " 3.SSL证书不匹配");
|
||||||
|
handleFailure(p, uuid);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "FAILED_DOWNLOAD":
|
||||||
|
log.warning("[Radio] "
|
||||||
|
+ p.getName()
|
||||||
|
+ " ✗ 下载失败");
|
||||||
|
log.warning("[Radio] "
|
||||||
|
+ "可能原因: "
|
||||||
|
+ "1.URL无法访问"
|
||||||
|
+ " 2.SSL证书错误"
|
||||||
|
+ " 3.网络超时"
|
||||||
|
+ " 4.文件不存在(404)");
|
||||||
|
handleFailure(p, uuid);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.info("[Radio] "
|
||||||
|
+ p.getName()
|
||||||
|
+ " 未知状态: "
|
||||||
|
+ status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFailure(Player p, UUID uuid) {
|
||||||
|
int failNum = failCount.getOrDefault(uuid, 0) + 1;
|
||||||
|
failCount.put(uuid, failNum);
|
||||||
|
|
||||||
|
log.warning("[Radio] " + p.getName()
|
||||||
|
+ " 失败次数: " + failNum + "/" + MAX_FAILED);
|
||||||
|
|
||||||
|
if (failNum >= MAX_FAILED) {
|
||||||
|
blocked.put(uuid, true);
|
||||||
|
log.severe("[Radio] " + p.getName()
|
||||||
|
+ " 已达最大失败次数,停止重试,"
|
||||||
|
+ "玩家继续留在服务器");
|
||||||
|
// 不踢出,只记录熔断,玩家正常游玩
|
||||||
|
p.sendMessage("§e§l[Radio] 资源包加载失败"
|
||||||
|
+ ",已取消下载资源包,部分画面音频可能出现问题"
|
||||||
|
+ "\n§d§l若想继续下载,请退出重进。若持续失败,请上报管理员");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 打了包没测试
|
||||||
|
p.sendMessage("§e§l[Radio] 下载失败,"
|
||||||
|
+ "重试... (" + failNum + "/" + MAX_FAILED + ")");
|
||||||
|
|
||||||
|
// 5秒后重试
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTaskLater(plugin, () -> {
|
||||||
|
if (p.isOnline() && plugin.radio != null) {
|
||||||
|
plugin.radio.sendResourcePack(p);
|
||||||
|
}
|
||||||
|
}, 100L);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,49 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -399,12 +399,6 @@ public class TicketManager {
|
|||||||
|| TYPE_PASSWORD.equals(type)) {
|
|| TYPE_PASSWORD.equals(type)) {
|
||||||
plugin.getDb().updateTicketField(
|
plugin.getDb().updateTicketField(
|
||||||
id, "assigned_to", "admin");
|
id, "assigned_to", "admin");
|
||||||
// 播放派单音效
|
|
||||||
if (plugin.getTicketSound() != null) {
|
|
||||||
plugin.getTicketSound()
|
|
||||||
.playSound(p, "assign");
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.getDb().updateTicketStatus(
|
plugin.getDb().updateTicketStatus(
|
||||||
id, "processing");
|
id, "processing");
|
||||||
p.sendMessage(
|
p.sendMessage(
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public class UpdateChecker {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
private final String currentVersion;
|
||||||
|
|
||||||
|
// ===== 仓库信息 =====
|
||||||
|
private static final String GITHUB_OWNER =
|
||||||
|
"ypsdf1";
|
||||||
|
private static final String GITHUB_REPO =
|
||||||
|
"sdf1_login";
|
||||||
|
private static final String GITEE_OWNER =
|
||||||
|
"nihaoshidifu";
|
||||||
|
private static final String GITEE_REPO =
|
||||||
|
"sdf1_login";
|
||||||
|
|
||||||
|
|
||||||
|
// API 地址
|
||||||
|
private static final String GITHUB_API =
|
||||||
|
"https://api.github.com/repos/"
|
||||||
|
+ GITHUB_OWNER + "/"
|
||||||
|
+ GITHUB_REPO
|
||||||
|
+ "/tags";
|
||||||
|
private static final String GITEE_API =
|
||||||
|
"https://gitee.com/api/v5/repos/"
|
||||||
|
+ GITEE_OWNER + "/"
|
||||||
|
+ GITEE_REPO
|
||||||
|
+ "/tags";
|
||||||
|
|
||||||
|
// 仓库页面链接(给用户看的)
|
||||||
|
private static final String GITHUB_LINK =
|
||||||
|
"https://github.com/"
|
||||||
|
+ GITHUB_OWNER + "/"
|
||||||
|
+ GITHUB_REPO + "/releases";
|
||||||
|
private static final String GITEE_LINK =
|
||||||
|
"https://gitee.com/"
|
||||||
|
+ GITEE_OWNER + "/"
|
||||||
|
+ GITEE_REPO + "/releases";
|
||||||
|
|
||||||
|
|
||||||
|
// 单次请求超时(毫秒)
|
||||||
|
private static final int TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
|
// 熔断阈值
|
||||||
|
private static final int TRIP_THRESHOLD = 5;
|
||||||
|
|
||||||
|
// ===== 熔断状态(内存态,重启自动清除) =====
|
||||||
|
private volatile boolean circuitTripped =
|
||||||
|
false;
|
||||||
|
private volatile int failCount = 0;
|
||||||
|
|
||||||
|
// ===== 自动检测禁用(持久化到文件) =====
|
||||||
|
private volatile boolean autoCheckDisabled =
|
||||||
|
false;
|
||||||
|
private final File disabledFile;
|
||||||
|
|
||||||
|
public UpdateChecker(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.currentVersion =
|
||||||
|
plugin.getDescription()
|
||||||
|
.getVersion();
|
||||||
|
this.disabledFile = new File(
|
||||||
|
plugin.getDataFolder(),
|
||||||
|
"update_disabled.dat");
|
||||||
|
loadDisabledState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 持久化禁用状态 ==========
|
||||||
|
|
||||||
|
private void loadDisabledState() {
|
||||||
|
if (disabledFile.exists()) {
|
||||||
|
try (BufferedReader br =
|
||||||
|
new BufferedReader(
|
||||||
|
new FileReader(
|
||||||
|
disabledFile))) {
|
||||||
|
String line = br.readLine();
|
||||||
|
if (line != null) {
|
||||||
|
autoCheckDisabled =
|
||||||
|
"true".equals(
|
||||||
|
line.trim());
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDisabledState() {
|
||||||
|
try (PrintWriter pw =
|
||||||
|
new PrintWriter(
|
||||||
|
new FileWriter(
|
||||||
|
disabledFile))) {
|
||||||
|
pw.println(autoCheckDisabled
|
||||||
|
? "true" : "false");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 公开方法 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onEnable 调用(异步静默)
|
||||||
|
* 熔断或禁用则直接跳过
|
||||||
|
*/
|
||||||
|
public void checkOnEnable() {
|
||||||
|
if (autoCheckDisabled) return;
|
||||||
|
if (circuitTripped) return;
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTaskAsynchronously(plugin,
|
||||||
|
() -> doCheck(null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /sdf1_login update 调用(异步)
|
||||||
|
* 忽略熔断和禁用状态,强制检测
|
||||||
|
* 只有拿到版号才解除熔断
|
||||||
|
*/
|
||||||
|
public void checkUpdate(
|
||||||
|
CommandSender sender) {
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTaskAsynchronously(plugin,
|
||||||
|
() ->
|
||||||
|
doCheck(sender,
|
||||||
|
false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 核心检测逻辑 ==========
|
||||||
|
private void doCheck(CommandSender sender,
|
||||||
|
boolean isAuto) {
|
||||||
|
|
||||||
|
if (isAuto) {
|
||||||
|
if (autoCheckDisabled
|
||||||
|
|| circuitTripped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通道1:GitHub
|
||||||
|
CheckResult gh =
|
||||||
|
tryChannel(GITHUB_API,
|
||||||
|
"GitHub", GITHUB_LINK);
|
||||||
|
|
||||||
|
// 通道2:Gitee
|
||||||
|
CheckResult ge =
|
||||||
|
tryChannel(GITEE_API,
|
||||||
|
"Gitee", GITEE_LINK);
|
||||||
|
|
||||||
|
// 选择成功的结果
|
||||||
|
CheckResult chosen = null;
|
||||||
|
String failMsg = null;
|
||||||
|
|
||||||
|
if (gh.isSuccess()) {
|
||||||
|
chosen = gh;
|
||||||
|
} else if (ge.isSuccess()) {
|
||||||
|
chosen = ge;
|
||||||
|
} else {
|
||||||
|
// 双通道均失败
|
||||||
|
failCount++;
|
||||||
|
// 拼接两个通道的失败原因
|
||||||
|
StringBuilder sb =
|
||||||
|
new StringBuilder();
|
||||||
|
if (gh.error != null) {
|
||||||
|
sb.append("§7GitHub: §c")
|
||||||
|
.append(gh.error);
|
||||||
|
}
|
||||||
|
if (ge.error != null) {
|
||||||
|
if (sb.length() > 0)
|
||||||
|
sb.append("\n");
|
||||||
|
sb.append("§7Gitee: §c")
|
||||||
|
.append(ge.error);
|
||||||
|
}
|
||||||
|
if (failCount >= TRIP_THRESHOLD) {
|
||||||
|
circuitTripped = true;
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Sdf1_login] 更新检测"
|
||||||
|
+ "已熔断,连续失败"
|
||||||
|
+ failCount + "次");
|
||||||
|
}
|
||||||
|
if (sender != null) {
|
||||||
|
final String msg =
|
||||||
|
sb.toString();
|
||||||
|
final boolean tripped =
|
||||||
|
circuitTripped;
|
||||||
|
final int fc = failCount;
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTask(plugin, () -> {
|
||||||
|
sender.sendMessage(
|
||||||
|
"§c§l[更新] "
|
||||||
|
+ "检测失败");
|
||||||
|
sender.sendMessage(msg);
|
||||||
|
sender.sendMessage(
|
||||||
|
"§e失败次数: "
|
||||||
|
+ fc + "/"
|
||||||
|
+ TRIP_THRESHOLD);
|
||||||
|
if (tripped) {
|
||||||
|
sender.sendMessage(
|
||||||
|
"§c已熔断,"
|
||||||
|
+ "请重启服务器"
|
||||||
|
+ "重置");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 至少一个通道成功
|
||||||
|
failCount = 0;
|
||||||
|
|
||||||
|
// 检查禁用信号
|
||||||
|
if (isDisableSignal(chosen.version)) {
|
||||||
|
autoCheckDisabled = true;
|
||||||
|
saveDisabledState();
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Sdf1_login] 远程版本信号"
|
||||||
|
+ " \"" + chosen.version
|
||||||
|
+ "\",自动更新检测已禁用");
|
||||||
|
if (sender != null) {
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTask(plugin, () ->
|
||||||
|
sender.sendMessage(
|
||||||
|
"§e[更新] 远程已"
|
||||||
|
+ "禁用自动"
|
||||||
|
+ "更新检测"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动调用:成功才解除熔断和禁用
|
||||||
|
if (!isAuto) {
|
||||||
|
if (circuitTripped) {
|
||||||
|
circuitTripped = false;
|
||||||
|
}
|
||||||
|
if (autoCheckDisabled) {
|
||||||
|
autoCheckDisabled = false;
|
||||||
|
saveDisabledState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String src = chosen.source;
|
||||||
|
final String lnk = chosen.link;
|
||||||
|
final String rv = chosen.version;
|
||||||
|
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTask(plugin, () -> {
|
||||||
|
if (sender == null) return;
|
||||||
|
|
||||||
|
sender.sendMessage(
|
||||||
|
"§e[更新] 检测成功"
|
||||||
|
+ " via: §a" + src);
|
||||||
|
sender.sendMessage(
|
||||||
|
"§e[更新] 本地版本:"
|
||||||
|
+ " §f"
|
||||||
|
+ currentVersion);
|
||||||
|
sender.sendMessage(
|
||||||
|
"§e[更新] 远程版本:"
|
||||||
|
+ " §f" + rv);
|
||||||
|
|
||||||
|
if (isNewer(rv,
|
||||||
|
currentVersion)) {
|
||||||
|
sender.sendMessage(
|
||||||
|
"§c[更新] 发现新"
|
||||||
|
+ "版本: " + rv);
|
||||||
|
sender.sendMessage(
|
||||||
|
"§a请前往仓库下载"
|
||||||
|
+ "更新:");
|
||||||
|
sender.sendMessage(
|
||||||
|
"§a" + lnk);
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(
|
||||||
|
"§a[更新] 已是"
|
||||||
|
+ "最新版本");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示另一通道的失败原因
|
||||||
|
// (仅信息,不影响结果)
|
||||||
|
CheckResult other =
|
||||||
|
(src.equals("GitHub"))
|
||||||
|
? ge : gh;
|
||||||
|
if (!other.isSuccess()
|
||||||
|
&& other.error != null) {
|
||||||
|
sender.sendMessage(
|
||||||
|
"§7[更新] "
|
||||||
|
+ other.source
|
||||||
|
+ "不可用: "
|
||||||
|
+ other.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ========== 禁用信号判断 ==========
|
||||||
|
|
||||||
|
private boolean isDisableSignal(
|
||||||
|
String version) {
|
||||||
|
if (version == null) return false;
|
||||||
|
String v = version.trim().toLowerCase();
|
||||||
|
return "999".equals(v)
|
||||||
|
|| "关闭".equals(v)
|
||||||
|
|| "false".equals(v)
|
||||||
|
|| "禁用".equals(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== HTTP 请求(带超时) ==========
|
||||||
|
|
||||||
|
// ========== 检测结果 ==========
|
||||||
|
|
||||||
|
private static class CheckResult {
|
||||||
|
final String version;
|
||||||
|
final String source;
|
||||||
|
final String link;
|
||||||
|
final String error;
|
||||||
|
|
||||||
|
CheckResult(String version,
|
||||||
|
String source,
|
||||||
|
String link,
|
||||||
|
String error) {
|
||||||
|
this.version = version;
|
||||||
|
this.source = source;
|
||||||
|
this.link = link;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSuccess() {
|
||||||
|
return version != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== HTTP 请求 ==========
|
||||||
|
|
||||||
|
private CheckResult tryChannel(
|
||||||
|
String apiUrl,
|
||||||
|
String sourceName,
|
||||||
|
String link) {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
URL url = new URL(apiUrl);
|
||||||
|
conn = (HttpURLConnection)
|
||||||
|
url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty(
|
||||||
|
"User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; "
|
||||||
|
+ "Win64; x64) AppleWebKit/537.36 "
|
||||||
|
+ "(KHTML, like Gecko) "
|
||||||
|
+ "Chrome/120.0.0.0 Safari/537.36");
|
||||||
|
conn.setRequestProperty(
|
||||||
|
"Accept",
|
||||||
|
"*/*");
|
||||||
|
conn.setRequestProperty(
|
||||||
|
"Accept-Language",
|
||||||
|
"zh-CN,zh;q=0.9");
|
||||||
|
conn.setConnectTimeout(TIMEOUT_MS);
|
||||||
|
conn.setReadTimeout(TIMEOUT_MS);
|
||||||
|
conn.setInstanceFollowRedirects(true);
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
|
||||||
|
if (code == 404) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName
|
||||||
|
+ ": 未找到 (HTTP 404)");
|
||||||
|
}
|
||||||
|
if (code == 403) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName
|
||||||
|
+ ": 被限流 (HTTP 403)");
|
||||||
|
}
|
||||||
|
if (code != 200) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName
|
||||||
|
+ ": HTTP " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedReader reader =
|
||||||
|
new BufferedReader(
|
||||||
|
new InputStreamReader(
|
||||||
|
conn
|
||||||
|
.getInputStream()));
|
||||||
|
StringBuilder sb =
|
||||||
|
new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine())
|
||||||
|
!= null) {
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
String tag =
|
||||||
|
parseTagName(sb.toString());
|
||||||
|
if (tag == null) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName
|
||||||
|
+ ": 无法解析版本号");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CheckResult(tag,
|
||||||
|
sourceName, link, null);
|
||||||
|
|
||||||
|
} catch (java.net
|
||||||
|
.SocketTimeoutException e) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName + ": 连接超时");
|
||||||
|
} catch (java.net.ConnectException e) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName
|
||||||
|
+ ": 连接被拒绝");
|
||||||
|
} catch (java.net
|
||||||
|
.UnknownHostException e) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName
|
||||||
|
+ ": DNS解析失败");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new CheckResult(null,
|
||||||
|
sourceName, link,
|
||||||
|
sourceName + ": "
|
||||||
|
+ e.getMessage());
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 JSON 中提取 tag_name
|
||||||
|
*/
|
||||||
|
private String parseTagName(String json) {
|
||||||
|
// tags API 返回数组: [{"name":"v1.1",...}]
|
||||||
|
// releases API 返回对象: {"tag_name":"v1.1"}
|
||||||
|
|
||||||
|
// 尝试 tag_name(releases格式)
|
||||||
|
int idx =
|
||||||
|
json.indexOf("\"tag_name\"");
|
||||||
|
if (idx >= 0) {
|
||||||
|
int colon =
|
||||||
|
json.indexOf(":", idx);
|
||||||
|
int q1 =
|
||||||
|
json.indexOf("\"", colon + 1);
|
||||||
|
int q2 =
|
||||||
|
json.indexOf("\"", q1 + 1);
|
||||||
|
if (q2 > q1) {
|
||||||
|
return cleanTag(
|
||||||
|
json.substring(
|
||||||
|
q1 + 1, q2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试 "name"(tags格式)
|
||||||
|
idx = json.indexOf("\"name\"");
|
||||||
|
if (idx >= 0) {
|
||||||
|
int colon =
|
||||||
|
json.indexOf(":", idx);
|
||||||
|
int q1 =
|
||||||
|
json.indexOf("\"", colon + 1);
|
||||||
|
int q2 =
|
||||||
|
json.indexOf("\"", q1 + 1);
|
||||||
|
if (q2 > q1) {
|
||||||
|
return cleanTag(
|
||||||
|
json.substring(
|
||||||
|
q1 + 1, q2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cleanTag(String tag) {
|
||||||
|
if (tag.startsWith("v")
|
||||||
|
|| tag.startsWith("V")) {
|
||||||
|
tag = tag.substring(1);
|
||||||
|
}
|
||||||
|
return tag.isEmpty() ? null : tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ========== 版本比较 ==========
|
||||||
|
|
||||||
|
private boolean isNewer(String remote,
|
||||||
|
String local) {
|
||||||
|
String[] r = remote.split("\\.");
|
||||||
|
String[] l = local.split("\\.");
|
||||||
|
int len = Math.max(r.length, l.length);
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
int ri = i < r.length
|
||||||
|
? parseNum(r[i]) : 0;
|
||||||
|
int li = i < l.length
|
||||||
|
? parseNum(l[i]) : 0;
|
||||||
|
if (ri > li) return true;
|
||||||
|
if (ri < li) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseNum(String s) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(
|
||||||
|
s.replaceAll("[^0-9]", ""));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class WelcomeManager {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
private final Random random = new Random();
|
||||||
|
private final List<String> registerMsgs =
|
||||||
|
new ArrayList<>();
|
||||||
|
private final List<String> loginMsgs =
|
||||||
|
new ArrayList<>();
|
||||||
|
|
||||||
|
public WelcomeManager(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadMessages() {
|
||||||
|
registerMsgs.clear();
|
||||||
|
loginMsgs.clear();
|
||||||
|
File f = new File(
|
||||||
|
plugin.getDataFolder(),
|
||||||
|
"欢迎仪式.txt");
|
||||||
|
if (!f.exists()) {
|
||||||
|
writeDefault(f);
|
||||||
|
}
|
||||||
|
try (BufferedReader r =
|
||||||
|
new BufferedReader(
|
||||||
|
new InputStreamReader(
|
||||||
|
new FileInputStream(f),
|
||||||
|
StandardCharsets
|
||||||
|
.UTF_8))) {
|
||||||
|
String section = "";
|
||||||
|
String line;
|
||||||
|
while ((line = r.readLine()) != null) {
|
||||||
|
if (line.startsWith("\uFEFF"))
|
||||||
|
line = line.substring(1);
|
||||||
|
String t = line.trim();
|
||||||
|
if (t.isEmpty()
|
||||||
|
|| t.startsWith("#"))
|
||||||
|
continue;
|
||||||
|
if (t.equals("注册")
|
||||||
|
|| t.equals("注册:")) {
|
||||||
|
section = "register";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t.equals("登录")
|
||||||
|
|| t.equals("登录:")) {
|
||||||
|
section = "login";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 支持 = 和 : 两种分隔
|
||||||
|
if (section.isEmpty()) {
|
||||||
|
if (t.startsWith("注册")
|
||||||
|
&& (t.contains("=")
|
||||||
|
|| t.contains(":"))) {
|
||||||
|
int idx = t.indexOf('=');
|
||||||
|
int idx2 = t.indexOf(':');
|
||||||
|
if (idx < 0) idx = idx2;
|
||||||
|
else if (idx2 >= 0)
|
||||||
|
idx = Math.min(idx, idx2);
|
||||||
|
String v = t.substring(idx + 1)
|
||||||
|
.trim();
|
||||||
|
if (!v.isEmpty())
|
||||||
|
registerMsgs.add(
|
||||||
|
colorize(v));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t.startsWith("登录")
|
||||||
|
&& (t.contains("=")
|
||||||
|
|| t.contains(":"))) {
|
||||||
|
int idx = t.indexOf('=');
|
||||||
|
int idx2 = t.indexOf(':');
|
||||||
|
if (idx < 0) idx = idx2;
|
||||||
|
else if (idx2 >= 0)
|
||||||
|
idx = Math.min(idx, idx2);
|
||||||
|
String v = t.substring(idx + 1)
|
||||||
|
.trim();
|
||||||
|
if (!v.isEmpty())
|
||||||
|
loginMsgs.add(
|
||||||
|
colorize(v));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!t.isEmpty()) {
|
||||||
|
if (section.equals("register"))
|
||||||
|
registerMsgs.add(
|
||||||
|
colorize(t));
|
||||||
|
else
|
||||||
|
loginMsgs.add(
|
||||||
|
colorize(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe(
|
||||||
|
"[欢迎] 加载失败: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[欢迎] 加载完成: 注册="
|
||||||
|
+ registerMsgs.size()
|
||||||
|
+ " 登录=" + loginMsgs.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRegister(Player p) {
|
||||||
|
sendRandom(p, registerMsgs, "注册");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onLogin(Player p) {
|
||||||
|
sendRandom(p, loginMsgs, "登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendRandom(Player p,
|
||||||
|
List<String> msgs, String tag) {
|
||||||
|
if (msgs.isEmpty()) return;
|
||||||
|
String msg = msgs.get(
|
||||||
|
random.nextInt(msgs.size()));
|
||||||
|
msg = msg.replace("{player}",
|
||||||
|
p.getName());
|
||||||
|
final String finalMsg = msg;
|
||||||
|
Bukkit.getScheduler()
|
||||||
|
.runTaskLater(plugin,
|
||||||
|
() -> Bukkit
|
||||||
|
.broadcastMessage(
|
||||||
|
finalMsg), 10L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String colorize(String msg) {
|
||||||
|
return msg.replace("&", "\u00a7");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDefault(File f) {
|
||||||
|
try (PrintWriter pw = new PrintWriter(
|
||||||
|
new OutputStreamWriter(
|
||||||
|
new FileOutputStream(f),
|
||||||
|
StandardCharsets
|
||||||
|
.UTF_8))) {
|
||||||
|
pw.println("# 欢迎仪式配置");
|
||||||
|
pw.println("# &为颜色代码");
|
||||||
|
pw.println("# {player} = 玩家名");
|
||||||
|
pw.println();
|
||||||
|
pw.println("注册:");
|
||||||
|
pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]");
|
||||||
|
pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]");
|
||||||
|
pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]");
|
||||||
|
pw.println();
|
||||||
|
pw.println("登录:");
|
||||||
|
pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]");
|
||||||
|
pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]");
|
||||||
|
pw.println("&b&l{player} &b回来了!&7[登录]");
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
name: Sdf1_login
|
||||||
|
version: 1.0
|
||||||
|
main: Sdf1_login.Main
|
||||||
|
api-version: '1.21'
|
||||||
|
commands:
|
||||||
|
sdf1_login:
|
||||||
|
description: Sdf1_login主命令
|
||||||
|
usage: /sdf1_login
|
||||||
|
reg:
|
||||||
|
description: 注册
|
||||||
|
usage: /reg <密码>
|
||||||
|
login:
|
||||||
|
description: 登录
|
||||||
|
usage: /login <密码>
|
||||||
|
l:
|
||||||
|
description: 登录快捷版
|
||||||
|
usage: /l <密码>
|
||||||
|
注册:
|
||||||
|
description: 注册(中文)
|
||||||
|
usage: /注册 <密码>
|
||||||
|
登录:
|
||||||
|
description: 登录(中文)
|
||||||
|
usage: /登录 <密码>
|
||||||
|
玩家信息:
|
||||||
|
description: 打开面板(中文)
|
||||||
|
usage: /玩家信息
|
||||||
|
改密码:
|
||||||
|
description: 修改密码(中文)
|
||||||
|
usage: /改密码 <旧密码> <新密码> <确认新密码>
|
||||||
|
设置邮箱:
|
||||||
|
description: 设置邮箱(中文)
|
||||||
|
usage: /设置邮箱 <邮箱地址>
|
||||||
|
撤销:
|
||||||
|
description: 撤销操作(中文)
|
||||||
|
usage: /撤销
|
||||||
|
删除:
|
||||||
|
description: 删除玩家(中文)
|
||||||
|
usage: /删除 <玩家> <密码>
|
||||||
|
找回密码:
|
||||||
|
description: 找回密码(中文)
|
||||||
|
usage: /找回密码
|
||||||
|
签到:
|
||||||
|
description: 每日签到(中文)
|
||||||
|
usage: /签到
|
||||||
|
#oa:
|
||||||
|
# description: 申请删除账号
|
||||||
|
# usage: /oa [玩家名]
|
||||||
|
删除账号:
|
||||||
|
description: 申请删除账号(中文)
|
||||||
|
usage: /删除账号 [玩家名]
|
||||||
|
通过:
|
||||||
|
description: 批准OA请求(中文)
|
||||||
|
usage: /通过 <编号>
|
||||||
|
拒绝:
|
||||||
|
description: 拒绝OA请求(中文)
|
||||||
|
usage: /拒绝 <编号> [理由]
|
||||||
|
accept:
|
||||||
|
description: 批准OA请求
|
||||||
|
usage: /accept <编号>
|
||||||
|
deny:
|
||||||
|
description: 拒绝OA请求
|
||||||
|
usage: /deny <编号> [理由]
|
||||||
|
email:
|
||||||
|
description: 绑定邮箱
|
||||||
|
usage: /email <邮箱>
|
||||||
|
绑定:
|
||||||
|
description: 绑定邮箱(中文)
|
||||||
|
usage: /绑定 <邮箱>
|
||||||
|
绑定邮箱:
|
||||||
|
description: 绑定邮箱
|
||||||
|
usage: /绑定邮箱
|
||||||
|
chat:
|
||||||
|
description: 聊天过滤管理
|
||||||
|
usage: /chat
|
||||||
|
recycle:
|
||||||
|
description: 打开垃圾回收站
|
||||||
|
usage: /recycle
|
||||||
|
垃圾清理:
|
||||||
|
description: 手动清理掉落物
|
||||||
|
usage: /垃圾清理
|
||||||
|
menu:
|
||||||
|
description: 雪球菜单开关控制
|
||||||
|
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
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class CheckInManager {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
public CheckInManager(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 今日是否已签到 =====
|
||||||
|
public boolean isCheckedInToday(
|
||||||
|
String name) {
|
||||||
|
String last = (String) plugin.getDb()
|
||||||
|
.getField(name,
|
||||||
|
"last_checkin_date");
|
||||||
|
String today = new SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd")
|
||||||
|
.format(new Date());
|
||||||
|
return today.equals(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 签到 =====
|
||||||
|
// ===== 签到 =====
|
||||||
|
public String checkIn(Player p) {
|
||||||
|
String name = p.getName();
|
||||||
|
if (isCheckedInToday(name)) {
|
||||||
|
return plugin.getConfig2()
|
||||||
|
.msg("checkin_already");
|
||||||
|
}
|
||||||
|
|
||||||
|
Object streakObj = plugin.getDb()
|
||||||
|
.getField(name, "checkin_streak");
|
||||||
|
int streak = streakObj != null
|
||||||
|
? ((Number) streakObj).intValue()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
try {
|
||||||
|
java.util.Date lastD =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd")
|
||||||
|
.parse(lastDate);
|
||||||
|
java.util.Date todayD =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd")
|
||||||
|
.parse(today);
|
||||||
|
long diff = todayD.getTime()
|
||||||
|
- lastD.getTime();
|
||||||
|
if (diff > 86400000L * 2) {
|
||||||
|
// 间隔超过2天,重置连续天数
|
||||||
|
streak = 1;
|
||||||
|
} else if (diff > 86400000L) {
|
||||||
|
// 间隔1-2天(连续签到),天数+1
|
||||||
|
streak++;
|
||||||
|
}
|
||||||
|
// 间隔<=1天(同一天),不改变连续天数
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 首次签到
|
||||||
|
streak = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double multi = 1.0;
|
||||||
|
if (streak >= 21) multi = 1.5;
|
||||||
|
else if (streak >= 14) multi = 1.3;
|
||||||
|
else if (streak >= 7) multi = 1.1;
|
||||||
|
|
||||||
|
int basePoints =
|
||||||
|
plugin.getConfig2().checkinPoints;
|
||||||
|
int points = (int) (basePoints * multi);
|
||||||
|
|
||||||
|
plugin.getDb().addPoints(name, points);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"checkin_streak", streak);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"last_checkin_date", today);
|
||||||
|
|
||||||
|
Object totalObj = plugin.getDb()
|
||||||
|
.getField(name, "total_checkin_days");
|
||||||
|
int total = totalObj != null
|
||||||
|
? ((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")
|
||||||
|
.replace("{points}",
|
||||||
|
String.valueOf(points))
|
||||||
|
.replace("{streak}",
|
||||||
|
String.valueOf(streak))
|
||||||
|
.replace("{multi}",
|
||||||
|
String.valueOf(multi))
|
||||||
|
+ " §e+" + bondReward + "债券";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 获取连续天数 =====
|
||||||
|
public int getStreak(String name) {
|
||||||
|
Object val = plugin.getDb()
|
||||||
|
.getField(name,
|
||||||
|
"checkin_streak");
|
||||||
|
if (val instanceof Number) {
|
||||||
|
return ((Number) val)
|
||||||
|
.intValue();
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
String name = p.getName();
|
||||||
|
String normalized =
|
||||||
|
normalizeDate(dateStr);
|
||||||
|
|
||||||
|
// 严格日期验证
|
||||||
|
java.text.SimpleDateFormat sdf =
|
||||||
|
new java.text.SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd");
|
||||||
|
sdf.setLenient(false);
|
||||||
|
java.util.Date target;
|
||||||
|
try {
|
||||||
|
target = sdf.parse(normalized);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return "§c无效日期: " + dateStr;
|
||||||
|
}
|
||||||
|
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今天不需要补签";
|
||||||
|
}
|
||||||
|
|
||||||
|
String threeDaysAgo = getPastDate(3);
|
||||||
|
if (normalized.compareTo(
|
||||||
|
threeDaysAgo) < 0) {
|
||||||
|
return "§c只能补签3天内"
|
||||||
|
+ "(最多到 " + threeDaysAgo + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object val = plugin.getDb()
|
||||||
|
.getField(name, "backcheck_dates");
|
||||||
|
String existing = val != null
|
||||||
|
? val.toString() : "";
|
||||||
|
boolean alreadyBacked = false;
|
||||||
|
if (!existing.isEmpty()) {
|
||||||
|
for (String d : existing.split(",")) {
|
||||||
|
if (normalizeDate(d.trim())
|
||||||
|
.equals(normalized)) {
|
||||||
|
alreadyBacked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alreadyBacked) {
|
||||||
|
return "§c该日期已补签: " + normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object normalCheck =
|
||||||
|
plugin.getDb().getField(
|
||||||
|
name, "last_checkin_date");
|
||||||
|
String lastCheckin = normalCheck != null
|
||||||
|
? normalCheck.toString() : "";
|
||||||
|
if (lastCheckin.equals(normalized)) {
|
||||||
|
return "§c该日期已签到: " + normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 补签消耗:3债券 + 10积分
|
||||||
|
// 积分不够时:用 9债券(3倍)代替 ★
|
||||||
|
int bondCost = 3;
|
||||||
|
int pointCost = 10;
|
||||||
|
int bondAltCost = 9; // 3倍债券替代
|
||||||
|
|
||||||
|
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 + "积分";
|
||||||
|
}
|
||||||
|
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消耗: " + costMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===== 执行补签 =====
|
||||||
|
// ===== 执行补签 =====
|
||||||
|
public void doBackCheckIn(
|
||||||
|
Player p, String dateStr) {
|
||||||
|
String name = p.getName();
|
||||||
|
Object val = plugin.getDb()
|
||||||
|
.getField(name, "backcheck_dates");
|
||||||
|
String existing = val != null
|
||||||
|
? val.toString() : "";
|
||||||
|
String updated = existing.isEmpty()
|
||||||
|
? dateStr
|
||||||
|
: existing + "," + dateStr;
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"backcheck_dates", updated);
|
||||||
|
int streak = getStreak(name);
|
||||||
|
plugin.getDb().setField(name,
|
||||||
|
"checkin_streak", streak + 1);
|
||||||
|
// ★ 补签不发放奖励,只记录
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 获取N天前日期 =====
|
||||||
|
private String getPastDate(
|
||||||
|
int daysAgo) {
|
||||||
|
Calendar cal =
|
||||||
|
Calendar.getInstance();
|
||||||
|
cal.add(Calendar.DAY_OF_MONTH,
|
||||||
|
-daysAgo);
|
||||||
|
return new SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd")
|
||||||
|
.format(cal.getTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
public class QuestStorage {
|
||||||
|
|
||||||
|
private Connection conn;
|
||||||
|
|
||||||
|
public QuestStorage(File dataFolder) {
|
||||||
|
try {
|
||||||
|
Class.forName("org.sqlite.JDBC");
|
||||||
|
File dbFile = new File(
|
||||||
|
dataFolder,
|
||||||
|
"quest_data.db");
|
||||||
|
conn = DriverManager.getConnection(
|
||||||
|
"jdbc:sqlite:"
|
||||||
|
+ dbFile.getPath());
|
||||||
|
initTable();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTable()
|
||||||
|
throws SQLException {
|
||||||
|
Statement stmt =
|
||||||
|
conn.createStatement();
|
||||||
|
stmt.executeUpdate(
|
||||||
|
"CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "quest_conditions ("
|
||||||
|
+ "player TEXT NOT NULL, "
|
||||||
|
+ "condition_key TEXT NOT NULL, "
|
||||||
|
+ "value TEXT DEFAULT '1', "
|
||||||
|
+ "timestamp INTEGER, "
|
||||||
|
+ "PRIMARY KEY "
|
||||||
|
+ "(player, condition_key))");
|
||||||
|
stmt.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记条件已完成(value = "1")
|
||||||
|
*/
|
||||||
|
public void setCondition(
|
||||||
|
String player,
|
||||||
|
String condition) {
|
||||||
|
setConditionRaw(
|
||||||
|
player, condition, "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入任意值(计数器等)
|
||||||
|
*/
|
||||||
|
public void setConditionRaw(
|
||||||
|
String player,
|
||||||
|
String condition,
|
||||||
|
String value) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
conn.prepareStatement(
|
||||||
|
"INSERT OR REPLACE "
|
||||||
|
+ "INTO "
|
||||||
|
+ "quest_conditions "
|
||||||
|
+ "(player, "
|
||||||
|
+ "condition_key, "
|
||||||
|
+ "value, timestamp) "
|
||||||
|
+ "VALUES "
|
||||||
|
+ "(?, ?, ?, ?)");
|
||||||
|
ps.setString(1, player);
|
||||||
|
ps.setString(2, condition);
|
||||||
|
ps.setString(3, value);
|
||||||
|
ps.setLong(4,
|
||||||
|
System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询条件值
|
||||||
|
*/
|
||||||
|
public Object getCondition(
|
||||||
|
String player,
|
||||||
|
String condition) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
conn.prepareStatement(
|
||||||
|
"SELECT value FROM "
|
||||||
|
+ "quest_conditions "
|
||||||
|
+ "WHERE player = ? "
|
||||||
|
+ "AND "
|
||||||
|
+ "condition_key = ?");
|
||||||
|
ps.setString(1, player);
|
||||||
|
ps.setString(2, condition);
|
||||||
|
ResultSet rs =
|
||||||
|
ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
String val =
|
||||||
|
rs.getString("value");
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
// 尝试解析为数字
|
||||||
|
try {
|
||||||
|
if (val.contains(".")) {
|
||||||
|
return Double.parseDouble(
|
||||||
|
val);
|
||||||
|
}
|
||||||
|
return Long.parseLong(val);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (conn != null
|
||||||
|
&& !conn.isClosed()) {
|
||||||
|
conn.close();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
# Sdf1系列登录插件使用说明
|
||||||
|
|
||||||
|
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
|
||||||
|
|
||||||
|
## 1. 功能介绍
|
||||||
|
|
||||||
|
| 名字 | 描述/作用 | 施工状态 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| 登录 | 用户登录/注册 | 已完成 |
|
||||||
|
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
||||||
|
| 工单系统 | 用户提交工单 | 已完成 |
|
||||||
|
| 签到系统 | 签到积分换奖励 | 已完成 |
|
||||||
|
| 新人礼包 | 新玩家7日礼包 | 已完成 |
|
||||||
|
| 挂机检测 | 挂机检测踢出 | 已完成 |
|
||||||
|
| 自定义菜单 | 可以自己diy指令 | 已完成 |
|
||||||
|
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
|
||||||
|
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
|
||||||
|
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
|
||||||
|
|
||||||
|
|
||||||
|
## 2.安装说明:
|
||||||
|
|
||||||
|
| 前置插件 | 选装类型 | 功能作用 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| [CY_beibao](https://gitee.com/nihaoshidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
|
||||||
|
| EssentialsX | 可选 | 主流经济系统
|
||||||
|
| [sdf1](https://gitee.com/nihaoshidifu/sdf1_plugn) | 可选,推荐 | CDK兑换系统
|
||||||
|
|
||||||
|
|
||||||
|
### 2.1 下载
|
||||||
|
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||||
|
|
||||||
|
### 2.2. 配置文件
|
||||||
|
|
||||||
|
#### 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>`进行段落换行
|
||||||
|
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ==========================================
|
||||||
|
# Sdf1_login 消息配置文件
|
||||||
|
# ==========================================
|
||||||
|
# 颜色代码使用 § 符号
|
||||||
|
# §0黑色 §1深蓝 §2深绿 §3深青
|
||||||
|
# §4深红 §5深紫 §6金色 §7灰色
|
||||||
|
# §8深灰 §9蓝色 §a绿色 §b浅蓝
|
||||||
|
# §c红色 §d粉色 §e黄色 §f白色
|
||||||
|
# §l粗体 §n下划线 §o斜体 §r重置
|
||||||
|
#
|
||||||
|
# 占位符用 {xxx} 表示,运行时自动替换
|
||||||
|
# 支持 <b>粗体</b> 标签
|
||||||
|
# 支持 \n 或 <br> 换行
|
||||||
|
# 修改后执行 /sdf1_login reload 重载
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# ---- 登录注册相关 ----
|
||||||
|
# login_timeout: 登录超时被踢出时的提示
|
||||||
|
login_timeout=§c登录超时,请重新登录
|
||||||
|
# not_registered: 未注册玩家收到的提示
|
||||||
|
not_registered=§e您尚未注册,请使用 /reg <密码>
|
||||||
|
# not_logged_in: 已注册但未登录的提示
|
||||||
|
not_logged_in=§e您尚未登录,请使用 /login <密码>
|
||||||
|
# already_registered: 已注册玩家再次注册时的提示
|
||||||
|
already_registered=§c您已注册过账号
|
||||||
|
# already_logged_in: 已登录玩家再次登录时的提示
|
||||||
|
already_logged_in=§c您已登录
|
||||||
|
# reg_success: 注册成功提示
|
||||||
|
reg_success=§a注册成功!欢迎来到服务器
|
||||||
|
# login_success: 登录成功提示
|
||||||
|
login_success=§a登录成功!
|
||||||
|
# login_failed: 密码错误提示
|
||||||
|
login_failed=§c密码错误,请重试
|
||||||
|
|
||||||
|
# ---- 密码相关 ----
|
||||||
|
# password_wrong: 旧密码验证失败
|
||||||
|
password_wrong=§c密码错误
|
||||||
|
# password_changed: 密码修改成功
|
||||||
|
password_changed=§a密码修改成功!
|
||||||
|
# password_format_error: 密码格式不符合要求
|
||||||
|
password_format_error=§c密码需6位以上,含大小写字母和数字
|
||||||
|
# password_same: 新密码与旧密码相同
|
||||||
|
password_same=§c新密码不能与旧密码相同
|
||||||
|
# reset_no_email: 找回密码时未绑定邮箱
|
||||||
|
reset_no_email=§c您未绑定邮箱,无法找回密码
|
||||||
|
# reset_sent: 临时密码已发送到邮箱
|
||||||
|
reset_sent=§a临时密码已发送到您的邮箱,请查收
|
||||||
|
|
||||||
|
# ---- 签到积分相关 ----
|
||||||
|
# checkin_already: 今日已签到
|
||||||
|
checkin_already=§c您今日已签到
|
||||||
|
# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率
|
||||||
|
checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}
|
||||||
|
# points_insufficient: 积分不足
|
||||||
|
points_insufficient=§c积分不足,当前: {points}
|
||||||
|
# points_purchase_success: 积分购买成功
|
||||||
|
points_purchase_success=§a购买成功!消耗 {count} 积分
|
||||||
|
|
||||||
|
# ---- 挂机检测相关 ----
|
||||||
|
# afk_set_enabled: 挂机检测开启
|
||||||
|
afk_set_enabled=§a挂机检测已开启
|
||||||
|
# afk_set_disabled: 挂机检测关闭
|
||||||
|
afk_set_disabled=§c挂机检测已关闭
|
||||||
|
# afk_kick: 因挂机被踢出
|
||||||
|
afk_kick=§c因挂机过久被踢出
|
||||||
|
|
||||||
|
# ---- 邮箱相关 ----
|
||||||
|
# email_set: 邮箱设置成功
|
||||||
|
email_set=§a邮箱设置成功:{email}
|
||||||
|
# email_format_error: 邮箱格式错误
|
||||||
|
email_format_error=§c邮箱格式不正确
|
||||||
|
|
||||||
|
# ---- 邀请相关 ----
|
||||||
|
# invite_code_generated: 邀请码生成
|
||||||
|
invite_code_generated=§a邀请码:{code}
|
||||||
|
# invite_used: 使用邀请码成功
|
||||||
|
invite_used=§a邀请成功,邀请人:{player}
|
||||||
|
# invite_self: 使用自己的邀请码
|
||||||
|
invite_self=§c不能使用自己的邀请码
|
||||||
|
# invite_not_found: 邀请码不存在
|
||||||
|
invite_not_found=§c邀请码不存在
|
||||||
|
# invite_referral_bonus: 被邀请人签到后邀请人获得奖励
|
||||||
|
invite_referral_bonus=§a邀请奖励!获得 {points} 积分
|
||||||
|
|
||||||
|
# ---- 新人礼包相关 ----
|
||||||
|
# gift_not_ready: 礼包条件未满足
|
||||||
|
gift_not_ready=§c礼包条件未满足
|
||||||
|
# gift_claimed: 礼包领取成功
|
||||||
|
gift_claimed=§a礼包领取成功!
|
||||||
|
|
||||||
|
# ---- 审批与IP限制 ----
|
||||||
|
# need_approval: 注册需审批
|
||||||
|
need_approval=§e注册需审批 (工单#{id})
|
||||||
|
# ip_max_accounts: IP注册已达上限
|
||||||
|
ip_max_accounts=§cIP注册已达上限
|
||||||
|
|
||||||
|
# ---- 管理员操作 ----
|
||||||
|
# admin_delete_confirm: 管理员删除账号确认提示
|
||||||
|
admin_delete_confirm=§c请在聊天中输入玩家名确认删除
|
||||||
|
|
||||||
|
# ---- 聊天过滤 ----
|
||||||
|
# chat_muted: 禁言玩家尝试聊天
|
||||||
|
chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天
|
||||||
|
# chat_url_blocked: 发送违规链接被拦截
|
||||||
|
# {url}=检测到的链接地址
|
||||||
|
chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}
|
||||||
|
# chat_url_violation: 当前违规次数
|
||||||
|
# {count}=当前违规次数
|
||||||
|
chat_url_violation=§7当前违规次数: §e{count}
|
||||||
|
# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数
|
||||||
|
chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)
|
||||||
|
# chat_url_broadcast: 全服通报,{player}=玩家名
|
||||||
|
chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚
|
||||||
|
|
||||||
|
# ---- 通用 ----
|
||||||
|
# 未知参数: 未知命令或权限不足时的提示
|
||||||
|
未知参数=§c未知参数或权限不足
|
||||||
|
# only_player: 仅玩家可用的命令
|
||||||
|
# only_player=§c此命令仅玩家可用
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.2 挂机白名单.txt
|
||||||
|
当挂机踢出开启时,该配置下的玩家不受挂机提出影响
|
||||||
|
|
||||||
|
### 2.2.3 SMTP设置.txt
|
||||||
|
|
||||||
|
此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。<br>可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ===== SMTP 邮件服务器配置 =====
|
||||||
|
smtp地址=smtp.example.com
|
||||||
|
smtp端口=465
|
||||||
|
smtp账号=mail@example.com
|
||||||
|
smtp密码=example.com
|
||||||
|
发件人名称=Sdf1_login
|
||||||
|
smtp加密=true
|
||||||
|
验证码接收邮箱=example@example.com
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.4 插件设置.txt
|
||||||
|
|
||||||
|
此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# ===== Sdf1_login 插件设置 =====
|
||||||
|
管理标签=admin
|
||||||
|
管理密码=qweasd
|
||||||
|
每IP最大账号数=3
|
||||||
|
审批模式=manual
|
||||||
|
自动审批延迟分钟=30
|
||||||
|
登录超时秒数=60
|
||||||
|
挂机踢出=false
|
||||||
|
挂机超时秒数=300
|
||||||
|
服务商积分倍率=2.0
|
||||||
|
基础经济奖励=100
|
||||||
|
经济奖励每评分=50.0
|
||||||
|
报单人积分奖励=10
|
||||||
|
# ---- 签到盲盒奖励 ----
|
||||||
|
签到固定金额=0
|
||||||
|
签到最小金额=10
|
||||||
|
签到最大金额=100
|
||||||
|
签到给积分=true
|
||||||
|
签到积分数量=10
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2.5 chat.txt
|
||||||
|
该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。<br>
|
||||||
|
可用的处罚规则:
|
||||||
|
- warn: 警告
|
||||||
|
- mute: 禁言
|
||||||
|
- kick: 踢出
|
||||||
|
- ban: 封禁
|
||||||
|
- banip:封禁IP
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Sdf1_chat 聊天过滤配置
|
||||||
|
通知管理员: true
|
||||||
|
全服通报: true
|
||||||
|
禁言时长: 300
|
||||||
|
|
||||||
|
白名单:
|
||||||
|
*.minecraft.net
|
||||||
|
baidu.com
|
||||||
|
github.com
|
||||||
|
|
||||||
|
处罚规则:
|
||||||
|
1:warn:0
|
||||||
|
2:warn:0
|
||||||
|
3:mute:300
|
||||||
|
5:mute:600
|
||||||
|
8:kick:0
|
||||||
|
10:ban:0
|
||||||
|
|
||||||
|
白名单玩家:
|
||||||
|
|
||||||
|
```
|
||||||
|
### 2.2.6 radio_config.txt
|
||||||
|
```txt
|
||||||
|
# ============ Sdf1 Radio Config ============
|
||||||
|
#
|
||||||
|
# resource-pack-url
|
||||||
|
# 填外部地址(HTTP或HTTPS均可)
|
||||||
|
# 例: https://example.com/radiopack.zip
|
||||||
|
# 留空 = 使用内置HTTP服务
|
||||||
|
#
|
||||||
|
# http-port
|
||||||
|
# 内置HTTP端口(仅resource-pack-url为空时生效)
|
||||||
|
#
|
||||||
|
# ============ Settings ===============
|
||||||
|
|
||||||
|
resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc
|
||||||
|
http-port=443
|
||||||
|
```
|
||||||
|
- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了
|
||||||
|
|
||||||
|
### 2.2.7 菜单.txt
|
||||||
|
- 自定义菜单配置文件
|
||||||
|
|
||||||
|
```json
|
||||||
|
# 服务器菜单
|
||||||
|
# 使用类json格式
|
||||||
|
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:主城传送
|
||||||
|
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
|
||||||
|
权限类型:玩家
|
||||||
|
图标:COMPASS
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:随机传送
|
||||||
|
指令:/rtp
|
||||||
|
权限类型:玩家
|
||||||
|
图标:ENDER_PEARL
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:擂台比武
|
||||||
|
指令:/dom tp PVP
|
||||||
|
权限类型:玩家
|
||||||
|
图标:netherite_spear
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:领地操作
|
||||||
|
指令:/dom
|
||||||
|
权限类型:玩家
|
||||||
|
图标:paper
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:逛商店
|
||||||
|
指令:/dom tp shop
|
||||||
|
权限类型:玩家
|
||||||
|
图标:honeycomb
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:传送开关
|
||||||
|
指令:/tptoggle
|
||||||
|
权限类型:玩家
|
||||||
|
图标:redstone_torch
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
标题:返回上一个位置
|
||||||
|
指令:/back
|
||||||
|
权限类型:玩家
|
||||||
|
图标:red_bed
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:前往小黑塔
|
||||||
|
指令:/dom tp 小黑塔
|
||||||
|
权限类型:玩家
|
||||||
|
图标:dragon_head
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:云背包
|
||||||
|
指令:/cy
|
||||||
|
权限类型:玩家
|
||||||
|
图标:potato
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:自杀
|
||||||
|
指令:/suicide
|
||||||
|
权限类型:玩家
|
||||||
|
图标:wooden_axe
|
||||||
|
}
|
||||||
|
{
|
||||||
|
标题:游戏商人
|
||||||
|
指令:/shop
|
||||||
|
权限类型:玩家
|
||||||
|
图标:shield
|
||||||
|
格子: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. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
|
||||||
|
<br>
|
||||||
|

|
||||||
|
5. 想查看玩家消费记录?小手一点,明明白白<br>
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2.4 打印流水部分介绍
|
||||||
|
1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天<br>
|
||||||
|
2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 3. 指令参数及介绍
|
||||||
|
|
||||||
|
- Sdf1_login 主指令<br>
|
||||||
|
- Sdf1_login reload 重载配置文件<br>
|
||||||
|
- reg 注册账号<br>
|
||||||
|
- login 登录账号<br>
|
||||||
|
- l 登录账号<br>
|
||||||
|
- sdf1_login del 删除玩家账号<br>
|
||||||
|
- sdf1_login radio 停止播放广播<br>
|
||||||
|
- 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>
|
||||||
|
|
||||||
|
| 指令 | 参数1| 参数2 | 参数3 | 结果 |
|
||||||
|
| :---: | :---: | :---: | :---: | :---: |
|
||||||
|
| protect | 无 | 无 | 无 | 帮助信息 |
|
||||||
|
| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 |
|
||||||
|
| protect | add | 玩家名 | 无| 加白指定玩家为全局 |
|
||||||
|
| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 |
|
||||||
|
| protect | additem | 物品名 | 无 | 加黑指定物品到全局 |
|
||||||
|
| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 |
|
||||||
|
| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 |
|
||||||
|
| protect | remove | 玩家名字 | 无 | 移除玩家为全白 |
|
||||||
|
| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 |
|
||||||
|
| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑
|
||||||
|
| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑
|
||||||
|
| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 |
|
||||||
|
| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 |
|
||||||
|
| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 |
|
||||||
|
|
||||||
|
示范:
|
||||||
|
- `/protect add 末地保护区 players` 添加players玩家到末地保护区<br>
|
||||||
|
- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品<br>
|
||||||
|
- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。<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 违规
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行截图
|
||||||
|
|
||||||
|
主界面
|
||||||
|

|
||||||
|
|
||||||
|
邀请数据
|
||||||
|

|
||||||
|
|
||||||
|
积分商店
|
||||||
|

|
||||||
|
|
||||||
|
任务中心
|
||||||
|

|
||||||
|
|
||||||
|
垃圾箱
|
||||||
|

|
||||||
|
|
||||||
|
管理员面板
|
||||||
|

|
||||||
|
|
||||||
|
管理面板-用户管理
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
工单中心
|
||||||
|

|
||||||
|
|
||||||
|
工单管理
|
||||||
|

|
||||||
|
|
||||||
|
我的工单
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
提交工单
|
||||||
|

|
||||||
|
|
||||||
|
服务商列表
|
||||||
|

|
||||||
|
|
||||||
|
服务商抢单大厅
|
||||||
|

|
||||||
|
|
||||||
|
服务商-接单界面
|
||||||
|

|
||||||
|
|
||||||
|
管理员-工单详情页面
|
||||||
|

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

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

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

|
||||||
|
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 109 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>sdf1</groupId>
|
||||||
|
<artifactId>Sdf1_login</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>1.21.4-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.MilkBowl</groupId>
|
||||||
|
<artifactId>VaultAPI</artifactId>
|
||||||
|
<version>1.7.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.46.0.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>Sdf1_login</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +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 + "秒" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||