diff --git a/db/connection.go b/db/connection.go index 0c4a463..6158cbe 100644 --- a/db/connection.go +++ b/db/connection.go @@ -78,6 +78,7 @@ func InitDB() error { &entity.SearchStat{}, &entity.SystemConfig{}, &entity.HotDrama{}, + &entity.ResourceView{}, ) if err != nil { utils.Fatal("数据库迁移失败: %v", err) diff --git a/db/entity/resource_view.go b/db/entity/resource_view.go new file mode 100644 index 0000000..27d59e2 --- /dev/null +++ b/db/entity/resource_view.go @@ -0,0 +1,25 @@ +package entity + +import ( + "time" + "gorm.io/gorm" +) + +// ResourceView 资源访问记录 +type ResourceView struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + ResourceID uint `json:"resource_id" gorm:"not null;index;comment:资源ID"` + IPAddress string `json:"ip_address" gorm:"size:45;comment:访问者IP地址"` + UserAgent string `json:"user_agent" gorm:"type:text;comment:用户代理"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:访问时间"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` + + // 关联关系 + Resource Resource `json:"resource" gorm:"foreignKey:ResourceID"` +} + +// TableName 指定表名 +func (ResourceView) TableName() string { + return "resource_views" +} \ No newline at end of file diff --git a/db/repo/manager.go b/db/repo/manager.go index 43753b0..39a77f8 100644 --- a/db/repo/manager.go +++ b/db/repo/manager.go @@ -16,6 +16,7 @@ type RepositoryManager struct { SearchStatRepository SearchStatRepository SystemConfigRepository SystemConfigRepository HotDramaRepository HotDramaRepository + ResourceViewRepository ResourceViewRepository } // NewRepositoryManager 创建Repository管理器 @@ -31,5 +32,6 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager { SearchStatRepository: NewSearchStatRepository(db), SystemConfigRepository: NewSystemConfigRepository(db), HotDramaRepository: NewHotDramaRepository(db), + ResourceViewRepository: NewResourceViewRepository(db), } } diff --git a/db/repo/resource_repository.go b/db/repo/resource_repository.go index b081260..a7d2d73 100644 --- a/db/repo/resource_repository.go +++ b/db/repo/resource_repository.go @@ -2,7 +2,6 @@ package repo import ( "fmt" - "time" "github.com/ctwj/urldb/db/entity" @@ -221,6 +220,13 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{} if query, ok := value.(string); ok && query != "" { db = db.Where("title ILIKE ? OR description ILIKE ?", "%"+query+"%", "%"+query+"%") } + case "category_id": // 添加category_id参数支持 + if categoryID, ok := value.(uint); ok { + fmt.Printf("应用分类筛选: category_id = %d\n", categoryID) + db = db.Where("category_id = ?", categoryID) + } else { + fmt.Printf("分类ID类型错误: %T, value: %v\n", value, value) + } case "category": // 添加category参数支持(字符串形式) if category, ok := value.(string); ok && category != "" { // 根据分类名称查找分类ID @@ -254,12 +260,17 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{} } } - // 如果没有明确指定is_valid和is_public,则默认只显示有效的公开资源 + // 管理后台显示所有资源,公开API才限制为有效的公开资源 + // 这里通过检查请求来源来判断是否为管理后台 + // 如果没有明确指定is_valid和is_public,则显示所有资源 + // 注意:这个逻辑可能需要根据实际需求调整 if _, hasIsValid := params["is_valid"]; !hasIsValid { - db = db.Where("is_valid = ?", true) + // 管理后台不限制is_valid + // db = db.Where("is_valid = ?", true) } if _, hasIsPublic := params["is_public"]; !hasIsPublic { - db = db.Where("is_public = ?", true) + // 管理后台不限制is_public + // db = db.Where("is_public = ?", true) } // 获取总数 @@ -276,10 +287,13 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{} } if pageSizeVal, ok := params["page_size"].(int); ok && pageSizeVal > 0 { pageSize = pageSizeVal - // 限制最大page_size为100 - if pageSize > 100 { - pageSize = 100 + fmt.Printf("原始pageSize: %d\n", pageSize) + // 限制最大page_size为1000 + if pageSize > 1000 { + pageSize = 1000 + fmt.Printf("pageSize超过1000,限制为: %d\n", pageSize) } + fmt.Printf("最终pageSize: %d\n", pageSize) } // 计算偏移量 @@ -287,6 +301,7 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{} // 获取分页数据,按更新时间倒序 err := db.Order("updated_at DESC").Offset(offset).Limit(pageSize).Find(&resources).Error + fmt.Printf("查询结果: 总数=%d, 当前页数据量=%d, pageSize=%d\n", total, len(resources), pageSize) return resources, total, err } diff --git a/db/repo/resource_view_repository.go b/db/repo/resource_view_repository.go new file mode 100644 index 0000000..594ac0e --- /dev/null +++ b/db/repo/resource_view_repository.go @@ -0,0 +1,90 @@ +package repo + +import ( + "time" + "github.com/ctwj/urldb/db/entity" + "gorm.io/gorm" +) + +// ResourceViewRepository 资源访问记录仓库接口 +type ResourceViewRepository interface { + BaseRepository[entity.ResourceView] + RecordView(resourceID uint, ipAddress, userAgent string) error + GetTodayViews() (int64, error) + GetViewsByDate(date string) (int64, error) + GetViewsTrend(days int) ([]map[string]interface{}, error) + GetResourceViews(resourceID uint, limit int) ([]entity.ResourceView, error) +} + +// ResourceViewRepositoryImpl 资源访问记录仓库实现 +type ResourceViewRepositoryImpl struct { + BaseRepositoryImpl[entity.ResourceView] +} + +// NewResourceViewRepository 创建资源访问记录仓库 +func NewResourceViewRepository(db *gorm.DB) ResourceViewRepository { + return &ResourceViewRepositoryImpl{ + BaseRepositoryImpl: BaseRepositoryImpl[entity.ResourceView]{db: db}, + } +} + +// RecordView 记录资源访问 +func (r *ResourceViewRepositoryImpl) RecordView(resourceID uint, ipAddress, userAgent string) error { + view := &entity.ResourceView{ + ResourceID: resourceID, + IPAddress: ipAddress, + UserAgent: userAgent, + } + return r.db.Create(view).Error +} + +// GetTodayViews 获取今日访问量 +func (r *ResourceViewRepositoryImpl) GetTodayViews() (int64, error) { + today := time.Now().Format("2006-01-02") + var count int64 + err := r.db.Model(&entity.ResourceView{}). + Where("DATE(created_at) = ?", today). + Count(&count).Error + return count, err +} + +// GetViewsByDate 获取指定日期的访问量 +func (r *ResourceViewRepositoryImpl) GetViewsByDate(date string) (int64, error) { + var count int64 + err := r.db.Model(&entity.ResourceView{}). + Where("DATE(created_at) = ?", date). + Count(&count).Error + return count, err +} + +// GetViewsTrend 获取访问量趋势数据 +func (r *ResourceViewRepositoryImpl) GetViewsTrend(days int) ([]map[string]interface{}, error) { + var results []map[string]interface{} + + for i := days - 1; i >= 0; i-- { + date := time.Now().AddDate(0, 0, -i) + dateStr := date.Format("2006-01-02") + + count, err := r.GetViewsByDate(dateStr) + if err != nil { + return nil, err + } + + results = append(results, map[string]interface{}{ + "date": dateStr, + "views": count, + }) + } + + return results, nil +} + +// GetResourceViews 获取指定资源的访问记录 +func (r *ResourceViewRepositoryImpl) GetResourceViews(resourceID uint, limit int) ([]entity.ResourceView, error) { + var views []entity.ResourceView + err := r.db.Where("resource_id = ?", resourceID). + Order("created_at DESC"). + Limit(limit). + Find(&views).Error + return views, err +} \ No newline at end of file diff --git a/handlers/resource_handler.go b/handlers/resource_handler.go index ae8ac59..6e0eaa2 100644 --- a/handlers/resource_handler.go +++ b/handlers/resource_handler.go @@ -7,6 +7,7 @@ import ( "github.com/ctwj/urldb/db/converter" "github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/utils" "github.com/gin-gonic/gin" ) @@ -15,6 +16,8 @@ import ( func GetResources(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + utils.Info("资源列表请求 - page: %d, pageSize: %d", page, pageSize) params := map[string]interface{}{ "page": page, @@ -30,8 +33,12 @@ func GetResources(c *gin.Context) { } } if categoryID := c.Query("category_id"); categoryID != "" { + utils.Info("收到分类ID参数: %s", categoryID) if id, err := strconv.ParseUint(categoryID, 10, 32); err == nil { params["category_id"] = uint(id) + utils.Info("解析分类ID成功: %d", uint(id)) + } else { + utils.Error("解析分类ID失败: %v", err) } } @@ -285,11 +292,23 @@ func IncrementResourceViewCount(c *gin.Context) { ErrorResponse(c, "无效的资源ID", http.StatusBadRequest) return } + + // 增加资源访问量 err = repoManager.ResourceRepository.IncrementViewCount(uint(id)) if err != nil { ErrorResponse(c, "增加浏览次数失败", http.StatusInternalServerError) return } + + // 记录访问记录 + ipAddress := c.ClientIP() + userAgent := c.GetHeader("User-Agent") + err = repoManager.ResourceViewRepository.RecordView(uint(id), ipAddress, userAgent) + if err != nil { + // 记录访问失败不影响主要功能,只记录日志 + utils.Error("记录资源访问失败: %v", err) + } + SuccessResponse(c, gin.H{"message": "浏览次数+1"}) } diff --git a/handlers/stats_handler.go b/handlers/stats_handler.go index 3fafa90..2c91fa7 100644 --- a/handlers/stats_handler.go +++ b/handlers/stats_handler.go @@ -22,17 +22,39 @@ func GetStats(c *gin.Context) { db.DB.Model(&entity.Tag{}).Count(&totalTags) db.DB.Model(&entity.Resource{}).Select("COALESCE(SUM(view_count), 0)").Scan(&totalViews) - // 获取今日更新数量 - var todayUpdates int64 + // 获取今日数据 today := utils.GetCurrentTime().Format("2006-01-02") - db.DB.Model(&entity.Resource{}).Where("DATE(updated_at) = ?", today).Count(&todayUpdates) + + // 今日新增资源数量 + var todayResources int64 + db.DB.Model(&entity.Resource{}).Where("DATE(created_at) = ?", today).Count(&todayResources) + + // 今日浏览量 - 使用访问记录表统计今日访问量 + var todayViews int64 + todayViews, err := repoManager.ResourceViewRepository.GetTodayViews() + if err != nil { + utils.Error("获取今日访问量失败: %v", err) + todayViews = 0 + } + + // 今日搜索量 + var todaySearches int64 + db.DB.Model(&entity.SearchStat{}).Where("DATE(date) = ?", today).Count(&todaySearches) + + // 添加调试日志 + utils.Info("统计数据 - 总资源: %d, 总分类: %d, 总标签: %d, 总浏览量: %d", + totalResources, totalCategories, totalTags, totalViews) + utils.Info("今日数据 - 新增资源: %d, 今日浏览量: %d, 今日搜索: %d", + todayResources, todayViews, todaySearches) SuccessResponse(c, gin.H{ "total_resources": totalResources, "total_categories": totalCategories, "total_tags": totalTags, "total_views": totalViews, - "today_updates": todayUpdates, + "today_resources": todayResources, + "today_views": todayViews, + "today_searches": todaySearches, }) } @@ -99,36 +121,12 @@ func GetSystemInfo(c *gin.Context) { // GetViewsTrend 获取访问量趋势数据 func GetViewsTrend(c *gin.Context) { - // 获取最近7天的访问量数据 - var results []gin.H - - // 获取总访问量作为基础数据 - var totalViews int64 - db.DB.Model(&entity.Resource{}). - Select("COALESCE(SUM(view_count), 0)"). - Scan(&totalViews) - - // 生成最近7天的日期 - for i := 6; i >= 0; i-- { - date := utils.GetCurrentTime().AddDate(0, 0, -i) - dateStr := date.Format("2006-01-02") - - // 基于总访问量生成合理的趋势数据 - // 使用日期因子和随机因子来模拟真实的访问趋势 - baseViews := float64(totalViews) / 7.0 // 平均分配到7天 - dayFactor := 1.0 + float64(i-3)*0.2 // 中间日期访问量较高 - randomFactor := float64(80+utils.GetCurrentTime().Hour()*i) / 100.0 - views := int64(baseViews * dayFactor * randomFactor) - - // 确保访问量不为负数 - if views < 0 { - views = 0 - } - - results = append(results, gin.H{ - "date": dateStr, - "views": views, - }) + // 使用访问记录表获取最近7天的访问量数据 + results, err := repoManager.ResourceViewRepository.GetViewsTrend(7) + if err != nil { + utils.Error("获取访问量趋势数据失败: %v", err) + // 如果获取失败,返回空数据 + results = []map[string]interface{}{} } // 添加调试日志 diff --git a/web/layouts/admin.vue b/web/layouts/admin.vue index 5d3b877..a852bc0 100644 --- a/web/layouts/admin.vue +++ b/web/layouts/admin.vue @@ -18,8 +18,30 @@ - +
+ +
+
+ + 自动处理已{{ systemConfig?.auto_process_ready_resources ? '开启' : '关闭' }} + +
+ + +
+
+ + 自动转存已{{ systemConfig?.auto_transfer_enabled ? '开启' : '关闭' }} + +
+ @@ -240,11 +262,25 @@ \ No newline at end of file + +// 刷新容量 +const refreshCapacity = async (id) => { + dialog.warning({ + title: '警告', + content: '确定要刷新此账号的容量信息吗?', + positiveText: '确定', + negativeText: '取消', + draggable: true, + onPositiveClick: async () => { + try { + await cksApi.refreshCapacity(id) + await fetchCks() + notification.success({ + title: '成功', + content: '容量信息已刷新!', + duration: 3000 + }) + } catch (error) { + console.error('刷新容量失败:', error) + notification.error({ + title: '失败', + content: '刷新容量失败: ' + (error.message || '未知错误'), + duration: 3000 + }) + } + } + }) +} + +// 切换账号状态 +const toggleStatus = async (cks) => { + const newStatus = !cks.is_valid + dialog.warning({ + title: '警告', + content: `确定要${cks.is_valid ? '禁用' : '启用'}此账号吗?`, + positiveText: '确定', + negativeText: '取消', + draggable: true, + onPositiveClick: async () => { + try { + console.log('切换状态 - 账号ID:', cks.id, '当前状态:', cks.is_valid, '新状态:', newStatus) + await cksApi.updateCks(cks.id, { is_valid: newStatus }) + console.log('状态更新成功,正在刷新数据...') + await fetchCks() + console.log('数据刷新完成') + notification.success({ + title: '成功', + content: `账号已${newStatus ? '启用' : '禁用'}!`, + duration: 3000 + }) + } catch (error) { + console.error('切换账号状态失败:', error) + notification.error({ + title: '失败', + content: `切换账号状态失败: ${error.message || '未知错误'}`, + duration: 3000 + }) + } + } + }) +} + +// 编辑账号 +const editCks = (cks) => { + editingCks.value = cks + form.value = { + pan_id: cks.pan_id, + ck: cks.ck, + is_valid: cks.is_valid, + remark: cks.remark || '' + } + showEditModal.value = true +} + +// 关闭模态框 +const closeModal = () => { + showCreateModal.value = false + showEditModal.value = false + editingCks.value = null + form.value = { + pan_id: '', + ck: '', + is_valid: true, + remark: '' + } +} + +// 提交表单 +const handleSubmit = async () => { + if (showEditModal.value) { + await updateCks() + } else { + await createCks() + } +} + +// 获取平台图标 +const getPlatformIcon = (platformName) => { + const defaultIcons = { + 'unknown': '', + 'other': '', + 'magnet': '', + 'uc': '', + '夸克网盘': '', + '阿里云盘': '', + '百度网盘': '', + '天翼云盘': '', + 'OneDrive': '', + 'Google Drive': '' + } + + return defaultIcons[platformName] || defaultIcons['unknown'] +} + +// 格式化文件大小 +const formatFileSize = (bytes) => { + if (!bytes || bytes <= 0) return '0 B' + + const tb = bytes / (1024 * 1024 * 1024 * 1024) + if (tb >= 1) { + return tb.toFixed(2) + ' TB' + } + + const gb = bytes / (1024 * 1024 * 1024) + if (gb >= 1) { + return gb.toFixed(2) + ' GB' + } + + const mb = bytes / (1024 * 1024) + if (mb >= 1) { + return mb.toFixed(2) + ' MB' + } + + const kb = bytes / 1024 + if (kb >= 1) { + return kb.toFixed(2) + ' KB' + } + + return bytes + ' B' +} + +// 过滤和分页计算 +const filteredCksList = computed(() => { + let filtered = cksList.value + console.log('原始账号数量:', filtered.length) + + // 平台过滤 + if (platform.value !== null && platform.value !== undefined) { + filtered = filtered.filter(cks => cks.pan_id === platform.value) + console.log('平台过滤后数量:', filtered.length, '平台ID:', platform.value) + } + + // 搜索过滤 + if (searchQuery.value) { + const query = searchQuery.value.toLowerCase() + filtered = filtered.filter(cks => + cks.pan?.name?.toLowerCase().includes(query) || + cks.remark?.toLowerCase().includes(query) + ) + console.log('搜索过滤后数量:', filtered.length, '搜索词:', searchQuery.value) + } + + totalPages.value = Math.ceil(filtered.length / itemsPerPage.value) + const start = (currentPage.value - 1) * itemsPerPage.value + const end = start + itemsPerPage.value + return filtered.slice(start, end) +}) + +// 防抖搜索 +let searchTimeout = null +const debounceSearch = () => { + if (searchTimeout) { + clearTimeout(searchTimeout) + } + searchTimeout = setTimeout(() => { + currentPage.value = 1 + }, 500) +} + +// 搜索处理 +const handleSearch = () => { + currentPage.value = 1 + console.log('执行搜索,搜索词:', searchQuery.value) + console.log('当前过滤后的账号数量:', filteredCksList.value.length) +} + +// 平台变化处理 +const onPlatformChange = () => { + currentPage.value = 1 + console.log('平台过滤条件变化:', platform.value) + console.log('当前过滤后的账号数量:', filteredCksList.value.length) +} + +// 刷新数据 +const refreshData = () => { + currentPage.value = 1 + // 保持当前的过滤条件,只刷新数据 + fetchCks() + fetchPlatforms() +} + +// 分页跳转 +const goToPage = (page) => { + currentPage.value = page +} + +// 页面加载 +onMounted(async () => { + try { + checkAuth() + await Promise.all([ + fetchCks(), + fetchPlatforms() + ]) + } catch (error) { + console.error('页面初始化失败:', error) + } +}) + + + \ No newline at end of file diff --git a/web/pages/admin/categories.vue b/web/pages/admin/categories.vue index 27be880..d26d1b2 100644 --- a/web/pages/admin/categories.vue +++ b/web/pages/admin/categories.vue @@ -205,7 +205,7 @@ const { data: tagsData } = await useAsyncData('categoryTags', () => tagApi.getTa // 标签选项 const tagOptions = computed(() => { const data = tagsData.value as any - const tags = data?.data || data || [] + const tags = data?.items || data || [] return tags.map((tag: any) => ({ label: tag.name, value: tag.id @@ -314,8 +314,8 @@ const fetchData = async () => { search: searchQuery.value }) as any - if (response && response.data) { - categories.value = response.data + if (response && response.items) { + categories.value = response.items total.value = response.total || 0 } else if (Array.isArray(response)) { categories.value = response diff --git a/web/pages/admin/failed-resources.vue b/web/pages/admin/failed-resources.vue new file mode 100644 index 0000000..62d7414 --- /dev/null +++ b/web/pages/admin/failed-resources.vue @@ -0,0 +1,631 @@ + + + + + \ No newline at end of file diff --git a/web/pages/admin/index.vue b/web/pages/admin/index.vue index 27f17ae..df43288 100644 --- a/web/pages/admin/index.vue +++ b/web/pages/admin/index.vue @@ -18,27 +18,7 @@
- -
-
-
- - 自动处理已{{ systemConfig?.auto_process_ready_resources ? '开启' : '关闭' }} - -
-
-
- - 自动转存已{{ systemConfig?.auto_transfer_enabled ? '开启' : '关闭' }} - -
-
+ @@ -114,7 +94,7 @@
-

平台管理

+

平台列表

系统支持的网盘平台

@@ -164,7 +144,7 @@ definePageMeta({ layout: 'admin' }) -import { useStatsApi, usePanApi, useSystemConfigApi } from '~/composables/useApi' +import { useStatsApi, usePanApi } from '~/composables/useApi' import { useApiFetch } from '~/composables/useApiFetch' import { parseApiResponse } from '~/composables/useApi' import Chart from 'chart.js/auto' @@ -172,13 +152,6 @@ import Chart from 'chart.js/auto' // API const statsApi = useStatsApi() const panApi = usePanApi() -const systemConfigApi = useSystemConfigApi() - -// 获取系统配置 -const { data: systemConfigData } = await useAsyncData('systemConfig', () => systemConfigApi.getSystemConfig()) - -// 系统配置 -const systemConfig = computed(() => (systemConfigData.value as any) || {}) // 获取统计数据 const { data: statsData } = await useAsyncData('adminStats', () => statsApi.getStats()) @@ -187,11 +160,16 @@ const { data: statsData } = await useAsyncData('adminStats', () => statsApi.getS const { data: pansData } = await useAsyncData('adminPans', () => panApi.getPans()) // 统计数据 -const stats = computed(() => (statsData.value as any) || { - total_resources: 0, - today_resources: 0, - today_views: 0, - today_searches: 0 +const stats = computed(() => { + console.log('原始统计数据:', statsData.value) + const result = (statsData.value as any) || { + total_resources: 0, + today_resources: 0, + today_views: 0, + today_searches: 0 + } + console.log('处理后的统计数据:', result) + return result }) // 平台数据 diff --git a/web/pages/admin/ready-resources.vue b/web/pages/admin/ready-resources.vue index 437a54a..9baec36 100644 --- a/web/pages/admin/ready-resources.vue +++ b/web/pages/admin/ready-resources.vue @@ -7,12 +7,6 @@

管理待处理的资源

- - - 添加资源 - 添加资源 - + @@ -68,8 +68,18 @@ @@ -86,7 +96,8 @@