improve:优化租户端任务日志

This commit is contained in:
martin committed 2026-06-10 22:08:36 +08:00
1 parent 1bede7b0ea
commit 2425cbad23
7 files changed
+209 -19

No files matched your search

+1
View File
@@ -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)
+11
View File
@@ -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{}
@@ -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)
}
+147
View File
@@ -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
}
})
}
+3
View File
@@ -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 获取租户工单列表
+4
View File
@@ -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<string, string | undefined> = {
@@ -60,6 +62,8 @@ const platformLabel: Record<string, string> = {
kling: 'Kling',
midjourney: 'Midjourney',
suno: 'Suno',
volcengine: '火山引擎',
ali: '阿里',
}
const columns: TableColumnData[] = [
+40 -19
View File
@@ -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<TaskItem | null>(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<string, string> = {
NOT_START: 'bg-gray-100 text-gray-800',
SUBMITTED: 'bg-blue-100 text-blue-800',
@@ -63,6 +73,8 @@ const platformLabel: Record<string, string> = {
kling: 'Kling',
midjourney: 'Midjourney',
suno: 'Suno',
volcengine: '火山引擎',
ali: '阿里',
}
function formatCost(n: number | undefined): string {
@@ -153,11 +165,20 @@ onMounted(() => {
<template>
<div class="space-y-6">
<!-- Page Header -->
<div class="page-header">
<div class="page-header flex items-center justify-between">
<div>
<h1 class="page-title">任务日志</h1>
<p class="page-description">查看异步生成任务(视频/图片/音乐)的执行记录</p>
</div>
<div class="flex items-center gap-2">
<button class="btn btn-secondary btn-sm" :disabled="exporting || loading" @click="exportFile('csv')">
<Icon v-if="exporting" name="refresh" size="xs" class="animate-spin" />
{{ exporting ? '导出中...' : '导出 CSV' }}
</button>
<button class="btn btn-secondary btn-sm" :disabled="exporting || loading" @click="exportFile('xlsx')">
{{ exporting ? '导出中...' : '导出 Excel' }}
</button>
</div>
</div>
<!-- Filters -->
@@ -174,7 +195,7 @@ onMounted(() => {
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500 whitespace-nowrap">平台</label>
<BaseSelect v-model="filterPlatform" :options="[{value:'',label:'全部'},{value:'sora',label:'Sora'},{value:'kling',label:'Kling'},{value:'midjourney',label:'Midjourney'},{value:'suno',label:'Suno'}]" container-class="w-[120px]" />
<BaseSelect v-model="filterPlatform" :options="[{value:'',label:'全部'},{value:'sora',label:'Sora'},{value:'kling',label:'Kling'},{value:'midjourney',label:'Midjourney'},{value:'suno',label:'Suno'},{value:'volcengine',label:'火山引擎'},{value:'ali',label:'阿里'}]" container-class="w-[120px]" />
</div>
<div class="ml-auto flex items-center gap-2">
<button class="btn btn-primary btn-sm" @click="applyFilters">搜索</button>
@@ -199,25 +220,25 @@ onMounted(() => {
<div v-else>
<div class="table-container">
<table class="table">
<table class="table table-fixed w-full">
<thead>
<tr>
<th>任务 ID</th>
<th>平台</th>
<th>状态</th>
<th>模型</th>
<th>费用</th>
<th>提交时间</th>
<th>完成时间</th>
<th></th>
<th class="w-52">任务 ID</th>
<th class="w-24">平台</th>
<th class="w-20">状态</th>
<th class="w-44">模型</th>
<th class="w-25">费用</th>
<th class="w-38">提交时间</th>
<th class="w-38">完成时间</th>
<th class="w-15">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="task in tasks" :key="task.id">
<td>
<td class="truncate" :title="task.public_task_id">
<span class="font-mono text-xs text-gray-600">{{ task.public_task_id }}</span>
</td>
<td>
<td class="truncate">
<span class="text-sm font-medium text-gray-700">{{ platformLabel[task.platform] || task.platform }}</span>
</td>
<td>
@@ -225,19 +246,19 @@ onMounted(() => {
{{ statusLabel[task.status] || task.status }}
</span>
</td>
<td>
<td class="truncate" :title="task.model_name">
<span class="text-sm text-gray-700">{{ task.model_name || '-' }}</span>
</td>
<td>
<td class="truncate">
<span v-if="task.billing_settled && task.actual_cost > 0" class="text-sm font-medium text-emerald-600">{{ formatCost(task.actual_cost) }}</span>
<span v-else-if="task.pre_deduct_amount > 0" class="text-sm text-gray-500">{{ formatCost(task.pre_deduct_amount) }} <span class="text-xs text-gray-400">(预扣)</span></span>
<span v-else class="text-sm text-gray-400">-</span>
</td>
<td>
<span class="text-xs text-gray-500 whitespace-nowrap">{{ formatTime(task.submit_time) }}</span>
<td class="truncate">
<span class="text-xs text-gray-500">{{ formatTime(task.submit_time) }}</span>
</td>
<td>
<span class="text-xs text-gray-500 whitespace-nowrap">{{ formatTime(task.finish_time) }}</span>
<td class="truncate">
<span class="text-xs text-gray-500">{{ formatTime(task.finish_time) }}</span>
</td>
<td>
<button class="btn btn-ghost btn-sm p-1.5" title="查看详情" @click="openDetail(task)">