Files
team-api/internal/logic/task/async_provider.go
T

470 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package task
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/relay"
do "github.com/qianfree/team-api/internal/model/do"
"github.com/qianfree/team-api/internal/utility/crypto"
"github.com/qianfree/team-api/relay/common"
)
// AsyncProvider 异步任务数据持久化实现
type AsyncProvider struct{}
var DefaultAsyncProvider = &AsyncProvider{}
// CreateTask 创建异步任务记录
func (p *AsyncProvider) CreateTask(ctx context.Context, task *common.AsyncTask) error {
_, err := dao.TskModelTasks.Ctx(ctx).Insert(map[string]any{
"public_task_id": task.PublicTaskID,
"request_id": task.RequestID,
"platform": task.Platform,
"action": task.Action,
"status": task.Status,
"progress": task.Progress,
"fail_reason": task.FailReason,
"tenant_id": task.TenantID,
"user_id": task.UserID,
"api_key_id": task.ApiKeyID,
"channel_id": task.ChannelID,
"model_name": task.ModelName,
"upstream_model": task.UpstreamModel,
"pre_deduct_amount": task.PreDeductAmount,
"actual_cost": task.ActualCost,
"billing_settled": task.BillingSettled,
"result_url": task.ResultURL,
"data": task.Data,
"private_data": task.PrivateData,
"submit_time": task.SubmitTime,
})
if err != nil {
return gerror.Wrapf(err, "create async task failed: public_id=%s", task.PublicTaskID)
}
IncrActiveTask()
return nil
}
// UpdateTask 更新任务记录
func (p *AsyncProvider) UpdateTask(ctx context.Context, task *common.AsyncTask) error {
_, err := dao.TskModelTasks.Ctx(ctx).
Where("id", task.ID).
Update(map[string]any{
"status": task.Status,
"progress": task.Progress,
"fail_reason": task.FailReason,
"actual_cost": task.ActualCost,
"billing_settled": task.BillingSettled,
"result_url": task.ResultURL,
"data": task.Data,
"start_time": task.StartTime,
"finish_time": task.FinishTime,
"updated_at": time.Now(),
})
if err != nil {
return gerror.Wrapf(err, "update async task failed: id=%d", task.ID)
}
return nil
}
// UpdateTaskCAS CAS 状态更新
func (p *AsyncProvider) UpdateTaskCAS(ctx context.Context, task *common.AsyncTask, oldStatus string) error {
result, err := dao.TskModelTasks.Ctx(ctx).
Where("id", task.ID).
Where("status", oldStatus).
Update(map[string]any{
"status": task.Status,
"progress": task.Progress,
"fail_reason": task.FailReason,
"actual_cost": task.ActualCost,
"billing_settled": task.BillingSettled,
"result_url": task.ResultURL,
"data": task.Data,
"start_time": task.StartTime,
"finish_time": task.FinishTime,
"updated_at": time.Now(),
})
if err != nil {
return gerror.Wrapf(err, "CAS update async task failed: id=%d", task.ID)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("CAS conflict: task %d status changed from %s by another process", task.ID, oldStatus)
}
return nil
}
// GetTaskByPublicID 根据公开任务 ID 查询
func (p *AsyncProvider) GetTaskByPublicID(ctx context.Context, publicTaskID string) (*common.AsyncTask, error) {
var row struct {
ID int64 `json:"id"`
PublicTaskID string `json:"public_task_id"`
RequestId string `json:"request_id"`
Platform string `json:"platform"`
Action string `json:"action"`
Status string `json:"status"`
Progress string `json:"progress"`
FailReason string `json:"fail_reason"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
ApiKeyID int64 `json:"api_key_id"`
ChannelID int64 `json:"channel_id"`
ModelName string `json:"model_name"`
UpstreamModel string `json:"upstream_model"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
ActualCost float64 `json:"actual_cost"`
BillingSettled bool `json:"billing_settled"`
ResultURL string `json:"result_url"`
Data json.RawMessage `json:"data"`
PrivateData json.RawMessage `json:"private_data"`
SubmitTime *time.Time `json:"submit_time"`
StartTime *time.Time `json:"start_time"`
FinishTime *time.Time `json:"finish_time"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
err := dao.TskModelTasks.Ctx(ctx).
Where("public_task_id", publicTaskID).
Scan(&row)
if err != nil {
return nil, gerror.Wrapf(err, "query async task failed: public_id=%s", publicTaskID)
}
if row.ID == 0 {
return nil, nil
}
return &common.AsyncTask{
ID: row.ID,
PublicTaskID: row.PublicTaskID,
RequestID: row.RequestId,
Platform: row.Platform,
Action: row.Action,
Status: row.Status,
Progress: row.Progress,
FailReason: row.FailReason,
TenantID: row.TenantID,
UserID: row.UserID,
ApiKeyID: row.ApiKeyID,
ChannelID: row.ChannelID,
ModelName: row.ModelName,
UpstreamModel: row.UpstreamModel,
PreDeductAmount: row.PreDeductAmount,
ActualCost: row.ActualCost,
BillingSettled: row.BillingSettled,
ResultURL: row.ResultURL,
Data: row.Data,
PrivateData: row.PrivateData,
SubmitTime: row.SubmitTime,
StartTime: row.StartTime,
FinishTime: row.FinishTime,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}, nil
}
// GetTaskByPublicIDAndUser 根据公开任务 ID + 用户 ID 查询
func (p *AsyncProvider) GetTaskByPublicIDAndUser(ctx context.Context, publicTaskID string, userID int64) (*common.AsyncTask, error) {
var row struct {
ID int64 `json:"id"`
PublicTaskID string `json:"public_task_id"`
RequestId string `json:"request_id"`
Platform string `json:"platform"`
Action string `json:"action"`
Status string `json:"status"`
Progress string `json:"progress"`
FailReason string `json:"fail_reason"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
ApiKeyID int64 `json:"api_key_id"`
ChannelID int64 `json:"channel_id"`
ModelName string `json:"model_name"`
UpstreamModel string `json:"upstream_model"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
ActualCost float64 `json:"actual_cost"`
BillingSettled bool `json:"billing_settled"`
ResultURL string `json:"result_url"`
Data json.RawMessage `json:"data"`
SubmitTime *time.Time `json:"submit_time"`
StartTime *time.Time `json:"start_time"`
FinishTime *time.Time `json:"finish_time"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
err := dao.TskModelTasks.Ctx(ctx).
Where("public_task_id", publicTaskID).
Where("user_id", userID).
Scan(&row)
if err != nil {
return nil, gerror.Wrapf(err, "query async task failed: public_id=%s user_id=%d", publicTaskID, userID)
}
if row.ID == 0 {
return nil, nil
}
return &common.AsyncTask{
ID: row.ID,
PublicTaskID: row.PublicTaskID,
RequestID: row.RequestId,
Platform: row.Platform,
Action: row.Action,
Status: row.Status,
Progress: row.Progress,
FailReason: row.FailReason,
TenantID: row.TenantID,
UserID: row.UserID,
ApiKeyID: row.ApiKeyID,
ChannelID: row.ChannelID,
ModelName: row.ModelName,
UpstreamModel: row.UpstreamModel,
PreDeductAmount: row.PreDeductAmount,
ActualCost: row.ActualCost,
BillingSettled: row.BillingSettled,
ResultURL: row.ResultURL,
Data: row.Data,
SubmitTime: row.SubmitTime,
StartTime: row.StartTime,
FinishTime: row.FinishTime,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}, nil
}
// GetNonTerminalTasks 获取所有非终态任务
func (p *AsyncProvider) GetNonTerminalTasks(ctx context.Context, limit int) ([]*common.AsyncTask, error) {
var rows []struct {
ID int64 `json:"id"`
PublicTaskID string `json:"public_task_id"`
RequestId string `json:"request_id"`
Platform string `json:"platform"`
Action string `json:"action"`
Status string `json:"status"`
Progress string `json:"progress"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
ApiKeyID int64 `json:"api_key_id"`
ChannelID int64 `json:"channel_id"`
ModelName string `json:"model_name"`
UpstreamModel string `json:"upstream_model"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
ActualCost float64 `json:"actual_cost"`
BillingSettled bool `json:"billing_settled"`
Data json.RawMessage `json:"data"`
PrivateData json.RawMessage `json:"private_data"`
SubmitTime *time.Time `json:"submit_time"`
CreatedAt time.Time `json:"created_at"`
}
err := dao.TskModelTasks.Ctx(ctx).
Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE").
Order("submit_time ASC").
Limit(limit).
Scan(&rows)
if err != nil {
return nil, gerror.Wrapf(err, "query non-terminal tasks failed")
}
tasks := make([]*common.AsyncTask, 0, len(rows))
for _, r := range rows {
tasks = append(tasks, &common.AsyncTask{
ID: r.ID,
PublicTaskID: r.PublicTaskID,
RequestID: r.RequestId,
Platform: r.Platform,
Action: r.Action,
Status: r.Status,
Progress: r.Progress,
TenantID: r.TenantID,
UserID: r.UserID,
ApiKeyID: r.ApiKeyID,
ChannelID: r.ChannelID,
ModelName: r.ModelName,
UpstreamModel: r.UpstreamModel,
PreDeductAmount: r.PreDeductAmount,
ActualCost: r.ActualCost,
BillingSettled: r.BillingSettled,
Data: r.Data,
PrivateData: r.PrivateData,
SubmitTime: r.SubmitTime,
CreatedAt: r.CreatedAt,
})
}
return tasks, nil
}
// GetTimedOutTasks 获取超时未完成任务
func (p *AsyncProvider) GetTimedOutTasks(ctx context.Context, cutoffUnix int64, limit int) ([]*common.AsyncTask, error) {
cutoffTime := time.Unix(cutoffUnix, 0)
var rows []struct {
ID int64 `json:"id"`
PublicTaskID string `json:"public_task_id"`
RequestId string `json:"request_id"`
Platform string `json:"platform"`
Status string `json:"status"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
ApiKeyID int64 `json:"api_key_id"`
ChannelID int64 `json:"channel_id"`
ModelName string `json:"model_name"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
PrivateData json.RawMessage `json:"private_data"`
SubmitTime *time.Time `json:"submit_time"`
}
err := dao.TskModelTasks.Ctx(ctx).
Where("status NOT IN (?, ?)", "SUCCESS", "FAILURE").
Where("submit_time < ?", cutoffTime).
Order("submit_time ASC").
Limit(limit).
Scan(&rows)
if err != nil {
return nil, gerror.Wrapf(err, "query timed-out tasks failed")
}
tasks := make([]*common.AsyncTask, 0, len(rows))
for _, r := range rows {
tasks = append(tasks, &common.AsyncTask{
ID: r.ID,
PublicTaskID: r.PublicTaskID,
RequestID: r.RequestId,
Platform: r.Platform,
Status: r.Status,
TenantID: r.TenantID,
UserID: r.UserID,
ApiKeyID: r.ApiKeyID,
ChannelID: r.ChannelID,
ModelName: r.ModelName,
PreDeductAmount: r.PreDeductAmount,
PrivateData: r.PrivateData,
SubmitTime: r.SubmitTime,
})
}
return tasks, nil
}
// GetUnsettledTasks 获取终态但未结算的任务(用于结算重试)
func (p *AsyncProvider) GetUnsettledTasks(ctx context.Context, limit int) ([]*common.AsyncTask, error) {
var rows []struct {
ID int64 `json:"id"`
PublicTaskID string `json:"public_task_id"`
RequestId string `json:"request_id"`
Platform string `json:"platform"`
Status string `json:"status"`
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
ApiKeyID int64 `json:"api_key_id"`
ChannelID int64 `json:"channel_id"`
ModelName string `json:"model_name"`
PreDeductAmount float64 `json:"pre_deduct_amount"`
ActualCost float64 `json:"actual_cost"`
BillingSettled bool `json:"billing_settled"`
Data json.RawMessage `json:"data"`
PrivateData json.RawMessage `json:"private_data"`
SubmitTime *time.Time `json:"submit_time"`
CreatedAt time.Time `json:"created_at"`
}
err := dao.TskModelTasks.Ctx(ctx).
Where("status IN (?, ?)", "SUCCESS", "FAILURE").
Where("billing_settled = ?", false).
Where("pre_deduct_amount > 0").
Order("submit_time ASC").
Limit(limit).
Scan(&rows)
if err != nil {
return nil, gerror.Wrapf(err, "query unsettled tasks failed")
}
tasks := make([]*common.AsyncTask, 0, len(rows))
for _, r := range rows {
tasks = append(tasks, &common.AsyncTask{
ID: r.ID,
PublicTaskID: r.PublicTaskID,
RequestID: r.RequestId,
Platform: r.Platform,
Status: r.Status,
TenantID: r.TenantID,
UserID: r.UserID,
ApiKeyID: r.ApiKeyID,
ChannelID: r.ChannelID,
ModelName: r.ModelName,
PreDeductAmount: r.PreDeductAmount,
ActualCost: r.ActualCost,
BillingSettled: r.BillingSettled,
Data: r.Data,
PrivateData: r.PrivateData,
SubmitTime: r.SubmitTime,
CreatedAt: r.CreatedAt,
})
}
return tasks, nil
}
// GetChannelByID 获取渠道基本信息(含从 chn_channel_keys 解密的 API Key)
func (p *AsyncProvider) GetChannelByID(ctx context.Context, channelID int64) (*common.ChannelBasicInfo, error) {
var row struct {
ID int64 `json:"id"`
Type int `json:"type"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
Settings json.RawMessage `json:"settings"`
}
err := dao.ChnChannels.Ctx(ctx).
Where("id", channelID).
Fields("id, type, name, base_url, settings").
Scan(&row)
if err != nil {
return nil, gerror.Wrapf(err, "query channel failed: id=%d", channelID)
}
if row.ID == 0 {
return nil, nil
}
// 从 chn_channel_keys 获取解密后的 API Key
apiKey, keyErr := getChannelApiKey(ctx, channelID)
if keyErr != nil {
return nil, gerror.Wrapf(keyErr, "get channel key failed: channelID=%d", channelID)
}
return &common.ChannelBasicInfo{
ID: row.ID,
Type: row.Type,
Name: row.Name,
BaseURL: row.BaseURL,
ApiKey: apiKey,
Settings: row.Settings,
}, nil
}
// getChannelApiKey 从 chn_channel_keys 获取并解密渠道 API Key
func getChannelApiKey(ctx context.Context, channelID int64) (string, error) {
type keyRow struct {
ID int64 `json:"id"`
EncryptedKey string `json:"encrypted_key"`
}
var key *keyRow
err := dao.ChnChannelKeys.Ctx(ctx).
Where("channel_id", channelID).
Where("status", "active").
Fields("id, encrypted_key").
Scan(&key)
if err != nil || key == nil {
return "", fmt.Errorf("no active key found for channel %d", channelID)
}
// 更新最后使用时间
dao.ChnChannelKeys.Ctx(ctx).
Where("id", key.ID).
Data(do.ChnChannelKeys{LastUsedAt: gtime.Now()}).
Update()
encKey := relay.GetEncryptionKey()
decrypted, err := crypto.DecryptString(encKey, key.EncryptedKey)
if err != nil {
return "", fmt.Errorf("decrypt key failed: %w", err)
}
return decrypted, nil
}