- .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>
6.2 KiB
6.2 KiB
Архитектура 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)
- Domain — сущности, value objects, события домена. Не зависит ни от чего.
- Application — use cases (MediatR handlers), DTO, интерфейсы (репозитории, внешние сервисы). Зависит только от Domain + Shared.
- Infrastructure — EF Core DbContext, репозитории, Identity, OpenIddict EF store, HTTP-клиенты к внешним сервисам (ОФД, SMS и т.д.). Зависит от Application + Domain.
- 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
- POS периодически (каждые N сек/мин + по событию) запрашивает у сервера delta:
GET /api/pos/sync?since={lastSyncTs}. - Сервер возвращает JSON с изменениями справочников (товары, цены, сотрудники, настройки) за период.
- POS применяет изменения в локальной SQLite БД в одной транзакции.
- Продажи, совершённые на POS, пушатся обратно:
POST /api/pos/salesс массивом документов. - Если связи нет — POS продолжает работать, копит изменения в локальной очереди, отправляет при восстановлении.
Conflict resolution
- Сервер — источник правды для справочников.
- POS — источник правды для продаж на своей кассе (каждая продажа имеет
posTerminalId+ локальныйexternalId, которые серверу гарантируют идемпотентность).
Фоновые задачи
Hangfire + PostgreSQL storage:
- Агрегация дневной выручки для dashboard.
- Очистка логов старше 90 дней.
- Ретрай отправки чеков в ОФД (когда появится интеграция).
- Еженедельная инвентаризация напоминаний.