Files
nysoure/server/dao/resource.go
2025-09-08 19:34:57 +08:00

450 lines
11 KiB
Go

package dao
import (
"errors"
"math/rand"
"nysoure/server/model"
"sync"
"sync/atomic"
"time"
"github.com/gofiber/fiber/v3/log"
"gorm.io/gorm"
)
func CreateResource(r model.Resource) (model.Resource, error) {
err := db.Transaction(func(tx *gorm.DB) error {
err := tx.Create(&r).Error
if err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", r.UserID).Update("resources_count", gorm.Expr("resources_count + ?", 1)).Error; err != nil {
return err
}
return nil
})
if err != nil {
return model.Resource{}, err
}
return r, nil
}
func GetResourceByID(id uint) (model.Resource, error) {
// Retrieve a resource by its ID from the database
var r model.Resource
if err := db.Preload("User").
Preload("Images").
Preload("Tags", func(db *gorm.DB) *gorm.DB {
return db.Select("id", "name", "type", "alias_of")
}).
Preload("Files").
Preload("Files.User").
First(&r, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return model.Resource{}, model.NewNotFoundError("Resource not found")
}
return model.Resource{}, err
}
for i, tag := range r.Tags {
if tag.AliasOf != nil {
t, err := GetTagByID(*tag.AliasOf)
if err != nil {
return model.Resource{}, err
} else {
r.Tags[i].Type = t.Type
}
}
}
return r, nil
}
func GetResourceList(page, pageSize int, sort model.RSort) ([]model.Resource, int, error) {
// Retrieve a list of resources with pagination
var resources []model.Resource
var total int64
if err := db.Model(&model.Resource{}).Count(&total).Error; err != nil {
return nil, 0, err
}
order := ""
switch sort {
case model.RSortTimeAsc:
order = "created_at ASC"
case model.RSortTimeDesc:
order = "created_at DESC"
case model.RSortViewsAsc:
order = "views ASC"
case model.RSortViewsDesc:
order = "views DESC"
case model.RSortDownloadsAsc:
order = "downloads ASC"
case model.RSortDownloadsDesc:
order = "downloads DESC"
default:
order = "created_at DESC" // Default sort order
}
if err := db.Offset((page - 1) * pageSize).Limit(pageSize).Preload("User").Preload("Images").Preload("Tags").Order(order).Find(&resources).Error; err != nil {
return nil, 0, err
}
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
return resources, int(totalPages), nil
}
func UpdateResource(r model.Resource) error {
// Update a resource in the database
images := r.Images
tags := r.Tags
r.Images = nil
r.Tags = nil
r.Files = nil
if err := db.Save(&r).Error; err != nil {
return err
}
if err := db.Model(&r).Association("Images").Replace(images); err != nil {
return err
}
if err := db.Model(&r).Association("Tags").Replace(tags); err != nil {
return err
}
return nil
}
func DeleteResource(id uint) error {
return db.Transaction(func(tx *gorm.DB) error {
var r model.Resource
if err := tx.Where("id = ?", id).First(&r).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return model.NewNotFoundError("Resource not found")
}
return err
}
if err := tx.Model(&model.File{}).Where("resource_id = ?", id).Delete(&model.File{}).Error; err != nil {
return err
}
if err := tx.Model(&model.User{}).Where("id = ?", r.UserID).Update("resources_count", gorm.Expr("resources_count - ?", 1)).Error; err != nil {
return err
}
if err := tx.Delete(&r).Error; err != nil {
return err
}
if err := tx.Model(&model.Activity{}).Where("type = ? AND ref_id = ?", model.ActivityTypeNewResource, id).Delete(&model.Activity{}).Error; err != nil {
return err
}
if err := tx.Model(&model.Activity{}).Where("type = ? AND ref_id = ?", model.ActivityTypeUpdateResource, id).Delete(&model.Activity{}).Error; err != nil {
return err
}
return nil
})
}
func GetResourceByTag(tagID uint, page int, pageSize int) ([]model.Resource, int, error) {
tag, err := GetTagByID(tagID)
if err != nil {
return nil, 0, err
}
if tag.AliasOf != nil {
tag, err = GetTagByID(*tag.AliasOf)
if err != nil {
return nil, 0, err
}
}
var tagIds []uint
tagIds = append(tagIds, tag.ID)
for _, alias := range tag.Aliases {
tagIds = append(tagIds, alias.ID)
}
var resources []model.Resource
var total int64
subQuery := db.Table("resource_tags").
Select("resource_id").
Where("tag_id IN ?", tagIds).
Group("resource_id")
if err := db.Model(&model.Resource{}).
Where("id IN (?)", subQuery).
Count(&total).Error; err != nil {
return nil, 0, err
}
if err := db.Where("id IN (?)", subQuery).
Offset((page - 1) * pageSize).
Limit(pageSize).
Preload("User").
Preload("Images").
Preload("Tags").
Preload("Files").
Order("created_at DESC").
Find(&resources).Error; err != nil {
return nil, 0, err
}
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
return resources, int(totalPages), nil
}
// CountResourcesByTag counts the number of resources associated with a specific tag.
func CountResourcesByTag(tagID uint) (int64, error) {
tag, err := GetTagByID(tagID)
if err != nil {
return 0, err
}
if tag.AliasOf != nil {
tag, err = GetTagByID(*tag.AliasOf)
if err != nil {
return 0, err
}
}
var tagIds []uint
tagIds = append(tagIds, tag.ID)
for _, alias := range tag.Aliases {
tagIds = append(tagIds, alias.ID)
}
var count int64
subQuery := db.Table("resource_tags").
Select("resource_id").
Where("tag_id IN ?", tagIds).
Group("resource_id")
if err := db.Model(&model.Resource{}).
Where("id IN (?)", subQuery).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func ExistsResource(id uint) (bool, error) {
var r model.Resource
if err := db.Model(&model.Resource{}).Where("id = ?", id).First(&r).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err
}
return true, nil
}
func GetResourcesByUsername(username string, page, pageSize int) ([]model.Resource, int, error) {
var user model.User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, model.NewNotFoundError("User not found")
}
return nil, 0, err
}
var resources []model.Resource
var total int64
if err := db.Model(&model.Resource{}).Where("user_id = ?", user.ID).Count(&total).Error; err != nil {
return nil, 0, err
}
if err := db.Model(&model.Resource{}).Where("user_id = ?", user.ID).Offset((page - 1) * pageSize).Limit(pageSize).Preload("User").Preload("Images").Preload("Tags").Order("created_at DESC").Find(&resources).Error; err != nil {
return nil, 0, err
}
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
return resources, int(totalPages), nil
}
// GetAllResources retrieves all resources from the database without all related data.
// It is used to generate a sitemap and rss feed.
func GetAllResources() ([]model.Resource, error) {
var resources []model.Resource
if err := db.Find(&resources).Error; err != nil {
return nil, err
}
return resources, nil
}
type CachedResourceStats struct {
id uint
views atomic.Int64
downloads atomic.Int64
}
var (
cachedResourcesStats = make(map[uint]*CachedResourceStats)
cacheMutex = sync.RWMutex{}
)
func init() {
go func() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for range ticker.C {
cacheMutex.Lock()
if len(cachedResourcesStats) == 0 {
cacheMutex.Unlock()
continue
}
err := db.Transaction(func(tx *gorm.DB) error {
for id, stats := range cachedResourcesStats {
var count int64
if err := tx.Model(&model.Resource{}).Where("id = ?", id).Count(&count).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warnf("Resource with ID %d not found, skipping stats update", id)
continue
}
return err
}
if count == 0 {
continue
}
if views := stats.views.Swap(0); views > 0 {
if err := tx.Model(&model.Resource{}).Where("id = ?", id).Update("views", gorm.Expr("views + ?", views)).Error; err != nil {
return err
}
}
if downloads := stats.downloads.Swap(0); downloads > 0 {
if err := tx.Model(&model.Resource{}).Where("id = ?", id).Update("downloads", gorm.Expr("downloads + ?", downloads)).Error; err != nil {
return err
}
}
}
return nil
})
if err != nil {
log.Error("Failed to update resource stats cache: ", err)
}
clear(cachedResourcesStats)
cacheMutex.Unlock()
}
}()
}
func AddResourceViewCount(id uint) error {
cacheMutex.RLock()
stats, exists := cachedResourcesStats[id]
cacheMutex.RUnlock()
if !exists {
cacheMutex.Lock()
stats, exists = cachedResourcesStats[id]
if !exists {
stats = &CachedResourceStats{id: id}
cachedResourcesStats[id] = stats
}
cacheMutex.Unlock()
}
stats.views.Add(1)
return nil
}
func AddResourceDownloadCount(id uint) error {
cacheMutex.RLock()
stats, exists := cachedResourcesStats[id]
cacheMutex.RUnlock()
if !exists {
cacheMutex.Lock()
stats, exists = cachedResourcesStats[id]
if !exists {
stats = &CachedResourceStats{id: id}
cachedResourcesStats[id] = stats
}
cacheMutex.Unlock()
}
stats.downloads.Add(1)
return nil
}
func RandomResource() (model.Resource, error) {
var maxID int64
if err := db.Model(&model.Resource{}).Select("MAX(id)").Scan(&maxID).Error; err != nil {
return model.Resource{}, err
}
for {
randomID := uint(1)
if maxID > 1 {
randomID = uint(1 + rand.Int63n(maxID-1))
}
var resource model.Resource
if err := db.
Preload("User").
Preload("Images").
Preload("Tags").
Preload("Files").
Where("id = ?", randomID).
First(&resource).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue // Try again if the resource does not exist
}
return model.Resource{}, err // Return error if any other issue occurs
}
return resource, nil // Return the found resource
}
}
func GetResourcesIdWithTag(tagID uint) ([]uint, error) {
tag, err := GetTagByID(tagID)
if err != nil {
return nil, err
}
if tag.AliasOf != nil {
tag, err = GetTagByID(*tag.AliasOf)
if err != nil {
return nil, err
}
}
var tagIds []uint
tagIds = append(tagIds, tag.ID)
for _, alias := range tag.Aliases {
tagIds = append(tagIds, alias.ID)
}
var result []model.Resource
subQuery := db.Table("resource_tags").
Select("resource_id").
Where("tag_id IN ?", tagIds).
Group("resource_id")
if err := db.Model(&model.Resource{}).
Where("id IN (?)", subQuery).
Order("created_at DESC").
Limit(10000).
Select("id", "created_at").
Find(&result).
Error; err != nil {
return nil, err
}
return tagIds, nil
}
func BatchGetResources(ids []uint) ([]model.Resource, error) {
idMap := make(map[uint]struct{})
uniqueIds := make([]uint, 0, len(ids))
for _, id := range ids {
if _, exists := idMap[id]; !exists {
idMap[id] = struct{}{}
uniqueIds = append(uniqueIds, id)
}
}
var resources []model.Resource
if err := db.
Where("id IN ?", uniqueIds).
Preload("User").
Preload("Images").
Preload("Tags").
Find(&resources).
Error; err != nil {
return nil, err
}
return resources, nil
}