103 lines
7.2 KiB
Markdown
103 lines
7.2 KiB
Markdown
# Требования к модулю: m06-fit-import
|
||
|
||
> Модуль — проект `Server.WatchBody.Import` (класс-библиотека) и режим
|
||
> `--mode import` единой точки входа CLI: разовый импорт .fit/.tcx/.gpx
|
||
> и пакета программы `program-import.json` из `Import.Dir`, разбор
|
||
> .fit в канонический факт `workout-facts/1` → `src.workout.facts_json`,
|
||
> сопоставление с тренировкой телефона (± 15 минут), архивирование.
|
||
> Источники: Ф-8.2–8.4, requirements 2.3, tech-req 2.3/5 (FitParser),
|
||
> `3-tech\program-import-format.md`, закрытые вопросы 10 (ОВ-1-А) и 12
|
||
> (О-4-Б). Самодостаточная копия требований (d-dev-6).
|
||
|
||
## 1. Назначение и границы
|
||
|
||
**В границах модуля:**
|
||
|
||
- проект `Server.WatchBody.Import` (ссылается на Core, NuGet
|
||
`Garmin.FIT.Sdk` — tech-req разд. 5, «официальный FIT SDK for C#»):
|
||
- `FitParser`: потоковый разбор .fit (события `Decode`):
|
||
`Session` (тип `strength_training`, старт, длительность), `Lap`,
|
||
`Record` (пульс), `Set` (повторения/вес) → канонический JSON
|
||
`workout-facts/1` (tech-req 2.3) + `avg_hr`, `hr_recovery_s`
|
||
(возврат к < 90 после последнего Lap, Ф-13.4), `fit_file_id`,
|
||
время старта;
|
||
- `TcxGpxParser`: .tcx/.gpx (XML) — упрощённый факт (тип, старт,
|
||
длительность, `records[]` пульса; подходов в этих форматах нет);
|
||
- `WorkoutImporter`: дедупликация по `fit_file_id`
|
||
(`UNIQUE(athlete_id, fit_file_id)` — дубль → пропуск без ошибки);
|
||
сопоставление тренировке телефона по `started_at` (окно ± 15 минут,
|
||
Ф-8.4) — факты пишутся в ту же строку; не сопоставилась — новая
|
||
строка «только .fit» (uuid генерирует сервер, Р2-Б); 🛡
|
||
сопоставление не удаляет и не переписывает телефонные данные;
|
||
- `ProgramPackageImporter`: пакет `program-import.json` →
|
||
`src.program` по `external_id` / `program_version` (повтор пакета —
|
||
новая версия max+1, формат — program-import-format.md; отсутствующие
|
||
упражнения библиотеки создаются, группа — по разделу фокуса, иначе
|
||
core + лог);
|
||
- `ImportService`: обход `Import.Dir\<имя атлета>\` (атрибуция файлов
|
||
атлету по подпапке — решение гейта 1.3), маршрутизация по
|
||
расширению, обработанные → `Import.ArchiveDir`, сбойные остаются
|
||
с записью в лог;
|
||
- режим `--mode import` в `Program.cs` `Server.WatchBody.Web` (единая
|
||
точка входа CLI — requirements 2.3): коды возврата 0 (успех/нет
|
||
новых) / 1 (авария конфигурации) / 2 (частичный сбой);
|
||
- конфиг `Import.Dir` / `Import.ArchiveDir` (требования разд. 3);
|
||
- тесты: интеграционные против docker `watchbody-pg`; .fit-фикстуры
|
||
генерируются энкодером SDK (`Encode`) в тестовом коде.
|
||
|
||
**Вне границ модуля:** забор из Garmin Connect (m07 — `FitFetcher`,
|
||
дедупликация та же), admin REST (m08), метрики по фактам (m04
|
||
пересчитает при следующем sync — wiring не входит), push-уведомления,
|
||
повторный разбор существующих `facts_json`.
|
||
|
||
## 2. Канонический факт `workout-facts/1` (tech-req 2.3)
|
||
|
||
```json
|
||
{"schema": "workout-facts/1",
|
||
"session": {"type": "strength_training", "start": "...ISO...",
|
||
"duration_s": 3600},
|
||
"laps": [{"index": 0, "start": "...", "duration_s": 120}],
|
||
"records": [{"t_offset_s": 0, "hr": 95}],
|
||
"sets": [{"index": 0, "reps": 12, "weight_kg": 100.0}],
|
||
"fit_file_id": "...", "device": "..."}
|
||
```
|
||
|
||
В `src.workout` дополнительно пишутся: `started_at` (из session),
|
||
`avg_hr` (среднее records), `hr_recovery_s` (секунды от конца
|
||
последнего Lap до первого Record с hr < 90; не достигнуто — null).
|
||
|
||
## 3. Инварианты (🛡 — нарушать запрещено)
|
||
|
||
1. **Изоляция атлета** — все записи по `athleteId`, выведенному из
|
||
подпапки; чужие данные не читаются и не пишутся.
|
||
2. **Сопоставление не разрушает** — .fit дописывает `facts_json`/
|
||
`avg_hr`/`hr_recovery_s`/`fit_file_id` в строку; поля телефона
|
||
(статус, session_kind, подходы) не трогаются.
|
||
3. **Идемпотентность** — повторный импорт того же .fit (по
|
||
`fit_file_id`) — пропуск; повторный импорт пакета (по
|
||
`external_id`) — новая версия, существующие не переписываются
|
||
(🛡 неизменяемость версий).
|
||
4. **Сбой одного файла не останавливает пакет** (tech-req 5) — файл
|
||
остаётся в `Import.Dir`, итоговый код возврата 2.
|
||
5. **API пишет факт-данные только из sync и импорта** — импорт не
|
||
создаёт/не меняет ничего, кроме `workout`(+sets нет)/`program`/
|
||
`program_version`/`exercise` (создание отсутствующих упражнений
|
||
пакета).
|
||
|
||
## 4. Критерии приёмки модуля
|
||
|
||
1. .fit с Session/Lap/Record/Set разбирается: `facts_json` соответствует
|
||
схеме `workout-facts/1`, `avg_hr`/`hr_recovery_s` посчитаны;
|
||
сопоставление с телефонной тренировкой в окне ± 15 минут — факты в
|
||
её строке; вне окна — строка «только .fit».
|
||
2. Повторный импорт того же .fit — пропуск (0 записей); .fit с чужим
|
||
`fit_file_id` — импорт.
|
||
3. Пакет программы: первая загрузка создаёт программу+версию,
|
||
упражнения отсутствующие создаются; повторный импорт — новая версия
|
||
(max+1), `external_id` сохраняется.
|
||
4. TCX/GPX — упрощённый факт, архивируются.
|
||
5. Сбой одного файла не мешает остальным; код возврата 2; сбойный файл
|
||
остаётся в `Import.Dir`.
|
||
6. Тесты интеграционные против docker `watchbody-pg` — зелёные;
|
||
сборка решения без ошибок.
|