@startuml title Создание отчёта о рабочем времени (face_work_time_json) actor Client participant "face/api" as API database "MongoDB\nuser_reports" as ReportsDB participant "report_checker\n(Worker)" as Checker participant "reporter.py" as Reporter participant "WorkTimeJsonReport" as WorkTime participant "data_loader" as DataLoader database "MongoDB\nface_events" as EventsDB database "MongoDB\nfaces" as FacesDB participant "tools.py" as Tools database "Redis" as Redis == 1. Создание задачи на отчёт == Client -> API: POST /reports\n(type=face_work_time_json) API -> API: FaceReport.create()\nvalidate options, build query API -> ReportsDB: insert(status='in_queue') API --> Client: {success: true, id: report_id} == 2. Обработка отчёта воркером == loop каждые SLEEP_INTERVAL секунд Checker -> ReportsDB: find_one_and_update\n(status='in_queue' → 'in_progress') end Checker -> Checker: ProcessPoolExecutor\ncreate_report_sync(report) note right: Новый процесс\nс asyncio.run() Checker -> Reporter: create_report(report) Reporter -> Reporter: @with_context\nустановить контекст пользователя Reporter -> Reporter: Выбрать класс из REGISTRY\n(face_work_time_json → WorkTimeJsonReport) Reporter -> WorkTime: make_report() == 3. Загрузка данных == WorkTime -> DataLoader: get_events(query) DataLoader -> EventsDB: find(query)\n[best_shot_time, face_id, camera_id] EventsDB --> DataLoader: events[] DataLoader --> WorkTime: events WorkTime -> DataLoader: get_faces(galleries, events,\ncameras_in, cameras_out, tz) loop для каждого события DataLoader -> DataLoader: result[face_id] = GroupedFace(face_id) DataLoader -> DataLoader: group.handle(best_shot_time, direction) end DataLoader -> DataLoader: _get_faces_without_events()\n(лица из галерей без событий) DataLoader -> FacesDB: find(_id NOT IN face_ids,\ngallery IN galleries) FacesDB --> DataLoader: faces_without_events DataLoader -> DataLoader: face_ids = events + faces_without_events == 4. Обогащение данных фотографиями == DataLoader -> DataLoader: _get_faces_data(face_ids) DataLoader -> FacesDB: find(_id IN face_ids)\n[person, photos, description, gallery_id] FacesDB --> DataLoader: faces_data[] loop для каждого face DataLoader -> DataLoader: _get_photo(face_data) alt photos пустой или None DataLoader --> DataLoader: return None note right #pink: **ПРИЧИНА 1**\nphotos отсутствует else photos есть DataLoader -> DataLoader: найти default фото\nили первое из списка DataLoader -> Tools: get_thumb_url(photo) alt есть thumbnails.thumbnail_200.url Tools --> DataLoader: url else есть thumbnails.thumbnail_200.obj_ref Tools -> Tools: storage.generate_presigned_url() Tools --> DataLoader: presigned_url else нет thumbnail_200 Tools --> DataLoader: None note right #pink: **ПРИЧИНА 2**\nнет thumbnail_200 end end DataLoader -> DataLoader: face.photo = photo_url end DataLoader --> WorkTime: List[GroupedFace] == 5. Генерация и сохранение == WorkTime -> WorkTime: _generate(query, faces, tz, schedule) note right: Формирует JSON:\n[name, photo, face_id, ...] WorkTime -> Redis: setex(key, json, TTL=1 day) WorkTime -> ReportsDB: update(status='done',\n_redis_key=key) == 6. Получение результата == Client -> API: GET /reports/{id}/json API -> ReportsDB: find(_id=id) API -> Redis: get(_redis_key) Redis --> API: json_data API --> Client: {summary: {...}, details: {...}} @enduml