Compare commits

..
2 Commits
Author SHA1 Message Date
youpaishidifu 203c9d423a 新增Web CDK接口,可对接Web系统进行兑换 2026-06-20 14:02:41 +08:00
youpaishidifu 2c7f765fb0 初始化登录插件 2026-06-19 16:00:29 +08:00
6 changed files with 746 additions and 33 deletions
+60
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";
@@ -364,6 +385,8 @@ public class BondManager {
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); e.printStackTrace();
} }
// ★ 通知监听器:交易已记录
notifyTransactionListeners(player, type, amount);
} }
@@ -697,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;
}
} }
@@ -172,6 +172,16 @@ public class LoginManager {
plugin.getDb().setField(name, plugin.getDb().setField(name,
"last_online_check", "last_online_check",
System.currentTimeMillis()); System.currentTimeMillis());
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
if (plugin.webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
plugin.webManager.syncOnlinePlayers();
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
}
});
}
// 登录成功关闭飞行 // 登录成功关闭飞行
p.setAllowFlight(false); p.setAllowFlight(false);
p.setFlying(false); p.setFlying(false);
+17
View File
@@ -474,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);
@@ -1161,6 +1167,17 @@ public class Main extends JavaPlugin
joinTime.remove(p.getUniqueId()); joinTime.remove(p.getUniqueId());
recordIPLogin(p); recordIPLogin(p);
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
if (webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
try {
webManager.syncOnlinePlayers();
} catch (Exception e) {
getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
}
});
}
// 只在有有效备份时恢复 // 只在有有效备份时恢复
restoreInventory(p); restoreInventory(p);
+44 -12
View File
@@ -48,12 +48,16 @@ public class UpdateChecker {
private static final String GITHUB_REPO = "sdf1_login"; private static final String GITHUB_REPO = "sdf1_login";
private static final String GITEE_OWNER = "nihaoshidifu"; private static final String GITEE_OWNER = "nihaoshidifu";
private static final String GITEE_REPO = "sdf1_login"; private static final String GITEE_REPO = "sdf1_login";
private static final String GYP_OWNER = "youpaishidifu";
private static final String GYP_REPO = "Sdf1_login";
// API 地址(使用releases/latest获取最新版本) // API 地址(使用releases/latest获取最新版本)
private static final String GITHUB_API = private static final String GITHUB_API =
"https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest"; "https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest";
private static final String GITEE_API = private static final String GITEE_API =
"https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest"; "https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest";
private static final String GYP_API =
"https://git.ypshidifu.cn/api/v1/repos/" + GYP_OWNER + "/" + GYP_REPO + "/releases/latest";
// 第三路备选:宝塔静态HTML // 第三路备选:宝塔静态HTML
private static final String FALLBACK_HTML = private static final String FALLBACK_HTML =
@@ -64,6 +68,8 @@ public class UpdateChecker {
"https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases"; "https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases";
private static final String GITEE_LINK = private static final String GITEE_LINK =
"https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases"; "https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases";
private static final String GYP_LINK =
"https://git.ypshidifu.cn/" + GYP_OWNER + "/" + GYP_REPO + "/releases";
// 单次请求超时(毫秒) // 单次请求超时(毫秒)
private static final int TIMEOUT_MS = 5000; private static final int TIMEOUT_MS = 5000;
@@ -157,18 +163,31 @@ public class UpdateChecker {
plugin.getLogger().info("[Sdf1_login] 启动自动检查更新..."); plugin.getLogger().info("[Sdf1_login] 启动自动检查更新...");
} }
// ★ 双通道并行执行,一路成功掐断另一路 // ★ 三通道并行执行,一路成功掐断其余
AtomicBoolean cancelled = new AtomicBoolean(false); AtomicBoolean cancelled = new AtomicBoolean(false);
AtomicReference<CheckResult> gypResult = new AtomicReference<>();
AtomicReference<CheckResult> ghResult = new AtomicReference<>(); AtomicReference<CheckResult> ghResult = new AtomicReference<>();
AtomicReference<CheckResult> geResult = new AtomicReference<>(); AtomicReference<CheckResult> geResult = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(2); CountDownLatch latch = new CountDownLatch(3);
// 通道0:git.ypshidifu.cn(最高优先级,Gitea格式)
Thread gypThread = new Thread(() -> {
if (!cancelled.get()) {
gypResult.set(tryChannel(GYP_API, "git.ypshidifu.cn", GYP_LINK));
if (gypResult.get().isSuccess()) {
cancelled.set(true);
}
}
latch.countDown();
});
gypThread.setDaemon(true);
// 通道1:GitHub // 通道1:GitHub
Thread ghThread = new Thread(() -> { Thread ghThread = new Thread(() -> {
if (!cancelled.get()) { if (!cancelled.get()) {
ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK)); ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK));
if (ghResult.get().isSuccess()) { if (ghResult.get().isSuccess()) {
cancelled.set(true); // 成功则取消另一路 cancelled.set(true);
} }
} }
latch.countDown(); latch.countDown();
@@ -180,34 +199,38 @@ public class UpdateChecker {
if (!cancelled.get()) { if (!cancelled.get()) {
geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK)); geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK));
if (geResult.get().isSuccess()) { if (geResult.get().isSuccess()) {
cancelled.set(true); // 成功则取消另一路 cancelled.set(true);
} }
} }
latch.countDown(); latch.countDown();
}); });
geThread.setDaemon(true); geThread.setDaemon(true);
gypThread.start();
ghThread.start(); ghThread.start();
geThread.start(); geThread.start();
try { try {
latch.await(); // 等待两路都完成 latch.await();
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
CheckResult gyp = gypResult.get();
CheckResult gh = ghResult.get(); CheckResult gh = ghResult.get();
CheckResult ge = geResult.get(); CheckResult ge = geResult.get();
// 选择成功的结果 // 选择成功的结果(优先级:git.ypshidifu.cn > GitHub > Gitee)
CheckResult chosen = null; CheckResult chosen = null;
if (gh != null && gh.isSuccess()) { if (gyp != null && gyp.isSuccess()) {
chosen = gyp;
} else if (gh != null && gh.isSuccess()) {
chosen = gh; chosen = gh;
} else if (ge != null && ge.isSuccess()) { } else if (ge != null && ge.isSuccess()) {
chosen = ge; chosen = ge;
} else { } else {
// ★ 双通道均失败,尝试第三路备选(宝塔静态HTML) // ★ 双通道均失败,尝试第三路备选(宝塔静态HTML)
plugin.getLogger().info("[Sdf1_login] GitHub/Gitee 均失败,尝试第三路备选 BaoTa..."); plugin.getLogger().info("[Sdf1_login] git.ypshidifu.cn/GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
CheckResult fbResult = tryFallbackChannel(); CheckResult fbResult = tryFallbackChannel();
if (fbResult != null && fbResult.isSuccess()) { if (fbResult != null && fbResult.isSuccess()) {
chosen = fbResult; chosen = fbResult;
@@ -215,7 +238,11 @@ public class UpdateChecker {
} else { } else {
failCount++; failCount++;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (gyp != null && gyp.error != null) {
sb.append("§7git.ypshidifu.cn: §c").append(gyp.error);
}
if (gh != null && gh.error != null) { if (gh != null && gh.error != null) {
if (sb.length() > 0) sb.append("\n");
sb.append("§7GitHub: §c").append(gh.error); sb.append("§7GitHub: §c").append(gh.error);
} }
if (ge != null && ge.error != null) { if (ge != null && ge.error != null) {
@@ -310,10 +337,15 @@ public class UpdateChecker {
sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本"); sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本");
} }
// 显示另一通道的失败原因 // 显示其他失败通道的错误信息
CheckResult other = (src.equals("GitHub")) ? ge : gh; if (gyp != null && !gyp.isSuccess() && gyp.error != null && !src.equals("git.ypshidifu.cn")) {
if (other != null && !other.isSuccess() && other.error != null) { sender.sendMessage("§7[Sdf1_login] [更新] git.ypshidifu.cn 不可用: " + gyp.error);
sender.sendMessage("§7[Sdf1_login] [更新] " + other.source + " 不可用: " + other.error); }
if (gh != null && !gh.isSuccess() && gh.error != null && !src.equals("GitHub")) {
sender.sendMessage("§7[Sdf1_login] [更新] GitHub 不可用: " + gh.error);
}
if (ge != null && !ge.isSuccess() && ge.error != null && !src.equals("Gitee")) {
sender.sendMessage("§7[Sdf1_login] [更新] Gitee 不可用: " + ge.error);
} }
}); });
} }
+614 -20
View File
@@ -124,6 +124,7 @@ public class WebManager {
syncIntervalMinutes = Integer.parseInt(getConfigValue("web通信-同步间隔分钟", "5")); syncIntervalMinutes = Integer.parseInt(getConfigValue("web通信-同步间隔分钟", "5"));
callbackPort = Integer.parseInt(getConfigValue("web通信-回调端口", "9090")); callbackPort = Integer.parseInt(getConfigValue("web通信-回调端口", "9090"));
secretKey = getConfigValue("web通信-密钥", secretKey); secretKey = getConfigValue("web通信-密钥", secretKey);
plugin.getLogger().info("[Web通信] 后端地址: " + webBaseUrl + " | 启用: " + enabled);
} }
/** /**
@@ -186,6 +187,16 @@ public class WebManager {
// ==================== 启动/停止 ==================== // ==================== 启动/停止 ====================
// ★ 注册交易监听器(需在BondManager初始化后调用)
public void registerTransactionListener(BondManager bondMgr) {
if (bondMgr == null) return;
bondMgr.addTransactionListener((playerName, type, amount) -> {
plugin.getLogger().info("[Web交易即时] 检测到交易: " + playerName + " " + type + " " + amount + ",立即推送");
requestImmediateTransactionSync();
});
plugin.getLogger().info("[Web通信] 交易即时推送监听器已注册");
}
public void start() { public void start() {
if (!enabled) { if (!enabled) {
plugin.getLogger().info("[Web通信] 未启用,在插件设置.txt中设置 web通信-启用=true"); plugin.getLogger().info("[Web通信] 未启用,在插件设置.txt中设置 web通信-启用=true");
@@ -224,6 +235,7 @@ public class WebManager {
syncOnlinePlayers(); syncOnlinePlayers();
syncShopData(); syncShopData();
syncBondBalances(); syncBondBalances();
syncBondTransactions();
syncAllPlayerIps(); syncAllPlayerIps();
// 首次同步只打一次,后续由调度器静默执行 // 首次同步只打一次,后续由调度器静默执行
plugin.getLogger().info("[Web通信] 首次全量同步完成"); plugin.getLogger().info("[Web通信] 首次全量同步完成");
@@ -244,6 +256,12 @@ public class WebManager {
scheduleNextSync(); scheduleNextSync();
startActiveSync(); startActiveSync();
// ★ 启动交易高频轮询(每5秒检查PHP端是否有待处理交易)
startTransactionPolling();
// ★ 启动CDK验证轮询(每2秒检查PHP端CDK验证请求)
startCdkValidationPolling();
plugin.getLogger().info("[Web通信] 实时同步调度器已启动(60~90秒随机,无人停止,有人恢复)"); plugin.getLogger().info("[Web通信] 实时同步调度器已启动(60~90秒随机,无人停止,有人恢复)");
plugin.getLogger().info("[Web通信] PHP回调服务器已启动,端口: " + callbackPort); plugin.getLogger().info("[Web通信] PHP回调服务器已启动,端口: " + callbackPort);
} }
@@ -540,6 +558,39 @@ public class WebManager {
}.runTaskAsynchronously(plugin); }.runTaskAsynchronously(plugin);
} }
// ★ 交易即时推送触发器:交易发生后立即推送交易记录到PHP
private volatile long lastImmediateTxSyncTime = 0;
private static final long MIN_IMMEDIATE_TX_SYNC_MS = 3000; // 最小间隔3秒,防止频繁推送
private volatile boolean pendingImmediateTxSync = false;
private void requestImmediateTransactionSync() {
long now = System.currentTimeMillis();
if (now - lastImmediateTxSyncTime < MIN_IMMEDIATE_TX_SYNC_MS) {
// 间隔太短,标记待处理
pendingImmediateTxSync = true;
return;
}
lastImmediateTxSyncTime = now;
pendingImmediateTxSync = false;
new BukkitRunnable() {
@Override
public void run() {
try {
syncBondTransactions();
} catch (Exception e) {
plugin.getLogger().warning("[Web交易即时] 推送异常: " + e.getMessage());
}
// 检查是否有待处理的推送
if (pendingImmediateTxSync) {
pendingImmediateTxSync = false;
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
try { syncBondTransactions(); } catch (Exception e) {}
}, 40L); // 2秒后执行
}
}
}.runTaskAsynchronously(plugin);
}
private void startActiveSync() { private void startActiveSync() {
new BukkitRunnable() { new BukkitRunnable() {
@Override @Override
@@ -605,6 +656,7 @@ public class WebManager {
syncOnlinePlayers(); syncOnlinePlayers();
pushWebLoginCredentials(); pushWebLoginCredentials();
syncUserRegistrations(); syncUserRegistrations();
syncBondTransactions();
plugin.getLogger().info("[Web通信] 检测到全员下线超60秒,执行最后一轮同步,调度器继续运行(玩家上线自动恢复)"); plugin.getLogger().info("[Web通信] 检测到全员下线超60秒,执行最后一轮同步,调度器继续运行(玩家上线自动恢复)");
plugin.getLogger().warning("\n" + plugin.getLogger().warning("\n" +
" _ \n" + " _ \n" +
@@ -654,6 +706,7 @@ public class WebManager {
syncOnlinePlayers(); syncOnlinePlayers();
pushWebLoginCredentials(); pushWebLoginCredentials();
syncUserRegistrations(); syncUserRegistrations();
syncBondTransactions();
syncAllPlayerIps(); syncAllPlayerIps();
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[Web通信] 全量同步异常: " + e.getMessage()); plugin.getLogger().warning("[Web通信] 全量同步异常: " + e.getMessage());
@@ -667,6 +720,303 @@ public class WebManager {
}.runTaskTimerAsynchronously(plugin, 20L * 5, 20L * 5); // 每5秒检查一次 }.runTaskTimerAsynchronously(plugin, 20L * 5, 20L * 5); // 每5秒检查一次
} }
/**
* 交易高频轮询:每5秒检查PHP端是否有待处理交易
* 检测到pending交易时立即拉取,不等60-90秒的全量同步
*/
/**
* 交易高频轮询:每5秒检查PHP端是否有待处理交易
* ★ 修复:移除activeSyncStopped检查,即使无在线玩家也要轮询(管理员可能在Web后台操作)
* 支持MC服务器和Web服务器不在同一台机器的场景
*/
private void startTransactionPolling() {
new BukkitRunnable() {
@Override
public void run() {
// ★ 不再检查activeSyncStopped,即使无在线玩家也要轮询Web交易
try {
String urlStr = webBaseUrl + "/api/sync.php?action=check_pending_transactions&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(8000);
conn.setReadTimeout(10000);
int code = conn.getResponseCode();
if (code != 200) {
plugin.getLogger().warning("[Web交易轮询] HTTP状态码: " + code);
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();
// 快速解析 "pending":N
int idx = resp.indexOf("\"pending\":");
if (idx >= 0) {
idx += 10;
int end = resp.indexOf("}", idx);
if (end < 0) end = resp.length();
String val = resp.substring(idx, end).replaceAll("[^0-9]", "");
int pending = Integer.parseInt(val);
if (pending > 0) {
plugin.getLogger().info("[Web交易轮询] 检测到 " + pending + " 笔待处理交易,立即拉取");
pullPendingTransactions();
}
} else {
plugin.getLogger().warning("[Web交易轮询] 响应格式异常: " + resp.substring(0, Math.min(200, resp.length())));
}
} catch (Exception e) {
plugin.getLogger().warning("[Web交易轮询] 异常: " + e.getMessage());
}
}
}.runTaskTimerAsynchronously(plugin, 20L * 5, 20L * 5); // 每5秒检查一次
}
// ==================== CDK验证轮询 ====================
/**
* 每2秒检查PHP端的CDK验证请求,本地验证后推送结果回去
* 让Web前端的CDK兑换可以验证Java端(bond.db)中的CDK
* 同时拉取sdf1插件的pending远程验证请求,发送到Web后端
*/
private void startCdkValidationPolling() {
new BukkitRunnable() {
@Override
public void run() {
try {
// === Part 1: 拉取Web端的CDK验证请求 → 本地验证 ===
pullWebCdkRequestsAndValidate();
// === Part 2: 拉取sdf1插件的pending请求 → 发送到Web验证 ===
pullSdf1PendingAndValidateWeb();
} catch (Exception e) {
// 静默,避免刷屏
}
}
}.runTaskTimerAsynchronously(plugin, 20L * 2, 20L * 2); // 每2秒检查一次
plugin.getLogger().info("[Web通信] CDK远程验证轮询已启动(每2秒)");
}
/**
* Part 1: Web端写cdk_validate_requests → Sdf1_login拉取 → 本地CDKManager.redeem → 写回结果
*/
private void pullWebCdkRequestsAndValidate() {
try {
String listUrl = webBaseUrl + "/api/sync.php?action=pull_cdk_validate_requests&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
URL url = new URL(listUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Sdf1-CDKValidator/1.0");
conn.setConnectTimeout(8000);
conn.setReadTimeout(10000);
int respCode = conn.getResponseCode();
if (respCode != 200) { 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 json = sb.toString();
if (!json.contains("\"success\":true")) return;
int reqIdx = json.indexOf("\"requests\":[");
if (reqIdx < 0) return;
int arrStart = json.indexOf('[', reqIdx);
int arrEnd = json.indexOf(']', arrStart);
if (arrEnd < 0) return;
String arr = json.substring(arrStart, arrEnd + 1);
if (arr.equals("[]")) return;
int pi = 0;
while (pi < arr.length()) {
int objStart = arr.indexOf('{', pi);
if (objStart < 0) break;
int objEnd = arr.indexOf('}', objStart);
if (objEnd < 0) break;
String obj = arr.substring(objStart, objEnd + 1);
pi = objEnd + 1;
String requestId = extractJsonStr(obj, "request_id");
String cdkCode = extractJsonStr(obj, "code");
String playerName = extractJsonStr(obj, "player_name");
if (requestId.isEmpty() || cdkCode.isEmpty()) continue;
if (playerName.isEmpty()) playerName = "web_remote";
// ★ 直接检查sdf1计分板CDK(bond.db只是钱包,不存CDK)
String status = "not_found";
int amount = 0;
try {
Object sdf1Plugin = Bukkit.getPluginManager().getPlugin("sdf1");
if (sdf1Plugin != null) {
plugin.getLogger().info("[CDK远程验证] 调用sdf1检查计分板: " + cdkCode);
java.lang.reflect.Method checkMethod = sdf1Plugin.getClass().getMethod("checkScoreBoardCdk", String.class);
String[] sbResult = (String[]) checkMethod.invoke(sdf1Plugin, cdkCode);
plugin.getLogger().info("[CDK远程验证] sdf1返回: " + (sbResult != null ? String.join(",", sbResult) : "null"));
if (sbResult != null && sbResult.length >= 2 && "success".equals(sbResult[0])) {
status = "success";
amount = Integer.parseInt(sbResult[1]);
plugin.getLogger().info("[CDK远程验证] 计分板CDK匹配: " + cdkCode + " 金额=" + amount);
} else if (sbResult != null && "not_bond".equals(sbResult[0])) {
status = "not_bond";
plugin.getLogger().info("[CDK远程验证] 计分板CDK存在但非债券类型: " + cdkCode);
} else {
plugin.getLogger().info("[CDK远程验证] 计分板CDK未找到: " + cdkCode);
}
} else {
plugin.getLogger().warning("[CDK远程验证] sdf1插件未加载");
}
} catch (Exception e) {
plugin.getLogger().warning("[CDK远程验证] 检查计分板异常: " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
// 推送结果回PHP
String pushUrl = webBaseUrl + "/api/sync.php?action=push_cdk_validate_result&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&request_id=" + java.net.URLEncoder.encode(requestId, "UTF-8")
+ "&code=" + java.net.URLEncoder.encode(cdkCode, "UTF-8")
+ "&status=" + java.net.URLEncoder.encode(status, "UTF-8")
+ "&amount=" + amount;
URL pushUrlObj = new URL(pushUrl);
HttpURLConnection pushConn = (HttpURLConnection) pushUrlObj.openConnection();
pushConn.setRequestMethod("GET");
pushConn.setConnectTimeout(8000);
pushConn.setReadTimeout(10000);
pushConn.getResponseCode();
pushConn.disconnect();
if (!"not_found".equals(status)) {
plugin.getLogger().info("[CDK远程验证] " + cdkCode + " → " + status + " player=" + playerName + (amount > 0 ? " 金额:" + amount : ""));
}
}
} catch (Exception e) {
// 静默
}
}
/**
* Part 2: sdf1插件pending队列 → Sdf1_login拉取 → 发送Web validate_cdk → 结果回传sdf1
*/
private void pullSdf1PendingAndValidateWeb() {
try {
// 通过反射获取sdf1插件(避免直接依赖Plugin类)
Object sdf1Plugin = Bukkit.getPluginManager().getPlugin("sdf1");
if (sdf1Plugin == null) return;
// 检查是否启用
java.lang.reflect.Method isEnabled = sdf1Plugin.getClass().getMethod("isEnabled");
if (!(Boolean) isEnabled.invoke(sdf1Plugin)) return;
// 调用 sdf1.Main.pullPendingCdkValidations()
java.lang.reflect.Method pullMethod = sdf1Plugin.getClass().getMethod("pullPendingCdkValidations");
Object[][] pendingList = (Object[][]) pullMethod.invoke(sdf1Plugin);
if (pendingList == null || pendingList.length == 0) return;
for (Object[] item : pendingList) {
String requestId = (String) item[0];
String cdkCode = (String) item[1];
String playerName = (String) item[2];
// ★ 先检查CDK是否存在(只读)
String status = "not_found";
int amount = 0;
try {
String validateUrl = webBaseUrl + "/api/sync.php?action=check_cdk_exists&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&code=" + java.net.URLEncoder.encode(cdkCode, "UTF-8");
URL vUrl = new URL(validateUrl);
HttpURLConnection vConn = (HttpURLConnection) vUrl.openConnection();
vConn.setRequestMethod("GET");
vConn.setConnectTimeout(8000);
vConn.setReadTimeout(10000);
int vCode = vConn.getResponseCode();
if (vCode == 200) {
BufferedReader vReader = new BufferedReader(new InputStreamReader(vConn.getInputStream(), StandardCharsets.UTF_8));
StringBuilder vSb = new StringBuilder();
String vLine;
while ((vLine = vReader.readLine()) != null) vSb.append(vLine);
vReader.close();
vConn.disconnect();
String vJson = vSb.toString();
// ★ 详细日志:PHP返回的原始JSON
plugin.getLogger().info("[CDK-Web验证] PHP原始返回: " + vJson);
String found = extractJsonStr(vJson, "found");
String st = extractJsonStr(vJson, "status");
String am = extractJsonStr(vJson, "amount");
plugin.getLogger().info("[CDK-Web验证] 解析结果: found=" + found + " status=" + st + " amount=" + am);
if ("true".equals(found) && "available".equals(st)) {
amount = am.isEmpty() ? 0 : Integer.parseInt(am);
// ★ CDK存在且可用,调用兑换API标记已使用+写流水
// status只在cdk_redeem_remote成功后才设为success
try {
String redeemUrl = webBaseUrl + "/api/sync.php?action=cdk_redeem_remote&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&code=" + java.net.URLEncoder.encode(cdkCode, "UTF-8")
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8");
URL rUrl = new URL(redeemUrl);
HttpURLConnection rConn = (HttpURLConnection) rUrl.openConnection();
rConn.setRequestMethod("GET");
rConn.setConnectTimeout(5000);
rConn.setReadTimeout(5000);
int rCode = rConn.getResponseCode();
if (rCode == 200) {
BufferedReader rReader = new BufferedReader(new InputStreamReader(rConn.getInputStream(), StandardCharsets.UTF_8));
StringBuilder rSb = new StringBuilder();
String rLine;
while ((rLine = rReader.readLine()) != null) rSb.append(rLine);
rReader.close();
rConn.disconnect();
String rJson = rSb.toString();
plugin.getLogger().info("[CDK-Web验证] 兑换结果: " + rJson);
String rSt = extractJsonStr(rJson, "status");
if ("success".equals(rSt)) {
status = "success";
} else if ("already_used".equals(rSt)) {
status = "already_used";
}
} else {
plugin.getLogger().warning("[CDK-Web验证] cdk_redeem_remote HTTP: " + rCode);
}
} catch (Exception e) {
plugin.getLogger().warning("[CDK-Web验证] 兑换请求失败: " + e.getMessage());
}
} else if ("true".equals(found) && "already_used".equals(st)) {
status = "already_used";
} else {
plugin.getLogger().info("[CDK-Web验证] CDK " + cdkCode + " 在Web端不存在或状态未知");
}
} else {
plugin.getLogger().warning("[CDK-Web验证] HTTP状态码: " + vCode + " CDK=" + cdkCode);
vConn.disconnect();
}
} catch (Exception e) {
plugin.getLogger().warning("[CDK-Web验证] 请求失败: " + e.getMessage());
}
// 回传结果给sdf1
java.lang.reflect.Method setResult = sdf1Plugin.getClass().getMethod("setCdkValidationResult", String.class, String.class, int.class);
setResult.invoke(sdf1Plugin, requestId, status, amount);
if (!"not_found".equals(status)) {
plugin.getLogger().info("[CDK-Web验证] " + cdkCode + " → " + status + " player=" + playerName + (amount > 0 ? " 金额:" + amount : ""));
}
}
} catch (Exception e) {
// 静默
}
}
// ==================== Token同步到Web ==================== // ==================== Token同步到Web ====================
/** /**
@@ -892,7 +1242,11 @@ public class WebManager {
urlStr.append("token=").append(java.net.URLEncoder.encode(token, "UTF-8")); urlStr.append("token=").append(java.net.URLEncoder.encode(token, "UTF-8"));
} }
String json = mapToJson(data); // ★ 在JSON body中也带上secretKey,作为token失效时的回退认证
Map<String, Object> bodyWithSecret = new LinkedHashMap<>(data);
bodyWithSecret.put("secret", secretKey);
String json = mapToJson(bodyWithSecret);
URL url = new URL(urlStr.toString()); URL url = new URL(urlStr.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
@@ -1028,6 +1382,20 @@ public class WebManager {
} }
} }
/**
* 简单JSON字符串值提取(不依赖第三方库)
*/
private static String extractJsonStr(String json, String key) {
int i = json.indexOf("\"" + key + "\"");
if (i < 0) return "";
int colon = json.indexOf(":", i);
int start = json.indexOf("\"", colon + 1);
if (start < 0) return "";
int end = json.indexOf("\"", start + 1);
if (end < 0) return "";
return json.substring(start + 1, end);
}
/** /**
* 简单JSON解析(提取字段值) * 简单JSON解析(提取字段值)
*/ */
@@ -1377,6 +1745,106 @@ public class WebManager {
} }
} }
// ==================== 游戏内交易记录同步到Web ====================
/**
* 推送游戏内交易记录(bond_transaction)到PHP端
* 使用 last_synced_tx_time 追踪已同步的最晚时间,增量推送
* 注意:shop_buy合并逻辑会UPDATE已有记录的时间,所以用时间追踪能确保合并后的记录也被同步
*/
private volatile long lastSyncedTxTime = 0;
public void syncBondTransactions() {
if (!enabled) return;
try {
BondManager bondMgr = plugin.getBonds();
if (bondMgr == null) return;
// 读取上次同步的最晚时间
loadLastSyncedTxTime();
// 获取增量交易记录
List<Map<String, Object>> txs = bondMgr.getTransactionsAfterTime(lastSyncedTxTime);
if (txs.isEmpty()) return;
// 生成同步token
String token = generateAndSyncToken("system", "sync");
Map<String, Object> body = new LinkedHashMap<>();
body.put("transactions", txs);
String response = httpPostWithToken("api/sync.php?action=sync_transactions", token, body);
plugin.getLogger().info("[Web交易同步] PHP响应: " + (response != null ? response.substring(0, Math.min(500, response.length())) : "null"));
if (response != null) {
Map<String, Object> result = parseJson(response);
Boolean success = (Boolean) result.get("success");
plugin.getLogger().info("[Web交易同步] parseJson结果: success=" + result.get("success") + ", data类型=" + (result.get("data") != null ? result.get("data").getClass().getSimpleName() : "null") + ", message=" + result.get("message"));
if (Boolean.TRUE.equals(success)) {
// 更新已同步的最晚时间
long maxTime = lastSyncedTxTime;
for (Map<String, Object> tx : txs) {
long t = ((Number) tx.get("time")).longValue();
if (t > maxTime) maxTime = t;
}
if (maxTime > lastSyncedTxTime) {
lastSyncedTxTime = maxTime;
saveLastSyncedTxTime(maxTime);
}
// data可能是Map(解析正确)也可能是String(parseJson解析嵌套JSON失败),安全处理
int synced = txs.size();
Object dataObj = result.get("data");
if (dataObj instanceof Map) {
Object syncedVal = ((Map<?, ?>) dataObj).get("synced");
if (syncedVal instanceof Number) {
synced = ((Number) syncedVal).intValue();
}
}
plugin.getLogger().info("[Web交易同步] 同步了 " + synced + " 笔游戏交易到PHP端 (发送了" + txs.size() + "笔)");
} else {
plugin.getLogger().warning("[Web交易同步] 游戏交易同步失败: " + result.get("message"));
}
} else {
plugin.getLogger().warning("[Web交易同步] PHP响应为空");
}
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 游戏交易同步异常: " + e.getMessage());
}
}
private void loadLastSyncedTxTime() {
if (lastSyncedTxTime > 0) return;
try {
File dbFile = new File(plugin.getDataFolder(), "web_sync.db");
if (!dbFile.exists()) return;
java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
java.sql.Statement st = conn.createStatement();
st.execute("CREATE TABLE IF NOT EXISTS sync_state (key TEXT PRIMARY KEY, value INTEGER DEFAULT 0)");
java.sql.ResultSet rs = st.executeQuery("SELECT value FROM sync_state WHERE key='last_synced_tx_time'");
if (rs.next()) {
lastSyncedTxTime = rs.getLong("value");
}
rs.close(); st.close(); conn.close();
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 读取last_synced_tx_time失败: " + e.getMessage());
}
}
private void saveLastSyncedTxTime(long time) {
try {
File dbFile = new File(plugin.getDataFolder(), "web_sync.db");
java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
java.sql.Statement st = conn.createStatement();
st.execute("CREATE TABLE IF NOT EXISTS sync_state (key TEXT PRIMARY KEY, value INTEGER DEFAULT 0)");
java.sql.PreparedStatement ps = conn.prepareStatement(
"INSERT OR REPLACE INTO sync_state (key, value) VALUES ('last_synced_tx_time', ?)");
ps.setLong(1, time);
ps.executeUpdate();
ps.close(); st.close(); conn.close();
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 保存last_synced_tx_time失败: " + e.getMessage());
}
}
/** /**
* 6. 推送注册数据到Web端 * 6. 推送注册数据到Web端
*/ */
@@ -1477,27 +1945,88 @@ public class WebManager {
/** /**
* 同步在线玩家列表到PHP端(用于Web登录状态检查) * 同步在线玩家列表到PHP端(用于Web登录状态检查)
* 注意:推送所有在线玩家(包括未登录的),PHP端通过 web_login_verified 判断是否已认证
*/ */
public void syncOnlinePlayers() { public void syncOnlinePlayers() {
if (!enabled) return; if (!enabled) {
plugin.getLogger().warning("[Web通信] syncOnlinePlayers: enabled=false, 跳过");
return;
}
try { try {
java.util.Collection<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers(); java.util.Collection<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers();
java.util.Set<String> loggedInPlayers = plugin.getLoggedIn(); // ★ 只推送已登录的玩家
List<Map<String, Object>> playersData = new ArrayList<>(); List<Map<String, Object>> playersData = new ArrayList<>();
for (Player p : onlinePlayers) { for (Player p : onlinePlayers) {
// ★ 关键:只有真正输入密码登录的玩家才推送到PHP
if (!loggedInPlayers.contains(p.getName())) continue;
Map<String, Object> playerInfo = new LinkedHashMap<>(); Map<String, Object> playerInfo = new LinkedHashMap<>();
playerInfo.put("name", p.getName()); playerInfo.put("name", p.getName());
playerInfo.put("login_time", System.currentTimeMillis() / 1000); playerInfo.put("login_time", System.currentTimeMillis() / 1000);
// ★ 推送当前IP(从Bukkit Player对象实时获取,不走login.db缓存)
try {
java.net.InetSocketAddress addr = p.getAddress();
if (addr != null && addr.getAddress() != null) {
playerInfo.put("ip", addr.getAddress().getHostAddress());
}
} catch (Exception ignored) {}
playersData.add(playerInfo); playersData.add(playerInfo);
} }
// ★ 使用表单格式发送(PHP会自动解析application/x-www-form-urlencoded到$_POST)
String playersJson = buildPlayersJsonArray(playersData); String playersJson = buildPlayersJsonArray(playersData);
String formBody = "secret=" + escapeUrl(secretKey) + "&players=" + escapeUrl(playersJson); plugin.getLogger().info("[Web通信] ★ syncOnlinePlayers: 在线=" + onlinePlayers.size() + " 已登录=" + loggedInPlayers.size() + " 推送=" + playersData.size() + " 玩家=" + loggedInPlayers);
// ★ Debug: 打印完整body // ★ 策略:优先GET(与push_player_login_status一致),确保数据到达PHP
plugin.getLogger().info("[Web通信] 在线玩家同步 body: " + formBody.substring(0, Math.min(500, formBody.length()))); // GET请求更可靠,不会被Web服务器/WAF拦截POST body
String getUrl = webBaseUrl + "/api/sync.php?action=sync_online_players"
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&players=" + java.net.URLEncoder.encode(playersJson, "UTF-8");
URL url = new URL(getUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
conn.setConnectTimeout(8000);
conn.setReadTimeout(10000);
int code = conn.getResponseCode();
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
String response = "";
if (is != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) sb.append(line);
reader.close();
response = sb.toString();
}
conn.disconnect();
if (code == 200 && response.contains("\"success\":true")) {
plugin.getLogger().info("[Web通信] ★ 在线玩家同步成功(GET): " + playersData.size() + "人");
} else {
plugin.getLogger().warning("[Web通信] ★ 在线玩家同步失败(GET): HTTP " + code + " - " + response.substring(0, Math.min(300, response.length())));
// GET失败时回退到POST
tryPostSync(playersJson, playersData.size());
}
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] syncOnlinePlayers异常: " + e.getClass().getSimpleName() + ": " + e.getMessage());
// 异常时尝试POST
try {
String playersJson = buildPlayersJsonArray(new ArrayList<>());
tryPostSync(playersJson, 0);
} catch (Exception ignored) {}
}
}
/**
* POST方式同步在线玩家(备用方案)
*/
private void tryPostSync(String playersJson, int count) {
try {
String formBody = "secret=" + escapeUrl(secretKey) + "&players=" + escapeUrl(playersJson);
plugin.getLogger().info("[Web通信] 尝试POST同步: " + count + "人");
URL url = new URL(webBaseUrl + "/api/sync.php?action=sync_online_players"); URL url = new URL(webBaseUrl + "/api/sync.php?action=sync_online_players");
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
@@ -1515,27 +2044,24 @@ public class WebManager {
int code = conn.getResponseCode(); int code = conn.getResponseCode();
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream(); InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
String response = "";
if (is != null) { if (is != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
String line; String line;
while ((line = reader.readLine()) != null) sb.append(line); while ((line = reader.readLine()) != null) sb.append(line);
reader.close(); reader.close();
String response = sb.toString(); response = sb.toString();
// ★ 修复:HTTP 200 不一定是成功,需要检查响应内容是否为有效 JSON
if (code == 200 && response.contains("\"success\":true")) {
plugin.getLogger().info("[Web通信] 在线玩家同步成功: " + playersData.size() + "人");
} else if (response.contains("Fatal error") || response.contains("Warning") || response.contains("<br")) {
// PHP 返回了错误信息,虽然 HTTP 200
plugin.getLogger().warning("[Web通信] PHP返回错误: HTTP " + code + " - " + response.substring(0, Math.min(200, response.length())));
} else {
plugin.getLogger().warning("[Web通信] 在线玩家同步异常: HTTP " + code + " - " + response.substring(0, Math.min(200, response.length())));
}
} }
conn.disconnect(); conn.disconnect();
if (code == 200 && response.contains("\"success\":true")) {
plugin.getLogger().info("[Web通信] POST同步成功: " + count + "人");
} else {
plugin.getLogger().warning("[Web通信] POST同步也失败: HTTP " + code + " - " + response.substring(0, Math.min(300, response.length())));
}
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[Web通信] 在线玩家同步异常: " + e.getMessage()); plugin.getLogger().warning("[Web通信] POST同步异常: " + e.getMessage());
} }
} }
@@ -2111,6 +2637,7 @@ public class WebManager {
public void run() { public void run() {
syncShopData(); syncShopData();
syncBondBalances(); syncBondBalances();
syncBondTransactions();
syncUserRegistrations(); syncUserRegistrations();
} }
}.runTaskAsynchronously(plugin); }.runTaskAsynchronously(plugin);
@@ -2740,6 +3267,7 @@ public class WebManager {
boolean ok = plugin.getBondManager().deductBonds(playerName, amount, boolean ok = plugin.getBondManager().deductBonds(playerName, amount,
"web_shop", itemId, "Web商城", "Web购买商品 " + itemId + " x" + itemCount); "web_shop", itemId, "Web商城", "Web购买商品 " + itemId + " x" + itemCount);
if (ok) { if (ok) {
int newBal = plugin.getBondManager().getBonds(playerName);
plugin.getLogger().info("[Web交易] 扣除成功: 玩家 " + playerName + " 购买商品 " + itemId + " x" + itemCount + ",金额: " + amount); plugin.getLogger().info("[Web交易] 扣除成功: 玩家 " + playerName + " 购买商品 " + itemId + " x" + itemCount + ",金额: " + amount);
log.info("[草原探险]MC草原探险服务器欢迎您,服务器ip:mc2.ypshidifu.cn,端口:30679"); log.info("[草原探险]MC草原探险服务器欢迎您,服务器ip:mc2.ypshidifu.cn,端口:30679");
@@ -2747,6 +3275,9 @@ public class WebManager {
Player player = plugin.getServer().getPlayer(playerName); Player player = plugin.getServer().getPlayer(playerName);
if (player != null && player.isOnline()) { if (player != null && player.isOnline()) {
plugin.getLogger().info("[Web交易] 玩家在线,立即发货"); plugin.getLogger().info("[Web交易] 玩家在线,立即发货");
// ★ 通知玩家购买成功
player.sendMessage("§6[商城] §fWeb购买 §e" + itemId + " x" + itemCount + " §f成功!§c-" + amount + "§f 债券");
player.sendMessage("§6[债券] §f余额: §e" + (newBal + amount) + " §7→ §a" + newBal);
dispatchItemByMaterialOrId(player, itemId, itemCount); dispatchItemByMaterialOrId(player, itemId, itemCount);
} else { } else {
plugin.getLogger().info("[Web交易] 玩家离线,保存到离线邮件"); plugin.getLogger().info("[Web交易] 玩家离线,保存到离线邮件");
@@ -2758,13 +3289,75 @@ public class WebManager {
confirmTransaction(txId); confirmTransaction(txId);
} else if (type.equals("admin_recharge") || type.equals("bond_recharge") || type.equals("recharge") || type.equals("admin_give")) { } else if (type.equals("admin_recharge") || type.equals("bond_recharge") || type.equals("recharge") || type.equals("admin_give")) {
// 管理员充值:增加债券(充值不受冻结限制) // 管理员充值:增加债券(充值不受冻结限制)
int balBefore = plugin.getBondManager().getBonds(playerName);
plugin.getBondManager().addBonds(playerName, amount, "web_recharge", "", "Web后台", "管理员充值"); plugin.getBondManager().addBonds(playerName, amount, "web_recharge", "", "Web后台", "管理员充值");
int balAfter = plugin.getBondManager().getBonds(playerName);
plugin.getLogger().info("[Web交易] 玩家 " + playerName + " 管理员充值,金额: " + amount); plugin.getLogger().info("[Web交易] 玩家 " + playerName + " 管理员充值,金额: " + amount);
// ★ 通知在线玩家
Player rechargePlayer = plugin.getServer().getPlayer(playerName);
if (rechargePlayer != null && rechargePlayer.isOnline()) {
rechargePlayer.sendMessage("§6[债券] §a管理员充值!§f +§a" + amount + "§f 债券");
rechargePlayer.sendMessage("§6[债券] §f余额: §e" + balBefore + " §7→ §a" + balAfter);
}
txSuccess = true;
confirmTransaction(txId);
} else if (type.equals("admin_deduct") || type.equals("admin_add")) {
// 管理员扣减/增加债券(Web后台操作)
int currentBalance = plugin.getBondManager().getBonds(playerName);
// ★ 从PHP的detail字段读取PHP计算的新余额(PHP和Java本地余额可能不同步)
int newBalance = -1;
try {
// detail格式: {"admin_action":"deduct","original":500,"new":400}
int newIdx = detail.indexOf("\"new\":");
if (newIdx >= 0) {
newIdx += 6; // skip "new":
int newEnd = detail.indexOf("}", newIdx);
if (newEnd < 0) newEnd = detail.indexOf(",", newIdx);
if (newEnd > newIdx) {
newBalance = Integer.parseInt(detail.substring(newIdx, newEnd).trim());
}
}
} catch (Exception e) {
plugin.getLogger().warning("[Web交易] 解析detail失败: " + detail);
}
if (newBalance >= 0) {
// PHP已计算好新余额,直接设置
plugin.getBondManager().setBonds(playerName, newBalance);
plugin.getLogger().info("[Web交易] 玩家 " + playerName + " " + (type.equals("admin_deduct") ? "管理员扣减" : "管理员增加") + ",PHP新余额: " + newBalance);
} else {
// fallback:用Java本地余额计算
newBalance = type.equals("admin_deduct") ? currentBalance - Math.abs(amount) : currentBalance + amount;
if (newBalance < 0) newBalance = 0;
plugin.getBondManager().setBonds(playerName, newBalance);
plugin.getLogger().info("[Web交易] 玩家 " + playerName + " " + (type.equals("admin_deduct") ? "管理员扣减" : "管理员增加") + ",Java计算新余额: " + newBalance);
}
// ★ 通知在线玩家动账结果
Player targetPlayer = plugin.getServer().getPlayer(playerName);
if (targetPlayer != null && targetPlayer.isOnline()) {
String actionText = type.equals("admin_deduct") ? "§c扣减" : "§a增加";
String changeText = type.equals("admin_deduct") ? "-" + Math.abs(amount) : "+" + amount;
targetPlayer.sendMessage("§6[债券] §f" + actionText + " §7" + Math.abs(amount) + "§f 债券(Web管理操作)");
targetPlayer.sendMessage("§6[债券] §f余额: §e" + currentBalance + " §7→ §a" + newBalance);
}
txSuccess = true;
confirmTransaction(txId); confirmTransaction(txId);
} else if (type.equals("cdk_redeem")) { } else if (type.equals("cdk_redeem")) {
// CDK兑换:增加债券(不受冻结限制) // CDK兑换:增加债券(不受冻结限制)
int balanceBefore = plugin.getBondManager().getBonds(playerName);
plugin.getBondManager().addBonds(playerName, amount, "web_cdk", "", "Web商城", "CDK兑换"); plugin.getBondManager().addBonds(playerName, amount, "web_cdk", "", "Web商城", "CDK兑换");
int balanceAfter = plugin.getBondManager().getBonds(playerName);
plugin.getLogger().info("[Web交易] 玩家 " + playerName + " CDK兑换,金额: " + amount); plugin.getLogger().info("[Web交易] 玩家 " + playerName + " CDK兑换,金额: " + amount);
// ★ 通知在线玩家
Player cdkPlayer = plugin.getServer().getPlayer(playerName);
if (cdkPlayer != null && cdkPlayer.isOnline()) {
cdkPlayer.sendMessage("§6[债券] §aCDK兑换成功!§f +§a" + amount + "§f 债券");
cdkPlayer.sendMessage("§6[债券] §f余额: §e" + balanceBefore + " §7→ §a" + balanceAfter);
}
txSuccess = true; txSuccess = true;
confirmTransaction(txId); confirmTransaction(txId);
} else { } else {
@@ -3140,6 +3733,7 @@ public class WebManager {
if (!Bukkit.getOnlinePlayers().isEmpty()) { if (!Bukkit.getOnlinePlayers().isEmpty()) {
syncShopData(); syncShopData();
syncBondBalances(); syncBondBalances();
syncBondTransactions();
pushWebLoginCredentials(); pushWebLoginCredentials();
pullPendingTransactions(); pullPendingTransactions();
pullShopStock(); pullShopStock();
@@ -3327,8 +3921,8 @@ public class WebManager {
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET"); conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0"); conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
conn.setConnectTimeout(3000); // 缩短超时到3秒 conn.setConnectTimeout(8000);
conn.setReadTimeout(3000); conn.setReadTimeout(10000);
int code = conn.getResponseCode(); int code = conn.getResponseCode();
if (code != 200) { if (code != 200) {
+1 -1
View File
@@ -1,5 +1,5 @@
name: Sdf1_login name: Sdf1_login
version: 2.2 version: 2.5
main: Sdf1_login.Main main: Sdf1_login.Main
api-version: '1.21' api-version: '1.21'
commands: commands: