Перейти к содержанию

Архитектура Silent Meadow

Обзор системы

Silent Meadow - это standalone CRM-платформа для управления продажами недвижимости с фокусом на автоматизацию работы с площадками объявлений (Avito, Циан) и интеграцию с внешними источниками лидов.

Microservices Architecture

┌──────────────────────────┐
│  Leadgen Platform        │  (Внешний сервис)
│  ─────────────────       │
│  • Парсинг Telegram      │
│  • AI-оценка лидов       │
│  • Semantic search       │
└──────────┬───────────────┘
           │ REST API / Webhook
┌──────────────────────────┐
│  Silent Meadow           │  (Этот проект)
│  ─────────────────       │
│                          │
│  ┌────────────────────┐  │
│  │   Frontend         │  │  React + TypeScript
│  │   (React SPA)      │  │  • Карта участков
│  │                    │  │  • Каталог домов
│  │                    │  │  • Управление Avito/Циан
│  │                    │  │  • CRM дашборд
│  └─────────┬──────────┘  │
│            │ HTTP/REST   │
│            ↓             │
│  ┌────────────────────┐  │
│  │   Backend          │  │  FastAPI + PostgreSQL
│  │   (FastAPI)        │  │  • CRUD недвижимости
│  │                    │  │  • Avito/Циан API
│  │                    │  │  • CRM логика
│  │                    │  │  • Аналитика
│  └─────────┬──────────┘  │
│            │             │
│     ┌──────┴──────┐      │
│     │             │      │
│  PostgreSQL    Redis     │
│  (БД)         (Кэш)      │
│                          │
└────────┬─────────────────┘
         └──→ External APIs:
              • Avito API (публикация, чаты, автоответчик)
              • Циан API (публикация объявлений)
              • Yandex Maps API (геокодинг, карты)
              • Telegram Bot API (CRM бот для менеджеров)
              • OpenAI API (генерация описаний, автоответчик)

Разделение ответственности

Сервис Ответственность
Leadgen Platform Парсинг Telegram, AI-оценка лидов, ChromaDB semantic search
Silent Meadow Управление недвижимостью, Avito/Циан интеграции, CRM, аналитика

Интеграция между сервисами

# Silent Meadow получает лиды из Leadgen Platform

# Вариант 1: REST API (polling)
GET https://leadgen.example.com/api/v1/leads?project_id=mdk&min_score=7

# Вариант 2: Webhook (push)
POST https://silent-meadow.example.com/api/webhooks/leadgen
{
  "lead_id": "uuid",
  "name": "Иван",
  "telegram_username": "@ivan",
  "message": "Ищу участок до 500К",
  "score": 8.5,
  "created_at": "2026-02-16T12:00:00Z"
}

Модели данных

1. Property (участки земли)

class Property(Base):
    __tablename__ = "properties"

    id: UUID = Field(primary_key=True)
    number: str = Field(nullable=False)              # "502", "503"
    cadastral_number: str = Field(unique=True)       # "50:18:0090613:670"
    area_sotok: float = Field(nullable=False)        # 10.0
    price: int = Field(nullable=False)               # 299000
    original_price: int | None                       # 389000 (для скидки)
    status: str = Field(default="available")         # available, reserved, sold
    latitude: float | None                           # 55.123456
    longitude: float | None                          # 37.123456
    settlement: str                                  # "Тихие Луга", "Мечтаево"
    description: str | None
    images: list[str] = Field(default=[])            # URLs фото
    created_at: datetime
    updated_at: datetime

    # Relations
    avito_ads: list["AvitoAd"]
    cian_ads: list["CianAd"]
    leads: list["Lead"]

Индексы: - cadastral_number (UNIQUE) - status (для фильтрации) - (latitude, longitude) (для пространственных запросов)


2. House (типовые дома)

class House(Base):
    __tablename__ = "houses"

    id: UUID = Field(primary_key=True)
    name: str = Field(nullable=False)                # "Шале", "Барнхаус", "Хай-тек"
    type: str = Field(nullable=False)                # "shale", "barnhouse", "hi_tech"
    area_sqm: int = Field(nullable=False)            # 40, 60, 80, 120
    price_economy: int = Field(nullable=False)       # Комплектация "Эконом"
    price_comfort: int = Field(nullable=False)       # Комплектация "Комфорт"
    description: str | None
    floor_plan_url: str | None                       # URL планировки
    images: list[str] = Field(default=[])            # URLs фото
    features: dict = Field(default={})               # {"bedrooms": 2, "bathrooms": 1}
    created_at: datetime
    updated_at: datetime

    # Relations
    avito_ads: list["AvitoAd"]
    cian_ads: list["CianAd"]
    leads: list["Lead"]

