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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
|
||||
123
internal/handler/channel_handler.go
Normal file
123
internal/handler/channel_handler.go
Normal file
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
32
internal/model/channel.go
Normal file
32
internal/model/channel.go
Normal file
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
|
||||
54
internal/repository/channel_repo.go
Normal file
54
internal/repository/channel_repo.go
Normal file
@@ -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
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
65
internal/service/channel_service.go
Normal file
65
internal/service/channel_service.go
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 游标分页搜索帖子
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -19,6 +19,7 @@ var HandlerSet = wire.NewSet(
|
||||
handler.NewPushHandler,
|
||||
handler.NewStickerHandler,
|
||||
handler.NewVoteHandler,
|
||||
handler.NewChannelHandler,
|
||||
handler.NewRoleHandler,
|
||||
handler.NewAdminUserHandler,
|
||||
handler.NewAdminPostHandler,
|
||||
|
||||
@@ -21,6 +21,7 @@ var RepositorySet = wire.NewSet(
|
||||
repository.NewGroupRepository,
|
||||
repository.NewStickerRepository,
|
||||
repository.NewVoteRepository,
|
||||
repository.NewChannelRepository,
|
||||
repository.NewScheduleRepository,
|
||||
ProvideUserActivityRepository,
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user