Compare commits

...
6 Commits
Author SHA1 Message Date
youpaishidifu ebcb11338c fix: 修复Web注册不写注册IP、玩家登录IP的bug; 新增库存高频轮询; 更新README下载地址 2026-06-22 16:26:17 +08:00
youpaishidifu 8d4731a873 fix: 添加官方仓库下载链接 2026-06-20 14:18:02 +08:00
youpaishidifu 203c9d423a 新增Web CDK接口,可对接Web系统进行兑换 2026-06-20 14:02:41 +08:00
youpaishidifu 2c7f765fb0 初始化登录插件 2026-06-19 16:00:29 +08:00
ypsdf1 8072855d94 Java:新增ip推送端口,后端可以查看玩家ip及归属
php:新增ip属地、实时在线数据,可以查看玩家ip、归属和实时在线情况
2026-06-18 12:37:53 +08:00
ypsdf1 873aca91bf feat: 和平模式修复ENDERMAN/WITHER、自定义菜单事不过三、BaoTa第三路备选
- AreaProtection: HOSTILE_TYPES添加ENDERMAN和WITHER;末影人使用remove()替代teleport()避免自动传送回来
- GUIManager+Main: 宝箱自定义物品存入菜单时事不过三原则(前两次警告,第三次没收)
- UpdateChecker: 添加BaoTa HTML第三路备选通道,GitHub/Gitee限流后自动降级
2026-06-08 13:11:50 +08:00
19 changed files with 6277 additions and 262 deletions
+25
View File
@@ -42,5 +42,30 @@ build/
txt/ txt/
*.xlsx *.xlsx
*.patch *.patch
backup/
新建*.txt 新建*.txt
群戏归类.txt 群戏归类.txt
# Windows 保留设备名(无法用普通 rm 删除)
nul
con
prn
aux
com1
com2
com3
com4
com5
com6
com7
com8
com9
lpt1
lpt2
lpt3
lpt4
lpt5
lpt6
lpt7
lpt8
lpt9
+1 -1
View File
@@ -8,7 +8,7 @@
</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_26" default="true" project-jdk-name="26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+30
View File
@@ -0,0 +1,30 @@
[01:19] Web端交易三个核心问题修复
==================================================
1. 商品ID无效(ACACIA_BOAT)- 已修复
- PHP shop.php 传 item_id="ACACIA_BOAT"(Material名称)
- Java dispatchItemByMaterialOrId() 已支持Material匹配+ID fallback
- displayName获取已修复:优先从ItemMeta.getDisplayName()读取,而不是直接用itemId
2. 扣款变给钱(bond_log符号反了)- 已修复
- BondManager.deductBondsInternal(): logLegacy(player, -amount) → logLegacy(player, amount)
- bond_log表的amount字段现在记录正数,UI根据type判断加减
3. 交易轮询慢(30-90秒)- 已修复
- PHP shop.php 和 cdk.php 在购买/兑换成功后立即POST到Java notify_sync回调
- Java收到notify_sync后调用requestImmediateTransactionPull()
- 最小间隔5秒,避免过度频繁
4. 回调服务器并发问题 - 已修复
- handleCallback()分离notify_sync和register_callback两种请求
- notify_sync不阻塞注册回调处理
5. detail JSON解析 - 已修复
- 支持转义引号\"的嵌套JSON解析
==================================================
下一步:
1. 用IDEA重新编译打包(Maven → Lifecycle → package)
2. 将Sdf1_login.jar复制到MC服务器plugins目录覆盖
3. 重启服务器测试
4. 验证:Web购物→立即到账、CDK兑换→余额同步、扣款方向正确
+2
View File
@@ -565,5 +565,7 @@ http-port=443
## 4. 下载地址 ## 4. 下载地址
- 官方文档:[https://wiki.ypshidifu.cn](https://wiki.ypshidifu.cn) (推荐)
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login) - 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) - Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
+571
View File
@@ -0,0 +1,571 @@
# Sdf1系列登录插件使用说明
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
## 1. 功能介绍
| 名字 | 描述/作用 | 施工状态 |
| :--- | :--- | :--- |
| 登录 | 用户登录/注册 | 已完成 |
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
| 工单系统 | 用户提交工单 | 已完成 |
| 签到系统 | 签到积分换奖励 | 已完成 |
| 新人礼包 | 新玩家7日礼包 | 已完成 |
| 挂机检测 | 挂机检测踢出 | 已完成 |
| 自定义菜单 | 可以自己diy指令 | 已完成 |
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
## 2.安装说明:
| 前置插件 | 选装类型 | 功能作用 |
| :--- | :--- | :--- |
| [CY_beibao](https://github.com/ypsdf1/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
| EssentialsX | 可选 | 主流经济系统
| [sdf1](https://github.com/ypsdf1/sdf1_plugn) | 可选,推荐 | CDK兑换系统
### 2.1 下载
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
### 2.2. 配置文件
#### 2.2.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>
![](img/shop4.png)
4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
![](img/shop7.png)<br>
![](img/shop6.png)
5. 想查看玩家消费记录?小手一点,明明白白<br>
![](img/shop5.png)
## 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>
![](img/menu1.png)
- 将你的自定义物品与雪球交换<br>
![](img/menu2.png)
- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了
## 5. pvp战神榜
- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域<br>
![](img/pvp1.png)
- 并且有着网游同款的击杀通报<br>
![](img/pvp3.png)
- 播报逻辑是:<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 违规
```
## 运行截图
主界面
![](img/login.png)
邀请数据
![](img/login2.png)
积分商店
![](img/login3.png)
任务中心
![](img/login4.png)
垃圾箱
![](img/login5.png)
管理员面板
![](img/login6.png)
管理面板-用户管理
![](img/login7.png)
工单中心
![](img/login8.png)
工单管理
![](img/login9.png)
我的工单
![](img/login10.png)
提交工单
![](img/login11.png)
服务商列表
![](img/login12.png)
服务商抢单大厅
![](img/login13.png)
服务商-接单界面
![](img/login14.png)
管理员-工单详情页面
![](img/login15.png)
商城主界面
![](img/shop1.png)
商城购物车
![](img/cart1.png)
商城退单
![](img/shop2.png)
## 4. 下载地址
- 官方仓库:[https://git.ypshidifu.cn/youpaishidifu/Sdf1_login](https://git.ypshidifu.cn/youpaishidifu/Sdf1_login) (推荐)
- 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)
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
+5 -4
View File
@@ -11,7 +11,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<java.version>21</java.version> <java.version>25</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -55,10 +55,11 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version> <version>3.14.0</version>
<configuration> <configuration>
<source>21</source> <source>25</source>
<target>21</target> <target>25</target>
<encoding>UTF-8</encoding>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
+60 -60
View File
@@ -1494,7 +1494,12 @@ public class AreaProtection implements Listener {
// 没名字或不在白名单,传送虚空 // 没名字或不在白名单,传送虚空
Location voidLoc = ent.getLocation().clone(); Location voidLoc = ent.getLocation().clone();
voidLoc.setY(ent.getWorld().getMinHeight() - 50); voidLoc.setY(ent.getWorld().getMinHeight() - 50);
// 末影人受伤害会自动传送回来,所以直接移除实体
if ("ENDERMAN".equals(ent.getType().name())) {
ent.remove();
} else {
ent.teleport(voidLoc); ent.teleport(voidLoc);
}
banished++; banished++;
/* plugin.getLogger().info("[和平] " /* plugin.getLogger().info("[和平] "
+ ent.getType().name() + ent.getType().name()
@@ -1518,7 +1523,12 @@ public class AreaProtection implements Listener {
// 没有保护期且不在白名单,传送虚空 // 没有保护期且不在白名单,传送虚空
Location voidLoc = ent.getLocation().clone(); Location voidLoc = ent.getLocation().clone();
voidLoc.setY(ent.getWorld().getMinHeight() - 50); voidLoc.setY(ent.getWorld().getMinHeight() - 50);
// 末影人受伤害会自动传送回来,直接移除
if ("ENDERMAN".equals(ent.getType().name())) {
ent.remove();
} else {
ent.teleport(voidLoc); ent.teleport(voidLoc);
}
banished++; banished++;
/* plugin.getLogger().info("[和平] " /* plugin.getLogger().info("[和平] "
+ ent.getType().name() + ent.getType().name()
@@ -1544,7 +1554,8 @@ public class AreaProtection implements Listener {
"ILLUSIONER", "VEX", "ILLUSIONER", "VEX",
"PHANTOM", "SHULKER", "WARDEN", "BREEZE", "PHANTOM", "SHULKER", "WARDEN", "BREEZE",
"PIGLIN", "PIGLIN_BRUTE", "ZOGLIN", "PIGLIN", "PIGLIN_BRUTE", "ZOGLIN",
"ZOMBIFIED_PIGLIN", "ENDER_DRAGON" "ZOMBIFIED_PIGLIN", "ENDER_DRAGON",
"ENDERMAN", "WITHER"
)); ));
private boolean isHostile(String typeName) { private boolean isHostile(String typeName) {
@@ -1628,15 +1639,42 @@ public class AreaProtection implements Listener {
&& z >= minZ && z <= maxZ; && z >= minZ && z <= maxZ;
} }
// ==================== 白名单检查 ==================== // ===== 统一白名单检查(供所有事件处理器使用)=====
/**
* 检查玩家是否在任意白名单中(全局白名单 + 区域白名单)
* 这是所有白名单判断的唯一入口
*/
private boolean isPlayerWhitelisted(String player, private boolean isPlayerWhitelisted(String player,
AreaConfig ac) { AreaConfig ac) {
if (globalPlayerWhitelist.contains( String lowerName = player.toLowerCase();
player.toLowerCase())) // 1. 全局白名单
if (globalPlayerWhitelist.contains(lowerName))
return true; return true;
// 2. 区域白名单
if (ac != null) {
Set<String> areaList = Set<String> areaList =
areaPlayerWhitelist.get(ac.name); areaPlayerWhitelist.get(ac.name);
if (areaList != null
&& areaList.contains(lowerName))
return true;
}
return false;
}
/**
* 快速检查:仅全局白名单(不带 AreaConfig)
*/
private boolean isGlobalWhite(String player) {
return globalPlayerWhitelist.contains(player.toLowerCase());
}
/**
* 快速检查:仅区域白名单
*/
private boolean isAreaWhite(String player, String areaName) {
Set<String> areaList =
areaPlayerWhitelist.get(areaName);
return areaList != null return areaList != null
&& areaList.contains(player.toLowerCase()); && areaList.contains(player.toLowerCase());
} }
@@ -2403,14 +2441,15 @@ public class AreaProtection implements Listener {
private boolean isPlayerExemptFromModeChange( private boolean isPlayerExemptFromModeChange(
Player p, AreaConfig ac) { Player p, AreaConfig ac) {
// if (p.isOp()) return true; // if (p.isOp()) return true;
Set<String> global = globalWhitelist.get("global"); String lower = p.getName().toLowerCase();
if (global != null // 1. 全局白名单
&& global.contains(p.getName().toLowerCase())) { if (globalPlayerWhitelist.contains(lower)) {
return true; return true;
} }
Set<String> areaWl = areaWhitelist.get(ac.name); // 2. 区域白名单
Set<String> areaWl = areaPlayerWhitelist.get(ac.name);
if (areaWl != null if (areaWl != null
&& areaWl.contains(p.getName().toLowerCase())) { && areaWl.contains(lower)) {
return true; return true;
} }
if (ac.modeExempt.contains(p.getName())) { if (ac.modeExempt.contains(p.getName())) {
@@ -4438,20 +4477,11 @@ public class AreaProtection implements Listener {
if (!(e.getDamager() instanceof Player)) return; if (!(e.getDamager() instanceof Player)) return;
Player p = (Player) e.getDamager(); Player p = (Player) e.getDamager();
Set<String> global = globalWhitelist.get("global"); // ★ 统一白名单检查(globalPlayerWhitelist + areaPlayerWhitelist)
if (global != null
&& global.contains(p.getName().toLowerCase())) {
return;
}
AreaConfig ac = findFrameArea(entity); AreaConfig ac = findFrameArea(entity);
if (ac == null || !ac.denyItemFrame) return; if (ac == null || !ac.denyItemFrame) return;
Set<String> areaWl = areaWhitelist.get(ac.name); if (isPlayerWhitelisted(p.getName(), ac)) return;
if (areaWl != null
&& areaWl.contains(p.getName().toLowerCase())) {
return;
}
e.setCancelled(true); e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); p.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
@@ -4487,22 +4517,12 @@ public class AreaProtection implements Listener {
Player p = e.getPlayer(); Player p = e.getPlayer();
// 全局白放行 // ★ 使用统一的白名单检查方法(globalPlayerWhitelist + areaPlayerWhitelist)
Set<String> global = globalWhitelist.get("global");
if (global != null
&& global.contains(p.getName().toLowerCase())) {
return;
}
AreaConfig ac = findFrameArea(clicked); AreaConfig ac = findFrameArea(clicked);
if (ac == null || !ac.denyItemFrame) return; if (ac == null || !ac.denyItemFrame) return;
// 区域白放行 // 统一白名单检查
Set<String> areaWl = areaWhitelist.get(ac.name); if (isPlayerWhitelisted(p.getName(), ac)) return;
if (areaWl != null
&& areaWl.contains(p.getName().toLowerCase())) {
return;
}
// 非白名单 → 拦截 // 非白名单 → 拦截
// 记录当前旋转角度 // 记录当前旋转角度
@@ -4553,22 +4573,13 @@ public class AreaProtection implements Listener {
if (!(e.getDamager() instanceof Player)) return; if (!(e.getDamager() instanceof Player)) return;
Player p = (Player) e.getDamager(); Player p = (Player) e.getDamager();
// 全局白放行
Set<String> gwl = globalWhitelist.get("global");
if (gwl != null
&& gwl.contains(p.getName().toLowerCase())) {
return;
}
AreaConfig ac = findFrameArea(entity); AreaConfig ac = findFrameArea(entity);
if (ac == null || !ac.denyItemFrame) return; if (ac == null) return;
// 区域白放行 // ★ 使用统一的白名单检查方法
Set<String> awl = areaWhitelist.get(ac.name); if (isPlayerWhitelisted(p.getName(), ac)) return;
if (awl != null
&& awl.contains(p.getName().toLowerCase())) { if (!ac.denyItemFrame) return;
return;
}
e.setCancelled(true); e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); p.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
@@ -4627,22 +4638,11 @@ public class AreaProtection implements Listener {
return; return;
} }
// 全局白放行 // ★ 统一白名单检查(globalPlayerWhitelist + areaPlayerWhitelist)
Set<String> gwl = globalWhitelist.get("global");
if (gwl != null
&& gwl.contains(shooter.getName().toLowerCase())) {
return;
}
AreaConfig ac = findFrameArea(hit); AreaConfig ac = findFrameArea(hit);
if (ac == null || !ac.denyItemFrame) return; if (ac == null || !ac.denyItemFrame) return;
// 区域白放行 if (isPlayerWhitelisted(shooter.getName(), ac)) return;
Set<String> awl = areaWhitelist.get(ac.name);
if (awl != null
&& awl.contains(shooter.getName().toLowerCase())) {
return;
}
e.setCancelled(true); e.setCancelled(true);
shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
+62 -1
View File
@@ -5,12 +5,33 @@ import org.bukkit.entity.Player;
import java.io.File; import java.io.File;
import java.sql.*; import java.sql.*;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class BondManager { public class BondManager {
private final Main plugin; private final Main plugin;
private Connection db; private Connection db;
// ===== 交易监听器 =====
public interface TransactionListener {
void onTransactionLogged(String playerName, String type, int amount);
}
private final List<TransactionListener> transactionListeners = new CopyOnWriteArrayList<>();
public void addTransactionListener(TransactionListener listener) {
transactionListeners.add(listener);
}
private void notifyTransactionListeners(String playerName, String type, int amount) {
for (TransactionListener listener : transactionListeners) {
try {
listener.onTransactionLogged(playerName, type, amount);
} catch (Exception e) {
plugin.getLogger().warning("[BondManager] 交易监听器异常: " + e.getMessage());
}
}
}
// ===== 交易类型常量 ===== // ===== 交易类型常量 =====
public static final String TX_ADMIN_GIVE = "admin_give"; public static final String TX_ADMIN_GIVE = "admin_give";
public static final String TX_ADMIN_DEDUCT = "admin_deduct"; public static final String TX_ADMIN_DEDUCT = "admin_deduct";
@@ -277,7 +298,8 @@ public class BondManager {
logTransaction(player, type, amount, logTransaction(player, type, amount,
targetPlayer, operator, reason, targetPlayer, operator, reason,
before, after); before, after);
logLegacy(player, -amount); // 记录扣除流水,use positive amount, UI uses type to determine sign
logLegacy(player, amount);
return true; return true;
} }
@@ -363,6 +385,8 @@ public class BondManager {
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); e.printStackTrace();
} }
// ★ 通知监听器:交易已记录
notifyTransactionListeners(player, type, amount);
} }
@@ -696,4 +720,41 @@ public class BondManager {
this.message = message; this.message = message;
} }
} }
// ======================== 同步用:获取指定时间之后的交易 ========================
/**
* 获取指定时间之后的所有交易记录(用于推送到Web端)
* 使用时间而非ID追踪,确保shop_buy合并后的UPDATE记录也能被同步
* @param afterTime 上次同步的最晚时间(毫秒),0表示从头开始
* @return 交易记录列表
*/
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE time > ? "
+ "ORDER BY time ASC "
+ "LIMIT 200");
ps.setLong(1, afterTime);
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_before", rs.getInt("balance_before"));
row.put("balance_after", rs.getInt("balance_after"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return list;
}
} }
@@ -356,6 +356,9 @@ public class ChatInputManager {
state.targetPlayer)) { state.targetPlayer)) {
db.deleteUser( db.deleteUser(
state.targetPlayer); state.targetPlayer);
if (plugin.webManager != null) {
plugin.webManager.deleteWebUser(state.targetPlayer);
}
p.sendMessage("§a已删除 " p.sendMessage("§a已删除 "
+ state.targetPlayer); + state.targetPlayer);
Player tgt = Player tgt =
@@ -390,6 +390,23 @@ public class DatabaseManager {
} }
} }
/**
* 更新玩家密码凭证(salt + hash),用于Web重新注册时同步
*/
public void updatePassword(String name, String hash, String salt) {
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE users SET password_hash = ?, password_salt = ? WHERE player_name = ?");
ps.setString(1, hash);
ps.setString(2, salt);
ps.setString(3, name);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// ========== 临时密码管理 ========== // ========== 临时密码管理 ==========
public void clearTempPassword(String name) { public void clearTempPassword(String name) {
+224 -15
View File
@@ -16,6 +16,7 @@ import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream; import org.bukkit.util.io.BukkitObjectOutputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.concurrent.ConcurrentHashMap;
@@ -49,6 +50,11 @@ public class GUIManager implements Listener {
private final Map<UUID, Integer> userMgmtPages = new HashMap<>(); private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
private static final int UMGMT_PAGE_SIZE = 45; private static final int UMGMT_PAGE_SIZE = 45;
// ★ 宝箱物品拦截计数(事不过三)
private final ConcurrentHashMap<String, Integer>
treasureMenuBlockCount =
new ConcurrentHashMap<>();
public GUIManager(Main plugin) { public GUIManager(Main plugin) {
@@ -393,6 +399,8 @@ public class GUIManager implements Listener {
public void openSubMenu( public void openSubMenu(
Player p, String fileName) { Player p, String fileName) {
// ★ 剥离MC颜色代码(§f、§7等),防止污染文件名
fileName = fileName.replaceAll("\u00A7[0-9a-fk-or]", "").trim();
String menuTitle = String menuTitle =
"§6§l" + fileName "§6§l" + fileName
.replace(".txt", ""); .replace(".txt", "");
@@ -400,21 +408,153 @@ public class GUIManager implements Listener {
null, 54, menuTitle); null, 54, menuTitle);
fillBg(g); fillBg(g);
java.io.File f = new java.io.File( java.io.File dir = plugin.getDataFolder();
plugin.getDataFolder(), fileName); java.io.File f = null;
if (!f.exists()) { String cleanName = fileName.trim();
p.sendMessage("§c文件不存在: "
+ fileName); plugin.getLogger().info(
return; "[Menu] openSubMenu: searching '"
+ cleanName + "' in '"
+ dir.getAbsolutePath() + "'");
// ★ 第1步:listFiles遍历匹配
java.io.File[] allTxt = dir.listFiles(
(d, n) -> n.toLowerCase()
.endsWith(".txt"));
if (allTxt != null) {
plugin.getLogger().info(
"[Menu] 目录.txt文件数: "
+ allTxt.length);
for (java.io.File ff : allTxt) {
plugin.getLogger().info(
"[Menu] > " + ff.getName()
+ " size=" + ff.length());
if (f == null && ff.getName()
.equalsIgnoreCase(cleanName)) {
f = ff;
} }
}
} else {
plugin.getLogger().warning(
"[Menu] listFiles返回null!");
}
// ★ 第2步:直接构造路径
if (f == null) {
f = new java.io.File(dir, cleanName);
plugin.getLogger().info(
"[Menu] 回退构造路径: "
+ f.getAbsolutePath()
+ " exists=" + f.exists()
+ " size=" + f.length());
}
// ★ 第3步:逐个尝试读取方法(不检查exists,直接试)
List<String> lines = null;
if (f != null) {
// 方法1:NIO UTF-8
try { try {
List<String> lines = lines = java.nio.file.Files
java.nio.file.Files .readAllLines(f.toPath(),
.readAllLines(
f.toPath(),
java.nio.charset java.nio.charset
.StandardCharsets .StandardCharsets
.UTF_8); .UTF_8);
plugin.getLogger().info(
"[Menu] UTF-8读取: "
+ lines.size() + "行");
} catch (Throwable t) {
plugin.getLogger().warning(
"[Menu] UTF-8失败: "
+ t.getClass().getSimpleName()
+ ": " + t.getMessage());
}
// 方法2:NIO GBK
if (lines == null || lines.isEmpty()) {
try {
lines = java.nio.file.Files
.readAllLines(f.toPath(),
java.nio.charset
.Charset
.forName("GBK"));
plugin.getLogger().info(
"[Menu] GBK读取: "
+ lines.size() + "行");
} catch (Throwable t) {
plugin.getLogger().warning(
"[Menu] GBK失败: "
+ t.getMessage());
}
}
// 方法3:FileInputStream原始字节
if (lines == null || lines.isEmpty()) {
try {
java.io.FileInputStream fis =
new java.io.FileInputStream(f);
byte[] raw = fis.readAllBytes();
fis.close();
if (raw.length > 0) {
plugin.getLogger().info(
"[Menu] 原始字节: "
+ raw.length + "B");
// 尝试UTF-8解码
String content = new String(raw,
java.nio.charset
.StandardCharsets
.UTF_8);
java.util.List<String> rawLines =
java.util.Arrays.asList(
content.split("\\r?\\n"));
lines = rawLines;
plugin.getLogger().info(
"[Menu] 原始读取: "
+ lines.size() + "行");
}
} catch (Throwable t) {
plugin.getLogger().warning(
"[Menu] 原始读取失败: "
+ t.getMessage());
}
}
}
// ★ 打印前5行调试
if (lines != null && !lines.isEmpty()) {
plugin.getLogger().info(
"[Menu] 最终行数: "
+ lines.size());
for (int i = 0;
i < Math.min(5, lines.size());
i++) {
plugin.getLogger().info(
"[Menu] L" + (i + 1)
+ ": [" + lines.get(i)
+ "]");
}
}
if (lines == null || lines.isEmpty()) {
p.sendMessage("§c§l子菜单读取失败§r §7"
+ fileName);
p.sendMessage("§7文件: "
+ (f != null
? f.getAbsolutePath()
+ " (" + f.length() + "B)"
: "null"));
p.sendMessage("§7存在: "
+ (f != null ? f.exists() : "N/A"));
if (allTxt != null) {
p.sendMessage("§7目录.txt文件:");
for (java.io.File ff : allTxt) {
p.sendMessage("§7 "
+ ff.getName()
+ " (" + ff.length()
+ "B)");
}
}
return;
}
List<String[]> items = List<String[]> items =
new ArrayList<>(); new ArrayList<>();
StringBuilder buf = StringBuilder buf =
@@ -478,11 +618,6 @@ public class GUIManager implements Listener {
it.setItemMeta(im); it.setItemMeta(im);
g.setItem(i, it); g.setItem(i, it);
} }
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 子菜单读取失败: "
+ e.getMessage());
}
g.setItem(49, mkItem( g.setItem(49, mkItem(
Material.ARROW, Material.ARROW,
@@ -1571,6 +1706,11 @@ public class GUIManager implements Listener {
Inventory topInv, Inventory topInv,
ItemStack item, int removeSlot) { ItemStack item, int removeSlot) {
// ★ 拦截宝箱自定义物品(事不过三原则)
if (handleTreasureItemBlock(p, item)) {
return;
}
int amount = item.getAmount(); int amount = item.getAmount();
removeSnowballFromInventory(p); //没收雪球菜单 removeSnowballFromInventory(p); //没收雪球菜单
// 1. 从来源移除 // 1. 从来源移除
@@ -1697,6 +1837,75 @@ public class GUIManager implements Listener {
} }
/** ★ 检测宝箱自定义物品(lore 含 §0§k 或 CUSTOM 标记) */
boolean isTreasureCustomItem(ItemStack item) {
if (item == null) return false;
ItemMeta meta = item.getItemMeta();
if (meta == null) return false;
List<String> lore = meta.getLore();
if (lore == null) return false;
for (String line : lore) {
if (line.contains("\u00a70\u00a7k")
|| line.contains("CUSTOM")) {
return true;
}
}
return false;
}
/**
* 宝箱自定义物品拦截(事不过三原则)
* @param player 玩家
* @param item 要存入的物品
* @return true表示已拦截(调用方应return),false表示未拦截(继续原逻辑)
*/
public boolean handleTreasureItemBlock(Player player, ItemStack item) {
if (!isTreasureCustomItem(item)) {
return false; // 不是宝箱物品,不拦截
}
String key = player.getUniqueId() + ":" + item.getType().name();
int count = treasureMenuBlockCount.getOrDefault(key, 0) + 1;
treasureMenuBlockCount.put(key, count);
if (count < 3) {
// 第1次或第2次:警告
player.sendMessage("§c§l[宝箱物品] §e该物品是宝箱自定义物品,无法存入菜单!");
player.sendMessage("§c§l[宝箱物品] §e第" + count + "/3次警告,第三次将没收物品!");
return true;
} else {
// 第3次:没收物品
// 1. 从玩家背包移除该物品
boolean removed = false;
for (int i = 0; i < player.getInventory().getSize(); i++) {
ItemStack invItem = player.getInventory().getItem(i);
if (invItem != null && invItem.isSimilar(item)) {
player.getInventory().setItem(i, null);
removed = true;
break;
}
}
// 2. 如果背包没找到,可能在光标上(普通点击/拖拽场景)
if (!removed) {
org.bukkit.inventory.InventoryView view = player.getOpenInventory();
ItemStack cursor = view.getCursor();
if (cursor != null && cursor.isSimilar(item)) {
view.setCursor(null);
removed = true;
}
}
// 3. 清零计数器
treasureMenuBlockCount.remove(key);
// 4. 发送没收提示
player.sendMessage("§c§l[宝箱物品] §e你多次尝试存入宝箱物品,物品已被没收!");
player.sendMessage("§c§l[宝箱物品] §c事不过三,这是第三次警告!");
// 5. 记录日志
plugin.getLogger().warning("[宝箱物品] 玩家 " + player.getName() + " 第三次尝试存入宝箱物品,物品已没收: " + item.getType().name());
return true;
}
}
// ===== 辅助方法 ===== // ===== 辅助方法 =====
// 只匹配带[菜单]的非雪球物品 // 只匹配带[菜单]的非雪球物品
@@ -172,6 +172,16 @@ public class LoginManager {
plugin.getDb().setField(name, plugin.getDb().setField(name,
"last_online_check", "last_online_check",
System.currentTimeMillis()); System.currentTimeMillis());
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
if (plugin.webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
plugin.webManager.syncOnlinePlayers();
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
}
});
}
// 登录成功关闭飞行 // 登录成功关闭飞行
p.setAllowFlight(false); p.setAllowFlight(false);
p.setFlying(false); p.setFlying(false);
+328 -111
View File
@@ -1,36 +1,12 @@
package Sdf1_login; package Sdf1_login;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.io.File;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.PlayerInteractEvent;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.Date;
import org.bukkit.command.CommandSender;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionEffect;
import org.bukkit.scheduler.BukkitTask;
import net.milkbowl.vault.economy.Economy; import net.milkbowl.vault.economy.Economy;
import org.bukkit.Bukkit; import org.bukkit.*;
import org.bukkit.Location; import org.bukkit.command.Command;
import org.bukkit.Material; import org.bukkit.command.CommandExecutor;
import org.bukkit.Sound; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@@ -39,28 +15,26 @@ import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.inventory.CraftItemEvent;
import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.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 org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.command.Command; import org.bukkit.potion.PotionEffectType;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.player.PlayerFishEvent;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap; import java.io.File;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class Main extends JavaPlugin public class Main extends JavaPlugin
@@ -100,7 +74,8 @@ public class Main extends JavaPlugin
private final Map<String, Integer> treasureDiscardCount = private final Map<String, Integer> treasureDiscardCount =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
// 在 private WelcomeManager welcome; 字段(若不存在)添加: // 在 private WelcomeManager welcome; 字段(若不存在)添加:
private WelcomeManager welcome; public WelcomeManager welcome;
public WebManager webManager;
private final Set<String> pendingAdminAuth = private final Set<String> pendingAdminAuth =
new TreeSet<>(String.CASE_INSENSITIVE_ORDER); new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
/* /*
@@ -190,6 +165,9 @@ public class Main extends JavaPlugin
//pvp //pvp
private PVPManager pvpManager; private PVPManager pvpManager;
private static class PwdRollback { private static class PwdRollback {
final String hash; final String hash;
final String salt; final String salt;
@@ -386,6 +364,10 @@ public class Main extends JavaPlugin
chatFilter.loadConfig(); chatFilter.loadConfig();
welcome = new WelcomeManager(this); welcome = new WelcomeManager(this);
// ===== 5.5 Web通信管理器 =====
webManager = new WebManager(this);
webManager.start();
// ===== 6. 垃圾箱 ===== // ===== 6. 垃圾箱 =====
garbage = new GarbageManager(this); garbage = new GarbageManager(this);
garbage.init(); garbage.init();
@@ -493,6 +475,12 @@ public class Main extends JavaPlugin
// 13 ====cypay债券==== // 13 ====cypay债券====
// 债券系统(独立DB) // 债券系统(独立DB)
bondManager = new BondManager(this); bondManager = new BondManager(this);
// ★ 注册交易监听器:交易发生后立即同步到Web端
if (webManager != null) {
webManager.registerTransactionListener(bondManager);
}
cdkManager = new CDKManager(this); cdkManager = new CDKManager(this);
userGroupManager = new UserGroupManager(this); userGroupManager = new UserGroupManager(this);
cypayCommand = new CypayCommand(this); cypayCommand = new CypayCommand(this);
@@ -558,7 +546,6 @@ public class Main extends JavaPlugin
updateChecker.checkOnEnable(); updateChecker.checkOnEnable();
getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈"); getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈");
getLogger().info("sdf1系列插件包含:\nsdf1自助兑奖插件\nCY_beibao云背包\nsdf1_login登陆插件\nSdf1_game娱乐游戏插件");
// 大字画 // 大字画
getLogger().info("\n" + getLogger().info("\n" +
" ____ _ __ _ _ _ \n" + " ____ _ __ _ _ _ \n" +
@@ -696,7 +683,9 @@ public class Main extends JavaPlugin
areaProtection.stopEnforceTask(); areaProtection.stopEnforceTask();
areaProtection.saveWhitelists(); areaProtection.saveWhitelists();
areaProtection.getAlreadyForced().clear(); areaProtection.getAlreadyForced().clear();
}
if (webManager != null) {
webManager.shutdown();
} }
} }
@@ -747,13 +736,27 @@ public class Main extends JavaPlugin
while (it.hasNext()) { while (it.hasNext()) {
Map.Entry<UUID, Long> entry = Map.Entry<UUID, Long> entry =
it.next(); it.next();
Player p = Bukkit.getPlayer(
entry.getKey());
if (p == null || !p.isOnline()) {
it.remove();
continue;
}
// ★ 检查Web登录验证状态(在超时踢出前检查)
if (webManager != null && webManager.isWebLoginVerified(p.getName())) {
getLogger().info("[Web登录] 玩家 " + p.getName() + " 已通过Web密码验证(超时检查中),自动登录");
webManager.clearWebLoginVerified(p.getName());
autoLogin(p, "web_password");
it.remove();
continue;
}
if (now - entry.getValue() if (now - entry.getValue()
> config.loginTimeout > config.loginTimeout
* 1000L) { * 1000L) {
Player p = Bukkit.getPlayer( if (!loggedIn.contains(
entry.getKey());
if (p != null && p.isOnline()
&& !loggedIn.contains(
p.getName())) p.getName()))
p.kickPlayer(config.msg( p.kickPlayer(config.msg(
"login_timeout")); "login_timeout"));
@@ -781,6 +784,16 @@ public class Main extends JavaPlugin
it.remove(); it.remove();
continue; continue;
} }
// ★ 检查Web登录验证状态(Java验证成功后立即记录)
if (webManager != null && webManager.isWebLoginVerified(p.getName())) {
getLogger().info("[Web登录] 玩家 " + p.getName() + " 已通过Web密码验证(登录提醒中),自动登录");
webManager.clearWebLoginVerified(p.getName());
autoLogin(p, "web_password");
it.remove();
continue;
}
// 已注册→提示登录,未注册→提示注册 // 已注册→提示登录,未注册→提示注册
if (db.userExists(p.getName())) if (db.userExists(p.getName()))
p.sendMessage(config.msg( p.sendMessage(config.msg(
@@ -1155,6 +1168,17 @@ public class Main extends JavaPlugin
joinTime.remove(p.getUniqueId()); joinTime.remove(p.getUniqueId());
recordIPLogin(p); recordIPLogin(p);
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
if (webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
try {
webManager.syncOnlinePlayers();
} catch (Exception e) {
getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
}
});
}
// 只在有有效备份时恢复 // 只在有有效备份时恢复
restoreInventory(p); restoreInventory(p);
@@ -1172,6 +1196,54 @@ public class Main extends JavaPlugin
} }
/**
* Web端密码验证入口
* PHP后端只做消息转发,密码校验必须在Java本地完成(login.db)
*/
public String handleWebPasswordVerify(String playerName, String password) {
getLogger().info("[Web密码验证] 开始验证 player=" + playerName + " pwdLen=" + (password != null ? password.length() : 0));
if (db.userExists(playerName)) {
getLogger().info("[Web密码验证] 玩家存在,获取盐值");
String salt = (String) db.getField(playerName, "password_salt");
String storedHash = (String) db.getField(playerName, "password_hash");
getLogger().info("[Web密码验证] DB中存储的hash=" + storedHash + " salt=" + salt);
if (salt != null) {
getLogger().info("[Web密码验证] 盐值获取成功,saltLen=" + salt.length());
String hash = PasswordUtils.hash(password, salt);
getLogger().info("[Web密码验证] 计算hash=" + hash + " 存储hash=" + storedHash + " 是否匹配=" + hash.equals(storedHash));
// ★ 与 /login 保持一致:先查主密码,再查临时密码
if (db.checkPassword(playerName, hash)) {
getLogger().info("[Web密码验证] ★ 主密码验证成功");
return "\"success\"";
}
getLogger().info("[Web密码验证] 主密码不匹配,检查临时密码...");
if (db.checkPasswordOrTemp(playerName, hash)) {
getLogger().info("[Web密码验证] ★ 临时密码验证成功");
return "\"success\"";
}
getLogger().info("[Web密码验证] 临时密码也不匹配,密码验证失败");
} else {
getLogger().warning("[Web密码验证] 盐值为null!");
}
} else {
getLogger().warning("[Web密码验证] 玩家不存在: " + playerName);
return "\"not_registered\"";
}
return "\"failed\"";
}
/**
* 处理Web登录确认(Token验证通过后的自动登录)
*/
public boolean handleWebLoginConfirmation(String playerName) {
Player p = Bukkit.getPlayer(playerName);
if (p != null && p.isOnline()) {
autoLogin(p, "web_token");
return true;
}
return false;
}
private void activateBeibao(Player p) { private void activateBeibao(Player p) {
try { try {
org.bukkit.plugin.Plugin cy = org.bukkit.plugin.Plugin cy =
@@ -1653,6 +1725,24 @@ public class Main extends JavaPlugin
Player p = e.getPlayer(); Player p = e.getPlayer();
String name = p.getName(); String name = p.getName();
String ip = getPlayerIP(p); String ip = getPlayerIP(p);
// 登录时自动生成Web登录Token
if (webManager != null) {
webManager.autoGenerateWebLoginToken(p);
// 加入时同步PHP后端的注册数据到Java本地
if (!db.userExists(name)) {
webManager.syncUserOnJoin(name);
}
// ★ 检查本地Web登录验证状态(Java验证成功后立即记录)
if (webManager.isWebLoginVerified(name)) {
getLogger().info("[Web登录] 玩家 " + name + " 已通过Web密码验证,自动登录");
webManager.clearWebLoginVerified(name);
autoLogin(p, "web_password");
return;
}
}
// 强制重发资源包(清掉客户端拒绝记录) // 强制重发资源包(清掉客户端拒绝记录)
Bukkit.getScheduler() Bukkit.getScheduler()
.runTaskLater(this, () -> { .runTaskLater(this, () -> {
@@ -2239,6 +2329,63 @@ public class Main extends JavaPlugin
// ★ Sdf1_game宝箱物品不干预,由Sdf1_game自行处理 // ★ Sdf1_game宝箱物品不干预,由Sdf1_game自行处理
} }
// ★ 防止雪球菜单被投掷出去(弱网兜底 + 防止快乐恶魂投喂)
@EventHandler(priority = EventPriority.HIGHEST)
public void onProjectileLaunch(
org.bukkit.event.entity.ProjectileLaunchEvent e) {
if (e.isCancelled()) return;
if (!(e.getEntity()
instanceof org.bukkit.entity.Snowball))
return;
org.bukkit.entity.Snowball sb =
(org.bukkit.entity.Snowball) e.getEntity();
// 检查发射者(可能是玩家或生物)
Object shooter = sb.getShooter();
// 如果是玩家发射菜单雪球 → 取消并归还
if (shooter instanceof Player) {
Player p = (Player) shooter;
ItemStack main =
p.getInventory().getItemInMainHand();
ItemStack off =
p.getInventory().getItemInOffHand();
if (isMenuSnowball(main)
|| isMenuSnowball(off)) {
e.setCancelled(true);
giveMenuSnowball(p);
return;
}
}
// 检查雪球的item meta是否携带菜单标记(兜底:恶魂投喂时雪球已被分离)
if (sb.hasMetadata("menu_snowball")) {
e.setCancelled(true);
return;
}
}
// ★ 防止任何生物拾取菜单雪球掉落物(防恶魂投喂)
@EventHandler(priority = EventPriority.HIGHEST)
public void onEntityPickupItem(
org.bukkit.event.entity.EntityPickupItemEvent e) {
if (e.isCancelled()) return;
ItemStack item = e.getItem().getItemStack();
if (isMenuSnowball(item)) {
e.setCancelled(true);
}
}
// ★ 防止玩家右键实体时交出菜单雪球(防恶魂投喂)
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerInteractEntity(
org.bukkit.event.player.PlayerInteractEntityEvent e) {
if (e.isCancelled()) return;
Player p = e.getPlayer();
ItemStack main = p.getInventory().getItemInMainHand();
ItemStack off = p.getInventory().getItemInOffHand();
if (isMenuSnowball(main) || isMenuSnowball(off)) {
e.setCancelled(true);
}
}
@EventHandler @EventHandler
public void onPickup(PlayerPickupItemEvent e) { public void onPickup(PlayerPickupItemEvent e) {
@@ -3008,62 +3155,6 @@ public class Main extends JavaPlugin
} }
// ===== 二级菜单(.txt)—— 移到前面 =====
if (title.startsWith("§6§l")
&& !title.equals(GUIManager.T_MAIN)
&& !title.equals(GUIManager.T_ADMIN)
&& !title.equals(GUIManager.T_MY_INFO)
&& !title.equals(GUIManager.T_INVITE)
&& !title.equals(
GUIManager.T_TASK_CENTER)
&& !title.equals(
GUIManager.T_GIFT_STAGES)
&& !title.equals("§6§l任务面板")
&& !title.equals(
"§6§l垃圾回收站")) {
e.setCancelled(true);
if (slot == 49) {
gui.openMain(p);
return;
}
// ★ bounds检查 ★
if (slot < 45
&& slot < e.getInventory()
.getSize()
&& e.getCurrentItem() != null) {
ItemMeta subLm =
e.getCurrentItem().getItemMeta();
if (subLm != null
&& subLm.getLore() != null) {
for (String subLn
: subLm.getLore()) {
if (subLn.contains(
"§7指令: §f")) {
String cmd =
subLn.replace(
"§7指令: §f",
"").trim();
if (cmd.isEmpty()
|| cmd.equals("/")
|| cmd.equals(
"null")) {
p.sendMessage(
"§c指令为空");
return;
}
p.closeInventory();
if (cmd.startsWith("/"))
cmd = cmd.substring(1);
Bukkit.dispatchCommand(
p, cmd);
return;
}
}
}
}
return;
}
// ===== [DEBUG] 标题追踪 ===== // ===== [DEBUG] 标题追踪 =====
{ {
String dt = title; String dt = title;
@@ -3114,7 +3205,8 @@ public class Main extends JavaPlugin
"§7指令: §f")) { "§7指令: §f")) {
String cmd = subLn String cmd = subLn
.replace("§7指令: §f", "") .replace("§7指令: §f", "")
.trim(); .trim()
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
if (cmd.isEmpty() if (cmd.isEmpty()
|| cmd.equals("/") || cmd.equals("/")
|| cmd.equals("null")) { || cmd.equals("null")) {
@@ -3125,7 +3217,12 @@ public class Main extends JavaPlugin
if (cmd.startsWith("/")) { if (cmd.startsWith("/")) {
cmd = cmd.substring(1); cmd = cmd.substring(1);
} }
// ★ 子菜单项如果是.txt文件,打开子菜单而非执行命令
if (cmd.endsWith(".txt")) {
gui.openSubMenu(p, cmd);
} else {
Bukkit.dispatchCommand(p, cmd); Bukkit.dispatchCommand(p, cmd);
}
return; return;
} }
} }
@@ -3375,7 +3472,8 @@ public class Main extends JavaPlugin
for (String ln : meta.getLore()) { for (String ln : meta.getLore()) {
if (ln.contains("§7指令: §f")) { if (ln.contains("§7指令: §f")) {
String cmd = ln.replace( String cmd = ln.replace(
"§7指令: §f", "").trim(); "§7指令: §f", "").trim()
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
if (cmd.isEmpty() if (cmd.isEmpty()
|| cmd.equals("/") || cmd.equals("/")
|| cmd.equals("null")) { || cmd.equals("null")) {
@@ -3976,6 +4074,11 @@ public class Main extends JavaPlugin
if (toIconSlot) { if (toIconSlot) {
ItemStack dragged = ItemStack dragged =
e.getOldCursor().clone(); e.getOldCursor().clone();
// ★ 拦截宝箱自定义物品(事不过三原则)
if (gui.handleTreasureItemBlock(p, dragged)) {
e.setCancelled(true);
return;
}
String b64 = gui.serializeItem(dragged); String b64 = gui.serializeItem(dragged);
if (b64 != null) { if (b64 != null) {
db.saveMenuIcon(p.getName(), b64, db.saveMenuIcon(p.getName(), b64,
@@ -4131,6 +4234,9 @@ public class Main extends JavaPlugin
sender.sendMessage("§e/protect removewhite <玩家> §7移除模式排除玩家"); sender.sendMessage("§e/protect removewhite <玩家> §7移除模式排除玩家");
sender.sendMessage("§e/protect listname <区域> §7查看和平白名单"); sender.sendMessage("§e/protect listname <区域> §7查看和平白名单");
sender.sendMessage("§e/protect listwhite §7查看模式排除名单"); sender.sendMessage("§e/protect listwhite §7查看模式排除名单");
sender.sendMessage("§a§l欢迎游玩草原探险服务器");
sender.sendMessage("§a§l服务器ip:mc2.ypshidifu.cn");
sender.sendMessage("§a§lJava免输端口,基岩版30679");
return true; return true;
} }
@@ -4151,6 +4257,9 @@ public class Main extends JavaPlugin
return true; return true;
} }
sender.sendMessage("§e[更新] 正在检查所有插件更新..."); sender.sendMessage("§e[更新] 正在检查所有插件更新...");
sender.sendMessage("§a§l欢迎游玩草原探险服务器");
sender.sendMessage("§a§l服务器ip:mc2.ypshidifu.cn");
sender.sendMessage("§a§lJava免输端口,基岩版30679");
checkAllPluginsUpdate(sender); checkAllPluginsUpdate(sender);
return true; return true;
} }
@@ -4181,6 +4290,51 @@ public class Main extends JavaPlugin
} }
return shopManager.handleCommand(sender, args); return shopManager.handleCommand(sender, args);
} }
// ★ menu 子命令 - 重载菜单配置
if (cmd.getName().equalsIgnoreCase("sdf1_login")
&& args.length >= 2
&& args[0].equalsIgnoreCase("menu")
&& args[1].equalsIgnoreCase("reload")) {
if (!isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
if (menu == null) {
sender.sendMessage("§c菜单系统未加载");
return true;
}
menu.loadMenu();
sender.sendMessage("§a菜单已重载,共 "
+ menu.getItems().size()
+ " 个菜单项");
return true;
}
// ★ weblogin 子命令 - 生成Web登录Token
if (sub.equals("weblogin")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可使用");
return true;
}
if (webManager == null) {
sender.sendMessage("§cWeb通信未初始化");
return true;
}
webManager.handleWebLogin((Player) sender);
return true;
}
// ★ 独立 /web 和 /控制台 命令 - 跟 /sdf1_login weblogin 一样
if (cmdName.equals("web") || cmdName.equals("控制台")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可使用");
return true;
}
if (webManager == null) {
sender.sendMessage("§cWeb通信未初始化");
return true;
}
webManager.handleWebLogin((Player) sender);
return true;
}
// set // set
if (sub.equals("set")) { if (sub.equals("set")) {
if (!isAdmin(sender)) { if (!isAdmin(sender)) {
@@ -4198,13 +4352,31 @@ public class Main extends JavaPlugin
return true; return true;
} }
String salt = (String) db.getField(tgt, "password_salt"); String salt = (String) db.getField(tgt, "password_salt");
if (salt == null || salt.trim().isEmpty()) {
sender.sendMessage("§c玩家密码盐值缺失,无法设置临时密码");
return true;
}
salt = salt.trim();
// 清理所有非 Base64 字符(反斜杠、引号等)
salt = salt.replaceAll("[^A-Za-z0-9+/=]", "");
if (salt.isEmpty()) {
sender.sendMessage("§c玩家密码盐值格式非法,无法设置临时密码");
return true;
}
String hash = PasswordUtils.hash(tempPwd, salt); String hash = PasswordUtils.hash(tempPwd, salt);
// ★ 只设置临时密码,不覆盖主密码
db.setField(tgt, "temp_password", hash); db.setField(tgt, "temp_password", hash);
db.setField(tgt, "temp_pw_expire", db.setField(tgt, "temp_pw_expire",
System.currentTimeMillis() + 300000L); System.currentTimeMillis() + 300000L);
db.setField(tgt, "temp_pw_used", 0); db.setField(tgt, "temp_pw_used", 0);
sender.sendMessage("§a已为 " + tgt + " 设置临时密码: " + tempPwd);
// ★ 立即发邮件(踢人之前) // ★ 推送密码凭证到 Web 端(含临时密码)
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
webManager.pushWebLoginCredentials();
});
// ★ 立即发邮件和推送密码到 Web
String emailAddr = (String) db.getField(tgt, "email"); String emailAddr = (String) db.getField(tgt, "email");
if (emailAddr != null && !emailAddr.isEmpty()) { if (emailAddr != null && !emailAddr.isEmpty()) {
// 获取最近登录信息 // 获取最近登录信息
@@ -4230,7 +4402,7 @@ public class Main extends JavaPlugin
Bukkit.getScheduler().runTaskAsynchronously(this, Bukkit.getScheduler().runTaskAsynchronously(this,
() -> { () -> {
// ★ 发送临时密码邮件(带登录信息) // ★ 发送临时密码邮件
boolean sent = email.sendTempPassword( boolean sent = email.sendTempPassword(
to, fName, pwd); to, fName, pwd);
// ★ 额外发送安全通知邮件 // ★ 额外发送安全通知邮件
@@ -4259,6 +4431,13 @@ public class Main extends JavaPlugin
sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知"); sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知");
} }
// ★ 推送新密码到 Web 端
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
webManager.pushWebLoginCredentials();
});
// ★ 删除重复的推送(已在上面推送过)
// 踢人 // 踢人
Player tp = Bukkit.getPlayer(tgt); Player tp = Bukkit.getPlayer(tgt);
if (tp != null && tp.isOnline()) if (tp != null && tp.isOnline())
@@ -4693,6 +4872,20 @@ public class Main extends JavaPlugin
if (!cmdName.equals("sdf1_login")) if (!cmdName.equals("sdf1_login"))
return false; return false;
// ★ /sdf1_login reload: 重载全部设置
if (args.length > 0 && args[0].equalsIgnoreCase("reload")) {
if (!isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
reloadConfig();
if (areaProtection != null) areaProtection.reload();
if (webManager != null) webManager.reloadWebConfig();
sender.sendMessage("§a[§2Sdf1_login§a] 插件全部设置已重载");
return true;
}
if (args.length == 0) { if (args.length == 0) {
if (!(sender instanceof Player)) { if (!(sender instanceof Player)) {
sender.sendMessage( sender.sendMessage(
@@ -4737,6 +4930,9 @@ public class Main extends JavaPlugin
pendingDeleteTask = null; pendingDeleteTask = null;
if (name == null) return; if (name == null) return;
db.deleteUser(name); db.deleteUser(name);
if (webManager != null) {
webManager.deleteWebUser(name);
}
getLogger().info( getLogger().info(
"[Sdf1_login] 已删除: " + name); "[Sdf1_login] 已删除: " + name);
Player tp = Bukkit.getPlayer(name); Player tp = Bukkit.getPlayer(name);
@@ -4772,6 +4968,9 @@ public class Main extends JavaPlugin
pendingDeleteName = null; pendingDeleteName = null;
pendingDeleteTask = null; pendingDeleteTask = null;
db.deleteUser(name); db.deleteUser(name);
if (webManager != null) {
webManager.deleteWebUser(name);
}
sender.sendMessage("§a已立即删除: " + name); sender.sendMessage("§a已立即删除: " + name);
Player tp = Bukkit.getPlayer(name); Player tp = Bukkit.getPlayer(name);
if (tp != null && tp.isOnline()) if (tp != null && tp.isOnline())
@@ -5008,6 +5207,11 @@ public class Main extends JavaPlugin
recordPasswordChange(p2.getName(), oldHash, salt); recordPasswordChange(p2.getName(), oldHash, salt);
db.setField(p2.getName(), "password_hash", newHash); db.setField(p2.getName(), "password_hash", newHash);
db.setField(p2.getName(), "password_salt", newSalt); db.setField(p2.getName(), "password_salt", newSalt);
// ★ 同步密码到Web端(异步推送,不阻塞主线程)
final Main self = this;
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
webManager.pushWebLoginCredentials();
});
if (!mainOk && tempOk) if (!mainOk && tempOk)
db.clearTempPassword(p2.getName()); db.clearTempPassword(p2.getName());
// pw 命令改密码成功后 // pw 命令改密码成功后
@@ -5289,6 +5493,11 @@ public class Main extends JavaPlugin
"password_hash", newHash); "password_hash", newHash);
db.setField(p2.getName(), db.setField(p2.getName(),
"password_salt", newSalt); "password_salt", newSalt);
// ★ 同步密码到Web端(异步推送,不阻塞主线程)
final Main self2 = this;
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
self2.webManager.pushWebLoginCredentials();
});
if (!mainOk && tempOk) { if (!mainOk && tempOk) {
db.clearTempPassword( db.clearTempPassword(
p2.getName()); p2.getName());
@@ -5767,7 +5976,7 @@ public class Main extends JavaPlugin
"take", "add", "kick", "take", "add", "kick",
"ticket", "oa", "stop", "ticket", "oa", "stop",
"shopadd", "shopdel", "back", "radio", "back", "shop", "shopadd", "shopdel", "back", "radio", "back", "shop",
"update", "updateall")); "update", "updateall", "menu"));
} else { } else {
list.addAll(Arrays.asList( list.addAll(Arrays.asList(
"pw", "email", "sign", "pw", "email", "sign",
@@ -5775,6 +5984,14 @@ public class Main extends JavaPlugin
"ticket", "oa", "stop")); "ticket", "oa", "stop"));
} }
} }
// ★ menu reload Tab补全
if (cmd.getName().equalsIgnoreCase("sdf1_login")
&& args.length == 2
&& args[0].equalsIgnoreCase("menu")) {
if (isAdmin(sender)) {
list.add("reload");
}
}
// 独立 /update 命令无子命令 // 独立 /update 命令无子命令
if (cmd.getName().equalsIgnoreCase("update")) { if (cmd.getName().equalsIgnoreCase("update")) {
return list; return list;
+23 -5
View File
@@ -83,18 +83,29 @@ public class MenuManager implements Listener {
return items; return items;
} }
// ===== 读取菜单.txt ===== // ===== 读取菜单.txt(主菜单唯一来源) =====
public void loadMenu() { public void loadMenu() {
items.clear(); items.clear();
// 优先菜单.txt,备选menu.txt
File f = new File( File f = new File(
plugin.getDataFolder(), plugin.getDataFolder(),
"菜单.txt"); "菜单.txt");
if (!f.exists()) createDefault(f); if (!f.exists()) {
File alt = new File(
plugin.getDataFolder(),
"menu.txt");
if (alt.exists()) {
f = alt;
} else {
createDefault(f);
}
}
parseFile(f); parseFile(f);
plugin.getLogger().info( plugin.getLogger().info(
"[Menu] 加载 " "[Menu] 加载 " + items.size()
+ items.size() + " 个菜单项 (源: "
+ " 个菜单项"); + f.getName() + ")");
} }
private void parseFile(File f) { private void parseFile(File f) {
@@ -372,9 +383,16 @@ public class MenuManager implements Listener {
// ===== 保存 ===== // ===== 保存 =====
public void saveMenu() { public void saveMenu() {
// 保存到加载源文件
File f = new File( File f = new File(
plugin.getDataFolder(), plugin.getDataFolder(),
"菜单.txt"); "菜单.txt");
if (!f.exists()) {
File alt = new File(
plugin.getDataFolder(),
"menu.txt");
if (alt.exists()) f = alt;
}
try (PrintWriter pw = new PrintWriter( try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter( new OutputStreamWriter(
new FileOutputStream(f), new FileOutputStream(f),
+6 -1
View File
@@ -19,7 +19,12 @@ public class PasswordUtils {
try { try {
MessageDigest md = MessageDigest md =
MessageDigest.getInstance("SHA-256"); MessageDigest.getInstance("SHA-256");
md.update(Base64.getDecoder().decode(salt)); // 清理非 Base64 字符,防止数据库中转义字符导致解析错误
String cleanSalt = salt.replaceAll("[^A-Za-z0-9+/=]", "");
if (cleanSalt.isEmpty()) {
throw new IllegalArgumentException("Invalid salt: " + salt);
}
md.update(Base64.getDecoder().decode(cleanSalt));
byte[] hash = byte[] hash =
md.digest(password.getBytes("UTF-8")); md.digest(password.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash); return Base64.getEncoder().encodeToString(hash);
+3 -3
View File
@@ -920,8 +920,8 @@ public class ShopManager implements Listener {
&& !n.equals("coupons.md")); && !n.equals("coupons.md"));
} }
if (allMd == null) { if (allMd == null) {
// plugin.getLogger().warning( plugin.getLogger().warning(
// "[Shop] shop目录为空"); "[Shop] shop目录为空");
return; return;
} }
@@ -1372,7 +1372,7 @@ public class ShopManager implements Listener {
for (ShopCategory cat : categories) for (ShopCategory cat : categories)
saveCategory(cat); saveCategory(cat);
} }
private ItemStack getShopStack( public ItemStack getShopStack(
ShopItem item, int amount) { ShopItem item, int amount) {
// 优先级1: 手动捕获的NBT数据 // 优先级1: 手动捕获的NBT数据
ItemStack cached = ItemStack cached =
+141 -16
View File
@@ -48,18 +48,28 @@ public class UpdateChecker {
private static final String GITHUB_REPO = "sdf1_login"; private static final String GITHUB_REPO = "sdf1_login";
private static final String GITEE_OWNER = "nihaoshidifu"; private static final String GITEE_OWNER = "nihaoshidifu";
private static final String GITEE_REPO = "sdf1_login"; private static final String GITEE_REPO = "sdf1_login";
private static final String GYP_OWNER = "youpaishidifu";
private static final String GYP_REPO = "Sdf1_login";
// API 地址(使用releases/latest获取最新版本) // API 地址(使用releases/latest获取最新版本)
private static final String GITHUB_API = private static final String GITHUB_API =
"https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest"; "https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest";
private static final String GITEE_API = private static final String GITEE_API =
"https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest"; "https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest";
private static final String GYP_API =
"https://git.ypshidifu.cn/api/v1/repos/" + GYP_OWNER + "/" + GYP_REPO + "/releases/latest";
// 第三路备选:宝塔静态HTML
private static final String FALLBACK_HTML =
"https://caoyuan.ypshidifu.cn/update_check.html";
// 仓库页面链接 // 仓库页面链接
private static final String GITHUB_LINK = private static final String GITHUB_LINK =
"https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases"; "https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases";
private static final String GITEE_LINK = private static final String GITEE_LINK =
"https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases"; "https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases";
private static final String GYP_LINK =
"https://git.ypshidifu.cn/" + GYP_OWNER + "/" + GYP_REPO + "/releases";
// 单次请求超时(毫秒) // 单次请求超时(毫秒)
private static final int TIMEOUT_MS = 5000; private static final int TIMEOUT_MS = 5000;
@@ -153,18 +163,31 @@ public class UpdateChecker {
plugin.getLogger().info("[Sdf1_login] 启动自动检查更新..."); plugin.getLogger().info("[Sdf1_login] 启动自动检查更新...");
} }
// ★ 双通道并行执行,一路成功掐断另一路 // ★ 三通道并行执行,一路成功掐断其余
AtomicBoolean cancelled = new AtomicBoolean(false); AtomicBoolean cancelled = new AtomicBoolean(false);
AtomicReference<CheckResult> gypResult = new AtomicReference<>();
AtomicReference<CheckResult> ghResult = new AtomicReference<>(); AtomicReference<CheckResult> ghResult = new AtomicReference<>();
AtomicReference<CheckResult> geResult = new AtomicReference<>(); AtomicReference<CheckResult> geResult = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(2); CountDownLatch latch = new CountDownLatch(3);
// 通道0:git.ypshidifu.cn(最高优先级,Gitea格式)
Thread gypThread = new Thread(() -> {
if (!cancelled.get()) {
gypResult.set(tryChannel(GYP_API, "git.ypshidifu.cn", GYP_LINK));
if (gypResult.get().isSuccess()) {
cancelled.set(true);
}
}
latch.countDown();
});
gypThread.setDaemon(true);
// 通道1:GitHub // 通道1:GitHub
Thread ghThread = new Thread(() -> { Thread ghThread = new Thread(() -> {
if (!cancelled.get()) { if (!cancelled.get()) {
ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK)); ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK));
if (ghResult.get().isSuccess()) { if (ghResult.get().isSuccess()) {
cancelled.set(true); // 成功则取消另一路 cancelled.set(true);
} }
} }
latch.countDown(); latch.countDown();
@@ -176,47 +199,64 @@ public class UpdateChecker {
if (!cancelled.get()) { if (!cancelled.get()) {
geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK)); geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK));
if (geResult.get().isSuccess()) { if (geResult.get().isSuccess()) {
cancelled.set(true); // 成功则取消另一路 cancelled.set(true);
} }
} }
latch.countDown(); latch.countDown();
}); });
geThread.setDaemon(true); geThread.setDaemon(true);
gypThread.start();
ghThread.start(); ghThread.start();
geThread.start(); geThread.start();
try { try {
latch.await(); // 等待两路都完成 latch.await();
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
CheckResult gyp = gypResult.get();
CheckResult gh = ghResult.get(); CheckResult gh = ghResult.get();
CheckResult ge = geResult.get(); CheckResult ge = geResult.get();
// 选择成功的结果 // 选择成功的结果(优先级:git.ypshidifu.cn > GitHub > Gitee)
CheckResult chosen = null; CheckResult chosen = null;
if (gh != null && gh.isSuccess()) { if (gyp != null && gyp.isSuccess()) {
chosen = gyp;
} else if (gh != null && gh.isSuccess()) {
chosen = gh; chosen = gh;
} else if (ge != null && ge.isSuccess()) { } else if (ge != null && ge.isSuccess()) {
chosen = ge; chosen = ge;
} else { } else {
// 双通道均失败 // ★ 双通道均失败,尝试第三路备选(宝塔静态HTML)
plugin.getLogger().info("[Sdf1_login] git.ypshidifu.cn/GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
CheckResult fbResult = tryFallbackChannel();
if (fbResult != null && fbResult.isSuccess()) {
chosen = fbResult;
plugin.getLogger().info("[Sdf1_login] 第三路备选 BaoTa 检查成功: " + fbResult.version);
} else {
failCount++; failCount++;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (gyp != null && gyp.error != null) {
sb.append("§7git.ypshidifu.cn: §c").append(gyp.error);
}
if (gh != null && gh.error != null) { if (gh != null && gh.error != null) {
if (sb.length() > 0) sb.append("\n");
sb.append("§7GitHub: §c").append(gh.error); sb.append("§7GitHub: §c").append(gh.error);
} }
if (ge != null && ge.error != null) { if (ge != null && ge.error != null) {
if (sb.length() > 0) sb.append("\n"); if (sb.length() > 0) sb.append("\n");
sb.append("§7Gitee: §c").append(ge.error); sb.append("§7Gitee: §c").append(ge.error);
} }
if (fbResult != null && fbResult.error != null) {
if (sb.length() > 0) sb.append("\n");
sb.append("§7BaoTa: §c").append(fbResult.error);
}
if (failCount >= TRIP_THRESHOLD) { if (failCount >= TRIP_THRESHOLD) {
circuitTripped = true; circuitTripped = true;
plugin.getLogger().warning( plugin.getLogger().warning(
"[Sdf1_login] 更新检测已熔断,连续失败" + failCount + "次"); "[Sdf1_login] 更新检测已熔断,连续失败" + failCount + "次");
// 熔断后安排2分钟后重试
scheduleRetry(); scheduleRetry();
} }
if (sender != null) { if (sender != null) {
@@ -234,6 +274,7 @@ public class UpdateChecker {
} }
return; return;
} }
}
// 至少一个通道成功 // 至少一个通道成功
failCount = 0; failCount = 0;
@@ -296,10 +337,15 @@ public class UpdateChecker {
sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本"); sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本");
} }
// 显示另一通道的失败原因 // 显示其他失败通道的错误信息
CheckResult other = (src.equals("GitHub")) ? ge : gh; if (gyp != null && !gyp.isSuccess() && gyp.error != null && !src.equals("git.ypshidifu.cn")) {
if (other != null && !other.isSuccess() && other.error != null) { sender.sendMessage("§7[Sdf1_login] [更新] git.ypshidifu.cn 不可用: " + gyp.error);
sender.sendMessage("§7[Sdf1_login] [更新] " + other.source + " 不可用: " + other.error); }
if (gh != null && !gh.isSuccess() && gh.error != null && !src.equals("GitHub")) {
sender.sendMessage("§7[Sdf1_login] [更新] GitHub 不可用: " + gh.error);
}
if (ge != null && !ge.isSuccess() && ge.error != null && !src.equals("Gitee")) {
sender.sendMessage("§7[Sdf1_login] [更新] Gitee 不可用: " + ge.error);
} }
}); });
} }
@@ -314,6 +360,81 @@ public class UpdateChecker {
// ========== HTTP 请求 ========== // ========== HTTP 请求 ==========
/**
* 第三路备选:从宝塔静态HTML解析版本信息
* HTML中用 <!--UPDATE_DATA 包裹结构化数据:
* pluginName|version|downloadLink
*/
private CheckResult tryFallbackChannel() {
HttpURLConnection conn = null;
try {
URL url = new URL(FALLBACK_HTML);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Sdf1-Plugin-UpdateChecker/1.0");
conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS);
conn.setInstanceFollowRedirects(true);
int code = conn.getResponseCode();
if (code != 200) {
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
"BaoTa: HTTP " + code);
}
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
// 解析 <!--UPDATE_DATA ... UPDATE_DATA--> 块
String html = sb.toString();
// ★ 去除 Cloudflare 注入的 CDN-CGI 链接(<a href=...>...</a>)
html = html.replaceAll("<a\\s[^>]*>[^<]*</a>", "");
int start = html.indexOf("<!--UPDATE_DATA");
int end = html.indexOf("UPDATE_DATA-->");
if (start < 0 || end < 0 || end <= start) {
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
"BaoTa: 无法解析更新数据块");
}
String block = html.substring(start, end);
// 逐行查找匹配当前插件名的记录
String[] lines = block.split("\n");
for (String l : lines) {
l = l.trim();
// 跳过空行、注释标记行、HTML注入标签
if (l.isEmpty() || l.startsWith("<!--") || l.startsWith("<")) continue;
if (l.startsWith("Sdf1_login|")) {
// 格式: Sdf1_login|1.0|https://...
String[] parts = l.split("\\|");
if (parts.length >= 2) {
String ver = parts[1].trim();
String link = parts.length >= 3 ? parts[2].trim() : GITHUB_LINK;
return new CheckResult(ver, "BaoTa", link, null);
}
}
}
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
"BaoTa: 未找到 Sdf1_login 的版本信息");
} catch (java.net.SocketTimeoutException e) {
return new CheckResult(null, "BaoTa", FALLBACK_HTML, "BaoTa: 连接超时");
} catch (Exception e) {
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
"BaoTa: " + e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
}
private static class CheckResult { private static class CheckResult {
final String version; final String version;
final String source; final String source;
@@ -340,9 +461,10 @@ public class UpdateChecker {
conn.setRequestMethod("GET"); conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", conn.setRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
conn.setRequestProperty("Accept", "*/*"); conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9"); conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
conn.setRequestProperty("Connection", "keep-alive");
conn.setConnectTimeout(TIMEOUT_MS); conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS); conn.setReadTimeout(TIMEOUT_MS);
conn.setInstanceFollowRedirects(true); conn.setInstanceFollowRedirects(true);
@@ -353,7 +475,10 @@ public class UpdateChecker {
return new CheckResult(null, sourceName, link, sourceName + ": 未找到 (HTTP 404)"); return new CheckResult(null, sourceName, link, sourceName + ": 未找到 (HTTP 404)");
} }
if (code == 403) { if (code == 403) {
return new CheckResult(null, sourceName, link, sourceName + ": 被限流 (HTTP 403)"); String errBody = "";
try { BufferedReader er = new BufferedReader(new InputStreamReader(conn.getErrorStream())); StringBuilder eb = new StringBuilder(); String el; while ((el = er.readLine()) != null) eb.append(el); er.close(); errBody = eb.toString(); } catch (Exception ignored) {}
String detail = errBody.isEmpty() ? "" : " | " + errBody.substring(0, Math.min(errBody.length(), 200));
return new CheckResult(null, sourceName, link, sourceName + ": 被限流 (HTTP 403)" + detail);
} }
if (code != 200) { if (code != 200) {
return new CheckResult(null, sourceName, link, sourceName + ": HTTP " + code); return new CheckResult(null, sourceName, link, sourceName + ": HTTP " + code);
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,5 +1,5 @@
name: Sdf1_login name: Sdf1_login
version: 2.1 version: 2.6
main: Sdf1_login.Main main: Sdf1_login.Main
api-version: '1.21' api-version: '1.21'
commands: commands:
@@ -96,7 +96,7 @@ commands:
商店: 商店:
description: 打开商店 description: 打开商店
usage: /商店 usage: /商店
#testorder: # testorder:
# description: 测试订单中心 # description: 测试订单中心
# usage: /testorder # usage: /testorder
protect: protect:
@@ -105,3 +105,9 @@ commands:
update: update:
description: 检查所有插件更新(集群控制) description: 检查所有插件更新(集群控制)
usage: /update usage: /update
web:
description: 生成Web端登录令牌
usage: /web
控制台:
description: 生成Web端登录令牌(中文版)
usage: /控制台