Files
PnvPanel/docs/backend-conventions.md
T
Leonid Pershin 9925968e22
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 36s
Update LiteCqrs integration and documentation
- Replaced references to local project connections with the NuGet package for LiteCqrs in multiple documentation files, ensuring clarity on dependency management.
- Updated architecture and backend conventions documentation to reflect the current state of the LiteCqrs library as a NuGet package, enhancing consistency across the project.
- Improved descriptions of CQRS implementation and command/query handling in the tech stack documentation, providing clearer guidance for developers.
2026-07-24 04:18:03 +03:00

14 KiB
Raw Blame History

Backend Conventions

Структура решения

Solution-файл — PnvPanel.slnx (новый XML-формат dotnet CLI, не классический .sln).

backend/
  PnvPanel.slnx
  src/
    PnvPanel.Domain/
      Common/            # Entity (единственный базовый класс — без AggregateRoot/IDomainEvent)
      Activation/         # ActivationRequest, ActivationStatus
      Apps/               # ClientApp, OsPlatform
      News/                # NewsPost
      Audit/              # AuditLog, AuditSource
      Configs/            # VpnConfig, ConfigStatus, TrafficSample
      Inbounds/           # Inbound, VpnProtocol
      Nodes/              # Node, NodeCredentials (VO), NodeStatus
      Telegram/            # TelegramLinkToken, TelegramLoginRequest, TelegramLoginStatus
      Exceptions/         # DomainException
    PnvPanel.Application/
      Common/
        Behaviors/       # ValidationBehavior, LoggingBehavior, UnitOfWorkBehavior (нет Authorization-поведения)
        Interfaces/      # IAppDbContext, IXuiPanelGateway, ICurrentUser, IIdentityService,
                          # ISecretProtector, IRealtimeNotifier, ITelegramNotifier, IRoleService
        Messaging/       # ISender, ICommand<T>, IQuery<T>, ICommandHandler<,>, IQueryHandler<,>, IPipelineBehavior<,>
        Models/          # Result, Result<T>, Error, PagedList<T>, RoleQuota
      Activation/         # RequestActivationCommand, GetActivationStatusQuery (пользовательские)
      Admin/
        Activation/       # ListActivationRequestsQuery, Approve/RejectActivationCommand
        Apps/             # CRUD ClientApp
        News/              # CRUD NewsPost
        Audit/            # ListAuditLogsQuery
        Inbounds/         # ListInbounds, PublishInbound
        Nodes/            # RegisterNode, UpdateNode, DeleteNode, SyncNode, ProbeNode, ListNodes
        Roles/            # ListRoles, CreateRole, UpdateRole, DeleteRole
        Stats/             # GetStatsQuery
        Users/            # ListUsers, BlockUser/UnblockUser, ChangeUserRole, ResetUserPassword,
                          # ForceRevokeConfig, GetUserConfigs
      Apps/               # ListAppsQuery (по ОС, для юзера)
      News/               # ListNewsQuery (пагинировано, для юзера)
      Auth/
        ChangePassword/, DeleteMyAccount/, Login/, Logout/, Me/, Refresh/, Register/
      Configs/
        Create/, Edit/, Rotate/, Revoke/, GetMyConfigs/, GetConfigLink/, GetMySubscription/,
        ListAvailableInbounds/
      Subscriptions/       # SubscriptionDto, GetUserSubscriptionQuery, GetConfigSubscriptionQuery
      Telegram/            # LinkTelegramCommand, CreateLinkTokenCommand, CreateLoginRequestCommand,
                          # Approve/RejectTelegramLoginCommand, GetLoginRequestStatusQuery, ...
                          # Telegram/Bot/ — контракт для бота (не сам Telegram.Bot)
    PnvPanel.Infrastructure/
      Persistence/
        AppDbContext.cs   # : IdentityDbContext<AppUser, AppRole, Guid>, IAppDbContext
        Configurations/  # IEntityTypeConfiguration<T>
        Migrations/
      Identity/          # AppUser, AppRole, JwtTokenService, RefreshTokenService, RoleService,
                          # DbInitializer (сидинг), IdentityService, CurrentUser
      Xui/               # XuiPanelGateway (единственный файл — кэш клиентов per-node внутри него)
      BackgroundJobs/    # TrafficSyncService, NodeHealthCheckService, TrafficRetentionService
      Security/          # DataProtectionSecretProtector
      Telegram/          # TelegramNotifier, TelegramOptions
      DependencyInjection.cs   # AddInfrastructure(...)
    PnvPanel.Api/
      Endpoints/         # 14 файлов, см. backend-conventions.md ниже и api-design.md
      Hubs/              # PanelHub, SignalRRealtimeNotifier (реализация IRealtimeNotifier — здесь,
                          # не в Infrastructure, т.к. нужен IHubContext<PanelHub>)
      Telegram/          # TelegramBotHostedService, PnvBotUpdateHandler, TelegramNotifier
      Common/            # RateLimiting (константы политик), ResultExtensions (Result -> IResult)
      Program.cs         # DI composition root, pipeline (нет отдельных Middleware/Extensions папок)
      appsettings*.json
  tests/
    PnvPanel.Domain.Tests/          # 54 теста
    PnvPanel.Application.Tests/     # 71 тест
    PnvPanel.IntegrationTests/      # 9 тестов, Testcontainers.PostgreSql + WebApplicationFactory<Program>

