From 23d7f1151e5cc4fd9464709d4fe15d6986745306 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sun, 26 Apr 2026 00:37:20 +0800 Subject: [PATCH] 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. --- cmd/server/wire_gen.go | 4 +- internal/dto/dto.go | 22 ++- internal/dto/segment.go | 39 +++++ internal/handler/post_handler.go | 150 +++++++++++++++++- internal/model/init.go | 3 + internal/model/post_reference.go | 40 +++++ internal/repository/post_ref_repo.go | 97 ++++++++++++ internal/router/router.go | 7 + internal/service/post_ref_service.go | 222 +++++++++++++++++++++++++++ internal/wire/repository.go | 3 + internal/wire/service.go | 10 ++ 11 files changed, 592 insertions(+), 5 deletions(-) create mode 100644 internal/model/post_reference.go create mode 100644 internal/repository/post_ref_repo.go create mode 100644 internal/service/post_ref_service.go diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index 60ea44f..2ae7d8e 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -67,7 +67,9 @@ func InitializeApp() (*App, error) { postService := wire.ProvidePostService(postRepository, systemMessageService, postAIService, cache, transactionManager, manager, moderationHooks, builtinHooks, logService) channelRepository := repository.NewChannelRepository(db) channelService := wire.ProvideChannelService(channelRepository, cache) - postHandler := handler.NewPostHandler(postService, userService, channelService) + postRefRepository := repository.NewPostRefRepository(db) + postRefService := wire.ProvidePostRefService(postRefRepository, postRepository) + postHandler := handler.NewPostHandler(postService, postRefService, userService, channelService) commentService := wire.ProvideCommentService(commentRepository, postRepository, systemMessageService, postAIService, cache, manager, logService) commentHandler := handler.NewCommentHandler(commentService) s3Client, err := wire.ProvideS3Client(config) diff --git a/internal/dto/dto.go b/internal/dto/dto.go index ec141f0..fe0ef7d 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -250,8 +250,9 @@ const ( SegmentTypeAt SegmentType = "at" SegmentTypeReply SegmentType = "reply" SegmentTypeFace SegmentType = "face" - SegmentTypeLink SegmentType = "link" - SegmentTypeVote SegmentType = "vote" + SegmentTypeLink SegmentType = "link" + SegmentTypeVote SegmentType = "vote" + SegmentTypePostRef SegmentType = "post_ref" ) // TextSegmentData 文本数据 @@ -325,6 +326,23 @@ type VoteOptionData struct { Content string `json:"content"` } +// PostRefSegmentData 帖子内链Segment数据 +type PostRefSegmentData struct { + PostID string `json:"post_id"` + Title string `json:"title,omitempty"` + Author *UserResponse `json:"author,omitempty"` + Status string `json:"status,omitempty"` + Accessible bool `json:"accessible"` + ClickCount int `json:"click_count,omitzero"` +} + +// PostRefBrief 帖子内链简要信息 (用于搜索联想) +type PostRefBrief struct { + ID string `json:"id"` + Title string `json:"title"` + Channel string `json:"channel,omitempty"` +} + // VoteSegmentData 投票Segment数据 type VoteSegmentData struct { Options []VoteOptionData `json:"options"` diff --git a/internal/dto/segment.go b/internal/dto/segment.go index 4a93194..695f449 100644 --- a/internal/dto/segment.go +++ b/internal/dto/segment.go @@ -145,6 +145,17 @@ func NewLinkSegment(url, title, description, image string) model.MessageSegment } } +// NewPostRefSegment 创建帖子内链Segment +func NewPostRefSegment(postID, title string) model.MessageSegment { + return model.MessageSegment{ + Type: string(SegmentTypePostRef), + Data: map[string]any{ + "post_id": postID, + "title": title, + }, + } +} + // ExtractVoteSegmentData 从消息链中提取投票Segment数据 func ExtractVoteSegmentData(segments model.MessageSegments) *VoteSegmentData { for _, segment := range segments { @@ -227,6 +238,12 @@ func ExtractTextContentFromModel(segments model.MessageSegments) string { } case "vote": result += "[投票]" + case "post_ref": + if title, ok := segment.Data["title"].(string); ok && title != "" { + result += fmt.Sprintf("[帖子:%s]", title) + } else { + result += "[帖子引用]" + } } } return result @@ -256,6 +273,28 @@ func ExtractMentionedUsers(segments model.MessageSegments) []string { return userIDs } +// ExtractReferencedPosts 从消息链中提取所有被引用的帖子ID +func ExtractReferencedPosts(segments model.MessageSegments) []string { + var postIDs []string + seen := make(map[string]bool) + + for _, segment := range segments { + if segment.Type == string(SegmentTypePostRef) { + postID, _ := segment.Data["post_id"].(string) + if postID != "" && !seen[postID] { + postIDs = append(postIDs, postID) + seen[postID] = true + } + } + } + return postIDs +} + +// HasPostRefSegment 检查消息链中是否包含帖子引用 +func HasPostRefSegment(segments model.MessageSegments) bool { + return HasSegmentType(segments, SegmentTypePostRef) +} + // IsAtAll 检查消息是否@了所有人 func IsAtAll(segments model.MessageSegments) bool { return slices.ContainsFunc(segments, func(segment model.MessageSegment) bool { diff --git a/internal/handler/post_handler.go b/internal/handler/post_handler.go index 243987b..5bac1a7 100644 --- a/internal/handler/post_handler.go +++ b/internal/handler/post_handler.go @@ -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") diff --git a/internal/model/init.go b/internal/model/init.go index 5116f1f..3c9f3a3 100644 --- a/internal/model/init.go +++ b/internal/model/init.go @@ -176,6 +176,9 @@ func autoMigrate(db *gorm.DB) error { // 用户资料审核相关 &UserProfileAudit{}, + + // 帖子内链引用关系 + &PostReference{}, ) if err != nil { return err diff --git a/internal/model/post_reference.go b/internal/model/post_reference.go new file mode 100644 index 0000000..124bd0e --- /dev/null +++ b/internal/model/post_reference.go @@ -0,0 +1,40 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PostReferenceStatus string + +const ( + PostRefActive PostReferenceStatus = "active" + PostRefDead PostReferenceStatus = "dead" + PostRefHidden PostReferenceStatus = "hidden" +) + +type PostReference struct { + ID string `json:"id" gorm:"type:varchar(36);primaryKey"` + SourcePostID string `json:"source_post_id" gorm:"type:varchar(36);not null;uniqueIndex:uq_source_target;index:idx_source_post"` + TargetPostID string `json:"target_post_id" gorm:"type:varchar(36);not null;uniqueIndex:uq_source_target;index:idx_target_post;index:idx_target_status"` + Status PostReferenceStatus `json:"status" gorm:"type:varchar(20);default:active;index:idx_target_status"` + ClickCount int `json:"click_count" gorm:"default:0"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime:false"` + + SourcePost *Post `json:"source_post,omitempty" gorm:"foreignKey:SourcePostID"` + TargetPost *Post `json:"target_post,omitempty" gorm:"foreignKey:TargetPostID"` +} + +func (pr *PostReference) BeforeCreate(tx *gorm.DB) error { + if pr.ID == "" { + pr.ID = uuid.New().String() + } + return nil +} + +func (PostReference) TableName() string { + return "post_references" +} diff --git a/internal/repository/post_ref_repo.go b/internal/repository/post_ref_repo.go new file mode 100644 index 0000000..a2cf8ea --- /dev/null +++ b/internal/repository/post_ref_repo.go @@ -0,0 +1,97 @@ +package repository + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "with_you/internal/model" +) + +type PostRefRepository interface { + BatchUpsert(ctx context.Context, sourcePostID string, targetPostIDs []string) error + DeleteBySourcePostID(ctx context.Context, sourcePostID string) error + GetSourcesByTargetPostID(ctx context.Context, targetPostID string, page, pageSize int) ([]model.PostReference, int64, error) + GetTargetsBySourcePostID(ctx context.Context, sourcePostID string) ([]model.PostReference, error) + MarkStatusByTargetPostID(ctx context.Context, targetPostID string, status model.PostReferenceStatus) error + IncrementClickCount(ctx context.Context, sourcePostID, targetPostID string) error +} + +type postRefRepository struct { + db *gorm.DB +} + +func NewPostRefRepository(db *gorm.DB) PostRefRepository { + return &postRefRepository{db: db} +} + +func (r *postRefRepository) BatchUpsert(ctx context.Context, sourcePostID string, targetPostIDs []string) error { + if len(targetPostIDs) == 0 { + return nil + } + refs := make([]model.PostReference, 0, len(targetPostIDs)) + for _, targetID := range targetPostIDs { + refs = append(refs, model.PostReference{ + SourcePostID: sourcePostID, + TargetPostID: targetID, + Status: model.PostRefActive, + }) + } + return r.db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "source_post_id"}, {Name: "target_post_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"status", "updated_at"}), + }).Create(&refs).Error +} + +func (r *postRefRepository) DeleteBySourcePostID(ctx context.Context, sourcePostID string) error { + return r.db.WithContext(ctx).Where("source_post_id = ?", sourcePostID).Delete(&model.PostReference{}).Error +} + +func (r *postRefRepository) GetSourcesByTargetPostID(ctx context.Context, targetPostID string, page, pageSize int) ([]model.PostReference, int64, error) { + var refs []model.PostReference + var total int64 + + if err := r.db.WithContext(ctx).Model(&model.PostReference{}). + Where("target_post_id = ? AND status = ?", targetPostID, model.PostRefActive). + Count(&total).Error; err != nil { + return nil, 0, err + } + + offset := (page - 1) * pageSize + if err := r.db.WithContext(ctx). + Where("target_post_id = ? AND status = ?", targetPostID, model.PostRefActive). + Preload("SourcePost"). + Preload("SourcePost.User"). + Order("created_at DESC"). + Offset(offset).Limit(pageSize). + Find(&refs).Error; err != nil { + return nil, 0, err + } + + return refs, total, nil +} + +func (r *postRefRepository) GetTargetsBySourcePostID(ctx context.Context, sourcePostID string) ([]model.PostReference, error) { + var refs []model.PostReference + if err := r.db.WithContext(ctx). + Where("source_post_id = ?", sourcePostID). + Preload("TargetPost"). + Preload("TargetPost.User"). + Find(&refs).Error; err != nil { + return nil, err + } + return refs, nil +} + +func (r *postRefRepository) MarkStatusByTargetPostID(ctx context.Context, targetPostID string, status model.PostReferenceStatus) error { + return r.db.WithContext(ctx).Model(&model.PostReference{}). + Where("target_post_id = ?", targetPostID). + Update("status", status).Error +} + +func (r *postRefRepository) IncrementClickCount(ctx context.Context, sourcePostID, targetPostID string) error { + return r.db.WithContext(ctx).Model(&model.PostReference{}). + Where("source_post_id = ? AND target_post_id = ?", sourcePostID, targetPostID). + UpdateColumn("click_count", gorm.Expr("click_count + 1")).Error +} diff --git a/internal/router/router.go b/internal/router/router.go index d9eb457..358620b 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -272,6 +272,7 @@ func (r *Router) setupRoutes() { // 使用 OptionalAuth 中间件来获取用户登录状态 posts.GET("", middleware.OptionalAuth(r.jwtService), r.postHandler.List) posts.GET("/search", middleware.OptionalAuth(r.jwtService), r.postHandler.Search) + posts.GET("/suggest", middleware.OptionalAuth(r.jwtService), r.postHandler.Suggest) posts.GET("/:id", middleware.OptionalAuth(r.jwtService), r.postHandler.GetByID) posts.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Create) posts.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Update) @@ -282,6 +283,12 @@ func (r *Router) setupRoutes() { // 分享计数(可选认证;仅已发布帖子生效) posts.POST("/:id/share", middleware.OptionalAuth(r.jwtService), r.postHandler.RecordShare) + // 内链相关路由 + posts.GET("/:id/related", middleware.OptionalAuth(r.jwtService), r.postHandler.GetRelatedPosts) + posts.GET("/:id/refs", middleware.OptionalAuth(r.jwtService), r.postHandler.GetReferencedPosts) + posts.GET("/suggest", middleware.OptionalAuth(r.jwtService), r.postHandler.Suggest) + posts.POST("/:id/ref-click", middleware.OptionalAuth(r.jwtService), r.postHandler.RecordRefClick) + // 点赞 posts.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Like) posts.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Unlike) diff --git a/internal/service/post_ref_service.go b/internal/service/post_ref_service.go new file mode 100644 index 0000000..3670932 --- /dev/null +++ b/internal/service/post_ref_service.go @@ -0,0 +1,222 @@ +package service + +import ( + "context" + "fmt" + "log" + "slices" + + "with_you/internal/dto" + "with_you/internal/model" + "with_you/internal/repository" +) + +type PostRefService interface { + ProcessPostReferences(ctx context.Context, sourcePostID string, segments model.MessageSegments) error + EnrichPostRefSegments(ctx context.Context, segments model.MessageSegments, currentUserID string) (model.MessageSegments, error) + RecordClick(ctx context.Context, sourcePostID, targetPostID string) error + HandlePostDeleted(ctx context.Context, postID string) error + HandlePostRecovered(ctx context.Context, postID string) error + HandlePostHidden(ctx context.Context, postID string) error + GetRelatedPosts(ctx context.Context, postID string, page, pageSize int) ([]*model.Post, int64, error) + GetReferencedPosts(ctx context.Context, postID string) ([]*model.Post, error) + SuggestPosts(ctx context.Context, keyword string, limit int) ([]dto.PostRefBrief, error) +} + +type postRefServiceImpl struct { + refRepo repository.PostRefRepository + postRepo repository.PostRepository +} + +func NewPostRefService(refRepo repository.PostRefRepository, postRepo repository.PostRepository) PostRefService { + return &postRefServiceImpl{ + refRepo: refRepo, + postRepo: postRepo, + } +} + +func (s *postRefServiceImpl) ProcessPostReferences(ctx context.Context, sourcePostID string, segments model.MessageSegments) error { + targetIDs := dto.ExtractReferencedPosts(segments) + if len(targetIDs) == 0 { + return nil + } + + validIDs := make([]string, 0, len(targetIDs)) + for _, tid := range targetIDs { + post, err := s.postRepo.GetByID(tid) + if err != nil { + log.Printf("[WARN] post_ref target %s not found for source %s: %v", tid, sourcePostID, err) + continue + } + if post.Status == model.PostStatusPublished && !post.IsDeleted { + validIDs = append(validIDs, tid) + } + } + + if len(validIDs) == 0 { + return nil + } + + return s.refRepo.BatchUpsert(ctx, sourcePostID, validIDs) +} + +func (s *postRefServiceImpl) EnrichPostRefSegments(ctx context.Context, segments model.MessageSegments, currentUserID string) (model.MessageSegments, error) { + postIDs := dto.ExtractReferencedPosts(segments) + if len(postIDs) == 0 { + return segments, nil + } + + posts, err := s.postRepo.GetByIDs(postIDs) + if err != nil { + log.Printf("[WARN] failed to get referenced posts: %v", err) + return segments, nil + } + + postMap := make(map[string]*model.Post, len(posts)) + for _, p := range posts { + postMap[p.ID] = p + } + + enriched := make(model.MessageSegments, len(segments)) + for i, seg := range segments { + enriched[i] = seg + if seg.Type == string(dto.SegmentTypePostRef) { + postID, _ := seg.Data["post_id"].(string) + enriched[i] = s.enrichSegment(seg, postID, postMap, currentUserID) + } + } + + return enriched, nil +} + +func (s *postRefServiceImpl) enrichSegment(seg model.MessageSegment, postID string, postMap map[string]*model.Post, currentUserID string) model.MessageSegment { + newSeg := model.MessageSegment{ + Type: seg.Type, + Data: map[string]any{"post_id": postID}, + } + + post, ok := postMap[postID] + if !ok || post == nil { + newSeg.Data["status"] = "deleted" + newSeg.Data["title"] = "该帖子已删除" + newSeg.Data["accessible"] = false + return newSeg + } + + if post.IsDeleted { + newSeg.Data["status"] = "deleted" + newSeg.Data["title"] = "该帖子已删除" + newSeg.Data["accessible"] = false + return newSeg + } + + if post.Status != model.PostStatusPublished { + if currentUserID == post.UserID { + newSeg.Data["status"] = string(post.Status) + newSeg.Data["title"] = post.Title + newSeg.Data["accessible"] = true + } else { + newSeg.Data["status"] = "hidden" + newSeg.Data["title"] = "该帖子不可见" + newSeg.Data["accessible"] = false + } + return newSeg + } + + newSeg.Data["status"] = "published" + newSeg.Data["title"] = post.Title + newSeg.Data["accessible"] = true + + if post.User != nil { + newSeg.Data["author"] = map[string]any{ + "id": post.User.ID, + "nickname": post.User.Nickname, + "avatar": post.User.Avatar, + } + } + + return newSeg +} + +func (s *postRefServiceImpl) RecordClick(ctx context.Context, sourcePostID, targetPostID string) error { + return s.refRepo.IncrementClickCount(ctx, sourcePostID, targetPostID) +} + +func (s *postRefServiceImpl) HandlePostDeleted(ctx context.Context, postID string) error { + return s.refRepo.MarkStatusByTargetPostID(ctx, postID, model.PostRefDead) +} + +func (s *postRefServiceImpl) HandlePostRecovered(ctx context.Context, postID string) error { + return s.refRepo.MarkStatusByTargetPostID(ctx, postID, model.PostRefActive) +} + +func (s *postRefServiceImpl) HandlePostHidden(ctx context.Context, postID string) error { + return s.refRepo.MarkStatusByTargetPostID(ctx, postID, model.PostRefHidden) +} + +func (s *postRefServiceImpl) GetRelatedPosts(ctx context.Context, postID string, page, pageSize int) ([]*model.Post, int64, error) { + refs, total, err := s.refRepo.GetSourcesByTargetPostID(ctx, postID, page, pageSize) + if err != nil { + return nil, 0, err + } + + posts := make([]*model.Post, 0, len(refs)) + for _, ref := range refs { + if ref.SourcePost != nil { + posts = append(posts, ref.SourcePost) + } + } + + return posts, total, nil +} + +func (s *postRefServiceImpl) GetReferencedPosts(ctx context.Context, postID string) ([]*model.Post, error) { + refs, err := s.refRepo.GetTargetsBySourcePostID(ctx, postID) + if err != nil { + return nil, err + } + + posts := make([]*model.Post, 0, len(refs)) + for _, ref := range refs { + if ref.TargetPost != nil && ref.TargetPost.Status == model.PostStatusPublished && !ref.TargetPost.IsDeleted { + posts = append(posts, ref.TargetPost) + } + } + + return posts, nil +} + +func (s *postRefServiceImpl) SuggestPosts(ctx context.Context, keyword string, limit int) ([]dto.PostRefBrief, error) { + if limit <= 0 { + limit = 10 + } + + posts, _, err := s.postRepo.Search(keyword, 1, limit) + if err != nil { + return nil, fmt.Errorf("search posts for suggest: %w", err) + } + + result := make([]dto.PostRefBrief, 0, len(posts)) + for _, p := range posts { + brief := dto.PostRefBrief{ + ID: p.ID, + Title: p.Title, + } + if p.ChannelID != nil { + brief.Channel = *p.ChannelID + } + result = append(result, brief) + } + + slices.SortFunc(result, func(a, b dto.PostRefBrief) int { + if len(a.Title) < len(b.Title) { + return -1 + } + if len(a.Title) > len(b.Title) { + return 1 + } + return 0 + }) + + return result, nil +} diff --git a/internal/wire/repository.go b/internal/wire/repository.go index 2189575..e4e98b1 100644 --- a/internal/wire/repository.go +++ b/internal/wire/repository.go @@ -43,6 +43,9 @@ var RepositorySet = wire.NewSet( // 用户资料审核相关仓储 repository.NewUserProfileAuditRepository, + + // 帖子内链引用关系 + repository.NewPostRefRepository, ) // ProvideUserActivityRepository 提供用户活跃数据仓储 diff --git a/internal/wire/service.go b/internal/wire/service.go index 2732e4c..a370b49 100644 --- a/internal/wire/service.go +++ b/internal/wire/service.go @@ -65,6 +65,9 @@ var ServiceSet = wire.NewSet( ProvideUserProfileAuditService, ProvideSetupService, + // 帖子内链引用 + ProvidePostRefService, + // 日志服务 ProvideAsyncLogManager, ProvideOperationLogService, @@ -486,3 +489,10 @@ func ProvideSetupService( ) service.SetupService { return service.NewSetupService(userRepo, roleRepo, casbinSvc, cfg.SetupSecret) } + +func ProvidePostRefService( + refRepo repository.PostRefRepository, + postRepo repository.PostRepository, +) service.PostRefService { + return service.NewPostRefService(refRepo, postRepo) +}