diff --git a/internal/application/article/article_application_service.go b/internal/application/article/article_application_service.go index f622622..d2f3642 100644 --- a/internal/application/article/article_application_service.go +++ b/internal/application/article/article_application_service.go @@ -15,6 +15,7 @@ type ArticleApplicationService interface { DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error) ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) + ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) // 文章状态管理 PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error diff --git a/internal/application/article/article_application_service_impl.go b/internal/application/article/article_application_service_impl.go index 92eb10f..68eb684 100644 --- a/internal/application/article/article_application_service_impl.go +++ b/internal/application/article/article_application_service_impl.go @@ -223,6 +223,43 @@ func (s *ArticleApplicationServiceImpl) ListArticles(ctx context.Context, query return response, nil } +// ListArticlesForAdmin 获取文章列表(管理员端) +func (s *ArticleApplicationServiceImpl) ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) { + // 1. 构建仓储查询 + repoQuery := &repoQueries.ListArticleQuery{ + Page: query.Page, + PageSize: query.PageSize, + Status: query.Status, + CategoryID: query.CategoryID, + TagID: query.TagID, + Title: query.Title, + Summary: query.Summary, + IsFeatured: query.IsFeatured, + OrderBy: query.OrderBy, + OrderDir: query.OrderDir, + } + + // 2. 调用仓储 + articles, total, err := s.articleRepo.ListArticlesForAdmin(ctx, repoQuery) + if err != nil { + s.logger.Error("获取文章列表失败", zap.Error(err)) + return nil, fmt.Errorf("获取文章列表失败: %w", err) + } + + // 3. 转换为响应对象 + items := responses.FromArticleEntitiesToListItemList(articles) + + response := &responses.ArticleListResponse{ + Total: total, + Page: query.Page, + Size: query.PageSize, + Items: items, + } + + s.logger.Info("获取文章列表成功", zap.Int64("total", total)) + return response, nil +} + // PublishArticle 发布文章 func (s *ArticleApplicationServiceImpl) PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error { diff --git a/internal/domains/article/repositories/article_repository_interface.go b/internal/domains/article/repositories/article_repository_interface.go index b2c4e71..9f339ef 100644 --- a/internal/domains/article/repositories/article_repository_interface.go +++ b/internal/domains/article/repositories/article_repository_interface.go @@ -18,6 +18,7 @@ type ArticleRepository interface { FindFeatured(ctx context.Context) ([]*entities.Article, error) Search(ctx context.Context, query *queries.SearchArticleQuery) ([]*entities.Article, int64, error) ListArticles(ctx context.Context, query *queries.ListArticleQuery) ([]*entities.Article, int64, error) + ListArticlesForAdmin(ctx context.Context, query *queries.ListArticleQuery) ([]*entities.Article, int64, error) // 统计方法 CountByCategoryID(ctx context.Context, categoryID string) (int64, error) diff --git a/internal/infrastructure/database/repositories/article/gorm_article_repository.go b/internal/infrastructure/database/repositories/article/gorm_article_repository.go index fc5ab52..6de38c6 100644 --- a/internal/infrastructure/database/repositories/article/gorm_article_repository.go +++ b/internal/infrastructure/database/repositories/article/gorm_article_repository.go @@ -263,11 +263,100 @@ func (r *GormArticleRepository) Search(ctx context.Context, query *repoQueries.S return result, total, nil } -// ListArticles 获取文章列表 +// ListArticles 获取文章列表(用户端) func (r *GormArticleRepository) ListArticles(ctx context.Context, query *repoQueries.ListArticleQuery) ([]*entities.Article, int64, error) { var articles []entities.Article var total int64 + dbQuery := r.db.WithContext(ctx).Model(&entities.Article{}). + Select("id, title, summary, cover_image, category_id, status, is_featured, published_at, created_at, updated_at, scheduled_at") + + // 用户端不显示归档文章 + dbQuery = dbQuery.Where("status != ?", entities.ArticleStatusArchived) + + // 应用筛选条件 + if query.Status != "" { + dbQuery = dbQuery.Where("status = ?", query.Status) + } + + if query.CategoryID != "" { + // 如果指定了分类ID,只查询该分类的文章(包括没有分类的文章,当CategoryID为空字符串时) + if query.CategoryID == "null" || query.CategoryID == "" { + // 查询没有分类的文章 + dbQuery = dbQuery.Where("category_id IS NULL OR category_id = ''") + } else { + // 查询指定分类的文章 + dbQuery = dbQuery.Where("category_id = ?", query.CategoryID) + } + } + + if query.TagID != "" { + // 如果指定了标签ID,只查询有关联该标签的文章 + // 使用子查询而不是JOIN,避免影响其他查询条件 + subQuery := r.db.WithContext(ctx).Table("article_tag_relations"). + Select("article_id"). + Where("tag_id = ?", query.TagID) + dbQuery = dbQuery.Where("id IN (?)", subQuery) + } + + if query.Title != "" { + dbQuery = dbQuery.Where("title ILIKE ?", "%"+query.Title+"%") + } + + if query.Summary != "" { + dbQuery = dbQuery.Where("summary ILIKE ?", "%"+query.Summary+"%") + } + + if query.IsFeatured != nil { + dbQuery = dbQuery.Where("is_featured = ?", *query.IsFeatured) + } + + // 获取总数 + if err := dbQuery.Count(&total).Error; err != nil { + r.logger.Error("获取文章列表总数失败", zap.Error(err)) + return nil, 0, err + } + + // 应用排序 + if query.OrderBy != "" { + orderDir := "DESC" + if query.OrderDir != "" { + orderDir = strings.ToUpper(query.OrderDir) + } + dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", query.OrderBy, orderDir)) + } else { + dbQuery = dbQuery.Order("created_at DESC") + } + + // 应用分页 + if query.Page > 0 && query.PageSize > 0 { + offset := (query.Page - 1) * query.PageSize + dbQuery = dbQuery.Offset(offset).Limit(query.PageSize) + } + + // 预加载关联数据 + dbQuery = dbQuery.Preload("Category").Preload("Tags") + + // 获取数据 + if err := dbQuery.Find(&articles).Error; err != nil { + r.logger.Error("获取文章列表失败", zap.Error(err)) + return nil, 0, err + } + + // 转换为指针切片 + result := make([]*entities.Article, len(articles)) + for i := range articles { + result[i] = &articles[i] + } + + return result, total, nil +} + +// ListArticlesForAdmin 获取文章列表(管理员端) +func (r *GormArticleRepository) ListArticlesForAdmin(ctx context.Context, query *repoQueries.ListArticleQuery) ([]*entities.Article, int64, error) { + var articles []entities.Article + var total int64 + dbQuery := r.db.WithContext(ctx).Model(&entities.Article{}). Select("id, title, summary, cover_image, category_id, status, is_featured, published_at, view_count, created_at, updated_at, scheduled_at") diff --git a/internal/infrastructure/http/handlers/article_handler.go b/internal/infrastructure/http/handlers/article_handler.go index ec69ad1..c290319 100644 --- a/internal/infrastructure/http/handlers/article_handler.go +++ b/internal/infrastructure/http/handlers/article_handler.go @@ -146,6 +146,55 @@ func (h *ArticleHandler) ListArticles(c *gin.Context) { h.responseBuilder.Success(c, response, "获取文章列表成功") } +// ListArticlesForAdmin 获取文章列表(管理员端) +// @Summary 获取文章列表(管理员端) +// @Description 分页获取文章列表,支持多种筛选条件,包含所有状态的文章 +// @Tags 文章管理-管理端 +// @Accept json +// @Produce json +// @Security Bearer +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(10) +// @Param status query string false "文章状态" +// @Param category_id query string false "分类ID" +// @Param tag_id query string false "标签ID" +// @Param title query string false "标题关键词" +// @Param summary query string false "摘要关键词" +// @Param is_featured query bool false "是否推荐" +// @Param order_by query string false "排序字段" +// @Param order_dir query string false "排序方向" +// @Success 200 {object} responses.ArticleListResponse "获取文章列表成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/admin/articles [get] +func (h *ArticleHandler) ListArticlesForAdmin(c *gin.Context) { + var query appQueries.ListArticleQuery + if err := h.validator.ValidateQuery(c, &query); err != nil { + return + } + + // 设置默认值 + if query.Page <= 0 { + query.Page = 1 + } + if query.PageSize <= 0 { + query.PageSize = 10 + } + if query.PageSize > 100 { + query.PageSize = 100 + } + + response, err := h.appService.ListArticlesForAdmin(c.Request.Context(), &query) + if err != nil { + h.logger.Error("获取文章列表失败", zap.Error(err)) + h.responseBuilder.InternalError(c, "获取文章列表失败") + return + } + + h.responseBuilder.Success(c, response, "获取文章列表成功") +} + // UpdateArticle 更新文章 diff --git a/internal/infrastructure/http/routes/article_routes.go b/internal/infrastructure/http/routes/article_routes.go index 1e7e6cc..4cb1605 100644 --- a/internal/infrastructure/http/routes/article_routes.go +++ b/internal/infrastructure/http/routes/article_routes.go @@ -68,6 +68,9 @@ func (r *ArticleRoutes) Register(router *sharedhttp.GinRouter) { // 统计信息 adminArticleGroup.GET("/stats", r.handler.GetArticleStats) // 获取文章统计 + // 文章列表查询 + adminArticleGroup.GET("", r.handler.ListArticlesForAdmin) // 获取文章列表(管理员端,包含所有状态) + // 文章管理 adminArticleGroup.POST("", r.handler.CreateArticle) // 创建文章 adminArticleGroup.PUT("/:id", r.handler.UpdateArticle) // 更新文章