package app import ( "context" "encoding/json" "fmt" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/response" ) type InstantTaskService struct { pool *pgxpool.Pool } func NewInstantTaskService(pool *pgxpool.Pool) *InstantTaskService { return &InstantTaskService{pool: pool} } type InstantTaskListParams struct { PromptRuleID *int64 Status *string Keyword *string CreatedFrom *time.Time CreatedTo *time.Time Page int PageSize int } type InstantTaskResponse struct { ID int64 `json:"id"` ArticleID *int64 `json:"article_id"` TaskBatchID *string `json:"task_batch_id"` PromptRuleID *int64 `json:"prompt_rule_id"` PromptRuleName *string `json:"prompt_rule_name"` Name string `json:"name"` Status string `json:"status"` ExecutionTime *string `json:"execution_time"` AutoPublishPlatforms []string `json:"auto_publish_platforms"` GenerateCount int `json:"generate_count"` Articles []InstantTaskArticleLink `json:"articles"` CreatedAt string `json:"created_at"` } type InstantTaskArticleLink struct { ArticleID int64 `json:"article_id"` Title *string `json:"title"` GenerateStatus string `json:"generate_status"` CreatedAt string `json:"created_at"` } type InstantTaskListResponse struct { Items []InstantTaskResponse `json:"items"` Total int64 `json:"total"` } func (s *InstantTaskService) List(ctx context.Context, params InstantTaskListParams) (*InstantTaskListResponse, error) { actor := auth.MustActor(ctx) if params.Page < 1 { params.Page = 1 } if params.PageSize < 1 || params.PageSize > 100 { params.PageSize = 20 } offset := (params.Page - 1) * params.PageSize const instantTaskBatchCTE = ` WITH base AS ( SELECT gt.id, gt.article_id, gt.task_batch_id, gt.operator_id, pr.id AS prompt_rule_id, pr.name AS prompt_rule_name, COALESCE(NULLIF(gt.input_params_json ->> 'task_name', ''), pr.name, '即时任务') AS task_name, gt.status, COALESCE(gt.started_at, gt.completed_at, gt.created_at) AS execution_time, gt.input_params_json, gt.created_at, CASE WHEN (gt.input_params_json ->> 'generate_count') ~ '^[0-9]+$' THEN GREATEST(1, LEAST(20, (gt.input_params_json ->> 'generate_count')::int)) ELSE 1 END AS generate_count, a.id AS active_article_id, COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')) AS article_title, COALESCE(a.generate_status, gt.status) AS article_generate_status, COALESCE(a.created_at, gt.created_at) AS article_created_at FROM generation_tasks gt LEFT JOIN articles a ON a.id = gt.article_id AND a.deleted_at IS NULL LEFT JOIN article_versions av ON av.id = a.current_version_id LEFT JOIN prompt_rules pr ON pr.id = COALESCE(a.prompt_rule_id, NULLIF(gt.input_params_json ->> 'prompt_rule_id', '')::bigint) WHERE gt.tenant_id = $1 AND gt.task_type = 'custom_generation' AND COALESCE(NULLIF(gt.input_params_json ->> 'generation_mode', ''), 'instant') = 'instant' AND ($2::bigint IS NULL OR pr.id = $2) AND ($4::text IS NULL OR COALESCE(NULLIF(gt.input_params_json ->> 'task_name', ''), pr.name, '') ILIKE '%' || $4 || '%') AND ($5::timestamptz IS NULL OR gt.created_at >= $5) AND ($6::timestamptz IS NULL OR gt.created_at <= $6) ), keyed AS ( SELECT *, COALESCE( NULLIF(task_batch_id, ''), NULLIF(input_params_json ->> 'task_batch_id', ''), CASE WHEN generate_count > 1 THEN CONCAT( 'legacy:', COALESCE(operator_id, 0), ':', COALESCE(prompt_rule_id, 0), ':', task_name, ':', generate_count, ':', to_char(date_trunc('second', created_at), 'YYYY-MM-DD"T"HH24:MI:SS') ) ELSE CONCAT('task:', id) END ) AS batch_key FROM base ), aggregated AS ( SELECT MIN(id) AS id, MIN(batch_key) AS batch_key, MIN(NULLIF(task_batch_id, '')) AS task_batch_id, (ARRAY_AGG(active_article_id ORDER BY created_at, id) FILTER (WHERE active_article_id IS NOT NULL))[1] AS article_id, (ARRAY_AGG(prompt_rule_id ORDER BY created_at, id) FILTER (WHERE prompt_rule_id IS NOT NULL))[1] AS prompt_rule_id, (ARRAY_AGG(prompt_rule_name ORDER BY created_at, id) FILTER (WHERE prompt_rule_name IS NOT NULL))[1] AS prompt_rule_name, (ARRAY_AGG(task_name ORDER BY created_at, id))[1] AS task_name, CASE WHEN COUNT(*) FILTER (WHERE article_generate_status IN ('generating', 'running')) > 0 THEN 'running' WHEN COUNT(*) FILTER (WHERE article_generate_status IN ('queued', 'pending')) > 0 THEN 'queued' WHEN COUNT(*) FILTER (WHERE article_generate_status = 'completed') > 0 AND COUNT(*) FILTER (WHERE article_generate_status = 'failed') > 0 THEN 'partial_success' WHEN COUNT(*) FILTER (WHERE article_generate_status = 'failed') = COUNT(*) THEN 'failed' WHEN COUNT(*) FILTER (WHERE article_generate_status = 'completed') = COUNT(*) THEN 'completed' ELSE (ARRAY_AGG(article_generate_status ORDER BY created_at DESC, id DESC))[1] END AS status, MIN(execution_time) AS execution_time, (ARRAY_AGG(input_params_json ORDER BY created_at, id))[1] AS input_params_json, MAX(generate_count) AS generate_count, MIN(created_at) AS created_at, COALESCE( JSONB_AGG( JSONB_BUILD_OBJECT( 'article_id', active_article_id, 'title', article_title, 'generate_status', article_generate_status, 'created_at', article_created_at ) ORDER BY created_at, id ) FILTER (WHERE active_article_id IS NOT NULL), '[]'::jsonb ) AS articles FROM keyed GROUP BY batch_key )` var total int64 err := s.pool.QueryRow(ctx, instantTaskBatchCTE+` SELECT COUNT(*) FROM aggregated WHERE ($3::text IS NULL OR status = $3) `, actor.TenantID, params.PromptRuleID, params.Status, params.Keyword, params.CreatedFrom, params.CreatedTo).Scan(&total) if err != nil { return nil, response.ErrInternal(50010, "count_failed", "failed to count instant tasks") } rows, err := s.pool.Query(ctx, instantTaskBatchCTE+` SELECT id, article_id, task_batch_id, prompt_rule_id, prompt_rule_name, task_name, status, execution_time, input_params_json, generate_count, articles, created_at FROM aggregated WHERE ($3::text IS NULL OR status = $3) ORDER BY created_at DESC, id DESC LIMIT $7 OFFSET $8 `, actor.TenantID, params.PromptRuleID, params.Status, params.Keyword, params.CreatedFrom, params.CreatedTo, params.PageSize, offset) if err != nil { return nil, response.ErrInternal(50010, "query_failed", "failed to list instant tasks") } defer rows.Close() items := make([]InstantTaskResponse, 0) for rows.Next() { var item InstantTaskResponse var executionAt interface{} var createdAt interface{} var inputParamsJSON []byte var articlesJSON []byte if err := rows.Scan( &item.ID, &item.ArticleID, &item.TaskBatchID, &item.PromptRuleID, &item.PromptRuleName, &item.Name, &item.Status, &executionAt, &inputParamsJSON, &item.GenerateCount, &articlesJSON, &createdAt, ); err != nil { return nil, response.ErrInternal(50010, "scan_failed", err.Error()) } item.ExecutionTime = stringPtrFromDBValue(executionAt) item.CreatedAt = fmt.Sprintf("%v", createdAt) item.Articles = parseInstantTaskArticleLinks(articlesJSON) autoPublishPlatforms, err := loadScheduleAutoPublishPlatforms(ctx, s.pool, actor.TenantID, inputParamsJSON) if err != nil { return nil, response.ErrInternal(50010, "query_failed", "failed to resolve instant task auto publish platforms") } item.AutoPublishPlatforms = autoPublishPlatforms if item.GenerateCount <= 0 { item.GenerateCount = len(item.Articles) } if item.GenerateCount <= 0 { item.GenerateCount = 1 } items = append(items, item) } if err := rows.Err(); err != nil { return nil, response.ErrInternal(50010, "scan_failed", "failed to iterate instant tasks") } return &InstantTaskListResponse{ Items: items, Total: total, }, nil } func stringPtrFromDBValue(value interface{}) *string { if value == nil { return nil } text := fmt.Sprintf("%v", value) return &text } func parseJSONMap(raw []byte) (map[string]interface{}, error) { if len(raw) == 0 { return map[string]interface{}{}, nil } payload := make(map[string]interface{}) if err := json.Unmarshal(raw, &payload); err != nil { return nil, err } return payload, nil } func parseInstantTaskArticleLinks(raw []byte) []InstantTaskArticleLink { if len(raw) == 0 { return []InstantTaskArticleLink{} } var links []InstantTaskArticleLink if err := json.Unmarshal(raw, &links); err != nil { return []InstantTaskArticleLink{} } if links == nil { return []InstantTaskArticleLink{} } return links }