mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update: config
This commit is contained in:
128
db/repo/hot_drama_repository.go
Normal file
128
db/repo/hot_drama_repository.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"res_db/db/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HotDramaRepository 热播剧仓储接口
|
||||
type HotDramaRepository interface {
|
||||
Create(drama *entity.HotDrama) error
|
||||
FindByID(id uint) (*entity.HotDrama, error)
|
||||
FindAll(page, pageSize int) ([]entity.HotDrama, int64, error)
|
||||
FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error)
|
||||
FindByDoubanID(doubanID string) (*entity.HotDrama, error)
|
||||
Upsert(drama *entity.HotDrama) error
|
||||
Delete(id uint) error
|
||||
DeleteByDoubanID(doubanID string) error
|
||||
DeleteOldRecords(days int) error
|
||||
}
|
||||
|
||||
// hotDramaRepository 热播剧仓储实现
|
||||
type hotDramaRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewHotDramaRepository 创建热播剧仓储实例
|
||||
func NewHotDramaRepository(db *gorm.DB) HotDramaRepository {
|
||||
return &hotDramaRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建热播剧记录
|
||||
func (r *hotDramaRepository) Create(drama *entity.HotDrama) error {
|
||||
return r.db.Create(drama).Error
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找热播剧
|
||||
func (r *hotDramaRepository) FindByID(id uint) (*entity.HotDrama, error) {
|
||||
var drama entity.HotDrama
|
||||
err := r.db.First(&drama, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &drama, nil
|
||||
}
|
||||
|
||||
// FindAll 查找所有热播剧(分页)
|
||||
func (r *hotDramaRepository) FindAll(page, pageSize int) ([]entity.HotDrama, int64, error) {
|
||||
var dramas []entity.HotDrama
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 获取总数
|
||||
if err := r.db.Model(&entity.HotDrama{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return dramas, total, nil
|
||||
}
|
||||
|
||||
// FindByCategory 根据分类查找热播剧(分页)
|
||||
func (r *hotDramaRepository) FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error) {
|
||||
var dramas []entity.HotDrama
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 获取总数
|
||||
if err := r.db.Model(&entity.HotDrama{}).Where("category = ?", category).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := r.db.Where("category = ?", category).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return dramas, total, nil
|
||||
}
|
||||
|
||||
// FindByDoubanID 根据豆瓣ID查找热播剧
|
||||
func (r *hotDramaRepository) FindByDoubanID(doubanID string) (*entity.HotDrama, error) {
|
||||
var drama entity.HotDrama
|
||||
err := r.db.Where("douban_id = ?", doubanID).First(&drama).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &drama, nil
|
||||
}
|
||||
|
||||
// Upsert 插入或更新热播剧记录
|
||||
func (r *hotDramaRepository) Upsert(drama *entity.HotDrama) error {
|
||||
if drama.DoubanID != "" {
|
||||
// 如果存在豆瓣ID,先尝试查找现有记录
|
||||
existing, err := r.FindByDoubanID(drama.DoubanID)
|
||||
if err == nil && existing != nil {
|
||||
// 更新现有记录
|
||||
drama.ID = existing.ID
|
||||
return r.db.Save(drama).Error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新记录
|
||||
return r.Create(drama)
|
||||
}
|
||||
|
||||
// Delete 删除热播剧记录
|
||||
func (r *hotDramaRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&entity.HotDrama{}, id).Error
|
||||
}
|
||||
|
||||
// DeleteByDoubanID 根据豆瓣ID删除热播剧记录
|
||||
func (r *hotDramaRepository) DeleteByDoubanID(doubanID string) error {
|
||||
return r.db.Where("douban_id = ?", doubanID).Delete(&entity.HotDrama{}).Error
|
||||
}
|
||||
|
||||
// DeleteOldRecords 删除指定天数前的旧记录
|
||||
func (r *hotDramaRepository) DeleteOldRecords(days int) error {
|
||||
return r.db.Where("created_at < NOW() - INTERVAL '? days'", days).Delete(&entity.HotDrama{}).Error
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type RepositoryManager struct {
|
||||
UserRepository UserRepository
|
||||
SearchStatRepository SearchStatRepository
|
||||
SystemConfigRepository SystemConfigRepository
|
||||
HotDramaRepository HotDramaRepository
|
||||
}
|
||||
|
||||
// NewRepositoryManager 创建Repository管理器
|
||||
@@ -29,5 +30,6 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
|
||||
UserRepository: NewUserRepository(db),
|
||||
SearchStatRepository: NewSearchStatRepository(db),
|
||||
SystemConfigRepository: NewSystemConfigRepository(db),
|
||||
HotDramaRepository: NewHotDramaRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"res_db/db/entity"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -10,25 +12,33 @@ import (
|
||||
type ResourceRepository interface {
|
||||
BaseRepository[entity.Resource]
|
||||
FindWithRelations() ([]entity.Resource, error)
|
||||
FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error)
|
||||
FindByCategoryID(categoryID uint) ([]entity.Resource, error)
|
||||
FindByCategoryIDPaginated(categoryID uint, page, limit int) ([]entity.Resource, int64, error)
|
||||
FindByPanID(panID uint) ([]entity.Resource, error)
|
||||
FindByPanIDPaginated(panID uint, page, limit int) ([]entity.Resource, int64, error)
|
||||
FindByIsValid(isValid bool) ([]entity.Resource, error)
|
||||
FindByIsPublic(isPublic bool) ([]entity.Resource, error)
|
||||
Search(query string, categoryID *uint, page, limit int) ([]entity.Resource, int64, error)
|
||||
IncrementViewCount(id uint) error
|
||||
FindWithTags() ([]entity.Resource, error)
|
||||
UpdateWithTags(resource *entity.Resource, tagIDs []uint) error
|
||||
GetLatestResources(limit int) ([]entity.Resource, error)
|
||||
GetCachedLatestResources(limit int) ([]entity.Resource, error)
|
||||
InvalidateCache() error
|
||||
}
|
||||
|
||||
// ResourceRepositoryImpl Resource的Repository实现
|
||||
type ResourceRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.Resource]
|
||||
cache map[string]interface{}
|
||||
}
|
||||
|
||||
// NewResourceRepository 创建Resource Repository
|
||||
func NewResourceRepository(db *gorm.DB) ResourceRepository {
|
||||
return &ResourceRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.Resource]{db: db},
|
||||
cache: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +49,24 @@ func (r *ResourceRepositoryImpl) FindWithRelations() ([]entity.Resource, error)
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// FindWithRelationsPaginated 分页查找包含关联关系的资源
|
||||
func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
db := r.db.Model(&entity.Resource{}).Preload("Category").Preload("Pan").Preload("Tags")
|
||||
|
||||
// 获取总数
|
||||
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
|
||||
}
|
||||
|
||||
// FindByCategoryID 根据分类ID查找
|
||||
func (r *ResourceRepositoryImpl) FindByCategoryID(categoryID uint) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
@@ -46,6 +74,24 @@ func (r *ResourceRepositoryImpl) FindByCategoryID(categoryID uint) ([]entity.Res
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// FindByCategoryIDPaginated 分页根据分类ID查找
|
||||
func (r *ResourceRepositoryImpl) FindByCategoryIDPaginated(categoryID uint, 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("category_id = ?", categoryID).Preload("Category").Preload("Tags")
|
||||
|
||||
// 获取总数
|
||||
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
|
||||
}
|
||||
|
||||
// FindByPanID 根据平台ID查找
|
||||
func (r *ResourceRepositoryImpl) FindByPanID(panID uint) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
@@ -53,6 +99,24 @@ func (r *ResourceRepositoryImpl) FindByPanID(panID uint) ([]entity.Resource, err
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// FindByPanIDPaginated 分页根据平台ID查找
|
||||
func (r *ResourceRepositoryImpl) FindByPanIDPaginated(panID uint, 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("pan_id = ?", panID).Preload("Category").Preload("Tags")
|
||||
|
||||
// 获取总数
|
||||
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
|
||||
}
|
||||
|
||||
// FindByIsValid 根据有效性查找
|
||||
func (r *ResourceRepositoryImpl) FindByIsValid(isValid bool) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
@@ -134,3 +198,43 @@ func (r *ResourceRepositoryImpl) UpdateWithTags(resource *entity.Resource, tagID
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetLatestResources 获取最新资源
|
||||
func (r *ResourceRepositoryImpl) GetLatestResources(limit int) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
err := r.db.Order("created_at DESC").Limit(limit).Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// GetCachedLatestResources 获取缓存的最新资源
|
||||
func (r *ResourceRepositoryImpl) GetCachedLatestResources(limit int) ([]entity.Resource, error) {
|
||||
cacheKey := fmt.Sprintf("latest_resources_%d", limit)
|
||||
|
||||
// 检查缓存
|
||||
if cached, exists := r.cache[cacheKey]; exists {
|
||||
if resources, ok := cached.([]entity.Resource); ok {
|
||||
return resources, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
resources, err := r.GetLatestResources(limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缓存结果(5分钟过期)
|
||||
r.cache[cacheKey] = resources
|
||||
go func() {
|
||||
time.Sleep(5 * time.Minute)
|
||||
delete(r.cache, cacheKey)
|
||||
}()
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// InvalidateCache 清除缓存
|
||||
func (r *ResourceRepositoryImpl) InvalidateCache() error {
|
||||
r.cache = make(map[string]interface{})
|
||||
return nil
|
||||
}
|
||||
|
||||
123
doc/HOT_DRAMA_FEATURE.md
Normal file
123
doc/HOT_DRAMA_FEATURE.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 热播剧功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
热播剧功能是一个自动获取和展示豆瓣热门电影、电视剧榜单的功能模块。系统会定时从豆瓣获取最新的热门影视作品信息,并保存到数据库中供用户浏览。
|
||||
|
||||
## 主要特性
|
||||
|
||||
### 1. 自动数据获取
|
||||
- 每小时自动从豆瓣获取热门电影和电视剧数据
|
||||
- 支持电影和电视剧两个分类
|
||||
- 获取内容包括:剧名、评分、年份、导演、演员等详细信息
|
||||
|
||||
### 2. 数据存储
|
||||
- 创建专门的热播剧数据表 `hot_dramas`
|
||||
- 支持按豆瓣ID去重,避免重复数据
|
||||
- 记录数据来源和获取时间
|
||||
|
||||
### 3. 前端展示
|
||||
- 美观的卡片式布局展示热播剧信息
|
||||
- 支持按分类筛选(全部/电影/电视剧)
|
||||
- 分页显示,支持大量数据
|
||||
- 响应式设计,适配各种设备
|
||||
|
||||
### 4. 管理功能
|
||||
- 管理员可以手动启动/停止定时任务
|
||||
- 支持手动获取热播剧数据
|
||||
- 查看调度器运行状态
|
||||
|
||||
## 数据库结构
|
||||
|
||||
### hot_dramas 表
|
||||
```sql
|
||||
CREATE TABLE hot_dramas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
rating DECIMAL(3,1) DEFAULT 0.0,
|
||||
year VARCHAR(10),
|
||||
directors VARCHAR(500),
|
||||
actors VARCHAR(1000),
|
||||
category VARCHAR(50),
|
||||
sub_type VARCHAR(50),
|
||||
source VARCHAR(50) DEFAULT 'douban',
|
||||
douban_id VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 热播剧管理
|
||||
- `GET /api/hot-dramas` - 获取热播剧列表
|
||||
- `GET /api/hot-dramas/:id` - 获取热播剧详情
|
||||
- `POST /api/hot-dramas` - 创建热播剧记录(管理员)
|
||||
- `PUT /api/hot-dramas/:id` - 更新热播剧记录(管理员)
|
||||
- `DELETE /api/hot-dramas/:id` - 删除热播剧记录(管理员)
|
||||
|
||||
### 调度器管理
|
||||
- `GET /api/scheduler/status` - 获取调度器状态
|
||||
- `POST /api/scheduler/hot-drama/start` - 启动热播剧定时任务(管理员)
|
||||
- `POST /api/scheduler/hot-drama/stop` - 停止热播剧定时任务(管理员)
|
||||
- `GET /api/scheduler/hot-drama/names` - 手动获取热播剧名字(管理员)
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 系统配置
|
||||
在系统配置中有一个 `auto_fetch_hot_drama_enabled` 字段,用于控制是否启用自动获取热播剧功能:
|
||||
|
||||
- `true`: 启用自动获取,系统会根据配置的间隔时间自动获取数据
|
||||
- `false`: 禁用自动获取,需要管理员手动启动
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 启用功能
|
||||
1. 登录管理后台
|
||||
2. 进入系统配置页面
|
||||
3. 开启"自动拉取热播剧名字"选项
|
||||
4. 保存配置
|
||||
|
||||
### 2. 查看热播剧
|
||||
1. 在首页点击"热播剧"按钮
|
||||
2. 进入热播剧页面
|
||||
3. 可以按分类筛选查看
|
||||
4. 支持分页浏览
|
||||
|
||||
### 3. 管理定时任务
|
||||
1. 管理员可以手动启动/停止定时任务
|
||||
2. 可以查看调度器运行状态
|
||||
3. 可以手动触发数据获取
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 后端架构
|
||||
- **实体层**: `db/entity/hot_drama.go` - 定义热播剧数据结构
|
||||
- **DTO层**: `db/dto/hot_drama.go` - 定义数据传输对象
|
||||
- **转换器**: `db/converter/hot_drama_converter.go` - 实体与DTO转换
|
||||
- **仓储层**: `db/repo/hot_drama_repository.go` - 数据库操作
|
||||
- **处理器**: `handlers/hot_drama_handler.go` - API接口处理
|
||||
- **调度器**: `utils/scheduler.go` - 定时任务管理
|
||||
- **豆瓣服务**: `utils/douban_service.go` - 豆瓣API调用
|
||||
|
||||
### 前端实现
|
||||
- **页面**: `web/pages/hot-dramas.vue` - 热播剧展示页面
|
||||
- **导航**: 在首页添加热播剧入口
|
||||
- **样式**: 使用Tailwind CSS实现响应式设计
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据来源**: 数据来源于豆瓣移动端API,如果API不可用会使用模拟数据
|
||||
2. **频率限制**: 定时任务每小时执行一次,避免对豆瓣服务器造成压力
|
||||
3. **数据去重**: 系统会根据豆瓣ID进行去重,避免重复数据
|
||||
4. **权限控制**: 管理功能需要管理员权限
|
||||
5. **错误处理**: 系统具备完善的错误处理机制,确保稳定性
|
||||
|
||||
## 扩展功能
|
||||
|
||||
未来可以考虑添加的功能:
|
||||
1. 支持更多数据源(如IMDB、烂番茄等)
|
||||
2. 添加用户收藏功能
|
||||
3. 支持热播剧搜索
|
||||
4. 添加数据统计和分析功能
|
||||
5. 支持热播剧推荐算法
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func GetCategories(c *gin.Context) {
|
||||
categories, err := repoManager.CategoryRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ func GetCategories(c *gin.Context) {
|
||||
}
|
||||
|
||||
responses := converter.ToCategoryResponseList(categories, resourceCounts)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
SuccessResponse(c, responses)
|
||||
}
|
||||
|
||||
// CreateCategory 创建分类
|
||||
func CreateCategory(c *gin.Context) {
|
||||
var req dto.CreateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ func CreateCategory(c *gin.Context) {
|
||||
|
||||
err := repoManager.CategoryRepository.Create(category)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": category.ID,
|
||||
"message": "分类创建成功",
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "分类创建成功",
|
||||
"category": converter.ToCategoryResponse(category, 0),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,19 +63,19 @@ func UpdateCategory(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := repoManager.CategoryRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
|
||||
ErrorResponse(c, "分类不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ func UpdateCategory(c *gin.Context) {
|
||||
|
||||
err = repoManager.CategoryRepository.Update(category)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "分类更新成功"})
|
||||
SuccessResponse(c, gin.H{"message": "分类更新成功"})
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
@@ -100,15 +100,15 @@ func DeleteCategory(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.CategoryRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "分类删除成功"})
|
||||
SuccessResponse(c, gin.H{"message": "分类删除成功"})
|
||||
}
|
||||
|
||||
@@ -15,19 +15,19 @@ import (
|
||||
func GetCks(c *gin.Context) {
|
||||
cks, err := repoManager.CksRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToCksResponseList(cks)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
SuccessResponse(c, responses)
|
||||
}
|
||||
|
||||
// CreateCks 创建Cookie
|
||||
func CreateCks(c *gin.Context) {
|
||||
var req dto.CreateCksRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,58 +43,83 @@ func CreateCks(c *gin.Context) {
|
||||
|
||||
err := repoManager.CksRepository.Create(cks)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": cks.ID,
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "Cookie创建成功",
|
||||
"cks": converter.ToCksResponse(cks),
|
||||
})
|
||||
}
|
||||
|
||||
// GetCksByID 根据ID获取Cookie详情
|
||||
func GetCksByID(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cks, err := repoManager.CksRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "Cookie不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToCksResponse(cks)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// UpdateCks 更新Cookie
|
||||
func UpdateCks(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateCksRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cks, err := repoManager.CksRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Cookie不存在"})
|
||||
ErrorResponse(c, "Cookie不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if req.PanID != 0 {
|
||||
cks.PanID = req.PanID
|
||||
}
|
||||
cks.Idx = req.Idx
|
||||
if req.Idx != 0 {
|
||||
cks.Idx = req.Idx
|
||||
}
|
||||
if req.Ck != "" {
|
||||
cks.Ck = req.Ck
|
||||
}
|
||||
cks.IsValid = req.IsValid
|
||||
cks.Space = req.Space
|
||||
cks.LeftSpace = req.LeftSpace
|
||||
if req.Space != 0 {
|
||||
cks.Space = req.Space
|
||||
}
|
||||
if req.LeftSpace != 0 {
|
||||
cks.LeftSpace = req.LeftSpace
|
||||
}
|
||||
if req.Remark != "" {
|
||||
cks.Remark = req.Remark
|
||||
}
|
||||
|
||||
err = repoManager.CksRepository.Update(cks)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Cookie更新成功"})
|
||||
SuccessResponse(c, gin.H{"message": "Cookie更新成功"})
|
||||
}
|
||||
|
||||
// DeleteCks 删除Cookie
|
||||
@@ -102,34 +127,34 @@ func DeleteCks(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.CksRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Cookie删除成功"})
|
||||
SuccessResponse(c, gin.H{"message": "Cookie删除成功"})
|
||||
}
|
||||
|
||||
// GetCksByID 根据ID获取Cookie
|
||||
func GetCksByID(c *gin.Context) {
|
||||
// GetCksByID 根据ID获取Cookie详情(使用全局repoManager)
|
||||
func GetCksByIDGlobal(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cks, err := repoManager.CksRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Cookie不存在"})
|
||||
ErrorResponse(c, "Cookie不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToCksResponse(cks)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
191
handlers/hot_drama_handler.go
Normal file
191
handlers/hot_drama_handler.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"res_db/db/converter"
|
||||
"res_db/db/dto"
|
||||
"res_db/db/entity"
|
||||
"res_db/db/repo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HotDramaHandler 热播剧处理器
|
||||
type HotDramaHandler struct {
|
||||
hotDramaRepo repo.HotDramaRepository
|
||||
}
|
||||
|
||||
// NewHotDramaHandler 创建热播剧处理器
|
||||
func NewHotDramaHandler(hotDramaRepo repo.HotDramaRepository) *HotDramaHandler {
|
||||
return &HotDramaHandler{
|
||||
hotDramaRepo: hotDramaRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHotDramaList 获取热播剧列表
|
||||
func (h *HotDramaHandler) GetHotDramaList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
category := c.Query("category")
|
||||
|
||||
var dramas []entity.HotDrama
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if category != "" {
|
||||
dramas, total, err = h.hotDramaRepo.FindByCategory(category, page, pageSize)
|
||||
} else {
|
||||
dramas, total, err = h.hotDramaRepo.FindAll(page, pageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取热播剧列表失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.HotDramaListToResponse(dramas)
|
||||
response.Total = int(total)
|
||||
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetHotDramaByID 根据ID获取热播剧详情
|
||||
func (h *HotDramaHandler) GetHotDramaByID(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
drama, err := h.hotDramaRepo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "热播剧不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.HotDramaToResponse(drama)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// CreateHotDrama 创建热播剧记录
|
||||
func CreateHotDrama(c *gin.Context) {
|
||||
var req dto.HotDramaRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
drama := converter.RequestToHotDrama(&req)
|
||||
if drama == nil {
|
||||
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err := repoManager.HotDramaRepository.Create(drama)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "创建热播剧记录失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.HotDramaToResponse(drama)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// UpdateHotDrama 更新热播剧记录
|
||||
func UpdateHotDrama(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.HotDramaRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
drama := converter.RequestToHotDrama(&req)
|
||||
if drama == nil {
|
||||
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
drama.ID = uint(id)
|
||||
|
||||
err = repoManager.HotDramaRepository.Upsert(drama)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "更新热播剧记录失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.HotDramaToResponse(drama)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// DeleteHotDrama 删除热播剧记录
|
||||
func DeleteHotDrama(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.HotDramaRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "删除热播剧记录失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "删除热播剧记录成功"})
|
||||
}
|
||||
|
||||
// GetHotDramaList 获取热播剧列表(使用全局repoManager)
|
||||
func GetHotDramaList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
category := c.Query("category")
|
||||
|
||||
var dramas []entity.HotDrama
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if category != "" {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, page, pageSize)
|
||||
} else {
|
||||
dramas, total, err = repoManager.HotDramaRepository.FindAll(page, pageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取热播剧列表失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.HotDramaListToResponse(dramas)
|
||||
response.Total = int(total)
|
||||
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetHotDramaByID 根据ID获取热播剧详情(使用全局repoManager)
|
||||
func GetHotDramaByID(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
drama, err := repoManager.HotDramaRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "热播剧不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.HotDramaToResponse(drama)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
@@ -15,19 +15,19 @@ import (
|
||||
func GetPans(c *gin.Context) {
|
||||
pans, err := repoManager.PanRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToPanResponseList(pans)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
ListResponse(c, responses, int64(len(responses)))
|
||||
}
|
||||
|
||||
// CreatePan 创建平台
|
||||
func CreatePan(c *gin.Context) {
|
||||
var req dto.CreatePanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ func CreatePan(c *gin.Context) {
|
||||
|
||||
err := repoManager.PanRepository.Create(pan)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
SuccessResponse(c, gin.H{
|
||||
"id": pan.ID,
|
||||
"message": "平台创建成功",
|
||||
})
|
||||
@@ -55,19 +55,19 @@ func UpdatePan(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdatePanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pan, err := repoManager.PanRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "平台不存在"})
|
||||
ErrorResponse(c, "平台不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,11 +84,11 @@ func UpdatePan(c *gin.Context) {
|
||||
|
||||
err = repoManager.PanRepository.Update(pan)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "平台更新成功"})
|
||||
SuccessResponse(c, gin.H{"message": "平台更新成功"})
|
||||
}
|
||||
|
||||
// DeletePan 删除平台
|
||||
@@ -96,17 +96,17 @@ func DeletePan(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.PanRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "平台删除成功"})
|
||||
SuccessResponse(c, gin.H{"message": "平台删除成功"})
|
||||
}
|
||||
|
||||
// GetPan 根据ID获取平台
|
||||
@@ -114,16 +114,16 @@ func GetPan(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pan, err := repoManager.PanRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "平台不存在"})
|
||||
ErrorResponse(c, "平台不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToPanResponse(pan)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
@@ -31,71 +31,92 @@ func GetReadyResources(c *gin.Context) {
|
||||
// 获取分页数据
|
||||
resources, total, err := repoManager.ReadyResourceRepository.FindWithPagination(page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToReadyResourceResponseList(resources)
|
||||
|
||||
// 使用标准化的分页响应格式
|
||||
PaginatedResponse(c, responses, page, pageSize, total)
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": responses,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateReadyResource 创建待处理资源
|
||||
func CreateReadyResource(c *gin.Context) {
|
||||
var req dto.CreateReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resource := &entity.ReadyResource{
|
||||
Title: req.Title,
|
||||
URL: req.URL,
|
||||
IP: req.IP,
|
||||
Title: req.Title,
|
||||
URL: req.URL,
|
||||
Category: req.Category,
|
||||
Tags: req.Tags,
|
||||
Img: req.Img,
|
||||
Source: req.Source,
|
||||
Extra: req.Extra,
|
||||
IP: req.IP,
|
||||
}
|
||||
|
||||
err := repoManager.ReadyResourceRepository.Create(resource)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
CreatedResponse(c, gin.H{"id": resource.ID}, "待处理资源创建成功")
|
||||
SuccessResponse(c, gin.H{
|
||||
"id": resource.ID,
|
||||
"message": "待处理资源创建成功",
|
||||
})
|
||||
}
|
||||
|
||||
// BatchCreateReadyResources 批量创建待处理资源
|
||||
func BatchCreateReadyResources(c *gin.Context) {
|
||||
var req dto.BatchCreateReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var resources []entity.ReadyResource
|
||||
for _, reqResource := range req.Resources {
|
||||
resource := entity.ReadyResource{
|
||||
Title: reqResource.Title,
|
||||
URL: reqResource.URL,
|
||||
IP: reqResource.IP,
|
||||
Title: reqResource.Title,
|
||||
URL: reqResource.URL,
|
||||
Category: reqResource.Category,
|
||||
Tags: reqResource.Tags,
|
||||
Img: reqResource.Img,
|
||||
Source: reqResource.Source,
|
||||
Extra: reqResource.Extra,
|
||||
IP: reqResource.IP,
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
CreatedResponse(c, gin.H{"count": len(resources)}, "批量创建成功")
|
||||
SuccessResponse(c, gin.H{
|
||||
"count": len(resources),
|
||||
"message": "批量创建成功",
|
||||
})
|
||||
}
|
||||
|
||||
// CreateReadyResourcesFromText 从文本创建待处理资源
|
||||
func CreateReadyResourcesFromText(c *gin.Context) {
|
||||
text := c.PostForm("text")
|
||||
if text == "" {
|
||||
ErrorResponse(c, http.StatusBadRequest, "文本内容不能为空")
|
||||
ErrorResponse(c, "文本内容不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,17 +139,20 @@ func CreateReadyResourcesFromText(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
ErrorResponse(c, http.StatusBadRequest, "未找到有效的URL")
|
||||
ErrorResponse(c, "未找到有效的URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
CreatedResponse(c, gin.H{"count": len(resources)}, "从文本创建成功")
|
||||
SuccessResponse(c, gin.H{
|
||||
"count": len(resources),
|
||||
"message": "从文本创建成功",
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteReadyResource 删除待处理资源
|
||||
@@ -136,34 +160,37 @@ func DeleteReadyResource(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, "无效的ID")
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.ReadyResourceRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SimpleSuccessResponse(c, "待处理资源删除成功")
|
||||
SuccessResponse(c, gin.H{"message": "待处理资源删除成功"})
|
||||
}
|
||||
|
||||
// ClearReadyResources 清空所有待处理资源
|
||||
func ClearReadyResources(c *gin.Context) {
|
||||
resources, err := repoManager.ReadyResourceRepository.FindAll()
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
err = repoManager.ReadyResourceRepository.Delete(resource.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, err.Error())
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"deleted_count": len(resources)}, "所有待处理资源已清空")
|
||||
SuccessResponse(c, gin.H{
|
||||
"deleted_count": len(resources),
|
||||
"message": "所有待处理资源已清空",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,89 +14,66 @@ import (
|
||||
// GetResources 获取资源列表
|
||||
func GetResources(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
categoryIDStr := c.Query("category_id")
|
||||
panIDStr := c.Query("pan_id")
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
categoryID := c.Query("category_id")
|
||||
search := c.Query("search")
|
||||
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if categoryIDStr != "" {
|
||||
categoryID, _ := strconv.ParseUint(categoryIDStr, 10, 32)
|
||||
resources, err = repoManager.ResourceRepository.FindByCategoryID(uint(categoryID))
|
||||
} else if panIDStr != "" {
|
||||
panID, _ := strconv.ParseUint(panIDStr, 10, 32)
|
||||
resources, err = repoManager.ResourceRepository.FindByPanID(uint(panID))
|
||||
if search != "" {
|
||||
resources, total, err = repoManager.ResourceRepository.Search(search, nil, page, pageSize)
|
||||
} else if categoryID != "" {
|
||||
categoryIDUint, _ := strconv.ParseUint(categoryID, 10, 32)
|
||||
resources, total, err = repoManager.ResourceRepository.FindByCategoryIDPaginated(uint(categoryIDUint), page, pageSize)
|
||||
} else {
|
||||
resources, err = repoManager.ResourceRepository.FindWithRelations()
|
||||
// 使用分页查询,避免加载所有数据
|
||||
resources, total, err = repoManager.ResourceRepository.FindWithRelationsPaginated(page, pageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 只返回公开的资源
|
||||
var publicResources []entity.Resource
|
||||
for _, resource := range resources {
|
||||
if resource.IsPublic {
|
||||
publicResources = append(publicResources, resource)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
start := (page - 1) * limit
|
||||
end := start + limit
|
||||
if start >= len(publicResources) {
|
||||
start = len(publicResources)
|
||||
}
|
||||
if end > len(publicResources) {
|
||||
end = len(publicResources)
|
||||
}
|
||||
|
||||
pagedResources := publicResources[start:end]
|
||||
responses := converter.ToResourceResponseList(pagedResources)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"resources": responses,
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": len(publicResources),
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetResourceByID 根据ID获取资源
|
||||
// GetResourceByID 根据ID获取资源详情
|
||||
func GetResourceByID(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resource, err := repoManager.ResourceRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if !resource.IsPublic {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
|
||||
ErrorResponse(c, "资源不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 增加浏览次数
|
||||
repoManager.ResourceRepository.IncrementViewCount(uint(id))
|
||||
if resource != nil {
|
||||
repoManager.ResourceRepository.IncrementViewCount(uint(id))
|
||||
}
|
||||
|
||||
response := converter.ToResourceResponse(resource)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// CreateResource 创建资源
|
||||
func CreateResource(c *gin.Context) {
|
||||
var req dto.CreateResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,7 +91,7 @@ func CreateResource(c *gin.Context) {
|
||||
|
||||
err := repoManager.ResourceRepository.Create(resource)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,14 +99,14 @@ func CreateResource(c *gin.Context) {
|
||||
if len(req.TagIDs) > 0 {
|
||||
err = repoManager.ResourceRepository.UpdateWithTags(resource, req.TagIDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": resource.ID,
|
||||
"message": "资源创建成功",
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "资源创建成功",
|
||||
"resource": converter.ToResourceResponse(resource),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,23 +115,23 @@ func UpdateResource(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resource, err := repoManager.ResourceRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
|
||||
ErrorResponse(c, "资源不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
// 更新资源信息
|
||||
if req.Title != "" {
|
||||
resource.Title = req.Title
|
||||
}
|
||||
@@ -179,22 +156,22 @@ func UpdateResource(c *gin.Context) {
|
||||
resource.IsValid = req.IsValid
|
||||
resource.IsPublic = req.IsPublic
|
||||
|
||||
err = repoManager.ResourceRepository.Update(resource)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 处理标签关联
|
||||
if req.TagIDs != nil {
|
||||
if len(req.TagIDs) > 0 {
|
||||
err = repoManager.ResourceRepository.UpdateWithTags(resource, req.TagIDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = repoManager.ResourceRepository.Update(resource)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "资源更新成功"})
|
||||
SuccessResponse(c, gin.H{"message": "资源更新成功"})
|
||||
}
|
||||
|
||||
// DeleteResource 删除资源
|
||||
@@ -202,61 +179,46 @@ func DeleteResource(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.ResourceRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "资源删除成功"})
|
||||
SuccessResponse(c, gin.H{"message": "资源删除成功"})
|
||||
}
|
||||
|
||||
// SearchResources 搜索资源
|
||||
func SearchResources(c *gin.Context) {
|
||||
query := c.Query("query")
|
||||
categoryIDStr := c.Query("category_id")
|
||||
query := c.Query("q")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
var categoryID *uint
|
||||
if categoryIDStr != "" {
|
||||
if id, err := strconv.ParseUint(categoryIDStr, 10, 32); err == nil {
|
||||
temp := uint(id)
|
||||
categoryID = &temp
|
||||
}
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if query == "" {
|
||||
// 搜索关键词为空时,返回最新记录(分页)
|
||||
resources, total, err = repoManager.ResourceRepository.FindWithRelationsPaginated(page, pageSize)
|
||||
} else {
|
||||
// 有搜索关键词时,执行搜索
|
||||
resources, total, err = repoManager.ResourceRepository.Search(query, nil, page, pageSize)
|
||||
}
|
||||
|
||||
// 记录搜索统计
|
||||
if query != "" {
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
repoManager.SearchStatRepository.RecordSearch(query, ip, userAgent)
|
||||
}
|
||||
|
||||
resources, total, err := repoManager.ResourceRepository.Search(query, categoryID, page, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 只返回公开的资源
|
||||
var publicResources []entity.Resource
|
||||
for _, resource := range resources {
|
||||
if resource.IsPublic {
|
||||
publicResources = append(publicResources, resource)
|
||||
}
|
||||
}
|
||||
|
||||
responses := converter.ToResourceResponseList(publicResources)
|
||||
|
||||
c.JSON(http.StatusOK, dto.SearchResponse{
|
||||
Resources: responses,
|
||||
Total: total,
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,72 +1,66 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// StandardResponse 标准化响应结构
|
||||
type StandardResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Pagination *PaginationInfo `json:"pagination,omitempty"`
|
||||
}
|
||||
|
||||
// PaginationInfo 分页信息
|
||||
type PaginationInfo struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
TotalPages int64 `json:"total_pages"`
|
||||
// Response 统一响应格式
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
// SuccessResponse 成功响应
|
||||
func SuccessResponse(c *gin.Context, data interface{}, message string) {
|
||||
c.JSON(200, StandardResponse{
|
||||
func SuccessResponse(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Success: true,
|
||||
Message: message,
|
||||
Message: "操作成功",
|
||||
Data: data,
|
||||
Code: 200,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应
|
||||
func ErrorResponse(c *gin.Context, statusCode int, message string) {
|
||||
c.JSON(statusCode, StandardResponse{
|
||||
func ErrorResponse(c *gin.Context, message string, code int) {
|
||||
if code == 0 {
|
||||
code = 500
|
||||
}
|
||||
c.JSON(code, Response{
|
||||
Success: false,
|
||||
Error: message,
|
||||
Message: message,
|
||||
Data: nil,
|
||||
Code: code,
|
||||
})
|
||||
}
|
||||
|
||||
// PaginatedResponse 分页响应
|
||||
func PaginatedResponse(c *gin.Context, data interface{}, page, pageSize int, total int64) {
|
||||
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
|
||||
|
||||
c.JSON(200, StandardResponse{
|
||||
// ListResponse 列表响应
|
||||
func ListResponse(c *gin.Context, data interface{}, total int64) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Pagination: &PaginationInfo{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
Message: "获取成功",
|
||||
Data: gin.H{
|
||||
"list": data,
|
||||
"total": total,
|
||||
},
|
||||
Code: 200,
|
||||
})
|
||||
}
|
||||
|
||||
// SimpleSuccessResponse 简单成功响应
|
||||
func SimpleSuccessResponse(c *gin.Context, message string) {
|
||||
c.JSON(200, StandardResponse{
|
||||
// PageResponse 分页响应
|
||||
func PageResponse(c *gin.Context, data interface{}, total int64, page, limit int) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Success: true,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// CreatedResponse 创建成功响应
|
||||
func CreatedResponse(c *gin.Context, data interface{}, message string) {
|
||||
c.JSON(201, StandardResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
Data: data,
|
||||
Message: "获取成功",
|
||||
Data: gin.H{
|
||||
"list": data,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
},
|
||||
Code: 200,
|
||||
})
|
||||
}
|
||||
|
||||
59
handlers/scheduler_handler.go
Normal file
59
handlers/scheduler_handler.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"res_db/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetSchedulerStatus 获取调度器状态
|
||||
func GetSchedulerStatus(c *gin.Context) {
|
||||
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
|
||||
|
||||
status := gin.H{
|
||||
"hot_drama_scheduler_running": scheduler.IsHotDramaSchedulerRunning(),
|
||||
}
|
||||
|
||||
SuccessResponse(c, status)
|
||||
}
|
||||
|
||||
// 启动热播剧定时任务
|
||||
func StartHotDramaScheduler(c *gin.Context) {
|
||||
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
|
||||
if scheduler.IsHotDramaSchedulerRunning() {
|
||||
ErrorResponse(c, "热播剧定时任务已在运行中", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
scheduler.StartHotDramaScheduler()
|
||||
SuccessResponse(c, gin.H{"message": "热播剧定时任务已启动"})
|
||||
}
|
||||
|
||||
// 停止热播剧定时任务
|
||||
func StopHotDramaScheduler(c *gin.Context) {
|
||||
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
|
||||
if !scheduler.IsHotDramaSchedulerRunning() {
|
||||
ErrorResponse(c, "热播剧定时任务未在运行", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
scheduler.StopHotDramaScheduler()
|
||||
SuccessResponse(c, gin.H{"message": "热播剧定时任务已停止"})
|
||||
}
|
||||
|
||||
// 手动触发热播剧定时任务
|
||||
func TriggerHotDramaScheduler(c *gin.Context) {
|
||||
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
|
||||
scheduler.StartHotDramaScheduler() // 直接启动一次
|
||||
SuccessResponse(c, gin.H{"message": "手动触发热播剧定时任务成功"})
|
||||
}
|
||||
|
||||
// 手动获取热播剧名字
|
||||
func FetchHotDramaNames(c *gin.Context) {
|
||||
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
|
||||
names, err := scheduler.GetHotDramaNames()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取热播剧名字失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
SuccessResponse(c, gin.H{"names": names})
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func RecordSearch(c *gin.Context) {
|
||||
var req dto.SearchStatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,173 +25,120 @@ func RecordSearch(c *gin.Context) {
|
||||
// 记录搜索
|
||||
err := repoManager.SearchStatRepository.RecordSearch(req.Keyword, ip, userAgent)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "记录搜索失败"})
|
||||
ErrorResponse(c, "记录搜索失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "搜索记录成功"})
|
||||
SuccessResponse(c, gin.H{"message": "搜索记录成功"})
|
||||
}
|
||||
|
||||
// GetSearchStats 获取搜索统计总览
|
||||
// GetSearchStats 获取搜索统计(使用全局repoManager)
|
||||
func GetSearchStats(c *gin.Context) {
|
||||
// 获取今日搜索量
|
||||
todayStats, err := repoManager.SearchStatRepository.GetDailyStats(1)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
stats, total, err := repoManager.SearchStatRepository.FindWithPagination(page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取今日统计失败"})
|
||||
ErrorResponse(c, "获取搜索统计失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取本周搜索量
|
||||
weekStats, err := repoManager.SearchStatRepository.GetDailyStats(7)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本周统计失败"})
|
||||
return
|
||||
}
|
||||
response := converter.ToSearchStatResponseList(stats)
|
||||
|
||||
// 获取本月搜索量
|
||||
monthStats, err := repoManager.SearchStatRepository.GetDailyStats(30)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本月统计失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取热门关键词
|
||||
hotKeywords, err := repoManager.SearchStatRepository.GetHotKeywords(30, 10)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取热门关键词失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取搜索趋势
|
||||
searchTrend, err := repoManager.SearchStatRepository.GetSearchTrend(30)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取搜索趋势失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 计算总搜索量
|
||||
var todaySearches, weekSearches, monthSearches int
|
||||
if len(todayStats) > 0 {
|
||||
todaySearches = todayStats[0].TotalSearches
|
||||
}
|
||||
for _, stat := range weekStats {
|
||||
weekSearches += stat.TotalSearches
|
||||
}
|
||||
for _, stat := range monthStats {
|
||||
monthSearches += stat.TotalSearches
|
||||
}
|
||||
|
||||
// 构建趋势数据
|
||||
var trendDays []string
|
||||
var trendValues []int
|
||||
for _, stat := range searchTrend {
|
||||
trendDays = append(trendDays, stat.Date.Format("01-02"))
|
||||
trendValues = append(trendValues, stat.TotalSearches)
|
||||
}
|
||||
|
||||
response := dto.SearchStatsResponse{
|
||||
TodaySearches: todaySearches,
|
||||
WeekSearches: weekSearches,
|
||||
MonthSearches: monthSearches,
|
||||
HotKeywords: converter.ToHotKeywordResponseList(hotKeywords),
|
||||
DailyStats: converter.ToDailySearchStatResponseList(searchTrend),
|
||||
SearchTrend: dto.SearchTrendResponse{
|
||||
Days: trendDays,
|
||||
Values: trendValues,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": response,
|
||||
"total": int(total),
|
||||
})
|
||||
}
|
||||
|
||||
// GetHotKeywords 获取热门关键词
|
||||
// GetHotKeywords 获取热门关键词(使用全局repoManager)
|
||||
func GetHotKeywords(c *gin.Context) {
|
||||
daysStr := c.DefaultQuery("days", "30")
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的天数参数"})
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的限制参数"})
|
||||
ErrorResponse(c, "无效的限制参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
keywords, err := repoManager.SearchStatRepository.GetHotKeywords(days, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取热门关键词失败"})
|
||||
ErrorResponse(c, "获取热门关键词失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToHotKeywordResponseList(keywords)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetDailyStats 获取每日统计
|
||||
// GetDailyStats 获取每日统计(使用全局repoManager)
|
||||
func GetDailyStats(c *gin.Context) {
|
||||
daysStr := c.DefaultQuery("days", "30")
|
||||
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的天数参数"})
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := repoManager.SearchStatRepository.GetDailyStats(days)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取每日统计失败"})
|
||||
ErrorResponse(c, "获取每日统计失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToDailySearchStatResponseList(stats)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetSearchTrend 获取搜索趋势
|
||||
// GetSearchTrend 获取搜索趋势(使用全局repoManager)
|
||||
func GetSearchTrend(c *gin.Context) {
|
||||
daysStr := c.DefaultQuery("days", "30")
|
||||
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的天数参数"})
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
trend, err := repoManager.SearchStatRepository.GetSearchTrend(days)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取搜索趋势失败"})
|
||||
ErrorResponse(c, "获取搜索趋势失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToDailySearchStatResponseList(trend)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetKeywordTrend 获取关键词趋势
|
||||
// GetKeywordTrend 获取关键词趋势(使用全局repoManager)
|
||||
func GetKeywordTrend(c *gin.Context) {
|
||||
keyword := c.Param("keyword")
|
||||
if keyword == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "关键词不能为空"})
|
||||
ErrorResponse(c, "关键词不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
daysStr := c.DefaultQuery("days", "30")
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的天数参数"})
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
trend, err := repoManager.SearchStatRepository.GetKeywordTrend(keyword, days)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取关键词趋势失败"})
|
||||
ErrorResponse(c, "获取关键词趋势失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToDailySearchStatResponseList(trend)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
@@ -1,46 +1,84 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"res_db/db"
|
||||
"res_db/db/entity"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetStats 获取统计信息
|
||||
// GetStats 获取基础统计信息
|
||||
func GetStats(c *gin.Context) {
|
||||
// 获取资源总数
|
||||
totalResources, err := repoManager.ResourceRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 获取数据库统计
|
||||
var totalResources, totalCategories, totalTags, totalViews int64
|
||||
db.DB.Model(&entity.Resource{}).Count(&totalResources)
|
||||
db.DB.Model(&entity.Category{}).Count(&totalCategories)
|
||||
db.DB.Model(&entity.Tag{}).Count(&totalTags)
|
||||
db.DB.Model(&entity.Resource{}).Select("COALESCE(SUM(view_count), 0)").Scan(&totalViews)
|
||||
|
||||
// 获取分类总数
|
||||
totalCategories, err := repoManager.CategoryRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取标签总数
|
||||
totalTags, err := repoManager.TagRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 计算总浏览次数
|
||||
var totalViews int64
|
||||
for _, resource := range totalResources {
|
||||
totalViews += int64(resource.ViewCount)
|
||||
}
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total_resources": len(totalResources),
|
||||
"total_categories": len(totalCategories),
|
||||
"total_tags": len(totalTags),
|
||||
SuccessResponse(c, gin.H{
|
||||
"total_resources": totalResources,
|
||||
"total_categories": totalCategories,
|
||||
"total_tags": totalTags,
|
||||
"total_views": totalViews,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPerformanceStats 获取性能监控信息
|
||||
func GetPerformanceStats(c *gin.Context) {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
// 获取数据库连接池状态
|
||||
sqlDB, err := db.DB.DB()
|
||||
var dbStats gin.H
|
||||
if err == nil {
|
||||
dbStats = gin.H{
|
||||
"max_open_connections": sqlDB.Stats().MaxOpenConnections,
|
||||
"open_connections": sqlDB.Stats().OpenConnections,
|
||||
"in_use": sqlDB.Stats().InUse,
|
||||
"idle": sqlDB.Stats().Idle,
|
||||
}
|
||||
} else {
|
||||
dbStats = gin.H{
|
||||
"error": "无法获取数据库连接池状态",
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
SuccessResponse(c, gin.H{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"memory": gin.H{
|
||||
"alloc": m.Alloc,
|
||||
"total_alloc": m.TotalAlloc,
|
||||
"sys": m.Sys,
|
||||
"num_gc": m.NumGC,
|
||||
"heap_alloc": m.HeapAlloc,
|
||||
"heap_sys": m.HeapSys,
|
||||
"heap_idle": m.HeapIdle,
|
||||
"heap_inuse": m.HeapInuse,
|
||||
},
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
"database": dbStats,
|
||||
"system": gin.H{
|
||||
"cpu_count": runtime.NumCPU(),
|
||||
"go_version": runtime.Version(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统信息
|
||||
func GetSystemInfo(c *gin.Context) {
|
||||
SuccessResponse(c, gin.H{
|
||||
"uptime": time.Since(startTime).String(),
|
||||
"start_time": startTime.Format("2006-01-02 15:04:05"),
|
||||
"version": "1.0.0",
|
||||
"environment": gin.H{
|
||||
"gin_mode": gin.Mode(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 记录启动时间
|
||||
var startTime = time.Now()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"res_db/db/converter"
|
||||
"res_db/db/dto"
|
||||
"res_db/db/repo"
|
||||
"res_db/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -25,120 +26,126 @@ func NewSystemConfigHandler(systemConfigRepo repo.SystemConfigRepository) *Syste
|
||||
func (h *SystemConfigHandler) GetConfig(c *gin.Context) {
|
||||
config, err := h.systemConfigRepo.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "获取系统配置失败")
|
||||
ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(config)
|
||||
SuccessResponse(c, configResponse, "获取系统配置成功")
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新系统配置
|
||||
func (h *SystemConfigHandler) UpdateConfig(c *gin.Context) {
|
||||
var req dto.SystemConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, "请求参数错误")
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if req.SiteTitle == "" {
|
||||
ErrorResponse(c, http.StatusBadRequest, "网站标题不能为空")
|
||||
ErrorResponse(c, "网站标题不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440 {
|
||||
ErrorResponse(c, http.StatusBadRequest, "自动处理间隔必须在1-1440分钟之间")
|
||||
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.PageSize < 10 || req.PageSize > 500 {
|
||||
ErrorResponse(c, http.StatusBadRequest, "每页显示数量必须在10-500之间")
|
||||
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
config := converter.RequestToSystemConfig(&req)
|
||||
if config == nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "数据转换失败")
|
||||
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
err := h.systemConfigRepo.Upsert(config)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "保存系统配置失败")
|
||||
ErrorResponse(c, "保存系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回更新后的配置
|
||||
updatedConfig, err := h.systemConfigRepo.FindFirst()
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "获取更新后的配置失败")
|
||||
ErrorResponse(c, "获取更新后的配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(updatedConfig)
|
||||
SuccessResponse(c, configResponse, "系统配置保存成功")
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
// GetSystemConfig 获取系统配置(使用全局repoManager)
|
||||
func GetSystemConfig(c *gin.Context) {
|
||||
config, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "获取系统配置失败")
|
||||
ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(config)
|
||||
SuccessResponse(c, configResponse, "获取系统配置成功")
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
// UpdateSystemConfig 更新系统配置(使用全局repoManager)
|
||||
func UpdateSystemConfig(c *gin.Context) {
|
||||
var req dto.SystemConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, "请求参数错误")
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if req.SiteTitle == "" {
|
||||
ErrorResponse(c, http.StatusBadRequest, "网站标题不能为空")
|
||||
ErrorResponse(c, "网站标题不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440 {
|
||||
ErrorResponse(c, http.StatusBadRequest, "自动处理间隔必须在1-1440分钟之间")
|
||||
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.PageSize < 10 || req.PageSize > 500 {
|
||||
ErrorResponse(c, http.StatusBadRequest, "每页显示数量必须在10-500之间")
|
||||
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
config := converter.RequestToSystemConfig(&req)
|
||||
if config == nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "数据转换失败")
|
||||
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
err := repoManager.SystemConfigRepository.Upsert(config)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "保存系统配置失败")
|
||||
ErrorResponse(c, "保存系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据配置更新定时任务状态(错误不影响配置保存)
|
||||
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
|
||||
if scheduler != nil {
|
||||
scheduler.UpdateSchedulerStatus(req.AutoFetchHotDramaEnabled)
|
||||
}
|
||||
|
||||
// 返回更新后的配置
|
||||
updatedConfig, err := repoManager.SystemConfigRepository.FindFirst()
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "获取更新后的配置失败")
|
||||
ErrorResponse(c, "获取更新后的配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(updatedConfig)
|
||||
SuccessResponse(c, configResponse, "系统配置保存成功")
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
@@ -15,19 +15,19 @@ import (
|
||||
func GetTags(c *gin.Context) {
|
||||
tags, err := repoManager.TagRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToTagResponseList(tags)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
SuccessResponse(c, responses)
|
||||
}
|
||||
|
||||
// CreateTag 创建标签
|
||||
func CreateTag(c *gin.Context) {
|
||||
var req dto.CreateTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,34 +38,53 @@ func CreateTag(c *gin.Context) {
|
||||
|
||||
err := repoManager.TagRepository.Create(tag)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": tag.ID,
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "标签创建成功",
|
||||
"tag": converter.ToTagResponse(tag),
|
||||
})
|
||||
}
|
||||
|
||||
// GetTagByID 根据ID获取标签详情
|
||||
func GetTagByID(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := repoManager.TagRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "标签不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToTagResponse(tag)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// UpdateTag 更新标签
|
||||
func UpdateTag(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := repoManager.TagRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
|
||||
ErrorResponse(c, "标签不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,11 +97,11 @@ func UpdateTag(c *gin.Context) {
|
||||
|
||||
err = repoManager.TagRepository.Update(tag)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "标签更新成功"})
|
||||
SuccessResponse(c, gin.H{"message": "标签更新成功"})
|
||||
}
|
||||
|
||||
// DeleteTag 删除标签
|
||||
@@ -90,53 +109,46 @@ func DeleteTag(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.TagRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "标签删除成功"})
|
||||
SuccessResponse(c, gin.H{"message": "标签删除成功"})
|
||||
}
|
||||
|
||||
// GetTagByID 根据ID获取标签
|
||||
func GetTagByID(c *gin.Context) {
|
||||
// GetTagByID 根据ID获取标签详情(使用全局repoManager)
|
||||
func GetTagByIDGlobal(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := repoManager.TagRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
|
||||
ErrorResponse(c, "标签不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToTagResponse(tag)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetResourceTags 获取资源的标签
|
||||
func GetResourceTags(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
// GetTags 获取标签列表(使用全局repoManager)
|
||||
func GetTagsGlobal(c *gin.Context) {
|
||||
tags, err := repoManager.TagRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := repoManager.TagRepository.FindByResourceID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToTagResponseList(tags)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
SuccessResponse(c, responses)
|
||||
}
|
||||
|
||||
@@ -16,23 +16,23 @@ import (
|
||||
func Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
||||
ErrorResponse(c, "用户名或密码错误", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "账户已被禁用"})
|
||||
ErrorResponse(c, "账户已被禁用", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !middleware.CheckPassword(req.Password, user.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
||||
ErrorResponse(c, "用户名或密码错误", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func Login(c *gin.Context) {
|
||||
// 生成JWT令牌
|
||||
token, err := middleware.GenerateToken(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"})
|
||||
ErrorResponse(c, "生成令牌失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,35 +51,35 @@ func Login(c *gin.Context) {
|
||||
User: converter.ToUserResponse(user),
|
||||
}
|
||||
|
||||
SuccessResponse(c, response, "登录成功")
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func Register(c *gin.Context) {
|
||||
var req dto.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
existingUser, _ := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if existingUser != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"})
|
||||
ErrorResponse(c, "用户名已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
existingEmail, _ := repoManager.UserRepository.FindByEmail(req.Email)
|
||||
if existingEmail != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已存在"})
|
||||
ErrorResponse(c, "邮箱已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
hashedPassword, err := middleware.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
ErrorResponse(c, "密码加密失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,11 +93,11 @@ func Register(c *gin.Context) {
|
||||
|
||||
err = repoManager.UserRepository.Create(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "注册成功",
|
||||
"user": converter.ToUserResponse(user),
|
||||
})
|
||||
@@ -107,40 +107,40 @@ func Register(c *gin.Context) {
|
||||
func GetUsers(c *gin.Context) {
|
||||
users, err := repoManager.UserRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToUserResponseList(users)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
SuccessResponse(c, responses)
|
||||
}
|
||||
|
||||
// CreateUser 创建用户(管理员)
|
||||
func CreateUser(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
existingUser, _ := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if existingUser != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"})
|
||||
ErrorResponse(c, "用户名已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
existingEmail, _ := repoManager.UserRepository.FindByEmail(req.Email)
|
||||
if existingEmail != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已存在"})
|
||||
ErrorResponse(c, "邮箱已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
hashedPassword, err := middleware.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
ErrorResponse(c, "密码加密失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,11 +154,11 @@ func CreateUser(c *gin.Context) {
|
||||
|
||||
err = repoManager.UserRepository.Create(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "用户创建成功",
|
||||
"user": converter.ToUserResponse(user),
|
||||
})
|
||||
@@ -169,19 +169,19 @@ func UpdateUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repoManager.UserRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
ErrorResponse(c, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,11 +198,11 @@ func UpdateUser(c *gin.Context) {
|
||||
|
||||
err = repoManager.UserRepository.Update(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "用户更新成功"})
|
||||
SuccessResponse(c, gin.H{"message": "用户更新成功"})
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户(管理员)
|
||||
@@ -210,33 +210,33 @@ func DeleteUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.UserRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "用户删除成功"})
|
||||
SuccessResponse(c, gin.H{"message": "用户删除成功"})
|
||||
}
|
||||
|
||||
// GetProfile 获取当前用户信息
|
||||
// GetProfile 获取用户资料
|
||||
func GetProfile(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
|
||||
ErrorResponse(c, "未认证", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repoManager.UserRepository.FindByID(userID.(uint))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
ErrorResponse(c, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToUserResponse(user)
|
||||
c.JSON(http.StatusOK, response)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
@@ -163,6 +163,11 @@ func createTables() error {
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
url VARCHAR(500) NOT NULL,
|
||||
category VARCHAR(100) DEFAULT NULL,
|
||||
tags VARCHAR(500) DEFAULT NULL,
|
||||
img VARCHAR(500) DEFAULT NULL,
|
||||
source VARCHAR(100) DEFAULT NULL,
|
||||
extra TEXT DEFAULT NULL,
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ip VARCHAR(45) DEFAULT NULL
|
||||
);`
|
||||
@@ -180,6 +185,42 @@ func createTables() error {
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
// 创建系统配置表
|
||||
systemConfigTable := `
|
||||
CREATE TABLE IF NOT EXISTS system_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_title VARCHAR(200) NOT NULL DEFAULT '网盘资源管理系统',
|
||||
site_description VARCHAR(500),
|
||||
keywords VARCHAR(500),
|
||||
author VARCHAR(100),
|
||||
copyright VARCHAR(200),
|
||||
auto_process_ready_resources BOOLEAN DEFAULT false,
|
||||
auto_process_interval INTEGER DEFAULT 30,
|
||||
auto_transfer_enabled BOOLEAN DEFAULT false,
|
||||
auto_fetch_hot_drama_enabled BOOLEAN DEFAULT false,
|
||||
page_size INTEGER DEFAULT 100,
|
||||
maintenance_mode BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
// 创建热播剧表
|
||||
hotDramaTable := `
|
||||
CREATE TABLE IF NOT EXISTS hot_dramas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
rating DECIMAL(3,1) DEFAULT 0.0,
|
||||
year VARCHAR(10),
|
||||
directors VARCHAR(500),
|
||||
actors VARCHAR(1000),
|
||||
category VARCHAR(50),
|
||||
sub_type VARCHAR(50),
|
||||
source VARCHAR(50) DEFAULT 'douban',
|
||||
douban_id VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
if _, err := DB.Exec(panTable); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -196,6 +237,14 @@ func createTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := DB.Exec(systemConfigTable); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := DB.Exec(hotDramaTable); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 插入默认分类
|
||||
insertDefaultCategories := `
|
||||
INSERT INTO categories (name, description) VALUES
|
||||
@@ -204,8 +253,12 @@ func createTables() error {
|
||||
('动漫', '动漫资源'),
|
||||
('音乐', '音乐资源'),
|
||||
('软件', '软件资源'),
|
||||
('游戏', '游戏资源'),
|
||||
('PC游戏', 'PC游戏资源'),
|
||||
('手机游戏', '手机游戏'),
|
||||
('文档', '文档资源'),
|
||||
('短剧', '短剧'),
|
||||
('学习资源', '学习资源'),
|
||||
('视频教程', '学习资源'),
|
||||
('其他', '其他资源')
|
||||
ON CONFLICT (name) DO NOTHING;`
|
||||
|
||||
@@ -213,30 +266,16 @@ func createTables() error {
|
||||
insertDefaultPans := `
|
||||
INSERT INTO pan (name, key, icon, remark) VALUES
|
||||
('baidu', 1, '<i class="fas fa-cloud text-blue-500"></i>', '百度网盘'),
|
||||
('pan.baidu', 2, '<i class="fas fa-cloud text-blue-500"></i>', '百度网盘'),
|
||||
('aliyun', 3, '<i class="fas fa-cloud text-orange-500"></i>', '阿里云盘'),
|
||||
('quark', 4, '<i class="fas fa-atom text-purple-500"></i>', '夸克网盘'),
|
||||
('teambition', 5, '<i class="fas fa-cloud text-orange-500"></i>', '阿里云盘'),
|
||||
('cloud.189', 6, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
|
||||
('e.189', 7, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
|
||||
('tianyi', 8, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
|
||||
('天翼', 9, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
|
||||
('xunlei', 10, '<i class="fas fa-bolt text-yellow-500"></i>', '迅雷云盘'),
|
||||
('weiyun', 11, '<i class="fas fa-cloud text-green-500"></i>', '微云'),
|
||||
('lanzou', 12, '<i class="fas fa-cloud text-blue-400"></i>', '蓝奏云'),
|
||||
('123', 13, '<i class="fas fa-cloud text-red-500"></i>', '123云盘'),
|
||||
('onedrive', 14, '<i class="fab fa-microsoft text-blue-600"></i>', 'OneDrive'),
|
||||
('google', 15, '<i class="fab fa-google-drive text-green-600"></i>', 'Google云盘'),
|
||||
('drive.google', 16, '<i class="fab fa-google-drive text-green-600"></i>', 'Google云盘'),
|
||||
('dropbox', 17, '<i class="fab fa-dropbox text-blue-500"></i>', 'Dropbox'),
|
||||
('ctfile', 18, '<i class="fas fa-folder text-yellow-600"></i>', '城通网盘'),
|
||||
('115', 19, '<i class="fas fa-cloud-upload-alt text-green-600"></i>', '115网盘'),
|
||||
('magnet', 20, '<i class="fas fa-magnet text-red-600"></i>', '磁力链接'),
|
||||
('uc', 21, '<i class="fas fa-cloud-download-alt text-purple-600"></i>', 'UC网盘'),
|
||||
('UC', 22, '<i class="fas fa-cloud-download-alt text-purple-600"></i>', 'UC网盘'),
|
||||
('yun.139', 23, '<i class="fas fa-cloud text-cyan-500"></i>', '移动云盘'),
|
||||
('unknown', 24, '<i class="fas fa-question-circle text-gray-400"></i>', '未知平台'),
|
||||
('other', 25, '<i class="fas fa-cloud text-gray-500"></i>', '其他')
|
||||
('aliyun', 2, '<i class="fas fa-cloud text-orange-500"></i>', '阿里云盘'),
|
||||
('quark', 3, '<i class="fas fa-atom text-purple-500"></i>', '夸克网盘'),
|
||||
('xunlei', 5, '<i class="fas fa-bolt text-yellow-500"></i>', '迅雷云盘'),
|
||||
('lanzou', 7, '<i class="fas fa-cloud text-blue-400"></i>', '蓝奏云'),
|
||||
('123', 8, '<i class="fas fa-cloud text-red-500"></i>', '123云盘'),
|
||||
('ctfile', 9, '<i class="fas fa-folder text-yellow-600"></i>', '城通网盘'),
|
||||
('115', 10, '<i class="fas fa-cloud-upload-alt text-green-600"></i>', '115网盘'),
|
||||
('magnet', 11, '<i class="fas fa-magnet text-red-600"></i>', '磁力链接'),
|
||||
('uc', 12, '<i class="fas fa-cloud-download-alt text-purple-600"></i>', 'UC网盘'),
|
||||
('other', 13, '<i class="fas fa-cloud text-gray-500"></i>', '其他')
|
||||
ON CONFLICT (name) DO NOTHING;`
|
||||
|
||||
if _, err := DB.Exec(insertDefaultCategories); err != nil {
|
||||
|
||||
514
utils/douban_service.go
Normal file
514
utils/douban_service.go
Normal file
@@ -0,0 +1,514 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// DoubanService 豆瓣服务
|
||||
type DoubanService struct {
|
||||
baseURL string
|
||||
client *resty.Client
|
||||
|
||||
// 电影榜单配置 - 4个大类,每个大类下有5个小类
|
||||
MovieCategories map[string]map[string]map[string]string
|
||||
|
||||
// 剧集榜单配置 - 2个大类
|
||||
TvCategories map[string]map[string]map[string]string
|
||||
}
|
||||
|
||||
// DoubanItem 豆瓣项目
|
||||
type DoubanItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Rating Rating `json:"rating"`
|
||||
Year string `json:"year"`
|
||||
Directors []string `json:"directors"`
|
||||
Actors []string `json:"actors"`
|
||||
}
|
||||
|
||||
// Rating 评分
|
||||
type Rating struct {
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
// DoubanCategory 豆瓣分类
|
||||
type DoubanCategory struct {
|
||||
Category string `json:"category"`
|
||||
Selected bool `json:"selected"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// DoubanResponse 豆瓣响应
|
||||
type DoubanResponse struct {
|
||||
Items []DoubanItem `json:"items"`
|
||||
Categories []DoubanCategory `json:"categories"`
|
||||
Total int `json:"total"`
|
||||
IsMockData bool `json:"is_mock_data,omitempty"`
|
||||
MockReason string `json:"mock_reason,omitempty"`
|
||||
Notice string `json:"notice,omitempty"`
|
||||
}
|
||||
|
||||
// DoubanResult 豆瓣结果
|
||||
type DoubanResult struct {
|
||||
Success bool `json:"success"`
|
||||
Data *DoubanResponse `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// NewDoubanService 创建新的豆瓣服务
|
||||
func NewDoubanService() *DoubanService {
|
||||
client := resty.New()
|
||||
client.SetTimeout(30 * time.Second)
|
||||
client.SetHeaders(map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
||||
"Referer": "https://m.douban.com/",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Connection": "keep-alive",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
})
|
||||
|
||||
// 初始化电影榜单配置
|
||||
movieCategories := map[string]map[string]map[string]string{
|
||||
"热门电影": {
|
||||
"全部": {"category": "热门", "type": "全部"},
|
||||
"华语": {"category": "热门", "type": "华语"},
|
||||
"欧美": {"category": "热门", "type": "欧美"},
|
||||
"韩国": {"category": "热门", "type": "韩国"},
|
||||
"日本": {"category": "热门", "type": "日本"},
|
||||
},
|
||||
"最新电影": {
|
||||
"全部": {"category": "最新", "type": "全部"},
|
||||
"华语": {"category": "最新", "type": "华语"},
|
||||
"欧美": {"category": "最新", "type": "欧美"},
|
||||
"韩国": {"category": "最新", "type": "韩国"},
|
||||
"日本": {"category": "最新", "type": "日本"},
|
||||
},
|
||||
"豆瓣高分": {
|
||||
"全部": {"category": "豆瓣高分", "type": "全部"},
|
||||
"华语": {"category": "豆瓣高分", "type": "华语"},
|
||||
"欧美": {"category": "豆瓣高分", "type": "欧美"},
|
||||
"韩国": {"category": "豆瓣高分", "type": "韩国"},
|
||||
"日本": {"category": "豆瓣高分", "type": "日本"},
|
||||
},
|
||||
"冷门佳片": {
|
||||
"全部": {"category": "冷门佳片", "type": "全部"},
|
||||
"华语": {"category": "冷门佳片", "type": "华语"},
|
||||
"欧美": {"category": "冷门佳片", "type": "欧美"},
|
||||
"韩国": {"category": "冷门佳片", "type": "韩国"},
|
||||
"日本": {"category": "冷门佳片", "type": "日本"},
|
||||
},
|
||||
}
|
||||
|
||||
// 初始化剧集榜单配置
|
||||
tvCategories := map[string]map[string]map[string]string{
|
||||
"最近热门剧集": {
|
||||
"综合": {"category": "tv", "type": "tv"},
|
||||
"国产剧": {"category": "tv", "type": "tv_domestic"},
|
||||
"欧美剧": {"category": "tv", "type": "tv_american"},
|
||||
"日剧": {"category": "tv", "type": "tv_japanese"},
|
||||
"韩剧": {"category": "tv", "type": "tv_korean"},
|
||||
"动画": {"category": "tv", "type": "tv_animation"},
|
||||
"纪录片": {"category": "tv", "type": "tv_documentary"},
|
||||
},
|
||||
"最近热门综艺": {
|
||||
"综合": {"category": "show", "type": "show"},
|
||||
"国内": {"category": "show", "type": "show_domestic"},
|
||||
"国外": {"category": "show", "type": "show_foreign"},
|
||||
},
|
||||
}
|
||||
|
||||
return &DoubanService{
|
||||
baseURL: "https://m.douban.com/rexxar/api/v2",
|
||||
client: client,
|
||||
MovieCategories: movieCategories,
|
||||
TvCategories: tvCategories,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMovieRanking 获取电影榜单数据
|
||||
func (ds *DoubanService) GetMovieRanking(category, rankingType string, start, limit int) (*DoubanResult, error) {
|
||||
log.Printf("获取电影榜单: %s - %s, start: %d, limit: %d", category, rankingType, start, limit)
|
||||
|
||||
// 构建请求参数
|
||||
params := map[string]string{
|
||||
"start": strconv.Itoa(start),
|
||||
"limit": strconv.Itoa(limit),
|
||||
}
|
||||
|
||||
// 根据不同的category和type添加特定参数
|
||||
if category != "热门" || rankingType != "全部" {
|
||||
if rankingType != "全部" {
|
||||
params["type"] = rankingType
|
||||
}
|
||||
if category != "热门" {
|
||||
params["category"] = category
|
||||
}
|
||||
}
|
||||
|
||||
var response *resty.Response
|
||||
var err error
|
||||
|
||||
// 尝试调用豆瓣API
|
||||
response, err = ds.client.R().
|
||||
SetQueryParams(params).
|
||||
Get(ds.baseURL + "/subject/recent_hot/movie")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("豆瓣API调用失败,使用模拟数据: %v", err)
|
||||
// 如果豆瓣API调用失败,使用模拟数据
|
||||
mockData := ds.getMockMovieData()
|
||||
mockData.IsMockData = true
|
||||
mockData.MockReason = "API调用失败"
|
||||
|
||||
return &DoubanResult{
|
||||
Success: true,
|
||||
Data: mockData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var apiResponse map[string]interface{}
|
||||
if err := json.Unmarshal(response.Body(), &apiResponse); err != nil {
|
||||
log.Printf("解析API响应失败: %v", err)
|
||||
mockData := ds.getMockMovieData()
|
||||
mockData.IsMockData = true
|
||||
mockData.MockReason = "解析API响应失败"
|
||||
|
||||
return &DoubanResult{
|
||||
Success: true,
|
||||
Data: mockData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 处理豆瓣移动端API的响应格式
|
||||
items := ds.extractItems(apiResponse)
|
||||
categories := ds.extractCategories(apiResponse)
|
||||
|
||||
// 如果没有获取到真实数据,使用模拟数据
|
||||
isMockData := false
|
||||
mockReason := ""
|
||||
|
||||
if len(items) == 0 {
|
||||
log.Println("API返回空数据,使用模拟数据")
|
||||
mockData := ds.getMockMovieData()
|
||||
items = mockData.Items
|
||||
isMockData = true
|
||||
mockReason = "API返回空数据"
|
||||
}
|
||||
|
||||
// 如果没有获取到categories,使用默认的电影分类
|
||||
if len(categories) == 0 {
|
||||
categories = []DoubanCategory{
|
||||
{Category: "热门", Selected: true, Type: "全部", Title: "热门"},
|
||||
{Category: "最新", Selected: false, Type: "全部", Title: "最新"},
|
||||
{Category: "豆瓣高分", Selected: false, Type: "全部", Title: "豆瓣高分"},
|
||||
{Category: "冷门佳片", Selected: false, Type: "全部", Title: "冷门佳片"},
|
||||
{Category: "热门", Selected: false, Type: "华语", Title: "华语"},
|
||||
{Category: "热门", Selected: false, Type: "欧美", Title: "欧美"},
|
||||
{Category: "热门", Selected: false, Type: "韩国", Title: "韩国"},
|
||||
{Category: "热门", Selected: false, Type: "日本", Title: "日本"},
|
||||
}
|
||||
}
|
||||
|
||||
// 根据请求的category和type更新selected状态
|
||||
for i := range categories {
|
||||
categories[i].Selected = categories[i].Category == category && categories[i].Type == rankingType
|
||||
}
|
||||
|
||||
// 限制返回数量
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
|
||||
result := &DoubanResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
Categories: categories,
|
||||
IsMockData: isMockData,
|
||||
MockReason: mockReason,
|
||||
}
|
||||
|
||||
if isMockData {
|
||||
result.Notice = "⚠️ 这是模拟数据,非豆瓣实时数据"
|
||||
}
|
||||
|
||||
return &DoubanResult{
|
||||
Success: true,
|
||||
Data: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTvRanking 获取电视剧榜单数据
|
||||
func (ds *DoubanService) GetTvRanking(category, rankingType string, start, limit int) (*DoubanResult, error) {
|
||||
log.Printf("获取电视剧榜单: %s - %s, start: %d, limit: %d", category, rankingType, start, limit)
|
||||
|
||||
// 构建请求参数
|
||||
params := map[string]string{
|
||||
"start": strconv.Itoa(start),
|
||||
"limit": strconv.Itoa(limit),
|
||||
}
|
||||
|
||||
// 根据不同的category和type添加特定参数
|
||||
if category != "tv" || rankingType != "tv" {
|
||||
if rankingType != "tv" {
|
||||
params["type"] = rankingType
|
||||
}
|
||||
if category != "tv" {
|
||||
params["category"] = category
|
||||
}
|
||||
}
|
||||
|
||||
var response *resty.Response
|
||||
var err error
|
||||
|
||||
// 尝试调用豆瓣API
|
||||
response, err = ds.client.R().
|
||||
SetQueryParams(params).
|
||||
Get(ds.baseURL + "/subject/recent_hot/tv")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("豆瓣TV API调用失败,使用模拟数据: %v", err)
|
||||
mockData := ds.getMockTvData()
|
||||
mockData.IsMockData = true
|
||||
mockData.MockReason = "API调用失败"
|
||||
|
||||
return &DoubanResult{
|
||||
Success: true,
|
||||
Data: mockData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var apiResponse map[string]interface{}
|
||||
if err := json.Unmarshal(response.Body(), &apiResponse); err != nil {
|
||||
log.Printf("解析TV API响应失败: %v", err)
|
||||
mockData := ds.getMockTvData()
|
||||
mockData.IsMockData = true
|
||||
mockData.MockReason = "解析API响应失败"
|
||||
|
||||
return &DoubanResult{
|
||||
Success: true,
|
||||
Data: mockData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 处理豆瓣移动端API的响应格式
|
||||
items := ds.extractItems(apiResponse)
|
||||
categories := ds.extractCategories(apiResponse)
|
||||
|
||||
// 如果没有获取到真实数据,使用模拟数据
|
||||
isMockData := false
|
||||
mockReason := ""
|
||||
|
||||
if len(items) == 0 {
|
||||
log.Println("TV API返回空数据,使用模拟数据")
|
||||
mockData := ds.getMockTvData()
|
||||
items = mockData.Items
|
||||
isMockData = true
|
||||
mockReason = "API返回空数据"
|
||||
}
|
||||
|
||||
// 如果没有获取到categories,使用默认的电视剧分类
|
||||
if len(categories) == 0 {
|
||||
categories = []DoubanCategory{
|
||||
{Category: "tv", Selected: true, Type: "tv", Title: "综合"},
|
||||
{Category: "tv", Selected: false, Type: "tv_domestic", Title: "国产剧"},
|
||||
{Category: "show", Selected: false, Type: "show", Title: "综艺"},
|
||||
{Category: "tv", Selected: false, Type: "tv_american", Title: "欧美剧"},
|
||||
{Category: "tv", Selected: false, Type: "tv_japanese", Title: "日剧"},
|
||||
{Category: "tv", Selected: false, Type: "tv_korean", Title: "韩剧"},
|
||||
{Category: "tv", Selected: false, Type: "tv_animation", Title: "动画"},
|
||||
{Category: "tv", Selected: false, Type: "tv_documentary", Title: "纪录片"},
|
||||
}
|
||||
}
|
||||
|
||||
// 根据请求的category和type更新selected状态
|
||||
for i := range categories {
|
||||
categories[i].Selected = categories[i].Category == category && categories[i].Type == rankingType
|
||||
}
|
||||
|
||||
// 限制返回数量
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
|
||||
result := &DoubanResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
Categories: categories,
|
||||
IsMockData: isMockData,
|
||||
MockReason: mockReason,
|
||||
}
|
||||
|
||||
if isMockData {
|
||||
result.Notice = "⚠️ 这是模拟数据,非豆瓣实时数据"
|
||||
}
|
||||
|
||||
return &DoubanResult{
|
||||
Success: true,
|
||||
Data: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMovieCategories 获取支持的电影类别
|
||||
func (ds *DoubanService) GetMovieCategories() map[string]map[string]map[string]string {
|
||||
return ds.MovieCategories
|
||||
}
|
||||
|
||||
// GetTvCategories 获取支持的电视剧类别
|
||||
func (ds *DoubanService) GetTvCategories() map[string]map[string]map[string]string {
|
||||
return ds.TvCategories
|
||||
}
|
||||
|
||||
// GetAllCategories 获取所有支持的类别
|
||||
func (ds *DoubanService) GetAllCategories() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"movie": ds.GetMovieCategories(),
|
||||
"tv": ds.GetTvCategories(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMovieSubCategories 获取电影特定大类下的小类
|
||||
func (ds *DoubanService) GetMovieSubCategories(mainCategory string) map[string]map[string]string {
|
||||
return ds.MovieCategories[mainCategory]
|
||||
}
|
||||
|
||||
// GetTvSubCategories 获取剧集特定大类下的小类
|
||||
func (ds *DoubanService) GetTvSubCategories(mainCategory string) map[string]map[string]string {
|
||||
return ds.TvCategories[mainCategory]
|
||||
}
|
||||
|
||||
// getMockMovieData 获取模拟电影数据
|
||||
func (ds *DoubanService) getMockMovieData() *DoubanResponse {
|
||||
return &DoubanResponse{
|
||||
Notice: "⚠️ 这是模拟数据,非豆瓣实时数据",
|
||||
Items: []DoubanItem{
|
||||
{
|
||||
ID: "1292052",
|
||||
Title: "肖申克的救赎",
|
||||
Rating: Rating{Value: 9.7},
|
||||
Year: "1994",
|
||||
Directors: []string{"弗兰克·德拉邦特"},
|
||||
Actors: []string{"蒂姆·罗宾斯", "摩根·弗里曼"},
|
||||
},
|
||||
{
|
||||
ID: "1291546",
|
||||
Title: "霸王别姬",
|
||||
Rating: Rating{Value: 9.6},
|
||||
Year: "1993",
|
||||
Directors: []string{"陈凯歌"},
|
||||
Actors: []string{"张国荣", "张丰毅", "巩俐"},
|
||||
},
|
||||
{
|
||||
ID: "1295644",
|
||||
Title: "阿甘正传",
|
||||
Rating: Rating{Value: 9.5},
|
||||
Year: "1994",
|
||||
Directors: []string{"罗伯特·泽米吉斯"},
|
||||
Actors: []string{"汤姆·汉克斯", "罗宾·怀特"},
|
||||
},
|
||||
},
|
||||
Total: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// getMockTvData 获取模拟电视剧数据
|
||||
func (ds *DoubanService) getMockTvData() *DoubanResponse {
|
||||
return &DoubanResponse{
|
||||
Notice: "⚠️ 这是模拟数据,非豆瓣实时数据",
|
||||
Items: []DoubanItem{
|
||||
{
|
||||
ID: "26794435",
|
||||
Title: "请回答1988",
|
||||
Rating: Rating{Value: 9.7},
|
||||
Year: "2015",
|
||||
Directors: []string{"申元浩"},
|
||||
Actors: []string{"李惠利", "朴宝剑", "柳俊烈"},
|
||||
},
|
||||
{
|
||||
ID: "1309163",
|
||||
Title: "大明王朝1566",
|
||||
Rating: Rating{Value: 9.7},
|
||||
Year: "2007",
|
||||
Directors: []string{"张黎"},
|
||||
Actors: []string{"陈宝国", "黄志忠", "王庆祥"},
|
||||
},
|
||||
{
|
||||
ID: "1309169",
|
||||
Title: "亮剑",
|
||||
Rating: Rating{Value: 9.3},
|
||||
Year: "2005",
|
||||
Directors: []string{"陈健", "张前"},
|
||||
Actors: []string{"李幼斌", "何政军", "张光北"},
|
||||
},
|
||||
},
|
||||
Total: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// extractItems 从API响应中提取项目列表
|
||||
func (ds *DoubanService) extractItems(response map[string]interface{}) []DoubanItem {
|
||||
var items []DoubanItem
|
||||
|
||||
// 尝试从不同的字段获取items
|
||||
if itemsData, ok := response["items"]; ok {
|
||||
if itemsBytes, err := json.Marshal(itemsData); err == nil {
|
||||
json.Unmarshal(itemsBytes, &items)
|
||||
}
|
||||
} else if subjectsData, ok := response["subjects"]; ok {
|
||||
if subjectsBytes, err := json.Marshal(subjectsData); err == nil {
|
||||
json.Unmarshal(subjectsBytes, &items)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// extractCategories 从API响应中提取分类列表
|
||||
func (ds *DoubanService) extractCategories(response map[string]interface{}) []DoubanCategory {
|
||||
var categories []DoubanCategory
|
||||
|
||||
if categoriesData, ok := response["categories"]; ok {
|
||||
if categoriesBytes, err := json.Marshal(categoriesData); err == nil {
|
||||
json.Unmarshal(categoriesBytes, &categories)
|
||||
}
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
// FetchHotDramaNames 获取热播剧名字(用于定时任务)
|
||||
func (ds *DoubanService) FetchHotDramaNames() ([]string, error) {
|
||||
var dramaNames []string
|
||||
|
||||
// 获取电影热门榜单
|
||||
movieResult, err := ds.GetMovieRanking("热门", "全部", 0, 10)
|
||||
if err != nil {
|
||||
log.Printf("获取电影榜单失败: %v", err)
|
||||
} else if movieResult.Success && movieResult.Data != nil {
|
||||
for _, item := range movieResult.Data.Items {
|
||||
dramaNames = append(dramaNames, item.Title)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取电视剧热门榜单
|
||||
tvResult, err := ds.GetTvRanking("tv", "tv", 0, 10)
|
||||
if err != nil {
|
||||
log.Printf("获取电视剧榜单失败: %v", err)
|
||||
} else if tvResult.Success && tvResult.Data != nil {
|
||||
for _, item := range tvResult.Data.Items {
|
||||
dramaNames = append(dramaNames, item.Title)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("获取到 %d 个热播剧名字", len(dramaNames))
|
||||
return dramaNames, nil
|
||||
}
|
||||
86
utils/global_scheduler.go
Normal file
86
utils/global_scheduler.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"res_db/db/repo"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GlobalScheduler 全局调度器管理器
|
||||
type GlobalScheduler struct {
|
||||
scheduler *Scheduler
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
globalScheduler *GlobalScheduler
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetGlobalScheduler 获取全局调度器实例(单例模式)
|
||||
func GetGlobalScheduler(hotDramaRepo repo.HotDramaRepository) *GlobalScheduler {
|
||||
once.Do(func() {
|
||||
globalScheduler = &GlobalScheduler{
|
||||
scheduler: NewScheduler(hotDramaRepo),
|
||||
}
|
||||
})
|
||||
return globalScheduler
|
||||
}
|
||||
|
||||
// StartHotDramaScheduler 启动热播剧定时任务
|
||||
func (gs *GlobalScheduler) StartHotDramaScheduler() {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if gs.scheduler.IsRunning() {
|
||||
log.Println("热播剧定时任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
gs.scheduler.StartHotDramaScheduler()
|
||||
log.Println("全局调度器已启动热播剧定时任务")
|
||||
}
|
||||
|
||||
// StopHotDramaScheduler 停止热播剧定时任务
|
||||
func (gs *GlobalScheduler) StopHotDramaScheduler() {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if !gs.scheduler.IsRunning() {
|
||||
log.Println("热播剧定时任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
gs.scheduler.StopHotDramaScheduler()
|
||||
log.Println("全局调度器已停止热播剧定时任务")
|
||||
}
|
||||
|
||||
// IsHotDramaSchedulerRunning 检查热播剧定时任务是否在运行
|
||||
func (gs *GlobalScheduler) IsHotDramaSchedulerRunning() bool {
|
||||
gs.mutex.RLock()
|
||||
defer gs.mutex.RUnlock()
|
||||
return gs.scheduler.IsRunning()
|
||||
}
|
||||
|
||||
// GetHotDramaNames 手动获取热播剧名字
|
||||
func (gs *GlobalScheduler) GetHotDramaNames() ([]string, error) {
|
||||
return gs.scheduler.GetHotDramaNames()
|
||||
}
|
||||
|
||||
// UpdateSchedulerStatus 根据系统配置更新调度器状态
|
||||
func (gs *GlobalScheduler) UpdateSchedulerStatus(autoFetchHotDramaEnabled bool) {
|
||||
gs.mutex.Lock()
|
||||
defer gs.mutex.Unlock()
|
||||
|
||||
if autoFetchHotDramaEnabled {
|
||||
if !gs.scheduler.IsRunning() {
|
||||
log.Println("系统配置启用自动拉取热播剧,启动定时任务")
|
||||
gs.scheduler.StartHotDramaScheduler()
|
||||
}
|
||||
} else {
|
||||
if gs.scheduler.IsRunning() {
|
||||
log.Println("系统配置禁用自动拉取热播剧,停止定时任务")
|
||||
gs.scheduler.StopHotDramaScheduler()
|
||||
}
|
||||
}
|
||||
}
|
||||
175
utils/scheduler.go
Normal file
175
utils/scheduler.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"res_db/db/entity"
|
||||
"res_db/db/repo"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Scheduler 定时任务管理器
|
||||
type Scheduler struct {
|
||||
doubanService *DoubanService
|
||||
hotDramaRepo repo.HotDramaRepository
|
||||
stopChan chan bool
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
// NewScheduler 创建新的定时任务管理器
|
||||
func NewScheduler(hotDramaRepo repo.HotDramaRepository) *Scheduler {
|
||||
return &Scheduler{
|
||||
doubanService: NewDoubanService(),
|
||||
hotDramaRepo: hotDramaRepo,
|
||||
stopChan: make(chan bool),
|
||||
isRunning: false,
|
||||
}
|
||||
}
|
||||
|
||||
// StartHotDramaScheduler 启动热播剧定时任务
|
||||
func (s *Scheduler) StartHotDramaScheduler() {
|
||||
if s.isRunning {
|
||||
log.Println("热播剧定时任务已在运行中")
|
||||
return
|
||||
}
|
||||
|
||||
s.isRunning = true
|
||||
log.Println("启动热播剧定时任务")
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Hour) // 每小时执行一次
|
||||
defer ticker.Stop()
|
||||
|
||||
// 立即执行一次
|
||||
s.fetchHotDramaData()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.fetchHotDramaData()
|
||||
case <-s.stopChan:
|
||||
log.Println("停止热播剧定时任务")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopHotDramaScheduler 停止热播剧定时任务
|
||||
func (s *Scheduler) StopHotDramaScheduler() {
|
||||
if !s.isRunning {
|
||||
log.Println("热播剧定时任务未在运行")
|
||||
return
|
||||
}
|
||||
|
||||
s.stopChan <- true
|
||||
s.isRunning = false
|
||||
log.Println("已发送停止信号给热播剧定时任务")
|
||||
}
|
||||
|
||||
// fetchHotDramaData 获取热播剧数据
|
||||
func (s *Scheduler) fetchHotDramaData() {
|
||||
log.Println("开始获取热播剧数据...")
|
||||
|
||||
dramaNames, err := s.doubanService.FetchHotDramaNames()
|
||||
if err != nil {
|
||||
log.Printf("获取热播剧数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("成功获取到 %d 个热播剧: %v", len(dramaNames), dramaNames)
|
||||
|
||||
// 处理获取到的热播剧数据
|
||||
s.processHotDramaNames(dramaNames)
|
||||
}
|
||||
|
||||
// processHotDramaNames 处理热播剧名字
|
||||
func (s *Scheduler) processHotDramaNames(dramaNames []string) {
|
||||
log.Printf("开始处理热播剧数据,共 %d 个", len(dramaNames))
|
||||
|
||||
// 获取电影和电视剧的详细数据
|
||||
s.processMovieData()
|
||||
s.processTvData()
|
||||
|
||||
log.Println("热播剧数据处理完成")
|
||||
}
|
||||
|
||||
// processMovieData 处理电影数据
|
||||
func (s *Scheduler) processMovieData() {
|
||||
log.Println("开始处理电影数据...")
|
||||
|
||||
// 获取电影热门榜单
|
||||
movieResult, err := s.doubanService.GetMovieRanking("热门", "全部", 0, 20)
|
||||
if err != nil {
|
||||
log.Printf("获取电影榜单失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if movieResult.Success && movieResult.Data != nil {
|
||||
for _, item := range movieResult.Data.Items {
|
||||
drama := &entity.HotDrama{
|
||||
Title: item.Title,
|
||||
Rating: item.Rating.Value,
|
||||
Year: item.Year,
|
||||
Directors: strings.Join(item.Directors, ", "),
|
||||
Actors: strings.Join(item.Actors, ", "),
|
||||
Category: "电影",
|
||||
SubType: "热门",
|
||||
Source: "douban",
|
||||
DoubanID: item.ID,
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := s.hotDramaRepo.Upsert(drama); err != nil {
|
||||
log.Printf("保存电影数据失败: %v", err)
|
||||
} else {
|
||||
log.Printf("成功保存电影: %s", item.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processTvData 处理电视剧数据
|
||||
func (s *Scheduler) processTvData() {
|
||||
log.Println("开始处理电视剧数据...")
|
||||
|
||||
// 获取电视剧热门榜单
|
||||
tvResult, err := s.doubanService.GetTvRanking("tv", "tv", 0, 20)
|
||||
if err != nil {
|
||||
log.Printf("获取电视剧榜单失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if tvResult.Success && tvResult.Data != nil {
|
||||
for _, item := range tvResult.Data.Items {
|
||||
drama := &entity.HotDrama{
|
||||
Title: item.Title,
|
||||
Rating: item.Rating.Value,
|
||||
Year: item.Year,
|
||||
Directors: strings.Join(item.Directors, ", "),
|
||||
Actors: strings.Join(item.Actors, ", "),
|
||||
Category: "电视剧",
|
||||
SubType: "热门",
|
||||
Source: "douban",
|
||||
DoubanID: item.ID,
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := s.hotDramaRepo.Upsert(drama); err != nil {
|
||||
log.Printf("保存电视剧数据失败: %v", err)
|
||||
} else {
|
||||
log.Printf("成功保存电视剧: %s", item.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsRunning 检查定时任务是否在运行
|
||||
func (s *Scheduler) IsRunning() bool {
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// GetHotDramaNames 手动获取热播剧名字(用于测试或手动调用)
|
||||
func (s *Scheduler) GetHotDramaNames() ([]string, error) {
|
||||
return s.doubanService.FetchHotDramaNames()
|
||||
}
|
||||
560
utils/url_checker.go
Normal file
560
utils/url_checker.go
Normal file
@@ -0,0 +1,560 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// 定义网盘服务类型
|
||||
const (
|
||||
UC = "uc"
|
||||
Aliyun = "aliyun"
|
||||
Quark = "quark"
|
||||
Pan115 = "115"
|
||||
Pan123 = "123pan"
|
||||
Tianyi = "tianyi"
|
||||
Xunlei = "xunlei"
|
||||
Baidu = "baidu"
|
||||
NotFound = "notfound"
|
||||
)
|
||||
|
||||
// 检查结果结构
|
||||
type CheckResult struct {
|
||||
URL string
|
||||
Status bool
|
||||
}
|
||||
|
||||
// 提取分享码和服务类型
|
||||
func ExtractShareId(urlStr string) (string, string) {
|
||||
return extractShareID(urlStr)
|
||||
}
|
||||
|
||||
// 提取分享码和服务类型
|
||||
func extractShareID(urlStr string) (string, string) {
|
||||
netDiskPatterns := map[string]struct {
|
||||
Domains []string
|
||||
Pattern *regexp.Regexp
|
||||
}{
|
||||
UC: {
|
||||
Domains: []string{"drive.uc.cn"},
|
||||
Pattern: regexp.MustCompile(`https?://drive\.uc\.cn/s/([a-zA-Z0-9]+)`),
|
||||
},
|
||||
Aliyun: {
|
||||
Domains: []string{"aliyundrive.com", "alipan.com"},
|
||||
Pattern: regexp.MustCompile(`https?://(?:www\.)?(?:aliyundrive|alipan)\.com/s/([a-zA-Z0-9]+)`),
|
||||
},
|
||||
Quark: {
|
||||
Domains: []string{"pan.quark.cn"},
|
||||
Pattern: regexp.MustCompile(`https?://(?:www\.)?pan\.quark\.cn/s/([a-zA-Z0-9]+)`),
|
||||
},
|
||||
Pan115: {
|
||||
Domains: []string{"115.com", "115cdn.com", "anxia.com"},
|
||||
Pattern: regexp.MustCompile(`https?://(?:www\.)?(?:115|115cdn|anxia)\.com/s/([a-zA-Z0-9]+)`),
|
||||
},
|
||||
Pan123: {
|
||||
Domains: []string{"123684.com", "123685.com", "123912.com", "123pan.com", "123pan.cn", "123592.com"},
|
||||
Pattern: regexp.MustCompile(`https?://(?:www\.)?(?:123684|123685|123912|123pan|123pan\.cn|123592)\.com/s/([a-zA-Z0-9-]+)`),
|
||||
},
|
||||
Tianyi: {
|
||||
Domains: []string{"cloud.189.cn"},
|
||||
Pattern: regexp.MustCompile(`https?://cloud\.189\.cn/(?:t/|web/share\?code=)([a-zA-Z0-9]+)`),
|
||||
},
|
||||
Xunlei: {
|
||||
Domains: []string{"pan.xunlei.com"},
|
||||
Pattern: regexp.MustCompile(`https?://(?:www\.)?pan\.xunlei\.com/s/([a-zA-Z0-9-]+)`),
|
||||
},
|
||||
Baidu: {
|
||||
Domains: []string{"pan.baidu.com", "yun.baidu.com"},
|
||||
Pattern: regexp.MustCompile(`https?://(?:[a-z]+\.)?(?:pan|yun)\.baidu\.com/(?:s/|share/init\?surl=)([a-zA-Z0-9_-]+)(?:\?|$)`),
|
||||
},
|
||||
}
|
||||
|
||||
for service, config := range netDiskPatterns {
|
||||
if containsDomain(urlStr, config.Domains) {
|
||||
match := config.Pattern.FindStringSubmatch(urlStr)
|
||||
if len(match) > 1 {
|
||||
return match[1], service
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", NotFound
|
||||
}
|
||||
|
||||
// 检查域名是否包含在列表中
|
||||
func containsDomain(urlStr string, domains []string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
host := u.Hostname()
|
||||
for _, domain := range domains {
|
||||
if strings.Contains(host, domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 创建HTTP客户端
|
||||
func createHTTPClient() *resty.Client {
|
||||
return resty.New().
|
||||
SetTimeout(10 * time.Second).
|
||||
SetRetryCount(3).
|
||||
SetRetryWaitTime(2 * time.Second).
|
||||
SetRetryMaxWaitTime(10 * time.Second)
|
||||
}
|
||||
|
||||
// 检查UC网盘链接
|
||||
func checkUC(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
url := fmt.Sprintf("https://drive.uc.cn/s/%s", shareID)
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Mobile Safari/537.36").
|
||||
SetHeader("Host", "drive.uc.cn").
|
||||
SetHeader("Referer", url).
|
||||
SetHeader("Origin", "https://drive.uc.cn").
|
||||
Get(url)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
bodyStr := resp.String()
|
||||
|
||||
// 检查错误关键词
|
||||
errorKeywords := []string{"失效", "不存在", "违规", "删除", "已过期", "被取消"}
|
||||
for _, keyword := range errorKeywords {
|
||||
if strings.Contains(bodyStr, keyword) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要访问码
|
||||
if strings.Contains(bodyStr, "class=\"main-body\"") && strings.Contains(bodyStr, "class=\"input-wrap\"") {
|
||||
fmt.Println("发现访问码输入框,判断为有效(需密码)")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查是否包含文件列表或分享内容
|
||||
if strings.Contains(bodyStr, "文件") || strings.Contains(bodyStr, "分享") || strings.Contains(bodyStr, "class=\"file-list\"") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 检查阿里云盘链接
|
||||
func checkAliyun(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
apiURL := "https://api.aliyundrive.com/adrive/v3/share_link/get_share_by_anonymous"
|
||||
data := map[string]string{"share_id": shareID}
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(data).
|
||||
Post(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var responseJSON map[string]interface{}
|
||||
err = json.Unmarshal(resp.Body(), &responseJSON)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if hasPwd, ok := responseJSON["has_pwd"].(bool); ok && hasPwd {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if code, ok := responseJSON["code"].(string); ok && code == "NotFound.ShareLink" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if _, ok := responseJSON["file_infos"]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查115网盘链接
|
||||
func check115(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
apiURL := "https://webapi.115.com/share/snap"
|
||||
params := map[string]string{
|
||||
"share_code": shareID,
|
||||
"receive_code": "",
|
||||
}
|
||||
|
||||
resp, err := client.R().
|
||||
SetQueryParams(params).
|
||||
Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var responseJSON map[string]interface{}
|
||||
err = json.Unmarshal(resp.Body(), &responseJSON)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if state, ok := responseJSON["state"].(bool); ok && state {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if errorMsg, ok := responseJSON["error"].(string); ok && strings.Contains(errorMsg, "请输入访问码") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 检查夸克网盘链接
|
||||
func checkQuark(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
apiURL := "https://drive.quark.cn/1/clouddrive/share/sharepage/token"
|
||||
data := map[string]string{"pwd_id": shareID, "passcode": ""}
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(data).
|
||||
Post(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var responseJSON map[string]interface{}
|
||||
err = json.Unmarshal(resp.Body(), &responseJSON)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if message, ok := responseJSON["message"].(string); ok && message == "ok" {
|
||||
data, ok := responseJSON["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
stoken, ok := data["stoken"].(string)
|
||||
if !ok || stoken == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
detailURL := fmt.Sprintf("https://drive-h.quark.cn/1/clouddrive/share/sharepage/detail?pwd_id=%s&stoken=%s&_fetch_share=1", shareID, url.QueryEscape(stoken))
|
||||
detailResp, err := client.R().Get(detailURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var detailResponseJSON map[string]interface{}
|
||||
err = json.Unmarshal(detailResp.Body(), &detailResponseJSON)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if status, ok := detailResponseJSON["status"].(float64); ok && status == 400 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if data, ok := detailResponseJSON["data"].(map[string]interface{}); ok {
|
||||
if share, ok := data["share"].(map[string]interface{}); ok {
|
||||
if status, ok := share["status"].(float64); ok && status == 1 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
} else if message, ok := responseJSON["message"].(string); ok && message == "需要提取码" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 检查123网盘链接
|
||||
func check123pan(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
apiURL := fmt.Sprintf("https://www.123pan.com/api/share/info?shareKey=%s", shareID)
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0").
|
||||
Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
bodyStr := resp.String()
|
||||
|
||||
if bodyStr == "" || strings.Contains(bodyStr, "分享页面不存在") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var responseJSON map[string]interface{}
|
||||
err = json.Unmarshal(resp.Body(), &responseJSON)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if code, ok := responseJSON["code"].(float64); ok && code != 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if data, ok := responseJSON["data"].(map[string]interface{}); ok {
|
||||
if hasPwd, ok := data["HasPwd"].(bool); ok && hasPwd {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查天翼网盘链接
|
||||
func checkTianyi(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
apiURL := "https://api.cloud.189.cn/open/share/getShareInfoByCodeV2.action"
|
||||
data := map[string]string{"shareCode": shareID}
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("Content-Type", "application/x-www-form-urlencoded").
|
||||
SetFormData(data).
|
||||
Post(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
bodyStr := resp.String()
|
||||
|
||||
errorKeywords := []string{"ShareInfoNotFound", "ShareNotFound", "FileNotFound", "ShareExpiredError", "ShareAuditNotPass"}
|
||||
for _, keyword := range errorKeywords {
|
||||
if strings.Contains(bodyStr, keyword) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(bodyStr, "needAccessCode") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查迅雷网盘链接
|
||||
func checkXunlei(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
tokenURL := "https://xluser-ssl.xunlei.com/v1/shield/captcha/init"
|
||||
data := map[string]interface{}{
|
||||
"client_id": "Xqp0kJBXWhwaTpB6",
|
||||
"device_id": "925b7631473a13716b791d7f28289cad",
|
||||
"action": "get:/drive/v1/share",
|
||||
"meta": map[string]interface{}{
|
||||
"package_name": "pan.xunlei.com",
|
||||
"client_version": "1.45.0",
|
||||
"captcha_sign": "1.fe2108ad808a74c9ac0243309242726c",
|
||||
"timestamp": "1645241033384",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(data).
|
||||
Post(tokenURL)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var tokenResponseJSON map[string]interface{}
|
||||
err = json.Unmarshal(resp.Body(), &tokenResponseJSON)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
token, ok := tokenResponseJSON["captcha_token"].(string)
|
||||
if !ok || token == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/share?share_id=%s", shareID)
|
||||
resp, err = client.R().
|
||||
SetHeader("x-captcha-token", token).
|
||||
SetHeader("x-client-id", "Xqp0kJBXWhwaTpB6").
|
||||
SetHeader("x-device-id", "925b7631473a13716b791d7f28289cad").
|
||||
Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
bodyStr := resp.String()
|
||||
|
||||
errorKeywords := []string{"NOT_FOUND", "SENSITIVE_RESOURCE", "EXPIRED"}
|
||||
for _, keyword := range errorKeywords {
|
||||
if strings.Contains(bodyStr, keyword) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(bodyStr, "PASS_CODE_EMPTY") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查百度网盘链接
|
||||
func checkBaidu(shareID string) (bool, error) {
|
||||
client := createHTTPClient()
|
||||
url := fmt.Sprintf("https://pan.baidu.com/s/%s", shareID)
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36").
|
||||
Get(url)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
bodyStr := resp.String()
|
||||
|
||||
errorKeywords := []string{"分享的文件已经被取消", "分享已过期", "你访问的页面不存在", "你所访问的页面"}
|
||||
for _, keyword := range errorKeywords {
|
||||
if strings.Contains(bodyStr, keyword) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(bodyStr, "请输入提取码") || strings.Contains(bodyStr, "提取文件") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if strings.Contains(bodyStr, "过期时间") || strings.Contains(bodyStr, "文件列表") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 检查URL有效性
|
||||
func CheckURL(urlStr string) (CheckResult, error) {
|
||||
shareID, service := extractShareID(urlStr)
|
||||
if shareID == "" || service == NotFound {
|
||||
fmt.Printf("无法识别的链接或网盘服务: %s\n", urlStr)
|
||||
return CheckResult{URL: urlStr, Status: false}, nil
|
||||
}
|
||||
|
||||
checkFunctions := map[string]func(string) (bool, error){
|
||||
UC: checkUC,
|
||||
Aliyun: checkAliyun,
|
||||
Quark: checkQuark,
|
||||
Pan115: check115,
|
||||
Pan123: check123pan,
|
||||
Tianyi: checkTianyi,
|
||||
Xunlei: checkXunlei,
|
||||
Baidu: checkBaidu,
|
||||
}
|
||||
|
||||
if fn, ok := checkFunctions[service]; ok {
|
||||
result, err := fn(shareID)
|
||||
if err != nil {
|
||||
return CheckResult{URL: urlStr, Status: false}, err
|
||||
}
|
||||
return CheckResult{URL: urlStr, Status: result}, nil
|
||||
}
|
||||
|
||||
fmt.Printf("未找到服务 %s 的检测函数\n", service)
|
||||
return CheckResult{URL: urlStr, Status: false}, nil
|
||||
}
|
||||
|
||||
// 主函数
|
||||
func Test() {
|
||||
urls := []string{
|
||||
// UC网盘
|
||||
"https://drive.uc.cn/s/e1ebe95d144c4?public=1", // UC网盘有效
|
||||
"https://drive.uc.cn/s/m7and23e132a1?public=1", // UC网盘无效
|
||||
// 阿里云
|
||||
"https://www.aliyundrive.com/s/hz1HHxhahsE", // aliyundrive 公开分享
|
||||
"https://www.alipan.com/s/QbaHJ71QjV1", // alipan 公开分享
|
||||
"https://www.alipan.com/s/GMrv1QCZhNB", // 带提取码
|
||||
"https://www.aliyundrive.com/s/p51zbVtgmy", // 链接错误 NotFound.ShareLink
|
||||
"https://www.aliyundrive.com/s/hZnj4qLMMd9", // 空文件
|
||||
// 115
|
||||
"https://115cdn.com/s/swh88n13z72?password=r9b2#",
|
||||
"https://anxia.com/s/swhm75q3z5o?password=ayss",
|
||||
"https://115.com/s/swhsaua36a1?password=oc92", // 带访问码
|
||||
"https://115.com/s/sw313r03zx1", // 分享的文件涉嫌违规,链接已失效
|
||||
// 夸克
|
||||
"https://pan.quark.cn/s/9803af406f13", // 公开分享
|
||||
"https://pan.quark.cn/s/f161a5364657", // 提取码
|
||||
"https://pan.quark.cn/s/9803af406f15", // 分享不存在
|
||||
"https://pan.quark.cn/s/b999385c0936", // 违规
|
||||
"https://pan.quark.cn/s/c66f71b6f7d5", // 取消分享
|
||||
// 123
|
||||
"https://www.123pan.com/s/i4uaTd-WHn0", // 公开分享
|
||||
"https://www.123912.com/s/U8f2Td-ZeOX",
|
||||
"https://www.123684.com/s/u9izjv-k3uWv",
|
||||
"https://www.123pan.com/s/A6cA-AKH11", // 外链不存在
|
||||
// 天翼
|
||||
"https://cloud.189.cn/t/viy2quQzMBne", // 公开分享
|
||||
"https://cloud.189.cn/web/share?code=UfUjiiFRbymq", // 带密码分享长链接
|
||||
"https://cloud.189.cn/t/vENFvuVNbyqa", // 外链不存在
|
||||
"https://cloud.189.cn/t/notexist", // 分享不存在
|
||||
// 百度
|
||||
"https://pan.baidu.com/s/1rIcc6X7D3rVzNSqivsRejw?pwd=0w0j", // 带提取码分享
|
||||
"https://pan.baidu.com/s/1TMhfQ5yNnlPPSGbw4RQ-LA?pwd=6j77", // 带提取码分享
|
||||
"https://pan.baidu.com/s/1J_CUxLKqC0h3Ypg4sQV0_g", // 无法识别
|
||||
"https://pan.baidu.com/s/1HlvGfj8qVUBym24X2I9ukA", // 分享被和谐
|
||||
"https://pan.baidu.com/s/1cgsY10lkrPGZ-zt8oVdR_w", // 分享已过期
|
||||
"https://pan.baidu.com/s/1R_itrvmA0ZyMMaHybg7G2Q", // 分享已删除
|
||||
"https://pan.baidu.com/s/1hqge8hI", // 分享链接错误
|
||||
"https://pan.baidu.com/s/1notexist", // 分享不存在
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan CheckResult, len(urls))
|
||||
|
||||
for _, url := range urls {
|
||||
wg.Add(1)
|
||||
go func(u string) {
|
||||
defer wg.Done()
|
||||
result, err := CheckURL(u)
|
||||
if err != nil {
|
||||
fmt.Printf("检查 %s 时出错: %v\n", u, err)
|
||||
result = CheckResult{URL: u, Status: false}
|
||||
}
|
||||
results <- result
|
||||
}(url)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
fmt.Println("\n检测结果:")
|
||||
for result := range results {
|
||||
fmt.Printf("%s - %s\n", result.URL, map[bool]string{true: "有效", false: "无效"}[result.Status])
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
// 统一响应解析函数
|
||||
export const parseApiResponse = <T>(response: any): T => {
|
||||
// 检查是否是新的统一响应格式
|
||||
if (response && typeof response === 'object' && 'code' in response && 'data' in response) {
|
||||
if (response.code === 200) {
|
||||
// 特殊处理pan接口返回的data.list格式
|
||||
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||
return response.data.list
|
||||
}
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.message || '请求失败')
|
||||
}
|
||||
}
|
||||
// 兼容旧格式,直接返回响应
|
||||
return response
|
||||
}
|
||||
|
||||
// 使用 $fetch 替代 axios,更好地处理 SSR
|
||||
export const useResourceApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
@@ -8,60 +26,67 @@ export const useResourceApi = () => {
|
||||
}
|
||||
|
||||
const getResources = async (params?: any) => {
|
||||
return await $fetch('/resources', {
|
||||
const response = await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getResource = async (id: number) => {
|
||||
return await $fetch(`/resources/${id}`, {
|
||||
const response = await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createResource = async (data: any) => {
|
||||
return await $fetch('/resources', {
|
||||
const response = await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateResource = async (id: number, data: any) => {
|
||||
return await $fetch(`/resources/${id}`, {
|
||||
const response = await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteResource = async (id: number) => {
|
||||
return await $fetch(`/resources/${id}`, {
|
||||
const response = await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const searchResources = async (params: any) => {
|
||||
return await $fetch('/search', {
|
||||
const response = await $fetch('/search', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getResourcesByPan = async (panId: number, params?: any) => {
|
||||
return await $fetch('/resources', {
|
||||
const response = await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params: { ...params, pan_id: panId },
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -80,27 +105,30 @@ export const useAuthApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const login = async (data: any) => {
|
||||
return await $fetch('/auth/login', {
|
||||
const response = await $fetch('/auth/login', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const register = async (data: any) => {
|
||||
return await $fetch('/auth/register', {
|
||||
const response = await $fetch('/auth/register', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getProfile = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return await $fetch('/auth/profile', {
|
||||
const response = await $fetch('/auth/profile', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -120,36 +148,40 @@ export const useCategoryApi = () => {
|
||||
}
|
||||
|
||||
const getCategories = async () => {
|
||||
return await $fetch('/categories', {
|
||||
const response = await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createCategory = async (data: any) => {
|
||||
return await $fetch('/categories', {
|
||||
const response = await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateCategory = async (id: number, data: any) => {
|
||||
return await $fetch(`/categories/${id}`, {
|
||||
const response = await $fetch(`/categories/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteCategory = async (id: number) => {
|
||||
return await $fetch(`/categories/${id}`, {
|
||||
const response = await $fetch(`/categories/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -170,43 +202,48 @@ export const usePanApi = () => {
|
||||
}
|
||||
|
||||
const getPans = async () => {
|
||||
return await $fetch('/pans', {
|
||||
const response = await $fetch('/pans', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getPan = async (id: number) => {
|
||||
return await $fetch(`/pans/${id}`, {
|
||||
const response = await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createPan = async (data: any) => {
|
||||
return await $fetch('/pans', {
|
||||
const response = await $fetch('/pans', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updatePan = async (id: number, data: any) => {
|
||||
return await $fetch(`/pans/${id}`, {
|
||||
const response = await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deletePan = async (id: number) => {
|
||||
return await $fetch(`/pans/${id}`, {
|
||||
const response = await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -228,44 +265,49 @@ export const useCksApi = () => {
|
||||
}
|
||||
|
||||
const getCks = async (params?: any) => {
|
||||
return await $fetch('/cks', {
|
||||
const response = await $fetch('/cks', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getCksByID = async (id: number) => {
|
||||
return await $fetch(`/cks/${id}`, {
|
||||
const response = await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createCks = async (data: any) => {
|
||||
return await $fetch('/cks', {
|
||||
const response = await $fetch('/cks', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateCks = async (id: number, data: any) => {
|
||||
return await $fetch(`/cks/${id}`, {
|
||||
const response = await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteCks = async (id: number) => {
|
||||
return await $fetch(`/cks/${id}`, {
|
||||
const response = await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -287,50 +329,56 @@ export const useTagApi = () => {
|
||||
}
|
||||
|
||||
const getTags = async () => {
|
||||
return await $fetch('/tags', {
|
||||
const response = await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getTag = async (id: number) => {
|
||||
return await $fetch(`/tags/${id}`, {
|
||||
const response = await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createTag = async (data: any) => {
|
||||
return await $fetch('/tags', {
|
||||
const response = await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateTag = async (id: number, data: any) => {
|
||||
return await $fetch(`/tags/${id}`, {
|
||||
const response = await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteTag = async (id: number) => {
|
||||
return await $fetch(`/tags/${id}`, {
|
||||
const response = await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getResourceTags = async (resourceId: number) => {
|
||||
return await $fetch(`/resources/${resourceId}/tags`, {
|
||||
const response = await $fetch(`/resources/${resourceId}/tags`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -353,57 +401,63 @@ export const useReadyResourceApi = () => {
|
||||
}
|
||||
|
||||
const getReadyResources = async (params?: any) => {
|
||||
return await $fetch('/ready-resources', {
|
||||
const response = await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createReadyResource = async (data: any) => {
|
||||
return await $fetch('/ready-resources', {
|
||||
const response = await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const batchCreateReadyResources = async (data: any) => {
|
||||
return await $fetch('/ready-resources/batch', {
|
||||
const response = await $fetch('/ready-resources/batch', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createReadyResourcesFromText = async (text: string) => {
|
||||
const formData = new FormData()
|
||||
formData.append('text', text)
|
||||
|
||||
return await $fetch('/ready-resources/text', {
|
||||
const response = await $fetch('/ready-resources/text', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteReadyResource = async (id: number) => {
|
||||
return await $fetch(`/ready-resources/${id}`, {
|
||||
const response = await $fetch(`/ready-resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const clearReadyResources = async () => {
|
||||
return await $fetch('/ready-resources', {
|
||||
const response = await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -421,9 +475,10 @@ export const useStatsApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getStats = async () => {
|
||||
return await $fetch('/stats', {
|
||||
const response = await $fetch('/stats', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -441,19 +496,21 @@ export const useSystemConfigApi = () => {
|
||||
}
|
||||
|
||||
const getSystemConfig = async () => {
|
||||
return await $fetch('/system/config', {
|
||||
const response = await $fetch('/system/config', {
|
||||
baseURL: config.public.apiBase,
|
||||
// GET接口不需要认证头
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateSystemConfig = async (data: any) => {
|
||||
return await $fetch('/system/config', {
|
||||
const response = await $fetch('/system/config', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -461,3 +518,98 @@ export const useSystemConfigApi = () => {
|
||||
updateSystemConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// 热播剧相关API
|
||||
export const useHotDramaApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getHotDramas = async () => {
|
||||
const response = await $fetch('/hot-dramas', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createHotDrama = async (data: any) => {
|
||||
const response = await $fetch('/hot-dramas', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateHotDrama = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/hot-dramas/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteHotDrama = async (id: number) => {
|
||||
const response = await $fetch(`/hot-dramas/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const fetchHotDramas = async () => {
|
||||
const response = await $fetch('/hot-dramas/fetch', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getHotDramas,
|
||||
createHotDrama,
|
||||
updateHotDrama,
|
||||
deleteHotDrama,
|
||||
fetchHotDramas,
|
||||
}
|
||||
}
|
||||
|
||||
// 监控相关API
|
||||
export const useMonitorApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getPerformanceStats = async () => {
|
||||
const response = await $fetch('/performance', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getSystemInfo = async () => {
|
||||
const response = await $fetch('/system/info', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getBasicStats = async () => {
|
||||
const response = await $fetch('/stats', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getPerformanceStats,
|
||||
getSystemInfo,
|
||||
getBasicStats,
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// 初始化用户状态
|
||||
userStore.initAuth()
|
||||
|
||||
// 如果用户未登录,重定向到首页
|
||||
// 如果用户未登录,重定向到登录页面
|
||||
if (!userStore.isAuthenticated) {
|
||||
return navigateTo('/')
|
||||
return navigateTo('/login')
|
||||
}
|
||||
})
|
||||
@@ -320,11 +320,9 @@ const fetchSystemConfig = async () => {
|
||||
try {
|
||||
const response = await getSystemConfig()
|
||||
console.log('admin系统配置响应:', response)
|
||||
if (response && response.success && response.data) {
|
||||
systemConfig.value = response.data
|
||||
} else if (response && response.data) {
|
||||
// 兼容非标准格式
|
||||
systemConfig.value = response.data
|
||||
// 使用新的统一响应格式,直接使用response
|
||||
if (response) {
|
||||
systemConfig.value = response
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
|
||||
214
web/pages/hot-dramas.vue
Normal file
214
web/pages/hot-dramas.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">热播剧榜单</h1>
|
||||
<p class="text-gray-600">实时获取豆瓣热门电影和电视剧榜单</p>
|
||||
</div>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div class="mb-6 flex flex-wrap gap-4">
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="category.value"
|
||||
@click="selectedCategory = category.value"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-lg font-medium transition-colors',
|
||||
selectedCategory === category.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
|
||||
]"
|
||||
>
|
||||
{{ category.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
|
||||
<!-- 热播剧列表 -->
|
||||
<div v-else-if="dramas.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<div
|
||||
v-for="drama in filteredDramas"
|
||||
:key="drama.id"
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<!-- 剧集信息 -->
|
||||
<div class="p-6">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-gray-900 line-clamp-2">
|
||||
{{ drama.title }}
|
||||
</h3>
|
||||
<div class="flex items-center ml-2">
|
||||
<span class="text-yellow-500 text-sm font-medium">{{ drama.rating }}</span>
|
||||
<span class="text-gray-400 text-xs ml-1">分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 年份和分类 -->
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span v-if="drama.year" class="text-sm text-gray-500">{{ drama.year }}</span>
|
||||
<span class="text-sm text-blue-600 bg-blue-100 px-2 py-1 rounded">
|
||||
{{ drama.category }}
|
||||
</span>
|
||||
<span v-if="drama.sub_type" class="text-sm text-gray-500 bg-gray-100 px-2 py-1 rounded">
|
||||
{{ drama.sub_type }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 导演 -->
|
||||
<div v-if="drama.directors" class="mb-2">
|
||||
<span class="text-xs text-gray-500">导演:</span>
|
||||
<span class="text-sm text-gray-700">{{ drama.directors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 演员 -->
|
||||
<div v-if="drama.actors" class="mb-3">
|
||||
<span class="text-xs text-gray-500">主演:</span>
|
||||
<span class="text-sm text-gray-700 line-clamp-2">{{ drama.actors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 数据来源 -->
|
||||
<div class="flex items-center justify-between text-xs text-gray-400">
|
||||
<span>来源:{{ drama.source }}</span>
|
||||
<span>{{ formatDate(drama.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="text-center py-12">
|
||||
<div class="text-gray-400 mb-4">
|
||||
<svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">暂无热播剧数据</h3>
|
||||
<p class="text-gray-500">请稍后再试或联系管理员</p>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="total > pageSize" class="mt-8 flex justify-center">
|
||||
<nav class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="changePage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="px-3 py-2 text-sm text-gray-700">
|
||||
第 {{ currentPage }} 页,共 {{ Math.ceil(total / pageSize) }} 页
|
||||
</span>
|
||||
<button
|
||||
@click="changePage(currentPage + 1)"
|
||||
:disabled="currentPage >= Math.ceil(total / pageSize)"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dramas = ref([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const selectedCategory = ref('')
|
||||
|
||||
// 分类选项
|
||||
const categories = ref([
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '电影', value: '电影' },
|
||||
{ label: '电视剧', value: '电视剧' }
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const filteredDramas = computed(() => {
|
||||
if (!selectedCategory.value) {
|
||||
return dramas.value
|
||||
}
|
||||
return dramas.value.filter(drama => drama.category === selectedCategory.value)
|
||||
})
|
||||
|
||||
// 获取热播剧列表
|
||||
const fetchDramas = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { useHotDramaApi } = await import('~/composables/useApi')
|
||||
const hotDramaApi = useHotDramaApi()
|
||||
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value
|
||||
}
|
||||
|
||||
if (selectedCategory.value) {
|
||||
params.category = selectedCategory.value
|
||||
}
|
||||
|
||||
const response = await hotDramaApi.getHotDramas(params)
|
||||
|
||||
// 使用新的统一响应格式
|
||||
if (response && response.items) {
|
||||
dramas.value = response.items
|
||||
total.value = response.total || 0
|
||||
} else {
|
||||
// 兼容旧格式
|
||||
dramas.value = Array.isArray(response) ? response : []
|
||||
total.value = dramas.value.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取热播剧列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
const changePage = (page) => {
|
||||
if (page >= 1 && page <= Math.ceil(total.value / pageSize.value)) {
|
||||
currentPage.value = page
|
||||
fetchDramas()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 监听分类变化
|
||||
watch(selectedCategory, () => {
|
||||
currentPage.value = 1
|
||||
fetchDramas()
|
||||
})
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
fetchDramas()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 flex flex-col">
|
||||
<!-- 全局加载状态 -->
|
||||
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
|
||||
@@ -13,7 +13,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="flex-1 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
@@ -22,6 +24,18 @@
|
||||
</a>
|
||||
</h1>
|
||||
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4 right-4 top-0 absolute">
|
||||
<NuxtLink
|
||||
to="/hot-dramas"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-film"></i> 热播剧
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/monitor"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
v-if="authInitialized && !userStore.isAuthenticated"
|
||||
to="/login"
|
||||
@@ -68,7 +82,7 @@
|
||||
class="px-2 py-1 text-xs rounded-full bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-100 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||
@click="filterByPlatform(platform.id)"
|
||||
>
|
||||
{{ getPlatformIcon(platform.name) }} {{ platform.name }}
|
||||
<span v-html="getPlatformIcon(platform.name)"></span> {{ platform.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -76,11 +90,11 @@
|
||||
<div class="flex justify-between mt-3 text-sm text-gray-600 dark:text-gray-300 px-2">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-day text-pink-600 mr-1"></i>
|
||||
今日更新: <span class="font-medium text-pink-600 ml-1 count-up" :data-target="todayUpdates">0</span>
|
||||
今日更新: <span class="font-medium text-pink-600 ml-1 count-up" :data-target="safeTodayUpdates">0</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-database text-blue-600 mr-1"></i>
|
||||
总资源数: <span class="font-medium text-blue-600 ml-1 count-up" :data-target="stats?.total_resources || 0">0</span>
|
||||
总资源数: <span class="font-medium text-blue-600 ml-1 count-up" :data-target="safeStats?.total_resources || 0">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,63 +114,75 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-if="loading" class="text-center py-8">
|
||||
<tr v-if="safeLoading" class="text-center py-8">
|
||||
<td colspan="3" class="text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="resources.length === 0" class="text-center py-8">
|
||||
<tr v-else-if="safeResources.length === 0" class="text-center py-8">
|
||||
<td colspan="3" class="text-gray-500 dark:text-gray-400">暂无数据</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="resource in (resources as unknown as ExtendedResource[])"
|
||||
:key="resource.id"
|
||||
:class="isUpdatedToday(resource.updated_at) ? 'hover:bg-pink-50 dark:hover:bg-pink-900 bg-pink-50/30 dark:bg-pink-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm">
|
||||
<div class="flex items-start">
|
||||
<span class="mr-2 flex-shrink-0">{{ getPlatformIcon(getPlatformName(resource.pan_id || 0)) }}</span>
|
||||
<span class="break-words">{{ resource.title }}</span>
|
||||
</div>
|
||||
<div class="sm:hidden mt-1">
|
||||
<button
|
||||
class="text-blue-600 hover:text-blue-800 text-xs flex items-center gap-1 show-link-btn"
|
||||
@click="toggleLink(resource)"
|
||||
>
|
||||
<i class="fas fa-eye"></i> 显示链接
|
||||
</button>
|
||||
<a
|
||||
v-if="resource.showLink"
|
||||
:href="resource.url"
|
||||
target="_blank"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline text-xs break-all"
|
||||
>
|
||||
{{ resource.url }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm hidden sm:table-cell">
|
||||
<button
|
||||
class="text-blue-600 hover:text-blue-800 flex items-center gap-1 show-link-btn"
|
||||
@click="toggleLink(resource)"
|
||||
>
|
||||
<i class="fas fa-eye"></i> 显示链接
|
||||
</button>
|
||||
<a
|
||||
v-if="resource.showLink"
|
||||
:href="resource.url"
|
||||
target="_blank"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline"
|
||||
>
|
||||
{{ resource.url }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-gray-500" :title="resource.updated_at">
|
||||
{{ formatRelativeTime(resource.updated_at) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(resource, index) in visibleResources"
|
||||
:key="resource.id"
|
||||
:class="isUpdatedToday(resource.updated_at) ? 'hover:bg-pink-50 dark:hover:bg-pink-900 bg-pink-50/30 dark:bg-pink-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-800'"
|
||||
v-intersection="onIntersection"
|
||||
:data-index="index"
|
||||
>
|
||||
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm">
|
||||
<div class="flex items-start">
|
||||
<span class="mr-2 flex-shrink-0" v-html="getPlatformIcon(getPlatformName(resource.pan_id || 0))"></span>
|
||||
<span class="break-words">{{ resource.title }}</span>
|
||||
</div>
|
||||
<div class="sm:hidden mt-1">
|
||||
<button
|
||||
class="text-blue-600 hover:text-blue-800 text-xs flex items-center gap-1 show-link-btn"
|
||||
@click="toggleLink(resource)"
|
||||
>
|
||||
<i class="fas fa-eye"></i> 显示链接
|
||||
</button>
|
||||
<a
|
||||
v-if="resource.showLink"
|
||||
:href="resource.url"
|
||||
target="_blank"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline text-xs break-all"
|
||||
>
|
||||
{{ resource.url }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm hidden sm:table-cell">
|
||||
<button
|
||||
class="text-blue-600 hover:text-blue-800 flex items-center gap-1 show-link-btn"
|
||||
@click="toggleLink(resource)"
|
||||
>
|
||||
<i class="fas fa-eye"></i> 显示链接
|
||||
</button>
|
||||
<a
|
||||
v-if="resource.showLink"
|
||||
:href="resource.url"
|
||||
target="_blank"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline"
|
||||
>
|
||||
{{ resource.url }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-gray-500" :title="resource.updated_at">
|
||||
{{ formatRelativeTime(resource.updated_at) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 加载更多按钮 -->
|
||||
<div v-if="hasMoreData && !safeLoading" class="text-center py-4">
|
||||
<button
|
||||
@click="loadMore"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
加载更多
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
@@ -212,10 +238,11 @@
|
||||
@close="closeModal"
|
||||
@save="handleSaveResource"
|
||||
/> -->
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="mt-8 py-6 border-t border-gray-200">
|
||||
<div class="max-w-7xl mx-auto text-center text-gray-600 text-sm">
|
||||
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
|
||||
<p class="mb-2">本站内容由网络爬虫自动抓取。本站不储存、复制、传播任何文件,仅作个人公益学习,请在获取后24小内删除!!!</p>
|
||||
<p>{{ systemConfig?.copyright || '© 2025 网盘资源管理系统 By 小七' }}</p>
|
||||
</div>
|
||||
@@ -224,10 +251,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const store = useResourceStore()
|
||||
const userStore = useUserStore()
|
||||
const { resources, categories, stats, loading } = storeToRefs(store)
|
||||
|
||||
// 响应式数据
|
||||
const searchQuery = ref('')
|
||||
const selectedPlatform = ref('')
|
||||
@@ -235,6 +258,76 @@ const authInitialized = ref(false) // 添加认证状态初始化标志
|
||||
const pageLoading = ref(true) // 添加页面加载状态
|
||||
const systemConfig = ref<SystemConfig | null>(null) // 添加系统配置状态
|
||||
|
||||
// 虚拟滚动相关
|
||||
const visibleResources = ref<any[]>([])
|
||||
const hasMoreData = ref(true)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const isLoadingMore = ref(false)
|
||||
|
||||
// 延迟初始化store,避免SSR过程中的错误
|
||||
let store: any = null
|
||||
let userStore: any = null
|
||||
|
||||
// 本地状态管理,避免SSR过程中的store访问
|
||||
const localResources = ref<any[]>([])
|
||||
const localCategories = ref<any[]>([])
|
||||
const localStats = ref<any>({ total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0 })
|
||||
const localLoading = ref(false)
|
||||
|
||||
// 安全的store访问
|
||||
const safeResources = computed(() => {
|
||||
try {
|
||||
if (process.client && store) {
|
||||
const storeRefs = storeToRefs(store)
|
||||
return (storeRefs as any).resources?.value || localResources.value
|
||||
}
|
||||
return localResources.value
|
||||
} catch (error) {
|
||||
console.error('获取resources时出错:', error)
|
||||
return localResources.value
|
||||
}
|
||||
})
|
||||
|
||||
const safeCategories = computed(() => {
|
||||
try {
|
||||
if (process.client && store) {
|
||||
const storeRefs = storeToRefs(store)
|
||||
return (storeRefs as any).categories?.value || localCategories.value
|
||||
}
|
||||
return localCategories.value
|
||||
} catch (error) {
|
||||
console.error('获取categories时出错:', error)
|
||||
return localCategories.value
|
||||
}
|
||||
})
|
||||
|
||||
const safeStats = computed(() => {
|
||||
try {
|
||||
if (process.client && store) {
|
||||
const storeRefs = storeToRefs(store)
|
||||
return (storeRefs as any).stats?.value || localStats.value
|
||||
}
|
||||
return localStats.value
|
||||
} catch (error) {
|
||||
console.error('获取stats时出错:', error)
|
||||
return localStats.value
|
||||
}
|
||||
})
|
||||
|
||||
const safeLoading = computed(() => {
|
||||
try {
|
||||
if (process.client && store) {
|
||||
const storeRefs = storeToRefs(store)
|
||||
return (storeRefs as any).loading?.value || localLoading.value
|
||||
}
|
||||
return localLoading.value
|
||||
} catch (error) {
|
||||
console.error('获取loading时出错:', error)
|
||||
return localLoading.value
|
||||
}
|
||||
})
|
||||
|
||||
// 动态SEO配置
|
||||
const seoConfig = computed(() => ({
|
||||
title: systemConfig.value?.site_title || '网盘资源管理系统',
|
||||
@@ -260,30 +353,44 @@ const seoConfig = computed(() => ({
|
||||
|
||||
// 页面元数据 - 使用watchEffect来避免组件卸载时的错误
|
||||
watchEffect(() => {
|
||||
if (systemConfig.value) {
|
||||
useHead({
|
||||
title: systemConfig.value.site_title || '网盘资源管理系统',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: systemConfig.value.site_description || '专业的网盘资源管理系统'
|
||||
},
|
||||
{
|
||||
name: 'keywords',
|
||||
content: systemConfig.value.keywords || '网盘,资源管理,文件分享'
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
content: systemConfig.value.author || '系统管理员'
|
||||
},
|
||||
{
|
||||
name: 'copyright',
|
||||
content: systemConfig.value.copyright || '© 2024 网盘资源管理系统'
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
// 默认SEO配置
|
||||
try {
|
||||
if (systemConfig.value && systemConfig.value.site_title) {
|
||||
useHead({
|
||||
title: systemConfig.value.site_title || '网盘资源管理系统',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: systemConfig.value.site_description || '专业的网盘资源管理系统'
|
||||
},
|
||||
{
|
||||
name: 'keywords',
|
||||
content: systemConfig.value.keywords || '网盘,资源管理,文件分享'
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
content: systemConfig.value.author || '系统管理员'
|
||||
},
|
||||
{
|
||||
name: 'copyright',
|
||||
content: systemConfig.value.copyright || '© 2024 网盘资源管理系统'
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
// 默认SEO配置
|
||||
useHead({
|
||||
title: '网盘资源管理系统',
|
||||
meta: [
|
||||
{ name: 'description', content: '专业的网盘资源管理系统' },
|
||||
{ name: 'keywords', content: '网盘,资源管理,文件分享' },
|
||||
{ name: 'author', content: '系统管理员' },
|
||||
{ name: 'copyright', content: '© 2024 网盘资源管理系统' }
|
||||
]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置页面元数据时出错:', error)
|
||||
// 使用默认配置作为后备
|
||||
useHead({
|
||||
title: '网盘资源管理系统',
|
||||
meta: [
|
||||
@@ -301,7 +408,6 @@ watchEffect(() => {
|
||||
|
||||
// const showAddResourceModal = ref(false)
|
||||
const editingResource = ref<any>(null)
|
||||
const currentPage = ref(1)
|
||||
const totalPages = ref(1)
|
||||
interface Platform {
|
||||
id: number
|
||||
@@ -353,6 +459,29 @@ interface SystemConfig {
|
||||
const platforms = ref<Platform[]>([])
|
||||
const todayUpdates = ref(0)
|
||||
|
||||
// 安全地计算今日更新数量
|
||||
const safeTodayUpdates = computed(() => {
|
||||
try {
|
||||
const resources = safeResources.value
|
||||
if (!resources || !Array.isArray(resources)) {
|
||||
return 0
|
||||
}
|
||||
const today = new Date().toDateString()
|
||||
return resources.filter((resource: any) => {
|
||||
if (!resource || !resource.updated_at) return false
|
||||
try {
|
||||
return new Date(resource.updated_at).toDateString() === today
|
||||
} catch (dateError) {
|
||||
console.error('解析日期时出错:', dateError)
|
||||
return false
|
||||
}
|
||||
}).length
|
||||
} catch (error) {
|
||||
console.error('计算今日更新数量时出错:', error)
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
// 防抖搜索
|
||||
let searchTimeout: NodeJS.Timeout
|
||||
const debounceSearch = () => {
|
||||
@@ -383,22 +512,53 @@ const fetchSystemConfig = async () => {
|
||||
onMounted(async () => {
|
||||
console.log('首页 - onMounted 开始')
|
||||
|
||||
// 初始化用户状态
|
||||
userStore.initAuth()
|
||||
authInitialized.value = true // 设置认证状态初始化完成
|
||||
|
||||
console.log('首页 - authInitialized:', authInitialized.value)
|
||||
console.log('首页 - isAuthenticated:', userStore.isAuthenticated)
|
||||
console.log('首页 - user:', userStore.userInfo)
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
store.fetchResources(),
|
||||
store.fetchCategories(),
|
||||
store.fetchStats(),
|
||||
fetchPlatforms(),
|
||||
fetchSystemConfig(), // 获取系统配置
|
||||
// 初始化store
|
||||
store = useResourceStore()
|
||||
userStore = useUserStore()
|
||||
|
||||
// 初始化用户状态
|
||||
userStore.initAuth()
|
||||
authInitialized.value = true // 设置认证状态初始化完成
|
||||
|
||||
console.log('首页 - authInitialized:', authInitialized.value)
|
||||
console.log('首页 - isAuthenticated:', userStore.isAuthenticated)
|
||||
console.log('首页 - user:', userStore.userInfo)
|
||||
|
||||
// 使用Promise.allSettled来确保即使某个请求失败也不会影响其他请求
|
||||
const results = await Promise.allSettled([
|
||||
store.fetchResources().then((data: any) => {
|
||||
localResources.value = data.resources || []
|
||||
return data
|
||||
}).catch((e: any) => {
|
||||
console.error('获取资源失败:', e)
|
||||
return { resources: [] }
|
||||
}),
|
||||
store.fetchCategories().then((data: any) => {
|
||||
localCategories.value = data.categories || []
|
||||
return data
|
||||
}).catch((e: any) => {
|
||||
console.error('获取分类失败:', e)
|
||||
return { categories: [] }
|
||||
}),
|
||||
store.fetchStats().then((data: any) => {
|
||||
localStats.value = data || { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0 }
|
||||
return data
|
||||
}).catch((e: any) => {
|
||||
console.error('获取统计失败:', e)
|
||||
return { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0 }
|
||||
}),
|
||||
fetchPlatforms().catch((e: any) => console.error('获取平台失败:', e)),
|
||||
fetchSystemConfig().catch((e: any) => console.error('获取系统配置失败:', e)),
|
||||
])
|
||||
|
||||
// 检查哪些请求成功了
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.error(`请求 ${index} 失败:`, result.reason)
|
||||
}
|
||||
})
|
||||
|
||||
animateCounters()
|
||||
} catch (error) {
|
||||
console.error('页面数据加载失败:', error)
|
||||
@@ -415,7 +575,9 @@ const fetchPlatforms = async () => {
|
||||
const { usePanApi } = await import('~/composables/useApi')
|
||||
const panApi = usePanApi()
|
||||
const response = await panApi.getPans() as any
|
||||
platforms.value = response.pans || []
|
||||
// 后端直接返回数组,不需要 .pans
|
||||
platforms.value = Array.isArray(response) ? response : []
|
||||
console.log('获取到的平台数据:', platforms.value)
|
||||
} catch (error) {
|
||||
console.error('获取平台列表失败:', error)
|
||||
}
|
||||
@@ -423,8 +585,20 @@ const fetchPlatforms = async () => {
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
const platformId = selectedPlatform.value ? parseInt(selectedPlatform.value) : undefined
|
||||
store.searchResources(searchQuery.value, platformId)
|
||||
try {
|
||||
if (!store || !process.client) {
|
||||
console.error('store未初始化或不在客户端')
|
||||
return
|
||||
}
|
||||
const platformId = selectedPlatform.value ? parseInt(selectedPlatform.value) : undefined
|
||||
store.searchResources(searchQuery.value, platformId).then((data: any) => {
|
||||
localResources.value = data.resources || []
|
||||
}).catch((error: any) => {
|
||||
console.error('搜索失败:', error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('搜索处理时出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 按平台筛选
|
||||
@@ -445,10 +619,18 @@ const getPlatformIcon = (platformName: string) => {
|
||||
// 如果找不到对应的平台或没有图标,使用默认图标
|
||||
const defaultIcons: Record<string, string> = {
|
||||
'unknown': '<i class="fas fa-question-circle text-gray-400"></i>',
|
||||
'other': '<i class="fas fa-cloud text-gray-500"></i>'
|
||||
'other': '<i class="fas fa-cloud text-gray-500"></i>',
|
||||
'magnet': '<i class="fas fa-magnet text-red-600"></i>',
|
||||
'uc': '<i class="fas fa-cloud-download-alt text-purple-600"></i>',
|
||||
'夸克网盘': '<i class="fas fa-cloud text-blue-600"></i>',
|
||||
'阿里云盘': '<i class="fas fa-cloud text-orange-600"></i>',
|
||||
'百度网盘': '<i class="fas fa-cloud text-blue-500"></i>',
|
||||
'天翼云盘': '<i class="fas fa-cloud text-red-500"></i>',
|
||||
'OneDrive': '<i class="fas fa-cloud text-blue-700"></i>',
|
||||
'Google Drive': '<i class="fas fa-cloud text-green-600"></i>'
|
||||
}
|
||||
|
||||
return defaultIcons['unknown']
|
||||
return defaultIcons[platformName] || defaultIcons['unknown']
|
||||
}
|
||||
|
||||
// 获取平台名称
|
||||
@@ -576,6 +758,50 @@ const handleSaveResource = async (resourceData: any) => {
|
||||
console.error('保存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 虚拟滚动相关方法
|
||||
const onIntersection = (entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && hasMoreData.value && !isLoadingMore.value) {
|
||||
loadMore()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
if (isLoadingMore.value || !hasMoreData.value) return
|
||||
|
||||
isLoadingMore.value = true
|
||||
try {
|
||||
currentPage.value++
|
||||
const newResources = await fetchResources(currentPage.value, pageSize.value)
|
||||
if (newResources && newResources.length > 0) {
|
||||
visibleResources.value.push(...newResources)
|
||||
} else {
|
||||
hasMoreData.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载更多失败:', error)
|
||||
currentPage.value-- // 回退页码
|
||||
} finally {
|
||||
isLoadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchResources = async (page: number, size: number) => {
|
||||
try {
|
||||
const { useResourceApi } = await import('~/composables/useApi')
|
||||
const resourceApi = useResourceApi()
|
||||
const response = await resourceApi.getResources({
|
||||
page,
|
||||
page_size: size
|
||||
}) as any
|
||||
return response.resources || []
|
||||
} catch (error) {
|
||||
console.error('获取资源失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
348
web/pages/monitor.vue
Normal file
348
web/pages/monitor.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">系统性能监控</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">实时监控系统运行状态和性能指标</p>
|
||||
</div>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<div class="mb-6 flex justify-between items-center">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="refreshData"
|
||||
:disabled="loading"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
||||
>
|
||||
<i class="fas fa-sync-alt" :class="{ 'fa-spin': loading }"></i>
|
||||
<span>{{ loading ? '刷新中...' : '刷新数据' }}</span>
|
||||
</button>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
最后更新: {{ lastUpdateTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">自动刷新:</label>
|
||||
<input
|
||||
v-model="autoRefresh"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{ autoRefreshInterval }}秒</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
|
||||
<!-- 监控数据 -->
|
||||
<div v-else class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<!-- 系统信息卡片 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-server mr-2 text-blue-600"></i>
|
||||
系统信息
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">运行时间:</span>
|
||||
<span class="font-medium">{{ systemInfo.uptime || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">启动时间:</span>
|
||||
<span class="font-medium">{{ systemInfo.start_time || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">版本:</span>
|
||||
<span class="font-medium">{{ systemInfo.version || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">运行模式:</span>
|
||||
<span class="font-medium">{{ systemInfo.environment?.gin_mode || 'N/A' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内存使用卡片 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-memory mr-2 text-green-600"></i>
|
||||
内存使用
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">当前分配:</span>
|
||||
<span class="font-medium">{{ formatBytes(performanceStats.memory?.alloc) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">总分配:</span>
|
||||
<span class="font-medium">{{ formatBytes(performanceStats.memory?.total_alloc) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">系统内存:</span>
|
||||
<span class="font-medium">{{ formatBytes(performanceStats.memory?.sys) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">堆内存:</span>
|
||||
<span class="font-medium">{{ formatBytes(performanceStats.memory?.heap_alloc) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">GC次数:</span>
|
||||
<span class="font-medium">{{ performanceStats.memory?.num_gc || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据库连接卡片 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-database mr-2 text-purple-600"></i>
|
||||
数据库连接
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">最大连接数:</span>
|
||||
<span class="font-medium">{{ performanceStats.database?.max_open_connections || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">当前连接数:</span>
|
||||
<span class="font-medium">{{ performanceStats.database?.open_connections || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">使用中:</span>
|
||||
<span class="font-medium">{{ performanceStats.database?.in_use || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">空闲:</span>
|
||||
<span class="font-medium">{{ performanceStats.database?.idle || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统资源卡片 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-microchip mr-2 text-orange-600"></i>
|
||||
系统资源
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">CPU核心数:</span>
|
||||
<span class="font-medium">{{ performanceStats.system?.cpu_count || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Go版本:</span>
|
||||
<span class="font-medium">{{ performanceStats.system?.go_version || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">协程数:</span>
|
||||
<span class="font-medium">{{ performanceStats.goroutines || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">时间戳:</span>
|
||||
<span class="font-medium">{{ formatTimestamp(performanceStats.timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基础统计卡片 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-chart-bar mr-2 text-red-600"></i>
|
||||
基础统计
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">资源总数:</span>
|
||||
<span class="font-medium">{{ basicStats.total_resources || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">分类总数:</span>
|
||||
<span class="font-medium">{{ basicStats.total_categories || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">标签总数:</span>
|
||||
<span class="font-medium">{{ basicStats.total_tags || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">总浏览量:</span>
|
||||
<span class="font-medium">{{ basicStats.total_views || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能图表卡片 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-chart-line mr-2 text-indigo-600"></i>
|
||||
性能趋势
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="text-center py-4">
|
||||
<div class="text-2xl font-bold text-indigo-600">
|
||||
{{ formatBytes(performanceStats.memory?.alloc) }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">当前内存使用</div>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-indigo-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: memoryUsagePercentage + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
内存使用率: {{ memoryUsagePercentage.toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="error" class="mt-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const lastUpdateTime = ref('')
|
||||
const autoRefresh = ref(false)
|
||||
const autoRefreshInterval = ref(30)
|
||||
|
||||
// 监控数据
|
||||
const systemInfo = ref<any>({})
|
||||
const performanceStats = ref<any>({})
|
||||
const basicStats = ref<any>({})
|
||||
|
||||
// 计算内存使用率
|
||||
const memoryUsagePercentage = computed(() => {
|
||||
const memory = performanceStats.value.memory
|
||||
if (!memory || !memory.sys) return 0
|
||||
return (memory.alloc / memory.sys) * 100
|
||||
})
|
||||
|
||||
// 格式化字节数
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!bytes) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化时间戳
|
||||
const formatTimestamp = (timestamp: number) => {
|
||||
if (!timestamp) return 'N/A'
|
||||
return new Date(timestamp * 1000).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
const fetchSystemInfo = async () => {
|
||||
try {
|
||||
const { useMonitorApi } = await import('~/composables/useApi')
|
||||
const monitorApi = useMonitorApi()
|
||||
const response = await monitorApi.getSystemInfo()
|
||||
systemInfo.value = response
|
||||
} catch (error) {
|
||||
console.error('获取系统信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取性能统计
|
||||
const fetchPerformanceStats = async () => {
|
||||
try {
|
||||
const { useMonitorApi } = await import('~/composables/useApi')
|
||||
const monitorApi = useMonitorApi()
|
||||
const response = await monitorApi.getPerformanceStats()
|
||||
performanceStats.value = response
|
||||
} catch (error) {
|
||||
console.error('获取性能统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取基础统计
|
||||
const fetchBasicStats = async () => {
|
||||
try {
|
||||
const { useMonitorApi } = await import('~/composables/useApi')
|
||||
const monitorApi = useMonitorApi()
|
||||
const response = await monitorApi.getBasicStats()
|
||||
basicStats.value = response
|
||||
} catch (error) {
|
||||
console.error('获取基础统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新所有数据
|
||||
const refreshData = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchSystemInfo(),
|
||||
fetchPerformanceStats(),
|
||||
fetchBasicStats()
|
||||
])
|
||||
lastUpdateTime.value = new Date().toLocaleString('zh-CN')
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取监控数据失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新定时器
|
||||
let autoRefreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 监听自动刷新设置
|
||||
const startAutoRefresh = () => {
|
||||
if (autoRefresh.value) {
|
||||
autoRefreshTimer = setInterval(refreshData, autoRefreshInterval.value * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer)
|
||||
autoRefreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 监听自动刷新变化
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
refreshData()
|
||||
if (autoRefresh.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
// 页面卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
</style>
|
||||
@@ -127,6 +127,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动转存 -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
自动转存
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
开启后,系统将自动转存资源到其他网盘平台
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="config.autoTransferEnabled"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动拉取热播剧 -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
自动拉取热播剧
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
开启后,系统将自动从豆瓣获取热播剧信息
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="config.autoFetchHotDramaEnabled"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自动处理间隔 -->
|
||||
<div v-if="config.autoProcessReadyResources" class="ml-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
@@ -238,6 +282,8 @@ const config = ref({
|
||||
// 自动处理配置
|
||||
autoProcessReadyResources: false,
|
||||
autoProcessInterval: 30,
|
||||
autoTransferEnabled: false, // 新增
|
||||
autoFetchHotDramaEnabled: false, // 新增
|
||||
|
||||
// 其他配置
|
||||
pageSize: 100,
|
||||
@@ -272,33 +318,23 @@ const loadConfig = async () => {
|
||||
loading.value = true
|
||||
const response = await getSystemConfig()
|
||||
console.log('系统配置响应:', response)
|
||||
if (response && response.success && response.data) {
|
||||
|
||||
// 使用新的统一响应格式,直接使用response
|
||||
if (response) {
|
||||
config.value = {
|
||||
siteTitle: response.data.site_title || '网盘资源管理系统',
|
||||
siteDescription: response.data.site_description || '专业的网盘资源管理系统',
|
||||
keywords: response.data.keywords || '网盘,资源管理,文件分享',
|
||||
author: response.data.author || '系统管理员',
|
||||
copyright: response.data.copyright || '© 2024 网盘资源管理系统',
|
||||
autoProcessReadyResources: response.data.auto_process_ready_resources || false,
|
||||
autoProcessInterval: response.data.auto_process_interval || 30,
|
||||
pageSize: response.data.page_size || 100,
|
||||
maintenanceMode: response.data.maintenance_mode || false
|
||||
siteTitle: response.site_title || '网盘资源管理系统',
|
||||
siteDescription: response.site_description || '专业的网盘资源管理系统',
|
||||
keywords: response.keywords || '网盘,资源管理,文件分享',
|
||||
author: response.author || '系统管理员',
|
||||
copyright: response.copyright || '© 2024 网盘资源管理系统',
|
||||
autoProcessReadyResources: response.auto_process_ready_resources || false,
|
||||
autoProcessInterval: response.auto_process_interval || 30,
|
||||
autoTransferEnabled: response.auto_transfer_enabled || false, // 新增
|
||||
autoFetchHotDramaEnabled: response.auto_fetch_hot_drama_enabled || false, // 新增
|
||||
pageSize: response.page_size || 100,
|
||||
maintenanceMode: response.maintenance_mode || false
|
||||
}
|
||||
systemConfig.value = response.data // 更新系统配置状态
|
||||
} else if (response && response.data) {
|
||||
// 兼容非标准格式
|
||||
config.value = {
|
||||
siteTitle: response.data.site_title || '网盘资源管理系统',
|
||||
siteDescription: response.data.site_description || '专业的网盘资源管理系统',
|
||||
keywords: response.data.keywords || '网盘,资源管理,文件分享',
|
||||
author: response.data.author || '系统管理员',
|
||||
copyright: response.data.copyright || '© 2024 网盘资源管理系统',
|
||||
autoProcessReadyResources: response.data.auto_process_ready_resources || false,
|
||||
autoProcessInterval: response.data.auto_process_interval || 30,
|
||||
pageSize: response.data.page_size || 100,
|
||||
maintenanceMode: response.data.maintenance_mode || false
|
||||
}
|
||||
systemConfig.value = response.data
|
||||
systemConfig.value = response // 更新系统配置状态
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
@@ -321,19 +357,24 @@ const saveConfig = async () => {
|
||||
copyright: config.value.copyright,
|
||||
auto_process_ready_resources: config.value.autoProcessReadyResources,
|
||||
auto_process_interval: config.value.autoProcessInterval,
|
||||
auto_transfer_enabled: config.value.autoTransferEnabled, // 新增
|
||||
auto_fetch_hot_drama_enabled: config.value.autoFetchHotDramaEnabled, // 新增
|
||||
page_size: config.value.pageSize,
|
||||
maintenance_mode: config.value.maintenanceMode
|
||||
}
|
||||
|
||||
const response = await updateSystemConfig(requestData)
|
||||
if (response.success) {
|
||||
// 使用新的统一响应格式,直接检查response是否存在
|
||||
if (response) {
|
||||
alert('配置保存成功!')
|
||||
// 重新加载配置以获取最新数据
|
||||
await loadConfig()
|
||||
} else {
|
||||
alert('保存配置失败:' + (response.message || '未知错误'))
|
||||
alert('保存配置失败:未知错误')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
alert('保存配置失败,请重试')
|
||||
alert('保存配置失败:' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -70,28 +70,26 @@ export const useUserStore = defineStore('user', {
|
||||
this.loading = true
|
||||
try {
|
||||
const authApi = useAuthApi()
|
||||
const response = await authApi.login(credentials)
|
||||
const response = await authApi.login(credentials) as any
|
||||
|
||||
console.log('login - 响应:', response)
|
||||
|
||||
// 处理标准化的响应格式
|
||||
if (response.success && response.data) {
|
||||
const { token, user } = response.data
|
||||
if (token && user) {
|
||||
this.token = token
|
||||
this.user = user
|
||||
this.isAuthenticated = true
|
||||
// 使用新的统一响应格式,直接检查response是否存在
|
||||
if (response && response.token && response.user) {
|
||||
const { token, user } = response
|
||||
this.token = token
|
||||
this.user = user
|
||||
this.isAuthenticated = true
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
||||
console.log('login - 状态保存成功:', user.username)
|
||||
console.log('login - localStorage token:', localStorage.getItem('token') ? 'saved' : 'not saved')
|
||||
console.log('login - localStorage user:', localStorage.getItem('user') ? 'saved' : 'not saved')
|
||||
console.log('login - 状态保存成功:', user.username)
|
||||
console.log('login - localStorage token:', localStorage.getItem('token') ? 'saved' : 'not saved')
|
||||
console.log('login - localStorage user:', localStorage.getItem('user') ? 'saved' : 'not saved')
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
return { success: false, message: '登录失败,服务器未返回有效数据' }
|
||||
@@ -154,7 +152,7 @@ export const useUserStore = defineStore('user', {
|
||||
async fetchProfile() {
|
||||
try {
|
||||
const authApi = useAuthApi()
|
||||
const user = await authApi.getProfile()
|
||||
const user = await authApi.getProfile() as any
|
||||
this.user = user
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
|
||||
Reference in New Issue
Block a user