Compare commits

...
2 Commits
Author SHA1 Message Date
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
15 changed files with 4599 additions and 129 deletions
+26 -1
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_25" default="true" project-jdk-name="25" 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兑换→余额同步、扣款方向正确
+1
View File
@@ -567,3 +567,4 @@ http-port=443
## 4. 下载地址 ## 4. 下载地址
- 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)
+1
View File
@@ -59,6 +59,7 @@
<configuration> <configuration>
<source>25</source> <source>25</source>
<target>25</target> <target>25</target>
<encoding>UTF-8</encoding>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
+49 -60
View File
@@ -1639,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 =
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 = Set<String> areaList =
areaPlayerWhitelist.get(ac.name); areaPlayerWhitelist.get(areaName);
return areaList != null return areaList != null
&& areaList.contains(player.toLowerCase()); && areaList.contains(player.toLowerCase());
} }
@@ -2414,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())) {
@@ -4449,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禁止破坏展示框");
@@ -4498,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;
}
// 非白名单 → 拦截 // 非白名单 → 拦截
// 记录当前旋转角度 // 记录当前旋转角度
@@ -4564,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禁止破坏展示框");
@@ -4638,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) {
@@ -172,6 +172,10 @@ 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) {
plugin.webManager.syncOnlinePlayers();
}
// 登录成功关闭飞行 // 登录成功关闭飞行
p.setAllowFlight(false); p.setAllowFlight(false);
p.setFlying(false); p.setFlying(false);
+229 -59
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,25 @@ 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.inventory.CraftItemEvent;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.player.*;
import org.bukkit.event.player.PlayerQuitEvent;
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 +73,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 +164,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 +363,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 +474,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 +545,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 +682,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 +735,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 +783,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 +1167,11 @@ public class Main extends JavaPlugin
joinTime.remove(p.getUniqueId()); joinTime.remove(p.getUniqueId());
recordIPLogin(p); recordIPLogin(p);
// ★ 立即同步在线状态到PHP(不等下次心跳30秒)
if (webManager != null) {
webManager.syncOnlinePlayers();
}
// 只在有有效备份时恢复 // 只在有有效备份时恢复
restoreInventory(p); restoreInventory(p);
@@ -1172,6 +1189,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 +1718,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, () -> {
@@ -4136,6 +4219,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;
} }
@@ -4156,6 +4242,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;
} }
@@ -4186,6 +4275,32 @@ public class Main extends JavaPlugin
} }
return shopManager.handleCommand(sender, args); return shopManager.handleCommand(sender, args);
} }
// ★ 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)) {
@@ -4203,13 +4318,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()) {
// 获取最近登录信息 // 获取最近登录信息
@@ -4235,7 +4368,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);
// ★ 额外发送安全通知邮件 // ★ 额外发送安全通知邮件
@@ -4264,6 +4397,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())
@@ -4698,6 +4838,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(
@@ -4742,6 +4896,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);
@@ -4777,6 +4934,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())
@@ -5013,6 +5173,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 命令改密码成功后
@@ -5287,13 +5452,18 @@ public class Main extends JavaPlugin
"password_same")); "password_same"));
return true; return true;
} }
recordPasswordChange( recordPasswordChange(
p2.getName(), p2.getName(),
oldHash, salt); oldHash, salt);
db.setField(p2.getName(), db.setField(p2.getName(),
"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());
+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 =
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -1,5 +1,5 @@
name: Sdf1_login name: Sdf1_login
version: 2.1 version: 2.3
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:
@@ -104,4 +104,10 @@ commands:
usage: /protect usage: /protect
update: update:
description: 检查所有插件更新(集群控制) description: 检查所有插件更新(集群控制)
usage: /update usage: /update
web:
description: 生成Web端登录令牌
usage: /web
控制台:
description: 生成Web端登录令牌(中文版)
usage: /控制台