Организация Application — по фичам (feature folders), внутри слоёв Clean Architecture.

Именование

  • Классы/методы/свойства — PascalCase; параметры/локальные — camelCase; приватные поля — _camelCase.
  • Команды — <Verb><Noun>Command (CreateVpnConfigCommand), запросы — <Get/List><Noun>Query.
  • Хендлеры — <Command/Query>Handler; валидаторы — <Command/Query>Validator.
  • DTO (Application-слой, возвращаются из Result<T>) — суффикс Dto (VpnConfigDto, NodeDto); конвертация из сущности — статический FromDomain(entity, ...) на самом DTO.
  • Тела запросов (Api-слой, только для JSON-полей, которых нет в готовой команде) — суффикс Body (CreateConfigBody, UpdateNodeBody) либо сам record команды биндится напрямую как тело (RegisterCommand, LoginCommand).
  • Тела ответов, которых нет как Application DTO (например, потому что Api-слой добавляет вычисляемое поле — абсолютный URL из токена) — суффикс ResponseDto (AuthResponseDto, ConfigLinkResponseDto, TelegramLoginStatusResponseDto), определяются прямо в файле эндпоинта.
  • Async-методы — суффикс Async, всегда принимают CancellationToken.
  • Один публичный тип на файл — с исключением: Api-слой держит вспомогательные Body/ResponseDto records в том же файле, что и класс эндпоинтов, который их использует (не выносятся отдельно).

Паттерны

  • Rich domain model: инварианты в сущностях (приватные сеттеры, фабричные методы Node.Register(...), поведенческие методы config.Revoke()), а не анемичные DTO-сущности.
  • CQRS через LiteCqrs.Net (собственная лёгкая CQRS-библиотека, не MediatR — NuGet-пакет): хендлеры реализуют ICommandHandler<TCommand,TResult> / IQueryHandler<,>; ISender резолвит их из DI и прогоняет через IPipelineBehavior<,> (LoggingBehaviorValidationBehaviorRequireActivationBehaviorUnitOfWorkBehavior, порядок задан в AddLiteCqrs(...)). Без доменных событий — хендлер сам вызывает нужные порты (realtime/Telegram/аудит) синхронно; библиотека умеет Notifications/pub-sub, но PnvPanel их не использует.
  • Порты в Application, адаптеры в Infrastructure/Api: никакого Npgsql/SignalR/ThreeXui.Net в Application/Domain — только интерфейсы, реализации могут жить и в Infrastructure, и в Api (IRealtimeNotifier реализован в Api/Hubs, т.к. завязан на IHubContext<PanelHub>).
  • IAppDbContext экспонирует DbSet<> и SaveChangesAsync; хендлеры пишут LINQ напрямую — выделенных репозиториев нет вообще.
  • Result-модель: команды/запросы возвращают Result/Result<T>; ResultExtensions.ToHttpResult() мапит Error.Type в HTTP-статус на границе Api.
  • Транзакция на команду: UnitOfWorkBehavior вызывает SaveChangesAsync после хендлера команды (не запросов) — отдельной BEGIN/COMMIT-транзакции вокруг этого нет, полагаемся на то, что один SaveChanges уже атомарен для одной единицы работы.
  • Компенсация при частичном сбое: если клиент успешно создан в 3x-ui, а SaveChanges в БД упал — хендлер вызывает RemoveClientAsync, чтобы не оставить сироту в панели.
  • Защита от гонок на квоте — pg_advisory_xact_lock, не оптимистичная блокировка: перед проверкой квоты роли CreateVpnConfigCommandHandler берёт pg_advisory_xact_lock(hashtext(userId)) — сериализует параллельные попытки создать конфиг одним и тем же пользователем в рамках транзакции. Отдельного rowversion/xmin на Node/VpnConfig нет (единственный IsConcurrencyToken в схеме — штатный ConcurrencyStamp таблиц Identity).

