diff --git a/db/repo/hot_drama_repository.go b/db/repo/hot_drama_repository.go new file mode 100644 index 0000000..6d8e416 --- /dev/null +++ b/db/repo/hot_drama_repository.go @@ -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 +} diff --git a/db/repo/manager.go b/db/repo/manager.go index bf22cc8..43753b0 100644 --- a/db/repo/manager.go +++ b/db/repo/manager.go @@ -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), } } diff --git a/db/repo/resource_repository.go b/db/repo/resource_repository.go index 29a447b..1aab4d7 100644 --- a/db/repo/resource_repository.go +++ b/db/repo/resource_repository.go @@ -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 +} diff --git a/doc/HOT_DRAMA_FEATURE.md b/doc/HOT_DRAMA_FEATURE.md new file mode 100644 index 0000000..c5cfe08 --- /dev/null +++ b/doc/HOT_DRAMA_FEATURE.md @@ -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. 支持热播剧推荐算法 \ No newline at end of file diff --git a/handlers/category_handler.go b/handlers/category_handler.go index 8fa29b6..52a9947 100644 --- a/handlers/category_handler.go +++ b/handlers/category_handler.go @@ -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": "分类删除成功"}) } diff --git a/handlers/cks_handler.go b/handlers/cks_handler.go index 382307e..62fd681 100644 --- a/handlers/cks_handler.go +++ b/handlers/cks_handler.go @@ -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) } diff --git a/handlers/hot_drama_handler.go b/handlers/hot_drama_handler.go new file mode 100644 index 0000000..9a56ad5 --- /dev/null +++ b/handlers/hot_drama_handler.go @@ -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) +} diff --git a/handlers/pan_handler.go b/handlers/pan_handler.go index 4e0665e..f064090 100644 --- a/handlers/pan_handler.go +++ b/handlers/pan_handler.go @@ -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) } diff --git a/handlers/ready_resource_handler.go b/handlers/ready_resource_handler.go index b929094..3c8d2cf 100644 --- a/handlers/ready_resource_handler.go +++ b/handlers/ready_resource_handler.go @@ -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": "所有待处理资源已清空", + }) } diff --git a/handlers/resource_handler.go b/handlers/resource_handler.go index fde078d..2085070 100644 --- a/handlers/resource_handler.go +++ b/handlers/resource_handler.go @@ -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, }) } diff --git a/handlers/response.go b/handlers/response.go index cad1f66..45f8b4b 100644 --- a/handlers/response.go +++ b/handlers/response.go @@ -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, }) } diff --git a/handlers/scheduler_handler.go b/handlers/scheduler_handler.go new file mode 100644 index 0000000..b3ba7f9 --- /dev/null +++ b/handlers/scheduler_handler.go @@ -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}) +} diff --git a/handlers/search_stat_handler.go b/handlers/search_stat_handler.go index 8dc4875..7734438 100644 --- a/handlers/search_stat_handler.go +++ b/handlers/search_stat_handler.go @@ -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) } diff --git a/handlers/stats_handler.go b/handlers/stats_handler.go index 70b11ee..136b605 100644 --- a/handlers/stats_handler.go +++ b/handlers/stats_handler.go @@ -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() diff --git a/handlers/system_config_handler.go b/handlers/system_config_handler.go index 88f698d..36a336f 100644 --- a/handlers/system_config_handler.go +++ b/handlers/system_config_handler.go @@ -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) } diff --git a/handlers/tag_handler.go b/handlers/tag_handler.go index e04684e..21ca52c 100644 --- a/handlers/tag_handler.go +++ b/handlers/tag_handler.go @@ -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) } diff --git a/handlers/user_handler.go b/handlers/user_handler.go index 6bb5d75..75a4c59 100644 --- a/handlers/user_handler.go +++ b/handlers/user_handler.go @@ -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) } diff --git a/models/database.go b/models/database.go index 6be6dfd..2df78e0 100644 --- a/models/database.go +++ b/models/database.go @@ -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, '', '百度网盘'), - ('pan.baidu', 2, '', '百度网盘'), - ('aliyun', 3, '', '阿里云盘'), - ('quark', 4, '', '夸克网盘'), - ('teambition', 5, '', '阿里云盘'), - ('cloud.189', 6, '', '天翼云盘'), - ('e.189', 7, '', '天翼云盘'), - ('tianyi', 8, '', '天翼云盘'), - ('天翼', 9, '', '天翼云盘'), - ('xunlei', 10, '', '迅雷云盘'), - ('weiyun', 11, '', '微云'), - ('lanzou', 12, '', '蓝奏云'), - ('123', 13, '', '123云盘'), - ('onedrive', 14, '', 'OneDrive'), - ('google', 15, '', 'Google云盘'), - ('drive.google', 16, '', 'Google云盘'), - ('dropbox', 17, '', 'Dropbox'), - ('ctfile', 18, '', '城通网盘'), - ('115', 19, '', '115网盘'), - ('magnet', 20, '', '磁力链接'), - ('uc', 21, '', 'UC网盘'), - ('UC', 22, '', 'UC网盘'), - ('yun.139', 23, '', '移动云盘'), - ('unknown', 24, '', '未知平台'), - ('other', 25, '', '其他') + ('aliyun', 2, '', '阿里云盘'), + ('quark', 3, '', '夸克网盘'), + ('xunlei', 5, '', '迅雷云盘'), + ('lanzou', 7, '', '蓝奏云'), + ('123', 8, '', '123云盘'), + ('ctfile', 9, '', '城通网盘'), + ('115', 10, '', '115网盘'), + ('magnet', 11, '', '磁力链接'), + ('uc', 12, '', 'UC网盘'), + ('other', 13, '', '其他') ON CONFLICT (name) DO NOTHING;` if _, err := DB.Exec(insertDefaultCategories); err != nil { diff --git a/utils/douban_service.go b/utils/douban_service.go new file mode 100644 index 0000000..cd978ed --- /dev/null +++ b/utils/douban_service.go @@ -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 +} diff --git a/utils/global_scheduler.go b/utils/global_scheduler.go new file mode 100644 index 0000000..86d7384 --- /dev/null +++ b/utils/global_scheduler.go @@ -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() + } + } +} diff --git a/utils/scheduler.go b/utils/scheduler.go new file mode 100644 index 0000000..15d1630 --- /dev/null +++ b/utils/scheduler.go @@ -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() +} diff --git a/utils/url_checker.go b/utils/url_checker.go new file mode 100644 index 0000000..2417325 --- /dev/null +++ b/utils/url_checker.go @@ -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]) + } +} diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index 0f9e353..cbc1e7a 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -1,3 +1,21 @@ +// 统一响应解析函数 +export const parseApiResponse = (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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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 }) + 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,23 +496,120 @@ 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 }) + return parseApiResponse(response) } return { getSystemConfig, 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 + }) + 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 + }) + 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 + }) + return parseApiResponse(response) + } + + const fetchHotDramas = async () => { + const response = await $fetch('/hot-dramas/fetch', { + baseURL: config.public.apiBase, + method: 'POST', + headers: getAuthHeaders() as Record + }) + 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, + } } \ No newline at end of file diff --git a/web/middleware/auth.ts b/web/middleware/auth.ts index fb293db..480b779 100644 --- a/web/middleware/auth.ts +++ b/web/middleware/auth.ts @@ -4,8 +4,8 @@ export default defineNuxtRouteMiddleware((to, from) => { // 初始化用户状态 userStore.initAuth() - // 如果用户未登录,重定向到首页 + // 如果用户未登录,重定向到登录页面 if (!userStore.isAuthenticated) { - return navigateTo('/') + return navigateTo('/login') } }) \ No newline at end of file diff --git a/web/pages/admin.vue b/web/pages/admin.vue index 5477389..6a93f59 100644 --- a/web/pages/admin.vue +++ b/web/pages/admin.vue @@ -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) diff --git a/web/pages/hot-dramas.vue b/web/pages/hot-dramas.vue new file mode 100644 index 0000000..0db5bb9 --- /dev/null +++ b/web/pages/hot-dramas.vue @@ -0,0 +1,214 @@ + + + + + \ No newline at end of file diff --git a/web/pages/index.vue b/web/pages/index.vue index 5e0c646..e6f04de 100644 --- a/web/pages/index.vue +++ b/web/pages/index.vue @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/web/pages/system-config.vue b/web/pages/system-config.vue index 7712829..99e1c8e 100644 --- a/web/pages/system-config.vue +++ b/web/pages/system-config.vue @@ -127,6 +127,50 @@ + +
+
+

+ 自动转存 +

+

+ 开启后,系统将自动转存资源到其他网盘平台 +

+
+
+ +
+
+ + +
+
+

+ 自动拉取热播剧 +

+

+ 开启后,系统将自动从豆瓣获取热播剧信息 +

+
+
+ +
+
+