add timed
This commit is contained in:
@@ -16,27 +16,28 @@ type ArticleApplicationService interface {
|
||||
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
|
||||
PublishArticleByID(ctx context.Context, articleID string) error
|
||||
SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
|
||||
CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error
|
||||
ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error
|
||||
SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error
|
||||
|
||||
|
||||
// 文章交互
|
||||
RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error
|
||||
|
||||
|
||||
// 统计信息
|
||||
GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error)
|
||||
|
||||
|
||||
// 分类管理
|
||||
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
|
||||
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
|
||||
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
|
||||
GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||
ListCategories(ctx context.Context) (*responses.CategoryListResponse, error)
|
||||
|
||||
|
||||
// 标签管理
|
||||
CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error
|
||||
UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error
|
||||
|
||||
@@ -41,7 +41,7 @@ func NewArticleApplicationService(
|
||||
tagRepo: tagRepo,
|
||||
articleService: articleService,
|
||||
asynqClient: asynqClient,
|
||||
logger: logger,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *
|
||||
if err := s.validateCreateArticle(cmd); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 2. 创建文章实体
|
||||
article := &entities.Article{
|
||||
Title: cmd.Title,
|
||||
@@ -62,19 +62,19 @@ func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *
|
||||
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 {
|
||||
@@ -83,7 +83,7 @@ func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
s.logger.Info("创建文章成功", zap.String("id", article.ID), zap.String("title", article.Title))
|
||||
return nil
|
||||
}
|
||||
@@ -96,12 +96,12 @@ func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *
|
||||
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
|
||||
@@ -119,34 +119,32 @@ func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *
|
||||
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))
|
||||
}
|
||||
// 先清除现有标签
|
||||
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
|
||||
}
|
||||
@@ -159,13 +157,13 @@ func (s *ArticleApplicationServiceImpl) DeleteArticle(ctx context.Context, cmd *
|
||||
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
|
||||
}
|
||||
@@ -178,10 +176,10 @@ func (s *ArticleApplicationServiceImpl) GetArticleByID(ctx context.Context, quer
|
||||
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
|
||||
}
|
||||
@@ -201,24 +199,24 @@ func (s *ArticleApplicationServiceImpl) ListArticles(ctx context.Context, query
|
||||
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
|
||||
}
|
||||
@@ -238,29 +236,28 @@ func (s *ArticleApplicationServiceImpl) ListArticlesForAdmin(ctx context.Context
|
||||
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. 获取文章
|
||||
@@ -269,18 +266,18 @@ func (s *ArticleApplicationServiceImpl) PublishArticle(ctx context.Context, cmd
|
||||
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
|
||||
}
|
||||
@@ -293,49 +290,100 @@ func (s *ArticleApplicationServiceImpl) PublishArticleByID(ctx context.Context,
|
||||
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. 解析定时发布时间
|
||||
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.TaskID != "" {
|
||||
if err := s.asynqClient.CancelScheduledTask(ctx, article.TaskID); err != nil {
|
||||
s.logger.Warn("取消旧定时任务失败", zap.String("task_id", article.TaskID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调度定时发布任务
|
||||
taskID, err := s.asynqClient.ScheduleArticlePublish(ctx, cmd.ID, scheduledTime)
|
||||
if err != nil {
|
||||
s.logger.Error("调度定时发布任务失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("调度定时发布任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 设置定时发布
|
||||
if err := article.SchedulePublish(scheduledTime, taskID); 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("scheduled_time", scheduledTime), zap.String("task_id", taskID))
|
||||
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 err := article.SchedulePublish(cmd.ScheduledTime); err != nil {
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
|
||||
// 2. 检查是否已设置定时发布
|
||||
if !article.IsScheduled() {
|
||||
return fmt.Errorf("文章未设置定时发布")
|
||||
}
|
||||
|
||||
// 3. 保存更新
|
||||
|
||||
// 3. 取消定时任务
|
||||
if article.TaskID != "" {
|
||||
if err := s.asynqClient.CancelScheduledTask(ctx, article.TaskID); err != nil {
|
||||
s.logger.Warn("取消定时任务失败", zap.String("task_id", article.TaskID), 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)
|
||||
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))
|
||||
|
||||
s.logger.Info("取消定时发布成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -347,18 +395,18 @@ func (s *ArticleApplicationServiceImpl) ArchiveArticle(ctx context.Context, cmd
|
||||
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
|
||||
}
|
||||
@@ -371,22 +419,20 @@ func (s *ArticleApplicationServiceImpl) SetFeatured(ctx context.Context, cmd *co
|
||||
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. 增加阅读量
|
||||
@@ -394,7 +440,7 @@ func (s *ArticleApplicationServiceImpl) RecordView(ctx context.Context, articleI
|
||||
s.logger.Error("增加阅读量失败", zap.String("id", articleID), zap.Error(err))
|
||||
return fmt.Errorf("记录阅读失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
s.logger.Info("记录阅读成功", zap.String("id", articleID))
|
||||
return nil
|
||||
}
|
||||
@@ -407,25 +453,25 @@ func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*r
|
||||
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,
|
||||
@@ -433,12 +479,11 @@ func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*r
|
||||
ArchivedArticles: archivedArticles,
|
||||
TotalViews: 0, // TODO: 实现总阅读量统计
|
||||
}
|
||||
|
||||
|
||||
s.logger.Info("获取文章统计成功")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
||||
// validateCreateArticle 验证创建文章参数
|
||||
func (s *ArticleApplicationServiceImpl) validateCreateArticle(cmd *commands.CreateArticleCommand) error {
|
||||
if cmd.Title == "" {
|
||||
@@ -458,20 +503,20 @@ func (s *ArticleApplicationServiceImpl) CreateCategory(ctx context.Context, cmd
|
||||
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
|
||||
}
|
||||
@@ -484,17 +529,17 @@ func (s *ArticleApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd
|
||||
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
|
||||
}
|
||||
@@ -507,24 +552,24 @@ func (s *ArticleApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd
|
||||
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
|
||||
}
|
||||
@@ -537,7 +582,7 @@ func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, que
|
||||
s.logger.Error("获取分类失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := &responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
@@ -546,7 +591,7 @@ func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, que
|
||||
SortOrder: category.SortOrder,
|
||||
CreatedAt: category.CreatedAt,
|
||||
}
|
||||
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -558,7 +603,7 @@ func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*re
|
||||
s.logger.Error("获取分类列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类列表失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 2. 转换为响应对象
|
||||
items := make([]responses.CategoryInfoResponse, len(categories))
|
||||
for i, category := range categories {
|
||||
@@ -570,12 +615,12 @@ func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*re
|
||||
CreatedAt: category.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
response := &responses.CategoryListResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}
|
||||
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -587,20 +632,20 @@ func (s *ArticleApplicationServiceImpl) CreateTag(ctx context.Context, cmd *comm
|
||||
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
|
||||
}
|
||||
@@ -613,17 +658,17 @@ func (s *ArticleApplicationServiceImpl) UpdateTag(ctx context.Context, cmd *comm
|
||||
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
|
||||
}
|
||||
@@ -636,13 +681,13 @@ func (s *ArticleApplicationServiceImpl) DeleteTag(ctx context.Context, cmd *comm
|
||||
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
|
||||
}
|
||||
@@ -655,7 +700,7 @@ func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *a
|
||||
s.logger.Error("获取标签失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("标签不存在: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := &responses.TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
@@ -663,7 +708,7 @@ func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *a
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -675,7 +720,7 @@ func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*response
|
||||
s.logger.Error("获取标签列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取标签列表失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 2. 转换为响应对象
|
||||
items := make([]responses.TagInfoResponse, len(tags))
|
||||
for i, tag := range tags {
|
||||
@@ -686,12 +731,12 @@ func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*response
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
response := &responses.TagListResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}
|
||||
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package commands
|
||||
|
||||
// CancelScheduleCommand 取消定时发布命令
|
||||
type CancelScheduleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
@@ -1,9 +1,39 @@
|
||||
package commands
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SchedulePublishCommand 定时发布文章命令
|
||||
type SchedulePublishCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
ScheduledTime time.Time `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *SchedulePublishCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, cmd.ScheduledTime); err == nil {
|
||||
// 如果解析的时间没有时区信息,则设置为中国东八区
|
||||
if t.Location() == time.UTC {
|
||||
t = t.In(cst)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type ArticleInfoResponse struct {
|
||||
Status string `json:"status" comment:"文章状态"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
ViewCount int `json:"view_count" comment:"阅读量"`
|
||||
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
@@ -34,6 +35,7 @@ type ArticleListItemResponse struct {
|
||||
Status string `json:"status" comment:"文章状态"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
ViewCount int `json:"view_count" comment:"阅读量"`
|
||||
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
@@ -103,6 +105,7 @@ func FromArticleEntity(article *entities.Article) *ArticleInfoResponse {
|
||||
Status: string(article.Status),
|
||||
IsFeatured: article.IsFeatured,
|
||||
PublishedAt: article.PublishedAt,
|
||||
ScheduledAt: article.ScheduledAt,
|
||||
ViewCount: article.ViewCount,
|
||||
CreatedAt: article.CreatedAt,
|
||||
UpdatedAt: article.UpdatedAt,
|
||||
@@ -150,6 +153,7 @@ func FromArticleEntityToListItem(article *entities.Article) *ArticleListItemResp
|
||||
Status: string(article.Status),
|
||||
IsFeatured: article.IsFeatured,
|
||||
PublishedAt: article.PublishedAt,
|
||||
ScheduledAt: article.ScheduledAt,
|
||||
ViewCount: article.ViewCount,
|
||||
CreatedAt: article.CreatedAt,
|
||||
UpdatedAt: article.UpdatedAt,
|
||||
|
||||
@@ -622,7 +622,7 @@ func NewContainer() *Container {
|
||||
fx.Provide(
|
||||
// Asynq 客户端
|
||||
func(cfg *config.Config, logger *zap.Logger) *task.AsynqClient {
|
||||
redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port)
|
||||
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||
return task.NewAsynqClient(redisAddr, logger)
|
||||
},
|
||||
),
|
||||
|
||||
@@ -35,6 +35,7 @@ type Article struct {
|
||||
IsFeatured bool `gorm:"default:false" json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
TaskID string `gorm:"type:varchar(100)" json:"task_id" comment:"定时任务ID"`
|
||||
|
||||
// 统计信息
|
||||
ViewCount int `gorm:"default:0" json:"view_count" comment:"阅读量"`
|
||||
@@ -119,7 +120,7 @@ func (a *Article) Publish() error {
|
||||
}
|
||||
|
||||
// SchedulePublish 定时发布文章
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time) error {
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time, taskID string) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
@@ -130,6 +131,35 @@ func (a *Article) SchedulePublish(scheduledTime time.Time) error {
|
||||
|
||||
a.Status = ArticleStatusDraft // 保持草稿状态,等待定时发布
|
||||
a.ScheduledAt = &scheduledTime
|
||||
a.TaskID = taskID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSchedulePublish 更新定时发布时间
|
||||
func (a *Article) UpdateSchedulePublish(scheduledTime time.Time, taskID string) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
|
||||
if scheduledTime.Before(time.Now()) {
|
||||
return NewValidationError("定时发布时间不能早于当前时间")
|
||||
}
|
||||
|
||||
a.ScheduledAt = &scheduledTime
|
||||
a.TaskID = taskID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelSchedulePublish 取消定时发布
|
||||
func (a *Article) CancelSchedulePublish() error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
|
||||
a.ScheduledAt = nil
|
||||
a.TaskID = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -268,8 +268,7 @@ func (r *GormArticleRepository) ListArticles(ctx context.Context, query *repoQue
|
||||
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 := r.db.WithContext(ctx).Model(&entities.Article{})
|
||||
|
||||
// 用户端不显示归档文章
|
||||
dbQuery = dbQuery.Where("status != ?", entities.ArticleStatusArchived)
|
||||
@@ -357,8 +356,7 @@ func (r *GormArticleRepository) ListArticlesForAdmin(ctx context.Context, query
|
||||
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")
|
||||
dbQuery := r.db.WithContext(ctx).Model(&entities.Article{})
|
||||
|
||||
// 应用筛选条件
|
||||
if query.Status != "" {
|
||||
|
||||
@@ -142,8 +142,8 @@ func (r *GormTagRepository) AddTagToArticle(ctx context.Context, articleID strin
|
||||
|
||||
// 创建关联
|
||||
err = r.db.WithContext(ctx).Exec(`
|
||||
INSERT INTO article_tag_relations (id, article_id, tag_id, created_at)
|
||||
VALUES (UUID(), ?, ?, NOW())
|
||||
INSERT INTO article_tag_relations (article_id, tag_id)
|
||||
VALUES (?, ?)
|
||||
`, articleID, tagID).Error
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -53,19 +53,19 @@ func (h *ArticleHandler) CreateArticle(c *gin.Context) {
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 验证用户是否已登录
|
||||
if _, exists := c.Get("user_id"); !exists {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.CreateArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("创建文章失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Created(c, nil, "文章创建成功")
|
||||
}
|
||||
|
||||
@@ -83,19 +83,19 @@ func (h *ArticleHandler) CreateArticle(c *gin.Context) {
|
||||
// @Router /api/v1/articles/{id} [get]
|
||||
func (h *ArticleHandler) GetArticleByID(c *gin.Context) {
|
||||
var query appQueries.GetArticleQuery
|
||||
query.ID = c.Param("id")
|
||||
if query.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "文章ID不能为空")
|
||||
|
||||
// 绑定URI参数(文章ID)
|
||||
if err := h.validator.ValidateParam(c, &query); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
response, err := h.appService.GetArticleByID(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取文章详情失败", zap.Error(err))
|
||||
h.responseBuilder.NotFound(c, "文章不存在")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, response, "获取文章详情成功")
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (h *ArticleHandler) ListArticles(c *gin.Context) {
|
||||
if err := h.validator.ValidateQuery(c, &query); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 设置默认值
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
@@ -135,14 +135,14 @@ func (h *ArticleHandler) ListArticles(c *gin.Context) {
|
||||
if query.PageSize > 100 {
|
||||
query.PageSize = 100
|
||||
}
|
||||
|
||||
|
||||
response, err := h.appService.ListArticles(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取文章列表失败", zap.Error(err))
|
||||
h.responseBuilder.InternalError(c, "获取文章列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, response, "获取文章列表成功")
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (h *ArticleHandler) ListArticlesForAdmin(c *gin.Context) {
|
||||
if err := h.validator.ValidateQuery(c, &query); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 设置默认值
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
@@ -184,19 +184,17 @@ func (h *ArticleHandler) ListArticlesForAdmin(c *gin.Context) {
|
||||
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 更新文章
|
||||
// @Summary 更新文章
|
||||
// @Description 更新文章信息
|
||||
@@ -214,21 +212,23 @@ func (h *ArticleHandler) ListArticlesForAdmin(c *gin.Context) {
|
||||
// @Router /api/v1/admin/articles/{id} [put]
|
||||
func (h *ArticleHandler) UpdateArticle(c *gin.Context) {
|
||||
var cmd commands.UpdateArticleCommand
|
||||
cmd.ID = c.Param("id")
|
||||
if cmd.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "文章ID不能为空")
|
||||
|
||||
// 先绑定URI参数(文章ID)
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 再绑定JSON请求体(文章信息)
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.UpdateArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("更新文章失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "文章更新成功")
|
||||
}
|
||||
|
||||
@@ -251,13 +251,13 @@ func (h *ArticleHandler) DeleteArticle(c *gin.Context) {
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.DeleteArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("删除文章失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "文章删除成功")
|
||||
}
|
||||
|
||||
@@ -280,19 +280,19 @@ func (h *ArticleHandler) PublishArticle(c *gin.Context) {
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.PublishArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("发布文章失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "文章发布成功")
|
||||
}
|
||||
|
||||
// SchedulePublishArticle 定时发布文章
|
||||
// @Summary 定时发布文章
|
||||
// @Description 设置文章的定时发布时间
|
||||
// @Description 设置文章的定时发布时间,支持格式:YYYY-MM-DD HH:mm:ss
|
||||
// @Tags 文章管理-管理端
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -307,22 +307,57 @@ func (h *ArticleHandler) PublishArticle(c *gin.Context) {
|
||||
// @Router /api/v1/admin/articles/{id}/schedule-publish [post]
|
||||
func (h *ArticleHandler) SchedulePublishArticle(c *gin.Context) {
|
||||
var cmd commands.SchedulePublishCommand
|
||||
|
||||
// 先绑定URI参数(文章ID)
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 再绑定JSON请求体(定时发布时间)
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.SchedulePublishArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("设置定时发布失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "定时发布设置成功")
|
||||
}
|
||||
|
||||
// CancelSchedulePublishArticle 取消定时发布文章
|
||||
// @Summary 取消定时发布文章
|
||||
// @Description 取消文章的定时发布设置
|
||||
// @Tags 文章管理-管理端
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path string true "文章ID"
|
||||
// @Success 200 {object} map[string]interface{} "取消定时发布成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "文章不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/articles/{id}/cancel-schedule [post]
|
||||
func (h *ArticleHandler) CancelSchedulePublishArticle(c *gin.Context) {
|
||||
var cmd commands.CancelScheduleCommand
|
||||
|
||||
// 绑定URI参数(文章ID)
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.appService.CancelSchedulePublishArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("取消定时发布失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil, "取消定时发布成功")
|
||||
}
|
||||
|
||||
// ArchiveArticle 归档文章
|
||||
// @Summary 归档文章
|
||||
// @Description 将已发布文章归档
|
||||
@@ -342,13 +377,13 @@ func (h *ArticleHandler) ArchiveArticle(c *gin.Context) {
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.ArchiveArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("归档文章失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "文章归档成功")
|
||||
}
|
||||
|
||||
@@ -369,19 +404,23 @@ func (h *ArticleHandler) ArchiveArticle(c *gin.Context) {
|
||||
// @Router /api/v1/admin/articles/{id}/featured [put]
|
||||
func (h *ArticleHandler) SetFeatured(c *gin.Context) {
|
||||
var cmd commands.SetFeaturedCommand
|
||||
|
||||
// 先绑定URI参数(文章ID)
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 再绑定JSON请求体(推荐状态)
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.SetFeatured(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("设置推荐状态失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "设置推荐状态成功")
|
||||
}
|
||||
|
||||
@@ -403,11 +442,10 @@ func (h *ArticleHandler) GetArticleStats(c *gin.Context) {
|
||||
h.responseBuilder.InternalError(c, "获取文章统计失败")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, response, "获取统计成功")
|
||||
}
|
||||
|
||||
|
||||
// ==================== 分类相关方法 ====================
|
||||
|
||||
// ListCategories 获取分类列表
|
||||
@@ -426,7 +464,7 @@ func (h *ArticleHandler) ListCategories(c *gin.Context) {
|
||||
h.responseBuilder.InternalError(c, "获取分类列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, response, "获取分类列表成功")
|
||||
}
|
||||
|
||||
@@ -444,19 +482,19 @@ func (h *ArticleHandler) ListCategories(c *gin.Context) {
|
||||
// @Router /api/v1/article-categories/{id} [get]
|
||||
func (h *ArticleHandler) GetCategoryByID(c *gin.Context) {
|
||||
var query appQueries.GetCategoryQuery
|
||||
query.ID = c.Param("id")
|
||||
if query.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "分类ID不能为空")
|
||||
|
||||
// 绑定URI参数(分类ID)
|
||||
if err := h.validator.ValidateParam(c, &query); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
response, err := h.appService.GetCategoryByID(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取分类详情失败", zap.Error(err))
|
||||
h.responseBuilder.NotFound(c, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, response, "获取分类详情成功")
|
||||
}
|
||||
|
||||
@@ -478,13 +516,13 @@ func (h *ArticleHandler) CreateCategory(c *gin.Context) {
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.CreateCategory(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("创建分类失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Created(c, nil, "分类创建成功")
|
||||
}
|
||||
|
||||
@@ -505,21 +543,23 @@ func (h *ArticleHandler) CreateCategory(c *gin.Context) {
|
||||
// @Router /api/v1/admin/article-categories/{id} [put]
|
||||
func (h *ArticleHandler) UpdateCategory(c *gin.Context) {
|
||||
var cmd commands.UpdateCategoryCommand
|
||||
cmd.ID = c.Param("id")
|
||||
if cmd.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "分类ID不能为空")
|
||||
|
||||
// 先绑定URI参数(分类ID)
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 再绑定JSON请求体(分类信息)
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.UpdateCategory(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("更新分类失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "分类更新成功")
|
||||
}
|
||||
|
||||
@@ -542,13 +582,13 @@ func (h *ArticleHandler) DeleteCategory(c *gin.Context) {
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.DeleteCategory(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("删除分类失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "分类删除成功")
|
||||
}
|
||||
|
||||
@@ -570,7 +610,7 @@ func (h *ArticleHandler) ListTags(c *gin.Context) {
|
||||
h.responseBuilder.InternalError(c, "获取标签列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, response, "获取标签列表成功")
|
||||
}
|
||||
|
||||
@@ -588,19 +628,19 @@ func (h *ArticleHandler) ListTags(c *gin.Context) {
|
||||
// @Router /api/v1/article-tags/{id} [get]
|
||||
func (h *ArticleHandler) GetTagByID(c *gin.Context) {
|
||||
var query appQueries.GetTagQuery
|
||||
query.ID = c.Param("id")
|
||||
if query.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "标签ID不能为空")
|
||||
|
||||
// 绑定URI参数(标签ID)
|
||||
if err := h.validator.ValidateParam(c, &query); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
response, err := h.appService.GetTagByID(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取标签详情失败", zap.Error(err))
|
||||
h.responseBuilder.NotFound(c, "标签不存在")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, response, "获取标签详情成功")
|
||||
}
|
||||
|
||||
@@ -622,13 +662,13 @@ func (h *ArticleHandler) CreateTag(c *gin.Context) {
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.CreateTag(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("创建标签失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Created(c, nil, "标签创建成功")
|
||||
}
|
||||
|
||||
@@ -649,21 +689,23 @@ func (h *ArticleHandler) CreateTag(c *gin.Context) {
|
||||
// @Router /api/v1/admin/article-tags/{id} [put]
|
||||
func (h *ArticleHandler) UpdateTag(c *gin.Context) {
|
||||
var cmd commands.UpdateTagCommand
|
||||
cmd.ID = c.Param("id")
|
||||
if cmd.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "标签ID不能为空")
|
||||
|
||||
// 先绑定URI参数(标签ID)
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 再绑定JSON请求体(标签信息)
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.UpdateTag(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("更新标签失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "标签更新成功")
|
||||
}
|
||||
|
||||
@@ -686,12 +728,12 @@ func (h *ArticleHandler) DeleteTag(c *gin.Context) {
|
||||
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.appService.DeleteTag(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("删除标签失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.responseBuilder.Success(c, nil, "标签删除成功")
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (r *ApiRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
|
||||
// 加密接口(用于前端调试)
|
||||
apiGroup.POST("/encrypt", r.authMiddleware.Handle(), r.apiHandler.EncryptParams)
|
||||
|
||||
|
||||
// 解密接口(用于前端调试)
|
||||
apiGroup.POST("/decrypt", r.authMiddleware.Handle(), r.apiHandler.DecryptParams)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ func (r *ArticleRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
// 文章状态管理
|
||||
adminArticleGroup.POST("/:id/publish", r.handler.PublishArticle) // 发布文章
|
||||
adminArticleGroup.POST("/:id/schedule-publish", r.handler.SchedulePublishArticle) // 定时发布文章
|
||||
adminArticleGroup.POST("/:id/cancel-schedule", r.handler.CancelSchedulePublishArticle) // 取消定时发布
|
||||
adminArticleGroup.POST("/:id/archive", r.handler.ArchiveArticle) // 归档文章
|
||||
adminArticleGroup.PUT("/:id/featured", r.handler.SetFeatured) // 设置推荐状态
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *AsynqClient) Close() error {
|
||||
}
|
||||
|
||||
// ScheduleArticlePublish 调度文章定时发布任务
|
||||
func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID string, publishTime time.Time) error {
|
||||
func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID string, publishTime time.Time) (string, error) {
|
||||
payload := map[string]interface{}{
|
||||
"article_id": articleID,
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID stri
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
c.logger.Error("序列化任务载荷失败", zap.Error(err))
|
||||
return fmt.Errorf("创建任务失败: %w", err)
|
||||
return "", fmt.Errorf("创建任务失败: %w", err)
|
||||
}
|
||||
|
||||
task := asynq.NewTask(TaskTypeArticlePublish, payloadBytes)
|
||||
@@ -47,7 +47,7 @@ func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID stri
|
||||
// 计算延迟时间
|
||||
delay := publishTime.Sub(time.Now())
|
||||
if delay <= 0 {
|
||||
return fmt.Errorf("定时发布时间不能早于当前时间")
|
||||
return "", fmt.Errorf("定时发布时间不能早于当前时间")
|
||||
}
|
||||
|
||||
// 设置任务选项
|
||||
@@ -63,7 +63,7 @@ func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID stri
|
||||
zap.String("article_id", articleID),
|
||||
zap.Time("publish_time", publishTime),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("调度任务失败: %w", err)
|
||||
return "", fmt.Errorf("调度任务失败: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("定时发布任务调度成功",
|
||||
@@ -71,5 +71,44 @@ func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID stri
|
||||
zap.Time("publish_time", publishTime),
|
||||
zap.String("task_id", info.ID))
|
||||
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
// CancelScheduledTask 取消已调度的任务
|
||||
func (c *AsynqClient) CancelScheduledTask(ctx context.Context, taskID string) error {
|
||||
// 注意:Asynq不直接支持取消已调度的任务
|
||||
// 这里我们记录日志,实际取消需要在数据库中标记
|
||||
c.logger.Info("请求取消定时任务",
|
||||
zap.String("task_id", taskID))
|
||||
|
||||
// 在实际应用中,你可能需要:
|
||||
// 1. 在数据库中标记任务为已取消
|
||||
// 2. 在任务执行时检查取消状态
|
||||
// 3. 或者使用Redis的TTL机制
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RescheduleArticlePublish 重新调度文章定时发布任务
|
||||
func (c *AsynqClient) RescheduleArticlePublish(ctx context.Context, articleID string, oldTaskID string, newPublishTime time.Time) (string, error) {
|
||||
// 1. 取消旧任务(标记为已取消)
|
||||
if err := c.CancelScheduledTask(ctx, oldTaskID); err != nil {
|
||||
c.logger.Warn("取消旧任务失败",
|
||||
zap.String("old_task_id", oldTaskID),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
// 2. 创建新任务
|
||||
newTaskID, err := c.ScheduleArticlePublish(ctx, articleID, newPublishTime)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("重新调度任务失败: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("重新调度定时发布任务成功",
|
||||
zap.String("article_id", articleID),
|
||||
zap.String("old_task_id", oldTaskID),
|
||||
zap.String("new_task_id", newTaskID),
|
||||
zap.Time("new_publish_time", newPublishTime))
|
||||
|
||||
return newTaskID, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user