This commit is contained in:
ryan
2026-06-08 14:57:19 +08:00
parent 72c74803be
commit f136c9abdb
9 changed files with 821 additions and 36 deletions
+203
View File
@@ -640,6 +640,63 @@ const docTemplate = `{
}
}
},
"/api/v1/admin/system-configs/smtp/test": {
"post": {
"security": [
{
"SessionCookie": []
}
],
"description": "使用传入的配置进行 SMTP 邮件发送测试,支持使用 ****** 占位符使用保存的数据库密码",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "测试 SMTP 邮件发送",
"parameters": [
{
"description": "测试请求参数",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/system_config.TestSMTPRequest"
}
}
],
"responses": {
"200": {
"description": "测试执行完毕",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.ResponseAny"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/system_config.TestSMTPResponse"
}
}
}
]
}
},
"400": {
"description": "参数错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
}
}
}
},
"/api/v1/admin/system-configs/{key}": {
"get": {
"security": [
@@ -2647,6 +2704,46 @@ const docTemplate = `{
}
}
},
"/api/v1/user/send-email-code": {
"post": {
"description": "向指定邮箱发送验证码(用于注册场景)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "发送邮箱验证码",
"parameters": [
{
"description": "发送验证码请求参数",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/user.sendEmailCodeRequest"
}
}
],
"responses": {
"200": {
"description": "发送成功",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"400": {
"description": "参数错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
}
}
}
},
"/f/{id}": {
"get": {
"description": "根据文件 ID 获取并提供已上传的临时或正式文件,若配置了缓存则优先走本地缓存,否则从 S3 等后端存储读取并流式返回",
@@ -3326,6 +3423,50 @@ const docTemplate = `{
}
}
},
"system_config.TestSMTPRequest": {
"type": "object",
"required": [
"smtp_host",
"smtp_password",
"smtp_port",
"smtp_username",
"to"
],
"properties": {
"smtp_host": {
"type": "string",
"maxLength": 255
},
"smtp_password": {
"type": "string",
"maxLength": 255
},
"smtp_port": {
"type": "integer"
},
"smtp_username": {
"type": "string",
"maxLength": 255
},
"to": {
"type": "string"
}
}
},
"system_config.TestSMTPResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"log": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"system_config.UpdateSystemConfigRequest": {
"type": "object",
"required": [
@@ -3351,6 +3492,9 @@ const docTemplate = `{
"end_time": {
"type": "string"
},
"payload": {
"type": "string"
},
"start_time": {
"type": "string"
},
@@ -3377,6 +3521,12 @@ const docTemplate = `{
"name": {
"type": "string"
},
"params": {
"type": "array",
"items": {
"$ref": "#/definitions/task.TaskParam"
}
},
"queue": {
"type": "string"
},
@@ -3392,6 +3542,35 @@ const docTemplate = `{
}
}
},
"task.TaskParam": {
"type": "object",
"properties": {
"Description": {
"description": "描述",
"type": "string"
},
"Label": {
"description": "显示名称",
"type": "string"
},
"Name": {
"description": "参数键名",
"type": "string"
},
"Placeholder": {
"description": "占位符",
"type": "string"
},
"Required": {
"description": "是否必填",
"type": "boolean"
},
"Type": {
"description": "类型:string, text, number",
"type": "string"
}
}
},
"upload.batchDownloadRequest": {
"type": "object",
"required": [
@@ -3492,6 +3671,9 @@ const docTemplate = `{
"user.loginRequest": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"password": {
"type": "string"
},
@@ -3503,9 +3685,15 @@ const docTemplate = `{
"user.registerRequest": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"display_name": {
"type": "string"
},
"email": {
"type": "string"
},
"nickname": {
"type": "string"
},
@@ -3517,6 +3705,21 @@ const docTemplate = `{
}
}
},
"user.sendEmailCodeRequest": {
"type": "object",
"required": [
"email",
"scene"
],
"properties": {
"email": {
"type": "string"
},
"scene": {
"type": "string"
}
}
},
"user.tokenResponse": {
"type": "object",
"properties": {
+203
View File
@@ -633,6 +633,63 @@
}
}
},
"/api/v1/admin/system-configs/smtp/test": {
"post": {
"security": [
{
"SessionCookie": []
}
],
"description": "使用传入的配置进行 SMTP 邮件发送测试,支持使用 ****** 占位符使用保存的数据库密码",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "测试 SMTP 邮件发送",
"parameters": [
{
"description": "测试请求参数",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/system_config.TestSMTPRequest"
}
}
],
"responses": {
"200": {
"description": "测试执行完毕",
"schema": {
"allOf": [
{
"$ref": "#/definitions/util.ResponseAny"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/system_config.TestSMTPResponse"
}
}
}
]
}
},
"400": {
"description": "参数错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
}
}
}
},
"/api/v1/admin/system-configs/{key}": {
"get": {
"security": [
@@ -2640,6 +2697,46 @@
}
}
},
"/api/v1/user/send-email-code": {
"post": {
"description": "向指定邮箱发送验证码(用于注册场景)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "发送邮箱验证码",
"parameters": [
{
"description": "发送验证码请求参数",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/user.sendEmailCodeRequest"
}
}
],
"responses": {
"200": {
"description": "发送成功",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
},
"400": {
"description": "参数错误",
"schema": {
"$ref": "#/definitions/util.ResponseAny"
}
}
}
}
},
"/f/{id}": {
"get": {
"description": "根据文件 ID 获取并提供已上传的临时或正式文件,若配置了缓存则优先走本地缓存,否则从 S3 等后端存储读取并流式返回",
@@ -3319,6 +3416,50 @@
}
}
},
"system_config.TestSMTPRequest": {
"type": "object",
"required": [
"smtp_host",
"smtp_password",
"smtp_port",
"smtp_username",
"to"
],
"properties": {
"smtp_host": {
"type": "string",
"maxLength": 255
},
"smtp_password": {
"type": "string",
"maxLength": 255
},
"smtp_port": {
"type": "integer"
},
"smtp_username": {
"type": "string",
"maxLength": 255
},
"to": {
"type": "string"
}
}
},
"system_config.TestSMTPResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"log": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"system_config.UpdateSystemConfigRequest": {
"type": "object",
"required": [
@@ -3344,6 +3485,9 @@
"end_time": {
"type": "string"
},
"payload": {
"type": "string"
},
"start_time": {
"type": "string"
},
@@ -3370,6 +3514,12 @@
"name": {
"type": "string"
},
"params": {
"type": "array",
"items": {
"$ref": "#/definitions/task.TaskParam"
}
},
"queue": {
"type": "string"
},
@@ -3385,6 +3535,35 @@
}
}
},
"task.TaskParam": {
"type": "object",
"properties": {
"Description": {
"description": "描述",
"type": "string"
},
"Label": {
"description": "显示名称",
"type": "string"
},
"Name": {
"description": "参数键名",
"type": "string"
},
"Placeholder": {
"description": "占位符",
"type": "string"
},
"Required": {
"description": "是否必填",
"type": "boolean"
},
"Type": {
"description": "类型:string, text, number",
"type": "string"
}
}
},
"upload.batchDownloadRequest": {
"type": "object",
"required": [
@@ -3485,6 +3664,9 @@
"user.loginRequest": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"password": {
"type": "string"
},
@@ -3496,9 +3678,15 @@
"user.registerRequest": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"display_name": {
"type": "string"
},
"email": {
"type": "string"
},
"nickname": {
"type": "string"
},
@@ -3510,6 +3698,21 @@
}
}
},
"user.sendEmailCodeRequest": {
"type": "object",
"required": [
"email",
"scene"
],
"properties": {
"email": {
"type": "string"
},
"scene": {
"type": "string"
}
}
},
"user.tokenResponse": {
"type": "object",
"properties": {
+133
View File
@@ -424,6 +424,37 @@ definitions:
- type
- value
type: object
system_config.TestSMTPRequest:
properties:
smtp_host:
maxLength: 255
type: string
smtp_password:
maxLength: 255
type: string
smtp_port:
type: integer
smtp_username:
maxLength: 255
type: string
to:
type: string
required:
- smtp_host
- smtp_password
- smtp_port
- smtp_username
- to
type: object
system_config.TestSMTPResponse:
properties:
error:
type: string
log:
type: string
success:
type: boolean
type: object
system_config.UpdateSystemConfigRequest:
properties:
description:
@@ -439,6 +470,8 @@ definitions:
properties:
end_time:
type: string
payload:
type: string
start_time:
type: string
task_type:
@@ -458,6 +491,10 @@ definitions:
type: integer
name:
type: string
params:
items:
$ref: '#/definitions/task.TaskParam'
type: array
queue:
type: string
retryable:
@@ -468,6 +505,27 @@ definitions:
type:
type: string
type: object
task.TaskParam:
properties:
Description:
description: 描述
type: string
Label:
description: 显示名称
type: string
Name:
description: 参数键名
type: string
Placeholder:
description: 占位符
type: string
Required:
description: 是否必填
type: boolean
Type:
description: 类型:string, text, number
type: string
type: object
upload.batchDownloadRequest:
properties:
ids:
@@ -535,6 +593,8 @@ definitions:
type: object
user.loginRequest:
properties:
code:
type: string
password:
type: string
username:
@@ -542,8 +602,12 @@ definitions:
type: object
user.registerRequest:
properties:
code:
type: string
display_name:
type: string
email:
type: string
nickname:
type: string
password:
@@ -551,6 +615,16 @@ definitions:
username:
type: string
type: object
user.sendEmailCodeRequest:
properties:
email:
type: string
scene:
type: string
required:
- email
- scene
type: object
user.tokenResponse:
properties:
record:
@@ -1111,6 +1185,39 @@ paths:
summary: 更新系统配置
tags:
- admin
/api/v1/admin/system-configs/smtp/test:
post:
consumes:
- application/json
description: 使用传入的配置进行 SMTP 邮件发送测试,支持使用 ****** 占位符使用保存的数据库密码
parameters:
- description: 测试请求参数
in: body
name: request
required: true
schema:
$ref: '#/definitions/system_config.TestSMTPRequest'
produces:
- application/json
responses:
"200":
description: 测试执行完毕
schema:
allOf:
- $ref: '#/definitions/util.ResponseAny'
- properties:
data:
$ref: '#/definitions/system_config.TestSMTPResponse'
type: object
"400":
description: 参数错误
schema:
$ref: '#/definitions/util.ResponseAny'
security:
- SessionCookie: []
summary: 测试 SMTP 邮件发送
tags:
- admin
/api/v1/admin/tasks/dispatch:
post:
consumes:
@@ -2176,6 +2283,32 @@ paths:
summary: 用户注册
tags:
- user
/api/v1/user/send-email-code:
post:
consumes:
- application/json
description: 向指定邮箱发送验证码(用于注册场景)
parameters:
- description: 发送验证码请求参数
in: body
name: request
required: true
schema:
$ref: '#/definitions/user.sendEmailCodeRequest'
produces:
- application/json
responses:
"200":
description: 发送成功
schema:
$ref: '#/definitions/util.ResponseAny'
"400":
description: 参数错误
schema:
$ref: '#/definitions/util.ResponseAny'
summary: 发送邮箱验证码
tags:
- user
/f/{id}:
get:
description: 根据文件 ID 获取并提供已上传的临时或正式文件,若配置了缓存则优先走本地缓存,否则从 S3 等后端存储读取并流式返回
+107 -35
View File
@@ -6,6 +6,7 @@ import {toast} from "sonner"
import {Button} from "@/components/ui/button"
import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {Textarea} from "@/components/ui/textarea"
import {Spinner} from "@/components/ui/spinner"
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
import {Calendar as CalendarIcon, Clock, FileText, Info, Layers, Play} from "lucide-react"
@@ -140,6 +141,7 @@ export function TaskManager() {
const [startTime, setStartTime] = useState<Date | undefined>(undefined)
const [endTime, setEndTime] = useState<Date | undefined>(undefined)
const [userId, setUserId] = useState("")
const [paramValues, setParamValues] = useState<Record<string, string>>({})
const fetchTaskTypes = useCallback(async () => {
try {
@@ -158,6 +160,21 @@ export function TaskManager() {
fetchTaskTypes()
}, [fetchTaskTypes])
useEffect(() => {
if (selectedTaskType) {
const targetTask = taskTypes.find(t => t.type === selectedTaskType)
if (targetTask?.params) {
const initialValues: Record<string, string> = {}
targetTask.params.forEach(p => {
initialValues[p.name] = ""
})
setParamValues(initialValues)
} else {
setParamValues({})
}
}
}, [selectedTaskType, taskTypes])
const handleDispatch = async () => {
if (!selectedTaskType) return
@@ -170,7 +187,7 @@ export function TaskManager() {
task_type: selectedTaskType
}
if (targetTask?.type === 'order_sync') {
if (targetTask?.supports_time) {
if (startTime) params.start_time = startTime.toISOString()
if (endTime) params.end_time = endTime.toISOString()
}
@@ -179,6 +196,21 @@ export function TaskManager() {
if (userId) params.user_id = userId
}
// Handle dynamic parameters
if (targetTask?.params && targetTask.params.length > 0) {
const payloadData: Record<string, string> = {}
for (const param of targetTask.params) {
const val = (paramValues[param.name] || "").trim()
if (param.required && !val) {
toast.error(`${param.label}不能为空`)
setDispatching(false)
return
}
payloadData[param.name] = val
}
params.payload = JSON.stringify(payloadData)
}
const taskID = await AdminService.dispatchTask(params)
toast.success('任务下发成功', {
@@ -189,6 +221,7 @@ export function TaskManager() {
setStartTime(undefined)
setEndTime(undefined)
setUserId("")
setParamValues({})
} catch (err) {
toast.error('任务下发失败', {
description: err instanceof Error ? err.message : '未知错误'
@@ -318,42 +351,81 @@ export function TaskManager() {
</div>
</div>
{selectedTaskType === 'order_sync' && (
<>
<div className="grid gap-2">
<Label>开始时间</Label>
<DatePickerWithTime date={startTime} setDate={setStartTime} />
<p className="text-xs text-muted-foreground">选择同步的起始时间点</p>
</div>
<div className="grid gap-2">
<Label>结束时间</Label>
<DatePickerWithTime date={endTime} setDate={setEndTime} />
<p className="text-xs text-muted-foreground">选择同步的结束时间点</p>
</div>
</>
)}
{(() => {
const targetTask = getSelectedTaskMeta()
if (targetTask?.supports_time) {
return (
<>
<div className="grid gap-2">
<Label>开始时间</Label>
<DatePickerWithTime date={startTime} setDate={setStartTime} />
<p className="text-xs text-muted-foreground">选择同步的起始时间点</p>
</div>
<div className="grid gap-2">
<Label>结束时间</Label>
<DatePickerWithTime date={endTime} setDate={setEndTime} />
<p className="text-xs text-muted-foreground">选择同步的结束时间点</p>
</div>
</>
)
}
return null
})()}
{selectedTaskType === 'user_gamification' && (
<div className="grid gap-2">
<Label htmlFor="user-id">用户 ID</Label>
<Input
id="user-id"
type="number"
placeholder="e.g. 10086"
className="font-mono text-xs"
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<p className="text-xs text-muted-foreground">指定需要更新积分数据的用户 ID</p>
</div>
)}
{(() => {
const targetTask = getSelectedTaskMeta()
if (!targetTask || !targetTask.params || targetTask.params.length === 0) {
return null
}
return (
<div className="space-y-4">
{targetTask.params.map((param) => (
<div key={param.name} className="grid gap-2">
<Label htmlFor={`param-${param.name}`} className="flex items-center gap-1">
{param.label}
{param.required && <span className="text-destructive font-bold">*</span>}
</Label>
{param.type === 'text' ? (
<Textarea
id={`param-${param.name}`}
placeholder={param.placeholder}
className="text-xs min-h-[80px]"
value={paramValues[param.name] || ""}
onChange={(e) => setParamValues(prev => ({ ...prev, [param.name]: e.target.value }))}
/>
) : (
<Input
id={`param-${param.name}`}
type={param.type === 'number' ? 'number' : 'text'}
placeholder={param.placeholder}
className="text-xs"
value={paramValues[param.name] || ""}
onChange={(e) => setParamValues(prev => ({ ...prev, [param.name]: e.target.value }))}
/>
)}
{param.description && (
<p className="text-[10px] text-muted-foreground">{param.description}</p>
)}
</div>
))}
</div>
)
})()}
{!['order_sync', 'user_gamification'].includes(selectedTaskType || '') && (
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 p-3 rounded-md border border-dashed">
<Info className="h-4 w-4" />
<span>此任务无需额外配置参数,确认后将直接下发。</span>
</div>
)}
{(() => {
const targetTask = getSelectedTaskMeta()
const hasTime = targetTask?.supports_time
const hasParams = targetTask?.params && targetTask.params.length > 0
if (!hasTime && !hasParams) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 p-3 rounded-md border border-dashed">
<Info className="h-4 w-4" />
<span>此任务无需额外配置参数,确认后将直接下发。</span>
</div>
)
}
return null
})()}
</div>
<DialogFooter>
@@ -193,6 +193,14 @@ export class AdminService extends BaseService {
supports_time: item.SupportsTime ?? item.supports_time ?? false,
max_retry: item.MaxRetry ?? item.max_retry ?? 0,
queue: item.Queue || item.queue || '',
params: (item.Params || item.params || []).map(p => ({
name: p.Name || p.name || '',
label: p.Label || p.label || '',
type: p.Type || p.type || '',
required: p.Required ?? p.required ?? false,
placeholder: p.Placeholder || p.placeholder || '',
description: p.Description || p.description || '',
})),
}));
}
+36
View File
@@ -46,6 +46,24 @@ export interface UpdateSystemConfigRequest {
// ==================== 任务管理 ====================
/**
* 任务参数响应
*/
export interface TaskParamResponse {
Name?: string;
name?: string;
Label?: string;
label?: string;
Type?: string;
type?: string;
Required?: boolean;
required?: boolean;
Placeholder?: string;
placeholder?: string;
Description?: string;
description?: string;
}
/**
* 任务类型响应
*/
@@ -64,6 +82,20 @@ export interface TaskTypeResponse {
max_retry?: number;
Queue?: string;
queue?: string;
Params?: TaskParamResponse[];
params?: TaskParamResponse[];
}
/**
* 任务参数定义
*/
export interface TaskParam {
name: string;
label: string;
type: string;
required: boolean;
placeholder: string;
description: string;
}
/**
@@ -84,6 +116,8 @@ export interface TaskMeta {
max_retry: number;
/** 队列名称 */
queue: string;
/** 任务所需的自定义参数定义 */
params?: TaskParam[];
}
/**
@@ -98,6 +132,8 @@ export interface DispatchTaskRequest {
end_time?: string;
/** 用户 ID(可选,仅部分任务需要) */
user_id?: string;
/** 任务自定义参数 JSON 字符串 */
payload?: string;
}
export type TaskExecutionStatus = 'pending' | 'running' | 'succeeded' | 'failed';
+41 -1
View File
@@ -17,6 +17,7 @@ limitations under the License.
package task
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
@@ -49,6 +50,7 @@ type DispatchTaskRequest struct {
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
UserID *uint64 `json:"user_id"`
Payload string `json:"payload"`
}
// DispatchTask 下发任务
@@ -78,7 +80,45 @@ func DispatchTask(c *gin.Context) {
return
}
taskID, err := task.DispatchTask(c.Request.Context(), req.TaskType, nil, "manual")
var payloadBytes []byte
if req.TaskType == task.TaskTypeSendEmail {
if strings.TrimSpace(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")
if err != nil {
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", TaskDispatchFailed, err)))
return
+53
View File
@@ -125,6 +125,59 @@ func TestDispatchTask(t *testing.T) {
assert.NotEmpty(t, taskID)
})
t.Run("dispatch send_email task successfully with valid payload", func(t *testing.T) {
payload := DispatchTaskRequest{
TaskType: task.TaskTypeSendEmail,
Payload: `{"to":"receiver@example.com","subject":"Test Subject","body":"Test Body"}`,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "Body: %s", w.Body.String())
var resp util.ResponseAny
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Empty(t, resp.ErrorMsg)
assert.NotNil(t, resp.Data)
})
t.Run("dispatch send_email task failure with invalid payload json", func(t *testing.T) {
payload := DispatchTaskRequest{
TaskType: task.TaskTypeSendEmail,
Payload: `{"to":`,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp util.ResponseAny
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Contains(t, resp.ErrorMsg, "无效的 JSON 格式")
})
t.Run("dispatch send_email task failure with missing fields", func(t *testing.T) {
payload := DispatchTaskRequest{
TaskType: task.TaskTypeSendEmail,
Payload: `{"to":"","subject":"Test","body":"Test"}`,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/admin/tasks/dispatch", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp util.ResponseAny
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Contains(t, resp.ErrorMsg, "不能为空")
})
t.Run("dispatch invalid task type failure", func(t *testing.T) {
payload := DispatchTaskRequest{
TaskType: "invalid_task_type",
+37
View File
@@ -31,6 +31,16 @@ const (
TaskTypeSendEmail = "send_email"
)
// TaskParam 任务参数定义
type TaskParam struct {
Name string `json:"Name"` // 参数键名
Label string `json:"Label"` // 显示名称
Type string `json:"Type"` // 类型:string, text, number
Required bool `json:"Required"` // 是否必填
Placeholder string `json:"Placeholder"` // 占位符
Description string `json:"Description"` // 描述
}
// TaskMeta 任务元数据
type TaskMeta struct {
Type string
@@ -41,6 +51,7 @@ type TaskMeta struct {
MaxRetry int
Queue string
Retryable bool // 是否支持手动重试
Params []TaskParam
}
// DispatchableTasks 可下发的任务列表
@@ -64,6 +75,32 @@ var DispatchableTasks = []TaskMeta{
MaxRetry: 3,
Queue: QueueDefault,
Retryable: true,
Params: []TaskParam{
{
Name: "to",
Label: "接收邮箱 (To)",
Type: "string",
Required: true,
Placeholder: "receiver@example.com",
Description: "接收邮件的目标邮箱地址",
},
{
Name: "subject",
Label: "邮件主题 (Subject)",
Type: "string",
Required: true,
Placeholder: "请输入邮件主题",
Description: "发送邮件的主题标题",
},
{
Name: "body",
Label: "邮件内容 (Body)",
Type: "text",
Required: true,
Placeholder: "请输入邮件内容(支持 HTML 格式)",
Description: "发送邮件的内容主体",
},
},
},
}