fix: 修复Web注册不写注册IP、玩家登录IP的bug; 新增库存高频轮询; 更新README下载地址

This commit is contained in:
youpaishidifu
2026-06-22 16:26:17 +08:00
parent 8d4731a873
commit ebcb11338c
7 changed files with 1154 additions and 106 deletions
+1 -1
View File
@@ -565,7 +565,7 @@ http-port=443
## 4. 下载地址
- 官方仓库:[https://git.ypshidifu.cn/youpaishidifu/Sdf1_login](https://git.ypshidifu.cn/youpaishidifu/Sdf1_login) (推荐)
- 官方文档:[https://wiki.ypshidifu.cn](https://wiki.ypshidifu.cn) (推荐)
- 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)
+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)
+146 -17
View File
@@ -399,6 +399,8 @@ public class GUIManager implements Listener {
public void openSubMenu(
Player p, String fileName) {
// ★ 剥离MC颜色代码(§f、§7等),防止污染文件名
fileName = fileName.replaceAll("\u00A7[0-9a-fk-or]", "").trim();
String menuTitle =
"§6§l" + fileName
.replace(".txt", "");
@@ -406,21 +408,153 @@ public class GUIManager implements Listener {
null, 54, menuTitle);
fillBg(g);
java.io.File f = new java.io.File(
plugin.getDataFolder(), fileName);
if (!f.exists()) {
p.sendMessage("§c文件不存在: "
java.io.File dir = plugin.getDataFolder();
java.io.File f = null;
String cleanName = fileName.trim();
plugin.getLogger().info(
"[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 {
lines = java.nio.file.Files
.readAllLines(f.toPath(),
java.nio.charset
.StandardCharsets
.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;
}
try {
List<String> lines =
java.nio.file.Files
.readAllLines(
f.toPath(),
java.nio.charset
.StandardCharsets
.UTF_8);
List<String[]> items =
new ArrayList<>();
StringBuilder buf =
@@ -484,11 +618,6 @@ public class GUIManager implements Listener {
it.setItemMeta(im);
g.setItem(i, it);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 子菜单读取失败: "
+ e.getMessage());
}
g.setItem(49, mkItem(
Material.ARROW,
+96 -60
View File
@@ -16,6 +16,7 @@ import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.inventory.CraftItemEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
@@ -2328,6 +2329,63 @@ public class Main extends JavaPlugin
// ★ 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
public void onPickup(PlayerPickupItemEvent e) {
@@ -3097,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] 标题追踪 =====
{
String dt = title;
@@ -3203,7 +3205,8 @@ public class Main extends JavaPlugin
"§7指令: §f")) {
String cmd = subLn
.replace("§7指令: §f", "")
.trim();
.trim()
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
if (cmd.isEmpty()
|| cmd.equals("/")
|| cmd.equals("null")) {
@@ -3214,7 +3217,12 @@ public class Main extends JavaPlugin
if (cmd.startsWith("/")) {
cmd = cmd.substring(1);
}
Bukkit.dispatchCommand(p, cmd);
// ★ 子菜单项如果是.txt文件,打开子菜单而非执行命令
if (cmd.endsWith(".txt")) {
gui.openSubMenu(p, cmd);
} else {
Bukkit.dispatchCommand(p, cmd);
}
return;
}
}
@@ -3464,7 +3472,8 @@ public class Main extends JavaPlugin
for (String ln : meta.getLore()) {
if (ln.contains("§7指令: §f")) {
String cmd = ln.replace(
"§7指令: §f", "").trim();
"§7指令: §f", "").trim()
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
if (cmd.isEmpty()
|| cmd.equals("/")
|| cmd.equals("null")) {
@@ -4281,6 +4290,25 @@ public class Main extends JavaPlugin
}
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)) {
@@ -5948,7 +5976,7 @@ public class Main extends JavaPlugin
"take", "add", "kick",
"ticket", "oa", "stop",
"shopadd", "shopdel", "back", "radio", "back", "shop",
"update", "updateall"));
"update", "updateall", "menu"));
} else {
list.addAll(Arrays.asList(
"pw", "email", "sign",
@@ -5956,6 +5984,14 @@ public class Main extends JavaPlugin
"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 命令无子命令
if (cmd.getName().equalsIgnoreCase("update")) {
return list;
+23 -5
View File
@@ -83,18 +83,29 @@ public class MenuManager implements Listener {
return items;
}
// ===== 读取菜单.txt =====
// ===== 读取菜单.txt(主菜单唯一来源) =====
public void loadMenu() {
items.clear();
// 优先菜单.txt,备选menu.txt
File f = new File(
plugin.getDataFolder(),
"菜单.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);
plugin.getLogger().info(
"[Menu] 加载 "
+ items.size()
+ " 个菜单项");
"[Menu] 加载 " + items.size()
+ " 个菜单项 (源: "
+ f.getName() + ")");
}
private void parseFile(File f) {
@@ -372,9 +383,16 @@ public class MenuManager implements Listener {
// ===== 保存 =====
public void saveMenu() {
// 保存到加载源文件
File f = new File(
plugin.getDataFolder(),
"菜单.txt");
if (!f.exists()) {
File alt = new File(
plugin.getDataFolder(),
"menu.txt");
if (alt.exists()) f = alt;
}
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
+316 -22
View File
@@ -237,6 +237,7 @@ public class WebManager {
syncBondBalances();
syncBondTransactions();
syncAllPlayerIps();
syncServiceProviders();
// 首次同步只打一次,后续由调度器静默执行
plugin.getLogger().info("[Web通信] 首次全量同步完成");
@@ -259,6 +260,9 @@ public class WebManager {
// ★ 启动交易高频轮询(每5秒检查PHP端是否有待处理交易)
startTransactionPolling();
// ★ 启动库存高频轮询(每5秒检测Web库存改动,无改动跳过,有改动立即更新)
startShopStockFastPolling();
// ★ 启动CDK验证轮询(每2秒检查PHP端CDK验证请求)
startCdkValidationPolling();
@@ -631,6 +635,7 @@ public class WebManager {
pushWebLoginCredentials();
syncUserRegistrations();
syncAllPlayerIps();
syncServiceProviders();
}
}
} catch (Exception e) {
@@ -708,6 +713,7 @@ public class WebManager {
syncUserRegistrations();
syncBondTransactions();
syncAllPlayerIps();
syncServiceProviders();
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 全量同步异常: " + e.getMessage());
} finally {
@@ -779,6 +785,163 @@ public class WebManager {
}.runTaskTimerAsynchronously(plugin, 20L * 5, 20L * 5); // 每5秒检查一次
}
// ==================== 库存高频轮询 ====================
/** 上次检测到的库存修改时间戳 */
private volatile long lastKnownShopStockModified = 0;
/** 连续失败计数 */
private volatile int shopStockPollFailCount = 0;
private static final int SHOP_STOCK_POLL_FAIL_THRESHOLD = 10;
/** 防重入锁:pullShopStock执行期间不重复拉取 */
private volatile boolean shopStockPulling = false;
/**
* 库存高频轮询:每5秒请求Web端库存变更检测接口
* 无改动跳过,有改动立即拉取完整库存并更新游戏
* 60~90秒的全量同步定时器作为兜底
*/
private void startShopStockFastPolling() {
new BukkitRunnable() {
@Override
public void run() {
try {
// ★ 防重入:正在拉取中则跳过
if (shopStockPulling) return;
// ★ 连续失败过多时跳过(等兜底定时器)
if (shopStockPollFailCount >= SHOP_STOCK_POLL_FAIL_THRESHOLD) {
if (shopStockPollFailCount % 12 != 0) {
shopStockPollFailCount++;
return;
}
}
String urlStr = webBaseUrl + "/api/sync.php?action=check_shop_stock_changed&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&last_modified=" + lastKnownShopStockModified;
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
shopStockPollFailCount++;
conn.disconnect();
return;
}
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();
conn.disconnect();
String resp = sb.toString();
shopStockPollFailCount = 0;
// 解析响应
boolean changed = resp.contains("\"changed\":true") || resp.contains("\"changed\": true");
if (changed) {
// ★ 从响应中提取服务端last_modified(先提取,后面决定是否采纳)
long serverLastModified = 0;
int lmIdx = resp.indexOf("\"last_modified\":");
if (lmIdx >= 0) {
lmIdx += 16;
int lmEnd = resp.indexOf(",", lmIdx);
if (lmEnd < 0) lmEnd = resp.indexOf("}", lmIdx);
if (lmEnd > lmIdx) {
try {
serverLastModified = Long.parseLong(
resp.substring(lmIdx, lmEnd).trim());
} catch (NumberFormatException e) { /* 忽略 */ }
}
}
// ★ 拉取并应用库存,只在成功更新后才推进时间戳
shopStockPulling = true;
try {
boolean applied = pullShopStockSync();
if (applied && serverLastModified > 0) {
lastKnownShopStockModified = serverLastModified;
}
// applied=false → 不更新lastKnownShopStockModified → 下次重试
} finally {
shopStockPulling = false;
}
}
} catch (Exception e) {
shopStockPollFailCount++;
}
}
}.runTaskTimerAsynchronously(plugin, 20L * 5, 20L * 5);
plugin.getLogger().info("[Web通信] 库存高频轮询已启动(每5秒)");
}
/**
* 同步拉取库存(阻塞调用,用于高频轮询器内避免嵌套异步)
* 逻辑与pullShopStock相同,但不在BukkitRunnable内执行
* @return 是否成功应用了库存更新
*/
private boolean pullShopStockSync() {
try {
String urlStr = webBaseUrl + "/api/sync.php?action=pull_shop_stock&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("[库存高频] pull_shop_stock HTTP " + code);
return false;
}
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();
if (!json.contains("\"success\":true")) {
plugin.getLogger().warning("[库存高频] pull_shop_stock 返回失败");
return false;
}
int dataStart = json.indexOf("\"items\":");
if (dataStart < 0) {
plugin.getLogger().warning("[库存高频] pull_shop_stock 无items字段");
return false;
}
String dataStr = json.substring(dataStart + 8);
int arrEnd = findMatchingBracket(dataStr, 0);
if (arrEnd < 0) {
plugin.getLogger().warning("[库存高频] pull_shop_stock JSON解析失败");
return false;
}
String arrStr = dataStr.substring(0, arrEnd + 1);
boolean applied = updateLocalShopStock(arrStr);
// ★ PHP端pullShopStock已自动清除admin_stock,无需再调clearAdminStock()
return applied;
} catch (Exception e) {
plugin.getLogger().warning("[库存高频] 拉取库存异常: " + e.getMessage());
return false;
}
}
// ==================== CDK验证轮询 ====================
/**
@@ -1557,6 +1720,46 @@ public class WebManager {
}
}
// ==================== 服务商列表同步到Web ====================
public void syncServiceProviders() {
try {
String token = generateAndSyncToken("system", "sync");
List<Map<String, Object>> providers = new ArrayList<>();
for (Map<String, Object> sp : plugin.getDb().getAllServiceProviders()) {
Map<String, Object> p = new LinkedHashMap<>();
p.put("player_name", sp.get("player_name"));
p.put("role", sp.get("role"));
p.put("active", sp.get("active"));
p.put("join_time", sp.get("join_time"));
providers.add(p);
}
if (providers.isEmpty()) {
plugin.getLogger().info("[Web通信] 无服务商,跳过同步");
return;
}
Map<String, Object> body = new LinkedHashMap<>();
body.put("data", providers);
String response = httpPostWithToken("api/sync.php?action=sync_service_providers", token, body);
if (response != null) {
Map<String, Object> result = parseJson(response);
Boolean success = (Boolean) result.get("success");
if (Boolean.TRUE.equals(success)) {
plugin.getLogger().info("[Web通信] 服务商同步成功: " + providers.size() + "人");
} else {
plugin.getLogger().warning("[Web通信] 服务商同步失败: " + result.get("message"));
}
} else {
plugin.getLogger().warning("[Web通信] 服务商同步请求失败");
}
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 服务商同步异常: " + e.getMessage());
}
}
/**
* 解析md商品文件
* 格式: | ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 |
@@ -2466,13 +2669,20 @@ public class WebManager {
boolean isRegistered = plugin.getDb().userExists(playerName);
boolean syncSuccess = false;
try {
// ★ 获取玩家真实IP地址
String playerIp = "";
java.net.InetSocketAddress addr = sender.getAddress();
if (addr != null && addr.getAddress() != null) {
playerIp = addr.getAddress().getHostAddress();
}
String urlStr = webBaseUrl + "/api/sync.php?action=push_player_login_status"
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
+ "&web_token=" + java.net.URLEncoder.encode(token, "UTF-8")
+ "&expire_seconds=" + tokenExpireSeconds
+ "&online=1"
+ "&registered=" + (isRegistered ? "1" : "0");
+ "&registered=" + (isRegistered ? "1" : "0")
+ "&ip=" + java.net.URLEncoder.encode(playerIp, "UTF-8");
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
@@ -2545,13 +2755,20 @@ public class WebManager {
boolean syncSuccess = false;
try {
// 用GET请求,所有参数通过URL传递
// ★ 获取玩家真实IP地址
String playerIp = "";
java.net.InetSocketAddress addr = player.getAddress();
if (addr != null && addr.getAddress() != null) {
playerIp = addr.getAddress().getHostAddress();
}
String urlStr = webBaseUrl + "/api/sync.php?action=push_player_login_status"
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
+ "&web_token=" + java.net.URLEncoder.encode(token, "UTF-8")
+ "&expire_seconds=" + tokenExpireSeconds
+ "&online=1"
+ "&registered=" + (isRegistered ? "1" : "0");
+ "&registered=" + (isRegistered ? "1" : "0")
+ "&ip=" + java.net.URLEncoder.encode(playerIp, "UTF-8");
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
@@ -2639,6 +2856,7 @@ public class WebManager {
syncBondBalances();
syncBondTransactions();
syncUserRegistrations();
syncServiceProviders();
}
}.runTaskAsynchronously(plugin);
sender.sendMessage("§a[Web] 同步任务已启动...");
@@ -3592,8 +3810,13 @@ public class WebManager {
if (arrEnd < 0) return;
String arrStr = dataStr.substring(0, arrEnd + 1);
// 更新本地商品库存
updateLocalShopStock(arrStr);
// ★ 更新本地商品库存,返回是否有改动
boolean changed = updateLocalShopStock(arrStr);
// ★ 如果有改动,清除PHP端的admin_stock标记(防止重复同步)
if (changed) {
clearAdminStock();
}
} catch (Exception e) {
// 静默处理
@@ -3603,20 +3826,42 @@ public class WebManager {
}
/**
* 更新本地商品库存
* 清除PHP端的管理员库存标记
*/
private void updateLocalShopStock(String itemsJson) {
private void clearAdminStock() {
try {
String urlStr = webBaseUrl + "/api/sync.php?action=clear_admin_stock&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.getResponseCode(); // 执行即可
conn.disconnect();
} catch (Exception e) {
// 静默处理
}
}
/**
* 更新本地商品库存
* @return 是否有任何库存被更新
*/
private boolean updateLocalShopStock(String itemsJson) {
try {
File shopDir = new File(plugin.getDataFolder(), "shop");
if (!shopDir.exists()) return;
if (!shopDir.exists()) return false;
File[] mdFiles = shopDir.listFiles((d, n) -> n.endsWith(".md"));
if (mdFiles == null) return;
if (mdFiles == null) return false;
// 解析Web端返回的库存数据
// 格式: [{"id":"ITEM_ID","stock":100,"last_sync":1234567890}, ...]
// 解析JSON提取id和stock
// 格式: [{"id":"ITEM_ID","stock":100,"last_sync":...,"admin_stock_override":true}, ...]
Map<String, Integer> webStock = new HashMap<>();
// ★ 跟踪哪些商品是管理员手动修改的(admin_stock_override=true)
Set<String> adminOverride = new HashSet<>();
int idx = 0;
while (true) {
int idStart = itemsJson.indexOf("\"id\":\"", idx);
@@ -3633,7 +3878,6 @@ public class WebManager {
if (stockEnd < 0) stockEnd = itemsJson.indexOf("}", stockStart);
if (stockEnd < 0) break;
String stockStr = itemsJson.substring(stockStart, stockEnd).trim();
// 去除可能的引号
stockStr = stockStr.replace("\"", "");
try {
@@ -3643,10 +3887,16 @@ public class WebManager {
// 忽略无效值
}
// ★ 检查admin_stock_override标记
int overrideCheck = itemsJson.indexOf("\"admin_stock_override\":", idEnd);
if (overrideCheck > idEnd && overrideCheck < stockEnd + 20) {
adminOverride.add(itemId);
}
idx = stockEnd + 1;
}
if (webStock.isEmpty()) return;
if (webStock.isEmpty()) return false;
// 更新每个md文件中的库存
int updatedCount = 0;
@@ -3671,17 +3921,21 @@ public class WebManager {
int newStock = webStock.get(itemId);
String oldStock = cols[6].trim();
// 始终用数字写入md文件,不写"无限"字符串(防止parseTableRow的Integer.parseInt失败)
String newStockStr = String.valueOf(newStock);
// 兼容旧文件中的"无限"文本:转为-1用于比较
int oldStockNum;
try {
oldStockNum = Integer.parseInt(oldStock);
} catch (NumberFormatException e) {
oldStockNum = ("无限".equals(oldStock) || "∞".equals(oldStock)) ? -1 : 0;
}
// 不要把Web端的0库存覆盖到游戏的无限库存(-1)
if (newStock == 0 && oldStockNum == -1) continue;
// ★ 管理员手动修改的库存:跳过保护逻辑,直接应用
// 管理员设stock=0表示"售罄",需要同步到游戏
if (!adminOverride.contains(itemId)) {
// stock<-1 为未定义值(-2以下),不生效,跳过同步
// -1=无限库存, 0=售罄, >=1=有库存,都正常同步
if (newStock < -1) continue;
}
if (oldStockNum != newStock) {
cols[6] = " " + newStockStr + " ";
@@ -3711,9 +3965,12 @@ public class WebManager {
plugin.getLogger().info("[Web通信] 已同步Web端商品库存,更新了 " + updatedCount + " 个商品");
lastPullShopStockLog = now;
}
return true;
}
return false;
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 更新商品库存失败: " + e.getMessage());
return false;
}
}
@@ -3724,20 +3981,38 @@ public class WebManager {
try {
File notifyFile = new File(plugin.getDataFolder(), "../sync_notify.txt");
if (notifyFile.exists()) {
long notifyTime = Long.parseLong(new String(java.nio.file.Files.readAllBytes(notifyFile.toPath())).trim());
String content = new String(java.nio.file.Files.readAllBytes(notifyFile.toPath())).trim();
// ★ 兼容两种格式:纯数字时间戳 或 JSON {"time":xxx,"tx_id":xxx}
long notifyTime = 0;
if (content.startsWith("{")) {
// JSON格式
int timeIdx = content.indexOf("\"time\":");
if (timeIdx > 0) {
int start = timeIdx + 8;
int end = content.indexOf(",", start);
if (end < 0) end = content.indexOf("}", start);
if (end > start) {
notifyTime = Long.parseLong(content.substring(start, end).trim());
}
}
} else {
notifyTime = Long.parseLong(content);
}
long now = System.currentTimeMillis();
// 如果通知时间在5分钟内,执行同步
if (now - notifyTime < 300000) {
if (notifyTime > 0 && now - notifyTime < 300000) {
// 有人在线才响应通知
if (!Bukkit.getOnlinePlayers().isEmpty()) {
syncShopData();
syncBondBalances();
syncBondTransactions();
pushWebLoginCredentials();
// ★ 先拉取(PHP→Java),再推送(Java→PHP),确保不覆盖管理员修改
pullPendingTransactions();
pullShopStock();
pullBondChanges();
syncOnlinePlayers();
pushWebLoginCredentials();
syncUserRegistrations();
syncServiceProviders();
syncShopData();
// 删除通知文件
notifyFile.delete();
}
@@ -4029,6 +4304,12 @@ public class WebManager {
plugin.getLogger().info("[Web注册] 游戏账号创建成功: " + fName);
}
// ★ 写入注册IP到login.db
if (!fIp.isEmpty()) {
dbMgr.setField(fName, "ip_address", fIp);
dbMgr.setField(fName, "register_ip", fIp);
}
// ★ 如果玩家当前在线,自动登录
Player onlinePlayer = Bukkit.getPlayer(fName);
if (onlinePlayer != null && onlinePlayer.isOnline()) {
@@ -4155,6 +4436,12 @@ public class WebManager {
dbMgr.createUser(fName, fHash, fSalt);
plugin.getLogger().info("[Web注册] 补全成功: " + fName);
// ★ 写入注册IP到login.db
if (!fIp.isEmpty()) {
dbMgr.setField(fName, "ip_address", fIp);
dbMgr.setField(fName, "register_ip", fIp);
}
Player onlinePlayer = Bukkit.getPlayer(fName);
if (onlinePlayer != null && onlinePlayer.isOnline()) {
Bukkit.getScheduler().runTask(plugin, () -> {
@@ -4303,6 +4590,7 @@ public class WebManager {
int points = Integer.parseInt(extractJsonNumber(dataJson, "points") != null ? extractJsonNumber(dataJson, "points") : "0");
int giftStage = Integer.parseInt(extractJsonNumber(dataJson, "gift_stage") != null ? extractJsonNumber(dataJson, "gift_stage") : "0");
int totalOnlineTime = Integer.parseInt(extractJsonNumber(dataJson, "total_online_time") != null ? extractJsonNumber(dataJson, "total_online_time") : "0");
String ipAddress = extractJsonString(dataJson, "ip_address");
plugin.getLogger().info("[Web注册] 玩家在PHP后端已注册: " + playerName + ", 有密码凭证:" + !extractJsonString(dataJson, "password_hash").isEmpty());
@@ -4310,6 +4598,7 @@ public class WebManager {
final String fEmail = email != null ? email : "";
final String fHash = passwordHash != null ? passwordHash : "";
final String fSalt = salt != null ? salt : "";
final String fIp = ipAddress != null ? ipAddress : "";
final long fRegisterTime = registerTime;
final long fLastLoginTime = lastLoginTime;
final int fPoints = points;
@@ -4333,6 +4622,11 @@ public class WebManager {
if (!fHash.isEmpty() && !fSalt.isEmpty()) {
dbMgr.createUser(fName, fHash, fSalt);
plugin.getLogger().info("[Web注册] 用户 " + fName + " 已从PHP后端同步注册数据到Java本地(含密码凭证)");
// ★ 写入注册IP到login.db
if (!fIp.isEmpty()) {
dbMgr.setField(fName, "ip_address", fIp);
dbMgr.setField(fName, "register_ip", fIp);
}
} else {
// PHP后端没有密码凭证,可能是通过register.php的webRegister()直接注册的
// 需要等待Java插件通过pollWebRegisterRequests()轮询后创建用户
+1 -1
View File
@@ -1,5 +1,5 @@
name: Sdf1_login
version: 2.5
version: 2.6
main: Sdf1_login.Main
api-version: '1.21'
commands: