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" "go.uber.org/zap" ) // AnnouncementApplicationServiceImpl 公告应用服务实现 type AnnouncementApplicationServiceImpl struct { announcementRepo repositories.AnnouncementRepository announcementService *services.AnnouncementService taskManager task_interfaces.TaskManager logger *zap.Logger } // NewAnnouncementApplicationService 创建公告应用服务 func NewAnnouncementApplicationService( announcementRepo repositories.AnnouncementRepository, announcementService *services.AnnouncementService, taskManager task_interfaces.TaskManager, logger *zap.Logger, ) AnnouncementApplicationService { return &AnnouncementApplicationServiceImpl{ announcementRepo: announcementRepo, announcementService: announcementService, taskManager: taskManager, logger: logger, } } // CreateAnnouncement 创建公告 func (s *AnnouncementApplicationServiceImpl) CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error { // 1. 创建公告实体 announcement := &entities.Announcement{ Title: cmd.Title, Content: cmd.Content, Status: entities.AnnouncementStatusDraft, } // 2. 调用领域服务验证 if err := s.announcementService.ValidateAnnouncement(announcement); err != nil { return fmt.Errorf("业务验证失败: %w", err) } // 3. 保存公告 _, err := s.announcementRepo.Create(ctx, *announcement) if err != nil { s.logger.Error("创建公告失败", zap.Error(err)) return fmt.Errorf("创建公告失败: %w", err) } s.logger.Info("创建公告成功", zap.String("id", announcement.ID), zap.String("title", announcement.Title)) return nil } // UpdateAnnouncement 更新公告 func (s *AnnouncementApplicationServiceImpl) UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error { // 1. 获取原公告 announcement, err := s.announcementRepo.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.announcementService.CanEdit(&announcement); err != nil { return fmt.Errorf("公告状态不允许编辑: %w", err) } // 3. 更新字段 if cmd.Title != "" { announcement.Title = cmd.Title } if cmd.Content != "" { announcement.Content = cmd.Content } // 4. 验证更新后的公告 if err := s.announcementService.ValidateAnnouncement(&announcement); err != nil { return fmt.Errorf("业务验证失败: %w", err) } // 5. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("更新公告失败: %w", err) } s.logger.Info("更新公告成功", zap.String("id", announcement.ID)) return nil } // DeleteAnnouncement 删除公告 func (s *AnnouncementApplicationServiceImpl) DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error { // 1. 检查公告是否存在 _, err := s.announcementRepo.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.announcementRepo.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 } // GetAnnouncementByID 获取公告详情 func (s *AnnouncementApplicationServiceImpl) GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error) { // 1. 获取公告 announcement, err := s.announcementRepo.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.FromAnnouncementEntity(&announcement) return response, nil } // ListAnnouncements 获取公告列表 func (s *AnnouncementApplicationServiceImpl) ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error) { // 1. 构建仓储查询 repoQuery := &repoQueries.ListAnnouncementQuery{ Page: query.Page, PageSize: query.PageSize, Status: query.Status, Title: query.Title, OrderBy: query.OrderBy, OrderDir: query.OrderDir, } // 2. 调用仓储 announcements, total, err := s.announcementRepo.ListAnnouncements(ctx, repoQuery) if err != nil { s.logger.Error("获取公告列表失败", zap.Error(err)) return nil, fmt.Errorf("获取公告列表失败: %w", err) } // 3. 转换为响应对象 items := responses.FromAnnouncementEntityList(announcements) response := &responses.AnnouncementListResponse{ Total: total, Page: query.Page, Size: query.PageSize, Items: items, } s.logger.Info("获取公告列表成功", zap.Int64("total", total)) return response, nil } // PublishAnnouncement 发布公告 func (s *AnnouncementApplicationServiceImpl) PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error { // 1. 获取公告 announcement, err := s.announcementRepo.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.announcementService.CanPublish(&announcement); err != nil { return fmt.Errorf("无法发布公告: %w", err) } // 3. 发布公告 if err := announcement.Publish(); err != nil { return fmt.Errorf("发布公告失败: %w", err) } // 4. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("发布公告失败: %w", err) } s.logger.Info("发布公告成功", zap.String("id", announcement.ID)) return nil } // PublishAnnouncementByID 通过ID发布公告 (用于定时任务) func (s *AnnouncementApplicationServiceImpl) PublishAnnouncementByID(ctx context.Context, announcementID string) error { // 1. 获取公告 announcement, err := s.announcementRepo.GetByID(ctx, announcementID) if err != nil { s.logger.Error("获取公告失败", zap.String("id", announcementID), zap.Error(err)) return fmt.Errorf("公告不存在: %w", err) } // 2. 检查是否已取消定时发布 if !announcement.IsScheduled() { s.logger.Info("公告定时发布已取消,跳过执行", zap.String("id", announcementID), zap.String("status", string(announcement.Status))) return nil // 静默返回,不报错 } // 3. 检查定时发布时间是否匹配 if announcement.ScheduledAt == nil { s.logger.Info("公告没有定时发布时间,跳过执行", zap.String("id", announcementID)) return nil } // 4. 发布公告 if err := announcement.Publish(); err != nil { return fmt.Errorf("发布公告失败: %w", err) } // 5. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("发布公告失败: %w", err) } s.logger.Info("定时发布公告成功", zap.String("id", announcement.ID)) return nil } // WithdrawAnnouncement 撤回公告 func (s *AnnouncementApplicationServiceImpl) WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error { // 1. 获取公告 announcement, err := s.announcementRepo.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.announcementService.CanWithdraw(&announcement); err != nil { return fmt.Errorf("无法撤回公告: %w", err) } // 3. 撤回公告 if err := announcement.Withdraw(); err != nil { return fmt.Errorf("撤回公告失败: %w", err) } // 4. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("撤回公告失败: %w", err) } s.logger.Info("撤回公告成功", zap.String("id", announcement.ID)) return nil } // ArchiveAnnouncement 归档公告 func (s *AnnouncementApplicationServiceImpl) ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error { // 1. 获取公告 announcement, err := s.announcementRepo.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.announcementService.CanArchive(&announcement); err != nil { return fmt.Errorf("无法归档公告: %w", err) } // 3. 归档公告 announcement.Status = entities.AnnouncementStatusArchived // 4. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("归档公告失败: %w", err) } s.logger.Info("归档公告成功", zap.String("id", announcement.ID)) return nil } // SchedulePublishAnnouncement 定时发布公告 func (s *AnnouncementApplicationServiceImpl) SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) 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. 获取公告 announcement, err := s.announcementRepo.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.announcementService.CanSchedulePublish(&announcement, scheduledTime); err != nil { return fmt.Errorf("无法设置定时发布: %w", err) } // 4. 取消旧任务(如果存在) if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil { s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err)) } // 5. 创建任务工厂 taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager) // 6. 创建并异步入队公告发布任务 if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask( ctx, cmd.ID, scheduledTime, "system", // 暂时使用系统用户ID ); err != nil { s.logger.Error("创建并入队公告发布任务失败", zap.Error(err)) return fmt.Errorf("创建定时发布任务失败: %w", err) } // 7. 设置定时发布 if err := announcement.SchedulePublish(scheduledTime); err != nil { return fmt.Errorf("设置定时发布失败: %w", err) } // 8. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("设置定时发布失败: %w", err) } s.logger.Info("设置定时发布成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime)) return nil } // UpdateSchedulePublishAnnouncement 更新定时发布公告 func (s *AnnouncementApplicationServiceImpl) UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) 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. 获取公告 announcement, err := s.announcementRepo.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 !announcement.IsScheduled() { return fmt.Errorf("公告未设置定时发布,无法修改时间") } // 4. 取消旧任务 if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil { s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err)) } // 5. 创建任务工厂 taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager) // 6. 创建并异步入队新的公告发布任务 if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask( ctx, cmd.ID, scheduledTime, "system", // 暂时使用系统用户ID ); err != nil { s.logger.Error("创建并入队公告发布任务失败", zap.Error(err)) return fmt.Errorf("创建定时发布任务失败: %w", err) } // 7. 更新定时发布时间 if err := announcement.UpdateSchedulePublish(scheduledTime); err != nil { return fmt.Errorf("更新定时发布时间失败: %w", err) } // 8. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("修改定时发布时间失败: %w", err) } s.logger.Info("修改定时发布时间成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime)) return nil } // CancelSchedulePublishAnnouncement 取消定时发布公告 func (s *AnnouncementApplicationServiceImpl) CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error { // 1. 获取公告 announcement, err := s.announcementRepo.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 !announcement.IsScheduled() { return fmt.Errorf("公告未设置定时发布,无需取消") } // 3. 取消任务 if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil { s.logger.Warn("取消任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err)) // 继续执行,即使取消任务失败也尝试取消定时发布状态 } // 4. 取消定时发布 if err := announcement.CancelSchedulePublish(); err != nil { return fmt.Errorf("取消定时发布失败: %w", err) } // 5. 保存更新 if err := s.announcementRepo.Update(ctx, announcement); err != nil { s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err)) return fmt.Errorf("取消定时发布失败: %w", err) } s.logger.Info("取消定时发布成功", zap.String("id", announcement.ID)) return nil } // GetAnnouncementStats 获取公告统计信息 func (s *AnnouncementApplicationServiceImpl) GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error) { // 1. 统计总数 total, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft) if err != nil { s.logger.Error("统计公告总数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } // 2. 统计各状态数量 published, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusPublished) if err != nil { s.logger.Error("统计已发布公告数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } draft, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft) if err != nil { s.logger.Error("统计草稿公告数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } archived, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusArchived) if err != nil { s.logger.Error("统计归档公告数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } // 3. 统计定时发布数量(需要查询有scheduled_at的草稿) scheduled, err := s.announcementRepo.FindScheduled(ctx) if err != nil { s.logger.Error("统计定时发布公告数失败", zap.Error(err)) return nil, fmt.Errorf("获取统计信息失败: %w", err) } response := &responses.AnnouncementStatsResponse{ TotalAnnouncements: total + published + archived, PublishedAnnouncements: published, DraftAnnouncements: draft, ArchivedAnnouncements: archived, ScheduledAnnouncements: int64(len(scheduled)), } return response, nil }