add: 给数据加上唯一key,支持资源多链接

This commit is contained in:
Kerwin
2025-07-24 18:45:32 +08:00
parent 595c44b437
commit 4463960447
17 changed files with 936 additions and 760 deletions

View File

@@ -125,6 +125,11 @@ func createIndexes(db *gorm.DB) {
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_is_valid ON resources(is_valid)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_is_public ON resources(is_public)")
// 待处理资源表索引
db.Exec("CREATE INDEX IF NOT EXISTS idx_ready_resource_key ON ready_resource(key)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_ready_resource_url ON ready_resource(url)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_ready_resource_create_time ON ready_resource(create_time DESC)")
// 搜索统计表索引
db.Exec("CREATE INDEX IF NOT EXISTS idx_search_stats_query ON search_stats(query)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_search_stats_created_at ON search_stats(created_at DESC)")

View File

@@ -179,6 +179,7 @@ func ToReadyResourceResponse(resource *entity.ReadyResource) dto.ReadyResourceRe
Img: resource.Img,
Source: resource.Source,
Extra: resource.Extra,
Key: resource.Key,
CreateTime: resource.CreateTime,
IP: resource.IP,
}
@@ -208,6 +209,7 @@ func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource
Img: req.Img,
Source: req.Source,
Extra: req.Extra,
Key: req.Key,
}
}

View File

