package com.example.playertime; import com.google.gson.*; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.server.MinecraftServer; import net.minecraft.server.PlayerManager; import net.minecraft.server.network.ServerPlayerEntity; import java.io.*; import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; // 导入API类,别忘了 import com.example.playertime.api.PlayerStatsResponse; import com.example.playertime.api.ServerStatusResponse; import com.example.playertime.api.OnlinePlayersResponse; import com.example.playertime.api.PlayerCountResponse; import com.example.playertime.api.WhitelistResponse; public class WebServer { private final HttpServer server; private final PlayerTimeTracker timeTracker; private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private final ExecutorService executor = Executors.newFixedThreadPool(4); private final MinecraftServer minecraftServer; private static final Map MIME_TYPES = Map.ofEntries( Map.entry("html", "text/html"), Map.entry("css", "text/css"), Map.entry("js", "application/javascript"), Map.entry("json", "application/json"), Map.entry("png", "image/png"), Map.entry("jpg", "image/jpeg"), Map.entry("jpeg", "image/jpeg"), Map.entry("gif", "image/gif"), Map.entry("svg", "image/svg+xml"), Map.entry("woff", "application/font-woff"), Map.entry("woff2", "application/font-woff2"), Map.entry("ttf", "application/font-sfnt"), Map.entry("eot", "application/vnd.ms-fontobject"), Map.entry("ico", "image/x-icon") // 添加 favicon.ico ); public WebServer(PlayerTimeTracker timeTracker, int port, MinecraftServer minecraftServer) throws IOException { this.minecraftServer = minecraftServer; if (port < 1 || port > 65535) { throw new IllegalArgumentException("Invalid port number: " + port); } this.timeTracker = timeTracker; this.server = HttpServer.create(new InetSocketAddress(port), 0); setupContexts(); } // 创建HTTP服务器 private void setupContexts() { // API: 获取玩家统计数据 server.createContext("/api/stats", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); // CORS 预检请求 return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { List statsList = timeTracker.getSortedPlayerStats(); List responseList = statsList.stream() .map(stats -> new PlayerStatsResponse( stats.playerName, stats.uuid.toString(), stats.getTotalTime(), PlayerTimeTracker.formatTime(stats.getTotalTime()), stats.last30Days, PlayerTimeTracker.formatTime(stats.last30Days), stats.last7Days, PlayerTimeTracker.formatTime(stats.last7Days) )) .collect(Collectors.toList()); String response = GSON.toJson(responseList); sendResponse(exchange, 200, response.getBytes(StandardCharsets.UTF_8), "application/json"); PlayerTimeMod.LOGGER.debug("[在线时间] API /api/stats 请求成功,返回 {} 条玩家数据。", responseList.size()); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 处理 /api/stats 请求失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // API: 获取语言文件内容,不公开 server.createContext("/api/lang", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { Map langMap = PlayerTimeMod.getLocalizationManager().getAllStrings(); String response = GSON.toJson(langMap); sendResponse(exchange, 200, response.getBytes(StandardCharsets.UTF_8), "application/json"); PlayerTimeMod.LOGGER.debug("[在线时间] API /api/lang 请求成功,返回 {} 条语言字符串。", langMap.size()); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 处理 /api/lang 请求失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // API: 获取在线玩家列表 server.createContext("/api/online-players", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { Map>> onlinePlayers = timeTracker.getOnlinePlayersCategorized(); OnlinePlayersResponse response = new OnlinePlayersResponse(onlinePlayers.get("whitelisted"), onlinePlayers.get("non_whitelisted")); sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json"); PlayerTimeMod.LOGGER.debug("[在线时间] API /api/online-players 请求成功。"); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 处理 /api/online-players 请求失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // API: 获取在线玩家数量 server.createContext("/api/player-count", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { Map counts = timeTracker.getOnlinePlayerCounts(); PlayerCountResponse response = new PlayerCountResponse(counts.get("total"), counts.get("whitelisted"), counts.get("non_whitelisted")); sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json"); PlayerTimeMod.LOGGER.debug("[在线时间] API /api/player-count 请求成功。"); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 处理 /api/player-count 请求失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // API: 获取白名单列表 server.createContext("/api/whitelist", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { List> whitelist = timeTracker.getWhitelistPlayers(); List responseList = whitelist.stream() .map(map -> new WhitelistResponse((String)map.get("name"), (String)map.get("uuid"), (Boolean)map.get("online"))) .collect(Collectors.toList()); sendResponse(exchange, 200, GSON.toJson(responseList).getBytes(StandardCharsets.UTF_8), "application/json"); PlayerTimeMod.LOGGER.debug("[在线时间] API /api/whitelist 请求成功,返回 {} 条白名单记录。", responseList.size()); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 处理 /api/whitelist 请求失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // API: 获取服务器状态 server.createContext("/api/server-status", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { Runtime runtime = Runtime.getRuntime(); ServerStatusResponse status = new ServerStatusResponse(); long maxMemory = runtime.maxMemory(); long totalMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); long usedMemory = totalMemory - freeMemory; status.memory.max = maxMemory; status.memory.total = totalMemory; status.memory.used = usedMemory; status.memory.free = freeMemory; status.memory.usage_percentage = (double) usedMemory / maxMemory * 100; status.available_processors = runtime.availableProcessors(); long uptimeMillis = ManagementFactory.getRuntimeMXBean().getUptime(); status.uptime = uptimeMillis / 1000; // 秒 status.uptime_formatted = formatUptime(uptimeMillis); try { File diskPartition = new File("."); status.disk.total = diskPartition.getTotalSpace(); status.disk.free = diskPartition.getFreeSpace(); status.disk.usable = diskPartition.getUsableSpace(); status.disk.usage_percentage = (double) (status.disk.total - status.disk.free) / status.disk.total * 100; } catch (Exception e) { PlayerTimeMod.LOGGER.warn("[在线时间] 无法获取磁盘信息", e); } status.server.version = minecraftServer.getVersion(); status.server.player_count = minecraftServer.getCurrentPlayerCount(); status.server.max_players = minecraftServer.getMaxPlayerCount(); status.server.motd = minecraftServer.getServerMotd(); status.server.average_tick_time_ms = minecraftServer.getAverageTickTime(); long[] tickTimes = minecraftServer.getTickTimes(); if (tickTimes != null && tickTimes.length > 0) { long[] recentTicksCopy = Arrays.copyOf(tickTimes, tickTimes.length); status.server.recent_tick_samples_ms = Arrays.stream(recentTicksCopy) .mapToDouble(tick -> tick / 1000000.0) .boxed() .collect(Collectors.toList()); } else { status.server.recent_tick_samples_ms = Collections.emptyList(); } sendResponse(exchange, 200, GSON.toJson(status).getBytes(StandardCharsets.UTF_8), "application/json"); PlayerTimeMod.LOGGER.debug("[在线时间] API /api/server-status 请求成功。"); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 处理 /api/server-status 请求失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // API: 获取原始玩家数据文件内容,建议最好别公开 server.createContext("/api/playerdata", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { Path dataFile = timeTracker.getDataFile(); if (!Files.exists(dataFile)) { PlayerTimeMod.LOGGER.warn("[在线时间] 玩家数据文件未找到: {}", dataFile); sendResponse(exchange, 404, "Data file not found"); return; } byte[] fileContent = Files.readAllBytes(dataFile); sendResponse(exchange, 200, fileContent, "application/json"); PlayerTimeMod.LOGGER.debug("[在线时间] 成功提供玩家数据文件 {}", dataFile); } catch (IOException e) { PlayerTimeMod.LOGGER.error("[在线时间] 读取玩家数据文件时出错", e); sendResponse(exchange, 500, "Error reading data file"); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 处理 /api/playerdata 请求时发生未知错误", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // 静态文件服务 server.createContext("/", exchange -> { try { String requestPath = exchange.getRequestURI().getPath(); String resourcePath = "assets/playertime/web" + requestPath; if (requestPath.equals("/")) { resourcePath += "index.html"; } if (resourcePath.contains("..")) { sendResponse(exchange, 403, "Forbidden"); return; } String finalResourcePath = resourcePath; InputStream is = FabricLoader.getInstance().getModContainer("playertime") .flatMap(container -> container.findPath(finalResourcePath)) .map(p -> { try { return p.toUri().toURL().openStream(); } catch (Exception e) { return null; } }) .orElse(null); if (is == null) { if (!requestPath.equals("/") && !requestPath.contains(".")) { PlayerTimeMod.LOGGER.debug("[在线时间] 找不到静态资源:{},尝试提供 index.html 作为 fallback。", resourcePath); resourcePath = "assets/playertime/web/index.html"; String finalResourcePath1 = resourcePath; is = FabricLoader.getInstance().getModContainer("playertime") .flatMap(container -> container.findPath(finalResourcePath1)) .map(p -> { try { return p.toUri().toURL().openStream(); } catch (Exception e) { return null; } }) .orElse(null); } if (is == null) { PlayerTimeMod.LOGGER.warn("[在线时间] 找不到静态资源: {}", finalResourcePath); sendResponse(exchange, 404, "Not Found"); return; } } String fileName = Paths.get(resourcePath).getFileName().toString(); String extension = ""; int dotIndex = fileName.lastIndexOf('.'); if (dotIndex > 0 && dotIndex < fileName.length() - 1) { extension = fileName.substring(dotIndex + 1).toLowerCase(); } String contentType = MIME_TYPES.getOrDefault(extension, "application/octet-stream"); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[4096]; int nRead; while ((nRead = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); is.close(); sendResponse(exchange, 200, buffer.toByteArray(), contentType); PlayerTimeMod.LOGGER.debug("[在线时间] 提供静态文件: {}", resourcePath); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 无法提供静态资源", e); sendResponse(exchange, 500, "Internal Server Error"); } }); server.setExecutor(executor); } // 跨域处理 private void handleCors(HttpExchange exchange) { exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, OPTIONS"); exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); } // 发送响应 private void sendResponse(HttpExchange exchange, int code, String response) throws IOException { sendResponse(exchange, code, response.getBytes(StandardCharsets.UTF_8), "text/plain"); } private void sendResponse(HttpExchange exchange, int code, byte[] response, String contentType) throws IOException { exchange.getResponseHeaders().set("Content-Type", contentType); exchange.sendResponseHeaders(code, response.length); try (OutputStream os = exchange.getResponseBody()) { os.write(response); } } // 启动 public void start() { server.start(); } // 停止 public void stop() { server.stop(3); executor.shutdown(); try { if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { executor.shutdownNow(); // 强制关闭 } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } // 格式化时间 private String formatUptime(long millis) { long seconds = millis / 1000; long days = seconds / 86400; seconds %= 86400; long hours = seconds / 3600; seconds %= 3600; long minutes = seconds / 60; seconds %= 60; StringBuilder sb = new StringBuilder(); if (days > 0) sb.append(days).append("天 "); if (hours > 0 || days > 0) sb.append(String.format("%02d小时 ", hours)); if (minutes > 0 || hours > 0 || days > 0) sb.append(String.format("%02d分钟 ", minutes)); sb.append(String.format("%02d秒", seconds)); return sb.toString().trim(); } }