Портфолио Богачева Николая + REST API формы обратной связи с AI (GigaChat), SMTP, rate limiting, метриками и Docker/Nginx.
Important
Для запуска не нужны ключи GigaChat, SMTP и metrics-токены.
Скопируйте .env.example → .env и поднимите compose.
Секреты (если понадобятся) клиент добавляет сам в .env / выпускает токен CLI.
POST /api/contact— валидация, PostgreSQL, AI-анализ, email owner + userGET /api/health— проверки db / redis / ai_configured / smtp_configuredGET /api/metrics— агрегаты (только с API-токеномmetrics:read)- Статический лендинг (RU/EN, dark/light) через Nginx
Архитектура: Router → Service → Repository (SQLAlchemy ORM).
Строковые поля в БД без NULL: не передано → "".
| Что | Версия / примечание |
|---|---|
| Docker Desktop / Engine | с Compose v2 |
| Свободный порт | 80 (или задайте HTTP_PORT в .env) |
| Git | клонирование репозитория |
| Опционально | GigaChat key, SMTP app-password — только для «полного» E2E |
Не требуется: Python на хосте, локальный Postgres/Redis, npm, заранее выпущенные токены.
git clone <repo-url> contact-forms
cd contact-forms
# Linux / macOS
cp .env.example .env
# Windows PowerShell
Copy-Item .env.example .env
docker compose up --build -d
docker compose psПорядок старта контейнеров:
- db, redis — healthcheck (
pg_isready,PING) - api — entrypoint ждёт DB + Redis,
alembic upgrade head, uvicorn; healthcheckGET /api/health - nginx — стартует только когда api healthy
Без секретов сервис уже полноценно отвечает:
| Проверка | Ожидание |
|---|---|
GET /api/health |
db=true, redis=true; status часто degraded (нет AI/SMTP keys) |
POST /api/contact |
201, ai.provider=fallback, email.owner/user=skipped |
GET / |
лендинг 200 |
URL после старта:
| URL | Назначение |
|---|---|
| http://localhost/ | лендинг |
| http://localhost/api/health | health |
| http://localhost/docs | OpenAPI (если ENABLE_DOCS=true) |
| http://localhost/redoc | ReDoc |
| http://localhost/api/metrics | метрики (нужен токен — см. ниже) |
# статус контейнеров (api/nginx должны быть healthy)
docker compose ps
# health
curl -s http://localhost/api/health
# contact (fallback AI, email skipped без SMTP)
curl -s -X POST http://localhost/api/contact \
-H "Content-Type: application/json" \
-d "{\"name\":\"Иван Иванов\",\"phone\":\"+79001234567\",\"email\":\"ivan@example.com\",\"comment\":\"Интересует сотрудничество по FastAPI\",\"locale\":\"ru\"}"
# логи api при проблемах
docker compose logs api --tail 100Windows PowerShell (health):
Invoke-RestMethod http://localhost/api/healthdocker compose down # остановить, volumes сохранить
docker compose down -v # + удалить БД/логи volumes
docker compose up --build -d # пересборка после правок кода/Dockerfile
docker compose up -d --force-recreate api # только api после смены .envШаблон: .env.example → .env (в .gitignore).
| Группа | Переменные | Обязательно для старта? |
|---|---|---|
| App | ENABLE_DOCS, CORS_ORIGINS, LOG_*, HTTP_PORT |
нет (есть defaults) |
| Postgres | POSTGRES_*, DATABASE_URL |
нет (compose defaults) |
| Redis / RL | REDIS_URL, RATE_LIMIT_* |
нет |
| Resources | DB_MEM_LIMIT, API_MEM_LIMIT, … |
нет |
| SMTP | SMTP_*, OWNER_EMAIL, EMAIL_ENABLED |
нет (без них email=skipped) |
| AI | AI_*, GIGACHAT_* |
нет (без ключа → fallback) |
Compose переопределяет DATABASE_URL / REDIS_URL на внутренние хосты db / redis.
Как в mail-manager: deploy.resources.limits в docker-compose.yml.
| Сервис | Default RAM | Default CPU |
|---|---|---|
| db | 512M | 1.0 |
| redis | 128M | 0.5 (+ maxmemory 64mb LRU) |
| api | 512M | 1.0 |
| nginx | 128M | 0.5 |
Переопределение в .env: API_MEM_LIMIT=768M, DB_CPU_LIMIT=1.5 и т.д.
На Docker Compose без Swarm лимиты
deploy.resourcesприменяются в Docker Compose v2 / Desktop; при отсутствии поддержки они игнорируются, стек всё равно стартует.
Токен не лежит в репозитории. После первого запуска:
docker compose exec api python -m scripts.create_api_token --name local --scope metrics:read
# скрипт один раз печатает cf_live_... — сохраните локально
curl -s http://localhost/api/metrics -H "Authorization: Bearer <token>"
# или
curl -s http://localhost/api/metrics -H "X-API-Key: <token>"В БД хранится только SHA-256 hash.
cd backend
python -m pip install -r requirements.txt
python -m pytest -qВнешние сервисы для unit-тестов не нужны.
E2E smoke против уже поднятого стека:
python backend/scripts/e2e_smoke.py http://localhost| Технология | Зачем |
|---|---|
| Python 3.12 + FastAPI | async REST, OpenAPI, DI |
| Pydantic v2 | строгая валидация входа |
| SQLAlchemy 2 async + asyncpg | ORM-only доступ к данным |
| Alembic | миграции при старте api |
| PostgreSQL 16 | contacts / utm / ai / email / metrics |
| Redis 7 | rate limit; при падении — memory fallback |
| aiosmtplib + Jinja2 | SMTP как в mail-manager |
| GigaChat SDK | AI из РФ; offline fallback обязателен |
| Nginx | :80, static + reverse proxy |
| Docker multi-stage | non-root, образ из backend/ |
Принципы: KISS, REST, слои, graceful degrade, секреты только в env.
contact-forms/
├── backend/
│ ├── app/
│ │ ├── main.py # app factory + lifespan
│ │ ├── api/ # routers + deps
│ │ ├── core/ # config, logs, errors, RL, security
│ │ ├── db/ # engine, models
│ │ ├── repositories/ # ORM data access
│ │ ├── schemas/ # Pydantic DTO
│ │ ├── services/ # contact, AI, email, metrics
│ │ │ └── ai/ # gigachat, yandex, fallback, prompts
│ │ ├── clients/smtp_client.py
│ │ └── templates/email/
│ ├── alembic/
│ ├── scripts/
│ │ ├── create_api_token.py
│ │ └── e2e_smoke.py
│ ├── tests/ # unit / offline suite
│ ├── Dockerfile
│ ├── entrypoint.sh # wait db+redis → migrate → uvicorn
│ └── requirements.txt
├── frontend/ # portfolio landing
├── nginx/
├── docs/ # доп. материалы (AI branches и т.п.)
├── docker-compose.yml
├── .env.example
├── postman/
└── README.md
Слои: API → Services → Repositories → Clients.
Base path: /api
Даже при сбое AI/SMTP → 201, если contact записан; статусы интеграций в теле.
Request
{
"name": "Иван Иванов",
"phone": "+79001234567",
"email": "ivan@example.com",
"comment": "Интересует сотрудничество по FastAPI",
"locale": "ru",
"utm": {
"source": "hh",
"medium": "referral",
"campaign": "portfolio",
"term": "",
"content": "",
"landing_path": "/?utm_source=hh",
"referrer": "https://hh.ru/"
}
}| Поле | Правила |
|---|---|
| name | 2–100, буквы/пробелы/дефис/апостроф |
| phone | 10–15 цифр, optional + |
| EmailStr | |
| comment | 10–2000 |
| locale | ru | en |
| utm.* | optional → "" если нет |
| website | honeypot (если заполнено — 201 без side effects) |
201 Response
{
"id": "uuid",
"message": "Обращение принято",
"ai": {
"used": false,
"provider": "fallback",
"sentiment": "positive",
"category": "collaboration",
"priority": "normal",
"suggested_reply": "…"
},
"email": { "owner": "skipped", "user": "skipped" }
}| Code | Когда |
|---|---|
| 201 | contact сохранён |
| 422 | validation |
| 429 | rate limit (Retry-After) |
| 503 | БД недоступна |
| 500 | unexpected (detail generic) |
{
"status": "ok|degraded|error",
"version": "1.0.0",
"checks": {
"db": true,
"redis": true,
"ai_configured": false,
"smtp_configured": false
}
}| status | смысл |
|---|---|
ok |
db + redis + ai_configured |
degraded |
db ok, но redis/ai не в ideal state |
error |
db недоступна |
Заголовки: Authorization: Bearer <token> или X-API-Key: <token>.
Scope: metrics:read. Источник — metrics_daily.
| Code | Когда |
|---|---|
| 200 | ok |
| 401 | нет токена |
| 403 | неверный / expired / bad scope |
OpenAPI: /docs, /redoc (ENABLE_DOCS).
Postman: postman/Contact_Forms_API.postman_collection.json.
| Provider | Роль |
|---|---|
| GigaChat | primary, РФ, freemium |
| YandexGPT | optional |
| rule-based fallback | always-on |
AI_PROVIDER=gigachat|yandex|fallback
Контракт: sentiment, sentiment_score, category, priority, summary, suggested_reply.
ai.used=true только при успешном ответе внешнего API.
Ветки промптов (match/case): job / collaboration / question / spam / other / generic — см. docs/gigachat_prompt_branches.md и backend/app/services/ai/prompts.py.
Note
Без ключа проект уже работает (fallback). Этот блок — только если нужен живой GigaChat.
- developers.sber.ru → Сбер ID → проект GigaChat API.
- Scope физлица:
GIGACHAT_API_PERS. - Скопируйте Authorization key (не OAuth access token).
- В
.env:
AI_ENABLED=true
AI_PROVIDER=gigachat
GIGACHAT_CREDENTIALS=ВАШ_AUTHORIZATION_KEY_ОДНОЙ_СТРОКОЙ
GIGACHAT_MODEL=GigaChat
GIGACHAT_SCOPE=GIGACHAT_API_PERS
GIGACHAT_VERIFY_SSL_CERTS=false- Применить:
docker compose up -d --force-recreate api- Проверка:
docker compose exec api python -c "from app.core.config import get_settings; s=get_settings(); print(s.ai_configured, len(s.gigachat_credentials or ''))"
curl -s http://localhost/api/health # ai_configured: true| Ситуация | ai.used |
ai.provider |
|---|---|---|
| Key ok | true |
gigachat |
| Key пустой / ошибка / timeout | false |
fallback |
Частые ошибки: забыли recreate api; key с кавычками/Bearer; SSL → GIGACHAT_VERIFY_SSL_CERTS=false для local; scope.
EMAIL_ENABLED=true
SMTP_HOST=smtp.yandex.ru
SMTP_PORT=465
SMTP_USERNAME=your@yandex.ru
SMTP_PASSWORD=app-password
SMTP_FROM=your@yandex.ru
SMTP_USE_SSL=true
OWNER_EMAIL=you@example.com- Пароль приложения у провайдера.
- Вставить в
.env(пустые user/pass = emailskipped). docker compose up -d --force-recreate api.
Письма: owner (AI summary) + user copy. Статусы: sent|failed|skipped.
{
"detail": "Человекочитаемое сообщение",
"code": "rate_limit_exceeded",
"request_id": "uuid"
}| Ситуация | HTTP | code |
|---|---|---|
| Pydantic | 422 | validation_error |
| Rate limit | 429 | rate_limit_exceeded |
| Metrics token | 401/403 | unauthorized / forbidden |
| DB down | 503 | database_unavailable |
| Unknown | 500 | internal_error |
AI/SMTP partial failure → 201 + честные статусы.
Volume api_logs → /app/logs в контейнере:
access.log— method, path, status, ms, ip, request_idapp.log/error.log
docker compose exec api ls -la /app/logs
docker compose logs -f apiСекреты не логируются. Ответ дополняется X-Request-ID.
| Сценарий | Поведение |
|---|---|
| Redis down | memory rate limiter, health degraded |
| GigaChat down/timeout | fallback, contact 201 |
| SMTP fail | contact saved, email failed |
| Honeypot | 201 без side effects |
| DB down at request | 503 |
| DB/Redis slow at boot | entrypoint ждёт до ~60s, иначе api не стартует |
| Body limit | Nginx (см. conf) |
| Симптом | Действие |
|---|---|
port is already allocated |
смените HTTP_PORT=8080 в .env |
| api не healthy | docker compose logs api — DB/Redis/migrate |
| nginx Up, api restarting | дождаться migrate; проверить .env syntax |
| contact 429 | подождать окно RL или docker compose exec redis redis-cli FLUSHDB (local only) |
ai_configured=false |
пустой GIGACHAT_CREDENTIALS — ожидаемо без ключа |
| email always skipped | не заданы SMTP_USERNAME / SMTP_PASSWORD |
- BackgroundTasks / очередь email после commit
- Prometheus + OpenTelemetry
- Captcha / Turnstile при росте спама
- Admin UI contacts
- Multi-worker + только Redis RL
- Кэш OAuth-токена GigaChat
- Playwright e2e лендинга
- Helm/K8s при cloud deploy
См. LICENSE (если файл добавлен в репозиторий).