304 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			304 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
|  | package handlers | |||
|  | 
 | |||
|  | import ( | |||
|  | 	"context" | |||
|  | 	"encoding/json" | |||
|  | 	"fmt" | |||
|  | 	"time" | |||
|  | 
 | |||
|  | 	"github.com/hibiken/asynq" | |||
|  | 	"go.uber.org/zap" | |||
|  | 
 | |||
|  | 	"tyapi-server/internal/application/article" | |||
|  | 	"tyapi-server/internal/infrastructure/task/entities" | |||
|  | 	"tyapi-server/internal/infrastructure/task/repositories" | |||
|  | 	"tyapi-server/internal/infrastructure/task/types" | |||
|  | ) | |||
|  | 
 | |||
|  | // ArticleTaskHandler 文章任务处理器 | |||
|  | type ArticleTaskHandler struct { | |||
|  | 	logger                *zap.Logger | |||
|  | 	articleApplicationService article.ArticleApplicationService | |||
|  | 	asyncTaskRepo         repositories.AsyncTaskRepository | |||
|  | } | |||
|  | 
 | |||
|  | // NewArticleTaskHandler 创建文章任务处理器 | |||
|  | func NewArticleTaskHandler(logger *zap.Logger, articleApplicationService article.ArticleApplicationService, asyncTaskRepo repositories.AsyncTaskRepository) *ArticleTaskHandler { | |||
|  | 	return &ArticleTaskHandler{ | |||
|  | 		logger:                logger, | |||
|  | 		articleApplicationService: articleApplicationService, | |||
|  | 		asyncTaskRepo:         asyncTaskRepo, | |||
|  | 	} | |||
|  | } | |||
|  | 
 | |||
