package article import ( "context" "fmt" "tyapi-server/internal/application/article/dto/commands" appQueries "tyapi-server/internal/application/article/dto/queries" "tyapi-server/internal/application/article/dto/responses" "tyapi-server/internal/domains/article/entities" "tyapi-server/internal/domains/article/repositories" repoQueries "tyapi-server/internal/domains/article/repositories/queries" "tyapi-server/internal/domains/article/services" "tyapi-server/internal/infrastructure/task" "tyapi-server/internal/shared/interfaces" "go.uber.org/zap" ) // ArticleApplicationServiceImpl 文章应用服务实现 type ArticleApplicationServiceImpl struct { articleRepo repositories.ArticleRepository categoryRepo repositories.CategoryRepository tagRepo repositories.TagRepository articleService *services.ArticleService asynqClient *task.AsynqClient logger *zap.Logger } // NewArticleApplicationService 创建文章应用服务 func NewArticleApplicationService( articleRepo repositories.ArticleRepository, categoryRepo repositories.CategoryRepository, tagRepo repositories.TagRepository, articleService *services.ArticleService, asynqClient *task.AsynqClient, logger *zap.Logger, ) ArticleApplicationService { return &ArticleApplicationServiceImpl{ articleRepo: articleRepo, categoryRepo: categoryRepo, tagRepo: tagRepo, articleService: articleService, asynqClient: asynqClient, logger: logger, } } // CreateArticle 创建文章 func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error { // 1. 参数验证 if err := s.validateCreateArticle(cmd); err != nil { return fmt.Errorf("参数验证失败: %w", err) } // 2. 创建文章实体 article := &entities.Article{ Title: cmd.Title, Content: cmd.Content, Summary: cmd.Summary, CoverImage: cmd.CoverImage, CategoryID: cmd.CategoryID, IsFeatured: cmd.IsFeatured, Status: entities.ArticleStatusDraft, } // 3. 调用领域服务验证 if err := s.articleService.ValidateArticle(article); err != nil { return fmt.Errorf("业务验证失败: %w", err) } // 4. 保存文章 _, err := s.articleRepo.Create(ctx, *article) if err != nil { s.logger.Error("创建文章失败", zap.Error(err)) return fmt.Errorf("创建文章失败: %w", err) } // 5. 处理标签关联 if len(cmd.TagIDs) > 0 { for _, tagID := range cmd.TagIDs { if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil { s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err)) } } } s.logger.Info("创建文章成功", zap.String("id", article.ID), zap.String("title", article.Title)) return nil } // UpdateArticle 更新文章 func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error { // 1. 获取原文章 article, err := s.articleRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("文章不存在: %w", err) } // 2. 检查是否可以编辑 if !article.CanEdit() { return fmt.Errorf("文章状态不允许编辑") } // 3. 更新字段 if cmd.Title != "" { article.Title = cmd.Title } if cmd.Content != "" { article.Content = cmd.Content } if cmd.Summary != "" { article.Summary = cmd.Summary } if cmd.CoverImage != "" { article.CoverImage = cmd.CoverImage } if cmd.CategoryID != "" { article.CategoryID = cmd.CategoryID } article.IsFeatured = cmd.IsFeatured // 4. 验证更新后的文章 if err := s.articleService.ValidateArticle(&article); err != nil { return fmt.Errorf("业务验证失败: %w", err) } // 5. 保存更新 if err := s.articleRepo.Update(ctx, article); err != nil { s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err)) return fmt.Errorf("更新文章失败: %w", err) } // 6. 处理标签关联 if len(cmd.TagIDs) > 0 { // 先清除现有标签 existingTags, _ := s.tagRepo.GetArticleTags(ctx, article.ID) for _, tag := range existingTags { s.tagRepo.RemoveTagFromArticle(ctx, article.ID, tag.ID) } // 添加新标签 for _, tagID := range cmd.TagIDs { if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil { s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err)) } } } s.logger.Info("更新文章成功", zap.String("id", article.ID)) return nil } // DeleteArticle 删除文章 func (s *ArticleApplicationServiceImpl) DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error { // 1. 检查文章是否存在 _, err := s.articleRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("文章不存在: %w", err) } // 2. 删除文章 if err := s.articleRepo.Delete(ctx, cmd.ID); err != nil { s.logger.Error("删除文章失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("删除文章失败: %w", err) } s.logger.Info("删除文章成功", zap.String("id", cmd.ID)) return nil } // GetArticleByID 根据ID获取文章 func (s *ArticleApplicationServiceImpl) GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error) { // 1. 获取文章 article, err := s.articleRepo.GetByID(ctx, query.ID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", query.ID), zap.Error(err)) return nil, fmt.Errorf("文章不存在: %w", err) } // 2. 转换为响应对象 response := responses.FromArticleEntity(&article) s.logger.Info("获取文章成功", zap.String("id", article.ID)) return response, nil } // ListArticles 获取文章列表 func (s *ArticleApplicationServiceImpl) ListArticles(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.ListArticles(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 } // 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 { // 1. 获取文章 article, err := s.articleRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("文章不存在: %w", err) } // 2. 发布文章 if err := article.Publish(); err != nil { return fmt.Errorf("发布文章失败: %w", err) } // 3. 保存更新 if err := s.articleRepo.Update(ctx, article); err != nil { s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err)) return fmt.Errorf("发布文章失败: %w", err) } s.logger.Info("发布文章成功", zap.String("id", article.ID)) return nil } // PublishArticleByID 通过ID发布文章 (用于定时任务) func (s *ArticleApplicationServiceImpl) PublishArticleByID(ctx context.Context, articleID string) error { // 1. 获取文章 article, err := s.articleRepo.GetByID(ctx, articleID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", articleID), zap.Error(err)) return fmt.Errorf("文章不存在: %w", err) } // 2. 发布文章 if err := article.Publish(); err != nil { return fmt.Errorf("发布文章失败: %w", err) } // 3. 保存更新 if err := s.articleRepo.Update(ctx, article); err != nil { s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err)) return fmt.Errorf("发布文章失败: %w", err) } s.logger.Info("定时发布文章成功", zap.String("id", article.ID)) return nil } // SchedulePublishArticle 定时发布文章 func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error { // 1. 获取文章 article, err := s.articleRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("文章不存在: %w", err) } // 2. 设置定时发布 if err := article.SchedulePublish(cmd.ScheduledTime); err != nil { return fmt.Errorf("设置定时发布失败: %w", err) } // 3. 保存更新 if err := s.articleRepo.Update(ctx, article); err != nil { s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err)) return fmt.Errorf("设置定时发布失败: %w", err) } // 4. 调度定时发布任务 if err := s.asynqClient.ScheduleArticlePublish(ctx, cmd.ID, cmd.ScheduledTime); err != nil { s.logger.Error("调度定时发布任务失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("调度定时发布任务失败: %w", err) } s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", cmd.ScheduledTime)) return nil } // ArchiveArticle 归档文章 func (s *ArticleApplicationServiceImpl) ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error { // 1. 获取文章 article, err := s.articleRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("文章不存在: %w", err) } // 2. 归档文章 if err := article.Archive(); err != nil { return fmt.Errorf("归档文章失败: %w", err) } // 3. 保存更新 if err := s.articleRepo.Update(ctx, article); err != nil { s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err)) return fmt.Errorf("归档文章失败: %w", err) } s.logger.Info("归档文章成功", zap.String("id", article.ID)) return nil } // SetFeatured 设置推荐状态 func (s *ArticleApplicationServiceImpl) SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error { // 1. 获取文章 article, err := s.articleRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("文章不存在: %w", err) } // 2. 设置推荐状态 article.SetFeatured(cmd.IsFeatured) // 3. 保存更新 if err := s.articleRepo.Update(ctx, article); err != nil { s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err)) return fmt.Errorf("设置推荐状态失败: %w", err) } s.logger.Info("设置推荐状态成功", zap.String("id", article.ID), zap.Bool("is_featured", cmd.IsFeatured)) return nil } // RecordView 记录阅读 func (s *ArticleApplicationServiceImpl) RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error { // 1. 增加阅读量 if err := s.articleRepo.IncrementViewCount(ctx, articleID); err != nil { s.logger.Error("增加阅读量失败", zap.String("id", articleID), zap.Error(err)) return fmt.Errorf("记录阅读失败: %w", err) } s.logger.Info("记录阅读成功", zap.String("id", articleID)) return nil } // GetArticleStats 获取文章统计 func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error) { // 1. 获取各种统计 totalArticles, err := s.articleRepo.CountByStatus(ctx, "") if err != nil { s.logger.Error("获取文章总数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } publishedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusPublished) if err != nil { s.logger.Error("获取已发布文章数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } draftArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusDraft) if err != nil { s.logger.Error("获取草稿文章数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } archivedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusArchived) if err != nil { s.logger.Error("获取归档文章数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } response := &responses.ArticleStatsResponse{ TotalArticles: totalArticles, PublishedArticles: publishedArticles, DraftArticles: draftArticles, ArchivedArticles: archivedArticles, TotalViews: 0, // TODO: 实现总阅读量统计 } s.logger.Info("获取文章统计成功") return response, nil } // validateCreateArticle 验证创建文章参数 func (s *ArticleApplicationServiceImpl) validateCreateArticle(cmd *commands.CreateArticleCommand) error { if cmd.Title == "" { return fmt.Errorf("文章标题不能为空") } if cmd.Content == "" { return fmt.Errorf("文章内容不能为空") } return nil } // ==================== 分类相关方法 ==================== // CreateCategory 创建分类 func (s *ArticleApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error { // 1. 参数验证 if err := s.validateCreateCategory(cmd); err != nil { return fmt.Errorf("参数验证失败: %w", err) } // 2. 创建分类实体 category := &entities.Category{ Name: cmd.Name, Description: cmd.Description, } // 3. 保存分类 _, err := s.categoryRepo.Create(ctx, *category) if err != nil { s.logger.Error("创建分类失败", zap.Error(err)) return fmt.Errorf("创建分类失败: %w", err) } s.logger.Info("创建分类成功", zap.String("id", category.ID), zap.String("name", category.Name)) return nil } // UpdateCategory 更新分类 func (s *ArticleApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error { // 1. 获取原分类 category, err := s.categoryRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("分类不存在: %w", err) } // 2. 更新字段 category.Name = cmd.Name category.Description = cmd.Description // 3. 保存更新 if err := s.categoryRepo.Update(ctx, category); err != nil { s.logger.Error("更新分类失败", zap.String("id", category.ID), zap.Error(err)) return fmt.Errorf("更新分类失败: %w", err) } s.logger.Info("更新分类成功", zap.String("id", category.ID)) return nil } // DeleteCategory 删除分类 func (s *ArticleApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error { // 1. 检查分类是否存在 category, err := s.categoryRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("分类不存在: %w", err) } // 2. 检查是否有文章使用此分类 count, err := s.articleRepo.CountByCategoryID(ctx, cmd.ID) if err != nil { s.logger.Error("检查分类使用情况失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("删除分类失败: %w", err) } if count > 0 { return fmt.Errorf("该分类下还有 %d 篇文章,无法删除", count) } // 3. 删除分类 if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil { s.logger.Error("删除分类失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("删除分类失败: %w", err) } s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("name", category.Name)) return nil } // GetCategoryByID 获取分类详情 func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) { // 1. 获取分类 category, err := s.categoryRepo.GetByID(ctx, query.ID) if err != nil { s.logger.Error("获取分类失败", zap.String("id", query.ID), zap.Error(err)) return nil, fmt.Errorf("分类不存在: %w", err) } // 2. 转换为响应对象 response := &responses.CategoryInfoResponse{ ID: category.ID, Name: category.Name, Description: category.Description, SortOrder: category.SortOrder, CreatedAt: category.CreatedAt, } return response, nil } // ListCategories 获取分类列表 func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*responses.CategoryListResponse, error) { // 1. 获取分类列表 categories, err := s.categoryRepo.List(ctx, interfaces.ListOptions{}) if err != nil { s.logger.Error("获取分类列表失败", zap.Error(err)) return nil, fmt.Errorf("获取分类列表失败: %w", err) } // 2. 转换为响应对象 items := make([]responses.CategoryInfoResponse, len(categories)) for i, category := range categories { items[i] = responses.CategoryInfoResponse{ ID: category.ID, Name: category.Name, Description: category.Description, SortOrder: category.SortOrder, CreatedAt: category.CreatedAt, } } response := &responses.CategoryListResponse{ Items: items, Total: len(items), } return response, nil } // ==================== 标签相关方法 ==================== // CreateTag 创建标签 func (s *ArticleApplicationServiceImpl) CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error { // 1. 参数验证 if err := s.validateCreateTag(cmd); err != nil { return fmt.Errorf("参数验证失败: %w", err) } // 2. 创建标签实体 tag := &entities.Tag{ Name: cmd.Name, Color: cmd.Color, } // 3. 保存标签 _, err := s.tagRepo.Create(ctx, *tag) if err != nil { s.logger.Error("创建标签失败", zap.Error(err)) return fmt.Errorf("创建标签失败: %w", err) } s.logger.Info("创建标签成功", zap.String("id", tag.ID), zap.String("name", tag.Name)) return nil } // UpdateTag 更新标签 func (s *ArticleApplicationServiceImpl) UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error { // 1. 获取原标签 tag, err := s.tagRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("标签不存在: %w", err) } // 2. 更新字段 tag.Name = cmd.Name tag.Color = cmd.Color // 3. 保存更新 if err := s.tagRepo.Update(ctx, tag); err != nil { s.logger.Error("更新标签失败", zap.String("id", tag.ID), zap.Error(err)) return fmt.Errorf("更新标签失败: %w", err) } s.logger.Info("更新标签成功", zap.String("id", tag.ID)) return nil } // DeleteTag 删除标签 func (s *ArticleApplicationServiceImpl) DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error { // 1. 检查标签是否存在 tag, err := s.tagRepo.GetByID(ctx, cmd.ID) if err != nil { s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("标签不存在: %w", err) } // 2. 删除标签 if err := s.tagRepo.Delete(ctx, cmd.ID); err != nil { s.logger.Error("删除标签失败", zap.String("id", cmd.ID), zap.Error(err)) return fmt.Errorf("删除标签失败: %w", err) } s.logger.Info("删除标签成功", zap.String("id", cmd.ID), zap.String("name", tag.Name)) return nil } // GetTagByID 获取标签详情 func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error) { // 1. 获取标签 tag, err := s.tagRepo.GetByID(ctx, query.ID) if err != nil { s.logger.Error("获取标签失败", zap.String("id", query.ID), zap.Error(err)) return nil, fmt.Errorf("标签不存在: %w", err) } // 2. 转换为响应对象 response := &responses.TagInfoResponse{ ID: tag.ID, Name: tag.Name, Color: tag.Color, CreatedAt: tag.CreatedAt, } return response, nil } // ListTags 获取标签列表 func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*responses.TagListResponse, error) { // 1. 获取标签列表 tags, err := s.tagRepo.List(ctx, interfaces.ListOptions{}) if err != nil { s.logger.Error("获取标签列表失败", zap.Error(err)) return nil, fmt.Errorf("获取标签列表失败: %w", err) } // 2. 转换为响应对象 items := make([]responses.TagInfoResponse, len(tags)) for i, tag := range tags { items[i] = responses.TagInfoResponse{ ID: tag.ID, Name: tag.Name, Color: tag.Color, CreatedAt: tag.CreatedAt, } } response := &responses.TagListResponse{ Items: items, Total: len(items), } return response, nil } // ==================== 验证方法 ==================== // validateCreateCategory 验证创建分类参数 func (s *ArticleApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error { if cmd.Name == "" { return fmt.Errorf("分类名称不能为空") } if len(cmd.Name) > 50 { return fmt.Errorf("分类名称长度不能超过50个字符") } if len(cmd.Description) > 200 { return fmt.Errorf("分类描述长度不能超过200个字符") } return nil } // validateCreateTag 验证创建标签参数 func (s *ArticleApplicationServiceImpl) validateCreateTag(cmd *commands.CreateTagCommand) error { if cmd.Name == "" { return fmt.Errorf("标签名称不能为空") } if len(cmd.Name) > 30 { return fmt.Errorf("标签名称长度不能超过30个字符") } if cmd.Color == "" { return fmt.Errorf("标签颜色不能为空") } // TODO: 添加十六进制颜色格式验证 return nil }