Skip to content

Commit a2765ab

Browse files
committed
feat(api): 支持查询离线玩家数据并优化超时逻辑
- 为 `/api/v1/player` 接口新增 `includeOffline=true` 参数以支持查询离线玩家 - 根据是否查询离线玩家动态调整超时时间(在线3秒,离线5秒) - 更新响应状态信息以区分在线与离线数据来源 - 优化错误提示信息,引导用户使用新参数查询离线玩家 feat(command): 新增白名单管理命令及Tab补全支持 - 添加 `/ca whitelist` 和 `/ca wl` 命令入口,支持 add/remove/list/check/sync 子命令 - 实现白名单添加、移除、检查、列表展示和同步功能 - 提供完整的异步处理和用户反馈机制 - 在Tab补全中加入白名单相关子命令提示 refactor(http): 扩展无需鉴权的API路径判断范围 - 允许 `/api/v1/register` 和 `/api/v1/player` 路径绕过白名单和管理员权限校验 - 用于支持公开注册和玩家查询接口访问
1 parent 716bbc8 commit a2765ab

3 files changed

Lines changed: 200 additions & 18 deletions

File tree

src/main/java/com/xaoxiao/convenientaccess/api/PlayerDataApiController.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ public PlayerDataApiController(org.bukkit.plugin.Plugin plugin) {
4646

4747
/**
4848
* 处理GET /api/v1/player?name=玩家名 - 获取玩家详细数据
49+
* 支持查询参数: includeOffline=true 来查询离线玩家
4950
* 注意: 必须在主线程中调用Bukkit API以避免线程安全问题
5051
* 优化: 添加并发限制和更短的超时时间
5152
*/
5253
public void handleGetPlayerData(HttpServletRequest request, HttpServletResponse response) throws IOException {
5354
// 从查询参数中获取玩家名
5455
String playerName = request.getParameter("name");
56+
String includeOfflineParam = request.getParameter("includeOffline");
57+
boolean includeOffline = "true".equalsIgnoreCase(includeOfflineParam);
5558

5659
try {
5760
if (playerName == null || playerName.trim().isEmpty()) {
@@ -72,35 +75,49 @@ public void handleGetPlayerData(HttpServletRequest request, HttpServletResponse
7275

7376
Bukkit.getScheduler().runTask(plugin, () -> {
7477
try {
75-
// 只查询在线玩家
78+
// 首先尝试获取在线玩家
7679
Player onlinePlayer = Bukkit.getPlayerExact(playerName);
7780

7881
if (onlinePlayer != null && onlinePlayer.isOnline()) {
7982
// 玩家在线,获取完整数据
8083
PlayerData playerData = collectOnlinePlayerData(onlinePlayer);
8184
future.complete(playerData);
85+
} else if (includeOffline) {
86+
// 如果允许查询离线玩家,尝试获取离线玩家数据
87+
@SuppressWarnings("deprecation")
88+
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
89+
90+
if (offlinePlayer.hasPlayedBefore()) {
91+
PlayerData playerData = collectOfflinePlayerData(offlinePlayer);
92+
future.complete(playerData);
93+
} else {
94+
future.completeExceptionally(new IllegalArgumentException("玩家不存在或从未登录过服务器"));
95+
}
8296
} else {
83-
future.completeExceptionally(new IllegalArgumentException("玩家不在线或不存在"));
97+
future.completeExceptionally(new IllegalArgumentException("玩家不在线 (提示: 使用 ?includeOffline=true 查询离线玩家)"));
8498
}
8599
} catch (Exception e) {
86100
future.completeExceptionally(e);
87101
}
88102
});
89103

90-
// 缩短超时时间到3秒 - 如果服务器TPS不健康,快速失败
91-
PlayerData playerData = future.get(3, TimeUnit.SECONDS);
92-
sendJsonResponse(response, 200, ApiResponse.success(playerData, "成功获取玩家数据"));
93-
logger.debug("成功获取在线玩家数据: {}", playerName);
104+
// 超时时间: 在线玩家3秒, 离线玩家5秒(需要磁盘IO)
105+
int timeout = includeOffline ? 5 : 3;
106+
PlayerData playerData = future.get(timeout, TimeUnit.SECONDS);
107+
108+
String statusMsg = playerData.isOnline ? "在线" : "离线";
109+
sendJsonResponse(response, 200, ApiResponse.success(playerData, "成功获取玩家数据(" + statusMsg + ")"));
110+
logger.debug("成功获取{}玩家数据: {}", statusMsg, playerName);
94111

95112
} finally {
96113
querySemaphore.release();
97114
}
98115

99116
} catch (IllegalArgumentException e) {
100117
sendJsonResponse(response, 404, ApiResponse.notFound(e.getMessage()));
101-
logger.debug("玩家不在线: {}", playerName);
118+
logger.debug("玩家查询失败: {} - {}", playerName, e.getMessage());
102119
} catch (java.util.concurrent.TimeoutException e) {
103-
logger.warn("获取玩家数据超时(3秒): {} - 服务器TPS可能不健康", playerName);
120+
logger.warn("获取玩家数据超时: {} - 服务器TPS可能不健康", playerName);
104121
sendJsonResponse(response, 504, ApiResponse.error("服务器繁忙,请稍后重试(TPS过低)"));
105122
} catch (Exception e) {
106123
logger.error("获取玩家数据时发生错误: {}", playerName, e);

src/main/java/com/xaoxiao/convenientaccess/command/ConvenientAccessCommand.java

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Arrays;
44
import java.util.List;
5+
import java.util.concurrent.CompletableFuture;
56

67
import org.bukkit.ChatColor;
78
import org.bukkit.command.Command;
@@ -48,6 +49,10 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
4849
case "backup":
4950
handleBackup(sender, args);
5051
break;
52+
case "whitelist":
53+
case "wl":
54+
handleWhitelist(sender, args);
55+
break;
5156
case "help":
5257
showHelp(sender);
5358
break;
@@ -67,11 +72,13 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
6772
}
6873

6974
if (args.length == 1) {
70-
return Arrays.asList("reload", "status", "cache", "backup", "help");
75+
return Arrays.asList("reload", "status", "cache", "backup", "whitelist", "wl", "help");
7176
} else if (args.length == 2 && "cache".equals(args[0])) {
7277
return Arrays.asList("clear", "stats");
7378
} else if (args.length == 2 && "backup".equals(args[0])) {
7479
return Arrays.asList("now", "list", "restore");
80+
} else if (args.length == 2 && ("whitelist".equals(args[0]) || "wl".equals(args[0]))) {
81+
return Arrays.asList("add", "remove", "list", "check", "sync");
7582
}
7683

7784
return null;
@@ -230,6 +237,154 @@ private void restoreBackup(CommandSender sender, String backupFileName) {
230237
});
231238
}
232239

240+
private void handleWhitelist(CommandSender sender, String[] args) {
241+
if (plugin.getWhitelistSystem() == null || plugin.getWhitelistSystem().getWhitelistManager() == null) {
242+
sender.sendMessage(ChatColor.RED + "白名单系统未初始化!");
243+
return;
244+
}
245+
246+
if (args.length < 2) {
247+
sender.sendMessage(ChatColor.RED + "用法: /ca whitelist <add|remove|list|check|sync>");
248+
return;
249+
}
250+
251+
switch (args[1].toLowerCase()) {
252+
case "add":
253+
if (args.length < 3) {
254+
sender.sendMessage(ChatColor.RED + "用法: /ca whitelist add <玩家名> [备注]");
255+
return;
256+
}
257+
handleWhitelistAdd(sender, args);
258+
break;
259+
case "remove":
260+
if (args.length < 3) {
261+
sender.sendMessage(ChatColor.RED + "用法: /ca whitelist remove <玩家名>");
262+
return;
263+
}
264+
handleWhitelistRemove(sender, args[2]);
265+
break;
266+
case "list":
267+
handleWhitelistList(sender);
268+
break;
269+
case "check":
270+
if (args.length < 3) {
271+
sender.sendMessage(ChatColor.RED + "用法: /ca whitelist check <玩家名>");
272+
return;
273+
}
274+
handleWhitelistCheck(sender, args[2]);
275+
break;
276+
case "sync":
277+
handleWhitelistSync(sender);
278+
break;
279+
default:
280+
sender.sendMessage(ChatColor.RED + "未知白名单命令: " + args[1]);
281+
break;
282+
}
283+
}
284+
285+
private void handleWhitelistAdd(CommandSender sender, String[] args) {
286+
String playerName = args[2];
287+
288+
sender.sendMessage(ChatColor.YELLOW + "正在添加玩家 " + playerName + " 到白名单...");
289+
290+
// 使用 addPlayerOffline 方法,它会自动获取UUID
291+
plugin.getWhitelistSystem().getWhitelistManager()
292+
.addPlayerOffline(playerName, sender.getName(), "CONSOLE",
293+
com.xaoxiao.convenientaccess.whitelist.WhitelistEntry.Source.ADMIN)
294+
.thenAccept(success -> {
295+
if (success) {
296+
sender.sendMessage(ChatColor.GREEN + "✓ 成功添加玩家 " + playerName + " 到白名单!");
297+
} else {
298+
sender.sendMessage(ChatColor.RED + "✗ 添加失败,玩家可能已在白名单中");
299+
}
300+
})
301+
.exceptionally(throwable -> {
302+
sender.sendMessage(ChatColor.RED + "✗ 添加失败: " + throwable.getMessage());
303+
return null;
304+
});
305+
}
306+
307+
private void handleWhitelistRemove(CommandSender sender, String playerName) {
308+
sender.sendMessage(ChatColor.YELLOW + "正在从白名单移除玩家 " + playerName + "...");
309+
310+
// 首先通过名称查找玩家的UUID
311+
plugin.getWhitelistSystem().getWhitelistManager()
312+
.searchPlayersByName(playerName, 1)
313+
.thenCompose(entries -> {
314+
if (entries.isEmpty()) {
315+
sender.sendMessage(ChatColor.RED + "✗ 玩家不在白名单中");
316+
return CompletableFuture.completedFuture(false);
317+
}
318+
319+
String uuid = entries.get(0).getUuid();
320+
return plugin.getWhitelistSystem().getWhitelistManager().removePlayer(uuid);
321+
})
322+
.thenAccept(success -> {
323+
if (success) {
324+
sender.sendMessage(ChatColor.GREEN + "✓ 成功从白名单移除玩家 " + playerName + "!");
325+
}
326+
})
327+
.exceptionally(throwable -> {
328+
sender.sendMessage(ChatColor.RED + "✗ 移除失败: " + throwable.getMessage());
329+
return null;
330+
});
331+
}
332+
333+
private void handleWhitelistList(CommandSender sender) {
334+
sender.sendMessage(ChatColor.YELLOW + "正在获取白名单列表...");
335+
336+
// 使用分页API获取前20个
337+
plugin.getWhitelistSystem().getWhitelistManager()
338+
.getWhitelistPaginated(1, 20, null, null, null, "added_at", "DESC", null, null)
339+
.thenAccept(result -> {
340+
if (result.getItems().isEmpty()) {
341+
sender.sendMessage(ChatColor.YELLOW + "白名单为空");
342+
return;
343+
}
344+
345+
sender.sendMessage(ChatColor.GOLD + "=== 白名单列表 (共 " + result.getTotal() + " 人) ===");
346+
int count = 0;
347+
for (com.xaoxiao.convenientaccess.whitelist.WhitelistEntry entry : result.getItems()) {
348+
count++;
349+
sender.sendMessage(ChatColor.YELLOW + String.format("%d. %s", count, entry.getName()));
350+
sender.sendMessage(ChatColor.GRAY + " UUID: " + entry.getUuid());
351+
if (count >= 10) {
352+
sender.sendMessage(ChatColor.GRAY + "... 还有 " + (result.getTotal() - 10) + " 个玩家");
353+
break;
354+
}
355+
}
356+
sender.sendMessage(ChatColor.GRAY + "提示: 使用API查看完整列表");
357+
})
358+
.exceptionally(throwable -> {
359+
sender.sendMessage(ChatColor.RED + "✗ 获取白名单失败: " + throwable.getMessage());
360+
return null;
361+
});
362+
}
363+
364+
private void handleWhitelistCheck(CommandSender sender, String playerName) {
365+
plugin.getWhitelistSystem().getWhitelistManager()
366+
.isPlayerWhitelistedByName(playerName)
367+
.thenAccept(isWhitelisted -> {
368+
if (isWhitelisted) {
369+
sender.sendMessage(ChatColor.GREEN + "✓ 玩家 " + playerName + " 在白名单中");
370+
} else {
371+
sender.sendMessage(ChatColor.RED + "✗ 玩家 " + playerName + " 不在白名单中");
372+
}
373+
})
374+
.exceptionally(throwable -> {
375+
sender.sendMessage(ChatColor.RED + "✗ 查询失败: " + throwable.getMessage());
376+
return null;
377+
});
378+
}
379+
380+
private void handleWhitelistSync(CommandSender sender) {
381+
sender.sendMessage(ChatColor.YELLOW + "正在触发白名单同步...");
382+
383+
// 触发全量同步
384+
plugin.getWhitelistSystem().getSyncTaskManager().scheduleFullSync();
385+
sender.sendMessage(ChatColor.GREEN + "✓ 同步任务已提交,请稍后查看同步结果");
386+
}
387+
233388
private void showHelp(CommandSender sender) {
234389
sender.sendMessage(ChatColor.GOLD + "=== ConvenientAccess 命令帮助 ===");
235390
sender.sendMessage(ChatColor.YELLOW + "/ca reload" + ChatColor.WHITE + " - 重载插件配置");
@@ -239,6 +394,11 @@ private void showHelp(CommandSender sender) {
239394
sender.sendMessage(ChatColor.YELLOW + "/ca backup now" + ChatColor.WHITE + " - 立即执行备份");
240395
sender.sendMessage(ChatColor.YELLOW + "/ca backup list" + ChatColor.WHITE + " - 查看备份列表");
241396
sender.sendMessage(ChatColor.YELLOW + "/ca backup restore <文件名>" + ChatColor.WHITE + " - 恢复备份");
397+
sender.sendMessage(ChatColor.YELLOW + "/ca whitelist add <玩家名> [备注]" + ChatColor.WHITE + " - 添加白名单");
398+
sender.sendMessage(ChatColor.YELLOW + "/ca whitelist remove <玩家名>" + ChatColor.WHITE + " - 移除白名单");
399+
sender.sendMessage(ChatColor.YELLOW + "/ca whitelist list" + ChatColor.WHITE + " - 查看白名单列表");
400+
sender.sendMessage(ChatColor.YELLOW + "/ca whitelist check <玩家名>" + ChatColor.WHITE + " - 检查是否在白名单");
401+
sender.sendMessage(ChatColor.YELLOW + "/ca whitelist sync" + ChatColor.WHITE + " - 触发白名单同步");
242402
sender.sendMessage(ChatColor.YELLOW + "/ca help" + ChatColor.WHITE + " - 显示此帮助信息");
243403

244404
// 显示API端点信息

src/main/java/com/xaoxiao/convenientaccess/http/HttpServer.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package com.xaoxiao.convenientaccess.http;
22

3-
import com.xaoxiao.convenientaccess.ConvenientAccessPlugin;
4-
import com.xaoxiao.convenientaccess.api.ApiManager;
5-
import com.xaoxiao.convenientaccess.api.ApiRouter;
3+
import java.io.IOException;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import java.util.logging.Level;
7+
68
import org.eclipse.jetty.server.Request;
79
import org.eclipse.jetty.server.Server;
810
import org.eclipse.jetty.server.handler.AbstractHandler;
911
import org.eclipse.jetty.util.thread.QueuedThreadPool;
1012

13+
import com.xaoxiao.convenientaccess.ConvenientAccessPlugin;
14+
import com.xaoxiao.convenientaccess.api.ApiManager;
15+
import com.xaoxiao.convenientaccess.api.ApiRouter;
16+
1117
import jakarta.servlet.http.HttpServletRequest;
1218
import jakarta.servlet.http.HttpServletResponse;
13-
import java.io.IOException;
14-
import java.util.HashMap;
15-
import java.util.Map;
16-
import java.util.logging.Level;
1719

1820
/**
1921
* HTTP服务器
@@ -139,10 +141,13 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
139141
}
140142

141143
/**
142-
* 判断是否为白名单或管理员API
144+
* 判断是否为白名单、用户注册、玩家数据或管理员API
143145
*/
144146
private boolean isWhitelistOrAdminApi(String path) {
145-
return path.startsWith("/api/v1/whitelist") || path.startsWith("/api/v1/admin");
147+
return path.startsWith("/api/v1/whitelist") ||
148+
path.startsWith("/api/v1/admin") ||
149+
path.startsWith("/api/v1/register") ||
150+
path.equals("/api/v1/player");
146151
}
147152

148153
/**

0 commit comments

Comments
 (0)