Merge pull request #187 from yukaidi1220/feat/contributions

安全加固、新功能、Bug 修复与代码质量改进
This commit is contained in:
qaiu
2026-05-29 23:53:09 +08:00
committed by GitHub
79 changed files with 1042 additions and 648 deletions

1
.gitignore vendored
View File

@@ -31,6 +31,7 @@ target/
sdkTest.log sdkTest.log
app.yml app.yml
app-local.yml app-local.yml
secret.yml
#some local files #some local files

View File

@@ -10,8 +10,13 @@ COPY ./web-service/target/netdisk-fast-download-bin.zip .
RUN unzip netdisk-fast-download-bin.zip && \ RUN unzip netdisk-fast-download-bin.zip && \
mv netdisk-fast-download/* ./ && \ mv netdisk-fast-download/* ./ && \
rm netdisk-fast-download-bin.zip && \ rm netdisk-fast-download-bin.zip && \
chmod +x run.sh chmod +x run.sh && \
mkdir -p db logs
EXPOSE 6400 6401 COPY ./docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["sh", "run.sh"] EXPOSE 6401
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@@ -1,9 +1,9 @@
# 一款网盘分享链接云解析快速下载服务 # 一款网盘分享链接云解析快速下载服务
QQ交流群1017480890 QQ交流群1017480890
<p align="center"> <p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a> <a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=main&style=flat"></a>
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a> <a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.24-blue?style=flat"></a> <a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-blue?style=flat"></a>
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a> <a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a> <a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
<a href="https://atomgit.com/QAIU/netdisk-fast-download"><img src="https://atomgit.com/QAIU/netdisk-fast-download/star/badge.svg" alt="AtomGit"></a> <a href="https://atomgit.com/QAIU/netdisk-fast-download"><img src="https://atomgit.com/QAIU/netdisk-fast-download/star/badge.svg" alt="AtomGit"></a>
@@ -419,7 +419,7 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
> 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径 > 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径
```shell ```shell
cd ~ cd ~
wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/v0.1.9b7/netdisk-fast-download-bin.zip wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/v3.0.2/netdisk-fast-download-bin.zip
unzip netdisk-fast-download-bin.zip unzip netdisk-fast-download-bin.zip
cd netdisk-fast-download cd netdisk-fast-download
bash service-install.sh bash service-install.sh

View File

@@ -1,6 +1,5 @@
#!/bin/bash #!/bin/bash
# set -x # set -x
LAUNCH_JAR="netdisk-fast-download.jar" LAUNCH_JAR="netdisk-fast-download.jar"
nohup java -Xmx512M -jar "$LAUNCH_JAR" "$@" >startup.log 2>&1 & exec java -Xmx${JVM_XMX:-512M} ${JVM_OPTS} -jar "$LAUNCH_JAR" "$@"
tail -f startup.log

View File

@@ -65,7 +65,7 @@
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
<version>42.7.3</version> <version>42.7.11</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -53,7 +53,7 @@ public class CreateDatabase {
stmt.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); stmt.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
LOGGER.info(">>>>>>>>>>> 数据库'{}'创建成功 <<<<<<<<<<<<", dbName); LOGGER.info(">>>>>>>>>>> 数据库'{}'创建成功 <<<<<<<<<<<<", dbName);
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); LOGGER.error("创建数据库失败", e);
} }
} }

View File

@@ -24,35 +24,39 @@ import java.util.*;
* @author <a href="https://qaiu.top">QAIU</a> * @author <a href="https://qaiu.top">QAIU</a>
*/ */
public class CreateTable { public class CreateTable {
public static Map<Class<?>, String> javaProperty2SqlColumnMap = new HashMap<>() {{ public static final Map<Class<?>, String> javaProperty2SqlColumnMap;
static {
Map<Class<?>, String> map = new HashMap<>();
// Java类型到SQL类型的映射 // Java类型到SQL类型的映射
put(Integer.class, "INT"); map.put(Integer.class, "INT");
put(Short.class, "SMALLINT"); map.put(Short.class, "SMALLINT");
put(Byte.class, "TINYINT"); map.put(Byte.class, "TINYINT");
put(Long.class, "BIGINT"); map.put(Long.class, "BIGINT");
put(java.math.BigDecimal.class, "DECIMAL"); map.put(java.math.BigDecimal.class, "DECIMAL");
put(Double.class, "DOUBLE"); map.put(Double.class, "DOUBLE");
put(Float.class, "REAL"); map.put(Float.class, "REAL");
put(Boolean.class, "BOOLEAN"); map.put(Boolean.class, "BOOLEAN");
put(String.class, "VARCHAR"); map.put(String.class, "VARCHAR");
put(Date.class, "TIMESTAMP"); map.put(Date.class, "TIMESTAMP");
put(java.time.LocalDateTime.class, "TIMESTAMP"); map.put(java.time.LocalDateTime.class, "TIMESTAMP");
put(java.sql.Timestamp.class, "TIMESTAMP"); map.put(java.sql.Timestamp.class, "TIMESTAMP");
put(java.sql.Date.class, "DATE"); map.put(java.sql.Date.class, "DATE");
put(java.sql.Time.class, "TIME"); map.put(java.sql.Time.class, "TIME");
// 基本数据类型 // 基本数据类型
put(int.class, "INT"); map.put(int.class, "INT");
put(short.class, "SMALLINT"); map.put(short.class, "SMALLINT");
put(byte.class, "TINYINT"); map.put(byte.class, "TINYINT");
put(long.class, "BIGINT"); map.put(long.class, "BIGINT");
put(double.class, "DOUBLE"); map.put(double.class, "DOUBLE");
put(float.class, "REAL"); map.put(float.class, "REAL");
put(boolean.class, "BOOLEAN"); map.put(boolean.class, "BOOLEAN");
}};
javaProperty2SqlColumnMap = Collections.unmodifiableMap(map);
}
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class); private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class);
public static String UNIQUE_PREFIX = "idx_"; public static final String UNIQUE_PREFIX = "idx_";
private static Case getCase(Class<?> clz) { private static Case getCase(Class<?> clz) {
return switch (clz.getName()) { return switch (clz.getName()) {

View File

@@ -16,6 +16,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
@@ -62,7 +64,12 @@ public final class Deploy {
path.append("-").append(args[0].replace("app-","")); path.append("-").append(args[0].replace("app-",""));
} }
// 读取yml配置 // 读取yml配置,优先当前目录,其次 resources/ 子目录
String configFile = path + ".yml";
if (!Files.exists(Path.of(configFile)) && Files.exists(Path.of("resources", configFile))) {
path.insert(0, "resources/");
LOGGER.info("从 resources/ 目录加载配置: {}", path + ".yml");
}
ConfigUtil.readYamlConfig(path.toString(), tempVertx) ConfigUtil.readYamlConfig(path.toString(), tempVertx)
.onSuccess(this::readConf) .onSuccess(this::readConf)
.onFailure(err -> { .onFailure(err -> {

View File

@@ -127,8 +127,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 错误请求处理 // 错误请求处理
mainRouter.errorHandler(405, ctx -> doFireJsonResultResponse(ctx, JsonResult mainRouter.errorHandler(405, ctx -> doFireJsonResultResponse(ctx, JsonResult
.error("Method Not Allowed", 405))); .error("Method Not Allowed", 405)));
mainRouter.errorHandler(404, ctx -> ctx.response().setStatusCode(404).setChunked(true) mainRouter.errorHandler(404, ctx -> {
.end("Internal server error: 404 not found")); ctx.response().setStatusCode(404).end("404 not found");
});
return mainRouter; return mainRouter;
} }
@@ -179,8 +180,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
if (ctx.statusCode() == 503 || ctx.failure() == null) { if (ctx.statusCode() == 503 || ctx.failure() == null) {
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503); doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503);
} else { } else {
ctx.failure().printStackTrace(); LOGGER.error("路由处理失败", ctx.failure());
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage()), 500); String msg = ctx.failure() != null ? ctx.failure().getMessage() : "未知异常";
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
} }
}); });
} else if (method.isAnnotationPresent(SockRouteMapper.class)) { } else if (method.isAnnotationPresent(SockRouteMapper.class)) {
@@ -198,7 +200,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
try { try {
ReflectionUtil.invokeWithArguments(method, instance, sock); ReflectionUtil.invokeWithArguments(method, instance, sock);
} catch (Throwable e) { } catch (Throwable e) {
e.printStackTrace(); LOGGER.error("WebSocket处理异常", e);
} }
}); });
if (url.endsWith("*")) { if (url.endsWith("*")) {
@@ -322,7 +324,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
parameterValueList.put(k, entity); parameterValueList.put(k, entity);
} }
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
e.printStackTrace(); LOGGER.error("实体类绑定异常: {}", typeName, e);
} }
} }
}); });
@@ -365,7 +367,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
Object entity = ParamUtil.multiMapToEntity(queryParams, aClass); Object entity = ParamUtil.multiMapToEntity(queryParams, aClass);
parameterValueList.put(k, entity); parameterValueList.put(k, entity);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LOGGER.error("参数绑定异常: {}", v.getRight().getName(), e);
} }
} else if (parameterValueList.get(k) == null } else if (parameterValueList.get(k) == null
&& JsonObject.class.getName().equals(v.getRight().getName())) { && JsonObject.class.getName().equals(v.getRight().getName())) {
@@ -408,22 +410,19 @@ public class RouterHandlerFactory implements BaseHttpApi {
doFireJsonResultResponse(ctx, JsonResult.data(null)); doFireJsonResultResponse(ctx, JsonResult.data(null));
} }
}).onFailure(e -> doFireJsonResultResponse(ctx, JsonResult.error(e.getMessage()), 500)); }).onFailure(e -> {
LOGGER.error("请求处理失败", e);
String msg = e.getMessage() != null ? e.getMessage() : "服务器内部错误";
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
});
} else { } else {
doFireJsonResultResponse(ctx, JsonResult.data(data)); doFireJsonResultResponse(ctx, JsonResult.data(data));
} }
} }
} catch (Throwable e) { } catch (Throwable e) {
e.printStackTrace(); LOGGER.error("请求处理异常", e);
String err = e.getMessage(); String msg = e.getMessage() != null ? e.getMessage() : "服务器内部错误";
if (e.getCause() != null) { doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
if (e.getCause() instanceof InvocationTargetException) {
err = ((InvocationTargetException) e.getCause()).getTargetException().getMessage();
} else {
err = e.getCause().getMessage();
}
}
doFireJsonResultResponse(ctx, JsonResult.error(err), 500);
} }
} }

View File

@@ -153,7 +153,7 @@ public class CommonUtil {
appVersion = properties.getProperty("app.version") + "build" + properties.getProperty("build"); appVersion = properties.getProperty("app.version") + "build" + properties.getProperty("build");
} }
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOGGER.error("读取app.properties失败", e);
} }
} }
return appVersion; return appVersion;

View File

@@ -10,6 +10,8 @@ import io.vertx.core.json.JsonObject;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/** /**
* 异步读取配置工具类 * 异步读取配置工具类
@@ -62,12 +64,35 @@ public class ConfigUtil {
// 异步获取配置 // 异步获取配置
// 成功直接完成 promise // 成功直接完成 promise
retriever.getConfig() retriever.getConfig()
.onSuccess(promise::complete) .onSuccess(config -> {
.onFailure(err -> { promise.complete(config);
// 配置读取失败,直接返回失败 Future
promise.fail(new RuntimeException(
"读取配置文件失败: " + path, err));
retriever.close(); retriever.close();
})
.onFailure(err -> {
retriever.close();
// 读取失败时,尝试从 resources/ 子目录读取(兼容 Docker 卷挂载场景)
String resourcesPath = "resources/" + path;
if (!path.startsWith("resources/") && Files.exists(Path.of(resourcesPath))) {
ConfigStoreOptions fallbackStore = new ConfigStoreOptions()
.setType("file")
.setFormat(format)
.setConfig(new JsonObject().put("path", resourcesPath));
ConfigRetriever fallbackRetriever = ConfigRetriever
.create(vertx, new ConfigRetrieverOptions().addStore(fallbackStore));
fallbackRetriever.getConfig()
.onSuccess(config -> {
promise.complete(config);
fallbackRetriever.close();
})
.onFailure(e2 -> {
promise.fail(new RuntimeException(
"读取配置文件失败: " + path + " (也尝试了 " + resourcesPath + ")", e2));
fallbackRetriever.close();
});
} else {
promise.fail(new RuntimeException(
"读取配置文件失败: " + path, err));
}
}); });
return promise.future(); return promise.future();

View File

@@ -1,7 +1,7 @@
package cn.qaiu.vx.core.util; package cn.qaiu.vx.core.util;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* vertx 上下文外的本地容器 为不在vertx线程的方法传递数据 * vertx 上下文外的本地容器 为不在vertx线程的方法传递数据
@@ -10,11 +10,10 @@ import java.util.Map;
* @author <a href="https://qaiu.top">QAIU</a> * @author <a href="https://qaiu.top">QAIU</a>
*/ */
public class LocalConstant { public class LocalConstant {
private static final Map<String, Object> LOCAL_CONST = new HashMap<>(); private static final Map<String, Object> LOCAL_CONST = new ConcurrentHashMap<>();
public static Map<String, Object> put(String k, Object v) { public static Map<String, Object> put(String k, Object v) {
if (LOCAL_CONST.containsKey(k)) return LOCAL_CONST; LOCAL_CONST.putIfAbsent(k, v);
LOCAL_CONST.put(k, v);
return LOCAL_CONST; return LOCAL_CONST;
} }

View File

@@ -25,6 +25,9 @@ import java.net.URL;
import java.text.ParseException; import java.text.ParseException;
import java.util.*; import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS; import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS;
/** /**
@@ -36,6 +39,8 @@ import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS;
*/ */
public final class ReflectionUtil { public final class ReflectionUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionUtil.class);
// 缓存Reflections实例避免重复扫描每次扫描约35K+值耗时1-3秒占用大量内存 // 缓存Reflections实例避免重复扫描每次扫描约35K+值耗时1-3秒占用大量内存
private static final Map<String, Reflections> REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); private static final Map<String, Reflections> REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
@@ -128,7 +133,7 @@ public final class ReflectionUtil {
parameterTypes[j - k])); parameterTypes[j - k]));
} }
} catch (NotFoundException e) { } catch (NotFoundException e) {
e.printStackTrace(); LOGGER.error("获取方法参数失败", e);
} }
return paramMap; return paramMap;
} }
@@ -183,7 +188,7 @@ public final class ReflectionUtil {
try { try {
return DateUtils.parseDate(value, fmt); return DateUtils.parseDate(value, fmt);
} catch (ParseException e) { } catch (ParseException e) {
e.printStackTrace(); LOGGER.error("日期解析失败: {}", value, e);
throw new RuntimeException("无法将格式化日期"); throw new RuntimeException("无法将格式化日期");
} }
default: default:
@@ -215,7 +220,7 @@ public final class ReflectionUtil {
} }
return arr; return arr;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LOGGER.error("数组类型转换失败: {}", value, e);
} }
return null; return null;
} }

View File

@@ -196,7 +196,7 @@ public class HttpProxyVerticle extends AbstractVerticle {
); );
}) })
.onFailure(err -> { .onFailure(err -> {
err.printStackTrace(); LOGGER.error("HTTP请求失败", err);
clientRequest.response().setStatusCode(502).end("Bad Gateway: Request failed"); clientRequest.response().setStatusCode(502).end("Bad Gateway: Request failed");
}); });
} }
@@ -222,7 +222,7 @@ public class HttpProxyVerticle extends AbstractVerticle {
} }
return port; return port;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LOGGER.error("提取端口失败: {}", urlString, e);
// 出现异常时返回 -1表示提取失败 // 出现异常时返回 -1表示提取失败
return -1; return -1;
} }

View File

