第53批:网页绑邮箱游戏内批准流——定时器A链尾轮询+clickable批准通知+/webbind命令
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m26s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-23 23:01:42 +08:00
parent 474b912afe
commit 5e9bd5f6bf
3 changed files with 168 additions and 0 deletions
+45
View File
@@ -9610,6 +9610,51 @@ public class Main extends JavaPlugin
return true;
}
// ★ 第53批:网页端绑邮箱 · 游戏内批准/拒绝(/webbind approve|reject <id>)
if (cmdName.equals("webbind")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可用");
return true;
}
final Player wp = (Player) sender;
if (isFrozen(wp)) {
wp.sendMessage("§c§l[登录] §f你还未登录,无法使用该命令。请先使用 §e/l <密码> §f登录");
return true;
}
if (args.length < 2
|| (!args[0].equalsIgnoreCase("approve")
&& !args[0].equalsIgnoreCase("reject"))) {
wp.sendMessage("§c用法: /webbind approve|reject <id>");
return true;
}
final String bindReqId = args[1];
if (!bindReqId.matches("[0-9]+")) {
wp.sendMessage("§c无效的请求ID");
return true;
}
if (webManager == null) {
wp.sendMessage("§cWeb通信未启用,无法处理");
return true;
}
final boolean bindApprove = args[0].equalsIgnoreCase("approve");
wp.sendMessage(bindApprove ? "§e正在提交批准…" : "§e正在提交拒绝…");
webManager.submitWebEmailBindResult(bindReqId, wp.getName(),
bindApprove ? "approve" : "reject", "", status -> {
if (status == null) {
wp.sendMessage("§c提交失败,请稍后重试");
} else if ("approved".equals(status)) {
wp.sendMessage("§a已批准!§f请回到网页输入登录密码完成绑定(3分钟内有效)");
} else if ("rejected".equals(status)) {
wp.sendMessage("§e已拒绝,网页端将直接返回,不做任何处置");
} else if ("expired".equals(status)) {
wp.sendMessage("§c请求已超时,未做任何处置");
} else {
wp.sendMessage("§7请求当前状态: §f" + status + " §7(重复操作已忽略)");
}
});
return true;
}
// 绑定邮箱
if (cmdName.equals("绑定邮箱")) {
// ★ 3.0 安全修复:邮箱绑定必须登录后才能执行(未登录越权绑邮箱 + 任意收件人 SMTP 滥发)
+120
View File
@@ -2638,6 +2638,11 @@ public class WebManager {
lastPollWebLoginExceptionLog = t;
}
}
// ★ 第53批:网页绑邮箱请求轮询(游戏内批准流,60秒时效)
try { pollWebEmailBindRequests(); }
catch (Exception e) {
plugin.getLogger().warning("[合并A-网页绑邮箱] 异常: " + e.getClass().getSimpleName() + " - " + e.getMessage());
}
});
}
@@ -11822,6 +11827,121 @@ public class WebManager {
}.runTaskAsynchronously(plugin);
}
// ==================== 第53批:网页端绑邮箱 · 游戏内批准 ====================
/**
* 轮询网页端发起的绑邮箱请求(60秒时效)。
* 在线 → 主线程发 clickable 批准/拒绝通知;离线 → 回调 offline;
* 后缀白名单不通过 → 回调 failed(超时由PHP侧惰性终结兜底)。
*/
private void pollWebEmailBindRequests() {
try {
String urlStr = webBaseUrl + "/api/sync.php?action=check_pending_web_email_bind_requests&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
String json = doGet(urlStr);
if (json == null || json.contains("database is locked")) return;
if (!json.contains("\"success\":true")) return;
int countStart = json.indexOf("\"count\":");
if (countStart < 0) return;
int countValStart = countStart + 8;
int countValEnd = countValStart;
while (countValEnd < json.length() && Character.isDigit(json.charAt(countValEnd))) countValEnd++;
if (countValEnd == countValStart) return;
int count = Integer.parseInt(json.substring(countValStart, countValEnd));
if (count == 0) return;
int requestsStart = json.indexOf("\"requests\":[");
if (requestsStart < 0) return;
int arrStart = requestsStart + 12;
int arrEnd = findMatchingBracket(json, arrStart - 1);
if (arrEnd < 0) return;
String arrStr = json.substring(arrStart, arrEnd);
int idx = 0;
while (idx < arrStr.length()) {
int objStart = arrStr.indexOf('{', idx);
if (objStart == -1) break;
int objEnd = arrStr.indexOf('}', objStart);
if (objEnd == -1) break;
String item = arrStr.substring(objStart, objEnd + 1);
idx = objEnd + 1;
final String reqId = extractJsonNumber(item, "id");
final String playerName = extractJsonString(item, "player_name");
final String emailAddr = extractJsonString(item, "email");
if (reqId == null || playerName == null || emailAddr == null) continue;
// ★ 邮箱后缀黑白名单:游戏配置为唯一权威(PHP侧无此配置)
if (config != null && !config.isEmailSuffixAllowed(emailAddr)) {
submitWebEmailBindResult(reqId, playerName, "failed",
"邮箱后缀不在允许范围内", null);
plugin.getLogger().info("[网页绑邮箱] 后缀校验拒绝: " + playerName + " -> " + emailAddr);
continue;
}
// ★ Bukkit API 必须主线程:查在线 + 发 clickable 通知
Bukkit.getScheduler().runTask(plugin, () -> {
Player target = Bukkit.getPlayerExact(playerName);
if (target == null || !target.isOnline()) {
submitWebEmailBindResult(reqId, playerName, "offline", "", null);
return;
}
target.sendMessage(net.kyori.adventure.text.Component.text("\n§6§l═══════ 网页端邮箱绑定请求 ═══════\n"));
target.sendMessage(net.kyori.adventure.text.Component.text(
"§f网页端请求为 §b" + playerName + " §f绑定邮箱 §b" + emailAddr));
target.sendMessage(net.kyori.adventure.text.Component.empty()
.append(net.kyori.adventure.text.Component.text("§a✔ [§f批准§a] §7点击后到网页输入登录密码完成绑定")
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
net.kyori.adventure.text.Component.text("§e点击批准(1分钟内有效)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand("/webbind approve " + reqId))));
target.sendMessage(net.kyori.adventure.text.Component.empty()
.append(net.kyori.adventure.text.Component.text("§c✘ [§f拒绝§c] §7拒绝后网页端直接返回,不做处置")
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
net.kyori.adventure.text.Component.text("§e点击拒绝")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand("/webbind reject " + reqId))));
target.sendMessage(net.kyori.adventure.text.Component.text("§7时效 §e1分钟 §7,超时自动作废。"));
});
}
} catch (Exception e) {
plugin.getLogger().warning("[网页绑邮箱轮询] 异常: " + e.getClass().getSimpleName() + " - " + e.getMessage());
}
}
/**
* 将游戏内批准/拒绝结果回传PHP(callback 可为 null,回调回主线程执行)。
*/
public void submitWebEmailBindResult(final String reqId, final String playerName,
final String action, final String failReason,
final java.util.function.Consumer<String> callback) {
new BukkitRunnable() {
@Override
public void run() {
String status = null;
try {
String bodyJson = "{\"request_id\":" + reqId
+ ",\"player\":\"" + escapeJson(playerName) + "\""
+ ",\"action\":\"" + escapeJson(action) + "\""
+ ",\"fail_reason\":\"" + escapeJson(failReason == null ? "" : failReason) + "\"}";
String postUrl = webBaseUrl + "/api/sync.php?action=web_email_bind_result&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
String resp = doPost(postUrl, bodyJson);
if (resp != null && resp.contains("\"success\":true")) {
status = extractJsonString(resp, "status");
}
} catch (Exception e) {
status = null;
}
final String fStatus = status;
Bukkit.getScheduler().runTask(plugin, () -> {
try {
if (callback != null) callback.accept(fStatus);
} catch (Exception ignored) {}
});
}
}.runTaskAsynchronously(plugin);
}
// ==================== 邮件验证码功能 ====================
/**
+3
View File
@@ -72,6 +72,9 @@ commands:
绑定邮箱:
description: 绑定邮箱
usage: /绑定邮箱
webbind:
description: 网页邮箱绑定批准/拒绝
usage: /webbind <approve|reject> <id>
chat:
description: 聊天过滤管理
usage: /chat