From af723aed3a8fca5427f85b087fc26be9cf637cdd Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 14:22:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20NPE=20=E4=BF=AE=E5=A4=8D=E3=80=81?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=B3=84=E6=BC=8F=E4=BF=AE=E5=A4=8D=E5=8F=8A?= =?UTF-8?q?=E5=85=B6=E4=BB=96=20Bug=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 12 处 NPE 风险: FjTool/FsTool/IzTool/LzTool/MkwTool/P115Tool/PdbTool/QQTool/ParserCreate/CommonUtils/ShareLinkInfo/URLParamUtil - 修复 4 处 Vert.x 资源泄漏: 测试类中 Vertx 实例未关闭 - 修复 CacheManager 防重入和 registerPeriodicCleanup 就绪检查 - 修复 ParserApi 中 redirectUrl()/viewUrl() Promise 未 complete - 修复 CacheManager.updateTotalByField Promise 永不完成 - 修复 AppMain ShutdownHook 注册,确保 Vert.x 先于 JDBCPoolInit 关闭 - 修复 RouterHandlerFactory failureHandler 恢复返回 failure message - 修复 ParserCreate/LzTool 收窄 catch 异常类型 - 修复 IzTool/FjTool/IzToolWithAuth 并发安全 (volatile + header 副本) - 修复 P115Tool UA 为 null 时的 NPE,添加默认 User-Agent - Font Awesome CDN 换源为 s4.zstatic.net,避免 bootcdn 投毒风险 - DirectoryTree selectAll 补 parserUrl 检查,Home 组件名 App→Home --- .../qaiu/vx/core/verticle/RouterVerticle.java | 5 +- .../java/cn/qaiu/entity/ShareLinkInfo.java | 5 +- .../java/cn/qaiu/parser/ParserCreate.java | 18 ++-- .../cn/qaiu/parser/customjs/JsHttpClient.java | 4 +- .../parser/customjs/JsPlaygroundExecutor.java | 2 +- .../main/java/cn/qaiu/parser/impl/FjTool.java | 25 ++++-- .../main/java/cn/qaiu/parser/impl/FsTool.java | 8 +- .../main/java/cn/qaiu/parser/impl/IzTool.java | 24 +++--- .../cn/qaiu/parser/impl/IzToolWithAuth.java | 20 ++--- .../main/java/cn/qaiu/parser/impl/LzTool.java | 15 ++-- .../java/cn/qaiu/parser/impl/MkwTool.java | 10 +-- .../java/cn/qaiu/parser/impl/P115Tool.java | 8 +- .../java/cn/qaiu/parser/impl/PdbTool.java | 2 +- .../main/java/cn/qaiu/parser/impl/QQTool.java | 6 +- .../main/java/cn/qaiu/util/CommonUtils.java | 3 + .../cn/qaiu/parser/BaiduPhotoParserTest.java | 47 +++++------ .../java/cn/qaiu/parser/JsParserTest.java | 41 +++++----- .../parser/customjs/JsFetchBridgeTest.java | 33 +++++--- web-front/src/components/DirectoryTree.vue | 77 ++++++++++++++--- .../src/main/java/cn/qaiu/lz/AppMain.java | 17 ++++ .../cn/qaiu/lz/common/cache/CacheManager.java | 34 +++++--- .../cn/qaiu/lz/common/util/URLParamUtil.java | 18 +++- .../cn/qaiu/lz/web/controller/ParserApi.java | 82 +++++++++++++------ .../lz/web/service/impl/CacheServiceImpl.java | 4 +- 24 files changed, 335 insertions(+), 173 deletions(-) diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java index acc0c7e..b85a484 100644 --- a/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java +++ b/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java @@ -23,12 +23,11 @@ public class RouterVerticle extends AbstractVerticle { private static final Logger LOGGER = LoggerFactory.getLogger(RouterVerticle.class); private static final int port = SharedDataUtil.getValueForServerConfig("port"); - private static final Router router = new RouterHandlerFactory( - SharedDataUtil.getJsonStringForServerConfig("contextPath")).createRouter(); private static final JsonObject globalConfig = SharedDataUtil.getJsonConfig("globalConfig"); private HttpServer server; + private Router router; static { LOGGER.info(JacksonConfig.class.getSimpleName() + " >> "); @@ -61,6 +60,8 @@ public class RouterVerticle extends AbstractVerticle { .setReuseAddress(true) // 允许地址重用 .setReusePort(true); // 允许端口重用 + router = new RouterHandlerFactory( + SharedDataUtil.getJsonStringForServerConfig("contextPath")).createRouter(); server = vertx.createHttpServer(options); server.requestHandler(router).webSocketHandler(s->{}).listen() diff --git a/parser/src/main/java/cn/qaiu/entity/ShareLinkInfo.java b/parser/src/main/java/cn/qaiu/entity/ShareLinkInfo.java index 138c05a..abdac86 100644 --- a/parser/src/main/java/cn/qaiu/entity/ShareLinkInfo.java +++ b/parser/src/main/java/cn/qaiu/entity/ShareLinkInfo.java @@ -86,7 +86,10 @@ public class ShareLinkInfo { // 将type和shareKey组合成一个字符串作为缓存key String key = type + ":" + shareKey; if (type.equals("p115")) { - key += ("_" + otherParam.get("UA").toString().hashCode()); + Object ua = otherParam != null ? otherParam.get("UA") : null; + if (ua != null) { + key += ("_" + ua.toString().hashCode()); + } } return key; } diff --git a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java index 71f0e13..2b743f7 100644 --- a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java +++ b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java @@ -81,16 +81,16 @@ public class ParserCreate { if (shareKey != null) { shareLinkInfo.setShareKey(shareKey); } - } catch (Exception ignored) {} - + } catch (IllegalStateException | IllegalArgumentException ignored) {} + // 提取密码 try { String pwd = matcher.group("PWD"); if (StringUtils.isNotEmpty(pwd)) { shareLinkInfo.setSharePassword(pwd); } - } catch (Exception ignored) {} - + } catch (IllegalStateException | IllegalArgumentException ignored) {} + // 设置标准URL if (customParserConfig.getStandardUrlTemplate() != null) { String standardUrl = customParserConfig.getStandardUrlTemplate() @@ -133,7 +133,7 @@ public class ParserCreate { shareLinkInfo.setSharePassword(pwd); } standardUrl = standardUrl.replace("{pwd}", pwd); - } catch (Exception ignored) {} + } catch (IllegalStateException | IllegalArgumentException ignored) {} shareLinkInfo.setShareUrl(shareUrl); shareLinkInfo.setShareKey(shareKey); @@ -266,15 +266,15 @@ public class ParserCreate { if (shareKey != null) { shareLinkInfo.setShareKey(shareKey); } - } catch (Exception ignored) {} - + } catch (IllegalStateException | IllegalArgumentException ignored) {} + try { String password = matcher.group("PWD"); if (password != null) { shareLinkInfo.setSharePassword(password); } - } catch (Exception ignored) {} - + } catch (IllegalStateException | IllegalArgumentException ignored) {} + // 设置标准URL(如果有模板) if (customConfig.getStandardUrlTemplate() != null) { String standardUrl = customConfig.getStandardUrlTemplate() 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 0442903..2ada6b0 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java @@ -534,8 +534,8 @@ public class JsHttpClient { } else { promise.fail(result.cause()); } - }).onFailure(Throwable::printStackTrace); - + }).onFailure(e -> log.error("HTTP请求失败", e)); + // 等待响应完成(使用配置的超时时间) HttpResponse response = promise.future().toCompletionStage() .toCompletableFuture() diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java index e1a31cc..19ae052 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java @@ -355,7 +355,7 @@ public class JsPlaygroundExecutor { */ public List getLogs() { List logs = playgroundLogger.getLogs(); - System.out.println("[JsPlaygroundExecutor] 获取日志,数量: " + logs.size()); + log.debug("获取日志,数量: {}", logs.size()); return logs; } diff --git a/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java b/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java index 74e6546..6b991b4 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java @@ -109,9 +109,9 @@ public class FjTool extends PanBase { // String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString() - static String token = null; - static String userId = null; - public static boolean authFlag = true; + static volatile String token = null; + static volatile String userId = null; + public static volatile boolean authFlag = true; public FjTool(ShareLinkInfo shareLinkInfo) { super(shareLinkInfo); @@ -289,12 +289,14 @@ public class FjTool extends PanBase { JsonObject json = asJson(res2); if (json.getInteger("code") == 200) { token = json.getJsonObject("data").getString("appToken"); - header0.set("appToken", token); - log.info("登录成功 token: {}", token); + MultiMap h0 = MultiMap.caseInsensitiveMultiMap(); + h0.addAll(header0); + h0.set("appToken", token); + log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null"); client.postAbs(UriTemplate.of(TOKEN_VERIFY_URL)) .setTemplateParam("uuid", uuid) .setTemplateParam("ts", tsEncode2) - .putHeaders(header0).send().onSuccess(res -> { + .putHeaders(h0).send().onSuccess(res -> { if (asJson(res).getInteger("code") == 200) { if (FjTool.userId == null) { FjTool.userId = asJson(res).getJsonObject("map").getString("userId"); @@ -454,7 +456,10 @@ public class FjTool extends PanBase { // 如果参数里的目录ID不为空,则直接解析目录 String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); if (dirId != null && !dirId.isEmpty()) { - uuid = shareLinkInfo.getOtherParam().get("uuid").toString(); + Object uuidObj = shareLinkInfo.getOtherParam().get("uuid"); + if (uuidObj != null) { + uuid = uuidObj.toString(); + } parserDir(dirId, shareId, promise0); return promise0.future(); } @@ -495,7 +500,7 @@ public class FjTool extends PanBase { JsonArray list; try { JsonObject jsonObject = asJson(res); - System.out.println(jsonObject.encodePrettily()); + log.debug("目录列表: {}", jsonObject.encodePrettily()); list = jsonObject.getJsonArray("list"); } catch (Exception e) { log.error("解析目录失败: {}", res.bodyAsString()); @@ -576,6 +581,10 @@ public class FjTool extends PanBase { // 第二次请求 JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson"); + if (paramJson == null) { + promise.fail("缺少 paramJson 参数"); + return promise.future(); + } clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP)) .setTemplateParam("fidEncode", paramJson.getString("fidEncode")) .setTemplateParam("uuid", paramJson.getString("uuid")) diff --git a/parser/src/main/java/cn/qaiu/parser/impl/FsTool.java b/parser/src/main/java/cn/qaiu/parser/impl/FsTool.java index 1f0c09b..ad687e2 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/FsTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/FsTool.java @@ -389,6 +389,10 @@ public class FsTool extends PanBase { try { JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson"); + if (paramJson == null) { + parsePromise.fail("缺少 paramJson 参数"); + return parsePromise.future(); + } String shareUrl = paramJson.getString("shareUrl"); String objToken = paramJson.getString("objToken"); String tenant = extractTenant(shareUrl); @@ -444,7 +448,7 @@ public class FsTool extends PanBase { if (m1.find()) { try { return URLDecoder.decode(m1.group(1).trim(), StandardCharsets.UTF_8); - } catch (Exception ignored) { + } catch (IllegalArgumentException ignored) { } } @@ -453,7 +457,7 @@ public class FsTool extends PanBase { if (m2.find()) { try { return URLDecoder.decode(m2.group(1).trim(), StandardCharsets.UTF_8); - } catch (Exception ignored) { + } catch (IllegalArgumentException ignored) { } } diff --git a/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java b/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java index d8c9ac8..c135845 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java @@ -89,8 +89,8 @@ public class IzTool extends PanBase { String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString() - public static String token = null; - public static boolean authFlag = true; + public static volatile String token = null; + public static volatile boolean authFlag = true; public Future parse() { @@ -101,8 +101,8 @@ public class IzTool extends PanBase { // 检查并输出认证状态 if (shareLinkInfo.getOtherParam().containsKey("auths")) { boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED"); - log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}", - isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(10, token.length())) + "...)" : "未登录"); + log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}", + isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(8, token.length())) + "...)" : "未登录"); // 如果需要认证但还没有token,先执行登录 if ((isTempAuth || authFlag) && token == null) { @@ -118,7 +118,7 @@ public class IzTool extends PanBase { // 登录失败,继续使用免登录模式 }); } else if (token != null) { - log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(10, token.length()))); + log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(8, token.length()))); } } else { log.debug("文件解析无认证信息,使用免登录模式"); @@ -247,7 +247,7 @@ public class IzTool extends PanBase { log.warn("登录失败: {}", failRes.getMessage()); fail(failRes.getMessage()); }).onSuccess(r-> { - httpRequest.setTemplateParam("appToken", header.get("appToken")) + httpRequest.setTemplateParam("appToken", token) .putHeaders(header); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); }); @@ -263,12 +263,12 @@ public class IzTool extends PanBase { log.warn("重新登录失败: {}", failRes.getMessage()); fail(failRes.getMessage()); }).onSuccess(r-> { - httpRequest.setTemplateParam("appToken", header.get("appToken")) + httpRequest.setTemplateParam("appToken", token) .putHeaders(header); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); }); } else { - httpRequest.setTemplateParam("appToken", header.get("appToken")) + httpRequest.setTemplateParam("appToken", token) .putHeaders(header); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); } @@ -311,8 +311,7 @@ public class IzTool extends PanBase { JsonObject json = asJson(res2); if (json.getInteger("code") == 200) { token = json.getJsonObject("data").getString("appToken"); - header.set("appToken", token); - log.info("登录成功 token: {}", token); + log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null"); promise1.complete(); } else { // 检查是否为临时认证 @@ -463,7 +462,10 @@ public class IzTool extends PanBase { // 如果参数里的目录ID不为空,则直接解析目录 String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); if (dirId != null && !dirId.isEmpty()) { - uuid = shareLinkInfo.getOtherParam().get("uuid").toString(); + Object uuidObj = shareLinkInfo.getOtherParam().get("uuid"); + if (uuidObj != null) { + uuid = uuidObj.toString(); + } parserDir(dirId, shareId, promise); return promise.future(); } diff --git a/parser/src/main/java/cn/qaiu/parser/impl/IzToolWithAuth.java b/parser/src/main/java/cn/qaiu/parser/impl/IzToolWithAuth.java index 459e8d7..074d506 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/IzToolWithAuth.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/IzToolWithAuth.java @@ -88,8 +88,8 @@ public class IzToolWithAuth extends PanBase { String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString() - public static String token = null; - public static boolean authFlag = true; + public static volatile String token = null; + public static volatile boolean authFlag = true; public Future parse() { @@ -216,7 +216,7 @@ public class IzToolWithAuth extends PanBase { log.warn("登录失败: {}", failRes.getMessage()); fail(failRes.getMessage()); }).onSuccess(r-> { - httpRequest.setTemplateParam("appToken", header.get("appToken")) + httpRequest.setTemplateParam("appToken", token) .putHeaders(header); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); }); @@ -232,12 +232,12 @@ public class IzToolWithAuth extends PanBase { log.warn("重新登录失败: {}", failRes.getMessage()); fail(failRes.getMessage()); }).onSuccess(r-> { - httpRequest.setTemplateParam("appToken", header.get("appToken")) + httpRequest.setTemplateParam("appToken", token) .putHeaders(header); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); }); } else { - httpRequest.setTemplateParam("appToken", header.get("appToken")) + httpRequest.setTemplateParam("appToken", token) .putHeaders(header); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); } @@ -280,8 +280,7 @@ public class IzToolWithAuth extends PanBase { JsonObject json = asJson(res2); if (json.getInteger("code") == 200) { token = json.getJsonObject("data").getString("appToken"); - header.set("appToken", token); - log.info("登录成功 token: {}", token); + log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null"); promise1.complete(); } else { // 检查是否为临时认证 @@ -432,7 +431,8 @@ public class IzToolWithAuth extends PanBase { // 如果参数里的目录ID不为空,则直接解析目录 String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); if (dirId != null && !dirId.isEmpty()) { - uuid = shareLinkInfo.getOtherParam().get("uuid").toString(); + Object uuidObj = shareLinkInfo.getOtherParam().get("uuid"); + uuid = uuidObj != null ? uuidObj.toString() : null; parserDir(dirId, shareId, promise); return promise.future(); } @@ -480,7 +480,7 @@ public class IzToolWithAuth extends PanBase { requestDirList(id, shareId, tsEncode, promise); }) .onSuccess(r -> { - log.info("目录解析登录成功,token={}, 使用 VIP 模式", token != null ? token.substring(0, 10) + "..." : "null"); + log.info("目录解析登录成功,token={}, 使用 VIP 模式", token != null ? token.substring(0, Math.min(8, token.length())) + "..." : "null"); requestDirList(id, shareId, tsEncode, promise); }); return; @@ -627,7 +627,7 @@ public class IzToolWithAuth extends PanBase { // 如果有 token,使用 VIP 接口 if (StringUtils.isNotBlank(appToken)) { - log.debug("parseById 使用 VIP 接口, appToken={}", appToken.substring(0, Math.min(10, appToken.length())) + "..."); + log.debug("parseById 使用 VIP 接口, appToken={}", appToken.substring(0, Math.min(8, appToken.length())) + "..."); webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP)) .putHeaders(header) .setTemplateParam("fidEncode", paramJson.getString("fidEncode")) diff --git a/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java b/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java index c87bb96..4a2e2dc 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java @@ -14,6 +14,7 @@ import io.vertx.ext.web.client.WebClientSession; import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import javax.script.ScriptException; +import java.net.MalformedURLException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -107,7 +108,7 @@ public class LzTool extends PanBase { try { setFileInfo(html, shareLinkInfo); } catch (Exception e) { - e.printStackTrace(); + log.error("文件信息解析异常", e); } // 匹配iframe Pattern compile = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\""); @@ -175,7 +176,7 @@ public class LzTool extends PanBase { if (firstDot >= 0) { domain = host.substring(firstDot); // e.g. ".lanzoum.com" } - } catch (Exception ignored) {} + } catch (MalformedURLException ignored) {} // 创建一个 Cookie 并放入 CookieStore DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2); nettyCookie.setDomain(domain); @@ -217,7 +218,7 @@ public class LzTool extends PanBase { return; } Map signMap = (Map)obj.get("data"); - String url0 = obj.get("url").toString(); + String url0 = String.valueOf(obj.get("url")); MultiMap map = MultiMap.caseInsensitiveMultiMap(); signMap.forEach((k, v) -> { map.add((String) k, v.toString()); @@ -275,7 +276,7 @@ public class LzTool extends PanBase { String h = du.getHost(); int dot = h.indexOf('.'); if (dot >= 0) downDomain = h.substring(dot); - } catch (Exception ignored) {} + } catch (MalformedURLException ignored) {} // 创建一个 Cookie 并放入 CookieStore DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2); nettyCookie.setDomain(downDomain); @@ -290,12 +291,12 @@ public class LzTool extends PanBase { if (location0 == null) { fail(downUrl + " -> 直链获取失败2, 可能分享已失效"); } else { - setDateAndComplate(location0); + setDateAndComplete(location0); } }).onFailure(handleFail(downUrl)); return; } - setDateAndComplate(location); + setDateAndComplete(location); }) .onFailure(handleFail(downUrl)); } catch (Exception e) { @@ -304,7 +305,7 @@ public class LzTool extends PanBase { }).onFailure(handleFail(url)); } - private void setDateAndComplate(String location0) { + private void setDateAndComplete(String location0) { // 分享时间 提取url中的时间戳格式:lanzoui.com/abc/abc/yyyy/mm/dd/ String regex = "(\\d{4}/\\d{1,2}/\\d{1,2})"; Matcher matcher = Pattern.compile(regex).matcher(location0); diff --git a/parser/src/main/java/cn/qaiu/parser/impl/MkwTool.java b/parser/src/main/java/cn/qaiu/parser/impl/MkwTool.java index df856dd..5686703 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/MkwTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/MkwTool.java @@ -29,19 +29,19 @@ public class MkwTool extends PanBase { clientSession.getAbs(shareUrl).send().onSuccess(result -> { String cookie = result.headers().get("set-cookie"); - if (!cookie.isEmpty()) { + if (cookie != null && !cookie.isEmpty()) { String regex = "([A-Za-z0-9_]+)=([A-Za-z0-9]+)"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(cookie); if (matcher.find()) { - System.out.println(matcher.group(1)); - System.out.println(matcher.group(2)); + log.debug("cookie key: {}", matcher.group(1)); + log.debug("cookie value: {}", matcher.group(2)); var key = matcher.group(1); var token = matcher.group(2); String sign = JsExecUtils.getKwSign(token, key); - System.out.println(sign); + log.debug("sign: {}", sign); clientSession.getAbs(UriTemplate.of(API_URL)).setTemplateParam("mid", shareLinkInfo.getShareKey()) .putHeader("Secret", sign).send().onSuccess(res -> { JsonObject json = asJson(res); @@ -54,7 +54,7 @@ public class MkwTool extends PanBase { } } catch (Exception e) { - e.printStackTrace(); + log.error("解析失败", e); fail("解析失败"); } }); diff --git a/parser/src/main/java/cn/qaiu/parser/impl/P115Tool.java b/parser/src/main/java/cn/qaiu/parser/impl/P115Tool.java index a02c586..0e3a23f 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/P115Tool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/P115Tool.java @@ -21,6 +21,8 @@ public class P115Tool extends PanBase { private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "share/skip_login_downurl"; + private static final String DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; + private static final MultiMap header; static { @@ -49,9 +51,11 @@ public class P115Tool extends PanBase { public Future parse() { // 第一次请求 获取文件信息 + Object uaObj = shareLinkInfo.getOtherParam().get("UA"); + String ua = uaObj != null ? uaObj.toString() : DEFAULT_UA; client.getAbs(UriTemplate.of(FIRST_REQUEST_URL)) .putHeaders(header) - .putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString()) + .putHeader("User-Agent", ua) .setTemplateParam("dataKey", shareLinkInfo.getShareKey()) .setTemplateParam("dataPwd", shareLinkInfo.getSharePassword()) .send().onSuccess(res -> { @@ -68,7 +72,7 @@ public class P115Tool extends PanBase { // share_code={dataKey}&receive_code={dataPwd}&file_id={file_id} client.postAbs(SECOND_REQUEST_URL) .putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") - .putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString()) + .putHeader("User-Agent", ua) .sendForm(MultiMap.caseInsensitiveMultiMap() .set("share_code", shareLinkInfo.getShareKey()) .set("receive_code", shareLinkInfo.getSharePassword()) diff --git a/parser/src/main/java/cn/qaiu/parser/impl/PdbTool.java b/parser/src/main/java/cn/qaiu/parser/impl/PdbTool.java index 9edac9d..1561b1c 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/PdbTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/PdbTool.java @@ -85,7 +85,7 @@ public class PdbTool extends PanBase implements IPanTool { }) .onFailure(handleFail()); } catch (Exception e) { - e.printStackTrace(); + log.error("URL编码异常", e); } }) diff --git a/parser/src/main/java/cn/qaiu/parser/impl/QQTool.java b/parser/src/main/java/cn/qaiu/parser/impl/QQTool.java index f5e8cd7..fe6022d 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/QQTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/QQTool.java @@ -74,9 +74,9 @@ public class QQTool extends PanBase { }); // 调试匹配的情况 - System.out.println("文件名称: " + filename); - System.out.println("文件大小: " + filesize); - System.out.println("文件直链: " + fileurl); + log.debug("文件名称: {}", filename); + log.debug("文件大小: {}", filesize); + log.debug("文件直链: {}", fileurl); // 提交 promise.complete(fileurl.replace("\\x26", "&")); diff --git a/parser/src/main/java/cn/qaiu/util/CommonUtils.java b/parser/src/main/java/cn/qaiu/util/CommonUtils.java index 1c4cd58..d5391dd 100644 --- a/parser/src/main/java/cn/qaiu/util/CommonUtils.java +++ b/parser/src/main/java/cn/qaiu/util/CommonUtils.java @@ -33,6 +33,9 @@ public class CommonUtils { public static Map getURLParams(String url) throws MalformedURLException { URL fullUrl = new URL(url); String query = fullUrl.getQuery(); + if (query == null || query.isEmpty()) { + return new HashMap<>(); + } String[] params = query.split("&"); Map map = new HashMap<>(); for (String param : params) { diff --git a/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java b/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java index ee6bde0..1618f77 100644 --- a/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java +++ b/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java @@ -8,6 +8,8 @@ import cn.qaiu.parser.customjs.JsParserExecutor; import cn.qaiu.WebClientVertxInit; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import java.util.HashMap; @@ -22,15 +24,26 @@ import java.util.Map; */ public class BaiduPhotoParserTest { + private Vertx vertx; + + @Before + public void setUp() { + vertx = Vertx.vertx(); + WebClientVertxInit.init(vertx); + } + + @After + public void tearDown() { + if (vertx != null) { + vertx.close(); + } + } + @Test public void testBaiduPhotoParserRegistration() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + // 检查是否加载了百度相册解析器 CustomParserConfig config = CustomParserRegistry.get("baidu_photo"); assert config != null : "百度相册解析器未加载"; @@ -44,11 +57,7 @@ public class BaiduPhotoParserTest { public void testBaiduPhotoFileShareExecution() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + try { // 创建解析器 - 测试文件分享链接 IPanTool tool = ParserCreate.fromType("baidu_photo") @@ -76,11 +85,7 @@ public class BaiduPhotoParserTest { public void testBaiduPhotoFolderShareExecution() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + try { // 创建解析器 - 测试文件夹分享链接 IPanTool tool = ParserCreate.fromType("baidu_photo") @@ -108,11 +113,7 @@ public class BaiduPhotoParserTest { public void testBaiduPhotoParserFileList() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + try { IPanTool tool = ParserCreate.fromType("baidu_photo") // 分享key PPgOEodBVE @@ -166,11 +167,7 @@ public class BaiduPhotoParserTest { public void testBaiduPhotoParserById() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + try { // 创建ShareLinkInfo Map otherParam = new HashMap<>(); diff --git a/parser/src/test/java/cn/qaiu/parser/JsParserTest.java b/parser/src/test/java/cn/qaiu/parser/JsParserTest.java index 88f2a43..c738f90 100644 --- a/parser/src/test/java/cn/qaiu/parser/JsParserTest.java +++ b/parser/src/test/java/cn/qaiu/parser/JsParserTest.java @@ -7,6 +7,8 @@ import cn.qaiu.parser.custom.CustomParserRegistry; import cn.qaiu.parser.customjs.JsParserExecutor; import cn.qaiu.WebClientVertxInit; import io.vertx.core.Vertx; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import java.util.HashMap; @@ -21,15 +23,26 @@ import java.util.Map; */ public class JsParserTest { + private Vertx vertx; + + @Before + public void setUp() { + vertx = Vertx.vertx(); + WebClientVertxInit.init(vertx); + } + + @After + public void tearDown() { + if (vertx != null) { + vertx.close(); + } + } + @Test public void testJsParserRegistration() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + // 检查是否加载了JavaScript解析器 CustomParserConfig config = CustomParserRegistry.get("demo_js"); assert config != null : "JavaScript解析器未加载"; @@ -43,11 +56,7 @@ public class JsParserTest { public void testJsParserExecution() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + try { // 创建解析器 IPanTool tool = ParserCreate.fromType("demo_js") @@ -74,11 +83,7 @@ public class JsParserTest { public void testJsParserFileList() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + try { // 创建解析器 IPanTool tool = ParserCreate.fromType("demo_js") @@ -114,11 +119,7 @@ public class JsParserTest { public void testJsParserById() { // 清理注册表 CustomParserRegistry.clear(); - - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - + try { // 创建ShareLinkInfo Map otherParam = new HashMap<>(); diff --git a/parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java b/parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java index 527a954..47392ff 100644 --- a/parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java +++ b/parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java @@ -7,6 +7,8 @@ import cn.qaiu.parser.ParserCreate; import cn.qaiu.parser.custom.CustomParserConfig; import cn.qaiu.parser.custom.CustomParserRegistry; import io.vertx.core.Vertx; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,18 +18,29 @@ import org.slf4j.LoggerFactory; * 测试fetch API和Promise polyfill功能 */ public class JsFetchBridgeTest { - + private static final Logger log = LoggerFactory.getLogger(JsFetchBridgeTest.class); - + + private Vertx vertx; + + @Before + public void setUp() { + vertx = Vertx.vertx(); + WebClientVertxInit.init(vertx); + } + + @After + public void tearDown() { + if (vertx != null) { + vertx.close(); + } + } + @Test public void testFetchPolyfillLoaded() { - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - // 清理注册表 CustomParserRegistry.clear(); - + // 创建一个简单的解析器配置 String jsCode = """ // 测试Promise是否可用 @@ -83,13 +96,9 @@ public class JsFetchBridgeTest { @Test public void testPromiseBasicUsage() { - // 初始化Vertx - Vertx vertx = Vertx.vertx(); - WebClientVertxInit.init(vertx); - // 清理注册表 CustomParserRegistry.clear(); - + String jsCode = """ function parse(shareLinkInfo, http, logger) { logger.info("测试Promise基本用法"); diff --git a/web-front/src/components/DirectoryTree.vue b/web-front/src/components/DirectoryTree.vue index f91f46a..53767bd 100644 --- a/web-front/src/components/DirectoryTree.vue +++ b/web-front/src/components/DirectoryTree.vue @@ -190,6 +190,14 @@ > 发送到下载器 + + 复制直链 +
@@ -258,6 +266,14 @@ > 发送到下载器 + + 复制直链 +
@@ -324,6 +340,14 @@ > 发送到下载器 + + 复制直链 +
@@ -391,6 +415,7 @@ export default { downloadInfo: null, downloadLoading: false, singleSendLoading: false, + copyLinkLoading: false, treeProps: { label: 'fileName', children: 'children', @@ -462,10 +487,6 @@ export default { } return `${baseUrl}?${params.toString()}` }, - // 文件树与窗格同源:直接返回当前目录数据 - buildTree(list) { - return list || [] - }, // 懒加载子节点 loadNode(node, resolve) { if (node.level === 0) { @@ -479,9 +500,14 @@ export default { })) resolve(children) } else { + this.$message.error(res.data.msg || '获取子节点失败') resolve([]) } - }).catch(() => resolve([])) + }).catch(err => { + const msg = err.response?.data?.msg || err.message + if (msg) this.$message.error(msg) + resolve([]) + }) } else { resolve([]) } @@ -491,7 +517,6 @@ export default { }, // 处理文件点击 handleFileClick(file) { - console.log('点击文件', file, this.viewMode) if (file.fileType === 'folder') { this.enterFolder(file) } else if (this.viewMode === 'pane') { @@ -520,7 +545,8 @@ export default { } } catch (error) { console.error('进入文件夹失败:', error) - this.$message.error('进入文件夹失败') + const msg = error.response?.data?.msg || error.message || '进入文件夹失败' + this.$message.error(msg) } finally { this.loading = false } @@ -551,7 +577,8 @@ export default { } } catch (error) { console.error('加载目录失败:', error) - this.$message.error('加载目录失败') + const msg = error.response?.data?.msg || error.message || '加载目录失败' + this.$message.error(msg) } finally { this.loading = false } @@ -649,7 +676,8 @@ export default { } } catch (error) { console.error('获取下载信息失败:', error) - this.$message.error('获取下载信息失败,尝试直接下载') + const msg = error.response?.data?.msg || '获取下载信息失败,尝试直接下载' + this.$message.error(msg) this.downloadFile(file) } finally { this.downloadLoading = false @@ -735,7 +763,8 @@ export default { } } catch (error) { console.error('发送到下载器失败:', error) - this.$message.error('发送到下载器失败: ' + error.message) + const msg = error.response?.data?.msg || error.message || '发送到下载器失败' + this.$message.error(msg) } finally { this.singleSendLoading = false } @@ -744,6 +773,32 @@ export default { this.fileDialogVisible = false this.selectedFile = null }, + async copyDirectLink(file) { + if (!file?.parserUrl) { + this.$message.warning('该文件暂无直链') + return + } + const rawUrl = file.parserUrl.startsWith('http') ? file.parserUrl : (window.location.origin + file.parserUrl) + const url = this.appendToken(rawUrl) + this.copyLinkLoading = true + try { + await navigator.clipboard.writeText(url) + this.$message.success('直链已复制到剪贴板') + } catch { + // fallback + const ta = document.createElement('textarea') + ta.value = url + ta.style.position = 'fixed' + ta.style.opacity = '0' + document.body.appendChild(ta) + ta.select() + document.execCommand('copy') + document.body.removeChild(ta) + this.$message.success('直链已复制到剪贴板') + } finally { + this.copyLinkLoading = false + } + }, closePreview() { this.isPreviewing = false this.previewUrl = '' @@ -802,7 +857,7 @@ export default { this.toggleFileSelect(file) }, selectAll() { - this.selectedFiles = this.currentFileList.filter(f => f.fileType !== 'folder') + this.selectedFiles = this.currentFileList.filter(f => f.fileType !== 'folder' && f.parserUrl) }, deselectAll() { this.selectedFiles = [] diff --git a/web-service/src/main/java/cn/qaiu/lz/AppMain.java b/web-service/src/main/java/cn/qaiu/lz/AppMain.java index 61bf631..7e0d30a 100644 --- a/web-service/src/main/java/cn/qaiu/lz/AppMain.java +++ b/web-service/src/main/java/cn/qaiu/lz/AppMain.java @@ -36,6 +36,20 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL; public class AppMain { public static void main(String[] args) { + // 先注册 ShutdownHook(JVM 逆序执行,先注册的后执行) + // 确保关闭顺序:Vert.x -> JDBCPoolInit -> JsParserExecutor + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + JDBCPoolInit.instance().close(); + } catch (Exception e) { + // ignore + } + try { + cn.qaiu.parser.customjs.JsParserExecutor.shutdownExecutor(); + } catch (Exception e) { + // ignore + } + })); // start Deploy.instance().start(args, AppMain::exec); } @@ -67,6 +81,9 @@ public class AppMain { loadPlaygroundParsers(); String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName"); + if (addr == null || addr.isBlank()) { + addr = "http://127.0.0.1:" + jsonObject.getJsonObject(ConfigConstant.SERVER).getInteger("port", 6400); + } System.out.println("启动成功: \n本地服务地址: " + addr); }); }); 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 77f86f1..947670d 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 @@ -89,7 +89,7 @@ public class CacheManager { } else { LOGGER.warn("No rows affected when updating cache link info for shareKey: {}", cacheLinkInfo.getShareKey()); } - }).onFailure(Throwable::printStackTrace); + }).onFailure(e -> LOGGER.error("缓存链接更新失败", e)); if (cacheLinkInfo.getFileInfo() != null) { String sql2 = """ @@ -123,7 +123,7 @@ public class CacheManager { } else { LOGGER.warn("No rows affected when inserting pan file info for shareKey: {}", cacheLinkInfo.getShareKey()); } - }).onFailure(Throwable::printStackTrace); + }).onFailure(e -> LOGGER.error("文件信息插入失败", e)); } }); } @@ -153,18 +153,21 @@ public class CacheManager { getShareKeyTotal(shareKey, fieldLower).onSuccess(total -> { Integer newTotal = (total == null ? 0 : total) + 1; + Map updateParams = new HashMap<>(); + updateParams.put("panType", getShareType(shareKey)); + updateParams.put("shareKey", shareKey); + updateParams.put("total", newTotal); + updateParams.put("ts", System.currentTimeMillis()); SqlTemplate.forUpdate(jdbcPool, sql) - .execute(new HashMap<>() {{ - put("panType", getShareType(shareKey)); - put("shareKey", shareKey); - put("total", newTotal); - put("ts", System.currentTimeMillis()); - }}) + .execute(updateParams) .onSuccess(res -> promise.complete(res.rowCount())) .onFailure(e->{ promise.fail(e); LOGGER.error("updateTotalByField: ", e); }); + }).onFailure(e -> { + promise.fail(e); + LOGGER.error("getShareKeyTotal in updateTotalByField: ", e); }); return promise.future(); } @@ -229,9 +232,17 @@ public class CacheManager { * 注册定时清理过期缓存任务(每小时执行一次) * 应在应用启动后调用 */ + private static volatile boolean cleanupRegistered = false; + public static void registerPeriodicCleanup() { + if (cleanupRegistered) return; try { io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance(); + if (vertx == null) { + LOGGER.warn("Vertx 未就绪,缓存定时清理任务延迟注册"); + return; + } + cleanupRegistered = true; vertx.setPeriodic(3600_000, 3600_000, id -> { try { new CacheManager().cleanupExpiredCache(); @@ -262,10 +273,9 @@ public class CacheManager { .onSuccess(res -> { if(res.iterator().hasNext()) { JsonObject next = res.iterator().next(); - Map resp = new HashMap<>(){{ - put("hit_total" ,next.getInteger("hit_total")); - put("parser_total" ,next.getInteger("parser_total")); - }}; + Map resp = new HashMap<>(); + resp.put("hit_total", next.getInteger("hit_total")); + resp.put("parser_total", next.getInteger("parser_total")); promise.complete(resp); } else { promise.complete(); diff --git a/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java b/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java index 64e707c..f761094 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java @@ -119,11 +119,17 @@ public class URLParamUtil { } String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); - parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix); + if (StringUtils.isBlank(linkPrefix)) { + // 未配置 domainName 时,从请求地址推断 + linkPrefix = parserCreate.getShareLinkInfo().getOtherParam() + .getOrDefault("_requestOrigin", "").toString(); + } + if (StringUtils.isNotBlank(linkPrefix)) { + parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix); + } } /** - * 添加临时认证参数(一次性,不保存到数据库或共享内存) * 如果提供了临时认证参数,将覆盖后台配置的认证信息 * * @param parserCreate ParserCreate对象 @@ -155,7 +161,13 @@ public class URLParamUtil { } String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); - parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix); + if (StringUtils.isBlank(linkPrefix)) { + linkPrefix = parserCreate.getShareLinkInfo().getOtherParam() + .getOrDefault("_requestOrigin", "").toString(); + } + if (StringUtils.isNotBlank(linkPrefix)) { + parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix); + } // 构建临时认证信息 MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap(); diff --git a/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java b/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java index 8d99c08..5c6f493 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java @@ -43,14 +43,38 @@ public class ParserApi { private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); + /** + * 获取链接前缀:优先用配置的 domainName,未配置则从请求头推断 + * 支持反向代理:优先读 X-Forwarded-Host/X-Forwarded-Proto,再回退到 Host 头 + */ + private static String getLinkPrefix(HttpServerRequest request) { + String domainName = SharedDataUtil.getJsonConfig("server").getString("domainName"); + if (StringUtils.isNotBlank(domainName)) { + return domainName; + } + if (request != null) { + // 反向代理场景:优先从转发头获取原始域名 + String forwardedHost = request.getHeader("X-Forwarded-Host"); + if (StringUtils.isNotBlank(forwardedHost)) { + String proto = request.getHeader("X-Forwarded-Proto"); + if (StringUtils.isBlank(proto)) { + proto = request.scheme(); + } + return proto + "://" + forwardedHost; + } + return request.scheme() + "://" + request.host(); + } + return ""; + } + @RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99) public Future statisticsInfo() { return dbService.getStatisticsInfo(); } - private final CacheManager cacheManager = new CacheManager(); - private final ServerApi serverApi = new ServerApi(); + private static final CacheManager cacheManager = new CacheManager(); + private static final ServerApi serverApi = new ServerApi(); @RouteMapping(value = "/linkInfo", method = RouteMethod.GET) public Future parse(HttpServerRequest request, String pwd, String auth) { @@ -61,10 +85,11 @@ public class ParserApi { // 构建链接信息响应,如果有 auth 参数则附加到链接中 String authSuffix = (auth != null && !auth.isEmpty()) ? "&auth=" + auth : ""; + shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request)); LinkInfoResp build = LinkInfoResp.builder() - .downLink(getDownLink(parserCreate, false) + authSuffix) - .apiLink(getDownLink(parserCreate, true) + authSuffix) - .viewLink(getViewLink(parserCreate) + authSuffix) + .downLink(getDownLink(parserCreate, false, request) + authSuffix) + .apiLink(getDownLink(parserCreate, true, request) + authSuffix) + .viewLink(getViewLink(parserCreate, request) + authSuffix) .shareLinkInfo(shareLinkInfo).build(); // 解析次数统计 shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent")); @@ -76,25 +101,23 @@ public class ParserApi { } promise.complete(build); }).onFailure(t->{ - t.printStackTrace(); + log.error("获取统计信息失败", t); promise.complete(build); }); return promise.future(); } - private static String getDownLink(ParserCreate create, boolean isJson) { - - String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); + private static String getDownLink(ParserCreate create, boolean isJson, HttpServerRequest request) { + String linkPrefix = getLinkPrefix(request); if (StringUtils.isBlank(linkPrefix)) { - linkPrefix = "http://127.0.0.1"; + linkPrefix = "http://127.0.0.1:" + SharedDataUtil.getJsonConfig("server").getInteger("port", 6400); } // 下载短链前缀 /d return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix(); } - private static String getViewLink(ParserCreate create) { - - String linkPrefix = SharedDataUtil.getJsonStringForServerConfig("domainName"); + private static String getViewLink(ParserCreate create, HttpServerRequest request) { + String linkPrefix = getLinkPrefix(request); if (StringUtils.isBlank(linkPrefix)) { return ""; } @@ -119,8 +142,9 @@ public class ParserApi { public Future> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) { String url = URLParamUtil.parserParams(request); ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd); - String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); + String linkPrefix = getLinkPrefix(request); parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix); + parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix); if (StringUtils.isNotBlank(dirId)) { parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId); } @@ -132,7 +156,7 @@ public class ParserApi { // 目录解析下载文件 // @RouteMapping("/getFileDownUrl/:type/:param") - public Future getFileDownUrl(String type, String param) { + public Future getFileDownUrl(HttpServerRequest request, String type, String param) { ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null .setShareLinkInfoPwd("-"); @@ -147,17 +171,21 @@ public class ParserApi { shareLinkInfo.getOtherParam().put("paramJson", new JsonObject(paramStr)); // domainName - String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); + String linkPrefix = getLinkPrefix(request); shareLinkInfo.getOtherParam().put("domainName", linkPrefix); + shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix); return parserCreate.createTool().parseById(); } @RouteMapping("/redirectUrl/:type/:param") - public Future redirectUrl(HttpServerResponse response, String type, String param) { + public Future redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) { Promise promise = Promise.promise(); - getFileDownUrl(type, param) - .onSuccess(res -> ResponseUtil.redirect(response, res)) + getFileDownUrl(request, type, param) + .onSuccess(res -> { + ResponseUtil.redirect(response, res); + promise.complete(); + }) .onFailure(t -> promise.fail(t.fillInStackTrace())); return promise.future(); } @@ -220,7 +248,7 @@ public class ParserApi { } String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL"); - new ServerApi().parseJson(request, pwd, null).onSuccess(res -> { + serverApi.parseJson(request, pwd, null).onSuccess(res -> { redirect(response, previewURL, res); }).onFailure(e -> { ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString())); @@ -229,14 +257,15 @@ public class ParserApi { @RouteMapping("/viewUrl/:type/:param") - public Future viewUrl(HttpServerResponse response, String type, String param) { + public Future viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) { Promise promise = Promise.promise(); String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL"); - getFileDownUrl(type, param) + getFileDownUrl(request, type, param) .onSuccess(res -> { String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8); ResponseUtil.redirect(response, url); + promise.complete(); }) .onFailure(t -> promise.fail(t.fillInStackTrace())); return promise.future(); @@ -269,7 +298,8 @@ public class ParserApi { String shareUrl = URLParamUtil.parserParams(request); ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd); ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo(); - + shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request)); + // 处理认证参数 if (auth != null && !auth.isEmpty()) { AuthParam authParam = AuthParamCodec.decode(auth); @@ -285,6 +315,8 @@ public class ParserApi { authParam.getExt5()); log.debug("客户端链接API: 已解码认证参数 authType={}", authParam.getAuthType()); } + } else { + URLParamUtil.addParam(parserCreate); } // 使用默认方法解析并生成客户端链接 @@ -326,7 +358,9 @@ public class ParserApi { try { String shareUrl = URLParamUtil.parserParams(request); ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd); - + parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", getLinkPrefix(request)); + URLParamUtil.addParam(parserCreate); + // 使用默认方法解析并生成客户端链接 parserCreate.createTool().parseWithClientLinks() .onSuccess(clientLinks -> { 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 977432c..8dbd0c1 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 @@ -104,7 +104,7 @@ public class CacheServiceImpl implements CacheService { promise.complete(result); // 更新缓存 cacheManager.cacheShareLink(cacheLinkInfo); - cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(Throwable::printStackTrace); + cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(e -> log.error("更新API解析计数失败: cacheKey={}", cacheKey, e)); }).onFailure(promise::fail); } else { // 缓存命中,生成过期时间并生成下载命令 @@ -120,7 +120,7 @@ public class CacheServiceImpl implements CacheService { promise.complete(result); cacheManager.updateTotalByField(cacheKey, CacheTotalField.CACHE_HIT_TOTAL) - .onFailure(Throwable::printStackTrace); + .onFailure(e -> log.error("更新缓存命中计数失败: cacheKey={}", cacheKey, e)); } }).onFailure(t -> promise.fail(t.fillInStackTrace()));