Возможные комбинации: - Шале: 40, 60, 80 м² - Барнхаус: 60, 80, 120 м² - Хай-тек: 80, 120 м²


3. AvitoAd (объявления на Avito)

class AvitoAd(Base):
    __tablename__ = "avito_ads"

    id: UUID = Field(primary_key=True)
    property_id: UUID | None = Field(foreign_key="properties.id")
    house_id: UUID | None = Field(foreign_key="houses.id")
    avito_ad_id: str | None = Field(unique=True)     # ID в системе Avito
    title: str = Field(nullable=False)               # "Участок 10 соток..."
    description: str = Field(nullable=False)
    price: int = Field(nullable=False)
    status: str = Field(default="draft")             # draft, active, paused, sold
    views: int = Field(default=0)
    contacts: int = Field(default=0)                 # Количество обращений
    published_at: datetime | None
    expires_at: datetime | None
    created_at: datetime
    updated_at: datetime

    # Relations
    property: "Property"
    house: "House"
    chats: list["AvitoChat"]

Статусы: - draft - создано, не опубликовано - active - опубликовано и активно - paused - снято с публикации - sold - продано


4. CianAd (объявления на Циан)

class CianAd(Base):
    __tablename__ = "cian_ads"

    id: UUID = Field(primary_key=True)
    property_id: UUID | None = Field(foreign_key="properties.id")
    house_id: UUID | None = Field(foreign_key="houses.id")
    cian_ad_id: str | None = Field(unique=True)      # ID в системе Циан
    title: str = Field(nullable=False)
    description: str = Field(nullable=False)
    price: int = Field(nullable=False)
    status: str = Field(default="draft")             # draft, active, paused, sold
    views: int = Field(default=0)
    contacts: int = Field(default=0)
    published_at: datetime | None
    expires_at: datetime | None
    created_at: datetime
    updated_at: datetime

    # Relations
    property: "Property"
    house: "House"

Статусы: аналогично AvitoAd


5. Lead (лиды из всех каналов)

class Lead(Base):
    __tablename__ = "leads"

    id: UUID = Field(primary_key=True)
    name: str | None
    contact: str | None                              # email или phone
    telegram_username: str | None
    phone: str | None
    message: str                                     # Исходное сообщение

    # Источник лида
    source: str = Field(nullable=False)              # leadgen_platform, avito, cian, manual
    source_lead_id: str | None                       # ID в исходной системе
    score: float | None = Field(ge=0, le=10)        # AI-оценка (только для leadgen)

    # Связь с недвижимостью
    property_id: UUID | None = Field(foreign_key="properties.id")
    house_id: UUID | None = Field(foreign_key="houses.id")

    # CRM статус
    status: str = Field(default="new")               # new, contacted, qualified, won, lost
    assigned_to: UUID | None = Field(foreign_key="users.id")
    contact_method: str | None                       # telegram, phone, email, avito_chat
    notes: str | None
    created_at: datetime
    updated_at: datetime

    # Relations
    property: "Property"
    house: "House"
    manager: "User"

Источники лидов: - leadgen_platform - лид из Telegram через Leadgen Platform API/webhook - avito - обращение через Avito чат - cian - обращение через Циан - manual - вручную добавленный лид


5. AvitoChat (чаты с покупателями)

class AvitoChat(Base):
    __tablename__ = "avito_chats"

    id: UUID = Field(primary_key=True)
    avito_ad_id: UUID = Field(foreign_key="avito_ads.id")
    avito_chat_id: str = Field(unique=True)          # ID чата в Avito
    buyer_name: str | None
    last_message: str | None
    last_message_at: datetime | None
    unread_count: int = Field(default=0)
    created_at: datetime

    # Relations
    ad: "AvitoAd"
    messages: list["AvitoMessage"]

API Endpoints

Properties API

GET    /v1/properties                  # Список участков (с фильтрами)
POST   /v1/properties                  # Создать участок
GET    /v1/properties/{id}             # Детали участка
PATCH  /v1/properties/{id}             # Обновить участок
DELETE /v1/properties/{id}             # Удалить участок
GET    /v1/map/properties              # GeoJSON для карты
GET    /v1/map/clusters                # Кластеры участков

Пример запроса:

GET /v1/properties?status=available&min_price=200000&max_price=400000&settlement=Тихие Луга

Пример ответа:

{
  "items": [
    {
      "id": "uuid",
      "number": "502",
      "cadastral_number": "50:18:0090613:670",
      "area_sotok": 10,
      "price": 299000,
      "original_price": 389000,
      "status": "available",
      "latitude": 55.123456,
      "longitude": 37.123456,
      "settlement": "Тихие Луга",
      "images": ["url1", "url2"]
    }
  ],
  "total": 436,
  "page": 1,
  "per_page": 20
}


