mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 04:17:23 +00:00
Add collection api.
This commit is contained in:
@@ -191,3 +191,11 @@ export interface Activity {
|
|||||||
comment?: Comment;
|
comment?: Comment;
|
||||||
file?: RFile;
|
file?: RFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Collection {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
article: string;
|
||||||
|
user: User;
|
||||||
|
images: Image[];
|
||||||
|
}
|
@@ -19,6 +19,7 @@ import {
|
|||||||
TagWithCount,
|
TagWithCount,
|
||||||
Activity,
|
Activity,
|
||||||
CommentWithRef,
|
CommentWithRef,
|
||||||
|
Collection,
|
||||||
} from "./models.ts";
|
} from "./models.ts";
|
||||||
|
|
||||||
class Network {
|
class Network {
|
||||||
@@ -688,6 +689,99 @@ class Network {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createCollection(
|
||||||
|
title: string,
|
||||||
|
article: string,
|
||||||
|
): Promise<Response<Collection>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.postForm(`${this.apiBaseUrl}/collection/create`, {
|
||||||
|
title,
|
||||||
|
article,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCollection(
|
||||||
|
id: number,
|
||||||
|
title: string,
|
||||||
|
article: string,
|
||||||
|
): Promise<Response<any>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.postForm(`${this.apiBaseUrl}/collection/update`, {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
article,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCollection(id: number): Promise<Response<any>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.postForm(`${this.apiBaseUrl}/collection/delete`, {
|
||||||
|
id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCollection(id: number): Promise<Response<Collection>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.get(`${this.apiBaseUrl}/collection/${id}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listUserCollections(page: number = 1): Promise<PageResponse<Collection>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.get(`${this.apiBaseUrl}/collection/list`, {
|
||||||
|
params: { page },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listCollectionResources(
|
||||||
|
collectionId: number,
|
||||||
|
page: number = 1,
|
||||||
|
): Promise<PageResponse<Resource>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.get(`${this.apiBaseUrl}/collection/${collectionId}/resources`, {
|
||||||
|
params: { page },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addResourceToCollection(
|
||||||
|
collectionId: number,
|
||||||
|
resourceId: number,
|
||||||
|
): Promise<Response<any>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.postForm(`${this.apiBaseUrl}/collection/add_resource`, {
|
||||||
|
collection_id: collectionId,
|
||||||
|
resource_id: resourceId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeResourceFromCollection(
|
||||||
|
collectionId: number,
|
||||||
|
resourceId: number,
|
||||||
|
): Promise<Response<any>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.postForm(`${this.apiBaseUrl}/collection/remove_resource`, {
|
||||||
|
collection_id: collectionId,
|
||||||
|
resource_id: resourceId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchUserCollections(
|
||||||
|
keyword: string,
|
||||||
|
): Promise<Response<Collection[]>> {
|
||||||
|
return this._callApi(() =>
|
||||||
|
axios.get(`${this.apiBaseUrl}/collection/search`, {
|
||||||
|
params: { keyword },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const network = new Network();
|
export const network = new Network();
|
||||||
|
6
main.go
6
main.go
@@ -1,11 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gofiber/fiber/v3"
|
|
||||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
|
||||||
"log"
|
"log"
|
||||||
"nysoure/server/api"
|
"nysoure/server/api"
|
||||||
"nysoure/server/middleware"
|
"nysoure/server/middleware"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -35,6 +36,7 @@ func main() {
|
|||||||
api.AddCommentRoutes(apiG)
|
api.AddCommentRoutes(apiG)
|
||||||
api.AddConfigRoutes(apiG)
|
api.AddConfigRoutes(apiG)
|
||||||
api.AddActivityRoutes(apiG)
|
api.AddActivityRoutes(apiG)
|
||||||
|
api.AddCollectionRoutes(apiG) // 新增
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatal(app.Listen(":3000"))
|
log.Fatal(app.Listen(":3000"))
|
||||||
|
227
server/api/collection.go
Normal file
227
server/api/collection.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nysoure/server/model"
|
||||||
|
"nysoure/server/service"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleCreateCollection(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
title := c.FormValue("title")
|
||||||
|
article := c.FormValue("article")
|
||||||
|
if title == "" || article == "" {
|
||||||
|
return model.NewRequestError("Title and article are required")
|
||||||
|
}
|
||||||
|
host := c.Hostname()
|
||||||
|
col, err := service.CreateCollection(uid, title, article, host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[model.CollectionView]{
|
||||||
|
Success: true,
|
||||||
|
Data: *col,
|
||||||
|
Message: "Collection created successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpdateCollection(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
idStr := c.FormValue("id")
|
||||||
|
title := c.FormValue("title")
|
||||||
|
article := c.FormValue("article")
|
||||||
|
if idStr == "" || title == "" || article == "" {
|
||||||
|
return model.NewRequestError("ID, title and article are required")
|
||||||
|
}
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid collection ID")
|
||||||
|
}
|
||||||
|
host := c.Hostname()
|
||||||
|
if err := service.UpdateCollection(uid, uint(id), title, article, host); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||||
|
Success: true,
|
||||||
|
Message: "Collection updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDeleteCollection(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
idStr := c.FormValue("id")
|
||||||
|
if idStr == "" {
|
||||||
|
return model.NewRequestError("ID is required")
|
||||||
|
}
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid collection ID")
|
||||||
|
}
|
||||||
|
if err := service.DeleteCollection(uid, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||||
|
Success: true,
|
||||||
|
Message: "Collection deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetCollection(c fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid collection ID")
|
||||||
|
}
|
||||||
|
col, err := service.GetCollectionByID(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[model.CollectionView]{
|
||||||
|
Success: true,
|
||||||
|
Data: *col,
|
||||||
|
Message: "Collection retrieved successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleListUserCollections(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
pageStr := c.Query("page", "1")
|
||||||
|
page, err := strconv.Atoi(pageStr)
|
||||||
|
if err != nil || page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
cols, total, err := service.ListUserCollections(uid, page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.PageResponse[*model.CollectionView]{
|
||||||
|
Success: true,
|
||||||
|
TotalPages: int(total),
|
||||||
|
Data: cols,
|
||||||
|
Message: "Collections retrieved successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleListCollectionResources(c fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid collection ID")
|
||||||
|
}
|
||||||
|
pageStr := c.Query("page", "1")
|
||||||
|
page, err := strconv.Atoi(pageStr)
|
||||||
|
if err != nil || page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
res, total, err := service.ListCollectionResources(uint(id), page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.PageResponse[*model.ResourceView]{
|
||||||
|
Success: true,
|
||||||
|
TotalPages: int(total),
|
||||||
|
Data: res,
|
||||||
|
Message: "Resources retrieved successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAddResourceToCollection(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
collectionIDStr := c.FormValue("collection_id")
|
||||||
|
resourceIDStr := c.FormValue("resource_id")
|
||||||
|
if collectionIDStr == "" || resourceIDStr == "" {
|
||||||
|
return model.NewRequestError("collection_id and resource_id are required")
|
||||||
|
}
|
||||||
|
collectionID, err := strconv.Atoi(collectionIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid collection_id")
|
||||||
|
}
|
||||||
|
resourceID, err := strconv.Atoi(resourceIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid resource_id")
|
||||||
|
}
|
||||||
|
if err := service.AddResourceToCollection(uid, uint(collectionID), uint(resourceID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||||
|
Success: true,
|
||||||
|
Message: "Resource added to collection successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRemoveResourceFromCollection(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
collectionIDStr := c.FormValue("collection_id")
|
||||||
|
resourceIDStr := c.FormValue("resource_id")
|
||||||
|
if collectionIDStr == "" || resourceIDStr == "" {
|
||||||
|
return model.NewRequestError("collection_id and resource_id are required")
|
||||||
|
}
|
||||||
|
collectionID, err := strconv.Atoi(collectionIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid collection_id")
|
||||||
|
}
|
||||||
|
resourceID, err := strconv.Atoi(resourceIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid resource_id")
|
||||||
|
}
|
||||||
|
if err := service.RemoveResourceFromCollection(uid, uint(collectionID), uint(resourceID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||||
|
Success: true,
|
||||||
|
Message: "Resource removed from collection successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSearchUserCollections(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("Unauthorized")
|
||||||
|
}
|
||||||
|
keyword := c.Query("keyword", "")
|
||||||
|
if keyword == "" {
|
||||||
|
return model.NewRequestError("keyword is required")
|
||||||
|
}
|
||||||
|
cols, err := service.SearchUserCollections(uid, keyword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[[]*model.CollectionView]{
|
||||||
|
Success: true,
|
||||||
|
Data: cols,
|
||||||
|
Message: "Collections found successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddCollectionRoutes(r fiber.Router) {
|
||||||
|
cg := r.Group("collection")
|
||||||
|
cg.Post("/create", handleCreateCollection)
|
||||||
|
cg.Post("/update", handleUpdateCollection)
|
||||||
|
cg.Post("/delete", handleDeleteCollection)
|
||||||
|
cg.Get("/:id", handleGetCollection)
|
||||||
|
cg.Get("/list", handleListUserCollections)
|
||||||
|
cg.Get("/:id/resources", handleListCollectionResources)
|
||||||
|
cg.Post("/add_resource", handleAddResourceToCollection)
|
||||||
|
cg.Post("/remove_resource", handleRemoveResourceFromCollection)
|
||||||
|
cg.Get("/search", handleSearchUserCollections)
|
||||||
|
}
|
193
server/dao/collection.go
Normal file
193
server/dao/collection.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nysoure/server/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateCollection(uid uint, title string, article string, images []uint) (model.Collection, error) {
|
||||||
|
var collection model.Collection
|
||||||
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
collection = model.Collection{
|
||||||
|
UserID: uid,
|
||||||
|
Title: title,
|
||||||
|
Article: article,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(collection).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Association("Images").Replace(images); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return model.Collection{}, err
|
||||||
|
}
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCollection(id uint, title string, article string, images []uint) error {
|
||||||
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
collection := &model.Collection{
|
||||||
|
Title: title,
|
||||||
|
Article: article,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Where("id = ?", id).Updates(collection).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Association("Images").Replace(images); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCollection(id uint) error {
|
||||||
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
collection := &model.Collection{}
|
||||||
|
|
||||||
|
if err := tx.Where("id = ?", id).First(collection).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Association("Images").Clear(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Association("Resources").Clear(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Delete(collection).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddResourceToCollection(collectionID uint, resourceID uint) error {
|
||||||
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
collection := &model.Collection{}
|
||||||
|
|
||||||
|
if err := tx.Where("id = ?", collectionID).First(collection).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Association("Resources").Append(&model.Resource{
|
||||||
|
Model: gorm.Model{
|
||||||
|
ID: resourceID,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveResourceFromCollection(collectionID uint, resourceID uint) error {
|
||||||
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
collection := &model.Collection{}
|
||||||
|
|
||||||
|
if err := tx.Where("id = ?", collectionID).First(collection).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Association("Resources").Delete(&model.Resource{
|
||||||
|
Model: gorm.Model{
|
||||||
|
ID: resourceID,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCollectionByID(id uint) (*model.Collection, error) {
|
||||||
|
collection := &model.Collection{}
|
||||||
|
if err := db.Preload("Images").Preload("Resources").Where("id = ?", id).First(collection).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListUserCollections(uid uint, page int, pageSize int) ([]*model.Collection, int64, error) {
|
||||||
|
var collections []*model.Collection
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
if err := db.Model(&model.Collection{}).Where("user_id = ?", uid).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Model(&model.Collection{}).
|
||||||
|
Preload("Images").
|
||||||
|
Preload("Resources").
|
||||||
|
Where("user_id = ?", uid).
|
||||||
|
Offset((page - 1) * pageSize).
|
||||||
|
Limit(pageSize).
|
||||||
|
Find(&collections).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
|
||||||
|
|
||||||
|
return collections, totalPages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListCollectionResources(collectionID uint, page int, pageSize int) ([]*model.Resource, int64, error) {
|
||||||
|
var resources []*model.Resource
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
if err := db.Raw(`
|
||||||
|
select count(*) from collection_resources
|
||||||
|
where collection_id = ?`, collectionID).Scan(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Model(&model.Resource{}).
|
||||||
|
Preload("User").
|
||||||
|
Preload("Images").
|
||||||
|
Preload("Tags").
|
||||||
|
Joins("JOIN collection_resources ON collection_resources.resource_id = resources.id").
|
||||||
|
Where("collection_resources.collection_id = ?", collectionID).
|
||||||
|
Offset((page - 1) * pageSize).
|
||||||
|
Limit(pageSize).
|
||||||
|
Find(&resources).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
|
||||||
|
|
||||||
|
return resources, totalPages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchUserCollections searches for collections by user ID and keyword limited to 10 results.
|
||||||
|
func SearchUserCollections(uid uint, keyword string) ([]*model.Collection, error) {
|
||||||
|
var collections []*model.Collection
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Model(&model.Collection{}).
|
||||||
|
Preload("Images").
|
||||||
|
Preload("Resources").
|
||||||
|
Where("user_id = ? AND title LIKE ?", uid, "%"+keyword+"%").
|
||||||
|
Limit(10).
|
||||||
|
Find(&collections).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return collections, nil
|
||||||
|
}
|
@@ -2,9 +2,10 @@ package dao
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"gorm.io/gorm"
|
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateImage(name string, width, height int) (model.Image, error) {
|
func CreateImage(name string, width, height int) (model.Image, error) {
|
||||||
@@ -45,6 +46,7 @@ func GetUnusedImages() ([]model.Image, error) {
|
|||||||
if err := db.
|
if err := db.
|
||||||
Where("NOT EXISTS (SELECT 1 FROM resource_images WHERE image_id = images.id)").
|
Where("NOT EXISTS (SELECT 1 FROM resource_images WHERE image_id = images.id)").
|
||||||
Where("NOT EXISTS (SELECT 1 FROM comment_images WHERE image_id = images.id)").
|
Where("NOT EXISTS (SELECT 1 FROM comment_images WHERE image_id = images.id)").
|
||||||
|
Where("NOT EXISTS (SELECT 1 FROM collection_images WHERE image_id = images.id)").
|
||||||
Where("created_at < ?", oneDayAgo).
|
Where("created_at < ?", oneDayAgo).
|
||||||
Find(&images).Error; err != nil {
|
Find(&images).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
31
server/model/collection.go
Normal file
31
server/model/collection.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type Collection struct {
|
||||||
|
gorm.Model
|
||||||
|
Title string `gorm:"not null"`
|
||||||
|
Article string `gorm:"not null"`
|
||||||
|
UserID uint `gorm:"not null"`
|
||||||
|
User User `gorm:"foreignKey:UserID;references:ID"`
|
||||||
|
Images []Image `gorm:"many2many:collection_images;"`
|
||||||
|
Resources []Resource `gorm:"many2many:collection_resources;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionView struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Article string `json:"article"`
|
||||||
|
User UserView `json:"user"`
|
||||||
|
Images []Image `json:"images"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Collection) ToView() *CollectionView {
|
||||||
|
return &CollectionView{
|
||||||
|
ID: c.ID,
|
||||||
|
Title: c.Title,
|
||||||
|
Article: c.Article,
|
||||||
|
User: c.User.ToView(),
|
||||||
|
Images: c.Images,
|
||||||
|
}
|
||||||
|
}
|
145
server/service/collection.go
Normal file
145
server/service/collection.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"nysoure/server/dao"
|
||||||
|
"nysoure/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a new collection.
|
||||||
|
func CreateCollection(uid uint, title, article string, host string) (*model.CollectionView, error) {
|
||||||
|
if uid == 0 || title == "" || article == "" {
|
||||||
|
return nil, errors.New("invalid parameters")
|
||||||
|
}
|
||||||
|
c, err := dao.CreateCollection(uid, title, article, findImagesInContent(article, host))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
view := c.ToView()
|
||||||
|
return view, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing collection with user validation.
|
||||||
|
func UpdateCollection(uid, id uint, title, article string, host string) error {
|
||||||
|
if uid == 0 || id == 0 || title == "" || article == "" {
|
||||||
|
return errors.New("invalid parameters")
|
||||||
|
}
|
||||||
|
collection, err := dao.GetCollectionByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if collection.UserID != uid {
|
||||||
|
return errors.New("user does not have permission to update this collection")
|
||||||
|
}
|
||||||
|
return dao.UpdateCollection(id, title, article, findImagesInContent(article, host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a collection by ID.
|
||||||
|
func DeleteCollection(uint, id uint) error {
|
||||||
|
user, err := dao.GetUserByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
collection, err := dao.GetCollectionByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.ID != collection.UserID && !user.IsAdmin {
|
||||||
|
return errors.New("user does not have permission to delete this collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return dao.DeleteCollection(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a resource to a collection with user validation.
|
||||||
|
func AddResourceToCollection(uid, collectionID, resourceID uint) error {
|
||||||
|
if uid == 0 || collectionID == 0 || resourceID == 0 {
|
||||||
|
return errors.New("invalid parameters")
|
||||||
|
}
|
||||||
|
collection, err := dao.GetCollectionByID(collectionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if collection.UserID != uid {
|
||||||
|
return errors.New("user does not have permission to modify this collection")
|
||||||
|
}
|
||||||
|
return dao.AddResourceToCollection(collectionID, resourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a resource from a collection with user validation.
|
||||||
|
func RemoveResourceFromCollection(uid, collectionID, resourceID uint) error {
|
||||||
|
if uid == 0 || collectionID == 0 || resourceID == 0 {
|
||||||
|
return errors.New("invalid parameters")
|
||||||
|
}
|
||||||
|
collection, err := dao.GetCollectionByID(collectionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if collection.UserID != uid {
|
||||||
|
return errors.New("user does not have permission to modify this collection")
|
||||||
|
}
|
||||||
|
return dao.RemoveResourceFromCollection(collectionID, resourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a collection by ID.
|
||||||
|
func GetCollectionByID(id uint) (*model.CollectionView, error) {
|
||||||
|
if id == 0 {
|
||||||
|
return nil, errors.New("invalid collection id")
|
||||||
|
}
|
||||||
|
c, err := dao.GetCollectionByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.ToView(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List collections of a user with pagination.
|
||||||
|
func ListUserCollections(uid uint, page int) ([]*model.CollectionView, int64, error) {
|
||||||
|
if uid == 0 || page < 1 {
|
||||||
|
return nil, 0, errors.New("invalid parameters")
|
||||||
|
}
|
||||||
|
collections, total, err := dao.ListUserCollections(uid, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
var views []*model.CollectionView
|
||||||
|
for _, c := range collections {
|
||||||
|
views = append(views, c.ToView())
|
||||||
|
}
|
||||||
|
return views, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List resources in a collection with pagination.
|
||||||
|
func ListCollectionResources(collectionID uint, page int) ([]*model.ResourceView, int64, error) {
|
||||||
|
if collectionID == 0 || page < 1 {
|
||||||
|
return nil, 0, errors.New("invalid parameters")
|
||||||
|
}
|
||||||
|
resources, total, err := dao.ListCollectionResources(collectionID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
var views []*model.ResourceView
|
||||||
|
for _, r := range resources {
|
||||||
|
v := r.ToView()
|
||||||
|
views = append(views, &v)
|
||||||
|
}
|
||||||
|
return views, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search user collections by keyword, limited to 10 results.
|
||||||
|
func SearchUserCollections(uid uint, keyword string) ([]*model.CollectionView, error) {
|
||||||
|
if uid == 0 || keyword == "" {
|
||||||
|
return nil, errors.New("invalid parameters")
|
||||||
|
}
|
||||||
|
collections, err := dao.SearchUserCollections(uid, keyword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var views []*model.CollectionView
|
||||||
|
for _, c := range collections {
|
||||||
|
views = append(views, c.ToView())
|
||||||
|
}
|
||||||
|
return views, nil
|
||||||
|
}
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"nysoure/server/dao"
|
"nysoure/server/dao"
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3/log"
|
"github.com/gofiber/fiber/v3/log"
|
||||||
@@ -22,39 +21,6 @@ type CommentRequest struct {
|
|||||||
// Images []uint `json:"images"` // Unrequired after new design
|
// Images []uint `json:"images"` // Unrequired after new design
|
||||||
}
|
}
|
||||||
|
|
||||||
func findImagesInContent(content string, host string) []uint {
|
|
||||||
// Handle both absolute and relative URLs
|
|
||||||
absolutePattern := `!\[.*?\]\((?:https?://` + host + `)?/api/image/(\d+)(?:\s+["'].*?["'])?\)`
|
|
||||||
relativePattern := `!\[.*?\]\(/api/image/(\d+)(?:\s+["'].*?["'])?\)`
|
|
||||||
|
|
||||||
// Combine patterns and compile regex
|
|
||||||
patterns := []string{absolutePattern, relativePattern}
|
|
||||||
|
|
||||||
// Store unique image IDs to avoid duplicates
|
|
||||||
imageIDs := make(map[uint]struct{})
|
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
re := regexp.MustCompile(pattern)
|
|
||||||
matches := re.FindAllStringSubmatch(content, -1)
|
|
||||||
|
|
||||||
for _, match := range matches {
|
|
||||||
if len(match) >= 2 {
|
|
||||||
if id, err := strconv.ParseUint(match[1], 10, 32); err == nil {
|
|
||||||
imageIDs[uint(id)] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert map keys to slice
|
|
||||||
result := make([]uint, 0, len(imageIDs))
|
|
||||||
for id := range imageIDs {
|
|
||||||
result = append(result, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType model.CommentType, host string) (*model.CommentView, error) {
|
func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType model.CommentType, host string) (*model.CommentView, error) {
|
||||||
if len(req.Content) == 0 {
|
if len(req.Content) == 0 {
|
||||||
return nil, model.NewRequestError("Content cannot be empty")
|
return nil, model.NewRequestError("Content cannot be empty")
|
||||||
|
@@ -6,6 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"nysoure/server/config"
|
"nysoure/server/config"
|
||||||
"nysoure/server/dao"
|
"nysoure/server/dao"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkUserCanUpload(uid uint) (bool, error) {
|
func checkUserCanUpload(uid uint) (bool, error) {
|
||||||
@@ -58,3 +60,36 @@ func verifyCfToken(cfToken string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findImagesInContent(content string, host string) []uint {
|
||||||
|
// Handle both absolute and relative URLs
|
||||||
|
absolutePattern := `!\[.*?\]\((?:https?://` + host + `)?/api/image/(\d+)(?:\s+["'].*?["'])?\)`
|
||||||
|
relativePattern := `!\[.*?\]\(/api/image/(\d+)(?:\s+["'].*?["'])?\)`
|
||||||
|
|
||||||
|
// Combine patterns and compile regex
|
||||||
|
patterns := []string{absolutePattern, relativePattern}
|
||||||
|
|
||||||
|
// Store unique image IDs to avoid duplicates
|
||||||
|
imageIDs := make(map[uint]struct{})
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
matches := re.FindAllStringSubmatch(content, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) >= 2 {
|
||||||
|
if id, err := strconv.ParseUint(match[1], 10, 32); err == nil {
|
||||||
|
imageIDs[uint(id)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map keys to slice
|
||||||
|
result := make([]uint, 0, len(imageIDs))
|
||||||
|
for id := range imageIDs {
|
||||||
|
result = append(result, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user