Skip to content

Commit c6ca113

Browse files
committed
fix: add folder cover fallback and align timeout from processing start
1 parent a7f5492 commit c6ca113

6 files changed

Lines changed: 70 additions & 34 deletions

File tree

backend/internal/api/folders.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ func GetFoldersHandler(c *gin.Context) {
147147
var coverCandidates []folderCoverCandidate
148148
latestPerFolderSubQuery := query.Model(&model.Task{}).
149149
Select("folder_id, MAX(created_at) AS max_created_at").
150-
Where("folder_id <> '' AND deleted_at IS NULL").
150+
Where("folder_id <> '' AND deleted_at IS NULL AND status = ?", "completed").
151151
Group("folder_id")
152152

153153
if err := query.Table("tasks AS t").
154154
Select("t.folder_id, t.thumbnail_path, t.local_path, t.thumbnail_url, t.image_url").
155155
Joins("JOIN (?) AS latest ON t.folder_id = latest.folder_id AND t.created_at = latest.max_created_at", latestPerFolderSubQuery).
156-
Where("t.deleted_at IS NULL").
156+
Where("t.deleted_at IS NULL AND t.status = ?", "completed").
157157
Find(&coverCandidates).Error; err != nil {
158158
log.Printf("[API] 查询文件夹封面候选失败: %v\n", err)
159159
}

backend/internal/api/task_timeout_reconcile.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"image-gen-service/internal/model"
1010
)
1111

12-
const onDemandTimeoutGrace = 60 * time.Second
12+
const onDemandTimeoutGrace = 10 * time.Second
1313
const activeReconcileMinInterval = 10 * time.Second
1414
const activeReconcileScanBatchSize = 500
1515
const activeReconcileUpdateBatchSize = 200
@@ -61,7 +61,11 @@ func isTaskTimedOut(task model.Task, now time.Time, timeoutMap map[string]time.D
6161
return false
6262
}
6363
timeout := taskTimeoutForProvider(task.ProviderName, timeoutMap)
64-
return now.Sub(task.CreatedAt) > timeout+onDemandTimeoutGrace
64+
startAt := task.CreatedAt
65+
if task.ProcessingStartedAt != nil && !task.ProcessingStartedAt.IsZero() {
66+
startAt = *task.ProcessingStartedAt
67+
}
68+
return now.Sub(startAt) > timeout+onDemandTimeoutGrace
6569
}
6670

