feat(api): add message segments support for posts and comments
Add rich message segment support enabling structured content (text, images, votes, mentions) in posts and comments. Includes segments field in models, DTOs, handlers, and services. Also adds @mention notification processing and migrates vote data to embedded segments.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"with_you/internal/model"
|
|
||||||
"time"
|
"time"
|
||||||
|
"with_you/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== User DTOs ====================
|
// ==================== User DTOs ====================
|
||||||
@@ -143,54 +143,56 @@ type PostImageResponse struct {
|
|||||||
|
|
||||||
// PostResponse 帖子响应(列表用)
|
// PostResponse 帖子响应(列表用)
|
||||||
type PostResponse struct {
|
type PostResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
ChannelID *string `json:"channel_id,omitempty"`
|
ChannelID *string `json:"channel_id,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Images []PostImageResponse `json:"images"`
|
Segments model.MessageSegments `json:"segments,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Images []PostImageResponse `json:"images"`
|
||||||
LikesCount int `json:"likes_count"`
|
Status string `json:"status,omitempty"`
|
||||||
CommentsCount int `json:"comments_count"`
|
LikesCount int `json:"likes_count"`
|
||||||
FavoritesCount int `json:"favorites_count"`
|
CommentsCount int `json:"comments_count"`
|
||||||
SharesCount int `json:"shares_count"`
|
FavoritesCount int `json:"favorites_count"`
|
||||||
ViewsCount int `json:"views_count"`
|
SharesCount int `json:"shares_count"`
|
||||||
IsPinned bool `json:"is_pinned"`
|
ViewsCount int `json:"views_count"`
|
||||||
IsLocked bool `json:"is_locked"`
|
IsPinned bool `json:"is_pinned"`
|
||||||
IsVote bool `json:"is_vote"`
|
IsLocked bool `json:"is_locked"`
|
||||||
CreatedAt string `json:"created_at"`
|
IsVote bool `json:"is_vote"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
ContentEditedAt string `json:"content_edited_at,omitempty"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
Author *UserResponse `json:"author"`
|
ContentEditedAt string `json:"content_edited_at,omitempty"`
|
||||||
IsLiked bool `json:"is_liked"`
|
Author *UserResponse `json:"author"`
|
||||||
IsFavorited bool `json:"is_favorited"`
|
IsLiked bool `json:"is_liked"`
|
||||||
Channel *PostChannelBrief `json:"channel,omitempty"`
|
IsFavorited bool `json:"is_favorited"`
|
||||||
|
Channel *PostChannelBrief `json:"channel,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostDetailResponse 帖子详情响应
|
// PostDetailResponse 帖子详情响应
|
||||||
type PostDetailResponse struct {
|
type PostDetailResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
ChannelID *string `json:"channel_id,omitempty"`
|
ChannelID *string `json:"channel_id,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Images []PostImageResponse `json:"images"`
|
Segments model.MessageSegments `json:"segments,omitempty"`
|
||||||
Status string `json:"status"`
|
Images []PostImageResponse `json:"images"`
|
||||||
LikesCount int `json:"likes_count"`
|
Status string `json:"status"`
|
||||||
CommentsCount int `json:"comments_count"`
|
LikesCount int `json:"likes_count"`
|
||||||
FavoritesCount int `json:"favorites_count"`
|
CommentsCount int `json:"comments_count"`
|
||||||
SharesCount int `json:"shares_count"`
|
FavoritesCount int `json:"favorites_count"`
|
||||||
ViewsCount int `json:"views_count"`
|
SharesCount int `json:"shares_count"`
|
||||||
IsPinned bool `json:"is_pinned"`
|
ViewsCount int `json:"views_count"`
|
||||||
IsLocked bool `json:"is_locked"`
|
IsPinned bool `json:"is_pinned"`
|
||||||
IsVote bool `json:"is_vote"`
|
IsLocked bool `json:"is_locked"`
|
||||||
CreatedAt string `json:"created_at"`
|
IsVote bool `json:"is_vote"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
ContentEditedAt string `json:"content_edited_at,omitempty"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
Author *UserResponse `json:"author"`
|
ContentEditedAt string `json:"content_edited_at,omitempty"`
|
||||||
IsLiked bool `json:"is_liked"`
|
Author *UserResponse `json:"author"`
|
||||||
IsFavorited bool `json:"is_favorited"`
|
IsLiked bool `json:"is_liked"`
|
||||||
Channel *PostChannelBrief `json:"channel,omitempty"`
|
IsFavorited bool `json:"is_favorited"`
|
||||||
|
Channel *PostChannelBrief `json:"channel,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Comment DTOs ====================
|
// ==================== Comment DTOs ====================
|
||||||
@@ -209,6 +211,7 @@ type CommentResponse struct {
|
|||||||
ParentID *string `json:"parent_id"`
|
ParentID *string `json:"parent_id"`
|
||||||
RootID *string `json:"root_id"`
|
RootID *string `json:"root_id"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Segments model.MessageSegments `json:"segments,omitempty"`
|
||||||
Images []CommentImageResponse `json:"images"`
|
Images []CommentImageResponse `json:"images"`
|
||||||
LikesCount int `json:"likes_count"`
|
LikesCount int `json:"likes_count"`
|
||||||
RepliesCount int `json:"replies_count"`
|
RepliesCount int `json:"replies_count"`
|
||||||
@@ -248,6 +251,7 @@ const (
|
|||||||
SegmentTypeReply SegmentType = "reply"
|
SegmentTypeReply SegmentType = "reply"
|
||||||
SegmentTypeFace SegmentType = "face"
|
SegmentTypeFace SegmentType = "face"
|
||||||
SegmentTypeLink SegmentType = "link"
|
SegmentTypeLink SegmentType = "link"
|
||||||
|
SegmentTypeVote SegmentType = "vote"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TextSegmentData 文本数据
|
// TextSegmentData 文本数据
|
||||||
@@ -315,6 +319,19 @@ type LinkSegmentData struct {
|
|||||||
Image string `json:"image,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VoteOptionData 投票选项数据
|
||||||
|
type VoteOptionData struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoteSegmentData 投票Segment数据
|
||||||
|
type VoteSegmentData struct {
|
||||||
|
Options []VoteOptionData `json:"options"`
|
||||||
|
MaxChoices int `json:"max_choices,omitempty"`
|
||||||
|
IsMultiChoice bool `json:"is_multi_choice,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Message DTOs ====================
|
// ==================== Message DTOs ====================
|
||||||
|
|
||||||
// MessageResponse 消息响应
|
// MessageResponse 消息响应
|
||||||
@@ -887,7 +904,16 @@ type CreateVotePostRequest struct {
|
|||||||
Content string `json:"content" binding:"max=2000"`
|
Content string `json:"content" binding:"max=2000"`
|
||||||
ChannelID *string `json:"channel_id,omitempty"`
|
ChannelID *string `json:"channel_id,omitempty"`
|
||||||
Images []string `json:"images"`
|
Images []string `json:"images"`
|
||||||
VoteOptions []string `json:"vote_options" binding:"required,min=2,max=10"` // 投票选项,至少2个最多10个
|
VoteOptions []string `json:"vote_options" binding:"required,min=2,max=10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePostWithSegmentsRequest 创建帖子请求(含 segments)
|
||||||
|
type CreatePostWithSegmentsRequest struct {
|
||||||
|
Title string `json:"title" binding:"required,max=200"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Segments model.MessageSegments `json:"segments"`
|
||||||
|
Images []string `json:"images"`
|
||||||
|
ChannelID *string `json:"channel_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VoteOptionDTO 投票选项DTO
|
// VoteOptionDTO 投票选项DTO
|
||||||
@@ -943,29 +969,30 @@ type AdminPostListResponse struct {
|
|||||||
|
|
||||||
// AdminPostDetailResponse 管理端帖子详情响应
|
// AdminPostDetailResponse 管理端帖子详情响应
|
||||||
type AdminPostDetailResponse struct {
|
type AdminPostDetailResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
ChannelID *string `json:"channel_id,omitempty"`
|
ChannelID *string `json:"channel_id,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Images []PostImageResponse `json:"images"`
|
Segments model.MessageSegments `json:"segments,omitempty"`
|
||||||
Status string `json:"status"`
|
Images []PostImageResponse `json:"images"`
|
||||||
LikesCount int `json:"likes_count,omitzero"`
|
Status string `json:"status"`
|
||||||
CommentsCount int `json:"comments_count,omitzero"`
|
LikesCount int `json:"likes_count,omitzero"`
|
||||||
FavoritesCount int `json:"favorites_count,omitzero"`
|
CommentsCount int `json:"comments_count,omitzero"`
|
||||||
SharesCount int `json:"shares_count,omitzero"`
|
FavoritesCount int `json:"favorites_count,omitzero"`
|
||||||
ViewsCount int `json:"views_count,omitzero"`
|
SharesCount int `json:"shares_count,omitzero"`
|
||||||
IsPinned bool `json:"is_pinned"`
|
ViewsCount int `json:"views_count,omitzero"`
|
||||||
IsFeatured bool `json:"is_featured"`
|
IsPinned bool `json:"is_pinned"`
|
||||||
IsLocked bool `json:"is_locked"`
|
IsFeatured bool `json:"is_featured"`
|
||||||
IsVote bool `json:"is_vote"`
|
IsLocked bool `json:"is_locked"`
|
||||||
RejectReason string `json:"reject_reason,omitempty"`
|
IsVote bool `json:"is_vote"`
|
||||||
ReviewedAt string `json:"reviewed_at,omitempty"`
|
RejectReason string `json:"reject_reason,omitempty"`
|
||||||
ReviewedBy string `json:"reviewed_by,omitempty"`
|
ReviewedAt string `json:"reviewed_at,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
ReviewedBy string `json:"reviewed_by,omitempty"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
ContentEditedAt string `json:"content_edited_at,omitempty"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
Author *UserResponse `json:"author"`
|
ContentEditedAt string `json:"content_edited_at,omitempty"`
|
||||||
|
Author *UserResponse `json:"author"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminModeratePostRequest 审核帖子请求
|
// AdminModeratePostRequest 审核帖子请求
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"with_you/internal/model"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"with_you/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== Post 转换 ====================
|
// ==================== Post 转换 ====================
|
||||||
@@ -67,6 +67,20 @@ func PostChannelBriefForPost(post *model.Post, channelByID map[string]*model.Cha
|
|||||||
return postChannelBrief(post, channelByID)
|
return postChannelBrief(post, channelByID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SegmentsOrDefault 如果 segments 为空但 content 非空,
|
||||||
|
// 返回降级的单 text segment;否则原样返回。
|
||||||
|
func SegmentsOrDefault(segments model.MessageSegments, content string) model.MessageSegments {
|
||||||
|
if len(segments) > 0 {
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
if content != "" {
|
||||||
|
return model.MessageSegments{
|
||||||
|
{Type: string(SegmentTypeText), Data: map[string]any{"text": content}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ConvertPostToResponse 将Post转换为PostResponse(列表用);channelByID 可为 nil
|
// ConvertPostToResponse 将Post转换为PostResponse(列表用);channelByID 可为 nil
|
||||||
func ConvertPostToResponse(post *model.Post, channelByID map[string]*model.Channel, isLiked, isFavorited bool) *PostResponse {
|
func ConvertPostToResponse(post *model.Post, channelByID map[string]*model.Channel, isLiked, isFavorited bool) *PostResponse {
|
||||||
if post == nil {
|
if post == nil {
|
||||||
@@ -89,6 +103,7 @@ func ConvertPostToResponse(post *model.Post, channelByID map[string]*model.Chann
|
|||||||
ChannelID: post.ChannelID,
|
ChannelID: post.ChannelID,
|
||||||
Title: post.Title,
|
Title: post.Title,
|
||||||
Content: post.Content,
|
Content: post.Content,
|
||||||
|
Segments: SegmentsOrDefault(post.Segments, post.Content),
|
||||||
Images: images,
|
Images: images,
|
||||||
Status: string(post.Status),
|
Status: string(post.Status),
|
||||||
LikesCount: post.LikesCount,
|
LikesCount: post.LikesCount,
|
||||||
@@ -131,6 +146,7 @@ func ConvertPostToDetailResponse(post *model.Post, channelByID map[string]*model
|
|||||||
ChannelID: post.ChannelID,
|
ChannelID: post.ChannelID,
|
||||||
Title: post.Title,
|
Title: post.Title,
|
||||||
Content: post.Content,
|
Content: post.Content,
|
||||||
|
Segments: SegmentsOrDefault(post.Segments, post.Content),
|
||||||
Images: images,
|
Images: images,
|
||||||
Status: string(post.Status),
|
Status: string(post.Status),
|
||||||
LikesCount: post.LikesCount,
|
LikesCount: post.LikesCount,
|
||||||
@@ -227,6 +243,7 @@ func ConvertCommentToResponse(comment *model.Comment, isLiked bool) *CommentResp
|
|||||||
ParentID: comment.ParentID,
|
ParentID: comment.ParentID,
|
||||||
RootID: comment.RootID,
|
RootID: comment.RootID,
|
||||||
Content: comment.Content,
|
Content: comment.Content,
|
||||||
|
Segments: SegmentsOrDefault(comment.Segments, comment.Content),
|
||||||
Images: images,
|
Images: images,
|
||||||
LikesCount: comment.LikesCount,
|
LikesCount: comment.LikesCount,
|
||||||
RepliesCount: comment.RepliesCount,
|
RepliesCount: comment.RepliesCount,
|
||||||
@@ -303,6 +320,7 @@ func ConvertCommentToResponseWithUser(comment *model.Comment, userID string, che
|
|||||||
ParentID: comment.ParentID,
|
ParentID: comment.ParentID,
|
||||||
RootID: comment.RootID,
|
RootID: comment.RootID,
|
||||||
Content: comment.Content,
|
Content: comment.Content,
|
||||||
|
Segments: SegmentsOrDefault(comment.Segments, comment.Content),
|
||||||
Images: images,
|
Images: images,
|
||||||
LikesCount: comment.LikesCount,
|
LikesCount: comment.LikesCount,
|
||||||
RepliesCount: comment.RepliesCount,
|
RepliesCount: comment.RepliesCount,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -145,6 +145,29 @@ func NewLinkSegment(url, title, description, image string) model.MessageSegment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractVoteSegmentData 从消息链中提取投票Segment数据
|
||||||
|
func ExtractVoteSegmentData(segments model.MessageSegments) *VoteSegmentData {
|
||||||
|
for _, segment := range segments {
|
||||||
|
if segment.Type == string(SegmentTypeVote) {
|
||||||
|
dataBytes, err := json.Marshal(segment.Data)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var voteData VoteSegmentData
|
||||||
|
if err := json.Unmarshal(dataBytes, &voteData); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return &voteData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasVoteSegment 检查消息链中是否包含投票Segment
|
||||||
|
func HasVoteSegment(segments model.MessageSegments) bool {
|
||||||
|
return HasSegmentType(segments, SegmentTypeVote)
|
||||||
|
}
|
||||||
|
|
||||||
// ExtractTextContentFromJSON 从JSON格式的segments中提取纯文本内容
|
// ExtractTextContentFromJSON 从JSON格式的segments中提取纯文本内容
|
||||||
// 用于从数据库读取的 []byte 格式的 segments
|
// 用于从数据库读取的 []byte 格式的 segments
|
||||||
// 已废弃:现在数据库直接存储 model.MessageSegments 类型
|
// 已废弃:现在数据库直接存储 model.MessageSegments 类型
|
||||||
@@ -202,6 +225,8 @@ func ExtractTextContentFromModel(segments model.MessageSegments) string {
|
|||||||
} else {
|
} else {
|
||||||
result += "[链接]"
|
result += "[链接]"
|
||||||
}
|
}
|
||||||
|
case "vote":
|
||||||
|
result += "[投票]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"with_you/internal/dto"
|
"with_you/internal/dto"
|
||||||
|
"with_you/internal/model"
|
||||||
"with_you/internal/pkg/cursor"
|
"with_you/internal/pkg/cursor"
|
||||||
"with_you/internal/pkg/response"
|
"with_you/internal/pkg/response"
|
||||||
"with_you/internal/service"
|
"with_you/internal/service"
|
||||||
@@ -33,10 +34,11 @@ func (h *CommentHandler) Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateRequest struct {
|
type CreateRequest struct {
|
||||||
PostID string `json:"post_id" binding:"required"`
|
PostID string `json:"post_id" binding:"required"`
|
||||||
Content string `json:"content"` // 内容可选,允许只发图片
|
Content string `json:"content"`
|
||||||
ParentID *string `json:"parent_id"`
|
Segments model.MessageSegments `json:"segments"`
|
||||||
Images []string `json:"images"` // 图片URL列表
|
ParentID *string `json:"parent_id"`
|
||||||
|
Images []string `json:"images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var req CreateRequest
|
var req CreateRequest
|
||||||
@@ -45,12 +47,18 @@ func (h *CommentHandler) Create(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证:评论必须有内容或图片
|
// 验证:评论必须有内容或图片或segments
|
||||||
if req.Content == "" && len(req.Images) == 0 {
|
if req.Content == "" && len(req.Images) == 0 && len(req.Segments) == 0 {
|
||||||
response.BadRequest(c, "评论内容或图片不能同时为空")
|
response.BadRequest(c, "评论内容或图片不能同时为空")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有content但有segments,从segments提取文本
|
||||||
|
content := req.Content
|
||||||
|
if content == "" && len(req.Segments) > 0 {
|
||||||
|
content = dto.ExtractTextContent(req.Segments)
|
||||||
|
}
|
||||||
|
|
||||||
// 将图片列表转换为JSON字符串
|
// 将图片列表转换为JSON字符串
|
||||||
var imagesJSON string
|
var imagesJSON string
|
||||||
if len(req.Images) > 0 {
|
if len(req.Images) > 0 {
|
||||||
@@ -58,7 +66,7 @@ func (h *CommentHandler) Create(c *gin.Context) {
|
|||||||
imagesJSON = string(imagesBytes)
|
imagesJSON = string(imagesBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
comment, err := h.commentService.Create(c.Request.Context(), req.PostID, userID, req.Content, req.ParentID, imagesJSON, req.Images)
|
comment, err := h.commentService.Create(c.Request.Context(), req.PostID, userID, content, req.Segments, req.ParentID, imagesJSON, req.Images)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.InternalServerError(c, "failed to create comment")
|
response.InternalServerError(c, "failed to create comment")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -52,20 +52,21 @@ func (h *PostHandler) Create(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateRequest struct {
|
var req dto.CreatePostWithSegmentsRequest
|
||||||
Title string `json:"title" binding:"required"`
|
|
||||||
Content string `json:"content" binding:"required"`
|
|
||||||
Images []string `json:"images"`
|
|
||||||
ChannelID *string `json:"channel_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var req CreateRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
post, err := h.postService.Create(c.Request.Context(), userID, req.Title, req.Content, req.Images, req.ChannelID)
|
content := req.Content
|
||||||
|
segments := req.Segments
|
||||||
|
|
||||||
|
if len(segments) == 0 && content == "" {
|
||||||
|
response.BadRequest(c, "content or segments is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := h.postService.Create(c.Request.Context(), userID, req.Title, content, segments, req.Images, req.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var moderationErr *service.PostModerationRejectedError
|
var moderationErr *service.PostModerationRejectedError
|
||||||
if errors.As(err, &moderationErr) {
|
if errors.As(err, &moderationErr) {
|
||||||
@@ -123,6 +124,7 @@ func (h *PostHandler) GetByID(c *gin.Context) {
|
|||||||
ChannelID: post.ChannelID,
|
ChannelID: post.ChannelID,
|
||||||
Title: post.Title,
|
Title: post.Title,
|
||||||
Content: post.Content,
|
Content: post.Content,
|
||||||
|
Segments: dto.SegmentsOrDefault(post.Segments, post.Content),
|
||||||
Images: dto.ConvertPostImagesToResponse(post.Images),
|
Images: dto.ConvertPostImagesToResponse(post.Images),
|
||||||
Status: string(post.Status),
|
Status: string(post.Status),
|
||||||
LikesCount: post.LikesCount,
|
LikesCount: post.LikesCount,
|
||||||
@@ -365,9 +367,10 @@ func (h *PostHandler) Update(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateRequest struct {
|
type UpdateRequest struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Images *[]string `json:"images"`
|
Segments model.MessageSegments `json:"segments"`
|
||||||
|
Images *[]string `json:"images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var req UpdateRequest
|
var req UpdateRequest
|
||||||
@@ -382,6 +385,12 @@ func (h *PostHandler) Update(c *gin.Context) {
|
|||||||
if req.Content != "" {
|
if req.Content != "" {
|
||||||
post.Content = req.Content
|
post.Content = req.Content
|
||||||
}
|
}
|
||||||
|
if len(req.Segments) > 0 {
|
||||||
|
post.Segments = req.Segments
|
||||||
|
if req.Content == "" {
|
||||||
|
post.Content = dto.ExtractTextContent(req.Segments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = h.postService.UpdateWithImages(c.Request.Context(), post, req.Images)
|
err = h.postService.UpdateWithImages(c.Request.Context(), post, req.Images)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ const (
|
|||||||
|
|
||||||
// Comment 评论实体
|
// Comment 评论实体
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||||
PostID string `json:"post_id" gorm:"type:varchar(36);not null;index:idx_comments_post_parent_status_created,priority:1"`
|
PostID string `json:"post_id" gorm:"type:varchar(36);not null;index:idx_comments_post_parent_status_created,priority:1"`
|
||||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
|
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
|
||||||
ParentID *string `json:"parent_id" gorm:"type:varchar(36);index:idx_comments_post_parent_status_created,priority:2"` // 父评论 ID(支持嵌套)
|
ParentID *string `json:"parent_id" gorm:"type:varchar(36);index:idx_comments_post_parent_status_created,priority:2"` // 父评论 ID(支持嵌套)
|
||||||
RootID *string `json:"root_id" gorm:"type:varchar(36);index:idx_comments_root_status_created,priority:1"` // 根评论 ID(用于高效查询)
|
RootID *string `json:"root_id" gorm:"type:varchar(36);index:idx_comments_root_status_created,priority:1"` // 根评论 ID(用于高效查询)
|
||||||
Content string `json:"content" gorm:"type:text;not null"`
|
Content string `json:"content" gorm:"type:text;not null"`
|
||||||
Images string `json:"images" gorm:"type:text"` // 图片URL列表,JSON数组格式
|
Segments MessageSegments `json:"segments,omitempty" gorm:"type:text"`
|
||||||
|
Images string `json:"images" gorm:"type:text"` // 图片URL列表,JSON数组格式
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
User *User `json:"-" gorm:"foreignKey:UserID"`
|
User *User `json:"-" gorm:"foreignKey:UserID"`
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ const (
|
|||||||
|
|
||||||
// Post 帖子实体
|
// Post 帖子实体
|
||||||
type Post struct {
|
type Post struct {
|
||||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;index:idx_posts_user_status_created,priority:1;not null"`
|
UserID string `json:"user_id" gorm:"type:varchar(36);index;index:idx_posts_user_status_created,priority:1;not null"`
|
||||||
ChannelID *string `json:"channel_id,omitempty" gorm:"type:varchar(36);index"`
|
ChannelID *string `json:"channel_id,omitempty" gorm:"type:varchar(36);index"`
|
||||||
Title string `json:"title" gorm:"type:varchar(200);not null"`
|
Title string `json:"title" gorm:"type:varchar(200);not null"`
|
||||||
Content string `json:"content" gorm:"type:text;not null"`
|
Content string `json:"content" gorm:"type:text;not null"`
|
||||||
|
Segments MessageSegments `json:"segments,omitempty" gorm:"type:text"`
|
||||||
|
|
||||||
// 关联
|
// 关联
|
||||||
// User 需要参与缓存序列化;否则列表命中缓存后会丢失作者信息,前端退化为“匿名用户”
|
// User 需要参与缓存序列化;否则列表命中缓存后会丢失作者信息,前端退化为“匿名用户”
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"with_you/internal/model"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"with_you/internal/model"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"with_you/internal/model"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"with_you/internal/model"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -16,6 +16,7 @@ type VoteRepository interface {
|
|||||||
GetUserVote(postID, userID string) (*model.UserVote, error)
|
GetUserVote(postID, userID string) (*model.UserVote, error)
|
||||||
UpdateOption(optionID, content string) error
|
UpdateOption(optionID, content string) error
|
||||||
DeleteOptionsByPostID(postID string) error
|
DeleteOptionsByPostID(postID string) error
|
||||||
|
CountVotesByOption(postID, optionID string) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// voteRepository 投票仓储实现
|
// voteRepository 投票仓储实现
|
||||||
@@ -72,9 +73,9 @@ func (r *voteRepository) Vote(postID, userID, optionID string) error {
|
|||||||
// 验证选项是否属于该帖子
|
// 验证选项是否属于该帖子
|
||||||
var option model.VoteOption
|
var option model.VoteOption
|
||||||
if err := tx.Where("id = ? AND post_id = ?", optionID, postID).First(&option).Error; err != nil {
|
if err := tx.Where("id = ? AND post_id = ?", optionID, postID).First(&option).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return errors.New("invalid option")
|
return errors.New("invalid option")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +101,9 @@ func (r *voteRepository) Unvote(postID, userID string) error {
|
|||||||
var userVote model.UserVote
|
var userVote model.UserVote
|
||||||
err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error
|
err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil // 没有投票记录,直接返回
|
return nil // 没有投票记录,直接返回
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,12 +145,16 @@ func (r *voteRepository) UpdateOption(optionID, content string) error {
|
|||||||
// DeleteOptionsByPostID 删除帖子的所有投票选项
|
// DeleteOptionsByPostID 删除帖子的所有投票选项
|
||||||
func (r *voteRepository) DeleteOptionsByPostID(postID string) error {
|
func (r *voteRepository) DeleteOptionsByPostID(postID string) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
// 删除关联的用户投票记录
|
|
||||||
if err := tx.Where("post_id = ?", postID).Delete(&model.UserVote{}).Error; err != nil {
|
if err := tx.Where("post_id = ?", postID).Delete(&model.UserVote{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除投票选项
|
|
||||||
return tx.Where("post_id = ?", postID).Delete(&model.VoteOption{}).Error
|
return tx.Where("post_id = ?", postID).Delete(&model.VoteOption{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountVotesByOption 统计指定选项的投票数
|
||||||
|
func (r *voteRepository) CountVotesByOption(postID, optionID string) (int, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.Model(&model.UserVote{}).Where("post_id = ? AND option_id = ?", postID, optionID).Count(&count).Error
|
||||||
|
return int(count), err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"with_you/internal/cache"
|
"with_you/internal/cache"
|
||||||
|
"with_you/internal/dto"
|
||||||
"with_you/internal/model"
|
"with_you/internal/model"
|
||||||
"with_you/internal/pkg/cursor"
|
"with_you/internal/pkg/cursor"
|
||||||
"with_you/internal/pkg/hook"
|
"with_you/internal/pkg/hook"
|
||||||
@@ -37,7 +39,7 @@ func NewCommentService(commentRepo repository.CommentRepository, postRepo reposi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create 创建评论
|
// Create 创建评论
|
||||||
func (s *CommentService) Create(ctx context.Context, postID, userID, content string, parentID *string, images string, imageURLs []string) (*model.Comment, error) {
|
func (s *CommentService) Create(ctx context.Context, postID, userID, content string, segments model.MessageSegments, parentID *string, images string, imageURLs []string) (*model.Comment, error) {
|
||||||
if s.postAIService != nil {
|
if s.postAIService != nil {
|
||||||
// 采用异步审核,前端先立即返回
|
// 采用异步审核,前端先立即返回
|
||||||
}
|
}
|
||||||
@@ -52,6 +54,7 @@ func (s *CommentService) Create(ctx context.Context, postID, userID, content str
|
|||||||
PostID: postID,
|
PostID: postID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
Segments: segments,
|
||||||
ParentID: parentID,
|
ParentID: parentID,
|
||||||
Images: images,
|
Images: images,
|
||||||
Status: model.CommentStatusPending,
|
Status: model.CommentStatusPending,
|
||||||
@@ -94,11 +97,38 @@ func (s *CommentService) Create(ctx context.Context, postID, userID, content str
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 异步处理 @提及通知
|
||||||
|
if len(segments) > 0 {
|
||||||
|
bgCtx := context.WithoutCancel(ctx)
|
||||||
|
go s.processCommentMentionNotifications(bgCtx, userID, postID, post.Title, segments)
|
||||||
|
}
|
||||||
|
|
||||||
go s.reviewCommentAsync(comment.ID, userID, postID, content, imageURLs, parentID, parentUserID, post.UserID)
|
go s.reviewCommentAsync(comment.ID, userID, postID, content, imageURLs, parentID, parentUserID, post.UserID)
|
||||||
|
|
||||||
return comment, nil
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processCommentMentionNotifications 处理评论中 @提及的通知
|
||||||
|
func (s *CommentService) processCommentMentionNotifications(ctx context.Context, authorID, postID, postTitle string, segments model.MessageSegments) {
|
||||||
|
if s.systemMessageService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mentionedUserIDs := dto.ExtractMentionedUsers(segments)
|
||||||
|
if len(mentionedUserIDs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetUserID := range mentionedUserIDs {
|
||||||
|
if targetUserID == authorID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.systemMessageService.SendMentionNotification(ctx, targetUserID, authorID, postID); err != nil {
|
||||||
|
log.Printf("[WARN] Failed to send comment mention notification to user %s: %v", targetUserID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *CommentService) reviewCommentAsync(
|
func (s *CommentService) reviewCommentAsync(
|
||||||
commentID, userID, postID, content string,
|
commentID, userID, postID, content string,
|
||||||
imageURLs []string,
|
imageURLs []string,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"with_you/internal/cache"
|
"with_you/internal/cache"
|
||||||
|
"with_you/internal/dto"
|
||||||
"with_you/internal/model"
|
"with_you/internal/model"
|
||||||
"with_you/internal/pkg/cursor"
|
"with_you/internal/pkg/cursor"
|
||||||
"with_you/internal/pkg/hook"
|
"with_you/internal/pkg/hook"
|
||||||
@@ -25,7 +26,7 @@ const (
|
|||||||
// PostService 帖子服务接口
|
// PostService 帖子服务接口
|
||||||
type PostService interface {
|
type PostService interface {
|
||||||
// 帖子CRUD
|
// 帖子CRUD
|
||||||
Create(ctx context.Context, userID, title, content string, images []string, channelID *string) (*model.Post, error)
|
Create(ctx context.Context, userID, title, content string, segments model.MessageSegments, images []string, channelID *string) (*model.Post, error)
|
||||||
GetByID(ctx context.Context, id string) (*model.Post, error)
|
GetByID(ctx context.Context, id string) (*model.Post, error)
|
||||||
Update(ctx context.Context, post *model.Post) error
|
Update(ctx context.Context, post *model.Post) error
|
||||||
UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error
|
UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error
|
||||||
@@ -98,13 +99,27 @@ type PostListResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create 创建帖子
|
// Create 创建帖子
|
||||||
func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, images []string, channelID *string) (*model.Post, error) {
|
func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, segments model.MessageSegments, images []string, channelID *string) (*model.Post, error) {
|
||||||
|
// 如果没有提供 content 但有 segments,从 segments 提取文本摘要
|
||||||
|
if content == "" && len(segments) > 0 {
|
||||||
|
content = dto.ExtractTextContent(segments)
|
||||||
|
}
|
||||||
|
// 如果有 segments 但 content 为空,将 content 设为空字符串(NOT NULL 约束)
|
||||||
|
if content == "" {
|
||||||
|
content = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 segments 中是否包含投票
|
||||||
|
isVote := dto.HasVoteSegment(segments)
|
||||||
|
|
||||||
post := &model.Post{
|
post := &model.Post{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ChannelID: channelID,
|
ChannelID: channelID,
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
Segments: segments,
|
||||||
Status: model.PostStatusPending,
|
Status: model.PostStatusPending,
|
||||||
|
IsVote: isVote,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.postRepo.Create(post, images)
|
err := s.postRepo.Create(post, images)
|
||||||
@@ -127,6 +142,12 @@ func (s *postServiceImpl) Create(ctx context.Context, userID, title, content str
|
|||||||
// 失效帖子列表缓存
|
// 失效帖子列表缓存
|
||||||
cache.InvalidatePostList(s.cache)
|
cache.InvalidatePostList(s.cache)
|
||||||
|
|
||||||
|
// 异步处理 @提及通知
|
||||||
|
if len(segments) > 0 {
|
||||||
|
bgCtx := context.WithoutCancel(ctx)
|
||||||
|
go s.processMentionNotifications(bgCtx, userID, post.ID, post.Title, segments)
|
||||||
|
}
|
||||||
|
|
||||||
// 异步执行审核流
|
// 异步执行审核流
|
||||||
go s.reviewPostAsync(post.ID, userID, title, content, images)
|
go s.reviewPostAsync(post.ID, userID, title, content, images)
|
||||||
|
|
||||||
@@ -246,6 +267,27 @@ func (s *postServiceImpl) notifyModerationRejected(userID, reason string) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processMentionNotifications 处理帖子中 @提及的通知
|
||||||
|
func (s *postServiceImpl) processMentionNotifications(ctx context.Context, authorID, postID, postTitle string, segments model.MessageSegments) {
|
||||||
|
if s.systemMessageService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mentionedUserIDs := dto.ExtractMentionedUsers(segments)
|
||||||
|
if len(mentionedUserIDs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetUserID := range mentionedUserIDs {
|
||||||
|
if targetUserID == authorID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.systemMessageService.SendMentionNotification(ctx, targetUserID, authorID, postID); err != nil {
|
||||||
|
log.Printf("[WARN] Failed to send mention notification to user %s: %v", targetUserID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetByID 根据ID获取帖子
|
// GetByID 根据ID获取帖子
|
||||||
func (s *postServiceImpl) GetByID(ctx context.Context, id string) (*model.Post, error) {
|
func (s *postServiceImpl) GetByID(ctx context.Context, id string) (*model.Post, error) {
|
||||||
return s.postRepo.GetByID(id)
|
return s.postRepo.GetByID(id)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -48,7 +48,6 @@ func NewVoteService(
|
|||||||
|
|
||||||
// CreateVotePost 创建投票帖子
|
// CreateVotePost 创建投票帖子
|
||||||
func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error) {
|
func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dto.CreateVotePostRequest) (*dto.PostResponse, error) {
|
||||||
// 验证投票选项数量
|
|
||||||
if len(req.VoteOptions) < 2 {
|
if len(req.VoteOptions) < 2 {
|
||||||
return nil, errors.New("投票选项至少需要2个")
|
return nil, errors.New("投票选项至少需要2个")
|
||||||
}
|
}
|
||||||
@@ -56,12 +55,37 @@ func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dt
|
|||||||
return nil, errors.New("投票选项最多10个")
|
return nil, errors.New("投票选项最多10个")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建普通帖子(设置IsVote=true)
|
// 构建 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 = " "
|
||||||
|
}
|
||||||
|
|
||||||
post := &model.Post{
|
post := &model.Post{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ChannelID: req.ChannelID,
|
ChannelID: req.ChannelID,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Content: req.Content,
|
Content: content,
|
||||||
|
Segments: segments,
|
||||||
Status: model.PostStatusPending,
|
Status: model.PostStatusPending,
|
||||||
IsVote: true,
|
IsVote: true,
|
||||||
}
|
}
|
||||||
@@ -71,14 +95,8 @@ func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dt
|
|||||||
return nil, err
|
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)
|
go s.reviewVotePostAsync(post.ID, userID, req.Title, content, req.Images)
|
||||||
|
|
||||||
// 重新查询以获取关联的User和Images
|
// 重新查询以获取关联的User和Images
|
||||||
createdPost, err := s.postRepo.GetByID(post.ID)
|
createdPost, err := s.postRepo.GetByID(post.ID)
|
||||||
@@ -86,7 +104,6 @@ func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dt
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为响应DTO
|
|
||||||
return s.convertToPostResponse(createdPost, userID), nil
|
return s.convertToPostResponse(createdPost, userID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +213,27 @@ func (s *VoteService) notifyModerationRejected(userID, reason string) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVoteOptions 获取投票选项
|
// resolveVoteOptions 解析投票选项(优先从 segments 读取,兼容旧数据从 vote_options 表读取)
|
||||||
func (s *VoteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error) {
|
func (s *VoteService) resolveVoteOptions(postID string, segments model.MessageSegments) ([]dto.VoteOptionDTO, error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
options, err := s.voteRepo.GetOptionsByPostID(postID)
|
options, err := s.voteRepo.GetOptionsByPostID(postID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -215,23 +251,35 @@ func (s *VoteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVoteResult 获取投票结果(包含用户投票状态)
|
// GetVoteOptions 获取投票选项(优先从 segments 读取,兼容旧数据从 vote_options 表读取)
|
||||||
func (s *VoteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error) {
|
func (s *VoteService) GetVoteOptions(postID string) ([]dto.VoteOptionDTO, error) {
|
||||||
// 获取所有投票选项
|
post, err := s.postRepo.GetByID(postID)
|
||||||
options, err := s.voteRepo.GetOptionsByPostID(postID)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.resolveVoteOptions(postID, post.Segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVoteResult 获取投票结果(包含用户投票状态)
|
||||||
|
func (s *VoteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO, error) {
|
||||||
|
post, err := s.postRepo.GetByID(postID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
optionDTOs, err := s.resolveVoteOptions(postID, post.Segments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的投票记录
|
|
||||||
userVote, err := s.voteRepo.GetUserVote(postID, userID)
|
userVote, err := s.voteRepo.GetUserVote(postID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建结果
|
|
||||||
result := &dto.VoteResultDTO{
|
result := &dto.VoteResultDTO{
|
||||||
Options: make([]dto.VoteOptionDTO, 0, len(options)),
|
Options: optionDTOs,
|
||||||
TotalVotes: 0,
|
TotalVotes: 0,
|
||||||
HasVoted: userVote != nil,
|
HasVoted: userVote != nil,
|
||||||
}
|
}
|
||||||
@@ -240,13 +288,8 @@ func (s *VoteService) GetVoteResult(postID, userID string) (*dto.VoteResultDTO,
|
|||||||
result.VotedOptionID = userVote.OptionID
|
result.VotedOptionID = userVote.OptionID
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, opt := range optionDTOs {
|
||||||
result.Options = append(result.Options, dto.VoteOptionDTO{
|
result.TotalVotes += opt.VotesCount
|
||||||
ID: option.ID,
|
|
||||||
Content: option.Content,
|
|
||||||
VotesCount: option.VotesCount,
|
|
||||||
})
|
|
||||||
result.TotalVotes += option.VotesCount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -308,6 +351,7 @@ func (s *VoteService) convertToPostResponse(post *model.Post, currentUserID stri
|
|||||||
UserID: post.UserID,
|
UserID: post.UserID,
|
||||||
Title: post.Title,
|
Title: post.Title,
|
||||||
Content: post.Content,
|
Content: post.Content,
|
||||||
|
Segments: dto.SegmentsOrDefault(post.Segments, post.Content),
|
||||||
LikesCount: post.LikesCount,
|
LikesCount: post.LikesCount,
|
||||||
CommentsCount: post.CommentsCount,
|
CommentsCount: post.CommentsCount,
|
||||||
FavoritesCount: post.FavoritesCount,
|
FavoritesCount: post.FavoritesCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user