internal/storage: SQLite-БД для задач и трассировок (таски + статус-машина, трассы с группировкой по task_id)
All checks were successful
build-test / build (push) Successful in 30s

This commit is contained in:
Hermes
2026-08-14 23:38:57 +05:00
parent cc180d7517
commit 089e196dbc
7 changed files with 821 additions and 0 deletions

138
internal/storage/tasks.go Normal file
View File

@@ -0,0 +1,138 @@
package storage
import (
"context"
"database/sql"
"fmt"
)
// CreateTask создаёт задачу со статусом draft. Возвращает её ID.
// chat_id и task_tag передаются извне (в диалоге — chat.Address + uuid).
func (s *Storage) CreateTask(ctx context.Context, t *Task) (int64, error) {
now := Now()
res, err := s.db.ExecContext(ctx, `
INSERT INTO tasks (chat_id, title, goal, repo, why, ac, task_tag, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
t.ChatID, t.Title, t.Goal, t.Repo, t.Why, t.AC,
t.TaskTag, StatusDraft, now, now,
)
if err != nil {
return 0, fmt.Errorf("%w: create task: %w", ErrDB, err)
}
id, err := res.LastInsertId()
if err != nil {
return 0, fmt.Errorf("%w: last insert id: %w", ErrDB, err)
}
t.ID = id
t.Status = StatusDraft
t.CreatedAt = now
t.UpdatedAt = now
return id, nil
}
// GetTask возвращает задачу по ID.
func (s *Storage) GetTask(ctx context.Context, id int64) (*Task, error) {
t := &Task{}
err := s.db.QueryRowContext(ctx, `
SELECT id, chat_id, title, goal, repo, why, ac, task_tag, status, created_at, updated_at
FROM tasks WHERE id = ?`, id).Scan(
&t.ID, &t.ChatID, &t.Title, &t.Goal, &t.Repo,
&t.Why, &t.AC, &t.TaskTag, &t.Status, &t.CreatedAt, &t.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("%w: task %d", ErrNotFound, id)
}
if err != nil {
return nil, fmt.Errorf("%w: get task %d: %w", ErrDB, id, err)
}
return t, nil
}
// UpdateTask обновляет поля задачи. Если newStatus указан и отличен от текущего —
// проверяет валидность перехода. Возвращает ошибку ErrInvalidStatus при недопустимом переходе.
func (s *Storage) UpdateTask(ctx context.Context, t *Task) error {
// получаем текущий статус для валидации перехода
var oldStatus Status
err := s.db.QueryRowContext(ctx, `SELECT status FROM tasks WHERE id = ?`, t.ID).Scan(&oldStatus)
if err == sql.ErrNoRows {
return fmt.Errorf("%w: task %d", ErrNotFound, t.ID)
}
if err != nil {
return fmt.Errorf("%w: get status %d: %w", ErrDB, t.ID, err)
}
if t.Status != "" && t.Status != oldStatus {
if !IsValidTransition(oldStatus, t.Status) {
return fmt.Errorf("%w: %s → %s for task %d", ErrInvalidStatus, oldStatus, t.Status, t.ID)
}
} else if t.Status == "" {
t.Status = oldStatus
}
now := Now()
res, err := s.db.ExecContext(ctx, `
UPDATE tasks
SET title=?, goal=?, repo=?, why=?, ac=?, status=?, updated_at=?
WHERE id=?`,
t.Title, t.Goal, t.Repo, t.Why, t.AC, t.Status, now, t.ID,
)
if err != nil {
return fmt.Errorf("%w: update task %d: %w", ErrDB, t.ID, err)
}
n, _ := res.RowsAffected()
if n == 0 {
return fmt.Errorf("%w: task %d", ErrNotFound, t.ID)
}
t.UpdatedAt = now
return nil
}
// ListTasks возвращает список задач с фильтрацией.
func (s *Storage) ListTasks(ctx context.Context, filter TaskFilter) ([]*Task, error) {
if filter.Limit <= 0 {
filter.Limit = 50
}
where := "1=1"
args := []any{}
if filter.ChatID != "" {
where += " AND chat_id = ?"
args = append(args, filter.ChatID)
}
if filter.Status != "" {
where += " AND status = ?"
args = append(args, string(filter.Status))
}
args = append(args, filter.Limit, filter.Offset)
rows, err := s.db.QueryContext(ctx, `
SELECT id, chat_id, title, goal, repo, why, ac, task_tag, status, created_at, updated_at
FROM tasks WHERE `+where+` ORDER BY updated_at DESC LIMIT ? OFFSET ?`, args...)
if err != nil {
return nil, fmt.Errorf("%w: list tasks: %w", ErrDB, err)
}
defer rows.Close()
var tasks []*Task
for rows.Next() {
t := &Task{}
if err := rows.Scan(&t.ID, &t.ChatID, &t.Title, &t.Goal, &t.Repo,
&t.Why, &t.AC, &t.TaskTag, &t.Status, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, fmt.Errorf("%w: scan task: %w", ErrDB, err)
}
tasks = append(tasks, t)
}
return tasks, rows.Err()
}
// DeleteTask удаляет задачу (каскадно — трассы). Только для тестов/админки.
func (s *Storage) DeleteTask(ctx context.Context, id int64) error {
res, err := s.db.ExecContext(ctx, `DELETE FROM tasks WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("%w: delete task %d: %w", ErrDB, id, err)
}
n, _ := res.RowsAffected()
if n == 0 {
return fmt.Errorf("%w: task %d", ErrNotFound, id)
}
return nil
}