From 2425cbad2332e84808155e65c05b1dfab6ed9a99 Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 10 Jun 2026 22:08:14 +0800 Subject: [PATCH] =?UTF-8?q?improve:=E4=BC=98=E5=8C=96=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E4=BB=BB=E5=8A=A1=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/tenant/tenant.go | 1 + api/tenant/v1/task_tenant.go | 11 ++ .../tenant/tenant_v1_task_tenant.go | 3 + internal/logic/tenant/task.go | 147 ++++++++++++++++++ internal/service/tenant.go | 3 + web/admin/src/views/ai/TaskLogsPage.vue | 4 + web/tenant/src/views/service/TaskLogsPage.vue | 59 ++++--- 7 files changed, 209 insertions(+), 19 deletions(-) diff --git a/api/tenant/tenant.go b/api/tenant/tenant.go index 5112c09..7aab302 100644 --- a/api/tenant/tenant.go +++ b/api/tenant/tenant.go @@ -153,6 +153,7 @@ type ITenantV1 interface { TenantLoginHistory(ctx context.Context, req *v1.TenantLoginHistoryReq) (res *v1.TenantLoginHistoryRes, err error) TenantTaskList(ctx context.Context, req *v1.TenantTaskListReq) (res *v1.TenantTaskListRes, err error) TenantTaskDetail(ctx context.Context, req *v1.TenantTaskDetailReq) (res *v1.TenantTaskDetailRes, err error) + TenantTaskExport(ctx context.Context, req *v1.TenantTaskExportReq) (res *v1.TenantTaskExportRes, err error) TenantTicketCreate(ctx context.Context, req *v1.TenantTicketCreateReq) (res *v1.TenantTicketCreateRes, err error) TenantTicketList(ctx context.Context, req *v1.TenantTicketListReq) (res *v1.TenantTicketListRes, err error) TenantTicketGet(ctx context.Context, req *v1.TenantTicketGetReq) (res *v1.TenantTicketGetRes, err error) diff --git a/api/tenant/v1/task_tenant.go b/api/tenant/v1/task_tenant.go index 340b85a..a614aee 100644 --- a/api/tenant/v1/task_tenant.go +++ b/api/tenant/v1/task_tenant.go @@ -47,3 +47,14 @@ type TenantTaskDetailReq struct { type TenantTaskDetailRes struct { Task TenantTaskItem `json:"task"` } + +// TenantTaskExportReq 导出异步任务日志请求 +type TenantTaskExportReq struct { + g.Meta `path:"/tasks/export" method:"get" mime:"json" tags:"租户控制台-任务管理" summary:"导出异步任务日志"` + Format string `json:"format" in:"query" d:"csv" v:"in:csv,xlsx" dc:"导出格式:csv / xlsx"` + Status string `json:"status" in:"query" dc:"筛选状态"` + Platform string `json:"platform" in:"query" dc:"筛选平台(sora/kling/midjourney/suno)"` + PublicTaskID string `json:"public_task_id" in:"query" dc:"任务ID(精确匹配)"` +} + +type TenantTaskExportRes struct{} diff --git a/internal/controller/tenant/tenant_v1_task_tenant.go b/internal/controller/tenant/tenant_v1_task_tenant.go index c438290..2b8f344 100644 --- a/internal/controller/tenant/tenant_v1_task_tenant.go +++ b/internal/controller/tenant/tenant_v1_task_tenant.go @@ -13,3 +13,6 @@ func (c *ControllerV1) TenantTaskList(ctx context.Context, req *v1.TenantTaskLis func (c *ControllerV1) TenantTaskDetail(ctx context.Context, req *v1.TenantTaskDetailReq) (res *v1.TenantTaskDetailRes, err error) { return service.Tenant().TenantTaskDetail(ctx, req) } +func (c *ControllerV1) TenantTaskExport(ctx context.Context, req *v1.TenantTaskExportReq) (res *v1.TenantTaskExportRes, err error) { + return service.Tenant().ExportTasks(ctx, req) +} diff --git a/internal/logic/tenant/task.go b/internal/logic/tenant/task.go index 28e1600..91e582a 100644 --- a/internal/logic/tenant/task.go +++ b/internal/logic/tenant/task.go @@ -2,11 +2,17 @@ package tenant import ( "context" + "fmt" + "strconv" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/middleware" + "github.com/qianfree/team-api/internal/utility/export" ) // TenantTaskList 租户异步任务列表 @@ -168,3 +174,144 @@ func (s *sTenant) TenantTaskDetail(ctx context.Context, req *v1.TenantTaskDetail Task: item, }, nil } + +// ExportTasks 导出异步任务日志 +// owner/admin 可导出租户所有任务,member 只能导出自己的任务 +func (s *sTenant) ExportTasks(ctx context.Context, req *v1.TenantTaskExportReq) (*v1.TenantTaskExportRes, error) { + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + role := middleware.GetUserRole(ctx) + + statusLabel := map[string]string{ + "NOT_START": "未开始", + "SUBMITTED": "已提交", + "IN_PROGRESS": "进行中", + "SUCCESS": "成功", + "FAILURE": "失败", + "TIMEOUT": "超时", + } + + platformLabel := map[string]string{ + "sora": "Sora", + "kling": "Kling", + "midjourney": "Midjourney", + "suno": "Suno", + } + + columns := []export.Column{ + {Field: "public_task_id", Header: "任务ID"}, + {Field: "platform_name", Header: "平台"}, + {Field: "status_name", Header: "状态"}, + {Field: "action", Header: "动作"}, + {Field: "model_name", Header: "模型"}, + {Field: "progress", Header: "进度"}, + {Field: "username", Header: "用户"}, + {Field: "pre_deduct_amount", Header: "预扣金额(USD)"}, + {Field: "actual_cost", Header: "实际费用(USD)"}, + {Field: "billing_settled", Header: "是否已结算"}, + {Field: "fail_reason", Header: "失败原因"}, + {Field: "submit_time", Header: "提交时间"}, + {Field: "finish_time", Header: "完成时间"}, + {Field: "created_at", Header: "创建时间"}, + } + + config := export.Config{ + Format: req.Format, + Filename: "任务日志_" + gtime.Now().Format("Ymd_His"), + Columns: columns, + } + + var conditions []string + var args []any + + conditions = append(conditions, "t.tenant_id = ?") + args = append(args, tenantID) + + // member 只能导出自己的任务 + if role == "member" { + conditions = append(conditions, "t.user_id = ?") + args = append(args, userID) + } + if req.Status != "" { + conditions = append(conditions, "t.status = ?") + args = append(args, req.Status) + } + if req.Platform != "" { + conditions = append(conditions, "t.platform = ?") + args = append(args, req.Platform) + } + if req.PublicTaskID != "" { + conditions = append(conditions, "t.public_task_id = ?") + args = append(args, req.PublicTaskID) + } + + where := "" + for i, c := range conditions { + if i > 0 { + where += " AND " + } + where += c + } + + fromClause := "tsk_model_tasks t LEFT JOIN tnt_users u ON t.user_id = u.id AND t.tenant_id = u.tenant_id" + + return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) { + offset := 0 + for { + dataSQL := fmt.Sprintf( + `SELECT t.public_task_id, t.platform, t.status, t.action, t.model_name, t.progress, + COALESCE(u.username, '') AS username, + t.pre_deduct_amount, t.actual_cost, t.billing_settled, + COALESCE(t.fail_reason, '') AS fail_reason, + t.submit_time, t.finish_time, t.created_at + FROM %s WHERE %s ORDER BY t.id DESC LIMIT 1000 OFFSET ?`, + fromClause, where, + ) + exportArgs := make([]any, len(args)+1) + copy(exportArgs, args) + exportArgs[len(args)] = offset + result, err := g.DB().Ctx(ctx).Query(ctx, dataSQL, exportArgs...) + if err != nil { + return + } + for _, row := range result { + m := make(map[string]any, len(row)) + for k, v := range row { + switch raw := v.Val().(type) { + case []byte: + s := string(raw) + if f, err := strconv.ParseFloat(s, 64); err == nil { + m[k] = f + } else { + m[k] = s + } + default: + m[k] = raw + } + } + // 翻译平台和状态为中文 + if platform, ok := m["platform"].(string); ok { + if label, exists := platformLabel[platform]; exists { + m["platform_name"] = label + } else { + m["platform_name"] = platform + } + } + if status, ok := m["status"].(string); ok { + if label, exists := statusLabel[status]; exists { + m["status_name"] = label + } else { + m["status_name"] = status + } + } + if !yield(m) { + return + } + } + if len(result) < 1000 { + break + } + offset += 1000 + } + }) +} diff --git a/internal/service/tenant.go b/internal/service/tenant.go index 9e220f2..9c9cfd8 100644 --- a/internal/service/tenant.go +++ b/internal/service/tenant.go @@ -294,6 +294,9 @@ type ( // TenantTaskDetail 租户异步任务详情 // owner/admin 可查看租户所有任务,member 只能查看自己的任务 TenantTaskDetail(ctx context.Context, req *v1.TenantTaskDetailReq) (*v1.TenantTaskDetailRes, error) + // ExportTasks 导出异步任务日志 + // owner/admin 可导出租户所有任务,member 只能导出自己的任务 + ExportTasks(ctx context.Context, req *v1.TenantTaskExportReq) (*v1.TenantTaskExportRes, error) // TicketCreate 创建工单 TicketCreate(ctx context.Context, req *v1.TenantTicketCreateReq) (*v1.TenantTicketCreateRes, error) // TicketList 获取租户工单列表 diff --git a/web/admin/src/views/ai/TaskLogsPage.vue b/web/admin/src/views/ai/TaskLogsPage.vue index 5520d6c..2f3d738 100644 --- a/web/admin/src/views/ai/TaskLogsPage.vue +++ b/web/admin/src/views/ai/TaskLogsPage.vue @@ -37,6 +37,8 @@ const platformOptions = [ { label: 'Kling', value: 'kling' }, { label: 'Midjourney', value: 'midjourney' }, { label: 'Suno', value: 'suno' }, + { label: '火山引擎', value: 'volcengine' }, + { label: '阿里', value: 'ali' }, ] const statusTagColor: Record = { @@ -60,6 +62,8 @@ const platformLabel: Record = { kling: 'Kling', midjourney: 'Midjourney', suno: 'Suno', + volcengine: '火山引擎', + ali: '阿里', } const columns: TableColumnData[] = [ diff --git a/web/tenant/src/views/service/TaskLogsPage.vue b/web/tenant/src/views/service/TaskLogsPage.vue index cb8f155..4a4970b 100644 --- a/web/tenant/src/views/service/TaskLogsPage.vue +++ b/web/tenant/src/views/service/TaskLogsPage.vue @@ -5,6 +5,7 @@ import Icon from '@/components/common/Icon.vue' import BaseModal from '@/components/common/BaseModal.vue' import BaseSelect from '../../components/common/BaseSelect.vue' import request from '@/utils/request' +import { useExport } from '@/composables/useExport' interface TaskItem { id: number @@ -40,6 +41,15 @@ const showDetail = ref(false) const detailLoading = ref(false) const detailTask = ref(null) +const { exporting, exportFile } = useExport({ + url: '/tenant/tasks/export', + getFilters: () => ({ + status: filterStatus.value || undefined, + platform: filterPlatform.value || undefined, + public_task_id: filterTaskId.value || undefined, + }), +}) + const statusBadge: Record = { NOT_START: 'bg-gray-100 text-gray-800', SUBMITTED: 'bg-blue-100 text-blue-800', @@ -63,6 +73,8 @@ const platformLabel: Record = { kling: 'Kling', midjourney: 'Midjourney', suno: 'Suno', + volcengine: '火山引擎', + ali: '阿里', } function formatCost(n: number | undefined): string { @@ -153,11 +165,20 @@ onMounted(() => {