Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55b534da6d | ||
|
|
10f45cbb7c | ||
|
|
f939f0a35b | ||
|
|
185a2bc9e4 | ||
|
|
c619675820 | ||
|
|
ee6c37d8c2 | ||
|
|
a0fef49d7a | ||
|
|
5cc253f697 | ||
|
|
7c6c443a29 | ||
|
|
5f22bea2c9 | ||
|
|
275f7210f6 | ||
|
|
d0f0c5a1bc | ||
|
|
bb0b4b26c3 | ||
|
|
f4b7603949 | ||
|
|
5abc5370fd | ||
|
|
b746306d67 | ||
|
|
21f512c447 | ||
|
|
c4613ef874 | ||
|
|
8693146d8f | ||
|
|
e9747821f4 | ||
|
|
9c4ce83256 | ||
|
|
b36e8ea9d7 | ||
|
|
390ef06bbb | ||
|
|
6f668dcf14 | ||
|
|
beb060542a | ||
|
|
da1a180ce9 | ||
|
|
79ecec5ce9 | ||
|
|
f75d4688bd | ||
|
|
1738f7f791 | ||
|
|
b645d3ae83 | ||
|
|
82048c3b73 | ||
|
|
1601364f3f | ||
|
|
2b1dbeaf42 | ||
|
|
4b5e50b0d9 | ||
|
|
e02422b774 | ||
|
|
676bf5cd40 | ||
|
|
16a2b586fd | ||
|
|
c1927a0ea9 | ||
|
|
cc7cdbb4a6 | ||
|
|
5f53d30be1 | ||
|
|
bf2d7c70c7 | ||
|
|
bc38acded3 | ||
|
|
f32b6cd5d0 | ||
|
|
28f894f988 | ||
|
|
ef0de7ad8c | ||
|
|
8fa1ed8fc2 | ||
|
|
dca0f1f0c1 | ||
|
|
8a5b084915 | ||
|
|
e6171fb34c | ||
|
|
5a625e5c0e | ||
|
|
37c25a9307 | ||
|
|
615149c83f | ||
|
|
a78ea7e5bd | ||
|
|
755a424530 | ||
|
|
c1948a888e | ||
|
|
7d8f4b32f7 | ||
|
|
0a9b053dff | ||
|
|
73440cca45 | ||
|
|
5987e1fb3e | ||
|
|
42722ca1d8 | ||
|
|
6efdea55aa | ||
|
|
f23e78e8dd | ||
|
|
26381fa6b0 | ||
|
|
eedd68d137 | ||
|
|
a04f16bfa4 | ||
|
|
93d6a1276a | ||
|
|
505b1d6c67 | ||
|
|
212ca3a879 | ||
|
|
65a35225d0 | ||
|
|
7ed0c04111 | ||
|
|
e8f70b286e | ||
|
|
e708524a41 | ||
|
|
a01dd06ef2 | ||
|
|
3c656eb880 | ||
|
|
89879e8c05 | ||
|
|
7c7ad6ba1f | ||
|
|
d0115c78f5 | ||
|
|
90e708c25f | ||
|
|
eb4aa1bb8d | ||
|
|
1f3a83b84d | ||
|
|
7bcec7e3b4 | ||
|
|
604ba2eec6 | ||
|
|
680108f499 | ||
|
|
301ed5648e | ||
|
|
b755b6b186 | ||
|
|
fa8da9ae25 | ||
|
|
03509eb723 | ||
|
|
4cd71a72e5 | ||
|
|
626313434a | ||
|
|
1a562a612c | ||
|
|
ddfb00298f | ||
|
|
cd7183f206 | ||
|
|
32bf77c3e0 | ||
|
|
9f6b811785 | ||
|
|
6c9034570c | ||
|
|
fe38437a7c | ||
|
|
06b01bf1f9 | ||
|
|
2fffdc30f5 | ||
|
|
5b701e499a | ||
|
|
25ecdb1078 | ||
|
|
1c884eb3b9 | ||
|
|
e4d2a1db52 | ||
|
|
4553b9a7a0 | ||
|
|
c290f5a6d9 | ||
|
|
8668bce863 | ||
|
|
390287876a | ||
|
|
f860cc1f57 | ||
|
|
f5106e782a | ||
|
|
c784b562c4 | ||
|
|
cd4fa8cc2b | ||
|
|
2041a7fa11 | ||
|
|
924903bd58 | ||
|
|
e3bf2d6d6b | ||
|
|
f87ae22924 | ||
|
|
868d99367d | ||
|
|
9864eeb21d | ||
|
|
510fdc48f6 | ||
|
|
fd110590af | ||
|
|
89caf91688 | ||
|
|
16dfa1500d |
24
.env.example
@@ -1,24 +0,0 @@
|
|||||||
# # 数据库配置
|
|
||||||
# DB_HOST=localhost
|
|
||||||
# DB_USER=your_username
|
|
||||||
# DB_PASSWORD=your_password
|
|
||||||
|
|
||||||
# # API密钥
|
|
||||||
# API_KEY=your_api_key
|
|
||||||
|
|
||||||
# # 其他敏感信息
|
|
||||||
# CLOUD115_TOKEN=your_token
|
|
||||||
|
|
||||||
# 代理信息
|
|
||||||
HTTP_PROXY_HOST=127.0.0.1
|
|
||||||
HTTP_PROXY_PORT=7890
|
|
||||||
|
|
||||||
# 115网盘配置
|
|
||||||
CLOUD115_USER_ID=your_user_id # 用户ID 可以不填
|
|
||||||
CLOUD115_COOKIE=your_cookie
|
|
||||||
|
|
||||||
# 夸克网盘配置
|
|
||||||
QUARK_COOKIE='your_cookie'
|
|
||||||
|
|
||||||
# RSS配置
|
|
||||||
RSS_BASE_URL=https://rsshub.rssforever.com/telegram/channel
|
|
||||||
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
coverage
|
||||||
57
.eslintrc.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
ignorePatterns: ["node_modules", "dist", "build", "coverage"],
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": 0,
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["frontend/**/*.{js,ts,vue}"],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
|
parser: "vue-eslint-parser",
|
||||||
|
parserOptions: {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:vue/vue3-recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
plugins: ["@typescript-eslint", "vue"],
|
||||||
|
rules: {
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"vue/require-default-prop": "off",
|
||||||
|
"vue/no-v-html": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["backend/**/*.{js,ts}"],
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/explicit-function-return-type": 0,
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
50
.github/workflows/docker-build-test.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Build and Push Multi-Arch Docker Image for Test
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # 添加手动触发
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write # 必须授权以推送镜像
|
||||||
|
env:
|
||||||
|
REPO_NAME: ${{ github.repository }}
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
IMAGE_NAME: cloudsaver
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 设置小写镜像名称
|
||||||
|
run: |
|
||||||
|
LOWER_NAME=$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "LOWER_NAME=$LOWER_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 登录到 GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: 登录到 Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: 设置 QEMU 支持多架构
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: 设置 Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: 构建并推送多架构 Docker 镜像
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64 # 指定架构:x86_64 和 ARM64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ env.LOWER_NAME }}:test
|
||||||
|
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:test
|
||||||
56
.github/workflows/docker-image.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Docker Image CI/CD
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*.*.*"] # 支持标签触发(如 v1.0.0)
|
||||||
|
workflow_dispatch: # 添加手动触发
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write # 必须授权以推送镜像
|
||||||
|
env:
|
||||||
|
REPO_NAME: ${{ github.repository }}
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
IMAGE_NAME: cloudsaver
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 设置小写镜像名称和版本
|
||||||
|
run: |
|
||||||
|
LOWER_NAME=$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "LOWER_NAME=$LOWER_NAME" >> $GITHUB_ENV
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 登录到 GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: 登录到 Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: 设置 QEMU 支持多架构
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: 设置 Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: 构建并推送 Docker 镜像
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64 # 指定架构:x86_64 和 ARM64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ env.LOWER_NAME }}:latest
|
||||||
|
ghcr.io/${{ env.LOWER_NAME }}:${{ env.VERSION }}
|
||||||
|
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||||
6
.gitignore
vendored
@@ -1,9 +1,15 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
logs/
|
||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
*.tar
|
||||||
|
|
||||||
|
# 数据库数据
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
# 保留模板
|
# 保留模板
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,6 @@ node_modules
|
|||||||
# 系统文件
|
# 系统文件
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# 版本控制
|
||||||
|
.git
|
||||||
14
.prettierrc.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
semi: true,
|
||||||
|
trailingComma: "es5",
|
||||||
|
singleQuote: false,
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
endOfLine: "auto",
|
||||||
|
arrowParens: "always",
|
||||||
|
bracketSpacing: true,
|
||||||
|
embeddedLanguageFormatting: "auto",
|
||||||
|
htmlWhitespaceSensitivity: "css",
|
||||||
|
vueIndentScriptAndStyle: false,
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"printWidth": 100,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"arrowParens": "always",
|
|
||||||
"vueIndentScriptAndStyle": true
|
|
||||||
}
|
|
||||||
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 构建前端项目
|
||||||
|
FROM node:18-alpine as frontend-build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 构建后端项目
|
||||||
|
FROM node:18-alpine as backend-build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install
|
||||||
|
COPY backend/ ./
|
||||||
|
RUN rm -f database.sqlite
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 生产环境镜像
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# 安装 Nginx
|
||||||
|
RUN apk add --no-cache nginx
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 创建配置和数据目录
|
||||||
|
RUN mkdir -p /app/config /app/data
|
||||||
|
|
||||||
|
# 复制前端构建产物到 Nginx
|
||||||
|
COPY --from=frontend-build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制 Nginx 配置文件
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# 复制后端构建产物到生产环境镜像
|
||||||
|
COPY --from=backend-build /app /app
|
||||||
|
|
||||||
|
# 安装生产环境依赖
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# 设置数据卷
|
||||||
|
VOLUME ["/app/config", "/app/data"]
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8008
|
||||||
|
|
||||||
|
# 启动脚本
|
||||||
|
COPY docker-entrypoint.sh /app/
|
||||||
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
378
README.md
@@ -1,94 +1,366 @@
|
|||||||
# CloudSaver
|
# CloudSaver
|
||||||
|
|
||||||
一个基于 Vue 3 + Express 的网盘资源搜索与转存工具。
|

|
||||||
|

|
||||||
|

|
||||||
|
[](https://github.com/jiangrui1994/CloudSaver/stargazers)
|
||||||
|

|
||||||
|
<a href="https://hellogithub.com/repository/d13663fb959345e7923ecaccc3387571" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d13663fb959345e7923ecaccc3387571&claim_uid=xP1MT4mSvN6wn5K&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||||
|
|
||||||
## 特别声明
|
一个基于 Vue 3 + Express 的网盘资源搜索与转存工具,支持响应式布局,移动端与PC完美适配,可通过 Docker 一键部署。
|
||||||
|
|
||||||
1. 此项目仅供学习交流使用,请勿用于非法用途。
|
官方Telegram群组:[https://t.me/cloud_saver](https://t.me/cloud_saver)
|
||||||
2. 仅支持个人使用,不支持任何形式的 commercial 使用。
|
|
||||||
3. 禁止在项目页面上进行任何形式的广告宣传。
|
|
||||||
4. 所有搜索到的资源均来自第三方,本项目不对其真实性、合法性做出任何保证。
|
|
||||||
|
|
||||||
## 注意事项
|
官方QQ交流群([二维码](https://github.com/jiangrui1994/CloudSaver?tab=readme-ov-file#%E8%81%94%E7%B3%BB%E6%96%B9%E5%BC%8F)):
|
||||||
|
|
||||||
1. 此项目的资源搜索需要用到代理环境,请自行搭建。
|
1039610300(满了)
|
||||||
2. 默认的代理地址为:`http://127.0.0.1`,端口为:`7890`,如需修改,请自行 `.env`中配置。
|
|
||||||
|
389429056(满了)
|
||||||
|
|
||||||
|
版本更新日志:[https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94](https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94)
|
||||||
|
|
||||||
|
CloudSaver部署与使用常见问题(包含更多搜索频道):[https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l](https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l)
|
||||||
|
密码 me16 点个Star呗~
|
||||||
|
|
||||||
|
⚠️关于项目更新与需求处理的核心声明:[https://www.yuque.com/xiaoruihenbangde/ggogn3/gt9cgqn2n3vergxx](https://www.yuque.com/xiaoruihenbangde/ggogn3/gt9cgqn2n3vergxx)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
⚠️**由于某些原因,[新版本](https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94)内容不包含在此开源仓库(停留在V0.2.5版本),如需使用,请使用docker镜像进行部署使用。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🔒 重要安全提醒|关于本项目私有化部署的强制建议**
|
||||||
|
|
||||||
|
为保障您的数据安全与隐私权益,请务必**通过Docker自行私有化部署本项目**。我们**强烈反对**使用任何第三方提供的在线网站或他人部署的服务,原因如下:
|
||||||
|
|
||||||
|
⚠️ **高风险预警**
|
||||||
|
|
||||||
|
- 本项目涉及**网盘Cookie等敏感凭据**,若使用他人服务:
|
||||||
|
▶ 您的账号密码、隐私文件可能遭恶意窃取或篡改
|
||||||
|
▶ 攻击者可利用Cookie直接登录您的网盘实施破坏
|
||||||
|
▶ 数据泄露、资产损失等后果需完全由使用者自行承担
|
||||||
|
|
||||||
|
🚫 **严正声明**
|
||||||
|
|
||||||
|
1. 本项目**从未且不会**提供任何形式的在线服务、公开Demo或托管平台
|
||||||
|
2. **任何声称与本项目相关的在线网站均为未授权第三方搭建**,存在蓄意作恶的高风险
|
||||||
|
3. 如因使用非自建服务导致损失,本项目开发者**不承担任何法律责任**
|
||||||
|
|
||||||
|
❓ **常见问题**
|
||||||
|
Q: 是否有在线Demo可直接试用?
|
||||||
|
A: **绝无可能!** 任何在线服务都与本项目无关,请立即关闭避免信息泄露
|
||||||
|
|
||||||
|
Q: 为何不能使用他人部署好的服务?
|
||||||
|
A: Cookie等同于账号密码,交出Cookie=交出家门钥匙,请勿将身家安全托付陌生人
|
||||||
|
|
||||||
|
Q: 如何确保100%安全?
|
||||||
|
A: 唯一可信方案:通过官方仓库代码+自主服务器部署,全程数据闭环
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🛡️ 最后一次严肃提醒**
|
||||||
|
您的账号安全只应掌握在自己手中!
|
||||||
|
请立即执行私有化部署 ▶ 避免无法挽回的数据灾难
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 支持多个资源订阅(电报群)源搜索
|
- 🔍 多源资源搜索
|
||||||
- 支持 115 网盘与夸克网盘资源转存
|
- 支持多个资源订阅源搜索
|
||||||
- 支持关键词搜索与资源链接解析
|
- 支持关键词搜索与资源链接解析
|
||||||
- 支持转存文件夹展示与选择
|
- 支持豆瓣热门榜单展示
|
||||||
|
- 💾 网盘资源转存
|
||||||
|
- 支持**115 网盘,夸克网盘,天翼网盘,123云盘**一键转存
|
||||||
|
- 支持转存文件夹展示与选择
|
||||||
|
- 👥 多用户系统
|
||||||
|
- 支持用户注册登录
|
||||||
|
- 支持管理员与普通用户权限区分
|
||||||
|
- 📱 响应式设计
|
||||||
|
- 支持 PC 端与移动端自适应布局
|
||||||
|
- 针对不同设备优化的交互体验
|
||||||
|
|
||||||
## 预览
|
## 产品展示
|
||||||
|
|
||||||
### 最新资源搜索
|
<details>
|
||||||
|
<summary>点击展开截图预览</summary>
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-172442.png" width="400">
|
### PC 端
|
||||||
|
|
||||||
### 转存
|
<div align="center">
|
||||||
|
<img src="./docs/images/pc/login.png" width="400" alt="PC登录页面">
|
||||||
|
<img src="./docs/images/pc/douban.png" width="400" alt="PC豆瓣榜单">
|
||||||
|
<p>登录页面/榜单</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-172609.png" width="400">
|
<div align="center">
|
||||||
|
<img src="./docs/images/pc/search.png" width="400" alt="PC资源搜索">
|
||||||
|
<img src="./docs/images/pc/detail.png" width="400" alt="PC资源详情">
|
||||||
|
<p>资源搜索/资源详情</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
### 关键词搜索
|
<div align="center">
|
||||||
|
<img src="./docs/images/pc/save.png" width="400" alt="PC资源转存">
|
||||||
|
<img src="./docs/images/pc/save1.png" width="400" alt="PC资源转存">
|
||||||
|
<p>资源转存</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-172710.png" width="400">
|
### 移动端
|
||||||
|
|
||||||
### 直接链接解析
|
<div align="center">
|
||||||
|
<div style="display: inline-block; margin: 0 20px;">
|
||||||
|
<img src="./docs/images/mobile/login.png" width="200" alt="移动端登录页面">
|
||||||
|
<img src="./docs/images/mobile/search.png" width="200" alt="移动端资源搜索">
|
||||||
|
<img src="./docs/images/mobile/save.png" width="200" alt="移动端资源转存">
|
||||||
|
<img src="./docs/images/mobile/save1.png" width="200" alt="移动端资源转存">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-173136.png" width="400">
|
</details>
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
### 前端
|
### 前端
|
||||||
|
|
||||||
- Vue 3
|
- 核心框架
|
||||||
- TypeScript
|
- Vue 3
|
||||||
- Element Plus
|
- TypeScript
|
||||||
- Pinia
|
- Vite
|
||||||
- Vue Router
|
- 状态管理
|
||||||
- Vite
|
- Pinia
|
||||||
|
- 路由管理
|
||||||
|
- Vue Router
|
||||||
|
- UI 组件库
|
||||||
|
- Element Plus (PC)
|
||||||
|
- Vant (Mobile)
|
||||||
|
- 工具库
|
||||||
|
- Axios
|
||||||
|
|
||||||
### 后端
|
### 后端
|
||||||
|
|
||||||
- Node.js
|
- 运行环境
|
||||||
- Express
|
- Node.js
|
||||||
- TypeScript
|
- Express
|
||||||
- RSS Parser
|
- 数据存储
|
||||||
|
- SQLite3
|
||||||
|
|
||||||
## 环境配置
|
## 环境要求
|
||||||
|
|
||||||
### Node.js 版本
|
- Node.js >= 18.x
|
||||||
|
- pnpm >= 8.x (推荐)
|
||||||
|
|
||||||
本项目需要 `Node.js` 版本 `18.x` 或更高版本。请确保在运行项目之前安装了正确的版本。
|
## 快速开始
|
||||||
|
|
||||||
### 后端配置项
|
### 开发环境
|
||||||
|
|
||||||
复制环境变量模板文件:
|
1. 克隆项目
|
||||||
|
|
||||||
```
|
```bash
|
||||||
cp .env.example ./backend/.env
|
git clone https://github.com/jiangrui1994/CloudSaver.git
|
||||||
|
cd CloudSaver
|
||||||
```
|
```
|
||||||
|
|
||||||
- 根据 `.env.example`文件在 `backend`目录下创建 `.env`文件。
|
2. 安装依赖
|
||||||
- `CLOUD115_COOKIE`:115 用户 cookie
|
|
||||||
- `QUARK_COOKIE`:quark cookie
|
|
||||||
|
|
||||||
## 使用
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
### 开发环境本地运行
|
3. 配置环境变量
|
||||||
|
|
||||||
- 安装依赖:`npm run install`
|
```bash
|
||||||
- 启动开发环境:`npm run dev`
|
cp ./backend/.env.example ./backend/.env
|
||||||
|
```
|
||||||
|
|
||||||
### 打包部署
|
根据 `.env.example` 文件说明配置必要的环境变量。
|
||||||
|
|
||||||
- 前端打包:`npm run build:frontend` 或者进入前端目录`frontend`,执行 `npm run build`
|
4. 启动开发服务器
|
||||||
- 将前端构建产物`dist`目录下的文件复制到服务器上,例如`nginx`的`html`目录下。
|
|
||||||
- 后端服务:先进入后端目录`backend`下`npm run build`构建,然后执行 `npm run start`启动,默认端口为`8009`。
|
|
||||||
- 通过`nginx`配置代理服务,将前端api的请求映射到后端服务。
|
|
||||||
|
|
||||||
## License
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
### 生产环境部署
|
||||||
|
|
||||||
|
1. 构建前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build:frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 构建后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 部署
|
||||||
|
|
||||||
|
说明:镜像源有**两个地址**供选择,下面部署命令中使用的是dockerhub托管的地址为例,github托管的地址请自行替换
|
||||||
|
|
||||||
|
- dockerhub托管:
|
||||||
|
- `jiangrui1994/cloudsaver:latest` 稳定版
|
||||||
|
- `jiangrui1994/cloudsaver:test` 测试版 (包含最新功能和bug修复,但可能不如稳定版稳定)
|
||||||
|
- github托管:
|
||||||
|
- `ghcr.io/jiangrui1994/cloudsaver:latest` 稳定版
|
||||||
|
- `ghcr.io/jiangrui1994/cloudsaver:test` 测试版 (包含最新功能和bug修复,但可能不如稳定版稳定)
|
||||||
|
|
||||||
|
#### 单容器部署
|
||||||
|
|
||||||
|
稳定版:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-p 8008:8008 \
|
||||||
|
-v /your/local/path/data:/app/data \
|
||||||
|
-v /your/local/path/config:/app/config \
|
||||||
|
--name cloud-saver \
|
||||||
|
jiangrui1994/cloudsaver:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
测试版(包含最新功能和bug修复,但可能不如稳定版稳定):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-p 8008:8008 \
|
||||||
|
-v /your/local/path/data:/app/data \
|
||||||
|
-v /your/local/path/config:/app/config \
|
||||||
|
--name cloud-saver \
|
||||||
|
jiangrui1994/cloudsaver:test
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose 部署
|
||||||
|
|
||||||
|
创建 `docker-compose.yml` 文件:
|
||||||
|
|
||||||
|
稳定版:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
cloudsaver:
|
||||||
|
image: jiangrui1994/cloudsaver:latest
|
||||||
|
container_name: cloud-saver
|
||||||
|
ports:
|
||||||
|
- "8008:8008"
|
||||||
|
volumes:
|
||||||
|
- /your/local/path/data:/app/data
|
||||||
|
- /your/local/path/config:/app/config
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
测试版:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
cloudsaver:
|
||||||
|
image: jiangrui1994/cloudsaver:test
|
||||||
|
container_name: cloud-saver
|
||||||
|
ports:
|
||||||
|
- "8008:8008"
|
||||||
|
volumes:
|
||||||
|
- /your/local/path/data:/app/data
|
||||||
|
- /your/local/path/config:/app/config
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
#### /app/config 目录说明
|
||||||
|
|
||||||
|
- `env` 文件:包含后端环境变量配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JWT配置
|
||||||
|
JWT_SECRET=your_jwt_secret_here
|
||||||
|
|
||||||
|
# Telegram配置
|
||||||
|
TELEGRAM_BASE_URL=https://t.me/s
|
||||||
|
|
||||||
|
# Telegram频道配置(0.3.0及之后版本无效)
|
||||||
|
TELE_CHANNELS=[{"id":"xxxx","name":"xxxx资源分享"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**: 测试版(:test标签)包含最新的功能开发和bug修复,但可能存在不稳定因素。建议生产环境使用稳定版(:latest标签)。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 资源搜索需要配置代理环境
|
||||||
|
2. 默认注册码
|
||||||
|
- 管理员:230713
|
||||||
|
- 普通用户:9527
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<div>
|
||||||
|
<p>qq交流群(4群)(1058040657)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 支持项目
|
||||||
|
|
||||||
|
如果您觉得这个项目对您有帮助,可以考虑给予一点支持,这将帮助我们持续改进项目 ❤️
|
||||||
|
|
||||||
|
您可以:
|
||||||
|
|
||||||
|
- ⭐ 给项目点个 Star
|
||||||
|
- 🎉 分享给更多有需要的朋友
|
||||||
|
- ☕ 请作者喝杯冰阔乐或咖啡
|
||||||
|
- 💰 **赞赏了一定记得和我联系**
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<div style="display: inline-block; margin: 0 20px;">
|
||||||
|
<img src="./docs/images/wechat_pay.jpg" height="300" alt="微信打赏">
|
||||||
|
<img src="./docs/images/alipay.png" height="300" alt="支付宝打赏">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 特别声明
|
||||||
|
|
||||||
|
1. 本项目仅供学习交流使用,请勿用于非法用途
|
||||||
|
2. 仅支持个人使用,不支持任何形式的商业使用
|
||||||
|
3. 禁止在项目页面进行任何形式的广告宣传
|
||||||
|
4. 所有搜索到的资源均来自第三方,本项目不对其真实性、合法性做出任何保证
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
1. Fork 本仓库
|
||||||
|
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||||
|
5. 提交 Pull Request
|
||||||
|
|
||||||
|
## 开源协议
|
||||||
|
|
||||||
|
本项目基于 MIT 协议开源 - 查看 [LICENSE](LICENSE) 文件了解更多细节
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
- 👨💻 感谢所有为这个项目做出贡献的开发者们!
|
||||||
|
- 👥 感谢所有使用本项目并提供反馈的用户!
|
||||||
|
- 感谢所有给予支持和鼓励的朋友们!
|
||||||
|
|
||||||
|
## 赞助
|
||||||
|
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
|
||||||
|
|
||||||
|
<a href="https://edgeone.ai/?from=github" target="_blank">亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne</a>
|
||||||
|
|
||||||
|
<img title="亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne" src="https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png" width="300">
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#jiangrui1994/cloudsaver&Date)
|
||||||
|
|||||||
7
TODO.md
@@ -1,7 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
- ~~资源列表增加网盘标识~~
|
|
||||||
- ~~增加对夸克网盘转存的支持~~
|
|
||||||
- ~~增加搜索框直接解析链接~~
|
|
||||||
- ~~替换rsshub改为直接从telegram获取资源信息~~
|
|
||||||
- ~~增加资源列表web源加载更多功能~~
|
|
||||||
9
backend/.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# JWT配置
|
||||||
|
JWT_SECRET=your_jwt_secret_here
|
||||||
|
|
||||||
|
# Telegram配置
|
||||||
|
TELEGRAM_BASE_URL=https://t.me/s
|
||||||
|
|
||||||
|
# Telegram频道配置
|
||||||
|
TELE_CHANNELS=[]
|
||||||
|
|
||||||
@@ -9,19 +9,27 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
|
"inversify": "^7.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"rss-parser": "^3.13.0",
|
"rss-parser": "^3.13.0",
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"tunnel": "^0.0.6"
|
"sqlite3": "^5.1.7",
|
||||||
|
"tunnel": "^0.0.6",
|
||||||
|
"winston": "^3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/node": "^20.11.25",
|
"@types/node": "^20.11.25",
|
||||||
"@types/tunnel": "^0.0.7",
|
"@types/tunnel": "^0.0.7",
|
||||||
"nodemon": "^3.1.0",
|
"nodemon": "^3.1.0",
|
||||||
|
|||||||
@@ -1,30 +1,57 @@
|
|||||||
|
// filepath: /d:/code/CloudDiskDown/backend/src/app.ts
|
||||||
|
import "./types/express";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import { container } from "./inversify.config";
|
||||||
import cookieParser from "cookie-parser";
|
import { TYPES } from "./core/types";
|
||||||
|
import { DatabaseService } from "./services/DatabaseService";
|
||||||
|
import { setupMiddlewares } from "./middleware";
|
||||||
import routes from "./routes/api";
|
import routes from "./routes/api";
|
||||||
|
import { logger } from "./utils/logger";
|
||||||
import { errorHandler } from "./middleware/errorHandler";
|
import { errorHandler } from "./middleware/errorHandler";
|
||||||
|
class App {
|
||||||
|
private app = express();
|
||||||
|
private databaseService = container.get<DatabaseService>(TYPES.DatabaseService);
|
||||||
|
|
||||||
const app = express();
|
constructor() {
|
||||||
|
this.setupExpress();
|
||||||
|
}
|
||||||
|
|
||||||
app.use(
|
private setupExpress(): void {
|
||||||
cors({
|
// 设置中间件
|
||||||
origin: "*",
|
setupMiddlewares(this.app);
|
||||||
credentials: true,
|
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
||||||
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(cookieParser());
|
// 设置路由
|
||||||
app.use(express.json());
|
this.app.use("/", routes);
|
||||||
app.use("/", routes);
|
this.app.use(errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
// 错误处理
|
public async start(): Promise<void> {
|
||||||
app.use(errorHandler);
|
try {
|
||||||
|
// 初始化数据库
|
||||||
|
await this.databaseService.initialize();
|
||||||
|
logger.info("数据库初始化成功");
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
// 启动服务器
|
||||||
app.listen(PORT, () => {
|
const port = process.env.PORT || 8009;
|
||||||
console.log(`Server is running on port ${PORT}`);
|
this.app.listen(port, () => {
|
||||||
|
logger.info(`
|
||||||
|
🚀 服务器启动成功
|
||||||
|
🌍 监听端口: ${port}
|
||||||
|
🔧 运行环境: ${process.env.NODE_ENV || "development"}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("服务器启动失败:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建并启动应用
|
||||||
|
const application = new App();
|
||||||
|
application.start().catch((error) => {
|
||||||
|
logger.error("应用程序启动失败:", error);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default application;
|
||||||
|
|||||||
9
backend/src/config/database.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// backend/src/config/database.ts
|
||||||
|
import { Sequelize } from "sequelize";
|
||||||
|
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: "sqlite",
|
||||||
|
storage: "./data/database.sqlite",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default sequelize;
|
||||||
@@ -11,82 +11,78 @@ interface Channel {
|
|||||||
interface CloudPatterns {
|
interface CloudPatterns {
|
||||||
baiduPan: RegExp;
|
baiduPan: RegExp;
|
||||||
tianyi: RegExp;
|
tianyi: RegExp;
|
||||||
weiyun: RegExp;
|
|
||||||
aliyun: RegExp;
|
aliyun: RegExp;
|
||||||
pan115: RegExp;
|
pan115: RegExp;
|
||||||
|
pan123: RegExp;
|
||||||
quark: RegExp;
|
quark: RegExp;
|
||||||
|
yidong: RegExp;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Cloud115Config {
|
|
||||||
userId: string;
|
|
||||||
cookie: string;
|
|
||||||
}
|
|
||||||
interface QuarkConfig {
|
|
||||||
userId: string;
|
|
||||||
cookie: string;
|
|
||||||
}
|
|
||||||
interface HttpProxyConfig {
|
|
||||||
host: string;
|
|
||||||
port: string;
|
|
||||||
}
|
|
||||||
interface Config {
|
interface Config {
|
||||||
rss: {
|
jwtSecret: string;
|
||||||
|
telegram: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
channels: Channel[];
|
channels: Channel[];
|
||||||
};
|
};
|
||||||
telegram: {
|
|
||||||
baseUrl: string;
|
|
||||||
};
|
|
||||||
httpProxy: HttpProxyConfig;
|
|
||||||
cloudPatterns: CloudPatterns;
|
cloudPatterns: CloudPatterns;
|
||||||
cloud115: Cloud115Config;
|
app: {
|
||||||
quark: QuarkConfig;
|
port: number;
|
||||||
|
env: string;
|
||||||
|
};
|
||||||
|
database: {
|
||||||
|
type: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
jwt: {
|
||||||
|
secret: string;
|
||||||
|
expiresIn: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从环境变量读取频道配置
|
||||||
|
const getTeleChannels = (): Channel[] => {
|
||||||
|
try {
|
||||||
|
const channelsStr = process.env.TELE_CHANNELS;
|
||||||
|
if (channelsStr) {
|
||||||
|
return JSON.parse(channelsStr);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("无法解析 TELE_CHANNELS 环境变量,使用默认配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
rss: {
|
app: {
|
||||||
baseUrl: process.env.RSS_BASE_URL || "https://rsshub.rssforever.com/telegram/channel",
|
port: parseInt(process.env.PORT || "8009"),
|
||||||
channels: [
|
env: process.env.NODE_ENV || "development",
|
||||||
{
|
|
||||||
id: "guaguale115",
|
|
||||||
name: "115网盘资源分享",
|
|
||||||
},
|
},
|
||||||
{
|
database: {
|
||||||
id: "hao115",
|
type: "sqlite",
|
||||||
name: "115网盘资源分享频道",
|
path: "./data/database.sqlite",
|
||||||
},
|
},
|
||||||
{
|
jwt: {
|
||||||
id: "yunpanshare",
|
secret: process.env.JWT_SECRET || "your-secret-key",
|
||||||
name: "网盘资源收藏(夸克)",
|
expiresIn: "6h",
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!",
|
||||||
|
|
||||||
telegram: {
|
telegram: {
|
||||||
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
|
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
|
||||||
|
channels: getTeleChannels(),
|
||||||
},
|
},
|
||||||
|
|
||||||
httpProxy: {
|
|
||||||
host: process.env.HTTP_PROXY_HOST || "127.0.0.1",
|
|
||||||
port: process.env.HTTP_PROXY_PORT || "7890",
|
|
||||||
},
|
|
||||||
|
|
||||||
cloudPatterns: {
|
cloudPatterns: {
|
||||||
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
|
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
|
||||||
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
|
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
|
||||||
weiyun: /https?:\/\/share\.weiyun\.com\/[^\s<>"]+/g,
|
aliyun: /https?:\/\/\w+\.(?:alipan|aliyundrive)\.com\/[^\s<>"]+/g,
|
||||||
aliyun: /https?:\/\/\w+\.aliyundrive\.com\/[^\s<>"]+/g,
|
// pan115有两个域名 115.com 和 anxia.com 和 115cdn.com
|
||||||
// pan115有两个域名 115.com 和 anxia.com
|
pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g,
|
||||||
pan115: /https?:\/\/(?:115|anxia)\.com\/s\/[^\s<>"]+/g,
|
// 修改为匹配所有以123开头的域名
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
pan123: /https?:\/\/(?:www\.)?123[^\/\s<>"]+\.com\/s\/[^\s<>"]+/g,
|
||||||
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
|
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
|
||||||
},
|
yidong: /https?:\/\/caiyun\.139\.com\/[^\s<>"]+/g,
|
||||||
|
|
||||||
cloud115: {
|
|
||||||
userId: "",
|
|
||||||
cookie: process.env.CLOUD115_COOKIE || "",
|
|
||||||
},
|
|
||||||
quark: {
|
|
||||||
userId: process.env.QUARK_USER_ID || "",
|
|
||||||
cookie: process.env.QUARK_COOKIE || "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
32
backend/src/controllers/BaseCloudController.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { BaseController } from "./BaseController";
|
||||||
|
import { ICloudStorageService } from "@/types/services";
|
||||||
|
|
||||||
|
export abstract class BaseCloudController extends BaseController {
|
||||||
|
constructor(protected cloudService: ICloudStorageService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShareInfo(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const { shareCode, receiveCode } = req.query;
|
||||||
|
// await this.cloudService.setCookie(req);
|
||||||
|
return await this.cloudService.getShareInfo(shareCode as string, receiveCode as string);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFolderList(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const { parentCid } = req.query;
|
||||||
|
await this.cloudService.setCookie(req);
|
||||||
|
return await this.cloudService.getFolderList(parentCid as string);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveFile(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
await this.cloudService.setCookie(req);
|
||||||
|
return await this.cloudService.saveSharedFile(req.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
backend/src/controllers/BaseController.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { ApiResponse } from "../core/ApiResponse";
|
||||||
|
interface ApiResponseData<T> {
|
||||||
|
data?: T;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class BaseController {
|
||||||
|
protected async handleRequest<T>(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
action: () => Promise<ApiResponseData<T> | void>
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result = await action();
|
||||||
|
if (result) {
|
||||||
|
res.json(ApiResponse.success(result.data, result.message));
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||||
|
res.status(200).json(ApiResponse.error(errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,11 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { Cloud115Service } from "../services/Cloud115Service";
|
import { Cloud115Service } from "../services/Cloud115Service";
|
||||||
import { config } from "../config";
|
import { injectable, inject } from "inversify";
|
||||||
import handleError from "../utils/handleError";
|
import { TYPES } from "../core/types";
|
||||||
import { handleResponse } from "../utils/responseHandler";
|
import { BaseCloudController } from "./BaseCloudController";
|
||||||
|
|
||||||
const { cookie } = config.cloud115;
|
@injectable()
|
||||||
|
export class Cloud115Controller extends BaseCloudController {
|
||||||
const cloud115 = new Cloud115Service(cookie);
|
constructor(@inject(TYPES.Cloud115Service) cloud115Service: Cloud115Service) {
|
||||||
|
super(cloud115Service);
|
||||||
export const cloud115Controller = {
|
|
||||||
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
|
||||||
try {
|
|
||||||
const { shareCode, receiveCode } = req.query;
|
|
||||||
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
|
|
||||||
handleResponse(res, result, true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "获取分享信息失败", next);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
|
||||||
try {
|
|
||||||
const { parentCid } = req.query;
|
|
||||||
const result = await cloud115.getFolderList(parentCid as string);
|
|
||||||
handleResponse(res, result, true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "获取目录列表失败", next);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveFile(req: Request, res: Response, next: NextFunction) {
|
|
||||||
try {
|
|
||||||
const { shareCode, receiveCode, fileId, folderId } = req.body;
|
|
||||||
const result = await cloud115.saveSharedFile({
|
|
||||||
shareCode,
|
|
||||||
receiveCode,
|
|
||||||
fileId,
|
|
||||||
cid: folderId,
|
|
||||||
});
|
|
||||||
handleResponse(res, result, true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "保存文件失败", next);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
25
backend/src/controllers/douban.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
|
import { DoubanService } from "../services/DoubanService";
|
||||||
|
import { BaseController } from "./BaseController";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class DoubanController extends BaseController {
|
||||||
|
constructor(@inject(TYPES.DoubanService) private doubanService: DoubanService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDoubanHotList(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query;
|
||||||
|
const result = await this.doubanService.getHotList({
|
||||||
|
type: type as string,
|
||||||
|
tag: tag as string,
|
||||||
|
page_limit: page_limit as string,
|
||||||
|
page_start: page_start as string,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
import { QuarkService } from "../services/QuarkService";
|
import { QuarkService } from "../services/QuarkService";
|
||||||
import { config } from "../config";
|
import { BaseCloudController } from "./BaseCloudController";
|
||||||
import { handleResponse } from "../utils/responseHandler";
|
|
||||||
import handleError from "../utils/handleError";
|
|
||||||
|
|
||||||
const { cookie } = config.quark;
|
@injectable()
|
||||||
|
export class QuarkController extends BaseCloudController {
|
||||||
const quark = new QuarkService(cookie);
|
constructor(@inject(TYPES.QuarkService) quarkService: QuarkService) {
|
||||||
|
super(quarkService);
|
||||||
export const quarkController = {
|
|
||||||
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
|
||||||
try {
|
|
||||||
const { pwdId, passcode } = req.query;
|
|
||||||
const result = await quark.getShareInfo(pwdId as string, passcode as string);
|
|
||||||
handleResponse(res, result, true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "获取分享信息失败", next);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
|
||||||
try {
|
|
||||||
const { parentCid } = req.query;
|
|
||||||
const result = await quark.getFolderList(parentCid as string);
|
|
||||||
handleResponse(res, result, true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "获取目录列表失败", next);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveFile(req: Request, res: Response, next: NextFunction) {
|
|
||||||
try {
|
|
||||||
const result = await quark.saveSharedFile(req.body);
|
|
||||||
handleResponse(res, result, true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "保存文件失败", next);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response } from "express";
|
||||||
import { RSSSearcher } from "../services/RSSSearcher";
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
import { Searcher } from "../services/Searcher";
|
import { Searcher } from "../services/Searcher";
|
||||||
import { handleResponse } from "../utils/responseHandler";
|
import { BaseController } from "./BaseController";
|
||||||
import handleError from "../utils/handleError";
|
|
||||||
|
|
||||||
export const resourceController = {
|
@injectable()
|
||||||
async rssSearch(req: Request, res: Response, next: NextFunction) {
|
export class ResourceController extends BaseController {
|
||||||
try {
|
constructor(@inject(TYPES.Searcher) private searcher: Searcher) {
|
||||||
const { keyword } = req.query;
|
super();
|
||||||
const searcher = new RSSSearcher();
|
|
||||||
const result = await searcher.searchAll(keyword as string);
|
|
||||||
handleResponse(res, result, true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "获取资源发生未知错误", next);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
async search(req: Request, res: Response, next: NextFunction) {
|
async search(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
await this.handleRequest(req, res, async () => {
|
||||||
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
|
const { keyword, channelId = "", lastMessageId = "" } = req.query;
|
||||||
const searcher = new Searcher();
|
return await this.searcher.searchAll(
|
||||||
const result = await searcher.searchAll(
|
|
||||||
keyword as string,
|
keyword as string,
|
||||||
channelId as string,
|
channelId as string,
|
||||||
lastMessageId as string
|
lastMessageId as string
|
||||||
);
|
);
|
||||||
handleResponse(res, result, true);
|
});
|
||||||
} catch (error) {
|
|
||||||
handleError(res, error, "获取资源发生未知错误", next);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
|||||||
28
backend/src/controllers/setting.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
|
import { SettingService } from "../services/SettingService";
|
||||||
|
import { BaseController } from "./BaseController";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SettingController extends BaseController {
|
||||||
|
constructor(@inject(TYPES.SettingService) private settingService: SettingService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
const role = Number(req.user?.role);
|
||||||
|
return await this.settingService.getSettings(userId, role);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
const role = Number(req.user?.role);
|
||||||
|
return await this.settingService.saveSettings(userId, role, req.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/src/controllers/sponsors.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
|
import { SponsorsService } from "../services/SponsorsService";
|
||||||
|
import { BaseController } from "./BaseController";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SponsorsController extends BaseController {
|
||||||
|
constructor(@inject(TYPES.SponsorsService) private sponsorsService: SponsorsService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
return await this.sponsorsService.getSponsors();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
30
backend/src/controllers/teleImages.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
|
import { ImageService } from "../services/ImageService";
|
||||||
|
import { BaseController } from "./BaseController";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class ImageController extends BaseController {
|
||||||
|
constructor(@inject(TYPES.ImageService) private imageService: ImageService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImages(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const url = decodeURIComponent((req.query.url as string) || "");
|
||||||
|
const response = await this.imageService.getImages(url);
|
||||||
|
|
||||||
|
// 设置正确的响应头
|
||||||
|
res.setHeader("Content-Type", response.headers["content-type"]);
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
|
||||||
|
// 确保清除任何可能导致304响应的头信息
|
||||||
|
res.removeHeader("etag");
|
||||||
|
res.removeHeader("last-modified");
|
||||||
|
|
||||||
|
// 直接传输图片数据
|
||||||
|
response.data.pipe(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/src/controllers/user.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
|
import { UserService } from "../services/UserService";
|
||||||
|
import { BaseController } from "./BaseController";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class UserController extends BaseController {
|
||||||
|
constructor(@inject(TYPES.UserService) private userService: UserService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const { username, password, registerCode } = req.body;
|
||||||
|
return await this.userService.register(username, password, registerCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleRequest(req, res, async () => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
return await this.userService.login(username, password);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
21
backend/src/core/ApiResponse.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export class ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
message?: string;
|
||||||
|
code: number;
|
||||||
|
|
||||||
|
private constructor(success: boolean, code: number, data?: T, message?: string) {
|
||||||
|
this.success = success;
|
||||||
|
this.code = code;
|
||||||
|
this.data = data;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
static success<T>(data?: T, message = "操作成功"): ApiResponse<T> {
|
||||||
|
return new ApiResponse(true, 0, data, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static error(message: string, code = 10000): ApiResponse<null> {
|
||||||
|
return new ApiResponse(false, code, null, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
backend/src/core/types.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export const TYPES = {
|
||||||
|
DatabaseService: Symbol.for("DatabaseService"),
|
||||||
|
Cloud115Service: Symbol.for("Cloud115Service"),
|
||||||
|
QuarkService: Symbol.for("QuarkService"),
|
||||||
|
Searcher: Symbol.for("Searcher"),
|
||||||
|
DoubanService: Symbol.for("DoubanService"),
|
||||||
|
ImageService: Symbol.for("ImageService"),
|
||||||
|
SettingService: Symbol.for("SettingService"),
|
||||||
|
UserService: Symbol.for("UserService"),
|
||||||
|
SponsorsService: Symbol.for("SponsorsService"),
|
||||||
|
|
||||||
|
Cloud115Controller: Symbol.for("Cloud115Controller"),
|
||||||
|
QuarkController: Symbol.for("QuarkController"),
|
||||||
|
ResourceController: Symbol.for("ResourceController"),
|
||||||
|
DoubanController: Symbol.for("DoubanController"),
|
||||||
|
ImageController: Symbol.for("ImageController"),
|
||||||
|
SettingController: Symbol.for("SettingController"),
|
||||||
|
UserController: Symbol.for("UserController"),
|
||||||
|
SponsorsController: Symbol.for("SponsorsController"),
|
||||||
|
};
|
||||||
45
backend/src/inversify.config.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Container } from "inversify";
|
||||||
|
import { TYPES } from "./core/types";
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { DatabaseService } from "./services/DatabaseService";
|
||||||
|
import { Cloud115Service } from "./services/Cloud115Service";
|
||||||
|
import { QuarkService } from "./services/QuarkService";
|
||||||
|
import { Searcher } from "./services/Searcher";
|
||||||
|
import { DoubanService } from "./services/DoubanService";
|
||||||
|
import { UserService } from "./services/UserService";
|
||||||
|
import { ImageService } from "./services/ImageService";
|
||||||
|
import { SettingService } from "./services/SettingService";
|
||||||
|
import { SponsorsService } from "./services/SponsorsService";
|
||||||
|
// Controllers
|
||||||
|
import { Cloud115Controller } from "./controllers/cloud115";
|
||||||
|
import { QuarkController } from "./controllers/quark";
|
||||||
|
import { ResourceController } from "./controllers/resource";
|
||||||
|
import { DoubanController } from "./controllers/douban";
|
||||||
|
import { ImageController } from "./controllers/teleImages";
|
||||||
|
import { SettingController } from "./controllers/setting";
|
||||||
|
import { UserController } from "./controllers/user";
|
||||||
|
import { SponsorsController } from "./controllers/sponsors";
|
||||||
|
const container = new Container();
|
||||||
|
|
||||||
|
// Services
|
||||||
|
container.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseService).inSingletonScope();
|
||||||
|
container.bind<Cloud115Service>(TYPES.Cloud115Service).to(Cloud115Service).inSingletonScope();
|
||||||
|
container.bind<QuarkService>(TYPES.QuarkService).to(QuarkService).inSingletonScope();
|
||||||
|
container.bind<Searcher>(TYPES.Searcher).to(Searcher).inSingletonScope();
|
||||||
|
container.bind<ImageService>(TYPES.ImageService).to(ImageService).inSingletonScope();
|
||||||
|
container.bind<SettingService>(TYPES.SettingService).to(SettingService).inSingletonScope();
|
||||||
|
container.bind<DoubanService>(TYPES.DoubanService).to(DoubanService).inSingletonScope();
|
||||||
|
container.bind<UserService>(TYPES.UserService).to(UserService).inSingletonScope();
|
||||||
|
container.bind<SponsorsService>(TYPES.SponsorsService).to(SponsorsService).inSingletonScope();
|
||||||
|
// Controllers
|
||||||
|
container.bind<Cloud115Controller>(TYPES.Cloud115Controller).to(Cloud115Controller);
|
||||||
|
container.bind<QuarkController>(TYPES.QuarkController).to(QuarkController);
|
||||||
|
container.bind<ResourceController>(TYPES.ResourceController).to(ResourceController);
|
||||||
|
container.bind<DoubanController>(TYPES.DoubanController).to(DoubanController);
|
||||||
|
container.bind<ImageController>(TYPES.ImageController).to(ImageController);
|
||||||
|
container.bind<SettingController>(TYPES.SettingController).to(SettingController);
|
||||||
|
container.bind<UserController>(TYPES.UserController).to(UserController);
|
||||||
|
container.bind<SponsorsController>(TYPES.SponsorsController).to(SponsorsController);
|
||||||
|
|
||||||
|
export { container };
|
||||||
43
backend/src/middleware/auth.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// filepath: /D:/code/CloudDiskDown/backend/src/middleware/auth.ts
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
import User from "../models/User";
|
||||||
|
import { config } from "../config";
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
user?: {
|
||||||
|
userId: string;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authMiddleware = async (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<void | Response> => {
|
||||||
|
if (req.path === "/user/login" || req.path === "/user/register" || req.path === "/tele-images/") {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ message: "未提供 token" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;
|
||||||
|
|
||||||
|
req.user = {
|
||||||
|
userId: decoded.userId,
|
||||||
|
role: decoded.role,
|
||||||
|
};
|
||||||
|
const user = await User.findOne({ where: { userId: decoded.userId } });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ message: "无效的 token" });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401).json({ message: "无效的 token" });
|
||||||
|
}
|
||||||
|
};
|
||||||
15
backend/src/middleware/cors.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
|
export const cors = () => {
|
||||||
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Cookie");
|
||||||
|
res.header("Access-Control-Allow-Credentials", "true");
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return res.sendStatus(200);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
|
interface CustomError extends Error {
|
||||||
console.error(err);
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errorHandler = (err: CustomError, req: Request, res: Response): void => {
|
||||||
res.status(err.status || 500).json({
|
res.status(err.status || 500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: err.message || "服务器内部错误",
|
error: err.message || "服务器内部错误",
|
||||||
|
|||||||
14
backend/src/middleware/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Application } from "express";
|
||||||
|
import express from "express";
|
||||||
|
import { authMiddleware } from "./auth";
|
||||||
|
import { requestLogger } from "./requestLogger";
|
||||||
|
import { rateLimiter } from "./rateLimiter";
|
||||||
|
import { cors } from "./cors";
|
||||||
|
|
||||||
|
export const setupMiddlewares = (app: Application) => {
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(requestLogger());
|
||||||
|
app.use(rateLimiter());
|
||||||
|
app.use(authMiddleware);
|
||||||
|
};
|
||||||
27
backend/src/middleware/rateLimiter.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
|
const requestCounts = new Map<string, { count: number; timestamp: number }>();
|
||||||
|
const WINDOW_MS = 60 * 1000; // 1分钟窗口
|
||||||
|
const MAX_REQUESTS = 600; // 每个IP每分钟最多60个请求
|
||||||
|
|
||||||
|
export const rateLimiter = () => {
|
||||||
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||||
|
const now = Date.now();
|
||||||
|
const record = requestCounts.get(ip) || { count: 0, timestamp: now };
|
||||||
|
|
||||||
|
if (now - record.timestamp > WINDOW_MS) {
|
||||||
|
record.count = 0;
|
||||||
|
record.timestamp = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
record.count++;
|
||||||
|
requestCounts.set(ip, record);
|
||||||
|
|
||||||
|
if (record.count > MAX_REQUESTS) {
|
||||||
|
return res.status(429).json({ message: "请求过于频繁,请稍后再试" });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
23
backend/src/middleware/requestLogger.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
const excludePaths = ["/tele-images/"];
|
||||||
|
|
||||||
|
export const requestLogger = () => {
|
||||||
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const start = Date.now();
|
||||||
|
res.on("finish", () => {
|
||||||
|
if (excludePaths.includes(req.path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
logger.info({
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
status: res.statusCode,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
export const validateRequest = (requiredParams: string[]) => {
|
export const validateRequest = (
|
||||||
|
requiredParams: string[]
|
||||||
|
): ((req: Request, res: Response, next: NextFunction) => Response | void) => {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]);
|
const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]);
|
||||||
if (missingParams.length > 0) {
|
if (missingParams.length > 0) {
|
||||||
|
|||||||
67
backend/src/models/GlobalSetting.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { DataTypes, Model, Optional } from "sequelize";
|
||||||
|
import sequelize from "../config/database";
|
||||||
|
|
||||||
|
export interface GlobalSettingAttributes {
|
||||||
|
id: number;
|
||||||
|
httpProxyHost: string;
|
||||||
|
httpProxyPort: number;
|
||||||
|
isProxyEnabled: boolean;
|
||||||
|
CommonUserCode: number;
|
||||||
|
AdminUserCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalSettingCreationAttributes extends Optional<GlobalSettingAttributes, "id"> {}
|
||||||
|
|
||||||
|
class GlobalSetting
|
||||||
|
extends Model<GlobalSettingAttributes, GlobalSettingCreationAttributes>
|
||||||
|
implements GlobalSettingAttributes
|
||||||
|
{
|
||||||
|
public id!: number;
|
||||||
|
public httpProxyHost!: string;
|
||||||
|
public httpProxyPort!: number;
|
||||||
|
public isProxyEnabled!: boolean;
|
||||||
|
public CommonUserCode!: number;
|
||||||
|
public AdminUserCode!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalSetting.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
httpProxyHost: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "127.0.0.1",
|
||||||
|
},
|
||||||
|
httpProxyPort: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 7890,
|
||||||
|
},
|
||||||
|
isProxyEnabled: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
CommonUserCode: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 9527,
|
||||||
|
},
|
||||||
|
AdminUserCode: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 230713,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "GlobalSetting",
|
||||||
|
tableName: "global_settings",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GlobalSetting;
|
||||||
62
backend/src/models/User.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { DataTypes, Model, Optional } from "sequelize";
|
||||||
|
import sequelize from "../config/database";
|
||||||
|
|
||||||
|
interface UserAttributes {
|
||||||
|
id: number;
|
||||||
|
userId?: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
role: number; // 修改为数字类型
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserCreationAttributes extends Optional<UserAttributes, "id"> {}
|
||||||
|
|
||||||
|
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
||||||
|
public id!: number;
|
||||||
|
public userId!: number;
|
||||||
|
public username!: string;
|
||||||
|
public password!: string;
|
||||||
|
public role!: number; // 实现数字类型的角色属性
|
||||||
|
}
|
||||||
|
|
||||||
|
User.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false, // 显式设置为不可为空
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID, // 对外暴露的不可预测ID
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
unique: true,
|
||||||
|
allowNull: false, // 显式设置为不可为空
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: DataTypes.INTEGER, // 修改为数字类型
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0, // 默认值为普通用户
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "User",
|
||||||
|
tableName: "users",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 角色映射
|
||||||
|
// 0: 普通用户
|
||||||
|
// 1: 管理员
|
||||||
|
|
||||||
|
export default User;
|
||||||
72
backend/src/models/UserSetting.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { DataTypes, Model, Optional } from "sequelize";
|
||||||
|
import sequelize from "../config/database";
|
||||||
|
import User from "./User";
|
||||||
|
|
||||||
|
interface UserSettingAttributes {
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
cloud115UserId?: string;
|
||||||
|
cloud115Cookie: string;
|
||||||
|
quarkCookie: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserSettingCreationAttributes extends Optional<UserSettingAttributes, "id"> {}
|
||||||
|
|
||||||
|
class UserSetting
|
||||||
|
extends Model<UserSettingAttributes, UserSettingCreationAttributes>
|
||||||
|
implements UserSettingAttributes
|
||||||
|
{
|
||||||
|
public id!: number;
|
||||||
|
public userId!: string;
|
||||||
|
public cloud115UserId?: string;
|
||||||
|
public cloud115Cookie!: string;
|
||||||
|
public quarkCookie!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSetting.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
references: {
|
||||||
|
model: User,
|
||||||
|
key: "userId",
|
||||||
|
},
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
},
|
||||||
|
cloud115UserId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
cloud115Cookie: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
quarkCookie: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "UserSetting",
|
||||||
|
tableName: "user_settings",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
User.hasOne(UserSetting, {
|
||||||
|
foreignKey: "userId",
|
||||||
|
as: "settings",
|
||||||
|
});
|
||||||
|
UserSetting.belongsTo(User, {
|
||||||
|
foreignKey: "userId",
|
||||||
|
as: "user",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserSetting;
|
||||||
@@ -1,22 +1,55 @@
|
|||||||
import express from "express";
|
import { Router } from "express";
|
||||||
import { cloud115Controller } from "../controllers/cloud115";
|
import { container } from "../inversify.config";
|
||||||
import { quarkController } from "../controllers/quark";
|
import { TYPES } from "../core/types";
|
||||||
import { resourceController } from "../controllers/resource";
|
import { Cloud115Controller } from "../controllers/cloud115";
|
||||||
|
import { QuarkController } from "../controllers/quark";
|
||||||
|
import { ResourceController } from "../controllers/resource";
|
||||||
|
import { DoubanController } from "../controllers/douban";
|
||||||
|
import { ImageController } from "../controllers/teleImages";
|
||||||
|
import { SettingController } from "../controllers/setting";
|
||||||
|
import { UserController } from "../controllers/user";
|
||||||
|
import { SponsorsController } from "../controllers/sponsors";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// 获取控制器实例
|
||||||
|
const cloud115Controller = container.get<Cloud115Controller>(TYPES.Cloud115Controller);
|
||||||
|
const quarkController = container.get<QuarkController>(TYPES.QuarkController);
|
||||||
|
const resourceController = container.get<ResourceController>(TYPES.ResourceController);
|
||||||
|
const doubanController = container.get<DoubanController>(TYPES.DoubanController);
|
||||||
|
const imageController = container.get<ImageController>(TYPES.ImageController);
|
||||||
|
const settingController = container.get<SettingController>(TYPES.SettingController);
|
||||||
|
const userController = container.get<UserController>(TYPES.UserController);
|
||||||
|
const sponsorsController = container.get<SponsorsController>(TYPES.SponsorsController);
|
||||||
|
|
||||||
|
// 用户相关路由
|
||||||
|
router.post("/user/login", (req, res) => userController.login(req, res));
|
||||||
|
router.post("/user/register", (req, res) => userController.register(req, res));
|
||||||
|
|
||||||
|
// 图片相关路由
|
||||||
|
router.get("/tele-images", (req, res) => imageController.getImages(req, res));
|
||||||
|
|
||||||
|
// 设置相关路由
|
||||||
|
router.get("/setting/get", (req, res) => settingController.get(req, res));
|
||||||
|
router.post("/setting/save", (req, res) => settingController.save(req, res));
|
||||||
|
|
||||||
// 资源搜索
|
// 资源搜索
|
||||||
router.get("/search", resourceController.search);
|
router.get("/search", (req, res) => resourceController.search(req, res));
|
||||||
router.get("/rssSearch", resourceController.rssSearch);
|
|
||||||
|
// 获取赞助者列表
|
||||||
|
router.get("/sponsors", (req, res) => sponsorsController.get(req, res));
|
||||||
|
|
||||||
// 115网盘相关
|
// 115网盘相关
|
||||||
router.get("/cloud115/share-info", cloud115Controller.getShareInfo);
|
router.get("/cloud115/share-info", (req, res) => cloud115Controller.getShareInfo(req, res));
|
||||||
router.get("/cloud115/folders", cloud115Controller.getFolderList);
|
router.get("/cloud115/folders", (req, res) => cloud115Controller.getFolderList(req, res));
|
||||||
router.post("/cloud115/save", cloud115Controller.saveFile);
|
router.post("/cloud115/save", (req, res) => cloud115Controller.saveFile(req, res));
|
||||||
|
|
||||||
// 夸克网盘相关
|
// 夸克网盘相关
|
||||||
router.get("/quark/share-info", quarkController.getShareInfo);
|
router.get("/quark/share-info", (req, res) => quarkController.getShareInfo(req, res));
|
||||||
router.get("/quark/folders", quarkController.getFolderList);
|
router.get("/quark/folders", (req, res) => quarkController.getFolderList(req, res));
|
||||||
router.post("/quark/save", quarkController.saveFile);
|
router.post("/quark/save", (req, res) => quarkController.saveFile(req, res));
|
||||||
|
|
||||||
|
// 获取豆瓣热门列表
|
||||||
|
router.get("/douban/hot", (req, res) => doubanController.getDoubanHotList(req, res));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
|
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
|
||||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
import { Logger } from "../utils/logger";
|
import { ShareInfoResponse, FolderListResponse, SaveFileParams } from "../types/cloud";
|
||||||
import { config } from "../config/index";
|
import { injectable } from "inversify";
|
||||||
import { ShareInfoResponse } from "../types/cloud115";
|
import { Request } from "express";
|
||||||
|
import UserSetting from "../models/UserSetting";
|
||||||
|
import { ICloudStorageService } from "@/types/services";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
export class Cloud115Service {
|
interface Cloud115ListItem {
|
||||||
|
cid: string;
|
||||||
|
n: string;
|
||||||
|
s: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cloud115FolderItem {
|
||||||
|
cid: string;
|
||||||
|
n: string;
|
||||||
|
ns: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class Cloud115Service implements ICloudStorageService {
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
|
private cookie: string = "";
|
||||||
|
|
||||||
constructor(cookie: string) {
|
constructor() {
|
||||||
if (!cookie) {
|
|
||||||
throw new Error("115网盘需要提供cookie进行身份验证");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api = createAxiosInstance(
|
this.api = createAxiosInstance(
|
||||||
"https://webapi.115.com",
|
"https://webapi.115.com",
|
||||||
AxiosHeaders.from({
|
AxiosHeaders.from({
|
||||||
@@ -29,13 +42,28 @@ export class Cloud115Service {
|
|||||||
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
|
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
cookie: cookie,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.api.interceptors.request.use((config) => {
|
||||||
|
config.headers.cookie = this.cookie;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCookie(req: Request): Promise<void> {
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
const userSetting = await UserSetting.findOne({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
if (userSetting && userSetting.dataValues.cloud115Cookie) {
|
||||||
|
this.cookie = userSetting.dataValues.cloud115Cookie;
|
||||||
|
} else {
|
||||||
|
throw new Error("请先设置115网盘cookie");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
||||||
try {
|
|
||||||
const response = await this.api.get("/share/snap", {
|
const response = await this.api.get("/share/snap", {
|
||||||
params: {
|
params: {
|
||||||
share_code: shareCode,
|
share_code: shareCode,
|
||||||
@@ -45,40 +73,29 @@ export class Cloud115Service {
|
|||||||
cid: "",
|
cid: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data?.state && response.data.data?.list?.length > 0) {
|
if (response.data?.state && response.data.data?.list?.length > 0) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
|
||||||
data: {
|
data: {
|
||||||
list: response.data.data.list.map((item: any) => ({
|
list: response.data.data.list.map((item: Cloud115ListItem) => ({
|
||||||
fileId: item.cid,
|
fileId: item.cid,
|
||||||
fileName: item.n,
|
fileName: item.n,
|
||||||
fileSize: item.s,
|
fileSize: item.s,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
|
logger.error("未找到文件信息:", response.data);
|
||||||
return {
|
throw new Error("未找到文件信息");
|
||||||
success: false,
|
|
||||||
error: "未找到文件信息",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFolderList(parentCid = "0") {
|
async getFolderList(parentCid = "0"): Promise<FolderListResponse> {
|
||||||
try {
|
|
||||||
const response = await this.api.get("/files", {
|
const response = await this.api.get("/files", {
|
||||||
params: {
|
params: {
|
||||||
aid: 1,
|
aid: 1,
|
||||||
cid: parentCid,
|
cid: parentCid,
|
||||||
o: "user_ptime",
|
o: "user_ptime",
|
||||||
asc: 0,
|
asc: 1,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
show_dir: 1,
|
show_dir: 1,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
@@ -86,63 +103,45 @@ export class Cloud115Service {
|
|||||||
format: "json",
|
format: "json",
|
||||||
star: 0,
|
star: 0,
|
||||||
suffix: "",
|
suffix: "",
|
||||||
natsort: 1,
|
natsort: 0,
|
||||||
|
snap: 0,
|
||||||
|
record_open_time: 1,
|
||||||
|
fc_mix: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data?.state) {
|
if (response.data?.state) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
|
||||||
data: response.data.data
|
data: response.data.data
|
||||||
.filter((item: any) => item.cid)
|
.filter((item: Cloud115FolderItem) => item.cid && !!item.ns)
|
||||||
.map((folder: any) => ({
|
.map((folder: Cloud115FolderItem) => ({
|
||||||
cid: folder.cid,
|
cid: folder.cid,
|
||||||
name: folder.n,
|
name: folder.n,
|
||||||
path: response.data.path,
|
path: response.data.path,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
Logger.error("获取目录列表失败:", response.data.error);
|
logger.error("获取目录列表失败:", response.data.error);
|
||||||
return {
|
throw new Error("获取115pan目录列表失败:" + response.data.error);
|
||||||
success: false,
|
|
||||||
error: "获取115pan目录列表失败:" + response.data.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error("获取目录列表失败:", error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "获取115pan目录列表失败",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSharedFile(params: {
|
async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> {
|
||||||
cid: string;
|
|
||||||
shareCode: string;
|
|
||||||
receiveCode: string;
|
|
||||||
fileId: string;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
const param = new URLSearchParams({
|
const param = new URLSearchParams({
|
||||||
cid: params.cid,
|
cid: params.folderId || "",
|
||||||
user_id: config.cloud115.userId,
|
share_code: params.shareCode || "",
|
||||||
share_code: params.shareCode,
|
receive_code: params.receiveCode || "",
|
||||||
receive_code: params.receiveCode,
|
file_id: params.fids?.[0] || "",
|
||||||
file_id: params.fileId,
|
|
||||||
});
|
});
|
||||||
const response = await this.api.post("/share/receive", param.toString());
|
const response = await this.api.post("/share/receive", param.toString());
|
||||||
|
logger.info("保存文件:", response.data);
|
||||||
|
if (response.data.state) {
|
||||||
return {
|
return {
|
||||||
success: response.data.state,
|
message: response.data.error,
|
||||||
error: response.data.error,
|
data: response.data.data,
|
||||||
data: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
logger.error("保存文件失败:", response.data.error);
|
||||||
|
throw new Error("保存115pan文件失败:" + response.data.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
backend/src/services/DatabaseService.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Sequelize, QueryTypes } from "sequelize";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
import { Searcher } from "./Searcher";
|
||||||
|
import sequelize from "../config/database";
|
||||||
|
|
||||||
|
// 全局设置默认值
|
||||||
|
const DEFAULT_GLOBAL_SETTINGS = {
|
||||||
|
httpProxyHost: "127.0.0.1",
|
||||||
|
httpProxyPort: 7890,
|
||||||
|
isProxyEnabled: false,
|
||||||
|
CommonUserCode: 9527,
|
||||||
|
AdminUserCode: 230713,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DatabaseService {
|
||||||
|
private sequelize: Sequelize;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.sequelize = sequelize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.sequelize.query("PRAGMA foreign_keys = OFF");
|
||||||
|
await this.cleanupBackupTables();
|
||||||
|
await this.sequelize.sync({ alter: true });
|
||||||
|
await this.sequelize.query("PRAGMA foreign_keys = ON");
|
||||||
|
await this.initializeGlobalSettings();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`数据库初始化失败: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeGlobalSettings(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const settings = await GlobalSetting.findOne();
|
||||||
|
if (!settings) {
|
||||||
|
await GlobalSetting.create(DEFAULT_GLOBAL_SETTINGS);
|
||||||
|
console.log("✅ Global settings initialized with default values.");
|
||||||
|
}
|
||||||
|
await Searcher.updateAxiosInstance();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to initialize global settings:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanupBackupTables(): Promise<void> {
|
||||||
|
const backupTables = await this.sequelize.query<{ name: string }>(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\_backup%' ESCAPE '\\'",
|
||||||
|
{ type: QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const table of backupTables) {
|
||||||
|
if (table?.name) {
|
||||||
|
await this.sequelize.query(`DROP TABLE IF EXISTS ${table.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其他数据库相关方法
|
||||||
|
}
|
||||||
64
backend/src/services/DoubanService.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { AxiosHeaders, AxiosInstance } from "axios";
|
||||||
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
|
|
||||||
|
interface DoubanSubject {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
rate: string;
|
||||||
|
cover: string;
|
||||||
|
url: string;
|
||||||
|
is_new: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DoubanService {
|
||||||
|
private baseUrl: string;
|
||||||
|
private api: AxiosInstance;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = "https://movie.douban.com/j";
|
||||||
|
this.api = createAxiosInstance(
|
||||||
|
this.baseUrl,
|
||||||
|
AxiosHeaders.from({
|
||||||
|
accept: "*/*",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||||
|
priority: "u=1, i",
|
||||||
|
"sec-ch-ua": '"Not A(Brand";v="8", "Chromium";v="132", "Microsoft Edge";v="132"',
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": '"Windows"',
|
||||||
|
"sec-fetch-dest": "empty",
|
||||||
|
"sec-fetch-mode": "cors",
|
||||||
|
"sec-fetch-site": "same-origin",
|
||||||
|
"x-requested-with": "XMLHttpRequest",
|
||||||
|
cookie:
|
||||||
|
'll="118282"; bid=StA6AQFsAWQ; _pk_id.100001.4cf6=6448be57b1b5ca7e.1723172321.; _vwo_uuid_v2=DC15B8183560FF1E538FFE1D480723310|c08e2d213ecb5510005f90a6ff332121; __utmv=30149280.6282; _vwo_uuid_v2=DC15B8183560FF1E538FFE1D480723310|c08e2d213ecb5510005f90a6ff332121; __utmz=30149280.1731915179.21.6.utmcsr=search.douban.com|utmccn=(referral)|utmcmd=referral|utmcct=/movie/subject_search; __utmz=223695111.1731915179.21.6.utmcsr=search.douban.com|utmccn=(referral)|utmcmd=referral|utmcct=/movie/subject_search; douban-fav-remind=1; __utmc=30149280; __utmc=223695111; _pk_ref.100001.4cf6=%5B%22%22%2C%22%22%2C1739176523%2C%22https%3A%2F%2Fsearch.douban.com%2Fmovie%2Fsubject_search%3Fsearch_text%3D%E8%84%91%E6%B4%9E%E5%A4%A7%E5%BC%80%26cat%3D1002%22%5D; _pk_ses.100001.4cf6=1; ap_v=0,6.0; __utma=30149280.859303574.1723448979.1739167503.1739176523.42; __utmb=30149280.0.10.1739176523; __utma=223695111.1882744177.1723448979.1739167503.1739176523.42; __utmb=223695111.0.10.1739176523',
|
||||||
|
Referer: "https://movie.douban.com/",
|
||||||
|
"Referrer-Policy": "unsafe-url",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHotList(params: {
|
||||||
|
type: string;
|
||||||
|
tag: string;
|
||||||
|
page_limit: string;
|
||||||
|
page_start: string;
|
||||||
|
}): Promise<{ data: DoubanSubject[] }> {
|
||||||
|
try {
|
||||||
|
const response = await this.api.get("/search_subjects", {
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
if (response.data && response.data.subjects) {
|
||||||
|
return {
|
||||||
|
data: response.data.subjects,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching hot list:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
backend/src/services/ImageService.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { injectable } from "inversify";
|
||||||
|
import axios, { AxiosInstance } from "axios";
|
||||||
|
import tunnel from "tunnel";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
import { GlobalSettingAttributes } from "../models/GlobalSetting";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class ImageService {
|
||||||
|
private axiosInstance: AxiosInstance | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// 移除构造函数中的初始化,改为懒加载
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureAxiosInstance(): Promise<AxiosInstance> {
|
||||||
|
if (!this.axiosInstance) {
|
||||||
|
const settings = await GlobalSetting.findOne();
|
||||||
|
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||||
|
|
||||||
|
this.axiosInstance = axios.create({
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
Accept: "image/*, */*",
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||||
|
},
|
||||||
|
withCredentials: false,
|
||||||
|
maxRedirects: 5,
|
||||||
|
httpsAgent: globalSetting.isProxyEnabled
|
||||||
|
? tunnel.httpsOverHttp({
|
||||||
|
proxy: {
|
||||||
|
host: globalSetting.httpProxyHost,
|
||||||
|
port: globalSetting.httpProxyPort,
|
||||||
|
headers: {
|
||||||
|
"Proxy-Authorization": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.axiosInstance.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.axiosInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAxiosInstance(): Promise<void> {
|
||||||
|
this.axiosInstance = null;
|
||||||
|
await this.ensureAxiosInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImages(url: string): Promise<any> {
|
||||||
|
const axiosInstance = await this.ensureAxiosInstance();
|
||||||
|
|
||||||
|
return await axiosInstance.get(url, {
|
||||||
|
responseType: "stream",
|
||||||
|
validateStatus: (status) => status >= 200 && status < 300,
|
||||||
|
headers: {
|
||||||
|
Referer: new URL(url).origin,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,39 @@
|
|||||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
import { AxiosInstance, AxiosHeaders } from "axios";
|
||||||
import { Logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
|
import { injectable } from "inversify";
|
||||||
|
import { Request } from "express";
|
||||||
|
import UserSetting from "../models/UserSetting";
|
||||||
|
import {
|
||||||
|
ShareInfoResponse,
|
||||||
|
FolderListResponse,
|
||||||
|
QuarkFolderItem,
|
||||||
|
SaveFileParams,
|
||||||
|
} from "../types/cloud";
|
||||||
|
import { ICloudStorageService } from "@/types/services";
|
||||||
|
|
||||||
export class QuarkService {
|
interface QuarkShareInfo {
|
||||||
|
stoken?: string;
|
||||||
|
pwdId?: string;
|
||||||
|
fileSize?: number;
|
||||||
|
list: {
|
||||||
|
fid: string;
|
||||||
|
file_name: string;
|
||||||
|
file_type: number;
|
||||||
|
share_fid_token: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class QuarkService implements ICloudStorageService {
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
|
private cookie: string = "";
|
||||||
|
|
||||||
constructor(cookie: string) {
|
constructor() {
|
||||||
if (!cookie) {
|
|
||||||
throw new Error("115网盘需要提供cookie进行身份验证");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api = createAxiosInstance(
|
this.api = createAxiosInstance(
|
||||||
"https://drive-h.quark.cn",
|
"https://drive-h.quark.cn",
|
||||||
AxiosHeaders.from({
|
AxiosHeaders.from({
|
||||||
cookie: cookie,
|
cookie: this.cookie,
|
||||||
accept: "application/json, text/plain, */*",
|
accept: "application/json, text/plain, */*",
|
||||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
@@ -26,41 +46,46 @@ export class QuarkService {
|
|||||||
"sec-fetch-site": "same-site",
|
"sec-fetch-site": "same-site",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.api.interceptors.request.use((config) => {
|
||||||
|
config.headers.cookie = this.cookie;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShareInfo(pwdId: string, passcode = "") {
|
async setCookie(req: Request): Promise<void> {
|
||||||
try {
|
const userId = req.user?.userId;
|
||||||
|
const userSetting = await UserSetting.findOne({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
if (userSetting && userSetting.dataValues.quarkCookie) {
|
||||||
|
this.cookie = userSetting.dataValues.quarkCookie;
|
||||||
|
} else {
|
||||||
|
throw new Error("请先设置夸克网盘cookie");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShareInfo(pwdId: string, passcode = ""): Promise<ShareInfoResponse> {
|
||||||
const response = await this.api.post(
|
const response = await this.api.post(
|
||||||
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
|
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
|
||||||
{
|
{
|
||||||
pwd_id: pwdId,
|
pwd_id: pwdId,
|
||||||
passcode: "",
|
passcode,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (response.data?.status === 200 && response.data.data) {
|
if (response.data?.status === 200 && response.data.data) {
|
||||||
const fileInfo = response.data.data;
|
const fileInfo = response.data.data;
|
||||||
if (fileInfo.stoken) {
|
if (fileInfo.stoken) {
|
||||||
let res = await this.getShareList(pwdId, fileInfo.stoken);
|
const res = await this.getShareList(pwdId, fileInfo.stoken);
|
||||||
return {
|
return {
|
||||||
success: true,
|
|
||||||
data: res,
|
data: res,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
throw new Error("获取夸克分享信息失败");
|
||||||
success: false,
|
|
||||||
error: "未找到文件信息",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShareList(pwdId: string, stoken: string) {
|
async getShareList(pwdId: string, stoken: string): Promise<ShareInfoResponse["data"]> {
|
||||||
try {
|
|
||||||
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
|
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
|
||||||
params: {
|
params: {
|
||||||
pr: "ucpro",
|
pr: "ucpro",
|
||||||
@@ -82,8 +107,8 @@ export class QuarkService {
|
|||||||
});
|
});
|
||||||
if (response.data?.data) {
|
if (response.data?.data) {
|
||||||
const list = response.data.data.list
|
const list = response.data.data.list
|
||||||
.filter((item: any) => item.fid)
|
.filter((item: QuarkShareInfo["list"][0]) => item.fid)
|
||||||
.map((folder: any) => ({
|
.map((folder: QuarkShareInfo["list"][0]) => ({
|
||||||
fileId: folder.fid,
|
fileId: folder.fid,
|
||||||
fileName: folder.file_name,
|
fileName: folder.file_name,
|
||||||
fileIdToken: folder.share_fid_token,
|
fileIdToken: folder.share_fid_token,
|
||||||
@@ -91,21 +116,17 @@ export class QuarkService {
|
|||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
pwdId,
|
pwdId,
|
||||||
stoken: stoken,
|
stoken,
|
||||||
|
fileSize: response.data.data.share?.size || 0,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
list: [],
|
list: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
Logger.error("获取目录列表失败:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFolderList(parentCid = "0") {
|
async getFolderList(parentCid = "0"): Promise<FolderListResponse> {
|
||||||
try {
|
|
||||||
const response = await this.api.get("/1/clouddrive/file/sort", {
|
const response = await this.api.get("/1/clouddrive/file/sort", {
|
||||||
params: {
|
params: {
|
||||||
pr: "ucpro",
|
pr: "ucpro",
|
||||||
@@ -121,58 +142,46 @@ export class QuarkService {
|
|||||||
__t: Date.now(),
|
__t: Date.now(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response.data?.data && response.data.data.list.length) {
|
if (response.data?.data && response.data.data.list) {
|
||||||
return {
|
const data = response.data.data.list
|
||||||
success: true,
|
.filter((item: QuarkFolderItem) => item.fid && item.file_type === 0)
|
||||||
data: response.data.data.list
|
.map((folder: QuarkFolderItem) => ({
|
||||||
.filter((item: any) => item.fid)
|
|
||||||
.map((folder: any) => ({
|
|
||||||
cid: folder.fid,
|
cid: folder.fid,
|
||||||
name: folder.file_name,
|
name: folder.file_name,
|
||||||
path: [],
|
path: [],
|
||||||
})),
|
}));
|
||||||
|
return {
|
||||||
|
data,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
Logger.error("获取目录列表失败:", response.data.error);
|
const message = "获取夸克目录列表失败:" + response.data.error;
|
||||||
return {
|
logger.error(message);
|
||||||
success: false,
|
throw new Error(message);
|
||||||
error: "获取夸克目录列表失败:" + response.data.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error("获取目录列表失败:", error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "获取夸克目录列表失败",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSharedFile(params: {
|
async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> {
|
||||||
fid_list: string[];
|
const quarkParams = {
|
||||||
fid_token_list: string[];
|
fid_list: params.fids,
|
||||||
to_pdir_fid: string;
|
fid_token_list: params.fidTokens,
|
||||||
pwd_id: string;
|
to_pdir_fid: params.folderId,
|
||||||
stoken: string;
|
pwd_id: params.shareCode,
|
||||||
pdir_fid: string;
|
stoken: params.receiveCode,
|
||||||
scene: string;
|
pdir_fid: "0",
|
||||||
}) {
|
scene: "link",
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const response = await this.api.post(
|
const response = await this.api.post(
|
||||||
`/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`,
|
`/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`,
|
||||||
params
|
quarkParams
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: response.data.code === 0,
|
message: response.data.message,
|
||||||
error: response.data.message,
|
|
||||||
data: response.data.data,
|
data: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
throw new Error(error instanceof Error ? error.message : "未知错误");
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import RSSParser from "rss-parser";
|
|
||||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
|
||||||
import { config } from "../config";
|
|
||||||
import { Logger } from "../utils/logger";
|
|
||||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
|
||||||
|
|
||||||
interface RSSItem {
|
|
||||||
title?: string;
|
|
||||||
link?: string;
|
|
||||||
pubDate?: string;
|
|
||||||
content?: string;
|
|
||||||
description?: string;
|
|
||||||
image?: string;
|
|
||||||
cloudLinks?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RSSSearcher {
|
|
||||||
private parser: RSSParser;
|
|
||||||
private axiosInstance: AxiosInstance;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.parser = new RSSParser({
|
|
||||||
customFields: {
|
|
||||||
item: [
|
|
||||||
["content:encoded", "content"],
|
|
||||||
["description", "description"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.axiosInstance = createAxiosInstance(
|
|
||||||
config.rss.baseUrl,
|
|
||||||
AxiosHeaders.from({
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
||||||
Accept: "application/xml,application/xhtml+xml,text/html,application/rss+xml",
|
|
||||||
}),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
|
||||||
const links: string[] = [];
|
|
||||||
let cloudType = "";
|
|
||||||
Object.values(config.cloudPatterns).forEach((pattern, index) => {
|
|
||||||
const matches = text.match(pattern);
|
|
||||||
if (matches) {
|
|
||||||
links.push(...matches);
|
|
||||||
cloudType = Object.keys(config.cloudPatterns)[index];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
links: [...new Set(links)],
|
|
||||||
cloudType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchAll(keyword: string) {
|
|
||||||
const allResults = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < config.rss.channels.length; i++) {
|
|
||||||
const channel = config.rss.channels[i];
|
|
||||||
try {
|
|
||||||
const rssUrl = `${config.rss.baseUrl}/${
|
|
||||||
channel.id
|
|
||||||
}${keyword ? `/searchQuery=${encodeURIComponent(keyword)}` : ""}`;
|
|
||||||
|
|
||||||
const results = await this.searchInRSSFeed(rssUrl);
|
|
||||||
if (results.items.length > 0) {
|
|
||||||
const channelResults = results.items
|
|
||||||
.filter((item: RSSItem) => item.cloudLinks && item.cloudLinks.length > 0)
|
|
||||||
.map((item: RSSItem) => ({
|
|
||||||
...item,
|
|
||||||
channel: channel.name + "(" + channel.id + ")",
|
|
||||||
}));
|
|
||||||
|
|
||||||
allResults.push(...channelResults);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchInRSSFeed(rssUrl: string) {
|
|
||||||
try {
|
|
||||||
const response = await this.axiosInstance.get(rssUrl);
|
|
||||||
const feed = await this.parser.parseString(response.data);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: feed.items.map((item: RSSItem) => {
|
|
||||||
const linkInfo = this.extractCloudLinks(item.content || item.description || "");
|
|
||||||
return {
|
|
||||||
title: item.title || "",
|
|
||||||
link: item.link || "",
|
|
||||||
pubDate: item.pubDate || "",
|
|
||||||
image: item.image || "",
|
|
||||||
cloudLinks: linkInfo.links,
|
|
||||||
cloudType: linkInfo.cloudType,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`RSS源解析错误: ${rssUrl}`, error);
|
|
||||||
return {
|
|
||||||
items: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,43 @@
|
|||||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
import { AxiosInstance, AxiosHeaders } from "axios";
|
||||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
import { GlobalSettingAttributes } from "../models/GlobalSetting";
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
import { Logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { injectable } from "inversify";
|
||||||
|
|
||||||
interface sourceItem {
|
interface sourceItem {
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
completeTitle?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
pubDate?: string;
|
pubDate?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
cloudLinks?: string[];
|
cloudLinks?: string[];
|
||||||
|
tags?: string[];
|
||||||
cloudType?: string;
|
cloudType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
export class Searcher {
|
export class Searcher {
|
||||||
private axiosInstance: AxiosInstance;
|
private static instance: Searcher;
|
||||||
|
private api: AxiosInstance | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.axiosInstance = createAxiosInstance(
|
this.initAxiosInstance();
|
||||||
|
Searcher.instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initAxiosInstance(isUpdate: boolean = false) {
|
||||||
|
let globalSetting = {} as GlobalSettingAttributes;
|
||||||
|
if (isUpdate) {
|
||||||
|
const settings = await GlobalSetting.findOne();
|
||||||
|
globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||||
|
}
|
||||||
|
this.api = createAxiosInstance(
|
||||||
config.telegram.baseUrl,
|
config.telegram.baseUrl,
|
||||||
AxiosHeaders.from({
|
AxiosHeaders.from({
|
||||||
accept:
|
accept:
|
||||||
@@ -37,10 +54,17 @@ export class Searcher {
|
|||||||
"sec-fetch-user": "?1",
|
"sec-fetch-user": "?1",
|
||||||
"upgrade-insecure-requests": "1",
|
"upgrade-insecure-requests": "1",
|
||||||
}),
|
}),
|
||||||
true
|
globalSetting?.isProxyEnabled,
|
||||||
|
globalSetting?.isProxyEnabled
|
||||||
|
? { host: globalSetting?.httpProxyHost, port: globalSetting?.httpProxyPort }
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async updateAxiosInstance(): Promise<void> {
|
||||||
|
await Searcher.instance.initAxiosInstance(true);
|
||||||
|
}
|
||||||
|
|
||||||
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
||||||
const links: string[] = [];
|
const links: string[] = [];
|
||||||
let cloudType = "";
|
let cloudType = "";
|
||||||
@@ -48,7 +72,7 @@ export class Searcher {
|
|||||||
const matches = text.match(pattern);
|
const matches = text.match(pattern);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
links.push(...matches);
|
links.push(...matches);
|
||||||
cloudType = Object.keys(config.cloudPatterns)[index];
|
if (!cloudType) cloudType = Object.keys(config.cloudPatterns)[index];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -58,20 +82,19 @@ export class Searcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchAll(keyword: string, channelId?: string, messageId?: string) {
|
async searchAll(keyword: string, channelId?: string, messageId?: string) {
|
||||||
const allResults = [];
|
const allResults: any[] = [];
|
||||||
const totalChannels = config.rss.channels.length;
|
|
||||||
|
|
||||||
const channelList = channelId
|
const channelList: any[] = channelId
|
||||||
? config.rss.channels.filter((channel) => channel.id === channelId)
|
? config.telegram.channels.filter((channel: any) => channel.id === channelId)
|
||||||
: config.rss.channels;
|
: config.telegram.channels;
|
||||||
|
|
||||||
for (let i = 0; i < channelList.length; i++) {
|
// 使用Promise.all进行并行请求
|
||||||
const channel = channelList[i];
|
const searchPromises = channelList.map(async (channel) => {
|
||||||
try {
|
try {
|
||||||
const messageIdparams = messageId ? `before=${messageId}` : "";
|
const messageIdparams = messageId ? `before=${messageId}` : "";
|
||||||
const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`;
|
const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`;
|
||||||
console.log(`Searching in channel ${channel.name} with URL: ${url}`);
|
console.log(`Searching in channel ${channel.name} with URL: ${url}`);
|
||||||
const results = await this.searchInWeb(url, channel.id);
|
return this.searchInWeb(url).then((results) => {
|
||||||
console.log(`Found ${results.items.length} items in channel ${channel.name}`);
|
console.log(`Found ${results.items.length} items in channel ${channel.name}`);
|
||||||
if (results.items.length > 0) {
|
if (results.items.length > 0) {
|
||||||
const channelResults = results.items
|
const channelResults = results.items
|
||||||
@@ -82,22 +105,42 @@ export class Searcher {
|
|||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
allResults.push(...channelResults);
|
allResults.push({
|
||||||
|
list: channelResults,
|
||||||
|
channelInfo: {
|
||||||
|
...channel,
|
||||||
|
channelLogo: results.channelLogo,
|
||||||
|
},
|
||||||
|
id: channel.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待所有请求完成
|
||||||
|
await Promise.all(searchPromises);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: allResults,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return allResults;
|
async searchInWeb(url: string) {
|
||||||
}
|
|
||||||
|
|
||||||
async searchInWeb(url: string, channelId: string) {
|
|
||||||
try {
|
try {
|
||||||
const response = await this.axiosInstance.get(url);
|
if (!this.api) {
|
||||||
|
throw new Error("Axios instance is not initialized");
|
||||||
|
}
|
||||||
|
const response = await this.api.get(url);
|
||||||
const html = response.data;
|
const html = response.data;
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const items: sourceItem[] = [];
|
const items: sourceItem[] = [];
|
||||||
|
let channelLogo = "";
|
||||||
|
$(".tgme_header_link").each((_, element) => {
|
||||||
|
channelLogo = $(element).find("img").attr("src") || "";
|
||||||
|
});
|
||||||
// 遍历每个消息容器
|
// 遍历每个消息容器
|
||||||
$(".tgme_widget_message_wrap").each((_, element) => {
|
$(".tgme_widget_message_wrap").each((_, element) => {
|
||||||
const messageEl = $(element);
|
const messageEl = $(element);
|
||||||
@@ -109,8 +152,24 @@ export class Searcher {
|
|||||||
?.toString()
|
?.toString()
|
||||||
.split("/")[1];
|
.split("/")[1];
|
||||||
|
|
||||||
// 提取标题 (消息截取100长度)
|
// 提取标题 (第一个<br>标签前的内容)
|
||||||
const title = messageEl.find(".js-message_text").text().trim().substring(0, 50) + "...";
|
const title =
|
||||||
|
messageEl
|
||||||
|
.find(".js-message_text")
|
||||||
|
.html()
|
||||||
|
?.split("<br>")[0]
|
||||||
|
.replace(/<[^>]+>/g, "")
|
||||||
|
.replace(/\n/g, "") || "";
|
||||||
|
|
||||||
|
// 提取描述 (第一个<a>标签前面的内容,不包含标题)
|
||||||
|
const content =
|
||||||
|
messageEl
|
||||||
|
.find(".js-message_text")
|
||||||
|
.html()
|
||||||
|
?.replace(title, "")
|
||||||
|
.split("<a")[0]
|
||||||
|
.replace(/<br>/g, "")
|
||||||
|
.trim() || "";
|
||||||
|
|
||||||
// 提取链接 (消息中的链接)
|
// 提取链接 (消息中的链接)
|
||||||
// const link = messageEl.find('.tgme_widget_message').data('post');
|
// const link = messageEl.find('.tgme_widget_message').data('post');
|
||||||
@@ -118,23 +177,24 @@ export class Searcher {
|
|||||||
// 提取发布时间
|
// 提取发布时间
|
||||||
const pubDate = messageEl.find("time").attr("datetime");
|
const pubDate = messageEl.find("time").attr("datetime");
|
||||||
|
|
||||||
// 提取内容 (完整消息文本)
|
|
||||||
const content = messageEl.find(".js-message_text").text();
|
|
||||||
|
|
||||||
// 提取描述 (消息文本中"描述:"后的内容)
|
|
||||||
const description = content.split("描述:")[1]?.split("\n")[0]?.trim();
|
|
||||||
|
|
||||||
// 提取图片
|
// 提取图片
|
||||||
const image = messageEl
|
const image = messageEl
|
||||||
.find(".tgme_widget_message_photo_wrap")
|
.find(".tgme_widget_message_photo_wrap")
|
||||||
.attr("style")
|
.attr("style")
|
||||||
?.match(/url\('(.+?)'\)/)?.[1];
|
?.match(/url\('(.+?)'\)/)?.[1];
|
||||||
|
|
||||||
|
const tags: string[] = [];
|
||||||
// 提取云盘链接
|
// 提取云盘链接
|
||||||
const links = messageEl
|
const links = messageEl
|
||||||
.find(".tgme_widget_message_text a")
|
.find(".tgme_widget_message_text a")
|
||||||
.map((_, el) => $(el).attr("href"))
|
.map((_, el) => $(el).attr("href"))
|
||||||
.get();
|
.get();
|
||||||
|
messageEl.find(".tgme_widget_message_text a").each((index, element) => {
|
||||||
|
const tagText = $(element).text();
|
||||||
|
if (tagText && tagText.startsWith("#")) {
|
||||||
|
tags.push(tagText);
|
||||||
|
}
|
||||||
|
});
|
||||||
const cloudInfo = this.extractCloudLinks(links.join(" "));
|
const cloudInfo = this.extractCloudLinks(links.join(" "));
|
||||||
// 添加到数组第一位
|
// 添加到数组第一位
|
||||||
items.unshift({
|
items.unshift({
|
||||||
@@ -142,18 +202,21 @@ export class Searcher {
|
|||||||
title,
|
title,
|
||||||
pubDate,
|
pubDate,
|
||||||
content,
|
content,
|
||||||
description,
|
|
||||||
image,
|
image,
|
||||||
cloudLinks: cloudInfo.links,
|
cloudLinks: cloudInfo.links,
|
||||||
cloudType: cloudInfo.cloudType,
|
cloudType: cloudInfo.cloudType,
|
||||||
|
tags,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return { items };
|
return { items: items, channelLogo };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`RSS源解析错误: ${url}`, error);
|
logger.error(`搜索错误: ${url}`, error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
|
channelLogo: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new Searcher();
|
||||||
|
|||||||
59
backend/src/services/SettingService.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { injectable, inject } from "inversify";
|
||||||
|
import { TYPES } from "../core/types";
|
||||||
|
import UserSetting from "../models/UserSetting";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
import { Searcher } from "./Searcher";
|
||||||
|
import { ImageService } from "./ImageService";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SettingService {
|
||||||
|
constructor(@inject(TYPES.ImageService) private imageService: ImageService) {}
|
||||||
|
|
||||||
|
async getSettings(userId: string | undefined, role: number | undefined) {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("用户ID无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
let userSettings = await UserSetting.findOne({ where: { userId: userId.toString() } });
|
||||||
|
if (!userSettings) {
|
||||||
|
userSettings = await UserSetting.create({
|
||||||
|
userId: userId.toString(),
|
||||||
|
cloud115Cookie: "",
|
||||||
|
quarkCookie: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalSetting = await GlobalSetting.findOne();
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
userSettings,
|
||||||
|
globalSetting: role === 1 ? globalSetting : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings(userId: string | undefined, role: number | undefined, settings: any) {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("用户ID无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userSettings, globalSetting } = settings;
|
||||||
|
await UserSetting.update(userSettings, { where: { userId: userId.toString() } });
|
||||||
|
|
||||||
|
if (role === 1 && globalSetting) {
|
||||||
|
await GlobalSetting.update(globalSetting, { where: {} });
|
||||||
|
}
|
||||||
|
await this.updateSettings();
|
||||||
|
return { message: "保存成功" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(/* 参数 */): Promise<void> {
|
||||||
|
// ... 其他代码 ...
|
||||||
|
|
||||||
|
// 修改这一行,使用注入的实例方法而不是静态方法
|
||||||
|
await this.imageService.updateAxiosInstance();
|
||||||
|
await Searcher.updateAxiosInstance();
|
||||||
|
|
||||||
|
// ... 其他代码 ...
|
||||||
|
}
|
||||||
|
}
|
||||||
25
backend/src/services/SponsorsService.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { injectable } from "inversify";
|
||||||
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
|
import { AxiosInstance } from "axios";
|
||||||
|
import sponsors from "../sponsors/sponsors.json";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SponsorsService {
|
||||||
|
private axiosInstance: AxiosInstance;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.axiosInstance = createAxiosInstance("http://oss.jiangmuxin.cn/cloudsaver/");
|
||||||
|
}
|
||||||
|
async getSponsors() {
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance.get("sponsors.json");
|
||||||
|
return {
|
||||||
|
data: response.data.sponsors,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
data: sponsors.sponsors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
backend/src/services/UserService.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { injectable } from "inversify";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { config } from "../config";
|
||||||
|
import User from "../models/User";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class UserService {
|
||||||
|
private isValidInput(input: string): boolean {
|
||||||
|
// 检查是否包含空格或汉字
|
||||||
|
const regex = /^[^\s\u4e00-\u9fa5]+$/;
|
||||||
|
return regex.test(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(username: string, password: string, registerCode: string) {
|
||||||
|
const globalSetting = await GlobalSetting.findOne();
|
||||||
|
const registerCodeList = [
|
||||||
|
globalSetting?.dataValues.CommonUserCode,
|
||||||
|
globalSetting?.dataValues.AdminUserCode,
|
||||||
|
];
|
||||||
|
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
|
||||||
|
throw new Error("注册码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证输入
|
||||||
|
if (!this.isValidInput(username) || !this.isValidInput(password)) {
|
||||||
|
throw new Error("用户名、密码或注册码不能包含空格或汉字");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
const existingUser = await User.findOne({ where: { username } });
|
||||||
|
if (existingUser) {
|
||||||
|
throw new Error("用户名已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
|
||||||
|
const user = await User.create({ username, password: hashedPassword, role });
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: user,
|
||||||
|
message: "用户注册成功",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username: string, password: string) {
|
||||||
|
const user = await User.findOne({ where: { username } });
|
||||||
|
if (!user || !(await bcrypt.compare(password, user.password))) {
|
||||||
|
throw new Error("用户名或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
|
||||||
|
expiresIn: "6h",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
54
backend/src/sponsors/sponsors.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"sponsors": [
|
||||||
|
{
|
||||||
|
"name": "立本狗头",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks1.jpg",
|
||||||
|
"message": "怒搓楼上狗头! "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "帝国鼻屎",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks2.jpg",
|
||||||
|
"message": "芜湖起飞! "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "雷霆222",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks3.jpg",
|
||||||
|
"message": "把我弄帅点 "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "黑田奈奈子",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks4.jpg",
|
||||||
|
"message": "流年笑掷 未来可期 ",
|
||||||
|
"link": "https://github.com/htnanako"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "原野🐇",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks5.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "我摆烂!",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks6.jpg",
|
||||||
|
"message": "人生苦短,及时行乐,卷什么卷,随缘摆烂 "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "田培",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks7.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "River",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks8.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "午夜学徒",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks9.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "阿潘",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks10.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "闹闹黑",
|
||||||
|
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks11.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
83
backend/src/types/cloud.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
export interface ShareInfoResponse {
|
||||||
|
data: {
|
||||||
|
list: ShareInfoItem[];
|
||||||
|
fileSize?: number;
|
||||||
|
pwdId?: string;
|
||||||
|
stoken?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetShareInfoParams {
|
||||||
|
shareCode: string;
|
||||||
|
receiveCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareInfoItem {
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
fileSize?: number;
|
||||||
|
fileIdToken?: string;
|
||||||
|
}
|
||||||
|
export interface FolderListResponse {
|
||||||
|
data: {
|
||||||
|
cid: string;
|
||||||
|
name: string;
|
||||||
|
path: { cid: string; name: string }[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveFileParams {
|
||||||
|
shareCode: string; // 分享code
|
||||||
|
receiveCode?: string; // 分享文件的密码
|
||||||
|
folderId?: string; // 文件夹id
|
||||||
|
fids?: string[]; // 存储文件id
|
||||||
|
fidTokens?: string[]; // 存储文件token
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveFileResponse {
|
||||||
|
message: string;
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareFileInfo {
|
||||||
|
shareCode: string;
|
||||||
|
receiveCode?: string;
|
||||||
|
fileId: string;
|
||||||
|
cid?: string;
|
||||||
|
fid_list?: string[];
|
||||||
|
fid_token_list?: string[];
|
||||||
|
to_pdir_fid?: string;
|
||||||
|
pwd_id?: string;
|
||||||
|
stoken?: string;
|
||||||
|
pdir_fid?: string;
|
||||||
|
scene?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuarkShareFileInfo {
|
||||||
|
fid_list: string[];
|
||||||
|
fid_token_list: string[];
|
||||||
|
to_pdir_fid: string;
|
||||||
|
pwd_id: string;
|
||||||
|
stoken: string;
|
||||||
|
pdir_fid: string;
|
||||||
|
scene: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuarkShareInfo {
|
||||||
|
stoken?: string;
|
||||||
|
pwdId?: string;
|
||||||
|
fileSize?: number;
|
||||||
|
list: {
|
||||||
|
fid: string;
|
||||||
|
file_name: string;
|
||||||
|
file_type: number;
|
||||||
|
share_fid_token: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuarkFolderItem {
|
||||||
|
fid: string;
|
||||||
|
file_name: string;
|
||||||
|
file_type: number;
|
||||||
|
}
|
||||||
@@ -5,9 +5,6 @@ export interface ShareInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ShareInfoResponse {
|
export interface ShareInfoResponse {
|
||||||
success: boolean;
|
data?: ShareInfo[];
|
||||||
data?: {
|
message?: string;
|
||||||
list: ShareInfo[];
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
backend/src/types/express.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
declare module "express" {
|
||||||
|
interface Request {
|
||||||
|
user?: {
|
||||||
|
userId: string;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
15
backend/src/types/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export interface Config {
|
||||||
|
app: {
|
||||||
|
port: number;
|
||||||
|
env: string;
|
||||||
|
};
|
||||||
|
database: {
|
||||||
|
type: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
jwt: {
|
||||||
|
secret: string;
|
||||||
|
expiresIn: string;
|
||||||
|
};
|
||||||
|
// ... 其他配置类型
|
||||||
|
}
|
||||||
9
backend/src/types/services.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
import { ShareInfoResponse, FolderListResponse, SaveFileParams } from "./cloud";
|
||||||
|
|
||||||
|
export interface ICloudStorageService {
|
||||||
|
setCookie(req: Request): Promise<void>;
|
||||||
|
getShareInfo(shareCode: string, receiveCode?: string): Promise<ShareInfoResponse>;
|
||||||
|
getFolderList(parentCid?: string): Promise<FolderListResponse>;
|
||||||
|
saveSharedFile(params: SaveFileParams): Promise<any>;
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
|
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
|
||||||
import tunnel from "tunnel";
|
import tunnel from "tunnel";
|
||||||
import { config } from "../config";
|
|
||||||
|
interface ProxyConfig {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function createAxiosInstance(
|
export function createAxiosInstance(
|
||||||
baseURL: string,
|
baseURL: string,
|
||||||
headers: AxiosRequestHeaders,
|
headers?: AxiosRequestHeaders,
|
||||||
useProxy: boolean = false
|
useProxy: boolean = false,
|
||||||
|
proxyConfig?: ProxyConfig
|
||||||
): AxiosInstance {
|
): AxiosInstance {
|
||||||
let agent;
|
let agent;
|
||||||
|
if (useProxy && proxyConfig) {
|
||||||
if (useProxy) {
|
|
||||||
agent = tunnel.httpsOverHttp({
|
agent = tunnel.httpsOverHttp({
|
||||||
proxy: {
|
proxy: proxyConfig,
|
||||||
host: config.httpProxy.host,
|
|
||||||
port: Number(config.httpProxy.port),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { Response, NextFunction } from "express";
|
import { Response, NextFunction } from "express";
|
||||||
import { Logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
interface CustomError {
|
||||||
|
name?: string;
|
||||||
|
message: string;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function handleError(
|
export default function handleError(
|
||||||
res: Response,
|
res: Response,
|
||||||
error: any,
|
error: CustomError | unknown,
|
||||||
message: string,
|
message: string,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
Logger.error(message, error);
|
logger.error(message, error);
|
||||||
next(error || { success: false, message });
|
next(error || { success: false, message });
|
||||||
}
|
}
|
||||||
|
|||||||
21
backend/src/utils/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { config } from "../config";
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserIdFromToken(req: Request): string | null {
|
||||||
|
try {
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Token not found");
|
||||||
|
}
|
||||||
|
const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;
|
||||||
|
return decoded.userId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid token:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,21 @@
|
|||||||
type LogLevel = "info" | "success" | "warn" | "error";
|
import winston from "winston";
|
||||||
|
import { config } from "../config";
|
||||||
|
|
||||||
export const Logger = {
|
const logger = winston.createLogger({
|
||||||
info(...args: any[]) {
|
level: config.app.env === "development" ? "debug" : "info",
|
||||||
this.log("info", ...args);
|
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||||
},
|
transports: [
|
||||||
|
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
|
||||||
|
new winston.transports.File({ filename: "logs/combined.log" }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
success(...args: any[]) {
|
if (config.app.env !== "production") {
|
||||||
this.log("success", ...args);
|
logger.add(
|
||||||
},
|
new winston.transports.Console({
|
||||||
|
format: winston.format.simple(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
warn(...args: any[]) {
|
export { logger };
|
||||||
this.log("warn", ...args);
|
|
||||||
},
|
|
||||||
|
|
||||||
error(...args: any[]) {
|
|
||||||
this.log("error", ...args);
|
|
||||||
},
|
|
||||||
|
|
||||||
log(level: LogLevel, ...args: any[]) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
||||||
|
|
||||||
switch (level) {
|
|
||||||
case "success":
|
|
||||||
console.log(prefix, ...args);
|
|
||||||
break;
|
|
||||||
case "warn":
|
|
||||||
console.warn(prefix, ...args);
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
console.error(prefix, ...args);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(prefix, ...args);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
17
backend/src/utils/response.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
|
||||||
|
interface ResponseData {
|
||||||
|
code?: number; // 业务状态码
|
||||||
|
message?: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendSuccess = (res: Response, response: ResponseData, businessCode: number = 0) => {
|
||||||
|
response.code = businessCode;
|
||||||
|
res.status(200).json(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendError = (res: Response, response: ResponseData, businessCode: number = 10000) => {
|
||||||
|
response.code = businessCode;
|
||||||
|
res.status(200).json(response);
|
||||||
|
};
|
||||||
@@ -12,9 +12,12 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"typeRoots": ["./node_modules/@types", "./src/types"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
},
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
13
docker-entrypoint.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# 如果配置目录下没有 env 文件,则复制示例文件
|
||||||
|
if [ ! -f /app/config/env ]; then
|
||||||
|
cp /app/.env.example /app/config/env
|
||||||
|
echo "已创建默认配置文件 /app/config/env,请根据需要修改配置"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建配置文件软链接
|
||||||
|
ln -sf /app/config/env /app/.env
|
||||||
|
|
||||||
|
# 启动 Nginx 和后端服务
|
||||||
|
nginx -g 'daemon off;' & npm run start
|
||||||
BIN
docs/images/alipay.png
Normal file
|
After Width: | Height: | Size: 610 KiB |
BIN
docs/images/mobile/douban.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/images/mobile/login.png
Normal file
|
After Width: | Height: | Size: 631 KiB |
BIN
docs/images/mobile/save.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/images/mobile/save1.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
docs/images/mobile/search.png
Normal file
|
After Width: | Height: | Size: 470 KiB |
BIN
docs/images/pc/detail.png
Normal file
|
After Width: | Height: | Size: 674 KiB |
BIN
docs/images/pc/douban.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
docs/images/pc/login.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
docs/images/pc/save.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
docs/images/pc/save1.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
docs/images/pc/search.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/images/qq.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 35 KiB |
BIN
docs/images/wechat.jpg
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
docs/images/wechat_2.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
docs/images/wechat_pay.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
3
frontend/auto-imports.d.ts
vendored
@@ -5,5 +5,6 @@
|
|||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
|
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||||
|
const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']
|
||||||
}
|
}
|
||||||
|
|||||||
46
frontend/components.d.ts
vendored
@@ -7,25 +7,63 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
AsideMenu: typeof import('./src/components/AsideMenu.vue')['default']
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElImage: typeof import('element-plus/es')['ElImage']
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
FolderSelect: typeof import('./src/components/FolderSelect.vue')['default']
|
FolderSelect: typeof import('./src/components/Home/FolderSelect.vue')['default']
|
||||||
ResourceList: typeof import('./src/components/ResourceList.vue')['default']
|
ResourceCard: typeof import('./src/components/Home/ResourceCard.vue')['default']
|
||||||
|
ResourceSelect: typeof import('./src/components/Home/ResourceSelect.vue')['default']
|
||||||
|
ResourceTable: typeof import('./src/components/Home/ResourceTable.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
||||||
|
VanBackTop: typeof import('vant/es')['BackTop']
|
||||||
|
VanButton: typeof import('vant/es')['Button']
|
||||||
|
VanCell: typeof import('vant/es')['Cell']
|
||||||
|
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||||
|
VanCheckbox: typeof import('vant/es')['Checkbox']
|
||||||
|
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
|
||||||
|
VanEmpty: typeof import('vant/es')['Empty']
|
||||||
|
VanField: typeof import('vant/es')['Field']
|
||||||
|
VanForm: typeof import('vant/es')['Form']
|
||||||
|
VanIcon: typeof import('vant/es')['Icon']
|
||||||
|
VanImage: typeof import('vant/es')['Image']
|
||||||
|
VanLoading: typeof import('vant/es')['Loading']
|
||||||
|
VanOverlay: typeof import('vant/es')['Overlay']
|
||||||
|
VanPopup: typeof import('vant/es')['Popup']
|
||||||
|
VanSearch: typeof import('vant/es')['Search']
|
||||||
|
VanSwitch: typeof import('vant/es')['Switch']
|
||||||
|
VanTab: typeof import('vant/es')['Tab']
|
||||||
|
VanTabbar: typeof import('vant/es')['Tabbar']
|
||||||
|
VanTabbarItem: typeof import('vant/es')['TabbarItem']
|
||||||
|
VanTabs: typeof import('vant/es')['Tabs']
|
||||||
|
VanTag: typeof import('vant/es')['Tag']
|
||||||
}
|
}
|
||||||
export interface ComponentCustomProperties {
|
export interface ComponentCustomProperties {
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
|||||||
BIN
frontend/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -3,7 +3,36 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<!-- 移动端适配 -->
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<meta name="keywords" content="网盘,资源搜索,云存储" />
|
||||||
|
<!-- SEO关键词 -->
|
||||||
|
<meta name="description" content="网盘资源搜索工具" />
|
||||||
|
<!-- 设置Web App描述 -->
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<!-- 设置主题颜色 -->
|
||||||
|
<meta property="og:title" content="CloudSaver" />
|
||||||
|
<!-- 社交媒体分享标题 -->
|
||||||
|
<meta property="og:description" content="网盘资源搜索工具" />
|
||||||
|
<!-- 社交媒体分享描述 -->
|
||||||
|
<meta property="og:url" content="https://github.com/jiangrui1994/CloudSaver" />
|
||||||
|
<!-- 社交媒体分享链接 -->
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<!-- Twitter卡片类型 -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<!-- 开启Web App功能 -->
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<!-- 设置状态栏样式 -->
|
||||||
|
<meta name="apple-mobile-web-app-title" content="CloudSaver" />
|
||||||
|
<!-- 设置Web App标题 -->
|
||||||
|
<link rel="apple-touch-icon" href="/logo-1.png" />
|
||||||
|
<!-- 设置Web App图标 -->
|
||||||
|
<link rel="mask-icon" href="/logo.svg" color="transparent" />
|
||||||
|
<!-- 设置Web App图标遮罩 -->
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
<title>CloudSaver</title>
|
<title>CloudSaver</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
frontend/logo-1.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
3751
frontend/logo.svg
Normal file
|
After Width: | Height: | Size: 282 KiB |
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "cloud-disk-web",
|
"name": "cloud-disk-web",
|
||||||
"version": "0.1.0",
|
"version": "0.2.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cloud-disk-web",
|
"name": "cloud-disk-web",
|
||||||
"version": "0.1.0",
|
"version": "0.2.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"element-plus": "^2.6.1",
|
"element-plus": "^2.6.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "cloud-saver-web",
|
"name": "cloud-saver-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.2.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
@@ -12,18 +12,25 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"element-plus": "^2.6.1",
|
"element-plus": "^2.6.1",
|
||||||
|
"gsap": "^3.12.7",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
|
"typeit": "^8.8.7",
|
||||||
|
"vant": "^4.9.17",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.25",
|
"@types/node": "^20.11.25",
|
||||||
|
"@vant/auto-import-resolver": "^1.3.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"postcss-pxtorem": "^6.1.0",
|
||||||
|
"sass": "^1.83.4",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.8",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"vite": "^5.1.5",
|
"vite": "^5.1.5",
|
||||||
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
"vue-tsc": "^2.0.6"
|
"vue-tsc": "^2.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
frontend/postcss.config.cjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-pxtorem": {
|
||||||
|
rootValue({ file }) {
|
||||||
|
return file.indexOf("vant") !== -1 || file.indexOf("mobile") !== -1 ? 50 : 75;
|
||||||
|
},
|
||||||
|
propList: ["*"],
|
||||||
|
exclude: (file) => {
|
||||||
|
return !file.includes("mobile") && !file.includes("vant");
|
||||||
|
},
|
||||||
|
minPixelValue: 2,
|
||||||
|
mediaQuery: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -7,5 +7,67 @@
|
|||||||
<style>
|
<style>
|
||||||
#app {
|
#app {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
--theme-color: #3e3e3e;
|
||||||
|
--theme-theme: #133ab3;
|
||||||
|
--theme-background: #fafafa;
|
||||||
|
--theme-other_background: #ffffff;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family:
|
||||||
|
v-sans,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
Segoe UI,
|
||||||
|
sans-serif,
|
||||||
|
"Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji",
|
||||||
|
Segoe UI Symbol;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--theme-color);
|
||||||
|
background-color: var(--theme-background);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端全局样式 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
#app {
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一按钮样式 */
|
||||||
|
.van-button {
|
||||||
|
height: 40px;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一输入框样式 */
|
||||||
|
.van-field {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统一卡片样式 */
|
||||||
|
.van-card {
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
margin: var(--spacing-base) 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
import type { ShareInfoResponse, Folder, Save115FileParams } from "@/types";
|
import type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from "@/types";
|
||||||
|
|
||||||
export const cloud115Api = {
|
export const cloud115Api = {
|
||||||
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
async getShareInfo(params: GetShareInfoParams) {
|
||||||
const { data } = await request.get("/api/cloud115/share-info", {
|
const { data } = await request.get<ShareInfoResponse>("/api/cloud115/share-info", {
|
||||||
params: { shareCode, receiveCode },
|
params,
|
||||||
});
|
});
|
||||||
return data;
|
return data as ShareInfoResponse;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFolderList(parentCid = "0"): Promise<{ data: Folder[] }> {
|
async getFolderList(parentCid = "0") {
|
||||||
const { data } = await request.get("/api/cloud115/folders", {
|
const res = await request.get<Folder[]>("/api/cloud115/folders", {
|
||||||
params: { parentCid },
|
params: { parentCid },
|
||||||
});
|
});
|
||||||
return data;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveFile(params: Save115FileParams) {
|
async saveFile(params: SaveFileParams) {
|
||||||
const { data } = await request.post("/api/cloud115/save", params);
|
const res = await request.post("/api/cloud115/save", params);
|
||||||
return data;
|
return res;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
11
frontend/src/api/douban.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
import { HotListItem, HotListParams } from "@/types/douban";
|
||||||
|
|
||||||
|
export const doubanApi = {
|
||||||
|
async getHotList(params: HotListParams) {
|
||||||
|
const { data } = await request.get<HotListItem[]>("/api/douban/hot", {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
import type { ShareInfoResponse, Folder, SaveQuarkFileParams } from "@/types";
|
import type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from "@/types";
|
||||||
|
|
||||||
export const quarkApi = {
|
export const quarkApi = {
|
||||||
async getShareInfo(pwdId: string, passcode = ""): Promise<ShareInfoResponse> {
|
async getShareInfo(params: GetShareInfoParams) {
|
||||||
const { data } = await request.get("/api/quark/share-info", {
|
const { data } = await request.get<ShareInfoResponse>("/api/quark/share-info", {
|
||||||
params: { pwdId, passcode },
|
params,
|
||||||
});
|
});
|
||||||
return data;
|
return data as ShareInfoResponse;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFolderList(parentCid = "0"): Promise<{ data: Folder[] }> {
|
async getFolderList(parentCid = "0") {
|
||||||
const { data } = await request.get("/api/quark/folders", {
|
const data = await request.get<Folder[]>("/api/quark/folders", {
|
||||||
params: { parentCid },
|
params: { parentCid },
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveFile(params: SaveQuarkFileParams) {
|
async saveFile(params: SaveFileParams) {
|
||||||
const { data } = await request.post("/api/quark/save", params);
|
return await request.post("/api/quark/save", params);
|
||||||
return data;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import request from "@/utils/request";
|
|||||||
import type { Resource } from "@/types/index";
|
import type { Resource } from "@/types/index";
|
||||||
|
|
||||||
export const resourceApi = {
|
export const resourceApi = {
|
||||||
search(keyword: string, backupPlan: boolean, channelId?: string, lastMessageId?: string) {
|
search(keyword: string, channelId?: string, lastMessageId?: string) {
|
||||||
return request.get<Resource[]>(`/api/${backupPlan ? "rssSearch" : "search"}`, {
|
return request.get<Resource[]>(`/api/search`, {
|
||||||
params: { keyword, channelId, lastMessageId },
|
params: { keyword, channelId, lastMessageId },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
17
frontend/src/api/setting.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
import type { GlobalSettingAttributes, UserSettingAttributes } from "@/types";
|
||||||
|
|
||||||
|
export const settingApi = {
|
||||||
|
getSetting: () => {
|
||||||
|
return request.get<{
|
||||||
|
userSettings: UserSettingAttributes;
|
||||||
|
globalSetting: GlobalSettingAttributes;
|
||||||
|
}>("/api/setting/get");
|
||||||
|
},
|
||||||
|
saveSetting: (data: {
|
||||||
|
userSettings: UserSettingAttributes;
|
||||||
|
globalSetting?: GlobalSettingAttributes | null;
|
||||||
|
}) => {
|
||||||
|
return request.post("/api/setting/save", data);
|
||||||
|
},
|
||||||
|
};
|
||||||
13
frontend/src/api/user.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
login: (data: { username: string; password: string }) => {
|
||||||
|
return request.post<{ token: string }>("/api/user/login", data);
|
||||||
|
},
|
||||||
|
register: (data: { username: string; password: string; registerCode: string }) => {
|
||||||
|
return request.post<{ token: string }>("/api/user/register", data);
|
||||||
|
},
|
||||||
|
getSponsors: () => {
|
||||||
|
return request.get("/api/sponsors?timestamp=" + Date.now());
|
||||||
|
},
|
||||||
|
};
|
||||||