@@ -10,9 +10,11 @@ type ReadyResourceRequest struct {
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:"资源组标识,可选,不提供则自动生成"`
}
// BatchReadyResourceRequest 批量待处理资源请求
type BatchReadyResourceRequest struct {
Resources []ReadyResourceRequest `json:"resources" validate:"required"`
Key string `json:"key" example:"批量资源的组标识,可选,不提供则自动生成"`
}

View File

@@ -117,11 +117,13 @@ type CreateReadyResourceRequest struct {
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 搜索请求

View File

@@ -88,6 +88,7 @@ type ReadyResourceResponse struct {
Img string `json:"img"`
Source string `json:"source"`
Extra string `json:"extra"`
Key string `json:"key"`
CreateTime time.Time `json:"create_time"`
IP *string `json:"ip"`
}

View File

@@ -17,6 +17,7 @@ type ReadyResource struct {
Img string `json:"img" gorm:"size:500;comment:封面链接"`
Source string `json:"source" gorm:"size:100;comment:数据来源"`
Extra string `json:"extra" gorm:"type:text;comment:额外附加数据"`
Key string `json:"key" gorm:"size:64;index;comment:资源组标识相同key表示同一组资源"`
CreateTime time.Time `json:"create_time" gorm:"default:CURRENT_TIMESTAMP"`
IP *string `json:"ip" gorm:"size:45;comment:IP地址"`
CreatedAt time.Time `json:"created_at"`

View File

@@ -27,6 +27,7 @@ type Resource struct {
ErrorMsg string `json:"error_msg" gorm:"size:255;comment:转存失败原因"`
CkID *uint `json:"ck_id" gorm:"comment:账号ID"`
Fid string `json:"fid" gorm:"size:128;comment:网盘文件ID"`
Key string `json:"key" gorm:"size:64;index;comment:资源组标识相同key表示同一组资源"`
// 关联关系
Category Category `json:"category" gorm:"foreignKey:CategoryID"`

View File

@@ -5,6 +5,7 @@ import (
"github.com/ctwj/urldb/db/entity"
gonanoid "github.com/matoous/go-nanoid/v2"
"gorm.io/gorm"
)
@@ -13,10 +14,13 @@ type ReadyResourceRepository interface {
BaseRepository[entity.ReadyResource]
FindByURL(url string) (*entity.ReadyResource, error)
FindByIP(ip string) ([]entity.ReadyResource, error)
FindByKey(key string) ([]entity.ReadyResource, error)
BatchCreate(resources []entity.ReadyResource) error
DeleteByURL(url string) error
DeleteByKey(key string) error
FindAllWithinDays(days int) ([]entity.ReadyResource, error)
BatchFindByURLs(urls []string) ([]entity.ReadyResource, error)
GenerateUniqueKey() (string, error)
}
// ReadyResourceRepositoryImpl ReadyResource的Repository实现
@@ -78,3 +82,34 @@ func (r *ReadyResourceRepositoryImpl) BatchFindByURLs(urls []string) ([]entity.R
err := r.db.Where("url IN ?", urls).Find(&resources).Error
return resources, err
}
// FindByKey 根据Key查找
func (r *ReadyResourceRepositoryImpl) FindByKey(key string) ([]entity.ReadyResource, error) {
var resources []entity.ReadyResource
err := r.db.Where("key = ?", key).Find(&resources).Error
return resources, err
}
// DeleteByKey 根据Key删除
func (r *ReadyResourceRepositoryImpl) DeleteByKey(key string) error {
return r.db.Where("key = ?", key).Delete(&entity.ReadyResource{}).Error
}
// GenerateUniqueKey 生成唯一的6位Base62 key
func (r *ReadyResourceRepositoryImpl) GenerateUniqueKey() (string, error) {
for i := 0; i < 20; i++ {
key, err := gonanoid.Generate("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 6)
if err != nil {
return "", err
}
var count int64
err = r.db.Model(&entity.ReadyResource{}).Where("key = ?", key).Count(&count).Error
if err != nil {
return "", err
}
if count == 0 {
return key, nil
}
}
return "", gorm.ErrInvalidData
}

1
go.mod
View File

@@ -35,6 +35,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect

2
go.sum
View File

@@ -76,6 +76,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=

View File

@@ -58,6 +58,16 @@ func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) {
// 设置来源
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 {
@@ -67,6 +77,7 @@ func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) {
SuccessResponse(c, gin.H{
"id": readyResource.ID,
"key": readyResource.Key,
})
}

View File

@@ -69,6 +69,16 @@ func CreateReadyResource(c *gin.Context) {
}
}
// 如果没有提供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,
@@ -79,6 +89,7 @@ func CreateReadyResource(c *gin.Context) {
Source: req.Source,
Extra: req.Extra,
IP: req.IP,
Key: req.Key,
}
err := repoManager.ReadyResourceRepository.Create(resource)
@@ -89,6 +100,7 @@ func CreateReadyResource(c *gin.Context) {
SuccessResponse(c, gin.H{
"id": resource.ID,
"key": resource.Key,
"message": "待处理资源创建成功",
})
}
@@ -132,7 +144,18 @@ func BatchCreateReadyResources(c *gin.Context) {
}
}
// 4. 过滤掉已存在的URL
// 4. 生成批量key如果请求中没有提供
batchKey := req.Key
if batchKey == "" {
key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
if err != nil {
ErrorResponse(c, "生成批量资源组标识失败: "+err.Error(), http.StatusInternalServerError)
return
}
batchKey = key
}
// 5. 过滤掉已存在的URL
var resources []entity.ReadyResource
for _, reqResource := range req.Resources {
url := reqResource.URL
@@ -145,6 +168,13 @@ func BatchCreateReadyResources(c *gin.Context) {
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,
@@ -155,6 +185,7 @@ func BatchCreateReadyResources(c *gin.Context) {
Source: reqResource.Source,
Extra: reqResource.Extra,
IP: reqResource.IP,
Key: resourceKey,
}
resources = append(resources, resource)
}
@@ -175,6 +206,7 @@ func BatchCreateReadyResources(c *gin.Context) {
SuccessResponse(c, gin.H{
"count": len(resources),
"key": batchKey,
"message": "批量创建成功",
})
}
@@ -261,3 +293,60 @@ func ClearReadyResources(c *gin.Context) {
"message": "所有待处理资源已清空",
})
}
// GetReadyResourcesByKey 根据key获取待处理资源
func GetReadyResourcesByKey(c *gin.Context) {
key := c.Param("key")
if key == "" {
ErrorResponse(c, "key参数不能为空", http.StatusBadRequest)
return
}
resources, err := repoManager.ReadyResourceRepository.FindByKey(key)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
responses := converter.ToReadyResourceResponseList(resources)
SuccessResponse(c, gin.H{
"data": responses,
"key": key,
"count": len(resources),
})
}
// DeleteReadyResourcesByKey 根据key删除待处理资源
func DeleteReadyResourcesByKey(c *gin.Context) {
key := c.Param("key")
if key == "" {
ErrorResponse(c, "key参数不能为空", http.StatusBadRequest)
return
}
// 先查询要删除的资源数量
resources, err := repoManager.ReadyResourceRepository.FindByKey(key)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
if len(resources) == 0 {
ErrorResponse(c, "未找到指定key的资源", http.StatusNotFound)
return
}
// 删除所有具有相同key的资源
err = repoManager.ReadyResourceRepository.DeleteByKey(key)
if err != nil {
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"deleted_count": len(resources),
"key": key,
"message": "资源组删除成功",
})
}

View File

@@ -171,6 +171,8 @@ func main() {
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", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearReadyResources)
api.GET("/ready-resources/key/:key", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResourcesByKey)
api.DELETE("/ready-resources/key/:key", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteReadyResourcesByKey)
// 用户管理(仅管理员)
api.GET("/users", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetUsers)

View File

@@ -486,6 +486,7 @@ func (s *Scheduler) convertReadyResourceToResource(readyResource entity.ReadyRes
PanID: s.getPanIDByServiceType(serviceType),
IsValid: true,
IsPublic: true,
Key: readyResource.Key,
}
// 如果有分类信息,尝试查找或创建分类

View File

@@ -96,14 +96,16 @@ import QRCode from 'qrcode'
interface Props {
visible: boolean
url: string
url?: string
}
interface Emits {
(e: 'close'): void
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
url: ''
})
const emit = defineEmits<Emits>()
const qrCanvas = ref<HTMLCanvasElement>()

View File

@@ -25,7 +25,7 @@ export default defineNuxtConfig({
})
],
optimizeDeps: {
include: ['naive-ui', 'vueuc', 'date-fns'],
include: ['vueuc', 'date-fns'],
exclude: ["oxc-parser"] // 强制使用 WASM 版本
}
},

1529
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff