From 2a5c25f41c692b9a9652173f216b15b9000849bd Mon Sep 17 00:00:00 2001 From: canglan Date: Mon, 13 Jul 2026 03:25:39 +0800 Subject: [PATCH] refactor: unified /report command with login/register/track, external auth endpoints --- backend/routes/external.js | 45 ++++- .../java/com/mcreport/plugin/ApiClient.java | 106 +++++----- .../com/mcreport/plugin/AppealCommand.java | 51 ----- .../com/mcreport/plugin/MCReportPlugin.java | 35 +++- .../com/mcreport/plugin/ReportCommand.java | 187 +++++++++++++++--- .../java/com/mcreport/plugin/SuggCommand.java | 50 ----- .../src/main/resources/config.yml | 12 +- .../src/main/resources/plugin.yml | 21 +- 8 files changed, 306 insertions(+), 201 deletions(-) delete mode 100644 mc-report-plugin/src/main/java/com/mcreport/plugin/AppealCommand.java delete mode 100644 mc-report-plugin/src/main/java/com/mcreport/plugin/SuggCommand.java diff --git a/backend/routes/external.js b/backend/routes/external.js index fce988d..1a11c19 100644 --- a/backend/routes/external.js +++ b/backend/routes/external.js @@ -1,6 +1,9 @@ const express = require('express'); -const { query, getRow } = require('../db'); -const { authenticate } = require('../middleware/auth'); +const bcrypt = require('bcryptjs'); +const { v4: uuid } = require('uuid'); +const { query, getRow, getConfig } = require('../db'); +const { generateToken } = require('../middleware/auth'); +const { sendEmail } = require('../mailer'); const router = express.Router(); @@ -17,6 +20,44 @@ function externalAuth(req, res, next) { return res.status(401).json({ error: '未授权' }); } +router.post('/auth/register', async (req, res) => { + try { + const { username, password, email, game_name, game_uid } = req.body; + if (!username || !password || !email || !game_name || !game_uid) return res.status(400).json({ error: '所有字段必填' }); + if (password.length < 6) return res.status(400).json({ error: '密码至少6位' }); + if (await getRow('SELECT id FROM users WHERE username = ?', [username])) return res.status(400).json({ error: '用户名已存在' }); + if (await getRow('SELECT id FROM users WHERE email = ?', [email])) return res.status(400).json({ error: '邮箱已注册' }); + + const hashed = bcrypt.hashSync(password, 10); + const verifyToken = uuid(); + await query('INSERT INTO users(username,password,email,game_name,game_uid,verify_token,verify_expires) VALUES (?,?,?,?,?,?,DATE_ADD(NOW(), INTERVAL 24 HOUR))', [username, hashed, email, game_name, game_uid, verifyToken]); + + const site = await getRow("SELECT v FROM settings WHERE k='site_url'"); + const sent = await sendEmail(email, 'verify_email', { + username, game_name, game_uid, + verify_link: `${site?.v||'http://localhost:3100'}#/verify?token=${verifyToken}`, + }); + + if (!sent) { + await query('UPDATE users SET email_verified=1,active=1,verify_token=NULL WHERE username=?', [username]); + return res.status(201).json({ message: '注册成功!已自动激活,请登录' }); + } + res.status(201).json({ message: '注册成功,请查收验证邮件' }); + } catch (e) { res.status(500).json({ error: '服务器错误' }); } +}); + +router.post('/auth/login', async (req, res) => { + try { + const { username, password } = req.body; + if (!username || !password) return res.status(400).json({ error: '请输入用户名和密码' }); + const user = await getRow('SELECT * FROM users WHERE username = ?', [username]); + if (!user || !bcrypt.compareSync(password, user.password)) return res.status(401).json({ error: '用户名或密码错误' }); + if (!user.active) return res.status(403).json({ error: '账号未激活' }); + const token = generateToken(user); + res.json({ token, user: { id: user.id, username: user.username, game_name: user.game_name, game_uid: user.game_uid, role: user.role } }); + } catch (e) { res.status(500).json({ error: '服务器错误' }); } +}); + router.use(externalAuth); router.post('/tickets', async (req, res) => { diff --git a/mc-report-plugin/src/main/java/com/mcreport/plugin/ApiClient.java b/mc-report-plugin/src/main/java/com/mcreport/plugin/ApiClient.java index da30737..789ec8f 100644 --- a/mc-report-plugin/src/main/java/com/mcreport/plugin/ApiClient.java +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/ApiClient.java @@ -2,14 +2,13 @@ package com.mcreport.plugin; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.UUID; public class ApiClient { @@ -22,66 +21,77 @@ public class ApiClient { this.externalKey = externalKey; } - public boolean submitTicket(String type, String title, String reporterName, UUID reporterUuid, - String targetName, UUID targetUuid, String reason, String description) { + public JsonObject request(String method, String path, JsonObject body, String token) { try { - JsonObject body = new JsonObject(); - body.addProperty("type", type); - body.addProperty("title", title); - body.addProperty("reporter_game_name", reporterName); - body.addProperty("reporter_game_uid", reporterUuid != null ? reporterUuid.toString() : ""); - - if (targetName != null && !targetName.isEmpty()) { - body.addProperty("target_game_name", targetName); - } - if (targetUuid != null) { - body.addProperty("target_game_uid", targetUuid.toString()); - } - if (reason != null && !reason.isEmpty()) { - body.addProperty("reason", reason); - } - if (description != null && !description.isEmpty()) { - body.addProperty("description", description); - } - - URL url = new URI(apiUrl + "/tickets").toURL(); + URL url = new URI(apiUrl + path).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); + conn.setRequestMethod(method); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("x-external-key", externalKey); - conn.setDoOutput(true); + if (token != null) conn.setRequestProperty("Authorization", "Bearer " + token); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); - try (OutputStream os = conn.getOutputStream()) { - byte[] input = gson.toJson(body).getBytes(StandardCharsets.UTF_8); - os.write(input, 0, input.length); + if (body != null) { + conn.setDoOutput(true); + try (OutputStream os = conn.getOutputStream()) { + byte[] input = gson.toJson(body).getBytes(StandardCharsets.UTF_8); + os.write(input); + } } int code = conn.getResponseCode(); + String response = code >= 200 && code < 300 + ? new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8) + : new String(conn.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); conn.disconnect(); - return code == 200 || code == 201; + + JsonObject json = JsonParser.parseString(response).getAsJsonObject(); + json.addProperty("_status", code); + return json; } catch (Exception e) { - return false; + JsonObject err = new JsonObject(); + err.addProperty("error", e.getMessage()); + err.addProperty("_status", 500); + return err; } } - public String resolveUuid(String playerName) { - try { - URL url = new URI("https://api.mojang.com/users/profiles/minecraft/" + playerName).toURL(); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); - if (conn.getResponseCode() != 200) { - conn.disconnect(); - return null; - } - String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - conn.disconnect(); - JsonObject json = gson.fromJson(response, JsonObject.class); - return json != null ? json.get("id").getAsString() : null; - } catch (Exception e) { - return null; - } + public JsonObject login(String username, String password) { + JsonObject body = new JsonObject(); + body.addProperty("username", username); + body.addProperty("password", password); + return request("POST", "/auth/login", body, null); + } + + public JsonObject register(String username, String password, String email, String gameName, String gameUid) { + JsonObject body = new JsonObject(); + body.addProperty("username", username); + body.addProperty("password", password); + body.addProperty("email", email); + body.addProperty("game_name", gameName); + body.addProperty("game_uid", gameUid); + return request("POST", "/auth/register", body, null); + } + + public JsonObject submitTicket(String token, String type, String title, + String reporterName, String reporterUid, + String targetName, String targetUid, + String reason, String description, boolean isAdminComplaint) { + JsonObject body = new JsonObject(); + body.addProperty("type", type); + body.addProperty("title", title); + body.addProperty("reporter_game_name", reporterName); + body.addProperty("reporter_game_uid", reporterUid); + if (targetName != null && !targetName.isEmpty()) body.addProperty("target_game_name", targetName); + if (targetUid != null) body.addProperty("target_game_uid", targetUid); + if (reason != null && !reason.isEmpty()) body.addProperty("reason", reason); + if (description != null && !description.isEmpty()) body.addProperty("description", description); + if (isAdminComplaint) body.addProperty("is_admin_complaint", "1"); + return request("POST", "/tickets", body, token); + } + + public JsonObject trackTicket(String trackingToken) { + return request("GET", "/tickets/tracking?token=" + trackingToken, null, null); } } diff --git a/mc-report-plugin/src/main/java/com/mcreport/plugin/AppealCommand.java b/mc-report-plugin/src/main/java/com/mcreport/plugin/AppealCommand.java deleted file mode 100644 index a738ba3..0000000 --- a/mc-report-plugin/src/main/java/com/mcreport/plugin/AppealCommand.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.mcreport.plugin; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -public class AppealCommand implements CommandExecutor { - - private final MCReportPlugin plugin; - - public AppealCommand(MCReportPlugin plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage("§c仅玩家可使用此命令"); - return true; - } - - if (args.length == 0) { - sender.sendMessage("§e/appeal <原因> §7- 提交申诉"); - return true; - } - - String reason = String.join(" ", args); - - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean ok = plugin.getApiClient().submitTicket( - "appeal", - "游戏内申诉: " + player.getName(), - player.getName(), - player.getUniqueId(), - null, null, - reason, - reason - ); - - if (ok) { - player.sendMessage("§a申诉已提交!请等待处理"); - } else { - player.sendMessage("§c提交失败,请联系管理员"); - } - }); - - return true; - } -} diff --git a/mc-report-plugin/src/main/java/com/mcreport/plugin/MCReportPlugin.java b/mc-report-plugin/src/main/java/com/mcreport/plugin/MCReportPlugin.java index 3cdf752..d68b2a9 100644 --- a/mc-report-plugin/src/main/java/com/mcreport/plugin/MCReportPlugin.java +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/MCReportPlugin.java @@ -1,21 +1,25 @@ package com.mcreport.plugin; +import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; +import java.util.*; + public final class MCReportPlugin extends JavaPlugin { private ApiClient apiClient; + private final Map sessions = new HashMap<>(); + private List adminPerms; @Override public void onEnable() { saveDefaultConfig(); - String apiUrl = getConfig().getString("api_url", "http://localhost:3100/api/external"); - String externalKey = getConfig().getString("external_key", ""); + String apiUrl = getConfig().getString("api_url"); + String externalKey = getConfig().getString("external_key"); apiClient = new ApiClient(apiUrl, externalKey); + adminPerms = getConfig().getStringList("admin_permissions"); getCommand("report").setExecutor(new ReportCommand(this)); - getCommand("sugg").setExecutor(new SuggCommand(this)); - getCommand("appeal").setExecutor(new AppealCommand(this)); getLogger().info("MC举报系统插件已启用"); } @@ -25,7 +29,26 @@ public final class MCReportPlugin extends JavaPlugin { getLogger().info("MC举报系统插件已卸载"); } - public ApiClient getApiClient() { - return apiClient; + public ApiClient getApiClient() { return apiClient; } + public List getAdminPerms() { return adminPerms; } + + public String getToken(Player player) { + return sessions.get(player.getUniqueId()); + } + + public void setToken(Player player, String token) { + sessions.put(player.getUniqueId(), token); + } + + public void removeToken(Player player) { + sessions.remove(player.getUniqueId()); + } + + public boolean isOnlineAdmin(Player target) { + if (target.isOp()) return true; + for (String perm : adminPerms) { + if (target.hasPermission(perm)) return true; + } + return false; } } diff --git a/mc-report-plugin/src/main/java/com/mcreport/plugin/ReportCommand.java b/mc-report-plugin/src/main/java/com/mcreport/plugin/ReportCommand.java index 5abe336..f8fe05f 100644 --- a/mc-report-plugin/src/main/java/com/mcreport/plugin/ReportCommand.java +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/ReportCommand.java @@ -1,11 +1,14 @@ package com.mcreport.plugin; +import com.google.gson.JsonObject; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.util.Arrays; + public class ReportCommand implements CommandExecutor { private final MCReportPlugin plugin; @@ -22,51 +25,173 @@ public class ReportCommand implements CommandExecutor { } if (args.length == 0) { - sender.sendMessage("§6===== MC举报系统 ======"); - sender.sendMessage("§e/report <玩家名> <原因> §7- 举报玩家"); - sender.sendMessage("§e/report <原因> §7- 提交事项举报"); - sender.sendMessage("§e/sugg <内容> §7- 提交建议"); - sender.sendMessage("§e/appeal <原因> §7- 提交申诉"); + showHelp(player); return true; } - Player target = Bukkit.getPlayer(args[0]); - String targetName = null; - String targetUuid = null; - int reasonStart = 0; + String sub = args[0].toLowerCase(); - if (target != null && target.isOnline()) { - targetName = target.getName(); - targetUuid = target.getUniqueId().toString(); - reasonStart = 1; + switch (sub) { + case "login": return handleLogin(player, args); + case "reg": case "register": return handleRegister(player, args); + case "track": return handleTrack(player, args); + case "举报": return handleReport(player, args, "report"); + case "建议": return handleReport(player, args, "suggestion"); + case "事项": return handleReport(player, args, "report"); + default: showHelp(player); return true; } + } - if (args.length <= reasonStart) { - sender.sendMessage("§c请填写举报原因"); + private void showHelp(Player player) { + player.sendMessage("§6===== MC举报系统 ====="); + String token = plugin.getToken(player); + if (token == null) { + player.sendMessage("§e/report reg <密码> <邮箱> §7- 注册账号"); + player.sendMessage("§e/report login <密码> §7- 登录"); + } else { + player.sendMessage("§e/report 举报 [玩家名] <原因> §7- 举报玩家"); + player.sendMessage("§e/report 建议 <内容> §7- 提交建议"); + player.sendMessage("§e/report 事项 <内容> §7- 事项提交"); + player.sendMessage("§e/report track <工单ID> §7- 追踪进度"); + } + } + + private boolean handleLogin(Player player, String[] args) { + if (args.length < 2) { + player.sendMessage("§c用法: /report login <密码>"); return true; } + String password = args[1]; + String username = player.getName(); - String reason = String.join(" ", java.util.Arrays.copyOfRange(args, reasonStart, args.length)); - + player.sendMessage("§7登录中..."); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean ok = plugin.getApiClient().submitTicket( - "report", - "游戏内举报: " + player.getName(), - player.getName(), - player.getUniqueId(), - targetName, - targetUuid != null ? java.util.UUID.fromString(targetUuid) : null, - reason, - null - ); - - if (ok) { - player.sendMessage("§a举报已提交!工作人员将尽快处理"); + JsonObject res = plugin.getApiClient().login(username, password); + int status = res.get("_status").getAsInt(); + if (status == 200) { + String token = res.get("token").getAsString(); + plugin.setToken(player, token); + player.sendMessage("§a登录成功!现在可以使用 /report 提交举报"); } else { - player.sendMessage("§c举报提交失败,请联系管理员"); + String err = res.has("error") ? res.get("error").getAsString() : "未知错误"; + player.sendMessage("§c登录失败: " + err); } }); + return true; + } + private boolean handleRegister(Player player, String[] args) { + if (args.length < 3) { + player.sendMessage("§c用法: /report reg <密码> <邮箱>"); + return true; + } + String password = args[1]; + String email = args[2]; + + if (password.length() < 6) { + player.sendMessage("§c密码至少6位"); + return true; + } + + player.sendMessage("§7注册中..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + JsonObject res = plugin.getApiClient().register( + player.getName(), password, email, + player.getName(), player.getUniqueId().toString() + ); + int status = res.get("_status").getAsInt(); + if (status == 201) { + String msg = res.get("message").getAsString(); + player.sendMessage("§a" + msg); + if (msg.contains("自动激活")) { + player.sendMessage("§e账号已激活,请使用 §a/report login <密码> §e登录"); + } else { + player.sendMessage("§e请查收邮箱验证邮件后使用 §a/report login <密码> §e登录"); + } + } else { + String err = res.has("error") ? res.get("error").getAsString() : "未知错误"; + player.sendMessage("§c注册失败: " + err); + } + }); + return true; + } + + private boolean handleReport(Player player, String[] args, String type) { + String token = plugin.getToken(player); + if (token == null) { + player.sendMessage("§c请先登录: /report login <密码>"); + return true; + } + + int contentStart = 1; + String targetName = null; + String targetUid = null; + boolean isAdminComplaint = false; + + if (type.equals("report") && args.length > 1) { + Player target = Bukkit.getPlayer(args[1]); + if (target != null && target.isOnline()) { + targetName = target.getName(); + targetUid = target.getUniqueId().toString(); + contentStart = 2; + isAdminComplaint = plugin.isOnlineAdmin(target); + } + } + + if (args.length <= contentStart) { + player.sendMessage("§c请填写" + (type.equals("report") ? "举报原因" : type.equals("suggestion") ? "建议内容" : "事项内容")); + return true; + } + + String content = String.join(" ", Arrays.copyOfRange(args, contentStart, args.length)); + String reason = type.equals("report") ? content : null; + String description = type.equals("suggestion") ? content : (type.equals("report") ? null : content); + + String title = targetName != null + ? "游戏内举报: " + targetName + : type.equals("suggestion") + ? "游戏内建议: " + player.getName() + : "游戏内举报: " + player.getName(); + + player.sendMessage("§7提交中..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + JsonObject res = plugin.getApiClient().submitTicket( + token, type, title, + player.getName(), player.getUniqueId().toString(), + targetName, targetUid, reason, description, isAdminComplaint + ); + int status = res.get("_status").getAsInt(); + if (status == 200 || status == 201) { + int id = res.get("id").getAsInt(); + String tracking = res.get("tracking_token").getAsString(); + player.sendMessage("§a提交成功!工单编号: §e#" + id); + player.sendMessage("§7追踪码: " + tracking.substring(0, 8) + "..." ); + player.sendMessage("§7可使用 /report track " + id + " 查询进度"); + } else { + String err = res.has("error") ? res.get("error").getAsString() : "未知错误"; + player.sendMessage("§c提交失败: " + err); + } + }); + return true; + } + + private boolean handleTrack(Player player, String[] args) { + if (args.length < 2) { + player.sendMessage("§c用法: /report track <工单ID>"); + return true; + } + String trackingToken = args[1]; + player.sendMessage("§7查询中..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + JsonObject res = plugin.getApiClient().trackTicket(trackingToken); + // The API returns an array, we wrap in a JSON object + if (res.has("error")) { + player.sendMessage("§c工单不存在或无权查看"); + return; + } + player.sendMessage("§6===== 工单列表 ====="); + player.sendMessage("§e可通过 Web 端查看详情"); + }); return true; } } diff --git a/mc-report-plugin/src/main/java/com/mcreport/plugin/SuggCommand.java b/mc-report-plugin/src/main/java/com/mcreport/plugin/SuggCommand.java deleted file mode 100644 index ffb276c..0000000 --- a/mc-report-plugin/src/main/java/com/mcreport/plugin/SuggCommand.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.mcreport.plugin; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -public class SuggCommand implements CommandExecutor { - - private final MCReportPlugin plugin; - - public SuggCommand(MCReportPlugin plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage("§c仅玩家可使用此命令"); - return true; - } - - if (args.length == 0) { - sender.sendMessage("§e/sugg <内容> §7- 提交建议"); - return true; - } - - String content = String.join(" ", args); - - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - boolean ok = plugin.getApiClient().submitTicket( - "suggestion", - "游戏内建议: " + player.getName(), - player.getName(), - player.getUniqueId(), - null, null, null, - content - ); - - if (ok) { - player.sendMessage("§a建议已提交!感谢反馈"); - } else { - player.sendMessage("§c提交失败,请联系管理员"); - } - }); - - return true; - } -} diff --git a/mc-report-plugin/src/main/resources/config.yml b/mc-report-plugin/src/main/resources/config.yml index d8a6e17..501570b 100644 --- a/mc-report-plugin/src/main/resources/config.yml +++ b/mc-report-plugin/src/main/resources/config.yml @@ -1,5 +1,13 @@ # MC举报系统 - 插件配置 -# API地址 (web服务地址) api_url: "http://localhost:3100/api/external" -# 外部API密钥 (安装时生成) external_key: "" + +# 管理权限检测 - 拥有以下任一权限的在线玩家视为管理员 +admin_permissions: + - "op" + - "mcreport.admin" + - "bukkit.command.ban" + - "minecraft.command.ban" + +# 踢出提示 +appeal_url: "http://localhost:3100/#/home/appeal" diff --git a/mc-report-plugin/src/main/resources/plugin.yml b/mc-report-plugin/src/main/resources/plugin.yml index dbdf963..13fad97 100644 --- a/mc-report-plugin/src/main/resources/plugin.yml +++ b/mc-report-plugin/src/main/resources/plugin.yml @@ -3,21 +3,20 @@ version: 1.0.0 main: com.mcreport.plugin.MCReportPlugin api-version: 1.20 author: Sea-Studio -description: MC举报系统 - 游戏内举报插件 +description: MC举报系统 - 游戏内举报/建议/注册登录一体化插件 commands: report: - description: 提交玩家举报 - usage: / <玩家名> [原因] + description: 游戏内举报/建议/账号管理 + usage: | + §6===== MC举报系统 ===== + §e/report login <密码> §7- 登录已有账号 + §e/report reg <密码> <邮箱> §7- 注册新账号 + §e/report 举报 [目标] <原因> §7- 举报玩家 + §e/report 建议 <内容> §7- 提交建议 + §e/report 事项 <内容> §7- 事项提交 + §e/report track <工单ID> §7- 追踪进度 aliases: [jubao,举报] - sugg: - description: 提交建议 - usage: / <内容> - aliases: [jianyi,建议] - appeal: - description: 提交申诉 - usage: / <原因> - aliases: [shensu,申诉] permissions: mcreport.use: