Files
eatvio_app/tz.md
2026-05-10 20:08:51 +03:00

11 KiB
Raw Blame History

Offline-First для модуля «Diary» — план реализации Контекст и описание задачи

Модуль Diary (дневник питания) приложения Eatvio полностью завязан на сетевые запросы через Retrofit/Dio. При отсутствии сети приложение падает в DiaryState.error и данные теряются. Необходимо реализовать стратегию Offline-First с тремя ключевыми компонентами:

Локальное хранение данных дневника и глоссария (Drift/SQLite)
Очередь мутаций (pending queue) — операции копятся офлайн и синхронизируются при появлении сети
Автосинхронизация при восстановлении сети через InternetConnectionCubit (уже существует)

Дедлайн утверждения плана: 14:00, 13 апреля 2026 г. ✅ Дедлайн готовности: 14:00, 12 мая 2026 г. Архитектурное решение

DiaryCubit │ └── DiaryRepository (MODIFY: добавить offline-логику) │ ├── DiaryService (Retrofit, доработка под Idempotency-Key) │ ├── DiaryLocalDataSource (NEW: фасад над Drift DAO) │ ├── DiaryDayDao → таблица diary_days │ ├── MealDao → таблица meals │ ├── PortionDao → таблица portions │ ├── GlossaryDao → таблица glossary_cache (201k записей) │ └── PendingOpsDao → таблица pending_operations │ └── DiarySyncService (NEW: FIFO-синхронизация) └── подписка на InternetConnectionCubit

Паттерн чтения: Cache-Aside → попытка загрузить из сети (учитывая ETag) → при успехе кэшируем → при ошибке сети возвращаем кэш

Паттерн записи: Write-Behind → немедленное локальное обновление + запись в очередь с уникальным Idempotency-Key → при наличии сети отправляем на сервер API Утвержденный Контракт и Требования к бэкенду

Caution

Статус Backend: На данный момент изучен код бекенда в папке eatvio. Все описанные ниже endpoints (/dump, /meta), использование Idempotency-Key, server_updated_at и единый формат ошибок в текущем коде Laravel отсутствуют (используется стандартная валидация FormRequest и ModelNotFound).

Таким образом, описанный ниже контракт является ЦЕЛЕВЫМ (To-Be) и Бэкенд-команда должна его имплементировать для успешной работы оффлайн-стратегии.
  1. Глоссарий для офлайн-кэша 1A. Meta-проверка версии данных

GET /api/glossaries/meta

Возвращает легкий объект сверки:

{ "total_count": 201691, "last_updated_at": "2026-04-08T10:30:00Z", "checksum": "sha256:..." }

Сценарий клиента: Сравниваем локальный checksum. Если отличается — вызываем /dump. 1B. Дамп продуктов

GET /api/glossaries/dump?updated_after=<iso_datetime> If-None-Match: "etag_из_предыдущего_ответа"

Сценарий клиента (Флаттер):

При ответе 304 Not Modified — локальный кэш актуален.
При ответе 200 OK — в фоне (в изоляте/через compute) вливаем данные в SQLite таблицу. Заголовки ETag и Last-Modified запоминаем.
  1. Дневник: идемпотентные операции

Мутирующие команды (create day, create portion и т.д.) теперь требуют заголовок:

Idempotency-Key:

Это позволит безопасно ретраить запросы (например, оборвавшиеся из-за скачка сети), не плодя дубли на сервере.

Сам API V2 для создания дня:

POST /api/v2/diary/create/day

Формат даты: Y-m-d\TH:i:sP (2026-04-08T07:30:00+03:00). 3. Инвалидация локального дневника

Во всех ответах DiaryResource (результат /index или /show) сервер обязан возвращать дату генерации/изменения:

{ ... "server_updated_at": "2026-04-08T10:30:00Z" }

Мобильный клиент будет сравнивать это число со своей записью в Drift diary_days.server_updated_at, чтобы решать, нужен ли merge/refresh. 4. Фото

Загрузка фото:

POST /api/diary/{diary}/meals/{type_no}/photo/upload

Ответ 200 с photo.id:

{ "photo": { "id": 456, "path": "...photo.jpg" } }

При загрузке из оффлайн-очереди, клиент сохранит этот ID к себе в локальную таблицу фотографий, чтобы иметь возможность позже выполнить удаление:

Удаление фото:

DELETE /api/diary/{diary}/meals/{type_no}/photo/{media}/delete

  1. Флоу очереди синхронизации (Формат ошибок)

Ответы с ошибками возвращаются в Envelope-формате:

{ "message": "Human readable message", "error": { "code": "validation_error", "message": "Human readable message", "retryable": false } }

Обработка на Флаттере (флоу Sync-провайдера):

Исключение DIO с кодом 4xx/5xx:
    Если в ответе есть error.retryable == false (ошибки валидации, порция/дневник не найдены, конфликт payload):
    → Пометить PendingOperation как FAILED. Отправить стейт в UI с нотификацией или подсветить порцию красным ("Не удалось сохранить").
    Если error.retryable == true или 500 Server Error или ConnectionTimeout
    → Оставить операцию в очереди (PENDING). Повторить попытку позже с backoff.

Proposed Changes (Изменения в Flutter приложении) Этап 1 — Инфраструктура Drift БД

Срок: 7–14 апреля 2026 (7 дней)

Подключить пакеты: drift, drift_flutter, sqlite3_flutter_libs, uuid.
Создать AppDatabase и схемы:
    diary_days, meals, portions, media
    glossary_cache
    pending_operations (Колонки: id (UUID), type, payload, idempotency_key, retry_count, status, local_file_path)
    sync_meta (для checksum и etag)

Этап 2 — DAO и LocalDataSource

Срок: 14–19 апреля 2026 (5 дней)

Написать методы DAO: сохранения/обновления дневника.
Написать методы Bulk Insert для glossary_cache.
Написать методы работы с очередью (markFailed, markSynced).

Этап 3 — Настройка Retrofit/Dio для Idempotency

Срок: 19–26 апреля 2026 (7 дней)

Обновить DiaryService.dart:
    Добавить параметр @Header("Idempotency-Key") String idempotencyKey во все методы POST/PUT.
    Обновить метод получения индекса дня под V2.
В DiaryRepository внедрить механизм: при мутировании данных, генерировать UUID → сохранить в pending_operations → отправить на сервер (передав туда UUID) → при успехе очистить → при ошибке ориентироваться на поле retryable из ответа.

Этап 4 — SyncService для Глоссария и Очереди

Срок: 26–30 апреля 2026 (4 дня)

Обернуть первичное скачивание /dump в compute(), разбирая JSON как stream либо большими чанками, чтобы 200к записей не заморозили фреймы.
Написать воркер синхронизации дневника (прослушивание InternetConnectionCubit), который проходит по таблице pending_operations и переотправляет POST-запросы.
Обработать удаление/загрузку фотографий.

Этап 5 — UI и оповещения (Обработка failed-статусов)

Срок: 30 апреля – 5 мая 2026 (5 дней)

На экранах: показать лоадер или жёлтую плашку "Оффлайн - N изменений в очереди".
Имплементировать механизм: если операция в статусе FAILED (error = validation_error), показывать toast "Ошибка валидации порции" или маркировать саму UI-строку порции красным.
Ограничение некоторых функций, если InternetConnectionCubit без связи (например, отправка на оценку консультанту).

Этап 6 & 7 — DI, Интеграция и Ретест

Срок: 5–12 мая 2026 (7 дней)

Инжектнуть зависимости через GetIt/Injectable.
Покрыть интеграционными тестами (diary_repository_offline_test.dart) поведение с retryable: true|false.
Подтвердить работоспособность на девайсе (заморозка сети руками, добавление, разморозка сети).

Итог для BE и FE

Клиент и Сервер разделяют логику retry/fail, используя поле retryable и Idempotency-Key. Это идеальная база для стабильного Offline-First.

Для старта работ FE ожидает завершения работ на BE (или хотя бы выкатывания моков/тестовой среды API по этому гайду).