From ebcb11338cf2e6fbf04973f6dbc593903a9a246e Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Mon, 22 Jun 2026 16:26:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DWeb=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=99=E6=B3=A8=E5=86=8CIP=E3=80=81=E7=8E=A9?= =?UTF-8?q?=E5=AE=B6=E7=99=BB=E5=BD=95IP=E7=9A=84bug;=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=BA=93=E5=AD=98=E9=AB=98=E9=A2=91=E8=BD=AE=E8=AF=A2;=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0README=E4=B8=8B=E8=BD=BD=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README.md.bak | 571 ++++++++++++++++++++++ src/main/java/Sdf1_login/GUIManager.java | 163 +++++- src/main/java/Sdf1_login/Main.java | 156 +++--- src/main/java/Sdf1_login/MenuManager.java | 28 +- src/main/java/Sdf1_login/WebManager.java | 338 ++++++++++++- src/main/resources/plugin.yml | 2 +- 7 files changed, 1154 insertions(+), 106 deletions(-) create mode 100644 README.md.bak diff --git a/README.md b/README.md index 74ed300..c8e92f3 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/README.md.bak b/README.md.bak new file mode 100644 index 0000000..c03880a --- /dev/null +++ b/README.md.bak @@ -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)
+同时兼容` `标签,对段落加粗,`\n 和
`进行段落换行 + + +```txt +# ========================================== +# Sdf1_login 消息配置文件 +# ========================================== +# 颜色代码使用 § 符号 +# §0黑色 §1深蓝 §2深绿 §3深青 +# §4深红 §5深紫 §6金色 §7灰色 +# §8深灰 §9蓝色 §a绿色 §b浅蓝 +# §c红色 §d粉色 §e黄色 §f白色 +# §l粗体 §n下划线 §o斜体 §r重置 +# +# 占位符用 {xxx} 表示,运行时自动替换 +# 支持 粗体 标签 +# 支持 \n 或
换行 +# 修改后执行 /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邮件服务器的配置,用于找回密码时发送邮件。
可通过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 +该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。
+可用的处罚规则: +- 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 商品配置 +- 配置此处的文件,负责展示游戏内所销售的商品。
+- 一个文件名,对应一个商品分类,使用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. 自助退款,买多买错,不用慌,一件退单很安心
+2. 购物车,买的多怕买错,先加购物车,后续统一结算
+3. 害怕订单找不到?一键查看历史订单
+ ![](img/shop4.png) +4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖 +![](img/shop7.png)
+![](img/shop6.png) +5. 想查看玩家消费记录?小手一点,明明白白
+![](img/shop5.png) + + + + +## 2.4 打印流水部分介绍 +1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天
+2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删 + + + +## 3. 指令参数及介绍 + +- Sdf1_login 主指令
+- Sdf1_login reload 重载配置文件
+- reg 注册账号
+- login 登录账号
+- l 登录账号
+- sdf1_login del 删除玩家账号
+- sdf1_login radio 停止播放广播
+- sdf1_login radio reload 重载广播并重新打包资源包
+- menu 开/关 是否开启菜单
+- printer 打印流水数据,支持不带参数&带玩家名为参数
+- sdf1_login printer 作用同printer
+- sdf1_login shop 商城 +- protect 区域防护 + + + +## 4. 自定义菜单物品 +- 打开主界面,选择"我的"进入
+![](img/menu1.png) +- 将你的自定义物品与雪球交换
+![](img/menu2.png) +- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了 + +## 5. pvp战神榜 +- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域
+![](img/pvp1.png) +- 并且有着网游同款的击杀通报
+![](img/pvp3.png) +- 播报逻辑是:
+- 3杀
+- 5杀
+- 5杀以上每杀一次都是一条播报
+- 终结者通报,触发连杀的玩家被终结也会有全服通报 + +## 6. 区域防护 +- 通过此功能,您可在服务器里需要防护的地方(比如展示框不想被玩家随便破坏、主城不准玩家袭击等)加以防护。
+- 还能够保护指定区域的玩家不受任何伤害。
+ +| 指令 | 参数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玩家到末地保护区
+- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品
+- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。
+- 所有参数`任意排序`,`区域名字`随便填写,强大的`兜底`会自动识别"宝宝画板",你可以放心、大胆的乱写 + + +| 参数 | 效果 | +| :---: | :---: | +| 和平模式 | 所有敌对生物全部进虚空 | +| 禁止袭击 | 立即清理玩家身上的袭击效果,传灾厄村民进虚空 | +| 清除XX效果 | 立即清除玩家身上的效果 +| 给与xx效果 | 根据配置文件,给予玩家指定等级、时长的效果 + +## 生物属性对照表 +| 中文名称 | 类型 | 附加属性 | +| :---: | :----: | :----: | +| 溺尸 | 近战/远程 | 亡灵、水生 | +| 骷髅 | 远程 | 亡灵 | +| 流浪者 | 远程 | 亡灵、寒冷群系变种 | +| 沼骸 | 远程 | 亡灵、沼泽变种 | +| 焦骸 | 远程 | 亡灵、沙漠变种、免疫阳光 | +| 掠夺者 | 远程 | 灾厄村民、袭击成员 | +| 卫道士 | 近战 | 灾厄村民、袭击成员 | +| 幻术师 | 远程 | 灾厄村民、Java专属、隐身分身 | +| 旋风人 | 远程 | 试炼密室生成、免疫弹射物、强击退 | +| 监守者 | 近战/声波远程 | 深暗之域、附带盲视效果 | +| 远古守卫者 | 近战/激光远程 | 海底神殿、水下、附加挖掘疲劳 | +| 守卫者 | 近战/激光远程 | 海底神殿、水下 | + + +配置文件解析:
+- 支持注释格式:`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) diff --git a/src/main/java/Sdf1_login/GUIManager.java b/src/main/java/Sdf1_login/GUIManager.java index 9b5de17..ce49d13 100644 --- a/src/main/java/Sdf1_login/GUIManager.java +++ b/src/main/java/Sdf1_login/GUIManager.java @@ -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 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 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 lines = - java.nio.file.Files - .readAllLines( - f.toPath(), - java.nio.charset - .StandardCharsets - .UTF_8); List 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, diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 6895ac7..433bbef 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -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; diff --git a/src/main/java/Sdf1_login/MenuManager.java b/src/main/java/Sdf1_login/MenuManager.java index d44ba05..71bd280 100644 --- a/src/main/java/Sdf1_login/MenuManager.java +++ b/src/main/java/Sdf1_login/MenuManager.java @@ -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), diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index ea7bf2c..e43cea2 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -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> providers = new ArrayList<>(); + for (Map sp : plugin.getDb().getAllServiceProviders()) { + Map 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 body = new LinkedHashMap<>(); + body.put("data", providers); + + String response = httpPostWithToken("api/sync.php?action=sync_service_providers", token, body); + + if (response != null) { + Map 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" - + "®istered=" + (isRegistered ? "1" : "0"); + + "®istered=" + (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" - + "®istered=" + (isRegistered ? "1" : "0"); + + "®istered=" + (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 webStock = new HashMap<>(); + // ★ 跟踪哪些商品是管理员手动修改的(admin_stock_override=true) + Set 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()轮询后创建用户 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index c372b4c..4db8866 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: Sdf1_login -version: 2.5 +version: 2.6 main: Sdf1_login.Main api-version: '1.21' commands: