Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/internal/api/folders.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,13 @@ func GetFoldersHandler(c *gin.Context) {
var coverCandidates []folderCoverCandidate
latestPerFolderSubQuery := query.Model(&model.Task{}).
Select("folder_id, MAX(created_at) AS max_created_at").
Where("folder_id <> '' AND deleted_at IS NULL").
Where("folder_id <> '' AND deleted_at IS NULL AND status = ?", "completed").
Group("folder_id")

if err := query.Table("tasks AS t").
Select("t.folder_id, t.thumbnail_path, t.local_path, t.thumbnail_url, t.image_url").
Joins("JOIN (?) AS latest ON t.folder_id = latest.folder_id AND t.created_at = latest.max_created_at", latestPerFolderSubQuery).
Where("t.deleted_at IS NULL").
Where("t.deleted_at IS NULL AND t.status = ?", "completed").
Find(&coverCandidates).Error; err != nil {
log.Printf("[API] 查询文件夹封面候选失败: %v\n", err)
}
Expand Down
10 changes: 7 additions & 3 deletions backend/internal/api/task_timeout_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"image-gen-service/internal/model"
)

const onDemandTimeoutGrace = 60 * time.Second
const onDemandTimeoutGrace = 10 * time.Second
const activeReconcileMinInterval = 10 * time.Second
const activeReconcileScanBatchSize = 500
const activeReconcileUpdateBatchSize = 200
Expand Down Expand Up @@ -61,7 +61,11 @@ func isTaskTimedOut(task model.Task, now time.Time, timeoutMap map[string]time.D
return false
}
timeout := taskTimeoutForProvider(task.ProviderName, timeoutMap)
return now.Sub(task.CreatedAt) > timeout+onDemandTimeoutGrace
startAt := task.CreatedAt
if task.ProcessingStartedAt != nil && !task.ProcessingStartedAt.IsZero() {
startAt = *task.ProcessingStartedAt
}
return now.Sub(startAt) > timeout+onDemandTimeoutGrace
}

func reconcileSingleTaskTimeoutOnDemand(ctx context.Context, task *model.Task) (bool, error) {
Expand Down Expand Up @@ -123,7 +127,7 @@ func reconcileActiveTasksTimeoutOnDemand(ctx context.Context) error {
for {
var activeTasks []model.Task
if err := model.DB.WithContext(ctx).
Select("id", "task_id", "status", "provider_name", "created_at").
Select("id", "task_id", "status", "provider_name", "created_at", "processing_started_at").
Where("status IN ?", []string{"pending", "processing"}).
Where("id > ?", lastID).
Order("id ASC").
Expand Down
10 changes: 7 additions & 3 deletions backend/internal/model/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func reconcileStaleActiveTasks() {
}

func reconcileTimedOutActiveTasks() {
const timeoutGrace = 2 * time.Minute
const timeoutGrace = 10 * time.Second
const batchSize = 500

timeoutMap := make(map[string]time.Duration)
Expand All @@ -162,7 +162,7 @@ func reconcileTimedOutActiveTasks() {
for {
now := time.Now()
var activeTasks []Task
if err := DB.Select("id", "task_id", "provider_name", "status", "created_at").
if err := DB.Select("id", "task_id", "provider_name", "status", "created_at", "processing_started_at").
Where("status IN ?", []string{"pending", "processing"}).
Where("id > ?", lastID).
Order("id ASC").
Expand All @@ -181,7 +181,11 @@ func reconcileTimedOutActiveTasks() {
if timeout <= 0 {
timeout = defaultTimeoutForProvider(task.ProviderName)
}
if now.Sub(task.CreatedAt) > timeout+timeoutGrace {
startAt := task.CreatedAt
if task.ProcessingStartedAt != nil && !task.ProcessingStartedAt.IsZero() {
startAt = *task.ProcessingStartedAt
}
Comment on lines +184 to +187
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这部分用于获取任务开始时间的逻辑与 backend/internal/api/task_timeout_reconcile.go 文件中的 isTaskTimedOut 函数内的逻辑重复了。根据代码审查规范,建议将此逻辑抽象成 model.Task 上的一个方法(例如 GetStartTime()),以消除重复代码并提高可维护性。

References
  1. 代码库中存在重复代码,应通过抽象为函数或组件来消除。 (Rule 214) (link)

if now.Sub(startAt) > timeout+timeoutGrace {
staleTaskIDs = append(staleTaskIDs, task.TaskID)
}
lastID = task.ID
Expand Down
39 changes: 20 additions & 19 deletions backend/internal/model/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,26 @@ type ProviderConfig struct {

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

// Folder 对应 folders 表,用于存储相册文件夹信息
Expand Down
33 changes: 27 additions & 6 deletions backend/internal/worker/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"log"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -146,7 +147,11 @@ func (wp *WorkerPool) processTask(task *Task) {
}

// 1. 更新状态为 processing
model.DB.Model(task.TaskModel).Update("status", "processing")
startedAt := time.Now()
model.DB.Model(task.TaskModel).Updates(map[string]interface{}{
"status": "processing",
"processing_started_at": &startedAt,
})

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

func fetchProviderTimeout(providerName string) time.Duration {
if model.DB == nil || providerName == "" {
return 500 * time.Second
name := strings.TrimSpace(strings.ToLower(providerName))
if strings.HasPrefix(name, "gemini") {
name = "gemini"
} else if strings.HasPrefix(name, "openai") {
name = "openai"
}

defaultTimeout := func(p string) time.Duration {
switch p {
case "gemini", "openai":
return 500 * time.Second
default:
return 150 * time.Second
}
}

if model.DB == nil || name == "" {
return defaultTimeout(name)
}
var cfg model.ProviderConfig
if err := model.DB.Select("timeout_seconds").Where("provider_name = ?", providerName).First(&cfg).Error; err != nil {
return 500 * time.Second
if err := model.DB.Select("timeout_seconds").Where("provider_name = ?", name).First(&cfg).Error; err != nil {
return defaultTimeout(name)
}
if cfg.TimeoutSeconds <= 0 {
return 500 * time.Second
return defaultTimeout(name)
}
return time.Duration(cfg.TimeoutSeconds) * time.Second
}
Comment on lines 280 to 308
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这个 fetchProviderTimeout 函数中的提供商名称规范化和默认超时逻辑与项目中的其他部分存在重复。例如,defaultTimeout 内部函数与 backend/internal/model/db.go 中的 defaultTimeoutForProvider 函数完全相同。根据代码审查规范,建议将这些逻辑统一,例如将相关函数导出为公共函数,并在所有需要的地方复用。

References
  1. 代码库中存在重复代码,应通过抽象为函数或组件来消除。 (Rule 214) (link)

8 changes: 7 additions & 1 deletion desktop/src/components/HistoryPanel/FolderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export const FolderCard = React.memo(function FolderCard({
onClick
}: FolderCardProps) {
const { t } = useTranslation();
const [coverLoadFailed, setCoverLoadFailed] = React.useState(false);

React.useEffect(() => {
setCoverLoadFailed(false);
}, [coverImage]);

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