diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index f8ca6bb..7307c30 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -61,7 +61,9 @@ func InitializeApp() (*App, error) { dataChangeLogService := wire.ProvideDataChangeLogService(asyncLogManager, dataChangeLogRepository) logService := wire.ProvideLogService(operationLogService, loginLogService, dataChangeLogService) userHandler := wire.ProvideUserHandler(userService, userActivityService, postService, commentService, logService) - postHandler := wire.ProvidePostHandler(postService, userService, logService) + channelRepository := repository.NewChannelRepository(db) + channelService := wire.ProvideChannelService(channelRepository) + postHandler := wire.ProvidePostHandler(postService, userService, channelService, logService) commentHandler := handler.NewCommentHandler(commentService) chatService := wire.ProvideChatService(db, messageRepository, userRepository, hub, cache) messageService := wire.ProvideMessageService(db, messageRepository, cache) @@ -87,8 +89,6 @@ func InitializeApp() (*App, error) { voteRepository := repository.NewVoteRepository(db) voteService := wire.ProvideVoteService(voteRepository, postRepository, cache, postAIService, systemMessageService) voteHandler := handler.NewVoteHandler(voteService, postService) - channelRepository := repository.NewChannelRepository(db) - channelService := wire.ProvideChannelService(channelRepository) channelHandler := handler.NewChannelHandler(channelService) scheduleRepository := repository.NewScheduleRepository(db) scheduleService := wire.ProvideScheduleService(scheduleRepository) diff --git a/internal/dto/dto.go b/internal/dto/dto.go index 602e057..aa82312 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -120,6 +120,12 @@ type AdminUpdateUserStatusRequest struct { // ==================== Post DTOs ==================== +// PostChannelBrief 帖子所属频道(列表/详情卡片展示) +type PostChannelBrief struct { + ID string `json:"id"` + Name string `json:"name"` +} + // PostImageResponse 帖子图片响应 type PostImageResponse struct { ID string `json:"id"` @@ -154,6 +160,7 @@ type PostResponse struct { Author *UserResponse `json:"author"` IsLiked bool `json:"is_liked"` IsFavorited bool `json:"is_favorited"` + Channel *PostChannelBrief `json:"channel,omitempty"` } // PostDetailResponse 帖子详情响应 @@ -179,6 +186,7 @@ type PostDetailResponse struct { Author *UserResponse `json:"author"` IsLiked bool `json:"is_liked"` IsFavorited bool `json:"is_favorited"` + Channel *PostChannelBrief `json:"channel,omitempty"` } // ==================== Comment DTOs ==================== diff --git a/internal/dto/post_converter.go b/internal/dto/post_converter.go index bb4d18a..5c717f5 100644 --- a/internal/dto/post_converter.go +++ b/internal/dto/post_converter.go @@ -31,8 +31,42 @@ func ConvertPostImagesToResponse(images []model.PostImage) []PostImageResponse { return result } -// ConvertPostToResponse 将Post转换为PostResponse(列表用) -func ConvertPostToResponse(post *model.Post, isLiked, isFavorited bool) *PostResponse { +func postChannelBrief(post *model.Post, channelByID map[string]*model.Channel) *PostChannelBrief { + if post == nil || post.ChannelID == nil || *post.ChannelID == "" || channelByID == nil { + return nil + } + ch := channelByID[*post.ChannelID] + if ch == nil { + return nil + } + return &PostChannelBrief{ID: ch.ID, Name: ch.Name} +} + +// CollectPostChannelIDs 从帖子列表收集去重后的频道 ID(用于批量查库填充 channel 名称) +func CollectPostChannelIDs(posts []*model.Post) []string { + seen := make(map[string]struct{}) + out := make([]string, 0) + for _, p := range posts { + if p == nil || p.ChannelID == nil || *p.ChannelID == "" { + continue + } + id := *p.ChannelID + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + return out +} + +// PostChannelBriefForPost 根据帖子与频道映射生成频道摘要(供 Handler 手动组装响应) +func PostChannelBriefForPost(post *model.Post, channelByID map[string]*model.Channel) *PostChannelBrief { + return postChannelBrief(post, channelByID) +} + +// ConvertPostToResponse 将Post转换为PostResponse(列表用);channelByID 可为 nil +func ConvertPostToResponse(post *model.Post, channelByID map[string]*model.Channel, isLiked, isFavorited bool) *PostResponse { if post == nil { return nil } @@ -69,11 +103,12 @@ func ConvertPostToResponse(post *model.Post, isLiked, isFavorited bool) *PostRes Author: author, IsLiked: isLiked, IsFavorited: isFavorited, + Channel: postChannelBrief(post, channelByID), } } -// ConvertPostToDetailResponse 将Post转换为PostDetailResponse -func ConvertPostToDetailResponse(post *model.Post, isLiked, isFavorited bool) *PostDetailResponse { +// ConvertPostToDetailResponse 将Post转换为PostDetailResponse;channelByID 可为 nil +func ConvertPostToDetailResponse(post *model.Post, channelByID map[string]*model.Channel, isLiked, isFavorited bool) *PostDetailResponse { if post == nil { return nil } @@ -110,11 +145,12 @@ func ConvertPostToDetailResponse(post *model.Post, isLiked, isFavorited bool) *P Author: author, IsLiked: isLiked, IsFavorited: isFavorited, + Channel: postChannelBrief(post, channelByID), } } -// ConvertPostsToResponse 将Post列表转换为响应列表(每个帖子独立检查点赞/收藏状态) -func ConvertPostsToResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse { +// ConvertPostsToResponse 将Post列表转换为响应列表(每个帖子独立检查点赞/收藏状态);channelByID 可为 nil +func ConvertPostsToResponse(posts []*model.Post, channelByID map[string]*model.Channel, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse { result := make([]*PostResponse, 0, len(posts)) for _, post := range posts { isLiked := false @@ -125,21 +161,21 @@ func ConvertPostsToResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[ if isFavoritedMap != nil { isFavorited = isFavoritedMap[post.ID] } - result = append(result, ConvertPostToResponse(post, isLiked, isFavorited)) + result = append(result, ConvertPostToResponse(post, channelByID, isLiked, isFavorited)) } return result } // BuildPostResponse 构建单个帖子响应(包含交互状态) // 这是一个语义化的辅助函数,便于 Handler 层调用 -func BuildPostResponse(post *model.Post, isLiked, isFavorited bool) *PostResponse { - return ConvertPostToResponse(post, isLiked, isFavorited) +func BuildPostResponse(post *model.Post, channelByID map[string]*model.Channel, isLiked, isFavorited bool) *PostResponse { + return ConvertPostToResponse(post, channelByID, isLiked, isFavorited) } // BuildPostsWithInteractionResponse 批量构建帖子响应(包含交互状态) // 这是一个语义化的辅助函数,便于 Handler 层调用 -func BuildPostsWithInteractionResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse { - return ConvertPostsToResponse(posts, isLikedMap, isFavoritedMap) +func BuildPostsWithInteractionResponse(posts []*model.Post, channelByID map[string]*model.Channel, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse { + return ConvertPostsToResponse(posts, channelByID, isLikedMap, isFavoritedMap) } // ==================== Comment 转换 ==================== diff --git a/internal/handler/post_handler.go b/internal/handler/post_handler.go index 079644a..ba65c68 100644 --- a/internal/handler/post_handler.go +++ b/internal/handler/post_handler.go @@ -17,18 +17,33 @@ import ( // PostHandler 帖子处理器 type PostHandler struct { - postService service.PostService - userService service.UserService + postService service.PostService + userService service.UserService + channelService service.ChannelService } // NewPostHandler 创建帖子处理器 -func NewPostHandler(postService service.PostService, userService service.UserService) *PostHandler { +func NewPostHandler(postService service.PostService, userService service.UserService, channelService service.ChannelService) *PostHandler { return &PostHandler{ - postService: postService, - userService: userService, + postService: postService, + userService: userService, + channelService: channelService, } } +// channelMapForPosts 批量解析帖子中的频道 ID,用于填充响应里的 channel 名称 +func (h *PostHandler) channelMapForPosts(posts []*model.Post) map[string]*model.Channel { + ids := dto.CollectPostChannelIDs(posts) + if len(ids) == 0 { + return nil + } + m, err := h.channelService.MapByIDs(ids) + if err != nil { + return nil + } + return m +} + // Create 创建帖子 func (h *PostHandler) Create(c *gin.Context) { userID := c.GetString("user_id") @@ -61,7 +76,8 @@ func (h *PostHandler) Create(c *gin.Context) { return } - response.Success(c, dto.ConvertPostToResponse(post, false, false)) + chMap := h.channelMapForPosts([]*model.Post{post}) + response.Success(c, dto.ConvertPostToResponse(post, chMap, false, false)) } // GetByID 获取帖子(不增加浏览量) @@ -123,6 +139,9 @@ func (h *PostHandler) GetByID(c *gin.Context) { IsFavorited: isFavorited, } + chMap := h.channelMapForPosts([]*model.Post{post}) + responseData.Channel = dto.PostChannelBriefForPost(post, chMap) + response.Success(c, responseData) } @@ -251,8 +270,8 @@ func (h *PostHandler) List(c *gin.Context) { } isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID) - // 转换为响应结构 - postResponses := dto.BuildPostsWithInteractionResponse(posts, isLikedMap, isFavoritedMap) + chMap := h.channelMapForPosts(posts) + postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap) response.Paginated(c, postResponses, total, page, pageSize) } @@ -308,8 +327,8 @@ func (h *PostHandler) ListByCursor(c *gin.Context) { } isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID) - // 转换为响应结构 - postResponses := dto.BuildPostsWithInteractionResponse(result.Items, isLikedMap, isFavoritedMap) + chMap := h.channelMapForPosts(result.Items) + postResponses := dto.BuildPostsWithInteractionResponse(result.Items, chMap, isLikedMap, isFavoritedMap) // 构建游标分页响应 cursorResp := &dto.PostCursorPageResponse{ @@ -378,7 +397,8 @@ func (h *PostHandler) Update(c *gin.Context) { currentUserID := c.GetString("user_id") isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), post.ID, currentUserID) - response.Success(c, dto.BuildPostResponse(post, isLiked, isFavorited)) + chMap := h.channelMapForPosts([]*model.Post{post}) + response.Success(c, dto.BuildPostResponse(post, chMap, isLiked, isFavorited)) } // Delete 删除帖子 @@ -437,7 +457,8 @@ func (h *PostHandler) Like(c *gin.Context) { // 获取交互状态 isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID) - response.Success(c, dto.BuildPostResponse(post, isLiked, isFavorited)) + chMap := h.channelMapForPosts([]*model.Post{post}) + response.Success(c, dto.BuildPostResponse(post, chMap, isLiked, isFavorited)) } // Unlike 取消点赞 @@ -466,7 +487,8 @@ func (h *PostHandler) Unlike(c *gin.Context) { // 获取交互状态 isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID) - response.Success(c, dto.BuildPostResponse(post, isLiked, isFavorited)) + chMap := h.channelMapForPosts([]*model.Post{post}) + response.Success(c, dto.BuildPostResponse(post, chMap, isLiked, isFavorited)) } // Favorite 收藏帖子 @@ -495,7 +517,8 @@ func (h *PostHandler) Favorite(c *gin.Context) { // 获取交互状态 isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID) - response.Success(c, dto.BuildPostResponse(post, isLiked, isFavorited)) + chMap := h.channelMapForPosts([]*model.Post{post}) + response.Success(c, dto.BuildPostResponse(post, chMap, isLiked, isFavorited)) } // Unfavorite 取消收藏 @@ -524,7 +547,8 @@ func (h *PostHandler) Unfavorite(c *gin.Context) { // 获取交互状态 isLiked, isFavorited := h.postService.GetPostInteractionStatusSingle(c.Request.Context(), id, userID) - response.Success(c, dto.BuildPostResponse(post, isLiked, isFavorited)) + chMap := h.channelMapForPosts([]*model.Post{post}) + response.Success(c, dto.BuildPostResponse(post, chMap, isLiked, isFavorited)) } // GetUserPosts 获取用户帖子列表(支持游标分页和偏移分页) @@ -557,8 +581,8 @@ func (h *PostHandler) GetUserPosts(c *gin.Context) { } isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID) - // 转换为响应结构 - postResponses := dto.BuildPostsWithInteractionResponse(posts, isLikedMap, isFavoritedMap) + chMap := h.channelMapForPosts(posts) + postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap) response.Paginated(c, postResponses, total, page, pageSize) } @@ -588,8 +612,8 @@ func (h *PostHandler) GetUserPostsByCursor(c *gin.Context) { } isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID) - // 转换为响应结构 - postResponses := dto.BuildPostsWithInteractionResponse(result.Items, isLikedMap, isFavoritedMap) + chMap := h.channelMapForPosts(result.Items) + postResponses := dto.BuildPostsWithInteractionResponse(result.Items, chMap, isLikedMap, isFavoritedMap) // 构建游标分页响应 cursorResp := &dto.PostCursorPageResponse{ @@ -623,8 +647,8 @@ func (h *PostHandler) GetFavorites(c *gin.Context) { } isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID) - // 转换为响应结构 - postResponses := dto.BuildPostsWithInteractionResponse(posts, isLikedMap, isFavoritedMap) + chMap := h.channelMapForPosts(posts) + postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap) response.Paginated(c, postResponses, total, page, pageSize) } @@ -659,8 +683,8 @@ func (h *PostHandler) Search(c *gin.Context) { } isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID) - // 转换为响应结构 - postResponses := dto.BuildPostsWithInteractionResponse(posts, isLikedMap, isFavoritedMap) + chMap := h.channelMapForPosts(posts) + postResponses := dto.BuildPostsWithInteractionResponse(posts, chMap, isLikedMap, isFavoritedMap) response.Paginated(c, postResponses, total, page, pageSize) } @@ -687,8 +711,8 @@ func (h *PostHandler) SearchByCursor(c *gin.Context) { } isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID) - // 转换为响应结构 - postResponses := dto.BuildPostsWithInteractionResponse(result.Items, isLikedMap, isFavoritedMap) + chMap := h.channelMapForPosts(result.Items) + postResponses := dto.BuildPostsWithInteractionResponse(result.Items, chMap, isLikedMap, isFavoritedMap) // 构建游标分页响应 cursorResp := &dto.PostCursorPageResponse{ diff --git a/internal/repository/channel_repo.go b/internal/repository/channel_repo.go index 58e47cb..8c40f66 100644 --- a/internal/repository/channel_repo.go +++ b/internal/repository/channel_repo.go @@ -48,6 +48,19 @@ func (r *ChannelRepository) GetByID(id string) (*model.Channel, error) { return &channel, nil } +// ListByIDs 按 ID 列表查询频道(用于帖子列表批量填充 channel 名称) +func (r *ChannelRepository) ListByIDs(ids []string) ([]*model.Channel, error) { + if len(ids) == 0 { + return nil, nil + } + var list []*model.Channel + err := r.db.Where("id IN ?", ids).Find(&list).Error + if err != nil { + return nil, err + } + return list, nil +} + func (r *ChannelRepository) Delete(id string) error { return r.db.Delete(&model.Channel{}, "id = ?", id).Error } diff --git a/internal/service/channel_service.go b/internal/service/channel_service.go index 3de3c03..15e84a0 100644 --- a/internal/service/channel_service.go +++ b/internal/service/channel_service.go @@ -8,6 +8,7 @@ import ( type ChannelService interface { ListActive() ([]*model.Channel, error) ListAll() ([]*model.Channel, error) + MapByIDs(ids []string) (map[string]*model.Channel, error) Create(name, slug, description string, sortOrder int, isActive bool) (*model.Channel, error) Update(id, name, slug, description string, sortOrder int, isActive bool) (*model.Channel, error) Delete(id string) error @@ -29,6 +30,20 @@ func (s *channelServiceImpl) ListAll() ([]*model.Channel, error) { return s.channelRepo.ListAll() } +func (s *channelServiceImpl) MapByIDs(ids []string) (map[string]*model.Channel, error) { + list, err := s.channelRepo.ListByIDs(ids) + if err != nil { + return nil, err + } + m := make(map[string]*model.Channel, len(list)) + for _, ch := range list { + if ch != nil { + m[ch.ID] = ch + } + } + return m, nil +} + func (s *channelServiceImpl) Create(name, slug, description string, sortOrder int, isActive bool) (*model.Channel, error) { channel := &model.Channel{ Name: name, diff --git a/internal/wire/handler.go b/internal/wire/handler.go index c074b3a..2ea8680 100644 --- a/internal/wire/handler.go +++ b/internal/wire/handler.go @@ -64,9 +64,10 @@ func ProvideUserHandler( func ProvidePostHandler( postService service.PostService, userService service.UserService, + channelService service.ChannelService, logService *service.LogService, ) *handler.PostHandler { - h := handler.NewPostHandler(postService, userService) + h := handler.NewPostHandler(postService, userService, channelService) if ps, ok := postService.(interface{ SetLogService(*service.LogService) }); ok { ps.SetLogService(logService) }