Files
cmbone/internal/services/image_optimization_service.go
T

280 lines
7.4 KiB
Go
Raw Normal View History

2026-07-09 00:47:35 +08:00
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 == "" {
2026-07-09 00:55:46 +08:00
_ = recordAppLog(s.store, appLogLevelWarn, "image_optimization", "create task failed", errImageTaskSourcePathRequired.Error())
2026-07-09 00:47:35 +08:00
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 {
2026-07-09 00:55:46 +08:00
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "begin task transaction failed", err.Error())
2026-07-09 00:47:35 +08:00
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 {
2026-07-09 00:55:46 +08:00
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "insert image task failed", err.Error())
2026-07-09 00:47:35 +08:00
return models.ImageTaskDetail{}, err
}
taskID, err := result.LastInsertId()
if err != nil {
2026-07-09 00:55:46 +08:00
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "read image task id failed", err.Error())
2026-07-09 00:47:35 +08:00
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 {
2026-07-09 00:55:46 +08:00
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "insert image task result failed", err.Error())
2026-07-09 00:47:35 +08:00
return models.ImageTaskDetail{}, err
}
resultID, err := result.LastInsertId()
if err != nil {
2026-07-09 00:55:46 +08:00
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "read image task result id failed", err.Error())
2026-07-09 00:47:35 +08:00
return models.ImageTaskDetail{}, err
}
if err = tx.Commit(); err != nil {
2026-07-09 00:55:46 +08:00
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "commit image task failed", err.Error())
2026-07-09 00:47:35 +08:00
return models.ImageTaskDetail{}, err
}
2026-07-09 00:55:46 +08:00
detail := models.ImageTaskDetail{
2026-07-09 00:47:35 +08:00
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,
},
2026-07-09 00:55:46 +08:00
}
_ = recordAuditLog(s.store, currentAuditActor(s.store), auditActionImageTaskCreate, "image_task", fmt.Sprint(taskID), fmt.Sprintf("source=%s operation=%s", sourcePath, operation))
return detail, nil
2026-07-09 00:47:35 +08:00
}
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))
}