SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/ Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue, контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер), Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог). Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0, тесты 1340/130/52/38/9 зелёные.
29 KiB
Дейл (Deal) — Этап 0: Каркас решения Implementation Plan
Исторический документ этапа 0. Актуальное состояние —
docs/superpowers/STATUS.mdиdocs/technical/Техническая-документация-Дейл.md.
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Создать каркас нового продукта «Дейл»: структуру src/, решение core (модульный монолит) с пустыми модулями, стандарты кода (.editorconfig + анализаторы), dev-Postgres со схемой на тенанта и tenant-контекст.
Architecture: Модульный монолит в src/core (один процесс, одно sln: Deal.Api + Deal.Modules.* + Deal.SharedKernel + Deal.Infrastructure + Deal.Contracts). Postgres: одна БД, системные таблицы в public, данные тенантов в tenant_<id>.*. Сервисы ml/ai/telegram — отдельные процессы со своими sln (создаются в этом этапе как пустые каталоги, наполняются позже). Фронтенд Vue переезжает как есть в src/frontend.
Tech Stack: .NET 10 (C#), ASP.NET Core (Web API + minimal), EF Core, Npgsql, xUnit, docker compose.
Spec: docs/architecture/2026-09-05-deal-architecture-design.md (разделы 2, 3, 4, 10, 11)
ТЗ: docs/spec/ТЗ-дейл-новая-архитектура.md (разделы 3, 11)
Global Constraints
- Проект НЕ git-репозиторий (рабочее дерево
C:\telbase, деплой docker compose). Вместо коммитов фиксируем затронутые файлы и результат проверок в отчёте задачи. Рабочая папка плана:.superpowers/sdd/deal-scaffold/. - Решение собирается на .NET 10 SDK (установлен:
10.0.400). - Код-стайл:
C:\telbase\Стиль_кода.docx+ адаптации: 1 тип = 1 файл; комментарии на русском; XML-doc только для public-контрактов; настройки черезIOptions<T>; без snake_case-хелперов и регионов; public-члены — только свойства; явные модификаторы доступа. - Все имена: namespace
Deal.*, проектыDeal.*, имя решенияDeal.sln. - Каждый публичный тип — в отдельном файле, имя файла = имя типа.
- Анализаторы:
Microsoft.CodeAnalysis.NetAnalyzersвключён; нарушения стиля — ошибки сборки (через.editorconfigseverity). - Запрещено: секреты в коде/репозитории; конкатенация SQL; magic numbers.
- Старый LeadRadar-код (
backend/,frontend/верхнего уровня) не трогаем, кроме переносаfrontend/→src/frontend/.
Task 1: Структура src/ и перенос фронтенда
Files:
- Create:
src/README.md - Create:
README.md(корневой, краткий)
Interfaces:
-
Consumes: — (старт)
-
Produces: структура папок
src/{core, ml-service, ai-service, telegram-service, contracts, frontend}; фронтенд перенесён вsrc/frontend/. -
Step 1: Создать структуру каталогов
Run:
mkdir -p src/core src/ml-service src/ai-service src/telegram-service src/contracts
- Step 2: Перенести фронтенд
Run:
mkdir -p src/frontend
cp -r frontend/* src/frontend/ && rm -rf frontend
Expected: src/frontend/ содержит package.json, src/, index.html и т.д.; старая папка frontend/ удалена.
- Step 3: Создать
src/README.md
# Дейл (Deal) — исходники
- `core/` — модульный монолит .NET (бизнес-логика, API)
- `ml-service/` — ML (.NET + ONNX), отдельный процесс
- `ai-service/` — LLM-фасад, отдельный процесс
- `telegram-service/` — ферма сессий Telegram, отдельный процесс
- `contracts/` — общие .proto (gRPC)
- `frontend/` — Vue (переехал из LeadRadar как есть)
Подробности: `docs/architecture/2026-09-05-deal-architecture-design.md`
- Step 4: Создать корневой
README.md
# Дейл (Deal)
SaaS-мониторинг Telegram: реальные заказы и клиенты вместо рекламы и дубликатов.
- Архитектура: `docs/architecture/2026-09-05-deal-architecture-design.md`
- ТЗ: `docs/spec/ТЗ-дейл-новая-архитектура.md`
- Техдок: `docs/technical/Техническая-документация-Дейл.md`
- Исходники: `src/`
- Step 5: Проверить
Run: ls src/ — 6 папок; ls src/frontend/ — файлы Vue-проекта; test -f README.md && echo ok.
Expected: все проверки успешны.
- Step 6: Зафиксировать в отчёте
task-1-report.md(файлы, результат проверок).
Task 2: Стандарты кода — .editorconfig, Directory.Build.props
Files:
- Create:
.editorconfig - Create:
src/core/Directory.Build.props
Interfaces:
-
Produces: единые правила для всех проектов
src/core; нарушения — ошибки сборки. -
Step 1: Создать корневой
.editorconfig
root = true
[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.{cs,vb}]
indent_size = 4
# Стиль фигурных скобок — Allman (на отдельной строке)
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
# using — в начале файла
dotnet_sort_system_directives_first = true
# Модификаторы доступа — всегда явные
dotnet_style_require_accessibility_modifiers = always:error
# this. — не требуется
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
# Члены
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = false:silent
[*.cs]
# Отключить лишние правила IDE, которые конфликтуют с код-стайлом проекта
dotnet_diagnostic.IDE0290.severity = none
- Step 2: Создать
src/core/Directory.Build.props
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
- Step 3: Зафиксировать в отчёте (проверка сборки — после Task 3).
Task 3: Решение Deal.sln и пустые проекты core
Files:
- Create:
src/core/Deal.sln - Create:
src/core/Deal.Api/Deal.Api.csproj+Program.cs - Create:
src/core/Deal.Modules.Pipeline/,...Kanban/,...Projects/,...Discovery/,...Settings/,...Tenants/(csproj + класс-маркер) - Create:
src/core/Deal.SharedKernel/,Deal.Infrastructure/,Deal.Contracts/(csproj + маркер)
Interfaces:
-
Produces: собираемое решение; проекты-модули, готовые к наполнению в следующих этапах.
-
Step 1: Создать решение и проекты командой
cd /c/telbase/src/core
dotnet new sln -n Deal
dotnet new web -n Deal.Api -o Deal.Api --no-https
dotnet new classlib -n Deal.Modules.Pipeline -o Deal.Modules.Pipeline
dotnet new classlib -n Deal.Modules.Kanban -o Deal.Modules.Kanban
dotnet new classlib -n Deal.Modules.Projects -o Deal.Modules.Projects
dotnet new classlib -n Deal.Modules.Discovery -o Deal.Modules.Discovery
dotnet new classlib -n Deal.Modules.Settings -o Deal.Modules.Settings
dotnet new classlib -n Deal.Modules.Tenants -o Deal.Modules.Tenants
dotnet new classlib -n Deal.SharedKernel -o Deal.SharedKernel
dotnet new classlib -n Deal.Infrastructure -o Deal.Infrastructure
dotnet new classlib -n Deal.Contracts -o Deal.Contracts
- Step 2: Добавить проекты в решение
dotnet sln Deal.sln add Deal.Api Deal.Modules.Pipeline Deal.Modules.Kanban Deal.Modules.Projects Deal.Modules.Discovery Deal.Modules.Settings Deal.Modules.Tenants Deal.SharedKernel Deal.Infrastructure Deal.Contracts
- Step 3: Удалить Class1.cs и добавить маркеры модулей
Каждый модуль получает публичный маркер-класс (1 тип = 1 файл), например Deal.Modules.Pipeline/PipelineModuleMarker.cs:
namespace Deal.Modules.Pipeline;
/// <summary>Маркер модуля Pipeline: используется для DI-сканирования и тестов.</summary>
public sealed class PipelineModuleMarker
{
}
Аналогично для всех модулей и Infrastructure/SharedKernel/Contracts (маркеры: InfrastructureMarker, SharedKernelMarker, ContractsMarker).
- Step 4: Ссылки между проектами (минимальные, по дизайн-доку)
dotnet add Deal.Api reference Deal.SharedKernel Deal.Contracts Deal.Infrastructure
dotnet add Deal.Modules.Pipeline reference Deal.SharedKernel Deal.Contracts
dotnet add Deal.Modules.Kanban reference Deal.SharedKernel Deal.Contracts
dotnet add Deal.Modules.Projects reference Deal.SharedKernel Deal.Contracts
dotnet add Deal.Modules.Discovery reference Deal.SharedKernel Deal.Contracts
dotnet add Deal.Modules.Settings reference Deal.SharedKernel Deal.Contracts
dotnet add Deal.Modules.Tenants reference Deal.SharedKernel Deal.Contracts
dotnet add Deal.Infrastructure reference Deal.SharedKernel Deal.Contracts
- Step 5: Минимальный Program.cs в Deal.Api (health)
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/api/health", () => Results.Ok(new { ok = true, service = "deal" }));
app.Run();
public partial class Program
{
}
- Step 6: Собрать решение
Run: dotnet build Deal.sln
Expected: Build succeeded, 0 warnings, 0 errors.
- Step 7: Проверить health локально
Run: dotnet run --project Deal.Api --urls http://localhost:5080 (в фоне), затем curl http://localhost:5080/api/health
Expected: {"ok":true,"service":"deal"} (процесс остановить после проверки).
- Step 8: Зафиксировать в отчёте
task-3-report.md.
Task 4: Тесты — xUnit-каркас
Files:
- Create:
src/core/tests/Deal.Tests.Unit/Deal.Tests.Unit.csproj - Test:
src/core/tests/Deal.Tests.Unit/MarkerTests.cs
Interfaces:
-
Consumes: маркеры модулей из Task 3.
-
Produces: тестовый проект, подключённый к решению.
-
Step 1: Создать тестовый проект
cd /c/telbase/src/core
dotnet new xunit -n Deal.Tests.Unit -o tests/Deal.Tests.Unit
dotnet sln Deal.sln add tests/Deal.Tests.Unit
dotnet add tests/Deal.Tests.Unit reference Deal.SharedKernel Deal.Modules.Pipeline Deal.Modules.Kanban Deal.Modules.Projects Deal.Modules.Discovery Deal.Modules.Settings Deal.Modules.Tenants
- Step 2: Написать тест на маркеры модулей
tests/Deal.Tests.Unit/MarkerTests.cs:
using Deal.Modules.Pipeline;
namespace Deal.Tests.Unit;
public sealed class MarkerTests
{
[Fact]
public void PipelineModuleMarker_IsPublicAndSealed()
{
Assert.True(typeof(PipelineModuleMarker).IsPublic);
Assert.True(typeof(PipelineModuleMarker).IsSealed);
}
}
- Step 3: Запустить тесты
Run: dotnet test tests/Deal.Tests.Unit
Expected: 1 тест PASS.
- Step 4: Зафиксировать в отчёте
task-4-report.md.
Task 5: Dev-Postgres в docker compose (схема на тенанта)
Files:
- Create:
deploy/compose.dev.yml - Create:
deploy/.env.example - Modify:
README.md(инструкция запуска dev-БД)
Interfaces:
-
Produces: dev-контейнер Postgres 16; БД
deal; схемаpublicготова к миграциям. -
Step 1: Создать
deploy/compose.dev.yml
services:
postgres:
image: postgres:16-alpine
container_name: deal-postgres
environment:
POSTGRES_DB: deal
POSTGRES_USER: deal
POSTGRES_PASSWORD: deal_dev_password
ports:
- "5433:5432" # 5432 может быть занят LeadRadar-стеком
volumes:
- deal_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U deal -d deal"]
interval: 5s
timeout: 3s
retries: 10
volumes:
deal_pgdata:
- Step 2: Создать
deploy/.env.example
DEAL_PG_HOST=localhost
DEAL_PG_PORT=5433
DEAL_PG_DB=deal
DEAL_PG_USER=deal
DEAL_PG_PASSWORD=deal_dev_password
- Step 3: Поднять контейнер
Run: docker compose -f deploy/compose.dev.yml up -d
Expected: deal-postgres running, healthy.
- Step 4: Проверить подключение
Run:
docker exec deal-postgres psql -U deal -d deal -c "SELECT current_database(), current_schema();"
Expected: deal | public
- Step 5: Дополнить README.md разделом «Запуск dev-окружения»
## Запуск dev-окружения
Postgres (схема на тенанта): `docker compose -f deploy/compose.dev.yml up -d`
- Step 6: Зафиксировать в отчёте
task-5-report.md.
Task 6: Tenant-контекст и подключение к Postgres
Files:
- Create:
src/core/Deal.SharedKernel/Tenants/TenantId.cs - Create:
src/core/Deal.SharedKernel/Tenants/ITenantContext.cs - Create:
src/core/Deal.Infrastructure/Data/TenantContext.cs - Create:
src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs - Modify:
Deal.Api/Program.cs - Test:
tests/Deal.Tests.Unit/TenantIdTests.cs
Interfaces:
-
Produces:
TenantId— readonly record struct, обёртка над строкой.ITenantContext—TenantId? TenantId { get; },bool HasTenant { get; },string? SchemaName { get; }.TenantContext— реализация на AsyncLocal.ConnectionStringProvider— строка подключения сsearch_path.
-
Step 1:
TenantId.cs(1 тип = 1 файл)
namespace Deal.SharedKernel.Tenants;
/// <summary>Идентификатор тенанта. Инвариант: непустой.</summary>
public readonly record struct TenantId(string Value)
{
public string Value { get; } = string.IsNullOrWhiteSpace(Value)
? throw new ArgumentException("TenantId не может быть пустым", nameof(Value))
: Value;
/// <summary>Имя схемы Postgres для тенанта.</summary>
public string SchemaName => $"tenant_{Value}";
}
- Step 2: Тест
TenantIdTests.cs
using Deal.SharedKernel.Tenants;
namespace Deal.Tests.Unit;
public sealed class TenantIdTests
{
[Fact]
public void SchemaName_PrefixesTenant()
{
var id = new TenantId("abc123");
Assert.Equal("tenant_abc123", id.SchemaName);
}
[Fact]
public void TenantId_Empty_Throws()
{
Assert.Throws<ArgumentException>(() => new TenantId(""));
}
}
- Step 3: Запустить тесты
Run: dotnet test tests/Deal.Tests.Unit
Expected: 3 теста PASS.
- Step 4:
ITenantContext.cs
namespace Deal.SharedKernel.Tenants;
/// <summary>Контекст текущего тенанта запроса.</summary>
public interface ITenantContext
{
TenantId? TenantId { get; }
bool HasTenant { get; }
/// <summary>Имя схемы текущего тенанта или null для системного контекста (public).</summary>
string? SchemaName { get; }
}
- Step 5:
TenantContext.cs(реализация в Infrastructure)
using Deal.SharedKernel.Tenants;
namespace Deal.Infrastructure.Data;
/// <summary>Контекст тенанта на AsyncLocal: пробрасывается через весь запрос.</summary>
public sealed class TenantContext : ITenantContext
{
private static readonly AsyncLocal<TenantId?> Current = new();
public TenantId? TenantId => Current.Value;
public bool HasTenant => Current.Value is not null;
public string? SchemaName => Current.Value?.SchemaName;
public void SetTenant(TenantId tenantId) => Current.Value = tenantId;
}
- Step 6:
ConnectionStringProvider.cs
using Deal.SharedKernel.Tenants;
using Microsoft.Extensions.Configuration;
namespace Deal.Infrastructure.Data;
/// <summary>Строит строку подключения к Postgres с учётом схемы тенанта.</summary>
public sealed class ConnectionStringProvider
{
private readonly string _baseConnectionString;
public ConnectionStringProvider(IConfiguration configuration)
{
_baseConnectionString = configuration.GetConnectionString("DealPostgres")
?? throw new InvalidOperationException("ConnectionStrings:DealPostgres не задан");
}
/// <summary>Строка подключения; при tenantId не null добавляет search_path к схеме тенанта.</summary>
public string ForTenant(TenantId? tenantId)
{
if (tenantId is null)
{
return _baseConnectionString;
}
return $"{_baseConnectionString};Search Path={tenantId.Value.SchemaName}";
}
}
- Step 7: Подключить в
Program.cs(DI)
using Deal.Infrastructure.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ITenantContext, TenantContext>();
builder.Services.AddSingleton<ConnectionStringProvider>();
var app = builder.Build();
(недостающие using Deal.SharedKernel.Tenants; добавить по месту)
- Step 8: Собрать и прогнать тесты
Run: dotnet build Deal.sln && dotnet test tests/Deal.Tests.Unit
Expected: build 0 ошибок, тесты PASS.
- Step 9: Зафиксировать в отчёте
task-6-report.md.
Task 7: EF Core + миграции (public)
Files:
- Create:
src/core/Deal.Infrastructure/Persistence/DealDbContext.cs - Create:
src/core/Deal.Infrastructure/Persistence/Entities/TenantEntity.cs - Create:
src/core/Deal.Infrastructure/Persistence/DealDbDesignTimeFactory.cs - Modify:
Deal.Api/Program.cs(регистрация DbContext) - Test:
tests/Deal.Tests.Unit/TenantEntityTests.cs
Interfaces:
-
Produces:
DealDbContext— базовый DbContext; системная сущность Tenant в схемеpublic.- Миграция
InitialPublic, применённая кpublic.
-
Step 1: Добавить EF Core пакеты в Infrastructure
cd /c/telbase/src/core
dotnet add Deal.Infrastructure package Microsoft.EntityFrameworkCore
dotnet add Deal.Infrastructure package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add Deal.Infrastructure package Microsoft.EntityFrameworkCore.Design
- Step 2:
TenantEntity.cs(вDeal.Infrastructure/Persistence/Entities/)
namespace Deal.Infrastructure.Persistence.Entities;
/// <summary>Тенант в системной схеме public.</summary>
public sealed class TenantEntity
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Status { get; set; } = "active";
public DateTimeOffset CreatedAt { get; set; }
}
- Step 3:
DealDbContext.cs
using Deal.Infrastructure.Persistence.Entities;
using Microsoft.EntityFrameworkCore;
namespace Deal.Infrastructure.Persistence;
/// <summary>Базовый DbContext. Системные сущности — в схеме public.</summary>
public sealed class DealDbContext(DbContextOptions<DealDbContext> options) : DbContext(options)
{
public DbSet<TenantEntity> Tenants => Set<TenantEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TenantEntity>(entity =>
{
entity.ToTable("tenants", "public");
entity.HasKey(x => x.Id);
entity.Property(x => x.Name).HasMaxLength(200).IsRequired();
});
}
}
- Step 4:
DealDbDesignTimeFactory.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Deal.Infrastructure.Persistence;
/// <summary>Фабрика для dotnet-ef (миграции). Читает строку подключения из env.</summary>
public sealed class DealDbDesignTimeFactory : IDesignTimeDbContextFactory<DealDbContext>
{
public DealDbContext CreateDbContext(string[] args)
{
var connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
var options = new DbContextOptionsBuilder<DealDbContext>()
.UseNpgsql(connectionString)
.Options;
return new DealDbContext(options);
}
}
- Step 5: Регистрация DbContext в Program.cs
using Deal.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
var connectionString = builder.Configuration.GetConnectionString("DealPostgres")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
builder.Services.AddDbContext<DealDbContext>(options => options.UseNpgsql(connectionString));
(в appsettings.Development.json положить ConnectionStrings:DealPostgres; в проде — из env)
- Step 6: Создать
appsettings.Development.jsonв Deal.Api
{
"ConnectionStrings": {
"DealPostgres": "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password"
}
}
- Step 7: Установить dotnet-ef tool и создать миграцию
dotnet tool install --global dotnet-ef
cd /c/telbase/src/core
dotnet ef migrations add InitialPublic --project Deal.Infrastructure --startup-project Deal.Api
- Step 8: Применить миграцию к public
dotnet ef database update --project Deal.Infrastructure --startup-project Deal.Api
- Step 9: Проверить таблицу
docker exec deal-postgres psql -U deal -d deal -c "\dt public.*"
Expected: таблицы tenants, __EFMigrationsHistory.
- Step 10: Тест
TenantEntityTests.cs
using Deal.Infrastructure.Persistence.Entities;
namespace Deal.Tests.Unit;
public sealed class TenantEntityTests
{
[Fact]
public void TenantEntity_Defaults_AreValid()
{
var entity = new TenantEntity();
Assert.Equal("active", entity.Status);
Assert.NotEqual(Guid.Empty, entity.Id == Guid.Empty ? Guid.Empty : entity.Id);
}
}
(тест проверяет дефолты; при необходимости скорректировать под реальную модель)
- Step 11: Зафиксировать в отчёте
task-7-report.md.
Task 8: Применение миграций ко всем схемам тенантов
Files:
- Create:
src/core/Deal.Infrastructure/Migrations/TenantSchemaMigrator.cs - Test:
tests/Deal.Tests.Unit/TenantSchemaMigratorTests.cs
Interfaces:
-
Consumes:
TenantId. -
Produces:
TenantSchemaMigrator— чистые функции формирования SQL для схем тенантов. -
Step 1: Написать тест
tests/Deal.Tests.Unit/TenantSchemaMigratorTests.cs:
using Deal.Infrastructure.Migrations;
namespace Deal.Tests.Unit;
public sealed class TenantSchemaMigratorTests
{
[Fact]
public void CreateSchemaSql_IsEscaped()
{
var sql = TenantSchemaMigrator.CreateSchemaSql("tenant_abc");
Assert.Contains("CREATE SCHEMA IF NOT EXISTS \"tenant_abc\"", sql);
Assert.DoesNotContain("; DROP", sql);
}
[Fact]
public void CreateSchemaSql_EscapesQuotes()
{
var sql = TenantSchemaMigrator.CreateSchemaSql("tenant_a\"b");
Assert.DoesNotContain("\"b\"", sql);
}
}
- Step 2:
TenantSchemaMigrator.cs
namespace Deal.Infrastructure.Migrations;
/// <summary>Миграции схем тенантов. Чистые функции формирования SQL.</summary>
public static class TenantSchemaMigrator
{
/// <summary>SQL создания схемы тенанта. Имя экранируется (не интерполируется из ввода).</summary>
public static string CreateSchemaSql(string schemaName)
{
var escaped = schemaName.Replace("\"", "\"\"");
return $"CREATE SCHEMA IF NOT EXISTS \"{escaped}\"";
}
/// <summary>Имена схем тенантов из БД.</summary>
public static string ListTenantSchemasSql() =>
"SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'tenant\\_%' ESCAPE '\\'";
}
- Step 3: Запустить тесты
Run: dotnet test tests/Deal.Tests.Unit
Expected: PASS.
- Step 4: Зафиксировать в отчёте
task-8-report.md.
Task 9: CI-скрипты и финальная проверка этапа
Files:
- Create:
scripts/build.sh - Create:
scripts/test.sh
Interfaces:
-
Produces: воспроизводимая сборка и тесты одной командой.
-
Step 1:
scripts/build.sh
#!/usr/bin/env sh
set -e
cd "$(dirname "$0")/../src/core"
dotnet build Deal.sln
- Step 2:
scripts/test.sh
#!/usr/bin/env sh
set -e
cd "$(dirname "$0")/../src/core"
dotnet test tests/Deal.Tests.Unit
- Step 3: Прогнать оба скрипта
Run: sh scripts/build.sh && sh scripts/test.sh
Expected: build succeeded, все тесты PASS.
- Step 4: Итоговая проверка этапа
Run:
-
dotnet build Deal.sln— 0 ошибок, 0 предупреждений; -
dotnet test tests/Deal.Tests.Unit— все PASS; -
docker ps—deal-postgreshealthy; -
curl http://localhost:5080/api/health—{"ok":true,"service":"deal"}. -
Step 5: Зафиксировать в отчёте
task-9-report.md+ обновитьprogress.md.
Self-Review
1. Spec coverage (дизайн-док):
- §2 (стратегия/структура) → Task 1, 3.
- §3 (структура src/) → Task 1, 3.
- §4 (мультитенантность: схема на тенанта, search_path) → Task 5, 6, 7, 8.
- §10 (деплой compose) → Task 5.
- §11 (стандарты: editorconfig, анализаторы, 1 тип = 1 файл) → Task 2, все задачи.
- Frontend-перенос → Task 1.
- Сервисы ml/ai/telegram — пустые каталоги (Task 1); их sln создаются в следующих этапах (вне scope этапа 0).
- Auth/инвайты/лимиты — следующие этапы (вне scope «каркаса»).
2. Placeholder scan: код во всех шагах конкретный. Task 7 Step 10 — тест на дефолты TenantEntity упрощён, с пометкой скорректировать под реальную модель.
3. Type consistency: TenantId, ITenantContext, TenantContext, ConnectionStringProvider, TenantSchemaMigrator, DealDbContext, TenantEntity — имена и сигнатуры согласованы между задачами 6–8.
Вне scope этапа 0: auth/сессии, модули с бизнес-логикой, gRPC-сервисы, .proto, админка, observability, безопасность сервисов, лимиты — отдельные планы следующих этапов.