Перейти к содержимому

LLD — низкоуровневый дизайн

LLD (Low-Level Design) — низкоуровневый (детальный) архитектурный дизайн. Описывает, как реализуется то, что зафиксировано в BRD: декомпозиция системы по уровням C4 (контейнеры, компоненты), сценарии взаимодействия (sequence) и спецификации внешних API. LLD — рабочая карта для команды разработки; при расхождении с BRD приоритет у BRD.

Проект: Web Shield AI Версия: 0.1 (черновик) Базовый документ: BRD Ответственный: А. Зубик (архитектор)


Многоуровневое проектирование сервиса Web Shield AI от контейнерной диаграммы (C2) до спецификации внешнего API.

Скоуп этого документа. Контейнеры Verdict API и Verdict Cache (Redis) показаны на C2 как design-задел (пунктирные интеграционные точки). По решению заказчика Verdict API в работу не берём — он оставлен в дизайне как зафиксированный контракт на будущее, на случай интеграции с PSP-шлюзом. В текущей реализации hits и вердикты пишутся в PostgreSQL, потребитель — только UI комплаенс-офицера и Reporter. Цель раздела — зафиксировать границы ответственности и точки интеграции так, чтобы при возможном расширении модель данных Scanner Engine не пришлось перекраивать.

Актуализация под реализацию. Диаграммы ниже сохраняют исходный проектный замысел (self-hosted LLM, vision/OCR, SSO). Фактически реализовано иначе: AI-слой — LLM-классификация и семантика через эмбеддинги облачного провайдера Mistral (+ vector DB Qdrant); OCR/vision и SSO вне области (см. BRD и RTM). Контейнеры читать с этой поправкой.

Принципы (унаследованы из практики lesson-05-lld):

  1. Contract First. Сначала фиксируется OpenAPI на внешнюю границу — Verdict API. Контракт страхует интеграционные риски с платёжным шлюзом, где SLA жёстче всего (p95 ≤ 200 мс).
  2. C4-уровни, по одному на диаграмму. Одна абстракция на диаграмму — без «слоёного пирога».
  3. Стрелки = глагол + технология. Каждая связь подписана действием и транспортом (HTTPS/JSON, gRPC, S3, AMQP и т.п.).
  4. High Cohesion + Low Coupling. Crawler / Scanner / AI Verifier — отдельные сервисы; AI-слой не знает деталей краула, общается через очередь и кандидатов.
  5. Резилентность — в LLD. Circuit Breaker / Retries / Timeouts на внешних клиентах (LLM, vision, headless). Деградация краула не должна влиять на доступность Verdict API (NFR-Availability).
  6. Single source of truth. Один владелец на одно правило (тариф проверяет Backend на инициации, не Scanner на каждой странице).

