Files

359 lines
12 KiB
Go
Raw Permalink 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/frame/g"
"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/model/entity"
"github.com/qianfree/team-api/internal/utility/crypto"
"github.com/qianfree/team-api/relay/common"
)
// AsyncProvider 异步任务数据持久化实现
type AsyncProvider struct{}
var DefaultAsyncProvider = &AsyncProvider{}
func asyncTaskFromEntity(row *entity.TskModelTasks) *common.AsyncTask {
if row == nil {
return 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: json.RawMessage(row.Data),
PrivateData: json.RawMessage(row.PrivateData),
SubmitTime: timePtrFromGTime(row.SubmitTime),
StartTime: timePtrFromGTime(row.StartTime),
FinishTime: timePtrFromGTime(row.FinishTime),
CreatedAt: timeFromGTime(row.CreatedAt),
UpdatedAt: timeFromGTime(row.UpdatedAt),
Deleted: row.Deleted,
DeletedAt: timePtrFromGTime(row.DeletedAt),
}
}
func asyncTasksFromEntities(rows []*entity.TskModelTasks) []*common.AsyncTask {
tasks := make([]*common.AsyncTask, 0, len(rows))
for _, row := range rows {
if task := asyncTaskFromEntity(row); task != nil {
tasks = append(tasks, task)
}
}
return tasks
}
func timePtrFromGTime(value *gtime.Time) *time.Time {
if value == nil {
return nil
}
t := value.Time
return &t
}
func timeFromGTime(value *gtime.Time) time.Time {
if value == nil {
return time.Time{}
}
return value.Time
}
// CreateTask 创建异步任务记录
func (p *AsyncProvider) CreateTask(ctx context.Context, task *common.AsyncTask) error {
g.Log().Infof(ctx, "[AsyncProvider] CreateTask start: public_id=%s, tenant=%d, user=%d, model=%s, platform=%s",
task.PublicTaskID, task.TenantID, task.UserID, task.ModelName, task.Platform)
// 转换时间类型
var submitTime *gtime.Time
if task.SubmitTime != nil {
submitTime = gtime.NewFromTime(*task.SubmitTime)
}
result, err := dao.TskModelTasks.Ctx(ctx).Data(do.TskModelTasks{
PublicTaskId: task.PublicTaskID,
RequestId: task.RequestID,
Platform: task.Platform,
Action: task.Action,
Status: task.Status,
Progress: task.Progress,
FailReason: task.FailReason,
TenantId: task.TenantID,
UserId: task.UserID,
ApiKeyId: task.ApiKeyID,
ChannelId: task.ChannelID,
ModelName: task.ModelName,
UpstreamModel: task.UpstreamModel,
PreDeductAmount: task.PreDeductAmount,
ActualCost: task.ActualCost,
BillingSettled: task.BillingSettled,
ResultUrl: task.ResultURL,
Data: task.Data,
PrivateData: task.PrivateData,
SubmitTime: submitTime,
}).Insert()
if err != nil {
g.Log().Errorf(ctx, "[AsyncProvider] CreateTask insert error: public_id=%s, err=%v", task.PublicTaskID, err)
return gerror.Wrapf(err, "create async task failed: public_id=%s", task.PublicTaskID)
}
// 验证插入是否成功
rowsAffected, err := result.RowsAffected()
if err != nil {
g.Log().Errorf(ctx, "[AsyncProvider] CreateTask get rows affected error: public_id=%s, err=%v", task.PublicTaskID, err)
return gerror.Wrapf(err, "get rows affected failed: public_id=%s", task.PublicTaskID)
}
if rowsAffected == 0 {
g.Log().Errorf(ctx, "[AsyncProvider] CreateTask no rows affected: public_id=%s", task.PublicTaskID)
return gerror.Newf("insert failed: no rows affected, public_id=%s", task.PublicTaskID)
}
lastInsertId, _ := result.LastInsertId()
g.Log().Infof(ctx, "[AsyncProvider] CreateTask success: public_id=%s, id=%d, rows=%d",
task.PublicTaskID, lastInsertId, rowsAffected)
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 *entity.TskModelTasks
err := dao.TskModelTasks.Ctx(ctx).
Where("public_task_id", publicTaskID).
Where("deleted", false).
Scan(&row)
if err != nil {
return nil, gerror.Wrapf(err, "query async task failed: public_id=%s", publicTaskID)
}
return asyncTaskFromEntity(row), nil
}
// GetTaskByPublicIDAndUser 根据公开任务 ID + 用户 ID 查询(软删除的任务视为不存在)
func (p *AsyncProvider) GetTaskByPublicIDAndUser(ctx context.Context, publicTaskID string, userID int64, tenantID int64) (*common.AsyncTask, error) {
var row *entity.TskModelTasks
err := dao.TskModelTasks.Ctx(ctx).
Where("public_task_id", publicTaskID).
Where("user_id", userID).
Where("tenant_id", tenantID).
Where("deleted", false).
Scan(&row)
if err != nil {
return nil, gerror.Wrapf(err, "query async task failed: public_id=%s user_id=%d", publicTaskID, userID)
}
return asyncTaskFromEntity(row), nil
}
// GetNonTerminalTasks 获取所有非终态任务
func (p *AsyncProvider) GetNonTerminalTasks(ctx context.Context, limit int) ([]*common.AsyncTask, error) {
var rows []*entity.TskModelTasks
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")
}
return asyncTasksFromEntities(rows), nil
}
// GetTimedOutTasks 获取超时未完成任务
func (p *AsyncProvider) GetTimedOutTasks(ctx context.Context, cutoffUnix int64, limit int) ([]*common.AsyncTask, error) {
cutoffTime := time.Unix(cutoffUnix, 0)
var rows []*entity.TskModelTasks
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")
}
return asyncTasksFromEntities(rows), nil
}
// GetUnsettledTasks 获取终态但未结算的任务(用于结算重试)
func (p *AsyncProvider) GetUnsettledTasks(ctx context.Context, limit int) ([]*common.AsyncTask, error) {
var rows []*entity.TskModelTasks
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")
}
return asyncTasksFromEntities(rows), nil
}
// SoftDeleteTask 软删除任务(DELETE /v1/videos/{id})。
// 仅标记 deleted,不动 status/计费字段:调用方保证只有终态且已结算的任务可删,
// 轮询 cron 的 GetNonTerminalTasks/GetUnsettledTasks 查询不感知 deleted 列。
func (p *AsyncProvider) SoftDeleteTask(ctx context.Context, task *common.AsyncTask) error {
result, err := dao.TskModelTasks.Ctx(ctx).
Where("id", task.ID).
Where("tenant_id", task.TenantID).
Update(map[string]any{
"deleted": true,
"deleted_at": gtime.Now(),
"result_url": "", // 清掉成品直链,避免软删后内部路径仍可寻址
"updated_at": time.Now(),
})
if err != nil {
return gerror.Wrapf(err, "soft delete async task failed: id=%d", task.ID)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("soft delete no rows affected: id=%d", task.ID)
}
g.Log().Infof(ctx, "[AsyncProvider] SoftDeleteTask: public_id=%s, tenant=%d", task.PublicTaskID, task.TenantID)
return 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 == nil {
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
}