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 }