Add project

Basic formatting applied. Unnecessary comments have been removed. Suspicious code is covered by TODO.
This commit is contained in:
2025-08-12 11:21:10 +09:00
parent bbcbe841a7
commit 33ab055b43
546 changed files with 176950 additions and 0 deletions

View File

@ -0,0 +1,106 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.DebtRequestsApi
{
public class HcsDebtRequestsApi
{
private HcsClientConfig config;
public HcsDebtRequestsApi(HcsClientConfig config)
{
this.config = config;
}
public async Task<HcsDebtSubrequest> ExportDSRByRequestNumber(
string requestNumber, CancellationToken token = default)
{
var worker = new HcsDebtSubrequestExporter(config);
return await worker.ExportDSRByRequestNumber(requestNumber, token);
}
/// <summary>
/// Получение списка запросов о наличии задолженности направленных в данный период
/// </summary>
public async Task<int> ExportDSRsByPeriodOfSending(
DateTime startDate,
DateTime endDate,
Guid? firstSubrequestGuid,
Action<HcsDebtSubrequest> resultHandler,
CancellationToken token = default)
{
var worker = new HcsDebtSubrequestExporter(config);
return await worker.ExportDSRsByPeriodOfSending(
startDate, endDate, firstSubrequestGuid, resultHandler, token);
}
/// <summary>
/// Отправка пакета ответов на запросы о наличии задолженности
/// </summary>
public async Task<int> ImportDSRsResponsesAsOneBatch(
HcsDebtResponse[] responses,
Action<HcsDebtResponse, HcsDebtResponseResult> resultHandler,
CancellationToken token = default)
{
var worker = new HcsDebtResponseImporter(config);
var results = await worker.ImportDSRResponses(responses, token);
foreach (var response in responses)
{
var result = results.FirstOrDefault(
x => x.SubrequestGuid == response.SubrequestGuid &&
x.TransportGuid == response.TransportGuid);
if (result == null)
{
result = new HcsDebtResponseResult();
result.TransportGuid = response.TransportGuid;
result.SubrequestGuid = response.SubrequestGuid;
result.Error = new HcsException(
$"В пакете результатов приема ответов нет" +
$" результата для подзапроса {response.SubrequestGuid}");
}
resultHandler(response, result);
}
return responses.Length;
}
/// <summary>
/// Отправка ответов на запросы о наличии задолженности для списков любой длины
/// </summary>
public async Task<int> ImportDSRsResponses(
HcsDebtResponse[] responses,
Action<HcsDebtResponse, HcsDebtResponseResult> resultHandler,
CancellationToken token = default)
{
int chunkSize = 20;
int i = 0;
HcsDebtResponse[][] chunks =
responses.GroupBy(s => i++ / chunkSize).Select(g => g.ToArray()).ToArray();
int n = 0;
foreach (var chunk in chunks)
{
n += await ImportDSRsResponsesAsOneBatch(chunk, resultHandler, token);
}
return n;
}
/// <summary>
/// Отправка ответа на один запрос о наличии задолженности
/// </summary>
public async Task<HcsDebtResponseResult> ImportDSRResponse(
HcsDebtResponse response, CancellationToken token = default)
{
HcsDebtResponse[] array = { response };
HcsDebtResponseResult result = null;
await ImportDSRsResponses(array, (x, y) => result = y, token);
return result;
}
}
}

View File