@@ -23,12 +23,11 @@ public class RouterVerticle extends AbstractVerticle {
private static final Logger LOGGER = LoggerFactory.getLogger(RouterVerticle.class); private static final Logger LOGGER = LoggerFactory.getLogger(RouterVerticle.class);
private static final int port = SharedDataUtil.getValueForServerConfig("port"); 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 static final JsonObject globalConfig = SharedDataUtil.getJsonConfig("globalConfig");
private HttpServer server; private HttpServer server;
private Router router;
static { static {
LOGGER.info(JacksonConfig.class.getSimpleName() + " >> "); LOGGER.info(JacksonConfig.class.getSimpleName() + " >> ");
@@ -61,6 +60,8 @@ public class RouterVerticle extends AbstractVerticle {
.setReuseAddress(true) // 允许地址重用 .setReuseAddress(true) // 允许地址重用
.setReusePort(true); // 允许端口重用 .setReusePort(true); // 允许端口重用
router = new RouterHandlerFactory(
SharedDataUtil.getJsonStringForServerConfig("contextPath")).createRouter();
server = vertx.createHttpServer(options); server = vertx.createHttpServer(options);
server.requestHandler(router).webSocketHandler(s->{}).listen() server.requestHandler(router).webSocketHandler(s->{}).listen()

9
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
# Fix permissions on volume-mounted directories (runs as root)
chown -R appuser:appgroup /app/db /app/logs /app/resources 2>/dev/null || true
# Run Java directly - entrypoint is PID 1, exec makes Java PID 1
# Docker SIGTERM goes directly to Java, triggering ShutdownHook
exec java -Xmx${JVM_XMX:-512M} ${JVM_OPTS} -Duser.timezone=${TZ:-Asia/Shanghai} -jar /app/netdisk-fast-download.jar

View File

@@ -4,26 +4,26 @@ NFD 解析器模块:聚合各类网盘/分享页解析,统一输出文件列
- 语言Java 17 - 语言Java 17
- 构建Maven - 构建Maven
- 模块版本10.1.17 - 模块版本10.2.5
## 依赖Maven Central ## 依赖Maven Central
```xml ```xml
<dependency> <dependency>
<groupId>cn.qaiu</groupId> <groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId> <artifactId>parser</artifactId>
<version>10.1.17</version> <version>10.2.5</version>
</dependency> </dependency>
``` ```
- Gradle Groovy DSL - Gradle Groovy DSL
```groovy ```groovy
dependencies { dependencies {
implementation 'cn.qaiu:parser:10.1.17' implementation 'cn.qaiu:parser:10.2.5'
} }
``` ```
- Gradle Kotlin DSL - Gradle Kotlin DSL
```kotlin ```kotlin
dependencies { dependencies {
implementation("cn.qaiu:parser:10.1.17") implementation("cn.qaiu:parser:10.2.5")
} }
``` ```

View File

@@ -28,7 +28,7 @@
<dependency> <dependency>
<groupId>cn.qaiu</groupId> <groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId> <artifactId>parser</artifactId>
<version>10.1.17</version> <version>10.2.5</version>
</dependency> </dependency>
``` ```

View File

@@ -11,7 +11,7 @@
<dependency> <dependency>
<groupId>cn.qaiu</groupId> <groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId> <artifactId>parser</artifactId>
<version>10.1.17</version> <version>10.2.5</version>
</dependency> </dependency>
``` ```

View File

@@ -12,7 +12,7 @@
<groupId>cn.qaiu</groupId> <groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId> <artifactId>parser</artifactId>
<version>10.2.5</version> <version>${parserVersion}</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>cn.qaiu:parser</name> <name>cn.qaiu:parser</name>
@@ -35,9 +35,9 @@
</developers> </developers>
<scm> <scm>
<connection>scm:git:https://github.com/qaiu/netdisk-fast-download.git</connection> <connection>scm:git:https://github.com/${github.owner}/${github.repo}.git</connection>
<developerConnection>scm:git:ssh://git@github.com:qaiu/netdisk-fast-download.git</developerConnection> <developerConnection>scm:git:ssh://git@github.com:${github.owner}/${github.repo}.git</developerConnection>
<url>https://github.com/qaiu/netdisk-fast-download</url> <url>https://github.com/${github.owner}/${github.repo}</url>
</scm> </scm>
<distributionManagement> <distributionManagement>
@@ -52,20 +52,19 @@
</distributionManagement> </distributionManagement>
<properties> <properties>
<revision>0.2.1</revision>
<java.version>17</java.version> <java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Versions --> <!-- Versions -->
<vertx.version>4.5.24</vertx.version> <vertx.version>4.5.27</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version> <org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.38</lombok.version> <lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.16</slf4j.version> <slf4j.version>2.0.16</slf4j.version>
<commons-lang3.version>3.18.0</commons-lang3.version> <commons-lang3.version>3.18.0</commons-lang3.version>
<jackson.version>2.18.6</jackson.version> <jackson.version>2.18.6</jackson.version>
<logback.version>1.5.19</logback.version> <logback.version>1.5.32</logback.version>
<junit.version>4.13.2</junit.version> <junit.version>4.13.2</junit.version>
</properties> </properties>
@@ -124,6 +123,41 @@
<build> <build>
<plugins> <plugins>
<!-- 从 git remote origin 自动识别 GitHub 仓库地址 -->
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>4.1.1</version>
<dependencies>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>4.0.24</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>initialize</phase>
<goals><goal>execute</goal></goals>
<configuration>
<scripts>
<script>
def url = 'git remote get-url origin'.execute().text.trim()
def m = (url =~ 'github\\.com[:/]([^/]+)/([^/.]+?)(?:\\.git)?$')
if (m.find()) {
project.properties.setProperty('github.owner', m.group(1))
project.properties.setProperty('github.repo', m.group(2))
} else {
project.properties.setProperty('github.owner', 'qaiu')
project.properties.setProperty('github.repo', 'netdisk-fast-download')
}
</script>
</scripts>
</configuration>
</execution>
</executions>
</plugin>
<!-- 编译 --> <!-- 编译 -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@@ -86,7 +86,10 @@ public class ShareLinkInfo {
// 将type和shareKey组合成一个字符串作为缓存key // 将type和shareKey组合成一个字符串作为缓存key
String key = type + ":" + shareKey; String key = type + ":" + shareKey;
if (type.equals("p115")) { 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; return key;
} }

View File

@@ -81,7 +81,7 @@ public class ParserCreate {
if (shareKey != null) { if (shareKey != null) {
shareLinkInfo.setShareKey(shareKey); shareLinkInfo.setShareKey(shareKey);
} }
} catch (Exception ignored) {} } catch (IllegalStateException | IllegalArgumentException ignored) {}
// 提取密码 // 提取密码
try { try {
@@ -89,7 +89,7 @@ public class ParserCreate {
if (StringUtils.isNotEmpty(pwd)) { if (StringUtils.isNotEmpty(pwd)) {
shareLinkInfo.setSharePassword(pwd); shareLinkInfo.setSharePassword(pwd);
} }
} catch (Exception ignored) {} } catch (IllegalStateException | IllegalArgumentException ignored) {}
// 设置标准URL // 设置标准URL
if (customParserConfig.getStandardUrlTemplate() != null) { if (customParserConfig.getStandardUrlTemplate() != null) {
@@ -133,7 +133,7 @@ public class ParserCreate {
shareLinkInfo.setSharePassword(pwd); shareLinkInfo.setSharePassword(pwd);
} }
standardUrl = standardUrl.replace("{pwd}", pwd); standardUrl = standardUrl.replace("{pwd}", pwd);
} catch (Exception ignored) {} } catch (IllegalStateException | IllegalArgumentException ignored) {}
shareLinkInfo.setShareUrl(shareUrl); shareLinkInfo.setShareUrl(shareUrl);
shareLinkInfo.setShareKey(shareKey); shareLinkInfo.setShareKey(shareKey);
@@ -266,14 +266,14 @@ public class ParserCreate {
if (shareKey != null) { if (shareKey != null) {
shareLinkInfo.setShareKey(shareKey); shareLinkInfo.setShareKey(shareKey);
} }
} catch (Exception ignored) {} } catch (IllegalStateException | IllegalArgumentException ignored) {}
try { try {
String password = matcher.group("PWD"); String password = matcher.group("PWD");
if (password != null) { if (password != null) {
shareLinkInfo.setSharePassword(password); shareLinkInfo.setSharePassword(password);
} }
} catch (Exception ignored) {} } catch (IllegalStateException | IllegalArgumentException ignored) {}
// 设置标准URL如果有模板 // 设置标准URL如果有模板
if (customConfig.getStandardUrlTemplate() != null) { if (customConfig.getStandardUrlTemplate() != null) {

View File

@@ -534,7 +534,7 @@ public class JsHttpClient {
} else { } else {
promise.fail(result.cause()); promise.fail(result.cause());
} }
}).onFailure(Throwable::printStackTrace); }).onFailure(e -> log.error("HTTP请求失败", e));
// 等待响应完成(使用配置的超时时间) // 等待响应完成(使用配置的超时时间)
HttpResponse<Buffer> response = promise.future().toCompletionStage() HttpResponse<Buffer> response = promise.future().toCompletionStage()

View File

@@ -33,7 +33,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class); private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
private static WorkerExecutor EXECUTOR; private static volatile WorkerExecutor EXECUTOR;
private static final Object EXECUTOR_LOCK = new Object(); private static final Object EXECUTOR_LOCK = new Object();
private static String FETCH_RUNTIME_JS = null; private static String FETCH_RUNTIME_JS = null;

View File

