Files
backend/internal/handler/post_handler.go

979 lines
28 KiB
Go
Raw Normal View History

package handler
import (
"context"
"errors"
"fmt"
"log"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"with_you/internal/dto"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/pkg/response"
"with_you/internal/service"
)
// PostHandler 帖子处理器
type PostHandler struct {
postService service.PostService
postRefService service.PostRefService
userService service.UserService
channelService service.ChannelService
}
// NewPostHandler 创建帖子处理器
func NewPostHandler(postService service.PostService, postRefService service.PostRefService, userService service.UserService, channelService service.ChannelService) *PostHandler {
return &PostHandler{
postService: postService,
postRefService: postRefService,
userService: userService,
channelService: channelService,
}
}
// collectAuthorIDs 从帖子列表中收集去重的作者ID
func collectAuthorIDs(posts []*model.Post) []string {
seen := make(map[string]struct{})
ids := make([]string, 0, len(posts))
for _, p := range posts {
if _, ok := seen[p.UserID]; !ok {
seen[p.UserID] = struct{}{}
ids = append(ids, p.UserID)
}
}
return ids
}
// filterBlockedPosts 过滤掉被当前用户拉黑的作者发布的帖子
func (h *PostHandler) filterBlockedPosts(ctx context.Context, posts []*model.Post, currentUserID string) []*model.Post {
if currentUserID == "" || len(posts) == 0 {
return posts
}
authorIDs := collectAuthorIDs(posts)
blockedMap, err := h.userService.IsBlockedBatch(ctx, currentUserID, authorIDs)
if err != nil {
return posts
}
filtered := make([]*model.Post, 0, len(posts))
for _, p := range posts {
if blockedMap[p.UserID] {
continue
}
filtered = append(filtered, p)
}
return filtered
}
// channelMapForPosts 批量解析帖子中的频道 ID用于填充响应里的 channel 名称
func (h *PostHandler) channelMapForPosts(posts []*model.Post) map[string]*model.Channel {
ids := dto.CollectPostChannelIDs(posts)
if len(ids) == 0 {
return nil
}
m, err := h.channelService.MapByIDs(ids)
if err != nil {
return nil
}
return m
}
// Create 创建帖子
func (h *PostHandler) Create(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var req dto.CreatePostWithSegmentsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
content := req.Content
segments := req.Segments
if len(segments) == 0 && content == "" {
response.BadRequest(c, "content or segments is required")
return
}
post, err := h.postService.Create(c.Request.Context(), userID, req.Title, content, segments, req.Images, req.ChannelID)
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
}
if h.postRefService != nil && len(segments) > 0 {
go func() {
bgCtx := context.Background()
if err := h.postRefService.ProcessPostReferences(bgCtx, post.ID, segments); err != nil {
log.Printf("[WARN] process post references failed for post %s: %v", post.ID, err)
}
}()
}
chMap := h.channelMapForPosts([]*model.Post{post})
response.Success(c, dto.ConvertPostToResponse(post, chMap, 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 端点单独记录
// 增强 post_ref segments
enrichedSegments := dto.SegmentsOrDefault(post.Segments, post.Content)
if h.postRefService != nil && len(enrichedSegments) > 0 {
if enriched, err := h.postRefService.EnrichPostRefSegments(c.Request.Context(), enrichedSegments, currentUserID); err == nil {
enrichedSegments = enriched
}
}
// 获取交互状态
isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, currentUserID)
var authorWithFollowStatus *dto.UserResponse
if post.User != nil {
if currentUserID != "" {
_, isFollowing, isFollowingMe, err := h.userService.GetUserByIDWithMutualFollowStatus(c.Request.Context(), post.UserID, currentUserID)
if err == nil {
authorWithFollowStatus = dto.ConvertUserToResponseWithMutualFollow(post.User, isFollowing, isFollowingMe)
} else {
authorWithFollowStatus = dto.ConvertUserToResponse(post.User)
}
} else {
authorWithFollowStatus = dto.ConvertUserToResponse(post.User)
}
}
// 构建响应
responseData := &dto.PostResponse{
ID: post.ID,
UserID: post.UserID,
ChannelID: post.ChannelID,
Title: post.Title,
Content: post.Content,
Segments: enrichedSegments,
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),
ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt),
Author: authorWithFollowStatus,
IsLiked: isLiked,
IsFavorited: isFavorited,
}
chMap := h.channelMapForPosts([]*model.Post{post})
responseData.Channel = dto.PostChannelBriefForPost(post, chMap)
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})
}
// RecordShare 记录帖子分享shares_count +1仅已发布
// 支持可选 JSON{"channel":"wechat"|"qq"|"copy_link"|"system"|...},用于后续统计
func (h *PostHandler) RecordShare(c *gin.Context) {
id := c.Param("id")
_ = c.GetString("user_id") // 预留:登录用户维度统计
type shareBody struct {
Channel string `json:"channel"`
}
var body shareBody
if c.Request.ContentLength > 0 {
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, "invalid json body")
return
}
}
sharesCount, err := h.postService.RecordShare(c.Request.Context(), id, c.GetString("user_id"))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.NotFound(c, "post not found")
return
}
fmt.Printf("[ERROR] RecordShare post %s: %v\n", id, err)
response.InternalServerError(c, "failed to record share")
return
}
data := gin.H{"success": true, "shares_count": sharesCount}
if body.Channel != "" {
data["channel"] = body.Channel
}
response.Success(c, data)
}
// List 获取帖子列表(支持游标分页和偏移分页)
func (h *PostHandler) List(c *gin.Context) {
// 检查是否使用游标分页
// 当 cursor 参数存在时(包括空字符串),使用游标分页
_, cursorExists := c.GetQuery("cursor")
if cursorExists {
h.ListByCursor(c)
return
}
// 偏移分页(向后兼容)
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
userID := c.Query("user_id")
tab := c.Query("tab")
var channelID *string
if cid := c.Query("channel_id"); cid != "" {
channelID = &cid
}
// 获取当前用户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, channelID)
case "hot":
posts, total, err = h.postService.GetHotPosts(c.Request.Context(), page, pageSize, channelID)
case "latest":
if userID != "" && userID == currentUserID {
posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID, channelID)
} else {
posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID, channelID)
}
default:
if userID != "" && userID == currentUserID {
posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID, channelID)
} else {
posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID, channelID)
}
}
if err != nil {
response.InternalServerError(c, "failed to get posts")
return
}
posts = h.filterBlockedPosts(c.Request.Context(), posts, currentUserID)
// 批量获取交互状态
postIDs := make([]string, len(posts))
for i, post := range posts {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(posts)
postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// ListByCursor 游标分页获取帖子列表
func (h *PostHandler) ListByCursor(c *gin.Context) {
userID := c.Query("user_id")
currentUserID := c.GetString("user_id")
tab := c.Query("tab")
var channelID *string
if cid := c.Query("channel_id"); cid != "" {
channelID = &cid
}
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 判断是否包含待审核帖子
includePending := userID != "" && userID == currentUserID
var result *cursor.CursorPageResult[*model.Post]
var err error
// 根据 tab 参数选择不同的获取方式
switch tab {
case "follow":
if currentUserID == "" {
response.Unauthorized(c, "请先登录")
return
}
result, err = h.postService.GetFollowingPostsByCursor(c.Request.Context(), currentUserID, channelID, req)
case "hot":
result, err = h.postService.GetHotPostsByCursor(c.Request.Context(), channelID, req)
case "latest":
// 最新帖子
result, err = h.postService.ListByCursor(c.Request.Context(), userID, includePending, channelID, req)
default:
// 默认获取最新帖子
result, err = h.postService.ListByCursor(c.Request.Context(), userID, includePending, channelID, req)
}
if err != nil {
response.InternalServerError(c, "failed to get posts")
return
}
result.Items = h.filterBlockedPosts(c.Request.Context(), result.Items, currentUserID)
// 批量获取交互状态
postIDs := make([]string, len(result.Items))
for i, post := range result.Items {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(result.Items)
postResponses := dto.BuildPostsWithInteractionResponse(result.Items, chMap, isLikedMap, isFavoritedMap)
// 构建游标分页响应
cursorResp := &dto.PostCursorPageResponse{
Items: postResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// 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"`
Segments model.MessageSegments `json:"segments"`
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
}
if len(req.Segments) > 0 {
post.Segments = req.Segments
if req.Content == "" {
post.Content = dto.ExtractTextContent(req.Segments)
}
}
err = h.postService.UpdateWithImages(c.Request.Context(), post, req.Images)
if err != nil {
response.InternalServerError(c, "failed to update post")
return
}
if h.postRefService != nil && len(req.Segments) > 0 {
go func() {
bgCtx := context.Background()
if err := h.postRefService.ProcessPostReferences(bgCtx, post.ID, post.Segments); err != nil {
log.Printf("[WARN] process post references failed for post %s: %v", post.ID, err)
}
}()
}
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")
isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), post.ID, currentUserID)
chMap := h.channelMapForPosts([]*model.Post{post})
response.Success(c, dto.BuildPostResponse(post, chMap, 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(), userID, id)
if err != nil {
response.HandleError(c, err, "failed to delete post")
return
}
if h.postRefService != nil {
go func() {
bgCtx := context.Background()
if err := h.postRefService.HandlePostDeleted(bgCtx, id); err != nil {
log.Printf("[WARN] mark dead post references failed for post %s: %v", id, err)
}
}()
}
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, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID)
chMap := h.channelMapForPosts([]*model.Post{post})
response.Success(c, dto.BuildPostResponse(post, chMap, 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, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID)
chMap := h.channelMapForPosts([]*model.Post{post})
response.Success(c, dto.BuildPostResponse(post, chMap, 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, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID)
chMap := h.channelMapForPosts([]*model.Post{post})
response.Success(c, dto.BuildPostResponse(post, chMap, 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, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID)
chMap := h.channelMapForPosts([]*model.Post{post})
response.Success(c, dto.BuildPostResponse(post, chMap, isLiked, isFavorited))
}
// GetUserPosts 获取用户帖子列表(支持游标分页和偏移分页)
func (h *PostHandler) GetUserPosts(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
canView, err := h.userService.CanViewPosts(c.Request.Context(), currentUserID, userID)
if err != nil {
response.InternalServerError(c, "failed to check privacy settings")
return
}
if !canView {
response.Forbidden(c, "该用户的帖子列表不对外公开")
return
}
// 检查是否拉黑了对方
if currentUserID != "" && currentUserID != userID {
isBlocked, _ := h.userService.IsBlocked(c.Request.Context(), currentUserID, userID)
if isBlocked {
// 检查是否使用游标分页
cursorStr := c.Query("cursor")
if cursorStr != "" {
response.Success(c, &dto.PostCursorPageResponse{
Items: []*dto.PostResponse{},
HasMore: false,
IsBlocked: true,
})
} else {
response.Success(c, gin.H{
"list": []*dto.PostResponse{},
"total": 0,
"page": 1,
"page_size": 20,
"total_pages": 0,
"is_blocked": true,
})
}
return
}
}
// 检查是否使用游标分页
cursorStr := c.Query("cursor")
if cursorStr != "" {
h.GetUserPostsByCursor(c)
return
}
// 偏移分页(向后兼容)
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
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
}
// 批量获取交互状态
postIDs := make([]string, len(posts))
for i, post := range posts {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(posts)
postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// GetUserPostsByCursor 游标分页获取用户帖子列表
func (h *PostHandler) GetUserPostsByCursor(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 判断是否包含待审核帖子
includePending := currentUserID != "" && currentUserID == userID
// 调用游标分页服务
result, err := h.postService.GetUserPostsByCursor(c.Request.Context(), userID, includePending, req)
if err != nil {
response.InternalServerError(c, "failed to get user posts")
return
}
// 批量获取交互状态
postIDs := make([]string, len(result.Items))
for i, post := range result.Items {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(result.Items)
postResponses := dto.BuildPostsWithInteractionResponse(result.Items, chMap, isLikedMap, isFavoritedMap)
// 构建游标分页响应
cursorResp := &dto.PostCursorPageResponse{
Items: postResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// GetFavorites 获取收藏列表
func (h *PostHandler) GetFavorites(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
canView, err := h.userService.CanViewFavorites(c.Request.Context(), currentUserID, userID)
if err != nil {
response.InternalServerError(c, "failed to check privacy settings")
return
}
if !canView {
response.Forbidden(c, "该用户的收藏列表不对外公开")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
posts, total, err := h.postService.GetFavorites(c.Request.Context(), userID, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to get favorites")
return
}
postIDs := make([]string, len(posts))
for i, post := range posts {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(posts)
postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// Search 搜索帖子
func (h *PostHandler) Search(c *gin.Context) {
keyword := c.Query("keyword")
// 检查是否使用游标分页
cursorStr := c.Query("cursor")
if cursorStr != "" {
h.SearchByCursor(c)
return
}
// 偏移分页(向后兼容)
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
posts, total, err := h.postService.Search(c.Request.Context(), keyword, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to search posts")
return
}
// 批量获取交互状态
currentUserID := c.GetString("user_id")
posts = h.filterBlockedPosts(c.Request.Context(), posts, currentUserID)
postIDs := make([]string, len(posts))
for i, post := range posts {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(posts)
postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// SearchByCursor 游标分页搜索帖子
func (h *PostHandler) SearchByCursor(c *gin.Context) {
keyword := c.Query("keyword")
currentUserID := c.GetString("user_id")
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 调用游标分页服务
result, err := h.postService.SearchByCursor(c.Request.Context(), keyword, req)
if err != nil {
response.InternalServerError(c, "failed to search posts")
return
}
result.Items = h.filterBlockedPosts(c.Request.Context(), result.Items, currentUserID)
// 批量获取交互状态
postIDs := make([]string, len(result.Items))
for i, post := range result.Items {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(result.Items)
postResponses := dto.BuildPostsWithInteractionResponse(result.Items, chMap, isLikedMap, isFavoritedMap)
// 构建游标分页响应
cursorResp := &dto.PostCursorPageResponse{
Items: postResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// RecordRefClick 记录内链点击
func (h *PostHandler) RecordRefClick(c *gin.Context) {
id := c.Param("id")
type RefClickRequest struct {
TargetPostID string `json:"target_post_id" binding:"required"`
}
var req RefClickRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "target_post_id is required")
return
}
if h.postRefService != nil {
if err := h.postRefService.RecordClick(c.Request.Context(), id, req.TargetPostID); err != nil {
log.Printf("[WARN] record ref click failed: %v", err)
}
}
response.Success(c, gin.H{"success": true})
}
// GetRelatedPosts 获取引用当前帖子的相关帖子
func (h *PostHandler) GetRelatedPosts(c *gin.Context) {
id := c.Param("id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
if h.postRefService == nil {
response.Paginated(c, []*dto.PostResponse{}, 0, page, pageSize)
return
}
posts, total, err := h.postRefService.GetRelatedPosts(c.Request.Context(), id, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to get related posts")
return
}
currentUserID := c.GetString("user_id")
posts = h.filterBlockedPosts(c.Request.Context(), posts, currentUserID)
postIDs := make([]string, len(posts))
for i, p := range posts {
postIDs[i] = p.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(posts)
postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap)
response.Paginated(c, postResponses, total, page, pageSize)
}
// GetReferencedPosts 获取当前帖子引用的目标帖子
func (h *PostHandler) GetReferencedPosts(c *gin.Context) {
id := c.Param("id")
if h.postRefService == nil {
response.Success(c, []*dto.PostResponse{})
return
}
posts, err := h.postRefService.GetReferencedPosts(c.Request.Context(), id)
if err != nil {
response.InternalServerError(c, "failed to get referenced posts")
return
}
currentUserID := c.GetString("user_id")
posts = h.filterBlockedPosts(c.Request.Context(), posts, currentUserID)
postIDs := make([]string, len(posts))
for i, p := range posts {
postIDs[i] = p.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
chMap := h.channelMapForPosts(posts)
postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap)
response.Success(c, postResponses)
}
// Suggest 帖子搜索联想
func (h *PostHandler) Suggest(c *gin.Context) {
q := c.Query("q")
if q == "" {
response.Success(c, []dto.PostRefBrief{})
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limit <= 0 || limit > 50 {
limit = 10
}
briefs, err := h.postRefService.SuggestPosts(c.Request.Context(), q, limit)
if err != nil {
response.InternalServerError(c, "failed to search posts")
return
}
response.Success(c, briefs)
}
// parseCursorRequest 解析游标分页请求参数
func (h *PostHandler) 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)
}