refactor: English subcommands, plugin login/track redesign, clean external API routes

This commit is contained in:
2026-07-13 03:35:13 +08:00
parent 2a5c25f41c
commit a51e841fc9
5 changed files with 163 additions and 166 deletions

View File

@@ -2,13 +2,11 @@ const express = require('express');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const { v4: uuid } = require('uuid'); const { v4: uuid } = require('uuid');
const { query, getRow, getConfig } = require('../db'); const { query, getRow, getConfig } = require('../db');
const { generateToken } = require('../middleware/auth'); const { generateToken, authenticate } = require('../middleware/auth');
const { sendEmail } = require('../mailer'); const { sendEmail } = require('../mailer');
const router = express.Router(); const router = express.Router();
const { getConfig } = require('../db');
function getExternalKey() { function getExternalKey() {
try { return getConfig()?.external_api_key || process.env.EXTERNAL_API_KEY || ''; } catch { return process.env.EXTERNAL_API_KEY || ''; } try { return getConfig()?.external_api_key || process.env.EXTERNAL_API_KEY || ''; } catch { return process.env.EXTERNAL_API_KEY || ''; }
} }
@@ -20,6 +18,7 @@ function externalAuth(req, res, next) {
return res.status(401).json({ error: '未授权' }); return res.status(401).json({ error: '未授权' });
} }
// ---- Auth (no external key needed) ----
router.post('/auth/register', async (req, res) => { router.post('/auth/register', async (req, res) => {
try { try {
const { username, password, email, game_name, game_uid } = req.body; const { username, password, email, game_name, game_uid } = req.body;
@@ -29,14 +28,10 @@ router.post('/auth/register', async (req, res) => {
if (await getRow('SELECT id FROM users WHERE email = ?', [email])) 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 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, 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 site = await getRow("SELECT v FROM settings WHERE k='site_url'");
const sent = await sendEmail(email, 'verify_email', { const sent = await sendEmail(email, 'verify_email', { username, game_name, game_uid, verify_link: `${site?.v||'http://localhost:3100'}#/verify?token=${uuid()}` });
username, game_name, game_uid,
verify_link: `${site?.v||'http://localhost:3100'}#/verify?token=${verifyToken}`,
});
if (!sent) { if (!sent) {
await query('UPDATE users SET email_verified=1,active=1,verify_token=NULL WHERE username=?', [username]); await query('UPDATE users SET email_verified=1,active=1,verify_token=NULL WHERE username=?', [username]);
@@ -58,74 +53,62 @@ router.post('/auth/login', async (req, res) => {
} catch (e) { res.status(500).json({ error: '服务器错误' }); } } catch (e) { res.status(500).json({ error: '服务器错误' }); }
}); });
// ---- Plugin API (JWT auth) ----
router.use(externalAuth); router.use(externalAuth);
router.get('/my-tickets', authenticate, async (req, res) => {
const { limit } = req.query;
const rows = await query(`SELECT id, type, title, status, priority, created_at, updated_at FROM tickets WHERE user_id = ? ORDER BY updated_at DESC LIMIT ?`, [req.user.id, Math.min(parseInt(limit)||10, 50)]);
res.json({ list: rows });
});
router.get('/my-tickets/:id', authenticate, async (req, res) => {
const t = await getRow(`SELECT id, type, title, status, priority, reporter_game_name, reporter_game_uid,
target_game_name, target_game_uid, reason, description, assigned_to, claim_note, created_at, updated_at
FROM tickets WHERE id = ? AND user_id = ?`, [req.params.id, req.user.id]);
if (!t) return res.status(404).json({ error: '工单不存在' });
res.json(t);
});
router.post('/tickets', async (req, res) => { router.post('/tickets', async (req, res) => {
const { type, title, reporter_game_name, reporter_game_uid, const { type, title, reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, reason, description, is_admin_complaint } = req.body;
target_game_name, target_game_uid, reason, description } = req.body;
if (!type || !['report','suggestion','appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' }); if (!type || !['report','suggestion','appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' });
if (!title) return res.status(400).json({ error: '标题不能为空' }); if (!title || !reporter_game_name || !reporter_game_uid) 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]); 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: '待处理工单已达上限' }); if (countRow.c >= 5) return res.status(400).json({ error: '待处理工单已达上限' });
const trackingToken = require('uuid').v4(); const trackingToken = uuid();
const [r] = await query(`INSERT INTO tickets(type,title,reporter_game_name,reporter_game_uid, 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,is_admin_complaint) VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
target_game_name,target_game_uid,reason,description,tracking_token) VALUES (?,?,?,?,?,?,?,?,?)`, [type, title, reporter_game_name, reporter_game_uid, target_game_name||'', target_game_uid||'', reason||'', description||'', trackingToken, is_admin_complaint?1:0]);
[type, title, reporter_game_name, reporter_game_uid, await query("INSERT INTO responses(ticket_id,content,is_staff) VALUES (?,?,0)", [r.insertId, `游戏内提交\n类型: ${type}\n提交人: ${reporter_game_name} (UID: ${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 }); res.status(201).json({ id: r.insertId, tracking_token: trackingToken });
}); });
router.get('/tickets', async (req, res) => { // ---- Java Server API (x-external-key only) ----
router.get('/all-tickets', async (req, res) => {
const { type, status, page, limit } = req.query; const { type, status, page, limit } = req.query;
let q = `SELECT t.id, t.type, t.title, t.status, t.priority, let q = `SELECT id, type, title, status, priority, reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, reason, description, assigned_to, claim_note, created_at, updated_at FROM tickets WHERE 1=1`;
t.reporter_game_name, t.reporter_game_uid,
t.target_game_name, t.target_game_uid,
t.reason, t.description, t.assigned_to, t.claim_note,
t.created_at, t.updated_at
FROM tickets t WHERE 1=1`;
const p = []; const p = [];
if (type) { q += ' AND t.type = ?'; p.push(type); } if (type) { q += ' AND type = ?'; p.push(type); }
if (status) { q += ' AND t.status = ?'; p.push(status); } if (status) { q += ' AND status = ?'; p.push(status); }
const pg = parseInt(page) || 1; const pg = parseInt(page) || 1;
const lm = Math.min(parseInt(limit) || 50, 200); const lm = Math.min(parseInt(limit) || 50, 200);
q += ` ORDER BY t.updated_at DESC LIMIT ${(pg-1)*lm}, ${lm}`; q += ` ORDER BY updated_at DESC LIMIT ${(pg-1)*lm}, ${lm}`;
res.json(await query(q, p)); res.json(await query(q, p));
}); });
router.get('/tickets/:id', async (req, res) => { router.get('/all-tickets/:id', async (req, res) => {
const t = await getRow(`SELECT id, type, title, status, priority, const t = await getRow('SELECT * FROM tickets WHERE id = ?', [req.params.id]);
reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, if (!t) return res.status(404).json({ error: 'Not found' });
reason, description, assigned_to, claim_note,
created_at, updated_at FROM tickets WHERE id = ?`, [req.params.id]);
if (!t) return res.status(404).json({ error: '工单不存在' });
t.responses = await query('SELECT content, is_staff, created_at FROM responses WHERE ticket_id = ? ORDER BY created_at ASC', [req.params.id]); t.responses = await query('SELECT content, is_staff, created_at FROM responses WHERE ticket_id = ? ORDER BY created_at ASC', [req.params.id]);
t.attachments = await query('SELECT id, original_name, mime_type, size FROM attachments WHERE ticket_id = ?', [req.params.id]); t.attachments = await query('SELECT id, original_name, mime_type, size FROM attachments WHERE ticket_id = ?', [req.params.id]);
res.json(t); res.json(t);
}); });
router.get('/reported-players', async (req, res) => { router.get('/reported-players', async (req, res) => {
const rows = await query(`SELECT target_game_name, target_game_uid, const rows = await query(`SELECT target_game_name, target_game_uid, COUNT(*) as total,
COUNT(*) as total, SUM(CASE WHEN status IN ('pending','processing','awaiting_info','appealing') THEN 1 ELSE 0 END) as active, SUM(CASE WHEN status IN ('pending','processing','awaiting_info','appealing') THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) as resolved, SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) as resolved,
MAX(created_at) as last_report MAX(created_at) as last_report
FROM tickets WHERE type='report' AND target_game_name IS NOT NULL AND target_game_name != '' FROM tickets WHERE type='report' AND target_game_name IS NOT NULL AND target_game_name != ''
@@ -142,14 +125,4 @@ router.get('/stats', async (req, res) => {
res.json({ total, byType, byStatus }); res.json({ total, byType, byStatus });
}); });
router.get('/users', async (req, res) => {
res.json(await query('SELECT id, username, email, game_name, game_uid, role, active, created_at FROM users ORDER BY id'));
});
router.get('/user/:id', async (req, res) => {
const u = await getRow('SELECT id, username, email, game_name, game_uid, role, active, created_at FROM users WHERE id = ?', [req.params.id]);
if (!u) return res.status(404).json({ error: '用户不存在' });
res.json(u);
});
module.exports = router; module.exports = router;

View File

@@ -91,7 +91,11 @@ public class ApiClient {
return request("POST", "/tickets", body, token); return request("POST", "/tickets", body, token);
} }
public JsonObject trackTicket(String trackingToken) { public JsonObject getMyTickets(String token) {
return request("GET", "/tickets/tracking?token=" + trackingToken, null, null); return request("GET", "/my-tickets?limit=10", null, token);
}
public JsonObject getTicket(String token, String id) {
return request("GET", "/my-tickets/" + id, null, token);
} }
} }

View File

@@ -9,6 +9,7 @@ public final class MCReportPlugin extends JavaPlugin {
private ApiClient apiClient; private ApiClient apiClient;
private final Map<UUID, String> sessions = new HashMap<>(); private final Map<UUID, String> sessions = new HashMap<>();
private final Map<UUID, String> loginNames = new HashMap<>();
private List<String> adminPerms; private List<String> adminPerms;
@Override @Override
@@ -42,8 +43,12 @@ public final class MCReportPlugin extends JavaPlugin {
public void removeToken(Player player) { public void removeToken(Player player) {
sessions.remove(player.getUniqueId()); sessions.remove(player.getUniqueId());
loginNames.remove(player.getUniqueId());
} }
public void setLoginName(Player player, String name) { loginNames.put(player.getUniqueId(), name); }
public String getLoginName(Player player) { return loginNames.getOrDefault(player.getUniqueId(), player.getName()); }
public boolean isOnlineAdmin(Player target) { public boolean isOnlineAdmin(Player target) {
if (target.isOp()) return true; if (target.isOp()) return true;
for (String perm : adminPerms) { for (String perm : adminPerms) {

View File

@@ -1,5 +1,6 @@
package com.mcreport.plugin; package com.mcreport.plugin;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.Command; import org.bukkit.command.Command;
@@ -7,7 +8,7 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.Arrays; import java.util.*;
public class ReportCommand implements CommandExecutor { public class ReportCommand implements CommandExecutor {
@@ -20,7 +21,7 @@ public class ReportCommand implements CommandExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage("§c仅玩家可使用此命令"); sender.sendMessage("§cPlayer only");
return true; return true;
} }
@@ -29,99 +30,76 @@ public class ReportCommand implements CommandExecutor {
return true; return true;
} }
String sub = args[0].toLowerCase(); switch (args[0].toLowerCase()) {
switch (sub) {
case "login": return handleLogin(player, args);
case "reg": case "register": return handleRegister(player, args); case "reg": case "register": return handleRegister(player, args);
case "track": return handleTrack(player, args); case "login": return handleLogin(player, args);
case "举报": return handleReport(player, args, "report"); case "report": case "r": return handleSubmit(player, args, "report");
case "建议": return handleReport(player, args, "suggestion"); case "suggest": case "s": return handleSubmit(player, args, "suggestion");
case "事项": return handleReport(player, args, "report"); case "track": case "t": return handleTrack(player, args);
default: showHelp(player); return true; default: showHelp(player); return true;
} }
} }
private void showHelp(Player player) { private void showHelp(Player player) {
player.sendMessage("§6===== MC举报系统 ====="); if (plugin.getToken(player) == null) {
String token = plugin.getToken(player); player.sendMessage("§6===== MCReport =====");
if (token == null) { player.sendMessage("§e/report reg <password> <email> [username] §7- Register");
player.sendMessage("§e/report reg <密码> <邮箱> §7- 注册账号"); player.sendMessage("§e/report login <password> [username] §7- Login");
player.sendMessage("§e/report login <密码> §7- 登录");
} else { } else {
player.sendMessage("§e/report 举报 [玩家名] <原因> §7- 举报玩家"); player.sendMessage("§6===== MCReport =====");
player.sendMessage("§e/report 建议 <内容> §7- 提交建议"); player.sendMessage("§e/report report [player] <reason> §7- Report a player");
player.sendMessage("§e/report 事项 <内容> §7- 事项提交"); player.sendMessage("§e/report suggest <content> §7- Submit suggestion");
player.sendMessage("§e/report track <工单ID> §7- 追踪进度"); player.sendMessage("§e/report track [id] §7- List/view tickets");
} }
} }
private boolean handleLogin(Player player, String[] args) { private boolean handleLogin(Player player, String[] args) {
if (args.length < 2) { if (args.length < 2) { player.sendMessage("§cUsage: /report login <password> [username]"); return true; }
player.sendMessage("§c用法: /report login <密码>"); String pwd = args[1];
return true; String username = args.length > 2 ? args[2] : player.getName();
}
String password = args[1];
String username = player.getName();
player.sendMessage("§7登录中..."); player.sendMessage("§7Logging in...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
JsonObject res = plugin.getApiClient().login(username, password); JsonObject res = plugin.getApiClient().login(username, pwd);
int status = res.get("_status").getAsInt(); int status = res.get("_status").getAsInt();
if (status == 200) { if (status == 200) {
String token = res.get("token").getAsString(); plugin.setToken(player, res.get("token").getAsString());
plugin.setToken(player, token); plugin.setLoginName(player, username);
player.sendMessage("§a登录成功!现在可以使用 /report 提交举报"); player.sendMessage("§aLogged in as §e" + username);
} else { } else {
String err = res.has("error") ? res.get("error").getAsString() : "未知错误"; player.sendMessage("§cLogin failed: " + res.get("error").getAsString());
player.sendMessage("§c登录失败: " + err);
} }
}); });
return true; return true;
} }
private boolean handleRegister(Player player, String[] args) { private boolean handleRegister(Player player, String[] args) {
if (args.length < 3) { if (args.length < 3) { player.sendMessage("§cUsage: /report reg <password> <email> [username]"); return true; }
player.sendMessage("§c用法: /report reg <密码> <邮箱>"); String pwd = args[1];
return true;
}
String password = args[1];
String email = args[2]; String email = args[2];
String username = args.length > 3 ? args[3] : player.getName();
if (password.length() < 6) { if (pwd.length() < 6) { player.sendMessage("§cPassword must be at least 6 characters"); return true; }
player.sendMessage("§c密码至少6位");
return true;
}
player.sendMessage("§7注册中..."); player.sendMessage("§7Registering...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
JsonObject res = plugin.getApiClient().register( JsonObject res = plugin.getApiClient().register(
player.getName(), password, email, username, pwd, email,
player.getName(), player.getUniqueId().toString() player.getName(), player.getUniqueId().toString()
); );
int status = res.get("_status").getAsInt(); int status = res.get("_status").getAsInt();
if (status == 201) { if (status == 201) {
String msg = res.get("message").getAsString(); player.sendMessage("§a" + res.get("message").getAsString());
player.sendMessage("§a" + msg); player.sendMessage("§6Username: §e" + username + " §6Game: §e" + player.getName());
if (msg.contains("自动激活")) {
player.sendMessage("§e账号已激活请使用 §a/report login <密码> §e登录");
} else { } else {
player.sendMessage("§e请查收邮箱验证邮件后使用 §a/report login <密码> §e登录"); player.sendMessage("§cRegister failed: " + res.get("error").getAsString());
}
} else {
String err = res.has("error") ? res.get("error").getAsString() : "未知错误";
player.sendMessage("§c注册失败: " + err);
} }
}); });
return true; return true;
} }
private boolean handleReport(Player player, String[] args, String type) { private boolean handleSubmit(Player player, String[] args, String type) {
String token = plugin.getToken(player); if (plugin.getToken(player) == null) { player.sendMessage("§cLogin first: /report login <password>"); return true; }
if (token == null) {
player.sendMessage("§c请先登录: /report login <密码>");
return true;
}
int contentStart = 1; int contentStart = 1;
String targetName = null; String targetName = null;
@@ -139,59 +117,97 @@ public class ReportCommand implements CommandExecutor {
} }
if (args.length <= contentStart) { if (args.length <= contentStart) {
player.sendMessage("§c请填写" + (type.equals("report") ? "举报原因" : type.equals("suggestion") ? "建议内容" : "事项内容")); player.sendMessage("§cPlease provide " + (type.equals("report")?"a reason":"content"));
return true; return true;
} }
String content = String.join(" ", Arrays.copyOfRange(args, contentStart, args.length)); 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 String title = targetName != null
? "游戏内举报: " + targetName ? "In-game report: " + targetName
: type.equals("suggestion") : type.equals("suggestion")
? "游戏内建议: " + player.getName() ? "In-game suggestion: " + player.getName()
: "游戏内举报: " + player.getName(); : "In-game report: " + player.getName();
player.sendMessage("§7提交中..."); player.sendMessage("§7Submitting...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
JsonObject res = plugin.getApiClient().submitTicket( JsonObject res = plugin.getApiClient().submitTicket(
token, type, title, plugin.getToken(player), type, title,
player.getName(), player.getUniqueId().toString(), player.getName(), player.getUniqueId().toString(),
targetName, targetUid, reason, description, isAdminComplaint targetName, targetUid,
type.equals("report")?content:null,
type.equals("suggestion")?content:null,
isAdminComplaint
); );
int status = res.get("_status").getAsInt(); if (res.get("_status").getAsInt() == 200 || res.get("_status").getAsInt() == 201) {
if (status == 200 || status == 201) { player.sendMessage("§aSubmitted! Ticket §e#" + res.get("id").getAsInt());
int id = res.get("id").getAsInt(); player.sendMessage("§7Use §e/report track §7to check progress");
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 { } else {
String err = res.has("error") ? res.get("error").getAsString() : "未知错误"; player.sendMessage("§cFailed: " + res.get("error").getAsString());
player.sendMessage("§c提交失败: " + err);
} }
}); });
return true; return true;
} }
private boolean handleTrack(Player player, String[] args) { private boolean handleTrack(Player player, String[] args) {
if (args.length < 2) { if (plugin.getToken(player) == null) { player.sendMessage("§cLogin first"); return true; }
player.sendMessage("§c用法: /report track <工单ID>");
return true; player.sendMessage("§7Loading...");
}
String trackingToken = args[1];
player.sendMessage("§7查询中...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
JsonObject res = plugin.getApiClient().trackTicket(trackingToken); if (args.length > 1) {
// The API returns an array, we wrap in a JSON object JsonObject res = plugin.getApiClient().getTicket(plugin.getToken(player), args[1]);
if (res.has("error")) { showTicketDetail(player, res);
player.sendMessage("§c工单不存在或无权查看"); } else {
return; JsonObject res = plugin.getApiClient().getMyTickets(plugin.getToken(player));
showTicketList(player, res);
} }
player.sendMessage("§6===== 工单列表 =====");
player.sendMessage("§e可通过 Web 端查看详情");
}); });
return true; return true;
} }
private void showTicketList(Player player, JsonObject res) {
if (res.get("_status").getAsInt() != 200 || !res.has("list")) {
player.sendMessage("§cFailed to load tickets");
return;
}
JsonArray list = res.getAsJsonArray("list");
if (list.size() == 0) {
player.sendMessage("§7No tickets yet");
return;
}
player.sendMessage("§6===== Your Tickets (last " + list.size() + ") =====");
for (int i = 0; i < list.size(); i++) {
JsonObject t = list.get(i).getAsJsonObject();
String type = t.get("type").getAsString();
String typeLabel = type.equals("report")?"":type.equals("suggestion")?"":type.equals("appeal")?"":"?";
String status = t.get("status").getAsString();
String statusColor = "";
switch (status) {
case "pending": statusColor = "§e"; break;
case "processing": statusColor = "§d"; break;
case "resolved": statusColor = "§a"; break;
case "rejected": statusColor = "§c"; break;
case "closed": statusColor = "§7"; break;
case "awaiting_info": statusColor = "§6"; break;
case "appealing": statusColor = "§c"; break;
}
player.sendMessage(" §e#" + t.get("id").getAsInt() + " §7" + statusColor + status + " §f" + t.get("title").getAsString().substring(0, Math.min(30, t.get("title").getAsString().length())));
}
player.sendMessage("§7Use §e/report track <id> §7for details");
}
private void showTicketDetail(Player player, JsonObject res) {
if (res.get("_status").getAsInt() != 200 || res.has("error")) {
player.sendMessage("§cTicket not found");
return;
}
JsonObject t = res;
player.sendMessage("§6===== Ticket #" + t.get("id").getAsInt() + " =====");
player.sendMessage("§eTitle: §f" + t.get("title").getAsString());
player.sendMessage("§eStatus: §f" + t.get("status").getAsString());
player.sendMessage("§eType: §f" + t.get("type").getAsString());
if (t.has("assigned_to") && !t.get("assigned_to").isJsonNull()) {
player.sendMessage("§eHandler: §f#" + t.get("assigned_to").getAsInt());
}
player.sendMessage("§eDate: §7" + t.get("created_at").getAsString());
}
} }

View File

@@ -3,22 +3,21 @@ version: 1.0.0
main: com.mcreport.plugin.MCReportPlugin main: com.mcreport.plugin.MCReportPlugin
api-version: 1.20 api-version: 1.20
author: Sea-Studio author: Sea-Studio
description: MC举报系统 - 游戏内举报/建议/注册登录一体化插件 description: MC Report System Plugin
commands: commands:
report: report:
description: 游戏内举报/建议/账号管理 description: Game reporting & account management
usage: | usage: |
§6===== MC举报系统 ===== §6===== MCReport =====
§e/report login <密码> §7- 登录已有账号 §e/report reg <password> <email> [username] §7- Register
§e/report reg <密码> <邮箱> §7- 注册新账号 §e/report login <password> [username] §7- Login
§e/report 举报 [目标] <原因> §7- 举报玩家 §e/report report [player] <reason> §7- Report player
§e/report 建议 <内容> §7- 提交建议 §e/report suggest <content> §7- Submit suggestion
§e/report 事项 <内容> §7- 事项提交 §e/report track [id] §7- Track tickets
§e/report track <工单ID> §7- 追踪进度
aliases: [jubao,举报] aliases: [jubao,举报]
permissions: permissions:
mcreport.use: mcreport.use:
description: 使用举报功能 description: Use report functions
default: true default: true