diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index 5099f8f..acd8931 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -571,13 +571,13 @@ class Network { return `${this.apiBaseUrl}/files/download/${fileId}?cf_token=${cfToken}`; } - async createComment( + async createResourceComment( resourceID: number, content: string, images: number[], ): Promise> { return this._callApi(() => - axios.post(`${this.apiBaseUrl}/comments/${resourceID}`, { + axios.post(`${this.apiBaseUrl}/comments/resource/${resourceID}`, { content, images, }), @@ -597,12 +597,12 @@ class Network { ); } - async listComments( + async listResourceComments( resourceID: number, page: number = 1, ): Promise> { return this._callApi(() => - axios.get(`${this.apiBaseUrl}/comments/${resourceID}`, { + axios.get(`${this.apiBaseUrl}/comments/resource/${resourceID}`, { params: { page }, }), ); diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index ac79a11..9b38e63 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -1221,7 +1221,7 @@ function CommentInput({ return; } } - const res = await network.createComment( + const res = await network.createResourceComment( resourceId, commentContent, imageIds, @@ -1339,7 +1339,7 @@ function CommentsList({ const [comments, setComments] = useState(null); useEffect(() => { - network.listComments(resourceId, page).then((res) => { + network.listResourceComments(resourceId, page).then((res) => { if (res.success) { setComments(res.data!); maxPageCallback(res.totalPages || 1); diff --git a/server/api/comment.go b/server/api/comment.go index bb57222..f77e805 100644 --- a/server/api/comment.go +++ b/server/api/comment.go @@ -11,14 +11,16 @@ import ( func AddCommentRoutes(router fiber.Router) { api := router.Group("/comments") - api.Post("/:resourceID", createComment) - api.Get("/:resourceID", listComments) - api.Get("/user/:username", listCommentsWithUser) + api.Post("/resource/:resourceID", createResourceComment) + api.Post("/reply/:commentID", createReplyComment) + api.Get("/resource/:resourceID", listResourceComments) + api.Get("/reply/:commentID", listResourceComments) + api.Get("/user/:username", listCommentsByUser) api.Put("/:commentID", updateComment) api.Delete("/:commentID", deleteComment) } -func createComment(c fiber.Ctx) error { +func createResourceComment(c fiber.Ctx) error { userID, ok := c.Locals("uid").(uint) if !ok { return model.NewRequestError("You must be logged in to comment") @@ -38,7 +40,7 @@ func createComment(c fiber.Ctx) error { return model.NewRequestError("Content cannot be empty") } - comment, err := service.CreateComment(req, userID, uint(resourceID), c.IP()) + comment, err := service.CreateComment(req, userID, uint(resourceID), c.IP(), model.CommentTypeResource) if err != nil { return err } @@ -49,7 +51,38 @@ func createComment(c fiber.Ctx) error { }) } -func listComments(c fiber.Ctx) error { +func createReplyComment(c fiber.Ctx) error { + userID, ok := c.Locals("uid").(uint) + if !ok { + return model.NewRequestError("You must be logged in to reply") + } + commentIDStr := c.Params("commentID") + commentID, err := strconv.Atoi(commentIDStr) + if err != nil { + return model.NewRequestError("Invalid comment ID") + } + + var req service.CommentRequest + if err := c.Bind().JSON(&req); err != nil { + return model.NewRequestError("Invalid request format") + } + + if req.Content == "" { + return model.NewRequestError("Content cannot be empty") + } + + comment, err := service.CreateComment(req, userID, uint(commentID), c.IP(), model.CommentTypeReply) + if err != nil { + return err + } + return c.Status(fiber.StatusCreated).JSON(model.Response[model.CommentView]{ + Success: true, + Data: *comment, + Message: "Reply created successfully", + }) +} + +func listResourceComments(c fiber.Ctx) error { resourceIDStr := c.Params("resourceID") resourceID, err := strconv.Atoi(resourceIDStr) if err != nil { @@ -60,7 +93,7 @@ func listComments(c fiber.Ctx) error { if err != nil { return model.NewRequestError("Invalid page number") } - comments, totalPages, err := service.ListComments(uint(resourceID), page) + comments, totalPages, err := service.ListResourceComments(uint(resourceID), page) if err != nil { return err } @@ -72,7 +105,7 @@ func listComments(c fiber.Ctx) error { }) } -func listCommentsWithUser(c fiber.Ctx) error { +func listCommentsByUser(c fiber.Ctx) error { username := c.Params("username") if username == "" { return model.NewRequestError("Username is required") diff --git a/server/dao/comment.go b/server/dao/comment.go index 34a91f5..fc725fc 100644 --- a/server/dao/comment.go +++ b/server/dao/comment.go @@ -6,13 +6,14 @@ import ( "gorm.io/gorm" ) -func CreateComment(content string, userID uint, resourceID uint, imageIDs []uint) (model.Comment, error) { +func CreateComment(content string, userID uint, resourceID uint, imageIDs []uint, cType model.CommentType) (model.Comment, error) { var comment model.Comment err := db.Transaction(func(tx *gorm.DB) error { comment = model.Comment{ - Content: content, - UserID: userID, - ResourceID: resourceID, + Content: content, + UserID: userID, + RefID: resourceID, + Type: cType, } if err := tx.Create(&comment).Error; err != nil { return err @@ -49,11 +50,24 @@ func GetCommentByResourceID(resourceID uint, page, pageSize int) ([]model.Commen var comments []model.Comment var total int64 - if err := db.Model(&model.Comment{}).Where("resource_id = ?", resourceID).Count(&total).Error; err != nil { + if err := db. + Model(&model.Comment{}). + Where("type = ?", model.CommentTypeResource). + Where("ref_id = ?", resourceID). + Count(&total).Error; err != nil { return nil, 0, err } - if err := db.Where("resource_id = ?", resourceID).Offset((page - 1) * pageSize).Limit(pageSize).Preload("User").Preload("Images").Order("created_at DESC").Find(&comments).Error; err != nil { + if err := db. + Model(&model.Comment{}). + Where("type = ?", model.CommentTypeResource). + Where("ref_id = ?", resourceID). + Offset((page - 1) * pageSize). + Limit(pageSize). + Preload("User"). + Preload("Images"). + Order("created_at DESC"). + Find(&comments).Error; err != nil { return nil, 0, err } @@ -70,10 +84,22 @@ func GetCommentsWithUser(username string, page, pageSize int) ([]model.Comment, } var comments []model.Comment var total int64 - if err := db.Model(&model.Comment{}).Where("user_id = ?", user.ID).Count(&total).Error; err != nil { + if err := db. + Model(&model.Comment{}). + Where("type = ?", model.CommentTypeResource). + Where("user_id = ?", user.ID). + Count(&total).Error; err != nil { return nil, 0, err } - if err := db.Where("user_id = ?", user.ID).Offset((page - 1) * pageSize).Limit(pageSize).Preload("User").Preload("Resource").Preload("Images").Order("created_at DESC").Find(&comments).Error; err != nil { + if err := db. + Model(&model.Comment{}). + Where("type = ?", model.CommentTypeResource). + Where("user_id = ?", user.ID). + Offset((page - 1) * pageSize). + Limit(pageSize).Preload("User"). + Preload("Images"). + Order("created_at DESC"). + Find(&comments).Error; err != nil { return nil, 0, err } totalPages := (int(total) + pageSize - 1) / pageSize @@ -82,7 +108,7 @@ func GetCommentsWithUser(username string, page, pageSize int) ([]model.Comment, func GetCommentByID(commentID uint) (*model.Comment, error) { var comment model.Comment - if err := db.Preload("User").Preload("Resource").Preload("Images").First(&comment, commentID).Error; err != nil { + if err := db.Preload("User").Preload("Images").First(&comment, commentID).Error; err != nil { return nil, err } return &comment, nil diff --git a/server/model/comment.go b/server/model/comment.go index bcb11e8..af22522 100644 --- a/server/model/comment.go +++ b/server/model/comment.go @@ -8,14 +8,21 @@ import ( type Comment struct { gorm.Model - Content string `gorm:"not null"` - ResourceID uint `gorm:"not null"` - UserID uint `gorm:"not null"` - User User `gorm:"foreignKey:UserID"` - Resource Resource `gorm:"foreignKey:ResourceID"` - Images []Image `gorm:"many2many:comment_images;"` + Content string `gorm:"not null"` + RefID uint `gorm:"not null;index:idx_refid_type,priority:1"` + Type CommentType `gorm:"not null;index:idx_refid_type,priority:2"` + UserID uint `gorm:"not null"` + User User `gorm:"foreignKey:UserID"` + Images []Image `gorm:"many2many:comment_images;"` } +type CommentType uint + +const ( + CommentTypeResource CommentType = iota + 1 + CommentTypeReply +) + type CommentView struct { ID uint `json:"id"` Content string `json:"content"` @@ -48,7 +55,7 @@ type CommentWithResourceView struct { Images []ImageView `json:"images"` } -func (c *Comment) ToViewWithResource() *CommentWithResourceView { +func (c *Comment) ToViewWithResource(r *Resource) *CommentWithResourceView { imageViews := make([]ImageView, 0, len(c.Images)) for _, img := range c.Images { imageViews = append(imageViews, img.ToView()) @@ -58,7 +65,7 @@ func (c *Comment) ToViewWithResource() *CommentWithResourceView { ID: c.ID, Content: c.Content, CreatedAt: c.CreatedAt, - Resource: c.Resource.ToView(), + Resource: r.ToView(), User: c.User.ToView(), Images: imageViews, } diff --git a/server/service/activity.go b/server/service/activity.go index 713c7f6..18f0eaa 100644 --- a/server/service/activity.go +++ b/server/service/activity.go @@ -27,7 +27,11 @@ func GetActivityList(page int) ([]model.ActivityView, int, error) { if err != nil { return nil, 0, err } - comment = c.ToViewWithResource() + r, err := dao.GetResourceByID(c.RefID) + if err != nil { + return nil, 0, err + } + comment = c.ToViewWithResource(&r) } else if activity.Type == model.ActivityTypeNewResource || activity.Type == model.ActivityTypeUpdateResource { r, err := dao.GetResourceByID(activity.RefID) if err != nil { diff --git a/server/service/comment.go b/server/service/comment.go index fdd1bf0..ef56f80 100644 --- a/server/service/comment.go +++ b/server/service/comment.go @@ -24,7 +24,7 @@ type CommentRequest struct { Images []uint `json:"images"` } -func CreateComment(req CommentRequest, userID uint, resourceID uint, ip string) (*model.CommentView, error) { +func CreateComment(req CommentRequest, userID uint, refID uint, ip string, cType model.CommentType) (*model.CommentView, error) { if !commentsLimiter.AllowRequest(ip) { log.Warnf("IP %s has exceeded the comment limit of %d comments per day", ip, maxCommentsPerIP) return nil, model.NewRequestError("Too many comments from this IP address, please try again later") @@ -40,7 +40,7 @@ func CreateComment(req CommentRequest, userID uint, resourceID uint, ip string) if len(req.Images) > maxImagePerComment { return nil, model.NewRequestError("Too many images, maximum is 9") } - resourceExists, err := dao.ExistsResource(resourceID) + resourceExists, err := dao.ExistsResource(refID) if err != nil { log.Error("Error checking resource existence:", err) return nil, model.NewInternalServerError("Error checking resource existence") @@ -56,7 +56,7 @@ func CreateComment(req CommentRequest, userID uint, resourceID uint, ip string) if !userExists { return nil, model.NewNotFoundError("User not found") } - c, err := dao.CreateComment(req.Content, userID, resourceID, req.Images) + c, err := dao.CreateComment(req.Content, userID, refID, req.Images, cType) if err != nil { log.Error("Error creating comment:", err) return nil, model.NewInternalServerError("Error creating comment") @@ -68,7 +68,7 @@ func CreateComment(req CommentRequest, userID uint, resourceID uint, ip string) return c.ToView(), nil } -func ListComments(resourceID uint, page int) ([]model.CommentView, int, error) { +func ListResourceComments(resourceID uint, page int) ([]model.CommentView, int, error) { resourceExists, err := dao.ExistsResource(resourceID) if err != nil { log.Error("Error checking resource existence:", err) @@ -97,7 +97,12 @@ func ListCommentsWithUser(username string, page int) ([]model.CommentWithResourc } res := make([]model.CommentWithResourceView, 0, len(comments)) for _, c := range comments { - res = append(res, *c.ToViewWithResource()) + r, err := dao.GetResourceByID(c.RefID) + if err != nil { + log.Error("Error getting resource for comment:", err) + return nil, 0, model.NewInternalServerError("Error getting resource for comment") + } + res = append(res, *c.ToViewWithResource(&r)) } return res, totalPages, nil }