Работа с 3x-ui

  • Только через порт IXuiPanelGateway. Гейтвей принимает Node/NodeId и разруливает per-node клиента.
  • Пароли нод расшифровываются ISecretProtector внутри Infrastructure, никогда не покидают слой.
  • Сетевые ошибки/недоступность → Result.Failure/статус ноды, не «пробрасываем» наружу как 500.

Async / Cancellation

  • Всё I/O — асинхронно; пробрасывать CancellationToken до EF Core и HTTP-вызовов.
  • Не блокировать (.Result/.Wait()).

Ошибки и логирование

  • Единый application/problem+json для ошибок API (ResultExtensions.ToProblem); коды: 400 (валидация), 401/403 (auth/не активирован), 404, 409 (конфликт домена — квота, дубликат), 422 (прочее), 429 (rate limit), 500.
  • Serilog + UseSerilogRequestLogging(); секреты (пароли, JWT, BotToken) не логировать. Структурного обогащения UserId/NodeId/ConfigId/CorrelationId пока нет — см. tech-stack.md.

Тестирование

  • PnvPanel.Domain.Tests (54 теста) — инварианты и поведение сущностей, без моков.
  • PnvPanel.Application.Tests (71 тест) — хендлеры на EF Core InMemory + подменённые порты (NSubstitute), проверка веток Result. InMemory, а не Sqlite — модель использует Postgres-специфичные типы (uuid[], jsonb), которые Sqlite не поддерживает, а InMemory просто игнорирует.
  • PnvPanel.IntegrationTests (9 тестов) — реальный PostgreSQL (Testcontainers.PostgreSql) + WebApplicationFactory<Program>, сквозные сценарии через реальный HTTP-контракт, включая проверку pg_advisory_xact_lock под параллельной нагрузкой на квоту; 3x-ui подменён FakeXuiPanelGateway.
  • Именование тестов: Method_Scenario_ExpectedResult. Обычные Assert.* из xUnit — без FluentAssertions.

Конфигурация

  • appsettings.json + appsettings.{Environment}.json + env vars (перекрывают); в dev — .env через docker-compose's env_file, локально без Docker — переменные окружения напрямую (проект не подключает dotnet user-secretsUserSecretsId в .csproj нет).
  • Секреты (JWT-ключ, строка подключения, Telegram:BotToken, пароль сид-админа) — только через env/secret-store.
  • Строго типизированные IOptions<T> для секций конфига (JwtOptions, AdminSeedOptions, TelegramOptions, RolesOptions, ...); явной валидации на старте (ValidateOnStart/data annotations) нет — отсутствующий обязательный секрет обнаружится при первом обращении (например, AddInfrastructure бросит InvalidOperationException, если не задан ConnectionStrings:Default), не раньше.

Стиль и качество кода

  • .editorconfig, nullable reference types включены; сборка в CI идёт с -c Release и должна быть без предупреждений.
  • dotnet format — локальная команда разработчика (см. корневой CLAUDE.md), в CI не запускается; CI гоняет только dotnet build/dotnet test (backend) и pnpm lint/typecheck/build (frontend).
  • Комментарии — по необходимости (почему, а не что, — см. примеры в коде: причина pg_advisory_xact_lock, причина Secure = request.IsHttps); публичные контракты портов документируем XML-doc там, где это не очевидно из имени.