|  | // HandleArticlePublish 处理文章发布任务 | |||
|  | func (h *ArticleTaskHandler) HandleArticlePublish(ctx context.Context, t *asynq.Task) error { | |||
|  | 	h.logger.Info("开始处理文章发布任务") | |||
|  | 
 | |||
|  | 	var payload ArticlePublishPayload | |||
|  | 	if err := json.Unmarshal(t.Payload(), &payload); err != nil { | |||
|  | 		h.logger.Error("解析文章发布任务载荷失败", zap.Error(err)) | |||
|  | 		h.updateTaskStatus(ctx, t, "failed", "解析任务载荷失败") | |||
|  | 		return err | |||
|  | 	} | |||
|  | 
 | |||
|  | 	h.logger.Info("处理文章发布任务", | |||
|  | 		zap.String("article_id", payload.ArticleID), | |||
|  | 		zap.Time("publish_at", payload.PublishAt)) | |||
|  | 
 | |||
|  | 	// 检查任务是否已被取消 | |||
|  | 	if err := h.checkTaskStatus(ctx, t); err != nil { | |||
|  | 		h.logger.Info("任务已被取消,跳过执行", zap.String("article_id", payload.ArticleID)) | |||
|  | 		return nil // 静默返回,不报错 | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 调用文章应用服务发布文章 | |||
|  | 	if h.articleApplicationService != nil { | |||
|  | 		err := h.articleApplicationService.PublishArticleByID(ctx, payload.ArticleID) | |||
|  | 		if err != nil { | |||
|  | 			h.logger.Error("文章发布失败", zap.String("article_id", payload.ArticleID), zap.Error(err)) | |||
|  | 			h.updateTaskStatus(ctx, t, "failed", "文章发布失败: "+err.Error()) | |||
|  | 			return err | |||
|  | 		} | |||
|  | 	} else { | |||
|  | 		h.logger.Warn("文章应用服务未初始化,跳过发布", zap.String("article_id", payload.ArticleID)) | |||
|  | 		h.updateTaskStatus(ctx, t, "failed", "文章应用服务未初始化") | |||
|  | 		return nil | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 更新任务状态为成功 | |||
|  | 	h.updateTaskStatus(ctx, t, "completed", "") | |||
|  | 	h.logger.Info("文章发布任务处理完成", zap.String("article_id", payload.ArticleID)) | |||
|  | 	return nil | |||
|  | } | |||
|  | 
 | |||
|  | // HandleArticleCancel 处理文章取消任务 | |||
|  | func (h *ArticleTaskHandler) HandleArticleCancel(ctx context.Context, t *asynq.Task) error { | |||
|  | 	h.logger.Info("开始处理文章取消任务") | |||
|  | 
 | |||
|  | 	var payload ArticleCancelPayload | |||
|  | 	if err := json.Unmarshal(t.Payload(), &payload); err != nil { | |||
|  | 		h.logger.Error("解析文章取消任务载荷失败", zap.Error(err)) | |||
|  | 		return err | |||
|  | 	} | |||
|  | 
 | |||
|  | 	h.logger.Info("处理文章取消任务", zap.String("article_id", payload.ArticleID)) | |||
|  | 
 | |||
|  | 	// 这里实现文章取消的具体逻辑 | |||
|  | 	// 例如:更新文章状态、取消定时发布等 | |||
|  | 
 | |||
|  | 	h.logger.Info("文章取消任务处理完成", zap.String("article_id", payload.ArticleID)) | |||
|  | 	return nil | |||
|  | } | |||
|  | 
 | |||
|  | // HandleArticleModify 处理文章修改任务 | |||
|  | func (h *ArticleTaskHandler) HandleArticleModify(ctx context.Context, t *asynq.Task) error { | |||
|  | 	h.logger.Info("开始处理文章修改任务") | |||
|  | 
 | |||
|  | 	var payload ArticleModifyPayload | |||
|  | 	if err := json.Unmarshal(t.Payload(), &payload); err != nil { | |||
|  | 		h.logger.Error("解析文章修改任务载荷失败", zap.Error(err)) | |||
|  | 		return err | |||
|  | 	} | |||
|  | 
 | |||
|  | 	h.logger.Info("处理文章修改任务", | |||
|  | 		zap.String("article_id", payload.ArticleID), | |||
|  | 		zap.Time("new_publish_at", payload.NewPublishAt)) | |||
|  | 
 | |||
|  | 	// 这里实现文章修改的具体逻辑 | |||
|  | 	// 例如:更新文章发布时间、重新调度任务等 | |||
|  | 
 | |||
|  | 	h.logger.Info("文章修改任务处理完成", zap.String("article_id", payload.ArticleID)) | |||
|  | 	return nil | |||
|  | } | |||
|  | 
 | |||
|  | // ArticlePublishPayload 文章发布任务载荷 | |||
|  | type ArticlePublishPayload struct { | |||
|  | 	ArticleID string    `json:"article_id"` | |||
|  | 	PublishAt time.Time `json:"publish_at"` | |||
|  | 	UserID    string    `json:"user_id"` | |||
|  | } | |||
|  | 
 | |||
|  | // GetType 获取任务类型 | |||
|  | func (p *ArticlePublishPayload) GetType() types.TaskType { | |||
|  | 	return types.TaskTypeArticlePublish | |||
|  | } | |||
|  | 
 | |||
|  | // ToJSON 序列化为JSON | |||
|  | func (p *ArticlePublishPayload) ToJSON() ([]byte, error) { | |||
|  | 	return json.Marshal(p) | |||
|  | } | |||
|  | 
 | |||
|  | // FromJSON 从JSON反序列化 | |||
|  | func (p *ArticlePublishPayload) FromJSON(data []byte) error { | |||
|  | 	return json.Unmarshal(data, p) | |||
|  | } | |||
|  | 
 | |||
|  | // ArticleCancelPayload 文章取消任务载荷 | |||
|  | type ArticleCancelPayload struct { | |||
|  | 	ArticleID string `json:"article_id"` | |||
|  | 	UserID    string `json:"user_id"` | |||
|  | } | |||
|  | 
 | |||
|  | // GetType 获取任务类型 | |||
|  | func (p *ArticleCancelPayload) GetType() types.TaskType { | |||
|  | 	return types.TaskTypeArticleCancel | |||
|  | } | |||
|  | 
 | |||
|  | // ToJSON 序列化为JSON | |||
|  | func (p *ArticleCancelPayload) ToJSON() ([]byte, error) { | |||
|  | 	return json.Marshal(p) | |||
|  | } | |||
|  | 
 | |||
|  | // FromJSON 从JSON反序列化 | |||
|  | func (p *ArticleCancelPayload) FromJSON(data []byte) error { | |||
|  | 	return json.Unmarshal(data, p) | |||
|  | } | |||
|  | 
 | |||
|  | // ArticleModifyPayload 文章修改任务载荷 | |||
|  | type ArticleModifyPayload struct { | |||
|  | 	ArticleID      string    `json:"article_id"` | |||
|  | 	NewPublishAt   time.Time `json:"new_publish_at"` | |||
|  | 	UserID         string    `json:"user_id"` | |||
|  | } | |||
|  | 
 | |||
|  | // GetType 获取任务类型 | |||
|  | func (p *ArticleModifyPayload) GetType() types.TaskType { | |||
|  | 	return types.TaskTypeArticleModify | |||
|  | } | |||
|  | 
 | |||
|  | // ToJSON 序列化为JSON | |||
|  | func (p *ArticleModifyPayload) ToJSON() ([]byte, error) { | |||
|  | 	return json.Marshal(p) | |||
|  | } | |||
|  | 
 | |||
|  | // FromJSON 从JSON反序列化 | |||
|  | func (p *ArticleModifyPayload) FromJSON(data []byte) error { | |||
|  | 	return json.Unmarshal(data, p) | |||
|  | } | |||
|  | 
 | |||
|  | // updateTaskStatus 更新任务状态 | |||
|  | func (h *ArticleTaskHandler) updateTaskStatus(ctx context.Context, t *asynq.Task, status string, errorMsg string) { | |||
|  | 	// 从任务载荷中提取任务ID | |||
|  | 	var payload map[string]interface{} | |||
|  | 	if err := json.Unmarshal(t.Payload(), &payload); err != nil { | |||
|  | 		h.logger.Error("解析任务载荷失败,无法更新状态", zap.Error(err)) | |||
|  | 		return | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 尝试从payload中获取任务ID | |||
|  | 	taskID, ok := payload["task_id"].(string) | |||
|  | 	if !ok { | |||
|  | 		// 如果没有task_id,尝试从article_id生成 | |||
|  | 		if articleID, ok := payload["article_id"].(string); ok { | |||
|  | 			taskID = fmt.Sprintf("article-publish-%s", articleID) | |||
|  | 		} else { | |||
|  | 			h.logger.Error("无法从任务载荷中获取任务ID") | |||
|  | 			return | |||
|  | 		} | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 根据状态决定更新方式 | |||
|  | 	if status == "failed" { | |||
|  | 		// 失败时:需要检查是否达到最大重试次数 | |||
|  | 		h.handleTaskFailure(ctx, taskID, errorMsg) | |||
|  | 	} else if status == "completed" { | |||
|  | 		// 成功时:清除错误信息并更新状态 | |||
|  | 		if err := h.asyncTaskRepo.UpdateStatusWithSuccess(ctx, taskID, entities.TaskStatus(status)); err != nil { | |||
|  | 			h.logger.Error("更新任务状态失败",  | |||
|  | 				zap.String("task_id", taskID), | |||
|  | 				zap.String("status", status), | |||
|  | 				zap.Error(err)) | |||
|  | 		} | |||
|  | 	} else { | |||
|  | 		// 其他状态:只更新状态 | |||
|  | 		if err := h.asyncTaskRepo.UpdateStatus(ctx, taskID, entities.TaskStatus(status)); err != nil { | |||
|  | 			h.logger.Error("更新任务状态失败",  | |||
|  | 				zap.String("task_id", taskID), | |||
|  | 				zap.String("status", status), | |||
|  | 				zap.Error(err)) | |||
|  | 		} | |||
|  | 	} | |||
|  | 
 | |||
|  | 	h.logger.Info("任务状态已更新",  | |||
|  | 		zap.String("task_id", taskID), | |||
|  | 		zap.String("status", status), | |||
|  | 		zap.String("error_msg", errorMsg)) | |||
|  | } | |||
|  | 
 | |||
|  | // handleTaskFailure 处理任务失败 | |||
|  | func (h *ArticleTaskHandler) handleTaskFailure(ctx context.Context, taskID string, errorMsg string) { | |||
|  | 	// 获取当前任务信息 | |||
|  | 	task, err := h.asyncTaskRepo.GetByID(ctx, taskID) | |||
|  | 	if err != nil { | |||
|  | 		h.logger.Error("获取任务信息失败", zap.String("task_id", taskID), zap.Error(err)) | |||
|  | 		return | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 增加重试次数 | |||
|  | 	newRetryCount := task.RetryCount + 1 | |||
|  | 	 | |||
|  | 	// 检查是否达到最大重试次数 | |||
|  | 	if newRetryCount >= task.MaxRetries { | |||
|  | 		// 达到最大重试次数,标记为最终失败 | |||
|  | 		if err := h.asyncTaskRepo.UpdateStatusWithRetryAndError(ctx, taskID, entities.TaskStatusFailed, errorMsg); err != nil { | |||
|  | 			h.logger.Error("更新任务状态失败",  | |||
|  | 				zap.String("task_id", taskID), | |||
|  | 				zap.String("status", "failed"), | |||
|  | 				zap.Error(err)) | |||
|  | 		} | |||
|  | 		h.logger.Info("任务最终失败,已达到最大重试次数",  | |||
|  | 			zap.String("task_id", taskID), | |||
|  | 			zap.Int("retry_count", newRetryCount), | |||
|  | 			zap.Int("max_retries", task.MaxRetries)) | |||
|  | 	} else { | |||
|  | 		// 未达到最大重试次数,保持pending状态,记录错误信息 | |||
|  | 		if err := h.asyncTaskRepo.UpdateRetryCountAndError(ctx, taskID, newRetryCount, errorMsg); err != nil { | |||
|  | 			h.logger.Error("更新任务重试次数失败",  | |||
|  | 				zap.String("task_id", taskID), | |||
|  | 				zap.Int("retry_count", newRetryCount), | |||
|  | 				zap.Error(err)) | |||
|  | 		} | |||
|  | 		h.logger.Info("任务失败,准备重试",  | |||
|  | 			zap.String("task_id", taskID), | |||
|  | 			zap.Int("retry_count", newRetryCount), | |||
|  | 			zap.Int("max_retries", task.MaxRetries)) | |||
|  | 	} | |||
|  | } | |||
|  | 
 | |||
|  | // checkTaskStatus 检查任务状态 | |||
|  | func (h *ArticleTaskHandler) checkTaskStatus(ctx context.Context, t *asynq.Task) error { | |||
|  | 	// 从任务载荷中提取任务ID | |||
|  | 	var payload map[string]interface{} | |||
|  | 	if err := json.Unmarshal(t.Payload(), &payload); err != nil { | |||
|  | 		h.logger.Error("解析任务载荷失败,无法检查状态", zap.Error(err)) | |||
|  | 		return err | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 尝试从payload中获取任务ID | |||
|  | 	taskID, ok := payload["task_id"].(string) | |||
|  | 	if !ok { | |||
|  | 		// 如果没有task_id,尝试从article_id生成 | |||
|  | 		if articleID, ok := payload["article_id"].(string); ok { | |||
|  | 			taskID = fmt.Sprintf("article-publish-%s", articleID) | |||
|  | 		} else { | |||
|  | 			h.logger.Error("无法从任务载荷中获取任务ID") | |||
|  | 			return fmt.Errorf("无法获取任务ID") | |||
|  | 		} | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 查询任务状态 | |||
|  | 	task, err := h.asyncTaskRepo.GetByID(ctx, taskID) | |||
|  | 	if err != nil { | |||
|  | 		h.logger.Error("查询任务状态失败", zap.String("task_id", taskID), zap.Error(err)) | |||
|  | 		return err | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// 检查任务是否已被取消 | |||
|  | 	if task.Status == entities.TaskStatusCancelled { | |||
|  | 		h.logger.Info("任务已被取消", zap.String("task_id", taskID)) | |||
|  | 		return fmt.Errorf("任务已被取消") | |||
|  | 	} | |||
|  | 
 | |||
|  | 	return nil | |||
|  | } |