mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-02-24 06:05:23 +00:00
更新 夸克解析、小飞机解析,前端版本号
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -56,7 +56,6 @@ test-filelist.java
|
|||||||
*.temp
|
*.temp
|
||||||
*.log
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
**/secret.yml
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|||||||
55
parser/src/main/java/cn/qaiu/parser/impl/IzSelectorTool.java
Normal file
55
parser/src/main/java/cn/qaiu/parser/impl/IzSelectorTool.java
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.parser.IPanTool;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 蓝奏云优享解析器选择器
|
||||||
|
* 根据配置的鉴权方式选择不同的解析器:
|
||||||
|
* - 如果配置了 username 和 password,则使用 IzToolWithAuth (支持大文件)
|
||||||
|
* - 否则使用 IzTool (免登录,仅支持小文件)
|
||||||
|
*/
|
||||||
|
public class IzSelectorTool implements IPanTool {
|
||||||
|
private final IPanTool selectedTool;
|
||||||
|
|
||||||
|
public IzSelectorTool(ShareLinkInfo shareLinkInfo) {
|
||||||
|
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||||
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
|
|
||||||
|
// 检查是否配置了账号密码
|
||||||
|
if (auths.contains("username") && auths.contains("password")) {
|
||||||
|
String username = auths.get("username");
|
||||||
|
String password = auths.get("password");
|
||||||
|
if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) {
|
||||||
|
// 使用 IzToolWithAuth (账密登录,支持大文件)
|
||||||
|
this.selectedTool = new IzToolWithAuth(shareLinkInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无认证信息或认证信息无效,使用免登录版本(仅支持小文件)
|
||||||
|
this.selectedTool = new IzTool(shareLinkInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parse() {
|
||||||
|
return selectedTool.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
|
return selectedTool.parseFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parseById() {
|
||||||
|
return selectedTool.parseById();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,35 +5,50 @@ import cn.qaiu.entity.ShareLinkInfo;
|
|||||||
import cn.qaiu.parser.PanBase;
|
import cn.qaiu.parser.PanBase;
|
||||||
import cn.qaiu.util.AESUtils;
|
import cn.qaiu.util.AESUtils;
|
||||||
import cn.qaiu.util.AcwScV2Generator;
|
import cn.qaiu.util.AcwScV2Generator;
|
||||||
|
import cn.qaiu.util.CommonUtils;
|
||||||
import cn.qaiu.util.FileSizeConverter;
|
import cn.qaiu.util.FileSizeConverter;
|
||||||
import io.netty.handler.codec.http.cookie.DefaultCookie;
|
import io.netty.handler.codec.http.cookie.DefaultCookie;
|
||||||
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.Promise;
|
||||||
|
import io.vertx.core.buffer.Buffer;
|
||||||
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 io.vertx.ext.web.client.HttpRequest;
|
||||||
|
import io.vertx.ext.web.client.HttpResponse;
|
||||||
import io.vertx.ext.web.client.WebClientSession;
|
import io.vertx.ext.web.client.WebClientSession;
|
||||||
import io.vertx.uritemplate.UriTemplate;
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 蓝奏云优享
|
* 蓝奏云优享
|
||||||
* v019b22
|
*
|
||||||
*/
|
*/
|
||||||
public class IzTool extends PanBase {
|
public class IzTool extends PanBase {
|
||||||
|
|
||||||
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
|
private static final String API_URL0 = "https://api.ilanzou.com/";
|
||||||
|
|
||||||
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
|
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
|
||||||
|
|
||||||
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
|
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
|
||||||
"&uuid={uuid}&extra=2×tamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
|
"&uuid={uuid}&extra=2×tamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
|
||||||
|
|
||||||
|
private static final String LOGIN_URL = API_URL_PREFIX +
|
||||||
|
"login?uuid={uuid}&devType=6&devCode={uuid}&devModel=chrome&devVersion=127&appVersion=×tamp={ts}&appToken=&extra=2";
|
||||||
|
|
||||||
|
// https://api.ilanzou.com/proved/user/info/map?devType=3&devModel=Chrome&uuid=TInRHH3QzRaMo-Ajl2PkJ&extra=2×tamp=EC2C6E7F45EB21338A17A7621E0BB437
|
||||||
|
private static final String TOKEN_VERIFY_URL = API_URL0 +
|
||||||
|
"proved/user/info/map?devType=6&devModel=Chrome&uuid={uuid}&extra=2×tamp={ts}";
|
||||||
|
|
||||||
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
|
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
|
||||||
"&devType=6&uuid={uuid}×tamp={ts}&auth={auth}&shareId={dataKey}";
|
"&devType=6&uuid={uuid}×tamp={ts}&auth={auth}&shareId={dataKey}";
|
||||||
// downloadId=x&enable=1&devType=6&uuid=x×tamp=x&auth=x&shareId=lGFndCM
|
|
||||||
|
private static final String SECOND_REQUEST_URL_VIP = API_URL_PREFIX + "file/redirect?uuid={uuid}&devType=6&devCode={uuid}" +
|
||||||
|
"&devModel=chrome&devVersion=127&appVersion=×tamp={ts}&appToken={appToken}&enable=1&downloadId={fidEncode}&auth={auth}";
|
||||||
|
|
||||||
|
|
||||||
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
|
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
|
||||||
"={uuid}&extra=2×tamp={ts}";
|
"={uuid}&extra=2×tamp={ts}";
|
||||||
@@ -42,16 +57,15 @@ public class IzTool extends PanBase {
|
|||||||
"={uuid}&extra=2×tamp={ts}&shareId={shareId}&folderId" +
|
"={uuid}&extra=2×tamp={ts}&shareId={shareId}&folderId" +
|
||||||
"={folderId}&offset=1&limit=60";
|
"={folderId}&offset=1&limit=60";
|
||||||
|
|
||||||
long nowTs = System.currentTimeMillis();
|
|
||||||
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
String uuid = UUID.randomUUID().toString();
|
|
||||||
|
|
||||||
private static final MultiMap header;
|
private static final MultiMap header;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
header = MultiMap.caseInsensitiveMultiMap();
|
header = MultiMap.caseInsensitiveMultiMap();
|
||||||
header.set("Accept", "application/json, text/plain, */*");
|
header.set("Accept", "application/json, text/plain, */*");
|
||||||
header.set("Accept-Encoding", "gzip, deflate, br, zstd");
|
header.set("Accept-Encoding", "gzip, deflate");
|
||||||
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||||
header.set("Cache-Control", "no-cache");
|
header.set("Cache-Control", "no-cache");
|
||||||
header.set("Connection", "keep-alive");
|
header.set("Connection", "keep-alive");
|
||||||
@@ -69,38 +83,59 @@ public class IzTool extends PanBase {
|
|||||||
header.set("sec-ch-ua-mobile", "?0");
|
header.set("sec-ch-ua-mobile", "?0");
|
||||||
header.set("sec-ch-ua-platform", "\"Windows\"");
|
header.set("sec-ch-ua-platform", "\"Windows\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
public IzTool(ShareLinkInfo shareLinkInfo) {
|
public IzTool(ShareLinkInfo shareLinkInfo) {
|
||||||
super(shareLinkInfo);
|
super(shareLinkInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCookie(String html) {
|
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
|
||||||
int beginIndex = html.indexOf("arg1='") + 6;
|
|
||||||
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
|
public static String token = null;
|
||||||
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
|
public static boolean authFlag = true;
|
||||||
// 创建一个 Cookie 并放入 CookieStore
|
|
||||||
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
|
||||||
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
|
|
||||||
nettyCookie.setPath("/"); // 设置路径
|
|
||||||
nettyCookie.setSecure(false);
|
|
||||||
nettyCookie.setHttpOnly(false);
|
|
||||||
webClientSession.cookieStore().put(nettyCookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
String shareId = shareLinkInfo.getShareKey();
|
|
||||||
|
|
||||||
// 24.5.12 ilanzou改规则无需计算shareId
|
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||||
// String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey));
|
long nowTs = System.currentTimeMillis();
|
||||||
|
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
||||||
|
|
||||||
// 第一次请求 获取文件信息
|
// 检查并输出认证状态
|
||||||
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
|
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||||
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
|
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||||
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
|
log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}",
|
||||||
|
isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(10, token.length())) + "...)" : "未登录");
|
||||||
|
|
||||||
|
// 如果需要认证但还没有token,先执行登录
|
||||||
|
if ((isTempAuth || authFlag) && token == null) {
|
||||||
|
log.info("文件解析需要登录,开始执行登录流程...");
|
||||||
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
|
return login(tsEncode, auths)
|
||||||
|
.compose(v -> {
|
||||||
|
log.info("文件解析预登录成功,继续解析流程");
|
||||||
|
return parseWithAuth(shareId, tsEncode);
|
||||||
|
})
|
||||||
|
.onFailure(err -> {
|
||||||
|
log.warn("文件解析预登录失败: {},尝试使用免登录模式", err.getMessage());
|
||||||
|
// 登录失败,继续使用免登录模式
|
||||||
|
});
|
||||||
|
} else if (token != null) {
|
||||||
|
log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(10, token.length())));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("文件解析无认证信息,使用免登录模式");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseWithAuth(shareId, tsEncode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<String> parseWithAuth(String shareId, String tsEncode) {
|
||||||
|
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
|
||||||
webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL))
|
webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL))
|
||||||
.setTemplateParam("uuid", uuid)
|
.setTemplateParam("uuid", uuid)
|
||||||
.setTemplateParam("ts", tsEncode)
|
.setTemplateParam("ts", tsEncode)
|
||||||
.send().onSuccess(r0 -> { // 忽略res
|
.send().onSuccess(r0 -> { // 忽略res
|
||||||
|
|
||||||
|
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
|
||||||
|
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
|
||||||
// 第一次请求 获取文件信息
|
// 第一次请求 获取文件信息
|
||||||
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
|
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
|
||||||
webClientSession.postAbs(UriTemplate.of(url))
|
webClientSession.postAbs(UriTemplate.of(url))
|
||||||
@@ -121,36 +156,43 @@ public class IzTool extends PanBase {
|
|||||||
.setTemplateParam("uuid", uuid)
|
.setTemplateParam("uuid", uuid)
|
||||||
.setTemplateParam("ts", tsEncode)
|
.setTemplateParam("ts", tsEncode)
|
||||||
.send().onSuccess(res2 -> {
|
.send().onSuccess(res2 -> {
|
||||||
handleParseResponse(asText(res2), shareId);
|
processFirstResponse(res2);
|
||||||
}).onFailure(handleFail(FIRST_REQUEST_URL));
|
}).onFailure(handleFail("请求1-重试"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleParseResponse(resBody, shareId);
|
processFirstResponse(res);
|
||||||
}).onFailure(handleFail(FIRST_REQUEST_URL));
|
}).onFailure(handleFail("请求1"));
|
||||||
});
|
|
||||||
|
}).onFailure(handleFail("请求1"));
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleParseResponse(String resBody, String shareId) {
|
/**
|
||||||
JsonObject resJson;
|
* 设置 cookie
|
||||||
try {
|
*/
|
||||||
resJson = new JsonObject(resBody);
|
private void setCookie(String html) {
|
||||||
} catch (Exception e) {
|
int beginIndex = html.indexOf("arg1='") + 6;
|
||||||
fail(FIRST_REQUEST_URL + " 解析JSON失败: " + resBody);
|
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
|
||||||
return;
|
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
|
||||||
}
|
// 创建一个 Cookie 并放入 CookieStore
|
||||||
if (resJson.isEmpty()) {
|
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
||||||
fail(FIRST_REQUEST_URL + " 返回内容为空");
|
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
|
||||||
return;
|
nettyCookie.setPath("/"); // 设置路径
|
||||||
|
nettyCookie.setSecure(false);
|
||||||
|
nettyCookie.setHttpOnly(false);
|
||||||
|
webClientSession.cookieStore().put(nettyCookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理第一次请求的响应
|
||||||
|
*/
|
||||||
|
private void processFirstResponse(HttpResponse<Buffer> res) {
|
||||||
|
JsonObject resJson = asJson(res);
|
||||||
if (resJson.getInteger("code") != 200) {
|
if (resJson.getInteger("code") != 200) {
|
||||||
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resJson.getJsonArray("list").isEmpty()) {
|
|
||||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
|
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
|
||||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||||
return;
|
return;
|
||||||
@@ -167,30 +209,251 @@ public class IzTool extends PanBase {
|
|||||||
promise.complete(fileList.getInteger("folderId").toString());
|
promise.complete(fileList.getInteger("folderId").toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 提取文件信息
|
||||||
|
extractFileInfo(fileList, fileInfo);
|
||||||
|
getDownURL(resJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getDownURL(JsonObject resJson) {
|
||||||
|
String dataKey = shareLinkInfo.getShareKey();
|
||||||
|
// 文件Id
|
||||||
|
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
|
||||||
String fileId = fileInfo.getString("fileIds");
|
String fileId = fileInfo.getString("fileIds");
|
||||||
String userId = fileInfo.getString("userId");
|
String userId = fileInfo.getString("userId");
|
||||||
// 其他参数
|
// 其他参数
|
||||||
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
|
long nowTs2 = System.currentTimeMillis();
|
||||||
|
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
|
||||||
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||||
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
|
||||||
// 第二次请求
|
|
||||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
// 检查是否有认证信息
|
||||||
|
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||||
|
// 检查是否为临时认证(临时认证每次都尝试登录)
|
||||||
|
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||||
|
// 如果是临时认证,或者是后台配置且authFlag为true,则尝试使用认证
|
||||||
|
if (isTempAuth || authFlag) {
|
||||||
|
log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag);
|
||||||
|
HttpRequest<Buffer> httpRequest =
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
|
||||||
.setTemplateParam("fidEncode", fidEncode)
|
.setTemplateParam("fidEncode", fidEncode)
|
||||||
.setTemplateParam("uuid", uuid)
|
.setTemplateParam("uuid", uuid)
|
||||||
.setTemplateParam("ts", tsEncode)
|
.setTemplateParam("ts", tsEncode2)
|
||||||
.setTemplateParam("auth", auth)
|
.setTemplateParam("auth", auth)
|
||||||
.setTemplateParam("shareId", shareId)
|
.setTemplateParam("dataKey", dataKey);
|
||||||
.putHeaders(header).send().onSuccess(res2 -> {
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
|
if (token == null) {
|
||||||
|
// 执行登录
|
||||||
|
login(tsEncode2, auths).onFailure(failRes-> {
|
||||||
|
log.warn("登录失败: {}", failRes.getMessage());
|
||||||
|
fail(failRes.getMessage());
|
||||||
|
}).onSuccess(r-> {
|
||||||
|
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||||
|
.putHeaders(header);
|
||||||
|
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 验证token
|
||||||
|
webClientSession.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.putHeaders(header).send().onSuccess(res -> {
|
||||||
|
// log.info("res: {}",asJson(res));
|
||||||
|
if (asJson(res).getInteger("code") != 200) {
|
||||||
|
login(tsEncode2, auths).onFailure(failRes -> {
|
||||||
|
log.warn("重新登录失败: {}", failRes.getMessage());
|
||||||
|
fail(failRes.getMessage());
|
||||||
|
}).onSuccess(r-> {
|
||||||
|
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||||
|
.putHeaders(header);
|
||||||
|
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||||
|
.putHeaders(header);
|
||||||
|
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
}
|
||||||
|
}).onFailure(handleFail("Token验证"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// authFlag 为 false,使用免登录解析
|
||||||
|
log.debug("authFlag=false,使用免登录解析");
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("fidEncode", fidEncode)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.setTemplateParam("auth", auth)
|
||||||
|
.setTemplateParam("dataKey", dataKey).send()
|
||||||
|
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有认证信息,使用免登录解析
|
||||||
|
log.debug("无认证信息,使用免登录解析");
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("fidEncode", fidEncode)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.setTemplateParam("auth", auth)
|
||||||
|
.setTemplateParam("dataKey", dataKey).send()
|
||||||
|
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<Void> login(String tsEncode2, MultiMap auths) {
|
||||||
|
Promise<Void> promise1 = Promise.promise();
|
||||||
|
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
|
||||||
|
.setTemplateParam("uuid",uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.putHeaders(header)
|
||||||
|
.sendJsonObject(JsonObject.of("loginName", auths.get("username"), "loginPwd", auths.get("password")))
|
||||||
|
.onSuccess(res2->{
|
||||||
|
JsonObject json = asJson(res2);
|
||||||
|
if (json.getInteger("code") == 200) {
|
||||||
|
token = json.getJsonObject("data").getString("appToken");
|
||||||
|
header.set("appToken", token);
|
||||||
|
log.info("登录成功 token: {}", token);
|
||||||
|
promise1.complete();
|
||||||
|
} else {
|
||||||
|
// 检查是否为临时认证
|
||||||
|
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||||
|
if (isTempAuth) {
|
||||||
|
// 临时认证失败,直接返回错误,不影响后台配置的认证
|
||||||
|
log.warn("临时认证失败: {}", json.getString("msg"));
|
||||||
|
promise1.fail("临时认证失败: " + json.getString("msg"));
|
||||||
|
} else {
|
||||||
|
// 后台配置的认证失败,设置authFlag并返回失败,让下次请求使用免登陆解析
|
||||||
|
log.warn("后台配置认证失败: {}, authFlag将设为false,请重新解析", json.getString("msg"));
|
||||||
|
authFlag = false;
|
||||||
|
promise1.fail("认证失败: " + json.getString("msg") + ", 请重新解析将使用免登陆模式");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.error("登录请求异常: {}", err.getMessage());
|
||||||
|
promise1.fail("登录请求异常: " + err.getMessage());
|
||||||
|
});
|
||||||
|
return promise1.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从接口返回数据中提取文件信息
|
||||||
|
*/
|
||||||
|
private void extractFileInfo(JsonObject fileList, JsonObject shareInfo) {
|
||||||
|
try {
|
||||||
|
// 文件名
|
||||||
|
String fileName = fileList.getString("fileName");
|
||||||
|
shareLinkInfo.getOtherParam().put("fileName", fileName);
|
||||||
|
|
||||||
|
// 文件大小 (KB -> Bytes)
|
||||||
|
Long fileSize = fileList.getLong("fileSize", 0L) * 1024;
|
||||||
|
shareLinkInfo.getOtherParam().put("fileSize", fileSize);
|
||||||
|
shareLinkInfo.getOtherParam().put("fileSizeFormat", FileSizeConverter.convertToReadableSize(fileSize));
|
||||||
|
|
||||||
|
// 文件图标
|
||||||
|
String fileIcon = fileList.getString("fileIcon");
|
||||||
|
if (StringUtils.isNotBlank(fileIcon)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("fileIcon", fileIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件ID
|
||||||
|
Long fileId = fileList.getLong("fileId");
|
||||||
|
if (fileId != null) {
|
||||||
|
shareLinkInfo.getOtherParam().put("fileId", fileId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件类型 (1=文件, 2=目录)
|
||||||
|
Integer fileType = fileList.getInteger("fileType", 1);
|
||||||
|
shareLinkInfo.getOtherParam().put("fileType", fileType == 1 ? "file" : "folder");
|
||||||
|
|
||||||
|
// 下载次数
|
||||||
|
Integer downloads = fileList.getInteger("fileDownloads", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("downloadCount", downloads);
|
||||||
|
|
||||||
|
// 点赞数
|
||||||
|
Integer likes = fileList.getInteger("fileLikes", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("likeCount", likes);
|
||||||
|
|
||||||
|
// 评论数
|
||||||
|
Integer comments = fileList.getInteger("fileComments", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("commentCount", comments);
|
||||||
|
|
||||||
|
// 评分
|
||||||
|
Double stars = fileList.getDouble("fileStars", 0.0);
|
||||||
|
shareLinkInfo.getOtherParam().put("stars", stars);
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
String updateTime = fileList.getString("updTime");
|
||||||
|
if (StringUtils.isNotBlank(updateTime)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("updateTime", updateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建时间
|
||||||
|
String createTime = null;
|
||||||
|
|
||||||
|
// 分享信息
|
||||||
|
if (shareInfo != null) {
|
||||||
|
// 分享ID
|
||||||
|
Integer shareId = shareInfo.getInteger("shareId");
|
||||||
|
if (shareId != null) {
|
||||||
|
shareLinkInfo.getOtherParam().put("shareId", shareId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传时间
|
||||||
|
String addTime = shareInfo.getString("addTime");
|
||||||
|
if (StringUtils.isNotBlank(addTime)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("createTime", addTime);
|
||||||
|
createTime = addTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览次数
|
||||||
|
Integer previewNum = shareInfo.getInteger("previewNum", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("previewCount", previewNum);
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
JsonObject userMap = shareInfo.getJsonObject("map");
|
||||||
|
if (userMap != null) {
|
||||||
|
String userName = userMap.getString("userName");
|
||||||
|
if (StringUtils.isNotBlank(userName)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("userName", userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIP信息
|
||||||
|
Integer isVip = userMap.getInteger("isVip", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("isVip", isVip == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 FileInfo 对象并存入 otherParam
|
||||||
|
FileInfo fileInfoObj = new FileInfo()
|
||||||
|
.setPanType(shareLinkInfo.getType())
|
||||||
|
.setFileName(fileName)
|
||||||
|
.setFileId(fileList.getLong("fileId") != null ? fileList.getLong("fileId").toString() : null)
|
||||||
|
.setSize(fileSize)
|
||||||
|
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
|
||||||
|
.setFileType(fileType == 1 ? "file" : "folder")
|
||||||
|
.setFileIcon(fileList.getString("fileIcon"))
|
||||||
|
.setDownloadCount(downloads)
|
||||||
|
.setCreateTime(createTime)
|
||||||
|
.setUpdateTime(updateTime);
|
||||||
|
shareLinkInfo.getOtherParam().put("fileInfo", fileInfoObj);
|
||||||
|
|
||||||
|
log.debug("提取文件信息成功: fileName={}, fileSize={}, downloads={}",
|
||||||
|
fileName, fileSize, downloads);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("提取文件信息失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void down(HttpResponse<Buffer> res2) {
|
||||||
MultiMap headers = res2.headers();
|
MultiMap headers = res2.headers();
|
||||||
if (!headers.contains("Location")) {
|
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) {
|
||||||
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + headers);
|
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
promise.complete(headers.get("Location"));
|
promise.complete(headers.get("Location"));
|
||||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 目录解析
|
||||||
@Override
|
@Override
|
||||||
public Future<List<FileInfo>> parseFileList() {
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
Promise<List<FileInfo>> promise = Promise.promise();
|
Promise<List<FileInfo>> promise = Promise.promise();
|
||||||
@@ -205,11 +468,7 @@ public class IzTool extends PanBase {
|
|||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
parse().onSuccess(id -> {
|
parse().onSuccess(id -> {
|
||||||
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
|
|
||||||
parserDir(id, shareId, promise);
|
parserDir(id, shareId, promise);
|
||||||
} else {
|
|
||||||
promise.fail("解析目录ID失败");
|
|
||||||
}
|
|
||||||
}).onFailure(failRes -> {
|
}).onFailure(failRes -> {
|
||||||
log.error("解析目录失败: {}", failRes.getMessage());
|
log.error("解析目录失败: {}", failRes.getMessage());
|
||||||
promise.fail(failRes);
|
promise.fail(failRes);
|
||||||
@@ -218,6 +477,22 @@ public class IzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
|
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
|
||||||
|
if (id != null && (id.startsWith("http://") || id.startsWith("https://"))) {
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
fileInfo.setFileName(id)
|
||||||
|
.setFileId(id)
|
||||||
|
.setFileType("file")
|
||||||
|
.setParserUrl(id)
|
||||||
|
.setPanType(shareLinkInfo.getType());
|
||||||
|
List<FileInfo> result = new ArrayList<>();
|
||||||
|
result.add(fileInfo);
|
||||||
|
promise.complete(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowTs = System.currentTimeMillis();
|
||||||
|
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
||||||
|
|
||||||
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
|
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
|
||||||
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
|
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
|
||||||
// 拿到目录ID
|
// 拿到目录ID
|
||||||
@@ -228,14 +503,48 @@ public class IzTool extends PanBase {
|
|||||||
.setTemplateParam("ts", tsEncode)
|
.setTemplateParam("ts", tsEncode)
|
||||||
.setTemplateParam("folderId", id)
|
.setTemplateParam("folderId", id)
|
||||||
.send().onSuccess(res -> {
|
.send().onSuccess(res -> {
|
||||||
JsonObject jsonObject;
|
String resBody = asText(res);
|
||||||
try {
|
// 检查是否包含 cookie 验证
|
||||||
jsonObject = asJson(res);
|
if (resBody.contains("var arg1='")) {
|
||||||
} catch (Exception e) {
|
log.debug("目录解析需要 cookie 验证,重新创建 session");
|
||||||
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
|
webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
|
setCookie(resBody);
|
||||||
|
// 重新请求目录列表
|
||||||
|
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.setTemplateParam("folderId", id)
|
||||||
|
.send().onSuccess(res2 -> {
|
||||||
|
processDirResponse(res2, shareId, promise);
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.error("目录解析重试失败: {}", err.getMessage());
|
||||||
|
promise.fail("目录解析失败: " + err.getMessage());
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// System.out.println(jsonObject.encodePrettily());
|
processDirResponse(res, shareId, promise);
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.error("目录解析请求失败: {}", err.getMessage());
|
||||||
|
promise.fail("目录解析失败: " + err.getMessage());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理目录解析响应
|
||||||
|
*/
|
||||||
|
private void processDirResponse(HttpResponse<Buffer> res, String shareId, Promise<List<FileInfo>> promise) {
|
||||||
|
try {
|
||||||
|
JsonObject jsonObject = asJson(res);
|
||||||
|
log.debug("目录解析响应: {}", jsonObject.encodePrettily());
|
||||||
|
|
||||||
|
if (!jsonObject.containsKey("list")) {
|
||||||
|
log.error("目录解析响应缺少 list 字段: {}", jsonObject);
|
||||||
|
promise.fail("目录解析失败: 响应格式错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
JsonArray list = jsonObject.getJsonArray("list");
|
JsonArray list = jsonObject.getJsonArray("list");
|
||||||
ArrayList<FileInfo> result = new ArrayList<>();
|
ArrayList<FileInfo> result = new ArrayList<>();
|
||||||
list.forEach(item->{
|
list.forEach(item->{
|
||||||
@@ -246,22 +555,20 @@ public class IzTool extends PanBase {
|
|||||||
String fileId = fileJson.getString("fileId");
|
String fileId = fileJson.getString("fileId");
|
||||||
String userId = fileJson.getString("userId");
|
String userId = fileJson.getString("userId");
|
||||||
|
|
||||||
// 回传用到的参数
|
// 其他参数 - 每个文件使用新的时间戳
|
||||||
//"fidEncode", paramJson.getString("fidEncode"))
|
long nowTs2 = System.currentTimeMillis();
|
||||||
//"uuid", paramJson.getString("uuid"))
|
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
|
||||||
//"ts", paramJson.getString("ts"))
|
|
||||||
//"auth", paramJson.getString("auth"))
|
|
||||||
//"shareId", paramJson.getString("shareId"))
|
|
||||||
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||||
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
|
||||||
|
|
||||||
|
// 回传用到的参数
|
||||||
JsonObject entries = JsonObject.of(
|
JsonObject entries = JsonObject.of(
|
||||||
"fidEncode", fidEncode,
|
"fidEncode", fidEncode,
|
||||||
"uuid", uuid,
|
"uuid", uuid,
|
||||||
"ts", tsEncode,
|
"ts", tsEncode2,
|
||||||
"auth", auth,
|
"auth", auth,
|
||||||
"shareId", shareId);
|
"shareId", shareId);
|
||||||
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
|
String param = CommonUtils.urlBase64Encode(entries.encode());
|
||||||
String param = new String(encode);
|
|
||||||
|
|
||||||
if (fileJson.getInteger("fileType") == 2) {
|
if (fileJson.getInteger("fileType") == 2) {
|
||||||
// 如果是目录
|
// 如果是目录
|
||||||
@@ -301,30 +608,29 @@ public class IzTool extends PanBase {
|
|||||||
result.add(fileInfo);
|
result.add(fileInfo);
|
||||||
});
|
});
|
||||||
promise.complete(result);
|
promise.complete(result);
|
||||||
}).onFailure(failRes -> {
|
} catch (Exception e) {
|
||||||
log.error("解析目录请求失败: {}", failRes.getMessage());
|
log.error("处理目录响应异常: {}", e.getMessage(), e);
|
||||||
promise.fail(failRes);
|
promise.fail("目录解析失败: " + e.getMessage());
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<String> parseById() {
|
public Future<String> parseById() {
|
||||||
// 第二次请求
|
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
||||||
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
|
// 使用免登录接口
|
||||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||||
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
||||||
.setTemplateParam("ts", paramJson.getString("ts"))
|
.setTemplateParam("ts", paramJson.getString("ts"))
|
||||||
.setTemplateParam("auth", paramJson.getString("auth"))
|
.setTemplateParam("auth", paramJson.getString("auth"))
|
||||||
.setTemplateParam("shareId", paramJson.getString("shareId"))
|
.setTemplateParam("dataKey", paramJson.getString("shareId"))
|
||||||
.putHeaders(header).send().onSuccess(res2 -> {
|
.send().onSuccess(this::down).onFailure(handleFail("parseById"));
|
||||||
MultiMap headers = res2.headers();
|
|
||||||
if (!headers.contains("Location")) {
|
|
||||||
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res2.headers());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
promise.complete(headers.get("Location"));
|
|
||||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void resetToken() {
|
||||||
|
token = null;
|
||||||
|
authFlag = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
658
parser/src/main/java/cn/qaiu/parser/impl/IzToolWithAuth.java
Normal file
658
parser/src/main/java/cn/qaiu/parser/impl/IzToolWithAuth.java
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.parser.PanBase;
|
||||||
|
import cn.qaiu.util.AESUtils;
|
||||||
|
import cn.qaiu.util.AcwScV2Generator;
|
||||||
|
import cn.qaiu.util.CommonUtils;
|
||||||
|
import cn.qaiu.util.FileSizeConverter;
|
||||||
|
import io.netty.handler.codec.http.cookie.DefaultCookie;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.core.buffer.Buffer;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.ext.web.client.HttpRequest;
|
||||||
|
import io.vertx.ext.web.client.HttpResponse;
|
||||||
|
import io.vertx.ext.web.client.WebClientSession;
|
||||||
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 蓝奏云优享 - 需要登录版本(支持大文件)
|
||||||
|
*/
|
||||||
|
public class IzToolWithAuth extends PanBase {
|
||||||
|
|
||||||
|
private static final String API_URL0 = "https://api.ilanzou.com/";
|
||||||
|
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
|
||||||
|
|
||||||
|
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
|
||||||
|
"&uuid={uuid}&extra=2×tamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
|
||||||
|
|
||||||
|
private static final String LOGIN_URL = API_URL_PREFIX +
|
||||||
|
"login?uuid={uuid}&devType=6&devCode={uuid}&devModel=chrome&devVersion=127&appVersion=×tamp={ts}&appToken=&extra=2";
|
||||||
|
|
||||||
|
// https://api.ilanzou.com/proved/user/info/map?devType=3&devModel=Chrome&uuid=TInRHH3QzRaMo-Ajl2PkJ&extra=2×tamp=EC2C6E7F45EB21338A17A7621E0BB437
|
||||||
|
private static final String TOKEN_VERIFY_URL = API_URL0 +
|
||||||
|
"proved/user/info/map?devType=6&devModel=Chrome&uuid={uuid}&extra=2×tamp={ts}";
|
||||||
|
|
||||||
|
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
|
||||||
|
"&devType=6&uuid={uuid}×tamp={ts}&auth={auth}&shareId={dataKey}";
|
||||||
|
|
||||||
|
private static final String SECOND_REQUEST_URL_VIP = API_URL_PREFIX + "file/redirect?uuid={uuid}&devType=6&devCode={uuid}" +
|
||||||
|
"&devModel=chrome&devVersion=127&appVersion=×tamp={ts}&appToken={appToken}&enable=1&downloadId={fidEncode}&auth={auth}";
|
||||||
|
|
||||||
|
|
||||||
|
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
|
||||||
|
"={uuid}&extra=2×tamp={ts}";
|
||||||
|
|
||||||
|
private static final String FILE_LIST_URL = API_URL_PREFIX + "/share/list?devType=6&devModel=Chrome&uuid" +
|
||||||
|
"={uuid}&extra=2×tamp={ts}&shareId={shareId}&folderId" +
|
||||||
|
"={folderId}&offset=1&limit=60";
|
||||||
|
|
||||||
|
|
||||||
|
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
|
|
||||||
|
private static final MultiMap header;
|
||||||
|
|
||||||
|
static {
|
||||||
|
header = MultiMap.caseInsensitiveMultiMap();
|
||||||
|
header.set("Accept", "application/json, text/plain, */*");
|
||||||
|
header.set("Accept-Encoding", "gzip, deflate");
|
||||||
|
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||||
|
header.set("Cache-Control", "no-cache");
|
||||||
|
header.set("Connection", "keep-alive");
|
||||||
|
header.set("Content-Length", "0");
|
||||||
|
header.set("DNT", "1");
|
||||||
|
header.set("Host", "api.ilanzou.com");
|
||||||
|
header.set("Origin", "https://www.ilanzou.com/");
|
||||||
|
header.set("Pragma", "no-cache");
|
||||||
|
header.set("Referer", "https://www.ilanzou.com/");
|
||||||
|
header.set("Sec-Fetch-Dest", "empty");
|
||||||
|
header.set("Sec-Fetch-Mode", "cors");
|
||||||
|
header.set("Sec-Fetch-Site", "cross-site");
|
||||||
|
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||||
|
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
|
||||||
|
header.set("sec-ch-ua-mobile", "?0");
|
||||||
|
header.set("sec-ch-ua-platform", "\"Windows\"");
|
||||||
|
}
|
||||||
|
public IzToolWithAuth(ShareLinkInfo shareLinkInfo) {
|
||||||
|
super(shareLinkInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
public static String token = null;
|
||||||
|
public static boolean authFlag = true;
|
||||||
|
|
||||||
|
public Future<String> parse() {
|
||||||
|
|
||||||
|
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||||
|
long nowTs = System.currentTimeMillis();
|
||||||
|
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
||||||
|
|
||||||
|
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
|
||||||
|
webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL))
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.send().onSuccess(r0 -> { // 忽略res
|
||||||
|
|
||||||
|
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
|
||||||
|
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
|
||||||
|
// 第一次请求 获取文件信息
|
||||||
|
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
|
||||||
|
webClientSession.postAbs(UriTemplate.of(url))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.send().onSuccess(res -> {
|
||||||
|
String resBody = asText(res);
|
||||||
|
// 检查是否包含 cookie 验证
|
||||||
|
if (resBody.contains("var arg1='")) {
|
||||||
|
webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
|
setCookie(resBody);
|
||||||
|
// 重新请求
|
||||||
|
webClientSession.postAbs(UriTemplate.of(url))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.send().onSuccess(res2 -> {
|
||||||
|
processFirstResponse(res2);
|
||||||
|
}).onFailure(handleFail("请求1-重试"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processFirstResponse(res);
|
||||||
|
}).onFailure(handleFail("请求1"));
|
||||||
|
|
||||||
|
}).onFailure(handleFail("请求1"));
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 cookie
|
||||||
|
*/
|
||||||
|
private void setCookie(String html) {
|
||||||
|
int beginIndex = html.indexOf("arg1='") + 6;
|
||||||
|
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
|
||||||
|
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
|
||||||
|
// 创建一个 Cookie 并放入 CookieStore
|
||||||
|
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
||||||
|
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
|
||||||
|
nettyCookie.setPath("/"); // 设置路径
|
||||||
|
nettyCookie.setSecure(false);
|
||||||
|
nettyCookie.setHttpOnly(false);
|
||||||
|
webClientSession.cookieStore().put(nettyCookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理第一次请求的响应
|
||||||
|
*/
|
||||||
|
private void processFirstResponse(HttpResponse<Buffer> res) {
|
||||||
|
JsonObject resJson = asJson(res);
|
||||||
|
if (resJson.getInteger("code") != 200) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 文件Id
|
||||||
|
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
|
||||||
|
// 如果是目录返回目录ID
|
||||||
|
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
|
||||||
|
if (fileList.getInteger("fileType") == 2) {
|
||||||
|
promise.complete(fileList.getInteger("folderId").toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 提取文件信息
|
||||||
|
extractFileInfo(fileList, fileInfo);
|
||||||
|
getDownURL(resJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getDownURL(JsonObject resJson) {
|
||||||
|
String dataKey = shareLinkInfo.getShareKey();
|
||||||
|
// 文件Id
|
||||||
|
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
|
||||||
|
String fileId = fileInfo.getString("fileIds");
|
||||||
|
String userId = fileInfo.getString("userId");
|
||||||
|
// 其他参数
|
||||||
|
long nowTs2 = System.currentTimeMillis();
|
||||||
|
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
|
||||||
|
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||||
|
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
|
||||||
|
|
||||||
|
// 检查是否有认证信息
|
||||||
|
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||||
|
// 检查是否为临时认证(临时认证每次都尝试登录)
|
||||||
|
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||||
|
// 如果是临时认证,或者是后台配置且authFlag为true,则尝试使用认证
|
||||||
|
if (isTempAuth || authFlag) {
|
||||||
|
log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag);
|
||||||
|
HttpRequest<Buffer> httpRequest =
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
|
||||||
|
.setTemplateParam("fidEncode", fidEncode)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.setTemplateParam("auth", auth)
|
||||||
|
.setTemplateParam("dataKey", dataKey);
|
||||||
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
|
if (token == null) {
|
||||||
|
// 执行登录
|
||||||
|
login(tsEncode2, auths).onFailure(failRes-> {
|
||||||
|
log.warn("登录失败: {}", failRes.getMessage());
|
||||||
|
fail(failRes.getMessage());
|
||||||
|
}).onSuccess(r-> {
|
||||||
|
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||||
|
.putHeaders(header);
|
||||||
|
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 验证token
|
||||||
|
webClientSession.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.putHeaders(header).send().onSuccess(res -> {
|
||||||
|
// log.info("res: {}",asJson(res));
|
||||||
|
if (asJson(res).getInteger("code") != 200) {
|
||||||
|
login(tsEncode2, auths).onFailure(failRes -> {
|
||||||
|
log.warn("重新登录失败: {}", failRes.getMessage());
|
||||||
|
fail(failRes.getMessage());
|
||||||
|
}).onSuccess(r-> {
|
||||||
|
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||||
|
.putHeaders(header);
|
||||||
|
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
httpRequest.setTemplateParam("appToken", header.get("appToken"))
|
||||||
|
.putHeaders(header);
|
||||||
|
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
}
|
||||||
|
}).onFailure(handleFail("Token验证"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// authFlag 为 false,使用免登录解析
|
||||||
|
log.debug("authFlag=false,使用免登录解析");
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("fidEncode", fidEncode)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.setTemplateParam("auth", auth)
|
||||||
|
.setTemplateParam("dataKey", dataKey).send()
|
||||||
|
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有认证信息,使用免登录解析
|
||||||
|
log.debug("无认证信息,使用免登录解析");
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("fidEncode", fidEncode)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.setTemplateParam("auth", auth)
|
||||||
|
.setTemplateParam("dataKey", dataKey).send()
|
||||||
|
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<Void> login(String tsEncode2, MultiMap auths) {
|
||||||
|
Promise<Void> promise1 = Promise.promise();
|
||||||
|
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
|
||||||
|
.setTemplateParam("uuid",uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode2)
|
||||||
|
.putHeaders(header)
|
||||||
|
.sendJsonObject(JsonObject.of("loginName", auths.get("username"), "loginPwd", auths.get("password")))
|
||||||
|
.onSuccess(res2->{
|
||||||
|
JsonObject json = asJson(res2);
|
||||||
|
if (json.getInteger("code") == 200) {
|
||||||
|
token = json.getJsonObject("data").getString("appToken");
|
||||||
|
header.set("appToken", token);
|
||||||
|
log.info("登录成功 token: {}", token);
|
||||||
|
promise1.complete();
|
||||||
|
} else {
|
||||||
|
// 检查是否为临时认证
|
||||||
|
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||||
|
if (isTempAuth) {
|
||||||
|
// 临时认证失败,直接返回错误,不影响后台配置的认证
|
||||||
|
log.warn("临时认证失败: {}", json.getString("msg"));
|
||||||
|
promise1.fail("临时认证失败: " + json.getString("msg"));
|
||||||
|
} else {
|
||||||
|
// 后台配置的认证失败,设置authFlag并返回失败,让下次请求使用免登陆解析
|
||||||
|
log.warn("后台配置认证失败: {}, authFlag将设为false,请重新解析", json.getString("msg"));
|
||||||
|
authFlag = false;
|
||||||
|
promise1.fail("认证失败: " + json.getString("msg") + ", 请重新解析将使用免登陆模式");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.error("登录请求异常: {}", err.getMessage());
|
||||||
|
promise1.fail("登录请求异常: " + err.getMessage());
|
||||||
|
});
|
||||||
|
return promise1.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从接口返回数据中提取文件信息
|
||||||
|
*/
|
||||||
|
private void extractFileInfo(JsonObject fileList, JsonObject shareInfo) {
|
||||||
|
try {
|
||||||
|
// 文件名
|
||||||
|
String fileName = fileList.getString("fileName");
|
||||||
|
shareLinkInfo.getOtherParam().put("fileName", fileName);
|
||||||
|
|
||||||
|
// 文件大小 (KB -> Bytes)
|
||||||
|
Long fileSize = fileList.getLong("fileSize", 0L) * 1024;
|
||||||
|
shareLinkInfo.getOtherParam().put("fileSize", fileSize);
|
||||||
|
shareLinkInfo.getOtherParam().put("fileSizeFormat", FileSizeConverter.convertToReadableSize(fileSize));
|
||||||
|
|
||||||
|
// 文件图标
|
||||||
|
String fileIcon = fileList.getString("fileIcon");
|
||||||
|
if (StringUtils.isNotBlank(fileIcon)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("fileIcon", fileIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件ID
|
||||||
|
Long fileId = fileList.getLong("fileId");
|
||||||
|
if (fileId != null) {
|
||||||
|
shareLinkInfo.getOtherParam().put("fileId", fileId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件类型 (1=文件, 2=目录)
|
||||||
|
Integer fileType = fileList.getInteger("fileType", 1);
|
||||||
|
shareLinkInfo.getOtherParam().put("fileType", fileType == 1 ? "file" : "folder");
|
||||||
|
|
||||||
|
// 下载次数
|
||||||
|
Integer downloads = fileList.getInteger("fileDownloads", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("downloadCount", downloads);
|
||||||
|
|
||||||
|
// 点赞数
|
||||||
|
Integer likes = fileList.getInteger("fileLikes", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("likeCount", likes);
|
||||||
|
|
||||||
|
// 评论数
|
||||||
|
Integer comments = fileList.getInteger("fileComments", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("commentCount", comments);
|
||||||
|
|
||||||
|
// 评分
|
||||||
|
Double stars = fileList.getDouble("fileStars", 0.0);
|
||||||
|
shareLinkInfo.getOtherParam().put("stars", stars);
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
String updateTime = fileList.getString("updTime");
|
||||||
|
if (StringUtils.isNotBlank(updateTime)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("updateTime", updateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建时间
|
||||||
|
String createTime = null;
|
||||||
|
|
||||||
|
// 分享信息
|
||||||
|
if (shareInfo != null) {
|
||||||
|
// 分享ID
|
||||||
|
Integer shareId = shareInfo.getInteger("shareId");
|
||||||
|
if (shareId != null) {
|
||||||
|
shareLinkInfo.getOtherParam().put("shareId", shareId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传时间
|
||||||
|
String addTime = shareInfo.getString("addTime");
|
||||||
|
if (StringUtils.isNotBlank(addTime)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("createTime", addTime);
|
||||||
|
createTime = addTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览次数
|
||||||
|
Integer previewNum = shareInfo.getInteger("previewNum", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("previewCount", previewNum);
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
JsonObject userMap = shareInfo.getJsonObject("map");
|
||||||
|
if (userMap != null) {
|
||||||
|
String userName = userMap.getString("userName");
|
||||||
|
if (StringUtils.isNotBlank(userName)) {
|
||||||
|
shareLinkInfo.getOtherParam().put("userName", userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIP信息
|
||||||
|
Integer isVip = userMap.getInteger("isVip", 0);
|
||||||
|
shareLinkInfo.getOtherParam().put("isVip", isVip == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 FileInfo 对象并存入 otherParam
|
||||||
|
FileInfo fileInfoObj = new FileInfo()
|
||||||
|
.setPanType(shareLinkInfo.getType())
|
||||||
|
.setFileName(fileName)
|
||||||
|
.setFileId(fileList.getLong("fileId") != null ? fileList.getLong("fileId").toString() : null)
|
||||||
|
.setSize(fileSize)
|
||||||
|
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
|
||||||
|
.setFileType(fileType == 1 ? "file" : "folder")
|
||||||
|
.setFileIcon(fileList.getString("fileIcon"))
|
||||||
|
.setDownloadCount(downloads)
|
||||||
|
.setCreateTime(createTime)
|
||||||
|
.setUpdateTime(updateTime);
|
||||||
|
shareLinkInfo.getOtherParam().put("fileInfo", fileInfoObj);
|
||||||
|
|
||||||
|
log.debug("提取文件信息成功: fileName={}, fileSize={}, downloads={}",
|
||||||
|
fileName, fileSize, downloads);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("提取文件信息失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void down(HttpResponse<Buffer> res2) {
|
||||||
|
MultiMap headers = res2.headers();
|
||||||
|
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) {
|
||||||
|
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
promise.complete(headers.get("Location"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目录解析
|
||||||
|
@Override
|
||||||
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
|
Promise<List<FileInfo>> promise = Promise.promise();
|
||||||
|
|
||||||
|
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||||
|
|
||||||
|
// 如果参数里的目录ID不为空,则直接解析目录
|
||||||
|
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||||
|
if (dirId != null && !dirId.isEmpty()) {
|
||||||
|
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
|
||||||
|
parserDir(dirId, shareId, promise);
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
parse().onSuccess(id -> {
|
||||||
|
parserDir(id, shareId, promise);
|
||||||
|
}).onFailure(failRes -> {
|
||||||
|
log.error("解析目录失败: {}", failRes.getMessage());
|
||||||
|
promise.fail(failRes);
|
||||||
|
});
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
|
||||||
|
if (id != null && (id.startsWith("http://") || id.startsWith("https://"))) {
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
fileInfo.setFileName(id)
|
||||||
|
.setFileId(id)
|
||||||
|
.setFileType("file")
|
||||||
|
.setParserUrl(id)
|
||||||
|
.setPanType(shareLinkInfo.getType());
|
||||||
|
List<FileInfo> result = new ArrayList<>();
|
||||||
|
result.add(fileInfo);
|
||||||
|
promise.complete(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowTs = System.currentTimeMillis();
|
||||||
|
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
||||||
|
|
||||||
|
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
|
||||||
|
|
||||||
|
// 检查是否需要登录(有认证信息且需要使用认证)
|
||||||
|
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||||
|
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||||
|
log.debug("目录解析检查认证: isTempAuth={}, authFlag={}, token={}", isTempAuth, authFlag, token != null ? "已有" : "null");
|
||||||
|
|
||||||
|
if ((isTempAuth || authFlag) && token == null) {
|
||||||
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
|
log.info("目录解析需要登录,开始执行登录...");
|
||||||
|
// 先登录获取 token
|
||||||
|
login(tsEncode, auths)
|
||||||
|
.onFailure(err -> {
|
||||||
|
log.warn("目录解析登录失败,使用免登录模式: {}", err.getMessage());
|
||||||
|
// 登录失败,继续使用免登录
|
||||||
|
requestDirList(id, shareId, tsEncode, promise);
|
||||||
|
})
|
||||||
|
.onSuccess(r -> {
|
||||||
|
log.info("目录解析登录成功,token={}, 使用 VIP 模式", token != null ? token.substring(0, 10) + "..." : "null");
|
||||||
|
requestDirList(id, shareId, tsEncode, promise);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (token != null) {
|
||||||
|
log.debug("目录解析已有 token,直接使用 VIP 模式");
|
||||||
|
} else {
|
||||||
|
log.debug("目录解析: authFlag=false 或为临时认证但已失败,使用免登录模式");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("目录解析无认证信息,使用免登录模式");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无需登录或已登录,直接请求
|
||||||
|
requestDirList(id, shareId, tsEncode, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求目录列表
|
||||||
|
*/
|
||||||
|
private void requestDirList(String id, String shareId, String tsEncode, Promise<List<FileInfo>> promise) {
|
||||||
|
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.setTemplateParam("folderId", id)
|
||||||
|
.send().onSuccess(res -> {
|
||||||
|
String resBody = asText(res);
|
||||||
|
// 检查是否包含 cookie 验证
|
||||||
|
if (resBody.contains("var arg1='")) {
|
||||||
|
log.debug("目录解析需要 cookie 验证,重新创建 session");
|
||||||
|
webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
|
setCookie(resBody);
|
||||||
|
// 重新请求目录列表
|
||||||
|
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.setTemplateParam("folderId", id)
|
||||||
|
.send().onSuccess(res2 -> {
|
||||||
|
processDirResponse(res2, shareId, promise);
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.error("目录解析重试失败: {}", err.getMessage());
|
||||||
|
promise.fail("目录解析失败: " + err.getMessage());
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processDirResponse(res, shareId, promise);
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.error("目录解析请求失败: {}", err.getMessage());
|
||||||
|
promise.fail("目录解析失败: " + err.getMessage());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理目录解析响应
|
||||||
|
*/
|
||||||
|
private void processDirResponse(HttpResponse<Buffer> res, String shareId, Promise<List<FileInfo>> promise) {
|
||||||
|
try {
|
||||||
|
JsonObject jsonObject = asJson(res);
|
||||||
|
log.debug("目录解析响应: {}", jsonObject.encodePrettily());
|
||||||
|
|
||||||
|
if (!jsonObject.containsKey("list")) {
|
||||||
|
log.error("目录解析响应缺少 list 字段: {}", jsonObject);
|
||||||
|
promise.fail("目录解析失败: 响应格式错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray list = jsonObject.getJsonArray("list");
|
||||||
|
ArrayList<FileInfo> result = new ArrayList<>();
|
||||||
|
list.forEach(item->{
|
||||||
|
JsonObject fileJson = (JsonObject) item;
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
|
||||||
|
// 映射已知字段
|
||||||
|
String fileId = fileJson.getString("fileId");
|
||||||
|
String userId = fileJson.getString("userId");
|
||||||
|
|
||||||
|
// 其他参数 - 每个文件使用新的时间戳
|
||||||
|
long nowTs2 = System.currentTimeMillis();
|
||||||
|
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
|
||||||
|
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||||
|
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
|
||||||
|
|
||||||
|
// 回传用到的参数(包含 token)
|
||||||
|
JsonObject entries = JsonObject.of(
|
||||||
|
"fidEncode", fidEncode,
|
||||||
|
"uuid", uuid,
|
||||||
|
"ts", tsEncode2,
|
||||||
|
"auth", auth,
|
||||||
|
"shareId", shareId,
|
||||||
|
"appToken", token != null ? token : "");
|
||||||
|
String param = CommonUtils.urlBase64Encode(entries.encode());
|
||||||
|
|
||||||
|
if (fileJson.getInteger("fileType") == 2) {
|
||||||
|
// 如果是目录
|
||||||
|
fileInfo.setFileName(fileJson.getString("name"))
|
||||||
|
.setFileId(fileJson.getString("folderId"))
|
||||||
|
.setCreateTime(fileJson.getString("updTime"))
|
||||||
|
.setFileType("folder")
|
||||||
|
.setSize(0L)
|
||||||
|
.setSizeStr("0B")
|
||||||
|
.setCreateBy(fileJson.getLong("userId").toString())
|
||||||
|
.setDownloadCount(fileJson.getInteger("fileDownloads"))
|
||||||
|
.setCreateTime(fileJson.getString("updTime"))
|
||||||
|
.setFileIcon(fileJson.getString("fileIcon"))
|
||||||
|
.setPanType(shareLinkInfo.getType())
|
||||||
|
// 设置目录解析的URL
|
||||||
|
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
|
||||||
|
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
|
||||||
|
result.add(fileInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long fileSize = fileJson.getLong("fileSize") * 1024;
|
||||||
|
fileInfo.setFileName(fileJson.getString("fileName"))
|
||||||
|
.setFileId(fileId)
|
||||||
|
.setCreateTime(fileJson.getString("createTime"))
|
||||||
|
.setFileType("file")
|
||||||
|
.setSize(fileSize)
|
||||||
|
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
|
||||||
|
.setCreateBy(fileJson.getLong("userId").toString())
|
||||||
|
.setDownloadCount(fileJson.getInteger("fileDownloads"))
|
||||||
|
.setCreateTime(fileJson.getString("updTime"))
|
||||||
|
.setFileIcon(fileJson.getString("fileIcon"))
|
||||||
|
.setPanType(shareLinkInfo.getType())
|
||||||
|
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
|
||||||
|
shareLinkInfo.getType(), param))
|
||||||
|
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
|
||||||
|
shareLinkInfo.getType(), param));
|
||||||
|
result.add(fileInfo);
|
||||||
|
});
|
||||||
|
promise.complete(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理目录响应异常: {}", e.getMessage(), e);
|
||||||
|
promise.fail("目录解析失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parseById() {
|
||||||
|
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
||||||
|
String appToken = paramJson.getString("appToken", "");
|
||||||
|
|
||||||
|
// 如果有 token,使用 VIP 接口
|
||||||
|
if (StringUtils.isNotBlank(appToken)) {
|
||||||
|
log.debug("parseById 使用 VIP 接口, appToken={}", appToken.substring(0, Math.min(10, appToken.length())) + "...");
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||||
|
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
||||||
|
.setTemplateParam("ts", paramJson.getString("ts"))
|
||||||
|
.setTemplateParam("auth", paramJson.getString("auth"))
|
||||||
|
.setTemplateParam("appToken", appToken)
|
||||||
|
.send().onSuccess(this::down).onFailure(handleFail("parseById-VIP"));
|
||||||
|
} else {
|
||||||
|
// 无 token,使用免登录接口
|
||||||
|
log.debug("parseById 使用免登录接口");
|
||||||
|
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||||
|
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
||||||
|
.setTemplateParam("ts", paramJson.getString("ts"))
|
||||||
|
.setTemplateParam("auth", paramJson.getString("auth"))
|
||||||
|
.setTemplateParam("dataKey", paramJson.getString("shareId"))
|
||||||
|
.send().onSuccess(this::down).onFailure(handleFail("parseById"));
|
||||||
|
}
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void resetToken() {
|
||||||
|
token = null;
|
||||||
|
authFlag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -259,32 +259,14 @@ public class QkTool extends PanBase {
|
|||||||
|
|
||||||
log.debug("找到 {} 个文件", files.size());
|
log.debug("找到 {} 个文件", files.size());
|
||||||
|
|
||||||
// 提取第一个文件的信息并保存到 otherParam
|
// 构建文件映射和文件ID列表(参考 kuake.py:下载结果通过 fid 回填文件信息)
|
||||||
try {
|
|
||||||
JsonObject firstFile = files.get(0);
|
|
||||||
FileInfo fileInfo = new FileInfo();
|
|
||||||
fileInfo.setFileId(firstFile.getString("fid"))
|
|
||||||
.setFileName(firstFile.getString("file_name"))
|
|
||||||
.setSize(firstFile.getLong("size", 0L))
|
|
||||||
.setSizeStr(FileSizeConverter.convertToReadableSize(firstFile.getLong("size", 0L)))
|
|
||||||
.setFileType(firstFile.getBoolean("file", true) ? "file" : "folder")
|
|
||||||
.setCreateTime(firstFile.getString("updated_at"))
|
|
||||||
.setUpdateTime(firstFile.getString("updated_at"))
|
|
||||||
.setPanType(shareLinkInfo.getType());
|
|
||||||
|
|
||||||
// 保存到 otherParam,供 CacheServiceImpl 使用
|
|
||||||
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
|
||||||
log.debug("夸克提取文件信息: {}", fileInfo.getFileName());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("夸克提取文件信息失败,继续解析: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取文件ID列表
|
|
||||||
List<String> fileIds = new ArrayList<>();
|
List<String> fileIds = new ArrayList<>();
|
||||||
|
Map<String, JsonObject> fileMap = new HashMap<>();
|
||||||
for (JsonObject file : files) {
|
for (JsonObject file : files) {
|
||||||
String fid = file.getString("fid");
|
String fid = file.getString("fid");
|
||||||
if (fid != null && !fid.isEmpty()) {
|
if (fid != null && !fid.isEmpty()) {
|
||||||
fileIds.add(fid);
|
fileIds.add(fid);
|
||||||
|
fileMap.put(fid, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,20 +276,55 @@ public class QkTool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 第三步:批量获取下载链接
|
// 第三步:批量获取下载链接
|
||||||
getDownloadLinksBatch(fileIds, stoken)
|
getDownloadLinksBatch(fileIds)
|
||||||
.onSuccess(downloadData -> {
|
.onSuccess(downloadData -> {
|
||||||
if (downloadData.isEmpty()) {
|
if (downloadData.isEmpty()) {
|
||||||
fail("未能获取到下载链接");
|
fail("未能获取到下载链接");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取第一个文件的下载链接
|
// 按 fid 对齐下载结果和文件信息,取首个有效下载链接
|
||||||
String downloadUrl = downloadData.get(0).getString("download_url");
|
String downloadUrl = null;
|
||||||
|
JsonObject matchedFile = null;
|
||||||
|
for (JsonObject item : downloadData) {
|
||||||
|
String fid = item.getString("fid");
|
||||||
|
String currentUrl = item.getString("download_url");
|
||||||
|
if (currentUrl != null && !currentUrl.isEmpty() && fid != null) {
|
||||||
|
JsonObject fileMeta = fileMap.get(fid);
|
||||||
|
if (fileMeta != null) {
|
||||||
|
downloadUrl = currentUrl;
|
||||||
|
matchedFile = fileMeta;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (downloadUrl == null || downloadUrl.isEmpty()) {
|
if (downloadUrl == null || downloadUrl.isEmpty()) {
|
||||||
fail("下载链接为空");
|
fail("下载链接为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取匹配文件的信息并保存到 otherParam
|
||||||
|
if (matchedFile != null) {
|
||||||
|
try {
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
fileInfo.setFileId(matchedFile.getString("fid"))
|
||||||
|
.setFileName(matchedFile.getString("file_name"))
|
||||||
|
.setSize(matchedFile.getLong("size", 0L))
|
||||||
|
.setSizeStr(FileSizeConverter.convertToReadableSize(matchedFile.getLong("size", 0L)))
|
||||||
|
.setFileType(matchedFile.getBoolean("file", true) ? "file" : "folder")
|
||||||
|
.setCreateTime(matchedFile.getString("updated_at"))
|
||||||
|
.setUpdateTime(matchedFile.getString("updated_at"))
|
||||||
|
.setPanType(shareLinkInfo.getType());
|
||||||
|
|
||||||
|
// 保存到 otherParam,供 CacheServiceImpl 使用
|
||||||
|
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
||||||
|
log.debug("夸克提取文件信息: {}", fileInfo.getFileName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("夸克提取文件信息失败,继续解析: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 夸克网盘需要配合下载请求头,保存下载请求头
|
// 夸克网盘需要配合下载请求头,保存下载请求头
|
||||||
Map<String, String> downloadHeaders = new HashMap<>();
|
Map<String, String> downloadHeaders = new HashMap<>();
|
||||||
downloadHeaders.put(HttpHeaders.COOKIE.toString(), header.get(HttpHeaders.COOKIE));
|
downloadHeaders.put(HttpHeaders.COOKIE.toString(), header.get(HttpHeaders.COOKIE));
|
||||||
@@ -329,17 +346,17 @@ public class QkTool extends PanBase {
|
|||||||
/**
|
/**
|
||||||
* 批量获取下载链接(分批处理)
|
* 批量获取下载链接(分批处理)
|
||||||
*/
|
*/
|
||||||
private Future<List<JsonObject>> getDownloadLinksBatch(List<String> fileIds, String stoken) {
|
private Future<List<JsonObject>> getDownloadLinksBatch(List<String> fileIds) {
|
||||||
List<JsonObject> allResults = new ArrayList<>();
|
List<JsonObject> allResults = new ArrayList<>();
|
||||||
Promise<List<JsonObject>> promise = Promise.promise();
|
Promise<List<JsonObject>> promise = Promise.promise();
|
||||||
|
|
||||||
// 同步处理每个批次
|
// 同步处理每个批次
|
||||||
processBatch(fileIds, stoken, 0, allResults, promise);
|
processBatch(fileIds, 0, allResults, promise);
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processBatch(List<String> fileIds, String stoken, int startIndex, List<JsonObject> allResults, Promise<List<JsonObject>> promise) {
|
private void processBatch(List<String> fileIds, int startIndex, List<JsonObject> allResults, Promise<List<JsonObject>> promise) {
|
||||||
if (startIndex >= fileIds.size()) {
|
if (startIndex >= fileIds.size()) {
|
||||||
// 所有批次处理完成
|
// 所有批次处理完成
|
||||||
promise.complete(allResults);
|
promise.complete(allResults);
|
||||||
@@ -382,7 +399,7 @@ public class QkTool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理下一批次
|
// 处理下一批次
|
||||||
processBatch(fileIds, stoken, endIndex, allResults, promise);
|
processBatch(fileIds, endIndex, allResults, promise);
|
||||||
})
|
})
|
||||||
.onFailure(t -> promise.fail("获取下载链接失败: " + t.getMessage()));
|
.onFailure(t -> promise.fail("获取下载链接失败: " + t.getMessage()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 项目简介移到卡片内 -->
|
<!-- 项目简介移到卡片内 -->
|
||||||
<div class="project-intro">
|
<div class="project-intro">
|
||||||
<div class="intro-title">NFD网盘直链解析0.2.1</div>
|
<div class="intro-title">NFD网盘直链解析0.2.1b2</div>
|
||||||
<div class="intro-desc">
|
<div class="intro-desc">
|
||||||
<div>支持网盘:蓝奏云、蓝奏云优享、小飞机盘、123云盘、奶牛快传、移动云空间、QQ邮箱云盘、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"> >> </el-link></div>
|
<div>支持网盘:蓝奏云、蓝奏云优享、小飞机盘、123云盘、奶牛快传、移动云空间、QQ邮箱云盘、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"> >> </el-link></div>
|
||||||
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
||||||
@@ -957,7 +957,8 @@ export default {
|
|||||||
} else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') {
|
} else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') {
|
||||||
// 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证
|
// 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证
|
||||||
const hasAuth = this.allAuthConfigs[panType]?.cookie ||
|
const hasAuth = this.allAuthConfigs[panType]?.cookie ||
|
||||||
this.allAuthConfigs[panType]?.username
|
this.allAuthConfigs[panType]?.username ||
|
||||||
|
(this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0
|
||||||
if (!hasAuth) {
|
if (!hasAuth) {
|
||||||
this.$message.info({
|
this.$message.info({
|
||||||
message: `${panName}的大文件解析需要配置认证信息,请在"配置认证"中添加`,
|
message: `${panName}的大文件解析需要配置认证信息,请在"配置认证"中添加`,
|
||||||
|
|||||||
4
web-service/src/main/resources/secret.yml
Normal file
4
web-service/src/main/resources/secret.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# This file contains sensitive information and should not be committed to version control.
|
||||||
|
# It is used to store the encryption key for the application.
|
||||||
|
encrypt:
|
||||||
|
key: "nfd_secret_key_32bytes_2026_abcd"
|
||||||
Reference in New Issue
Block a user