@ -0,0 +1,135 @@
using Hcs.ClientApi.RemoteCaller;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DebtRequests = Hcs.Service.Async.DebtRequests.v14_5_0_1;
namespace Hcs.Service.Async.DebtRequests.v14_5_0_1
{
public partial class AckRequestAck : IHcsAck { }
public partial class getStateResult : IHcsGetStateResult { }
public partial class Fault : IHcsFault { }
public partial class HeaderType : IHcsHeaderType { }
}
namespace Hcs.ClientApi.DebtRequestsApi
{
/// Метод для отправки запросов к сервису запросов о наличии задолженности
/// Описание: http://open-gkh.ru/DebtRequestsServiceAsync/
public class HcsDebtRequestsMethod : HcsRemoteCallMethod
{
public HcsEndPoints EndPoint => HcsEndPoints.DebtRequestsAsync;
public HcsDebtRequestsMethod(HcsClientConfig config) : base(config)
{
}
public DebtRequests.RequestHeader CreateRequestHeader() =>
HcsRequestHelper.CreateHeader<DebtRequests.RequestHeader>(ClientConfig);
public System.ServiceModel.EndpointAddress RemoteAddress
=> GetEndpointAddress(HcsConstants.EndPointLocator.GetPath(EndPoint));
private DebtRequests.DebtRequestsAsyncPortClient NewPortClient()
{
var client = new DebtRequests.DebtRequestsAsyncPortClient(_binding, RemoteAddress);
ConfigureEndpointCredentials(client.Endpoint, client.ClientCredentials);
return client;
}
/// <summary>
/// Метод отправления запроса
/// </summary>
public async Task<IHcsAck> SendAsync(object request, CancellationToken token)
{
Func<Task<IHcsAck>> func = async () => await SendBareAsync(request);
return await RunRepeatableTaskInsistentlyAsync(func, token);
}
private async Task<IHcsAck> SendBareAsync(object request)
{
if (request == null) throw new ArgumentNullException("Null request");
string version = HcsRequestHelper.GetRequestVersionString(request);
_config.Log($"Отправляю {RemoteAddress.Uri}/{request.GetType().Name}" +
$" в версии {version} {ThreadIdText}...");
IHcsAck ack;
using (var client = NewPortClient())
{
switch (request)
{
case DebtRequests.exportDebtSubrequestsRequest x:
{
var response = await client.exportDebtSubrequestsAsync(x.RequestHeader, x.exportDSRsRequest);
ack = response.AckRequest.Ack;
break;
}
case DebtRequests.importResponsesRequest x:
{
var response = await client.importResponsesAsync(x.RequestHeader, x.importDSRResponsesRequest);
ack = response.AckRequest.Ack;
break;
}
default:
throw new HcsException($"Неизвестный тип запроса: {request.GetType().Name}");
}
}
_config.Log($"Запрос принят в обработку, подтверждение {ack.MessageGUID}");
return ack;
}
/// <summary>
/// Выполняет однократную проверку наличия результата.
/// Возвращает null если результата еще нет.
/// </summary>
protected override async Task<IHcsGetStateResult> TryGetResultAsync(
IHcsAck sourceAck, CancellationToken token = default)
{
Func<Task<IHcsGetStateResult>> func = async () => await TryGetResultBareAsync(sourceAck);
return await RunRepeatableTaskInsistentlyAsync(func, token);
}
private async Task<IHcsGetStateResult> TryGetResultBareAsync(IHcsAck sourceAck)
{
using (var client = NewPortClient())
{
var requestHeader = HcsRequestHelper.CreateHeader<DebtRequests.RequestHeader>(_config);
var requestBody = new DebtRequests.getStateRequest { MessageGUID = sourceAck.MessageGUID };
var response = await client.getStateAsync(requestHeader, requestBody);
var resultBody = response.getStateResult;
if (resultBody.RequestState == HcsAsyncRequestStateTypes.Ready)
{
CheckResultForErrors(resultBody);
return resultBody;
}
return null;
}
}
private void CheckResultForErrors(IHcsGetStateResult result)
{
if (result == null) throw new HcsException("Пустой result");
if (result.Items == null) throw new HcsException("Пустой result.Items");
result.Items.OfType<DebtRequests.Fault>().ToList().ForEach(x =>
{
throw HcsRemoteException.CreateNew(x.ErrorCode, x.ErrorMessage);
});
result.Items.OfType<DebtRequests.ErrorMessageType>().ToList().ForEach(x =>
{
throw HcsRemoteException.CreateNew(x.ErrorCode, x.Description);
});
}
}
}

View File

