The moderation system now uses a "pass/review" strategy instead of "pass/review/block", treating hard violations the same as review (only routing to manual review rather than auto-rejecting). Add fallback-approve behavior in the hook system so slow moderation APIs never silently block normal content when they time out or get cancelled - timed-out moderation now approves with "ReviewedBy: system" instead of leaving the zero-value result that would reject content. Update post/comment/vote services and handlers to use the new unified behavior. Simplify moderation prompts to emphasize "宁可放过,不可误伤" (prefer false positives over false negatives).
980 lines
28 KiB
Go
980 lines
28 KiB
Go
package handler
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"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, req.ClientRequestID)
|
||
if err != nil {
|
||
// 幂等命中"进行中"占位:提示客户端稍候,避免重复提交
|
||
if errors.Is(err, service.ErrDuplicatePostRequest) {
|
||
response.ErrorWithStatusCode(c, http.StatusTooManyRequests, 429, err.Error())
|
||
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.ConvertUserToResponse(post.User, dto.WithFollowing(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)
|
||
}
|