update: 更新批量添加接口,支持一个资源多个链接

This commit is contained in:
ctwj
2025-07-25 01:19:21 +08:00
parent 4463960447
commit 443d67ad78
11 changed files with 196 additions and 321 deletions

View File

@@ -195,23 +195,23 @@ func ToReadyResourceResponseList(resources []entity.ReadyResource) []dto.ReadyRe
} }
// RequestToReadyResource 将ReadyResourceRequest转换为ReadyResource实体 // RequestToReadyResource 将ReadyResourceRequest转换为ReadyResource实体
func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource { // func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource {
if req == nil { // if req == nil {
return nil // return nil
} // }
return &entity.ReadyResource{ // return &entity.ReadyResource{
Title: &req.Title, // Title: &req.Title,
Description: req.Description, // Description: req.Description,
URL: req.Url, // URL: req.Url,
Category: req.Category, // Category: req.Category,
Tags: req.Tags, // Tags: req.Tags,
Img: req.Img, // Img: req.Img,
Source: req.Source, // Source: req.Source,
Extra: req.Extra, // Extra: req.Extra,
Key: req.Key, // Key: req.Key,
} // }
} // }
// SystemConfigToPublicResponse 返回不含 api_token 的系统配置响应 // SystemConfigToPublicResponse 返回不含 api_token 的系统配置响应
func SystemConfigToPublicResponse(config *entity.SystemConfig) gin.H { func SystemConfigToPublicResponse(config *entity.SystemConfig) gin.H {

View File

@@ -2,19 +2,17 @@ package dto
// ReadyResourceRequest 待处理资源请求 // ReadyResourceRequest 待处理资源请求
type ReadyResourceRequest struct { type ReadyResourceRequest struct {
Title string `json:"title" validate:"required" example:"示例资源标题"` Title string `json:"title" validate:"required" example:"示例资源标题"`
Description string `json:"description" example:"这是一个示例资源描述"` Description string `json:"description" example:"这是一个示例资源描述"`
Url string `json:"url" validate:"required" example:"https://example.com/resource"` Url []string `json:"url" validate:"required" example:"https://example.com/resource"`
Category string `json:"category" example:"示例分类"` Category string `json:"category" example:"示例分类"`
Tags string `json:"tags" example:"标签1,标签2"` Tags string `json:"tags" example:"标签1,标签2"`
Img string `json:"img" example:"https://example.com/image.jpg"` Img string `json:"img" example:"https://example.com/image.jpg"`
Source string `json:"source" example:"数据来源"` Source string `json:"source" example:"数据来源"`
Extra string `json:"extra" example:"额外信息"` Extra string `json:"extra" example:"额外信息"`
Key string `json:"key" example:"资源组标识,可选,不提供则自动生成"`
} }
// BatchReadyResourceRequest 批量待处理资源请求 // BatchReadyResourceRequest 批量待处理资源请求
type BatchReadyResourceRequest struct { type BatchReadyResourceRequest struct {
Resources []ReadyResourceRequest `json:"resources" validate:"required"` Resources []ReadyResourceRequest `json:"resources" validate:"required"`
Key string `json:"key" example:"批量资源的组标识,可选,不提供则自动生成"`
} }

View File

@@ -108,22 +108,21 @@ type UpdateTagRequest struct {
// CreateReadyResourceRequest 创建待处理资源请求 // CreateReadyResourceRequest 创建待处理资源请求
type CreateReadyResourceRequest struct { type CreateReadyResourceRequest struct {
Title *string `json:"title"` Title *string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
URL string `json:"url" binding:"required"` URL []string `json:"url" binding:"required"`
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"`
IP *string `json:"ip"` IP *string `json:"ip"`
Key string `json:"key"` Key string `json:"key"`
} }
// BatchCreateReadyResourceRequest 批量创建待处理资源请求 // BatchCreateReadyResourceRequest 批量创建待处理资源请求
type BatchCreateReadyResourceRequest struct { type BatchCreateReadyResourceRequest struct {
Resources []CreateReadyResourceRequest `json:"resources" binding:"required"` Resources []CreateReadyResourceRequest `json:"resources" binding:"required"`
Key string `json:"key"`
} }
// SearchRequest 搜索请求 // SearchRequest 搜索请求

View File

@@ -324,7 +324,7 @@ func RefreshCapacity(c *gin.Context) {
cks.UsedSpace = userInfo.UsedSpace cks.UsedSpace = userInfo.UsedSpace
cks.IsValid = userInfo.VIPStatus // 根据VIP状态更新有效性 cks.IsValid = userInfo.VIPStatus // 根据VIP状态更新有效性
err = repoManager.CksRepository.Update(cks) err = repoManager.CksRepository.UpdateWithAllFields(cks)
if err != nil { if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError) ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return return

View File

@@ -3,8 +3,8 @@ package handlers
import ( import (
"strconv" "strconv"
"github.com/ctwj/urldb/db/converter"
"github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/dto"
"github.com/ctwj/urldb/db/entity"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -17,70 +17,6 @@ func NewPublicAPIHandler() *PublicAPIHandler {
return &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 // AddBatchResources godoc
// @Summary 批量添加资源 // @Summary 批量添加资源
// @Description 通过公开API批量添加多个资源到待处理列表 // @Description 通过公开API批量添加多个资源到待处理列表
@@ -106,26 +42,62 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
return return
} }
// 验证每个资源 // 收集所有待提交的URL去重
for i, resource := range req.Resources { urlSet := make(map[string]struct{})
if resource.Title == "" { for _, resource := range req.Resources {
ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源标题不能为空", 400) for _, u := range resource.Url {
return if u != "" {
} urlSet[u] = struct{}{}
}
if resource.Url == "" {
ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源URL不能为空", 400)
return
} }
} }
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 var createdResources []uint
for _, resourceReq := range req.Resources { for _, resourceReq := range req.Resources {
readyResource := converter.RequestToReadyResource(&resourceReq) // 生成 key每组同一个 key
if readyResource != nil { key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
readyResource.Source = "公开API批量添加" if err != nil {
err := repoManager.ReadyResourceRepository.Create(readyResource) 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 { if err == nil {
createdResources = append(createdResources, readyResource.ID) createdResources = append(createdResources, readyResource.ID)
} }

View File

@@ -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 批量创建待处理资源 // BatchCreateReadyResources 批量创建待处理资源
func BatchCreateReadyResources(c *gin.Context) { func BatchCreateReadyResources(c *gin.Context) {
var req dto.BatchCreateReadyResourceRequest var req dto.BatchCreateReadyResourceRequest
@@ -116,10 +57,14 @@ func BatchCreateReadyResources(c *gin.Context) {
// 1. 先收集所有待提交的URL去重 // 1. 先收集所有待提交的URL去重
urlSet := make(map[string]struct{}) urlSet := make(map[string]struct{})
for _, reqResource := range req.Resources { for _, reqResource := range req.Resources {
if reqResource.URL == "" { if len(reqResource.URL) == 0 {
continue continue
} }
urlSet[reqResource.URL] = struct{}{} for _, u := range reqResource.URL {
if u != "" {
urlSet[u] = struct{}{}
}
}
} }
uniqueUrls := make([]string, 0, len(urlSet)) uniqueUrls := make([]string, 0, len(urlSet))
for url := range urlSet { for url := range urlSet {
@@ -144,50 +89,42 @@ func BatchCreateReadyResources(c *gin.Context) {
} }
} }
// 4. 生成批量key如果请求中没有提供 // 5. 过滤掉已存在的URL
batchKey := req.Key var resources []entity.ReadyResource
if batchKey == "" { for _, reqResource := range req.Resources {
if len(reqResource.URL) == 0 {
continue
}
key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey() key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
if err != nil { if err != nil {
ErrorResponse(c, "生成批量资源组标识失败: "+err.Error(), http.StatusInternalServerError) ErrorResponse(c, "生成批量资源组标识失败: "+err.Error(), http.StatusInternalServerError)
return 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 resource := entity.ReadyResource{
var resources []entity.ReadyResource Title: reqResource.Title,
for _, reqResource := range req.Resources { Description: reqResource.Description,
url := reqResource.URL URL: url,
if url == "" { Category: reqResource.Category,
continue 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 { if len(resources) == 0 {
@@ -206,7 +143,6 @@ func BatchCreateReadyResources(c *gin.Context) {
SuccessResponse(c, gin.H{ SuccessResponse(c, gin.H{
"count": len(resources), "count": len(resources),
"key": batchKey,
"message": "批量创建成功", "message": "批量创建成功",
}) })
} }

View File

@@ -102,8 +102,6 @@ func main() {
publicAPI := api.Group("/public") publicAPI := api.Group("/public")
publicAPI.Use(middleware.PublicAPIAuth()) publicAPI.Use(middleware.PublicAPIAuth())
{ {
// 单个添加资源
publicAPI.POST("/resources/add", publicAPIHandler.AddSingleResource)
// 批量添加资源 // 批量添加资源
publicAPI.POST("/resources/batch-add", publicAPIHandler.AddBatchResources) publicAPI.POST("/resources/batch-add", publicAPIHandler.AddBatchResources)
// 资源搜索 // 资源搜索
@@ -166,7 +164,6 @@ func main() {
// 待处理资源管理 // 待处理资源管理
api.GET("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResources) 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/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchCreateReadyResources)
api.POST("/ready-resources/text", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResourcesFromText) api.POST("/ready-resources/text", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResourcesFromText)
api.DELETE("/ready-resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteReadyResource) api.DELETE("/ready-resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteReadyResource)

View File

@@ -4,10 +4,11 @@
<div class="mb-4 flex-1 w-1"> <div class="mb-4 flex-1 w-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输入格式说明</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输入格式说明</label>
<div class="bg-gray-50 dark:bg-gray-800 p-3 rounded text-sm text-gray-600 dark:text-gray-300 mb-4"> <div class="bg-gray-50 dark:bg-gray-800 p-3 rounded text-sm text-gray-600 dark:text-gray-300 mb-4">
<p class="mb-2"><strong>格式要求</strong>标题和URL两行为一组标题必填</p> <p class="mb-2"><strong>格式要求</strong>标题和URL为一组标题必填, 同一标题URL支持多行</p>
<pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs"> <pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs">
电影标题1 电影1
https://pan.baidu.com/s/123456 https://pan.baidu.com/s/123456 # 百度网盘 电影1
https://pan.quark.com/s/123456 # 夸克网盘 电影1
电影标题2 电影标题2
https://pan.baidu.com/s/789012 https://pan.baidu.com/s/789012
电视剧标题3 电视剧标题3
@@ -59,30 +60,11 @@ const validateInput = () => {
throw new Error('请输入有效的资源内容') throw new Error('请输入有效的资源内容')
} }
// 检查是否为偶数行(标题+URL为一组 // 首行必须为标题
if (lines.length % 2 !== 0) { if (/^https?:\/\//i.test(lines[0])) {
throw new Error('资源格式错误标题和URL必须成对出现请检查是否缺少标题或URL') // 你可以用 alert、ElMessage 或其它方式提示
} alert('首行必须为标题,不能为链接!')
return
// 检查每组的标题是否为空
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}`)
}
} }
} }
@@ -96,14 +78,30 @@ const handleSubmit = async () => {
const lines = batchInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean) const lines = batchInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean)
const resources = [] const resources = []
for (let i = 0; i < lines.length; i += 2) { let currentTitle = ''
const title = lines[i] let currentUrls = []
const url = lines[i + 1]
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({ resources.push({
title: title, title: currentTitle,
url: url, url: currentUrls.slice()
source: '批量添加'
}) })
} }

View File

@@ -22,12 +22,27 @@ export function useApiFetch<T = any>(
...options, ...options,
headers, headers,
onResponse({ response }) { 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 // 统一处理 code/message
if (response._data && response._data.code && response._data.code !== 200) { if (response._data && response._data.code && response._data.code !== 200) {
throw new Error(response._data.message || '请求失败') throw new Error(response._data.message || '请求失败')
} }
}, },
onResponseError({ error }: { error: any }) { onResponseError({ error }: { error: any }) {
console.log('error', error)
// 检查是否为"无效的令牌"错误 // 检查是否为"无效的令牌"错误
if (error?.data?.error === '无效的令牌') { if (error?.data?.error === '无效的令牌') {
// 清除用户状态 // 清除用户状态

View File

@@ -296,7 +296,6 @@
API使用说明 API使用说明
</h3> </h3>
<div class="text-xs text-blue-700 dark:text-blue-300 space-y-1"> <div class="text-xs text-blue-700 dark:text-blue-300 space-y-1">
<p> 单个添加资源: POST /api/public/resources/add</p>
<p> 批量添加资源: POST /api/public/resources/batch-add</p> <p> 批量添加资源: POST /api/public/resources/batch-add</p>
<p> 资源搜索: GET /api/public/resources/search</p> <p> 资源搜索: GET /api/public/resources/search</p>
<p> 热门剧: GET /api/public/hot-dramas</p> <p> 热门剧: GET /api/public/hot-dramas</p>

View File

@@ -46,57 +46,6 @@
<!-- API接口列表 --> <!-- API接口列表 -->
<div class="space-y-8"> <div class="space-y-8">
<!-- 单个添加资源 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
<div class="bg-green-600 text-white px-6 py-4">
<h3 class="text-xl font-semibold flex items-center">
<i class="fas fa-plus-circle mr-2"></i>
单个添加资源
</h3>
<p class="text-green-100 mt-1">添加单个资源到待处理列表</p>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">请求信息</h4>
<div class="space-y-2 text-sm">
<p><strong>方法</strong><span class="bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 px-2 py-1 rounded">POST</span></p>
<p><strong>路径</strong><code class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">/api/public/resources/add</code></p>
<p><strong>认证</strong><span class="text-red-600 dark:text-red-400">必需</span></p>
</div>
</div>
<div>
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">请求参数</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
<pre class="text-sm overflow-x-auto"><code>{
"title": "资源标题",
"description": "资源描述",
"url": "资源链接",
"category": "分类名称",
"tags": "标签1,标签2",
"img": "封面图片链接",
"source": "数据来源",
"extra": "额外信息"
}</code></pre>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">响应示例</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
<pre class="text-sm overflow-x-auto"><code>{
"success": true,
"message": "资源添加成功,已进入待处理列表",
"data": {
"id": 123
},
"code": 200
}</code></pre>
</div>
</div>
</div>
</div>
<!-- 批量添加资源 --> <!-- 批量添加资源 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
<div class="bg-purple-600 text-white px-6 py-4"> <div class="bg-purple-600 text-white px-6 py-4">
@@ -104,7 +53,7 @@
<i class="fas fa-layer-group mr-2"></i> <i class="fas fa-layer-group mr-2"></i>
批量添加资源 批量添加资源
</h3> </h3>
<p class="text-purple-100 mt-1">批量添加多个资源到待处理列表</p> <p class="text-purple-100 mt-1">批量添加多个资源到待处理列表每个资源可包含多个链接url为数组标题和url为必填项</p>
</div> </div>
<div class="p-6"> <div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@@ -113,22 +62,28 @@
<div class="space-y-2 text-sm"> <div class="space-y-2 text-sm">
<p><strong>方法</strong><span class="bg-purple-100 dark:bg-purple-800 text-purple-800 dark:text-purple-200 px-2 py-1 rounded">POST</span></p> <p><strong>方法</strong><span class="bg-purple-100 dark:bg-purple-800 text-purple-800 dark:text-purple-200 px-2 py-1 rounded">POST</span></p>
<p><strong>路径</strong><code class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">/api/public/resources/batch-add</code></p> <p><strong>路径</strong><code class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">/api/public/resources/batch-add</code></p>
<p><strong>认证</strong><span class="text-red-600 dark:text-red-400">必需</span></p> <p><strong>认证</strong><span class="text-red-600 dark:text-red-400">必需</span>X-API-Token</p>
</div> </div>
</div> </div>
<div> <div>
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">请求参数</h4> <h4 class="font-semibold text-gray-900 dark:text-white mb-3">请求参数</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">title url 是必填项其他字段均为选填</p>
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4"> <div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
<pre class="text-sm overflow-x-auto"><code>{ <pre class="text-sm overflow-x-auto"><code>{
"resources": [ "resources": [
{ {
"title": "资源1", "title": "资源1",
"url": "链接1", "description": "描述1",
"description": "描述1" "url": ["链接1", "链接2"],
"category": "分类",
"tags": "标签1,标签2",
"img": "图片链接",
"source": "数据来源",
"extra": "额外信息"
}, },
{ {
"title": "资源2", "title": "资源2",
"url": "链接2", "url": ["链接3"],
"description": "描述2" "description": "描述2"
} }
] ]
@@ -141,7 +96,7 @@
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4"> <div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
<pre class="text-sm overflow-x-auto"><code>{ <pre class="text-sm overflow-x-auto"><code>{
"success": true, "success": true,
"message": "批量添加成功,共添加 2 个资源", "message": "批量添加成功",
"data": { "data": {
"created_count": 2, "created_count": 2,
"created_ids": [123, 124] "created_ids": [123, 124]
@@ -323,14 +278,15 @@
<pre class="text-sm overflow-x-auto"><code># 设置API Token <pre class="text-sm overflow-x-auto"><code># 设置API Token
API_TOKEN="your_api_token_here" 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 "Content-Type: application/json" \
-H "X-API-Token: $API_TOKEN" \ -H "X-API-Token: $API_TOKEN" \
-d '{ -d '{
"title": "测试资源", "resources": [
"url": "https://example.com/resource", { "title": "测试资源1", "url": ["https://example.com/resource1"], "description": "描述1" },
"description": "测试描述" { "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) alert(res.message)
} }
}) })
// 单个添加资源 // 批量添加资源
fetch('/api/public/resources/add', { fetch('/api/public/resources/batch-add', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Token': 'your_token' }, 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 => res.json())
.then(res => { .then(res => {
if (res.success) { if (res.success) {
alert('添加成功ID' + res.data.id) alert('添加成功ID: ' + res.data.created_ids.join(', '))
} else { } else {
alert(res.message) alert(res.message)
} }