@@ -355,7 +355,7 @@ public class JsPlaygroundExecutor {
*/ */
public List<JsPlaygroundLogger.LogEntry> getLogs() { public List<JsPlaygroundLogger.LogEntry> getLogs() {
List<JsPlaygroundLogger.LogEntry> logs = playgroundLogger.getLogs(); List<JsPlaygroundLogger.LogEntry> logs = playgroundLogger.getLogs();
System.out.println("[JsPlaygroundExecutor] 获取日志,数量: " + logs.size()); log.debug("获取日志,数量: {}", logs.size());
return logs; return logs;
} }

View File

@@ -4,6 +4,9 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* 演练场日志收集器 * 演练场日志收集器
* 收集JavaScript执行过程中的日志信息 * 收集JavaScript执行过程中的日志信息
@@ -13,7 +16,10 @@ import java.util.List;
*/ */
public class JsPlaygroundLogger { public class JsPlaygroundLogger {
private static final Logger log = LoggerFactory.getLogger(JsPlaygroundLogger.class);
// 使用线程安全的列表 // 使用线程安全的列表
private static final int MAX_LOG_SIZE = 1000;
private final List<LogEntry> logs = Collections.synchronizedList(new ArrayList<>()); private final List<LogEntry> logs = Collections.synchronizedList(new ArrayList<>());
/** /**
@@ -59,6 +65,18 @@ public class JsPlaygroundLogger {
return obj.toString(); return obj.toString();
} }
/**
* 添加日志条目,超过最大容量时移除最早的条目
*/
private void addLog(LogEntry entry) {
synchronized (logs) {
if (logs.size() >= MAX_LOG_SIZE) {
logs.remove(0);
}
logs.add(entry);
}
}
/** /**
* 记录日志(内部方法) * 记录日志(内部方法)
* @param level 日志级别 * @param level 日志级别
@@ -67,8 +85,8 @@ public class JsPlaygroundLogger {
*/ */
private void log(String level, Object message, String source) { private void log(String level, Object message, String source) {
String msg = toString(message); String msg = toString(message);
logs.add(new LogEntry(level, msg, source)); addLog(new LogEntry(level, msg, source));
System.out.println("[" + source + "PlaygroundLogger] " + level + ": " + msg); log.debug("[{}PlaygroundLogger] {}: {}", source, level, msg);
} }
/** /**
@@ -111,8 +129,8 @@ public class JsPlaygroundLogger {
if (throwable != null) { if (throwable != null) {
msg = msg + ": " + throwable.getMessage(); msg = msg + ": " + throwable.getMessage();
} }
logs.add(new LogEntry("ERROR", msg, "JS")); addLog(new LogEntry("ERROR", msg, "JS"));
System.out.println("[JSPlaygroundLogger] ERROR: " + msg); log.debug("[JSPlaygroundLogger] ERROR: {}", msg);
} }
// ===== 以下是供Java层调用的内部方法 ===== // ===== 以下是供Java层调用的内部方法 =====
@@ -153,8 +171,8 @@ public class JsPlaygroundLogger {
if (throwable != null) { if (throwable != null) {
msg = msg + ": " + throwable.getMessage(); msg = msg + ": " + throwable.getMessage();
} }
logs.add(new LogEntry("ERROR", msg, "JAVA")); addLog(new LogEntry("ERROR", msg, "JAVA"));
System.out.println("[JAVAPlaygroundLogger] ERROR: " + msg); log.debug("[JAVAPlaygroundLogger] ERROR: {}", msg);
} }
/** /**

View File

@@ -139,21 +139,20 @@ public class JsScriptLoader {
try { try {
String jarPath = jarUrl.getPath().substring(5, jarUrl.getPath().indexOf("!")); String jarPath = jarUrl.getPath().substring(5, jarUrl.getPath().indexOf("!"));
JarFile jarFile = new JarFile(jarPath);
Enumeration<JarEntry> entries = jarFile.entries(); try (JarFile jarFile = new JarFile(jarPath)) {
while (entries.hasMoreElements()) { Enumeration<JarEntry> entries = jarFile.entries();
JarEntry entry = entries.nextElement(); while (entries.hasMoreElements()) {
String entryName = entry.getName(); JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.startsWith(RESOURCE_PATH + "/") && if (entryName.startsWith(RESOURCE_PATH + "/") &&
entryName.endsWith(".js") && entryName.endsWith(".js") &&
!isExcludedFile(entryName.substring(entryName.lastIndexOf('/') + 1))) { !isExcludedFile(entryName.substring(entryName.lastIndexOf('/') + 1))) {
resourceFiles.add(entryName); resourceFiles.add(entryName);
}
} }
} }
jarFile.close();
} catch (Exception e) { } catch (Exception e) {
log.debug("解析JAR包资源文件失败", e); log.debug("解析JAR包资源文件失败", e);
} }

View File

@@ -109,9 +109,9 @@ public class FjTool extends PanBase {
// String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString() // String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
static String token = null; static volatile String token = null;
static String userId = null; static volatile String userId = null;
public static boolean authFlag = true; public static volatile boolean authFlag = true;
public FjTool(ShareLinkInfo shareLinkInfo) { public FjTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
@@ -289,12 +289,14 @@ public class FjTool extends PanBase {
JsonObject json = asJson(res2); JsonObject json = asJson(res2);
if (json.getInteger("code") == 200) { if (json.getInteger("code") == 200) {
token = json.getJsonObject("data").getString("appToken"); token = json.getJsonObject("data").getString("appToken");
header0.set("appToken", token); MultiMap h0 = MultiMap.caseInsensitiveMultiMap();
log.info("登录成功 token: {}", token); 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)) client.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
.setTemplateParam("uuid", uuid) .setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2) .setTemplateParam("ts", tsEncode2)
.putHeaders(header0).send().onSuccess(res -> { .putHeaders(h0).send().onSuccess(res -> {
if (asJson(res).getInteger("code") == 200) { if (asJson(res).getInteger("code") == 200) {
if (FjTool.userId == null) { if (FjTool.userId == null) {
FjTool.userId = asJson(res).getJsonObject("map").getString("userId"); FjTool.userId = asJson(res).getJsonObject("map").getString("userId");
@@ -454,7 +456,10 @@ public class FjTool extends PanBase {
// 如果参数里的目录ID不为空则直接解析目录 // 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) { 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); parserDir(dirId, shareId, promise0);
return promise0.future(); return promise0.future();
} }
@@ -495,7 +500,7 @@ public class FjTool extends PanBase {
JsonArray list; JsonArray list;
try { try {
JsonObject jsonObject = asJson(res); JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily()); log.debug("目录列表: {}", jsonObject.encodePrettily());
list = jsonObject.getJsonArray("list"); list = jsonObject.getJsonArray("list");
} catch (Exception e) { } catch (Exception e) {
log.error("解析目录失败: {}", res.bodyAsString()); log.error("解析目录失败: {}", res.bodyAsString());
@@ -576,6 +581,10 @@ public class FjTool extends PanBase {
// 第二次请求 // 第二次请求
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson"); JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
if (paramJson == null) {
promise.fail("缺少 paramJson 参数");
return promise.future();
}
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP)) clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
.setTemplateParam("fidEncode", paramJson.getString("fidEncode")) .setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid")) .setTemplateParam("uuid", paramJson.getString("uuid"))

View File

@@ -389,6 +389,10 @@ public class FsTool extends PanBase {
try { try {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson"); JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
if (paramJson == null) {
parsePromise.fail("缺少 paramJson 参数");
return parsePromise.future();
}
String shareUrl = paramJson.getString("shareUrl"); String shareUrl = paramJson.getString("shareUrl");
String objToken = paramJson.getString("objToken"); String objToken = paramJson.getString("objToken");
String tenant = extractTenant(shareUrl); String tenant = extractTenant(shareUrl);
@@ -444,7 +448,7 @@ public class FsTool extends PanBase {
if (m1.find()) { if (m1.find()) {
try { try {
return URLDecoder.decode(m1.group(1).trim(), StandardCharsets.UTF_8); 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()) { if (m2.find()) {
try { try {
return URLDecoder.decode(m2.group(1).trim(), StandardCharsets.UTF_8); return URLDecoder.decode(m2.group(1).trim(), StandardCharsets.UTF_8);
} catch (Exception ignored) { } catch (IllegalArgumentException ignored) {
} }
} }

View File

@@ -89,8 +89,8 @@ public class IzTool extends PanBase {
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString() String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
public static String token = null; public static volatile String token = null;
public static boolean authFlag = true; public static volatile boolean authFlag = true;
public Future<String> parse() { public Future<String> parse() {
@@ -102,7 +102,7 @@ public class IzTool extends PanBase {
if (shareLinkInfo.getOtherParam().containsKey("auths")) { if (shareLinkInfo.getOtherParam().containsKey("auths")) {
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED"); boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}", log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}",
isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(10, token.length())) + "...)" : "未登录"); isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(8, token.length())) + "...)" : "未登录");
// 如果需要认证但还没有token先执行登录 // 如果需要认证但还没有token先执行登录
if ((isTempAuth || authFlag) && token == null) { if ((isTempAuth || authFlag) && token == null) {
@@ -118,7 +118,7 @@ public class IzTool extends PanBase {
// 登录失败,继续使用免登录模式 // 登录失败,继续使用免登录模式
}); });
} else if (token != null) { } 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 { } else {
log.debug("文件解析无认证信息,使用免登录模式"); log.debug("文件解析无认证信息,使用免登录模式");
@@ -247,7 +247,7 @@ public class IzTool extends PanBase {
log.warn("登录失败: {}", failRes.getMessage()); log.warn("登录失败: {}", failRes.getMessage());
fail(failRes.getMessage()); fail(failRes.getMessage());
}).onSuccess(r-> { }).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken")) httpRequest.setTemplateParam("appToken", token)
.putHeaders(header); .putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}); });
@@ -263,12 +263,12 @@ public class IzTool extends PanBase {
log.warn("重新登录失败: {}", failRes.getMessage()); log.warn("重新登录失败: {}", failRes.getMessage());
fail(failRes.getMessage()); fail(failRes.getMessage());
}).onSuccess(r-> { }).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken")) httpRequest.setTemplateParam("appToken", token)
.putHeaders(header); .putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}); });
} else { } else {
httpRequest.setTemplateParam("appToken", header.get("appToken")) httpRequest.setTemplateParam("appToken", token)
.putHeaders(header); .putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
} }
@@ -311,8 +311,7 @@ public class IzTool extends PanBase {
JsonObject json = asJson(res2); JsonObject json = asJson(res2);
if (json.getInteger("code") == 200) { if (json.getInteger("code") == 200) {
token = json.getJsonObject("data").getString("appToken"); token = json.getJsonObject("data").getString("appToken");
header.set("appToken", token); log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null");
log.info("登录成功 token: {}", token);
promise1.complete(); promise1.complete();
} else { } else {
// 检查是否为临时认证 // 检查是否为临时认证
@@ -463,7 +462,10 @@ public class IzTool extends PanBase {
// 如果参数里的目录ID不为空则直接解析目录 // 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) { 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); parserDir(dirId, shareId, promise);
return promise.future(); return promise.future();
} }

View File

@@ -88,8 +88,8 @@ public class IzToolWithAuth extends PanBase {
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString() String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
public static String token = null; public static volatile String token = null;
public static boolean authFlag = true; public static volatile boolean authFlag = true;
public Future<String> parse() { public Future<String> parse() {
@@ -216,7 +216,7 @@ public class IzToolWithAuth extends PanBase {
log.warn("登录失败: {}", failRes.getMessage()); log.warn("登录失败: {}", failRes.getMessage());
fail(failRes.getMessage()); fail(failRes.getMessage());
}).onSuccess(r-> { }).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken")) httpRequest.setTemplateParam("appToken", token)
.putHeaders(header); .putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}); });
@@ -232,12 +232,12 @@ public class IzToolWithAuth extends PanBase {
log.warn("重新登录失败: {}", failRes.getMessage()); log.warn("重新登录失败: {}", failRes.getMessage());
fail(failRes.getMessage()); fail(failRes.getMessage());
}).onSuccess(r-> { }).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken")) httpRequest.setTemplateParam("appToken", token)
.putHeaders(header); .putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}); });
} else { } else {
httpRequest.setTemplateParam("appToken", header.get("appToken")) httpRequest.setTemplateParam("appToken", token)
.putHeaders(header); .putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2")); httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
} }
@@ -280,8 +280,7 @@ public class IzToolWithAuth extends PanBase {
JsonObject json = asJson(res2); JsonObject json = asJson(res2);
if (json.getInteger("code") == 200) { if (json.getInteger("code") == 200) {
token = json.getJsonObject("data").getString("appToken"); token = json.getJsonObject("data").getString("appToken");
header.set("appToken", token); log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null");
log.info("登录成功 token: {}", token);
promise1.complete(); promise1.complete();
} else { } else {
// 检查是否为临时认证 // 检查是否为临时认证
@@ -432,7 +431,8 @@ public class IzToolWithAuth extends PanBase {
// 如果参数里的目录ID不为空则直接解析目录 // 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) { 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); parserDir(dirId, shareId, promise);
return promise.future(); return promise.future();
} }
@@ -480,7 +480,7 @@ public class IzToolWithAuth extends PanBase {
requestDirList(id, shareId, tsEncode, promise); requestDirList(id, shareId, tsEncode, promise);
}) })
.onSuccess(r -> { .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); requestDirList(id, shareId, tsEncode, promise);
}); });
return; return;
@@ -627,7 +627,7 @@ public class IzToolWithAuth extends PanBase {
// 如果有 token使用 VIP 接口 // 如果有 token使用 VIP 接口
if (StringUtils.isNotBlank(appToken)) { 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)) webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
.putHeaders(header) .putHeaders(header)
.setTemplateParam("fidEncode", paramJson.getString("fidEncode")) .setTemplateParam("fidEncode", paramJson.getString("fidEncode"))

View File

@@ -14,6 +14,7 @@ import io.vertx.ext.web.client.WebClientSession;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.net.MalformedURLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -107,7 +108,7 @@ public class LzTool extends PanBase {
try { try {
setFileInfo(html, shareLinkInfo); setFileInfo(html, shareLinkInfo);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.error("文件信息解析异常", e);
} }
// 匹配iframe // 匹配iframe
Pattern compile = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\""); Pattern compile = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\"");
@@ -175,7 +176,7 @@ public class LzTool extends PanBase {
if (firstDot >= 0) { if (firstDot >= 0) {
domain = host.substring(firstDot); // e.g. ".lanzoum.com" domain = host.substring(firstDot); // e.g. ".lanzoum.com"
} }
} catch (Exception ignored) {} } catch (MalformedURLException ignored) {}
// 创建一个 Cookie 并放入 CookieStore // 创建一个 Cookie 并放入 CookieStore
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2); DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(domain); nettyCookie.setDomain(domain);
@@ -217,7 +218,7 @@ public class LzTool extends PanBase {
return; return;
} }
Map<?, ?> signMap = (Map<?, ?>)obj.get("data"); Map<?, ?> signMap = (Map<?, ?>)obj.get("data");
String url0 = obj.get("url").toString(); String url0 = String.valueOf(obj.get("url"));
MultiMap map = MultiMap.caseInsensitiveMultiMap(); MultiMap map = MultiMap.caseInsensitiveMultiMap();
signMap.forEach((k, v) -> { signMap.forEach((k, v) -> {
map.add((String) k, v.toString()); map.add((String) k, v.toString());
@@ -275,7 +276,7 @@ public class LzTool extends PanBase {
String h = du.getHost(); String h = du.getHost();
int dot = h.indexOf('.'); int dot = h.indexOf('.');
if (dot >= 0) downDomain = h.substring(dot); if (dot >= 0) downDomain = h.substring(dot);
} catch (Exception ignored) {} } catch (MalformedURLException ignored) {}
// 创建一个 Cookie 并放入 CookieStore // 创建一个 Cookie 并放入 CookieStore
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2); DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(downDomain); nettyCookie.setDomain(downDomain);
@@ -290,12 +291,12 @@ public class LzTool extends PanBase {
if (location0 == null) { if (location0 == null) {
fail(downUrl + " -> 直链获取失败2, 可能分享已失效"); fail(downUrl + " -> 直链获取失败2, 可能分享已失效");
} else { } else {
setDateAndComplate(location0); setDateAndComplete(location0);
} }
}).onFailure(handleFail(downUrl)); }).onFailure(handleFail(downUrl));
return; return;
} }
setDateAndComplate(location); setDateAndComplete(location);
}) })
.onFailure(handleFail(downUrl)); .onFailure(handleFail(downUrl));
} catch (Exception e) { } catch (Exception e) {
@@ -304,7 +305,7 @@ public class LzTool extends PanBase {
}).onFailure(handleFail(url)); }).onFailure(handleFail(url));
} }
private void setDateAndComplate(String location0) { private void setDateAndComplete(String location0) {
// 分享时间 提取url中的时间戳格式lanzoui.com/abc/abc/yyyy/mm/dd/ // 分享时间 提取url中的时间戳格式lanzoui.com/abc/abc/yyyy/mm/dd/
String regex = "(\\d{4}/\\d{1,2}/\\d{1,2})"; String regex = "(\\d{4}/\\d{1,2}/\\d{1,2})";
Matcher matcher = Pattern.compile(regex).matcher(location0); Matcher matcher = Pattern.compile(regex).matcher(location0);

View File

@@ -86,10 +86,10 @@ public class MkgsTool extends PanBase {
// 查找并输出 hash 字段的值 // 查找并输出 hash 字段的值
if (matcher.find()) { if (matcher.find()) {
String hashValue = matcher.group(1); // 获取第一个捕获组 String hashValue = matcher.group(1); // 获取第一个捕获组
System.out.println(hashValue); log.debug("hash: {}", hashValue);
client.getAbs(UriTemplate.of(API_URL)).setTemplateParam("hash", hashValue).send().onSuccess(res3 -> { client.getAbs(UriTemplate.of(API_URL)).setTemplateParam("hash", hashValue).send().onSuccess(res3 -> {
JsonObject jsonObject = asJson(res3); JsonObject jsonObject = asJson(res3);
System.out.println(jsonObject.encodePrettily()); log.debug("API response: {}", jsonObject.encodePrettily());
if (jsonObject.containsKey("url")) { if (jsonObject.containsKey("url")) {
promise.complete(jsonObject.getString("url")); promise.complete(jsonObject.getString("url"));
} else { } else {

View File

@@ -29,19 +29,19 @@ public class MkwTool extends PanBase {
clientSession.getAbs(shareUrl).send().onSuccess(result -> { clientSession.getAbs(shareUrl).send().onSuccess(result -> {
String cookie = result.headers().get("set-cookie"); 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]+)"; String regex = "([A-Za-z0-9_]+)=([A-Za-z0-9]+)";
Pattern pattern = Pattern.compile(regex); Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(cookie); Matcher matcher = pattern.matcher(cookie);
if (matcher.find()) { if (matcher.find()) {
System.out.println(matcher.group(1)); log.debug("cookie key: {}", matcher.group(1));
System.out.println(matcher.group(2)); log.debug("cookie value: {}", matcher.group(2));
var key = matcher.group(1); var key = matcher.group(1);
var token = matcher.group(2); var token = matcher.group(2);
String sign = JsExecUtils.getKwSign(token, key); String sign = JsExecUtils.getKwSign(token, key);
System.out.println(sign); log.debug("sign: {}", sign);
clientSession.getAbs(UriTemplate.of(API_URL)).setTemplateParam("mid", shareLinkInfo.getShareKey()) clientSession.getAbs(UriTemplate.of(API_URL)).setTemplateParam("mid", shareLinkInfo.getShareKey())
.putHeader("Secret", sign).send().onSuccess(res -> { .putHeader("Secret", sign).send().onSuccess(res -> {
JsonObject json = asJson(res); JsonObject json = asJson(res);
@@ -54,7 +54,7 @@ public class MkwTool extends PanBase {
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.error("解析失败", e);
fail("解析失败"); fail("解析失败");
} }
}); });

View File

@@ -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 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; private static final MultiMap header;
static { static {
@@ -49,9 +51,11 @@ public class P115Tool extends PanBase {
public Future<String> parse() { public Future<String> parse() {
// 第一次请求 获取文件信息 // 第一次请求 获取文件信息
Object uaObj = shareLinkInfo.getOtherParam().get("UA");
String ua = uaObj != null ? uaObj.toString() : DEFAULT_UA;
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL)) client.getAbs(UriTemplate.of(FIRST_REQUEST_URL))
.putHeaders(header) .putHeaders(header)
.putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString()) .putHeader("User-Agent", ua)
.setTemplateParam("dataKey", shareLinkInfo.getShareKey()) .setTemplateParam("dataKey", shareLinkInfo.getShareKey())
.setTemplateParam("dataPwd", shareLinkInfo.getSharePassword()) .setTemplateParam("dataPwd", shareLinkInfo.getSharePassword())
.send().onSuccess(res -> { .send().onSuccess(res -> {
@@ -68,7 +72,7 @@ public class P115Tool extends PanBase {
// share_code={dataKey}&receive_code={dataPwd}&file_id={file_id} // share_code={dataKey}&receive_code={dataPwd}&file_id={file_id}
client.postAbs(SECOND_REQUEST_URL) client.postAbs(SECOND_REQUEST_URL)
.putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") .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() .sendForm(MultiMap.caseInsensitiveMultiMap()
.set("share_code", shareLinkInfo.getShareKey()) .set("share_code", shareLinkInfo.getShareKey())
.set("receive_code", shareLinkInfo.getSharePassword()) .set("receive_code", shareLinkInfo.getSharePassword())

View File

@@ -85,7 +85,7 @@ public class PdbTool extends PanBase implements IPanTool {
}) })
.onFailure(handleFail()); .onFailure(handleFail());
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.error("URL编码异常", e);
} }
}) })

View File

@@ -129,7 +129,7 @@ public class PodTool extends PanBase {
if (urlMatcher.find()) { if (urlMatcher.find()) {
String url = urlMatcher.group("url"); String url = urlMatcher.group("url");
System.out.println("URL: " + url); log.debug("URL: {}", url);
return url; return url;
} }
throw new RuntimeException("URL匹配失败"); throw new RuntimeException("URL匹配失败");
@@ -172,7 +172,7 @@ public class PodTool extends PanBase {
if (tokenMatcher.find()) { if (tokenMatcher.find()) {
String token = tokenMatcher.group(1); String token = tokenMatcher.group(1);
System.out.println("Token: " + token); log.debug("Token: {}***", token.length() > 4 ? token.substring(0, 4) : "***");
return token; return token;
} }
throw new RuntimeException("token匹配失败"); throw new RuntimeException("token匹配失败");
@@ -198,8 +198,8 @@ public class PodTool extends PanBase {
// 发送请求并处理响应 // 发送请求并处理响应
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> { .thenApply(response -> {
System.out.println("Response Status Code: " + response.statusCode()); log.debug("Response Status Code: {}", response.statusCode());
System.out.println("Response Body: " + response.body()); log.debug("Response Body: {}", response.body());
promise.complete(response.body()); promise.complete(response.body());
return null; return null;
}); });

View File

@@ -74,9 +74,9 @@ public class QQTool extends PanBase {
}); });
// 调试匹配的情况 // 调试匹配的情况
System.out.println("文件名称: " + filename); log.debug("文件名称: {}", filename);
System.out.println("文件大小: " + filesize); log.debug("文件大小: {}", filesize);
System.out.println("文件直链: " + fileurl); log.debug("文件直链: {}", fileurl);
// 提交 // 提交
promise.complete(fileurl.replace("\\x26", "&")); promise.complete(fileurl.replace("\\x26", "&"));

View File

@@ -3,9 +3,11 @@ package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo; import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase; import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.HeaderUtils; import cn.qaiu.util.HeaderUtils;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.MultiMap; import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -13,27 +15,33 @@ import org.slf4j.LoggerFactory;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* QQ闪传 <br> * QQ闪传 <br>
* 只能客户端上传 支持Android QQ 9.2.5, MACOS QQ 6.9.78可生成分享链接通过浏览器下载支持超大文件有效期默认7天暂时没找到续期方法。<br> * 支持多文件、多级目录解析。通过 GetFileList API 获取文件列表BatchDownload API 获取下载直链。<br>
* 有效期默认7天。
*/ */
public class QQscTool extends PanBase { public class QQscTool extends PanBase {
Logger LOG = LoggerFactory.getLogger(QQscTool.class); Logger LOG = LoggerFactory.getLogger(QQscTool.class);
private static final String API_URL = "https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.qqntv2.richmedia.InnerProxy/BatchDownload"; private static final String BATCH_DOWNLOAD_API =
"https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.qqntv2.richmedia.InnerProxy/BatchDownload";
private static final MultiMap HEADERS = HeaderUtils.parseHeaders(""" private static final String GET_FILE_LIST_API =
"https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.file.FileFlashTrans/GetFileList";
private static final MultiMap BATCH_DOWNLOAD_HEADERS = HeaderUtils.parseHeaders("""
Accept-Encoding: gzip, deflate Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9 Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive Connection: keep-alive
Cookie: uin=9000002; p_uin=9000002 Cookie: uin=9000002; p_uin=9000002
DNT: 1 DNT: 1
Origin: https://qfile.qq.com Origin: https://qfile.qq.com
Referer: https://qfile.qq.com/q/Xolxtv5b4O
Sec-Fetch-Dest: empty Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin Sec-Fetch-Site: same-origin
@@ -46,86 +54,257 @@ public class QQscTool extends PanBase {
x-oidb: {"uint32_command":"0x9248", "uint32_service_type":"4"} x-oidb: {"uint32_command":"0x9248", "uint32_service_type":"4"}
"""); """);
private static final MultiMap GET_FILE_LIST_HEADERS = HeaderUtils.parseHeaders("""
Accept-Encoding: gzip, deflate
Cookie: uin=9000002; p_uin=9000002
Origin: https://qfile.qq.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
content-type: application/json
x-oidb: {"uint32_command":"0x93d4", "uint32_service_type":"1"}
""");
public QQscTool(ShareLinkInfo shareLinkInfo) { public QQscTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
} }
@Override
public Future<String> parse() { public Future<String> parse() {
String jsonTemplate = """
{"req_head":{"agent":8},"download_info":[{"batch_id":"%s","scene":{"business_type":4,"app_type":22,"scene_type":5},"index_node":{"file_uuid":"%s"},"url_type":2,"download_scene":0}],"scene_type":103}
""";
client.getAbs(shareLinkInfo.getShareUrl()).send(result -> { client.getAbs(shareLinkInfo.getShareUrl()).send(result -> {
if (result.succeeded()) { if (result.failed()) {
String htmlJs = result.result().bodyAsString();
LOG.debug("获取到的HTML内容: {}", htmlJs);
String fileUUID = getFileUUID(htmlJs);
String fileName = extractFileNameFromTitle(htmlJs);
if (fileName != null) {
LOG.info("提取到的文件名: {}", fileName);
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(fileName);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
} else {
LOG.warn("未能提取到文件名");
}
if (fileUUID != null) {
LOG.info("提取到的文件UUID: {}", fileUUID);
String formatted = jsonTemplate.formatted(fileUUID, fileUUID);
JsonObject entries = new JsonObject(formatted);
client.postAbs(API_URL)
.putHeaders(HEADERS)
.sendJsonObject(entries)
.onSuccess(result2 -> {
if (result2.statusCode() == 200) {
JsonObject body = asJson(result2);
LOG.debug("API响应内容: {}", body.encodePrettily());
// {
// "retcode": 0,
// "cost": 132,
// "message": "",
// "error": {
// "message": "",
// "code": 0
// },
// "data": {
// "download_rsp": [{
// 取 download_rsp
if (!body.containsKey("retcode") || body.getInteger("retcode") != 0) {
promise.fail("API请求失败错误信息: " + body.encodePrettily());
return;
}
JsonArray downloadRsp = body.getJsonObject("data").getJsonArray("download_rsp");
if (downloadRsp != null && !downloadRsp.isEmpty()) {
String url = downloadRsp.getJsonObject(0).getString("url");
if (fileName != null) {
url = url + "&filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8);
}
promise.complete(url);
} else {
promise.fail("API响应中缺少 download_rsp");
}
} else {
promise.fail("API请求失败状态码: " + result2.statusCode());
}
}).onFailure(e -> {
LOG.error("API请求异常", e);
promise.fail(e);
});
} else {
LOG.error("未能提取到文件UUID");
promise.fail("未能提取到文件UUID");
}
} else {
LOG.error("请求失败: {}", result.cause().getMessage()); LOG.error("请求失败: {}", result.cause().getMessage());
promise.fail(result.cause()); promise.fail(result.cause());
return;
}
String html = result.result().bodyAsString();
String fileName = extractFileNameFromTitle(html);
if (fileName != null) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(fileName);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
}
// 尝试用 GetFileList API 获取第一个文件的下载链接
String filesetId = extractFilesetId(html);
if (filesetId != null) {
fetchFileList(filesetId, "").onSuccess(fileList -> {
for (int i = 0; i < fileList.size(); i++) {
JsonObject file = fileList.getJsonObject(i);
if (!file.getBoolean("is_dir", false)) {
String physicalId = file.getJsonObject("physical").getString("id");
String name = file.getString("name");
downloadFile(physicalId, name);
return;
}
}
promise.fail("未找到可下载的文件");
}).onFailure(e -> {
LOG.warn("GetFileList 失败,回退到旧解析方式: {}", e.getMessage());
parseLegacy(html, fileName);
});
} else {
parseLegacy(html, fileName);
} }
}); });
return promise.future(); return promise.future();
} }
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> resultPromise = Promise.promise();
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
client.getAbs(shareLinkInfo.getShareUrl()).send(result -> {
if (result.failed()) {
resultPromise.fail(result.cause());
return;
}
String html = result.result().bodyAsString();
String filesetId = extractFilesetId(html);
if (filesetId == null) {
resultPromise.fail("无法从页面提取 filesetId");
return;
}
String parentId = dirId != null ? dirId : "";
fetchFileList(filesetId, parentId).onSuccess(fileList -> {
try {
List<FileInfo> list = new ArrayList<>();
String panType = shareLinkInfo.getType();
for (int i = 0; i < fileList.size(); i++) {
JsonObject file = fileList.getJsonObject(i);
FileInfo fileInfo = new FileInfo();
String name = file.getString("name");
String cliFileid = file.getString("cli_fileid");
boolean isDir = file.getBoolean("is_dir", false);
String sizeStr = file.getString("file_size");
fileInfo.setFileName(name)
.setFileId(cliFileid)
.setPanType(panType)
.setSizeStr(sizeStr);
if (isDir) {
fileInfo.setFileType("folder")
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s",
getDomainName(),
URLEncoder.encode(shareLinkInfo.getShareUrl(), StandardCharsets.UTF_8),
cliFileid));
} else {
String physicalId = file.getJsonObject("physical").getString("id");
JsonObject paramJson = new JsonObject()
.put("fileId", physicalId)
.put("fileName", name)
.put("cliFileid", cliFileid);
String param = CommonUtils.urlBase64Encode(paramJson.encode());
fileInfo.setFileType("file")
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
getDomainName(), panType, param));
}
list.add(fileInfo);
}
resultPromise.complete(list);
} catch (Exception e) {
resultPromise.fail(e);
}
}).onFailure(resultPromise::fail);
});
return resultPromise.future();
}
@Override
public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
String fileId = paramJson.getString("fileId");
String fileName = paramJson.getString("fileName");
Promise<String> p = Promise.promise();
callBatchDownload(fileId, fileName, p);
return p.future();
}
// ========== 内部方法 ==========
/**
* 调用 BatchDownload API 获取单个文件的下载直链
*/
private void downloadFile(String physicalId, String fileName) {
callBatchDownload(physicalId, fileName, promise);
}
private void callBatchDownload(String physicalId, String fileName, Promise<String> p) {
String body = """
{"req_head":{"agent":8},"download_info":[{"batch_id":"%s","scene":{"business_type":4,"app_type":22,"scene_type":5},"index_node":{"file_uuid":"%s"},"url_type":2,"download_scene":0}],"scene_type":103}
""".formatted(physicalId, physicalId);
client.postAbs(BATCH_DOWNLOAD_API)
.putHeaders(BATCH_DOWNLOAD_HEADERS)
.sendJsonObject(new JsonObject(body))
.onSuccess(resp -> {
if (resp.statusCode() != 200) {
p.fail("BatchDownload 请求失败,状态码: " + resp.statusCode());
return;
}
JsonObject respBody = asJson(resp);
if (!respBody.containsKey("retcode") || respBody.getInteger("retcode") != 0) {
p.fail("BatchDownload 请求失败: " + respBody.encodePrettily());
return;
}
JsonArray downloadRsp = respBody.getJsonObject("data").getJsonArray("download_rsp");
if (downloadRsp == null || downloadRsp.isEmpty()) {
p.fail("BatchDownload 响应中缺少 download_rsp");
return;
}
String url = downloadRsp.getJsonObject(0).getString("url");
if (url != null && url.startsWith("&filename=")) {
p.fail("该文件已被和谐");
return;
}
if (fileName != null) {
url = url + "&filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8);
}
p.complete(url);
})
.onFailure(e -> {
LOG.error("BatchDownload 请求异常", e);
p.fail(e);
});
}
/**
* 调用 GetFileList API 获取指定目录下的文件列表
*/
private Future<JsonArray> fetchFileList(String filesetId, String parentId) {
Promise<JsonArray> p = Promise.promise();
JsonObject body = new JsonObject()
.put("fileset_id", filesetId)
.put("req_infos", new JsonArray()
.add(new JsonObject()
.put("parent_id", parentId)
.put("req_depth", 1)
.put("count", 50)
.put("filter_condition", new JsonObject().put("file_category", 0))
.put("sort_conditions", new JsonArray()
.add(new JsonObject()
.put("sort_field", 0)
.put("sort_order", 0)))))
.put("support_folder_status", true);
MultiMap headers = GET_FILE_LIST_HEADERS.set("Referer", shareLinkInfo.getShareUrl());
client.postAbs(GET_FILE_LIST_API)
.putHeaders(headers)
.sendJsonObject(body)
.onSuccess(resp -> {
if (resp.statusCode() != 200) {
p.fail("GetFileList 请求失败,状态码: " + resp.statusCode());
return;
}
JsonObject respBody = asJson(resp);
if (respBody.getInteger("retcode", -1) != 0) {
p.fail("GetFileList 请求失败: " + respBody.getString("message", "未知错误"));
return;
}
JsonArray fileLists = respBody.getJsonObject("data").getJsonArray("file_lists");
if (fileLists == null || fileLists.isEmpty()) {
p.fail("GetFileList 响应中缺少 file_lists");
return;
}
JsonArray fileList = fileLists.getJsonObject(0).getJsonArray("file_list");
p.complete(fileList != null ? fileList : new JsonArray());
})
.onFailure(e -> {
LOG.error("GetFileList 请求异常", e);
p.fail(e);
});
return p.future();
}
/**
* 从 HTML 的 __NUXT_DATA__ 中提取 fileset_id
*/
String extractFilesetId(String html) {
// Nuxt __NUXT_DATA__ 中 fileset_id 出现在缓存 key 的嵌套 JSON 中
// 直接匹配 fileset_id 后面最近的 UUID跳过转义引号、冒号等非hex字符
Pattern pattern = Pattern.compile(
"fileset_id[^a-f0-9]*([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})");
Matcher matcher = pattern.matcher(html);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/**
* 旧版解析方式(兼容单文件链接,通过 HTML 字符串搜索提取 UUID
*/
private void parseLegacy(String html, String fileName) {
String fileUUID = getFileUUID(html);
if (fileUUID == null) {
promise.fail("未能提取到文件UUID");
return;
}
LOG.info("使用旧版解析提取到的文件UUID: {}", fileUUID);
downloadFile(fileUUID, fileName);
}
String getFileUUID(String htmlJs) { String getFileUUID(String htmlJs) {
String keyword = "\"download_limit_status\""; String keyword = "\"download_limit_status\"";
String marker = "},\""; String marker = "},\"";
@@ -140,32 +319,23 @@ public class QQscTool extends PanBase {
String extracted = htmlJs.substring(quoteStart, quoteEnd); String extracted = htmlJs.substring(quoteStart, quoteEnd);
LOG.debug("提取结果: {}", extracted); LOG.debug("提取结果: {}", extracted);
return extracted; return extracted;
} else {
LOG.error("未找到结束引号: {}", marker);
} }
} else {
LOG.error("未找到标记: {} 在关键字: {} 之后", marker, keyword);
} }
} else {
LOG.error("未找到关键字: {}", keyword);
} }
return null; return null;
} }
public static String extractFileNameFromTitle(String content) { public static String extractFileNameFromTitle(String content) {
// 匹配<title>和</title>之间的内容
Pattern pattern = Pattern.compile("<title>(.*?)</title>"); Pattern pattern = Pattern.compile("<title>(.*?)</title>");
Matcher matcher = pattern.matcher(content); Matcher matcher = pattern.matcher(content);
if (matcher.find()) { if (matcher.find()) {
String fullTitle = matcher.group(1); String fullTitle = matcher.group(1);
// 按 "" 分割,取前半部分
int sepIndex = fullTitle.indexOf(""); int sepIndex = fullTitle.indexOf("");
if (sepIndex != -1) { if (sepIndex != -1) {
return fullTitle.substring(0, sepIndex); return fullTitle.substring(0, sepIndex);
} }
return fullTitle; // 如果没有分隔符,就返回全部 return fullTitle;
} }
return null; return null;
} }
} }