flowchart LR
  officer([Комплаенс-офицер])
  admin([Администратор сервиса])
  gw[[Платёжный шлюз / PSP]]
  merchant[[Сайт мерчанта]]
  sso[[Keycloak / Корпоративный SSO]]
  llm[[LLM Provider · Qwen self-hosted]]
  vision[[Vision / OCR Provider]]
  smtp[[SMTP / Webhook receiver]]

  subgraph SH[Web Shield AI]
    ui[Admin Web UI<br/>React / TypeScript]
    api[Backend / Control Plane<br/>Python · FastAPI]
    verdict[Verdict API · фаза 3<br/>Python · FastAPI · stateless]
    sched[Crawl Scheduler<br/>Celery beat · окно рекраула по тарифу]
    crawler[Crawler Worker<br/>httpx · respect robots.txt]
    headless[Headless Renderer<br/>Playwright · JS fallback FR-06]
    scanner[Scanner Engine<br/>Aho-Corasick + pymorphy3 ru/be]
    ai[AI Verifier · Premium<br/>LLM / embeddings / vision]
    reporter[Reporter<br/>PDF · история · экспорт]
    bus[(Message Bus<br/>RabbitMQ / Redis Streams)]
    pg[(SQL DB · PostgreSQL<br/>tenants · sites · hits · verdicts · audit)]
    qdr[(Vector DB · Qdrant<br/>embeddings стоп-фраз · Premium)]
    s3[(Object Storage · S3<br/>HTML-снимки · скриншоты · immutable)]
    cache[(Verdict Cache · Redis · фаза 3<br/>verdict by domain/URL · TTL=recrawl_interval)]
  end

  officer --> ui
  admin --> ui
  ui -->|REST · HTTPS/JSON| api
  ui -->|OIDC| sso
  api --> sso

  gw -. "фаза 3: GET /v1/verdict · HTTPS/JSON · JWT" .-> verdict
  verdict -. фаза 3 .-> cache
  verdict -. фаза 3 · miss .-> pg

  api --> pg
  api --> sched
  sched -->|enqueue crawl job| bus
  bus --> crawler
  crawler -->|HTTPS GET| merchant
  crawler -.->|JS-heavy → fallback| headless
  headless -->|HTTPS GET| merchant
  crawler -->|publish page| bus
  bus --> scanner
  scanner --> pg
  scanner --> s3
  scanner -. Premium candidates .-> bus
  bus --> ai
  ai -->|HTTPS/JSON| llm
  ai -->|HTTPS/JSON| vision
  ai --> qdr
  ai --> pg
  scanner -. "фаза 3: invalidate" .-> cache
  ai -. "фаза 3: invalidate" .-> cache

  api -->|alerts FR-17| smtp
  reporter --> pg
  reporter --> s3
  reporter -->|PDF FR-19| officer
КонтейнерЗачемПочему отдельный (deploy-критерий)
Admin Web UIУправление сайтами, стоп-листами, разметка срабатываний (FR-03, FR-04, FR-18)React SPA, отдельный CDN/static, релизится чаще API
Backend / Control PlaneMulti-tenant CRUD, тарифы (FR-02), управление расписанием, алертинг (FR-17)Бизнес-правила и SSO; не должен страдать от нагрузки краула
Verdict API (фаза 3)Низколатентный (p95 ≤ 200 мс) вердикт по URL/домену для платёжного шлюза (FR-15, FR-16)Отдельный сервис из-за SLA: деградация краула не имеет права валить hot path оплаты (NFR-Availability ≥ 99.5%). В MVP не реализуется — контракт зафиксирован заранее, см. раздел 5
Crawl SchedulerОчередь рекраула, окна и лимиты по тарифу (FR-13, FR-02)Workload-pattern «фоновые задачи + cron», изолируется от API
Crawler WorkerHTTP-обход страниц, robots.txt, троттлинг по домену (FR-05)I/O-bound, скейлится по входной очереди, отдельный namespace
Headless RendererJS-рендеринг страниц, где статический обход вернул мало текста (FR-06)Тяжёлые ресурсы (Chromium); только Premium / по необходимости
Scanner EngineИзвлечение текста, морфологический матчинг ru/be, анти-обфускация (FR-07, FR-08, FR-09)Ядро MVP; CPU-bound, скейлится отдельно. Раскрыт в разделе 3
AI Verifier (Premium)LLM-классификация (FR-10), семантический поиск (FR-21), OCR/vision (FR-11)GPU-нода / дорогой downstream; получает только кандидатов, не сырые страницы (инвариант экономики Премиума)
ReporterDaily/ondemand PDF-отчёты, история (FR-14, FR-19, FR-20)CronJob, batch-workload
Message BusРазвязка Crawler → Scanner → AI Verifier; backpressure, retriesЛюбой временный лаг AI не блокирует краул
SQL DB (PostgreSQL)Тенанты, сайты, стоп-листы, hits, verdicts, audit logManaged-сервис, HA, бэкапы
Vector DB (Qdrant)Эмбеддинги стоп-фраз и страниц для семантики (FR-21, Premium)Специализированное хранилище, только Premium
Object Storage (S3)HTML-снимки и скриншоты — immutable, append-only с TTL по политике (FR-12)Lifecycle-rules, дешёвое хранение
Verdict Cache (Redis) (фаза 3)Кэш вердиктов по домену/URL для p95 ≤ 200 мсБез кэша Verdict API не уложится в SLA. В MVP не нужен — потребитель один (UI), читает напрямую из PG

