Files
backend/internal/service/post_ref_service.go
lafay 23d7f1151e
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 1m18s
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.
2026-04-26 00:37:20 +08:00

223 lines
6.2 KiB
Go

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
}