From de5a46f3925803682388b095073568f7dee3a99b Mon Sep 17 00:00:00 2001 From: canglan Date: Mon, 13 Jul 2026 01:28:33 +0800 Subject: [PATCH] feat: Paper plugin + external API POST endpoint --- backend/routes/external.js | 35 ++++++++ mc-report-plugin/pom.xml | 47 ++++++++++ .../java/com/mcreport/plugin/ApiClient.java | 87 +++++++++++++++++++ .../com/mcreport/plugin/AppealCommand.java | 51 +++++++++++ .../com/mcreport/plugin/MCReportPlugin.java | 31 +++++++ .../com/mcreport/plugin/ReportCommand.java | 72 +++++++++++++++ .../java/com/mcreport/plugin/SuggCommand.java | 50 +++++++++++ .../src/main/resources/config.yml | 5 ++ .../src/main/resources/plugin.yml | 25 ++++++ 9 files changed, 403 insertions(+) create mode 100644 mc-report-plugin/pom.xml create mode 100644 mc-report-plugin/src/main/java/com/mcreport/plugin/ApiClient.java create mode 100644 mc-report-plugin/src/main/java/com/mcreport/plugin/AppealCommand.java create mode 100644 mc-report-plugin/src/main/java/com/mcreport/plugin/MCReportPlugin.java create mode 100644 mc-report-plugin/src/main/java/com/mcreport/plugin/ReportCommand.java create mode 100644 mc-report-plugin/src/main/java/com/mcreport/plugin/SuggCommand.java create mode 100644 mc-report-plugin/src/main/resources/config.yml create mode 100644 mc-report-plugin/src/main/resources/plugin.yml diff --git a/backend/routes/external.js b/backend/routes/external.js index 109c19c..fce988d 100644 --- a/backend/routes/external.js +++ b/backend/routes/external.js @@ -19,6 +19,41 @@ function externalAuth(req, res, next) { router.use(externalAuth); +router.post('/tickets', async (req, res) => { + const { type, title, reporter_game_name, reporter_game_uid, + target_game_name, target_game_uid, reason, description } = req.body; + + if (!type || !['report','suggestion','appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' }); + if (!title) return res.status(400).json({ error: '标题不能为空' }); + if (!reporter_game_name || !reporter_game_uid) return res.status(400).json({ error: '请填写游戏名称和UID' }); + + if (type === 'report') { + if (!reason) return res.status(400).json({ error: '请填写举报原因' }); + } + if (type === 'suggestion' && !description) return res.status(400).json({ error: '建议内容不能为空' }); + if (type === 'appeal') { + if (!reason) return res.status(400).json({ error: '请填写申诉理由' }); + if (!description) return res.status(400).json({ error: '请填写详细申诉内容' }); + } + + const countRow = await getRow("SELECT COUNT(*) as c FROM tickets WHERE (reporter_game_name = ? OR reporter_game_uid = ?) AND status IN ('pending','processing','awaiting_info','appealing')", [reporter_game_name, reporter_game_uid]); + if (countRow.c >= 5) return res.status(400).json({ error: '待处理工单已达上限' }); + + const trackingToken = require('uuid').v4(); + const [r] = await query(`INSERT INTO tickets(type,title,reporter_game_name,reporter_game_uid, + target_game_name,target_game_uid,reason,description,tracking_token) VALUES (?,?,?,?,?,?,?,?,?)`, + [type, title, reporter_game_name, reporter_game_uid, + (type==='report')?(target_game_name||''):null, + (type==='report')?(target_game_uid||''):null, + (type==='report'||type==='appeal')?reason:null, + (type==='suggestion'||type==='appeal')?description:'', + trackingToken]); + + await query("INSERT INTO responses(ticket_id, content, is_staff) VALUES (?,?,0)", [r.insertId, `游戏内${type==='report'?'举报':type==='appeal'?'申诉':'建议'}\n提交人: ${reporter_game_name} (UID: ${reporter_game_uid})\n${reason||description||''}`]); + + res.status(201).json({ id: r.insertId, tracking_token: trackingToken }); +}); + router.get('/tickets', async (req, res) => { const { type, status, page, limit } = req.query; let q = `SELECT t.id, t.type, t.title, t.status, t.priority, diff --git a/mc-report-plugin/pom.xml b/mc-report-plugin/pom.xml new file mode 100644 index 0000000..aac02d1 --- /dev/null +++ b/mc-report-plugin/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.mcreport + mc-report-plugin + 1.0.0 + jar + + + 17 + 17 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.20.4-R0.1-SNAPSHOT + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + + 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 new file mode 100644 index 0000000..da30737 --- /dev/null +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/ApiClient.java @@ -0,0 +1,87 @@ +package com.mcreport.plugin; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +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 { + + private final String apiUrl; + private final String externalKey; + private final Gson gson = new Gson(); + + public ApiClient(String apiUrl, String externalKey) { + this.apiUrl = apiUrl; + this.externalKey = externalKey; + } + + public boolean submitTicket(String type, String title, String reporterName, UUID reporterUuid, + String targetName, UUID targetUuid, String reason, String description) { + 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(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("x-external-key", externalKey); + conn.setDoOutput(true); + 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); + } + + int code = conn.getResponseCode(); + conn.disconnect(); + return code == 200 || code == 201; + } catch (Exception e) { + return false; + } + } + + 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; + } + } +} 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 new file mode 100644 index 0000000..a738ba3 --- /dev/null +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/AppealCommand.java @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000..3cdf752 --- /dev/null +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/MCReportPlugin.java @@ -0,0 +1,31 @@ +package com.mcreport.plugin; + +import org.bukkit.plugin.java.JavaPlugin; + +public final class MCReportPlugin extends JavaPlugin { + + private ApiClient apiClient; + + @Override + public void onEnable() { + saveDefaultConfig(); + String apiUrl = getConfig().getString("api_url", "http://localhost:3100/api/external"); + String externalKey = getConfig().getString("external_key", ""); + apiClient = new ApiClient(apiUrl, externalKey); + + getCommand("report").setExecutor(new ReportCommand(this)); + getCommand("sugg").setExecutor(new SuggCommand(this)); + getCommand("appeal").setExecutor(new AppealCommand(this)); + + getLogger().info("MC举报系统插件已启用"); + } + + @Override + public void onDisable() { + getLogger().info("MC举报系统插件已卸载"); + } + + public ApiClient getApiClient() { + return apiClient; + } +} 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 new file mode 100644 index 0000000..5abe336 --- /dev/null +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/ReportCommand.java @@ -0,0 +1,72 @@ +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 ReportCommand implements CommandExecutor { + + private final MCReportPlugin plugin; + + public ReportCommand(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("§6===== MC举报系统 ======"); + sender.sendMessage("§e/report <玩家名> <原因> §7- 举报玩家"); + sender.sendMessage("§e/report <原因> §7- 提交事项举报"); + sender.sendMessage("§e/sugg <内容> §7- 提交建议"); + sender.sendMessage("§e/appeal <原因> §7- 提交申诉"); + return true; + } + + Player target = Bukkit.getPlayer(args[0]); + String targetName = null; + String targetUuid = null; + int reasonStart = 0; + + if (target != null && target.isOnline()) { + targetName = target.getName(); + targetUuid = target.getUniqueId().toString(); + reasonStart = 1; + } + + if (args.length <= reasonStart) { + sender.sendMessage("§c请填写举报原因"); + return true; + } + + String reason = String.join(" ", java.util.Arrays.copyOfRange(args, reasonStart, args.length)); + + 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举报已提交!工作人员将尽快处理"); + } else { + player.sendMessage("§c举报提交失败,请联系管理员"); + } + }); + + 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 new file mode 100644 index 0000000..ffb276c --- /dev/null +++ b/mc-report-plugin/src/main/java/com/mcreport/plugin/SuggCommand.java @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..d8a6e17 --- /dev/null +++ b/mc-report-plugin/src/main/resources/config.yml @@ -0,0 +1,5 @@ +# MC举报系统 - 插件配置 +# API地址 (web服务地址) +api_url: "http://localhost:3100/api/external" +# 外部API密钥 (安装时生成) +external_key: "" diff --git a/mc-report-plugin/src/main/resources/plugin.yml b/mc-report-plugin/src/main/resources/plugin.yml new file mode 100644 index 0000000..dbdf963 --- /dev/null +++ b/mc-report-plugin/src/main/resources/plugin.yml @@ -0,0 +1,25 @@ +name: MCReport +version: 1.0.0 +main: com.mcreport.plugin.MCReportPlugin +api-version: 1.20 +author: Sea-Studio +description: MC举报系统 - 游戏内举报插件 + +commands: + report: + description: 提交玩家举报 + usage: / <玩家名> [原因] + aliases: [jubao,举报] + sugg: + description: 提交建议 + usage: / <内容> + aliases: [jianyi,建议] + appeal: + description: 提交申诉 + usage: / <原因> + aliases: [shensu,申诉] + +permissions: + mcreport.use: + description: 使用举报功能 + default: true