Houses API

GET    /v1/houses                      # Каталог домов
POST   /v1/houses                      # Добавить дом
GET    /v1/houses/{id}                 # Детали дома
PATCH  /v1/houses/{id}                 # Обновить дом
DELETE /v1/houses/{id}                 # Удалить дом

Avito API

POST   /v1/avito/auth                  # Подключить Avito аккаунт (OAuth2)
GET    /v1/avito/auth/status           # Статус подключения
GET    /v1/avito/auth/callback         # OAuth callback

POST   /v1/avito/ads                   # Создать объявление
GET    /v1/avito/ads                   # Список объявлений
GET    /v1/avito/ads/{id}              # Детали объявления
PATCH  /v1/avito/ads/{id}              # Обновить объявление
DELETE /v1/avito/ads/{id}              # Удалить объявление
POST   /v1/avito/ads/{id}/publish      # Опубликовать
POST   /v1/avito/ads/{id}/pause        # Приостановить

GET    /v1/avito/chats                 # Чаты с покупателями
GET    /v1/avito/chats/{id}/messages   # История сообщений
POST   /v1/avito/chats/{id}/message    # Отправить сообщение
POST   /v1/avito/webhook               # Webhook от Avito

Циан API

POST   /v1/cian/auth                   # Подключить Циан аккаунт
GET    /v1/cian/auth/status            # Статус подключения

POST   /v1/cian/ads                    # Создать объявление
GET    /v1/cian/ads                    # Список объявлений
GET    /v1/cian/ads/{id}               # Детали объявления
PATCH  /v1/cian/ads/{id}               # Обновить объявление
DELETE /v1/cian/ads/{id}               # Удалить объявление
POST   /v1/cian/ads/{id}/publish       # Опубликовать
POST   /v1/cian/ads/{id}/pause         # Приостановить

GET    /v1/cian/stats                  # Статистика по объявлениям

Leadgen Platform Integration

POST   /v1/webhooks/leadgen            # Webhook от Leadgen Platform
GET    /v1/leadgen/status              # Статус интеграции
GET    /v1/leads                       # Все лиды (из всех источников)
GET    /v1/leads/{id}                  # Детали лида
PATCH  /v1/leads/{id}                  # Обновить лид (статус, заметки)

Telegram CRM Bot

POST   /v1/telegram/notify             # Отправить уведомление менеджеру
GET    /v1/telegram/bot/status         # Статус бота

Yandex Maps API

GET    /v1/map/properties              # Все участки в формате GeoJSON
GET    /v1/map/clusters?zoom={level}   # Кластеры для уровня zoom
GET    /v1/map/geocode?address={addr}  # Геокодинг адреса

Схема взаимодействия компонентов

1. Получение лидов из Leadgen Platform → CRM

[Leadgen Platform]
    ↓ (Webhook или REST API)
POST /v1/webhooks/leadgen
{
  "lead_id": "uuid",
  "name": "Иван",
  "telegram_username": "@ivan",
  "message": "Ищу участок до 500К",
  "score": 8.5
}
[Silent Meadow Backend]
    ↓ (создание лида в БД)
[Lead сохранен]
[Telegram CRM бот]
[Уведомление менеджера]

2. Создание объявления на Avito

[Manager UI] → POST /v1/avito/ads
[Backend: AvitoService]
    ↓ (OAuth2 access token)
[Avito API: POST /items]
[AvitoAd сохранен в БД]
    ↓ (webhook от Avito)
[Обновление статистики: views, contacts]

3. Автоответчик Avito

[Покупатель → сообщение в Avito]
    ↓ (webhook)
[POST /v1/avito/webhook]
[AvitoAutoResponder]
    ↓ (GPT-4: анализ вопроса)
[Генерация ответа на базе данных об участке/доме]
[Отправка через Avito API]
    ↓ (создание лида)
[Lead сохранен в БД]
[Уведомление менеджера в Telegram]

Безопасность

Аутентификация

  • JWT токены для API (access + refresh)
  • Cookies для веб-сессий (httpOnly, secure)
  • OAuth2 для интеграции с Avito

Авторизация

  • RBAC: роли admin, manager, viewer
  • Менеджеры видят только свои лиды
  • Админы имеют полный доступ

Rate Limiting

# FastAPI middleware
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    # Redis-based rate limiting
    # 100 requests per minute per IP
    pass

