Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
This commit is contained in:
885
internal/dto/converter.go
Normal file
885
internal/dto/converter.go
Normal file
@@ -0,0 +1,885 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ==================== User 转换 ====================
|
||||
|
||||
// getAvatarOrDefault 获取头像URL,如果为空则返回在线头像生成服务的URL
|
||||
func getAvatarOrDefault(user *model.User) string {
|
||||
return utils.GetAvatarOrDefault(user.Username, user.Nickname, user.Avatar)
|
||||
}
|
||||
|
||||
// ConvertUserToResponse 将User转换为UserResponse
|
||||
func ConvertUserToResponse(user *model.User) *UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Avatar: getAvatarOrDefault(user),
|
||||
CoverURL: user.CoverURL,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
PostsCount: user.PostsCount,
|
||||
FollowersCount: user.FollowersCount,
|
||||
FollowingCount: user.FollowingCount,
|
||||
CreatedAt: FormatTime(user.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUserToResponseWithFollowing 将User转换为UserResponse(包含关注状态)
|
||||
func ConvertUserToResponseWithFollowing(user *model.User, isFollowing bool) *UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Avatar: getAvatarOrDefault(user),
|
||||
CoverURL: user.CoverURL,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
PostsCount: user.PostsCount,
|
||||
FollowersCount: user.FollowersCount,
|
||||
FollowingCount: user.FollowingCount,
|
||||
IsFollowing: isFollowing,
|
||||
IsFollowingMe: false, // 默认false,需要单独计算
|
||||
CreatedAt: FormatTime(user.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUserToResponseWithPostsCount 将User转换为UserResponse(使用实时计算的帖子数量)
|
||||
func ConvertUserToResponseWithPostsCount(user *model.User, postsCount int) *UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Avatar: getAvatarOrDefault(user),
|
||||
CoverURL: user.CoverURL,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
PostsCount: postsCount,
|
||||
FollowersCount: user.FollowersCount,
|
||||
FollowingCount: user.FollowingCount,
|
||||
CreatedAt: FormatTime(user.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUserToResponseWithMutualFollow 将User转换为UserResponse(包含双向关注状态)
|
||||
func ConvertUserToResponseWithMutualFollow(user *model.User, isFollowing, isFollowingMe bool) *UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Avatar: getAvatarOrDefault(user),
|
||||
CoverURL: user.CoverURL,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
PostsCount: user.PostsCount,
|
||||
FollowersCount: user.FollowersCount,
|
||||
FollowingCount: user.FollowingCount,
|
||||
IsFollowing: isFollowing,
|
||||
IsFollowingMe: isFollowingMe,
|
||||
CreatedAt: FormatTime(user.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUserToResponseWithMutualFollowAndPostsCount 将User转换为UserResponse(包含双向关注状态和实时计算的帖子数量)
|
||||
func ConvertUserToResponseWithMutualFollowAndPostsCount(user *model.User, isFollowing, isFollowingMe bool, postsCount int) *UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Avatar: getAvatarOrDefault(user),
|
||||
CoverURL: user.CoverURL,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
PostsCount: postsCount,
|
||||
FollowersCount: user.FollowersCount,
|
||||
FollowingCount: user.FollowingCount,
|
||||
IsFollowing: isFollowing,
|
||||
IsFollowingMe: isFollowingMe,
|
||||
CreatedAt: FormatTime(user.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUserToDetailResponse 将User转换为UserDetailResponse
|
||||
func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserDetailResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Email: user.Email,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Avatar: getAvatarOrDefault(user),
|
||||
CoverURL: user.CoverURL,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
PostsCount: user.PostsCount,
|
||||
FollowersCount: user.FollowersCount,
|
||||
FollowingCount: user.FollowingCount,
|
||||
IsVerified: user.IsVerified,
|
||||
CreatedAt: FormatTime(user.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUserToDetailResponseWithPostsCount 将User转换为UserDetailResponse(使用实时计算的帖子数量)
|
||||
func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int) *UserDetailResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserDetailResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Email: user.Email,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Phone: user.Phone, // 仅当前用户自己可见
|
||||
Avatar: getAvatarOrDefault(user),
|
||||
CoverURL: user.CoverURL,
|
||||
Bio: user.Bio,
|
||||
Website: user.Website,
|
||||
Location: user.Location,
|
||||
PostsCount: postsCount,
|
||||
FollowersCount: user.FollowersCount,
|
||||
FollowingCount: user.FollowingCount,
|
||||
IsVerified: user.IsVerified,
|
||||
CreatedAt: FormatTime(user.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertUsersToResponse 将User列表转换为响应列表
|
||||
func ConvertUsersToResponse(users []*model.User) []*UserResponse {
|
||||
result := make([]*UserResponse, 0, len(users))
|
||||
for _, user := range users {
|
||||
result = append(result, ConvertUserToResponse(user))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ConvertUsersToResponseWithMutualFollow 将User列表转换为响应列表(包含双向关注状态)
|
||||
// followingStatusMap: key是用户ID,value是[isFollowing, isFollowingMe]
|
||||
func ConvertUsersToResponseWithMutualFollow(users []*model.User, followingStatusMap map[string][2]bool) []*UserResponse {
|
||||
result := make([]*UserResponse, 0, len(users))
|
||||
for _, user := range users {
|
||||
status, ok := followingStatusMap[user.ID]
|
||||
if ok {
|
||||
result = append(result, ConvertUserToResponseWithMutualFollow(user, status[0], status[1]))
|
||||
} else {
|
||||
result = append(result, ConvertUserToResponse(user))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ConvertUsersToResponseWithMutualFollowAndPostsCount 将User列表转换为响应列表(包含双向关注状态和实时计算的帖子数量)
|
||||
// followingStatusMap: key是用户ID,value是[isFollowing, isFollowingMe]
|
||||
// postsCountMap: key是用户ID,value是帖子数量
|
||||
func ConvertUsersToResponseWithMutualFollowAndPostsCount(users []*model.User, followingStatusMap map[string][2]bool, postsCountMap map[string]int64) []*UserResponse {
|
||||
result := make([]*UserResponse, 0, len(users))
|
||||
for _, user := range users {
|
||||
status, hasStatus := followingStatusMap[user.ID]
|
||||
postsCount, hasPostsCount := postsCountMap[user.ID]
|
||||
|
||||
// 如果没有帖子数量,使用数据库中的值
|
||||
if !hasPostsCount {
|
||||
postsCount = int64(user.PostsCount)
|
||||
}
|
||||
|
||||
if hasStatus {
|
||||
result = append(result, ConvertUserToResponseWithMutualFollowAndPostsCount(user, status[0], status[1], int(postsCount)))
|
||||
} else {
|
||||
result = append(result, ConvertUserToResponseWithPostsCount(user, int(postsCount)))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== Post 转换 ====================
|
||||
|
||||
// ConvertPostImageToResponse 将PostImage转换为PostImageResponse
|
||||
func ConvertPostImageToResponse(img *model.PostImage) PostImageResponse {
|
||||
if img == nil {
|
||||
return PostImageResponse{}
|
||||
}
|
||||
return PostImageResponse{
|
||||
ID: img.ID,
|
||||
URL: img.URL,
|
||||
ThumbnailURL: img.ThumbnailURL,
|
||||
Width: img.Width,
|
||||
Height: img.Height,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertPostImagesToResponse 将PostImage列表转换为响应列表
|
||||
func ConvertPostImagesToResponse(images []model.PostImage) []PostImageResponse {
|
||||
result := make([]PostImageResponse, 0, len(images))
|
||||
for i := range images {
|
||||
result = append(result, ConvertPostImageToResponse(&images[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ConvertPostToResponse 将Post转换为PostResponse(列表用)
|
||||
func ConvertPostToResponse(post *model.Post, isLiked, isFavorited bool) *PostResponse {
|
||||
if post == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
images := make([]PostImageResponse, 0)
|
||||
for _, img := range post.Images {
|
||||
images = append(images, ConvertPostImageToResponse(&img))
|
||||
}
|
||||
|
||||
var author *UserResponse
|
||||
if post.User != nil {
|
||||
author = ConvertUserToResponse(post.User)
|
||||
}
|
||||
|
||||
return &PostResponse{
|
||||
ID: post.ID,
|
||||
UserID: post.UserID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Images: images,
|
||||
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: FormatTime(post.CreatedAt),
|
||||
Author: author,
|
||||
IsLiked: isLiked,
|
||||
IsFavorited: isFavorited,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertPostToDetailResponse 将Post转换为PostDetailResponse
|
||||
func ConvertPostToDetailResponse(post *model.Post, isLiked, isFavorited bool) *PostDetailResponse {
|
||||
if post == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
images := make([]PostImageResponse, 0)
|
||||
for _, img := range post.Images {
|
||||
images = append(images, ConvertPostImageToResponse(&img))
|
||||
}
|
||||
|
||||
var author *UserResponse
|
||||
if post.User != nil {
|
||||
author = ConvertUserToResponse(post.User)
|
||||
}
|
||||
|
||||
return &PostDetailResponse{
|
||||
ID: post.ID,
|
||||
UserID: post.UserID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Images: images,
|
||||
Status: string(post.Status),
|
||||
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: FormatTime(post.CreatedAt),
|
||||
UpdatedAt: FormatTime(post.UpdatedAt),
|
||||
Author: author,
|
||||
IsLiked: isLiked,
|
||||
IsFavorited: isFavorited,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertPostsToResponse 将Post列表转换为响应列表(每个帖子独立检查点赞/收藏状态)
|
||||
func ConvertPostsToResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse {
|
||||
result := make([]*PostResponse, 0, len(posts))
|
||||
for _, post := range posts {
|
||||
isLiked := false
|
||||
isFavorited := false
|
||||
if isLikedMap != nil {
|
||||
isLiked = isLikedMap[post.ID]
|
||||
}
|
||||
if isFavoritedMap != nil {
|
||||
isFavorited = isFavoritedMap[post.ID]
|
||||
}
|
||||
result = append(result, ConvertPostToResponse(post, isLiked, isFavorited))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== Comment 转换 ====================
|
||||
|
||||
// ConvertCommentToResponse 将Comment转换为CommentResponse
|
||||
func ConvertCommentToResponse(comment *model.Comment, isLiked bool) *CommentResponse {
|
||||
if comment == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var author *UserResponse
|
||||
if comment.User != nil {
|
||||
author = ConvertUserToResponse(comment.User)
|
||||
}
|
||||
|
||||
// 转换子回复(扁平化结构)
|
||||
var replies []*CommentResponse
|
||||
if len(comment.Replies) > 0 {
|
||||
replies = make([]*CommentResponse, 0, len(comment.Replies))
|
||||
for _, reply := range comment.Replies {
|
||||
replies = append(replies, ConvertCommentToResponse(reply, false))
|
||||
}
|
||||
}
|
||||
|
||||
// TargetID 就是 ParentID,前端根据这个 ID 找到被回复用户的昵称
|
||||
var targetID *string
|
||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||
targetID = comment.ParentID
|
||||
}
|
||||
|
||||
// 解析图片JSON
|
||||
var images []CommentImageResponse
|
||||
if comment.Images != "" {
|
||||
var urlList []string
|
||||
if err := json.Unmarshal([]byte(comment.Images), &urlList); err == nil {
|
||||
images = make([]CommentImageResponse, 0, len(urlList))
|
||||
for _, url := range urlList {
|
||||
images = append(images, CommentImageResponse{URL: url})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &CommentResponse{
|
||||
ID: comment.ID,
|
||||
PostID: comment.PostID,
|
||||
UserID: comment.UserID,
|
||||
ParentID: comment.ParentID,
|
||||
RootID: comment.RootID,
|
||||
Content: comment.Content,
|
||||
Images: images,
|
||||
LikesCount: comment.LikesCount,
|
||||
RepliesCount: comment.RepliesCount,
|
||||
CreatedAt: FormatTime(comment.CreatedAt),
|
||||
Author: author,
|
||||
IsLiked: isLiked,
|
||||
TargetID: targetID,
|
||||
Replies: replies,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertCommentsToResponse 将Comment列表转换为响应列表
|
||||
func ConvertCommentsToResponse(comments []*model.Comment, isLiked bool) []*CommentResponse {
|
||||
result := make([]*CommentResponse, 0, len(comments))
|
||||
for _, comment := range comments {
|
||||
result = append(result, ConvertCommentToResponse(comment, isLiked))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsLikedChecker 点赞状态检查器接口
|
||||
type IsLikedChecker interface {
|
||||
IsLiked(ctx context.Context, commentID, userID string) bool
|
||||
}
|
||||
|
||||
// ConvertCommentToResponseWithUser 将Comment转换为CommentResponse(根据用户ID检查点赞状态)
|
||||
func ConvertCommentToResponseWithUser(comment *model.Comment, userID string, checker IsLikedChecker) *CommentResponse {
|
||||
if comment == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查当前用户是否点赞了该评论
|
||||
isLiked := false
|
||||
if userID != "" && checker != nil {
|
||||
isLiked = checker.IsLiked(context.Background(), comment.ID, userID)
|
||||
}
|
||||
|
||||
var author *UserResponse
|
||||
if comment.User != nil {
|
||||
author = ConvertUserToResponse(comment.User)
|
||||
}
|
||||
|
||||
// 转换子回复(扁平化结构),递归检查点赞状态
|
||||
var replies []*CommentResponse
|
||||
if len(comment.Replies) > 0 {
|
||||
replies = make([]*CommentResponse, 0, len(comment.Replies))
|
||||
for _, reply := range comment.Replies {
|
||||
replies = append(replies, ConvertCommentToResponseWithUser(reply, userID, checker))
|
||||
}
|
||||
}
|
||||
|
||||
// TargetID 就是 ParentID,前端根据这个 ID 找到被回复用户的昵称
|
||||
var targetID *string
|
||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||
targetID = comment.ParentID
|
||||
}
|
||||
|
||||
// 解析图片JSON
|
||||
var images []CommentImageResponse
|
||||
if comment.Images != "" {
|
||||
var urlList []string
|
||||
if err := json.Unmarshal([]byte(comment.Images), &urlList); err == nil {
|
||||
images = make([]CommentImageResponse, 0, len(urlList))
|
||||
for _, url := range urlList {
|
||||
images = append(images, CommentImageResponse{URL: url})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &CommentResponse{
|
||||
ID: comment.ID,
|
||||
PostID: comment.PostID,
|
||||
UserID: comment.UserID,
|
||||
ParentID: comment.ParentID,
|
||||
RootID: comment.RootID,
|
||||
Content: comment.Content,
|
||||
Images: images,
|
||||
LikesCount: comment.LikesCount,
|
||||
RepliesCount: comment.RepliesCount,
|
||||
CreatedAt: FormatTime(comment.CreatedAt),
|
||||
Author: author,
|
||||
IsLiked: isLiked,
|
||||
TargetID: targetID,
|
||||
Replies: replies,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertCommentsToResponseWithUser 将Comment列表转换为响应列表(根据用户ID检查点赞状态)
|
||||
func ConvertCommentsToResponseWithUser(comments []*model.Comment, userID string, checker IsLikedChecker) []*CommentResponse {
|
||||
result := make([]*CommentResponse, 0, len(comments))
|
||||
for _, comment := range comments {
|
||||
result = append(result, ConvertCommentToResponseWithUser(comment, userID, checker))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== Notification 转换 ====================
|
||||
|
||||
// ConvertNotificationToResponse 将Notification转换为NotificationResponse
|
||||
func ConvertNotificationToResponse(notification *model.Notification) *NotificationResponse {
|
||||
if notification == nil {
|
||||
return nil
|
||||
}
|
||||
return &NotificationResponse{
|
||||
ID: notification.ID,
|
||||
UserID: notification.UserID,
|
||||
Type: string(notification.Type),
|
||||
Title: notification.Title,
|
||||
Content: notification.Content,
|
||||
Data: notification.Data,
|
||||
IsRead: notification.IsRead,
|
||||
CreatedAt: FormatTime(notification.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertNotificationsToResponse 将Notification列表转换为响应列表
|
||||
func ConvertNotificationsToResponse(notifications []*model.Notification) []*NotificationResponse {
|
||||
result := make([]*NotificationResponse, 0, len(notifications))
|
||||
for _, n := range notifications {
|
||||
result = append(result, ConvertNotificationToResponse(n))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== Message 转换 ====================
|
||||
|
||||
// ConvertMessageToResponse 将Message转换为MessageResponse
|
||||
func ConvertMessageToResponse(message *model.Message) *MessageResponse {
|
||||
if message == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 直接使用 segments,不需要解析
|
||||
segments := make(model.MessageSegments, len(message.Segments))
|
||||
for i, seg := range message.Segments {
|
||||
segments[i] = model.MessageSegment{
|
||||
Type: seg.Type,
|
||||
Data: seg.Data,
|
||||
}
|
||||
}
|
||||
|
||||
return &MessageResponse{
|
||||
ID: message.ID,
|
||||
ConversationID: message.ConversationID,
|
||||
SenderID: message.SenderID,
|
||||
Seq: message.Seq,
|
||||
Segments: segments,
|
||||
ReplyToID: message.ReplyToID,
|
||||
Status: string(message.Status),
|
||||
Category: string(message.Category),
|
||||
CreatedAt: FormatTime(message.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertConversationToResponse 将Conversation转换为ConversationResponse
|
||||
// participants: 会话参与者列表(用户信息)
|
||||
// unreadCount: 当前用户的未读消息数
|
||||
// lastMessage: 最后一条消息
|
||||
func ConvertConversationToResponse(conv *model.Conversation, participants []*model.User, unreadCount int, lastMessage *model.Message, isPinned bool) *ConversationResponse {
|
||||
if conv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var participantResponses []*UserResponse
|
||||
for _, p := range participants {
|
||||
participantResponses = append(participantResponses, ConvertUserToResponse(p))
|
||||
}
|
||||
|
||||
// 转换群组信息
|
||||
var groupResponse *GroupResponse
|
||||
if conv.Group != nil {
|
||||
groupResponse = GroupToResponse(conv.Group)
|
||||
}
|
||||
|
||||
return &ConversationResponse{
|
||||
ID: conv.ID,
|
||||
Type: string(conv.Type),
|
||||
IsPinned: isPinned,
|
||||
Group: groupResponse,
|
||||
LastSeq: conv.LastSeq,
|
||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||
UnreadCount: unreadCount,
|
||||
Participants: participantResponses,
|
||||
CreatedAt: FormatTime(conv.CreatedAt),
|
||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertConversationToDetailResponse 将Conversation转换为ConversationDetailResponse
|
||||
func ConvertConversationToDetailResponse(conv *model.Conversation, participants []*model.User, unreadCount int64, lastMessage *model.Message, myLastReadSeq int64, otherLastReadSeq int64, isPinned bool) *ConversationDetailResponse {
|
||||
if conv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var participantResponses []*UserResponse
|
||||
for _, p := range participants {
|
||||
participantResponses = append(participantResponses, ConvertUserToResponse(p))
|
||||
}
|
||||
|
||||
return &ConversationDetailResponse{
|
||||
ID: conv.ID,
|
||||
Type: string(conv.Type),
|
||||
IsPinned: isPinned,
|
||||
LastSeq: conv.LastSeq,
|
||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||
UnreadCount: unreadCount,
|
||||
Participants: participantResponses,
|
||||
MyLastReadSeq: myLastReadSeq,
|
||||
OtherLastReadSeq: otherLastReadSeq,
|
||||
CreatedAt: FormatTime(conv.CreatedAt),
|
||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertMessagesToResponse 将Message列表转换为响应列表
|
||||
func ConvertMessagesToResponse(messages []*model.Message) []*MessageResponse {
|
||||
result := make([]*MessageResponse, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
result = append(result, ConvertMessageToResponse(msg))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ConvertConversationsToResponse 将Conversation列表转换为响应列表
|
||||
func ConvertConversationsToResponse(convs []*model.Conversation) []*ConversationResponse {
|
||||
result := make([]*ConversationResponse, 0, len(convs))
|
||||
for _, conv := range convs {
|
||||
result = append(result, ConvertConversationToResponse(conv, nil, 0, nil, false))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== PushRecord 转换 ====================
|
||||
|
||||
// PushRecordToResponse 将PushRecord转换为PushRecordResponse
|
||||
func PushRecordToResponse(record *model.PushRecord) *PushRecordResponse {
|
||||
if record == nil {
|
||||
return nil
|
||||
}
|
||||
resp := &PushRecordResponse{
|
||||
ID: record.ID,
|
||||
MessageID: record.MessageID,
|
||||
PushChannel: string(record.PushChannel),
|
||||
PushStatus: string(record.PushStatus),
|
||||
RetryCount: record.RetryCount,
|
||||
CreatedAt: record.CreatedAt,
|
||||
}
|
||||
if record.PushedAt != nil {
|
||||
resp.PushedAt = *record.PushedAt
|
||||
}
|
||||
if record.DeliveredAt != nil {
|
||||
resp.DeliveredAt = *record.DeliveredAt
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// PushRecordsToResponse 将PushRecord列表转换为响应列表
|
||||
func PushRecordsToResponse(records []*model.PushRecord) []*PushRecordResponse {
|
||||
result := make([]*PushRecordResponse, 0, len(records))
|
||||
for _, record := range records {
|
||||
result = append(result, PushRecordToResponse(record))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== DeviceToken 转换 ====================
|
||||
|
||||
// DeviceTokenToResponse 将DeviceToken转换为DeviceTokenResponse
|
||||
func DeviceTokenToResponse(token *model.DeviceToken) *DeviceTokenResponse {
|
||||
if token == nil {
|
||||
return nil
|
||||
}
|
||||
resp := &DeviceTokenResponse{
|
||||
ID: token.ID,
|
||||
DeviceID: token.DeviceID,
|
||||
DeviceType: string(token.DeviceType),
|
||||
IsActive: token.IsActive,
|
||||
DeviceName: token.DeviceName,
|
||||
CreatedAt: token.CreatedAt,
|
||||
}
|
||||
if token.LastUsedAt != nil {
|
||||
resp.LastUsedAt = *token.LastUsedAt
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// DeviceTokensToResponse 将DeviceToken列表转换为响应列表
|
||||
func DeviceTokensToResponse(tokens []*model.DeviceToken) []*DeviceTokenResponse {
|
||||
result := make([]*DeviceTokenResponse, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
result = append(result, DeviceTokenToResponse(token))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== SystemMessage 转换 ====================
|
||||
|
||||
// SystemMessageToResponse 将Message转换为SystemMessageResponse
|
||||
func SystemMessageToResponse(msg *model.Message) *SystemMessageResponse {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 从 segments 中提取文本内容
|
||||
content := ExtractTextContentFromModel(msg.Segments)
|
||||
|
||||
resp := &SystemMessageResponse{
|
||||
ID: msg.ID,
|
||||
SenderID: msg.SenderID,
|
||||
ReceiverID: "", // 系统消息的接收者需要从上下文获取
|
||||
Content: content,
|
||||
Category: string(msg.Category),
|
||||
SystemType: string(msg.SystemType),
|
||||
CreatedAt: msg.CreatedAt,
|
||||
}
|
||||
if msg.ExtraData != nil {
|
||||
resp.ExtraData = map[string]interface{}{
|
||||
"actor_id": msg.ExtraData.ActorID,
|
||||
"actor_name": msg.ExtraData.ActorName,
|
||||
"avatar_url": msg.ExtraData.AvatarURL,
|
||||
"target_id": msg.ExtraData.TargetID,
|
||||
"target_type": msg.ExtraData.TargetType,
|
||||
"action_url": msg.ExtraData.ActionURL,
|
||||
"action_time": msg.ExtraData.ActionTime,
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// SystemMessagesToResponse 将Message列表转换为SystemMessageResponse列表
|
||||
func SystemMessagesToResponse(messages []*model.Message) []*SystemMessageResponse {
|
||||
result := make([]*SystemMessageResponse, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
result = append(result, SystemMessageToResponse(msg))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SystemNotificationToResponse 将SystemNotification转换为SystemMessageResponse
|
||||
func SystemNotificationToResponse(n *model.SystemNotification) *SystemMessageResponse {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
resp := &SystemMessageResponse{
|
||||
ID: strconv.FormatInt(n.ID, 10),
|
||||
SenderID: model.SystemSenderIDStr, // 系统发送者
|
||||
ReceiverID: n.ReceiverID,
|
||||
Content: n.Content,
|
||||
Category: "notification",
|
||||
SystemType: string(n.Type),
|
||||
IsRead: n.IsRead,
|
||||
CreatedAt: n.CreatedAt,
|
||||
}
|
||||
if n.ExtraData != nil {
|
||||
resp.ExtraData = map[string]interface{}{
|
||||
"actor_id": n.ExtraData.ActorID,
|
||||
"actor_id_str": n.ExtraData.ActorIDStr,
|
||||
"actor_name": n.ExtraData.ActorName,
|
||||
"avatar_url": n.ExtraData.AvatarURL,
|
||||
"target_id": n.ExtraData.TargetID,
|
||||
"target_title": n.ExtraData.TargetTitle,
|
||||
"target_type": n.ExtraData.TargetType,
|
||||
"action_url": n.ExtraData.ActionURL,
|
||||
"action_time": n.ExtraData.ActionTime,
|
||||
"group_id": n.ExtraData.GroupID,
|
||||
"group_name": n.ExtraData.GroupName,
|
||||
"group_avatar": n.ExtraData.GroupAvatar,
|
||||
"group_description": n.ExtraData.GroupDescription,
|
||||
"flag": n.ExtraData.Flag,
|
||||
"request_type": n.ExtraData.RequestType,
|
||||
"request_status": n.ExtraData.RequestStatus,
|
||||
"reason": n.ExtraData.Reason,
|
||||
"target_user_id": n.ExtraData.TargetUserID,
|
||||
"target_user_name": n.ExtraData.TargetUserName,
|
||||
"target_user_avatar": n.ExtraData.TargetUserAvatar,
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// SystemNotificationsToResponse 将SystemNotification列表转换为SystemMessageResponse列表
|
||||
func SystemNotificationsToResponse(notifications []*model.SystemNotification) []*SystemMessageResponse {
|
||||
result := make([]*SystemMessageResponse, 0, len(notifications))
|
||||
for _, n := range notifications {
|
||||
result = append(result, SystemNotificationToResponse(n))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== Group 转换 ====================
|
||||
|
||||
// GroupToResponse 将Group转换为GroupResponse
|
||||
func GroupToResponse(group *model.Group) *GroupResponse {
|
||||
if group == nil {
|
||||
return nil
|
||||
}
|
||||
return &GroupResponse{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
Avatar: group.Avatar,
|
||||
Description: group.Description,
|
||||
OwnerID: group.OwnerID,
|
||||
MemberCount: group.MemberCount,
|
||||
MaxMembers: group.MaxMembers,
|
||||
JoinType: int(group.JoinType),
|
||||
MuteAll: group.MuteAll,
|
||||
CreatedAt: FormatTime(group.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// GroupsToResponse 将Group列表转换为GroupResponse列表
|
||||
func GroupsToResponse(groups []model.Group) []*GroupResponse {
|
||||
result := make([]*GroupResponse, 0, len(groups))
|
||||
for i := range groups {
|
||||
result = append(result, GroupToResponse(&groups[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupMemberToResponse 将GroupMember转换为GroupMemberResponse
|
||||
func GroupMemberToResponse(member *model.GroupMember) *GroupMemberResponse {
|
||||
if member == nil {
|
||||
return nil
|
||||
}
|
||||
return &GroupMemberResponse{
|
||||
ID: member.ID,
|
||||
GroupID: member.GroupID,
|
||||
UserID: member.UserID,
|
||||
Role: member.Role,
|
||||
Nickname: member.Nickname,
|
||||
Muted: member.Muted,
|
||||
JoinTime: FormatTime(member.JoinTime),
|
||||
}
|
||||
}
|
||||
|
||||
// GroupMemberToResponseWithUser 将GroupMember转换为GroupMemberResponse(包含用户信息)
|
||||
func GroupMemberToResponseWithUser(member *model.GroupMember, user *model.User) *GroupMemberResponse {
|
||||
if member == nil {
|
||||
return nil
|
||||
}
|
||||
resp := GroupMemberToResponse(member)
|
||||
if user != nil {
|
||||
resp.User = ConvertUserToResponse(user)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// GroupMembersToResponse 将GroupMember列表转换为GroupMemberResponse列表
|
||||
func GroupMembersToResponse(members []model.GroupMember) []*GroupMemberResponse {
|
||||
result := make([]*GroupMemberResponse, 0, len(members))
|
||||
for i := range members {
|
||||
result = append(result, GroupMemberToResponse(&members[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupAnnouncementToResponse 将GroupAnnouncement转换为GroupAnnouncementResponse
|
||||
func GroupAnnouncementToResponse(announcement *model.GroupAnnouncement) *GroupAnnouncementResponse {
|
||||
if announcement == nil {
|
||||
return nil
|
||||
}
|
||||
return &GroupAnnouncementResponse{
|
||||
ID: announcement.ID,
|
||||
GroupID: announcement.GroupID,
|
||||
Content: announcement.Content,
|
||||
AuthorID: announcement.AuthorID,
|
||||
IsPinned: announcement.IsPinned,
|
||||
CreatedAt: FormatTime(announcement.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// GroupAnnouncementsToResponse 将GroupAnnouncement列表转换为GroupAnnouncementResponse列表
|
||||
func GroupAnnouncementsToResponse(announcements []model.GroupAnnouncement) []*GroupAnnouncementResponse {
|
||||
result := make([]*GroupAnnouncementResponse, 0, len(announcements))
|
||||
for i := range announcements {
|
||||
result = append(result, GroupAnnouncementToResponse(&announcements[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
819
internal/dto/dto.go
Normal file
819
internal/dto/dto.go
Normal file
@@ -0,0 +1,819 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ==================== User DTOs ====================
|
||||
|
||||
// UserResponse 用户信息响应
|
||||
type UserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Avatar string `json:"avatar"`
|
||||
CoverURL string `json:"cover_url"` // 头图URL
|
||||
Bio string `json:"bio"`
|
||||
Website string `json:"website"`
|
||||
Location string `json:"location"`
|
||||
PostsCount int `json:"posts_count"`
|
||||
FollowersCount int `json:"followers_count"`
|
||||
FollowingCount int `json:"following_count"`
|
||||
IsFollowing bool `json:"is_following"` // 当前用户是否关注了该用户
|
||||
IsFollowingMe bool `json:"is_following_me"` // 该用户是否关注了当前用户
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// UserDetailResponse 用户详情响应
|
||||
type UserDetailResponse struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email *string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Phone *string `json:"phone,omitempty"` // 仅当前用户自己可见
|
||||
Avatar string `json:"avatar"`
|
||||
CoverURL string `json:"cover_url"` // 头图URL
|
||||
Bio string `json:"bio"`
|
||||
Website string `json:"website"`
|
||||
Location string `json:"location"`
|
||||
PostsCount int `json:"posts_count"`
|
||||
FollowersCount int `json:"followers_count"`
|
||||
FollowingCount int `json:"following_count"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsFollowing bool `json:"is_following"` // 当前用户是否关注了该用户
|
||||
IsFollowingMe bool `json:"is_following_me"` // 该用户是否关注了当前用户
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ==================== Post DTOs ====================
|
||||
|
||||
// PostImageResponse 帖子图片响应
|
||||
type PostImageResponse struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
// PostResponse 帖子响应(列表用)
|
||||
type PostResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Images []PostImageResponse `json:"images"`
|
||||
LikesCount int `json:"likes_count"`
|
||||
CommentsCount int `json:"comments_count"`
|
||||
FavoritesCount int `json:"favorites_count"`
|
||||
SharesCount int `json:"shares_count"`
|
||||
ViewsCount int `json:"views_count"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
IsVote bool `json:"is_vote"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Author *UserResponse `json:"author"`
|
||||
IsLiked bool `json:"is_liked"`
|
||||
IsFavorited bool `json:"is_favorited"`
|
||||
}
|
||||
|
||||
// PostDetailResponse 帖子详情响应
|
||||
type PostDetailResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Images []PostImageResponse `json:"images"`
|
||||
Status string `json:"status"`
|
||||
LikesCount int `json:"likes_count"`
|
||||
CommentsCount int `json:"comments_count"`
|
||||
FavoritesCount int `json:"favorites_count"`
|
||||
SharesCount int `json:"shares_count"`
|
||||
ViewsCount int `json:"views_count"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
IsVote bool `json:"is_vote"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Author *UserResponse `json:"author"`
|
||||
IsLiked bool `json:"is_liked"`
|
||||
IsFavorited bool `json:"is_favorited"`
|
||||
}
|
||||
|
||||
// ==================== Comment DTOs ====================
|
||||
|
||||
// CommentImageResponse 评论图片响应
|
||||
type CommentImageResponse struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// CommentResponse 评论响应(扁平化结构,类似B站/抖音)
|
||||
// 第一层级正常展示,第二三四五层级在第一层级的评论区扁平展示
|
||||
type CommentResponse struct {
|
||||
ID string `json:"id"`
|
||||
PostID string `json:"post_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
RootID *string `json:"root_id"`
|
||||
Content string `json:"content"`
|
||||
Images []CommentImageResponse `json:"images"`
|
||||
LikesCount int `json:"likes_count"`
|
||||
RepliesCount int `json:"replies_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Author *UserResponse `json:"author"`
|
||||
IsLiked bool `json:"is_liked"`
|
||||
TargetID *string `json:"target_id,omitempty"` // 被回复的评论ID,前端根据此ID找到被回复用户的昵称
|
||||
Replies []*CommentResponse `json:"replies,omitempty"` // 子回复列表(扁平化,所有层级都在这里)
|
||||
}
|
||||
|
||||
// ==================== Notification DTOs ====================
|
||||
|
||||
// NotificationResponse 通知响应
|
||||
type NotificationResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Data string `json:"data"`
|
||||
IsRead bool `json:"is_read"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ==================== Message Segment DTOs ====================
|
||||
|
||||
// SegmentType Segment类型
|
||||
type SegmentType string
|
||||
|
||||
const (
|
||||
SegmentTypeText SegmentType = "text"
|
||||
SegmentTypeImage SegmentType = "image"
|
||||
SegmentTypeVoice SegmentType = "voice"
|
||||
SegmentTypeVideo SegmentType = "video"
|
||||
SegmentTypeFile SegmentType = "file"
|
||||
SegmentTypeAt SegmentType = "at"
|
||||
SegmentTypeReply SegmentType = "reply"
|
||||
SegmentTypeFace SegmentType = "face"
|
||||
SegmentTypeLink SegmentType = "link"
|
||||
)
|
||||
|
||||
// TextSegmentData 文本数据
|
||||
type TextSegmentData struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// ImageSegmentData 图片数据
|
||||
type ImageSegmentData struct {
|
||||
URL string `json:"url"`
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// VoiceSegmentData 语音数据
|
||||
type VoiceSegmentData struct {
|
||||
URL string `json:"url"`
|
||||
Duration int `json:"duration,omitempty"` // 秒
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// VideoSegmentData 视频数据
|
||||
type VideoSegmentData struct {
|
||||
URL string `json:"url"`
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
Duration int `json:"duration,omitempty"` // 秒
|
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||
FileSize int64 `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
// FileSegmentData 文件数据
|
||||
type FileSegmentData struct {
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
}
|
||||
|
||||
// AtSegmentData @数据
|
||||
type AtSegmentData struct {
|
||||
UserID string `json:"user_id"` // "all" 表示@所有人
|
||||
Nickname string `json:"nickname,omitempty"`
|
||||
}
|
||||
|
||||
// ReplySegmentData 回复数据
|
||||
type ReplySegmentData struct {
|
||||
ID string `json:"id"` // 被回复消息的ID
|
||||
}
|
||||
|
||||
// FaceSegmentData 表情数据
|
||||
type FaceSegmentData struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// LinkSegmentData 链接数据
|
||||
type LinkSegmentData struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
// ==================== Message DTOs ====================
|
||||
|
||||
// MessageResponse 消息响应
|
||||
type MessageResponse struct {
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
SenderID string `json:"sender_id"`
|
||||
Seq int64 `json:"seq"`
|
||||
Segments model.MessageSegments `json:"segments"` // 消息链(必须)
|
||||
ReplyToID *string `json:"reply_to_id,omitempty"` // 被回复消息的ID(用于关联查找)
|
||||
Status string `json:"status"`
|
||||
Category string `json:"category,omitempty"` // 消息类别:chat, notification, announcement
|
||||
CreatedAt string `json:"created_at"`
|
||||
Sender *UserResponse `json:"sender"`
|
||||
}
|
||||
|
||||
// ConversationResponse 会话响应
|
||||
type ConversationResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
Group *GroupResponse `json:"group,omitempty"`
|
||||
LastSeq int64 `json:"last_seq"`
|
||||
LastMessage *MessageResponse `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int `json:"unread_count"`
|
||||
Participants []*UserResponse `json:"participants,omitempty"` // 私聊时使用
|
||||
MemberCount int `json:"member_count,omitempty"` // 群聊时使用
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ConversationParticipantResponse 会话参与者响应
|
||||
type ConversationParticipantResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
LastReadSeq int64 `json:"last_read_seq"`
|
||||
Muted bool `json:"muted"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
}
|
||||
|
||||
// ==================== Auth DTOs ====================
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
User *UserResponse `json:"user"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// RegisterResponse 注册响应
|
||||
type RegisterResponse struct {
|
||||
User *UserResponse `json:"user"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// RefreshTokenResponse 刷新Token响应
|
||||
type RefreshTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// ==================== Common DTOs ====================
|
||||
|
||||
// SuccessResponse 通用成功响应
|
||||
type SuccessResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// AvailableResponse 可用性检查响应
|
||||
type AvailableResponse struct {
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
// CountResponse 数量响应
|
||||
type CountResponse struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// URLResponse URL响应
|
||||
type URLResponse struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ==================== Chat Request DTOs ====================
|
||||
|
||||
// CreateConversationRequest 创建会话请求
|
||||
type CreateConversationRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"` // 目标用户ID (UUID格式)
|
||||
}
|
||||
|
||||
// SendMessageRequest 发送消息请求
|
||||
type SendMessageRequest struct {
|
||||
Segments model.MessageSegments `json:"segments" binding:"required"` // 消息链(必须)
|
||||
ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID (string类型)
|
||||
}
|
||||
|
||||
// MarkReadRequest 标记已读请求
|
||||
type MarkReadRequest struct {
|
||||
LastReadSeq int64 `json:"last_read_seq" binding:"required"` // 已读到的seq位置
|
||||
}
|
||||
|
||||
// SetConversationPinnedRequest 设置会话置顶请求
|
||||
type SetConversationPinnedRequest struct {
|
||||
ConversationID string `json:"conversation_id" binding:"required"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
}
|
||||
|
||||
// ==================== Chat Response DTOs ====================
|
||||
|
||||
// ConversationListResponse 会话列表响应
|
||||
type ConversationListResponse struct {
|
||||
Conversations []*ConversationResponse `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ConversationDetailResponse 会话详情响应
|
||||
type ConversationDetailResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
LastSeq int64 `json:"last_seq"`
|
||||
LastMessage *MessageResponse `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
Participants []*UserResponse `json:"participants"`
|
||||
MyLastReadSeq int64 `json:"my_last_read_seq"` // 当前用户的已读位置
|
||||
OtherLastReadSeq int64 `json:"other_last_read_seq"` // 对方用户的已读位置
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UnreadCountResponse 未读数响应
|
||||
type UnreadCountResponse struct {
|
||||
TotalUnreadCount int64 `json:"total_unread_count"` // 所有会话的未读总数
|
||||
}
|
||||
|
||||
// ConversationUnreadCountResponse 单个会话未读数响应
|
||||
type ConversationUnreadCountResponse struct {
|
||||
ConversationID string `json:"conversation_id"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
}
|
||||
|
||||
// MessageListResponse 消息列表响应
|
||||
type MessageListResponse struct {
|
||||
Messages []*MessageResponse `json:"messages"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// MessageSyncResponse 消息同步响应(增量同步)
|
||||
type MessageSyncResponse struct {
|
||||
Messages []*MessageResponse `json:"messages"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
// ==================== 设备Token DTOs ====================
|
||||
|
||||
// RegisterDeviceRequest 注册设备请求
|
||||
type RegisterDeviceRequest struct {
|
||||
DeviceID string `json:"device_id" binding:"required"`
|
||||
DeviceType string `json:"device_type" binding:"required,oneof=ios android web"`
|
||||
PushToken string `json:"push_token"`
|
||||
DeviceName string `json:"device_name"`
|
||||
}
|
||||
|
||||
// DeviceTokenResponse 设备Token响应
|
||||
type DeviceTokenResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IsActive bool `json:"is_active"`
|
||||
DeviceName string `json:"device_name"`
|
||||
LastUsedAt time.Time `json:"last_used_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ==================== 推送记录 DTOs ====================
|
||||
|
||||
// PushRecordResponse 推送记录响应
|
||||
type PushRecordResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
MessageID string `json:"message_id"`
|
||||
PushChannel string `json:"push_channel"`
|
||||
PushStatus string `json:"push_status"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
PushedAt time.Time `json:"pushed_at,omitempty"`
|
||||
DeliveredAt time.Time `json:"delivered_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// PushRecordListResponse 推送记录列表响应
|
||||
type PushRecordListResponse struct {
|
||||
Records []*PushRecordResponse `json:"records"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// ==================== 系统消息 DTOs ====================
|
||||
|
||||
// SystemMessageResponse 系统消息响应
|
||||
type SystemMessageResponse struct {
|
||||
ID string `json:"id"`
|
||||
SenderID string `json:"sender_id"`
|
||||
ReceiverID string `json:"receiver_id"`
|
||||
Content string `json:"content"`
|
||||
Category string `json:"category"`
|
||||
SystemType string `json:"system_type"`
|
||||
ExtraData map[string]interface{} `json:"extra_data,omitempty"`
|
||||
IsRead bool `json:"is_read"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// SystemMessageListResponse 系统消息列表响应
|
||||
type SystemMessageListResponse struct {
|
||||
Messages []*SystemMessageResponse `json:"messages"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// SystemUnreadCountResponse 系统消息未读数响应
|
||||
type SystemUnreadCountResponse struct {
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
}
|
||||
|
||||
// ==================== 时间格式化 ====================
|
||||
|
||||
// FormatTime 格式化时间
|
||||
func FormatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
|
||||
// FormatTimePointer 格式化时间指针
|
||||
func FormatTimePointer(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return FormatTime(*t)
|
||||
}
|
||||
|
||||
// ==================== Group DTOs ====================
|
||||
|
||||
// CreateGroupRequest 创建群组请求
|
||||
type CreateGroupRequest struct {
|
||||
Name string `json:"name" binding:"required,max=50"`
|
||||
Description string `json:"description" binding:"max=500"`
|
||||
MemberIDs []string `json:"member_ids"`
|
||||
}
|
||||
|
||||
// UpdateGroupRequest 更新群组请求
|
||||
type UpdateGroupRequest struct {
|
||||
Name string `json:"name" binding:"omitempty,max=50"`
|
||||
Description string `json:"description" binding:"omitempty,max=500"`
|
||||
Avatar string `json:"avatar" binding:"omitempty,url"`
|
||||
}
|
||||
|
||||
// InviteMembersRequest 邀请成员请求
|
||||
type InviteMembersRequest struct {
|
||||
MemberIDs []string `json:"member_ids" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// TransferOwnerRequest 转让群主请求
|
||||
type TransferOwnerRequest struct {
|
||||
NewOwnerID string `json:"new_owner_id" binding:"required"`
|
||||
}
|
||||
|
||||
// SetRoleRequest 设置角色请求
|
||||
type SetRoleRequest struct {
|
||||
Role string `json:"role" binding:"required,oneof=admin member"`
|
||||
}
|
||||
|
||||
// SetNicknameRequest 设置昵称请求
|
||||
type SetNicknameRequest struct {
|
||||
Nickname string `json:"nickname" binding:"max=50"`
|
||||
}
|
||||
|
||||
// MuteMemberRequest 禁言成员请求
|
||||
type MuteMemberRequest struct {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
// SetMuteAllRequest 设置全员禁言请求
|
||||
type SetMuteAllRequest struct {
|
||||
MuteAll bool `json:"mute_all"`
|
||||
}
|
||||
|
||||
// SetJoinTypeRequest 设置加群方式请求
|
||||
type SetJoinTypeRequest struct {
|
||||
JoinType int `json:"join_type" binding:"min=0,max=2"`
|
||||
}
|
||||
|
||||
// CreateAnnouncementRequest 创建群公告请求
|
||||
type CreateAnnouncementRequest struct {
|
||||
Content string `json:"content" binding:"required,max=2000"`
|
||||
}
|
||||
|
||||
// GroupResponse 群组响应
|
||||
type GroupResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Description string `json:"description"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
MemberCount int `json:"member_count"`
|
||||
MaxMembers int `json:"max_members"`
|
||||
JoinType int `json:"join_type"`
|
||||
MuteAll bool `json:"mute_all"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// GroupMemberResponse 群成员响应
|
||||
type GroupMemberResponse struct {
|
||||
ID string `json:"id"`
|
||||
GroupID string `json:"group_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
Nickname string `json:"nickname"`
|
||||
Muted bool `json:"muted"`
|
||||
JoinTime string `json:"join_time"`
|
||||
User *UserResponse `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// GroupAnnouncementResponse 群公告响应
|
||||
type GroupAnnouncementResponse struct {
|
||||
ID string `json:"id"`
|
||||
GroupID string `json:"group_id"`
|
||||
Content string `json:"content"`
|
||||
AuthorID string `json:"author_id"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// GroupListResponse 群组列表响应
|
||||
type GroupListResponse struct {
|
||||
List []*GroupResponse `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// GroupMemberListResponse 群成员列表响应
|
||||
type GroupMemberListResponse struct {
|
||||
List []*GroupMemberResponse `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// GroupAnnouncementListResponse 群公告列表响应
|
||||
type GroupAnnouncementListResponse struct {
|
||||
List []*GroupAnnouncementResponse `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ==================== WebSocket Event DTOs ====================
|
||||
|
||||
// WSEventResponse WebSocket事件响应结构体
|
||||
// 用于后端推送消息给前端的标准格式
|
||||
type WSEventResponse struct {
|
||||
ID string `json:"id"` // 事件唯一ID (UUID)
|
||||
Time int64 `json:"time"` // 时间戳 (毫秒)
|
||||
Type string `json:"type"` // 事件类型 (message, notification, system等)
|
||||
DetailType string `json:"detail_type"` // 详细类型 (private, group, like, comment等)
|
||||
Seq string `json:"seq"` // 消息序列号
|
||||
Segments model.MessageSegments `json:"segments"` // 消息段数组
|
||||
SenderID string `json:"sender_id"` // 发送者用户ID
|
||||
}
|
||||
|
||||
// ==================== WebSocket Request DTOs ====================
|
||||
|
||||
// SendMessageParams 发送消息参数(用于 REST API)
|
||||
type SendMessageParams struct {
|
||||
DetailType string `json:"detail_type"` // 消息类型: private, group
|
||||
ConversationID string `json:"conversation_id"` // 会话ID
|
||||
Segments model.MessageSegments `json:"segments"` // 消息内容(消息段数组)
|
||||
ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID
|
||||
}
|
||||
|
||||
// DeleteMsgParams 撤回消息参数
|
||||
type DeleteMsgParams struct {
|
||||
MessageID string `json:"message_id"` // 消息ID
|
||||
}
|
||||
|
||||
// ==================== Group Action Params ====================
|
||||
|
||||
// SetGroupKickParams 群组踢人参数
|
||||
type SetGroupKickParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
UserID string `json:"user_id"` // 被踢用户ID
|
||||
RejectAddRequest bool `json:"reject_add_request"` // 是否拒绝再次加群
|
||||
}
|
||||
|
||||
// SetGroupBanParams 群组单人禁言参数
|
||||
type SetGroupBanParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
UserID string `json:"user_id"` // 被禁言用户ID
|
||||
Duration int64 `json:"duration"` // 禁言时长(秒),0表示解除禁言
|
||||
}
|
||||
|
||||
// SetGroupWholeBanParams 群组全员禁言参数
|
||||
type SetGroupWholeBanParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
Enable bool `json:"enable"` // 是否开启全员禁言
|
||||
}
|
||||
|
||||
// SetGroupAdminParams 群组设置管理员参数
|
||||
type SetGroupAdminParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
UserID string `json:"user_id"` // 被设置的用户ID
|
||||
Enable bool `json:"enable"` // 是否设置为管理员
|
||||
}
|
||||
|
||||
// SetGroupNameParams 设置群名参数
|
||||
type SetGroupNameParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
GroupName string `json:"group_name"` // 新群名
|
||||
}
|
||||
|
||||
// SetGroupAvatarParams 设置群头像参数
|
||||
type SetGroupAvatarParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
Avatar string `json:"avatar"` // 头像URL
|
||||
}
|
||||
|
||||
// SetGroupLeaveParams 退出群组参数
|
||||
type SetGroupLeaveParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
}
|
||||
|
||||
// SetGroupAddRequestParams 处理加群请求参数
|
||||
type SetGroupAddRequestParams struct {
|
||||
Flag string `json:"flag"` // 加群请求的flag标识
|
||||
Approve bool `json:"approve"` // 是否同意
|
||||
Reason string `json:"reason"` // 拒绝理由(当approve为false时)
|
||||
}
|
||||
|
||||
// GetConversationListParams 获取会话列表参数
|
||||
type GetConversationListParams struct {
|
||||
Page int `json:"page"` // 页码
|
||||
PageSize int `json:"page_size"` // 每页数量
|
||||
}
|
||||
|
||||
// GetGroupInfoParams 获取群信息参数
|
||||
type GetGroupInfoParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
}
|
||||
|
||||
// GetGroupMemberListParams 获取群成员列表参数
|
||||
type GetGroupMemberListParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
Page int `json:"page"` // 页码
|
||||
PageSize int `json:"page_size"` // 每页数量
|
||||
}
|
||||
|
||||
// ==================== Conversation Action Params ====================
|
||||
|
||||
// CreateConversationParams 创建会话参数
|
||||
type CreateConversationParams struct {
|
||||
UserID string `json:"user_id"` // 目标用户ID(私聊)
|
||||
}
|
||||
|
||||
// MarkReadParams 标记已读参数
|
||||
type MarkReadParams struct {
|
||||
ConversationID string `json:"conversation_id"` // 会话ID
|
||||
LastReadSeq int64 `json:"last_read_seq"` // 最后已读消息序号
|
||||
}
|
||||
|
||||
// SetConversationPinnedParams 设置会话置顶参数
|
||||
type SetConversationPinnedParams struct {
|
||||
ConversationID string `json:"conversation_id"` // 会话ID
|
||||
IsPinned bool `json:"is_pinned"` // 是否置顶
|
||||
}
|
||||
|
||||
// ==================== Group Action Params (Additional) ====================
|
||||
|
||||
// CreateGroupParams 创建群组参数
|
||||
type CreateGroupParams struct {
|
||||
Name string `json:"name"` // 群名
|
||||
Description string `json:"description,omitempty"` // 群描述
|
||||
MemberIDs []string `json:"member_ids,omitempty"` // 初始成员ID列表
|
||||
}
|
||||
|
||||
// GetUserGroupsParams 获取用户群组列表参数
|
||||
type GetUserGroupsParams struct {
|
||||
Page int `json:"page"` // 页码
|
||||
PageSize int `json:"page_size"` // 每页数量
|
||||
}
|
||||
|
||||
// TransferOwnerParams 转让群主参数
|
||||
type TransferOwnerParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
NewOwnerID string `json:"new_owner_id"` // 新群主ID
|
||||
}
|
||||
|
||||
// InviteMembersParams 邀请成员参数
|
||||
type InviteMembersParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
MemberIDs []string `json:"member_ids"` // 被邀请的用户ID列表
|
||||
}
|
||||
|
||||
// JoinGroupParams 加入群组参数
|
||||
type JoinGroupParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
}
|
||||
|
||||
// SetNicknameParams 设置群内昵称参数
|
||||
type SetNicknameParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
Nickname string `json:"nickname"` // 群内昵称
|
||||
}
|
||||
|
||||
// SetJoinTypeParams 设置加群方式参数
|
||||
type SetJoinTypeParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
JoinType int `json:"join_type"` // 加群方式:0-允许任何人加入,1-需要审批,2-不允许加入
|
||||
}
|
||||
|
||||
// CreateAnnouncementParams 创建群公告参数
|
||||
type CreateAnnouncementParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
Content string `json:"content"` // 公告内容
|
||||
}
|
||||
|
||||
// GetAnnouncementsParams 获取群公告列表参数
|
||||
type GetAnnouncementsParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
Page int `json:"page"` // 页码
|
||||
PageSize int `json:"page_size"` // 每页数量
|
||||
}
|
||||
|
||||
// DeleteAnnouncementParams 删除群公告参数
|
||||
type DeleteAnnouncementParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
AnnouncementID string `json:"announcement_id"` // 公告ID
|
||||
}
|
||||
|
||||
// DissolveGroupParams 解散群组参数
|
||||
type DissolveGroupParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
}
|
||||
|
||||
// GetMyMemberInfoParams 获取我在群组中的成员信息参数
|
||||
type GetMyMemberInfoParams struct {
|
||||
GroupID string `json:"group_id"` // 群组ID
|
||||
}
|
||||
|
||||
// ==================== Vote DTOs ====================
|
||||
|
||||
// CreateVotePostRequest 创建投票帖子请求
|
||||
type CreateVotePostRequest struct {
|
||||
Title string `json:"title" binding:"required,max=200"`
|
||||
Content string `json:"content" binding:"max=2000"`
|
||||
CommunityID string `json:"community_id"`
|
||||
Images []string `json:"images"`
|
||||
VoteOptions []string `json:"vote_options" binding:"required,min=2,max=10"` // 投票选项,至少2个最多10个
|
||||
}
|
||||
|
||||
// VoteOptionDTO 投票选项DTO
|
||||
type VoteOptionDTO struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
VotesCount int `json:"votes_count"`
|
||||
}
|
||||
|
||||
// VoteResultDTO 投票结果DTO
|
||||
type VoteResultDTO struct {
|
||||
Options []VoteOptionDTO `json:"options"`
|
||||
TotalVotes int `json:"total_votes"`
|
||||
HasVoted bool `json:"has_voted"`
|
||||
VotedOptionID string `json:"voted_option_id,omitempty"`
|
||||
}
|
||||
|
||||
// ==================== WebSocket Response DTOs ====================
|
||||
|
||||
// WSResponse WebSocket响应结构体
|
||||
type WSResponse struct {
|
||||
Success bool `json:"success"` // 是否成功
|
||||
Action string `json:"action"` // 响应原action
|
||||
Data interface{} `json:"data,omitempty"` // 响应数据
|
||||
Error string `json:"error,omitempty"` // 错误信息
|
||||
}
|
||||
362
internal/dto/segment.go
Normal file
362
internal/dto/segment.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
)
|
||||
|
||||
// ParseSegmentData 解析Segment数据到目标结构体
|
||||
func ParseSegmentData(segment model.MessageSegment, target interface{}) error {
|
||||
dataBytes, err := json.Marshal(segment.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(dataBytes, target)
|
||||
}
|
||||
|
||||
// NewTextSegment 创建文本Segment
|
||||
func NewTextSegment(content string) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeText),
|
||||
Data: map[string]interface{}{"text": content},
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageSegment 创建图片Segment
|
||||
func NewImageSegment(url string, width, height int, thumbnailURL string) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeImage),
|
||||
Data: map[string]interface{}{
|
||||
"url": url,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"thumbnail_url": thumbnailURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageSegmentWithSize 创建带文件大小的图片Segment
|
||||
func NewImageSegmentWithSize(url string, width, height int, thumbnailURL string, fileSize int64) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeImage),
|
||||
Data: map[string]interface{}{
|
||||
"url": url,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"thumbnail_url": thumbnailURL,
|
||||
"file_size": fileSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewVoiceSegment 创建语音Segment
|
||||
func NewVoiceSegment(url string, duration int, fileSize int64) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeVoice),
|
||||
Data: map[string]interface{}{
|
||||
"url": url,
|
||||
"duration": duration,
|
||||
"file_size": fileSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewVideoSegment 创建视频Segment
|
||||
func NewVideoSegment(url string, width, height, duration int, thumbnailURL string, fileSize int64) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeVideo),
|
||||
Data: map[string]interface{}{
|
||||
"url": url,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"duration": duration,
|
||||
"thumbnail_url": thumbnailURL,
|
||||
"file_size": fileSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewFileSegment 创建文件Segment
|
||||
func NewFileSegment(url, name string, size int64, mimeType string) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeFile),
|
||||
Data: map[string]interface{}{
|
||||
"url": url,
|
||||
"name": name,
|
||||
"size": size,
|
||||
"mime_type": mimeType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewAtSegment 创建@Segment,只存储 user_id,昵称由前端根据群成员列表实时解析
|
||||
func NewAtSegment(userID string) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeAt),
|
||||
Data: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewAtAllSegment 创建@所有人Segment
|
||||
func NewAtAllSegment() model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeAt),
|
||||
Data: map[string]interface{}{
|
||||
"user_id": "all",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewReplySegment 创建回复Segment
|
||||
func NewReplySegment(messageID string) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeReply),
|
||||
Data: map[string]interface{}{"id": messageID},
|
||||
}
|
||||
}
|
||||
|
||||
// NewFaceSegment 创建表情Segment
|
||||
func NewFaceSegment(id int, name, url string) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeFace),
|
||||
Data: map[string]interface{}{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"url": url,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewLinkSegment 创建链接Segment
|
||||
func NewLinkSegment(url, title, description, image string) model.MessageSegment {
|
||||
return model.MessageSegment{
|
||||
Type: string(SegmentTypeLink),
|
||||
Data: map[string]interface{}{
|
||||
"url": url,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"image": image,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractTextContentFromJSON 从JSON格式的segments中提取纯文本内容
|
||||
// 用于从数据库读取的 []byte 格式的 segments
|
||||
// 已废弃:现在数据库直接存储 model.MessageSegments 类型
|
||||
func ExtractTextContentFromJSON(segmentsJSON []byte) string {
|
||||
if len(segmentsJSON) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var segments model.MessageSegments
|
||||
if err := json.Unmarshal(segmentsJSON, &segments); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ExtractTextContentFromModel(segments)
|
||||
}
|
||||
|
||||
// ExtractTextContentFromModel 从 model.MessageSegments 中提取纯文本内容
|
||||
func ExtractTextContentFromModel(segments model.MessageSegments) string {
|
||||
var result string
|
||||
for _, segment := range segments {
|
||||
switch segment.Type {
|
||||
case "text":
|
||||
if text, ok := segment.Data["text"].(string); ok {
|
||||
result += text
|
||||
}
|
||||
case "at":
|
||||
userID, _ := segment.Data["user_id"].(string)
|
||||
if userID == "all" {
|
||||
result += "@所有人 "
|
||||
} else if userID != "" {
|
||||
// 昵称由前端实时解析,后端文本提取仅用于推送通知兜底
|
||||
result += "@某人 "
|
||||
}
|
||||
case "image":
|
||||
result += "[图片]"
|
||||
case "voice":
|
||||
result += "[语音]"
|
||||
case "video":
|
||||
result += "[视频]"
|
||||
case "file":
|
||||
if name, ok := segment.Data["name"].(string); ok && name != "" {
|
||||
result += fmt.Sprintf("[文件:%s]", name)
|
||||
} else {
|
||||
result += "[文件]"
|
||||
}
|
||||
case "face":
|
||||
if name, ok := segment.Data["name"].(string); ok && name != "" {
|
||||
result += fmt.Sprintf("[%s]", name)
|
||||
} else {
|
||||
result += "[表情]"
|
||||
}
|
||||
case "link":
|
||||
if title, ok := segment.Data["title"].(string); ok && title != "" {
|
||||
result += fmt.Sprintf("[链接:%s]", title)
|
||||
} else {
|
||||
result += "[链接]"
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ExtractTextContent 从消息链中提取纯文本内容
|
||||
// 用于搜索、通知展示等场景
|
||||
func ExtractTextContent(segments model.MessageSegments) string {
|
||||
return ExtractTextContentFromModel(segments)
|
||||
}
|
||||
|
||||
// ExtractMentionedUsers 从消息链中提取被@的用户ID列表
|
||||
// 不包括 "all"(@所有人)
|
||||
func ExtractMentionedUsers(segments model.MessageSegments) []string {
|
||||
var userIDs []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, segment := range segments {
|
||||
if segment.Type == string(SegmentTypeAt) {
|
||||
userID, _ := segment.Data["user_id"].(string)
|
||||
if userID != "all" && userID != "" && !seen[userID] {
|
||||
userIDs = append(userIDs, userID)
|
||||
seen[userID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
// IsAtAll 检查消息是否@了所有人
|
||||
func IsAtAll(segments model.MessageSegments) bool {
|
||||
for _, segment := range segments {
|
||||
if segment.Type == string(SegmentTypeAt) {
|
||||
if userID, ok := segment.Data["user_id"].(string); ok && userID == "all" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetReplyMessageID 从消息链中获取被回复的消息ID
|
||||
// 如果没有回复segment,返回空字符串
|
||||
func GetReplyMessageID(segments model.MessageSegments) string {
|
||||
for _, segment := range segments {
|
||||
if segment.Type == string(SegmentTypeReply) {
|
||||
if id, ok := segment.Data["id"].(string); ok {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BuildSegmentsFromContent 从旧版content构建segments
|
||||
// 用于兼容旧版本消息
|
||||
func BuildSegmentsFromContent(contentType, content string, mediaURL *string) model.MessageSegments {
|
||||
var segments model.MessageSegments
|
||||
|
||||
switch contentType {
|
||||
case "text":
|
||||
segments = append(segments, NewTextSegment(content))
|
||||
case "image":
|
||||
if mediaURL != nil {
|
||||
segments = append(segments, NewImageSegment(*mediaURL, 0, 0, ""))
|
||||
}
|
||||
case "voice":
|
||||
if mediaURL != nil {
|
||||
segments = append(segments, NewVoiceSegment(*mediaURL, 0, 0))
|
||||
}
|
||||
case "video":
|
||||
if mediaURL != nil {
|
||||
segments = append(segments, NewVideoSegment(*mediaURL, 0, 0, 0, "", 0))
|
||||
}
|
||||
case "file":
|
||||
if mediaURL != nil {
|
||||
segments = append(segments, NewFileSegment(*mediaURL, content, 0, ""))
|
||||
}
|
||||
default:
|
||||
// 默认当作文本处理
|
||||
if content != "" {
|
||||
segments = append(segments, NewTextSegment(content))
|
||||
}
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
// HasSegmentType 检查消息链中是否包含指定类型的segment
|
||||
func HasSegmentType(segments model.MessageSegments, segmentType SegmentType) bool {
|
||||
for _, segment := range segments {
|
||||
if segment.Type == string(segmentType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSegmentsByType 获取消息链中所有指定类型的segment
|
||||
func GetSegmentsByType(segments model.MessageSegments, segmentType SegmentType) []model.MessageSegment {
|
||||
var result []model.MessageSegment
|
||||
for _, segment := range segments {
|
||||
if segment.Type == string(segmentType) {
|
||||
result = append(result, segment)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetFirstImageURL 获取消息链中第一张图片的URL
|
||||
// 如果没有图片,返回空字符串
|
||||
func GetFirstImageURL(segments model.MessageSegments) string {
|
||||
for _, segment := range segments {
|
||||
if segment.Type == string(SegmentTypeImage) {
|
||||
if url, ok := segment.Data["url"].(string); ok {
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetFirstMediaURL 获取消息链中第一个媒体文件的URL(图片/视频/语音/文件)
|
||||
// 用于兼容旧版本API
|
||||
func GetFirstMediaURL(segments model.MessageSegments) string {
|
||||
for _, segment := range segments {
|
||||
switch segment.Type {
|
||||
case string(SegmentTypeImage), string(SegmentTypeVideo), string(SegmentTypeVoice), string(SegmentTypeFile):
|
||||
if url, ok := segment.Data["url"].(string); ok {
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DetermineContentType 从消息链推断消息类型(用于兼容旧版本)
|
||||
func DetermineContentType(segments model.MessageSegments) string {
|
||||
if len(segments) == 0 {
|
||||
return "text"
|
||||
}
|
||||
|
||||
// 优先检查媒体类型
|
||||
for _, segment := range segments {
|
||||
switch segment.Type {
|
||||
case string(SegmentTypeImage):
|
||||
return "image"
|
||||
case string(SegmentTypeVideo):
|
||||
return "video"
|
||||
case string(SegmentTypeVoice):
|
||||
return "voice"
|
||||
case string(SegmentTypeFile):
|
||||
return "file"
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回text
|
||||
return "text"
|
||||
}
|
||||
Reference in New Issue
Block a user