improve:优化租户端任务日志
This commit is contained in:
1 parent
1bede7b0ea
commit
2425cbad23
7 files changed
+209
-19
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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 获取租户工单列表
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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)">
|
||||
|
||||
Reference in new issue
Block a user