Архитектурные инварианты, отражённые на C2

Заголовок раздела «Архитектурные инварианты, отражённые на C2»
  • Verdict API не запускает краул. Только чтение кэша, при miss — fallback в PG. Никаких синхронных вызовов в Scanner/AI.
  • Двухступенчатый пайплайн. Crawler → Scanner (дешёвый детерминированный проход) → опционально AI Verifier (для Premium, по кандидатам). Не инвертировать.
  • Один владелец на тариф. Лимиты (частота, глубина, число сайтов) применяет Crawl Scheduler на инициации. Scanner и AI Verifier не валидируют тариф на каждой странице.
  • Immutable evidence. S3-снимки пишутся append-only и привязаны к scan_id — это юридическая защита перед регулятором / платёжной системой.

flowchart LR
  bus[[Message Bus<br/>page.ready]]
  pg[(SQL DB)]
  s3[(Object Storage)]
  cache[(Verdict Cache)]
  busOut[[Message Bus<br/>candidate.ready · Premium]]

  subgraph SE[Scanner Engine]
    ctrl[Consumer<br/>aiopika · ack only after persist]
    extr[Text Extractor<br/>trafilatura · отсечь nav/menu/footer · FR-07]
    norm[Normalizer<br/>casefold · NFC · diacritics · FR-08]
    deob[Anti-obfuscation Layer<br/>гомоглифы · разрядка · транслит · FR-09]
    morph[Morphology Engine<br/>pymorphy3 ru/be · lemma index]
    ac[Aho-Corasick Matcher<br/>per-tenant automaton · FR-08]
    agg[Candidate Aggregator<br/>группировка hits по странице + категория]
    evi[Evidence Writer<br/>HTML+screenshot ref · immutable · FR-12]
    persist[Verdict Updater<br/>upsert hits · пересчёт verdict · BR]
    invalidator[Cache Invalidator<br/>домен/URL keys]
    premium{{Premium?<br/>tenant.tier == premium}}
  end

  bus --> ctrl
  ctrl --> extr --> norm --> deob --> morph --> ac --> agg
  agg --> evi --> s3
  agg --> persist --> pg
  persist --> invalidator --> cache
  agg --> premium
  premium -->|yes · только кандидаты| busOut
  premium -->|no · стоп| persist
КомпонентНазначениеГраница ответственности
ConsumerЧтение page.ready из шины, ack только после записи в PG/S3Не знает деталей матчинга; гарантирует at-least-once
Text ExtractorИзвлекает значимый текст (статья, описание товара), отсекает nav/menu/footer (FR-07)Не делает нормализацию и не классифицирует
Normalizercasefold, NFC, удаление диакритики, нормализация пробеловЧистый функционал, без I/O
Anti-obfuscation LayerРазрядка букв («з а п р е щ»), гомоглифы (кириллица/латиница), транслит (FR-09). На Basic — базовая, на Premium — продвинутаяИзолирует «магию» — единственное место в Scanner, чувствительное к подписке
Morphology Enginepymorphy3 для ru/be: лемматизация текста и стоп-фраз (FR-08)Не лезет в матчинг; отдаёт лемматизированную форму
Aho-Corasick MatcherОдин автомат на тенанта, перестраивается при изменении стоп-листаТолько точные/морфо-совпадения; семантика — не сюда
Candidate AggregatorГруппирует hits по странице, добавляет категорию нарушенияРешает, что считается «кандидатом» для Premium-пайплайна
Evidence WriterСохраняет HTML-снимок и ссылку на скриншот в S3 (FR-12); пишет immutable recordОдин раз, без перезаписи
Verdict UpdaterUpsert hits в PG, пересчёт site.verdict по бизнес-правилу: «1+ подтверждённый prohibited → non-compliant»Учитывает FR-18 (ложные не считаются)
Cache InvalidatorСбрасывает ключи Verdict Cache по домену и URLЕдинственная запись в Redis из Scanner — иначе race с AI Verifier
Premium-gateРешает, отправлять ли кандидатов в AI Verifier по tenant.tierТариф проверяется здесь, не на каждой странице до этого
  • Анти-обфускация выделена отдельным слоем — её сложность (юникод-классы, гомоглифы) изолирована от матчера; матчер видит «чистый» нормализованный поток.
  • Aho-Corasick + морфология — это и есть тот «дешёвый первый проход», который выполняется для обоих тарифов. Все дорогие модели работают только по выходу Candidate Aggregator.
  • Evidence Writer перед Verdict Updater — порядок специально такой: сначала фиксируем доказательство (S3 immutable), потом отмечаем hit в PG. Это даёт юридическую гарантию: каждый зафиксированный hit имеет привязанное доказательство.
  • Cache Invalidator только в Scanner — даже если AI Verifier потом понизит уверенность hit’а, инвалидация уже сделана; Verdict API увидит обновлённое состояние при следующем чтении.

