2026-03-09 21:28:58 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
2026-03-12 23:40:21 +08:00
|
|
|
|
"math/rand"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"carrot_bbs/internal/cache"
|
|
|
|
|
|
"carrot_bbs/internal/model"
|
|
|
|
|
|
"carrot_bbs/internal/pkg/gorse"
|
|
|
|
|
|
"carrot_bbs/internal/repository"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 缓存TTL常量
|
|
|
|
|
|
const (
|
|
|
|
|
|
PostListTTL = 30 * time.Second // 帖子列表缓存30秒
|
|
|
|
|
|
PostListNullTTL = 5 * time.Second
|
|
|
|
|
|
PostListJitterRatio = 0.15
|
|
|
|
|
|
anonymousViewUserID = "_anon_view"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
// PostService 帖子服务接口
|
|
|
|
|
|
type PostService interface {
|
|
|
|
|
|
// 帖子CRUD
|
|
|
|
|
|
Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error)
|
|
|
|
|
|
GetByID(ctx context.Context, id string) (*model.Post, error)
|
|
|
|
|
|
Update(ctx context.Context, post *model.Post) error
|
|
|
|
|
|
UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error
|
|
|
|
|
|
Delete(ctx context.Context, id string) error
|
|
|
|
|
|
|
|
|
|
|
|
// 帖子列表
|
|
|
|
|
|
List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error)
|
|
|
|
|
|
GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
|
|
|
|
|
|
GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
|
|
|
|
|
|
GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error)
|
|
|
|
|
|
GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
|
|
|
|
|
|
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error)
|
|
|
|
|
|
|
|
|
|
|
|
// 关注和推荐
|
|
|
|
|
|
GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
|
|
|
|
|
|
GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error)
|
|
|
|
|
|
GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
|
|
|
|
|
|
|
|
|
|
|
|
// 交互功能
|
|
|
|
|
|
Like(ctx context.Context, postID, userID string) error
|
|
|
|
|
|
Unlike(ctx context.Context, postID, userID string) error
|
|
|
|
|
|
IsLiked(ctx context.Context, postID, userID string) bool
|
|
|
|
|
|
Favorite(ctx context.Context, postID, userID string) error
|
|
|
|
|
|
Unfavorite(ctx context.Context, postID, userID string) error
|
|
|
|
|
|
IsFavorited(ctx context.Context, postID, userID string) bool
|
|
|
|
|
|
|
|
|
|
|
|
// 交互状态查询
|
|
|
|
|
|
GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error)
|
|
|
|
|
|
GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool)
|
|
|
|
|
|
|
|
|
|
|
|
// 其他
|
|
|
|
|
|
IncrementViews(ctx context.Context, postID, userID string) error
|
2026-03-15 02:25:10 +08:00
|
|
|
|
|
|
|
|
|
|
// 日志服务设置
|
|
|
|
|
|
SetLogService(logService *LogService)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// postServiceImpl 帖子服务实现
|
|
|
|
|
|
type postServiceImpl struct {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
postRepo *repository.PostRepository
|
|
|
|
|
|
systemMessageService SystemMessageService
|
|
|
|
|
|
cache cache.Cache
|
|
|
|
|
|
gorseClient gorse.Client
|
|
|
|
|
|
postAIService *PostAIService
|
2026-03-15 02:25:10 +08:00
|
|
|
|
txManager repository.TransactionManager
|
|
|
|
|
|
logService *LogService
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewPostService 创建帖子服务
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager) PostService {
|
|
|
|
|
|
return &postServiceImpl{
|
2026-03-09 21:28:58 +08:00
|
|
|
|
postRepo: postRepo,
|
|
|
|
|
|
systemMessageService: systemMessageService,
|
2026-03-13 09:38:18 +08:00
|
|
|
|
cache: cacheBackend,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
gorseClient: gorseClient,
|
|
|
|
|
|
postAIService: postAIService,
|
2026-03-13 09:38:18 +08:00
|
|
|
|
txManager: txManager,
|
2026-03-15 02:25:10 +08:00
|
|
|
|
logService: nil,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 02:25:10 +08:00
|
|
|
|
// SetLogService 设置日志服务
|
|
|
|
|
|
func (s *postServiceImpl) SetLogService(logService *LogService) {
|
|
|
|
|
|
s.logService = logService
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// PostListResult 帖子列表缓存结果
|
|
|
|
|
|
type PostListResult struct {
|
|
|
|
|
|
Posts []*model.Post
|
|
|
|
|
|
Total int64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create 创建帖子
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
post := &model.Post{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
Title: title,
|
|
|
|
|
|
Content: content,
|
|
|
|
|
|
Status: model.PostStatusPending,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := s.postRepo.Create(post, images)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 02:25:10 +08:00
|
|
|
|
// 记录操作日志
|
|
|
|
|
|
if s.logService != nil {
|
|
|
|
|
|
s.logService.OperationLog.RecordOperation(&model.OperationLog{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
Operation: string(model.OpPostCreate),
|
|
|
|
|
|
Module: "post",
|
|
|
|
|
|
TargetType: "post",
|
|
|
|
|
|
TargetID: post.ID,
|
|
|
|
|
|
Status: "success",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 失效帖子列表缓存
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
|
|
|
|
|
|
|
|
|
|
|
// 同步到Gorse推荐系统(异步)
|
|
|
|
|
|
go s.reviewPostAsync(post.ID, userID, title, content, images)
|
|
|
|
|
|
|
|
|
|
|
|
// 重新查询以获取关联的 User 和 Images
|
|
|
|
|
|
return s.postRepo.GetByID(post.ID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) reviewPostAsync(postID, userID, title, content string, images []string) {
|
2026-03-12 08:38:14 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
|
log.Printf("[ERROR] Panic in post moderation async flow, fallback publish post=%s panic=%v", postID, r)
|
|
|
|
|
|
if err := s.updateModerationStatusWithRetry(postID, model.PostStatusPublished, "", "system"); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to publish post %s after panic recovery: %v", postID, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.invalidatePostCaches(postID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 未启用AI时,直接发布
|
|
|
|
|
|
if s.postAIService == nil || !s.postAIService.IsEnabled() {
|
2026-03-12 08:38:14 +08:00
|
|
|
|
if err := s.updateModerationStatusWithRetry(postID, model.PostStatusPublished, "", "system"); err != nil {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
log.Printf("[WARN] Failed to publish post without AI moderation: %v", err)
|
2026-03-10 12:58:23 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
s.invalidatePostCaches(postID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := s.postAIService.ModeratePost(context.Background(), title, content, images)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
var rejectedErr *PostModerationRejectedError
|
|
|
|
|
|
if errors.As(err, &rejectedErr) {
|
2026-03-12 08:38:14 +08:00
|
|
|
|
if updateErr := s.updateModerationStatusWithRetry(postID, model.PostStatusRejected, rejectedErr.UserMessage(), "ai"); updateErr != nil {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
log.Printf("[WARN] Failed to reject post %s: %v", postID, updateErr)
|
2026-03-10 12:58:23 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
s.invalidatePostCaches(postID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
s.notifyModerationRejected(userID, rejectedErr.Reason)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 规则审核不可用时,降级为发布,避免长时间pending
|
2026-03-12 08:38:14 +08:00
|
|
|
|
if updateErr := s.updateModerationStatusWithRetry(postID, model.PostStatusPublished, "", "system"); updateErr != nil {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
log.Printf("[WARN] Failed to publish post %s after moderation error: %v", postID, updateErr)
|
2026-03-10 12:58:23 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
s.invalidatePostCaches(postID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
log.Printf("[WARN] Post moderation failed, fallback publish post=%s err=%v", postID, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 08:38:14 +08:00
|
|
|
|
if err := s.updateModerationStatusWithRetry(postID, model.PostStatusPublished, "", "ai"); err != nil {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
log.Printf("[WARN] Failed to publish post %s: %v", postID, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-10 12:58:23 +08:00
|
|
|
|
s.invalidatePostCaches(postID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
if s.gorseClient.IsEnabled() {
|
|
|
|
|
|
post, getErr := s.postRepo.GetByID(postID)
|
|
|
|
|
|
if getErr != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to load published post for gorse sync: %v", getErr)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
categories := s.buildPostCategories(post)
|
|
|
|
|
|
comment := post.Title
|
|
|
|
|
|
textToEmbed := post.Title + " " + post.Content
|
|
|
|
|
|
if upsertErr := s.gorseClient.UpsertItemWithEmbedding(context.Background(), post.ID, categories, comment, textToEmbed); upsertErr != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to upsert item to Gorse: %v", upsertErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
|
2026-03-12 08:38:14 +08:00
|
|
|
|
const maxAttempts = 3
|
|
|
|
|
|
const retryDelay = 200 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
|
|
var lastErr error
|
|
|
|
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
|
|
|
|
if err := s.postRepo.UpdateModerationStatus(postID, status, rejectReason, reviewedBy); err != nil {
|
|
|
|
|
|
lastErr = err
|
|
|
|
|
|
if attempt < maxAttempts {
|
|
|
|
|
|
log.Printf("[WARN] UpdateModerationStatus failed post=%s attempt=%d/%d err=%v", postID, attempt, maxAttempts, err)
|
|
|
|
|
|
time.Sleep(time.Duration(attempt) * retryDelay)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return lastErr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) invalidatePostCaches(postID string) {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) notifyModerationRejected(userID, reason string) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
content := "您发布的帖子未通过AI审核,请修改后重试。"
|
|
|
|
|
|
if strings.TrimSpace(reason) != "" {
|
|
|
|
|
|
content = fmt.Sprintf("您发布的帖子未通过AI审核,原因:%s。请修改后重试。", reason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if err := s.systemMessageService.SendSystemAnnouncement(
|
|
|
|
|
|
context.Background(),
|
|
|
|
|
|
[]string{userID},
|
|
|
|
|
|
"帖子审核未通过",
|
|
|
|
|
|
content,
|
|
|
|
|
|
); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to send moderation reject notification: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByID 根据ID获取帖子
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetByID(ctx context.Context, id string) (*model.Post, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.postRepo.GetByID(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update 更新帖子
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Update(ctx context.Context, post *model.Post) error {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
return s.UpdateWithImages(ctx, post, nil)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateWithImages 更新帖子并可选更新图片(images=nil 表示不更新图片)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
err := s.postRepo.UpdateWithImages(post, images)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存和列表缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, post.ID)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete 删除帖子
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Delete(ctx context.Context, id string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
err := s.postRepo.Delete(id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存和列表缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, id)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
|
|
|
|
|
|
|
|
|
|
|
// 从Gorse中删除帖子(异步)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if s.gorseClient.IsEnabled() {
|
|
|
|
|
|
if err := s.gorseClient.DeleteItem(context.Background(), id); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to delete item from Gorse: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// List 获取帖子列表(带缓存)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
cacheSettings := cache.GetSettings()
|
|
|
|
|
|
postListTTL := cacheSettings.PostListTTL
|
|
|
|
|
|
if postListTTL <= 0 {
|
|
|
|
|
|
postListTTL = PostListTTL
|
|
|
|
|
|
}
|
|
|
|
|
|
nullTTL := cacheSettings.NullTTL
|
|
|
|
|
|
if nullTTL <= 0 {
|
|
|
|
|
|
nullTTL = PostListNullTTL
|
|
|
|
|
|
}
|
|
|
|
|
|
jitter := cacheSettings.JitterRatio
|
|
|
|
|
|
if jitter <= 0 {
|
|
|
|
|
|
jitter = PostListJitterRatio
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// 生成缓存键(包含 userID 维度与可见性维度,避免作者视角污染公开视角)
|
|
|
|
|
|
visibilityUserKey := userID
|
|
|
|
|
|
if includePending && userID != "" {
|
|
|
|
|
|
visibilityUserKey = "owner:" + userID
|
|
|
|
|
|
}
|
|
|
|
|
|
cacheKey := cache.PostListKey("latest", visibilityUserKey, page, pageSize)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
2026-03-12 23:40:21 +08:00
|
|
|
|
result, err := cache.GetOrLoadTyped(
|
2026-03-09 21:28:58 +08:00
|
|
|
|
s.cache,
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
postListTTL,
|
|
|
|
|
|
jitter,
|
|
|
|
|
|
nullTTL,
|
|
|
|
|
|
func() (*PostListResult, error) {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
posts, total, err := s.postRepo.List(page, pageSize, userID, includePending)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &PostListResult{Posts: posts, Total: total}, nil
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if result == nil {
|
|
|
|
|
|
return []*model.Post{}, 0, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 兼容历史脏缓存:旧缓存序列化会丢失 Post.User,导致前端显示“匿名用户”
|
|
|
|
|
|
// 这里检测并回源重建一次缓存,避免在 TTL 内持续返回缺失作者的数据。
|
|
|
|
|
|
missingAuthor := false
|
|
|
|
|
|
for _, post := range result.Posts {
|
|
|
|
|
|
if post != nil && post.UserID != "" && post.User == nil {
|
|
|
|
|
|
missingAuthor = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if missingAuthor {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
posts, total, loadErr := s.postRepo.List(page, pageSize, userID, includePending)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if loadErr != nil {
|
|
|
|
|
|
return nil, 0, loadErr
|
|
|
|
|
|
}
|
|
|
|
|
|
result = &PostListResult{Posts: posts, Total: total}
|
|
|
|
|
|
cache.SetWithJitter(s.cache, cacheKey, result, postListTTL, jitter)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result.Posts, result.Total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetLatestPosts 获取最新帖子(语义化别名)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
return s.List(ctx, page, pageSize, userID, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
return s.List(ctx, page, pageSize, userID, true)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUserPosts 获取用户帖子
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
return s.postRepo.GetUserPosts(userID, page, pageSize, includePending)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Like 点赞
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Like(ctx context.Context, postID, userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 获取帖子信息用于发送通知
|
|
|
|
|
|
post, err := s.postRepo.GetByID(postID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err = s.postRepo.Like(postID, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
|
|
|
|
|
|
// 发送点赞通知(不给自己发通知)
|
|
|
|
|
|
if s.systemMessageService != nil && post.UserID != userID {
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
notifyErr := s.systemMessageService.SendLikeNotification(context.Background(), post.UserID, userID, postID)
|
|
|
|
|
|
if notifyErr != nil {
|
2026-03-09 22:18:53 +08:00
|
|
|
|
log.Printf("[ERROR] Error sending like notification: %v", notifyErr)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 推送点赞行为到Gorse(异步)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if s.gorseClient.IsEnabled() {
|
|
|
|
|
|
if err := s.gorseClient.InsertFeedback(context.Background(), gorse.FeedbackTypeLike, userID, postID); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to insert like feedback to Gorse: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Unlike 取消点赞
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Unlike(ctx context.Context, postID, userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
err := s.postRepo.Unlike(postID, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
|
|
|
|
|
|
// 删除Gorse中的点赞反馈(异步)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if s.gorseClient.IsEnabled() {
|
|
|
|
|
|
if err := s.gorseClient.DeleteFeedback(context.Background(), gorse.FeedbackTypeLike, userID, postID); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to delete like feedback from Gorse: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsLiked 检查是否点赞
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) IsLiked(ctx context.Context, postID, userID string) bool {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.postRepo.IsLiked(postID, userID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Favorite 收藏
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Favorite(ctx context.Context, postID, userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 获取帖子信息用于发送通知
|
|
|
|
|
|
post, err := s.postRepo.GetByID(postID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err = s.postRepo.Favorite(postID, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
|
|
|
|
|
|
// 发送收藏通知(不给自己发通知)
|
|
|
|
|
|
if s.systemMessageService != nil && post.UserID != userID {
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
notifyErr := s.systemMessageService.SendFavoriteNotification(context.Background(), post.UserID, userID, postID)
|
|
|
|
|
|
if notifyErr != nil {
|
2026-03-09 22:18:53 +08:00
|
|
|
|
log.Printf("[ERROR] Error sending favorite notification: %v", notifyErr)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 推送收藏行为到Gorse(异步)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if s.gorseClient.IsEnabled() {
|
|
|
|
|
|
if err := s.gorseClient.InsertFeedback(context.Background(), gorse.FeedbackTypeStar, userID, postID); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to insert favorite feedback to Gorse: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Unfavorite 取消收藏
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Unfavorite(ctx context.Context, postID, userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
err := s.postRepo.Unfavorite(postID, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
|
|
|
|
|
|
// 删除Gorse中的收藏反馈(异步)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if s.gorseClient.IsEnabled() {
|
|
|
|
|
|
if err := s.gorseClient.DeleteFeedback(context.Background(), gorse.FeedbackTypeStar, userID, postID); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to delete favorite feedback from Gorse: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsFavorited 检查是否收藏
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) IsFavorited(ctx context.Context, postID, userID string) bool {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.postRepo.IsFavorited(postID, userID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 20:52:50 +08:00
|
|
|
|
// GetPostInteractionStatus 批量获取帖子的交互状态(点赞、收藏)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error) {
|
2026-03-10 20:52:50 +08:00
|
|
|
|
isLikedMap := make(map[string]bool)
|
|
|
|
|
|
isFavoritedMap := make(map[string]bool)
|
|
|
|
|
|
|
|
|
|
|
|
if userID == "" || len(postIDs) == 0 {
|
|
|
|
|
|
return isLikedMap, isFavoritedMap, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 00:47:17 +08:00
|
|
|
|
// 使用批量查询替代循环中的单个查询,解决 N+1 问题
|
|
|
|
|
|
isLikedMap = s.postRepo.IsLikedBatch(postIDs, userID)
|
|
|
|
|
|
isFavoritedMap = s.postRepo.IsFavoritedBatch(postIDs, userID)
|
2026-03-10 20:52:50 +08:00
|
|
|
|
|
|
|
|
|
|
return isLikedMap, isFavoritedMap, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetPostInteractionStatusSingle 获取单个帖子的交互状态
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool) {
|
2026-03-10 20:52:50 +08:00
|
|
|
|
if userID == "" {
|
|
|
|
|
|
return false, false
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.postRepo.IsLiked(postID, userID), s.postRepo.IsFavorited(postID, userID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// IncrementViews 增加帖子观看量并同步到Gorse
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) IncrementViews(ctx context.Context, postID, userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if err := s.postRepo.IncrementViews(postID); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同步浏览行为到Gorse(异步)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if !s.gorseClient.IsEnabled() {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
feedbackUserID := userID
|
|
|
|
|
|
if feedbackUserID == "" {
|
|
|
|
|
|
feedbackUserID = anonymousViewUserID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.gorseClient.InsertFeedback(context.Background(), gorse.FeedbackTypeRead, feedbackUserID, postID); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to insert read feedback to Gorse: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetFavorites 获取收藏列表
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.postRepo.GetFavorites(userID, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Search 搜索帖子
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.postRepo.Search(keyword, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetFollowingPosts 获取关注用户的帖子(带缓存)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
cacheSettings := cache.GetSettings()
|
|
|
|
|
|
postListTTL := cacheSettings.PostListTTL
|
|
|
|
|
|
if postListTTL <= 0 {
|
|
|
|
|
|
postListTTL = PostListTTL
|
|
|
|
|
|
}
|
|
|
|
|
|
nullTTL := cacheSettings.NullTTL
|
|
|
|
|
|
if nullTTL <= 0 {
|
|
|
|
|
|
nullTTL = PostListNullTTL
|
|
|
|
|
|
}
|
|
|
|
|
|
jitter := cacheSettings.JitterRatio
|
|
|
|
|
|
if jitter <= 0 {
|
|
|
|
|
|
jitter = PostListJitterRatio
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成缓存键
|
|
|
|
|
|
cacheKey := cache.PostListKey("follow", userID, page, pageSize)
|
|
|
|
|
|
|
|
|
|
|
|
result, err := cache.GetOrLoadTyped[*PostListResult](
|
|
|
|
|
|
s.cache,
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
postListTTL,
|
|
|
|
|
|
jitter,
|
|
|
|
|
|
nullTTL,
|
|
|
|
|
|
func() (*PostListResult, error) {
|
|
|
|
|
|
posts, total, err := s.postRepo.GetFollowingPosts(userID, page, pageSize)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &PostListResult{Posts: posts, Total: total}, nil
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if result == nil {
|
|
|
|
|
|
return []*model.Post{}, 0, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return result.Posts, result.Total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetHotPosts 获取热门帖子(使用Gorse非个性化推荐)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 如果Gorse启用,使用自定义的非个性化推荐器
|
|
|
|
|
|
if s.gorseClient.IsEnabled() {
|
|
|
|
|
|
offset := (page - 1) * pageSize
|
|
|
|
|
|
// 使用 most_liked_weekly 推荐器获取周热门
|
|
|
|
|
|
// 多取1条用于判断是否还有下一页
|
|
|
|
|
|
itemIDs, err := s.gorseClient.GetNonPersonalized(ctx, "most_liked_weekly", pageSize+1, offset, "")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Gorse GetNonPersonalized failed: %v, fallback to database", err)
|
|
|
|
|
|
return s.getHotPostsFromDB(ctx, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(itemIDs) > 0 {
|
|
|
|
|
|
hasNext := len(itemIDs) > pageSize
|
|
|
|
|
|
if hasNext {
|
|
|
|
|
|
itemIDs = itemIDs[:pageSize]
|
|
|
|
|
|
}
|
|
|
|
|
|
posts, err := s.postRepo.GetByIDs(itemIDs)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
// 近似 total:当 hasNext 为 true 时,按分页窗口估算,避免因脏数据/缺失数据导致总页数被低估
|
|
|
|
|
|
estimatedTotal := int64(offset + len(posts))
|
|
|
|
|
|
if hasNext {
|
|
|
|
|
|
estimatedTotal = int64(offset + pageSize + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
return posts, estimatedTotal, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 降级:从数据库获取
|
|
|
|
|
|
return s.getHotPostsFromDB(ctx, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getHotPostsFromDB 从数据库获取热门帖子(降级路径)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) getHotPostsFromDB(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 直接查询数据库,不再使用本地缓存(Gorse失败降级时使用)
|
|
|
|
|
|
posts, total, err := s.postRepo.GetHotPosts(page, pageSize)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return posts, total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetRecommendedPosts 获取推荐帖子
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 如果Gorse未启用或用户未登录,降级为热门帖子
|
|
|
|
|
|
if !s.gorseClient.IsEnabled() || userID == "" {
|
|
|
|
|
|
return s.GetHotPosts(ctx, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 23:40:21 +08:00
|
|
|
|
// 计算偏移量:第一页使用随机偏移量,增加推荐多样性
|
|
|
|
|
|
var offset int
|
|
|
|
|
|
if page == 1 {
|
|
|
|
|
|
// 第一页随机偏移 0-50,让每次刷新看到不同的推荐内容
|
|
|
|
|
|
offset = rand.Intn(51)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
offset = (page - 1) * pageSize
|
|
|
|
|
|
}
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 从Gorse获取推荐列表
|
|
|
|
|
|
// 多取1条用于判断是否还有下一页
|
|
|
|
|
|
itemIDs, err := s.gorseClient.GetRecommend(ctx, userID, pageSize+1, offset)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Gorse recommendation failed: %v, fallback to hot posts", err)
|
|
|
|
|
|
return s.GetHotPosts(ctx, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有推荐结果,降级为热门帖子
|
|
|
|
|
|
if len(itemIDs) == 0 {
|
|
|
|
|
|
return s.GetHotPosts(ctx, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hasNext := len(itemIDs) > pageSize
|
|
|
|
|
|
if hasNext {
|
|
|
|
|
|
itemIDs = itemIDs[:pageSize]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据ID列表查询帖子详情
|
|
|
|
|
|
posts, err := s.postRepo.GetByIDs(itemIDs)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 近似 total:当 hasNext 为 true 时,按分页窗口估算,避免因脏数据/缺失数据导致总页数被低估
|
|
|
|
|
|
estimatedTotal := int64(offset + len(posts))
|
|
|
|
|
|
if hasNext {
|
|
|
|
|
|
estimatedTotal = int64(offset + pageSize + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
return posts, estimatedTotal, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// buildPostCategories 构建帖子的类别标签
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *postServiceImpl) buildPostCategories(post *model.Post) []string {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var categories []string
|
|
|
|
|
|
|
|
|
|
|
|
// 热度标签
|
|
|
|
|
|
if post.ViewsCount > 1000 {
|
|
|
|
|
|
categories = append(categories, "hot_high")
|
|
|
|
|
|
} else if post.ViewsCount > 100 {
|
|
|
|
|
|
categories = append(categories, "hot_medium")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点赞标签
|
|
|
|
|
|
if post.LikesCount > 100 {
|
|
|
|
|
|
categories = append(categories, "likes_100+")
|
|
|
|
|
|
} else if post.LikesCount > 50 {
|
|
|
|
|
|
categories = append(categories, "likes_50+")
|
|
|
|
|
|
} else if post.LikesCount > 10 {
|
|
|
|
|
|
categories = append(categories, "likes_10+")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 评论标签
|
|
|
|
|
|
if post.CommentsCount > 50 {
|
|
|
|
|
|
categories = append(categories, "comments_50+")
|
|
|
|
|
|
} else if post.CommentsCount > 10 {
|
|
|
|
|
|
categories = append(categories, "comments_10+")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 时间标签
|
|
|
|
|
|
age := time.Since(post.CreatedAt)
|
|
|
|
|
|
if age < 24*time.Hour {
|
|
|
|
|
|
categories = append(categories, "today")
|
|
|
|
|
|
} else if age < 7*24*time.Hour {
|
|
|
|
|
|
categories = append(categories, "this_week")
|
|
|
|
|
|
} else if age < 30*24*time.Hour {
|
|
|
|
|
|
categories = append(categories, "this_month")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return categories
|
|
|
|
|
|
}
|
2026-03-13 09:38:18 +08:00
|
|
|
|
|
|
|
|
|
|
// ========== 事务管理器示例方法 ==========
|
|
|
|
|
|
|
|
|
|
|
|
// DeletePostWithTransaction 使用事务管理器删除帖子(示例)
|
|
|
|
|
|
// 此方法展示如何在 Service 层使用事务管理器控制跨多个 Repository 的事务
|
|
|
|
|
|
// 当需要在一个事务中执行多个 Repository 操作时,可以使用此模式
|
|
|
|
|
|
func (s *postServiceImpl) DeletePostWithTransaction(ctx context.Context, postID string) error {
|
|
|
|
|
|
// 使用事务管理器执行事务
|
|
|
|
|
|
return s.txManager.RunInTransaction(ctx, func(ctx context.Context) error {
|
|
|
|
|
|
// 在同一个事务中执行删除操作
|
|
|
|
|
|
// Repository 方法会通过 context 获取事务 TX
|
|
|
|
|
|
return s.postRepo.DeleteWithContext(ctx, postID)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|