feat(api,esppScheduleSync): добавлена логика подстановки часового пояса рабочей группы ответственной за эк в расписании генератора

This commit is contained in:
Mikhail Kuznetsov
2026-01-30 12:29:37 +10:00
parent fddee13fff
commit 9f7111f2e9
2 changed files with 193 additions and 152 deletions

View File

@@ -6,6 +6,7 @@ using PARR.BLL.Services.Interfaces;
using PARR.DAL.Contracts;
using PARR.DAL.Models;
using PARR.DAL.Services.Interfaces;
using PARR.DAL.Services.Interfaces.Schedule;
using PARR.DAL.TransformServices;
using PARR.EsppScheduleSync.Domain;
using PARR.EsppScheduleSync.Settings;
@@ -21,7 +22,9 @@ namespace PARR.EsppScheduleSync
private readonly ISyncService<EsppObjectSchedule> syncService;
private readonly SettingsFromDb settingsFromDb;
private readonly IServiceProvider serviceProvider;
//private readonly INextRunModifierService nextRunModifierService;
private string noneExcludeCalendarEsppValue;
private Dictionary<string, string> responseAreaTimeOffsetDict;
public ScheduleSyncher(
ILogger<ScheduleSyncher> logger,
@@ -30,7 +33,6 @@ namespace PARR.EsppScheduleSync
ISyncService<EsppObjectSchedule> syncService,
SettingsFromDb settingsFromDb,
IServiceProvider serviceProvider
//INextRunModifierService nextRunModifierService
)
{
this.logger = logger;
@@ -39,17 +41,30 @@ namespace PARR.EsppScheduleSync
this.syncService = syncService;
this.settingsFromDb = settingsFromDb;
this.serviceProvider = serviceProvider;
//this.nextRunModifierService = nextRunModifierService;
this.responseAreaTimeOffsetDict = new Dictionary<string, string>();
this.noneExcludeCalendarEsppValue = string.Empty;
if (globalSettings.MqSettings == null)
{
logger.LogError("Нет секции настроек хранилища. MqSettings, EsppTemplates");
throw new Exception("Нет секции настроек хранилища. MqSettings, EsppTemplates");
}
if (string.IsNullOrEmpty(globalSettings.ParsingSeparator))
throw new ArgumentException("ParsingSeparator не задан в GlobalSettings.");
}
public async Task StartAsync()
{
if (string.IsNullOrEmpty(noneExcludeCalendarEsppValue))
await GetNoneExcludeCalendarEsppValueAsync();
if (responseAreaTimeOffsetDict.Count == 0)
await GetResponseAreaTimeOffsetsAsync();
var isConnected = await mqService.InitConsumerAsync(globalSettings!.MqSettings!, SyncScheduleAsync);
if (!isConnected)
@@ -58,6 +73,26 @@ namespace PARR.EsppScheduleSync
logger.LogInformation("Запущена проверка очереди {QueueName}.", globalSettings.MqSettings!.QueueName);
}
private async Task GetResponseAreaTimeOffsetsAsync()
{
using var scope = serviceProvider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IScheduleResponseAreaTimeOffsetService>();
responseAreaTimeOffsetDict = await service.Get().ToDictionaryAsync(t => t.ResponseArea, t => t.TimeOffset);
}
private async Task GetNoneExcludeCalendarEsppValueAsync()
{
using var scope = serviceProvider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IScheduleExcludeTypeService>();
var noneExcludeType = await service.Get().AsNoTracking().FirstOrDefaultAsync(t => t.Code == nameof(ScheduleExcludeTypeEnum.None));
if (noneExcludeType == null)
throw new InvalidOperationException($"Не найден тип исключения с кодом '{nameof(ScheduleExcludeTypeEnum.None)}'");
noneExcludeCalendarEsppValue = noneExcludeType.EsppValue;
}
public async Task StopAsync()
{
@@ -69,108 +104,116 @@ namespace PARR.EsppScheduleSync
private async Task SyncScheduleAsync(string str)
{
const int expectedParts = 24;
var separator = globalSettings.ParsingSeparator;
var parts = str.Split(separator);
if (parts.Length != expectedParts)
try
{
logger.LogWarning("Некорректное количество полей в строке расписания: {Actual} (ожидается {Expected})", parts.Length, expectedParts);
await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj);
return;
}
const int expectedParts = 24;
var separator = globalSettings.ParsingSeparator;
var esppScheduleId = parts[0]; // ScheduleEsppId из ЕСПП
var templateNameRaw = parts[6];
var templateName = templateNameRaw.ToUpper();
// Проверка префикса (как в ParseStrToEsppObject)
if (string.IsNullOrEmpty(settingsFromDb.TemplatePrefixWithoutVariable) ||
!templateName.Contains(settingsFromDb.TemplatePrefixWithoutVariable))
{
logger.LogWarning("Имя шаблона '{TemplateName}' не соответствует префиксу '{Prefix}'. Пропущено.", templateName, settingsFromDb.TemplatePrefixWithoutVariable);
await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj);
return;
}
// === ЕДИНЫЙ ЗАПРОС: ищем по имени ИЛИ по ScheduleEsppId ===
using var scope = serviceProvider.CreateScope();
var templateService = scope.ServiceProvider.GetRequiredService<ITemplateService>();
var candidates = await templateService.Get()
.Where(t => t.Name == templateName || t.ScheduleEsppId == esppScheduleId)
.ToListAsync();
var templateByName = candidates.FirstOrDefault(t => t.Name == templateName);
var templateByScheduleId = candidates.FirstOrDefault(t => t.ScheduleEsppId == esppScheduleId);
if (templateByName == null && templateByScheduleId == null)
{
// Нет ни по имени, ни по ID
logger.LogWarning(
"Расписание из ЕСПП не привязано ни к одному шаблону: TemplateName='{TemplateName}', ScheduleEsppId='{EsppId}'",
templateName,
esppScheduleId
);
}
else if (templateByName == null && templateByScheduleId != null)
{
// Есть только по ID → имя не совпадает
logger.LogWarning(
"Расхождение привязки: расписание из ЕСПП с ScheduleEsppId='{EsppId}' и TemplateName='{TemplateName}' " +
"соответствует шаблону в БД с именем '{DbTemplateName}', Id='{TemplateId}'.",
esppScheduleId,
templateName,
templateByScheduleId.Name,
templateByScheduleId.Id
);
}
else if (templateByName != null)
{
var dbScheduleId = templateByName.ScheduleEsppId ?? string.Empty;
if (string.IsNullOrEmpty(dbScheduleId))
var parts = str.Split(separator);
if (parts.Length != expectedParts)
{
// Утерян ID в БД
logger.LogWarning("Некорректное количество полей в строке расписания: {Actual} (ожидается {Expected})", parts.Length, expectedParts);
await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj);
return;
}
var esppScheduleId = parts[0]; // ScheduleEsppId из ЕСПП
var templateNameRaw = parts[6];
var templateName = templateNameRaw.ToUpper();
// Проверка префикса (как в ParseStrToEsppObject)
if (string.IsNullOrEmpty(settingsFromDb.TemplatePrefixWithoutVariable) ||
!templateName.Contains(settingsFromDb.TemplatePrefixWithoutVariable))
{
logger.LogWarning("Имя шаблона '{TemplateName}' не соответствует префиксу '{Prefix}'. Пропущено.", templateName, settingsFromDb.TemplatePrefixWithoutVariable);
await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj);
return;
}
// === ЕДИНЫЙ ЗАПРОС: ищем по имени ИЛИ по ScheduleEsppId ===
using var scope = serviceProvider.CreateScope();
var templateService = scope.ServiceProvider.GetRequiredService<ITemplateService>();
var candidates = await templateService.Get()
.Where(t => t.Name == templateName || t.ScheduleEsppId == esppScheduleId)
.ToListAsync();
var templateByName = candidates.FirstOrDefault(t => t.Name == templateName);
var templateByScheduleId = candidates.FirstOrDefault(t => t.ScheduleEsppId == esppScheduleId);
if (templateByName == null && templateByScheduleId == null)
{
// Нет ни по имени, ни по ID
logger.LogWarning(
"У шаблона '{TemplateName}' (Id='{TemplateId}') отсутствует ScheduleEsppId в БД, но в ЕСПП он равен '{EsppId}'",
"Расписание из ЕСПП не привязано ни к одному шаблону: TemplateName='{TemplateName}', ScheduleEsppId='{EsppId}'",
templateName,
templateByName.Id,
esppScheduleId
);
}
else if (dbScheduleId != esppScheduleId)
else if (templateByName == null && templateByScheduleId != null)
{
// ID не совпадают — проверяем, не занят ли esppScheduleId другим шаблоном
var conflictingTemplate = candidates.FirstOrDefault(t =>
t.Id != templateByName.Id && t.ScheduleEsppId == esppScheduleId);
// Есть только по ID → имя не совпадает
logger.LogWarning(
"Расхождение привязки: расписание из ЕСПП с ScheduleEsppId='{EsppId}' и TemplateName='{TemplateName}' " +
"соответствует шаблону в БД с именем '{DbTemplateName}', Id='{TemplateId}'.",
esppScheduleId,
templateName,
templateByScheduleId.Name,
templateByScheduleId.Id
);
}
else if (templateByName != null)
{
var dbScheduleId = templateByName.ScheduleEsppId ?? string.Empty;
if (conflictingTemplate != null)
if (string.IsNullOrEmpty(dbScheduleId))
{
// Утерян ID в БД
logger.LogWarning(
"Конфликт ScheduleEsppId: расписание '{EsppId}' из ЕСПП с именем '{TemplateName}' " +
"уже привязано к другому шаблону '{OtherTemplateName}' (Id='{OtherTemplateId}') в БД.",
esppScheduleId,
templateName,
conflictingTemplate.Name,
conflictingTemplate.Id
);
}
else
{
logger.LogWarning(
"Несовпадение ScheduleEsppId для шаблона '{TemplateName}' (Id='{TemplateId}'): в БД='{DbId}', в ЕСПП='{EsppId}'",
"У шаблона '{TemplateName}' (Id='{TemplateId}') отсутствует ScheduleEsppId в БД, но в ЕСПП он равен '{EsppId}'",
templateName,
templateByName.Id,
dbScheduleId,
esppScheduleId
);
}
else if (dbScheduleId != esppScheduleId)
{
// ID не совпадают — проверяем, не занят ли esppScheduleId другим шаблоном
var conflictingTemplate = candidates.FirstOrDefault(t =>
t.Id != templateByName.Id && t.ScheduleEsppId == esppScheduleId);
if (conflictingTemplate != null)
{
logger.LogWarning(
"Конфликт ScheduleEsppId: расписание '{EsppId}' из ЕСПП с именем '{TemplateName}' " +
"уже привязано к другому шаблону '{OtherTemplateName}' (Id='{OtherTemplateId}') в БД.",
esppScheduleId,
templateName,
conflictingTemplate.Name,
conflictingTemplate.Id
);
}
else
{
logger.LogWarning(
"Несовпадение ScheduleEsppId для шаблона '{TemplateName}' (Id='{TemplateId}'): в БД='{DbId}', в ЕСПП='{EsppId}'",
templateName,
templateByName.Id,
dbScheduleId,
esppScheduleId
);
}
}
}
// Передаём оригинальную строку в стандартный синхронизатор
await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при синхронизации расписания из строки: {InputString}", str);
}
// Передаём оригинальную строку в стандартный синхронизатор
await syncService.SyncEsppObjectAsync(str, ParseStrToEsppObject, ConvertDbObjToEsppObj);
}
@@ -194,7 +237,6 @@ namespace PARR.EsppScheduleSync
nextRunByAccountRobotTimeZone = nextRunModifierService.GetNextRunByAccountRobotTimeZone(template.NextRun);
}
var esppObjectFromDb = new EsppObjectSchedule
{
TemplateName = template.Name.ToUpper(),
@@ -213,7 +255,7 @@ namespace PARR.EsppScheduleSync
//BasisTime = EsppScheduleHelpers.GetGenerationTime(nextRunModifierService.GetNextRunByAccountRobotTimeZone(template.NextRun)),
Scheduled = EsppScheduleHelpers.GetNextRun(nextRunByAccountRobotTimeZone),
BasisTime = EsppScheduleHelpers.GetGenerationTime(nextRunByAccountRobotTimeZone),
Timezone = settingsFromDb.ScheduleTimezone,
Timezone = GetTimezone(template),
//Мы решили, что для всех расписаний "Отсутствует дата завершения", если что-то поменяется, тут нужно переделать
TerminationType = settingsFromDb.ScheduleRepeatRange == "Отсутствует дата завершения" ? "forever" : "",
CompleteAfter = ""
@@ -223,6 +265,31 @@ namespace PARR.EsppScheduleSync
return ClearOptionalFields(esppObjectFromDb);
}
private string GetTimezone(Template template)
{
if (template.Job?.Group?.IsWorkGroupTimezone != true)
return settingsFromDb.ScheduleTimezone;
var responseArea = template.Unit?.BaseFields?.ResponseArea;
if (string.IsNullOrEmpty(responseArea))
{
throw new InvalidOperationException(
$"У шаблона Id={template.Id}, Name='{template.Name}' не задана ResponseArea в Unit.BaseFields, " +
"но включена настройка 'использовать часовой пояс рабочей группы'.");
}
if (!responseAreaTimeOffsetDict.TryGetValue(responseArea, out var offset))
{
throw new InvalidOperationException(
$"Не найдено временное смещение для ResponseArea '{responseArea}' у шаблона Id={template.Id}, Name='{template.Name}'. " +
"Проверьте наличие записи в таблице ScheduleResponseAreaTimeOffset.");
}
return offset;
}
/// <summary>
/// Получить исключение - Календарь
@@ -476,7 +543,9 @@ namespace PARR.EsppScheduleSync
esppObject.ResponseArea = string.Empty;
esppObject.WorkGroup = string.Empty;
esppObject.CompleteAfter = string.Empty;
//esppObject.V60calendar = string.Empty;
if (esppObject.TypeV60calendar == noneExcludeCalendarEsppValue)
esppObject.V60calendar = string.Empty;
// В ЕСПП, при изменении "Повторять задачу", остаются предыдущие значения, их не нужно синхронизировать (касается только данных полученных из ЕСПП, в БД все ок)
// т.е. если стояло Ежедненвно:понедельник, а изменили например на Еженедельно..., то в ежедневно значения останутся, но будут отрабатывать значения из Еженедельно.