mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 19:37:33 +08:00
Merge branch 'main' of https://github.com/ctwj/urldb into feat_xunlei
This commit is contained in:
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
|
||||
```
|
||||
32
ChangeLog.md
Normal file
32
ChangeLog.md
Normal file
@@ -0,0 +1,32 @@
|
||||
### 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
|
||||
|
||||
29
README.md
29
README.md
@@ -35,30 +35,24 @@
|
||||
- [服务器要求](https://ecn5khs4t956.feishu.cn/wiki/W8YBww1Mmiu4Cdkp5W4c8pFNnMf?from=from_copylink)
|
||||
- [QQ机器人](https://github.com/ctwj/astrbot_plugin_urldb)
|
||||
|
||||
### v1.2.3
|
||||
1. 添加图片上传功能
|
||||
2. 添加Logo配置项,首页Logo显示
|
||||
3. 后台界面体验优化
|
||||
### v1.2.5
|
||||
1. 修复一些Bug
|
||||
|
||||
### v1.2.1
|
||||
1. 修复转存移除广告失败的问题和添加广告失败的问题
|
||||
2. 管理后台UI优化
|
||||
3. 首页添加描述显示
|
||||
### v1.2.4
|
||||
|
||||
### v1.2.0
|
||||
1. 新增手动批量转存
|
||||
2. 新增QQ机器人
|
||||
3. 新增任务管理功能
|
||||
4. 自动转存改版(批量转存修改为,显示二维码时自动转存)
|
||||
5. 新增支持第三方统计代码配置
|
||||
1. 搜索增强,毫秒级响应,关键字高亮显示
|
||||
2. 修复版本显示不正确的问题
|
||||
3. 配置项新增Meilisearch配置
|
||||
|
||||
### v1.0.0
|
||||
[详细改动记录](https://github.com/ctwj/urldb/blob/main/ChangeLog.md)
|
||||
|
||||
当前特性
|
||||
1. 支持API,手动批量录入资源
|
||||
2. 支持,自动判断资源有效性
|
||||
3. 支持自动转存
|
||||
3. 支持自动转存(Quark)
|
||||
4. 支持平台多账号管理(Quark)
|
||||
5. 支持简单的数据统计
|
||||
|
||||
6. 支持Meilisearch
|
||||
|
||||
|
||||
---
|
||||
@@ -66,7 +60,6 @@
|
||||
## 📸 项目截图
|
||||
|
||||
|
||||
|
||||
### 🏠 首页
|
||||

|
||||
|
||||
|
||||
@@ -263,6 +263,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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
@@ -26,6 +27,8 @@ func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
|
||||
Cover: resource.Cover,
|
||||
Author: resource.Author,
|
||||
ErrorMsg: resource.ErrorMsg,
|
||||
SyncedToMeilisearch: resource.SyncedToMeilisearch,
|
||||
SyncedAt: resource.SyncedAt,
|
||||
}
|
||||
|
||||
// 设置分类名称
|
||||
@@ -47,6 +50,89 @@ 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 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() {
|
||||
// Key字段在ResourceResponse中不存在,跳过
|
||||
}
|
||||
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))
|
||||
|
||||
@@ -78,6 +78,18 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +199,28 @@ 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 len(updatedKeys) > 0 {
|
||||
utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys)
|
||||
@@ -219,6 +253,12 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
entity.ConfigResponseFieldPageSize: 100,
|
||||
entity.ConfigResponseFieldMaintenanceMode: false,
|
||||
entity.ConfigResponseFieldEnableRegister: true, // 默认开启注册功能
|
||||
entity.ConfigResponseFieldThirdPartyStatsCode: "",
|
||||
entity.ConfigResponseFieldMeilisearchEnabled: false,
|
||||
entity.ConfigResponseFieldMeilisearchHost: "localhost",
|
||||
entity.ConfigResponseFieldMeilisearchPort: "7700",
|
||||
entity.ConfigResponseFieldMeilisearchMasterKey: "",
|
||||
entity.ConfigResponseFieldMeilisearchIndexName: "resources",
|
||||
}
|
||||
|
||||
// 将键值对转换为map
|
||||
@@ -280,6 +320,18 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
}
|
||||
case entity.ConfigKeyThirdPartyStatsCode:
|
||||
response[entity.ConfigResponseFieldThirdPartyStatsCode] = config.Value
|
||||
case entity.ConfigKeyMeilisearchEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldMeilisearchEnabled] = val
|
||||
}
|
||||
case entity.ConfigKeyMeilisearchHost:
|
||||
response[entity.ConfigResponseFieldMeilisearchHost] = config.Value
|
||||
case entity.ConfigKeyMeilisearchPort:
|
||||
response[entity.ConfigResponseFieldMeilisearchPort] = config.Value
|
||||
case entity.ConfigKeyMeilisearchMasterKey:
|
||||
response[entity.ConfigResponseFieldMeilisearchMasterKey] = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
response[entity.ConfigResponseFieldMeilisearchIndexName] = config.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,5 +367,10 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,16 @@ type ResourceResponse struct {
|
||||
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"`
|
||||
// 高亮字段
|
||||
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 分类响应
|
||||
|
||||
@@ -35,6 +35,13 @@ 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"`
|
||||
}
|
||||
|
||||
// SystemConfigResponse 系统配置响应
|
||||
@@ -76,6 +83,13 @@ 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"`
|
||||
}
|
||||
|
||||
// SystemConfigItem 单个配置项
|
||||
|
||||
@@ -28,6 +28,8 @@ type Resource struct {
|
||||
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,13 @@ const (
|
||||
|
||||
// 三方统计配置
|
||||
ConfigKeyThirdPartyStatsCode = "third_party_stats_code"
|
||||
|
||||
// Meilisearch配置
|
||||
ConfigKeyMeilisearchEnabled = "meilisearch_enabled"
|
||||
ConfigKeyMeilisearchHost = "meilisearch_host"
|
||||
ConfigKeyMeilisearchPort = "meilisearch_port"
|
||||
ConfigKeyMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigKeyMeilisearchIndexName = "meilisearch_index_name"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
@@ -84,6 +91,13 @@ const (
|
||||
|
||||
// 三方统计配置字段
|
||||
ConfigResponseFieldThirdPartyStatsCode = "third_party_stats_code"
|
||||
|
||||
// Meilisearch配置字段
|
||||
ConfigResponseFieldMeilisearchEnabled = "meilisearch_enabled"
|
||||
ConfigResponseFieldMeilisearchHost = "meilisearch_host"
|
||||
ConfigResponseFieldMeilisearchPort = "meilisearch_port"
|
||||
ConfigResponseFieldMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigResponseFieldMeilisearchIndexName = "meilisearch_index_name"
|
||||
)
|
||||
|
||||
// ConfigDefaultValue 配置默认值常量
|
||||
@@ -120,4 +134,11 @@ const (
|
||||
|
||||
// 三方统计配置默认值
|
||||
ConfigDefaultThirdPartyStatsCode = ""
|
||||
|
||||
// Meilisearch配置默认值
|
||||
ConfigDefaultMeilisearchEnabled = "false"
|
||||
ConfigDefaultMeilisearchHost = "localhost"
|
||||
ConfigDefaultMeilisearchPort = "7700"
|
||||
ConfigDefaultMeilisearchMasterKey = ""
|
||||
ConfigDefaultMeilisearchIndexName = "resources"
|
||||
)
|
||||
|
||||
@@ -34,6 +34,14 @@ 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)
|
||||
}
|
||||
|
||||
// ResourceRepositoryImpl Resource的Repository实现
|
||||
@@ -461,19 +469,145 @@ func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime t
|
||||
// GetByURL 根据URL获取资源
|
||||
func (r *ResourceRepositoryImpl) GetByURL(url string) (*entity.Resource, error) {
|
||||
var resource entity.Resource
|
||||
err := r.GetDB().Where("url = ?", url).First(&resource).Error
|
||||
err := r.db.Where("url = ?", url).First(&resource).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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").
|
||||
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").
|
||||
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
|
||||
}
|
||||
|
||||
@@ -121,10 +121,18 @@ 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},
|
||||
}
|
||||
|
||||
err = r.UpsertConfigs(defaultConfigs)
|
||||
@@ -149,12 +157,18 @@ 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},
|
||||
}
|
||||
|
||||
// 检查现有配置中是否有缺失的配置项
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
image: ctwj/urldb-backend:1.2.3
|
||||
image: ctwj/urldb-backend:1.2.5
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
image: ctwj/urldb-frontend:1.2.3
|
||||
image: ctwj/urldb-frontend:1.2.5
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NUXT_PUBLIC_API_SERVER: http://backend:8080/api
|
||||
|
||||
@@ -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)。
|
||||
@@ -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 停止服务"
|
||||
6
go.mod
6
go.mod
@@ -10,11 +10,17 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/meilisearch/meilisearch-go v0.33.1
|
||||
golang.org/x/crypto v0.40.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/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
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=
|
||||
@@ -37,6 +39,8 @@ github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ
|
||||
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/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -81,6 +85,8 @@ 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=
|
||||
@@ -114,6 +120,8 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
|
||||
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=
|
||||
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-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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": "索引设置更新成功"})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -182,6 +183,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
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")
|
||||
|
||||
@@ -195,6 +197,59 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
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,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
total = docTotal
|
||||
} else {
|
||||
utils.Error("Meilisearch搜索失败,回退到数据库搜索: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果Meilisearch未启用或搜索失败,使用数据库搜索
|
||||
if meilisearchManager == nil || !meilisearchManager.IsEnabled() || err != nil {
|
||||
// 构建搜索条件
|
||||
params := map[string]interface{}{
|
||||
"page": page,
|
||||
@@ -212,50 +267,59 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
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)
|
||||
// 执行数据库搜索
|
||||
resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "搜索失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤违禁词
|
||||
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"),
|
||||
// 获取违禁词配置(只获取一次)
|
||||
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
|
||||
return repoManager.SystemConfigRepository.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||||
})
|
||||
if err != nil {
|
||||
utils.Error("获取违禁词配置失败: %v", err)
|
||||
cleanWords = []string{} // 如果获取失败,使用空列表
|
||||
}
|
||||
|
||||
// 转换为响应格式并添加违禁词标记
|
||||
var resourceResponses []gin.H
|
||||
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"),
|
||||
}
|
||||
|
||||
// 添加违禁词标记
|
||||
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)
|
||||
}
|
||||
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
|
||||
@@ -64,19 +64,123 @@ func GetResources(c *gin.Context) {
|
||||
params["pan_name"] = panName
|
||||
}
|
||||
|
||||
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
// 获取违禁词配置(只获取一次)
|
||||
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
|
||||
return repoManager.SystemConfigRepository.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||||
})
|
||||
if err != nil {
|
||||
utils.Error("获取违禁词配置失败: %v", err)
|
||||
cleanWords = []string{} // 如果获取失败,使用空列表
|
||||
}
|
||||
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
// 如果有搜索关键词且启用了Meilisearch,优先使用Meilisearch搜索
|
||||
if search := c.Query("search"); search != "" && meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
// 构建Meilisearch过滤器
|
||||
filters := make(map[string]interface{})
|
||||
if panID := c.Query("pan_id"); panID != "" {
|
||||
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
|
||||
// 直接使用pan_id进行过滤
|
||||
filters["pan_id"] = id
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Meilisearch搜索
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
docs, docTotal, err := service.Search(search, filters, page, pageSize)
|
||||
if err == nil {
|
||||
|
||||
// 将Meilisearch文档转换为ResourceResponse(包含高亮信息)并处理违禁词
|
||||
var resourceResponses []dto.ResourceResponse
|
||||
for _, doc := range docs {
|
||||
resourceResponse := converter.ToResourceResponseFromMeilisearch(doc)
|
||||
|
||||
// 处理违禁词(Meilisearch场景,需要处理高亮标记)
|
||||
if len(cleanWords) > 0 {
|
||||
forbiddenInfo := utils.CheckResourceForbiddenWords(resourceResponse.Title, resourceResponse.Description, cleanWords)
|
||||
if forbiddenInfo.HasForbiddenWords {
|
||||
resourceResponse.Title = forbiddenInfo.ProcessedTitle
|
||||
resourceResponse.Description = forbiddenInfo.ProcessedDesc
|
||||
resourceResponse.TitleHighlight = forbiddenInfo.ProcessedTitle
|
||||
resourceResponse.DescriptionHighlight = forbiddenInfo.ProcessedDesc
|
||||
}
|
||||
resourceResponse.HasForbiddenWords = forbiddenInfo.HasForbiddenWords
|
||||
resourceResponse.ForbiddenWords = forbiddenInfo.ForbiddenWords
|
||||
}
|
||||
|
||||
resourceResponses = append(resourceResponses, resourceResponse)
|
||||
}
|
||||
|
||||
// 返回Meilisearch搜索结果(包含高亮信息)
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": resourceResponses,
|
||||
"total": docTotal,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"source": "meilisearch",
|
||||
})
|
||||
return
|
||||
} else {
|
||||
utils.Error("Meilisearch搜索失败,回退到数据库搜索: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果Meilisearch未启用、搜索失败或没有搜索关键词,使用数据库搜索
|
||||
if meilisearchManager == nil || !meilisearchManager.IsEnabled() || len(resources) == 0 {
|
||||
resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": converter.ToResourceResponseList(resources),
|
||||
// 处理违禁词替换和标记
|
||||
var processedResources []entity.Resource
|
||||
if len(cleanWords) > 0 {
|
||||
processedResources = utils.ProcessResourcesForbiddenWords(resources, cleanWords)
|
||||
} else {
|
||||
processedResources = resources
|
||||
}
|
||||
|
||||
// 转换为响应格式并添加违禁词标记
|
||||
var resourceResponses []gin.H
|
||||
for i, processedResource := range processedResources {
|
||||
// 使用原始资源进行检查违禁词(数据库搜索场景,使用普通处理)
|
||||
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, // 使用处理后的描述
|
||||
"pan_id": processedResource.PanID,
|
||||
"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"),
|
||||
}
|
||||
|
||||
// 添加违禁词标记
|
||||
resourceResponse["has_forbidden_words"] = forbiddenInfo.HasForbiddenWords
|
||||
resourceResponse["forbidden_words"] = forbiddenInfo.ForbiddenWords
|
||||
|
||||
resourceResponses = append(resourceResponses, resourceResponse)
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
responseData := gin.H{
|
||||
"data": resourceResponses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
// GetResourceByID 根据ID获取资源
|
||||
@@ -164,6 +268,15 @@ func CreateResource(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到Meilisearch
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
go func() {
|
||||
if err := meilisearchManager.SyncResourceToMeilisearch(resource); err != nil {
|
||||
utils.Error("同步资源到Meilisearch失败: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "资源创建成功",
|
||||
"resource": converter.ToResourceResponse(resource),
|
||||
@@ -240,6 +353,15 @@ func UpdateResource(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到Meilisearch
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
go func() {
|
||||
if err := meilisearchManager.SyncResourceToMeilisearch(resource); err != nil {
|
||||
utils.Error("同步资源到Meilisearch失败: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "资源更新成功"})
|
||||
}
|
||||
|
||||
@@ -271,6 +393,46 @@ func SearchResources(c *gin.Context) {
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
// 如果启用了Meilisearch,优先使用Meilisearch搜索
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
// 构建过滤器
|
||||
filters := make(map[string]interface{})
|
||||
if categoryID := c.Query("category_id"); categoryID != "" {
|
||||
if id, err := strconv.ParseUint(categoryID, 10, 32); err == nil {
|
||||
filters["category"] = uint(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Meilisearch搜索
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
docs, docTotal, err := service.Search(query, 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,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
total = docTotal
|
||||
} else {
|
||||
utils.Error("Meilisearch搜索失败,回退到数据库搜索: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果Meilisearch未启用或搜索失败,使用数据库搜索
|
||||
if meilisearchManager == nil || !meilisearchManager.IsEnabled() || err != nil {
|
||||
if query == "" {
|
||||
// 搜索关键词为空时,返回最新记录(分页)
|
||||
resources, total, err = repoManager.ResourceRepository.FindWithRelationsPaginated(page, pageSize)
|
||||
@@ -278,6 +440,7 @@ func SearchResources(c *gin.Context) {
|
||||
// 有搜索关键词时,执行搜索
|
||||
resources, total, err = repoManager.ResourceRepository.Search(query, nil, page, pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -195,6 +195,17 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
// 刷新系统配置缓存
|
||||
pan.RefreshSystemConfigCache()
|
||||
|
||||
// 重新加载Meilisearch配置(如果Meilisearch配置有变更)
|
||||
if req.MeilisearchEnabled != nil || req.MeilisearchHost != nil || req.MeilisearchPort != nil || req.MeilisearchMasterKey != nil || req.MeilisearchIndexName != nil {
|
||||
if meilisearchManager != nil {
|
||||
if err := meilisearchManager.ReloadConfig(); err != nil {
|
||||
utils.Error("重新加载Meilisearch配置失败: %v", err)
|
||||
} else {
|
||||
utils.Debug("Meilisearch配置重新加载成功")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据配置更新定时任务状态(错误不影响配置保存)
|
||||
scheduler := scheduler.GetGlobalScheduler(
|
||||
repoManager.HotDramaRepository,
|
||||
@@ -311,6 +322,12 @@ func ToggleAutoProcess(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 确保配置缓存已刷新
|
||||
if err := repoManager.SystemConfigRepository.SafeRefreshConfigCache(); err != nil {
|
||||
utils.Error("刷新配置缓存失败: %v", err)
|
||||
// 不返回错误,因为配置已经保存成功
|
||||
}
|
||||
|
||||
// 更新定时任务状态
|
||||
scheduler := scheduler.GetGlobalScheduler(
|
||||
repoManager.HotDramaRepository,
|
||||
|
||||
@@ -50,7 +50,7 @@ func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("创建批量转存任务: %s,资源数量: %d,选择账号数量: %d", req.Title, len(req.Resources), len(req.SelectedAccounts))
|
||||
utils.Debug("创建批量转存任务: %s,资源数量: %d,选择账号数量: %d", req.Title, len(req.Resources), len(req.SelectedAccounts))
|
||||
|
||||
// 构建任务配置
|
||||
taskConfig := map[string]interface{}{
|
||||
@@ -105,7 +105,7 @@ func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
utils.Info("批量转存任务创建完成: %d, 共 %d 项", newTask.ID, len(req.Resources))
|
||||
utils.Debug("批量转存任务创建完成: %d, 共 %d 项", newTask.ID, len(req.Resources))
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"task_id": newTask.ID,
|
||||
@@ -123,8 +123,6 @@ func (h *TaskHandler) StartTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("启动任务: %d", taskID)
|
||||
|
||||
err = h.taskManager.StartTask(uint(taskID))
|
||||
if err != nil {
|
||||
utils.Error("启动任务失败: %v", err)
|
||||
@@ -132,6 +130,8 @@ func (h *TaskHandler) StartTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("启动任务: %d", taskID)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "任务启动成功",
|
||||
})
|
||||
@@ -146,8 +146,6 @@ func (h *TaskHandler) StopTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("停止任务: %d", taskID)
|
||||
|
||||
err = h.taskManager.StopTask(uint(taskID))
|
||||
if err != nil {
|
||||
utils.Error("停止任务失败: %v", err)
|
||||
@@ -155,6 +153,8 @@ func (h *TaskHandler) StopTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("停止任务: %d", taskID)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "任务停止成功",
|
||||
})
|
||||
@@ -169,8 +169,6 @@ func (h *TaskHandler) PauseTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("暂停任务: %d", taskID)
|
||||
|
||||
err = h.taskManager.PauseTask(uint(taskID))
|
||||
if err != nil {
|
||||
utils.Error("暂停任务失败: %v", err)
|
||||
@@ -178,6 +176,8 @@ func (h *TaskHandler) PauseTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("暂停任务: %d", taskID)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "任务暂停成功",
|
||||
})
|
||||
@@ -234,13 +234,25 @@ func (h *TaskHandler) GetTaskStatus(c *gin.Context) {
|
||||
|
||||
// GetTasks 获取任务列表
|
||||
func (h *TaskHandler) GetTasks(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
taskType := c.Query("task_type")
|
||||
// 获取查询参数
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("pageSize", "10")
|
||||
taskType := c.Query("taskType")
|
||||
status := c.Query("status")
|
||||
|
||||
utils.Info("GetTasks: 获取任务列表 page=%d, pageSize=%d, taskType=%s, status=%s", page, pageSize, taskType, status)
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
pageSize, err := strconv.Atoi(pageSizeStr)
|
||||
if err != nil || pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
utils.Debug("GetTasks: 获取任务列表 page=%d, pageSize=%d, taskType=%s, status=%s", page, pageSize, taskType, status)
|
||||
|
||||
// 获取任务列表
|
||||
tasks, total, err := h.repoMgr.TaskRepository.GetList(page, pageSize, taskType, status)
|
||||
if err != nil {
|
||||
utils.Error("获取任务列表失败: %v", err)
|
||||
@@ -248,19 +260,19 @@ func (h *TaskHandler) GetTasks(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("GetTasks: 从数据库获取到 %d 个任务", len(tasks))
|
||||
utils.Debug("GetTasks: 从数据库获取到 %d 个任务", len(tasks))
|
||||
|
||||
// 为每个任务添加运行状态
|
||||
var result []gin.H
|
||||
// 获取任务运行状态
|
||||
var taskList []gin.H
|
||||
for _, task := range tasks {
|
||||
isRunning := h.taskManager.IsTaskRunning(task.ID)
|
||||
utils.Info("GetTasks: 任务 %d (%s) 数据库状态: %s, TaskManager运行状态: %v", task.ID, task.Title, task.Status, isRunning)
|
||||
utils.Debug("GetTasks: 任务 %d (%s) 数据库状态: %s, TaskManager运行状态: %v", task.ID, task.Title, task.Status, isRunning)
|
||||
|
||||
result = append(result, gin.H{
|
||||
taskList = append(taskList, gin.H{
|
||||
"id": task.ID,
|
||||
"title": task.Title,
|
||||
"description": task.Description,
|
||||
"task_type": task.Type,
|
||||
"type": task.Type,
|
||||
"status": task.Status,
|
||||
"total_items": task.TotalItems,
|
||||
"processed_items": task.ProcessedItems,
|
||||
@@ -273,10 +285,11 @@ func (h *TaskHandler) GetTasks(c *gin.Context) {
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"items": result,
|
||||
"tasks": taskList,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": pageSize,
|
||||
"page_size": pageSize,
|
||||
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -348,7 +361,7 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
||||
|
||||
// 检查任务是否在运行
|
||||
if h.taskManager.IsTaskRunning(uint(taskID)) {
|
||||
ErrorResponse(c, "任务正在运行,请先停止任务", http.StatusBadRequest)
|
||||
ErrorResponse(c, "任务正在运行中,无法删除", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -368,7 +381,8 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("任务删除成功: %d", taskID)
|
||||
utils.Debug("任务删除成功: %d", taskID)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "任务删除成功",
|
||||
})
|
||||
|
||||
69
main.go
69
main.go
@@ -1,14 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ctwj/urldb/db"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/handlers"
|
||||
"github.com/ctwj/urldb/middleware"
|
||||
"github.com/ctwj/urldb/scheduler"
|
||||
"github.com/ctwj/urldb/services"
|
||||
"github.com/ctwj/urldb/task"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
@@ -18,6 +22,18 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 检查命令行参数
|
||||
if len(os.Args) > 1 && os.Args[1] == "version" {
|
||||
versionInfo := utils.GetVersionInfo()
|
||||
fmt.Printf("版本: v%s\n", versionInfo.Version)
|
||||
fmt.Printf("构建时间: %s\n", versionInfo.BuildTime.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf("Git提交: %s\n", versionInfo.GitCommit)
|
||||
fmt.Printf("Git分支: %s\n", versionInfo.GitBranch)
|
||||
fmt.Printf("Go版本: %s\n", versionInfo.GoVersion)
|
||||
fmt.Printf("平台: %s/%s\n", versionInfo.Platform, versionInfo.Arch)
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化日志系统
|
||||
if err := utils.InitLogger(nil); err != nil {
|
||||
log.Fatal("初始化日志系统失败:", err)
|
||||
@@ -76,6 +92,12 @@ func main() {
|
||||
transferProcessor := task.NewTransferProcessor(repoManager)
|
||||
taskManager.RegisterProcessor(transferProcessor)
|
||||
|
||||
// 初始化Meilisearch管理器
|
||||
meilisearchManager := services.NewMeilisearchManager(repoManager)
|
||||
if err := meilisearchManager.Initialize(); err != nil {
|
||||
utils.Error("初始化Meilisearch管理器失败: %v", err)
|
||||
}
|
||||
|
||||
// 恢复运行中的任务(服务器重启后)
|
||||
if err := taskManager.RecoverRunningTasks(); err != nil {
|
||||
utils.Error("恢复运行中任务失败: %v", err)
|
||||
@@ -98,6 +120,37 @@ func main() {
|
||||
// 将Repository管理器注入到handlers中
|
||||
handlers.SetRepositoryManager(repoManager)
|
||||
|
||||
// 设置Meilisearch管理器到handlers中
|
||||
handlers.SetMeilisearchManager(meilisearchManager)
|
||||
|
||||
// 设置全局调度器的Meilisearch管理器
|
||||
scheduler.SetGlobalMeilisearchManager(meilisearchManager)
|
||||
|
||||
// 初始化并启动调度器
|
||||
globalScheduler := scheduler.GetGlobalScheduler(
|
||||
repoManager.HotDramaRepository,
|
||||
repoManager.ReadyResourceRepository,
|
||||
repoManager.ResourceRepository,
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
|
||||
// 根据系统配置启动相应的调度任务
|
||||
autoFetchHotDrama, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoFetchHotDramaEnabled)
|
||||
autoProcessReadyResources, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoProcessReadyResources)
|
||||
autoTransferEnabled, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
|
||||
|
||||
globalScheduler.UpdateSchedulerStatusWithAutoTransfer(
|
||||
autoFetchHotDrama,
|
||||
autoProcessReadyResources,
|
||||
autoTransferEnabled,
|
||||
)
|
||||
|
||||
utils.Info("调度器初始化完成")
|
||||
|
||||
// 设置公开API中间件的Repository管理器
|
||||
middleware.SetRepositoryManager(repoManager)
|
||||
|
||||
@@ -110,6 +163,9 @@ func main() {
|
||||
// 创建文件处理器
|
||||
fileHandler := handlers.NewFileHandler(repoManager.FileRepository, repoManager.SystemConfigRepository, repoManager.UserRepository)
|
||||
|
||||
// 创建Meilisearch处理器
|
||||
meilisearchHandler := handlers.NewMeilisearchHandler(meilisearchManager)
|
||||
|
||||
// API路由
|
||||
api := r.Group("/api")
|
||||
{
|
||||
@@ -242,6 +298,19 @@ func main() {
|
||||
api.GET("/version/full", handlers.GetFullVersionInfo)
|
||||
api.GET("/version/check-update", handlers.CheckUpdate)
|
||||
|
||||
// Meilisearch管理路由
|
||||
api.GET("/meilisearch/status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetStatus)
|
||||
api.GET("/meilisearch/unsynced-count", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetUnsyncedCount)
|
||||
api.GET("/meilisearch/unsynced", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetUnsyncedResources)
|
||||
api.GET("/meilisearch/synced", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetSyncedResources)
|
||||
api.GET("/meilisearch/resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetAllResources)
|
||||
api.POST("/meilisearch/sync-all", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.SyncAllResources)
|
||||
api.GET("/meilisearch/sync-progress", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetSyncProgress)
|
||||
api.POST("/meilisearch/stop-sync", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.StopSync)
|
||||
api.POST("/meilisearch/clear-index", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.ClearIndex)
|
||||
api.POST("/meilisearch/test-connection", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.TestConnection)
|
||||
api.POST("/meilisearch/update-settings", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.UpdateIndexSettings)
|
||||
|
||||
// 文件上传相关路由
|
||||
api.POST("/files/upload", middleware.AuthMiddleware(), fileHandler.UploadFile)
|
||||
api.GET("/files", middleware.AuthMiddleware(), fileHandler.GetFileList)
|
||||
|
||||
@@ -27,8 +27,8 @@ type Claims struct {
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
utils.Info("AuthMiddleware - 收到请求: %s %s", c.Request.Method, c.Request.URL.Path)
|
||||
utils.Info("AuthMiddleware - Authorization头: %s", authHeader)
|
||||
// utils.Info("AuthMiddleware - 收到请求: %s %s", c.Request.Method, c.Request.URL.Path)
|
||||
// utils.Info("AuthMiddleware - Authorization头: %s", authHeader)
|
||||
|
||||
if authHeader == "" {
|
||||
utils.Error("AuthMiddleware - 未提供认证令牌")
|
||||
@@ -39,24 +39,24 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
utils.Error("AuthMiddleware - 无效的认证格式: %s", authHeader)
|
||||
// utils.Error("AuthMiddleware - 无效的认证格式: %s", authHeader)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证格式"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
utils.Info("AuthMiddleware - 解析令牌: %s", tokenString[:10]+"...")
|
||||
// utils.Info("AuthMiddleware - 解析令牌: %s", tokenString[:10]+"...")
|
||||
|
||||
claims, err := parseToken(tokenString)
|
||||
if err != nil {
|
||||
utils.Error("AuthMiddleware - 令牌解析失败: %v", err)
|
||||
// utils.Error("AuthMiddleware - 令牌解析失败: %v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的令牌"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("AuthMiddleware - 令牌验证成功,用户: %s, 角色: %s", claims.Username, claims.Role)
|
||||
// utils.Info("AuthMiddleware - 令牌验证成功,用户: %s, 角色: %s", claims.Username, claims.Role)
|
||||
|
||||
// 将用户信息存储到上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
@@ -72,13 +72,13 @@ func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
|
||||
// c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
// c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -106,23 +106,23 @@ func GenerateToken(user *entity.User) (string, error) {
|
||||
|
||||
// parseToken 解析JWT令牌
|
||||
func parseToken(tokenString string) (*Claims, error) {
|
||||
utils.Info("parseToken - 开始解析令牌")
|
||||
// utils.Info("parseToken - 开始解析令牌")
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
utils.Error("parseToken - JWT解析失败: %v", err)
|
||||
// utils.Error("parseToken - JWT解析失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
utils.Info("parseToken - 令牌解析成功,用户ID: %d", claims.UserID)
|
||||
// utils.Info("parseToken - 令牌解析成功,用户ID: %d", claims.UserID)
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
utils.Error("parseToken - 令牌无效或签名错误")
|
||||
// utils.Error("parseToken - 令牌无效或签名错误")
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
-- 添加文件哈希字段
|
||||
ALTER TABLE files ADD COLUMN file_hash VARCHAR(64) COMMENT '文件哈希值';
|
||||
CREATE UNIQUE INDEX idx_files_hash ON files(file_hash);
|
||||
|
||||
-- 添加同步状态字段
|
||||
ALTER TABLE resources ADD COLUMN synced_to_meilisearch BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE resources ADD COLUMN synced_at TIMESTAMP NULL;
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX idx_resources_synced ON resources(synced_to_meilisearch, synced_at);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON COLUMN resources.synced_to_meilisearch IS '是否已同步到Meilisearch';
|
||||
COMMENT ON COLUMN resources.synced_at IS '同步时间';
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/services"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
@@ -16,8 +17,20 @@ type GlobalScheduler struct {
|
||||
var (
|
||||
globalScheduler *GlobalScheduler
|
||||
once sync.Once
|
||||
// 全局Meilisearch管理器
|
||||
globalMeilisearchManager *services.MeilisearchManager
|
||||
)
|
||||
|
||||
// SetGlobalMeilisearchManager 设置全局Meilisearch管理器
|
||||
func SetGlobalMeilisearchManager(manager *services.MeilisearchManager) {
|
||||
globalMeilisearchManager = manager
|
||||
}
|
||||
|
||||
// GetGlobalMeilisearchManager 获取全局Meilisearch管理器
|
||||
func GetGlobalMeilisearchManager() *services.MeilisearchManager {
|
||||
return globalMeilisearchManager
|
||||
}
|
||||
|
||||
// GetGlobalScheduler 获取全局调度器实例(单例模式)
|
||||
func GetGlobalScheduler(hotDramaRepo repo.HotDramaRepository, readyResourceRepo repo.ReadyResourceRepository, resourceRepo repo.ResourceRepository, systemConfigRepo repo.SystemConfigRepository, panRepo repo.PanRepository, cksRepo repo.CksRepository, tagRepo repo.TagRepository, categoryRepo repo.CategoryRepository) *GlobalScheduler {
|
||||
once.Do(func() {
|
||||
@@ -34,12 +47,12 @@ func (gs *GlobalScheduler) StartHotDramaScheduler() {
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if gs.manager.IsHotDramaRunning() {
|
||||
utils.Info("热播剧定时任务已在运行中")
|
||||
utils.Debug("热播剧定时任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StartHotDramaScheduler()
|
||||
utils.Info("全局调度器已启动热播剧定时任务")
|
||||
utils.Debug("全局调度器已启动热播剧定时任务")
|
||||
}
|
||||
|
||||
// StopHotDramaScheduler 停止热播剧定时任务
|
||||
@@ -48,12 +61,12 @@ func (gs *GlobalScheduler) StopHotDramaScheduler() {
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if !gs.manager.IsHotDramaRunning() {
|
||||
utils.Info("热播剧定时任务未在运行")
|
||||
utils.Debug("热播剧定时任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StopHotDramaScheduler()
|
||||
utils.Info("全局调度器已停止热播剧定时任务")
|
||||
utils.Debug("全局调度器已停止热播剧定时任务")
|
||||
}
|
||||
|
||||
// IsHotDramaSchedulerRunning 检查热播剧定时任务是否在运行
|
||||
@@ -74,12 +87,12 @@ func (gs *GlobalScheduler) StartReadyResourceScheduler() {
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if gs.manager.IsReadyResourceRunning() {
|
||||
utils.Info("待处理资源自动处理任务已在运行中")
|
||||
utils.Debug("待处理资源自动处理任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StartReadyResourceScheduler()
|
||||
utils.Info("全局调度器已启动待处理资源自动处理任务")
|
||||
utils.Debug("全局调度器已启动待处理资源自动处理任务")
|
||||
}
|
||||
|
||||
// StopReadyResourceScheduler 停止待处理资源自动处理任务
|
||||
@@ -88,12 +101,12 @@ func (gs *GlobalScheduler) StopReadyResourceScheduler() {
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if !gs.manager.IsReadyResourceRunning() {
|
||||
utils.Info("待处理资源自动处理任务未在运行")
|
||||
utils.Debug("待处理资源自动处理任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
gs.manager.StopReadyResourceScheduler()
|
||||
utils.Info("全局调度器已停止待处理资源自动处理任务")
|
||||
utils.Debug("全局调度器已停止待处理资源自动处理任务")
|
||||
}
|
||||
|
||||
// IsReadyResourceRunning 检查待处理资源自动处理任务是否在运行
|
||||
|
||||
@@ -48,28 +48,28 @@ func NewManager(
|
||||
|
||||
// StartAll 启动所有调度任务
|
||||
func (m *Manager) StartAll() {
|
||||
utils.Info("启动所有调度任务")
|
||||
utils.Debug("启动所有调度任务")
|
||||
|
||||
// 启动热播剧调度任务
|
||||
m.hotDramaScheduler.Start()
|
||||
// 启动热播剧定时任务
|
||||
m.StartHotDramaScheduler()
|
||||
|
||||
// 启动待处理资源调度任务
|
||||
m.readyResourceScheduler.Start()
|
||||
|
||||
utils.Info("所有调度任务已启动")
|
||||
utils.Debug("所有调度任务已启动")
|
||||
}
|
||||
|
||||
// StopAll 停止所有调度任务
|
||||
func (m *Manager) StopAll() {
|
||||
utils.Info("停止所有调度任务")
|
||||
utils.Debug("停止所有调度任务")
|
||||
|
||||
// 停止热播剧调度任务
|
||||
m.hotDramaScheduler.Stop()
|
||||
// 停止热播剧定时任务
|
||||
m.StopHotDramaScheduler()
|
||||
|
||||
// 停止待处理资源调度任务
|
||||
m.readyResourceScheduler.Stop()
|
||||
|
||||
utils.Info("所有调度任务已停止")
|
||||
utils.Debug("所有调度任务已停止")
|
||||
}
|
||||
|
||||
// StartHotDramaScheduler 启动热播剧调度任务
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewReadyResourceScheduler(base *BaseScheduler) *ReadyResourceScheduler {
|
||||
// Start 启动待处理资源定时任务
|
||||
func (r *ReadyResourceScheduler) Start() {
|
||||
if r.readyResourceRunning {
|
||||
utils.Info("待处理资源自动处理任务已在运行中")
|
||||
utils.Debug("待处理资源自动处理任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (r *ReadyResourceScheduler) Start() {
|
||||
r.processReadyResources()
|
||||
}()
|
||||
} else {
|
||||
utils.Info("上一次待处理资源任务还在执行中,跳过本次执行")
|
||||
utils.Debug("上一次待处理资源任务还在执行中,跳过本次执行")
|
||||
}
|
||||
case <-r.GetStopChan():
|
||||
utils.Info("停止待处理资源自动处理任务")
|
||||
@@ -76,7 +76,7 @@ func (r *ReadyResourceScheduler) Start() {
|
||||
// Stop 停止待处理资源定时任务
|
||||
func (r *ReadyResourceScheduler) Stop() {
|
||||
if !r.readyResourceRunning {
|
||||
utils.Info("待处理资源自动处理任务未在运行")
|
||||
utils.Debug("待处理资源自动处理任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (r *ReadyResourceScheduler) IsReadyResourceRunning() bool {
|
||||
|
||||
// processReadyResources 处理待处理资源
|
||||
func (r *ReadyResourceScheduler) processReadyResources() {
|
||||
utils.Info("开始处理待处理资源...")
|
||||
utils.Debug("开始处理待处理资源...")
|
||||
|
||||
// 检查系统配置,确认是否启用自动处理
|
||||
autoProcess, err := r.systemConfigRepo.GetConfigBool(entity.ConfigKeyAutoProcessReadyResources)
|
||||
@@ -102,7 +102,7 @@ func (r *ReadyResourceScheduler) processReadyResources() {
|
||||
}
|
||||
|
||||
if !autoProcess {
|
||||
utils.Info("自动处理待处理资源功能已禁用")
|
||||
utils.Debug("自动处理待处理资源功能已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,11 +115,11 @@ func (r *ReadyResourceScheduler) processReadyResources() {
|
||||
}
|
||||
|
||||
if len(readyResources) == 0 {
|
||||
utils.Info("没有待处理的资源")
|
||||
utils.Debug("没有待处理的资源")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info(fmt.Sprintf("找到 %d 个待处理资源,开始处理...", len(readyResources)))
|
||||
utils.Debug(fmt.Sprintf("找到 %d 个待处理资源,开始处理...", len(readyResources)))
|
||||
|
||||
processedCount := 0
|
||||
factory := panutils.GetInstance() // 使用单例模式
|
||||
@@ -132,7 +132,7 @@ func (r *ReadyResourceScheduler) processReadyResources() {
|
||||
continue
|
||||
}
|
||||
if exits {
|
||||
utils.Info(fmt.Sprintf("资源已存在: %s", readyResource.URL))
|
||||
utils.Debug(fmt.Sprintf("资源已存在: %s", readyResource.URL))
|
||||
r.readyResourceRepo.Delete(readyResource.ID)
|
||||
continue
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (r *ReadyResourceScheduler) processReadyResources() {
|
||||
if updateErr := r.readyResourceRepo.Update(&readyResource); updateErr != nil {
|
||||
utils.Error(fmt.Sprintf("更新错误信息失败 (ID: %d): %v", readyResource.ID, updateErr))
|
||||
} else {
|
||||
utils.Info(fmt.Sprintf("已保存错误信息到资源 (ID: %d): %s", readyResource.ID, err.Error()))
|
||||
utils.Debug(fmt.Sprintf("已保存错误信息到资源 (ID: %d): %s", readyResource.ID, err.Error()))
|
||||
}
|
||||
|
||||
// 处理失败后删除资源,避免重复处理
|
||||
@@ -155,11 +155,13 @@ func (r *ReadyResourceScheduler) processReadyResources() {
|
||||
// 处理成功,删除readyResource
|
||||
r.readyResourceRepo.Delete(readyResource.ID)
|
||||
processedCount++
|
||||
utils.Info(fmt.Sprintf("成功处理资源: %s", readyResource.URL))
|
||||
utils.Debug(fmt.Sprintf("成功处理资源: %s", readyResource.URL))
|
||||
}
|
||||
}
|
||||
|
||||
if processedCount > 0 {
|
||||
utils.Info(fmt.Sprintf("待处理资源处理完成,共处理 %d 个资源", processedCount))
|
||||
}
|
||||
}
|
||||
|
||||
// convertReadyResourceToResource 将待处理资源转换为正式资源
|
||||
@@ -187,28 +189,28 @@ func (r *ReadyResourceScheduler) convertReadyResourceToResource(readyResource en
|
||||
}
|
||||
|
||||
// 检查违禁词
|
||||
forbiddenWords, err := r.systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||||
if err == nil && forbiddenWords != "" {
|
||||
words := strings.Split(forbiddenWords, ",")
|
||||
var matchedWords []string
|
||||
title := strings.ToLower(resource.Title)
|
||||
description := strings.ToLower(resource.Description)
|
||||
// forbiddenWords, err := r.systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||||
// if err == nil && forbiddenWords != "" {
|
||||
// words := strings.Split(forbiddenWords, ",")
|
||||
// var matchedWords []string
|
||||
// title := strings.ToLower(resource.Title)
|
||||
// description := strings.ToLower(resource.Description)
|
||||
|
||||
for _, word := range words {
|
||||
word = strings.TrimSpace(word)
|
||||
if word != "" {
|
||||
wordLower := strings.ToLower(word)
|
||||
if strings.Contains(title, wordLower) || strings.Contains(description, wordLower) {
|
||||
matchedWords = append(matchedWords, word)
|
||||
}
|
||||
}
|
||||
}
|
||||
// for _, word := range words {
|
||||
// word = strings.TrimSpace(word)
|
||||
// if word != "" {
|
||||
// wordLower := strings.ToLower(word)
|
||||
// if strings.Contains(title, wordLower) || strings.Contains(description, wordLower) {
|
||||
// matchedWords = append(matchedWords, word)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if len(matchedWords) > 0 {
|
||||
utils.Warn(fmt.Sprintf("资源包含违禁词: %s, 违禁词: %s", resource.Title, strings.Join(matchedWords, ", ")))
|
||||
return fmt.Errorf("存在违禁词: %s", strings.Join(matchedWords, ", "))
|
||||
}
|
||||
}
|
||||
// if len(matchedWords) > 0 {
|
||||
// utils.Warn(fmt.Sprintf("资源包含违禁词: %s, 违禁词: %s", resource.Title, strings.Join(matchedWords, ", ")))
|
||||
// return fmt.Errorf("存在违禁词: %s", strings.Join(matchedWords, ", "))
|
||||
// }
|
||||
// }
|
||||
|
||||
// 不是夸克,直接保存
|
||||
if serviceType != panutils.Quark {
|
||||
@@ -342,6 +344,31 @@ func (r *ReadyResourceScheduler) convertReadyResourceToResource(readyResource en
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到Meilisearch
|
||||
utils.Debug(fmt.Sprintf("准备同步资源到Meilisearch - 资源ID: %d, URL: %s", resource.ID, resource.URL))
|
||||
utils.Debug(fmt.Sprintf("globalMeilisearchManager: %v", globalMeilisearchManager != nil))
|
||||
|
||||
if globalMeilisearchManager != nil {
|
||||
utils.Debug(fmt.Sprintf("Meilisearch管理器已初始化,检查启用状态"))
|
||||
isEnabled := globalMeilisearchManager.IsEnabled()
|
||||
utils.Debug(fmt.Sprintf("Meilisearch启用状态: %v", isEnabled))
|
||||
|
||||
if isEnabled {
|
||||
utils.Debug(fmt.Sprintf("Meilisearch已启用,开始同步资源"))
|
||||
go func() {
|
||||
if err := globalMeilisearchManager.SyncResourceToMeilisearch(resource); err != nil {
|
||||
utils.Error("同步资源到Meilisearch失败: %v", err)
|
||||
} else {
|
||||
utils.Info(fmt.Sprintf("资源已同步到Meilisearch: %s", resource.URL))
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
utils.Debug("Meilisearch未启用,跳过同步")
|
||||
}
|
||||
} else {
|
||||
utils.Debug("Meilisearch管理器未初始化,跳过同步")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
178
scripts/build.sh
Normal file
178
scripts/build.sh
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 编译脚本 - 自动注入版本信息
|
||||
# 用法: ./scripts/build.sh [target]
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 获取当前版本
|
||||
get_current_version() {
|
||||
cat VERSION
|
||||
}
|
||||
|
||||
# 获取Git信息
|
||||
get_git_commit() {
|
||||
git rev-parse --short HEAD 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
get_git_branch() {
|
||||
git branch --show-current 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
# 获取构建时间
|
||||
get_build_time() {
|
||||
date '+%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
|
||||
# 编译函数
|
||||
build() {
|
||||
local target=${1:-"main"}
|
||||
local version=$(get_current_version)
|
||||
local git_commit=$(get_git_commit)
|
||||
local git_branch=$(get_git_branch)
|
||||
local build_time=$(get_build_time)
|
||||
|
||||
echo -e "${BLUE}开始编译...${NC}"
|
||||
echo -e "版本: ${GREEN}${version}${NC}"
|
||||
echo -e "Git提交: ${GREEN}${git_commit}${NC}"
|
||||
echo -e "Git分支: ${GREEN}${git_branch}${NC}"
|
||||
echo -e "构建时间: ${GREEN}${build_time}${NC}"
|
||||
|
||||
# 构建 ldflags
|
||||
local ldflags="-X 'github.com/ctwj/urldb/utils.Version=${version}'"
|
||||
ldflags="${ldflags} -X 'github.com/ctwj/urldb/utils.BuildTime=${build_time}'"
|
||||
ldflags="${ldflags} -X 'github.com/ctwj/urldb/utils.GitCommit=${git_commit}'"
|
||||
ldflags="${ldflags} -X 'github.com/ctwj/urldb/utils.GitBranch=${git_branch}'"
|
||||
|
||||
# 编译 - 使用跨平台编译设置
|
||||
echo -e "${YELLOW}编译中...${NC}"
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "${ldflags}" -o "${target}" .
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}编译成功!${NC}"
|
||||
echo -e "可执行文件: ${GREEN}${target}${NC}"
|
||||
echo -e "目标平台: ${GREEN}Linux${NC}"
|
||||
|
||||
# 显示版本信息(在Linux环境下)
|
||||
echo -e "${BLUE}版本信息验证:${NC}"
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
./${target} version 2>/dev/null || echo "无法验证版本信息"
|
||||
else
|
||||
echo "当前非Linux环境,无法直接验证版本信息"
|
||||
echo "请将编译后的文件复制到Linux服务器上验证"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}编译失败!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理函数
|
||||
clean() {
|
||||
echo -e "${YELLOW}清理编译文件...${NC}"
|
||||
rm -f main
|
||||
echo -e "${GREEN}清理完成${NC}"
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
show_help() {
|
||||
echo -e "${BLUE}编译脚本${NC}"
|
||||
echo ""
|
||||
echo "用法: $0 [命令]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " build [target] 编译程序 (当前平台)"
|
||||
echo " build-linux [target] 编译Linux版本 (推荐)"
|
||||
echo " clean 清理编译文件"
|
||||
echo " help 显示此帮助信息"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 # 编译Linux版本 (默认)"
|
||||
echo " $0 build-linux # 编译Linux版本"
|
||||
echo " $0 build-linux app # 编译Linux版本为 app"
|
||||
echo " $0 build # 编译当前平台版本"
|
||||
echo " $0 clean # 清理编译文件"
|
||||
echo ""
|
||||
echo "注意:"
|
||||
echo " - Linux版本使用静态链接,适合部署到服务器"
|
||||
echo " - 默认编译Linux版本,无需复制VERSION文件"
|
||||
}
|
||||
|
||||
# Linux编译函数
|
||||
build_linux() {
|
||||
local target=${1:-"main"}
|
||||
local version=$(get_current_version)
|
||||
local git_commit=$(get_git_commit)
|
||||
local git_branch=$(get_git_branch)
|
||||
local build_time=$(get_build_time)
|
||||
|
||||
echo -e "${BLUE}开始Linux编译...${NC}"
|
||||
echo -e "版本: ${GREEN}${version}${NC}"
|
||||
echo -e "Git提交: ${GREEN}${git_commit}${NC}"
|
||||
echo -e "Git分支: ${GREEN}${git_branch}${NC}"
|
||||
echo -e "构建时间: ${GREEN}${build_time}${NC}"
|
||||
|
||||
# 构建 ldflags
|
||||
local ldflags="-X 'github.com/ctwj/urldb/utils.Version=${version}'"
|
||||
ldflags="${ldflags} -X 'github.com/ctwj/urldb/utils.BuildTime=${build_time}'"
|
||||
ldflags="${ldflags} -X 'github.com/ctwj/urldb/utils.GitCommit=${git_commit}'"
|
||||
ldflags="${ldflags} -X 'github.com/ctwj/urldb/utils.GitBranch=${git_branch}'"
|
||||
|
||||
# Linux编译
|
||||
echo -e "${YELLOW}编译中...${NC}"
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "${ldflags}" -o "${target}" .
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}Linux编译成功!${NC}"
|
||||
echo -e "可执行文件: ${GREEN}${target}${NC}"
|
||||
echo -e "目标平台: ${GREEN}Linux${NC}"
|
||||
echo -e "静态链接: ${GREEN}是${NC}"
|
||||
|
||||
# 显示文件信息
|
||||
if command -v file >/dev/null 2>&1; then
|
||||
echo -e "${BLUE}文件信息:${NC}"
|
||||
file "${target}"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}注意: 请在Linux服务器上验证版本信息${NC}"
|
||||
else
|
||||
echo -e "${RED}Linux编译失败!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
case $1 in
|
||||
"build")
|
||||
build $2
|
||||
;;
|
||||
"build-linux")
|
||||
build_linux $2
|
||||
;;
|
||||
"clean")
|
||||
clean
|
||||
;;
|
||||
"help"|"-h"|"--help")
|
||||
show_help
|
||||
;;
|
||||
"")
|
||||
build_linux
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}错误: 未知命令 '$1'${NC}"
|
||||
echo "使用 '$0 help' 查看帮助信息"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main "$@"
|
||||
183
scripts/docker-build.sh
Normal file
183
scripts/docker-build.sh
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker构建脚本
|
||||
# 用法: ./scripts/docker-build.sh [version]
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 获取版本号
|
||||
get_version() {
|
||||
if [ -n "$1" ]; then
|
||||
echo "$1"
|
||||
else
|
||||
cat VERSION
|
||||
fi
|
||||
}
|
||||
|
||||
# 获取Git信息
|
||||
get_git_commit() {
|
||||
git rev-parse --short HEAD 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
get_git_branch() {
|
||||
git branch --show-current 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
# 构建Docker镜像
|
||||
build_docker() {
|
||||
local version=$(get_version $1)
|
||||
local skip_frontend=$2
|
||||
local git_commit=$(get_git_commit)
|
||||
local git_branch=$(get_git_branch)
|
||||
local build_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo -e "${BLUE}开始Docker构建...${NC}"
|
||||
echo -e "版本: ${GREEN}${version}${NC}"
|
||||
echo -e "Git提交: ${GREEN}${git_commit}${NC}"
|
||||
echo -e "Git分支: ${GREEN}${git_branch}${NC}"
|
||||
echo -e "构建时间: ${GREEN}${build_time}${NC}"
|
||||
if [ "$skip_frontend" = "true" ]; then
|
||||
echo -e "跳过前端构建: ${GREEN}是${NC}"
|
||||
fi
|
||||
|
||||
# 直接使用 docker build,避免 buildx 的复杂性
|
||||
BUILD_CMD="docker build"
|
||||
echo -e "${BLUE}使用构建命令: ${BUILD_CMD}${NC}"
|
||||
|
||||
# 构建前端镜像(可选)
|
||||
if [ "$skip_frontend" != "true" ]; then
|
||||
echo -e "${YELLOW}构建前端镜像...${NC}"
|
||||
FRONTEND_CMD="${BUILD_CMD} --build-arg VERSION=${version} --build-arg GIT_COMMIT=${git_commit} --build-arg GIT_BRANCH=${git_branch} --build-arg BUILD_TIME=${build_time} --target frontend -t ctwj/urldb-frontend:${version} ."
|
||||
echo -e "${BLUE}执行命令: ${FRONTEND_CMD}${NC}"
|
||||
${BUILD_CMD} \
|
||||
--build-arg VERSION=${version} \
|
||||
--build-arg GIT_COMMIT=${git_commit} \
|
||||
--build-arg GIT_BRANCH=${git_branch} \
|
||||
--build-arg "BUILD_TIME=${build_time}" \
|
||||
--target frontend \
|
||||
-t ctwj/urldb-frontend:${version} \
|
||||
.
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}前端构建失败!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}跳过前端构建${NC}"
|
||||
fi
|
||||
|
||||
# 构建后端镜像
|
||||
echo -e "${YELLOW}构建后端镜像...${NC}"
|
||||
BACKEND_CMD="${BUILD_CMD} --build-arg VERSION=${version} --build-arg GIT_COMMIT=${git_commit} --build-arg GIT_BRANCH=${git_branch} --build-arg BUILD_TIME=${build_time} --target backend -t ctwj/urldb-backend:${version} ."
|
||||
echo -e "${BLUE}执行命令: ${BACKEND_CMD}${NC}"
|
||||
${BUILD_CMD} \
|
||||
--build-arg VERSION=${version} \
|
||||
--build-arg GIT_COMMIT=${git_commit} \
|
||||
--build-arg GIT_BRANCH=${git_branch} \
|
||||
--build-arg BUILD_TIME="${build_time}" \
|
||||
--target backend \
|
||||
-t ctwj/urldb-backend:${version} \
|
||||
.
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}后端构建失败!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Docker构建完成!${NC}"
|
||||
echo -e "镜像标签:"
|
||||
echo -e " ${GREEN}ctwj/urldb-backend:${version}${NC}"
|
||||
if [ "$skip_frontend" != "true" ]; then
|
||||
echo -e " ${GREEN}ctwj/urldb-frontend:${version}${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 推送镜像
|
||||
push_images() {
|
||||
local version=$(get_version $1)
|
||||
|
||||
echo -e "${YELLOW}推送镜像到Docker Hub...${NC}"
|
||||
|
||||
# 推送后端镜像
|
||||
docker push ctwj/urldb-backend:${version}
|
||||
|
||||
# 推送前端镜像
|
||||
docker push ctwj/urldb-frontend:${version}
|
||||
|
||||
echo -e "${GREEN}镜像推送完成!${NC}"
|
||||
}
|
||||
|
||||
# 清理镜像
|
||||
clean_images() {
|
||||
local version=$(get_version $1)
|
||||
|
||||
echo -e "${YELLOW}清理Docker镜像...${NC}"
|
||||
docker rmi ctwj/urldb-backend:${version} 2>/dev/null || true
|
||||
docker rmi ctwj/urldb-frontend:${version} 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}镜像清理完成${NC}"
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
show_help() {
|
||||
echo -e "${BLUE}Docker构建脚本${NC}"
|
||||
echo ""
|
||||
echo "用法: $0 [命令] [版本] [选项]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " build [version] [--skip-frontend] 构建Docker镜像"
|
||||
echo " push [version] 推送镜像到Docker Hub"
|
||||
echo " clean [version] 清理Docker镜像"
|
||||
echo " help 显示此帮助信息"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " --skip-frontend 跳过前端构建"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 build # 构建当前版本镜像"
|
||||
echo " $0 build 1.2.4 # 构建指定版本镜像"
|
||||
echo " $0 build 1.2.4 --skip-frontend # 构建指定版本镜像,跳过前端"
|
||||
echo " $0 push 1.2.4 # 推送指定版本镜像"
|
||||
echo " $0 clean # 清理当前版本镜像"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
case $1 in
|
||||
"build")
|
||||
# 检查是否有 --skip-frontend 选项
|
||||
local skip_frontend="false"
|
||||
if [ "$3" = "--skip-frontend" ]; then
|
||||
skip_frontend="true"
|
||||
fi
|
||||
build_docker $2 $skip_frontend
|
||||
;;
|
||||
"push")
|
||||
push_images $2
|
||||
;;
|
||||
"clean")
|
||||
clean_images $2
|
||||
;;
|
||||
"help"|"-h"|"--help")
|
||||
show_help
|
||||
;;
|
||||
"")
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}错误: 未知命令 '$1'${NC}"
|
||||
echo "使用 '$0 help' 查看帮助信息"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main "$@"
|
||||
777
services/meilisearch_manager.go
Normal file
777
services/meilisearch_manager.go
Normal file
@@ -0,0 +1,777 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// MeilisearchManager Meilisearch管理器
|
||||
type MeilisearchManager struct {
|
||||
service *MeilisearchService
|
||||
repoMgr *repo.RepositoryManager
|
||||
configRepo repo.SystemConfigRepository
|
||||
mutex sync.RWMutex
|
||||
status MeilisearchStatus
|
||||
stopChan chan struct{}
|
||||
isRunning bool
|
||||
|
||||
// 同步进度控制
|
||||
syncMutex sync.RWMutex
|
||||
syncProgress SyncProgress
|
||||
isSyncing bool
|
||||
syncStopChan chan struct{}
|
||||
}
|
||||
|
||||
// SyncProgress 同步进度
|
||||
type SyncProgress struct {
|
||||
IsRunning bool `json:"is_running"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ProcessedCount int64 `json:"processed_count"`
|
||||
SyncedCount int64 `json:"synced_count"`
|
||||
FailedCount int64 `json:"failed_count"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EstimatedTime string `json:"estimated_time"`
|
||||
CurrentBatch int `json:"current_batch"`
|
||||
TotalBatches int `json:"total_batches"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
}
|
||||
|
||||
// MeilisearchStatus Meilisearch状态
|
||||
type MeilisearchStatus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Healthy bool `json:"healthy"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
ErrorCount int `json:"error_count"`
|
||||
LastError string `json:"last_error"`
|
||||
DocumentCount int64 `json:"document_count"`
|
||||
}
|
||||
|
||||
// NewMeilisearchManager 创建Meilisearch管理器
|
||||
func NewMeilisearchManager(repoMgr *repo.RepositoryManager) *MeilisearchManager {
|
||||
return &MeilisearchManager{
|
||||
repoMgr: repoMgr,
|
||||
stopChan: make(chan struct{}),
|
||||
syncStopChan: make(chan struct{}),
|
||||
status: MeilisearchStatus{
|
||||
Enabled: false,
|
||||
Healthy: false,
|
||||
LastCheck: time.Now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化Meilisearch服务
|
||||
func (m *MeilisearchManager) Initialize() error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// 设置configRepo
|
||||
m.configRepo = m.repoMgr.SystemConfigRepository
|
||||
|
||||
// 获取配置
|
||||
enabled, err := m.configRepo.GetConfigBool(entity.ConfigKeyMeilisearchEnabled)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch启用状态失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
utils.Debug("Meilisearch未启用,清理服务状态")
|
||||
m.status.Enabled = false
|
||||
m.service = nil
|
||||
// 停止监控循环
|
||||
if m.stopChan != nil {
|
||||
close(m.stopChan)
|
||||
m.stopChan = make(chan struct{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
host, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchHost)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch主机配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
port, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchPort)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch端口配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
masterKey, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchMasterKey)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch主密钥配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
indexName, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchIndexName)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch索引名配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
m.service = NewMeilisearchService(host, port, masterKey, indexName, enabled)
|
||||
m.status.Enabled = enabled
|
||||
|
||||
// 如果启用,创建索引并更新设置
|
||||
if enabled {
|
||||
utils.Debug("Meilisearch已启用,创建索引并更新设置")
|
||||
|
||||
// 创建索引
|
||||
if err := m.service.CreateIndex(); err != nil {
|
||||
utils.Error("创建Meilisearch索引失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
if err := m.service.UpdateIndexSettings(); err != nil {
|
||||
utils.Error("更新Meilisearch索引设置失败: %v", err)
|
||||
}
|
||||
|
||||
// 立即进行一次健康检查
|
||||
go func() {
|
||||
m.checkHealth()
|
||||
// 启动监控
|
||||
go m.monitorLoop()
|
||||
}()
|
||||
} else {
|
||||
utils.Debug("Meilisearch未启用")
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch服务初始化完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled 检查是否启用
|
||||
func (m *MeilisearchManager) IsEnabled() bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.status.Enabled
|
||||
}
|
||||
|
||||
// ReloadConfig 重新加载配置
|
||||
func (m *MeilisearchManager) ReloadConfig() error {
|
||||
utils.Debug("重新加载Meilisearch配置")
|
||||
return m.Initialize()
|
||||
}
|
||||
|
||||
// GetService 获取Meilisearch服务
|
||||
func (m *MeilisearchManager) GetService() *MeilisearchService {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.service
|
||||
}
|
||||
|
||||
// GetStatus 获取状态
|
||||
func (m *MeilisearchManager) GetStatus() (MeilisearchStatus, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
utils.Debug("获取Meilisearch状态 - 启用状态: %v, 健康状态: %v, 服务实例: %v", m.status.Enabled, m.status.Healthy, m.service != nil)
|
||||
|
||||
if m.service != nil && m.service.IsEnabled() {
|
||||
utils.Debug("Meilisearch服务已初始化且启用,尝试获取索引统计")
|
||||
|
||||
// 获取索引统计
|
||||
stats, err := m.service.GetIndexStats()
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch索引统计失败: %v", err)
|
||||
// 即使获取统计失败,也返回当前状态
|
||||
} else {
|
||||
utils.Debug("Meilisearch索引统计: %+v", stats)
|
||||
|
||||
// 更新文档数量
|
||||
if count, ok := stats["numberOfDocuments"].(float64); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
utils.Debug("文档数量 (float64): %d", int64(count))
|
||||
} else if count, ok := stats["numberOfDocuments"].(int64); ok {
|
||||
m.status.DocumentCount = count
|
||||
utils.Debug("文档数量 (int64): %d", count)
|
||||
} else if count, ok := stats["numberOfDocuments"].(int); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
utils.Debug("文档数量 (int): %d", int64(count))
|
||||
} else {
|
||||
utils.Error("无法解析文档数量,类型: %T, 值: %v", stats["numberOfDocuments"], stats["numberOfDocuments"])
|
||||
}
|
||||
|
||||
// 不更新启用状态,保持配置中的状态
|
||||
// 启用状态应该由配置控制,而不是由服务状态控制
|
||||
}
|
||||
} else {
|
||||
utils.Debug("Meilisearch服务未初始化或未启用 - service: %v, enabled: %v", m.service != nil, m.service != nil && m.service.IsEnabled())
|
||||
}
|
||||
|
||||
return m.status, nil
|
||||
}
|
||||
|
||||
// GetStatusWithHealthCheck 获取状态并同时进行健康检查
|
||||
func (m *MeilisearchManager) GetStatusWithHealthCheck() (MeilisearchStatus, error) {
|
||||
// 先进行健康检查
|
||||
m.checkHealth()
|
||||
|
||||
// 然后获取状态
|
||||
return m.GetStatus()
|
||||
}
|
||||
|
||||
// SyncResourceToMeilisearch 同步资源到Meilisearch
|
||||
func (m *MeilisearchManager) SyncResourceToMeilisearch(resource *entity.Resource) error {
|
||||
utils.Debug(fmt.Sprintf("开始同步资源到Meilisearch - 资源ID: %d, URL: %s", resource.ID, resource.URL))
|
||||
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
utils.Debug("Meilisearch服务未初始化或未启用")
|
||||
return fmt.Errorf("Meilisearch服务未初始化或未启用")
|
||||
}
|
||||
|
||||
// 先进行健康检查
|
||||
if err := m.service.HealthCheck(); err != nil {
|
||||
utils.Error(fmt.Sprintf("Meilisearch健康检查失败: %v", err))
|
||||
return fmt.Errorf("Meilisearch健康检查失败: %v", err)
|
||||
}
|
||||
|
||||
// 确保索引存在
|
||||
if err := m.service.CreateIndex(); err != nil {
|
||||
utils.Error(fmt.Sprintf("创建Meilisearch索引失败: %v", err))
|
||||
return fmt.Errorf("创建Meilisearch索引失败: %v", err)
|
||||
}
|
||||
|
||||
doc := m.convertResourceToDocument(resource)
|
||||
err := m.service.BatchAddDocuments([]MeilisearchDocument{doc})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 标记为已同步
|
||||
return m.repoMgr.ResourceRepository.MarkAsSyncedToMeilisearch([]uint{resource.ID})
|
||||
}
|
||||
|
||||
// SyncAllResources 同步所有资源
|
||||
func (m *MeilisearchManager) SyncAllResources() (int, error) {
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
return 0, fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 检查是否已经在同步中
|
||||
m.syncMutex.Lock()
|
||||
if m.isSyncing {
|
||||
m.syncMutex.Unlock()
|
||||
return 0, fmt.Errorf("同步操作正在进行中")
|
||||
}
|
||||
|
||||
// 初始化同步状态
|
||||
m.isSyncing = true
|
||||
m.syncProgress = SyncProgress{
|
||||
IsRunning: true,
|
||||
TotalCount: 0,
|
||||
ProcessedCount: 0,
|
||||
SyncedCount: 0,
|
||||
FailedCount: 0,
|
||||
StartTime: time.Now(),
|
||||
CurrentBatch: 0,
|
||||
TotalBatches: 0,
|
||||
ErrorMessage: "",
|
||||
}
|
||||
// 重新创建停止通道
|
||||
m.syncStopChan = make(chan struct{})
|
||||
m.syncMutex.Unlock()
|
||||
|
||||
// 在goroutine中执行同步,避免阻塞
|
||||
go func() {
|
||||
defer func() {
|
||||
m.syncMutex.Lock()
|
||||
m.isSyncing = false
|
||||
m.syncProgress.IsRunning = false
|
||||
m.syncMutex.Unlock()
|
||||
}()
|
||||
|
||||
m.syncAllResourcesInternal()
|
||||
}()
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// DebugGetAllDocuments 调试:获取所有文档
|
||||
func (m *MeilisearchManager) DebugGetAllDocuments() error {
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
utils.Debug("开始调试:获取Meilisearch中的所有文档")
|
||||
_, err := m.service.GetAllDocuments()
|
||||
if err != nil {
|
||||
utils.Error("调试获取所有文档失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Debug("调试完成:已获取所有文档")
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncAllResourcesInternal 内部同步方法
|
||||
func (m *MeilisearchManager) syncAllResourcesInternal() {
|
||||
// 健康检查
|
||||
if err := m.service.HealthCheck(); err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("Meilisearch不可用: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
if err := m.service.CreateIndex(); err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("创建索引失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("开始同步所有资源到Meilisearch...")
|
||||
|
||||
// 获取总资源数量
|
||||
totalCount, err := m.repoMgr.ResourceRepository.CountUnsyncedToMeilisearch()
|
||||
if err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("获取资源总数失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 分批处理
|
||||
batchSize := 100
|
||||
totalBatches := int((totalCount + int64(batchSize) - 1) / int64(batchSize))
|
||||
|
||||
// 更新总数量和总批次
|
||||
m.syncMutex.Lock()
|
||||
m.syncProgress.TotalCount = totalCount
|
||||
m.syncProgress.TotalBatches = totalBatches
|
||||
m.syncMutex.Unlock()
|
||||
|
||||
offset := 0
|
||||
totalSynced := 0
|
||||
currentBatch := 0
|
||||
|
||||
// 预加载所有分类和平台数据到缓存
|
||||
categoryCache := make(map[uint]string)
|
||||
panCache := make(map[uint]string)
|
||||
|
||||
// 获取所有分类
|
||||
categories, err := m.repoMgr.CategoryRepository.FindAll()
|
||||
if err == nil {
|
||||
for _, category := range categories {
|
||||
categoryCache[category.ID] = category.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有平台
|
||||
pans, err := m.repoMgr.PanRepository.FindAll()
|
||||
if err == nil {
|
||||
for _, pan := range pans {
|
||||
panCache[pan.ID] = pan.Name
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
currentBatch++
|
||||
|
||||
// 获取一批资源(在goroutine中执行,避免阻塞)
|
||||
resourcesChan := make(chan []entity.Resource, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
// 直接查询未同步的资源,不使用分页
|
||||
resources, _, err := m.repoMgr.ResourceRepository.FindUnsyncedToMeilisearch(1, batchSize)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
resourcesChan <- resources
|
||||
}()
|
||||
|
||||
// 等待数据库查询结果或停止信号(添加超时)
|
||||
select {
|
||||
case resources := <-resourcesChan:
|
||||
if len(resources) == 0 {
|
||||
utils.Info("资源同步完成,总共同步 %d 个资源", totalSynced)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 转换为Meilisearch文档(使用缓存)
|
||||
var docs []MeilisearchDocument
|
||||
for _, resource := range resources {
|
||||
doc := m.convertResourceToDocumentWithCache(&resource, categoryCache, panCache)
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 批量添加到Meilisearch(在goroutine中执行,避免阻塞)
|
||||
meilisearchErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
err := m.service.BatchAddDocuments(docs)
|
||||
meilisearchErrChan <- err
|
||||
}()
|
||||
|
||||
// 等待Meilisearch操作结果或停止信号(添加超时)
|
||||
select {
|
||||
case err := <-meilisearchErrChan:
|
||||
if err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("批量添加文档失败: %v", err))
|
||||
return
|
||||
}
|
||||
case <-time.After(60 * time.Second): // 60秒超时
|
||||
m.updateSyncProgress("", "", "Meilisearch操作超时")
|
||||
utils.Error("Meilisearch操作超时")
|
||||
return
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 标记为已同步(在goroutine中执行,避免阻塞)
|
||||
var resourceIDs []uint
|
||||
for _, resource := range resources {
|
||||
resourceIDs = append(resourceIDs, resource.ID)
|
||||
}
|
||||
|
||||
markErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
err := m.repoMgr.ResourceRepository.MarkAsSyncedToMeilisearch(resourceIDs)
|
||||
markErrChan <- err
|
||||
}()
|
||||
|
||||
// 等待标记操作结果或停止信号(添加超时)
|
||||
select {
|
||||
case err := <-markErrChan:
|
||||
if err != nil {
|
||||
utils.Error("标记资源同步状态失败: %v", err)
|
||||
}
|
||||
case <-time.After(30 * time.Second): // 30秒超时
|
||||
utils.Error("标记资源同步状态超时")
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
}
|
||||
|
||||
totalSynced += len(docs)
|
||||
offset += len(resources)
|
||||
|
||||
// 更新进度
|
||||
m.updateSyncProgress(fmt.Sprintf("%d", totalSynced), fmt.Sprintf("%d", currentBatch), "")
|
||||
|
||||
utils.Debug("已同步 %d 个资源到Meilisearch (批次 %d/%d)", totalSynced, currentBatch, totalBatches)
|
||||
|
||||
// 检查是否已经同步完所有资源
|
||||
if len(resources) == 0 {
|
||||
utils.Info("资源同步完成,总共同步 %d 个资源", totalSynced)
|
||||
return
|
||||
}
|
||||
|
||||
case <-time.After(30 * time.Second): // 30秒超时
|
||||
m.updateSyncProgress("", "", "数据库查询超时")
|
||||
utils.Error("数据库查询超时")
|
||||
return
|
||||
|
||||
case err := <-errChan:
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("获取资源失败: %v", err))
|
||||
return
|
||||
case <-m.syncStopChan:
|
||||
utils.Info("同步操作被停止")
|
||||
return
|
||||
}
|
||||
|
||||
// 避免过于频繁的请求
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
utils.Info("资源同步完成,总共同步 %d 个资源", totalSynced)
|
||||
}
|
||||
|
||||
// updateSyncProgress 更新同步进度
|
||||
func (m *MeilisearchManager) updateSyncProgress(syncedCount, currentBatch, errorMessage string) {
|
||||
m.syncMutex.Lock()
|
||||
defer m.syncMutex.Unlock()
|
||||
|
||||
if syncedCount != "" {
|
||||
if count, err := strconv.ParseInt(syncedCount, 10, 64); err == nil {
|
||||
m.syncProgress.SyncedCount = count
|
||||
}
|
||||
}
|
||||
|
||||
if currentBatch != "" {
|
||||
if batch, err := strconv.Atoi(currentBatch); err == nil {
|
||||
m.syncProgress.CurrentBatch = batch
|
||||
}
|
||||
}
|
||||
|
||||
if errorMessage != "" {
|
||||
m.syncProgress.ErrorMessage = errorMessage
|
||||
m.syncProgress.IsRunning = false
|
||||
}
|
||||
|
||||
// 计算预估时间
|
||||
if m.syncProgress.SyncedCount > 0 {
|
||||
elapsed := time.Since(m.syncProgress.StartTime)
|
||||
rate := float64(m.syncProgress.SyncedCount) / elapsed.Seconds()
|
||||
if rate > 0 {
|
||||
remaining := float64(m.syncProgress.TotalCount-m.syncProgress.SyncedCount) / rate
|
||||
m.syncProgress.EstimatedTime = fmt.Sprintf("%.0f秒", remaining)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUnsyncedCount 获取未同步资源数量
|
||||
func (m *MeilisearchManager) GetUnsyncedCount() (int64, error) {
|
||||
// 直接查询未同步的资源数量
|
||||
return m.repoMgr.ResourceRepository.CountUnsyncedToMeilisearch()
|
||||
}
|
||||
|
||||
// GetUnsyncedResources 获取未同步的资源
|
||||
func (m *MeilisearchManager) GetUnsyncedResources(page, pageSize int) ([]entity.Resource, int64, error) {
|
||||
// 查询未同步到Meilisearch的资源
|
||||
return m.repoMgr.ResourceRepository.FindUnsyncedToMeilisearch(page, pageSize)
|
||||
}
|
||||
|
||||
// GetSyncedResources 获取已同步的资源
|
||||
func (m *MeilisearchManager) GetSyncedResources(page, pageSize int) ([]entity.Resource, int64, error) {
|
||||
// 查询已同步到Meilisearch的资源
|
||||
return m.repoMgr.ResourceRepository.FindSyncedToMeilisearch(page, pageSize)
|
||||
}
|
||||
|
||||
// GetAllResources 获取所有资源
|
||||
func (m *MeilisearchManager) GetAllResources(page, pageSize int) ([]entity.Resource, int64, error) {
|
||||
// 查询所有资源
|
||||
return m.repoMgr.ResourceRepository.FindAllWithPagination(page, pageSize)
|
||||
}
|
||||
|
||||
// GetSyncProgress 获取同步进度
|
||||
func (m *MeilisearchManager) GetSyncProgress() SyncProgress {
|
||||
m.syncMutex.RLock()
|
||||
defer m.syncMutex.RUnlock()
|
||||
return m.syncProgress
|
||||
}
|
||||
|
||||
// StopSync 停止同步
|
||||
func (m *MeilisearchManager) StopSync() {
|
||||
m.syncMutex.Lock()
|
||||
defer m.syncMutex.Unlock()
|
||||
|
||||
if m.isSyncing {
|
||||
// 发送停止信号
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
// 通道已经关闭,不需要再次关闭
|
||||
default:
|
||||
close(m.syncStopChan)
|
||||
}
|
||||
|
||||
m.isSyncing = false
|
||||
m.syncProgress.IsRunning = false
|
||||
m.syncProgress.ErrorMessage = "同步已停止"
|
||||
utils.Debug("同步操作已停止")
|
||||
}
|
||||
}
|
||||
|
||||
// ClearIndex 清空索引
|
||||
func (m *MeilisearchManager) ClearIndex() error {
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 清空Meilisearch索引
|
||||
if err := m.service.ClearIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 标记所有资源为未同步
|
||||
return m.repoMgr.ResourceRepository.MarkAllAsUnsyncedToMeilisearch()
|
||||
}
|
||||
|
||||
// convertResourceToDocument 转换资源为搜索文档
|
||||
func (m *MeilisearchManager) convertResourceToDocument(resource *entity.Resource) MeilisearchDocument {
|
||||
// 获取关联数据
|
||||
var categoryName string
|
||||
if resource.CategoryID != nil {
|
||||
category, err := m.repoMgr.CategoryRepository.FindByID(*resource.CategoryID)
|
||||
if err == nil {
|
||||
categoryName = category.Name
|
||||
}
|
||||
}
|
||||
|
||||
var panName string
|
||||
if resource.PanID != nil {
|
||||
pan, err := m.repoMgr.PanRepository.FindByID(*resource.PanID)
|
||||
if err == nil {
|
||||
panName = pan.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签 - 从关联的Tags字段获取
|
||||
var tagNames []string
|
||||
if resource.Tags != nil {
|
||||
for _, tag := range resource.Tags {
|
||||
tagNames = append(tagNames, tag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return MeilisearchDocument{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
Key: resource.Key,
|
||||
Category: categoryName,
|
||||
Tags: tagNames,
|
||||
PanName: panName,
|
||||
PanID: resource.PanID,
|
||||
Author: resource.Author,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertResourceToDocumentWithCache 转换资源为搜索文档(使用缓存)
|
||||
func (m *MeilisearchManager) convertResourceToDocumentWithCache(resource *entity.Resource, categoryCache map[uint]string, panCache map[uint]string) MeilisearchDocument {
|
||||
// 从缓存获取关联数据
|
||||
var categoryName string
|
||||
if resource.CategoryID != nil {
|
||||
if name, exists := categoryCache[*resource.CategoryID]; exists {
|
||||
categoryName = name
|
||||
}
|
||||
}
|
||||
|
||||
var panName string
|
||||
if resource.PanID != nil {
|
||||
if name, exists := panCache[*resource.PanID]; exists {
|
||||
panName = name
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签 - 从关联的Tags字段获取
|
||||
var tagNames []string
|
||||
if resource.Tags != nil {
|
||||
for _, tag := range resource.Tags {
|
||||
tagNames = append(tagNames, tag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return MeilisearchDocument{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
Key: resource.Key,
|
||||
Category: categoryName,
|
||||
Tags: tagNames,
|
||||
PanName: panName,
|
||||
PanID: resource.PanID,
|
||||
Author: resource.Author,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// monitorLoop 监控循环
|
||||
func (m *MeilisearchManager) monitorLoop() {
|
||||
if m.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
m.isRunning = true
|
||||
ticker := time.NewTicker(30 * time.Second) // 每30秒检查一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.checkHealth()
|
||||
case <-m.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkHealth 检查健康状态
|
||||
func (m *MeilisearchManager) checkHealth() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.status.LastCheck = time.Now()
|
||||
|
||||
utils.Debug("开始健康检查 - 服务实例: %v, 启用状态: %v", m.service != nil, m.service != nil && m.service.IsEnabled())
|
||||
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
utils.Debug("Meilisearch服务未初始化或未启用")
|
||||
m.status.Healthy = false
|
||||
m.status.LastError = "Meilisearch未启用"
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("开始检查Meilisearch健康状态")
|
||||
|
||||
if err := m.service.HealthCheck(); err != nil {
|
||||
m.status.Healthy = false
|
||||
m.status.ErrorCount++
|
||||
m.status.LastError = err.Error()
|
||||
utils.Error("Meilisearch健康检查失败: %v", err)
|
||||
} else {
|
||||
m.status.Healthy = true
|
||||
m.status.ErrorCount = 0
|
||||
m.status.LastError = ""
|
||||
utils.Debug("Meilisearch健康检查成功")
|
||||
|
||||
// 健康检查通过后,更新文档数量
|
||||
if stats, err := m.service.GetIndexStats(); err == nil {
|
||||
if count, ok := stats["numberOfDocuments"].(float64); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
} else if count, ok := stats["numberOfDocuments"].(int64); ok {
|
||||
m.status.DocumentCount = count
|
||||
} else if count, ok := stats["numberOfDocuments"].(int); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止监控
|
||||
func (m *MeilisearchManager) Stop() {
|
||||
if !m.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
close(m.stopChan)
|
||||
m.isRunning = false
|
||||
utils.Debug("Meilisearch监控服务已停止")
|
||||
}
|
||||
561
services/meilisearch_service.go
Normal file
561
services/meilisearch_service.go
Normal file
@@ -0,0 +1,561 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
)
|
||||
|
||||
// MeilisearchDocument 搜索文档结构
|
||||
type MeilisearchDocument 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"`
|
||||
Key string `json:"key"`
|
||||
Category string `json:"category"`
|
||||
Tags []string `json:"tags"`
|
||||
PanName string `json:"pan_name"`
|
||||
PanID *uint `json:"pan_id"`
|
||||
Author string `json:"author"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// 高亮字段
|
||||
TitleHighlight string `json:"_title_highlight,omitempty"`
|
||||
DescriptionHighlight string `json:"_description_highlight,omitempty"`
|
||||
CategoryHighlight string `json:"_category_highlight,omitempty"`
|
||||
TagsHighlight []string `json:"_tags_highlight,omitempty"`
|
||||
}
|
||||
|
||||
// MeilisearchService Meilisearch服务
|
||||
type MeilisearchService struct {
|
||||
client meilisearch.ServiceManager
|
||||
index meilisearch.IndexManager
|
||||
indexName string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewMeilisearchService 创建Meilisearch服务
|
||||
func NewMeilisearchService(host, port, masterKey, indexName string, enabled bool) *MeilisearchService {
|
||||
if !enabled {
|
||||
return &MeilisearchService{
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// 构建服务器URL
|
||||
serverURL := fmt.Sprintf("http://%s:%s", host, port)
|
||||
|
||||
// 创建客户端
|
||||
var client meilisearch.ServiceManager
|
||||
|
||||
if masterKey != "" {
|
||||
client = meilisearch.New(serverURL, meilisearch.WithAPIKey(masterKey))
|
||||
} else {
|
||||
client = meilisearch.New(serverURL)
|
||||
}
|
||||
|
||||
// 获取索引
|
||||
index := client.Index(indexName)
|
||||
|
||||
return &MeilisearchService{
|
||||
client: client,
|
||||
index: index,
|
||||
indexName: indexName,
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// IsEnabled 检查是否启用
|
||||
func (m *MeilisearchService) IsEnabled() bool {
|
||||
return m.enabled
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (m *MeilisearchService) HealthCheck() error {
|
||||
if !m.enabled {
|
||||
utils.Debug("Meilisearch未启用,跳过健康检查")
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
utils.Debug("开始Meilisearch健康检查")
|
||||
|
||||
// 使用官方SDK的健康检查
|
||||
_, err := m.client.Health()
|
||||
if err != nil {
|
||||
utils.Error("Meilisearch健康检查失败: %v", err)
|
||||
return fmt.Errorf("Meilisearch健康检查失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch健康检查成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateIndex 创建索引
|
||||
func (m *MeilisearchService) CreateIndex() error {
|
||||
if !m.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建索引配置
|
||||
indexConfig := &meilisearch.IndexConfig{
|
||||
Uid: m.indexName,
|
||||
PrimaryKey: "id",
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
_, err := m.client.CreateIndex(indexConfig)
|
||||
if err != nil {
|
||||
// 如果索引已存在,返回成功
|
||||
utils.Debug("Meilisearch索引创建失败或已存在: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引创建成功: %s", m.indexName)
|
||||
|
||||
// 配置索引设置
|
||||
settings := &meilisearch.Settings{
|
||||
// 配置可过滤的属性
|
||||
FilterableAttributes: []string{
|
||||
"pan_id",
|
||||
"pan_name",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可搜索的属性
|
||||
SearchableAttributes: []string{
|
||||
"title",
|
||||
"description",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可排序的属性
|
||||
SortableAttributes: []string{
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"id",
|
||||
},
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
_, err = m.index.UpdateSettings(settings)
|
||||
if err != nil {
|
||||
utils.Error("更新Meilisearch索引设置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引设置更新成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateIndexSettings 更新索引设置
|
||||
func (m *MeilisearchService) UpdateIndexSettings() error {
|
||||
if !m.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 配置索引设置
|
||||
settings := &meilisearch.Settings{
|
||||
// 配置可过滤的属性
|
||||
FilterableAttributes: []string{
|
||||
"pan_id",
|
||||
"pan_name",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可搜索的属性
|
||||
SearchableAttributes: []string{
|
||||
"title",
|
||||
"description",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可排序的属性
|
||||
SortableAttributes: []string{
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"id",
|
||||
},
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
_, err := m.index.UpdateSettings(settings)
|
||||
if err != nil {
|
||||
utils.Error("更新Meilisearch索引设置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引设置更新成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchAddDocuments 批量添加文档
|
||||
func (m *MeilisearchService) BatchAddDocuments(docs []MeilisearchDocument) error {
|
||||
utils.Debug(fmt.Sprintf("开始批量添加文档到Meilisearch - 文档数量: %d", len(docs)))
|
||||
|
||||
if !m.enabled {
|
||||
utils.Debug("Meilisearch未启用,跳过批量添加")
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
if len(docs) == 0 {
|
||||
utils.Debug("文档列表为空,跳过批量添加")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为interface{}切片
|
||||
var documents []interface{}
|
||||
for i, doc := range docs {
|
||||
utils.Debug(fmt.Sprintf("转换文档 %d - ID: %d, 标题: %s", i+1, doc.ID, doc.Title))
|
||||
documents = append(documents, doc)
|
||||
}
|
||||
|
||||
utils.Debug(fmt.Sprintf("开始调用Meilisearch API添加 %d 个文档", len(documents)))
|
||||
|
||||
// 批量添加文档
|
||||
_, err := m.index.AddDocuments(documents, nil)
|
||||
if err != nil {
|
||||
utils.Error(fmt.Sprintf("Meilisearch批量添加文档失败: %v", err))
|
||||
return fmt.Errorf("Meilisearch批量添加文档失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug(fmt.Sprintf("成功批量添加 %d 个文档到Meilisearch", len(docs)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search 搜索文档
|
||||
func (m *MeilisearchService) Search(query string, filters map[string]interface{}, page, pageSize int) ([]MeilisearchDocument, int64, error) {
|
||||
|
||||
if !m.enabled {
|
||||
return nil, 0, fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 构建搜索请求
|
||||
searchRequest := &meilisearch.SearchRequest{
|
||||
Query: query,
|
||||
Offset: int64((page - 1) * pageSize),
|
||||
Limit: int64(pageSize),
|
||||
// 启用高亮功能
|
||||
AttributesToHighlight: []string{"title", "description", "category", "tags"},
|
||||
HighlightPreTag: "<mark>",
|
||||
HighlightPostTag: "</mark>",
|
||||
}
|
||||
|
||||
// 添加过滤器
|
||||
if len(filters) > 0 {
|
||||
var filterStrings []string
|
||||
for key, value := range filters {
|
||||
switch key {
|
||||
case "pan_id":
|
||||
// 直接使用pan_id进行过滤
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("pan_id = %v", value))
|
||||
case "pan_name":
|
||||
// 使用pan_name进行过滤
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("pan_name = %q", value))
|
||||
case "category":
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("category = %q", value))
|
||||
case "tags":
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("tags = %q", value))
|
||||
default:
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("%s = %q", key, value))
|
||||
}
|
||||
}
|
||||
if len(filterStrings) > 0 {
|
||||
searchRequest.Filter = filterStrings
|
||||
}
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
result, err := m.index.Search(query, searchRequest)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("搜索失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
var documents []MeilisearchDocument
|
||||
|
||||
// 如果没有任何结果,直接返回
|
||||
if len(result.Hits) == 0 {
|
||||
utils.Debug("没有搜索结果")
|
||||
return documents, result.EstimatedTotalHits, nil
|
||||
}
|
||||
|
||||
for _, hit := range result.Hits {
|
||||
// 将hit转换为MeilisearchDocument
|
||||
doc := MeilisearchDocument{}
|
||||
|
||||
// 解析JSON数据 - 使用反射
|
||||
hitValue := reflect.ValueOf(hit)
|
||||
|
||||
if hitValue.Kind() == reflect.Map {
|
||||
for _, key := range hitValue.MapKeys() {
|
||||
keyStr := key.String()
|
||||
value := hitValue.MapIndex(key).Interface()
|
||||
|
||||
// 处理_formatted字段(包含所有高亮内容)
|
||||
if keyStr == "_formatted" {
|
||||
if rawValue, ok := value.(json.RawMessage); ok {
|
||||
// 解析_formatted字段中的高亮内容
|
||||
var formattedData map[string]interface{}
|
||||
if err := json.Unmarshal(rawValue, &formattedData); err == nil {
|
||||
// 提取高亮字段
|
||||
if titleHighlight, ok := formattedData["title"].(string); ok {
|
||||
doc.TitleHighlight = titleHighlight
|
||||
}
|
||||
if descHighlight, ok := formattedData["description"].(string); ok {
|
||||
doc.DescriptionHighlight = descHighlight
|
||||
}
|
||||
if categoryHighlight, ok := formattedData["category"].(string); ok {
|
||||
doc.CategoryHighlight = categoryHighlight
|
||||
}
|
||||
if tagsHighlight, ok := formattedData["tags"].([]interface{}); ok {
|
||||
var tags []string
|
||||
for _, tag := range tagsHighlight {
|
||||
if tagStr, ok := tag.(string); ok {
|
||||
tags = append(tags, tagStr)
|
||||
}
|
||||
}
|
||||
doc.TagsHighlight = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch keyStr {
|
||||
case "id":
|
||||
if rawID, ok := value.(json.RawMessage); ok {
|
||||
var id float64
|
||||
if err := json.Unmarshal(rawID, &id); err == nil {
|
||||
doc.ID = uint(id)
|
||||
}
|
||||
}
|
||||
case "title":
|
||||
if rawTitle, ok := value.(json.RawMessage); ok {
|
||||
var title string
|
||||
if err := json.Unmarshal(rawTitle, &title); err == nil {
|
||||
doc.Title = title
|
||||
}
|
||||
}
|
||||
case "description":
|
||||
if rawDesc, ok := value.(json.RawMessage); ok {
|
||||
var description string
|
||||
if err := json.Unmarshal(rawDesc, &description); err == nil {
|
||||
doc.Description = description
|
||||
}
|
||||
}
|
||||
case "url":
|
||||
if rawURL, ok := value.(json.RawMessage); ok {
|
||||
var url string
|
||||
if err := json.Unmarshal(rawURL, &url); err == nil {
|
||||
doc.URL = url
|
||||
}
|
||||
}
|
||||
case "save_url":
|
||||
if rawSaveURL, ok := value.(json.RawMessage); ok {
|
||||
var saveURL string
|
||||
if err := json.Unmarshal(rawSaveURL, &saveURL); err == nil {
|
||||
doc.SaveURL = saveURL
|
||||
}
|
||||
}
|
||||
case "file_size":
|
||||
if rawFileSize, ok := value.(json.RawMessage); ok {
|
||||
var fileSize string
|
||||
if err := json.Unmarshal(rawFileSize, &fileSize); err == nil {
|
||||
doc.FileSize = fileSize
|
||||
}
|
||||
}
|
||||
case "key":
|
||||
if rawKey, ok := value.(json.RawMessage); ok {
|
||||
var key string
|
||||
if err := json.Unmarshal(rawKey, &key); err == nil {
|
||||
doc.Key = key
|
||||
}
|
||||
}
|
||||
case "category":
|
||||
if rawCategory, ok := value.(json.RawMessage); ok {
|
||||
var category string
|
||||
if err := json.Unmarshal(rawCategory, &category); err == nil {
|
||||
doc.Category = category
|
||||
}
|
||||
}
|
||||
case "tags":
|
||||
if rawTags, ok := value.(json.RawMessage); ok {
|
||||
var tags []string
|
||||
if err := json.Unmarshal(rawTags, &tags); err == nil {
|
||||
doc.Tags = tags
|
||||
}
|
||||
}
|
||||
case "pan_name":
|
||||
if rawPanName, ok := value.(json.RawMessage); ok {
|
||||
var panName string
|
||||
if err := json.Unmarshal(rawPanName, &panName); err == nil {
|
||||
doc.PanName = panName
|
||||
}
|
||||
}
|
||||
case "pan_id":
|
||||
if rawPanID, ok := value.(json.RawMessage); ok {
|
||||
var panID float64
|
||||
if err := json.Unmarshal(rawPanID, &panID); err == nil {
|
||||
panIDUint := uint(panID)
|
||||
doc.PanID = &panIDUint
|
||||
}
|
||||
}
|
||||
case "author":
|
||||
if rawAuthor, ok := value.(json.RawMessage); ok {
|
||||
var author string
|
||||
if err := json.Unmarshal(rawAuthor, &author); err == nil {
|
||||
doc.Author = author
|
||||
}
|
||||
}
|
||||
case "created_at":
|
||||
if rawCreatedAt, ok := value.(json.RawMessage); ok {
|
||||
var createdAt string
|
||||
if err := json.Unmarshal(rawCreatedAt, &createdAt); err == nil {
|
||||
// 尝试多种时间格式
|
||||
var t time.Time
|
||||
var parseErr error
|
||||
formats := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05Z",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05.000Z",
|
||||
}
|
||||
for _, format := range formats {
|
||||
if t, parseErr = time.Parse(format, createdAt); parseErr == nil {
|
||||
doc.CreatedAt = t
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "updated_at":
|
||||
if rawUpdatedAt, ok := value.(json.RawMessage); ok {
|
||||
var updatedAt string
|
||||
if err := json.Unmarshal(rawUpdatedAt, &updatedAt); err == nil {
|
||||
// 尝试多种时间格式
|
||||
var t time.Time
|
||||
var parseErr error
|
||||
formats := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05Z",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05.000Z",
|
||||
}
|
||||
for _, format := range formats {
|
||||
if t, parseErr = time.Parse(format, updatedAt); parseErr == nil {
|
||||
doc.UpdatedAt = t
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 高亮字段处理 - 已移除,现在使用_formatted字段
|
||||
}
|
||||
}
|
||||
} else {
|
||||
utils.Error("hit不是Map类型,无法解析")
|
||||
}
|
||||
|
||||
documents = append(documents, doc)
|
||||
}
|
||||
|
||||
return documents, result.EstimatedTotalHits, nil
|
||||
}
|
||||
|
||||
// GetAllDocuments 获取所有文档(用于调试)
|
||||
func (m *MeilisearchService) GetAllDocuments() ([]MeilisearchDocument, error) {
|
||||
if !m.enabled {
|
||||
return nil, fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 构建搜索请求,获取所有文档
|
||||
searchRequest := &meilisearch.SearchRequest{
|
||||
Query: "",
|
||||
Offset: 0,
|
||||
Limit: 1000, // 获取前1000个文档
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
result, err := m.index.Search("", searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取所有文档失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("获取所有文档,总数: %d", result.EstimatedTotalHits)
|
||||
utils.Debug("获取到的文档数量: %d", len(result.Hits))
|
||||
|
||||
// 解析结果
|
||||
var documents []MeilisearchDocument
|
||||
utils.Debug("获取到 %d 个文档", len(result.Hits))
|
||||
|
||||
// 只显示前3个文档的字段信息
|
||||
for i, hit := range result.Hits {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
utils.Debug("文档%d的字段:", i+1)
|
||||
hitValue := reflect.ValueOf(hit)
|
||||
if hitValue.Kind() == reflect.Map {
|
||||
for _, key := range hitValue.MapKeys() {
|
||||
keyStr := key.String()
|
||||
value := hitValue.MapIndex(key).Interface()
|
||||
if rawValue, ok := value.(json.RawMessage); ok {
|
||||
utils.Debug(" %s: %s", keyStr, string(rawValue))
|
||||
} else {
|
||||
utils.Debug(" %s: %v", keyStr, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return documents, nil
|
||||
}
|
||||
|
||||
// GetIndexStats 获取索引统计信息
|
||||
func (m *MeilisearchService) GetIndexStats() (map[string]interface{}, error) {
|
||||
if !m.enabled {
|
||||
return map[string]interface{}{
|
||||
"enabled": false,
|
||||
"message": "Meilisearch未启用",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取索引统计
|
||||
stats, err := m.index.GetStats()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取索引统计失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch统计 - 文档数: %d, 索引中: %v", stats.NumberOfDocuments, stats.IsIndexing)
|
||||
|
||||
// 转换为map
|
||||
result := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"numberOfDocuments": stats.NumberOfDocuments,
|
||||
"isIndexing": stats.IsIndexing,
|
||||
"fieldDistribution": stats.FieldDistribution,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ClearIndex 清空索引
|
||||
func (m *MeilisearchService) ClearIndex() error {
|
||||
if !m.enabled {
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 清空索引
|
||||
_, err := m.index.DeleteAllDocuments()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清空索引失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引已清空")
|
||||
return nil
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func (tm *TaskManager) RegisterProcessor(processor TaskProcessor) {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
tm.processors[processor.GetTaskType()] = processor
|
||||
utils.Info("注册任务处理器: %s", processor.GetTaskType())
|
||||
utils.Debug("注册任务处理器: %s", processor.GetTaskType())
|
||||
}
|
||||
|
||||
// getRegisteredProcessors 获取已注册的处理器列表(用于调试)
|
||||
@@ -56,11 +56,11 @@ func (tm *TaskManager) StartTask(taskID uint) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
utils.Info("StartTask: 尝试启动任务 %d", taskID)
|
||||
utils.Debug("StartTask: 尝试启动任务 %d", taskID)
|
||||
|
||||
// 检查任务是否已在运行
|
||||
if _, exists := tm.running[taskID]; exists {
|
||||
utils.Info("任务 %d 已在运行中", taskID)
|
||||
utils.Debug("任务 %d 已在运行中", taskID)
|
||||
return fmt.Errorf("任务 %d 已在运行中", taskID)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (tm *TaskManager) StartTask(taskID uint) error {
|
||||
return fmt.Errorf("获取任务失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Info("StartTask: 获取到任务 %d, 类型: %s, 状态: %s", task.ID, task.Type, task.Status)
|
||||
utils.Debug("StartTask: 获取到任务 %d, 类型: %s, 状态: %s", task.ID, task.Type, task.Status)
|
||||
|
||||
// 获取处理器
|
||||
processor, exists := tm.processors[string(task.Type)]
|
||||
@@ -80,13 +80,13 @@ func (tm *TaskManager) StartTask(taskID uint) error {
|
||||
return fmt.Errorf("未找到任务类型 %s 的处理器", task.Type)
|
||||
}
|
||||
|
||||
utils.Info("StartTask: 找到处理器 %s", task.Type)
|
||||
utils.Debug("StartTask: 找到处理器 %s", task.Type)
|
||||
|
||||
// 创建上下文
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
tm.running[taskID] = cancel
|
||||
|
||||
utils.Info("StartTask: 启动后台任务协程")
|
||||
utils.Debug("StartTask: 启动后台任务协程")
|
||||
// 启动后台任务
|
||||
go tm.processTask(ctx, task, processor)
|
||||
|
||||
@@ -189,10 +189,10 @@ func (tm *TaskManager) processTask(ctx context.Context, task *entity.Task, proce
|
||||
tm.mu.Lock()
|
||||
delete(tm.running, task.ID)
|
||||
tm.mu.Unlock()
|
||||
utils.Info("processTask: 任务 %d 处理完成,清理资源", task.ID)
|
||||
utils.Debug("processTask: 任务 %d 处理完成,清理资源", task.ID)
|
||||
}()
|
||||
|
||||
utils.Info("processTask: 开始处理任务: %d, 类型: %s", task.ID, task.Type)
|
||||
utils.Debug("processTask: 开始处理任务: %d, 类型: %s", task.ID, task.Type)
|
||||
|
||||
// 更新任务状态为运行中
|
||||
err := tm.repoMgr.TaskRepository.UpdateStatus(task.ID, "running")
|
||||
@@ -230,7 +230,7 @@ func (tm *TaskManager) processTask(ctx context.Context, task *entity.Task, proce
|
||||
|
||||
// 如果当前批次有处理中的任务项,重置它们为pending状态(服务器重启恢复)
|
||||
if processingItems > 0 {
|
||||
utils.Info("任务 %d 发现 %d 个处理中的任务项,重置为pending状态", task.ID, processingItems)
|
||||
utils.Debug("任务 %d 发现 %d 个处理中的任务项,重置为pending状态", task.ID, processingItems)
|
||||
err = tm.repoMgr.TaskItemRepository.ResetProcessingItems(task.ID)
|
||||
if err != nil {
|
||||
utils.Error("重置处理中任务项失败: %v", err)
|
||||
@@ -249,13 +249,13 @@ func (tm *TaskManager) processTask(ctx context.Context, task *entity.Task, proce
|
||||
successItems := completedItems
|
||||
failedItems := initialFailedItems
|
||||
|
||||
utils.Info("任务 %d 统计信息: 总计=%d, 已完成=%d, 已失败=%d, 待处理=%d",
|
||||
utils.Debug("任务 %d 统计信息: 总计=%d, 已完成=%d, 已失败=%d, 待处理=%d",
|
||||
task.ID, totalItems, completedItems, failedItems, currentBatchItems)
|
||||
|
||||
for _, item := range items {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.Info("任务 %d 被取消", task.ID)
|
||||
utils.Debug("任务 %d 被取消", task.ID)
|
||||
return
|
||||
default:
|
||||
// 处理单个任务项
|
||||
|
||||
287
utils/forbidden_words.go
Normal file
287
utils/forbidden_words.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// ForbiddenWordsProcessor 违禁词处理器
|
||||
type ForbiddenWordsProcessor struct{}
|
||||
|
||||
// NewForbiddenWordsProcessor 创建违禁词处理器实例
|
||||
func NewForbiddenWordsProcessor() *ForbiddenWordsProcessor {
|
||||
return &ForbiddenWordsProcessor{}
|
||||
}
|
||||
|
||||
// CheckContainsForbiddenWords 检查字符串是否包含违禁词
|
||||
// 参数:
|
||||
// - text: 要检查的文本
|
||||
// - forbiddenWords: 违禁词列表
|
||||
//
|
||||
// 返回:
|
||||
// - bool: 是否包含违禁词
|
||||
// - []string: 匹配到的违禁词列表
|
||||
func (p *ForbiddenWordsProcessor) CheckContainsForbiddenWords(text string, forbiddenWords []string) (bool, []string) {
|
||||
if len(forbiddenWords) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var matchedWords []string
|
||||
textLower := strings.ToLower(text)
|
||||
|
||||
for _, word := range forbiddenWords {
|
||||
wordLower := strings.ToLower(word)
|
||||
if strings.Contains(textLower, wordLower) {
|
||||
matchedWords = append(matchedWords, word)
|
||||
}
|
||||
}
|
||||
|
||||
return len(matchedWords) > 0, matchedWords
|
||||
}
|
||||
|
||||
// ReplaceForbiddenWords 替换字符串中的违禁词为 *
|
||||
// 参数:
|
||||
// - text: 要处理的文本
|
||||
// - forbiddenWords: 违禁词列表
|
||||
//
|
||||
// 返回:
|
||||
// - string: 替换后的文本
|
||||
func (p *ForbiddenWordsProcessor) ReplaceForbiddenWords(text string, forbiddenWords []string) string {
|
||||
if len(forbiddenWords) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
result := text
|
||||
// 按长度降序排序,避免短词替换后影响长词的匹配
|
||||
sortedWords := make([]string, len(forbiddenWords))
|
||||
copy(sortedWords, forbiddenWords)
|
||||
|
||||
// 简单的长度排序(这里可以优化为更复杂的排序)
|
||||
for i := 0; i < len(sortedWords)-1; i++ {
|
||||
for j := i + 1; j < len(sortedWords); j++ {
|
||||
if len(sortedWords[i]) < len(sortedWords[j]) {
|
||||
sortedWords[i], sortedWords[j] = sortedWords[j], sortedWords[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, word := range sortedWords {
|
||||
// 使用正则表达式进行不区分大小写的替换
|
||||
// 对于中文,不使用单词边界,直接替换
|
||||
re := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(word))
|
||||
// 使用字符长度而不是字节长度
|
||||
charCount := len([]rune(word))
|
||||
result = re.ReplaceAllString(result, strings.Repeat("*", charCount))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ReplaceForbiddenWordsWithHighlight 替换字符串中的违禁词为 *(处理高亮标记)
|
||||
// 参数:
|
||||
// - text: 要处理的文本(可能包含高亮标记)
|
||||
// - forbiddenWords: 违禁词列表
|
||||
//
|
||||
// 返回:
|
||||
// - string: 替换后的文本
|
||||
func (p *ForbiddenWordsProcessor) ReplaceForbiddenWordsWithHighlight(text string, forbiddenWords []string) string {
|
||||
if len(forbiddenWords) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
// 1. 先移除所有高亮标记,获取纯文本
|
||||
cleanText := regexp.MustCompile(`<mark>(.*?)</mark>`).ReplaceAllString(text, "$1")
|
||||
|
||||
// 2. 检查纯文本中是否包含违禁词
|
||||
hasForbidden := false
|
||||
for _, word := range forbiddenWords {
|
||||
re := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(word))
|
||||
if re.MatchString(cleanText) {
|
||||
hasForbidden = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果包含违禁词,则替换非高亮文本
|
||||
if hasForbidden {
|
||||
return p.ReplaceForbiddenWords(text, forbiddenWords)
|
||||
}
|
||||
|
||||
// 4. 如果不包含违禁词,直接返回原文本
|
||||
return text
|
||||
}
|
||||
|
||||
// ProcessForbiddenWords 处理违禁词:检查并替换
|
||||
// 参数:
|
||||
// - text: 要处理的文本
|
||||
// - forbiddenWords: 违禁词列表
|
||||
//
|
||||
// 返回:
|
||||
// - bool: 是否包含违禁词
|
||||
// - []string: 匹配到的违禁词列表
|
||||
// - string: 替换后的文本
|
||||
func (p *ForbiddenWordsProcessor) ProcessForbiddenWords(text string, forbiddenWords []string) (bool, []string, string) {
|
||||
contains, matchedWords := p.CheckContainsForbiddenWords(text, forbiddenWords)
|
||||
replacedText := p.ReplaceForbiddenWords(text, forbiddenWords)
|
||||
return contains, matchedWords, replacedText
|
||||
}
|
||||
|
||||
// ParseForbiddenWordsConfig 解析违禁词配置字符串
|
||||
// 参数:
|
||||
// - config: 违禁词配置字符串,多个词用逗号分隔
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 处理后的违禁词列表
|
||||
func (p *ForbiddenWordsProcessor) ParseForbiddenWordsConfig(config string) []string {
|
||||
if config == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
words := strings.Split(config, ",")
|
||||
var cleanWords []string
|
||||
for _, word := range words {
|
||||
word = strings.TrimSpace(word)
|
||||
if word != "" {
|
||||
cleanWords = append(cleanWords, word)
|
||||
}
|
||||
}
|
||||
|
||||
return cleanWords
|
||||
}
|
||||
|
||||
// 全局实例,方便直接调用
|
||||
var DefaultForbiddenWordsProcessor = NewForbiddenWordsProcessor()
|
||||
|
||||
// 便捷函数,直接调用全局实例
|
||||
|
||||
// CheckContainsForbiddenWords 检查字符串是否包含违禁词(便捷函数)
|
||||
func CheckContainsForbiddenWords(text string, forbiddenWords []string) (bool, []string) {
|
||||
return DefaultForbiddenWordsProcessor.CheckContainsForbiddenWords(text, forbiddenWords)
|
||||
}
|
||||
|
||||
// ReplaceForbiddenWords 替换字符串中的违禁词为 *(便捷函数)
|
||||
func ReplaceForbiddenWords(text string, forbiddenWords []string) string {
|
||||
return DefaultForbiddenWordsProcessor.ReplaceForbiddenWords(text, forbiddenWords)
|
||||
}
|
||||
|
||||
// ReplaceForbiddenWordsWithHighlight 替换字符串中的违禁词为 *(处理高亮标记,便捷函数)
|
||||
func ReplaceForbiddenWordsWithHighlight(text string, forbiddenWords []string) string {
|
||||
return DefaultForbiddenWordsProcessor.ReplaceForbiddenWordsWithHighlight(text, forbiddenWords)
|
||||
}
|
||||
|
||||
// ProcessForbiddenWords 处理违禁词:检查并替换(便捷函数)
|
||||
func ProcessForbiddenWords(text string, forbiddenWords []string) (bool, []string, string) {
|
||||
return DefaultForbiddenWordsProcessor.ProcessForbiddenWords(text, forbiddenWords)
|
||||
}
|
||||
|
||||
// ParseForbiddenWordsConfig 解析违禁词配置字符串(便捷函数)
|
||||
func ParseForbiddenWordsConfig(config string) []string {
|
||||
return DefaultForbiddenWordsProcessor.ParseForbiddenWordsConfig(config)
|
||||
}
|
||||
|
||||
// RemoveDuplicates 去除字符串切片中的重复项
|
||||
func RemoveDuplicates(slice []string) []string {
|
||||
keys := make(map[string]bool)
|
||||
var result []string
|
||||
for _, item := range slice {
|
||||
if _, value := keys[item]; !value {
|
||||
keys[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ResourceForbiddenInfo 资源违禁词信息
|
||||
type ResourceForbiddenInfo struct {
|
||||
HasForbiddenWords bool `json:"has_forbidden_words"`
|
||||
ForbiddenWords []string `json:"forbidden_words"`
|
||||
ProcessedTitle string `json:"-"` // 不序列化,仅内部使用
|
||||
ProcessedDesc string `json:"-"` // 不序列化,仅内部使用
|
||||
}
|
||||
|
||||
// CheckResourceForbiddenWords 检查资源是否包含违禁词(检查标题和描述)
|
||||
// 参数:
|
||||
// - title: 资源标题
|
||||
// - description: 资源描述
|
||||
// - forbiddenWords: 违禁词列表
|
||||
//
|
||||
// 返回:
|
||||
// - ResourceForbiddenInfo: 包含检查结果和处理后的文本
|
||||
func CheckResourceForbiddenWords(title, description string, forbiddenWords []string) ResourceForbiddenInfo {
|
||||
|
||||
if len(forbiddenWords) == 0 {
|
||||
return ResourceForbiddenInfo{
|
||||
HasForbiddenWords: false,
|
||||
ForbiddenWords: []string{},
|
||||
ProcessedTitle: title,
|
||||
ProcessedDesc: description,
|
||||
}
|
||||
}
|
||||
|
||||
// 分别检查标题和描述
|
||||
titleHasForbidden, titleMatchedWords := CheckContainsForbiddenWords(title, forbiddenWords)
|
||||
descHasForbidden, descMatchedWords := CheckContainsForbiddenWords(description, forbiddenWords)
|
||||
|
||||
// 合并结果
|
||||
hasForbiddenWords := titleHasForbidden || descHasForbidden
|
||||
var matchedWords []string
|
||||
if titleHasForbidden {
|
||||
matchedWords = append(matchedWords, titleMatchedWords...)
|
||||
}
|
||||
if descHasForbidden {
|
||||
matchedWords = append(matchedWords, descMatchedWords...)
|
||||
}
|
||||
// 去重
|
||||
matchedWords = RemoveDuplicates(matchedWords)
|
||||
|
||||
// 处理文本(替换违禁词)
|
||||
processedTitle := ReplaceForbiddenWords(title, forbiddenWords)
|
||||
processedDesc := ReplaceForbiddenWords(description, forbiddenWords)
|
||||
|
||||
return ResourceForbiddenInfo{
|
||||
HasForbiddenWords: hasForbiddenWords,
|
||||
ForbiddenWords: matchedWords,
|
||||
ProcessedTitle: processedTitle,
|
||||
ProcessedDesc: processedDesc,
|
||||
}
|
||||
}
|
||||
|
||||
// GetForbiddenWordsFromConfig 从系统配置获取违禁词列表
|
||||
// 参数:
|
||||
// - getConfigFunc: 获取配置的函数
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 解析后的违禁词列表
|
||||
// - error: 获取配置时的错误
|
||||
func GetForbiddenWordsFromConfig(getConfigFunc func() (string, error)) ([]string, error) {
|
||||
forbiddenWords, err := getConfigFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseForbiddenWordsConfig(forbiddenWords), nil
|
||||
}
|
||||
|
||||
// ProcessResourcesForbiddenWords 批量处理资源的违禁词
|
||||
// 参数:
|
||||
// - resources: 资源切片
|
||||
// - forbiddenWords: 违禁词列表
|
||||
//
|
||||
// 返回:
|
||||
// - 处理后的资源切片
|
||||
func ProcessResourcesForbiddenWords(resources []entity.Resource, forbiddenWords []string) []entity.Resource {
|
||||
if len(forbiddenWords) == 0 {
|
||||
return resources
|
||||
}
|
||||
|
||||
for i := range resources {
|
||||
// 处理标题中的违禁词
|
||||
resources[i].Title = ReplaceForbiddenWords(resources[i].Title, forbiddenWords)
|
||||
// 处理描述中的违禁词
|
||||
resources[i].Description = ReplaceForbiddenWords(resources[i].Description, forbiddenWords)
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
@@ -23,13 +23,14 @@ type VersionInfo struct {
|
||||
|
||||
// 编译时注入的版本信息
|
||||
var (
|
||||
Version = getVersionFromFile()
|
||||
// 这些变量将在编译时通过 ldflags 注入
|
||||
Version = "unknown" // 默认版本,编译时会被覆盖
|
||||
BuildTime = GetCurrentTimeString()
|
||||
GitCommit = "unknown"
|
||||
GitBranch = "unknown"
|
||||
)
|
||||
|
||||
// getVersionFromFile 从VERSION文件读取版本号
|
||||
// getVersionFromFile 从VERSION文件读取版本号(备用方案)
|
||||
func getVersionFromFile() string {
|
||||
data, err := os.ReadFile("VERSION")
|
||||
if err != nil {
|
||||
@@ -42,11 +43,29 @@ func getVersionFromFile() string {
|
||||
func GetVersionInfo() *VersionInfo {
|
||||
buildTime, _ := ParseTime(BuildTime)
|
||||
|
||||
// 检查版本信息是否通过编译时注入
|
||||
version := Version
|
||||
gitCommit := GitCommit
|
||||
gitBranch := GitBranch
|
||||
|
||||
// 如果编译时注入的版本是默认值,尝试从文件读取
|
||||
if version == "unknown" {
|
||||
version = getVersionFromFile()
|
||||
}
|
||||
|
||||
// 如果Git信息是默认值,尝试从文件读取
|
||||
if gitCommit == "unknown" {
|
||||
gitCommit = "unknown"
|
||||
}
|
||||
if gitBranch == "unknown" {
|
||||
gitBranch = "unknown"
|
||||
}
|
||||
|
||||
return &VersionInfo{
|
||||
Version: Version,
|
||||
Version: version,
|
||||
BuildTime: buildTime,
|
||||
GitCommit: GitCommit,
|
||||
GitBranch: GitBranch,
|
||||
GitCommit: gitCommit,
|
||||
GitBranch: gitBranch,
|
||||
GoVersion: runtime.Version(),
|
||||
NodeVersion: getNodeVersion(),
|
||||
Platform: runtime.GOOS,
|
||||
|
||||
@@ -32,4 +32,14 @@
|
||||
.resource-card {
|
||||
@apply bg-white rounded-lg shadow-md border border-gray-200 p-4 hover:shadow-lg transition-shadow duration-200;
|
||||
}
|
||||
|
||||
/* 搜索高亮样式 */
|
||||
mark {
|
||||
@apply bg-yellow-200 text-yellow-900 px-1 py-0.5 rounded font-medium;
|
||||
}
|
||||
|
||||
/* 暗色模式下的高亮样式 */
|
||||
.dark mark {
|
||||
@apply bg-yellow-600 text-yellow-100;
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 自动处理状态 -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<NuxtLink to="/admin/feature-config" class="flex items-center space-x-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 px-2 py-1 rounded transition-colors">
|
||||
<div :class="autoProcessEnabled ? 'w-2 h-2 bg-green-500 rounded-full' : 'w-2 h-2 bg-gray-400 rounded-full'"></div>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">
|
||||
自动处理{{ autoProcessEnabled ? '已开启' : '已关闭' }}
|
||||
</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- 自动转存状态 -->
|
||||
<div class="flex items-center space-x-2">
|
||||
|
||||
@@ -21,15 +21,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useApiFetch } from '~/composables/useApiFetch'
|
||||
import { parseApiResponse } from '~/composables/useApi'
|
||||
|
||||
// 使用版本信息组合式函数
|
||||
const { versionInfo, fetchVersionInfo } = useVersion()
|
||||
|
||||
// 获取系统配置
|
||||
const { data: systemConfigData } = await useAsyncData('footerSystemConfig',
|
||||
() => useApiFetch('/system/config').then(parseApiResponse)
|
||||
)
|
||||
|
||||
const systemConfig = computed(() => (systemConfigData.value as any) || { copyright: '© 2025 老九网盘资源数据库 By 老九' })
|
||||
import { useSystemConfigStore } from '~/stores/systemConfig'
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
await systemConfigStore.initConfig(false, false)
|
||||
const systemConfig = computed(() => systemConfigStore.config)
|
||||
console.log(systemConfig.value)
|
||||
|
||||
// 组件挂载时获取版本信息
|
||||
onMounted(() => {
|
||||
|
||||
@@ -9,6 +9,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 违禁词禁止访问状态 -->
|
||||
<div v-else-if="forbidden" class="space-y-4">
|
||||
<div class="flex flex-col items-center justify-center py-4">
|
||||
<!-- 使用SVG图标 -->
|
||||
<div class="mb-6">
|
||||
<img src="/assets/svg/forbidden.svg" alt="禁止访问" class="w-48 h-48" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-red-600 dark:text-red-400 mb-2">禁止访问</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">该资源包含违禁内容,无法访问</p>
|
||||
<n-button @click="closeModal" class="bg-red-500 hover:bg-red-600 text-white">
|
||||
我知道了
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="space-y-4">
|
||||
<n-alert type="error" :show-icon="false">
|
||||
@@ -150,6 +165,8 @@ interface Props {
|
||||
platform?: string
|
||||
message?: string
|
||||
error?: string
|
||||
forbidden?: boolean
|
||||
forbidden_words?: string[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
|
||||
@@ -226,3 +226,33 @@ function log(...args: any[]) {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
|
||||
// Meilisearch管理API
|
||||
export const useMeilisearchApi = () => {
|
||||
const getStatus = () => useApiFetch('/meilisearch/status').then(parseApiResponse)
|
||||
const getUnsyncedCount = () => useApiFetch('/meilisearch/unsynced-count').then(parseApiResponse)
|
||||
const getUnsyncedResources = (params?: any) => useApiFetch('/meilisearch/unsynced', { params }).then(parseApiResponse)
|
||||
const getSyncedResources = (params?: any) => useApiFetch('/meilisearch/synced', { params }).then(parseApiResponse)
|
||||
const getAllResources = (params?: any) => useApiFetch('/meilisearch/resources', { params }).then(parseApiResponse)
|
||||
const testConnection = (data: any) => useApiFetch('/meilisearch/test-connection', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const syncAllResources = () => useApiFetch('/meilisearch/sync-all', { method: 'POST' }).then(parseApiResponse)
|
||||
const stopSync = () => useApiFetch('/meilisearch/stop-sync', { method: 'POST' }).then(parseApiResponse)
|
||||
const clearIndex = () => useApiFetch('/meilisearch/clear-index', { method: 'POST' }).then(parseApiResponse)
|
||||
const updateIndexSettings = () => useApiFetch('/meilisearch/update-settings', { method: 'POST' }).then(parseApiResponse)
|
||||
const getSyncProgress = () => useApiFetch('/meilisearch/sync-progress').then(parseApiResponse)
|
||||
const debugGetAllDocuments = () => useApiFetch('/meilisearch/debug/documents').then(parseApiResponse)
|
||||
return {
|
||||
getStatus,
|
||||
getUnsyncedCount,
|
||||
getUnsyncedResources,
|
||||
getSyncedResources,
|
||||
getAllResources,
|
||||
testConnection,
|
||||
syncAllResources,
|
||||
stopSync,
|
||||
clearIndex,
|
||||
updateIndexSettings,
|
||||
getSyncProgress,
|
||||
debugGetAllDocuments
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ interface VersionResponse {
|
||||
|
||||
export const useVersion = () => {
|
||||
const versionInfo = ref<VersionInfo>({
|
||||
version: '1.2.3',
|
||||
version: '1.2.5',
|
||||
build_time: '',
|
||||
git_commit: 'unknown',
|
||||
git_branch: 'unknown',
|
||||
|
||||
@@ -150,6 +150,7 @@ export const adminNewNavigationItems = [
|
||||
active: (route: any) => route.path.startsWith('/admin/site-config'),
|
||||
group: 'system'
|
||||
},
|
||||
|
||||
{
|
||||
key: 'version',
|
||||
label: '版本信息',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "res-db-web",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -33,7 +33,14 @@
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-8">
|
||||
<!-- 自动处理配置组 -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<div class="w-1 h-6 bg-blue-500 rounded-full"></div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">自动处理配置</h3>
|
||||
</div>
|
||||
|
||||
<!-- 自动处理 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
@@ -57,6 +64,108 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meilisearch搜索优化配置组 -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<div class="w-1 h-6 bg-green-500 rounded-full"></div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">搜索优化配置</h3>
|
||||
</div>
|
||||
|
||||
<!-- 启用Meilisearch -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">启用Meilisearch搜索优化</label>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">开启后,系统将使用Meilisearch提供更快的搜索体验</span>
|
||||
</div>
|
||||
<n-switch v-model:value="configForm.meilisearch_enabled" />
|
||||
</div>
|
||||
|
||||
<!-- Meilisearch服务器配置 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" :class="{ 'opacity-50': !configForm.meilisearch_enabled }">
|
||||
<!-- 服务器地址 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">服务器地址</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_host"
|
||||
placeholder="localhost"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 端口 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">端口</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_port"
|
||||
placeholder="7700"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 主密钥 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">主密钥 (可选)</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_master_key"
|
||||
placeholder="留空表示无认证"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 索引名称 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">索引名称</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_index_name"
|
||||
placeholder="resources"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<n-button
|
||||
type="info"
|
||||
size="small"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
@click="testMeilisearchConnection"
|
||||
:loading="testingConnection"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-plug"></i>
|
||||
</template>
|
||||
测试连接
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="navigateTo('/admin/meilisearch-management')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-cogs"></i>
|
||||
</template>
|
||||
搜索优化管理
|
||||
</n-button>
|
||||
|
||||
<!-- 健康状态和未同步数量显示 -->
|
||||
<div v-if="meilisearchStatus" class="flex items-center space-x-4 ml-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-2 h-2 rounded-full" :class="meilisearchStatus.healthy ? 'bg-green-500' : 'bg-red-500'"></div>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">健康状态: {{ meilisearchStatus.healthy ? '正常' : '异常' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fas fa-sync-alt text-purple-500"></i>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">未同步: {{ unsyncedCount || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -184,6 +293,11 @@ interface FeatureConfigForm {
|
||||
ad_keywords: string
|
||||
auto_insert_ad: string
|
||||
hot_drama_auto_fetch: boolean
|
||||
meilisearch_enabled: boolean
|
||||
meilisearch_host: string
|
||||
meilisearch_port: string
|
||||
meilisearch_master_key: string
|
||||
meilisearch_index_name: string
|
||||
}
|
||||
|
||||
// 使用配置改动检测
|
||||
@@ -204,13 +318,23 @@ const {
|
||||
auto_transfer_min_space: 'auto_transfer_min_space',
|
||||
ad_keywords: 'ad_keywords',
|
||||
auto_insert_ad: 'auto_insert_ad',
|
||||
hot_drama_auto_fetch: 'auto_fetch_hot_drama_enabled'
|
||||
hot_drama_auto_fetch: 'auto_fetch_hot_drama_enabled',
|
||||
meilisearch_enabled: 'meilisearch_enabled',
|
||||
meilisearch_host: 'meilisearch_host',
|
||||
meilisearch_port: 'meilisearch_port',
|
||||
meilisearch_master_key: 'meilisearch_master_key',
|
||||
meilisearch_index_name: 'meilisearch_index_name'
|
||||
}
|
||||
})
|
||||
|
||||
const notification = useNotification()
|
||||
const saving = ref(false)
|
||||
const activeTab = ref('resource')
|
||||
const testingConnection = ref(false)
|
||||
|
||||
// Meilisearch状态
|
||||
const meilisearchStatus = ref<any>(null)
|
||||
const unsyncedCount = ref(0)
|
||||
|
||||
// 配置表单数据
|
||||
const configForm = ref<FeatureConfigForm>({
|
||||
@@ -220,7 +344,12 @@ const configForm = ref<FeatureConfigForm>({
|
||||
auto_transfer_min_space: '500',
|
||||
ad_keywords: '',
|
||||
auto_insert_ad: '',
|
||||
hot_drama_auto_fetch: false
|
||||
hot_drama_auto_fetch: false,
|
||||
meilisearch_enabled: false,
|
||||
meilisearch_host: '',
|
||||
meilisearch_port: '',
|
||||
meilisearch_master_key: '',
|
||||
meilisearch_index_name: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
@@ -241,7 +370,12 @@ const fetchConfig = async () => {
|
||||
auto_transfer_min_space: String(response.auto_transfer_min_space || 500),
|
||||
ad_keywords: response.ad_keywords || '',
|
||||
auto_insert_ad: response.auto_insert_ad || '',
|
||||
hot_drama_auto_fetch: response.auto_fetch_hot_drama_enabled || false
|
||||
hot_drama_auto_fetch: response.auto_fetch_hot_drama_enabled || false,
|
||||
meilisearch_enabled: response.meilisearch_enabled || false,
|
||||
meilisearch_host: response.meilisearch_host || '',
|
||||
meilisearch_port: String(response.meilisearch_port || 7700),
|
||||
meilisearch_master_key: response.meilisearch_master_key || '',
|
||||
meilisearch_index_name: response.meilisearch_index_name || 'resources'
|
||||
}
|
||||
|
||||
configForm.value = { ...configData }
|
||||
@@ -269,7 +403,12 @@ const saveConfig = async () => {
|
||||
auto_transfer_min_space: configForm.value.auto_transfer_min_space,
|
||||
ad_keywords: configForm.value.ad_keywords,
|
||||
auto_insert_ad: configForm.value.auto_insert_ad,
|
||||
hot_drama_auto_fetch: configForm.value.hot_drama_auto_fetch
|
||||
hot_drama_auto_fetch: configForm.value.hot_drama_auto_fetch,
|
||||
meilisearch_enabled: configForm.value.meilisearch_enabled,
|
||||
meilisearch_host: configForm.value.meilisearch_host,
|
||||
meilisearch_port: configForm.value.meilisearch_port,
|
||||
meilisearch_master_key: configForm.value.meilisearch_master_key,
|
||||
meilisearch_index_name: configForm.value.meilisearch_index_name
|
||||
})
|
||||
|
||||
const { useSystemConfigApi } = await import('~/composables/useApi')
|
||||
@@ -327,9 +466,70 @@ const saveConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试Meilisearch连接
|
||||
const testMeilisearchConnection = async () => {
|
||||
testingConnection.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.testConnection({
|
||||
host: configForm.value.meilisearch_host,
|
||||
port: parseInt(configForm.value.meilisearch_port, 10),
|
||||
masterKey: configForm.value.meilisearch_master_key,
|
||||
indexName: configForm.value.meilisearch_index_name || 'resources'
|
||||
})
|
||||
notification.success({
|
||||
content: 'Meilisearch连接测试成功!',
|
||||
duration: 3000
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Meilisearch连接测试失败:', error)
|
||||
notification.error({
|
||||
content: `Meilisearch连接测试失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
testingConnection.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Meilisearch状态
|
||||
const fetchMeilisearchStatus = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const status = await meilisearchApi.getStatus()
|
||||
meilisearchStatus.value = status
|
||||
} catch (error: any) {
|
||||
console.error('获取Meilisearch状态失败:', error)
|
||||
notification.error({
|
||||
content: `获取Meilisearch状态失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未同步文档数量
|
||||
const fetchUnsyncedCount = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const response = await meilisearchApi.getUnsyncedCount() as any
|
||||
unsyncedCount.value = response?.count || 0
|
||||
} catch (error: any) {
|
||||
console.error('获取未同步文档数量失败:', error)
|
||||
notification.error({
|
||||
content: `获取未同步文档数量失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取配置
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
fetchMeilisearchStatus()
|
||||
fetchUnsyncedCount()
|
||||
})
|
||||
|
||||
|
||||
|
||||
764
web/pages/admin/meilisearch-management.vue
Normal file
764
web/pages/admin/meilisearch-management.vue
Normal file
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面标题 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">搜索优化管理</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">管理 Meilisearch 搜索服务状态和数据同步</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<n-button @click="refreshStatus" :loading="refreshing" :disabled="syncProgress.is_running">
|
||||
<template #icon>
|
||||
<i class="fas fa-refresh"></i>
|
||||
</template>
|
||||
刷新状态
|
||||
</n-button>
|
||||
<n-button @click="navigateTo('/admin/feature-config')" type="info" :disabled="syncProgress.is_running">
|
||||
<template #icon>
|
||||
<i class="fas fa-cog"></i>
|
||||
</template>
|
||||
配置设置
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态卡片 -->
|
||||
<n-card class="mb-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<!-- 启用状态 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-power-off text-sm" :class="status.enabled ? 'text-green-500' : 'text-red-500'"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">启用状态</p>
|
||||
<p class="text-sm font-medium" :class="status.enabled ? 'text-green-600' : 'text-red-600'">
|
||||
{{ status.enabled ? '已启用' : '未启用' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 健康状态 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-heartbeat text-sm" :class="status.healthy ? 'text-green-500' : 'text-red-500'"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">健康状态</p>
|
||||
<p class="text-sm font-medium" :class="status.healthy ? 'text-green-600' : 'text-red-600'">
|
||||
{{ status.healthy ? '正常' : '异常' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文档数量 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-database text-sm text-blue-500"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">索引文档</p>
|
||||
<p class="text-sm font-medium text-blue-600">{{ status.documentCount || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最后检查时间 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-clock text-sm text-purple-500"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">最后检查</p>
|
||||
<p class="text-xs font-medium text-purple-600">{{ formatTime(status.lastCheck) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="status.lastError" class="mt-3 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
|
||||
<div class="flex items-start space-x-2">
|
||||
<i class="fas fa-exclamation-triangle text-red-500 mt-0.5 text-sm"></i>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-red-800 dark:text-red-200">错误信息</p>
|
||||
<p class="text-xs text-red-700 dark:text-red-300">{{ status.lastError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 数据同步管理 -->
|
||||
<n-card class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<!-- 标题、过滤条件和操作按钮 -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-lg font-semibold">资源列表</h4>
|
||||
<div class="flex space-x-3">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="syncAllResources"
|
||||
:loading="syncing"
|
||||
:disabled="unsyncedCount === 0 || syncProgress.is_running"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-upload"></i>
|
||||
</template>
|
||||
同步所有资源
|
||||
</n-button>
|
||||
<!-- 停止同步按钮已隐藏 -->
|
||||
<n-button
|
||||
type="error"
|
||||
@click="clearIndex"
|
||||
:loading="clearing"
|
||||
:disabled="syncProgress.is_running"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
清空索引
|
||||
</n-button>
|
||||
<!-- <n-button
|
||||
type="info"
|
||||
@click="updateIndexSettings"
|
||||
:loading="updatingSettings"
|
||||
:disabled="syncProgress.is_running"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-cogs"></i>
|
||||
</template>
|
||||
更新索引设置
|
||||
</n-button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 过滤条件 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">同步状态:</span>
|
||||
<n-select
|
||||
v-model:value="syncFilter"
|
||||
:options="syncFilterOptions"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
:disabled="syncProgress.is_running"
|
||||
@update:value="onSyncFilterChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">总计: {{ totalCount }} 个</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 同步进度显示 -->
|
||||
<div v-if="syncProgress.is_running" class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h5 class="text-sm font-medium text-blue-800 dark:text-blue-200">同步进度</h5>
|
||||
<span class="text-xs text-blue-600 dark:text-blue-300">
|
||||
批次 {{ syncProgress.current_batch }}/{{ syncProgress.total_batches }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="w-full bg-blue-200 dark:bg-blue-800 rounded-full h-2 mb-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: progressPercentage + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">已同步:</span>
|
||||
<span class="font-medium">{{ syncProgress.synced_count }}/{{ syncProgress.total_count }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">进度:</span>
|
||||
<span class="font-medium">{{ progressPercentage.toFixed(1) }}%</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">预估剩余:</span>
|
||||
<span class="font-medium">{{ syncProgress.estimated_time || '计算中...' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">开始时间:</span>
|
||||
<span class="font-medium">{{ formatTime(syncProgress.start_time) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="syncProgress.error_message" class="mt-2 p-2 bg-red-100 dark:bg-red-900/20 rounded text-xs text-red-700 dark:text-red-300">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
{{ syncProgress.error_message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div v-if="resources.length > 0">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="resources"
|
||||
:pagination="pagination"
|
||||
:max-height="400"
|
||||
virtual-scroll
|
||||
:loading="loadingResources"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!loadingResources" class="text-center py-8 text-gray-500">
|
||||
暂无资源数据
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useNotification, useDialog } from 'naive-ui'
|
||||
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const notification = useNotification()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 状态数据
|
||||
const status = ref({
|
||||
enabled: false,
|
||||
healthy: false,
|
||||
documentCount: 0,
|
||||
lastCheck: null as Date | null,
|
||||
lastError: '',
|
||||
errorCount: 0
|
||||
})
|
||||
|
||||
const systemConfig = ref({
|
||||
meilisearch_host: '',
|
||||
meilisearch_port: '',
|
||||
meilisearch_master_key: '',
|
||||
meilisearch_index_name: ''
|
||||
})
|
||||
|
||||
// 定义资源类型
|
||||
interface Resource {
|
||||
id: number
|
||||
title: string
|
||||
category?: {
|
||||
name: string
|
||||
}
|
||||
synced_to_meilisearch: boolean
|
||||
synced_at?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 同步状态过滤选项
|
||||
const syncFilterOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '已同步', value: 'synced' },
|
||||
{ label: '未同步', value: 'unsynced' }
|
||||
]
|
||||
|
||||
const syncFilter = ref('unsynced') // 默认显示未同步
|
||||
const totalCount = ref(0)
|
||||
const resources = ref<Resource[]>([])
|
||||
const unsyncedCount = ref(0)
|
||||
|
||||
// 加载状态
|
||||
const refreshing = ref(false)
|
||||
const syncing = ref(false)
|
||||
const clearing = ref(false)
|
||||
const updatingSettings = ref(false)
|
||||
const loadingResources = ref(false)
|
||||
const stopping = ref(false)
|
||||
|
||||
// 同步进度
|
||||
const syncProgress = ref({
|
||||
is_running: false,
|
||||
total_count: 0,
|
||||
processed_count: 0,
|
||||
synced_count: 0,
|
||||
failed_count: 0,
|
||||
start_time: null as Date | null,
|
||||
estimated_time: '',
|
||||
current_batch: 0,
|
||||
total_batches: 0,
|
||||
error_message: ''
|
||||
})
|
||||
|
||||
// 计算进度百分比
|
||||
const progressPercentage = computed(() => {
|
||||
if (syncProgress.value.total_count === 0) return 0
|
||||
return (syncProgress.value.synced_count / syncProgress.value.total_count) * 100
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [500, 1000, 2000],
|
||||
onChange: (page: number) => {
|
||||
pagination.value.page = page
|
||||
fetchResources()
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
fetchResources()
|
||||
}
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
render: (row: Resource) => {
|
||||
return row.category?.name || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '同步状态',
|
||||
key: 'synced_to_meilisearch',
|
||||
width: 100,
|
||||
render: (row: Resource) => {
|
||||
return row.synced_to_meilisearch ? '已同步' : '未同步'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '同步时间',
|
||||
key: 'synced_at',
|
||||
width: 180,
|
||||
render: (row: Resource) => {
|
||||
return row.synced_at ? formatTime(row.synced_at) : '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
render: (row: Resource) => {
|
||||
return formatTime(row.created_at)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: Date | string | null) => {
|
||||
if (!time) return '未知'
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取状态
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const response = await meilisearchApi.getStatus() as any
|
||||
|
||||
if (response) {
|
||||
status.value = {
|
||||
enabled: response.enabled || false,
|
||||
healthy: response.healthy || false,
|
||||
documentCount: response.document_count || response.documentCount || 0,
|
||||
lastCheck: response.last_check ? new Date(response.last_check) : response.lastCheck ? new Date(response.lastCheck) : null,
|
||||
lastError: response.last_error || response.lastError || '',
|
||||
errorCount: response.error_count || response.errorCount || 0
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取状态失败:', error)
|
||||
notification.error({
|
||||
content: `获取状态失败: ${error?.message || error}`,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取系统配置
|
||||
const fetchSystemConfig = async () => {
|
||||
try {
|
||||
const { useSystemConfigApi } = await import('~/composables/useApi')
|
||||
const systemConfigApi = useSystemConfigApi()
|
||||
const response = await systemConfigApi.getSystemConfig() as any
|
||||
|
||||
if (response) {
|
||||
systemConfig.value = {
|
||||
meilisearch_host: response.meilisearch_host || '',
|
||||
meilisearch_port: response.meilisearch_port || '',
|
||||
meilisearch_master_key: response.meilisearch_master_key || '',
|
||||
meilisearch_index_name: response.meilisearch_index_name || ''
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未同步数量
|
||||
const fetchUnsyncedCount = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const response = await meilisearchApi.getUnsyncedCount() as any
|
||||
|
||||
if (response) {
|
||||
unsyncedCount.value = response.count || 0
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取未同步数量失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态
|
||||
const refreshStatus = async () => {
|
||||
refreshing.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchStatus(),
|
||||
fetchSystemConfig(),
|
||||
fetchUnsyncedCount()
|
||||
])
|
||||
notification.success({
|
||||
content: '状态刷新成功',
|
||||
duration: 2000
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('刷新状态失败:', error)
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 同步所有资源
|
||||
const syncAllResources = async () => {
|
||||
syncing.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.syncAllResources()
|
||||
|
||||
notification.success({
|
||||
content: '同步已开始,请查看进度',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 开始轮询进度
|
||||
startProgressPolling()
|
||||
} catch (error: any) {
|
||||
console.error('同步资源失败:', error)
|
||||
notification.error({
|
||||
content: `同步资源失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 停止同步
|
||||
const stopSync = async () => {
|
||||
stopping.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.stopSync()
|
||||
|
||||
notification.success({
|
||||
content: '同步已停止',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 立即更新进度状态为已停止
|
||||
syncProgress.value.is_running = false
|
||||
syncProgress.value.error_message = '同步已停止'
|
||||
|
||||
// 停止轮询
|
||||
stopProgressPolling()
|
||||
|
||||
// 刷新状态
|
||||
await refreshStatus()
|
||||
} catch (error: any) {
|
||||
console.error('停止同步失败:', error)
|
||||
notification.error({
|
||||
content: `停止同步失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
stopping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 进度轮询
|
||||
let progressInterval: NodeJS.Timeout | null = null
|
||||
|
||||
const startProgressPolling = () => {
|
||||
// 清除之前的轮询
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval)
|
||||
}
|
||||
|
||||
// 立即获取一次进度
|
||||
fetchSyncProgress()
|
||||
|
||||
// 每2秒轮询一次
|
||||
progressInterval = setInterval(() => {
|
||||
fetchSyncProgress()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const stopProgressPolling = () => {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval)
|
||||
progressInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSyncProgress = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const progress = await meilisearchApi.getSyncProgress() as any
|
||||
|
||||
if (progress) {
|
||||
syncProgress.value = {
|
||||
is_running: progress.is_running || false,
|
||||
total_count: progress.total_count || 0,
|
||||
processed_count: progress.processed_count || 0,
|
||||
synced_count: progress.synced_count || 0,
|
||||
failed_count: progress.failed_count || 0,
|
||||
start_time: progress.start_time ? new Date(progress.start_time) : null,
|
||||
estimated_time: progress.estimated_time || '',
|
||||
current_batch: progress.current_batch || 0,
|
||||
total_batches: progress.total_batches || 0,
|
||||
error_message: progress.error_message || ''
|
||||
}
|
||||
|
||||
// 如果同步完成或出错,停止轮询
|
||||
if (!progress.is_running) {
|
||||
stopProgressPolling()
|
||||
|
||||
// 只有在有同步进度时才显示完成消息
|
||||
if (progress.synced_count > 0 || progress.error_message) {
|
||||
if (progress.error_message) {
|
||||
notification.error({
|
||||
content: `同步失败: ${progress.error_message}`,
|
||||
duration: 5000
|
||||
})
|
||||
} else {
|
||||
notification.success({
|
||||
content: `同步完成,共同步 ${progress.synced_count} 个资源`,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态和表格
|
||||
await Promise.all([
|
||||
refreshStatus(),
|
||||
fetchResources()
|
||||
])
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取同步进度失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 静默获取同步进度,不显示任何提示
|
||||
const fetchSyncProgressSilent = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const progress = await meilisearchApi.getSyncProgress() as any
|
||||
|
||||
if (progress) {
|
||||
syncProgress.value = {
|
||||
is_running: progress.is_running || false,
|
||||
total_count: progress.total_count || 0,
|
||||
processed_count: progress.processed_count || 0,
|
||||
synced_count: progress.synced_count || 0,
|
||||
failed_count: progress.failed_count || 0,
|
||||
start_time: progress.start_time ? new Date(progress.start_time) : null,
|
||||
estimated_time: progress.estimated_time || '',
|
||||
current_batch: progress.current_batch || 0,
|
||||
total_batches: progress.total_batches || 0,
|
||||
error_message: progress.error_message || ''
|
||||
}
|
||||
|
||||
// 如果同步完成或出错,停止轮询
|
||||
if (!progress.is_running) {
|
||||
stopProgressPolling()
|
||||
|
||||
// 静默刷新状态和表格,不显示任何提示
|
||||
await Promise.all([
|
||||
refreshStatus(),
|
||||
fetchResources()
|
||||
])
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取同步进度失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 同步状态过滤变化处理
|
||||
const onSyncFilterChange = () => {
|
||||
pagination.value.page = 1
|
||||
fetchResources()
|
||||
}
|
||||
|
||||
// 获取资源列表
|
||||
const fetchResources = async () => {
|
||||
loadingResources.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
|
||||
let response: any
|
||||
if (syncFilter.value === 'unsynced') {
|
||||
// 获取未同步资源
|
||||
response = await meilisearchApi.getUnsyncedResources({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
} else if (syncFilter.value === 'synced') {
|
||||
// 获取已同步资源
|
||||
response = await meilisearchApi.getSyncedResources({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
} else {
|
||||
// 获取所有资源
|
||||
response = await meilisearchApi.getAllResources({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
}
|
||||
|
||||
if (response && response.resources) {
|
||||
resources.value = response.resources
|
||||
totalCount.value = response.total || 0
|
||||
// 更新分页信息
|
||||
if (response.total !== undefined) {
|
||||
pagination.value.itemCount = response.total
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取资源失败:', error)
|
||||
notification.error({
|
||||
content: `获取资源失败: ${error?.message || error}`,
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
loadingResources.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未同步资源(保留兼容性)
|
||||
const fetchUnsyncedResources = async () => {
|
||||
syncFilter.value = 'unsynced'
|
||||
await fetchResources()
|
||||
}
|
||||
|
||||
// 清空索引
|
||||
const clearIndex = async () => {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
dialog.error({
|
||||
title: '确认清空索引',
|
||||
content: '此操作将清空所有 Meilisearch 索引数据,确定要继续吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: resolve,
|
||||
onNegativeClick: reject
|
||||
})
|
||||
})
|
||||
|
||||
clearing.value = true
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.clearIndex()
|
||||
|
||||
notification.success({
|
||||
content: '索引清空成功',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 刷新状态
|
||||
await refreshStatus()
|
||||
} catch (error: any) {
|
||||
if (error) {
|
||||
console.error('清空索引失败:', error)
|
||||
notification.error({
|
||||
content: `清空索引失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
clearing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
const updateIndexSettings = async () => {
|
||||
updatingSettings.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.updateIndexSettings()
|
||||
|
||||
notification.success({
|
||||
content: '索引设置已更新',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 刷新状态
|
||||
await refreshStatus()
|
||||
} catch (error: any) {
|
||||
console.error('更新索引设置失败:', error)
|
||||
notification.error({
|
||||
content: `更新索引设置失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
updatingSettings.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
refreshStatus()
|
||||
fetchResources()
|
||||
// 静默检查同步进度,不显示任何提示
|
||||
fetchSyncProgressSilent().then(() => {
|
||||
// 如果检测到有同步在进行,开始轮询
|
||||
if (syncProgress.value.is_running) {
|
||||
startProgressPolling()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 页面卸载时清理轮询
|
||||
onUnmounted(() => {
|
||||
stopProgressPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
</style>
|
||||
@@ -59,7 +59,7 @@
|
||||
: '需要手动处理待处理资源'
|
||||
}}
|
||||
</div>
|
||||
<n-button
|
||||
<!-- <n-button
|
||||
@click="refreshConfig"
|
||||
:disabled="updatingConfig"
|
||||
size="small"
|
||||
@@ -69,7 +69,7 @@
|
||||
<template #icon>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-button> -->
|
||||
<n-button
|
||||
@click="toggleAutoProcess"
|
||||
:disabled="updatingConfig"
|
||||
|
||||
@@ -188,14 +188,20 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="text-center py-12">
|
||||
<div class="text-gray-400 dark:text-gray-500 mb-4">
|
||||
<i class="fas fa-film text-6xl"></i>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center space-y-4">
|
||||
<img
|
||||
src="/assets/svg/empty.svg"
|
||||
alt="暂无热播剧数据"
|
||||
class="!w-64 !h-64 sm:w-64 sm:h-64 opacity-60 dark:opacity-40"
|
||||
/>
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">暂无热播剧数据</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">请稍后再试或联系管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<AppFooter />
|
||||
|
||||
@@ -135,9 +135,24 @@
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="safeResources.length === 0" class="text-center py-8">
|
||||
<td colspan="1" class="text-gray-500 dark:text-gray-400 sm:hidden">暂无数据</td>
|
||||
<td colspan="3" class="text-gray-500 dark:text-gray-400 hidden sm:table-cell">暂无数据</td>
|
||||
<tr v-else-if="safeResources.length === 0" class="text-center py-12">
|
||||
<td colspan="3" class="text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center justify-center space-y-4">
|
||||
<img
|
||||
src="/assets/svg/empty.svg"
|
||||
alt="暂无数据"
|
||||
class="!w-64 !h-64 sm:w-64 sm:h-64 opacity-60 dark:opacity-40"
|
||||
/>
|
||||
<div class="text-center">
|
||||
<p class="text-lg font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||
{{ searchQuery ? '没有找到相关资源' : '暂无资源数据' }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500">
|
||||
{{ searchQuery ? '请尝试其他关键词或清除搜索条件' : '资源正在整理中,请稍后再来查看' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(resource, index) in safeResources"
|
||||
@@ -149,10 +164,9 @@
|
||||
<div class="flex items-start">
|
||||
<span class="mr-2 flex-shrink-0" v-html="getPlatformIcon(resource.pan_id || 0)"></span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="break-words font-medium">{{ resource.title }}</div>
|
||||
<div class="break-words font-medium" v-html="resource.title_highlight || resource.title"></div>
|
||||
<!-- 显示描述 -->
|
||||
<div v-if="resource.description" class="text-xs text-gray-600 dark:text-gray-400 mt-1 break-words line-clamp-2">
|
||||
{{ resource.description }}
|
||||
<div v-if="resource.description_highlight || resource.description" class="text-xs text-gray-600 dark:text-gray-400 mt-1 break-words line-clamp-2" v-html="resource.description_highlight || resource.description">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,6 +216,8 @@
|
||||
:platform="selectedResource?.platform"
|
||||
:message="selectedResource?.message"
|
||||
:error="selectedResource?.error"
|
||||
:forbidden="selectedResource?.forbidden"
|
||||
:forbidden_words="selectedResource?.forbidden_words"
|
||||
@close="showLinkModal = false"
|
||||
/>
|
||||
|
||||
@@ -273,12 +289,24 @@ const handleLogoError = (event: Event) => {
|
||||
// 使用 useAsyncData 获取资源数据
|
||||
const { data: resourcesData, pending, refresh } = await useAsyncData(
|
||||
() => `resources-1-${route.query.search || ''}-${route.query.platform || ''}`,
|
||||
() => resourceApi.getResources({
|
||||
async () => {
|
||||
// 如果有搜索关键词,使用带搜索参数的资源接口(后端会优先使用Meilisearch)
|
||||
if (route.query.search) {
|
||||
return await resourceApi.getResources({
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
search: route.query.search as string || '',
|
||||
search: route.query.search as string,
|
||||
pan_id: route.query.platform as string || ''
|
||||
})
|
||||
} else {
|
||||
// 没有搜索关键词时,使用普通资源接口获取最新数据
|
||||
return await resourceApi.getResources({
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
pan_id: route.query.platform as string || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 获取统计数据
|
||||
@@ -399,8 +427,8 @@ onMounted(() => {
|
||||
|
||||
|
||||
// 获取平台名称
|
||||
const getPlatformIcon = (panId: string) => {
|
||||
const platform = (platforms.value as any).find((p: any) => p.id === panId)
|
||||
const getPlatformIcon = (panId: string | number) => {
|
||||
const platform = (platforms.value as any).find((p: any) => p.id == panId)
|
||||
return platform?.icon || '未知平台'
|
||||
}
|
||||
|
||||
@@ -408,6 +436,18 @@ const getPlatformIcon = (panId: string) => {
|
||||
|
||||
// 切换链接显示
|
||||
const toggleLink = async (resource: any) => {
|
||||
// 如果包含违禁词,直接显示禁止访问,不发送请求
|
||||
if (resource.has_forbidden_words) {
|
||||
selectedResource.value = {
|
||||
...resource,
|
||||
forbidden: true,
|
||||
error: '该资源包含违禁内容,无法访问',
|
||||
forbidden_words: resource.forbidden_words || []
|
||||
}
|
||||
showLinkModal.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
selectedResource.value = { ...resource, loading: true }
|
||||
showLinkModal.value = true
|
||||
@@ -427,9 +467,10 @@ const toggleLink = async (resource: any) => {
|
||||
platform: linkData.platform,
|
||||
message: linkData.message
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('获取资源链接失败:', error)
|
||||
// 出错时使用原始资源信息
|
||||
|
||||
// 其他错误
|
||||
selectedResource.value = {
|
||||
...resource,
|
||||
loading: false,
|
||||
|
||||
1
web/public/assets/svg/empty.svg
Normal file
1
web/public/assets/svg/empty.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 31 KiB |
1
web/public/assets/svg/forbidden.svg
Normal file
1
web/public/assets/svg/forbidden.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
1
web/public/assets/svg/qrcode.svg
Normal file
1
web/public/assets/svg/qrcode.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 34 KiB |
1
web/public/assets/svg/risk.svg
Normal file
1
web/public/assets/svg/risk.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
@@ -52,21 +52,21 @@ export const useUserStore = defineStore('user', {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const userStr = localStorage.getItem('user')
|
||||
console.log('initAuth - token:', token ? 'exists' : 'not found')
|
||||
console.log('initAuth - userStr:', userStr ? 'exists' : 'not found')
|
||||
// console.log('initAuth - token:', token ? 'exists' : 'not found')
|
||||
// console.log('initAuth - userStr:', userStr ? 'exists' : 'not found')
|
||||
|
||||
if (token && userStr) {
|
||||
try {
|
||||
this.token = token
|
||||
this.user = JSON.parse(userStr)
|
||||
this.isAuthenticated = true
|
||||
console.log('initAuth - 状态恢复成功:', this.user?.username)
|
||||
// console.log('initAuth - 状态恢复成功:', this.user?.username)
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
this.logout()
|
||||
}
|
||||
} else {
|
||||
console.log('initAuth - 没有找到有效的登录信息')
|
||||
// console.log('initAuth - 没有找到有效的登录信息')
|
||||
// 确保状态一致
|
||||
this.token = null
|
||||
this.user = null
|
||||
|
||||
Reference in New Issue
Block a user