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 }