diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a26d259..b075d3c 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -60,6 +60,7 @@ jobs: - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@v3 if: github.event_name != 'pull_request' + continue-on-error: true with: ignore-maven-wrapper: true @@ -97,5 +98,5 @@ jobs: push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: | - ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }} - ghcr.io/qaiu/netdisk-fast-download:latest + ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }} + ghcr.io/${{ github.repository }}:latest diff --git a/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java b/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java index b22039f..12e89ab 100644 --- a/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java +++ b/core-database/src/main/java/cn/qaiu/db/pool/JDBCPoolInit.java @@ -17,7 +17,7 @@ import org.slf4j.LoggerFactory; * * @author QAIU */ -public class JDBCPoolInit { +public class JDBCPoolInit implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class); @@ -101,4 +101,16 @@ public class JDBCPoolInit { synchronized public JDBCPool getPool() { return pool; } + + /** + * 关闭连接池,释放数据库资源 + */ + @Override + public synchronized void close() { + if (pool != null) { + pool.close(); + LOGGER.info("数据库连接池已关闭: URL={}", url); + pool = null; + } + } } diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index e6717be..09c2f9f 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -65,7 +65,11 @@ public final class Deploy { // 读取yml配置 ConfigUtil.readYamlConfig(path.toString(), tempVertx) .onSuccess(this::readConf) - .onFailure(Throwable::printStackTrace); + .onFailure(err -> { + LOGGER.error("读取配置文件失败: {}", err.getMessage(), err); + LockSupport.unpark(mainThread); + System.exit(-1); + }); LockSupport.park(); deployVerticle(); } @@ -137,6 +141,17 @@ public final class Deploy { vertxOptions.getWorkerPoolSize()); var vertx = Vertx.vertx(vertxOptions); VertxHolder.init(vertx); + + // 注册 ShutdownHook,确保进程退出时优雅关闭资源 + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + LOGGER.info("JVM shutting down, closing Vert.x..."); + try { + vertx.close().toCompletionStage().toCompletableFuture().get(10, java.util.concurrent.TimeUnit.SECONDS); + LOGGER.info("Vert.x closed successfully"); + } catch (Exception e) { + LOGGER.warn("Vert.x close error or timeout", e); + } + })); //配置保存在共享数据中 var sharedData = vertx.sharedData(); LocalMap localMap = sharedData.getLocalMap(LOCAL); diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java index 0b218c0..b1ae974 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/ParamUtil.java @@ -36,16 +36,20 @@ public final class ParamUtil { public static MultiMap paramsToMap(String paramString) { MultiMap entries = MultiMap.caseInsensitiveMultiMap(); - if (paramString == null) return entries; + if (paramString == null || paramString.isEmpty()) return entries; String[] params = paramString.split("&"); if (params.length == 0) return entries; for (String param : params) { - String[] kv = param.split("="); + if (param == null || param.isEmpty()) { + continue; + } + String[] kv = param.split("=", 2); if (kv.length == 2) { entries.set(kv[0], kv[1]); - } else { + } else if (kv.length == 1) { entries.set(kv[0], ""); } + // kv.length == 0 时(空字符串),跳过 } return entries; } diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java index 89a48ba..3d30eee 100644 --- a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java +++ b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java @@ -241,7 +241,7 @@ public final class ReflectionUtil { public static boolean isBasicTypeArray(CtClass ctClass) { if (!ctClass.isArray()) { return false; - } else return (ctClass.getName().matches("^(boolen|char|byte|short|int|long|float|double|String)\\[]$")); + } else return (ctClass.getName().matches("^(boolean|char|byte|short|int|long|float|double|String)\\[]$")); } /** diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java index 64fd990..aa713ea 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java @@ -129,16 +129,25 @@ public class HttpProxyVerticle extends AbstractVerticle { clientRequest.response().setStatusCode(403).end(); return; } - String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":"); - if (split.length > 1) { - // TODO - String username = proxyServerConf.getString("username"); - String password = proxyServerConf.getString("password"); - if (!split[0].equals(username) || !split[1].equals(password)) { - LOGGER.info("-----auth failed------\nusername: {}\npassword: {}", username, password); - clientRequest.response().setStatusCode(403).end(); - return; - } + String[] split; + try { + split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":"); + } catch (IllegalArgumentException e) { + LOGGER.warn("Proxy-Authorization header is not valid Base64"); + clientRequest.response().setStatusCode(403).end(); + return; + } + if (split.length <= 1) { + LOGGER.warn("Proxy-Authorization header format invalid: missing username:password separator"); + clientRequest.response().setStatusCode(403).end(); + return; + } + String username = proxyServerConf.getString("username"); + String password = proxyServerConf.getString("password"); + if (!split[0].equals(username) || !split[1].equals(password)) { + LOGGER.info("-----auth failed------\nusername: {}", split[0]); + clientRequest.response().setStatusCode(403).end(); + return; } } diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java index 8cfa6d4..717bc19 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java @@ -46,7 +46,7 @@ public class PostExecVerticle extends AbstractVerticle { return; } LOGGER.info("PostExecVerticle 开始执行..."); - + if (appRunImplementations != null && !appRunImplementations.isEmpty()) { appRunImplementations.forEach(appRun -> { try { @@ -61,7 +61,7 @@ public class PostExecVerticle extends AbstractVerticle { } else { LOGGER.info("未找到 AppRun 接口的实现类"); } - + LOGGER.info("PostExecVerticle 执行完成"); startPromise.complete(); } diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java index 977c4d7..7715758 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java @@ -351,7 +351,7 @@ public class ReverseProxyVerticle extends AbstractVerticle { String host = url.getHost(); int port = url.getPort(); if (port == -1) { - port = 80; + port = 443; } String originPath = url.getPath(); LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port); diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java index 946b339..0ff23d7 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java @@ -5,11 +5,15 @@ import cn.qaiu.vx.core.base.BaseAsyncService; import cn.qaiu.vx.core.util.ReflectionUtil; import io.vertx.core.AbstractVerticle; import io.vertx.core.Promise; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.json.JsonObject; import io.vertx.serviceproxy.ServiceBinder; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -24,6 +28,7 @@ public class ServiceVerticle extends AbstractVerticle { Logger LOGGER = LoggerFactory.getLogger(ServiceVerticle.class); private static final AtomicInteger ID = new AtomicInteger(1); private static final Set> handlers; + private final List> consumers = new ArrayList<>(); static { Reflections reflections = ReflectionUtil.getReflections(); @@ -39,7 +44,10 @@ public class ServiceVerticle extends AbstractVerticle { try { serviceNames.append(asyncService.getName()).append("|"); BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService); - binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance); + String address = asInstance.getAddress(); + MessageConsumer consumer = binder.setAddress(address) + .register(asInstance.getAsyncInterfaceClass(), asInstance); + consumers.add(consumer); } catch (Exception e) { LOGGER.error("Failed to register service: {}", asyncService.getName(), e); } @@ -49,4 +57,19 @@ public class ServiceVerticle extends AbstractVerticle { } startPromise.complete(); } + + @Override + public void stop(Promise stopPromise) { + int count = consumers.size(); + consumers.forEach(consumer -> { + try { + consumer.unregister(); + } catch (Exception e) { + LOGGER.warn("Failed to unregister service consumer at address: {}", consumer.address(), e); + } + }); + consumers.clear(); + LOGGER.info("ServiceVerticle stopped, unregistered {} services", count); + stopPromise.complete(); + } } diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java index c8c4d5d..b6a5e70 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java @@ -32,7 +32,7 @@ public class HttpProxyConf { public HttpProxyConf() { this.username = DEFAULT_USERNAME; this.password = DEFAULT_PASSWORD; - this.timeout = DEFAULT_PORT; + this.port = DEFAULT_PORT; this.timeout = DEFAULT_TIMEOUT; this.preProxyOptions = new ProxyOptions(); } diff --git a/parser/src/main/java/cn/qaiu/parser/PanBase.java b/parser/src/main/java/cn/qaiu/parser/PanBase.java index c1bc071..7ea50c4 100644 --- a/parser/src/main/java/cn/qaiu/parser/PanBase.java +++ b/parser/src/main/java/cn/qaiu/parser/PanBase.java @@ -40,28 +40,34 @@ public abstract class PanBase implements IPanTool { protected Promise promise = Promise.promise(); /** - * Http client + * 共享的 WebClient 实例(线程安全,避免每请求创建导致资源泄漏) */ - protected WebClient client = WebClient.create(WebClientVertxInit.get(), + private static final WebClient SHARED_CLIENT = WebClient.create(WebClientVertxInit.get(), new WebClientOptions()); + private static final WebClient SHARED_CLIENT_NO_REDIRECTS = WebClient.create(WebClientVertxInit.get(), + new WebClientOptions().setFollowRedirects(false)); + private static final WebClient SHARED_CLIENT_DISABLE_UA = WebClient.create(WebClientVertxInit.get(), + new WebClientOptions().setUserAgentEnabled(false)); /** - * Http client session (会话管理, 带cookie请求) + * Http client (默认使用共享实例,代理模式下使用独立实例) + */ + protected WebClient client = SHARED_CLIENT; + + /** + * Http client session (会话管理, 带cookie请求, 每实例独立) */ protected WebClientSession clientSession = WebClientSession.create(client); /** * Http client 不自动跳转 */ - protected WebClient clientNoRedirects = WebClient.create(WebClientVertxInit.get(), - new WebClientOptions().setFollowRedirects(false)); + protected WebClient clientNoRedirects = SHARED_CLIENT_NO_REDIRECTS; /** * Http client disable UserAgent */ - protected WebClient clientDisableUA = WebClient.create(WebClientVertxInit.get() - , new WebClientOptions().setUserAgentEnabled(false) - ); + protected WebClient clientDisableUA = SHARED_CLIENT_DISABLE_UA; protected ShareLinkInfo shareLinkInfo; diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java index 24dbeef..0442903 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java @@ -61,7 +61,7 @@ public class JsHttpClient { }; public JsHttpClient() { - this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());; + this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions()); this.clientSession = WebClientSession.create(client); this.headers = MultiMap.caseInsensitiveMultiMap(); // 设置默认的Accept-Encoding头以支持压缩响应 @@ -677,4 +677,13 @@ public class JsHttpClient { return buffer.length(); } } + + /** + * 关闭 WebClient 释放连接池资源 + */ + public void close() { + if (client != null) { + client.close(); + } + } } diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java index 6b81116..69310b4 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java @@ -29,12 +29,13 @@ import java.util.stream.Collectors; * @author QAIU * Create at 2025/10/17 */ -public class JsParserExecutor implements IPanTool { - +public class JsParserExecutor implements IPanTool, AutoCloseable { + private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class); - - private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32); - + + private static WorkerExecutor EXECUTOR; + private static final Object EXECUTOR_LOCK = new Object(); + private static String FETCH_RUNTIME_JS = null; private final CustomParserConfig config; @@ -146,12 +147,57 @@ public class JsParserExecutor implements IPanTool { } } + /** + * 释放资源(ScriptEngine 和 HttpClient),避免内存泄漏 + */ + @Override + public void close() { + if (httpClient != null) { + httpClient.close(); + } + // 清除 ScriptEngine 持有的 Java 对象引用,帮助 GC 回收 + if (engine != null) { + engine.put("http", null); + engine.put("logger", null); + engine.put("shareLinkInfo", null); + engine.put("JavaFetch", null); + } + } + + /** + * 关闭全局 WorkerExecutor(应在应用关闭时调用) + */ + public static void shutdownExecutor() { + synchronized (EXECUTOR_LOCK) { + if (EXECUTOR != null) { + EXECUTOR.close(); + EXECUTOR = null; + log.info("JsParserExecutor WorkerExecutor 已关闭"); + } + } + } + + /** + * 获取或创建 WorkerExecutor(懒加载) + */ + private static WorkerExecutor getExecutor() { + if (EXECUTOR != null) { + return EXECUTOR; + } + synchronized (EXECUTOR_LOCK) { + if (EXECUTOR == null) { + EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32); + } + return EXECUTOR; + } + } + @Override public Future parse() { jsLogger.info("开始执行JavaScript解析器: {}", config.getType()); // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程 - return EXECUTOR.executeBlocking(() -> { + return getExecutor().executeBlocking(() -> { // 直接调用全局parse函数 Object parseFunction = engine.get("parse"); if (parseFunction == null) { @@ -173,7 +219,7 @@ public class JsParserExecutor implements IPanTool { } else { throw new RuntimeException("parse函数类型错误"); } - }); + }).onComplete(ar -> close()); } @Override @@ -181,7 +227,7 @@ public class JsParserExecutor implements IPanTool { jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType()); // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程 - return EXECUTOR.executeBlocking(() -> { + return getExecutor().executeBlocking(() -> { // 直接调用全局parseFileList函数 Object parseFileListFunction = engine.get("parseFileList"); if (parseFileListFunction == null) { @@ -206,7 +252,7 @@ public class JsParserExecutor implements IPanTool { } else { throw new RuntimeException("parseFileList函数类型错误"); } - }); + }).onComplete(ar -> close()); } @Override @@ -214,7 +260,7 @@ public class JsParserExecutor implements IPanTool { jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType()); // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程 - return EXECUTOR.executeBlocking(() -> { + return getExecutor().executeBlocking(() -> { // 直接调用全局parseById函数 Object parseByIdFunction = engine.get("parseById"); if (parseByIdFunction == null) { @@ -237,7 +283,7 @@ public class JsParserExecutor implements IPanTool { } else { throw new RuntimeException("parseById函数类型错误"); } - }); + }).onComplete(ar -> close()); } /** diff --git a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java index 62f8a9c..453a294 100644 --- a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java +++ b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java @@ -21,11 +21,11 @@ import static cn.qaiu.util.AESUtils.encrypt; */ public class JsExecUtils { private static final Invocable inv; + private static final ScriptEngineManager ENGINE_MANAGER = new ScriptEngineManager(); // 初始化脚本引擎 static { - ScriptEngineManager engineManager = new ScriptEngineManager(); - ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎 + ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎 try { engine.eval(JsContent.ye123); @@ -45,12 +45,11 @@ public class JsExecUtils { } /** - * 调用执行蓝奏云js文件 + * 调用执行蓝奏云js文件(每次动态JS代码,无法复用引擎) */ public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException, NoSuchMethodException { - ScriptEngineManager engineManager = new ScriptEngineManager(); - ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎 + ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎 engine.eval(JsContent.lz + "\n" + jsText); Invocable inv = (Invocable) engine; //调用js中的函数 @@ -63,12 +62,11 @@ public class JsExecUtils { /** - * 调用执行蓝奏云js文件 + * 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例) */ public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException, NoSuchMethodException { - ScriptEngineManager engineManager = new ScriptEngineManager(); - ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎 + ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎 engine.eval(jsText); Invocable inv = (Invocable) engine; //调用js中的函数 diff --git a/parser/src/test/resources/fetch-runtime.js b/parser/src/test/resources/fetch-runtime.js new file mode 100644 index 0000000..6d46086 --- /dev/null +++ b/parser/src/test/resources/fetch-runtime.js @@ -0,0 +1,329 @@ +// ==FetchRuntime== +// @name Fetch API Polyfill for ES5 +// @description Fetch API and Promise implementation for ES5 JavaScript engines +// @version 1.0.0 +// @author QAIU +// ============== + +/** + * Simple Promise implementation compatible with ES5 + * Supports basic Promise functionality needed for fetch API + */ +function SimplePromise(executor) { + var state = 'pending'; + var value; + var handlers = []; + var self = this; + + function resolve(result) { + if (state !== 'pending') return; + state = 'fulfilled'; + value = result; + handlers.forEach(handle); + handlers = []; + } + + function reject(err) { + if (state !== 'pending') return; + state = 'rejected'; + value = err; + handlers.forEach(handle); + handlers = []; + } + + function handle(handler) { + if (state === 'pending') { + handlers.push(handler); + } else { + setTimeout(function() { + if (state === 'fulfilled' && typeof handler.onFulfilled === 'function') { + try { + var result = handler.onFulfilled(value); + if (result && typeof result.then === 'function') { + result.then(handler.resolve, handler.reject); + } else { + handler.resolve(result); + } + } catch (e) { + handler.reject(e); + } + } + if (state === 'rejected' && typeof handler.onRejected === 'function') { + try { + var result = handler.onRejected(value); + if (result && typeof result.then === 'function') { + result.then(handler.resolve, handler.reject); + } else { + handler.resolve(result); + } + } catch (e) { + handler.reject(e); + } + } else if (state === 'rejected' && !handler.onRejected) { + handler.reject(value); + } + }, 0); + } + } + + this.then = function(onFulfilled, onRejected) { + return new SimplePromise(function(resolveNext, rejectNext) { + handle({ + onFulfilled: onFulfilled, + onRejected: onRejected, + resolve: resolveNext, + reject: rejectNext + }); + }); + }; + + this['catch'] = function(onRejected) { + return this.then(null, onRejected); + }; + + this['finally'] = function(onFinally) { + return this.then( + function(value) { + return SimplePromise.resolve(onFinally()).then(function() { + return value; + }); + }, + function(reason) { + return SimplePromise.resolve(onFinally()).then(function() { + throw reason; + }); + } + ); + }; + + try { + executor(resolve, reject); + } catch (e) { + reject(e); + } +} + +// Static methods +SimplePromise.resolve = function(value) { + if (value && typeof value.then === 'function') { + return value; + } + return new SimplePromise(function(resolve) { + resolve(value); + }); +}; + +SimplePromise.reject = function(reason) { + return new SimplePromise(function(resolve, reject) { + reject(reason); + }); +}; + +SimplePromise.all = function(promises) { + return new SimplePromise(function(resolve, reject) { + var results = []; + var remaining = promises.length; + + if (remaining === 0) { + resolve(results); + return; + } + + function handleResult(index, value) { + results[index] = value; + remaining--; + if (remaining === 0) { + resolve(results); + } + } + + for (var i = 0; i < promises.length; i++) { + (function(index) { + var promise = promises[index]; + if (promise && typeof promise.then === 'function') { + promise.then( + function(value) { handleResult(index, value); }, + reject + ); + } else { + handleResult(index, promise); + } + })(i); + } + }); +}; + +SimplePromise.race = function(promises) { + return new SimplePromise(function(resolve, reject) { + if (promises.length === 0) { + // Per spec, Promise.race with empty array stays pending forever + return; + } + + for (var i = 0; i < promises.length; i++) { + var promise = promises[i]; + if (promise && typeof promise.then === 'function') { + promise.then(resolve, reject); + } else { + resolve(promise); + return; + } + } + }); +}; + +// Make Promise global if not already defined +if (typeof Promise === 'undefined') { + var Promise = SimplePromise; +} + +/** + * Response object that mimics the Fetch API Response + */ +function FetchResponse(jsHttpResponse) { + this._jsResponse = jsHttpResponse; + this.status = jsHttpResponse.statusCode(); + this.ok = this.status >= 200 && this.status < 300; + + // Map HTTP status codes to standard status text + var statusTexts = { + 200: 'OK', + 201: 'Created', + 204: 'No Content', + 301: 'Moved Permanently', + 302: 'Found', + 304: 'Not Modified', + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout' + }; + + this.statusText = statusTexts[this.status] || (this.ok ? 'OK' : 'Error'); + this.headers = { + get: function(name) { + return jsHttpResponse.header(name); + }, + has: function(name) { + return jsHttpResponse.header(name) !== null; + }, + entries: function() { + var headerMap = jsHttpResponse.headers(); + var entries = []; + for (var key in headerMap) { + if (headerMap.hasOwnProperty(key)) { + entries.push([key, headerMap[key]]); + } + } + return entries; + } + }; +} + +FetchResponse.prototype.text = function() { + var body = this._jsResponse.body(); + return SimplePromise.resolve(body || ''); +}; + +FetchResponse.prototype.json = function() { + var self = this; + return this.text().then(function(text) { + try { + return JSON.parse(text); + } catch (e) { + throw new Error('Invalid JSON: ' + e.message); + } + }); +}; + +FetchResponse.prototype.arrayBuffer = function() { + var bytes = this._jsResponse.bodyBytes(); + return SimplePromise.resolve(bytes); +}; + +FetchResponse.prototype.blob = function() { + // Blob not supported in ES5, return bytes + return this.arrayBuffer(); +}; + +/** + * Fetch API implementation using JavaFetch bridge + * @param {string} url - Request URL + * @param {Object} options - Fetch options (method, headers, body, etc.) + * @returns {Promise} + */ +function fetch(url, options) { + return new SimplePromise(function(resolve, reject) { + try { + // Parse options + options = options || {}; + var method = (options.method || 'GET').toUpperCase(); + var headers = options.headers || {}; + var body = options.body; + + // Prepare request options for JavaFetch + var requestOptions = { + method: method, + headers: {} + }; + + // Convert headers to simple object + if (headers) { + if (typeof headers.forEach === 'function') { + // Headers object + headers.forEach(function(value, key) { + requestOptions.headers[key] = value; + }); + } else if (typeof headers === 'object') { + // Plain object + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + requestOptions.headers[key] = headers[key]; + } + } + } + } + + // Add body if present + if (body !== undefined && body !== null) { + if (typeof body === 'string') { + requestOptions.body = body; + } else if (typeof body === 'object') { + // Assume JSON + requestOptions.body = JSON.stringify(body); + if (!requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) { + requestOptions.headers['Content-Type'] = 'application/json'; + } + } + } + + // Call JavaFetch bridge + var jsHttpResponse = JavaFetch.fetch(url, requestOptions); + + // Create Response object + var response = new FetchResponse(jsHttpResponse); + resolve(response); + + } catch (e) { + reject(e); + } + }); +} + +// Export for global use +if (typeof window !== 'undefined') { + window.fetch = fetch; + window.Promise = Promise; +} else if (typeof global !== 'undefined') { + global.fetch = fetch; + global.Promise = Promise; +} diff --git a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java index 90cf3a7..77f86f1 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/cache/CacheManager.java @@ -200,6 +200,51 @@ public class CacheManager { return promise.future(); } + /** + * 清理过期缓存记录,防止数据库无限增长 + * @return 删除的行数 + */ + public Future cleanupExpiredCache() { + String sql = "DELETE FROM cache_link_info WHERE expiration > 0 AND expiration < #{now}"; + Map params = new HashMap<>(); + params.put("now", System.currentTimeMillis()); + Promise promise = Promise.promise(); + SqlTemplate.forUpdate(jdbcPool, sql) + .execute(params) + .onSuccess(res -> { + int deleted = res.rowCount(); + if (deleted > 0) { + LOGGER.info("清理过期缓存记录 {} 条", deleted); + } + promise.complete(deleted); + }) + .onFailure(e -> { + LOGGER.error("清理过期缓存失败", e); + promise.fail(e); + }); + return promise.future(); + } + + /** + * 注册定时清理过期缓存任务(每小时执行一次) + * 应在应用启动后调用 + */ + public static void registerPeriodicCleanup() { + try { + io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance(); + vertx.setPeriodic(3600_000, 3600_000, id -> { + try { + new CacheManager().cleanupExpiredCache(); + } catch (Exception e) { + LOGGER.warn("定时清理缓存任务跳过(数据库可能未就绪)", e); + } + }); + LOGGER.info("缓存定时清理任务已注册(每小时执行)"); + } catch (Exception e) { + LOGGER.warn("注册缓存定时清理任务失败", e); + } + } + public Future> getShareKeyTotal(String shareKey) { String sql = """ SELECT `share_key`, SUM(cache_hit_total) AS hit_total, SUM(api_parser_total) AS parser_total diff --git a/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java b/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java index e595714..0330d6f 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/interceptorImpl/RateLimiter.java @@ -28,7 +28,7 @@ public class RateLimiter { MAX_REQUESTS, TIME_WINDOW, PATH_REG); } - synchronized public static Future checkRateLimit(HttpServerRequest request) { + public static Future checkRateLimit(HttpServerRequest request) { Promise promise = Promise.promise(); if (!request.path().matches(PATH_REG)) { // 如果请求路径不匹配正则,则不进行限流 @@ -38,7 +38,13 @@ public class RateLimiter { String ip = request.remoteAddress().host(); - ipRequestMap.compute(ip, (key, requestInfo) -> { + // 定期清理过期条目,防止 Map 无限增长 + if (ipRequestMap.size() > 1000) { + long now = System.currentTimeMillis(); + ipRequestMap.entrySet().removeIf(entry -> now - entry.getValue().timestamp > TIME_WINDOW); + } + + RequestInfo info = ipRequestMap.compute(ip, (key, requestInfo) -> { long currentTime = System.currentTimeMillis(); if (requestInfo == null || currentTime - requestInfo.timestamp > TIME_WINDOW) { // 初始化或重置计数器 @@ -50,7 +56,6 @@ public class RateLimiter { } }); - RequestInfo info = ipRequestMap.get(ip); if (info.count > MAX_REQUESTS) { // 超过限制 // 计算剩余时间 @@ -66,8 +71,8 @@ public class RateLimiter { } private static class RequestInfo { - int count; - long timestamp; + volatile int count; + volatile long timestamp; RequestInfo(int count, long time) { this.count = count; diff --git a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java index a908e18..977432c 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java @@ -29,6 +29,11 @@ public class CacheServiceImpl implements CacheService { private final CacheManager cacheManager = new CacheManager(); + static { + // 服务类加载时注册缓存定时清理任务 + CacheManager.registerPeriodicCleanup(); + } + private Future getAndSaveCachedShareLink(ParserCreate parserCreate) { // 认证、域名相关(检查是否已经添加过参数,避免重复调用)