CORS

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-domain.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Secrets Management

  • .env файлы для разработки
  • Environment variables в production
  • AWS Secrets Manager / HashiCorp Vault (опционально)

Кэширование

Redis Cache Strategy

# Кэш участков (TTL: 5 минут)
@cache(ttl=300, key="properties:all")
async def get_all_properties():
    pass

# Кэш статистики Avito (TTL: 1 час)
@cache(ttl=3600, key="avito:stats:{ad_id}")
async def get_avito_ad_stats(ad_id: str):
    pass

# Инвалидация при изменении
@invalidate_cache(pattern="properties:*")
async def update_property(property_id: UUID, data: dict):
    pass

Масштабирование

Горизонтальное масштабирование Backend

  • FastAPI поддерживает async/await
  • Деплой нескольких инстансов за Load Balancer
  • Stateless архитектура (сессии в Redis)

Database Optimization

  • Индексы на часто запрашиваемых полях
  • Connection pooling (SQLAlchemy)
  • Read replicas для аналитики

CDN для статики

  • Изображения участков/домов → S3 + CloudFront
  • Frontend build → Vercel CDN

Мониторинг и логирование

Logging

import structlog

logger = structlog.get_logger()
logger.info("property_created", property_id=str(property.id), number=property.number)

Мониторинг

  • Sentry - отслеживание ошибок
  • Prometheus + Grafana - метрики (requests/s, latency, DB queries)
  • Uptime monitoring - healthcheck endpoints

Healthcheck

GET /health
{
  "status": "ok",
  "database": "connected",
  "redis": "connected",
  "avito_api": "authenticated"
}

Технологический стек (детально)

Backend

  • FastAPI 0.109 - веб-фреймворк
  • SQLAlchemy 2.0 - ORM
  • Alembic - миграции
  • Pydantic - валидация данных
  • Redis - кэширование
  • PostgreSQL 15 - основная БД

Frontend

  • React 18.2 - UI
  • TypeScript 5.2 - типизация
  • TailwindCSS 3.4 - стили
  • React Query - управление серверным состоянием
  • Zustand - локальное состояние
  • React Hook Form - формы
  • Yandex Maps React - карты

Infrastructure

  • Docker + Docker Compose - контейнеризация
  • GitHub Actions - CI/CD
  • Render.com - хостинг backend
  • Vercel - хостинг frontend
  • AWS S3 - хранилище изображений

Диаграмма компонентов (детально)

┌──────────────────────────────────────────────────────────────┐
│                         FRONTEND                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │Properties│  │  Houses  │  │  Avito   │  │   Циан   │   │
│  │   Page   │  │   Page   │  │   Page   │  │   Page   │   │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘   │
│       │             │             │             │           │
│       └─────────────┴─────────────┴─────────────┘           │
│                            │                                 │
│                     ┌──────┴──────┐                          │
│                     │ API Client  │                          │
│                     └──────┬──────┘                          │
└────────────────────────────┼────────────────────────────────┘
                             │ HTTP/REST
┌────────────────────────────┼────────────────────────────────┐
│                   BACKEND (FastAPI)                          │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌──────────┐│
│  │Properties │  │   Avito   │  │   Циан    │  │Leadgen   ││
│  │ Service   │  │  Service  │  │  Service  │  │Integration││
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  └────┬─────┘│
│        │              │              │              │       │
│  ┌─────┴──────────────┴──────────────┴──────────────┴────┐ │
│  │              Database Layer (SQLAlchemy)                 │ │
│  └───────────────────────┬──────────────────────────────┘ │
└──────────────────────────┼────────────────────────────────┘
         ┌─────────────────┴──────────────────┐
         │                                    │
    ┌────┴────┐                         ┌─────┴─────┐
    │PostgreSQL│                         │   Redis   │
    └──────────┘                         └───────────┘

Внешние сервисы: - Leadgen Platform - получение лидов из Telegram - Avito API - публикация объявлений, чаты - Циан API - публикация объявлений - Yandex Maps API - геокодинг, карты - Telegram Bot API - CRM бот для менеджеров


Следующие шаги

  1. Инициализация проекта (FastAPI + PostgreSQL + React)
  2. Реализация моделей данных (Property, House, AvitoAd, CianAd, Lead)
  3. Создание REST API endpoints
  4. Интеграция с Avito API (публикация, чаты, автоответчик)
  5. Интеграция с Циан API
  6. Интеграция с Yandex Maps API
  7. Интеграция с Leadgen Platform (webhook/REST API)
  8. Telegram CRM бот для менеджеров
  9. Разработка Frontend (карта, каталоги, CRM дашборд)
  10. Тестирование и деплой

Подробности смотрите в docs/plan.md