mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-17 04:43:02 +00:00
Compare commits
128 Commits
0.1.8.fixe
...
v0.1.9b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca91302d28 | ||
|
|
e07272a5dc | ||
|
|
461305e1df | ||
|
|
8e8ab10a0f | ||
|
|
e754326925 | ||
|
|
4c92994c6f | ||
|
|
66c57f47ac | ||
|
|
ec689eadd8 | ||
|
|
c1e15709a7 | ||
|
|
2848937ce7 | ||
|
|
42ff0c21b2 | ||
|
|
3ed7e547e6 | ||
|
|
fad8e688df | ||
|
|
b2f2dcac4c | ||
|
|
fcba78e977 | ||
|
|
77c9d777a1 | ||
|
|
4460659210 | ||
|
|
8631524107 | ||
|
|
0579588814 | ||
|
|
df2bfb6ac7 | ||
|
|
517b6f8910 | ||
|
|
94a46d2833 | ||
|
|
1631a0faa1 | ||
|
|
06d5943cb6 | ||
|
|
3095e13676 | ||
|
|
482cbce7e8 | ||
|
|
ef2fc3ab98 | ||
|
|
5b57b05eae | ||
|
|
093579c6f5 | ||
|
|
c2d4990d7f | ||
|
|
40e8380738 | ||
|
|
b716e1e861 | ||
|
|
8432d4952c | ||
|
|
dd8f085f63 | ||
|
|
161ff8d8a3 | ||
|
|
1390cd0104 | ||
|
|
7a02b1e97f | ||
|
|
036f107c90 | ||
|
|
5652383450 | ||
|
|
9a047a5da0 | ||
|
|
8975743a37 | ||
|
|
0e30eafe49 | ||
|
|
7facb62f21 | ||
|
|
30d43cb961 | ||
|
|
c505b17e35 | ||
|
|
080c4c753d | ||
|
|
ade0d34d91 | ||
|
|
56d082eb0b | ||
|
|
795c4529ba | ||
|
|
0f5cfe22ea | ||
|
|
925ad2c3a5 | ||
|
|
f3e96907fe | ||
|
|
75a1e58a7d | ||
|
|
379e889f71 | ||
|
|
40c06f397b | ||
|
|
9e9302436e | ||
|
|
6d816d4193 | ||
|
|
438eda9c08 | ||
|
|
ace39e4633 | ||
|
|
7712391f29 | ||
|
|
65f08dcb02 | ||
|
|
1d332aa6f4 | ||
|
|
ba81641517 | ||
|
|
fb30bdb879 | ||
|
|
fc451d3b41 | ||
|
|
ffee1f3462 | ||
|
|
f30027dd13 | ||
|
|
8b6aad17f4 | ||
|
|
b77930adfb | ||
|
|
aff8f88076 | ||
|
|
4e6582e24c | ||
|
|
fa9acaccfd | ||
|
|
0414f85f12 | ||
|
|
527dd0eeb4 | ||
|
|
74ed7475c9 | ||
|
|
54dc3dba96 | ||
|
|
9980159090 | ||
|
|
0b193ebb00 | ||
|
|
f5fc9843b2 | ||
|
|
df1f67dd26 | ||
|
|
b069a5f576 | ||
|
|
7686763a03 | ||
|
|
635a6eac37 | ||
|
|
877edc535f | ||
|
|
01d59e3c1e | ||
|
|
fece2799e3 | ||
|
|
de9756ee86 | ||
|
|
51f047a51b | ||
|
|
04b66e82b7 | ||
|
|
df89253647 | ||
|
|
45dbca794e | ||
|
|
857bf28f99 | ||
|
|
e07ce15228 | ||
|
|
0637bcfd8e | ||
|
|
23db0563ac | ||
|
|
ccba71aa4e | ||
|
|
fee4bf2ad6 | ||
|
|
5052fea9ef | ||
|
|
e85215fca1 | ||
|
|
e42fe45329 | ||
|
|
4240815bd1 | ||
|
|
6f0c5305e2 | ||
|
|
757005cad8 | ||
|
|
81651ad97c | ||
|
|
f3763b6058 | ||
|
|
82478dc485 | ||
|
|
703fd05d43 | ||
|
|
ff868b6e2a | ||
|
|
051a74b37b | ||
|
|
a0a1085623 | ||
|
|
2612d3919c | ||
|
|
6f123a236f | ||
|
|
71e57e6a08 | ||
|
|
7cb18d8186 | ||
|
|
cdbf670ece | ||
|
|
e0dafee617 | ||
|
|
c37bce1563 | ||
|
|
0b3c77d644 | ||
|
|
2cf85caf86 | ||
|
|
594010ba88 | ||
|
|
d91460d2e2 | ||
|
|
89713e6ac9 | ||
|
|
17c9b2538c | ||
|
|
d337b003cb | ||
|
|
8f1485656b | ||
|
|
f0c4ec3031 | ||
|
|
458be84aca | ||
|
|
c7716aad34 |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: https://blog.qaiu.top/archives/da-shang-zhuan-yong # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
29
.github/workflows/maven.yml
vendored
29
.github/workflows/maven.yml
vendored
@@ -15,9 +15,21 @@ permissions:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
tags:
|
||||||
|
- '*' # 只有推送tag时才会触发构建
|
||||||
|
branches-ignore:
|
||||||
|
- '*' # 排除所有分支的提交
|
||||||
|
paths-ignore:
|
||||||
|
- 'bin/**'
|
||||||
|
- '.github/**'
|
||||||
|
- '.mvn/**'
|
||||||
|
- '.run/**'
|
||||||
|
- '.vscode/**'
|
||||||
|
- '*.txt'
|
||||||
|
- '*.md'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches:
|
||||||
|
- "main"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -30,8 +42,6 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
|
||||||
cache-dependency-path: 'web-front/yarn.lock'
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
@@ -70,6 +80,13 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Extract git tag
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
GIT_TAG=$(git tag --points-at HEAD | head -n 1)
|
||||||
|
echo "tag=$GIT_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
@@ -77,5 +94,5 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:${{ github.sha }}
|
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
|
||||||
ghcr.io/${{ github.repository }}:main
|
ghcr.io/qaiu/netdisk-fast-download:latest
|
||||||
|
|||||||
257
README.md
257
README.md
@@ -1,72 +1,106 @@
|
|||||||
云盘解析服务 (nfd云解析)
|
<p align="center">
|
||||||
预览地址 https://lz.qaiu.top
|
<img src="https://github.com/user-attachments/assets/87401aae-b0b6-4ffb-bbeb-44756404d26f" alt="项目预览图" />
|
||||||
|
</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://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>
|
||||||
|
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# netdisk-fast-download 网盘分享链接云解析服务
|
||||||
|
QQ群:1017480890
|
||||||
|
|
||||||
|
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||||
|
|
||||||
|
[预览地址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)
|
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
|
||||||
**注意: 请不要过度依赖lz.qaiu.top预览地址服务,建议本地搭建或者云服务器自行搭建。
|
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
|
||||||
解析次数过多IP会被部分网盘厂商限制,不推荐做公共解析。**
|
**小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
||||||
|
**注意: 请不要过度依赖lz.qaiu.top预览地址服务,建议本地搭建或者云服务器自行搭建。解析次数过多IP会被部分网盘厂商限制,不推荐做公共解析。**
|
||||||
[](https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml)
|
|
||||||
[](https://www.oracle.com/cn/java/technologies/downloads/)
|
|
||||||
[](https://vertx-china.github.io/)
|
|
||||||
[](https://github.com/qaiu/netdisk-fast-download/releases/latest)
|
|
||||||
|
|
||||||
## 项目介绍
|
|
||||||
网盘直链解析工具能把网盘分享下载链接转化为直链,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享。
|
|
||||||
**0.1.8及以上版本json接口格式有调整,尤其依赖lz.qaiu.top做下载服务的朋友们记得修改, 参考json返回数据格式示例**
|
|
||||||
|
|
||||||
|
|
||||||
*重要声明:本项目仅供学习参考;请不要将此项目用于任何商业用途,否则可能带来严重的后果。转发/分享该项目请注明来源*
|
|
||||||
|
|
||||||
## 网盘支持情况:
|
## 网盘支持情况:
|
||||||
> 20230905 奶牛云直链做了防盗链,需加入请求头:Referer: https://cowtransfer.com/
|
> 20230905 奶牛云直链做了防盗链,需加入请求头:Referer: https://cowtransfer.com/
|
||||||
> 20230824 123云盘解析大文件(>100MB)失效,需要登录
|
> 20230824 123云盘解析大文件(>100MB)失效,需要登录
|
||||||
> 20230722 UC网盘解析失效,需要登录
|
> 20230722 UC网盘解析失效,需要登录
|
||||||
|
|
||||||
`网盘名称(网盘标识):`
|
网盘名称-网盘标识:
|
||||||
|
|
||||||
- [蓝奏云 (lz)](https://pc.woozooo.com/)
|
|
||||||
- [蓝奏云优享 (iz)](https://www.ilanzou.com/)
|
|
||||||
- [奶牛快传 (cow)](https://cowtransfer.com/)
|
|
||||||
- [移动云云空间 (ec)](https://www.ecpan.cn/web)
|
|
||||||
- [小飞机网盘 (fj)](https://www.feijipan.com/)
|
|
||||||
- [亿方云 (fc)](https://www.fangcloud.com/)
|
|
||||||
- [123云盘 (ye)](https://www.123pan.com/)
|
|
||||||
- [文叔叔 (ws)](https://www.wenshushu.cn/)
|
|
||||||
- [联想乐云 (le)](https://lecloud.lenovo.com/)
|
|
||||||
- [QQ邮箱文件中转站 (qq)](https://mail.qq.com/)
|
|
||||||
- [超星网盘-开发中 (cx)](https://passport2.chaoxing.com/login?newversion=true&refer=https%3A%2F%2Fpan-yz.chaoxing.com%2F)
|
|
||||||
- [城通网盘(ct)](https://www.ctfile.com)
|
|
||||||
- [网易云音乐(mne)](https://music.163.com)
|
|
||||||
- [Cloudreve自建网盘(ce)](https://github.com/cloudreve/Cloudreve)
|
|
||||||
|
|
||||||
|
- [蓝奏云-lz](https://pc.woozooo.com/)
|
||||||
|
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
||||||
|
- [奶牛快传-cow](https://cowtransfer.com/)
|
||||||
|
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
||||||
|
- [小飞机网盘-fj](https://www.feijipan.com/)
|
||||||
|
- [亿方云-fc](https://www.fangcloud.com/)
|
||||||
|
- [123云盘-ye](https://www.123pan.com/)
|
||||||
|
- ~[115网盘(失效)-p115](https://115.com/)~
|
||||||
|
- [118网盘(已停服)-p118](https://www.118pan.com/)
|
||||||
|
- [文叔叔-ws](https://www.wenshushu.cn/)
|
||||||
|
- [联想乐云-le](https://lecloud.lenovo.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)
|
||||||
|
- [酷我音乐分享链接-mkws](https://kuwo.cn)
|
||||||
|
- [QQ音乐分享链接-mqqs](https://y.qq.com)
|
||||||
|
- 咪咕音乐分享链接(开发中)
|
||||||
|
- [Cloudreve自建网盘-ce](https://github.com/cloudreve/Cloudreve)
|
||||||
|
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
|
||||||
|
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
|
||||||
|
- Google云盘-pgd
|
||||||
|
- Onedrive-pod
|
||||||
|
- Dropbox-pdp
|
||||||
|
- iCloud-pic
|
||||||
|
### 仅专属版提供
|
||||||
|
- [移动云盘-p139](https://yun.139.com/)
|
||||||
|
- [联通云盘-pwo](https://pan.wo.cn/)
|
||||||
|
- [天翼云盘-p189](https://cloud.189.cn/)
|
||||||
|
|
||||||
### API接口说明
|
### API接口说明
|
||||||
your_host指的是您的域名或者IP,实际使用时替换为实际域名或者IP,端口默认6400,可以使用nginx代理来做域名访问。
|
your_host指的是您的域名或者IP,实际使用时替换为实际域名或者IP,端口默认6400,可以使用nginx代理来做域名访问。
|
||||||
解析方式分为两种类型直接跳转下载文件和获取下载链接,
|
解析方式分为两种类型直接跳转下载文件和获取下载链接,
|
||||||
每一种都提供了两种接口形式: `通用接口parser?url=`和`网盘标志/分享key拼接的短地址(标志短链)`,所有规则参考示例。
|
每一种都提供了两种接口形式: `通用接口parser?url=`和`网盘标志/分享key拼接的短地址(标志短链)`,所有规则参考示例。
|
||||||
- 通用接口: `/parser?url=分享链接`,加密分享需要加上参数pwd=密码;
|
- 通用接口: `/parser?url=分享链接&pwd=密码` 没有分享密码去掉&pwd参数;
|
||||||
- 标志短链: `/网盘标识/分享key` 在分享Key后面加上@密码;
|
- 标志短链: `/d/网盘标识/分享key@密码` 没有分享密码去掉@密码;
|
||||||
- 直链JSON: `通用接口`和`标志短链`前加上`/json` 加密分享的密码规则同上;
|
- 直链JSON: `/json/网盘标识/分享key@密码`和`/json/parser?url=分享链接&pwd=密码`
|
||||||
- 网盘标识参考上面网盘支持情况
|
- 网盘标识参考上面网盘支持情况
|
||||||
- 当带有分享密码时需要加上密码参数(pwd)
|
- 当带有分享密码时需要加上密码参数(pwd)
|
||||||
- 移动云云空间,小飞机网盘的加密分享的密码可以忽略
|
- 移动云云空间,小飞机网盘的加密分享的密码可以忽略
|
||||||
- 移动云空间分享key取分享链接中的data参数,比如`&data=xxx`的参数就是xxx
|
- 移动云空间分享key取分享链接中的data参数,比如`&data=xxx`的参数就是xxx
|
||||||
|
|
||||||
API规则:
|
API规则:
|
||||||
```
|
> 建议使用UrlEncode编码分享链接
|
||||||
|
|
||||||
1. 解析并自动302跳转 :
|
1. 解析并自动302跳转
|
||||||
http://your_host/parser?url=分享链接&pwd=xxx
|
http://your_host/parser?url=分享链接&pwd=xxx
|
||||||
http://your_host/网盘标识/分享key@分享密码
|
http://your_host/parser?url=UrlEncode(分享链接)&pwd=xxx
|
||||||
|
http://your_host/d/网盘标识/分享key@分享密码
|
||||||
2. 获取解析后的直链--JSON格式
|
2. 获取解析后的直链--JSON格式
|
||||||
http://your_host/json/parser?url=分享链接&pwd=xxx
|
http://your_host/json/parser?url=分享链接&pwd=xxx
|
||||||
http://your_host/json/网盘标识/分享key@分享密码
|
http://your_host/json/网盘标识/分享key@分享密码
|
||||||
|
3. 文件夹解析v0.1.8fixed3新增
|
||||||
|
http://your_host/json/getFileList?url=分享链接&pwd=xxx
|
||||||
|
|
||||||
|
|
||||||
|
### json接口说明
|
||||||
|
|
||||||
|
1. 文件解析:/json/parser?url=分享链接&pwd=xxx
|
||||||
|
|
||||||
```
|
|
||||||
json返回数据格式示例:
|
json返回数据格式示例:
|
||||||
`shareKey`: 全局分享key
|
`shareKey`: 全局分享key
|
||||||
`directLink`: 下载链接
|
`directLink`: 下载链接
|
||||||
`cacheHit`: 是否为缓存链接
|
`cacheHit`: 是否为缓存链接
|
||||||
`expires`: 缓存到期时间
|
`expires`: 缓存到期时间
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 200,
|
"code": 200,
|
||||||
@@ -83,6 +117,79 @@ json返回数据格式示例:
|
|||||||
"timestamp": 1726637151902
|
"timestamp": 1726637151902
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
2. 分享链接详情接口 /v2/linkInfo?url=分享链接
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"success": true,
|
||||||
|
"count": 0,
|
||||||
|
"data": {
|
||||||
|
"downLink": "https://lz.qaiu.top/d/fj/xx",
|
||||||
|
"apiLink": "https://lz.qaiu.top/json/fj/xx",
|
||||||
|
"cacheHitTotal": 5,
|
||||||
|
"parserTotal": 2,
|
||||||
|
"sumTotal": 7,
|
||||||
|
"shareLinkInfo": {
|
||||||
|
"shareKey": "xx",
|
||||||
|
"panName": "小飞机网盘",
|
||||||
|
"type": "fj",
|
||||||
|
"sharePassword": "",
|
||||||
|
"shareUrl": "https://share.feijipan.com/s/xx",
|
||||||
|
"standardUrl": "https://www.feijix.com/s/xx",
|
||||||
|
"otherParam": {
|
||||||
|
"UA": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
|
||||||
|
},
|
||||||
|
"cacheKey": "fj:xx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": 1736489219402
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
|
||||||
|
/v2/getFileList?url=分享链接&pwd=分享密码
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"fileName": "xxx",
|
||||||
|
"fileId": "xxx",
|
||||||
|
"fileIcon": null,
|
||||||
|
"size": 999,
|
||||||
|
"sizeStr": "999 M",
|
||||||
|
"fileType": "file/folder",
|
||||||
|
"filePath": null,
|
||||||
|
"createTime": "17 小时前",
|
||||||
|
"updateTime": null,
|
||||||
|
"createBy": null,
|
||||||
|
"description": null,
|
||||||
|
"downloadCount": 下载次数,
|
||||||
|
"panType": "lz",
|
||||||
|
"parserUrl": "下载链接/文件夹链接",
|
||||||
|
"extParameters": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. 解析次数统计接口 /v2/statisticsInfo
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"success": true,
|
||||||
|
"count": 0,
|
||||||
|
"data": {
|
||||||
|
"parserTotal": 320508,
|
||||||
|
"cacheTotal": 5957910,
|
||||||
|
"total": 6278418
|
||||||
|
},
|
||||||
|
"timestamp": 1736489378770
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
IDEA HttpClient示例:
|
IDEA HttpClient示例:
|
||||||
|
|
||||||
@@ -156,21 +263,22 @@ mvn package
|
|||||||
|
|
||||||
### Docker 部署(Main分支)
|
### Docker 部署(Main分支)
|
||||||
|
|
||||||
|
#### 海外服务器Docker部署
|
||||||
```shell
|
```shell
|
||||||
# 创建目录
|
# 创建目录
|
||||||
mkdir -p netdisk-fast-download
|
mkdir -p netdisk-fast-download
|
||||||
cd netdisk-fast-download
|
cd netdisk-fast-download
|
||||||
|
|
||||||
# 拉取镜像
|
# 拉取镜像
|
||||||
docker pull ghcr.io/qaiu/netdisk-fast-download:main
|
docker pull ghcr.io/qaiu/netdisk-fast-download:latest
|
||||||
|
|
||||||
# 复制配置文件(或下载仓库web-service\src\main\resources)
|
# 复制配置文件(或下载仓库web-service\src\main\resources)
|
||||||
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:main
|
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:latest
|
||||||
docker cp netdisk-fast-download:/app/resources ./resources
|
docker cp netdisk-fast-download:/app/resources ./resources
|
||||||
docker rm netdisk-fast-download
|
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:main
|
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端口
|
# 反代6401端口
|
||||||
|
|
||||||
@@ -178,7 +286,32 @@ docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-sto
|
|||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once netdisk-fast-download
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once netdisk-fast-download
|
||||||
```
|
```
|
||||||
|
|
||||||
### [宝塔安装参考](https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng)
|
#### 国内Docker部署
|
||||||
|
```shell
|
||||||
|
# 创建目录
|
||||||
|
mkdir -p netdisk-fast-download
|
||||||
|
cd netdisk-fast-download
|
||||||
|
|
||||||
|
# 拉取镜像
|
||||||
|
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: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:latest
|
||||||
|
|
||||||
|
# 反代6401端口
|
||||||
|
|
||||||
|
# 升级容器
|
||||||
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once netdisk-fast-download
|
||||||
|
```
|
||||||
|
|
||||||
|
### 宝塔部署指引 -> [点击进入宝塔部署教程](https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng)
|
||||||
|
|
||||||
|
### Linux命令行部署
|
||||||
> 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径
|
> 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径
|
||||||
```shell
|
```shell
|
||||||
cd ~
|
cd ~
|
||||||
@@ -220,17 +353,30 @@ resources目录下包含服务端配置文件 配置文件自带说明,具体
|
|||||||
app-dev.yml 可以配置解析服务相关信息, 包括端口,域名,缓存时长等
|
app-dev.yml 可以配置解析服务相关信息, 包括端口,域名,缓存时长等
|
||||||
server-proxy.yml 可以配置代理服务运行的相关信息, 包括前端反向代理端口,路径等
|
server-proxy.yml 可以配置代理服务运行的相关信息, 包括前端反向代理端口,路径等
|
||||||
|
|
||||||
|
### ip代理配置说明
|
||||||
|
有时候解析量很大,IP容易被ban,这时候可以使用其他服务器搭建nfd-proxy代理服务。
|
||||||
|
|
||||||
|
修改配置文件:
|
||||||
|
app-dev.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
proxy:
|
||||||
|
- panTypes: pgd,pdb,pod # 网盘标识
|
||||||
|
type: http # 支持http/socks4/socks5
|
||||||
|
host: 127.0.0.1 # 代理IP
|
||||||
|
port: 7890 # 端口
|
||||||
|
username: # 用户名
|
||||||
|
password: # 密码
|
||||||
|
```
|
||||||
|
nfd-proxy搭建http代理服务器
|
||||||
|
参考https://github.com/nfd-parser/nfd-proxy
|
||||||
|
|
||||||
## 0.1.9 开发计划
|
## 0.1.9 开发计划
|
||||||
- 超星网盘解析 doing
|
|
||||||
- 带Referer头的js请求下载 doing
|
|
||||||
- 城通网盘解析 √
|
|
||||||
- 目录解析(专属版)
|
- 目录解析(专属版)
|
||||||
- 带cookie/token参数解析大文件(专属版)
|
- 带cookie/token参数解析大文件(专属版)
|
||||||
- docker
|
|
||||||
|
|
||||||
**技术栈:**
|
**技术栈:**
|
||||||
Jdk17+Vert.x4.4.1
|
Jdk17+Vert.x4
|
||||||
Core模块集成Vert.x实现类似spring的注解式路由API
|
Core模块集成Vert.x实现类似spring的注解式路由API
|
||||||
|
|
||||||
|
|
||||||
@@ -238,13 +384,26 @@ Core模块集成Vert.x实现类似spring的注解式路由API
|
|||||||
|
|
||||||
[](https://star-history.com/#qaiu/netdisk-fast-download&Date)
|
[](https://star-history.com/#qaiu/netdisk-fast-download&Date)
|
||||||
|
|
||||||
|
## **免责声明**
|
||||||
|
- 用户在使用本项目时,应自行承担风险,并确保其行为符合当地法律法规及网盘服务提供商的使用条款。
|
||||||
|
- 开发者不对用户因使用本项目而导致的任何后果负责,包括但不限于数据丢失、隐私泄露、账号封禁或其他任何形式的损害。
|
||||||
|
|
||||||
## 支持该项目
|
## 支持该项目
|
||||||
开源不易,用爱发电,本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
|
开源不易,用爱发电,本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
|
||||||
赞助88元以上, 可以优先体验专享版--大文件解析,目录解析
|
|
||||||
|
|
||||||
|
### 关于专属版
|
||||||
|
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联调云盘的解析支持
|
||||||
|
199元, 包含部署服务和首页定制, 需提供宝塔环境
|
||||||
|
可以提供功能定制开发, 加v价格详谈:
|
||||||
|
<p>qq: 197575894</p>
|
||||||
|
<p>wechat: imcoding_</p>
|
||||||
|
|
||||||
|
<!--
|
||||||

|

|
||||||
|
|
||||||
[手机端支付宝打赏跳转链接](https://qr.alipay.com/fkx01882dnoxxtjenhlxt53)
|
[手机端支付宝打赏跳转链接](https://qr.alipay.com/fkx01882dnoxxtjenhlxt53)
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal
|
|
||||||
|
|
||||||
rem 获取当前 Java 版本信息并搜索是否包含 "17."
|
|
||||||
java -version 2>&1 | find "17." >nul
|
|
||||||
|
|
||||||
rem 如果找不到 JDK 17.x,则下载并安装
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo JDK 17.x not found. Downloading and installing...
|
|
||||||
|
|
||||||
REM 这里添加下载和安装 JDK 的代码
|
|
||||||
|
|
||||||
rem 验证安装
|
|
||||||
java -version
|
|
||||||
|
|
||||||
echo JDK 17.x installation complete.
|
|
||||||
) else (
|
|
||||||
echo JDK 17.x is already installed.
|
|
||||||
)
|
|
||||||
|
|
||||||
endlocal
|
|
||||||
pause
|
|
||||||
86
bin/nfd-install.sh
Normal file
86
bin/nfd-install.sh
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ----------- 配置区域 ------------
|
||||||
|
# JRE 下载目录
|
||||||
|
JRE_DIR="/opt/custom-jre17"
|
||||||
|
# 使用阿里云镜像下载 JRE(OpenJDK 17)
|
||||||
|
JRE_TARBALL_URL="https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jre/x64/linux/OpenJDK17U-jre_x64_linux_hotspot_17.0.15_6.tar.gz"
|
||||||
|
|
||||||
|
# ZIP 文件下载相关
|
||||||
|
ZIP_URL="http://www.722shop.top:6401/parser?url="
|
||||||
|
ZIP_DEST_DIR="/opt/target-zip"
|
||||||
|
ZIP_FILE_NAME="nfd.zip"
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
# 创建目录
|
||||||
|
mkdir -p "$JRE_DIR"
|
||||||
|
mkdir -p "$ZIP_DEST_DIR"
|
||||||
|
|
||||||
|
# -------- 检查 unzip 是否存在 --------
|
||||||
|
if ! command -v unzip >/dev/null 2>&1; then
|
||||||
|
echo "unzip 未安装,正在安装..."
|
||||||
|
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update && apt-get install -y unzip
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
yum install -y unzip
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
dnf install -y unzip
|
||||||
|
else
|
||||||
|
echo "不支持的包管理器,无法自动安装 unzip,请手动安装后重试。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "unzip 已安装"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------- 下载并解压 JRE --------
|
||||||
|
echo "下载 JRE 17 到 $JRE_DIR..."
|
||||||
|
curl -L "$JRE_TARBALL_URL" -o "$JRE_DIR/jre17.tar.gz"
|
||||||
|
|
||||||
|
echo "解压 JRE..."
|
||||||
|
tar -xzf "$JRE_DIR/jre17.tar.gz" -C "$JRE_DIR" --strip-components=1
|
||||||
|
rm "$JRE_DIR/jre17.tar.gz"
|
||||||
|
echo "JRE 解压完成"
|
||||||
|
|
||||||
|
# -------- 下载 ZIP 文件 --------
|
||||||
|
ZIP_PATH="$ZIP_DEST_DIR/$ZIP_FILE_NAME"
|
||||||
|
echo "下载 ZIP 文件到 $ZIP_PATH..."
|
||||||
|
curl -L "$ZIP_URL" -o "$ZIP_PATH"
|
||||||
|
|
||||||
|
# -------- 解压 ZIP 文件 --------
|
||||||
|
echo "解压 ZIP 文件到 $ZIP_DEST_DIR..."
|
||||||
|
unzip -o "$ZIP_PATH" -d "$ZIP_DEST_DIR"
|
||||||
|
echo "解压完成"
|
||||||
|
|
||||||
|
# -------- 启动 JAR 程序 --------
|
||||||
|
echo "进入 JAR 目录并后台运行程序..."
|
||||||
|
|
||||||
|
JAR_DIR="/opt/target-zip/netdisk-fast-download"
|
||||||
|
JAR_FILE="netdisk-fast-download.jar"
|
||||||
|
JAVA_BIN="$JRE_DIR/bin/java"
|
||||||
|
LOG_FILE="$JAR_DIR/app.log"
|
||||||
|
|
||||||
|
if [ ! -d "$JAR_DIR" ]; then
|
||||||
|
echo "[错误] 找不到 JAR 目录: $JAR_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$JAR_DIR"
|
||||||
|
|
||||||
|
if [ ! -f "$JAR_FILE" ]; then
|
||||||
|
echo "[错误] 找不到 JAR 文件: $JAR_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$JAVA_BIN" ]; then
|
||||||
|
echo "[错误] 找不到可执行的 java: $JAVA_BIN"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 后台运行,日志记录
|
||||||
|
nohup "$JAVA_BIN" -jar "$JAR_FILE" > "$LOG_FILE" 2>&1 &
|
||||||
|
|
||||||
|
echo "程序已在后台启动 ✅"
|
||||||
|
echo "日志路径: $LOG_FILE"
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<name>netdisk-fast-download</name>
|
<name>netdisk-fast-download</name>
|
||||||
<description>netdisk fast download service</description>
|
<description>netdisk fast download service</description>
|
||||||
<executable>java</executable>
|
<executable>java</executable>
|
||||||
<arguments> -server -Xmx128m -jar ${jar} </arguments>
|
<arguments> -server -Xmx128m -Dfile.encoding=utf8 -jar ${jar} </arguments>
|
||||||
<logpath>${dd}\logs</logpath>
|
<logpath>${dd}\logs</logpath>
|
||||||
<log mode="roll-by-time">
|
<log mode="roll-by-time">
|
||||||
<pattern>yyyyMMdd</pattern>
|
<pattern>yyyyMMdd</pattern>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-lang3</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
<version>3.12.0</version>
|
<version>3.18.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.vertx</groupId>
|
<groupId>io.vertx</groupId>
|
||||||
@@ -59,6 +59,18 @@
|
|||||||
<artifactId>vertx-jdbc-client</artifactId>
|
<artifactId>vertx-jdbc-client</artifactId>
|
||||||
<version>${vertx.version}</version>
|
<version>${vertx.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<version>9.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- PG驱动-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>42.7.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public @interface Constraint {
|
|||||||
boolean notNull() default false;
|
boolean notNull() default false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 唯一键约束 TODO 待实现
|
|
||||||
* @return 唯一键约束
|
* @return 唯一键约束
|
||||||
*/
|
*/
|
||||||
String uniqueKey() default "";
|
String uniqueKey() default "";
|
||||||
@@ -32,7 +31,7 @@ public @interface Constraint {
|
|||||||
*/
|
*/
|
||||||
String defaultValue() default "";
|
String defaultValue() default "";
|
||||||
/**
|
/**
|
||||||
* 默认值是否是函数
|
* 默认值是否是函数 like value=NOW()
|
||||||
* @return false 不是函数
|
* @return false 不是函数
|
||||||
*/
|
*/
|
||||||
boolean defaultValueIsFunction() default false;
|
boolean defaultValueIsFunction() default false;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package cn.qaiu.db.ddl;
|
||||||
|
|
||||||
|
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class CreateDatabase {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析数据库URL,获取数据库名
|
||||||
|
* @param url 数据库URL
|
||||||
|
* @return 数据库名
|
||||||
|
*/
|
||||||
|
public static String getDatabaseName(String url) {
|
||||||
|
// 正则表达式匹配数据库名
|
||||||
|
String regex = "jdbc:mysql://[^/]+/(\\w+)(\\?.*)?";
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
Matcher matcher = pattern.matcher(url);
|
||||||
|
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid database URL: " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用JDBC原生方法创建数据库
|
||||||
|
* @param url 数据库连接URL
|
||||||
|
* @param user 数据库用户名
|
||||||
|
* @param password 数据库密码
|
||||||
|
*/
|
||||||
|
public static void createDatabase(String url, String user, String password) {
|
||||||
|
String dbName = getDatabaseName(url);
|
||||||
|
LOGGER.info(">>>>>>>>>>> 创建数据库:'{}' <<<<<<<<<<<< ", dbName);
|
||||||
|
|
||||||
|
// 去掉数据库名,构建不带数据库名的URL
|
||||||
|
String baseUrl = url.substring(0, url.lastIndexOf("/") + 1) + "?characterEncoding=UTF-8&useUnicode=true";
|
||||||
|
|
||||||
|
try (Connection conn = DriverManager.getConnection(baseUrl, user, password);
|
||||||
|
Statement stmt = conn.createStatement()) {
|
||||||
|
// 创建数据库
|
||||||
|
stmt.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||||
|
LOGGER.info(">>>>>>>>>>> 数据库'{}'创建成功 <<<<<<<<<<<<", dbName);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createDatabase(JsonObject dbConfig) {
|
||||||
|
createDatabase(
|
||||||
|
dbConfig.getString("jdbcUrl"),
|
||||||
|
dbConfig.getString("username"),
|
||||||
|
dbConfig.getString("password")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@ import io.vertx.codegen.format.CamelCase;
|
|||||||
import io.vertx.codegen.format.Case;
|
import io.vertx.codegen.format.Case;
|
||||||
import io.vertx.codegen.format.LowerCamelCase;
|
import io.vertx.codegen.format.LowerCamelCase;
|
||||||
import io.vertx.codegen.format.SnakeCase;
|
import io.vertx.codegen.format.SnakeCase;
|
||||||
import io.vertx.jdbcclient.JDBCPool;
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.sqlclient.Pool;
|
||||||
import io.vertx.sqlclient.templates.annotations.Column;
|
import io.vertx.sqlclient.templates.annotations.Column;
|
||||||
import io.vertx.sqlclient.templates.annotations.RowMapped;
|
import io.vertx.sqlclient.templates.annotations.RowMapped;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -14,9 +16,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建表
|
* 创建表
|
||||||
@@ -24,154 +24,312 @@ import java.util.Set;
|
|||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
*/
|
*/
|
||||||
public class CreateTable {
|
public class CreateTable {
|
||||||
public static Map<Class<?>, String> javaProperty2SqlColumnMap = new HashMap<>();
|
public static Map<Class<?>, String> javaProperty2SqlColumnMap = new HashMap<>() {{
|
||||||
|
// Java类型到SQL类型的映射
|
||||||
|
put(Integer.class, "INT");
|
||||||
|
put(Short.class, "SMALLINT");
|
||||||
|
put(Byte.class, "TINYINT");
|
||||||
|
put(Long.class, "BIGINT");
|
||||||
|
put(java.math.BigDecimal.class, "DECIMAL");
|
||||||
|
put(Double.class, "DOUBLE");
|
||||||
|
put(Float.class, "REAL");
|
||||||
|
put(Boolean.class, "BOOLEAN");
|
||||||
|
put(String.class, "VARCHAR");
|
||||||
|
put(Date.class, "TIMESTAMP");
|
||||||
|
put(java.time.LocalDateTime.class, "TIMESTAMP");
|
||||||
|
put(java.sql.Timestamp.class, "TIMESTAMP");
|
||||||
|
put(java.sql.Date.class, "DATE");
|
||||||
|
put(java.sql.Time.class, "TIME");
|
||||||
|
|
||||||
|
// 基本数据类型
|
||||||
|
put(int.class, "INT");
|
||||||
|
put(short.class, "SMALLINT");
|
||||||
|
put(byte.class, "TINYINT");
|
||||||
|
put(long.class, "BIGINT");
|
||||||
|
put(double.class, "DOUBLE");
|
||||||
|
put(float.class, "REAL");
|
||||||
|
put(boolean.class, "BOOLEAN");
|
||||||
|
}};
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class);
|
||||||
|
public static String UNIQUE_PREFIX = "idx_";
|
||||||
static {
|
|
||||||
javaProperty2SqlColumnMap.put(Integer.class, "INT");
|
|
||||||
javaProperty2SqlColumnMap.put(Short.class, "SMALLINT");
|
|
||||||
javaProperty2SqlColumnMap.put(Byte.class, "TINYINT");
|
|
||||||
javaProperty2SqlColumnMap.put(Long.class, "BIGINT");
|
|
||||||
javaProperty2SqlColumnMap.put(java.math.BigDecimal.class, "DECIMAL");
|
|
||||||
javaProperty2SqlColumnMap.put(Double.class, "DOUBLE");
|
|
||||||
javaProperty2SqlColumnMap.put(Float.class, "REAL");
|
|
||||||
javaProperty2SqlColumnMap.put(Boolean.class, "BOOLEAN");
|
|
||||||
javaProperty2SqlColumnMap.put(String.class, "VARCHAR");
|
|
||||||
javaProperty2SqlColumnMap.put(java.util.Date.class, "TIMESTAMP");
|
|
||||||
javaProperty2SqlColumnMap.put(java.time.LocalDateTime.class, "TIMESTAMP");
|
|
||||||
javaProperty2SqlColumnMap.put(java.sql.Timestamp.class, "TIMESTAMP");
|
|
||||||
javaProperty2SqlColumnMap.put(java.sql.Date.class, "DATE");
|
|
||||||
javaProperty2SqlColumnMap.put(java.sql.Time.class, "TIME");
|
|
||||||
|
|
||||||
javaProperty2SqlColumnMap.put(int.class, "INT");
|
|
||||||
javaProperty2SqlColumnMap.put(short.class, "SMALLINT");
|
|
||||||
javaProperty2SqlColumnMap.put(byte.class, "TINYINT");
|
|
||||||
javaProperty2SqlColumnMap.put(long.class, "BIGINT");
|
|
||||||
javaProperty2SqlColumnMap.put(double.class, "DOUBLE");
|
|
||||||
javaProperty2SqlColumnMap.put(float.class, "REAL");
|
|
||||||
javaProperty2SqlColumnMap.put(boolean.class, "BOOLEAN");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Case getCase(Class<?> clz) {
|
private static Case getCase(Class<?> clz) {
|
||||||
switch (clz.getName()) {
|
return switch (clz.getName()) {
|
||||||
case "io.vertx.codegen.format.CamelCase":
|
case "io.vertx.codegen.format.CamelCase" -> CamelCase.INSTANCE;
|
||||||
return CamelCase.INSTANCE;
|
case "io.vertx.codegen.format.SnakeCase" -> SnakeCase.INSTANCE;
|
||||||
case "io.vertx.codegen.format.SnakeCase":
|
case "io.vertx.codegen.format.LowerCamelCase" -> LowerCamelCase.INSTANCE;
|
||||||
return SnakeCase.INSTANCE;
|
default -> throw new UnsupportedOperationException();
|
||||||
case "io.vertx.codegen.format.LowerCamelCase":
|
};
|
||||||
return LowerCamelCase.INSTANCE;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCreateTableSQL(Class<?> clz, JDBCType type) {
|
public static List<String> getCreateTableSQL(Class<?> clz, JDBCType type) {
|
||||||
String quotationMarks = type == JDBCType.H2DB ? "\"" : "`";
|
// 获取表名和主键
|
||||||
String endStr = type == JDBCType.H2DB ? ");" : ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
TableInfo tableInfo = extractTableInfo(clz, type);
|
||||||
// 判断类上是否有次注解
|
|
||||||
String primaryKey = null; // 主键
|
// 构建表的SQL语句
|
||||||
String tableName = null; // 表名
|
List<String> sqlList = new ArrayList<>();
|
||||||
|
StringBuilder sb = new StringBuilder(50);
|
||||||
|
sb.append("CREATE TABLE IF NOT EXISTS ")
|
||||||
|
.append(tableInfo.quotationMarks).append(tableInfo.tableName).append(tableInfo.quotationMarks)
|
||||||
|
.append(" ( \r\n ");
|
||||||
|
|
||||||
|
// 处理字段并生成列定义
|
||||||
|
List<String> indexSQLs = new ArrayList<>();
|
||||||
|
processFields(clz, tableInfo, sb, indexSQLs);
|
||||||
|
|
||||||
|
// 去掉最后一个逗号并添加表尾部信息
|
||||||
|
String tableSQL = sb.substring(0, sb.lastIndexOf(",")) + tableInfo.endStr;
|
||||||
|
sqlList.add(tableSQL);
|
||||||
|
|
||||||
|
// 添加索引SQL
|
||||||
|
sqlList.addAll(indexSQLs);
|
||||||
|
|
||||||
|
return sqlList;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 修改extractTableInfo方法,处理没有Table注解时默认使用id字段作为主键
|
||||||
|
private static TableInfo extractTableInfo(Class<?> clz, JDBCType type) {
|
||||||
|
String quotationMarks;
|
||||||
|
String endStr;
|
||||||
|
if (type == JDBCType.MySQL) {
|
||||||
|
quotationMarks = "`";
|
||||||
|
endStr = ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
||||||
|
} else {
|
||||||
|
quotationMarks = "\"";
|
||||||
|
endStr = ");";
|
||||||
|
}
|
||||||
|
|
||||||
|
String primaryKey = null;
|
||||||
|
String tableName = null;
|
||||||
Case caseFormat = SnakeCase.INSTANCE;
|
Case caseFormat = SnakeCase.INSTANCE;
|
||||||
|
|
||||||
|
// 判断类上是否有RowMapped注解
|
||||||
if (clz.isAnnotationPresent(RowMapped.class)) {
|
if (clz.isAnnotationPresent(RowMapped.class)) {
|
||||||
RowMapped annotation = clz.getAnnotation(RowMapped.class);
|
RowMapped annotation = clz.getAnnotation(RowMapped.class);
|
||||||
Class<? extends Case> formatter = annotation.formatter();
|
caseFormat = getCase(annotation.formatter());
|
||||||
caseFormat = getCase(formatter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断类上是否有Table注解
|
||||||
if (clz.isAnnotationPresent(Table.class)) {
|
if (clz.isAnnotationPresent(Table.class)) {
|
||||||
// 获取类上的注解
|
|
||||||
Table annotation = clz.getAnnotation(Table.class);
|
Table annotation = clz.getAnnotation(Table.class);
|
||||||
// 输出注解上的类名
|
tableName = StringUtils.isNotEmpty(annotation.value())
|
||||||
String tableNameAnnotation = annotation.value();
|
? annotation.value()
|
||||||
if (StringUtils.isNotEmpty(tableNameAnnotation)) {
|
: LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
|
||||||
tableName = tableNameAnnotation;
|
|
||||||
} else {
|
|
||||||
tableName = LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
|
|
||||||
}
|
|
||||||
primaryKey = annotation.keyFields();
|
primaryKey = annotation.keyFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果表名仍为null,使用类名转下划线命名作为表名
|
||||||
|
if (StringUtils.isEmpty(tableName)) {
|
||||||
|
tableName = LowerCamelCase.INSTANCE.to(SnakeCase.INSTANCE, clz.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果主键为空,默认使用id字段作为主键
|
||||||
|
if (StringUtils.isEmpty(primaryKey)) {
|
||||||
|
try {
|
||||||
|
clz.getDeclaredField("id");
|
||||||
|
primaryKey = "id";
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
// 如果没有id字段,不设置主键
|
||||||
|
primaryKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TableInfo(tableName, quotationMarks, endStr, primaryKey, caseFormat, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改processFields方法,处理索引
|
||||||
|
private static void processFields(Class<?> clz, TableInfo tableInfo, StringBuilder sb, List<String> indexSQLs) {
|
||||||
Field[] fields = clz.getDeclaredFields();
|
Field[] fields = clz.getDeclaredFields();
|
||||||
String column;
|
for (Field field : fields) {
|
||||||
int[] decimalSize = {22, 2};
|
// 跳过无效字段
|
||||||
int varcharSize = 255;
|
if (isIgnoredField(field)) {
|
||||||
StringBuilder sb = new StringBuilder(50);
|
|
||||||
sb.append("CREATE TABLE IF NOT EXISTS ").append(quotationMarks).append(tableName).append(quotationMarks).append(" ( \r\n ");
|
|
||||||
boolean firstId = true;
|
|
||||||
for (Field f : fields) {
|
|
||||||
Class<?> paramType = f.getType();
|
|
||||||
String sqlType = javaProperty2SqlColumnMap.get(paramType);
|
|
||||||
if (f.getName().equals("serialVersionUID") || StringUtils.isEmpty(sqlType) || f.isAnnotationPresent(TableGenIgnore.class)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
column = LowerCamelCase.INSTANCE.to(caseFormat, f.getName());
|
|
||||||
if (f.isAnnotationPresent(Column.class)) {
|
// 获取字段名和SQL类型
|
||||||
Column columnAnnotation = f.getAnnotation(Column.class);
|
String column = LowerCamelCase.INSTANCE.to(tableInfo.caseFormat, field.getName());
|
||||||
//输出注解属性
|
String sqlType = javaProperty2SqlColumnMap.get(field.getType());
|
||||||
|
|
||||||
|
// 处理字段注解
|
||||||
|
column = processColumnAnnotation(field, column);
|
||||||
|
int[] decimalSize = {22, 2};
|
||||||
|
int varcharSize = 255;
|
||||||
|
if (field.isAnnotationPresent(Length.class)) {
|
||||||
|
Length length = field.getAnnotation(Length.class);
|
||||||
|
decimalSize = length.decimalSize();
|
||||||
|
varcharSize = length.varcharSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建列定义
|
||||||
|
sb.append(tableInfo.quotationMarks).append(column).append(tableInfo.quotationMarks)
|
||||||
|
.append(" ").append(sqlType);
|
||||||
|
appendTypeLength(sqlType, sb, decimalSize, varcharSize);
|
||||||
|
appendConstraints(field, sb, tableInfo);
|
||||||
|
appendPrimaryKey(tableInfo.primaryKey, column, sb);
|
||||||
|
|
||||||
|
// 添加索引
|
||||||
|
appendIndex(tableInfo, indexSQLs, field);
|
||||||
|
|
||||||
|
sb.append(",\n ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否忽略字段
|
||||||
|
private static boolean isIgnoredField(Field field) {
|
||||||
|
return field.getName().equals("serialVersionUID")
|
||||||
|
|| StringUtils.isEmpty(javaProperty2SqlColumnMap.get(field.getType()))
|
||||||
|
|| field.isAnnotationPresent(TableGenIgnore.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理Column注解
|
||||||
|
private static String processColumnAnnotation(Field field, String column) {
|
||||||
|
if (field.isAnnotationPresent(Column.class)) {
|
||||||
|
Column columnAnnotation = field.getAnnotation(Column.class);
|
||||||
if (StringUtils.isNotBlank(columnAnnotation.name())) {
|
if (StringUtils.isNotBlank(columnAnnotation.name())) {
|
||||||
column = columnAnnotation.name();
|
column = columnAnnotation.name();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (f.isAnnotationPresent(Length.class)) {
|
return column;
|
||||||
Length fieldAnnotation = f.getAnnotation(Length.class);
|
|
||||||
decimalSize = fieldAnnotation.decimalSize();
|
|
||||||
varcharSize = fieldAnnotation.varcharSize();
|
|
||||||
}
|
}
|
||||||
sb.append(quotationMarks).append(column).append(quotationMarks);
|
|
||||||
sb.append(" ").append(sqlType);
|
|
||||||
// 添加类型长度
|
// 添加类型长度
|
||||||
if (sqlType.equals("DECIMAL")) {
|
private static void appendTypeLength(String sqlType, StringBuilder sb, int[] decimalSize, int varcharSize) {
|
||||||
|
if ("DECIMAL".equals(sqlType)) {
|
||||||
sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
|
sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
|
||||||
}
|
} else if ("VARCHAR".equals(sqlType)) {
|
||||||
if (sqlType.equals("VARCHAR")) {
|
|
||||||
sb.append("(").append(varcharSize).append(")");
|
sb.append("(").append(varcharSize).append(")");
|
||||||
}
|
}
|
||||||
if (f.isAnnotationPresent(Constraint.class)) {
|
}
|
||||||
Constraint constraintAnnotation = f.getAnnotation(Constraint.class);
|
|
||||||
if (constraintAnnotation.notNull()) {
|
// 添加约束
|
||||||
//非空约束
|
private static void appendConstraints(Field field, StringBuilder sb, TableInfo tableInfo) {
|
||||||
|
JDBCType type = tableInfo.dbType;
|
||||||
|
|
||||||
|
if (field.isAnnotationPresent(Constraint.class)) {
|
||||||
|
Constraint constraint = field.getAnnotation(Constraint.class);
|
||||||
|
if (constraint.notNull()) {
|
||||||
sb.append(" NOT NULL");
|
sb.append(" NOT NULL");
|
||||||
}
|
}
|
||||||
String apostrophe = constraintAnnotation.defaultValueIsFunction() ? "" : "'";
|
String apostrophe = constraint.defaultValueIsFunction() ? "" : "'";
|
||||||
if (StringUtils.isNotEmpty(constraintAnnotation.defaultValue())) {
|
if (StringUtils.isNotEmpty(constraint.defaultValue())) {
|
||||||
//默认值约束
|
sb.append(" DEFAULT ").append(apostrophe).append(constraint.defaultValue()).append(apostrophe);
|
||||||
sb.append(" DEFAULT ").append(apostrophe).append(constraintAnnotation.defaultValue()).append(apostrophe);
|
|
||||||
}
|
}
|
||||||
if (constraintAnnotation.autoIncrement() && paramType.equals(Integer.class) || paramType.equals(Long.class)) {
|
if (constraint.autoIncrement()) {
|
||||||
////自增
|
if (type == JDBCType.PostgreSQL) {
|
||||||
|
// 需要移除字段类型(最后一个单词)
|
||||||
|
if (field.getType().equals(Integer.class)) {
|
||||||
|
sb.delete(sb.lastIndexOf(" "), sb.length());
|
||||||
|
sb.append(" SERIAL");
|
||||||
|
} else if (field.getType().equals(Long.class)) {
|
||||||
|
sb.delete(sb.lastIndexOf(" "), sb.length());
|
||||||
|
sb.append(" BIGSERIAL");
|
||||||
|
}
|
||||||
|
} else if (field.getType().equals(Integer.class) || field.getType().equals(Long.class)) {
|
||||||
sb.append(" AUTO_INCREMENT");
|
sb.append(" AUTO_INCREMENT");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加主键
|
||||||
|
private static void appendPrimaryKey(String primaryKey, String column, StringBuilder sb) {
|
||||||
if (StringUtils.isEmpty(primaryKey)) {
|
if (StringUtils.isEmpty(primaryKey)) {
|
||||||
if (firstId) {//类型转换
|
return;
|
||||||
sb.append(" PRIMARY KEY");
|
|
||||||
firstId = false;
|
|
||||||
}
|
}
|
||||||
|
if (primaryKey.equalsIgnoreCase(column)) {
|
||||||
|
sb.append(" PRIMARY KEY");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendIndex(TableInfo tableInfo, List<String> indexSQLs, Field field) {
|
||||||
|
if (!field.isAnnotationPresent(Constraint.class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Constraint constraint = field.getAnnotation(Constraint.class);
|
||||||
|
if (StringUtils.isEmpty(constraint.uniqueKey())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String indexName = UNIQUE_PREFIX + tableInfo.tableName + "_" + constraint.uniqueKey();
|
||||||
|
String columnName = field.getName();
|
||||||
|
|
||||||
|
// 检查是否已有相同索引名称的索引
|
||||||
|
Optional<String> existingIndex = indexSQLs.stream()
|
||||||
|
.filter(sql -> sql.contains(tableInfo.quotationMarks + indexName + tableInfo.quotationMarks))
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (existingIndex.isPresent()) {
|
||||||
|
// 如果存在相同索引名称,追加字段到索引定义中
|
||||||
|
String updatedIndex = existingIndex.get().replaceFirst(
|
||||||
|
"\\(([^)]+)\\)", // 匹配索引字段列表
|
||||||
|
"($1, " + tableInfo.quotationMarks + columnName + tableInfo.quotationMarks + ")"
|
||||||
|
);
|
||||||
|
indexSQLs.remove(existingIndex.get());
|
||||||
|
indexSQLs.add(updatedIndex);
|
||||||
} else {
|
} else {
|
||||||
if (primaryKey.equals(column.toLowerCase())) {
|
// 如果不存在相同索引名称,创建新的索引
|
||||||
sb.append(" PRIMARY KEY");
|
String indexSQL = String.format(
|
||||||
|
"CREATE UNIQUE INDEX %s %s%s%s ON %s%s%s (%s%s%s);",
|
||||||
|
tableInfo.dbType == JDBCType.MySQL ? "" : "IF NOT EXISTS",
|
||||||
|
tableInfo.quotationMarks, indexName, tableInfo.quotationMarks,
|
||||||
|
tableInfo.quotationMarks, tableInfo.tableName, tableInfo.quotationMarks,
|
||||||
|
tableInfo.quotationMarks, columnName, tableInfo.quotationMarks
|
||||||
|
);
|
||||||
|
indexSQLs.add(indexSQL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb.append(",\n ");
|
|
||||||
}
|
|
||||||
String sql = sb.toString();
|
|
||||||
//去掉最后一个逗号
|
|
||||||
int lastIndex = sql.lastIndexOf(",");
|
|
||||||
sql = sql.substring(0, lastIndex) + sql.substring(lastIndex + 1);
|
|
||||||
return sql.substring(0, sql.length() - 1) + endStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void createTable(JDBCPool pool, JDBCType type) {
|
// 表信息类
|
||||||
Set<Class<?>> tableClassList = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
|
private record TableInfo(
|
||||||
if (tableClassList.isEmpty()) LOGGER.info("Table model class not fount");
|
String tableName, // 表名
|
||||||
tableClassList.forEach(clazz -> {
|
String quotationMarks, // 引号或反引号
|
||||||
String createTableSQL = getCreateTableSQL(clazz, type);
|
String endStr, // 表尾部信息
|
||||||
|
String primaryKey, // 主键字段
|
||||||
pool.query(createTableSQL).execute().onSuccess(
|
Case caseFormat, // 命名格式
|
||||||
rs -> LOGGER.info("table auto generate:\n" + createTableSQL)
|
JDBCType dbType // 数据库类型
|
||||||
).onFailure(e -> {
|
) {
|
||||||
LOGGER.error(e.getMessage() + " SQL: \n" + createTableSQL);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Future<Void> createTable(Pool pool, JDBCType type) {
|
||||||
|
Promise<Void> promise = Promise.promise();
|
||||||
|
Set<Class<?>> tableClasses = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
|
||||||
|
|
||||||
|
if (tableClasses.isEmpty()) {
|
||||||
|
LOGGER.warn("Table model class not found");
|
||||||
|
promise.complete();
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Future<Object>> futures = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Class<?> clazz : tableClasses) {
|
||||||
|
List<String> sqlList = getCreateTableSQL(clazz, type);
|
||||||
|
LOGGER.info("Class `{}` auto-generate table", clazz.getName());
|
||||||
|
|
||||||
|
for (String sql : sqlList) {
|
||||||
|
try {
|
||||||
|
pool.query(sql).execute().toCompletionStage().toCompletableFuture().join();
|
||||||
|
futures.add(Future.succeededFuture());
|
||||||
|
LOGGER.debug("Executed SQL:\n{}", sql);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String message = e.getMessage();
|
||||||
|
if (message != null && message.contains("Duplicate key name")) {
|
||||||
|
LOGGER.warn("Ignoring duplicate key error: {}", message);
|
||||||
|
futures.add(Future.succeededFuture());
|
||||||
|
} else {
|
||||||
|
LOGGER.error("SQL Error: {}\nSQL: {}", message, sql);
|
||||||
|
futures.add(Future.failedFuture(e));
|
||||||
|
throw new RuntimeException(e); // Stop execution for other exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.all(futures).onSuccess(r -> promise.complete()).onFailure(promise::fail);
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package cn.qaiu.db.pool;
|
package cn.qaiu.db.pool;
|
||||||
|
|
||||||
import cn.qaiu.db.ddl.CreateTable;
|
import cn.qaiu.db.ddl.CreateTable;
|
||||||
|
import cn.qaiu.db.ddl.CreateDatabase;
|
||||||
import cn.qaiu.vx.core.util.VertxHolder;
|
import cn.qaiu.vx.core.util.VertxHolder;
|
||||||
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Vertx;
|
import io.vertx.core.Vertx;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.jdbcclient.JDBCPool;
|
import io.vertx.jdbcclient.JDBCPool;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -17,18 +20,29 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class JDBCPoolInit {
|
public class JDBCPoolInit {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
||||||
|
|
||||||
|
private static final String providerClass = io.vertx.ext.jdbc.spi.impl.HikariCPDataSourceProvider.class.getName();
|
||||||
|
|
||||||
private JDBCPool pool = null;
|
private JDBCPool pool = null;
|
||||||
JsonObject dbConfig;
|
JsonObject dbConfig;
|
||||||
Vertx vertx = VertxHolder.getVertxInstance();
|
Vertx vertx = VertxHolder.getVertxInstance();
|
||||||
String url;
|
String url;
|
||||||
|
|
||||||
private final JDBCType type;
|
private final JDBCType type;
|
||||||
|
|
||||||
private static JDBCPoolInit instance;
|
private static JDBCPoolInit instance;
|
||||||
|
|
||||||
|
public JDBCType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
public JDBCPoolInit(Builder builder) {
|
public JDBCPoolInit(Builder builder) {
|
||||||
this.dbConfig = builder.dbConfig;
|
this.dbConfig = builder.dbConfig;
|
||||||
this.url = builder.url;
|
this.url = builder.url;
|
||||||
this.type = builder.type;
|
this.type = JDBCType.getJDBCTypeByURL(builder.url);
|
||||||
|
if (StringUtils.isBlank(builder.dbConfig.getString("provider_class"))) {
|
||||||
|
builder.dbConfig.put("provider_class", providerClass);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
@@ -42,12 +56,10 @@ public class JDBCPoolInit {
|
|||||||
public static class Builder {
|
public static class Builder {
|
||||||
private JsonObject dbConfig;
|
private JsonObject dbConfig;
|
||||||
private String url;
|
private String url;
|
||||||
private JDBCType type;
|
|
||||||
|
|
||||||
public Builder config(JsonObject dbConfig) {
|
public Builder config(JsonObject dbConfig) {
|
||||||
this.dbConfig = dbConfig;
|
this.dbConfig = dbConfig;
|
||||||
this.url = dbConfig.getString("jdbcUrl");
|
this.url = dbConfig.getString("jdbcUrl");
|
||||||
this.type = JDBCUtil.getJDBCType(dbConfig.getString("driverClassName"));
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,23 +71,27 @@ public class JDBCPoolInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* init h2db<br>
|
* init h2db<br>
|
||||||
* 这个方法只允许调用一次
|
* 这个方法只允许调用一次
|
||||||
*/
|
*/
|
||||||
synchronized public void initPool() {
|
synchronized public Future<Void> initPool() {
|
||||||
if (pool != null) {
|
if (pool != null) {
|
||||||
LOGGER.error("pool 重复初始化");
|
LOGGER.error("pool 重复初始化");
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据库连接
|
// 初始化数据库连接
|
||||||
// 初始化连接池
|
// 初始化连接池
|
||||||
pool = JDBCPool.pool(vertx, dbConfig);
|
if (type == JDBCType.MySQL) {
|
||||||
CreateTable.createTable(pool, type);
|
CreateDatabase.createDatabase(dbConfig);
|
||||||
LOGGER.info("数据库连接初始化: URL=" + url);
|
|
||||||
}
|
}
|
||||||
|
pool = JDBCPool.pool(vertx, dbConfig);
|
||||||
|
LOGGER.info("数据库连接初始化: URL=" + url);
|
||||||
|
return CreateTable.createTable(pool, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取连接池
|
* 获取连接池
|
||||||
|
|||||||
@@ -1,9 +1,35 @@
|
|||||||
package cn.qaiu.db.pool;
|
package cn.qaiu.db.pool;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
* @date 2023/10/10 14:06
|
* @since 2023/10/10 14:06
|
||||||
*/
|
*/
|
||||||
public enum JDBCType {
|
public enum JDBCType {
|
||||||
MySQL, H2DB
|
// 添加驱动类型字段
|
||||||
|
MySQL("jdbc:mysql:"),
|
||||||
|
H2DB("jdbc:h2:"),
|
||||||
|
PostgreSQL("jdbc:postgresql:");
|
||||||
|
private final String urlPrefix; // JDBC URL 前缀
|
||||||
|
|
||||||
|
// 构造函数
|
||||||
|
JDBCType(String urlPrefix) {
|
||||||
|
this.urlPrefix = urlPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 JDBC URL 前缀
|
||||||
|
public String getUrlPrefix() {
|
||||||
|
return urlPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 JDBC URL 获取 JDBC 类型
|
||||||
|
public static JDBCType getJDBCTypeByURL(String jdbcURL) {
|
||||||
|
for (JDBCType jdbcType : values()) {
|
||||||
|
if (StringUtils.startsWithIgnoreCase(jdbcURL, jdbcType.getUrlPrefix())) {
|
||||||
|
return jdbcType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RuntimeException("不支持的SQL类型: " + jdbcURL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package cn.qaiu.db.pool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
|
||||||
* @date 2023/10/10 14:05
|
|
||||||
*/
|
|
||||||
public class JDBCUtil {
|
|
||||||
public static JDBCType getJDBCType(String deviceName) {
|
|
||||||
switch (deviceName) {
|
|
||||||
case "com.mysql.cj.jdbc.Driver":
|
|
||||||
case "com.mysql.jdbc.Driver":
|
|
||||||
return JDBCType.MySQL;
|
|
||||||
case "org.h2.Driver":
|
|
||||||
return JDBCType.H2DB;
|
|
||||||
}
|
|
||||||
throw new RuntimeException("不支持的SQL驱动类型: " + deviceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.build.timestamp.format>yyMMdd_HHmm</maven.build.timestamp.format>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ public final class Deploy {
|
|||||||
public void start(String[] args, Handler<JsonObject> handle) {
|
public void start(String[] args, Handler<JsonObject> handle) {
|
||||||
this.mainThread = Thread.currentThread();
|
this.mainThread = Thread.currentThread();
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
if (args.length > 0) {
|
if (args.length > 0 && args[0].startsWith("app-")) {
|
||||||
// 启动参数dev或者prod
|
// 启动参数dev或者prod
|
||||||
path.append("-").append(args[0]);
|
path.append("-").append(args[0].replace("app-",""));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取yml配置
|
// 读取yml配置
|
||||||
|
|||||||
@@ -175,8 +175,13 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
|||||||
route.handler(ResponseTimeHandler.create());
|
route.handler(ResponseTimeHandler.create());
|
||||||
route.handler(ctx -> handlerMethod(instance, method, ctx)).failureHandler(ctx -> {
|
route.handler(ctx -> handlerMethod(instance, method, ctx)).failureHandler(ctx -> {
|
||||||
if (ctx.response().ended()) return;
|
if (ctx.response().ended()) return;
|
||||||
|
// 超时处理器状态码503
|
||||||
|
if (ctx.statusCode() == 503 || ctx.failure() == null) {
|
||||||
|
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员", 500));
|
||||||
|
} else {
|
||||||
ctx.failure().printStackTrace();
|
ctx.failure().printStackTrace();
|
||||||
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500));
|
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
|
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
|
||||||
// websocket 基于sockJs
|
// websocket 基于sockJs
|
||||||
|
|||||||
@@ -16,18 +16,25 @@ public interface BeforeInterceptor extends Handler<RoutingContext> {
|
|||||||
default Handler<RoutingContext> doHandle() {
|
default Handler<RoutingContext> doHandle() {
|
||||||
|
|
||||||
return ctx -> {
|
return ctx -> {
|
||||||
|
// 加同步锁
|
||||||
|
synchronized (BeforeInterceptor.class) {
|
||||||
ctx.put(IS_NEXT, false);
|
ctx.put(IS_NEXT, false);
|
||||||
BeforeInterceptor.this.handle(ctx);
|
BeforeInterceptor.this.handle(ctx);
|
||||||
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
|
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
|
||||||
sendError(ctx, 403);
|
sendError(ctx, 403);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
default void doNext(RoutingContext context) {
|
default void doNext(RoutingContext context) {
|
||||||
|
// 设置上下文状态为可以继续执行
|
||||||
|
// 添加同步锁保障多线程下执行时序
|
||||||
|
synchronized (BeforeInterceptor.class) {
|
||||||
context.put(IS_NEXT, true);
|
context.put(IS_NEXT, true);
|
||||||
context.next();
|
context.next();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void handle(RoutingContext context);
|
void handle(RoutingContext context);
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,10 @@ public class JsonResult<T> implements Serializable {
|
|||||||
|
|
||||||
private int code = SUCCESS_CODE;//状态码
|
private int code = SUCCESS_CODE;//状态码
|
||||||
|
|
||||||
private String msg = SUCCESS_MESSAGE;//消息
|
private String msg = SUCCESS_MESSAGE; //消息
|
||||||
|
|
||||||
private boolean success = true; //是否成功
|
private boolean success = true; //是否成功
|
||||||
|
|
||||||
private int count;
|
|
||||||
|
|
||||||
private T data;
|
private T data;
|
||||||
|
|
||||||
private long timestamp = System.currentTimeMillis(); //时间戳
|
private long timestamp = System.currentTimeMillis(); //时间戳
|
||||||
@@ -54,20 +52,6 @@ public class JsonResult<T> implements Serializable {
|
|||||||
this.success = success;
|
this.success = success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonResult(int code, String msg, boolean success, T data, int count) {
|
|
||||||
this(code, msg, success, data);
|
|
||||||
this.count = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCount() {
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonResult<T> setCount(int count) {
|
|
||||||
this.count = count;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCode() {
|
public int getCode() {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
@@ -136,20 +120,9 @@ public class JsonResult<T> implements Serializable {
|
|||||||
return new JsonResult<>(SUCCESS_CODE, msg, true, data);
|
return new JsonResult<>(SUCCESS_CODE, msg, true, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应成功消息和数据实体
|
|
||||||
public static <T> JsonResult<T> data(String msg, T data, int count) {
|
|
||||||
if (StringUtils.isEmpty(msg)) msg = SUCCESS_MESSAGE;
|
|
||||||
return new JsonResult<>(SUCCESS_CODE, msg, true, data, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应数据实体
|
// 响应数据实体
|
||||||
public static <T> JsonResult<T> data(T data) {
|
public static <T> JsonResult<T> data(T data) {
|
||||||
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data, 0);
|
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data);
|
||||||
}
|
|
||||||
|
|
||||||
// 响应数据实体
|
|
||||||
public static <T> JsonResult<T> data(T data, int count) {
|
|
||||||
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data, count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应成功消息
|
// 响应成功消息
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class CommonUtil {
|
|||||||
try {
|
try {
|
||||||
properties.load(CommonUtil.class.getClassLoader().getResourceAsStream("app.properties"));
|
properties.load(CommonUtil.class.getClassLoader().getResourceAsStream("app.properties"));
|
||||||
if (!properties.isEmpty()) {
|
if (!properties.isEmpty()) {
|
||||||
appVersion = properties.getProperty("app.version");
|
appVersion = properties.getProperty("app.version") + "build" + properties.getProperty("build");
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|||||||
@@ -26,10 +26,20 @@ public class ResponseUtil {
|
|||||||
.end(jsonObject.encode());
|
.end(jsonObject.encode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject) {
|
||||||
|
ctx.putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
.setStatusCode(200)
|
||||||
|
.end(jsonObject.encode());
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> void fireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult) {
|
public static <T> void fireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult) {
|
||||||
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
|
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> void fireJsonResultResponse(HttpServerResponse ctx, JsonResult<T> jsonResult) {
|
||||||
|
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
public static void fireTextResponse(RoutingContext ctx, String text) {
|
public static void fireTextResponse(RoutingContext ctx, String text) {
|
||||||
ctx.response().putHeader(CONTENT_TYPE, "text/html; charset=utf-8").end(text);
|
ctx.response().putHeader(CONTENT_TYPE, "text/html; charset=utf-8").end(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
app.version=${project.version}
|
app.version=${project.version}
|
||||||
|
build=${maven.build.timestamp}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<configuration>
|
|
||||||
|
|
||||||
<!-- 日志自定义颜色 -->
|
|
||||||
<!-- https://logback.qos.ch/manual/layouts.html#coloring -->
|
|
||||||
|
|
||||||
<!--日志文件主目录:这里${user.home}为当前服务器用户主目录-->
|
|
||||||
<property name="LOG_HOME" value="logs"/>
|
|
||||||
|
|
||||||
<property name="LOGBACK_DEFAULT" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
|
|
||||||
<property name="CUSTOMER_PATTERN2" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) -> %magenta([%15.15thread]) %cyan(%-40.40logger{39}) : %msg%n"/>
|
|
||||||
|
|
||||||
<!--配置日志文件(File)-->
|
|
||||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
|
||||||
<!--设置策略-->
|
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
|
||||||
<!--日志文件路径:这里%d{yyyyMMdd}表示按天分类日志-->
|
|
||||||
<FileNamePattern>${LOG_HOME}/%d{yyyyMMdd}/run.log</FileNamePattern>
|
|
||||||
<!--日志保留天数-->
|
|
||||||
<MaxHistory>15</MaxHistory>
|
|
||||||
</rollingPolicy>
|
|
||||||
<!--设置格式-->
|
|
||||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
|
||||||
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
|
|
||||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
|
||||||
<!-- 或者使用默认配置 -->
|
|
||||||
<!--<pattern>${FILE_LOG_PATTERN}</pattern>-->
|
|
||||||
<charset>utf8</charset>
|
|
||||||
</encoder>
|
|
||||||
<!--日志文件最大的大小-->
|
|
||||||
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
|
||||||
<MaxFileSize>100MB</MaxFileSize>
|
|
||||||
</triggeringPolicy>
|
|
||||||
</appender>
|
|
||||||
|
|
||||||
<!-- 将文件输出设置成异步输出 -->
|
|
||||||
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
|
|
||||||
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
|
|
||||||
<discardingThreshold>0</discardingThreshold>
|
|
||||||
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
|
|
||||||
<queueSize>256</queueSize>
|
|
||||||
<!-- 添加附加的appender,最多只能添加一个 -->
|
|
||||||
<appender-ref ref="FILE"/>
|
|
||||||
</appender>
|
|
||||||
|
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
|
||||||
<encoder>
|
|
||||||
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
|
|
||||||
<pattern>${CUSTOMER_PATTERN2}</pattern>
|
|
||||||
</encoder>
|
|
||||||
</appender>
|
|
||||||
<logger name="io.netty" level="warn"/>
|
|
||||||
<logger name="io.vertx" level="info"/>
|
|
||||||
<root level="debug">
|
|
||||||
<appender-ref ref="STDOUT"/>
|
|
||||||
<appender-ref ref="FILE"/>
|
|
||||||
</root>
|
|
||||||
</configuration>
|
|
||||||
10
note.txt
10
note.txt
@@ -60,7 +60,15 @@ jwt鉴权用户
|
|||||||
文件信息: 文件/文件夹, 文件数量, 文件大小, 文件类型; 链接信息: 解析次数, 缓存次数等)
|
文件信息: 文件/文件夹, 文件数量, 文件大小, 文件类型; 链接信息: 解析次数, 缓存次数等)
|
||||||
|
|
||||||
微服务设计:
|
微服务设计:
|
||||||
|
TODO
|
||||||
|
|
||||||
|
后台管理:
|
||||||
|
菜单:
|
||||||
|
网盘管理: token配置, 启用/禁用
|
||||||
|
短链管理: 短链列表, 新增, 删除
|
||||||
|
解析统计: 下载次数统计, 下载流量统计, 详细解析列表
|
||||||
|
状态监视: 服务请求并发数; 来源IP列表: 拉黑, 限制次数; Nginx
|
||||||
|
系统配置: 管理员账户, 系统参数: 域名配置, 预览URL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<artifactId>parser</artifactId>
|
<artifactId>parser</artifactId>
|
||||||
|
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>${project.groupId}:${project.artifactId}</name>
|
<name>cn.qaiu:parser</name>
|
||||||
<description>NFD parser</description>
|
<description>NFD parser</description>
|
||||||
<url>https://qaiu.top</url>
|
<url>https://qaiu.top</url>
|
||||||
|
|
||||||
|
|||||||
@@ -7,52 +7,75 @@ public class FileInfo {
|
|||||||
/**
|
/**
|
||||||
* 文件名
|
* 文件名
|
||||||
*/
|
*/
|
||||||
String fileName;
|
private String fileName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件ID
|
* 文件ID
|
||||||
*/
|
*/
|
||||||
String fileId;
|
private String fileId;
|
||||||
|
|
||||||
|
private String fileIcon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件大小(byte)
|
* 文件大小(byte)
|
||||||
*/
|
*/
|
||||||
Long size;
|
private Long size;
|
||||||
|
|
||||||
|
private String sizeStr;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIME类型
|
* 类型
|
||||||
*/
|
*/
|
||||||
String fileMIME;
|
private String fileType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件路径
|
* 文件路径
|
||||||
*/
|
*/
|
||||||
String filePath;
|
private String filePath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建(上传)时间 yyyy-MM-dd HH:mm:ss格式
|
* 创建(上传)时间 yyyy-MM-dd HH:mm:ss格式
|
||||||
*/
|
*/
|
||||||
String createTime;
|
private String createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上次修改时间
|
||||||
|
*/
|
||||||
|
private String updateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建者
|
* 创建者
|
||||||
*/
|
*/
|
||||||
String createBy;
|
private String createBy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件描述
|
* 文件描述
|
||||||
*/
|
*/
|
||||||
String description;
|
private String description;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载次数
|
* 下载次数
|
||||||
*/
|
*/
|
||||||
Integer downloadCount;
|
private Integer downloadCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网盘标识
|
||||||
|
*/
|
||||||
|
private String panType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* nfd下载链接(可能获取不到)
|
||||||
|
* note: 不是下载直链
|
||||||
|
*/
|
||||||
|
private String parserUrl;
|
||||||
|
|
||||||
|
//预览地址
|
||||||
|
private String previewUrl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扩展参数
|
* 扩展参数
|
||||||
*/
|
*/
|
||||||
Map<String, Object> extParameters;
|
private Map<String, Object> extParameters;
|
||||||
|
|
||||||
public String getFileName() {
|
public String getFileName() {
|
||||||
return fileName;
|
return fileName;
|
||||||
@@ -72,6 +95,15 @@ public class FileInfo {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFileIcon() {
|
||||||
|
return fileIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfo setFileIcon(String fileIcon) {
|
||||||
|
this.fileIcon = fileIcon;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getSize() {
|
public Long getSize() {
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
@@ -81,12 +113,21 @@ public class FileInfo {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFileMIME() {
|
public String getSizeStr() {
|
||||||
return fileMIME;
|
return sizeStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileInfo setFileMIME(String fileMIME) {
|
public FileInfo setSizeStr(String sizeStr) {
|
||||||
this.fileMIME = fileMIME;
|
this.sizeStr = sizeStr;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileType() {
|
||||||
|
return fileType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfo setFileType(String fileType) {
|
||||||
|
this.fileType = fileType;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +149,15 @@ public class FileInfo {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUpdateTime() {
|
||||||
|
return updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfo setUpdateTime(String updateTime) {
|
||||||
|
this.updateTime = updateTime;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public String getCreateBy() {
|
public String getCreateBy() {
|
||||||
return createBy;
|
return createBy;
|
||||||
}
|
}
|
||||||
@@ -135,6 +185,31 @@ public class FileInfo {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPanType() {
|
||||||
|
return panType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfo setPanType(String panType) {
|
||||||
|
this.panType = panType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getParserUrl() {
|
||||||
|
return parserUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileInfo setParserUrl(String parserUrl) {
|
||||||
|
this.parserUrl = parserUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public String getPreviewUrl() {
|
||||||
|
return previewUrl;
|
||||||
|
}
|
||||||
|
public FileInfo setPreviewUrl(String previewUrl) {
|
||||||
|
this.previewUrl = previewUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Object> getExtParameters() {
|
public Map<String, Object> getExtParameters() {
|
||||||
return extParameters;
|
return extParameters;
|
||||||
}
|
}
|
||||||
@@ -143,4 +218,21 @@ public class FileInfo {
|
|||||||
this.extParameters = extParameters;
|
this.extParameters = extParameters;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "FileInfo{" +
|
||||||
|
"fileName='" + fileName + '\'' +
|
||||||
|
", fileId='" + fileId + '\'' +
|
||||||
|
", size=" + size +
|
||||||
|
", fileType='" + fileType + '\'' +
|
||||||
|
", filePath='" + filePath + '\'' +
|
||||||
|
", createTime='" + createTime + '\'' +
|
||||||
|
", updateTime='" + updateTime + '\'' +
|
||||||
|
", createBy='" + createBy + '\'' +
|
||||||
|
", description='" + description + '\'' +
|
||||||
|
", downloadCount=" + downloadCount +
|
||||||
|
", extParameters=" + extParameters +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ public class ShareLinkInfo {
|
|||||||
private String shareUrl; // 原始分享链接
|
private String shareUrl; // 原始分享链接
|
||||||
private String standardUrl; // 规范化的标准链接
|
private String standardUrl; // 规范化的标准链接
|
||||||
|
|
||||||
private Map<String, Object> otherParam; // 其他参数
|
/**
|
||||||
|
* 其他参数预定义
|
||||||
|
* dirId: 目录ID 传入
|
||||||
|
* auths: 认证相关 传入
|
||||||
|
* UA: 浏览器请求头 传入
|
||||||
|
* fileInfo: 解析成功的文件信息对象 传出
|
||||||
|
*/
|
||||||
|
private Map<String, Object> otherParam;
|
||||||
|
|
||||||
private ShareLinkInfo(Builder builder) {
|
private ShareLinkInfo(Builder builder) {
|
||||||
this.shareKey = builder.shareKey;
|
this.shareKey = builder.shareKey;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package cn.qaiu.parser;//package cn.qaiu.lz.common.parser;
|
package cn.qaiu.parser;//package cn.qaiu.lz.common.parser;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface IPanTool {
|
public interface IPanTool {
|
||||||
Future<String> parse();
|
Future<String> parse();
|
||||||
@@ -8,4 +12,24 @@ public interface IPanTool {
|
|||||||
default String parseSync() {
|
default String parseSync() {
|
||||||
return parse().toCompletionStage().toCompletableFuture().join();
|
return parse().toCompletionStage().toCompletableFuture().join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析文件列表
|
||||||
|
* @return List
|
||||||
|
*/
|
||||||
|
default Future<List<FileInfo>> parseFileList() {
|
||||||
|
Promise<List<FileInfo>> promise = Promise.promise();
|
||||||
|
promise.fail("Not implemented yet");
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件ID获取下载链接
|
||||||
|
* @return url
|
||||||
|
*/
|
||||||
|
default Future<String> parseById() {
|
||||||
|
Promise<String> promise = Promise.promise();
|
||||||
|
promise.complete("Not implemented yet");
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import cn.qaiu.entity.ShareLinkInfo;
|
|||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Handler;
|
import io.vertx.core.Handler;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.json.DecodeException;
|
import io.vertx.core.buffer.Buffer;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.core.net.ProxyOptions;
|
import io.vertx.core.net.ProxyOptions;
|
||||||
import io.vertx.core.net.ProxyType;
|
import io.vertx.core.net.ProxyType;
|
||||||
@@ -17,8 +17,14 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析器抽象类包含promise, HTTP Client, 默认失败方法等;
|
* 解析器抽象类包含promise, HTTP Client, 默认失败方法等;
|
||||||
@@ -92,6 +98,14 @@ public abstract class PanBase implements IPanTool {
|
|||||||
protected PanBase() {
|
protected PanBase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String baseMsg() {
|
||||||
|
if (shareLinkInfo.getShareUrl() != null) {
|
||||||
|
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": url=" + shareLinkInfo.getShareUrl();
|
||||||
|
}
|
||||||
|
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": key=" + shareLinkInfo.getShareKey() +
|
||||||
|
";pwd=" + shareLinkInfo.getSharePassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 失败时生成异常消息
|
* 失败时生成异常消息
|
||||||
@@ -102,13 +116,22 @@ public abstract class PanBase implements IPanTool {
|
|||||||
*/
|
*/
|
||||||
protected void fail(Throwable t, String errorMsg, Object... args) {
|
protected void fail(Throwable t, String errorMsg, Object... args) {
|
||||||
try {
|
try {
|
||||||
|
// 判断是否已经完成
|
||||||
|
if (promise.future().isComplete()) {
|
||||||
|
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
|
||||||
|
return;
|
||||||
|
}
|
||||||
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
|
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
|
||||||
log.error("解析异常: " + s, t.fillInStackTrace());
|
log.error("解析异常: " + s, t.fillInStackTrace());
|
||||||
promise.fail(shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": 解析异常: " + s + " -> " + t);
|
promise.fail(baseMsg() + ": 解析异常: " + s + " -> " + t);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("ErrorMsg format fail. The parameter has been discarded", e);
|
log.error("ErrorMsg format fail. The parameter has been discarded", e);
|
||||||
log.error("解析异常: " + errorMsg, t.fillInStackTrace());
|
log.error("解析异常: " + errorMsg, t.fillInStackTrace());
|
||||||
promise.fail(shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": 解析异常: " + errorMsg + " -> " + t);
|
if (promise.future().isComplete()) {
|
||||||
|
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
promise.fail(baseMsg() + ": 解析异常: " + errorMsg + " -> " + t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,11 +143,20 @@ public abstract class PanBase implements IPanTool {
|
|||||||
*/
|
*/
|
||||||
protected void fail(String errorMsg, Object... args) {
|
protected void fail(String errorMsg, Object... args) {
|
||||||
try {
|
try {
|
||||||
|
// 判断是否已经完成
|
||||||
|
if (promise.future().isComplete()) {
|
||||||
|
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
|
||||||
|
return;
|
||||||
|
}
|
||||||
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
|
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
|
||||||
promise.fail(shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + " - 解析异常: " + s);
|
promise.fail(baseMsg() + " - 解析异常: " + s);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
if (promise.future().isComplete()) {
|
||||||
|
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.error("ErrorMsg format fail. The parameter has been discarded", e);
|
log.error("ErrorMsg format fail. The parameter has been discarded", e);
|
||||||
promise.fail(shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + " - 解析异常: " + errorMsg);
|
promise.fail(baseMsg() + " - 解析异常: " + errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +171,7 @@ public abstract class PanBase implements IPanTool {
|
|||||||
* @return Handler
|
* @return Handler
|
||||||
*/
|
*/
|
||||||
protected Handler<Throwable> handleFail(String errorMsg) {
|
protected Handler<Throwable> handleFail(String errorMsg) {
|
||||||
return t -> fail(shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + " - 请求异常 {}: -> {}", errorMsg, t.fillInStackTrace());
|
return t -> fail(baseMsg() + " - 请求异常 {}: -> {}", errorMsg, t.fillInStackTrace());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Handler<Throwable> handleFail() {
|
protected Handler<Throwable> handleFail() {
|
||||||
@@ -149,16 +181,62 @@ public abstract class PanBase implements IPanTool {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* bodyAsJsonObject的封装, 会自动处理异常
|
* bodyAsJsonObject的封装, 会自动处理异常
|
||||||
|
*
|
||||||
* @param res HttpResponse
|
* @param res HttpResponse
|
||||||
* @return JsonObject
|
* @return JsonObject
|
||||||
*/
|
*/
|
||||||
protected JsonObject asJson(HttpResponse<?> res) {
|
protected JsonObject asJson(HttpResponse<?> res) {
|
||||||
|
// 检查响应头中的Content-Encoding是否为gzip
|
||||||
|
String contentEncoding = res.getHeader("Content-Encoding");
|
||||||
try {
|
try {
|
||||||
|
if ("gzip".equalsIgnoreCase(contentEncoding)) {
|
||||||
|
// 如果是gzip压缩的响应体,解压
|
||||||
|
return new JsonObject(decompressGzip((Buffer) res.body()));
|
||||||
|
} else {
|
||||||
return res.bodyAsJsonObject();
|
return res.bodyAsJsonObject();
|
||||||
} catch (DecodeException e) {
|
|
||||||
fail("解析失败: json格式异常: {}", res.bodyAsString());
|
|
||||||
throw new RuntimeException("解析失败: json格式异常");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
if ("gzip".equalsIgnoreCase(contentEncoding)) {
|
||||||
|
// 如果是gzip压缩的响应体,解压
|
||||||
|
try {
|
||||||
|
log.error(decompressGzip((Buffer) res.body()));
|
||||||
|
fail(decompressGzip((Buffer) res.body()));
|
||||||
|
//throw new RuntimeException("响应不是JSON格式");
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("响应gzip解压失败");
|
||||||
|
fail("响应gzip解压失败: {}", ex.getMessage());
|
||||||
|
//throw new RuntimeException("响应gzip解压失败", ex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("解析失败: json格式异常: {}", res.bodyAsString());
|
||||||
|
fail("解析失败: json格式异常: {}", res.bodyAsString());
|
||||||
|
//throw new RuntimeException("解析失败: json格式异常");
|
||||||
|
}
|
||||||
|
return JsonObject.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* body To text的封装, 会自动处理异常, 会自动解压gzip
|
||||||
|
* @param res HttpResponse
|
||||||
|
* @return String
|
||||||
|
*/
|
||||||
|
protected String asText(HttpResponse<?> res) {
|
||||||
|
// 检查响应头中的Content-Encoding是否为gzip
|
||||||
|
String contentEncoding = res.getHeader("Content-Encoding");
|
||||||
|
try {
|
||||||
|
if ("gzip".equalsIgnoreCase(contentEncoding)) {
|
||||||
|
// 如果是gzip压缩的响应体,解压
|
||||||
|
return decompressGzip((Buffer) res.body());
|
||||||
|
} else {
|
||||||
|
return res.bodyAsString();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail("解析失败: res格式异常");
|
||||||
|
//throw new RuntimeException("解析失败: res格式异常");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void complete(String url) {
|
protected void complete(String url) {
|
||||||
@@ -191,4 +269,35 @@ public abstract class PanBase implements IPanTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压gzip数据
|
||||||
|
* @param compressedData compressedData
|
||||||
|
* @return String
|
||||||
|
* @throws IOException IOException
|
||||||
|
*/
|
||||||
|
private String decompressGzip(Buffer compressedData) throws IOException {
|
||||||
|
try (ByteArrayInputStream bais = new ByteArrayInputStream(compressedData.getBytes());
|
||||||
|
GZIPInputStream gzis = new GZIPInputStream(bais);
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(gzis,
|
||||||
|
StandardCharsets.UTF_8))) {
|
||||||
|
|
||||||
|
// 用于存储解压后的字符串
|
||||||
|
StringBuilder decompressedData = new StringBuilder();
|
||||||
|
|
||||||
|
// 逐行读取解压后的数据
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
decompressedData.append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 此时decompressedData.toString()包含了解压后的字符串
|
||||||
|
return decompressedData.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getDomainName(){
|
||||||
|
return shareLinkInfo.getOtherParam().getOrDefault("domainName", "").toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,82 @@ public enum PanDomainTemplate {
|
|||||||
|
|
||||||
|
|
||||||
// 网盘定义
|
// 网盘定义
|
||||||
|
/*
|
||||||
|
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("蓝奏云",
|
LZ("蓝奏云",
|
||||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?lanzou[a-z]\\.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|" +
|
||||||
|
"ilanzou|" +
|
||||||
|
"lanzoui|" +
|
||||||
|
"lanzoug|" +
|
||||||
|
"lanzoum" +
|
||||||
|
")\\.com/(.+/)?(?<KEY>.+)"),
|
||||||
"https://lanzoux.com/{shareKey}",
|
"https://lanzoux.com/{shareKey}",
|
||||||
LzTool.class),
|
LzTool.class),
|
||||||
|
|
||||||
@@ -59,14 +133,74 @@ public enum PanDomainTemplate {
|
|||||||
"https://iwx.mail.qq.com/ftn/download/{shareKey}",
|
"https://iwx.mail.qq.com/ftn/download/{shareKey}",
|
||||||
"https://mail.qq.com",
|
"https://mail.qq.com",
|
||||||
QQTool.class),
|
QQTool.class),
|
||||||
|
// https://wx.mail.qq.com/s?k=uAG9JR42Rqgt010mFp
|
||||||
|
QQW("QQ邮箱云盘",
|
||||||
|
compile("https://i?wx\\.mail\\.qq\\.com/s\\?k=(?<KEY>.+)"),
|
||||||
|
"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/
|
// https://f.ws59.cn/f/或者https://www.wenshushu.cn/f/
|
||||||
WS("文叔叔",
|
WS("文叔叔",
|
||||||
compile("https://(f\\.ws(\\d{2})\\.cn|www\\.wenshushu\\.cn)/f/(?<KEY>.+)"),
|
compile("https://(f\\.ws(\\d{2})\\.cn|www\\.wenshushu\\.cn)/f/(?<KEY>.+)"),
|
||||||
"https://www.wenshushu.cn/f/{shareKey}",
|
"https://www.wenshushu.cn/f/{shareKey}",
|
||||||
WsTool.class),
|
WsTool.class),
|
||||||
// https://www.123pan.com/s/
|
// 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网盘",
|
YE("123网盘",
|
||||||
compile("https://www\\.(123pan|123865|123684)\\.com/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}",
|
"https://www.123pan.com/s/{shareKey}",
|
||||||
YeTool.class),
|
YeTool.class),
|
||||||
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
|
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
|
||||||
@@ -81,7 +215,8 @@ public enum PanDomainTemplate {
|
|||||||
"https://cowtransfer.com/s/{shareKey}",
|
"https://cowtransfer.com/s/{shareKey}",
|
||||||
CowTool.class),
|
CowTool.class),
|
||||||
CT("城通网盘",
|
CT("城通网盘",
|
||||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/file/(?<KEY>.+)"),
|
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" +
|
||||||
|
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
|
||||||
"https://474b.com/file/{shareKey}",
|
"https://474b.com/file/{shareKey}",
|
||||||
CtTool.class),
|
CtTool.class),
|
||||||
// https://xxx.118pan.com/bxxx
|
// https://xxx.118pan.com/bxxx
|
||||||
@@ -96,6 +231,7 @@ public enum PanDomainTemplate {
|
|||||||
"https://www.vyuyun.com/s/{shareKey}?password={pwd}",
|
"https://www.vyuyun.com/s/{shareKey}?password={pwd}",
|
||||||
PvyyTool.class),
|
PvyyTool.class),
|
||||||
// https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp
|
// https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp
|
||||||
|
// https://1drv.ms/u/c/abfd0a26e47d3458/EdYACWvPq85Et797YmvL5LgBruUKoNxqIFATXhIv1PI2_Q?e=z4ffNJ
|
||||||
POD("OneDrive",
|
POD("OneDrive",
|
||||||
compile("https://1drv\\.ms/(?<KEY>.+)"),
|
compile("https://1drv\\.ms/(?<KEY>.+)"),
|
||||||
"https://1drv\\.ms/{shareKey}",
|
"https://1drv\\.ms/{shareKey}",
|
||||||
@@ -117,18 +253,29 @@ public enum PanDomainTemplate {
|
|||||||
"https://www.dropbox.com/scl/fi/{shareKey}/?rlkey={pwd}&dl=0",
|
"https://www.dropbox.com/scl/fi/{shareKey}/?rlkey={pwd}&dl=0",
|
||||||
PdbTool.class),
|
PdbTool.class),
|
||||||
P115("115网盘",
|
P115("115网盘",
|
||||||
compile("https://(115|anxia).com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?"),
|
compile("https://(115|anxia).com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?([&#].*)?"),
|
||||||
"https://115.com/s/{shareKey}?password={pwd}",
|
"https://115.com/s/{shareKey}?password={pwd}",
|
||||||
P115Tool.class),
|
P115Tool.class),
|
||||||
|
// 链接:https://www.yunpan.com/surl_yD7wz4VgU9v(提取码:fc70)
|
||||||
|
// P360("360云盘(需要referer头)",
|
||||||
|
// compile("https://www\\.yunpan\\.com/(?<KEY>\\w+)"),
|
||||||
|
// "https://www.yunpan.com/{shareKey}",
|
||||||
|
// P360Tool.class),
|
||||||
|
|
||||||
|
// https://pan-yz.cldisk.com/external/m/file/953658049102462976
|
||||||
|
Pcx("超星云盘(需要referer头)",
|
||||||
|
compile("https://pan-yz\\.cldisk\\.com/external/m/file/(?<KEY>\\w+)"),
|
||||||
|
"https://pan-yz.cldisk.com/external/m/file/{shareKey}",
|
||||||
|
PcxTool.class),
|
||||||
// =====================音乐类解析 分享链接标志->MxxS (单歌曲/普通音质)==========================
|
// =====================音乐类解析 分享链接标志->MxxS (单歌曲/普通音质)==========================
|
||||||
// http://163cn.tv/xxx
|
// http://163cn.tv/xxx
|
||||||
MNES("网易云音乐分享",
|
MNES("网易云音乐分享",
|
||||||
compile("http(s)?://163cn\\.tv/(?<KEY>.+)"),
|
compile("http(s)?://163cn\\.tv/(?<KEY>.+)"),
|
||||||
"http://163cn.tv/{shareKey}",
|
"https://163cn.tv/{shareKey}",
|
||||||
MnesTool.class),
|
MnesTool.class),
|
||||||
// https://music.163.com/#/song?id=xxx
|
// https://music.163.com/#/song?id=xxx
|
||||||
MNE("网易云音乐歌曲详情",
|
MNE("网易云音乐歌曲详情",
|
||||||
compile("https://music\\.163\\.com/(#/)?song\\?id=(?<KEY>.+)"),
|
compile("https://(y.)?music\\.163\\.com/(#|m/)?song\\?id=(?<KEY>.+)(&.*)?"),
|
||||||
"https://music.163.com/#/song?id={shareKey}",
|
"https://music.163.com/#/song?id={shareKey}",
|
||||||
MnesTool.MneTool.class),
|
MnesTool.MneTool.class),
|
||||||
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
|
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
|
||||||
@@ -167,7 +314,7 @@ public enum PanDomainTemplate {
|
|||||||
MMGS("咪咕音乐分享",
|
MMGS("咪咕音乐分享",
|
||||||
compile("https://music\\.migu\\.cn/v3/music/song/(?<KEY>.+)(\\?.*)?"),
|
compile("https://music\\.migu\\.cn/v3/music/song/(?<KEY>.+)(\\?.*)?"),
|
||||||
"https://music.migu.cn/v3/music/song/{shareKey}",
|
"https://music.migu.cn/v3/music/song/{shareKey}",
|
||||||
MkwTool.class),
|
MmgTool.class),
|
||||||
// =====================私有盘解析==========================
|
// =====================私有盘解析==========================
|
||||||
|
|
||||||
// Cloudreve自定义域名解析, 解析器CeTool兜底策略, 即任意域名如果匹配不到对应的规则, 则由CeTool统一处理,
|
// Cloudreve自定义域名解析, 解析器CeTool兜底策略, 即任意域名如果匹配不到对应的规则, 则由CeTool统一处理,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package cn.qaiu.parser;
|
|||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
import static cn.qaiu.parser.PanDomainTemplate.KEY;
|
import static cn.qaiu.parser.PanDomainTemplate.KEY;
|
||||||
@@ -20,9 +22,12 @@ public class ParserCreate {
|
|||||||
private final PanDomainTemplate panDomainTemplate;
|
private final PanDomainTemplate panDomainTemplate;
|
||||||
private final ShareLinkInfo shareLinkInfo;
|
private final ShareLinkInfo shareLinkInfo;
|
||||||
|
|
||||||
|
private String standardUrl;
|
||||||
|
|
||||||
public ParserCreate(PanDomainTemplate panDomainTemplate, ShareLinkInfo shareLinkInfo) {
|
public ParserCreate(PanDomainTemplate panDomainTemplate, ShareLinkInfo shareLinkInfo) {
|
||||||
this.panDomainTemplate = panDomainTemplate;
|
this.panDomainTemplate = panDomainTemplate;
|
||||||
this.shareLinkInfo = shareLinkInfo;
|
this.shareLinkInfo = shareLinkInfo;
|
||||||
|
this.standardUrl = panDomainTemplate.getStandardUrlTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -38,17 +43,19 @@ public class ParserCreate {
|
|||||||
}
|
}
|
||||||
Matcher matcher = this.panDomainTemplate.getPattern().matcher(shareUrl);
|
Matcher matcher = this.panDomainTemplate.getPattern().matcher(shareUrl);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
String shareKey = matcher.group(KEY);
|
String k0 = matcher.group(KEY);
|
||||||
|
String shareKey = URLEncoder.encode(k0, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// 返回规范化的标准链接
|
// 返回规范化的标准链接
|
||||||
String standardUrl = getStandardUrlTemplate()
|
standardUrl = getStandardUrlTemplate()
|
||||||
.replace("{shareKey}", shareKey);
|
.replace("{shareKey}", k0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String pwd = matcher.group(PWD);
|
String pwd = matcher.group(PWD);
|
||||||
if (StringUtils.isNotEmpty(pwd)) {
|
if (StringUtils.isNotEmpty(pwd)) {
|
||||||
shareLinkInfo.setSharePassword(pwd);
|
shareLinkInfo.setSharePassword(pwd);
|
||||||
}
|
}
|
||||||
standardUrl = standardUrl .replace("{pwd}", pwd);
|
standardUrl = standardUrl.replace("{pwd}", pwd);
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
shareLinkInfo.setShareUrl(shareUrl);
|
shareLinkInfo.setShareUrl(shareUrl);
|
||||||
@@ -88,7 +95,11 @@ public class ParserCreate {
|
|||||||
shareLinkInfo.setShareUrl(standardUrl);
|
shareLinkInfo.setShareUrl(standardUrl);
|
||||||
} else {
|
} else {
|
||||||
shareLinkInfo.setShareKey(shareKey);
|
shareLinkInfo.setShareKey(shareKey);
|
||||||
shareLinkInfo.setStandardUrl(panDomainTemplate.getStandardUrlTemplate().replace("{shareKey}", shareKey));
|
standardUrl = standardUrl.replace("{shareKey}", shareKey);
|
||||||
|
shareLinkInfo.setStandardUrl(standardUrl);
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(shareLinkInfo.getShareUrl())) {
|
||||||
|
shareLinkInfo.setShareUrl(standardUrl);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -111,6 +122,11 @@ public class ParserCreate {
|
|||||||
public ParserCreate setShareLinkInfoPwd(String pwd) {
|
public ParserCreate setShareLinkInfoPwd(String pwd) {
|
||||||
if (pwd != null) {
|
if (pwd != null) {
|
||||||
shareLinkInfo.setSharePassword(pwd);
|
shareLinkInfo.setSharePassword(pwd);
|
||||||
|
standardUrl = standardUrl.replace("{pwd}", pwd);
|
||||||
|
shareLinkInfo.setStandardUrl(standardUrl);
|
||||||
|
if (shareLinkInfo.getShareUrl().contains("{pwd}")) {
|
||||||
|
shareLinkInfo.setShareUrl(standardUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,25 @@ import io.vertx.core.json.JsonObject;
|
|||||||
import io.vertx.ext.web.client.HttpRequest;
|
import io.vertx.ext.web.client.HttpRequest;
|
||||||
import io.vertx.uritemplate.UriTemplate;
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <a href="https://www.ctfile.com">诚通网盘</a>
|
* <a href="https://www.ctfile.com">诚通网盘</a>
|
||||||
*/
|
*/
|
||||||
public class CtTool extends PanBase {
|
public class CtTool extends PanBase {
|
||||||
private static final String API_URL_PREFIX = "https://webapi.ctfile.com";
|
private static final String API_URL_PREFIX = "https://webapi.ctfile.com";
|
||||||
|
|
||||||
private static final String API1 = API_URL_PREFIX + "/getfile.php?path=file" +
|
// https://webapi.ctfile.com/getfile.php?path=f&f=55050874-1246660795-6464f6&
|
||||||
|
// passcode=7548&token=30wiijxs1fzhb6brw0p9m6&r=0.5885881231735761&
|
||||||
|
// ref=&url=https%3A%2F%2F474b.com%2Ff%2F55050874-1246660795-6464f6%3Fp%3D7548
|
||||||
|
private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" +
|
||||||
"&f={shareKey}&passcode={pwd}&token={token}&r={rand}&ref=";
|
"&f={shareKey}&passcode={pwd}&token={token}&r={rand}&ref=";
|
||||||
|
|
||||||
|
//https://webapi.ctfile.com/get_file_url.php?uid=55050874&fid=1246660795&folder_id=0&
|
||||||
|
// file_chk=054bc20461f5c63ff82015b9e69fb7fc&mb=1&token=30wiijxs1fzhb6brw0p9m6&app=0&
|
||||||
|
// acheck=1&verifycode=&rd=0.965929071503574
|
||||||
private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" +
|
private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" +
|
||||||
"uid={uid}&fid={fid}&folder_id=0&file_chk={file_chk}&mb=0&token={token}&app=0&acheck=0&verifycode=" +
|
"uid={uid}&fid={fid}&folder_id=0&file_chk={file_chk}&mb=0&token={token}&app=0&acheck=0&verifycode=" +
|
||||||
"&rd={rand}";
|
"&rd={rand}";
|
||||||
@@ -49,8 +59,13 @@ public class CtTool extends PanBase {
|
|||||||
String[] split = shareKey.split("-");
|
String[] split = shareKey.split("-");
|
||||||
String uid = split[0], fid = split[1];
|
String uid = split[0], fid = split[1];
|
||||||
String token = RandomStringGenerator.generateRandomString();
|
String token = RandomStringGenerator.generateRandomString();
|
||||||
|
// 获取url path
|
||||||
|
int i1 = shareLinkInfo.getShareUrl().indexOf("com/");
|
||||||
|
int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/");
|
||||||
|
String path = shareLinkInfo.getShareUrl().substring(i1 + 4, i2);
|
||||||
|
|
||||||
HttpRequest<Buffer> bufferHttpRequest1 = clientSession.getAbs(UriTemplate.of(API1))
|
HttpRequest<Buffer> bufferHttpRequest1 = clientSession.getAbs(UriTemplate.of(API1))
|
||||||
|
.setTemplateParam("path", path)
|
||||||
.setTemplateParam("shareKey", shareKey)
|
.setTemplateParam("shareKey", shareKey)
|
||||||
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
||||||
.setTemplateParam("token", token)
|
.setTemplateParam("token", token)
|
||||||
@@ -79,10 +94,10 @@ public class CtTool extends PanBase {
|
|||||||
}
|
}
|
||||||
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
|
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
|
||||||
} else {
|
} else {
|
||||||
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson, "file_chk");
|
fail("解析失败, file_chk找不到, 可能分享已失效或者分享密码不对: {}", fileJson);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson, "file");
|
fail("解析失败, 文件信息为空, 可能分享已失效");
|
||||||
}
|
}
|
||||||
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
|
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
|
||||||
return promise.future();
|
return promise.future();
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
package cn.qaiu.parser.impl;
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.parser.PanBase;
|
import cn.qaiu.parser.PanBase;
|
||||||
import cn.qaiu.util.AESUtils;
|
import cn.qaiu.util.AESUtils;
|
||||||
|
import cn.qaiu.util.FileSizeConverter;
|
||||||
import cn.qaiu.util.UUIDUtil;
|
import cn.qaiu.util.UUIDUtil;
|
||||||
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.buffer.Buffer;
|
import io.vertx.core.buffer.Buffer;
|
||||||
|
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.HttpRequest;
|
||||||
import io.vertx.uritemplate.UriTemplate;
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.util.ArrayList;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.util.Base64;
|
||||||
import java.io.IOException;
|
import java.util.List;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.zip.GZIPInputStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小飞机网盘
|
* 小飞机网盘
|
||||||
@@ -42,8 +44,19 @@ public class FjTool extends PanBase {
|
|||||||
"={uuid}&extra=2×tamp={ts}";
|
"={uuid}&extra=2×tamp={ts}";
|
||||||
// https://api.feijipan.com/ws/buy/vip/list?devType=6&devModel=Chrome&uuid=WQAl5yBy1naGudJEILBvE&extra=2×tamp=E2C53155F6D09417A27981561134CB73
|
// https://api.feijipan.com/ws/buy/vip/list?devType=6&devModel=Chrome&uuid=WQAl5yBy1naGudJEILBvE&extra=2×tamp=E2C53155F6D09417A27981561134CB73
|
||||||
|
|
||||||
|
// https://api.feijipan.com/ws/share/list?devType=6&devModel=Chrome&uuid=pwRWqwbk1J-KMTlRZowrn&extra=2×tamp=C5F8A68C53121AB21FA35BA3529E8758&shareId=fmAuOh3m&folderId=28986333&offset=1&limit=60
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
private static final MultiMap header;
|
private static final MultiMap header;
|
||||||
|
|
||||||
|
|
||||||
|
long nowTs = System.currentTimeMillis();
|
||||||
|
String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs));
|
||||||
|
String uuid = UUIDUtil.fjUuid(); // 也可以使用 UUID.randomUUID().toString()
|
||||||
|
|
||||||
static {
|
static {
|
||||||
header = MultiMap.caseInsensitiveMultiMap();
|
header = MultiMap.caseInsensitiveMultiMap();
|
||||||
header.set("Accept", "application/json, text/plain, */*");
|
header.set("Accept", "application/json, text/plain, */*");
|
||||||
@@ -71,67 +84,54 @@ public class FjTool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
final String dataKey = shareLinkInfo.getShareKey();
|
|
||||||
|
|
||||||
// 240530 此处shareId又改为了原始的shareId
|
// 240530 此处shareId又改为了原始的shareId
|
||||||
String shareId = dataKey; // String.valueOf(AESUtils.idEncrypt(dataKey));
|
// String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||||
long nowTs = System.currentTimeMillis();
|
final String shareId = shareLinkInfo.getShareKey();
|
||||||
String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs));
|
|
||||||
String uuid = UUIDUtil.fjUuid(); // 也可以使用 UUID.randomUUID().toString()
|
|
||||||
|
|
||||||
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
|
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
|
||||||
|
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
|
||||||
|
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
|
||||||
|
|
||||||
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
|
client.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
|
||||||
|
|
||||||
// long nowTs0 = System.currentTimeMillis();
|
|
||||||
String tsEncode0 = AESUtils.encrypt2Hex(Long.toString(nowTs));
|
|
||||||
// 第一次请求 获取文件信息
|
// 第一次请求 获取文件信息
|
||||||
// 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
|
||||||
client.postAbs(UriTemplate.of(FIRST_REQUEST_URL))
|
client.postAbs(UriTemplate.of(url))
|
||||||
.putHeaders(header)
|
.putHeaders(header)
|
||||||
.setTemplateParam("shareId", shareId)
|
.setTemplateParam("shareId", shareId)
|
||||||
.setTemplateParam("uuid", uuid)
|
.setTemplateParam("uuid", uuid)
|
||||||
.setTemplateParam("ts", tsEncode)
|
.setTemplateParam("ts", tsEncode)
|
||||||
.send().onSuccess(res -> {
|
.send().onSuccess(res -> {
|
||||||
// 处理GZ压缩
|
JsonObject resJson = asJson(res);
|
||||||
// 使用GZIPInputStream来解压数据
|
|
||||||
String decompressedString;
|
|
||||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(res.body().getBytes());
|
|
||||||
GZIPInputStream gzis = new GZIPInputStream(bais);
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(gzis, StandardCharsets.UTF_8))) {
|
|
||||||
|
|
||||||
// 用于存储解压后的字符串
|
|
||||||
StringBuilder decompressedData = new StringBuilder();
|
|
||||||
|
|
||||||
// 逐行读取解压后的数据
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
decompressedData.append(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 此时decompressedData.toString()包含了解压后的字符串
|
|
||||||
decompressedString = decompressedData.toString();
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
// 处理可能的IO异常
|
|
||||||
fail(FIRST_REQUEST_URL + " 响应异常");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 处理GZ压缩结束
|
|
||||||
|
|
||||||
JsonObject resJson = new JsonObject(decompressedString);
|
|
||||||
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").size() == 0) {
|
if (resJson.getJsonArray("list").isEmpty()) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
|
||||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 文件Id
|
// 文件Id
|
||||||
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
String fileId = fileInfo.getString("fileIds");
|
String fileId = fileInfo.getString("fileIds");
|
||||||
String userId = fileInfo.getString("userId");
|
String userId = fileInfo.getString("userId");
|
||||||
// 其他参数
|
// 其他参数
|
||||||
@@ -148,8 +148,8 @@ public class FjTool extends PanBase {
|
|||||||
.setTemplateParam("uuid", uuid)
|
.setTemplateParam("uuid", uuid)
|
||||||
.setTemplateParam("ts", tsEncode2)
|
.setTemplateParam("ts", tsEncode2)
|
||||||
.setTemplateParam("auth", auth)
|
.setTemplateParam("auth", auth)
|
||||||
.setTemplateParam("dataKey", dataKey);
|
.setTemplateParam("dataKey", shareId);
|
||||||
System.out.println(httpRequest.toString());
|
// System.out.println(httpRequest.toString());
|
||||||
httpRequest.send().onSuccess(res2 -> {
|
httpRequest.send().onSuccess(res2 -> {
|
||||||
MultiMap headers = res2.headers();
|
MultiMap headers = res2.headers();
|
||||||
if (!headers.contains("Location")) {
|
if (!headers.contains("Location")) {
|
||||||
@@ -164,4 +164,145 @@ public class FjTool extends PanBase {
|
|||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 -> {
|
||||||
|
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
|
||||||
|
parserDir(id, shareId, promise);
|
||||||
|
} else {
|
||||||
|
promise.fail("解析目录ID失败");
|
||||||
|
}
|
||||||
|
}).onFailure(failRes -> {
|
||||||
|
log.error("解析目录失败: {}", failRes.getMessage());
|
||||||
|
promise.fail(failRes);
|
||||||
|
});
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
|
||||||
|
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
|
||||||
|
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
|
||||||
|
// 拿到目录ID
|
||||||
|
client.postAbs(UriTemplate.of(FILE_LIST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.setTemplateParam("folderId", id)
|
||||||
|
.send().onSuccess(res -> {
|
||||||
|
JsonObject jsonObject;
|
||||||
|
try {
|
||||||
|
jsonObject = asJson(res);
|
||||||
|
} catch (Exception e) {
|
||||||
|
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// System.out.println(jsonObject.encodePrettily());
|
||||||
|
JsonArray list = jsonObject.getJsonArray("list");
|
||||||
|
ArrayList<FileInfo> result = new ArrayList<>();
|
||||||
|
list.forEach(item->{
|
||||||
|
JsonObject fileJson = (JsonObject) item;
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
|
||||||
|
// 映射已知字段fileInfo
|
||||||
|
String fileId = fileJson.getString("fileId");
|
||||||
|
String userId = fileJson.getString("userId");
|
||||||
|
|
||||||
|
// 其他参数
|
||||||
|
long nowTs2 = System.currentTimeMillis();
|
||||||
|
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
|
||||||
|
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
|
||||||
|
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
|
||||||
|
|
||||||
|
// 回传用到的参数
|
||||||
|
//"fidEncode", paramJson.getString("fidEncode"))
|
||||||
|
//"uuid", paramJson.getString("uuid"))
|
||||||
|
//"ts", paramJson.getString("ts"))
|
||||||
|
//"auth", paramJson.getString("auth"))
|
||||||
|
//"shareId", paramJson.getString("shareId"))
|
||||||
|
JsonObject entries = JsonObject.of(
|
||||||
|
"fidEncode", fidEncode,
|
||||||
|
"uuid", uuid,
|
||||||
|
"ts", tsEncode2,
|
||||||
|
"auth", auth,
|
||||||
|
"shareId", shareId);
|
||||||
|
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
|
||||||
|
String param = new String(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(fileJson.getString("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);
|
||||||
|
}).onFailure(failRes -> {
|
||||||
|
log.error("解析目录请求失败: {}", failRes.getMessage());
|
||||||
|
promise.fail(failRes);
|
||||||
|
});;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parseById() {
|
||||||
|
// 第二次请求
|
||||||
|
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
|
||||||
|
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.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"))
|
||||||
|
.putHeaders(header).send().onSuccess(res2 -> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package cn.qaiu.parser.impl;
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.parser.PanBase;
|
import cn.qaiu.parser.PanBase;
|
||||||
import cn.qaiu.util.AESUtils;
|
import cn.qaiu.util.AESUtils;
|
||||||
|
import cn.qaiu.util.FileSizeConverter;
|
||||||
|
import cn.qaiu.util.UUIDUtil;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.MultiMap;
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.uritemplate.UriTemplate;
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 蓝奏云优享
|
* 蓝奏云优享
|
||||||
@@ -16,46 +22,105 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
public class IzTool extends PanBase {
|
public class IzTool extends PanBase {
|
||||||
|
|
||||||
public static final String SHARE_URL_PREFIX = "https://www.ilanzou.com/s/";
|
|
||||||
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 +
|
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
|
||||||
"recommend/list?devModel=Chrome&extra=2&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 SECOND_REQUEST_URL = API_URL_PREFIX +
|
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
|
||||||
"file/redirect?downloadId={fidEncode}&enable=1&devType=6&uuid={uuid}×tamp={ts}&auth={auth}&shareId={shareId}";
|
"&devType=6&uuid={uuid}×tamp={ts}&auth={auth}&shareId={dataKey}";
|
||||||
// downloadId=x&enable=1&devType=6&uuid=x×tamp=x&auth=x&shareId=lGFndCM
|
// downloadId=x&enable=1&devType=6&uuid=x×tamp=x&auth=x&shareId=lGFndCM
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
long nowTs = System.currentTimeMillis();
|
||||||
|
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
private static final MultiMap header;
|
||||||
|
|
||||||
|
static {
|
||||||
|
header = MultiMap.caseInsensitiveMultiMap();
|
||||||
|
header.set("Accept", "application/json, text/plain, */*");
|
||||||
|
header.set("Accept-Encoding", "gzip, deflate, br, zstd");
|
||||||
|
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 IzTool(ShareLinkInfo shareLinkInfo) {
|
public IzTool(ShareLinkInfo shareLinkInfo) {
|
||||||
super(shareLinkInfo);
|
super(shareLinkInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
String dataKey = shareLinkInfo.getShareKey();
|
String shareId = shareLinkInfo.getShareKey();
|
||||||
|
|
||||||
// 24.5.12 ilanzou改规则无需计算shareId
|
// 24.5.12 ilanzou改规则无需计算shareId
|
||||||
// String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey));
|
// String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey));
|
||||||
|
|
||||||
|
// 第一次请求 获取文件信息
|
||||||
|
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
|
||||||
|
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
|
||||||
|
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
|
||||||
|
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.send().onSuccess(r0 -> { // 忽略res
|
||||||
// 第一次请求 获取文件信息
|
// 第一次请求 获取文件信息
|
||||||
// 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
|
||||||
client.postAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("shareId", dataKey).send().onSuccess(res -> {
|
client.postAbs(UriTemplate.of(url))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.send().onSuccess(res -> {
|
||||||
JsonObject resJson = asJson(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").size() == 0) {
|
if (resJson.getJsonArray("list").isEmpty()) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
|
||||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 文件Id
|
// 文件Id
|
||||||
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
String fileId = fileInfo.getString("fileIds");
|
String fileId = fileInfo.getString("fileIds");
|
||||||
String userId = fileInfo.getString("userId");
|
String userId = fileInfo.getString("userId");
|
||||||
// 其他参数
|
// 其他参数
|
||||||
long nowTs = System.currentTimeMillis();
|
|
||||||
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
|
||||||
String uuid = UUID.randomUUID().toString();
|
|
||||||
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
|
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
|
||||||
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||||
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
||||||
@@ -65,16 +130,154 @@ public class IzTool extends PanBase {
|
|||||||
.setTemplateParam("uuid", uuid)
|
.setTemplateParam("uuid", uuid)
|
||||||
.setTemplateParam("ts", tsEncode)
|
.setTemplateParam("ts", tsEncode)
|
||||||
.setTemplateParam("auth", auth)
|
.setTemplateParam("auth", auth)
|
||||||
.setTemplateParam("shareId", dataKey).send().onSuccess(res2 -> {
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.putHeaders(header).send().onSuccess(res2 -> {
|
||||||
MultiMap headers = res2.headers();
|
MultiMap headers = res2.headers();
|
||||||
if (!headers.contains("Location")) {
|
if (!headers.contains("Location")) {
|
||||||
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res.headers());
|
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + headers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
promise.complete(headers.get("Location"));
|
promise.complete(headers.get("Location"));
|
||||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||||
}).onFailure(handleFail(FIRST_REQUEST_URL));
|
}).onFailure(handleFail(FIRST_REQUEST_URL));
|
||||||
|
});
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 -> {
|
||||||
|
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
|
||||||
|
parserDir(id, shareId, promise);
|
||||||
|
} else {
|
||||||
|
promise.fail("解析目录ID失败");
|
||||||
|
}
|
||||||
|
}).onFailure(failRes -> {
|
||||||
|
log.error("解析目录失败: {}", failRes.getMessage());
|
||||||
|
promise.fail(failRes);
|
||||||
|
});
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
|
||||||
|
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
|
||||||
|
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
|
||||||
|
// 拿到目录ID
|
||||||
|
client.postAbs(UriTemplate.of(FILE_LIST_URL))
|
||||||
|
.putHeaders(header)
|
||||||
|
.setTemplateParam("shareId", shareId)
|
||||||
|
.setTemplateParam("uuid", uuid)
|
||||||
|
.setTemplateParam("ts", tsEncode)
|
||||||
|
.setTemplateParam("folderId", id)
|
||||||
|
.send().onSuccess(res -> {
|
||||||
|
JsonObject jsonObject;
|
||||||
|
try {
|
||||||
|
jsonObject = asJson(res);
|
||||||
|
} catch (Exception e) {
|
||||||
|
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// System.out.println(jsonObject.encodePrettily());
|
||||||
|
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");
|
||||||
|
|
||||||
|
// 回传用到的参数
|
||||||
|
//"fidEncode", paramJson.getString("fidEncode"))
|
||||||
|
//"uuid", paramJson.getString("uuid"))
|
||||||
|
//"ts", paramJson.getString("ts"))
|
||||||
|
//"auth", paramJson.getString("auth"))
|
||||||
|
//"shareId", paramJson.getString("shareId"))
|
||||||
|
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||||
|
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
||||||
|
JsonObject entries = JsonObject.of(
|
||||||
|
"fidEncode", fidEncode,
|
||||||
|
"uuid", uuid,
|
||||||
|
"ts", tsEncode,
|
||||||
|
"auth", auth,
|
||||||
|
"shareId", shareId);
|
||||||
|
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
|
||||||
|
String param = new String(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);
|
||||||
|
}).onFailure(failRes -> {
|
||||||
|
log.error("解析目录请求失败: {}", failRes.getMessage());
|
||||||
|
promise.fail(failRes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parseById() {
|
||||||
|
// 第二次请求
|
||||||
|
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
|
||||||
|
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||||
|
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
||||||
|
.setTemplateParam("ts", paramJson.getString("ts"))
|
||||||
|
.setTemplateParam("auth", paramJson.getString("auth"))
|
||||||
|
.setTemplateParam("shareId", paramJson.getString("shareId"))
|
||||||
|
.putHeaders(header).send().onSuccess(res2 -> {
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
package cn.qaiu.parser.impl;
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.parser.PanBase;
|
import cn.qaiu.parser.PanBase;
|
||||||
|
import cn.qaiu.util.CastUtil;
|
||||||
|
import cn.qaiu.util.FileSizeConverter;
|
||||||
|
import cn.qaiu.util.HeaderUtils;
|
||||||
import cn.qaiu.util.JsExecUtils;
|
import cn.qaiu.util.JsExecUtils;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.MultiMap;
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.ext.web.client.WebClient;
|
import io.vertx.ext.web.client.WebClient;
|
||||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||||
|
|
||||||
import javax.script.ScriptException;
|
import javax.script.ScriptException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -28,7 +35,6 @@ public class LzTool extends PanBase {
|
|||||||
super(shareLinkInfo);
|
super(shareLinkInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
String sUrl = shareLinkInfo.getStandardUrl();
|
String sUrl = shareLinkInfo.getStandardUrl();
|
||||||
String pwd = shareLinkInfo.getSharePassword();
|
String pwd = shareLinkInfo.getSharePassword();
|
||||||
@@ -41,23 +47,11 @@ public class LzTool extends PanBase {
|
|||||||
Matcher matcher = compile.matcher(html);
|
Matcher matcher = compile.matcher(html);
|
||||||
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
|
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
|
||||||
if (!matcher.find()) {
|
if (!matcher.find()) {
|
||||||
// 处理一下JS
|
|
||||||
String jsText = getJsText(html);
|
|
||||||
|
|
||||||
if (jsText == null) {
|
|
||||||
fail(SHARE_URL_PREFIX + " -> " + sUrl + ": js脚本匹配失败, 可能分享已失效");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jsText = jsText.replace("document.getElementById('pwd').value", "\"" + pwd + "\"");
|
|
||||||
int i = jsText.indexOf("document.getElementById('rpt')");
|
|
||||||
if (i > 0) {
|
|
||||||
jsText = jsText.substring(0, i);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
String jsText = getJsByPwd(pwd, html, "document.getElementById('rpt')");
|
||||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
|
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
|
||||||
getDownURL(sUrl, client, (Map<String, String>) scriptObjectMirror.get("data"));
|
getDownURL(sUrl, client, scriptObjectMirror);
|
||||||
} catch (ScriptException | NoSuchMethodException e) {
|
} catch (Exception e) {
|
||||||
fail(e, "js引擎执行失败");
|
fail(e, "js引擎执行失败");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -75,7 +69,7 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
|
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
|
||||||
getDownURL(sUrl, client, (Map<String, String>) scriptObjectMirror.get("data"));
|
getDownURL(sUrl, client, scriptObjectMirror);
|
||||||
} catch (ScriptException | NoSuchMethodException e) {
|
} catch (ScriptException | NoSuchMethodException e) {
|
||||||
fail(e, "js引擎执行失败");
|
fail(e, "js引擎执行失败");
|
||||||
}
|
}
|
||||||
@@ -85,6 +79,20 @@ public class LzTool extends PanBase {
|
|||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getJsByPwd(String pwd, String html, String subText) {
|
||||||
|
String jsText = getJsText(html);
|
||||||
|
|
||||||
|
if (jsText == null) {
|
||||||
|
throw new RuntimeException("js脚本匹配失败, 可能分享已失效");
|
||||||
|
}
|
||||||
|
jsText = jsText.replace("document.getElementById('pwd').value", "\"" + pwd + "\"");
|
||||||
|
int i = jsText.indexOf(subText);
|
||||||
|
if (i > 0) {
|
||||||
|
jsText = jsText.substring(0, i);
|
||||||
|
}
|
||||||
|
return jsText;
|
||||||
|
}
|
||||||
|
|
||||||
private String getJsText(String html) {
|
private String getJsText(String html) {
|
||||||
String jsTagStart = "<script type=\"text/javascript\">";
|
String jsTagStart = "<script type=\"text/javascript\">";
|
||||||
String jsTagEnd = "</script>";
|
String jsTagEnd = "</script>";
|
||||||
@@ -94,14 +102,60 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
int startPos = index + jsTagStart.length();
|
int startPos = index + jsTagStart.length();
|
||||||
int endPos = html.indexOf(jsTagEnd, startPos);
|
int endPos = html.indexOf(jsTagEnd, startPos);
|
||||||
return html.substring(startPos, endPos);
|
return html.substring(startPos, endPos).replaceAll("<!--.*-->", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getDownURL(String key, WebClient client, Map<String, ?> signMap) {
|
private void getDownURL(String key, WebClient client, Map<String, ?> obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
fail("需要访问密码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<?, ?> signMap = (Map<?, ?>)obj.get("data");
|
||||||
|
String url0 = obj.get("url").toString();
|
||||||
MultiMap map = MultiMap.caseInsensitiveMultiMap();
|
MultiMap map = MultiMap.caseInsensitiveMultiMap();
|
||||||
signMap.forEach((k, v) -> {
|
signMap.forEach((k, v) -> {
|
||||||
map.set(k, v.toString());
|
map.add((String) k, v.toString());
|
||||||
});
|
});
|
||||||
|
MultiMap headers = HeaderUtils.parseHeaders("""
|
||||||
|
Accept: application/json, text/javascript, */*
|
||||||
|
Accept-Encoding: gzip, deflate, br, zstd
|
||||||
|
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Connection: keep-alive
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Pragma: no-cache
|
||||||
|
Sec-Fetch-Dest: empty
|
||||||
|
Sec-Fetch-Mode: cors
|
||||||
|
Sec-Fetch-Site: same-origin
|
||||||
|
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0
|
||||||
|
X-Requested-With: XMLHttpRequest
|
||||||
|
sec-ch-ua: "Chromium";v="134", "Not:A-Brand";v="24", "Microsoft Edge";v="134"
|
||||||
|
sec-ch-ua-mobile: ?0
|
||||||
|
sec-ch-ua-platform: "Windows"
|
||||||
|
""");
|
||||||
|
|
||||||
|
headers.set("referer", key);
|
||||||
|
// action=downprocess&signs=%3Fctdf&websignkey=I5gl&sign=BWMGOF1sBTRWXwI9BjZdYVA7BDhfNAIyUG9UawJtUGMIPlAhACkCa1UyUTAAYFxvUj5XY1E7UGFXaFVq&websign=&kd=1&ves=1
|
||||||
|
String url = SHARE_URL_PREFIX + url0;
|
||||||
|
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
||||||
|
try {
|
||||||
|
JsonObject urlJson = asJson(res2);
|
||||||
|
if (urlJson.getInteger("zt") != 1) {
|
||||||
|
fail(urlJson.getString("inf"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
|
||||||
|
headers.remove("Referer");
|
||||||
|
client.getAbs(downUrl).putHeaders(headers).send()
|
||||||
|
.onSuccess(res3 -> promise.complete(res3.headers().get("Location")))
|
||||||
|
.onFailure(handleFail(downUrl));
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail("解析异常");
|
||||||
|
}
|
||||||
|
}).onFailure(handleFail(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MultiMap getHeaders(String key) {
|
||||||
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||||
var userAgent2 = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, " +
|
var userAgent2 = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, " +
|
||||||
"like " +
|
"like " +
|
||||||
@@ -111,18 +165,76 @@ public class LzTool extends PanBase {
|
|||||||
headers.set("sec-ch-ua-platform", "Android");
|
headers.set("sec-ch-ua-platform", "Android");
|
||||||
headers.set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
|
headers.set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
|
||||||
headers.set("sec-ch-ua-mobile", "sec-ch-ua-mobile");
|
headers.set("sec-ch-ua-mobile", "sec-ch-ua-mobile");
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
String url = SHARE_URL_PREFIX + "/ajaxm.php";
|
|
||||||
|
@Override
|
||||||
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
|
Promise<List<FileInfo>> promise = Promise.promise();
|
||||||
|
|
||||||
|
String sUrl = shareLinkInfo.getShareUrl();
|
||||||
|
String pwd = shareLinkInfo.getSharePassword();
|
||||||
|
|
||||||
|
WebClient client = clientNoRedirects;
|
||||||
|
client.getAbs(sUrl).send().onSuccess(res -> {
|
||||||
|
String html = res.bodyAsString();
|
||||||
|
try {
|
||||||
|
String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
|
||||||
|
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
|
||||||
|
Map<String, Object> data = CastUtil.cast(scriptObjectMirror.get("data"));
|
||||||
|
MultiMap map = MultiMap.caseInsensitiveMultiMap();
|
||||||
|
data.forEach((k, v) -> map.set(k, v.toString()));
|
||||||
|
log.debug("解析参数: {}", map);
|
||||||
|
MultiMap headers = getHeaders(sUrl);
|
||||||
|
|
||||||
|
String url = SHARE_URL_PREFIX + "/filemoreajax.php?file=" + data.get("fid");
|
||||||
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
||||||
JsonObject urlJson = asJson(res2);
|
JsonObject fileListJson = asJson(res2);
|
||||||
if (urlJson.getInteger("zt") != 1) {
|
if (fileListJson.getInteger("zt") != 1) {
|
||||||
fail(urlJson.getString("inf"));
|
promise.fail(baseMsg() + fileListJson.getString("info"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
|
List<FileInfo> list = new ArrayList<>();
|
||||||
client.getAbs(downUrl).putHeaders(headers).send()
|
fileListJson.getJsonArray("text").forEach(item -> {
|
||||||
.onSuccess(res3 -> promise.complete(res3.headers().get("Location")))
|
/*
|
||||||
.onFailure(handleFail(downUrl));
|
{
|
||||||
}).onFailure(handleFail(url));
|
"icon": "apk",
|
||||||
|
"t": 0,
|
||||||
|
"id": "iULV2n4361c",
|
||||||
|
"name_all": "xx.apk",
|
||||||
|
"size": "49.8 M",
|
||||||
|
"time": "2021-03-19",
|
||||||
|
"duan": "in4361",
|
||||||
|
"p_ico": 0
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
JsonObject fileJson = (JsonObject) item;
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
String size = fileJson.getString("size");
|
||||||
|
Long sizeNum = FileSizeConverter.convertToBytes(size);
|
||||||
|
String panType = shareLinkInfo.getType();
|
||||||
|
String id = fileJson.getString("id");
|
||||||
|
fileInfo.setFileName(fileJson.getString("name_all"))
|
||||||
|
.setFileId(id)
|
||||||
|
.setCreateTime(fileJson.getString("time"))
|
||||||
|
.setFileType(fileJson.getString("icon"))
|
||||||
|
.setSizeStr(fileJson.getString("size"))
|
||||||
|
.setSize(sizeNum)
|
||||||
|
.setPanType(panType)
|
||||||
|
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id)
|
||||||
|
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
|
||||||
|
shareLinkInfo.getType(), id));
|
||||||
|
;
|
||||||
|
log.debug("文件信息: {}", fileInfo);
|
||||||
|
list.add(fileInfo);
|
||||||
|
});
|
||||||
|
promise.complete(list);
|
||||||
|
});
|
||||||
|
} catch (ScriptException | NoSuchMethodException e) {
|
||||||
|
promise.fail(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return promise.future();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import io.vertx.uritemplate.UriTemplate;
|
|||||||
* 需要请求API的UA和请求下载链接的UA保持一致,安卓Chrome需要访问电脑版才能下载
|
* 需要请求API的UA和请求下载链接的UA保持一致,安卓Chrome需要访问电脑版才能下载
|
||||||
*/
|
*/
|
||||||
public class P115Tool extends PanBase {
|
public class P115Tool extends PanBase {
|
||||||
private static final String API_URL_PREFIX = "https://anxia.com/webapi/";
|
private static final String API_URL_PREFIX = "https://115cdn.com/webapi/";
|
||||||
|
|
||||||
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "share/snap?share_code={dataKey}&offset=0" +
|
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "share/snap?share_code={dataKey}&offset=0" +
|
||||||
"&limit=20&receive_code={dataPwd}&cid=";
|
"&limit=20&receive_code={dataPwd}&cid=";
|
||||||
@@ -31,10 +31,10 @@ public class P115Tool extends PanBase {
|
|||||||
header.set("Connection", "keep-alive");
|
header.set("Connection", "keep-alive");
|
||||||
header.set("Content-Length", "0");
|
header.set("Content-Length", "0");
|
||||||
header.set("DNT", "1");
|
header.set("DNT", "1");
|
||||||
header.set("Host", "anxia.com");
|
header.set("Host", "115cdn.com");
|
||||||
header.set("Origin", "https://anxia.com");
|
header.set("Origin", "https://115cdn.com");
|
||||||
header.set("Pragma", "no-cache");
|
header.set("Pragma", "no-cache");
|
||||||
header.set("Referer", "https://anxia.com");
|
header.set("Referer", "https://115cdn.com");
|
||||||
header.set("Sec-Fetch-Dest", "empty");
|
header.set("Sec-Fetch-Dest", "empty");
|
||||||
header.set("Sec-Fetch-Mode", "cors");
|
header.set("Sec-Fetch-Mode", "cors");
|
||||||
header.set("Sec-Fetch-Site", "cross-site");
|
header.set("Sec-Fetch-Site", "cross-site");
|
||||||
@@ -75,8 +75,8 @@ public class P115Tool extends PanBase {
|
|||||||
.set("file_id", fileId))
|
.set("file_id", fileId))
|
||||||
.onSuccess(res2 -> {
|
.onSuccess(res2 -> {
|
||||||
JsonObject resJson2 = asJson(res2);
|
JsonObject resJson2 = asJson(res2);
|
||||||
if (!resJson.getBoolean("state")) {
|
if (!resJson2.getBoolean("state")) {
|
||||||
fail(FIRST_REQUEST_URL + " 解析错误: " + resJson);
|
fail(FIRST_REQUEST_URL + " 解析错误: " + resJson2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// data.url.url
|
// data.url.url
|
||||||
|
|||||||
@@ -31,18 +31,20 @@ public class P118Tool extends PanBase {
|
|||||||
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
|
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
|
||||||
Matcher matcher = compile.matcher(res.bodyAsString());
|
Matcher matcher = compile.matcher(res.bodyAsString());
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
System.out.println(matcher.group(1));
|
//c: 0x63
|
||||||
complete(matcher.group(1));
|
//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 {
|
} else {
|
||||||
fail();
|
fail();
|
||||||
}
|
}
|
||||||
}).onFailure(handleFail(""));
|
}).onFailure(handleFail(""));
|
||||||
return future();
|
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);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
109
parser/src/main/java/cn/qaiu/parser/impl/P360Tool.java
Normal file
109
parser/src/main/java/cn/qaiu/parser/impl/P360Tool.java
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.parser.PanBase;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.buffer.Buffer;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://yunpan.360.cn">360AI云盘</a>
|
||||||
|
* 360AI云盘解析
|
||||||
|
* 下载链接需要Referer: https://link.yunpan.com/
|
||||||
|
*/
|
||||||
|
public class P360Tool extends PanBase {
|
||||||
|
public P360Tool(ShareLinkInfo shareLinkInfo) {
|
||||||
|
super(shareLinkInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<String> parse() {
|
||||||
|
// https://www.yunpan.com/surl_yD7jK9d4W6D 获取302跳转地址
|
||||||
|
clientNoRedirects.getAbs(shareLinkInfo.getShareUrl()).send()
|
||||||
|
.onSuccess(res -> {
|
||||||
|
String location = res.getHeader("Location");
|
||||||
|
if (location != null) {
|
||||||
|
down302(location);
|
||||||
|
} else {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}).onFailure(handleFail(""));
|
||||||
|
|
||||||
|
|
||||||
|
return future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void down302(String url) {
|
||||||
|
// 获取URL前缀 https://214004.link.yunpan.com/lk/surl_yD7ZU4WreR8 -> https://214004.link.yunpan.com/
|
||||||
|
String urlPrefix = url.substring(0, url.indexOf("/", 8));
|
||||||
|
|
||||||
|
clientSession.getAbs(url)
|
||||||
|
.send()
|
||||||
|
.onSuccess(res -> {
|
||||||
|
// find "nid": "17402043311959599"
|
||||||
|
Pattern compile = Pattern.compile("\"nid\": \"([^\"]+)\"");
|
||||||
|
Matcher matcher = compile.matcher(res.bodyAsString());
|
||||||
|
AtomicReference<String> nid = new AtomicReference<>();
|
||||||
|
if (matcher.find()) {
|
||||||
|
nid.set(matcher.group(1));
|
||||||
|
} else {
|
||||||
|
// 需要验证密码
|
||||||
|
/*
|
||||||
|
* POST https://4aec17.link.yunpan.com/share/verifyPassword
|
||||||
|
* Content-type: application/x-www-form-urlencoded UTF-8
|
||||||
|
* Referer: https://4aec17.link.yunpan.com/lk/surl_yD7jK9d4W6D
|
||||||
|
*
|
||||||
|
* shorturl=surl_yD7jK9d4W6D&linkpassword=d969
|
||||||
|
*/
|
||||||
|
clientSession.postAbs(urlPrefix + "/share/verifyPassword")
|
||||||
|
.putHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.putHeader("Referer", urlPrefix)
|
||||||
|
.sendBuffer(Buffer.buffer("shorturl=" + shareLinkInfo.getShareKey() + "&linkpassword" +
|
||||||
|
"=" + shareLinkInfo.getSharePassword()))
|
||||||
|
.onSuccess(res2 -> {
|
||||||
|
JsonObject entries = asJson(res2);
|
||||||
|
if (entries.getInteger("errno") == 0) {
|
||||||
|
clientSession.getAbs(url)
|
||||||
|
.send()
|
||||||
|
.onSuccess(res3 -> {
|
||||||
|
Matcher matcher1 = compile.matcher(res3.bodyAsString());
|
||||||
|
if (matcher1.find()) {
|
||||||
|
nid.set(matcher1.group(1));
|
||||||
|
} else {
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
down(urlPrefix, nid.get());
|
||||||
|
}).onFailure(handleFail(""));
|
||||||
|
} else {
|
||||||
|
fail(entries.encode());
|
||||||
|
}
|
||||||
|
}).onFailure(handleFail(""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
down(urlPrefix, nid.get());
|
||||||
|
}).onFailure(handleFail(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void down(String urlPrefix, String nid) {
|
||||||
|
clientSession.postAbs(urlPrefix + "/share/downloadfile")
|
||||||
|
.putHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.putHeader("Referer", urlPrefix)
|
||||||
|
.sendBuffer(Buffer.buffer("shorturl=" + shareLinkInfo.getShareKey() + "&nid=" + nid))
|
||||||
|
.onSuccess(res2 -> {
|
||||||
|
JsonObject entries = asJson(res2);
|
||||||
|
String downloadurl = entries.getJsonObject("data").getString("downloadurl");
|
||||||
|
complete(downloadurl);
|
||||||
|
}).onFailure(handleFail(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// public static void main(String[] args) {
|
||||||
|
// String s = new P360Tool(ShareLinkInfo.newBuilder().shareUrl("https://www.yunpan.com/surl_yD7ZU4WreR8")
|
||||||
|
// .shareKey("surl_yD7ZU4WreR8")
|
||||||
|
// .build()).parseSync();
|
||||||
|
// System.out.println(s);
|
||||||
|
// }
|
||||||
|
}
|
||||||
47
parser/src/main/java/cn/qaiu/parser/impl/PcxTool.java
Normal file
47
parser/src/main/java/cn/qaiu/parser/impl/PcxTool.java
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.parser.PanBase;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://passport2.chaoxing.com">超星云盘</a>
|
||||||
|
*/
|
||||||
|
public class PcxTool extends PanBase {
|
||||||
|
|
||||||
|
public PcxTool(ShareLinkInfo shareLinkInfo) {
|
||||||
|
super(shareLinkInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<String> parse() {
|
||||||
|
client.getAbs(shareLinkInfo.getShareUrl())
|
||||||
|
.send().onSuccess(res -> {
|
||||||
|
// 'download': 'https://d0.ananas.chaoxing.com/download/de08dcf546e4dd88a17bead86ff6338d?at_=1740211698795&ak_=d62a3acbd5ce43e1e8565b67990691e4&ad_=8c4ef22e980ee0dd9532ec3757ab19f8&fn=33.c'
|
||||||
|
String body = res.bodyAsString();
|
||||||
|
// 获取download
|
||||||
|
String str = "var fileinfo = {";
|
||||||
|
String fileInfo = res.bodyAsString().substring(res.bodyAsString().indexOf(str) + str.length() - 1
|
||||||
|
, res.bodyAsString().indexOf("};") + 1);
|
||||||
|
fileInfo = fileInfo.replace("'", "\"");
|
||||||
|
JsonObject jsonObject = new JsonObject(fileInfo);
|
||||||
|
String download = jsonObject.getString("download");
|
||||||
|
if (download.contains("fn=")) {
|
||||||
|
complete(download);
|
||||||
|
} else {
|
||||||
|
fail("获取下载链接失败: 不支持的文件类型: {}", jsonObject.getString("suffix"));
|
||||||
|
}
|
||||||
|
}).onFailure(handleFail(shareLinkInfo.getShareUrl()));
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public static void main(String[] args) {
|
||||||
|
// String s = new PcxTool(ShareLinkInfo.newBuilder().shareUrl("https://pan-yz.cldisk.com/external/m/file/953658049102462976")
|
||||||
|
// .shareKey("953658049102462976")
|
||||||
|
// .build()).parseSync();
|
||||||
|
// System.out.println(s);
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
package cn.qaiu.parser.impl;
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.WebClientVertxInit;
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.parser.PanBase;
|
import cn.qaiu.parser.PanBase;
|
||||||
|
import cn.qaiu.util.HeaderUtils;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.ext.web.client.WebClient;
|
||||||
import io.vertx.uritemplate.UriTemplate;
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 微雨云
|
* 微雨云
|
||||||
*/
|
*/
|
||||||
@@ -12,26 +20,68 @@ 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_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}";
|
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
|
||||||
|
cache-control: no-cache
|
||||||
|
dnt: 1
|
||||||
|
origin: https://www.vyuyun.com
|
||||||
|
pragma: no-cache
|
||||||
|
priority: u=1, i
|
||||||
|
referer: https://www.vyuyun.com/
|
||||||
|
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
|
||||||
|
sec-ch-ua-mobile: ?0
|
||||||
|
sec-ch-ua-platform: "Windows"
|
||||||
|
sec-fetch-dest: empty
|
||||||
|
sec-fetch-mode: cors
|
||||||
|
sec-fetch-site: same-site
|
||||||
|
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 String api;
|
||||||
public PvyyTool(ShareLinkInfo shareLinkInfo) {
|
public PvyyTool(ShareLinkInfo shareLinkInfo) {
|
||||||
super(shareLinkInfo);
|
super(shareLinkInfo);
|
||||||
|
api = new String(hexArray);
|
||||||
|
System.out.println(api);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Future<String> parse() {
|
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))
|
client.getAbs(UriTemplate.of(API_URL_PREFIX1))
|
||||||
.setTemplateParam("key", shareLinkInfo.getShareKey())
|
.setTemplateParam("key", shareLinkInfo.getShareKey())
|
||||||
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
||||||
.putHeader("referer", "https://www.vyuyun.com")
|
.putHeaders(header)
|
||||||
.send().onSuccess(res -> {
|
.send().onSuccess(res -> {
|
||||||
try {
|
try {
|
||||||
String id = asJson(res).getJsonObject("data").getJsonObject("data").getString("id");
|
String id = asJson(res).getJsonObject("data").getJsonObject("data").getString("id");
|
||||||
|
|
||||||
client.getAbs(UriTemplate.of(API_URL_PREFIX2))
|
client.getAbs(UriTemplate.of(apiUrl))
|
||||||
.setTemplateParam("key", shareLinkInfo.getShareKey())
|
.setTemplateParam("key", shareLinkInfo.getShareKey())
|
||||||
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
||||||
.setTemplateParam("id", id)
|
.setTemplateParam("id", id)
|
||||||
.putHeader("referer", "https://www.vyuyun.com")
|
.putHeaders(header).send().onSuccess(res2 -> {
|
||||||
.send().onSuccess(res2 -> {
|
|
||||||
try {
|
try {
|
||||||
// data->downInfo->url
|
// data->downInfo->url
|
||||||
String url =
|
String url =
|
||||||
@@ -42,10 +92,100 @@ public class PvyyTool extends PanBase {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
fail(asJson(res).encodePrettily());
|
fail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return future();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
171
parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java
Normal file
171
parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
61
parser/src/main/java/cn/qaiu/parser/impl/QQwTool.java
Normal file
61
parser/src/main/java/cn/qaiu/parser/impl/QQwTool.java
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class QQwTool extends QQTool {
|
||||||
|
|
||||||
|
public QQwTool(ShareLinkInfo shareLinkInfo) {
|
||||||
|
super(shareLinkInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parse() {
|
||||||
|
client.getAbs(shareLinkInfo.getStandardUrl()).send().onSuccess(res -> {
|
||||||
|
String html = res.bodyAsString();
|
||||||
|
String url = extractVariables(html).get("url");
|
||||||
|
if (url != null) {
|
||||||
|
String url302 = url.replace("\\x26", "&");
|
||||||
|
promise.complete(url302);
|
||||||
|
|
||||||
|
/*
|
||||||
|
clientNoRedirects.getAbs(url302).send().onSuccess(res2 -> {
|
||||||
|
MultiMap headers = res2.headers();
|
||||||
|
if (headers.contains("Location")) {
|
||||||
|
promise.complete(headers.get("Location"));
|
||||||
|
} else {
|
||||||
|
fail("找不到重定向URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
}).onFailure(handleFail());
|
||||||
|
*/
|
||||||
|
} else {
|
||||||
|
fail("分享链接解析失败, 可能是链接失效");
|
||||||
|
}
|
||||||
|
}).onFailure(handleFail());
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
String variableName = matcher.group(1); // 变量名
|
||||||
|
String variableValue = matcher.group(3); // 变量值
|
||||||
|
variables.put(variableName, variableValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
}
|
||||||
122
parser/src/main/java/cn/qaiu/parser/impl/UcTool.java
Normal file
122
parser/src/main/java/cn/qaiu/parser/impl/UcTool.java
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.parser.PanBase;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UC网盘解析
|
||||||
|
*/
|
||||||
|
public class UcTool extends PanBase {
|
||||||
|
private static final String API_URL_PREFIX = "https://pc-api.uc.cn/1/clouddrive/";
|
||||||
|
|
||||||
|
public static final String SHARE_URL_PREFIX = "https://fast.uc.cn/s/";
|
||||||
|
|
||||||
|
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "share/sharepage/token?entry=ft&fr=pc&pr" +
|
||||||
|
"=UCBrowser";
|
||||||
|
|
||||||
|
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "transfer_share/detail?pwd_id={pwd_id}&passcode" +
|
||||||
|
"={passcode}&stoken={stoken}";
|
||||||
|
|
||||||
|
private static final String THIRD_REQUEST_URL = API_URL_PREFIX + "file/download?entry=ft&fr=pc&pr=UCBrowser";
|
||||||
|
|
||||||
|
public UcTool(ShareLinkInfo shareLinkInfo) {
|
||||||
|
super(shareLinkInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<String> parse() {
|
||||||
|
var dataKey = shareLinkInfo.getShareKey();
|
||||||
|
var passcode = shareLinkInfo.getSharePassword();
|
||||||
|
|
||||||
|
var jsonObject = JsonObject.of("share_for_transfer", true);
|
||||||
|
jsonObject.put("pwd_id", dataKey);
|
||||||
|
jsonObject.put("passcode", passcode);
|
||||||
|
// 第一次请求 获取文件信息
|
||||||
|
client.postAbs(FIRST_REQUEST_URL).sendJsonObject(jsonObject).onSuccess(res -> {
|
||||||
|
log.debug("第一阶段 {}", res.body());
|
||||||
|
var resJson = res.bodyAsJsonObject();
|
||||||
|
if (resJson.getInteger("code") != 0) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var stoken = resJson.getJsonObject("data").getString("stoken");
|
||||||
|
// 第二次请求
|
||||||
|
client.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||||
|
.setTemplateParam("pwd_id", dataKey)
|
||||||
|
.setTemplateParam("passcode", passcode)
|
||||||
|
.setTemplateParam("stoken", stoken)
|
||||||
|
.send().onSuccess(res2 -> {
|
||||||
|
log.debug("第二阶段 {}", res2.body());
|
||||||
|
JsonObject resJson2 = res2.bodyAsJsonObject();
|
||||||
|
if (resJson2.getInteger("code") != 0) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 文件信息
|
||||||
|
var info = resJson2.getJsonObject("data").getJsonArray("list").getJsonObject(0);
|
||||||
|
// 第二次请求
|
||||||
|
var bodyJson = JsonObject.of()
|
||||||
|
.put("fids", JsonArray.of(info.getString("fid")))
|
||||||
|
.put("pwd_id", dataKey)
|
||||||
|
.put("stoken", stoken)
|
||||||
|
.put("fids_token", JsonArray.of(info.getString("share_fid_token")));
|
||||||
|
client.postAbs(THIRD_REQUEST_URL).sendJsonObject(bodyJson)
|
||||||
|
.onSuccess(res3 -> {
|
||||||
|
log.debug("第三阶段 {}", res3.body());
|
||||||
|
var resJson3 = res3.bodyAsJsonObject();
|
||||||
|
if (resJson3.getInteger("code") != 0) {
|
||||||
|
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
promise.complete(resJson3.getJsonArray("data").getJsonObject(0).getString("download_url"));
|
||||||
|
}).onFailure(handleFail(THIRD_REQUEST_URL));
|
||||||
|
|
||||||
|
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||||
|
}
|
||||||
|
).onFailure(handleFail(FIRST_REQUEST_URL));
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
// https://dl-uf-zb.pds.uc.cn/l3PNAKfz/64623447/
|
||||||
|
// 646b0de6e9f13000c9b14ba182b805312795a82a/
|
||||||
|
// 646b0de6717e1bfa5bb44dd2a456f103c5177850?
|
||||||
|
// Expires=1737784900&OSSAccessKeyId=LTAI5tJJpWQEfrcKHnd1LqsZ&
|
||||||
|
// Signature=oBVV3anhv3tBKanHUcEIsktkB%2BM%3D&x-oss-traffic-limit=503316480
|
||||||
|
// &response-content-disposition=attachment%3B%20filename%3DC%2523%2520Shell%2520%2528C%2523%2520Offline%2520Compiler%2529_2.5.16.apks
|
||||||
|
// %3Bfilename%2A%3Dutf-8%27%27C%2523%2520Shell%2520%2528C%2523%2520Offline%2520Compiler%2529_2.5.16.apks
|
||||||
|
|
||||||
|
//eyJ4OmF1IjoiLSIsIng6dWQiOiI0LU4tNS0wLTYtTi0zLWZ0LTAtMi1OLU4iLCJ4OnNwIjoiMTAwIiwieDp0b2tlbiI6IjQtZjY0ZmMxMDFjZmQxZGVkNTRkMGM0NmMzYzliMzkyOWYtNS03LTE1MzYxMS1kYWNiMzY2NWJiYWE0ZjVlOWQzNzgwMGVjNjQwMzE2MC0wLTAtMC0wLTQ5YzUzNTE3OGIxOTY0YzhjYzUwYzRlMDk5MTZmYWRhIiwieDp0dGwiOiIxMDgwMCJ9
|
||||||
|
//eyJjYWxsYmFja0JvZHlUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsImNhbGxiYWNrU3RhZ2UiOiJiZWZvcmUtZXhlY3V0ZSIsImNhbGxiYWNrRmFpbHVyZUFjdGlvbiI6Imlnbm9yZSIsImNhbGxiYWNrVXJsIjoiaHR0cHM6Ly9hdXRoLWNkbi51Yy5jbi9vdXRlci9vc3MvY2hlY2twbGF5IiwiY2FsbGJhY2tCb2R5Ijoie1wiaG9zdFwiOiR7aHR0cEhlYWRlci5ob3N0fSxcInNpemVcIjoke3NpemV9LFwicmFuZ2VcIjoke2h0dHBIZWFkZXIucmFuZ2V9LFwicmVmZXJlclwiOiR7aHR0cEhlYWRlci5yZWZlcmVyfSxcImNvb2tpZVwiOiR7aHR0cEhlYWRlci5jb29raWV9LFwibWV0aG9kXCI6JHtodHRwSGVhZGVyLm1ldGhvZH0sXCJpcFwiOiR7Y2xpZW50SXB9LFwicG9ydFwiOiR7Y2xpZW50UG9ydH0sXCJvYmplY3RcIjoke29iamVjdH0sXCJzcFwiOiR7eDpzcH0sXCJ1ZFwiOiR7eDp1ZH0sXCJ0b2tlblwiOiR7eDp0b2tlbn0sXCJhdVwiOiR7eDphdX0sXCJ0dGxcIjoke3g6dHRsfSxcImR0X3NwXCI6JHt4OmR0X3NwfSxcImhzcFwiOiR7eDpoc3B9LFwiY2xpZW50X3Rva2VuXCI6JHtxdWVyeVN0cmluZy5jbGllbnRfdG9rZW59fSJ9
|
||||||
|
//callback-var {"x:au":"-","x:ud":"4-N-5-0-6-N-3-ft-0-2-N-N","x:sp":"100","x:token":"4-f64fc101cfd1ded54d0c46c3c9b3929f-5-7-153611-dacb3665bbaa4f5e9d37800ec6403160-0-0-0-0-49c535178b1964c8cc50c4e09916fada","x:ttl":"10800"}
|
||||||
|
//callback {"callbackBodyType":"application/json","callbackStage":"before-execute","callbackFailureAction":"ignore","callbackUrl":"https://auth-cdn.uc.cn/outer/oss/checkplay","callbackBody":"{\"host\":${httpHeader.host},\"size\":${size},\"range\":${httpHeader.range},\"referer\":${httpHeader.referer},\"cookie\":${httpHeader.cookie},\"method\":${httpHeader.method},\"ip\":${clientIp},\"port\":${clientPort},\"object\":${object},\"sp\":${x:sp},\"ud\":${x:ud},\"token\":${x:token},\"au\":${x:au},\"ttl\":${x:ttl},\"dt_sp\":${x:dt_sp},\"hsp\":${x:hsp},\"client_token\":${queryString.client_token}}"}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// callback-var
|
||||||
|
{
|
||||||
|
"x:au": "-",
|
||||||
|
"x:ud": "4-N-5-0-6-N-3-ft-0-2-N-N",
|
||||||
|
"x:sp": "100",
|
||||||
|
"x:token": "4-f64fc101cfd1ded54d0c46c3c9b3929f-5-7-153611-dacb3665bbaa4f5e9d37800ec6403160-0-0-0-0-49c535178b1964c8cc50c4e09916fada",
|
||||||
|
"x:ttl": "10800"
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback
|
||||||
|
{
|
||||||
|
"callbackBodyType": "application/json",
|
||||||
|
"callbackStage": "before-execute",
|
||||||
|
"callbackFailureAction": "ignore",
|
||||||
|
"callbackUrl": "https://auth-cdn.uc.cn/outer/oss/checkplay",
|
||||||
|
"callbackBody": "{\"host\":${httpHeader.host},\"size\":${size},\"range\":${httpHeader.range},\"referer\":${httpHeader.referer},\"cookie\":${httpHeader.cookie},\"method\":${httpHeader.method},\"ip\":${clientIp},\"port\":${clientPort},\"object\":${object},\"sp\":${x:sp},\"ud\":${x:ud},\"token\":${x:token},\"au\":${x:au},\"ttl\":${x:ttl},\"dt_sp\":${x:dt_sp},\"hsp\":${x:hsp},\"client_token\":${queryString.client_token}}"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
new UcTool(ShareLinkInfo.newBuilder().shareUrl("https://fast.uc.cn/s/33197dd53ace4").shareKey("33197dd53ace4").build()).parse().onSuccess(
|
||||||
|
System.out::println
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,32 @@
|
|||||||
package cn.qaiu.parser.impl;
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.parser.PanBase;
|
import cn.qaiu.parser.PanBase;
|
||||||
import cn.qaiu.util.CommonUtils;
|
import cn.qaiu.util.CommonUtils;
|
||||||
|
import cn.qaiu.util.FileSizeConverter;
|
||||||
import cn.qaiu.util.JsExecUtils;
|
import cn.qaiu.util.JsExecUtils;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.core.json.pointer.JsonPointer;
|
||||||
import io.vertx.ext.web.client.WebClient;
|
import io.vertx.ext.web.client.WebClient;
|
||||||
import io.vertx.uritemplate.UriTemplate;
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static cn.qaiu.util.RandomStringGenerator.gen36String;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 123网盘
|
* 123网盘
|
||||||
*/
|
*/
|
||||||
@@ -27,11 +37,31 @@ public class YeTool extends PanBase {
|
|||||||
|
|
||||||
private static final String GET_FILE_INFO_URL = "https://www.123pan.com/a/api/share/get?limit=100&next=1&orderBy" +
|
private static final String GET_FILE_INFO_URL = "https://www.123pan.com/a/api/share/get?limit=100&next=1&orderBy" +
|
||||||
"=file_name&orderDirection=asc" +
|
"=file_name&orderDirection=asc" +
|
||||||
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId=0&Page=1&event=homeListFile&operateType=1";
|
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId={ParentFileId}&Page=1&event=homeListFile&operateType=1";
|
||||||
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/a/api/share/download/info?{authK}={authV}";
|
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/a/api/share/download/info?{authK}={authV}";
|
||||||
|
|
||||||
|
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info?{authK}={authV}";
|
||||||
|
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
|
||||||
|
|
||||||
public YeTool(ShareLinkInfo shareLinkInfo) {
|
public YeTool(ShareLinkInfo shareLinkInfo) {
|
||||||
super(shareLinkInfo);
|
super(shareLinkInfo);
|
||||||
|
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
|
||||||
|
header.set("App-Version", "3");
|
||||||
|
header.set("Cache-Control", "no-cache");
|
||||||
|
header.set("Connection", "keep-alive");
|
||||||
|
//header.set("DNT", "1");
|
||||||
|
//header.set("Host", "www.123pan.com");
|
||||||
|
header.set("LoginUuid", gen36String());
|
||||||
|
header.set("Pragma", "no-cache");
|
||||||
|
header.set("Referer", shareLinkInfo.getStandardUrl());
|
||||||
|
header.set("Sec-Fetch-Dest", "empty");
|
||||||
|
header.set("Sec-Fetch-Mode", "cors");
|
||||||
|
header.set("Sec-Fetch-Site", "same-origin");
|
||||||
|
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0");
|
||||||
|
header.set("platform", "web");
|
||||||
|
header.set("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Microsoft Edge\";v=\"127\", \"Chromium\";v=\"127\"");
|
||||||
|
header.set("sec-ch-ua-mobile", "?0");
|
||||||
|
header.set("sec-ch-ua-platform", "Windows");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
@@ -72,6 +102,7 @@ public class YeTool extends PanBase {
|
|||||||
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
|
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
|
||||||
.setTemplateParam("shareKey", shareKey)
|
.setTemplateParam("shareKey", shareKey)
|
||||||
.setTemplateParam("pwd", pwd)
|
.setTemplateParam("pwd", pwd)
|
||||||
|
.setTemplateParam("ParentFileId", "0")
|
||||||
// .setTemplateParam("authKey", AESUtils.getAuthKey("/a/api/share/get"))
|
// .setTemplateParam("authKey", AESUtils.getAuthKey("/a/api/share/get"))
|
||||||
.putHeader("Platform", "web")
|
.putHeader("Platform", "web")
|
||||||
.putHeader("App-Version", "3")
|
.putHeader("App-Version", "3")
|
||||||
@@ -81,9 +112,23 @@ public class YeTool extends PanBase {
|
|||||||
fail("{} 状态码异常 {}", dataKey, infoJson);
|
fail("{} 状态码异常 {}", dataKey, infoJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObject getFileInfoJson =
|
JsonObject getFileInfoJson =
|
||||||
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
|
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
|
||||||
getFileInfoJson.put("ShareKey", shareKey);
|
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);
|
getDownUrl(client, getFileInfoJson);
|
||||||
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
|
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
|
||||||
} else {
|
} else {
|
||||||
@@ -94,6 +139,11 @@ public class YeTool extends PanBase {
|
|||||||
|
|
||||||
JsonObject reqBodyJson = resListJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
|
JsonObject reqBodyJson = resListJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
|
||||||
reqBodyJson.put("ShareKey", shareKey);
|
reqBodyJson.put("ShareKey", shareKey);
|
||||||
|
if (reqBodyJson.getInteger("Type") == 1) {
|
||||||
|
// 文件夹
|
||||||
|
getZipDownUrl(client, reqBodyJson);
|
||||||
|
return;
|
||||||
|
}
|
||||||
getDownUrl(client, reqBodyJson);
|
getDownUrl(client, reqBodyJson);
|
||||||
}).onFailure(this.handleFail(FIRST_REQUEST_URL));
|
}).onFailure(this.handleFail(FIRST_REQUEST_URL));
|
||||||
|
|
||||||
@@ -112,6 +162,21 @@ public class YeTool extends PanBase {
|
|||||||
jsonObject.put("Etag", reqBodyJson.getString("Etag"));
|
jsonObject.put("Etag", reqBodyJson.getString("Etag"));
|
||||||
|
|
||||||
// 调用JS文件获取签名
|
// 调用JS文件获取签名
|
||||||
|
down(client, jsonObject, DOWNLOAD_API_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void getZipDownUrl(WebClient client, JsonObject reqBodyJson) {
|
||||||
|
log.info(reqBodyJson.encodePrettily());
|
||||||
|
JsonObject jsonObject = new JsonObject();
|
||||||
|
// {"ShareKey":"LH3rTd-1ENed","fileIdList":[{"fileId":17525952}]}
|
||||||
|
jsonObject.put("ShareKey", reqBodyJson.getString("ShareKey"));
|
||||||
|
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", reqBodyJson.getInteger("FileId"))));
|
||||||
|
// 调用JS文件获取签名
|
||||||
|
down(client, jsonObject, BATCH_DOWNLOAD_API_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void down(WebClient client, JsonObject jsonObject, String api) {
|
||||||
ScriptObjectMirror getSign;
|
ScriptObjectMirror getSign;
|
||||||
try {
|
try {
|
||||||
getSign = JsExecUtils.executeJs("getSign", "/a/api/share/download/info");
|
getSign = JsExecUtils.executeJs("getSign", "/a/api/share/download/info");
|
||||||
@@ -121,7 +186,7 @@ public class YeTool extends PanBase {
|
|||||||
}
|
}
|
||||||
log.info("ye getSign: {}={}", getSign.get("0").toString(), getSign.get("1").toString());
|
log.info("ye getSign: {}={}", getSign.get("0").toString(), getSign.get("1").toString());
|
||||||
|
|
||||||
client.postAbs(UriTemplate.of(DOWNLOAD_API_URL))
|
client.postAbs(UriTemplate.of(api))
|
||||||
.setTemplateParam("authK", getSign.get("0").toString())
|
.setTemplateParam("authK", getSign.get("0").toString())
|
||||||
.setTemplateParam("authV", getSign.get("1").toString())
|
.setTemplateParam("authV", getSign.get("1").toString())
|
||||||
.putHeader("Platform", "web")
|
.putHeader("Platform", "web")
|
||||||
@@ -138,7 +203,8 @@ public class YeTool extends PanBase {
|
|||||||
fail("Ye: downURLJson格式异常->" + downURLJson);
|
fail("Ye: downURLJson格式异常->" + downURLJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
|
String downURL = downURLJson.getJsonObject("data")
|
||||||
|
.getString(api.contains("batch_download_share_info")? "DownloadUrl" : "DownloadURL");
|
||||||
try {
|
try {
|
||||||
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
|
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
|
||||||
String params = urlParams.get("params");
|
String params = urlParams.get("params");
|
||||||
@@ -167,4 +233,121 @@ public class YeTool extends PanBase {
|
|||||||
}
|
}
|
||||||
}).onFailure(this.handleFail(DOWNLOAD_API_URL));
|
}).onFailure(this.handleFail(DOWNLOAD_API_URL));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// dir parser
|
||||||
|
@Override
|
||||||
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
|
Promise<List<FileInfo>> promise = Promise.promise();
|
||||||
|
|
||||||
|
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");
|
||||||
|
if (StringUtils.isNotBlank(dirId)) {
|
||||||
|
parentFileId = dirId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 构造文件列表接口的URL
|
||||||
|
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
|
||||||
|
.setTemplateParam("shareKey", shareKey)
|
||||||
|
.setTemplateParam("pwd", pwd)
|
||||||
|
.setTemplateParam("ParentFileId", parentFileId)
|
||||||
|
.putHeaders(header)
|
||||||
|
.send().onSuccess(res -> {
|
||||||
|
JsonObject response = asJson(res);
|
||||||
|
if (response.getInteger("code") != 0) {
|
||||||
|
promise.fail("API错误: " + response.getString("message"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
|
||||||
|
List<FileInfo> result = new ArrayList<>();
|
||||||
|
|
||||||
|
// 遍历返回的文件和目录信息
|
||||||
|
for (int i = 0; i < infoList.size(); i++) {
|
||||||
|
JsonObject item = infoList.getJsonObject(i);
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
// "FileId": 16603582,
|
||||||
|
// "FileName": "pdf",
|
||||||
|
// "Type": 1,
|
||||||
|
// "Size": 0,
|
||||||
|
// "ContentType": "0",
|
||||||
|
// "S3KeyFlag": "",
|
||||||
|
// "CreateAt": "2025-07-09T06:56:20+08:00",
|
||||||
|
// "UpdateAt": "2025-07-09T06:56:20+08:00",
|
||||||
|
// "Etag": "",
|
||||||
|
// "DownloadUrl": "",
|
||||||
|
// "Status": 0,
|
||||||
|
// "ParentFileId": 16603579,
|
||||||
|
// "Category": 0,
|
||||||
|
// "PunishFlag": 0,
|
||||||
|
// "StorageNode": "m0",
|
||||||
|
// "PreviewType": 0
|
||||||
|
|
||||||
|
// =>
|
||||||
|
// {
|
||||||
|
// "ShareKey":"iaKtVv-FTaCd",
|
||||||
|
// "FileID":16604189,
|
||||||
|
// "S3keyFlag":"1815268665-0",
|
||||||
|
// "Size":425929,
|
||||||
|
// "Etag":"70049de67075ab2b269c62d690424601",
|
||||||
|
// "OrderId":""}
|
||||||
|
JsonObject postData = JsonObject.of()
|
||||||
|
.put("ShareKey", shareKey)
|
||||||
|
.put("FileID", item.getInteger("FileId"))
|
||||||
|
.put("S3keyFlag", item.getString("S3KeyFlag"))
|
||||||
|
.put("Size", item.getLong("Size"))
|
||||||
|
.put("Etag", item.getString("Etag"));
|
||||||
|
|
||||||
|
byte[] encode = Base64.getEncoder().encode(postData.encode().getBytes());
|
||||||
|
String param = new String(encode);
|
||||||
|
|
||||||
|
if (item.getInteger("Type") == 0) { // 文件
|
||||||
|
fileInfo.setFileName(item.getString("FileName"))
|
||||||
|
.setFileId(item.getString("FileId"))
|
||||||
|
.setFileType("file")
|
||||||
|
.setSize(item.getLong("Size"))
|
||||||
|
.setCreateTime(item.getString("CreateAt"))
|
||||||
|
.setUpdateTime(item.getString("UpdateAt"))
|
||||||
|
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")))
|
||||||
|
.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);
|
||||||
|
} else if (item.getInteger("Type") == 1) { // 目录
|
||||||
|
fileInfo.setFileName(item.getString("FileName"))
|
||||||
|
.setFileId(item.getString("FileId"))
|
||||||
|
.setCreateTime(item.getString("CreateAt"))
|
||||||
|
.setUpdateTime(item.getString("UpdateAt"))
|
||||||
|
.setSize(0L)
|
||||||
|
.setFileType("folder")
|
||||||
|
.setParserUrl(
|
||||||
|
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
|
||||||
|
getDomainName(),
|
||||||
|
shareLinkInfo.getShareUrl(),
|
||||||
|
item.getString("FileId"),
|
||||||
|
pwd)
|
||||||
|
);
|
||||||
|
result.add(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promise.complete(result);
|
||||||
|
}).onFailure(promise::fail);
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parseById() {
|
||||||
|
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
||||||
|
// 调用下载接口获取直链
|
||||||
|
down(client, paramJson, DOWNLOAD_API_URL);
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
parser/src/main/java/cn/qaiu/util/CastUtil.java
Normal file
18
parser/src/main/java/cn/qaiu/util/CastUtil.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package cn.qaiu.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为任意类型 旨在消除泛型转换时的异常
|
||||||
|
*/
|
||||||
|
public interface CastUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 泛型转换
|
||||||
|
* @param object 要转换的object
|
||||||
|
* @param <T> T
|
||||||
|
* @return T
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T> T cast(Object object) {
|
||||||
|
return (T) object;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
parser/src/main/java/cn/qaiu/util/FileSizeConverter.java
Normal file
35
parser/src/main/java/cn/qaiu/util/FileSizeConverter.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package cn.qaiu.util;
|
||||||
|
|
||||||
|
public class FileSizeConverter {
|
||||||
|
|
||||||
|
public static long convertToBytes(String sizeStr) {
|
||||||
|
if (sizeStr == null || sizeStr.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Invalid file size string");
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeStr = sizeStr.trim().toUpperCase();
|
||||||
|
char unit = sizeStr.charAt(sizeStr.length() - 1);
|
||||||
|
double size = Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 1));
|
||||||
|
|
||||||
|
return switch (unit) {
|
||||||
|
case 'B' -> (long) size;
|
||||||
|
case 'K' -> (long) (size * 1024);
|
||||||
|
case 'M' -> (long) (size * 1024 * 1024);
|
||||||
|
case 'G' -> (long) (size * 1024 * 1024 * 1024);
|
||||||
|
default -> throw new IllegalArgumentException("Unknown file size unit: " + unit);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String convertToReadableSize(long bytes) {
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return bytes + " B";
|
||||||
|
} else if (bytes < 1024 * 1024) {
|
||||||
|
return String.format("%.1f K", bytes / 1024.0);
|
||||||
|
} else if (bytes < 1024 * 1024 * 1024) {
|
||||||
|
return String.format("%.1f M", bytes / (1024.0 * 1024));
|
||||||
|
} else {
|
||||||
|
return String.format("%.1f G", bytes / (1024.0 * 1024 * 1024));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
parser/src/main/java/cn/qaiu/util/HeaderUtils.java
Normal file
35
parser/src/main/java/cn/qaiu/util/HeaderUtils.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package cn.qaiu.util;
|
||||||
|
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
|
|
||||||
|
public class HeaderUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将请求头字符串转换为Vert.x的MultiMap对象
|
||||||
|
*
|
||||||
|
* @param headerString 请求头字符串
|
||||||
|
* @return MultiMap对象
|
||||||
|
*/
|
||||||
|
public static MultiMap parseHeaders(String headerString) {
|
||||||
|
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||||
|
|
||||||
|
if (headerString == null || headerString.isEmpty()) {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按行分割字符串
|
||||||
|
String[] lines = headerString.split("\n");
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
// 按冒号分割键值对
|
||||||
|
String[] parts = line.split(":", 2);
|
||||||
|
if (parts.length == 2) {
|
||||||
|
String key = parts[0].trim();
|
||||||
|
String value = parts[1].trim();
|
||||||
|
headers.add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,7 +124,10 @@ public interface JsContent {
|
|||||||
},
|
},
|
||||||
ajax: function (obj) {
|
ajax: function (obj) {
|
||||||
signObj = obj
|
signObj = obj
|
||||||
}
|
},
|
||||||
|
val: function(a) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -134,7 +137,6 @@ public interface JsContent {
|
|||||||
jQuery.fn.init.prototype = jQuery.fn;
|
jQuery.fn.init.prototype = jQuery.fn;
|
||||||
|
|
||||||
|
|
||||||
// 伪装jquery.ajax函数获取关键数据
|
|
||||||
$.ajax = function (obj) {
|
$.ajax = function (obj) {
|
||||||
signObj = obj
|
signObj = obj
|
||||||
}
|
}
|
||||||
@@ -142,11 +144,16 @@ public interface JsContent {
|
|||||||
var document = {
|
var document = {
|
||||||
getElementById: function (v) {
|
getElementById: function (v) {
|
||||||
return {
|
return {
|
||||||
value: 'v'
|
value: 'v',
|
||||||
|
style: {
|
||||||
|
display: ''
|
||||||
|
},
|
||||||
|
addEventListener: function() {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var window = {location: {}}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
String kwSignString = """
|
String kwSignString = """
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package cn.qaiu.util;
|
package cn.qaiu.util;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class RandomStringGenerator {
|
public class RandomStringGenerator {
|
||||||
private static final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
private static final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
@@ -19,4 +20,10 @@ public class RandomStringGenerator {
|
|||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String gen36String() {
|
||||||
|
String uuid = UUID.randomUUID().toString().toLowerCase();
|
||||||
|
// 移除短横线
|
||||||
|
return uuid.replace("-", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<logger name="io.netty" level="warn"/>
|
<logger name="io.netty" level="warn"/>
|
||||||
<logger name="io.vertx" level="info"/>
|
<logger name="io.vertx" level="info"/>
|
||||||
<logger name="com.zaxxer.hikari" level="info"/>
|
<logger name="com.zaxxer.hikari" level="info"/>
|
||||||
|
<logger name="cn.qaiu" level="debug"/>
|
||||||
<root level="info">
|
<root level="info">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
<!-- <appender-ref ref="FILE"/>-->
|
<!-- <appender-ref ref="FILE"/>-->
|
||||||
|
|||||||
4
pom.xml
4
pom.xml
@@ -27,9 +27,9 @@
|
|||||||
|
|
||||||
<vertx.version>4.5.6</vertx.version>
|
<vertx.version>4.5.6</vertx.version>
|
||||||
<org.reflections.version>0.10.2</org.reflections.version>
|
<org.reflections.version>0.10.2</org.reflections.version>
|
||||||
<lombok.version>1.18.30</lombok.version>
|
<lombok.version>1.18.38</lombok.version>
|
||||||
<slf4j.version>2.0.5</slf4j.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>
|
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
|
||||||
<jackson.version>2.14.2</jackson.version>
|
<jackson.version>2.14.2</jackson.version>
|
||||||
<logback.version>1.5.8</logback.version>
|
<logback.version>1.5.8</logback.version>
|
||||||
|
|||||||
1
web-front/.gitignore
vendored
1
web-front/.gitignore
vendored
@@ -24,3 +24,4 @@ pnpm-debug.log*
|
|||||||
/nfd-front.zip
|
/nfd-front.zip
|
||||||
/nfd-front
|
/nfd-front
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
/yarn.lock
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## 关于如何将前端项目和java一块打包:
|
## 关于如何将前端项目和java一块打包:
|
||||||
1. 先打包前端模块
|
1. 先打包前端模块
|
||||||
2. ~~打包后请将当前目录下的nfd-front目录放置在项目下webroot目录, 然后使用maven打包java模块即可~~ `npm run build` 会直接打包到后端代理目录下, 无需复制
|
2. 运行`npm run build`
|
||||||
3. 项目部署后演示页面的代理端口是6401默认使用http, 如需https可以加nginx代理, 也可以使用本项目自带的代理服务和配置证书路径
|
3. 项目部署后演示页面的代理端口是6401默认使用http, 如需https可以加nginx代理, 也可以使用本项目自带的代理服务和配置证书路径
|
||||||
|
|
||||||
## nginx配置
|
## nginx配置
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/cli-plugin-babel/preset'
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@vue/babel-plugin-transform-vue-jsx'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
|
"dev": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
@@ -15,18 +16,22 @@
|
|||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"element-plus": "^2.8.7",
|
"element-plus": "^2.8.7",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
"splitpanes": "^4.0.4",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue3-json-viewer": "^2.2.2"
|
"vue-router": "^4.5.1",
|
||||||
|
"vue3-json-viewer": "2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/eslint-parser": "^7.25.9",
|
"@babel/eslint-parser": "^7.25.9",
|
||||||
|
"@babel/plugin-transform-class-properties": "^7.26.0",
|
||||||
|
"@vue/babel-plugin-transform-vue-jsx": "^1.4.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^11.1.0",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-plugin-vue": "^9.30.0",
|
"eslint-plugin-vue": "^9.30.0",
|
||||||
"filemanager-webpack-plugin": "8.0.0"
|
"filemanager-webpack-plugin": "8.0.0"
|
||||||
},
|
},
|
||||||
@@ -48,5 +53,12 @@
|
|||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not dead"
|
"not dead"
|
||||||
]
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0 <=22.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"eslint": "^9.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
165
web-front/public/LICENSE.txt
Normal file
165
web-front/public/LICENSE.txt
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
Fonticons, Inc. (https://fontawesome.com)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Font Awesome Free License
|
||||||
|
|
||||||
|
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||||
|
commercial projects, open source projects, or really almost whatever you want.
|
||||||
|
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||||
|
|
||||||
|
The Font Awesome Free download is licensed under a Creative Commons
|
||||||
|
Attribution 4.0 International License and applies to all icons packaged
|
||||||
|
as SVG and JS file types.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Fonts: SIL OFL 1.1 License
|
||||||
|
|
||||||
|
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||||
|
packaged as web and desktop font files.
|
||||||
|
|
||||||
|
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
|
||||||
|
with Reserved Font Name: "Font Awesome".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
SIL OPEN FONT LICENSE
|
||||||
|
Version 1.1 - 26 February 2007
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting — in part or in whole — any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||||
|
non-icon files.
|
||||||
|
|
||||||
|
Copyright 2024 Fonticons, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without limitation the rights to use, copy,
|
||||||
|
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Attribution
|
||||||
|
|
||||||
|
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||||
|
Awesome Free files already contain embedded comments with sufficient
|
||||||
|
attribution, so you shouldn't need to do anything additional when using these
|
||||||
|
files normally.
|
||||||
|
|
||||||
|
We've kept attribution comments terse, so we ask that you do not actively work
|
||||||
|
to remove them from files, especially code. They're a great way for folks to
|
||||||
|
learn about Font Awesome.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Brand Icons
|
||||||
|
|
||||||
|
All brand icons are trademarks of their respective owners. The use of these
|
||||||
|
trademarks does not indicate endorsement of the trademark holder by Font
|
||||||
|
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||||
|
to represent the company, product, or service to which they refer.**
|
||||||
9
web-front/public/css/all.min.css
vendored
Normal file
9
web-front/public/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
web-front/public/images/logo.jpg
Normal file
BIN
web-front/public/images/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -9,6 +9,8 @@
|
|||||||
content="Netdisk fast download,网盘直链解析工具">
|
content="Netdisk fast download,网盘直链解析工具">
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="Netdisk fast download 网盘直链解析工具">
|
content="Netdisk fast download 网盘直链解析工具">
|
||||||
|
<!-- Font Awesome 图标库 -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<style>
|
<style>
|
||||||
.page-loading-wrap {
|
.page-loading-wrap {
|
||||||
padding: 120px;
|
padding: 120px;
|
||||||
|
|||||||
693
web-front/public/list.html
Normal file
693
web-front/public/list.html
Normal file
@@ -0,0 +1,693 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>网盘目录管理系统</title>
|
||||||
|
<!-- 本地引用Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="./css/all.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 80px;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #3498db, #2c3e50);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #eaeaea;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item:hover {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item i {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 10px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover .item-icon {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder .item-icon {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image .item-icon {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document .item-icon {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive .item-icon {
|
||||||
|
color: #9b59b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio .item-icon {
|
||||||
|
color: #1abc9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video .item-icon {
|
||||||
|
color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code .item-icon {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #95a5a6;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px 20px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 5rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 300px;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 5px solid rgba(52, 152, 219, 0.2);
|
||||||
|
border-top: 5px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn i {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-view {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 15px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.grid-view {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1><i class="fas fa-cloud"></i> 网盘目录管理系统</h1>
|
||||||
|
<p class="subtitle">管理您的文件与文件夹,操作简单直观</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="breadcrumb" id="breadcrumb">
|
||||||
|
<!-- 面包屑导航会通过JS动态生成 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="grid-view" id="file-grid">
|
||||||
|
<!-- 文件列表会通过JS动态生成 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-bar">
|
||||||
|
<button class="btn" id="back-btn">
|
||||||
|
<i class="fas fa-arrow-left"></i> 返回上一级
|
||||||
|
</button>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<i class="fas fa-folder"></i> <span id="folder-count">0</span> 个文件夹
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<i class="fas fa-file"></i> <span id="file-count">0</span> 个文件
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 文件类型映射
|
||||||
|
const fileTypeIcons = {
|
||||||
|
// 图片
|
||||||
|
'jpg': { icon: 'fa-file-image', type: 'image' },
|
||||||
|
'jpeg': { icon: 'fa-file-image', type: 'image' },
|
||||||
|
'png': { icon: 'fa-file-image', type: 'image' },
|
||||||
|
'gif': { icon: 'fa-file-image', type: 'image' },
|
||||||
|
'bmp': { icon: 'fa-file-image', type: 'image' },
|
||||||
|
'svg': { icon: 'fa-file-image', type: 'image' },
|
||||||
|
'webp': { icon: 'fa-file-image', type: 'image' },
|
||||||
|
|
||||||
|
// 文档
|
||||||
|
'pdf': { icon: 'fa-file-pdf', type: 'document' },
|
||||||
|
'doc': { icon: 'fa-file-word', type: 'document' },
|
||||||
|
'docx': { icon: 'fa-file-word', type: 'document' },
|
||||||
|
'xls': { icon: 'fa-file-excel', type: 'document' },
|
||||||
|
'xlsx': { icon: 'fa-file-excel', type: 'document' },
|
||||||
|
'ppt': { icon: 'fa-file-powerpoint', type: 'document' },
|
||||||
|
'pptx': { icon: 'fa-file-powerpoint', type: 'document' },
|
||||||
|
'txt': { icon: 'fa-file-alt', type: 'document' },
|
||||||
|
'rtf': { icon: 'fa-file-alt', type: 'document' },
|
||||||
|
|
||||||
|
// 压缩文件
|
||||||
|
'zip': { icon: 'fa-file-archive', type: 'archive' },
|
||||||
|
'rar': { icon: 'fa-file-archive', type: 'archive' },
|
||||||
|
'7z': { icon: 'fa-file-archive', type: 'archive' },
|
||||||
|
'tar': { icon: 'fa-file-archive', type: 'archive' },
|
||||||
|
'gz': { icon: 'fa-file-archive', type: 'archive' },
|
||||||
|
|
||||||
|
// 音频
|
||||||
|
'mp3': { icon: 'fa-file-audio', type: 'audio' },
|
||||||
|
'wav': { icon: 'fa-file-audio', type: 'audio' },
|
||||||
|
'ogg': { icon: 'fa-file-audio', type: 'audio' },
|
||||||
|
'flac': { icon: 'fa-file-audio', type: 'audio' },
|
||||||
|
|
||||||
|
// 视频
|
||||||
|
'mp4': { icon: 'fa-file-video', type: 'video' },
|
||||||
|
'avi': { icon: 'fa-file-video', type: 'video' },
|
||||||
|
'mov': { icon: 'fa-file-video', type: 'video' },
|
||||||
|
'wmv': { icon: 'fa-file-video', type: 'video' },
|
||||||
|
'mkv': { icon: 'fa-file-video', type: 'video' },
|
||||||
|
'flv': { icon: 'fa-file-video', type: 'video' },
|
||||||
|
|
||||||
|
// 代码
|
||||||
|
'html': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'htm': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'css': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'js': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'json': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'php': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'py': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'java': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'c': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'cpp': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'h': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'sh': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'bat': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
'md': { icon: 'fa-file-code', type: 'code' },
|
||||||
|
|
||||||
|
// 默认
|
||||||
|
'default': { icon: 'fa-file', type: 'document' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const obj = new URL(window.location.href);
|
||||||
|
// 获取 URL 参数
|
||||||
|
const params = obj.searchParams;
|
||||||
|
const shareUrl = params.get('url');
|
||||||
|
const pwd = params.get('pwd');
|
||||||
|
// 动态拼接并编码参数
|
||||||
|
const apiUrl = `${window.location.origin}/v2/getFileList?url=${encodeURIComponent(shareUrl)}&pwd=${encodeURIComponent(pwd)}`;
|
||||||
|
|
||||||
|
// 当前目录状态
|
||||||
|
let currentDir = {
|
||||||
|
// url: 'http://192.168.101.227:6401/v2/getFileList?url=https://share.feijipan.com/s/3pMsofZd&pwd=qaiu',
|
||||||
|
// 动态获取url encode 参数
|
||||||
|
url: apiUrl,
|
||||||
|
name: '全部文件'
|
||||||
|
};
|
||||||
|
const pathStack = [currentDir];
|
||||||
|
|
||||||
|
// DOM 元素
|
||||||
|
const breadcrumbEl = document.getElementById('breadcrumb');
|
||||||
|
const fileGridEl = document.getElementById('file-grid');
|
||||||
|
const backBtn = document.getElementById('back-btn');
|
||||||
|
const folderCountEl = document.getElementById('folder-count');
|
||||||
|
const fileCountEl = document.getElementById('file-count');
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
renderBreadcrumb();
|
||||||
|
fetchFileList(currentDir.url);
|
||||||
|
|
||||||
|
// 返回上一级按钮事件
|
||||||
|
backBtn.addEventListener('click', goBack);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染面包屑导航
|
||||||
|
function renderBreadcrumb() {
|
||||||
|
breadcrumbEl.innerHTML = '';
|
||||||
|
|
||||||
|
pathStack.forEach((item, index) => {
|
||||||
|
const itemEl = document.createElement('div');
|
||||||
|
itemEl.className = 'breadcrumb-item';
|
||||||
|
itemEl.textContent = item.name;
|
||||||
|
|
||||||
|
if (index < pathStack.length - 1) {
|
||||||
|
itemEl.addEventListener('click', () => {
|
||||||
|
// 点击面包屑项返回对应目录
|
||||||
|
goToDirectory(index);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
itemEl.style.cursor = 'default';
|
||||||
|
itemEl.style.fontWeight = '600';
|
||||||
|
itemEl.style.color = '#2c3e50';
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbEl.appendChild(itemEl);
|
||||||
|
|
||||||
|
// 如果不是最后一个,添加分隔符
|
||||||
|
if (index < pathStack.length - 1) {
|
||||||
|
const separator = document.createElement('i');
|
||||||
|
separator.className = 'fas fa-chevron-right';
|
||||||
|
breadcrumbEl.appendChild(separator);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件列表
|
||||||
|
async function fetchFileList(url) {
|
||||||
|
try {
|
||||||
|
// 显示加载状态
|
||||||
|
fileGridEl.innerHTML = `
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 200 && data.success) {
|
||||||
|
renderFileList(data.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(data.msg || '获取文件列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文件列表失败:', error);
|
||||||
|
fileGridEl.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<h3>加载失败</h3>
|
||||||
|
<p>${error.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染文件列表
|
||||||
|
function renderFileList(files) {
|
||||||
|
fileGridEl.innerHTML = '';
|
||||||
|
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
fileGridEl.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
<h3>此文件夹为空</h3>
|
||||||
|
<p>暂无文件或文件夹</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
folderCountEl.textContent = '0';
|
||||||
|
fileCountEl.textContent = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let folderCount = 0;
|
||||||
|
let fileCount = 0;
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
|
||||||
|
if (file.fileType === 'folder') {
|
||||||
|
// 文件夹
|
||||||
|
item.className = 'item folder';
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="item-icon">
|
||||||
|
<i class="fas fa-folder"></i>
|
||||||
|
</div>
|
||||||
|
<div class="item-name">${file.fileName || '未命名文件夹'}</div>
|
||||||
|
<div class="item-meta">
|
||||||
|
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
folderCount++;
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
enterFolder(file);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 文件
|
||||||
|
const fileExt = getFileExtension(file.fileName);
|
||||||
|
const fileTypeInfo = fileTypeIcons[fileExt.toLowerCase()] || fileTypeIcons['default'];
|
||||||
|
|
||||||
|
item.className = `item ${fileTypeInfo.type}`;
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="item-icon">
|
||||||
|
<i class="fas ${fileTypeInfo.icon}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="item-name">${file.fileName}</div>
|
||||||
|
<div class="item-meta">
|
||||||
|
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
fileCount++;
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
handleFileClick(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fileGridEl.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
folderCountEl.textContent = folderCount;
|
||||||
|
fileCountEl.textContent = fileCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
function getFileExtension(filename) {
|
||||||
|
if (!filename) return '';
|
||||||
|
return filename.split('.').pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入文件夹
|
||||||
|
function enterFolder(folder) {
|
||||||
|
if (!folder.parserUrl) {
|
||||||
|
alert('无法进入该文件夹,缺少访问链接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDir = {
|
||||||
|
url: folder.parserUrl,
|
||||||
|
name: folder.fileName || '未命名文件夹'
|
||||||
|
};
|
||||||
|
|
||||||
|
pathStack.push(newDir);
|
||||||
|
currentDir = newDir;
|
||||||
|
|
||||||
|
fetchFileList(currentDir.url);
|
||||||
|
renderBreadcrumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
function handleFileClick(file) {
|
||||||
|
if (!file.parserUrl) {
|
||||||
|
alert('无法操作该文件,缺少必要链接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更友好的选择对话框
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.style.position = 'fixed';
|
||||||
|
modal.style.top = '0';
|
||||||
|
modal.style.left = '0';
|
||||||
|
modal.style.width = '100%';
|
||||||
|
modal.style.height = '100%';
|
||||||
|
modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modal.style.justifyContent = 'center';
|
||||||
|
modal.style.alignItems = 'center';
|
||||||
|
modal.style.zIndex = '1000';
|
||||||
|
|
||||||
|
const dialog = document.createElement('div');
|
||||||
|
dialog.style.backgroundColor = 'white';
|
||||||
|
dialog.style.padding = '20px';
|
||||||
|
dialog.style.borderRadius = '8px';
|
||||||
|
dialog.style.width = '300px';
|
||||||
|
dialog.style.textAlign = 'center';
|
||||||
|
|
||||||
|
dialog.innerHTML = `
|
||||||
|
<p style="margin-bottom: 20px;">${file.fileName || '未命名文件'}</p>
|
||||||
|
<div style="display: flex; justify-content: center; gap: 15px;">
|
||||||
|
<button id="preview-btn" style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
预览文件
|
||||||
|
</button>
|
||||||
|
<button id="download-btn" style="padding: 8px 15px; background: #2ecc71; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
下载文件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.appendChild(dialog);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// 预览按钮事件
|
||||||
|
dialog.querySelector('#preview-btn').addEventListener('click', () => {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
const previewUrl = file.previewUrl || file.parserUrl;
|
||||||
|
window.open(previewUrl, '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载按钮事件
|
||||||
|
dialog.querySelector('#download-btn').addEventListener('click', () => {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
iframe.src = file.parserUrl;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击蒙层关闭
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一级
|
||||||
|
function goBack() {
|
||||||
|
if (pathStack.length > 1) {
|
||||||
|
pathStack.pop();
|
||||||
|
currentDir = pathStack[pathStack.length - 1];
|
||||||
|
|
||||||
|
fetchFileList(currentDir.url);
|
||||||
|
renderBreadcrumb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到指定目录
|
||||||
|
function goToDirectory(index) {
|
||||||
|
pathStack.splice(index + 1);
|
||||||
|
currentDir = pathStack[pathStack.length - 1];
|
||||||
|
|
||||||
|
fetchFileList(currentDir.url);
|
||||||
|
renderBreadcrumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return '未知日期';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return isNaN(date.getTime())
|
||||||
|
? '未知日期'
|
||||||
|
: `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||||
|
} catch {
|
||||||
|
return '未知日期';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
web-front/public/webfonts/fa-brands-400.ttf
Normal file
BIN
web-front/public/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
web-front/public/webfonts/fa-brands-400.woff2
Normal file
BIN
web-front/public/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
web-front/public/webfonts/fa-regular-400.ttf
Normal file
BIN
web-front/public/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
web-front/public/webfonts/fa-regular-400.woff2
Normal file
BIN
web-front/public/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
web-front/public/webfonts/fa-solid-900.ttf
Normal file
BIN
web-front/public/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
web-front/public/webfonts/fa-solid-900.woff2
Normal file
BIN
web-front/public/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
web-front/public/webfonts/fa-v4compatibility.ttf
Normal file
BIN
web-front/public/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
web-front/public/webfonts/fa-v4compatibility.woff2
Normal file
BIN
web-front/public/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
@@ -1,367 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
<router-view></router-view>
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-card class="box-card">
|
|
||||||
|
|
||||||
<div style="text-align: right"><DarkMode/></div>
|
|
||||||
<div class="demo-basic--circle">
|
|
||||||
<div class="block" style="text-align: center;">
|
|
||||||
<img :height="150" src="../public/images/lanzou111.png" alt="lz"></img>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 style="text-align: center;">NFD网盘直链解析0.1.8_bate3(API演示)</h3>
|
|
||||||
<div class="typo">
|
|
||||||
<p style="text-align: center;">
|
|
||||||
<span>
|
|
||||||
<el-link href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="nofollow">
|
|
||||||
<u>GitHub</u></el-link>
|
|
||||||
</span>
|
|
||||||
<span style="margin-left: 30px">
|
|
||||||
<el-link href="https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng" target="_blank"
|
|
||||||
rel="nofollow"><u>宝塔部署安装教程</u>
|
|
||||||
</el-link>
|
|
||||||
</span>
|
|
||||||
<span style="margin-left: 30px">
|
|
||||||
<el-link href="https://blog.qaiu.top" target="_blank"
|
|
||||||
rel="nofollow"><u>QAIU博客</u>
|
|
||||||
</el-link>
|
|
||||||
</span></p>
|
|
||||||
<p><strong>目前支持 </strong>蓝奏云/蓝奏云优享/小飞机盘/123云盘/奶牛快传/移动云云空间/亿方云/文叔叔/QQ邮箱文件中转站</p>
|
|
||||||
<p>已加入缓存机制, 如果遇到解析出的下载链接失效的情况请及时到<a href="https://github.com/qaiu/netdisk-fast-download/issues">
|
|
||||||
<u><strong>项目GitHub反馈</strong></u></a></p>
|
|
||||||
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
|
|
||||||
<!-- <p>节点2: 成功:{{ node2Info.success }},失败:{{ node2Info.fail }},总数:{{ node2Info.total }}</p>-->
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<div class="main" v-loading="isLoading">
|
|
||||||
<div class="grid-content">
|
|
||||||
|
|
||||||
<!-- 开关按钮,控制是否自动读取剪切板 -->
|
|
||||||
<el-switch
|
|
||||||
v-model="autoReadClipboard"
|
|
||||||
active-text="自动识别剪切板"
|
|
||||||
></el-switch>
|
|
||||||
|
|
||||||
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
|
|
||||||
<template #prepend>分享链接</template>
|
|
||||||
<template #append v-if="!autoReadClipboard">
|
|
||||||
<el-button @click="() => getPaste(1)">读取剪切板</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-input placeholder="请输入密码" v-model="password" id="url">
|
|
||||||
<template #prepend>分享密码</template>
|
|
||||||
</el-input>
|
|
||||||
<el-input v-show="getLink2" :value="getLink2" id="url">
|
|
||||||
<template #prepend>智能直链</template>
|
|
||||||
<template #append>
|
|
||||||
<el-button v-clipboard:copy="getLink2"
|
|
||||||
v-clipboard:success="onCopy"
|
|
||||||
v-clipboard:error="onError">
|
|
||||||
<el-icon><CopyDocument/></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<p style="text-align: center">
|
|
||||||
<el-button style="margin-left: 40px;margin-bottom: 10px" @click="onSubmit">解析测试</el-button>
|
|
||||||
<el-button style="margin-left: 20px;margin-bottom: 10px" @click="genMd">生成Markdown链接</el-button>
|
|
||||||
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
|
|
||||||
<el-button style="margin-left: 20px" @click="getTj">链接信息统计</el-button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="respData.code" style="margin-top: 10px">
|
|
||||||
<strong>解析结果: </strong>
|
|
||||||
<json-viewer
|
|
||||||
:value="respData"
|
|
||||||
:expand-depth=5
|
|
||||||
copyable
|
|
||||||
boxed
|
|
||||||
sort
|
|
||||||
/>
|
|
||||||
<a :href="downUrl" v-show="downUrl">点击下载</a>
|
|
||||||
</div>
|
|
||||||
<div v-if="mdText" style="text-align: center">
|
|
||||||
<el-input :value="mdText" readonly>
|
|
||||||
<template #append>
|
|
||||||
<el-button v-clipboard:copy="mdText"
|
|
||||||
v-clipboard:success="onCopy"
|
|
||||||
v-clipboard:error="onError">
|
|
||||||
<el-icon><CopyDocument/></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center" v-show="showQrc">
|
|
||||||
<canvas ref="qrcodeCanvas"></canvas>
|
|
||||||
<div style="text-align: center"><el-link target="_blank" :href="codeUrl">{{ codeUrl }}</el-link></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="tjData.shareLinkInfo">
|
|
||||||
<el-descriptions class="margin-top" title="分享详情" :column="1" border>
|
|
||||||
<template slot="extra">
|
|
||||||
<el-button type="primary" size="small">操作</el-button>
|
|
||||||
</template>
|
|
||||||
<el-descriptions-item label="网盘名称">{{ tjData.shareLinkInfo.panName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="网盘标识">{{ tjData.shareLinkInfo.type }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="分享Key">{{ tjData.shareLinkInfo.shareKey }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="分享链接"> <el-link target="_blank" :href="tjData.shareLinkInfo.shareUrl">{{ tjData.shareLinkInfo.shareUrl }}</el-link></el-descriptions-item>
|
|
||||||
<el-descriptions-item label="jsonApi链接"> <el-link target="_blank" :href="tjData.apiLink">{{ tjData.apiLink }}</el-link></el-descriptions-item>
|
|
||||||
<el-descriptions-item label="302下载链接"> <el-link target="_blank" :href="tjData.downLink">{{ tjData.downLink }}</el-link></el-descriptions-item>
|
|
||||||
<el-descriptions-item label="解析次数">{{ tjData.parserTotal }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="缓存命中次数">{{ tjData.cacheHitTotal }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="总请求次数">{{ tjData.sumTotal }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
|
||||||
import QRCode from 'qrcode'
|
|
||||||
import DarkMode from '@/components/DarkMode'
|
|
||||||
|
|
||||||
import parserUrl from './parserUrl1'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App'
|
||||||
components: {DarkMode},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// baseAPI: `${location.protocol}//${location.hostname}:6400`,
|
|
||||||
baseAPI: `${location.protocol}//${location.host}`,
|
|
||||||
autoReadClipboard: true, // 开关状态,默认为自动读取
|
|
||||||
current: {}, // 当前分享
|
|
||||||
showQrc: false,
|
|
||||||
codeUrl: '',
|
|
||||||
mdText: '',
|
|
||||||
link: "",
|
|
||||||
password: "",
|
|
||||||
isLoading: false,
|
|
||||||
downUrl: null,
|
|
||||||
select: "lz",
|
|
||||||
respData: {},
|
|
||||||
tjData: {},
|
|
||||||
panList: [
|
|
||||||
{
|
|
||||||
name: "蓝奏云",
|
|
||||||
value: 'lz'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "奶牛快传",
|
|
||||||
value: 'cow'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "移动云空间",
|
|
||||||
value: 'ec'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "UC网盘",
|
|
||||||
value: 'uc',
|
|
||||||
disabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "小飞机网盘",
|
|
||||||
value: 'fj'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "360亿方云",
|
|
||||||
value: 'fc'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "123云盘",
|
|
||||||
value: 'ye'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
getLink: null,
|
|
||||||
getLink2: '',
|
|
||||||
getLinkInfo: null,
|
|
||||||
node1Info: {},
|
|
||||||
node2Info: {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// toggleDark() {
|
|
||||||
// toggleDark()
|
|
||||||
// },
|
|
||||||
check() {
|
|
||||||
this.mdText = ''
|
|
||||||
this.showQrc = false
|
|
||||||
this.respData = {}
|
|
||||||
this.tjData = {}
|
|
||||||
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
|
|
||||||
this.$message.error("请输入有效链接!")
|
|
||||||
throw new Error('请输入有效链接')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSubmit() {
|
|
||||||
this.check()
|
|
||||||
this.isLoading = true
|
|
||||||
this.downUrl = ''
|
|
||||||
this.respData = {}
|
|
||||||
this.getLink2 = `${this.baseAPI}/parser?url=${this.link}`
|
|
||||||
// this.getLink = `${location.protocol}//${location.host}/api/json/parser?url=${this.link}`
|
|
||||||
// this.getLink = `${location.protocol}//${location.host}/json/parser`
|
|
||||||
if (this.password) {
|
|
||||||
this.getLink2 += `&pwd=${this.password}`
|
|
||||||
}
|
|
||||||
axios.get(this.getLink, {params: {url: this.link, pwd: this.password}}).then(
|
|
||||||
response => {
|
|
||||||
this.isLoading = false
|
|
||||||
this.respData = response.data
|
|
||||||
if (response.data.code === 200) {
|
|
||||||
this.$message({
|
|
||||||
message: response.data.msg,
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
this.downUrl = response.data.data.directLink
|
|
||||||
} else {
|
|
||||||
this.$message.error(response.data.msg)
|
|
||||||
}
|
|
||||||
this.getInfo()
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
this.isLoading = false
|
|
||||||
this.$message.error(error.message)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onCopy() {
|
|
||||||
this.$message.success('复制成功')
|
|
||||||
},
|
|
||||||
onError() {
|
|
||||||
this.$message.error('复制失败')
|
|
||||||
},
|
|
||||||
getInfo() {
|
|
||||||
// 初始化统计信息
|
|
||||||
axios.get('/v2/statisticsInfo').then(
|
|
||||||
response => {
|
|
||||||
if (response.data.success) {
|
|
||||||
this.node1Info = response.data.data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// axios.get('/n2/statisticsInfo').then(
|
|
||||||
// response => {
|
|
||||||
// if (response.data.success) {
|
|
||||||
// this.node2Info = response.data.data
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
},
|
|
||||||
genMd() {
|
|
||||||
this.check()
|
|
||||||
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
|
|
||||||
response => {
|
|
||||||
this.isLoading = false
|
|
||||||
if (response.data.code === 200) {
|
|
||||||
this.$message({
|
|
||||||
message: response.data.msg,
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
this.mdText = this.buildMd('快速下载地址',response.data.data.downLink)
|
|
||||||
} else {
|
|
||||||
this.$message.error(response.data.msg)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
buildMd(title, url) {
|
|
||||||
return `[${title}](${url})`
|
|
||||||
},
|
|
||||||
generateQRCode() {
|
|
||||||
this.check()
|
|
||||||
const options = { // 设置二维码的参数,例如大小、边距等
|
|
||||||
width: 150,
|
|
||||||
height: 150,
|
|
||||||
margin: 2
|
|
||||||
};
|
|
||||||
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
|
|
||||||
response => {
|
|
||||||
this.isLoading = false
|
|
||||||
if (response.data.code === 200) {
|
|
||||||
this.$message({
|
|
||||||
message: response.data.msg,
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
this.codeUrl = response.data.data.downLink
|
|
||||||
QRCode.toCanvas(this.$refs.qrcodeCanvas, this.codeUrl, options, error => {
|
|
||||||
if (error) console.error(error);
|
|
||||||
});
|
|
||||||
this.showQrc = true
|
|
||||||
} else {
|
|
||||||
this.$message.error(response.data.msg)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getTj() {
|
|
||||||
this.check()
|
|
||||||
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
|
|
||||||
response => {
|
|
||||||
this.isLoading = false
|
|
||||||
if (response.data.code === 200) {
|
|
||||||
this.$message({
|
|
||||||
message: response.data.msg,
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
this.tjData = response.data.data
|
|
||||||
} else {
|
|
||||||
this.$message.error(response.data.msg)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async getPaste(v) {
|
|
||||||
const text = await navigator.clipboard.readText();
|
|
||||||
console.log('获取到的文本内容是:', text);
|
|
||||||
let linkInfo = parserUrl.parseLink(text);
|
|
||||||
let pwd = parserUrl.parsePwd(text) || '';
|
|
||||||
if (linkInfo.link) {
|
|
||||||
if(linkInfo.link !== this.link || pwd !== this.password ) {
|
|
||||||
this.password = pwd;
|
|
||||||
this.link = linkInfo.link;
|
|
||||||
this.getLink2 = `${this.baseAPI}/parser?url=${this.link}`
|
|
||||||
if (this.link) this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`);
|
|
||||||
} else {
|
|
||||||
v || this.$message.warning(`[${linkInfo.name}]分享信息无变化`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$message.warning("未能提取到分享链接, 该分享可能尚未支持, 你可以复制任意网盘/音乐App的分享到该页面, 系统智能识别")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getLinkInfo = `${this.baseAPI}/v2/linkInfo`
|
|
||||||
this.getLink = `${this.baseAPI}/json/parser`
|
|
||||||
let item = window.localStorage.getItem("autoReadClipboard");
|
|
||||||
if (item) {
|
|
||||||
this.autoReadClipboard = (item === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getInfo()
|
|
||||||
|
|
||||||
// 页面首次加载时,根据开关状态判断是否读取剪切板
|
|
||||||
if (this.autoReadClipboard) {
|
|
||||||
this.getPaste()
|
|
||||||
}
|
|
||||||
// 当文档获得焦点时触发
|
|
||||||
window.addEventListener('focus', () => {
|
|
||||||
if (this.autoReadClipboard) {
|
|
||||||
this.getPaste()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
autoReadClipboard(val) {
|
|
||||||
window.localStorage.setItem("autoReadClipboard", val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
#app {
|
#app {
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -371,62 +25,12 @@ export default {
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
body:before {
|
padding: 0;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
opacity: .3;
|
|
||||||
z-index: -1;
|
|
||||||
position: fixed;
|
|
||||||
}
|
}
|
||||||
|
nav li {
|
||||||
.grid-content {
|
display: inline;
|
||||||
margin-top: 1em;
|
margin-right: 15px;
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select .el-input {
|
|
||||||
width: 130px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-card {
|
|
||||||
margin-top: 4em !important;
|
|
||||||
margin-bottom: 4em !important;
|
|
||||||
opacity: .8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 700px) {
|
|
||||||
.box-card {
|
|
||||||
margin-top: 1em !important;
|
|
||||||
margin-bottom: 1em !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download h3 {
|
|
||||||
margin-top: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download button {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typo {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typo a {
|
|
||||||
color: #0077ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
height: 10px;
|
|
||||||
margin-bottom: .8em;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, .12);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,14 +11,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref,watch } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import { useDark, useToggle } from '@vueuse/core'
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
/** 引入Element-Plus图标 */
|
/** 引入Element-Plus图标 */
|
||||||
import { Sunny, Moon } from '@element-plus/icons-vue'
|
import { Sunny, Moon } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'DarkMode'
|
name: 'DarkMode'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['theme-change'])
|
||||||
|
|
||||||
/** 切换模式 */
|
/** 切换模式 */
|
||||||
const isDark = useDark({})
|
const isDark = useDark({})
|
||||||
|
|
||||||
@@ -30,8 +34,32 @@ if (item) {
|
|||||||
}
|
}
|
||||||
/** 是否切换为暗黑模式 */
|
/** 是否切换为暗黑模式 */
|
||||||
const darkMode = ref(item)
|
const darkMode = ref(item)
|
||||||
|
|
||||||
watch(darkMode, (newValue) => {
|
watch(darkMode, (newValue) => {
|
||||||
console.log(`darkMode: ${newValue}`)
|
console.log(`darkMode: ${newValue}`)
|
||||||
window.localStorage.setItem("darkMode", newValue);
|
window.localStorage.setItem("darkMode", newValue);
|
||||||
|
|
||||||
|
// 发射主题变化事件
|
||||||
|
emit('theme-change', newValue)
|
||||||
|
|
||||||
|
// 应用主题到body
|
||||||
|
if (newValue) {
|
||||||
|
document.body.classList.add('dark-theme')
|
||||||
|
document.documentElement.classList.add('dark-theme')
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark-theme')
|
||||||
|
document.documentElement.classList.remove('dark-theme')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化时发射当前主题状态
|
||||||
|
emit('theme-change', darkMode.value)
|
||||||
|
|
||||||
|
// 应用初始主题
|
||||||
|
if (darkMode.value) {
|
||||||
|
document.body.classList.add('dark-theme')
|
||||||
|
document.documentElement.classList.add('dark-theme')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
993
web-front/src/components/DirectoryTree.vue
Normal file
993
web-front/src/components/DirectoryTree.vue
Normal file
@@ -0,0 +1,993 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="directory-tree" :class="{ 'dark-theme': isDarkTheme }">
|
||||||
|
<template v-if="viewMode === 'pane'">
|
||||||
|
<!-- 窗格模式(原有) -->
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in pathStack"
|
||||||
|
:key="index"
|
||||||
|
class="breadcrumb-item"
|
||||||
|
:class="{ 'active': index === pathStack.length - 1 }"
|
||||||
|
@click="goToDirectory(index)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-folder" v-if="index === 0"></i>
|
||||||
|
<i class="fas fa-chevron-right" v-else-if="index > 0"></i>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-grid" v-loading="loading">
|
||||||
|
<div
|
||||||
|
v-for="file in currentFileList"
|
||||||
|
:key="file.fileName"
|
||||||
|
class="file-item"
|
||||||
|
:class="getFileTypeClass(file)"
|
||||||
|
@click="handleFileClick(file)"
|
||||||
|
>
|
||||||
|
<div class="file-icon">
|
||||||
|
<i :class="getFileIcon(file)"></i>
|
||||||
|
</div>
|
||||||
|
<div class="file-name">{{ file.fileName }}</div>
|
||||||
|
<div class="file-meta">
|
||||||
|
{{ file.sizeStr || '0B' }} · {{ formatDate(file.createTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading && (!currentFileList || currentFileList.length === 0)" class="empty-state">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
<h3>此文件夹为空</h3>
|
||||||
|
<p>暂无文件或文件夹</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="goBack"
|
||||||
|
:disabled="pathStack.length <= 1"
|
||||||
|
icon="el-icon-arrow-left"
|
||||||
|
>
|
||||||
|
返回上一级
|
||||||
|
</el-button>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat-item">
|
||||||
|
<i class="fas fa-folder"></i> {{ folderCount }} 个文件夹
|
||||||
|
</span>
|
||||||
|
<span class="stat-item">
|
||||||
|
<i class="fas fa-file"></i> {{ fileCount }} 个文件
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="viewMode === 'tree'">
|
||||||
|
<div class="content-card">
|
||||||
|
<splitpanes class="split-theme custom-splitpanes" style="height:100%;">
|
||||||
|
<pane>
|
||||||
|
<div class="tree-sidebar">
|
||||||
|
<el-tree
|
||||||
|
:data="treeData"
|
||||||
|
:props="treeProps"
|
||||||
|
node-key="id"
|
||||||
|
lazy
|
||||||
|
:load="loadNode"
|
||||||
|
highlight-current
|
||||||
|
@node-click="onNodeClick"
|
||||||
|
:default-expand-all="false"
|
||||||
|
:default-expanded-keys="['root']"
|
||||||
|
:render-content="renderContent"
|
||||||
|
style="background:transparent;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</pane>
|
||||||
|
<pane>
|
||||||
|
<div class="tree-content">
|
||||||
|
<div v-if="selectedNode">
|
||||||
|
<div class="file-detail-icon-wrap">
|
||||||
|
<i :class="getFileIcon(selectedNode)" class="file-detail-icon"></i>
|
||||||
|
</div>
|
||||||
|
<h4>{{ selectedNode.fileName }}</h4>
|
||||||
|
<div v-if="selectedNode.fileType === 'folder'">
|
||||||
|
<ul>
|
||||||
|
<li v-for="file in selectedNode.children || []" :key="file.id">
|
||||||
|
<i :class="getFileIcon(file)"></i> {{ file.fileName }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p>类型: {{ getFileTypeClass(selectedNode) }}</p>
|
||||||
|
<p>大小: {{ selectedNode.sizeStr || '0B' }}</p>
|
||||||
|
<p>创建时间: {{ formatDate(selectedNode.createTime) }}</p>
|
||||||
|
<!-- 文件详情区下载按钮 -->
|
||||||
|
<el-button v-if="selectedNode && selectedNode.parserUrl" @click="previewFile(selectedNode)">打开</el-button>
|
||||||
|
<a
|
||||||
|
v-if="selectedNode && selectedNode.parserUrl"
|
||||||
|
:href="selectedNode.parserUrl"
|
||||||
|
download
|
||||||
|
target="_blank"
|
||||||
|
class="el-button el-button--success"
|
||||||
|
style="margin-left: 8px;"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else style="color: #888;">请选择左侧文件或文件夹</div>
|
||||||
|
</div>
|
||||||
|
</pane>
|
||||||
|
</splitpanes>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 文件操作对话框(窗格模式下) -->
|
||||||
|
<el-dialog
|
||||||
|
v-if="viewMode === 'pane'"
|
||||||
|
title="文件操作"
|
||||||
|
v-model="fileDialogVisible"
|
||||||
|
width="400px"
|
||||||
|
:before-close="closeFileDialog"
|
||||||
|
>
|
||||||
|
<div class="file-dialog-content">
|
||||||
|
<p><strong>{{ selectedFile?.fileName || '未命名文件' }}</strong></p>
|
||||||
|
<p class="file-info">
|
||||||
|
大小: {{ selectedFile?.sizeStr || '0B' }}<br>
|
||||||
|
创建时间: {{ formatDate(selectedFile?.createTime) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="previewFile(selectedFile)">打开</el-button>
|
||||||
|
<!-- 弹窗下载按钮 -->
|
||||||
|
<a
|
||||||
|
v-if="selectedFile && selectedFile.parserUrl"
|
||||||
|
:href="selectedFile.parserUrl"
|
||||||
|
download
|
||||||
|
target="_blank"
|
||||||
|
class="el-button el-button--success"
|
||||||
|
style="margin-left: 8px;"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
<div v-if="isPreviewing" class="preview-mask">
|
||||||
|
<div class="preview-toolbar">
|
||||||
|
<el-button size="small" @click="closePreview">关闭预览</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="openPreviewInNewTab">新窗口打开</el-button>
|
||||||
|
</div>
|
||||||
|
<iframe :src="previewUrl" frameborder="0" class="preview-iframe"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElTree } from 'element-plus'
|
||||||
|
import { Splitpanes, Pane } from 'splitpanes'
|
||||||
|
import 'splitpanes/dist/splitpanes.css'
|
||||||
|
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DirectoryTree',
|
||||||
|
components: { ElTree, Splitpanes, Pane },
|
||||||
|
props: {
|
||||||
|
fileList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
shareUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
viewMode: {
|
||||||
|
type: String,
|
||||||
|
default: 'pane' // 'pane' or 'tree'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
pathStack: [{ name: '全部文件', url: '' }],
|
||||||
|
currentFileList: [],
|
||||||
|
fileDialogVisible: false,
|
||||||
|
selectedFile: null,
|
||||||
|
isDarkTheme: false,
|
||||||
|
initialized: false,
|
||||||
|
// 文件树模式相关
|
||||||
|
treeData: [],
|
||||||
|
selectedNode: null,
|
||||||
|
isPreviewing: false,
|
||||||
|
previewUrl: '',
|
||||||
|
treeProps: {
|
||||||
|
label: 'fileName',
|
||||||
|
children: 'children',
|
||||||
|
isLeaf: 'isLeaf'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
folderCount() {
|
||||||
|
return this.currentFileList.filter(file => file.fileType === 'folder').length
|
||||||
|
},
|
||||||
|
fileCount() {
|
||||||
|
return this.currentFileList.filter(file => file.fileType !== 'folder').length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
fileList: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newList) {
|
||||||
|
// 根节点children为当前目录下所有文件/文件夹
|
||||||
|
this.treeData = [
|
||||||
|
{
|
||||||
|
id: 'root',
|
||||||
|
fileName: '全部文件',
|
||||||
|
fileType: 'folder',
|
||||||
|
children: (newList || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
isLeaf: item.fileType !== 'folder'
|
||||||
|
})),
|
||||||
|
isLeaf: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
this.currentFileList = newList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...fileTypeUtils,
|
||||||
|
// 构建API URL
|
||||||
|
buildApiUrl() {
|
||||||
|
const baseUrl = `${window.location.origin}/v2/getFileList`
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
url: this.shareUrl
|
||||||
|
})
|
||||||
|
if (this.password) {
|
||||||
|
params.append('pwd', this.password)
|
||||||
|
}
|
||||||
|
return `${baseUrl}?${params.toString()}`
|
||||||
|
},
|
||||||
|
// 文件树与窗格同源:直接返回当前目录数据
|
||||||
|
buildTree(list) {
|
||||||
|
return list || []
|
||||||
|
},
|
||||||
|
// 懒加载子节点
|
||||||
|
loadNode(node, resolve) {
|
||||||
|
if (node.level === 0) {
|
||||||
|
// 根节点
|
||||||
|
resolve(this.treeData[0].children)
|
||||||
|
} else if (node.data.fileType === 'folder' && node.data.parserUrl) {
|
||||||
|
axios.get(node.data.parserUrl).then(res => {
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
const children = (res.data.data || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
isLeaf: item.fileType !== 'folder'
|
||||||
|
}))
|
||||||
|
resolve(children)
|
||||||
|
} else {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
}).catch(() => resolve([]))
|
||||||
|
} else {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNodeClick(data) {
|
||||||
|
this.selectedNode = data
|
||||||
|
},
|
||||||
|
// 处理文件点击
|
||||||
|
handleFileClick(file) {
|
||||||
|
console.log('点击文件', file, this.viewMode)
|
||||||
|
if (file.fileType === 'folder') {
|
||||||
|
this.enterFolder(file)
|
||||||
|
} else if (this.viewMode === 'pane') {
|
||||||
|
this.selectedFile = file
|
||||||
|
this.fileDialogVisible = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 进入文件夹
|
||||||
|
async enterFolder(folder) {
|
||||||
|
if (!folder.parserUrl) {
|
||||||
|
this.$message.error('无法进入该文件夹,缺少访问链接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
const response = await axios.get(folder.parserUrl)
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
const newDir = {
|
||||||
|
url: folder.parserUrl,
|
||||||
|
name: folder.fileName || '未命名文件夹'
|
||||||
|
}
|
||||||
|
this.pathStack.push(newDir)
|
||||||
|
this.currentFileList = response.data.data || []
|
||||||
|
} else {
|
||||||
|
this.$message.error(response.data.msg || '获取文件夹内容失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('进入文件夹失败:', error)
|
||||||
|
this.$message.error('进入文件夹失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
if (this.pathStack.length > 1) {
|
||||||
|
this.pathStack.pop()
|
||||||
|
this.loadCurrentDirectory()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goToDirectory(index) {
|
||||||
|
this.pathStack.splice(index + 1)
|
||||||
|
this.loadCurrentDirectory()
|
||||||
|
},
|
||||||
|
async loadCurrentDirectory() {
|
||||||
|
const currentDir = this.pathStack[this.pathStack.length - 1]
|
||||||
|
if (!currentDir.url) {
|
||||||
|
this.currentFileList = this.fileList
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
const response = await axios.get(currentDir.url)
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
this.currentFileList = response.data.data || []
|
||||||
|
} else {
|
||||||
|
this.$message.error(response.data.msg || '加载目录失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载目录失败:', error)
|
||||||
|
this.$message.error('加载目录失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 预览文件
|
||||||
|
previewFile(file) {
|
||||||
|
if (file?.previewUrl || file?.parserUrl) {
|
||||||
|
this.previewUrl = file.previewUrl || file.parserUrl
|
||||||
|
this.isPreviewing = true
|
||||||
|
} else {
|
||||||
|
this.$message.warning('该文件暂无预览链接')
|
||||||
|
}
|
||||||
|
this.closeFileDialog()
|
||||||
|
},
|
||||||
|
// 下载文件
|
||||||
|
downloadFile(file) {
|
||||||
|
if (file?.parserUrl) {
|
||||||
|
const iframe = document.createElement('iframe')
|
||||||
|
iframe.style.display = 'none'
|
||||||
|
iframe.src = file.parserUrl
|
||||||
|
document.body.appendChild(iframe)
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(iframe)
|
||||||
|
}, 1000)
|
||||||
|
this.$message.success('开始下载文件')
|
||||||
|
} else {
|
||||||
|
this.$message.warning('该文件暂无下载链接')
|
||||||
|
}
|
||||||
|
this.closeFileDialog()
|
||||||
|
},
|
||||||
|
closeFileDialog() {
|
||||||
|
this.fileDialogVisible = false
|
||||||
|
this.selectedFile = null
|
||||||
|
},
|
||||||
|
closePreview() {
|
||||||
|
this.isPreviewing = false
|
||||||
|
this.previewUrl = ''
|
||||||
|
},
|
||||||
|
openPreviewInNewTab() {
|
||||||
|
if (this.previewUrl) {
|
||||||
|
window.open(this.previewUrl, '_blank')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(timestamp) {
|
||||||
|
if (!timestamp) return '未知时间'
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toLocaleString('zh-CN')
|
||||||
|
},
|
||||||
|
checkTheme() {
|
||||||
|
this.isDarkTheme = document.body.classList.contains('dark-theme') ||
|
||||||
|
document.documentElement.classList.contains('dark-theme')
|
||||||
|
},
|
||||||
|
renderContent(h, { node, data, store }) {
|
||||||
|
const isFolder = data.fileType === 'folder'
|
||||||
|
return h('div', {
|
||||||
|
class: 'custom-tree-node'
|
||||||
|
}, [
|
||||||
|
h('i', {
|
||||||
|
class: [this.getFileIcon(data), { 'folder-icon': isFolder, 'file-icon': !isFolder }]
|
||||||
|
}),
|
||||||
|
h('span', {
|
||||||
|
class: ['node-label', { 'folder-text': isFolder, 'file-text': !isFolder }]
|
||||||
|
}, node.label)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.checkTheme()
|
||||||
|
this.initialized = true
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
this._observer = new MutationObserver(() => {
|
||||||
|
this.checkTheme()
|
||||||
|
})
|
||||||
|
this._observer.observe(document.body, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this._observer) {
|
||||||
|
this._observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body, #app, .main-container, .directory-tree, .content-card {
|
||||||
|
/* overflow: hidden; */
|
||||||
|
/* overflow: auto; */
|
||||||
|
/* position: relative; */
|
||||||
|
}
|
||||||
|
.main-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.directory-tree {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-tree.dark-theme {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #eaeaea;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .breadcrumb {
|
||||||
|
background: #404040;
|
||||||
|
border-bottom-color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item:hover {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .breadcrumb-item.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item i {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 4px;
|
||||||
|
min-height: 80px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .file-item {
|
||||||
|
background: #404040;
|
||||||
|
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .file-item:hover {
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover .file-icon {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder .file-icon {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image .file-icon {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document .file-icon {
|
||||||
|
color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive .file-icon {
|
||||||
|
color: #9b59b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio .file-icon {
|
||||||
|
color: #1abc9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video .file-icon {
|
||||||
|
color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code .file-icon {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .file-meta {
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 10px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .empty-state {
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .empty-state i {
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .empty-state h3 {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .action-bar {
|
||||||
|
background: #404040;
|
||||||
|
border-top-color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .stats {
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-dialog-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .file-info {
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
.tree-sidebar {
|
||||||
|
width: 220px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-right: 1px solid #eaeaea;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.directory-tree.dark-theme .tree-sidebar {
|
||||||
|
background: #232323;
|
||||||
|
border-right: 1px solid #404040;
|
||||||
|
}
|
||||||
|
.file-tree-root, .tree-node ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.tree-node {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.tree-node.selected > .tree-node-label {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.directory-tree.dark-theme .tree-node.selected > .tree-node-label {
|
||||||
|
background: #333c4d;
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
.tree-node-label {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
.tree-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义树节点样式 */
|
||||||
|
.custom-tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 6px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .custom-tree-node i {
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .node-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .custom-tree-node .node-label {
|
||||||
|
color: #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件夹样式 */
|
||||||
|
.custom-tree-node .folder-icon {
|
||||||
|
color: #409eff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .custom-tree-node .folder-icon {
|
||||||
|
color: #4a9eff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .folder-text {
|
||||||
|
color: #409eff !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .custom-tree-node .folder-text {
|
||||||
|
color: #4a9eff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件样式 */
|
||||||
|
.custom-tree-node .file-icon {
|
||||||
|
color: #95a5a6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .custom-tree-node .file-icon {
|
||||||
|
color: #bdc3c7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node .file-text {
|
||||||
|
color: #606266 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .custom-tree-node .file-text {
|
||||||
|
color: #e1e1e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特殊文件类型图标颜色 */
|
||||||
|
.custom-tree-node i.fa-file-image {
|
||||||
|
color: #e74c3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-pdf {
|
||||||
|
color: #e74c3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-word {
|
||||||
|
color: #3498db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-excel {
|
||||||
|
color: #27ae60 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-powerpoint {
|
||||||
|
color: #f39c12 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-archive {
|
||||||
|
color: #9b59b6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-audio {
|
||||||
|
color: #1abc9c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-video {
|
||||||
|
color: #d35400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node i.fa-file-code {
|
||||||
|
color: #27ae60 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 树节点悬停效果 */
|
||||||
|
.el-tree-node__content:hover .custom-tree-node {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-tree-node__content:hover .custom-tree-node {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中节点样式 */
|
||||||
|
.el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
|
||||||
|
background-color: #333c4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-mask { position: fixed; z-index: 9999; left: 0; top: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; }
|
||||||
|
.preview-toolbar { padding: 12px; background: #232323; text-align: right; }
|
||||||
|
.preview-iframe { flex: 1; width: 100vw; border: none; background: #222; }
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
min-height: 500px;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.dark-theme .content-card {
|
||||||
|
background: #232323;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-theme {
|
||||||
|
flex: 1 1 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-sidebar, .tree-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-content {
|
||||||
|
padding: 40px 16px 16px 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-detail-icon-wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.file-detail-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #409eff;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dark-theme .file-detail-icon {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* splitpanes 拖拽条自定义按钮 */
|
||||||
|
.custom-splitpanes .splitpanes__splitter {
|
||||||
|
position: relative;
|
||||||
|
background: #e0e0e0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.custom-splitpanes .splitpanes__splitter:hover {
|
||||||
|
background: #b3b3b3;
|
||||||
|
}
|
||||||
|
.custom-splitpanes .splitpanes__splitter:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
border: 1.5px solid #d0d0d0;
|
||||||
|
z-index: 2;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dark-theme .custom-splitpanes .splitpanes__splitter:after {
|
||||||
|
background: #232323;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-bar {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
padding: 12px 18px 0 0;
|
||||||
|
}
|
||||||
|
.feedback-link {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 14px;
|
||||||
|
background: #fff5f5;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.feedback-link:first-child { margin-left: 0; }
|
||||||
|
.feedback-link:hover {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.dark-theme .feedback-link {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #ff7675;
|
||||||
|
border-color: #ff7675;
|
||||||
|
}
|
||||||
|
.dark-theme .feedback-link:hover {
|
||||||
|
background: #ff7675;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
.feedback-icon {
|
||||||
|
font-size: 1.15em;
|
||||||
|
color: #e74c3c;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
.feedback-link:hover .feedback-icon {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
|
||||||
|
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
|
||||||
|
.dark-theme .feedback-icon {
|
||||||
|
color: #ff7675;
|
||||||
|
}
|
||||||
|
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
|
||||||
|
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.file-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.file-item {
|
||||||
|
padding: 6px 2px;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
.file-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.file-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.action-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,22 +9,24 @@ import 'element-plus/dist/index.css'
|
|||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import './styles/dark/css-vars.css'
|
import './styles/dark/css-vars.css'
|
||||||
|
import router from './router/index.js'
|
||||||
|
|
||||||
window.$vueApp = Vue.createApp(App)
|
const app = Vue.createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
window.$vueApp.component(key, component)
|
app.component(key, component)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import JsonViewer as a Vue.js plugin
|
// Import JsonViewer as a Vue.js plugin
|
||||||
window.$vueApp.use(JsonViewer)
|
app.use(JsonViewer)
|
||||||
window.$vueApp.use(DirectiveExtensions)
|
app.use(DirectiveExtensions)
|
||||||
|
|
||||||
// or
|
// or
|
||||||
// components: {JsonViewer}
|
// components: {JsonViewer}
|
||||||
|
|
||||||
window.$vueApp.use(VueClipboard)
|
app.use(VueClipboard)
|
||||||
window.$vueApp.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
window.$vueApp.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
// 修改自 https://github.com/syhyz1990/panAI
|
// 修改自 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 = {
|
let opt = {
|
||||||
// 'baidu': {
|
// 'baidu': {
|
||||||
@@ -293,6 +288,12 @@
|
|||||||
host: /mail\.qq\.com/,
|
host: /mail\.qq\.com/,
|
||||||
name: 'QQ邮箱中转站'
|
name: 'QQ邮箱中转站'
|
||||||
},
|
},
|
||||||
|
QQsc: {
|
||||||
|
// qfile.qq.com
|
||||||
|
reg: /https:\/\/qfile\.qq\.com\/q\/.+/,
|
||||||
|
host: /qfile\.qq\.com/,
|
||||||
|
name: 'QQ闪传'
|
||||||
|
},
|
||||||
pan118: {
|
pan118: {
|
||||||
reg: /https:\/\/(?:[a-zA-Z\d-]+\.)?118pan\.com\/b.+/,
|
reg: /https:\/\/(?:[a-zA-Z\d-]+\.)?118pan\.com\/b.+/,
|
||||||
host: /118pan\.com/,
|
host: /118pan\.com/,
|
||||||
|
|||||||
17
web-front/src/router/index.js
Normal file
17
web-front/src/router/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Home from '@/views/Home.vue'
|
||||||
|
import ShowFile from '@/views/ShowFile.vue'
|
||||||
|
import ShowList from '@/views/ShowList.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/', component: Home },
|
||||||
|
{ path: '/showFile', component: ShowFile },
|
||||||
|
{ path: '/showList', component: ShowList }
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory('/'),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/* 全局样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 亮色主题 */
|
||||||
|
body {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题 */
|
||||||
|
body.dark-theme {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 暗色主题适配 */
|
||||||
|
.dark-theme .el-card {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
border-color: #404040 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-input__inner {
|
||||||
|
background-color: #404040 !important;
|
||||||
|
border-color: #555555 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-input__inner::placeholder {
|
||||||
|
color: #bdc3c7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-button {
|
||||||
|
background-color: #404040 !important;
|
||||||
|
border-color: #555555 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-button:hover {
|
||||||
|
background-color: #555555 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-button--primary {
|
||||||
|
background-color: #409eff !important;
|
||||||
|
border-color: #409eff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-button--primary:hover {
|
||||||
|
background-color: #66b1ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-button--success {
|
||||||
|
background-color: #67c23a !important;
|
||||||
|
border-color: #67c23a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-button--success:hover {
|
||||||
|
background-color: #85ce61 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-switch__core {
|
||||||
|
background-color: #555555 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-descriptions {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-descriptions__label {
|
||||||
|
color: #bdc3c7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-descriptions__content {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-dialog {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-dialog__title {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-dialog__body {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-message {
|
||||||
|
background-color: #2d2d2d !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-message--success {
|
||||||
|
background-color: #67c23a !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-message--error {
|
||||||
|
background-color: #f56c6c !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-message--warning {
|
||||||
|
background-color: #e6a23c !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .el-message--info {
|
||||||
|
background-color: #909399 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接颜色 */
|
||||||
|
.dark-theme a {
|
||||||
|
color: #4a9eff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme a:hover {
|
||||||
|
color: #66b1ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.dark-theme ::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme ::-webkit-scrollbar-track {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme ::-webkit-scrollbar-thumb {
|
||||||
|
background: #555555;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选择文本样式 */
|
||||||
|
.dark-theme ::selection {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme ::-moz-selection {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|||||||
85
web-front/src/utils/fileTypeUtils.js
Normal file
85
web-front/src/utils/fileTypeUtils.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const fileTypeUtils = {
|
||||||
|
getFileExtension(filename) {
|
||||||
|
if (!filename) return ''
|
||||||
|
return filename.split('.').pop()
|
||||||
|
},
|
||||||
|
getFileTypeClass(file) {
|
||||||
|
if (file.fileType === 'folder') return 'folder'
|
||||||
|
const ext = this.getFileExtension(file.fileName)
|
||||||
|
const fileTypes = {
|
||||||
|
'image': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'],
|
||||||
|
'document': ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf'],
|
||||||
|
'archive': ['zip', 'rar', '7z', 'tar', 'gz'],
|
||||||
|
'audio': ['mp3', 'wav', 'ogg', 'flac'],
|
||||||
|
'video': ['mp4', 'avi', 'mov', 'wmv', 'mkv', 'flv'],
|
||||||
|
'code': ['html', 'htm', 'css', 'js', 'json', 'php', 'py', 'java', 'c', 'cpp', 'h', 'sh', 'bat', 'md']
|
||||||
|
}
|
||||||
|
for (const [type, extensions] of Object.entries(fileTypes)) {
|
||||||
|
if (extensions.includes(ext.toLowerCase())) {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'document'
|
||||||
|
},
|
||||||
|
getFileIcon(file) {
|
||||||
|
if (file.fileType === 'folder') return 'fas fa-folder'
|
||||||
|
const ext = this.getFileExtension(file.fileName)
|
||||||
|
const iconMap = {
|
||||||
|
'jpg': 'fas fa-file-image', 'jpeg': 'fas fa-file-image', 'png': 'fas fa-file-image',
|
||||||
|
'gif': 'fas fa-file-image', 'bmp': 'fas fa-file-image', 'svg': 'fas fa-file-image', 'webp': 'fas fa-file-image',
|
||||||
|
'pdf': 'fas fa-file-pdf', 'doc': 'fas fa-file-word', 'docx': 'fas fa-file-word',
|
||||||
|
'xls': 'fas fa-file-excel', 'xlsx': 'fas fa-file-excel', 'ppt': 'fas fa-file-powerpoint', 'pptx': 'fas fa-file-powerpoint',
|
||||||
|
'txt': 'fas fa-file-alt', 'rtf': 'fas fa-file-alt',
|
||||||
|
'zip': 'fas fa-file-archive', 'rar': 'fas fa-file-archive', '7z': 'fas fa-file-archive',
|
||||||
|
'tar': 'fas fa-file-archive', 'gz': 'fas fa-file-archive',
|
||||||
|
'mp3': 'fas fa-file-audio', 'wav': 'fas fa-file-audio', 'ogg': 'fas fa-file-audio', 'flac': 'fas fa-file-audio',
|
||||||
|
'mp4': 'fas fa-file-video', 'avi': 'fas fa-file-video', 'mov': 'fas fa-file-video',
|
||||||
|
'wmv': 'fas fa-file-video', 'mkv': 'fas fa-file-video', 'flv': 'fas fa-file-video',
|
||||||
|
'html': 'fas fa-file-code', 'htm': 'fas fa-file-code', 'css': 'fas fa-file-code',
|
||||||
|
'js': 'fas fa-file-code', 'json': 'fas fa-file-code', 'php': 'fas fa-file-code',
|
||||||
|
'py': 'fas fa-file-code', 'java': 'fas fa-file-code', 'c': 'fas fa-file-code',
|
||||||
|
'cpp': 'fas fa-file-code', 'h': 'fas fa-file-code', 'sh': 'fas fa-file-code',
|
||||||
|
'bat': 'fas fa-file-code', 'md': 'fas fa-file-code'
|
||||||
|
}
|
||||||
|
return iconMap[ext.toLowerCase()] || 'fas fa-file'
|
||||||
|
},
|
||||||
|
extractFileNameAndExt(url) {
|
||||||
|
if (!url) return { name: '', ext: '' }
|
||||||
|
const filenameParams = [
|
||||||
|
'response-content-disposition', 'filename', 'filename*', 'fn', 'fname', 'download_name'
|
||||||
|
];
|
||||||
|
let name = null;
|
||||||
|
try {
|
||||||
|
const u = new URL(url, window.location.origin);
|
||||||
|
for (const param of filenameParams) {
|
||||||
|
const value = u.searchParams.get(param);
|
||||||
|
if (value) {
|
||||||
|
if (param === 'response-content-disposition') {
|
||||||
|
const match = value.match(/filename\*?=(.*'')?(?<FN>.*)/i);
|
||||||
|
name = match && match.groups && match.groups['FN'] ? match.groups['FN'] : value;
|
||||||
|
} else {
|
||||||
|
name = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name) {
|
||||||
|
name = decodeURIComponent(name).replace(/['"]/g, '');
|
||||||
|
} else {
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const paths = decodedUrl.split('/');
|
||||||
|
name = paths[paths.length - 1].split('?')[0];
|
||||||
|
}
|
||||||
|
let ext = '';
|
||||||
|
if (name) {
|
||||||
|
const spl = name.split('.');
|
||||||
|
ext = spl.length > 1 ? spl[spl.length - 1].toLowerCase() : '';
|
||||||
|
}
|
||||||
|
return { name, ext };
|
||||||
|
} catch {
|
||||||
|
return { name: '', ext: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fileTypeUtils
|
||||||
879
web-front/src/views/Home.vue
Normal file
879
web-front/src/views/Home.vue
Normal file
@@ -0,0 +1,879 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app" :class="{ 'dark-theme': isDarkMode }">
|
||||||
|
<!-- <el-dialog
|
||||||
|
v-model="showRiskDialog"
|
||||||
|
title="使用本网站您应改同意"
|
||||||
|
width="300px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:show-close="false"
|
||||||
|
center
|
||||||
|
>
|
||||||
|
<div style="font-size:1.08em;line-height:1.8;">
|
||||||
|
请勿在本平台分享、传播任何违法内容,包括但不限于:<br>
|
||||||
|
违规视频、游戏外挂、侵权资源、涉政涉黄等。<br>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="ackRisk">我知道了</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog> -->
|
||||||
|
<!-- 顶部反馈栏(小号、灰色、无红边框) -->
|
||||||
|
<div class="feedback-bar">
|
||||||
|
<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/netdisk-fast-download" target="_blank" rel="noopener" class="feedback-link mini">
|
||||||
|
<i class="fab fa-github feedback-icon"></i>
|
||||||
|
源码
|
||||||
|
</a>
|
||||||
|
<a href="https://blog.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
|
||||||
|
<i class="fas fa-blog feedback-icon"></i>
|
||||||
|
博客
|
||||||
|
</a>
|
||||||
|
<a href="https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng" target="_blank" rel="noopener" class="feedback-link mini">
|
||||||
|
<i class="fas fa-server feedback-icon"></i>
|
||||||
|
部署
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20" style="margin-left: 0; margin-right: 0;">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div style="text-align: right">
|
||||||
|
<DarkMode @theme-change="handleThemeChange" />
|
||||||
|
</div>
|
||||||
|
<div class="demo-basic--circle">
|
||||||
|
<div class="block" style="text-align: center;">
|
||||||
|
<img :height="150" src="../../public/images/lanzou111.png" alt="lz">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 项目简介移到卡片内 -->
|
||||||
|
<div class="project-intro">
|
||||||
|
<div class="intro-title">NFD网盘直链解析0.1.9_bate8</div>
|
||||||
|
<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云盘</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="typo">
|
||||||
|
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="main" v-loading="isLoading">
|
||||||
|
<div class="grid-content">
|
||||||
|
<!-- 开关按钮,控制是否自动读取剪切板 -->
|
||||||
|
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
|
||||||
|
|
||||||
|
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
|
||||||
|
<template #prepend>分享链接</template>
|
||||||
|
<template #append v-if="!autoReadClipboard">
|
||||||
|
<el-button @click="getPaste(true)">读取剪切板</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<el-input placeholder="请输入密码" v-model="password" id="url">
|
||||||
|
<template #prepend>分享密码</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<el-input v-show="directLink" :value="directLink" id="url">
|
||||||
|
<template #prepend>智能直链</template>
|
||||||
|
<template #append>
|
||||||
|
<el-button v-clipboard:copy="directLink" v-clipboard:success="onCopy" v-clipboard:error="onError">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<p style="text-align: center">
|
||||||
|
<el-button style="margin-left: 40px" @click="parseFile">解析文件</el-button>
|
||||||
|
<el-button style="margin-left: 20px" @click="parseDirectory">解析目录</el-button>
|
||||||
|
<el-button style="margin-left: 20px" @click="generateMarkdown">生成Markdown</el-button>
|
||||||
|
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
|
||||||
|
<el-button style="margin-left: 20px" @click="getStatistics">分享统计</el-button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 解析结果 -->
|
||||||
|
<div v-if="parseResult.code" style="margin-top: 10px">
|
||||||
|
<strong>解析结果: </strong>
|
||||||
|
<json-viewer :value="parseResult" :expand-depth="5" copyable boxed sort />
|
||||||
|
<!-- 文件信息美化展示区 -->
|
||||||
|
<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" rel="noreferrer noopener">点击下载</a>
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row" v-if="parseResult.data?.downloadShortUrl">
|
||||||
|
<span class="file-meta-label">下载短链:</span>
|
||||||
|
<a :href="parseResult.data.downloadShortUrl" target="_blank" class="file-meta-link">{{ parseResult.data.downloadShortUrl }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row">
|
||||||
|
<span class="file-meta-label">文件预览:</span>
|
||||||
|
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="file-meta-link">点击预览</a>
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row">
|
||||||
|
<span class="file-meta-label">文件名:</span>{{ extractFileNameAndExt(downloadUrl).name }}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row">
|
||||||
|
<span class="file-meta-label">文件类型:</span>{{ getFileTypeClass({ fileName: extractFileNameAndExt(downloadUrl).name }) }}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
|
||||||
|
<span class="file-meta-label">文件大小:</span>{{ parseResult.data.sizeStr }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown链接 -->
|
||||||
|
<div v-if="markdownText" style="text-align: center">
|
||||||
|
<el-input :value="markdownText" readonly>
|
||||||
|
<template #append>
|
||||||
|
<el-button v-clipboard:copy="markdownText" v-clipboard:success="onCopy" v-clipboard:error="onError">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二维码 -->
|
||||||
|
<div style="text-align: center" v-show="showQRCode">
|
||||||
|
<canvas ref="qrcodeCanvas"></canvas>
|
||||||
|
<div style="text-align: center">
|
||||||
|
<el-link target="_blank" :href="qrCodeUrl">{{ qrCodeUrl }}</el-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div v-if="statisticsData.shareLinkInfo">
|
||||||
|
<el-descriptions class="margin-top" title="分享详情" :column="1" border>
|
||||||
|
<template slot="extra">
|
||||||
|
<el-button type="primary" size="small">操作</el-button>
|
||||||
|
</template>
|
||||||
|
<el-descriptions-item label="网盘名称">{{ statisticsData.shareLinkInfo.panName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="网盘标识">{{ statisticsData.shareLinkInfo.type }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="分享Key">{{ statisticsData.shareLinkInfo.shareKey }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="分享链接">
|
||||||
|
<el-link target="_blank" :href="statisticsData.shareLinkInfo.shareUrl">{{ statisticsData.shareLinkInfo.shareUrl }}</el-link>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="jsonApi链接">
|
||||||
|
<el-link target="_blank" :href="statisticsData.apiLink">{{ statisticsData.apiLink }}</el-link>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="302下载链接">
|
||||||
|
<el-link target="_blank" :href="statisticsData.downLink">{{ statisticsData.downLink }}</el-link>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="302预览链接">
|
||||||
|
<el-link target="_blank" :href="statisticsData.viewLink">{{ statisticsData.viewLink }}</el-link>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="解析次数">{{ statisticsData.parserTotal }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="缓存命中次数">{{ statisticsData.cacheHitTotal }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总请求次数">{{ statisticsData.sumTotal }}</el-descriptions-item>
|
||||||
|
</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;">
|
||||||
|
<el-radio-group v-model="directoryViewMode" size="small">
|
||||||
|
<el-radio-button label="pane">窗格</el-radio-button>
|
||||||
|
<el-radio-button label="tree">文件树</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<DirectoryTree
|
||||||
|
:file-list="directoryData"
|
||||||
|
:share-url="link"
|
||||||
|
:password="password"
|
||||||
|
:view-mode="directoryViewMode"
|
||||||
|
@file-click="handleFileClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-row>
|
||||||
|
<!-- 文件解析结果区下方加分享按钮 -->
|
||||||
|
<!-- <div v-if="parseResult.code && downloadUrl" style="margin-top: 10px; text-align: right;">-->
|
||||||
|
<!-- <el-button type="primary" @click="copyShowFileLink">分享文件直链</el-button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- 目录解析结果区下方加分享按钮 -->
|
||||||
|
<!-- <div v-if="showDirectoryTree && directoryData.length" style="margin-top: 10px; text-align: right;">-->
|
||||||
|
<!-- <el-input :value="showListLink" readonly style="width: 350px; margin-right: 10px;">-->
|
||||||
|
<!-- <template #append>-->
|
||||||
|
<!-- <el-button v-clipboard:copy="showListLink" v-clipboard:success="onCopy" v-clipboard:error="onError">-->
|
||||||
|
<!-- <el-icon><CopyDocument /></el-icon>复制分享链接-->
|
||||||
|
<!-- </el-button>-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </el-input>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import QRCode from 'qrcode'
|
||||||
|
import DarkMode from '@/components/DarkMode'
|
||||||
|
import DirectoryTree from '@/components/DirectoryTree'
|
||||||
|
import parserUrl from '../parserUrl1'
|
||||||
|
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: { DarkMode, DirectoryTree },
|
||||||
|
mixins: [fileTypeUtils],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
baseAPI: `${location.protocol}//${location.host}`,
|
||||||
|
autoReadClipboard: true,
|
||||||
|
isDarkMode: false,
|
||||||
|
isLoading: false,
|
||||||
|
|
||||||
|
// 输入数据
|
||||||
|
link: "",
|
||||||
|
password: "",
|
||||||
|
|
||||||
|
// 解析结果
|
||||||
|
parseResult: {},
|
||||||
|
downloadUrl: null,
|
||||||
|
directLink: '',
|
||||||
|
previewBaseUrl,
|
||||||
|
|
||||||
|
// 功能结果
|
||||||
|
markdownText: '',
|
||||||
|
showQRCode: false,
|
||||||
|
qrCodeUrl: '',
|
||||||
|
statisticsData: {},
|
||||||
|
|
||||||
|
// 目录树
|
||||||
|
showDirectoryTree: false,
|
||||||
|
directoryData: [],
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
node1Info: {},
|
||||||
|
node2Info: {},
|
||||||
|
hasWarnedNoLink: false,
|
||||||
|
directoryViewMode: 'pane', // 新增,目录树展示模式
|
||||||
|
hasClipboardSuccessTip: false, // 新增,聚焦期间只提示一次
|
||||||
|
showRiskDialog: false,
|
||||||
|
baseUrl: location.origin,
|
||||||
|
showListLink: '',
|
||||||
|
|
||||||
|
errorDialogVisible: false,
|
||||||
|
errorDetail: null,
|
||||||
|
errorButtonVisible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 主题切换
|
||||||
|
handleThemeChange(isDark) {
|
||||||
|
this.isDarkMode = isDark
|
||||||
|
},
|
||||||
|
|
||||||
|
// 验证输入
|
||||||
|
validateInput() {
|
||||||
|
this.clearResults()
|
||||||
|
|
||||||
|
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
|
||||||
|
this.$message.error("请输入有效链接!")
|
||||||
|
throw new Error('请输入有效链接')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除结果
|
||||||
|
clearResults() {
|
||||||
|
this.parseResult = {}
|
||||||
|
this.downloadUrl = null
|
||||||
|
this.markdownText = ''
|
||||||
|
this.showQRCode = false
|
||||||
|
this.statisticsData = {}
|
||||||
|
this.showDirectoryTree = false
|
||||||
|
this.directoryData = []
|
||||||
|
},
|
||||||
|
|
||||||
|
// 统一API调用
|
||||||
|
async callAPI(endpoint, params = {}) {
|
||||||
|
this.errorButtonVisible = false
|
||||||
|
try {
|
||||||
|
this.isLoading = true
|
||||||
|
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
|
||||||
|
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
// 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) {
|
||||||
|
this.$message.error(error.message || '网络错误')
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 文件解析
|
||||||
|
async parseFile() {
|
||||||
|
try {
|
||||||
|
this.validateInput()
|
||||||
|
const params = { url: this.link }
|
||||||
|
if (this.password) params.pwd = this.password
|
||||||
|
|
||||||
|
const result = await this.callAPI('/json/parser', params)
|
||||||
|
this.parseResult = result
|
||||||
|
this.downloadUrl = result.data?.directLink
|
||||||
|
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
|
||||||
|
this.$message.success('文件解析成功!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文件解析失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 目录解析
|
||||||
|
async parseDirectory() {
|
||||||
|
try {
|
||||||
|
this.validateInput()
|
||||||
|
const params = { url: this.link }
|
||||||
|
if (this.password) params.pwd = this.password
|
||||||
|
|
||||||
|
const result = await this.callAPI('/v2/linkInfo', params)
|
||||||
|
const data = result.data
|
||||||
|
|
||||||
|
// 检查是否支持目录解析
|
||||||
|
const supportedPans = ["iz", "lz", "fj", "ye"]
|
||||||
|
if (!supportedPans.includes(data.shareLinkInfo.type)) {
|
||||||
|
this.$message.error("当前网盘不支持目录解析")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取目录数据
|
||||||
|
const directoryResult = await this.callAPI('/v2/getFileList', params)
|
||||||
|
this.directoryData = directoryResult.data || []
|
||||||
|
this.showDirectoryTree = true
|
||||||
|
// 自动赋值分享链接
|
||||||
|
this.showListLink = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
|
||||||
|
|
||||||
|
this.$message.success(`目录解析成功!共找到 ${this.directoryData.length} 个文件/文件夹`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('目录解析失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生成Markdown
|
||||||
|
async generateMarkdown() {
|
||||||
|
try {
|
||||||
|
this.validateInput()
|
||||||
|
const params = { url: this.link }
|
||||||
|
if (this.password) params.pwd = this.password
|
||||||
|
|
||||||
|
const result = await this.callAPI('/v2/linkInfo', params)
|
||||||
|
this.markdownText = this.buildMarkdown('快速下载地址', result.data.downLink)
|
||||||
|
this.$message.success('Markdown生成成功!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成Markdown失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
async generateQRCode() {
|
||||||
|
try {
|
||||||
|
this.validateInput()
|
||||||
|
const params = { url: this.link }
|
||||||
|
if (this.password) params.pwd = this.password
|
||||||
|
|
||||||
|
const result = await this.callAPI('/v2/linkInfo', params)
|
||||||
|
this.qrCodeUrl = result.data.downLink
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
margin: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
QRCode.toCanvas(this.$refs.qrcodeCanvas, this.qrCodeUrl, options, error => {
|
||||||
|
if (error) console.error(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.showQRCode = true
|
||||||
|
this.$message.success('二维码生成成功!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成二维码失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取统计信息
|
||||||
|
async getStatistics() {
|
||||||
|
try {
|
||||||
|
this.validateInput()
|
||||||
|
const params = { url: this.link }
|
||||||
|
if (this.password) params.pwd = this.password
|
||||||
|
|
||||||
|
const result = await this.callAPI('/v2/linkInfo', params)
|
||||||
|
this.statisticsData = result.data
|
||||||
|
this.$message.success('统计信息获取成功!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计信息失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 构建Markdown链接
|
||||||
|
buildMarkdown(title, url) {
|
||||||
|
return `[${title}](${url})`
|
||||||
|
},
|
||||||
|
|
||||||
|
// 复制成功
|
||||||
|
onCopy() {
|
||||||
|
this.$message.success('复制成功')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 复制失败
|
||||||
|
onError() {
|
||||||
|
this.$message.error('复制失败')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 文件点击处理
|
||||||
|
handleFileClick(file) {
|
||||||
|
if (file.parserUrl) {
|
||||||
|
window.open(file.parserUrl, '_blank')
|
||||||
|
} else {
|
||||||
|
this.$message.warning('该文件暂无下载链接')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取剪切板内容
|
||||||
|
async getPaste(isManual = false) {
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText()
|
||||||
|
console.log('获取到的文本内容是:', text)
|
||||||
|
|
||||||
|
const linkInfo = parserUrl.parseLink(text)
|
||||||
|
const pwd = parserUrl.parsePwd(text) || ''
|
||||||
|
|
||||||
|
if (linkInfo.link) {
|
||||||
|
if (linkInfo.link !== this.link || pwd !== this.password) {
|
||||||
|
this.password = pwd
|
||||||
|
this.link = linkInfo.link
|
||||||
|
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
|
||||||
|
// 聚焦期间只提示一次
|
||||||
|
if (!this.hasClipboardSuccessTip) {
|
||||||
|
this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)
|
||||||
|
this.hasClipboardSuccessTip = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.warning(`[${linkInfo.name}]分享信息无变化`)
|
||||||
|
}
|
||||||
|
this.hasWarnedNoLink = false // 有效链接后重置
|
||||||
|
} else {
|
||||||
|
if (isManual || !this.hasWarnedNoLink) {
|
||||||
|
this.$message.warning("未能提取到分享链接, 该分享可能尚未支持, 你可以复制任意网盘/音乐App的分享到该页面, 系统智能识别")
|
||||||
|
this.hasWarnedNoLink = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取剪切板失败:', error)
|
||||||
|
this.$message.error('读取剪切板失败,请检查浏览器权限')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取统计信息
|
||||||
|
async getInfo() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/v2/statisticsInfo')
|
||||||
|
if (response.data.success) {
|
||||||
|
this.node1Info = response.data.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计信息失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增切换目录树展示模式方法
|
||||||
|
setDirectoryViewMode(mode) {
|
||||||
|
this.directoryViewMode = mode
|
||||||
|
},
|
||||||
|
|
||||||
|
// 文件名和类型提取方法(复用 DirectoryTree 的静态方法)
|
||||||
|
extractFileNameAndExt(url) {
|
||||||
|
return fileTypeUtils.extractFileNameAndExt(url)
|
||||||
|
},
|
||||||
|
getFileTypeClass(file) {
|
||||||
|
return fileTypeUtils.getFileTypeClass(file)
|
||||||
|
},
|
||||||
|
ackRisk() {
|
||||||
|
window.localStorage.setItem('nfd_risk_ack', '1')
|
||||||
|
this.showRiskDialog = false
|
||||||
|
},
|
||||||
|
copyShowFileLink() {
|
||||||
|
const url = `${this.baseUrl}/showFile?url=${encodeURIComponent(this.downloadUrl)}`
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
ElMessage.success('文件分享链接已复制!')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
copyShowListLink() {
|
||||||
|
const url = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
|
||||||
|
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('复制失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// 从localStorage读取设置
|
||||||
|
const savedAutoRead = window.localStorage.getItem("autoReadClipboard")
|
||||||
|
if (savedAutoRead !== null) {
|
||||||
|
this.autoReadClipboard = savedAutoRead === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取初始统计信息
|
||||||
|
this.getInfo()
|
||||||
|
|
||||||
|
// 自动读取剪切板
|
||||||
|
if (this.autoReadClipboard) {
|
||||||
|
this.getPaste()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听窗口焦点事件
|
||||||
|
window.addEventListener('focus', () => {
|
||||||
|
if (this.autoReadClipboard) {
|
||||||
|
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
|
||||||
|
this.getPaste()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 首次打开页面弹出风险提示
|
||||||
|
if (!window.localStorage.getItem('nfd_risk_ack')) {
|
||||||
|
this.showRiskDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
downloadUrl(val) {
|
||||||
|
if (!val) {
|
||||||
|
this.$router.push('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
autoReadClipboard(val) {
|
||||||
|
window.localStorage.setItem("autoReadClipboard", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body.dark-theme {
|
||||||
|
background-color: #181818;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
/* 不设置 background-color */
|
||||||
|
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: auto;
|
||||||
|
padding: 1em;
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app.dark-theme {
|
||||||
|
/* 不设置 background-color */
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-card {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 4em !important;
|
||||||
|
margin-bottom: 4em !important;
|
||||||
|
opacity: 1 !important; /* 只要不透明 */
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app.dark-theme .box-card {
|
||||||
|
background: #232323 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
#app {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
margin: 0 !important; /* 关键:去掉 auto 居中 */
|
||||||
|
max-width: 100vw !important;
|
||||||
|
}
|
||||||
|
#app .box-card {
|
||||||
|
margin: 1em 4px !important; /* 上下1em,左右4px */
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-content {
|
||||||
|
margin-top: 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select .el-input {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-tree-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app.dark-theme .directory-tree-container {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download h3 {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download button {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typo {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typo a {
|
||||||
|
color: #0077ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app.dark-theme .typo a {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 10px;
|
||||||
|
margin-bottom: .8em;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, .12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app.dark-theme hr {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, .12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-bar {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
padding: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
.feedback-link {
|
||||||
|
color: #888;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.feedback-link:first-child { margin-left: 0; }
|
||||||
|
.feedback-link:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.dark-theme .feedback-link {
|
||||||
|
background: transparent;
|
||||||
|
color: #bbb;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.dark-theme .feedback-link:hover {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.feedback-link.mini {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.feedback-icon {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #888;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
.feedback-link:hover .feedback-icon {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
|
||||||
|
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
|
||||||
|
.dark-theme .feedback-icon {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
|
||||||
|
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
|
||||||
|
.feedback-link:nth-child(4) .feedback-icon { color: #409eff; }
|
||||||
|
.dark-theme .feedback-link:nth-child(4) .feedback-icon { color: #4a9eff; }
|
||||||
|
|
||||||
|
.project-intro {
|
||||||
|
margin: 0 auto 18px auto;
|
||||||
|
max-width: 700px;
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
font-size: 1.02rem;
|
||||||
|
}
|
||||||
|
.intro-title {
|
||||||
|
font-size: 1.18rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.intro-desc {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.dark-theme .project-intro, .dark-theme .intro-title, .dark-theme .intro-desc {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
.dark-theme .intro-title {
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
.file-meta-info-card {
|
||||||
|
margin: 18px auto 0 auto;
|
||||||
|
max-width: 600px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||||
|
padding: 18px 24px 12px 24px;
|
||||||
|
font-size: 1.02rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
#app.dark-theme .file-meta-info-card {
|
||||||
|
background: #232323;
|
||||||
|
color: #eee;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
.file-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.01em;
|
||||||
|
}
|
||||||
|
.file-meta-label {
|
||||||
|
min-width: 90px;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
#app.dark-theme .file-meta-label {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
.file-meta-link {
|
||||||
|
color: #409eff;
|
||||||
|
word-break: break-all;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
#app.dark-theme .file-meta-link {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
#app.dark-theme .jv-container {
|
||||||
|
background: #232323 !important;
|
||||||
|
color: #eee !important;
|
||||||
|
border-color: #444 !important;
|
||||||
|
}
|
||||||
|
#app.dark-theme .jv-key {
|
||||||
|
color: #4a9eff !important;
|
||||||
|
}
|
||||||
|
#app.dark-theme .jv-number {
|
||||||
|
color: #f39c12 !important;
|
||||||
|
}
|
||||||
|
#app.dark-theme .jv-string {
|
||||||
|
color: #27ae60 !important;
|
||||||
|
}
|
||||||
|
#app.dark-theme .jv-boolean {
|
||||||
|
color: #e67e22 !important;
|
||||||
|
}
|
||||||
|
#app.dark-theme .jv-null {
|
||||||
|
color: #e74c3c !important;
|
||||||
|
}
|
||||||
|
#app.jv-container .jv-item.jv-object {
|
||||||
|
color: #32ba6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-bar {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto; /* 居中 */
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.feedback-bar {
|
||||||
|
max-width: 480px; /* 和移动端卡片宽度一致 */
|
||||||
|
padding-right: 8px; /* 和卡片内容对齐 */
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jv-container.jv-light .jv-item.jv-object {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
web-front/src/views/ShowFile.vue
Normal file
118
web-front/src/views/ShowFile.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="show-file-page">
|
||||||
|
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
|
||||||
|
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="parseResult.code">
|
||||||
|
<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">点击下载</a>
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row">
|
||||||
|
<span class="file-meta-label">文件名:</span>{{ fileTypeUtils.extractFileNameAndExt(downloadUrl).name }}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row">
|
||||||
|
<span class="file-meta-label">文件类型:</span>{{ fileTypeUtils.getFileTypeClass({ fileName: fileTypeUtils.extractFileNameAndExt(downloadUrl).name }) }}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
|
||||||
|
<span class="file-meta-label">文件大小:</span>{{ parseResult.data.sizeStr }}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta-row">
|
||||||
|
<span class="file-meta-label">在线预览:</span>
|
||||||
|
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="preview-btn">点击在线预览</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else style="text-align:center;margin-top:40px;">未获取到有效解析结果</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||||
|
import { previewBaseUrl } from '@/views/Home.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ShowFile',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
error: '',
|
||||||
|
parseResult: {},
|
||||||
|
downloadUrl: '',
|
||||||
|
fileTypeUtils,
|
||||||
|
previewBaseUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchFile() {
|
||||||
|
const url = this.$route.query.url
|
||||||
|
if (!url) {
|
||||||
|
this.error = '缺少 url 参数'
|
||||||
|
this.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/json/parser', { params: { url } })
|
||||||
|
this.parseResult = res.data
|
||||||
|
this.downloadUrl = res.data.data?.directLink
|
||||||
|
} catch (e) {
|
||||||
|
this.error = '解析失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.show-file-page {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto;
|
||||||
|
}
|
||||||
|
.file-meta-info-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||||
|
padding: 18px 24px 12px 24px;
|
||||||
|
font-size: 1.02rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.file-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.01em;
|
||||||
|
}
|
||||||
|
.file-meta-label {
|
||||||
|
min-width: 90px;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.file-meta-link {
|
||||||
|
color: #409eff;
|
||||||
|
word-break: break-all;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.preview-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 16px;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 8px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.preview-btn:hover {
|
||||||
|
background: #1867c0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
107
web-front/src/views/ShowList.vue
Normal file
107
web-front/src/views/ShowList.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="show-list-page">
|
||||||
|
<div class="list-title-wrap">
|
||||||
|
<h2 class="list-title">{{ url }} 目录</h2>
|
||||||
|
<div class="list-subtitle">
|
||||||
|
<a :href="url" target="_blank">原始分享链接</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right;margin-bottom:12px;">
|
||||||
|
<DarkMode @theme-change="toggleTheme" style="float: left;"/>
|
||||||
|
<el-radio-group v-model="viewMode" size="small" style="margin-left:20px;">
|
||||||
|
<el-radio-button label="pane">窗格</el-radio-button>
|
||||||
|
<el-radio-button label="tree">目录树</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
|
||||||
|
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
|
||||||
|
<div v-else>
|
||||||
|
<DirectoryTree
|
||||||
|
:file-list="directoryData"
|
||||||
|
:share-url="url"
|
||||||
|
:password="''"
|
||||||
|
:view-mode="viewMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import DirectoryTree from '@/components/DirectoryTree'
|
||||||
|
import DarkMode from '@/components/DarkMode'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ShowList',
|
||||||
|
components: { DirectoryTree, DarkMode },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
error: '',
|
||||||
|
directoryData: [],
|
||||||
|
url: '',
|
||||||
|
viewMode: 'pane'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchList() {
|
||||||
|
this.url = this.$route.query.url
|
||||||
|
if (!this.url) {
|
||||||
|
this.error = '缺少 url 参数'
|
||||||
|
this.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
|
||||||
|
this.directoryData = res.data.data || []
|
||||||
|
} catch (e) {
|
||||||
|
this.error = '目录解析失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleTheme(isDark) {
|
||||||
|
if (isDark) {
|
||||||
|
document.body.classList.add('dark-theme')
|
||||||
|
document.documentElement.classList.add('dark-theme')
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark-theme')
|
||||||
|
document.documentElement.classList.remove('dark-theme')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.show-list-page {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 40px auto;
|
||||||
|
}
|
||||||
|
.list-title-wrap {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.list-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #409eff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.list-subtitle {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.list-subtitle a {
|
||||||
|
color: #409eff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.list-subtitle a:hover {
|
||||||
|
color: #1867c0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,15 +16,27 @@ module.exports = {
|
|||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 6444,
|
port: 6444,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/': {
|
'/parser': {
|
||||||
target: 'http://127.0.0.1:6400', // 请求本地
|
target: 'http://127.0.0.1:6400/', // 请求本地
|
||||||
|
ws: false
|
||||||
|
},
|
||||||
|
'/v2': {
|
||||||
|
target: 'http://127.0.0.1:6400/', // 请求本地
|
||||||
|
ws: false
|
||||||
|
},
|
||||||
|
'/json': {
|
||||||
|
target: 'http://127.0.0.1:6400/', // 请求本地
|
||||||
|
ws: false
|
||||||
|
},
|
||||||
|
'/d': {
|
||||||
|
target: 'http://127.0.0.1:6400/', // 请求本地
|
||||||
ws: false
|
ws: false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
configureWebpack: {
|
configureWebpack: {
|
||||||
// provide the app's title in webpack's name field, so that
|
// provide the app's title in webpack's name field, so that
|
||||||
// it can be accessed in index.html to inject the correct title.
|
// it can be accessed in list.html to inject the correct title.
|
||||||
name: 'Netdisk fast download',
|
name: 'Netdisk fast download',
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
5843
web-front/yarn.lock
5843
web-front/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ package cn.qaiu.lz;
|
|||||||
import cn.qaiu.WebClientVertxInit;
|
import cn.qaiu.WebClientVertxInit;
|
||||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||||
|
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
||||||
import cn.qaiu.vx.core.Deploy;
|
import cn.qaiu.vx.core.Deploy;
|
||||||
import cn.qaiu.vx.core.util.ConfigConstant;
|
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||||
import cn.qaiu.vx.core.util.VertxHolder;
|
import cn.qaiu.vx.core.util.VertxHolder;
|
||||||
@@ -11,19 +12,23 @@ import io.vertx.core.json.JsonArray;
|
|||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.core.json.jackson.DatabindCodec;
|
import io.vertx.core.json.jackson.DatabindCodec;
|
||||||
import io.vertx.core.shareddata.LocalMap;
|
import io.vertx.core.shareddata.LocalMap;
|
||||||
|
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
|
import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 程序入口
|
* vertx程序入口
|
||||||
* <br>Create date 2021-05-08 13:00:01
|
|
||||||
*
|
*
|
||||||
* @author qaiu
|
* <br>Create date 2021-05-08 13:00:01
|
||||||
|
* @author qaiu yyzy
|
||||||
*/
|
*/
|
||||||
public class AppMain {
|
public class AppMain {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// start
|
||||||
Deploy.instance().start(args, AppMain::exec);
|
Deploy.instance().start(args, AppMain::exec);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,32 +41,46 @@ public class AppMain {
|
|||||||
private static void exec(JsonObject jsonObject) {
|
private static void exec(JsonObject jsonObject) {
|
||||||
WebClientVertxInit.init(VertxHolder.getVertxInstance());
|
WebClientVertxInit.init(VertxHolder.getVertxInstance());
|
||||||
DatabindCodec.mapper().registerModule(new JavaTimeModule());
|
DatabindCodec.mapper().registerModule(new JavaTimeModule());
|
||||||
|
// 限流
|
||||||
|
if (jsonObject.containsKey("rateLimit")) {
|
||||||
|
JsonObject rateLimit = jsonObject.getJsonObject("rateLimit");
|
||||||
|
RateLimiter.init(rateLimit);
|
||||||
|
}
|
||||||
// 数据库
|
// 数据库
|
||||||
if (jsonObject.getJsonObject(ConfigConstant.SERVER).getBoolean("enableDatabase")) {
|
if (jsonObject.getJsonObject(ConfigConstant.SERVER).getBoolean("enableDatabase")) {
|
||||||
JDBCPoolInit.builder().config(jsonObject.getJsonObject("dataSource")).build().initPool();
|
JDBCPoolInit.builder().config(jsonObject.getJsonObject("dataSource"))
|
||||||
|
.build()
|
||||||
|
.initPool().onSuccess(PreparedStatement -> {
|
||||||
|
VertxHolder.getVertxInstance().setTimer(1000, id -> {
|
||||||
|
System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
|
||||||
|
System.out.println("数据库连接成功");
|
||||||
|
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
|
||||||
|
System.out.println("启动成功: \n本地服务地址: " + addr);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 缓存
|
// 缓存
|
||||||
if (jsonObject.containsKey(ConfigConstant.CACHE)) {
|
if (jsonObject.containsKey(ConfigConstant.CACHE)) {
|
||||||
CacheConfigLoader.init(jsonObject.getJsonObject(ConfigConstant.CACHE));
|
CacheConfigLoader.init(jsonObject.getJsonObject(ConfigConstant.CACHE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData().getLocalMap(LOCAL);
|
||||||
// 代理
|
// 代理
|
||||||
if (jsonObject.containsKey(ConfigConstant.PROXY)) {
|
if (jsonObject.containsKey(ConfigConstant.PROXY)) {
|
||||||
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData().getLocalMap(LOCAL);
|
|
||||||
JsonArray proxyJsonArray = jsonObject.getJsonArray(ConfigConstant.PROXY);
|
JsonArray proxyJsonArray = jsonObject.getJsonArray(ConfigConstant.PROXY);
|
||||||
if (proxyJsonArray != null) {
|
if (proxyJsonArray != null) {
|
||||||
|
JsonObject jsonObject1 = new JsonObject();
|
||||||
proxyJsonArray.forEach(proxyJson -> {
|
proxyJsonArray.forEach(proxyJson -> {
|
||||||
String panTypes = ((JsonObject)proxyJson).getString("panTypes");
|
String panTypes = ((JsonObject)proxyJson).getString("panTypes");
|
||||||
|
|
||||||
if (!panTypes.isEmpty()) {
|
if (!panTypes.isEmpty()) {
|
||||||
JsonObject jsonObject1 = new JsonObject();
|
|
||||||
for (String s : panTypes.split(",")) {
|
for (String s : panTypes.split(",")) {
|
||||||
jsonObject1.put(s, proxyJson);
|
jsonObject1.put(s, proxyJson);
|
||||||
}
|
}
|
||||||
localMap.put("proxy", jsonObject1);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
localMap.put("proxy", jsonObject1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ public class CacheConfigLoader {
|
|||||||
TYPE = config.getString("type");
|
TYPE = config.getString("type");
|
||||||
Integer defaultDuration = config.getInteger("defaultDuration");
|
Integer defaultDuration = config.getInteger("defaultDuration");
|
||||||
DEFAULT_DURATION = defaultDuration == null ? 60 : defaultDuration;
|
DEFAULT_DURATION = defaultDuration == null ? 60 : defaultDuration;
|
||||||
config.getJsonObject("duration").getMap().forEach((k,v) -> {
|
JsonObject duration = config.getJsonObject("duration");
|
||||||
|
if (duration == null) return;
|
||||||
|
duration.getMap().forEach((k, v) -> {
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
CONFIGS.put(k, DEFAULT_DURATION);
|
CONFIGS.put(k, DEFAULT_DURATION);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package cn.qaiu.lz.common.cache;
|
package cn.qaiu.lz.common.cache;
|
||||||
|
|
||||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
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.CacheLinkInfo;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
@@ -8,13 +9,17 @@ import io.vertx.core.json.JsonObject;
|
|||||||
import io.vertx.jdbcclient.JDBCPool;
|
import io.vertx.jdbcclient.JDBCPool;
|
||||||
import io.vertx.sqlclient.Row;
|
import io.vertx.sqlclient.Row;
|
||||||
import io.vertx.sqlclient.templates.SqlTemplate;
|
import io.vertx.sqlclient.templates.SqlTemplate;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class CacheManager {
|
public class CacheManager {
|
||||||
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
|
private final JDBCPool 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) {
|
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 sql = "SELECT share_key as shareKey, direct_link as directLink, expiration FROM cache_link_info WHERE share_key = #{share_key}";
|
||||||
@@ -33,16 +38,31 @@ public class CacheManager {
|
|||||||
cacheHit = new CacheLinkInfo(JsonObject.of("cacheHit", false, "shareKey", cacheKey));
|
cacheHit = new CacheLinkInfo(JsonObject.of("cacheHit", false, "shareKey", cacheKey));
|
||||||
}
|
}
|
||||||
promise.complete(cacheHit);
|
promise.complete(cacheHit);
|
||||||
}).onFailure(Throwable::printStackTrace);
|
}).onFailure(e->{
|
||||||
|
promise.fail(e);
|
||||||
|
LOGGER.error("cache get:", e);
|
||||||
|
});
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 插入或更新缓存数据
|
// 插入或更新缓存数据
|
||||||
public Future<Void> cacheShareLink(CacheLinkInfo cacheLinkInfo) {
|
public Future<Void> cacheShareLink(CacheLinkInfo cacheLinkInfo) {
|
||||||
String sql = "MERGE INTO cache_link_info (share_key, direct_link, expiration) " +
|
String sql;
|
||||||
|
if (jdbcType == JDBCType.MySQL) {
|
||||||
|
sql = """
|
||||||
|
INSERT INTO cache_link_info (share_key, direct_link, expiration)
|
||||||
|
VALUES (#{shareKey}, #{directLink}, #{expiration})
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
direct_link = VALUES(direct_link),
|
||||||
|
expiration = VALUES(expiration);
|
||||||
|
""";
|
||||||
|
} else { // 运行H2
|
||||||
|
sql = "MERGE INTO cache_link_info (share_key, direct_link, expiration) " +
|
||||||
"KEY (share_key) " +
|
"KEY (share_key) " +
|
||||||
"VALUES (#{shareKey}, #{directLink}, #{expiration})";
|
"VALUES (#{shareKey}, #{directLink}, #{expiration})";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 直接传递 CacheLinkInfo 实体类
|
// 直接传递 CacheLinkInfo 实体类
|
||||||
return SqlTemplate.forUpdate(jdbcPool, sql)
|
return SqlTemplate.forUpdate(jdbcPool, sql)
|
||||||
@@ -55,11 +75,23 @@ public class CacheManager {
|
|||||||
public Future<Integer> updateTotalByField(String shareKey, CacheTotalField field) {
|
public Future<Integer> updateTotalByField(String shareKey, CacheTotalField field) {
|
||||||
Promise<Integer> promise = Promise.promise();
|
Promise<Integer> promise = Promise.promise();
|
||||||
String fieldLower = field.name().toLowerCase();
|
String fieldLower = field.name().toLowerCase();
|
||||||
String sql = """
|
String sql;
|
||||||
|
if (jdbcType == JDBCType.MySQL) { // 假设你有一个标识当前数据库类型的布尔变量
|
||||||
|
sql = """
|
||||||
|
INSERT INTO `api_statistics_info` (`pan_type`, `share_key`, `{field}`, `update_ts`)
|
||||||
|
VALUES (#{panType}, #{shareKey}, #{total}, #{ts})
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`pan_type` = VALUES(`pan_type`),
|
||||||
|
`{field}` = VALUES(`{field}`),
|
||||||
|
`update_ts` = VALUES(`update_ts`);
|
||||||
|
""".replace("{field}", fieldLower);
|
||||||
|
} else { // 运行H2
|
||||||
|
sql = """
|
||||||
MERGE INTO `api_statistics_info` (`pan_type`, `share_key`, `{field}`, `update_ts`)
|
MERGE INTO `api_statistics_info` (`pan_type`, `share_key`, `{field}`, `update_ts`)
|
||||||
KEY (`share_key`)
|
KEY (`share_key`)
|
||||||
VALUES (#{panType}, #{shareKey}, #{total}, #{ts})
|
VALUES (#{panType}, #{shareKey}, #{total}, #{ts})
|
||||||
""".replace("{field}", fieldLower);
|
""".replace("{field}", fieldLower);
|
||||||
|
}
|
||||||
|
|
||||||
getShareKeyTotal(shareKey, fieldLower).onSuccess(total -> {
|
getShareKeyTotal(shareKey, fieldLower).onSuccess(total -> {
|
||||||
Integer newTotal = (total == null ? 0 : total) + 1;
|
Integer newTotal = (total == null ? 0 : total) + 1;
|
||||||
@@ -71,7 +103,10 @@ public class CacheManager {
|
|||||||
put("ts", System.currentTimeMillis());
|
put("ts", System.currentTimeMillis());
|
||||||
}})
|
}})
|
||||||
.onSuccess(res -> promise.complete(res.rowCount()))
|
.onSuccess(res -> promise.complete(res.rowCount()))
|
||||||
.onFailure(Throwable::printStackTrace);
|
.onFailure(e->{
|
||||||
|
promise.fail(e);
|
||||||
|
LOGGER.error("updateTotalByField: ", e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
@@ -84,10 +119,12 @@ public class CacheManager {
|
|||||||
|
|
||||||
public Future<Integer> getShareKeyTotal(String shareKey, String name) {
|
public Future<Integer> getShareKeyTotal(String shareKey, String name) {
|
||||||
String sql = """
|
String sql = """
|
||||||
select `share_key`, sum({total_name}) sum_num
|
SELECT `share_key`, SUM({total_name}) AS sum_num
|
||||||
from `api_statistics_info`
|
FROM `api_statistics_info`
|
||||||
group by `share_key` having `share_key` = #{shareKey};
|
WHERE `share_key` = #{shareKey}
|
||||||
|
GROUP BY `share_key`;
|
||||||
""".replace("{total_name}", name);
|
""".replace("{total_name}", name);
|
||||||
|
|
||||||
Promise<Integer> promise = Promise.promise();
|
Promise<Integer> promise = Promise.promise();
|
||||||
Map<String, Object> paramMap = new HashMap<>();
|
Map<String, Object> paramMap = new HashMap<>();
|
||||||
paramMap.put("shareKey", shareKey);
|
paramMap.put("shareKey", shareKey);
|
||||||
@@ -98,16 +135,21 @@ public class CacheManager {
|
|||||||
Integer total = res.iterator().hasNext() ?
|
Integer total = res.iterator().hasNext() ?
|
||||||
res.iterator().next().getInteger("sum_num") : null;
|
res.iterator().next().getInteger("sum_num") : null;
|
||||||
promise.complete(total);
|
promise.complete(total);
|
||||||
|
}).onFailure(e->{
|
||||||
|
promise.fail(e);
|
||||||
|
LOGGER.error("getShareKeyTotal: ", e);
|
||||||
});
|
});
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Future<Map<String, Integer>> getShareKeyTotal(String shareKey) {
|
public Future<Map<String, Integer>> getShareKeyTotal(String shareKey) {
|
||||||
String sql = """
|
String sql = """
|
||||||
select `share_key`, sum(cache_hit_total) hit_total, sum(api_parser_total) parser_total,
|
SELECT `share_key`, SUM(cache_hit_total) AS hit_total, SUM(api_parser_total) AS parser_total
|
||||||
from `api_statistics_info`
|
FROM `api_statistics_info`
|
||||||
group by `share_key` having `share_key` = #{shareKey}
|
WHERE `share_key` = #{shareKey}
|
||||||
|
GROUP BY `share_key`;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
Promise<Map<String, Integer>> promise = Promise.promise();
|
Promise<Map<String, Integer>> promise = Promise.promise();
|
||||||
Map<String, Object> paramMap = new HashMap<>();
|
Map<String, Object> paramMap = new HashMap<>();
|
||||||
paramMap.put("shareKey", shareKey);
|
paramMap.put("shareKey", shareKey);
|
||||||
@@ -125,6 +167,9 @@ public class CacheManager {
|
|||||||
} else {
|
} else {
|
||||||
promise.complete();
|
promise.complete();
|
||||||
}
|
}
|
||||||
|
}).onFailure(e->{
|
||||||
|
promise.fail(e);
|
||||||
|
LOGGER.error("getShareKeyTotal0: ", e);
|
||||||
});
|
});
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package cn.qaiu.lz.common.interceptorImpl;
|
|||||||
|
|
||||||
import cn.qaiu.vx.core.annotaions.HandleSortFilter;
|
import cn.qaiu.vx.core.annotaions.HandleSortFilter;
|
||||||
import cn.qaiu.vx.core.interceptor.BeforeInterceptor;
|
import cn.qaiu.vx.core.interceptor.BeforeInterceptor;
|
||||||
|
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||||
import cn.qaiu.vx.core.util.SharedDataUtil;
|
import cn.qaiu.vx.core.util.SharedDataUtil;
|
||||||
import io.vertx.core.json.JsonArray;
|
import io.vertx.core.json.JsonArray;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import static cn.qaiu.vx.core.util.ConfigConstant.IGNORES_REG;
|
import static cn.qaiu.vx.core.util.ConfigConstant.IGNORES_REG;
|
||||||
|
import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 前置拦截器实现
|
* 前置拦截器实现
|
||||||
@@ -20,8 +23,43 @@ public class DefaultInterceptor implements BeforeInterceptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(RoutingContext ctx) {
|
public void handle(RoutingContext ctx) {
|
||||||
// System.out.println("进入前置拦截器1->" + ctx.request().path());
|
// 读取配置 如果配置了限流 则进行限流
|
||||||
|
if (!SharedDataUtil.getJsonConfig(ConfigConstant.GLOBAL_CONFIG).containsKey("rateLimit")) {
|
||||||
doNext(ctx);
|
doNext(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonObject rateLimit = SharedDataUtil.getJsonConfig(ConfigConstant.GLOBAL_CONFIG)
|
||||||
|
.getJsonObject("rateLimit");
|
||||||
|
// # 限流配置
|
||||||
|
//rateLimit:
|
||||||
|
// # 是否启用限流
|
||||||
|
// enable: true
|
||||||
|
// # 限流的请求数
|
||||||
|
// limit: 1000
|
||||||
|
// # 限流的时间窗口(单位秒)
|
||||||
|
// timeWindow: 60
|
||||||
|
if (rateLimit.getBoolean("enable")) {
|
||||||
|
// 获取当前请求的路径
|
||||||
|
String path = ctx.request().path();
|
||||||
|
// 正则匹配路径
|
||||||
|
if (ignores.stream().anyMatch(ignore -> path.matches(ignore.toString()))) {
|
||||||
|
// 如果匹配到忽略的路径,则不进行限流
|
||||||
|
doNext(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RateLimiter.checkRateLimit(ctx.request())
|
||||||
|
.onSuccess(v -> {
|
||||||
|
// 继续执行下一个拦截器
|
||||||
|
doNext(ctx);
|
||||||
|
})
|
||||||
|
.onFailure(t -> {
|
||||||
|
// 限流失败,返回错误响应
|
||||||
|
log.warn("Rate limit exceeded for path: {}", path);
|
||||||
|
ctx.response().putHeader(CONTENT_TYPE, "text/html; charset=utf-8")
|
||||||
|
.setStatusCode(429)
|
||||||
|
.end(t.getMessage());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package cn.qaiu.lz.common.interceptorImpl;
|
||||||
|
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class RateLimiter {
|
||||||
|
|
||||||
|
private static final Map<String, RequestInfo> ipRequestMap = new ConcurrentHashMap<>();
|
||||||
|
private static int MAX_REQUESTS = 10; // 最大请求次数
|
||||||
|
private static long TIME_WINDOW = 60 * 1000; // 时间窗口(毫秒)
|
||||||
|
|
||||||
|
private static String PATH_REG; // 限流路径正则
|
||||||
|
|
||||||
|
public static void init(JsonObject rateLimitConfig) {
|
||||||
|
MAX_REQUESTS = rateLimitConfig.getInteger("limit", 10);
|
||||||
|
TIME_WINDOW = rateLimitConfig.getInteger("timeWindow", 60) * 1000L; // 转换为毫秒
|
||||||
|
PATH_REG = rateLimitConfig.getString("pathReg", "/.*");
|
||||||
|
log.info("RateLimiter initialized with max requests: {}, time window: {} ms, path regex: {}",
|
||||||
|
MAX_REQUESTS, TIME_WINDOW, PATH_REG);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized public static Future<Void> checkRateLimit(HttpServerRequest request) {
|
||||||
|
Promise<Void> promise = Promise.promise();
|
||||||
|
if (!request.path().matches(PATH_REG)) {
|
||||||
|
// 如果请求路径不匹配正则,则不进行限流
|
||||||
|
promise.complete();
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
String ip = request.remoteAddress().host();
|
||||||
|
|
||||||
|
ipRequestMap.compute(ip, (key, requestInfo) -> {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
if (requestInfo == null || currentTime - requestInfo.timestamp > TIME_WINDOW) {
|
||||||
|
// 初始化或重置计数器
|
||||||
|
return new RequestInfo(1, currentTime);
|
||||||
|
} else {
|
||||||
|
// 增加计数器
|
||||||
|
requestInfo.count++;
|
||||||
|
return requestInfo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
RequestInfo info = ipRequestMap.get(ip);
|
||||||
|
if (info.count > MAX_REQUESTS) {
|
||||||
|
// 超过限制
|
||||||
|
// 计算剩余时间
|
||||||
|
long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp);
|
||||||
|
BigDecimal bigDecimal = BigDecimal.valueOf(remainingTime / 1000.0)
|
||||||
|
.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
promise.fail("请求次数太多了,请" + bigDecimal + "秒后再试。");
|
||||||
|
} else {
|
||||||
|
// 未超过限制,继续处理
|
||||||
|
promise.complete();
|
||||||
|
}
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RequestInfo {
|
||||||
|
int count;
|
||||||
|
long timestamp;
|
||||||
|
|
||||||
|
RequestInfo(int count, long time) {
|
||||||
|
this.count = count;
|
||||||
|
this.timestamp = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ public class URLParamUtil {
|
|||||||
boolean firstParam = !decodedUrl.contains("?");
|
boolean firstParam = !decodedUrl.contains("?");
|
||||||
|
|
||||||
for (String paramName : params.names()) {
|
for (String paramName : params.names()) {
|
||||||
if (!paramName.equals("url") && !paramName.equals("pwd")) { // 忽略 "url" 和 "pwd" 参数
|
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId") && !paramName.equals("uuid")) { // 忽略 "url" 和 "pwd" 参数
|
||||||
if (firstParam) {
|
if (firstParam) {
|
||||||
urlBuilder.append("?");
|
urlBuilder.append("?");
|
||||||
firstParam = false;
|
firstParam = false;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package cn.qaiu.lz.web.controller;
|
package cn.qaiu.lz.web.controller;
|
||||||
|
|
||||||
|
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.lz.common.cache.CacheManager;
|
import cn.qaiu.lz.common.cache.CacheManager;
|
||||||
import cn.qaiu.lz.common.util.URLParamUtil;
|
import cn.qaiu.lz.common.util.URLParamUtil;
|
||||||
|
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
||||||
import cn.qaiu.lz.web.model.LinkInfoResp;
|
import cn.qaiu.lz.web.model.LinkInfoResp;
|
||||||
import cn.qaiu.lz.web.model.StatisticsInfo;
|
import cn.qaiu.lz.web.model.StatisticsInfo;
|
||||||
import cn.qaiu.lz.web.model.SysUser;
|
import cn.qaiu.lz.web.model.SysUser;
|
||||||
@@ -14,32 +16,29 @@ import cn.qaiu.parser.ParserCreate;
|
|||||||
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
||||||
import cn.qaiu.vx.core.annotaions.RouteMapping;
|
import cn.qaiu.vx.core.annotaions.RouteMapping;
|
||||||
import cn.qaiu.vx.core.enums.RouteMethod;
|
import cn.qaiu.vx.core.enums.RouteMethod;
|
||||||
|
import cn.qaiu.vx.core.model.JsonResult;
|
||||||
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
||||||
|
import cn.qaiu.vx.core.util.ResponseUtil;
|
||||||
import cn.qaiu.vx.core.util.SharedDataUtil;
|
import cn.qaiu.vx.core.util.SharedDataUtil;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.http.HttpServerRequest;
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
|
import io.vertx.core.http.HttpServerResponse;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.net.URLEncoder;
|
||||||
import java.util.List;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Map;
|
import java.util.*;
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RouteHandler(value = "/v2", order = 10)
|
@RouteHandler(value = "/v2", order = 10)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ParserApi {
|
public class ParserApi {
|
||||||
|
|
||||||
private final UserService userService = AsyncServiceUtil.getAsyncServiceInstance(UserService.class);
|
|
||||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||||
|
|
||||||
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
|
||||||
public Future<SysUser> login(SysUser user) {
|
|
||||||
log.info("<------- login: {}", user.getUsername());
|
|
||||||
return userService.login(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
|
@RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
|
||||||
public Future<StatisticsInfo> statisticsInfo() {
|
public Future<StatisticsInfo> statisticsInfo() {
|
||||||
@@ -47,6 +46,7 @@ public class ParserApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final CacheManager cacheManager = new CacheManager();
|
private final CacheManager cacheManager = new CacheManager();
|
||||||
|
private final ServerApi serverApi = new ServerApi();
|
||||||
|
|
||||||
@RouteMapping(value = "/linkInfo", method = RouteMethod.GET)
|
@RouteMapping(value = "/linkInfo", method = RouteMethod.GET)
|
||||||
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd) {
|
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd) {
|
||||||
@@ -57,6 +57,7 @@ public class ParserApi {
|
|||||||
LinkInfoResp build = LinkInfoResp.builder()
|
LinkInfoResp build = LinkInfoResp.builder()
|
||||||
.downLink(getDownLink(parserCreate, false))
|
.downLink(getDownLink(parserCreate, false))
|
||||||
.apiLink(getDownLink(parserCreate, true))
|
.apiLink(getDownLink(parserCreate, true))
|
||||||
|
.viewLink(getViewLink(parserCreate))
|
||||||
.shareLinkInfo(shareLinkInfo).build();
|
.shareLinkInfo(shareLinkInfo).build();
|
||||||
// 解析次数统计
|
// 解析次数统计
|
||||||
shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent"));
|
shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent"));
|
||||||
@@ -84,6 +85,15 @@ public class ParserApi {
|
|||||||
return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix();
|
return linkPrefix + (isJson ? "/json/" : "/d/") + create.genPathSuffix();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String getViewLink(ParserCreate create) {
|
||||||
|
|
||||||
|
String linkPrefix = SharedDataUtil.getJsonStringForServerConfig("domainName");
|
||||||
|
if (StringUtils.isBlank(linkPrefix)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return linkPrefix + "/v2/view/" + create.genPathSuffix();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取支持的网盘列表
|
* 获取支持的网盘列表
|
||||||
* @return list-map: name: 网盘名, type: 网盘标识, url: 网盘域名地址
|
* @return list-map: name: 网盘名, type: 网盘标识, url: 网盘域名地址
|
||||||
@@ -93,8 +103,102 @@ public class ParserApi {
|
|||||||
return Arrays.stream(PanDomainTemplate.values()).map(pan -> new TreeMap<String, String>() {{
|
return Arrays.stream(PanDomainTemplate.values()).map(pan -> new TreeMap<String, String>() {{
|
||||||
put("name", pan.getDisplayName());
|
put("name", pan.getDisplayName());
|
||||||
put("type", pan.name().toLowerCase());
|
put("type", pan.name().toLowerCase());
|
||||||
|
put("shareUrlFormat", pan.getStandardUrlTemplate());
|
||||||
put("url", pan.getPanDomain());
|
put("url", pan.getPanDomain());
|
||||||
}}).collect(Collectors.toList());
|
}}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RouteMapping("/getFileList")
|
||||||
|
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) {
|
||||||
|
String url = URLParamUtil.parserParams(request);
|
||||||
|
ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd);
|
||||||
|
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||||
|
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||||
|
if (StringUtils.isNotBlank(dirId)) {
|
||||||
|
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(uuid)) {
|
||||||
|
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
|
||||||
|
}
|
||||||
|
return parserCreate.createTool().parseFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目录解析下载文件
|
||||||
|
// @RouteMapping("/getFileDownUrl/:type/:param")
|
||||||
|
public Future<String> getFileDownUrl(String type, String param) {
|
||||||
|
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
|
||||||
|
.setShareLinkInfoPwd("-");
|
||||||
|
|
||||||
|
if (param.isEmpty()) {
|
||||||
|
Promise<String> promise = Promise.promise();
|
||||||
|
promise.fail("下载参数为空");
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
String paramStr = new String(Base64.getDecoder().decode(param));
|
||||||
|
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||||
|
shareLinkInfo.getOtherParam().put("paramJson", new JsonObject(paramStr));
|
||||||
|
|
||||||
|
// domainName
|
||||||
|
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||||
|
shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
|
||||||
|
return parserCreate.createTool().parseById();
|
||||||
|
}
|
||||||
|
|
||||||
|
@RouteMapping("/redirectUrl/:type/:param")
|
||||||
|
public Future<Void> redirectUrl(HttpServerResponse response, String type, String param) {
|
||||||
|
Promise<Void> promise = Promise.promise();
|
||||||
|
|
||||||
|
getFileDownUrl(type, param)
|
||||||
|
.onSuccess(res -> ResponseUtil.redirect(response, res))
|
||||||
|
.onFailure(t -> promise.fail(t.fillInStackTrace()));
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览媒体文件
|
||||||
|
*/
|
||||||
|
@RouteMapping(value = "/view/:type/:key", method = RouteMethod.GET, order = 2)
|
||||||
|
public void view(HttpServerRequest request, HttpServerResponse response, String type, String key) {
|
||||||
|
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
|
||||||
|
serverApi.parseKeyJson(request, type, key).onSuccess(res -> {
|
||||||
|
redirect(response, previewURL, res);
|
||||||
|
}).onFailure(e -> {
|
||||||
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void redirect(HttpServerResponse response, String previewURL, CacheLinkInfo res) {
|
||||||
|
String directLink = res.getDirectLink();
|
||||||
|
ResponseUtil.redirect(response, previewURL + URLEncoder.encode(directLink, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览媒体文件-目录预览
|
||||||
|
*/
|
||||||
|
@RouteMapping(value = "/preview", method = RouteMethod.GET, order = 9)
|
||||||
|
public void viewURL(HttpServerRequest request, HttpServerResponse response, String pwd) {
|
||||||
|
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
|
||||||
|
new ServerApi().parseJson(request, pwd).onSuccess(res -> {
|
||||||
|
redirect(response, previewURL, res);
|
||||||
|
}).onFailure(e -> {
|
||||||
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@RouteMapping("/viewUrl/:type/:param")
|
||||||
|
public Future<Void> viewUrl(HttpServerResponse response, String type, String param) {
|
||||||
|
Promise<Void> promise = Promise.promise();
|
||||||
|
|
||||||
|
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
|
||||||
|
getFileDownUrl(type, param)
|
||||||
|
.onSuccess(res -> {
|
||||||
|
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
|
||||||
|
ResponseUtil.redirect(response, url);
|
||||||
|
})
|
||||||
|
.onFailure(t -> promise.fail(t.fillInStackTrace()));
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package cn.qaiu.lz.web.controller;
|
||||||
|
|
||||||
|
import cn.qaiu.lz.web.service.ShoutService;
|
||||||
|
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
||||||
|
import cn.qaiu.vx.core.annotaions.RouteMapping;
|
||||||
|
import cn.qaiu.vx.core.enums.RouteMethod;
|
||||||
|
import cn.qaiu.vx.core.model.JsonResult;
|
||||||
|
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
|
||||||
|
@RouteHandler("/v2/shout")
|
||||||
|
public class ShoutController {
|
||||||
|
|
||||||
|
private final ShoutService shoutService = AsyncServiceUtil.getAsyncServiceInstance(ShoutService.class);
|
||||||
|
|
||||||
|
@RouteMapping(value = "/submit", method = RouteMethod.POST)
|
||||||
|
public Future<JsonObject> submitMessage(RoutingContext ctx) {
|
||||||
|
String content = ctx.body().asJsonObject().getString("content");
|
||||||
|
if (content == null || content.trim().isEmpty()) {
|
||||||
|
return Future.failedFuture("内容不能为空");
|
||||||
|
}
|
||||||
|
return shoutService.submitMessage(content, ctx.request().remoteAddress().host()).compose(code ->
|
||||||
|
Future.succeededFuture(JsonResult.data(code).toJsonObject()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RouteMapping(value = "/retrieve", method = RouteMethod.GET)
|
||||||
|
public Future<JsonObject> retrieveMessage(RoutingContext ctx) {
|
||||||
|
String code = ctx.request().getParam("code");
|
||||||
|
if (code == null || code.length() != 6) {
|
||||||
|
return Future.failedFuture("提取码必须为6位数字");
|
||||||
|
}
|
||||||
|
return shoutService.retrieveMessage(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,13 +21,13 @@ public class ApiStatisticsInfo implements ToJson {
|
|||||||
/**
|
/**
|
||||||
* pan type 单独拿出来便于统计.
|
* pan type 单独拿出来便于统计.
|
||||||
*/
|
*/
|
||||||
@Length(varcharSize = 4)
|
@Length(varcharSize = 16)
|
||||||
private String panType;
|
private String panType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分享key type:key
|
* 分享key type:key
|
||||||
*/
|
*/
|
||||||
@Length(varcharSize = 4096)
|
@Length(varcharSize = 1024)
|
||||||
private String shareKey;
|
private String shareKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cn.qaiu.lz.web.model;
|
|||||||
import cn.qaiu.db.ddl.Length;
|
import cn.qaiu.db.ddl.Length;
|
||||||
import cn.qaiu.db.ddl.Table;
|
import cn.qaiu.db.ddl.Table;
|
||||||
import cn.qaiu.db.ddl.TableGenIgnore;
|
import cn.qaiu.db.ddl.TableGenIgnore;
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.lz.common.ToJson;
|
import cn.qaiu.lz.common.ToJson;
|
||||||
import io.vertx.codegen.annotations.DataObject;
|
import io.vertx.codegen.annotations.DataObject;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
@@ -22,7 +23,7 @@ public class CacheLinkInfo implements ToJson {
|
|||||||
/**
|
/**
|
||||||
* 缓存key: type:ShareKey; e.g. lz:xxxx
|
* 缓存key: type:ShareKey; e.g. lz:xxxx
|
||||||
*/
|
*/
|
||||||
@Length(varcharSize = 4096)
|
@Length(varcharSize = 1024)
|
||||||
private String shareKey;
|
private String shareKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +49,8 @@ public class CacheLinkInfo implements ToJson {
|
|||||||
*/
|
*/
|
||||||
private Long expiration;
|
private Long expiration;
|
||||||
|
|
||||||
|
private FileInfo fileInfo;
|
||||||
|
|
||||||
|
|
||||||
// 使用 JsonObject 构造
|
// 使用 JsonObject 构造
|
||||||
public CacheLinkInfo(JsonObject json) {
|
public CacheLinkInfo(JsonObject json) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class LinkInfoResp {
|
|||||||
// 解析链接
|
// 解析链接
|
||||||
private String downLink;
|
private String downLink;
|
||||||
private String apiLink;
|
private String apiLink;
|
||||||
|
private String viewLink;
|
||||||
private Integer cacheHitTotal;
|
private Integer cacheHitTotal;
|
||||||
private Integer parserTotal;
|
private Integer parserTotal;
|
||||||
private Integer sumTotal;
|
private Integer sumTotal;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cn.qaiu.lz.web.model;
|
||||||
|
|
||||||
|
import cn.qaiu.db.ddl.Constraint;
|
||||||
|
import cn.qaiu.db.ddl.Length;
|
||||||
|
import cn.qaiu.db.ddl.Table;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隔空喊话消息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Table("t_messages")
|
||||||
|
public class ShoutMessage {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Constraint(autoIncrement= true, notNull = true)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Length(varcharSize = 16)
|
||||||
|
@Constraint(notNull = true, uniqueKey = "uk_code")
|
||||||
|
private String code; // 6位提取码
|
||||||
|
|
||||||
|
@Length(varcharSize = 4096)
|
||||||
|
private String content; // 消息内容
|
||||||
|
|
||||||
|
@Length(varcharSize = 32)
|
||||||
|
private String ip; // 发送者IP
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private Date createTime = new Date(); // 创建时间
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private Date expireTime; // 过期时间
|
||||||
|
|
||||||
|
private Boolean isUsed = false; // 是否已使用
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package cn.qaiu.lz.web.model;
|
|||||||
import cn.qaiu.db.ddl.Table;
|
import cn.qaiu.db.ddl.Table;
|
||||||
import cn.qaiu.lz.common.ToJson;
|
import cn.qaiu.lz.common.ToJson;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import io.vertx.codegen.annotations.DataObject;
|
import io.vertx.codegen.annotations.DataObject;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -14,12 +15,27 @@ import java.time.format.DateTimeFormatter;
|
|||||||
@Data
|
@Data
|
||||||
@DataObject
|
@DataObject
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Table("t_user")
|
@Table("sys_user")
|
||||||
public class SysUser implements ToJson {
|
public class SysUser implements ToJson {
|
||||||
private String id;
|
private String id;
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
private String password;
|
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;
|
private Integer age;
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
@@ -28,9 +44,17 @@ public class SysUser implements ToJson {
|
|||||||
this.id = json.getString("id");
|
this.id = json.getString("id");
|
||||||
this.username = json.getString("username");
|
this.username = json.getString("username");
|
||||||
this.password = json.getString("password");
|
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");
|
this.age = json.getInteger("age");
|
||||||
if (json.getString("createTime") != null) {
|
if (json.getString("createTime") != null) {
|
||||||
this.createTime = LocalDateTime.parse(json.getString("createTime"));
|
this.createTime = LocalDateTime.parse(json.getString("createTime"));
|
||||||
}
|
}
|
||||||
|
if (json.getString("lastLoginTime") != null) {
|
||||||
|
this.lastLoginTime = LocalDateTime.parse(json.getString("lastLoginTime"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package cn.qaiu.lz.web.service;
|
||||||
|
|
||||||
|
import cn.qaiu.vx.core.base.BaseAsyncService;
|
||||||
|
import io.vertx.codegen.annotations.ProxyGen;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
|
||||||
|
@ProxyGen
|
||||||
|
public interface ShoutService extends BaseAsyncService {
|
||||||
|
// 提交消息并返回提取码
|
||||||
|
Future<String> submitMessage(String content, String host);
|
||||||
|
|
||||||
|
// 通过提取码获取消息
|
||||||
|
Future<JsonObject> retrieveMessage(String code);
|
||||||
|
}
|
||||||
@@ -4,14 +4,48 @@ import cn.qaiu.lz.web.model.SysUser;
|
|||||||
import cn.qaiu.vx.core.base.BaseAsyncService;
|
import cn.qaiu.vx.core.base.BaseAsyncService;
|
||||||
import io.vertx.codegen.annotations.ProxyGen;
|
import io.vertx.codegen.annotations.ProxyGen;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* lz-web
|
* 用户服务接口
|
||||||
* <br>Create date 2021/8/27 14:06
|
* <br>Create date 2021/8/27 14:06
|
||||||
*
|
*
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
*/
|
*/
|
||||||
@ProxyGen
|
@ProxyGen
|
||||||
public interface UserService extends BaseAsyncService {
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,11 @@ public class DbServiceImpl implements DbService {
|
|||||||
JDBCPool client = JDBCPoolInit.instance().getPool();
|
JDBCPool client = JDBCPoolInit.instance().getPool();
|
||||||
Promise<StatisticsInfo> promise = Promise.promise();
|
Promise<StatisticsInfo> promise = Promise.promise();
|
||||||
String sql = """
|
String sql = """
|
||||||
select sum(api_parser_total) parserTotal,sum("cache_hit_total") cacheTotal,
|
select sum(api_parser_total) as parserTotal, sum(cache_hit_total) as cacheTotal,
|
||||||
sum(api_parser_total) + sum("cache_hit_total") total
|
sum(api_parser_total) + sum(cache_hit_total) as total
|
||||||
from "api_statistics_info";
|
from api_statistics_info;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
SqlTemplate.forQuery(client, sql).mapTo(StatisticsInfo.class).execute(new HashMap<>()).onSuccess(row -> {
|
SqlTemplate.forQuery(client, sql).mapTo(StatisticsInfo.class).execute(new HashMap<>()).onSuccess(row -> {
|
||||||
StatisticsInfo info;
|
StatisticsInfo info;
|
||||||
if ((info = row.iterator().next()) != null) {
|
if ((info = row.iterator().next()) != null) {
|
||||||
@@ -59,7 +60,10 @@ public class DbServiceImpl implements DbService {
|
|||||||
} else {
|
} else {
|
||||||
promise.fail("t_parser_log_info查询为空");
|
promise.fail("t_parser_log_info查询为空");
|
||||||
}
|
}
|
||||||
}).onFailure(promise::fail);
|
}).onFailure(e->{
|
||||||
|
log.error("getStatisticsInfo: ", e);
|
||||||
|
promise.fail(e);
|
||||||
|
});
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package cn.qaiu.lz.web.service.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||||
|
import cn.qaiu.lz.web.service.ShoutService;
|
||||||
|
import cn.qaiu.vx.core.annotaions.Service;
|
||||||
|
import cn.qaiu.vx.core.model.JsonResult;
|
||||||
|
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.Tuple;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@SuppressWarnings("SqlResolve") // 这里是为了避免检查SQL语句的警告
|
||||||
|
public class ShoutServiceImpl implements ShoutService {
|
||||||
|
private static final int CODE_LENGTH = 6;
|
||||||
|
private static final int EXPIRE_HOURS = 24;
|
||||||
|
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> submitMessage(String content, String host) {
|
||||||
|
Promise<String> promise = Promise.promise();
|
||||||
|
String code = generateRandomCode();
|
||||||
|
// 判断一下当前code是否存在消息
|
||||||
|
LocalDateTime expireTime = LocalDateTime.now().plusHours(EXPIRE_HOURS);
|
||||||
|
|
||||||
|
String sql = "INSERT INTO t_messages (code, content, expire_time, ip) VALUES (?, ?, ?, ?)";
|
||||||
|
|
||||||
|
jdbcPool.preparedQuery(sql)
|
||||||
|
.execute(Tuple.of(code, content,
|
||||||
|
java.sql.Timestamp.from(expireTime.atZone(ZoneId.systemDefault()).toInstant()),
|
||||||
|
host))
|
||||||
|
.onSuccess(res -> {
|
||||||
|
log.info("Message submitted with code: {}", code);
|
||||||
|
promise.complete(code);
|
||||||
|
})
|
||||||
|
.onFailure(err -> {
|
||||||
|
log.error("Failed to submit message", err);
|
||||||
|
promise.fail(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<JsonObject> retrieveMessage(String code) {
|
||||||
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
|
String sql = "SELECT content FROM t_messages WHERE code = ? AND expire_time > NOW()";
|
||||||
|
|
||||||
|
jdbcPool.preparedQuery(sql)
|
||||||
|
.execute(Tuple.of(code))
|
||||||
|
.onSuccess(rows -> {
|
||||||
|
if (rows.size() > 0) {
|
||||||
|
String content = rows.iterator().next().getString("content");
|
||||||
|
// 标记为已使用
|
||||||
|
markAsUsed(code);
|
||||||
|
promise.complete(JsonResult.data(content).toJsonObject());
|
||||||
|
} else {
|
||||||
|
promise.fail("无效的提取码或消息已过期");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onFailure(err -> {
|
||||||
|
log.error("Failed to retrieve message", err);
|
||||||
|
promise.fail(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markAsUsed(String code) {
|
||||||
|
String sql = "UPDATE t_messages SET is_used = TRUE WHERE code = ?";
|
||||||
|
jdbcPool.preparedQuery(sql).execute(Tuple.of(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRandomCode() {
|
||||||
|
Random random = new Random();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||||
|
sb.append(random.nextInt(10));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,414 @@
|
|||||||
package cn.qaiu.lz.web.service.impl;
|
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.model.SysUser;
|
||||||
import cn.qaiu.lz.web.service.UserService;
|
import cn.qaiu.lz.web.service.UserService;
|
||||||
import cn.qaiu.vx.core.annotaions.Service;
|
import cn.qaiu.vx.core.annotaions.Service;
|
||||||
import io.vertx.core.Future;
|
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
|
* <br>Create date 2021/8/27 14:09
|
||||||
*
|
*
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class UserServiceImpl implements UserService {
|
public class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
@Override
|
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
|
||||||
public Future<SysUser> login(SysUser user) {
|
|
||||||
|
|
||||||
try {
|
// 初始化方法,确保管理员用户存在
|
||||||
TimeUnit.SECONDS.sleep(2);
|
public void init() {
|
||||||
} catch (InterruptedException e) {
|
// 检查管理员用户是否存在
|
||||||
throw new RuntimeException(e);
|
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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Future.succeededFuture(user);
|
|
||||||
|
// 新增一个工具方法来过滤敏感信息
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,9 @@ server:
|
|||||||
enableDatabase: true
|
enableDatabase: true
|
||||||
# 服务域名或者IP 生成二维码链接时需要
|
# 服务域名或者IP 生成二维码链接时需要
|
||||||
domainName: http://127.0.0.1:6401
|
domainName: http://127.0.0.1:6401
|
||||||
|
# 预览服务URL
|
||||||
|
previewURL: https://nfd-parser.github.io/nfd-preview/preview.html?src=
|
||||||
|
# domainName: https://lz.qaiu.top
|
||||||
|
|
||||||
# 反向代理服务器配置路径(不用加后缀)
|
# 反向代理服务器配置路径(不用加后缀)
|
||||||
proxyConf: server-proxy
|
proxyConf: server-proxy
|
||||||
@@ -25,19 +28,33 @@ custom:
|
|||||||
routeTimeOut: 15000
|
routeTimeOut: 15000
|
||||||
# 拦截器匹配规则
|
# 拦截器匹配规则
|
||||||
ignoresReg:
|
ignoresReg:
|
||||||
- /v2/statisticsInfo
|
# - /v2/statisticsInfo
|
||||||
- .*/test.*$
|
- .*/test.*$
|
||||||
|
|
||||||
# 参数注入的实体类包路径匹配正则 (防止同名类引发歧义)
|
# 参数注入的实体类包路径匹配正则 (防止同名类引发歧义)
|
||||||
entityPackagesReg:
|
entityPackagesReg:
|
||||||
- ^cn\.qaiu\.lz\.web\.model\..*
|
- ^cn\.qaiu\.lz\.web\.model\..*
|
||||||
|
|
||||||
|
# 限流配置
|
||||||
|
#rateLimit:
|
||||||
|
# # 是否启用限流
|
||||||
|
# enable: true
|
||||||
|
# # 限流的请求数
|
||||||
|
# limit: 10
|
||||||
|
# # 限流的时间窗口(单位秒)
|
||||||
|
# timeWindow: 10
|
||||||
|
# # 路径匹配规则
|
||||||
|
# pathReg: ^/v2/.*
|
||||||
|
|
||||||
|
|
||||||
# 数据源配置
|
# 数据源配置
|
||||||
dataSource:
|
dataSource:
|
||||||
provider_class: io.vertx.ext.jdbc.spi.impl.HikariCPDataSourceProvider
|
# PGSQL配置示例
|
||||||
|
#jdbcUrl: jdbc:postgresql://localhost:5432/zzzmy
|
||||||
|
# MySQL配置示例
|
||||||
|
#jdbcUrl: jdbc:mysql://127.0.0.1:3306/nfddata?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=GMT%2B8&useSSL=false
|
||||||
|
# 内置数据源H2db配置
|
||||||
jdbcUrl: jdbc:h2:file:./db/nfdData;MODE=MySQL;DATABASE_TO_UPPER=FALSE
|
jdbcUrl: jdbc:h2:file:./db/nfdData;MODE=MySQL;DATABASE_TO_UPPER=FALSE
|
||||||
driverClassName: org.h2.Driver
|
|
||||||
username: root
|
username: root
|
||||||
password: '123456'
|
password: '123456'
|
||||||
|
|
||||||
@@ -46,24 +63,26 @@ cache:
|
|||||||
# 该配置未使用,后续加入其他Cache实现时,区分类型
|
# 该配置未使用,后续加入其他Cache实现时,区分类型
|
||||||
type: h2db
|
type: h2db
|
||||||
# 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大
|
# 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大
|
||||||
defaultDuration: 59
|
defaultDuration: 5
|
||||||
# 具体网盘的缓存配置,如果不加配置则不缓存,每次请求都会请求网盘API,格式:网盘标识: 时长
|
# 具体网盘的缓存配置,如果不加配置则不缓存,每次请求都会请求网盘API,格式:网盘标识: 时长
|
||||||
duration:
|
duration:
|
||||||
ce:
|
ce: 5
|
||||||
cow:
|
cow: 5
|
||||||
ec:
|
ec: 5
|
||||||
fc:
|
fc:
|
||||||
fj:
|
fj: 20
|
||||||
iz: 20
|
iz: 20
|
||||||
le: 2879
|
le: 2879
|
||||||
lz: 20
|
lz: 20
|
||||||
qq: 9999999
|
qq: 9999999
|
||||||
ws:
|
qqw: 30
|
||||||
ye:
|
ws: 10
|
||||||
|
ye: -1
|
||||||
mne: 30
|
mne: 30
|
||||||
mqq: 30
|
mqq: 30
|
||||||
mkg: 30
|
mkg: 30
|
||||||
p115: 5
|
p115: 30
|
||||||
|
ct: 30
|
||||||
|
|
||||||
# httpClient静态代理服务器配置(外网代理)
|
# httpClient静态代理服务器配置(外网代理)
|
||||||
proxy:
|
proxy:
|
||||||
@@ -74,7 +93,3 @@ proxy:
|
|||||||
# username:
|
# username:
|
||||||
# password:
|
# password:
|
||||||
|
|
||||||
|
|
||||||
# 代理池配置
|
|
||||||
#ip-pool:
|
|
||||||
# api-url:
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# 网盘分享链接云解析服务 API 测试
|
||||||
|
# 本文件包含了系统所有API接口的测试请求
|
||||||
|
# 使用方法:
|
||||||
|
# 1. 先运行登录接口获取token
|
||||||
|
# 2. 将返回的token替换所有请求中的YOUR_TOKEN_HERE
|
||||||
|
# 3. 对于需要ID的请求,将实际ID替换TOKEN_ID
|
||||||
|
|
||||||
|
### 用户接口 ###
|
||||||
|
|
||||||
|
### 登录接口
|
||||||
|
POST http://localhost:6400/api/user/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin123"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 用户注册
|
||||||
|
POST http://localhost:6400/api/user/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "password123",
|
||||||
|
"email": "testuser@example.com",
|
||||||
|
"phone": "13800138000"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 获取用户信息
|
||||||
|
# 使用登录接口返回的token替换下面的YOUR_TOKEN_HERE
|
||||||
|
GET http://localhost:6400/api/user/info
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
|
||||||
|
|
||||||
|
### 验证Token
|
||||||
|
POST http://localhost:6400/api/user/validate-token
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 更新用户信息
|
||||||
|
PUT http://localhost:6400/api/user/update
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "new-email@example.com",
|
||||||
|
"phone": "13900139000",
|
||||||
|
"avatar": "https://example.com/avatar.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 管理员接口 ###
|
||||||
|
|
||||||
|
### 获取所有网盘Token
|
||||||
|
GET http://localhost:6400/api/admin/tokens
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
|
||||||
|
|
||||||
|
### 添加网盘Token
|
||||||
|
POST http://localhost:6400/api/admin/token
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer YOUR_TOKEN_HERE
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "yidong",
|
||||||
|
"description": "移动云盘token",
|
||||||
|
"token": "abc123xyz456"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 获取单个网盘Token
|
||||||
|
# 替换下面的TOKEN_ID为实际的token ID
|
||||||
|
GET http://localhost:6400/api/admin/token/TOKEN_ID
|
||||||
|
Authorization: Bearer YOUR_TOKEN_HERE
|
||||||
|
|
||||||
|
### 更新网盘Token
|
||||||
|
# 替换下面的TOKEN_ID为实际的token ID
|
||||||
|
PUT http://localhost:6400/api/admin/token/TOKEN_ID
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer YOUR_TOKEN_HERE
|
||||||
|
|
||||||
|
{
|
||||||
|
"description": "更新后的描述",
|
||||||
|
"token": "new-token-value"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 删除网盘Token
|
||||||
|
# 替换下面的TOKEN_ID为实际的token ID
|
||||||
|
DELETE http://localhost:6400/api/admin/token/TOKEN_ID
|
||||||
|
Authorization: Bearer YOUR_TOKEN_HERE
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user