Compare commits

...

38 Commits

Author SHA1 Message Date
q
6647fc5371 fixed. ye解析,去除正则匹配, 分享key去除后缀, #123, #125 2025-09-15 09:44:32 +08:00
q
b67544f0cd fixed. ye解析,去除正则匹配, #124,#125 2025-09-15 09:25:39 +08:00
qaiu
ef5826a73b Merge pull request #124 from qaiu/dependabot/npm_and_yarn/web-front/axios-1.12.0
Bump axios from 1.11.0 to 1.12.0 in /web-front
2025-09-12 17:41:37 +08:00
dependabot[bot]
a48adbd0df Bump axios from 1.11.0 to 1.12.0 in /web-front
Bumps [axios](https://github.com/axios/axios) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-12 09:37:49 +00:00
q
5c60493a24 修复123解析 #123 2025-09-12 17:34:07 +08:00
q
55e6227de0 优化docker脚本5 2025-09-10 18:23:58 +08:00
q
24a7395004 优化docker脚本4 2025-09-10 18:16:37 +08:00
q
b2a7187fc5 优化docker脚本3 2025-09-10 18:10:51 +08:00
q
ace7cdc88e 优化docker脚本2 2025-09-10 18:05:14 +08:00
q
2e909b5868 优化docker脚本 2025-09-10 17:53:49 +08:00
q
de78bcbc98 docker多平台支持,add 树莓派 2025-09-10 17:45:27 +08:00
q
c560f0e902 docker多平台支持 2025-09-10 17:27:44 +08:00
q
88860c9302 docker多平台支持 2025-09-10 17:21:33 +08:00
q
ef65d0e095 Merge remote-tracking branch 'origin/main' 2025-09-10 17:10:52 +08:00
q
6438505f4a icloud国际版支持,QQ邮箱文件信息获取 2025-09-10 17:10:39 +08:00
qaiu
1be5030dd1 添加docker多平台支持 2025-09-10 17:06:40 +08:00
q
421b2f4a42 Merge remote-tracking branch 'origin/main' 2025-08-19 18:57:00 +08:00
q
a66bf84381 直链API添加文件信息
修复蓝奏目录文件大小处理报错问题 #120
2025-08-19 18:56:42 +08:00
qaiu
0c4d366d6d 更新 README.md 2025-08-14 19:36:09 +08:00
qaiu
a1d0a921fa 更新 README.md 2025-08-14 19:28:44 +08:00
q
2092230a61 更新文档 2025-08-14 15:27:03 +08:00
q
6e5ae6eff3 Merge remote-tracking branch 'origin/main' 2025-08-12 13:32:09 +08:00
q
4f8259d772 1. iz match fixed
2. redirect res content add "text/html; charset=utf-8"
2025-08-12 13:29:59 +08:00
qaiu
8b987d9824 Update README.md 2025-08-11 13:32:33 +08:00
q
e8ba451d18 build status 2025-08-11 13:26:06 +08:00
q
77758db463 Merge remote-tracking branch 'origin/main' 2025-08-11 13:23:59 +08:00
q
6c58598a8e 用户API 2025-08-11 13:23:30 +08:00
qaiu
3ac35230a3 Update README.md 2025-08-11 13:18:00 +08:00
q
ca91302d28 Merge remote-tracking branch 'origin/main' 2025-08-11 13:14:59 +08:00
q
e07272a5dc 添加支持 QQ闪传,微雨云,优化前端逻辑 2025-08-11 13:14:43 +08:00
qaiu
461305e1df Update README.md 2025-08-08 12:33:46 +08:00
q
8e8ab10a0f Merge remote-tracking branch 'origin/main' 2025-08-05 15:50:50 +08:00
q
e754326925 fixed p118 link 2025-08-05 15:50:32 +08:00
qaiu
4c92994c6f 更新 README.md 2025-08-05 13:14:09 +08:00
qaiu
66c57f47ac Merge pull request #113 from qaiu/dependabot/maven/org.apache.commons-commons-lang3-3.18.0
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0
2025-07-30 09:17:42 +08:00
dependabot[bot]
ec689eadd8 Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0
Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 09:05:14 +00:00
qaiu
c1e15709a7 Merge pull request #110 from qaiu/dependabot/maven/core-database/org.apache.commons-commons-lang3-3.18.0
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0 in /core-database
2025-07-21 17:02:12 +08:00
dependabot[bot]
4460659210 Bump org.apache.commons:commons-lang3 in /core-database
Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 01:16:21 +00:00
36 changed files with 1767 additions and 204 deletions

View File

@@ -80,6 +80,8 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract git tag
id: tag
@@ -93,6 +95,7 @@ jobs:
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
ghcr.io/qaiu/netdisk-fast-download:latest

View File

@@ -1,7 +1,10 @@
FROM eclipse-temurin:17-alpine
FROM eclipse-temurin:17-jre
WORKDIR /app
# 安装 unzip
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
COPY ./web-service/target/netdisk-fast-download-bin.zip .
RUN unzip netdisk-fast-download-bin.zip && \

View File

@@ -3,7 +3,7 @@
</p>
<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.9b2&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=v0.1.9b8a&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://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.6-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>
@@ -18,8 +18,28 @@ QQ群1017480890
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
[预览地址1](https://lz.qaiu.top)
[预览地址2](http://www.722shop.top:6401)
## 快速开始
命令行下载分享文件:
```shell
curl -LOJ "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
```
或者使用wget:
```shell
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
```
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4):
```
### 调用演示站下载:
https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234
### 调用演示站预览:
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4
```
## 预览地址
[预览地址1](https://lz.qaiu.top)
[预览地址2](http://www.722shop.top:6401)
[天翼云盘大文件解析限时开放](https://189.qaiu.top)
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
@@ -44,7 +64,8 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [118网盘(已停服)-p118](https://www.118pan.com/)
- [文叔叔-ws](https://www.wenshushu.cn/)
- [联想乐云-le](https://lecloud.lenovo.com/)
- [QQ邮箱文件中转站-qq](https://mail.qq.com/)
- [QQ邮箱云盘-qqw](https://mail.qq.com/)
- [QQ闪传-qqsc](https://nutty.qq.com/nutty/ssr/26797.html)
- [城通网盘-ct](https://www.ctfile.com)
- [网易云音乐分享链接-mnes](https://music.163.com)
- [酷狗音乐分享链接-mkgs](https://www.kugou.com)
@@ -63,7 +84,7 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [联通云盘-pwo](https://pan.wo.cn/)
- [天翼云盘-p189](https://cloud.189.cn/)
### API接口说明
## API接口说明
your_host指的是您的域名或者IP实际使用时替换为实际域名或者IP端口默认6400可以使用nginx代理来做域名访问。
解析方式分为两种类型直接跳转下载文件和获取下载链接,
每一种都提供了两种接口形式: `通用接口parser?url=``网盘标志/分享key拼接的短地址标志短链`,所有规则参考示例。
@@ -91,7 +112,7 @@ API规则:
### json接口说明
1. 文件解析:/json/parser?url=分享链接&pwd=xxx
#### 1. 文件解析:/json/parser?url=分享链接&pwd=xxx
json返回数据格式示例:
`shareKey`: 全局分享key
@@ -115,7 +136,7 @@ json返回数据格式示例:
"timestamp": 1726637151902
}
```
2. 分享链接详情接口 /v2/linkInfo?url=分享链接
#### 2. 分享链接详情接口 /v2/linkInfo?url=分享链接
```json
{
"code": 200,
@@ -144,7 +165,7 @@ json返回数据格式示例:
"timestamp": 1736489219402
}
```
3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
#### 3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
/v2/getFileList?url=分享链接&pwd=分享密码
```json
@@ -165,7 +186,7 @@ json返回数据格式示例:
"updateTime": null,
"createBy": null,
"description": null,
"downloadCount": 下载次数,
"downloadCount": "下载次数",
"panType": "lz",
"parserUrl": "下载链接/文件夹链接",
"extParameters": null
@@ -173,7 +194,7 @@ json返回数据格式示例:
]
}
```
4. 解析次数统计接口 /v2/statisticsInfo
#### 4. 解析次数统计接口 /v2/statisticsInfo
```json
{
"code": 200,
@@ -268,15 +289,15 @@ mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
docker pull ghcr.io/qaiu/netdisk-fast-download:lastest
docker pull ghcr.io/qaiu/netdisk-fast-download:latest
# 复制配置文件或下载仓库web-service\src\main\resources
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:lastest
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:latest
docker cp netdisk-fast-download:/app/resources ./resources
docker rm netdisk-fast-download
# 启动容器
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.io/qaiu/netdisk-fast-download:lastest
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.io/qaiu/netdisk-fast-download:latest
# 反代6401端口
@@ -291,15 +312,15 @@ mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
docker pull ghcr.nju.edu.cn/qaiu/netdisk-fast-download:lastest
docker pull ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
# 复制配置文件或下载仓库web-service\src\main\resources
docker create --name netdisk-fast-download ghcr.nju.edu.cn/qaiu/netdisk-fast-download:lastest
docker create --name netdisk-fast-download ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
docker cp netdisk-fast-download:/app/resources ./resources
docker rm netdisk-fast-download
# 启动容器
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.nju.edu.cn/qaiu/netdisk-fast-download:lastest
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
# 反代6401端口

14
bin/stop.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
# set -x
# 找到运行中的 Java 进程的 PID
PID=$(ps -ef | grep 'netdisk-fast-download.jar' | grep -v grep | awk '{print $2}')
if [ -z "$PID" ]; then
echo "未找到正在运行的进程 netdisk-fast-download.jar"
exit 1
else
# 杀掉进程
echo "停止 netdisk-fast-download.jar (PID: $PID)..."
kill -9 "$PID"
fi

View File

@@ -41,7 +41,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
<version>3.18.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>

View File

@@ -8,7 +8,11 @@ public interface ConfigConstant {
String SERVER = "server";
String CACHE = "cache";
String PROXY_SERVER = "proxy-server";
String PROXY = "proxy";
String AUTHS = "auths";
String GLOBAL_CONFIG = "globalConfig";
String CUSTOM_CONFIG = "customConfig";
String ASYNC_SERVICE_INSTANCES = "asyncServiceInstances";

View File

@@ -12,7 +12,8 @@ import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
public class ResponseUtil {
public static void redirect(HttpServerResponse response, String url) {
response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
response.putHeader(CONTENT_TYPE, "text/html; charset=utf-8")
.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
}
public static void redirect(HttpServerResponse response, String url, Promise<?> promise) {

View File

@@ -72,6 +72,9 @@ public class FileInfo {
//预览地址
private String previewUrl;
// 文件hash默认类型为md5
private String hash;
/**
* 扩展参数
*/
@@ -210,6 +213,15 @@ public class FileInfo {
return this;
}
public String getHash() {
return hash;
}
public FileInfo setHash(String hash) {
this.hash = hash;
return this;
}
public Map<String, Object> getExtParameters() {
return extParameters;
}

View File

@@ -202,17 +202,18 @@ public abstract class PanBase implements IPanTool {
try {
log.error(decompressGzip((Buffer) res.body()));
fail(decompressGzip((Buffer) res.body()));
throw new RuntimeException("响应不是JSON格式");
//throw new RuntimeException("响应不是JSON格式");
} catch (IOException ex) {
log.error("响应gzip解压失败");
fail("响应gzip解压失败: {}", ex.getMessage());
throw new RuntimeException("响应gzip解压失败", ex);
//throw new RuntimeException("响应gzip解压失败", ex);
}
} else {
log.error("解析失败: json格式异常: {}", res.bodyAsString());
fail("解析失败: json格式异常: {}", res.bodyAsString());
throw new RuntimeException("解析失败: json格式异常");
//throw new RuntimeException("解析失败: json格式异常");
}
return JsonObject.of();
}
}
@@ -233,8 +234,9 @@ public abstract class PanBase implements IPanTool {
}
} catch (Exception e) {
fail("解析失败: res格式异常");
throw new RuntimeException("解析失败: res格式异常");
//throw new RuntimeException("解析失败: res格式异常");
}
return null;
}
protected void complete(String url) {

View File

@@ -23,9 +23,87 @@ import static java.util.regex.Pattern.compile;
public enum PanDomainTemplate {
// https://www.ilanzou.com/s/
IZ("蓝奏云优享",
compile("https://www\\.ilanzou\\.com/s/(?<KEY>.+)"),
"https://www.ilanzou.com/s/{shareKey}",
IzTool.class),
// 网盘定义
/*
lanzoul.com
lanzouh.com
lanosso.com
lanpv.com
bakstotre.com
lanzouo.com
lanzov.com
lanpw.com
ulanzou.com
lanzouf.com
lanzn.com
lanzouj.com
lanzouk.com
lanzouq.com
lanzouv.com
lanzoue.com
lanzouw.com
lanzoub.com
lanzouu.com
lanwp.com
lanzouy.com
lanzoup.com
woozooo.com
lanzv.com
dmpdmp.com
lanrar.com
webgetstore.com
lanzb.com
lanzoux.com
lanzout.com
lanzouc.com
ilanzou.com
lanzoui.com
lanzoug.com
lanzoum.com
t-is.cn
*/
LZ("蓝奏云",
compile("https://(?:[a-zA-Z\\d-]+\\.)?((lanzou[a-z])|(lanzn))\\.com/(.+/)?(?<KEY>.+)"),
compile("https://(?:[a-zA-Z\\d-]+\\.)?(" +
"lanzoul|" +
"lanzouh|" +
"lanosso|" +
"lanpv|" +
"bakstotre|" +
"lanzouo|" +
"lanzov|" +
"lanpw|" +
"ulanzou|" +
"lanzouf|" +
"lanzn|" +
"lanzouj|" +
"lanzouk|" +
"lanzouq|" +
"lanzouv|" +
"lanzoue|" +
"lanzouw|" +
"lanzoub|" +
"lanzouu|" +
"lanwp|" +
"lanzouy|" +
"lanzoup|" +
"woozooo|" +
"lanzv|" +
"dmpdmp|" +
"lanrar|" +
"webgetstore|" +
"lanzb|" +
"lanzoux|" +
"lanzout|" +
"lanzouc|" +
"lanzoui|" +
"lanzoug|" +
"lanzoum" +
")\\.com/(.+/)?(?<KEY>.+)"),
"https://lanzoux.com/{shareKey}",
LzTool.class),
@@ -48,11 +126,6 @@ public enum PanDomainTemplate {
"https://v2.fangcloud.com/s/{shareKey}",
"https://www.fangcloud.com/",
FcTool.class),
// https://www.ilanzou.com/s/
IZ("蓝奏云优享",
compile("https://www\\.ilanzou\\.com/s/(?<KEY>.+)"),
"https://www.ilanzou.com/s/{shareKey}",
IzTool.class),
// https://wx.mail.qq.com/ftn/download?
QQ("QQ邮箱中转站",
compile("https://i?wx\\.mail\\.qq\\.com/ftn/download\\?(?<KEY>.+)"),
@@ -65,14 +138,68 @@ public enum PanDomainTemplate {
"https://wx.mail.qq.com/s?k={shareKey}",
"https://mail.qq.com",
QQwTool.class),
// https://qfile.qq.com/q/xxx
QQSC("QQ闪传",
compile("https://qfile\\.qq\\.com/q/(?<KEY>.+)"),
"https://qfile.qq.com/q/{shareKey}",
QQscTool.class),
// https://f.ws59.cn/f/或者https://www.wenshushu.cn/f/
WS("文叔叔",
compile("https://(f\\.ws(\\d{2})\\.cn|www\\.wenshushu\\.cn)/f/(?<KEY>.+)"),
"https://www.wenshushu.cn/f/{shareKey}",
WsTool.class),
// https://www.123pan.com/s/
/*
123254.com
123957.com
123295.com
123panpay.com
123860.com
123pan.com
123245.com
123278.com
123842.com
123294.com
123865.com
123773.com
123624.com
123684.com
123641.com
123259.com
123912.com
123952.com
123652.com
123pan.cn
123635.com
123242.com
123795.com
*/
YE("123网盘",
compile("https://www\\.(123pan\\.com|123865\\.com|123684\\.com|123912\\.com|123pan\\.cn)/s/(?<KEY>.+)(.html)?"),
compile("https://www\\.(" +
"123254\\.com|" +
"123957\\.com|" +
"123295\\.com|" +
"123panpay\\.com|" +
"123860\\.com|" +
"123pan\\.com|" +
"123245\\.com|" +
"123278\\.com|" +
"123842\\.com|" +
"123294\\.com|" +
"123865\\.com|" +
"123773\\.com|" +
"123624\\.com|" +
"123684\\.com|" +
"123641\\.com|" +
"123259\\.com|" +
"123912\\.com|" +
"123952\\.com|" +
"123652\\.com|" +
"123pan\\.cn|" +
"123635\\.com|" +
"123242\\.com|" +
"123795\\.com" +
")/s/(?<KEY>.+)(.html)?"),
"https://www.123pan.com/s/{shareKey}",
YeTool.class),
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
@@ -116,7 +243,7 @@ public enum PanDomainTemplate {
PgdTool.class),
// iCloud https://www.icloud.com.cn/iclouddrive/xxx#fonts
PIC("iCloud",
compile("https://www\\.icloud\\.com\\.cn/iclouddrive/(?<KEY>[a-z_A-Z\\d-=]+)(#(.+))?"),
compile("https://www\\.icloud\\.com(\\.cn)?/iclouddrive/(?<KEY>[a-z_A-Z\\d-=]+)(#(.+))?"),
"https://www.icloud.com.cn/iclouddrive/{shareKey}",
PicTool.class),
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/emqx-5.0.26-el7-amd64.tar.gz?rlkey=3uoi4bxz5mv93jmlaws0nlol1&e=8&st=fe0lclc2&dl=0
@@ -143,7 +270,7 @@ public enum PanDomainTemplate {
// http://163cn.tv/xxx
MNES("网易云音乐分享",
compile("http(s)?://163cn\\.tv/(?<KEY>.+)"),
"http://163cn.tv/{shareKey}",
"https://163cn.tv/{shareKey}",
MnesTool.class),
// https://music.163.com/#/song?id=xxx
MNE("网易云音乐歌曲详情",

View File

@@ -28,7 +28,7 @@ import java.util.regex.Pattern;
*/
public class LzTool extends PanBase {
public static final String SHARE_URL_PREFIX = "https://wwwa.lanzoux.com";
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoup.com";
public LzTool(ShareLinkInfo shareLinkInfo) {
@@ -225,7 +225,6 @@ public class LzTool extends PanBase {
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id)
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
shareLinkInfo.getType(), id));
;
log.debug("文件信息: {}", fileInfo);
list.add(fileInfo);
});

View File

@@ -31,18 +31,20 @@ public class P118Tool extends PanBase {
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
Matcher matcher = compile.matcher(res.bodyAsString());
if (matcher.find()) {
System.out.println(matcher.group(1));
complete(matcher.group(1));
//c: 0x63
//o: 0x6F
//m: 0x6D
//1: 0x31
///: 0x2F
char[] chars1 = new char[]{99, 111, 109, 49, 47};
char[] chars2 = new char[]{99, 111, 109, 47};
String group = matcher.group(1).replace(String.valueOf(chars1), String.valueOf(chars2));
System.out.println(group);
complete(group);
} else {
fail();
}
}).onFailure(handleFail(""));
return future();
}
// public static void main(String[] args) {
// String s = new P118Tool(ShareLinkInfo.newBuilder().shareUrl("https://xiguage.118pan.com/b11848261").shareKey(
// "11848261").build()).parseSync();
// System.out.println(s);
// }
}

View File

@@ -1,12 +1,21 @@
package cn.qaiu.parser.impl;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.HeaderUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.pointer.JsonPointer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate;
import java.util.List;
/**
* 微雨云
*/
@@ -14,6 +23,10 @@ public class PvyyTool extends PanBase {
private static final String API_URL_PREFIX1 = "https://www.vyuyun.com/apiv1/share/file/{key}?password={pwd}";
private static final String API_URL_PREFIX2 = "https://www.vyuyun.com/apiv1/share/getShareDownUrl/{key}/{id}?password={pwd}";
byte[] hexArray = {
0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x31, 0x36, 0x2e, 0x32, 0x30, 0x35, 0x2e,
0x39, 0x36, 0x2e, 0x31, 0x39, 0x38, 0x3a, 0x33, 0x30, 0x30, 0x30, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x2f
};
private static final MultiMap header = HeaderUtils.parseHeaders("""
accept-language: zh-CN,zh;q=0.9,en;q=0.8
@@ -32,21 +45,52 @@ public class PvyyTool extends PanBase {
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
""");
private final String api;
public PvyyTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
api = new String(hexArray);
}
@Override
public Future<String> parse() {
//
// 请求downcode
WebClient.create(WebClientVertxInit.get())
.getAbs(api + shareLinkInfo.getShareKey())
.send()
.onSuccess(res -> {
if (res.statusCode() == 200) {
String code = res.bodyAsString();
log.info("vyy url:{}, code:{}", shareLinkInfo.getStandardUrl(), code);
String downApi = API_URL_PREFIX2 + "&downcode=" + code;
getDownUrl(downApi);
} else {
fail("code获取失败");
}
}).onFailure(handleFail("code服务异常"));
return future();
}
private void getDownUrl(String apiUrl) {
client.getAbs(UriTemplate.of(API_URL_PREFIX1))
.setTemplateParam("key", shareLinkInfo.getShareKey())
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.putHeaders(header)
.send().onSuccess(res -> {
try {
String id = asJson(res).getJsonObject("data").getJsonObject("data").getString("id");
JsonObject resJson = asJson(res);
if (!resJson.containsKey("code") || resJson.getInteger("code") != 0) {
fail("获取文件信息失败: " + resJson.getString("message"));
return;
}
JsonObject fileData = resJson.getJsonObject("data").getJsonObject("data");
if (fileData == null) {
fail("文件数据为空");
return;
}
setFileInfo(fileData);
String id = fileData.getString("id");
client.getAbs(UriTemplate.of(API_URL_PREFIX2))
client.getAbs(UriTemplate.of(apiUrl))
.setTemplateParam("key", shareLinkInfo.getShareKey())
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.setTemplateParam("id", id)
@@ -61,10 +105,117 @@ public class PvyyTool extends PanBase {
}
});
} catch (Exception ignored) {
fail(asJson(res).encodePrettily());
fail();
}
});
return future();
}
private void setFileInfo(JsonObject fileData) {
JsonObject attributes = fileData.getJsonObject("attributes");
JsonObject user = (JsonObject)(JsonPointer.from("/relationships/user/data").queryJson(fileData));
int downCount = (Integer)(JsonPointer.from("/relationships/shared/data/attributes/down").queryJson(fileData));
String filesize = attributes.getString("filesize");
FileInfo fileInfo = new FileInfo()
.setFileId(fileData.getString("id"))
.setFileName(attributes.getString("basename"))
.setFileType(attributes.getString("mimetype"))
.setPanType(shareLinkInfo.getType())
.setCreateBy(user.getString("email"))
.setDownloadCount(downCount)
.setSize(FileSizeConverter.convertToBytes(filesize))
.setSizeStr(filesize);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
}
private static final String DIR_API = "https://www.vyuyun.com/apiv1/share/folders/809Pt6/bMjnUg?sort=created_at&direction=DESC&password={pwd}";
private static final String SHARE_TYPE_API = "https://www.vyuyun.com/apiv1/share/info/{key}?password={pwd}";
//
// @Override
// public Future<List<FileInfo>> parseFileList() {
// Promise<List<FileInfo>> promise = Promise.promise();
// client.getAbs(UriTemplate.of(SHARE_TYPE_API))
// .setTemplateParam("key", shareLinkInfo.getShareKey())
// .setTemplateParam("pwd", shareLinkInfo.getSharePassword()).send().onSuccess(res -> {
// // "data" -> "attributes"->type
// String type = asJson(res).getJsonObject("data").getJsonObject("attributes").getString("type");
// if ("folder".equals(type)) {
// // 文件夹
// client.getAbs(UriTemplate.of(DIR_API))
// .setTemplateParam("key", shareLinkInfo.getShareKey())
// .setTemplateParam("pwd", shareLinkInfo.getSharePassword())
// .send().onSuccess(res2 -> { try {
//
// try {
// // 新的解析逻辑
// var arr = asJson(res2).getJsonObject("data").getJsonArray("data");
// List<FileInfo> list = arr.stream().map(o -> {
// FileInfo fileInfo = new FileInfo();
// var jo = ((io.vertx.core.json.JsonObject) o).getJsonObject("data");
// String fileType = jo.getString("type");
// fileInfo.setFileId(jo.getString("id"));
// fileInfo.setFileName(jo.getJsonObject("attributes").getString("name"));
// // 文件大小可能为null或字符串
// Object sizeObj = jo.getJsonObject("attributes").getValue("filesize");
// if (sizeObj instanceof Number) {
// fileInfo.setSize(((Number) sizeObj).longValue());
// } else if (sizeObj instanceof String sizeStr) {
// try {
// getSize(fileInfo, sizeStr);
// } catch (Exception e) {
// fileInfo.setSize(0L);
// }
// } else {
// fileInfo.setSize(0L);
// }
// fileInfo.setFileType("folder".equals(fileType) ? "folder" : "file");
// return fileInfo;
// }).toList();
// promise.complete(list);
// } catch (Exception ignored) {
// promise.fail(asJson(res2).encodePrettily());
// }
// }).onFailure(t->{
// promise.fail("获取文件夹内容失败: " + t.getMessage());
// });
// } else if ("file".equals(type)) {
// // 单文件
// FileInfo fileInfo = new FileInfo();
// var jo = asJson(res).getJsonObject("data").getJsonObject("attributes");
// fileInfo.setFileId(asJson(res).getJsonObject("data").getString("id"));
// fileInfo.setFileName(jo.getString("name"));
// Object sizeObj = jo.getValue("filesize");
// if (sizeObj instanceof Number) {
// fileInfo.setSize(((Number) sizeObj).longValue());
// } else if (sizeObj instanceof String sizeStr) {
// try {
// getSize(fileInfo, sizeStr);
// } catch (Exception e) {
// fileInfo.setSize(0L);
// }
// } else {
// fileInfo.setSize(0L);
// }
// fileInfo.setFileType("file");
// promise.complete(List.of(fileInfo));
// } else {
// promise.fail("未知的分享类型");
// }
// });
// return promise.future();
// }
//
// private void getSize(FileInfo fileInfo, String sizeStr) {
// if (sizeStr.endsWith("KB")) {
// fileInfo.setSize(Long.parseLong(sizeStr.replace("KB", "").trim()) * 1024);
// } else if (sizeStr.endsWith("MB")) {
// fileInfo.setSize(Long.parseLong(sizeStr.replace("MB", "").trim()) * 1024 * 1024);
// } else {
// fileInfo.setSize(Long.parseLong(sizeStr));
// }
// }
//
// @Override
// public Future<String> parseById() {
// return super.parseById();
// }
}

View File

@@ -0,0 +1,171 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.HeaderUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* QQ闪传 <br>
* 只能客户端上传 支持Android QQ 9.2.5, MACOS QQ 6.9.78可生成分享链接通过浏览器下载支持超大文件有效期默认7天暂时没找到续期方法。<br>
*/
public class QQscTool extends PanBase {
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 MultiMap HEADERS = HeaderUtils.parseHeaders("""
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Cookie: uin=9000002; p_uin=9000002
DNT: 1
Origin: https://qfile.qq.com
Referer: https://qfile.qq.com/q/Xolxtv5b4O
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
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
accept: application/json
content-type: application/json
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
x-oidb: {"uint32_command":"0x9248", "uint32_service_type":"4"}
""");
public QQscTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
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 -> {
if (result.succeeded()) {
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());
promise.fail(result.cause());
}
});
return promise.future();
}
String getFileUUID(String htmlJs) {
String keyword = "\"download_limit_status\"";
String marker = "},\"";
int startIndex = htmlJs.indexOf(keyword);
if (startIndex != -1) {
int markerIndex = htmlJs.indexOf(marker, startIndex);
if (markerIndex != -1) {
int quoteStart = markerIndex + marker.length();
int quoteEnd = htmlJs.indexOf("\"", quoteStart);
if (quoteEnd != -1) {
String extracted = htmlJs.substring(quoteStart, quoteEnd);
LOG.debug("提取结果: {}", extracted);
return extracted;
} else {
LOG.error("未找到结束引号: {}", marker);
}
} else {
LOG.error("未找到标记: {} 在关键字: {} 之后", marker, keyword);
}
} else {
LOG.error("未找到关键字: {}", keyword);
}
return null;
}
public static String extractFileNameFromTitle(String content) {
// 匹配<title>和</title>之间的内容
Pattern pattern = Pattern.compile("<title>(.*?)</title>");
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
String fullTitle = matcher.group(1);
// 按 "" 分割,取前半部分
int sepIndex = fullTitle.indexOf("");
if (sepIndex != -1) {
return fullTitle.substring(0, sepIndex);
}
return fullTitle; // 如果没有分隔符,就返回全部
}
return null;
}
}

View File

@@ -1,5 +1,6 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import io.vertx.core.Future;
import java.util.HashMap;
@@ -15,9 +16,15 @@ public class QQwTool extends QQTool {
@Override
public Future<String> parse() {
client.getAbs(shareLinkInfo.getStandardUrl()).send().onSuccess(res -> {
client.getAbs(shareLinkInfo.getShareUrl()).send().onSuccess(res -> {
String html = res.bodyAsString();
String url = extractVariables(html).get("url");
Map<String, String> stringStringMap = extractVariables(html);
String url = stringStringMap.get("url");
String fn = stringStringMap.get("filename");
String size = stringStringMap.get("filesize");
String createBy = stringStringMap.get("nick");
FileInfo fileInfo = new FileInfo().setFileName(fn).setSize(Long.parseLong(size)).setCreateBy(createBy);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
if (url != null) {
String url302 = url.replace("\\x26", "&");
promise.complete(url302);
@@ -44,16 +51,17 @@ public class QQwTool extends QQTool {
private Map<String, String> extractVariables(String jsCode) {
Map<String, String> variables = new HashMap<>();
// 正则表达式匹配 var 变量定义
String regex = "var\\s+(\\w+)\\s*=\\s*([\"']?)([^\"';\\s]+)\\2\n";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(jsCode);
String regex = "\\s+var\\s+(\\w+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^;\\r\\n]*))";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(jsCode);
while (matcher.find()) {
String variableName = matcher.group(1); // 变量名
String variableValue = matcher.group(3); // 变量值
variables.put(variableName, variableValue);
while (m.find()) {
String name = m.group(1);
String value = m.group(2) != null ? m.group(2)
: m.group(3) != null ? m.group(3)
: m.group(4);
variables.put(name, value);
}
return variables;

View File

@@ -66,87 +66,41 @@ public class YeTool extends PanBase {
public Future<String> parse() {
final String dataKey = shareLinkInfo.getShareKey();
final String shareKey = shareLinkInfo.getShareKey().replaceAll("(\\..*)|(#.*)", "");
final String pwd = shareLinkInfo.getSharePassword();
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("key", dataKey).send().onSuccess(res -> {
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", pwd)
.setTemplateParam("ParentFileId", "0")
// .setTemplateParam("authKey", AESUtils.getAuthKey("/a/api/share/get"))
.putHeader("Platform", "web")
.putHeader("App-Version", "3")
.send().onSuccess(res2 -> {
JsonObject infoJson = asJson(res2);
if (infoJson.getInteger("code") != 0) {
fail("{} 状态码异常 {}", shareKey, infoJson);
return;
}
String html = res.bodyAsString();
// 判断分享是否已经失效
if (html.contains("分享链接已失效")) {
fail("该分享已失效({})已失效", shareLinkInfo.getShareUrl());
return;
}
JsonObject getFileInfoJson =
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
getFileInfoJson.put("ShareKey", shareKey);
Pattern compile = Pattern.compile("window.g_initialProps\\s*=\\s*(.*);");
Matcher matcher = compile.matcher(html);
if (!matcher.find()) {
fail("该分享({})文件信息找不到, 可能分享已失效", shareLinkInfo.getShareUrl());
return;
}
String fileInfoString = matcher.group(1);
JsonObject fileInfoJson = new JsonObject(fileInfoString);
JsonObject resJson = fileInfoJson.getJsonObject("res");
JsonObject resListJson = fileInfoJson.getJsonObject("reslist");
if (resJson == null || resJson.getInteger("code") != 0) {
fail(dataKey + " 解析到异常JSON: " + resJson);
return;
}
String shareKey = resJson.getJsonObject("data").getString("ShareKey");
if (resListJson == null || resListJson.getInteger("code") != 0) {
// 加密分享
if (StringUtils.isNotEmpty(pwd)) {
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", pwd)
.setTemplateParam("ParentFileId", "0")
// .setTemplateParam("authKey", AESUtils.getAuthKey("/a/api/share/get"))
.putHeader("Platform", "web")
.putHeader("App-Version", "3")
.send().onSuccess(res2 -> {
JsonObject infoJson = asJson(res2);
if (infoJson.getInteger("code") != 0) {
fail("{} 状态码异常 {}", dataKey, infoJson);
return;
}
JsonObject getFileInfoJson =
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
getFileInfoJson.put("ShareKey", shareKey);
// 判断是否为文件夹: data->InfoList->0->Type: 1为文件夹, 0为文件
try {
int type = (Integer)JsonPointer.from("/data/InfoList/0/Type").queryJson(infoJson);
if (type == 1) {
getZipDownUrl(client, getFileInfoJson);
return;
}
} catch (Exception exception) {
fail("该分享[{}]解析异常: {}", dataKey, exception.getMessage());
return;
}
getDownUrl(client, getFileInfoJson);
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
} else {
fail("该分享[{}]需要密码",dataKey);
}
return;
}
JsonObject reqBodyJson = resListJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
reqBodyJson.put("ShareKey", shareKey);
if (reqBodyJson.getInteger("Type") == 1) {
// 文件夹
getZipDownUrl(client, reqBodyJson);
return;
}
getDownUrl(client, reqBodyJson);
}).onFailure(this.handleFail(FIRST_REQUEST_URL));
// 判断是否为文件夹: data->InfoList->0->Type: 1为文件夹, 0为文件
try {
int type = (Integer)JsonPointer.from("/data/InfoList/0/Type").queryJson(infoJson);
if (type == 1) {
getZipDownUrl(client, getFileInfoJson);
return;
}
} catch (Exception exception) {
fail("该分享[{}]解析异常: {}", shareKey, exception.getMessage());
return;
}
getDownUrl(client, getFileInfoJson);
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
return promise.future();
}
@@ -243,7 +197,6 @@ public class YeTool extends PanBase {
String shareKey = shareLinkInfo.getShareKey(); // 分享链接的唯一标识
String pwd = shareLinkInfo.getSharePassword(); // 分享密码
String parentFileId = "0"; // 根目录的文件ID
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");

View File

@@ -7,8 +7,17 @@ public class FileSizeConverter {
throw new IllegalArgumentException("Invalid file size string");
}
sizeStr = sizeStr.trim().toUpperCase();
char unit = sizeStr.charAt(sizeStr.length() - 1);
sizeStr = sizeStr.replace(",","").trim().toUpperCase();
// 判断是2位单位还是1位单位
// 判断单位是否为2位
int unitIndex = sizeStr.length() - 1;
char unit = sizeStr.charAt(unitIndex);
if (Character.isLetter(sizeStr.charAt(unitIndex - 1))) {
unit = sizeStr.charAt(unitIndex - 1);
sizeStr = sizeStr.substring(0, unitIndex - 1);
} else {
sizeStr = sizeStr.substring(0, unitIndex);
}
double size = Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 1));
return switch (unit) {

View File

@@ -29,4 +29,20 @@ public class TestRegex {
System.out.println(matcher.group(1));
}
}
@Test
public void testYeShareKey() {
String url = "ABCD1234-asdasd";
String shareKey = url.replaceAll("(\\..*)|(#.*)", "");
System.out.println(shareKey);
url = "ABCD1234-adasd.html";
shareKey = url.replaceAll("(\\..*)|(#.*)", "");
System.out.println(shareKey);
url = "ABCD1234-adasd#123123";
shareKey = url.replaceAll("(\\..*)|(#.*)", "");
System.out.println(shareKey);
url = "ABCD1234-adasd.html#123123";
shareKey = url.replaceAll("(\\..*)|(#.*)", "");
System.out.println(shareKey);
}
}

View File

@@ -29,7 +29,7 @@
<org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.5</slf4j.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
<jackson.version>2.14.2</jackson.version>
<logback.version>1.5.8</logback.version>

View File

@@ -11,7 +11,7 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^11.2.0",
"axios": "^1.7.4",
"axios": "1.12.0",
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"element-plus": "^2.8.7",

View File

@@ -153,6 +153,13 @@
}
}
</style>
<script>
const saved = localStorage.getItem('isDarkMode') === 'true'
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (saved || (!saved && systemDark)) {
document.body.classList.add('dark-theme')
}
</script>
</head>
<body>
<div id="app">

View File

@@ -1,10 +1,5 @@
// 修改自 https://github.com/syhyz1990/panAI
const util = {
isMobile: (() => !!navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone|HarmonyOS|MicroMessenger)/i))(),
}
let opt = {
// 'baidu': {
@@ -293,6 +288,12 @@
host: /mail\.qq\.com/,
name: 'QQ邮箱中转站'
},
QQsc: {
// qfile.qq.com
reg: /https:\/\/qfile\.qq\.com\/q\/.+/,
host: /qfile\.qq\.com/,
name: 'QQ闪传'
},
pan118: {
reg: /https:\/\/(?:[a-zA-Z\d-]+\.)?118pan\.com\/b.+/,
host: /118pan\.com/,

View File

@@ -1,5 +1,5 @@
<template>
<div id="app" :class="{ 'dark-theme': isDarkMode }">
<div id="app" v-cloak :class="{ 'dark-theme': isDarkMode }">
<!-- <el-dialog
v-model="showRiskDialog"
title="使用本网站您应改同意"
@@ -19,13 +19,13 @@
</el-dialog> -->
<!-- 顶部反馈栏小号灰色无红边框 -->
<div class="feedback-bar">
<a href="https://github.com/qaiu/lz.qaiu.top/issues" target="_blank" rel="noopener" class="feedback-link mini">
<a href="https://github.com/qaiu/netdisk-fast-download/issues" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-bug feedback-icon"></i>
反馈
</a>
<a href="https://github.com/qaiu/lz.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
<a href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fab fa-github feedback-icon"></i>
GitHub
源码
</a>
<a href="https://blog.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-blog feedback-icon"></i>
@@ -48,9 +48,9 @@
</div>
<!-- 项目简介移到卡片内 -->
<div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.1.9_bate7</div>
<div class="intro-title">NFD网盘直链解析0.1.9_bate9</div>
<div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间亿方云文叔叔QQ邮箱文件中转站等</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"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
</div>
</div>
@@ -102,7 +102,7 @@
<div v-if="downloadUrl" class="file-meta-info-card">
<div class="file-meta-row">
<span class="file-meta-label">下载链接</span>
<a :href="downloadUrl" target="_blank" class="file-meta-link">{{ downloadUrl }}</a>
<a :href="downloadUrl" target="_blank" class="file-meta-link" rel="noreferrer noopener">点击下载</a>
</div>
<div class="file-meta-row" v-if="parseResult.data?.downloadShortUrl">
<span class="file-meta-label">下载短链</span>
@@ -170,6 +170,30 @@
</el-descriptions>
</div>
<!-- 错误时显示小按钮 -->
<div v-if="errorButtonVisible" style="text-align: center; margin-top: 10px;">
<el-button type="text" @click="errorDialogVisible = true"> 反馈错误详情>> </el-button>
</div>
<!-- 错误 JSON 弹窗 -->
<el-dialog
v-model="errorDialogVisible"
width="60%">
<template #title>
错误详情
<el-link
@click.prevent="copyErrorDetails"
target="_blank"
style="margin-left:8px"
type="primary">
复制当前错误信息提交Issue
</el-link>
</template>
<json-viewer :value="errorDetail" :expand-depth="5" copyable boxed sort />
<template #footer>
<el-button @click="errorDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 目录树组件 -->
<div v-if="showDirectoryTree" class="directory-tree-container">
<div style="margin-bottom: 10px; text-align: right;">
@@ -203,7 +227,9 @@
<!-- </template>-->
<!-- </el-input>-->
<!-- </div>-->
</div>
</template>
<script>
@@ -257,12 +283,19 @@ export default {
showRiskDialog: false,
baseUrl: location.origin,
showListLink: '',
errorDialogVisible: false,
errorDetail: null,
errorButtonVisible: false
}
},
methods: {
// 主题切换
handleThemeChange(isDark) {
this.isDarkMode = isDark
document.body.classList.toggle('dark-theme', isDark)
window.localStorage.setItem('isDarkMode', isDark)
},
// 验证输入
@@ -288,6 +321,7 @@ export default {
// 统一API调用
async callAPI(endpoint, params = {}) {
this.errorButtonVisible = false
try {
this.isLoading = true
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
@@ -296,6 +330,9 @@ export default {
// this.$message.success(response.data.msg || '操作成功')
return response.data
} else {
// 在页面右下角显示一个“查看详情”按钮 可以查看原json
this.errorDetail = response?.data
this.errorButtonVisible = true
throw new Error(response.data.msg || '操作失败')
}
} catch (error) {
@@ -508,6 +545,18 @@ export default {
navigator.clipboard.writeText(url).then(() => {
ElMessage.success('目录分享链接已复制!')
})
},
copyErrorDetails() {
const text = `分享链接:${this.link}
分享密码:${this.password || ''}
错误信息:${JSON.stringify(this.errorDetail, null, 2)}`;
navigator.clipboard.writeText(text).then(() => {
this.$message.success('已复制分享信息和错误详情');
window.open('https://github.com/qaiu/netdisk-fast-download/issues/new', '_blank');
}).catch(() => {
this.$message.error('复制失败');
});
}
},
@@ -554,6 +603,8 @@ export default {
</script>
<style>
[v-cloak] { display: none; }
body {
background-color: #f5f7fa;
color: #2c3e50;
@@ -826,4 +877,8 @@ hr {
padding-left: 8px;
}
}
.jv-container.jv-light .jv-item.jv-object {
color: #888;
}
</style>

View File

@@ -7,7 +7,7 @@
<div class="file-meta-info-card">
<div class="file-meta-row">
<span class="file-meta-label">下载链接</span>
<a :href="downloadUrl" target="_blank" class="file-meta-link">{{ downloadUrl }}</a>
<a :href="downloadUrl" target="_blank" class="file-meta-link">点击下载</a>
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件名</span>{{ fileTypeUtils.extractFileNameAndExt(downloadUrl).name }}

View File

@@ -3,29 +3,38 @@ package cn.qaiu.lz.common.cache;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.db.pool.JDBCType;
import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.model.PanFileInfo;
import cn.qaiu.lz.web.model.PanFileInfoRowMapper;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.templates.SqlTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class CacheManager {
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
private final Pool jdbcPool = JDBCPoolInit.instance().getPool();
private final JDBCType jdbcType = JDBCPoolInit.instance().getType();
private static final Logger LOGGER = LoggerFactory.getLogger(CacheManager.class);
public Future<CacheLinkInfo> get(String cacheKey) {
String sql = "SELECT share_key as shareKey, direct_link as directLink, expiration FROM cache_link_info WHERE share_key = #{share_key}";
String sql2 = "SELECT * FROM pan_file_info WHERE share_key = #{share_key}";
Map<String, Object> params = new HashMap<>();
params.put("share_key", cacheKey);
Promise<CacheLinkInfo> promise = Promise.promise();
Future<RowSet<PanFileInfo>> execute = SqlTemplate.forQuery(jdbcPool, sql2)
.mapTo(PanFileInfoRowMapper.INSTANCE)
.execute(params);
SqlTemplate.forQuery(jdbcPool, sql)
.mapTo(CacheLinkInfo.class)
.execute(params)
@@ -34,10 +43,18 @@ public class CacheManager {
if (rows.size() > 0) {
cacheHit = rows.iterator().next();
cacheHit.setCacheHit(true);
execute.onSuccess(r2 -> {
if (r2.size() > 0) {
cacheHit.setFileInfo(r2.iterator().next().toFileInfo());
}
promise.complete(cacheHit);
}).onFailure(e -> {
promise.complete(cacheHit);
});
} else {
cacheHit = new CacheLinkInfo(JsonObject.of("cacheHit", false, "shareKey", cacheKey));
promise.complete(cacheHit);
}
promise.complete(cacheHit);
}).onFailure(e->{
promise.fail(e);
LOGGER.error("cache get:", e);
@@ -47,7 +64,7 @@ public class CacheManager {
// 插入或更新缓存数据
public Future<Void> cacheShareLink(CacheLinkInfo cacheLinkInfo) {
public void cacheShareLink(CacheLinkInfo cacheLinkInfo) {
String sql;
if (jdbcType == JDBCType.MySQL) {
sql = """
@@ -63,12 +80,53 @@ public class CacheManager {
"VALUES (#{shareKey}, #{directLink}, #{expiration})";
}
// 直接传递 CacheLinkInfo 实体类
return SqlTemplate.forUpdate(jdbcPool, sql)
SqlTemplate.forUpdate(jdbcPool, sql)
.mapFrom(CacheLinkInfo.class) // 将实体类映射为 Tuple 参数
.execute(cacheLinkInfo)
.mapEmpty();
.execute(cacheLinkInfo).onSuccess(result -> {
if (result.rowCount() > 0) {
LOGGER.debug("Cache link info updated for shareKey: {}", cacheLinkInfo.getShareKey());
} else {
LOGGER.warn("No rows affected when updating cache link info for shareKey: {}", cacheLinkInfo.getShareKey());
}
}).onFailure(Throwable::printStackTrace);
if (cacheLinkInfo.getFileInfo() != null) {
String sql2 = """
INSERT IGNORE INTO pan_file_info (
share_key, file_name, file_id, file_icon, size, size_str, file_type,
file_path, create_time, update_time, create_by, description, download_count,
pan_type, parser_url, preview_url, hash
) VALUES (
#{shareKey}, #{fileName}, #{fileId}, #{fileIcon}, #{size}, #{sizeStr}, #{fileType},
#{filePath}, #{createTime}, #{updateTime}, #{createBy}, #{description}, #{downloadCount},
#{panType}, #{parserUrl}, #{previewUrl}, #{hash}
);
""";
// 判断文件信息是否缓存
SqlTemplate
.forQuery(jdbcPool, "SELECT count(1) AS count FROM pan_file_info WHERE share_key = #{share_key};")
.mapTo(Row::toJson)
.execute(Collections.singletonMap("share_key", cacheLinkInfo.getShareKey()))
.onSuccess(rows -> {
JsonObject row = rows.iterator().next();
int count = row.getInteger("count");
if (count == 0) {
// 没有缓存,执行插入
PanFileInfo fileInfo = PanFileInfo.fromFileInfo(cacheLinkInfo.getFileInfo());
fileInfo.setShareKey(cacheLinkInfo.getShareKey());
SqlTemplate.forUpdate(jdbcPool, sql2)
.mapFrom(PanFileInfo.class) // 将实体类映射为 Tuple 参数
.execute(fileInfo).onSuccess(result -> {
if (result.rowCount() > 0) {
LOGGER.debug("Pan file info inserted for shareKey: {}", cacheLinkInfo.getShareKey());
} else {
LOGGER.warn("No rows affected when inserting pan file info for shareKey: {}", cacheLinkInfo.getShareKey());
}
}).onFailure(Throwable::printStackTrace);
}
});
}
}
// 写入网盘厂商API解析次数

View File

@@ -0,0 +1,185 @@
package cn.qaiu.lz.common.util;
import cn.qaiu.lz.web.model.SysUser;
import io.vertx.core.json.JsonObject;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Date;
/**
* JWT工具类用于生成和验证JWT token
*/
public class JwtUtil {
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000; // token过期时间24小时
private static final String SECRET_KEY = "netdisk-fast-download-jwt-secret-key"; // 密钥
private static final String ALGORITHM = "HmacSHA256";
/**
* 生成JWT token
*
* @param user 用户信息
* @return JWT token
*/
public static String generateToken(SysUser user) {
long expireTime = getExpireTime();
// Header
JsonObject header = new JsonObject()
.put("alg", "HS256")
.put("typ", "JWT");
// Payload
JsonObject payload = new JsonObject()
.put("id", user.getId())
.put("username", user.getUsername())
.put("role", user.getRole())
.put("exp", expireTime)
.put("iat", System.currentTimeMillis())
.put("iss", "netdisk-fast-download");
// Base64 encode header and payload
String encodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(header.encode().getBytes(StandardCharsets.UTF_8));
String encodedPayload = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.encode().getBytes(StandardCharsets.UTF_8));
// Create signature
String signature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
// Combine to form JWT
return encodedHeader + "." + encodedPayload + "." + signature;
}
/**
* 使用HMAC-SHA256算法生成签名
*
* @param data 要签名的数据
* @param key 密钥
* @return 签名
*/
private static String hmacSha256(String data, String key) {
try {
Mac sha256Hmac = Mac.getInstance(ALGORITHM);
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
sha256Hmac.init(secretKey);
byte[] signedBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding().encodeToString(signedBytes);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error creating HMAC SHA256 signature", e);
}
}
/**
* 验证JWT token
*
* @param token JWT token
* @return 如果token有效返回true否则返回false
*/
public static boolean validateToken(String token) {
try {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}
String encodedHeader = parts[0];
String encodedPayload = parts[1];
String signature = parts[2];
// 验证签名
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
if (!expectedSignature.equals(signature)) {
return false;
}
// 验证过期时间
String payload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
JsonObject payloadJson = new JsonObject(payload);
long expTime = payloadJson.getLong("exp", 0L);
return System.currentTimeMillis() < expTime;
} catch (Exception e) {
return false;
}
}
/**
* 从token中获取用户ID
*
* @param token JWT token
* @return 用户ID
*/
public static String getUserIdFromToken(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return null;
}
// Base64解码
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonObject jsonObject = new JsonObject(payload);
return jsonObject.getString("id");
}
/**
* 从token中获取用户名
*
* @param token JWT token
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return null;
}
// Base64解码
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonObject jsonObject = new JsonObject(payload);
return jsonObject.getString("username");
}
/**
* 从token中获取用户角色
*
* @param token JWT token
* @return 用户角色
*/
public static String getRoleFromToken(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return null;
}
// Base64解码
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonObject jsonObject = new JsonObject(payload);
return jsonObject.getString("role");
}
/**
* 获取过期时间
*
* @return 过期时间戳
*/
private static long getExpireTime() {
return System.currentTimeMillis() + EXPIRE_TIME;
}
/**
* 将过期时间戳转换为LocalDateTime
*
* @param expireTime 过期时间戳
* @return LocalDateTime
*/
public static LocalDateTime getExpireTimeAsLocalDateTime(long expireTime) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(expireTime), ZoneId.systemDefault());
}
}

View File

@@ -0,0 +1,90 @@
package cn.qaiu.lz.common.util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 密码加密工具类
* 使用SHA-256算法加盐进行密码加密和验证
*/
public class PasswordUtil {
private static final String ALGORITHM = "SHA-256";
private static final int SALT_LENGTH = 16; // 盐的长度
private static final String DELIMITER = ":"; // 用于分隔盐和哈希值的分隔符
/**
* 对密码进行加密
*
* @param plainPassword 明文密码
* @return 加密后的密码格式salt:hash
*/
public static String hashPassword(String plainPassword) {
if (plainPassword == null || plainPassword.isEmpty()) {
throw new IllegalArgumentException("密码不能为空");
}
try {
// 生成随机盐
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
// 计算哈希值
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
md.update(salt);
byte[] hashedPassword = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
// 将盐和哈希值编码为Base64并拼接
String saltBase64 = Base64.getEncoder().encodeToString(salt);
String hashBase64 = Base64.getEncoder().encodeToString(hashedPassword);
// 返回格式salt:hash
return saltBase64 + DELIMITER + hashBase64;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("加密算法不可用", e);
}
}
/**
* 验证密码是否正确
*
* @param plainPassword 明文密码
* @param hashedPassword 加密后的密码格式salt:hash
* @return 如果密码匹配返回true否则返回false
*/
public static boolean checkPassword(String plainPassword, String hashedPassword) {
if (plainPassword == null || hashedPassword == null || hashedPassword.isEmpty()) {
return false;
}
try {
// 分割盐和哈希值
String[] parts = hashedPassword.split(DELIMITER);
if (parts.length != 2) {
return false;
}
String saltBase64 = parts[0];
String hashBase64 = parts[1];
// 解码盐
byte[] salt = Base64.getDecoder().decode(saltBase64);
// 使用相同的盐计算哈希值
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
md.update(salt);
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash);
// 比较计算出的哈希值和存储的哈希值
return hashBase64.equals(calculatedHashBase64);
} catch (Exception e) {
// 如果发生异常例如格式不正确返回false
return false;
}
}
}

View File

@@ -1,7 +1,13 @@
package cn.qaiu.lz.common.util;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.SharedDataUtil;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@@ -72,4 +78,42 @@ public class URLParamUtil {
return urlBuilder.toString();
}
/**
* 添加共享链接的其他参数到ParserCreate对象中
* @param parserCreate ParserCreate对象包含共享链接信息
*/
public static void addParam(ParserCreate parserCreate) {
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
.getLocalMap(ConfigConstant.LOCAL);
String type = parserCreate.getShareLinkInfo().getType();
if (localMap.containsKey(ConfigConstant.PROXY)) {
JsonObject proxy = (JsonObject) localMap.get(ConfigConstant.PROXY);
if (proxy.containsKey(type)) {
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.PROXY, proxy.getJsonObject(type));
}
}
if (localMap.containsKey(ConfigConstant.AUTHS)) {
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
if (auths.containsKey(type)) {
// 需要处理引号
MultiMap entries = MultiMap.caseInsensitiveMultiMap();
JsonObject jsonObject = auths.getJsonObject(type);
if (jsonObject != null) {
jsonObject.forEach(entity -> {
if (entity == null || entity.getValue() == null) {
return;
}
entries.set(entity.getKey(), entity.getValue().toString());
});
}
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, entries);
}
}
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
}
}

View File

@@ -5,8 +5,10 @@ import cn.qaiu.db.ddl.Table;
import cn.qaiu.db.ddl.TableGenIgnore;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.lz.common.ToJson;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.jackson.DatabindCodec;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -66,6 +68,11 @@ public class CacheLinkInfo implements ToJson {
if (json.containsKey("expiration")) {
this.setExpiration(json.getLong("expiration"));
}
if (json.containsKey("fileInfo")) {
ObjectMapper mapper = DatabindCodec.mapper(); // Vert.x 自带的 mapper
this.setFileInfo(mapper.convertValue(json.getJsonObject("fileInfo"), FileInfo.class));
}
this.setCacheHit(json.getBoolean("cacheHit", false));
}

View File

@@ -0,0 +1,163 @@
package cn.qaiu.lz.web.model;
import cn.qaiu.db.ddl.Table;
import cn.qaiu.entity.FileInfo;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.format.SnakeCase;
import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.templates.annotations.RowMapped;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author <a href="https://qaiu.top">QAIU</a>
* @date 2025/8/4 12:38
*/
@Table(keyFields = "share_key")
@DataObject
@RowMapped(formatter = SnakeCase.class)
@NoArgsConstructor
@Data
public class PanFileInfo {
String shareKey;
/**
* 文件名
*/
private String fileName;
/**
* 文件ID
*/
private String fileId;
private String fileIcon;
/**
* 文件大小(byte)
*/
private Long size;
private String sizeStr;
/**
* 类型
*/
private String fileType;
/**
* 文件路径
*/
private String filePath;
/**
* 创建(上传)时间 yyyy-MM-dd HH:mm:ss格式
*/
private String createTime;
/**
* 上次修改时间
*/
private String updateTime;
/**
* 创建者
*/
private String createBy;
/**
* 文件描述
*/
private String description;
/**
* 下载次数
*/
private Integer downloadCount;
/**
* 网盘标识
*/
private String panType;
/**
* nfd下载链接(可能获取不到)
* note: 不是下载直链
*/
private String parserUrl;
//预览地址
private String previewUrl;
// 文件hash默认类型为md5
private String hash;
public PanFileInfo(JsonObject jsonObject) {
this.shareKey = jsonObject.getString("shareKey");
this.fileName = jsonObject.getString("fileName");
this.fileId = jsonObject.getString("fileId");
this.fileIcon = jsonObject.getString("fileIcon");
this.size = jsonObject.getLong("size");
this.sizeStr = jsonObject.getString("sizeStr");
this.fileType = jsonObject.getString("fileType");
this.filePath = jsonObject.getString("filePath");
this.createTime = jsonObject.getString("createTime");
this.updateTime = jsonObject.getString("updateTime");
this.createBy = jsonObject.getString("createBy");
this.description = jsonObject.getString("description");
this.downloadCount = jsonObject.getInteger("downloadCount");
this.panType = jsonObject.getString("panType");
this.parserUrl = jsonObject.getString("parserUrl");
this.previewUrl = jsonObject.getString("previewUrl");
this.hash = jsonObject.getString("hash");
}
public static PanFileInfo fromFileInfo(FileInfo info) {
PanFileInfo panFileInfo = new PanFileInfo();
if (info == null) {
return panFileInfo;
}
// 拷贝 FileInfo 的字段
panFileInfo.setFileName(info.getFileName());
panFileInfo.setFileId(info.getFileId());
panFileInfo.setFileIcon(info.getFileIcon());
panFileInfo.setSize(info.getSize());
panFileInfo.setSizeStr(info.getSizeStr());
panFileInfo.setFileType(info.getFileType());
panFileInfo.setFilePath(info.getFilePath());
panFileInfo.setCreateTime(info.getCreateTime());
panFileInfo.setUpdateTime(info.getUpdateTime());
panFileInfo.setCreateBy(info.getCreateBy());
panFileInfo.setDescription(info.getDescription());
panFileInfo.setDownloadCount(info.getDownloadCount());
panFileInfo.setPanType(info.getPanType());
panFileInfo.setParserUrl(info.getParserUrl());
panFileInfo.setPreviewUrl(info.getPreviewUrl());
panFileInfo.setHash(info.getHash());
return panFileInfo;
}
public FileInfo toFileInfo() {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(this.getFileName());
fileInfo.setFileId(this.getFileId());
fileInfo.setFileIcon(this.getFileIcon());
fileInfo.setSize(this.getSize());
fileInfo.setSizeStr(this.getSizeStr());
fileInfo.setFileType(this.getFileType());
fileInfo.setFilePath(this.getFilePath());
fileInfo.setCreateTime(this.getCreateTime());
fileInfo.setUpdateTime(this.getUpdateTime());
fileInfo.setCreateBy(this.getCreateBy());
fileInfo.setDescription(this.getDescription());
fileInfo.setDownloadCount(this.getDownloadCount());
fileInfo.setPanType(this.getPanType());
fileInfo.setParserUrl(this.getParserUrl());
fileInfo.setPreviewUrl(this.getPreviewUrl());
fileInfo.setHash(this.getHash());
return fileInfo;
}
}

View File

@@ -3,6 +3,7 @@ package cn.qaiu.lz.web.model;
import cn.qaiu.db.ddl.Table;
import cn.qaiu.lz.common.ToJson;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;
import lombok.Data;
@@ -14,12 +15,27 @@ import java.time.format.DateTimeFormatter;
@Data
@DataObject
@NoArgsConstructor
@Table("t_user")
@Table("sys_user")
public class SysUser implements ToJson {
private String id;
private String username;
private String password;
private String email;
private String phone;
private String avatar;
// 用户状态0-禁用1-正常
private Integer status;
// 用户角色admin-管理员user-普通用户
private String role;
// 最后登录时间
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime lastLoginTime;
private Integer age;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createTime;
@@ -28,9 +44,17 @@ public class SysUser implements ToJson {
this.id = json.getString("id");
this.username = json.getString("username");
this.password = json.getString("password");
this.email = json.getString("email");
this.phone = json.getString("phone");
this.avatar = json.getString("avatar");
this.status = json.getInteger("status");
this.role = json.getString("role");
this.age = json.getInteger("age");
if (json.getString("createTime") != null) {
this.createTime = LocalDateTime.parse(json.getString("createTime"));
}
if (json.getString("lastLoginTime") != null) {
this.lastLoginTime = LocalDateTime.parse(json.getString("lastLoginTime"));
}
}
}

View File

@@ -4,14 +4,48 @@ import cn.qaiu.lz.web.model.SysUser;
import cn.qaiu.vx.core.base.BaseAsyncService;
import io.vertx.codegen.annotations.ProxyGen;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
/**
* lz-web
* 用户服务接口
* <br>Create date 2021/8/27 14:06
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@ProxyGen
public interface UserService extends BaseAsyncService {
Future<SysUser> login(SysUser user);
/**
* 用户登录
* @param user 包含用户名和密码的用户对象
* @return 登录成功返回用户信息和token失败返回错误信息
*/
Future<JsonObject> login(SysUser user);
/**
* 根据用户名获取用户信息
* @param username 用户名
* @return 用户信息
*/
Future<SysUser> getUserByUsername(String username);
/**
* 创建新用户
* @param user 用户信息
* @return 创建成功返回用户信息,失败返回错误信息
*/
Future<SysUser> createUser(SysUser user);
/**
* 更新用户信息
* @param user 用户信息
* @return 更新成功返回用户信息,失败返回错误信息
*/
Future<SysUser> updateUser(SysUser user);
/**
* 验证token
* @param token JWT token
* @return 验证成功返回用户信息,失败返回错误信息
*/
Future<JsonObject> validateToken(String token);
}

View File

@@ -1,38 +1,33 @@
package cn.qaiu.lz.web.service.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.common.cache.CacheConfigLoader;
import cn.qaiu.lz.common.cache.CacheManager;
import cn.qaiu.lz.common.cache.CacheTotalField;
import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.service.CacheService;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.vx.core.annotaions.Service;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Date;
@Service
@Slf4j
public class CacheServiceImpl implements CacheService {
private final CacheManager cacheManager = new CacheManager();
private Future<CacheLinkInfo> getAndSaveCachedShareLink(ParserCreate parserCreate) {
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
.getLocalMap(ConfigConstant.LOCAL);
if (localMap.containsKey(ConfigConstant.PROXY)) {
JsonObject proxy = (JsonObject) localMap.get(ConfigConstant.PROXY);
String type = parserCreate.getShareLinkInfo().getType();
if (proxy.containsKey(type)) {
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.PROXY, proxy.getJsonObject(type));
}
}
URLParamUtil.addParam(parserCreate);
Promise<CacheLinkInfo> promise = Promise.promise();
// 构建组合的缓存key
@@ -46,20 +41,36 @@ public class CacheServiceImpl implements CacheService {
// parse
result.setCacheHit(false);
result.setExpiration(0L);
parserCreate.createTool().parse().onSuccess(redirectUrl -> {
IPanTool tool;
try {
tool = parserCreate.createTool();
} catch (Exception e) {
promise.fail(e.getCause().getCause());
return;
}
tool.parse().onSuccess(redirectUrl -> {
long expires = System.currentTimeMillis() +
CacheConfigLoader.getDuration(shareLinkInfo.getType()) * 60 * 1000L;
result.setDirectLink(redirectUrl);
// result.setExpires(generateDate(expires));
promise.complete(result);
// 更新缓存
// 将直链存储到缓存
CacheLinkInfo cacheLinkInfo = new CacheLinkInfo(JsonObject.of(
"directLink", redirectUrl,
"expiration", expires,
"shareKey", cacheKey
));
cacheManager.cacheShareLink(cacheLinkInfo).onFailure(Throwable::printStackTrace);
if (shareLinkInfo.getOtherParam().containsKey("fileInfo")) {
try {
FileInfo fileInfo = (FileInfo) shareLinkInfo.getOtherParam().get("fileInfo");
result.setFileInfo(fileInfo);
cacheLinkInfo.setFileInfo(fileInfo);
} catch (Exception ignored) {
log.error("文件对象转换异常");
}
}
promise.complete(result);
// 更新缓存
// 将直链存储到缓存
cacheManager.cacheShareLink(cacheLinkInfo);
cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(Throwable::printStackTrace);
}).onFailure(promise::fail);
} else {

View File

@@ -1,29 +1,414 @@
package cn.qaiu.lz.web.service.impl;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.lz.common.util.JwtUtil;
import cn.qaiu.lz.common.util.PasswordUtil;
import cn.qaiu.lz.web.model.SysUser;
import cn.qaiu.lz.web.service.UserService;
import cn.qaiu.vx.core.annotaions.Service;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.Tuple;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.UUID;
/**
* lz-web
* 用户服务实现类
* <br>Create date 2021/8/27 14:09
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Override
public Future<SysUser> login(SysUser user) {
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
return Future.succeededFuture(user);
// 初始化方法,确保管理员用户存在
public void init() {
// 检查管理员用户是否存在
getUserByUsername("admin")
.onSuccess(user -> {
log.info("管理员用户已存在");
})
.onFailure(err -> {
// 创建管理员用户
SysUser admin = new SysUser();
admin.setId(UUID.randomUUID().toString());
admin.setUsername("admin");
admin.setPassword(PasswordUtil.hashPassword("admin123"));
admin.setEmail("admin@example.com");
admin.setRole("admin");
admin.setStatus(1);
admin.setCreateTime(LocalDateTime.now());
createUser(admin)
.onSuccess(result -> log.info("管理员用户创建成功"))
.onFailure(error -> log.error("管理员用户创建失败", error));
});
}
}
// 新增一个工具方法来过滤敏感信息
private SysUser filterSensitiveInfo(SysUser user) {
if (user != null) {
SysUser filtered = new SysUser();
// 复制除密码外的所有字段
filtered.setId(user.getId());
filtered.setUsername(user.getUsername());
filtered.setEmail(user.getEmail());
filtered.setPhone(user.getPhone());
filtered.setAvatar(user.getAvatar());
filtered.setRole(user.getRole());
filtered.setStatus(user.getStatus());
filtered.setCreateTime(user.getCreateTime());
filtered.setLastLoginTime(user.getLastLoginTime());
return filtered;
}
return null;
}
// 将Row转换为SysUser对象
private SysUser rowToUser(Row row) {
if (row == null) {
return null;
}
SysUser user = new SysUser();
user.setId(row.getString("id"));
user.setUsername(row.getString("username"));
user.setPassword(row.getString("password"));
user.setEmail(row.getString("email"));
user.setPhone(row.getString("phone"));
user.setAvatar(row.getString("avatar"));
user.setRole(row.getString("role"));
user.setStatus(row.getInteger("status"));
// 处理日期时间字段
LocalDateTime createTime = row.getLocalDateTime("create_time");
if (createTime != null) {
user.setCreateTime(createTime);
}
LocalDateTime lastLoginTime = row.getLocalDateTime("last_login_time");
if (lastLoginTime != null) {
user.setLastLoginTime(lastLoginTime);
}
return user;
}
@Override
public Future<JsonObject> login(SysUser user) {
// 参数校验
if (user == null || user.getUsername() == null || user.getPassword() == null) {
return Future.succeededFuture(new JsonObject()
.put("success", false)
.put("message", "用户名和密码不能为空"));
}
Promise<JsonObject> promise = Promise.promise();
// 查询用户
String sql = "SELECT * FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(sql)
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
// 验证密码
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "密码错误"));
return;
}
// 更新最后登录时间
LocalDateTime now = LocalDateTime.now();
existUser.setLastLoginTime(now);
// 更新数据库中的最后登录时间
String updateSql = "UPDATE sys_user SET last_login_time = ? WHERE username = ?";
jdbcPool.preparedQuery(updateSql)
.execute(Tuple.of(
Timestamp.from(now.atZone(ZoneId.systemDefault()).toInstant()),
existUser.getUsername()
))
.onFailure(err -> log.error("更新最后登录时间失败", err));
// 生成token
String token = JwtUtil.generateToken(existUser);
// 返回用户信息和token
JsonObject value = JsonObject.mapFrom(existUser);
value.remove("password");
promise.complete(new JsonObject()
.put("success", true)
.put("message", "登录成功")
.put("token", token)
.put("user", value));
})
.onFailure(err -> {
log.error("登录查询失败", err);
promise.complete(new JsonObject()
.put("success", false)
.put("message", "登录失败: " + err.getMessage()));
});
return promise.future();
}
@Override
public Future<SysUser> getUserByUsername(String username) {
if (username == null || username.isEmpty()) {
return Future.failedFuture("用户名不能为空");
}
Promise<SysUser> promise = Promise.promise();
String sql = "SELECT * FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(sql)
.execute(Tuple.of(username))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
return;
}
Row row = rows.iterator().next();
SysUser user = rowToUser(row);
promise.complete(filterSensitiveInfo(user));
})
.onFailure(err -> {
log.error("查询用户失败", err);
promise.fail("查询用户失败: " + err.getMessage());
});
return promise.future();
}
@Override
public Future<SysUser> createUser(SysUser user) {
// 参数校验
if (user == null || user.getUsername() == null || user.getPassword() == null) {
return Future.failedFuture("用户名和密码不能为空");
}
Promise<SysUser> promise = Promise.promise();
// 先检查用户是否已存在
String checkSql = "SELECT COUNT(*) as count FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(checkSql)
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
Row row = rows.iterator().next();
long count = row.getLong("count");
if (count > 0) {
promise.fail("用户名已存在");
return;
}
// 设置用户ID和创建时间
if (user.getId() == null) {
user.setId(UUID.randomUUID().toString());
}
if (user.getCreateTime() == null) {
user.setCreateTime(LocalDateTime.now());
}
// 设置默认角色和状态
if (user.getRole() == null) {
user.setRole("user");
}
if (user.getStatus() == null) {
user.setStatus(1);
}
// 对密码进行加密
String plainPassword = user.getPassword();
user.setPassword(PasswordUtil.hashPassword(plainPassword));
// 插入用户
String insertSql = "INSERT INTO sys_user (id, username, password, email, phone, avatar, role, status, create_time) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
jdbcPool.preparedQuery(insertSql)
.execute(Tuple.of(
user.getId(),
user.getUsername(),
user.getPassword(),
user.getEmail(),
user.getPhone(),
user.getAvatar(),
user.getRole(),
user.getStatus(),
Timestamp.from(user.getCreateTime().atZone(ZoneId.systemDefault()).toInstant())
))
.onSuccess(result -> {
promise.complete(filterSensitiveInfo(user));
})
.onFailure(err -> {
log.error("创建用户失败", err);
promise.fail("创建用户失败: " + err.getMessage());
});
})
.onFailure(err -> {
log.error("检查用户是否存在失败", err);
promise.fail("创建用户失败: " + err.getMessage());
});
return promise.future();
}
@Override
public Future<SysUser> updateUser(SysUser user) {
// 参数校验
if (user == null || user.getUsername() == null) {
return Future.failedFuture("用户名不能为空");
}
Promise<SysUser> promise = Promise.promise();
// 先检查用户是否存在
String checkSql = "SELECT * FROM sys_user WHERE username = ?";
jdbcPool.preparedQuery(checkSql)
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
// 构建更新SQL
StringBuilder updateSql = new StringBuilder("UPDATE sys_user SET ");
Tuple params = Tuple.tuple();
if (user.getEmail() != null) {
updateSql.append("email = ?, ");
params.addValue(user.getEmail());
}
if (user.getPhone() != null) {
updateSql.append("phone = ?, ");
params.addValue(user.getPhone());
}
if (user.getAvatar() != null) {
updateSql.append("avatar = ?, ");
params.addValue(user.getAvatar());
}
if (user.getStatus() != null) {
updateSql.append("status = ?, ");
params.addValue(user.getStatus());
}
if (user.getRole() != null) {
updateSql.append("role = ?, ");
params.addValue(user.getRole());
}
if (user.getPassword() != null) {
updateSql.append("password = ?, ");
params.addValue(PasswordUtil.hashPassword(user.getPassword()));
}
// 移除最后的逗号和空格
String sql = updateSql.toString();
if (sql.endsWith(", ")) {
sql = sql.substring(0, sql.length() - 2);
}
// 如果没有要更新的字段,直接返回
if (params.size() == 0) {
promise.complete(filterSensitiveInfo(existUser));
return;
}
// 添加WHERE条件
sql += " WHERE username = ?";
params.addValue(user.getUsername());
// 执行更新
jdbcPool.preparedQuery(sql)
.execute(params)
.onSuccess(result -> {
// 重新查询用户信息
getUserByUsername(user.getUsername())
.onSuccess(promise::complete)
.onFailure(promise::fail);
})
.onFailure(err -> {
log.error("更新用户失败", err);
promise.fail("更新用户失败: " + err.getMessage());
});
})
.onFailure(err -> {
log.error("查询用户失败", err);
promise.fail("更新用户失败: " + err.getMessage());
});
return promise.future();
}
@Override
public Future<JsonObject> validateToken(String token) {
if (token == null || token.isEmpty()) {
return Future.succeededFuture(new JsonObject()
.put("success", false)
.put("message", "Token不能为空"));
}
// 验证token
boolean isValid = JwtUtil.validateToken(token);
if (!isValid) {
return Future.succeededFuture(new JsonObject()
.put("success", false)
.put("message", "Token无效或已过期"));
}
// 获取用户信息
String username = JwtUtil.getUsernameFromToken(token);
Promise<JsonObject> promise = Promise.promise();
getUserByUsername(username)
.onSuccess(user -> {
promise.complete(new JsonObject()
.put("success", true)
.put("message", "Token有效")
.put("user", JsonObject.mapFrom(user)));
})
.onFailure(err -> {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
});
return promise.future();
}
}

View File

@@ -36,15 +36,15 @@ custom:
- ^cn\.qaiu\.lz\.web\.model\..*
# 限流配置
rateLimit:
# 是否启用限流
enable: true
# 限流的请求数
limit: 10
# 限流的时间窗口(单位秒)
timeWindow: 10
# 路径匹配规则
pathReg: ^/v2/.*
#rateLimit:
# # 是否启用限流
# enable: true
# # 限流的请求数
# limit: 10
# # 限流的时间窗口(单位秒)
# timeWindow: 10
# # 路径匹配规则
# pathReg: ^/v2/.*
# 数据源配置

View File

@@ -1,13 +1,13 @@
###
POST http://127.0.0.1:6400/v2/shout/submit
POST https://lzzz.qaiu.top/v2/shout/submit
Content-Type: application/json
{
"content": "CREATE UNIQUE INDEX `idx_uk_code` ON `t_messages` (`code`);"
"content": "ok 123123123123123123123123123123123123123123123123123123123123231123123"
}
###
GET http://127.0.0.1:6400/v2/shout/retrieve?code=878696
GET http://lzzz.qaiu.top/v2/shout/retrieve?code=414016
###
响应:
@@ -16,3 +16,6 @@ GET http://127.0.0.1:6400/v2/shout/retrieve?code=878696
"code": 200,
"msg": "success"
}
###
https://gfs302n511.userstorage.mega.co.nz/dl/XwiiRG-Z97rz7wcbWdDmcd654FGkYU3FJncTobxhpPR9GVSggHJQsyMGdkLsWEiIIf71RUXcQPtV7ljVc0Z3tA_ThaUb9msdh7tS0z-2CbaRYSM5176DFxDKQtG84g