fix
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormScheduledTaskRepository GORM定时任务仓储实现
|
||||
type GormScheduledTaskRepository struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// 编译时检查接口实现
|
||||
var _ repositories.ScheduledTaskRepository = (*GormScheduledTaskRepository)(nil)
|
||||
|
||||
// NewGormScheduledTaskRepository 创建GORM定时任务仓储
|
||||
func NewGormScheduledTaskRepository(db *gorm.DB, logger *zap.Logger) *GormScheduledTaskRepository {
|
||||
return &GormScheduledTaskRepository{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建定时任务记录
|
||||
func (r *GormScheduledTaskRepository) Create(ctx context.Context, task entities.ScheduledTask) (entities.ScheduledTask, error) {
|
||||
r.logger.Info("创建定时任务记录", zap.String("task_id", task.TaskID), zap.String("article_id", task.ArticleID))
|
||||
|
||||
err := r.db.WithContext(ctx).Create(&task).Error
|
||||
if err != nil {
|
||||
r.logger.Error("创建定时任务记录失败", zap.Error(err))
|
||||
return task, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// GetByTaskID 根据Asynq任务ID获取任务记录
|
||||
func (r *GormScheduledTaskRepository) GetByTaskID(ctx context.Context, taskID string) (entities.ScheduledTask, error) {
|
||||
var task entities.ScheduledTask
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Article").
|
||||
Where("task_id = ?", taskID).
|
||||
First(&task).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return task, fmt.Errorf("定时任务不存在")
|
||||
}
|
||||
r.logger.Error("获取定时任务失败", zap.String("task_id", taskID), zap.Error(err))
|
||||
return task, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// GetByArticleID 根据文章ID获取任务记录
|
||||
func (r *GormScheduledTaskRepository) GetByArticleID(ctx context.Context, articleID string) (entities.ScheduledTask, error) {
|
||||
var task entities.ScheduledTask
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Article").
|
||||
Where("article_id = ? AND status IN (?)", articleID, []string{"pending", "running"}).
|
||||
First(&task).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return task, fmt.Errorf("文章没有活动的定时任务")
|
||||
}
|
||||
r.logger.Error("获取文章定时任务失败", zap.String("article_id", articleID), zap.Error(err))
|
||||
return task, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// Update 更新任务记录
|
||||
func (r *GormScheduledTaskRepository) Update(ctx context.Context, task entities.ScheduledTask) error {
|
||||
r.logger.Info("更新定时任务记录", zap.String("task_id", task.TaskID), zap.String("status", string(task.Status)))
|
||||
|
||||
err := r.db.WithContext(ctx).Save(&task).Error
|
||||
if err != nil {
|
||||
r.logger.Error("更新定时任务记录失败", zap.String("task_id", task.TaskID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除任务记录
|
||||
func (r *GormScheduledTaskRepository) Delete(ctx context.Context, taskID string) error {
|
||||
r.logger.Info("删除定时任务记录", zap.String("task_id", taskID))
|
||||
|
||||
err := r.db.WithContext(ctx).Where("task_id = ?", taskID).Delete(&entities.ScheduledTask{}).Error
|
||||
if err != nil {
|
||||
r.logger.Error("删除定时任务记录失败", zap.String("task_id", taskID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkAsCancelled 标记任务为已取消
|
||||
func (r *GormScheduledTaskRepository) MarkAsCancelled(ctx context.Context, taskID string) error {
|
||||
r.logger.Info("标记定时任务为已取消", zap.String("task_id", taskID))
|
||||
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&entities.ScheduledTask{}).
|
||||
Where("task_id = ? AND status IN (?)", taskID, []string{"pending", "running"}).
|
||||
Updates(map[string]interface{}{
|
||||
"status": entities.TaskStatusCancelled,
|
||||
"completed_at": time.Now(),
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
r.logger.Error("标记定时任务为已取消失败", zap.String("task_id", taskID), zap.Error(result.Error))
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
r.logger.Warn("没有找到需要取消的定时任务", zap.String("task_id", taskID))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveTasks 获取活动状态的任务列表
|
||||
func (r *GormScheduledTaskRepository) GetActiveTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
|
||||
var tasks []entities.ScheduledTask
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Article").
|
||||
Where("status IN (?)", []string{"pending", "running"}).
|
||||
Order("scheduled_at ASC").
|
||||
Find(&tasks).Error
|
||||
|
||||
if err != nil {
|
||||
r.logger.Error("获取活动定时任务列表失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// GetExpiredTasks 获取过期的任务列表
|
||||
func (r *GormScheduledTaskRepository) GetExpiredTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
|
||||
var tasks []entities.ScheduledTask
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Article").
|
||||
Where("status = ? AND scheduled_at < ?", entities.TaskStatusPending, time.Now()).
|
||||
Order("scheduled_at ASC").
|
||||
Find(&tasks).Error
|
||||
|
||||
if err != nil {
|
||||
r.logger.Error("获取过期定时任务列表失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
@@ -446,6 +446,43 @@ func (h *ArticleHandler) GetArticleStats(c *gin.Context) {
|
||||
h.responseBuilder.Success(c, response, "获取统计成功")
|
||||
}
|
||||
|
||||
// UpdateSchedulePublishArticle 修改定时发布时间
|
||||
// @Summary 修改定时发布时间
|
||||
// @Description 修改文章的定时发布时间
|
||||
// @Tags 文章管理-管理端
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path string true "文章ID"
|
||||
// @Param request body commands.SchedulePublishCommand true "修改定时发布请求"
|
||||
// @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}/update-schedule-publish [post]
|
||||
func (h *ArticleHandler) UpdateSchedulePublishArticle(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.UpdateSchedulePublishArticle(c.Request.Context(), &cmd); err != nil {
|
||||
h.logger.Error("修改定时发布时间失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil, "修改定时发布时间成功")
|
||||
}
|
||||
|
||||
// ==================== 分类相关方法 ====================
|
||||
|
||||
// ListCategories 获取分类列表
|
||||
|
||||
@@ -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/update-schedule-publish", r.handler.UpdateSchedulePublishArticle) // 修改定时发布时间
|
||||
adminArticleGroup.POST("/:id/cancel-schedule", r.handler.CancelSchedulePublishArticle) // 取消定时发布
|
||||
adminArticleGroup.POST("/:id/archive", r.handler.ArchiveArticle) // 归档文章
|
||||
adminArticleGroup.PUT("/:id/featured", r.handler.SetFeatured) // 设置推荐状态
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
@@ -16,18 +17,21 @@ type ArticlePublisher interface {
|
||||
|
||||
// ArticleTaskHandler 文章任务处理器
|
||||
type ArticleTaskHandler struct {
|
||||
publisher ArticlePublisher
|
||||
logger *zap.Logger
|
||||
publisher ArticlePublisher
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewArticleTaskHandler 创建文章任务处理器
|
||||
func NewArticleTaskHandler(
|
||||
publisher ArticlePublisher,
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository,
|
||||
logger *zap.Logger,
|
||||
) *ArticleTaskHandler {
|
||||
return &ArticleTaskHandler{
|
||||
publisher: publisher,
|
||||
logger: logger,
|
||||
publisher: publisher,
|
||||
scheduledTaskRepo: scheduledTaskRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,14 +49,49 @@ func (h *ArticleTaskHandler) HandleArticlePublish(ctx context.Context, t *asynq.
|
||||
return fmt.Errorf("任务载荷中缺少文章ID")
|
||||
}
|
||||
|
||||
// 获取任务状态记录
|
||||
task, err := h.scheduledTaskRepo.GetByTaskID(ctx, t.ResultWriter().TaskID())
|
||||
if err != nil {
|
||||
h.logger.Error("获取任务状态记录失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(err))
|
||||
// 继续执行,不阻断任务
|
||||
} else {
|
||||
// 检查任务是否已取消
|
||||
if task.IsCancelled() {
|
||||
h.logger.Info("任务已取消,跳过执行", zap.String("task_id", t.ResultWriter().TaskID()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 标记任务为正在执行
|
||||
task.MarkAsRunning()
|
||||
if err := h.scheduledTaskRepo.Update(ctx, task); err != nil {
|
||||
h.logger.Warn("更新任务状态失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 执行文章发布
|
||||
if err := h.publisher.PublishArticleByID(ctx, articleID); err != nil {
|
||||
// 更新任务状态为失败
|
||||
if task.ID != "" {
|
||||
task.MarkAsFailed(err.Error())
|
||||
if updateErr := h.scheduledTaskRepo.Update(ctx, task); updateErr != nil {
|
||||
h.logger.Warn("更新任务失败状态失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(updateErr))
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Error("定时发布文章失败",
|
||||
zap.String("article_id", articleID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("定时发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新任务状态为已完成
|
||||
if task.ID != "" {
|
||||
task.MarkAsCompleted()
|
||||
if err := h.scheduledTaskRepo.Update(ctx, task); err != nil {
|
||||
h.logger.Warn("更新任务完成状态失败", zap.String("task_id", t.ResultWriter().TaskID()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("定时发布文章成功", zap.String("article_id", articleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
@@ -12,16 +14,18 @@ import (
|
||||
|
||||
// AsynqClient Asynq 客户端
|
||||
type AsynqClient struct {
|
||||
client *asynq.Client
|
||||
logger *zap.Logger
|
||||
client *asynq.Client
|
||||
logger *zap.Logger
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository
|
||||
}
|
||||
|
||||
// NewAsynqClient 创建 Asynq 客户端
|
||||
func NewAsynqClient(redisAddr string, logger *zap.Logger) *AsynqClient {
|
||||
func NewAsynqClient(redisAddr string, scheduledTaskRepo repositories.ScheduledTaskRepository, logger *zap.Logger) *AsynqClient {
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
|
||||
return &AsynqClient{
|
||||
client: client,
|
||||
logger: logger,
|
||||
client: client,
|
||||
logger: logger,
|
||||
scheduledTaskRepo: scheduledTaskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +70,20 @@ func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID stri
|
||||
return "", fmt.Errorf("调度任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建任务状态记录
|
||||
scheduledTask := entities.ScheduledTask{
|
||||
TaskID: info.ID,
|
||||
TaskType: TaskTypeArticlePublish,
|
||||
ArticleID: articleID,
|
||||
Status: entities.TaskStatusPending,
|
||||
ScheduledAt: publishTime,
|
||||
}
|
||||
|
||||
if _, err := c.scheduledTaskRepo.Create(ctx, scheduledTask); err != nil {
|
||||
c.logger.Error("创建任务状态记录失败", zap.String("task_id", info.ID), zap.Error(err))
|
||||
// 不返回错误,因为Asynq任务已经创建成功
|
||||
}
|
||||
|
||||
c.logger.Info("定时发布任务调度成功",
|
||||
zap.String("article_id", articleID),
|
||||
zap.Time("publish_time", publishTime),
|
||||
@@ -76,16 +94,17 @@ func (c *AsynqClient) ScheduleArticlePublish(ctx context.Context, articleID stri
|
||||
|
||||
// CancelScheduledTask 取消已调度的任务
|
||||
func (c *AsynqClient) CancelScheduledTask(ctx context.Context, taskID string) error {
|
||||
// 注意:Asynq不直接支持取消已调度的任务
|
||||
// 这里我们记录日志,实际取消需要在数据库中标记
|
||||
c.logger.Info("请求取消定时任务",
|
||||
c.logger.Info("标记定时任务为已取消",
|
||||
zap.String("task_id", taskID))
|
||||
|
||||
// 在实际应用中,你可能需要:
|
||||
// 1. 在数据库中标记任务为已取消
|
||||
// 2. 在任务执行时检查取消状态
|
||||
// 3. 或者使用Redis的TTL机制
|
||||
|
||||
|
||||
// 标记数据库中的任务状态为已取消
|
||||
if err := c.scheduledTaskRepo.MarkAsCancelled(ctx, taskID); err != nil {
|
||||
c.logger.Warn("标记任务状态为已取消失败", zap.String("task_id", taskID), zap.Error(err))
|
||||
// 不返回错误,因为Asynq任务可能已经执行完成
|
||||
}
|
||||
|
||||
// Asynq不支持直接取消任务,我们通过数据库状态来标记
|
||||
// 任务执行时会检查文章状态,如果已取消则跳过执行
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user