переход на реальное api

This commit is contained in:
gorbenko.pavel
2026-07-24 12:27:42 +05:00
parent 2759c1ef7d
commit fa13e93190
24 changed files with 271 additions and 99 deletions
@@ -20,7 +20,7 @@ public static class InfrastructureServiceCollectionExtensions
services.Configure<TnsuPortalOptions>(configuration.GetSection("TnsuPortal"));
services.AddHttpClient<TnsuPortalTokenProvider>();
services.AddHttpClient<HttpTnsuPortalClient>((sp, client) =>
services.AddHttpClient<ITnsuPortalClient, HttpTnsuPortalClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<TnsuPortalOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
@@ -22,8 +22,10 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
{
e.HasKey(x => x.Id);
e.Property(x => x.RouteType).HasConversion<string>().HasMaxLength(32).IsRequired();
e.Property(x => x.Company).HasMaxLength(256).IsRequired();
e.Property(x => x.Department).HasMaxLength(256);
e.HasOne(x => x.RequestType).WithMany(x => x.Routes).HasForeignKey(x => x.RequestTypeId);
e.HasIndex(x => new { x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject }).IsUnique();
e.HasIndex(x => new { x.RequestTypeId, x.Department, x.Company, x.ByProject }).IsUnique();
});
modelBuilder.Entity<TemplateRouteStep>(e =>
@@ -12,7 +12,7 @@ using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(ApprovalRouteDbContext))]
[Migration("20260723074943_InitialCreate")]
[Migration("20260724071302_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
@@ -61,13 +61,16 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnType("boolean")
.HasColumnName("by_project");
b.Property<Guid>("CompanyId")
.HasColumnType("uuid")
.HasColumnName("company_id");
b.Property<string>("Company")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("company");
b.Property<Guid?>("DepartmentId")
.HasColumnType("uuid")
.HasColumnName("department_id");
b.Property<string>("Department")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("department");
b.Property<Guid>("RequestTypeId")
.HasColumnType("uuid")
@@ -86,9 +89,9 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
b.HasKey("Id")
.HasName("pk_template_routes");
b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject")
b.HasIndex("RequestTypeId", "Department", "Company", "ByProject")
.IsUnique()
.HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by");
.HasDatabaseName("ix_template_routes_request_type_id_department_company_by_proje");
b.ToTable("template_routes", (string)null);
});
@@ -31,8 +31,8 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
id = table.Column<Guid>(type: "uuid", nullable: false),
route_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
request_type_id = table.Column<Guid>(type: "uuid", nullable: false),
department_id = table.Column<Guid>(type: "uuid", nullable: true),
company_id = table.Column<Guid>(type: "uuid", nullable: false),
department = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
company = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
by_project = table.Column<bool>(type: "boolean", nullable: false),
requires_control = table.Column<bool>(type: "boolean", nullable: false)
},
@@ -78,9 +78,9 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
columns: new[] { "route_id", "order_no" });
migrationBuilder.CreateIndex(
name: "ix_template_routes_request_type_id_department_id_company_id_by",
name: "ix_template_routes_request_type_id_department_company_by_proje",
table: "template_routes",
columns: new[] { "request_type_id", "department_id", "company_id", "by_project" },
columns: new[] { "request_type_id", "department", "company", "by_project" },
unique: true);
}
@@ -58,13 +58,16 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnType("boolean")
.HasColumnName("by_project");
b.Property<Guid>("CompanyId")
.HasColumnType("uuid")
.HasColumnName("company_id");
b.Property<string>("Company")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("company");
b.Property<Guid?>("DepartmentId")
.HasColumnType("uuid")
.HasColumnName("department_id");
b.Property<string>("Department")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("department");
b.Property<Guid>("RequestTypeId")
.HasColumnType("uuid")
@@ -83,9 +86,9 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
b.HasKey("Id")
.HasName("pk_template_routes");
b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject")
b.HasIndex("RequestTypeId", "Department", "Company", "ByProject")
.IsUnique()
.HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by");
.HasDatabaseName("ix_template_routes_request_type_id_department_company_by_proje");
b.ToTable("template_routes", (string)null);
});
@@ -1,20 +1,74 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Tnsu.ApprovalRoute.Domain.Enums;
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
// Клиент api.tnsu.kz (api-tnsu-portal). GetEmployeeByEmailAsync и GetProjectExecutorsAsync — заглушки:
// на момент написания у api-tnsu-portal нет эндпоинта GET /Employee/ByEmail (есть только ByIIN/Manager),
// а форма ответа GET /Project/{oid} в его openapi-спеке не типизирована. Заменить на реальные вызовы,
// когда эндпоинты будут готовы — токен уже прокидывается через TnsuPortalTokenProvider.
public sealed class HttpTnsuPortalClient(HttpClient httpClient, TnsuPortalTokenProvider tokenProvider)
// Клиент api.tnsu.kz (api-tnsu-portal). Сервисный токен прокидывается через TnsuPortalTokenProvider.
public sealed class HttpTnsuPortalClient(HttpClient httpClient, TnsuPortalTokenProvider tokenProvider) : ITnsuPortalClient
{
public Task<EmployeeDirectoryInfo> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken)
public async Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken)
{
return Task.FromResult(new EmployeeDirectoryInfo(
DepartmentId: Guid.Parse("00000000-0000-0000-0000-000000000001"),
CompanyId: Guid.Parse("00000000-0000-0000-0000-000000000002")));
using var request = new HttpRequestMessage(HttpMethod.Get, $"/Employee/ByEmail?email={Uri.EscapeDataString(email)}");
await AuthorizeAsync(request, cancellationToken);
using var response = await httpClient.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
var employee = await response.Content.ReadFromJsonAsync<EmployeeResponse>(cancellationToken);
if (employee is null)
return null;
return new EmployeeDirectoryInfo(employee.Company ?? string.Empty, employee.Section ?? string.Empty);
}
public Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken)
public async Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<ProjectExecutorInfo>>([]);
using var request = new HttpRequestMessage(HttpMethod.Get, $"/Project/{projectId}");
await AuthorizeAsync(request, cancellationToken);
using var response = await httpClient.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
return [];
response.EnsureSuccessStatusCode();
var project = await response.Content.ReadFromJsonAsync<ProjectResponse>(cancellationToken);
if (project is null)
return [];
// Каждая роль проекта — email сотрудника в соответствующем поле (project_manager / planner / manager_hse / security).
var executors = new List<ProjectExecutorInfo>();
AddIfPresent(executors, ProjectRole.ProjectManager, project.ProjectManager);
AddIfPresent(executors, ProjectRole.Planner, project.Planner);
AddIfPresent(executors, ProjectRole.ManagerHSE, project.ManagerHSE);
AddIfPresent(executors, ProjectRole.Security, project.Security);
return executors;
}
private static void AddIfPresent(List<ProjectExecutorInfo> executors, ProjectRole role, string? email)
{
if (!string.IsNullOrWhiteSpace(email))
executors.Add(new ProjectExecutorInfo(role, email));
}
private async Task AuthorizeAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await tokenProvider.GetAccessTokenAsync(cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
private sealed record EmployeeResponse(
[property: JsonPropertyName("company")] string? Company,
[property: JsonPropertyName("section")] string? Section);
private sealed record ProjectResponse(
[property: JsonPropertyName("projectManager")] string? ProjectManager,
[property: JsonPropertyName("planner")] string? Planner,
[property: JsonPropertyName("managerHSE")] string? ManagerHSE,
[property: JsonPropertyName("security")] string? Security);
}
@@ -0,0 +1,8 @@
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
public interface ITnsuPortalClient
{
Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken);
Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken);
}
@@ -2,6 +2,6 @@ using Tnsu.ApprovalRoute.Domain.Enums;
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
public sealed record EmployeeDirectoryInfo(Guid DepartmentId, Guid CompanyId);
public sealed record EmployeeDirectoryInfo(string Company, string Department);
public sealed record ProjectExecutorInfo(string Email, string Position, ProjectRole ProjectRole);
public sealed record ProjectExecutorInfo(ProjectRole ProjectRole, string Email);