2026-04-23 22:29:34 +08:00
|
|
|
|
package handler
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
|
2026-04-22 16:01:59 +08:00
|
|
|
|
"with_you/internal/dto"
|
2026-04-23 22:29:34 +08:00
|
|
|
|
"with_you/internal/model"
|
2026-04-22 16:01:59 +08:00
|
|
|
|
"with_you/internal/pkg/cursor"
|
|
|
|
|
|
"with_you/internal/pkg/response"
|
|
|
|
|
|
"with_you/internal/service"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// CommentHandler 评论处理器
|
|
|
|
|
|
type CommentHandler struct {
|
|
|
|
|
|
commentService *service.CommentService
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewCommentHandler 创建评论处理器
|
|
|
|
|
|
func NewCommentHandler(commentService *service.CommentService) *CommentHandler {
|
|
|
|
|
|
return &CommentHandler{
|
|
|
|
|
|
commentService: commentService,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create 创建评论
|
|
|
|
|
|
func (h *CommentHandler) Create(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
response.Unauthorized(c, "")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type CreateRequest struct {
|
2026-04-23 22:29:34 +08:00
|
|
|
|
PostID string `json:"post_id" binding:"required"`
|
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
|
Segments model.MessageSegments `json:"segments"`
|
|
|
|
|
|
ParentID *string `json:"parent_id"`
|
|
|
|
|
|
Images []string `json:"images"`
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req CreateRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
response.BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:29:34 +08:00
|
|
|
|
// 验证:评论必须有内容或图片或segments
|
|
|
|
|
|
if req.Content == "" && len(req.Images) == 0 && len(req.Segments) == 0 {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
response.BadRequest(c, "评论内容或图片不能同时为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:29:34 +08:00
|
|
|
|
// 如果没有content但有segments,从segments提取文本
|
|
|
|
|
|
content := req.Content
|
|
|
|
|
|
if content == "" && len(req.Segments) > 0 {
|
|
|
|
|
|
content = dto.ExtractTextContent(req.Segments)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 将图片列表转换为JSON字符串
|
|
|
|
|
|
var imagesJSON string
|
|
|
|
|
|
if len(req.Images) > 0 {
|
|
|
|
|
|
imagesBytes, _ := json.Marshal(req.Images)
|
|
|
|
|
|
imagesJSON = string(imagesBytes)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:29:34 +08:00
|
|
|
|
comment, err := h.commentService.Create(c.Request.Context(), req.PostID, userID, content, req.Segments, req.ParentID, imagesJSON, req.Images)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to create comment")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.Success(c, dto.ConvertCommentToResponse(comment, false))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByID 获取单条评论详情
|
|
|
|
|
|
func (h *CommentHandler) GetByID(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
comment, err := h.commentService.GetByID(c.Request.Context(), id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.NotFound(c, "comment not found")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp := dto.ConvertCommentToResponse(comment, h.commentService.IsLiked(c.Request.Context(), id, userID))
|
|
|
|
|
|
response.Success(c, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByPostID 获取帖子评论
|
|
|
|
|
|
func (h *CommentHandler) GetByPostID(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
postID := c.Param("id")
|
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
|
|
|
|
|
|
|
|
comments, total, err := h.commentService.GetByPostID(c.Request.Context(), postID, page, pageSize)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to get comments")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为响应结构,检查每个评论的点赞状态
|
|
|
|
|
|
commentResponses := dto.ConvertCommentsToResponseWithUser(comments, userID, h.commentService)
|
|
|
|
|
|
|
|
|
|
|
|
response.Paginated(c, commentResponses, total, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetReplies 获取回复
|
|
|
|
|
|
func (h *CommentHandler) GetReplies(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
parentID := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
comments, err := h.commentService.GetReplies(c.Request.Context(), parentID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to get replies")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为响应结构,检查每个回复的点赞状态
|
|
|
|
|
|
commentResponses := dto.ConvertCommentsToResponseWithUser(comments, userID, h.commentService)
|
|
|
|
|
|
|
|
|
|
|
|
response.Success(c, commentResponses)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetRepliesByRootID 根据根评论ID分页获取回复(扁平化)
|
|
|
|
|
|
func (h *CommentHandler) GetRepliesByRootID(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
rootID := c.Param("id")
|
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
|
|
|
|
|
|
|
|
|
|
|
replies, total, err := h.commentService.GetRepliesByRootID(c.Request.Context(), rootID, page, pageSize)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to get replies")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为响应结构,检查每个回复的点赞状态
|
|
|
|
|
|
replyResponses := dto.ConvertCommentsToResponseWithUser(replies, userID, h.commentService)
|
|
|
|
|
|
|
|
|
|
|
|
response.Paginated(c, replyResponses, total, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update 更新评论
|
|
|
|
|
|
func (h *CommentHandler) Update(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
response.Unauthorized(c, "")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
comment, err := h.commentService.GetByID(c.Request.Context(), id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.NotFound(c, "comment not found")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if comment.UserID != userID {
|
|
|
|
|
|
response.Forbidden(c, "cannot update others' comment")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type UpdateRequest struct {
|
|
|
|
|
|
Content string `json:"content" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req UpdateRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
response.BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
comment.Content = req.Content
|
|
|
|
|
|
|
|
|
|
|
|
err = h.commentService.Update(c.Request.Context(), comment)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to update comment")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.Success(c, dto.ConvertCommentToResponse(comment, false))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete 删除评论
|
|
|
|
|
|
func (h *CommentHandler) Delete(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
response.Unauthorized(c, "")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
comment, err := h.commentService.GetByID(c.Request.Context(), id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.NotFound(c, "comment not found")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if comment.UserID != userID {
|
|
|
|
|
|
response.Forbidden(c, "cannot delete others' comment")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err = h.commentService.Delete(c.Request.Context(), id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to delete comment")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.SuccessWithMessage(c, "comment deleted", nil)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Like 点赞评论
|
|
|
|
|
|
func (h *CommentHandler) Like(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
response.Unauthorized(c, "")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
err := h.commentService.Like(c.Request.Context(), id, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to like comment")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.SuccessWithMessage(c, "liked", nil)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Unlike 取消点赞评论
|
|
|
|
|
|
func (h *CommentHandler) Unlike(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
response.Unauthorized(c, "")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
err := h.commentService.Unlike(c.Request.Context(), id, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to unlike comment")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.SuccessWithMessage(c, "unliked", nil)
|
|
|
|
|
|
}
|
feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.
Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
2026-03-20 23:03:23 +08:00
|
|
|
|
|
|
|
|
|
|
// GetByPostIDByCursor 游标分页获取帖子评论
|
|
|
|
|
|
func (h *CommentHandler) GetByPostIDByCursor(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
postID := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
// 解析游标分页请求
|
|
|
|
|
|
req := h.parseCursorRequest(c)
|
|
|
|
|
|
|
|
|
|
|
|
// 调用游标分页服务
|
|
|
|
|
|
result, err := h.commentService.GetCommentsByCursor(c.Request.Context(), postID, req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to get comments")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为响应结构,检查每个评论的点赞状态
|
|
|
|
|
|
commentResponses := dto.ConvertCommentsToResponseWithUser(result.Items, userID, h.commentService)
|
|
|
|
|
|
|
|
|
|
|
|
// 构建游标分页响应
|
|
|
|
|
|
cursorResp := &dto.CommentCursorPageResponse{
|
|
|
|
|
|
Items: commentResponses,
|
|
|
|
|
|
NextCursor: result.NextCursor,
|
|
|
|
|
|
PrevCursor: result.PrevCursor,
|
|
|
|
|
|
HasMore: result.HasMore,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.Success(c, cursorResp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetRepliesByRootIDByCursor 游标分页获取根评论的回复
|
|
|
|
|
|
func (h *CommentHandler) GetRepliesByRootIDByCursor(c *gin.Context) {
|
|
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
|
|
rootID := c.Param("id")
|
|
|
|
|
|
|
|
|
|
|
|
// 解析游标分页请求
|
|
|
|
|
|
req := h.parseCursorRequest(c)
|
|
|
|
|
|
|
|
|
|
|
|
// 调用游标分页服务
|
|
|
|
|
|
result, err := h.commentService.GetRepliesByCursor(c.Request.Context(), rootID, req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
response.InternalServerError(c, "failed to get replies")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为响应结构,检查每个回复的点赞状态
|
|
|
|
|
|
replyResponses := dto.ConvertCommentsToResponseWithUser(result.Items, userID, h.commentService)
|
|
|
|
|
|
|
|
|
|
|
|
// 构建游标分页响应
|
|
|
|
|
|
cursorResp := &dto.CommentCursorPageResponse{
|
|
|
|
|
|
Items: replyResponses,
|
|
|
|
|
|
NextCursor: result.NextCursor,
|
|
|
|
|
|
PrevCursor: result.PrevCursor,
|
|
|
|
|
|
HasMore: result.HasMore,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response.Success(c, cursorResp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// parseCursorRequest 解析游标分页请求参数
|
|
|
|
|
|
func (h *CommentHandler) parseCursorRequest(c *gin.Context) *cursor.PageRequest {
|
|
|
|
|
|
cursorStr := c.Query("cursor")
|
|
|
|
|
|
direction := cursor.Direction(c.DefaultQuery("direction", "forward"))
|
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
|
|
|
|
|
|
|
|
return cursor.NewPageRequest(cursorStr, direction, pageSize)
|
|
|
|
|
|
}
|