4. Sequence — «Платёжный шлюз спрашивает вердикт» (FR-15, FR-16) (фаза 3)

Заголовок раздела «4. Sequence — «Платёжный шлюз спрашивает вердикт» (FR-15, FR-16) (фаза 3)»

Hot path при оплате: SLA p95 ≤ 200 мс, не вызывает live-краул, читает только закэшированный вердикт. Не входит в MVP — сценарий зафиксирован для фиксации интеграционного контракта с PSP-шлюзом.

sequenceDiagram
  autonumber
  actor B as Покупатель
  participant GW as Платёжный шлюз
  participant VA as Verdict API
  participant RC as Verdict Cache (Redis)
  participant PG as SQL DB
  participant L as Audit log

  B->>GW: Pay (merchant_id, URL)
  GW->>VA: GET /v1/verdict?domain=…&url=… (JWT)
  VA->>VA: validate JWT, tenant scope
  VA->>RC: GET verdict:{tenant}:{url_hash}

  alt cache hit (горячий путь)
    RC-->>VA: {status, confidence, scanned_at}
    VA-->>GW: 200 OK · verdict
  else cache miss (cold)
    RC-->>VA: nil
    VA->>PG: SELECT verdict FROM site WHERE … (read replica)
    PG-->>VA: row | not_found
    alt found
      VA->>RC: SETEX verdict:… TTL=recrawl_interval
      VA-->>GW: 200 OK · verdict
    else not_found
      VA-->>GW: 200 OK · {status: "not_monitored"}
    end
  end

  Note over VA,L: Async — никогда в hot path
  VA-)L: append audit (tenant, url, decision, latency_ms)

  GW->>GW: if status=="warning" → show banner / require 2FA
  GW->>GW: if status=="block"   → reject transaction
  GW-->>B: UI: continue / warning / blocked

Связность C2/C3 ↔ Sequence (критерий приёмки):

ШагКонтейнер / компонент
1–2Платёжный шлюз → Verdict API (внешняя граница)
3Verdict API: JWT + tenant scope
4–5Verdict Cache (Redis) — горячий путь
6–9SQL DB read replica + populate cache (cold path)
10Audit log — async, не в hot path
11–13Решение шлюза по status (clean / warning / block)

После того как Scanner Engine нашёл кандидата, дальнейшая судьба зависит от тарифа:

  • Basic. Аggregator сразу зовёт Verdict Updater → hit фиксируется как prohibited.unverified. Вердикт сайта — бинарный.
  • Premium. Aggregator публикует candidate.ready в шину; AI Verifier (LLM + embeddings + vision) ставит confidence. Только после ответа Verifier’а пишется итоговый hit с confidence и категорией; Cache Invalidator срабатывает в обоих случаях.

Это прямое следствие инварианта «двухступенчатый пайплайн» из BRD: дорогие модели не гоняются по всем страницам.


Контракт фиксируется заранее (Contract First), чтобы при интеграции с банком-эквайером не пришлось перекраивать модель данных Scanner Engine. В фазе 1 (MVP) этот сервис не разворачивается. Полная спецификация — artifacts/verdict-openapi.yaml (TODO, при выходе на фазу 3). Ключевые решения:

