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