2026-04-23 22:29:34 +08:00
|
|
|
|
package service
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"strings"
|
2026-03-12 08:38:14 +08:00
|
|
|
|
"time"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
2026-04-22 16:01:59 +08:00
|
|
|
|
"with_you/internal/cache"
|
|
|
|
|
|
"with_you/internal/dto"
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
apperrors "with_you/internal/errors"
|
2026-04-22 16:01:59 +08:00
|
|
|
|
"with_you/internal/model"
|
|
|
|
|
|
"with_you/internal/pkg/hook"
|
|
|
|
|
|
"with_you/internal/repository"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
// VoteService 投票服务接口
|
|
|
|
|
|
type VoteService interface {
|
|
|
|
|
|
CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error)
|
|
|
|
|
|
GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error)
|
|
|
|
|
|
GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error)
|
|
|
|
|
|
Vote(ctx context.Context, postID, userID, optionID string) error
|
|
|
|
|
|
Unvote(ctx context.Context, postID, userID string) error
|
|
|
|
|
|
UpdateVoteOption(ctx context.Context, postID, optionID, userID, content string) error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// voteService 投票服务实现
|
|
|
|
|
|
type voteService struct {
|
2026-03-26 18:14:16 +08:00
|
|
|
|
voteRepo repository.VoteRepository
|
|
|
|
|
|
postRepo repository.PostRepository
|
2026-03-09 21:28:58 +08:00
|
|
|
|
cache cache.Cache
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
postAIService PostAIService
|
2026-03-09 21:28:58 +08:00
|
|
|
|
systemMessageService SystemMessageService
|
2026-04-11 04:23:24 +08:00
|
|
|
|
hookManager *hook.Manager
|
|
|
|
|
|
logService *LogService
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewVoteService(
|
2026-03-26 18:14:16 +08:00
|
|
|
|
voteRepo repository.VoteRepository,
|
|
|
|
|
|
postRepo repository.PostRepository,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
cache cache.Cache,
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
postAIService PostAIService,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
systemMessageService SystemMessageService,
|
2026-04-11 04:23:24 +08:00
|
|
|
|
hookManager *hook.Manager,
|
|
|
|
|
|
logService *LogService,
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
) VoteService {
|
|
|
|
|
|
return &voteService{
|
2026-03-09 21:28:58 +08:00
|
|
|
|
voteRepo: voteRepo,
|
|
|
|
|
|
postRepo: postRepo,
|
|
|
|
|
|
cache: cache,
|
|
|
|
|
|
postAIService: postAIService,
|
|
|
|
|
|
systemMessageService: systemMessageService,
|
2026-04-11 04:23:24 +08:00
|
|
|
|
hookManager: hookManager,
|
|
|
|
|
|
logService: logService,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateVotePost 创建投票帖子
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if len(req.VoteOptions) < 2 {
|
|
|
|
|
|
return nil, errors.New("投票选项至少需要2个")
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(req.VoteOptions) > 10 {
|
|
|
|
|
|
return nil, errors.New("投票选项最多10个")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:00:38 +08:00
|
|
|
|
// 幂等检查:同一 userID + clientRequestID 在 TTL 内只创建一次。
|
|
|
|
|
|
// 重复请求(多次点击、网络重试)会命中已回填的 postID,直接返回首次创建的帖子。
|
|
|
|
|
|
if req.ClientRequestID != "" && s.cache != nil {
|
|
|
|
|
|
idemKey := cache.PostCreateIdempotentKey(userID, req.ClientRequestID)
|
|
|
|
|
|
result := cache.TryAcquireIdempotency(ctx, s.cache, idemKey, PostCreateIdempotentTTL)
|
|
|
|
|
|
if !result.Acquired {
|
|
|
|
|
|
// 命中已完成的请求:返回首次创建的帖子。
|
|
|
|
|
|
if result.ExistingValue != "" {
|
|
|
|
|
|
if existing, err := s.postRepo.GetByID(result.ExistingValue); err == nil && existing != nil {
|
|
|
|
|
|
return s.convertToPostResponse(existing, userID), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 命中"进行中"占位:前一个请求还在处理,拒绝重复提交。
|
|
|
|
|
|
return nil, ErrDuplicatePostRequest
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:29:34 +08:00
|
|
|
|
// 构建 vote segment
|
|
|
|
|
|
voteOptions := make([]dto.VoteOptionData, len(req.VoteOptions))
|
|
|
|
|
|
for i, opt := range req.VoteOptions {
|
|
|
|
|
|
voteOptions[i] = dto.VoteOptionData{
|
|
|
|
|
|
ID: fmt.Sprintf("opt_%d", i+1),
|
|
|
|
|
|
Content: opt,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
segments := model.MessageSegments{
|
|
|
|
|
|
dto.NewTextSegment(req.Content),
|
|
|
|
|
|
{
|
|
|
|
|
|
Type: string(dto.SegmentTypeVote),
|
|
|
|
|
|
Data: map[string]any{
|
|
|
|
|
|
"options": voteOptions,
|
|
|
|
|
|
"max_choices": 1,
|
|
|
|
|
|
"is_multi_choice": false,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
content := req.Content
|
|
|
|
|
|
if content == "" {
|
|
|
|
|
|
content = " "
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
post := &model.Post{
|
2026-03-24 22:27:53 +08:00
|
|
|
|
UserID: userID,
|
|
|
|
|
|
ChannelID: req.ChannelID,
|
|
|
|
|
|
Title: req.Title,
|
2026-04-23 22:29:34 +08:00
|
|
|
|
Content: content,
|
|
|
|
|
|
Segments: segments,
|
2026-03-24 22:27:53 +08:00
|
|
|
|
Status: model.PostStatusPending,
|
|
|
|
|
|
IsVote: true,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := s.postRepo.Create(post, req.Images)
|
|
|
|
|
|
if err != nil {
|
2026-06-26 17:00:38 +08:00
|
|
|
|
// 创建失败:释放幂等占位,允许用户重试。
|
|
|
|
|
|
if req.ClientRequestID != "" && s.cache != nil {
|
|
|
|
|
|
cache.ReleaseIdempotency(ctx, s.cache, cache.PostCreateIdempotentKey(userID, req.ClientRequestID))
|
|
|
|
|
|
}
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:00:38 +08:00
|
|
|
|
// 创建成功:回填幂等键为 postID,使后续重复请求返回同一帖子。
|
|
|
|
|
|
if req.ClientRequestID != "" && s.cache != nil {
|
|
|
|
|
|
cache.CompleteIdempotency(ctx, s.cache, cache.PostCreateIdempotentKey(userID, req.ClientRequestID), post.ID, PostCreateIdempotentTTL)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 异步审核
|
2026-04-23 22:29:34 +08:00
|
|
|
|
go s.reviewVotePostAsync(post.ID, userID, req.Title, content, req.Images)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 重新查询以获取关联的User和Images
|
|
|
|
|
|
createdPost, err := s.postRepo.GetByID(post.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s.convertToPostResponse(createdPost, userID), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) reviewVotePostAsync(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 vote 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 vote post %s after panic recovery: %v", postID, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2026-04-11 04:23:24 +08:00
|
|
|
|
if s.hookManager == nil {
|
2026-03-12 08:38:14 +08:00
|
|
|
|
if err := s.updateModerationStatusWithRetry(postID, model.PostStatusPublished, "", "system"); err != nil {
|
2026-04-11 04:23:24 +08:00
|
|
|
|
log.Printf("[WARN] Failed to publish vote post without hook manager: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 04:23:24 +08:00
|
|
|
|
var authorID uint
|
2026-04-30 12:26:25 +08:00
|
|
|
|
if _, err := fmt.Sscanf(userID, "%d", &authorID); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Invalid userID format in vote moderation: %q, err: %v", userID, err)
|
|
|
|
|
|
if err := s.updateModerationStatusWithRetry(postID, model.PostStatusPublished, "", "system"); err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to publish vote post %s after invalid userID: %v", postID, err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-11 04:23:24 +08:00
|
|
|
|
|
|
|
|
|
|
result := &hook.ModerationResult{}
|
|
|
|
|
|
s.hookManager.TriggerWithMetadata(context.Background(), hook.HookPostPreModerate, authorID, &hook.PostModerateHookData{
|
|
|
|
|
|
PostID: postID,
|
|
|
|
|
|
Title: title,
|
|
|
|
|
|
Content: content,
|
|
|
|
|
|
Images: images,
|
|
|
|
|
|
AuthorID: authorID,
|
|
|
|
|
|
}, map[string]any{
|
|
|
|
|
|
"result": result,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
s.hookManager.Trigger(context.Background(), hook.HookPostModerated, authorID, &hook.PostModeratedHookData{
|
|
|
|
|
|
PostID: postID,
|
|
|
|
|
|
AuthorID: authorID,
|
|
|
|
|
|
Approved: result.Approved,
|
|
|
|
|
|
RejectReason: result.RejectReason,
|
|
|
|
|
|
ReviewedBy: result.ReviewedBy,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if !result.Approved {
|
|
|
|
|
|
if result.NeedsReview {
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-11 04:23:24 +08:00
|
|
|
|
if updateErr := s.updateModerationStatusWithRetry(postID, model.PostStatusRejected, result.RejectReason, result.ReviewedBy); updateErr != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to reject vote post %s: %v", postID, updateErr)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
2026-04-11 04:23:24 +08:00
|
|
|
|
s.notifyModerationRejected(userID, result.RejectReason)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 04:23:24 +08:00
|
|
|
|
if err := s.updateModerationStatusWithRetry(postID, model.PostStatusPublished, "", result.ReviewedBy); err != nil {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
log.Printf("[WARN] Failed to publish vote post %s: %v", postID, err)
|
2026-04-11 04:23:24 +08:00
|
|
|
|
return
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
2026-04-11 04:23:24 +08:00
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
cache.InvalidatePostList(s.cache)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) 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 for vote post failed post=%s attempt=%d/%d err=%v", postID, attempt, maxAttempts, err)
|
|
|
|
|
|
time.Sleep(time.Duration(attempt) * retryDelay)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return lastErr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) 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() {
|
|
|
|
|
|
_ = s.systemMessageService.SendSystemAnnouncement(
|
|
|
|
|
|
context.Background(),
|
|
|
|
|
|
[]string{userID},
|
|
|
|
|
|
"投票帖审核未通过",
|
|
|
|
|
|
content,
|
|
|
|
|
|
)
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:29:34 +08:00
|
|
|
|
// resolveVoteOptions 解析投票选项(优先从 segments 读取,兼容旧数据从 vote_options 表读取)
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) resolveVoteOptions(postID string, segments model.MessageSegments) ([]dto.VoteOptionDTO, error) {
|
2026-04-23 22:29:34 +08:00
|
|
|
|
if len(segments) > 0 {
|
|
|
|
|
|
voteData := dto.ExtractVoteSegmentData(segments)
|
|
|
|
|
|
if voteData != nil {
|
|
|
|
|
|
result := make([]dto.VoteOptionDTO, 0, len(voteData.Options))
|
|
|
|
|
|
for _, opt := range voteData.Options {
|
|
|
|
|
|
count, err := s.voteRepo.CountVotesByOption(postID, opt.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("[WARN] Failed to count votes for option %s in post %s: %v", opt.ID, postID, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
result = append(result, dto.VoteOptionDTO{
|
|
|
|
|
|
ID: opt.ID,
|
|
|
|
|
|
Content: opt.Content,
|
|
|
|
|
|
VotesCount: count,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:29:34 +08:00
|
|
|
|
// GetVoteOptions 获取投票选项(优先从 segments 读取,兼容旧数据从 vote_options 表读取)
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error) {
|
2026-04-23 22:29:34 +08:00
|
|
|
|
post, err := s.postRepo.GetByID(postID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s.resolveVoteOptions(postID, post.Segments)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// GetVoteResult 获取投票结果(包含用户投票状态)
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error) {
|
2026-04-23 22:29:34 +08:00
|
|
|
|
post, err := s.postRepo.GetByID(postID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
optionDTOs, err := s.resolveVoteOptions(postID, post.Segments)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
userVote, err := s.voteRepo.GetUserVote(postID, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := &dto.VoteResultDTO{
|
2026-04-23 22:29:34 +08:00
|
|
|
|
Options: optionDTOs,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
TotalVotes: 0,
|
|
|
|
|
|
HasVoted: userVote != nil,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if userVote != nil {
|
|
|
|
|
|
result.VotedOptionID = userVote.OptionID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 22:29:34 +08:00
|
|
|
|
for _, opt := range optionDTOs {
|
|
|
|
|
|
result.TotalVotes += opt.VotesCount
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Vote 投票
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) Vote(ctx context.Context, postID, userID, optionID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 调用voteRepo.Vote
|
|
|
|
|
|
err := s.voteRepo.Vote(postID, userID, optionID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Unvote 取消投票
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) Unvote(ctx context.Context, postID, userID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 调用voteRepo.Unvote
|
|
|
|
|
|
err := s.voteRepo.Unvote(postID, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失效帖子详情缓存
|
|
|
|
|
|
cache.InvalidatePostDetail(s.cache, postID)
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateVoteOption 更新投票选项(作者权限)
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) UpdateVoteOption(ctx context.Context, postID, optionID, userID, content string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 获取帖子信息
|
|
|
|
|
|
post, err := s.postRepo.GetByID(postID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证用户是否为帖子作者
|
|
|
|
|
|
if post.UserID != userID {
|
2026-04-30 12:26:25 +08:00
|
|
|
|
return apperrors.ErrForbidden
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用voteRepo.UpdateOption
|
|
|
|
|
|
return s.voteRepo.UpdateOption(optionID, content)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// convertToPostResponse 将Post模型转换为PostResponse DTO
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (s *voteService) convertToPostResponse(post *model.Post, currentUserID string) *dto.PostResponse {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if post == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response := &dto.PostResponse{
|
2026-04-11 04:23:24 +08:00
|
|
|
|
ID: post.ID,
|
|
|
|
|
|
UserID: post.UserID,
|
|
|
|
|
|
Title: post.Title,
|
|
|
|
|
|
Content: post.Content,
|
2026-04-23 22:29:34 +08:00
|
|
|
|
Segments: dto.SegmentsOrDefault(post.Segments, post.Content),
|
2026-04-11 04:23:24 +08:00
|
|
|
|
LikesCount: post.LikesCount,
|
|
|
|
|
|
CommentsCount: post.CommentsCount,
|
|
|
|
|
|
FavoritesCount: post.FavoritesCount,
|
|
|
|
|
|
SharesCount: post.SharesCount,
|
|
|
|
|
|
ViewsCount: post.ViewsCount,
|
|
|
|
|
|
IsPinned: post.IsPinned,
|
|
|
|
|
|
IsLocked: post.IsLocked,
|
|
|
|
|
|
IsVote: post.IsVote,
|
2026-03-23 14:08:36 +08:00
|
|
|
|
CreatedAt: dto.FormatTime(post.CreatedAt),
|
|
|
|
|
|
UpdatedAt: dto.FormatTime(post.UpdatedAt),
|
|
|
|
|
|
ContentEditedAt: dto.FormatTimePointer(post.ContentEditedAt),
|
|
|
|
|
|
Images: make([]dto.PostImageResponse, 0, len(post.Images)),
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换图片
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|