- .NET 8 LTS solution with 7 projects (domain/application/infrastructure/api/shared/pos.core/pos[WPF]) - Central package management (Directory.Packages.props), .editorconfig, global.json pin to 8.0.417 - PostgreSQL 14 dev DB via existing brew service; food_market database created - ASP.NET Identity + OpenIddict 5 (password + refresh token flows) with ephemeral dev keys - EF Core 8 + Npgsql; multi-tenant query filter via reflection over ITenantEntity - Initial migration: 13 tables (Identity + OpenIddict + organizations) - AuthorizationController implements /connect/token; seeders create demo org + admin - Protected /api/me endpoint returns current user + org claims - React 19 + Vite 8 + Tailwind v4 SPA with TanStack Query, React Router 7 - Login flow with dev-admin placeholder, bearer interceptor + refresh token fallback - docs/architecture.md, CLAUDE.md, README.md Verified end-to-end: health check, password grant issues JWT with org_id, web app builds successfully (310 kB gzipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
6.2 KiB
Markdown
88 lines
6.2 KiB
Markdown
# Архитектура food-market
|
||
|
||
## Общая схема
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Internet / LAN магазина │
|
||
└─────────┬────────────────────────┬──────────────────────┘
|
||
│ │
|
||
│ HTTPS │ HTTPS (+ офлайн-буфер)
|
||
▼ ▼
|
||
┌───────────────────┐ ┌──────────────────────┐
|
||
│ web admin (React) │ │ Windows POS (WPF) │
|
||
│ — браузер │ │ — с локальной БД │
|
||
│ │ │ — работает с весами │
|
||
└─────────┬──────────┘ └──────────┬───────────┘
|
||
│ │
|
||
└─────────────┬─────────────┘
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ food-market.api │
|
||
│ ASP.NET Core + OpenIddict │
|
||
│ SignalR hubs для sync │
|
||
└──────────┬───────────────────┘
|
||
│
|
||
┌───────────┼──────────┐
|
||
▼ ▼ ▼
|
||
┌─────────┐ ┌──────────┐ ┌──────────┐
|
||
│Postgres │ │ Hangfire │ │ Logs │
|
||
│ 16 │ │ (jobs) │ │ Serilog │
|
||
└─────────┘ └──────────┘ └──────────┘
|
||
```
|
||
|
||
## Слои сервера (Clean Architecture)
|
||
|
||
1. **Domain** — сущности, value objects, события домена. Не зависит ни от чего.
|
||
2. **Application** — use cases (MediatR handlers), DTO, интерфейсы (репозитории, внешние сервисы). Зависит только от Domain + Shared.
|
||
3. **Infrastructure** — EF Core DbContext, репозитории, Identity, OpenIddict EF store, HTTP-клиенты к внешним сервисам (ОФД, SMS и т.д.). Зависит от Application + Domain.
|
||
4. **Api** — ASP.NET Core host: controllers, middleware, DI wiring, хостинг. Зависит от всех выше.
|
||
|
||
## Мультитенантность
|
||
|
||
### Модель данных
|
||
- `Organization` — корневая сущность тенанта. Сама НЕ tenant-scoped.
|
||
- Любая другая доменная сущность реализует `ITenantEntity` (поле `OrganizationId`).
|
||
- `User.OrganizationId` — к какой организации принадлежит пользователь.
|
||
|
||
### Изоляция
|
||
- При каждом запросе `HttpContextTenantContext` извлекает `org_id` из JWT.
|
||
- `AppDbContext.OnModelCreating` проходит по всем Entity Types и для каждого `ITenantEntity` добавляет `HasQueryFilter` через reflection.
|
||
- В итоге `_db.Products.ToListAsync()` вернёт только товары текущей организации.
|
||
- Роль `SuperAdmin` обходит фильтр (для техподдержки).
|
||
|
||
### Стамповка
|
||
- При `SaveChanges` если добавляется новая `ITenantEntity` и её `OrganizationId == Empty`, она автоматически получает `OrganizationId` из текущего tenant context.
|
||
- То же с timestamps: `CreatedAt` при add, `UpdatedAt` при modify.
|
||
|
||
## Аутентификация
|
||
|
||
- **OpenIddict 5** в роли OAuth2/OIDC сервера.
|
||
- **Password Flow** для первичного логина (логин/пароль → токены).
|
||
- **Refresh Token Flow** для продления сессии.
|
||
- В dev подписи/шифрование — ephemeral ключи; в prod — настоящие X.509 сертификаты.
|
||
- Access token содержит claims: `sub` (user id), `name`, `email`, `role`, `org_id`.
|
||
- Токен валидируется через `AddValidation().UseLocalServer()` (без дополнительного запроса к IdP).
|
||
|
||
## Синхронизация с POS
|
||
|
||
### Паттерн: Pull-sync + incremental
|
||
|
||
1. POS периодически (каждые N сек/мин + по событию) запрашивает у сервера delta: `GET /api/pos/sync?since={lastSyncTs}`.
|
||
2. Сервер возвращает JSON с изменениями справочников (товары, цены, сотрудники, настройки) за период.
|
||
3. POS применяет изменения в локальной SQLite БД в одной транзакции.
|
||
4. Продажи, совершённые на POS, пушатся обратно: `POST /api/pos/sales` с массивом документов.
|
||
5. Если связи нет — POS продолжает работать, копит изменения в локальной очереди, отправляет при восстановлении.
|
||
|
||
### Conflict resolution
|
||
- Сервер — источник правды для справочников.
|
||
- POS — источник правды для продаж на своей кассе (каждая продажа имеет `posTerminalId` + локальный `externalId`, которые серверу гарантируют идемпотентность).
|
||
|
||
## Фоновые задачи
|
||
|
||
Hangfire + PostgreSQL storage:
|
||
- Агрегация дневной выручки для dashboard.
|
||
- Очистка логов старше 90 дней.
|
||
- Ретрай отправки чеков в ОФД (когда появится интеграция).
|
||
- Еженедельная инвентаризация напоминаний.
|