package services import ( "cmbone/internal/models" "database/sql" "errors" "fmt" "os" "path/filepath" "strings" "time" ) const ( imageTaskStatusSucceeded = "succeeded" imageTaskStatusCanceled = "canceled" defaultImageOperation = "enhance" defaultTaskLimit = 50 maxTaskLimit = 200 ) var ( errImageTaskSourcePathRequired = errors.New("source path is required") errImageTaskNotFound = errors.New("image task not found") errImageTaskCannotCancel = errors.New("image task cannot be canceled") ) type ImageOptimizationService struct { store *SuiStore } func NewImageOptimizationService(store *SuiStore) *ImageOptimizationService { return &ImageOptimizationService{store: store} } func (s *ImageOptimizationService) CreateTask(input models.ImageTaskInput) (models.ImageTaskDetail, error) { sourcePath := strings.TrimSpace(input.SourcePath) if sourcePath == "" { _ = recordAppLog(s.store, appLogLevelWarn, "image_optimization", "create task failed", errImageTaskSourcePathRequired.Error()) return models.ImageTaskDetail{}, errImageTaskSourcePathRequired } operation := strings.TrimSpace(input.Operation) if operation == "" { operation = defaultImageOperation } now := time.Now().UTC().Format(time.RFC3339) beforeSize := fileSizeOrZero(sourcePath) afterSize := simulatedAfterSize(beforeSize) tx, err := s.store.DB.Begin() if err != nil { _ = recordAppLog(s.store, appLogLevelError, "image_optimization", "begin task transaction failed", err.Error()) return models.ImageTaskDetail{}, err } defer func() { if err != nil { _ = tx.Rollback() } }() result, err := tx.Exec(` INSERT INTO image_tasks (source_path, status, operation, duration_ms, error_message, created_at, updated_at) VALUES (?, ?, ?, ?, '', ?, ?) `, sourcePath, imageTaskStatusSucceeded, operation, 1200, now, now) if err != nil { _ = recordAppLog(s.store, appLogLevelError, "image_optimization", "insert image task failed", err.Error()) return models.ImageTaskDetail{}, err } taskID, err := result.LastInsertId() if err != nil { _ = recordAppLog(s.store, appLogLevelError, "image_optimization", "read image task id failed", err.Error()) return models.ImageTaskDetail{}, err } outputPath := simulatedOutputPath(sourcePath, operation) result, err = tx.Exec(` INSERT INTO image_task_results (task_id, output_path, before_size, after_size, created_at) VALUES (?, ?, ?, ?, ?) `, taskID, outputPath, beforeSize, afterSize, now) if err != nil { _ = recordAppLog(s.store, appLogLevelError, "image_optimization", "insert image task result failed", err.Error()) return models.ImageTaskDetail{}, err } resultID, err := result.LastInsertId() if err != nil { _ = recordAppLog(s.store, appLogLevelError, "image_optimization", "read image task result id failed", err.Error()) return models.ImageTaskDetail{}, err } if err = tx.Commit(); err != nil { _ = recordAppLog(s.store, appLogLevelError, "image_optimization", "commit image task failed", err.Error()) return models.ImageTaskDetail{}, err } detail := models.ImageTaskDetail{ Task: models.ImageTask{ ID: taskID, SourcePath: sourcePath, Status: imageTaskStatusSucceeded, Operation: operation, DurationMs: 1200, ErrorMessage: "", CreatedAt: now, UpdatedAt: now, }, Result: models.ImageTaskResult{ ID: resultID, TaskID: taskID, OutputPath: outputPath, BeforeSize: beforeSize, AfterSize: afterSize, CreatedAt: now, }, } _ = recordAuditLog(s.store, currentAuditActor(s.store), auditActionImageTaskCreate, "image_task", fmt.Sprint(taskID), fmt.Sprintf("source=%s operation=%s", sourcePath, operation)) return detail, nil } func (s *ImageOptimizationService) ListTasks(filter models.ImageTaskFilter) ([]models.ImageTask, error) { limit := filter.Limit if limit <= 0 { limit = defaultTaskLimit } if limit > maxTaskLimit { limit = maxTaskLimit } offset := filter.Offset if offset < 0 { offset = 0 } query := strings.Builder{} query.WriteString(` SELECT id, source_path, status, operation, duration_ms, error_message, created_at, updated_at FROM image_tasks WHERE 1 = 1 `) args := make([]any, 0, 4) if strings.TrimSpace(filter.Status) != "" { query.WriteString(" AND status = ?") args = append(args, strings.TrimSpace(filter.Status)) } if strings.TrimSpace(filter.Operation) != "" { query.WriteString(" AND operation = ?") args = append(args, strings.TrimSpace(filter.Operation)) } query.WriteString(" ORDER BY id DESC LIMIT ? OFFSET ?") args = append(args, limit, offset) rows, err := s.store.DB.Query(query.String(), args...) if err != nil { return nil, err } defer rows.Close() tasks := make([]models.ImageTask, 0) for rows.Next() { task, err := scanImageTask(rows) if err != nil { return nil, err } tasks = append(tasks, task) } if err := rows.Err(); err != nil { return nil, err } return tasks, nil } func (s *ImageOptimizationService) GetTask(id int64) (models.ImageTaskDetail, error) { row := s.store.DB.QueryRow(` SELECT t.id, t.source_path, t.status, t.operation, t.duration_ms, t.error_message, t.created_at, t.updated_at, r.id, r.task_id, r.output_path, r.before_size, r.after_size, r.created_at FROM image_tasks t LEFT JOIN image_task_results r ON r.task_id = t.id WHERE t.id = ? ORDER BY r.id DESC LIMIT 1 `, id) detail, err := scanImageTaskDetail(row) if errors.Is(err, sql.ErrNoRows) { return models.ImageTaskDetail{}, errImageTaskNotFound } return detail, err } func (s *ImageOptimizationService) CancelTask(id int64) error { result, err := s.store.DB.Exec(` UPDATE image_tasks SET status = ?, updated_at = ? WHERE id = ? AND status IN ('pending', 'processing') `, imageTaskStatusCanceled, time.Now().UTC().Format(time.RFC3339), id) if err != nil { return err } affected, err := result.RowsAffected() if err != nil { return err } if affected == 0 { return errImageTaskCannotCancel } return nil } type rowScanner interface { Scan(dest ...any) error } func scanImageTask(scanner rowScanner) (models.ImageTask, error) { var task models.ImageTask err := scanner.Scan( &task.ID, &task.SourcePath, &task.Status, &task.Operation, &task.DurationMs, &task.ErrorMessage, &task.CreatedAt, &task.UpdatedAt, ) return task, err } func scanImageTaskDetail(scanner rowScanner) (models.ImageTaskDetail, error) { var detail models.ImageTaskDetail err := scanner.Scan( &detail.Task.ID, &detail.Task.SourcePath, &detail.Task.Status, &detail.Task.Operation, &detail.Task.DurationMs, &detail.Task.ErrorMessage, &detail.Task.CreatedAt, &detail.Task.UpdatedAt, &detail.Result.ID, &detail.Result.TaskID, &detail.Result.OutputPath, &detail.Result.BeforeSize, &detail.Result.AfterSize, &detail.Result.CreatedAt, ) return detail, err } func fileSizeOrZero(path string) int64 { info, err := os.Stat(path) if err != nil || info.IsDir() { return 0 } return info.Size() } func simulatedAfterSize(beforeSize int64) int64 { if beforeSize <= 0 { return 0 } return beforeSize * 82 / 100 } func simulatedOutputPath(sourcePath string, operation string) string { dir := filepath.Dir(sourcePath) ext := filepath.Ext(sourcePath) name := strings.TrimSuffix(filepath.Base(sourcePath), ext) if name == "" || name == "." { name = "image" } if ext == "" { ext = ".jpg" } return filepath.Join(dir, fmt.Sprintf("%s_%s_optimized%s", name, operation, ext)) }