feat: Из dal перенесены все модели в Domain. Из dal переименованы service в repository, вынесены в Core.
This commit is contained in:
14
PARR.DAL/Repositories/AgentHistoryRepository.cs
Normal file
14
PARR.DAL/Repositories/AgentHistoryRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class AgentHistoryRepository : BaseRepository<AgentHistory>, IAgentHistoryRepository
|
||||
{
|
||||
public AgentHistoryRepository(DataContext dataContext, ILogger<AgentHistoryRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/DistributionPeriodRepository.cs
Normal file
13
PARR.DAL/Repositories/DistributionPeriodRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class DistributionPeriodRepository : BaseRepository<DistributionPeriod>, IDistributionPeriodRepository
|
||||
{
|
||||
public DistributionPeriodRepository(DataContext dataContext, ILogger<DistributionPeriodRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
121
PARR.DAL/Repositories/EsppSchTypeConfigRepository.cs
Normal file
121
PARR.DAL/Repositories/EsppSchTypeConfigRepository.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.DomainModels;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class EsppSchTypeConfigRepository : BaseRepository<EsppSchTypeConfig>, IEsppSchTypeConfigRepository
|
||||
{
|
||||
public EsppSchTypeConfigRepository(DataContext dataContext, ILogger<EsppSchTypeConfigRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
public IQueryable<EsppSchTypeConfig> GetWithSchIncludes()
|
||||
{
|
||||
return Get()
|
||||
.Include(t => t.EsppSchValues)
|
||||
.Include(t => t.EsppSchTypeSchedule)
|
||||
.Include(t => t.EsppSchType)
|
||||
.ThenInclude(t => t!.EsppSchTypeValues)
|
||||
.ThenInclude(t => t!.EsppSchValues);
|
||||
}
|
||||
|
||||
|
||||
public async Task<EsppScheduleDto?> GetEsppScheduleDtoAsync(Guid jobGroupId)
|
||||
{
|
||||
//Формирует расписание в нормальном понятном виде из БД
|
||||
|
||||
var items = await EntityContext.EsppSchValues.Where(t => t.JobGroupId == jobGroupId)
|
||||
.Select(t => new
|
||||
{
|
||||
t.EsppSchTypeConfig!.Order,
|
||||
Type = t.EsppSchTypeConfig.EsppSchType!,
|
||||
Value = t.EsppSchTypeValue!,
|
||||
TypeSchedule = t.EsppSchTypeConfig.EsppSchTypeSchedule!
|
||||
})
|
||||
.OrderBy(t => t.Order)
|
||||
.ToListAsync();
|
||||
|
||||
if (items.Count == 0)
|
||||
return null;
|
||||
|
||||
var typeSchedule = items.First().TypeSchedule;
|
||||
|
||||
var dto = new EsppScheduleDto
|
||||
{
|
||||
TypeSchedule = typeSchedule,
|
||||
Values = items.Select(t => new EsppScheduleValDto
|
||||
{
|
||||
Order = t.Order,
|
||||
Type = t.Type,
|
||||
Value = t.Value
|
||||
}
|
||||
).ToList()
|
||||
};
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
||||
#region original GetEsppScheduleDtoAsync
|
||||
//public async Task<EsppScheduleDto?> GetEsppScheduleDtoAsync(Guid jobGroupId)
|
||||
//{
|
||||
// //Формирует расписание в нормальном понятном виде из БД
|
||||
|
||||
// var schValues = await dataContext.EsppSchValues
|
||||
// .AsNoTracking()
|
||||
// .Include(t => t.EsppSchTypeConfig)
|
||||
// .ThenInclude(t => t!.EsppSchTypeSchedule)
|
||||
// .Include(t => t.EsppSchTypeConfig)
|
||||
// .ThenInclude(t => t.EsppSchType)
|
||||
// .Include(t => t.EsppSchTypeValue)
|
||||
// .Include(t => t.JobGroup)
|
||||
// .Where(t => t.JobGroup!.Id == jobGroupId)
|
||||
// .OrderBy(t => t.EsppSchTypeConfig!.Order)
|
||||
// .ToListAsync();
|
||||
|
||||
|
||||
// if (!schValues.Any())
|
||||
// return null;
|
||||
|
||||
|
||||
// var values = new List<EsppScheduleValDto>();
|
||||
|
||||
// foreach (var schValue in schValues)
|
||||
// {
|
||||
// var value = new EsppScheduleValDto
|
||||
// {
|
||||
// Order = schValue.EsppSchTypeConfig!.Order,
|
||||
// Type = schValue.EsppSchTypeConfig!.EsppSchType!,
|
||||
// Value = schValue.EsppSchTypeValue!
|
||||
// };
|
||||
|
||||
// values.Add(value);
|
||||
// }
|
||||
|
||||
|
||||
// var dto = new EsppScheduleDto
|
||||
// {
|
||||
// TypeSchedule = schValues.First().EsppSchTypeConfig!.EsppSchTypeSchedule!,
|
||||
// Values = values.OrderBy(t => t.Order).ToList()
|
||||
// };
|
||||
|
||||
|
||||
// return dto;
|
||||
//}
|
||||
#endregion
|
||||
|
||||
|
||||
public EsppScheduleDto? GetEsppScheduleDto(Guid jobGroupId)
|
||||
{
|
||||
var result = Task.Run(async () =>
|
||||
{
|
||||
return await GetEsppScheduleDtoAsync(jobGroupId);
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
PARR.DAL/Repositories/EsppSchTypeScheduleRepository.cs
Normal file
31
PARR.DAL/Repositories/EsppSchTypeScheduleRepository.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class EsppSchTypeScheduleRepository : IEsppSchTypeScheduleRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public EsppSchTypeScheduleRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
|
||||
public IQueryable<EsppSchTypeSchedule> Get()
|
||||
{
|
||||
return dataContext.EsppSchTypeSchedules;
|
||||
}
|
||||
|
||||
|
||||
public async Task<EsppSchTypeSchedule?> GetAsync(int id)
|
||||
{
|
||||
return await dataContext.EsppSchTypeSchedules
|
||||
.FirstOrDefaultAsync(t => t.Id == id);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/EsppSchTypeValueRepository.cs
Normal file
13
PARR.DAL/Repositories/EsppSchTypeValueRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class EsppSchTypeValueRepository : BaseRepository<EsppSchTypeValue>, IEsppSchTypeValueRepository
|
||||
{
|
||||
public EsppSchTypeValueRepository(DataContext dataContext, ILogger<EsppSchTypeValueRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/Job/FieldFilterRepository.cs
Normal file
13
PARR.DAL/Repositories/Job/FieldFilterRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
internal class FieldFilterRepository : BaseRepository<JobFieldFilter>, IFieldFilterRepository
|
||||
{
|
||||
public FieldFilterRepository(DataContext dataContext, ILogger<FieldFilterRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
21
PARR.DAL/Repositories/Job/JobAutoControlRepository.cs
Normal file
21
PARR.DAL/Repositories/Job/JobAutoControlRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
internal class JobAutoControlRepository : IJobAutoControlRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public JobAutoControlRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<JobAutoControl> Get()
|
||||
{
|
||||
return dataContext.JobAutoControls;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
PARR.DAL/Repositories/Job/JobGroupRepository.cs
Normal file
19
PARR.DAL/Repositories/Job/JobGroupRepository.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
internal class JobGroupRepository : BaseRepository<JobGroup>, IJobGroupRepository
|
||||
{
|
||||
public JobGroupRepository(DataContext dataContext, ILogger<JobGroupRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
|
||||
public void DeleteDistributionConfig(JobGroupDistributionConfig distributionConfig)
|
||||
{
|
||||
EntityContext.JobGroupDistributionConfigs.Remove(distributionConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/Job/JobGroupTypeRepository.cs
Normal file
13
PARR.DAL/Repositories/Job/JobGroupTypeRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
internal class JobGroupTypeRepository : BaseRepository<JobGroupType>, IJobGroupTypeRepository
|
||||
{
|
||||
public JobGroupTypeRepository(DataContext dataContext, ILogger<JobGroupTypeRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
39
PARR.DAL/Repositories/Job/JobRepository.cs
Normal file
39
PARR.DAL/Repositories/Job/JobRepository.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
internal class JobRepository : BaseRepository<Domain.Entities.Job.Job>, IJobRepository
|
||||
{
|
||||
public JobRepository(DataContext dataContext, ILogger<JobRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
public override Task<bool> CreateAsync(Domain.Entities.Job.Job obj)
|
||||
{
|
||||
if (obj.AutoControl == null)
|
||||
obj.AutoControl = new JobAutoControl
|
||||
{
|
||||
JobId = obj.Id
|
||||
};
|
||||
|
||||
return base.CreateAsync(obj);
|
||||
}
|
||||
|
||||
|
||||
public override Task<bool> AddRangeAsync(List<Domain.Entities.Job.Job> objs)
|
||||
{
|
||||
objs.ForEach(job =>
|
||||
{
|
||||
if (job.AutoControl == null)
|
||||
job.AutoControl = new JobAutoControl
|
||||
{
|
||||
JobId = job.Id
|
||||
};
|
||||
});
|
||||
|
||||
return base.AddRangeAsync(objs);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/Job/JobUnitFilterRepository.cs
Normal file
13
PARR.DAL/Repositories/Job/JobUnitFilterRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.DAL.Repositories.Job
|
||||
{
|
||||
internal class JobUnitFilterRepository : BaseRepository<JobUnitFilter>, IJobUnitFilterRepository
|
||||
{
|
||||
public JobUnitFilterRepository(DataContext dataContext, ILogger<JobUnitFilterRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/OrderRepository.cs
Normal file
13
PARR.DAL/Repositories/OrderRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class OrderRepository : BaseRepository<Order>, IOrderRepository
|
||||
{
|
||||
public OrderRepository(DataContext dataContext, ILogger<OrderRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
56
PARR.DAL/Repositories/OrderStatusRepository.cs
Normal file
56
PARR.DAL/Repositories/OrderStatusRepository.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class OrderStatusRepository : IOrderStatusRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
private readonly ILogger<OrderStatusRepository> logger;
|
||||
|
||||
public OrderStatusRepository(DataContext dataContext, ILogger<OrderStatusRepository> logger)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public IQueryable<OrderStatus> Get()
|
||||
{
|
||||
logger.LogDebug("Получаю набор статусов нарядов");
|
||||
return dataContext.OrderStatuses;
|
||||
}
|
||||
|
||||
|
||||
public OrderStatusEnum GetOrderStatusByName(string statusName)
|
||||
{
|
||||
logger.LogDebug("Поиск статуса наряда по имени: {StatusName}", statusName);
|
||||
|
||||
switch (statusName.Trim())
|
||||
{
|
||||
case "1-Направлен в группу":
|
||||
logger.LogDebug("Найден статус: {StatusEnum}", OrderStatusEnum.New);
|
||||
return OrderStatusEnum.New;
|
||||
case "2-В работе":
|
||||
logger.LogDebug("Найден статус: {StatusEnum}", OrderStatusEnum.InWork);
|
||||
return OrderStatusEnum.InWork;
|
||||
case "3-Приостановлен":
|
||||
logger.LogDebug("Найден статус: {StatusEnum}", OrderStatusEnum.Stop);
|
||||
return OrderStatusEnum.Stop;
|
||||
case "4-Выполнен":
|
||||
logger.LogDebug("Найден статус: {StatusEnum}", OrderStatusEnum.Complete);
|
||||
return OrderStatusEnum.Complete;
|
||||
case "5-Закрыт":
|
||||
logger.LogDebug("Найден статус: {StatusEnum}", OrderStatusEnum.Closed);
|
||||
return OrderStatusEnum.Closed;
|
||||
default:
|
||||
logger.LogWarning(
|
||||
"Не смог распознать статус наряда, вернул значение по умолчанию: {DefaultStatus}, входящее значение: {InputStatus}",
|
||||
OrderStatusEnum.New, statusName);
|
||||
return OrderStatusEnum.New;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
PARR.DAL/Repositories/ParrComponentRepository.cs
Normal file
21
PARR.DAL/Repositories/ParrComponentRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class ParrComponentRepository : IParrComponentRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public ParrComponentRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<ParrComponent> Get()
|
||||
{
|
||||
return dataContext.ParrComponents;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
PARR.DAL/Repositories/ProcessRepository.cs
Normal file
14
PARR.DAL/Repositories/ProcessRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class ProcessRepository : BaseRepository<Process>, IProcessRepository
|
||||
{
|
||||
public ProcessRepository(ILogger<ProcessRepository> logger, DataContext dataContext) : base(logger, dataContext) { }
|
||||
|
||||
}
|
||||
}
|
||||
151
PARR.DAL/Repositories/RobotConfigurationRepository.cs
Normal file
151
PARR.DAL/Repositories/RobotConfigurationRepository.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class RobotConfigurationRepository : BaseRepository<RobotConfiguration>, IRobotConfigurationRepository
|
||||
{
|
||||
public RobotConfigurationRepository(DataContext dataContext, ILogger<RobotConfigurationRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
|
||||
public void ChangeTaskStatus(TaskStatusEnum taskStatus, RobotConfiguration configuration)
|
||||
{
|
||||
configuration.TaskStatusCode = (int)taskStatus;
|
||||
|
||||
switch (taskStatus)
|
||||
{
|
||||
case TaskStatusEnum.Creating:
|
||||
ChangeRobotStatus(RobotStatusEnum.Wait, configuration);
|
||||
break;
|
||||
case TaskStatusEnum.Updating:
|
||||
ChangeRobotStatus(RobotStatusEnum.Wait, configuration);
|
||||
break;
|
||||
case TaskStatusEnum.Ok:
|
||||
ChangeRobotStatus(RobotStatusEnum.Complete, configuration);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool SetUpdateTaskStatusIfAllow(RobotConfiguration configuration)
|
||||
{
|
||||
var updatingStatus = TaskStatusEnum.Updating;
|
||||
|
||||
// ставить статус Updating можно только если текущий статус == Ok
|
||||
if (configuration.TaskStatusCode != (int)TaskStatusEnum.Ok)
|
||||
{
|
||||
int taskStatusValue = configuration.TaskStatusCode;
|
||||
string taskStatusName = Enum.IsDefined(typeof(TaskStatusEnum), taskStatusValue)
|
||||
? ((TaskStatusEnum)taskStatusValue).ToString()
|
||||
: $"Unknown ({taskStatusValue})";
|
||||
|
||||
logger.LogInformation("Нельзя установить статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}, так как текущий статус {currentStatus}",
|
||||
updatingStatus, configuration.Id, configuration.TemplateId, taskStatusName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Если хотим поставить Updating у шаблона, можно ставить только в том случае, если у шаблона уже есть ScheduleEsppId
|
||||
if (configuration.RobotCode == (int)RobotsEnum.TemplateOrder)
|
||||
{
|
||||
// есть ли связь у config с templetes, может инклуда нет, мало ли
|
||||
if (configuration.Template == null)
|
||||
{
|
||||
logger.LogWarning("При изменении статуса задания на обновление шаблона, не смог проверить наличае ScheduleEsppId, так как нет Include с Templates. Пропустил эту проверку. configurationId: {configurationId}", configuration.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (configuration.Template.ScheduleEsppId == null)
|
||||
{
|
||||
logger.LogInformation("Нельзя установить статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}, так как у шаблона отсутсвтует ScheduleEsppId=null",
|
||||
updatingStatus, configuration.Id, configuration.TemplateId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Статус ОК, можно ставить Updating
|
||||
ChangeTaskStatus(updatingStatus, configuration);
|
||||
logger.LogInformation("Установлен статус {newStatus} для конфигурации {configurationId}, templateId: {templateId}", updatingStatus, configuration.Id, configuration.TemplateId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public void ChangeRobotStatus(RobotStatusEnum robotStatus, RobotConfiguration configuration)
|
||||
{
|
||||
configuration.RobotStatusCode = (int)robotStatus;
|
||||
|
||||
switch (robotStatus)
|
||||
{
|
||||
case RobotStatusEnum.InProgress:
|
||||
configuration.AttemptsNumber++;
|
||||
configuration.LastRobotStatusUpdated = DateTimeOffset.UtcNow;
|
||||
break;
|
||||
//case RobotStatusEnum.Error:
|
||||
// break;
|
||||
case RobotStatusEnum.Complete:
|
||||
configuration.LastRobotStatusUpdated = DateTimeOffset.UtcNow;
|
||||
break;
|
||||
case RobotStatusEnum.Wait:
|
||||
configuration.LastRobotStatusUpdated = null;
|
||||
configuration.AttemptsNumber = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public RobotConfiguration GetFromTemplateByRobotCode(RobotsEnum robotsEnum, Template template)
|
||||
{
|
||||
var config = template.RobotConfigurations.FirstOrDefault(t => t.RobotCode == (int)robotsEnum);
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
logger.LogError($"У шаблона нет конфигурации роботов. TemplateId: {template.Id}");
|
||||
throw new Exception($"У шаблона нет конфигурации роботов. TemplateId: {template.Id}");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
// Поиск невыполненных заданий и установка им статуса ошибки робота
|
||||
public async Task FindUnfulfilledTaskAndSetRobotErrorStatusAsync(int robotAttemptsNumber, TimeSpan robotWaitTime)
|
||||
{
|
||||
//Ищем `RobotStatusCode` = 22 и `LastStatusUpdated` истекло и `AttemptsNumber` >= допустимого значения из настроек,
|
||||
//ставим всем этим записям `RobotStatusCode`= 33
|
||||
|
||||
var endDate = DateTimeOffset.UtcNow.Add(-robotWaitTime);
|
||||
|
||||
var configObjs = await EntitySet.Where(t =>
|
||||
t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
||||
&& t.AttemptsNumber >= robotAttemptsNumber
|
||||
&& t.LastRobotStatusUpdated <= endDate
|
||||
).ToListAsync();
|
||||
|
||||
if (!configObjs.Any())
|
||||
return;
|
||||
|
||||
configObjs.ForEach(item =>
|
||||
{
|
||||
ChangeRobotStatus(RobotStatusEnum.Error, item);
|
||||
logger.LogInformation($"Устанавливаю RobotStatus: {RobotStatusEnum.Error} для RobotConfigurationId {item.Id}");
|
||||
});
|
||||
|
||||
var result = await CommitAsync();
|
||||
|
||||
if (!result)
|
||||
logger.LogError($"Ошибка при сохранении изменений RobotStatus для RobotConfigurationId: item.Id, RobotStatus: {RobotStatusEnum.Error}");
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
21
PARR.DAL/Repositories/RobotHistoryLevelRepository.cs
Normal file
21
PARR.DAL/Repositories/RobotHistoryLevelRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class RobotHistoryLevelRepository : IRobotHistoryLevelRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public RobotHistoryLevelRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<RobotHistoryLevel> Get()
|
||||
{
|
||||
return dataContext.RobotHistoryLevels;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
PARR.DAL/Repositories/RobotHistoryRepository.cs
Normal file
14
PARR.DAL/Repositories/RobotHistoryRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class RobotHistoryRepository : BaseRepository<RobotHistory>, IRobotHistoryRepository
|
||||
{
|
||||
public RobotHistoryRepository(DataContext dataContext, ILogger<RobotHistoryRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
}
|
||||
}
|
||||
21
PARR.DAL/Repositories/RobotRepository.cs
Normal file
21
PARR.DAL/Repositories/RobotRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class RobotRepository : IRobotRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public RobotRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<Robot> Get()
|
||||
{
|
||||
return dataContext.Robots;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
PARR.DAL/Repositories/RobotStatusRepository.cs
Normal file
21
PARR.DAL/Repositories/RobotStatusRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class RobotStatusRepository : IRobotStatusRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public RobotStatusRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<RobotStatus> Get()
|
||||
{
|
||||
return dataContext.RobotStatuses;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/RoleRepository.cs
Normal file
13
PARR.DAL/Repositories/RoleRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class RoleRepository : BaseRepository<Role>, IRoleRepository
|
||||
{
|
||||
public RoleRepository(DataContext dataContext, ILogger<RoleRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
|
||||
namespace PARR.DAL.Repositories.Schedule
|
||||
{
|
||||
internal class ScheduleExcludeTypeCalendarRepository : BaseRepository<ScheduleExcludeTypeCalendar>, IScheduleExcludeTypeCalendarRepository
|
||||
{
|
||||
public ScheduleExcludeTypeCalendarRepository(DataContext dataContext, ILogger<ScheduleExcludeTypeCalendarRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
|
||||
namespace PARR.DAL.Repositories.Schedule
|
||||
{
|
||||
internal class ScheduleExcludeTypeRepository : BaseRepository<ScheduleExcludeType>, IScheduleExcludeTypeRepository
|
||||
{
|
||||
public ScheduleExcludeTypeRepository(DataContext dataContext, ILogger<ScheduleExcludeTypeRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Schedule;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Contracts;
|
||||
using PARR.Domain.Entities.Schedule;
|
||||
|
||||
namespace PARR.DAL.Repositories.Schedule
|
||||
{
|
||||
internal sealed class ScheduleResponseAreaTimeOffsetRepository : IScheduleResponseAreaTimeOffsetRepository
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, ScheduleResponseAreaTimeOffset> offsetList;
|
||||
|
||||
/// <summary>
|
||||
/// ЗО по умолчанию
|
||||
/// </summary>
|
||||
private readonly string defaultResponseArea;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки, если не нашли в offsetList
|
||||
/// </summary>
|
||||
private readonly ScheduleResponseAreaTimeOffset defaultOffset;
|
||||
private readonly ILogger<ScheduleResponseAreaTimeOffsetRepository> logger;
|
||||
|
||||
public ScheduleResponseAreaTimeOffsetRepository(DataContext dataContext, SettingsFromDb settingsFromDb, ILogger<ScheduleResponseAreaTimeOffsetRepository> logger)
|
||||
{
|
||||
offsetList = dataContext.ScheduleResponseAreaTimeOffsets
|
||||
.AsNoTracking()
|
||||
.ToDictionary(t => t.ResponseArea, t => t, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (string.IsNullOrEmpty(settingsFromDb.DefaultResponseAreaToTimeOffset))
|
||||
defaultResponseArea = "17-МСК";
|
||||
else
|
||||
defaultResponseArea = settingsFromDb.DefaultResponseAreaToTimeOffset;
|
||||
|
||||
|
||||
defaultOffset = new ScheduleResponseAreaTimeOffset
|
||||
{
|
||||
ResponseArea = "17-МСК",
|
||||
EsppValue = "",
|
||||
UtcTimeOffset = new TimeSpan(3, 0, 0)
|
||||
};
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyDictionary<string, ScheduleResponseAreaTimeOffset> GetAll => offsetList;
|
||||
|
||||
public ScheduleResponseAreaTimeOffset GetDefault => GetByResponseAreaOrDefault(defaultResponseArea);
|
||||
|
||||
public ScheduleResponseAreaTimeOffset GetByResponseAreaOrDefault(string responseArea)
|
||||
{
|
||||
offsetList.TryGetValue(responseArea, out var value);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
logger.LogDebug("Получил часовой пояс по ЗО '{responseArea}', esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}", responseArea, value.EsppValue, value.UtcTimeOffset);
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("Не смог получить часовой пояс по ЗО '{responseArea}', вернул часово пояс по умолчанию: name: {name}, esppValue: {EsppValue}, utcTimeOffset: {utcTimeOffset}",
|
||||
responseArea, defaultOffset.ResponseArea, defaultOffset.EsppValue, defaultOffset.UtcTimeOffset);
|
||||
return defaultOffset;
|
||||
}
|
||||
//return offsetList.TryGetValue(responseArea, out var value) ? value : defaultOffset;
|
||||
}
|
||||
|
||||
|
||||
//public IQueryable<ScheduleResponseAreaTimeOffset> Get()
|
||||
//{
|
||||
// return dataContext.ScheduleResponseAreaTimeOffsets;
|
||||
//}
|
||||
|
||||
//public async Task<string?> GetTimeOffsetByResponseAreaAsync(string responseArea)
|
||||
//{
|
||||
// var result = await dataContext.ScheduleResponseAreaTimeOffsets.FirstOrDefaultAsync(t => t.ResponseArea == responseArea);
|
||||
|
||||
// return result != null ? result.TimeOffset : null;
|
||||
//}
|
||||
}
|
||||
}
|
||||
20
PARR.DAL/Repositories/StatusTemplateRepository.cs
Normal file
20
PARR.DAL/Repositories/StatusTemplateRepository.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class StatusTemplateRepository : IStatusTemplateRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public StatusTemplateRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<Domain.Entities.TaskStatus> Get()
|
||||
{
|
||||
return dataContext.TaskStatuses;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
PARR.DAL/Repositories/SubprocessRepository.cs
Normal file
14
PARR.DAL/Repositories/SubprocessRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class SubprocessRepository : BaseRepository<Subprocess>, ISubprocessRepository
|
||||
{
|
||||
public SubprocessRepository(ILogger<SubprocessRepository> logger, DataContext dataContext) : base(logger, dataContext) { }
|
||||
|
||||
}
|
||||
}
|
||||
20
PARR.DAL/Repositories/TaskStatusRepository.cs
Normal file
20
PARR.DAL/Repositories/TaskStatusRepository.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class TaskStatusRepository : ITaskStatusRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public TaskStatusRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<Domain.Entities.TaskStatus> Get()
|
||||
{
|
||||
return dataContext.TaskStatuses;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
PARR.DAL/Repositories/TemplateHistoryRepository.cs
Normal file
13
PARR.DAL/Repositories/TemplateHistoryRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class TemplateHistoryRepository : BaseRepository<TemplateHistory>, ITemplateHistoryRepository
|
||||
{
|
||||
public TemplateHistoryRepository(DataContext dataContext, ILogger<TemplateHistoryRepository> logger) : base(logger, dataContext) { }
|
||||
}
|
||||
}
|
||||
183
PARR.DAL/Repositories/TemplateRepository.cs
Normal file
183
PARR.DAL/Repositories/TemplateRepository.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Enums;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class TemplateRepository : BaseRepository<Template>, ITemplateRepository
|
||||
{
|
||||
public TemplateRepository(DataContext dataContext, ILogger<TemplateRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
public async Task<Template?> GetTemplateByNameAsync(string name)
|
||||
{
|
||||
logger.LogDebug("Поиск шаблона по имени: {TemplateName}", name);
|
||||
|
||||
var template = await GetWithIncludes()
|
||||
.Include(t => t.RobotConfigurations)
|
||||
.FirstOrDefaultAsync(t => t.Name == name);
|
||||
|
||||
if (template != null)
|
||||
{
|
||||
logger.LogDebug("Шаблон найден: {TemplateId}, имя: {TemplateName}", template.Id, template.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Шаблон с именем {TemplateName} не найден", name);
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
public IQueryable<Template> GetWithIncludes()
|
||||
{
|
||||
logger.LogDebug("Получаю шаблоны с include связями");
|
||||
|
||||
return Get()
|
||||
.Include(h => h.Unit)
|
||||
.ThenInclude(t => t!.UnitValues)
|
||||
.ThenInclude(t => t.Value)
|
||||
.Include(h => h.Unit)
|
||||
.ThenInclude(t => t!.UnitValues)
|
||||
.ThenInclude(t => t.Field)
|
||||
.Include(a => a.Job)
|
||||
.ThenInclude(t => t!.Tnk)
|
||||
.ThenInclude(s => s!.Subprocess)
|
||||
.ThenInclude(p => p!.Process)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Group)
|
||||
.ThenInclude(t => t!.GroupType)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Group)
|
||||
.ThenInclude(t => t.DistributionConfig)
|
||||
.ThenInclude(t => t.DistributionPeriod);
|
||||
}
|
||||
|
||||
public override Task<bool> CreateAsync(Template obj)
|
||||
{
|
||||
logger.LogDebug("Создание шаблона: {TemplateName}", obj.Name);
|
||||
|
||||
// добавление роботов для шаблона
|
||||
obj.RobotConfigurations = new List<RobotConfiguration>
|
||||
{
|
||||
// робот по управлению шаблоном
|
||||
new RobotConfiguration
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
TemplateId = obj.Id,
|
||||
RobotCode = (int)RobotsEnum.TemplateOrder,
|
||||
TaskStatusCode = (int)TaskStatusEnum.Creating,
|
||||
RobotStatusCode = (int)RobotStatusEnum.Wait,
|
||||
AttemptsNumber = 0,
|
||||
LastRobotStatusUpdated = null
|
||||
},
|
||||
// робот по управлениею расписанием
|
||||
new RobotConfiguration
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
TemplateId = obj.Id,
|
||||
RobotCode = (int)RobotsEnum.ScheduleOrder,
|
||||
TaskStatusCode = (int)TaskStatusEnum.Creating,
|
||||
RobotStatusCode = (int)RobotStatusEnum.Wait,
|
||||
AttemptsNumber = 0,
|
||||
LastRobotStatusUpdated = null
|
||||
}
|
||||
};
|
||||
|
||||
logger.LogDebug("Добавлены роботы для шаблона {TemplateName}", obj.Name);
|
||||
|
||||
return base.CreateAsync(obj);
|
||||
}
|
||||
|
||||
|
||||
public async Task<Guid?> ReserveUnusedTemplateAsync(Guid newUnitId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogDebug("Резервирую неиспользуемый шаблон с проверкой конфигураций роботов для UnitId: {UnitId}", newUnitId);
|
||||
|
||||
var sql = @"
|
||||
UPDATE ""Templates""
|
||||
SET ""StatusTypeId"" = @NewStatus,
|
||||
""DateModified"" = @DateModified,
|
||||
""InitiatorIp"" = @InitiatorIp,
|
||||
""InitiatorParrComponentId"" = @InitiatorComponent,
|
||||
""InitiatorComment"" = @InitiatorComment
|
||||
WHERE ""Id"" = (
|
||||
SELECT t.""Id""
|
||||
FROM ""Templates"" t
|
||||
WHERE t.""StatusTypeId"" = @OldStatus
|
||||
AND t.""UnitId"" != @NewUnitId
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM ""RobotConfigurations"" rc
|
||||
WHERE rc.""TemplateId"" = t.""Id""
|
||||
AND rc.""RobotCode"" = @RobotCode1
|
||||
AND rc.""TaskStatusCode"" = @TaskStatus
|
||||
AND rc.""RobotStatusCode"" = @RobotStatus
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM ""RobotConfigurations"" rc
|
||||
WHERE rc.""TemplateId"" = t.""Id""
|
||||
AND rc.""RobotCode"" = @RobotCode2
|
||||
AND rc.""TaskStatusCode"" = @TaskStatus
|
||||
AND rc.""RobotStatusCode"" = @RobotStatus
|
||||
)
|
||||
ORDER BY t.""DateCreated"" ASC
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING ""Id"";";
|
||||
|
||||
var parameters = new[]
|
||||
{
|
||||
new NpgsqlParameter("@NewStatus", (int)TemplateStatusTypeEnum.Updating),
|
||||
new NpgsqlParameter("@DateModified", DateTimeOffset.UtcNow),
|
||||
new NpgsqlParameter("@InitiatorIp", initiator.InitiatorIp ?? (object)DBNull.Value),
|
||||
new NpgsqlParameter("@InitiatorComponent",
|
||||
initiator.InitiatorParrComponentId.HasValue
|
||||
? (object)(int)initiator.InitiatorParrComponentId.Value
|
||||
: DBNull.Value),
|
||||
new NpgsqlParameter("@InitiatorComment", initiator.InitiatorComment ?? (object)DBNull.Value),
|
||||
new NpgsqlParameter("@OldStatus", (int)TemplateStatusTypeEnum.Unused),
|
||||
new NpgsqlParameter("@NewUnitId", newUnitId),
|
||||
// Параметры для проверки конфигураций роботов
|
||||
new NpgsqlParameter("@RobotCode1", (int)RobotsEnum.TemplateOrder),
|
||||
new NpgsqlParameter("@RobotCode2", (int)RobotsEnum.ScheduleOrder),
|
||||
new NpgsqlParameter("@TaskStatus", (int)TaskStatusEnum.Ok),
|
||||
new NpgsqlParameter("@RobotStatus", (int)RobotStatusEnum.Complete)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await EntityContext.Database
|
||||
.SqlQueryRaw<Guid>(sql, parameters)
|
||||
.ToListAsync();
|
||||
|
||||
var reservedTemplateId = result.FirstOrDefault();
|
||||
|
||||
if (reservedTemplateId != default(Guid))
|
||||
{
|
||||
logger.LogInformation("Успешно зарезервирован шаблон с ID: {TemplateId} для UnitId: {UnitId}",
|
||||
reservedTemplateId, newUnitId);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Не удалось зарезервировать шаблон для UnitId: {UnitId} (не найдено подходящих конфигураций роботов)", newUnitId);
|
||||
}
|
||||
|
||||
return reservedTemplateId;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка при резервировании шаблона для UnitId: {UnitId}", newUnitId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
PARR.DAL/Repositories/TemplateStatusTypeRepository.cs
Normal file
22
PARR.DAL/Repositories/TemplateStatusTypeRepository.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class TemplateStatusTypeRepository : ITemplateStatusTypeRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public TemplateStatusTypeRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
|
||||
public IQueryable<TemplateStatusType> Get()
|
||||
{
|
||||
return dataContext.TemplateStatusTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
PARR.DAL/Repositories/TnkRepository.cs
Normal file
14
PARR.DAL/Repositories/TnkRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class TnkRepository : BaseRepository<Tnk>, ITnkRepository
|
||||
{
|
||||
public TnkRepository(ILogger<TnkRepository> logger, DataContext dataContext) : base(logger, dataContext) { }
|
||||
|
||||
}
|
||||
}
|
||||
19
PARR.DAL/Repositories/Unit/UnitFieldRepository.cs
Normal file
19
PARR.DAL/Repositories/Unit/UnitFieldRepository.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.DAL.Repositories.Unit
|
||||
{
|
||||
internal class UnitFieldRepository : BaseRepository<UnitField>, IUnitFieldRepository
|
||||
{
|
||||
public UnitFieldRepository(DataContext dataContext, ILogger<UnitFieldRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
public async Task<UnitField?> GetByAihitNameAsync(string name)
|
||||
{
|
||||
return await EntitySet.FirstOrDefaultAsync(uf => uf.AihitName.ToLower().Trim() == name.ToLower().Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
26
PARR.DAL/Repositories/Unit/UnitFieldValueRepository.cs
Normal file
26
PARR.DAL/Repositories/Unit/UnitFieldValueRepository.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.DAL.Repositories.Unit
|
||||
{
|
||||
internal class UnitFieldValueRepository : BaseRepository<UnitFieldValue>, IUnitFieldValueRepository
|
||||
{
|
||||
public UnitFieldValueRepository(DataContext dataContext, ILogger<UnitFieldValueRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
|
||||
public async Task<UnitFieldValue?> GetByValueNameAsync(string? value)
|
||||
{
|
||||
var query = EntitySet
|
||||
.Include(v => v.FieldValues);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return await query.FirstOrDefaultAsync(uf => uf.Value == null);
|
||||
|
||||
return await query.FirstOrDefaultAsync(uf => uf.Value!.ToLower().Trim() == value.ToLower().Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
64
PARR.DAL/Repositories/Unit/UnitInUnitRepository.cs
Normal file
64
PARR.DAL/Repositories/Unit/UnitInUnitRepository.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.DAL.Repositories.Unit
|
||||
{
|
||||
internal class UnitInUnitRepository : IUnitInUnitRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
private readonly ILogger<UnitInUnitRepository> logger;
|
||||
|
||||
public UnitInUnitRepository(
|
||||
DataContext dataContext,
|
||||
ILogger<UnitInUnitRepository> logger
|
||||
)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public IQueryable<UnitInUnit> Get()
|
||||
{
|
||||
return dataContext.UnitInUnits;
|
||||
}
|
||||
|
||||
|
||||
public Task<List<UnitInUnit>> GetByParentIdAsync(Guid parentId)
|
||||
{
|
||||
return dataContext.UnitInUnits
|
||||
.Where(u => u.ParentUnitId == parentId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public Task<List<UnitInUnit>> GetByChildIdAsync(Guid childId)
|
||||
{
|
||||
return dataContext.UnitInUnits
|
||||
.Where(u => u.ChildUnitId == childId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<UnitInUnit>> GetParentLinksByChildIdsAsync(IEnumerable<Guid> childUnitIds)
|
||||
{
|
||||
var set = childUnitIds.ToHashSet();
|
||||
return await dataContext.UnitInUnits
|
||||
.AsNoTracking()
|
||||
.Where(uinu => set.Contains(uinu.ChildUnitId))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<UnitInUnit>> GetChildLinksByParentIdsAsync(IEnumerable<Guid> parentUnitIds)
|
||||
{
|
||||
var set = parentUnitIds.ToHashSet();
|
||||
return await dataContext.UnitInUnits
|
||||
.AsNoTracking()
|
||||
.Where(uinu => set.Contains(uinu.ParentUnitId))
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
132
PARR.DAL/Repositories/Unit/UnitInValueRepository.cs
Normal file
132
PARR.DAL/Repositories/Unit/UnitInValueRepository.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.DAL.Repositories.Unit
|
||||
{
|
||||
internal class UnitInValueRepository : IUnitInValueRepository
|
||||
{
|
||||
private readonly ILogger<UnitInValueRepository> logger;
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public UnitInValueRepository(
|
||||
ILogger<UnitInValueRepository> logger,
|
||||
DataContext dataContext
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public async Task<List<UnitInValue>> GetByUnitIdAsync(Guid unitId)
|
||||
{
|
||||
return await dataContext.UnitInValues.AsNoTracking()
|
||||
.Include(t => t.Value)
|
||||
.Where(uv => uv.UnitId == unitId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<UnitInValue>> GetByUnitIdsAsync(IEnumerable<Guid> unitIds)
|
||||
{
|
||||
return await dataContext.UnitInValues.AsNoTracking()
|
||||
.Include(t => t.Value)
|
||||
.Where(uv => unitIds.Contains(uv.UnitId))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<(string FieldName, string? Value)>> GetFieldValuesAsync(
|
||||
Guid unitId,
|
||||
IReadOnlyCollection<string> aihitNames)
|
||||
{
|
||||
if (aihitNames == null || aihitNames.Count == 0)
|
||||
return new List<(string, string?)>();
|
||||
|
||||
var keyValuePairs = await dataContext.UnitInValues
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Where(uiv => uiv.UnitId == unitId
|
||||
&& uiv.Field != null
|
||||
&& uiv.Value != null
|
||||
&& aihitNames.Contains(uiv.Field.AihitName.ToUpper()))
|
||||
.Select(uiv => new { Key = uiv.Field!.AihitName.ToUpper(), Value = uiv.Value!.Value })
|
||||
.ToListAsync();
|
||||
|
||||
// Журналируем если не нашли поля
|
||||
var foundFieldNames = keyValuePairs.Select(kvp => kvp.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var missing = aihitNames
|
||||
.Where(name => !foundFieldNames.Contains(name.ToUpper()))
|
||||
.ToList();
|
||||
if (missing.Any())
|
||||
{
|
||||
logger.LogDebug("UnitInValueService: поля не найдены для unitId={UnitId}: {Fields}",
|
||||
unitId, string.Join(", ", missing));
|
||||
}
|
||||
|
||||
return keyValuePairs.Select(kvp => (kvp.Key, kvp.Value)).ToList();
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<UnitInValue>> GetByUnitIdsAndFieldIdsAsync(IEnumerable<Guid> unitIds, IEnumerable<Guid> fieldIds)
|
||||
{
|
||||
var unitIdSet = unitIds.ToHashSet();
|
||||
var fieldIdSet = fieldIds.ToHashSet();
|
||||
|
||||
return await dataContext.UnitInValues
|
||||
.AsNoTracking()
|
||||
.Include(uv => uv.Value)
|
||||
.Where(uv => unitIdSet.Contains(uv.UnitId) && fieldIdSet.Contains(uv.FieldId))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public IQueryable<UnitInValue> Get()
|
||||
{
|
||||
return dataContext.UnitInValues;
|
||||
}
|
||||
|
||||
public async Task<string?> GetMostFrequentValueForFieldAsync(List<Guid> unitIds, string fieldName)
|
||||
{
|
||||
if (unitIds == null || !unitIds.Any() || string.IsNullOrWhiteSpace(fieldName))
|
||||
{
|
||||
logger.LogDebug("GetMostFrequentValueForFieldAsync: пустой список юнитов или имя поля. unitIds count: {Count}, fieldName: {FieldName}", unitIds?.Count ?? 0, fieldName);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.LogDebug("Поиск наиболее частого значения для поля '{FieldName}' среди {Count} юнитов.", fieldName, unitIds.Count);
|
||||
|
||||
var result = await dataContext.UnitInValues
|
||||
.AsNoTracking()
|
||||
.Where(uv =>
|
||||
unitIds.Contains(uv.UnitId)
|
||||
)
|
||||
.Join(
|
||||
dataContext.UnitFields,
|
||||
uv => uv.FieldId,
|
||||
f => f.Id,
|
||||
(uv, f) => new { uv, f }
|
||||
)
|
||||
.Where(x =>
|
||||
EF.Functions.ILike(x.f.AihitName, fieldName)
|
||||
)
|
||||
.Join(
|
||||
dataContext.UnitFieldValues,
|
||||
x => x.uv.ValueId,
|
||||
v => v.Id,
|
||||
(x, v) => new { x.uv.UnitId, v.Value }
|
||||
)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Value))
|
||||
.GroupBy(x => x.Value)
|
||||
.Select(g => new { Value = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(x => x.Count)
|
||||
.ThenBy(x => x.Value)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var mostFrequentValue = result?.Value;
|
||||
logger.LogDebug("Наиболее частое значение для поля '{FieldName}': {Value}", fieldName, mostFrequentValue);
|
||||
return mostFrequentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
PARR.DAL/Repositories/Unit/UnitKiiUnitRepository.cs
Normal file
21
PARR.DAL/Repositories/Unit/UnitKiiUnitRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.DAL.Repositories.Unit
|
||||
{
|
||||
internal class UnitKiiUnitRepository : IUnitKiiUnitRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public UnitKiiUnitRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<UnitKiiUnit> Get()
|
||||
{
|
||||
return dataContext.UnitKiiUnits;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
|
||||
namespace PARR.DAL.Repositories.Unit
|
||||
{
|
||||
internal class UnitRegionalEkPtkGroupRepository : IUnitRegionalEkPtkGroupRepository
|
||||
{
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public UnitRegionalEkPtkGroupRepository(DataContext dataContext)
|
||||
{
|
||||
this.dataContext = dataContext;
|
||||
}
|
||||
|
||||
public IQueryable<UnitRegionalEkPtkGroup> Get()
|
||||
{
|
||||
return dataContext.UnitRegionalEkPtkGroups;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
PARR.DAL/Repositories/Unit/UnitRepository.cs
Normal file
23
PARR.DAL/Repositories/Unit/UnitRepository.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
|
||||
namespace PARR.DAL.Repositories.Unit
|
||||
{
|
||||
internal class UnitRepository : BaseRepository<Domain.Entities.Unit.Unit>, IUnitRepository
|
||||
{
|
||||
public UnitRepository(DataContext dataContext, ILogger<UnitRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
|
||||
public IQueryable<Domain.Entities.Unit.Unit> GetWithIncludes()
|
||||
{
|
||||
return Get()
|
||||
.Include(t => t.UnitValues)
|
||||
.ThenInclude(t => t.Field)
|
||||
.Include(t => t.UnitValues)
|
||||
.ThenInclude(t => t.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
PARR.DAL/Repositories/UserRepository.cs
Normal file
21
PARR.DAL/Repositories/UserRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class UserRepository : BaseRepository<User>, IUserRepository
|
||||
{
|
||||
public UserRepository(DataContext dataContext, ILogger<UserRepository> logger) : base(logger, dataContext) { }
|
||||
|
||||
public async Task<User?> GetByIpWithRolesAsync(string ipAddress)
|
||||
{
|
||||
return await EntitySet
|
||||
.Include(t => t.Roles).ThenInclude(t => t.Role)
|
||||
.FirstOrDefaultAsync(t => t.Ip == ipAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
PARR.DAL/Repositories/WeekendDayRepository.cs
Normal file
108
PARR.DAL/Repositories/WeekendDayRepository.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Common.Interfaces;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.DAL.Context;
|
||||
using PARR.DAL.Contracts;
|
||||
using PARR.DAL.Repositories.Base;
|
||||
using PARR.Domain.Entities;
|
||||
|
||||
namespace PARR.DAL.Repositories
|
||||
{
|
||||
internal class WeekendDayRepository : BaseRepository<WeekendDay>, IWeekendDayRepository
|
||||
{
|
||||
private readonly IRedisCacheService redisCacheService;
|
||||
private readonly SettingsFromDb settings;
|
||||
|
||||
public WeekendDayRepository(
|
||||
DataContext dataContext,
|
||||
ILogger<WeekendDayRepository> logger,
|
||||
IRedisCacheService redisCacheService,
|
||||
SettingsFromDb settings
|
||||
) : base(logger, dataContext)
|
||||
{
|
||||
this.redisCacheService = redisCacheService;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
public IQueryable<DateOnly> GetWeekends(DateOnly start, DateOnly end)
|
||||
{
|
||||
return EntitySet.Where(t => t.Date >= start && t.Date <= end).AsNoTracking().Select(t => t.Date);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> IsWorkDayAsync(DateOnly date, bool useCache = false)
|
||||
{
|
||||
if (useCache)
|
||||
{
|
||||
// смотрим, есть ли в кэше рабочий день
|
||||
var workdayInCache = await redisCacheService.GetCachedDataAsync<DateOnly?>(GetWorkDayKey(date));
|
||||
if (workdayInCache != null)
|
||||
return true;
|
||||
|
||||
// смотрим, есть ли в кэше выходной день
|
||||
var weekendInCache = await redisCacheService.GetCachedDataAsync<DateOnly?>(GetWeekendKey(date));
|
||||
if (weekendInCache != null)
|
||||
return false;
|
||||
}
|
||||
|
||||
var weekendInDb = await EntitySet.FirstOrDefaultAsync(t => t.Date == date);
|
||||
|
||||
var isWorkday = weekendInDb == null;
|
||||
|
||||
//сохраняем всегда в КЭШ значение выходного и рабочего дня
|
||||
var cacheKey = isWorkday ? GetWorkDayKey(date) : GetWeekendKey(date);
|
||||
await redisCacheService.SetCachedDataAsync(cacheKey, date, settings.WeekendCacheTtl);
|
||||
|
||||
return isWorkday;
|
||||
}
|
||||
|
||||
|
||||
public override async Task<bool> CreateAsync(WeekendDay obj)
|
||||
{
|
||||
// При создании выходного дня, смотрим, был ли он в кэше, если был, удаляем
|
||||
await redisCacheService.DeleteCachedDataAsync(GetWeekendKey(obj.Date));
|
||||
await redisCacheService.DeleteCachedDataAsync(GetWorkDayKey(obj.Date));
|
||||
|
||||
return await base.CreateAsync(obj);
|
||||
}
|
||||
|
||||
|
||||
public override bool Delete(WeekendDay obj)
|
||||
{
|
||||
// При создании выходного дня, смотрим, был ли он в кэше, если был, удаляем
|
||||
redisCacheService.DeleteCachedData(GetWeekendKey(obj.Date));
|
||||
redisCacheService.DeleteCachedData(GetWorkDayKey(obj.Date));
|
||||
|
||||
return base.Delete(obj);
|
||||
}
|
||||
|
||||
|
||||
public override async Task<bool> DeleteAsync(Guid id)
|
||||
{
|
||||
var exist = await GetAsync(id);
|
||||
if (exist == null)
|
||||
{
|
||||
logger.LogError($"Ошибка при удалении из БД. Не найдена запись в БД с id: {id}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return Delete(exist);
|
||||
}
|
||||
|
||||
|
||||
private string GetWeekendKey(DateOnly day)
|
||||
{
|
||||
// return $"weekend_{day.ToString("yyyy-MM-dd")}";
|
||||
return redisCacheService.GetKey(new[] { "weekend", day.ToString("yyyy-MM-dd") });
|
||||
}
|
||||
|
||||
private string GetWorkDayKey(DateOnly day)
|
||||
{
|
||||
//return $"workday_{day.ToString("yyyy-MM-dd")}";
|
||||
return redisCacheService.GetKey(new[] { "workday", day.ToString("yyyy-MM-dd") });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user