6771
func reconcileSingleTaskTimeoutOnDemand(ctx context.Context, task *model.Task) (bool, error) {
@@ -123,7 +127,7 @@ func reconcileActiveTasksTimeoutOnDemand(ctx context.Context) error {
123127
for {
124128
var activeTasks []model.Task
125129
if err := model.DB.WithContext(ctx).
126-
Select("id", "task_id", "status", "provider_name", "created_at").
130+
Select("id", "task_id", "status", "provider_name", "created_at", "processing_started_at").
127131
Where("status IN ?", []string{"pending", "processing"}).
128132
Where("id > ?", lastID).
129133
Order("id ASC").

backend/internal/model/db.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func reconcileStaleActiveTasks() {
143143
}
144144

145145
func reconcileTimedOutActiveTasks() {
146-
const timeoutGrace = 2 * time.Minute
146+
const timeoutGrace = 10 * time.Second
147147
const batchSize = 500
148148

149149
timeoutMap := make(map[string]time.Duration)
@@ -162,7 +162,7 @@ func reconcileTimedOutActiveTasks() {
162162
for {
163163
now := time.Now()
164164
var activeTasks []Task
165-
if err := DB.Select("id", "task_id", "provider_name", "status", "created_at").
165+
if err := DB.Select("id", "task_id", "provider_name", "status", "created_at", "processing_started_at").
166166
Where("status IN ?", []string{"pending", "processing"}).
167167
Where("id > ?", lastID).
168168
Order("id ASC").
@@ -181,7 +181,11 @@ func reconcileTimedOutActiveTasks() {
181181
if timeout <= 0 {
182182
timeout = defaultTimeoutForProvider(task.ProviderName)
183183
}
184-
if now.Sub(task.CreatedAt) > timeout+timeoutGrace {
184+
startAt := task.CreatedAt
185+
if task.ProcessingStartedAt != nil && !task.ProcessingStartedAt.IsZero() {
186+
startAt = *task.ProcessingStartedAt
187+
}
188+
if now.Sub(startAt) > timeout+timeoutGrace {
185189
staleTaskIDs = append(staleTaskIDs, task.TaskID)
186190
}
187191
lastID = task.ID

backend/internal/model/models.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,26 @@ type ProviderConfig struct {
2525

2626
// Task 对应 tasks 表,用于存储生成任务的状态和结果
2727
type Task struct {
28-
ID uint `gorm:"primaryKey" json:"id"`
29-
TaskID string `gorm:"uniqueIndex;not null" json:"task_id"` // 外部调用的唯一 ID
30-
Prompt string `gorm:"index:idx_prompt_search;index" json:"prompt"` // 提示词,添加复合索引支持搜索
31-
FolderID string `gorm:"index" json:"folder_id"` // 所属文件夹 ID(可选)
32-
ProviderName string `gorm:"index" json:"provider_name"` // 使用的 Provider
33-
ModelID string `gorm:"index" json:"model_id"` // 使用的模型 ID
34-
Status string `gorm:"index:idx_status_created;not null" json:"status"` // 状态,与创建时间组成复合索引
35-
ErrorMessage string `json:"error_message"` // 错误信息
36-
ImageURL string `json:"image_url"` // OSS 访问地址
37-
LocalPath string `json:"local_path"` // 本地存储路径
38-
ThumbnailURL string `json:"thumbnail_url"` // 缩略图 OSS 访问地址
39-
ThumbnailPath string `json:"thumbnail_path"` // 缩略图本地存储路径
40-
Width int `json:"width"` // 图片宽度
41-
Height int `json:"height"` // 图片高度
42-
TotalCount int `gorm:"default:1" json:"total_count"` // 申请生成的数量
43-
ConfigSnapshot string `json:"config_snapshot"` // 生成时的配置快照
44-
CreatedAt time.Time `gorm:"index:idx_status_created;index" json:"created_at"` // 创建时间
45-
CompletedAt *time.Time `json:"completed_at"`
46-
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
28+
ID uint `gorm:"primaryKey" json:"id"`
29+
TaskID string `gorm:"uniqueIndex;not null" json:"task_id"` // 外部调用的唯一 ID
30+
Prompt string `gorm:"index:idx_prompt_search;index" json:"prompt"` // 提示词,添加复合索引支持搜索
31+
FolderID string `gorm:"index" json:"folder_id"` // 所属文件夹 ID(可选)
32+
ProviderName string `gorm:"index" json:"provider_name"` // 使用的 Provider
33+
ModelID string `gorm:"index" json:"model_id"` // 使用的模型 ID
34+
Status string `gorm:"index:idx_status_created;not null" json:"status"` // 状态,与创建时间组成复合索引
35+
ErrorMessage string `json:"error_message"` // 错误信息
36+
ImageURL string `json:"image_url"` // OSS 访问地址
37+
LocalPath string `json:"local_path"` // 本地存储路径
38+
ThumbnailURL string `json:"thumbnail_url"` // 缩略图 OSS 访问地址
39+
ThumbnailPath string `json:"thumbnail_path"` // 缩略图本地存储路径
40+
Width int `json:"width"` // 图片宽度
41+
Height int `json:"height"` // 图片高度
42+
TotalCount int `gorm:"default:1" json:"total_count"` // 申请生成的数量
43+
ConfigSnapshot string `json:"config_snapshot"` // 生成时的配置快照
44+
CreatedAt time.Time `gorm:"index:idx_status_created;index" json:"created_at"` // 创建时间
45+
ProcessingStartedAt *time.Time `gorm:"index" json:"processing_started_at"` // 实际开始处理时间(不含排队时间)
46+
CompletedAt *time.Time `json:"completed_at"`
47+
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
4748
}
4849

4950
// Folder 对应 folders 表,用于存储相册文件夹信息

backend/internal/worker/pool.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"log"
99
"runtime/debug"
10+
"strings"
1011
"sync"
1112
"sync/atomic"
1213
"time"
@@ -146,7 +147,11 @@ func (wp *WorkerPool) processTask(task *Task) {
146147
}
147148

148149
// 1. 更新状态为 processing
149-
model.DB.Model(task.TaskModel).Update("status", "processing")
150+
startedAt := time.Now()
151+
model.DB.Model(task.TaskModel).Updates(map[string]interface{}{
152+
"status": "processing",
153+
"processing_started_at": &startedAt,
154+
})
150155

151156
// 2. 获取 Provider
152157
p := provider.GetProvider(task.TaskModel.ProviderName)
@@ -273,15 +278,31 @@ func (wp *WorkerPool) failTask(taskModel *model.Task, err error) {
273278
}
274279

275280
func fetchProviderTimeout(providerName string) time.Duration {
276-
if model.DB == nil || providerName == "" {
277-
return 500 * time.Second
281+
name := strings.TrimSpace(strings.ToLower(providerName))
282+
if strings.HasPrefix(name, "gemini") {
283+
name = "gemini"
284+
} else if strings.HasPrefix(name, "openai") {
285+
name = "openai"
286+
}
287+
288+
defaultTimeout := func(p string) time.Duration {
289+
switch p {
290+
case "gemini", "openai":
291+
return 500 * time.Second
292+
default:
293+
return 150 * time.Second
294+
}
295+
}
296+
297+
if model.DB == nil || name == "" {
298+
return defaultTimeout(name)
278299
}
279300
var cfg model.ProviderConfig
280-
if err := model.DB.Select("timeout_seconds").Where("provider_name = ?", providerName).First(&cfg).Error; err != nil {
281-
return 500 * time.Second
301+
if err := model.DB.Select("timeout_seconds").Where("provider_name = ?", name).First(&cfg).Error; err != nil {
302+
return defaultTimeout(name)
282303
}
283304
if cfg.TimeoutSeconds <= 0 {
284-
return 500 * time.Second
305+
return defaultTimeout(name)
285306
}
286307
return time.Duration(cfg.TimeoutSeconds) * time.Second
287308
}

desktop/src/components/HistoryPanel/FolderCard.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export const FolderCard = React.memo(function FolderCard({
2323
onClick
2424
}: FolderCardProps) {
2525
const { t } = useTranslation();
26+
const [coverLoadFailed, setCoverLoadFailed] = React.useState(false);
27+
28+
React.useEffect(() => {
29+
setCoverLoadFailed(false);
30+
}, [coverImage]);
2631

2732
return (
2833
<div
@@ -32,13 +37,14 @@ export const FolderCard = React.memo(function FolderCard({
3237
>
3338
{/* 封面图区域 - 正方形裁剪 */}
3439
<div className="relative w-full aspect-square bg-gray-100 overflow-hidden">
35-
{coverImage ? (
40+
{coverImage && !coverLoadFailed ? (
3641
<img
3742
src={coverImage}
3843
alt={folder.name}
3944
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
4045
loading="lazy"
4146
decoding="async"
47+
onError={() => setCoverLoadFailed(true)}
4248
/>
4349
) : (
4450
<div className="w-full h-full flex items-center justify-center text-gray-400 bg-gradient-to-br from-gray-50 to-gray-100">

0 commit comments

Comments
 (0)