mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
Compare commits
192 Commits
v1.2.3
...
1af7fbd355
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af7fbd355 | ||
|
|
8e35a6e507 | ||
|
|
d113dcd926 | ||
|
|
8708e869a4 | ||
|
|
6c84b8d7b7 | ||
|
|
fc4cf8ecfb | ||
|
|
447512d809 | ||
|
|
64e7169140 | ||
|
|
99d2c7f20f | ||
|
|
9e6b5a58c4 | ||
|
|
040e6bc6bf | ||
|
|
3370f75d5e | ||
|
|
11a3204c18 | ||
|
|
5276112e48 | ||
|
|
3bd0fde82f | ||
|
|
61e5cbf80d | ||
|
|
57f7bab443 | ||
|
|
242e12c29c | ||
|
|
f9a1043431 | ||
|
|
5dc431ab24 | ||
|
|
c50282bec8 | ||
|
|
b99a97c0a9 | ||
|
|
5c1aaf245d | ||
|
|
30448841f6 | ||
|
|
7cddb243bc | ||
|
|
c15132b45a | ||
|
|
04b3838cea | ||
|
|
70276b68ee | ||
|
|
fe8aaff92e | ||
|
|
236051f6c4 | ||
|
|
01bc8f0450 | ||
|
|
5b7e7b73ad | ||
|
|
0e88374905 | ||
|
|
ca175ec59d | ||
|
|
ec4e0762d5 | ||
|
|
081a3a7222 | ||
|
|
6b8d2b3cf0 | ||
|
|
9333f9da94 | ||
|
|
806a724fb5 | ||
|
|
487f5c9559 | ||
|
|
18b7f89c49 | ||
|
|
db902f3742 | ||
|
|
42baa891f8 | ||
|
|
02d5d00510 | ||
|
|
d95c69142a | ||
|
|
2638ccb1e4 | ||
|
|
886d91ab10 | ||
|
|
ddad95be41 | ||
|
|
273800459f | ||
|
|
dbe24af4ac | ||
|
|
a598ef508c | ||
|
|
1ca4cce6bc | ||
|
|
270022188e | ||
|
|
7e80a1c2b2 | ||
|
|
6e7914f056 | ||
|
|
dbde0e1675 | ||
|
|
b840680df0 | ||
|
|
651987731b | ||
|
|
fb26d166d6 | ||
|
|
8baf5c6c3d | ||
|
|
005aa71cc2 | ||
|
|
61beed6788 | ||
|
|
53aebf2a15 | ||
|
|
1fe9487833 | ||
|
|
6476ce1369 | ||
|
|
1ad3a07930 | ||
|
|
22fd1dcf81 | ||
|
|
f8cfe307ae | ||
|
|
84ee0d9e53 | ||
|
|
40e3350a4b | ||
|
|
013fe71925 | ||
|
|
6be7ae871d | ||
|
|
89e2aca968 | ||
|
|
f006d84b03 | ||
|
|
7ce3839b9b | ||
|
|
52ea019374 | ||
|
|
4c738d1030 | ||
|
|
ec00f2d823 | ||
|
|
54542ff8ee | ||
|
|
0050c6bba3 | ||
|
|
4ceed8fd4b | ||
|
|
2e5dd8360e | ||
|
|
40ad48f5cf | ||
|
|
921bdc43cb | ||
|
|
0df7d8bf23 | ||
|
|
fdc75705aa | ||
|
|
a28dd4840b | ||
|
|
061b94cf61 | ||
|
|
0d28b322b7 | ||
|
|
ee06e110bd | ||
|
|
7acfa300ea | ||
|
|
b4689d2f99 | ||
|
|
6074d91467 | ||
|
|
e30e381adf | ||
|
|
516746f722 | ||
|
|
4da07b3ea4 | ||
|
|
da8a2ad169 | ||
|
|
e2832b9e36 | ||
|
|
bdb43531e8 | ||
|
|
51dbf0f03a | ||
|
|
10294e093f | ||
|
|
6816ab0550 | ||
|
|
357e09ef52 | ||
|
|
3a50af844e | ||
|
|
01c371b503 | ||
|
|
338a535531 | ||
|
|
19e92719c3 | ||
|
|
2727bef91b | ||
|
|
193ed24316 | ||
|
|
ba155bd253 | ||
|
|
4ca6e05fe0 | ||
|
|
169706bfbc | ||
|
|
2568d9b6a4 | ||
|
|
d3279ded92 | ||
|
|
5bcf1bb5ef | ||
|
|
547b58c7ba | ||
|
|
b9fbe58a3d | ||
|
|
6b92061d09 | ||
|
|
3aa2963211 | ||
|
|
6fa9036705 | ||
|
|
091be5ef70 | ||
|
|
a24d32776c | ||
|
|
982e4f942e | ||
|
|
9d2c4e8978 | ||
|
|
cd8c519b3a | ||
|
|
1eb37baa87 | ||
|
|
b97f56c455 | ||
|
|
8ced3d0327 | ||
|
|
bada678490 | ||
|
|
8be837fcbf | ||
|
|
cb0c77a565 | ||
|
|
2ef6e4debb | ||
|
|
5a4d3b9eb4 | ||
|
|
ade5e4d2ed | ||
|
|
595a0a917c | ||
|
|
d23a6b26e4 | ||
|
|
9690a63646 | ||
|
|
2a5bf19e7d | ||
|
|
eeeb2aefbb | ||
|
|
9c838e369f | ||
|
|
5a4918812a | ||
|
|
08af3d9b6f | ||
|
|
cafe2ce406 | ||
|
|
e481775e27 | ||
|
|
4c9cef249e | ||
|
|
056aa229fe | ||
|
|
6f8bcfd356 | ||
|
|
5b0e4ea4a7 | ||
|
|
fc77d43614 | ||
|
|
67828458b0 | ||
|
|
e51446abf8 | ||
|
|
1d6929db00 | ||
|
|
b58e805718 | ||
|
|
aa1aa47eba | ||
|
|
3aed6bd24d | ||
|
|
1c71156784 | ||
|
|
f2ee574fae | ||
|
|
074058ac5c | ||
|
|
07cb6977e4 | ||
|
|
baae1da1e0 | ||
|
|
9e7b214812 | ||
|
|
37004107d0 | ||
|
|
4aab45cda5 | ||
|
|
2853287b1d | ||
|
|
46e5cee810 | ||
|
|
fac32cdfe6 | ||
|
|
3a90a89b08 | ||
|
|
80a94c0f05 | ||
|
|
d49ce77350 | ||
|
|
800b511116 | ||
|
|
292384f281 | ||
|
|
b8b0cc760d | ||
|
|
002267e436 | ||
|
|
0d54dffa19 | ||
|
|
d2c9d79658 | ||
|
|
f70850d465 | ||
|
|
223b1af714 | ||
|
|
76a64492a2 | ||
|
|
d6224ab25c | ||
|
|
9708157566 | ||
|
|
bfaf93c849 | ||
|
|
8cf1575232 | ||
|
|
17c05870a3 | ||
|
|
d531be3c36 | ||
|
|
edde7afdc8 | ||
|
|
77216cf380 | ||
|
|
1b898eda37 | ||
|
|
da3fc11b2e | ||
|
|
cbf673126e | ||
|
|
aa7d6ea2fe | ||
|
|
841eb05f68 | ||
|
|
eeca85942f |
62
.github/workflows/release.yml
vendored
Normal file
62
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
id-token: write
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
# 可选:添加输入参数,用于测试不同的场景
|
||||
inputs:
|
||||
version-suffix:
|
||||
description: 'Version suffix for testing (e.g., -test, -rc)'
|
||||
required: false
|
||||
default: '-test'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Build Linux binary
|
||||
run: |
|
||||
chmod +x scripts/build.sh
|
||||
./scripts/build.sh build-linux
|
||||
|
||||
- name: Rename binary
|
||||
run: mv main urldb-${{ github.ref_name }}-linux-amd64
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build Frontend
|
||||
run: |
|
||||
cd web
|
||||
npm install --frozen-lockfile
|
||||
npm run build
|
||||
|
||||
- name: Create frontend archive
|
||||
run: |
|
||||
cd web
|
||||
tar -czf ../frontend-${{ github.ref_name }}.tar.gz .output/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
urldb-${{ github.ref_name }}-linux-amd64
|
||||
frontend-${{ github.ref_name }}.tar.gz
|
||||
generate_release_notes: true
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -123,4 +123,11 @@ dist/
|
||||
.dockerignore
|
||||
|
||||
# Air live reload
|
||||
tmp/
|
||||
tmp/
|
||||
|
||||
#
|
||||
data/
|
||||
.claude/
|
||||
main
|
||||
output.zip
|
||||
CLAUDE.md
|
||||
130
BUILD.md
Normal file
130
BUILD.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 编译说明
|
||||
|
||||
## 方案1:使用编译脚本(推荐)
|
||||
|
||||
### 在Git Bash中执行:
|
||||
|
||||
```bash
|
||||
# 给脚本添加执行权限(首次使用)
|
||||
chmod +x scripts/build.sh
|
||||
|
||||
# 编译Linux版本(推荐,用于服务器部署)
|
||||
./scripts/build.sh
|
||||
|
||||
# 或者明确指定编译Linux版本
|
||||
./scripts/build.sh build-linux
|
||||
|
||||
# 或者指定目标文件名
|
||||
./scripts/build.sh build-linux myapp
|
||||
|
||||
# 编译当前平台版本(用于本地测试)
|
||||
./scripts/build.sh build
|
||||
```
|
||||
|
||||
### 编译脚本功能:
|
||||
- 自动读取 `VERSION` 文件中的版本号
|
||||
- 自动获取Git提交信息和分支信息
|
||||
- 自动获取构建时间
|
||||
- 将版本信息编译到可执行文件中
|
||||
- 支持跨平台编译(默认编译Linux版本)
|
||||
- 使用静态链接,适合服务器部署
|
||||
|
||||
## 方案2:手动编译
|
||||
|
||||
### Linux版本(推荐):
|
||||
|
||||
```bash
|
||||
# 获取版本信息
|
||||
VERSION=$(cat VERSION)
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
BUILD_TIME=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 编译Linux版本
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-X 'github.com/ctwj/urldb/utils.Version=${VERSION}' -X 'github.com/ctwj/urldb/utils.BuildTime=${BUILD_TIME}' -X 'github.com/ctwj/urldb/utils.GitCommit=${GIT_COMMIT}' -X 'github.com/ctwj/urldb/utils.GitBranch=${GIT_BRANCH}'" -o main .
|
||||
```
|
||||
|
||||
### 当前平台版本:
|
||||
|
||||
```bash
|
||||
# 获取版本信息
|
||||
VERSION=$(cat VERSION)
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||
BUILD_TIME=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 编译当前平台版本
|
||||
go build -ldflags "-X 'github.com/ctwj/urldb/utils.Version=${VERSION}' -X 'github.com/ctwj/urldb/utils.BuildTime=${BUILD_TIME}' -X 'github.com/ctwj/urldb/utils.GitCommit=${GIT_COMMIT}' -X 'github.com/ctwj/urldb/utils.GitBranch=${GIT_BRANCH}'" -o main .
|
||||
```
|
||||
|
||||
## 验证版本信息
|
||||
|
||||
编译完成后,可以通过以下方式验证版本信息:
|
||||
|
||||
```bash
|
||||
# 命令行验证
|
||||
./main version
|
||||
|
||||
# 启动服务器后通过API验证
|
||||
curl http://localhost:8080/api/version
|
||||
```
|
||||
|
||||
## 部署说明
|
||||
|
||||
使用方案1编译后,部署时只需要:
|
||||
|
||||
1. 复制可执行文件到服务器
|
||||
2. 启动程序
|
||||
|
||||
**不再需要复制 `VERSION` 文件**,因为版本信息已经编译到程序中。
|
||||
|
||||
### 使用部署脚本(可选)
|
||||
|
||||
```bash
|
||||
# 给部署脚本添加执行权限
|
||||
chmod +x scripts/deploy-example.sh
|
||||
|
||||
# 部署到服务器
|
||||
./scripts/deploy-example.sh root example.com /opt/urldb
|
||||
```
|
||||
|
||||
### 使用Docker构建脚本:
|
||||
|
||||
```bash
|
||||
# 给脚本添加执行权限
|
||||
chmod +x scripts/docker-build.sh
|
||||
|
||||
# 构建Docker镜像
|
||||
./scripts/docker-build.sh build
|
||||
|
||||
# 构建指定版本镜像
|
||||
./scripts/docker-build.sh build 1.2.4
|
||||
|
||||
# 推送镜像到Docker Hub
|
||||
./scripts/docker-build.sh push 1.2.4
|
||||
```
|
||||
|
||||
### 手动Docker构建:
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build --target backend -t ctwj/urldb-backend:1.2.3 .
|
||||
docker build --target frontend -t ctwj/urldb-frontend:1.2.3 .
|
||||
```
|
||||
|
||||
## 版本管理
|
||||
|
||||
更新版本号:
|
||||
|
||||
```bash
|
||||
# 更新版本号
|
||||
./scripts/version.sh patch # 修订版本
|
||||
./scripts/version.sh minor # 次版本
|
||||
./scripts/version.sh major # 主版本
|
||||
|
||||
# 然后重新编译
|
||||
./scripts/build.sh
|
||||
|
||||
# 或者构建Docker镜像
|
||||
./scripts/docker-build.sh build
|
||||
```
|
||||
88
CHANGELOG.md
88
CHANGELOG.md
@@ -1,88 +0,0 @@
|
||||
# 📝 更新日志
|
||||
|
||||
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范。
|
||||
|
||||
## [v1.1.0]
|
||||
|
||||
1. 新增违禁词功能
|
||||
2. 管理后台体验优化
|
||||
3. bug修复
|
||||
|
||||
## [v1.0.0]
|
||||
|
||||
1. 自动转存
|
||||
2. 自动资源处理
|
||||
|
||||
### 新增
|
||||
- 项目开源准备
|
||||
- 完善文档和贡献指南
|
||||
- 添加LICENSE文件
|
||||
|
||||
### 修复
|
||||
- 修复README格式问题
|
||||
- 优化项目结构说明
|
||||
|
||||
## [100 - 202401XX
|
||||
|
||||
### 新增
|
||||
- 🎉 首次发布
|
||||
- 📁 多平台网盘支持(夸克、阿里云盘、百度网盘、UC网盘)
|
||||
- 🔍 智能搜索功能
|
||||
- 📊 数据统计和分析
|
||||
- 🏷️ 标签系统
|
||||
- 👥 用户权限管理
|
||||
- 📦 批量资源管理
|
||||
- 🔄 自动处理功能
|
||||
- 📈 热播剧管理
|
||||
- ⚙️ 系统配置管理
|
||||
- 🔐 JWT认证系统
|
||||
- 📱 响应式设计
|
||||
- 🌙 深色模式支持
|
||||
- 🎨 现代化UI界面
|
||||
|
||||
### 技术特性
|
||||
- 🦀 基于Golang 1023的高性能后端
|
||||
- ⚡ Nuxt.js 3 + Vue 3前端框架
|
||||
- 🗄️ PostgreSQL数据库
|
||||
- 🔧 GORM ORM框架
|
||||
- 🐳 Docker容器化部署
|
||||
- 📝 TypeScript类型安全
|
||||
|
||||
### 核心功能
|
||||
- 资源管理:增删改查、批量操作
|
||||
- 分类管理:资源分类和标签
|
||||
- 平台管理:多网盘平台支持
|
||||
- 搜索统计:全文搜索和数据分析
|
||||
- 系统配置:灵活的参数配置
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
### 版本号格式
|
||||
- **主版本号**:不兼容的API修改
|
||||
- **次版本号**:向下兼容的功能性新增
|
||||
- **修订号**:向下兼容的问题修正
|
||||
|
||||
### 更新类型
|
||||
- 🎉 **重大更新** - 新版本发布
|
||||
- ✨ **新增功能** - 新功能或特性
|
||||
- 🐛 **问题修复** - Bug修复
|
||||
- 🔧 **优化改进** - 性能优化或代码改进
|
||||
- 📚 **文档更新** - 文档或注释更新
|
||||
- 🎨 **界面优化** - UI/UX改进
|
||||
- ⚡ **性能提升** - 性能相关改进
|
||||
- 🔒 **安全更新** - 安全相关修复
|
||||
- 🧪 **测试相关** - 测试用例或测试工具
|
||||
- 🚀 **部署相关** - 部署或构建相关
|
||||
|
||||
---
|
||||
|
||||
## 贡献
|
||||
|
||||
如果您想为更新日志做出贡献,请:
|
||||
|
||||
1. 在提交代码时使用规范的提交信息2. 在Pull Request中描述您的更改
|
||||
3. 遵循项目的贡献指南
|
||||
|
||||
---
|
||||
54
ChangeLog.md
Normal file
54
ChangeLog.md
Normal file
@@ -0,0 +1,54 @@
|
||||
### v1.3.4
|
||||
1. 添加详情页
|
||||
|
||||
### v1.3.3
|
||||
1. 公众号自动回复
|
||||
|
||||
### v1.3.2
|
||||
1. 二维码美化
|
||||
2. TelegramBot参数调整
|
||||
3. 修复一些问题
|
||||
|
||||
### v1.3.1
|
||||
1. 添加API访问日志
|
||||
2. 添加首页公告
|
||||
3. TG机器人,添加资源选择模式
|
||||
|
||||
### v1.3.0
|
||||
1. 新增 Telegram Bot
|
||||
2. 新增扩容
|
||||
3. 支持迅雷云盘
|
||||
4. UI优化
|
||||
|
||||
### v1.2.5
|
||||
1. 修复一些Bug
|
||||
|
||||
### v1.2.4
|
||||
|
||||
1. 搜索增强,毫秒级响应,关键字高亮显示
|
||||
2. 修复版本显示不正确的问题
|
||||
3. 配置项新增Meilisearch配置
|
||||
|
||||
### v1.2.3
|
||||
1. 添加图片上传功能
|
||||
2. 添加Logo配置项,首页Logo显示
|
||||
3. 后台界面体验优化
|
||||
|
||||
### v1.2.1
|
||||
1. 修复转存移除广告失败的问题和添加广告失败的问题
|
||||
2. 管理后台UI优化
|
||||
3. 首页添加描述显示
|
||||
|
||||
### v1.2.0
|
||||
1. 新增手动批量转存
|
||||
2. 新增QQ机器人
|
||||
3. 新增任务管理功能
|
||||
4. 自动转存改版(批量转存修改为,显示二维码时自动转存)
|
||||
5. 新增支持第三方统计代码配置
|
||||
|
||||
### v1.0.0
|
||||
1. 支持API,手动批量录入资源
|
||||
2. 支持,自动判断资源有效性
|
||||
3. 支持自动转存
|
||||
4. 支持平台多账号管理(Quark)
|
||||
5. 支持简单的数据统计
|
||||
23
Dockerfile
23
Dockerfile
@@ -28,11 +28,26 @@ WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 先复制VERSION文件,确保构建时能正确读取版本号
|
||||
COPY VERSION ./
|
||||
|
||||
# 复制所有源代码
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
|
||||
|
||||
# 定义构建参数
|
||||
ARG VERSION
|
||||
ARG GIT_COMMIT
|
||||
ARG GIT_BRANCH
|
||||
ARG BUILD_TIME
|
||||
|
||||
# 获取版本信息并编译
|
||||
RUN VERSION=${VERSION:-$(cat VERSION)} && \
|
||||
GIT_COMMIT=${GIT_COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")} && \
|
||||
GIT_BRANCH=${GIT_BRANCH:-$(git branch --show-current 2>/dev/null || echo "unknown")} && \
|
||||
BUILD_TIME=${BUILD_TIME:-$(date '+%Y-%m-%d %H:%M:%S')} && \
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \
|
||||
-ldflags "-X 'github.com/ctwj/urldb/utils.Version=${VERSION}' \
|
||||
-X 'github.com/ctwj/urldb/utils.BuildTime=${BUILD_TIME}' \
|
||||
-X 'github.com/ctwj/urldb/utils.GitCommit=${GIT_COMMIT}' \
|
||||
-X 'github.com/ctwj/urldb/utils.GitBranch=${GIT_BRANCH}'" \
|
||||
-o main .
|
||||
|
||||
# 后端运行阶段
|
||||
FROM alpine:latest AS backend
|
||||
|
||||
55
README.md
55
README.md
@@ -10,6 +10,10 @@
|
||||
|
||||
**一个现代化的网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘 **
|
||||
|
||||
免费电报资源频道: [@xypan](https://t.me/xypan) 自动推送资源
|
||||
|
||||
免费电报资源机器人: [@L9ResBot](https://t.me/L9ResBot) 发送 搜索 + 名字 可搜索资源
|
||||
|
||||
🌐 [在线演示](https://pan.l9.lc) | 📖 [文档](https://ecn5khs4t956.feishu.cn/wiki/PsnDwtxghiP0mLkTiruczKtxnwd?from=from_copylink) | 🐛 [问题反馈](https://github.com/ctwj/urldb/issues) | ⭐ [给个星标](https://github.com/ctwj/urldb)
|
||||
|
||||
### 支持的网盘平台
|
||||
@@ -20,7 +24,7 @@
|
||||
| 阿里云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 夸克网盘 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
|
||||
| 天翼云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 迅雷云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 迅雷云盘 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
|
||||
| UC网盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 123云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 115网盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
@@ -34,31 +38,23 @@
|
||||
- [文档说明](https://ecn5khs4t956.feishu.cn/wiki/PsnDwtxghiP0mLkTiruczKtxnwd?from=from_copylink)
|
||||
- [服务器要求](https://ecn5khs4t956.feishu.cn/wiki/W8YBww1Mmiu4Cdkp5W4c8pFNnMf?from=from_copylink)
|
||||
- [QQ机器人](https://github.com/ctwj/astrbot_plugin_urldb)
|
||||
- [Telegram机器人](https://ecn5khs4t956.feishu.cn/wiki/SwkQw6AzRiFes7kxJXac3pd2ncb?from=from_copylink)
|
||||
- [微信公众号自动回复](https://ecn5khs4t956.feishu.cn/wiki/APOEwOyDYicKGHk7gTzcQKpynkf?from=from_copylink)
|
||||
|
||||
### v1.2.3
|
||||
1. 添加图片上传功能
|
||||
2. 添加Logo配置项,首页Logo显示
|
||||
3. 后台界面体验优化
|
||||
### v1.3.3
|
||||
1. 新增公众号自动回复
|
||||
2. 修复一些问题
|
||||
|
||||
### v1.2.1
|
||||
1. 修复转存移除广告失败的问题和添加广告失败的问题
|
||||
2. 管理后台UI优化
|
||||
3. 首页添加描述显示
|
||||
|
||||
### v1.2.0
|
||||
1. 新增手动批量转存
|
||||
2. 新增QQ机器人
|
||||
3. 新增任务管理功能
|
||||
4. 自动转存改版(批量转存修改为,显示二维码时自动转存)
|
||||
5. 新增支持第三方统计代码配置
|
||||
[详细改动记录](https://github.com/ctwj/urldb/blob/main/ChangeLog.md)
|
||||
|
||||
### v1.0.0
|
||||
当前特性
|
||||
1. 支持API,手动批量录入资源
|
||||
2. 支持,自动判断资源有效性
|
||||
3. 支持自动转存
|
||||
4. 支持平台多账号管理(Quark)
|
||||
4. 支持平台多账号管理
|
||||
5. 支持简单的数据统计
|
||||
|
||||
6. 支持Meilisearch
|
||||
|
||||
|
||||
---
|
||||
@@ -66,7 +62,6 @@
|
||||
## 📸 项目截图
|
||||
|
||||
|
||||
|
||||
### 🏠 首页
|
||||

|
||||
|
||||
@@ -128,17 +123,12 @@ PORT=8080
|
||||
|
||||
# 时区配置
|
||||
TIMEZONE=Asia/Shanghai
|
||||
```
|
||||
|
||||
### 镜像构建
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO # 日志级别 (DEBUG, INFO, WARN, ERROR, FATAL)
|
||||
DEBUG=false # 调试模式开关
|
||||
STRUCTURED_LOG=false # 结构化日志开关 (JSON格式)
|
||||
```
|
||||
docker build -t ctwj/urldb-frontend:1.0.7 --target frontend .
|
||||
docker build -t ctwj/urldb-backend:1.0.7 --target backend .
|
||||
docker push ctwj/urldb-frontend:1.0.7
|
||||
docker push ctwj/urldb-backend:1.0.7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 许可证
|
||||
@@ -158,11 +148,8 @@ docker push ctwj/urldb-backend:1.0.7
|
||||
|
||||
---
|
||||
|
||||
## 📞 联系我们
|
||||
|
||||
- **项目地址**: [https://github.com/ctwj/urldb](https://github.com/ctwj/urldb)
|
||||
- **问题反馈**: [Issues](https://github.com/ctwj/urldb/issues)
|
||||
- **邮箱**: 510199617@qq.com
|
||||
## 📞 交流群
|
||||
- **TG**: [Telegram 技术交流群](https://t.me/+QF9OMpOv-PBjZGEx)
|
||||
|
||||
---
|
||||
|
||||
@@ -172,4 +159,4 @@ docker push ctwj/urldb-backend:1.0.7
|
||||
|
||||
Made with ❤️ by [老九]
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
@@ -255,9 +257,9 @@ func (a *AlipanService) DeleteFiles(fileList []string) (*TransferResult, error)
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func (a *AlipanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||
func (a *AlipanService) GetUserInfo(cookie *string) (*UserInfo, error) {
|
||||
// 设置Cookie
|
||||
a.SetHeader("Cookie", cookie)
|
||||
a.SetHeader("Cookie", *cookie)
|
||||
|
||||
// 获取access token
|
||||
accessToken, err := a.manageAccessToken()
|
||||
@@ -347,6 +349,11 @@ func (a *AlipanService) getAlipan1(shareID string) (*AlipanShareInfo, error) {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByEntity 根据 entity.Cks 获取用户信息(待实现)
|
||||
func (a *AlipanService) GetUserInfoByEntity(cks entity.Cks) (*UserInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getAlipan2 通过分享id获取X-Share-Token
|
||||
func (a *AlipanService) getAlipan2(shareID string) (*AlipanShareToken, error) {
|
||||
data := map[string]interface{}{
|
||||
@@ -399,6 +406,9 @@ func (a *AlipanService) getAlipan4(shareData map[string]interface{}) (*AlipanSha
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (u *AlipanService) SetCKSRepository(cksRepo repo.CksRepository, entity entity.Cks) {
|
||||
}
|
||||
|
||||
// manageAccessToken 管理access token
|
||||
func (a *AlipanService) manageAccessToken() (string, error) {
|
||||
if a.accessToken != "" {
|
||||
|
||||
@@ -2,6 +2,9 @@ package pan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
)
|
||||
|
||||
// BaiduPanService 百度网盘服务
|
||||
@@ -50,9 +53,9 @@ func (b *BaiduPanService) DeleteFiles(fileList []string) (*TransferResult, error
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func (b *BaiduPanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||
func (b *BaiduPanService) GetUserInfo(cookie *string) (*UserInfo, error) {
|
||||
// 设置Cookie
|
||||
b.SetHeader("Cookie", cookie)
|
||||
b.SetHeader("Cookie", *cookie)
|
||||
|
||||
// 调用百度网盘用户信息API
|
||||
userInfoURL := "https://pan.baidu.com/api/gettemplatevariable"
|
||||
@@ -101,3 +104,21 @@ func (b *BaiduPanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||
ServiceType: "baidu",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByEntity 根据 entity.Cks 获取用户信息(待实现)
|
||||
func (b *BaiduPanService) GetUserInfoByEntity(cks entity.Cks) (*UserInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (u *BaiduPanService) SetCKSRepository(cksRepo repo.CksRepository, entity entity.Cks) {
|
||||
}
|
||||
|
||||
func (x *BaiduPanService) UpdateConfig(config *PanConfig) {
|
||||
if config == nil {
|
||||
return
|
||||
}
|
||||
x.config = config
|
||||
if config.Cookie != "" {
|
||||
x.SetHeader("Cookie", config.Cookie)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
)
|
||||
|
||||
// ServiceType 定义网盘服务类型
|
||||
@@ -74,6 +77,7 @@ type UserInfo struct {
|
||||
UsedSpace int64 `json:"usedSpace"` // 已使用空间
|
||||
TotalSpace int64 `json:"totalSpace"` // 总空间
|
||||
ServiceType string `json:"serviceType"` // 服务类型
|
||||
ExtraData string `json:"extraData"` // 额外信息
|
||||
}
|
||||
|
||||
// PanService 网盘服务接口
|
||||
@@ -88,10 +92,14 @@ type PanService interface {
|
||||
DeleteFiles(fileList []string) (*TransferResult, error)
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
GetUserInfo(cookie string) (*UserInfo, error)
|
||||
GetUserInfo(ck *string) (*UserInfo, error)
|
||||
|
||||
// GetServiceType 获取服务类型
|
||||
GetServiceType() ServiceType
|
||||
|
||||
SetCKSRepository(cksRepo repo.CksRepository, entity entity.Cks)
|
||||
|
||||
UpdateConfig(config *PanConfig)
|
||||
}
|
||||
|
||||
// PanFactory 网盘工厂
|
||||
@@ -249,6 +257,9 @@ func ExtractShareId(url string) (string, ServiceType) {
|
||||
shareID = url[substring:]
|
||||
|
||||
// 去除可能的锚点
|
||||
if hashIndex := strings.Index(shareID, "?"); hashIndex != -1 {
|
||||
shareID = shareID[:hashIndex]
|
||||
}
|
||||
if hashIndex := strings.Index(shareID, "#"); hashIndex != -1 {
|
||||
shareID = shareID[:hashIndex]
|
||||
}
|
||||
|
||||
@@ -29,35 +29,31 @@ var configRefreshChan = make(chan bool, 1)
|
||||
|
||||
// 单例相关变量
|
||||
var (
|
||||
quarkInstance *QuarkPanService
|
||||
quarkOnce sync.Once
|
||||
systemConfigRepo repo.SystemConfigRepository
|
||||
systemConfigOnce sync.Once
|
||||
)
|
||||
|
||||
// NewQuarkPanService 创建夸克网盘服务(单例模式)
|
||||
func NewQuarkPanService(config *PanConfig) *QuarkPanService {
|
||||
quarkOnce.Do(func() {
|
||||
quarkInstance = &QuarkPanService{
|
||||
BasePanService: NewBasePanService(config),
|
||||
}
|
||||
quarkInstance := &QuarkPanService{
|
||||
BasePanService: NewBasePanService(config),
|
||||
}
|
||||
|
||||
// 设置夸克网盘的默认请求头
|
||||
quarkInstance.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Sec-Ch-Ua": `"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"`,
|
||||
"Sec-Ch-Ua-Mobile": "?0",
|
||||
"Sec-Ch-Ua-Platform": `"Windows"`,
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-site",
|
||||
"Referer": "https://pan.quark.cn/",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"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",
|
||||
"Cookie": config.Cookie,
|
||||
})
|
||||
// 设置夸克网盘的默认请求头
|
||||
quarkInstance.SetHeaders(map[string]string{
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Sec-Ch-Ua": `"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"`,
|
||||
"Sec-Ch-Ua-Mobile": "?0",
|
||||
"Sec-Ch-Ua-Platform": `"Windows"`,
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-site",
|
||||
"Referer": "https://pan.quark.cn/",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"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",
|
||||
"Cookie": config.Cookie,
|
||||
})
|
||||
|
||||
// 更新配置
|
||||
@@ -947,10 +943,10 @@ type PasswordResult struct {
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func (q *QuarkPanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||
func (q *QuarkPanService) GetUserInfo(cookie *string) (*UserInfo, error) {
|
||||
// 临时设置cookie
|
||||
originalCookie := q.GetHeader("Cookie")
|
||||
q.SetHeader("Cookie", cookie)
|
||||
q.SetHeader("Cookie", *cookie)
|
||||
defer q.SetHeader("Cookie", originalCookie) // 恢复原始cookie
|
||||
|
||||
// 获取用户基本信息
|
||||
@@ -1028,6 +1024,9 @@ func (q *QuarkPanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (xq *QuarkPanService) SetCKSRepository(cksRepo repo.CksRepository, entity entity.Cks) {
|
||||
}
|
||||
|
||||
// formatBytes 格式化字节数为可读格式
|
||||
func formatBytes(bytes int64) string {
|
||||
const unit = 1024
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package pan
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
)
|
||||
|
||||
// UCService UC网盘服务
|
||||
type UCService struct {
|
||||
@@ -47,10 +52,20 @@ func (u *UCService) DeleteFiles(fileList []string) (*TransferResult, error) {
|
||||
return ErrorResult("UC网盘文件删除功能暂未实现"), nil
|
||||
}
|
||||
|
||||
func (x *UCService) UpdateConfig(config *PanConfig) {
|
||||
if config == nil {
|
||||
return
|
||||
}
|
||||
x.config = config
|
||||
if config.Cookie != "" {
|
||||
x.SetHeader("Cookie", config.Cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func (u *UCService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||
func (u *UCService) GetUserInfo(cookie *string) (*UserInfo, error) {
|
||||
// 设置Cookie
|
||||
u.SetHeader("Cookie", cookie)
|
||||
u.SetHeader("Cookie", *cookie)
|
||||
|
||||
// 调用UC网盘用户信息API
|
||||
userInfoURL := "https://drive.uc.cn/api/user/info"
|
||||
@@ -97,3 +112,11 @@ func (u *UCService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||
ServiceType: "uc",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByEntity 根据 entity.Cks 获取用户信息(待实现)
|
||||
func (u *UCService) GetUserInfoByEntity(cks entity.Cks) (*UserInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (u *UCService) SetCKSRepository(cksRepo repo.CksRepository, entity entity.Cks) {
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func extractShareID(urlStr string) (string, string) {
|
||||
},
|
||||
XunleiStr: {
|
||||
Domains: []string{"pan.xunlei.com"},
|
||||
Pattern: regexp.MustCompile(`https?://(?:www\.)?pan\.xunlei\.com/s/([a-zA-Z0-9-]+)`),
|
||||
Pattern: regexp.MustCompile(`https?://(?:www\.)?pan\.xunlei\.com/s/([a-zA-Z0-9-_]+)`),
|
||||
},
|
||||
BaiduStr: {
|
||||
Domains: []string{"pan.baidu.com", "yun.baidu.com"},
|
||||
|
||||
444
common/xunlei.txt
Normal file
444
common/xunlei.txt
Normal file
@@ -0,0 +1,444 @@
|
||||
POST /v1/shield/captcha/init HTTP/1.1
|
||||
Host: xluser-ssl.xunlei.com
|
||||
Connection: close
|
||||
Content-Length: 502
|
||||
sec-ch-ua-platform: "macOS"
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
x-device-name: PC-Chrome
|
||||
sec-ch-ua-mobile: ?0
|
||||
x-device-model: chrome%2F139.0.0.0
|
||||
x-provider-name: NONE
|
||||
x-platform-version: 1
|
||||
content-type: application/json
|
||||
x-client-id: XW5SkOhLDjnOZP7J
|
||||
x-protocol-version: 301
|
||||
x-net-work-type: NONE
|
||||
x-os-version: MacIntel
|
||||
x-device-sign: wdi10.c24ecadc44c643637d127fb847dbe36d74ee67f56a443148fc801eb27bb3e058
|
||||
accept-language: zh-cn
|
||||
x-sdk-version: 8.1.4
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-client-version: 1.1.6
|
||||
DNT: 1
|
||||
Accept: */*
|
||||
Origin: https://i.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://i.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
|
||||
{"client_id":"XW5SkOhLDjnOZP7J","action":"POST:/v1/auth/verification","device_id":"c24ecadc44c643637d127fb847dbe36d","captcha_token":"ck0.iomdNE7hSgjR_6Q8bb4T0diVDSUD2Q2XRAdXr3xiVyvgSks1GLMw88pwxSSiTMiPcJojvVGxjKk58tg0iFMLPVOIi1qdstLeWtIJfgk2C2FtyNtl-XveEYFy_gyW4qUVYkeEPoDScctqSBNjDKvCIpLuCh3p6dKXFpiMAMBcY8USOYzutMt0oO_L-a-YisQGG9x6yN2Iik3fPAu4_IbfhdBctqha10OajDCPBaRqjdZtBuFifxq9qMpSUiZWuP6FiZ8hxj66_mrgY-yW90lCYT6JerSal78OYByU8DWh6UnfUzRgrhsqQukgeZv9YEtE","meta":{"phone_number":"+86 18163659661"}}
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 13:50:28 GMT
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Connection: close
|
||||
Vary: Accept-Encoding
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Project-Id, X-Device-Id, X-Request-Id, X-Captcha-Token, X-Client-Id, X-Sdk-Version, X-Client-Version, X-Action, X-Auto-Login, X-Device-Name, X-Device-Model, X-Net-Work-Type, X-Os-Version, X-Protocol-Version, X-Platform-Version, X-Provider-Name, X-Device-Sign, X-Client-Channel-Id, X-Peer-Id
|
||||
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, PATCH, OPTIONS
|
||||
Access-Control-Allow-Origin: https://i.xunlei.com
|
||||
Access-Control-Max-Age: 86400
|
||||
Strict-Transport-Security: max-age=5184000; includeSubDomains
|
||||
Vary: Origin, Accept-Encoding
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Dns-Prefetch-Control: off
|
||||
X-Download-Options: noopen
|
||||
X-Frame-Options: DENY
|
||||
X-Request-Id: 421c1f2621e9acd295973c3df960ce37
|
||||
X-Xss-Protection: 1; mode=block
|
||||
Content-Length: 340
|
||||
|
||||
{"captcha_token":"ck0.yiuSDIIJNBkSmuz9aViGbFVX39L7wcxw6GjlMB7xBDTx7trq1EvQFLQzfzSCrDdicNDrda35Pt6lps-mUFzKehxZo3g_Xmw93R7UpImXB-9zsFVeaNGrQ1V0J0TJ4TbTpKJ6UIEr3agYs29g5DAZmuHxgIg1GjFq53GzGv6sh-eshFYA9tViXoTGo0OjmwdTvFnsPaQhgn0Ubn6Io8LDQpnKOAw74L5zKxxzowd7kSZqpJeeDuFxAeIGOHnl0FGtL6S4e5Cswa-xixQZLq_ufT7rxiJW4bM4ndgsC6jFFxY","expires_in":300}
|
||||
|
||||
|
||||
===================
|
||||
|
||||
POST /v1/auth/verification HTTP/1.1
|
||||
Host: xluser-ssl.xunlei.com
|
||||
Connection: close
|
||||
Content-Length: 98
|
||||
sec-ch-ua-platform: "macOS"
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
x-device-name: PC-Chrome
|
||||
sec-ch-ua-mobile: ?0
|
||||
x-device-model: chrome%2F139.0.0.0
|
||||
x-provider-name: NONE
|
||||
x-platform-version: 1
|
||||
content-type: application/json
|
||||
x-client-id: XW5SkOhLDjnOZP7J
|
||||
x-protocol-version: 301
|
||||
x-net-work-type: NONE
|
||||
x-os-version: MacIntel
|
||||
x-device-sign: wdi10.c24ecadc44c643637d127fb847dbe36d74ee67f56a443148fc801eb27bb3e058
|
||||
accept-language: zh-cn
|
||||
x-sdk-version: 8.1.4
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-captcha-token: ck0.yiuSDIIJNBkSmuz9aViGbFVX39L7wcxw6GjlMB7xBDTx7trq1EvQFLQzfzSCrDdicNDrda35Pt6lps-mUFzKehxZo3g_Xmw93R7UpImXB-9zsFVeaNGrQ1V0J0TJ4TbTpKJ6UIEr3agYs29g5DAZmuHxgIg1GjFq53GzGv6sh-eshFYA9tViXoTGo0OjmwdTvFnsPaQhgn0Ubn6Io8LDQpnKOAw74L5zKxxzowd7kSZqpJeeDuFxAeIGOHnl0FGtL6S4e5Cswa-xixQZLq_ufT7rxiJW4bM4ndgsC6jFFxY
|
||||
x-client-version: 1.1.6
|
||||
DNT: 1
|
||||
Accept: */*
|
||||
Origin: https://i.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://i.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
|
||||
{"phone_number":"+86 18163659661","target":"ANY","usage":"SIGN_IN","client_id":"XW5SkOhLDjnOZP7J"}
|
||||
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 13:50:28 GMT
|
||||
Content-Type: application/json
|
||||
Connection: close
|
||||
Vary: Accept-Encoding
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type,Accept, X-Project-Id, X-Device-Id, X-Request-Id, X-Captcha-Token, X-Client-Id, x-sdk-version, x-client-version, x-device-name, x-device-model, x-captcha-token, x-net-work-type, x-os-version, x-protocol-version, x-platform-version, x-provider-name, x-client-channel-id, x-appname, x-appid, x-device-sign, x-auto-login, x-peer-id, x-action
|
||||
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, PATCH, OPTIONS
|
||||
Access-Control-Allow-Origin: https://i.xunlei.com
|
||||
Access-Control-Max-Age: 86400
|
||||
Vary: Origin, Accept-Encoding
|
||||
Vary: Accept-Encoding
|
||||
Content-Length: 691
|
||||
|
||||
{"verification_id":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImRhMjNiMGFiLTc5NjAtNDQzNS1hMmY1LWNmMGMzMjAxNWE0NiJ9.eyJhdWQiOiJYVzVTa09oTERqbk9aUDdKIiwiZXhwIjoxNzU1Njk4MTI4LCJwIjoiKzg2IDE4MTYzNjU5NjYxIiwicHJvamVjdF9pZCI6IjJydms0ZTNna2RubDd1MWtsMGsiLCJ0IjoiYUtYU3BHTnljSFFGelYtVTVicFQifQ.JN2i0feZedA4VDrCZzMDc0MEnVSe6j6jhB11RsSvt9qiByge5lCsYuMGz-RwxMiU_FEnUxYHSzPJu82sskU4a66k8AOXBqCyhLy3TlSq1KkUXl7uylGRxf99AZfJhZs0Rgm_H---rWIxx8x4DJdrQxWp5hcUCmSGL95p47xJGQDayNhb4Y-eOup9DYik6KOAzHtGl8NRzeE-k-XCXiGMRc-sv2mILPpWWinVhSExR2fHhDcjNtsPJgSguEv7Kqevg029fXSQ-uZAh9WmPkW5rHnb-e7buXMrSOGtKdV7AVarRRWWa039M7L8rrYmq33dv5IX_BvGUk7elAaMmWXrxw", "is_user":true, "expires_in":300, "selected_channel":"VERIFICATION_PHONE"}
|
||||
|
||||
=======================
|
||||
|
||||
POST /v1/auth/verification/verify HTTP/1.1
|
||||
Host: xluser-ssl.xunlei.com
|
||||
Connection: close
|
||||
Content-Length: 676
|
||||
sec-ch-ua-platform: "macOS"
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
x-device-name: PC-Chrome
|
||||
sec-ch-ua-mobile: ?0
|
||||
x-device-model: chrome%2F139.0.0.0
|
||||
x-provider-name: NONE
|
||||
x-platform-version: 1
|
||||
content-type: application/json
|
||||
x-client-id: XW5SkOhLDjnOZP7J
|
||||
x-protocol-version: 301
|
||||
x-net-work-type: NONE
|
||||
x-os-version: MacIntel
|
||||
x-device-sign: wdi10.c24ecadc44c643637d127fb847dbe36d74ee67f56a443148fc801eb27bb3e058
|
||||
accept-language: zh-cn
|
||||
x-sdk-version: 8.1.4
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-client-version: 1.1.6
|
||||
DNT: 1
|
||||
Accept: */*
|
||||
Origin: https://i.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://i.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
|
||||
{"verification_id":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImRhMjNiMGFiLTc5NjAtNDQzNS1hMmY1LWNmMGMzMjAxNWE0NiJ9.eyJhdWQiOiJYVzVTa09oTERqbk9aUDdKIiwiZXhwIjoxNzU1Njk4MTI4LCJwIjoiKzg2IDE4MTYzNjU5NjYxIiwicHJvamVjdF9pZCI6IjJydms0ZTNna2RubDd1MWtsMGsiLCJ0IjoiYUtYU3BHTnljSFFGelYtVTVicFQifQ.JN2i0feZedA4VDrCZzMDc0MEnVSe6j6jhB11RsSvt9qiByge5lCsYuMGz-RwxMiU_FEnUxYHSzPJu82sskU4a66k8AOXBqCyhLy3TlSq1KkUXl7uylGRxf99AZfJhZs0Rgm_H---rWIxx8x4DJdrQxWp5hcUCmSGL95p47xJGQDayNhb4Y-eOup9DYik6KOAzHtGl8NRzeE-k-XCXiGMRc-sv2mILPpWWinVhSExR2fHhDcjNtsPJgSguEv7Kqevg029fXSQ-uZAh9WmPkW5rHnb-e7buXMrSOGtKdV7AVarRRWWa039M7L8rrYmq33dv5IX_BvGUk7elAaMmWXrxw","verification_code":"454882","client_id":"XW5SkOhLDjnOZP7J"}
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 13:50:46 GMT
|
||||
Content-Type: application/json
|
||||
Connection: close
|
||||
Vary: Accept-Encoding
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type,Accept, X-Project-Id, X-Device-Id, X-Request-Id, X-Captcha-Token, X-Client-Id, x-sdk-version, x-client-version, x-device-name, x-device-model, x-captcha-token, x-net-work-type, x-os-version, x-protocol-version, x-platform-version, x-provider-name, x-client-channel-id, x-appname, x-appid, x-device-sign, x-auto-login, x-peer-id, x-action
|
||||
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, PATCH, OPTIONS
|
||||
Access-Control-Allow-Origin: https://i.xunlei.com
|
||||
Access-Control-Max-Age: 86400
|
||||
Vary: Origin, Accept-Encoding
|
||||
Vary: Accept-Encoding
|
||||
Content-Length: 706
|
||||
|
||||
{"verification_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImRhMjNiMGFiLTc5NjAtNDQzNS1hMmY1LWNmMGMzMjAxNWE0NiJ9.eyJhdWQiOiJYVzVTa09oTERqbk9aUDdKIiwiZXhwIjoxNzU1Njk4NDQ2LCJpc3MiOiJodHRwczovL2FwaXMueGJhc2UuY2xvdWQiLCJwaG9uZV9udW1iZXIiOiIrODYgMTgxNjM2NTk2NjEiLCJwcm9qZWN0X2lkIjoiMnJ2azRlM2drZG5sN3Uxa2wwayIsInJlc3VsdCI6IjAiLCJ0eXBlIjoidmVyaWZpY2F0aW9uIn0.EGyqiswEF72e_OiiL0sLhZRkZpCbnd-atG4zCAXTIkaoWQ5Gpuceg1lyXGDT-HNo-BtmtqjZBXgJveO8j1q12w2l1iloaYvarVDmIgEzH-Iq-LN5BHcUYJCLZKIhd0sU1SpoU7U3Hjv837TACJ9L2PS3g9evtqyXNv-E6_9U0xwTj_0BCKbil3qyOtlp-W24RY2yOkUPN4uKLlQAUpIcujDsKRjTsZvIzoED7RZutHsdwg4qhy5VaquP9hc62z6HSAwhtTlp4cwXEMpkev3PfjDzAbE1h935UqVgm3NmaylCAcICRB5VwfLwe8qLAT_N7-gFXMwdaqJPrDoOkWZZpg", "expires_in":600}
|
||||
|
||||
====================================
|
||||
|
||||
|
||||
POST /v1/auth/signin HTTP/1.1
|
||||
Host: xluser-ssl.xunlei.com
|
||||
Connection: close
|
||||
Content-Length: 777
|
||||
sec-ch-ua-platform: "macOS"
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
x-device-name: PC-Chrome
|
||||
sec-ch-ua-mobile: ?0
|
||||
x-device-model: chrome%2F139.0.0.0
|
||||
x-provider-name: NONE
|
||||
x-platform-version: 1
|
||||
content-type: application/json
|
||||
x-client-id: XW5SkOhLDjnOZP7J
|
||||
x-protocol-version: 301
|
||||
x-net-work-type: NONE
|
||||
x-os-version: MacIntel
|
||||
x-device-sign: wdi10.c24ecadc44c643637d127fb847dbe36d74ee67f56a443148fc801eb27bb3e058
|
||||
accept-language: zh-cn
|
||||
x-sdk-version: 8.1.4
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-captcha-token: ck0.yiuSDIIJNBkSmuz9aViGbFVX39L7wcxw6GjlMB7xBDTx7trq1EvQFLQzfzSCrDdicNDrda35Pt6lps-mUFzKehxZo3g_Xmw93R7UpImXB-9zsFVeaNGrQ1V0J0TJ4TbTpKJ6UIEr3agYs29g5DAZmuHxgIg1GjFq53GzGv6sh-eshFYA9tViXoTGo0OjmwdTvFnsPaQhgn0Ubn6Io8LDQpnKOAw74L5zKxxzowd7kSZqpJeeDuFxAeIGOHnl0FGtL6S4e5Cswa-xixQZLq_ufT7rxiJW4bM4ndgsC6jFFxY
|
||||
x-client-version: 1.1.6
|
||||
DNT: 1
|
||||
Accept: */*
|
||||
Origin: https://i.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://i.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
|
||||
{"username":"+86 18163659661","verification_code":"454882","verification_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImRhMjNiMGFiLTc5NjAtNDQzNS1hMmY1LWNmMGMzMjAxNWE0NiJ9.eyJhdWQiOiJYVzVTa09oTERqbk9aUDdKIiwiZXhwIjoxNzU1Njk4NDQ2LCJpc3MiOiJodHRwczovL2FwaXMueGJhc2UuY2xvdWQiLCJwaG9uZV9udW1iZXIiOiIrODYgMTgxNjM2NTk2NjEiLCJwcm9qZWN0X2lkIjoiMnJ2azRlM2drZG5sN3Uxa2wwayIsInJlc3VsdCI6IjAiLCJ0eXBlIjoidmVyaWZpY2F0aW9uIn0.EGyqiswEF72e_OiiL0sLhZRkZpCbnd-atG4zCAXTIkaoWQ5Gpuceg1lyXGDT-HNo-BtmtqjZBXgJveO8j1q12w2l1iloaYvarVDmIgEzH-Iq-LN5BHcUYJCLZKIhd0sU1SpoU7U3Hjv837TACJ9L2PS3g9evtqyXNv-E6_9U0xwTj_0BCKbil3qyOtlp-W24RY2yOkUPN4uKLlQAUpIcujDsKRjTsZvIzoED7RZutHsdwg4qhy5VaquP9hc62z6HSAwhtTlp4cwXEMpkev3PfjDzAbE1h935UqVgm3NmaylCAcICRB5VwfLwe8qLAT_N7-gFXMwdaqJPrDoOkWZZpg","client_id":"XW5SkOhLDjnOZP7J"}
|
||||
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 13:50:47 GMT
|
||||
Content-Type: application/json
|
||||
Connection: close
|
||||
Vary: Accept-Encoding
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type,Accept, X-Project-Id, X-Device-Id, X-Request-Id, X-Captcha-Token, X-Client-Id, x-sdk-version, x-client-version, x-device-name, x-device-model, x-captcha-token, x-net-work-type, x-os-version, x-protocol-version, x-platform-version, x-provider-name, x-client-channel-id, x-appname, x-appid, x-device-sign, x-auto-login, x-peer-id, x-action
|
||||
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, PATCH, OPTIONS
|
||||
Access-Control-Allow-Origin: https://i.xunlei.com
|
||||
Access-Control-Max-Age: 86400
|
||||
Vary: Origin, Accept-Encoding
|
||||
Vary: Accept-Encoding
|
||||
Content-Length: 983
|
||||
|
||||
{"token_type":"Bearer", "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjAzZjlmMzYxLWI3MjktNDVjNi04MjI0LTNiNWEwNmJmOGU0NSJ9.eyJpc3MiOiJodHRwczovL3hsdXNlci1zc2wueHVubGVpLmNvbSIsInN1YiI6IjEyMTk2MzY5NTIiLCJhdWQiOiJYVzVTa09oTERqbk9aUDdKIiwiZXhwIjoxNzU1NzA1MDQ3LCJpYXQiOjE3NTU2OTc4NDcsImF0X2hhc2giOiJyLnJLZG5uWDNNRWZDZloyWTBNdlJYRnciLCJzY29wZSI6InVzZXIgcGFuIHByb2ZpbGUgb2ZmbGluZSIsInByb2plY3RfaWQiOiIycnZrNGUzZ2tkbmw3dTFrbDBrIiwibWV0YSI6eyJhIjoiR3hkMjNQK0VreGFXWVJ3K1FwdUtyRTZmb3kwRGh2aE5UMmhSbnd2S3F5VT0ifX0.s6mbN3Imr2WKDexAMXW7C5FoqPF4_eS0oPFyHTe-DbcmvTuC1KcRcDlCjt92An8A2wluvD4t1BbHSGv_1U8CFcE_VGtWJoy3yPoscfyGLQCbz38UY-q9r94s8ABtYTe4fZLOHRB20uc71aB87rGDe0IyzafkimgSbrETZiS4v95VvuZbP_YTwAdcAuiRRgMb1YWvAkkBEWTlvRVUFryCZVP0oecanpeDrXxUxV_SqAtI-ix-mCw5N1g91B88tkg7FtJfGTS5LA8KTXBIiAq73-jPzZ0padssq4uFVEiKXGOAO9rKtsI7gBxsQpW9do_bh0g2JYVz7Op5OLvMrGwJTw", "refresh_token":"a1.TK4L3Xi38Gil0rcGFvQx777bbE7luNneIpEPbPOFLF1pxmSu62Yr", "expires_in":7200, "sub":"1219636952", "user_id":"1219636952"}
|
||||
|
||||
|
||||
|
||||
======================
|
||||
|
||||
|
||||
|
||||
GET /v1/user/me HTTP/1.1
|
||||
Host: xluser-ssl.xunlei.com
|
||||
Connection: close
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjAzZjlmMzYxLWI3MjktNDVjNi04MjI0LTNiNWEwNmJmOGU0NSJ9.eyJpc3MiOiJodHRwczovL3hsdXNlci1zc2wueHVubGVpLmNvbSIsInN1YiI6IjEyMTk2MzY5NTIiLCJhdWQiOiJYVzVTa09oTERqbk9aUDdKIiwiZXhwIjoxNzU1NzA1MDQ3LCJpYXQiOjE3NTU2OTc4NDcsImF0X2hhc2giOiJyLnJLZG5uWDNNRWZDZloyWTBNdlJYRnciLCJzY29wZSI6InVzZXIgcGFuIHByb2ZpbGUgb2ZmbGluZSIsInByb2plY3RfaWQiOiIycnZrNGUzZ2tkbmw3dTFrbDBrIiwibWV0YSI6eyJhIjoiR3hkMjNQK0VreGFXWVJ3K1FwdUtyRTZmb3kwRGh2aE5UMmhSbnd2S3F5VT0ifX0.s6mbN3Imr2WKDexAMXW7C5FoqPF4_eS0oPFyHTe-DbcmvTuC1KcRcDlCjt92An8A2wluvD4t1BbHSGv_1U8CFcE_VGtWJoy3yPoscfyGLQCbz38UY-q9r94s8ABtYTe4fZLOHRB20uc71aB87rGDe0IyzafkimgSbrETZiS4v95VvuZbP_YTwAdcAuiRRgMb1YWvAkkBEWTlvRVUFryCZVP0oecanpeDrXxUxV_SqAtI-ix-mCw5N1g91B88tkg7FtJfGTS5LA8KTXBIiAq73-jPzZ0padssq4uFVEiKXGOAO9rKtsI7gBxsQpW9do_bh0g2JYVz7Op5OLvMrGwJTw
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
x-device-name: PC-Chrome
|
||||
sec-ch-ua-mobile: ?0
|
||||
x-device-model: chrome%2F139.0.0.0
|
||||
x-provider-name: NONE
|
||||
x-platform-version: 1
|
||||
content-type: application/json
|
||||
x-client-id: XW5SkOhLDjnOZP7J
|
||||
x-protocol-version: 301
|
||||
x-net-work-type: NONE
|
||||
x-os-version: MacIntel
|
||||
x-device-sign: wdi10.c24ecadc44c643637d127fb847dbe36d74ee67f56a443148fc801eb27bb3e058
|
||||
accept-language: zh-cn
|
||||
x-sdk-version: 8.1.4
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-client-version: 1.1.6
|
||||
DNT: 1
|
||||
Accept: */*
|
||||
Origin: https://i.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://i.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 13:50:47 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 1954
|
||||
Connection: close
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type,Accept, X-Project-Id, X-Device-Id, X-Request-Id, X-Captcha-Token, X-Client-Id, x-sdk-version, x-client-version, x-device-name, x-device-model, x-captcha-token, x-net-work-type, x-os-version, x-protocol-version, x-platform-version, x-provider-name, x-client-channel-id, x-appname, x-appid, x-device-sign, x-auto-login, x-peer-id, x-action
|
||||
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, PATCH, OPTIONS
|
||||
Access-Control-Allow-Origin: https://i.xunlei.com
|
||||
Access-Control-Max-Age: 86400
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Vary: Origin, Accept-Encoding
|
||||
Vary: Accept-Encoding
|
||||
|
||||
{"sub":"1219636952", "name":"王维ด้้้้้็็", "picture":"https://xfile2.a.88cdn.com/file/k/avatar/default", "phone_number":"+86 181***661", "providers":[{"id":"u", "provider_user_id":"2327081043"}, {"id":"qq.com", "provider_user_id":"UID_AC1EE453B67AF1B266C5CA0B4FB99A49"}], "status":"ACTIVE", "created_at":"2025-07-09T09:34:56Z", "password_updated_at":"2025-07-09T09:34:56Z", "id":"1219636952", "vips":[{"id":"vip15_0_0_2_15_0"}], "vip_info":[{"register":"19700101", "autodeduct":"0", "daily":"-10", "expire":"0", "grow":"0", "is_vip":"0", "last_pay":"0", "level":"0", "pay_id":"0", "remind":"0", "is_year":"0", "user_vas":"2", "vas_type":"0", "vip_icon":{"general":"https://xluser-ssl.xunlei.com/v1/file/icon/level/svip/deactivate_a/svip_level1_deactivate.png", "small":"https://xluser-ssl.xunlei.com/v1/file/icon/level/svip/deactivate_b/svip_level1_deactivate-1.png"}}, {"register":"0", "autodeduct":"0", "daily":"0", "expire":"0", "grow":"0", "is_vip":"0", "last_pay":"0", "level":"0", "pay_id":"0", "remind":"0", "is_year":"0", "user_vas":"15", "vas_type":"2", "vip_icon":{}}, {"register":"19700101", "autodeduct":"0", "daily":"0", "expire":"0", "grow":"0", "is_vip":"0", "last_pay":"0", "level":"0", "pay_id":"0", "remind":"0", "is_year":"0", "user_vas":"33", "vas_type":"0", "vip_icon":{}}, {"register":"19700101", "autodeduct":"0", "daily":"0", "expire":"0", "grow":"0", "is_vip":"0", "last_pay":"0", "level":"0", "pay_id":"0", "remind":"0", "is_year":"0", "user_vas":"303", "vas_type":"0", "vip_icon":{}}, {"register":"19700101", "autodeduct":"0", "daily":"0", "expire":"0", "grow":"0", "is_vip":"0", "last_pay":"0", "level":"0", "pay_id":"0", "remind":"0", "is_year":"0", "user_vas":"306", "vas_type":"0", "vip_icon":{"general":"https://xluser-ssl.xunlei.com/v1/file/icon/level/svip/snnual_deactivate/im_ypvip_deactivate.png", "small":"https://xluser-ssl.xunlei.com/v1/file/icon/level/svip/normal_b/im_ypvip_pure_normal.png"}}]}
|
||||
|
||||
|
||||
===========================
|
||||
|
||||
|
||||
|
||||
POST /v1/auth/token HTTP/1.1
|
||||
Host: xluser-ssl.xunlei.com
|
||||
Connection: close
|
||||
Content-Length: 427
|
||||
sec-ch-ua-platform: "macOS"
|
||||
x-device-sign: wdi10.c24ecadc44c643637d127fb847dbe36d74ee67f56a443148fc801eb27bb3e058
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
x-sdk-version: 3.4.20
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
sec-ch-ua-mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
DNT: 1
|
||||
content-type: application/json
|
||||
x-client-id: Xqp0kJBXWhwaTpB6
|
||||
x-protocol-version: 301
|
||||
Accept: */*
|
||||
Origin: https://pan.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://pan.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9,ko-KR;q=0.8,ko;q=0.7
|
||||
|
||||
{"code":"a1.oGotq0yXVGil0zJF5BS1YPllaP2RT3SbqOTaGs7SjmtE7VIPc9LcpaFchdkrjN3xTGPlXo7Q7SlEu6oNg_aW76tbjo6524JMW5vS_Ga8jHFTGuhiLXiJ3UP6qBx0C79hRFS_zFLuzIzCwQtkGF8Eksuyeg3G42jxWPLrzQBswiz3oqU8Ssusbw","grant_type":"authorization_code","code_verifier":"NnmDL5IumVBn9i8TOU15QrhBvbb995tv","redirect_uri":"https://pan.xunlei.com/login/?path=%2F%E6%88%91%E7%9A%84%E8%BD%AC%E5%AD%98&sso_sign_in_in_iframe=","client_id":"Xqp0kJBXWhwaTpB6"}
|
||||
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 13:50:51 GMT
|
||||
Content-Type: application/json
|
||||
Connection: close
|
||||
Vary: Accept-Encoding
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type,Accept, X-Project-Id, X-Device-Id, X-Request-Id, X-Captcha-Token, X-Client-Id, x-sdk-version, x-client-version, x-device-name, x-device-model, x-captcha-token, x-net-work-type, x-os-version, x-protocol-version, x-platform-version, x-provider-name, x-client-channel-id, x-appname, x-appid, x-device-sign, x-auto-login, x-peer-id, x-action
|
||||
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, PATCH, OPTIONS
|
||||
Access-Control-Allow-Origin: https://pan.xunlei.com
|
||||
Access-Control-Max-Age: 86400
|
||||
Vary: Origin, Accept-Encoding
|
||||
Vary: Accept-Encoding
|
||||
Content-Length: 975
|
||||
|
||||
{"token_type":"Bearer", "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjAzZjlmMzYxLWI3MjktNDVjNi04MjI0LTNiNWEwNmJmOGU0NSJ9.eyJpc3MiOiJodHRwczovL3hsdXNlci1zc2wueHVubGVpLmNvbSIsInN1YiI6IjEyMTk2MzY5NTIiLCJhdWQiOiJYcXAwa0pCWFdod2FUcEI2IiwiZXhwIjoxNzU1NzQxMDUxLCJpYXQiOjE3NTU2OTc4NTEsImF0X2hhc2giOiJyLmN0UWtJMUNmU1NpNkt2RFVUa0xidHciLCJzY29wZSI6InByb2ZpbGUgcGFuIHNzbyB1c2VyIiwicHJvamVjdF9pZCI6IjJydms0ZTNna2RubDd1MWtsMGsiLCJtZXRhIjp7ImEiOiJHOHNKVlJ6RjUzOE51eDRLOCt0VUtUR1hqdlZOTUczZlNrUEVaUWZLSWRBPSJ9fQ.ktWdGaShnGuU9CFj_awHaHfAoH0gkVBvdf4A24WPkRn-MUSpGRtUuo30VZay-wTbJ2UBEmw0pNtAeAfB97qzpPtVigDf9vD2WE9jIz61dASPxi6JIvioUfRzbgWa_Qj8a0bHc1Zvfufu9cSIzm0PPE7rSHYyq5oNfFxAAaY5k9cp2I4DclZon7hHJ0439knsu8MQEwQY42bRWppKqaI5q66Zlzeoc0t2I0Ehs2XYIyVyG2EYmFlbIWrpahudUTz6PbcrDMoHf8Y1hPnJn5ij7D32YSLXeP7ighQZDqaFoJHXJxqcHYToKut4JfQHCROkbqSwq3_I75k_A36gYtxVdA", "refresh_token":"a1.wve0uF2TK2il0rsGZhkUjjZRACg1R12R9OUdpmPbat2kKwtM", "expires_in":43200, "sub":"1219636952", "user_id":"1219636952"}
|
||||
|
||||
|
||||
==============================
|
||||
|
||||
GET /drive/v1/share?share_id=VOY4fDN-35yNfnqBJ3lSXfK4A1&pass_code=t84g&limit=100&pass_code_token=GdrGFHvaZTbIUKxOiJKyrWZpcdGoHysJZBa5iv7N8mmsNElcU%2F3M8%2BJkp1NO0cMKlIN%2F0QHZ%2FpmCTyNmiGIs4g%3D%3D&page_token=&thumbnail_size=SIZE_SMALL HTTP/1.1
|
||||
Host: api-pan.xunlei.com
|
||||
Connection: close
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjAzZjlmMzYxLWI3MjktNDVjNi04MjI0LTNiNWEwNmJmOGU0NSJ9.eyJpc3MiOiJodHRwczovL3hsdXNlci1zc2wueHVubGVpLmNvbSIsInN1YiI6IjEyMTk2MzY5NTIiLCJhdWQiOiJYcXAwa0pCWFdod2FUcEI2IiwiZXhwIjoxNzU1NzQxMDUxLCJpYXQiOjE3NTU2OTc4NTEsImF0X2hhc2giOiJyLmN0UWtJMUNmU1NpNkt2RFVUa0xidHciLCJzY29wZSI6InByb2ZpbGUgcGFuIHNzbyB1c2VyIiwicHJvamVjdF9pZCI6IjJydms0ZTNna2RubDd1MWtsMGsiLCJtZXRhIjp7ImEiOiJHOHNKVlJ6RjUzOE51eDRLOCt0VUtUR1hqdlZOTUczZlNrUEVaUWZLSWRBPSJ9fQ.ktWdGaShnGuU9CFj_awHaHfAoH0gkVBvdf4A24WPkRn-MUSpGRtUuo30VZay-wTbJ2UBEmw0pNtAeAfB97qzpPtVigDf9vD2WE9jIz61dASPxi6JIvioUfRzbgWa_Qj8a0bHc1Zvfufu9cSIzm0PPE7rSHYyq5oNfFxAAaY5k9cp2I4DclZon7hHJ0439knsu8MQEwQY42bRWppKqaI5q66Zlzeoc0t2I0Ehs2XYIyVyG2EYmFlbIWrpahudUTz6PbcrDMoHf8Y1hPnJn5ij7D32YSLXeP7ighQZDqaFoJHXJxqcHYToKut4JfQHCROkbqSwq3_I75k_A36gYtxVdA
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
sec-ch-ua-mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-captcha-token: ck0.heh7e1aWMkJ3z0Lz7aFmAWoI0-wDx6Os2o8d_ZF5nJEO_jk11h3FtCwOQG68zkXSaXe4T-dz5NSeT6xuqsy0RoNY5xzgT9J5b6WRAMeTsCqmb_DbntFifcvedEWK92zeFSGoNThN8MjawUXYsG8Y7C1mpVf4ftkLzjrToROm-ZJzUN18VIVur8F0U7SLKnaom-S63QLDXGcR3Qat0OVjoScqSZ9ESKOKAvd9GZ3ZRbKExXEpp16Rc_t3YHG3HFIJFsjQbn6OTsVyV4Ku1OXqJV-8iM8XVawlOxhBKrk8GtIok-UUI4CFXNjFWlrSHkQzN3GbEVPiDJaD7qoATZNUecCJig_oSWZsYKYLWiFgg2QEEzqI21C67LKygm8o1YXEQCVi2v-aMyojBnJwS3I9RsFqhU4u3ChszjyVFnQOMpCtXdLLmn2fGBYrsZP_dNcZlhZYw3yxEImvLsQ5s_5mzQ.ClMI9dP3v4wzEhBYcXAwa0pCWFdod2FUcEI2GgYxLjkyLjciDnBhbi54dW5sZWkuY29tKiBjMjRlY2FkYzQ0YzY0MzYzN2QxMjdmYjg0N2RiZTM2ZBKAAWU_LJemk9r1ThQlFBr75NH03TjR0K-PMY2-QIsEcET8mqJo4uN7iyjGfFeZmcxczsjMBbBpO_3XeYR3Wdgatr2yqImozJKh8Ek3j1718cLVywaQ79z8j_Lj85J1-JfLYPkfUW1q8uBi4Tjz8vs3uSSuOtO5Fy-OF6NyGoYtvxJF
|
||||
DNT: 1
|
||||
content-type: application/json
|
||||
x-client-id: Xqp0kJBXWhwaTpB6
|
||||
Accept: */*
|
||||
Origin: https://pan.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://pan.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9,ko-KR;q=0.8,ko;q=0.7
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 15:00:15 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 1912
|
||||
Connection: close
|
||||
Grpc-Metadata-Content-Type: application/grpc
|
||||
Access-Control-Allow-Origin: https://pan.xunlei.com
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Methods: PUT, POST, GET, DELETE, OPTIONS, PATCH
|
||||
Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,csrf-token,X-Captcha-Token,X-Device-Id,X-Guid,X-Project-Id,X-Client-Id,X-Peer-Id,X-User-Id,X-Request-From,X-Client-Version-Code,space-authorization
|
||||
Access-Control-Expose-Headers: csrf-token
|
||||
|
||||
{"share_status":"OK","share_status_text":"","file_num":"1","expiration_left":"-1","expiration_left_seconds":"-1","expiration_at":"-1","restore_count_left":"-1","files":[{"kind":"drive#folder","id":"VOY4UMZhqz1ZHO8_WNwF6V5JA1","parent_id":"VOMiZQDpN_rzJ8WNgSSCMExcA1","name":"金子般我的明星 금쪽같은 내스타 (2025)","user_id":"924119402","size":"0","revision":"5","file_extension":"","mime_type":"","starred":false,"web_content_link":"","created_time":"2025-08-20T11:19:51.349+08:00","modified_time":"2025-08-20T11:25:36.083+08:00","icon_link":"https://backstage-img-ssl.a.88cdn.com/019fc2a136a2881181e73fea74a4836efc02195d","thumbnail_link":"","md5_checksum":"","hash":"","links":{},"phase":"PHASE_TYPE_COMPLETE","audit":{"status":"STATUS_OK","message":"正常资源","title":""},"medias":[],"trashed":false,"delete_time":"","original_url":"","params":{"file_property_count":"2","file_property_size":"2345590740","platform_icon":"https://backstage-img-ssl.a.88cdn.com/05e4f2d4a751f1895746a15da2d391105418a66d","tags":"NEW"},"original_file_index":0,"space":"","apps":[],"writable":true,"folder_type":"NORMAL","collection":null,"sort_name":"金子般我的明星 금쪽같은 내스타 (0000002025)","user_modified_time":"2025-08-20T11:25:35.939+08:00","spell_name":[],"file_category":"OTHER","tags":[],"reference_events":[],"reference_resource":null}],"user_info":{"user_id":"924119402","portrait_url":"https://xfile2.a.88cdn.com/file/k/avatar/default","nickname":"什么都不知道","avatar":"https://xfile2.a.88cdn.com/file/k/avatar/default"},"next_page_token":"","pass_code_token":"GdrGFHvaZTbIUKxOiJKyrWZpcdGoHysJZBa5iv7N8mmsNElcU/3M8+Jkp1NO0cMKlIN/0QHZ/pmCTyNmiGIs4g==","title":"金子般我的明星 금쪽같은 내스타 (2025)","icon_link":"https://backstage-img-ssl.a.88cdn.com/019fc2a136a2881181e73fea74a4836efc02195d","thumbnail_link":"","contain_sensitive_resource_text":"","params":{}}
|
||||
|
||||
=========================
|
||||
|
||||
|
||||
POST /drive/v1/share/restore HTTP/1.1
|
||||
Host: api-pan.xunlei.com
|
||||
Connection: close
|
||||
Content-Length: 250
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjAzZjlmMzYxLWI3MjktNDVjNi04MjI0LTNiNWEwNmJmOGU0NSJ9.eyJpc3MiOiJodHRwczovL3hsdXNlci1zc2wueHVubGVpLmNvbSIsInN1YiI6IjEyMTk2MzY5NTIiLCJhdWQiOiJYcXAwa0pCWFdod2FUcEI2IiwiZXhwIjoxNzU1NzQxMDUxLCJpYXQiOjE3NTU2OTc4NTEsImF0X2hhc2giOiJyLmN0UWtJMUNmU1NpNkt2RFVUa0xidHciLCJzY29wZSI6InByb2ZpbGUgcGFuIHNzbyB1c2VyIiwicHJvamVjdF9pZCI6IjJydms0ZTNna2RubDd1MWtsMGsiLCJtZXRhIjp7ImEiOiJHOHNKVlJ6RjUzOE51eDRLOCt0VUtUR1hqdlZOTUczZlNrUEVaUWZLSWRBPSJ9fQ.ktWdGaShnGuU9CFj_awHaHfAoH0gkVBvdf4A24WPkRn-MUSpGRtUuo30VZay-wTbJ2UBEmw0pNtAeAfB97qzpPtVigDf9vD2WE9jIz61dASPxi6JIvioUfRzbgWa_Qj8a0bHc1Zvfufu9cSIzm0PPE7rSHYyq5oNfFxAAaY5k9cp2I4DclZon7hHJ0439knsu8MQEwQY42bRWppKqaI5q66Zlzeoc0t2I0Ehs2XYIyVyG2EYmFlbIWrpahudUTz6PbcrDMoHf8Y1hPnJn5ij7D32YSLXeP7ighQZDqaFoJHXJxqcHYToKut4JfQHCROkbqSwq3_I75k_A36gYtxVdA
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
sec-ch-ua-mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-captcha-token: ck0.heh7e1aWMkJ3z0Lz7aFmAWoI0-wDx6Os2o8d_ZF5nJEO_jk11h3FtCwOQG68zkXSaXe4T-dz5NSeT6xuqsy0RoNY5xzgT9J5b6WRAMeTsCqmb_DbntFifcvedEWK92zeFSGoNThN8MjawUXYsG8Y7C1mpVf4ftkLzjrToROm-ZJzUN18VIVur8F0U7SLKnaom-S63QLDXGcR3Qat0OVjoScqSZ9ESKOKAvd9GZ3ZRbKExXEpp16Rc_t3YHG3HFIJFsjQbn6OTsVyV4Ku1OXqJV-8iM8XVawlOxhBKrk8GtIok-UUI4CFXNjFWlrSHkQzN3GbEVPiDJaD7qoATZNUecCJig_oSWZsYKYLWiFgg2QEEzqI21C67LKygm8o1YXEQCVi2v-aMyojBnJwS3I9RsFqhU4u3ChszjyVFnQOMpCtXdLLmn2fGBYrsZP_dNcZlhZYw3yxEImvLsQ5s_5mzQ.ClMI9dP3v4wzEhBYcXAwa0pCWFdod2FUcEI2GgYxLjkyLjciDnBhbi54dW5sZWkuY29tKiBjMjRlY2FkYzQ0YzY0MzYzN2QxMjdmYjg0N2RiZTM2ZBKAAWU_LJemk9r1ThQlFBr75NH03TjR0K-PMY2-QIsEcET8mqJo4uN7iyjGfFeZmcxczsjMBbBpO_3XeYR3Wdgatr2yqImozJKh8Ek3j1718cLVywaQ79z8j_Lj85J1-JfLYPkfUW1q8uBi4Tjz8vs3uSSuOtO5Fy-OF6NyGoYtvxJF
|
||||
DNT: 1
|
||||
content-type: application/json
|
||||
x-client-id: Xqp0kJBXWhwaTpB6
|
||||
Accept: */*
|
||||
Origin: https://pan.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://pan.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9,ko-KR;q=0.8,ko;q=0.7
|
||||
|
||||
{"parent_id":"","share_id":"VOY4fDN-35yNfnqBJ3lSXfK4A1","pass_code_token":"GdrGFHvaZTbIUKxOiJKyrWZpcdGoHysJZBa5iv7N8mmsNElcU/3M8+Jkp1NO0cMKlIN/0QHZ/pmCTyNmiGIs4g==","ancestor_ids":[],"file_ids":["VOY4UMZhqz1ZHO8_WNwF6V5JA1"],"specify_parent_id":true}
|
||||
|
||||
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 15:02:59 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 149
|
||||
Connection: close
|
||||
Grpc-Metadata-Content-Type: application/grpc
|
||||
Access-Control-Allow-Origin: https://pan.xunlei.com
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Methods: PUT, POST, GET, DELETE, OPTIONS, PATCH
|
||||
Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,csrf-token,X-Captcha-Token,X-Device-Id,X-Guid,X-Project-Id,X-Client-Id,X-Peer-Id,X-User-Id,X-Request-From,X-Client-Version-Code,space-authorization
|
||||
Access-Control-Expose-Headers: csrf-token
|
||||
|
||||
{"share_status":"OK","share_status_text":"","file_id":"","restore_status":"RESTORE_START","restore_task_id":"VOY7-IPZkcoBobh3Az0dfyxRA1","params":{}}
|
||||
|
||||
==================
|
||||
|
||||
|
||||
|
||||
GET /drive/v1/tasks/VOY7-IPZkcoBobh3Az0dfyxRA1 HTTP/1.1
|
||||
Host: api-pan.xunlei.com
|
||||
Connection: close
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjAzZjlmMzYxLWI3MjktNDVjNi04MjI0LTNiNWEwNmJmOGU0NSJ9.eyJpc3MiOiJodHRwczovL3hsdXNlci1zc2wueHVubGVpLmNvbSIsInN1YiI6IjEyMTk2MzY5NTIiLCJhdWQiOiJYcXAwa0pCWFdod2FUcEI2IiwiZXhwIjoxNzU1NzQxMDUxLCJpYXQiOjE3NTU2OTc4NTEsImF0X2hhc2giOiJyLmN0UWtJMUNmU1NpNkt2RFVUa0xidHciLCJzY29wZSI6InByb2ZpbGUgcGFuIHNzbyB1c2VyIiwicHJvamVjdF9pZCI6IjJydms0ZTNna2RubDd1MWtsMGsiLCJtZXRhIjp7ImEiOiJHOHNKVlJ6RjUzOE51eDRLOCt0VUtUR1hqdlZOTUczZlNrUEVaUWZLSWRBPSJ9fQ.ktWdGaShnGuU9CFj_awHaHfAoH0gkVBvdf4A24WPkRn-MUSpGRtUuo30VZay-wTbJ2UBEmw0pNtAeAfB97qzpPtVigDf9vD2WE9jIz61dASPxi6JIvioUfRzbgWa_Qj8a0bHc1Zvfufu9cSIzm0PPE7rSHYyq5oNfFxAAaY5k9cp2I4DclZon7hHJ0439knsu8MQEwQY42bRWppKqaI5q66Zlzeoc0t2I0Ehs2XYIyVyG2EYmFlbIWrpahudUTz6PbcrDMoHf8Y1hPnJn5ij7D32YSLXeP7ighQZDqaFoJHXJxqcHYToKut4JfQHCROkbqSwq3_I75k_A36gYtxVdA
|
||||
x-device-id: c24ecadc44c643637d127fb847dbe36d
|
||||
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
|
||||
sec-ch-ua-mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||
x-captcha-token: ck0.heh7e1aWMkJ3z0Lz7aFmAWoI0-wDx6Os2o8d_ZF5nJEO_jk11h3FtCwOQG68zkXSaXe4T-dz5NSeT6xuqsy0RoNY5xzgT9J5b6WRAMeTsCqmb_DbntFifcvedEWK92zeFSGoNThN8MjawUXYsG8Y7C1mpVf4ftkLzjrToROm-ZJzUN18VIVur8F0U7SLKnaom-S63QLDXGcR3Qat0OVjoScqSZ9ESKOKAvd9GZ3ZRbKExXEpp16Rc_t3YHG3HFIJFsjQbn6OTsVyV4Ku1OXqJV-8iM8XVawlOxhBKrk8GtIok-UUI4CFXNjFWlrSHkQzN3GbEVPiDJaD7qoATZNUecCJig_oSWZsYKYLWiFgg2QEEzqI21C67LKygm8o1YXEQCVi2v-aMyojBnJwS3I9RsFqhU4u3ChszjyVFnQOMpCtXdLLmn2fGBYrsZP_dNcZlhZYw3yxEImvLsQ5s_5mzQ.ClMI9dP3v4wzEhBYcXAwa0pCWFdod2FUcEI2GgYxLjkyLjciDnBhbi54dW5sZWkuY29tKiBjMjRlY2FkYzQ0YzY0MzYzN2QxMjdmYjg0N2RiZTM2ZBKAAWU_LJemk9r1ThQlFBr75NH03TjR0K-PMY2-QIsEcET8mqJo4uN7iyjGfFeZmcxczsjMBbBpO_3XeYR3Wdgatr2yqImozJKh8Ek3j1718cLVywaQ79z8j_Lj85J1-JfLYPkfUW1q8uBi4Tjz8vs3uSSuOtO5Fy-OF6NyGoYtvxJF
|
||||
DNT: 1
|
||||
content-type: application/json
|
||||
x-client-id: Xqp0kJBXWhwaTpB6
|
||||
Accept: */*
|
||||
Origin: https://pan.xunlei.com
|
||||
Sec-Fetch-Site: same-site
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://pan.xunlei.com/
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9,ko-KR;q=0.8,ko;q=0.7
|
||||
|
||||
|
||||
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Date: Wed, 20 Aug 2025 15:03:01 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 745
|
||||
Connection: close
|
||||
Grpc-Metadata-Content-Type: application/grpc
|
||||
Access-Control-Allow-Origin: https://pan.xunlei.com
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Methods: PUT, POST, GET, DELETE, OPTIONS, PATCH
|
||||
Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,csrf-token,X-Captcha-Token,X-Device-Id,X-Guid,X-Project-Id,X-Client-Id,X-Peer-Id,X-User-Id,X-Request-From,X-Client-Version-Code,space-authorization
|
||||
Access-Control-Expose-Headers: csrf-token
|
||||
|
||||
{"kind":"drive#task","id":"VOY7-IPZkcoBobh3Az0dfyxRA1","name":"restore","type":"restore","user_id":"1219636952","statuses":[],"status_size":0,"params":{"notify_restore_reward":"VOY7-IcLzcXgdt9SPIA0Naa-A1","notify_restore_skin":"VOY7-Ic2zcXgdt9SPIA0Na_kA1","share_id":"VOY4fDN-35yNfnqBJ3lSXfK4A1","trace_file_ids":"{\"VOY4UMZhqz1ZHO8_WNwF6V5JA1\":\"VOY7-IXUzcXgdt9SPIA0NaWuA1\"}"},"file_id":"","file_name":"","file_size":"0","message":"完成","created_time":"2025-08-20T23:02:59.492+08:00","updated_time":"2025-08-20T23:03:00.376+08:00","third_task_id":"","phase":"PHASE_TYPE_COMPLETE","progress":100,"icon_link":"https://backstage-img-ssl.a.88cdn.com/05e4f2d4a751f1895746a15da2d391105418a66d","callback":"","reference_resource":null,"space":""}
|
||||
|
||||
|
||||
|
||||
================================
|
||||
39
common/xunlei_credentials.go
Normal file
39
common/xunlei_credentials.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package pan
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// XunleiAccountCredentials 迅雷账号凭据结构
|
||||
type XunleiAccountCredentials struct {
|
||||
Username string `json:"username"` // 手机号(不包含+86前缀)
|
||||
Password string `json:"password"` // 密码
|
||||
RefreshToken string `json:"refresh_token"` // 当前有效的refresh_token
|
||||
}
|
||||
|
||||
// ParseCredentialsFromCk 从ck字段解析账号凭据
|
||||
func ParseCredentialsFromCk(ck string) (*XunleiAccountCredentials, error) {
|
||||
var credentials XunleiAccountCredentials
|
||||
if err := json.Unmarshal([]byte(ck), &credentials); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &credentials, nil
|
||||
}
|
||||
|
||||
// IsAccountCredentials 检查ck是否包含账号密码信息
|
||||
func IsAccountCredentials(ck string) bool {
|
||||
var credentials map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(ck), &credentials); err != nil {
|
||||
return false
|
||||
}
|
||||
_, hasUsername := credentials["username"]
|
||||
_, hasPassword := credentials["password"]
|
||||
return hasUsername && hasPassword
|
||||
}
|
||||
|
||||
// ToJsonString 转换为JSON字符串
|
||||
func (c *XunleiAccountCredentials) ToJsonString() (string, error) {
|
||||
data, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
232
common/xunlei_login.go
Normal file
232
common/xunlei_login.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package pan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 新增常量定义
|
||||
const (
|
||||
XLUSER_CLIENT_ID = "XW5SkOhLDjnOZP7J" // 登录
|
||||
PAN_CLIENT_ID = "Xqp0kJBXWhwaTpB6" // 获取文件列表
|
||||
CLIENT_SECRET = "Og9Vr1L8Ee6bh0olFxFDRg"
|
||||
CLIENT_VERSION = "1.92.9" // 更新为与xunlei_3项目相同的版本
|
||||
PACKAG_ENAME = "pan.xunlei.com"
|
||||
)
|
||||
|
||||
var SALTS = []string{
|
||||
"QG3/GhopO+5+T",
|
||||
"1Sv94+ANND3lDmmw",
|
||||
"q2eTxRva8b3B5d",
|
||||
"m2",
|
||||
"VIc5CZRBMU71ENfbOh0+RgWIuzLy",
|
||||
"66M8Wpw6nkBEekOtL6e",
|
||||
"N0rucK7S8W/vrRkfPto5urIJJS8dVY0S",
|
||||
"oLAR7pdUVUAp9xcuHWzrU057aUhdCJrt",
|
||||
"6lxcykBSsfI//GR9",
|
||||
"r50cz+1I4gbU/fk8",
|
||||
"tdwzrTc4SNFC4marNGTgf05flC85A",
|
||||
"qvNVUDFjfsOMqvdi2gB8gCvtaJAIqxXs",
|
||||
}
|
||||
|
||||
// captchaSign 生成验证码签名 - 完全复制自xunlei_3项目
|
||||
func (x *XunleiPanService) captchaSign(clientId string, deviceID string, timestamp string) string {
|
||||
sign := clientId + CLIENT_VERSION + PACKAG_ENAME + deviceID + timestamp
|
||||
log.Printf("urldb 签名基础字符串: %s", sign)
|
||||
for _, salt := range SALTS { // salt =
|
||||
hash := md5.Sum([]byte(sign + salt))
|
||||
sign = hex.EncodeToString(hash[:])
|
||||
}
|
||||
log.Printf("urldb 最终签名: 1.%s", sign)
|
||||
return fmt.Sprintf("1.%s", sign)
|
||||
}
|
||||
|
||||
// getTimestamp 获取当前时间戳
|
||||
func (x *XunleiPanService) getTimestamp() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// LoginWithCredentials 使用账号密码登录
|
||||
func (x *XunleiPanService) LoginWithCredentials(username, password string) (XunleiTokenData, error) {
|
||||
loginURL := "https://xluser-ssl.xunlei.com/v1/auth/signin"
|
||||
|
||||
// 初始化验证码 - 完全模仿xunlei_3的CaptchaInit方法
|
||||
captchaURL := "https://xluser-ssl.xunlei.com/v1/shield/captcha/init"
|
||||
|
||||
// 构造meta参数(完全模仿xunlei_3,只包含phone_number)
|
||||
meta := map[string]interface{}{
|
||||
"phone_number": "+86" + username,
|
||||
}
|
||||
|
||||
// 构造验证码请求(完全模仿xunlei_3)
|
||||
captchaBody := map[string]interface{}{
|
||||
"client_id": XLUSER_CLIENT_ID,
|
||||
"action": "POST:/v1/auth/signin",
|
||||
"device_id": x.deviceId,
|
||||
"meta": meta,
|
||||
}
|
||||
|
||||
log.Printf("发送验证码初始化请求: %+v", captchaBody)
|
||||
resp, err := x.sendCaptchaRequest(captchaURL, captchaBody)
|
||||
if err != nil {
|
||||
return XunleiTokenData{}, fmt.Errorf("获取验证码失败: %v", err)
|
||||
}
|
||||
|
||||
if resp["captcha_token"] == nil {
|
||||
return XunleiTokenData{}, fmt.Errorf("获取验证码失败: 响应中没有captcha_token")
|
||||
}
|
||||
|
||||
captchaToken, ok := resp["captcha_token"].(string)
|
||||
if !ok {
|
||||
return XunleiTokenData{}, fmt.Errorf("获取验证码失败: captcha_token格式错误")
|
||||
}
|
||||
log.Printf("成功获取captcha_token: %s", captchaToken)
|
||||
|
||||
// 构造登录请求数据
|
||||
loginData := map[string]interface{}{
|
||||
"client_id": XLUSER_CLIENT_ID,
|
||||
"client_secret": CLIENT_SECRET,
|
||||
"password": password,
|
||||
"username": "+86 " + username,
|
||||
"captcha_token": captchaToken,
|
||||
}
|
||||
|
||||
// 发送登录请求
|
||||
userInfo, err := x.sendCaptchaRequest(loginURL, loginData)
|
||||
if err != nil {
|
||||
return XunleiTokenData{}, fmt.Errorf("登录请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 提取token信息
|
||||
accessToken, ok := userInfo["access_token"].(string)
|
||||
if !ok {
|
||||
return XunleiTokenData{}, fmt.Errorf("登录响应中没有access_token")
|
||||
}
|
||||
|
||||
refreshToken, ok := userInfo["refresh_token"].(string)
|
||||
if !ok {
|
||||
return XunleiTokenData{}, fmt.Errorf("登录响应中没有refresh_token")
|
||||
}
|
||||
|
||||
sub, ok := userInfo["sub"].(string)
|
||||
if !ok {
|
||||
sub = ""
|
||||
}
|
||||
|
||||
// 计算过期时间
|
||||
expiresIn := int64(3600) // 默认1小时
|
||||
if exp, ok := userInfo["expires_in"].(float64); ok {
|
||||
expiresIn = int64(exp)
|
||||
}
|
||||
expiresAt := time.Now().Unix() + expiresIn - 60 // 减去60秒缓冲
|
||||
|
||||
log.Printf("登录成功,获取到token")
|
||||
return XunleiTokenData{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: expiresIn,
|
||||
ExpiresAt: expiresAt,
|
||||
Sub: sub,
|
||||
TokenType: "Bearer",
|
||||
UserId: sub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sendCaptchaRequest 发送验证码请求 - 完全复制xunlei_3的sendRequest实现
|
||||
func (x *XunleiPanService) sendCaptchaRequest(url string, data map[string]interface{}) (map[string]interface{}, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("发送验证码请求URL: %s", url)
|
||||
log.Printf("发送验证码请求数据: %s", string(jsonData))
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 完全复制xunlei_3的请求头设置
|
||||
reqHeaders := x.getHeadersForRequest(nil)
|
||||
// 添加特定的headers
|
||||
reqHeaders["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
reqHeaders["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"
|
||||
|
||||
for k, v := range reqHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
// 根据URL确定使用哪个client_id
|
||||
if strings.Contains(url, "shield/captcha/init") {
|
||||
// 对于验证码初始化,如果数据中指定了client_id,则使用该client_id
|
||||
if clientID, ok := data["client_id"].(string); ok {
|
||||
req.Header.Set("X-Client-Id", clientID)
|
||||
} else {
|
||||
// 默认使用PAN_CLIENT_ID用于API相关的验证码
|
||||
req.Header.Set("X-Client-Id", PAN_CLIENT_ID)
|
||||
}
|
||||
} else if strings.Contains(url, "auth/") {
|
||||
// 对于认证相关的请求,使用登录相关的client_id
|
||||
req.Header.Set("X-Client-Id", XLUSER_CLIENT_ID)
|
||||
} else {
|
||||
// 对于一般的API请求,使用PAN_CLIENT_ID
|
||||
req.Header.Set("X-Client-Id", PAN_CLIENT_ID)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("验证码响应状态码: %d", resp.StatusCode)
|
||||
log.Printf("验证码响应内容: %s", string(body))
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("JSON 解析失败: %v, raw: %s", err, string(body))
|
||||
}
|
||||
|
||||
log.Printf("解析后的响应: %+v", result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getHeadersForRequest 获取请求头
|
||||
func (x *XunleiPanService) getHeadersForRequest(accessToken *string) map[string]string {
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
|
||||
// 这里我们简化处理,因为验证码请求不需要这些
|
||||
// if x.CaptchaToken != nil {
|
||||
// headers["User-Agent"] = x.buildCustomUserAgent()
|
||||
// headers["X-Captcha-Token"] = *x.CaptchaToken
|
||||
// } else {
|
||||
headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
||||
// }
|
||||
|
||||
// if accessToken != nil {
|
||||
// headers["Authorization"] = fmt.Sprintf("Bearer %s", *accessToken)
|
||||
// }
|
||||
|
||||
// if x.DeviceID != "" {
|
||||
// headers["X-Device-Id"] = x.DeviceID
|
||||
// }
|
||||
|
||||
return headers
|
||||
}
|
||||
897
common/xunlei_pan.bak
Normal file
897
common/xunlei_pan.bak
Normal file
@@ -0,0 +1,897 @@
|
||||
package pan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
)
|
||||
|
||||
// CaptchaData 存储在数据库中的验证码令牌数据
|
||||
type CaptchaData struct {
|
||||
CaptchaToken string `json:"captcha_token"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
// XunleiExtraData 所有额外数据的容器
|
||||
type XunleiTokenData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Sub string `json:"sub"`
|
||||
TokenType string `json:"token_type"`
|
||||
UserId string `json:"user_id"`
|
||||
}
|
||||
|
||||
type XunleiExtraData struct {
|
||||
Captcha *CaptchaData
|
||||
Token *XunleiTokenData
|
||||
}
|
||||
|
||||
type XunleiPanService struct {
|
||||
*BasePanService
|
||||
configMutex sync.RWMutex
|
||||
clientId string
|
||||
deviceId string
|
||||
entity entity.Cks
|
||||
cksRepo repo.CksRepository
|
||||
extra XunleiExtraData // 需要保存到数据库的token信息
|
||||
}
|
||||
|
||||
// 配置化 API Host
|
||||
func (x *XunleiPanService) apiHost(apiType string) string {
|
||||
if apiType == "user" {
|
||||
return "https://xluser-ssl.xunlei.com"
|
||||
}
|
||||
return "https://api-pan.xunlei.com"
|
||||
}
|
||||
|
||||
func (x *XunleiPanService) setCommonHeader(req *http.Request) {
|
||||
for k, v := range x.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// NewXunleiPanService 创建迅雷网盘服务
|
||||
func NewXunleiPanService(config *PanConfig) *XunleiPanService {
|
||||
xunleiInstance := &XunleiPanService{
|
||||
BasePanService: NewBasePanService(config),
|
||||
clientId: "Xqp0kJBXWhwaTpB6",
|
||||
deviceId: "925b7631473a13716b791d7f28289cad",
|
||||
extra: XunleiExtraData{}, // Initialize extra with zero values
|
||||
}
|
||||
xunleiInstance.SetHeaders(map[string]string{
|
||||
"Accept": "*/;",
|
||||
"Accept-Encoding": "deflate",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://pan.xunlei.com",
|
||||
"Pragma": "no-cache",
|
||||
"Priority": "u=1,i",
|
||||
"Referer": "https://pan.xunlei.com/",
|
||||
"sec-ch-ua": `"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"`,
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": `"Windows"`,
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-site",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
||||
"Authorization": "",
|
||||
"x-captcha-token": "",
|
||||
"x-client-id": xunleiInstance.clientId,
|
||||
"x-device-id": xunleiInstance.deviceId,
|
||||
})
|
||||
|
||||
xunleiInstance.UpdateConfig(config)
|
||||
return xunleiInstance
|
||||
}
|
||||
|
||||
// SetCKSRepository 设置 CksRepository 和 entity
|
||||
func (x *XunleiPanService) SetCKSRepository(cksRepo repo.CksRepository, entity entity.Cks) {
|
||||
x.cksRepo = cksRepo
|
||||
x.entity = entity
|
||||
var extra XunleiExtraData
|
||||
if err := json.Unmarshal([]byte(x.entity.Extra), &extra); err != nil {
|
||||
log.Printf("解析 extra 数据失败: %v,使用空数据", err)
|
||||
}
|
||||
x.extra = extra
|
||||
}
|
||||
|
||||
// GetXunleiInstance 获取迅雷网盘服务单例实例
|
||||
func GetXunleiInstance() *XunleiPanService {
|
||||
return NewXunleiPanService(nil)
|
||||
}
|
||||
|
||||
func (x *XunleiPanService) GetAccessTokenByRefreshToken(refreshToken string) (XunleiTokenData, error) {
|
||||
// 构造请求体
|
||||
body := map[string]interface{}{
|
||||
"client_id": x.clientId,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refreshToken,
|
||||
}
|
||||
|
||||
// 过滤 headers(移除 Authorization 和 x-captcha-token)
|
||||
filteredHeaders := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
if k != "Authorization" && k != "x-captcha-token" {
|
||||
filteredHeaders[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 API 获取新的 token
|
||||
resp, err := x.requestXunleiApi("https://xluser-ssl.xunlei.com/v1/auth/token", "POST", body, nil, filteredHeaders)
|
||||
if err != nil {
|
||||
return XunleiTokenData{}, fmt.Errorf("获取 access_token 请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 正确做法:用 exists 判断
|
||||
if _, exists := resp["access_token"]; exists {
|
||||
// 会输出,即使值为 nil
|
||||
} else {
|
||||
return XunleiTokenData{}, fmt.Errorf("获取 access_token 请求失败: %v 不存在", "access_token")
|
||||
}
|
||||
|
||||
// 计算过期时间(当前时间 + expires_in - 60 秒缓冲)
|
||||
currentTime := time.Now().Unix()
|
||||
expiresAt := currentTime + int64(resp["expires_in"].(float64)) - 60
|
||||
resp["expires_at"] = expiresAt
|
||||
jsonBytes, _ := json.Marshal(resp)
|
||||
|
||||
var result XunleiTokenData
|
||||
json.Unmarshal(jsonBytes, &result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getAccessToken 获取 Access Token(内部包含缓存判断、刷新、保存)- 匹配 PHP 版本
|
||||
func (x *XunleiPanService) getAccessToken() (string, error) {
|
||||
// 检查 Access Token 是否有效
|
||||
currentTime := time.Now().Unix()
|
||||
if x.extra.Token != nil && x.extra.Token.AccessToken != "" && x.extra.Token.ExpiresAt > currentTime {
|
||||
return x.extra.Token.AccessToken, nil
|
||||
}
|
||||
newData, err := x.GetAccessTokenByRefreshToken(x.extra.Token.RefreshToken)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取 access_token 失败: %v", err)
|
||||
}
|
||||
|
||||
x.extra.Token.AccessToken = newData.AccessToken
|
||||
x.extra.Token.ExpiresAt = newData.ExpiresAt
|
||||
|
||||
// 保存到数据库
|
||||
extraBytes, err := json.Marshal(x.extra)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化 extra 数据失败: %v", err)
|
||||
}
|
||||
x.entity.Extra = string(extraBytes)
|
||||
if err := x.cksRepo.UpdateWithAllFields(&x.entity); err != nil {
|
||||
return "", fmt.Errorf("保存 access_token 到数据库失败: %v", err)
|
||||
}
|
||||
return newData.AccessToken, nil
|
||||
}
|
||||
|
||||
// getCaptchaToken 获取 captcha_token - 匹配 PHP 版本
|
||||
func (x *XunleiPanService) getCaptchaToken() (string, error) {
|
||||
// 检查 Captcha Token 是否有效
|
||||
currentTime := time.Now().Unix()
|
||||
if x.extra.Captcha != nil && x.extra.Captcha.CaptchaToken != "" && x.extra.Captcha.ExpiresAt > currentTime {
|
||||
return x.extra.Captcha.CaptchaToken, nil
|
||||
}
|
||||
|
||||
// 构造请求体
|
||||
body := map[string]interface{}{
|
||||
"client_id": x.clientId,
|
||||
"action": "get:/drive/v1/share",
|
||||
"device_id": x.deviceId,
|
||||
"meta": map[string]interface{}{
|
||||
"username": "",
|
||||
"phone_number": "",
|
||||
"email": "",
|
||||
"package_name": "pan.xunlei.com",
|
||||
"client_version": "1.45.0",
|
||||
"captcha_sign": "1.fe2108ad808a74c9ac0243309242726c",
|
||||
"timestamp": "1645241033384",
|
||||
"user_id": "0",
|
||||
},
|
||||
}
|
||||
|
||||
captchaHeaders := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"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",
|
||||
}
|
||||
|
||||
// 调用 API 获取 captcha_token
|
||||
resp, err := x.requestXunleiApi("https://xluser-ssl.xunlei.com/v1/shield/captcha/init", "POST", body, nil, captchaHeaders)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取 captcha_token 请求失败: %v", err)
|
||||
}
|
||||
|
||||
if resp["captcha_token"] != nil && resp["captcha_token"] != "" {
|
||||
//
|
||||
} else {
|
||||
return "", fmt.Errorf("获取 captcha_token 失败: %v", resp)
|
||||
}
|
||||
|
||||
// 计算过期时间(当前时间 + expires_in - 10 秒缓冲)
|
||||
expiresAt := currentTime + int64(resp["expires_in"].(float64)) - 10
|
||||
|
||||
// 更新 extra 数据
|
||||
if x.extra.Captcha == nil {
|
||||
x.extra.Captcha = &CaptchaData{}
|
||||
}
|
||||
x.extra.Captcha.CaptchaToken = resp["captcha_token"].(string)
|
||||
x.extra.Captcha.ExpiresAt = expiresAt
|
||||
|
||||
// 保存到数据库
|
||||
extraBytes, err := json.Marshal(x.extra)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化 extra 数据失败: %v", err)
|
||||
}
|
||||
x.entity.Extra = string(extraBytes)
|
||||
if err := x.cksRepo.UpdateWithAllFields(&x.entity); err != nil {
|
||||
return "", fmt.Errorf("保存 captcha_token 到数据库失败: %v", err)
|
||||
}
|
||||
|
||||
return resp["captcha_token"].(string), nil
|
||||
}
|
||||
|
||||
// requestXunleiApi 迅雷 API 通用请求方法 - 使用 BasePanService 方法
|
||||
func (x *XunleiPanService) requestXunleiApi(url string, method string, data map[string]interface{}, queryParams map[string]string, headers map[string]string) (map[string]interface{}, error) {
|
||||
var respData []byte
|
||||
var err error
|
||||
|
||||
// 先更新当前请求的 headers
|
||||
originalHeaders := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
originalHeaders[k] = v
|
||||
}
|
||||
|
||||
// 临时设置请求的 headers
|
||||
for k, v := range headers {
|
||||
x.SetHeader(k, v)
|
||||
}
|
||||
defer func() {
|
||||
// 恢复原始 headers
|
||||
for k, v := range originalHeaders {
|
||||
x.SetHeader(k, v)
|
||||
}
|
||||
}()
|
||||
|
||||
// 根据方法调用相应的 BasePanService 方法
|
||||
if method == "GET" {
|
||||
respData, err = x.HTTPGet(url, queryParams)
|
||||
} else if method == "POST" {
|
||||
respData, err = x.HTTPPost(url, data, queryParams)
|
||||
} else {
|
||||
return nil, fmt.Errorf("不支持的HTTP方法: %s", method)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(respData, &result); err != nil {
|
||||
return nil, fmt.Errorf("JSON 解析失败: %v, raw: %s", err, string(respData))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (x *XunleiPanService) UpdateConfig(config *PanConfig) {
|
||||
if config == nil {
|
||||
return
|
||||
}
|
||||
x.configMutex.Lock()
|
||||
defer x.configMutex.Unlock()
|
||||
x.config = config
|
||||
if config.Cookie != "" {
|
||||
x.SetHeader("Cookie", config.Cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// GetServiceType 获取服务类型
|
||||
func (x *XunleiPanService) GetServiceType() ServiceType {
|
||||
return Xunlei
|
||||
}
|
||||
|
||||
func extractCode(url string) string {
|
||||
// 查找 pwd= 的位置
|
||||
if pwdIndex := strings.Index(url, "pwd="); pwdIndex != -1 {
|
||||
code := url[pwdIndex+4:]
|
||||
// 移除 # 及后面的内容(如果存在)
|
||||
if hashIndex := strings.Index(code, "#"); hashIndex != -1 {
|
||||
code = code[:hashIndex]
|
||||
}
|
||||
return code
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Transfer 转存分享链接 - 实现 PanService 接口,匹配 XunleiPan.php 的逻辑
|
||||
func (x *XunleiPanService) Transfer(shareID string) (*TransferResult, error) {
|
||||
// 读取配置(线程安全)
|
||||
x.configMutex.RLock()
|
||||
config := x.config
|
||||
x.configMutex.RUnlock()
|
||||
|
||||
log.Printf("开始处理迅雷分享: %s", shareID)
|
||||
|
||||
// 1️⃣ 获取 AccessToken 和 CaptchaToken
|
||||
accessToken, err := x.getAccessToken()
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("获取accessToken失败: %v", err)), nil
|
||||
}
|
||||
|
||||
captchaToken, err := x.getCaptchaToken()
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("获取captchaToken失败: %v", err)), nil
|
||||
}
|
||||
|
||||
// 转存模式:实现完整的转存流程
|
||||
thisCode := extractCode(config.URL)
|
||||
|
||||
// 获取分享详情
|
||||
shareDetail, err := x.getShare(shareID, thisCode, accessToken, captchaToken)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("获取分享详情失败: %v", err)), nil
|
||||
}
|
||||
if shareDetail["share_status"].(string) != "OK" {
|
||||
return ErrorResult(fmt.Sprintf("获取分享详情失败: %v", "分享状态异常")), nil
|
||||
}
|
||||
if shareDetail["file_num"].(string) == "0" {
|
||||
return ErrorResult(fmt.Sprintf("获取分享详情失败: %v", "文件列表为空")), nil
|
||||
}
|
||||
|
||||
parent_id := "" // 默认存储路径
|
||||
|
||||
// 检查是否为检验模式
|
||||
if config.IsType == 1 {
|
||||
// 检验模式:直接获取分享信息
|
||||
urls := map[string]interface{}{
|
||||
"title": shareDetail["title"],
|
||||
"share_url": config.URL,
|
||||
"stoken": "",
|
||||
}
|
||||
return SuccessResult("检验成功", urls), nil
|
||||
}
|
||||
|
||||
// files := shareDetail["files"].([]interface{})
|
||||
// fileIDs := make([]string, 0)
|
||||
// for _, file := range files {
|
||||
// fileMap := file.(map[string]interface{})
|
||||
// if fid, ok := fileMap["id"].(string); ok {
|
||||
// fileIDs = append(fileIDs, fid)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 处理广告过滤(这里简化处理)
|
||||
// TODO: 添加广告文件过滤逻辑
|
||||
|
||||
// 转存资源
|
||||
restoreResult, err := x.getRestore(shareID, shareDetail, accessToken, captchaToken, parent_id)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("转存失败: %v", err)), nil
|
||||
}
|
||||
|
||||
// 获取转存任务信息
|
||||
taskID := restoreResult["restore_task_id"].(string)
|
||||
|
||||
// 等待转存完成
|
||||
taskResp, err := x.waitForTask(taskID, accessToken, captchaToken)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("等待转存完成失败: %v", err)), nil
|
||||
}
|
||||
|
||||
// 获取任务结果以获取文件ID
|
||||
existingFileIds := make([]string, 0)
|
||||
if params, ok2 := taskResp["params"].(map[string]interface{}); ok2 {
|
||||
if traceIds, ok3 := params["trace_file_ids"].(string); ok3 {
|
||||
traceData := make(map[string]interface{})
|
||||
json.Unmarshal([]byte(traceIds), &traceData)
|
||||
for _, fid := range traceData {
|
||||
existingFileIds = append(existingFileIds, fid.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建分享链接
|
||||
expirationDays := "-1"
|
||||
if config.ExpiredType == 2 {
|
||||
expirationDays = "2"
|
||||
}
|
||||
|
||||
// 根据share_id获取到分享链接
|
||||
shareResult, err := x.getSharePassword(existingFileIds, accessToken, captchaToken, expirationDays)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("创建分享链接失败: %v", err)), nil
|
||||
}
|
||||
|
||||
var fid string
|
||||
if len(existingFileIds) > 1 {
|
||||
fid = strings.Join(existingFileIds, ",")
|
||||
} else {
|
||||
fid = existingFileIds[0]
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"title": "",
|
||||
"shareUrl": shareResult["share_url"].(string) + "?pwd=" + shareResult["pass_code"].(string),
|
||||
"code": shareResult["pass_code"].(string),
|
||||
"fid": fid,
|
||||
}
|
||||
|
||||
return SuccessResult("转存成功", result), nil
|
||||
}
|
||||
|
||||
// waitForTask 等待任务完成 - 使用 HTTPGet 方法
|
||||
func (x *XunleiPanService) waitForTask(taskID string, accessToken, captchaToken string) (map[string]interface{}, error) {
|
||||
maxRetries := 50
|
||||
retryDelay := 2 * time.Second
|
||||
|
||||
for retryIndex := 0; retryIndex < maxRetries; retryIndex++ {
|
||||
result, err := x.getTaskStatus(taskID, retryIndex, accessToken, captchaToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if int64(result["progress"].(float64)) == 100 { // 任务完成
|
||||
return result, nil
|
||||
}
|
||||
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("任务超时")
|
||||
}
|
||||
|
||||
// getTaskStatus 获取任务状态 - 使用 HTTPGet 方法
|
||||
func (x *XunleiPanService) getTaskStatus(taskID string, retryIndex int, accessToken, captchaToken string) (map[string]interface{}, error) {
|
||||
apiURL := x.apiHost("") + "/drive/v1/tasks/" + taskID
|
||||
queryParams := map[string]string{}
|
||||
|
||||
// 设置 request 所需的 headers
|
||||
headers := map[string]string{
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
"x-captcha-token": captchaToken,
|
||||
}
|
||||
|
||||
resp, err := x.requestXunleiApi(apiURL, "GET", nil, queryParams, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByEntity 根据 entity.Cks 获取用户信息(待实现)
|
||||
func (x *XunleiPanService) GetUserInfoByEntity(cks entity.Cks) (*UserInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getShare 获取分享详情 - 匹配 PHP 版本
|
||||
func (x *XunleiPanService) getShare(shareID, passCode, accessToken, captchaToken string) (map[string]interface{}, error) {
|
||||
// 设置 headers
|
||||
headers := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
headers[k] = v
|
||||
}
|
||||
headers["Authorization"] = "Bearer " + accessToken
|
||||
headers["x-captcha-token"] = captchaToken
|
||||
|
||||
queryParams := map[string]string{
|
||||
"share_id": shareID,
|
||||
"pass_code": passCode,
|
||||
"limit": "100",
|
||||
"pass_code_token": "",
|
||||
"page_token": "",
|
||||
"thumbnail_size": "SIZE_SMALL",
|
||||
}
|
||||
|
||||
return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/share", "GET", nil, queryParams, headers)
|
||||
}
|
||||
|
||||
// getRestore 转存到网盘 - 匹配 PHP 版本
|
||||
func (x *XunleiPanService) getRestore(shareID string, infoData map[string]interface{}, accessToken, captchaToken, parentID string) (map[string]interface{}, error) {
|
||||
ids := make([]string, 0)
|
||||
if files, ok := infoData["files"].([]interface{}); ok {
|
||||
for _, file := range files {
|
||||
if fileMap, ok2 := file.(map[string]interface{}); ok2 {
|
||||
if id, ok3 := fileMap["id"].(string); ok3 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
passCodeToken := ""
|
||||
if token, ok := infoData["pass_code_token"]; ok {
|
||||
if tokenStr, ok2 := token.(string); ok2 {
|
||||
passCodeToken = tokenStr
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"parent_id": parentID,
|
||||
"share_id": shareID,
|
||||
"pass_code_token": passCodeToken,
|
||||
"ancestor_ids": []string{},
|
||||
"specify_parent_id": true,
|
||||
"file_ids": ids,
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
headers[k] = v
|
||||
}
|
||||
headers["Authorization"] = "Bearer " + accessToken
|
||||
headers["x-captcha-token"] = captchaToken
|
||||
|
||||
return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/share/restore", "POST", data, nil, headers)
|
||||
}
|
||||
|
||||
// getTasks 获取转存任务状态 - 匹配 PHP 版本
|
||||
func (x *XunleiPanService) getTasks(taskID, accessToken, captchaToken string) (map[string]interface{}, error) {
|
||||
headers := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
headers[k] = v
|
||||
}
|
||||
headers["Authorization"] = "Bearer " + accessToken
|
||||
headers["x-captcha-token"] = captchaToken
|
||||
|
||||
return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/tasks/"+taskID, "GET", nil, nil, headers)
|
||||
}
|
||||
|
||||
// getSharePassword 创建分享链接 - 匹配 PHP 版本
|
||||
func (x *XunleiPanService) getSharePassword(fileIDs []string, accessToken, captchaToken, expirationDays string) (map[string]interface{}, error) {
|
||||
data := map[string]interface{}{
|
||||
"file_ids": fileIDs,
|
||||
"share_to": "copy",
|
||||
"params": map[string]interface{}{
|
||||
"subscribe_push": "false",
|
||||
"WithPassCodeInLink": "true",
|
||||
},
|
||||
"title": "云盘资源分享",
|
||||
"restore_limit": "-1",
|
||||
"expiration_days": expirationDays,
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
headers[k] = v
|
||||
}
|
||||
headers["Authorization"] = "Bearer " + accessToken
|
||||
headers["x-captcha-token"] = captchaToken
|
||||
|
||||
return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/share", "POST", data, nil, headers)
|
||||
}
|
||||
|
||||
// getShareInfo 获取分享信息(用于检验模式)
|
||||
func (x *XunleiPanService) getShareInfo(shareID string) (*XLShareInfo, error) {
|
||||
// 使用现有的 GetShareFolder 方法获取分享信息
|
||||
shareDetail, err := x.GetShareFolder(shareID, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构造分享信息
|
||||
shareInfo := &XLShareInfo{
|
||||
ShareID: shareID,
|
||||
Title: fmt.Sprintf("迅雷分享_%s", shareID),
|
||||
Files: make([]XLFileInfo, 0),
|
||||
}
|
||||
|
||||
// 处理文件信息
|
||||
for _, file := range shareDetail.Data.Files {
|
||||
shareInfo.Files = append(shareInfo.Files, XLFileInfo{
|
||||
FileID: file.FileID,
|
||||
Name: file.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return shareInfo, nil
|
||||
}
|
||||
|
||||
// GetFiles 获取文件列表 - 匹配 PHP 版本接口调用
|
||||
func (x *XunleiPanService) GetFiles(pdirFid string) (*TransferResult, error) {
|
||||
log.Printf("开始获取迅雷网盘文件列表,目录ID: %s", pdirFid)
|
||||
|
||||
// 获取 tokens
|
||||
accessToken, err := x.getAccessToken()
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("获取accessToken失败: %v", err)), nil
|
||||
}
|
||||
|
||||
captchaToken, err := x.getCaptchaToken()
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("获取captchaToken失败: %v", err)), nil
|
||||
}
|
||||
|
||||
// 设置 headers
|
||||
headers := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
headers[k] = v
|
||||
}
|
||||
headers["Authorization"] = "Bearer " + accessToken
|
||||
headers["x-captcha-token"] = captchaToken
|
||||
|
||||
filters := map[string]interface{}{
|
||||
"phase": map[string]interface{}{
|
||||
"eq": "PHASE_TYPE_COMPLETE",
|
||||
},
|
||||
"trashed": map[string]interface{}{
|
||||
"eq": false,
|
||||
},
|
||||
}
|
||||
|
||||
filtersStr, _ := json.Marshal(filters)
|
||||
queryParams := map[string]string{
|
||||
"parent_id": pdirFid,
|
||||
"filters": string(filtersStr),
|
||||
"with_audit": "true",
|
||||
"thumbnail_size": "SIZE_SMALL",
|
||||
"limit": "50",
|
||||
}
|
||||
|
||||
result, err := x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/files", "GET", nil, queryParams, headers)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("获取文件列表失败: %v", err)), nil
|
||||
}
|
||||
|
||||
if code, ok := result["code"].(float64); ok && code != 0 {
|
||||
return ErrorResult("获取文件列表失败"), nil
|
||||
}
|
||||
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
if files, ok2 := data["files"]; ok2 {
|
||||
return SuccessResult("获取成功", files), nil
|
||||
}
|
||||
}
|
||||
|
||||
return SuccessResult("获取成功", []interface{}{}), nil
|
||||
}
|
||||
|
||||
// DeleteFiles 删除文件 - 实现 PanService 接口
|
||||
func (x *XunleiPanService) DeleteFiles(fileList []string) (*TransferResult, error) {
|
||||
log.Printf("开始删除迅雷网盘文件,文件数量: %d", len(fileList))
|
||||
|
||||
// 使用现有的 ShareBatchDelete 方法删除分享
|
||||
result, err := x.ShareBatchDelete(fileList)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("删除文件失败: %v", err)), nil
|
||||
}
|
||||
|
||||
if result.Code != 0 {
|
||||
return ErrorResult(fmt.Sprintf("删除文件失败: %s", result.Msg)), nil
|
||||
}
|
||||
|
||||
return SuccessResult("删除成功", nil), nil
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息 - 实现 PanService 接口,cookie 参数为 refresh_token,先获取 access_token 再访问 API
|
||||
func (x *XunleiPanService) GetUserInfo(cookie *string) (*UserInfo, error) {
|
||||
userInfo := &UserInfo{}
|
||||
accessToken, err := x.getAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
captchaToken, err := x.getCaptchaToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
for k, v := range x.headers {
|
||||
headers[k] = v
|
||||
}
|
||||
headers["Authorization"] = "Bearer " + accessToken
|
||||
headers["x-captcha-token"] = captchaToken
|
||||
|
||||
resp, err := x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/about", "GET", nil, nil, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户信息失败: %v", err)
|
||||
}
|
||||
limit := resp["quota"].(map[string]interface{})["limit"].(string)
|
||||
limitInt, _ := strconv.ParseInt(limit, 10, 64)
|
||||
used := resp["quota"].(map[string]interface{})["usage"].(string)
|
||||
usedInt, _ := strconv.ParseInt(used, 10, 64)
|
||||
userInfo.TotalSpace = limitInt
|
||||
userInfo.UsedSpace = usedInt
|
||||
|
||||
// 获取用户信息
|
||||
respData, err := x.requestXunleiApi(x.apiHost("user")+"/v1/user/me", "GET", nil, nil, headers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户信息失败: %v", err)
|
||||
}
|
||||
|
||||
vipInfo := respData["vip_info"].([]interface{})
|
||||
isVip := vipInfo[0].(map[string]interface{})["is_vip"].(string) != "0"
|
||||
|
||||
userInfo.Username = respData["name"].(string)
|
||||
userInfo.ServiceType = x.GetServiceType().String()
|
||||
userInfo.VIPStatus = isVip
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// GetShareList 严格对齐 GET + query(使用 BasePanService)
|
||||
func (x *XunleiPanService) GetShareList(pageToken string) (*XLShareListResp, error) {
|
||||
api := x.apiHost("") + "/drive/v1/share/list"
|
||||
queryParams := map[string]string{
|
||||
"limit": "100",
|
||||
"thumbnail_size": "SIZE_SMALL",
|
||||
}
|
||||
if pageToken != "" {
|
||||
queryParams["page_token"] = pageToken
|
||||
}
|
||||
|
||||
respData, err := x.HTTPGet(api, queryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取分享列表失败: %v", err)
|
||||
}
|
||||
|
||||
var data XLShareListResp
|
||||
if err := json.Unmarshal(respData, &data); err != nil {
|
||||
return nil, fmt.Errorf("解析分享列表失败: %v", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// FileBatchShare 创建分享(使用 BasePanService)
|
||||
func (x *XunleiPanService) FileBatchShare(ids []string, needPassword bool, expirationDays int) (*XLBatchShareResp, error) {
|
||||
apiURL := x.apiHost("") + "/drive/v1/share/batch"
|
||||
body := map[string]interface{}{
|
||||
"file_ids": ids,
|
||||
"need_password": needPassword,
|
||||
"expiration_days": expirationDays,
|
||||
}
|
||||
|
||||
respData, err := x.HTTPPost(apiURL, body, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建分享失败: %v", err)
|
||||
}
|
||||
|
||||
var data XLBatchShareResp
|
||||
if err := json.Unmarshal(respData, &data); err != nil {
|
||||
return nil, fmt.Errorf("解析分享响应失败: %v", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// ShareBatchDelete 取消分享(使用 BasePanService)
|
||||
func (x *XunleiPanService) ShareBatchDelete(ids []string) (*XLCommonResp, error) {
|
||||
apiURL := x.apiHost("") + "/drive/v1/share/batch/delete"
|
||||
body := map[string]interface{}{
|
||||
"share_ids": ids,
|
||||
}
|
||||
|
||||
respData, err := x.HTTPPost(apiURL, body, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("删除分享失败: %v", err)
|
||||
}
|
||||
|
||||
var data XLCommonResp
|
||||
if err := json.Unmarshal(respData, &data); err != nil {
|
||||
return nil, fmt.Errorf("解析删除响应失败: %v", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// GetShareFolder 获取分享内容(使用 BasePanService)
|
||||
func (x *XunleiPanService) GetShareFolder(shareID, passCodeToken, parentID string) (*XLShareFolderResp, error) {
|
||||
apiURL := x.apiHost("") + "/drive/v1/share/detail"
|
||||
body := map[string]interface{}{
|
||||
"share_id": shareID,
|
||||
"pass_code_token": passCodeToken,
|
||||
"parent_id": parentID,
|
||||
"limit": 100,
|
||||
"thumbnail_size": "SIZE_LARGE",
|
||||
"order": "6",
|
||||
}
|
||||
|
||||
respData, err := x.HTTPPost(apiURL, body, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取分享文件夹失败: %v", err)
|
||||
}
|
||||
|
||||
var data XLShareFolderResp
|
||||
if err := json.Unmarshal(respData, &data); err != nil {
|
||||
return nil, fmt.Errorf("解析分享文件夹失败: %v", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// Restore 转存(使用 BasePanService)
|
||||
func (x *XunleiPanService) Restore(shareID, passCodeToken string, fileIDs []string) (*XLRestoreResp, error) {
|
||||
apiURL := x.apiHost("") + "/drive/v1/share/restore"
|
||||
body := map[string]interface{}{
|
||||
"share_id": shareID,
|
||||
"pass_code_token": passCodeToken,
|
||||
"file_ids": fileIDs,
|
||||
"folder_type": "NORMAL",
|
||||
"specify_parent_id": true,
|
||||
"parent_id": "",
|
||||
}
|
||||
|
||||
respData, err := x.HTTPPost(apiURL, body, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("转存失败: %v", err)
|
||||
}
|
||||
|
||||
var data XLRestoreResp
|
||||
if err := json.Unmarshal(respData, &data); err != nil {
|
||||
return nil, fmt.Errorf("解析转存响应失败: %v", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// 结构体完全对齐 xunleix
|
||||
type XLShareListResp struct {
|
||||
Data struct {
|
||||
List []struct {
|
||||
ShareID string `json:"share_id"`
|
||||
Title string `json:"title"`
|
||||
} `json:"list"`
|
||||
} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type XLBatchShareResp struct {
|
||||
Data struct {
|
||||
ShareURL string `json:"share_url"`
|
||||
} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type XLCommonResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type XLShareFolderResp struct {
|
||||
Data struct {
|
||||
Files []struct {
|
||||
FileID string `json:"file_id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"files"`
|
||||
} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type XLRestoreResp struct {
|
||||
Data struct {
|
||||
TaskID string `json:"task_id"`
|
||||
} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// 新增辅助结构体
|
||||
type XLShareInfo struct {
|
||||
ShareID string `json:"share_id"`
|
||||
Title string `json:"title"`
|
||||
Files []XLFileInfo `json:"files"`
|
||||
}
|
||||
|
||||
type XLFileInfo struct {
|
||||
FileID string `json:"file_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type XLTaskResult struct {
|
||||
Status int `json:"status"`
|
||||
TaskID string `json:"task_id"`
|
||||
Data struct {
|
||||
ShareID string `json:"share_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
676
config/config.go
Normal file
676
config/config.go
Normal file
@@ -0,0 +1,676 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// ConfigManager 统一配置管理器
|
||||
type ConfigManager struct {
|
||||
repo *repo.RepositoryManager
|
||||
|
||||
// 内存缓存
|
||||
cache map[string]*ConfigItem
|
||||
cacheMutex sync.RWMutex
|
||||
cacheOnce sync.Once
|
||||
|
||||
// 配置更新通知
|
||||
configUpdateCh chan string
|
||||
watchers []chan string
|
||||
watcherMutex sync.Mutex
|
||||
|
||||
// 加载时间
|
||||
lastLoadTime time.Time
|
||||
}
|
||||
|
||||
// ConfigItem 配置项结构
|
||||
type ConfigItem struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Group string `json:"group"` // 配置分组
|
||||
Category string `json:"category"` // 配置分类
|
||||
IsSensitive bool `json:"is_sensitive"` // 是否是敏感信息
|
||||
}
|
||||
|
||||
// ConfigGroup 配置分组
|
||||
type ConfigGroup string
|
||||
|
||||
const (
|
||||
GroupDatabase ConfigGroup = "database"
|
||||
GroupServer ConfigGroup = "server"
|
||||
GroupSecurity ConfigGroup = "security"
|
||||
GroupSearch ConfigGroup = "search"
|
||||
GroupTelegram ConfigGroup = "telegram"
|
||||
GroupCache ConfigGroup = "cache"
|
||||
GroupMeilisearch ConfigGroup = "meilisearch"
|
||||
GroupSEO ConfigGroup = "seo"
|
||||
GroupAutoProcess ConfigGroup = "auto_process"
|
||||
GroupOther ConfigGroup = "other"
|
||||
)
|
||||
|
||||
// NewConfigManager 创建配置管理器
|
||||
func NewConfigManager(repoManager *repo.RepositoryManager) *ConfigManager {
|
||||
cm := &ConfigManager{
|
||||
repo: repoManager,
|
||||
cache: make(map[string]*ConfigItem),
|
||||
configUpdateCh: make(chan string, 100), // 缓冲通道防止阻塞
|
||||
}
|
||||
|
||||
// 启动配置更新监听器
|
||||
go cm.startConfigUpdateListener()
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// startConfigUpdateListener 启动配置更新监听器
|
||||
func (cm *ConfigManager) startConfigUpdateListener() {
|
||||
for key := range cm.configUpdateCh {
|
||||
cm.notifyWatchers(key)
|
||||
}
|
||||
}
|
||||
|
||||
// notifyWatchers 通知所有监听器配置已更新
|
||||
func (cm *ConfigManager) notifyWatchers(key string) {
|
||||
cm.watcherMutex.Lock()
|
||||
defer cm.watcherMutex.Unlock()
|
||||
|
||||
for _, watcher := range cm.watchers {
|
||||
select {
|
||||
case watcher <- key:
|
||||
default:
|
||||
// 如果通道阻塞,跳过该监听器
|
||||
utils.Warn("配置监听器通道阻塞,跳过通知: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddConfigWatcher 添加配置变更监听器
|
||||
func (cm *ConfigManager) AddConfigWatcher() chan string {
|
||||
cm.watcherMutex.Lock()
|
||||
defer cm.watcherMutex.Unlock()
|
||||
|
||||
watcher := make(chan string, 10) // 为每个监听器创建缓冲通道
|
||||
cm.watchers = append(cm.watchers, watcher)
|
||||
return watcher
|
||||
}
|
||||
|
||||
// GetConfig 获取配置项
|
||||
func (cm *ConfigManager) GetConfig(key string) (*ConfigItem, error) {
|
||||
// 先尝试从内存缓存获取
|
||||
item, exists := cm.getCachedConfig(key)
|
||||
if exists {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// 如果缓存中没有,从数据库获取
|
||||
config, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 将数据库配置转换为ConfigItem并缓存
|
||||
item = &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||
|
||||
// 缓存配置
|
||||
cm.setCachedConfig(key, item)
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// GetConfigValue 获取配置值
|
||||
func (cm *ConfigManager) GetConfigValue(key string) (string, error) {
|
||||
item, err := cm.GetConfig(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return item.Value, nil
|
||||
}
|
||||
|
||||
// GetConfigBool 获取布尔值配置
|
||||
func (cm *ConfigManager) GetConfigBool(key string) (bool, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(value) {
|
||||
case "true", "1", "yes", "on":
|
||||
return true, nil
|
||||
case "false", "0", "no", "off", "":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("无法将配置值 '%s' 转换为布尔值", value)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigInt 获取整数值配置
|
||||
func (cm *ConfigManager) GetConfigInt(key string) (int, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
// GetConfigInt64 获取64位整数值配置
|
||||
func (cm *ConfigManager) GetConfigInt64(key string) (int64, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
|
||||
// GetConfigFloat64 获取浮点数配置
|
||||
func (cm *ConfigManager) GetConfigFloat64(key string) (float64, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
|
||||
// SetConfig 设置配置值
|
||||
func (cm *ConfigManager) SetConfig(key, value string) error {
|
||||
// 更新数据库
|
||||
config := &entity.SystemConfig{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Type: "string", // 默认类型,实际类型应该从现有配置中获取
|
||||
}
|
||||
|
||||
// 获取现有配置以确定类型
|
||||
existing, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||
if err == nil {
|
||||
config.Type = existing.Type
|
||||
} else {
|
||||
// 如果配置不存在,尝试从默认配置中获取类型
|
||||
config.Type = cm.getDefaultConfigType(key)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
err = cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config})
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
item := &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||
|
||||
cm.setCachedConfig(key, item)
|
||||
|
||||
// 发送更新通知
|
||||
cm.configUpdateCh <- key
|
||||
|
||||
utils.Info("配置已更新: %s = %s", key, value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConfigWithType 设置配置值(指定类型)
|
||||
func (cm *ConfigManager) SetConfigWithType(key, value, configType string) error {
|
||||
config := &entity.SystemConfig{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Type: configType,
|
||||
}
|
||||
|
||||
err := cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config})
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
item := &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||
|
||||
cm.setCachedConfig(key, item)
|
||||
|
||||
// 发送更新通知
|
||||
cm.configUpdateCh <- key
|
||||
|
||||
utils.Info("配置已更新: %s = %s (type: %s)", key, value, configType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGroupByConfigKey 根据配置键获取分组
|
||||
func (cm *ConfigManager) getGroupByConfigKey(key string) ConfigGroup {
|
||||
switch {
|
||||
case strings.HasPrefix(key, "database_"), strings.HasPrefix(key, "db_"):
|
||||
return GroupDatabase
|
||||
case strings.HasPrefix(key, "server_"), strings.HasPrefix(key, "port"), strings.HasPrefix(key, "host"):
|
||||
return GroupServer
|
||||
case strings.HasPrefix(key, "api_"), strings.HasPrefix(key, "jwt_"), strings.HasPrefix(key, "password"):
|
||||
return GroupSecurity
|
||||
case strings.Contains(key, "meilisearch"):
|
||||
return GroupMeilisearch
|
||||
case strings.Contains(key, "telegram"):
|
||||
return GroupTelegram
|
||||
case strings.Contains(key, "cache"), strings.Contains(key, "redis"):
|
||||
return GroupCache
|
||||
case strings.Contains(key, "seo"), strings.Contains(key, "title"), strings.Contains(key, "keyword"):
|
||||
return GroupSEO
|
||||
case strings.Contains(key, "auto_"):
|
||||
return GroupAutoProcess
|
||||
case strings.Contains(key, "forbidden"), strings.Contains(key, "ad_"):
|
||||
return GroupOther
|
||||
default:
|
||||
return GroupOther
|
||||
}
|
||||
}
|
||||
|
||||
// getCategoryByConfigKey 根据配置键获取分类
|
||||
func (cm *ConfigManager) getCategoryByConfigKey(key string) string {
|
||||
switch {
|
||||
case key == entity.ConfigKeySiteTitle || key == entity.ConfigKeySiteDescription:
|
||||
return "basic_info"
|
||||
case key == entity.ConfigKeyKeywords || key == entity.ConfigKeyAuthor:
|
||||
return "seo"
|
||||
case key == entity.ConfigKeyAutoProcessReadyResources || key == entity.ConfigKeyAutoProcessInterval:
|
||||
return "auto_process"
|
||||
case key == entity.ConfigKeyAutoTransferEnabled || key == entity.ConfigKeyAutoTransferLimitDays:
|
||||
return "auto_transfer"
|
||||
case key == entity.ConfigKeyMeilisearchEnabled || key == entity.ConfigKeyMeilisearchHost:
|
||||
return "search"
|
||||
case key == entity.ConfigKeyTelegramBotEnabled || key == entity.ConfigKeyTelegramBotApiKey:
|
||||
return "telegram"
|
||||
case key == entity.ConfigKeyMaintenanceMode || key == entity.ConfigKeyEnableRegister:
|
||||
return "system"
|
||||
case key == entity.ConfigKeyForbiddenWords || key == entity.ConfigKeyAdKeywords:
|
||||
return "filtering"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
// isSensitiveConfig 判断是否是敏感配置
|
||||
func (cm *ConfigManager) isSensitiveConfig(key string) bool {
|
||||
switch key {
|
||||
case entity.ConfigKeyApiToken,
|
||||
entity.ConfigKeyMeilisearchMasterKey,
|
||||
entity.ConfigKeyTelegramBotApiKey,
|
||||
entity.ConfigKeyTelegramProxyUsername,
|
||||
entity.ConfigKeyTelegramProxyPassword:
|
||||
return true
|
||||
default:
|
||||
return strings.Contains(strings.ToLower(key), "password") ||
|
||||
strings.Contains(strings.ToLower(key), "secret") ||
|
||||
strings.Contains(strings.ToLower(key), "key") ||
|
||||
strings.Contains(strings.ToLower(key), "token")
|
||||
}
|
||||
}
|
||||
|
||||
// getDefaultConfigType 获取默认配置类型
|
||||
func (cm *ConfigManager) getDefaultConfigType(key string) string {
|
||||
switch key {
|
||||
case entity.ConfigKeyAutoProcessReadyResources,
|
||||
entity.ConfigKeyAutoTransferEnabled,
|
||||
entity.ConfigKeyAutoFetchHotDramaEnabled,
|
||||
entity.ConfigKeyMaintenanceMode,
|
||||
entity.ConfigKeyEnableRegister,
|
||||
entity.ConfigKeyMeilisearchEnabled,
|
||||
entity.ConfigKeyTelegramBotEnabled:
|
||||
return entity.ConfigTypeBool
|
||||
case entity.ConfigKeyAutoProcessInterval,
|
||||
entity.ConfigKeyAutoTransferLimitDays,
|
||||
entity.ConfigKeyAutoTransferMinSpace,
|
||||
entity.ConfigKeyPageSize:
|
||||
return entity.ConfigTypeInt
|
||||
case entity.ConfigKeyAnnouncements:
|
||||
return entity.ConfigTypeJSON
|
||||
default:
|
||||
return entity.ConfigTypeString
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAllConfigs 加载所有配置到缓存
|
||||
func (cm *ConfigManager) LoadAllConfigs() error {
|
||||
configs, err := cm.repo.SystemConfigRepository.FindAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载所有配置失败: %v", err)
|
||||
}
|
||||
|
||||
cm.cacheMutex.Lock()
|
||||
defer cm.cacheMutex.Unlock()
|
||||
|
||||
// 清空现有缓存
|
||||
cm.cache = make(map[string]*ConfigItem)
|
||||
|
||||
// 更新缓存
|
||||
for _, config := range configs {
|
||||
item := &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(), // 实际应该从数据库获取
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(config.Key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(config.Key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(config.Key)
|
||||
|
||||
cm.cache[config.Key] = item
|
||||
}
|
||||
|
||||
cm.lastLoadTime = time.Now()
|
||||
|
||||
utils.Info("已加载 %d 个配置项到缓存", len(configs))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshConfigCache 刷新配置缓存
|
||||
func (cm *ConfigManager) RefreshConfigCache() error {
|
||||
return cm.LoadAllConfigs()
|
||||
}
|
||||
|
||||
// GetCachedConfig 获取缓存的配置
|
||||
func (cm *ConfigManager) getCachedConfig(key string) (*ConfigItem, bool) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
item, exists := cm.cache[key]
|
||||
return item, exists
|
||||
}
|
||||
|
||||
// setCachedConfig 设置缓存的配置
|
||||
func (cm *ConfigManager) setCachedConfig(key string, item *ConfigItem) {
|
||||
cm.cacheMutex.Lock()
|
||||
defer cm.cacheMutex.Unlock()
|
||||
|
||||
cm.cache[key] = item
|
||||
}
|
||||
|
||||
// GetConfigByGroup 按分组获取配置
|
||||
func (cm *ConfigManager) GetConfigByGroup(group ConfigGroup) (map[string]*ConfigItem, error) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
result := make(map[string]*ConfigItem)
|
||||
|
||||
for key, item := range cm.cache {
|
||||
if ConfigGroup(item.Group) == group {
|
||||
result[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetConfigByCategory 按分类获取配置
|
||||
func (cm *ConfigManager) GetConfigByCategory(category string) (map[string]*ConfigItem, error) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
result := make(map[string]*ConfigItem)
|
||||
|
||||
for key, item := range cm.cache {
|
||||
if item.Category == category {
|
||||
result[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteConfig 删除配置
|
||||
func (cm *ConfigManager) DeleteConfig(key string) error {
|
||||
// 先查找配置获取ID
|
||||
config, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 从数据库删除
|
||||
err = cm.repo.SystemConfigRepository.Delete(config.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 从缓存中移除
|
||||
cm.cacheMutex.Lock()
|
||||
delete(cm.cache, key)
|
||||
cm.cacheMutex.Unlock()
|
||||
|
||||
utils.Info("配置已删除: %s", key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSensitiveConfigKeys 获取所有敏感配置键
|
||||
func (cm *ConfigManager) GetSensitiveConfigKeys() []string {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
var sensitiveKeys []string
|
||||
for key, item := range cm.cache {
|
||||
if item.IsSensitive {
|
||||
sensitiveKeys = append(sensitiveKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
return sensitiveKeys
|
||||
}
|
||||
|
||||
// GetConfigWithMask 获取配置值(敏感配置会被遮蔽)
|
||||
func (cm *ConfigManager) GetConfigWithMask(key string) (*ConfigItem, error) {
|
||||
item, err := cm.GetConfig(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if item.IsSensitive {
|
||||
// 创建副本并遮蔽敏感值
|
||||
maskedItem := *item
|
||||
maskedItem.Value = cm.maskSensitiveValue(item.Value)
|
||||
return &maskedItem, nil
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// maskSensitiveValue 遮蔽敏感值
|
||||
func (cm *ConfigManager) maskSensitiveValue(value string) string {
|
||||
if len(value) <= 4 {
|
||||
return "****"
|
||||
}
|
||||
|
||||
// 保留前2个和后2个字符,中间用****替代
|
||||
return value[:2] + "****" + value[len(value)-2:]
|
||||
}
|
||||
|
||||
// GetConfigAsJSON 获取配置为JSON格式
|
||||
func (cm *ConfigManager) GetConfigAsJSON() ([]byte, error) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
// 创建副本,敏感配置使用遮蔽值
|
||||
configMap := make(map[string]*ConfigItem)
|
||||
for key, item := range cm.cache {
|
||||
if item.IsSensitive {
|
||||
maskedItem := *item
|
||||
maskedItem.Value = cm.maskSensitiveValue(item.Value)
|
||||
configMap[key] = &maskedItem
|
||||
} else {
|
||||
configMap[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
return json.MarshalIndent(configMap, "", " ")
|
||||
}
|
||||
|
||||
// GetConfigStatistics 获取配置统计信息
|
||||
func (cm *ConfigManager) GetConfigStatistics() map[string]interface{} {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total_configs": len(cm.cache),
|
||||
"last_load_time": cm.lastLoadTime,
|
||||
"cache_size_bytes": len(cm.cache) * 100, // 估算每个配置约100字节
|
||||
"groups": make(map[string]int),
|
||||
"types": make(map[string]int),
|
||||
"categories": make(map[string]int),
|
||||
"sensitive_configs": 0,
|
||||
"config_keys": make([]string, 0),
|
||||
}
|
||||
|
||||
groups := make(map[string]int)
|
||||
types := make(map[string]int)
|
||||
categories := make(map[string]int)
|
||||
|
||||
for key, item := range cm.cache {
|
||||
// 统计分组
|
||||
groups[item.Group]++
|
||||
|
||||
// 统计类型
|
||||
types[item.Type]++
|
||||
|
||||
// 统计分类
|
||||
categories[item.Category]++
|
||||
|
||||
// 统计敏感配置
|
||||
if item.IsSensitive {
|
||||
stats["sensitive_configs"] = stats["sensitive_configs"].(int) + 1
|
||||
}
|
||||
|
||||
// 添加配置键到列表
|
||||
keys := stats["config_keys"].([]string)
|
||||
keys = append(keys, key)
|
||||
stats["config_keys"] = keys
|
||||
}
|
||||
|
||||
stats["groups"] = groups
|
||||
stats["types"] = types
|
||||
stats["categories"] = categories
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// GetEnvironmentConfig 从环境变量获取配置
|
||||
func (cm *ConfigManager) GetEnvironmentConfig(key string) (string, bool) {
|
||||
value := os.Getenv(key)
|
||||
if value != "" {
|
||||
return value, true
|
||||
}
|
||||
|
||||
// 尝试使用大写版本的键
|
||||
value = os.Getenv(strings.ToUpper(key))
|
||||
if value != "" {
|
||||
return value, true
|
||||
}
|
||||
|
||||
// 尝试使用大写带下划线的格式
|
||||
upperKey := strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
||||
value = os.Getenv(upperKey)
|
||||
if value != "" {
|
||||
return value, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetConfigWithEnvFallback 获取配置,环境变量优先
|
||||
func (cm *ConfigManager) GetConfigWithEnvFallback(configKey, envKey string) (string, error) {
|
||||
// 优先从环境变量获取
|
||||
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||
return envValue, nil
|
||||
}
|
||||
|
||||
// 如果环境变量不存在,从数据库获取
|
||||
return cm.GetConfigValue(configKey)
|
||||
}
|
||||
|
||||
// GetConfigIntWithEnvFallback 获取整数配置,环境变量优先
|
||||
func (cm *ConfigManager) GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) {
|
||||
// 优先从环境变量获取
|
||||
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||
return strconv.Atoi(envValue)
|
||||
}
|
||||
|
||||
// 如果环境变量不存在,从数据库获取
|
||||
return cm.GetConfigInt(configKey)
|
||||
}
|
||||
|
||||
// GetConfigBoolWithEnvFallback 获取布尔配置,环境变量优先
|
||||
func (cm *ConfigManager) GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) {
|
||||
// 优先从环境变量获取
|
||||
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||
switch strings.ToLower(envValue) {
|
||||
case "true", "1", "yes", "on":
|
||||
return true, nil
|
||||
case "false", "0", "no", "off", "":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("无法将环境变量值 '%s' 转换为布尔值", envValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果环境变量不存在,从数据库获取
|
||||
return cm.GetConfigBool(configKey)
|
||||
}
|
||||
124
config/global.go
Normal file
124
config/global.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
globalConfigManager *ConfigManager
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// SetGlobalConfigManager 设置全局配置管理器
|
||||
func SetGlobalConfigManager(cm *ConfigManager) {
|
||||
globalConfigManager = cm
|
||||
}
|
||||
|
||||
// GetGlobalConfigManager 获取全局配置管理器
|
||||
func GetGlobalConfigManager() *ConfigManager {
|
||||
return globalConfigManager
|
||||
}
|
||||
|
||||
// GetConfig 获取配置值(全局函数)
|
||||
func GetConfig(key string) (*ConfigItem, error) {
|
||||
if globalConfigManager == nil {
|
||||
return nil, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfig(key)
|
||||
}
|
||||
|
||||
// GetConfigValue 获取配置值(全局函数)
|
||||
func GetConfigValue(key string) (string, error) {
|
||||
if globalConfigManager == nil {
|
||||
return "", ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigValue(key)
|
||||
}
|
||||
|
||||
// GetConfigBool 获取布尔配置值(全局函数)
|
||||
func GetConfigBool(key string) (bool, error) {
|
||||
if globalConfigManager == nil {
|
||||
return false, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigBool(key)
|
||||
}
|
||||
|
||||
// GetConfigInt 获取整数配置值(全局函数)
|
||||
func GetConfigInt(key string) (int, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigInt(key)
|
||||
}
|
||||
|
||||
// GetConfigInt64 获取64位整数配置值(全局函数)
|
||||
func GetConfigInt64(key string) (int64, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigInt64(key)
|
||||
}
|
||||
|
||||
// GetConfigFloat64 获取浮点数配置值(全局函数)
|
||||
func GetConfigFloat64(key string) (float64, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigFloat64(key)
|
||||
}
|
||||
|
||||
// SetConfig 设置配置值(全局函数)
|
||||
func SetConfig(key, value string) error {
|
||||
if globalConfigManager == nil {
|
||||
return ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.SetConfig(key, value)
|
||||
}
|
||||
|
||||
// SetConfigWithType 设置配置值(指定类型,全局函数)
|
||||
func SetConfigWithType(key, value, configType string) error {
|
||||
if globalConfigManager == nil {
|
||||
return ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.SetConfigWithType(key, value, configType)
|
||||
}
|
||||
|
||||
// GetConfigWithEnvFallback 获取配置值(环境变量优先,全局函数)
|
||||
func GetConfigWithEnvFallback(configKey, envKey string) (string, error) {
|
||||
if globalConfigManager == nil {
|
||||
return "", ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigWithEnvFallback(configKey, envKey)
|
||||
}
|
||||
|
||||
// GetConfigIntWithEnvFallback 获取整数配置值(环境变量优先,全局函数)
|
||||
func GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigIntWithEnvFallback(configKey, envKey)
|
||||
}
|
||||
|
||||
// GetConfigBoolWithEnvFallback 获取布尔配置值(环境变量优先,全局函数)
|
||||
func GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) {
|
||||
if globalConfigManager == nil {
|
||||
return false, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigBoolWithEnvFallback(configKey, envKey)
|
||||
}
|
||||
|
||||
// ErrConfigManagerNotInitialized 配置管理器未初始化错误
|
||||
var ErrConfigManagerNotInitialized = &ConfigError{
|
||||
Code: "CONFIG_MANAGER_NOT_INITIALIZED",
|
||||
Message: "配置管理器未初始化",
|
||||
}
|
||||
|
||||
// ConfigError 配置错误
|
||||
type ConfigError struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ConfigError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
31
config/sync.go
Normal file
31
config/sync.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// SyncWithRepository 同步配置管理器与Repository的缓存
|
||||
func (cm *ConfigManager) SyncWithRepository(repoManager *repo.RepositoryManager) {
|
||||
// 监听配置变更事件并同步缓存
|
||||
// 这是一个抽象概念,实际实现需要修改Repository接口
|
||||
|
||||
// 当配置更新时,通知Repository清理缓存
|
||||
go func() {
|
||||
watcher := cm.AddConfigWatcher()
|
||||
for {
|
||||
select {
|
||||
case key := <-watcher:
|
||||
// 通知Repository层清理缓存(如果Repository支持)
|
||||
utils.Debug("配置 %s 已更新,可能需要同步到Repository缓存", key)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// UpdateRepositoryCache 当配置管理器更新配置时,通知Repository层同步
|
||||
func (cm *ConfigManager) UpdateRepositoryCache(repoManager *repo.RepositoryManager) {
|
||||
// 这个函数需要Repository支持特定的缓存清理方法
|
||||
// 由于现有Repository没有提供这样的接口,我们只能依赖数据库同步
|
||||
utils.Info("配置已通过配置管理器更新,Repository层将从数据库重新加载")
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
@@ -45,8 +47,22 @@ func InitDB() error {
|
||||
host, port, user, password, dbname)
|
||||
|
||||
var err error
|
||||
// 配置慢查询日志
|
||||
slowThreshold := getEnvInt("DB_SLOW_THRESHOLD_MS", 200)
|
||||
logLevel := logger.Info
|
||||
if os.Getenv("ENV") == "production" {
|
||||
logLevel = logger.Warn
|
||||
}
|
||||
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
Logger: logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: time.Duration(slowThreshold) * time.Millisecond,
|
||||
LogLevel: logLevel,
|
||||
Colorful: true,
|
||||
},
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,10 +74,17 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
|
||||
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
|
||||
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
|
||||
// 优化数据库连接池参数
|
||||
maxOpenConns := getEnvInt("DB_MAX_OPEN_CONNS", 50)
|
||||
maxIdleConns := getEnvInt("DB_MAX_IDLE_CONNS", 20)
|
||||
connMaxLifetime := getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 30)
|
||||
|
||||
sqlDB.SetMaxOpenConns(maxOpenConns) // 最大打开连接数
|
||||
sqlDB.SetMaxIdleConns(maxIdleConns) // 最大空闲连接数
|
||||
sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Minute) // 连接最大生命周期
|
||||
|
||||
utils.Info("数据库连接池配置 - 最大连接: %d, 空闲连接: %d, 生命周期: %d分钟",
|
||||
maxOpenConns, maxIdleConns, connMaxLifetime)
|
||||
|
||||
// 检查是否需要迁移(只在开发环境或首次启动时)
|
||||
if shouldRunMigration() {
|
||||
@@ -82,6 +105,12 @@ func InitDB() error {
|
||||
&entity.Task{},
|
||||
&entity.TaskItem{},
|
||||
&entity.File{},
|
||||
&entity.TelegramChannel{},
|
||||
&entity.APIAccessLog{},
|
||||
&entity.APIAccessLogStats{},
|
||||
&entity.APIAccessLogSummary{},
|
||||
&entity.Report{},
|
||||
&entity.CopyrightClaim{},
|
||||
)
|
||||
if err != nil {
|
||||
utils.Fatal("数据库迁移失败: %v", err)
|
||||
@@ -146,6 +175,7 @@ func autoMigrate() error {
|
||||
&entity.SearchStat{},
|
||||
&entity.HotDrama{},
|
||||
&entity.File{},
|
||||
&entity.TelegramChannel{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -181,7 +211,15 @@ func createIndexes(db *gorm.DB) {
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resource_tags_resource_id ON resource_tags(resource_id)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resource_tags_tag_id ON resource_tags(tag_id)")
|
||||
|
||||
utils.Info("数据库索引创建完成(已移除全文搜索索引,准备使用Meilisearch)")
|
||||
// API访问日志表索引 - 高性能查询优化
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_created_at ON api_access_logs(created_at DESC)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_endpoint_status ON api_access_logs(endpoint, response_status)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_ip_created ON api_access_logs(ip, created_at DESC)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_method_endpoint ON api_access_logs(method, endpoint)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_response_time ON api_access_logs(processing_time)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_error_logs ON api_access_logs(response_status, created_at DESC) WHERE response_status >= 400")
|
||||
|
||||
utils.Info("数据库索引创建完成(已移除全文搜索索引,准备使用Meilisearch,新增API访问日志性能索引)")
|
||||
}
|
||||
|
||||
// insertDefaultDataIfEmpty 只在数据库为空时插入默认数据
|
||||
@@ -263,6 +301,13 @@ func insertDefaultDataIfEmpty() error {
|
||||
{Key: entity.ConfigKeyAutoInsertAd, Value: entity.ConfigDefaultAutoInsertAd, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchEnabled, Value: entity.ConfigDefaultMeilisearchEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyMeilisearchHost, Value: entity.ConfigDefaultMeilisearchHost, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
for _, config := range defaultSystemConfigs {
|
||||
@@ -288,3 +333,19 @@ func insertDefaultDataIfEmpty() error {
|
||||
utils.Info("默认数据插入完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEnvInt 获取环境变量中的整数值,如果不存在则返回默认值
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
utils.Warn("环境变量 %s 的值 '%s' 不是有效的整数,使用默认值 %d", key, value, defaultValue)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return intValue
|
||||
}
|
||||
|
||||
66
db/converter/api_access_log_converter.go
Normal file
66
db/converter/api_access_log_converter.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// ToAPIAccessLogResponse 将APIAccessLog实体转换为APIAccessLogResponse
|
||||
func ToAPIAccessLogResponse(log *entity.APIAccessLog) dto.APIAccessLogResponse {
|
||||
return dto.APIAccessLogResponse{
|
||||
ID: log.ID,
|
||||
IP: log.IP,
|
||||
UserAgent: log.UserAgent,
|
||||
Endpoint: log.Endpoint,
|
||||
Method: log.Method,
|
||||
RequestParams: log.RequestParams,
|
||||
ResponseStatus: log.ResponseStatus,
|
||||
ResponseData: log.ResponseData,
|
||||
ProcessCount: log.ProcessCount,
|
||||
ErrorMessage: log.ErrorMessage,
|
||||
ProcessingTime: log.ProcessingTime,
|
||||
CreatedAt: log.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIAccessLogResponseList 将APIAccessLog实体列表转换为APIAccessLogResponse列表
|
||||
func ToAPIAccessLogResponseList(logs []entity.APIAccessLog) []dto.APIAccessLogResponse {
|
||||
responses := make([]dto.APIAccessLogResponse, len(logs))
|
||||
for i, log := range logs {
|
||||
responses[i] = ToAPIAccessLogResponse(&log)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// ToAPIAccessLogSummaryResponse 将APIAccessLogSummary实体转换为APIAccessLogSummaryResponse
|
||||
func ToAPIAccessLogSummaryResponse(summary *entity.APIAccessLogSummary) dto.APIAccessLogSummaryResponse {
|
||||
return dto.APIAccessLogSummaryResponse{
|
||||
TotalRequests: summary.TotalRequests,
|
||||
TodayRequests: summary.TodayRequests,
|
||||
WeekRequests: summary.WeekRequests,
|
||||
MonthRequests: summary.MonthRequests,
|
||||
ErrorRequests: summary.ErrorRequests,
|
||||
UniqueIPs: summary.UniqueIPs,
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIAccessLogStatsResponse 将APIAccessLogStats实体转换为APIAccessLogStatsResponse
|
||||
func ToAPIAccessLogStatsResponse(stat entity.APIAccessLogStats) dto.APIAccessLogStatsResponse {
|
||||
return dto.APIAccessLogStatsResponse{
|
||||
Endpoint: stat.Endpoint,
|
||||
Method: stat.Method,
|
||||
RequestCount: stat.RequestCount,
|
||||
ErrorCount: stat.ErrorCount,
|
||||
AvgProcessTime: stat.AvgProcessTime,
|
||||
LastAccess: stat.LastAccess,
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIAccessLogStatsResponseList 将APIAccessLogStats实体列表转换为APIAccessLogStatsResponse列表
|
||||
func ToAPIAccessLogStatsResponseList(stats []entity.APIAccessLogStats) []dto.APIAccessLogStatsResponse {
|
||||
responses := make([]dto.APIAccessLogStatsResponse, len(stats))
|
||||
for i, stat := range stats {
|
||||
responses[i] = ToAPIAccessLogStatsResponse(stat)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
@@ -10,22 +11,25 @@ import (
|
||||
// ToResourceResponse 将Resource实体转换为ResourceResponse
|
||||
func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
|
||||
response := dto.ResourceResponse{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
PanID: resource.PanID,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
CategoryID: resource.CategoryID,
|
||||
ViewCount: resource.ViewCount,
|
||||
IsValid: resource.IsValid,
|
||||
IsPublic: resource.IsPublic,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
Cover: resource.Cover,
|
||||
Author: resource.Author,
|
||||
ErrorMsg: resource.ErrorMsg,
|
||||
ID: resource.ID,
|
||||
Key: resource.Key,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
PanID: resource.PanID,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
CategoryID: resource.CategoryID,
|
||||
ViewCount: resource.ViewCount,
|
||||
IsValid: resource.IsValid,
|
||||
IsPublic: resource.IsPublic,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
Cover: resource.Cover,
|
||||
Author: resource.Author,
|
||||
ErrorMsg: resource.ErrorMsg,
|
||||
SyncedToMeilisearch: resource.SyncedToMeilisearch,
|
||||
SyncedAt: resource.SyncedAt,
|
||||
}
|
||||
|
||||
// 设置分类名称
|
||||
@@ -33,6 +37,18 @@ func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
|
||||
response.CategoryName = resource.Category.Name
|
||||
}
|
||||
|
||||
// 设置平台信息
|
||||
if resource.Pan.ID != 0 {
|
||||
panResponse := dto.PanResponse{
|
||||
ID: resource.Pan.ID,
|
||||
Name: resource.Pan.Name,
|
||||
Key: resource.Pan.Key,
|
||||
Icon: resource.Pan.Icon,
|
||||
Remark: resource.Pan.Remark,
|
||||
}
|
||||
response.Pan = &panResponse
|
||||
}
|
||||
|
||||
// 转换标签
|
||||
response.Tags = make([]dto.TagResponse, len(resource.Tags))
|
||||
for i, tag := range resource.Tags {
|
||||
@@ -47,6 +63,92 @@ func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
|
||||
return response
|
||||
}
|
||||
|
||||
// ToResourceResponseFromMeilisearch 将MeilisearchDocument转换为ResourceResponse(包含高亮信息)
|
||||
func ToResourceResponseFromMeilisearch(doc interface{}) dto.ResourceResponse {
|
||||
// 使用反射来获取MeilisearchDocument的字段
|
||||
docValue := reflect.ValueOf(doc)
|
||||
if docValue.Kind() == reflect.Ptr {
|
||||
docValue = docValue.Elem()
|
||||
}
|
||||
|
||||
response := dto.ResourceResponse{}
|
||||
|
||||
// 获取基本字段
|
||||
if idField := docValue.FieldByName("ID"); idField.IsValid() {
|
||||
response.ID = uint(idField.Uint())
|
||||
}
|
||||
if titleField := docValue.FieldByName("Title"); titleField.IsValid() {
|
||||
response.Title = titleField.String()
|
||||
}
|
||||
if descField := docValue.FieldByName("Description"); descField.IsValid() {
|
||||
response.Description = descField.String()
|
||||
}
|
||||
if urlField := docValue.FieldByName("URL"); urlField.IsValid() {
|
||||
response.URL = urlField.String()
|
||||
}
|
||||
if coverField := docValue.FieldByName("Cover"); coverField.IsValid() {
|
||||
response.Cover = coverField.String()
|
||||
}
|
||||
if saveURLField := docValue.FieldByName("SaveURL"); saveURLField.IsValid() {
|
||||
response.SaveURL = saveURLField.String()
|
||||
}
|
||||
if fileSizeField := docValue.FieldByName("FileSize"); fileSizeField.IsValid() {
|
||||
response.FileSize = fileSizeField.String()
|
||||
}
|
||||
if keyField := docValue.FieldByName("Key"); keyField.IsValid() {
|
||||
response.Key = keyField.String()
|
||||
}
|
||||
if categoryField := docValue.FieldByName("Category"); categoryField.IsValid() {
|
||||
response.CategoryName = categoryField.String()
|
||||
}
|
||||
if authorField := docValue.FieldByName("Author"); authorField.IsValid() {
|
||||
response.Author = authorField.String()
|
||||
}
|
||||
if createdAtField := docValue.FieldByName("CreatedAt"); createdAtField.IsValid() {
|
||||
response.CreatedAt = createdAtField.Interface().(time.Time)
|
||||
}
|
||||
if updatedAtField := docValue.FieldByName("UpdatedAt"); updatedAtField.IsValid() {
|
||||
response.UpdatedAt = updatedAtField.Interface().(time.Time)
|
||||
}
|
||||
|
||||
// 处理PanID
|
||||
if panIDField := docValue.FieldByName("PanID"); panIDField.IsValid() && !panIDField.IsNil() {
|
||||
panIDPtr := panIDField.Interface().(*uint)
|
||||
if panIDPtr != nil {
|
||||
response.PanID = panIDPtr
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Tags
|
||||
if tagsField := docValue.FieldByName("Tags"); tagsField.IsValid() {
|
||||
tags := tagsField.Interface().([]string)
|
||||
response.Tags = make([]dto.TagResponse, len(tags))
|
||||
for i, tagName := range tags {
|
||||
response.Tags[i] = dto.TagResponse{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理高亮字段
|
||||
if titleHighlightField := docValue.FieldByName("TitleHighlight"); titleHighlightField.IsValid() {
|
||||
response.TitleHighlight = titleHighlightField.String()
|
||||
}
|
||||
if descHighlightField := docValue.FieldByName("DescriptionHighlight"); descHighlightField.IsValid() {
|
||||
response.DescriptionHighlight = descHighlightField.String()
|
||||
}
|
||||
if categoryHighlightField := docValue.FieldByName("CategoryHighlight"); categoryHighlightField.IsValid() {
|
||||
response.CategoryHighlight = categoryHighlightField.String()
|
||||
}
|
||||
if tagsHighlightField := docValue.FieldByName("TagsHighlight"); tagsHighlightField.IsValid() {
|
||||
tagsHighlight := tagsHighlightField.Interface().([]string)
|
||||
response.TagsHighlight = make([]string, len(tagsHighlight))
|
||||
copy(response.TagsHighlight, tagsHighlight)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ToResourceResponseList 将Resource实体列表转换为ResourceResponse列表
|
||||
func ToResourceResponseList(resources []entity.Resource) []dto.ResourceResponse {
|
||||
responses := make([]dto.ResourceResponse, len(resources))
|
||||
@@ -176,7 +278,7 @@ func ToReadyResourceResponse(resource *entity.ReadyResource) dto.ReadyResourceRe
|
||||
if isDeleted {
|
||||
deletedAt = &resource.DeletedAt.Time
|
||||
}
|
||||
|
||||
|
||||
return dto.ReadyResourceResponse{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
|
||||
95
db/converter/copyright_claim_converter.go
Normal file
95
db/converter/copyright_claim_converter.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// CopyrightClaimToResponseWithResources 将版权申述实体和关联资源转换为响应对象
|
||||
func CopyrightClaimToResponseWithResources(claim *entity.CopyrightClaim, resources []*entity.Resource) *dto.CopyrightClaimResponse {
|
||||
if claim == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换关联的资源信息
|
||||
var resourceInfos []dto.ResourceInfo
|
||||
for _, resource := range resources {
|
||||
categoryName := ""
|
||||
if resource.Category.ID != 0 {
|
||||
categoryName = resource.Category.Name
|
||||
}
|
||||
|
||||
panName := ""
|
||||
if resource.Pan.ID != 0 {
|
||||
panName = resource.Pan.Name
|
||||
}
|
||||
|
||||
resourceInfo := dto.ResourceInfo{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
Category: categoryName,
|
||||
PanName: panName,
|
||||
ViewCount: resource.ViewCount,
|
||||
IsValid: resource.IsValid,
|
||||
CreatedAt: resource.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
resourceInfos = append(resourceInfos, resourceInfo)
|
||||
}
|
||||
|
||||
return &dto.CopyrightClaimResponse{
|
||||
ID: claim.ID,
|
||||
ResourceKey: claim.ResourceKey,
|
||||
Identity: claim.Identity,
|
||||
ProofType: claim.ProofType,
|
||||
Reason: claim.Reason,
|
||||
ContactInfo: claim.ContactInfo,
|
||||
ClaimantName: claim.ClaimantName,
|
||||
ProofFiles: claim.ProofFiles,
|
||||
UserAgent: claim.UserAgent,
|
||||
IPAddress: claim.IPAddress,
|
||||
Status: claim.Status,
|
||||
Note: claim.Note,
|
||||
CreatedAt: claim.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: claim.UpdatedAt.Format(time.RFC3339),
|
||||
Resources: resourceInfos,
|
||||
}
|
||||
}
|
||||
|
||||
// CopyrightClaimToResponse 将版权申述实体转换为响应对象(不包含资源详情)
|
||||
func CopyrightClaimToResponse(claim *entity.CopyrightClaim) *dto.CopyrightClaimResponse {
|
||||
if claim == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &dto.CopyrightClaimResponse{
|
||||
ID: claim.ID,
|
||||
ResourceKey: claim.ResourceKey,
|
||||
Identity: claim.Identity,
|
||||
ProofType: claim.ProofType,
|
||||
Reason: claim.Reason,
|
||||
ContactInfo: claim.ContactInfo,
|
||||
ClaimantName: claim.ClaimantName,
|
||||
ProofFiles: claim.ProofFiles,
|
||||
UserAgent: claim.UserAgent,
|
||||
IPAddress: claim.IPAddress,
|
||||
Status: claim.Status,
|
||||
Note: claim.Note,
|
||||
CreatedAt: claim.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: claim.UpdatedAt.Format(time.RFC3339),
|
||||
Resources: []dto.ResourceInfo{}, // 空的资源列表
|
||||
}
|
||||
}
|
||||
|
||||
// CopyrightClaimsToResponse 将版权申述实体列表转换为响应对象列表
|
||||
func CopyrightClaimsToResponse(claims []*entity.CopyrightClaim) []*dto.CopyrightClaimResponse {
|
||||
var responses []*dto.CopyrightClaimResponse
|
||||
for _, claim := range claims {
|
||||
responses = append(responses, CopyrightClaimToResponse(claim))
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@@ -29,6 +29,7 @@ func HotDramaToResponse(drama *entity.HotDrama) *dto.HotDramaResponse {
|
||||
PosterURL: drama.PosterURL,
|
||||
Category: drama.Category,
|
||||
SubType: drama.SubType,
|
||||
Rank: drama.Rank,
|
||||
Source: drama.Source,
|
||||
DoubanID: drama.DoubanID,
|
||||
DoubanURI: drama.DoubanURI,
|
||||
@@ -49,6 +50,7 @@ func RequestToHotDrama(req *dto.HotDramaRequest) *entity.HotDrama {
|
||||
Actors: req.Actors,
|
||||
Category: req.Category,
|
||||
SubType: req.SubType,
|
||||
Rank: req.Rank,
|
||||
Source: req.Source,
|
||||
DoubanID: req.DoubanID,
|
||||
}
|
||||
|
||||
89
db/converter/report_converter.go
Normal file
89
db/converter/report_converter.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// ReportToResponseWithResources 将举报实体和关联资源转换为响应对象
|
||||
func ReportToResponseWithResources(report *entity.Report, resources []*entity.Resource) *dto.ReportResponse {
|
||||
if report == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换关联的资源信息
|
||||
var resourceInfos []dto.ResourceInfo
|
||||
for _, resource := range resources {
|
||||
categoryName := ""
|
||||
if resource.Category.ID != 0 {
|
||||
categoryName = resource.Category.Name
|
||||
}
|
||||
|
||||
panName := ""
|
||||
if resource.Pan.ID != 0 {
|
||||
panName = resource.Pan.Name
|
||||
}
|
||||
|
||||
resourceInfo := dto.ResourceInfo{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
Category: categoryName,
|
||||
PanName: panName,
|
||||
ViewCount: resource.ViewCount,
|
||||
IsValid: resource.IsValid,
|
||||
CreatedAt: resource.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
resourceInfos = append(resourceInfos, resourceInfo)
|
||||
}
|
||||
|
||||
return &dto.ReportResponse{
|
||||
ID: report.ID,
|
||||
ResourceKey: report.ResourceKey,
|
||||
Reason: report.Reason,
|
||||
Description: report.Description,
|
||||
Contact: report.Contact,
|
||||
UserAgent: report.UserAgent,
|
||||
IPAddress: report.IPAddress,
|
||||
Status: report.Status,
|
||||
Note: report.Note,
|
||||
CreatedAt: report.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: report.UpdatedAt.Format(time.RFC3339),
|
||||
Resources: resourceInfos,
|
||||
}
|
||||
}
|
||||
|
||||
// ReportToResponse 将举报实体转换为响应对象(不包含资源详情)
|
||||
func ReportToResponse(report *entity.Report) *dto.ReportResponse {
|
||||
if report == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &dto.ReportResponse{
|
||||
ID: report.ID,
|
||||
ResourceKey: report.ResourceKey,
|
||||
Reason: report.Reason,
|
||||
Description: report.Description,
|
||||
Contact: report.Contact,
|
||||
UserAgent: report.UserAgent,
|
||||
IPAddress: report.IPAddress,
|
||||
Status: report.Status,
|
||||
Note: report.Note,
|
||||
CreatedAt: report.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: report.UpdatedAt.Format(time.RFC3339),
|
||||
Resources: []dto.ResourceInfo{}, // 空的资源列表
|
||||
}
|
||||
}
|
||||
|
||||
// ReportsToResponse 将举报实体列表转换为响应对象列表
|
||||
func ReportsToResponse(reports []*entity.Report) []*dto.ReportResponse {
|
||||
var responses []*dto.ReportResponse
|
||||
for _, report := range reports {
|
||||
responses = append(responses, ReportToResponse(report))
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -78,6 +79,41 @@ func SystemConfigToResponse(configs []entity.SystemConfig) *dto.SystemConfigResp
|
||||
}
|
||||
case entity.ConfigKeyThirdPartyStatsCode:
|
||||
response.ThirdPartyStatsCode = config.Value
|
||||
case entity.ConfigKeyMeilisearchEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.MeilisearchEnabled = val
|
||||
}
|
||||
case entity.ConfigKeyMeilisearchHost:
|
||||
response.MeilisearchHost = config.Value
|
||||
case entity.ConfigKeyMeilisearchPort:
|
||||
response.MeilisearchPort = config.Value
|
||||
case entity.ConfigKeyMeilisearchMasterKey:
|
||||
response.MeilisearchMasterKey = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
response.MeilisearchIndexName = config.Value
|
||||
case entity.ConfigKeyEnableAnnouncements:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.EnableAnnouncements = val
|
||||
}
|
||||
case entity.ConfigKeyAnnouncements:
|
||||
if config.Value == "" || config.Value == "[]" {
|
||||
response.Announcements = ""
|
||||
} else {
|
||||
// 在响应时保持为字符串,后续由前端处理
|
||||
response.Announcements = config.Value
|
||||
}
|
||||
case entity.ConfigKeyEnableFloatButtons:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.EnableFloatButtons = val
|
||||
}
|
||||
case entity.ConfigKeyWechatSearchImage:
|
||||
response.WechatSearchImage = config.Value
|
||||
case entity.ConfigKeyTelegramQrImage:
|
||||
response.TelegramQrImage = config.Value
|
||||
case entity.ConfigKeyQrCodeStyle:
|
||||
response.QrCodeStyle = config.Value
|
||||
case entity.ConfigKeyWebsiteURL:
|
||||
response.SiteURL = config.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +223,61 @@ func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig {
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyThirdPartyStatsCode)
|
||||
}
|
||||
|
||||
// Meilisearch配置 - 只处理被设置的字段
|
||||
if req.MeilisearchEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchEnabled, Value: strconv.FormatBool(*req.MeilisearchEnabled), Type: entity.ConfigTypeBool})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchEnabled)
|
||||
}
|
||||
if req.MeilisearchHost != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchHost, Value: *req.MeilisearchHost, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchHost)
|
||||
}
|
||||
if req.MeilisearchPort != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchPort, Value: *req.MeilisearchPort, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchPort)
|
||||
}
|
||||
if req.MeilisearchMasterKey != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchMasterKey, Value: *req.MeilisearchMasterKey, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchMasterKey)
|
||||
}
|
||||
if req.MeilisearchIndexName != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchIndexName, Value: *req.MeilisearchIndexName, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchIndexName)
|
||||
}
|
||||
|
||||
// 界面配置处理
|
||||
if req.EnableAnnouncements != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableAnnouncements, Value: strconv.FormatBool(*req.EnableAnnouncements), Type: entity.ConfigTypeBool})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyEnableAnnouncements)
|
||||
}
|
||||
if req.Announcements != nil {
|
||||
// 将数组转换为JSON字符串
|
||||
if jsonBytes, err := json.Marshal(*req.Announcements); err == nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAnnouncements, Value: string(jsonBytes), Type: entity.ConfigTypeJSON})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyAnnouncements)
|
||||
}
|
||||
}
|
||||
if req.EnableFloatButtons != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableFloatButtons, Value: strconv.FormatBool(*req.EnableFloatButtons), Type: entity.ConfigTypeBool})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyEnableFloatButtons)
|
||||
}
|
||||
if req.WechatSearchImage != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyWechatSearchImage, Value: *req.WechatSearchImage, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyWechatSearchImage)
|
||||
}
|
||||
if req.TelegramQrImage != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyTelegramQrImage, Value: *req.TelegramQrImage, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyTelegramQrImage)
|
||||
}
|
||||
if req.QrCodeStyle != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyQrCodeStyle, Value: *req.QrCodeStyle, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyQrCodeStyle)
|
||||
}
|
||||
if req.SiteURL != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyWebsiteURL, Value: *req.SiteURL, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyWebsiteURL)
|
||||
}
|
||||
|
||||
// 记录更新的配置项
|
||||
if len(updatedKeys) > 0 {
|
||||
utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys)
|
||||
@@ -195,33 +286,28 @@ func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig {
|
||||
return configs
|
||||
}
|
||||
|
||||
// SystemConfigToPublicResponse 返回不含 api_token 的系统配置响应
|
||||
// SystemConfigToPublicResponse 返回不含敏感配置的系统配置响应
|
||||
func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]interface{} {
|
||||
response := map[string]interface{}{
|
||||
entity.ConfigResponseFieldID: 0,
|
||||
entity.ConfigResponseFieldCreatedAt: utils.GetCurrentTimeString(),
|
||||
entity.ConfigResponseFieldUpdatedAt: utils.GetCurrentTimeString(),
|
||||
entity.ConfigResponseFieldSiteTitle: entity.ConfigDefaultSiteTitle,
|
||||
entity.ConfigResponseFieldSiteDescription: entity.ConfigDefaultSiteDescription,
|
||||
entity.ConfigResponseFieldKeywords: entity.ConfigDefaultKeywords,
|
||||
entity.ConfigResponseFieldAuthor: entity.ConfigDefaultAuthor,
|
||||
entity.ConfigResponseFieldCopyright: entity.ConfigDefaultCopyright,
|
||||
"site_logo": "",
|
||||
entity.ConfigResponseFieldAutoProcessReadyResources: false,
|
||||
entity.ConfigResponseFieldAutoProcessInterval: 30,
|
||||
entity.ConfigResponseFieldAutoTransferEnabled: false,
|
||||
entity.ConfigResponseFieldAutoTransferLimitDays: 0,
|
||||
entity.ConfigResponseFieldAutoTransferMinSpace: 100,
|
||||
entity.ConfigResponseFieldAutoFetchHotDramaEnabled: false,
|
||||
entity.ConfigResponseFieldForbiddenWords: "",
|
||||
entity.ConfigResponseFieldAdKeywords: "",
|
||||
entity.ConfigResponseFieldAutoInsertAd: "",
|
||||
entity.ConfigResponseFieldPageSize: 100,
|
||||
entity.ConfigResponseFieldMaintenanceMode: false,
|
||||
entity.ConfigResponseFieldEnableRegister: true, // 默认开启注册功能
|
||||
entity.ConfigResponseFieldID: 0,
|
||||
entity.ConfigResponseFieldCreatedAt: utils.GetCurrentTimeString(),
|
||||
entity.ConfigResponseFieldUpdatedAt: utils.GetCurrentTimeString(),
|
||||
entity.ConfigResponseFieldSiteTitle: entity.ConfigDefaultSiteTitle,
|
||||
entity.ConfigResponseFieldSiteDescription: entity.ConfigDefaultSiteDescription,
|
||||
entity.ConfigResponseFieldKeywords: entity.ConfigDefaultKeywords,
|
||||
entity.ConfigResponseFieldAuthor: entity.ConfigDefaultAuthor,
|
||||
entity.ConfigResponseFieldCopyright: entity.ConfigDefaultCopyright,
|
||||
"site_logo": "",
|
||||
entity.ConfigResponseFieldAdKeywords: "",
|
||||
entity.ConfigResponseFieldAutoInsertAd: "",
|
||||
entity.ConfigResponseFieldPageSize: 100,
|
||||
entity.ConfigResponseFieldMaintenanceMode: false,
|
||||
entity.ConfigResponseFieldEnableRegister: true, // 默认开启注册功能
|
||||
entity.ConfigResponseFieldThirdPartyStatsCode: "",
|
||||
entity.ConfigResponseFieldWebsiteURL: "",
|
||||
}
|
||||
|
||||
// 将键值对转换为map
|
||||
// 将键值对转换为map,过滤掉敏感配置
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeySiteTitle:
|
||||
@@ -236,32 +322,6 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
response[entity.ConfigResponseFieldCopyright] = config.Value
|
||||
case entity.ConfigKeySiteLogo:
|
||||
response["site_logo"] = config.Value
|
||||
case entity.ConfigKeyAutoProcessReadyResources:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoProcessReadyResources] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoProcessInterval:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoProcessInterval] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoTransferEnabled] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferLimitDays:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoTransferLimitDays] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferMinSpace:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoTransferMinSpace] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoFetchHotDramaEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoFetchHotDramaEnabled] = val
|
||||
}
|
||||
case entity.ConfigKeyForbiddenWords:
|
||||
response[entity.ConfigResponseFieldForbiddenWords] = config.Value
|
||||
case entity.ConfigKeyAdKeywords:
|
||||
response[entity.ConfigResponseFieldAdKeywords] = config.Value
|
||||
case entity.ConfigKeyAutoInsertAd:
|
||||
@@ -280,6 +340,49 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
}
|
||||
case entity.ConfigKeyThirdPartyStatsCode:
|
||||
response[entity.ConfigResponseFieldThirdPartyStatsCode] = config.Value
|
||||
case entity.ConfigKeyEnableAnnouncements:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response["enable_announcements"] = val
|
||||
}
|
||||
case entity.ConfigKeyAnnouncements:
|
||||
if config.Value == "" || config.Value == "[]" {
|
||||
response["announcements"] = ""
|
||||
} else {
|
||||
response["announcements"] = config.Value
|
||||
}
|
||||
case entity.ConfigKeyEnableFloatButtons:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response["enable_float_buttons"] = val
|
||||
}
|
||||
case entity.ConfigKeyWechatSearchImage:
|
||||
response["wechat_search_image"] = config.Value
|
||||
case entity.ConfigKeyTelegramQrImage:
|
||||
response["telegram_qr_image"] = config.Value
|
||||
case entity.ConfigKeyQrCodeStyle:
|
||||
response["qr_code_style"] = config.Value
|
||||
case entity.ConfigKeyWebsiteURL:
|
||||
response[entity.ConfigResponseFieldWebsiteURL] = config.Value
|
||||
case entity.ConfigKeyAutoProcessReadyResources:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response["auto_process_ready_resources"] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response["auto_transfer_enabled"] = val
|
||||
}
|
||||
// 跳过不需要返回给公众的配置
|
||||
case entity.ConfigKeyAutoProcessInterval:
|
||||
case entity.ConfigKeyAutoTransferLimitDays:
|
||||
case entity.ConfigKeyAutoTransferMinSpace:
|
||||
case entity.ConfigKeyAutoFetchHotDramaEnabled:
|
||||
case entity.ConfigKeyMeilisearchEnabled:
|
||||
case entity.ConfigKeyMeilisearchHost:
|
||||
case entity.ConfigKeyMeilisearchPort:
|
||||
case entity.ConfigKeyMeilisearchMasterKey:
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
case entity.ConfigKeyForbiddenWords:
|
||||
// 这些配置不返回给公众
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,5 +418,17 @@ func getDefaultConfigResponse() *dto.SystemConfigResponse {
|
||||
MaintenanceMode: false,
|
||||
EnableRegister: true, // 默认开启注册功能
|
||||
ThirdPartyStatsCode: entity.ConfigDefaultThirdPartyStatsCode,
|
||||
MeilisearchEnabled: false,
|
||||
MeilisearchHost: entity.ConfigDefaultMeilisearchHost,
|
||||
MeilisearchPort: entity.ConfigDefaultMeilisearchPort,
|
||||
MeilisearchMasterKey: entity.ConfigDefaultMeilisearchMasterKey,
|
||||
MeilisearchIndexName: entity.ConfigDefaultMeilisearchIndexName,
|
||||
EnableAnnouncements: false,
|
||||
Announcements: "",
|
||||
EnableFloatButtons: false,
|
||||
WechatSearchImage: entity.ConfigDefaultWechatSearchImage,
|
||||
TelegramQrImage: entity.ConfigDefaultTelegramQrImage,
|
||||
QrCodeStyle: entity.ConfigDefaultQrCodeStyle,
|
||||
SiteURL: entity.ConfigDefaultWebsiteURL,
|
||||
}
|
||||
}
|
||||
|
||||
307
db/converter/telegram_channel_converter.go
Normal file
307
db/converter/telegram_channel_converter.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// TelegramChannelToResponse 将TelegramChannel实体转换为响应DTO
|
||||
func TelegramChannelToResponse(channel entity.TelegramChannel) dto.TelegramChannelResponse {
|
||||
return dto.TelegramChannelResponse{
|
||||
ID: channel.ID,
|
||||
ChatID: channel.ChatID,
|
||||
ChatName: channel.ChatName,
|
||||
ChatType: channel.ChatType,
|
||||
PushEnabled: channel.PushEnabled,
|
||||
PushFrequency: channel.PushFrequency,
|
||||
PushStartTime: channel.PushStartTime,
|
||||
PushEndTime: channel.PushEndTime,
|
||||
ContentCategories: channel.ContentCategories,
|
||||
ContentTags: channel.ContentTags,
|
||||
IsActive: channel.IsActive,
|
||||
ResourceStrategy: channel.ResourceStrategy,
|
||||
TimeLimit: channel.TimeLimit,
|
||||
LastPushAt: channel.LastPushAt,
|
||||
RegisteredBy: channel.RegisteredBy,
|
||||
RegisteredAt: channel.RegisteredAt,
|
||||
}
|
||||
}
|
||||
|
||||
// TelegramChannelsToResponse 将TelegramChannel实体列表转换为响应DTO列表
|
||||
func TelegramChannelsToResponse(channels []entity.TelegramChannel) []dto.TelegramChannelResponse {
|
||||
var responses []dto.TelegramChannelResponse
|
||||
for _, channel := range channels {
|
||||
responses = append(responses, TelegramChannelToResponse(channel))
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// RequestToTelegramChannel 将请求DTO转换为TelegramChannel实体
|
||||
func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy string) entity.TelegramChannel {
|
||||
channel := entity.TelegramChannel{
|
||||
ChatID: req.ChatID,
|
||||
ChatName: req.ChatName,
|
||||
ChatType: req.ChatType,
|
||||
PushEnabled: req.PushEnabled,
|
||||
PushFrequency: req.PushFrequency,
|
||||
PushStartTime: req.PushStartTime,
|
||||
PushEndTime: req.PushEndTime,
|
||||
ContentCategories: req.ContentCategories,
|
||||
ContentTags: req.ContentTags,
|
||||
IsActive: req.IsActive,
|
||||
RegisteredBy: registeredBy,
|
||||
RegisteredAt: time.Now(),
|
||||
}
|
||||
|
||||
// 设置默认值(如果为空)
|
||||
if req.ResourceStrategy == "" {
|
||||
channel.ResourceStrategy = "random"
|
||||
} else {
|
||||
channel.ResourceStrategy = req.ResourceStrategy
|
||||
}
|
||||
|
||||
if req.TimeLimit == "" {
|
||||
channel.TimeLimit = "none"
|
||||
} else {
|
||||
channel.TimeLimit = req.TimeLimit
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// TelegramBotConfigToResponse 将Telegram bot配置转换为响应DTO
|
||||
func TelegramBotConfigToResponse(
|
||||
botEnabled bool,
|
||||
botApiKey string,
|
||||
autoReplyEnabled bool,
|
||||
autoReplyTemplate string,
|
||||
autoDeleteEnabled bool,
|
||||
autoDeleteInterval int,
|
||||
proxyEnabled bool,
|
||||
proxyType string,
|
||||
proxyHost string,
|
||||
proxyPort int,
|
||||
proxyUsername string,
|
||||
proxyPassword string,
|
||||
) dto.TelegramBotConfigResponse {
|
||||
return dto.TelegramBotConfigResponse{
|
||||
BotEnabled: botEnabled,
|
||||
BotApiKey: botApiKey,
|
||||
AutoReplyEnabled: autoReplyEnabled,
|
||||
AutoReplyTemplate: autoReplyTemplate,
|
||||
AutoDeleteEnabled: autoDeleteEnabled,
|
||||
AutoDeleteInterval: autoDeleteInterval,
|
||||
ProxyEnabled: proxyEnabled,
|
||||
ProxyType: proxyType,
|
||||
ProxyHost: proxyHost,
|
||||
ProxyPort: proxyPort,
|
||||
ProxyUsername: proxyUsername,
|
||||
ProxyPassword: proxyPassword,
|
||||
}
|
||||
}
|
||||
|
||||
// SystemConfigToTelegramBotConfig 将系统配置转换为Telegram bot配置响应
|
||||
func SystemConfigToTelegramBotConfig(configs []entity.SystemConfig) dto.TelegramBotConfigResponse {
|
||||
botEnabled := false
|
||||
botApiKey := ""
|
||||
autoReplyEnabled := true
|
||||
autoReplyTemplate := "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
|
||||
autoDeleteEnabled := false
|
||||
autoDeleteInterval := 60
|
||||
proxyEnabled := false
|
||||
proxyType := "http"
|
||||
proxyHost := ""
|
||||
proxyPort := 8080
|
||||
proxyUsername := ""
|
||||
proxyPassword := ""
|
||||
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeyTelegramBotEnabled:
|
||||
botEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramBotApiKey:
|
||||
botApiKey = config.Value
|
||||
case entity.ConfigKeyTelegramAutoReplyEnabled:
|
||||
autoReplyEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramAutoReplyTemplate:
|
||||
autoReplyTemplate = config.Value
|
||||
case entity.ConfigKeyTelegramAutoDeleteEnabled:
|
||||
autoDeleteEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramAutoDeleteInterval:
|
||||
if config.Value != "" {
|
||||
// 简单解析整数,这里可以改进错误处理
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(config.Value, "%d", &val); err == nil {
|
||||
autoDeleteInterval = val
|
||||
}
|
||||
}
|
||||
case entity.ConfigKeyTelegramProxyEnabled:
|
||||
proxyEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyTelegramProxyType:
|
||||
proxyType = config.Value
|
||||
case entity.ConfigKeyTelegramProxyHost:
|
||||
proxyHost = config.Value
|
||||
case entity.ConfigKeyTelegramProxyPort:
|
||||
if config.Value != "" {
|
||||
var val int
|
||||
if _, err := fmt.Sscanf(config.Value, "%d", &val); err == nil {
|
||||
proxyPort = val
|
||||
}
|
||||
}
|
||||
case entity.ConfigKeyTelegramProxyUsername:
|
||||
proxyUsername = config.Value
|
||||
case entity.ConfigKeyTelegramProxyPassword:
|
||||
proxyPassword = config.Value
|
||||
}
|
||||
}
|
||||
|
||||
return TelegramBotConfigToResponse(
|
||||
botEnabled,
|
||||
botApiKey,
|
||||
autoReplyEnabled,
|
||||
autoReplyTemplate,
|
||||
autoDeleteEnabled,
|
||||
autoDeleteInterval,
|
||||
proxyEnabled,
|
||||
proxyType,
|
||||
proxyHost,
|
||||
proxyPort,
|
||||
proxyUsername,
|
||||
proxyPassword,
|
||||
)
|
||||
}
|
||||
|
||||
// TelegramBotConfigRequestToSystemConfigs 将Telegram bot配置请求转换为系统配置实体列表
|
||||
func TelegramBotConfigRequestToSystemConfigs(req dto.TelegramBotConfigRequest) []entity.SystemConfig {
|
||||
configs := []entity.SystemConfig{}
|
||||
|
||||
// 添加调试日志
|
||||
utils.Debug("[TELEGRAM:CONVERTER] 转换请求: %+v", req)
|
||||
|
||||
if req.BotEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramBotEnabled,
|
||||
Value: boolToString(*req.BotEnabled),
|
||||
Type: entity.ConfigTypeBool,
|
||||
})
|
||||
}
|
||||
|
||||
if req.BotApiKey != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramBotApiKey,
|
||||
Value: *req.BotApiKey,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoReplyEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoReplyEnabled,
|
||||
Value: boolToString(*req.AutoReplyEnabled),
|
||||
Type: entity.ConfigTypeBool,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoReplyTemplate != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoReplyTemplate,
|
||||
Value: *req.AutoReplyTemplate,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoDeleteEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoDeleteEnabled,
|
||||
Value: boolToString(*req.AutoDeleteEnabled),
|
||||
Type: entity.ConfigTypeBool,
|
||||
})
|
||||
}
|
||||
|
||||
if req.AutoDeleteInterval != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramAutoDeleteInterval,
|
||||
Value: intToString(*req.AutoDeleteInterval),
|
||||
Type: entity.ConfigTypeInt,
|
||||
})
|
||||
}
|
||||
|
||||
if req.ProxyEnabled != nil {
|
||||
utils.Debug("[TELEGRAM:CONVERTER] 添加代理启用配置: %v", *req.ProxyEnabled)
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramProxyEnabled,
|
||||
Value: boolToString(*req.ProxyEnabled),
|
||||
Type: entity.ConfigTypeBool,
|
||||
})
|
||||
}
|
||||
|
||||
if req.ProxyType != nil {
|
||||
utils.Debug("[TELEGRAM:CONVERTER] 添加代理类型配置: %s", *req.ProxyType)
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramProxyType,
|
||||
Value: *req.ProxyType,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
if req.ProxyHost != nil {
|
||||
utils.Debug("[TELEGRAM:CONVERTER] 添加代理主机配置: %s", *req.ProxyHost)
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramProxyHost,
|
||||
Value: *req.ProxyHost,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
if req.ProxyPort != nil {
|
||||
utils.Debug("[TELEGRAM:CONVERTER] 添加代理端口配置: %d", *req.ProxyPort)
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramProxyPort,
|
||||
Value: intToString(*req.ProxyPort),
|
||||
Type: entity.ConfigTypeInt,
|
||||
})
|
||||
}
|
||||
|
||||
if req.ProxyUsername != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramProxyUsername,
|
||||
Value: *req.ProxyUsername,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
if req.ProxyPassword != nil {
|
||||
configs = append(configs, entity.SystemConfig{
|
||||
Key: entity.ConfigKeyTelegramProxyPassword,
|
||||
Value: *req.ProxyPassword,
|
||||
Type: entity.ConfigTypeString,
|
||||
})
|
||||
}
|
||||
|
||||
utils.Debug("[TELEGRAM:CONVERTER] 转换完成,共生成 %d 个配置项", len(configs))
|
||||
for i, config := range configs {
|
||||
if strings.Contains(config.Key, "proxy") {
|
||||
utils.Debug("[TELEGRAM:CONVERTER] 配置项 %d: %s = %s", i+1, config.Key, config.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
// 辅助函数:布尔转换为字符串
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// 辅助函数:整数转换为字符串
|
||||
func intToString(i int) string {
|
||||
return fmt.Sprintf("%d", i)
|
||||
}
|
||||
88
db/converter/wechat_bot_converter.go
Normal file
88
db/converter/wechat_bot_converter.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// WechatBotConfigRequestToSystemConfigs 将微信机器人配置请求转换为系统配置实体
|
||||
func WechatBotConfigRequestToSystemConfigs(req dto.WechatBotConfigRequest) []entity.SystemConfig {
|
||||
configs := []entity.SystemConfig{
|
||||
{Key: entity.ConfigKeyWechatBotEnabled, Value: wechatBoolToString(req.Enabled)},
|
||||
{Key: entity.ConfigKeyWechatAppId, Value: req.AppID},
|
||||
{Key: entity.ConfigKeyWechatAppSecret, Value: req.AppSecret},
|
||||
{Key: entity.ConfigKeyWechatToken, Value: req.Token},
|
||||
{Key: entity.ConfigKeyWechatEncodingAesKey, Value: req.EncodingAesKey},
|
||||
{Key: entity.ConfigKeyWechatWelcomeMessage, Value: req.WelcomeMessage},
|
||||
{Key: entity.ConfigKeyWechatAutoReplyEnabled, Value: wechatBoolToString(req.AutoReplyEnabled)},
|
||||
{Key: entity.ConfigKeyWechatSearchLimit, Value: wechatIntToString(req.SearchLimit)},
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
// SystemConfigToWechatBotConfig 将系统配置转换为微信机器人配置响应
|
||||
func SystemConfigToWechatBotConfig(configs []entity.SystemConfig) dto.WechatBotConfigResponse {
|
||||
resp := dto.WechatBotConfigResponse{
|
||||
Enabled: false,
|
||||
AppID: "",
|
||||
AppSecret: "",
|
||||
Token: "",
|
||||
EncodingAesKey: "",
|
||||
WelcomeMessage: "欢迎关注老九网盘资源库!发送关键词即可搜索资源。",
|
||||
AutoReplyEnabled: true,
|
||||
SearchLimit: 5,
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeyWechatBotEnabled:
|
||||
resp.Enabled = config.Value == "true"
|
||||
case entity.ConfigKeyWechatAppId:
|
||||
resp.AppID = config.Value
|
||||
case entity.ConfigKeyWechatAppSecret:
|
||||
resp.AppSecret = config.Value
|
||||
case entity.ConfigKeyWechatToken:
|
||||
resp.Token = config.Value
|
||||
case entity.ConfigKeyWechatEncodingAesKey:
|
||||
resp.EncodingAesKey = config.Value
|
||||
case entity.ConfigKeyWechatWelcomeMessage:
|
||||
if config.Value != "" {
|
||||
resp.WelcomeMessage = config.Value
|
||||
}
|
||||
case entity.ConfigKeyWechatAutoReplyEnabled:
|
||||
resp.AutoReplyEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyWechatSearchLimit:
|
||||
if config.Value != "" {
|
||||
resp.SearchLimit = wechatStringToInt(config.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// 辅助函数 - 使用大写名称避免与其他文件中的函数冲突
|
||||
func wechatBoolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func wechatIntToString(i int) string {
|
||||
return strconv.Itoa(i)
|
||||
}
|
||||
|
||||
func wechatStringToInt(s string) int {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
||||
55
db/dto/api_access_log.go
Normal file
55
db/dto/api_access_log.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// APIAccessLogResponse API访问日志响应
|
||||
type APIAccessLogResponse struct {
|
||||
ID uint `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseStatus int `json:"response_status"`
|
||||
ResponseData string `json:"response_data"`
|
||||
ProcessCount int `json:"process_count"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
ProcessingTime int64 `json:"processing_time"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// APIAccessLogSummaryResponse API访问日志汇总响应
|
||||
type APIAccessLogSummaryResponse struct {
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
TodayRequests int64 `json:"today_requests"`
|
||||
WeekRequests int64 `json:"week_requests"`
|
||||
MonthRequests int64 `json:"month_requests"`
|
||||
ErrorRequests int64 `json:"error_requests"`
|
||||
UniqueIPs int64 `json:"unique_ips"`
|
||||
}
|
||||
|
||||
// APIAccessLogStatsResponse 按端点统计响应
|
||||
type APIAccessLogStatsResponse struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
AvgProcessTime int64 `json:"avg_process_time"`
|
||||
LastAccess time.Time `json:"last_access"`
|
||||
}
|
||||
|
||||
// APIAccessLogListResponse API访问日志列表响应
|
||||
type APIAccessLogListResponse struct {
|
||||
Data []APIAccessLogResponse `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// APIAccessLogFilterRequest API访问日志过滤请求
|
||||
type APIAccessLogFilterRequest struct {
|
||||
StartDate string `json:"start_date,omitempty"`
|
||||
EndDate string `json:"end_date,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
Page int `json:"page,omitempty" default:"1"`
|
||||
PageSize int `json:"page_size,omitempty" default:"20"`
|
||||
}
|
||||
46
db/dto/copyright_claim.go
Normal file
46
db/dto/copyright_claim.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package dto
|
||||
|
||||
// CopyrightClaimCreateRequest 版权申述创建请求
|
||||
type CopyrightClaimCreateRequest struct {
|
||||
ResourceKey string `json:"resource_key" validate:"required,max=255"`
|
||||
Identity string `json:"identity" validate:"required,max=50"`
|
||||
ProofType string `json:"proof_type" validate:"required,max=50"`
|
||||
Reason string `json:"reason" validate:"required,max=2000"`
|
||||
ContactInfo string `json:"contact_info" validate:"required,max=255"`
|
||||
ClaimantName string `json:"claimant_name" validate:"required,max=100"`
|
||||
ProofFiles string `json:"proof_files" validate:"omitempty,max=2000"`
|
||||
UserAgent string `json:"user_agent" validate:"omitempty,max=1000"`
|
||||
IPAddress string `json:"ip_address" validate:"omitempty,max=45"`
|
||||
}
|
||||
|
||||
// CopyrightClaimUpdateRequest 版权申述更新请求
|
||||
type CopyrightClaimUpdateRequest struct {
|
||||
Status string `json:"status" validate:"required,oneof=pending approved rejected"`
|
||||
Note string `json:"note" validate:"omitempty,max=1000"`
|
||||
}
|
||||
|
||||
// CopyrightClaimResponse 版权申述响应
|
||||
type CopyrightClaimResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ResourceKey string `json:"resource_key"`
|
||||
Identity string `json:"identity"`
|
||||
ProofType string `json:"proof_type"`
|
||||
Reason string `json:"reason"`
|
||||
ContactInfo string `json:"contact_info"`
|
||||
ClaimantName string `json:"claimant_name"`
|
||||
ProofFiles string `json:"proof_files"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Status string `json:"status"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Resources []ResourceInfo `json:"resources"`
|
||||
}
|
||||
|
||||
// CopyrightClaimListRequest 版权申述列表请求
|
||||
type CopyrightClaimListRequest struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1"`
|
||||
PageSize int `query:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
Status string `query:"status" validate:"omitempty,oneof=pending approved rejected"`
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type HotDramaRequest struct {
|
||||
PosterURL string `json:"poster_url"`
|
||||
Category string `json:"category"`
|
||||
SubType string `json:"sub_type"`
|
||||
Rank int `json:"rank"`
|
||||
Source string `json:"source"`
|
||||
DoubanID string `json:"douban_id"`
|
||||
DoubanURI string `json:"douban_uri"`
|
||||
@@ -41,6 +42,7 @@ type HotDramaResponse struct {
|
||||
PosterURL string `json:"poster_url"`
|
||||
Category string `json:"category"`
|
||||
SubType string `json:"sub_type"`
|
||||
Rank int `json:"rank"`
|
||||
Source string `json:"source"`
|
||||
DoubanID string `json:"douban_id"`
|
||||
DoubanURI string `json:"douban_uri"`
|
||||
|
||||
55
db/dto/report.go
Normal file
55
db/dto/report.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package dto
|
||||
|
||||
// ReportCreateRequest 举报创建请求
|
||||
type ReportCreateRequest struct {
|
||||
ResourceKey string `json:"resource_key" validate:"required,max=255"`
|
||||
Reason string `json:"reason" validate:"required,max=100"`
|
||||
Description string `json:"description" validate:"required,max=1000"`
|
||||
Contact string `json:"contact" validate:"omitempty,max=255"`
|
||||
UserAgent string `json:"user_agent" validate:"omitempty,max=1000"`
|
||||
IPAddress string `json:"ip_address" validate:"omitempty,max=45"`
|
||||
}
|
||||
|
||||
// ReportUpdateRequest 举报更新请求
|
||||
type ReportUpdateRequest struct {
|
||||
Status string `json:"status" validate:"required,oneof=pending approved rejected"`
|
||||
Note string `json:"note" validate:"omitempty,max=1000"`
|
||||
}
|
||||
|
||||
// ResourceInfo 资源信息
|
||||
type ResourceInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
SaveURL string `json:"save_url"`
|
||||
FileSize string `json:"file_size"`
|
||||
Category string `json:"category"`
|
||||
PanName string `json:"pan_name"`
|
||||
ViewCount int `json:"view_count"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ReportResponse 举报响应
|
||||
type ReportResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ResourceKey string `json:"resource_key"`
|
||||
Reason string `json:"reason"`
|
||||
Description string `json:"description"`
|
||||
Contact string `json:"contact"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Status string `json:"status"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Resources []ResourceInfo `json:"resources"` // 关联的资源列表
|
||||
}
|
||||
|
||||
// ReportListRequest 举报列表请求
|
||||
type ReportListRequest struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1"`
|
||||
PageSize int `query:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
Status string `query:"status" validate:"omitempty,oneof=pending approved rejected"`
|
||||
}
|
||||
@@ -12,24 +12,36 @@ type SearchResponse struct {
|
||||
|
||||
// ResourceResponse 资源响应
|
||||
type ResourceResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
PanID *uint `json:"pan_id"`
|
||||
SaveURL string `json:"save_url"`
|
||||
FileSize string `json:"file_size"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
ViewCount int `json:"view_count"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Tags []TagResponse `json:"tags"`
|
||||
Cover string `json:"cover"`
|
||||
Author string `json:"author"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
ID uint `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
PanID *uint `json:"pan_id"`
|
||||
SaveURL string `json:"save_url"`
|
||||
FileSize string `json:"file_size"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
ViewCount int `json:"view_count"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Tags []TagResponse `json:"tags"`
|
||||
Cover string `json:"cover"`
|
||||
Author string `json:"author"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
SyncedToMeilisearch bool `json:"synced_to_meilisearch"`
|
||||
SyncedAt *time.Time `json:"synced_at"`
|
||||
Pan *PanResponse `json:"pan,omitempty"` // 平台信息
|
||||
// 高亮字段
|
||||
TitleHighlight string `json:"title_highlight,omitempty"`
|
||||
DescriptionHighlight string `json:"description_highlight,omitempty"`
|
||||
CategoryHighlight string `json:"category_highlight,omitempty"`
|
||||
TagsHighlight []string `json:"tags_highlight,omitempty"`
|
||||
// 违禁词相关字段
|
||||
HasForbiddenWords bool `json:"has_forbidden_words"`
|
||||
ForbiddenWords []string `json:"forbidden_words"`
|
||||
}
|
||||
|
||||
// CategoryResponse 分类响应
|
||||
@@ -62,19 +74,20 @@ type PanResponse struct {
|
||||
|
||||
// CksResponse Cookie响应
|
||||
type CksResponse struct {
|
||||
ID uint `json:"id"`
|
||||
PanID uint `json:"pan_id"`
|
||||
Idx int `json:"idx"`
|
||||
Ck string `json:"ck"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
Space int64 `json:"space"`
|
||||
LeftSpace int64 `json:"left_space"`
|
||||
UsedSpace int64 `json:"used_space"`
|
||||
Username string `json:"username"`
|
||||
VipStatus bool `json:"vip_status"`
|
||||
ServiceType string `json:"service_type"`
|
||||
Remark string `json:"remark"`
|
||||
Pan *PanResponse `json:"pan,omitempty"`
|
||||
ID uint `json:"id"`
|
||||
PanID uint `json:"pan_id"`
|
||||
Idx int `json:"idx"`
|
||||
Ck string `json:"ck"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
Space int64 `json:"space"`
|
||||
LeftSpace int64 `json:"left_space"`
|
||||
UsedSpace int64 `json:"used_space"`
|
||||
Username string `json:"username"`
|
||||
VipStatus bool `json:"vip_status"`
|
||||
ServiceType string `json:"service_type"`
|
||||
Remark string `json:"remark"`
|
||||
TransferredCount int64 `json:"transferred_count"` // 已转存资源数
|
||||
Pan *PanResponse `json:"pan,omitempty"`
|
||||
}
|
||||
|
||||
// ReadyResourceResponse 待处理资源响应
|
||||
|
||||
@@ -35,6 +35,24 @@ type SystemConfigRequest struct {
|
||||
|
||||
// 三方统计配置
|
||||
ThirdPartyStatsCode *string `json:"third_party_stats_code,omitempty"` // 三方统计代码
|
||||
|
||||
// Meilisearch配置
|
||||
MeilisearchEnabled *bool `json:"meilisearch_enabled,omitempty"`
|
||||
MeilisearchHost *string `json:"meilisearch_host,omitempty"`
|
||||
MeilisearchPort *string `json:"meilisearch_port,omitempty"`
|
||||
MeilisearchMasterKey *string `json:"meilisearch_master_key,omitempty"`
|
||||
MeilisearchIndexName *string `json:"meilisearch_index_name,omitempty"`
|
||||
|
||||
// 界面配置
|
||||
EnableAnnouncements *bool `json:"enable_announcements,omitempty"`
|
||||
Announcements *[]map[string]interface{} `json:"announcements,omitempty"`
|
||||
EnableFloatButtons *bool `json:"enable_float_buttons,omitempty"`
|
||||
WechatSearchImage *string `json:"wechat_search_image,omitempty"`
|
||||
TelegramQrImage *string `json:"telegram_qr_image,omitempty"`
|
||||
QrCodeStyle *string `json:"qr_code_style,omitempty"`
|
||||
|
||||
// 网站URL配置
|
||||
SiteURL *string `json:"site_url,omitempty"`
|
||||
}
|
||||
|
||||
// SystemConfigResponse 系统配置响应
|
||||
@@ -76,6 +94,24 @@ type SystemConfigResponse struct {
|
||||
|
||||
// 三方统计配置
|
||||
ThirdPartyStatsCode string `json:"third_party_stats_code"` // 三方统计代码
|
||||
|
||||
// Meilisearch配置
|
||||
MeilisearchEnabled bool `json:"meilisearch_enabled"`
|
||||
MeilisearchHost string `json:"meilisearch_host"`
|
||||
MeilisearchPort string `json:"meilisearch_port"`
|
||||
MeilisearchMasterKey string `json:"meilisearch_master_key"`
|
||||
MeilisearchIndexName string `json:"meilisearch_index_name"`
|
||||
|
||||
// 界面配置
|
||||
EnableAnnouncements bool `json:"enable_announcements"`
|
||||
Announcements string `json:"announcements"`
|
||||
EnableFloatButtons bool `json:"enable_float_buttons"`
|
||||
WechatSearchImage string `json:"wechat_search_image"`
|
||||
TelegramQrImage string `json:"telegram_qr_image"`
|
||||
QrCodeStyle string `json:"qr_code_style"`
|
||||
|
||||
// 网站URL配置
|
||||
SiteURL string `json:"site_url"`
|
||||
}
|
||||
|
||||
// SystemConfigItem 单个配置项
|
||||
|
||||
105
db/dto/telegram_channel.go
Normal file
105
db/dto/telegram_channel.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// TelegramChannelRequest 创建 Telegram 频道/群组请求
|
||||
type TelegramChannelRequest struct {
|
||||
ChatID int64 `json:"chat_id" binding:"required"`
|
||||
ChatName string `json:"chat_name" binding:"required"`
|
||||
ChatType string `json:"chat_type" binding:"required"` // channel 或 group
|
||||
PushEnabled bool `json:"push_enabled"`
|
||||
PushFrequency int `json:"push_frequency"`
|
||||
PushStartTime string `json:"push_start_time"`
|
||||
PushEndTime string `json:"push_end_time"`
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ResourceStrategy string `json:"resource_strategy"`
|
||||
TimeLimit string `json:"time_limit"`
|
||||
}
|
||||
|
||||
// TelegramChannelUpdateRequest 更新 Telegram 频道/群组请求(ChatID可选)
|
||||
type TelegramChannelUpdateRequest struct {
|
||||
ChatID int64 `json:"chat_id"` // 可选,用于验证
|
||||
ChatName string `json:"chat_name" binding:"required"`
|
||||
ChatType string `json:"chat_type" binding:"required"` // channel 或 group
|
||||
PushEnabled bool `json:"push_enabled"`
|
||||
PushFrequency int `json:"push_frequency"`
|
||||
PushStartTime string `json:"push_start_time"`
|
||||
PushEndTime string `json:"push_end_time"`
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ResourceStrategy string `json:"resource_strategy"`
|
||||
TimeLimit string `json:"time_limit"`
|
||||
}
|
||||
|
||||
// TelegramChannelResponse Telegram 频道/群组响应
|
||||
type TelegramChannelResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ChatID int64 `json:"chat_id"`
|
||||
ChatName string `json:"chat_name"`
|
||||
ChatType string `json:"chat_type"`
|
||||
PushEnabled bool `json:"push_enabled"`
|
||||
PushFrequency int `json:"push_frequency"`
|
||||
PushStartTime string `json:"push_start_time"`
|
||||
PushEndTime string `json:"push_end_time"`
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ResourceStrategy string `json:"resource_strategy"`
|
||||
TimeLimit string `json:"time_limit"`
|
||||
LastPushAt *time.Time `json:"last_push_at"`
|
||||
RegisteredBy string `json:"registered_by"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
}
|
||||
|
||||
// TelegramBotConfigRequest Telegram 机器人配置请求
|
||||
type TelegramBotConfigRequest struct {
|
||||
BotEnabled *bool `json:"bot_enabled"`
|
||||
BotApiKey *string `json:"bot_api_key"`
|
||||
AutoReplyEnabled *bool `json:"auto_reply_enabled"`
|
||||
AutoReplyTemplate *string `json:"auto_reply_template"`
|
||||
AutoDeleteEnabled *bool `json:"auto_delete_enabled"`
|
||||
AutoDeleteInterval *int `json:"auto_delete_interval"`
|
||||
ProxyEnabled *bool `json:"proxy_enabled"`
|
||||
ProxyType *string `json:"proxy_type"`
|
||||
ProxyHost *string `json:"proxy_host"`
|
||||
ProxyPort *int `json:"proxy_port"`
|
||||
ProxyUsername *string `json:"proxy_username"`
|
||||
ProxyPassword *string `json:"proxy_password"`
|
||||
}
|
||||
|
||||
// TelegramBotConfigResponse Telegram 机器人配置响应
|
||||
type TelegramBotConfigResponse struct {
|
||||
BotEnabled bool `json:"bot_enabled"`
|
||||
BotApiKey string `json:"bot_api_key"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
AutoReplyTemplate string `json:"auto_reply_template"`
|
||||
AutoDeleteEnabled bool `json:"auto_delete_enabled"`
|
||||
AutoDeleteInterval int `json:"auto_delete_interval"`
|
||||
ProxyEnabled bool `json:"proxy_enabled"`
|
||||
ProxyType string `json:"proxy_type"`
|
||||
ProxyHost string `json:"proxy_host"`
|
||||
ProxyPort int `json:"proxy_port"`
|
||||
ProxyUsername string `json:"proxy_username"`
|
||||
ProxyPassword string `json:"proxy_password"`
|
||||
}
|
||||
|
||||
// ValidateTelegramApiKeyRequest 验证 Telegram API Key 请求
|
||||
type ValidateTelegramApiKeyRequest struct {
|
||||
ApiKey string `json:"api_key" binding:"required"`
|
||||
ProxyEnabled bool `json:"proxy_enabled"`
|
||||
ProxyType string `json:"proxy_type"`
|
||||
ProxyHost string `json:"proxy_host"`
|
||||
ProxyPort int `json:"proxy_port"`
|
||||
ProxyUsername string `json:"proxy_username"`
|
||||
ProxyPassword string `json:"proxy_password"`
|
||||
}
|
||||
|
||||
// ValidateTelegramApiKeyResponse 验证 Telegram API Key 响应
|
||||
type ValidateTelegramApiKeyResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Error string `json:"error,omitempty"`
|
||||
BotInfo map[string]interface{} `json:"bot_info,omitempty"`
|
||||
}
|
||||
25
db/dto/wechat_bot.go
Normal file
25
db/dto/wechat_bot.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package dto
|
||||
|
||||
// WechatBotConfigRequest 微信公众号机器人配置请求
|
||||
type WechatBotConfigRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"app_id"`
|
||||
AppSecret string `json:"app_secret"`
|
||||
Token string `json:"token"`
|
||||
EncodingAesKey string `json:"encoding_aes_key"`
|
||||
WelcomeMessage string `json:"welcome_message"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
SearchLimit int `json:"search_limit"`
|
||||
}
|
||||
|
||||
// WechatBotConfigResponse 微信公众号机器人配置响应
|
||||
type WechatBotConfigResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"app_id"`
|
||||
AppSecret string `json:"app_secret"`
|
||||
Token string `json:"token"`
|
||||
EncodingAesKey string `json:"encoding_aes_key"`
|
||||
WelcomeMessage string `json:"welcome_message"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
SearchLimit int `json:"search_limit"`
|
||||
}
|
||||
50
db/entity/api_access_log.go
Normal file
50
db/entity/api_access_log.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// APIAccessLog API访问日志模型
|
||||
type APIAccessLog struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
IP string `json:"ip" gorm:"size:45;not null;comment:客户端IP地址"`
|
||||
UserAgent string `json:"user_agent" gorm:"size:500;comment:用户代理"`
|
||||
Endpoint string `json:"endpoint" gorm:"size:255;not null;comment:访问的接口路径"`
|
||||
Method string `json:"method" gorm:"size:10;not null;comment:HTTP方法"`
|
||||
RequestParams string `json:"request_params" gorm:"type:text;comment:查询参数(JSON格式)"`
|
||||
ResponseStatus int `json:"response_status" gorm:"default:200;comment:响应状态码"`
|
||||
ResponseData string `json:"response_data" gorm:"type:text;comment:响应数据(JSON格式)"`
|
||||
ProcessCount int `json:"process_count" gorm:"default:0;comment:处理数量(查询结果数或添加的数量)"`
|
||||
ErrorMessage string `json:"error_message" gorm:"size:500;comment:错误消息"`
|
||||
ProcessingTime int64 `json:"processing_time" gorm:"comment:处理时间(毫秒)"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (APIAccessLog) TableName() string {
|
||||
return "api_access_logs"
|
||||
}
|
||||
|
||||
// APIAccessLogSummary API访问日志汇总统计
|
||||
type APIAccessLogSummary struct {
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
TodayRequests int64 `json:"today_requests"`
|
||||
WeekRequests int64 `json:"week_requests"`
|
||||
MonthRequests int64 `json:"month_requests"`
|
||||
ErrorRequests int64 `json:"error_requests"`
|
||||
UniqueIPs int64 `json:"unique_ips"`
|
||||
}
|
||||
|
||||
// APIAccessLogStats 按端点统计
|
||||
type APIAccessLogStats struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
AvgProcessTime int64 `json:"avg_process_time"`
|
||||
LastAccess time.Time `json:"last_access"`
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type Cks struct {
|
||||
VipStatus bool `json:"vip_status" gorm:"default:false;comment:VIP状态"`
|
||||
ServiceType string `json:"service_type" gorm:"size:20;comment:服务类型"`
|
||||
Remark string `json:"remark" gorm:"size:64;not null;comment:备注"`
|
||||
Extra string `json:"extra" gorm:"type:text;comment:额外的中间数据如token等"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
|
||||
32
db/entity/copyright_claim.go
Normal file
32
db/entity/copyright_claim.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CopyrightClaim 版权申述实体
|
||||
type CopyrightClaim struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ResourceKey string `gorm:"type:varchar(255);not null;index" json:"resource_key"` // 资源唯一标识
|
||||
Identity string `gorm:"type:varchar(50);not null" json:"identity"` // 申述人身份
|
||||
ProofType string `gorm:"type:varchar(50);not null" json:"proof_type"` // 证明类型
|
||||
Reason string `gorm:"type:text;not null" json:"reason"` // 申述理由
|
||||
ContactInfo string `gorm:"type:varchar(255);not null" json:"contact_info"` // 联系信息
|
||||
ClaimantName string `gorm:"type:varchar(100);not null" json:"claimant_name"` // 申述人姓名
|
||||
ProofFiles string `gorm:"type:text" json:"proof_files"` // 证明文件(JSON格式)
|
||||
UserAgent string `gorm:"type:text" json:"user_agent"` // 用户代理
|
||||
IPAddress string `gorm:"type:varchar(45)" json:"ip_address"` // IP地址
|
||||
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // 处理状态: pending, approved, rejected
|
||||
ProcessedAt *time.Time `json:"processed_at"` // 处理时间
|
||||
ProcessedBy *uint `json:"processed_by"` // 处理人ID
|
||||
Note string `gorm:"type:text" json:"note"` // 处理备注
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (CopyrightClaim) TableName() string {
|
||||
return "copyright_claims"
|
||||
}
|
||||
@@ -27,6 +27,7 @@ type HotDrama struct {
|
||||
// 分类信息
|
||||
Category string `json:"category" gorm:"size:50"` // 分类(电影/电视剧)
|
||||
SubType string `json:"sub_type" gorm:"size:50"` // 子类型(华语/欧美/韩国/日本等)
|
||||
Rank int `json:"rank" gorm:"default:0"` // 排序(豆瓣返回顺序)
|
||||
|
||||
// 数据来源
|
||||
Source string `json:"source" gorm:"size:50;default:'douban'"` // 数据来源
|
||||
|
||||
29
db/entity/report.go
Normal file
29
db/entity/report.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Report 举报实体
|
||||
type Report struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ResourceKey string `gorm:"type:varchar(255);not null;index" json:"resource_key"` // 资源唯一标识
|
||||
Reason string `gorm:"type:varchar(100);not null" json:"reason"` // 举报原因
|
||||
Description string `gorm:"type:text" json:"description"` // 详细描述
|
||||
Contact string `gorm:"type:varchar(255)" json:"contact"` // 联系方式
|
||||
UserAgent string `gorm:"type:text" json:"user_agent"` // 用户代理
|
||||
IPAddress string `gorm:"type:varchar(45)" json:"ip_address"` // IP地址
|
||||
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // 处理状态: pending, approved, rejected
|
||||
ProcessedAt *time.Time `json:"processed_at"` // 处理时间
|
||||
ProcessedBy *uint `json:"processed_by"` // 处理人ID
|
||||
Note string `gorm:"type:text" json:"note"` // 处理备注
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (Report) TableName() string {
|
||||
return "reports"
|
||||
}
|
||||
@@ -8,26 +8,28 @@ import (
|
||||
|
||||
// Resource 资源模型
|
||||
type Resource struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Title string `json:"title" gorm:"size:255;not null;comment:资源标题"`
|
||||
Description string `json:"description" gorm:"type:text;comment:资源描述"`
|
||||
URL string `json:"url" gorm:"size:128;comment:资源链接"`
|
||||
PanID *uint `json:"pan_id" gorm:"comment:平台ID"`
|
||||
SaveURL string `json:"save_url" gorm:"size:500;comment:转存后的链接"`
|
||||
FileSize string `json:"file_size" gorm:"size:100;comment:文件大小"`
|
||||
CategoryID *uint `json:"category_id" gorm:"comment:分类ID"`
|
||||
ViewCount int `json:"view_count" gorm:"default:0;comment:浏览次数"`
|
||||
IsValid bool `json:"is_valid" gorm:"default:true;comment:是否有效"`
|
||||
IsPublic bool `json:"is_public" gorm:"default:true;comment:是否公开"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
Cover string `json:"cover" gorm:"size:500;comment:封面"`
|
||||
Author string `json:"author" gorm:"size:100;comment:作者"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"size:255;comment:转存失败原因"`
|
||||
CkID *uint `json:"ck_id" gorm:"comment:账号ID"`
|
||||
Fid string `json:"fid" gorm:"size:128;comment:网盘文件ID"`
|
||||
Key string `json:"key" gorm:"size:64;index;comment:资源组标识,相同key表示同一组资源"`
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Title string `json:"title" gorm:"size:255;not null;comment:资源标题"`
|
||||
Description string `json:"description" gorm:"type:text;comment:资源描述"`
|
||||
URL string `json:"url" gorm:"size:128;comment:资源链接"`
|
||||
PanID *uint `json:"pan_id" gorm:"comment:平台ID"`
|
||||
SaveURL string `json:"save_url" gorm:"size:500;comment:转存后的链接"`
|
||||
FileSize string `json:"file_size" gorm:"size:100;comment:文件大小"`
|
||||
CategoryID *uint `json:"category_id" gorm:"comment:分类ID"`
|
||||
ViewCount int `json:"view_count" gorm:"default:0;comment:浏览次数"`
|
||||
IsValid bool `json:"is_valid" gorm:"default:true;comment:是否有效"`
|
||||
IsPublic bool `json:"is_public" gorm:"default:true;comment:是否公开"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
Cover string `json:"cover" gorm:"size:500;comment:封面"`
|
||||
Author string `json:"author" gorm:"size:100;comment:作者"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"size:255;comment:转存失败原因"`
|
||||
CkID *uint `json:"ck_id" gorm:"comment:账号ID"`
|
||||
Fid string `json:"fid" gorm:"size:128;comment:网盘文件ID"`
|
||||
Key string `json:"key" gorm:"size:64;index;comment:资源组标识,相同key表示同一组资源"`
|
||||
SyncedToMeilisearch bool `json:"synced_to_meilisearch" gorm:"default:false;comment:是否已同步到Meilisearch"`
|
||||
SyncedAt *time.Time `json:"synced_at" gorm:"comment:同步时间"`
|
||||
|
||||
// 关联关系
|
||||
Category Category `json:"category" gorm:"foreignKey:CategoryID"`
|
||||
@@ -39,3 +41,23 @@ type Resource struct {
|
||||
func (Resource) TableName() string {
|
||||
return "resources"
|
||||
}
|
||||
|
||||
// GetTitle 获取资源标题(实现utils.Resource接口)
|
||||
func (r *Resource) GetTitle() string {
|
||||
return r.Title
|
||||
}
|
||||
|
||||
// GetDescription 获取资源描述(实现utils.Resource接口)
|
||||
func (r *Resource) GetDescription() string {
|
||||
return r.Description
|
||||
}
|
||||
|
||||
// SetTitle 设置资源标题(实现utils.Resource接口)
|
||||
func (r *Resource) SetTitle(title string) {
|
||||
r.Title = title
|
||||
}
|
||||
|
||||
// SetDescription 设置资源描述(实现utils.Resource接口)
|
||||
func (r *Resource) SetDescription(description string) {
|
||||
r.Description = description
|
||||
}
|
||||
|
||||
@@ -35,6 +35,53 @@ const (
|
||||
|
||||
// 三方统计配置
|
||||
ConfigKeyThirdPartyStatsCode = "third_party_stats_code"
|
||||
|
||||
// Meilisearch配置
|
||||
ConfigKeyMeilisearchEnabled = "meilisearch_enabled"
|
||||
ConfigKeyMeilisearchHost = "meilisearch_host"
|
||||
ConfigKeyMeilisearchPort = "meilisearch_port"
|
||||
ConfigKeyMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigKeyMeilisearchIndexName = "meilisearch_index_name"
|
||||
|
||||
// Telegram配置
|
||||
ConfigKeyTelegramBotEnabled = "telegram_bot_enabled"
|
||||
ConfigKeyTelegramBotApiKey = "telegram_bot_api_key"
|
||||
ConfigKeyTelegramAutoReplyEnabled = "telegram_auto_reply_enabled"
|
||||
ConfigKeyTelegramAutoReplyTemplate = "telegram_auto_reply_template"
|
||||
ConfigKeyTelegramAutoDeleteEnabled = "telegram_auto_delete_enabled"
|
||||
ConfigKeyTelegramAutoDeleteInterval = "telegram_auto_delete_interval"
|
||||
ConfigKeyTelegramProxyEnabled = "telegram_proxy_enabled"
|
||||
ConfigKeyTelegramProxyType = "telegram_proxy_type"
|
||||
ConfigKeyTelegramProxyHost = "telegram_proxy_host"
|
||||
ConfigKeyTelegramProxyPort = "telegram_proxy_port"
|
||||
ConfigKeyTelegramProxyUsername = "telegram_proxy_username"
|
||||
ConfigKeyTelegramProxyPassword = "telegram_proxy_password"
|
||||
|
||||
// 微信公众号配置
|
||||
ConfigKeyWechatBotEnabled = "wechat_bot_enabled"
|
||||
ConfigKeyWechatAppId = "wechat_app_id"
|
||||
ConfigKeyWechatAppSecret = "wechat_app_secret"
|
||||
ConfigKeyWechatToken = "wechat_token"
|
||||
ConfigKeyWechatEncodingAesKey = "wechat_encoding_aes_key"
|
||||
ConfigKeyWechatWelcomeMessage = "wechat_welcome_message"
|
||||
ConfigKeyWechatAutoReplyEnabled = "wechat_auto_reply_enabled"
|
||||
ConfigKeyWechatSearchLimit = "wechat_search_limit"
|
||||
|
||||
// 界面配置
|
||||
ConfigKeyEnableAnnouncements = "enable_announcements"
|
||||
ConfigKeyAnnouncements = "announcements"
|
||||
ConfigKeyEnableFloatButtons = "enable_float_buttons"
|
||||
ConfigKeyWechatSearchImage = "wechat_search_image"
|
||||
ConfigKeyTelegramQrImage = "telegram_qr_image"
|
||||
ConfigKeyQrCodeStyle = "qr_code_style"
|
||||
|
||||
// Sitemap配置
|
||||
ConfigKeySitemapConfig = "sitemap_config"
|
||||
ConfigKeySitemapLastGenerateTime = "sitemap_last_generate_time"
|
||||
ConfigKeySitemapAutoGenerateEnabled = "sitemap_auto_generate_enabled"
|
||||
|
||||
// 网站URL配置
|
||||
ConfigKeyWebsiteURL = "website_url"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
@@ -84,6 +131,48 @@ const (
|
||||
|
||||
// 三方统计配置字段
|
||||
ConfigResponseFieldThirdPartyStatsCode = "third_party_stats_code"
|
||||
|
||||
// Meilisearch配置字段
|
||||
ConfigResponseFieldMeilisearchEnabled = "meilisearch_enabled"
|
||||
ConfigResponseFieldMeilisearchHost = "meilisearch_host"
|
||||
ConfigResponseFieldMeilisearchPort = "meilisearch_port"
|
||||
ConfigResponseFieldMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigResponseFieldMeilisearchIndexName = "meilisearch_index_name"
|
||||
|
||||
// Telegram配置字段
|
||||
ConfigResponseFieldTelegramBotEnabled = "telegram_bot_enabled"
|
||||
ConfigResponseFieldTelegramBotApiKey = "telegram_bot_api_key"
|
||||
ConfigResponseFieldTelegramAutoReplyEnabled = "telegram_auto_reply_enabled"
|
||||
ConfigResponseFieldTelegramAutoReplyTemplate = "telegram_auto_reply_template"
|
||||
ConfigResponseFieldTelegramAutoDeleteEnabled = "telegram_auto_delete_enabled"
|
||||
ConfigResponseFieldTelegramAutoDeleteInterval = "telegram_auto_delete_interval"
|
||||
ConfigResponseFieldTelegramProxyEnabled = "telegram_proxy_enabled"
|
||||
ConfigResponseFieldTelegramProxyType = "telegram_proxy_type"
|
||||
ConfigResponseFieldTelegramProxyHost = "telegram_proxy_host"
|
||||
ConfigResponseFieldTelegramProxyPort = "telegram_proxy_port"
|
||||
ConfigResponseFieldTelegramProxyUsername = "telegram_proxy_username"
|
||||
ConfigResponseFieldTelegramProxyPassword = "telegram_proxy_password"
|
||||
|
||||
// 微信公众号配置字段
|
||||
ConfigResponseFieldWechatBotEnabled = "wechat_bot_enabled"
|
||||
ConfigResponseFieldWechatAppId = "wechat_app_id"
|
||||
ConfigResponseFieldWechatAppSecret = "wechat_app_secret"
|
||||
ConfigResponseFieldWechatToken = "wechat_token"
|
||||
ConfigResponseFieldWechatEncodingAesKey = "wechat_encoding_aes_key"
|
||||
ConfigResponseFieldWechatWelcomeMessage = "wechat_welcome_message"
|
||||
ConfigResponseFieldWechatAutoReplyEnabled = "wechat_auto_reply_enabled"
|
||||
ConfigResponseFieldWechatSearchLimit = "wechat_search_limit"
|
||||
|
||||
// 界面配置字段
|
||||
ConfigResponseFieldEnableAnnouncements = "enable_announcements"
|
||||
ConfigResponseFieldAnnouncements = "announcements"
|
||||
ConfigResponseFieldEnableFloatButtons = "enable_float_buttons"
|
||||
ConfigResponseFieldWechatSearchImage = "wechat_search_image"
|
||||
ConfigResponseFieldTelegramQrImage = "telegram_qr_image"
|
||||
ConfigResponseFieldQrCodeStyle = "qr_code_style"
|
||||
|
||||
// 网站URL配置字段
|
||||
ConfigResponseFieldWebsiteURL = "site_url"
|
||||
)
|
||||
|
||||
// ConfigDefaultValue 配置默认值常量
|
||||
@@ -120,4 +209,46 @@ const (
|
||||
|
||||
// 三方统计配置默认值
|
||||
ConfigDefaultThirdPartyStatsCode = ""
|
||||
|
||||
// Meilisearch配置默认值
|
||||
ConfigDefaultMeilisearchEnabled = "false"
|
||||
ConfigDefaultMeilisearchHost = "localhost"
|
||||
ConfigDefaultMeilisearchPort = "7700"
|
||||
ConfigDefaultMeilisearchMasterKey = ""
|
||||
ConfigDefaultMeilisearchIndexName = "resources"
|
||||
|
||||
// Telegram配置默认值
|
||||
ConfigDefaultTelegramBotEnabled = "false"
|
||||
ConfigDefaultTelegramBotApiKey = ""
|
||||
ConfigDefaultTelegramAutoReplyEnabled = "true"
|
||||
ConfigDefaultTelegramAutoReplyTemplate = "您好!我可以帮您搜索网盘资源,请输入您要搜索的内容。"
|
||||
ConfigDefaultTelegramAutoDeleteEnabled = "false"
|
||||
ConfigDefaultTelegramAutoDeleteInterval = "60"
|
||||
ConfigDefaultTelegramProxyEnabled = "false"
|
||||
ConfigDefaultTelegramProxyType = "http"
|
||||
ConfigDefaultTelegramProxyHost = ""
|
||||
ConfigDefaultTelegramProxyPort = "8080"
|
||||
ConfigDefaultTelegramProxyUsername = ""
|
||||
ConfigDefaultTelegramProxyPassword = ""
|
||||
|
||||
// 微信公众号配置默认值
|
||||
ConfigDefaultWechatBotEnabled = "false"
|
||||
ConfigDefaultWechatAppId = ""
|
||||
ConfigDefaultWechatAppSecret = ""
|
||||
ConfigDefaultWechatToken = ""
|
||||
ConfigDefaultWechatEncodingAesKey = ""
|
||||
ConfigDefaultWechatWelcomeMessage = "欢迎关注老九网盘资源库!发送关键词即可搜索资源。"
|
||||
ConfigDefaultWechatAutoReplyEnabled = "true"
|
||||
ConfigDefaultWechatSearchLimit = "5"
|
||||
|
||||
// 界面配置默认值
|
||||
ConfigDefaultEnableAnnouncements = "false"
|
||||
ConfigDefaultAnnouncements = ""
|
||||
ConfigDefaultEnableFloatButtons = "false"
|
||||
ConfigDefaultWechatSearchImage = ""
|
||||
ConfigDefaultTelegramQrImage = ""
|
||||
ConfigDefaultQrCodeStyle = "Plain"
|
||||
|
||||
// 网站URL配置默认值
|
||||
ConfigDefaultWebsiteURL = ""
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ type TaskType string
|
||||
|
||||
const (
|
||||
TaskTypeBatchTransfer TaskType = "batch_transfer" // 批量转存
|
||||
TaskTypeExpansion TaskType = "expansion" // 账号扩容
|
||||
)
|
||||
|
||||
// Task 任务表
|
||||
|
||||
48
db/entity/telegram_channel.go
Normal file
48
db/entity/telegram_channel.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TelegramChannel Telegram 频道/群组实体
|
||||
type TelegramChannel struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Telegram 频道/群组信息
|
||||
ChatID int64 `json:"chat_id" gorm:"not null;comment:Telegram 聊天ID"`
|
||||
ChatName string `json:"chat_name" gorm:"size:255;not null;comment:聊天名称"`
|
||||
ChatType string `json:"chat_type" gorm:"size:50;not null;comment:类型:channel/group"`
|
||||
|
||||
// 推送配置
|
||||
PushEnabled bool `json:"push_enabled" gorm:"default:true;comment:是否启用推送"`
|
||||
PushFrequency int `json:"push_frequency" gorm:"default:5;comment:推送频率(分钟)"`
|
||||
PushStartTime string `json:"push_start_time" gorm:"size:10;comment:推送开始时间,格式HH:mm"`
|
||||
PushEndTime string `json:"push_end_time" gorm:"size:10;comment:推送结束时间,格式HH:mm"`
|
||||
ContentCategories string `json:"content_categories" gorm:"type:text;comment:推送的内容分类,用逗号分隔"`
|
||||
ContentTags string `json:"content_tags" gorm:"type:text;comment:推送的标签,用逗号分隔"`
|
||||
|
||||
// 频道状态
|
||||
IsActive bool `json:"is_active" gorm:"default:true;comment:是否活跃"`
|
||||
LastPushAt *time.Time `json:"last_push_at" gorm:"comment:最后推送时间"`
|
||||
|
||||
// 注册信息
|
||||
RegisteredBy string `json:"registered_by" gorm:"size:100;comment:注册者用户名"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
|
||||
// API配置
|
||||
API string `json:"api" gorm:"size:255;comment:API地址"`
|
||||
Token string `json:"token" gorm:"size:255;comment:访问令牌"`
|
||||
ApiType string `json:"api_type" gorm:"size:50;comment:API类型"`
|
||||
IsPushSavedInfo bool `json:"is_push_saved_info" gorm:"default:false;comment:是否只推送已转存资源"`
|
||||
|
||||
// 资源策略和时间限制配置
|
||||
ResourceStrategy string `json:"resource_strategy" gorm:"size:20;default:'random';comment:资源策略:latest-最新优先,transferred-已转存优先,random-纯随机"`
|
||||
TimeLimit string `json:"time_limit" gorm:"size:20;default:'none';comment:时间限制:none-无限制,week-一周内,month-一月内"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (TelegramChannel) TableName() string {
|
||||
return "telegram_channels"
|
||||
}
|
||||
169
db/repo/api_access_log_repository.go
Normal file
169
db/repo/api_access_log_repository.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// APIAccessLogRepository API访问日志Repository接口
|
||||
type APIAccessLogRepository interface {
|
||||
BaseRepository[entity.APIAccessLog]
|
||||
RecordAccess(ip, userAgent, endpoint, method string, requestParams interface{}, responseStatus int, responseData interface{}, processCount int, errorMessage string, processingTime int64) error
|
||||
GetSummary() (*entity.APIAccessLogSummary, error)
|
||||
GetStatsByEndpoint() ([]entity.APIAccessLogStats, error)
|
||||
FindWithFilters(page, limit int, startDate, endDate *time.Time, endpoint, ip string) ([]entity.APIAccessLog, int64, error)
|
||||
ClearOldLogs(days int) error
|
||||
}
|
||||
|
||||
// APIAccessLogRepositoryImpl API访问日志Repository实现
|
||||
type APIAccessLogRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.APIAccessLog]
|
||||
}
|
||||
|
||||
// NewAPIAccessLogRepository 创建API访问日志Repository
|
||||
func NewAPIAccessLogRepository(db *gorm.DB) APIAccessLogRepository {
|
||||
return &APIAccessLogRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.APIAccessLog]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// RecordAccess 记录API访问
|
||||
func (r *APIAccessLogRepositoryImpl) RecordAccess(ip, userAgent, endpoint, method string, requestParams interface{}, responseStatus int, responseData interface{}, processCount int, errorMessage string, processingTime int64) error {
|
||||
log := entity.APIAccessLog{
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
Endpoint: endpoint,
|
||||
Method: method,
|
||||
ResponseStatus: responseStatus,
|
||||
ProcessCount: processCount,
|
||||
ErrorMessage: errorMessage,
|
||||
ProcessingTime: processingTime,
|
||||
}
|
||||
|
||||
// 序列化请求参数
|
||||
if requestParams != nil {
|
||||
if paramsJSON, err := json.Marshal(requestParams); err == nil {
|
||||
log.RequestParams = string(paramsJSON)
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化响应数据(限制大小,避免存储大量数据)
|
||||
if responseData != nil {
|
||||
if dataJSON, err := json.Marshal(responseData); err == nil {
|
||||
// 限制响应数据长度,避免存储过多数据
|
||||
dataStr := string(dataJSON)
|
||||
if len(dataStr) > 2000 {
|
||||
dataStr = dataStr[:2000] + "..."
|
||||
}
|
||||
log.ResponseData = dataStr
|
||||
}
|
||||
}
|
||||
|
||||
return r.db.Create(&log).Error
|
||||
}
|
||||
|
||||
// GetSummary 获取访问日志汇总
|
||||
func (r *APIAccessLogRepositoryImpl) GetSummary() (*entity.APIAccessLogSummary, error) {
|
||||
var summary entity.APIAccessLogSummary
|
||||
now := utils.GetCurrentTime()
|
||||
todayStr := now.Format(utils.TimeFormatDate)
|
||||
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format(utils.TimeFormatDate)
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
|
||||
// 总请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Count(&summary.TotalRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 今日请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("DATE(created_at) = ?", todayStr).Count(&summary.TodayRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 本周请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("created_at >= ?", weekStart).Count(&summary.WeekRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 本月请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("created_at >= ?", monthStart).Count(&summary.MonthRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 错误请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("response_status >= 400").Count(&summary.ErrorRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 唯一IP数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Distinct("ip").Count(&summary.UniqueIPs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
// GetStatsByEndpoint 按端点获取统计
|
||||
func (r *APIAccessLogRepositoryImpl) GetStatsByEndpoint() ([]entity.APIAccessLogStats, error) {
|
||||
var stats []entity.APIAccessLogStats
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
endpoint,
|
||||
method,
|
||||
COUNT(*) as request_count,
|
||||
SUM(CASE WHEN response_status >= 400 THEN 1 ELSE 0 END) as error_count,
|
||||
AVG(processing_time) as avg_process_time,
|
||||
MAX(created_at) as last_access
|
||||
FROM api_access_logs
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY endpoint, method
|
||||
ORDER BY request_count DESC
|
||||
`
|
||||
|
||||
err := r.db.Raw(query).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// FindWithFilters 带过滤条件的分页查找访问日志
|
||||
func (r *APIAccessLogRepositoryImpl) FindWithFilters(page, limit int, startDate, endDate *time.Time, endpoint, ip string) ([]entity.APIAccessLog, int64, error) {
|
||||
var logs []entity.APIAccessLog
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
query := r.db.Model(&entity.APIAccessLog{})
|
||||
|
||||
// 添加过滤条件
|
||||
if startDate != nil {
|
||||
query = query.Where("created_at >= ?", *startDate)
|
||||
}
|
||||
if endDate != nil {
|
||||
query = query.Where("created_at <= ?", *endDate)
|
||||
}
|
||||
if endpoint != "" {
|
||||
query = query.Where("endpoint LIKE ?", "%"+endpoint+"%")
|
||||
}
|
||||
if ip != "" {
|
||||
query = query.Where("ip = ?", ip)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据,按创建时间倒序排列
|
||||
err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
// ClearOldLogs 清理旧日志
|
||||
func (r *APIAccessLogRepositoryImpl) ClearOldLogs(days int) error {
|
||||
cutoffDate := utils.GetCurrentTime().AddDate(0, 0, -days)
|
||||
return r.db.Where("created_at < ?", cutoffDate).Delete(&entity.APIAccessLog{}).Error
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type BaseRepository[T any] interface {
|
||||
Update(entity *T) error
|
||||
Delete(id uint) error
|
||||
FindWithPagination(page, limit int) ([]T, int64, error)
|
||||
GetDB() *gorm.DB
|
||||
}
|
||||
|
||||
// BaseRepositoryImpl 基础Repository实现
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -10,6 +13,7 @@ import (
|
||||
type CksRepository interface {
|
||||
BaseRepository[entity.Cks]
|
||||
FindByPanID(panID uint) ([]entity.Cks, error)
|
||||
FindByIds(ids []uint) ([]*entity.Cks, error)
|
||||
FindByIsValid(isValid bool) ([]entity.Cks, error)
|
||||
UpdateSpace(id uint, space, leftSpace int64) error
|
||||
DeleteByPanID(panID uint) error
|
||||
@@ -65,14 +69,31 @@ func (r *CksRepositoryImpl) FindAll() ([]entity.Cks, error) {
|
||||
|
||||
// FindByID 根据ID查找Cks,预加载Pan关联数据
|
||||
func (r *CksRepositoryImpl) FindByID(id uint) (*entity.Cks, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var cks entity.Cks
|
||||
err := r.db.Preload("Pan").First(&cks, id).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("FindByID失败: ID=%d, 错误=%v, 查询耗时=%v", id, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("FindByID成功: ID=%d, 查询耗时=%v", id, queryDuration)
|
||||
return &cks, nil
|
||||
}
|
||||
|
||||
func (r *CksRepositoryImpl) FindByIds(ids []uint) ([]*entity.Cks, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var cks []*entity.Cks
|
||||
err := r.db.Preload("Pan").Where("id IN ?", ids).Find(&cks).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("FindByIds失败: IDs数量=%d, 错误=%v, 查询耗时=%v", len(ids), err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("FindByIds成功: 找到%d个账号,查询耗时=%v", len(cks), queryDuration)
|
||||
return cks, nil
|
||||
}
|
||||
|
||||
// UpdateWithAllFields 更新Cks,包括零值字段
|
||||
func (r *CksRepositoryImpl) UpdateWithAllFields(cks *entity.Cks) error {
|
||||
return r.db.Save(cks).Error
|
||||
|
||||
87
db/repo/copyright_claim_repository.go
Normal file
87
db/repo/copyright_claim_repository.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// CopyrightClaimRepository 版权申述Repository接口
|
||||
type CopyrightClaimRepository interface {
|
||||
BaseRepository[entity.CopyrightClaim]
|
||||
GetByResourceKey(resourceKey string) ([]*entity.CopyrightClaim, error)
|
||||
List(status string, page, pageSize int) ([]*entity.CopyrightClaim, int64, error)
|
||||
UpdateStatus(id uint, status string, processedBy *uint, note string) error
|
||||
// 兼容原有方法名
|
||||
GetByID(id uint) (*entity.CopyrightClaim, error)
|
||||
}
|
||||
|
||||
// CopyrightClaimRepositoryImpl 版权申述Repository实现
|
||||
type CopyrightClaimRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.CopyrightClaim]
|
||||
}
|
||||
|
||||
// NewCopyrightClaimRepository 创建版权申述Repository
|
||||
func NewCopyrightClaimRepository(db *gorm.DB) CopyrightClaimRepository {
|
||||
return &CopyrightClaimRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.CopyrightClaim]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建版权申述
|
||||
func (r *CopyrightClaimRepositoryImpl) Create(claim *entity.CopyrightClaim) error {
|
||||
return r.GetDB().Create(claim).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取版权申述
|
||||
func (r *CopyrightClaimRepositoryImpl) GetByID(id uint) (*entity.CopyrightClaim, error) {
|
||||
var claim entity.CopyrightClaim
|
||||
err := r.GetDB().Where("id = ?", id).First(&claim).Error
|
||||
return &claim, err
|
||||
}
|
||||
|
||||
// GetByResourceKey 获取某个资源的所有版权申述
|
||||
func (r *CopyrightClaimRepositoryImpl) GetByResourceKey(resourceKey string) ([]*entity.CopyrightClaim, error) {
|
||||
var claims []*entity.CopyrightClaim
|
||||
err := r.GetDB().Where("resource_key = ?", resourceKey).Find(&claims).Error
|
||||
return claims, err
|
||||
}
|
||||
|
||||
// List 获取版权申述列表
|
||||
func (r *CopyrightClaimRepositoryImpl) List(status string, page, pageSize int) ([]*entity.CopyrightClaim, int64, error) {
|
||||
var claims []*entity.CopyrightClaim
|
||||
var total int64
|
||||
|
||||
query := r.GetDB().Model(&entity.CopyrightClaim{})
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&claims).Error
|
||||
return claims, total, err
|
||||
}
|
||||
|
||||
// Update 更新版权申述
|
||||
func (r *CopyrightClaimRepositoryImpl) Update(claim *entity.CopyrightClaim) error {
|
||||
return r.GetDB().Save(claim).Error
|
||||
}
|
||||
|
||||
// UpdateStatus 更新版权申述状态
|
||||
func (r *CopyrightClaimRepositoryImpl) UpdateStatus(id uint, status string, processedBy *uint, note string) error {
|
||||
return r.GetDB().Model(&entity.CopyrightClaim{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"processed_at": gorm.Expr("NOW()"),
|
||||
"processed_by": processedBy,
|
||||
"note": note,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// Delete 删除版权申述
|
||||
func (r *CopyrightClaimRepositoryImpl) Delete(id uint) error {
|
||||
return r.GetDB().Delete(&entity.CopyrightClaim{}, id).Error
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (r *FileRepositoryImpl) SearchFiles(search string, fileType, status string,
|
||||
// 添加搜索条件
|
||||
if search != "" {
|
||||
query = query.Where("original_name LIKE ?", "%"+search+"%")
|
||||
utils.Info("添加搜索条件: original_name LIKE '%%%s%%'", search)
|
||||
utils.Info("添加搜索条件: file_name LIKE '%%%s%%'", search)
|
||||
}
|
||||
|
||||
if fileType != "" {
|
||||
|
||||
@@ -12,6 +12,7 @@ type HotDramaRepository interface {
|
||||
FindByID(id uint) (*entity.HotDrama, error)
|
||||
FindAll(page, pageSize int) ([]entity.HotDrama, int64, error)
|
||||
FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error)
|
||||
FindByCategoryAndSubType(category, subType string, page, pageSize int) ([]entity.HotDrama, int64, error)
|
||||
FindByDoubanID(doubanID string) (*entity.HotDrama, error)
|
||||
Upsert(drama *entity.HotDrama) error
|
||||
Delete(id uint) error
|
||||
@@ -59,7 +60,7 @@ func (r *hotDramaRepository) FindAll(page, pageSize int) ([]entity.HotDrama, int
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error
|
||||
err := r.db.Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -80,7 +81,28 @@ func (r *hotDramaRepository) FindByCategory(category string, page, pageSize int)
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := r.db.Where("category = ?", category).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error
|
||||
err := r.db.Where("category = ?", category).Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return dramas, total, nil
|
||||
}
|
||||
|
||||
// FindByCategoryAndSubType 根据分类和子类型查找热播剧(分页)
|
||||
func (r *hotDramaRepository) FindByCategoryAndSubType(category, subType string, page, pageSize int) ([]entity.HotDrama, int64, error) {
|
||||
var dramas []entity.HotDrama
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 获取总数
|
||||
if err := r.db.Model(&entity.HotDrama{}).Where("category = ? AND sub_type = ?", category, subType).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := r.db.Where("category = ? AND sub_type = ?", category, subType).Order("rank ASC").Offset(offset).Limit(pageSize).Find(&dramas).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -6,38 +6,46 @@ import (
|
||||
|
||||
// RepositoryManager Repository管理器
|
||||
type RepositoryManager struct {
|
||||
PanRepository PanRepository
|
||||
CksRepository CksRepository
|
||||
ResourceRepository ResourceRepository
|
||||
CategoryRepository CategoryRepository
|
||||
TagRepository TagRepository
|
||||
ReadyResourceRepository ReadyResourceRepository
|
||||
UserRepository UserRepository
|
||||
SearchStatRepository SearchStatRepository
|
||||
SystemConfigRepository SystemConfigRepository
|
||||
HotDramaRepository HotDramaRepository
|
||||
ResourceViewRepository ResourceViewRepository
|
||||
TaskRepository TaskRepository
|
||||
TaskItemRepository TaskItemRepository
|
||||
FileRepository FileRepository
|
||||
PanRepository PanRepository
|
||||
CksRepository CksRepository
|
||||
ResourceRepository ResourceRepository
|
||||
CategoryRepository CategoryRepository
|
||||
TagRepository TagRepository
|
||||
ReadyResourceRepository ReadyResourceRepository
|
||||
UserRepository UserRepository
|
||||
SearchStatRepository SearchStatRepository
|
||||
SystemConfigRepository SystemConfigRepository
|
||||
HotDramaRepository HotDramaRepository
|
||||
ResourceViewRepository ResourceViewRepository
|
||||
TaskRepository TaskRepository
|
||||
TaskItemRepository TaskItemRepository
|
||||
FileRepository FileRepository
|
||||
TelegramChannelRepository TelegramChannelRepository
|
||||
APIAccessLogRepository APIAccessLogRepository
|
||||
ReportRepository ReportRepository
|
||||
CopyrightClaimRepository CopyrightClaimRepository
|
||||
}
|
||||
|
||||
// NewRepositoryManager 创建Repository管理器
|
||||
func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
|
||||
return &RepositoryManager{
|
||||
PanRepository: NewPanRepository(db),
|
||||
CksRepository: NewCksRepository(db),
|
||||
ResourceRepository: NewResourceRepository(db),
|
||||
CategoryRepository: NewCategoryRepository(db),
|
||||
TagRepository: NewTagRepository(db),
|
||||
ReadyResourceRepository: NewReadyResourceRepository(db),
|
||||
UserRepository: NewUserRepository(db),
|
||||
SearchStatRepository: NewSearchStatRepository(db),
|
||||
SystemConfigRepository: NewSystemConfigRepository(db),
|
||||
HotDramaRepository: NewHotDramaRepository(db),
|
||||
ResourceViewRepository: NewResourceViewRepository(db),
|
||||
TaskRepository: NewTaskRepository(db),
|
||||
TaskItemRepository: NewTaskItemRepository(db),
|
||||
FileRepository: NewFileRepository(db),
|
||||
PanRepository: NewPanRepository(db),
|
||||
CksRepository: NewCksRepository(db),
|
||||
ResourceRepository: NewResourceRepository(db),
|
||||
CategoryRepository: NewCategoryRepository(db),
|
||||
TagRepository: NewTagRepository(db),
|
||||
ReadyResourceRepository: NewReadyResourceRepository(db),
|
||||
UserRepository: NewUserRepository(db),
|
||||
SearchStatRepository: NewSearchStatRepository(db),
|
||||
SystemConfigRepository: NewSystemConfigRepository(db),
|
||||
HotDramaRepository: NewHotDramaRepository(db),
|
||||
ResourceViewRepository: NewResourceViewRepository(db),
|
||||
TaskRepository: NewTaskRepository(db),
|
||||
TaskItemRepository: NewTaskItemRepository(db),
|
||||
FileRepository: NewFileRepository(db),
|
||||
TelegramChannelRepository: NewTelegramChannelRepository(db),
|
||||
APIAccessLogRepository: NewAPIAccessLogRepository(db),
|
||||
ReportRepository: NewReportRepository(db),
|
||||
CopyrightClaimRepository: NewCopyrightClaimRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
114
db/repo/pagination.go
Normal file
114
db/repo/pagination.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PaginationResult 分页查询结果
|
||||
type PaginationResult[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// PaginationOptions 分页查询选项
|
||||
type PaginationOptions struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDir string `json:"order_dir"` // asc or desc
|
||||
Preloads []string `json:"preloads"` // 需要预加载的关联
|
||||
Filters map[string]interface{} `json:"filters"` // 过滤条件
|
||||
}
|
||||
|
||||
// DefaultPaginationOptions 默认分页选项
|
||||
func DefaultPaginationOptions() *PaginationOptions {
|
||||
return &PaginationOptions{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
OrderBy: "id",
|
||||
OrderDir: "desc",
|
||||
Preloads: []string{},
|
||||
Filters: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// PaginatedQuery 通用分页查询函数
|
||||
func PaginatedQuery[T any](db *gorm.DB, options *PaginationOptions) (*PaginationResult[T], error) {
|
||||
// 验证分页参数
|
||||
if options.Page < 1 {
|
||||
options.Page = 1
|
||||
}
|
||||
if options.PageSize < 1 || options.PageSize > 1000 {
|
||||
options.PageSize = 20
|
||||
}
|
||||
|
||||
// 应用预加载
|
||||
query := db.Model(new(T))
|
||||
for _, preload := range options.Preloads {
|
||||
query = query.Preload(preload)
|
||||
}
|
||||
|
||||
// 应用过滤条件
|
||||
for key, value := range options.Filters {
|
||||
// 处理特殊过滤条件
|
||||
switch key {
|
||||
case "search":
|
||||
// 搜索条件需要特殊处理
|
||||
if searchStr, ok := value.(string); ok && searchStr != "" {
|
||||
query = query.Where("title ILIKE ? OR description ILIKE ?", "%"+searchStr+"%", "%"+searchStr+"%")
|
||||
}
|
||||
case "category_id":
|
||||
if categoryID, ok := value.(uint); ok {
|
||||
query = query.Where("category_id = ?", categoryID)
|
||||
}
|
||||
case "pan_id":
|
||||
if panID, ok := value.(uint); ok {
|
||||
query = query.Where("pan_id = ?", panID)
|
||||
}
|
||||
case "is_valid":
|
||||
if isValid, ok := value.(bool); ok {
|
||||
query = query.Where("is_valid = ?", isValid)
|
||||
}
|
||||
case "is_public":
|
||||
if isPublic, ok := value.(bool); ok {
|
||||
query = query.Where("is_public = ?", isPublic)
|
||||
}
|
||||
default:
|
||||
// 通用过滤条件
|
||||
query = query.Where(key+" = ?", value)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
orderClause := options.OrderBy + " " + options.OrderDir
|
||||
query = query.Order(orderClause)
|
||||
|
||||
// 计算偏移量
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
|
||||
// 获取总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
var data []T
|
||||
if err := query.Offset(offset).Limit(options.PageSize).Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
totalPages := int((total + int64(options.PageSize) - 1) / int64(options.PageSize))
|
||||
|
||||
return &PaginationResult[T]{
|
||||
Data: data,
|
||||
Total: total,
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
type PanRepository interface {
|
||||
BaseRepository[entity.Pan]
|
||||
FindWithCks() ([]entity.Pan, error)
|
||||
FindIdByServiceType(serviceType string) (int, error)
|
||||
}
|
||||
|
||||
// PanRepositoryImpl Pan的Repository实现
|
||||
@@ -30,3 +33,12 @@ func (r *PanRepositoryImpl) FindWithCks() ([]entity.Pan, error) {
|
||||
err := r.db.Preload("Cks").Find(&pans).Error
|
||||
return pans, err
|
||||
}
|
||||
|
||||
func (r *PanRepositoryImpl) FindIdByServiceType(serviceType string) (int, error) {
|
||||
var pan entity.Pan
|
||||
err := r.db.Where("name = ?", serviceType).Find(&pan).Error
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("获取panId失败: %v", serviceType)
|
||||
}
|
||||
return int(pan.ID), nil
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (r *ReadyResourceRepositoryImpl) BatchFindByURLs(urls []string) ([]entity.R
|
||||
// FindByKey 根据Key查找
|
||||
func (r *ReadyResourceRepositoryImpl) FindByKey(key string) ([]entity.ReadyResource, error) {
|
||||
var resources []entity.ReadyResource
|
||||
err := r.db.Where("key = ?", key).Find(&resources).Error
|
||||
err := r.db.Unscoped().Where("key = ?", key).Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
|
||||
87
db/repo/report_repository.go
Normal file
87
db/repo/report_repository.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// ReportRepository 举报Repository接口
|
||||
type ReportRepository interface {
|
||||
BaseRepository[entity.Report]
|
||||
GetByResourceKey(resourceKey string) ([]*entity.Report, error)
|
||||
List(status string, page, pageSize int) ([]*entity.Report, int64, error)
|
||||
UpdateStatus(id uint, status string, processedBy *uint, note string) error
|
||||
// 兼容原有方法名
|
||||
GetByID(id uint) (*entity.Report, error)
|
||||
}
|
||||
|
||||
// ReportRepositoryImpl 举报Repository实现
|
||||
type ReportRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.Report]
|
||||
}
|
||||
|
||||
// NewReportRepository 创建举报Repository
|
||||
func NewReportRepository(db *gorm.DB) ReportRepository {
|
||||
return &ReportRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.Report]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建举报
|
||||
func (r *ReportRepositoryImpl) Create(report *entity.Report) error {
|
||||
return r.GetDB().Create(report).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取举报
|
||||
func (r *ReportRepositoryImpl) GetByID(id uint) (*entity.Report, error) {
|
||||
var report entity.Report
|
||||
err := r.GetDB().Where("id = ?", id).First(&report).Error
|
||||
return &report, err
|
||||
}
|
||||
|
||||
// GetByResourceKey 获取某个资源的所有举报
|
||||
func (r *ReportRepositoryImpl) GetByResourceKey(resourceKey string) ([]*entity.Report, error) {
|
||||
var reports []*entity.Report
|
||||
err := r.GetDB().Where("resource_key = ?", resourceKey).Find(&reports).Error
|
||||
return reports, err
|
||||
}
|
||||
|
||||
// List 获取举报列表
|
||||
func (r *ReportRepositoryImpl) List(status string, page, pageSize int) ([]*entity.Report, int64, error) {
|
||||
var reports []*entity.Report
|
||||
var total int64
|
||||
|
||||
query := r.GetDB().Model(&entity.Report{})
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&reports).Error
|
||||
return reports, total, err
|
||||
}
|
||||
|
||||
// Update 更新举报
|
||||
func (r *ReportRepositoryImpl) Update(report *entity.Report) error {
|
||||
return r.GetDB().Save(report).Error
|
||||
}
|
||||
|
||||
// UpdateStatus 更新举报状态
|
||||
func (r *ReportRepositoryImpl) UpdateStatus(id uint, status string, processedBy *uint, note string) error {
|
||||
return r.GetDB().Model(&entity.Report{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"processed_at": gorm.Expr("NOW()"),
|
||||
"processed_by": processedBy,
|
||||
"note": note,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// Delete 删除举报
|
||||
func (r *ReportRepositoryImpl) Delete(id uint) error {
|
||||
return r.GetDB().Delete(&entity.Report{}, id).Error
|
||||
}
|
||||
@@ -2,9 +2,12 @@ package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -34,6 +37,20 @@ type ResourceRepository interface {
|
||||
GetByURL(url string) (*entity.Resource, error)
|
||||
UpdateSaveURL(id uint, saveURL string) error
|
||||
CreateResourceTag(resourceTag *entity.ResourceTag) error
|
||||
FindByIDs(ids []uint) ([]entity.Resource, error)
|
||||
FindUnsyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error)
|
||||
FindSyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error)
|
||||
CountUnsyncedToMeilisearch() (int64, error)
|
||||
CountSyncedToMeilisearch() (int64, error)
|
||||
MarkAsSyncedToMeilisearch(ids []uint) error
|
||||
MarkAllAsUnsyncedToMeilisearch() error
|
||||
FindAllWithPagination(page, limit int) ([]entity.Resource, int64, error)
|
||||
GetRandomResourceWithFilters(categoryFilter, tagFilter string, isPushSavedInfo bool) (*entity.Resource, error)
|
||||
DeleteRelatedResources(ckID uint) (int64, error)
|
||||
CountResourcesByCkID(ckID uint) (int64, error)
|
||||
FindByResourceKey(key string) ([]entity.Resource, error)
|
||||
FindByKey(key string) ([]entity.Resource, error)
|
||||
GetHotResources(limit int) ([]entity.Resource, error)
|
||||
}
|
||||
|
||||
// ResourceRepositoryImpl Resource的Repository实现
|
||||
@@ -59,38 +76,21 @@ func (r *ResourceRepositoryImpl) FindWithRelations() ([]entity.Resource, error)
|
||||
|
||||
// FindWithRelationsPaginated 分页查找包含关联关系的资源
|
||||
func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 优化查询:只预加载必要的关联,并添加排序
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Order("updated_at DESC") // 按更新时间倒序,显示最新内容
|
||||
|
||||
// 获取总数(使用缓存键)
|
||||
cacheKey := fmt.Sprintf("resources_total_%d_%d", page, limit)
|
||||
if cached, exists := r.cache[cacheKey]; exists {
|
||||
if totalCached, ok := cached.(int64); ok {
|
||||
total = totalCached
|
||||
}
|
||||
} else {
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// 缓存总数(5分钟)
|
||||
r.cache[cacheKey] = total
|
||||
go func() {
|
||||
time.Sleep(5 * time.Minute)
|
||||
delete(r.cache, cacheKey)
|
||||
}()
|
||||
// 使用新的分页查询功能
|
||||
options := &PaginationOptions{
|
||||
Page: page,
|
||||
PageSize: limit,
|
||||
OrderBy: "updated_at",
|
||||
OrderDir: "desc",
|
||||
Preloads: []string{"Category", "Pan"},
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
result, err := PaginatedQuery[entity.Resource](r.db, options)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return result.Data, result.Total, nil
|
||||
}
|
||||
|
||||
// FindByCategoryID 根据分类ID查找
|
||||
@@ -209,6 +209,7 @@ func (r *ResourceRepositoryImpl) SearchByPanID(query string, panID uint, page, l
|
||||
|
||||
// SearchWithFilters 根据参数进行搜索
|
||||
func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}) ([]entity.Resource, int64, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
@@ -246,6 +247,23 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
||||
Where("resource_tags.tag_id = ?", tagEntity.ID)
|
||||
}
|
||||
}
|
||||
case "tag_ids": // 添加tag_ids参数支持(标签ID列表)
|
||||
if tagIdsStr, ok := value.(string); ok && tagIdsStr != "" {
|
||||
// 将逗号分隔的标签ID字符串转换为整数ID数组
|
||||
tagIdStrs := strings.Split(tagIdsStr, ",")
|
||||
var tagIds []uint
|
||||
for _, idStr := range tagIdStrs {
|
||||
idStr = strings.TrimSpace(idStr) // 去除空格
|
||||
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
|
||||
tagIds = append(tagIds, uint(id))
|
||||
}
|
||||
}
|
||||
if len(tagIds) > 0 {
|
||||
// 通过中间表查找包含任一标签的资源
|
||||
db = db.Joins("JOIN resource_tags ON resources.id = resource_tags.resource_id").
|
||||
Where("resource_tags.tag_id IN ?", tagIds)
|
||||
}
|
||||
}
|
||||
case "pan_id": // 添加pan_id参数支持
|
||||
if panID, ok := value.(uint); ok {
|
||||
db = db.Where("pan_id = ?", panID)
|
||||
@@ -283,6 +301,20 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
||||
db = db.Where("pan_id = ?", panEntity.ID)
|
||||
}
|
||||
}
|
||||
case "exclude_ids": // 添加exclude_ids参数支持
|
||||
if excludeIDs, ok := value.([]uint); ok && len(excludeIDs) > 0 {
|
||||
// 限制排除ID的数量,避免SQL语句过长
|
||||
maxExcludeIDs := 5000 // 限制排除ID数量,避免SQL语句过长
|
||||
if len(excludeIDs) > maxExcludeIDs {
|
||||
// 只取最近的maxExcludeIDs个ID进行排除
|
||||
startIndex := len(excludeIDs) - maxExcludeIDs
|
||||
truncatedExcludeIDs := excludeIDs[startIndex:]
|
||||
db = db.Where("id NOT IN ?", truncatedExcludeIDs)
|
||||
utils.Debug("SearchWithFilters: 排除ID数量过多,截取最近%d个ID", len(truncatedExcludeIDs))
|
||||
} else {
|
||||
db = db.Where("id NOT IN ?", excludeIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,9 +357,37 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
||||
// 计算偏移量
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 获取分页数据,按更新时间倒序
|
||||
err := db.Order("updated_at DESC").Offset(offset).Limit(pageSize).Find(&resources).Error
|
||||
fmt.Printf("查询结果: 总数=%d, 当前页数据量=%d, pageSize=%d\n", total, len(resources), pageSize)
|
||||
// 处理排序参数
|
||||
orderBy := "updated_at"
|
||||
orderDir := "DESC"
|
||||
|
||||
if orderByVal, ok := params["order_by"].(string); ok && orderByVal != "" {
|
||||
// 验证排序字段,防止SQL注入
|
||||
validOrderByFields := map[string]bool{
|
||||
"created_at": true,
|
||||
"updated_at": true,
|
||||
"view_count": true,
|
||||
"title": true,
|
||||
"id": true,
|
||||
}
|
||||
if validOrderByFields[orderByVal] {
|
||||
orderBy = orderByVal
|
||||
}
|
||||
}
|
||||
|
||||
if orderDirVal, ok := params["order_dir"].(string); ok && orderDirVal != "" {
|
||||
// 验证排序方向
|
||||
if orderDirVal == "ASC" || orderDirVal == "DESC" {
|
||||
orderDir = orderDirVal
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分页数据,应用排序
|
||||
queryStart := utils.GetCurrentTime()
|
||||
err := db.Order(fmt.Sprintf("%s %s", orderBy, orderDir)).Offset(offset).Limit(pageSize).Find(&resources).Error
|
||||
queryDuration := time.Since(queryStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("SearchWithFilters完成: 总数=%d, 当前页数据量=%d, 排序=%s %s, 查询耗时=%v, 总耗时=%v", total, len(resources), orderBy, orderDir, queryDuration, totalDuration)
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
@@ -460,20 +520,282 @@ func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime t
|
||||
|
||||
// GetByURL 根据URL获取资源
|
||||
func (r *ResourceRepositoryImpl) GetByURL(url string) (*entity.Resource, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var resource entity.Resource
|
||||
err := r.GetDB().Where("url = ?", url).First(&resource).Error
|
||||
err := r.db.Where("url = ?", url).First(&resource).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("GetByURL失败: URL=%s, 错误=%v, 查询耗时=%v", url, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("GetByURL成功: URL=%s, 查询耗时=%v", url, queryDuration)
|
||||
return &resource, nil
|
||||
}
|
||||
|
||||
// UpdateSaveURL 更新资源的转存链接
|
||||
// FindByIDs 根据ID列表查找资源
|
||||
func (r *ResourceRepositoryImpl) FindByIDs(ids []uint) ([]entity.Resource, error) {
|
||||
if len(ids) == 0 {
|
||||
return []entity.Resource{}, nil
|
||||
}
|
||||
|
||||
var resources []entity.Resource
|
||||
err := r.db.Where("id IN ?", ids).Preload("Category").Preload("Pan").Preload("Tags").Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// UpdateSaveURL 更新保存URL
|
||||
func (r *ResourceRepositoryImpl) UpdateSaveURL(id uint, saveURL string) error {
|
||||
return r.GetDB().Model(&entity.Resource{}).Where("id = ?", id).Update("save_url", saveURL).Error
|
||||
return r.db.Model(&entity.Resource{}).Where("id = ?", id).Update("save_url", saveURL).Error
|
||||
}
|
||||
|
||||
// CreateResourceTag 创建资源与标签的关联
|
||||
func (r *ResourceRepositoryImpl) CreateResourceTag(resourceTag *entity.ResourceTag) error {
|
||||
return r.GetDB().Create(resourceTag).Error
|
||||
return r.db.Create(resourceTag).Error
|
||||
}
|
||||
|
||||
// FindUnsyncedToMeilisearch 查找未同步到Meilisearch的资源
|
||||
func (r *ResourceRepositoryImpl) FindUnsyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 查询未同步的资源
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", false).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Preload("Tags"). // 添加Tags预加载
|
||||
Order("updated_at DESC")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
// CountUnsyncedToMeilisearch 统计未同步到Meilisearch的资源数量
|
||||
func (r *ResourceRepositoryImpl) CountUnsyncedToMeilisearch() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// MarkAsSyncedToMeilisearch 标记资源为已同步到Meilisearch
|
||||
func (r *ResourceRepositoryImpl) MarkAsSyncedToMeilisearch(ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return r.db.Model(&entity.Resource{}).
|
||||
Where("id IN ?", ids).
|
||||
Updates(map[string]interface{}{
|
||||
"synced_to_meilisearch": true,
|
||||
"synced_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// MarkAllAsUnsyncedToMeilisearch 标记所有资源为未同步到Meilisearch
|
||||
func (r *ResourceRepositoryImpl) MarkAllAsUnsyncedToMeilisearch() error {
|
||||
return r.db.Model(&entity.Resource{}).
|
||||
Where("1 = 1"). // 添加WHERE条件以更新所有记录
|
||||
Updates(map[string]interface{}{
|
||||
"synced_to_meilisearch": false,
|
||||
"synced_at": nil,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// FindSyncedToMeilisearch 查找已同步到Meilisearch的资源
|
||||
func (r *ResourceRepositoryImpl) FindSyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 查询已同步的资源
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", true).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Preload("Tags").
|
||||
Order("updated_at DESC")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
// CountSyncedToMeilisearch 统计已同步到Meilisearch的资源数量
|
||||
func (r *ResourceRepositoryImpl) CountSyncedToMeilisearch() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", true).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// FindAllWithPagination 分页查找所有资源
|
||||
func (r *ResourceRepositoryImpl) FindAllWithPagination(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 查询所有资源
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Preload("Tags").
|
||||
Order("updated_at DESC")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
// GetRandomResourceWithFilters 使用 PostgreSQL RANDOM() 功能随机获取一个符合条件的资源
|
||||
func (r *ResourceRepositoryImpl) GetRandomResourceWithFilters(categoryFilter, tagFilter string, isPushSavedInfo bool) (*entity.Resource, error) {
|
||||
// 构建查询条件
|
||||
query := r.db.Model(&entity.Resource{}).Preload("Category").Preload("Pan").Preload("Tags")
|
||||
|
||||
// 基础条件:有效且公开的资源
|
||||
query = query.Where("is_valid = ? AND is_public = ?", true, true)
|
||||
|
||||
// 根据分类过滤
|
||||
if categoryFilter != "" {
|
||||
// 查找分类ID
|
||||
var categoryEntity entity.Category
|
||||
if err := r.db.Where("name ILIKE ?", "%"+categoryFilter+"%").First(&categoryEntity).Error; err == nil {
|
||||
query = query.Where("category_id = ?", categoryEntity.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据标签过滤
|
||||
if tagFilter != "" {
|
||||
// 查找标签ID
|
||||
var tagEntity entity.Tag
|
||||
if err := r.db.Where("name ILIKE ?", "%"+tagFilter+"%").First(&tagEntity).Error; err == nil {
|
||||
// 通过中间表查找包含该标签的资源
|
||||
query = query.Joins("JOIN resource_tags ON resources.id = resource_tags.resource_id").
|
||||
Where("resource_tags.tag_id = ?", tagEntity.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// // 根据是否只推送已转存资源过滤
|
||||
// if isPushSavedInfo {
|
||||
// query = query.Where("save_url IS NOT NULL AND save_url != '' AND TRIM(save_url) != ''")
|
||||
// }
|
||||
|
||||
// 使用 PostgreSQL 的 RANDOM() 进行随机排序,并限制为1个结果
|
||||
var resource entity.Resource
|
||||
err := query.Order("RANDOM()").Limit(1).First(&resource).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resource, nil
|
||||
}
|
||||
|
||||
// DeleteRelatedResources 删除关联资源,清空 fid、ck_id 和 save_url 三个字段
|
||||
func (r *ResourceRepositoryImpl) DeleteRelatedResources(ckID uint) (int64, error) {
|
||||
result := r.db.Model(&entity.Resource{}).
|
||||
Where("ck_id = ?", ckID).
|
||||
Updates(map[string]interface{}{
|
||||
"fid": nil, // 清空 fid 字段
|
||||
"ck_id": 0, // 清空 ck_id 字段
|
||||
"save_url": "", // 清空 save_url 字段
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return 0, result.Error
|
||||
}
|
||||
|
||||
return result.RowsAffected, nil
|
||||
}
|
||||
|
||||
// CountResourcesByCkID 统计指定账号ID的资源数量
|
||||
func (r *ResourceRepositoryImpl) CountResourcesByCkID(ckID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&entity.Resource{}).
|
||||
Where("ck_id = ?", ckID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// FindByKey 根据Key查找资源(同一组资源)
|
||||
func (r *ResourceRepositoryImpl) FindByKey(key string) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
err := r.db.Where("key = ?", key).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Preload("Tags").
|
||||
Order("pan_id ASC").
|
||||
Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// GetHotResources 获取热门资源(按查看次数排序,去重,限制数量)
|
||||
func (r *ResourceRepositoryImpl) GetHotResources(limit int) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
|
||||
// 按key分组,获取每个key中查看次数最高的资源,然后按查看次数排序
|
||||
err := r.db.Table("resources").
|
||||
Select(`
|
||||
resources.*,
|
||||
ROW_NUMBER() OVER (PARTITION BY key ORDER BY view_count DESC) as rn
|
||||
`).
|
||||
Where("is_public = ? AND view_count > 0", true).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Preload("Tags").
|
||||
Order("view_count DESC").
|
||||
Limit(limit * 2). // 获取更多数据以确保去重后有足够的结果
|
||||
Find(&resources).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 按key去重,保留每个key的第一个(即查看次数最高的)
|
||||
seenKeys := make(map[string]bool)
|
||||
var hotResources []entity.Resource
|
||||
for _, resource := range resources {
|
||||
if !seenKeys[resource.Key] {
|
||||
seenKeys[resource.Key] = true
|
||||
hotResources = append(hotResources, resource)
|
||||
if len(hotResources) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hotResources, nil
|
||||
}
|
||||
|
||||
// FindByResourceKey 根据资源Key查找资源
|
||||
func (r *ResourceRepositoryImpl) FindByResourceKey(key string) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
err := r.GetDB().Where("key = ?", key).Find(&resources).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package repo
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
@@ -100,8 +101,11 @@ func (r *SystemConfigRepositoryImpl) UpsertConfigs(configs []entity.SystemConfig
|
||||
|
||||
// GetOrCreateDefault 获取配置或创建默认配置
|
||||
func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
configs, err := r.FindAll()
|
||||
initialQueryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("获取所有系统配置失败: %v,耗时: %v", err, initialQueryDuration)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -121,17 +125,37 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
{Key: entity.ConfigKeyAutoTransferMinSpace, Value: entity.ConfigDefaultAutoTransferMinSpace, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: entity.ConfigDefaultAutoFetchHotDramaEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyApiToken, Value: entity.ConfigDefaultApiToken, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyForbiddenWords, Value: entity.ConfigDefaultForbiddenWords, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAdKeywords, Value: entity.ConfigDefaultAdKeywords, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAutoInsertAd, Value: entity.ConfigDefaultAutoInsertAd, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchEnabled, Value: entity.ConfigDefaultMeilisearchEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyMeilisearchHost, Value: entity.ConfigDefaultMeilisearchHost, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyEnableAnnouncements, Value: entity.ConfigDefaultEnableAnnouncements, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyAnnouncements, Value: entity.ConfigDefaultAnnouncements, Type: entity.ConfigTypeJSON},
|
||||
{Key: entity.ConfigKeyEnableFloatButtons, Value: entity.ConfigDefaultEnableFloatButtons, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyWechatSearchImage, Value: entity.ConfigDefaultWechatSearchImage, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyTelegramQrImage, Value: entity.ConfigDefaultTelegramQrImage, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyQrCodeStyle, Value: entity.ConfigDefaultQrCodeStyle, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyWebsiteURL, Value: entity.ConfigDefaultWebsiteURL, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
createStart := utils.GetCurrentTime()
|
||||
err = r.UpsertConfigs(defaultConfigs)
|
||||
createDuration := time.Since(createStart)
|
||||
if err != nil {
|
||||
utils.Error("创建默认系统配置失败: %v,耗时: %v", err, createDuration)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Info("创建默认系统配置成功,数量: %d,总耗时: %v", len(defaultConfigs), totalDuration)
|
||||
return defaultConfigs, nil
|
||||
}
|
||||
|
||||
@@ -149,12 +173,24 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
entity.ConfigKeyAutoTransferMinSpace: {Key: entity.ConfigKeyAutoTransferMinSpace, Value: entity.ConfigDefaultAutoTransferMinSpace, Type: entity.ConfigTypeInt},
|
||||
entity.ConfigKeyAutoFetchHotDramaEnabled: {Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: entity.ConfigDefaultAutoFetchHotDramaEnabled, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyApiToken: {Key: entity.ConfigKeyApiToken, Value: entity.ConfigDefaultApiToken, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyForbiddenWords: {Key: entity.ConfigKeyForbiddenWords, Value: entity.ConfigDefaultForbiddenWords, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyAdKeywords: {Key: entity.ConfigKeyAdKeywords, Value: entity.ConfigDefaultAdKeywords, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyAutoInsertAd: {Key: entity.ConfigKeyAutoInsertAd, Value: entity.ConfigDefaultAutoInsertAd, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyPageSize: {Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
entity.ConfigKeyMaintenanceMode: {Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyEnableRegister: {Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyThirdPartyStatsCode: {Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchEnabled: {Key: entity.ConfigKeyMeilisearchEnabled, Value: entity.ConfigDefaultMeilisearchEnabled, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyMeilisearchHost: {Key: entity.ConfigKeyMeilisearchHost, Value: entity.ConfigDefaultMeilisearchHost, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchPort: {Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchMasterKey: {Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchIndexName: {Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyEnableAnnouncements: {Key: entity.ConfigKeyEnableAnnouncements, Value: entity.ConfigDefaultEnableAnnouncements, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyAnnouncements: {Key: entity.ConfigKeyAnnouncements, Value: entity.ConfigDefaultAnnouncements, Type: entity.ConfigTypeJSON},
|
||||
entity.ConfigKeyEnableFloatButtons: {Key: entity.ConfigKeyEnableFloatButtons, Value: entity.ConfigDefaultEnableFloatButtons, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyWechatSearchImage: {Key: entity.ConfigKeyWechatSearchImage, Value: entity.ConfigDefaultWechatSearchImage, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyTelegramQrImage: {Key: entity.ConfigKeyTelegramQrImage, Value: entity.ConfigDefaultTelegramQrImage, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyWebsiteURL: {Key: entity.ConfigKeyWebsiteURL, Value: entity.ConfigDefaultWebsiteURL, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
// 检查现有配置中是否有缺失的配置项
|
||||
@@ -173,17 +209,24 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
|
||||
// 如果有缺失的配置项,则添加它们
|
||||
if len(missingConfigs) > 0 {
|
||||
upsertStart := utils.GetCurrentTime()
|
||||
err = r.UpsertConfigs(missingConfigs)
|
||||
upsertDuration := time.Since(upsertStart)
|
||||
if err != nil {
|
||||
utils.Error("添加缺失的系统配置失败: %v,耗时: %v", err, upsertDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("添加缺失的系统配置完成,数量: %d,耗时: %v", len(missingConfigs), upsertDuration)
|
||||
// 重新获取所有配置
|
||||
configs, err = r.FindAll()
|
||||
if err != nil {
|
||||
utils.Error("重新获取所有系统配置失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("GetOrCreateDefault完成,总数: %d,总耗时: %v", len(configs), totalDuration)
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -58,8 +61,15 @@ func (r *TaskItemRepositoryImpl) DeleteByTaskID(taskID uint) error {
|
||||
|
||||
// GetByTaskIDAndStatus 根据任务ID和状态获取任务项
|
||||
func (r *TaskItemRepositoryImpl) GetByTaskIDAndStatus(taskID uint, status string) ([]*entity.TaskItem, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var items []*entity.TaskItem
|
||||
err := r.db.Where("task_id = ? AND status = ?", taskID, status).Order("id ASC").Find(&items).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("GetByTaskIDAndStatus失败: 任务ID=%d, 状态=%s, 错误=%v, 查询耗时=%v", taskID, status, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("GetByTaskIDAndStatus成功: 任务ID=%d, 状态=%s, 数量=%d, 查询耗时=%v", taskID, status, len(items), queryDuration)
|
||||
return items, err
|
||||
}
|
||||
|
||||
@@ -93,19 +103,36 @@ func (r *TaskItemRepositoryImpl) GetListByTaskID(taskID uint, page, pageSize int
|
||||
|
||||
// UpdateStatus 更新任务项状态
|
||||
func (r *TaskItemRepositoryImpl) UpdateStatus(id uint, status string) error {
|
||||
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Update("status", status).Error
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatus失败: ID=%d, 状态=%s, 错误=%v, 更新耗时=%v", id, status, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatus成功: ID=%d, 状态=%s, 更新耗时=%v", id, status, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatusAndOutput 更新任务项状态和输出数据
|
||||
func (r *TaskItemRepositoryImpl) UpdateStatusAndOutput(id uint, status, outputData string) error {
|
||||
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"output_data": outputData,
|
||||
}).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndOutput失败: ID=%d, 状态=%s, 错误=%v, 更新耗时=%v", id, status, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndOutput成功: ID=%d, 状态=%s, 更新耗时=%v", id, status, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStatsByTaskID 获取任务项统计信息
|
||||
func (r *TaskItemRepositoryImpl) GetStatsByTaskID(taskID uint) (map[string]int, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var results []struct {
|
||||
Status string
|
||||
Count int
|
||||
@@ -117,7 +144,9 @@ func (r *TaskItemRepositoryImpl) GetStatsByTaskID(taskID uint) (map[string]int,
|
||||
Group("status").
|
||||
Find(&results).Error
|
||||
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("GetStatsByTaskID失败: 任务ID=%d, 错误=%v, 查询耗时=%v", taskID, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -134,12 +163,22 @@ func (r *TaskItemRepositoryImpl) GetStatsByTaskID(taskID uint) (map[string]int,
|
||||
stats["total"] += result.Count
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("GetStatsByTaskID成功: 任务ID=%d, 统计信息=%v, 查询耗时=%v, 总耗时=%v", taskID, stats, queryDuration, totalDuration)
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// ResetProcessingItems 重置处理中的任务项为pending状态
|
||||
func (r *TaskItemRepositoryImpl) ResetProcessingItems(taskID uint) error {
|
||||
return r.db.Model(&entity.TaskItem{}).
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.TaskItem{}).
|
||||
Where("task_id = ? AND status = ?", taskID, "processing").
|
||||
Update("status", "pending").Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("ResetProcessingItems失败: 任务ID=%d, 错误=%v, 更新耗时=%v", taskID, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("ResetProcessingItems成功: 任务ID=%d, 更新耗时=%v", taskID, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -15,6 +18,8 @@ type TaskRepository interface {
|
||||
UpdateProgress(id uint, progress float64, progressData string) error
|
||||
UpdateStatusAndMessage(id uint, status, message string) error
|
||||
UpdateTaskStats(id uint, processed, success, failed int) error
|
||||
UpdateStartedAt(id uint) error
|
||||
UpdateCompletedAt(id uint) error
|
||||
}
|
||||
|
||||
// TaskRepositoryImpl 任务仓库实现
|
||||
@@ -31,11 +36,15 @@ func NewTaskRepository(db *gorm.DB) TaskRepository {
|
||||
|
||||
// GetByID 根据ID获取任务
|
||||
func (r *TaskRepositoryImpl) GetByID(id uint) (*entity.Task, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var task entity.Task
|
||||
err := r.db.First(&task, id).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("GetByID失败: ID=%d, 错误=%v, 查询耗时=%v", id, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("GetByID成功: ID=%d, 查询耗时=%v", id, queryDuration)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
@@ -51,6 +60,7 @@ func (r *TaskRepositoryImpl) Delete(id uint) error {
|
||||
|
||||
// GetList 获取任务列表
|
||||
func (r *TaskRepositoryImpl) GetList(page, pageSize int, taskType, status string) ([]*entity.Task, int64, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var tasks []*entity.Task
|
||||
var total int64
|
||||
|
||||
@@ -58,79 +68,178 @@ func (r *TaskRepositoryImpl) GetList(page, pageSize int, taskType, status string
|
||||
|
||||
// 添加过滤条件
|
||||
if taskType != "" {
|
||||
query = query.Where("task_type = ?", taskType)
|
||||
query = query.Where("type = ?", taskType)
|
||||
}
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
countStart := utils.GetCurrentTime()
|
||||
err := query.Count(&total).Error
|
||||
countDuration := time.Since(countStart)
|
||||
if err != nil {
|
||||
utils.Error("GetList获取总数失败: 错误=%v, 查询耗时=%v", err, countDuration)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
queryStart := utils.GetCurrentTime()
|
||||
err = query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&tasks).Error
|
||||
queryDuration := time.Since(queryStart)
|
||||
if err != nil {
|
||||
utils.Error("GetList查询失败: 错误=%v, 查询耗时=%v", err, queryDuration)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("GetList完成: 任务类型=%s, 状态=%s, 页码=%d, 页面大小=%d, 总数=%d, 结果数=%d, 总耗时=%v", taskType, status, page, pageSize, total, len(tasks), totalDuration)
|
||||
return tasks, total, nil
|
||||
}
|
||||
|
||||
// UpdateStatus 更新任务状态
|
||||
func (r *TaskRepositoryImpl) UpdateStatus(id uint, status string) error {
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatus失败: ID=%d, 状态=%s, 错误=%v, 更新耗时=%v", id, status, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatus成功: ID=%d, 状态=%s, 更新耗时=%v", id, status, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProgress 更新任务进度
|
||||
func (r *TaskRepositoryImpl) UpdateProgress(id uint, progress float64, progressData string) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
// 检查progress和progress_data字段是否存在
|
||||
var count int64
|
||||
err := r.db.Raw("SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'tasks' AND column_name = 'progress'").Count(&count).Error
|
||||
if err != nil || count == 0 {
|
||||
// 如果检查失败或字段不存在,只更新processed_items等现有字段
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"processed_items": progress, // 使用progress作为processed_items的近似值
|
||||
}).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateProgress失败(字段不存在): ID=%d, 进度=%f, 错误=%v, 更新耗时=%v, 总耗时=%v", id, progress, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateProgress成功(字段不存在): ID=%d, 进度=%f, 更新耗时=%v, 总耗时=%v", id, progress, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 字段存在,正常更新
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err = r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"progress": progress,
|
||||
"progress_data": progressData,
|
||||
}).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateProgress失败: ID=%d, 进度=%f, 错误=%v, 更新耗时=%v, 总耗时=%v", id, progress, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateProgress成功: ID=%d, 进度=%f, 更新耗时=%v, 总耗时=%v", id, progress, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatusAndMessage 更新任务状态和消息
|
||||
func (r *TaskRepositoryImpl) UpdateStatusAndMessage(id uint, status, message string) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
// 检查message字段是否存在
|
||||
var count int64
|
||||
err := r.db.Raw("SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'tasks' AND column_name = 'message'").Count(&count).Error
|
||||
if err != nil {
|
||||
// 如果检查失败,只更新状态
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndMessage失败(检查失败): ID=%d, 状态=%s, 错误=%v, 更新耗时=%v, 总耗时=%v", id, status, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndMessage成功(检查失败): ID=%d, 状态=%s, 更新耗时=%v, 总耗时=%v", id, status, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
// message字段存在,更新状态和消息
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"message": message,
|
||||
}).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndMessage失败(字段存在): ID=%d, 状态=%s, 错误=%v, 更新耗时=%v, 总耗时=%v", id, status, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndMessage成功(字段存在): ID=%d, 状态=%s, 更新耗时=%v, 总耗时=%v", id, status, updateDuration, totalDuration)
|
||||
return nil
|
||||
} else {
|
||||
// message字段不存在,只更新状态
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndMessage失败(字段不存在): ID=%d, 状态=%s, 错误=%v, 更新耗时=%v, 总耗时=%v", id, status, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndMessage成功(字段不存在): ID=%d, 状态=%s, 更新耗时=%v, 总耗时=%v", id, status, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTaskStats 更新任务统计信息
|
||||
func (r *TaskRepositoryImpl) UpdateTaskStats(id uint, processed, success, failed int) error {
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"processed_items": processed,
|
||||
"success_items": success,
|
||||
"failed_items": failed,
|
||||
}).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateTaskStats失败: ID=%d, 处理数=%d, 成功数=%d, 失败数=%d, 错误=%v, 更新耗时=%v", id, processed, success, failed, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateTaskStats成功: ID=%d, 处理数=%d, 成功数=%d, 失败数=%d, 更新耗时=%v", id, processed, success, failed, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStartedAt 更新任务开始时间
|
||||
func (r *TaskRepositoryImpl) UpdateStartedAt(id uint) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
now := time.Now()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("started_at", now).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStartedAt失败: ID=%d, 错误=%v, 更新耗时=%v", id, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStartedAt成功: ID=%d, 更新耗时=%v", id, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCompletedAt 更新任务完成时间
|
||||
func (r *TaskRepositoryImpl) UpdateCompletedAt(id uint) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
now := time.Now()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("completed_at", now).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateCompletedAt失败: ID=%d, 错误=%v, 更新耗时=%v", id, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateCompletedAt成功: ID=%d, 更新耗时=%v", id, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
156
db/repo/telegram_channel_repository.go
Normal file
156
db/repo/telegram_channel_repository.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TelegramChannelRepository interface {
|
||||
BaseRepository[entity.TelegramChannel]
|
||||
FindActiveChannels() ([]entity.TelegramChannel, error)
|
||||
FindByChatID(chatID int64) (*entity.TelegramChannel, error)
|
||||
FindByChatType(chatType string) ([]entity.TelegramChannel, error)
|
||||
UpdateLastPushAt(id uint, lastPushAt time.Time) error
|
||||
FindDueForPush() ([]entity.TelegramChannel, error)
|
||||
CleanupDuplicateChannels() error
|
||||
FindActiveChannelsByTypes(chatTypes []string) ([]entity.TelegramChannel, error)
|
||||
}
|
||||
|
||||
type TelegramChannelRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.TelegramChannel]
|
||||
}
|
||||
|
||||
func NewTelegramChannelRepository(db *gorm.DB) TelegramChannelRepository {
|
||||
return &TelegramChannelRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.TelegramChannel]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// 实现基类方法
|
||||
func (r *TelegramChannelRepositoryImpl) Create(entity *entity.TelegramChannel) error {
|
||||
return r.db.Create(entity).Error
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) Update(entity *entity.TelegramChannel) error {
|
||||
return r.db.Save(entity).Error
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) Delete(id uint) error {
|
||||
return r.db.Delete(&entity.TelegramChannel{}, id).Error
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) FindByID(id uint) (*entity.TelegramChannel, error) {
|
||||
var channel entity.TelegramChannel
|
||||
err := r.db.First(&channel, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *TelegramChannelRepositoryImpl) FindAll() ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Order("created_at desc").Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// FindActiveChannels 查找活跃的频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindActiveChannels() ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Where("is_active = ? AND push_enabled = ?", true, true).Order("created_at desc").Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// FindByChatID 根据 ChatID 查找频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindByChatID(chatID int64) (*entity.TelegramChannel, error) {
|
||||
var channel entity.TelegramChannel
|
||||
err := r.db.Where("chat_id = ?", chatID).First(&channel).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
// FindByChatType 根据类型查找频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindByChatType(chatType string) ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Where("chat_type = ?", chatType).Order("created_at desc").Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// FindActiveChannelsByTypes 根据多个类型查找活跃频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindActiveChannelsByTypes(chatTypes []string) ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Where("chat_type IN (?) AND is_active = ?", chatTypes, true).Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// UpdateLastPushAt 更新最后推送时间
|
||||
func (r *TelegramChannelRepositoryImpl) UpdateLastPushAt(id uint, lastPushAt time.Time) error {
|
||||
return r.db.Model(&entity.TelegramChannel{}).Where("id = ?", id).Update("last_push_at", lastPushAt).Error
|
||||
}
|
||||
|
||||
// FindDueForPush 查找需要推送的频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindDueForPush() ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
// 查找活跃、启用推送的频道,且距离上次推送已超过推送频率小时的记录
|
||||
|
||||
// 先获取所有活跃且启用推送的频道
|
||||
err := r.db.Where("is_active = ? AND push_enabled = ?", true, true).Find(&channels).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 在内存中过滤出需要推送的频道(更可靠的跨数据库方案)
|
||||
var dueChannels []entity.TelegramChannel
|
||||
now := time.Now()
|
||||
|
||||
// 用于去重的map,以chat_id为键
|
||||
seenChatIDs := make(map[int64]bool)
|
||||
|
||||
for _, channel := range channels {
|
||||
// 检查是否已经处理过这个chat_id(去重)
|
||||
if seenChatIDs[channel.ChatID] {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果从未推送过,或者距离上次推送已超过推送频率小时
|
||||
isDue := false
|
||||
if channel.LastPushAt == nil {
|
||||
isDue = true
|
||||
} else {
|
||||
// 计算下次推送时间:上次推送时间 + 推送频率分钟
|
||||
nextPushTime := channel.LastPushAt.Add(time.Duration(channel.PushFrequency) * time.Minute)
|
||||
if now.After(nextPushTime) {
|
||||
isDue = true
|
||||
}
|
||||
}
|
||||
|
||||
if isDue {
|
||||
dueChannels = append(dueChannels, channel)
|
||||
seenChatIDs[channel.ChatID] = true // 标记此chat_id已处理
|
||||
}
|
||||
}
|
||||
|
||||
return dueChannels, nil
|
||||
}
|
||||
|
||||
// CleanupDuplicateChannels 清理重复的频道记录,保留ID最小的记录
|
||||
func (r *TelegramChannelRepositoryImpl) CleanupDuplicateChannels() error {
|
||||
// 使用SQL查询找出重复的chat_id,并删除除了ID最小外的所有记录
|
||||
query := `
|
||||
DELETE t1 FROM telegram_channels t1
|
||||
INNER JOIN (
|
||||
SELECT chat_id, MIN(id) as min_id
|
||||
FROM telegram_channels
|
||||
GROUP BY chat_id
|
||||
HAVING COUNT(*) > 1
|
||||
) t2 ON t1.chat_id = t2.chat_id
|
||||
WHERE t1.id > t2.min_id
|
||||
`
|
||||
|
||||
return r.db.Exec(query).Error
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
<?php
|
||||
namespace netdisk\pan;
|
||||
|
||||
class QuarkPan extends BasePan
|
||||
{
|
||||
public function __construct($config = [])
|
||||
{
|
||||
parent::__construct($config);
|
||||
$this->urlHeader = [
|
||||
'Accept: application/json, text/plain, */*',
|
||||
'Accept-Language: zh-CN,zh;q=0.9',
|
||||
'content-type: application/json;charset=UTF-8',
|
||||
'sec-ch-ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
||||
'sec-ch-ua-mobile: ?0',
|
||||
'sec-ch-ua-platform: "Windows"',
|
||||
'sec-fetch-dest: empty',
|
||||
'sec-fetch-mode: cors',
|
||||
'sec-fetch-site: same-site',
|
||||
'Referer: https://pan.quark.cn/',
|
||||
'Referrer-Policy: strict-origin-when-cross-origin',
|
||||
'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',
|
||||
'cookie: ' . Config('qfshop.quark_cookie')
|
||||
];
|
||||
}
|
||||
|
||||
public function getFiles($pdir_fid=0)
|
||||
{
|
||||
// 原 getFiles 方法内容
|
||||
$urlData = [];
|
||||
$queryParams = [
|
||||
'pr' => 'ucpro',
|
||||
'fr' => 'pc',
|
||||
'uc_param_str' => '',
|
||||
'pdir_fid' => $pdir_fid,
|
||||
'_page' => 1,
|
||||
'_size' => 50,
|
||||
'_fetch_total' => 1,
|
||||
'_fetch_sub_dirs' => 0,
|
||||
'_sort' => 'file_type:asc,updated_at:desc',
|
||||
];
|
||||
|
||||
$res = curlHelper("https://drive-pc.quark.cn/1/clouddrive/file/sort", "GET", json_encode($urlData), $this->urlHeader,$queryParams)['body'];
|
||||
$res = json_decode($res, true);
|
||||
if($res['status'] !== 200){
|
||||
return jerr2($res['message']=='require login [guest]'?'夸克未登录,请检查cookie':$res['message']);
|
||||
}
|
||||
|
||||
return jok2('获取成功',$res['data']['list']);
|
||||
}
|
||||
|
||||
public function transfer($pwd_id)
|
||||
{
|
||||
if(empty($this->stoken)){
|
||||
//获取要转存夸克资源的stoken
|
||||
$res = $this->getStoken($pwd_id);
|
||||
if($res['status'] !== 200) return jerr2($res['message']);
|
||||
$infoData = $res['data'];
|
||||
|
||||
if($this->isType == 1){
|
||||
$urls['title'] = $infoData['title'];
|
||||
$urls['share_url'] = $this->url;
|
||||
$urls['stoken'] = $infoData['stoken'];
|
||||
return jok2('检验成功', $urls);
|
||||
}
|
||||
$stoken = $infoData['stoken'];
|
||||
$stoken = str_replace(' ', '+', $stoken);
|
||||
}else{
|
||||
$stoken = str_replace(' ', '+', $this->stoken);
|
||||
}
|
||||
|
||||
//获取要转存夸克资源的详细内容
|
||||
$res = $this->getShare($pwd_id,$stoken);
|
||||
if($res['status']!== 200) return jerr2($res['message']);
|
||||
$detail = $res['data'];
|
||||
|
||||
$fid_list = [];
|
||||
$fid_token_list = [];
|
||||
$title = $detail['share']['title']; //资源名称
|
||||
foreach ($detail['list'] as $key => $value) {
|
||||
$fid_list[] = $value['fid'];
|
||||
$fid_token_list[] = $value['share_fid_token'];
|
||||
}
|
||||
|
||||
//转存资源到指定文件夹
|
||||
$res = $this->getShareSave($pwd_id,$stoken,$fid_list,$fid_token_list);
|
||||
if($res['status']!== 200) return jerr2($res['message']);
|
||||
$task_id = $res['data']['task_id'];
|
||||
|
||||
//转存后根据task_id获取转存到自己网盘后的信息
|
||||
$retry_index = 0;
|
||||
$myData = '';
|
||||
while ($myData=='' || $myData['status'] != 2) {
|
||||
$res = $this->getShareTask($task_id, $retry_index);
|
||||
if($res['message']== 'capacity limit[{0}]'){
|
||||
return jerr2('容量不足');
|
||||
}
|
||||
if($res['status']!== 200) {
|
||||
return jerr2($res['message']);
|
||||
}
|
||||
$myData = $res['data'];
|
||||
$retry_index++;
|
||||
// 可以添加一个最大重试次数的限制,防止无限循环
|
||||
if ($retry_index > 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
//删除转存后可能有的广告
|
||||
$banned = Config('qfshop.quark_banned')??''; //如果出现这些字样就删除
|
||||
if(!empty($banned)){
|
||||
$bannedList = explode(',', $banned);
|
||||
$pdir_fid = $myData['save_as']['save_as_top_fids'][0];
|
||||
$dellist = [];
|
||||
$plist = $this->getPdirFid($pdir_fid);
|
||||
if(!empty($plist)){
|
||||
foreach ($plist as $key => $value) {
|
||||
// 检查$value['file_name']是否包含$bannedList中的任何一项
|
||||
$contains = false;
|
||||
foreach ($bannedList as $item) {
|
||||
if (strpos($value['file_name'], $item) !== false) {
|
||||
$contains = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($contains) {
|
||||
$dellist[] = $value['fid'];
|
||||
}
|
||||
}
|
||||
if(count($plist) === count($dellist)){
|
||||
//要删除的资源数如果和原数据资源数一样 就全部删除并终止下面的分享
|
||||
$this->deletepdirFid([$pdir_fid]);
|
||||
return jerr2("资源内容为空");
|
||||
}else{
|
||||
if (!empty($dellist)) {
|
||||
$this->deletepdirFid($dellist);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
$shareFid = $myData['save_as']['save_as_top_fids'];
|
||||
//分享资源并拿到更新后的task_id
|
||||
$res = $this->getShareBtn($myData['save_as']['save_as_top_fids'],$title);
|
||||
if($res['status']!== 200) return jerr2($res['message']);
|
||||
$task_id = $res['data']['task_id'];
|
||||
|
||||
//根据task_id拿到share_id
|
||||
$retry_index = 0;
|
||||
$myData = '';
|
||||
while ($myData=='' || $myData['status'] != 2) {
|
||||
$res = $this->getShareTask($task_id, $retry_index);
|
||||
if($res['status']!== 200) continue;
|
||||
$myData = $res['data'];
|
||||
$retry_index++;
|
||||
// 可以添加一个最大重试次数的限制,防止无限循环
|
||||
if ($retry_index > 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//根据share_id 获取到分享链接
|
||||
$res = $this->getSharePassword($myData['share_id']);
|
||||
if($res['status']!== 200) return jerr2($res['message']);
|
||||
$share = $res['data'];
|
||||
// $share['fid'] = $share['first_file']['fid'];
|
||||
$share['fid'] = (is_array($shareFid) && count($shareFid) > 1) ? $shareFid : $share['first_file']['fid'];
|
||||
|
||||
return jok2('转存成功', $share);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取要转存资源的stoken
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getStoken($pwd_id)
|
||||
{
|
||||
$urlData = array(
|
||||
'passcode' => '',
|
||||
'pwd_id' => $pwd_id,
|
||||
);
|
||||
$queryParams = [
|
||||
'pr' => 'ucpro',
|
||||
'fr' => 'pc',
|
||||
'uc_param_str' => '',
|
||||
];
|
||||
return $this->executeApiRequest(
|
||||
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/token",
|
||||
"POST",
|
||||
$urlData,
|
||||
$queryParams
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取要转存资源的详细内容
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getShare($pwd_id,$stoken)
|
||||
{
|
||||
$urlData = array();
|
||||
$queryParams = [
|
||||
"pr" => "ucpro",
|
||||
"fr" => "pc",
|
||||
"uc_param_str" => "",
|
||||
"pwd_id" => $pwd_id,
|
||||
"stoken" => $stoken,
|
||||
"pdir_fid" => "0",
|
||||
"force" => "0",
|
||||
"_page" => "1",
|
||||
"_size" => "100",
|
||||
"_fetch_banner" => "1",
|
||||
"_fetch_share" => "1",
|
||||
"_fetch_total" => "1",
|
||||
"_sort" => "file_type:asc,updated_at:desc"
|
||||
];
|
||||
return $this->executeApiRequest(
|
||||
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail",
|
||||
"GET",
|
||||
$urlData,
|
||||
$queryParams
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转存资源到指定文件夹
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getShareSave($pwd_id,$stoken,$fid_list,$fid_token_list)
|
||||
{
|
||||
if(!empty($this->to_pdir_fid)){
|
||||
$to_pdir_fid = $this->to_pdir_fid;
|
||||
}else{
|
||||
$to_pdir_fid = Config('qfshop.quark_file'); //默认存储路径
|
||||
if($this->expired_type == 2){
|
||||
$to_pdir_fid = Config('qfshop.quark_file_time'); //临时资源路径
|
||||
}
|
||||
}
|
||||
|
||||
$urlData = array(
|
||||
'fid_list' => $fid_list,
|
||||
'fid_token_list' => $fid_token_list,
|
||||
'to_pdir_fid' => $to_pdir_fid,
|
||||
'pwd_id' => $pwd_id,
|
||||
'stoken' => $stoken,
|
||||
'pdir_fid' => "0",
|
||||
'scene' => "link",
|
||||
);
|
||||
$queryParams = [
|
||||
"entry" => "update_share",
|
||||
"pr" => "ucpro",
|
||||
"fr" => "pc",
|
||||
"uc_param_str" => ""
|
||||
];
|
||||
|
||||
return $this->executeApiRequest(
|
||||
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/save",
|
||||
"POST",
|
||||
$urlData,
|
||||
$queryParams
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享资源拿到task_id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getShareBtn($fid_list,$title)
|
||||
{
|
||||
if(!empty($this->ad_fid)){
|
||||
$fid_list[] = $this->ad_fid;
|
||||
}
|
||||
$urlData = array(
|
||||
'fid_list' => $fid_list,
|
||||
'expired_type' => $this->expired_type,
|
||||
'title' => $title,
|
||||
'url_type' => 1,
|
||||
);
|
||||
$queryParams = [
|
||||
"pr" => "ucpro",
|
||||
"fr" => "pc",
|
||||
"uc_param_str" => ""
|
||||
];
|
||||
|
||||
return $this->executeApiRequest(
|
||||
"https://drive-pc.quark.cn/1/clouddrive/share",
|
||||
"POST",
|
||||
$urlData,
|
||||
$queryParams
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据task_id拿到自己的资源信息
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getShareTask($task_id,$retry_index)
|
||||
{
|
||||
$urlData = array();
|
||||
$queryParams = [
|
||||
"pr" => "ucpro",
|
||||
"fr" => "pc",
|
||||
"uc_param_str" => "",
|
||||
"task_id" => $task_id,
|
||||
"retry_index" => $retry_index
|
||||
];
|
||||
return $this->executeApiRequest(
|
||||
"https://drive-pc.quark.cn/1/clouddrive/task",
|
||||
"GET",
|
||||
$urlData,
|
||||
$queryParams
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据share_id 获取到分享链接
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getSharePassword($share_id)
|
||||
{
|
||||
$urlData = array(
|
||||
'share_id' => $share_id,
|
||||
);
|
||||
$queryParams = [
|
||||
"pr" => "ucpro",
|
||||
"fr" => "pc",
|
||||
"uc_param_str" => ""
|
||||
];
|
||||
return $this->executeApiRequest(
|
||||
"https://drive-pc.quark.cn/1/clouddrive/share/password",
|
||||
"POST",
|
||||
$urlData,
|
||||
$queryParams
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deletepdirFid($filelist)
|
||||
{
|
||||
$urlData = array(
|
||||
'action_type' => 2,
|
||||
'exclude_fids' => [],
|
||||
'filelist' => $filelist,
|
||||
);
|
||||
$queryParams = [
|
||||
"pr" => "ucpro",
|
||||
"fr" => "pc",
|
||||
"uc_param_str" => ""
|
||||
];
|
||||
return $this->executeApiRequest(
|
||||
"https://drive-pc.quark.cn/1/clouddrive/file/delete",
|
||||
"POST",
|
||||
$urlData,
|
||||
$queryParams
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取夸克网盘指定文件夹内容
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getPdirFid($pdir_fid)
|
||||
{
|
||||
$urlData = [];
|
||||
$queryParams = [
|
||||
'pr' => 'ucpro',
|
||||
'fr' => 'pc',
|
||||
'uc_param_str' => '',
|
||||
'pdir_fid' => $pdir_fid,
|
||||
'_page' => 1,
|
||||
'_size' => 200,
|
||||
'_fetch_total' => 1,
|
||||
'_fetch_sub_dirs' => 0,
|
||||
'_sort' => 'file_type:asc,updated_at:desc',
|
||||
];
|
||||
try {
|
||||
$res = curlHelper("https://drive-pc.quark.cn/1/clouddrive/file/sort", "GET", json_encode($urlData), $this->urlHeader,$queryParams)['body'];
|
||||
$res = json_decode($res, true);
|
||||
if($res['status'] !== 200){
|
||||
return [];
|
||||
}
|
||||
return $res['data']['list'];
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行API请求并处理重试逻辑
|
||||
*
|
||||
* @param string $url 请求URL
|
||||
* @param string $method 请求方法(GET/POST)
|
||||
* @param array $data 请求数据
|
||||
* @param array $queryParams 查询参数
|
||||
* @param int $maxRetries 最大重试次数
|
||||
* @param int $retryDelay 重试延迟(秒)
|
||||
* @return array 响应结果
|
||||
*/
|
||||
protected function executeApiRequest($url, $method, $data = [], $queryParams = [], $maxRetries = 3, $retryDelay = 2)
|
||||
{
|
||||
$attempt = 0;
|
||||
while ($attempt < $maxRetries) {
|
||||
$attempt++;
|
||||
try {
|
||||
$res = curlHelper($url, $method, json_encode($data), $this->urlHeader, $queryParams)['body'];
|
||||
return json_decode($res, true);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logApiError($url, $attempt, $e->getMessage());
|
||||
if ($attempt < $maxRetries) {
|
||||
sleep($retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['status' => 500, 'message' => '接口请求异常'];
|
||||
}
|
||||
/**
|
||||
* 记录API错误日志
|
||||
*
|
||||
* @param string $prefix 日志前缀
|
||||
* @param int $attempt 尝试次数
|
||||
* @param mixed $error 错误信息
|
||||
*/
|
||||
protected function logApiError($prefix, $attempt, $error)
|
||||
{
|
||||
$errorMsg = is_scalar($error) ? $error : json_encode($error);
|
||||
$logMessage = date('Y-m-d H:i:s') . ' ' . $prefix . '请求失败(尝试次数: ' . $attempt . ') 错误: ' . $errorMsg . "\n";
|
||||
file_put_contents('error.log', $logMessage, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
596
demo/pan/XunleiPan.php
Normal file
596
demo/pan/XunleiPan.php
Normal file
@@ -0,0 +1,596 @@
|
||||
<?php
|
||||
|
||||
namespace netdisk\pan;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
class XunleiPan extends BasePan
|
||||
{
|
||||
private $clientId = 'Xqp0kJBXWhwaTpB6';
|
||||
private $deviceId = '925b7631473a13716b791d7f28289cad';
|
||||
|
||||
public function __construct($config = [])
|
||||
{
|
||||
parent::__construct($config);
|
||||
|
||||
$this->urlHeader = [
|
||||
'Accept: */*',
|
||||
'Accept-Encoding: gzip, deflate',
|
||||
'Accept-Language: zh-CN,zh;q=0.9',
|
||||
'Cache-Control: no-cache',
|
||||
'Content-Type: application/json',
|
||||
'Origin: https://pan.xunlei.com',
|
||||
'Pragma: no-cache',
|
||||
'Priority: u=1,i',
|
||||
'Referer: https://pan.xunlei.com/',
|
||||
'sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
|
||||
'sec-ch-ua-mobile: ?0',
|
||||
'sec-ch-ua-platform: "Windows"',
|
||||
'sec-fetch-dest: empty',
|
||||
'sec-fetch-mode: cors',
|
||||
'sec-fetch-site: same-site',
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
|
||||
'Authorization: ',
|
||||
'x-captcha-token: ',
|
||||
'x-client-id: ' . $this->clientId,
|
||||
'x-device-id: ' . $this->deviceId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 核心方法:获取 Access Token(内部包含缓存判断、刷新、保存)
|
||||
*/
|
||||
private function getAccessToken()
|
||||
{
|
||||
$tokenFile = __DIR__ . '/xunlei_token.json';
|
||||
|
||||
// 1️⃣ 先读取缓存
|
||||
if (file_exists($tokenFile)) {
|
||||
$data = json_decode(file_get_contents($tokenFile), true);
|
||||
if (isset($data['access_token'], $data['expires_at']) && time() < $data['expires_at']) {
|
||||
return $data['access_token']; // 缓存有效
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ 构造请求体
|
||||
$body = [
|
||||
'client_id' => $this->clientId,
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => Config('qfshop.xunlei_cookie')
|
||||
];
|
||||
|
||||
// 3️⃣ 构造请求头(直接传入,不用处理 Authorization/x-captcha-token)
|
||||
$headers = array_filter($this->urlHeader, function ($h) {
|
||||
return strpos($h, 'Authorization') === false && strpos($h, 'x-captcha-token') === false;
|
||||
});
|
||||
|
||||
// 4️⃣ 调用封装请求方法
|
||||
$res = $this->requestXunleiApi(
|
||||
'https://xluser-ssl.xunlei.com/v1/auth/token',
|
||||
'POST',
|
||||
$body,
|
||||
[], // GET 参数为空
|
||||
$headers // headers 直接传入
|
||||
);
|
||||
|
||||
// 5️⃣ 判断返回
|
||||
if ($res['code'] !== 0 || !isset($res['data']['access_token'])) {
|
||||
return ''; // 获取失败
|
||||
}
|
||||
|
||||
$resData = $res['data'];
|
||||
|
||||
// 6️⃣ 计算过期时间(当前时间 + expires_in - 60 秒缓冲)
|
||||
$expiresAt = time() + intval($resData['expires_in']) - 60;
|
||||
|
||||
// 7️⃣ 缓存到文件
|
||||
file_put_contents($tokenFile, json_encode([
|
||||
'access_token' => $resData['access_token'],
|
||||
'refresh_token' => $resData['refresh_token'],
|
||||
'expires_at' => $expiresAt
|
||||
]));
|
||||
|
||||
// 8️⃣ 同步刷新 refresh_token 到数据库
|
||||
Db::name('conf')->where('conf_key', 'xunlei_cookie')->update([
|
||||
'conf_value' => $resData['refresh_token']
|
||||
]);
|
||||
|
||||
// 9️⃣ 返回 token
|
||||
return $resData['access_token'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ✅ 获取 captcha_token
|
||||
*/
|
||||
private function getCaptchaToken()
|
||||
{
|
||||
$tokenFile = __DIR__ . '/xunlei_captcha.json';
|
||||
|
||||
// 1️⃣ 先读取缓存
|
||||
if (file_exists($tokenFile)) {
|
||||
$data = json_decode(file_get_contents($tokenFile), true);
|
||||
if (isset($data['captcha_token']) && isset($data['expires_at'])) {
|
||||
if (time() < $data['expires_at']) {
|
||||
return $data['captcha_token']; // 缓存有效
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ 构造请求体
|
||||
$body = [
|
||||
'client_id' => $this->clientId,
|
||||
'action' => "get:/drive/v1/share",
|
||||
'device_id' => $this->deviceId,
|
||||
'meta' => [
|
||||
'username' => '',
|
||||
'phone_number' => '',
|
||||
'email' => '',
|
||||
'package_name' => 'pan.xunlei.com',
|
||||
'client_version' => '1.45.0',
|
||||
'captcha_sign' => '1.fe2108ad808a74c9ac0243309242726c',
|
||||
'timestamp' => '1645241033384',
|
||||
'user_id' => '0'
|
||||
]
|
||||
];
|
||||
|
||||
// 3️⃣ 构造请求头
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'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'
|
||||
];
|
||||
|
||||
// 4️⃣ 调用封装请求方法
|
||||
$res = $this->requestXunleiApi(
|
||||
"https://xluser-ssl.xunlei.com/v1/shield/captcha/init",
|
||||
'POST',
|
||||
$body,
|
||||
[], // GET 参数为空
|
||||
$headers // headers 传入即用
|
||||
);
|
||||
|
||||
if ($res['code'] !== 0 || !isset($res['data']['captcha_token'])) {
|
||||
return ''; // 获取失败
|
||||
}
|
||||
|
||||
$data = $res['data'];
|
||||
|
||||
// 5️⃣ 计算过期时间(当前时间 + expires_in - 10 秒缓冲)
|
||||
$expiresAt = time() + intval($data['expires_in']) - 10;
|
||||
|
||||
// 6️⃣ 缓存到文件
|
||||
file_put_contents($tokenFile, json_encode([
|
||||
'captcha_token' => $data['captcha_token'],
|
||||
'expires_at' => $expiresAt
|
||||
]));
|
||||
|
||||
return $data['captcha_token'];
|
||||
}
|
||||
|
||||
|
||||
public function getFiles($pdir_fid = '')
|
||||
{
|
||||
// 1️⃣ 获取 AccessToken
|
||||
$accessToken = $this->getAccessToken();
|
||||
if (empty($accessToken)) {
|
||||
return jerr2('登录状态异常,获取accessToken失败');
|
||||
}
|
||||
|
||||
// 2️⃣ 获取 CaptchaToken
|
||||
$captchaToken = $this->getCaptchaToken();
|
||||
if (empty($captchaToken)) {
|
||||
return jerr2('获取 captchaToken 失败');
|
||||
}
|
||||
|
||||
// 3️⃣ 构造 headers
|
||||
$headers = array_map(function ($h) use ($accessToken, $captchaToken) {
|
||||
if (str_starts_with($h, 'Authorization: ')) {
|
||||
return 'Authorization: Bearer ' . $accessToken;
|
||||
}
|
||||
if (str_starts_with($h, 'x-captcha-token: ')) {
|
||||
return 'x-captcha-token: ' . $captchaToken;
|
||||
}
|
||||
return $h;
|
||||
}, $this->urlHeader);
|
||||
|
||||
// 4️⃣ 构造请求体和 GET 参数
|
||||
$filters = [
|
||||
"phase" => ["eq" => "PHASE_TYPE_COMPLETE"],
|
||||
"trashed" => ["eq" => false],
|
||||
];
|
||||
|
||||
$filtersStr = urlencode(json_encode($filters));
|
||||
$urlData = [];
|
||||
$queryParams = [
|
||||
'parent_id' => $pdir_fid ?: '',
|
||||
'filters' => '{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}',
|
||||
'with_audit' => true,
|
||||
'thumbnail_size' => 'SIZE_SMALL',
|
||||
'limit' => 50,
|
||||
];
|
||||
|
||||
// 5️⃣ 调用封装方法请求
|
||||
$res = $this->requestXunleiApi(
|
||||
"https://api-pan.xunlei.com/drive/v1/files",
|
||||
'GET',
|
||||
$urlData,
|
||||
$queryParams,
|
||||
$headers
|
||||
);
|
||||
|
||||
// 6️⃣ 检查结果
|
||||
if ($res['code'] !== 0 || !isset($res['data']['files'])) {
|
||||
return jerr2($res['msg'] ?? '获取文件列表失败');
|
||||
}
|
||||
return jok2('获取成功', $res['data']['files']);
|
||||
}
|
||||
|
||||
|
||||
public function transfer($pwd_id)
|
||||
{
|
||||
// 1️⃣ 获取 AccessToken
|
||||
$accessToken = $this->getAccessToken();
|
||||
if (empty($accessToken)) {
|
||||
return jerr2('登录状态异常');
|
||||
}
|
||||
|
||||
// 2️⃣ 获取 CaptchaToken
|
||||
$captchaToken = $this->getCaptchaToken();
|
||||
if (empty($captchaToken)) {
|
||||
return jerr2('登录异常');
|
||||
}
|
||||
|
||||
// 3️⃣ 构造 headers
|
||||
$this->urlHeader = array_map(function ($h) use ($accessToken, $captchaToken) {
|
||||
if (str_starts_with($h, 'Authorization: ')) {
|
||||
return 'Authorization: Bearer ' . $accessToken;
|
||||
}
|
||||
if (str_starts_with($h, 'x-captcha-token: ')) {
|
||||
return 'x-captcha-token: ' . $captchaToken;
|
||||
}
|
||||
return $h;
|
||||
}, $this->urlHeader);
|
||||
|
||||
$pwd_id = strtok($pwd_id, '?');
|
||||
$this->code = str_replace('#', '', $this->code);
|
||||
|
||||
$res = $this->getShare($pwd_id, $this->code);
|
||||
if ($res['code'] !== 200) return jerr2($res['message']);
|
||||
$infoData = $res['data'];
|
||||
|
||||
if ($this->isType == 1) {
|
||||
$urls['title'] = $infoData['title'];
|
||||
$urls['share_url'] = $this->url;
|
||||
$urls['stoken'] = '';
|
||||
return jok2('检验成功', $urls);
|
||||
}
|
||||
|
||||
//转存到网盘
|
||||
$res = $this->getRestore($pwd_id, $infoData);
|
||||
if ($res['code'] !== 200) return jerr2($res['message']);
|
||||
|
||||
|
||||
//获取转存后的文件信息
|
||||
$tasData = $res['data'];
|
||||
$retry_index = 0;
|
||||
$myData = '';
|
||||
while ($myData == '' || $myData['progress'] != 100) {
|
||||
$res = $this->getTasks($tasData);
|
||||
if ($res['code'] !== 200) return jerr2($res['message']);
|
||||
$myData = $res['data'];
|
||||
$retry_index++;
|
||||
// 可以添加一个最大重试次数的限制,防止无限循环
|
||||
if ($retry_index > 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($myData['progress'] != 100) {
|
||||
return jerr2($myData['message'] ?? '转存失败');
|
||||
}
|
||||
|
||||
$result = [];
|
||||
if (isset($myData['params']['trace_file_ids']) && !empty($myData['params']['trace_file_ids'])) {
|
||||
$traceData = json_decode($myData['params']['trace_file_ids'], true);
|
||||
if (is_array($traceData)) {
|
||||
$result = array_values($traceData);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
//删除转存后可能有的广告
|
||||
$banned = Config('qfshop.quark_banned') ?? ''; //如果出现这些字样就删除
|
||||
if (!empty($banned)) {
|
||||
$bannedList = explode(',', $banned);
|
||||
$pdir_fid = $result[0];
|
||||
$dellist = [];
|
||||
$plists = $this->getFiles($pdir_fid);
|
||||
$plist = $plists['data'];
|
||||
if (!empty($plist)) {
|
||||
foreach ($plist as $key => $value) {
|
||||
// 检查$value['name']是否包含$bannedList中的任何一项
|
||||
$contains = false;
|
||||
foreach ($bannedList as $item) {
|
||||
if (strpos($value['name'], $item) !== false) {
|
||||
$contains = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($contains) {
|
||||
$dellist[] = $value['id'];
|
||||
}
|
||||
}
|
||||
if (count($plist) === count($dellist)) {
|
||||
//要删除的资源数如果和原数据资源数一样 就全部删除并终止下面的分享
|
||||
$this->deletepdirFid([$pdir_fid]);
|
||||
return jerr2("资源内容为空");
|
||||
} else {
|
||||
if (!empty($dellist)) {
|
||||
$this->deletepdirFid($dellist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
//根据share_id 获取到分享链接
|
||||
$res = $this->getSharePassword($result);
|
||||
if ($res['code'] !== 200) return jerr2($res['message']);
|
||||
|
||||
|
||||
$title = $infoData['files'][0]['name'] ?? '';
|
||||
$share = [
|
||||
'title' => $title,
|
||||
'share_url' => $res['data']['share_url'] . '?pwd=' . $res['data']['pass_code'],
|
||||
'code' => $res['data']['pass_code'],
|
||||
'fid' => $result,
|
||||
];
|
||||
|
||||
return jok2('转存成功', $share);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源分享信息
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getShare($pwd_id, $pass_code)
|
||||
{
|
||||
$urlData = [];
|
||||
$queryParams = [
|
||||
'share_id' => $pwd_id,
|
||||
'pass_code' => $pass_code,
|
||||
'limit' => 100,
|
||||
'pass_code_token' => '',
|
||||
'page_token' => '',
|
||||
'thumbnail_size' => 'SIZE_SMALL',
|
||||
];
|
||||
$res = $this->requestXunleiApi(
|
||||
"https://api-pan.xunlei.com/drive/v1/share",
|
||||
'GET',
|
||||
$urlData,
|
||||
$queryParams,
|
||||
$this->urlHeader
|
||||
);
|
||||
if (!empty($res['data']['error_code'])) {
|
||||
return jerr2($res['data']['error_description'] ?? 'getShare失败');
|
||||
}
|
||||
if (isset($res['data']['share_status']) && $res['data']['share_status'] !== 'OK') {
|
||||
if (!empty($res['data']['share_status_text'])) {
|
||||
return jerr2($res['data']['share_status_text']);
|
||||
}
|
||||
|
||||
if ($res['data']['share_status'] === 'SENSITIVE_RESOURCE') {
|
||||
return jerr2('该分享内容可能因为涉及侵权、色情、反动、低俗等信息,无法访问!');
|
||||
}
|
||||
|
||||
return jerr2('资源已失效');
|
||||
}
|
||||
|
||||
return jok2('ok', $res['data']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转存到网盘
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getRestore($pwd_id, $infoData)
|
||||
{
|
||||
$parent_id = Config('qfshop.xunlei_file'); //默认存储路径
|
||||
if ($this->expired_type == 2) {
|
||||
$parent_id = Config('qfshop.xunlei_file_time'); //临时资源路径
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
if (isset($infoData['files']) && is_array($infoData['files']) && !empty($infoData['files'])) {
|
||||
$ids = array_column($infoData['files'], 'id');
|
||||
}
|
||||
|
||||
$urlData = [
|
||||
'parent_id' => $parent_id,
|
||||
'share_id' => $pwd_id,
|
||||
"pass_code_token" => $infoData['pass_code_token'],
|
||||
'ancestor_ids' => [],
|
||||
'specify_parent_id' => true,
|
||||
'file_ids' => $ids,
|
||||
];
|
||||
$queryParams = [];
|
||||
$res = $this->requestXunleiApi(
|
||||
"https://api-pan.xunlei.com/drive/v1/share/restore",
|
||||
'POST',
|
||||
$urlData,
|
||||
$queryParams,
|
||||
$this->urlHeader
|
||||
);
|
||||
if (!empty($res['data']['error_code'])) {
|
||||
return jerr2($res['data']['error_description'] ?? 'getRestore失败');
|
||||
}
|
||||
return jok2('ok', $res['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取转存后的文件信息
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getTasks($infoData)
|
||||
{
|
||||
$urlData = [];
|
||||
$queryParams = [];
|
||||
$res = $this->requestXunleiApi(
|
||||
"https://api-pan.xunlei.com/drive/v1/tasks/" . $infoData['restore_task_id'],
|
||||
'GET',
|
||||
$urlData,
|
||||
$queryParams,
|
||||
$this->urlHeader
|
||||
);
|
||||
if (!empty($res['data']['error_code'])) {
|
||||
return jerr2($res['data']['error_description'] ?? 'getTasks失败');
|
||||
}
|
||||
return jok2('ok', $res['data']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取分享链接
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getSharePassword($result)
|
||||
{
|
||||
// $result[] = '';
|
||||
$expiration_days = '-1';
|
||||
if ($this->expired_type == 2) {
|
||||
$expiration_days = '2';
|
||||
}
|
||||
$urlData = [
|
||||
'file_ids' => $result,
|
||||
'share_to' => 'copy',
|
||||
'params' => [
|
||||
'subscribe_push' => 'false',
|
||||
'WithPassCodeInLink' => 'true'
|
||||
],
|
||||
'title' => '云盘资源分享',
|
||||
'restore_limit' => '-1',
|
||||
'expiration_days' => $expiration_days
|
||||
];
|
||||
|
||||
$queryParams = [];
|
||||
$res = $this->requestXunleiApi(
|
||||
"https://api-pan.xunlei.com/drive/v1/share",
|
||||
'POST',
|
||||
$urlData,
|
||||
$queryParams,
|
||||
$this->urlHeader
|
||||
);
|
||||
if (!empty($res['data']['error_code'])) {
|
||||
return jerr2($res['data']['error_description'] ?? 'getSharePassword失败');
|
||||
}
|
||||
return jok2('ok', $res['data']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deletepdirFid($filelist)
|
||||
{
|
||||
// 1️⃣ 获取 AccessToken
|
||||
$accessToken = $this->getAccessToken();
|
||||
if (empty($accessToken)) {
|
||||
return jerr2('登录状态异常,获取accessToken失败');
|
||||
}
|
||||
|
||||
// 2️⃣ 获取 CaptchaToken
|
||||
$captchaToken = $this->getCaptchaToken();
|
||||
if (empty($captchaToken)) {
|
||||
return jerr2('获取 captchaToken 失败');
|
||||
}
|
||||
|
||||
// 3️⃣ 构造 headers
|
||||
$this->urlHeader = array_map(function ($h) use ($accessToken, $captchaToken) {
|
||||
if (str_starts_with($h, 'Authorization: ')) {
|
||||
return 'Authorization: Bearer ' . $accessToken;
|
||||
}
|
||||
if (str_starts_with($h, 'x-captcha-token: ')) {
|
||||
return 'x-captcha-token: ' . $captchaToken;
|
||||
}
|
||||
return $h;
|
||||
}, $this->urlHeader);
|
||||
|
||||
$urlData = [
|
||||
'ids' => $filelist,
|
||||
'space' => ''
|
||||
];
|
||||
|
||||
$queryParams = [];
|
||||
$res = $this->requestXunleiApi(
|
||||
"https://api-pan.xunlei.com/drive/v1/files:batchDelete",
|
||||
'POST',
|
||||
$urlData,
|
||||
$queryParams,
|
||||
$this->urlHeader
|
||||
);
|
||||
|
||||
return ['status' => 200];
|
||||
}
|
||||
|
||||
/**
|
||||
* Xunlei API 通用请求方法
|
||||
*
|
||||
* @param string $url 接口地址
|
||||
* @param string $method GET 或 POST
|
||||
* @param array $data POST 数据
|
||||
* @param array $query GET 查询参数
|
||||
* @param array $headers 请求头,传啥用啥
|
||||
* @return array 返回解析后的 JSON 或错误信息
|
||||
*/
|
||||
private function requestXunleiApi(
|
||||
string $url,
|
||||
string $method = 'GET',
|
||||
array $data = [],
|
||||
array $query = [],
|
||||
array $headers = []
|
||||
): array {
|
||||
// 拼接 GET 参数
|
||||
if (!empty($query)) {
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "gzip, deflate"); // 明确只使用gzip和deflate编码
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 不验证证书
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 不验证域名
|
||||
|
||||
if (strtoupper($method) === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
} elseif (strtoupper($method) === 'PATCH') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
}
|
||||
|
||||
|
||||
$body = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errno) return ['code' => 1, 'msg' => "请求失败: $error"];
|
||||
|
||||
$json = json_decode($body, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return ['code' => 1, 'msg' => '返回 JSON 解析失败', 'raw' => $body];
|
||||
}
|
||||
|
||||
return ['code' => 0, 'data' => $json];
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
image: ctwj/urldb-backend:1.2.3
|
||||
image: ctwj/urldb-backend:1.3.4
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
@@ -38,10 +38,10 @@ services:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
image: ctwj/urldb-frontend:1.2.3
|
||||
image: ctwj/urldb-frontend:1.3.4
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NUXT_PUBLIC_API_SERVER: http://backend:8080/api
|
||||
NUXT_PUBLIC_API_CLIENT: /api
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
doc.l9.lc
|
||||
@@ -1,51 +0,0 @@
|
||||
# 🚀 urlDB - 老九网盘资源数据库
|
||||
|
||||
> 一个现代化的老九网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 🎯 支持的网盘平台
|
||||
|
||||
| 平台 | 录入 | 转存 | 分享 |
|
||||
|------|-------|-----|------|
|
||||
| 百度网盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 阿里云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 夸克网盘 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
|
||||
| 天翼云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 迅雷云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| UC网盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 123云盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
| 115网盘 | ✅ 支持 | 🚧 开发中 | 🚧 开发中 |
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
### 🎯 核心功能
|
||||
- **📁 多平台网盘支持** - 支持多种主流网盘平台
|
||||
- **🔍 公开API** - 支持API数据录入,资源搜索
|
||||
- **🏷️ 自动预处理** - 系统自动处理资源,对数据进行有效性判断
|
||||
- **📊 自动转存分享** - 有效资源,如果属于支持类型将自动转存分享
|
||||
- **📱 多账号管理** - 同平台支持多账号管理
|
||||
|
||||
## 📞 联系我们
|
||||
|
||||
- **项目地址**: [https://github.com/ctwj/urldb](https://github.com/ctwj/urldb)
|
||||
- **问题反馈**: [Issues](https://github.com/ctwj/urldb/issues)
|
||||
- **邮箱**: 510199617@qq.com
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**如果这个项目对您有帮助,请给我们一个 ⭐ Star!**
|
||||
|
||||
Made with ❤️ by [老九]
|
||||
|
||||
</div>
|
||||
@@ -1,177 +0,0 @@
|
||||
# 文档使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
本项目使用 [docsify](https://docsify.js.org/) 生成文档网站。docsify 是一个轻量级的文档生成器,无需构建静态文件,只需要一个 `index.html` 文件即可。
|
||||
|
||||
## 文档结构
|
||||
|
||||
```
|
||||
docs/
|
||||
├── index.html # 文档主页
|
||||
├── docsify.config.js # docsify 配置文件
|
||||
├── README.md # 首页内容
|
||||
├── _sidebar.md # 侧边栏导航
|
||||
├── start-docs.sh # 启动脚本
|
||||
├── guide/ # 使用指南
|
||||
│ ├── quick-start.md # 快速开始
|
||||
│ ├── local-development.md # 本地开发
|
||||
│ └── docker-deployment.md # Docker 部署
|
||||
├── api/ # API 文档
|
||||
│ └── overview.md # API 概览
|
||||
├── architecture/ # 架构文档
|
||||
│ └── overview.md # 架构概览
|
||||
├── faq.md # 常见问题
|
||||
├── changelog.md # 更新日志
|
||||
└── license.md # 许可证
|
||||
```
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 方法一:使用启动脚本(推荐)
|
||||
|
||||
```bash
|
||||
# 进入文档目录
|
||||
cd docs
|
||||
|
||||
# 运行启动脚本
|
||||
./start-docs.sh
|
||||
```
|
||||
|
||||
脚本会自动:
|
||||
- 检查是否安装了 docsify-cli
|
||||
- 如果没有安装,会自动安装
|
||||
- 启动文档服务
|
||||
- 在浏览器中打开文档
|
||||
|
||||
### 方法二:手动启动
|
||||
|
||||
```bash
|
||||
# 安装 docsify-cli(如果未安装)
|
||||
npm install -g docsify-cli
|
||||
|
||||
# 进入文档目录
|
||||
cd docs
|
||||
|
||||
# 启动服务
|
||||
docsify serve . --port 3000 --open
|
||||
```
|
||||
|
||||
## 访问文档
|
||||
|
||||
启动成功后,文档将在以下地址可用:
|
||||
- 本地访问:http://localhost:3000
|
||||
- 局域网访问:http://[你的IP]:3000
|
||||
|
||||
## 文档特性
|
||||
|
||||
### 1. 搜索功能
|
||||
- 支持全文搜索
|
||||
- 搜索结果高亮显示
|
||||
- 支持中文搜索
|
||||
|
||||
### 2. 代码高亮
|
||||
支持多种编程语言的语法高亮:
|
||||
- Go
|
||||
- JavaScript/TypeScript
|
||||
- SQL
|
||||
- YAML
|
||||
- JSON
|
||||
- Bash
|
||||
|
||||
### 3. 代码复制
|
||||
- 一键复制代码块
|
||||
- 复制成功提示
|
||||
|
||||
### 4. 页面导航
|
||||
- 侧边栏导航
|
||||
- 页面间导航
|
||||
- 自动回到顶部
|
||||
|
||||
### 5. 响应式设计
|
||||
- 支持移动端访问
|
||||
- 自适应屏幕尺寸
|
||||
|
||||
## 自定义配置
|
||||
|
||||
### 修改主题
|
||||
在 `docsify.config.js` 中修改配置:
|
||||
|
||||
```javascript
|
||||
window.$docsify = {
|
||||
name: '你的项目名称',
|
||||
repo: '你的仓库地址',
|
||||
// 其他配置...
|
||||
}
|
||||
```
|
||||
|
||||
### 添加新页面
|
||||
1. 在相应目录下创建 `.md` 文件
|
||||
2. 在 `_sidebar.md` 中添加导航链接
|
||||
3. 刷新页面即可看到新页面
|
||||
|
||||
### 修改样式
|
||||
可以通过添加自定义 CSS 来修改样式:
|
||||
|
||||
```html
|
||||
<!-- 在 index.html 中添加 -->
|
||||
<link rel="stylesheet" href="./custom.css">
|
||||
```
|
||||
|
||||
## 部署到生产环境
|
||||
|
||||
### 静态部署
|
||||
docsify 生成的文档可以部署到任何静态文件服务器:
|
||||
|
||||
```bash
|
||||
# 构建静态文件(可选)
|
||||
docsify generate docs docs/_site
|
||||
|
||||
# 部署到 GitHub Pages
|
||||
git subtree push --prefix docs origin gh-pages
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
```bash
|
||||
# 使用 nginx 镜像
|
||||
docker run -d -p 80:80 -v $(pwd)/docs:/usr/share/nginx/html nginx
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 启动时提示端口被占用
|
||||
A: 可以指定其他端口:
|
||||
```bash
|
||||
docsify serve . --port 3001
|
||||
```
|
||||
|
||||
### Q: 搜索功能不工作
|
||||
A: 确保在 `index.html` 中引入了搜索插件:
|
||||
```html
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
|
||||
```
|
||||
|
||||
### Q: 代码高亮不显示
|
||||
A: 确保引入了相应的 Prism.js 组件:
|
||||
```html
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-go.min.js"></script>
|
||||
```
|
||||
|
||||
## 维护说明
|
||||
|
||||
### 更新文档
|
||||
1. 修改相应的 `.md` 文件
|
||||
2. 刷新浏览器即可看到更新
|
||||
|
||||
### 添加新功能
|
||||
1. 在 `docsify.config.js` 中添加插件配置
|
||||
2. 在 `index.html` 中引入相应的插件文件
|
||||
|
||||
### 版本控制
|
||||
建议将文档与代码一起进行版本控制,确保文档与代码版本同步。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [docsify 官方文档](https://docsify.js.org/)
|
||||
- [docsify 插件市场](https://docsify.js.org/#/plugins)
|
||||
- [Markdown 语法指南](https://docsify.js.org/#/zh-cn/markdown)
|
||||
@@ -1,15 +0,0 @@
|
||||
<!-- docs/_sidebar.md -->
|
||||
|
||||
* [🏠 首页](/)
|
||||
* [🚀 快速开始](guide/quick-start.md)
|
||||
* [🐳 Docker部署](guide/docker-deployment.md)
|
||||
* [💻 本地开发](guide/local-development.md)
|
||||
|
||||
* 📚 API 文档
|
||||
* [公开API](api/overview.md)
|
||||
|
||||
* 📄 其他
|
||||
* [常见问题](faq.md)
|
||||
* [更新日志](changelog.md)
|
||||
* [许可证](license.md)
|
||||
* [版本管理](github-version-management.md)
|
||||
@@ -1,418 +0,0 @@
|
||||
# API 文档概览
|
||||
|
||||
## 概述
|
||||
|
||||
老九网盘资源数据库提供了一套完整的 RESTful API 接口,支持资源管理、搜索、热门剧获取等功能。所有 API 都需要进行认证,使用 API Token 进行身份验证。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础URL**: `http://localhost:8080/api`
|
||||
- **认证方式**: API Token
|
||||
- **数据格式**: JSON
|
||||
- **字符编码**: UTF-8
|
||||
|
||||
## 认证说明
|
||||
|
||||
### 认证方式
|
||||
|
||||
所有 API 都需要提供 API Token 进行认证,支持两种方式:
|
||||
|
||||
1. **请求头方式**(推荐)
|
||||
```
|
||||
X-API-Token: your_token_here
|
||||
```
|
||||
|
||||
2. **查询参数方式**
|
||||
```
|
||||
?api_token=your_token_here
|
||||
```
|
||||
|
||||
### 获取 Token
|
||||
|
||||
请联系管理员在系统配置中设置 API Token。
|
||||
|
||||
## API 接口列表
|
||||
|
||||
### 1. 单个添加资源
|
||||
|
||||
**接口描述**: 添加单个资源到待处理列表
|
||||
|
||||
**请求信息**:
|
||||
- **方法**: `POST`
|
||||
- **路径**: `/api/public/resources/add`
|
||||
- **认证**: 必需
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"title": "资源标题",
|
||||
"description": "资源描述",
|
||||
"url": "资源链接",
|
||||
"category": "分类名称",
|
||||
"tags": "标签1,标签2",
|
||||
"img": "封面图片链接",
|
||||
"source": "数据来源",
|
||||
"extra": "额外信息"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "资源添加成功,已进入待处理列表",
|
||||
"data": {
|
||||
"id": 123
|
||||
},
|
||||
"code": 200
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 批量添加资源
|
||||
|
||||
**接口描述**: 批量添加多个资源到待处理列表
|
||||
|
||||
**请求信息**:
|
||||
- **方法**: `POST`
|
||||
- **路径**: `/api/public/resources/batch-add`
|
||||
- **认证**: 必需
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"resources": [
|
||||
{
|
||||
"title": "资源1",
|
||||
"url": "链接1",
|
||||
"description": "描述1"
|
||||
},
|
||||
{
|
||||
"title": "资源2",
|
||||
"url": "链接2",
|
||||
"description": "描述2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "批量添加成功,共添加 2 个资源",
|
||||
"data": {
|
||||
"created_count": 2,
|
||||
"created_ids": [123, 124]
|
||||
},
|
||||
"code": 200
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 资源搜索
|
||||
|
||||
**接口描述**: 搜索资源,支持关键词、标签、分类过滤
|
||||
|
||||
**请求信息**:
|
||||
- **方法**: `GET`
|
||||
- **路径**: `/api/public/resources/search`
|
||||
- **认证**: 必需
|
||||
|
||||
**查询参数**:
|
||||
- `keyword` - 搜索关键词
|
||||
- `tag` - 标签过滤
|
||||
- `category` - 分类过滤
|
||||
- `page` - 页码(默认1)
|
||||
- `page_size` - 每页数量(默认20,最大100)
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "搜索成功",
|
||||
"data": {
|
||||
"resources": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "资源标题",
|
||||
"url": "资源链接",
|
||||
"description": "资源描述",
|
||||
"view_count": 100,
|
||||
"created_at": "2024-12-19 10:00:00",
|
||||
"updated_at": "2024-12-19 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
},
|
||||
"code": 200
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 热门剧列表
|
||||
|
||||
**接口描述**: 获取热门剧列表,支持分页
|
||||
|
||||
**请求信息**:
|
||||
- **方法**: `GET`
|
||||
- **路径**: `/api/public/hot-dramas`
|
||||
- **认证**: 必需
|
||||
|
||||
**查询参数**:
|
||||
- `page` - 页码(默认1)
|
||||
- `page_size` - 每页数量(默认20,最大100)
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取热门剧成功",
|
||||
"data": {
|
||||
"hot_dramas": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "剧名",
|
||||
"description": "剧集描述",
|
||||
"img": "封面图片",
|
||||
"url": "详情链接",
|
||||
"rating": 8.5,
|
||||
"year": "2024",
|
||||
"region": "中国大陆",
|
||||
"genres": "剧情,悬疑",
|
||||
"category": "电视剧",
|
||||
"created_at": "2024-12-19 10:00:00",
|
||||
"updated_at": "2024-12-19 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 20,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
},
|
||||
"code": 200
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码说明
|
||||
|
||||
### HTTP 状态码
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 请求成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 认证失败(Token无效或缺失) |
|
||||
| 500 | 服务器内部错误 |
|
||||
| 503 | 系统维护中或API Token未配置 |
|
||||
|
||||
### 响应格式
|
||||
|
||||
所有 API 响应都遵循统一的格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true/false,
|
||||
"message": "响应消息",
|
||||
"data": {}, // 响应数据
|
||||
"code": 200 // 状态码
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### cURL 示例
|
||||
|
||||
```bash
|
||||
# 设置API Token
|
||||
API_TOKEN="your_api_token_here"
|
||||
|
||||
# 单个添加资源
|
||||
curl -X POST "http://localhost:8080/api/public/resources/add" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Token: $API_TOKEN" \
|
||||
-d '{
|
||||
"title": "测试资源",
|
||||
"url": "https://example.com/resource",
|
||||
"description": "测试描述"
|
||||
}'
|
||||
|
||||
# 搜索资源
|
||||
curl -X GET "http://localhost:8080/api/public/resources/search?keyword=测试" \
|
||||
-H "X-API-Token: $API_TOKEN"
|
||||
|
||||
# 获取热门剧
|
||||
curl -X GET "http://localhost:8080/api/public/hot-dramas?page=1&page_size=5" \
|
||||
-H "X-API-Token: $API_TOKEN"
|
||||
```
|
||||
|
||||
### JavaScript 示例
|
||||
|
||||
```javascript
|
||||
const API_TOKEN = 'your_api_token_here';
|
||||
const BASE_URL = 'http://localhost:8080/api';
|
||||
|
||||
// 添加资源
|
||||
async function addResource(resourceData) {
|
||||
const response = await fetch(`${BASE_URL}/public/resources/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Token': API_TOKEN
|
||||
},
|
||||
body: JSON.stringify(resourceData)
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// 搜索资源
|
||||
async function searchResources(keyword, page = 1) {
|
||||
const response = await fetch(
|
||||
`${BASE_URL}/public/resources/search?keyword=${encodeURIComponent(keyword)}&page=${page}`,
|
||||
{
|
||||
headers: {
|
||||
'X-API-Token': API_TOKEN
|
||||
}
|
||||
}
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// 获取热门剧
|
||||
async function getHotDramas(page = 1, pageSize = 20) {
|
||||
const response = await fetch(
|
||||
`${BASE_URL}/public/hot-dramas?page=${page}&page_size=${pageSize}`,
|
||||
{
|
||||
headers: {
|
||||
'X-API-Token': API_TOKEN
|
||||
}
|
||||
}
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Python 示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
API_TOKEN = 'your_api_token_here'
|
||||
BASE_URL = 'http://localhost:8080/api'
|
||||
|
||||
headers = {
|
||||
'X-API-Token': API_TOKEN,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# 添加资源
|
||||
def add_resource(resource_data):
|
||||
response = requests.post(
|
||||
f'{BASE_URL}/public/resources/add',
|
||||
headers=headers,
|
||||
json=resource_data
|
||||
)
|
||||
return response.json()
|
||||
|
||||
# 搜索资源
|
||||
def search_resources(keyword, page=1):
|
||||
params = {
|
||||
'keyword': keyword,
|
||||
'page': page
|
||||
}
|
||||
response = requests.get(
|
||||
f'{BASE_URL}/public/resources/search',
|
||||
headers={'X-API-Token': API_TOKEN},
|
||||
params=params
|
||||
)
|
||||
return response.json()
|
||||
|
||||
# 获取热门剧
|
||||
def get_hot_dramas(page=1, page_size=20):
|
||||
params = {
|
||||
'page': page,
|
||||
'page_size': page_size
|
||||
}
|
||||
response = requests.get(
|
||||
f'{BASE_URL}/public/hot-dramas',
|
||||
headers={'X-API-Token': API_TOKEN},
|
||||
params=params
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 错误处理
|
||||
|
||||
始终检查响应的 `success` 字段和 HTTP 状态码:
|
||||
|
||||
```javascript
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
console.error('API调用失败:', data.message);
|
||||
// 处理错误
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 分页处理
|
||||
|
||||
对于支持分页的接口,建议实现分页逻辑:
|
||||
|
||||
```javascript
|
||||
async function getAllResources(keyword) {
|
||||
let allResources = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await searchResources(keyword, page);
|
||||
if (response.success) {
|
||||
allResources.push(...response.data.resources);
|
||||
hasMore = response.data.resources.length > 0;
|
||||
page++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allResources;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 请求频率限制
|
||||
|
||||
避免过于频繁的 API 调用,建议实现请求间隔:
|
||||
|
||||
```javascript
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function searchWithDelay(keyword) {
|
||||
const result = await searchResources(keyword);
|
||||
await delay(1000); // 等待1秒
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Token 安全**: 请妥善保管您的 API Token,不要泄露给他人
|
||||
2. **请求限制**: 避免过于频繁的请求,以免影响系统性能
|
||||
3. **数据格式**: 确保请求数据格式正确,特别是 JSON 格式
|
||||
4. **错误处理**: 始终实现适当的错误处理机制
|
||||
5. **版本兼容**: API 可能会进行版本更新,请关注更新通知
|
||||
|
||||
## 技术支持
|
||||
|
||||
如果您在使用 API 过程中遇到问题,请:
|
||||
|
||||
1. 检查 API Token 是否正确
|
||||
2. 确认请求格式是否符合要求
|
||||
3. 查看错误响应中的详细信息
|
||||
4. 联系技术支持团队
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本站内容由网络爬虫自动抓取。本站不储存、复制、传播任何文件,仅作个人公益学习,请在获取后24小时内删除!
|
||||
@@ -1,100 +0,0 @@
|
||||
# 📝 更新日志
|
||||
|
||||
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范。
|
||||
|
||||
## [未发布]
|
||||
|
||||
### 新增
|
||||
- 自动转存调度功能
|
||||
- 支持更多网盘平台
|
||||
- 性能优化和监控
|
||||
|
||||
### 修复
|
||||
- 修复已知问题
|
||||
- 改进用户体验
|
||||
|
||||
## [1.0.0] - 2024-01-01
|
||||
|
||||
### 新增
|
||||
- 🎉 首次发布
|
||||
- ✨ 完整的网盘资源管理系统
|
||||
- 🔐 JWT 用户认证系统
|
||||
- 📁 多平台网盘支持
|
||||
- 🔍 资源搜索和管理
|
||||
- 🏷️ 分类和标签系统
|
||||
- 📊 统计和监控功能
|
||||
- 🐳 Docker 容器化部署
|
||||
- 📱 响应式前端界面
|
||||
- 🌙 深色模式支持
|
||||
|
||||
### 支持的网盘平台
|
||||
- 百度网盘
|
||||
- 阿里云盘
|
||||
- 夸克网盘
|
||||
- 天翼云盘
|
||||
- 迅雷云盘
|
||||
- UC网盘
|
||||
- 123云盘
|
||||
- 115网盘
|
||||
|
||||
### 技术特性
|
||||
- **后端**: Go + Gin + GORM + PostgreSQL
|
||||
- **前端**: Nuxt.js 3 + Vue 3 + TypeScript + Tailwind CSS
|
||||
- **部署**: Docker + Docker Compose
|
||||
- **认证**: JWT Token
|
||||
- **架构**: 前后端分离
|
||||
|
||||
## [0.9.0] - 2024-12-15
|
||||
|
||||
### 新增
|
||||
- 🚀 项目初始化
|
||||
- 📋 基础功能开发
|
||||
- 🏗️ 架构设计完成
|
||||
- 🔧 开发环境搭建
|
||||
|
||||
### 技术栈确定
|
||||
- 后端技术栈选型
|
||||
- 前端技术栈选型
|
||||
- 数据库设计
|
||||
- API 接口设计
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
### 版本号格式
|
||||
- **主版本号**: 不兼容的 API 修改
|
||||
- **次版本号**: 向下兼容的功能性新增
|
||||
- **修订号**: 向下兼容的问题修正
|
||||
|
||||
### 更新类型
|
||||
- 🎉 **重大更新**: 新版本发布
|
||||
- ✨ **新增功能**: 新功能添加
|
||||
- 🔧 **功能改进**: 现有功能优化
|
||||
- 🐛 **问题修复**: Bug 修复
|
||||
- 📝 **文档更新**: 文档改进
|
||||
- 🚀 **性能优化**: 性能提升
|
||||
- 🔒 **安全更新**: 安全相关更新
|
||||
- 🎨 **界面优化**: UI/UX 改进
|
||||
|
||||
## 贡献指南
|
||||
|
||||
如果您想为项目做出贡献,请:
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 打开 Pull Request
|
||||
|
||||
## 反馈
|
||||
|
||||
如果您发现任何问题或有建议,请:
|
||||
|
||||
- 提交 [Issue](https://github.com/ctwj/urldb/issues)
|
||||
- 发送邮件到 510199617@qq.com
|
||||
- 在 [讨论区](https://github.com/ctwj/urldb/discussions) 交流
|
||||
|
||||
---
|
||||
|
||||
**注意**: 此更新日志记录了项目的重要变更。对于详细的开发日志,请查看 Git 提交历史。
|
||||
@@ -1,53 +0,0 @@
|
||||
// docsify 配置文件
|
||||
window.$docsify = {
|
||||
name: '老九网盘链接数据库',
|
||||
repo: 'https://github.com/ctwj/urldb',
|
||||
loadSidebar: '_sidebar.md',
|
||||
subMaxLevel: 3,
|
||||
auto2top: true,
|
||||
// 添加侧边栏配置
|
||||
sidebarDisplayLevel: 1,
|
||||
// 添加错误处理
|
||||
notFoundPage: true,
|
||||
search: {
|
||||
maxAge: 86400000,
|
||||
paths: 'auto',
|
||||
placeholder: '搜索文档...',
|
||||
noData: '找不到结果',
|
||||
depth: 6
|
||||
},
|
||||
copyCode: {
|
||||
buttonText: '复制',
|
||||
errorText: '错误',
|
||||
successText: '已复制'
|
||||
},
|
||||
pagination: {
|
||||
previousText: '上一页',
|
||||
nextText: '下一页',
|
||||
crossChapter: true,
|
||||
crossChapterText: true,
|
||||
},
|
||||
plugins: [
|
||||
function(hook, vm) {
|
||||
hook.beforeEach(function (html) {
|
||||
// 添加页面标题
|
||||
var url = '#' + vm.route.path;
|
||||
var title = vm.route.path === '/' ? '首页' : vm.route.path.replace('/', '');
|
||||
return html + '\n\n---\n\n' +
|
||||
'<div style="text-align: center; color: #666; font-size: 14px;">' +
|
||||
'最后更新: ' + new Date().toLocaleDateString('zh-CN') +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
// 添加侧边栏加载调试
|
||||
hook.doneEach(function() {
|
||||
console.log('Docsify loaded, sidebar should be visible');
|
||||
if (document.querySelector('.sidebar-nav')) {
|
||||
console.log('Sidebar element found');
|
||||
} else {
|
||||
console.log('Sidebar element not found');
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
};
|
||||
26
docs/faq.md
26
docs/faq.md
@@ -1,26 +0,0 @@
|
||||
# ❓ 常见问题
|
||||
|
||||
## 部署相关
|
||||
|
||||
### Q: 默认账号密码是多少?
|
||||
|
||||
**A:** 可以通过以下方式解决:
|
||||
|
||||
1. admin/password
|
||||
|
||||
### Q: 批量添加了资源,但是系统里面没有出现,也搜索不到?
|
||||
|
||||
**A:** 可以通过以下方式解决:
|
||||
|
||||
1. 需要先开启自动处理待处理任务的开关
|
||||
2. 定时任务每5分钟执行一次,可能需要等待
|
||||
3. 如果添加的链接地址无效, 会被程序过滤
|
||||
|
||||
### Q: 没有自动转存?
|
||||
|
||||
**A:** 可以通过以下方式解决:
|
||||
|
||||
1. 需要先添加账号
|
||||
2. 开启定时任务
|
||||
3. 等待任务完成
|
||||
4. 只要支持的网盘地址才会被自动转存并分享
|
||||
@@ -1,253 +0,0 @@
|
||||
# GitHub版本管理指南
|
||||
|
||||
本项目使用GitHub进行版本管理,支持自动创建Release和标签。
|
||||
|
||||
## 版本管理流程
|
||||
|
||||
### 1. 版本号规范
|
||||
|
||||
遵循[语义化版本](https://semver.org/lang/zh-CN/)规范:
|
||||
|
||||
- **主版本号** (Major): 不兼容的API修改
|
||||
- **次版本号** (Minor): 向下兼容的功能性新增
|
||||
- **修订号** (Patch): 向下兼容的问题修正
|
||||
|
||||
### 2. 版本管理命令
|
||||
|
||||
#### 显示版本信息
|
||||
```bash
|
||||
./scripts/version.sh show
|
||||
```
|
||||
|
||||
#### 更新版本号
|
||||
```bash
|
||||
# 修订版本 (1.0.0 -> 1.0.1)
|
||||
./scripts/version.sh patch
|
||||
|
||||
# 次版本 (1.0.0 -> 1.1.0)
|
||||
./scripts/version.sh minor
|
||||
|
||||
# 主版本 (1.0.0 -> 2.0.0)
|
||||
./scripts/version.sh major
|
||||
```
|
||||
|
||||
#### 发布版本到GitHub
|
||||
```bash
|
||||
./scripts/version.sh release
|
||||
```
|
||||
|
||||
### 3. 自动发布流程
|
||||
|
||||
当执行版本更新命令时,脚本会:
|
||||
|
||||
1. **更新版本号**: 修改 `VERSION` 文件
|
||||
2. **同步文件**: 更新 `package.json`、`docker-compose.yml`、`README.md`
|
||||
3. **创建Git标签**: 自动创建版本标签
|
||||
4. **推送代码**: 推送代码和标签到GitHub
|
||||
5. **创建Release**: 自动创建GitHub Release
|
||||
|
||||
### 4. 手动发布流程
|
||||
|
||||
如果自动发布失败,可以手动发布:
|
||||
|
||||
#### 步骤1: 更新版本号
|
||||
```bash
|
||||
./scripts/version.sh patch # 或 minor, major
|
||||
```
|
||||
|
||||
#### 步骤2: 提交更改
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "chore: bump version to v1.0.1"
|
||||
```
|
||||
|
||||
#### 步骤3: 创建标签
|
||||
```bash
|
||||
git tag v1.0.1
|
||||
```
|
||||
|
||||
#### 步骤4: 推送到GitHub
|
||||
```bash
|
||||
git push origin main
|
||||
git push origin v1.0.1
|
||||
```
|
||||
|
||||
#### 步骤5: 创建Release
|
||||
在GitHub网页上:
|
||||
1. 进入项目页面
|
||||
2. 点击 "Releases"
|
||||
3. 点击 "Create a new release"
|
||||
4. 选择标签 `v1.0.1`
|
||||
5. 填写Release说明
|
||||
6. 发布
|
||||
|
||||
### 5. GitHub CLI工具
|
||||
|
||||
#### 安装GitHub CLI
|
||||
```bash
|
||||
# macOS
|
||||
brew install gh
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install gh
|
||||
|
||||
# Windows
|
||||
winget install GitHub.cli
|
||||
```
|
||||
|
||||
#### 登录GitHub
|
||||
```bash
|
||||
gh auth login
|
||||
```
|
||||
|
||||
#### 创建Release
|
||||
```bash
|
||||
gh release create v1.0.1 \
|
||||
--title "Release v1.0.1" \
|
||||
--notes "修复了一些bug" \
|
||||
--draft=false \
|
||||
--prerelease=false
|
||||
```
|
||||
|
||||
### 6. 版本检查
|
||||
|
||||
#### API接口
|
||||
- `GET /api/version/check-update` - 检查GitHub上的最新版本
|
||||
|
||||
#### 前端页面
|
||||
- 访问 `/version` 页面查看版本信息和更新状态
|
||||
|
||||
### 7. 版本历史
|
||||
|
||||
#### 查看所有标签
|
||||
```bash
|
||||
git tag -l
|
||||
```
|
||||
|
||||
#### 查看标签详情
|
||||
```bash
|
||||
git show v1.0.1
|
||||
```
|
||||
|
||||
#### 查看版本历史
|
||||
```bash
|
||||
git log --oneline --decorate
|
||||
```
|
||||
|
||||
### 8. 回滚版本
|
||||
|
||||
如果需要回滚到之前的版本:
|
||||
|
||||
#### 删除本地标签
|
||||
```bash
|
||||
git tag -d v1.0.1
|
||||
```
|
||||
|
||||
#### 删除远程标签
|
||||
```bash
|
||||
git push origin :refs/tags/v1.0.1
|
||||
```
|
||||
|
||||
#### 回滚代码
|
||||
```bash
|
||||
git reset --hard v1.0.0
|
||||
git push --force origin main
|
||||
```
|
||||
|
||||
### 9. 最佳实践
|
||||
|
||||
#### 提交信息规范
|
||||
```bash
|
||||
# 功能开发
|
||||
git commit -m "feat: 添加新功能"
|
||||
|
||||
# Bug修复
|
||||
git commit -m "fix: 修复某个bug"
|
||||
|
||||
# 文档更新
|
||||
git commit -m "docs: 更新文档"
|
||||
|
||||
# 版本更新
|
||||
git commit -m "chore: bump version to v1.0.1"
|
||||
```
|
||||
|
||||
#### 分支管理
|
||||
- `main`: 主分支,用于发布
|
||||
- `develop`: 开发分支
|
||||
- `feature/*`: 功能分支
|
||||
- `hotfix/*`: 热修复分支
|
||||
|
||||
#### Release说明模板
|
||||
```markdown
|
||||
## Release v1.0.1
|
||||
|
||||
**发布日期**: 2024-01-15
|
||||
|
||||
### 更新内容
|
||||
|
||||
- 修复了某个bug
|
||||
- 添加了新功能
|
||||
- 优化了性能
|
||||
|
||||
### 下载
|
||||
|
||||
- [源码 (ZIP)](https://github.com/ctwj/urldb/archive/v1.0.1.zip)
|
||||
- [源码 (TAR.GZ)](https://github.com/ctwj/urldb/archive/v1.0.1.tar.gz)
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/ctwj/urldb.git
|
||||
cd urldb
|
||||
|
||||
# 切换到指定版本
|
||||
git checkout v1.0.1
|
||||
|
||||
# 使用Docker部署
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
### 更新日志
|
||||
|
||||
详细更新日志请查看 [CHANGELOG.md](https://github.com/ctwj/urldb/blob/v1.0.1/CHANGELOG.md)
|
||||
```
|
||||
|
||||
### 10. 故障排除
|
||||
|
||||
#### 常见问题
|
||||
|
||||
1. **GitHub CLI未安装**
|
||||
```bash
|
||||
# 安装GitHub CLI
|
||||
brew install gh # macOS
|
||||
```
|
||||
|
||||
2. **GitHub CLI未登录**
|
||||
```bash
|
||||
# 登录GitHub
|
||||
gh auth login
|
||||
```
|
||||
|
||||
3. **标签已存在**
|
||||
```bash
|
||||
# 删除本地标签
|
||||
git tag -d v1.0.1
|
||||
|
||||
# 删除远程标签
|
||||
git push origin :refs/tags/v1.0.1
|
||||
```
|
||||
|
||||
4. **推送失败**
|
||||
```bash
|
||||
# 检查远程仓库
|
||||
git remote -v
|
||||
|
||||
# 重新设置远程仓库
|
||||
git remote set-url origin https://github.com/ctwj/urldb.git
|
||||
```
|
||||
|
||||
#### 获取帮助
|
||||
```bash
|
||||
./scripts/version.sh help
|
||||
```
|
||||
@@ -1,352 +0,0 @@
|
||||
# 🐳 Docker 部署
|
||||
|
||||
## 概述
|
||||
|
||||
urlDB 支持使用 Docker 进行容器化部署,提供了完整的前后端分离架构。
|
||||
|
||||
## 系统架构
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| frontend | 3000 | Nuxt.js 前端应用 |
|
||||
| backend | 8080 | Go API 后端服务 |
|
||||
| postgres | 5432 | PostgreSQL 数据库 |
|
||||
|
||||
## 快速部署
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ctwj/urldb.git
|
||||
cd urldb
|
||||
```
|
||||
|
||||
### 2. 使用启动脚本(推荐)
|
||||
|
||||
```bash
|
||||
# 给脚本执行权限
|
||||
chmod +x docker-start.sh
|
||||
|
||||
# 启动服务
|
||||
./docker-start.sh
|
||||
```
|
||||
|
||||
### 3. 手动启动
|
||||
|
||||
```bash
|
||||
# 构建并启动所有服务
|
||||
docker compose up --build -d
|
||||
|
||||
# 查看服务状态
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
可以通过修改 `docker-compose.yml` 文件中的环境变量来配置服务:
|
||||
|
||||
后端 backend
|
||||
```yaml
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: password
|
||||
DB_NAME: url_db
|
||||
PORT: 8080
|
||||
```
|
||||
|
||||
前端 frontend
|
||||
```yaml
|
||||
environment:
|
||||
API_BASE: /api
|
||||
```
|
||||
|
||||
### 端口映射
|
||||
|
||||
如果需要修改端口映射,可以编辑 `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "3001:3000" # 前端端口
|
||||
- "8081:8080" # API端口
|
||||
- "5433:5432" # 数据库端口
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 服务管理
|
||||
|
||||
```bash
|
||||
# 启动服务
|
||||
docker compose up -d
|
||||
|
||||
# 停止服务
|
||||
docker compose down
|
||||
|
||||
# 重启服务
|
||||
docker compose restart
|
||||
|
||||
# 查看服务状态
|
||||
docker compose ps
|
||||
|
||||
# 查看日志
|
||||
docker compose logs -f [service_name]
|
||||
```
|
||||
|
||||
### 数据管理
|
||||
|
||||
```bash
|
||||
# 备份数据库
|
||||
docker compose exec postgres pg_dump -U postgres url_db > backup.sql
|
||||
|
||||
# 恢复数据库
|
||||
docker compose exec -T postgres psql -U postgres url_db < backup.sql
|
||||
|
||||
# 进入数据库
|
||||
docker compose exec postgres psql -U postgres url_db
|
||||
```
|
||||
|
||||
### 容器管理
|
||||
|
||||
```bash
|
||||
# 进入容器
|
||||
docker compose exec [service_name] sh
|
||||
|
||||
# 查看容器资源使用
|
||||
docker stats
|
||||
|
||||
# 清理未使用的资源
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 安装 Docker 和 Docker Compose
|
||||
# 确保服务器有足够资源(建议 4GB+ 内存)
|
||||
|
||||
# 创建部署目录
|
||||
mkdir -p /opt/urldb
|
||||
cd /opt/urldb
|
||||
```
|
||||
|
||||
### 2. 配置文件
|
||||
|
||||
创建生产环境配置文件:
|
||||
|
||||
```bash
|
||||
# 复制项目文件
|
||||
git clone https://github.com/ctwj/urldb.git .
|
||||
|
||||
# 创建环境变量文件
|
||||
cp env.example .env.prod
|
||||
|
||||
# 编辑生产环境配置
|
||||
vim .env.prod
|
||||
```
|
||||
|
||||
### 3. 启动服务
|
||||
|
||||
```bash
|
||||
# 使用生产环境配置启动
|
||||
docker compose -f docker-compose.yml --env-file .env.prod up -d
|
||||
|
||||
# 检查服务状态
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
### 4. 配置反向代理
|
||||
|
||||
#### Nginx 配置示例
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 前端代理
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# API 代理
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. SSL 配置
|
||||
|
||||
```bash
|
||||
# 使用 Let's Encrypt 获取证书
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
|
||||
# 或使用自签名证书
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout /etc/ssl/private/urldb.key \
|
||||
-out /etc/ssl/certs/urldb.crt
|
||||
```
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 1. 日志管理
|
||||
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker compose logs -f
|
||||
|
||||
# 查看特定服务日志
|
||||
docker compose logs -f backend
|
||||
|
||||
# 导出日志
|
||||
docker compose logs > urldb.log
|
||||
```
|
||||
|
||||
### 2. 性能监控
|
||||
|
||||
```bash
|
||||
# 查看容器资源使用
|
||||
docker stats
|
||||
|
||||
# 查看系统资源
|
||||
htop
|
||||
df -h
|
||||
free -h
|
||||
```
|
||||
|
||||
### 3. 备份策略
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 创建备份脚本 backup.sh
|
||||
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/backup/urldb"
|
||||
|
||||
# 创建备份目录
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# 备份数据库
|
||||
docker compose exec -T postgres pg_dump -U postgres url_db > $BACKUP_DIR/db_$DATE.sql
|
||||
|
||||
# 备份上传文件
|
||||
tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz uploads/
|
||||
|
||||
# 删除7天前的备份
|
||||
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete
|
||||
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
|
||||
```
|
||||
|
||||
### 4. 自动更新
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 创建更新脚本 update.sh
|
||||
|
||||
cd /opt/urldb
|
||||
|
||||
# 拉取最新代码
|
||||
git pull origin main
|
||||
|
||||
# 重新构建并启动
|
||||
docker compose down
|
||||
docker compose up --build -d
|
||||
|
||||
# 检查服务状态
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 服务启动失败
|
||||
|
||||
```bash
|
||||
# 查看详细错误信息
|
||||
docker compose logs [service_name]
|
||||
|
||||
# 检查端口占用
|
||||
netstat -tulpn | grep :3000
|
||||
netstat -tulpn | grep :8080
|
||||
|
||||
# 检查磁盘空间
|
||||
df -h
|
||||
```
|
||||
|
||||
### 2. 数据库连接问题
|
||||
|
||||
```bash
|
||||
# 检查数据库状态
|
||||
docker compose exec postgres pg_isready -U postgres
|
||||
|
||||
# 检查数据库日志
|
||||
docker compose logs postgres
|
||||
|
||||
# 重启数据库服务
|
||||
docker compose restart postgres
|
||||
```
|
||||
|
||||
### 3. 前端无法访问后端
|
||||
|
||||
```bash
|
||||
# 检查网络连接
|
||||
docker compose exec frontend ping backend
|
||||
|
||||
# 检查 API 配置
|
||||
docker compose exec frontend env | grep API_BASE
|
||||
|
||||
# 测试 API 连接
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
### 4. 内存不足
|
||||
|
||||
```bash
|
||||
# 查看内存使用
|
||||
free -h
|
||||
|
||||
# 增加 swap 空间
|
||||
sudo fallocate -l 2G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
### 1. 网络安全
|
||||
|
||||
- 使用防火墙限制端口访问
|
||||
- 配置 SSL/TLS 加密
|
||||
- 定期更新系统和 Docker 版本
|
||||
|
||||
### 2. 数据安全
|
||||
|
||||
- 定期备份数据库
|
||||
- 使用强密码
|
||||
- 限制数据库访问权限
|
||||
|
||||
### 3. 容器安全
|
||||
|
||||
- 使用非 root 用户运行容器
|
||||
- 定期更新镜像
|
||||
- 扫描镜像漏洞
|
||||
|
||||
## 下一步
|
||||
|
||||
- [了解系统配置](../guide/configuration.md)
|
||||
- [查看 API 文档](../api/overview.md)
|
||||
- [学习监控和维护](../development/deployment.md)
|
||||
@@ -1,302 +0,0 @@
|
||||
# 💻 本地开发
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 1. 安装必需软件
|
||||
|
||||
#### Go 环境
|
||||
```bash
|
||||
# 下载并安装 Go 1.23+
|
||||
# 访问 https://golang.org/dl/
|
||||
# 或使用包管理器安装
|
||||
|
||||
# 验证安装
|
||||
go version
|
||||
```
|
||||
|
||||
#### Node.js 环境
|
||||
```bash
|
||||
# 下载并安装 Node.js 18+
|
||||
# 访问 https://nodejs.org/
|
||||
# 或使用 nvm 安装
|
||||
|
||||
# 验证安装
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
#### PostgreSQL 数据库
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt update
|
||||
sudo apt install postgresql postgresql-contrib
|
||||
|
||||
# macOS (使用 Homebrew)
|
||||
brew install postgresql
|
||||
|
||||
# 启动服务
|
||||
sudo systemctl start postgresql # Linux
|
||||
brew services start postgresql # macOS
|
||||
```
|
||||
|
||||
#### pnpm (推荐)
|
||||
```bash
|
||||
# 安装 pnpm
|
||||
npm install -g pnpm
|
||||
|
||||
# 验证安装
|
||||
pnpm --version
|
||||
```
|
||||
|
||||
### 2. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ctwj/urldb.git
|
||||
cd urldb
|
||||
```
|
||||
|
||||
## 后端开发
|
||||
|
||||
### 1. 环境配置
|
||||
|
||||
```bash
|
||||
# 复制环境变量文件
|
||||
cp env.example .env
|
||||
|
||||
# 编辑环境变量
|
||||
vim .env
|
||||
```
|
||||
|
||||
### 2. 数据库设置
|
||||
|
||||
```sql
|
||||
-- 登录 PostgreSQL
|
||||
sudo -u postgres psql
|
||||
|
||||
-- 创建数据库
|
||||
CREATE DATABASE url_db;
|
||||
|
||||
-- 创建用户(可选)
|
||||
CREATE USER url_user WITH PASSWORD 'your_password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE url_db TO url_user;
|
||||
|
||||
-- 退出
|
||||
\q
|
||||
```
|
||||
|
||||
### 3. 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装 Go 依赖
|
||||
go mod tidy
|
||||
|
||||
# 验证依赖
|
||||
go mod verify
|
||||
```
|
||||
|
||||
### 4. 启动后端服务
|
||||
|
||||
```bash
|
||||
# 开发模式启动
|
||||
go run main.go
|
||||
|
||||
# 或使用 air 热重载(推荐)
|
||||
go install github.com/cosmtrek/air@latest
|
||||
air
|
||||
```
|
||||
|
||||
## 前端开发
|
||||
|
||||
### 1. 进入前端目录
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
# 使用 pnpm (推荐)
|
||||
pnpm install
|
||||
|
||||
# 或使用 npm
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
pnpm dev
|
||||
|
||||
# 或使用 npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 4. 访问前端
|
||||
|
||||
前端服务启动后,访问 http://localhost:3000
|
||||
|
||||
## 开发工具
|
||||
|
||||
### 推荐的 IDE 和插件
|
||||
|
||||
#### VS Code
|
||||
- **Go** - Go 语言支持
|
||||
- **Vetur** 或 **Volar** - Vue.js 支持
|
||||
- **PostgreSQL** - 数据库支持
|
||||
- **Docker** - Docker 支持
|
||||
- **GitLens** - Git 增强
|
||||
|
||||
#### GoLand / IntelliJ IDEA
|
||||
- 内置 Go 和 Vue.js 支持
|
||||
- 数据库工具
|
||||
- Docker 集成
|
||||
|
||||
### 代码格式化
|
||||
|
||||
```bash
|
||||
# Go 代码格式化
|
||||
go fmt ./...
|
||||
|
||||
# 前端代码格式化
|
||||
cd web
|
||||
pnpm format
|
||||
```
|
||||
|
||||
### 代码检查
|
||||
|
||||
```bash
|
||||
# Go 代码检查
|
||||
go vet ./...
|
||||
|
||||
# 前端代码检查
|
||||
cd web
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 后端调试
|
||||
|
||||
```bash
|
||||
# 使用 delve 调试器
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
dlv debug main.go
|
||||
|
||||
# 或使用 VS Code 调试配置
|
||||
```
|
||||
|
||||
### 前端调试
|
||||
|
||||
```bash
|
||||
# 启动开发服务器时开启调试
|
||||
cd web
|
||||
pnpm dev --inspect
|
||||
```
|
||||
|
||||
### 数据库调试
|
||||
|
||||
```bash
|
||||
# 连接数据库
|
||||
psql -h localhost -U postgres -d url_db
|
||||
|
||||
# 查看表结构
|
||||
\dt
|
||||
|
||||
# 查看数据
|
||||
SELECT * FROM users LIMIT 5;
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
### 后端测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test ./...
|
||||
|
||||
# 运行特定测试
|
||||
go test ./handlers
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
### 前端测试
|
||||
|
||||
```bash
|
||||
cd web
|
||||
|
||||
# 运行单元测试
|
||||
pnpm test
|
||||
|
||||
# 运行 E2E 测试
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
### 后端构建
|
||||
|
||||
```bash
|
||||
# 构建二进制文件
|
||||
go build -o urlDB main.go
|
||||
|
||||
# 交叉编译
|
||||
GOOS=linux GOARCH=amd64 go build -o urlDB-linux main.go
|
||||
```
|
||||
|
||||
### 前端构建
|
||||
|
||||
```bash
|
||||
cd web
|
||||
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
|
||||
# 预览构建结果
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 端口冲突
|
||||
|
||||
如果遇到端口被占用的问题:
|
||||
|
||||
```bash
|
||||
# 查看端口占用
|
||||
lsof -i :8080
|
||||
lsof -i :3000
|
||||
|
||||
# 杀死进程
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### 2. 数据库连接失败
|
||||
|
||||
检查 `.env` 文件中的数据库配置:
|
||||
|
||||
```bash
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=your_password
|
||||
DB_NAME=url_db
|
||||
```
|
||||
|
||||
### 3. 前端依赖安装失败
|
||||
|
||||
```bash
|
||||
# 清除缓存
|
||||
pnpm store prune
|
||||
rm -rf node_modules
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [了解项目架构](../architecture/overview.md)
|
||||
- [查看 API 文档](../api/overview.md)
|
||||
- [学习代码规范](../development/coding-standards.md)
|
||||
@@ -1,36 +0,0 @@
|
||||
# 🚀 快速开始
|
||||
|
||||
## 环境要求
|
||||
|
||||
在开始使用 urlDB 之前,请确保您的系统满足以下要求:
|
||||
|
||||
### 推荐配置
|
||||
- **CPU**: 2核
|
||||
- **内存**: 2GB+
|
||||
- **存储**: 20GB+ 可用空间
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ctwj/urldb.git
|
||||
cd urldb
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
### 2. 访问应用
|
||||
|
||||
启动成功后,您可以通过以下地址访问:
|
||||
|
||||
- **前端界面**: http://localhost:3030
|
||||
默认用户密码: admin/password
|
||||
|
||||
|
||||
## 🆘 遇到问题?
|
||||
|
||||
如果您在部署过程中遇到问题,请:
|
||||
|
||||
1. 查看 [常见问题](../faq.md)
|
||||
2. 检查 [更新日志](../changelog.md)
|
||||
3. 提交 [Issue](https://github.com/ctwj/urldb/issues)
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>urlDB - 老九网盘资源数据库</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="一个现代化的网盘资源数据库,支持多网盘自动化转存分享">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dark.css" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" href="https://img.icons8.com/color/48/000000/database.png" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
|
||||
<script src="docsify.config.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/copy-code.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/pagination.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-go.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-javascript.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-typescript.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-sql.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-yaml.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
# 许可证
|
||||
|
||||
## GNU General Public License v3.0
|
||||
|
||||
本项目采用 GNU General Public License v3.0 (GPL-3.0) 许可证。
|
||||
|
||||
### 许可证概述
|
||||
|
||||
GPL-3.0 是一个自由软件许可证,它确保软件保持自由和开放。该许可证的主要特点包括:
|
||||
|
||||
- **自由使用**: 您可以自由地运行、研究、修改和分发软件
|
||||
- **源代码开放**: 修改后的代码必须同样开源
|
||||
- **专利保护**: 包含专利授权条款
|
||||
- **兼容性**: 与大多数开源许可证兼容
|
||||
|
||||
### 主要条款
|
||||
|
||||
1. **自由使用和分发**
|
||||
- 您可以自由地使用、复制、分发和修改本软件
|
||||
- 您可以商业使用本软件
|
||||
|
||||
2. **源代码要求**
|
||||
- 如果您分发修改后的版本,必须同时提供源代码
|
||||
- 源代码必须采用相同的许可证
|
||||
|
||||
3. **专利授权**
|
||||
- 贡献者自动授予用户专利使用权
|
||||
- 保护用户免受专利诉讼
|
||||
|
||||
4. **免责声明**
|
||||
- 软件按"原样"提供,不提供任何保证
|
||||
- 作者不承担任何责任
|
||||
|
||||
### 完整许可证文本
|
||||
|
||||
```
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
[... 完整许可证文本请访问 https://www.gnu.org/licenses/gpl-3.0.html ...]
|
||||
```
|
||||
|
||||
### 如何遵守许可证
|
||||
|
||||
如果您使用或修改本项目:
|
||||
|
||||
1. **保留许可证信息**: 不要删除或修改许可证文件
|
||||
2. **注明修改**: 在修改的代码中添加适当的注释
|
||||
3. **分发源代码**: 如果分发修改版本,必须提供源代码
|
||||
4. **使用相同许可证**: 修改版本必须使用相同的GPL-3.0许可证
|
||||
|
||||
### 贡献代码
|
||||
|
||||
当您向本项目贡献代码时,您同意:
|
||||
|
||||
- 您的贡献将采用GPL-3.0许可证
|
||||
- 您拥有或有权许可您贡献的代码
|
||||
- 您授予项目维护者使用您贡献代码的权利
|
||||
|
||||
### 联系方式
|
||||
|
||||
如果您对许可证有任何疑问,请联系项目维护者。
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本许可证信息仅供参考,完整和权威的许可证文本请参考 [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||
132
docs/logging.md
Normal file
132
docs/logging.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 日志系统说明
|
||||
|
||||
## 概述
|
||||
|
||||
本项目使用自定义的日志系统,支持多种日志级别、环境差异化配置和结构化日志记录。
|
||||
|
||||
## 日志级别
|
||||
|
||||
日志系统支持以下级别(按严重程度递增):
|
||||
|
||||
1. **DEBUG** - 调试信息,用于开发和故障排除
|
||||
2. **INFO** - 一般信息,记录系统正常运行状态
|
||||
3. **WARN** - 警告信息,表示可能的问题但不影响系统运行
|
||||
4. **ERROR** - 错误信息,表示系统错误但可以继续运行
|
||||
5. **FATAL** - 致命错误,系统将退出
|
||||
|
||||
## 环境配置
|
||||
|
||||
### 日志级别配置
|
||||
|
||||
可以通过环境变量配置日志级别:
|
||||
|
||||
```bash
|
||||
# 设置日志级别(DEBUG, INFO, WARN, ERROR, FATAL)
|
||||
LOG_LEVEL=DEBUG
|
||||
|
||||
# 或者启用调试模式(等同于DEBUG级别)
|
||||
DEBUG=true
|
||||
```
|
||||
|
||||
默认情况下,开发环境使用DEBUG级别,生产环境使用INFO级别。
|
||||
|
||||
### 结构化日志
|
||||
|
||||
可以通过环境变量启用结构化日志(JSON格式):
|
||||
|
||||
```bash
|
||||
# 启用结构化日志
|
||||
STRUCTURED_LOG=true
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本日志记录
|
||||
|
||||
```go
|
||||
import "github.com/ctwj/urldb/utils"
|
||||
|
||||
// 基本日志记录
|
||||
utils.Debug("调试信息: %s", debugInfo)
|
||||
utils.Info("一般信息: %s", info)
|
||||
utils.Warn("警告信息: %s", warning)
|
||||
utils.Error("错误信息: %s", err)
|
||||
utils.Fatal("致命错误: %s", fatalErr) // 程序将退出
|
||||
```
|
||||
|
||||
### 结构化日志记录
|
||||
|
||||
结构化日志允许添加额外的字段信息,便于日志分析:
|
||||
|
||||
```go
|
||||
// 带字段的结构化日志
|
||||
utils.DebugWithFields(map[string]interface{}{
|
||||
"user_id": 123,
|
||||
"action": "login",
|
||||
"ip": "192.168.1.1",
|
||||
}, "用户登录调试信息")
|
||||
|
||||
utils.InfoWithFields(map[string]interface{}{
|
||||
"task_id": 456,
|
||||
"status": "completed",
|
||||
"duration_ms": 1250,
|
||||
}, "任务处理完成")
|
||||
|
||||
utils.ErrorWithFields(map[string]interface{}{
|
||||
"error_code": 500,
|
||||
"error": "database connection failed",
|
||||
"component": "database",
|
||||
}, "数据库连接失败: %v", err)
|
||||
```
|
||||
|
||||
## 日志输出
|
||||
|
||||
日志默认输出到:
|
||||
- 控制台(标准输出)
|
||||
- 文件(logs目录下的app_日期.log文件)
|
||||
|
||||
日志文件支持轮转,单个文件最大100MB,最多保留5个备份文件,日志文件最长保留30天。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **选择合适的日志级别**:
|
||||
- DEBUG:详细的调试信息,仅在开发和故障排除时使用
|
||||
- INFO:重要的业务流程和状态变更
|
||||
- WARN:可预期的问题和异常情况
|
||||
- ERROR:系统错误和异常
|
||||
- FATAL:系统无法继续运行的致命错误
|
||||
|
||||
2. **使用结构化日志**:
|
||||
- 对于需要后续分析的日志,使用结构化日志
|
||||
- 添加有意义的字段,如用户ID、任务ID、请求ID等
|
||||
- 避免在字段中包含敏感信息
|
||||
|
||||
3. **性能监控**:
|
||||
- 记录关键操作的执行时间
|
||||
- 使用duration_ms字段记录毫秒级耗时
|
||||
|
||||
4. **安全日志**:
|
||||
- 记录所有认证和授权相关的操作
|
||||
- 包含客户端IP和用户信息
|
||||
- 记录失败的访问尝试
|
||||
|
||||
## 示例
|
||||
|
||||
```go
|
||||
// 性能监控示例
|
||||
startTime := time.Now()
|
||||
// 执行操作...
|
||||
duration := time.Since(startTime)
|
||||
utils.DebugWithFields(map[string]interface{}{
|
||||
"operation": "database_query",
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
}, "数据库查询完成,耗时: %v", duration)
|
||||
|
||||
// 安全日志示例
|
||||
utils.InfoWithFields(map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"ip": clientIP,
|
||||
"action": "login",
|
||||
"status": "success",
|
||||
}, "用户登录成功 - 用户ID: %d, IP: %s", userID, clientIP)
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 启动 docsify 文档服务脚本
|
||||
|
||||
echo "🚀 启动 docsify 文档服务..."
|
||||
|
||||
# 检查是否安装了 docsify-cli
|
||||
if ! command -v docsify &> /dev/null; then
|
||||
echo "❌ 未检测到 docsify-cli,正在安装..."
|
||||
npm install -g docsify-cli
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ docsify-cli 安装失败,请手动安装:"
|
||||
echo " npm install -g docsify-cli"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 获取当前脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "📖 文档目录: $SCRIPT_DIR"
|
||||
echo "🌐 启动文档服务..."
|
||||
|
||||
# 启动 docsify 服务
|
||||
docsify serve "$SCRIPT_DIR" --port 3000 --open
|
||||
|
||||
echo "✅ 文档服务已启动!"
|
||||
echo "📱 访问地址: http://localhost:3000"
|
||||
echo "🛑 按 Ctrl+C 停止服务"
|
||||
@@ -14,4 +14,9 @@ TIMEZONE=Asia/Shanghai
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=5MB
|
||||
MAX_FILE_SIZE=5MB
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO # 日志级别 (DEBUG, INFO, WARN, ERROR, FATAL)
|
||||
DEBUG=false # 调试模式开关
|
||||
STRUCTURED_LOG=false
|
||||
BIN
font/SourceHanSansSC-Bold.otf
Normal file
BIN
font/SourceHanSansSC-Bold.otf
Normal file
Binary file not shown.
BIN
font/SourceHanSansSC-Regular.otf
Normal file
BIN
font/SourceHanSansSC-Regular.otf
Normal file
Binary file not shown.
46
go.mod
46
go.mod
@@ -1,20 +1,45 @@
|
||||
module github.com/ctwj/urldb
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.3
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.40.0
|
||||
github.com/meilisearch/meilisearch-go v0.33.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/silenceper/wechat/v2 v2.1.10
|
||||
golang.org/x/crypto v0.41.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/image v0.32.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
@@ -23,7 +48,7 @@ require (
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -33,7 +58,6 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -44,10 +68,10 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
golang.org/x/arch v0.19.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
186
go.sum
186
go.sum
@@ -1,8 +1,24 @@
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M=
|
||||
github.com/alicebob/miniredis/v2 v2.30.0/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
@@ -10,6 +26,14 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
||||
@@ -32,18 +56,43 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -60,6 +109,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -73,6 +124,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
@@ -81,74 +134,177 @@ github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZ
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/meilisearch/meilisearch-go v0.33.1 h1:IWM8iJU7UyuIoRiTTLONvpbEgMhP/yTrnNfSnxj4wu0=
|
||||
github.com/meilisearch/meilisearch-go v0.33.1/go.mod h1:dY4nxhVc0Ext8Kn7u2YohJCsEjirg80DdcOmfNezUYg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/silenceper/wechat/v2 v2.1.10 h1:jMg0//CZBIuogEvuXgxJQuJ47SsPPAqFrrbOtro2pko=
|
||||
github.com/silenceper/wechat/v2 v2.1.10/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
|
||||
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
100
handlers/api_access_log_handler.go
Normal file
100
handlers/api_access_log_handler.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetAPIAccessLogs 获取API访问日志
|
||||
func GetAPIAccessLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
endpoint := c.Query("endpoint")
|
||||
ip := c.Query("ip")
|
||||
|
||||
var startDate, endDate *time.Time
|
||||
|
||||
if startDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil {
|
||||
startDate = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
if endDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil {
|
||||
// 设置为当天结束时间
|
||||
endOfDay := parsed.Add(24*time.Hour - time.Second)
|
||||
endDate = &endOfDay
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
logs, total, err := repoManager.APIAccessLogRepository.FindWithFilters(page, pageSize, startDate, endDate, endpoint, ip)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取API访问日志失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToAPIAccessLogResponseList(logs)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": response,
|
||||
"total": int(total),
|
||||
"page": page,
|
||||
"limit": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAPIAccessLogSummary 获取API访问日志汇总
|
||||
func GetAPIAccessLogSummary(c *gin.Context) {
|
||||
summary, err := repoManager.APIAccessLogRepository.GetSummary()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取API访问日志汇总失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToAPIAccessLogSummaryResponse(summary)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetAPIAccessLogStats 获取API访问日志统计
|
||||
func GetAPIAccessLogStats(c *gin.Context) {
|
||||
stats, err := repoManager.APIAccessLogRepository.GetStatsByEndpoint()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取API访问日志统计失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToAPIAccessLogStatsResponseList(stats)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// ClearAPIAccessLogs 清理API访问日志
|
||||
func ClearAPIAccessLogs(c *gin.Context) {
|
||||
daysStr := c.Query("days")
|
||||
if daysStr == "" {
|
||||
ErrorResponse(c, "请提供要清理的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil || days < 1 {
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.APIAccessLogRepository.ClearOldLogs(days)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "清理API访问日志失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "API访问日志清理成功"})
|
||||
}
|
||||
@@ -2,11 +2,18 @@ package handlers
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/services"
|
||||
)
|
||||
|
||||
var repoManager *repo.RepositoryManager
|
||||
var meilisearchManager *services.MeilisearchManager
|
||||
|
||||
// SetRepositoryManager 设置Repository管理器
|
||||
func SetRepositoryManager(rm *repo.RepositoryManager) {
|
||||
repoManager = rm
|
||||
func SetRepositoryManager(manager *repo.RepositoryManager) {
|
||||
repoManager = manager
|
||||
}
|
||||
|
||||
// SetMeilisearchManager 设置Meilisearch管理器
|
||||
func SetMeilisearchManager(manager *services.MeilisearchManager) {
|
||||
meilisearchManager = manager
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -21,7 +23,49 @@ func GetCks(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToCksResponseList(cks)
|
||||
// 使用新的逻辑创建 CksResponse
|
||||
var responses []dto.CksResponse
|
||||
for _, ck := range cks {
|
||||
// 获取平台信息
|
||||
var pan *dto.PanResponse
|
||||
if ck.PanID != 0 {
|
||||
panEntity, err := repoManager.PanRepository.FindByID(ck.PanID)
|
||||
if err == nil && panEntity != nil {
|
||||
pan = &dto.PanResponse{
|
||||
ID: panEntity.ID,
|
||||
Name: panEntity.Name,
|
||||
Key: panEntity.Key,
|
||||
Icon: panEntity.Icon,
|
||||
Remark: panEntity.Remark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 统计转存资源数
|
||||
count, err := repoManager.ResourceRepository.CountResourcesByCkID(ck.ID)
|
||||
if err != nil {
|
||||
count = 0 // 统计失败时设为0
|
||||
}
|
||||
|
||||
response := dto.CksResponse{
|
||||
ID: ck.ID,
|
||||
PanID: ck.PanID,
|
||||
Idx: ck.Idx,
|
||||
Ck: ck.Ck,
|
||||
IsValid: ck.IsValid,
|
||||
Space: ck.Space,
|
||||
LeftSpace: ck.LeftSpace,
|
||||
UsedSpace: ck.UsedSpace,
|
||||
Username: ck.Username,
|
||||
VipStatus: ck.VipStatus,
|
||||
ServiceType: ck.ServiceType,
|
||||
Remark: ck.Remark,
|
||||
TransferredCount: count,
|
||||
Pan: pan,
|
||||
}
|
||||
responses = append(responses, response)
|
||||
}
|
||||
|
||||
SuccessResponse(c, responses)
|
||||
}
|
||||
|
||||
@@ -51,6 +95,8 @@ func CreateCks(c *gin.Context) {
|
||||
serviceType = panutils.BaiduPan
|
||||
case "uc":
|
||||
serviceType = panutils.UC
|
||||
case "xunlei":
|
||||
serviceType = panutils.Xunlei
|
||||
default:
|
||||
ErrorResponse(c, "不支持的平台类型", http.StatusBadRequest)
|
||||
return
|
||||
@@ -64,28 +110,110 @@ func CreateCks(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
userInfo, err := service.GetUserInfo(req.Ck)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无法获取用户信息,账号创建失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var cks *entity.Cks
|
||||
// 迅雷网盘,使用账号密码登录
|
||||
if serviceType == panutils.Xunlei {
|
||||
// 解析账号密码信息
|
||||
credentials, err := panutils.ParseCredentialsFromCk(req.Ck)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "账号密码格式错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
leftSpaceBytes := userInfo.TotalSpace - userInfo.UsedSpace
|
||||
// 验证账号密码
|
||||
if credentials.Username == "" || credentials.Password == "" {
|
||||
ErrorResponse(c, "请提供完整的账号和密码", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建Cks实体
|
||||
cks := &entity.Cks{
|
||||
PanID: req.PanID,
|
||||
Idx: req.Idx,
|
||||
Ck: req.Ck,
|
||||
IsValid: userInfo.VIPStatus, // 根据VIP状态设置有效性
|
||||
Space: userInfo.TotalSpace,
|
||||
LeftSpace: leftSpaceBytes,
|
||||
UsedSpace: userInfo.UsedSpace,
|
||||
Username: userInfo.Username,
|
||||
VipStatus: userInfo.VIPStatus,
|
||||
ServiceType: userInfo.ServiceType,
|
||||
Remark: req.Remark,
|
||||
var tokenData *panutils.XunleiTokenData
|
||||
var username string
|
||||
|
||||
// 使用账号密码登录
|
||||
xunleiService := service.(*panutils.XunleiPanService)
|
||||
token, err := xunleiService.LoginWithCredentials(credentials.Username, credentials.Password)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "账号密码登录失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tokenData = &token
|
||||
username = credentials.Username
|
||||
|
||||
// 构建extra数据
|
||||
extra := panutils.XunleiExtraData{
|
||||
Token: tokenData,
|
||||
Captcha: &panutils.CaptchaData{},
|
||||
}
|
||||
|
||||
// 如果有账号密码信息,保存到extra中
|
||||
if credentials.Username != "" && credentials.Password != "" {
|
||||
extra.Credentials = credentials
|
||||
}
|
||||
|
||||
extraStr, _ := json.Marshal(extra)
|
||||
|
||||
// 声明userInfo变量
|
||||
var userInfo *panutils.UserInfo
|
||||
|
||||
// 设置CKSRepository以便获取用户信息
|
||||
xunleiService.SetCKSRepository(repoManager.CksRepository, entity.Cks{})
|
||||
|
||||
// 获取用户信息
|
||||
userInfo, err = xunleiService.GetUserInfo(nil)
|
||||
if err != nil {
|
||||
log.Printf("获取迅雷用户信息失败,使用默认值: %v", err)
|
||||
// 如果获取失败,使用默认值
|
||||
userInfo = &panutils.UserInfo{
|
||||
Username: username,
|
||||
VIPStatus: false,
|
||||
ServiceType: "xunlei",
|
||||
TotalSpace: 0,
|
||||
UsedSpace: 0,
|
||||
}
|
||||
}
|
||||
|
||||
leftSpaceBytes := userInfo.TotalSpace - userInfo.UsedSpace
|
||||
|
||||
// 创建Cks实体
|
||||
cks = &entity.Cks{
|
||||
PanID: req.PanID,
|
||||
Idx: req.Idx,
|
||||
Ck: req.Ck, // 保持原始输入
|
||||
IsValid: userInfo.VIPStatus, // 根据VIP状态设置有效性
|
||||
Space: userInfo.TotalSpace,
|
||||
LeftSpace: leftSpaceBytes,
|
||||
UsedSpace: userInfo.UsedSpace,
|
||||
Username: userInfo.Username,
|
||||
VipStatus: userInfo.VIPStatus,
|
||||
ServiceType: userInfo.ServiceType,
|
||||
Extra: string(extraStr),
|
||||
Remark: req.Remark,
|
||||
}
|
||||
} else {
|
||||
// 获取用户信息
|
||||
userInfo, err := service.GetUserInfo(&req.Ck)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无法获取用户信息,账号创建失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
leftSpaceBytes := userInfo.TotalSpace - userInfo.UsedSpace
|
||||
|
||||
// 创建Cks实体
|
||||
cks = &entity.Cks{
|
||||
PanID: req.PanID,
|
||||
Idx: req.Idx,
|
||||
Ck: req.Ck,
|
||||
IsValid: userInfo.VIPStatus, // 根据VIP状态设置有效性
|
||||
Space: userInfo.TotalSpace,
|
||||
LeftSpace: leftSpaceBytes,
|
||||
UsedSpace: userInfo.UsedSpace,
|
||||
Username: userInfo.Username,
|
||||
VipStatus: userInfo.VIPStatus,
|
||||
ServiceType: userInfo.ServiceType,
|
||||
Extra: userInfo.ExtraData,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
}
|
||||
|
||||
err = repoManager.CksRepository.Create(cks)
|
||||
@@ -293,6 +421,8 @@ func RefreshCapacity(c *gin.Context) {
|
||||
serviceType = panutils.BaiduPan
|
||||
case "uc":
|
||||
serviceType = panutils.UC
|
||||
case "xunlei":
|
||||
serviceType = panutils.Xunlei
|
||||
default:
|
||||
ErrorResponse(c, "不支持的平台类型", http.StatusBadRequest)
|
||||
return
|
||||
@@ -306,13 +436,22 @@ func RefreshCapacity(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取最新的用户信息
|
||||
userInfo, err := service.GetUserInfo(cks.Ck)
|
||||
var userInfo *panutils.UserInfo
|
||||
service.SetCKSRepository(repoManager.CksRepository, *cks) // 迅雷需要初始化 token 后才能获取,
|
||||
|
||||
// 根据服务类型调用不同的GetUserInfo方法
|
||||
switch s := service.(type) {
|
||||
case *panutils.XunleiPanService:
|
||||
// 迅雷网盘使用存储在extra中的token,不需要传递ck参数
|
||||
userInfo, err = s.GetUserInfo(nil)
|
||||
default:
|
||||
// 其他网盘使用ck参数
|
||||
userInfo, err = service.GetUserInfo(&cks.Ck)
|
||||
}
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无法获取用户信息,刷新失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
leftSpaceBytes := userInfo.TotalSpace - userInfo.UsedSpace
|
||||
|
||||
// 更新账号信息
|
||||
@@ -322,7 +461,7 @@ func RefreshCapacity(c *gin.Context) {
|
||||
cks.Space = userInfo.TotalSpace
|
||||
cks.LeftSpace = leftSpaceBytes
|
||||
cks.UsedSpace = userInfo.UsedSpace
|
||||
cks.IsValid = userInfo.VIPStatus // 根据VIP状态更新有效性
|
||||
// cks.IsValid = userInfo.VIPStatus // 根据VIP状态更新有效性
|
||||
|
||||
err = repoManager.CksRepository.UpdateWithAllFields(cks)
|
||||
if err != nil {
|
||||
@@ -335,3 +474,25 @@ func RefreshCapacity(c *gin.Context) {
|
||||
"cks": converter.ToCksResponse(cks),
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteRelatedResources 删除关联资源
|
||||
func DeleteRelatedResources(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用资源库删除关联资源
|
||||
affectedRows, err := repoManager.ResourceRepository.DeleteRelatedResources(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "删除关联资源失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "关联资源删除成功",
|
||||
"affected_rows": affectedRows,
|
||||
})
|
||||
}
|
||||
|
||||
312
handlers/copyright_claim_handler.go
Normal file
312
handlers/copyright_claim_handler.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CopyrightClaimHandler struct {
|
||||
copyrightClaimRepo repo.CopyrightClaimRepository
|
||||
resourceRepo repo.ResourceRepository
|
||||
validate *validator.Validate
|
||||
}
|
||||
|
||||
func NewCopyrightClaimHandler(copyrightClaimRepo repo.CopyrightClaimRepository, resourceRepo repo.ResourceRepository) *CopyrightClaimHandler {
|
||||
return &CopyrightClaimHandler{
|
||||
copyrightClaimRepo: copyrightClaimRepo,
|
||||
resourceRepo: resourceRepo,
|
||||
validate: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCopyrightClaim 创建版权申述
|
||||
// @Summary 创建版权申述
|
||||
// @Description 提交资源版权申述
|
||||
// @Tags CopyrightClaim
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CopyrightClaimCreateRequest true "版权申述信息"
|
||||
// @Success 200 {object} Response{data=dto.CopyrightClaimResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /copyright-claims [post]
|
||||
func (h *CopyrightClaimHandler) CreateCopyrightClaim(c *gin.Context) {
|
||||
var req dto.CopyrightClaimCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建版权申述实体
|
||||
claim := &entity.CopyrightClaim{
|
||||
ResourceKey: req.ResourceKey,
|
||||
Identity: req.Identity,
|
||||
ProofType: req.ProofType,
|
||||
Reason: req.Reason,
|
||||
ContactInfo: req.ContactInfo,
|
||||
ClaimantName: req.ClaimantName,
|
||||
ProofFiles: req.ProofFiles,
|
||||
UserAgent: req.UserAgent,
|
||||
IPAddress: req.IPAddress,
|
||||
Status: "pending", // 默认为待处理
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := h.copyrightClaimRepo.Create(claim); err != nil {
|
||||
ErrorResponse(c, "创建版权申述失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := converter.CopyrightClaimToResponse(claim)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetCopyrightClaim 获取版权申述详情
|
||||
// @Summary 获取版权申述详情
|
||||
// @Description 根据ID获取版权申述详情
|
||||
// @Tags CopyrightClaim
|
||||
// @Produce json
|
||||
// @Param id path int true "版权申述ID"
|
||||
// @Success 200 {object} Response{data=dto.CopyrightClaimResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 404 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /copyright-claims/{id} [get]
|
||||
func (h *CopyrightClaimHandler) GetCopyrightClaim(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
claim, err := h.copyrightClaimRepo.GetByID(uint(id))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
ErrorResponse(c, "版权申述不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ErrorResponse(c, "获取版权申述失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, converter.CopyrightClaimToResponse(claim))
|
||||
}
|
||||
|
||||
// ListCopyrightClaims 获取版权申述列表
|
||||
// @Summary 获取版权申述列表
|
||||
// @Description 获取版权申述列表(支持分页和状态筛选)
|
||||
// @Tags CopyrightClaim
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param status query string false "处理状态"
|
||||
// @Success 200 {object} Response{data=object{items=[]dto.CopyrightClaimResponse,total=int}}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /copyright-claims [get]
|
||||
func (h *CopyrightClaimHandler) ListCopyrightClaims(c *gin.Context) {
|
||||
var req dto.CopyrightClaimListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Page == 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
claims, total, err := h.copyrightClaimRepo.List(req.Status, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取版权申述列表失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为包含资源信息的响应
|
||||
var responses []*dto.CopyrightClaimResponse
|
||||
for _, claim := range claims {
|
||||
// 查询关联的资源信息
|
||||
resources, err := h.getResourcesByResourceKey(claim.ResourceKey)
|
||||
if err != nil {
|
||||
// 如果查询资源失败,使用空资源列表
|
||||
responses = append(responses, converter.CopyrightClaimToResponse(claim))
|
||||
} else {
|
||||
// 使用包含资源详情的转换函数
|
||||
responses = append(responses, converter.CopyrightClaimToResponseWithResources(claim, resources))
|
||||
}
|
||||
}
|
||||
|
||||
PageResponse(c, responses, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// getResourcesByResourceKey 根据资源key获取关联的资源列表
|
||||
func (h *CopyrightClaimHandler) getResourcesByResourceKey(resourceKey string) ([]*entity.Resource, error) {
|
||||
// 从资源仓库获取与key关联的所有资源
|
||||
resources, err := h.resourceRepo.FindByResourceKey(resourceKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 将 []entity.Resource 转换为 []*entity.Resource
|
||||
var resourcePointers []*entity.Resource
|
||||
for i := range resources {
|
||||
resourcePointers = append(resourcePointers, &resources[i])
|
||||
}
|
||||
|
||||
return resourcePointers, nil
|
||||
}
|
||||
|
||||
// UpdateCopyrightClaim 更新版权申述状态
|
||||
// @Summary 更新版权申述状态
|
||||
// @Description 更新版权申述处理状态
|
||||
// @Tags CopyrightClaim
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "版权申述ID"
|
||||
// @Param request body dto.CopyrightClaimUpdateRequest true "更新信息"
|
||||
// @Success 200 {object} Response{data=dto.CopyrightClaimResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 404 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /copyright-claims/{id} [put]
|
||||
func (h *CopyrightClaimHandler) UpdateCopyrightClaim(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CopyrightClaimUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前版权申述
|
||||
_, err = h.copyrightClaimRepo.GetByID(uint(id))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
ErrorResponse(c, "版权申述不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ErrorResponse(c, "获取版权申述失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
processedBy := uint(0) // 从上下文获取当前用户ID,如果存在的话
|
||||
if currentUser := c.GetUint("user_id"); currentUser > 0 {
|
||||
processedBy = currentUser
|
||||
}
|
||||
|
||||
if err := h.copyrightClaimRepo.UpdateStatus(uint(id), req.Status, &processedBy, req.Note); err != nil {
|
||||
ErrorResponse(c, "更新版权申述状态失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取更新后的版权申述信息
|
||||
updatedClaim, err := h.copyrightClaimRepo.GetByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取更新后版权申述信息失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, converter.CopyrightClaimToResponse(updatedClaim))
|
||||
}
|
||||
|
||||
// DeleteCopyrightClaim 删除版权申述
|
||||
// @Summary 删除版权申述
|
||||
// @Description 删除版权申述记录
|
||||
// @Tags CopyrightClaim
|
||||
// @Produce json
|
||||
// @Param id path int true "版权申述ID"
|
||||
// @Success 200 {object} Response
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /copyright-claims/{id} [delete]
|
||||
func (h *CopyrightClaimHandler) DeleteCopyrightClaim(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.copyrightClaimRepo.Delete(uint(id)); err != nil {
|
||||
ErrorResponse(c, "删除版权申述失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, nil)
|
||||
}
|
||||
|
||||
// GetCopyrightClaimByResource 获取某个资源的版权申述列表
|
||||
// @Summary 获取资源版权申述列表
|
||||
// @Description 获取某个资源的所有版权申述记录
|
||||
// @Tags CopyrightClaim
|
||||
// @Produce json
|
||||
// @Param resource_key path string true "资源Key"
|
||||
// @Success 200 {object} Response{data=[]dto.CopyrightClaimResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /copyright-claims/resource/{resource_key} [get]
|
||||
func (h *CopyrightClaimHandler) GetCopyrightClaimByResource(c *gin.Context) {
|
||||
resourceKey := c.Param("resource_key")
|
||||
if resourceKey == "" {
|
||||
ErrorResponse(c, "资源Key不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := h.copyrightClaimRepo.GetByResourceKey(resourceKey)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取资源版权申述失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, converter.CopyrightClaimsToResponse(claims))
|
||||
}
|
||||
|
||||
// RegisterCopyrightClaimRoutes 注册版权申述相关路由
|
||||
func RegisterCopyrightClaimRoutes(router *gin.RouterGroup, copyrightClaimRepo repo.CopyrightClaimRepository, resourceRepo repo.ResourceRepository) {
|
||||
handler := NewCopyrightClaimHandler(copyrightClaimRepo, resourceRepo)
|
||||
|
||||
claims := router.Group("/copyright-claims")
|
||||
{
|
||||
claims.POST("", handler.CreateCopyrightClaim) // 创建版权申述
|
||||
claims.GET("/:id", handler.GetCopyrightClaim) // 获取版权申述详情
|
||||
claims.GET("", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handler.ListCopyrightClaims) // 获取版权申述列表
|
||||
claims.PUT("/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handler.UpdateCopyrightClaim) // 更新版权申述状态
|
||||
claims.DELETE("/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handler.DeleteCopyrightClaim) // 删除版权申述
|
||||
claims.GET("/resource/:resource_key", handler.GetCopyrightClaimByResource) // 获取资源版权申述列表
|
||||
}
|
||||
}
|
||||
@@ -246,33 +246,23 @@ func (h *FileHandler) GetFileList(c *gin.Context) {
|
||||
utils.Info("文件列表请求参数: page=%d, pageSize=%d, search='%s', fileType='%s', status='%s', userID=%d",
|
||||
req.Page, req.PageSize, req.Search, req.FileType, req.Status, req.UserID)
|
||||
|
||||
// 获取当前用户ID和角色
|
||||
userIDInterface, exists := c.Get("user_id")
|
||||
roleInterface, _ := c.Get("role")
|
||||
// 获取当前用户ID和角色(现在总是有认证)
|
||||
userID := c.GetUint("user_id")
|
||||
role := c.GetString("role")
|
||||
|
||||
var userID uint
|
||||
var role string
|
||||
if exists {
|
||||
userID = userIDInterface.(uint)
|
||||
}
|
||||
if roleInterface != nil {
|
||||
role = roleInterface.(string)
|
||||
}
|
||||
utils.Info("GetFileList - 用户ID: %d, 角色: %s", userID, role)
|
||||
|
||||
// 根据用户角色决定查询范围
|
||||
var files []entity.File
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if exists && role == "admin" {
|
||||
if role == "admin" {
|
||||
// 管理员可以查看所有文件
|
||||
files, total, err = h.fileRepo.SearchFiles(req.Search, req.FileType, req.Status, req.UserID, req.Page, req.PageSize)
|
||||
} else if userID > 0 {
|
||||
} else {
|
||||
// 普通用户只能查看自己的文件
|
||||
files, total, err = h.fileRepo.SearchFiles(req.Search, req.FileType, req.Status, userID, req.Page, req.PageSize)
|
||||
} else {
|
||||
// 未登录用户只能查看公开文件
|
||||
files, total, err = h.fileRepo.FindPublicFiles(req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -450,3 +440,80 @@ func (h *FileHandler) calculateFileHash(filePath string) (string, error) {
|
||||
}
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// UploadWechatVerifyFile 上传微信公众号验证文件(TXT文件)
|
||||
// 无需认证,仅支持TXT文件,不记录数据库,直接保存到uploads目录
|
||||
func (h *FileHandler) UploadWechatVerifyFile(c *gin.Context) {
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
ErrorResponse(c, "未提供文件", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件扩展名必须是.txt
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if ext != ".txt" {
|
||||
ErrorResponse(c, "仅支持TXT文件", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小(限制1MB)
|
||||
if file.Size > 1*1024*1024 {
|
||||
ErrorResponse(c, "文件大小不能超过1MB", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成文件名(使用原始文件名,但确保是安全的)
|
||||
originalName := filepath.Base(file.Filename)
|
||||
safeFileName := h.makeSafeFileName(originalName)
|
||||
|
||||
// 确保uploads目录存在
|
||||
uploadsDir := "./uploads"
|
||||
if err := os.MkdirAll(uploadsDir, 0755); err != nil {
|
||||
ErrorResponse(c, "创建上传目录失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建完整文件路径
|
||||
filePath := filepath.Join(uploadsDir, safeFileName)
|
||||
|
||||
// 保存文件
|
||||
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||
ErrorResponse(c, "保存文件失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置文件权限
|
||||
if err := os.Chmod(filePath, 0644); err != nil {
|
||||
utils.Warn("设置文件权限失败: %v", err)
|
||||
}
|
||||
|
||||
// 返回成功响应
|
||||
accessURL := fmt.Sprintf("/%s", safeFileName)
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "验证文件上传成功",
|
||||
"file_name": safeFileName,
|
||||
"access_url": accessURL,
|
||||
}
|
||||
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// makeSafeFileName 生成安全的文件名,移除危险字符
|
||||
func (h *FileHandler) makeSafeFileName(filename string) string {
|
||||
// 移除路径分隔符和特殊字符
|
||||
safeName := strings.ReplaceAll(filename, "/", "_")
|
||||
safeName = strings.ReplaceAll(safeName, "\\", "_")
|
||||
safeName = strings.ReplaceAll(safeName, "..", "_")
|
||||
|
||||
// 限制文件名长度
|
||||
if len(safeName) > 100 {
|
||||
ext := filepath.Ext(safeName)
|
||||
name := safeName[:100-len(ext)]
|
||||
safeName = name + ext
|
||||
}
|
||||
|
||||
return safeName
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -94,6 +98,87 @@ func CreateHotDrama(c *gin.Context) {
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetPosterImage 获取海报图片代理
|
||||
func GetPosterImage(c *gin.Context) {
|
||||
url := c.Query("url")
|
||||
if url == "" {
|
||||
ErrorResponse(c, "图片URL不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 简单的URL验证
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
ErrorResponse(c, "无效的图片URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查If-Modified-Since头,实现条件请求
|
||||
ifModifiedSince := c.GetHeader("If-Modified-Since")
|
||||
if ifModifiedSince != "" {
|
||||
// 如果存在,说明浏览器有缓存,检查是否过期
|
||||
ifLastModified, err := time.Parse("Mon, 02 Jan 2006 15:04:05 GMT", ifModifiedSince)
|
||||
if err == nil && time.Since(ifLastModified) < 86400*time.Second { // 24小时内
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查ETag头 - 基于URL生成,保证相同URL有相同ETag
|
||||
ifNoneMatch := c.GetHeader("If-None-Match")
|
||||
if ifNoneMatch != "" {
|
||||
etag := fmt.Sprintf(`"%x"`, len(url)) // 简单的基于URL长度的ETag
|
||||
if ifNoneMatch == etag {
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
client := resty.New().
|
||||
SetTimeout(30 * time.Second).
|
||||
SetRetryCount(2).
|
||||
SetRetryWaitTime(1 * time.Second)
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeaders(map[string]string{
|
||||
"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",
|
||||
"Referer": "https://m.douban.com/",
|
||||
"Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
}).
|
||||
Get(url)
|
||||
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取图片失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
ErrorResponse(c, fmt.Sprintf("获取图片失败,状态码: %d", resp.StatusCode()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
contentType := resp.Header().Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "image/jpeg"
|
||||
}
|
||||
c.Header("Content-Type", contentType)
|
||||
|
||||
// 增强缓存策略
|
||||
c.Header("Cache-Control", "public, max-age=604800, s-maxage=86400") // 客户端7天,代理1天
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
|
||||
|
||||
// 设置缓存验证头(基于URL长度生成的简单ETag)
|
||||
etag := fmt.Sprintf(`"%x"`, len(url))
|
||||
c.Header("ETag", etag)
|
||||
c.Header("Last-Modified", time.Now().Add(-86400*time.Second).Format("Mon, 02 Jan 2006 15:04:05 GMT")) // 设为1天前,避免立即过期
|
||||
|
||||
// 返回图片数据
|
||||
c.Data(resp.StatusCode(), contentType, resp.Body())
|
||||
}
|
||||
|
||||
// UpdateHotDrama 更新热播剧记录
|
||||
func UpdateHotDrama(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
@@ -149,6 +234,7 @@ func GetHotDramaList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
category := c.Query("category")
|
||||
subType := c.Query("sub_type")
|
||||
|
||||
var dramas []entity.HotDrama
|
||||
var total int64
|
||||
@@ -156,13 +242,17 @@ func GetHotDramaList(c *gin.Context) {
|
||||
|
||||
// 如果page_size很大(比如>=1000),则获取所有数据
|
||||
if pageSize >= 1000 {
|
||||
if category != "" {
|
||||
if category != "" && subType != "" {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindByCategoryAndSubType(category, subType, 1, 10000)
|
||||
} else if category != "" {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, 1, 10000)
|
||||
} else {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindAll(1, 10000)
|
||||
}
|
||||
} else {
|
||||
if category != "" {
|
||||
if category != "" && subType != "" {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindByCategoryAndSubType(category, subType, page, pageSize)
|
||||
} else if category != "" {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, page, pageSize)
|
||||
} else {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindAll(page, pageSize)
|
||||
|
||||
188
handlers/log_handler.go
Normal file
188
handlers/log_handler.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetSystemLogs 获取系统日志
|
||||
func GetSystemLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
level := c.Query("level")
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
search := c.Query("search")
|
||||
|
||||
var startDate, endDate *time.Time
|
||||
|
||||
if startDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil {
|
||||
startDate = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
if endDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil {
|
||||
// 设置为当天结束时间
|
||||
endOfDay := parsed.Add(24*time.Hour - time.Second)
|
||||
endDate = &endOfDay
|
||||
}
|
||||
}
|
||||
|
||||
// 使用日志查看器获取日志
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
|
||||
// 获取日志文件列表
|
||||
logFiles, err := logViewer.GetLogFiles()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志文件失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果指定了日期范围,只选择对应日期的日志文件
|
||||
if startDate != nil || endDate != nil {
|
||||
var filteredFiles []string
|
||||
for _, file := range logFiles {
|
||||
fileInfo, err := utils.GetFileInfo(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
shouldInclude := true
|
||||
if startDate != nil {
|
||||
if fileInfo.ModTime().Before(*startDate) {
|
||||
shouldInclude = false
|
||||
}
|
||||
}
|
||||
if endDate != nil {
|
||||
if fileInfo.ModTime().After(*endDate) {
|
||||
shouldInclude = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldInclude {
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
}
|
||||
logFiles = filteredFiles
|
||||
}
|
||||
|
||||
// 限制读取的文件数量以提高性能
|
||||
if len(logFiles) > 10 {
|
||||
logFiles = logFiles[:10] // 只处理最近的10个文件
|
||||
}
|
||||
|
||||
var allLogs []utils.LogEntry
|
||||
for _, file := range logFiles {
|
||||
// 读取日志文件
|
||||
fileLogs, err := logViewer.ParseLogEntriesFromFile(file, level, search)
|
||||
if err != nil {
|
||||
utils.Error("解析日志文件失败 %s: %v", file, err)
|
||||
continue
|
||||
}
|
||||
allLogs = append(allLogs, fileLogs...)
|
||||
}
|
||||
|
||||
// 按时间排序(最新的在前)
|
||||
utils.SortLogEntriesByTime(allLogs, false)
|
||||
|
||||
// 应用分页
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if start > len(allLogs) {
|
||||
start = len(allLogs)
|
||||
}
|
||||
if end > len(allLogs) {
|
||||
end = len(allLogs)
|
||||
}
|
||||
|
||||
pagedLogs := allLogs[start:end]
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": pagedLogs,
|
||||
"total": len(allLogs),
|
||||
"page": page,
|
||||
"limit": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSystemLogFiles 获取系统日志文件列表
|
||||
func GetSystemLogFiles(c *gin.Context) {
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
files, err := logViewer.GetLogFiles()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志文件列表失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取每个文件的详细信息
|
||||
var fileInfos []gin.H
|
||||
for _, file := range files {
|
||||
info, err := utils.GetFileInfo(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fileInfos = append(fileInfos, gin.H{
|
||||
"name": info.Name(),
|
||||
"size": info.Size(),
|
||||
"mod_time": info.ModTime(),
|
||||
"path": file,
|
||||
})
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": fileInfos,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSystemLogSummary 获取系统日志统计摘要
|
||||
func GetSystemLogSummary(c *gin.Context) {
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
files, err := logViewer.GetLogFiles()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志文件列表失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
stats, err := logViewer.GetLogStats(files)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志统计信息失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"summary": stats,
|
||||
"files_count": len(files),
|
||||
})
|
||||
}
|
||||
|
||||
// ClearSystemLogs 清理系统日志
|
||||
func ClearSystemLogs(c *gin.Context) {
|
||||
daysStr := c.Query("days")
|
||||
if daysStr == "" {
|
||||
ErrorResponse(c, "请提供要清理的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil || days < 1 {
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
err = logViewer.CleanOldLogs(days)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "清理系统日志失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "系统日志清理成功"})
|
||||
}
|
||||
270
handlers/meilisearch_handler.go
Normal file
270
handlers/meilisearch_handler.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/services"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MeilisearchHandler Meilisearch处理器
|
||||
type MeilisearchHandler struct {
|
||||
meilisearchManager *services.MeilisearchManager
|
||||
}
|
||||
|
||||
// NewMeilisearchHandler 创建Meilisearch处理器
|
||||
func NewMeilisearchHandler(meilisearchManager *services.MeilisearchManager) *MeilisearchHandler {
|
||||
return &MeilisearchHandler{
|
||||
meilisearchManager: meilisearchManager,
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnection 测试Meilisearch连接
|
||||
func (h *MeilisearchHandler) TestConnection(c *gin.Context) {
|
||||
var req struct {
|
||||
Host string `json:"host"`
|
||||
Port interface{} `json:"port"` // 支持字符串或数字
|
||||
MasterKey string `json:"masterKey"`
|
||||
IndexName string `json:"indexName"` // 可选字段
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必要字段
|
||||
if req.Host == "" {
|
||||
ErrorResponse(c, "主机地址不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换port为字符串
|
||||
var portStr string
|
||||
switch v := req.Port.(type) {
|
||||
case string:
|
||||
portStr = v
|
||||
case float64:
|
||||
portStr = strconv.Itoa(int(v))
|
||||
case int:
|
||||
portStr = strconv.Itoa(v)
|
||||
default:
|
||||
portStr = "7700" // 默认端口
|
||||
}
|
||||
|
||||
// 如果没有提供索引名称,使用默认值
|
||||
indexName := req.IndexName
|
||||
if indexName == "" {
|
||||
indexName = "resources"
|
||||
}
|
||||
|
||||
// 创建临时服务进行测试
|
||||
service := services.NewMeilisearchService(req.Host, portStr, req.MasterKey, indexName, true)
|
||||
|
||||
if err := service.HealthCheck(); err != nil {
|
||||
ErrorResponse(c, "连接测试失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "连接测试成功"})
|
||||
}
|
||||
|
||||
// GetStatus 获取Meilisearch状态
|
||||
func (h *MeilisearchHandler) GetStatus(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"enabled": false,
|
||||
"healthy": false,
|
||||
"message": "Meilisearch未初始化",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.meilisearchManager.GetStatusWithHealthCheck()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取状态失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, status)
|
||||
}
|
||||
|
||||
// GetUnsyncedCount 获取未同步资源数量
|
||||
func (h *MeilisearchHandler) GetUnsyncedCount(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{"count": 0})
|
||||
return
|
||||
}
|
||||
|
||||
count, err := h.meilisearchManager.GetUnsyncedCount()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取未同步数量失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"count": count})
|
||||
}
|
||||
|
||||
// GetUnsyncedResources 获取未同步的资源
|
||||
func (h *MeilisearchHandler) GetUnsyncedResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
resources, total, err := h.meilisearchManager.GetUnsyncedResources(page, pageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取未同步资源失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSyncedResources 获取已同步的资源
|
||||
func (h *MeilisearchHandler) GetSyncedResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
resources, total, err := h.meilisearchManager.GetSyncedResources(page, pageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取已同步资源失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllResources 获取所有资源
|
||||
func (h *MeilisearchHandler) GetAllResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
resources, total, err := h.meilisearchManager.GetAllResources(page, pageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取所有资源失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// SyncAllResources 同步所有资源
|
||||
func (h *MeilisearchHandler) SyncAllResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("开始同步所有资源到Meilisearch...")
|
||||
|
||||
_, err := h.meilisearchManager.SyncAllResources()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "同步失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "同步已开始,请查看进度",
|
||||
})
|
||||
}
|
||||
|
||||
// GetSyncProgress 获取同步进度
|
||||
func (h *MeilisearchHandler) GetSyncProgress(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
progress := h.meilisearchManager.GetSyncProgress()
|
||||
SuccessResponse(c, progress)
|
||||
}
|
||||
|
||||
// StopSync 停止同步
|
||||
func (h *MeilisearchHandler) StopSync(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.meilisearchManager.StopSync()
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "同步已停止",
|
||||
})
|
||||
}
|
||||
|
||||
// ClearIndex 清空索引
|
||||
func (h *MeilisearchHandler) ClearIndex(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.meilisearchManager.ClearIndex(); err != nil {
|
||||
ErrorResponse(c, "清空索引失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "清空索引成功"})
|
||||
}
|
||||
|
||||
// UpdateIndexSettings 更新索引设置
|
||||
func (h *MeilisearchHandler) UpdateIndexSettings(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
service := h.meilisearchManager.GetService()
|
||||
if service == nil {
|
||||
ErrorResponse(c, "Meilisearch服务未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.UpdateIndexSettings(); err != nil {
|
||||
ErrorResponse(c, "更新索引设置失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "索引设置更新成功"})
|
||||
}
|
||||
565
handlers/og_image.go
Normal file
565
handlers/og_image.go
Normal file
@@ -0,0 +1,565 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/ctwj/urldb/db"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/fogleman/gg"
|
||||
"image/color"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
)
|
||||
|
||||
// OGImageHandler 处理OG图片生成请求
|
||||
type OGImageHandler struct{}
|
||||
|
||||
// NewOGImageHandler 创建新的OG图片处理器
|
||||
func NewOGImageHandler() *OGImageHandler {
|
||||
return &OGImageHandler{}
|
||||
}
|
||||
|
||||
// Resource 简化的资源结构体
|
||||
type Resource struct {
|
||||
Title string
|
||||
Description string
|
||||
Cover string
|
||||
Key string
|
||||
}
|
||||
|
||||
// getResourceByKey 通过key获取资源信息
|
||||
func (h *OGImageHandler) getResourceByKey(key string) (*Resource, error) {
|
||||
// 这里简化处理,实际应该从数据库查询
|
||||
// 为了演示,我们先返回一个模拟的资源
|
||||
// 在实际应用中,您需要连接数据库并查询
|
||||
|
||||
// 模拟数据库查询 - 实际应用中请替换为真实的数据库查询
|
||||
dbInstance := db.DB
|
||||
if dbInstance == nil {
|
||||
return nil, fmt.Errorf("数据库连接失败")
|
||||
}
|
||||
|
||||
var resource entity.Resource
|
||||
result := dbInstance.Where("key = ?", key).First(&resource)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return &Resource{
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
Cover: resource.Cover,
|
||||
Key: resource.Key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateOGImage 生成OG图片
|
||||
func (h *OGImageHandler) GenerateOGImage(c *gin.Context) {
|
||||
// 获取请求参数
|
||||
key := strings.TrimSpace(c.Query("key"))
|
||||
title := strings.TrimSpace(c.Query("title"))
|
||||
description := strings.TrimSpace(c.Query("description"))
|
||||
siteName := strings.TrimSpace(c.Query("site_name"))
|
||||
theme := strings.TrimSpace(c.Query("theme"))
|
||||
coverUrl := strings.TrimSpace(c.Query("cover"))
|
||||
|
||||
width, _ := strconv.Atoi(c.Query("width"))
|
||||
height, _ := strconv.Atoi(c.Query("height"))
|
||||
|
||||
// 如果提供了key,从数据库获取资源信息
|
||||
if key != "" {
|
||||
resource, err := h.getResourceByKey(key)
|
||||
if err == nil && resource != nil {
|
||||
if title == "" {
|
||||
title = resource.Title
|
||||
}
|
||||
if description == "" {
|
||||
description = resource.Description
|
||||
}
|
||||
if coverUrl == "" && resource.Cover != "" {
|
||||
coverUrl = resource.Cover
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if title == "" {
|
||||
title = "老九网盘资源数据库"
|
||||
}
|
||||
if siteName == "" {
|
||||
siteName = "老九网盘"
|
||||
}
|
||||
if width <= 0 || width > 2000 {
|
||||
width = 1200
|
||||
}
|
||||
if height <= 0 || height > 2000 {
|
||||
height = 630
|
||||
}
|
||||
|
||||
// 获取当前请求的域名
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
domain := scheme + "://" + host
|
||||
|
||||
// 生成图片
|
||||
imageBuffer, err := createOGImage(title, description, siteName, theme, width, height, coverUrl, key, domain)
|
||||
if err != nil {
|
||||
utils.Error("生成OG图片失败: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate image: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回图片
|
||||
c.Data(http.StatusOK, "image/png", imageBuffer.Bytes())
|
||||
c.Header("Content-Type", "image/png")
|
||||
c.Header("Cache-Control", "public, max-age=3600")
|
||||
}
|
||||
|
||||
// createOGImage 创建OG图片
|
||||
func createOGImage(title, description, siteName, theme string, width, height int, coverUrl, key, domain string) (*bytes.Buffer, error) {
|
||||
dc := gg.NewContext(width, height)
|
||||
|
||||
// 设置圆角裁剪区域
|
||||
cornerRadius := 20.0
|
||||
dc.DrawRoundedRectangle(0, 0, float64(width), float64(height), cornerRadius)
|
||||
|
||||
// 设置背景色
|
||||
bgColor := getBackgroundColor(theme)
|
||||
dc.SetColor(bgColor)
|
||||
dc.Fill()
|
||||
|
||||
// 绘制渐变效果
|
||||
gradient := gg.NewLinearGradient(0, 0, float64(width), float64(height))
|
||||
gradient.AddColorStop(0, getGradientStartColor(theme))
|
||||
gradient.AddColorStop(1, getGradientEndColor(theme))
|
||||
dc.SetFillStyle(gradient)
|
||||
dc.Fill()
|
||||
|
||||
// 定义布局区域
|
||||
imageAreaWidth := width / 3 // 左侧1/3用于图片
|
||||
textAreaWidth := width * 2 / 3 // 右侧2/3用于文案
|
||||
textAreaX := imageAreaWidth // 文案区域起始X坐标
|
||||
|
||||
// 统一的字体加载函数,确保中文显示正常
|
||||
loadChineseFont := func(fontSize float64) bool {
|
||||
// 优先使用项目字体
|
||||
if err := dc.LoadFontFace("font/SourceHanSansSC-Regular.otf", fontSize); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Windows系统常见字体,按优先级顺序尝试
|
||||
commonFonts := []string{
|
||||
"C:/Windows/Fonts/msyh.ttc", // 微软雅黑
|
||||
"C:/Windows/Fonts/simhei.ttf", // 黑体
|
||||
"C:/Windows/Fonts/simsun.ttc", // 宋体
|
||||
}
|
||||
|
||||
for _, fontPath := range commonFonts {
|
||||
if err := dc.LoadFontFace(fontPath, fontSize); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都失败了,尝试使用粗体版本
|
||||
boldFonts := []string{
|
||||
"C:/Windows/Fonts/msyhbd.ttc", // 微软雅黑粗体
|
||||
"C:/Windows/Fonts/simhei.ttf", // 黑体
|
||||
}
|
||||
|
||||
for _, fontPath := range boldFonts {
|
||||
if err := dc.LoadFontFace(fontPath, fontSize); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 加载基础字体(24px)
|
||||
fontLoaded := loadChineseFont(24)
|
||||
dc.SetHexColor("#ffffff")
|
||||
|
||||
// 绘制封面图片(如果存在)
|
||||
if coverUrl != "" {
|
||||
if err := drawCoverImageInLeftArea(dc, coverUrl, width, height, imageAreaWidth); err != nil {
|
||||
utils.Error("绘制封面图片失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置站点标识
|
||||
dc.DrawStringAnchored(siteName, float64(textAreaX)+60, 50, 0, 0.5)
|
||||
|
||||
// 绘制标题
|
||||
dc.SetHexColor("#ffffff")
|
||||
|
||||
// 标题在右侧区域显示,考虑文案宽度限制
|
||||
maxTitleWidth := float64(textAreaWidth - 120) // 右侧区域减去左右边距
|
||||
|
||||
// 动态调整字体大小以适应文案区域,使用统一的字体加载逻辑
|
||||
fontSize := 48.0
|
||||
titleFontLoaded := false
|
||||
for fontSize > 24 { // 最小字体24
|
||||
// 优先使用项目粗体字体
|
||||
if err := dc.LoadFontFace("font/SourceHanSansSC-Bold.otf", fontSize); err == nil {
|
||||
titleWidth, _ := dc.MeasureString(title)
|
||||
if titleWidth <= maxTitleWidth {
|
||||
titleFontLoaded = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// 尝试系统粗体字体
|
||||
boldFonts := []string{
|
||||
"C:/Windows/Fonts/msyhbd.ttc", // 微软雅黑粗体
|
||||
"C:/Windows/Fonts/simhei.ttf", // 黑体
|
||||
}
|
||||
for _, fontPath := range boldFonts {
|
||||
if err := dc.LoadFontFace(fontPath, fontSize); err == nil {
|
||||
titleWidth, _ := dc.MeasureString(title)
|
||||
if titleWidth <= maxTitleWidth {
|
||||
titleFontLoaded = true
|
||||
break
|
||||
}
|
||||
break // 找到可用字体就跳出内层循环
|
||||
}
|
||||
}
|
||||
if titleFontLoaded {
|
||||
break
|
||||
}
|
||||
}
|
||||
fontSize -= 4
|
||||
}
|
||||
|
||||
// 如果粗体字体都失败了,使用常规字体
|
||||
if !titleFontLoaded {
|
||||
loadChineseFont(36) // 使用稍大的常规字体
|
||||
}
|
||||
|
||||
// 标题左对齐显示在右侧区域
|
||||
titleX := float64(textAreaX) + 60
|
||||
titleY := float64(height)/2 - 80
|
||||
dc.DrawString(title, titleX, titleY)
|
||||
|
||||
// 绘制描述
|
||||
if description != "" {
|
||||
dc.SetHexColor("#e5e7eb")
|
||||
// 使用统一的字体加载逻辑
|
||||
loadChineseFont(28)
|
||||
|
||||
// 自动换行处理,适配右侧区域宽度
|
||||
wrappedDesc := wrapText(dc, description, float64(textAreaWidth-120))
|
||||
descY := titleY + 60 // 标题下方
|
||||
|
||||
for i, line := range wrappedDesc {
|
||||
y := descY + float64(i)*30 // 行高30像素
|
||||
dc.DrawString(line, titleX, y)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加装饰性元素
|
||||
drawDecorativeElements(dc, width, height, theme)
|
||||
|
||||
// 绘制底部URL访问地址
|
||||
if key != "" && domain != "" {
|
||||
resourceURL := domain + "/r/" + key
|
||||
dc.SetHexColor("#d1d5db") // 浅灰色
|
||||
|
||||
// 使用统一的字体加载逻辑
|
||||
loadChineseFont(20)
|
||||
|
||||
// URL位置:底部居中,距离底部边缘40像素,给更多空间
|
||||
urlY := float64(height) - 40
|
||||
|
||||
dc.DrawStringAnchored(resourceURL, float64(width)/2, urlY, 0.5, 0.5)
|
||||
}
|
||||
|
||||
// 添加调试信息(仅在开发环境)
|
||||
if title == "DEBUG" {
|
||||
dc.SetHexColor("#ff0000")
|
||||
dc.DrawString("Font loaded: "+strconv.FormatBool(fontLoaded), 50, float64(height)-80)
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
buf := &bytes.Buffer{}
|
||||
err := dc.EncodePNG(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// getBackgroundColor 获取背景色
|
||||
func getBackgroundColor(theme string) color.RGBA {
|
||||
switch theme {
|
||||
case "dark":
|
||||
return color.RGBA{31, 41, 55, 255} // slate-800
|
||||
case "blue":
|
||||
return color.RGBA{29, 78, 216, 255} // blue-700
|
||||
case "green":
|
||||
return color.RGBA{6, 95, 70, 255} // emerald-800
|
||||
case "purple":
|
||||
return color.RGBA{109, 40, 217, 255} // violet-700
|
||||
default:
|
||||
return color.RGBA{55, 65, 81, 255} // gray-800
|
||||
}
|
||||
}
|
||||
|
||||
// getGradientStartColor 获取渐变起始色
|
||||
func getGradientStartColor(theme string) color.Color {
|
||||
switch theme {
|
||||
case "dark":
|
||||
return color.RGBA{15, 23, 42, 255} // slate-900
|
||||
case "blue":
|
||||
return color.RGBA{30, 58, 138, 255} // blue-900
|
||||
case "green":
|
||||
return color.RGBA{6, 78, 59, 255} // emerald-900
|
||||
case "purple":
|
||||
return color.RGBA{91, 33, 182, 255} // violet-800
|
||||
default:
|
||||
return color.RGBA{31, 41, 55, 255} // gray-800
|
||||
}
|
||||
}
|
||||
|
||||
// getGradientEndColor 获取渐变结束色
|
||||
func getGradientEndColor(theme string) color.Color {
|
||||
switch theme {
|
||||
case "dark":
|
||||
return color.RGBA{55, 65, 81, 255} // slate-700
|
||||
case "blue":
|
||||
return color.RGBA{59, 130, 246, 255} // blue-500
|
||||
case "green":
|
||||
return color.RGBA{16, 185, 129, 255} // emerald-500
|
||||
case "purple":
|
||||
return color.RGBA{139, 92, 246, 255} // violet-500
|
||||
default:
|
||||
return color.RGBA{75, 85, 99, 255} // gray-600
|
||||
}
|
||||
}
|
||||
|
||||
// wrapText 文本自动换行处理
|
||||
func wrapText(dc *gg.Context, text string, maxWidth float64) []string {
|
||||
var lines []string
|
||||
words := []rune(text)
|
||||
|
||||
currentLine := ""
|
||||
for _, word := range words {
|
||||
testLine := currentLine + string(word)
|
||||
width, _ := dc.MeasureString(testLine)
|
||||
|
||||
if width > maxWidth && len(currentLine) > 0 {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = string(word)
|
||||
} else {
|
||||
currentLine = testLine
|
||||
}
|
||||
}
|
||||
|
||||
if currentLine != "" {
|
||||
lines = append(lines, currentLine)
|
||||
}
|
||||
|
||||
// 最多显示3行
|
||||
if len(lines) > 3 {
|
||||
lines = lines[:3]
|
||||
// 在最后一行添加省略号
|
||||
if len(lines[2]) > 3 {
|
||||
lines[2] = lines[2][:len(lines[2])-3] + "..."
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// drawDecorativeElements 绘制装饰性元素
|
||||
func drawDecorativeElements(dc *gg.Context, width, height int, theme string) {
|
||||
// 绘制装饰性圆点
|
||||
dc.SetHexColor("#ffffff")
|
||||
dc.SetLineWidth(2)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
x := float64(100 + i*150)
|
||||
y := float64(100 + (i%2)*200)
|
||||
dc.DrawCircle(x, y, 8)
|
||||
dc.Stroke()
|
||||
}
|
||||
|
||||
// 绘制底部装饰线
|
||||
dc.DrawLine(60, float64(height-80), float64(width-60), float64(height-80))
|
||||
dc.Stroke()
|
||||
}
|
||||
|
||||
// drawCoverImageInLeftArea 在左侧1/3区域绘制封面图片
|
||||
func drawCoverImageInLeftArea(dc *gg.Context, coverUrl string, width, height int, imageAreaWidth int) error {
|
||||
// 下载封面图片
|
||||
resp, err := http.Get(coverUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取图片数据
|
||||
imgData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取图片尺寸和宽高比
|
||||
bounds := img.Bounds()
|
||||
imgWidth := bounds.Dx()
|
||||
imgHeight := bounds.Dy()
|
||||
aspectRatio := float64(imgWidth) / float64(imgHeight)
|
||||
|
||||
// 计算图片区域的可显示尺寸,留出边距
|
||||
padding := 40
|
||||
maxImageWidth := imageAreaWidth - padding*2
|
||||
maxImageHeight := height - padding*2
|
||||
|
||||
var scaledImg image.Image
|
||||
var drawWidth, drawHeight, drawX, drawY int
|
||||
|
||||
// 判断是竖图还是横图,采用不同的缩放策略
|
||||
if aspectRatio < 1.0 {
|
||||
// 竖图:充满整个左侧区域(去掉边距)
|
||||
drawHeight = height - padding*2 // 留上下边距
|
||||
drawWidth = int(float64(drawHeight) * aspectRatio)
|
||||
|
||||
// 如果宽度超出左侧区域,则以宽度为准充满整个区域宽度
|
||||
if drawWidth > imageAreaWidth - padding*2 {
|
||||
drawWidth = imageAreaWidth - padding*2
|
||||
drawHeight = int(float64(drawWidth) / aspectRatio)
|
||||
}
|
||||
|
||||
// 缩放图片
|
||||
scaledImg = scaleImage(img, drawWidth, drawHeight)
|
||||
|
||||
// 垂直居中,水平居左
|
||||
drawX = padding
|
||||
drawY = (height - drawHeight) / 2
|
||||
} else {
|
||||
// 横图:优先占满宽度
|
||||
drawWidth = maxImageWidth
|
||||
drawHeight = int(float64(drawWidth) / aspectRatio)
|
||||
|
||||
// 如果高度超出限制,则以高度为准
|
||||
if drawHeight > maxImageHeight {
|
||||
drawHeight = maxImageHeight
|
||||
drawWidth = int(float64(drawHeight) * aspectRatio)
|
||||
}
|
||||
|
||||
// 缩放图片
|
||||
scaledImg = scaleImage(img, drawWidth, drawHeight)
|
||||
|
||||
// 水平居中,垂直居中
|
||||
drawX = (imageAreaWidth - drawWidth) / 2
|
||||
drawY = (height - drawHeight) / 2
|
||||
}
|
||||
|
||||
// 绘制图片
|
||||
dc.DrawImage(scaledImg, drawX, drawY)
|
||||
|
||||
// 添加半透明遮罩效果,让文字更清晰(仅在有图片时添加)
|
||||
maskColor := color.RGBA{0, 0, 0, 80} // 半透明黑色,透明度稍低
|
||||
dc.SetColor(maskColor)
|
||||
dc.DrawRectangle(float64(drawX), float64(drawY), float64(drawWidth), float64(drawHeight))
|
||||
dc.Fill()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scaleImage 图片缩放函数
|
||||
func scaleImage(img image.Image, width, height int) image.Image {
|
||||
// 使用 gg 库的 Scale 变换来实现缩放
|
||||
srcWidth := img.Bounds().Dx()
|
||||
srcHeight := img.Bounds().Dy()
|
||||
|
||||
// 创建目标尺寸的画布
|
||||
dc := gg.NewContext(width, height)
|
||||
|
||||
// 计算缩放比例
|
||||
scaleX := float64(width) / float64(srcWidth)
|
||||
scaleY := float64(height) / float64(srcHeight)
|
||||
|
||||
// 应用缩放变换并绘制图片
|
||||
dc.Scale(scaleX, scaleY)
|
||||
dc.DrawImage(img, 0, 0)
|
||||
|
||||
return dc.Image()
|
||||
}
|
||||
|
||||
// drawCoverImage 绘制封面图片(保留原函数作为备用)
|
||||
func drawCoverImage(dc *gg.Context, coverUrl string, width, height int) error {
|
||||
// 下载封面图片
|
||||
resp, err := http.Get(coverUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取图片数据
|
||||
imgData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算封面图片的位置和大小,放置在左侧
|
||||
coverWidth := 200 // 封面图宽度
|
||||
coverHeight := 280 // 封面图高度
|
||||
coverX := 50
|
||||
coverY := (height - coverHeight) / 2
|
||||
|
||||
// 绘制封面图片(按比例缩放)
|
||||
bounds := img.Bounds()
|
||||
imgWidth := bounds.Dx()
|
||||
imgHeight := bounds.Dy()
|
||||
|
||||
// 计算缩放比例,保持宽高比
|
||||
scaleX := float64(coverWidth) / float64(imgWidth)
|
||||
scaleY := float64(coverHeight) / float64(imgHeight)
|
||||
scale := scaleX
|
||||
if scaleY < scaleX {
|
||||
scale = scaleY
|
||||
}
|
||||
|
||||
// 计算缩放后的尺寸
|
||||
newWidth := int(float64(imgWidth) * scale)
|
||||
newHeight := int(float64(imgHeight) * scale)
|
||||
|
||||
// 居中绘制
|
||||
offsetX := coverX + (coverWidth-newWidth)/2
|
||||
offsetY := coverY + (coverHeight-newHeight)/2
|
||||
|
||||
dc.DrawImage(img, offsetX, offsetY)
|
||||
|
||||
// 添加半透明遮罩效果,让文字更清晰
|
||||
maskColor := color.RGBA{0, 0, 0, 120} // 半透明黑色
|
||||
dc.SetColor(maskColor)
|
||||
dc.DrawRectangle(float64(coverX), float64(coverY), float64(coverWidth), float64(coverHeight))
|
||||
dc.Fill()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -68,6 +71,32 @@ func (h *PublicAPIHandler) filterForbiddenWords(resources []entity.Resource) ([]
|
||||
return filteredResources, uniqueForbiddenWords
|
||||
}
|
||||
|
||||
// logAPIAccess 记录API访问日志
|
||||
func (h *PublicAPIHandler) logAPIAccess(c *gin.Context, startTime time.Time, processCount int, responseData interface{}, errorMessage string) {
|
||||
endpoint := c.Request.URL.Path
|
||||
method := c.Request.Method
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
// 计算处理时间
|
||||
processingTime := time.Since(startTime).Milliseconds()
|
||||
|
||||
// 获取查询参数
|
||||
var requestParams interface{}
|
||||
if method == "GET" {
|
||||
requestParams = c.Request.URL.Query()
|
||||
} else {
|
||||
// 对于POST请求,尝试从上下文中获取请求体(如果之前已解析)
|
||||
if req, exists := c.Get("request_body"); exists {
|
||||
requestParams = req
|
||||
}
|
||||
}
|
||||
|
||||
// 记录API访问日志 - 使用简单日志记录
|
||||
h.recordAPIAccessToDB(ip, userAgent, endpoint, method, requestParams,
|
||||
c.Writer.Status(), responseData, processCount, errorMessage, processingTime)
|
||||
}
|
||||
|
||||
// AddBatchResources godoc
|
||||
// @Summary 批量添加资源
|
||||
// @Description 通过公开API批量添加多个资源到待处理列表
|
||||
@@ -82,17 +111,28 @@ func (h *PublicAPIHandler) filterForbiddenWords(resources []entity.Resource) ([]
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/public/resources/batch-add [post]
|
||||
func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
var req dto.BatchReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logAPIAccess(c, startTime, 0, nil, "请求参数错误: "+err.Error())
|
||||
ErrorResponse(c, "请求参数错误: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 存储请求体用于日志记录
|
||||
c.Set("request_body", req)
|
||||
|
||||
if len(req.Resources) == 0 {
|
||||
ErrorResponse(c, "资源列表不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录API访问安全日志
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
utils.Info("PublicAPI.AddBatchResources - API访问 - IP: %s, UserAgent: %s, 资源数量: %d", clientIP, userAgent, len(req.Resources))
|
||||
|
||||
// 收集所有待提交的URL,去重
|
||||
urlSet := make(map[string]struct{})
|
||||
for _, resource := range req.Resources {
|
||||
@@ -124,6 +164,7 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
// 生成 key(每组同一个 key)
|
||||
key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
|
||||
if err != nil {
|
||||
h.logAPIAccess(c, startTime, len(createdResources), nil, "生成资源组标识失败: "+err.Error())
|
||||
ErrorResponse(c, "生成资源组标识失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
@@ -155,10 +196,12 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
responseData := gin.H{
|
||||
"created_count": len(createdResources),
|
||||
"created_ids": createdResources,
|
||||
})
|
||||
}
|
||||
h.logAPIAccess(c, startTime, len(createdResources), responseData, "")
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
// SearchResources godoc
|
||||
@@ -178,13 +221,21 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/public/resources/search [get]
|
||||
func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
// 获取查询参数
|
||||
startTime := time.Now()
|
||||
|
||||
// 记录API访问安全日志
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
keyword := c.Query("keyword")
|
||||
tag := c.Query("tag")
|
||||
category := c.Query("category")
|
||||
panID := c.Query("pan_id")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "20")
|
||||
|
||||
utils.Info("PublicAPI.SearchResources - API访问 - IP: %s, UserAgent: %s, Keyword: %s, Tag: %s, Category: %s, PanID: %s",
|
||||
clientIP, userAgent, keyword, tag, category, panID)
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
@@ -195,67 +246,133 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 构建搜索条件
|
||||
params := map[string]interface{}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
// 如果启用了Meilisearch,优先使用Meilisearch搜索
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
// 构建过滤器
|
||||
filters := make(map[string]interface{})
|
||||
if category != "" {
|
||||
filters["category"] = category
|
||||
}
|
||||
if tag != "" {
|
||||
filters["tags"] = tag
|
||||
}
|
||||
if panID != "" {
|
||||
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
|
||||
// 根据pan_id获取pan_name
|
||||
pan, err := repoManager.PanRepository.FindByID(uint(id))
|
||||
if err == nil && pan != nil {
|
||||
filters["pan_name"] = pan.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Meilisearch搜索
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
docs, docTotal, err := service.Search(keyword, filters, page, pageSize)
|
||||
if err == nil {
|
||||
// 将Meilisearch文档转换为Resource实体(保持兼容性)
|
||||
for _, doc := range docs {
|
||||
resource := entity.Resource{
|
||||
ID: doc.ID,
|
||||
Title: doc.Title,
|
||||
Description: doc.Description,
|
||||
URL: doc.URL,
|
||||
SaveURL: doc.SaveURL,
|
||||
FileSize: doc.FileSize,
|
||||
Key: doc.Key,
|
||||
PanID: doc.PanID,
|
||||
Cover: doc.Cover,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
total = docTotal
|
||||
} else {
|
||||
utils.Error("Meilisearch搜索失败,回退到数据库搜索: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
params["search"] = keyword
|
||||
// 如果Meilisearch未启用或搜索失败,使用数据库搜索
|
||||
if meilisearchManager == nil || !meilisearchManager.IsEnabled() || err != nil {
|
||||
// 构建搜索条件
|
||||
params := map[string]interface{}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
params["search"] = keyword
|
||||
}
|
||||
|
||||
if tag != "" {
|
||||
params["tag"] = tag
|
||||
}
|
||||
|
||||
if category != "" {
|
||||
params["category"] = category
|
||||
}
|
||||
if panID != "" {
|
||||
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
|
||||
params["pan_id"] = uint(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行数据库搜索
|
||||
resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
if err != nil {
|
||||
h.logAPIAccess(c, startTime, 0, nil, "搜索失败: "+err.Error())
|
||||
ErrorResponse(c, "搜索失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if tag != "" {
|
||||
params["tag"] = tag
|
||||
}
|
||||
|
||||
if category != "" {
|
||||
params["category"] = category
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
// 获取违禁词配置(只获取一次)
|
||||
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
|
||||
return repoManager.SystemConfigRepository.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||||
})
|
||||
if err != nil {
|
||||
ErrorResponse(c, "搜索失败: "+err.Error(), 500)
|
||||
return
|
||||
utils.Error("获取违禁词配置失败: %v", err)
|
||||
cleanWords = []string{} // 如果获取失败,使用空列表
|
||||
}
|
||||
|
||||
// 过滤违禁词
|
||||
filteredResources, foundForbiddenWords := h.filterForbiddenWords(resources)
|
||||
|
||||
// 计算过滤后的总数
|
||||
filteredTotal := len(filteredResources)
|
||||
|
||||
// 转换为响应格式
|
||||
// 转换为响应格式并添加违禁词标记
|
||||
var resourceResponses []gin.H
|
||||
for _, resource := range filteredResources {
|
||||
resourceResponses = append(resourceResponses, gin.H{
|
||||
"id": resource.ID,
|
||||
"title": resource.Title,
|
||||
"url": resource.URL,
|
||||
"description": resource.Description,
|
||||
"view_count": resource.ViewCount,
|
||||
"created_at": resource.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"updated_at": resource.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
for i, processedResource := range resources {
|
||||
originalResource := resources[i]
|
||||
forbiddenInfo := utils.CheckResourceForbiddenWords(originalResource.Title, originalResource.Description, cleanWords)
|
||||
|
||||
resourceResponse := gin.H{
|
||||
"id": processedResource.ID,
|
||||
"title": forbiddenInfo.ProcessedTitle, // 使用处理后的标题
|
||||
"url": processedResource.URL,
|
||||
"description": forbiddenInfo.ProcessedDesc, // 使用处理后的描述
|
||||
"view_count": processedResource.ViewCount,
|
||||
"created_at": processedResource.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"updated_at": processedResource.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
"cover": processedResource.Cover, // 添加封面字段
|
||||
}
|
||||
|
||||
// 添加违禁词标记
|
||||
resourceResponse["has_forbidden_words"] = forbiddenInfo.HasForbiddenWords
|
||||
resourceResponse["forbidden_words"] = forbiddenInfo.ForbiddenWords
|
||||
resourceResponses = append(resourceResponses, resourceResponse)
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
responseData := gin.H{
|
||||
"list": resourceResponses,
|
||||
"total": filteredTotal,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": pageSize,
|
||||
}
|
||||
|
||||
// 如果存在违禁词过滤,添加提醒字段
|
||||
if len(foundForbiddenWords) > 0 {
|
||||
responseData["forbidden_words_filtered"] = true
|
||||
responseData["filtered_forbidden_words"] = foundForbiddenWords
|
||||
responseData["original_total"] = total
|
||||
responseData["filtered_count"] = total - int64(filteredTotal)
|
||||
}
|
||||
|
||||
h.logAPIAccess(c, startTime, len(resourceResponses), responseData, "")
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
@@ -273,9 +390,16 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/public/hot-dramas [get]
|
||||
func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 记录API访问安全日志
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "20")
|
||||
|
||||
utils.Info("PublicAPI.GetHotDramas - API访问 - IP: %s, UserAgent: %s", clientIP, userAgent)
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
@@ -289,6 +413,7 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
// 获取热门剧
|
||||
hotDramas, total, err := repoManager.HotDramaRepository.FindAll(page, pageSize)
|
||||
if err != nil {
|
||||
h.logAPIAccess(c, startTime, 0, nil, "获取热门剧失败: "+err.Error())
|
||||
ErrorResponse(c, "获取热门剧失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
@@ -312,10 +437,58 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
responseData := gin.H{
|
||||
"hot_dramas": hotDramaResponses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
h.logAPIAccess(c, startTime, len(hotDramaResponses), responseData, "")
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
// recordAPIAccessToDB 记录API访问日志到数据库
|
||||
func (h *PublicAPIHandler) recordAPIAccessToDB(ip, userAgent, endpoint, method string,
|
||||
requestParams interface{}, responseStatus int, responseData interface{},
|
||||
processCount int, errorMessage string, processingTime int64) {
|
||||
|
||||
// 只记录重要的API访问(有错误或处理时间较长的)
|
||||
if errorMessage == "" && processingTime < 1000 && responseStatus < 400 {
|
||||
return // 跳过正常的快速请求
|
||||
}
|
||||
|
||||
// 转换参数为JSON字符串
|
||||
var requestParamsStr, responseDataStr string
|
||||
if requestParams != nil {
|
||||
if jsonBytes, err := json.Marshal(requestParams); err == nil {
|
||||
requestParamsStr = string(jsonBytes)
|
||||
}
|
||||
}
|
||||
if responseData != nil {
|
||||
if jsonBytes, err := json.Marshal(responseData); err == nil {
|
||||
responseDataStr = string(jsonBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建日志记录
|
||||
logEntry := &entity.APIAccessLog{
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
Endpoint: endpoint,
|
||||
Method: method,
|
||||
RequestParams: requestParamsStr,
|
||||
ResponseStatus: responseStatus,
|
||||
ResponseData: responseDataStr,
|
||||
ProcessCount: processCount,
|
||||
ErrorMessage: errorMessage,
|
||||
ProcessingTime: processingTime,
|
||||
}
|
||||
|
||||
// 异步保存到数据库(避免影响API性能)
|
||||
go func() {
|
||||
if err := repoManager.APIAccessLogRepository.Create(logEntry); err != nil {
|
||||
// 记录失败只输出到系统日志,不影响API
|
||||
utils.Error("保存API访问日志失败: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
310
handlers/report_handler.go
Normal file
310
handlers/report_handler.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ReportHandler struct {
|
||||
reportRepo repo.ReportRepository
|
||||
resourceRepo repo.ResourceRepository
|
||||
validate *validator.Validate
|
||||
}
|
||||
|
||||
func NewReportHandler(reportRepo repo.ReportRepository, resourceRepo repo.ResourceRepository) *ReportHandler {
|
||||
return &ReportHandler{
|
||||
reportRepo: reportRepo,
|
||||
resourceRepo: resourceRepo,
|
||||
validate: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateReport 创建举报
|
||||
// @Summary 创建举报
|
||||
// @Description 提交资源举报
|
||||
// @Tags Report
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.ReportCreateRequest true "举报信息"
|
||||
// @Success 200 {object} Response{data=dto.ReportResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /reports [post]
|
||||
func (h *ReportHandler) CreateReport(c *gin.Context) {
|
||||
var req dto.ReportCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建举报实体
|
||||
report := &entity.Report{
|
||||
ResourceKey: req.ResourceKey,
|
||||
Reason: req.Reason,
|
||||
Description: req.Description,
|
||||
Contact: req.Contact,
|
||||
UserAgent: req.UserAgent,
|
||||
IPAddress: req.IPAddress,
|
||||
Status: "pending", // 默认为待处理
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := h.reportRepo.Create(report); err != nil {
|
||||
ErrorResponse(c, "创建举报失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := converter.ReportToResponse(report)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetReport 获取举报详情
|
||||
// @Summary 获取举报详情
|
||||
// @Description 根据ID获取举报详情
|
||||
// @Tags Report
|
||||
// @Produce json
|
||||
// @Param id path int true "举报ID"
|
||||
// @Success 200 {object} Response{data=dto.ReportResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 404 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /reports/{id} [get]
|
||||
func (h *ReportHandler) GetReport(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.reportRepo.GetByID(uint(id))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
ErrorResponse(c, "举报不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ErrorResponse(c, "获取举报失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ReportToResponse(report)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// ListReports 获取举报列表
|
||||
// @Summary 获取举报列表
|
||||
// @Description 获取举报列表(支持分页和状态筛选)
|
||||
// @Tags Report
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param status query string false "处理状态"
|
||||
// @Success 200 {object} Response{data=object{items=[]dto.ReportResponse,total=int}}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /reports [get]
|
||||
func (h *ReportHandler) ListReports(c *gin.Context) {
|
||||
var req dto.ReportListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Page == 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
reports, total, err := h.reportRepo.List(req.Status, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取举报列表失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取每个举报关联的资源
|
||||
var reportResponses []*dto.ReportResponse
|
||||
for _, report := range reports {
|
||||
// 通过资源key查找关联的资源
|
||||
resources, err := h.getResourcesByResourceKey(report.ResourceKey)
|
||||
if err != nil {
|
||||
// 如果获取资源失败,仍然返回基本的举报信息
|
||||
reportResponses = append(reportResponses, converter.ReportToResponse(report))
|
||||
} else {
|
||||
// 使用包含资源详情的转换函数
|
||||
response := converter.ReportToResponseWithResources(report, resources)
|
||||
reportResponses = append(reportResponses, response)
|
||||
}
|
||||
}
|
||||
|
||||
PageResponse(c, reportResponses, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// getResourcesByResourceKey 根据资源key获取关联的资源列表
|
||||
func (h *ReportHandler) getResourcesByResourceKey(resourceKey string) ([]*entity.Resource, error) {
|
||||
// 从资源仓库获取与key关联的所有资源
|
||||
resources, err := h.resourceRepo.FindByResourceKey(resourceKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 将 []entity.Resource 转换为 []*entity.Resource
|
||||
var resourcePointers []*entity.Resource
|
||||
for i := range resources {
|
||||
resourcePointers = append(resourcePointers, &resources[i])
|
||||
}
|
||||
|
||||
return resourcePointers, nil
|
||||
}
|
||||
|
||||
// UpdateReport 更新举报状态
|
||||
// @Summary 更新举报状态
|
||||
// @Description 更新举报处理状态
|
||||
// @Tags Report
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "举报ID"
|
||||
// @Param request body dto.ReportUpdateRequest true "更新信息"
|
||||
// @Success 200 {object} Response{data=dto.ReportResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 404 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /reports/{id} [put]
|
||||
func (h *ReportHandler) UpdateReport(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.ReportUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前举报
|
||||
_, err = h.reportRepo.GetByID(uint(id))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
ErrorResponse(c, "举报不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ErrorResponse(c, "获取举报失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
processedBy := uint(0) // 从上下文获取当前用户ID,如果存在的话
|
||||
if currentUser := c.GetUint("user_id"); currentUser > 0 {
|
||||
processedBy = currentUser
|
||||
}
|
||||
|
||||
if err := h.reportRepo.UpdateStatus(uint(id), req.Status, &processedBy, req.Note); err != nil {
|
||||
ErrorResponse(c, "更新举报状态失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取更新后的举报信息
|
||||
updatedReport, err := h.reportRepo.GetByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取更新后举报信息失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, converter.ReportToResponse(updatedReport))
|
||||
}
|
||||
|
||||
// DeleteReport 删除举报
|
||||
// @Summary 删除举报
|
||||
// @Description 删除举报记录
|
||||
// @Tags Report
|
||||
// @Produce json
|
||||
// @Param id path int true "举报ID"
|
||||
// @Success 200 {object} Response
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /reports/{id} [delete]
|
||||
func (h *ReportHandler) DeleteReport(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.reportRepo.Delete(uint(id)); err != nil {
|
||||
ErrorResponse(c, "删除举报失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, nil)
|
||||
}
|
||||
|
||||
// GetReportByResource 获取某个资源的举报列表
|
||||
// @Summary 获取资源举报列表
|
||||
// @Description 获取某个资源的所有举报记录
|
||||
// @Tags Report
|
||||
// @Produce json
|
||||
// @Param resource_key path string true "资源Key"
|
||||
// @Success 200 {object} Response{data=[]dto.ReportResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /reports/resource/{resource_key} [get]
|
||||
func (h *ReportHandler) GetReportByResource(c *gin.Context) {
|
||||
resourceKey := c.Param("resource_key")
|
||||
if resourceKey == "" {
|
||||
ErrorResponse(c, "资源Key不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
reports, err := h.reportRepo.GetByResourceKey(resourceKey)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取资源举报失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, converter.ReportsToResponse(reports))
|
||||
}
|
||||
|
||||
// RegisterReportRoutes 注册举报相关路由
|
||||
func RegisterReportRoutes(router *gin.RouterGroup, reportRepo repo.ReportRepository, resourceRepo repo.ResourceRepository) {
|
||||
handler := NewReportHandler(reportRepo, resourceRepo)
|
||||
|
||||
reports := router.Group("/reports")
|
||||
{
|
||||
reports.POST("", handler.CreateReport) // 创建举报
|
||||
reports.GET("/:id", handler.GetReport) // 获取举报详情
|
||||
reports.GET("", handler.ListReports) // 获取举报列表
|
||||
reports.PUT("/:id", handler.UpdateReport) // 更新举报状态
|
||||
reports.DELETE("/:id", handler.DeleteReport) // 删除举报
|
||||
reports.GET("/resource/:resource_key", handler.GetReportByResource) // 获取资源举报列表
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user