@ -0,0 +1,40 @@
using System;
namespace Hcs.ClientApi.DebtRequestsApi
{
/// <summary>
/// Ответ на запрос о наличии задолженности
/// </summary>
public class HcsDebtResponse
{
// Добавить в XML-описание
public Guid TransportGuid; // Идентификатор ответа в отправляющей системе
public Guid SubrequestGuid; // Идентификатор подзапроса
public bool HasDebt;
public HcsPersonalData[] PersonalData;
public string Description;
}
/// <summary>
/// Сведения о должнике
/// </summary>
public class HcsPersonalData
{
public string FirstName;
public string MiddleName;
public string LastName;
}
/// <summary>
/// Результат отправки ответа на запрос о наличии задолженности
/// </summary>
public class HcsDebtResponseResult
{
// Добавить в XML-описание
public Guid TransportGuid; // Идентификатор ответа в отправляющей системе
public Guid SubrequestGuid; // Идентификатор подзапроса
public Exception Error; // Ожибка отправки если указано
public DateTime UpdateDate; // Дата успешного приема ответа если не указана ошибка
public bool HasError => (Error != null);
}
}

View File

@ -0,0 +1,141 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DebtRequests = Hcs.Service.Async.DebtRequests.v14_5_0_1;
namespace Hcs.ClientApi.DebtRequestsApi
{
public class HcsDebtResponseImporter : HcsDebtRequestsMethod
{
public HcsDebtResponseImporter(HcsClientConfig config) : base(config)
{
}
public async Task<HcsDebtResponseResult[]> ImportDSRResponses(
HcsDebtResponse[] debtResponses, CancellationToken token = default)
{
if (debtResponses == null || debtResponses.Length == 0)
throw new ArgumentException("Пустой debtResponses");
var actions = debtResponses.Select(x => ConvertToImportAction(x)).ToArray();
var requestHeader = CreateRequestHeader();
var requestBody = new DebtRequests.importDSRResponsesRequest
{
Id = HcsConstants.SignedXmlElementId,
// TODO: Проверить комментарий
// Версия предустановлена в WSDL, реальная версия шаблонов дает ошибку "Bad Request"
//version = HcsConstants.DefaultHCSVersionString,
action = actions
};
var request = new DebtRequests.importResponsesRequest
{
RequestHeader = requestHeader,
importDSRResponsesRequest = requestBody
};
var ack = await SendAsync(request, token);
var result = await WaitForResultAsync(ack, true, token);
var responseResults = result.Items.Select(
x => ParseDebtResponseResultSafely(x)).ToArray();
if (debtResponses.Length != responseResults.Length)
throw new HcsException(
$"Количество направленных ответов {debtResponses.Length} не совпадает" +
$" с количеством {responseResults.Length} результатов обработки");
foreach (var response in debtResponses)
{
var found = responseResults.FirstOrDefault(x => x.TransportGuid == response.TransportGuid);
if (found != null) found.SubrequestGuid = response.SubrequestGuid;
}
return responseResults;
}
private DebtRequests.importDSRResponsesRequestAction ConvertToImportAction(
HcsDebtResponse source)
{
DebtRequests.DebtInfoType[] debtInfo = null;
if (source.HasDebt)
{
if (IsArrayEmpty(source.PersonalData)) throw new HcsException("Не указаны должники");
debtInfo = source.PersonalData.Select(x => new DebtRequests.DebtInfoType
{
person = new DebtRequests.DebtInfoTypePerson
{
firstName = x.FirstName,
lastName = x.LastName,
middleName = x.MiddleName
}
}).ToArray();
}
var responseData = new DebtRequests.ImportDSRResponseType()
{
hasDebt = source.HasDebt,
description = source.Description,
Items = debtInfo,
// TODO: Проверить комментарий
//debtInfo = debtInfo, // Так было в hcs-v13
executorGUID = ClientConfig.ExecutorGUID
};
return new DebtRequests.importDSRResponsesRequestAction()
{
subrequestGUID = source.SubrequestGuid.ToString(),
TransportGUID = source.TransportGuid.ToString(),
actionType = DebtRequests.DSRResponseActionType.Send,
responseData = responseData
};
}
private HcsDebtResponseResult ParseDebtResponseResultSafely(object resultItem)
{
try
{
return ParseDebtResponseResult(resultItem);
}
catch (Exception e)
{
return new HcsDebtResponseResult() { Error = e };
}
}
private HcsDebtResponseResult ParseDebtResponseResult(object resultItem)
{
if (resultItem == null) throw new HcsException("Пустой resultItem");
var common = resultItem as DebtRequests.CommonResultType;
if (common == null) throw new HcsException($"Неожиданный тип экземпляра ответа {resultItem.GetType()}");
if (common.Items == null || common.Items.Length == 0)
throw new HcsException("Пустой набор common.Items");
var result = new HcsDebtResponseResult();
foreach (var commonItem in common.Items)
{
if (commonItem == null) throw new HcsException("Пустой commonItem");
switch (commonItem)
{
case DebtRequests.CommonResultTypeError error:
result.Error = new HcsRemoteException(error.ErrorCode, error.Description);
break;
case DateTime updateDate:
result.UpdateDate = updateDate;
break;
default:
throw new HcsException($"Неожиданный тип сommonItem" + commonItem.GetType());
}
}
result.TransportGuid = ParseGuid(common.TransportGUID);
return result;
}
}
}

View File

@ -0,0 +1,39 @@
using System;
namespace Hcs.ClientApi.DebtRequestsApi
{
/// <summary>
/// Подзапрос о наличии задолженности за ЖКУ у организаци предоставляющей ЖКУ.
/// В терминологии ГИСЖКХ это называется Subrequests, потому что сама ГИСЖКХ выбирает организации,
/// которым (пере)направляется оригинальный запрос о наличии задолженности направленный его источником
/// в ГИСЖКХ.
/// </summary>
public class HcsDebtSubrequest
{
public enum ResponseStatusType { Sent, NotSent, AutoGenerated }
// TODO: Добавить XML-описания
public Guid SubrequestGuid; // Идентификатор подзапроса направленный конкретному поставщику ЖКУ
public Guid RequestGuid; // Идентификатор первичного запроса направленного соццентром всем поставщикам
public string RequestNumber; // Номер запроса
public DateTime SentDate; // Дата направления
public string Address; // Строка адреса из запроса
public Guid FiasHouseGuid; // Идентификатор здания в ФИАС
public Guid GisHouseGuid; // Идентификатор здания в ГИСЖКХ
public Guid HМObjectGuid; // Идентификатор помещения в ГИСЖКХ (v14)
public string HMObjectType; // Тип помещения (v14)
public string AddressDetails; // Номер помещения (не заполняется в v14)
public DateTime DebtStartDate; // Начало периода задолженности
public DateTime DebtEndDate; // Конец периода задолженности
public ResponseStatusType ResponseStatus; // Признак отправления запроса
public DateTime ResponseDate; // Дата ответа
public override string ToString()
{
return
$"ПодзапросОНЗ #{RequestNumber}" +
$" Address=[{Address}] Details=[{AddressDetails}]" +
$" HMO={HМObjectGuid} Sent={SentDate} ResponseStatus={ResponseStatus}";
}
}
}