openapi: 3.1.0
info:
title: Shield Verdict API
version: 1.0.0
description: |
Низколатентный вердикт комплаенса по домену/URL мерчанта для
встраивания в платёжный поток (FR-15, FR-16). SLA: p95 ≤ 200 мс.
servers:
- url: https://verdict.shield.example/v1
paths:
/verdict:
get:
operationId: getVerdict
summary: Получить вердикт по домену или конкретному URL
security: [{ bearerAuth: [] }]
parameters:
- { in: query, name: domain, schema: { type: string }, required: true,
description: "ASCII/IDN-домен мерчанта; нижний регистр" }
- { in: query, name: url, schema: { type: string, format: uri }, required: false,
description: "Конкретный URL страницы; если опущен — вердикт по домену" }
responses:
'200':
description: Вердикт получен (включая not_monitored — это валидный исход, не ошибка)
content:
application/json:
schema: { $ref: '#/components/schemas/Verdict' }
examples:
clean:
value:
status: clean
scanned_at: "2026-05-27T18:42:11Z"
tier: basic
warning_premium:
value:
status: warning
confidence: 0.83
categories: ["prohibited.tobacco"]
scanned_at: "2026-05-28T03:11:00Z"
tier: premium
evidence_ref: "s3://shield-evidence/2026/05/28/abc123/"
not_monitored:
value: { status: not_monitored }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/RateLimited' }
'503': { $ref: '#/components/responses/UpstreamUnavailable' }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
schemas:
Verdict:
type: object
required: [status]
properties:
status:
type: string
enum: [clean, warning, block, not_monitored, needs_attention]
confidence:
type: number
minimum: 0
maximum: 1
description: "Только для Premium-тенанта; для Basic вердикт бинарный (BR из раздела 5 BRD)."
categories:
type: array
items: { type: string }
scanned_at: { type: string, format: date-time }
tier: { type: string, enum: [basic, premium] }
evidence_ref:
type: string
description: "Ссылка на immutable доказательства (FR-12), только при status=warning|block."
responses:
Unauthorized: { description: JWT отсутствует/невалиден,
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } } }
RateLimited:
description: Лимит запросов по тенанту/тарифу превышен
headers:
Retry-After: { schema: { type: integer }, description: "Секунды до следующей попытки" }
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
UpstreamUnavailable:
description: Кэш и БД одновременно недоступны (Circuit Breaker open)
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
  • not_monitored — это 200 OK, не 404. Платёжный шлюз спрашивает «есть ли у тебя данные по этому мерчанту»; отсутствие — валидный бизнес-исход, обрабатывается тем же путём, что и clean. (Параллель с lesson-05: unrecognized интент тоже остался в 200, а не уходил в 422.) HTTP-статус описывает судьбу HTTP-обмена, а не бизнес-исход.
  • confidence только для Premium. Бизнес-правило BR из BRD: Basic — бинарный вердикт; Premium — с уверенностью. В контракте это выражено nullable-полем + комментарием.
  • evidence_ref указывается только при warning/block. Это прямая привязка к FR-12 (доказательства) и юридическая прослеживаемость.
  • 503 явный. Когда Redis и реплика PG разом недоступны — Circuit Breaker open, шлюз должен переключиться на свой fallback (например, всегда warning). Лучше явный 503, чем медленный таймаут на 200 мс SLA.
  • Retry-After в 429. Лимиты по тарифу: жадный клиент должен знать, когда повторять.
  • Версионирование URI (/v1/...). Verdict API — внешний контракт, ломающие изменения требуют новой версии в URL.
  • Security — Bearer JWT с tenant scope. Изоляция тенантов на уровне токена; Verdict API проверяет scope == domain.owner_tenant.

6. Соответствие критериям многоуровневого проектирования

