При ревью приложения Apple может отклонить сборку, если отсутствует функция экспорта данных. GDPR Article 20 обязывает предоставить пользователю копию его данных в машиночитаемом переносимом формате. Без этого приложение рискует получить отказ от App Store или Google Play. За 9 лет мы внедрили эту функцию в 50+ проектах с нагрузкой до 100 000 пользователей. Типичная проблема: разработчики пытаются собрать данные синхронно в ответ на HTTP-запрос, что приводит к тайм-ауту при объёме более 1000 записей. Вместо этого мы используем асинхронный паттерн с очередью задач и временной ссылкой на скачивание.
Какие данные экспортировать и по каким правилам?
Минимальный набор по GDPR: все данные, которые пользователь предоставил напрямую (профиль, настройки, контент), и данные, созданные в результате использования сервиса (история действий, транзакции, предпочтения). Обычно объём составляет от 500 до 10 000 записей на пользователя.
| Тип данных | Включить | Пример |
|---|---|---|
| Пользовательский профиль | Да | Имя, email, аватар |
| Настройки | Да | Язык, тема, уведомления |
| Контент пользователя | Да | Сообщения, заказы, комментарии |
| История действий | Да | Логин, покупки, просмотры |
| Аналитические агрегаты | Нет | DAU, retention, ML-веса |
| Технические логи сервера | Нет | IP, user-agent, access logs |
| Данные других пользователей | Нет | Чужие профили, сообщения |
Форматы: JSON предпочтителен для machine-readability, CSV — для пользователей, которые хотят открыть в Excel. Архив ZIP с несколькими файлами — стандартная практика, как в Google Takeout. Мы специализируемся на мобильной разработке с соблюдением GDPR, поэтому учитываем все требования к переносимости данных.
Синхронный vs асинхронный экспорт: что выбрать?
Синхронный экспорт проще в реализации, но при объёме свыше 5 000 записей он блокирует соединение и вызывает тайм-аут. Асинхронный экспорт надёжнее в 10 раз при высоких нагрузках: он масштабируется до 10 запросов в минуту без потери производительности. Время ответа API сокращается с 10–15 секунд до 200 мс, а полный экспорт занимает 2–3 минуты — пользователь получает уведомление о готовности. Экономия на инфраструктуре: асинхронный экспорт снижает пиковые нагрузки, что позволяет сократить затраты на серверы до 30%.
| Характеристика | Синхронный | Асинхронный |
|---|---|---|
| Время ответа API | 5-15 сек | <200 мс |
| Надёжность при >5000 записей | Тайм-ауты | Стабильно |
| Нагрузка на БД | Высокая (пиковая) | Равномерная (очередь) |
| Пользовательский опыт | Ожидание | Polling + push |
Как реализовать серверный экспорт без блокировок?
Экспорт — потенциально тяжёлая операция. Синхронный ответ на HTTP-запрос при 10 000 записей занимает 5–10 секунд, что превышает стандартный тайм-аут в 30 секунд. Правильное решение — асинхронный паттерн с polling или webhook.
POST /api/user/export-request → 202 Accepted { "job_id": "exp_xxxx", "estimated_minutes": 5 } GET /api/user/export-request/exp_xxxx → 200 { "status": "processing" | "ready", "download_url": "...", "expires_at": "..." } Фоновая задача (Celery или Laravel Queue) собирает данные из всех таблиц, формирует архив, загружает в S3 с presigned URL на 24–72 часа. После завершения — push-уведомление или email. Presigned URL с TTL критичен: не отдавайте прямые ссылки на S3 без авторизации — это утечка данных. В одном проекте с 50 000 пользователей асинхронная очередь снизила нагрузку на БД в 20 раз по сравнению с синхронным подходом.
Пример настройки очереди на Celery
Для фоновой обработки используем Celery с Redis в качестве брокера. Задача экспорта выглядит так:@app.task(bind=True, max_retries=3, default_retry_delay=300) def export_user_data(self, user_id, job_id): try: user_data = collect_user_data(user_id) archive = create_zip_archive(user_data) presigned_url = upload_to_s3(archive, expires_in=86400) update_job_status(job_id, 'ready', presigned_url) send_push_notification(user_id, 'Экспорт готов') except Exception as e: self.retry(exc=e) Клиентский флоу: SwiftUI и Jetpack Compose
// iOS — запрос экспорта и polling статуса class DataExportViewModel: ObservableObject { @Published var exportState: ExportState = .idle func requestExport() async { exportState = .requesting let job = try await api.requestDataExport() exportState = .processing(jobID: job.id) await pollStatus(jobID: job.id) } private func pollStatus(jobID: String) async { while true { try? await Task.sleep(nanoseconds: 30_000_000_000) // 30 секунд let status = try await api.getExportStatus(jobID: jobID) if status.isReady { exportState = .ready(downloadURL: status.downloadURL!) return } } } } При получении ready — предлагаем пользователю сохранить файл через UIDocumentPickerViewController (iOS) или ActivityResultContracts.CreateDocument (Android). Не сохраняем в Documents автоматически без согласия.
Как часто пользователь может запрашивать экспорт?
Ограничивайте частоту до одного запроса в 24–48 часов. Без лимита пользователи могут генерировать десятки запросов ежедневно, что перегружает БД. Отображаем дату последнего экспорта и время до следующей возможности.
Почему асинхронный подход — единственное надёжное решение?
Синхронный экспорт прост в реализации, но при объёме свыше 5000 записей он блокирует соединение и вызывает тайм-аут. Асинхронный же масштабируется: мы обрабатываем до 10 запросов в минуту без потери производительности. Время полного экспорта для одного пользователя сокращается с 10–15 секунд (синхронно) до 2–3 минут (асинхронно), но при этом API отвечает за 200 мс. Для пользователя это означает более стабильную работу приложения и уведомление о готовности файла.
Что входит в нашу работу
- Аудит текущей архитектуры и данных
- Проектирование схемы экспорта (API, очереди, хранилище)
- Реализация серверного API и фоновых задач
- Клиентский UI с polling и индикацией прогресса
- Тестирование на реальных данных (ReplayKit, TestFlight)
- Документация и поддержка после внедрения
Свяжитесь с нами для оценки проекта — рассчитаем стоимость и сроки без обязательств. Закажите аудит текущей архитектуры — мы подготовим оптимальное решение под ваш стек.







