feat(post): enhance post handling with channel integration

- Updated PostHandler to include channel information in post responses.
- Introduced PostChannelBrief DTO to represent channel details associated with posts.
- Modified post conversion functions to support channel data, ensuring proper mapping of channel IDs to channel names.
- Enhanced ChannelRepository and ChannelService to facilitate batch retrieval of channels by IDs.
- Updated wire generation to inject channel service into post handler for improved functionality.
This commit is contained in:
lafay
2026-03-25 00:58:41 +08:00
parent fc0d6ff19d
commit 7c14cf5bab
7 changed files with 137 additions and 40 deletions

View File

@@ -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)

View File

@@ -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 ====================

View File

@@ -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转换为PostDetailResponsechannelByID 可为 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 转换 ====================

View File

@@ -19,16 +19,31 @@ import (
type PostHandler struct {
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,
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{

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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)
}