362 lines
11 KiB
Go
362 lines
11 KiB
Go
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/billing"
|
|
"github.com/qianfree/team-api/internal/logic/common"
|
|
"github.com/qianfree/team-api/internal/middleware"
|
|
"github.com/qianfree/team-api/internal/utility/export"
|
|
)
|
|
|
|
// TenantTaskList 租户异步任务列表
|
|
// owner/admin 可查看租户所有任务,member 只能查看自己的任务
|
|
func (s *sTenant) TenantTaskList(ctx context.Context, req *v1.TenantTaskListReq) (*v1.TenantTaskListRes, error) {
|
|
tenantID := middleware.GetTenantID(ctx)
|
|
role := middleware.GetUserRole(ctx)
|
|
userID := middleware.GetUserID(ctx)
|
|
|
|
if err := common.ValidateDateTimeParam(req.StartDate, "开始时间"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := common.ValidateDateTimeParam(req.EndDate, "结束时间"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := dao.TskModelTasks.Ctx(ctx).
|
|
LeftJoin("tnt_users u", "u.id = tsk_model_tasks.user_id AND u.tenant_id = tsk_model_tasks.tenant_id").
|
|
Fields("tsk_model_tasks.*, COALESCE(u.username, '') as username").
|
|
Where("tsk_model_tasks.tenant_id", tenantID).
|
|
OrderDesc("tsk_model_tasks.id")
|
|
|
|
// member 只能看自己的任务
|
|
if role == "member" {
|
|
m = m.Where("tsk_model_tasks.user_id", userID)
|
|
}
|
|
|
|
if req.Status != "" {
|
|
m = m.Where("tsk_model_tasks.status", req.Status)
|
|
}
|
|
if req.Platform != "" {
|
|
m = m.Where("tsk_model_tasks.platform", req.Platform)
|
|
}
|
|
if req.PublicTaskID != "" {
|
|
m = m.Where("tsk_model_tasks.public_task_id", req.PublicTaskID)
|
|
}
|
|
if req.StartDate != "" {
|
|
m = m.Where("tsk_model_tasks.created_at >= ?", common.StartOfRange(req.StartDate))
|
|
}
|
|
if req.EndDate != "" {
|
|
m = m.Where("tsk_model_tasks.created_at <= ?", common.EndOfRange(req.EndDate))
|
|
}
|
|
|
|
var tasks []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"`
|
|
PreDeductAmount float64 `json:"pre_deduct_amount"`
|
|
ActualCost float64 `json:"actual_cost"`
|
|
BillingSettled bool `json:"billing_settled"`
|
|
ResultUrl string `json:"result_url"`
|
|
Username string `json:"username"`
|
|
SubmitTime *string `json:"submit_time"`
|
|
FinishTime *string `json:"finish_time"`
|
|
CreatedAt *string `json:"created_at"`
|
|
}
|
|
|
|
var total int
|
|
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
|
err := m.Page(page, pageSize).ScanAndCount(&tasks, &total, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list := make([]v1.TenantTaskItem, 0, len(tasks))
|
|
for _, t := range tasks {
|
|
item := v1.TenantTaskItem{
|
|
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,
|
|
Username: t.Username,
|
|
}
|
|
if t.SubmitTime != nil {
|
|
item.SubmitTime = *t.SubmitTime
|
|
}
|
|
if t.FinishTime != nil {
|
|
item.FinishTime = *t.FinishTime
|
|
}
|
|
if t.CreatedAt != nil {
|
|
item.CreatedAt = *t.CreatedAt
|
|
}
|
|
list = append(list, item)
|
|
}
|
|
|
|
return &v1.TenantTaskListRes{
|
|
List: list,
|
|
Total: total,
|
|
Page: req.Page,
|
|
PageSize: req.PageSize,
|
|
}, nil
|
|
}
|
|
|
|
// TenantTaskDetail 租户异步任务详情
|
|
// owner/admin 可查看租户所有任务,member 只能查看自己的任务
|
|
func (s *sTenant) TenantTaskDetail(ctx context.Context, req *v1.TenantTaskDetailReq) (*v1.TenantTaskDetailRes, error) {
|
|
tenantID := middleware.GetTenantID(ctx)
|
|
role := middleware.GetUserRole(ctx)
|
|
userID := middleware.GetUserID(ctx)
|
|
|
|
var task *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"`
|
|
PreDeductAmount float64 `json:"pre_deduct_amount"`
|
|
ActualCost float64 `json:"actual_cost"`
|
|
BillingSettled bool `json:"billing_settled"`
|
|
ResultUrl string `json:"result_url"`
|
|
UserId int64 `json:"user_id"`
|
|
SubmitTime *string `json:"submit_time"`
|
|
FinishTime *string `json:"finish_time"`
|
|
CreatedAt *string `json:"created_at"`
|
|
}
|
|
|
|
m := dao.TskModelTasks.Ctx(ctx).Where("id", req.ID).Where("tenant_id", tenantID)
|
|
// member 只能看自己的任务
|
|
if role == "member" {
|
|
m = m.Where("user_id", userID)
|
|
}
|
|
|
|
err := m.Scan(&task)
|
|
if err = common.IgnoreScanNoRows(err); err != nil {
|
|
return nil, err
|
|
}
|
|
if task == nil {
|
|
return nil, common.NewNotFoundError("任务")
|
|
}
|
|
|
|
item := v1.TenantTaskItem{
|
|
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,
|
|
}
|
|
if task.SubmitTime != nil {
|
|
item.SubmitTime = *task.SubmitTime
|
|
}
|
|
if task.FinishTime != nil {
|
|
item.FinishTime = *task.FinishTime
|
|
}
|
|
if task.CreatedAt != nil {
|
|
item.CreatedAt = *task.CreatedAt
|
|
}
|
|
|
|
// 对 re-host 到对象存储的结果图,生成新鲜的缩略图 URL 供详情弹窗内联预览,并刷新原图 URL
|
|
// (规避 result_url 24h 预签名过期)。tenantID 已限定为当前租户,不跨租户。
|
|
if thumb, orig := common.TaskResultImageURLs(ctx, tenantID, task.PublicTaskId, 600); thumb != "" {
|
|
item.ResultThumbURL = thumb
|
|
if orig != "" {
|
|
item.ResultURL = orig
|
|
}
|
|
}
|
|
|
|
return &v1.TenantTaskDetailRes{
|
|
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)
|
|
|
|
if err := common.ValidateDateTimeParam(req.StartDate, "开始时间"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := common.ValidateDateTimeParam(req.EndDate, "结束时间"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
statusLabel := map[string]string{
|
|
"NOT_START": "未开始",
|
|
"SUBMITTED": "已提交",
|
|
"IN_PROGRESS": "进行中",
|
|
"SUCCESS": "成功",
|
|
"FAILURE": "失败",
|
|
"TIMEOUT": "超时",
|
|
}
|
|
|
|
platformLabel := map[string]string{
|
|
"sora": "Sora",
|
|
"kling": "Kling",
|
|
"midjourney": "Midjourney",
|
|
"suno": "Suno",
|
|
"volcengine": "火山引擎",
|
|
"ali": "阿里",
|
|
"minimax": "MiniMax",
|
|
}
|
|
|
|
// 预扣/实际费用属 bil_ 记账层,币种 = 系统本位币
|
|
moneyCurrency := billing.Currency(ctx)
|
|
|
|
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: "预扣金额(" + moneyCurrency + ")"},
|
|
{Field: "actual_cost", Header: "实际费用(" + moneyCurrency + ")"},
|
|
{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)
|
|
}
|
|
if req.StartDate != "" {
|
|
conditions = append(conditions, "t.created_at >= ?")
|
|
args = append(args, common.StartOfRange(req.StartDate))
|
|
}
|
|
if req.EndDate != "" {
|
|
conditions = append(conditions, "t.created_at <= ?")
|
|
args = append(args, common.EndOfRange(req.EndDate))
|
|
}
|
|
|
|
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
|
|
}
|
|
})
|
|
}
|