mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update: 完善处理失败的资源管理
This commit is contained in:
@@ -222,23 +222,24 @@ func ExtractShareId(url string) (string, ServiceType) {
|
|||||||
|
|
||||||
// 提取分享ID
|
// 提取分享ID
|
||||||
shareID := ""
|
shareID := ""
|
||||||
substring := strings.Index(url, "/s/")
|
substring := -1
|
||||||
if substring == -1 {
|
|
||||||
substring = strings.Index(url, "/t/") // 天翼云 是 t
|
if index := strings.Index(url, "/s/"); index != -1 {
|
||||||
shareID = url[substring+3:]
|
substring = index + 3
|
||||||
}
|
} else if index := strings.Index(url, "/t/"); index != -1 {
|
||||||
if substring == -1 {
|
substring = index + 3
|
||||||
substring = strings.Index(url, "/web/share?code=") // 天翼云 带密码
|
} else if index := strings.Index(url, "/web/share?code="); index != -1 {
|
||||||
shareID = url[substring+11:]
|
substring = index + 16
|
||||||
}
|
} else if index := strings.Index(url, "/p/"); index != -1 {
|
||||||
if substring == -1 {
|
substring = index + 3
|
||||||
substring = strings.Index(url, "/p/") // 天翼云 是 p
|
|
||||||
shareID = url[substring+3:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if substring == -1 {
|
if substring == -1 {
|
||||||
return "", NotFound
|
return "", NotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shareID = url[substring:]
|
||||||
|
|
||||||
// 去除可能的锚点
|
// 去除可能的锚点
|
||||||
if hashIndex := strings.Index(shareID, "#"); hashIndex != -1 {
|
if hashIndex := strings.Index(shareID, "#"); hashIndex != -1 {
|
||||||
shareID = shareID[:hashIndex]
|
shareID = shareID[:hashIndex]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package converter
|
package converter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ctwj/urldb/db/dto"
|
"github.com/ctwj/urldb/db/dto"
|
||||||
"github.com/ctwj/urldb/db/entity"
|
"github.com/ctwj/urldb/db/entity"
|
||||||
)
|
)
|
||||||
@@ -169,6 +171,12 @@ func ToCksResponseList(cksList []entity.Cks) []dto.CksResponse {
|
|||||||
|
|
||||||
// ToReadyResourceResponse 将ReadyResource实体转换为ReadyResourceResponse
|
// ToReadyResourceResponse 将ReadyResource实体转换为ReadyResourceResponse
|
||||||
func ToReadyResourceResponse(resource *entity.ReadyResource) dto.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{
|
return dto.ReadyResourceResponse{
|
||||||
ID: resource.ID,
|
ID: resource.ID,
|
||||||
Title: resource.Title,
|
Title: resource.Title,
|
||||||
@@ -183,6 +191,8 @@ func ToReadyResourceResponse(resource *entity.ReadyResource) dto.ReadyResourceRe
|
|||||||
ErrorMsg: resource.ErrorMsg,
|
ErrorMsg: resource.ErrorMsg,
|
||||||
CreateTime: resource.CreateTime,
|
CreateTime: resource.CreateTime,
|
||||||
IP: resource.IP,
|
IP: resource.IP,
|
||||||
|
DeletedAt: deletedAt,
|
||||||
|
IsDeleted: isDeleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,19 +79,21 @@ type CksResponse struct {
|
|||||||
|
|
||||||
// ReadyResourceResponse 待处理资源响应
|
// ReadyResourceResponse 待处理资源响应
|
||||||
type ReadyResourceResponse struct {
|
type ReadyResourceResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
Img string `json:"img"`
|
Img string `json:"img"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Extra string `json:"extra"`
|
Extra string `json:"extra"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
ErrorMsg string `json:"error_msg"`
|
ErrorMsg string `json:"error_msg"`
|
||||||
CreateTime time.Time `json:"create_time"`
|
CreateTime time.Time `json:"create_time"`
|
||||||
IP *string `json:"ip"`
|
IP *string `json:"ip"`
|
||||||
|
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats 统计信息
|
// Stats 统计信息
|
||||||
|
|||||||
@@ -23,8 +23,13 @@ type ReadyResourceRepository interface {
|
|||||||
GenerateUniqueKey() (string, error)
|
GenerateUniqueKey() (string, error)
|
||||||
FindWithErrors() ([]entity.ReadyResource, error)
|
FindWithErrors() ([]entity.ReadyResource, error)
|
||||||
FindWithErrorsPaginated(page, limit int) ([]entity.ReadyResource, int64, 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)
|
FindWithoutErrors() ([]entity.ReadyResource, error)
|
||||||
ClearErrorMsg(id uint) error
|
ClearErrorMsg(id uint) error
|
||||||
|
ClearErrorMsgAndRestore(id uint) error
|
||||||
|
ClearAllErrorsByQuery(errorFilter string) (int64, error) // 批量清除错误信息并真正删除资源
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadyResourceRepositoryImpl ReadyResource的Repository实现
|
// ReadyResourceRepositoryImpl ReadyResource的Repository实现
|
||||||
@@ -118,20 +123,20 @@ func (r *ReadyResourceRepositoryImpl) GenerateUniqueKey() (string, error) {
|
|||||||
return "", gorm.ErrInvalidData
|
return "", gorm.ErrInvalidData
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindWithErrors 查找有错误信息的资源(deleted_at为空且存在error_msg)
|
// FindWithErrors 查找有错误信息的资源(包括软删除的)
|
||||||
func (r *ReadyResourceRepositoryImpl) FindWithErrors() ([]entity.ReadyResource, error) {
|
func (r *ReadyResourceRepositoryImpl) FindWithErrors() ([]entity.ReadyResource, error) {
|
||||||
var resources []entity.ReadyResource
|
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
|
return resources, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindWithErrorsPaginated 分页查找有错误信息的资源(deleted_at为空且存在error_msg)
|
// FindWithErrorsPaginated 分页查找有错误信息的资源(包括软删除的)
|
||||||
func (r *ReadyResourceRepositoryImpl) FindWithErrorsPaginated(page, limit int) ([]entity.ReadyResource, int64, error) {
|
func (r *ReadyResourceRepositoryImpl) FindWithErrorsPaginated(page, limit int) ([]entity.ReadyResource, int64, error) {
|
||||||
var resources []entity.ReadyResource
|
var resources []entity.ReadyResource
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
offset := (page - 1) * limit
|
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 {
|
if err := db.Count(&total).Error; err != nil {
|
||||||
@@ -150,7 +155,75 @@ func (r *ReadyResourceRepositoryImpl) FindWithoutErrors() ([]entity.ReadyResourc
|
|||||||
return resources, err
|
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 清除指定资源的错误信息
|
// ClearErrorMsg 清除指定资源的错误信息
|
||||||
func (r *ReadyResourceRepositoryImpl) ClearErrorMsg(id uint) error {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ func GetReadyResourcesWithErrors(c *gin.Context) {
|
|||||||
// 获取分页参数
|
// 获取分页参数
|
||||||
pageStr := c.DefaultQuery("page", "1")
|
pageStr := c.DefaultQuery("page", "1")
|
||||||
pageSizeStr := c.DefaultQuery("page_size", "100")
|
pageSizeStr := c.DefaultQuery("page_size", "100")
|
||||||
|
errorFilter := c.Query("error_filter")
|
||||||
|
|
||||||
page, err := strconv.Atoi(pageStr)
|
page, err := strconv.Atoi(pageStr)
|
||||||
if err != nil || page < 1 {
|
if err != nil || page < 1 {
|
||||||
@@ -327,8 +328,8 @@ func GetReadyResourcesWithErrors(c *gin.Context) {
|
|||||||
pageSize = 100
|
pageSize = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取有错误的资源(分页)
|
// 获取有错误的资源(分页,包括软删除的)
|
||||||
resources, total, err := repoManager.ReadyResourceRepository.FindWithErrorsPaginated(page, pageSize)
|
resources, total, err := repoManager.ReadyResourceRepository.FindWithErrorsPaginatedIncludingDeleted(page, pageSize, errorFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -465,3 +466,109 @@ func RetryFailedResources(c *gin.Context) {
|
|||||||
"retryable_count": getRetryableErrorCount(resources),
|
"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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -217,6 +217,9 @@ func main() {
|
|||||||
api.GET("/ready-resources/errors", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResourcesWithErrors)
|
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/: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/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)
|
api.GET("/users", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetUsers)
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ func (s *Scheduler) convertReadyResourceToResource(readyResource entity.ReadyRes
|
|||||||
return NewNoAccountError(serviceType.String())
|
return NewNoAccountError(serviceType.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择第一个有效的账号
|
// 选择第一个有效的账号 TODO 需要优化,随机选择账号
|
||||||
var selectedAccount *entity.Cks
|
var selectedAccount *entity.Cks
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
if account.IsValid {
|
if account.IsValid {
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ const pageConfig = computed(() => {
|
|||||||
'/monitor': { title: '系统监控', icon: 'fas fa-desktop', description: '系统性能监控' },
|
'/monitor': { title: '系统监控', icon: 'fas fa-desktop', description: '系统性能监控' },
|
||||||
'/admin/add-resource': { title: '添加资源', icon: 'fas fa-plus', description: '添加新资源' },
|
'/admin/add-resource': { title: '添加资源', icon: 'fas fa-plus', description: '添加新资源' },
|
||||||
'/api-docs': { title: 'API文档', icon: 'fas fa-book', 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: '管理页面' }
|
return configs[route.path] || { title: props.title, icon: 'fas fa-cog', description: '管理页面' }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ export const parseApiResponse = <T>(response: any): T => {
|
|||||||
page_size: response.data.limit
|
page_size: response.data.limit
|
||||||
} as T
|
} 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
|
return response.data
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.message || '请求失败')
|
throw new Error(response.message || '请求失败')
|
||||||
@@ -122,6 +134,9 @@ export const useReadyResourceApi = () => {
|
|||||||
const clearReadyResources = () => useApiFetch('/ready-resources', { method: 'DELETE' }).then(parseApiResponse)
|
const clearReadyResources = () => useApiFetch('/ready-resources', { method: 'DELETE' }).then(parseApiResponse)
|
||||||
const clearErrorMsg = (id: number) => useApiFetch(`/ready-resources/${id}/clear-error`, { method: 'POST' }).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 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 {
|
return {
|
||||||
getReadyResources,
|
getReadyResources,
|
||||||
getFailedResources,
|
getFailedResources,
|
||||||
@@ -131,7 +146,10 @@ export const useReadyResourceApi = () => {
|
|||||||
deleteReadyResource,
|
deleteReadyResource,
|
||||||
clearReadyResources,
|
clearReadyResources,
|
||||||
clearErrorMsg,
|
clearErrorMsg,
|
||||||
retryFailedResources
|
retryFailedResources,
|
||||||
|
batchRestoreToReadyPool,
|
||||||
|
batchRestoreToReadyPoolByQuery,
|
||||||
|
clearAllErrorsByQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export function useApiFetch<T = any>(
|
|||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
onResponse({ response }) {
|
onResponse({ response }) {
|
||||||
|
console.log('API响应:', {
|
||||||
|
status: response.status,
|
||||||
|
data: response._data,
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
|
||||||
if (response.status === 401 ||
|
if (response.status === 401 ||
|
||||||
(response._data && (response._data.code === 401 || response._data.error === '无效的令牌'))
|
(response._data && (response._data.code === 401 || response._data.error === '无效的令牌'))
|
||||||
) {
|
) {
|
||||||
@@ -38,6 +44,7 @@ export function useApiFetch<T = any>(
|
|||||||
|
|
||||||
// 统一处理 code/message
|
// 统一处理 code/message
|
||||||
if (response._data && response._data.code && response._data.code !== 200) {
|
if (response._data && response._data.code && response._data.code !== 200) {
|
||||||
|
console.error('API错误响应:', response._data)
|
||||||
throw new Error(response._data.message || '请求失败')
|
throw new Error(response._data.message || '请求失败')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,18 +25,53 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@click="retryAllFailed"
|
@click="retryAllFailed"
|
||||||
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
:disabled="!errorFilter.trim() || isProcessing"
|
||||||
|
:class="[
|
||||||
|
'w-full sm:w-auto px-4 py-2 rounded-md transition-colors text-center flex items-center justify-center gap-2',
|
||||||
|
errorFilter.trim() && !isProcessing
|
||||||
|
? 'bg-green-600 hover:bg-green-700'
|
||||||
|
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<i class="fas fa-redo"></i> 重试所有失败
|
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
|
||||||
|
<i v-else class="fas fa-redo"></i>
|
||||||
|
{{ isProcessing ? '处理中...' : '重新放入待处理池' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="clearAllErrors"
|
@click="clearAllErrors"
|
||||||
class="w-full sm:w-auto px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
:disabled="!errorFilter.trim() || isProcessing"
|
||||||
|
:class="[
|
||||||
|
'w-full sm:w-auto px-4 py-2 rounded-md transition-colors text-center flex items-center justify-center gap-2',
|
||||||
|
errorFilter.trim() && !isProcessing
|
||||||
|
? 'bg-yellow-600 hover:bg-yellow-700'
|
||||||
|
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<i class="fas fa-broom"></i> 清除所有错误
|
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
|
||||||
|
<i v-else class="fas fa-trash"></i>
|
||||||
|
{{ isProcessing ? '处理中...' : '删除失败资源' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
|
<!-- 错误信息过滤 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-input
|
||||||
|
v-model:value="errorFilter"
|
||||||
|
type="text"
|
||||||
|
placeholder="过滤错误信息..."
|
||||||
|
class="w-48"
|
||||||
|
clearable
|
||||||
|
@input="onErrorFilterChange"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="errorFilter"
|
||||||
|
@click="clearErrorFilter"
|
||||||
|
class="px-2 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
title="清除过滤条件"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="refreshData"
|
@click="refreshData"
|
||||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||||
@@ -46,20 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错误统计 -->
|
|
||||||
<div v-if="errorStats && Object.keys(errorStats).length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 mb-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">错误类型统计</h3>
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
||||||
<div
|
|
||||||
v-for="(count, type) in errorStats"
|
|
||||||
:key="type"
|
|
||||||
class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3 text-center"
|
|
||||||
>
|
|
||||||
<div class="text-2xl font-bold text-red-600 dark:text-red-400">{{ count }}</div>
|
|
||||||
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">{{ getErrorTypeName(type) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 失败资源列表 -->
|
<!-- 失败资源列表 -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||||
@@ -68,6 +90,7 @@
|
|||||||
<thead class="bg-red-800 dark:bg-red-900 text-white dark:text-gray-100 sticky top-0 z-10">
|
<thead class="bg-red-800 dark:bg-red-900 text-white dark:text-gray-100 sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-medium">状态</th>
|
||||||
<th class="px-4 py-3 text-left text-sm font-medium">标题</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">标题</th>
|
||||||
<th class="px-4 py-3 text-left text-sm font-medium">URL</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">URL</th>
|
||||||
<th class="px-4 py-3 text-left text-sm font-medium">错误信息</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">错误信息</th>
|
||||||
@@ -78,12 +101,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 max-h-96 overflow-y-auto">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 max-h-96 overflow-y-auto">
|
||||||
<tr v-if="loading" class="text-center py-8">
|
<tr v-if="loading" class="text-center py-8">
|
||||||
<td colspan="7" class="text-gray-500 dark:text-gray-400">
|
<td colspan="8" class="text-gray-500 dark:text-gray-400">
|
||||||
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
|
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="failedResources.length === 0">
|
<tr v-else-if="failedResources.length === 0">
|
||||||
<td colspan="7">
|
<td colspan="8">
|
||||||
<div class="flex flex-col items-center justify-center py-12">
|
<div class="flex flex-col items-center justify-center py-12">
|
||||||
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
||||||
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
|
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
|
||||||
@@ -97,11 +120,30 @@
|
|||||||
<tr
|
<tr
|
||||||
v-for="resource in failedResources"
|
v-for="resource in failedResources"
|
||||||
:key="resource.id"
|
:key="resource.id"
|
||||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
:class="[
|
||||||
|
'hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors',
|
||||||
|
resource.is_deleted ? 'bg-gray-100 dark:bg-gray-700' : ''
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ resource.id }}</td>
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ resource.id }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<span
|
||||||
|
v-if="resource.is_deleted"
|
||||||
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||||
|
title="已删除"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash mr-1"></i>已删除
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||||
|
title="正常"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check mr-1"></i>正常
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
<span v-if="resource.title" :title="resource.title">{{ escapeHtml(resource.title) }}</span>
|
<span v-if="resource.title && resource.title !== null" :title="resource.title">{{ escapeHtml(resource.title) }}</span>
|
||||||
<span v-else class="text-gray-400 dark:text-gray-500 italic">未设置</span>
|
<span v-else class="text-gray-400 dark:text-gray-500 italic">未设置</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-sm">
|
<td class="px-4 py-3 text-sm">
|
||||||
@@ -168,6 +210,9 @@
|
|||||||
<!-- 总资源数 -->
|
<!-- 总资源数 -->
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个失败资源
|
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个失败资源
|
||||||
|
<span v-if="errorFilter" class="ml-2 text-blue-600 dark:text-blue-400">
|
||||||
|
(已过滤)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600"></div>
|
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600"></div>
|
||||||
@@ -216,6 +261,9 @@
|
|||||||
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
|
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个失败资源
|
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个失败资源
|
||||||
|
<span v-if="errorFilter" class="ml-2 text-blue-600 dark:text-blue-400">
|
||||||
|
(已过滤)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,11 +280,13 @@ definePageMeta({
|
|||||||
|
|
||||||
interface FailedResource {
|
interface FailedResource {
|
||||||
id: number
|
id: number
|
||||||
title?: string
|
title?: string | null
|
||||||
url: string
|
url: string
|
||||||
error_msg: string
|
error_msg: string
|
||||||
create_time: string
|
create_time: string
|
||||||
ip?: string
|
ip?: string | null
|
||||||
|
deleted_at?: string | null
|
||||||
|
is_deleted: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const failedResources = ref<FailedResource[]>([])
|
const failedResources = ref<FailedResource[]>([])
|
||||||
@@ -249,10 +299,12 @@ const pageSize = ref(100)
|
|||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const totalPages = ref(0)
|
const totalPages = ref(0)
|
||||||
|
|
||||||
// 错误统计
|
|
||||||
const errorStats = ref<Record<string, number>>({})
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
// 过滤相关状态
|
||||||
|
const errorFilter = ref('')
|
||||||
|
|
||||||
// 获取失败资源API
|
// 获取失败资源API
|
||||||
import { useReadyResourceApi } from '~/composables/useApi'
|
import { useReadyResourceApi } from '~/composables/useApi'
|
||||||
const readyResourceApi = useReadyResourceApi()
|
const readyResourceApi = useReadyResourceApi()
|
||||||
@@ -261,28 +313,49 @@ const readyResourceApi = useReadyResourceApi()
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await readyResourceApi.getFailedResources({
|
const params: any = {
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
page_size: pageSize.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
|
failedResources.value = response.data
|
||||||
totalCount.value = response.total || 0
|
totalCount.value = response.total || 0
|
||||||
totalPages.value = Math.ceil((response.total || 0) / pageSize.value)
|
totalPages.value = Math.ceil((response.total || 0) / pageSize.value)
|
||||||
errorStats.value = response.error_stats || {}
|
|
||||||
} else {
|
} else {
|
||||||
|
console.log('fetchData - 使用空数据格式')
|
||||||
failedResources.value = []
|
failedResources.value = []
|
||||||
totalCount.value = 0
|
totalCount.value = 0
|
||||||
totalPages.value = 1
|
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) {
|
} catch (error) {
|
||||||
console.error('获取失败资源失败:', error)
|
console.error('获取失败资源失败:', error)
|
||||||
failedResources.value = []
|
failedResources.value = []
|
||||||
totalCount.value = 0
|
totalCount.value = 0
|
||||||
totalPages.value = 1
|
totalPages.value = 1
|
||||||
errorStats.value = {}
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -332,6 +405,28 @@ const visiblePages = computed(() => {
|
|||||||
return pages
|
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 = () => {
|
const refreshData = () => {
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -402,22 +497,52 @@ const deleteResource = async (id: number) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重试所有失败资源
|
// 处理状态
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
|
||||||
|
// 重新放入待处理池
|
||||||
const retryAllFailed = async () => {
|
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({
|
dialog.warning({
|
||||||
title: '警告',
|
title: '确认操作',
|
||||||
content: '确定要重试所有可重试的失败资源吗?',
|
content: `确定要将 ${count} 个资源重新放入待处理池吗?`,
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
draggable: true,
|
draggable: true,
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
|
if (isProcessing.value) return // 防止重复点击
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await readyResourceApi.retryFailedResources() as any
|
const response = await readyResourceApi.batchRestoreToReadyPoolByQuery(queryParams) as any
|
||||||
alert(`重试操作完成:\n总数量:${response.total_count}\n已清除:${response.cleared_count}\n跳过:${response.skipped_count}`)
|
alert(`操作完成:\n总数量:${response.total_count}\n成功处理:${response.success_count}\n失败:${response.failed_count}`)
|
||||||
fetchData()
|
fetchData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重试所有失败资源失败:', error)
|
console.error('重新放入待处理池失败:', error)
|
||||||
alert('重试失败')
|
alert('操作失败')
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -425,19 +550,49 @@ const retryAllFailed = async () => {
|
|||||||
|
|
||||||
// 清除所有错误
|
// 清除所有错误
|
||||||
const clearAllErrors = 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({
|
dialog.warning({
|
||||||
title: '警告',
|
title: '警告',
|
||||||
content: '确定要清除所有失败资源的错误信息吗?此操作不可恢复!',
|
content: `确定要删除 ${count} 个失败资源吗?此操作将永久删除这些资源,不可恢复!`,
|
||||||
positiveText: '确定',
|
positiveText: '确定删除',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
draggable: true,
|
draggable: true,
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
|
if (isProcessing.value) return // 防止重复点击
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 这里需要实现批量清除错误的API
|
console.log('开始调用删除API,参数:', queryParams)
|
||||||
alert('批量清除错误功能待实现')
|
const response = await readyResourceApi.clearAllErrorsByQuery(queryParams) as any
|
||||||
} catch (error) {
|
console.log('删除API响应:', response)
|
||||||
console.error('清除所有错误失败:', error)
|
alert(`操作完成:\n删除失败资源:${response.affected_rows} 个资源`)
|
||||||
alert('清除失败')
|
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
|
return errorMsg.length > 50 ? errorMsg.substring(0, 50) + '...' : errorMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取错误类型名称
|
|
||||||
const getErrorTypeName = (type: string) => {
|
|
||||||
const typeNames: Record<string, string> = {
|
|
||||||
'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 () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -83,6 +83,12 @@
|
|||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/failed-resources"
|
||||||
|
class="w-full sm:w-auto px-4 py-2 bg-red-600 hover:bg-red-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus"></i> 错误资源
|
||||||
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/add-resource"
|
to="/admin/add-resource"
|
||||||
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<n-input
|
<n-input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
v-model="form.username"
|
v-model:value="form.username"
|
||||||
required
|
required
|
||||||
:class="{ 'border-red-500': errors.username }"
|
:class="{ 'border-red-500': errors.username }"
|
||||||
/>
|
/>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<n-input
|
<n-input
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
v-model="form.password"
|
v-model:value="form.password"
|
||||||
required
|
required
|
||||||
:class="{ 'border-red-500': errors.password }"
|
:class="{ 'border-red-500': errors.password }"
|
||||||
/>
|
/>
|
||||||
@@ -87,12 +87,15 @@ const validateForm = () => {
|
|||||||
errors.username = ''
|
errors.username = ''
|
||||||
errors.password = ''
|
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 = '请输入用户名'
|
errors.username = '请输入用户名'
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.password.trim()) {
|
if (!form.password || !form.password.trim()) {
|
||||||
errors.password = '请输入密码'
|
errors.password = '请输入密码'
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -101,7 +104,17 @@ const validateForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogin = async () => {
|
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({
|
const result = await userStore.login({
|
||||||
username: form.username,
|
username: form.username,
|
||||||
|
|||||||
Reference in New Issue
Block a user