837 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			837 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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"
 | |
| 	task_entities "tyapi-server/internal/infrastructure/task/entities"
 | |
| 	task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
 | |
| 	shared_interfaces "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
 | |
| 	taskManager      task_interfaces.TaskManager
 | |
| 	logger           *zap.Logger
 | |
| }
 | |
| 
 | |
| // NewArticleApplicationService 创建文章应用服务
 | |
| func NewArticleApplicationService(
 | |
| 	articleRepo repositories.ArticleRepository,
 | |
| 	categoryRepo repositories.CategoryRepository,
 | |
| 	tagRepo repositories.TagRepository,
 | |
| 	articleService *services.ArticleService,
 | |
| 	taskManager task_interfaces.TaskManager,
 | |
| 	logger *zap.Logger,
 | |
| ) ArticleApplicationService {
 | |
| 	return &ArticleApplicationServiceImpl{
 | |
| 		articleRepo:    articleRepo,
 | |
| 		categoryRepo:   categoryRepo,
 | |
| 		tagRepo:        tagRepo,
 | |
| 		articleService: articleService,
 | |
| 		taskManager:    taskManager,
 | |
| 		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. 处理标签关联
 | |
| 	// 先清除现有标签
 | |
| 	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 !article.IsScheduled() {
 | |
| 		s.logger.Info("文章定时发布已取消,跳过执行",
 | |
| 			zap.String("id", articleID),
 | |
| 			zap.String("status", string(article.Status)))
 | |
| 		return nil // 静默返回,不报错
 | |
| 	}
 | |
| 
 | |
| 	// 3. 检查定时发布时间是否匹配
 | |
| 	if article.ScheduledAt == nil {
 | |
| 		s.logger.Info("文章没有定时发布时间,跳过执行",
 | |
| 			zap.String("id", articleID))
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// 4. 发布文章
 | |
| 	if err := article.Publish(); 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)
 | |
| 	}
 | |
| 
 | |
| 	s.logger.Info("定时发布文章成功", zap.String("id", article.ID))
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // SchedulePublishArticle 定时发布文章
 | |
| func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
 | |
| 	// 1. 解析定时发布时间
 | |
| 	scheduledTime, err := cmd.GetScheduledTime()
 | |
| 	if err != nil {
 | |
| 		s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
 | |
| 		return fmt.Errorf("定时发布时间格式错误: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// 2. 获取文章
 | |
| 	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)
 | |
| 	}
 | |
| 
 | |
| 	// 3. 取消旧任务
 | |
| 	if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
 | |
| 		s.logger.Warn("取消旧任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
 | |
| 	}
 | |
| 
 | |
| 	// 4. 创建任务工厂
 | |
| 	taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
 | |
| 
 | |
| 	// 5. 创建并异步入队文章发布任务
 | |
| 	if err := taskFactory.CreateAndEnqueueArticlePublishTask(
 | |
| 		ctx,
 | |
| 		cmd.ID,
 | |
| 		scheduledTime,
 | |
| 		"system", // 暂时使用系统用户ID
 | |
| 	); err != nil {
 | |
| 		s.logger.Error("创建并入队文章发布任务失败", zap.Error(err))
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// 6. 设置定时发布
 | |
| 	if err := article.SchedulePublish(scheduledTime); err != nil {
 | |
| 		return fmt.Errorf("设置定时发布失败: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// 7. 保存更新
 | |
| 	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.Time("scheduled_time", scheduledTime))
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // CancelSchedulePublishArticle 取消定时发布文章
 | |
| func (s *ArticleApplicationServiceImpl) CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) 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.IsScheduled() {
 | |
| 		return fmt.Errorf("文章未设置定时发布")
 | |
| 	}
 | |
| 
 | |
| 	// 3. 取消定时任务
 | |
| 	if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
 | |
| 		s.logger.Warn("取消定时任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
 | |
| 		// 不返回错误,继续执行取消定时发布
 | |
| 	}
 | |
| 
 | |
| 	// 4. 取消定时发布
 | |
| 	if err := article.CancelSchedulePublish(); 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)
 | |
| 	}
 | |
| 
 | |
| 	s.logger.Info("取消定时发布成功", zap.String("id", article.ID))
 | |
| 	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, shared_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, shared_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
 | |
| }
 | |
| 
 | |
| // UpdateSchedulePublishArticle 修改定时发布时间
 | |
| func (s *ArticleApplicationServiceImpl) UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
 | |
| 	// 1. 解析定时发布时间
 | |
| 	scheduledTime, err := cmd.GetScheduledTime()
 | |
| 	if err != nil {
 | |
| 		s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
 | |
| 		return fmt.Errorf("定时发布时间格式错误: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// 2. 获取文章
 | |
| 	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)
 | |
| 	}
 | |
| 
 | |
| 	// 3. 检查是否已设置定时发布
 | |
| 	if !article.IsScheduled() {
 | |
| 		return fmt.Errorf("文章未设置定时发布,无法修改时间")
 | |
| 	}
 | |
| 
 | |
| 	// 4. 更新数据库中的任务调度时间
 | |
| 	if err := s.taskManager.UpdateTaskSchedule(ctx, cmd.ID, scheduledTime); err != nil {
 | |
| 		s.logger.Error("更新任务调度时间失败", zap.String("id", cmd.ID), zap.Error(err))
 | |
| 		return fmt.Errorf("修改定时发布时间失败: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// 5. 更新定时发布
 | |
| 	if err := article.UpdateSchedulePublish(scheduledTime); err != nil {
 | |
| 		return fmt.Errorf("更新定时发布失败: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// 6. 保存更新
 | |
| 	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.Time("new_scheduled_time", scheduledTime))
 | |
| 	return 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
 | |
| }
 |