mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
框架化
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: "new-async-task"
|
name: "new-async-task"
|
||||||
description: "项目专用:指导如何在本项目中新增异步任务(TaskHandler),包括常量定义、参数传递(TaskParam)、处理器实现、Admin 校验、注册、Cron 调度、AppendLog 日志规范等完整步骤。"
|
description: "项目专用:指导如何在本项目中新增异步任务(TaskHandler),包括常量定义、参数传递(TaskParam)、PayloadValidator 接口、处理器实现、注册、Cron 调度、AppendLog 日志规范等完整步骤。"
|
||||||
---
|
---
|
||||||
|
|
||||||
# 新增异步任务开发指南
|
# 新增异步任务开发指南
|
||||||
@@ -26,9 +26,9 @@ TaskHandler.Execute (← 你写这一步)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**关键文件**:
|
**关键文件**:
|
||||||
- `internal/task/handler.go` — `TaskHandler` 接口、`TaskResult` 类型
|
- `internal/task/handler.go` — `TaskHandler` 接口、`TaskResult` 类型、`PayloadValidator` 可选接口
|
||||||
- `internal/task/constants.go` — 任务类型常量、`TaskMeta`、`TaskParam`、`DispatchableTasks`
|
- `internal/task/constants.go` — 任务类型常量、`TaskMeta`、`TaskParam`、`DispatchableTasks`
|
||||||
- `internal/task/executor.go` — 注册、下发、执行、日志追加
|
- `internal/task/executor.go` — 注册、下发、执行、日志追加、参数校验分发
|
||||||
- `internal/task/worker/worker.go` — Worker 启动、处理器注册、路由
|
- `internal/task/worker/worker.go` — Worker 启动、处理器注册、路由
|
||||||
- `internal/task/scheduler/scheduler.go` — Cron 定时调度
|
- `internal/task/scheduler/scheduler.go` — Cron 定时调度
|
||||||
- `internal/model/task_execution.go` — `TaskExecution` GORM 模型
|
- `internal/model/task_execution.go` — `TaskExecution` GORM 模型
|
||||||
@@ -119,13 +119,15 @@ const TaskTypeCleanupUploads = "cleanup_unused_uploads"
|
|||||||
| `Placeholder` | 前端输入框占位文字 |
|
| `Placeholder` | 前端输入框占位文字 |
|
||||||
| `Description` | 前端显示的参数说明 |
|
| `Description` | 前端显示的参数说明 |
|
||||||
|
|
||||||
**注意**:`TaskParam` 纯粹是前端表单元数据。服务端不基于它做参数校验——校验逻辑在 Admin dispatch handler 和 Handler Execute 中各写一份。
|
**注意**:`TaskParam` 纯粹是前端表单元数据。服务端参数校验通过 Handler 实现 `PayloadValidator` 接口完成(见第 2 步)。
|
||||||
|
|
||||||
### 第 2 步:实现 TaskHandler
|
### 第 2 步:实现 TaskHandler(+ 可选 PayloadValidator)
|
||||||
|
|
||||||
在 `internal/apps/<module>/` 下创建 `tasks.go`。
|
在 `internal/apps/<module>/` 下创建 `tasks.go`。
|
||||||
|
|
||||||
**无参数的任务**(参考 `internal/apps/upload/tasks.go`):
|
#### 无参数的任务(参考 `internal/apps/upload/tasks.go`)
|
||||||
|
|
||||||
|
只需实现 `TaskHandler` 接口:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type CleanupUnusedUploadsHandler struct{}
|
type CleanupUnusedUploadsHandler struct{}
|
||||||
@@ -137,9 +139,9 @@ func (h *CleanupUnusedUploadsHandler) Execute(ctx context.Context, payload []byt
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**带参数的任务**(参考 `internal/apps/user/tasks.go`):
|
#### 带参数的任务(参考 `internal/apps/user/tasks.go`)
|
||||||
|
|
||||||
需要定义自己的 Payload 结构体,在 `Execute` 中反序列化:
|
需要定义 Payload 结构体,并实现 `PayloadValidator` 接口进行参数校验:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 定义载荷结构体(字段与 TaskParam.Name 对应)
|
// 定义载荷结构体(字段与 TaskParam.Name 对应)
|
||||||
@@ -151,16 +153,40 @@ type SendEmailPayload struct {
|
|||||||
|
|
||||||
type SendEmailHandler struct{}
|
type SendEmailHandler struct{}
|
||||||
|
|
||||||
func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.TaskResult, error) {
|
// ValidatePayload 实现 task.PayloadValidator 接口
|
||||||
// 第一步:反序列化参数
|
// 框架在 Admin 下发时自动调用,校验失败返回 error,前端收到 400 + 错误信息
|
||||||
|
func (h *SendEmailHandler) ValidatePayload(payload []byte) ([]byte, error) {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil, errors.New("任务参数不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SendEmailPayload
|
||||||
|
if err := json.Unmarshal(payload, &req); err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的 JSON 格式: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标准化:Trim 空白
|
||||||
|
req.To = strings.TrimSpace(req.To)
|
||||||
|
req.Subject = strings.TrimSpace(req.Subject)
|
||||||
|
req.Body = strings.TrimSpace(req.Body)
|
||||||
|
|
||||||
|
// 校验必填字段
|
||||||
|
if req.To == "" || req.Subject == "" || req.Body == "" {
|
||||||
|
return nil, errors.New("to、subject、body 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回标准化后的 bytes,会作为最终 payload 存入 DB 和入队
|
||||||
|
return json.Marshal(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute 实现 task.TaskHandler 接口
|
||||||
|
func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.TaskResult, error) {
|
||||||
var req SendEmailPayload
|
var req SendEmailPayload
|
||||||
if err := json.Unmarshal(payload, &req); err != nil {
|
if err := json.Unmarshal(payload, &req); err != nil {
|
||||||
task.AppendLog(ctx, "解析参数失败: %v", err)
|
|
||||||
return nil, fmt.Errorf("解析参数失败: %w", err)
|
return nil, fmt.Errorf("解析参数失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
task.AppendLog(ctx, "开始发送邮件到: %s, 主题: %s", req.To, req.Subject)
|
task.AppendLog(ctx, "开始发送邮件到: %s, 主题: %s", req.To, req.Subject)
|
||||||
|
|
||||||
// ... 业务逻辑 ...
|
// ... 业务逻辑 ...
|
||||||
|
|
||||||
msg := fmt.Sprintf("邮件成功发送至: %s", req.To)
|
msg := fmt.Sprintf("邮件成功发送至: %s", req.To)
|
||||||
@@ -169,77 +195,28 @@ func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.T
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**`PayloadValidator` 接口说明**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type PayloadValidator interface {
|
||||||
|
ValidatePayload(payload []byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这是一个**可选接口**。框架在 Admin 下发任务时通过 `task.ValidateAndNormalizePayload` 自动检测:
|
||||||
|
|
||||||
|
- Handler 实现了 `PayloadValidator` → 调用 `ValidatePayload`,校验通过返回标准化后的 bytes,失败返回 error(前端收到 400)
|
||||||
|
- Handler 没实现 → 直接透传原始 payload,不做任何校验
|
||||||
|
|
||||||
|
**不需要修改 Admin dispatch handler**。`routers.go` 中的 `DispatchTask` 函数已经是通用的,会自动调用对应 Handler 的 `ValidatePayload`。
|
||||||
|
|
||||||
|
**`Execute` 中仍然需要 `json.Unmarshal`**,因为任务可能通过 Scheduler 或重试触发,不经过 Admin 校验路径。但不需要重复必填校验。
|
||||||
|
|
||||||
**返回值约定**:
|
**返回值约定**:
|
||||||
- 成功:返回 `&task.TaskResult{Message: "摘要", Detail: "可选详细JSON"}`。`ProcessTask` 会将 `Message + "\n" + Detail` 存入 `TaskExecution.Result`。
|
- 成功:返回 `&task.TaskResult{Message: "摘要", Detail: "可选详细JSON"}`。`ProcessTask` 会将 `Message + "\n" + Detail` 存入 `TaskExecution.Result`。
|
||||||
- 失败:返回 `nil, fmt.Errorf("...")`。`ProcessTask` 会将错误信息存入 `TaskExecution.ErrorMessage`,状态标记为 `failed`,并将 error 返回给 Asynq 触发自动重试。
|
- 失败:返回 `nil, fmt.Errorf("...")`。`ProcessTask` 会将错误信息存入 `TaskExecution.ErrorMessage`,状态标记为 `failed`,并将 error 返回给 Asynq 触发自动重试。
|
||||||
|
|
||||||
### 第 3 步:在 Admin dispatch handler 中添加参数校验
|
### 第 3 步:在 worker.go 注册
|
||||||
|
|
||||||
打开 `internal/apps/admin/task/routers.go`,在 `DispatchTask` 函数中为新任务类型添加校验逻辑。
|
|
||||||
|
|
||||||
下发请求结构体中,前端传入的参数通过 `Payload` 字段(JSON 字符串)传递:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type DispatchTaskRequest struct {
|
|
||||||
TaskType string `json:"task_type" binding:"required"`
|
|
||||||
StartTime *time.Time `json:"start_time"`
|
|
||||||
EndTime *time.Time `json:"end_time"`
|
|
||||||
UserID *uint64 `json:"user_id"`
|
|
||||||
Payload string `json:"payload"` // ← 前端传的参数 JSON 字符串
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
在 `DispatchTask` 函数中按 task type 分支校验:
|
|
||||||
|
|
||||||
```go
|
|
||||||
var payloadBytes []byte
|
|
||||||
if req.TaskType == task.TaskTypeSendEmail {
|
|
||||||
// 校验 payload 非空
|
|
||||||
if strings.TrimSpace(req.Payload) == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, util.Err("任务参数 Payload 不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 解析 JSON 并校验必填字段
|
|
||||||
var mailPayload struct {
|
|
||||||
To string `json:"to"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Body string `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(req.Payload), &mailPayload); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, util.Err("无效的 JSON 格式: "+err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Trim + 必填校验
|
|
||||||
mailPayload.To = strings.TrimSpace(mailPayload.To)
|
|
||||||
mailPayload.Subject = strings.TrimSpace(mailPayload.Subject)
|
|
||||||
mailPayload.Body = strings.TrimSpace(mailPayload.Body)
|
|
||||||
if mailPayload.To == "" || mailPayload.Subject == "" || mailPayload.Body == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, util.Err("to、subject、body 不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 序列化回 bytes 传给 DispatchTask
|
|
||||||
payloadBytes, _ = json.Marshal(mailPayload)
|
|
||||||
} else {
|
|
||||||
// 无参数任务:直接透传 payload
|
|
||||||
if req.Payload != "" {
|
|
||||||
payloadBytes = []byte(req.Payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
taskID, err := task.DispatchTask(c.Request.Context(), req.TaskType, payloadBytes, "manual")
|
|
||||||
```
|
|
||||||
|
|
||||||
**参数传递的完整链路**:
|
|
||||||
|
|
||||||
```
|
|
||||||
前端表单(根据 TaskMeta.Params 动态渲染)
|
|
||||||
→ POST /api/v1/admin/tasks/dispatch { task_type, payload: JSON字符串 }
|
|
||||||
→ Admin DispatchTask handler 校验 + json.Marshal
|
|
||||||
→ task.DispatchTask(payloadBytes) 存入 DB + 入队 Asynq
|
|
||||||
→ ProcessTask → handler.Execute(payload) → handler 内 json.Unmarshal
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第 4 步:在 worker.go 注册
|
|
||||||
|
|
||||||
打开 `internal/task/worker/worker.go`,做两件事:
|
打开 `internal/task/worker/worker.go`,做两件事:
|
||||||
|
|
||||||
@@ -263,7 +240,7 @@ mux.HandleFunc(task.SendEmailTask, task.ProcessTask)
|
|||||||
|
|
||||||
所有路由都指向同一个 `task.ProcessTask`,它内部根据 Asynq task type 查找对应的 handler 分发执行。
|
所有路由都指向同一个 `task.ProcessTask`,它内部根据 Asynq task type 查找对应的 handler 分发执行。
|
||||||
|
|
||||||
### 第 5 步(可选):添加 Cron 定时调度
|
### 第 4 步(可选):添加 Cron 定时调度
|
||||||
|
|
||||||
如果任务需要定时执行,编辑 `internal/task/scheduler/scheduler.go`,在 `StartScheduler()` 中注册:
|
如果任务需要定时执行,编辑 `internal/task/scheduler/scheduler.go`,在 `StartScheduler()` 中注册:
|
||||||
|
|
||||||
@@ -297,6 +274,21 @@ scheduler:
|
|||||||
|
|
||||||
**注意**:通过 Scheduler 定时触发的任务不会创建 `TaskExecution` 记录(直接入队 Asynq)。`ProcessTask` 执行时若在数据库中找不到对应记录,会打印警告日志但仍会正常执行业务逻辑。如果需要状态追踪,应通过 Admin API 手动下发。
|
**注意**:通过 Scheduler 定时触发的任务不会创建 `TaskExecution` 记录(直接入队 Asynq)。`ProcessTask` 执行时若在数据库中找不到对应记录,会打印警告日志但仍会正常执行业务逻辑。如果需要状态追踪,应通过 Admin API 手动下发。
|
||||||
|
|
||||||
|
## 参数校验的完整链路
|
||||||
|
|
||||||
|
```
|
||||||
|
前端表单(根据 TaskMeta.Params 动态渲染)
|
||||||
|
→ POST /api/v1/admin/tasks/dispatch { task_type, payload: JSON字符串 }
|
||||||
|
→ task.ValidateAndNormalizePayload(asynqTaskType, payload)
|
||||||
|
→ Handler 实现了 PayloadValidator?
|
||||||
|
→ 是:调用 ValidatePayload(),校验 + 标准化
|
||||||
|
→ 否:直接透传
|
||||||
|
→ task.DispatchTask(validatedPayload) 存入 DB + 入队 Asynq
|
||||||
|
→ ProcessTask → handler.Execute(payload) → handler 内 json.Unmarshal 使用参数
|
||||||
|
```
|
||||||
|
|
||||||
|
关键点:Admin dispatch handler (`routers.go`) 是**通用的**,不需要按任务类型写 if 分支。新增带参数的任务时,只需在 Handler 上实现 `ValidatePayload` 方法即可。
|
||||||
|
|
||||||
## AppendLog 使用指南
|
## AppendLog 使用指南
|
||||||
|
|
||||||
`task.AppendLog(ctx, format string, args ...interface{})` 在 `TaskHandler.Execute` 内部调用,将日志追加到 `TaskExecution.Log` 字段。管理端 API 可查看完整执行日志。
|
`task.AppendLog(ctx, format string, args ...interface{})` 在 `TaskHandler.Execute` 内部调用,将日志追加到 `TaskExecution.Log` 字段。管理端 API 可查看完整执行日志。
|
||||||
@@ -320,8 +312,9 @@ scheduler:
|
|||||||
|
|
||||||
## 框架自动处理的事项
|
## 框架自动处理的事项
|
||||||
|
|
||||||
开发者不需要关心以下内容,全部由 `ProcessTask` 框架层透明处理:
|
开发者不需要关心以下内容,全部由框架透明处理:
|
||||||
|
|
||||||
|
- **参数校验分发**:`ValidateAndNormalizePayload` 自动检测 Handler 是否实现 `PayloadValidator`,实现了就调用,否则透传
|
||||||
- **记录创建**:`DispatchTask` 自动创建 `TaskExecution` 记录,生成唯一 `TaskID`(格式 `{triggeredBy}_{taskType}_{snowflakeID}`)
|
- **记录创建**:`DispatchTask` 自动创建 `TaskExecution` 记录,生成唯一 `TaskID`(格式 `{triggeredBy}_{taskType}_{snowflakeID}`)
|
||||||
- **状态流转**:`pending` → `running` → `succeeded`/`failed`
|
- **状态流转**:`pending` → `running` → `succeeded`/`failed`
|
||||||
- **耗时统计**:自动记录 `StartedAt`、`FinishedAt`、`Duration`(毫秒)
|
- **耗时统计**:自动记录 `StartedAt`、`FinishedAt`、`Duration`(毫秒)
|
||||||
@@ -343,5 +336,5 @@ scheduler:
|
|||||||
## 参考:现有任务处理器
|
## 参考:现有任务处理器
|
||||||
|
|
||||||
- `internal/apps/upload/tasks.go` — `CleanupUnusedUploadsHandler`:无参数任务,展示游标分页批量处理、每批次 AppendLog、事务内操作、单条失败 continue 不终止。
|
- `internal/apps/upload/tasks.go` — `CleanupUnusedUploadsHandler`:无参数任务,展示游标分页批量处理、每批次 AppendLog、事务内操作、单条失败 continue 不终止。
|
||||||
- `internal/apps/user/tasks.go` — `SendEmailHandler`:带参数任务,展示 Payload 结构体定义、json.Unmarshal 反序列化、参数日志记录。
|
- `internal/apps/user/tasks.go` — `SendEmailHandler`:带参数任务,展示 Payload 结构体定义、`PayloadValidator` 实现(`ValidatePayload` 用于校验+标准化)、`Execute` 中 json.Unmarshal 使用参数。
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
package task
|
package task
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -81,44 +80,17 @@ func DispatchTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var payloadBytes []byte
|
var payloadBytes []byte
|
||||||
if req.TaskType == task.TaskTypeSendEmail {
|
if strings.TrimSpace(req.Payload) != "" {
|
||||||
if strings.TrimSpace(req.Payload) == "" {
|
payloadBytes = []byte(req.Payload)
|
||||||
c.JSON(http.StatusBadRequest, util.Err("任务参数 Payload 不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var mailPayload struct {
|
|
||||||
To string `json:"to"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Body string `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(req.Payload), &mailPayload); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, util.Err("无效的 JSON 格式: "+err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mailPayload.To = strings.TrimSpace(mailPayload.To)
|
|
||||||
mailPayload.Subject = strings.TrimSpace(mailPayload.Subject)
|
|
||||||
mailPayload.Body = strings.TrimSpace(mailPayload.Body)
|
|
||||||
|
|
||||||
if mailPayload.To == "" || mailPayload.Subject == "" || mailPayload.Body == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, util.Err("收件人地址 (to)、邮件主题 (subject) 和邮件内容 (body) 不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
payloadBytes, err = json.Marshal(mailPayload)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, util.Err("构造邮件任务载荷失败"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if req.Payload != "" {
|
|
||||||
payloadBytes = []byte(req.Payload)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
taskID, err := task.DispatchTask(c.Request.Context(), req.TaskType, payloadBytes, "manual")
|
validated, err := task.ValidateAndNormalizePayload(meta.AsynqTask, payloadBytes)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskID, err := task.DispatchTask(c.Request.Context(), req.TaskType, validated, "manual")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", TaskDispatchFailed, err)))
|
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", TaskDispatchFailed, err)))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/linux-do/credit/internal/model"
|
"github.com/linux-do/credit/internal/model"
|
||||||
"github.com/linux-do/credit/internal/task"
|
"github.com/linux-do/credit/internal/task"
|
||||||
@@ -38,6 +39,29 @@ type SendEmailPayload struct {
|
|||||||
// SendEmailHandler 发送验证码邮件的异步任务处理器
|
// SendEmailHandler 发送验证码邮件的异步任务处理器
|
||||||
type SendEmailHandler struct{}
|
type SendEmailHandler struct{}
|
||||||
|
|
||||||
|
// ValidatePayload 实现 task.PayloadValidator 接口
|
||||||
|
// 校验并标准化邮件发送参数,框架在 Admin 下发时自动调用
|
||||||
|
func (h *SendEmailHandler) ValidatePayload(payload []byte) ([]byte, error) {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil, errors.New("任务参数不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SendEmailPayload
|
||||||
|
if err := json.Unmarshal(payload, &req); err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的 JSON 格式: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.To = strings.TrimSpace(req.To)
|
||||||
|
req.Subject = strings.TrimSpace(req.Subject)
|
||||||
|
req.Body = strings.TrimSpace(req.Body)
|
||||||
|
|
||||||
|
if req.To == "" || req.Subject == "" || req.Body == "" {
|
||||||
|
return nil, errors.New("to、subject、body 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(req)
|
||||||
|
}
|
||||||
|
|
||||||
// Execute 执行邮件异步发送逻辑
|
// Execute 执行邮件异步发送逻辑
|
||||||
func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.TaskResult, error) {
|
func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.TaskResult, error) {
|
||||||
var req SendEmailPayload
|
var req SendEmailPayload
|
||||||
|
|||||||
@@ -46,6 +46,20 @@ func getHandler(asynqTaskType string) (TaskHandler, bool) {
|
|||||||
return h, ok
|
return h, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateAndNormalizePayload 校验并标准化任务参数。
|
||||||
|
// 如果 Handler 实现了 PayloadValidator,调用其 ValidatePayload 方法;
|
||||||
|
// 否则直接返回原始 payload。
|
||||||
|
func ValidateAndNormalizePayload(asynqTaskType string, payload []byte) ([]byte, error) {
|
||||||
|
handler, ok := getHandler(asynqTaskType)
|
||||||
|
if !ok {
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
if validator, ok := handler.(PayloadValidator); ok {
|
||||||
|
return validator.ValidatePayload(payload)
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
// contextKey 用于 context 存取 taskID
|
// contextKey 用于 context 存取 taskID
|
||||||
type contextKey string
|
type contextKey string
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ type TaskResult struct {
|
|||||||
Detail string // 可选的详细结果 JSON
|
Detail string // 可选的详细结果 JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PayloadValidator 可选接口,带参数的任务 Handler 应实现此接口。
|
||||||
|
// 框架在 Admin 下发时自动调用,完成参数校验和标准化(如 Trim 空白)。
|
||||||
|
// 无参数的任务无需实现,框架会直接透传 payload。
|
||||||
|
type PayloadValidator interface {
|
||||||
|
ValidatePayload(payload []byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
// TaskHandler 异步任务处理器接口
|
// TaskHandler 异步任务处理器接口
|
||||||
// 所有异步任务必须实现此接口,框架将自动管理任务执行记录的创建、状态流转和日志写入。
|
// 所有异步任务必须实现此接口,框架将自动管理任务执行记录的创建、状态流转和日志写入。
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user