feat(posts): add post reference/internal linking functionality
Add support for referencing other posts within messages through a new post_ref segment type. Includes new PostReference model to track relationships, PostRefService for processing references, and new endpoints for post suggestions, related posts, and reference click tracking.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -19,14 +20,16 @@ import (
|
||||
// PostHandler 帖子处理器
|
||||
type PostHandler struct {
|
||||
postService service.PostService
|
||||
postRefService service.PostRefService
|
||||
userService service.UserService
|
||||
channelService service.ChannelService
|
||||
}
|
||||
|
||||
// NewPostHandler 创建帖子处理器
|
||||
func NewPostHandler(postService service.PostService, userService service.UserService, channelService service.ChannelService) *PostHandler {
|
||||
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,
|
||||
}
|
||||
@@ -113,6 +116,15 @@ func (h *PostHandler) Create(c *gin.Context) {
|
||||
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))
|
||||
}
|
||||
@@ -136,6 +148,14 @@ func (h *PostHandler) GetByID(c *gin.Context) {
|
||||
|
||||
// 注意:不再自动增加浏览量,浏览量通过 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)
|
||||
|
||||
@@ -160,7 +180,7 @@ func (h *PostHandler) GetByID(c *gin.Context) {
|
||||
ChannelID: post.ChannelID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Segments: dto.SegmentsOrDefault(post.Segments, post.Content),
|
||||
Segments: enrichedSegments,
|
||||
Images: dto.ConvertPostImagesToResponse(post.Images),
|
||||
Status: string(post.Status),
|
||||
LikesCount: post.LikesCount,
|
||||
@@ -438,6 +458,15 @@ func (h *PostHandler) Update(c *gin.Context) {
|
||||
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")
|
||||
@@ -479,6 +508,15 @@ func (h *PostHandler) Delete(c *gin.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -828,6 +866,114 @@ func (h *PostHandler) SearchByCursor(c *gin.Context) {
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user