View File

@@ -67,11 +67,7 @@ public class WsTool extends PanBase {
String filepid = asJson(res2).getJsonObject("data").getString("ufileid"); // 文件夹pid String filepid = asJson(res2).getJsonObject("data").getString("ufileid"); // 文件夹pid
String filebid = asJson(res2).getJsonObject("data").getString("boxid"); // 文件夹bid String filebid = asJson(res2).getJsonObject("data").getString("boxid"); // 文件夹bid
// 调试输出文件夹信息 log.debug("文件夹期限: {}, 大小: {}, pid: {}, bid: {}", filetime, filesize, filepid, filebid);
System.out.println("文件夹期限: " + filetime);
System.out.println("文件夹大小: " + filesize);
System.out.println("文件夹pid: " + filepid);
System.out.println("文件夹bid: " + filebid);
// 获取文件信息 // 获取文件信息
httpClient.postAbs(SHARE_URL_API + "ufile/list").putHeaders(headers) httpClient.postAbs(SHARE_URL_API + "ufile/list").putHeaders(headers)
@@ -97,9 +93,7 @@ public class WsTool extends PanBase {
String filefid = asJson(res3).getJsonObject("data") String filefid = asJson(res3).getJsonObject("data")
.getJsonArray("fileList").getJsonObject(0).getString("fid"); // 文件fid .getJsonArray("fileList").getJsonObject(0).getString("fid"); // 文件fid
// 调试输出文件信息 log.debug("文件名称: {}, fid: {}", filename, filefid);
System.out.println("文件名称: " + filename);
System.out.println("文件fid: " + filefid);
// 检查文件是否失效 // 检查文件是否失效
httpClient.postAbs(SHARE_URL_API + "dl/sign").putHeaders(headers) httpClient.postAbs(SHARE_URL_API + "dl/sign").putHeaders(headers)
@@ -114,8 +108,7 @@ public class WsTool extends PanBase {
// 获取直链 // 获取直链
String fileurl = asJson(res4).getJsonObject("data").getString("url"); String fileurl = asJson(res4).getJsonObject("data").getString("url");
// 调试输出文件直链 log.debug("文件直链: {}", fileurl);
System.out.println("文件直链: " + fileurl);
if (!fileurl.equals("")) { if (!fileurl.equals("")) {
promise.complete(URLDecoder.decode(fileurl, StandardCharsets.UTF_8)); promise.complete(URLDecoder.decode(fileurl, StandardCharsets.UTF_8));

View File

@@ -14,7 +14,6 @@ import java.security.spec.X509EncodedKeySpec;
import java.util.Base64; import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Random;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -299,7 +298,7 @@ public class AESUtils {
//length用户要求产生字符串的长度 //length用户要求产生字符串的长度
public static String getRandomString(int length){ public static String getRandomString(int length){
String str="abcdefghijklmnopqrstuvwxyz0123456789"; String str="abcdefghijklmnopqrstuvwxyz0123456789";
Random random=new Random(); SecureRandom random=new SecureRandom();
StringBuilder sb=new StringBuilder(); StringBuilder sb=new StringBuilder();
for(int i=0;i<length;i++){ for(int i=0;i<length;i++){
int number=random.nextInt(36); int number=random.nextInt(36);

View File

@@ -33,6 +33,9 @@ public class CommonUtils {
public static Map<String, String> getURLParams(String url) throws MalformedURLException { public static Map<String, String> getURLParams(String url) throws MalformedURLException {
URL fullUrl = new URL(url); URL fullUrl = new URL(url);
String query = fullUrl.getQuery(); String query = fullUrl.getQuery();
if (query == null || query.isEmpty()) {
return new HashMap<>();
}
String[] params = query.split("&"); String[] params = query.split("&");
Map<String, String> map = new HashMap<>(); Map<String, String> map = new HashMap<>();
for (String param : params) { for (String param : params) {

View File

@@ -5,6 +5,8 @@ import io.vertx.core.Vertx;
import io.vertx.core.http.impl.headers.HeadersMultiMap; import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientSession; import io.vertx.ext.web.client.WebClientSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -15,6 +17,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class IpExtractor { public class IpExtractor {
private static final Logger log = LoggerFactory.getLogger(IpExtractor.class);
public static void main(String[] args) throws InterruptedException { public static void main(String[] args) throws InterruptedException {
@@ -42,9 +46,9 @@ public class IpExtractor {
WebClient client = WebClient.create(Vertx.vertx()); WebClient client = WebClient.create(Vertx.vertx());
WebClientSession webClientSession = WebClientSession.create(client); WebClientSession webClientSession = WebClientSession.create(client);
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res->{ webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res->{
System.out.println(res.toString()); log.debug("response: {}", res.toString());
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res2->{ webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res2->{
System.out.println(res2.toString()); log.debug("response2: {}", res2.toString());
}); });
}); });

View File

@@ -8,15 +8,19 @@ import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.ext.web.client.HttpResponse; import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientSession; import io.vertx.ext.web.client.WebClientSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ReqIpUtil { public class ReqIpUtil {
public static String BASE_URL = "https://ip.ihuan.me"; private static final Logger log = LoggerFactory.getLogger(ReqIpUtil.class);
public static String BASE_URL_TEMPLATE = BASE_URL + "/{path}";
public static final String BASE_URL = "https://ip.ihuan.me";
public static final String BASE_URL_TEMPLATE = BASE_URL + "/{path}";
// GET https://ip.ihuan.me/mouse.do -> $("input[name='key']").val("30b4975b5547fed806bd2b9caa18485a"); // GET https://ip.ihuan.me/mouse.do -> $("input[name='key']").val("30b4975b5547fed806bd2b9caa18485a");
public static String PATH1 = "mouse.do"; public static final String PATH1 = "mouse.do";
public static String PATH2 = "tqdl.html"; public static final String PATH2 = "tqdl.html";
// 创建请求头Map // 创建请求头Map
static MultiMap headers = new HeadersMultiMap(); static MultiMap headers = new HeadersMultiMap();
@@ -58,15 +62,15 @@ public class ReqIpUtil {
void next(AsyncResult<HttpResponse<Buffer>> response) { void next(AsyncResult<HttpResponse<Buffer>> response) {
if (response.failed()) { if (response.failed()) {
response.cause().printStackTrace(); log.error("请求失败", response.cause());
} else { } else {
HttpResponse<Buffer> res = response.result(); HttpResponse<Buffer> res = response.result();
System.out.println("Received response with status code " + res.statusCode()); log.debug("Received response with status code {}", res.statusCode());
System.out.println("Body: " + res.body()); log.debug("Body: {}", res.body());
webClientSession.getAbs(BASE_URL_TEMPLATE).setTemplateParam("path", PATH1) webClientSession.getAbs(BASE_URL_TEMPLATE).setTemplateParam("path", PATH1)
.putHeaders(headers) // 将请求头Map添加到请求中 .putHeaders(headers) // 将请求头Map添加到请求中
.send(response2 -> { .send(response2 -> {
System.out.println(response2.result().bodyAsString()); log.debug("response2: {}", response2.result().bodyAsString());
}); });
} }

View File

@@ -2,6 +2,9 @@ package cn.qaiu.util;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL; import java.net.URL;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -10,6 +13,8 @@ import java.util.Map;
public class URLUtil { public class URLUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(URLUtil.class);
private final Map<String, String> queryParams = new HashMap<>(); private final Map<String, String> queryParams = new HashMap<>();
// 构造函数传入URL并解析参数 // 构造函数传入URL并解析参数
@@ -31,7 +36,7 @@ public class URLUtil {
} }
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LOGGER.error("URL解析失败: {}", url, e);
} }
} }

View File

@@ -8,6 +8,8 @@ import cn.qaiu.parser.customjs.JsParserExecutor;
import cn.qaiu.WebClientVertxInit; import cn.qaiu.WebClientVertxInit;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.HashMap; import java.util.HashMap;
@@ -22,15 +24,26 @@ import java.util.Map;
*/ */
public class BaiduPhotoParserTest { 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 @Test
public void testBaiduPhotoParserRegistration() { public void testBaiduPhotoParserRegistration() {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
// 检查是否加载了百度相册解析器 // 检查是否加载了百度相册解析器
CustomParserConfig config = CustomParserRegistry.get("baidu_photo"); CustomParserConfig config = CustomParserRegistry.get("baidu_photo");
assert config != null : "百度相册解析器未加载"; assert config != null : "百度相册解析器未加载";
@@ -45,10 +58,6 @@ public class BaiduPhotoParserTest {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
try { try {
// 创建解析器 - 测试文件分享链接 // 创建解析器 - 测试文件分享链接
IPanTool tool = ParserCreate.fromType("baidu_photo") IPanTool tool = ParserCreate.fromType("baidu_photo")
@@ -77,10 +86,6 @@ public class BaiduPhotoParserTest {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
try { try {
// 创建解析器 - 测试文件夹分享链接 // 创建解析器 - 测试文件夹分享链接
IPanTool tool = ParserCreate.fromType("baidu_photo") IPanTool tool = ParserCreate.fromType("baidu_photo")
@@ -109,10 +114,6 @@ public class BaiduPhotoParserTest {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
try { try {
IPanTool tool = ParserCreate.fromType("baidu_photo") IPanTool tool = ParserCreate.fromType("baidu_photo")
// 分享key PPgOEodBVE // 分享key PPgOEodBVE
@@ -167,10 +168,6 @@ public class BaiduPhotoParserTest {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
try { try {
// 创建ShareLinkInfo // 创建ShareLinkInfo
Map<String, Object> otherParam = new HashMap<>(); Map<String, Object> otherParam = new HashMap<>();

View File

@@ -7,6 +7,8 @@ import cn.qaiu.parser.custom.CustomParserRegistry;
import cn.qaiu.parser.customjs.JsParserExecutor; import cn.qaiu.parser.customjs.JsParserExecutor;
import cn.qaiu.WebClientVertxInit; import cn.qaiu.WebClientVertxInit;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.HashMap; import java.util.HashMap;
@@ -21,15 +23,26 @@ import java.util.Map;
*/ */
public class JsParserTest { 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 @Test
public void testJsParserRegistration() { public void testJsParserRegistration() {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
// 检查是否加载了JavaScript解析器 // 检查是否加载了JavaScript解析器
CustomParserConfig config = CustomParserRegistry.get("demo_js"); CustomParserConfig config = CustomParserRegistry.get("demo_js");
assert config != null : "JavaScript解析器未加载"; assert config != null : "JavaScript解析器未加载";
@@ -44,10 +57,6 @@ public class JsParserTest {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
try { try {
// 创建解析器 // 创建解析器
IPanTool tool = ParserCreate.fromType("demo_js") IPanTool tool = ParserCreate.fromType("demo_js")
@@ -75,10 +84,6 @@ public class JsParserTest {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
try { try {
// 创建解析器 // 创建解析器
IPanTool tool = ParserCreate.fromType("demo_js") IPanTool tool = ParserCreate.fromType("demo_js")
@@ -115,10 +120,6 @@ public class JsParserTest {
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
try { try {
// 创建ShareLinkInfo // 创建ShareLinkInfo
Map<String, Object> otherParam = new HashMap<>(); Map<String, Object> otherParam = new HashMap<>();

View File

@@ -7,6 +7,8 @@ import cn.qaiu.parser.ParserCreate;
import cn.qaiu.parser.custom.CustomParserConfig; import cn.qaiu.parser.custom.CustomParserConfig;
import cn.qaiu.parser.custom.CustomParserRegistry; import cn.qaiu.parser.custom.CustomParserRegistry;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -19,12 +21,23 @@ public class JsFetchBridgeTest {
private static final Logger log = LoggerFactory.getLogger(JsFetchBridgeTest.class); 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 @Test
public void testFetchPolyfillLoaded() { public void testFetchPolyfillLoaded() {
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();
@@ -83,10 +96,6 @@ public class JsFetchBridgeTest {
@Test @Test
public void testPromiseBasicUsage() { public void testPromiseBasicUsage() {
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
// 清理注册表 // 清理注册表
CustomParserRegistry.clear(); CustomParserRegistry.clear();

View File

@@ -25,16 +25,17 @@
<packageDirectory>${project.basedir}/web-service/target/package</packageDirectory> <packageDirectory>${project.basedir}/web-service/target/package</packageDirectory>
<!-- Vert.x 4.5.24 已包含安全修复,无需单独指定 Netty 版本 --> <!-- Vert.x 4.5.27 包含安全修复,无需单独指定 Netty 版本 -->
<vertx.version>4.5.24</vertx.version> <vertx.version>4.5.27</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version> <org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.38</lombok.version> <lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.16</slf4j.version> <slf4j.version>2.0.16</slf4j.version>
<commons-lang3.version>3.18.0</commons-lang3.version> <commons-lang3.version>3.18.0</commons-lang3.version>
<commons-beanutils2.version>2.0.0</commons-beanutils2.version> <commons-beanutils2.version>2.0.0</commons-beanutils2.version>
<parserVersion>10.2.5</parserVersion>
<jackson.version>2.18.6</jackson.version> <jackson.version>2.18.6</jackson.version>
<!-- Logback 最新稳定版 --> <!-- Logback 最新稳定版 -->
<logback.version>1.5.18</logback.version> <logback.version>1.5.32</logback.version>
<junit.version>4.13.2</junit.version> <junit.version>4.13.2</junit.version>
</properties> </properties>
@@ -74,7 +75,7 @@
<dependency> <dependency>
<groupId>cn.qaiu</groupId> <groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId> <artifactId>parser</artifactId>
<version>10.2.5</version> <version>${parserVersion}</version>
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@@ -5,15 +5,15 @@
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"dev": "vue-cli-service serve", "dev": "vue-cli-service serve",
"build": "vue-cli-service build && node scripts/compress-vs.js", "build": "node scripts/sync-version.js && vue-cli-service build && node scripts/compress-vs.js",
"build:no-compress": "vue-cli-service build", "build:no-compress": "node scripts/sync-version.js && vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@monaco-editor/loader": "^1.4.0", "@monaco-editor/loader": "^1.4.0",
"@vueuse/core": "^11.2.0", "@vueuse/core": "^11.2.0",
"axios": "1.13.5", "axios": "1.16.1",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",

View File

@@ -10,7 +10,7 @@
<meta name="description" <meta name="description"
content="Netdisk fast download 网盘直链解析工具"> content="Netdisk fast download 网盘直链解析工具">
<!-- Font Awesome 图标库 - 使用国内CDN --> <!-- Font Awesome 图标库 - 使用国内CDN -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 迅雷 JS-SDK --> <!-- 迅雷 JS-SDK -->
<script src="//open.thunderurl.com/thunder-link.js"></script> <script src="//open.thunderurl.com/thunder-link.js"></script>
<style> <style>

View File

@@ -0,0 +1,23 @@
const fs = require('fs');
const path = require('path');
const pomPath = path.resolve(__dirname, '../../pom.xml');
const pkgPath = path.resolve(__dirname, '../package.json');
const pomContent = fs.readFileSync(pomPath, 'utf-8');
const match = pomContent.match(/<revision>([^<]+)<\/revision>/);
if (!match) {
console.error('sync-version: <revision> not found in root pom.xml');
process.exit(1);
}
const version = match[1];
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
if (pkg.version === version) {
console.log(`sync-version: package.json already at ${version}`);
process.exit(0);
}
pkg.version = version;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
console.log(`sync-version: package.json ${pkg.version} -> ${version}`);

View File

@@ -36,7 +36,6 @@ if (item) {
const darkMode = ref(item) const darkMode = ref(item)
watch(darkMode, (newValue) => { watch(darkMode, (newValue) => {
console.log(`darkMode: ${newValue}`)
window.localStorage.setItem("darkMode", newValue); window.localStorage.setItem("darkMode", newValue);
// 发射主题变化事件 // 发射主题变化事件

View File

@@ -190,6 +190,14 @@
> >
<i class="fas fa-paper-plane"></i> 发送到下载器 <i class="fas fa-paper-plane"></i> 发送到下载器
</el-button> </el-button>
<el-button
v-if="selectedNode.parserUrl"
size="small"
@click="copyDirectLink(selectedNode)"
:loading="copyLinkLoading"
>
<i class="fas fa-link"></i> 复制直链
</el-button>
</div> </div>
</div> </div>
<div v-else class="file-detail-empty"> <div v-else class="file-detail-empty">
@@ -258,6 +266,14 @@
> >
<i class="fas fa-paper-plane"></i> 发送到下载器 <i class="fas fa-paper-plane"></i> 发送到下载器
</el-button> </el-button>
<el-button
v-if="selectedNode.parserUrl"
size="small"
@click="copyDirectLink(selectedNode)"
:loading="copyLinkLoading"
>
<i class="fas fa-link"></i> 复制直链
</el-button>
</div> </div>
</div> </div>
<div v-else class="file-detail-empty"> <div v-else class="file-detail-empty">
@@ -324,6 +340,14 @@
> >
发送到下载器 发送到下载器
</el-button> </el-button>
<el-button
v-if="selectedFile && selectedFile.parserUrl"
@click="copyDirectLink(selectedFile)"
style="margin-left: 8px;"
:loading="copyLinkLoading"
>
复制直链
</el-button>
</span> </span>
</el-dialog> </el-dialog>
<div v-if="isPreviewing" class="preview-mask"> <div v-if="isPreviewing" class="preview-mask">
@@ -391,6 +415,7 @@ export default {
downloadInfo: null, downloadInfo: null,
downloadLoading: false, downloadLoading: false,
singleSendLoading: false, singleSendLoading: false,
copyLinkLoading: false,
treeProps: { treeProps: {
label: 'fileName', label: 'fileName',
children: 'children', children: 'children',
@@ -462,10 +487,6 @@ export default {
} }
return `${baseUrl}?${params.toString()}` return `${baseUrl}?${params.toString()}`
}, },
// 文件树与窗格同源:直接返回当前目录数据
buildTree(list) {
return list || []
},
// 懒加载子节点 // 懒加载子节点
loadNode(node, resolve) { loadNode(node, resolve) {
if (node.level === 0) { if (node.level === 0) {
@@ -479,9 +500,14 @@ export default {
})) }))
resolve(children) resolve(children)
} else { } else {
this.$message.error(res.data.msg || '获取子节点失败')
resolve([]) resolve([])
} }
}).catch(() => resolve([])) }).catch(err => {
const msg = err.response?.data?.msg || err.message
if (msg) this.$message.error(msg)
resolve([])
})
} else { } else {
resolve([]) resolve([])
} }
@@ -491,7 +517,6 @@ export default {
}, },
// 处理文件点击 // 处理文件点击
handleFileClick(file) { handleFileClick(file) {
console.log('点击文件', file, this.viewMode)
if (file.fileType === 'folder') { if (file.fileType === 'folder') {
this.enterFolder(file) this.enterFolder(file)
} else if (this.viewMode === 'pane') { } else if (this.viewMode === 'pane') {
@@ -520,7 +545,8 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('进入文件夹失败:', error) console.error('进入文件夹失败:', error)
this.$message.error('进入文件夹失败') const msg = error.response?.data?.msg || error.message || '进入文件夹失败'
this.$message.error(msg)
} finally { } finally {
this.loading = false this.loading = false
} }
@@ -551,7 +577,8 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('加载目录失败:', error) console.error('加载目录失败:', error)
this.$message.error('加载目录失败') const msg = error.response?.data?.msg || error.message || '加载目录失败'
this.$message.error(msg)
} finally { } finally {
this.loading = false this.loading = false
} }
@@ -649,7 +676,8 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('获取下载信息失败:', error) console.error('获取下载信息失败:', error)
this.$message.error('获取下载信息失败,尝试直接下载') const msg = error.response?.data?.msg || '获取下载信息失败,尝试直接下载'
this.$message.error(msg)
this.downloadFile(file) this.downloadFile(file)
} finally { } finally {
this.downloadLoading = false this.downloadLoading = false
@@ -735,7 +763,8 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('发送到下载器失败:', error) console.error('发送到下载器失败:', error)
this.$message.error('发送到下载器失败: ' + error.message) const msg = error.response?.data?.msg || error.message || '发送到下载器失败'
this.$message.error(msg)
} finally { } finally {
this.singleSendLoading = false this.singleSendLoading = false
} }
@@ -744,6 +773,32 @@ export default {
this.fileDialogVisible = false this.fileDialogVisible = false
this.selectedFile = null 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() { closePreview() {
this.isPreviewing = false this.isPreviewing = false
this.previewUrl = '' this.previewUrl = ''
@@ -802,7 +857,7 @@ export default {
this.toggleFileSelect(file) this.toggleFileSelect(file)
}, },
selectAll() { selectAll() {
this.selectedFiles = this.currentFileList.filter(f => f.fileType !== 'folder') this.selectedFiles = this.currentFileList.filter(f => f.fileType !== 'folder' && f.parserUrl)
}, },
deselectAll() { deselectAll() {
this.selectedFiles = [] this.selectedFiles = []

View File

@@ -1,125 +0,0 @@
import axios from 'axios'
// 创建 axios 实例
const api = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:6400',
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
api.interceptors.request.use(
config => {
// 可以在这里添加认证token等
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
response => {
return response.data
},
error => {
console.error('API请求错误:', error)
if (error.response) {
// 服务器返回错误状态码
const message = error.response.data?.message || error.response.data?.error || '服务器错误'
return Promise.reject(new Error(message))
} else if (error.request) {
// 网络错误
return Promise.reject(new Error('网络连接失败,请检查网络设置'))
} else {
// 其他错误
return Promise.reject(new Error(error.message || '请求失败'))
}
}
)
// 客户端链接 API
export const clientLinksApi = {
/**
* 获取所有客户端下载链接
* @param {string} shareUrl - 分享链接
* @param {string} password - 提取码(可选)
* @returns {Promise} 客户端链接响应
*/
async getClientLinks(shareUrl, password = '') {
const params = new URLSearchParams()
params.append('url', shareUrl)
if (password) {
params.append('pwd', password)
}
return await api.get(`/v2/clientLinks?${params.toString()}`)
},
/**
* 获取指定类型的客户端下载链接
* @param {string} shareUrl - 分享链接
* @param {string} password - 提取码(可选)
* @param {string} clientType - 客户端类型
* @returns {Promise} 指定类型的客户端链接
*/
async getClientLink(shareUrl, password = '', clientType) {
const params = new URLSearchParams()
params.append('url', shareUrl)
if (password) {
params.append('pwd', password)
}
params.append('clientType', clientType)
return await api.get(`/v2/clientLink?${params.toString()}`)
}
}
// 其他 API如果需要的话
export const parserApi = {
/**
* 解析分享链接
* @param {string} shareUrl - 分享链接
* @param {string} password - 提取码(可选)
* @returns {Promise} 解析结果
*/
async parseLink(shareUrl, password = '') {
const params = new URLSearchParams()
params.append('url', shareUrl)
if (password) {
params.append('pwd', password)
}
return await api.get(`/v2/linkInfo?${params.toString()}`)
},
/**
* 获取文件列表
* @param {string} shareUrl - 分享链接
* @param {string} password - 提取码(可选)
* @param {string} dirId - 目录ID可选
* @param {string} uuid - UUID可选
* @returns {Promise} 文件列表
*/
async getFileList(shareUrl, password = '', dirId = '', uuid = '') {
const params = new URLSearchParams()
params.append('url', shareUrl)
if (password) {
params.append('pwd', password)
}
if (dirId) {
params.append('dirId', dirId)
}
if (uuid) {
params.append('uuid', uuid)
}
return await api.get(`/v2/getFileList?${params.toString()}`)
}
}
export default api

View File

@@ -0,0 +1,6 @@
/**
* 前端全局常量
*/
/** 预览服务基础 URL */
export const PREVIEW_BASE_URL = 'https://nfd-parser.github.io/nfd-preview/preview.html?src='

View File

@@ -410,7 +410,6 @@ function addThunderDownload(tasks, config) {
if (userAgent) taskParam.userAgent = userAgent if (userAgent) taskParam.userAgent = userAgent
taskParam.threadCount = '1' taskParam.threadCount = '1'
console.log('[Thunder SDK] newTask params:', JSON.stringify(taskParam))
window.thunderLink.newTask(taskParam) window.thunderLink.newTask(taskParam)
return Promise.resolve('thunder-ok') return Promise.resolve('thunder-ok')
} }

View File

@@ -313,7 +313,6 @@ export async function loadTypesFromApi(monaco) {
cachedContent, cachedContent,
'file:///types.js' 'file:///types.js'
); );
console.log('从缓存加载types.js成功');
// 异步更新缓存 // 异步更新缓存
updateTypesJsCache(); updateTypesJsCache();
return; return;
@@ -334,7 +333,6 @@ export async function loadTypesFromApi(monaco) {
typesJsContent, typesJsContent,
'file:///types.js' 'file:///types.js'
); );
console.log('加载types.js成功并已缓存');
} }
} catch (error) { } catch (error) {
console.warn('加载types.js失败使用内置类型定义:', error); console.warn('加载types.js失败使用内置类型定义:', error);
@@ -350,7 +348,6 @@ async function updateTypesJsCache() {
if (response.ok) { if (response.ok) {
const typesJsContent = await response.text(); const typesJsContent = await response.text();
localStorage.setItem('playground_types_js', typesJsContent); localStorage.setItem('playground_types_js', typesJsContent);
console.log('types.js缓存已更新');
} }
} catch (error) { } catch (error) {
console.warn('更新types.js缓存失败:', error); console.warn('更新types.js缓存失败:', error);

View File

@@ -293,24 +293,6 @@ export default {
return clientConfig[type]?.downloadUrl || '#' return clientConfig[type]?.downloadUrl || '#'
} }
// 判断是否应该显示下载客户端按钮
const shouldShowDownloadButton = (type) => {
const os = getOSInfo()
switch (type) {
case 'CURL':
// cURL 在 Windows 上可能需要安装
return os === 'windows'
case 'ARIA2':
// Aria2 需要手动安装
return true
case 'THUNDER':
// 迅雷主要在 Windows 上使用
return os === 'windows'
default:
return false
}
}
// 获取操作系统信息 // 获取操作系统信息
const getOSInfo = () => { const getOSInfo = () => {
const userAgent = navigator.userAgent.toLowerCase() const userAgent = navigator.userAgent.toLowerCase()
@@ -369,7 +351,7 @@ export default {
copyToClipboard(link) copyToClipboard(link)
return return
} }
window.open(link, '_blank') window.open(link, '_blank', 'noopener,noreferrer')
ElMessage.success('正在唤起迅雷下载') ElMessage.success('正在唤起迅雷下载')
break break
@@ -383,13 +365,6 @@ export default {
} }
} }
// 下载客户端
const downloadClient = (type) => {
const url = getClientDownloadUrl(type)
window.open(url, '_blank')
ElMessage.success(`正在跳转到 ${getClientDisplayName(type)} 下载页面`)
}
// 格式化文件大小 // 格式化文件大小
const formatFileSize = (bytes) => { const formatFileSize = (bytes) => {
if (!bytes) return '未知' if (!bytes) return '未知'
@@ -440,9 +415,7 @@ export default {
getTextareaRows, getTextareaRows,
goBack, goBack,
getClientLogo, getClientLogo,
downloadClient,
handleImageError, handleImageError,
shouldShowDownloadButton,
getClientSupportsCookie, getClientSupportsCookie,
goToAuthConfig goToAuthConfig
} }

View File

@@ -19,11 +19,11 @@
</el-dialog> --> </el-dialog> -->
<!-- 顶部反馈栏小号灰色无红边框 --> <!-- 顶部反馈栏小号灰色无红边框 -->
<div class="feedback-bar"> <div class="feedback-bar">
<a href="https://github.com/qaiu/netdisk-fast-download/issues" target="_blank" rel="noopener" class="feedback-link mini"> <a :href="githubRepoUrl + '/issues'" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-bug feedback-icon"></i> <i class="fas fa-bug feedback-icon"></i>
反馈 反馈
</a> </a>
<a href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="noopener" class="feedback-link mini"> <a :href="githubRepoUrl" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fab fa-github feedback-icon"></i> <i class="fab fa-github feedback-icon"></i>
源码 源码
</a> </a>
@@ -73,9 +73,9 @@
</div> </div>
<!-- 项目简介移到卡片内 --> <!-- 项目简介移到卡片内 -->
<div class="project-intro"> <div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.3.0</div> <div class="intro-title">NFD网盘直链解析 {{ projectVersion }}</div>
<div class="intro-desc"> <div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘iCloud移动云空间联想乐云QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div> <div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘iCloud移动云空间联想乐云QQ闪传等 <el-link style="color:#606cf5" :href="githubRepoUrl + '?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5'" target="_blank"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div> <div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
</div> </div>
</div> </div>
@@ -90,7 +90,7 @@
<!-- 开关按钮控制是否自动读取剪切板 --> <!-- 开关按钮控制是否自动读取剪切板 -->
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch> <el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url"> <el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url" @paste="onPaste">
<template #prepend>分享链接</template> <template #prepend>分享链接</template>
<template #append v-if="!autoReadClipboard"> <template #append v-if="!autoReadClipboard">
<el-button @click="getPaste(true)">读取剪切板</el-button> <el-button @click="getPaste(true)">读取剪切板</el-button>
@@ -595,11 +595,10 @@ import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { playgroundApi } from '@/utils/playgroundApi' import { playgroundApi } from '@/utils/playgroundApi'
import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService' import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService'
import { PREVIEW_BASE_URL } from '@/utils/constants'
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
export default { export default {
name: 'App', name: 'Home',
components: { DarkMode, DirectoryTree, DownloadDialog }, components: { DarkMode, DirectoryTree, DownloadDialog },
mixins: [fileTypeUtils], mixins: [fileTypeUtils],
data() { data() {
@@ -617,7 +616,7 @@ export default {
parseResult: {}, parseResult: {},
downloadUrl: null, downloadUrl: null,
directLink: '', directLink: '',
previewBaseUrl, previewBaseUrl: PREVIEW_BASE_URL,
// 功能结果 // 功能结果
markdownText: '', markdownText: '',
@@ -714,6 +713,12 @@ export default {
} }
}, },
computed: { computed: {
githubRepoUrl() {
return process.env.VUE_APP_GITHUB_REPO_URL
},
projectVersion() {
return process.env.VUE_APP_VERSION || '0.0.0'
},
// 检查是否配置了认证信息(针对当前链接的网盘类型) // 检查是否配置了认证信息(针对当前链接的网盘类型)
hasAuthConfig() { hasAuthConfig() {
const panType = this.getCurrentPanType() const panType = this.getCurrentPanType()
@@ -959,18 +964,16 @@ export default {
// 优先使用个人配置 // 优先使用个人配置
if (this.allAuthConfigs[panType]) { if (this.allAuthConfigs[panType]) {
config = this.allAuthConfigs[panType] config = this.allAuthConfigs[panType]
console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`)
} else { } else {
// 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth // 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth
try { try {
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } }) const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
const encryptedAuth = response.data?.data?.encryptedAuth const encryptedAuth = response.data?.data?.encryptedAuth
if (encryptedAuth) { if (encryptedAuth) {
console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`)
return encryptedAuth return encryptedAuth
} }
} catch (e) { } catch (e) {
console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`) // no available donated account
} }
return '' return ''
} }
@@ -1091,17 +1094,45 @@ export default {
} }
}, },
// 识别并转换短链输入(如 lz:shareKey@pwd // 识别并转换短链输入(如 lz:shareKey@pwd,或从文本中提取链接
normalizeShortcutInput() { normalizeShortcutInput() {
const shortInfo = this.expandShortFormat(this.link) if (!this.link) return
if (!shortInfo) return const trimmed = this.link.trim()
if (!trimmed) return
this.link = shortInfo.link // 已经是直接链接,跳过
if (!this.password && shortInfo.pwd) { if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return
this.password = shortInfo.pwd
// 尝试短格式
const shortInfo = this.expandShortFormat(trimmed)
if (shortInfo) {
this.link = shortInfo.link
if (!this.password && shortInfo.pwd) {
this.password = shortInfo.pwd
}
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
this.updateDirectLink()
return
} }
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
this.updateDirectLink() // 从文本中自动提取链接
const linkInfo = parserUrl.parseLink(trimmed)
if (linkInfo.link) {
this.link = linkInfo.link
const pwd = parserUrl.parsePwd(trimmed)
if (!this.password && pwd) {
this.password = pwd
}
this.$message.success(`已从文本中识别到 ${linkInfo.name} 分享链接`)
this.updateDirectLink()
}
},
// 粘贴事件:从粘贴的文本中自动提取链接
onPaste(e) {
this.$nextTick(() => {
this.normalizeShortcutInput()
})
}, },
// 清除结果 // 清除结果
@@ -1131,12 +1162,19 @@ export default {
// this.$message.success(response.data.msg || '操作成功') // this.$message.success(response.data.msg || '操作成功')
return response.data return response.data
} else { } else {
// 在页面右下角显示一个查看详情”按钮 可以查看原json // 在页面右下角显示一个查看详情”按钮 可以查看原json
this.errorDetail = response?.data this.errorDetail = response?.data
this.errorButtonVisible = true this.errorButtonVisible = true
throw new Error(response.data.msg || '操作失败') throw new Error(response.data.msg || '操作失败')
} }
} catch (error) { } catch (error) {
// HTTP 非2xx时从响应体中提取后端返回的错误信息
if (error.response?.data?.msg) {
this.errorDetail = error.response.data
this.errorButtonVisible = true
this.$message.error(error.response.data.msg)
throw new Error(error.response.data.msg)
}
this.$message.error(error.message || '网络错误') this.$message.error(error.message || '网络错误')
throw error throw error
} finally { } finally {
@@ -1309,7 +1347,7 @@ export default {
// 文件点击处理 // 文件点击处理
handleFileClick(file) { handleFileClick(file) {
if (file.parserUrl) { if (file.parserUrl) {
window.open(file.parserUrl, '_blank') window.open(file.parserUrl, '_blank', 'noopener,noreferrer')
} else { } else {
this.$message.warning('该文件暂无下载链接') this.$message.warning('该文件暂无下载链接')
} }
@@ -1319,7 +1357,6 @@ export default {
async getPaste(isManual = false) { async getPaste(isManual = false) {
try { try {
const text = await navigator.clipboard.readText() const text = await navigator.clipboard.readText()
console.log('获取到的文本内容是:', text)
const shortInfo = this.expandShortFormat(text) const shortInfo = this.expandShortFormat(text)
if (shortInfo) { if (shortInfo) {
@@ -1364,7 +1401,9 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('读取剪切板失败:', error) console.error('读取剪切板失败:', error)
this.$message.error('读取剪切板失败,请检查浏览器权限') if (isManual) {
this.$message.warning('读取剪切板失败,请手动粘贴链接到输入框')
}
} }
}, },
@@ -1439,7 +1478,7 @@ export default {
错误信息:${JSON.stringify(this.errorDetail, null, 2)}`; 错误信息:${JSON.stringify(this.errorDetail, null, 2)}`;
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
this.$message.success('已复制分享信息和错误详情'); this.$message.success('已复制分享信息和错误详情');
window.open('https://github.com/qaiu/netdisk-fast-download/issues/new', '_blank'); window.open(`${this.githubRepoUrl}/issues/new`, '_blank', 'noopener,noreferrer');
}).catch(() => { }).catch(() => {
this.$message.error('复制失败'); this.$message.error('复制失败');
}); });
@@ -1786,12 +1825,13 @@ export default {
} }
// 监听窗口焦点事件 // 监听窗口焦点事件
window.addEventListener('focus', () => { this._onFocusHandler = () => {
if (this.autoReadClipboard) { if (this.autoReadClipboard) {
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次 this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
this.getPaste() this.getPaste()
} }
}) }
window.addEventListener('focus', this._onFocusHandler)
// 首次打开页面弹出风险提示 // 首次打开页面弹出风险提示
if (!window.localStorage.getItem('nfd_risk_ack')) { if (!window.localStorage.getItem('nfd_risk_ack')) {
@@ -1799,6 +1839,12 @@ export default {
} }
}, },
beforeUnmount() {
if (this._onFocusHandler) {
window.removeEventListener('focus', this._onFocusHandler)
}
},
watch: { watch: {
downloadUrl(val) { downloadUrl(val) {
if (!val) { if (!val) {

View File

@@ -653,22 +653,22 @@
<p>更多详细信息请参考 GitHub 仓库文档</p> <p>更多详细信息请参考 GitHub 仓库文档</p>
<ul> <ul>
<li> <li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/JAVASCRIPT_PARSER_GUIDE.md" target="_blank" rel="noopener noreferrer"> <a :href="githubRepoUrl + '/blob/main/parser/doc/JAVASCRIPT_PARSER_GUIDE.md'" target="_blank" rel="noopener noreferrer">
JavaScript 解析器开发指南 JavaScript 解析器开发指南
</a> </a>
</li> </li>
<li> <li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/CUSTOM_PARSER_GUIDE.md" target="_blank" rel="noopener noreferrer"> <a :href="githubRepoUrl + '/blob/main/parser/doc/CUSTOM_PARSER_GUIDE.md'" target="_blank" rel="noopener noreferrer">
自定义解析器扩展指南 自定义解析器扩展指南
</a> </a>
</li> </li>
<li> <li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/CUSTOM_PARSER_QUICKSTART.md" target="_blank" rel="noopener noreferrer"> <a :href="githubRepoUrl + '/blob/main/parser/doc/CUSTOM_PARSER_QUICKSTART.md'" target="_blank" rel="noopener noreferrer">
快速开始教程 快速开始教程
</a> </a>
</li> </li>
<li> <li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/README.md" target="_blank" rel="noopener noreferrer"> <a :href="githubRepoUrl + '/blob/main/parser/README.md'" target="_blank" rel="noopener noreferrer">
解析器模块文档 解析器模块文档
</a> </a>
</li> </li>
@@ -858,6 +858,7 @@ export default {
}, },
setup() { setup() {
const router = useRouter(); const router = useRouter();
const githubRepoUrl = process.env.VUE_APP_GITHUB_REPO_URL;
// 语言常量 // 语言常量
const LANGUAGE = { const LANGUAGE = {
@@ -1178,7 +1179,7 @@ function parseById(shareLinkInfo, http, logger) {
// 新窗口打开首页 // 新窗口打开首页
const goHomeInNewWindow = () => { const goHomeInNewWindow = () => {
window.open('/', '_blank'); window.open('/', '_blank', 'noopener,noreferrer');
}; };
// 检查是否有未保存的文件 // 检查是否有未保存的文件
@@ -1758,7 +1759,6 @@ function parseFileList(shareLinkInfo, http, logger) {
testParams.value.method testParams.value.method
); );
console.log('测试结果:', result);
testResult.value = result; testResult.value = result;
// 将日志添加到控制台 // 将日志添加到控制台
@@ -1820,10 +1820,8 @@ function parseFileList(shareLinkInfo, http, logger) {
loadingList.value = true; loadingList.value = true;
try { try {
const result = await playgroundApi.getParserList(); const result = await playgroundApi.getParserList();
console.log('获取解析器列表响应:', result);
// 检查响应格式 // 检查响应格式
if (result.code === 200 || result.success) { if (result.code === 200 || result.success) {
console.log('列表数据:', result.data);
parserList.value = result.data || []; parserList.value = result.data || [];
} else if (result.data && Array.isArray(result.data)) { } else if (result.data && Array.isArray(result.data)) {
// 如果data直接是数组 // 如果data直接是数组
@@ -1857,7 +1855,6 @@ function parseFileList(shareLinkInfo, http, logger) {
try { try {
const codeToPublish = currentCode.value; const codeToPublish = currentCode.value;
const result = await playgroundApi.saveParser(codeToPublish); const result = await playgroundApi.saveParser(codeToPublish);
console.log('保存解析器响应:', result);
// 检查响应格式 // 检查响应格式
if (result.code === 200 || result.success) { if (result.code === 200 || result.success) {
// 从响应或代码中提取type信息 // 从响应或代码中提取type信息
@@ -2223,6 +2220,8 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
}, 100); }, 100);
}; };
let themeObserver = null;
onMounted(async () => { onMounted(async () => {
// 初始化移动端检测 // 初始化移动端检测
updateIsMobile(); updateIsMobile();
@@ -2249,10 +2248,10 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
const html = document.documentElement; const html = document.documentElement;
if (html && html.classList) { if (html && html.classList) {
try { try {
const observer = new MutationObserver(() => { themeObserver = new MutationObserver(() => {
checkDarkMode(); checkDarkMode();
}); });
observer.observe(html, { themeObserver.observe(html, {
attributes: true, attributes: true,
attributeFilter: ['class', 'data-theme'] attributeFilter: ['class', 'data-theme']
}); });
@@ -2269,9 +2268,11 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
window.removeEventListener('resize', updateIsMobile); window.removeEventListener('resize', updateIsMobile);
// 移除页面关闭/刷新前的提示 // 移除页面关闭/刷新前的提示
window.removeEventListener('beforeunload', handleBeforeUnload); window.removeEventListener('beforeunload', handleBeforeUnload);
themeObserver?.disconnect();
}); });
return { return {
githubRepoUrl,
LANGUAGE, LANGUAGE,
editorRef, editorRef,
jsCode, jsCode,

View File

@@ -32,7 +32,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import fileTypeUtils from '@/utils/fileTypeUtils' import fileTypeUtils from '@/utils/fileTypeUtils'
import { previewBaseUrl } from '@/views/Home.vue' import { PREVIEW_BASE_URL } from '@/utils/constants'
export default { export default {
name: 'ShowFile', name: 'ShowFile',
@@ -44,7 +44,7 @@ export default {
downloadUrl: '', downloadUrl: '',
shareUrl: '', // 添加原始分享链接 shareUrl: '', // 添加原始分享链接
fileTypeUtils, fileTypeUtils,
previewBaseUrl previewBaseUrl: PREVIEW_BASE_URL
} }
}, },
methods: { methods: {
@@ -73,7 +73,7 @@ export default {
this.parseResult = res.data this.parseResult = res.data
this.downloadUrl = res.data.data?.directLink this.downloadUrl = res.data.data?.directLink
} catch (e) { } catch (e) {
this.error = '解析失败' this.error = e.response?.data?.msg || e.response?.data?.error || '解析失败'
} finally { } finally {
this.loading = false this.loading = false
} }

View File

@@ -55,7 +55,7 @@ export default {
const res = await axios.get('/v2/getFileList', { params: { url: this.url } }) const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
this.directoryData = res.data.data || [] this.directoryData = res.data.data || []
} catch (e) { } catch (e) {
this.error = '目录解析失败' this.error = e.response?.data?.msg || e.response?.data?.error || '目录解析失败'
} finally { } finally {
this.loading = false this.loading = false
} }

View File

@@ -1,10 +1,34 @@
const path = require("path"); const path = require("path");
const { execSync } = require("child_process");
const webpack = require("webpack");
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, dir) return path.join(__dirname, dir)
} }
// 从 git remote origin 自动识别 GitHub 仓库地址
function getGitHubRepoUrl() {
try {
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8', cwd: path.resolve(__dirname, '..') }).trim();
const match = remoteUrl.match(/github\.com[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
if (match) return `https://github.com/${match[1]}`;
} catch (e) {}
return 'https://github.com/qaiu/netdisk-fast-download';
}
// 从根 pom.xml 读取项目版本号(单一版本来源)
function getProjectVersion() {
try {
const pomContent = require('fs').readFileSync(path.resolve(__dirname, '../pom.xml'), 'utf-8');
const match = pomContent.match(/<revision>([^<]+)<\/revision>/);
if (match) return match[1];
} catch (e) {}
return require('./package.json').version;
}
const PROJECT_VERSION = getProjectVersion();
const GITHUB_REPO_URL = getGitHubRepoUrl();
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin'); const FileManagerPlugin = require('filemanager-webpack-plugin');
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin'); const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
@@ -55,6 +79,10 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({
'process.env.VUE_APP_GITHUB_REPO_URL': JSON.stringify(GITHUB_REPO_URL),
'process.env.VUE_APP_VERSION': JSON.stringify(PROJECT_VERSION)
}),
new MonacoEditorPlugin({ new MonacoEditorPlugin({
languages: ['javascript', 'typescript', 'json'], languages: ['javascript', 'typescript', 'json'],
features: ['coreCommands', 'find', 'format', 'suggest', 'quickCommand'], features: ['coreCommands', 'find', 'format', 'suggest', 'quickCommand'],

View File

@@ -36,6 +36,20 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
public class AppMain { public class AppMain {
public static void main(String[] args) { public static void main(String[] args) {
// 先注册 ShutdownHookJVM 逆序执行,先注册的后执行)
// 确保关闭顺序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 // start
Deploy.instance().start(args, AppMain::exec); Deploy.instance().start(args, AppMain::exec);
} }
@@ -67,6 +81,9 @@ public class AppMain {
loadPlaygroundParsers(); loadPlaygroundParsers();
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName"); 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); System.out.println("启动成功: \n本地服务地址: " + addr);
}); });
}); });

View File

@@ -89,7 +89,7 @@ public class CacheManager {
} else { } else {
LOGGER.warn("No rows affected when updating cache link info for shareKey: {}", cacheLinkInfo.getShareKey()); 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) { if (cacheLinkInfo.getFileInfo() != null) {
String sql2 = """ String sql2 = """
@@ -123,7 +123,7 @@ public class CacheManager {
} else { } else {
LOGGER.warn("No rows affected when inserting pan file info for shareKey: {}", cacheLinkInfo.getShareKey()); 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 -> { getShareKeyTotal(shareKey, fieldLower).onSuccess(total -> {
Integer newTotal = (total == null ? 0 : total) + 1; Integer newTotal = (total == null ? 0 : total) + 1;
Map<String, Object> 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) SqlTemplate.forUpdate(jdbcPool, sql)
.execute(new HashMap<>() {{ .execute(updateParams)
put("panType", getShareType(shareKey));
put("shareKey", shareKey);
put("total", newTotal);
put("ts", System.currentTimeMillis());
}})
.onSuccess(res -> promise.complete(res.rowCount())) .onSuccess(res -> promise.complete(res.rowCount()))
.onFailure(e->{ .onFailure(e->{
promise.fail(e); promise.fail(e);
LOGGER.error("updateTotalByField: ", e); LOGGER.error("updateTotalByField: ", e);
}); });
}).onFailure(e -> {
promise.fail(e);
LOGGER.error("getShareKeyTotal in updateTotalByField: ", e);
}); });
return promise.future(); return promise.future();
} }
@@ -229,9 +232,17 @@ public class CacheManager {
* 注册定时清理过期缓存任务(每小时执行一次) * 注册定时清理过期缓存任务(每小时执行一次)
* 应在应用启动后调用 * 应在应用启动后调用
*/ */
private static volatile boolean cleanupRegistered = false;
public static void registerPeriodicCleanup() { public static void registerPeriodicCleanup() {
if (cleanupRegistered) return;
try { try {
io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance(); 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 -> { vertx.setPeriodic(3600_000, 3600_000, id -> {
try { try {
new CacheManager().cleanupExpiredCache(); new CacheManager().cleanupExpiredCache();
@@ -262,10 +273,9 @@ public class CacheManager {
.onSuccess(res -> { .onSuccess(res -> {
if(res.iterator().hasNext()) { if(res.iterator().hasNext()) {
JsonObject next = res.iterator().next(); JsonObject next = res.iterator().next();
Map<String, Integer> resp = new HashMap<>(){{ Map<String, Integer> resp = new HashMap<>();
put("hit_total" ,next.getInteger("hit_total")); resp.put("hit_total", next.getInteger("hit_total"));
put("parser_total" ,next.getInteger("parser_total")); resp.put("parser_total", next.getInteger("parser_total"));
}};
promise.complete(resp); promise.complete(resp);
} else { } else {
promise.complete(); promise.complete();

View File

@@ -48,6 +48,6 @@ public class LogStatistics implements AfterInterceptor {
.execute(info) .execute(info)
.onSuccess(res -> { .onSuccess(res -> {
log.info("inserted log: id={}, path={}, code={}", info.getId(), info.getPath(), info.getCode()); log.info("inserted log: id={}, path={}, code={}", info.getId(), info.getPath(), info.getCode());
}).onFailure(Throwable::printStackTrace); }).onFailure(e -> log.error("插入解析日志失败: id={}", info.getId(), e));
} }
} }

View File

@@ -10,6 +10,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j @Slf4j
public class RateLimiter { public class RateLimiter {
@@ -51,12 +52,12 @@ public class RateLimiter {
return new RequestInfo(1, currentTime); return new RequestInfo(1, currentTime);
} else { } else {
// 增加计数器 // 增加计数器
requestInfo.count++; requestInfo.count.incrementAndGet();
return requestInfo; return requestInfo;
} }
}); });
if (info.count > MAX_REQUESTS) { if (info.count.get() > MAX_REQUESTS) {
// 超过限制 // 超过限制
// 计算剩余时间 // 计算剩余时间
long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp); long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp);
@@ -71,11 +72,11 @@ public class RateLimiter {
} }
private static class RequestInfo { private static class RequestInfo {
volatile int count; final AtomicInteger count;
volatile long timestamp; volatile long timestamp;
RequestInfo(int count, long time) { RequestInfo(int count, long time) {
this.count = count; this.count = new AtomicInteger(count);
this.timestamp = time; this.timestamp = time;
} }
} }

View File

@@ -7,6 +7,7 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -93,9 +94,10 @@ public class JwtUtil {
String encodedPayload = parts[1]; String encodedPayload = parts[1];
String signature = parts[2]; String signature = parts[2];
// 验证签名 // 验证签名(使用常量时间比较防止时序攻击)
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY); String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
if (!expectedSignature.equals(signature)) { if (!MessageDigest.isEqual(expectedSignature.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8))) {
return false; return false;
} }

View File

@@ -80,8 +80,10 @@ public class PasswordUtil {
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8)); byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash); String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash);
// 比较计算出的哈希值和存储的哈希值 // 比较计算出的哈希值和存储的哈希值(使用常量时间比较防止时序攻击)
return hashBase64.equals(calculatedHashBase64); return MessageDigest.isEqual(
hashBase64.getBytes(StandardCharsets.UTF_8),
calculatedHashBase64.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) { } catch (Exception e) {
// 如果发生异常例如格式不正确返回false // 如果发生异常例如格式不正确返回false
return false; return false;

View File

@@ -119,11 +119,17 @@ public class URLParamUtil {
} }
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); 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对象 * @param parserCreate ParserCreate对象
@@ -155,7 +161,13 @@ public class URLParamUtil {
} }
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); 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(); MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap();

View File

@@ -43,14 +43,38 @@ public class ParserApi {
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); 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) @RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
public Future<StatisticsInfo> statisticsInfo() { public Future<StatisticsInfo> statisticsInfo() {
return dbService.getStatisticsInfo(); return dbService.getStatisticsInfo();
} }
private final CacheManager cacheManager = new CacheManager(); private static final CacheManager cacheManager = new CacheManager();
private final ServerApi serverApi = new ServerApi(); private static final ServerApi serverApi = new ServerApi();
@RouteMapping(value = "/linkInfo", method = RouteMethod.GET) @RouteMapping(value = "/linkInfo", method = RouteMethod.GET)
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd, String auth) { public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd, String auth) {
@@ -61,10 +85,11 @@ public class ParserApi {
// 构建链接信息响应,如果有 auth 参数则附加到链接中 // 构建链接信息响应,如果有 auth 参数则附加到链接中
String authSuffix = (auth != null && !auth.isEmpty()) ? "&auth=" + auth : ""; String authSuffix = (auth != null && !auth.isEmpty()) ? "&auth=" + auth : "";
shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request));
LinkInfoResp build = LinkInfoResp.builder() LinkInfoResp build = LinkInfoResp.builder()
.downLink(getDownLink(parserCreate, false) + authSuffix) .downLink(getDownLink(parserCreate, false, request) + authSuffix)
.apiLink(getDownLink(parserCreate, true) + authSuffix) .apiLink(getDownLink(parserCreate, true, request) + authSuffix)
.viewLink(getViewLink(parserCreate) + authSuffix) .viewLink(getViewLink(parserCreate, request) + authSuffix)
.shareLinkInfo(shareLinkInfo).build(); .shareLinkInfo(shareLinkInfo).build();
// 解析次数统计 // 解析次数统计
shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent")); shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent"));
@@ -76,25 +101,23 @@ public class ParserApi {
} }
promise.complete(build); promise.complete(build);
}).onFailure(t->{ }).onFailure(t->{
t.printStackTrace(); log.error("获取统计信息失败", t);
promise.complete(build); promise.complete(build);
}); });
return promise.future(); return promise.future();
} }
private static String getDownLink(ParserCreate create, boolean isJson) { private static String getDownLink(ParserCreate create, boolean isJson, HttpServerRequest request) {
String linkPrefix = getLinkPrefix(request);
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
if (StringUtils.isBlank(linkPrefix)) { if (StringUtils.isBlank(linkPrefix)) {
linkPrefix = "http://127.0.0.1"; linkPrefix = "http://127.0.0.1:" + SharedDataUtil.getJsonConfig("server").getInteger("port", 6400);
} }
// 下载短链前缀 /d // 下载短链前缀 /d
return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix(); return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix();
} }
private static String getViewLink(ParserCreate create) { private static String getViewLink(ParserCreate create, HttpServerRequest request) {
String linkPrefix = getLinkPrefix(request);
String linkPrefix = SharedDataUtil.getJsonStringForServerConfig("domainName");
if (StringUtils.isBlank(linkPrefix)) { if (StringUtils.isBlank(linkPrefix)) {
return ""; return "";
} }
@@ -119,8 +142,9 @@ public class ParserApi {
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) { public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) {
String url = URLParamUtil.parserParams(request); String url = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd); 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("domainName", linkPrefix);
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix);
if (StringUtils.isNotBlank(dirId)) { if (StringUtils.isNotBlank(dirId)) {
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId); parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
} }
@@ -132,7 +156,7 @@ public class ParserApi {
// 目录解析下载文件 // 目录解析下载文件
// @RouteMapping("/getFileDownUrl/:type/:param") // @RouteMapping("/getFileDownUrl/:type/:param")
public Future<String> getFileDownUrl(String type, String param) { public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param) {
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
.setShareLinkInfoPwd("-"); .setShareLinkInfoPwd("-");
@@ -147,17 +171,21 @@ public class ParserApi {
shareLinkInfo.getOtherParam().put("paramJson", new JsonObject(paramStr)); shareLinkInfo.getOtherParam().put("paramJson", new JsonObject(paramStr));
// domainName // domainName
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName"); String linkPrefix = getLinkPrefix(request);
shareLinkInfo.getOtherParam().put("domainName", linkPrefix); shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix);
return parserCreate.createTool().parseById(); return parserCreate.createTool().parseById();
} }
@RouteMapping("/redirectUrl/:type/:param") @RouteMapping("/redirectUrl/:type/:param")
public Future<Void> redirectUrl(HttpServerResponse response, String type, String param) { public Future<Void> redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise(); Promise<Void> promise = Promise.promise();
getFileDownUrl(type, param) getFileDownUrl(request, type, param)
.onSuccess(res -> ResponseUtil.redirect(response, res)) .onSuccess(res -> {
ResponseUtil.redirect(response, res);
promise.complete();
})
.onFailure(t -> promise.fail(t.fillInStackTrace())); .onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future(); return promise.future();
} }
@@ -220,7 +248,7 @@ public class ParserApi {
} }
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL"); String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
new ServerApi().parseJson(request, pwd, null).onSuccess(res -> { serverApi.parseJson(request, pwd, null).onSuccess(res -> {
redirect(response, previewURL, res); redirect(response, previewURL, res);
}).onFailure(e -> { }).onFailure(e -> {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString())); ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
@@ -229,14 +257,15 @@ public class ParserApi {
@RouteMapping("/viewUrl/:type/:param") @RouteMapping("/viewUrl/:type/:param")
public Future<Void> viewUrl(HttpServerResponse response, String type, String param) { public Future<Void> viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise(); Promise<Void> promise = Promise.promise();
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL"); String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
getFileDownUrl(type, param) getFileDownUrl(request, type, param)
.onSuccess(res -> { .onSuccess(res -> {
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8); String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
ResponseUtil.redirect(response, url); ResponseUtil.redirect(response, url);
promise.complete();
}) })
.onFailure(t -> promise.fail(t.fillInStackTrace())); .onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future(); return promise.future();
@@ -269,6 +298,7 @@ public class ParserApi {
String shareUrl = URLParamUtil.parserParams(request); String shareUrl = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd); ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo(); ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
shareLinkInfo.getOtherParam().put("_requestOrigin", getLinkPrefix(request));
// 处理认证参数 // 处理认证参数
if (auth != null && !auth.isEmpty()) { if (auth != null && !auth.isEmpty()) {
@@ -285,6 +315,8 @@ public class ParserApi {
authParam.getExt5()); authParam.getExt5());
log.debug("客户端链接API: 已解码认证参数 authType={}", authParam.getAuthType()); log.debug("客户端链接API: 已解码认证参数 authType={}", authParam.getAuthType());
} }
} else {
URLParamUtil.addParam(parserCreate);
} }
// 使用默认方法解析并生成客户端链接 // 使用默认方法解析并生成客户端链接
@@ -326,6 +358,8 @@ public class ParserApi {
try { try {
String shareUrl = URLParamUtil.parserParams(request); String shareUrl = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd); ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", getLinkPrefix(request));
URLParamUtil.addParam(parserCreate);
// 使用默认方法解析并生成客户端链接 // 使用默认方法解析并生成客户端链接
parserCreate.createTool().parseWithClientLinks() parserCreate.createTool().parseWithClientLinks()

View File

@@ -28,6 +28,7 @@ import java.io.BufferedReader;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@@ -129,8 +130,11 @@ public class PlaygroundApi {
return promise.future(); return promise.future();
} }
// 验证密码 // 验证密码(使用常量时间比较防止时序攻击)
if (config.getPassword().equals(password)) { String storedPassword = config.getPassword();
if (storedPassword != null && MessageDigest.isEqual(
storedPassword.getBytes(StandardCharsets.UTF_8),
password.getBytes(StandardCharsets.UTF_8))) {
String token = config.generateToken(); String token = config.generateToken();
JsonObject tokenData = new JsonObject().put("token", token); JsonObject tokenData = new JsonObject().put("token", token);
promise.complete(JsonResult.data(tokenData).toJsonObject()); promise.complete(JsonResult.data(tokenData).toJsonObject());
@@ -299,7 +303,6 @@ public class PlaygroundApi {
}).onFailure(e -> { }).onFailure(e -> {
long executionTime = System.currentTimeMillis() - startTime; long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage(); String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场执行失败", e); log.error("演练场执行失败", e);
@@ -317,7 +320,6 @@ public class PlaygroundApi {
PlaygroundTestResp response = PlaygroundTestResp.builder() PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false) .success(false)
.error(errorMessage) .error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime) .executionTime(executionTime)
.logs(respLogs) .logs(respLogs)
.build(); .build();
@@ -328,14 +330,12 @@ public class PlaygroundApi {
} catch (Exception e) { } catch (Exception e) {
long executionTime = System.currentTimeMillis() - startTime; long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage(); String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场初始化失败", e); log.error("演练场初始化失败", e);
PlaygroundTestResp response = PlaygroundTestResp.builder() PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false) .success(false)
.error(errorMessage) .error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime) .executionTime(executionTime)
.logs(new ArrayList<>()) .logs(new ArrayList<>())
.build(); .build();
@@ -346,8 +346,7 @@ public class PlaygroundApi {
log.error("解析请求参数失败", e); log.error("解析请求参数失败", e);
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder() promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false) .success(false)
.error("解析请求参数失败: " + e.getMessage()) .error("解析请求参数失败")
.stackTrace(getStackTrace(e))
.build())); .build()));
} }
@@ -696,18 +695,5 @@ public class PlaygroundApi {
} }
return ip; return ip;
} }
/**
* 获取异常堆栈信息
*/
private String getStackTrace(Throwable throwable) {
if (throwable == null) {
return "";
}
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
throwable.printStackTrace(pw);
return sw.toString();
}
} }

View File

@@ -68,7 +68,8 @@ public class ServerApi {
key = keys[0]; key = keys[0];
pwd = keys[1]; pwd = keys[1];
} }
return cacheService.getCachedByShareKeyAndPwd(type, key, pwd, JsonObject.of("UA",request.headers().get("user-agent"))); String origin = resolveOrigin(request);
return cacheService.getCachedByShareKeyAndPwd(type, key, pwd, JsonObject.of("UA",request.headers().get("user-agent"), "_requestOrigin", origin));
} }
@RouteMapping(value = "/:type/:key", method = RouteMethod.GET) @RouteMapping(value = "/:type/:key", method = RouteMethod.GET)
@@ -80,7 +81,8 @@ public class ServerApi {
key = keys[0]; key = keys[0];
pwd = keys[1]; pwd = keys[1];
} }
cacheService.getCachedByShareKeyAndPwd(type, key, pwd, JsonObject.of("UA",request.headers().get("user-agent"))) String origin = resolveOrigin(request);
cacheService.getCachedByShareKeyAndPwd(type, key, pwd, JsonObject.of("UA",request.headers().get("user-agent"), "_requestOrigin", origin))
.onSuccess(res -> ResponseUtil.redirect( .onSuccess(res -> ResponseUtil.redirect(
response.putHeader("nfd-cache-hit", res.getCacheHit().toString()) response.putHeader("nfd-cache-hit", res.getCacheHit().toString())
.putHeader("nfd-cache-expires", res.getExpires()), .putHeader("nfd-cache-expires", res.getExpires()),
@@ -89,6 +91,21 @@ public class ServerApi {
return promise.future(); return promise.future();
} }
/**
* 解析请求来源地址,支持反向代理
*/
private static String resolveOrigin(HttpServerRequest request) {
String forwardedHost = request.getHeader("X-Forwarded-Host");
if (forwardedHost != null && !forwardedHost.isBlank()) {
String proto = request.getHeader("X-Forwarded-Proto");
if (proto == null || proto.isBlank()) {
proto = request.scheme();
}
return proto + "://" + forwardedHost;
}
return request.scheme() + "://" + request.host();
}
/** /**
* 构建 otherParam包含 UA 和解码后的认证参数 * 构建 otherParam包含 UA 和解码后的认证参数
* *
@@ -97,7 +114,7 @@ public class ServerApi {
* @return JsonObject * @return JsonObject
*/ */
private JsonObject buildOtherParam(HttpServerRequest request, String auth) { private JsonObject buildOtherParam(HttpServerRequest request, String auth) {
JsonObject otherParam = JsonObject.of("UA", request.headers().get("user-agent")); JsonObject otherParam = JsonObject.of("UA", request.headers().get("user-agent"), "_requestOrigin", resolveOrigin(request));
// 解码认证参数 // 解码认证参数
if (auth != null && !auth.isEmpty()) { if (auth != null && !auth.isEmpty()) {

View File

@@ -104,7 +104,7 @@ public class CacheServiceImpl implements CacheService {
promise.complete(result); promise.complete(result);
// 更新缓存 // 更新缓存
cacheManager.cacheShareLink(cacheLinkInfo); 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); }).onFailure(promise::fail);
} else { } else {
// 缓存命中,生成过期时间并生成下载命令 // 缓存命中,生成过期时间并生成下载命令
@@ -120,7 +120,7 @@ public class CacheServiceImpl implements CacheService {
promise.complete(result); promise.complete(result);
cacheManager.updateTotalByField(cacheKey, CacheTotalField.CACHE_HIT_TOTAL) cacheManager.updateTotalByField(cacheKey, CacheTotalField.CACHE_HIT_TOTAL)
.onFailure(Throwable::printStackTrace); .onFailure(e -> log.error("更新缓存命中计数失败: cacheKey={}", cacheKey, e));
} }
}).onFailure(t -> promise.fail(t.fillInStackTrace())); }).onFailure(t -> promise.fail(t.fillInStackTrace()));

View File

@@ -42,12 +42,11 @@ public class DbServiceImpl implements DbService {
@Override @Override
public Future<JsonObject> sayOk(String data) { public Future<JsonObject> sayOk(String data) {
log.info("say ok1 -> wait..."); log.info("say ok1 -> wait...");
try { Promise<JsonObject> promise = Promise.promise();
Thread.sleep(4000); cn.qaiu.vx.core.util.VertxHolder.getVertxInstance().setTimer(4000, id -> {
} catch (InterruptedException e) { promise.complete(JsonObject.mapFrom(JsonResult.data("Hi: " + data)));
e.printStackTrace(); });
} return promise.future();
return Future.succeededFuture(JsonObject.mapFrom(JsonResult.data("Hi: " + data)));
} }
@Override @Override

View File

@@ -125,7 +125,7 @@ public class UserServiceImpl implements UserService {
if (rows.size() == 0) { if (rows.size() == 0) {
promise.complete(new JsonObject() promise.complete(new JsonObject()
.put("success", false) .put("success", false)
.put("message", "用户不存在")); .put("message", "用户名或密码错误"));
return; return;
} }
@@ -136,7 +136,7 @@ public class UserServiceImpl implements UserService {
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) { if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
promise.complete(new JsonObject() promise.complete(new JsonObject()
.put("success", false) .put("success", false)
.put("message", "密码错误")); .put("message", "用户名或密码错误"));
return; return;
} }
@@ -169,7 +169,7 @@ public class UserServiceImpl implements UserService {
log.error("登录查询失败", err); log.error("登录查询失败", err);
promise.complete(new JsonObject() promise.complete(new JsonObject()
.put("success", false) .put("success", false)
.put("message", "登录失败: " + err.getMessage())); .put("message", "登录失败,请稍后重试"));
}); });
return promise.future(); return promise.future();
@@ -189,7 +189,7 @@ public class UserServiceImpl implements UserService {
.execute(Tuple.of(username)) .execute(Tuple.of(username))
.onSuccess(rows -> { .onSuccess(rows -> {
if (rows.size() == 0) { if (rows.size() == 0) {
promise.fail("用户不存在"); promise.fail("用户名或密码错误");
return; return;
} }
@@ -296,7 +296,7 @@ public class UserServiceImpl implements UserService {
.execute(Tuple.of(user.getUsername())) .execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> { .onSuccess(rows -> {
if (rows.size() == 0) { if (rows.size() == 0) {
promise.fail("用户不存在"); promise.fail("用户名或密码错误");
return; return;
} }
@@ -406,7 +406,7 @@ public class UserServiceImpl implements UserService {
.onFailure(err -> { .onFailure(err -> {
promise.complete(new JsonObject() promise.complete(new JsonObject()
.put("success", false) .put("success", false)
.put("message", "用户不存在")); .put("message", "认证失败,请重新登录"));
}); });
return promise.future(); return promise.future();

View File

@@ -4,13 +4,12 @@ server:
contextPath: / contextPath: /
# 使用数据库 # 使用数据库
enableDatabase: true enableDatabase: true
# 服务域名或者IP 生成二维码链接时需要 # 服务域名或者IP 生成二维码链接时需要,不设置则自动从请求地址获取
domainName: http://127.0.0.1:6401 # domainName: http://127.0.0.1:6401
# 预览服务URL # 预览服务URL
previewURL: https://nfd-parser.github.io/nfd-preview/preview.html?src= previewURL: https://nfd-parser.github.io/nfd-preview/preview.html?src=
# auth参数加密密钥16位AES密钥 # auth参数加密密钥16位AES密钥
authEncryptKey: 'nfd_auth_key2026' authEncryptKey: 'nfd_auth_key2026'
# domainName: https://lz.qaiu.top
# 反向代理服务器配置路径(不用加后缀) # 反向代理服务器配置路径(不用加后缀)
proxyConf: server-proxy proxyConf: server-proxy