Improve Comment model to support multiple comment type.

This commit is contained in:
2025-06-26 20:54:35 +08:00
parent f18465bba3
commit 5d0b201fde
7 changed files with 112 additions and 37 deletions

View File

@@ -571,13 +571,13 @@ class Network {
return `${this.apiBaseUrl}/files/download/${fileId}?cf_token=${cfToken}`; return `${this.apiBaseUrl}/files/download/${fileId}?cf_token=${cfToken}`;
} }
async createComment( async createResourceComment(
resourceID: number, resourceID: number,
content: string, content: string,
images: number[], images: number[],
): Promise<Response<any>> { ): Promise<Response<any>> {
return this._callApi(() => return this._callApi(() =>
axios.post(`${this.apiBaseUrl}/comments/${resourceID}`, { axios.post(`${this.apiBaseUrl}/comments/resource/${resourceID}`, {
content, content,
images, images,
}), }),
@@ -597,12 +597,12 @@ class Network {
); );
} }
async listComments( async listResourceComments(
resourceID: number, resourceID: number,
page: number = 1, page: number = 1,
): Promise<PageResponse<Comment>> { ): Promise<PageResponse<Comment>> {
return this._callApi(() => return this._callApi(() =>
axios.get(`${this.apiBaseUrl}/comments/${resourceID}`, { axios.get(`${this.apiBaseUrl}/comments/resource/${resourceID}`, {
params: { page }, params: { page },
}), }),
); );

View File

@@ -1221,7 +1221,7 @@ function CommentInput({
return; return;
} }
} }
const res = await network.createComment( const res = await network.createResourceComment(
resourceId, resourceId,
commentContent, commentContent,
imageIds, imageIds,
@@ -1339,7 +1339,7 @@ function CommentsList({
const [comments, setComments] = useState<Comment[] | null>(null); const [comments, setComments] = useState<Comment[] | null>(null);
useEffect(() => { useEffect(() => {
network.listComments(resourceId, page).then((res) => { network.listResourceComments(resourceId, page).then((res) => {
if (res.success) { if (res.success) {
setComments(res.data!); setComments(res.data!);
maxPageCallback(res.totalPages || 1); maxPageCallback(res.totalPages || 1);

View File

@@ -11,14 +11,16 @@ import (
func AddCommentRoutes(router fiber.Router) { func AddCommentRoutes(router fiber.Router) {
api := router.Group("/comments") api := router.Group("/comments")
api.Post("/:resourceID", createComment) api.Post("/resource/:resourceID", createResourceComment)
api.Get("/:resourceID", listComments) api.Post("/reply/:commentID", createReplyComment)
api.Get("/user/:username", listCommentsWithUser) api.Get("/resource/:resourceID", listResourceComments)
api.Get("/reply/:commentID", listResourceComments)
api.Get("/user/:username", listCommentsByUser)
api.Put("/:commentID", updateComment) api.Put("/:commentID", updateComment)
api.Delete("/:commentID", deleteComment) api.Delete("/:commentID", deleteComment)
} }
func createComment(c fiber.Ctx) error { func createResourceComment(c fiber.Ctx) error {
userID, ok := c.Locals("uid").(uint) userID, ok := c.Locals("uid").(uint)
if !ok { if !ok {
return model.NewRequestError("You must be logged in to comment") 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") 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 { if err != nil {
return err 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") resourceIDStr := c.Params("resourceID")
resourceID, err := strconv.Atoi(resourceIDStr) resourceID, err := strconv.Atoi(resourceIDStr)
if err != nil { if err != nil {
@@ -60,7 +93,7 @@ func listComments(c fiber.Ctx) error {
if err != nil { if err != nil {
return model.NewRequestError("Invalid page number") 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 { if err != nil {
return err 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") username := c.Params("username")
if username == "" { if username == "" {
return model.NewRequestError("Username is required") return model.NewRequestError("Username is required")

View File

@@ -6,13 +6,14 @@ import (
"gorm.io/gorm" "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 var comment model.Comment
err := db.Transaction(func(tx *gorm.DB) error { err := db.Transaction(func(tx *gorm.DB) error {
comment = model.Comment{ comment = model.Comment{
Content: content, Content: content,
UserID: userID, UserID: userID,
ResourceID: resourceID, RefID: resourceID,
Type: cType,
} }
if err := tx.Create(&comment).Error; err != nil { if err := tx.Create(&comment).Error; err != nil {
return err return err
@@ -49,11 +50,24 @@ func GetCommentByResourceID(resourceID uint, page, pageSize int) ([]model.Commen
var comments []model.Comment var comments []model.Comment
var total int64 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 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 return nil, 0, err
} }
@@ -70,10 +84,22 @@ func GetCommentsWithUser(username string, page, pageSize int) ([]model.Comment,
} }
var comments []model.Comment var comments []model.Comment
var total int64 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 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 return nil, 0, err
} }
totalPages := (int(total) + pageSize - 1) / pageSize 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) { func GetCommentByID(commentID uint) (*model.Comment, error) {
var comment model.Comment 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 nil, err
} }
return &comment, nil return &comment, nil

View File

@@ -8,14 +8,21 @@ import (
type Comment struct { type Comment struct {
gorm.Model gorm.Model
Content string `gorm:"not null"` Content string `gorm:"not null"`
ResourceID uint `gorm:"not null"` RefID uint `gorm:"not null;index:idx_refid_type,priority:1"`
UserID uint `gorm:"not null"` Type CommentType `gorm:"not null;index:idx_refid_type,priority:2"`
User User `gorm:"foreignKey:UserID"` UserID uint `gorm:"not null"`
Resource Resource `gorm:"foreignKey:ResourceID"` User User `gorm:"foreignKey:UserID"`
Images []Image `gorm:"many2many:comment_images;"` Images []Image `gorm:"many2many:comment_images;"`
} }
type CommentType uint
const (
CommentTypeResource CommentType = iota + 1
CommentTypeReply
)
type CommentView struct { type CommentView struct {
ID uint `json:"id"` ID uint `json:"id"`
Content string `json:"content"` Content string `json:"content"`
@@ -48,7 +55,7 @@ type CommentWithResourceView struct {
Images []ImageView `json:"images"` Images []ImageView `json:"images"`
} }
func (c *Comment) ToViewWithResource() *CommentWithResourceView { func (c *Comment) ToViewWithResource(r *Resource) *CommentWithResourceView {
imageViews := make([]ImageView, 0, len(c.Images)) imageViews := make([]ImageView, 0, len(c.Images))
for _, img := range c.Images { for _, img := range c.Images {
imageViews = append(imageViews, img.ToView()) imageViews = append(imageViews, img.ToView())
@@ -58,7 +65,7 @@ func (c *Comment) ToViewWithResource() *CommentWithResourceView {
ID: c.ID, ID: c.ID,
Content: c.Content, Content: c.Content,
CreatedAt: c.CreatedAt, CreatedAt: c.CreatedAt,
Resource: c.Resource.ToView(), Resource: r.ToView(),
User: c.User.ToView(), User: c.User.ToView(),
Images: imageViews, Images: imageViews,
} }

View File

@@ -27,7 +27,11 @@ func GetActivityList(page int) ([]model.ActivityView, int, error) {
if err != nil { if err != nil {
return nil, 0, err 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 { } else if activity.Type == model.ActivityTypeNewResource || activity.Type == model.ActivityTypeUpdateResource {
r, err := dao.GetResourceByID(activity.RefID) r, err := dao.GetResourceByID(activity.RefID)
if err != nil { if err != nil {

View File

@@ -24,7 +24,7 @@ type CommentRequest struct {
Images []uint `json:"images"` 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) { if !commentsLimiter.AllowRequest(ip) {
log.Warnf("IP %s has exceeded the comment limit of %d comments per day", ip, maxCommentsPerIP) 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") 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 { if len(req.Images) > maxImagePerComment {
return nil, model.NewRequestError("Too many images, maximum is 9") return nil, model.NewRequestError("Too many images, maximum is 9")
} }
resourceExists, err := dao.ExistsResource(resourceID) resourceExists, err := dao.ExistsResource(refID)
if err != nil { if err != nil {
log.Error("Error checking resource existence:", err) log.Error("Error checking resource existence:", err)
return nil, model.NewInternalServerError("Error checking resource existence") return nil, model.NewInternalServerError("Error checking resource existence")
@@ -56,7 +56,7 @@ func CreateComment(req CommentRequest, userID uint, resourceID uint, ip string)
if !userExists { if !userExists {
return nil, model.NewNotFoundError("User not found") 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 { if err != nil {
log.Error("Error creating comment:", err) log.Error("Error creating comment:", err)
return nil, model.NewInternalServerError("Error creating comment") 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 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) resourceExists, err := dao.ExistsResource(resourceID)
if err != nil { if err != nil {
log.Error("Error checking resource existence:", err) 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)) res := make([]model.CommentWithResourceView, 0, len(comments))
for _, c := range 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 return res, totalPages, nil
} }