mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
add: 给数据加上唯一key,支持资源多链接
This commit is contained in:
@@ -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)")
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:"批量资源的组标识,可选,不提供则自动生成"`
|
||||
}
|
||||
|
||||
@@ -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 搜索请求
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
@@ -66,7 +76,8 @@ func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) {
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"id": readyResource.ID,
|
||||
"id": readyResource.ID,
|
||||
"key": readyResource.Key,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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": "资源组删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
2
main.go
2
main.go
@@ -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)
|
||||
|
||||
@@ -486,6 +486,7 @@ func (s *Scheduler) convertReadyResourceToResource(readyResource entity.ReadyRes
|
||||
PanID: s.getPanIDByServiceType(serviceType),
|
||||
IsValid: true,
|
||||
IsPublic: true,
|
||||
Key: readyResource.Key,
|
||||
}
|
||||
|
||||
// 如果有分类信息,尝试查找或创建分类
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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
1529
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user