refactor(task):重构大模型异步任务日志功能

This commit is contained in:
qian committed 2026-05-16 19:52:08 +08:00
1 parent e53b5abc7f
commit bc058cee45
28 files changed
+537 -429

No files matched your search

+1 -1
View File
@@ -290,7 +290,7 @@ wallets 表包含 `frozen_balance` 字段,用于支付中/退款中的金额
| `aud_` | 审计日志 | 三 | `aud_request_logs` `aud_operation_logs` `aud_sensitive_access_logs` `aud_login_history` |
| `spt_` | 客户支持(工单+帮助+反馈) | 三/六 | `spt_tickets` `spt_replies` `spt_attachments` `spt_categories` `spt_articles` `spt_feedbacks` |
| `ops_` | 运维监控 | 四 | `ops_alert_rules` `ops_alert_events` `ops_system_metrics` |
| `tsk_` | 异步任务 | 四 | `tsk_tasks` `tsk_task_logs` `tsk_async_tasks` |
| `tsk_` | 异步任务 | 四 | `tsk_tasks` `tsk_task_logs` `tsk_model_tasks` |
| `fil_` | 文件存储 | 四 | `fil_files` |
| `opn_` | 开放平台 + Webhook | 五 | `opn_apps` `opn_webhook_configs` `opn_webhook_events` `opn_webhook_delivery_logs` |
| `clg_` | 更新日志 | 六 | `clg_changelogs` |
-1
View File
@@ -196,7 +196,6 @@ type IAdminV1 interface {
TaskList(ctx context.Context, req *v1.TaskListReq) (res *v1.TaskListRes, err error)
TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (res *v1.TaskDetailRes, err error)
TaskCancel(ctx context.Context, req *v1.TaskCancelReq) (res *v1.TaskCancelRes, err error)
TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (res *v1.TaskRetryRes, err error)
TenantCreate(ctx context.Context, req *v1.TenantCreateReq) (res *v1.TenantCreateRes, err error)
TenantList(ctx context.Context, req *v1.TenantListReq) (res *v1.TenantListRes, err error)
TenantGet(ctx context.Context, req *v1.TenantGetReq) (res *v1.TenantGetRes, err error)
+28 -38
View File
@@ -2,65 +2,55 @@ package v1
import "github.com/gogf/gf/v2/frame/g"
// === 任务管理 ===
// === 大模型异步任务管理 ===
type TaskListReq struct {
g.Meta `path:"/tasks" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"任务列表"`
g.Meta `path:"/tasks" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"大模型异步任务列表"`
Page int `json:"page" in:"query" d:"1" v:"min:1" dc:"页码"`
PageSize int `json:"page_size" in:"query" d:"20" v:"min:1|max:100" dc:"每页数量"`
Status string `json:"status" in:"query" dc:"筛选状态"`
Handler string `json:"handler" in:"query" dc:"筛选handler"`
Platform string `json:"platform" in:"query" dc:"筛选平台(sora/kling/midjourney/suno)"`
}
type TaskItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Handler string `json:"handler"`
Status string `json:"status"`
MaxRetries int `json:"max_retries"`
RetryCount int `json:"retry_count"`
ErrorMessage string `json:"error_message,omitempty"`
StartedAt string `json:"started_at,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
ScheduledAt string `json:"scheduled_at,omitempty"`
CreatedAt string `json:"created_at"`
type ModelTaskItem struct {
ID int64 `json:"id"`
PublicTaskID string `json:"public_task_id"`
Platform string `json:"platform"`
Action string `json:"action"`
Status string `json:"status"`
Progress string `json:"progress"`
ModelName string `json:"model_name"`
FailReason string `json:"fail_reason,omitempty"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
ActualCost float64 `json:"actual_cost"`
BillingSettled bool `json:"billing_settled"`
ResultURL string `json:"result_url,omitempty"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
SubmitTime string `json:"submit_time,omitempty"`
FinishTime string `json:"finish_time,omitempty"`
CreatedAt string `json:"created_at"`
}
type TaskListRes struct {
List []TaskItem `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
List []ModelTaskItem `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type TaskDetailReq struct {
g.Meta `path:"/tasks/{id}" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"任务详情"`
g.Meta `path:"/tasks/{id}" method:"get" mime:"json" tags:"管理后台-任务管理" summary:"大模型异步任务详情"`
ID int64 `json:"id" in:"path" v:"required" dc:"任务ID"`
}
type TaskLogItem struct {
Level string `json:"level"`
Message string `json:"message"`
CreatedAt string `json:"created_at"`
}
type TaskDetailRes struct {
Task TaskItem `json:"task"`
RecentLogs []TaskLogItem `json:"recent_logs"`
Task ModelTaskItem `json:"task"`
}
type TaskCancelReq struct {
g.Meta `path:"/tasks/{id}/cancel" method:"post" mime:"json" tags:"管理后台-任务管理" summary:"取消任务"`
g.Meta `path:"/tasks/{id}/cancel" method:"post" mime:"json" tags:"管理后台-任务管理" summary:"取消大模型异步任务"`
ID int64 `json:"id" in:"path" v:"required" dc:"任务ID"`
}
type TaskCancelRes struct{}
type TaskRetryReq struct {
g.Meta `path:"/tasks/{id}/retry" method:"post" mime:"json" tags:"管理后台-任务管理" summary:"重试任务"`
ID int64 `json:"id" in:"path" v:"required" dc:"任务ID"`
}
type TaskRetryRes struct {
TaskID int64 `json:"task_id"`
}
@@ -1,12 +0,0 @@
package admin
import (
"context"
"github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/service"
)
func (c *ControllerV1) TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (res *v1.TaskRetryRes, err error) {
return service.Admin().TaskRetry(ctx, req)
}
+2 -2
View File
@@ -29,9 +29,9 @@ type ChnChannelKeysColumns struct {
LastUsedAt string // 最后使用时间
LastError string // 最后一次错误信息
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
KeyType string // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
TokenExpiresAt string // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
UpdatedAt string // 更新时间
}
// chnChannelKeysColumns holds the columns for the table chn_channel_keys.
@@ -44,9 +44,9 @@ var chnChannelKeysColumns = ChnChannelKeysColumns{
LastUsedAt: "last_used_at",
LastError: "last_error",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
KeyType: "key_type",
TokenExpiresAt: "token_expires_at",
UpdatedAt: "updated_at",
}
// NewChnChannelKeysDao creates and returns a new DAO object for table data access.
+2 -2
View File
@@ -30,12 +30,12 @@ type MdlModelsColumns struct {
MaxOutputTokens string // 最大输出 token 数
Description string // 模型描述
Tags string // 标签(如 reasoning、vision、function_calling)
Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等)
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeprecatedAt string // 标记弃用的时间(NULL表示未弃用)
SunsetDate string // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
ReplacementModel string // 推荐替代模型名(NULL表示无替代)
Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等)
}
// mdlModelsColumns holds the columns for the table mdl_models.
@@ -49,12 +49,12 @@ var mdlModelsColumns = MdlModelsColumns{
MaxOutputTokens: "max_output_tokens",
Description: "description",
Tags: "tags",
Capabilities: "capabilities",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeprecatedAt: "deprecated_at",
SunsetDate: "sunset_date",
ReplacementModel: "replacement_model",
Capabilities: "capabilities",
}
// NewMdlModelsDao creates and returns a new DAO object for table data access.
+2 -2
View File
@@ -31,8 +31,8 @@ type NtfMessagesColumns struct {
IsRead string // 是否已读:0=未读, 1=已读
IsBroadcast string // 是否广播消息:0=个人消息, 1=广播消息
Metadata string // 附加元数据(JSONB,如关联资源ID、跳转链接等)
TargetRoles string // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
CreatedAt string // 创建时间
TargetRoles string // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
}
// ntfMessagesColumns holds the columns for the table ntf_messages.
@@ -47,8 +47,8 @@ var ntfMessagesColumns = NtfMessagesColumns{
IsRead: "is_read",
IsBroadcast: "is_broadcast",
Metadata: "metadata",
TargetRoles: "target_roles",
CreatedAt: "created_at",
TargetRoles: "target_roles",
}
// NewNtfMessagesDao creates and returns a new DAO object for table data access.
+87
View File
@@ -0,0 +1,87 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// PlgExampleLogsDao is the data access object for the table plg_example_logs.
type PlgExampleLogsDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns PlgExampleLogsColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// PlgExampleLogsColumns defines and stores column names for the table plg_example_logs.
type PlgExampleLogsColumns struct {
Id string //
TenantId string //
Action string //
Detail string //
CreatedAt string //
}
// plgExampleLogsColumns holds the columns for the table plg_example_logs.
var plgExampleLogsColumns = PlgExampleLogsColumns{
Id: "id",
TenantId: "tenant_id",
Action: "action",
Detail: "detail",
CreatedAt: "created_at",
}
// NewPlgExampleLogsDao creates and returns a new DAO object for table data access.
func NewPlgExampleLogsDao(handlers ...gdb.ModelHandler) *PlgExampleLogsDao {
return &PlgExampleLogsDao{
group: "default",
table: "plg_example_logs",
columns: plgExampleLogsColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *PlgExampleLogsDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *PlgExampleLogsDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *PlgExampleLogsDao) Columns() PlgExampleLogsColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *PlgExampleLogsDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *PlgExampleLogsDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *PlgExampleLogsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -11,16 +11,16 @@ import (
"github.com/gogf/gf/v2/frame/g"
)
// TskAsyncTasksDao is the data access object for the table tsk_async_tasks.
type TskAsyncTasksDao struct {
// TskModelTasksDao is the data access object for the table tsk_model_tasks.
type TskModelTasksDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns TskAsyncTasksColumns // columns contains all the column names of Table for convenient usage.
columns TskModelTasksColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// TskAsyncTasksColumns defines and stores column names for the table tsk_async_tasks.
type TskAsyncTasksColumns struct {
// TskModelTasksColumns defines and stores column names for the table tsk_model_tasks.
type TskModelTasksColumns struct {
Id string //
PublicTaskId string //
Platform string //
@@ -47,8 +47,8 @@ type TskAsyncTasksColumns struct {
UpdatedAt string //
}
// tskAsyncTasksColumns holds the columns for the table tsk_async_tasks.
var tskAsyncTasksColumns = TskAsyncTasksColumns{
// tskModelTasksColumns holds the columns for the table tsk_model_tasks.
var tskModelTasksColumns = TskModelTasksColumns{
Id: "id",
PublicTaskId: "public_task_id",
Platform: "platform",
@@ -75,38 +75,38 @@ var tskAsyncTasksColumns = TskAsyncTasksColumns{
UpdatedAt: "updated_at",
}
// NewTskAsyncTasksDao creates and returns a new DAO object for table data access.
func NewTskAsyncTasksDao(handlers ...gdb.ModelHandler) *TskAsyncTasksDao {
return &TskAsyncTasksDao{
// NewTskModelTasksDao creates and returns a new DAO object for table data access.
func NewTskModelTasksDao(handlers ...gdb.ModelHandler) *TskModelTasksDao {
return &TskModelTasksDao{
group: "default",
table: "tsk_async_tasks",
columns: tskAsyncTasksColumns,
table: "tsk_model_tasks",
columns: tskModelTasksColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *TskAsyncTasksDao) DB() gdb.DB {
func (dao *TskModelTasksDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *TskAsyncTasksDao) Table() string {
func (dao *TskModelTasksDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *TskAsyncTasksDao) Columns() TskAsyncTasksColumns {
func (dao *TskModelTasksDao) Columns() TskModelTasksColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *TskAsyncTasksDao) Group() string {
func (dao *TskModelTasksDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *TskAsyncTasksDao) Ctx(ctx context.Context) *gdb.Model {
func (dao *TskModelTasksDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
@@ -120,6 +120,6 @@ func (dao *TskAsyncTasksDao) Ctx(ctx context.Context) *gdb.Model {
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *TskAsyncTasksDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
func (dao *TskModelTasksDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -8,20 +8,15 @@ import (
"github.com/qianfree/team-api/internal/dao/internal"
)
// internalTskAsyncTasksDao is an internal type for wrapping the internal DAO implementation.
type internalTskAsyncTasksDao = *internal.TskAsyncTasksDao
// tskAsyncTasksDao is the data access object for the table tsk_async_tasks.
// plgExampleLogsDao is the data access object for the table plg_example_logs.
// You can define custom methods on it to extend its functionality as needed.
type tskAsyncTasksDao struct {
internalTskAsyncTasksDao
type plgExampleLogsDao struct {
*internal.PlgExampleLogsDao
}
var (
// TskAsyncTasks is a globally accessible object for table tsk_async_tasks operations.
TskAsyncTasks = tskAsyncTasksDao{
internal.NewTskAsyncTasksDao(),
}
// PlgExampleLogs is a globally accessible object for table plg_example_logs operations.
PlgExampleLogs = plgExampleLogsDao{internal.NewPlgExampleLogsDao()}
)
// Add your custom methods and functionality below.
+22
View File
@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"github.com/qianfree/team-api/internal/dao/internal"
)
// tskModelTasksDao is the data access object for the table tsk_model_tasks.
// You can define custom methods on it to extend its functionality as needed.
type tskModelTasksDao struct {
*internal.TskModelTasksDao
}
var (
// TskModelTasks is a globally accessible object for table tsk_model_tasks operations.
TskModelTasks = tskModelTasksDao{internal.NewTskModelTasksDao()}
)
// Add your custom methods and functionality below.
+93 -125
View File
@@ -14,28 +14,34 @@ import (
do "github.com/qianfree/team-api/internal/model/do"
)
// TaskList 任务列表
// TaskList 大模型异步任务列表
func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskListRes, error) {
m := dao.TskTasks.Ctx(ctx).OrderDesc("id")
m := dao.TskModelTasks.Ctx(ctx).OrderDesc("id")
if req.Status != "" {
m = m.Where("status", req.Status)
}
if req.Handler != "" {
m = m.Where("handler", req.Handler)
if req.Platform != "" {
m = m.Where("platform", req.Platform)
}
var tasks []struct {
Id int64 `json:"id"`
Name string `json:"name"`
Handler string `json:"handler"`
Status string `json:"status"`
MaxRetries int `json:"max_retries"`
RetryCount int `json:"retry_count"`
ErrorMessage string `json:"error_message"`
StartedAt *gtime.Time `json:"started_at"`
FinishedAt *gtime.Time `json:"finished_at"`
ScheduledAt *gtime.Time `json:"scheduled_at"`
CreatedAt *gtime.Time `json:"created_at"`
Id int64 `json:"id"`
PublicTaskId string `json:"public_task_id"`
Platform string `json:"platform"`
Action string `json:"action"`
Status string `json:"status"`
Progress string `json:"progress"`
ModelName string `json:"model_name"`
FailReason string `json:"fail_reason"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
ActualCost float64 `json:"actual_cost"`
BillingSettled bool `json:"billing_settled"`
ResultUrl string `json:"result_url"`
TenantId int64 `json:"tenant_id"`
UserId int64 `json:"user_id"`
SubmitTime *gtime.Time `json:"submit_time"`
FinishTime *gtime.Time `json:"finish_time"`
CreatedAt *gtime.Time `json:"created_at"`
}
var total int
@@ -44,26 +50,30 @@ func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskLis
return nil, err
}
list := make([]v1.TaskItem, 0, len(tasks))
list := make([]v1.ModelTaskItem, 0, len(tasks))
for _, t := range tasks {
item := v1.TaskItem{
ID: t.Id,
Name: t.Name,
Handler: t.Handler,
Status: t.Status,
MaxRetries: t.MaxRetries,
RetryCount: t.RetryCount,
ErrorMessage: t.ErrorMessage,
CreatedAt: t.CreatedAt.Format("Y-m-d H:i:s"),
item := v1.ModelTaskItem{
ID: t.Id,
PublicTaskID: t.PublicTaskId,
Platform: t.Platform,
Action: t.Action,
Status: t.Status,
Progress: t.Progress,
ModelName: t.ModelName,
FailReason: t.FailReason,
PreDeductAmount: t.PreDeductAmount,
ActualCost: t.ActualCost,
BillingSettled: t.BillingSettled,
ResultURL: t.ResultUrl,
TenantID: t.TenantId,
UserID: t.UserId,
CreatedAt: t.CreatedAt.Format("Y-m-d H:i:s"),
}
if t.StartedAt != nil {
item.StartedAt = t.StartedAt.Format("Y-m-d H:i:s")
if t.SubmitTime != nil {
item.SubmitTime = t.SubmitTime.Format("Y-m-d H:i:s")
}
if t.FinishedAt != nil {
item.FinishedAt = t.FinishedAt.Format("Y-m-d H:i:s")
}
if t.ScheduledAt != nil {
item.ScheduledAt = t.ScheduledAt.Format("Y-m-d H:i:s")
if t.FinishTime != nil {
item.FinishTime = t.FinishTime.Format("Y-m-d H:i:s")
}
list = append(list, item)
}
@@ -76,22 +86,30 @@ func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskLis
}, nil
}
// TaskDetail 任务详情
// TaskDetail 大模型异步任务详情
func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.TaskDetailRes, error) {
var task *struct {
Id int64 `json:"id"`
Name string `json:"name"`
Handler string `json:"handler"`
Status string `json:"status"`
MaxRetries int `json:"max_retries"`
RetryCount int `json:"retry_count"`
ErrorMessage string `json:"error_message"`
StartedAt *gtime.Time `json:"started_at"`
FinishedAt *gtime.Time `json:"finished_at"`
ScheduledAt *gtime.Time `json:"scheduled_at"`
CreatedAt *gtime.Time `json:"created_at"`
Id int64 `json:"id"`
PublicTaskId string `json:"public_task_id"`
Platform string `json:"platform"`
Action string `json:"action"`
Status string `json:"status"`
Progress string `json:"progress"`
ModelName string `json:"model_name"`
UpstreamModel string `json:"upstream_model"`
FailReason string `json:"fail_reason"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
ActualCost float64 `json:"actual_cost"`
BillingSettled bool `json:"billing_settled"`
ResultUrl string `json:"result_url"`
TenantId int64 `json:"tenant_id"`
UserId int64 `json:"user_id"`
SubmitTime *gtime.Time `json:"submit_time"`
StartTime *gtime.Time `json:"start_time"`
FinishTime *gtime.Time `json:"finish_time"`
CreatedAt *gtime.Time `json:"created_at"`
}
err := dao.TskTasks.Ctx(ctx).Where("id", req.ID).Scan(&task)
err := dao.TskModelTasks.Ctx(ctx).Where("id", req.ID).Scan(&task)
if err != nil {
return nil, err
}
@@ -99,106 +117,56 @@ func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.Tas
return nil, gerror.NewCode(gcode.New(consts.CodeNotFound, consts.MsgNotFound, nil), consts.MsgNotFound)
}
taskItem := v1.TaskItem{
ID: task.Id,
Name: task.Name,
Handler: task.Handler,
Status: task.Status,
MaxRetries: task.MaxRetries,
RetryCount: task.RetryCount,
ErrorMessage: task.ErrorMessage,
CreatedAt: task.CreatedAt.Format("Y-m-d H:i:s"),
item := v1.ModelTaskItem{
ID: task.Id,
PublicTaskID: task.PublicTaskId,
Platform: task.Platform,
Action: task.Action,
Status: task.Status,
Progress: task.Progress,
ModelName: task.ModelName,
FailReason: task.FailReason,
PreDeductAmount: task.PreDeductAmount,
ActualCost: task.ActualCost,
BillingSettled: task.BillingSettled,
ResultURL: task.ResultUrl,
TenantID: task.TenantId,
UserID: task.UserId,
CreatedAt: task.CreatedAt.Format("Y-m-d H:i:s"),
}
if task.StartedAt != nil {
taskItem.StartedAt = task.StartedAt.Format("Y-m-d H:i:s")
if task.SubmitTime != nil {
item.SubmitTime = task.SubmitTime.Format("Y-m-d H:i:s")
}
if task.FinishedAt != nil {
taskItem.FinishedAt = task.FinishedAt.Format("Y-m-d H:i:s")
}
if task.ScheduledAt != nil {
taskItem.ScheduledAt = task.ScheduledAt.Format("Y-m-d H:i:s")
}
// 查询最近日志
var logs []struct {
Level string `json:"level"`
Message string `json:"message"`
CreatedAt *gtime.Time `json:"created_at"`
}
_ = dao.TskTaskLogs.Ctx(ctx).
Where("task_id", req.ID).
OrderDesc("id").
Limit(50).
Scan(&logs)
logList := make([]v1.TaskLogItem, 0, len(logs))
for _, l := range logs {
logList = append(logList, v1.TaskLogItem{
Level: l.Level,
Message: l.Message,
CreatedAt: l.CreatedAt.Format("Y-m-d H:i:s"),
})
if task.FinishTime != nil {
item.FinishTime = task.FinishTime.Format("Y-m-d H:i:s")
}
return &v1.TaskDetailRes{
Task: taskItem,
RecentLogs: logList,
Task: item,
}, nil
}
// TaskCancel 取消任务
// TaskCancel 取消大模型异步任务
func (s *sAdmin) TaskCancel(ctx context.Context, req *v1.TaskCancelReq) (*v1.TaskCancelRes, error) {
result, err := dao.TskTasks.Ctx(ctx).
result, err := dao.TskModelTasks.Ctx(ctx).
Where("id", req.ID).
WhereIn("status", []string{"pending", "running"}).
Data(do.TskTasks{
Status: "cancelled",
FinishedAt: gtime.Now(),
WhereIn("status", []string{"NOT_START", "SUBMITTED", "IN_PROGRESS"}).
Data(do.TskModelTasks{
Status: "FAILURE",
FailReason: "管理员手动取消",
FinishTime: gtime.Now(),
}).Update()
if err != nil {
return nil, err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return nil, gerror.New("任务无法取消(可能已完成或已取消)")
return nil, gerror.New("任务无法取消(可能已完成或已失败)")
}
return &v1.TaskCancelRes{}, nil
}
// TaskRetry 重试失败任务
func (s *sAdmin) TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (*v1.TaskRetryRes, error) {
var task *struct {
Id int64 `json:"id"`
Status string `json:"status"`
RetryCount int `json:"retry_count"`
MaxRetries int `json:"max_retries"`
}
err := dao.TskTasks.Ctx(ctx).Where("id", req.ID).Scan(&task)
if err != nil {
return nil, err
}
if task == nil {
return nil, gerror.NewCode(gcode.New(consts.CodeNotFound, consts.MsgNotFound, nil), consts.MsgNotFound)
}
if task.Status != "failed" {
return nil, gerror.NewCode(gcode.New(consts.CodeTaskNotRetryable, consts.MsgTaskNotRetryable, nil),
consts.MsgTaskNotRetryable)
}
_, err = dao.TskTasks.Ctx(ctx).Where("id", req.ID).Data(do.TskTasks{
Status: "pending",
RetryCount: task.RetryCount + 1,
ErrorMessage: "",
StartedAt: nil,
FinishedAt: nil,
}).Update()
if err != nil {
return nil, err
}
return &v1.TaskRetryRes{TaskID: task.Id}, nil
}
// MarkStuckTasksFailed 标记卡住的任务为失败(定时任务)
// MarkStuckTasksFailed 标记卡住的系统任务为失败(定时任务,操作 tsk_tasks 表)
func MarkStuckTasksFailed(ctx context.Context) error {
threshold := time.Now().Add(-30 * time.Minute)
_, err := dao.TskTasks.Ctx(ctx).
+1 -1
View File
@@ -30,7 +30,7 @@ func HasActiveTasks() bool {
// InitActiveCount initializes the counter from the database at startup.
func InitActiveCount(ctx context.Context) {
count, err := dao.TskAsyncTasks.Ctx(ctx).
count, err := dao.TskModelTasks.Ctx(ctx).
Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE").
Count()
if err != nil {
+7 -7
View File
@@ -19,7 +19,7 @@ var DefaultAsyncProvider = &AsyncProvider{}
// CreateTask 创建异步任务记录
func (p *AsyncProvider) CreateTask(ctx context.Context, task *common.AsyncTask) error {
_, err := dao.TskAsyncTasks.Ctx(ctx).Insert(map[string]any{
_, err := dao.TskModelTasks.Ctx(ctx).Insert(map[string]any{
"public_task_id": task.PublicTaskID,
"platform": task.Platform,
"action": task.Action,
@@ -49,7 +49,7 @@ func (p *AsyncProvider) CreateTask(ctx context.Context, task *common.AsyncTask)
// UpdateTask 更新任务记录
func (p *AsyncProvider) UpdateTask(ctx context.Context, task *common.AsyncTask) error {
_, err := dao.TskAsyncTasks.Ctx(ctx).
_, err := dao.TskModelTasks.Ctx(ctx).
Where("id", task.ID).
Update(map[string]any{
"status": task.Status,
@@ -71,7 +71,7 @@ func (p *AsyncProvider) UpdateTask(ctx context.Context, task *common.AsyncTask)
// UpdateTaskCAS CAS 状态更新
func (p *AsyncProvider) UpdateTaskCAS(ctx context.Context, task *common.AsyncTask, oldStatus string) error {
result, err := dao.TskAsyncTasks.Ctx(ctx).
result, err := dao.TskModelTasks.Ctx(ctx).
Where("id", task.ID).
Where("status", oldStatus).
Update(map[string]any{
@@ -124,7 +124,7 @@ func (p *AsyncProvider) GetTaskByPublicID(ctx context.Context, publicTaskID stri
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
err := dao.TskAsyncTasks.Ctx(ctx).
err := dao.TskModelTasks.Ctx(ctx).
Where("public_task_id", publicTaskID).
Scan(&row)
if err != nil {
@@ -188,7 +188,7 @@ func (p *AsyncProvider) GetTaskByPublicIDAndUser(ctx context.Context, publicTask
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
err := dao.TskAsyncTasks.Ctx(ctx).
err := dao.TskModelTasks.Ctx(ctx).
Where("public_task_id", publicTaskID).
Where("user_id", userID).
Scan(&row)
@@ -248,7 +248,7 @@ func (p *AsyncProvider) GetNonTerminalTasks(ctx context.Context, limit int) ([]*
SubmitTime *time.Time `json:"submit_time"`
CreatedAt time.Time `json:"created_at"`
}
err := dao.TskAsyncTasks.Ctx(ctx).
err := dao.TskModelTasks.Ctx(ctx).
Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE").
Order("submit_time ASC").
Limit(limit).
@@ -301,7 +301,7 @@ func (p *AsyncProvider) GetTimedOutTasks(ctx context.Context, cutoffUnix int64,
PrivateData json.RawMessage `json:"private_data"`
SubmitTime *time.Time `json:"submit_time"`
}
err := dao.TskAsyncTasks.Ctx(ctx).
err := dao.TskModelTasks.Ctx(ctx).
Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE").
Where("submit_time < ?", cutoffTime).
Order("submit_time ASC").
+1 -1
View File
@@ -20,7 +20,7 @@ type ChnChannelKeys struct {
LastUsedAt *gtime.Time // 最后使用时间
LastError any // 最后一次错误信息
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
KeyType any // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
TokenExpiresAt *gtime.Time // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
UpdatedAt *gtime.Time // 更新时间
}
+1 -1
View File
@@ -21,10 +21,10 @@ type MdlModels struct {
MaxOutputTokens any // 最大输出 token 数
Description any // 模型描述
Tags []string // 标签(如 reasoning、vision、function_calling)
Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等)
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
DeprecatedAt *gtime.Time // 标记弃用的时间(NULL表示未弃用)
SunsetDate *gtime.Time // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
ReplacementModel any // 推荐替代模型名(NULL表示无替代)
Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等)
}
+1 -1
View File
@@ -22,6 +22,6 @@ type NtfMessages struct {
IsRead any // 是否已读:0=未读, 1=已读
IsBroadcast any // 是否广播消息:0=个人消息, 1=广播消息
Metadata any // 附加元数据(JSONB,如关联资源ID、跳转链接等)
TargetRoles any // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
CreatedAt *gtime.Time // 创建时间
TargetRoles any // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
}
+20
View File
@@ -0,0 +1,20 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// PlgExampleLogs is the golang structure of table plg_example_logs for DAO operations like Where/Data.
type PlgExampleLogs struct {
g.Meta `orm:"table:plg_example_logs, do:true"`
Id any //
TenantId any //
Action any //
Detail any //
CreatedAt *gtime.Time //
}
@@ -9,9 +9,9 @@ import (
"github.com/gogf/gf/v2/os/gtime"
)
// TskAsyncTasks is the golang structure of table tsk_async_tasks for DAO operations like Where/Data.
type TskAsyncTasks struct {
g.Meta `orm:"table:tsk_async_tasks, do:true"`
// TskModelTasks is the golang structure of table tsk_model_tasks for DAO operations like Where/Data.
type TskModelTasks struct {
g.Meta `orm:"table:tsk_model_tasks, do:true"`
Id any //
PublicTaskId any //
Platform any //
+1 -1
View File
@@ -18,7 +18,7 @@ type ChnChannelKeys struct {
LastUsedAt *gtime.Time `json:"last_used_at" orm:"last_used_at" description:"最后使用时间"` // 最后使用时间
LastError string `json:"last_error" orm:"last_error" description:"最后一次错误信息"` // 最后一次错误信息
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
KeyType string `json:"key_type" orm:"key_type" description:"Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)"` // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)
TokenExpiresAt *gtime.Time `json:"token_expires_at" orm:"token_expires_at" description:"OAuth access_token 过期时间(仅 key_type=oauth 时有值)"` // OAuth access_token 过期时间(仅 key_type=oauth 时有值)
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
}
+1 -1
View File
@@ -19,10 +19,10 @@ type MdlModels struct {
MaxOutputTokens int `json:"max_output_tokens" orm:"max_output_tokens" description:"最大输出 token 数"` // 最大输出 token 数
Description string `json:"description" orm:"description" description:"模型描述"` // 模型描述
Tags []string `json:"tags" orm:"tags" description:"标签(如 reasoning、vision、function_calling)"` // 标签(如 reasoning、vision、function_calling)
Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间
DeprecatedAt *gtime.Time `json:"deprecated_at" orm:"deprecated_at" description:"标记弃用的时间(NULL表示未弃用)"` // 标记弃用的时间(NULL表示未弃用)
SunsetDate *gtime.Time `json:"sunset_date" orm:"sunset_date" description:"计划下线日期(到达后返回410 Gone,NULL表示未设置)"` // 计划下线日期(到达后返回410 Gone,NULL表示未设置)
ReplacementModel string `json:"replacement_model" orm:"replacement_model" description:"推荐替代模型名(NULL表示无替代)"` // 推荐替代模型名(NULL表示无替代)
Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等)
}
+1 -1
View File
@@ -20,6 +20,6 @@ type NtfMessages struct {
IsRead int `json:"is_read" orm:"is_read" description:"是否已读:0=未读, 1=已读"` // 是否已读:0=未读, 1=已读
IsBroadcast int `json:"is_broadcast" orm:"is_broadcast" description:"是否广播消息:0=个人消息, 1=广播消息"` // 是否广播消息:0=个人消息, 1=广播消息
Metadata string `json:"metadata" orm:"metadata" description:"附加元数据(JSONB,如关联资源ID、跳转链接等)"` // 附加元数据(JSONB,如关联资源ID、跳转链接等)
TargetRoles string `json:"target_roles" orm:"target_roles" description:"目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)"` // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间
TargetRoles string `json:"target_roles" orm:"target_roles" description:"目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)"` // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)
}
+18
View File
@@ -0,0 +1,18 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// PlgExampleLogs is the golang structure for table plg_example_logs.
type PlgExampleLogs struct {
Id int64 `json:"id" orm:"id" description:""` //
TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` //
Action string `json:"action" orm:"action" description:""` //
Detail string `json:"detail" orm:"detail" description:""` //
CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` //
}
@@ -8,8 +8,8 @@ import (
"github.com/gogf/gf/v2/os/gtime"
)
// TskAsyncTasks is the golang structure for table tsk_async_tasks.
type TskAsyncTasks struct {
// TskModelTasks is the golang structure for table tsk_model_tasks.
type TskModelTasks struct {
Id int64 `json:"id" orm:"id" description:""` //
PublicTaskId string `json:"public_task_id" orm:"public_task_id" description:""` //
Platform string `json:"platform" orm:"platform" description:""` //
+3 -5
View File
@@ -337,14 +337,12 @@ type (
GetSettings(ctx context.Context, req *v1.AdminSettingsGetReq) (*v1.AdminSettingsGetRes, error)
// UpdateSettings batch-updates settings for a given category.
UpdateSettings(ctx context.Context, req *v1.AdminSettingsUpdateReq) (*v1.AdminSettingsUpdateRes, error)
// TaskList 任务列表
// TaskList 大模型异步任务列表
TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskListRes, error)
// TaskDetail 任务详情
// TaskDetail 大模型异步任务详情
TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.TaskDetailRes, error)
// TaskCancel 取消任务
// TaskCancel 取消大模型异步任务
TaskCancel(ctx context.Context, req *v1.TaskCancelReq) (*v1.TaskCancelRes, error)
// TaskRetry 重试失败任务
TaskRetry(ctx context.Context, req *v1.TaskRetryReq) (*v1.TaskRetryRes, error)
// TenantSelect returns a lightweight paginated tenant list for dropdown selectors.
TenantSelect(ctx context.Context, req *v1.TenantSelectReq) (*v1.TenantSelectRes, error)
// CreateTenant creates a new tenant with its owner user and wallet.
@@ -1,22 +0,0 @@
-- +goose Up
-- 租户模型分配增加自定义缓存定价覆盖字段和阶梯定价支持
-- 缓存定价覆盖
ALTER TABLE mdl_tenant_models
ADD COLUMN IF NOT EXISTS custom_cache_read_price NUMERIC(20,10),
ADD COLUMN IF NOT EXISTS custom_cache_creation_price NUMERIC(20,10);
COMMENT ON COLUMN mdl_tenant_models.custom_cache_read_price IS '自定义缓存读取价格($/1M token),NULL 表示使用基础定价';
COMMENT ON COLUMN mdl_tenant_models.custom_cache_creation_price IS '自定义缓存创建价格($/1M token),NULL 表示使用基础定价';
-- 阶梯定价
ALTER TABLE mdl_tenant_models
ADD COLUMN IF NOT EXISTS custom_pricing_tiers JSONB;
COMMENT ON COLUMN mdl_tenant_models.custom_pricing_tiers IS '自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]';
-- +goose Down
ALTER TABLE mdl_tenant_models
DROP COLUMN IF EXISTS custom_cache_read_price,
DROP COLUMN IF EXISTS custom_cache_creation_price,
DROP COLUMN IF EXISTS custom_pricing_tiers;
@@ -0,0 +1,97 @@
-- +goose Up
-- 租户模型自定义定价 + 异步任务表重命名
-- 1. 租户模型分配增加自定义缓存定价覆盖字段和阶梯定价支持
ALTER TABLE mdl_tenant_models
ADD COLUMN IF NOT EXISTS custom_cache_read_price NUMERIC(20,10),
ADD COLUMN IF NOT EXISTS custom_cache_creation_price NUMERIC(20,10);
COMMENT ON COLUMN mdl_tenant_models.custom_cache_read_price IS '自定义缓存读取价格($/1M token),NULL 表示使用基础定价';
COMMENT ON COLUMN mdl_tenant_models.custom_cache_creation_price IS '自定义缓存创建价格($/1M token),NULL 表示使用基础定价';
ALTER TABLE mdl_tenant_models
ADD COLUMN IF NOT EXISTS custom_pricing_tiers JSONB;
COMMENT ON COLUMN mdl_tenant_models.custom_pricing_tiers IS '自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]';
-- 2. 将 tsk_async_tasks 重命名为 tsk_model_tasks,并添加表和字段注释
ALTER TABLE tsk_async_tasks RENAME TO tsk_model_tasks;
ALTER TABLE tsk_model_tasks RENAME CONSTRAINT uk_tsk_async_tasks_public_id TO uk_tsk_model_tasks_public_id;
ALTER INDEX idx_tsk_async_tasks_active RENAME TO idx_tsk_model_tasks_active;
ALTER INDEX idx_tsk_async_tasks_status RENAME TO idx_tsk_model_tasks_status;
ALTER INDEX idx_tsk_async_tasks_submit_time RENAME TO idx_tsk_model_tasks_submit_time;
ALTER INDEX idx_tsk_async_tasks_user RENAME TO idx_tsk_model_tasks_user;
COMMENT ON TABLE tsk_model_tasks IS '异步模型任务表,存储 AI 模型异步调用任务(如视频生成、图像生成、音乐生成等)的状态和结果';
COMMENT ON COLUMN tsk_model_tasks.id IS '主键ID';
COMMENT ON COLUMN tsk_model_tasks.public_task_id IS '对外公开的任务ID(task_xxxxx 格式),用于 API 响应和客户端查询';
COMMENT ON COLUMN tsk_model_tasks.platform IS '任务所属平台:sora、kling、suno、midjourney、volcengine';
COMMENT ON COLUMN tsk_model_tasks.action IS '任务动作类型。通用:generate;Suno:music、lyrics;';
COMMENT ON COLUMN tsk_model_tasks.status IS '任务状态:NOT_START-未开始、SUBMITTED-已提交、IN_PROGRESS-进行中、SUCCESS-成功、FAILURE-失败';
COMMENT ON COLUMN tsk_model_tasks.progress IS '任务进度,字符串格式百分比(如 0%、50%、100%)';
COMMENT ON COLUMN tsk_model_tasks.fail_reason IS '任务失败原因文本';
COMMENT ON COLUMN tsk_model_tasks.tenant_id IS '发起任务的租户ID';
COMMENT ON COLUMN tsk_model_tasks.user_id IS '发起任务的租户用户ID';
COMMENT ON COLUMN tsk_model_tasks.api_key_id IS '调用方使用的 API Key ID';
COMMENT ON COLUMN tsk_model_tasks.channel_id IS '转发请求的渠道ID';
COMMENT ON COLUMN tsk_model_tasks.model_name IS '用户请求的模型名称(如 sora-1.0-turbo、midjourney 等)';
COMMENT ON COLUMN tsk_model_tasks.upstream_model IS '上游供应商实际使用的模型名称,可能与请求模型不同';
COMMENT ON COLUMN tsk_model_tasks.pre_deduct_amount IS '提交任务时的预扣金额(USD),任务完成后根据实际用量结算差额';
COMMENT ON COLUMN tsk_model_tasks.actual_cost IS '任务实际消费金额(USD),成功时按此结算,失败时退还预扣金额';
COMMENT ON COLUMN tsk_model_tasks.billing_settled IS '计费是否已结算(true 表示已完成预扣与实际金额的多退少补)';
COMMENT ON COLUMN tsk_model_tasks.result_url IS '任务完成后的结果资源 URL(如生成的视频/图片/音频的下载地址)';
COMMENT ON COLUMN tsk_model_tasks.data IS '上游供应商返回的完整响应数据(JSONB),可返回给用户查看';
COMMENT ON COLUMN tsk_model_tasks.private_data IS '内部私有数据(JSONB),含 upstream_task_id 等敏感字段,用于轮询上游状态,不返回给用户';
COMMENT ON COLUMN tsk_model_tasks.submit_time IS '任务提交到上游供应商的时间';
COMMENT ON COLUMN tsk_model_tasks.start_time IS '上游供应商开始执行任务的时间';
COMMENT ON COLUMN tsk_model_tasks.finish_time IS '任务完成(成功或失败)的时间';
COMMENT ON COLUMN tsk_model_tasks.created_at IS '记录创建时间';
COMMENT ON COLUMN tsk_model_tasks.updated_at IS '记录更新时间';
-- +goose Down
-- 移除注释
COMMENT ON TABLE tsk_model_tasks IS NULL;
COMMENT ON COLUMN tsk_model_tasks.id IS NULL;
COMMENT ON COLUMN tsk_model_tasks.public_task_id IS NULL;
COMMENT ON COLUMN tsk_model_tasks.platform IS NULL;
COMMENT ON COLUMN tsk_model_tasks.action IS NULL;
COMMENT ON COLUMN tsk_model_tasks.status IS NULL;
COMMENT ON COLUMN tsk_model_tasks.progress IS NULL;
COMMENT ON COLUMN tsk_model_tasks.fail_reason IS NULL;
COMMENT ON COLUMN tsk_model_tasks.tenant_id IS NULL;
COMMENT ON COLUMN tsk_model_tasks.user_id IS NULL;
COMMENT ON COLUMN tsk_model_tasks.api_key_id IS NULL;
COMMENT ON COLUMN tsk_model_tasks.channel_id IS NULL;
COMMENT ON COLUMN tsk_model_tasks.model_name IS NULL;
COMMENT ON COLUMN tsk_model_tasks.upstream_model IS NULL;
COMMENT ON COLUMN tsk_model_tasks.pre_deduct_amount IS NULL;
COMMENT ON COLUMN tsk_model_tasks.actual_cost IS NULL;
COMMENT ON COLUMN tsk_model_tasks.billing_settled IS NULL;
COMMENT ON COLUMN tsk_model_tasks.result_url IS NULL;
COMMENT ON COLUMN tsk_model_tasks.data IS NULL;
COMMENT ON COLUMN tsk_model_tasks.private_data IS NULL;
COMMENT ON COLUMN tsk_model_tasks.submit_time IS NULL;
COMMENT ON COLUMN tsk_model_tasks.start_time IS NULL;
COMMENT ON COLUMN tsk_model_tasks.finish_time IS NULL;
COMMENT ON COLUMN tsk_model_tasks.created_at IS NULL;
COMMENT ON COLUMN tsk_model_tasks.updated_at IS NULL;
-- 恢复索引名
ALTER INDEX idx_tsk_model_tasks_user RENAME TO idx_tsk_async_tasks_user;
ALTER INDEX idx_tsk_model_tasks_submit_time RENAME TO idx_tsk_async_tasks_submit_time;
ALTER INDEX idx_tsk_model_tasks_status RENAME TO idx_tsk_async_tasks_status;
ALTER INDEX idx_tsk_model_tasks_active RENAME TO idx_tsk_async_tasks_active;
-- 恢复约束名
ALTER TABLE tsk_model_tasks RENAME CONSTRAINT uk_tsk_model_tasks_public_id TO uk_tsk_async_tasks_public_id;
-- 恢复表名
ALTER TABLE tsk_model_tasks RENAME TO tsk_async_tasks;
-- 移除定价字段
ALTER TABLE mdl_tenant_models
DROP COLUMN IF EXISTS custom_cache_read_price,
DROP COLUMN IF EXISTS custom_cache_creation_price,
DROP COLUMN IF EXISTS custom_pricing_tiers;
+120 -172
View File
@@ -18,38 +18,66 @@ const pagination = reactive({
})
const filterStatus = ref<string | null>(null)
const filterHandler = ref('')
const filterDateRange = ref<string[]>([])
const filterPlatform = ref<string | null>(null)
const statusOptions = [
{ label: '全部状态', value: '' },
{ label: '待执行', value: 'pending' },
{ label: '执行中', value: 'running' },
{ label: '成功', value: 'succeeded' },
{ label: '失败', value: 'failed' },
{ label: '已取消', value: 'cancelled' },
{ label: '未开始', value: 'NOT_START' },
{ label: '已提交', value: 'SUBMITTED' },
{ label: '进行中', value: 'IN_PROGRESS' },
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAILURE' },
]
const platformOptions = [
{ label: '全部平台', value: '' },
{ label: 'Sora', value: 'sora' },
{ label: 'Kling', value: 'kling' },
{ label: 'Midjourney', value: 'midjourney' },
{ label: 'Suno', value: 'suno' },
]
const statusTagColor: Record<string, string | undefined> = {
pending: 'gray',
running: 'arcoblue',
succeeded: 'green',
failed: 'red',
cancelled: 'orangered',
NOT_START: 'gray',
SUBMITTED: 'arcoblue',
IN_PROGRESS: 'arcoblue',
SUCCESS: 'green',
FAILURE: 'red',
}
const statusTagLabel: Record<string, string> = {
pending: '待执行',
running: '执行中',
succeeded: '成功',
failed: '失败',
cancelled: '已取消',
NOT_START: '未开始',
SUBMITTED: '已提交',
IN_PROGRESS: '进行中',
SUCCESS: '成功',
FAILURE: '失败',
}
const platformLabel: Record<string, string> = {
sora: 'Sora',
kling: 'Kling',
midjourney: 'Midjourney',
suno: 'Suno',
}
const columns: TableColumnData[] = [
{ title: 'ID', dataIndex: 'id', width: 70 },
{ title: '任务名称', dataIndex: 'name', width: 180, ellipsis: true },
{ title: 'Handler', dataIndex: 'handler', width: 220, ellipsis: true },
{ title: '任务ID', dataIndex: 'public_task_id', width: 160, ellipsis: true, tooltip: true },
{
title: '平台',
dataIndex: 'platform',
width: 110,
render({ record }) {
return platformLabel[record.platform] || record.platform
},
},
{
title: '动作',
dataIndex: 'action',
width: 120,
ellipsis: true,
tooltip: true,
},
{
title: '状态',
dataIndex: 'status',
@@ -62,26 +90,43 @@ const columns: TableColumnData[] = [
},
},
{
title: '重试',
dataIndex: 'retry_count',
title: '进度',
dataIndex: 'progress',
width: 80,
},
{
title: '模型',
dataIndex: 'model_name',
width: 150,
ellipsis: true,
tooltip: true,
},
{
title: '费用',
dataIndex: 'actual_cost',
width: 100,
render({ record }) {
return `${record.retry_count}/${record.max_retries}`
if (record.billing_settled && record.actual_cost > 0) {
return `$${record.actual_cost.toFixed(4)}`
}
if (record.pre_deduct_amount > 0) {
return `$${record.pre_deduct_amount.toFixed(4)} (预扣)`
}
return '-'
},
},
{ title: '开始时间', dataIndex: 'started_at', width: 170 },
{ title: '完成时间', dataIndex: 'finished_at', width: 170 },
{ title: '创建时间', dataIndex: 'created_at', width: 170 },
{ title: '提交时间', dataIndex: 'submit_time', width: 170 },
{ title: '完成时间', dataIndex: 'finish_time', width: 170 },
{
title: '操作',
dataIndex: 'actions',
width: 160,
width: 120,
fixed: 'right',
render({ record }) {
const buttons: any[] = [
h(Button, { size: 'small', type: 'text', onClick: () => openDetail(record) }, () => '详情'),
]
if (record.status === 'pending' || record.status === 'running') {
if (record.status === 'NOT_START' || record.status === 'SUBMITTED' || record.status === 'IN_PROGRESS') {
buttons.push(
h(Popconfirm, {
content: '确定取消该任务?',
@@ -89,14 +134,6 @@ const columns: TableColumnData[] = [
}, () => h(Button, { size: 'small', type: 'text', status: 'warning' }, () => '取消')),
)
}
if (record.status === 'failed') {
buttons.push(
h(Popconfirm, {
content: '确定重试该任务?',
onOk: () => retryTask(record),
}, () => h(Button, { size: 'small', type: 'text', status: 'success' }, () => '重试')),
)
}
return h(Space, { size: 0 }, () => buttons)
},
},
@@ -110,11 +147,7 @@ async function fetchData() {
page_size: pagination.pageSize,
}
if (filterStatus.value) params.status = filterStatus.value
if (filterHandler.value) params.handler = filterHandler.value
if (filterDateRange.value.length === 2) {
params.start_date = filterDateRange.value[0]
params.end_date = filterDateRange.value[1]
}
if (filterPlatform.value) params.platform = filterPlatform.value
const res: any = await request.get('/admin/tasks', { params })
const raw = res.data?.data
data.value = (raw?.list || []).filter(Boolean)
@@ -134,8 +167,7 @@ function handleFilter() {
function resetFilter() {
filterStatus.value = null
filterHandler.value = ''
filterDateRange.value = []
filterPlatform.value = null
pagination.current = 1
fetchData()
}
@@ -150,36 +182,20 @@ async function cancelTask(row: any) {
}
}
async function retryTask(row: any) {
try {
await request.post(`/admin/tasks/${row.id}/retry`)
Message.success('任务已重新排队')
fetchData()
} catch {
// error handled by interceptor
}
}
// === Detail Drawer ===
const showDetail = ref(false)
const detailLoading = ref(false)
const detailData = ref<any>(null)
const detailExecutions = ref<any[]>([])
const detailLogs = ref<any[]>([])
async function openDetail(row: any) {
detailData.value = row
detailExecutions.value = []
detailLogs.value = []
showDetail.value = true
detailLoading.value = true
try {
const res: any = await request.get(`/admin/tasks/${row.id}`)
const raw = res.data?.data
if (raw) {
detailData.value = raw.task || row
detailExecutions.value = (raw.executions || []).filter(Boolean)
detailLogs.value = (raw.recent_logs || []).filter(Boolean)
if (raw?.task) {
detailData.value = raw.task
}
} catch {
// error handled by interceptor
@@ -188,40 +204,6 @@ async function openDetail(row: any) {
}
}
const logLevelColor: Record<string, string | undefined> = {
info: 'arcoblue',
warn: 'orangered',
error: 'red',
}
const execColumns: TableColumnData[] = [
{ title: '#', dataIndex: 'execution_order', width: 50 },
{
title: '状态',
dataIndex: 'status',
width: 80,
render({ record }) {
return h(Tag, {
color: statusTagColor[record.status],
size: 'small',
}, () => record.status)
},
},
{ title: '开始时间', dataIndex: 'started_at', width: 170 },
{ title: '完成时间', dataIndex: 'finished_at', width: 170 },
{
title: '耗时',
dataIndex: 'duration_ms',
width: 100,
render({ record }) {
if (!record.duration_ms) return '-'
if (record.duration_ms < 1000) return `${record.duration_ms}ms`
return `${(record.duration_ms / 1000).toFixed(1)}s`
},
},
{ title: '错误信息', dataIndex: 'error_message', ellipsis: true },
]
onMounted(() => {
fetchData()
})
@@ -229,7 +211,7 @@ onMounted(() => {
<template>
<div class="page-table">
<PageHeader title="任务日志" description="系统异步任务的执行记录与管理" />
<PageHeader title="任务日志" description="大模型异步生成任务(视频/图片/音乐)的执行记录与管理" />
<!-- Filters -->
<ACard :bordered="false" class="mb-4">
@@ -242,18 +224,12 @@ onMounted(() => {
style="width: 130px"
@change="handleFilter"
/>
<AInput
v-model="filterHandler"
placeholder="搜索 Handler..."
<ASelect
v-model="filterPlatform"
:options="platformOptions"
placeholder="平台"
allow-clear
style="width: 220px"
@keydown.enter="handleFilter"
@clear="handleFilter"
/>
<ARangePicker
v-model="filterDateRange"
style="width: 260px"
format="YYYY-MM-DD"
style="width: 130px"
@change="handleFilter"
/>
<AButton type="primary" @click="handleFilter">搜索</AButton>
@@ -267,7 +243,7 @@ onMounted(() => {
:columns="columns"
:data="data"
:loading="loading"
:scroll="{ x: 1300 }"
:scroll="{ x: 1400 }"
:bordered="false"
:stripe="true"
:pagination="false"
@@ -290,51 +266,51 @@ onMounted(() => {
<ADrawer v-model:visible="showDetail" :width="620" title="任务详情">
<ASpin :loading="detailLoading">
<template v-if="detailData">
<!-- Basic Info -->
<h3 class="detail-section-title">基本信息</h3>
<ADescriptions :column="2" bordered size="small">
<ADescriptionsItem label="任务名称">{{ detailData.name }}</ADescriptionsItem>
<ADescriptionsItem label="任务ID">{{ detailData.public_task_id }}</ADescriptionsItem>
<ADescriptionsItem label="状态">
<Tag :color="statusTagColor[detailData.status]" size="small">
{{ statusTagLabel[detailData.status] || detailData.status }}
</Tag>
</ADescriptionsItem>
<ADescriptionsItem label="Handler" :span="2">{{ detailData.handler }}</ADescriptionsItem>
<ADescriptionsItem label="重试次数">{{ detailData.retry_count }} / {{ detailData.max_retries }}</ADescriptionsItem>
<ADescriptionsItem label="计划时间">{{ detailData.scheduled_at || '-' }}</ADescriptionsItem>
<ADescriptionsItem label="开始时间">{{ detailData.started_at || '-' }}</ADescriptionsItem>
<ADescriptionsItem label="完成时间">{{ detailData.finished_at || '-' }}</ADescriptionsItem>
<ADescriptionsItem label="平台">{{ platformLabel[detailData.platform] || detailData.platform }}</ADescriptionsItem>
<ADescriptionsItem label="动作">{{ detailData.action }}</ADescriptionsItem>
<ADescriptionsItem label="模型">{{ detailData.model_name }}</ADescriptionsItem>
<ADescriptionsItem label="进度">{{ detailData.progress }}</ADescriptionsItem>
<ADescriptionsItem label="预扣金额">
<span v-if="detailData.pre_deduct_amount > 0">${{ detailData.pre_deduct_amount.toFixed(4) }}</span>
<span v-else>-</span>
</ADescriptionsItem>
<ADescriptionsItem label="实际费用">
<span v-if="detailData.billing_settled">${{ detailData.actual_cost.toFixed(4) }}</span>
<span v-else>未结算</span>
</ADescriptionsItem>
<ADescriptionsItem label="提交时间">{{ detailData.submit_time || '-' }}</ADescriptionsItem>
<ADescriptionsItem label="完成时间">{{ detailData.finish_time || '-' }}</ADescriptionsItem>
<ADescriptionsItem label="创建时间">{{ detailData.created_at }}</ADescriptionsItem>
<ADescriptionsItem label="错误信息" :span="2">
<span v-if="detailData.error_message" style="color: var(--color-danger-6)">{{ detailData.error_message }}</span>
<span v-else style="color: var(--color-text-4)">-</span>
<ADescriptionsItem label="租户ID">{{ detailData.tenant_id }}</ADescriptionsItem>
<ADescriptionsItem label="用户ID">{{ detailData.user_id }}</ADescriptionsItem>
<ADescriptionsItem v-if="detailData.fail_reason" label="失败原因" :span="2">
<span style="color: var(--color-danger-6)">{{ detailData.fail_reason }}</span>
</ADescriptionsItem>
</ADescriptions>
<!-- Executions -->
<h3 class="detail-section-title">执行记录</h3>
<ATable
v-if="detailExecutions.length > 0"
:columns="execColumns"
:data="detailExecutions"
:bordered="false"
:stripe="true"
:pagination="false"
size="small"
row-key="execution_order"
/>
<p v-else style="color: var(--color-text-4); font-size: 13px;">暂无执行记录</p>
<!-- Logs -->
<h3 class="detail-section-title">最近日志</h3>
<div v-if="detailLogs.length > 0" class="log-list">
<div v-for="log in detailLogs" :key="log.created_at + log.message" class="log-item">
<Tag :color="logLevelColor[log.level]" size="small" class="log-level-tag">{{ log.level }}</Tag>
<span class="log-message">{{ log.message }}</span>
<span class="log-time">{{ log.created_at }}</span>
<!-- Result -->
<template v-if="detailData.result_url">
<h3 class="detail-section-title">结果</h3>
<div class="result-preview">
<img
v-if="detailData.result_url && /\.(jpg|jpeg|png|gif|webp)(\?|$)/i.test(detailData.result_url)"
:src="detailData.result_url"
alt="任务结果"
style="max-width: 100%; border-radius: 6px;"
/>
<a v-else :href="detailData.result_url" target="_blank" class="result-link">
{{ detailData.result_url }}
</a>
</div>
</div>
<p v-else style="color: var(--color-text-4); font-size: 13px;">暂无日志</p>
</template>
</template>
</ASpin>
</ADrawer>
@@ -361,42 +337,14 @@ onMounted(() => {
margin-top: 0;
}
.log-list {
.result-preview {
background: var(--color-fill-1);
border-radius: 6px;
padding: 8px;
max-height: 300px;
overflow-y: auto;
padding: 12px;
}
.log-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 4px;
border-bottom: 1px solid var(--color-fill-3);
font-size: 13px;
}
.log-item:last-child {
border-bottom: none;
}
.log-level-tag {
flex-shrink: 0;
margin-top: 1px;
}
.log-message {
flex: 1;
color: var(--ta-text-primary);
.result-link {
color: rgb(var(--arcoblue-6));
word-break: break-all;
}
.log-time {
flex-shrink: 0;
color: var(--color-text-4);
font-size: 12px;
white-space: nowrap;
}
</style>