mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
邮箱
This commit is contained in:
+203
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 等后端存储读取并流式返回
|
||||
|
||||
@@ -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 || '',
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "发送邮件的内容主体",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user