From 203c9d423afd141b0c6ce2b2907f0b3b242d73e2 Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Sat, 20 Jun 2026 14:02:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EWeb=20CDK=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E5=AF=B9=E6=8E=A5Web=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E5=85=91=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Sdf1_login/LoginManager.java | 10 +- src/main/java/Sdf1_login/Main.java | 10 +- src/main/java/Sdf1_login/UpdateChecker.java | 56 +++- src/main/java/Sdf1_login/WebManager.java | 271 +++++++++++++++++++- src/main/resources/plugin.yml | 2 +- 5 files changed, 328 insertions(+), 21 deletions(-) diff --git a/src/main/java/Sdf1_login/LoginManager.java b/src/main/java/Sdf1_login/LoginManager.java index 92353d6..8477707 100644 --- a/src/main/java/Sdf1_login/LoginManager.java +++ b/src/main/java/Sdf1_login/LoginManager.java @@ -172,9 +172,15 @@ public class LoginManager { plugin.getDb().setField(name, "last_online_check", System.currentTimeMillis()); - // ★ 立即同步在线状态到PHP + // ★ 立即同步在线状态到PHP(异步,避免阻塞主线程) if (plugin.webManager != null) { - plugin.webManager.syncOnlinePlayers(); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try { + plugin.webManager.syncOnlinePlayers(); + } catch (Exception e) { + plugin.getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage()); + } + }); } // 登录成功关闭飞行 p.setAllowFlight(false); diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 027a0f9..6895ac7 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -1167,9 +1167,15 @@ public class Main extends JavaPlugin joinTime.remove(p.getUniqueId()); recordIPLogin(p); - // ★ 立即同步在线状态到PHP(不等下次心跳30秒) + // ★ 立即同步在线状态到PHP(异步,避免阻塞主线程) if (webManager != null) { - webManager.syncOnlinePlayers(); + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + try { + webManager.syncOnlinePlayers(); + } catch (Exception e) { + getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage()); + } + }); } // 只在有有效备份时恢复 diff --git a/src/main/java/Sdf1_login/UpdateChecker.java b/src/main/java/Sdf1_login/UpdateChecker.java index 3d82d56..661bef3 100644 --- a/src/main/java/Sdf1_login/UpdateChecker.java +++ b/src/main/java/Sdf1_login/UpdateChecker.java @@ -48,12 +48,16 @@ public class UpdateChecker { private static final String GITHUB_REPO = "sdf1_login"; private static final String GITEE_OWNER = "nihaoshidifu"; 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获取最新版本) private static final String GITHUB_API = "https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest"; private static final String GITEE_API = "https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest"; + private static final String GYP_API = + "https://git.ypshidifu.cn/api/v1/repos/" + GYP_OWNER + "/" + GYP_REPO + "/releases/latest"; // 第三路备选:宝塔静态HTML private static final String FALLBACK_HTML = @@ -64,6 +68,8 @@ public class UpdateChecker { "https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases"; private static final String GITEE_LINK = "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; @@ -157,18 +163,31 @@ public class UpdateChecker { plugin.getLogger().info("[Sdf1_login] 启动自动检查更新..."); } - // ★ 双通道并行执行,一路成功掐断另一路 + // ★ 三通道并行执行,一路成功掐断其余 AtomicBoolean cancelled = new AtomicBoolean(false); + AtomicReference gypResult = new AtomicReference<>(); AtomicReference ghResult = new AtomicReference<>(); AtomicReference 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 Thread ghThread = new Thread(() -> { if (!cancelled.get()) { ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK)); if (ghResult.get().isSuccess()) { - cancelled.set(true); // 成功则取消另一路 + cancelled.set(true); } } latch.countDown(); @@ -180,34 +199,38 @@ public class UpdateChecker { if (!cancelled.get()) { geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK)); if (geResult.get().isSuccess()) { - cancelled.set(true); // 成功则取消另一路 + cancelled.set(true); } } latch.countDown(); }); geThread.setDaemon(true); + gypThread.start(); ghThread.start(); geThread.start(); try { - latch.await(); // 等待两路都完成 + latch.await(); } catch (InterruptedException ignored) { } + CheckResult gyp = gypResult.get(); CheckResult gh = ghResult.get(); CheckResult ge = geResult.get(); - // 选择成功的结果 + // 选择成功的结果(优先级:git.ypshidifu.cn > GitHub > Gitee) CheckResult chosen = null; - if (gh != null && gh.isSuccess()) { + if (gyp != null && gyp.isSuccess()) { + chosen = gyp; + } else if (gh != null && gh.isSuccess()) { chosen = gh; } else if (ge != null && ge.isSuccess()) { chosen = ge; } else { // ★ 双通道均失败,尝试第三路备选(宝塔静态HTML) - plugin.getLogger().info("[Sdf1_login] GitHub/Gitee 均失败,尝试第三路备选 BaoTa..."); + plugin.getLogger().info("[Sdf1_login] git.ypshidifu.cn/GitHub/Gitee 均失败,尝试第三路备选 BaoTa..."); CheckResult fbResult = tryFallbackChannel(); if (fbResult != null && fbResult.isSuccess()) { chosen = fbResult; @@ -215,7 +238,11 @@ public class UpdateChecker { } else { failCount++; 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 (sb.length() > 0) sb.append("\n"); sb.append("§7GitHub: §c").append(gh.error); } if (ge != null && ge.error != null) { @@ -310,10 +337,15 @@ public class UpdateChecker { sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本"); } - // 显示另一通道的失败原因 - CheckResult other = (src.equals("GitHub")) ? ge : gh; - if (other != null && !other.isSuccess() && other.error != null) { - sender.sendMessage("§7[Sdf1_login] [更新] " + other.source + " 不可用: " + other.error); + // 显示其他失败通道的错误信息 + if (gyp != null && !gyp.isSuccess() && gyp.error != null && !src.equals("git.ypshidifu.cn")) { + sender.sendMessage("§7[Sdf1_login] [更新] git.ypshidifu.cn 不可用: " + gyp.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); } }); } diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index c06d4ab..ea7bf2c 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -124,6 +124,7 @@ public class WebManager { syncIntervalMinutes = Integer.parseInt(getConfigValue("web通信-同步间隔分钟", "5")); callbackPort = Integer.parseInt(getConfigValue("web通信-回调端口", "9090")); secretKey = getConfigValue("web通信-密钥", secretKey); + plugin.getLogger().info("[Web通信] 后端地址: " + webBaseUrl + " | 启用: " + enabled); } /** @@ -258,6 +259,9 @@ public class WebManager { // ★ 启动交易高频轮询(每5秒检查PHP端是否有待处理交易) startTransactionPolling(); + // ★ 启动CDK验证轮询(每2秒检查PHP端CDK验证请求) + startCdkValidationPolling(); + plugin.getLogger().info("[Web通信] 实时同步调度器已启动(60~90秒随机,无人停止,有人恢复)"); plugin.getLogger().info("[Web通信] PHP回调服务器已启动,端口: " + callbackPort); } @@ -737,8 +741,8 @@ public class WebManager { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0"); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); + conn.setConnectTimeout(8000); + conn.setReadTimeout(10000); int code = conn.getResponseCode(); if (code != 200) { plugin.getLogger().warning("[Web交易轮询] HTTP状态码: " + code); @@ -775,6 +779,244 @@ public class WebManager { }.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 ==================== /** @@ -1140,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解析(提取字段值) */ @@ -1708,6 +1964,13 @@ public class WebManager { Map playerInfo = new LinkedHashMap<>(); playerInfo.put("name", p.getName()); 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); } @@ -3658,8 +3921,8 @@ public class WebManager { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0"); - conn.setConnectTimeout(3000); // 缩短超时到3秒 - conn.setReadTimeout(3000); + conn.setConnectTimeout(8000); + conn.setReadTimeout(10000); int code = conn.getResponseCode(); if (code != 200) { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 24dc427..c372b4c 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: Sdf1_login -version: 2.3 +version: 2.5 main: Sdf1_login.Main api-version: '1.21' commands: