/
rezvich
/
Prik
Обзор
Документация
Войти
/
rezvich
/
Prik
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
Application/AttachService.cs
434 строки
17 KB
rezvich
подправил логику
03 авг 2026, 17:51
03 авг 2026, 17:51
3ae2ecb
Код
Авторство
О чём код?
// prik/Application/AttachService.cs using Microsoft.Extensions.Logging; using prik.Domain; using prik.Infrastructure.Settings; using prik.Infrastructure.Soap; namespace prik.Application { public interface IAttachService { Task RunAsync(List<PrikRow> rows, CancellationToken ct); } public sealed class AttachService : IAttachService { private readonly ILogger<AttachService> _log; private readonly AppSettings _cfg; private readonly ISoapClient _soap; private readonly FerzlParser _parser; private readonly TimeProvider _timeProvider; public AttachService( ILogger<AttachService> log, AppSettings cfg, ISoapClient soap, FerzlParser parser, TimeProvider timeProvider) { _log = log; _cfg = cfg; _soap = soap; _parser = parser; _timeProvider = timeProvider; } public async Task RunAsync(List<PrikRow> rows, CancellationToken ct) { var target = rows.Where(r => string.IsNullOrWhiteSpace(r.Comment)).ToList(); if (target.Count == 0) { return; } var max = Math.Min(_cfg.Pipeline.MaxWorkers, 10); using var sem = new SemaphoreSlim(max, max); var groups = target .GroupBy(GetSequenceKey) .Select(group => group.OrderBy(row => row.SrcNrec).ToList()) .ToList(); var tasks = groups.Select(async group => { await sem.WaitAsync(ct); try { foreach (var row in group) { try { row.Comment = await AttachOneAsync(row, ct); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception ex) { row.Comment = "ATTACH: " + ex.Message; } } } finally { sem.Release(); } }); await Task.WhenAll(tasks); var ok = target.Count(r => r.Comment == "OK"); var err = target.Count - ok; _log.LogInformation("Attach done. total={Total} ok={Ok} err={Err}", target.Count, ok, err); } private async Task<string> AttachOneAsync(PrikRow row, CancellationToken ct) { var requiredError = ValidateRequiredFields(row); if (requiredError is not null) { return requiredError; } var today = _timeProvider.GetLocalNow().Date; if (row.AttachmentOperation == AttachmentOperationKind.UpdateExisting) { var existingStart = row.ExistingAttach?.DateAttachB; if (!existingStart.HasValue) { return "ФЛК: план обновления не содержит исходную dateAttachB из ФЕРЗЛ."; } var updateError = AttachmentBusinessRules.ValidateExistingPeriodUpdate(existingStart.Value, row.Lpudx); if (updateError is not null) { return updateError; } row.EffectiveDateAttachB = existingStart.Value.Date; return await RegisterAsync( row, dateAttachBOverride: existingStart.Value.Date, op: "registerAttach(update-existing)", ct); } if (row.AttachmentOperation == AttachmentOperationKind.ReplaceExisting) { var existingStart = row.ExistingAttach?.DateAttachB; if (!existingStart.HasValue) { return "ФЛК: план сохранения истории не содержит действующее прикрепление."; } var replacementError = AttachmentBusinessRules.ValidateReplacement( existingStart.Value, row.Lpudt!.Value, row.Lpudx, today); if (replacementError is not null) { return replacementError; } var closeResult = await CloseExistingAttachAsync(row, ct); if (!string.Equals(closeResult, "OK", StringComparison.OrdinalIgnoreCase)) { _log.LogWarning( "Close existing attach failed. New attach cancelled. enp={Enp} oldMo={OldMo} oldDep={OldDep} error={Error}", row.Enp, row.ExistingAttach?.MoCode, row.ExistingAttach?.MoDepId, closeResult); return closeResult; } row.EffectiveDateAttachB = row.Lpudt.Value.Date; return await CompleteReplacementAsync(row, ct); } var createError = AttachmentBusinessRules.ValidateNewPeriod(row.Lpudt, row.Lpudx, today); if (createError is not null) { return createError; } row.EffectiveDateAttachB = row.Lpudt!.Value.Date; return await RegisterAsync(row, dateAttachBOverride: null, op: "registerAttach(create)", ct); } private async Task<string> CompleteReplacementAsync(PrikRow row, CancellationToken ct) { var (extId, xml) = AttachXmlBuilder.BuildRegisterAttach(row); try { var result = await SendRegisterPayloadAsync(xml, extId, "registerAttach(create-after-close)", ct); return string.Equals(result, "OK", StringComparison.OrdinalIgnoreCase) ? "OK" : CriticalReplacementError(result); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception firstError) { _log.LogError( firstError, "New attach result is unknown after old attach was closed. Verify FERZL before retry. enp={Enp} mo={Mo} profile={Profile}", row.Enp, row.Lpu, row.AreaType); } await Task.Delay(TimeSpan.FromSeconds(1), ct); var firstVerification = await TryVerifyDesiredAttachmentAsync(row, ct); if (firstVerification == true) { _log.LogWarning( "New attach confirmed by FERZL after an uncertain response. enp={Enp} mo={Mo} profile={Profile}", row.Enp, row.Lpu, row.AreaType); return "OK"; } if (firstVerification is null) { return CriticalReplacementError( "результат создания неизвестен, контрольный запрос ФЕРЗЛ также завершился ошибкой; автоматический повтор не выполнен"); } _log.LogWarning( "Desired attach not found after uncertain response. Retry once with the same externalRequestId={ExtId}. enp={Enp} mo={Mo}", extId, row.Enp, row.Lpu); try { var retryResult = await SendRegisterPayloadAsync(xml, extId, "registerAttach(create-after-close-retry)", ct); return string.Equals(retryResult, "OK", StringComparison.OrdinalIgnoreCase) ? "OK" : CriticalReplacementError(retryResult); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception retryError) { _log.LogError( retryError, "Retry of new attach failed after old attach was closed. enp={Enp} mo={Mo} profile={Profile}", row.Enp, row.Lpu, row.AreaType); } await Task.Delay(TimeSpan.FromSeconds(1), ct); var finalVerification = await TryVerifyDesiredAttachmentAsync(row, ct); return finalVerification == true ? "OK" : CriticalReplacementError( finalVerification is null ? "повтор завершился с неопределённым результатом, контрольный запрос ФЕРЗЛ не выполнен" : "после повторной попытки новое действующее прикрепление не найдено в ФЕРЗЛ"); } private async Task<string> RegisterAsync( PrikRow row, DateTime? dateAttachBOverride, string op, CancellationToken ct) { var (extId, xml) = AttachXmlBuilder.BuildRegisterAttach(row, dateAttachBOverride); return await SendRegisterPayloadAsync(xml, extId, op, ct); } private async Task<string> SendRegisterPayloadAsync( string xml, string externalRequestId, string op, CancellationToken ct) { var response = await _soap.PostAsync( _cfg.Soap.AttachUrl, xml, op, externalRequestId, ct); var error = _parser.ParseErrorMessage(response); return SoapErrorRules.IsAttachLessThanYear(error) ? "OK" : error ?? "OK"; } private async Task<bool?> TryVerifyDesiredAttachmentAsync(PrikRow row, CancellationToken ct) { try { var (extId, xml) = FerzlXmlBuilder.BuildByEnp(row.Enp!.Trim()); var response = await _soap.PostAsync( _cfg.Soap.FerzlUrl, xml, "getPersonDataByENP(verify-after-close)", extId, ct, isIdempotent: true); var error = _parser.ParseErrorMessage(response); if (error is not null) { _log.LogWarning("FERZL verification returned an error: {Error}", error); return null; } var requestedProfile = row.AreaType?.ToString() ?? ""; var requestedMo = (row.Lpu ?? "").Trim(); var requestedStart = row.Lpudt!.Value.Date; return _parser.ParseAttachItems(response).Any(item => { var status = Value(item, "attachStatus"); var profile = Value(item, "areaType"); var mo = Value(item, "moCode"); var start = ParseFerzlDate(Value(item, "dateAttachB")); return status.StartsWith("Д", StringComparison.OrdinalIgnoreCase) && string.Equals(profile, requestedProfile, StringComparison.OrdinalIgnoreCase) && string.Equals(mo, requestedMo, StringComparison.OrdinalIgnoreCase) && start?.Date == requestedStart; }); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception ex) { _log.LogError(ex, "Failed to verify desired attach in FERZL. enp={Enp} mo={Mo}", row.Enp, row.Lpu); return null; } } private static string? ValidateRequiredFields(PrikRow row) { if (string.IsNullOrWhiteSpace(row.Enp)) { return "Нет ENP для отправки прикрепления в ФЕРЗЛ"; } if (!row.Lpudt.HasValue) { return "Некорректная дата lpudt (ожидается yyyymmdd, например 20251101)"; } if (string.IsNullOrWhiteSpace(row.DepId)) { return "Отсутствует ОИД структурного подразделения"; } if (string.IsNullOrWhiteSpace(row.MoId)) { return "Отсутствует ОИД МО"; } return string.IsNullOrWhiteSpace(row.Lpu) ? "Отсутствует код МО" : null; } private static string GetSequenceKey(PrikRow row) { var enp = (row.Enp ?? "").Trim(); return enp.Length == 0 ? $"row:{row.SrcNrec}" : $"{enp}|{row.AreaType}"; } private static string CriticalReplacementError(string details) => "КРИТИЧЕСКАЯ ОШИБКА: старое прикрепление закрыто, но новое не подтверждено. " + "Требуется немедленная сверка ФЕРЗЛ. " + details; private static string Value(IReadOnlyDictionary<string, string> item, string key) => item.TryGetValue(key, out var value) ? (value ?? "").Trim() : ""; private static DateTime? ParseFerzlDate(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return DateTime.TryParse( value.Trim(), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AllowWhiteSpaces, out var parsed) ? parsed.Date : null; } private async Task<string> CloseExistingAttachAsync(PrikRow row, CancellationToken ct) { var old = row.ExistingAttach ?? throw new InvalidOperationException("ExistingAttach is null."); HashSet<string> allowedAttachMethods = ["1", "2", "3"]; if (string.IsNullOrWhiteSpace(old.MoId)) { _log.LogInformation("Неправильный MoId {MoId}, меняю на {MoId}", old.MoId, row.MoId); old.MoId = row.MoId; } if (string.IsNullOrEmpty(old.AttachMethod) || !allowedAttachMethods.Contains(old.AttachMethod)) { _log.LogInformation("Неправильный AttachMethod {AttachMethod}, меняю на 1", old.AttachMethod); old.AttachMethod = "1"; } if (!row.Lpudt.HasValue) { return "Открепление невозможно: отсутствует дата нового прикрепления."; } if (!old.DateAttachB.HasValue) { return "Открепление невозможно: ФЕРЗЛ не вернул дату начала действующего прикрепления."; } var closeDate = row.Lpudt.Value.Date.AddDays(-1); if (closeDate < old.DateAttachB.Value.Date) { return $"Открепление невозможно: дата окончания {closeDate:yyyy-MM-dd} раньше даты начала текущего прикрепления {old.DateAttachB.Value:yyyy-MM-dd}."; } var (extId, xml) = AttachXmlBuilder.BuildCloseExistingAttach(row); var resp = await _soap.PostAsync( url: _cfg.Soap.AttachUrl, envelopeXml: xml, op: "registerAttach(close-old)", externalRequestId: extId, ct: ct); var err = _parser.ParseErrorMessage(resp); if (!string.IsNullOrWhiteSpace(err)) { return "Ошибка открепления текущего прикрепления: " + err; } _log.LogInformation( "Old attach closed before new attach. enp={Enp} oldMo={OldMo} oldDep={OldDep} oldDate={OldDate:yyyy-MM-dd} closeDate={CloseDate:yyyy-MM-dd}", row.Enp, old.MoCode, old.MoDepId, old.DateAttachB.Value.Date, closeDate); return "OK"; } } }