feat(posts): add post reference/internal linking functionality
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 1m18s

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:
lafay
2026-04-26 00:37:20 +08:00
parent 898c0e6d9c
commit 23d7f1151e
11 changed files with 592 additions and 5 deletions

View File

@@ -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")