diff --git a/db/converter/converter.go b/db/converter/converter.go index 2cf372a..01cef39 100644 --- a/db/converter/converter.go +++ b/db/converter/converter.go @@ -195,23 +195,23 @@ func ToReadyResourceResponseList(resources []entity.ReadyResource) []dto.ReadyRe } // RequestToReadyResource 将ReadyResourceRequest转换为ReadyResource实体 -func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource { - if req == nil { - return nil - } +// func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource { +// if req == nil { +// return nil +// } - return &entity.ReadyResource{ - Title: &req.Title, - Description: req.Description, - URL: req.Url, - Category: req.Category, - Tags: req.Tags, - Img: req.Img, - Source: req.Source, - Extra: req.Extra, - Key: req.Key, - } -} +// return &entity.ReadyResource{ +// Title: &req.Title, +// Description: req.Description, +// URL: req.Url, +// Category: req.Category, +// Tags: req.Tags, +// Img: req.Img, +// Source: req.Source, +// Extra: req.Extra, +// Key: req.Key, +// } +// } // SystemConfigToPublicResponse 返回不含 api_token 的系统配置响应 func SystemConfigToPublicResponse(config *entity.SystemConfig) gin.H { diff --git a/db/dto/ready_resource.go b/db/dto/ready_resource.go index 907c81d..eaf4db4 100644 --- a/db/dto/ready_resource.go +++ b/db/dto/ready_resource.go @@ -2,19 +2,17 @@ package dto // ReadyResourceRequest 待处理资源请求 type ReadyResourceRequest struct { - Title string `json:"title" validate:"required" example:"示例资源标题"` - Description string `json:"description" example:"这是一个示例资源描述"` - Url string `json:"url" validate:"required" example:"https://example.com/resource"` - Category string `json:"category" example:"示例分类"` - Tags string `json:"tags" example:"标签1,标签2"` - Img string `json:"img" example:"https://example.com/image.jpg"` - Source string `json:"source" example:"数据来源"` - Extra string `json:"extra" example:"额外信息"` - Key string `json:"key" example:"资源组标识,可选,不提供则自动生成"` + Title string `json:"title" validate:"required" example:"示例资源标题"` + Description string `json:"description" example:"这是一个示例资源描述"` + Url []string `json:"url" validate:"required" example:"https://example.com/resource"` + Category string `json:"category" example:"示例分类"` + Tags string `json:"tags" example:"标签1,标签2"` + Img string `json:"img" example:"https://example.com/image.jpg"` + Source string `json:"source" example:"数据来源"` + Extra string `json:"extra" example:"额外信息"` } // BatchReadyResourceRequest 批量待处理资源请求 type BatchReadyResourceRequest struct { Resources []ReadyResourceRequest `json:"resources" validate:"required"` - Key string `json:"key" example:"批量资源的组标识,可选,不提供则自动生成"` } diff --git a/db/dto/request.go b/db/dto/request.go index 7c48911..98d86b7 100644 --- a/db/dto/request.go +++ b/db/dto/request.go @@ -108,22 +108,21 @@ type UpdateTagRequest struct { // CreateReadyResourceRequest 创建待处理资源请求 type CreateReadyResourceRequest struct { - Title *string `json:"title"` - Description string `json:"description"` - URL string `json:"url" binding:"required"` - Category string `json:"category"` - Tags string `json:"tags"` - Img string `json:"img"` - Source string `json:"source"` - Extra string `json:"extra"` - IP *string `json:"ip"` - Key string `json:"key"` + Title *string `json:"title"` + Description string `json:"description"` + URL []string `json:"url" binding:"required"` + Category string `json:"category"` + Tags string `json:"tags"` + Img string `json:"img"` + Source string `json:"source"` + Extra string `json:"extra"` + IP *string `json:"ip"` + Key string `json:"key"` } // BatchCreateReadyResourceRequest 批量创建待处理资源请求 type BatchCreateReadyResourceRequest struct { Resources []CreateReadyResourceRequest `json:"resources" binding:"required"` - Key string `json:"key"` } // SearchRequest 搜索请求 diff --git a/handlers/cks_handler.go b/handlers/cks_handler.go index 0d6d2d9..a3d7bce 100644 --- a/handlers/cks_handler.go +++ b/handlers/cks_handler.go @@ -324,7 +324,7 @@ func RefreshCapacity(c *gin.Context) { cks.UsedSpace = userInfo.UsedSpace cks.IsValid = userInfo.VIPStatus // 根据VIP状态更新有效性 - err = repoManager.CksRepository.Update(cks) + err = repoManager.CksRepository.UpdateWithAllFields(cks) if err != nil { ErrorResponse(c, err.Error(), http.StatusInternalServerError) return diff --git a/handlers/public_api_handler.go b/handlers/public_api_handler.go index 961cbd4..967ab3e 100644 --- a/handlers/public_api_handler.go +++ b/handlers/public_api_handler.go @@ -3,8 +3,8 @@ package handlers import ( "strconv" - "github.com/ctwj/urldb/db/converter" "github.com/ctwj/urldb/db/dto" + "github.com/ctwj/urldb/db/entity" "github.com/gin-gonic/gin" ) @@ -17,70 +17,6 @@ func NewPublicAPIHandler() *PublicAPIHandler { return &PublicAPIHandler{} } -// AddSingleResource godoc -// @Summary 单个添加资源 -// @Description 通过公开API添加单个资源到待处理列表 -// @Tags PublicAPI -// @Accept json -// @Produce json -// @Param X-API-Token header string true "API访问令牌" -// @Param data body dto.ReadyResourceRequest true "资源信息" -// @Success 200 {object} map[string]interface{} "添加成功" -// @Failure 400 {object} map[string]interface{} "请求参数错误" -// @Failure 401 {object} map[string]interface{} "认证失败" -// @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/public/resources/add [post] -func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) { - var req dto.ReadyResourceRequest - if err := c.ShouldBindJSON(&req); err != nil { - ErrorResponse(c, "请求参数错误: "+err.Error(), 400) - return - } - - // 验证必填字段 - if req.Title == "" { - ErrorResponse(c, "标题不能为空", 400) - return - } - - if req.Url == "" { - ErrorResponse(c, "URL不能为空", 400) - return - } - - // 转换为实体 - readyResource := converter.RequestToReadyResource(&req) - if readyResource == nil { - ErrorResponse(c, "数据转换失败", 500) - return - } - - // 设置来源 - readyResource.Source = "公开API" - - // 如果没有提供key,则自动生成 - if readyResource.Key == "" { - key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey() - if err != nil { - ErrorResponse(c, "生成资源组标识失败: "+err.Error(), 500) - return - } - readyResource.Key = key - } - - // 保存到数据库 - err := repoManager.ReadyResourceRepository.Create(readyResource) - if err != nil { - ErrorResponse(c, "添加资源失败: "+err.Error(), 500) - return - } - - SuccessResponse(c, gin.H{ - "id": readyResource.ID, - "key": readyResource.Key, - }) -} - // AddBatchResources godoc // @Summary 批量添加资源 // @Description 通过公开API批量添加多个资源到待处理列表 @@ -106,26 +42,62 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) { return } - // 验证每个资源 - for i, resource := range req.Resources { - if resource.Title == "" { - ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源标题不能为空", 400) - return - } - - if resource.Url == "" { - ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源URL不能为空", 400) - return + // 收集所有待提交的URL,去重 + urlSet := make(map[string]struct{}) + for _, resource := range req.Resources { + for _, u := range resource.Url { + if u != "" { + urlSet[u] = struct{}{} + } } } + uniqueUrls := make([]string, 0, len(urlSet)) + for url := range urlSet { + uniqueUrls = append(uniqueUrls, url) + } + + // 批量查重 + readyList, _ := repoManager.ReadyResourceRepository.BatchFindByURLs(uniqueUrls) + existReadyUrls := make(map[string]struct{}) + for _, r := range readyList { + existReadyUrls[r.URL] = struct{}{} + } + resourceList, _ := repoManager.ResourceRepository.BatchFindByURLs(uniqueUrls) + existResourceUrls := make(map[string]struct{}) + for _, r := range resourceList { + existResourceUrls[r.URL] = struct{}{} + } - // 批量保存 var createdResources []uint for _, resourceReq := range req.Resources { - readyResource := converter.RequestToReadyResource(&resourceReq) - if readyResource != nil { - readyResource.Source = "公开API批量添加" - err := repoManager.ReadyResourceRepository.Create(readyResource) + // 生成 key(每组同一个 key) + key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey() + if err != nil { + ErrorResponse(c, "生成资源组标识失败: "+err.Error(), 500) + return + } + for _, url := range resourceReq.Url { + if url == "" { + continue + } + if _, ok := existReadyUrls[url]; ok { + continue + } + if _, ok := existResourceUrls[url]; ok { + continue + } + readyResource := entity.ReadyResource{ + Title: &resourceReq.Title, + Description: resourceReq.Description, + URL: url, + Category: resourceReq.Category, + Tags: resourceReq.Tags, + Img: resourceReq.Img, + Source: "api", + Extra: resourceReq.Extra, + Key: key, + } + err := repoManager.ReadyResourceRepository.Create(&readyResource) if err == nil { createdResources = append(createdResources, readyResource.ID) } diff --git a/handlers/ready_resource_handler.go b/handlers/ready_resource_handler.go index cd6c657..9541874 100644 --- a/handlers/ready_resource_handler.go +++ b/handlers/ready_resource_handler.go @@ -46,65 +46,6 @@ func GetReadyResources(c *gin.Context) { }) } -// CreateReadyResource 创建待处理资源 -func CreateReadyResource(c *gin.Context) { - var req dto.CreateReadyResourceRequest - if err := c.ShouldBindJSON(&req); err != nil { - ErrorResponse(c, err.Error(), http.StatusBadRequest) - return - } - - if req.URL != "" { - // 检查待处理资源表 - readyList, _ := repoManager.ReadyResourceRepository.BatchFindByURLs([]string{req.URL}) - if len(readyList) > 0 { - ErrorResponse(c, "该URL已存在于待处理资源列表", http.StatusBadRequest) - return - } - // 检查资源表 - resourceList, _ := repoManager.ResourceRepository.BatchFindByURLs([]string{req.URL}) - if len(resourceList) > 0 { - ErrorResponse(c, "该URL已存在于资源列表", http.StatusBadRequest) - return - } - } - - // 如果没有提供key,则自动生成 - if req.Key == "" { - key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey() - if err != nil { - ErrorResponse(c, "生成资源组标识失败: "+err.Error(), http.StatusInternalServerError) - return - } - req.Key = key - } - - resource := &entity.ReadyResource{ - Title: req.Title, - Description: req.Description, - URL: req.URL, - Category: req.Category, - Tags: req.Tags, - Img: req.Img, - Source: req.Source, - Extra: req.Extra, - IP: req.IP, - Key: req.Key, - } - - err := repoManager.ReadyResourceRepository.Create(resource) - if err != nil { - ErrorResponse(c, err.Error(), http.StatusInternalServerError) - return - } - - SuccessResponse(c, gin.H{ - "id": resource.ID, - "key": resource.Key, - "message": "待处理资源创建成功", - }) -} - // BatchCreateReadyResources 批量创建待处理资源 func BatchCreateReadyResources(c *gin.Context) { var req dto.BatchCreateReadyResourceRequest @@ -116,10 +57,14 @@ func BatchCreateReadyResources(c *gin.Context) { // 1. 先收集所有待提交的URL,去重 urlSet := make(map[string]struct{}) for _, reqResource := range req.Resources { - if reqResource.URL == "" { + if len(reqResource.URL) == 0 { continue } - urlSet[reqResource.URL] = struct{}{} + for _, u := range reqResource.URL { + if u != "" { + urlSet[u] = struct{}{} + } + } } uniqueUrls := make([]string, 0, len(urlSet)) for url := range urlSet { @@ -144,50 +89,42 @@ func BatchCreateReadyResources(c *gin.Context) { } } - // 4. 生成批量key(如果请求中没有提供) - batchKey := req.Key - if batchKey == "" { + // 5. 过滤掉已存在的URL + var resources []entity.ReadyResource + for _, reqResource := range req.Resources { + if len(reqResource.URL) == 0 { + continue + } key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey() if err != nil { ErrorResponse(c, "生成批量资源组标识失败: "+err.Error(), http.StatusInternalServerError) return } - batchKey = key - } + for _, url := range reqResource.URL { + if url == "" { + continue + } + if _, ok := existReadyUrls[url]; ok { + continue + } + if _, ok := existResourceUrls[url]; ok { + continue + } - // 5. 过滤掉已存在的URL - var resources []entity.ReadyResource - for _, reqResource := range req.Resources { - url := reqResource.URL - if url == "" { - continue + resource := entity.ReadyResource{ + Title: reqResource.Title, + Description: reqResource.Description, + URL: url, + Category: reqResource.Category, + Tags: reqResource.Tags, + Img: reqResource.Img, + Source: reqResource.Source, + Extra: reqResource.Extra, + IP: reqResource.IP, + Key: key, + } + resources = append(resources, resource) } - if _, ok := existReadyUrls[url]; ok { - continue - } - if _, ok := existResourceUrls[url]; ok { - continue - } - - // 使用批量key或单个key - resourceKey := batchKey - if reqResource.Key != "" { - resourceKey = reqResource.Key - } - - resource := entity.ReadyResource{ - Title: reqResource.Title, - Description: reqResource.Description, - URL: reqResource.URL, - Category: reqResource.Category, - Tags: reqResource.Tags, - Img: reqResource.Img, - Source: reqResource.Source, - Extra: reqResource.Extra, - IP: reqResource.IP, - Key: resourceKey, - } - resources = append(resources, resource) } if len(resources) == 0 { @@ -206,7 +143,6 @@ func BatchCreateReadyResources(c *gin.Context) { SuccessResponse(c, gin.H{ "count": len(resources), - "key": batchKey, "message": "批量创建成功", }) } diff --git a/main.go b/main.go index c06b6d9..64f3798 100644 --- a/main.go +++ b/main.go @@ -102,8 +102,6 @@ func main() { publicAPI := api.Group("/public") publicAPI.Use(middleware.PublicAPIAuth()) { - // 单个添加资源 - publicAPI.POST("/resources/add", publicAPIHandler.AddSingleResource) // 批量添加资源 publicAPI.POST("/resources/batch-add", publicAPIHandler.AddBatchResources) // 资源搜索 @@ -166,7 +164,6 @@ func main() { // 待处理资源管理 api.GET("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResources) - api.POST("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResource) api.POST("/ready-resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchCreateReadyResources) api.POST("/ready-resources/text", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResourcesFromText) api.DELETE("/ready-resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteReadyResource) diff --git a/web/components/BatchAddResource.vue b/web/components/BatchAddResource.vue index dc0c4dc..ba59b71 100644 --- a/web/components/BatchAddResource.vue +++ b/web/components/BatchAddResource.vue @@ -4,10 +4,11 @@
-

