283 lines
7.4 KiB
Go
283 lines
7.4 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"log"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"carrot_bbs/internal/cache"
|
|||
|
|
"carrot_bbs/internal/dto"
|
|||
|
|
"carrot_bbs/internal/model"
|
|||
|
|
"carrot_bbs/internal/repository"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// VoteService 投票服务
|
|||
|
|
type VoteService struct {
|
|||
|
|
voteRepo *repository.VoteRepository
|
|||
|
|
postRepo *repository.PostRepository
|
|||
|
|
cache cache.Cache
|
|||
|
|
postAIService *PostAIService
|
|||
|
|
systemMessageService SystemMessageService
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewVoteService 创建投票服务
|
|||
|
|
func NewVoteService(
|
|||
|
|
voteRepo *repository.VoteRepository,
|
|||
|
|
postRepo *repository.PostRepository,
|
|||
|
|
cache cache.Cache,
|
|||
|
|
postAIService *PostAIService,
|
|||
|
|
systemMessageService SystemMessageService,
|
|||
|
|
) *VoteService {
|
|||
|
|
return &VoteService{
|
|||
|
|
voteRepo: voteRepo,
|
|||
|
|
postRepo: postRepo,
|
|||
|
|
cache: cache,
|
|||
|
|
postAIService: postAIService,
|
|||
|
|
systemMessageService: systemMessageService,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CreateVotePost 创建投票帖子
|
|||
|
|
func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error) {
|
|||
|
|
// 验证投票选项数量
|
|||
|
|
if len(req.VoteOptions) < 2 {
|
|||
|
|
return nil, errors.New("投票选项至少需要2个")
|
|||
|
|
}
|
|||
|
|
if len(req.VoteOptions) > 10 {
|
|||
|
|
return nil, errors.New("投票选项最多10个")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建普通帖子(设置IsVote=true)
|
|||
|
|
post := &model.Post{
|
|||
|
|
UserID: userID,
|
|||
|
|
CommunityID: req.CommunityID,
|
|||
|
|
Title: req.Title,
|
|||
|
|
Content: req.Content,
|
|||
|
|
Status: model.PostStatusPending,
|
|||
|
|
IsVote: true,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err := s.postRepo.Create(post, req.Images)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建投票选项
|
|||
|
|
err = s.voteRepo.CreateOptions(post.ID, req.VoteOptions)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 异步审核
|
|||
|
|
go s.reviewVotePostAsync(post.ID, userID, req.Title, req.Content, req.Images)
|
|||
|
|
|
|||
|
|
// 重新查询以获取关联的User和Images
|
|||
|
|
createdPost, err := s.postRepo.GetByID(post.ID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换为响应DTO
|
|||
|
|
return s.convertToPostResponse(createdPost, userID), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *VoteService) reviewVotePostAsync(postID, userID, title, content string, images []string) {
|
|||
|
|
if s.postAIService == nil || !s.postAIService.IsEnabled() {
|
|||
|
|
if err := s.postRepo.UpdateModerationStatus(postID, model.PostStatusPublished, "", "system"); err != nil {
|
|||
|
|
log.Printf("[WARN] Failed to publish vote post without AI moderation: %v", err)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err := s.postAIService.ModeratePost(context.Background(), title, content, images)
|
|||
|
|
if err != nil {
|
|||
|
|
var rejectedErr *PostModerationRejectedError
|
|||
|
|
if errors.As(err, &rejectedErr) {
|
|||
|
|
if updateErr := s.postRepo.UpdateModerationStatus(postID, model.PostStatusRejected, rejectedErr.UserMessage(), "ai"); updateErr != nil {
|
|||
|
|
log.Printf("[WARN] Failed to reject vote post %s: %v", postID, updateErr)
|
|||
|
|
}
|
|||
|
|
s.notifyModerationRejected(userID, rejectedErr.Reason)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if updateErr := s.postRepo.UpdateModerationStatus(postID, model.PostStatusPublished, "", "system"); updateErr != nil {
|
|||
|
|
log.Printf("[WARN] Failed to publish vote post %s after moderation error: %v", postID, updateErr)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := s.postRepo.UpdateModerationStatus(postID, model.PostStatusPublished, "", "ai"); err != nil {
|
|||
|
|
log.Printf("[WARN] Failed to publish vote post %s: %v", postID, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *VoteService) notifyModerationRejected(userID, reason string) {
|
|||
|
|
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
content := "您发布的投票帖未通过AI审核,请修改后重试。"
|
|||
|
|
if strings.TrimSpace(reason) != "" {
|
|||
|
|
content = fmt.Sprintf("您发布的投票帖未通过AI审核,原因:%s。请修改后重试。", reason)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
go func() {
|
|||
|
|
_ = s.systemMessageService.SendSystemAnnouncement(
|
|||
|
|
context.Background(),
|
|||
|
|
[]string{userID},
|
|||
|
|
"投票帖审核未通过",
|
|||
|
|
content,
|
|||
|
|
)
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetVoteOptions 获取投票选项
|
|||
|
|
func (s *VoteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error) {
|
|||
|
|
options, err := s.voteRepo.GetOptionsByPostID(postID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result := make([]dto.VoteOptionDTO, 0, len(options))
|
|||
|
|
for _, option := range options {
|
|||
|
|
result = append(result, dto.VoteOptionDTO{
|
|||
|
|
ID: option.ID,
|
|||
|
|
Content: option.Content,
|
|||
|
|
VotesCount: option.VotesCount,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetVoteResult 获取投票结果(包含用户投票状态)
|
|||
|
|
func (s *VoteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error) {
|
|||
|
|
// 获取所有投票选项
|
|||
|
|
options, err := s.voteRepo.GetOptionsByPostID(postID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取用户的投票记录
|
|||
|
|
userVote, err := s.voteRepo.GetUserVote(postID, userID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建结果
|
|||
|
|
result := &dto.VoteResultDTO{
|
|||
|
|
Options: make([]dto.VoteOptionDTO, 0, len(options)),
|
|||
|
|
TotalVotes: 0,
|
|||
|
|
HasVoted: userVote != nil,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if userVote != nil {
|
|||
|
|
result.VotedOptionID = userVote.OptionID
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, option := range options {
|
|||
|
|
result.Options = append(result.Options, dto.VoteOptionDTO{
|
|||
|
|
ID: option.ID,
|
|||
|
|
Content: option.Content,
|
|||
|
|
VotesCount: option.VotesCount,
|
|||
|
|
})
|
|||
|
|
result.TotalVotes += option.VotesCount
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Vote 投票
|
|||
|
|
func (s *VoteService) Vote(ctx context.Context, postID, userID, optionID string) error {
|
|||
|
|
// 调用voteRepo.Vote
|
|||
|
|
err := s.voteRepo.Vote(postID, userID, optionID)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 失效帖子详情缓存
|
|||
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Unvote 取消投票
|
|||
|
|
func (s *VoteService) Unvote(ctx context.Context, postID, userID string) error {
|
|||
|
|
// 调用voteRepo.Unvote
|
|||
|
|
err := s.voteRepo.Unvote(postID, userID)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 失效帖子详情缓存
|
|||
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UpdateVoteOption 更新投票选项(作者权限)
|
|||
|
|
func (s *VoteService) UpdateVoteOption(ctx context.Context, postID, optionID, userID, content string) error {
|
|||
|
|
// 获取帖子信息
|
|||
|
|
post, err := s.postRepo.GetByID(postID)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证用户是否为帖子作者
|
|||
|
|
if post.UserID != userID {
|
|||
|
|
return errors.New("只有帖子作者可以更新投票选项")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用voteRepo.UpdateOption
|
|||
|
|
return s.voteRepo.UpdateOption(optionID, content)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// convertToPostResponse 将Post模型转换为PostResponse DTO
|
|||
|
|
func (s *VoteService) convertToPostResponse(post *model.Post, currentUserID string) *dto.PostResponse {
|
|||
|
|
if post == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
response := &dto.PostResponse{
|
|||
|
|
ID: post.ID,
|
|||
|
|
UserID: post.UserID,
|
|||
|
|
Title: post.Title,
|
|||
|
|
Content: post.Content,
|
|||
|
|
LikesCount: post.LikesCount,
|
|||
|
|
CommentsCount: post.CommentsCount,
|
|||
|
|
FavoritesCount: post.FavoritesCount,
|
|||
|
|
SharesCount: post.SharesCount,
|
|||
|
|
ViewsCount: post.ViewsCount,
|
|||
|
|
IsPinned: post.IsPinned,
|
|||
|
|
IsLocked: post.IsLocked,
|
|||
|
|
IsVote: post.IsVote,
|
|||
|
|
CreatedAt: dto.FormatTime(post.CreatedAt),
|
|||
|
|
Images: make([]dto.PostImageResponse, 0, len(post.Images)),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换图片
|
|||
|
|
for _, img := range post.Images {
|
|||
|
|
response.Images = append(response.Images, dto.PostImageResponse{
|
|||
|
|
ID: img.ID,
|
|||
|
|
URL: img.URL,
|
|||
|
|
ThumbnailURL: img.ThumbnailURL,
|
|||
|
|
Width: img.Width,
|
|||
|
|
Height: img.Height,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换作者信息
|
|||
|
|
if post.User != nil {
|
|||
|
|
response.Author = &dto.UserResponse{
|
|||
|
|
ID: post.User.ID,
|
|||
|
|
Username: post.User.Username,
|
|||
|
|
Nickname: post.User.Nickname,
|
|||
|
|
Avatar: post.User.Avatar,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return response
|
|||
|
|
}
|