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.
223 lines
6.2 KiB
Go
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
|
|
}
|