хранилище, файлы записей и очередь переведены на встроенную PocketBase

- записи, метаданные и файлы съехались под один каталог данных; появилась
  панель владельца, а gin, goqu, goose и требование CGO ушли
- захват задачи стал одним запросом с RETURNING; заведены число попыток,
  состояние dead и нарастающая пауза вместо признака is_error
- имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с
  идентификатором записи оно собирало бы ссылку на скачивание
This commit is contained in:
av
2026-08-12 08:31:59 +03:00
parent 09cedc4e61
commit 01cc31d45f
55 changed files with 5238 additions and 1235 deletions
+22 -14
View File
@@ -4,25 +4,33 @@ import (
"time"
)
// Где лежит копия файла. Поле названо `location`, а не `storage`: последним
// словом зовут само хранилище, и третий смысл у одного слова развёл бы по
// разным вещам запись о файле и хранилище, в котором она лежит.
const (
StorageLocal = "local"
StorageS3 = "s3"
LocationLocal = "local"
LocationS3 = "s3"
)
// MaxRecordSize — потолок размера одного файла записи. Выведен из расчётного
// потолка записи в шесть часов с запасом на видео, а не из замера.
//
// Число нужно назвать **явно** в двух местах сразу: у поля файла в хранилище
// нулевой потолок значит не «без предела», а умолчание библиотеки в 5 МиБ, а у
// тела запроса приёма умолчание роутера отсекало бы запись раньше, чем она
// дойдёт до обработчика — без строки в журнале приёма.
const MaxRecordSize int64 = 8 << 30 // 8 ГиБ
// File — одна физическая копия: исходник, результат конвертации и копия во
// внешнем хранилище — три разные записи.
type File struct {
Id string
Storage string
Id string
Location string
// FileName — имя, под которым файл лежит: у местной копии это имя, заданное
// сервисом, у внешней — ключ объекта. Своего суффикса хранилище к заданному
// имени не дописывает: суффикс появляется только у имён, которые оно строит
// само из имени отправителя, а это умолчание не применяется.
FileName string
Size int64
CreatedAt time.Time
}
func (f *File) CopyWithStorage(newId, storage string) *File {
return &File{
Id: newId,
Storage: storage,
FileName: f.FileName,
Size: f.Size,
CreatedAt: time.Now(),
}
}
+30 -2
View File
@@ -9,11 +9,11 @@ type TranscribeJob struct {
State string
Source string
FileID *string
IsError bool
ErrorText *string
AcquisitionID *string
AcquireTime *time.Time
DelayTime *time.Time
Attempts int // Число попыток: растёт при захвате, обнуляется на шаге без отказа
RecognitionOpID *string // ID операции распознавания в Yandex Cloud
TranscriptionText *string // Результат распознавания
TgChatId *int64 // Telegram: в какой чат отправить результат распознавания
@@ -28,6 +28,11 @@ const (
StateTranscribe = "transcribe"
StateDone = "done"
StateFailed = "failed"
// StateDead — задача, которую мы повторяли и перестали. От `failed` она
// отличается тем, чей это приговор: в `failed` задачу переводит шаг,
// рассудивший об этой записи окончательно, а сюда она уходит без такого
// суждения. Ни один шаг конвейера в неё не переводит сам.
StateDead = "dead"
)
const (
@@ -43,6 +48,10 @@ func (j *TranscribeJob) MoveToState(state string) {
j.DelayTime = nil
j.AcquisitionID = nil
j.AcquireTime = nil
// Шаг, дошедший до перехода, завершился без отказа, а попытки считают
// именно отказавшие: иначе задача, прошедшая конвейер целиком, накопила бы
// их поштучно и умерла бы здоровой.
j.Attempts = 0
j.UpdatedAt = time.Now()
}
@@ -59,6 +68,25 @@ func (j *TranscribeJob) Done(transcriptionText string) {
func (j *TranscribeJob) Fail(errText string) {
j.MoveToState(StateFailed)
j.IsError = true
j.ErrorText = &errText
}
// RetryAfter освобождает отказавшую задачу для повтора: захват снимается,
// пауза ставится, а число попыток сохраняется — по нему растёт пауза и
// наступает предел.
func (j *TranscribeJob) RetryAfter(delay time.Time) {
j.AcquisitionID = nil
j.AcquireTime = nil
j.DelayTime = &delay
j.UpdatedAt = time.Now()
}
// Die переводит задачу, исчерпавшую попытки, в состояние «мертва». Число
// попыток при этом сохраняется: по нему видно, сколько раз мы пробовали, а
// возвращает задачу в работу владелец правкой состояния.
func (j *TranscribeJob) Die(errText string) {
attempts := j.Attempts
j.MoveToState(StateDead)
j.Attempts = attempts
j.ErrorText = &errText
}