Files

240 lines
7.8 KiB
Go

package admin
import (
"context"
"encoding/json"
"time"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
do "github.com/qianfree/team-api/internal/model/do"
)
// TaskList 大模型异步任务列表
func (s *sAdmin) TaskList(ctx context.Context, req *v1.TaskListReq) (*v1.TaskListRes, error) {
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).OrderDesc("id")
if req.Status != "" {
m = m.Where("status", req.Status)
}
if req.Platform != "" {
m = m.Where("platform", req.Platform)
}
if req.PublicTaskID != "" {
m = m.Where("public_task_id", req.PublicTaskID)
}
if req.ModelName != "" {
m = m.Where("model_name", req.ModelName)
}
if req.TenantID > 0 {
m = m.Where("tenant_id", req.TenantID)
}
if req.UserID > 0 {
m = m.Where("user_id", req.UserID)
}
if req.StartDate != "" {
m = m.Where("created_at >= ?", common.StartOfRange(req.StartDate))
}
if req.EndDate != "" {
m = m.Where("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"`
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"`
}
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
var total int
err := m.Page(page, pageSize).ScanAndCount(&tasks, &total, false)
if err != nil {
return nil, err
}
list := make([]v1.ModelTaskItem, 0, len(tasks))
for _, t := range tasks {
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.SubmitTime != nil {
item.SubmitTime = t.SubmitTime.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)
}
return &v1.TaskListRes{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// TaskDetail 大模型异步任务详情
func (s *sAdmin) TaskDetail(ctx context.Context, req *v1.TaskDetailReq) (*v1.TaskDetailRes, error) {
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"`
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"`
PrivateData string `json:"private_data"`
RequestId string `json:"request_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.TskModelTasks.Ctx(ctx).Where("id", req.ID).Scan(&task)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if task == nil {
return nil, gerror.NewCode(gcode.New(consts.CodeNotFound, consts.MsgNotFound, nil), consts.MsgNotFound)
}
item := v1.ModelTaskItem{
ID: task.Id,
PublicTaskID: task.PublicTaskId,
Platform: task.Platform,
Action: task.Action,
Status: task.Status,
Progress: task.Progress,
ModelName: task.ModelName,
UpstreamModel: task.UpstreamModel,
FailReason: task.FailReason,
PreDeductAmount: task.PreDeductAmount,
ActualCost: task.ActualCost,
BillingSettled: task.BillingSettled,
ResultURL: task.ResultUrl,
TenantID: task.TenantId,
UserID: task.UserId,
RequestID: task.RequestId,
CreatedAt: task.CreatedAt.Format("Y-m-d H:i:s"),
}
if task.SubmitTime != nil {
item.SubmitTime = task.SubmitTime.Format("Y-m-d H:i:s")
}
if task.StartTime != nil {
item.StartTime = task.StartTime.Format("Y-m-d H:i:s")
}
if task.FinishTime != nil {
item.FinishTime = task.FinishTime.Format("Y-m-d H:i:s")
}
// 从 private_data 提取上游排障标识:仅取 upstream_task_id / upstream_request_id 两个键,
// 计费上下文等其余内部数据不外露(同步图片任务无上游任务 ID,两字段为空)
if task.PrivateData != "" {
var pd struct {
UpstreamTaskID string `json:"upstream_task_id"`
UpstreamRequestID string `json:"upstream_request_id"`
}
if json.Unmarshal([]byte(task.PrivateData), &pd) == nil {
item.UpstreamTaskID = pd.UpstreamTaskID
item.UpstreamRequestID = pd.UpstreamRequestID
}
}
// 对 re-host 到对象存储的结果图,生成新鲜的缩略图 URL 供详情弹窗内联预览,并刷新原图 URL
// (规避 result_url 24h 预签名过期)。非图片/上游直链任务查不到文件记录,保持原 result_url。
if thumb, orig := common.TaskResultImageURLs(ctx, task.TenantId, task.PublicTaskId, 600); thumb != "" {
item.ResultThumbURL = thumb
if orig != "" {
item.ResultURL = orig
}
}
return &v1.TaskDetailRes{
Task: item,
}, nil
}
// TaskCancel 取消大模型异步任务
func (s *sAdmin) TaskCancel(ctx context.Context, req *v1.TaskCancelReq) (*v1.TaskCancelRes, error) {
result, err := dao.TskModelTasks.Ctx(ctx).
Where("id", req.ID).
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, common.NewBadRequestError("任务无法取消(可能已完成或已失败)")
}
return &v1.TaskCancelRes{}, nil
}
// MarkStuckTasksFailed 标记卡住的系统任务为失败(定时任务,操作 tsk_tasks 表)
func MarkStuckTasksFailed(ctx context.Context) error {
threshold := gtime.Now().Add(-30 * time.Minute)
_, err := dao.TskTasks.Ctx(ctx).
Where("status", "running").
Where("started_at < ?", threshold).
Data(do.TskTasks{
Status: "failed",
ErrorMessage: "任务执行超时(30分钟无响应)",
FinishedAt: gtime.Now(),
}).Update()
return err
}