forked from ddrilling/AsbCloudServer
Сервисы для уведомлений
1. Добавил репозиторий для уведомлений 2. Добавил сервисы для уведомлений
This commit is contained in:
parent
399a8a6c59
commit
96786b1be7
39
AsbCloudApp/Repositories/INotificationRepository.cs
Normal file
39
AsbCloudApp/Repositories/INotificationRepository.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
|
||||||
|
namespace AsbCloudApp.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Репозиторий для уведомлений
|
||||||
|
/// </summary>
|
||||||
|
public interface INotificationRepository : ICrudRepository<NotificationDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Метод для получения не отправленных уведомлений
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idUser"></param>
|
||||||
|
/// <param name="idNotificationTransport"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IEnumerable<NotificationDto>> GetUnsentNotificationsAsync(int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод получения уведомлений по параметрам
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="skip"></param>
|
||||||
|
/// <param name="take"></param>
|
||||||
|
/// <param name="idUser"></param>
|
||||||
|
/// <param name="idNotificationTransport"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int? skip,
|
||||||
|
int? take,
|
||||||
|
int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
41
AsbCloudApp/Services/INotificationSendingQueueService.cs
Normal file
41
AsbCloudApp/Services/INotificationSendingQueueService.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using AsbCloudApp.Data;
|
||||||
|
|
||||||
|
namespace AsbCloudApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис для добавление уведомлений в очередь
|
||||||
|
/// </summary>
|
||||||
|
public interface INotificationSendingQueueService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Флаг для проверки пустая ли коллекция
|
||||||
|
/// </summary>
|
||||||
|
bool IsEmpty { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавление одного уведомления в очередь
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notificationDto"></param>
|
||||||
|
void Enqueue(NotificationDto notificationDto);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавление нескольких уведомлений в очередь
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notifications"></param>
|
||||||
|
void EnqueueRange(IEnumerable<NotificationDto> notifications);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Извлечение элемента из очереди и его удаление
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notification"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool TryDequeue(out NotificationDto notification);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод ожидания нового уведомления
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
void Wait(CancellationToken cancellationToken);
|
||||||
|
}
|
52
AsbCloudApp/Services/INotificationService.cs
Normal file
52
AsbCloudApp/Services/INotificationService.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AsbCloudApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Интерфейс для работы с уведомлениями
|
||||||
|
/// </summary>
|
||||||
|
public interface INotificationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Метод отправки нового уведомления
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idUser"></param>
|
||||||
|
/// <param name="idNotificationTransport"></param>
|
||||||
|
/// <param name="idNotificationCategory"></param>
|
||||||
|
/// <param name="title"></param>
|
||||||
|
/// <param name="subject"></param>
|
||||||
|
/// <param name="timeToLife"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task SendNotificationAsync(int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
int idNotificationCategory,
|
||||||
|
string title,
|
||||||
|
string subject,
|
||||||
|
TimeSpan timeToLife,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод обновления уведомления
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idNotification"></param>
|
||||||
|
/// <param name="isRead"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UpdateNotificationAsync(int idNotification,
|
||||||
|
bool isRead,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод отправки уведомлений, которые не были отправлены
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idUser"></param>
|
||||||
|
/// <param name="idNotificationTransport"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task ResendNotificationAsync(int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
@ -143,6 +143,10 @@ namespace AsbCloudInfrastructure
|
|||||||
|
|
||||||
services.AddTransient<IGtrRepository, GtrWitsRepository>();
|
services.AddTransient<IGtrRepository, GtrWitsRepository>();
|
||||||
|
|
||||||
|
services.AddTransient<INotificationService, NotificationService>();
|
||||||
|
services.AddTransient<INotificationRepository, NotificationRepository>();
|
||||||
|
services.AddSingleton<INotificationSendingQueueService, NotificationSendingQueueService>();
|
||||||
|
|
||||||
// admin crud services:
|
// admin crud services:
|
||||||
services.AddTransient<ICrudRepository<TelemetryDto>, CrudCacheRepositoryBase<TelemetryDto, Telemetry>>(s =>
|
services.AddTransient<ICrudRepository<TelemetryDto>, CrudCacheRepositoryBase<TelemetryDto, Telemetry>>(s =>
|
||||||
new CrudCacheRepositoryBase<TelemetryDto, Telemetry>(
|
new CrudCacheRepositoryBase<TelemetryDto, Telemetry>(
|
||||||
|
75
AsbCloudInfrastructure/Repository/NotificationRepository.cs
Normal file
75
AsbCloudInfrastructure/Repository/NotificationRepository.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
|
using AsbCloudDb;
|
||||||
|
using AsbCloudDb.Model;
|
||||||
|
using Mapster;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AsbCloudInfrastructure.Repository;
|
||||||
|
|
||||||
|
public class NotificationRepository : CrudRepositoryBase<NotificationDto, Notification>, INotificationRepository
|
||||||
|
{
|
||||||
|
public NotificationRepository(IAsbCloudDbContext context) : base(context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationRepository(IAsbCloudDbContext context,
|
||||||
|
Func<DbSet<Notification>, IQueryable<Notification>> makeQuery) : base(context, makeQuery)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<NotificationDto>> GetUnsentNotificationsAsync(int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var notifications = await dbContext.Notifications
|
||||||
|
.Where(x => x.IdUser == idUser &&
|
||||||
|
x.IdNotificationTransport == idNotificationTransport &&
|
||||||
|
x.SentDateAtUtc == null)
|
||||||
|
.Include(x => x.NotificationTransport)
|
||||||
|
.Include(x => x.NotificationCategory)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return notifications.Select(x => x.Adapt<NotificationDto>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int? skip,
|
||||||
|
int? take,
|
||||||
|
int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
skip ??= 0;
|
||||||
|
take ??= 10;
|
||||||
|
|
||||||
|
var query = dbContext.Notifications
|
||||||
|
.Where(x => x.IdNotificationTransport == idNotificationTransport &&
|
||||||
|
x.IdUser == idUser &&
|
||||||
|
x.SentDateAtUtc != null);
|
||||||
|
|
||||||
|
var result = new PaginationContainer<NotificationDto>()
|
||||||
|
{
|
||||||
|
Skip = skip.Value,
|
||||||
|
Take = take.Value,
|
||||||
|
Count = await query.CountAsync(cancellationToken),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.Count == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
result.Items = await query
|
||||||
|
.OrderBy(x => x.SentDateAtUtc)
|
||||||
|
.SkipTake(skip, take)
|
||||||
|
.Include(x => x.NotificationCategory)
|
||||||
|
.Include(x => x.NotificationTransport)
|
||||||
|
.Select(x => x.Adapt<NotificationDto>())
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
|
||||||
|
namespace AsbCloudInfrastructure.Services;
|
||||||
|
|
||||||
|
public class NotificationSendingQueueService : INotificationSendingQueueService
|
||||||
|
{
|
||||||
|
private readonly ManualResetEventSlim manualResetEventSlim = new();
|
||||||
|
private readonly ConcurrentQueue<NotificationDto> notificationsQueue = new();
|
||||||
|
|
||||||
|
public bool IsEmpty => notificationsQueue.IsEmpty;
|
||||||
|
|
||||||
|
public void Enqueue(NotificationDto notification)
|
||||||
|
{
|
||||||
|
notificationsQueue.Enqueue(notification);
|
||||||
|
manualResetEventSlim.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueRange(IEnumerable<NotificationDto> notifications)
|
||||||
|
{
|
||||||
|
foreach (var notification in notifications)
|
||||||
|
{
|
||||||
|
notificationsQueue.Enqueue(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
manualResetEventSlim.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryDequeue(out NotificationDto notification)
|
||||||
|
{
|
||||||
|
return notificationsQueue.TryDequeue(out notification!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Wait(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
manualResetEventSlim.Wait(cancellationToken);
|
||||||
|
manualResetEventSlim.Reset();
|
||||||
|
}
|
||||||
|
}
|
71
AsbCloudInfrastructure/Services/NotificationService.cs
Normal file
71
AsbCloudInfrastructure/Services/NotificationService.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Exceptions;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
|
||||||
|
namespace AsbCloudInfrastructure.Services;
|
||||||
|
|
||||||
|
public class NotificationService : INotificationService
|
||||||
|
{
|
||||||
|
private readonly INotificationSendingQueueService notificationSendingQueueService;
|
||||||
|
private readonly INotificationRepository notificationRepository;
|
||||||
|
|
||||||
|
public NotificationService(INotificationSendingQueueService notificationSendingQueueService,
|
||||||
|
INotificationRepository notificationRepository)
|
||||||
|
{
|
||||||
|
this.notificationSendingQueueService = notificationSendingQueueService;
|
||||||
|
this.notificationRepository = notificationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendNotificationAsync(int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
int idNotificationCategory,
|
||||||
|
string title,
|
||||||
|
string subject,
|
||||||
|
TimeSpan timeToLife,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
NotificationDto notification = new()
|
||||||
|
{
|
||||||
|
IdUser = idUser,
|
||||||
|
IdNotificationTransport = idNotificationTransport,
|
||||||
|
IdNotificationCategory = idNotificationCategory,
|
||||||
|
Title = title,
|
||||||
|
Subject = subject,
|
||||||
|
TimeToLife = timeToLife
|
||||||
|
};
|
||||||
|
|
||||||
|
await notificationRepository.InsertAsync(notification,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
notificationSendingQueueService.Enqueue(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateNotificationAsync(int idNotification,
|
||||||
|
bool isRead,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var notification = await notificationRepository.GetOrDefaultAsync(idNotification,
|
||||||
|
cancellationToken) ?? throw new ArgumentInvalidException("Уведомление не найдено",
|
||||||
|
nameof(idNotification));
|
||||||
|
|
||||||
|
notification.IsRead = isRead;
|
||||||
|
|
||||||
|
await notificationRepository.UpdateAsync(notification,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResendNotificationAsync(int idUser,
|
||||||
|
int idNotificationTransport,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var notifications = await notificationRepository.GetUnsentNotificationsAsync(idUser,
|
||||||
|
idNotificationTransport,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
notificationSendingQueueService.EnqueueRange(notifications);
|
||||||
|
}
|
||||||
|
}
|
129
AsbCloudWebApi/Controllers/NotificationController.cs
Normal file
129
AsbCloudWebApi/Controllers/NotificationController.cs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AsbCloudApp.Data;
|
||||||
|
using AsbCloudApp.Repositories;
|
||||||
|
using AsbCloudApp.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace AsbCloudWebApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Уведомления
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/notification")]
|
||||||
|
public class NotificationController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly INotificationService notificationService;
|
||||||
|
private readonly INotificationRepository notificationRepository;
|
||||||
|
|
||||||
|
public NotificationController(INotificationService notificationService,
|
||||||
|
INotificationRepository notificationRepository)
|
||||||
|
{
|
||||||
|
this.notificationService = notificationService;
|
||||||
|
this.notificationRepository = notificationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод отправки уведомления
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idUser">Id пользователя</param>
|
||||||
|
/// <param name="idNotificationTransport">Id способа отправки уведомления</param>
|
||||||
|
/// <param name="idNotificationCategory">Id категории уведомления</param>
|
||||||
|
/// <param name="title">Заголовок уведомления</param>
|
||||||
|
/// <param name="subject">Сообщение уведомления</param>
|
||||||
|
/// <param name="timeToLife">Время жизни уведомления</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost]
|
||||||
|
[Route("send")]
|
||||||
|
public async Task<IActionResult> SendAsync([Required] int idUser,
|
||||||
|
[Required] int idNotificationTransport,
|
||||||
|
[Required] int idNotificationCategory,
|
||||||
|
[Required] string title,
|
||||||
|
[Required] string subject,
|
||||||
|
[Required] TimeSpan timeToLife,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await notificationService.SendNotificationAsync(idUser,
|
||||||
|
idNotificationTransport,
|
||||||
|
idNotificationCategory,
|
||||||
|
title,
|
||||||
|
subject,
|
||||||
|
timeToLife,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод обновления уведомления
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idNotification">Id уведомления</param>
|
||||||
|
/// <param name="isRead">Прочитано ли уведомление</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPut]
|
||||||
|
[Route("update")]
|
||||||
|
public async Task<IActionResult> UpdateAsync([Required] int idNotification,
|
||||||
|
[Required] bool isRead,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await notificationService.UpdateNotificationAsync(idNotification,
|
||||||
|
isRead,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод получения уведомлений по параметрам
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="skip">Кол-во пропускаемых записей</param>
|
||||||
|
/// <param name="take">Кол-во выбираемых записей</param>
|
||||||
|
/// <param name="idNotificationTransport">Id способа доставки уведомления</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("getList")]
|
||||||
|
[ProducesResponseType(typeof(PaginationContainer<NotificationDto>), (int)System.Net.HttpStatusCode.OK)]
|
||||||
|
public async Task<IActionResult> GetListAsync(int? skip,
|
||||||
|
int? take,
|
||||||
|
[Required] int idNotificationTransport,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int? idUser = User.GetUserId();
|
||||||
|
|
||||||
|
if (!idUser.HasValue)
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
var result = await notificationRepository.GetNotificationsAsync(skip,
|
||||||
|
take,
|
||||||
|
idUser.Value,
|
||||||
|
idNotificationTransport,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метод удаления уведомления
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idNotification">Id уведомления</param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpDelete]
|
||||||
|
[Route("delete")]
|
||||||
|
public async Task<IActionResult> DeleteAsync([Required] int idNotification,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await notificationRepository.DeleteAsync(idNotification,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.OpenApi.Any;
|
||||||
|
|
||||||
namespace AsbCloudWebApi
|
namespace AsbCloudWebApi
|
||||||
{
|
{
|
||||||
@ -18,6 +19,7 @@ namespace AsbCloudWebApi
|
|||||||
{
|
{
|
||||||
services.AddSwaggerGen(c =>
|
services.AddSwaggerGen(c =>
|
||||||
{
|
{
|
||||||
|
c.MapType<TimeSpan>(() => new OpenApiSchema { Type = "string", Example = new OpenApiString("00:00:00") });
|
||||||
c.MapType<DateOnly>(() => new OpenApiSchema { Type = "string", Format = "date" });
|
c.MapType<DateOnly>(() => new OpenApiSchema { Type = "string", Format = "date" });
|
||||||
c.MapType<JsonValue>(() => new OpenApiSchema {
|
c.MapType<JsonValue>(() => new OpenApiSchema {
|
||||||
AnyOf = new OpenApiSchema[]
|
AnyOf = new OpenApiSchema[]
|
||||||
|
Loading…
Reference in New Issue
Block a user