Files
backend/internal/handler/post_handler.go
lan 86ef150fec Replace websocket flow with SSE support in backend.
Update handlers, services, router, and data conversion logic to support server-sent events and related message pipeline changes.

Made-with: Cursor
2026-03-10 12:58:23 +08:00

511 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"errors"
"fmt"
"strconv"
"github.com/gin-gonic/gin"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
)
// PostHandler 帖子处理器
type PostHandler struct {
postService *service.PostService
userService *service.UserService
}
// NewPostHandler 创建帖子处理器
func NewPostHandler(postService *service.PostService, userService *service.UserService) *PostHandler {
return &PostHandler{
postService: postService,
userService: userService,
}
}
// Create 创建帖子
func (h *PostHandler) Create(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
type CreateRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Images []string `json:"images"`
}
var req CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
post, err := h.postService.Create(c.Request.Context(), userID, req.Title, req.Content, req.Images)
if err != nil {
var moderationErr *service.PostModerationRejectedError
if errors.As(err, &moderationErr) {
response.BadRequest(c, moderationErr.UserMessage())
return
}
response.InternalServerError(c, "failed to create post")
return
}
response.Success(c, dto.ConvertPostToResponse(post, false, false))
}
// GetByID 获取帖子(不增加浏览量)
func (h *PostHandler) GetByID(c *gin.Context) {
id := c.Param("id")
post, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.NotFound(c, "post not found")
return
}
// 非作者不可查看未发布内容
currentUserID := c.GetString("user_id")
if post.Status != model.PostStatusPublished && post.UserID != currentUserID {
response.NotFound(c, "post not found")
return
}
// 注意:不再自动增加浏览量,浏览量通过 RecordView 端点单独记录
var isLiked, isFavorited bool
if currentUserID != "" {
isLiked = h.postService.IsLiked(c.Request.Context(), id, currentUserID)
isFavorited = h.postService.IsFavorited(c.Request.Context(), id, currentUserID)
}
// 如果有当前用户,检查与帖子作者的相互关注状态
var authorWithFollowStatus *dto.UserResponse
if currentUserID != "" && post.User != nil {
_, isFollowing, isFollowingMe, err := h.userService.GetUserByIDWithMutualFollowStatus(c.Request.Context(), post.UserID, currentUserID)
if err == nil {
authorWithFollowStatus = dto.ConvertUserToResponseWithMutualFollow(post.User, isFollowing, isFollowingMe)
} else {
// 如果出错使用默认的author
authorWithFollowStatus = dto.ConvertUserToResponse(post.User)
}
}
// 构建响应
responseData := &dto.PostResponse{
ID: post.ID,
UserID: post.UserID,
Title: post.Title,
Content: post.Content,
Images: dto.ConvertPostImagesToResponse(post.Images),
Status: string(post.Status),
LikesCount: post.LikesCount,
CommentsCount: post.CommentsCount,
FavoritesCount: post.FavoritesCount,
SharesCount: post.SharesCount,
ViewsCount: post.ViewsCount,
IsPinned: post.IsPinned,
IsLocked: post.IsLocked,
IsVote: post.IsVote,
CreatedAt: dto.FormatTime(post.CreatedAt),
UpdatedAt: dto.FormatTime(post.UpdatedAt),
Author: authorWithFollowStatus,
IsLiked: isLiked,
IsFavorited: isFavorited,
}
response.Success(c, responseData)
}
// RecordView 记录帖子浏览(增加浏览量)
func (h *PostHandler) RecordView(c *gin.Context) {
id := c.Param("id")
userID := c.GetString("user_id")
// 验证帖子存在
_, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.NotFound(c, "post not found")
return
}
// 增加浏览量
if err := h.postService.IncrementViews(c.Request.Context(), id, userID); err != nil {
fmt.Printf("[ERROR] Failed to increment views for post %s: %v\n", id, err)
response.InternalServerError(c, "failed to record view")
return
}
response.Success(c, gin.H{"success": true})
}
// List 获取帖子列表
func (h *PostHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
userID := c.Query("user_id")
tab := c.Query("tab") // recommend, follow, hot, latest
// 获取当前用户ID
currentUserID := c.GetString("user_id")
var posts []*model.Post
var total int64
var err error
// 根据 tab 参数选择不同的获取方式
switch tab {
case "follow":
// 获取关注用户的帖子,需要登录
if currentUserID == "" {
response.Unauthorized(c, "请先登录")
return
}
posts, total, err = h.postService.GetFollowingPosts(c.Request.Context(), currentUserID, page, pageSize)
case "hot":
// 获取热门帖子
posts, total, err = h.postService.GetHotPosts(c.Request.Context(), page, pageSize)
case "recommend":
// 推荐帖子从Gorse获取个性化推荐
posts, total, err = h.postService.GetRecommendedPosts(c.Request.Context(), currentUserID, page, pageSize)
case "latest":
// 最新帖子
if userID != "" && userID == currentUserID {
posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID)
} else {
posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID)
}
default:
// 默认获取最新帖子
if userID != "" && userID == currentUserID {
posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID)
} else {
posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID)
}
}
if err != nil {
response.InternalServerError(c, "failed to get posts")
return
}
isLikedMap := make(map[string]bool)
isFavoritedMap := make(map[string]bool)
if currentUserID != "" {
for _, post := range posts {
isLiked := h.postService.IsLiked(c.Request.Context(), post.ID, currentUserID)
isFavorited := h.postService.IsFavorited(c.Request.Context(), post.ID, currentUserID)
isLikedMap[post.ID] = isLiked
isFavoritedMap[post.ID] = isFavorited
}
}
// 转换为响应结构
postResponses := dto.ConvertPostsToResponse(posts, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// Update 更新帖子
func (h *PostHandler) Update(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
post, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.NotFound(c, "post not found")
return
}
if post.UserID != userID {
response.Forbidden(c, "cannot update others' post")
return
}
type UpdateRequest struct {
Title string `json:"title"`
Content string `json:"content"`
Images *[]string `json:"images"`
}
var req UpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if req.Title != "" {
post.Title = req.Title
}
if req.Content != "" {
post.Content = req.Content
}
err = h.postService.UpdateWithImages(c.Request.Context(), post, req.Images)
if err != nil {
response.InternalServerError(c, "failed to update post")
return
}
post, err = h.postService.GetByID(c.Request.Context(), post.ID)
if err != nil {
response.InternalServerError(c, "failed to get updated post")
return
}
currentUserID := c.GetString("user_id")
var isLiked, isFavorited bool
if currentUserID != "" {
isLiked = h.postService.IsLiked(c.Request.Context(), post.ID, currentUserID)
isFavorited = h.postService.IsFavorited(c.Request.Context(), post.ID, currentUserID)
}
response.Success(c, dto.ConvertPostToResponse(post, isLiked, isFavorited))
}
// Delete 删除帖子
func (h *PostHandler) Delete(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
post, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.NotFound(c, "post not found")
return
}
if post.UserID != userID {
response.Forbidden(c, "cannot delete others' post")
return
}
err = h.postService.Delete(c.Request.Context(), id)
if err != nil {
response.InternalServerError(c, "failed to delete post")
return
}
response.SuccessWithMessage(c, "post deleted", nil)
}
// Like 点赞帖子
func (h *PostHandler) Like(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
err := h.postService.Like(c.Request.Context(), id, userID)
if err != nil {
response.InternalServerError(c, "failed to like post")
return
}
// 获取更新后的帖子状态
post, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.InternalServerError(c, "failed to get post")
return
}
isLiked := h.postService.IsLiked(c.Request.Context(), id, userID)
isFavorited := h.postService.IsFavorited(c.Request.Context(), id, userID)
response.Success(c, dto.ConvertPostToResponse(post, isLiked, isFavorited))
}
// Unlike 取消点赞
func (h *PostHandler) Unlike(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
err := h.postService.Unlike(c.Request.Context(), id, userID)
if err != nil {
response.InternalServerError(c, "failed to unlike post")
return
}
// 获取更新后的帖子状态
post, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.InternalServerError(c, "failed to get post")
return
}
isLiked := h.postService.IsLiked(c.Request.Context(), id, userID)
isFavorited := h.postService.IsFavorited(c.Request.Context(), id, userID)
response.Success(c, dto.ConvertPostToResponse(post, isLiked, isFavorited))
}
// Favorite 收藏帖子
func (h *PostHandler) Favorite(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
err := h.postService.Favorite(c.Request.Context(), id, userID)
if err != nil {
response.InternalServerError(c, "failed to favorite post")
return
}
// 获取更新后的帖子状态
post, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.InternalServerError(c, "failed to get post")
return
}
isLiked := h.postService.IsLiked(c.Request.Context(), id, userID)
isFavorited := h.postService.IsFavorited(c.Request.Context(), id, userID)
response.Success(c, dto.ConvertPostToResponse(post, isLiked, isFavorited))
}
// Unfavorite 取消收藏
func (h *PostHandler) Unfavorite(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
id := c.Param("id")
err := h.postService.Unfavorite(c.Request.Context(), id, userID)
if err != nil {
response.InternalServerError(c, "failed to unfavorite post")
return
}
// 获取更新后的帖子状态
post, err := h.postService.GetByID(c.Request.Context(), id)
if err != nil {
response.InternalServerError(c, "failed to get post")
return
}
isLiked := h.postService.IsLiked(c.Request.Context(), id, userID)
isFavorited := h.postService.IsFavorited(c.Request.Context(), id, userID)
response.Success(c, dto.ConvertPostToResponse(post, isLiked, isFavorited))
}
// GetUserPosts 获取用户帖子列表
func (h *PostHandler) GetUserPosts(c *gin.Context) {
userID := c.Param("id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
currentUserID := c.GetString("user_id")
includePending := currentUserID != "" && currentUserID == userID
posts, total, err := h.postService.GetUserPosts(c.Request.Context(), userID, page, pageSize, includePending)
if err != nil {
response.InternalServerError(c, "failed to get user posts")
return
}
// 获取当前用户ID用于判断点赞和收藏状态
isLikedMap := make(map[string]bool)
isFavoritedMap := make(map[string]bool)
if currentUserID != "" {
for _, post := range posts {
isLikedMap[post.ID] = h.postService.IsLiked(c.Request.Context(), post.ID, currentUserID)
isFavoritedMap[post.ID] = h.postService.IsFavorited(c.Request.Context(), post.ID, currentUserID)
}
}
// 转换为响应结构
postResponses := dto.ConvertPostsToResponse(posts, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// GetFavorites 获取收藏列表
func (h *PostHandler) GetFavorites(c *gin.Context) {
userID := c.Param("id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
posts, total, err := h.postService.GetFavorites(c.Request.Context(), userID, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to get favorites")
return
}
// 获取当前用户ID用于判断点赞和收藏状态
currentUserID := c.GetString("user_id")
isLikedMap := make(map[string]bool)
isFavoritedMap := make(map[string]bool)
if currentUserID != "" {
for _, post := range posts {
isLikedMap[post.ID] = h.postService.IsLiked(c.Request.Context(), post.ID, currentUserID)
isFavoritedMap[post.ID] = h.postService.IsFavorited(c.Request.Context(), post.ID, currentUserID)
}
}
// 转换为响应结构
postResponses := dto.ConvertPostsToResponse(posts, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// Search 搜索帖子
func (h *PostHandler) Search(c *gin.Context) {
keyword := c.Query("keyword")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
posts, total, err := h.postService.Search(c.Request.Context(), keyword, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to search posts")
return
}
// 获取当前用户ID用于判断点赞和收藏状态
currentUserID := c.GetString("user_id")
isLikedMap := make(map[string]bool)
isFavoritedMap := make(map[string]bool)
if currentUserID != "" {
for _, post := range posts {
isLikedMap[post.ID] = h.postService.IsLiked(c.Request.Context(), post.ID, currentUserID)
isFavoritedMap[post.ID] = h.postService.IsFavorited(c.Request.Context(), post.ID, currentUserID)
}
}
// 转换为响应结构
postResponses := dto.ConvertPostsToResponse(posts, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}