格式要求:标题和URL两行为一组,标题为必填项

+

格式要求:标题和URL为一组,标题必填, 同一标题URL支持多行

-电影标题1
-https://pan.baidu.com/s/123456
+电影1
+https://pan.baidu.com/s/123456  # 百度网盘 电影1 
+https://pan.quark.com/s/123456  # 夸克网盘 电影1 
 电影标题2
 https://pan.baidu.com/s/789012
 电视剧标题3
@@ -59,30 +60,11 @@ const validateInput = () => {
     throw new Error('请输入有效的资源内容')
   }
 
-  // 检查是否为偶数行(标题+URL为一组)
-  if (lines.length % 2 !== 0) {
-    throw new Error('资源格式错误:标题和URL必须成对出现,请检查是否缺少标题或URL')
-  }
-
-  // 检查每组的标题是否为空
-  for (let i = 0; i < lines.length; i += 2) {
-    const title = lines[i]
-    const url = lines[i + 1]
-
-    if (!title) {
-      throw new Error(`第${i + 1}行标题不能为空`)
-    }
-
-    if (!url) {
-      throw new Error(`第${i + 2}行URL不能为空`)
-    }
-
-    // 验证URL格式
-    try {
-      new URL(url)
-    } catch {
-      throw new Error(`第${i + 2}行URL格式无效: ${url}`)
-    }
+  // 首行必须为标题
+  if (/^https?:\/\//i.test(lines[0])) {
+    // 你可以用 alert、ElMessage 或其它方式提示
+    alert('首行必须为标题,不能为链接!')
+    return
   }
 }
 
