dana-medical-site/internal/storage/reports.go
2026-10-14 13:33:13 +00:00

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
}