From 5bd21e156df70f0a7337ba67226dfabcca022418 Mon Sep 17 00:00:00 2001 From: ctwj <908504609@qq.com> Date: Sun, 3 Aug 2025 22:40:22 +0800 Subject: [PATCH] =?UTF-8?q?update:=20=E5=AE=8C=E5=96=84=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E8=B5=84=E6=BA=90=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/pan_factory.go | 25 +-- db/converter/converter.go | 10 + db/dto/response.go | 28 +-- db/repo/ready_resource_repository.go | 83 ++++++++- handlers/ready_resource_handler.go | 111 ++++++++++- main.go | 3 + utils/scheduler.go | 2 +- web/components/AdminHeader.vue | 3 +- web/composables/useApi.ts | 20 +- web/composables/useApiFetch.ts | 7 + web/pages/admin/failed-resources.vue | 268 ++++++++++++++++++++------- web/pages/admin/ready-resources.vue | 6 + web/pages/login.vue | 23 ++- 13 files changed, 484 insertions(+), 105 deletions(-) diff --git a/common/pan_factory.go b/common/pan_factory.go index d9da4d4..2ecda08 100644 --- a/common/pan_factory.go +++ b/common/pan_factory.go @@ -222,23 +222,24 @@ func ExtractShareId(url string) (string, ServiceType) { // 提取分享ID shareID := "" - substring := strings.Index(url, "/s/") - if substring == -1 { - substring = strings.Index(url, "/t/") // 天翼云 是 t - shareID = url[substring+3:] - } - if substring == -1 { - substring = strings.Index(url, "/web/share?code=") // 天翼云 带密码 - shareID = url[substring+11:] - } - if substring == -1 { - substring = strings.Index(url, "/p/") // 天翼云 是 p - shareID = url[substring+3:] + substring := -1 + + if index := strings.Index(url, "/s/"); index != -1 { + substring = index + 3 + } else if index := strings.Index(url, "/t/"); index != -1 { + substring = index + 3 + } else if index := strings.Index(url, "/web/share?code="); index != -1 { + substring = index + 16 + } else if index := strings.Index(url, "/p/"); index != -1 { + substring = index + 3 } + if substring == -1 { return "", NotFound } + shareID = url[substring:] + // 去除可能的锚点 if hashIndex := strings.Index(shareID, "#"); hashIndex != -1 { shareID = shareID[:hashIndex] diff --git a/db/converter/converter.go b/db/converter/converter.go index 25d2f72..7268119 100644 --- a/db/converter/converter.go +++ b/db/converter/converter.go @@ -1,6 +1,8 @@ package converter import ( + "time" + "github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/entity" ) @@ -169,6 +171,12 @@ func ToCksResponseList(cksList []entity.Cks) []dto.CksResponse { // ToReadyResourceResponse 将ReadyResource实体转换为ReadyResourceResponse func ToReadyResourceResponse(resource *entity.ReadyResource) dto.ReadyResourceResponse { + isDeleted := !resource.DeletedAt.Time.IsZero() + var deletedAt *time.Time + if isDeleted { + deletedAt = &resource.DeletedAt.Time + } + return dto.ReadyResourceResponse{ ID: resource.ID, Title: resource.Title, @@ -183,6 +191,8 @@ func ToReadyResourceResponse(resource *entity.ReadyResource) dto.ReadyResourceRe ErrorMsg: resource.ErrorMsg, CreateTime: resource.CreateTime, IP: resource.IP, + DeletedAt: deletedAt, + IsDeleted: isDeleted, } } diff --git a/db/dto/response.go b/db/dto/response.go index 8669d96..d7d3ce1 100644 --- a/db/dto/response.go +++ b/db/dto/response.go @@ -79,19 +79,21 @@ type CksResponse struct { // ReadyResourceResponse 待处理资源响应 type ReadyResourceResponse struct { - ID uint `json:"id"` - Title *string `json:"title"` - Description string `json:"description"` - URL string `json:"url"` - Category string `json:"category"` - Tags string `json:"tags"` - Img string `json:"img"` - Source string `json:"source"` - Extra string `json:"extra"` - Key string `json:"key"` - ErrorMsg string `json:"error_msg"` - CreateTime time.Time `json:"create_time"` - IP *string `json:"ip"` + ID uint `json:"id"` + Title *string `json:"title"` + Description string `json:"description"` + URL string `json:"url"` + Category string `json:"category"` + Tags string `json:"tags"` + Img string `json:"img"` + Source string `json:"source"` + Extra string `json:"extra"` + Key string `json:"key"` + ErrorMsg string `json:"error_msg"` + CreateTime time.Time `json:"create_time"` + IP *string `json:"ip"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` + IsDeleted bool `json:"is_deleted"` } // Stats 统计信息 diff --git a/db/repo/ready_resource_repository.go b/db/repo/ready_resource_repository.go index 371e89a..679655d 100644 --- a/db/repo/ready_resource_repository.go +++ b/db/repo/ready_resource_repository.go @@ -23,8 +23,13 @@ type ReadyResourceRepository interface { GenerateUniqueKey() (string, error) FindWithErrors() ([]entity.ReadyResource, error) FindWithErrorsPaginated(page, limit int) ([]entity.ReadyResource, int64, error) + FindWithErrorsIncludingDeleted() ([]entity.ReadyResource, error) + FindWithErrorsPaginatedIncludingDeleted(page, limit int, errorFilter string) ([]entity.ReadyResource, int64, error) + FindWithErrorsByQuery(errorFilter string) ([]entity.ReadyResource, error) FindWithoutErrors() ([]entity.ReadyResource, error) ClearErrorMsg(id uint) error + ClearErrorMsgAndRestore(id uint) error + ClearAllErrorsByQuery(errorFilter string) (int64, error) // 批量清除错误信息并真正删除资源 } // ReadyResourceRepositoryImpl ReadyResource的Repository实现 @@ -118,20 +123,20 @@ func (r *ReadyResourceRepositoryImpl) GenerateUniqueKey() (string, error) { return "", gorm.ErrInvalidData } -// FindWithErrors 查找有错误信息的资源(deleted_at为空且存在error_msg) +// FindWithErrors 查找有错误信息的资源(包括软删除的) func (r *ReadyResourceRepositoryImpl) FindWithErrors() ([]entity.ReadyResource, error) { var resources []entity.ReadyResource - err := r.db.Where("deleted_at IS NULL AND error_msg != '' AND error_msg IS NOT NULL").Find(&resources).Error + err := r.db.Unscoped().Where("error_msg != '' AND error_msg IS NOT NULL").Find(&resources).Error return resources, err } -// FindWithErrorsPaginated 分页查找有错误信息的资源(deleted_at为空且存在error_msg) +// FindWithErrorsPaginated 分页查找有错误信息的资源(包括软删除的) func (r *ReadyResourceRepositoryImpl) FindWithErrorsPaginated(page, limit int) ([]entity.ReadyResource, int64, error) { var resources []entity.ReadyResource var total int64 offset := (page - 1) * limit - db := r.db.Model(&entity.ReadyResource{}).Where("deleted_at IS NULL AND error_msg != '' AND error_msg IS NOT NULL") + db := r.db.Model(&entity.ReadyResource{}).Unscoped().Where("error_msg != '' AND error_msg IS NOT NULL") // 获取总数 if err := db.Count(&total).Error; err != nil { @@ -150,7 +155,75 @@ func (r *ReadyResourceRepositoryImpl) FindWithoutErrors() ([]entity.ReadyResourc return resources, err } +// FindWithErrorsIncludingDeleted 查找有错误信息的资源(包括软删除的,用于管理页面) +func (r *ReadyResourceRepositoryImpl) FindWithErrorsIncludingDeleted() ([]entity.ReadyResource, error) { + var resources []entity.ReadyResource + err := r.db.Unscoped().Where("error_msg != '' AND error_msg IS NOT NULL").Find(&resources).Error + return resources, err +} + +// FindWithErrorsPaginatedIncludingDeleted 分页查找有错误信息的资源(包括软删除的,用于管理页面) +func (r *ReadyResourceRepositoryImpl) FindWithErrorsPaginatedIncludingDeleted(page, limit int, errorFilter string) ([]entity.ReadyResource, int64, error) { + var resources []entity.ReadyResource + var total int64 + + offset := (page - 1) * limit + db := r.db.Model(&entity.ReadyResource{}).Unscoped().Where("error_msg != '' AND error_msg IS NOT NULL") + + // 如果有错误过滤条件,添加到查询中 + if errorFilter != "" { + db = db.Where("error_msg ILIKE ?", "%"+errorFilter+"%") + } + + // 获取总数 + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 获取分页数据 + err := db.Offset(offset).Limit(limit).Order("created_at DESC").Find(&resources).Error + return resources, total, err +} + // ClearErrorMsg 清除指定资源的错误信息 func (r *ReadyResourceRepositoryImpl) ClearErrorMsg(id uint) error { - return r.db.Model(&entity.ReadyResource{}).Where("id = ?", id).Update("error_msg", "").Error + return r.db.Model(&entity.ReadyResource{}).Where("id = ?", id).Update("error_msg", "").Update("deleted_at", nil).Error +} + +// ClearErrorMsgAndRestore 清除错误信息并恢复软删除的资源 +func (r *ReadyResourceRepositoryImpl) ClearErrorMsgAndRestore(id uint) error { + return r.db.Unscoped().Model(&entity.ReadyResource{}).Where("id = ?", id).Updates(map[string]interface{}{ + "error_msg": "", + "deleted_at": nil, + }).Error +} + +// FindWithErrorsByQuery 根据查询条件查找有错误信息的资源(不分页,用于批量操作) +func (r *ReadyResourceRepositoryImpl) FindWithErrorsByQuery(errorFilter string) ([]entity.ReadyResource, error) { + var resources []entity.ReadyResource + + db := r.db.Model(&entity.ReadyResource{}).Unscoped().Where("error_msg != '' AND error_msg IS NOT NULL") + + // 如果有错误过滤条件,添加到查询中 + if errorFilter != "" { + db = db.Where("error_msg ILIKE ?", "%"+errorFilter+"%") + } + + err := db.Order("created_at DESC").Find(&resources).Error + return resources, err +} + +// ClearAllErrorsByQuery 根据查询条件批量清除错误信息并真正删除资源 +func (r *ReadyResourceRepositoryImpl) ClearAllErrorsByQuery(errorFilter string) (int64, error) { + db := r.db.Unscoped().Model(&entity.ReadyResource{}).Where("error_msg != '' AND error_msg IS NOT NULL") + + // 如果有错误过滤条件,添加到查询中 + if errorFilter != "" { + db = db.Where("error_msg ILIKE ?", "%"+errorFilter+"%") + } + + // 真正删除资源(物理删除) + result := db.Delete(&entity.ReadyResource{}) + + return result.RowsAffected, result.Error } diff --git a/handlers/ready_resource_handler.go b/handlers/ready_resource_handler.go index 9be8e8b..6842a9f 100644 --- a/handlers/ready_resource_handler.go +++ b/handlers/ready_resource_handler.go @@ -316,6 +316,7 @@ func GetReadyResourcesWithErrors(c *gin.Context) { // 获取分页参数 pageStr := c.DefaultQuery("page", "1") pageSizeStr := c.DefaultQuery("page_size", "100") + errorFilter := c.Query("error_filter") page, err := strconv.Atoi(pageStr) if err != nil || page < 1 { @@ -327,8 +328,8 @@ func GetReadyResourcesWithErrors(c *gin.Context) { pageSize = 100 } - // 获取有错误的资源(分页) - resources, total, err := repoManager.ReadyResourceRepository.FindWithErrorsPaginated(page, pageSize) + // 获取有错误的资源(分页,包括软删除的) + resources, total, err := repoManager.ReadyResourceRepository.FindWithErrorsPaginatedIncludingDeleted(page, pageSize, errorFilter) if err != nil { ErrorResponse(c, err.Error(), http.StatusInternalServerError) return @@ -465,3 +466,109 @@ func RetryFailedResources(c *gin.Context) { "retryable_count": getRetryableErrorCount(resources), }) } + +// BatchRestoreToReadyPool 批量将失败资源重新放入待处理池 +func BatchRestoreToReadyPool(c *gin.Context) { + var req struct { + IDs []uint `json:"ids" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "请求参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + if len(req.IDs) == 0 { + ErrorResponse(c, "资源ID列表不能为空", http.StatusBadRequest) + return + } + + successCount := 0 + failedCount := 0 + + for _, id := range req.IDs { + // 清除错误信息并恢复软删除的资源 + err := repoManager.ReadyResourceRepository.ClearErrorMsgAndRestore(id) + if err != nil { + failedCount++ + continue + } + successCount++ + } + + SuccessResponse(c, gin.H{ + "message": "批量重新放入待处理池操作完成", + "total_count": len(req.IDs), + "success_count": successCount, + "failed_count": failedCount, + }) +} + +// BatchRestoreToReadyPoolByQuery 根据查询条件批量将失败资源重新放入待处理池 +func BatchRestoreToReadyPoolByQuery(c *gin.Context) { + var req struct { + ErrorFilter string `json:"error_filter"` + } + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "请求参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 根据查询条件获取所有符合条件的资源 + resources, err := repoManager.ReadyResourceRepository.FindWithErrorsByQuery(req.ErrorFilter) + if err != nil { + ErrorResponse(c, "查询资源失败: "+err.Error(), http.StatusInternalServerError) + return + } + + if len(resources) == 0 { + SuccessResponse(c, gin.H{ + "message": "没有找到符合条件的资源", + "total_count": 0, + "success_count": 0, + "failed_count": 0, + }) + return + } + + successCount := 0 + failedCount := 0 + for _, resource := range resources { + err := repoManager.ReadyResourceRepository.ClearErrorMsgAndRestore(resource.ID) + if err != nil { + failedCount++ + continue + } + successCount++ + } + + SuccessResponse(c, gin.H{ + "message": "批量重新放入待处理池操作完成", + "total_count": len(resources), + "success_count": successCount, + "failed_count": failedCount, + }) +} + +// ClearAllErrorsByQuery 根据查询条件批量清除错误信息并删除资源 +func ClearAllErrorsByQuery(c *gin.Context) { + var req struct { + ErrorFilter string `json:"error_filter"` + } + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "请求参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 根据查询条件批量删除失败资源 + affectedRows, err := repoManager.ReadyResourceRepository.ClearAllErrorsByQuery(req.ErrorFilter) + if err != nil { + ErrorResponse(c, "批量删除失败资源失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, gin.H{ + "message": "批量删除失败资源操作完成", + "affected_rows": affectedRows, + }) +} diff --git a/main.go b/main.go index 209c857..4d66ae6 100644 --- a/main.go +++ b/main.go @@ -217,6 +217,9 @@ func main() { api.GET("/ready-resources/errors", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResourcesWithErrors) api.POST("/ready-resources/:id/clear-error", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearErrorMsg) api.POST("/ready-resources/retry-failed", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.RetryFailedResources) + api.POST("/ready-resources/batch-restore", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchRestoreToReadyPool) + api.POST("/ready-resources/batch-restore-by-query", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchRestoreToReadyPoolByQuery) + api.POST("/ready-resources/clear-all-errors-by-query", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearAllErrorsByQuery) // 用户管理(仅管理员) api.GET("/users", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetUsers) diff --git a/utils/scheduler.go b/utils/scheduler.go index ba5bbc2..49a2c90 100644 --- a/utils/scheduler.go +++ b/utils/scheduler.go @@ -462,7 +462,7 @@ func (s *Scheduler) convertReadyResourceToResource(readyResource entity.ReadyRes return NewNoAccountError(serviceType.String()) } - // 选择第一个有效的账号 + // 选择第一个有效的账号 TODO 需要优化,随机选择账号 var selectedAccount *entity.Cks for _, account := range accounts { if account.IsValid { diff --git a/web/components/AdminHeader.vue b/web/components/AdminHeader.vue index dca899f..4ab1437 100644 --- a/web/components/AdminHeader.vue +++ b/web/components/AdminHeader.vue @@ -125,7 +125,8 @@ const pageConfig = computed(() => { '/monitor': { title: '系统监控', icon: 'fas fa-desktop', description: '系统性能监控' }, '/admin/add-resource': { title: '添加资源', icon: 'fas fa-plus', description: '添加新资源' }, '/api-docs': { title: 'API文档', icon: 'fas fa-book', description: '接口文档说明' }, - '/admin/version': { title: '版本信息', icon: 'fas fa-code-branch', description: '系统版本详情' } + '/admin/version': { title: '版本信息', icon: 'fas fa-code-branch', description: '系统版本详情' }, + '/admin/failed-resources': { title: '错误资源', icon: 'fas fa-code-branch', description: '错误资源' } } return configs[route.path] || { title: props.title, icon: 'fas fa-cog', description: '管理页面' } }) diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index 2941d63..2901f94 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -35,6 +35,18 @@ export const parseApiResponse = (response: any): T => { page_size: response.data.limit } as T } + // 特殊处理失败资源接口,返回完整的data结构 + if (response.data && response.data.data && Array.isArray(response.data.data) && response.data.total !== undefined) { + return response.data + } + // 特殊处理登录接口,直接返回data部分(包含token和user) + if (response.data && response.data.token && response.data.user) { + return response.data + } + // 特殊处理删除操作响应,直接返回data部分 + if (response.data && response.data.affected_rows !== undefined) { + return response.data + } return response.data } else { throw new Error(response.message || '请求失败') @@ -122,6 +134,9 @@ export const useReadyResourceApi = () => { const clearReadyResources = () => useApiFetch('/ready-resources', { method: 'DELETE' }).then(parseApiResponse) const clearErrorMsg = (id: number) => useApiFetch(`/ready-resources/${id}/clear-error`, { method: 'POST' }).then(parseApiResponse) const retryFailedResources = () => useApiFetch('/ready-resources/retry-failed', { method: 'POST' }).then(parseApiResponse) + const batchRestoreToReadyPool = (ids: number[]) => useApiFetch('/ready-resources/batch-restore', { method: 'POST', body: { ids } }).then(parseApiResponse) + const batchRestoreToReadyPoolByQuery = (queryParams: any) => useApiFetch('/ready-resources/batch-restore-by-query', { method: 'POST', body: queryParams }).then(parseApiResponse) + const clearAllErrorsByQuery = (queryParams: any) => useApiFetch('/ready-resources/clear-all-errors-by-query', { method: 'POST', body: queryParams }).then(parseApiResponse) return { getReadyResources, getFailedResources, @@ -131,7 +146,10 @@ export const useReadyResourceApi = () => { deleteReadyResource, clearReadyResources, clearErrorMsg, - retryFailedResources + retryFailedResources, + batchRestoreToReadyPool, + batchRestoreToReadyPoolByQuery, + clearAllErrorsByQuery } } diff --git a/web/composables/useApiFetch.ts b/web/composables/useApiFetch.ts index e501ab8..bb67179 100644 --- a/web/composables/useApiFetch.ts +++ b/web/composables/useApiFetch.ts @@ -22,6 +22,12 @@ export function useApiFetch( ...options, headers, onResponse({ response }) { + console.log('API响应:', { + status: response.status, + data: response._data, + url: url + }) + if (response.status === 401 || (response._data && (response._data.code === 401 || response._data.error === '无效的令牌')) ) { @@ -38,6 +44,7 @@ export function useApiFetch( // 统一处理 code/message if (response._data && response._data.code && response._data.code !== 200) { + console.error('API错误响应:', response._data) throw new Error(response._data.message || '请求失败') } }, diff --git a/web/pages/admin/failed-resources.vue b/web/pages/admin/failed-resources.vue index bb0c3f7..ba5fcf8 100644 --- a/web/pages/admin/failed-resources.vue +++ b/web/pages/admin/failed-resources.vue @@ -25,18 +25,53 @@
-
+
+ +
+ + +
- -
-

错误类型统计

-
-
-
{{ count }}
-
{{ getErrorTypeName(type) }}
-
-
-
+
@@ -68,6 +90,7 @@ ID + 状态 标题 URL 错误信息 @@ -78,12 +101,12 @@ - + 加载中... - +
@@ -97,11 +120,30 @@
{{ resource.id }} + +
+ 已删除 + + + 正常 + + - {{ escapeHtml(resource.title) }} + {{ escapeHtml(resource.title) }} 未设置 @@ -168,6 +210,9 @@
{{ totalCount }} 个失败资源 + + (已过滤) +
@@ -216,6 +261,9 @@
{{ totalCount }} 个失败资源 + + (已过滤) +
@@ -232,11 +280,13 @@ definePageMeta({ interface FailedResource { id: number - title?: string + title?: string | null url: string error_msg: string create_time: string - ip?: string + ip?: string | null + deleted_at?: string | null + is_deleted: boolean } const failedResources = ref([]) @@ -249,10 +299,12 @@ const pageSize = ref(100) const totalCount = ref(0) const totalPages = ref(0) -// 错误统计 -const errorStats = ref>({}) + const dialog = useDialog() +// 过滤相关状态 +const errorFilter = ref('') + // 获取失败资源API import { useReadyResourceApi } from '~/composables/useApi' const readyResourceApi = useReadyResourceApi() @@ -261,28 +313,49 @@ const readyResourceApi = useReadyResourceApi() const fetchData = async () => { loading.value = true try { - const response = await readyResourceApi.getFailedResources({ + const params: any = { page: currentPage.value, page_size: pageSize.value - }) as any + } - if (response && response.data) { + // 如果有过滤条件,添加到查询参数中 + if (errorFilter.value.trim()) { + params.error_filter = errorFilter.value.trim() + } + + console.log('fetchData - 开始获取失败资源,参数:', params) + + const response = await readyResourceApi.getFailedResources(params) as any + + console.log('fetchData - 原始响应:', response) + + if (response && response.data && Array.isArray(response.data)) { + console.log('fetchData - 使用response.data格式(数组)') failedResources.value = response.data totalCount.value = response.total || 0 totalPages.value = Math.ceil((response.total || 0) / pageSize.value) - errorStats.value = response.error_stats || {} } else { + console.log('fetchData - 使用空数据格式') failedResources.value = [] totalCount.value = 0 totalPages.value = 1 - errorStats.value = {} + } + + console.log('fetchData - 处理后的数据:', { + failedResourcesCount: failedResources.value.length, + totalCount: totalCount.value, + totalPages: totalPages.value + }) + + // 打印第一个资源的数据结构(如果存在) + if (failedResources.value.length > 0) { + console.log('fetchData - 第一个资源的数据结构:', failedResources.value[0]) } } catch (error) { console.error('获取失败资源失败:', error) failedResources.value = [] totalCount.value = 0 totalPages.value = 1 - errorStats.value = {} } finally { loading.value = false } @@ -332,6 +405,28 @@ const visiblePages = computed(() => { return pages }) +// 防抖函数 +const debounce = (func: Function, delay: number) => { + let timeoutId: NodeJS.Timeout + return (...args: any[]) => { + clearTimeout(timeoutId) + timeoutId = setTimeout(() => func.apply(null, args), delay) + } +} + +// 错误过滤输入变化处理(防抖) +const onErrorFilterChange = debounce(() => { + currentPage.value = 1 // 重置到第一页 + fetchData() +}, 300) + +// 清除错误过滤 +const clearErrorFilter = () => { + errorFilter.value = '' + currentPage.value = 1 // 重置到第一页 + fetchData() +} + // 刷新数据 const refreshData = () => { fetchData() @@ -402,22 +497,52 @@ const deleteResource = async (id: number) => { }) } -// 重试所有失败资源 +// 处理状态 +const isProcessing = ref(false) + +// 重新放入待处理池 const retryAllFailed = async () => { + if (totalCount.value === 0) { + alert('没有可处理的资源') + return + } + + // 检查是否有过滤条件 + if (!errorFilter.value.trim()) { + alert('请先设置过滤条件,以避免处理所有失败资源') + return + } + + // 构建查询条件 + const queryParams: any = {} + + // 如果有过滤条件,添加到查询参数中 + if (errorFilter.value.trim()) { + queryParams.error_filter = errorFilter.value.trim() + } + + const count = totalCount.value + dialog.warning({ - title: '警告', - content: '确定要重试所有可重试的失败资源吗?', + title: '确认操作', + content: `确定要将 ${count} 个资源重新放入待处理池吗?`, positiveText: '确定', negativeText: '取消', draggable: true, onPositiveClick: async () => { + if (isProcessing.value) return // 防止重复点击 + + isProcessing.value = true + try { - const response = await readyResourceApi.retryFailedResources() as any - alert(`重试操作完成:\n总数量:${response.total_count}\n已清除:${response.cleared_count}\n跳过:${response.skipped_count}`) + const response = await readyResourceApi.batchRestoreToReadyPoolByQuery(queryParams) as any + alert(`操作完成:\n总数量:${response.total_count}\n成功处理:${response.success_count}\n失败:${response.failed_count}`) fetchData() } catch (error) { - console.error('重试所有失败资源失败:', error) - alert('重试失败') + console.error('重新放入待处理池失败:', error) + alert('操作失败') + } finally { + isProcessing.value = false } } }) @@ -425,19 +550,49 @@ const retryAllFailed = async () => { // 清除所有错误 const clearAllErrors = async () => { + // 检查是否有过滤条件 + if (!errorFilter.value.trim()) { + alert('请先设置过滤条件,以避免删除所有失败资源') + return + } + + // 构建查询条件 + const queryParams: any = {} + + // 如果有过滤条件,添加到查询参数中 + if (errorFilter.value.trim()) { + queryParams.error_filter = errorFilter.value.trim() + } + + const count = totalCount.value + dialog.warning({ title: '警告', - content: '确定要清除所有失败资源的错误信息吗?此操作不可恢复!', - positiveText: '确定', + content: `确定要删除 ${count} 个失败资源吗?此操作将永久删除这些资源,不可恢复!`, + positiveText: '确定删除', negativeText: '取消', draggable: true, onPositiveClick: async () => { + if (isProcessing.value) return // 防止重复点击 + + isProcessing.value = true + try { - // 这里需要实现批量清除错误的API - alert('批量清除错误功能待实现') - } catch (error) { - console.error('清除所有错误失败:', error) - alert('清除失败') + console.log('开始调用删除API,参数:', queryParams) + const response = await readyResourceApi.clearAllErrorsByQuery(queryParams) as any + console.log('删除API响应:', response) + alert(`操作完成:\n删除失败资源:${response.affected_rows} 个资源`) + fetchData() + } catch (error: any) { + console.error('删除失败资源失败:', error) + console.error('错误详情:', { + message: error?.message, + stack: error?.stack, + response: error?.response + }) + alert('删除失败') + } finally { + isProcessing.value = false } } }) @@ -477,24 +632,7 @@ const truncateError = (errorMsg: string) => { return errorMsg.length > 50 ? errorMsg.substring(0, 50) + '...' : errorMsg } -// 获取错误类型名称 -const getErrorTypeName = (type: string) => { - const typeNames: Record = { - 'NO_ACCOUNT': '无账号', - 'NO_VALID_ACCOUNT': '无有效账号', - 'TRANSFER_FAILED': '转存失败', - 'LINK_CHECK_FAILED': '链接检查失败', - 'UNSUPPORTED_LINK': '不支持的链接', - 'INVALID_LINK': '无效链接', - 'SERVICE_CREATION_FAILED': '服务创建失败', - 'TAG_PROCESSING_FAILED': '标签处理失败', - 'CATEGORY_PROCESSING_FAILED': '分类处理失败', - 'RESOURCE_SAVE_FAILED': '资源保存失败', - 'PLATFORM_NOT_FOUND': '平台未找到', - 'UNKNOWN': '未知错误' - } - return typeNames[type] || type -} + // 页面加载时获取数据 onMounted(async () => { diff --git a/web/pages/admin/ready-resources.vue b/web/pages/admin/ready-resources.vue index 9a6b555..a211ddc 100644 --- a/web/pages/admin/ready-resources.vue +++ b/web/pages/admin/ready-resources.vue @@ -83,6 +83,12 @@
+ + 错误资源 + @@ -27,7 +27,7 @@ @@ -87,12 +87,15 @@ const validateForm = () => { errors.username = '' errors.password = '' - if (!form.username.trim()) { + console.log('validateForm - username:', form.username) + console.log('validateForm - password:', form.password ? '***' : 'empty') + + if (!form.username || !form.username.trim()) { errors.username = '请输入用户名' return false } - if (!form.password.trim()) { + if (!form.password || !form.password.trim()) { errors.password = '请输入密码' return false } @@ -101,7 +104,17 @@ const validateForm = () => { } const handleLogin = async () => { - if (!validateForm()) return + console.log('handleLogin - 开始登录,表单数据:', { + username: form.username, + password: form.password ? '***' : 'empty' + }) + + if (!validateForm()) { + console.log('handleLogin - 表单验证失败') + return + } + + console.log('handleLogin - 表单验证通过,开始调用登录API') const result = await userStore.login({ username: form.username,