View File

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DebtRequests = Hcs.Service.Async.DebtRequests.v14_5_0_1;
namespace Hcs.ClientApi.DebtRequestsApi
{
/// <summary>
/// Метод получения данных о направленных нам (под)запросах о наличии задолженности
/// </summary>
public class HcsDebtSubrequestExporter : HcsDebtRequestsMethod
{
public HcsDebtSubrequestExporter(HcsClientConfig config) : base(config)
{
EnableMinimalResponseWaitDelay = true;
}
public class DSRsBatch
{
public List<HcsDebtSubrequest> DebtSubrequests = new List<HcsDebtSubrequest>();
public Guid NextSubrequestGuid;
public bool LastPage;
}
public async Task<HcsDebtSubrequest> ExportDSRByRequestNumber(string requestNumber, CancellationToken token)
{
var conditionTypes = new List<DebtRequests.ItemsChoiceType5>();
var conditionValues = new List<object>();
conditionTypes.Add(DebtRequests.ItemsChoiceType5.requestNumber);
conditionValues.Add(requestNumber);
var result = await ExportSubrequestBatchByCondition(
conditionTypes.ToArray(), conditionValues.ToArray(), token);
int n = result.DebtSubrequests.Count;
if (n == 0) return null;
if (n == 1) return result.DebtSubrequests[0];
throw new HcsException(
$"По номеру запроса о наличии задолженности №{requestNumber}" +
$" получено несколько ({n}) ответов, ожидался только один");
}
public async Task<int> ExportDSRsByPeriodOfSending(
DateTime startDate, DateTime endDate, Guid? firstSubrequestGuid,
Action<HcsDebtSubrequest> resultHandler, CancellationToken token = default)
{
int numResults = 0;
Guid? nextSubrequestGuid = firstSubrequestGuid;
bool firstGuidIsReliable = false;
while (true)
{
if (numResults == 0) Log("Запрашиваем первую партию записей...");
else Log($"Запрашиваем следующую партию записей, уже получено {numResults}...");
var batch = await ExportDSRsBatchByPeriodOfSending(
startDate, endDate, nextSubrequestGuid, token, firstGuidIsReliable);
foreach (var s in batch.DebtSubrequests)
{
if (resultHandler != null) resultHandler(s);
numResults += 1;
}
if (batch.LastPage) break;
nextSubrequestGuid = batch.NextSubrequestGuid;
firstGuidIsReliable = true;
}
return numResults;
}
public async Task<DSRsBatch> ExportDSRsBatchByPeriodOfSending(
DateTime startDate, DateTime endDate, Guid? firstSubrequestGuid,
CancellationToken token, bool firstGuidIsReliable)
{
var conditionTypes = new List<DebtRequests.ItemsChoiceType5>();
var conditionValues = new List<object>();
conditionTypes.Add(DebtRequests.ItemsChoiceType5.periodOfSendingRequest);
conditionValues.Add(new DebtRequests.Period() { startDate = startDate, endDate = endDate });
if (firstSubrequestGuid != null)
{
conditionTypes.Add(DebtRequests.ItemsChoiceType5.exportSubrequestGUID);
conditionValues.Add(firstSubrequestGuid.ToString());
}
Func<Task<DSRsBatch>> taskFunc = async ()
=> await ExportSubrequestBatchByCondition(
conditionTypes.ToArray(), conditionValues.ToArray(), token);
Func<Exception, bool> canIgnoreFunc = delegate (Exception e)
{
return CanIgnoreSuchException(e, firstGuidIsReliable);
};
return await RunRepeatableTaskAsync(taskFunc, canIgnoreFunc, int.MaxValue);
}
private async Task<DSRsBatch> ExportSubrequestBatchByCondition(
DebtRequests.ItemsChoiceType5[] conditionTypes, object[] conditionValues,
CancellationToken token)
{
var requestHeader = CreateRequestHeader();
var requestBody = new DebtRequests.exportDSRsRequest
{
Id = HcsConstants.SignedXmlElementId,
// TODO: Тут напрямую указывается версия
version = "14.0.0.0",
ItemsElementName = conditionTypes,
Items = conditionValues
};
var request = new DebtRequests.exportDebtSubrequestsRequest
{
RequestHeader = requestHeader,
exportDSRsRequest = requestBody
};
var ack = await SendAsync(request, token);
try
{
var result = await WaitForResultAsync(ack, true, token);
return ParseExportResultBatch(result);
}
catch (HcsNoResultsRemoteException)
{
return new DSRsBatch() { LastPage = true };
}
}
private DSRsBatch ParseExportResultBatch(RemoteCaller.IHcsGetStateResult result)
{
var batch = new DSRsBatch();
result.Items.OfType<DebtRequests.exportDSRsResultType>().ToList().ForEach(r =>
{
Log($"Принято запросов о наличии задолженности: {r.subrequestData?.Count()}");
// на последней странице вывода может не быть ни одной записи
if (r.subrequestData != null)
{
r.subrequestData.ToList().ForEach(s => { batch.DebtSubrequests.Add(Adapt(s)); });
}
if (r.pagedOutput == null || r.pagedOutput.Item == null) batch.LastPage = true;
else
{
var item = r.pagedOutput.Item;
if (item is bool && (bool)item == true) batch.LastPage = true;
else if (!Guid.TryParse(item.ToString(), out batch.NextSubrequestGuid))
throw new HcsException($"Неожиданное значение pagedOutput [{item}]");
}
});
return batch;
}
private HcsDebtSubrequest Adapt(DebtRequests.DSRType s)
{
var dsr = new HcsDebtSubrequest();
dsr.SubrequestGuid = ParseGuid(s.subrequestGUID);
dsr.RequestGuid = ParseGuid(s.requestInfo.requestGUID);
dsr.RequestNumber = s.requestInfo.requestNumber;
dsr.SentDate = s.requestInfo.sentDate;
dsr.Address = s.requestInfo.housingFundObject.address;
var hfo = s.requestInfo.housingFundObject;
if (hfo.Items != null &&
hfo.ItemsElementName != null &&
hfo.Items.Length == hfo.ItemsElementName.Length)
{
for (int i = 0; i < hfo.Items.Length; i++)
{
string itemValue = hfo.Items[i];
switch (hfo.ItemsElementName[i])
{
case DebtRequests.ItemsChoiceType7.HMobjectGUID:
dsr.HМObjectGuid = ParseGuid(itemValue);
break;
case DebtRequests.ItemsChoiceType7.houseGUID:
dsr.GisHouseGuid = ParseGuid(itemValue);
break;
case DebtRequests.ItemsChoiceType7.adressType:
dsr.HMObjectType = itemValue;
break;
case DebtRequests.ItemsChoiceType7.addressDetails:
dsr.AddressDetails = itemValue;
break;
}
}
}
if (!string.IsNullOrEmpty(hfo.fiasHouseGUID))
{
dsr.FiasHouseGuid = ParseGuid(hfo.fiasHouseGUID);
}
// TODO: Проверить комментарий
// Из hcs-v13
//dsr.GisHouseGuid = ParseGuid(s.requestInfo.housingFundObject.houseGUID);
//dsr.AddressDetails = s.requestInfo.housingFundObject.addressDetails;
dsr.DebtStartDate = s.requestInfo.period.startDate;
dsr.DebtEndDate = s.requestInfo.period.endDate;
dsr.ResponseStatus = ConvertStatusType(s.responseStatus);
dsr.ResponseDate = s.requestInfo.responseDate;
return dsr;
}
private HcsDebtSubrequest.ResponseStatusType ConvertStatusType(DebtRequests.ResponseStatusType type)
{
switch (type)
{
case DebtRequests.ResponseStatusType.Sent: return HcsDebtSubrequest.ResponseStatusType.Sent;
case DebtRequests.ResponseStatusType.NotSent: return HcsDebtSubrequest.ResponseStatusType.NotSent;
case DebtRequests.ResponseStatusType.AutoGenerated: return HcsDebtSubrequest.ResponseStatusType.AutoGenerated;
default: throw new HcsException("Неизвестный статус отправки ответа: " + type);
}
}
// TODO: Проверить игнорирование ошибок
private bool CanIgnoreSuchException(Exception e, bool firstGuidIsReliable)
{
// "Произошла ошибка при передаче данных. Попробуйте осуществить передачу данных повторно."
if (HcsUtil.EnumerateInnerExceptions(e).Any(
x => x is HcsRemoteException && (x as HcsRemoteException).ErrorCode == "EXP001000"))
{
return true;
}
// Возникающий на больших списках отказ возобновляемый, учитывем факт что GUID был
// получен из ГИСЖКХ и явно является надежным
if (firstGuidIsReliable && HcsUtil.EnumerateInnerExceptions(e).Any(
x => x.Message != null && x.Message.Contains("Error loading content: Content not found for guid:")))
{
return true;
}
return false;
}
}
}