From fc0d6ff19d4c7f95582cbeece96270ea79a6f47f Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Tue, 24 Mar 2026 22:27:53 +0800 Subject: [PATCH] feat(channel): integrate channel functionality into post management - Added Channel support in Post model, including ChannelID field in PostRequest and PostResponse DTOs. - Updated PostService and PostRepository to handle channel-specific logic for creating and listing posts. - Introduced ChannelRepository and ChannelService, along with corresponding handlers for managing channels. - Enhanced router to include routes for channel management and public channel listing. - Seeded default channels in the database during initialization. --- cmd/server/wire.go | 2 + cmd/server/wire_gen.go | 7 +- internal/dto/dto.go | 6 +- internal/dto/post_converter.go | 2 + internal/handler/channel_handler.go | 123 +++++++++++++++++++++++++ internal/handler/post_handler.go | 30 ++++-- internal/model/channel.go | 32 +++++++ internal/model/init.go | 43 +++++++++ internal/model/post.go | 2 +- internal/repository/channel_repo.go | 54 +++++++++++ internal/repository/post_repo.go | 10 +- internal/router/router.go | 19 ++++ internal/service/admin_post_service.go | 2 +- internal/service/channel_service.go | 65 +++++++++++++ internal/service/post_service.go | 42 +++++---- internal/service/vote_service.go | 12 +-- internal/wire/handler.go | 1 + internal/wire/repository.go | 1 + internal/wire/service.go | 5 + 19 files changed, 416 insertions(+), 42 deletions(-) create mode 100644 internal/handler/channel_handler.go create mode 100644 internal/model/channel.go create mode 100644 internal/repository/channel_repo.go create mode 100644 internal/service/channel_service.go diff --git a/cmd/server/wire.go b/cmd/server/wire.go index f358563..a13ed5e 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -36,6 +36,7 @@ func ProvideRouter( groupHandler *handler.GroupHandler, stickerHandler *handler.StickerHandler, voteHandler *handler.VoteHandler, + channelHandler *handler.ChannelHandler, scheduleHandler *handler.ScheduleHandler, roleHandler *handler.RoleHandler, adminUserHandler *handler.AdminUserHandler, @@ -62,6 +63,7 @@ func ProvideRouter( groupHandler, stickerHandler, voteHandler, + channelHandler, scheduleHandler, roleHandler, adminUserHandler, diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index 88828d9..f8ca6bb 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -87,6 +87,9 @@ 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) server := wire.ProvideGRPCServer(config, logger) @@ -111,7 +114,7 @@ func InitializeApp() (*App, error) { adminLogHandler := handler.NewAdminLogHandler(operationLogService, loginLogService, dataChangeLogService) qrCodeLoginService := wire.ProvideQRCodeLoginService(client, hub, jwtService, userService, userActivityService, logService) qrCodeHandler := handler.NewQRCodeHandler(qrCodeLoginService) - router := ProvideRouter(userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, logService, userActivityService, casbinService) + router := ProvideRouter(userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, logService, userActivityService, casbinService) hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, cache) app := NewApp(config, db, router, pushService, hotRankWorker, server, logger) return app, nil @@ -133,6 +136,7 @@ func ProvideRouter( groupHandler *handler.GroupHandler, stickerHandler *handler.StickerHandler, voteHandler *handler.VoteHandler, + channelHandler *handler.ChannelHandler, scheduleHandler *handler.ScheduleHandler, roleHandler *handler.RoleHandler, adminUserHandler *handler.AdminUserHandler, @@ -159,6 +163,7 @@ func ProvideRouter( groupHandler, stickerHandler, voteHandler, + channelHandler, scheduleHandler, roleHandler, adminUserHandler, diff --git a/internal/dto/dto.go b/internal/dto/dto.go index ab3de99..602e057 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -135,6 +135,7 @@ type PostImageResponse struct { type PostResponse struct { ID string `json:"id"` UserID string `json:"user_id"` + ChannelID *string `json:"channel_id,omitempty"` Title string `json:"title"` Content string `json:"content"` Images []PostImageResponse `json:"images"` @@ -159,6 +160,7 @@ type PostResponse struct { type PostDetailResponse struct { ID string `json:"id"` UserID string `json:"user_id"` + ChannelID *string `json:"channel_id,omitempty"` Title string `json:"title"` Content string `json:"content"` Images []PostImageResponse `json:"images"` @@ -870,7 +872,7 @@ type GetMyMemberInfoParams struct { type CreateVotePostRequest struct { Title string `json:"title" binding:"required,max=200"` Content string `json:"content" binding:"max=2000"` - CommunityID string `json:"community_id"` + ChannelID *string `json:"channel_id,omitempty"` Images []string `json:"images"` VoteOptions []string `json:"vote_options" binding:"required,min=2,max=10"` // 投票选项,至少2个最多10个 } @@ -930,7 +932,7 @@ type AdminPostListResponse struct { type AdminPostDetailResponse struct { ID string `json:"id"` UserID string `json:"user_id"` - CommunityID string `json:"community_id"` + ChannelID *string `json:"channel_id,omitempty"` Title string `json:"title"` Content string `json:"content"` Images []PostImageResponse `json:"images"` diff --git a/internal/dto/post_converter.go b/internal/dto/post_converter.go index f2e806e..bb4d18a 100644 --- a/internal/dto/post_converter.go +++ b/internal/dto/post_converter.go @@ -50,6 +50,7 @@ func ConvertPostToResponse(post *model.Post, isLiked, isFavorited bool) *PostRes return &PostResponse{ ID: post.ID, UserID: post.UserID, + ChannelID: post.ChannelID, Title: post.Title, Content: post.Content, Images: images, @@ -90,6 +91,7 @@ func ConvertPostToDetailResponse(post *model.Post, isLiked, isFavorited bool) *P return &PostDetailResponse{ ID: post.ID, UserID: post.UserID, + ChannelID: post.ChannelID, Title: post.Title, Content: post.Content, Images: images, diff --git a/internal/handler/channel_handler.go b/internal/handler/channel_handler.go new file mode 100644 index 0000000..505bf79 --- /dev/null +++ b/internal/handler/channel_handler.go @@ -0,0 +1,123 @@ +package handler + +import ( + "carrot_bbs/internal/model" + "carrot_bbs/internal/pkg/response" + "carrot_bbs/internal/service" + + "github.com/gin-gonic/gin" +) + +type ChannelHandler struct { + channelService service.ChannelService +} + +func NewChannelHandler(channelService service.ChannelService) *ChannelHandler { + return &ChannelHandler{channelService: channelService} +} + +type channelResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description,omitempty"` + SortOrder int `json:"sort_order"` + IsActive bool `json:"is_active"` +} + +func toChannelResponse(c *model.Channel) *channelResponse { + if c == nil { + return nil + } + return &channelResponse{ + ID: c.ID, + Name: c.Name, + Slug: c.Slug, + Description: c.Description, + SortOrder: c.SortOrder, + IsActive: c.IsActive, + } +} + +// ListPublic 公开频道列表(仅启用) +func (h *ChannelHandler) ListPublic(c *gin.Context) { + items, err := h.channelService.ListActive() + if err != nil { + response.InternalServerError(c, "failed to get channels") + return + } + result := make([]*channelResponse, 0, len(items)) + for _, item := range items { + result = append(result, toChannelResponse(item)) + } + response.Success(c, gin.H{"list": result}) +} + +// AdminList 管理端频道列表(全部) +func (h *ChannelHandler) AdminList(c *gin.Context) { + items, err := h.channelService.ListAll() + if err != nil { + response.InternalServerError(c, "failed to get channels") + return + } + result := make([]*channelResponse, 0, len(items)) + for _, item := range items { + result = append(result, toChannelResponse(item)) + } + response.Success(c, gin.H{"list": result}) +} + +type adminChannelUpsertRequest struct { + Name string `json:"name" binding:"required,max=100"` + Slug string `json:"slug" binding:"required,max=100"` + Description string `json:"description" binding:"max=255"` + SortOrder int `json:"sort_order"` + IsActive *bool `json:"is_active"` +} + +func (h *ChannelHandler) AdminCreate(c *gin.Context) { + var req adminChannelUpsertRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, err.Error()) + return + } + isActive := true + if req.IsActive != nil { + isActive = *req.IsActive + } + item, err := h.channelService.Create(req.Name, req.Slug, req.Description, req.SortOrder, isActive) + if err != nil { + response.InternalServerError(c, "failed to create channel") + return + } + response.Success(c, toChannelResponse(item)) +} + +func (h *ChannelHandler) AdminUpdate(c *gin.Context) { + id := c.Param("id") + var req adminChannelUpsertRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, err.Error()) + return + } + isActive := true + if req.IsActive != nil { + isActive = *req.IsActive + } + item, err := h.channelService.Update(id, req.Name, req.Slug, req.Description, req.SortOrder, isActive) + if err != nil { + response.InternalServerError(c, "failed to update channel") + return + } + response.Success(c, toChannelResponse(item)) +} + +func (h *ChannelHandler) AdminDelete(c *gin.Context) { + id := c.Param("id") + if err := h.channelService.Delete(id); err != nil { + response.InternalServerError(c, "failed to delete channel") + return + } + response.Success(c, gin.H{"success": true}) +} + diff --git a/internal/handler/post_handler.go b/internal/handler/post_handler.go index 470c482..079644a 100644 --- a/internal/handler/post_handler.go +++ b/internal/handler/post_handler.go @@ -38,9 +38,10 @@ func (h *PostHandler) Create(c *gin.Context) { } type CreateRequest struct { - Title string `json:"title" binding:"required"` - Content string `json:"content" binding:"required"` - Images []string `json:"images"` + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + Images []string `json:"images"` + ChannelID *string `json:"channel_id,omitempty"` } var req CreateRequest @@ -49,7 +50,7 @@ func (h *PostHandler) Create(c *gin.Context) { return } - post, err := h.postService.Create(c.Request.Context(), userID, req.Title, req.Content, req.Images) + post, err := h.postService.Create(c.Request.Context(), userID, req.Title, req.Content, req.Images, req.ChannelID) if err != nil { var moderationErr *service.PostModerationRejectedError if errors.As(err, &moderationErr) { @@ -101,6 +102,7 @@ func (h *PostHandler) GetByID(c *gin.Context) { responseData := &dto.PostResponse{ ID: post.ID, UserID: post.UserID, + ChannelID: post.ChannelID, Title: post.Title, Content: post.Content, Images: dto.ConvertPostImagesToResponse(post.Images), @@ -197,6 +199,10 @@ func (h *PostHandler) List(c *gin.Context) { page, pageSize = normalizePagination(page, pageSize) userID := c.Query("user_id") tab := c.Query("tab") + var channelID *string + if cid := c.Query("channel_id"); cid != "" { + channelID = &cid + } // 获取当前用户ID currentUserID := c.GetString("user_id") @@ -220,16 +226,16 @@ func (h *PostHandler) List(c *gin.Context) { case "latest": // 最新帖子 if userID != "" && userID == currentUserID { - posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID) + posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID, channelID) } else { - posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID) + posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID, channelID) } default: // 默认获取最新帖子 if userID != "" && userID == currentUserID { - posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID) + posts, total, err = h.postService.GetLatestPostsForOwner(c.Request.Context(), page, pageSize, userID, channelID) } else { - posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID) + posts, total, err = h.postService.GetLatestPosts(c.Request.Context(), page, pageSize, userID, channelID) } } @@ -256,6 +262,10 @@ func (h *PostHandler) ListByCursor(c *gin.Context) { userID := c.Query("user_id") currentUserID := c.GetString("user_id") tab := c.Query("tab") + var channelID *string + if cid := c.Query("channel_id"); cid != "" { + channelID = &cid + } // 解析游标分页请求 req := h.parseCursorRequest(c) @@ -280,10 +290,10 @@ func (h *PostHandler) ListByCursor(c *gin.Context) { result, err = h.postService.GetHotPostsByCursor(c.Request.Context(), req) case "latest": // 最新帖子 - result, err = h.postService.ListByCursor(c.Request.Context(), userID, includePending, req) + result, err = h.postService.ListByCursor(c.Request.Context(), userID, includePending, channelID, req) default: // 默认获取最新帖子 - result, err = h.postService.ListByCursor(c.Request.Context(), userID, includePending, req) + result, err = h.postService.ListByCursor(c.Request.Context(), userID, includePending, channelID, req) } if err != nil { diff --git a/internal/model/channel.go b/internal/model/channel.go new file mode 100644 index 0000000..08e63db --- /dev/null +++ b/internal/model/channel.go @@ -0,0 +1,32 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Channel 频道配置(用于帖子分区) +type Channel struct { + ID string `json:"id" gorm:"type:varchar(36);primaryKey"` + Name string `json:"name" gorm:"type:varchar(100);not null"` + Slug string `json:"slug" gorm:"type:varchar(100);uniqueIndex;not null"` + Description string `json:"description" gorm:"type:varchar(255)"` + SortOrder int `json:"sort_order" gorm:"default:0;index"` + IsActive bool `json:"is_active" gorm:"default:true;index"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} + +func (c *Channel) BeforeCreate(tx *gorm.DB) error { + if c.ID == "" { + c.ID = uuid.New().String() + } + return nil +} + +func (Channel) TableName() string { + return "channels" +} + diff --git a/internal/model/init.go b/internal/model/init.go index d343900..fabe39d 100644 --- a/internal/model/init.go +++ b/internal/model/init.go @@ -98,6 +98,7 @@ func autoMigrate(db *gorm.DB) error { // 帖子相关 &Post{}, &PostImage{}, + &Channel{}, // 评论相关 &Comment{}, @@ -178,6 +179,11 @@ func autoMigrate(db *gorm.DB) error { return fmt.Errorf("failed to seed permissions: %w", err) } + // 初始化频道配置种子数据 + if err := seedChannels(db); err != nil { + return fmt.Errorf("failed to seed channels: %w", err) + } + return nil } @@ -315,6 +321,43 @@ func seedPermissions(db *gorm.DB) error { return nil } +// seedChannels 初始化频道种子数据(按 slug 幂等) +func seedChannels(db *gorm.DB) error { + defaultChannels := []Channel{ + {Name: "跳蚤市场", Slug: "market", Description: "二手交易与闲置发布", SortOrder: 10, IsActive: true}, + {Name: "课程交流", Slug: "courses", Description: "课程资料、选课与学习讨论", SortOrder: 20, IsActive: true}, + {Name: "工作实习", Slug: "jobs", Description: "实习、内推与求职信息", SortOrder: 30, IsActive: true}, + {Name: "考研考公", Slug: "exam", Description: "备考经验、资料与互助", SortOrder: 40, IsActive: true}, + {Name: "保研出国", Slug: "graduate-abroad", Description: "保研、留学申请与经验分享", SortOrder: 50, IsActive: true}, + } + + for _, item := range defaultChannels { + var existing Channel + err := db.Where("slug = ?", item.Slug).First(&existing).Error + if err == nil { + updates := map[string]any{ + "name": item.Name, + "description": item.Description, + "sort_order": item.SortOrder, + "is_active": item.IsActive, + } + if uerr := db.Model(&existing).Updates(updates).Error; uerr != nil { + return uerr + } + continue + } + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + if cerr := db.Create(&item).Error; cerr != nil { + return cerr + } + } + + zap.L().Info("Seeded channels successfully", zap.Int("count", len(defaultChannels))) + return nil +} + // CloseDB 关闭数据库连接 func CloseDB(db *gorm.DB) { if sqlDB, err := db.DB(); err == nil { diff --git a/internal/model/post.go b/internal/model/post.go index d95adb5..4546506 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -22,7 +22,7 @@ const ( type Post struct { ID string `json:"id" gorm:"type:varchar(36);primaryKey"` UserID string `json:"user_id" gorm:"type:varchar(36);index;index:idx_posts_user_status_created,priority:1;not null"` - CommunityID string `json:"community_id" gorm:"type:varchar(36);index"` + ChannelID *string `json:"channel_id,omitempty" gorm:"type:varchar(36);index"` Title string `json:"title" gorm:"type:varchar(200);not null"` Content string `json:"content" gorm:"type:text;not null"` diff --git a/internal/repository/channel_repo.go b/internal/repository/channel_repo.go new file mode 100644 index 0000000..58e47cb --- /dev/null +++ b/internal/repository/channel_repo.go @@ -0,0 +1,54 @@ +package repository + +import ( + "carrot_bbs/internal/model" + + "gorm.io/gorm" +) + +// ChannelRepository 频道配置仓储 +type ChannelRepository struct { + db *gorm.DB +} + +func NewChannelRepository(db *gorm.DB) *ChannelRepository { + return &ChannelRepository{db: db} +} + +func (r *ChannelRepository) ListActive() ([]*model.Channel, error) { + var channels []*model.Channel + err := r.db. + Where("is_active = ?", true). + Order("sort_order ASC, created_at ASC"). + Find(&channels).Error + return channels, err +} + +func (r *ChannelRepository) ListAll() ([]*model.Channel, error) { + var channels []*model.Channel + err := r.db. + Order("sort_order ASC, created_at ASC"). + Find(&channels).Error + return channels, err +} + +func (r *ChannelRepository) Create(channel *model.Channel) error { + return r.db.Create(channel).Error +} + +func (r *ChannelRepository) Update(channel *model.Channel) error { + return r.db.Save(channel).Error +} + +func (r *ChannelRepository) GetByID(id string) (*model.Channel, error) { + var channel model.Channel + if err := r.db.First(&channel, "id = ?", id).Error; err != nil { + return nil, err + } + return &channel, nil +} + +func (r *ChannelRepository) Delete(id string) error { + return r.db.Delete(&model.Channel{}, "id = ?", id).Error +} + diff --git a/internal/repository/post_repo.go b/internal/repository/post_repo.go index d0d267a..c2c1074 100644 --- a/internal/repository/post_repo.go +++ b/internal/repository/post_repo.go @@ -166,7 +166,7 @@ func (r *PostRepository) Delete(id string) error { // List 分页获取帖子列表 // includePending=true 时,仅在指定 userID 下额外返回 pending(用于作者查看自己待审核帖子) -func (r *PostRepository) List(page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) { +func (r *PostRepository) List(page, pageSize int, userID string, includePending bool, channelID *string) ([]*model.Post, int64, error) { var posts []*model.Post var total int64 @@ -183,6 +183,9 @@ func (r *PostRepository) List(page, pageSize int, userID string, includePending } else { query = query.Where("status = ?", model.PostStatusPublished) } + if channelID != nil && *channelID != "" { + query = query.Where("channel_id = ?", *channelID) + } query.Count(&total) @@ -849,7 +852,7 @@ func (r *PostRepository) UpdateFeatureStatus(postID string, isFeatured bool) err // GetPostsByCursor 游标分页获取帖子列表 // includePending=true 时,仅在指定 userID 下额外返回 pending(用于作者查看自己待审核帖子) -func (r *PostRepository) GetPostsByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) { +func (r *PostRepository) GetPostsByCursor(ctx context.Context, userID string, includePending bool, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) { db := r.getDB(ctx).WithContext(ctx) // 构建基础查询 @@ -866,6 +869,9 @@ func (r *PostRepository) GetPostsByCursor(ctx context.Context, userID string, in } else { query = query.Where("status = ?", model.PostStatusPublished) } + if channelID != nil && *channelID != "" { + query = query.Where("channel_id = ?", *channelID) + } // 使用游标构建器 builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc). diff --git a/internal/router/router.go b/internal/router/router.go index 17ef342..6fe80a7 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -23,6 +23,7 @@ type Router struct { groupHandler *handler.GroupHandler stickerHandler *handler.StickerHandler voteHandler *handler.VoteHandler + channelHandler *handler.ChannelHandler scheduleHandler *handler.ScheduleHandler roleHandler *handler.RoleHandler adminUserHandler *handler.AdminUserHandler @@ -51,6 +52,7 @@ func New( groupHandler *handler.GroupHandler, stickerHandler *handler.StickerHandler, voteHandler *handler.VoteHandler, + channelHandler *handler.ChannelHandler, scheduleHandler *handler.ScheduleHandler, roleHandler *handler.RoleHandler, adminUserHandler *handler.AdminUserHandler, @@ -82,6 +84,7 @@ func New( groupHandler: groupHandler, stickerHandler: stickerHandler, voteHandler: voteHandler, + channelHandler: channelHandler, scheduleHandler: scheduleHandler, roleHandler: roleHandler, adminUserHandler: adminUserHandler, @@ -238,6 +241,14 @@ func (r *Router) setupRoutes() { posts.DELETE("/:id/vote", authMiddleware, r.voteHandler.Unvote) // 取消投票 } + // 频道路由(公开) + if r.channelHandler != nil { + channels := v1.Group("/channels") + { + channels.GET("", r.channelHandler.ListPublic) + } + } + // 课表路由 if r.scheduleHandler != nil { schedule := v1.Group("/schedule") @@ -449,6 +460,14 @@ func (r *Router) setupRoutes() { admin.PUT("/posts/:id/feature", r.adminPostHandler.SetPostFeature) } + // 频道管理 + if r.channelHandler != nil { + admin.GET("/channels", r.channelHandler.AdminList) + admin.POST("/channels", r.channelHandler.AdminCreate) + admin.PUT("/channels/:id", r.channelHandler.AdminUpdate) + admin.DELETE("/channels/:id", r.channelHandler.AdminDelete) + } + // 评论管理 if r.adminCommentHandler != nil { admin.GET("/comments", r.adminCommentHandler.GetCommentList) diff --git a/internal/service/admin_post_service.go b/internal/service/admin_post_service.go index 109f4e1..f5e39e8 100644 --- a/internal/service/admin_post_service.go +++ b/internal/service/admin_post_service.go @@ -322,7 +322,7 @@ func convertPostToAdminDetailResponse(post *model.Post) *dto.AdminPostDetailResp return &dto.AdminPostDetailResponse{ ID: post.ID, UserID: post.UserID, - CommunityID: post.CommunityID, + ChannelID: post.ChannelID, Title: post.Title, Content: post.Content, Images: images, diff --git a/internal/service/channel_service.go b/internal/service/channel_service.go new file mode 100644 index 0000000..3de3c03 --- /dev/null +++ b/internal/service/channel_service.go @@ -0,0 +1,65 @@ +package service + +import ( + "carrot_bbs/internal/model" + "carrot_bbs/internal/repository" +) + +type ChannelService interface { + ListActive() ([]*model.Channel, error) + ListAll() ([]*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 +} + +type channelServiceImpl struct { + channelRepo *repository.ChannelRepository +} + +func NewChannelService(channelRepo *repository.ChannelRepository) ChannelService { + return &channelServiceImpl{channelRepo: channelRepo} +} + +func (s *channelServiceImpl) ListActive() ([]*model.Channel, error) { + return s.channelRepo.ListActive() +} + +func (s *channelServiceImpl) ListAll() ([]*model.Channel, error) { + return s.channelRepo.ListAll() +} + +func (s *channelServiceImpl) Create(name, slug, description string, sortOrder int, isActive bool) (*model.Channel, error) { + channel := &model.Channel{ + Name: name, + Slug: slug, + Description: description, + SortOrder: sortOrder, + IsActive: isActive, + } + if err := s.channelRepo.Create(channel); err != nil { + return nil, err + } + return channel, nil +} + +func (s *channelServiceImpl) Update(id, name, slug, description string, sortOrder int, isActive bool) (*model.Channel, error) { + channel, err := s.channelRepo.GetByID(id) + if err != nil { + return nil, err + } + channel.Name = name + channel.Slug = slug + channel.Description = description + channel.SortOrder = sortOrder + channel.IsActive = isActive + if err := s.channelRepo.Update(channel); err != nil { + return nil, err + } + return channel, nil +} + +func (s *channelServiceImpl) Delete(id string) error { + return s.channelRepo.Delete(id) +} + diff --git a/internal/service/post_service.go b/internal/service/post_service.go index be121f8..e334b32 100644 --- a/internal/service/post_service.go +++ b/internal/service/post_service.go @@ -25,22 +25,22 @@ const ( // PostService 帖子服务接口 type PostService interface { // 帖子CRUD - Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) + Create(ctx context.Context, userID, title, content string, images []string, channelID *string) (*model.Post, error) GetByID(ctx context.Context, id string) (*model.Post, error) Update(ctx context.Context, post *model.Post) error UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error Delete(ctx context.Context, id string) error // 帖子列表 - List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) - GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) - GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) + List(ctx context.Context, page, pageSize int, userID string, includePending bool, channelID *string) ([]*model.Post, int64, error) + GetLatestPosts(ctx context.Context, page, pageSize int, userID string, channelID *string) ([]*model.Post, int64, error) + GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string, channelID *string) ([]*model.Post, int64, error) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) // 游标分页方法 - ListByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) + ListByCursor(ctx context.Context, userID string, includePending bool, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) SearchByCursor(ctx context.Context, keyword string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) GetUserPostsByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) GetFollowingPostsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) @@ -106,12 +106,13 @@ type PostListResult struct { } // Create 创建帖子 -func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) { +func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, images []string, channelID *string) (*model.Post, error) { post := &model.Post{ - UserID: userID, - Title: title, - Content: content, - Status: model.PostStatusPending, + UserID: userID, + ChannelID: channelID, + Title: title, + Content: content, + Status: model.PostStatusPending, } err := s.postRepo.Create(post, images) @@ -288,7 +289,7 @@ func (s *postServiceImpl) Delete(ctx context.Context, id string) error { } // List 获取帖子列表(带缓存) -func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) { +func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID string, includePending bool, channelID *string) ([]*model.Post, int64, error) { cacheSettings := cache.GetSettings() postListTTL := cacheSettings.PostListTTL if postListTTL <= 0 { @@ -308,6 +309,9 @@ func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID s if includePending && userID != "" { visibilityUserKey = "owner:" + userID } + if channelID != nil && *channelID != "" { + visibilityUserKey += ":channel:" + *channelID + } cacheKey := cache.PostListKey("latest", visibilityUserKey, page, pageSize) result, err := cache.GetOrLoadTyped( @@ -317,7 +321,7 @@ func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID s jitter, nullTTL, func() (*PostListResult, error) { - posts, total, err := s.postRepo.List(page, pageSize, userID, includePending) + posts, total, err := s.postRepo.List(page, pageSize, userID, includePending, channelID) if err != nil { return nil, err } @@ -341,7 +345,7 @@ func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID s } } if missingAuthor { - posts, total, loadErr := s.postRepo.List(page, pageSize, userID, includePending) + posts, total, loadErr := s.postRepo.List(page, pageSize, userID, includePending, channelID) if loadErr != nil { return nil, 0, loadErr } @@ -353,13 +357,13 @@ func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID s } // GetLatestPosts 获取最新帖子(语义化别名) -func (s *postServiceImpl) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) { - return s.List(ctx, page, pageSize, userID, false) +func (s *postServiceImpl) GetLatestPosts(ctx context.Context, page, pageSize int, userID string, channelID *string) ([]*model.Post, int64, error) { + return s.List(ctx, page, pageSize, userID, false, channelID) } // GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核) -func (s *postServiceImpl) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) { - return s.List(ctx, page, pageSize, userID, true) +func (s *postServiceImpl) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string, channelID *string) ([]*model.Post, int64, error) { + return s.List(ctx, page, pageSize, userID, true, channelID) } // GetUserPosts 获取用户帖子 @@ -647,11 +651,11 @@ func (s *postServiceImpl) DeletePostWithTransaction(ctx context.Context, postID // ========== 游标分页方法 ========== // ListByCursor 游标分页获取帖子列表 -func (s *postServiceImpl) ListByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) { +func (s *postServiceImpl) ListByCursor(ctx context.Context, userID string, includePending bool, channelID *string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) { // 规范化请求参数 req.Normalize() - return s.postRepo.GetPostsByCursor(ctx, userID, includePending, req) + return s.postRepo.GetPostsByCursor(ctx, userID, includePending, channelID, req) } // SearchByCursor 游标分页搜索帖子 diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go index 6ed0106..6b360ca 100644 --- a/internal/service/vote_service.go +++ b/internal/service/vote_service.go @@ -52,12 +52,12 @@ func (s *VoteService) CreateVotePost(ctx context.Context, userID string, req *dt // 创建普通帖子(设置IsVote=true) post := &model.Post{ - UserID: userID, - CommunityID: req.CommunityID, - Title: req.Title, - Content: req.Content, - Status: model.PostStatusPending, - IsVote: true, + UserID: userID, + ChannelID: req.ChannelID, + Title: req.Title, + Content: req.Content, + Status: model.PostStatusPending, + IsVote: true, } err := s.postRepo.Create(post, req.Images) diff --git a/internal/wire/handler.go b/internal/wire/handler.go index a50d03e..c074b3a 100644 --- a/internal/wire/handler.go +++ b/internal/wire/handler.go @@ -19,6 +19,7 @@ var HandlerSet = wire.NewSet( handler.NewPushHandler, handler.NewStickerHandler, handler.NewVoteHandler, + handler.NewChannelHandler, handler.NewRoleHandler, handler.NewAdminUserHandler, handler.NewAdminPostHandler, diff --git a/internal/wire/repository.go b/internal/wire/repository.go index ab0e601..9683368 100644 --- a/internal/wire/repository.go +++ b/internal/wire/repository.go @@ -21,6 +21,7 @@ var RepositorySet = wire.NewSet( repository.NewGroupRepository, repository.NewStickerRepository, repository.NewVoteRepository, + repository.NewChannelRepository, repository.NewScheduleRepository, ProvideUserActivityRepository, diff --git a/internal/wire/service.go b/internal/wire/service.go index 8de4473..0c71b30 100644 --- a/internal/wire/service.go +++ b/internal/wire/service.go @@ -39,6 +39,7 @@ var ServiceSet = wire.NewSet( ProvideUserService, ProvideStickerService, ProvideVoteService, + ProvideChannelService, ProvideChatService, ProvideScheduleService, ProvideScheduleSyncService, @@ -172,6 +173,10 @@ func ProvideVoteService( return service.NewVoteService(voteRepo, postRepo, cache, postAIService, systemMessageService) } +func ProvideChannelService(channelRepo *repository.ChannelRepository) service.ChannelService { + return service.NewChannelService(channelRepo) +} + // ProvideChatService 提供聊天服务 // Note: sensitiveService 传 nil,与 main.go 保持一致 func ProvideChatService(