Update handlers, services, router, and data conversion logic to support server-sent events and related message pipeline changes. Made-with: Cursor
511 lines
14 KiB
Go
511 lines
14 KiB
Go
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)
|
||
}
|