126 lines
5.0 KiB
Go
126 lines
5.0 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type Report struct {
|
|
ID int64 `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Clinic string `json:"clinic"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
Stage int `json:"stage"`
|
|
Progress int `json:"progress"`
|
|
Stages json.RawMessage `json:"stages"`
|
|
SourceName string `json:"source_name"`
|
|
SourceSize int64 `json:"source_size"`
|
|
Summary json.RawMessage `json:"summary"`
|
|
ResultName string `json:"result_name"`
|
|
ResultSize int `json:"result_size"`
|
|
Error string `json:"error"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
FinishedAt *time.Time `json:"finished_at"`
|
|
}
|
|
|
|
const reportCols = `id, kind, clinic, title, status, stage, progress, stages, source_name, source_size,
|
|
summary, result_name, COALESCE(length(result), 0), error, created_at, finished_at`
|
|
|
|
func scanReport(row pgx.Row) (*Report, error) {
|
|
var r Report
|
|
err := row.Scan(&r.ID, &r.Kind, &r.Clinic, &r.Title, &r.Status, &r.Stage, &r.Progress, &r.Stages, &r.SourceName,
|
|
&r.SourceSize, &r.Summary, &r.ResultName, &r.ResultSize, &r.Error, &r.CreatedAt, &r.FinishedAt)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return &r, err
|
|
}
|
|
|
|
func (s *Storage) CreateReport(ctx context.Context, clinic, kind, title, sourceName string, sourceSize int64, adminID int64) (int64, error) {
|
|
var id int64
|
|
err := s.pool.QueryRow(ctx, `
|
|
INSERT INTO reports (clinic, kind, title, source_name, source_size, admin_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
|
clinic, kind, title, sourceName, sourceSize, adminID).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
// ReportProgress записывает текущий этап и длительности завершённых этапов.
|
|
func (s *Storage) ReportProgress(ctx context.Context, id int64, stage, progress int, stagesMS []int64) error {
|
|
st, _ := json.Marshal(stagesMS)
|
|
_, err := s.pool.Exec(ctx, `UPDATE reports SET stage=$2, progress=$3, stages=$4 WHERE id=$1 AND status='running'`,
|
|
id, stage, progress, st)
|
|
return err
|
|
}
|
|
|
|
// FinishReport закрывает задание; статус меняется, только пока оно выполняется —
|
|
// отменённое не станет «готовым», даже если расчёт успел закончиться.
|
|
func (s *Storage) FinishReport(ctx context.Context, id int64, status string, stagesMS []int64, summary any, result []byte, resultName, errText string) error {
|
|
st, _ := json.Marshal(stagesMS)
|
|
sm, _ := json.Marshal(summary)
|
|
_, err := s.pool.Exec(ctx, `
|
|
UPDATE reports SET status=$2, stages=$3, summary=$4, result=$5, result_name=$6, error=$7,
|
|
progress=CASE WHEN $2 = 'done' THEN 100 ELSE progress END, finished_at=now()
|
|
WHERE id=$1 AND status='running'`,
|
|
id, status, st, sm, result, resultName, errText)
|
|
return err
|
|
}
|
|
|
|
func (s *Storage) CancelReport(ctx context.Context, id int64) (bool, error) {
|
|
tag, err := s.pool.Exec(ctx, `UPDATE reports SET status='cancelled', finished_at=now() WHERE id=$1 AND status='running'`, id)
|
|
return tag.RowsAffected() > 0, err
|
|
}
|
|
|
|
func (s *Storage) Report(ctx context.Context, id int64) (*Report, error) {
|
|
return scanReport(s.pool.QueryRow(ctx, `SELECT `+reportCols+` FROM reports WHERE id=$1`, id))
|
|
}
|
|
|
|
func (s *Storage) Reports(ctx context.Context, clinic string, limit int) ([]Report, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT `+reportCols+` FROM reports WHERE clinic=$1 ORDER BY created_at DESC, id DESC LIMIT $2`, clinic, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := []Report{}
|
|
for rows.Next() {
|
|
r, err := scanReport(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Storage) ReportResult(ctx context.Context, id int64) (name string, data []byte, err error) {
|
|
err = s.pool.QueryRow(ctx, `SELECT result_name, result FROM reports WHERE id=$1 AND status='done' AND result IS NOT NULL`, id).Scan(&name, &data)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", nil, ErrNotFound
|
|
}
|
|
return name, data, err
|
|
}
|
|
|
|
func (s *Storage) DeleteReport(ctx context.Context, id int64) error {
|
|
tag, err := s.pool.Exec(ctx, `DELETE FROM reports WHERE id=$1 AND status <> 'running'`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FailInterruptedReports — после перезапуска сервера задания, которые шли, уже не закончатся.
|
|
// Готовые отчёты хранятся без срока: удаляются только вручную из админки.
|
|
func (s *Storage) FailInterruptedReports(ctx context.Context) error {
|
|
_, err := s.pool.Exec(ctx, `
|
|
UPDATE reports SET status='failed', error='Сервер перезапускался во время расчёта — запустите отчёт ещё раз', finished_at=now()
|
|
WHERE status='running'`)
|
|
return err
|
|
}
|