refactor: unified /report command with login/register/track, external auth endpoints

This commit is contained in:
2026-07-13 03:25:39 +08:00
parent de5a46f392
commit 2a5c25f41c
8 changed files with 306 additions and 201 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<UUID, String> sessions = new HashMap<>();
private List<String> 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<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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"

View File

@@ -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: /<command> <玩家名> [原因]
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: /<command> <内容>
aliases: [jianyi,建议]
appeal:
description: 提交申诉
usage: /<command> <原因>
aliases: [shensu,申诉]
permissions:
mcreport.use: