update: config

This commit is contained in:
Kerwin
2025-07-11 17:45:16 +08:00
parent f7fb4e6f14
commit 65591ce102
30 changed files with 3617 additions and 647 deletions

View File

@@ -0,0 +1,128 @@
package repo
import (
"res_db/db/entity"
"gorm.io/gorm"
)
// HotDramaRepository 热播剧仓储接口
type HotDramaRepository interface {
Create(drama *entity.HotDrama) error
FindByID(id uint) (*entity.HotDrama, error)
FindAll(page, pageSize int) ([]entity.HotDrama, int64, error)
FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error)
FindByDoubanID(doubanID string) (*entity.HotDrama, error)
Upsert(drama *entity.HotDrama) error
Delete(id uint) error
DeleteByDoubanID(doubanID string) error
DeleteOldRecords(days int) error
}
// hotDramaRepository 热播剧仓储实现
type hotDramaRepository struct {
db *gorm.DB
}
// NewHotDramaRepository 创建热播剧仓储实例
func NewHotDramaRepository(db *gorm.DB) HotDramaRepository {
return &hotDramaRepository{db: db}
}
// Create 创建热播剧记录
func (r *hotDramaRepository) Create(drama *entity.HotDrama) error {
return r.db.Create(drama).Error
}
// FindByID 根据ID查找热播剧
func (r *hotDramaRepository) FindByID(id uint) (*entity.HotDrama, error) {
var drama entity.HotDrama
err := r.db.First(&drama, id).Error
if err != nil {
return nil, err
}
return &drama, nil
}
// FindAll 查找所有热播剧(分页)
func (r *hotDramaRepository) FindAll(page, pageSize int) ([]entity.HotDrama, int64, error) {
var dramas []entity.HotDrama
var total int64
offset := (page - 1) * pageSize
// 获取总数
if err := r.db.Model(&entity.HotDrama{}).Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error
if err != nil {
return nil, 0, err
}
return dramas, total, nil
}
// FindByCategory 根据分类查找热播剧(分页)
func (r *hotDramaRepository) FindByCategory(category string, page, pageSize int) ([]entity.HotDrama, int64, error) {
var dramas []entity.HotDrama
var total int64
offset := (page - 1) * pageSize
// 获取总数
if err := r.db.Model(&entity.HotDrama{}).Where("category = ?", category).Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := r.db.Where("category = ?", category).Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&dramas).Error
if err != nil {
return nil, 0, err
}
return dramas, total, nil
}
// FindByDoubanID 根据豆瓣ID查找热播剧
func (r *hotDramaRepository) FindByDoubanID(doubanID string) (*entity.HotDrama, error) {
var drama entity.HotDrama
err := r.db.Where("douban_id = ?", doubanID).First(&drama).Error
if err != nil {
return nil, err
}
return &drama, nil
}
// Upsert 插入或更新热播剧记录
func (r *hotDramaRepository) Upsert(drama *entity.HotDrama) error {
if drama.DoubanID != "" {
// 如果存在豆瓣ID先尝试查找现有记录
existing, err := r.FindByDoubanID(drama.DoubanID)
if err == nil && existing != nil {
// 更新现有记录
drama.ID = existing.ID
return r.db.Save(drama).Error
}
}
// 创建新记录
return r.Create(drama)
}
// Delete 删除热播剧记录
func (r *hotDramaRepository) Delete(id uint) error {
return r.db.Delete(&entity.HotDrama{}, id).Error
}
// DeleteByDoubanID 根据豆瓣ID删除热播剧记录
func (r *hotDramaRepository) DeleteByDoubanID(doubanID string) error {
return r.db.Where("douban_id = ?", doubanID).Delete(&entity.HotDrama{}).Error
}
// DeleteOldRecords 删除指定天数前的旧记录
func (r *hotDramaRepository) DeleteOldRecords(days int) error {
return r.db.Where("created_at < NOW() - INTERVAL '? days'", days).Delete(&entity.HotDrama{}).Error
}

View File

@@ -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),
}
}

View File

@@ -1,7 +1,9 @@
package repo
import (
"fmt"
"res_db/db/entity"
"time"
"gorm.io/gorm"
)
@@ -10,25 +12,33 @@ import (
type ResourceRepository interface {
BaseRepository[entity.Resource]
FindWithRelations() ([]entity.Resource, error)
FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error)
FindByCategoryID(categoryID uint) ([]entity.Resource, error)
FindByCategoryIDPaginated(categoryID uint, page, limit int) ([]entity.Resource, int64, error)
FindByPanID(panID uint) ([]entity.Resource, error)
FindByPanIDPaginated(panID uint, page, limit int) ([]entity.Resource, int64, error)
FindByIsValid(isValid bool) ([]entity.Resource, error)
FindByIsPublic(isPublic bool) ([]entity.Resource, error)
Search(query string, categoryID *uint, page, limit int) ([]entity.Resource, int64, error)
IncrementViewCount(id uint) error
FindWithTags() ([]entity.Resource, error)
UpdateWithTags(resource *entity.Resource, tagIDs []uint) error
GetLatestResources(limit int) ([]entity.Resource, error)
GetCachedLatestResources(limit int) ([]entity.Resource, error)
InvalidateCache() error
}
// ResourceRepositoryImpl Resource的Repository实现
type ResourceRepositoryImpl struct {
BaseRepositoryImpl[entity.Resource]
cache map[string]interface{}
}
// NewResourceRepository 创建Resource Repository
func NewResourceRepository(db *gorm.DB) ResourceRepository {
return &ResourceRepositoryImpl{
BaseRepositoryImpl: BaseRepositoryImpl[entity.Resource]{db: db},
cache: make(map[string]interface{}),
}
}
@@ -39,6 +49,24 @@ func (r *ResourceRepositoryImpl) FindWithRelations() ([]entity.Resource, error)
return resources, err
}
// FindWithRelationsPaginated 分页查找包含关联关系的资源
func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) {
var resources []entity.Resource
var total int64
offset := (page - 1) * limit
db := r.db.Model(&entity.Resource{}).Preload("Category").Preload("Pan").Preload("Tags")
// 获取总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := db.Offset(offset).Limit(limit).Find(&resources).Error
return resources, total, err
}
// FindByCategoryID 根据分类ID查找
func (r *ResourceRepositoryImpl) FindByCategoryID(categoryID uint) ([]entity.Resource, error) {
var resources []entity.Resource
@@ -46,6 +74,24 @@ func (r *ResourceRepositoryImpl) FindByCategoryID(categoryID uint) ([]entity.Res
return resources, err
}
// FindByCategoryIDPaginated 分页根据分类ID查找
func (r *ResourceRepositoryImpl) FindByCategoryIDPaginated(categoryID uint, page, limit int) ([]entity.Resource, int64, error) {
var resources []entity.Resource
var total int64
offset := (page - 1) * limit
db := r.db.Model(&entity.Resource{}).Where("category_id = ?", categoryID).Preload("Category").Preload("Tags")
// 获取总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := db.Offset(offset).Limit(limit).Find(&resources).Error
return resources, total, err
}
// FindByPanID 根据平台ID查找
func (r *ResourceRepositoryImpl) FindByPanID(panID uint) ([]entity.Resource, error) {
var resources []entity.Resource
@@ -53,6 +99,24 @@ func (r *ResourceRepositoryImpl) FindByPanID(panID uint) ([]entity.Resource, err
return resources, err
}
// FindByPanIDPaginated 分页根据平台ID查找
func (r *ResourceRepositoryImpl) FindByPanIDPaginated(panID uint, page, limit int) ([]entity.Resource, int64, error) {
var resources []entity.Resource
var total int64
offset := (page - 1) * limit
db := r.db.Model(&entity.Resource{}).Where("pan_id = ?", panID).Preload("Category").Preload("Tags")
// 获取总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取分页数据
err := db.Offset(offset).Limit(limit).Find(&resources).Error
return resources, total, err
}
// FindByIsValid 根据有效性查找
func (r *ResourceRepositoryImpl) FindByIsValid(isValid bool) ([]entity.Resource, error) {
var resources []entity.Resource
@@ -134,3 +198,43 @@ func (r *ResourceRepositoryImpl) UpdateWithTags(resource *entity.Resource, tagID
return nil
})
}
// GetLatestResources 获取最新资源
func (r *ResourceRepositoryImpl) GetLatestResources(limit int) ([]entity.Resource, error) {
var resources []entity.Resource
err := r.db.Order("created_at DESC").Limit(limit).Find(&resources).Error
return resources, err
}
// GetCachedLatestResources 获取缓存的最新资源
func (r *ResourceRepositoryImpl) GetCachedLatestResources(limit int) ([]entity.Resource, error) {
cacheKey := fmt.Sprintf("latest_resources_%d", limit)
// 检查缓存
if cached, exists := r.cache[cacheKey]; exists {
if resources, ok := cached.([]entity.Resource); ok {
return resources, nil
}
}
// 从数据库获取
resources, err := r.GetLatestResources(limit)
if err != nil {
return nil, err
}
// 缓存结果5分钟过期
r.cache[cacheKey] = resources
go func() {
time.Sleep(5 * time.Minute)
delete(r.cache, cacheKey)
}()
return resources, nil
}
// InvalidateCache 清除缓存
func (r *ResourceRepositoryImpl) InvalidateCache() error {
r.cache = make(map[string]interface{})
return nil
}

123
doc/HOT_DRAMA_FEATURE.md Normal file
View File

@@ -0,0 +1,123 @@
# 热播剧功能说明
## 功能概述
热播剧功能是一个自动获取和展示豆瓣热门电影、电视剧榜单的功能模块。系统会定时从豆瓣获取最新的热门影视作品信息,并保存到数据库中供用户浏览。
## 主要特性
### 1. 自动数据获取
- 每小时自动从豆瓣获取热门电影和电视剧数据
- 支持电影和电视剧两个分类
- 获取内容包括:剧名、评分、年份、导演、演员等详细信息
### 2. 数据存储
- 创建专门的热播剧数据表 `hot_dramas`
- 支持按豆瓣ID去重避免重复数据
- 记录数据来源和获取时间
### 3. 前端展示
- 美观的卡片式布局展示热播剧信息
- 支持按分类筛选(全部/电影/电视剧)
- 分页显示,支持大量数据
- 响应式设计,适配各种设备
### 4. 管理功能
- 管理员可以手动启动/停止定时任务
- 支持手动获取热播剧数据
- 查看调度器运行状态
## 数据库结构
### hot_dramas 表
```sql
CREATE TABLE hot_dramas (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
rating DECIMAL(3,1) DEFAULT 0.0,
year VARCHAR(10),
directors VARCHAR(500),
actors VARCHAR(1000),
category VARCHAR(50),
sub_type VARCHAR(50),
source VARCHAR(50) DEFAULT 'douban',
douban_id VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## API 接口
### 热播剧管理
- `GET /api/hot-dramas` - 获取热播剧列表
- `GET /api/hot-dramas/:id` - 获取热播剧详情
- `POST /api/hot-dramas` - 创建热播剧记录(管理员)
- `PUT /api/hot-dramas/:id` - 更新热播剧记录(管理员)
- `DELETE /api/hot-dramas/:id` - 删除热播剧记录(管理员)
### 调度器管理
- `GET /api/scheduler/status` - 获取调度器状态
- `POST /api/scheduler/hot-drama/start` - 启动热播剧定时任务(管理员)
- `POST /api/scheduler/hot-drama/stop` - 停止热播剧定时任务(管理员)
- `GET /api/scheduler/hot-drama/names` - 手动获取热播剧名字(管理员)
## 配置说明
### 系统配置
在系统配置中有一个 `auto_fetch_hot_drama_enabled` 字段,用于控制是否启用自动获取热播剧功能:
- `true`: 启用自动获取,系统会根据配置的间隔时间自动获取数据
- `false`: 禁用自动获取,需要管理员手动启动
## 使用流程
### 1. 启用功能
1. 登录管理后台
2. 进入系统配置页面
3. 开启"自动拉取热播剧名字"选项
4. 保存配置
### 2. 查看热播剧
1. 在首页点击"热播剧"按钮
2. 进入热播剧页面
3. 可以按分类筛选查看
4. 支持分页浏览
### 3. 管理定时任务
1. 管理员可以手动启动/停止定时任务
2. 可以查看调度器运行状态
3. 可以手动触发数据获取
## 技术实现
### 后端架构
- **实体层**: `db/entity/hot_drama.go` - 定义热播剧数据结构
- **DTO层**: `db/dto/hot_drama.go` - 定义数据传输对象
- **转换器**: `db/converter/hot_drama_converter.go` - 实体与DTO转换
- **仓储层**: `db/repo/hot_drama_repository.go` - 数据库操作
- **处理器**: `handlers/hot_drama_handler.go` - API接口处理
- **调度器**: `utils/scheduler.go` - 定时任务管理
- **豆瓣服务**: `utils/douban_service.go` - 豆瓣API调用
### 前端实现
- **页面**: `web/pages/hot-dramas.vue` - 热播剧展示页面
- **导航**: 在首页添加热播剧入口
- **样式**: 使用Tailwind CSS实现响应式设计
## 注意事项
1. **数据来源**: 数据来源于豆瓣移动端API如果API不可用会使用模拟数据
2. **频率限制**: 定时任务每小时执行一次,避免对豆瓣服务器造成压力
3. **数据去重**: 系统会根据豆瓣ID进行去重避免重复数据
4. **权限控制**: 管理功能需要管理员权限
5. **错误处理**: 系统具备完善的错误处理机制,确保稳定性
## 扩展功能
未来可以考虑添加的功能:
1. 支持更多数据源如IMDB、烂番茄等
2. 添加用户收藏功能
3. 支持热播剧搜索
4. 添加数据统计和分析功能
5. 支持热播剧推荐算法

View File

@@ -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": "分类删除成功"})
}

View File

@@ -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)
}

View File

@@ -0,0 +1,191 @@
package handlers
import (
"net/http"
"strconv"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/entity"
"res_db/db/repo"
"github.com/gin-gonic/gin"
)
// HotDramaHandler 热播剧处理器
type HotDramaHandler struct {
hotDramaRepo repo.HotDramaRepository
}
// NewHotDramaHandler 创建热播剧处理器
func NewHotDramaHandler(hotDramaRepo repo.HotDramaRepository) *HotDramaHandler {
return &HotDramaHandler{
hotDramaRepo: hotDramaRepo,
}
}
// GetHotDramaList 获取热播剧列表
func (h *HotDramaHandler) GetHotDramaList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
category := c.Query("category")
var dramas []entity.HotDrama
var total int64
var err error
if category != "" {
dramas, total, err = h.hotDramaRepo.FindByCategory(category, page, pageSize)
} else {
dramas, total, err = h.hotDramaRepo.FindAll(page, pageSize)
}
if err != nil {
ErrorResponse(c, "获取热播剧列表失败", http.StatusInternalServerError)
return
}
response := converter.HotDramaListToResponse(dramas)
response.Total = int(total)
SuccessResponse(c, response)
}
// GetHotDramaByID 根据ID获取热播剧详情
func (h *HotDramaHandler) GetHotDramaByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
drama, err := h.hotDramaRepo.FindByID(uint(id))
if err != nil {
ErrorResponse(c, "热播剧不存在", http.StatusNotFound)
return
}
response := converter.HotDramaToResponse(drama)
SuccessResponse(c, response)
}
// CreateHotDrama 创建热播剧记录
func CreateHotDrama(c *gin.Context) {
var req dto.HotDramaRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
drama := converter.RequestToHotDrama(&req)
if drama == nil {
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
return
}
err := repoManager.HotDramaRepository.Create(drama)
if err != nil {
ErrorResponse(c, "创建热播剧记录失败", http.StatusInternalServerError)
return
}
response := converter.HotDramaToResponse(drama)
SuccessResponse(c, response)
}
// UpdateHotDrama 更新热播剧记录
func UpdateHotDrama(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
var req dto.HotDramaRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
return
}
drama := converter.RequestToHotDrama(&req)
if drama == nil {
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
return
}
drama.ID = uint(id)
err = repoManager.HotDramaRepository.Upsert(drama)
if err != nil {
ErrorResponse(c, "更新热播剧记录失败", http.StatusInternalServerError)
return
}
response := converter.HotDramaToResponse(drama)
SuccessResponse(c, response)
}
// DeleteHotDrama 删除热播剧记录
func DeleteHotDrama(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
err = repoManager.HotDramaRepository.Delete(uint(id))
if err != nil {
ErrorResponse(c, "删除热播剧记录失败", http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{"message": "删除热播剧记录成功"})
}
// GetHotDramaList 获取热播剧列表使用全局repoManager
func GetHotDramaList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
category := c.Query("category")
var dramas []entity.HotDrama
var total int64
var err error
if category != "" {
dramas, total, err = repoManager.HotDramaRepository.FindByCategory(category, page, pageSize)
} else {
dramas, total, err = repoManager.HotDramaRepository.FindAll(page, pageSize)
}
if err != nil {
ErrorResponse(c, "获取热播剧列表失败", http.StatusInternalServerError)
return
}
response := converter.HotDramaListToResponse(dramas)
response.Total = int(total)
SuccessResponse(c, response)
}
// GetHotDramaByID 根据ID获取热播剧详情使用全局repoManager
func GetHotDramaByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
drama, err := repoManager.HotDramaRepository.FindByID(uint(id))
if err != nil {
ErrorResponse(c, "热播剧不存在", http.StatusNotFound)
return
}
response := converter.HotDramaToResponse(drama)
SuccessResponse(c, response)
}

View File

@@ -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)
}

View File

@@ -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": "所有待处理资源已清空",
})
}

View File

@@ -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,
})
}

View File

@@ -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,
})
}

View File

@@ -0,0 +1,59 @@
package handlers
import (
"net/http"
"res_db/utils"
"github.com/gin-gonic/gin"
)
// GetSchedulerStatus 获取调度器状态
func GetSchedulerStatus(c *gin.Context) {
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
status := gin.H{
"hot_drama_scheduler_running": scheduler.IsHotDramaSchedulerRunning(),
}
SuccessResponse(c, status)
}
// 启动热播剧定时任务
func StartHotDramaScheduler(c *gin.Context) {
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
if scheduler.IsHotDramaSchedulerRunning() {
ErrorResponse(c, "热播剧定时任务已在运行中", http.StatusBadRequest)
return
}
scheduler.StartHotDramaScheduler()
SuccessResponse(c, gin.H{"message": "热播剧定时任务已启动"})
}
// 停止热播剧定时任务
func StopHotDramaScheduler(c *gin.Context) {
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
if !scheduler.IsHotDramaSchedulerRunning() {
ErrorResponse(c, "热播剧定时任务未在运行", http.StatusBadRequest)
return
}
scheduler.StopHotDramaScheduler()
SuccessResponse(c, gin.H{"message": "热播剧定时任务已停止"})
}
// 手动触发热播剧定时任务
func TriggerHotDramaScheduler(c *gin.Context) {
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
scheduler.StartHotDramaScheduler() // 直接启动一次
SuccessResponse(c, gin.H{"message": "手动触发热播剧定时任务成功"})
}
// 手动获取热播剧名字
func FetchHotDramaNames(c *gin.Context) {
scheduler := utils.GetGlobalScheduler(repoManager.HotDramaRepository)
names, err := scheduler.GetHotDramaNames()
if err != nil {
ErrorResponse(c, "获取热播剧名字失败: "+err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{"names": names})
}

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -163,6 +163,11 @@ func createTables() error {
id SERIAL PRIMARY KEY,
title VARCHAR(255),
url VARCHAR(500) NOT NULL,
category VARCHAR(100) DEFAULT NULL,
tags VARCHAR(500) DEFAULT NULL,
img VARCHAR(500) DEFAULT NULL,
source VARCHAR(100) DEFAULT NULL,
extra TEXT DEFAULT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip VARCHAR(45) DEFAULT NULL
);`
@@ -180,6 +185,42 @@ func createTables() error {
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
// 创建系统配置表
systemConfigTable := `
CREATE TABLE IF NOT EXISTS system_configs (
id SERIAL PRIMARY KEY,
site_title VARCHAR(200) NOT NULL DEFAULT '网盘资源管理系统',
site_description VARCHAR(500),
keywords VARCHAR(500),
author VARCHAR(100),
copyright VARCHAR(200),
auto_process_ready_resources BOOLEAN DEFAULT false,
auto_process_interval INTEGER DEFAULT 30,
auto_transfer_enabled BOOLEAN DEFAULT false,
auto_fetch_hot_drama_enabled BOOLEAN DEFAULT false,
page_size INTEGER DEFAULT 100,
maintenance_mode BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
// 创建热播剧表
hotDramaTable := `
CREATE TABLE IF NOT EXISTS hot_dramas (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
rating DECIMAL(3,1) DEFAULT 0.0,
year VARCHAR(10),
directors VARCHAR(500),
actors VARCHAR(1000),
category VARCHAR(50),
sub_type VARCHAR(50),
source VARCHAR(50) DEFAULT 'douban',
douban_id VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
if _, err := DB.Exec(panTable); err != nil {
return err
}
@@ -196,6 +237,14 @@ func createTables() error {
return err
}
if _, err := DB.Exec(systemConfigTable); err != nil {
return err
}
if _, err := DB.Exec(hotDramaTable); err != nil {
return err
}
// 插入默认分类
insertDefaultCategories := `
INSERT INTO categories (name, description) VALUES
@@ -204,8 +253,12 @@ func createTables() error {
('动漫', '动漫资源'),
('音乐', '音乐资源'),
('软件', '软件资源'),
('游戏', '游戏资源'),
('PC游戏', 'PC游戏资源'),
('手机游戏', '手机游戏'),
('文档', '文档资源'),
('短剧', '短剧'),
('学习资源', '学习资源'),
('视频教程', '学习资源'),
('其他', '其他资源')
ON CONFLICT (name) DO NOTHING;`
@@ -213,30 +266,16 @@ func createTables() error {
insertDefaultPans := `
INSERT INTO pan (name, key, icon, remark) VALUES
('baidu', 1, '<i class="fas fa-cloud text-blue-500"></i>', '百度网盘'),
('pan.baidu', 2, '<i class="fas fa-cloud text-blue-500"></i>', '百度网盘'),
('aliyun', 3, '<i class="fas fa-cloud text-orange-500"></i>', '阿里云盘'),
('quark', 4, '<i class="fas fa-atom text-purple-500"></i>', '夸克网盘'),
('teambition', 5, '<i class="fas fa-cloud text-orange-500"></i>', '阿里云盘'),
('cloud.189', 6, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
('e.189', 7, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
('tianyi', 8, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
('天翼', 9, '<i class="fas fa-cloud text-cyan-500"></i>', '天翼云盘'),
('xunlei', 10, '<i class="fas fa-bolt text-yellow-500"></i>', '迅雷云盘'),
('weiyun', 11, '<i class="fas fa-cloud text-green-500"></i>', '微云'),
('lanzou', 12, '<i class="fas fa-cloud text-blue-400"></i>', '蓝奏云'),
('123', 13, '<i class="fas fa-cloud text-red-500"></i>', '123云盘'),
('onedrive', 14, '<i class="fab fa-microsoft text-blue-600"></i>', 'OneDrive'),
('google', 15, '<i class="fab fa-google-drive text-green-600"></i>', 'Google云盘'),
('drive.google', 16, '<i class="fab fa-google-drive text-green-600"></i>', 'Google云盘'),
('dropbox', 17, '<i class="fab fa-dropbox text-blue-500"></i>', 'Dropbox'),
('ctfile', 18, '<i class="fas fa-folder text-yellow-600"></i>', '城通网盘'),
('115', 19, '<i class="fas fa-cloud-upload-alt text-green-600"></i>', '115网盘'),
('magnet', 20, '<i class="fas fa-magnet text-red-600"></i>', '磁力链接'),
('uc', 21, '<i class="fas fa-cloud-download-alt text-purple-600"></i>', 'UC网盘'),
('UC', 22, '<i class="fas fa-cloud-download-alt text-purple-600"></i>', 'UC网盘'),
('yun.139', 23, '<i class="fas fa-cloud text-cyan-500"></i>', '移动云盘'),
('unknown', 24, '<i class="fas fa-question-circle text-gray-400"></i>', '未知平台'),
('other', 25, '<i class="fas fa-cloud text-gray-500"></i>', '其他')
('aliyun', 2, '<i class="fas fa-cloud text-orange-500"></i>', '阿里云盘'),
('quark', 3, '<i class="fas fa-atom text-purple-500"></i>', '夸克网盘'),
('xunlei', 5, '<i class="fas fa-bolt text-yellow-500"></i>', '迅雷云盘'),
('lanzou', 7, '<i class="fas fa-cloud text-blue-400"></i>', '蓝奏云'),
('123', 8, '<i class="fas fa-cloud text-red-500"></i>', '123云盘'),
('ctfile', 9, '<i class="fas fa-folder text-yellow-600"></i>', '城通网盘'),
('115', 10, '<i class="fas fa-cloud-upload-alt text-green-600"></i>', '115网盘'),
('magnet', 11, '<i class="fas fa-magnet text-red-600"></i>', '磁力链接'),
('uc', 12, '<i class="fas fa-cloud-download-alt text-purple-600"></i>', 'UC网盘'),
('other', 13, '<i class="fas fa-cloud text-gray-500"></i>', '其他')
ON CONFLICT (name) DO NOTHING;`
if _, err := DB.Exec(insertDefaultCategories); err != nil {

514
utils/douban_service.go Normal file
View File

@@ -0,0 +1,514 @@
package utils
import (
"encoding/json"
"log"
"strconv"
"time"
"github.com/go-resty/resty/v2"
)
// DoubanService 豆瓣服务
type DoubanService struct {
baseURL string
client *resty.Client
// 电影榜单配置 - 4个大类每个大类下有5个小类
MovieCategories map[string]map[string]map[string]string
// 剧集榜单配置 - 2个大类
TvCategories map[string]map[string]map[string]string
}
// DoubanItem 豆瓣项目
type DoubanItem struct {
ID string `json:"id"`
Title string `json:"title"`
Rating Rating `json:"rating"`
Year string `json:"year"`
Directors []string `json:"directors"`
Actors []string `json:"actors"`
}
// Rating 评分
type Rating struct {
Value float64 `json:"value"`
}
// DoubanCategory 豆瓣分类
type DoubanCategory struct {
Category string `json:"category"`
Selected bool `json:"selected"`
Type string `json:"type"`
Title string `json:"title"`
}
// DoubanResponse 豆瓣响应
type DoubanResponse struct {
Items []DoubanItem `json:"items"`
Categories []DoubanCategory `json:"categories"`
Total int `json:"total"`
IsMockData bool `json:"is_mock_data,omitempty"`
MockReason string `json:"mock_reason,omitempty"`
Notice string `json:"notice,omitempty"`
}
// DoubanResult 豆瓣结果
type DoubanResult struct {
Success bool `json:"success"`
Data *DoubanResponse `json:"data,omitempty"`
Message string `json:"message,omitempty"`
}
// NewDoubanService 创建新的豆瓣服务
func NewDoubanService() *DoubanService {
client := resty.New()
client.SetTimeout(30 * time.Second)
client.SetHeaders(map[string]string{
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
"Referer": "https://m.douban.com/",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
})
// 初始化电影榜单配置
movieCategories := map[string]map[string]map[string]string{
"热门电影": {
"全部": {"category": "热门", "type": "全部"},
"华语": {"category": "热门", "type": "华语"},
"欧美": {"category": "热门", "type": "欧美"},
"韩国": {"category": "热门", "type": "韩国"},
"日本": {"category": "热门", "type": "日本"},
},
"最新电影": {
"全部": {"category": "最新", "type": "全部"},
"华语": {"category": "最新", "type": "华语"},
"欧美": {"category": "最新", "type": "欧美"},
"韩国": {"category": "最新", "type": "韩国"},
"日本": {"category": "最新", "type": "日本"},
},
"豆瓣高分": {
"全部": {"category": "豆瓣高分", "type": "全部"},
"华语": {"category": "豆瓣高分", "type": "华语"},
"欧美": {"category": "豆瓣高分", "type": "欧美"},
"韩国": {"category": "豆瓣高分", "type": "韩国"},
"日本": {"category": "豆瓣高分", "type": "日本"},
},
"冷门佳片": {
"全部": {"category": "冷门佳片", "type": "全部"},
"华语": {"category": "冷门佳片", "type": "华语"},
"欧美": {"category": "冷门佳片", "type": "欧美"},
"韩国": {"category": "冷门佳片", "type": "韩国"},
"日本": {"category": "冷门佳片", "type": "日本"},
},
}
// 初始化剧集榜单配置
tvCategories := map[string]map[string]map[string]string{
"最近热门剧集": {
"综合": {"category": "tv", "type": "tv"},
"国产剧": {"category": "tv", "type": "tv_domestic"},
"欧美剧": {"category": "tv", "type": "tv_american"},
"日剧": {"category": "tv", "type": "tv_japanese"},
"韩剧": {"category": "tv", "type": "tv_korean"},
"动画": {"category": "tv", "type": "tv_animation"},
"纪录片": {"category": "tv", "type": "tv_documentary"},
},
"最近热门综艺": {
"综合": {"category": "show", "type": "show"},
"国内": {"category": "show", "type": "show_domestic"},
"国外": {"category": "show", "type": "show_foreign"},
},
}
return &DoubanService{
baseURL: "https://m.douban.com/rexxar/api/v2",
client: client,
MovieCategories: movieCategories,
TvCategories: tvCategories,
}
}
// GetMovieRanking 获取电影榜单数据
func (ds *DoubanService) GetMovieRanking(category, rankingType string, start, limit int) (*DoubanResult, error) {
log.Printf("获取电影榜单: %s - %s, start: %d, limit: %d", category, rankingType, start, limit)
// 构建请求参数
params := map[string]string{
"start": strconv.Itoa(start),
"limit": strconv.Itoa(limit),
}
// 根据不同的category和type添加特定参数
if category != "热门" || rankingType != "全部" {
if rankingType != "全部" {
params["type"] = rankingType
}
if category != "热门" {
params["category"] = category
}
}
var response *resty.Response
var err error
// 尝试调用豆瓣API
response, err = ds.client.R().
SetQueryParams(params).
Get(ds.baseURL + "/subject/recent_hot/movie")
if err != nil {
log.Printf("豆瓣API调用失败使用模拟数据: %v", err)
// 如果豆瓣API调用失败使用模拟数据
mockData := ds.getMockMovieData()
mockData.IsMockData = true
mockData.MockReason = "API调用失败"
return &DoubanResult{
Success: true,
Data: mockData,
}, nil
}
var apiResponse map[string]interface{}
if err := json.Unmarshal(response.Body(), &apiResponse); err != nil {
log.Printf("解析API响应失败: %v", err)
mockData := ds.getMockMovieData()
mockData.IsMockData = true
mockData.MockReason = "解析API响应失败"
return &DoubanResult{
Success: true,
Data: mockData,
}, nil
}
// 处理豆瓣移动端API的响应格式
items := ds.extractItems(apiResponse)
categories := ds.extractCategories(apiResponse)
// 如果没有获取到真实数据,使用模拟数据
isMockData := false
mockReason := ""
if len(items) == 0 {
log.Println("API返回空数据使用模拟数据")
mockData := ds.getMockMovieData()
items = mockData.Items
isMockData = true
mockReason = "API返回空数据"
}
// 如果没有获取到categories使用默认的电影分类
if len(categories) == 0 {
categories = []DoubanCategory{
{Category: "热门", Selected: true, Type: "全部", Title: "热门"},
{Category: "最新", Selected: false, Type: "全部", Title: "最新"},
{Category: "豆瓣高分", Selected: false, Type: "全部", Title: "豆瓣高分"},
{Category: "冷门佳片", Selected: false, Type: "全部", Title: "冷门佳片"},
{Category: "热门", Selected: false, Type: "华语", Title: "华语"},
{Category: "热门", Selected: false, Type: "欧美", Title: "欧美"},
{Category: "热门", Selected: false, Type: "韩国", Title: "韩国"},
{Category: "热门", Selected: false, Type: "日本", Title: "日本"},
}
}
// 根据请求的category和type更新selected状态
for i := range categories {
categories[i].Selected = categories[i].Category == category && categories[i].Type == rankingType
}
// 限制返回数量
if len(items) > limit {
items = items[:limit]
}
result := &DoubanResponse{
Items: items,
Total: len(items),
Categories: categories,
IsMockData: isMockData,
MockReason: mockReason,
}
if isMockData {
result.Notice = "⚠️ 这是模拟数据,非豆瓣实时数据"
}
return &DoubanResult{
Success: true,
Data: result,
}, nil
}
// GetTvRanking 获取电视剧榜单数据
func (ds *DoubanService) GetTvRanking(category, rankingType string, start, limit int) (*DoubanResult, error) {
log.Printf("获取电视剧榜单: %s - %s, start: %d, limit: %d", category, rankingType, start, limit)
// 构建请求参数
params := map[string]string{
"start": strconv.Itoa(start),
"limit": strconv.Itoa(limit),
}
// 根据不同的category和type添加特定参数
if category != "tv" || rankingType != "tv" {
if rankingType != "tv" {
params["type"] = rankingType
}
if category != "tv" {
params["category"] = category
}
}
var response *resty.Response
var err error
// 尝试调用豆瓣API
response, err = ds.client.R().
SetQueryParams(params).
Get(ds.baseURL + "/subject/recent_hot/tv")
if err != nil {
log.Printf("豆瓣TV API调用失败使用模拟数据: %v", err)
mockData := ds.getMockTvData()
mockData.IsMockData = true
mockData.MockReason = "API调用失败"
return &DoubanResult{
Success: true,
Data: mockData,
}, nil
}
var apiResponse map[string]interface{}
if err := json.Unmarshal(response.Body(), &apiResponse); err != nil {
log.Printf("解析TV API响应失败: %v", err)
mockData := ds.getMockTvData()
mockData.IsMockData = true
mockData.MockReason = "解析API响应失败"
return &DoubanResult{
Success: true,
Data: mockData,
}, nil
}
// 处理豆瓣移动端API的响应格式
items := ds.extractItems(apiResponse)
categories := ds.extractCategories(apiResponse)
// 如果没有获取到真实数据,使用模拟数据
isMockData := false
mockReason := ""
if len(items) == 0 {
log.Println("TV API返回空数据使用模拟数据")
mockData := ds.getMockTvData()
items = mockData.Items
isMockData = true
mockReason = "API返回空数据"
}
// 如果没有获取到categories使用默认的电视剧分类
if len(categories) == 0 {
categories = []DoubanCategory{
{Category: "tv", Selected: true, Type: "tv", Title: "综合"},
{Category: "tv", Selected: false, Type: "tv_domestic", Title: "国产剧"},
{Category: "show", Selected: false, Type: "show", Title: "综艺"},
{Category: "tv", Selected: false, Type: "tv_american", Title: "欧美剧"},
{Category: "tv", Selected: false, Type: "tv_japanese", Title: "日剧"},
{Category: "tv", Selected: false, Type: "tv_korean", Title: "韩剧"},
{Category: "tv", Selected: false, Type: "tv_animation", Title: "动画"},
{Category: "tv", Selected: false, Type: "tv_documentary", Title: "纪录片"},
}
}
// 根据请求的category和type更新selected状态
for i := range categories {
categories[i].Selected = categories[i].Category == category && categories[i].Type == rankingType
}
// 限制返回数量
if len(items) > limit {
items = items[:limit]
}
result := &DoubanResponse{
Items: items,
Total: len(items),
Categories: categories,
IsMockData: isMockData,
MockReason: mockReason,
}
if isMockData {
result.Notice = "⚠️ 这是模拟数据,非豆瓣实时数据"
}
return &DoubanResult{
Success: true,
Data: result,
}, nil
}
// GetMovieCategories 获取支持的电影类别
func (ds *DoubanService) GetMovieCategories() map[string]map[string]map[string]string {
return ds.MovieCategories
}
// GetTvCategories 获取支持的电视剧类别
func (ds *DoubanService) GetTvCategories() map[string]map[string]map[string]string {
return ds.TvCategories
}
// GetAllCategories 获取所有支持的类别
func (ds *DoubanService) GetAllCategories() map[string]interface{} {
return map[string]interface{}{
"movie": ds.GetMovieCategories(),
"tv": ds.GetTvCategories(),
}
}
// GetMovieSubCategories 获取电影特定大类下的小类
func (ds *DoubanService) GetMovieSubCategories(mainCategory string) map[string]map[string]string {
return ds.MovieCategories[mainCategory]
}
// GetTvSubCategories 获取剧集特定大类下的小类
func (ds *DoubanService) GetTvSubCategories(mainCategory string) map[string]map[string]string {
return ds.TvCategories[mainCategory]
}
// getMockMovieData 获取模拟电影数据
func (ds *DoubanService) getMockMovieData() *DoubanResponse {
return &DoubanResponse{
Notice: "⚠️ 这是模拟数据,非豆瓣实时数据",
Items: []DoubanItem{
{
ID: "1292052",
Title: "肖申克的救赎",
Rating: Rating{Value: 9.7},
Year: "1994",
Directors: []string{"弗兰克·德拉邦特"},
Actors: []string{"蒂姆·罗宾斯", "摩根·弗里曼"},
},
{
ID: "1291546",
Title: "霸王别姬",
Rating: Rating{Value: 9.6},
Year: "1993",
Directors: []string{"陈凯歌"},
Actors: []string{"张国荣", "张丰毅", "巩俐"},
},
{
ID: "1295644",
Title: "阿甘正传",
Rating: Rating{Value: 9.5},
Year: "1994",
Directors: []string{"罗伯特·泽米吉斯"},
Actors: []string{"汤姆·汉克斯", "罗宾·怀特"},
},
},
Total: 3,
}
}
// getMockTvData 获取模拟电视剧数据
func (ds *DoubanService) getMockTvData() *DoubanResponse {
return &DoubanResponse{
Notice: "⚠️ 这是模拟数据,非豆瓣实时数据",
Items: []DoubanItem{
{
ID: "26794435",
Title: "请回答1988",
Rating: Rating{Value: 9.7},
Year: "2015",
Directors: []string{"申元浩"},
Actors: []string{"李惠利", "朴宝剑", "柳俊烈"},
},
{
ID: "1309163",
Title: "大明王朝1566",
Rating: Rating{Value: 9.7},
Year: "2007",
Directors: []string{"张黎"},
Actors: []string{"陈宝国", "黄志忠", "王庆祥"},
},
{
ID: "1309169",
Title: "亮剑",
Rating: Rating{Value: 9.3},
Year: "2005",
Directors: []string{"陈健", "张前"},
Actors: []string{"李幼斌", "何政军", "张光北"},
},
},
Total: 3,
}
}
// extractItems 从API响应中提取项目列表
func (ds *DoubanService) extractItems(response map[string]interface{}) []DoubanItem {
var items []DoubanItem
// 尝试从不同的字段获取items
if itemsData, ok := response["items"]; ok {
if itemsBytes, err := json.Marshal(itemsData); err == nil {
json.Unmarshal(itemsBytes, &items)
}
} else if subjectsData, ok := response["subjects"]; ok {
if subjectsBytes, err := json.Marshal(subjectsData); err == nil {
json.Unmarshal(subjectsBytes, &items)
}
}
return items
}
// extractCategories 从API响应中提取分类列表
func (ds *DoubanService) extractCategories(response map[string]interface{}) []DoubanCategory {
var categories []DoubanCategory
if categoriesData, ok := response["categories"]; ok {
if categoriesBytes, err := json.Marshal(categoriesData); err == nil {
json.Unmarshal(categoriesBytes, &categories)
}
}
return categories
}
// FetchHotDramaNames 获取热播剧名字(用于定时任务)
func (ds *DoubanService) FetchHotDramaNames() ([]string, error) {
var dramaNames []string
// 获取电影热门榜单
movieResult, err := ds.GetMovieRanking("热门", "全部", 0, 10)
if err != nil {
log.Printf("获取电影榜单失败: %v", err)
} else if movieResult.Success && movieResult.Data != nil {
for _, item := range movieResult.Data.Items {
dramaNames = append(dramaNames, item.Title)
}
}
// 获取电视剧热门榜单
tvResult, err := ds.GetTvRanking("tv", "tv", 0, 10)
if err != nil {
log.Printf("获取电视剧榜单失败: %v", err)
} else if tvResult.Success && tvResult.Data != nil {
for _, item := range tvResult.Data.Items {
dramaNames = append(dramaNames, item.Title)
}
}
log.Printf("获取到 %d 个热播剧名字", len(dramaNames))
return dramaNames, nil
}

86
utils/global_scheduler.go Normal file
View File

@@ -0,0 +1,86 @@
package utils
import (
"log"
"res_db/db/repo"
"sync"
)
// GlobalScheduler 全局调度器管理器
type GlobalScheduler struct {
scheduler *Scheduler
mutex sync.RWMutex
}
var (
globalScheduler *GlobalScheduler
once sync.Once
)
// GetGlobalScheduler 获取全局调度器实例(单例模式)
func GetGlobalScheduler(hotDramaRepo repo.HotDramaRepository) *GlobalScheduler {
once.Do(func() {
globalScheduler = &GlobalScheduler{
scheduler: NewScheduler(hotDramaRepo),
}
})
return globalScheduler
}
// StartHotDramaScheduler 启动热播剧定时任务
func (gs *GlobalScheduler) StartHotDramaScheduler() {
gs.mutex.Lock()
defer gs.mutex.Unlock()
if gs.scheduler.IsRunning() {
log.Println("热播剧定时任务已在运行中")
return
}
gs.scheduler.StartHotDramaScheduler()
log.Println("全局调度器已启动热播剧定时任务")
}
// StopHotDramaScheduler 停止热播剧定时任务
func (gs *GlobalScheduler) StopHotDramaScheduler() {
gs.mutex.Lock()
defer gs.mutex.Unlock()
if !gs.scheduler.IsRunning() {
log.Println("热播剧定时任务未在运行")
return
}
gs.scheduler.StopHotDramaScheduler()
log.Println("全局调度器已停止热播剧定时任务")
}
// IsHotDramaSchedulerRunning 检查热播剧定时任务是否在运行
func (gs *GlobalScheduler) IsHotDramaSchedulerRunning() bool {
gs.mutex.RLock()
defer gs.mutex.RUnlock()
return gs.scheduler.IsRunning()
}
// GetHotDramaNames 手动获取热播剧名字
func (gs *GlobalScheduler) GetHotDramaNames() ([]string, error) {
return gs.scheduler.GetHotDramaNames()
}
// UpdateSchedulerStatus 根据系统配置更新调度器状态
func (gs *GlobalScheduler) UpdateSchedulerStatus(autoFetchHotDramaEnabled bool) {
gs.mutex.Lock()
defer gs.mutex.Unlock()
if autoFetchHotDramaEnabled {
if !gs.scheduler.IsRunning() {
log.Println("系统配置启用自动拉取热播剧,启动定时任务")
gs.scheduler.StartHotDramaScheduler()
}
} else {
if gs.scheduler.IsRunning() {
log.Println("系统配置禁用自动拉取热播剧,停止定时任务")
gs.scheduler.StopHotDramaScheduler()
}
}
}

175
utils/scheduler.go Normal file
View File

@@ -0,0 +1,175 @@
package utils
import (
"log"
"res_db/db/entity"
"res_db/db/repo"
"strings"
"time"
)
// Scheduler 定时任务管理器
type Scheduler struct {
doubanService *DoubanService
hotDramaRepo repo.HotDramaRepository
stopChan chan bool
isRunning bool
}
// NewScheduler 创建新的定时任务管理器
func NewScheduler(hotDramaRepo repo.HotDramaRepository) *Scheduler {
return &Scheduler{
doubanService: NewDoubanService(),
hotDramaRepo: hotDramaRepo,
stopChan: make(chan bool),
isRunning: false,
}
}
// StartHotDramaScheduler 启动热播剧定时任务
func (s *Scheduler) StartHotDramaScheduler() {
if s.isRunning {
log.Println("热播剧定时任务已在运行中")
return
}
s.isRunning = true
log.Println("启动热播剧定时任务")
go func() {
ticker := time.NewTicker(1 * time.Hour) // 每小时执行一次
defer ticker.Stop()
// 立即执行一次
s.fetchHotDramaData()
for {
select {
case <-ticker.C:
s.fetchHotDramaData()
case <-s.stopChan:
log.Println("停止热播剧定时任务")
return
}
}
}()
}
// StopHotDramaScheduler 停止热播剧定时任务
func (s *Scheduler) StopHotDramaScheduler() {
if !s.isRunning {
log.Println("热播剧定时任务未在运行")
return
}
s.stopChan <- true
s.isRunning = false
log.Println("已发送停止信号给热播剧定时任务")
}
// fetchHotDramaData 获取热播剧数据
func (s *Scheduler) fetchHotDramaData() {
log.Println("开始获取热播剧数据...")
dramaNames, err := s.doubanService.FetchHotDramaNames()
if err != nil {
log.Printf("获取热播剧数据失败: %v", err)
return
}
log.Printf("成功获取到 %d 个热播剧: %v", len(dramaNames), dramaNames)
// 处理获取到的热播剧数据
s.processHotDramaNames(dramaNames)
}
// processHotDramaNames 处理热播剧名字
func (s *Scheduler) processHotDramaNames(dramaNames []string) {
log.Printf("开始处理热播剧数据,共 %d 个", len(dramaNames))
// 获取电影和电视剧的详细数据
s.processMovieData()
s.processTvData()
log.Println("热播剧数据处理完成")
}
// processMovieData 处理电影数据
func (s *Scheduler) processMovieData() {
log.Println("开始处理电影数据...")
// 获取电影热门榜单
movieResult, err := s.doubanService.GetMovieRanking("热门", "全部", 0, 20)
if err != nil {
log.Printf("获取电影榜单失败: %v", err)
return
}
if movieResult.Success && movieResult.Data != nil {
for _, item := range movieResult.Data.Items {
drama := &entity.HotDrama{
Title: item.Title,
Rating: item.Rating.Value,
Year: item.Year,
Directors: strings.Join(item.Directors, ", "),
Actors: strings.Join(item.Actors, ", "),
Category: "电影",
SubType: "热门",
Source: "douban",
DoubanID: item.ID,
}
// 保存到数据库
if err := s.hotDramaRepo.Upsert(drama); err != nil {
log.Printf("保存电影数据失败: %v", err)
} else {
log.Printf("成功保存电影: %s", item.Title)
}
}
}
}
// processTvData 处理电视剧数据
func (s *Scheduler) processTvData() {
log.Println("开始处理电视剧数据...")
// 获取电视剧热门榜单
tvResult, err := s.doubanService.GetTvRanking("tv", "tv", 0, 20)
if err != nil {
log.Printf("获取电视剧榜单失败: %v", err)
return
}
if tvResult.Success && tvResult.Data != nil {
for _, item := range tvResult.Data.Items {
drama := &entity.HotDrama{
Title: item.Title,
Rating: item.Rating.Value,
Year: item.Year,
Directors: strings.Join(item.Directors, ", "),
Actors: strings.Join(item.Actors, ", "),
Category: "电视剧",
SubType: "热门",
Source: "douban",
DoubanID: item.ID,
}
// 保存到数据库
if err := s.hotDramaRepo.Upsert(drama); err != nil {
log.Printf("保存电视剧数据失败: %v", err)
} else {
log.Printf("成功保存电视剧: %s", item.Title)
}
}
}
}
// IsRunning 检查定时任务是否在运行
func (s *Scheduler) IsRunning() bool {
return s.isRunning
}
// GetHotDramaNames 手动获取热播剧名字(用于测试或手动调用)
func (s *Scheduler) GetHotDramaNames() ([]string, error) {
return s.doubanService.FetchHotDramaNames()
}

560
utils/url_checker.go Normal file
View File

@@ -0,0 +1,560 @@
package utils
import (
"encoding/json"
"fmt"
"net/url"
"regexp"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
)
// 定义网盘服务类型
const (
UC = "uc"
Aliyun = "aliyun"
Quark = "quark"
Pan115 = "115"
Pan123 = "123pan"
Tianyi = "tianyi"
Xunlei = "xunlei"
Baidu = "baidu"
NotFound = "notfound"
)
// 检查结果结构
type CheckResult struct {
URL string
Status bool
}
// 提取分享码和服务类型
func ExtractShareId(urlStr string) (string, string) {
return extractShareID(urlStr)
}
// 提取分享码和服务类型
func extractShareID(urlStr string) (string, string) {
netDiskPatterns := map[string]struct {
Domains []string
Pattern *regexp.Regexp
}{
UC: {
Domains: []string{"drive.uc.cn"},
Pattern: regexp.MustCompile(`https?://drive\.uc\.cn/s/([a-zA-Z0-9]+)`),
},
Aliyun: {
Domains: []string{"aliyundrive.com", "alipan.com"},
Pattern: regexp.MustCompile(`https?://(?:www\.)?(?:aliyundrive|alipan)\.com/s/([a-zA-Z0-9]+)`),
},
Quark: {
Domains: []string{"pan.quark.cn"},
Pattern: regexp.MustCompile(`https?://(?:www\.)?pan\.quark\.cn/s/([a-zA-Z0-9]+)`),
},
Pan115: {
Domains: []string{"115.com", "115cdn.com", "anxia.com"},
Pattern: regexp.MustCompile(`https?://(?:www\.)?(?:115|115cdn|anxia)\.com/s/([a-zA-Z0-9]+)`),
},
Pan123: {
Domains: []string{"123684.com", "123685.com", "123912.com", "123pan.com", "123pan.cn", "123592.com"},
Pattern: regexp.MustCompile(`https?://(?:www\.)?(?:123684|123685|123912|123pan|123pan\.cn|123592)\.com/s/([a-zA-Z0-9-]+)`),
},
Tianyi: {
Domains: []string{"cloud.189.cn"},
Pattern: regexp.MustCompile(`https?://cloud\.189\.cn/(?:t/|web/share\?code=)([a-zA-Z0-9]+)`),
},
Xunlei: {
Domains: []string{"pan.xunlei.com"},
Pattern: regexp.MustCompile(`https?://(?:www\.)?pan\.xunlei\.com/s/([a-zA-Z0-9-]+)`),
},
Baidu: {
Domains: []string{"pan.baidu.com", "yun.baidu.com"},
Pattern: regexp.MustCompile(`https?://(?:[a-z]+\.)?(?:pan|yun)\.baidu\.com/(?:s/|share/init\?surl=)([a-zA-Z0-9_-]+)(?:\?|$)`),
},
}
for service, config := range netDiskPatterns {
if containsDomain(urlStr, config.Domains) {
match := config.Pattern.FindStringSubmatch(urlStr)
if len(match) > 1 {
return match[1], service
}
}
}
return "", NotFound
}
// 检查域名是否包含在列表中
func containsDomain(urlStr string, domains []string) bool {
u, err := url.Parse(urlStr)
if err != nil {
return false
}
host := u.Hostname()
for _, domain := range domains {
if strings.Contains(host, domain) {
return true
}
}
return false
}
// 创建HTTP客户端
func createHTTPClient() *resty.Client {
return resty.New().
SetTimeout(10 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(2 * time.Second).
SetRetryMaxWaitTime(10 * time.Second)
}
// 检查UC网盘链接
func checkUC(shareID string) (bool, error) {
client := createHTTPClient()
url := fmt.Sprintf("https://drive.uc.cn/s/%s", shareID)
resp, err := client.R().
SetHeader("User-Agent", "Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Mobile Safari/537.36").
SetHeader("Host", "drive.uc.cn").
SetHeader("Referer", url).
SetHeader("Origin", "https://drive.uc.cn").
Get(url)
if err != nil {
return false, err
}
if resp.StatusCode() != 200 {
return false, nil
}
bodyStr := resp.String()
// 检查错误关键词
errorKeywords := []string{"失效", "不存在", "违规", "删除", "已过期", "被取消"}
for _, keyword := range errorKeywords {
if strings.Contains(bodyStr, keyword) {
return false, nil
}
}
// 检查是否需要访问码
if strings.Contains(bodyStr, "class=\"main-body\"") && strings.Contains(bodyStr, "class=\"input-wrap\"") {
fmt.Println("发现访问码输入框,判断为有效(需密码)")
return true, nil
}
// 检查是否包含文件列表或分享内容
if strings.Contains(bodyStr, "文件") || strings.Contains(bodyStr, "分享") || strings.Contains(bodyStr, "class=\"file-list\"") {
return true, nil
}
return false, nil
}
// 检查阿里云盘链接
func checkAliyun(shareID string) (bool, error) {
client := createHTTPClient()
apiURL := "https://api.aliyundrive.com/adrive/v3/share_link/get_share_by_anonymous"
data := map[string]string{"share_id": shareID}
resp, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody(data).
Post(apiURL)
if err != nil {
return false, err
}
var responseJSON map[string]interface{}
err = json.Unmarshal(resp.Body(), &responseJSON)
if err != nil {
return false, err
}
if hasPwd, ok := responseJSON["has_pwd"].(bool); ok && hasPwd {
return true, nil
}
if code, ok := responseJSON["code"].(string); ok && code == "NotFound.ShareLink" {
return false, nil
}
if _, ok := responseJSON["file_infos"]; !ok {
return false, nil
}
return true, nil
}
// 检查115网盘链接
func check115(shareID string) (bool, error) {
client := createHTTPClient()
apiURL := "https://webapi.115.com/share/snap"
params := map[string]string{
"share_code": shareID,
"receive_code": "",
}
resp, err := client.R().
SetQueryParams(params).
Get(apiURL)
if err != nil {
return false, err
}
var responseJSON map[string]interface{}
err = json.Unmarshal(resp.Body(), &responseJSON)
if err != nil {
return false, err
}
if state, ok := responseJSON["state"].(bool); ok && state {
return true, nil
}
if errorMsg, ok := responseJSON["error"].(string); ok && strings.Contains(errorMsg, "请输入访问码") {
return true, nil
}
return false, nil
}
// 检查夸克网盘链接
func checkQuark(shareID string) (bool, error) {
client := createHTTPClient()
apiURL := "https://drive.quark.cn/1/clouddrive/share/sharepage/token"
data := map[string]string{"pwd_id": shareID, "passcode": ""}
resp, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody(data).
Post(apiURL)
if err != nil {
return false, err
}
var responseJSON map[string]interface{}
err = json.Unmarshal(resp.Body(), &responseJSON)
if err != nil {
return false, err
}
if message, ok := responseJSON["message"].(string); ok && message == "ok" {
data, ok := responseJSON["data"].(map[string]interface{})
if !ok {
return false, nil
}
stoken, ok := data["stoken"].(string)
if !ok || stoken == "" {
return false, nil
}
detailURL := fmt.Sprintf("https://drive-h.quark.cn/1/clouddrive/share/sharepage/detail?pwd_id=%s&stoken=%s&_fetch_share=1", shareID, url.QueryEscape(stoken))
detailResp, err := client.R().Get(detailURL)
if err != nil {
return false, err
}
var detailResponseJSON map[string]interface{}
err = json.Unmarshal(detailResp.Body(), &detailResponseJSON)
if err != nil {
return false, err
}
if status, ok := detailResponseJSON["status"].(float64); ok && status == 400 {
return true, nil
}
if data, ok := detailResponseJSON["data"].(map[string]interface{}); ok {
if share, ok := data["share"].(map[string]interface{}); ok {
if status, ok := share["status"].(float64); ok && status == 1 {
return true, nil
}
}
}
return false, nil
} else if message, ok := responseJSON["message"].(string); ok && message == "需要提取码" {
return true, nil
}
return false, nil
}
// 检查123网盘链接
func check123pan(shareID string) (bool, error) {
client := createHTTPClient()
apiURL := fmt.Sprintf("https://www.123pan.com/api/share/info?shareKey=%s", shareID)
resp, err := client.R().
SetHeader("User-Agent", "Mozilla/5.0").
Get(apiURL)
if err != nil {
return false, err
}
bodyStr := resp.String()
if bodyStr == "" || strings.Contains(bodyStr, "分享页面不存在") {
return false, nil
}
var responseJSON map[string]interface{}
err = json.Unmarshal(resp.Body(), &responseJSON)
if err != nil {
return false, err
}
if code, ok := responseJSON["code"].(float64); ok && code != 0 {
return false, nil
}
if data, ok := responseJSON["data"].(map[string]interface{}); ok {
if hasPwd, ok := data["HasPwd"].(bool); ok && hasPwd {
return true, nil
}
}
return true, nil
}
// 检查天翼网盘链接
func checkTianyi(shareID string) (bool, error) {
client := createHTTPClient()
apiURL := "https://api.cloud.189.cn/open/share/getShareInfoByCodeV2.action"
data := map[string]string{"shareCode": shareID}
resp, err := client.R().
SetHeader("Content-Type", "application/x-www-form-urlencoded").
SetFormData(data).
Post(apiURL)
if err != nil {
return false, err
}
bodyStr := resp.String()
errorKeywords := []string{"ShareInfoNotFound", "ShareNotFound", "FileNotFound", "ShareExpiredError", "ShareAuditNotPass"}
for _, keyword := range errorKeywords {
if strings.Contains(bodyStr, keyword) {
return false, nil
}
}
if strings.Contains(bodyStr, "needAccessCode") {
return true, nil
}
return true, nil
}
// 检查迅雷网盘链接
func checkXunlei(shareID string) (bool, error) {
client := createHTTPClient()
tokenURL := "https://xluser-ssl.xunlei.com/v1/shield/captcha/init"
data := map[string]interface{}{
"client_id": "Xqp0kJBXWhwaTpB6",
"device_id": "925b7631473a13716b791d7f28289cad",
"action": "get:/drive/v1/share",
"meta": map[string]interface{}{
"package_name": "pan.xunlei.com",
"client_version": "1.45.0",
"captcha_sign": "1.fe2108ad808a74c9ac0243309242726c",
"timestamp": "1645241033384",
},
}
resp, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody(data).
Post(tokenURL)
if err != nil {
return false, err
}
var tokenResponseJSON map[string]interface{}
err = json.Unmarshal(resp.Body(), &tokenResponseJSON)
if err != nil {
return false, err
}
token, ok := tokenResponseJSON["captcha_token"].(string)
if !ok || token == "" {
return false, nil
}
apiURL := fmt.Sprintf("https://api-pan.xunlei.com/drive/v1/share?share_id=%s", shareID)
resp, err = client.R().
SetHeader("x-captcha-token", token).
SetHeader("x-client-id", "Xqp0kJBXWhwaTpB6").
SetHeader("x-device-id", "925b7631473a13716b791d7f28289cad").
Get(apiURL)
if err != nil {
return false, err
}
bodyStr := resp.String()
errorKeywords := []string{"NOT_FOUND", "SENSITIVE_RESOURCE", "EXPIRED"}
for _, keyword := range errorKeywords {
if strings.Contains(bodyStr, keyword) {
return false, nil
}
}
if strings.Contains(bodyStr, "PASS_CODE_EMPTY") {
return true, nil
}
return true, nil
}
// 检查百度网盘链接
func checkBaidu(shareID string) (bool, error) {
client := createHTTPClient()
url := fmt.Sprintf("https://pan.baidu.com/s/%s", shareID)
resp, err := client.R().
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36").
Get(url)
if err != nil {
return false, err
}
bodyStr := resp.String()
errorKeywords := []string{"分享的文件已经被取消", "分享已过期", "你访问的页面不存在", "你所访问的页面"}
for _, keyword := range errorKeywords {
if strings.Contains(bodyStr, keyword) {
return false, nil
}
}
if strings.Contains(bodyStr, "请输入提取码") || strings.Contains(bodyStr, "提取文件") {
return true, nil
}
if strings.Contains(bodyStr, "过期时间") || strings.Contains(bodyStr, "文件列表") {
return true, nil
}
return false, nil
}
// 检查URL有效性
func CheckURL(urlStr string) (CheckResult, error) {
shareID, service := extractShareID(urlStr)
if shareID == "" || service == NotFound {
fmt.Printf("无法识别的链接或网盘服务: %s\n", urlStr)
return CheckResult{URL: urlStr, Status: false}, nil
}
checkFunctions := map[string]func(string) (bool, error){
UC: checkUC,
Aliyun: checkAliyun,
Quark: checkQuark,
Pan115: check115,
Pan123: check123pan,
Tianyi: checkTianyi,
Xunlei: checkXunlei,
Baidu: checkBaidu,
}
if fn, ok := checkFunctions[service]; ok {
result, err := fn(shareID)
if err != nil {
return CheckResult{URL: urlStr, Status: false}, err
}
return CheckResult{URL: urlStr, Status: result}, nil
}
fmt.Printf("未找到服务 %s 的检测函数\n", service)
return CheckResult{URL: urlStr, Status: false}, nil
}
// 主函数
func Test() {
urls := []string{
// UC网盘
"https://drive.uc.cn/s/e1ebe95d144c4?public=1", // UC网盘有效
"https://drive.uc.cn/s/m7and23e132a1?public=1", // UC网盘无效
// 阿里云
"https://www.aliyundrive.com/s/hz1HHxhahsE", // aliyundrive 公开分享
"https://www.alipan.com/s/QbaHJ71QjV1", // alipan 公开分享
"https://www.alipan.com/s/GMrv1QCZhNB", // 带提取码
"https://www.aliyundrive.com/s/p51zbVtgmy", // 链接错误 NotFound.ShareLink
"https://www.aliyundrive.com/s/hZnj4qLMMd9", // 空文件
// 115
"https://115cdn.com/s/swh88n13z72?password=r9b2#",
"https://anxia.com/s/swhm75q3z5o?password=ayss",
"https://115.com/s/swhsaua36a1?password=oc92", // 带访问码
"https://115.com/s/sw313r03zx1", // 分享的文件涉嫌违规,链接已失效
// 夸克
"https://pan.quark.cn/s/9803af406f13", // 公开分享
"https://pan.quark.cn/s/f161a5364657", // 提取码
"https://pan.quark.cn/s/9803af406f15", // 分享不存在
"https://pan.quark.cn/s/b999385c0936", // 违规
"https://pan.quark.cn/s/c66f71b6f7d5", // 取消分享
// 123
"https://www.123pan.com/s/i4uaTd-WHn0", // 公开分享
"https://www.123912.com/s/U8f2Td-ZeOX",
"https://www.123684.com/s/u9izjv-k3uWv",
"https://www.123pan.com/s/A6cA-AKH11", // 外链不存在
// 天翼
"https://cloud.189.cn/t/viy2quQzMBne", // 公开分享
"https://cloud.189.cn/web/share?code=UfUjiiFRbymq", // 带密码分享长链接
"https://cloud.189.cn/t/vENFvuVNbyqa", // 外链不存在
"https://cloud.189.cn/t/notexist", // 分享不存在
// 百度
"https://pan.baidu.com/s/1rIcc6X7D3rVzNSqivsRejw?pwd=0w0j", // 带提取码分享
"https://pan.baidu.com/s/1TMhfQ5yNnlPPSGbw4RQ-LA?pwd=6j77", // 带提取码分享
"https://pan.baidu.com/s/1J_CUxLKqC0h3Ypg4sQV0_g", // 无法识别
"https://pan.baidu.com/s/1HlvGfj8qVUBym24X2I9ukA", // 分享被和谐
"https://pan.baidu.com/s/1cgsY10lkrPGZ-zt8oVdR_w", // 分享已过期
"https://pan.baidu.com/s/1R_itrvmA0ZyMMaHybg7G2Q", // 分享已删除
"https://pan.baidu.com/s/1hqge8hI", // 分享链接错误
"https://pan.baidu.com/s/1notexist", // 分享不存在
}
var wg sync.WaitGroup
results := make(chan CheckResult, len(urls))
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
result, err := CheckURL(u)
if err != nil {
fmt.Printf("检查 %s 时出错: %v\n", u, err)
result = CheckResult{URL: u, Status: false}
}
results <- result
}(url)
}
go func() {
wg.Wait()
close(results)
}()
fmt.Println("\n检测结果")
for result := range results {
fmt.Printf("%s - %s\n", result.URL, map[bool]string{true: "有效", false: "无效"}[result.Status])
}
}

View File

@@ -1,3 +1,21 @@
// 统一响应解析函数
export const parseApiResponse = <T>(response: any): T => {
// 检查是否是新的统一响应格式
if (response && typeof response === 'object' && 'code' in response && 'data' in response) {
if (response.code === 200) {
// 特殊处理pan接口返回的data.list格式
if (response.data && response.data.list && Array.isArray(response.data.list)) {
return response.data.list
}
return response.data
} else {
throw new Error(response.message || '请求失败')
}
}
// 兼容旧格式,直接返回响应
return response
}
// 使用 $fetch 替代 axios更好地处理 SSR
export const useResourceApi = () => {
const config = useRuntimeConfig()
@@ -8,60 +26,67 @@ export const useResourceApi = () => {
}
const getResources = async (params?: any) => {
return await $fetch('/resources', {
const response = await $fetch('/resources', {
baseURL: config.public.apiBase,
params,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const getResource = async (id: number) => {
return await $fetch(`/resources/${id}`, {
const response = await $fetch(`/resources/${id}`, {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const createResource = async (data: any) => {
return await $fetch('/resources', {
const response = await $fetch('/resources', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const updateResource = async (id: number, data: any) => {
return await $fetch(`/resources/${id}`, {
const response = await $fetch(`/resources/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const deleteResource = async (id: number) => {
return await $fetch(`/resources/${id}`, {
const response = await $fetch(`/resources/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const searchResources = async (params: any) => {
return await $fetch('/search', {
const response = await $fetch('/search', {
baseURL: config.public.apiBase,
params,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const getResourcesByPan = async (panId: number, params?: any) => {
return await $fetch('/resources', {
const response = await $fetch('/resources', {
baseURL: config.public.apiBase,
params: { ...params, pan_id: panId },
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
return {
@@ -80,27 +105,30 @@ export const useAuthApi = () => {
const config = useRuntimeConfig()
const login = async (data: any) => {
return await $fetch('/auth/login', {
const response = await $fetch('/auth/login', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
return parseApiResponse(response)
}
const register = async (data: any) => {
return await $fetch('/auth/register', {
const response = await $fetch('/auth/register', {
baseURL: config.public.apiBase,
method: 'POST',
body: data
})
return parseApiResponse(response)
}
const getProfile = async () => {
const token = localStorage.getItem('token')
return await $fetch('/auth/profile', {
const response = await $fetch('/auth/profile', {
baseURL: config.public.apiBase,
headers: token ? { Authorization: `Bearer ${token}` } : {}
})
return parseApiResponse(response)
}
return {
@@ -120,36 +148,40 @@ export const useCategoryApi = () => {
}
const getCategories = async () => {
return await $fetch('/categories', {
const response = await $fetch('/categories', {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const createCategory = async (data: any) => {
return await $fetch('/categories', {
const response = await $fetch('/categories', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const updateCategory = async (id: number, data: any) => {
return await $fetch(`/categories/${id}`, {
const response = await $fetch(`/categories/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const deleteCategory = async (id: number) => {
return await $fetch(`/categories/${id}`, {
const response = await $fetch(`/categories/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
return {
@@ -170,43 +202,48 @@ export const usePanApi = () => {
}
const getPans = async () => {
return await $fetch('/pans', {
const response = await $fetch('/pans', {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const getPan = async (id: number) => {
return await $fetch(`/pans/${id}`, {
const response = await $fetch(`/pans/${id}`, {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const createPan = async (data: any) => {
return await $fetch('/pans', {
const response = await $fetch('/pans', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const updatePan = async (id: number, data: any) => {
return await $fetch(`/pans/${id}`, {
const response = await $fetch(`/pans/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const deletePan = async (id: number) => {
return await $fetch(`/pans/${id}`, {
const response = await $fetch(`/pans/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
return {
@@ -228,44 +265,49 @@ export const useCksApi = () => {
}
const getCks = async (params?: any) => {
return await $fetch('/cks', {
const response = await $fetch('/cks', {
baseURL: config.public.apiBase,
params,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const getCksByID = async (id: number) => {
return await $fetch(`/cks/${id}`, {
const response = await $fetch(`/cks/${id}`, {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const createCks = async (data: any) => {
return await $fetch('/cks', {
const response = await $fetch('/cks', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const updateCks = async (id: number, data: any) => {
return await $fetch(`/cks/${id}`, {
const response = await $fetch(`/cks/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const deleteCks = async (id: number) => {
return await $fetch(`/cks/${id}`, {
const response = await $fetch(`/cks/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
return {
@@ -287,50 +329,56 @@ export const useTagApi = () => {
}
const getTags = async () => {
return await $fetch('/tags', {
const response = await $fetch('/tags', {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const getTag = async (id: number) => {
return await $fetch(`/tags/${id}`, {
const response = await $fetch(`/tags/${id}`, {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const createTag = async (data: any) => {
return await $fetch('/tags', {
const response = await $fetch('/tags', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const updateTag = async (id: number, data: any) => {
return await $fetch(`/tags/${id}`, {
const response = await $fetch(`/tags/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const deleteTag = async (id: number) => {
return await $fetch(`/tags/${id}`, {
const response = await $fetch(`/tags/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const getResourceTags = async (resourceId: number) => {
return await $fetch(`/resources/${resourceId}/tags`, {
const response = await $fetch(`/resources/${resourceId}/tags`, {
baseURL: config.public.apiBase,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
return {
@@ -353,57 +401,63 @@ export const useReadyResourceApi = () => {
}
const getReadyResources = async (params?: any) => {
return await $fetch('/ready-resources', {
const response = await $fetch('/ready-resources', {
baseURL: config.public.apiBase,
params,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const createReadyResource = async (data: any) => {
return await $fetch('/ready-resources', {
const response = await $fetch('/ready-resources', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const batchCreateReadyResources = async (data: any) => {
return await $fetch('/ready-resources/batch', {
const response = await $fetch('/ready-resources/batch', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const createReadyResourcesFromText = async (text: string) => {
const formData = new FormData()
formData.append('text', text)
return await $fetch('/ready-resources/text', {
const response = await $fetch('/ready-resources/text', {
baseURL: config.public.apiBase,
method: 'POST',
body: formData,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const deleteReadyResource = async (id: number) => {
return await $fetch(`/ready-resources/${id}`, {
const response = await $fetch(`/ready-resources/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const clearReadyResources = async () => {
return await $fetch('/ready-resources', {
const response = await $fetch('/ready-resources', {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
return {
@@ -421,9 +475,10 @@ export const useStatsApi = () => {
const config = useRuntimeConfig()
const getStats = async () => {
return await $fetch('/stats', {
const response = await $fetch('/stats', {
baseURL: config.public.apiBase,
})
return parseApiResponse(response)
}
return {
@@ -441,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<string, string>
})
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<string, string>
})
return parseApiResponse(response)
}
const updateHotDrama = async (id: number, data: any) => {
const response = await $fetch(`/hot-dramas/${id}`, {
baseURL: config.public.apiBase,
method: 'PUT',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const deleteHotDrama = async (id: number) => {
const response = await $fetch(`/hot-dramas/${id}`, {
baseURL: config.public.apiBase,
method: 'DELETE',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
const fetchHotDramas = async () => {
const response = await $fetch('/hot-dramas/fetch', {
baseURL: config.public.apiBase,
method: 'POST',
headers: getAuthHeaders() as Record<string, string>
})
return parseApiResponse(response)
}
return {
getHotDramas,
createHotDrama,
updateHotDrama,
deleteHotDrama,
fetchHotDramas,
}
}
// 监控相关API
export const useMonitorApi = () => {
const config = useRuntimeConfig()
const getPerformanceStats = async () => {
const response = await $fetch('/performance', {
baseURL: config.public.apiBase,
})
return parseApiResponse(response)
}
const getSystemInfo = async () => {
const response = await $fetch('/system/info', {
baseURL: config.public.apiBase,
})
return parseApiResponse(response)
}
const getBasicStats = async () => {
const response = await $fetch('/stats', {
baseURL: config.public.apiBase,
})
return parseApiResponse(response)
}
return {
getPerformanceStats,
getSystemInfo,
getBasicStats,
}
}

View File

@@ -4,8 +4,8 @@ export default defineNuxtRouteMiddleware((to, from) => {
// 初始化用户状态
userStore.initAuth()
// 如果用户未登录,重定向到首页
// 如果用户未登录,重定向到登录页面
if (!userStore.isAuthenticated) {
return navigateTo('/')
return navigateTo('/login')
}
})

View File

@@ -320,11 +320,9 @@ const fetchSystemConfig = async () => {
try {
const response = await getSystemConfig()
console.log('admin系统配置响应:', response)
if (response && response.success && response.data) {
systemConfig.value = response.data
} else if (response && response.data) {
// 兼容非标准格式
systemConfig.value = response.data
// 使用新的统一响应格式直接使用response
if (response) {
systemConfig.value = response
}
} catch (error) {
console.error('获取系统配置失败:', error)

214
web/pages/hot-dramas.vue Normal file
View File

@@ -0,0 +1,214 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="container mx-auto px-4 py-8">
<!-- 页面标题 -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">热播剧榜单</h1>
<p class="text-gray-600">实时获取豆瓣热门电影和电视剧榜单</p>
</div>
<!-- 筛选器 -->
<div class="mb-6 flex flex-wrap gap-4">
<button
v-for="category in categories"
:key="category.value"
@click="selectedCategory = category.value"
:class="[
'px-4 py-2 rounded-lg font-medium transition-colors',
selectedCategory === category.value
? 'bg-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
]"
>
{{ category.label }}
</button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="flex justify-center items-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
<!-- 热播剧列表 -->
<div v-else-if="dramas.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div
v-for="drama in filteredDramas"
:key="drama.id"
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
>
<!-- 剧集信息 -->
<div class="p-6">
<div class="flex items-start justify-between mb-3">
<h3 class="text-lg font-semibold text-gray-900 line-clamp-2">
{{ drama.title }}
</h3>
<div class="flex items-center ml-2">
<span class="text-yellow-500 text-sm font-medium">{{ drama.rating }}</span>
<span class="text-gray-400 text-xs ml-1"></span>
</div>
</div>
<!-- 年份和分类 -->
<div class="flex items-center gap-2 mb-3">
<span v-if="drama.year" class="text-sm text-gray-500">{{ drama.year }}</span>
<span class="text-sm text-blue-600 bg-blue-100 px-2 py-1 rounded">
{{ drama.category }}
</span>
<span v-if="drama.sub_type" class="text-sm text-gray-500 bg-gray-100 px-2 py-1 rounded">
{{ drama.sub_type }}
</span>
</div>
<!-- 导演 -->
<div v-if="drama.directors" class="mb-2">
<span class="text-xs text-gray-500">导演</span>
<span class="text-sm text-gray-700">{{ drama.directors }}</span>
</div>
<!-- 演员 -->
<div v-if="drama.actors" class="mb-3">
<span class="text-xs text-gray-500">主演</span>
<span class="text-sm text-gray-700 line-clamp-2">{{ drama.actors }}</span>
</div>
<!-- 数据来源 -->
<div class="flex items-center justify-between text-xs text-gray-400">
<span>来源{{ drama.source }}</span>
<span>{{ formatDate(drama.created_at) }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="text-center py-12">
<div class="text-gray-400 mb-4">
<svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">暂无热播剧数据</h3>
<p class="text-gray-500">请稍后再试或联系管理员</p>
</div>
<!-- 分页 -->
<div v-if="total > pageSize" class="mt-8 flex justify-center">
<nav class="flex items-center space-x-2">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage <= 1"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
上一页
</button>
<span class="px-3 py-2 text-sm text-gray-700">
{{ currentPage }} {{ Math.ceil(total / pageSize) }}
</span>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage >= Math.ceil(total / pageSize)"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
下一页
</button>
</nav>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
// 响应式数据
const loading = ref(false)
const dramas = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const selectedCategory = ref('')
// 分类选项
const categories = ref([
{ label: '全部', value: '' },
{ label: '电影', value: '电影' },
{ label: '电视剧', value: '电视剧' }
])
// 计算属性
const filteredDramas = computed(() => {
if (!selectedCategory.value) {
return dramas.value
}
return dramas.value.filter(drama => drama.category === selectedCategory.value)
})
// 获取热播剧列表
const fetchDramas = async () => {
loading.value = true
try {
const { useHotDramaApi } = await import('~/composables/useApi')
const hotDramaApi = useHotDramaApi()
const params = {
page: currentPage.value,
page_size: pageSize.value
}
if (selectedCategory.value) {
params.category = selectedCategory.value
}
const response = await hotDramaApi.getHotDramas(params)
// 使用新的统一响应格式
if (response && response.items) {
dramas.value = response.items
total.value = response.total || 0
} else {
// 兼容旧格式
dramas.value = Array.isArray(response) ? response : []
total.value = dramas.value.length
}
} catch (error) {
console.error('获取热播剧列表失败:', error)
} finally {
loading.value = false
}
}
// 切换页面
const changePage = (page) => {
if (page >= 1 && page <= Math.ceil(total.value / pageSize.value)) {
currentPage.value = page
fetchDramas()
}
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN')
}
// 监听分类变化
watch(selectedCategory, () => {
currentPage.value = 1
fetchDramas()
})
// 页面加载时获取数据
onMounted(() => {
fetchDramas()
})
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 flex flex-col">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
@@ -13,7 +13,9 @@
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 主要内容区域 -->
<div class="flex-1 p-3 sm:p-5">
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
@@ -22,6 +24,18 @@
</a>
</h1>
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4 right-4 top-0 absolute">
<NuxtLink
to="/hot-dramas"
class="w-full sm:w-auto px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-film"></i> 热播剧
</NuxtLink>
<NuxtLink
to="/monitor"
class="w-full sm:w-auto px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-chart-line"></i> 系统监控
</NuxtLink>
<NuxtLink
v-if="authInitialized && !userStore.isAuthenticated"
to="/login"
@@ -68,7 +82,7 @@
class="px-2 py-1 text-xs rounded-full bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-100 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
@click="filterByPlatform(platform.id)"
>
{{ getPlatformIcon(platform.name) }} {{ platform.name }}
<span v-html="getPlatformIcon(platform.name)"></span> {{ platform.name }}
</button>
</div>
@@ -76,11 +90,11 @@
<div class="flex justify-between mt-3 text-sm text-gray-600 dark:text-gray-300 px-2">
<div class="flex items-center">
<i class="fas fa-calendar-day text-pink-600 mr-1"></i>
今日更新: <span class="font-medium text-pink-600 ml-1 count-up" :data-target="todayUpdates">0</span>
今日更新: <span class="font-medium text-pink-600 ml-1 count-up" :data-target="safeTodayUpdates">0</span>
</div>
<div class="flex items-center">
<i class="fas fa-database text-blue-600 mr-1"></i>
总资源数: <span class="font-medium text-blue-600 ml-1 count-up" :data-target="stats?.total_resources || 0">0</span>
总资源数: <span class="font-medium text-blue-600 ml-1 count-up" :data-target="safeStats?.total_resources || 0">0</span>
</div>
</div>
</div>
@@ -100,63 +114,75 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-if="loading" class="text-center py-8">
<tr v-if="safeLoading" class="text-center py-8">
<td colspan="3" class="text-gray-500 dark:text-gray-400">
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
</td>
</tr>
<tr v-else-if="resources.length === 0" class="text-center py-8">
<tr v-else-if="safeResources.length === 0" class="text-center py-8">
<td colspan="3" class="text-gray-500 dark:text-gray-400">暂无数据</td>
</tr>
<tr
v-for="resource in (resources as unknown as ExtendedResource[])"
:key="resource.id"
:class="isUpdatedToday(resource.updated_at) ? 'hover:bg-pink-50 dark:hover:bg-pink-900 bg-pink-50/30 dark:bg-pink-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-800'"
>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm">
<div class="flex items-start">
<span class="mr-2 flex-shrink-0">{{ getPlatformIcon(getPlatformName(resource.pan_id || 0)) }}</span>
<span class="break-words">{{ resource.title }}</span>
</div>
<div class="sm:hidden mt-1">
<button
class="text-blue-600 hover:text-blue-800 text-xs flex items-center gap-1 show-link-btn"
@click="toggleLink(resource)"
>
<i class="fas fa-eye"></i> 显示链接
</button>
<a
v-if="resource.showLink"
:href="resource.url"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline text-xs break-all"
>
{{ resource.url }}
</a>
</div>
</td>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm hidden sm:table-cell">
<button
class="text-blue-600 hover:text-blue-800 flex items-center gap-1 show-link-btn"
@click="toggleLink(resource)"
>
<i class="fas fa-eye"></i> 显示链接
</button>
<a
v-if="resource.showLink"
:href="resource.url"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline"
>
{{ resource.url }}
</a>
</td>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-gray-500" :title="resource.updated_at">
{{ formatRelativeTime(resource.updated_at) }}
</td>
</tr>
<tr
v-for="(resource, index) in visibleResources"
:key="resource.id"
:class="isUpdatedToday(resource.updated_at) ? 'hover:bg-pink-50 dark:hover:bg-pink-900 bg-pink-50/30 dark:bg-pink-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-800'"
v-intersection="onIntersection"
:data-index="index"
>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm">
<div class="flex items-start">
<span class="mr-2 flex-shrink-0" v-html="getPlatformIcon(getPlatformName(resource.pan_id || 0))"></span>
<span class="break-words">{{ resource.title }}</span>
</div>
<div class="sm:hidden mt-1">
<button
class="text-blue-600 hover:text-blue-800 text-xs flex items-center gap-1 show-link-btn"
@click="toggleLink(resource)"
>
<i class="fas fa-eye"></i> 显示链接
</button>
<a
v-if="resource.showLink"
:href="resource.url"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline text-xs break-all"
>
{{ resource.url }}
</a>
</div>
</td>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm hidden sm:table-cell">
<button
class="text-blue-600 hover:text-blue-800 flex items-center gap-1 show-link-btn"
@click="toggleLink(resource)"
>
<i class="fas fa-eye"></i> 显示链接
</button>
<a
v-if="resource.showLink"
:href="resource.url"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline"
>
{{ resource.url }}
</a>
</td>
<td class="px-2 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-gray-500" :title="resource.updated_at">
{{ formatRelativeTime(resource.updated_at) }}
</td>
</tr>
</tbody>
</table>
<!-- 加载更多按钮 -->
<div v-if="hasMoreData && !safeLoading" class="text-center py-4">
<button
@click="loadMore"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
加载更多
</button>
</div>
</div>
<!-- 分页 -->
@@ -212,10 +238,11 @@
@close="closeModal"
@save="handleSaveResource"
/> -->
</div>
<!-- 页脚 -->
<footer class="mt-8 py-6 border-t border-gray-200">
<div class="max-w-7xl mx-auto text-center text-gray-600 text-sm">
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p>{{ systemConfig?.copyright || '© 2025 网盘资源管理系统 By 小七' }}</p>
</div>
@@ -224,10 +251,6 @@
</template>
<script setup lang="ts">
const store = useResourceStore()
const userStore = useUserStore()
const { resources, categories, stats, loading } = storeToRefs(store)
// 响应式数据
const searchQuery = ref('')
const selectedPlatform = ref('')
@@ -235,6 +258,76 @@ const authInitialized = ref(false) // 添加认证状态初始化标志
const pageLoading = ref(true) // 添加页面加载状态
const systemConfig = ref<SystemConfig | null>(null) // 添加系统配置状态
// 虚拟滚动相关
const visibleResources = ref<any[]>([])
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = ref(20)
const isLoadingMore = ref(false)
// 延迟初始化store避免SSR过程中的错误
let store: any = null
let userStore: any = null
// 本地状态管理避免SSR过程中的store访问
const localResources = ref<any[]>([])
const localCategories = ref<any[]>([])
const localStats = ref<any>({ total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0 })
const localLoading = ref(false)
// 安全的store访问
const safeResources = computed(() => {
try {
if (process.client && store) {
const storeRefs = storeToRefs(store)
return (storeRefs as any).resources?.value || localResources.value
}
return localResources.value
} catch (error) {
console.error('获取resources时出错:', error)
return localResources.value
}
})
const safeCategories = computed(() => {
try {
if (process.client && store) {
const storeRefs = storeToRefs(store)
return (storeRefs as any).categories?.value || localCategories.value
}
return localCategories.value
} catch (error) {
console.error('获取categories时出错:', error)
return localCategories.value
}
})
const safeStats = computed(() => {
try {
if (process.client && store) {
const storeRefs = storeToRefs(store)
return (storeRefs as any).stats?.value || localStats.value
}
return localStats.value
} catch (error) {
console.error('获取stats时出错:', error)
return localStats.value
}
})
const safeLoading = computed(() => {
try {
if (process.client && store) {
const storeRefs = storeToRefs(store)
return (storeRefs as any).loading?.value || localLoading.value
}
return localLoading.value
} catch (error) {
console.error('获取loading时出错:', error)
return localLoading.value
}
})
// 动态SEO配置
const seoConfig = computed(() => ({
title: systemConfig.value?.site_title || '网盘资源管理系统',
@@ -260,30 +353,44 @@ const seoConfig = computed(() => ({
// 页面元数据 - 使用watchEffect来避免组件卸载时的错误
watchEffect(() => {
if (systemConfig.value) {
useHead({
title: systemConfig.value.site_title || '网盘资源管理系统',
meta: [
{
name: 'description',
content: systemConfig.value.site_description || '专业的网盘资源管理系统'
},
{
name: 'keywords',
content: systemConfig.value.keywords || '网盘,资源管理,文件分享'
},
{
name: 'author',
content: systemConfig.value.author || '系统管理员'
},
{
name: 'copyright',
content: systemConfig.value.copyright || '© 2024 网盘资源管理系统'
}
]
})
} else {
// 默认SEO配置
try {
if (systemConfig.value && systemConfig.value.site_title) {
useHead({
title: systemConfig.value.site_title || '网盘资源管理系统',
meta: [
{
name: 'description',
content: systemConfig.value.site_description || '专业的网盘资源管理系统'
},
{
name: 'keywords',
content: systemConfig.value.keywords || '网盘,资源管理,文件分享'
},
{
name: 'author',
content: systemConfig.value.author || '系统管理员'
},
{
name: 'copyright',
content: systemConfig.value.copyright || '© 2024 网盘资源管理系统'
}
]
})
} else {
// 默认SEO配置
useHead({
title: '网盘资源管理系统',
meta: [
{ name: 'description', content: '专业的网盘资源管理系统' },
{ name: 'keywords', content: '网盘,资源管理,文件分享' },
{ name: 'author', content: '系统管理员' },
{ name: 'copyright', content: '© 2024 网盘资源管理系统' }
]
})
}
} catch (error) {
console.error('设置页面元数据时出错:', error)
// 使用默认配置作为后备
useHead({
title: '网盘资源管理系统',
meta: [
@@ -301,7 +408,6 @@ watchEffect(() => {
// const showAddResourceModal = ref(false)
const editingResource = ref<any>(null)
const currentPage = ref(1)
const totalPages = ref(1)
interface Platform {
id: number
@@ -353,6 +459,29 @@ interface SystemConfig {
const platforms = ref<Platform[]>([])
const todayUpdates = ref(0)
// 安全地计算今日更新数量
const safeTodayUpdates = computed(() => {
try {
const resources = safeResources.value
if (!resources || !Array.isArray(resources)) {
return 0
}
const today = new Date().toDateString()
return resources.filter((resource: any) => {
if (!resource || !resource.updated_at) return false
try {
return new Date(resource.updated_at).toDateString() === today
} catch (dateError) {
console.error('解析日期时出错:', dateError)
return false
}
}).length
} catch (error) {
console.error('计算今日更新数量时出错:', error)
return 0
}
})
// 防抖搜索
let searchTimeout: NodeJS.Timeout
const debounceSearch = () => {
@@ -383,22 +512,53 @@ const fetchSystemConfig = async () => {
onMounted(async () => {
console.log('首页 - onMounted 开始')
// 初始化用户状态
userStore.initAuth()
authInitialized.value = true // 设置认证状态初始化完成
console.log('首页 - authInitialized:', authInitialized.value)
console.log('首页 - isAuthenticated:', userStore.isAuthenticated)
console.log('首页 - user:', userStore.userInfo)
try {
await Promise.all([
store.fetchResources(),
store.fetchCategories(),
store.fetchStats(),
fetchPlatforms(),
fetchSystemConfig(), // 获取系统配置
// 初始化store
store = useResourceStore()
userStore = useUserStore()
// 初始化用户状态
userStore.initAuth()
authInitialized.value = true // 设置认证状态初始化完成
console.log('首页 - authInitialized:', authInitialized.value)
console.log('首页 - isAuthenticated:', userStore.isAuthenticated)
console.log('首页 - user:', userStore.userInfo)
// 使用Promise.allSettled来确保即使某个请求失败也不会影响其他请求
const results = await Promise.allSettled([
store.fetchResources().then((data: any) => {
localResources.value = data.resources || []
return data
}).catch((e: any) => {
console.error('获取资源失败:', e)
return { resources: [] }
}),
store.fetchCategories().then((data: any) => {
localCategories.value = data.categories || []
return data
}).catch((e: any) => {
console.error('获取分类失败:', e)
return { categories: [] }
}),
store.fetchStats().then((data: any) => {
localStats.value = data || { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0 }
return data
}).catch((e: any) => {
console.error('获取统计失败:', e)
return { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0 }
}),
fetchPlatforms().catch((e: any) => console.error('获取平台失败:', e)),
fetchSystemConfig().catch((e: any) => console.error('获取系统配置失败:', e)),
])
// 检查哪些请求成功了
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`请求 ${index} 失败:`, result.reason)
}
})
animateCounters()
} catch (error) {
console.error('页面数据加载失败:', error)
@@ -415,7 +575,9 @@ const fetchPlatforms = async () => {
const { usePanApi } = await import('~/composables/useApi')
const panApi = usePanApi()
const response = await panApi.getPans() as any
platforms.value = response.pans || []
// 后端直接返回数组,不需要 .pans
platforms.value = Array.isArray(response) ? response : []
console.log('获取到的平台数据:', platforms.value)
} catch (error) {
console.error('获取平台列表失败:', error)
}
@@ -423,8 +585,20 @@ const fetchPlatforms = async () => {
// 搜索处理
const handleSearch = () => {
const platformId = selectedPlatform.value ? parseInt(selectedPlatform.value) : undefined
store.searchResources(searchQuery.value, platformId)
try {
if (!store || !process.client) {
console.error('store未初始化或不在客户端')
return
}
const platformId = selectedPlatform.value ? parseInt(selectedPlatform.value) : undefined
store.searchResources(searchQuery.value, platformId).then((data: any) => {
localResources.value = data.resources || []
}).catch((error: any) => {
console.error('搜索失败:', error)
})
} catch (error) {
console.error('搜索处理时出错:', error)
}
}
// 按平台筛选
@@ -445,10 +619,18 @@ const getPlatformIcon = (platformName: string) => {
// 如果找不到对应的平台或没有图标,使用默认图标
const defaultIcons: Record<string, string> = {
'unknown': '<i class="fas fa-question-circle text-gray-400"></i>',
'other': '<i class="fas fa-cloud text-gray-500"></i>'
'other': '<i class="fas fa-cloud text-gray-500"></i>',
'magnet': '<i class="fas fa-magnet text-red-600"></i>',
'uc': '<i class="fas fa-cloud-download-alt text-purple-600"></i>',
'夸克网盘': '<i class="fas fa-cloud text-blue-600"></i>',
'阿里云盘': '<i class="fas fa-cloud text-orange-600"></i>',
'百度网盘': '<i class="fas fa-cloud text-blue-500"></i>',
'天翼云盘': '<i class="fas fa-cloud text-red-500"></i>',
'OneDrive': '<i class="fas fa-cloud text-blue-700"></i>',
'Google Drive': '<i class="fas fa-cloud text-green-600"></i>'
}
return defaultIcons['unknown']
return defaultIcons[platformName] || defaultIcons['unknown']
}
// 获取平台名称
@@ -576,6 +758,50 @@ const handleSaveResource = async (resourceData: any) => {
console.error('保存失败:', error)
}
}
// 虚拟滚动相关方法
const onIntersection = (entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
if (entry.isIntersecting && hasMoreData.value && !isLoadingMore.value) {
loadMore()
}
})
}
const loadMore = async () => {
if (isLoadingMore.value || !hasMoreData.value) return
isLoadingMore.value = true
try {
currentPage.value++
const newResources = await fetchResources(currentPage.value, pageSize.value)
if (newResources && newResources.length > 0) {
visibleResources.value.push(...newResources)
} else {
hasMoreData.value = false
}
} catch (error) {
console.error('加载更多失败:', error)
currentPage.value-- // 回退页码
} finally {
isLoadingMore.value = false
}
}
const fetchResources = async (page: number, size: number) => {
try {
const { useResourceApi } = await import('~/composables/useApi')
const resourceApi = useResourceApi()
const response = await resourceApi.getResources({
page,
page_size: size
}) as any
return response.resources || []
} catch (error) {
console.error('获取资源失败:', error)
return []
}
}
</script>
<style scoped>

348
web/pages/monitor.vue Normal file
View File

@@ -0,0 +1,348 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<div class="container mx-auto px-4 py-8">
<!-- 页面标题 -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">系统性能监控</h1>
<p class="text-gray-600 dark:text-gray-400">实时监控系统运行状态和性能指标</p>
</div>
<!-- 刷新按钮 -->
<div class="mb-6 flex justify-between items-center">
<div class="flex items-center space-x-4">
<button
@click="refreshData"
:disabled="loading"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
>
<i class="fas fa-sync-alt" :class="{ 'fa-spin': loading }"></i>
<span>{{ loading ? '刷新中...' : '刷新数据' }}</span>
</button>
<div class="text-sm text-gray-500 dark:text-gray-400">
最后更新: {{ lastUpdateTime }}
</div>
</div>
<div class="flex items-center space-x-2">
<label class="text-sm text-gray-600 dark:text-gray-400">自动刷新:</label>
<input
v-model="autoRefresh"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span class="text-sm text-gray-500 dark:text-gray-400">{{ autoRefreshInterval }}</span>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="flex justify-center items-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
<!-- 监控数据 -->
<div v-else class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
<!-- 系统信息卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-server mr-2 text-blue-600"></i>
系统信息
</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">运行时间:</span>
<span class="font-medium">{{ systemInfo.uptime || 'N/A' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">启动时间:</span>
<span class="font-medium">{{ systemInfo.start_time || 'N/A' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">版本:</span>
<span class="font-medium">{{ systemInfo.version || 'N/A' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">运行模式:</span>
<span class="font-medium">{{ systemInfo.environment?.gin_mode || 'N/A' }}</span>
</div>
</div>
</div>
<!-- 内存使用卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-memory mr-2 text-green-600"></i>
内存使用
</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">当前分配:</span>
<span class="font-medium">{{ formatBytes(performanceStats.memory?.alloc) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">总分配:</span>
<span class="font-medium">{{ formatBytes(performanceStats.memory?.total_alloc) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">系统内存:</span>
<span class="font-medium">{{ formatBytes(performanceStats.memory?.sys) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">堆内存:</span>
<span class="font-medium">{{ formatBytes(performanceStats.memory?.heap_alloc) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">GC次数:</span>
<span class="font-medium">{{ performanceStats.memory?.num_gc || 0 }}</span>
</div>
</div>
</div>
<!-- 数据库连接卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-database mr-2 text-purple-600"></i>
数据库连接
</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">最大连接数:</span>
<span class="font-medium">{{ performanceStats.database?.max_open_connections || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">当前连接数:</span>
<span class="font-medium">{{ performanceStats.database?.open_connections || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">使用中:</span>
<span class="font-medium">{{ performanceStats.database?.in_use || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">空闲:</span>
<span class="font-medium">{{ performanceStats.database?.idle || 0 }}</span>
</div>
</div>
</div>
<!-- 系统资源卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-microchip mr-2 text-orange-600"></i>
系统资源
</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">CPU核心数:</span>
<span class="font-medium">{{ performanceStats.system?.cpu_count || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Go版本:</span>
<span class="font-medium">{{ performanceStats.system?.go_version || 'N/A' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">协程数:</span>
<span class="font-medium">{{ performanceStats.goroutines || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">时间戳:</span>
<span class="font-medium">{{ formatTimestamp(performanceStats.timestamp) }}</span>
</div>
</div>
</div>
<!-- 基础统计卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-chart-bar mr-2 text-red-600"></i>
基础统计
</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">资源总数:</span>
<span class="font-medium">{{ basicStats.total_resources || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">分类总数:</span>
<span class="font-medium">{{ basicStats.total_categories || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">标签总数:</span>
<span class="font-medium">{{ basicStats.total_tags || 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">总浏览量:</span>
<span class="font-medium">{{ basicStats.total_views || 0 }}</span>
</div>
</div>
</div>
<!-- 性能图表卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-chart-line mr-2 text-indigo-600"></i>
性能趋势
</h3>
<div class="space-y-3">
<div class="text-center py-4">
<div class="text-2xl font-bold text-indigo-600">
{{ formatBytes(performanceStats.memory?.alloc) }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">当前内存使用</div>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-indigo-600 h-2 rounded-full transition-all duration-300"
:style="{ width: memoryUsagePercentage + '%' }"
></div>
</div>
<div class="text-center text-sm text-gray-500 dark:text-gray-400">
内存使用率: {{ memoryUsagePercentage.toFixed(1) }}%
</div>
</div>
</div>
</div>
<!-- 错误提示 -->
<div v-if="error" class="mt-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<div class="flex items-center">
<i class="fas fa-exclamation-triangle mr-2"></i>
<span>{{ error }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
// 响应式数据
const loading = ref(false)
const error = ref('')
const lastUpdateTime = ref('')
const autoRefresh = ref(false)
const autoRefreshInterval = ref(30)
// 监控数据
const systemInfo = ref<any>({})
const performanceStats = ref<any>({})
const basicStats = ref<any>({})
// 计算内存使用率
const memoryUsagePercentage = computed(() => {
const memory = performanceStats.value.memory
if (!memory || !memory.sys) return 0
return (memory.alloc / memory.sys) * 100
})
// 格式化字节数
const formatBytes = (bytes: number) => {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 格式化时间戳
const formatTimestamp = (timestamp: number) => {
if (!timestamp) return 'N/A'
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
// 获取系统信息
const fetchSystemInfo = async () => {
try {
const { useMonitorApi } = await import('~/composables/useApi')
const monitorApi = useMonitorApi()
const response = await monitorApi.getSystemInfo()
systemInfo.value = response
} catch (error) {
console.error('获取系统信息失败:', error)
}
}
// 获取性能统计
const fetchPerformanceStats = async () => {
try {
const { useMonitorApi } = await import('~/composables/useApi')
const monitorApi = useMonitorApi()
const response = await monitorApi.getPerformanceStats()
performanceStats.value = response
} catch (error) {
console.error('获取性能统计失败:', error)
}
}
// 获取基础统计
const fetchBasicStats = async () => {
try {
const { useMonitorApi } = await import('~/composables/useApi')
const monitorApi = useMonitorApi()
const response = await monitorApi.getBasicStats()
basicStats.value = response
} catch (error) {
console.error('获取基础统计失败:', error)
}
}
// 刷新所有数据
const refreshData = async () => {
loading.value = true
error.value = ''
try {
await Promise.all([
fetchSystemInfo(),
fetchPerformanceStats(),
fetchBasicStats()
])
lastUpdateTime.value = new Date().toLocaleString('zh-CN')
} catch (err: any) {
error.value = err.message || '获取监控数据失败'
} finally {
loading.value = false
}
}
// 自动刷新定时器
let autoRefreshTimer: NodeJS.Timeout | null = null
// 监听自动刷新设置
const startAutoRefresh = () => {
if (autoRefresh.value) {
autoRefreshTimer = setInterval(refreshData, autoRefreshInterval.value * 1000)
}
}
const stopAutoRefresh = () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
}
// 监听自动刷新变化
watch(autoRefresh, (newValue) => {
if (newValue) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
})
// 页面加载时获取数据
onMounted(() => {
refreshData()
if (autoRefresh.value) {
startAutoRefresh()
}
})
// 页面卸载时清理定时器
onUnmounted(() => {
stopAutoRefresh()
})
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

View File

@@ -127,6 +127,50 @@
</div>
</div>
<!-- 自动转存 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
自动转存
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动转存资源到其他网盘平台
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoTransferEnabled"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动拉取热播剧 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
自动拉取热播剧
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动从豆瓣获取热播剧信息
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoFetchHotDramaEnabled"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动处理间隔 -->
<div v-if="config.autoProcessReadyResources" class="ml-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
@@ -238,6 +282,8 @@ const config = ref({
// 自动处理配置
autoProcessReadyResources: false,
autoProcessInterval: 30,
autoTransferEnabled: false, // 新增
autoFetchHotDramaEnabled: false, // 新增
// 其他配置
pageSize: 100,
@@ -272,33 +318,23 @@ const loadConfig = async () => {
loading.value = true
const response = await getSystemConfig()
console.log('系统配置响应:', response)
if (response && response.success && response.data) {
// 使用新的统一响应格式直接使用response
if (response) {
config.value = {
siteTitle: response.data.site_title || '网盘资源管理系统',
siteDescription: response.data.site_description || '专业的网盘资源管理系统',
keywords: response.data.keywords || '网盘,资源管理,文件分享',
author: response.data.author || '系统管理员',
copyright: response.data.copyright || '© 2024 网盘资源管理系统',
autoProcessReadyResources: response.data.auto_process_ready_resources || false,
autoProcessInterval: response.data.auto_process_interval || 30,
pageSize: response.data.page_size || 100,
maintenanceMode: response.data.maintenance_mode || false
siteTitle: response.site_title || '网盘资源管理系统',
siteDescription: response.site_description || '专业的网盘资源管理系统',
keywords: response.keywords || '网盘,资源管理,文件分享',
author: response.author || '系统管理员',
copyright: response.copyright || '© 2024 网盘资源管理系统',
autoProcessReadyResources: response.auto_process_ready_resources || false,
autoProcessInterval: response.auto_process_interval || 30,
autoTransferEnabled: response.auto_transfer_enabled || false, // 新增
autoFetchHotDramaEnabled: response.auto_fetch_hot_drama_enabled || false, // 新增
pageSize: response.page_size || 100,
maintenanceMode: response.maintenance_mode || false
}
systemConfig.value = response.data // 更新系统配置状态
} else if (response && response.data) {
// 兼容非标准格式
config.value = {
siteTitle: response.data.site_title || '网盘资源管理系统',
siteDescription: response.data.site_description || '专业的网盘资源管理系统',
keywords: response.data.keywords || '网盘,资源管理,文件分享',
author: response.data.author || '系统管理员',
copyright: response.data.copyright || '© 2024 网盘资源管理系统',
autoProcessReadyResources: response.data.auto_process_ready_resources || false,
autoProcessInterval: response.data.auto_process_interval || 30,
pageSize: response.data.page_size || 100,
maintenanceMode: response.data.maintenance_mode || false
}
systemConfig.value = response.data
systemConfig.value = response // 更新系统配置状态
}
} catch (error) {
console.error('加载配置失败:', error)
@@ -321,19 +357,24 @@ const saveConfig = async () => {
copyright: config.value.copyright,
auto_process_ready_resources: config.value.autoProcessReadyResources,
auto_process_interval: config.value.autoProcessInterval,
auto_transfer_enabled: config.value.autoTransferEnabled, // 新增
auto_fetch_hot_drama_enabled: config.value.autoFetchHotDramaEnabled, // 新增
page_size: config.value.pageSize,
maintenance_mode: config.value.maintenanceMode
}
const response = await updateSystemConfig(requestData)
if (response.success) {
// 使用新的统一响应格式直接检查response是否存在
if (response) {
alert('配置保存成功!')
// 重新加载配置以获取最新数据
await loadConfig()
} else {
alert('保存配置失败:' + (response.message || '未知错误'))
alert('保存配置失败:未知错误')
}
} catch (error) {
console.error('保存配置失败:', error)
alert('保存配置失败,请重试')
alert('保存配置失败' + (error.message || '未知错误'))
} finally {
loading.value = false
}

View File

@@ -70,28 +70,26 @@ export const useUserStore = defineStore('user', {
this.loading = true
try {
const authApi = useAuthApi()
const response = await authApi.login(credentials)
const response = await authApi.login(credentials) as any
console.log('login - 响应:', response)
// 处理标准化的响应格式
if (response.success && response.data) {
const { token, user } = response.data
if (token && user) {
this.token = token
this.user = user
this.isAuthenticated = true
// 保存到localStorage
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
console.log('login - 状态保存成功:', user.username)
console.log('login - localStorage token:', localStorage.getItem('token') ? 'saved' : 'not saved')
console.log('login - localStorage user:', localStorage.getItem('user') ? 'saved' : 'not saved')
return { success: true }
}
// 使用新的统一响应格式直接检查response是否存在
if (response && response.token && response.user) {
const { token, user } = response
this.token = token
this.user = user
this.isAuthenticated = true
// 保存到localStorage
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
console.log('login - 状态保存成功:', user.username)
console.log('login - localStorage token:', localStorage.getItem('token') ? 'saved' : 'not saved')
console.log('login - localStorage user:', localStorage.getItem('user') ? 'saved' : 'not saved')
return { success: true }
}
return { success: false, message: '登录失败,服务器未返回有效数据' }
@@ -154,7 +152,7 @@ export const useUserStore = defineStore('user', {
async fetchProfile() {
try {
const authApi = useAuthApi()
const user = await authApi.getProfile()
const user = await authApi.getProfile() as any
this.user = user
return { success: true }
} catch (error: any) {