Заголовок раздела «6. Соответствие критериям многоуровневого проектирования»
  • Соответствие нотации C4. C2 — контейнеры, C3 — компоненты внутри Scanner Engine; обе диаграммы Mermaid, связи направлены, на каждой стрелке указан транспорт.
  • Связность C3 ↔ Sequence. Hot path (Verdict API) использует Verdict Cache → SQL DB → audit. Scanner-пайплайн привязан к шагам Premium handoff. Каждый шаг sequence ссылается на свой компонент или контейнер.
  • Качество API. Спецификация содержит схему Verdict, три примера ответа (clean, warning_premium, not_monitored), коды ошибок 401/429/503 с RFC 7807 (application/problem+json), заголовок Retry-After, security-схему JWT.

  • TLS-mTLS со шлюзом vs JWT-only. При входе банка-эквайера может потребоваться mTLS поверх JWT — решается на этапе интеграции.
  • Webhook от Backend в шлюз для push-инвалидации кэша при срочном изменении вердикта — фаза 2.
  • OCR-конвейер. Картинки идут через Crawler → Scanner.ImageRefExtractor → AI Verifier.Vision. Описывается отдельно при выходе фазы 2.
  • Стратегия миграции стоп-листов при обновлении (rebuild автомата без даунтайма). Решается в HLD реализации Scanner.

8. Расширение продукта: домен InvestiGate + Onboarding

Заголовок раздела «8. Расширение продукта: домен InvestiGate + Onboarding»

Модули Onboarding и InvestiGate реализованы в admin (контроль-плейн), без отдельных сервисов и без изменения контракта со сканером (admin↔scanner — прежний Scanner API). Подробный план и состав — admin/ROADMAP-modules.md. Доменная модель (схемой владеет admin, миграции):

classDiagram
  Merchant "1" --> "*" Site : websites
  Merchant "1" --> "*" MerchantPerson : директора/UBO
  Merchant "1" --> "*" MerchantAddress
  Merchant "1" --> "*" MerchantDocument
  Merchant "1" --> "*" InvestigateCase : кейсы
  InvestigateCase "1" --> "*" CaseIndicator : 49 индикаторов
  InvestigateCase "1" --> "1" CremAssessment : CREM
  InvestigateCase "1" --> "*" CaseRecommendation : checklist
  Application ..> Merchant : convert()
  Application ..> InvestigateCase : convert()
  IndicatorDefinition ..> CaseIndicator : каталог (snapshot)
  MccReference ..> InvestigateCase : регуляторика по MCC

Поток: Application (заявка) →convert()Merchant (+структура) + InvestigateCase (на created: наполнение индикаторами каталога, авто-деривация контент-индикаторов из сканера/MCC, расчёт risk score, генерация recommendation checklist) → разметка офицером → решение Accept/Decline (проекция на Merchant.lifecycle_status) → PDF-отчёт.

Risk score — вычитательная модель (RiskScoreCalculator): старт 100%, вычеты по сработавшим индикаторам (red/amber) и за непроведённое ревью обязательных, пол 1%; уровень LOW/MEDIUM/HIGH. CREM (CremCalculator) — кредитная экспозиция (FEaD/EtPR/сценарии/залоги), чистые формулы. Разделение модулей в UI — nav-группами одной панели (не отдельные панели — см. ROADMAP «Упаковка»).

Каналы intake Application (FR-30/31): ручной ввод и CSV (UI), API ingest (POST /api/onboarding/applications, per-tenant Bearer tenants.ingest_token) и публичная форма (/onboarding/{form_token}, без создания юзера). Все → Application(source=…)convert().

Связка кейс ↔ мониторинг (FR-46): завершение скана сайта (Scanner API sites/{id}/scanned) у мерчанта с открытым кейсом → InvestigateCase::refreshFromScanner() (пересбор scanner-индикаторов

  • пересчёт score), не затирая ручную разметку; ежегодный rerun high-risk (investigate:rerun-due по расписанию); частота мониторинга сайта auto наследуется от риск-категории мерчанта.

Transaction Laundering (FR-50, частично): portfolio cross-check / corporate structure (PHP), whois + reverse-IP, SiteReveal, outgoing/backlinks и related-domains/редиректы (сканер → Scanner API), тест-карты (ручной ввод). Остаток — входные слова на внешних ресурсах, гео-клоакинг, load balancing.

RBAC (FR-01): роли super_admin/admin/officer/auditor (read-only) — spatie-пермишены (Filament Shield), BelongsToTenant-scope; админ может отключать пользователей (disabled_atcanAccessPanel=false).