2025-06-23 14:01:09 +08:00

451 lines
20 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, String> 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<PlayerTimeTracker.PlayerTimeStatsWithNames> statsList = timeTracker.getSortedPlayerStats();
List<PlayerStatsResponse> 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<String, String> 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<String, List<Map<String, String>>> 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<String, Integer> 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<Map<String, Object>> whitelist = timeTracker.getWhitelistPlayers();
List<WhitelistResponse> 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();
}
}