@@ -96,14 +78,30 @@ const handleSubmit = async () => {
     const lines = batchInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean)
     const resources = []
 
-    for (let i = 0; i < lines.length; i += 2) {
-      const title = lines[i]
-      const url = lines[i + 1]
+    let currentTitle = ''
+    let currentUrls = []
 
+    for (const line of lines) {
+      // 判断是否为 url(以 http/https 开头)
+      if (/^https?:\/\//i.test(line)) {
+        currentUrls.push(line)
+      } else {
+        // 新标题,先保存上一个
+        if (currentTitle && currentUrls.length) {
+          resources.push({
+            title: currentTitle,
+            url: currentUrls.slice()
+          })
+        }
+        currentTitle = line
+        currentUrls = []
+      }
+    }
+    // 处理最后一组
+    if (currentTitle && currentUrls.length) {
       resources.push({
-        title: title,
-        url: url,
-        source: '批量添加'
+        title: currentTitle,
+        url: currentUrls.slice()
       })
     }
 
diff --git a/web/composables/useApiFetch.ts b/web/composables/useApiFetch.ts
index f8b5e02..e501ab8 100644
--- a/web/composables/useApiFetch.ts
+++ b/web/composables/useApiFetch.ts
@@ -22,12 +22,27 @@ export function useApiFetch(
     ...options,
     headers,
     onResponse({ response }) {
+      if (response.status === 401 ||
+        (response._data && (response._data.code === 401 || response._data.error === '无效的令牌'))
+      ) {
+        userStore.logout()
+        if (process.client) {
+          window.location.href = '/login'
+        }
+        // 触发 onResponseError 逻辑
+        throw Object.assign(new Error('登录已过期,请重新登录'), {
+          data: response._data,
+          status: response.status,
+        })
+      }
+
       // 统一处理 code/message
       if (response._data && response._data.code && response._data.code !== 200) {
         throw new Error(response._data.message || '请求失败')
       }
     },
     onResponseError({ error }: { error: any }) {
+      console.log('error', error)
       // 检查是否为"无效的令牌"错误
       if (error?.data?.error === '无效的令牌') {
         // 清除用户状态
diff --git a/web/pages/admin/system-config.vue b/web/pages/admin/system-config.vue
index fb90d4b..e9be440 100644
--- a/web/pages/admin/system-config.vue
+++ b/web/pages/admin/system-config.vue
@@ -296,7 +296,6 @@
                     API使用说明
                   
                   
-

• 单个添加资源: POST /api/public/resources/add

• 批量添加资源: POST /api/public/resources/batch-add

• 资源搜索: GET /api/public/resources/search

• 热门剧: GET /api/public/hot-dramas

diff --git a/web/pages/api-docs.vue b/web/pages/api-docs.vue index 1475447..9bd81fd 100644 --- a/web/pages/api-docs.vue +++ b/web/pages/api-docs.vue @@ -46,57 +46,6 @@
- -
-
-

- - 单个添加资源 -

-

添加单个资源到待处理列表

-
-
-
-
-

请求信息

-
-

方法:POST

-

路径:/api/public/resources/add

-

认证:必需

-
-
-
-

请求参数

-
-
{
-  "title": "资源标题",
-  "description": "资源描述",
-  "url": "资源链接",
-  "category": "分类名称",
-  "tags": "标签1,标签2",
-  "img": "封面图片链接",
-  "source": "数据来源",
-  "extra": "额外信息"
-}
-
-
-
-
-

响应示例

-
-
{
-  "success": true,
-  "message": "资源添加成功,已进入待处理列表",
-  "data": {
-    "id": 123
-  },
-  "code": 200
-}
-
-
-
-
-
@@ -104,7 +53,7 @@ 批量添加资源 -

批量添加多个资源到待处理列表

+

批量添加多个资源到待处理列表,每个资源可包含多个链接(url为数组),标题和url为必填项

@@ -113,22 +62,28 @@

方法:POST

路径:/api/public/resources/batch-add

-

认证:必需

+

认证:必需(X-API-Token)

请求参数

+

title 和 url 是必填项,其他字段均为选填

{
   "resources": [
     {
       "title": "资源1",
-      "url": "链接1",
-      "description": "描述1"
+      "description": "描述1",
+      "url": ["链接1", "链接2"],
+      "category": "分类",
+      "tags": "标签1,标签2",
+      "img": "图片链接",
+      "source": "数据来源",
+      "extra": "额外信息"
     },
     {
-      "title": "资源2", 
-      "url": "链接2",
+      "title": "资源2",
+      "url": ["链接3"],
       "description": "描述2"
     }
   ]
@@ -141,7 +96,7 @@
                 
{
   "success": true,
-  "message": "批量添加成功,共添加 2 个资源",
+  "message": "批量添加成功",
   "data": {
     "created_count": 2,
     "created_ids": [123, 124]
@@ -323,14 +278,15 @@
               
# 设置API Token
 API_TOKEN="your_api_token_here"
 
-# 单个添加资源
-curl -X POST "http://localhost:8080/api/public/resources/add" \
+# 批量添加资源
+curl -X POST "http://localhost:8080/api/public/resources/batch-add" \
   -H "Content-Type: application/json" \
   -H "X-API-Token: $API_TOKEN" \
   -d '{
-    "title": "测试资源",
-    "url": "https://example.com/resource",
-    "description": "测试描述"
+    "resources": [
+      { "title": "测试资源1", "url": ["https://example.com/resource1"], "description": "描述1" },
+      { "title": "测试资源2", "url": ["https://example.com/resource2", "https://example.com/resource3"], "description": "描述2" }
+    ]
   }'
 
 # 搜索资源
@@ -355,16 +311,21 @@ fetch('/api/public/resources/search?q=测试', { headers: { 'X-API-Token': 'your
       alert(res.message)
     }
   })
-// 单个添加资源
-fetch('/api/public/resources/add', {
+// 批量添加资源
+fetch('/api/public/resources/batch-add', {
   method: 'POST',
   headers: { 'Content-Type': 'application/json', 'X-API-Token': 'your_token' },
-  body: JSON.stringify({ title: 'xxx', url: 'xxx' })
+  body: JSON.stringify({
+    resources: [
+      { title: 'xxx', url: ['xxx'], description: 'xxx' },
+      { title: 'yyy', url: ['yyy', 'zzz'], description: 'yyy' }
+    ]
+  })
 })
   .then(res => res.json())
   .then(res => {
     if (res.success) {
-      alert('添加成功,ID:' + res.data.id)
+      alert('添加成功,ID: ' + res.data.created_ids.join(', '))
     } else {
       alert(res.message)
     }