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,35 @@
using System;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Информация о зданиях и помещениях в зданиях (объектах жилого фонда),
/// которые связаны с договором (в договоре имеются Лицевые Счета в
/// указанных объектах)
/// </summary>
public class ГисАдресныйОбъект
{
public Guid ГуидДоговора;
public Guid ГуидВерсииДоговора;
public Guid ГуидЗданияФиас;
public Guid ГуидАдресногоОбъекта;
public class ИзвестныеТипыЗдания
{
public const string MKD = "MKD";
public const string ZHD = "ZHD";
public const string ZHDBlockZastroyki = "ZHDBlockZastroyki";
}
public string ТипЗдания;
public string НомерПомещения;
public string НомерКомнаты;
public bool СвязанСДоговором(ГисДоговор договор) => договор != null && договор.ГуидДоговора == ГуидДоговора;
public override string ToString()
{
return $"Тип=[{ТипЗдания}] ЗданиеФиас=[{ГуидЗданияФиас}] Объект[{ГуидАдресногоОбъекта}] Помещ[{НомерПомещения}] Комн[{НомерКомнаты}]";
}
}
}

View File

@ -0,0 +1,140 @@
using Newtonsoft.Json;
using System;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Договор с ресурсоснабжающей организацией
/// </summary>
public class ГисДоговор
{
public Guid ГуидДоговора;
public Guid ГуидВерсииДоговора;
public string НомерВерсии;
public ГисСостояниеДоговора СостояниеДоговора;
public ГисСтатусВерсииДоговора СтатусВерсииДоговора;
public ГисТипДоговораРСО ТипДоговораРСО;
public string НомерДоговора;
public DateTime? ДатаЗаключения;
public DateTime? ДатаВступленияВСилу;
public bool НачисленияРазмещаетРСО;
public bool ПриборыРазмещаетРСО;
public ГисКонтрагент Контрагент;
public ГисПредметДоговора[] ПредметыДоговора;
public ГисПриложение[] ПриложенияДоговора;
[JsonIgnore]
public bool ЭтоДоговорИКУ => (ТипДоговораРСО == ГисТипДоговораРСО.НеПубличныйИлиНеНежилые);
[JsonIgnore]
public bool ЭтоДоговорНежилогоПомещения => (ТипДоговораРСО == ГисТипДоговораРСО.ПубличныйИлиНежилые);
[JsonIgnore]
public bool ЭтоПроектДоговора => (СтатусВерсииДоговора == ГисСтатусВерсииДоговора.Проект);
[JsonIgnore]
public bool Расторгнут => (СтатусВерсииДоговора == ГисСтатусВерсииДоговора.Расторгнут);
[JsonIgnore]
public bool ИмеетГуидДоговора => (ГуидДоговора != default);
[JsonIgnore]
public bool ПриниматьИзГисНаАнализ
{
get
{
return СтатусВерсииДоговора switch
{
// TODO: Проверить комментирование этой строчки
//ГисСтатусВерсииДоговора.Проект => false, // С 15.11.2024 принимаем проекты
ГисСтатусВерсииДоговора.Аннулирован => false,
ГисСтатусВерсииДоговора.Расторгнут => false,
_ => true
};
}
}
public override string ToString()
{
return $"Договор №{НомерДоговора} Тип={ТипДоговораРСО}" +
$" Статус={СтатусВерсииДоговора} Состояние={СостояниеДоговора}" +
$" Заключен={HcsUtil.FormatDate(ДатаЗаключения)}" +
$" №Версии={НомерВерсии} ГуидДог={ГуидДоговора}" +
$" ГуидВерсии={ГуидВерсииДоговора}";
}
}
public enum ГисТипДоговораРСО
{
/// <summary>
/// Договор не является публичным и/или присутствует заключенный на бумажном
/// носителе (электронной форме) и/или не заключен в отношении нежилых помещений
/// в многоквартирных домах (IsContract в терминах HCS)
/// </summary>
НеПубличныйИлиНеНежилые,
/// <summary>
/// Договор является публичным и/или отсутствует заключенный на бумажном носителе
/// (в электронной форме) и/или заключен в отношении нежилых помещений в
/// многоквартирных домах (IsNotContract в терминах HCS)
/// </summary>
ПубличныйИлиНежилые
}
public enum ГисСостояниеДоговора
{
НеВступилВСилу, // NotTakeEffect
Действующий, // Proceed
ИстекСрокДействия // Expired
}
public enum ГисСтатусВерсииДоговора
{
Размещен, // Posted
Расторгнут, // Terminated
Проект, // Draft
Аннулирован // Annul
}
public class ГисПредметДоговора
{
// Вид КУ. Ссылка на НСИ "Вид коммунальной услуги" (реестровый номер 3)
public string КодНсиУслуги;
public Guid ГуидНсиУслуги;
public string ИмяНсиУслуги;
// Коммунальный ресурс. Ссылка на НСИ "Тарифицируемый ресурс" (реестровый номер 239)
public string КодНсиРесурса;
public Guid ГуидНсиРесурса;
public string ИмяНсиРесурса;
}
public class ГисПриложение
{
/// <summary>
/// Имя файла приложения
/// </summary>
public string ИмяПриложения;
/// <summary>
/// Пояснение к файлу приложения
/// </summary>
public string ОписаниеПриложения;
/// <summary>
/// ГУИД файла приложения из ext-bus-file-store-service
/// </summary>
public Guid ГуидПриложения;
/// <summary>
/// Хэш файла приложения в устаревшем стандарте "ГОСТ Р 34.11-94" в Binhex
/// </summary>
public string ХэшПриложения;
}
}

View File

@ -0,0 +1,197 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Сборный класс для получения сразу всех договоров, их лицевых счетов
/// и приборов учета привязанных к лицевым счетам договоров
/// </summary>
public class ГисДоговорыИПриборы
{
public DateTime ДатаНачалаСборки;
public DateTime ДатаКонцаСборки;
/// <summary>
/// Договоры ресурсоснабжения
/// </summary>
public List<ГисДоговор> ДоговорыРСО = new List<ГисДоговор>();
public List<ГисАдресныйОбъект> АдресаОбъектов = new List<ГисАдресныйОбъект>();
public List<ГисЗдание> Здания = new List<ГисЗдание>();
public List<ГисЛицевойСчет> ЛицевыеСчета = new List<ГисЛицевойСчет>();
public List<ГисПриборУчета> ПриборыУчета = new List<ГисПриборУчета>();
public ГисДоговор НайтиДоговорПоНомеру(string номерДоговора)
=> ДоговорыРСО.FirstOrDefault(x => x.НомерДоговора == номерДоговора);
public bool ЭтотЛицевойСчетСвязанСДоговорами(ГисЛицевойСчет лс)
{
return ДоговорыРСО.Any(договор => лс.СвязанСДоговором(договор));
}
public ГисЗдание НайтиЗданиеПомещения(Guid гуидПомещения)
{
foreach (var здание in Здания)
{
foreach (var помещение in здание.Помещения)
{
if (помещение.ГуидПомещения == гуидПомещения) return здание;
}
}
return null;
}
public ГисЗдание НайтиЗданиеЛицевогоСчета(ГисЛицевойСчет лс)
{
if (лс.Размещения == null) return null;
foreach (var размещение in лс.Размещения)
{
if (размещение.ГуидПомещения == null) continue;
var здание = НайтиЗданиеПомещения((Guid)размещение.ГуидПомещения);
if (здание != null) return здание;
}
return null;
}
public void УдалитьЛицевыеСчетаЗдания(Guid гуидЗданияФиас)
{
var здание = Здания.FirstOrDefault(x => x.ГуидЗданияФиас == гуидЗданияФиас);
if (здание == null || здание.Помещения == null) return;
var лсДляУдаления = new List<ГисЛицевойСчет>();
var гуидыПомещенийЗдания = new HashSet<Guid>(здание.Помещения.Select(x => x.ГуидПомещения));
foreach (var лс in ЛицевыеСчета)
{
if (лс.Размещения == null) continue;
foreach (var размещениеЛС in лс.Размещения)
{
if (размещениеЛС.ГуидПомещения == null) continue;
if (гуидыПомещенийЗдания.Contains((Guid)размещениеЛС.ГуидПомещения))
{
лсДляУдаления.Add(лс);
break;
}
}
}
foreach (var лсУдалить in лсДляУдаления)
ЛицевыеСчета.Remove(лсУдалить);
}
public int ЗаменитьЛицевыеСчетаЗданияВЛокальномСнимке(
Guid гуидЗданияФиас, IEnumerable<ГисЛицевойСчет> лицевые)
{
УдалитьЛицевыеСчетаЗдания(гуидЗданияФиас);
var живые = лицевые.Where(лс => лс.ДействуетСейчас && ЭтотЛицевойСчетСвязанСДоговорами(лс));
ЛицевыеСчета.AddRange(живые);
return живые.Count();
}
public bool ЭтотПриборСвязанСЛицевымиСчетами(ГисПриборУчета прибор)
{
return ЛицевыеСчета.Any(лс => прибор.СвязанСЛицевымСчетом(лс));
}
public IEnumerable<ГисАдресныйОбъект> ДатьАдресаОбъектовДоговора(ГисДоговор договор)
{
return АдресаОбъектов.Where(x => x.СвязанСДоговором(договор));
}
public IEnumerable<ГисПриборУчета> ДатьПриборыУчетаДоговора(ГисДоговор договор)
{
var адресаДоговора = ДатьАдресаОбъектовДоговора(договор).ToArray();
var лицевыеДоговора = ЛицевыеСчета.Where(x => x.СвязанСДоговором(договор)).ToArray();
var приборы = new List<ГисПриборУчета>();
foreach (var прибор in ПриборыУчета)
{
if (прибор == null) continue;
if (договор.ЭтоДоговорИКУ)
{
foreach (var адрес in адресаДоговора)
{
if (прибор.СвязанСАдреснымОбъектом(адрес)) приборы.Add(прибор);
}
}
if (договор.ЭтоДоговорНежилогоПомещения)
{
foreach (var лицевой in лицевыеДоговора)
{
if (прибор.СвязанСЛицевымСчетом(лицевой)) приборы.Add(прибор);
}
}
}
return приборы;
}
public IEnumerable<ГисПомещение> ДатьПомещенияАдресногоОбъекта(ГисАдресныйОбъект адрес)
{
var здание = Здания.FirstOrDefault(x => x.ГуидЗданияФиас == адрес.ГуидЗданияФиас);
if (здание == null) return new List<ГисПомещение>();
return здание.Помещения;
}
public IEnumerable<ГисПриборУчета> ДатьПриборыУчетаЛицевогоСчета(ГисЛицевойСчет лс)
{
return ПриборыУчета.Where(x => x.СвязанСЛицевымСчетом(лс));
}
public ГисЗданиеПомещение НайтиПомещениеЛицевогоСчета(ГисЛицевойСчет лс)
{
foreach (var размещение in лс.Размещения)
{
foreach (var здание in Здания)
{
if (здание.Помещения == null) continue;
foreach (var помещение in здание.Помещения)
{
if (помещение.ГуидПомещения == размещение.ГуидПомещения)
{
return new ГисЗданиеПомещение(здание, помещение);
}
}
}
}
return new ГисЗданиеПомещение(null, null);
}
public static ГисДоговорыИПриборы ПрочитатьФайлJson(string jsonFileName)
{
using (StreamReader file = File.OpenText(jsonFileName))
{
JsonSerializer serializer = new JsonSerializer();
return (ГисДоговорыИПриборы)serializer.Deserialize(file, typeof(ГисДоговорыИПриборы));
}
}
public void ЗаписатьФайлJson(string jsonFileName)
{
using (StreamWriter file = File.CreateText(jsonFileName))
{
JsonSerializer serializer = new JsonSerializer();
serializer.Serialize(file, this);
}
}
public override string ToString()
{
return $"ДоговорыРСО={ДоговорыРСО.Count} Адреса={АдресаОбъектов.Count}" +
$" ЛС={ЛицевыеСчета.Count} ПУ={ПриборыУчета.Count}";
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Linq;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Дом ГИС ЖКХ
/// </summary>
public class ГисЗдание
{
public ГисТипДома ТипДома;
public Guid ГуидЗданияФиас;
public string НомерДомаГис;
public ГисПомещение[] Помещения;
public override string ToString()
{
return $"{ТипДома} дом №ГИС={НомерДомаГис} Помещения={Помещения.Count()}";
}
}
public enum ГисТипДома { Многоквартирный, Жилой };
}

View File

@ -0,0 +1,8 @@
namespace Hcs.ClientApi.DataTypes
{
public record struct ГисЗданиеПомещение(ГисЗдание Здание, ГисПомещение Помещение)
{
public bool Пустое => (Здание == null || Помещение == null);
public bool Заполнено => !Пустое;
}
}

View File

@ -0,0 +1,72 @@
using Newtonsoft.Json;
using System;
using System.Linq;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Сведения о контрагенте договора РСО
/// </summary>
public class ГисКонтрагент
{
public ГисТипКонтрагента ТипКонтрагента;
/// <summary>
/// ГУИД из реестра организаций ГИС ЖКХ
/// </summary>
public Guid? ГуидОрганизации;
/// <summary>
/// ГУИД версии организации из реестра организаций ГИС ЖКХ необходим
/// для размещения Лицевого счета
/// </summary>
public Guid? ГуидВерсииОрганизации;
/// <summary>
/// Сведения об индивидуальном физическом лице
/// </summary>
public ГисИндивид Индивид;
}
public enum ГисТипКонтрагента
{
НеУказано,
ВладелецПомещения,
УправляющаяКомпания
}
public class ГисИндивид
{
public string Фамилия;
public string Имя;
public string Отчество;
public string СНИЛС;
public string НомерДокумента;
public string СерияДокумента;
public DateTime? ДатаДокумента;
[JsonIgnore]
public bool СНИЛСЗаполнен
=> !string.IsNullOrEmpty(СНИЛС);
[JsonIgnore]
public string СНИЛСТолькоЦифры
=> СНИЛСЗаполнен ? string.Concat(СНИЛС.Where(char.IsDigit)) : null;
[JsonIgnore]
public bool СНИЛСЗаполненВернойДлины
=> (СНИЛСЗаполнен && СНИЛСТолькоЦифры.Length == 11);
public void ПроверитьЗаполнениеСНИЛС()
{
if (!СНИЛСЗаполненВернойДлины)
throw new HcsException($"В СНИЛС контрагента ФЛ должно быть указано 11 цифр: {СНИЛС}");
}
public void ПроверитьЗаполнениеФИО()
{
if (string.IsNullOrEmpty(Фамилия)) throw new HcsException("Не заполнена Фамилия контрагента ФЛ");
if (string.IsNullOrEmpty(Имя)) throw new HcsException("Не заполнено Имя контрагента ФЛ");
}
}
}

View File

@ -0,0 +1,112 @@
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Text;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Лицевой счет из ГИС ЖКХ имеет номер единого лицевого счета (ЕЛС)
/// и выдается на каждую точку поставки в жилом доме для договора
/// по нежилым помещениям
/// </summary>
public class ГисЛицевойСчет
{
public Guid ГуидЛицевогоСчета;
public string НомерЛицевогоСчета;
public string НомерЕЛС;
public DateTime? ДатаСоздания;
public DateTime? ДатаЗакрытия;
public string КодНсиПричиныЗакрытия;
public string ИмяПричиныЗакрытия;
public decimal? ПолнаяПлощадь;
public decimal? ЖилаяПлощадь;
public string КодЖКУ;
public ГисРазмещениеЛС[] Размещения;
public ГисОснованиеЛС[] Основания;
public bool СвязанСДоговором(ГисДоговор договор)
{
if (договор != null && Основания != null && Основания.Any(
основание => основание.ГуидДоговора == договор.ГуидВерсииДоговора ||
основание.ГуидДоговора == договор.ГуидДоговора ||
string.Compare(основание.НомерДоговора, договор.НомерДоговора) == 0)) return true;
return false;
}
[JsonIgnore]
public bool ДействуетСейчас => (ДатаЗакрытия == null);
[JsonIgnore]
public string ОписаниеРазмещений
{
get
{
var accomod = new StringBuilder();
foreach (var x in Размещения) accomod.Append($"[{x}]");
return accomod.ToString();
}
}
[JsonIgnore]
public string ОписаниеОснований
{
get
{
if (Основания == null) return null;
var reasons = new StringBuilder();
foreach (var x in Основания) reasons.Append($"[{x}]");
return reasons.ToString();
}
}
public override string ToString()
{
return $"ЛС №{НомерЛицевогоСчета} ЕЛС={НомерЕЛС}" +
$" Создан={HcsUtil.FormatDate(ДатаСоздания)}" +
$" Закрыт={HcsUtil.FormatDate(ДатаЗакрытия)}" +
$" Размещения={ОписаниеРазмещений}" +
$" Основания={ОписаниеОснований}";
}
}
/// <summary>
/// Лицевой счет может быть привязан к нескольким размещениям.
/// Каждое размещение может быть или в здании, или в жилой комнате или в помещении
/// </summary>
public class ГисРазмещениеЛС
{
public Guid? ГуидЗдания;
public Guid? ГуидПомещения;
public Guid? ГуидЖилойКомнаты;
public decimal? ПроцентДоли;
public override string ToString()
{
if (ГуидЗдания != null) return $"Здание={ГуидЗдания}";
if (ГуидПомещения != null) return $"Помещение={ГуидПомещения}";
if (ГуидЖилойКомнаты != null) return $"ЖилКомната={ГуидЖилойКомнаты}";
return "";
}
}
public enum ГисТипОснованияЛС { ДоговорРСО, Соцнайм, Договор }
/// <summary>
/// Основание создания лицевого счета (договор на основании которого открыт ЛС)
/// </summary>
public class ГисОснованиеЛС
{
public ГисТипОснованияЛС ТипОснованияЛС;
public Guid ГуидДоговора;
public string НомерДоговора;
public override string ToString()
{
return $"{ТипОснованияЛС}={ГуидДоговора}";
}
}
}

View File

@ -0,0 +1,57 @@
using Newtonsoft.Json;
using System;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Сведения из реестра оргнаизации ГИС ЖКХ
/// </summary>
public class ГисОрганизация
{
public Guid ГуидОрганизации;
public Guid ГуидВерсииОрганизации;
public ГисТипОрганизации ТипОрганизации;
public string КраткоеИмяОрганизации;
public string ПолноеИмяОрганизации;
public bool Действующая;
public string ИНН;
public string КПП;
public string ОГРН;
public string ОКОПФ;
public string Фамилия;
public string Имя;
public string Отчество;
public string ЮридическийАдрес;
public DateTime? ДатаЛиквидации;
[JsonIgnore]
public const int ДлинаОГРН = 13;
[JsonIgnore]
public const int ДлинаОГРНИП = 15;
public override string ToString()
{
string имя = ТипОрганизации == ГисТипОрганизации.ИП ?
$"ИП {Фамилия} {Имя} {Отчество}" : КраткоеИмяОрганизации;
return $"{ТипОрганизации}: [{имя}] ИНН={ИНН} КПП={КПП} Действующая={Действующая}" +
$" ГУИД={ГуидОрганизации} Версия={ГуидВерсииОрганизации}";
}
}
public enum ГисТипОрганизации { НетУказано, ЮЛ, ИП, Филиал, Иностранный }
}

View File

@ -0,0 +1,43 @@
using System;
using System.Text;
namespace Hcs.ClientApi.DataTypes
{
public class ГисПоказания
{
public DateTime ДатаСнятия;
public string ПоказанияТ1;
public string ПоказанияТ2;
public string ПоказанияТ3;
public override string ToString()
{
var buf = new StringBuilder();
if (!string.IsNullOrEmpty(ПоказанияТ1))
{
buf.AppendFormat("Т1={0}", ПоказанияТ1);
}
if (!string.IsNullOrEmpty(ПоказанияТ2))
{
if (buf.Length > 0) buf.Append(" ");
buf.AppendFormat("Т2={0}", ПоказанияТ2);
}
if (!string.IsNullOrEmpty(ПоказанияТ3))
{
if (buf.Length > 0) buf.Append(" ");
buf.AppendFormat("Т3={0}", ПоказанияТ3);
}
if (ДатаСнятия != default)
{
if (buf.Length > 0) buf.Append(" ");
buf.AppendFormat("на {0:d}", ДатаСнятия);
}
return buf.ToString();
}
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Жилое или нежилое момещение в доме ГИС ЖКХ
/// </summary>
public class ГисПомещение
{
public Guid ГуидПомещения;
public bool ЭтоЖилоеПомещение;
public string НомерПомещения;
public DateTime? ДатаПрекращения;
public string Аннулирование;
public override string ToString()
{
return $"ГисПомещение={НомерПомещения} Жилое={ЭтоЖилоеПомещение} Guid={ГуидПомещения} Прекращено={ДатаПрекращения} Аннул={Аннулирование}";
}
}
}

View File

@ -0,0 +1,102 @@
using Newtonsoft.Json;
using System;
using System.Linq;
namespace Hcs.ClientApi.DataTypes
{
/// <summary>
/// Прибор учета из ГИС ЖКХ может быть привязан к списку зданий (ОДПУ)
/// или к списку Лицевых счетов (все виды кроме ОДПУ)
/// </summary>
public class ГисПриборУчета
{
public Guid ГуидПрибораУчета;
public Guid ГуидВерсииПрибора;
public string НомерПрибораУчетаГис;
public DateTime? ДатаРазмещенияВерсии;
public Guid ГуидВладельцаПрибора;
public ГисСтатусПрибораУчета СтатусПрибораУчета;
public ГисВидПрибораУчета ВидПрибораУчета;
public string ЗаводскойНомер;
public string МодельПрибораУчета;
[JsonIgnore]
public bool КоэффициентТрансформацииУказан => (КоэффициентТрансформации > 0);
public decimal КоэффициентТрансформации;
public string ПоказаниеТ1;
public string ПоказаниеТ2;
public string ПоказаниеТ3;
// TODO: Добавить эти комментарии как XML-комментарии
public DateTime? ДатаИзготовления; // Обязательно при импорте
public DateTime? ДатаУстановки;
public DateTime? ДатаВводаВЭксплуатацию; // Обязательно кроме ОДПУ
public DateTime? ДатаПоследнейПоверки; // Обязательно для ОДПУ
public bool РежимДистанционногоОпроса; // Признак наличия ИСУ
public string ОписаниеДистанционногоОпроса; // Наименование ИСУ
public Guid[] ГуидыЗданийФиас;
public Guid[] ГуидыЛицевыхСчетов;
public Guid[] ГуидыПомещений;
public Guid[] ГуидыЖилыхКомнат;
[JsonIgnore]
public bool ЭтоАктивный => (СтатусПрибораУчета == ГисСтатусПрибораУчета.Активный);
[JsonIgnore]
public bool ЭтоАрхивный => (СтатусПрибораУчета == ГисСтатусПрибораУчета.Архивный);
[JsonIgnore]
public bool ЭтоОДПУ => (ВидПрибораУчета == ГисВидПрибораУчета.ОДПУ);
[JsonIgnore]
public bool ЭтоНежилоеПомещение => (ВидПрибораУчета == ГисВидПрибораУчета.НежилоеПомещение);
[JsonIgnore]
public bool ЭтоПриборЮЛ => ЭтоОДПУ || ЭтоНежилоеПомещение;
public bool СвязанСЛицевымСчетом(ГисЛицевойСчет лс)
{
if (лс == null || ГуидыЛицевыхСчетов == null || ГуидыЛицевыхСчетов.Length == 0) return false;
if (ГуидыЛицевыхСчетов.Length == 1) return (ГуидыЛицевыхСчетов[0] == лс.ГуидЛицевогоСчета);
return ГуидыЛицевыхСчетов.Contains(лс.ГуидЛицевогоСчета);
}
public bool СвязанСАдреснымОбъектом(ГисАдресныйОбъект адрес)
{
// TODO: Для лицевых счетов здесь надо тестировать ГУИД помещения
if (адрес == null || ГуидыЗданийФиас == null || ГуидыЗданийФиас.Length == 0) return false;
if (ГуидыЗданийФиас.Length == 1) return ГуидыЗданийФиас[0] == адрес.ГуидЗданияФиас;
return ГуидыЗданийФиас.Contains(адрес.ГуидЗданияФиас);
}
public bool ЗаполненГуидЗданияФиас => (ГуидыЗданийФиас != null && ГуидыЗданийФиас.Length > 0);
public Guid? ОдинГуидЗданияФиас => (ЗаполненГуидЗданияФиас ? ГуидыЗданийФиас[0] : null);
public override string ToString()
{
int числоЛС = ГуидыЛицевыхСчетов != null ? ГуидыЛицевыхСчетов.Length : 0;
int числоДомов = ГуидыЗданийФиас != null ? ГуидыЗданийФиас.Length : 0;
return $"{ВидПрибораУчета} {СтатусПрибораУчета} №{ЗаводскойНомер} [{МодельПрибораУчета}]" +
$"Гуид={ГуидПрибораУчета} ЧислоЛС={числоЛС} ЧислоДомов={числоДомов}";
}
}
public enum ГисВидПрибораУчета
{
ЖилоеПомещение,
НежилоеПомещение,
ОДПУ,
ЖилойДом,
ЖилаяКомната,
КоммунальнаяКвартира
}
public enum ГисСтатусПрибораУчета { Активный, Архивный }
}

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;
}
}
}

View File

@ -0,0 +1,27 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.DeviceMeteringApi
{
/// <summary>
/// Методы ГИС ЖКХ сервиса hcs-device-metering (показания приборов учета)
/// </summary>
public class HcsDeviceMeteringApi
{
public HcsClientConfig Config { get; private set; }
public HcsDeviceMeteringApi(HcsClientConfig config)
{
this.Config = config;
}
public async Task<DateTime> РазместитьПоказания(
ГисПриборУчета прибор, ГисПоказания показания, CancellationToken token = default)
{
var method = new HcsMethodImportMeteringDevicesValues(Config);
return await method.ImportMeteringDevicesValues(прибор, показания, token);
}
}
}

View File

@ -0,0 +1,118 @@
using Hcs.ClientApi.RemoteCaller;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DeviceMetering = Hcs.Service.Async.DeviceMetering.v14_5_0_1;
namespace Hcs.Service.Async.DeviceMetering.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.DeviceMeteringApi
{
public class HcsDeviceMeteringMethod : HcsRemoteCallMethod
{
public HcsEndPoints EndPoint => HcsEndPoints.DeviceMeteringAsync;
public DeviceMetering.RequestHeader CreateRequestHeader() =>
HcsRequestHelper.CreateHeader<DeviceMetering.RequestHeader>(ClientConfig);
public HcsDeviceMeteringMethod(HcsClientConfig config) : base(config) { }
public System.ServiceModel.EndpointAddress RemoteAddress
=> GetEndpointAddress(HcsConstants.EndPointLocator.GetPath(EndPoint));
private DeviceMetering.DeviceMeteringPortTypesAsyncClient NewPortClient()
{
var client = new DeviceMetering.DeviceMeteringPortTypesAsyncClient(_binding, RemoteAddress);
ConfigureEndpointCredentials(client.Endpoint, client.ClientCredentials);
return client;
}
public async Task<IHcsGetStateResult> SendAndWaitResultAsync(
object request,
Func<DeviceMetering.DeviceMeteringPortTypesAsyncClient, Task<IHcsAck>> sender,
CancellationToken token)
{
while (true)
{
try
{
if (CanBeRestarted)
{
return await RunRepeatableTaskInsistentlyAsync(
async () => await SendAndWaitResultAsyncImpl(request, sender, token), token);
}
else
{
return await SendAndWaitResultAsyncImpl(request, sender, token);
}
}
catch (HcsRestartTimeoutException e)
{
if (!CanBeRestarted) throw new HcsException("Превышен лимит ожидания выполнения запроса", e);
Log($"Перезапускаем запрос типа {request.GetType().Name}...");
}
}
}
private async Task<IHcsGetStateResult> SendAndWaitResultAsyncImpl(
object request,
Func<DeviceMetering.DeviceMeteringPortTypesAsyncClient, Task<IHcsAck>> sender,
CancellationToken token)
{
if (request == null) throw new ArgumentNullException(nameof(request));
string version = HcsRequestHelper.GetRequestVersionString(request);
_config.Log($"Отправляем запрос: {RemoteAddress.Uri}/{request.GetType().Name} в версии {version}...");
var stopWatch = System.Diagnostics.Stopwatch.StartNew();
IHcsAck ack;
using (var client = NewPortClient())
{
ack = await sender(client);
}
stopWatch.Stop();
_config.Log($"Запрос принят в обработку за {stopWatch.ElapsedMilliseconds}мс., подтверждение {ack.MessageGUID}");
var stateResult = await WaitForResultAsync(ack, true, token);
stateResult.Items.OfType<DeviceMetering.ErrorMessageType>().ToList().ForEach(x =>
{
throw HcsRemoteException.CreateNew(x.ErrorCode, x.Description);
});
return stateResult;
}
/// <summary>
/// Выполняет однократную проверку наличия результата.
/// Возвращает null если результата еще нет.
/// </summary>
protected override async Task<IHcsGetStateResult> TryGetResultAsync(IHcsAck sourceAck, CancellationToken token)
{
using (var client = NewPortClient())
{
var requestHeader = HcsRequestHelper.CreateHeader<DeviceMetering.RequestHeader>(_config);
var requestBody = new DeviceMetering.getStateRequest { MessageGUID = sourceAck.MessageGUID };
var response = await client.getStateAsync(requestHeader, requestBody);
var resultBody = response.getStateResult;
if (resultBody.RequestState == HcsAsyncRequestStateTypes.Ready)
{
return resultBody;
}
return null;
}
}
}
}

View File

@ -0,0 +1,22 @@
using System.Text.RegularExpressions;
namespace Hcs.ClientApi.DeviceMeteringApi
{
public class HcsDeviceMeteringUtil
{
public static string ConvertMeterReading(string reading, bool isRequired)
{
if (string.IsNullOrEmpty(reading)) return (isRequired ? "0" : null);
// TODO: Проверить комментарий
// Исправляем типичный отказ ГИС в приеме показаний: заменяем запятую на точку
string betterReading = reading.Contains(",") ? reading.Replace(",", ".") : reading;
// Шаблон из: http://open-gkh.ru/MeteringDeviceBase/MeteringValueType.html
var match = Regex.Match(betterReading, "^\\d{1,15}(\\.\\d{1,7})?$");
if (match.Success) return betterReading;
throw new HcsException($"Значение показания \"{reading}\" не соответствует требованиям ГИС: N.N");
}
}
}

View File

@ -0,0 +1,76 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DeviceMetering = Hcs.Service.Async.DeviceMetering.v14_5_0_1;
namespace Hcs.ClientApi.DeviceMeteringApi
{
/// <summary>
/// Размещение в ГИС показаний прибора учета
/// http://open-gkh.ru/DeviceMetering/importMeteringDeviceValuesRequest.html
/// </summary>
public class HcsMethodImportMeteringDevicesValues : HcsDeviceMeteringMethod
{
public HcsMethodImportMeteringDevicesValues(HcsClientConfig config) : base(config)
{
CanBeRestarted = false;
}
public async Task<DateTime> ImportMeteringDevicesValues(
ГисПриборУчета прибор, ГисПоказания показания, CancellationToken token)
{
if (прибор == null) throw new ArgumentNullException(nameof(прибор));
if (показания == null) throw new ArgumentNullException(nameof(показания));
var current = new DeviceMetering.importMeteringDeviceValuesRequestMeteringDevicesValuesElectricDeviceValueCurrentValue()
{
TransportGUID = FormatGuid(Guid.NewGuid()),
DateValue = показания.ДатаСнятия,
MeteringValueT1 = HcsDeviceMeteringUtil.ConvertMeterReading(показания.ПоказанияТ1, false),
MeteringValueT2 = HcsDeviceMeteringUtil.ConvertMeterReading(показания.ПоказанияТ2, false),
MeteringValueT3 = HcsDeviceMeteringUtil.ConvertMeterReading(показания.ПоказанияТ3, false)
};
var electric = new DeviceMetering.importMeteringDeviceValuesRequestMeteringDevicesValuesElectricDeviceValue()
{
CurrentValue = current
};
var value = new DeviceMetering.importMeteringDeviceValuesRequestMeteringDevicesValues()
{
ItemElementName = DeviceMetering.ItemChoiceType.MeteringDeviceRootGUID,
Item = FormatGuid(прибор.ГуидПрибораУчета),
Item1 = electric
};
var request = new DeviceMetering.importMeteringDeviceValuesRequest()
{
Id = HcsConstants.SignedXmlElementId,
MeteringDevicesValues = [value]
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.importMeteringDeviceValuesAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
if (IsArrayEmpty(stateResult.Items)) throw new HcsException("Пустой stateResult.Items");
stateResult.Items.OfType<DeviceMetering.CommonResultTypeError>().ToList()
.ForEach(error => { throw HcsRemoteException.CreateNew(error.ErrorCode, error.Description); });
var commonResult = RequireSingleItem<DeviceMetering.CommonResultType>(stateResult.Items);
if (IsArrayEmpty(commonResult.Items)) throw new HcsException("Пустой commonResult.Items");
DateTime датаПриема = commonResult.Items.OfType<DateTime>().FirstOrDefault();
if (датаПриема == default) throw new HcsException("Сервер не вернул дату приема им показаний");
return датаПриема;
}
}
}

View File

@ -0,0 +1,394 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.FileStoreServiceApi
{
/// <summary>
/// Описание протокола в файле "ГИС ЖКХ. Альбом ТФФ 14.5.0.1.docx"
/// 2.5 Описание протокола обмена файлами с внешними системами
public class HcsFileStoreServiceApi
{
public HcsClientConfig Config { get; private set; }
/// <summary>
/// Максимальный размер в байтах части файла, которую разрешено
/// загружать на сервер по спецификации протокола
/// </summary>
private const int MAX_PART_LENGTH = 5242880;
public HcsFileStoreServiceApi(HcsClientConfig config)
{
this.Config = config;
}
/// <summary>
/// Путь к сервису хранения файлов ГИС ЖКХ на серевере API
/// </summary>
private const string ExtBusFileStoreServiceRest = "ext-bus-file-store-service/rest";
/// <summary>
/// Получение файла ранее загруженного в ГИС ЖКХ
/// </summary>
public async Task<HcsFile> DownloadFile(
Guid fileGuid, HcsFileStoreContext context, CancellationToken token)
{
long length = await GetFileLength(context, fileGuid, token);
if (length <= MAX_PART_LENGTH) return await DownloadSmallFile(fileGuid, context, token);
return await DownloadLargeFile(fileGuid, length, context, token);
}
/// <summary>
/// Получение файла по частям (не более 5Мб) по GUID файла
/// </summary>
private async Task<HcsFile> DownloadLargeFile(
Guid fileGuid, long fileSize, HcsFileStoreContext context, CancellationToken token)
{
if (fileSize <= MAX_PART_LENGTH)
throw new ArgumentException("Too short file for partial download");
string requestUri = ComposeFileDownloadUri(fileGuid, context);
var resultStream = new MemoryStream();
string resultContentType = null;
using (var client = BuildHttpClient())
{
long doneSize = 0;
while (doneSize < fileSize)
{
long remainderSize = fileSize - doneSize;
long partSize = Math.Min(remainderSize, MAX_PART_LENGTH);
long fromPosition = doneSize;
long toPosition = fromPosition + partSize - 1;
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Range = new RangeHeaderValue(fromPosition, toPosition);
var response = await SendRequestAsync(client, request, token);
resultContentType = response.Content.Headers.ContentType.ToString();
long? responseSize = response.Content.Headers.ContentLength;
if (responseSize == null || (long)responseSize != partSize)
throw new HcsException($"Получена часть файла длиной {responseSize}, а запрашивалась длина {partSize}");
using (var partStream = await response.Content.ReadAsStreamAsync())
{
partStream.Position = 0;
await partStream.CopyToAsync(resultStream);
}
doneSize += partSize;
}
resultStream.Position = 0;
return new HcsFile(null, resultContentType, resultStream);
}
}
private string ComposeFileDownloadUri(Guid fileGuid, HcsFileStoreContext context)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(fileGuid)}?getfile";
return Config.ComposeEndpointUri(endpointName);
}
/// <summary>
/// Получение файла одной частью (не более 5Мб) по GUID файла
/// </summary>
private async Task<HcsFile> DownloadSmallFile(
Guid fileGuid, HcsFileStoreContext context, CancellationToken token)
{
string requestUri = ComposeFileDownloadUri(fileGuid, context);
using (var client = BuildHttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
var response = await SendRequestAsync(client, request, token);
return new HcsFile(
null,
response.Content.Headers.ContentType.ToString(),
await response.Content.ReadAsStreamAsync());
}
}
public async Task<Guid> UploadFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
{
if (file is null) throw new ArgumentNullException(nameof(file));
if (file.Length <= MAX_PART_LENGTH)
{
return await UploadSmallFile(file, context, token);
}
else
{
return await UploadLargeFile(file, context, token);
}
}
/// <summary>
/// Отправка большого файла по частям
/// </summary>
private async Task<Guid> UploadLargeFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
{
using var client = BuildHttpClient();
if (file.Length == 0) throw new ArgumentException("Нельзя передавать файл нулевой длины");
int numParts = (int)Math.Ceiling((double)file.Length / MAX_PART_LENGTH);
Config.Log($"Запрашиваю UploadID для большого файла {file.FileName}");
Guid uploadId = await QueryNewUploadId(client, context, file, numParts, token);
Config.Log($"Получил UploadID {uploadId} для отправки файла {file.FileName} размером {file.Length} байт");
long partOffset = 0;
for (int partNumber = 1; partNumber <= numParts; partNumber++)
{
long lengthLeft = file.Length - partOffset;
int partLength = (int)Math.Min(lengthLeft, MAX_PART_LENGTH);
var partStream = new HcsPartialStream(file.Stream, partOffset, partLength);
Config.Log($"Отправляю часть {partNumber}/{numParts} размером {partLength} байт для файла {file.FileName}");
await UploadFilePart(client, context, uploadId, partNumber, partStream, token);
partOffset += partLength;
}
Config.Log($"Отправляем признак завершения передачи файла {file.FileName}");
await CompleteUpload(client, context, uploadId, token);
Config.Log($"Файл {file.FileName} успешно передан, получен код файла {uploadId}");
return uploadId;
}
/// <summary>
/// Получение кода для загрузки большого файла из нескольких частей менее 5Мб
/// </summary>
private async Task<Guid> QueryNewUploadId(
HttpClient client, HcsFileStoreContext context, HcsFile file, int numParts, CancellationToken token)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/?upload";
string requestUri = Config.ComposeEndpointUri(endpointName);
var content = new StringContent("");
content.Headers.Add("X-Upload-Filename", CleanUploadFileName(file.FileName));
content.Headers.Add("X-Upload-Length", file.Length.ToString());
content.Headers.Add("X-Upload-Part-Count", numParts.ToString());
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
request.Content = content;
var response = await SendRequestAsync(client, request, token);
return ParseUploadIdFromReponse(response);
}
private async Task UploadFilePart(
HttpClient client, HcsFileStoreContext context, Guid uploadId, int partNumber, Stream partStream,
CancellationToken token)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(uploadId)}";
string requestUri = Config.ComposeEndpointUri(endpointName);
var content = new StreamContent(partStream);
content.Headers.Add("X-Upload-Partnumber", partNumber.ToString());
content.Headers.ContentMD5 = ComputeMD5(partStream);
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
request.Content = content;
await SendRequestAsync(client, request, token);
}
private async Task CompleteUpload(HttpClient client, HcsFileStoreContext context, Guid uploadId, CancellationToken token)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(uploadId)}?completed";
string requestUri = Config.ComposeEndpointUri(endpointName);
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
await SendRequestAsync(client, request, token);
}
/// <summary>
/// Загрузка в ГИС ЖКХ файла до 5Мб размером одной операцией
/// </summary>
private async Task<Guid> UploadSmallFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
{
using var client = BuildHttpClient();
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}";
string requestUri = Config.ComposeEndpointUri(endpointName);
Config.Log($"Начинаю upload малого файла [{file.FileName}] типа [{file.ContentType}] длиной {file.Length}");
if (file.Stream.Length != file.Length)
throw new HcsException($"Длина файла {file.Length} не соответствует размеру данных {file.Stream.Length}");
file.Stream.Position = 0;
var content = new StreamContent(file.Stream);
content.Headers.Add("X-Upload-Filename", CleanUploadFileName(file.FileName));
content.Headers.ContentMD5 = ComputeMD5(file.Stream);
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
request.Content = content;
var response = await SendRequestAsync(client, request, token);
return ParseUploadIdFromReponse(response);
}
/// <summary>
/// Получение информации о загружаемом или загруженном файле
/// </summary>
public async Task<long> GetFileLength(HcsFileStoreContext context, Guid fileId, CancellationToken token)
{
using var client = BuildHttpClient();
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(fileId)}";
string requestUri = Config.ComposeEndpointUri(endpointName);
var request = new HttpRequestMessage(HttpMethod.Head, requestUri);
var response = await SendRequestAsync(client, request, token);
long length = 0;
var lengthString = SearchResponseHeader(response, "X-Upload-Length");
if (!string.IsNullOrEmpty(lengthString) && long.TryParse(lengthString, out length)) return length;
throw new HcsException("В ответе сервера не указана длина файла");
}
/// <summary>
/// Возвращает вычисленное значение AttachmentHASH для данных файла @stream
/// </summary>
public string ComputeAttachmentHash(Stream stream)
{
var client = Config as HcsClient;
if (client == null) throw new HcsException("Не доступен объект HcsClient для вычиления AttachmentHASH");
// TODO: Проверить комментарий
// В декабре 2024 у меня сломалось вычисление AttachmentHASH для файлов, я стал вычислять
// явно верным алгоритмом ГОСТ94 для больших файлов уже неверное значение суммы. В январе
// 2025 путем перебора вариантов я обнаружил что ГИСЖКХ теперь вычисляет AttachmentHASH
// только по первой части большого файла.
//int hashSourceMaxLength = MAX_PART_LENGTH;
//if (stream.Length <= hashSourceMaxLength) return client.ComputeGost94Hash(stream);
//return client.ComputeGost94Hash(new HcsPartialStream(stream, 0, hashSourceMaxLength));
// 29.01.2025 СТП ГИС ЖКХ ответила что "проведены работы" и теперь
// я вижу что они снова вычисляют AttachmantHASH по полному файлу
return client.ComputeGost94Hash(stream);
}
private async Task<HttpResponseMessage> SendRequestAsync(
HttpClient client, HttpRequestMessage request, CancellationToken token)
{
token.ThrowIfCancellationRequested();
Config.Log($"Отправляю запрос {request.Method} \"{request.RequestUri}\"...");
var response = await client.SendAsync(request, token);
if (response.IsSuccessStatusCode) return response;
throw new HcsException(DescribeResponseError(response));
}
private Guid ParseUploadIdFromReponse(HttpResponseMessage response)
{
string uploadIdheaderName = "X-Upload-UploadID";
var uploadId = SearchResponseHeader(response, uploadIdheaderName);
if (uploadId != null) return HcsUtil.ParseGuid(uploadId);
throw new HcsException($"В ответе сервера нет заголовка {uploadIdheaderName}");
}
private HttpClient BuildHttpClient()
{
var _clientHandler = new HttpClientHandler();
_clientHandler.ClientCertificates.Add(Config.Certificate);
_clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
var client = new HttpClient(_clientHandler);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Add("X-Upload-OrgPPAGUID", Config.OrgPPAGUID);
return client;
}
private string DescribeResponseError(HttpResponseMessage response)
{
string errorHeader = "X-Upload-Error";
var message = SearchResponseHeader(response, errorHeader);
if (message != null)
{
if (knownErrors.ContainsKey(message)) return $"{errorHeader}: {message} ({knownErrors[message]})";
return $"{errorHeader}: {message}";
}
return $"HTTP response status {response.StatusCode}";
}
private string SearchResponseHeader(HttpResponseMessage response, string headerName)
{
if (response.Headers.Any(x => x.Key == headerName))
{
var pair = response.Headers.First(x => x.Key == headerName);
if (pair.Value != null && pair.Value.Any())
{
return pair.Value.First();
}
}
return null;
}
private static Dictionary<string, string> knownErrors = new Dictionary<string, string>() {
{ "FieldValidationException", "не пройдены проверки на корректность заполнения полей (обязательность, формат и т.п.)" },
{ "FileNotFoundException", "не пройдены проверки на существование файла" },
{ "InvalidStatusException", "не пройдены проверки на корректный статус файла" },
{ "InvalidSizeException", "некорректный запрашиваемый размер файла" },
{ "FileVirusInfectionException", "содержимое файла инфицировано" },
{ "FileVirusNotCheckedException", "проверка на вредоносное содержимое не выполнялась" },
{ "FilePermissionException", "организация и внешняя система не имеют полномочий на скачивание файла" },
{ "DataProviderValidationException", "поставщик данных не найден, заблокирован или неактивен" },
{ "CertificateValidationException", "информационная система не найдена по отпечатку или заблокирована" },
{ "HashConflictException", "не пройдены проверки на соответствие контрольной сумме" },
{ "InvalidPartNumberException", "не пройдены проверки на номер части (номер превышает количество частей, указанных в инициализации)" },
{ "ContextNotFoundException", "неверное имя хранилища файлов" },
{ "ExtensionException", "недопустимое расширение файла" },
{ "DetectionException", "не удалось определить MIME-тип загружаемого файла" },
{ "INT002029", "сервис недоступен: выполняются регламентные работы" }
};
private byte[] ComputeMD5(System.IO.Stream stream)
{
var position = stream.Position;
var md5 = System.Security.Cryptography.MD5.Create().ComputeHash(stream);
stream.Position = position;
return md5;
}
/// <summary>
/// Готовит имя размещаемого файла для помещения в заголовок HTTP-запроса
/// </summary>
private string CleanUploadFileName(string fileName)
{
if (fileName is null) return null;
string bannedSymbols = "<>?:|*%\\\"";
var buf = new StringBuilder();
foreach (char ch in fileName)
{
if (bannedSymbols.Contains(ch)) buf.Append('_');
else buf.Append(ch);
}
// Спецификация предписывает кодировать имя файла по стандарту MIME RFC2047
// https://datatracker.ietf.org/doc/html/rfc2047
// Как имя кодировки можно явно использовать константу "windows-1251".
string characterSet = Encoding.Default.WebName;
return EncodedWord.RFC2047.Encode(
buf.ToString(), EncodedWord.RFC2047.ContentEncoding.Base64, characterSet);
}
}
}

View File

@ -0,0 +1,374 @@
//===============================================================================
// RFC2047 (Encoded Word) Decoder
// https://github.com/grumpydev/RFC2047-Encoded-Word-Encoder-Decoder/blob/master/EncodedWord/RFC2047.cs
// http://tools.ietf.org/html/rfc2047
//===============================================================================
// Copyright © Steven Robbins. All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
namespace EncodedWord
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
/// <summary>
/// Provides support for decoding RFC2047 (Encoded Word) encoded text
/// </summary>
public static class RFC2047
{
/// <summary>
/// Regex for parsing encoded word sections
/// From http://tools.ietf.org/html/rfc2047#section-3
/// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
/// </summary>
private static readonly Regex EncodedWordFormatRegEx = new Regex(@"=\?(?<charset>.*?)\?(?<encoding>[qQbB])\?(?<encodedtext>.*?)\?=", RegexOptions.Singleline | RegexOptions.Compiled);
/// <summary>
/// Regex for removing CRLF SPACE separators from between encoded words
/// </summary>
private static readonly Regex EncodedWordSeparatorRegEx = new Regex(@"\?=\r\n =\?", RegexOptions.Compiled);
/// <summary>
/// Replacement string for removing CRLF SPACE separators
/// </summary>
private const string SeparatorReplacement = @"?==?";
/// <summary>
/// The maximum line length allowed
/// </summary>
private const int MaxLineLength = 75;
/// <summary>
/// Regex for "Q-Encoding" hex bytes from http://tools.ietf.org/html/rfc2047#section-4.2
/// </summary>
private static readonly Regex QEncodingHexCodeRegEx = new Regex(@"(=(?<hexcode>[0-9a-fA-F][0-9a-fA-F]))", RegexOptions.Compiled);
/// <summary>
/// Regex for replacing _ with space as declared in http://tools.ietf.org/html/rfc2047#section-4.2
/// </summary>
private static readonly Regex QEncodingSpaceRegEx = new Regex("_", RegexOptions.Compiled);
/// <summary>
/// Format for an encoded string
/// </summary>
private const string EncodedStringFormat = @"=?{0}?{1}?{2}?=";
/// <summary>
/// Special characters, as defined by RFC2047
/// </summary>
private static readonly char[] SpecialCharacters = { '(', ')', '<', '>', '@', ',', ';', ':', '<', '>', '/', '[', ']', '?', '.', '=', '\t' };
/// <summary>
/// Represents a content encoding type defined in RFC2047
/// </summary>
public enum ContentEncoding
{
/// <summary>
/// Unknown / invalid encoding
/// </summary>
Unknown,
/// <summary>
/// "Q Encoding" (reduced character set) encoding
/// http://tools.ietf.org/html/rfc2047#section-4.2
/// </summary>
QEncoding,
/// <summary>
/// Base 64 encoding
/// http://tools.ietf.org/html/rfc2047#section-4.1
/// </summary>
Base64
}
/// <summary>
/// Encode a string into RFC2047
/// </summary>
/// <param name="plainString">Plain string to encode</param>
/// <param name="contentEncoding">Content encoding to use</param>
/// <param name="characterSet">Character set used by plainString</param>
/// <returns>Encoded string</returns>
public static string Encode(string plainString, ContentEncoding contentEncoding = ContentEncoding.QEncoding, string characterSet = "iso-8859-1")
{
if (String.IsNullOrEmpty(plainString))
{
return String.Empty;
}
if (contentEncoding == ContentEncoding.Unknown)
{
throw new ArgumentException("contentEncoding cannot be unknown for encoding.", "contentEncoding");
}
if (!IsSupportedCharacterSet(characterSet))
{
throw new ArgumentException("characterSet is not supported", "characterSet");
}
var textEncoding = Encoding.GetEncoding(characterSet);
var encoder = GetContentEncoder(contentEncoding);
var encodedContent = encoder.Invoke(plainString, textEncoding);
return BuildEncodedString(characterSet, contentEncoding, encodedContent);
}
/// <summary>
/// Decode a string containing RFC2047 encoded sections
/// </summary>
/// <param name="encodedString">String contaning encoded sections</param>
/// <returns>Decoded string</returns>
public static string Decode(string encodedString)
{
// Remove separators
var decodedString = EncodedWordSeparatorRegEx.Replace(encodedString, SeparatorReplacement);
return EncodedWordFormatRegEx.Replace(
decodedString,
m =>
{
var contentEncoding = GetContentEncodingType(m.Groups["encoding"].Value);
if (contentEncoding == ContentEncoding.Unknown)
{
// Regex should never match, but return anyway
return string.Empty;
}
var characterSet = m.Groups["charset"].Value;
if (!IsSupportedCharacterSet(characterSet))
{
// Fall back to iso-8859-1 if invalid/unsupported character set found
characterSet = @"iso-8859-1";
}
var textEncoding = Encoding.GetEncoding(characterSet);
var contentDecoder = GetContentDecoder(contentEncoding);
var encodedText = m.Groups["encodedtext"].Value;
return contentDecoder.Invoke(encodedText, textEncoding);
});
}
/// <summary>
/// Determines if a character set is supported
/// </summary>
/// <param name="characterSet">Character set name</param>
/// <returns>Bool representing whether the character set is supported</returns>
private static bool IsSupportedCharacterSet(string characterSet)
{
return Encoding.GetEncodings()
.Where(e => String.Equals(e.Name, characterSet, StringComparison.InvariantCultureIgnoreCase))
.Any();
}
/// <summary>
/// Gets the content encoding type from the encoding character
/// </summary>
/// <param name="contentEncodingCharacter">Content contentEncodingCharacter character</param>
/// <returns>ContentEncoding type</returns>
private static ContentEncoding GetContentEncodingType(string contentEncodingCharacter)
{
switch (contentEncodingCharacter)
{
case "Q":
case "q":
return ContentEncoding.QEncoding;
case "B":
case "b":
return ContentEncoding.Base64;
default:
return ContentEncoding.Unknown;
}
}
/// <summary>
/// Gets the content decoder delegate for the given content encoding type
/// </summary>
/// <param name="contentEncoding">Content encoding type</param>
/// <returns>Decoding delegate</returns>
private static Func<string, Encoding, string> GetContentDecoder(ContentEncoding contentEncoding)
{
switch (contentEncoding)
{
case ContentEncoding.Base64:
return DecodeBase64;
case ContentEncoding.QEncoding:
return DecodeQEncoding;
default:
// Will never get here, but return a "null" delegate anyway
return (s, e) => String.Empty;
}
}
/// <summary>
/// Gets the content encoder delegate for the given content encoding type
/// </summary>
/// <param name="contentEncoding">Content encoding type</param>
/// <returns>Encoding delegate</returns>
private static Func<string, Encoding, string> GetContentEncoder(ContentEncoding contentEncoding)
{
switch (contentEncoding)
{
case ContentEncoding.Base64:
return EncodeBase64;
case ContentEncoding.QEncoding:
return EncodeQEncoding;
default:
// Will never get here, but return a "null" delegate anyway
return (s, e) => String.Empty;
}
}
/// <summary>
/// Decodes a base64 encoded string
/// </summary>
/// <param name="encodedText">Encoded text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Decoded string</returns>
private static string DecodeBase64(string encodedText, Encoding textEncoder)
{
var encodedBytes = Convert.FromBase64String(encodedText);
return textEncoder.GetString(encodedBytes);
}
/// <summary>
/// Encodes a base64 encoded string
/// </summary>
/// <param name="plainText">Plain text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Encoded string</returns>
private static string EncodeBase64(string plainText, Encoding textEncoder)
{
var plainTextBytes = textEncoder.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
}
/// <summary>
/// Decodes a "Q encoded" string
/// </summary>
/// <param name="encodedText">Encoded text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Decoded string</returns>
private static string DecodeQEncoding(string encodedText, Encoding textEncoder)
{
var decodedText = QEncodingSpaceRegEx.Replace(encodedText, " ");
decodedText = QEncodingHexCodeRegEx.Replace(
decodedText,
m =>
{
var hexString = m.Groups["hexcode"].Value;
int characterValue;
if (!int.TryParse(hexString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue))
{
return String.Empty;
}
return textEncoder.GetString(new[] { (byte)characterValue });
});
return decodedText;
}
/// <summary>
/// Encodes a "Q encoded" string
/// </summary>
/// <param name="plainText">Plain text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Encoded string</returns>
private static string EncodeQEncoding(string plainText, Encoding textEncoder)
{
if (textEncoder.GetByteCount(plainText) != plainText.Length)
{
throw new ArgumentException("Q encoding only supports single byte encodings", "textEncoder");
}
var specialBytes = textEncoder.GetBytes(SpecialCharacters);
var sb = new StringBuilder(plainText.Length);
var plainBytes = textEncoder.GetBytes(plainText);
// Replace "high" values
for (int i = 0; i < plainBytes.Length; i++)
{
if (plainBytes[i] <= 127 && !specialBytes.Contains(plainBytes[i]))
{
sb.Append(Convert.ToChar(plainBytes[i]));
}
else
{
sb.Append("=");
sb.Append(Convert.ToString(plainBytes[i], 16).ToUpper());
}
}
return sb.ToString().Replace(" ", "_");
}
/// <summary>
/// Builds the full encoded string representation
/// </summary>
/// <param name="characterSet">Characterset to use</param>
/// <param name="contentEncoding">Content encoding to use</param>
/// <param name="encodedContent">Content, encoded to the above parameters</param>
/// <returns>Valid RFC2047 string</returns>
private static string BuildEncodedString(string characterSet, ContentEncoding contentEncoding, string encodedContent)
{
var encodingCharacter = String.Empty;
switch (contentEncoding)
{
case ContentEncoding.Base64:
encodingCharacter = "B";
break;
case ContentEncoding.QEncoding:
encodingCharacter = "Q";
break;
}
var wrapperLength = string.Format(EncodedStringFormat, characterSet, encodingCharacter, String.Empty).Length;
var chunkLength = MaxLineLength - wrapperLength;
if (encodedContent.Length <= chunkLength)
{
return string.Format(EncodedStringFormat, characterSet, encodingCharacter, encodedContent);
}
var sb = new StringBuilder();
foreach (var chunk in SplitStringByLength(encodedContent, chunkLength))
{
sb.AppendFormat(EncodedStringFormat, characterSet, encodingCharacter, chunk);
sb.Append("\r\n ");
}
return sb.ToString();
}
/// <summary>
/// Splits a string into chunks
/// </summary>
/// <param name="inputString">Input string</param>
/// <param name="chunkSize">Size of each chunk</param>
/// <returns>String collection of chunked strings</returns>
public static IEnumerable<string> SplitStringByLength(this string inputString, int chunkSize)
{
for (int index = 0; index < inputString.Length; index += chunkSize)
{
yield return inputString.Substring(index, Math.Min(chunkSize, inputString.Length - index));
}
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace Hcs.ClientApi
{
public class HcsActionLogger : IHcsLogger
{
private Action<string> logger;
public HcsActionLogger(Action<string> logger)
{
this.logger = logger;
}
public void WriteLine(string message)
{
logger(message);
}
}
}

View File

@ -0,0 +1,96 @@
using GostCryptography.Base;
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
namespace Hcs.ClientApi
{
public static class HcsCertificateHelper
{
public static bool IsGostPrivateKey(this X509Certificate2 certificate)
{
try
{
if (certificate.HasPrivateKey)
{
var cspInfo = certificate.GetPrivateKeyInfo();
if (cspInfo.ProviderType == (int)ProviderType.CryptoPro ||
cspInfo.ProviderType == (int)ProviderType.VipNet ||
cspInfo.ProviderType == (int)ProviderType.CryptoPro_2012_512 ||
cspInfo.ProviderType == (int)ProviderType.CryptoPro_2012_1024)
return true;
else
return false;
}
return false;
}
catch
{
return false;
}
}
public static Hcs.GostXades.CryptoProviderTypeEnum GetProviderType(this X509Certificate2 certificate)
{
return (Hcs.GostXades.CryptoProviderTypeEnum)GetProviderInfo(certificate).Item1;
}
public static Tuple<int, string> GetProviderInfo(this X509Certificate2 certificate)
{
if (certificate.HasPrivateKey)
{
var cspInfo = certificate.GetPrivateKeyInfo();
return new Tuple<int, string>(cspInfo.ProviderType, cspInfo.ProviderName);
}
else
throw new Exception("Certificate has no private key");
}
public static X509Certificate2 FindCertificate(Func<X509Certificate2, bool> predicate)
{
if (predicate == null) throw new ArgumentException("Null subject predicate");
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
var collection = store.Certificates
.OfType<X509Certificate2>()
.Where(x => x.HasPrivateKey && x.IsGostPrivateKey());
var now = DateTime.Now;
return collection.First(
x => now >= x.NotBefore && now <= x.NotAfter && predicate(x));
}
finally
{
store.Close();
}
}
public static X509Certificate2 ShowCertificateUI()
{
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
var collection = store.Certificates
.OfType<X509Certificate2>()
.Where(x => x.HasPrivateKey && x.IsGostPrivateKey());
string prompt = "Выберите сертификат";
var cert = X509Certificate2UI.SelectFromCollection(
new X509Certificate2Collection(
collection.ToArray()), prompt, "", X509SelectionFlag.SingleSelection)[0];
return cert;
}
finally
{
store.Close();
}
}
}
}

View File

@ -0,0 +1,70 @@
using GostCryptography.Gost_R3411;
using Hcs.ClientApi.DebtRequestsApi;
using Hcs.ClientApi.DeviceMeteringApi;
using Hcs.ClientApi.FileStoreServiceApi;
using Hcs.ClientApi.HouseManagementApi;
using Hcs.ClientApi.OrgRegistryCommonApi;
using Hcs.ClientApi.RemoteCaller;
using System;
using System.Security.Cryptography.X509Certificates;
namespace Hcs.ClientApi
{
/// <summary>
/// Единый клиент для вызова всех реализованных функций интеграции с ГИС ЖКХ
/// </summary>
public class HcsClient : HcsClientConfig
{
public HcsClient()
{
HcsServicePointConfig.InitConfig();
// TODO: Вынести в конфиг
// Роль поставщика информации по умолчанию
Role = HcsOrganizationRoles.RSO;
}
public void SetSigningCertificate(X509Certificate2 cert, string pin = null)
{
if (cert == null) throw new ArgumentNullException("Не указан сертификат для подписания данных");
if (pin == null) pin = HcsConstants.DefaultCertificatePin;
Certificate = cert;
CertificateThumbprint = cert.Thumbprint;
CertificatePassword = pin;
CryptoProviderType = cert.GetProviderType();
}
public HcsDebtRequestsApi DebtRequests => new HcsDebtRequestsApi(this);
public HcsHouseManagementApi HouseManagement => new HcsHouseManagementApi(this);
public HcsOrgRegistryCommonApi OrgRegistryCommon => new HcsOrgRegistryCommonApi(this);
public HcsFileStoreServiceApi FileStoreService => new HcsFileStoreServiceApi(this);
public HcsDeviceMeteringApi DeviceMeteringService => new HcsDeviceMeteringApi(this);
public X509Certificate2 FindCertificate(Func<X509Certificate2, bool> predicate)
{
return HcsCertificateHelper.FindCertificate(predicate);
}
public X509Certificate2 ShowCertificateUI()
{
return HcsCertificateHelper.ShowCertificateUI();
}
/// <summary>
/// Производит для потока хэш по алгоритму "ГОСТ Р 34.11-94" в строке binhex
/// </summary>
public string ComputeGost94Hash(System.IO.Stream stream)
{
// API HouseManagement указывает, что файлы приложенные к договору должны размещаться
// с AttachmentHASH по стандарту ГОСТ. Оказывается, ГИСЖКХ требует применения устаревшего
// алгоритма ГОСТ Р 34.11-94 (соответствует `rhash --gost94-cryptopro file` в linux).
using var algorithm = new Gost_R3411_94_HashAlgorithm(GostCryptoProviderType);
var savedPosition = stream.Position;
stream.Position = 0;
var hashValue = HcsUtil.ConvertToHexString(algorithm.ComputeHash(stream));
stream.Position = savedPosition;
return hashValue;
}
}
}

View File

@ -0,0 +1,95 @@
using System.Security.Cryptography.X509Certificates;
namespace Hcs.ClientApi
{
/// <summary>
/// Конфигурация клиента
/// </summary>
public class HcsClientConfig
{
/// <summary>
/// Идентификатор поставщика данных ГИС
/// </summary>
public string OrgPPAGUID { get; set; }
/// <summary>
/// Идентификатор организации в ГИС
/// </summary>
public string OrgEntityGUID { get; set; }
/// <summary>
/// Тип криптопровайдера полученный из сертификата
/// </summary>
public Hcs.GostXades.CryptoProviderTypeEnum CryptoProviderType { get; internal set; }
public GostCryptography.Base.ProviderType GostCryptoProviderType =>
(GostCryptography.Base.ProviderType)CryptoProviderType;
/// <summary>
/// Сертификат клиента для применения при формировании запросов
/// </summary>
public X509Certificate2 Certificate { get; internal set; }
/// <summary>
/// Отпечаток сертификата
/// </summary>
public string CertificateThumbprint { get; internal set; }
/// <summary>
/// Пароль доступа к сертификату
/// </summary>
public string CertificatePassword { get; internal set; }
/// <summary>
/// Исполнитель/сотрудник ГИСЖКХ от которого будут регистрироваться ответы
/// </summary>
public string ExecutorGUID { get; set; }
/// <summary>
/// Признак - указывает на то, что используется внешний туннель (stunnel)
/// </summary>
public bool UseTunnel { get; set; }
/// <summary>
/// true - использовать адреса ППАК стенда иначе СИТ
/// </summary>
public bool IsPPAK { get; set; }
/// <summary>
/// Роль
/// </summary>
public HcsOrganizationRoles Role { get; set; }
/// <summary>
/// Устанавливаемый пользователем приемник отладочных сообщений
/// </summary>
public IHcsLogger Logger { get; set; }
/// <summary>
/// Выводит сообщение в установленный приемник отладочных сообщений
/// </summary>
public void Log(string message) => Logger?.WriteLine(message);
/// <summary>
/// Устанавливаемый пользователем механизм перехвата содержимого отправляемых
/// и принимаемых пакетов
/// </summary>
public IHcsMessageCapture MessageCapture;
/// <summary>
/// Отправляет тело сообщения в установленный перехватчик
/// </summary>
public void MaybeCaptureMessage(bool sent, string messageBody)
=> MessageCapture?.CaptureMessage(sent, messageBody);
public string ComposeEndpointUri(string endpointName)
{
if (UseTunnel)
return $"http://{HcsConstants.Address.UriTunnel}/{endpointName}";
return IsPPAK ?
$"https://{HcsConstants.Address.UriPPAK}/{endpointName}"
: $"https://{HcsConstants.Address.UriSIT01}/{endpointName}";
}
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace Hcs.ClientApi
{
public class HcsConsoleLogger : IHcsLogger
{
public void WriteLine(string message)
{
Console.WriteLine(message);
}
}
}

View File

@ -0,0 +1,119 @@
using System.Collections.Generic;
namespace Hcs.ClientApi
{
public static class HcsConstants
{
/// <summary>
/// Имя XML-элемента в сообщении, которое будет подписываться в фильтре
/// отправки подписывающем XML
/// </summary>
public const string SignedXmlElementId = "signed-data-container";
/// <summary>
/// Если PIN сертификата не указан пользователем, применяется это значение
/// по умолчанию для сертификатов RuToken
/// </summary>
public const string DefaultCertificatePin = "12345678";
public static class Address
{
public const string UriPPAK = "api.dom.gosuslugi.ru";
public const string UriSIT01 = "sit01.dom.test.gosuslugi.ru:10081";
public const string UriSIT02 = "sit02.dom.test.gosuslugi.ru:10081";
public const string UriTunnel = "127.0.0.1:8080";
}
public static class EndPointLocator
{
static Dictionary<HcsEndPoints, string> _endPoints;
static EndPointLocator()
{
if (_endPoints == null)
_endPoints = new Dictionary<HcsEndPoints, string>();
_endPoints.Add(HcsEndPoints.BillsAsync, "ext-bus-bills-service/services/BillsAsync");
_endPoints.Add(HcsEndPoints.DeviceMetering, "ext-bus-device-metering-service/services/DeviceMetering");
_endPoints.Add(HcsEndPoints.DeviceMeteringAsync, "ext-bus-device-metering-service/services/DeviceMeteringAsync");
_endPoints.Add(HcsEndPoints.HomeManagement, "ext-bus-home-management-service/services/HomeManagement");
_endPoints.Add(HcsEndPoints.HomeManagementAsync, "ext-bus-home-management-service/services/HomeManagementAsync");
_endPoints.Add(HcsEndPoints.DebtRequestsAsync, "ext-bus-debtreq-service/services/DebtRequestsAsync");
_endPoints.Add(HcsEndPoints.Licenses, "ext-bus-licenses-service/services/Licenses");
_endPoints.Add(HcsEndPoints.LicensesAsync, "ext-bus-licenses-service/services/LicensesAsync");
_endPoints.Add(HcsEndPoints.Nsi, "ext-bus-nsi-service/services/Nsi");
_endPoints.Add(HcsEndPoints.NsiAsync, "ext-bus-nsi-service/services/NsiAsync");
_endPoints.Add(HcsEndPoints.NsiCommon, "ext-bus-nsi-common-service/services/NsiCommon");
_endPoints.Add(HcsEndPoints.NsiCommonAsync, "ext-bus-nsi-common-service/services/NsiCommonAsync");
_endPoints.Add(HcsEndPoints.OrgRegistryCommon, "ext-bus-org-registry-common-service/services/OrgRegistryCommon");
_endPoints.Add(HcsEndPoints.OrgRegistryCommonAsync, "ext-bus-org-registry-common-service/services/OrgRegistryCommonAsync");
_endPoints.Add(HcsEndPoints.OrgRegistry, "ext-bus-org-registry-service/services/OrgRegistry");
_endPoints.Add(HcsEndPoints.OrgRegistryAsync, "ext-bus-org-registry-service/services/OrgRegistryAsync");
_endPoints.Add(HcsEndPoints.PaymentsAsync, "ext-bus-payment-service/services/PaymentAsync");
}
public static string GetPath(HcsEndPoints endPoint)
{
return _endPoints[endPoint];
}
}
public static class UserAuth
{
public const string Name = "sit";
public const string Passwd = "xw{p&&Ee3b9r8?amJv*]";
}
}
/// <summary>
/// Имена конечных точек
/// </summary>
public enum HcsEndPoints
{
OrgRegistry,
OrgRegistryAsync,
OrgRegistryCommon,
OrgRegistryCommonAsync,
NsiCommon,
NsiCommonAsync,
Nsi,
NsiAsync,
HomeManagement,
HomeManagementAsync,
DebtRequestsAsync,
Bills,
BillsAsync,
Licenses,
LicensesAsync,
DeviceMetering,
DeviceMeteringAsync,
PaymentsAsync
}
/// <summary>
/// Роли организаций в ГИС
/// </summary>
public enum HcsOrganizationRoles
{
/// <summary>
/// УК/ТСЖ/ЖСК
/// </summary>
UK,
/// <summary>
/// Ресурсоснабжающая организация
/// </summary>
RSO,
/// <summary>
/// Расчетный центр
/// </summary>
RC,
}
public class HcsAsyncRequestStateTypes
{
public const int Received = 1;
public const int InProgress = 2;
public const int Ready = 3;
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Linq;
namespace Hcs.ClientApi
{
public class HcsException : Exception
{
public HcsMemoryMessageCapture MessageCapture { get; private set; }
public HcsException(string message) : base(message)
{
}
public HcsException(string message, Exception nestedException) : base(message, nestedException)
{
}
public HcsException(string message, HcsMemoryMessageCapture capture, Exception nestedException)
: base(message, nestedException)
{
MessageCapture = capture;
}
public static HcsException FindHcsException(Exception e)
{
return HcsUtil.EnumerateInnerExceptions(e).OfType<HcsException>().FirstOrDefault();
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.IO;
using System.Linq;
namespace Hcs.ClientApi
{
public class HcsFile
{
public string FileName { get; private set; }
public string ContentType { get; private set; }
public Stream Stream { get; private set; }
public long Length => Stream.Length;
public HcsFile(string fileName, string contentType, Stream stream)
{
FileName = fileName;
ContentType = contentType ?? throw new ArgumentNullException(nameof(ContentType));
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
}
/// <summary>
/// По имени файла возвращает строку MIME Content-Type или null если тип MIME не найден
/// </summary>
public static string GetMimeContentTypeForFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return null;
string extension = Path.GetExtension(fileName).ToLower();
var mimeType = AllowedMimeTypes.FirstOrDefault(x => x.Extension == extension);
if (mimeType == null) return null;
return mimeType.ContentType;
}
public record struct MimeType(string Extension, string ContentType);
/// <summary>
/// Типы MIME допустимые для загрузки в ГИС ЖКХ
/// </summary>
public static MimeType[] AllowedMimeTypes =
{
new MimeType(".pdf", "application/pdf"),
new MimeType(".xls", "application/excel"),
new MimeType(".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
new MimeType(".doc", "application/msword"),
new MimeType(".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
new MimeType(".rtf", "application/rtf"),
new MimeType(".jpg", "image/jpeg"),
new MimeType(".jpeg", "image/jpeg"),
new MimeType(".tif", "image/tiff"),
new MimeType(".tiff", "image/tiff")
// TODO: Проверить комментарий
// В спецификации есть другие типы файлов .zip, .sgn и т.д.
};
}
}

View File

@ -0,0 +1,37 @@
namespace Hcs.ClientApi
{
/// <summary>
/// Раздел хранилища файлов (Attachment) из документации: "ГИС ЖКХ. Альбом ТФФ 14.5.0.1.docx"
/// 2.6 Перечень контекстов хранилищ функциональных подсистем
/// </summary>
public enum HcsFileStoreContext
{
/// <summary>
/// Управление домами. Лицевые счета.
/// </summary>
homemanagement,
/// <summary>
/// Управление контентом
/// </summary>
contentmanagement,
/// <summary>
/// Электронные счета
/// </summary>
bills,
/// <summary>
/// Запросов о наличии задолженности по оплате ЖКУ
/// </summary>
debtreq
}
public static class HcsFileStoreContextExtensions
{
public static string GetName(this HcsFileStoreContext context)
{
return context.ToString();
}
}
}

View File

@ -0,0 +1,45 @@
using System.Text;
namespace Hcs.ClientApi
{
/// <summary>
/// Реализация механизма захвата содержимого сообщений SOAP записывающая
/// каждое сообщение в отдельный файл на диске
/// </summary>
public class HcsFileWriterMessageCapture : IHcsMessageCapture
{
private string directory;
private IHcsLogger logger;
public HcsFileWriterMessageCapture()
{
}
public HcsFileWriterMessageCapture(string directory, IHcsLogger logger)
{
this.directory = directory;
this.logger = logger;
}
public void CaptureMessage(bool sent, string body)
{
int index = 0;
int maxIndex = 1000000;
string fileName;
do
{
index += 1;
if (index > maxIndex) throw new HcsException("Превышен максимум индекса файлов захвата сообщений");
fileName = index.ToString("D3") + "_" + (sent ? "message" : "response") + ".xml";
if (!string.IsNullOrEmpty(directory))
{
fileName = System.IO.Path.Combine(directory, fileName);
}
} while (System.IO.File.Exists(fileName));
if (logger != null) logger.WriteLine($"Writing message file: {fileName}...");
System.IO.File.WriteAllText(fileName, body, Encoding.UTF8);
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.IO;
using System.Text;
namespace Hcs.ClientApi
{
/// <summary>
/// Реализация захвата содержимого отправляемых и принимаемых SOAP сообщений,
/// которая хранит данные в памяти
/// </summary>
public class HcsMemoryMessageCapture : IHcsMessageCapture
{
private MemoryStream messageCaptureStream;
private StreamWriter messageCaptureWriter;
private Encoding encoding => Encoding.UTF8;
public HcsMemoryMessageCapture()
{
messageCaptureStream = new MemoryStream();
messageCaptureWriter = new StreamWriter(messageCaptureStream, encoding);
}
void IHcsMessageCapture.CaptureMessage(bool sentOrReceived, string messageBody)
{
if (messageCaptureStream.Position > 0) messageCaptureWriter.WriteLine("");
messageCaptureWriter.Write("<!--");
messageCaptureWriter.Write(sentOrReceived ? "SENT " : "RECV ");
messageCaptureWriter.Write(DateTime.Now.ToString());
messageCaptureWriter.WriteLine("-->");
messageCaptureWriter.Write(messageBody);
messageCaptureWriter.Flush();
}
public byte[] GetData()
{
var buf = messageCaptureStream.GetBuffer();
int size = (int)messageCaptureStream.Length;
var data = new byte[size];
Buffer.BlockCopy(buf, 0, data, 0, size);
return data;
}
public override string ToString()
{
return encoding.GetString(GetData());
}
}
}

View File

@ -0,0 +1,26 @@
using System;
namespace Hcs.ClientApi
{
/// <summary>
/// Исключение указывает на то что сервер обнаружил что у
/// него нет объектов для выдачи по условию
/// </summary>
public class HcsNoResultsRemoteException : HcsRemoteException
{
public HcsNoResultsRemoteException(string description) :
base(HcsRemoteException.KnownCodes.НетОбъектовДляЭкспорта, description)
{
}
public HcsNoResultsRemoteException(string errorCode, string description) :
base(errorCode, description)
{
}
public HcsNoResultsRemoteException(string errorCode, string description, Exception nested) :
base(errorCode, description, nested)
{
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Hcs.ClientApi
{
public class HcsParallel
{
/// <summary>
/// Асинхронно обрабатывает все элементы @values типа @T методом @processor в параллельном режиме,
/// используя максимум @maxThreads потоков
/// </summary>
public static async Task ForEachAsync<T>(IEnumerable<T> values, Func<T, Task> processor, int maxThreads)
{
await Task.Run(() => ForEach(values, processor, maxThreads));
}
/// <summary>
/// Обрабатывает все элементы @values типа @T методом @processor в параллельном режиме,
/// используя максимум @maxThreads потоков
/// </summary>
public static void ForEach<T>(IEnumerable<T> values, Func<T, Task> processor, int maxThreads)
{
var taskList = new List<Task>();
var enumerator = values.GetEnumerator();
int numTasksFinished = 0;
while (true)
{
// Наполняем массив ожидания следующими задачами
while (taskList.Count < maxThreads)
{
if (!enumerator.MoveNext()) break;
// Запускаем новую задачу в отсоединенном потоке
Task newTask = Task.Run(() => processor(enumerator.Current));
taskList.Add(newTask);
}
// Если массив ожидания пуст, работа окончена
if (taskList.Count == 0) return;
// Ждем завершение любой задачи из массива ожидания
int finishedIndex = Task.WaitAny(taskList.ToArray());
var finishedTask = taskList[finishedIndex];
numTasksFinished += 1;
// Удаляем задачу из массива ожидания чтобы более ее не ждать
taskList.Remove(finishedTask);
// Если задача завершилась успешно уходим на добавление новой задачи
if (!finishedTask.IsFaulted &&
!finishedTask.IsCanceled) continue;
// Задача завершилась аномально, ждем завершения других запущенных задач
if (taskList.Count > 0) Task.WaitAll(taskList.ToArray());
// Составляем список всех возникших ошибок включая первую
taskList.Insert(0, finishedTask);
var errors = new List<Exception>();
foreach (var task in taskList)
{
if (task.IsFaulted) errors.Add(task.Exception);
if (task.IsCanceled) errors.Add(new Exception("Task was cancelled"));
}
// Аномально завершаем обработку
string message =
$"Ошибка параллельной обработки №{numTasksFinished} из {values.Count()}" +
$" объектов типа {typeof(T).FullName}";
throw new AggregateException(message, errors.ToArray());
}
}
}
}

View File

@ -0,0 +1,80 @@
using System;
using System.IO;
namespace Hcs.ClientApi
{
/// <summary>
/// Поток байтов который является частью другого потока байтов
/// https://stackoverflow.com/questions/60592147/partial-stream-of-filestream
/// </summary>
public class HcsPartialStream : Stream
{
public Stream Stream { get; private set; }
public long StreamStart { get; private set; }
public long StreamLength { get; private set; }
public long StreamEnd { get; private set; }
public HcsPartialStream(Stream stream, long offset, long size)
{
Stream = stream;
StreamStart = offset;
StreamLength = size;
StreamEnd = offset + size;
stream.Seek(offset, SeekOrigin.Begin);
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => Math.Min((Stream.Length - StreamStart), StreamLength);
public override long Position
{
get => Stream.Position - StreamStart;
set => Stream.Position = StreamStart + value;
}
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
var p = Stream.Position;
if (p < StreamStart)
{
Stream.Position = StreamStart;
}
if (p > StreamEnd)
{
return 0;
}
if (p + count > StreamEnd)
{
count = (int)(StreamEnd - p);
}
return Stream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
// Seek will be complicated as there are three origin types.
// You can do it yourself.
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Linq;
namespace Hcs.ClientApi
{
/// <summary>
/// Сообщение об ошибке возникшей на удаленном сервере ГИС ЖКХ
/// </summary>
public class HcsRemoteException : HcsException
{
public string ErrorCode { get; private set; }
public string Description { get; private set; }
public class KnownCodes
{
public const string НетОбъектовДляЭкспорта = "INT002012";
public const string ОтсутствуетВРеестре = "INT002000";
public const string ДоступЗапрещен = "AUT011003";
}
public HcsRemoteException(string errorCode, string description)
: base(MakeMessage(errorCode, description))
{
this.ErrorCode = errorCode;
this.Description = description;
}
public HcsRemoteException(string errorCode, string description, Exception nestedException)
: base(MakeMessage(errorCode, description), nestedException)
{
this.ErrorCode = errorCode;
this.Description = description;
}
private static string MakeMessage(string errorCode, string description)
=> $"Удаленная система вернула ошибку: [{errorCode}] {description}";
public static HcsRemoteException CreateNew(string errorCode, string description, Exception nested = null)
{
if (string.Compare(errorCode, KnownCodes.НетОбъектовДляЭкспорта) == 0)
return new HcsNoResultsRemoteException(errorCode, description, nested);
return new HcsRemoteException(errorCode, description);
}
public static HcsRemoteException CreateNew(HcsRemoteException nested)
{
if (nested == null) throw new ArgumentNullException("nested exception");
return CreateNew(nested.ErrorCode, nested.Description, nested);
}
/// <summary>
/// Возвращает true если ошибка @e или ее вложенные ошибки модержат @errorCode
/// </summary>
public static bool ContainsErrorCode(Exception e, string errorCode)
{
if (e == null) return false;
return HcsUtil.EnumerateInnerExceptions(e).OfType<HcsRemoteException>().Where(x => x.ErrorCode == errorCode).Any();
}
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace Hcs.ClientApi
{
public class HcsRestartTimeoutException : HcsException
{
public HcsRestartTimeoutException(string message) : base(message)
{
}
public HcsRestartTimeoutException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Hcs.ClientApi
{
public class HcsUtil
{
/// <summary>
/// Возвращает описание исключения одной строкой
/// </summary>
public static string DescribeException(Exception e)
{
string separator = "";
var buf = new StringBuilder();
buf.Append("[");
foreach (var inner in EnumerateInnerExceptions(e))
{
buf.Append(separator);
buf.Append(inner.GetType().Name);
buf.Append(":");
buf.Append(inner.Message);
separator = "-->";
}
buf.Append("]");
return buf.ToString();
}
/// <summary>
/// Возвращает список все вложенных исключений для данного исключения
/// </summary>
public static List<Exception> EnumerateInnerExceptions(Exception e)
{
var list = new List<Exception>();
WalkInnerExceptionsRecurse(e, list);
return list;
}
private static void WalkInnerExceptionsRecurse(Exception e, List<Exception> list)
{
if (e == null || list.Contains(e)) return;
list.Add(e);
WalkInnerExceptionsRecurse(e.InnerException, list);
if (e is AggregateException)
{
var aggregate = e as AggregateException;
foreach (var inner in aggregate.InnerExceptions)
{
WalkInnerExceptionsRecurse(inner, list);
}
}
}
public static string FormatGuid(Guid guid) => guid.ToString();
public static Guid ParseGuid(string guid)
{
try
{
return Guid.Parse(guid);
}
catch (Exception e)
{
throw new HcsException($"Невозможно прочитать GUID из строки [{guid}]", e);
}
}
public static string FormatDate(DateTime date)
{
return date.ToString("yyyyMMdd");
}
public static string FormatDate(DateTime? date)
{
return (date == null) ? string.Empty : FormatDate((DateTime)date);
}
/// <summary>
/// Преобразует массиб байтов в строку в формате binhex
/// </summary>
public static string ConvertToHexString(byte[] ba)
{
var buf = new StringBuilder(ba.Length * 2);
foreach (byte b in ba) buf.AppendFormat("{0:x2}", b);
return buf.ToString();
}
}
}

View File

@ -0,0 +1,229 @@
using Org.BouncyCastle.Asn1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace Hcs.ClientApi
{
/// <summary>
/// Методы работы с сертификатами X509, которых нет в системе
/// </summary>
public class HcsX509Tools
{
public static bool IsValidCertificate(X509Certificate2 cert)
{
var now = DateTime.Now;
return (now >= cert.NotBefore && now <= GetNotAfterDate(cert));
}
public static IEnumerable<X509Certificate2> EnumerateCertificates(bool includeInvalid = false)
{
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
try
{
var now = DateTime.Now;
foreach (var x in store.Certificates)
{
if (includeInvalid) yield return x;
else if (IsValidCertificate(x)) yield return x;
}
}
finally
{
store.Close();
}
}
public static X509Certificate2 FindCertificate(Func<X509Certificate2, bool> predicate)
{
if (predicate == null) throw new ArgumentException("Null subject predicate");
return EnumerateCertificates(true).FirstOrDefault(x => predicate(x));
}
public static X509Certificate2 FindValidCertificate(Func<X509Certificate2, bool> predicate)
{
if (predicate == null) throw new ArgumentException("Null subject predicate");
return EnumerateCertificates(false).FirstOrDefault(x => predicate(x));
}
/// <summary>
/// Возвращает Common Name сертификата
/// </summary>
public static string GetCommonName(X509Certificate2 x509cert)
{
return x509cert.GetNameInfo(X509NameType.SimpleName, false);
}
/// <summary>
/// Возвращает дату окончания действия сертификата
/// </summary>
public static DateTime GetNotAfterDate(X509Certificate2 x509cert)
{
// Сначала пытаемся определить срок первичного ключа, а затем уже самого сертификата
DateTime? датаОкончания = GetPrivateKeyUsageEndDate(x509cert);
if (датаОкончания != null) return (DateTime)датаОкончания;
return x509cert.NotAfter;
}
/// <summary>
/// Известные номера расширений сертификата
/// </summary>
private class KnownOids
{
public const string PrivateKeyUsagePeriod = "2.5.29.16";
}
public static DateTime? GetPrivateKeyUsageEndDate(X509Certificate2 x509cert)
{
foreach (var ext in x509cert.Extensions)
{
if (ext.Oid.Value == KnownOids.PrivateKeyUsagePeriod)
{
// Дата начала с индексом 0, дата окончания с индексом 1
return ParseAsn1Datetime(ext, 1);
}
}
return null;
}
/// <summary>
/// Разбирает значение типа дата из серии значений ASN1 присоединенных к расширению
/// </summary>
private static DateTime? ParseAsn1Datetime(X509Extension ext, int valueIndex)
{
try
{
Asn1Object asnObject = (new Asn1InputStream(ext.RawData)).ReadObject();
if (asnObject == null) return null;
var asnSequence = Asn1Sequence.GetInstance(asnObject);
if (asnSequence.Count <= valueIndex) return null;
var asn = (Asn1TaggedObject)asnSequence[valueIndex];
var asnStr = Asn1OctetString.GetInstance(asn, false);
string s = Encoding.UTF8.GetString(asnStr.GetOctets());
int year = int.Parse(s.Substring(0, 4));
int month = int.Parse(s.Substring(4, 2));
int day = int.Parse(s.Substring(6, 2));
int hour = int.Parse(s.Substring(8, 2));
int minute = int.Parse(s.Substring(10, 2));
int second = int.Parse(s.Substring(12, 2));
// Последний символ - буква 'Z'
return new DateTime(year, month, day, hour, minute, second);
}
catch (Exception)
{
return null;
}
}
public static string ДатьСтрокуФИОСертификатаСДатойОкончания(X509Certificate2 x509cert)
{
var фио = ДатьФИОСертификата(x509cert);
return фио.Фамилия + " " + фио.Имя + " " + фио.Отчество +
" до " + GetNotAfterDate(x509cert).ToString("dd.MM.yyyy");
}
public static string ДатьСтрокуФИОСертификата(X509Certificate2 x509cert)
{
var фио = ДатьФИОСертификата(x509cert);
return фио.Фамилия + " " + фио.Имя + " " + фио.Отчество;
}
/// <summary>
/// Возвращает массив из трех строк, содержащих соответственно Фамилию, Имя и Отчество
/// полученных из данных сертификата. Если сертификат не содержит ФИО возвращается массив
/// из трех пустых строк. Это не точный метод определять имя, он предполагает что
/// поля SN, G, CN содержат ФИО в определенном порядке, что правдоподобно но не обязательно.
/// </summary>
public static (string Фамилия, string Имя, string Отчество) ДатьФИОСертификата(X509Certificate2 x509cert)
{
string фам = "", имя = "", отч = "";
// Сначала ищем поля surname (SN) и given-name (G)
string sn = DecodeSubjectField(x509cert, "SN");
string g = DecodeSubjectField(x509cert, "G");
if (!string.IsNullOrEmpty(sn) && !string.IsNullOrEmpty(g))
{
фам = sn;
string[] gParts = g.Split(' ');
if (gParts != null && gParts.Length >= 1) имя = gParts[0];
if (gParts != null && gParts.Length >= 2) отч = gParts[1];
}
else
{
// Иначе берем три первых слова из common name (CN), игнорируя кавычки
string cn = DecodeSubjectField(x509cert, "CN");
if (!string.IsNullOrEmpty(cn))
{
cn = new StringBuilder(cn).Replace("\"", "").ToString();
char[] separators = { ' ', ';' };
string[] cnParts = cn.Split(separators);
if (cnParts != null && cnParts.Length >= 1) фам = cnParts[0];
if (cnParts != null && cnParts.Length >= 2) имя = cnParts[1];
if (cnParts != null && cnParts.Length >= 3) отч = cnParts[2];
}
}
return (фам, имя, отч);
}
/// <summary>
/// Возвращает строку ИНН владельца сертификата
/// </summary>
public static string ДатьИННСертификата(X509Certificate2 x509cert)
{
return DecodeSubjectField(x509cert, "ИНН");
}
/// <summary>
/// Возвращает значение поля с именем @subName включенного в различимое имя Subject
/// </summary>
private static string DecodeSubjectField(X509Certificate2 x509cert, string subName)
{
// Чтобы посмотреть все поля сертификата
//System.Diagnostics.Trace.WriteLine("x509decode=" + x509cert.SubjectName.Decode(
//X500DistinguishedNameFlags.UseNewLines));
// Декодируем различимое имя на отдельные строки через переводы строк для надежности разбора
string decoded = x509cert.SubjectName.Decode(X500DistinguishedNameFlags.UseNewLines);
char[] separators = { '\n', '\r' };
string[] parts = decoded.Split(separators, StringSplitOptions.RemoveEmptyEntries);
if (parts == null) return null;
// Каждая часть начинается с имени и отделяется от значения символом равно
foreach (string part in parts)
{
if (part.Length <= subName.Length + 1) continue;
if (part.StartsWith(subName) && part[subName.Length] == '=')
{
return part.Substring(subName.Length + 1);
}
}
return null;
}
public static int Compare(X509Certificate2 x, X509Certificate2 y)
{
if (x == null && y != null) return -1;
if (x != null && y == null) return 1;
if (x == null && y == null) return 0;
// Сначала сравниваем ФИО
int sign = string.Compare(ДатьСтрокуФИОСертификата(x), ДатьСтрокуФИОСертификата(y), true);
if (sign != 0) return sign;
// Затем дату окончания действия
return GetNotAfterDate(x).CompareTo(GetNotAfterDate(y));
}
public class CertificateComparer : IComparer<X509Certificate2>
{
public int Compare(X509Certificate2 x, X509Certificate2 y) => HcsX509Tools.Compare(x, y);
}
}
}

View File

@ -0,0 +1,132 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод получения из ГИС ЖКХ полного реестра договоров ресурсоснабжения
/// и всех связанных с ними лицевых счетов и приборов учета
/// </summary>
internal class HcsContractRegistryDownloader
{
private HcsHouseManagementApi api;
internal HcsContractRegistryDownloader(HcsHouseManagementApi api)
{
this.api = api;
}
private void ThrowOperationCancelled()
{
throw new HcsException("Операция прервана пользователем");
}
/// <summary>
/// Получить все договоры РСО удовлетворяющие фильтру @фильтрДоговоров
/// с подчиненными объектами (ЛС и ПУ)
/// </summary>
internal async Task<ГисДоговорыИПриборы> ПолучитьВсеДоговорыИПриборы(
Func<ГисДоговор, bool> фильтрДоговоров, CancellationToken token)
{
// TODO: Проверить комментарий
// В процессе будет много запросов, возвращающих мало данных,
// но требующих стандартного ожидания в несколько секунд, что
// суммарно складывается в целые часы. Экспериментально установлено
// что ГИС ЖКХ способна пережить некоторую параллельность запросов
// к ней. Так, с параллельностью в 5 потоков получение данных
// по 3000 договорам РСО (70000 ПУ) длится 2,5 часа.
int числоПотоковПараллельности = 5;
var все = new ГисДоговорыИПриборы();
все.ДатаНачалаСборки = DateTime.Now;
Action<ГисДоговор> обработчикДоговора = (ГисДоговор договор) =>
{
if (фильтрДоговоров(договор)) все.ДоговорыРСО.Add(договор);
};
await api.ПолучитьДоговорыРСО(обработчикДоговора, token);
int сделаноДоговоров = 0;
Action<ГисАдресныйОбъект> обработчикАдреса = все.АдресаОбъектов.Add;
Func<ГисДоговор, Task> обработчикДоговораАдреса = async (договор) =>
{
if (token.IsCancellationRequested) ThrowOperationCancelled();
api.Config.Log($"Получаем адреса договора #{++сделаноДоговоров}/{все.ДоговорыРСО.Count()}...");
await api.ПолучитьАдресаДоговораРСО(договор, обработчикАдреса, token);
};
await HcsParallel.ForEachAsync(все.ДоговорыРСО, обработчикДоговораАдреса, числоПотоковПараллельности);
var гуидыЗданий = все.АдресаОбъектов.Select(x => x.ГуидЗданияФиас).Distinct();
int сделаноЗданий = 0;
Func<Guid, Task> обработчикЗдания = async (гуидЗдания) =>
{
if (token.IsCancellationRequested) ThrowOperationCancelled();
api.Config.Log($"Получаем помещения здания #{++сделаноЗданий}/{гуидыЗданий.Count()}...");
try
{
var здание = await api.ПолучитьЗданиеПоГуидФиас(гуидЗдания, token);
все.Здания.Add(здание);
}
catch (Exception e)
{
if (HcsRemoteException.ContainsErrorCode(e, HcsRemoteException.KnownCodes.ОтсутствуетВРеестре))
{
api.Config.Log($"Не удалось получить здание по ФИАС ГУИД {гуидЗдания}: здание отсутствует в реестре");
var здание = new ГисЗдание() { ГуидЗданияФиас = гуидЗдания };
все.Здания.Add(здание);
}
else if (HcsRemoteException.ContainsErrorCode(e, HcsRemoteException.KnownCodes.ДоступЗапрещен))
{
api.Config.Log($"Не удалось получить здание по ФИАС ГУИД {гуидЗдания}: доступ запрещен");
var здание = new ГисЗдание() { ГуидЗданияФиас = гуидЗдания };
все.Здания.Add(здание);
}
else
{
throw new HcsException($"Вложенная ошибка получения здания {гуидЗдания}", e);
}
}
};
await HcsParallel.ForEachAsync(гуидыЗданий, обработчикЗдания, числоПотоковПараллельности);
сделаноЗданий = 0;
Action<ГисЛицевойСчет> обработчикЛС = (ГисЛицевойСчет лс) =>
{
if (лс.ДействуетСейчас && все.ЭтотЛицевойСчетСвязанСДоговорами(лс))
{
все.ЛицевыеСчета.Add(лс);
}
};
Func<Guid, Task> обработчикЗданияЛС = async (гуидЗдания) =>
{
if (token.IsCancellationRequested) ThrowOperationCancelled();
api.Config.Log($"Получаем ЛС по зданию #{++сделаноЗданий}/{гуидыЗданий.Count()}...");
await api.ПолучитьЛицевыеСчетаПоЗданию(гуидЗдания, обработчикЛС, token);
};
await HcsParallel.ForEachAsync(гуидыЗданий, обработчикЗданияЛС, числоПотоковПараллельности);
сделаноЗданий = 0;
Action<ГисПриборУчета> обработчикПУ = (ГисПриборУчета прибор) =>
{
if (прибор.ЭтоАктивный && (прибор.ЭтоОДПУ || все.ЭтотПриборСвязанСЛицевымиСчетами(прибор)))
{
все.ПриборыУчета.Add(прибор);
}
};
Func<Guid, Task> обработчикЗданияПУ = async (гуидЗдания) =>
{
if (token.IsCancellationRequested) ThrowOperationCancelled();
api.Config.Log($"Получаем ПУ по зданию #{++сделаноЗданий}/{гуидыЗданий.Count()}...");
await api.ПолучитьПриборыУчетаПоЗданию(гуидЗдания, обработчикПУ, token);
};
await HcsParallel.ForEachAsync(гуидыЗданий, обработчикЗданияПУ, числоПотоковПараллельности);
все.ДатаКонцаСборки = DateTime.Now;
return все;
}
}
}

View File

@ -0,0 +1,189 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Методы ГИС ЖКХ сервиса hcs-house-management (Договоры, ЛицевыеСчета, Приборы учета)
/// </summary>
public class HcsHouseManagementApi
{
public HcsClientConfig Config { get; private set; }
public HcsHouseManagementApi(HcsClientConfig config)
{
this.Config = config;
}
/// <summary>
/// Размещает договор и возвращает дату размещения
/// </summary>
public async Task<DateTime> РазместитьДоговор(
ГисДоговор договор, IEnumerable<ГисАдресныйОбъект> адреса, CancellationToken token = default)
{
var method = new HcsMethodImportSupplyResourceContractData(Config);
return await method.ImportContract(договор, адреса, token);
}
/// <summary>
/// Размещает лицевой счет и возвращает его ЕЛС
/// </summary>
public async Task<string> РазместитьЛицевойСчет(
ГисДоговор договор, ГисЛицевойСчет лицевойСчет, CancellationToken token = default)
{
var method = new HcsMethodImportAccountData(Config);
return await method.ImportAccount(договор, лицевойСчет, token);
}
/// <summary>
/// Размещает прибор учета и возвращает его ГУИД
/// </summary>
public async Task<Guid> РазместитьПриборУчета(
ГисПриборУчета прибор, CancellationToken token = default)
{
var method = new HcsMethodImportMeteringDeviceData(Config);
return await method.ImportMeteringDevice(прибор, token);
}
public async Task<DateTime> АрхивироватьПриборУчета(
ГисПриборУчета прибор, CancellationToken token = default)
{
var method = new HcsMethodImportMeteringDeviceData(Config);
return await method.ArchiveMeteringDevice(прибор, token);
}
public async Task<DateTime> РасторгнутьДоговор(
ГисДоговор договор, DateTime датаРасторжения, CancellationToken token = default)
{
var method = new HcsMethodImportSupplyResourceContractData(Config);
return await method.TerminateContract(договор, датаРасторжения, token);
}
public async Task<DateTime> АннулироватьДоговор(
ГисДоговор договор, string причина, CancellationToken token = default)
{
var method = new HcsMethodImportSupplyResourceContractData(Config);
return await method.AnnulContract(договор, причина, token);
}
public async Task УдалитьПроектДоговора(
ГисДоговор договор, CancellationToken token = default)
{
var method = new HcsMethodImportSupplyResourceContractProject(Config);
await method.DeleteContractProject(договор, token);
}
/// <summary>
/// Переводит проект договора в состояние "Размещен"
/// </summary>
public async Task РазместитьПроектДоговора(
ГисДоговор договор, CancellationToken token = default)
{
var method = new HcsMethodImportSupplyResourceContractProject(Config);
await method.PlaceContractProject(договор, token);
}
/// <summary>
/// Получение одного договора ресурсоснабжения по его ГУИД.
/// Если такого договора нет, будет выброшено HcsNoResultsRemoteException.
/// </summary>
public async Task<ГисДоговор> ПолучитьДоговорРСО(
Guid гуидДоговора, CancellationToken token = default)
{
var method = new HcsMethodExportSupplyResourceContractData(Config);
method.EnableMinimalResponseWaitDelay = true;
return await method.QueryOne(гуидДоговора, token);
}
/// <summary>
/// Получение одного договора ресурсоснабжения по его номеру.
/// Если такого договора нет, будет выброшено HcsNoResultsRemoteException.
/// </summary>
public async Task<ГисДоговор[]> ПолучитьДоговорыРСО(
string номерДоговора, CancellationToken token = default)
{
var method = new HcsMethodExportSupplyResourceContractData(Config);
method.EnableMinimalResponseWaitDelay = true;
return await method.QueryByContractNumber(номерДоговора, token);
}
/// <summary>
/// Получение списка договоров ресурсоснабжения
/// </summary>
public async Task<int> ПолучитьДоговорыРСО(
Action<ГисДоговор> resultHandler, CancellationToken token = default)
{
var method = new HcsMethodExportSupplyResourceContractData(Config);
return await method.QueryAll(resultHandler, token);
}
/// <summary>
/// Запрос на экспорт объектов жилищного фонда из договора ресурсоснабжения
/// </summary>
public async Task<int> ПолучитьАдресаДоговораРСО(
ГисДоговор договор, Action<ГисАдресныйОбъект> resultHandler, CancellationToken token = default)
{
var method = new HcsMethodExportSupplyResourceContractObjectAddress(Config);
return await method.QueryAddresses(договор, resultHandler, token);
}
/// <summary>
/// Размещение изменения списка адресных объектов в договоре
/// </summary>
public async Task РазместитьАдресаДоговораРСО(
ГисДоговор договор,
IEnumerable<ГисАдресныйОбъект> адресаДляРазмещения,
IEnumerable<ГисАдресныйОбъект> адресаДляУдаления,
CancellationToken token)
{
var method = new HcsMethodImportSupplyResourceContractObjectAddress(Config);
await method.ImportObjectAddresses(договор, адресаДляРазмещения, адресаДляУдаления, token);
}
/// <summary>
/// Получение списка лицевых счетов для одного здания
/// </summary>
public async Task<int> ПолучитьЛицевыеСчетаПоЗданию(
Guid fiasHouseGuid, Action<ГисЛицевойСчет> resultHandler, CancellationToken token = default)
{
var method = new HcsMethodExportAccountData(Config);
return await method.Query(fiasHouseGuid, null, resultHandler, token);
}
/// <summary>
/// Получение списка приборов учета для одного здания
/// </summary>
public async Task<int> ПолучитьПриборыУчетаПоЗданию(
Guid fiasHouseGuid, Action<ГисПриборУчета> resultHandler, CancellationToken token = default)
{
var method = new HcsMethodExportMeteringDeviceData(Config);
return await method.ExportByHouse(fiasHouseGuid, resultHandler, token);
}
/// <summary>
/// Пример получения данных об одном здании по его идентификатору в ФИАС
/// </summary>
public async Task<ГисЗдание> ПолучитьЗданиеПоГуидФиас(Guid fiasHouseGuid, CancellationToken token = default)
{
try
{
var method = new HcsMethodExportHouse(Config);
return await method.ExportHouseByFiasGuid(fiasHouseGuid, token);
}
catch (HcsException e)
{
throw new HcsException($"Не удалось получить здание по ФИАС GUID {fiasHouseGuid}", e);
}
}
public async Task<ГисДоговорыИПриборы> ПолучитьВсеДоговорыИПриборы(
Func<ГисДоговор, bool> contractFilter, CancellationToken token = default)
{
return await (new HcsContractRegistryDownloader(this))
.ПолучитьВсеДоговорыИПриборы(contractFilter, token);
}
}
}

View File

@ -0,0 +1,162 @@
using Hcs.ClientApi.RemoteCaller;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.Service.Async.HouseManagement.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.HouseManagementApi
{
public class HcsHouseManagementMethod : HcsRemoteCallMethod
{
public HcsEndPoints EndPoint => HcsEndPoints.HomeManagementAsync;
public HouseManagement.RequestHeader CreateRequestHeader() =>
HcsRequestHelper.CreateHeader<HouseManagement.RequestHeader>(ClientConfig);
public HcsHouseManagementMethod(HcsClientConfig config) : base(config) { }
public System.ServiceModel.EndpointAddress RemoteAddress
=> GetEndpointAddress(HcsConstants.EndPointLocator.GetPath(EndPoint));
private HouseManagement.HouseManagementPortsTypeAsyncClient NewPortClient()
{
var client = new HouseManagement.HouseManagementPortsTypeAsyncClient(_binding, RemoteAddress);
ConfigureEndpointCredentials(client.Endpoint, client.ClientCredentials);
return client;
}
public async Task<IHcsGetStateResult> SendAndWaitResultAsync(
object request,
Func<HouseManagement.HouseManagementPortsTypeAsyncClient, Task<IHcsAck>> sender,
CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
try
{
if (CanBeRestarted)
{
return await RunRepeatableTaskInsistentlyAsync(
async () => await SendAndWaitResultAsyncImpl(request, sender, token), token);
}
else
{
return await SendAndWaitResultAsyncImpl(request, sender, token);
}
}
catch (HcsRestartTimeoutException e)
{
if (!CanBeRestarted) throw new HcsException("Превышен лимит ожидания выполнения запроса", e);
Log($"Перезапускаем запрос типа {request.GetType().Name}...");
}
}
}
private async Task<IHcsGetStateResult> SendAndWaitResultAsyncImpl(
object request,
Func<HouseManagement.HouseManagementPortsTypeAsyncClient, Task<IHcsAck>> sender,
CancellationToken token)
{
if (request == null) throw new ArgumentNullException(nameof(request));
string version = HcsRequestHelper.GetRequestVersionString(request);
_config.Log($"Отправляем запрос: {RemoteAddress.Uri}/{request.GetType().Name} в версии {version}...");
var stopWatch = System.Diagnostics.Stopwatch.StartNew();
IHcsAck ack;
using (var client = NewPortClient())
{
ack = await sender(client);
}
stopWatch.Stop();
_config.Log($"Запрос принят в обработку за {stopWatch.ElapsedMilliseconds}мс., подтверждение {ack.MessageGUID}");
var stateResult = await WaitForResultAsync(ack, true, token);
stateResult.Items.OfType<HouseManagement.ErrorMessageType>().ToList().ForEach(x =>
{
throw HcsRemoteException.CreateNew(x.ErrorCode, x.Description);
});
return stateResult;
}
/// <summary>
/// Выполняет однократную проверку наличия результата.
/// Возвращает null если результата еще нет.
/// </summary>
protected override async Task<IHcsGetStateResult> TryGetResultAsync(IHcsAck sourceAck, CancellationToken token)
{
using (var client = NewPortClient())
{
var requestHeader = HcsRequestHelper.CreateHeader<HouseManagement.RequestHeader>(_config);
var requestBody = new HouseManagement.getStateRequest { MessageGUID = sourceAck.MessageGUID };
var response = await client.getStateAsync(requestHeader, requestBody);
var resultBody = response.getStateResult;
if (resultBody.RequestState == HcsAsyncRequestStateTypes.Ready)
{
return resultBody;
}
return null;
}
}
/// <summary>
/// Разбирает стандартный ответ HCS на операцию импорта с приемом ошибок
/// </summary>
protected HouseManagement.getStateResultImportResultCommonResult ParseSingleImportResult(IHcsGetStateResult stateResult)
{
return ParseImportResults(stateResult, 1, true).First();
}
/// <summary>
/// Разбирает стандартный ответ HCS на операцию импорта с приемом ошибок
/// </summary>
protected HouseManagement.getStateResultImportResultCommonResult[] ParseImportResults(
IHcsGetStateResult stateResult, int commonResultRequiredCount, bool checkItemErrors)
{
var importResult = RequireSingleItem<HouseManagement.getStateResultImportResult>(stateResult.Items);
if (IsArrayEmpty(importResult.Items)) throw new HcsException("Пустой ImportResult.Items");
importResult.Items.OfType<HouseManagement.ErrorMessageType>().ToList()
.ForEach(error => { throw HcsRemoteException.CreateNew(error.ErrorCode, error.Description); });
var commonResults = importResult.Items.OfType<HouseManagement.getStateResultImportResultCommonResult>();
foreach (var commonResult in commonResults)
{
if (IsArrayEmpty(commonResult.Items)) throw new HcsException("Пустой CommonResult.Items");
if (checkItemErrors) CheckCommonResultErrors(commonResult);
}
if (commonResults.Count() != commonResultRequiredCount)
{
throw new HcsException(
$"Число результатов {commonResults.Count()} типа CommonResult не равно {commonResultRequiredCount}");
}
return commonResults.ToArray();
}
protected void CheckCommonResultErrors(HouseManagement.getStateResultImportResultCommonResult commonResult)
{
commonResult.Items.OfType<HouseManagement.CommonResultTypeError>().ToList()
.ForEach(error => { throw HcsRemoteException.CreateNew(error.ErrorCode, error.Description); });
}
}
}

View File

@ -0,0 +1,90 @@
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Методы и константы для работы с номенклатурно-справочной информацией (НСИ)
/// применяемой в сервисе hcs-house-management
/// https://my.dom.gosuslugi.ru/#!/open-data
/// </summary>
public class HcsHouseManagementNsi
{
// Ссылка на НСИ "54 Причина расторжения договора" (реестровый номер 54)
// https://my.dom.gosuslugi.ru/#!/open-data-passport?passportName=7710474375-nsi-54
public class ПричинаРасторженияДоговора
{
public static HouseManagement.nsiRef ПоВзаимномуСогласиюСторон => new HouseManagement.nsiRef()
{
Name = "По взаимному согласию сторон",
Code = "4",
GUID = "4a481322-05c9-47cb-9d05-30387dff1f93"
};
}
// Ссылка на НСИ "22 Причина закрытия лицевого счета" (реестровый номер 22)
// https://dom.gosuslugi.ru/opendataapi/nsi-22/v1
public class ПричинаЗакрытияЛицевогоСчета
{
public static HouseManagement.nsiRef РасторжениеДоговора => new HouseManagement.nsiRef()
{
Name = "Расторжение договора",
Code = "11",
GUID = "7ee8b4db-dabc-40eb-9009-f4f80b36bfe5"
};
}
// Ссылка на НСИ "Причина архивации прибора учета" (реестровый номер 21)
// https://my.dom.gosuslugi.ru/#!/open-data-passport?passportName=7710474375-nsi-21
public class ПричинаАрхивацииПрибораУчета
{
public static HouseManagement.nsiRef ИстекСрокЭксплуатации => new HouseManagement.nsiRef()
{
Code = "12",
GUID = "2b8f44f9-7ca1-44f5-803a-af80d6912f36",
Name = "Истек срок эксплуатации прибора учета"
};
public static HouseManagement.nsiRef Ошибка => new HouseManagement.nsiRef()
{
Code = "4",
GUID = "d723696f-5ed7-4923-ad6a-9c2c5bce5032",
Name = "Ошибка"
};
}
// Ссылка на НСИ "Основание заключения договора" (реестровый номер 58)
// https://my.dom.gosuslugi.ru/#!/open-data-passport?passportName=7710474375-nsi-58
public class ОснованиеЗаключенияДоговора
{
public static HouseManagement.nsiRef ЗаявлениеПотребителя => new HouseManagement.nsiRef()
{
Code = "7",
GUID = "93cd9d85-91b8-4bf9-ae48-c5f1e691949f",
Name = "Заявление потребителя"
};
public static HouseManagement.nsiRef ДоговорУправления => new HouseManagement.nsiRef()
{
Code = "3",
GUID = "11efe618-79f8-4f53-bfd6-11620e8e9e1e",
Name = "Договор управления"
};
}
public static HouseManagement.ContractSubjectTypeServiceType ElectricSupplyServiceType
=> new HouseManagement.ContractSubjectTypeServiceType()
{
Code = "4",
GUID = "903c7763-73f8-4af2-9ec2-94ee08c7beaa",
Name = "Электроснабжение"
};
public static HouseManagement.ContractSubjectTypeMunicipalResource ElectricSupplyMunicipalResource
=> new HouseManagement.ContractSubjectTypeMunicipalResource()
{
Code = "8",
GUID = "7379be86-6c95-4e41-b000-3bc703d35969",
Name = "Электрическая энергия"
};
}
}

View File

@ -0,0 +1,190 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод получения реестра лицевых счетов
/// </summary>
public class HcsMethodExportAccountData : HcsHouseManagementMethod
{
public HcsMethodExportAccountData(HcsClientConfig config) : base(config)
{
CanBeRestarted = true;
}
/// <summary>
/// Получает реестр лицевых счетов по зданию с данным ГУИД ФИАС или по списку номеров ЕЛС
/// </summary>
public async Task<int> Query(
Guid? fiasHouseGuid, IEnumerable<string> unifiedAccountNumbers,
Action<ГисЛицевойСчет> resultHandler, CancellationToken token)
{
int numResults = 0;
var itemNames = new List<HouseManagement.ItemsChoiceType18> { };
List<string> items = new List<string> { };
if (fiasHouseGuid != null)
{
itemNames.Add(HouseManagement.ItemsChoiceType18.FIASHouseGuid);
items.Add(FormatGuid(fiasHouseGuid));
}
if (unifiedAccountNumbers != null)
{
if (unifiedAccountNumbers.Count() > 1000)
throw new HcsException($"Слишком много ЕЛС в запросе {unifiedAccountNumbers.Count()} > допустимых 1000");
foreach (var un in unifiedAccountNumbers)
{
itemNames.Add(HouseManagement.ItemsChoiceType18.UnifiedAccountNumber);
items.Add(un);
}
}
try
{
var request = new HouseManagement.exportAccountRequest
{
Id = HcsConstants.SignedXmlElementId,
Items = items.ToArray(),
ItemsElementName = itemNames.ToArray()
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.exportAccountDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
stateResult.Items.OfType<HouseManagement.exportAccountResultType>().ToList().ForEach(
account => { resultHandler(Adopt(account)); numResults += 1; });
}
catch (HcsNoResultsRemoteException)
{
return 0;
}
return numResults;
}
private ГисЛицевойСчет Adopt(HouseManagement.exportAccountResultType source)
{
return new ГисЛицевойСчет()
{
ГуидЛицевогоСчета = ParseGuid(source.AccountGUID),
НомерЕЛС = source.UnifiedAccountNumber,
НомерЛицевогоСчета = source.AccountNumber,
ПолнаяПлощадь = (source.TotalSquareSpecified ? (decimal?)source.TotalSquare : null),
ЖилаяПлощадь = (source.ResidentialSquareSpecified ? (decimal?)source.ResidentialSquare : null),
КодЖКУ = source.ServiceID,
ДатаСоздания = (source.CreationDateSpecified ? (DateTime?)source.CreationDate : null),
ДатаЗакрытия = (source.Closed != null ? (DateTime?)source.Closed.CloseDate : null),
КодНсиПричиныЗакрытия = (source.Closed != null ? source.Closed.CloseReason.Code : null),
ИмяПричиныЗакрытия = (source.Closed != null ? source.Closed.Description : null),
Размещения = Adopt(source.Accommodation),
Основания = Adopt(source.AccountReasons)
};
}
private ГисОснованиеЛС[] Adopt(HouseManagement.exportAccountResultTypeAccountReasons source)
{
if (source == null) throw new ArgumentNullException("HouseManagement.exportAccountResultTypeAccountReasons");
var основания = new List<ГисОснованиеЛС>();
if (source.SupplyResourceContract != null)
{
foreach (var sr in source.SupplyResourceContract)
{
var основание = new ГисОснованиеЛС();
основание.ТипОснованияЛС = ГисТипОснованияЛС.ДоговорРСО;
for (int i = 0; i < sr.Items.Length; i++)
{
switch (sr.ItemsElementName[i])
{
case HouseManagement.ItemsChoiceType9.ContractGUID:
основание.ГуидДоговора = ParseGuid(sr.Items[i]);
break;
}
}
if (основание.ГуидДоговора == default(Guid))
throw new HcsException("Для основания ЛС не указан ГУИД договора РСО");
основания.Add(основание);
}
}
if (source.SocialHireContract != null)
{
var sh = source.SocialHireContract;
var основание = new ГисОснованиеЛС();
основание.ТипОснованияЛС = ГисТипОснованияЛС.Соцнайм;
for (int i = 0; i < sh.Items.Length; i++)
{
object itemValue = sh.Items[i];
switch (sh.ItemsElementName[i])
{
case HouseManagement.ItemsChoiceType10.ContractGUID:
основание.ГуидДоговора = ParseGuid(itemValue);
break;
case HouseManagement.ItemsChoiceType10.ContractNumber:
основание.НомерДоговора = (itemValue != null ? itemValue.ToString() : null);
break;
}
}
if (основание.ГуидДоговора == default(Guid))
throw new HcsException("Для основания ЛС не указан ГУИД договора соцнайма");
основания.Add(основание);
}
if (source.Contract != null)
{
var основание = new ГисОснованиеЛС();
основание.ТипОснованияЛС = ГисТипОснованияЛС.Договор;
основание.ГуидДоговора = ParseGuid(source.Contract.ContractGUID);
основания.Add(основание);
}
// TODO: Проверить комментарий
// Непонятно что делать с остальными типам основания и даже следует ли их
// расшифровывать или считать ошибкой пустого списка оснований
return основания.ToArray();
}
private ГисРазмещениеЛС[] Adopt(HouseManagement.AccountExportTypeAccommodation[] array)
{
if (array == null) throw new ArgumentNullException("HouseManagement.AccountExportTypeAccommodation");
return array.ToList().Select(x => Adopt(x)).ToArray();
}
private ГисРазмещениеЛС Adopt(HouseManagement.AccountExportTypeAccommodation source)
{
var размещение = new ГисРазмещениеЛС();
размещение.ПроцентДоли = (source.SharePercentSpecified ? (decimal?)source.SharePercent : null);
switch (source.ItemElementName)
{
case HouseManagement.ItemChoiceType7.FIASHouseGuid: размещение.ГуидЗдания = ParseGuid(source.Item); break;
case HouseManagement.ItemChoiceType7.PremisesGUID: размещение.ГуидПомещения = ParseGuid(source.Item); break;
case HouseManagement.ItemChoiceType7.LivingRoomGUID: размещение.ГуидЖилойКомнаты = ParseGuid(source.Item); break;
default: throw new HcsException("Неизвестный тип размещения ЛС: " + source.ItemElementName);
}
return размещение;
}
}
}

View File

@ -0,0 +1,101 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод получения информации о доме и его помещениях
/// </summary>
public class HcsMethodExportHouse : HcsHouseManagementMethod
{
public HcsMethodExportHouse(HcsClientConfig config) : base(config)
{
CanBeRestarted = true;
}
public async Task<ГисЗдание> ExportHouseByFiasGuid(Guid fiasHouseGuid, CancellationToken token)
{
var request = new HouseManagement.exportHouseRequest
{
Id = HcsConstants.SignedXmlElementId,
FIASHouseGuid = FormatGuid(fiasHouseGuid),
// TODO: Тут хардкод версии
version = "12.2.0.1"
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var response = await portClient.exportHouseDataAsync(CreateRequestHeader(), request);
return response.AckRequest.Ack;
}, token);
return Adopt(RequireSingleItem<HouseManagement.exportHouseResultType>(stateResult.Items));
}
private ГисЗдание Adopt(HouseManagement.exportHouseResultType source)
{
bool заполнен = false;
var дом = new ГисЗдание();
дом.НомерДомаГис = source.HouseUniqueNumber;
var помещения = new List<ГисПомещение>();
var apartmentHouse = source.Item as HouseManagement.exportHouseResultTypeApartmentHouse;
if (apartmentHouse != null)
{
дом.ТипДома = ГисТипДома.Многоквартирный;
дом.ГуидЗданияФиас = ParseGuid(apartmentHouse.BasicCharacteristicts.FIASHouseGuid);
if (apartmentHouse.ResidentialPremises != null)
{
apartmentHouse.ResidentialPremises.ToList().ForEach(x => помещения.Add(Adopt(x)));
}
if (apartmentHouse.NonResidentialPremises != null)
{
apartmentHouse.NonResidentialPremises.ToList().ForEach(x => помещения.Add(Adopt(x)));
}
заполнен = true;
}
var livingHouse = source.Item as HouseManagement.exportHouseResultTypeLivingHouse;
if (livingHouse != null)
{
дом.ТипДома = ГисТипДома.Жилой;
дом.ГуидЗданияФиас = ParseGuid(livingHouse.BasicCharacteristicts.FIASHouseGuid);
заполнен = true;
}
if (!заполнен) throw new HcsException("В информации о доме неизвестный тип данных: " + source.Item);
дом.Помещения = помещения.ToArray();
return дом;
}
private ГисПомещение Adopt(HouseManagement.exportHouseResultTypeApartmentHouseResidentialPremises source)
{
return new ГисПомещение()
{
ЭтоЖилоеПомещение = true,
НомерПомещения = source.PremisesNum,
ГуидПомещения = ParseGuid(source.PremisesGUID),
ДатаПрекращения = source.TerminationDateSpecified ? source.TerminationDate : null,
Аннулирование = source.AnnulmentInfo
};
}
private ГисПомещение Adopt(HouseManagement.exportHouseResultTypeApartmentHouseNonResidentialPremises source)
{
return new ГисПомещение()
{
ЭтоЖилоеПомещение = false,
НомерПомещения = source.PremisesNum,
ГуидПомещения = ParseGuid(source.PremisesGUID)
};
}
}
}

View File

@ -0,0 +1,165 @@
using Hcs.ClientApi.DataTypes;
using Hcs.Service.Async.HouseManagement.v14_5_0_1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод получения списка приборов учета
/// </summary>
public class HcsMethodExportMeteringDeviceData : HcsHouseManagementMethod
{
public HcsMethodExportMeteringDeviceData(HcsClientConfig config) : base(config)
{
CanBeRestarted = true;
}
/// <summary>
/// Получение списка приборов учета для одного здания
/// </summary>
public async Task<int> ExportByHouse(
Guid fiasHouseGuid, Action<ГисПриборУчета> resultHandler, CancellationToken token)
{
List<HouseManagement.ItemsChoiceType4> itemNames = [HouseManagement.ItemsChoiceType4.FIASHouseGuid];
List<string> items = [FormatGuid(fiasHouseGuid)];
var request = new HouseManagement.exportMeteringDeviceDataRequest
{
Id = HcsConstants.SignedXmlElementId,
Items = items.ToArray(),
ItemsElementName = itemNames.ToArray(),
// TODO: Проверить комментарий
//version = "11.1.0.2" // Версия указана в API
};
int numResults = 0;
try
{
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.exportMeteringDeviceDataAsync(CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
stateResult.Items.OfType<HouseManagement.exportMeteringDeviceDataResultType>().ToList().ForEach(
device => { resultHandler(Adopt(device)); numResults += 1; }
);
}
catch (HcsNoResultsRemoteException)
{
return 0;
}
return numResults;
}
private ГисПриборУчета Adopt(HouseManagement.exportMeteringDeviceDataResultType source)
{
var прибор = new ГисПриборУчета()
{
ГуидПрибораУчета = ParseGuid(source.MeteringDeviceRootGUID),
ГуидВерсииПрибора = ParseGuid(source.MeteringDeviceVersionGUID),
НомерПрибораУчетаГис = source.MeteringDeviceGISGKHNumber,
ЗаводскойНомер = source.BasicChatacteristicts.MeteringDeviceNumber,
МодельПрибораУчета = source.BasicChatacteristicts.MeteringDeviceModel,
ДатаРазмещенияВерсии = source.UpdateDateTime
};
if (!IsArrayEmpty(source.MeteringOwner))
{
прибор.ГуидВладельцаПрибора = ParseGuid(source.MeteringOwner[0]);
}
switch (source.StatusRootDoc)
{
case HouseManagement.exportMeteringDeviceDataResultTypeStatusRootDoc.Active:
прибор.СтатусПрибораУчета = ГисСтатусПрибораУчета.Активный;
break;
case HouseManagement.exportMeteringDeviceDataResultTypeStatusRootDoc.Archival:
прибор.СтатусПрибораУчета = ГисСтатусПрибораУчета.Архивный;
break;
default:
throw new HcsException($"Неизвестный статус ПУ {source.StatusRootDoc} для №{прибор.ЗаводскойНомер}");
}
var basic = source.BasicChatacteristicts;
прибор.ДатаУстановки = basic.InstallationDateSpecified ? basic.InstallationDate : null;
прибор.ДатаВводаВЭксплуатацию = basic.CommissioningDateSpecified ? basic.CommissioningDate : null;
прибор.ДатаПоследнейПоверки = basic.FirstVerificationDateSpecified ? basic.FirstVerificationDate : null;
прибор.ДатаИзготовления = basic.FactorySealDateSpecified ? basic.FactorySealDate : null;
прибор.РежимДистанционногоОпроса = basic.RemoteMeteringMode;
прибор.ОписаниеДистанционногоОпроса = basic.RemoteMeteringInfo;
object basicItem = basic.Item;
bool типНайден = false;
CallOnType<HouseManagement.MeteringDeviceBasicCharacteristicsTypeResidentialPremiseDevice>(basicItem, x =>
{
прибор.ГуидыЛицевыхСчетов = ParseGuidArray(x.AccountGUID);
прибор.ГуидыПомещений = ParseGuidArray(x.PremiseGUID);
прибор.ВидПрибораУчета = ГисВидПрибораУчета.ЖилоеПомещение;
типНайден = true;
});
CallOnType<HouseManagement.MeteringDeviceBasicCharacteristicsTypeNonResidentialPremiseDevice>(basicItem, x =>
{
прибор.ГуидыЛицевыхСчетов = ParseGuidArray(x.AccountGUID);
прибор.ГуидыПомещений = ParseGuidArray(x.PremiseGUID);
прибор.ВидПрибораУчета = ГисВидПрибораУчета.НежилоеПомещение;
типНайден = true;
});
CallOnType<HouseManagement.MeteringDeviceBasicCharacteristicsTypeCollectiveDevice>(basicItem, x =>
{
прибор.ГуидыЗданийФиас = ParseGuidArray(x.FIASHouseGuid);
прибор.ВидПрибораУчета = ГисВидПрибораУчета.ОДПУ;
типНайден = true;
});
CallOnType<HouseManagement.MeteringDeviceBasicCharacteristicsTypeCollectiveApartmentDevice>(basicItem, x =>
{
прибор.ГуидыЛицевыхСчетов = ParseGuidArray(x.AccountGUID);
прибор.ГуидыПомещений = ParseGuidArray(x.PremiseGUID);
прибор.ВидПрибораУчета = ГисВидПрибораУчета.КоммунальнаяКвартира;
типНайден = true;
});
CallOnType<HouseManagement.MeteringDeviceBasicCharacteristicsTypeLivingRoomDevice>(basicItem, x =>
{
прибор.ГуидыЛицевыхСчетов = ParseGuidArray(x.AccountGUID);
прибор.ГуидыЖилыхКомнат = ParseGuidArray(x.LivingRoomGUID);
прибор.ВидПрибораУчета = ГисВидПрибораУчета.ЖилаяКомната;
типНайден = true;
});
CallOnType<HouseManagement.MeteringDeviceBasicCharacteristicsTypeApartmentHouseDevice>(basicItem, x =>
{
прибор.ГуидыЗданийФиас = ParseGuidArray(x.FIASHouseGuid);
прибор.ГуидыЛицевыхСчетов = ParseGuidArray(x.AccountGUID);
прибор.ВидПрибораУчета = ГисВидПрибораУчета.ЖилойДом;
типНайден = true;
});
if (!типНайден) throw new HcsException($"Неизвестный тип ПУ {basicItem} для №{прибор.ЗаводскойНомер}");
foreach (var electric in source.Items.OfType<MunicipalResourceElectricExportType>())
{
прибор.КоэффициентТрансформации =
(electric.TransformationRatioSpecified ? electric.TransformationRatio : 0);
прибор.ПоказаниеТ1 = electric.MeteringValueT1;
прибор.ПоказаниеТ2 = electric.MeteringValueT2;
прибор.ПоказаниеТ3 = electric.MeteringValueT3;
}
return прибор;
}
}
}

View File

@ -0,0 +1,297 @@
using Hcs.ClientApi.DataTypes;
using Hcs.ClientApi.RemoteCaller;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод получения реестра договоров ресурсоснабжения (ДРСО)
/// </summary>
public class HcsMethodExportSupplyResourceContractData : HcsHouseManagementMethod
{
public HcsMethodExportSupplyResourceContractData(HcsClientConfig config) : base(config)
{
CanBeRestarted = true;
}
/// <summary>
/// Получает один договор ресурсоснабжения по его GUID
/// </summary>
public async Task<ГисДоговор> QueryOne(Guid contractRootGuid, CancellationToken token)
{
ГисДоговор договор = null;
Action<ГисДоговор> handler = (result) => { договор = result; };
await QueryOneBatch(contractRootGuid, null, handler, null, token);
if (договор == null)
throw new HcsNoResultsRemoteException($"Нет договора РСО с ГУИД {contractRootGuid}");
return договор;
}
/// <summary>
/// Получает один договор ресурсоснабжения по его номеру договора
/// </summary>
public async Task<ГисДоговор[]> QueryByContractNumber(string contractNumber, CancellationToken token)
{
var list = new List<ГисДоговор>();
Action<ГисДоговор> handler = list.Add;
await QueryOneBatch(null, contractNumber, handler, null, token);
if (!list.Any()) throw new HcsNoResultsRemoteException($"Нет договора РСО с номером {contractNumber}");
return list.ToArray();
}
/// <summary>
/// Получает полный список реестра договоров ресурсоснабжения
/// </summary>
public async Task<int> QueryAll(Action<ГисДоговор> resultHandler, CancellationToken token)
{
int numResults = 0;
int numPages = 0;
Action<ГисДоговор> countingHandler = (result) =>
{
numResults += 1;
resultHandler(result);
};
Guid? nextGuid = null;
while (true)
{
if (++numPages > 1) Log($"Запрашиваем страницу #{numPages} данных...");
var paged = await QueryOneBatch(null, null, countingHandler, nextGuid, token);
if (paged.IsLastPage) break;
nextGuid = paged.NextGuid;
}
return numResults;
}
private async Task<HcsPagedResultState> QueryOneBatch(
Guid? contractRootGuid, string contractNumber, Action<ГисДоговор> resultHandler,
Guid? exportNextGuid, CancellationToken token)
{
var itemNames = new List<HouseManagement.ItemsChoiceType27> { };
List<object> items = new List<object> { };
if (contractRootGuid != null)
{
itemNames.Add(HouseManagement.ItemsChoiceType27.ContractRootGUID);
items.Add(FormatGuid(contractRootGuid));
}
if (contractNumber != null)
{
itemNames.Add(HouseManagement.ItemsChoiceType27.ContractNumber);
items.Add(contractNumber);
}
if (exportNextGuid != null)
{
itemNames.Add(HouseManagement.ItemsChoiceType27.ExportContractRootGUID);
items.Add(FormatGuid(exportNextGuid));
}
var request = new HouseManagement.exportSupplyResourceContractRequest
{
Id = HcsConstants.SignedXmlElementId,
Items = items.ToArray(),
ItemsElementName = itemNames.ToArray(),
// TODO: Проверить хардкод версии
version = "13.1.1.1" // Значение из сообщения об ошибке от сервера HCS
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.exportSupplyResourceContractDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
var result = RequireSingleItem
<HouseManagement.getStateResultExportSupplyResourceContractResult>(stateResult.Items);
foreach (var c in result.Contract)
{
resultHandler(Adopt(c));
}
return new HcsPagedResultState(result.Item);
}
private ГисДоговор Adopt(HouseManagement.exportSupplyResourceContractResultType source)
{
var договор = new ГисДоговор()
{
ГуидДоговора = ParseGuid(source.ContractRootGUID),
ГуидВерсииДоговора = ParseGuid(source.ContractGUID),
НомерВерсии = source.VersionNumber,
СостояниеДоговора = Adopt(source.ContractState),
СтатусВерсииДоговора = Adopt(source.VersionStatus)
};
if (source.Item is HouseManagement.ExportSupplyResourceContractTypeIsContract isContract)
{
договор.ТипДоговораРСО = ГисТипДоговораРСО.НеПубличныйИлиНеНежилые;
договор.НомерДоговора = isContract.ContractNumber;
договор.ДатаЗаключения = (DateTime?)isContract.SigningDate;
договор.ДатаВступленияВСилу = (DateTime?)isContract.EffectiveDate;
if (isContract.ContractAttachment != null)
{
договор.ПриложенияДоговора = isContract.ContractAttachment.Select(AdoptAttachment).ToArray();
}
}
if (source.Item is HouseManagement.ExportSupplyResourceContractTypeIsNotContract isNotContract)
{
договор.ТипДоговораРСО = ГисТипДоговораРСО.ПубличныйИлиНежилые;
договор.НомерДоговора = isNotContract.ContractNumber;
договор.ДатаЗаключения = isNotContract.SigningDateSpecified ? isNotContract.SigningDate : null;
договор.ДатаВступленияВСилу = isNotContract.EffectiveDateSpecified ? isNotContract.EffectiveDate : null;
if (isNotContract.ContractAttachment != null)
{
договор.ПриложенияДоговора = isNotContract.ContractAttachment.Select(AdoptAttachment).ToArray();
}
}
var предметы = new List<ГисПредметДоговора>();
foreach (var subject in source.ContractSubject)
{
var предмет = new ГисПредметДоговора()
{
КодНсиУслуги = subject.ServiceType.Code,
ГуидНсиУслуги = ParseGuid(subject.ServiceType.GUID),
ИмяНсиУслуги = subject.ServiceType.Name,
КодНсиРесурса = subject.MunicipalResource.Code,
ГуидНсиРесурса = ParseGuid(subject.MunicipalResource.GUID),
ИмяНсиРесурса = subject.MunicipalResource.Name
};
предметы.Add(предмет);
}
договор.ПредметыДоговора = предметы.ToArray();
договор.Контрагент = AdoptCounterparty(source.Item1);
if (source.CountingResourceSpecified)
{
if (source.CountingResource == HouseManagement.ExportSupplyResourceContractTypeCountingResource.R)
договор.НачисленияРазмещаетРСО = true;
}
if (source.MeteringDeviceInformationSpecified)
{
if (source.MeteringDeviceInformation == true)
договор.ПриборыРазмещаетРСО = true;
}
return договор;
}
private ГисПриложение AdoptAttachment(HouseManagement.AttachmentType attachment)
{
return new ГисПриложение()
{
ИмяПриложения = attachment.Name,
ГуидПриложения = ParseGuid(attachment.Attachment.AttachmentGUID),
ХэшПриложения = attachment.AttachmentHASH
};
}
/// <summary>
/// Разбор сведений о контрагенте - второй стороне договора
/// </summary>
private ГисКонтрагент AdoptCounterparty(object item1)
{
switch (item1)
{
case HouseManagement.ExportSupplyResourceContractTypeApartmentBuildingOwner owner:
return AdoptCounterpartyEntity(owner.Item, ГисТипКонтрагента.ВладелецПомещения);
case HouseManagement.ExportSupplyResourceContractTypeApartmentBuildingRepresentativeOwner rep:
return AdoptCounterpartyEntity(rep.Item, ГисТипКонтрагента.ВладелецПомещения);
case HouseManagement.ExportSupplyResourceContractTypeApartmentBuildingSoleOwner sole:
return AdoptCounterpartyEntity(sole.Item, ГисТипКонтрагента.ВладелецПомещения);
case HouseManagement.ExportSupplyResourceContractTypeLivingHouseOwner owner:
return AdoptCounterpartyEntity(owner.Item, ГисТипКонтрагента.ВладелецПомещения);
case HouseManagement.ExportSupplyResourceContractTypeOrganization uk:
return new ГисКонтрагент()
{
ТипКонтрагента = ГисТипКонтрагента.УправляющаяКомпания,
ГуидОрганизации = ParseGuid(uk.orgRootEntityGUID)
};
}
return new ГисКонтрагент() { ТипКонтрагента = ГисТипКонтрагента.НеУказано };
}
/// <summary>
/// Разбор ссылки на контрагента - второй стороны договора
/// </summary>
private ГисКонтрагент AdoptCounterpartyEntity(object item, ГисТипКонтрагента типКонтрагента)
{
switch (item)
{
case HouseManagement.DRSORegOrgType org:
return new ГисКонтрагент()
{
ТипКонтрагента = типКонтрагента,
ГуидОрганизации = ParseGuid(org.orgRootEntityGUID)
};
case HouseManagement.DRSOIndType ind:
var индивид = new ГисИндивид()
{
Фамилия = ind.Surname,
Имя = ind.FirstName,
Отчество = ind.Patronymic
};
switch (ind.Item)
{
case string снилс: индивид.СНИЛС = снилс; break;
case HouseManagement.ID id:
индивид.НомерДокумента = id.Number;
индивид.СерияДокумента = id.Series;
индивид.ДатаДокумента = id.IssueDate;
break;
}
return new ГисКонтрагент() { ТипКонтрагента = типКонтрагента, Индивид = индивид, };
}
return new ГисКонтрагент() { ТипКонтрагента = ГисТипКонтрагента.НеУказано };
}
internal static ГисСтатусВерсииДоговора Adopt(
HouseManagement.exportSupplyResourceContractResultTypeVersionStatus source)
{
switch (source)
{
case HouseManagement.exportSupplyResourceContractResultTypeVersionStatus.Posted: return ГисСтатусВерсииДоговора.Размещен;
case HouseManagement.exportSupplyResourceContractResultTypeVersionStatus.Terminated: return ГисСтатусВерсииДоговора.Расторгнут;
case HouseManagement.exportSupplyResourceContractResultTypeVersionStatus.Draft: return ГисСтатусВерсииДоговора.Проект;
case HouseManagement.exportSupplyResourceContractResultTypeVersionStatus.Annul: return ГисСтатусВерсииДоговора.Аннулирован;
default: throw NewUnexpectedObjectException(source);
}
}
internal static ГисСостояниеДоговора Adopt(
HouseManagement.exportSupplyResourceContractResultTypeContractState source)
{
switch (source)
{
case HouseManagement.exportSupplyResourceContractResultTypeContractState.Expired: return ГисСостояниеДоговора.ИстекСрокДействия;
case HouseManagement.exportSupplyResourceContractResultTypeContractState.NotTakeEffect: return ГисСостояниеДоговора.НеВступилВСилу;
case HouseManagement.exportSupplyResourceContractResultTypeContractState.Proceed: return ГисСостояниеДоговора.Действующий;
default: throw NewUnexpectedObjectException(source);
}
}
}
}

View File

@ -0,0 +1,125 @@
using Hcs.ClientApi.DataTypes;
using Hcs.ClientApi.RemoteCaller;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод получения списка адресных объектов по договору ресурсоснабжения
/// </summary>
public class HcsMethodExportSupplyResourceContractObjectAddress : HcsHouseManagementMethod
{
public HcsMethodExportSupplyResourceContractObjectAddress(HcsClientConfig config) : base(config)
{
EnableMinimalResponseWaitDelay = true;
CanBeRestarted = true;
}
/// <summary>
/// Запрос на экспорт объектов жилищного фонда из договора ресурсоснабжения
/// </summary>
public async Task<int> QueryAddresses(
ГисДоговор договор, Action<ГисАдресныйОбъект> resultHandler, CancellationToken token)
{
int numResults = 0;
Action<ГисАдресныйОбъект> countingHandler = (result) =>
{
numResults += 1;
resultHandler(result);
};
Guid? nextGuid = null;
while (true)
{
var paged = await QueryOneBatch(договор, countingHandler, nextGuid, token);
if (paged.IsLastPage) break;
nextGuid = paged.NextGuid;
numResults += 1;
}
return numResults;
}
private async Task<HcsPagedResultState> QueryOneBatch(
ГисДоговор договор, Action<ГисАдресныйОбъект> resultHandler,
Guid? firstGuid, CancellationToken token)
{
var itemNames = new List<HouseManagement.ItemsChoiceType29> { };
List<string> items = new List<string> { };
if (договор.ГуидВерсииДоговора != default)
{
itemNames.Add(HouseManagement.ItemsChoiceType29.ContractGUID);
items.Add(FormatGuid(договор.ГуидВерсииДоговора));
}
else
{
itemNames.Add(HouseManagement.ItemsChoiceType29.ContractRootGUID);
items.Add(FormatGuid(договор.ГуидДоговора));
}
// TODO: Проверить комментарий
// Если указан guid следующей страницы данных, добавляем его в параметры
// (на 20.12.2023 эта функция не работает, первый пакет содержит 1000 записей
// и запрос второго пакета с ExportObjectGUID возвращает "Bad request")
if (firstGuid != null)
{
itemNames.Add(HouseManagement.ItemsChoiceType29.ExportObjectGUID);
items.Add(FormatGuid(firstGuid));
}
var request = new HouseManagement.exportSupplyResourceContractObjectAddressRequest
{
Id = HcsConstants.SignedXmlElementId,
Items = items.ToArray(),
ItemsElementName = itemNames.ToArray(),
// TODO: Проверить хардкод версии
version = "13.1.1.1" // Номер версии из сообщения об ошибке сервера HCS
};
try
{
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.exportSupplyResourceContractObjectAddressDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
var result = RequireSingleItem
<HouseManagement.getStateResultExportSupplyResourceContractObjectAddress>(stateResult.Items);
foreach (var x in result.ObjectAddress)
{
resultHandler(Adopt(x));
}
return new HcsPagedResultState(result.Item);
}
catch (HcsNoResultsRemoteException)
{
return HcsPagedResultState.IsLastPageResultState;
}
}
private ГисАдресныйОбъект Adopt(
HouseManagement.exportSupplyResourceContractObjectAddressResultType source)
{
return new ГисАдресныйОбъект()
{
ТипЗдания = (source.HouseTypeSpecified ? source.HouseType.ToString() : null),
ГуидЗданияФиас = ParseGuid(source.FIASHouseGuid),
ГуидДоговора = ParseGuid(source.ContractRootGUID),
ГуидВерсииДоговора = ParseGuid(source.ContractGUID),
ГуидАдресногоОбъекта = ParseGuid(source.ObjectGUID),
НомерПомещения = source.ApartmentNumber,
НомерКомнаты = source.RoomNumber
};
}
}
}

View File

@ -0,0 +1,187 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Операции размещения и закрытия Лицевых счетов в ГИС ЖКХ
/// </summary>
public class HcsMethodImportAccountData : HcsHouseManagementMethod
{
public HcsMethodImportAccountData(HcsClientConfig config) : base(config)
{
CanBeRestarted = false;
}
/// <summary>
/// Размещение нового Лицевого счета если ГисЛицевойСчет.ГуидЛицевогоСчета не заполнен,
/// размещение новой версии лицевого счета если заполнен.
/// Возвращает Единый номер лицевого счета в ГИС ЖКХ для размещенного ЛС
/// http://open-gkh.ru/HouseManagement/importAccountRequest/Account.html
/// </summary>
public async Task<string> ImportAccount(
ГисДоговор договор, ГисЛицевойСчет лицевойСчет, CancellationToken token)
{
if (лицевойСчет == null) throw new ArgumentNullException(nameof(лицевойСчет));
if (договор == null) throw new ArgumentNullException(nameof(договор));
var account = ConvertToAccount(договор, лицевойСчет);
var result = await CallImportAccountData(account, token);
return result.UnifiedAccountNumber;
}
private HouseManagement.importAccountRequestAccount ConvertToAccount(
ГисДоговор договор, ГисЛицевойСчет лицевойСчет)
{
var account = new HouseManagement.importAccountRequestAccount()
{
TransportGUID = FormatGuid(Guid.NewGuid()),
AccountNumber = лицевойСчет.НомерЛицевогоСчета
};
if (лицевойСчет.ГуидЛицевогоСчета != default)
{
account.AccountGUID = FormatGuid(лицевойСчет.ГуидЛицевогоСчета);
}
if (договор.ГуидДоговора == null) throw new HcsException("Не указан ГуидДоговора для размещения ЛС");
var reasonRSO = new HouseManagement.AccountReasonsImportTypeSupplyResourceContract()
{
Items = [FormatGuid(договор.ГуидДоговора)],
ItemsElementName = [HouseManagement.ItemsChoiceType9.ContractGUID]
};
account.AccountReasons = new HouseManagement.AccountReasonsImportType()
{
SupplyResourceContract = [reasonRSO]
};
account.ItemElementName = HouseManagement.ItemChoiceType18.isRSOAccount;
account.Item = true;
if (IsArrayEmpty(лицевойСчет.Размещения))
throw new HcsException($"Не указаны размещения ЛС №{лицевойСчет.НомерЛицевогоСчета}");
account.Accommodation = лицевойСчет.Размещения.Select(ConvertToAccomodation).ToArray();
if (лицевойСчет.ДатаЗакрытия != null)
{
account.Closed = new HouseManagement.ClosedAccountAttributesType()
{
CloseDate = (DateTime)лицевойСчет.ДатаЗакрытия,
CloseReason = HcsHouseManagementNsi.ПричинаЗакрытияЛицевогоСчета.РасторжениеДоговора
};
}
account.PayerInfo = new HouseManagement.AccountTypePayerInfo()
{
Item = ConvertToAccountContragent(договор.Контрагент)
};
return account;
}
private object ConvertToAccountContragent(ГисКонтрагент контрагент)
{
if (контрагент == null) throw new HcsException("В договоре не заполнен Контрагент");
if (контрагент.ГуидОрганизации != null)
{
if (контрагент.ГуидВерсииОрганизации == null)
throw new HcsException("Для размещения ЛС в договоре с ЮЛ обязательно указание ГисКонтрагент.ГуидВерсииОрганизации");
return new HouseManagement.RegOrgVersionType()
{
orgVersionGUID = FormatGuid(контрагент.ГуидВерсииОрганизации)
};
}
if (контрагент.Индивид != null)
{
контрагент.Индивид.ПроверитьЗаполнениеСНИЛС();
контрагент.Индивид.ПроверитьЗаполнениеФИО();
return new HouseManagement.AccountIndType()
{
FirstName = контрагент.Индивид.Имя,
Patronymic = контрагент.Индивид.Отчество,
Surname = контрагент.Индивид.Фамилия,
Item = контрагент.Индивид.СНИЛСТолькоЦифры
};
}
throw new HcsException("Не указана ни организация ни индивид для размещения ЛС");
}
private HouseManagement.AccountTypeAccommodation ConvertToAccomodation(ГисРазмещениеЛС размещение)
{
if (размещение == null) throw new HcsException("Пустое размещение для ЛС");
var accomodation = new HouseManagement.AccountTypeAccommodation();
if (размещение.ГуидПомещения != null)
{
accomodation.ItemElementName = HouseManagement.ItemChoiceType19.PremisesGUID;
accomodation.Item = FormatGuid(размещение.ГуидПомещения);
}
else if (размещение.ГуидЖилойКомнаты != null)
{
accomodation.ItemElementName = HouseManagement.ItemChoiceType19.LivingRoomGUID;
accomodation.Item = FormatGuid(размещение.ГуидЖилойКомнаты);
}
else
{
throw new HcsException("Не указан ГУИД помещения или комнаты для ЛС");
}
if (размещение.ПроцентДоли != null)
{
accomodation.SharePercent = (decimal)размещение.ПроцентДоли;
accomodation.SharePercentSpecified = true;
}
return accomodation;
}
private async Task<(string UnifiedAccountNumber, DateTime UpdateDate)> CallImportAccountData(
HouseManagement.importAccountRequestAccount account,
CancellationToken token)
{
var request = new HouseManagement.importAccountRequest
{
Id = HcsConstants.SignedXmlElementId,
Account = [account]
// TODO: Проверить комментарий
//version = "13.1.1.1" // Версия указана в API
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.importAccountDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
var commonResult = ParseSingleImportResult(stateResult);
switch (commonResult.ItemElementName)
{
case HouseManagement.ItemChoiceType2.ImportAccount:
var accountResult = RequireType<HouseManagement.getStateResultImportResultCommonResultImportAccount>(commonResult.Item);
DateTime updateDate = commonResult.Items.OfType<DateTime>().FirstOrDefault();
if (updateDate == default)
throw new HcsException("В ответе сервера не указана дата обновления лицевого счета");
return (accountResult.UnifiedAccountNumber, updateDate);
default:
throw new HcsException($"Неожиданная структура в пакете результата: {commonResult.ItemElementName}");
}
}
}
}

View File

@ -0,0 +1,207 @@
using Hcs.ClientApi.DataTypes;
using Hcs.ClientApi.DeviceMeteringApi;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод передачи в ГИС ЖКХ сведений о приборе учета (новом или уже существующем)
/// </summary>
public class HcsMethodImportMeteringDeviceData : HcsHouseManagementMethod
{
public HcsMethodImportMeteringDeviceData(HcsClientConfig config) : base(config)
{
CanBeRestarted = false;
}
/// <summary>
/// Размещение нового прибора учета если ГисПриборУчета.ГуидПрибораУчета не заполнен,
/// размещение новой версии прибора учета если заполнен.
/// Возвращает GUID размещенного прибора учета
/// http://open-gkh.ru/HouseManagement/importMeteringDeviceDataRequest.html
/// </summary>
public async Task<Guid> ImportMeteringDevice(ГисПриборУчета прибор, CancellationToken token)
{
if (прибор == null) throw new ArgumentNullException(nameof(прибор));
var device = ConvertToMeteringDevice(прибор);
var result = await CallImportMeteringDevice(device, token);
return result.MeteringDeviceGuid;
}
/// <summary>
/// Выполняет архивацию(удаление) прибора учета в ГИС.
/// В структуре ПриборУчета используется только поле ГуидВерсииПрибора.
/// </summary>
public async Task<DateTime> ArchiveMeteringDevice(ГисПриборУчета приборУчета, CancellationToken token)
{
var archive = new HouseManagement.importMeteringDeviceDataRequestMeteringDeviceDeviceDataToUpdateArchiveDevice();
archive.ArchivingReason = HcsHouseManagementNsi.ПричинаАрхивацииПрибораУчета.ИстекСрокЭксплуатации;
var update = new HouseManagement.importMeteringDeviceDataRequestMeteringDeviceDeviceDataToUpdate();
update.MeteringDeviceVersionGUID = FormatGuid(приборУчета.ГуидВерсииПрибора);
update.Item = archive;
var device = new HouseManagement.importMeteringDeviceDataRequestMeteringDevice();
device.TransportGUID = FormatGuid(Guid.NewGuid());
device.Item = update;
var result = await CallImportMeteringDevice(device, token);
return result.UpdateDate;
}
private HouseManagement.importMeteringDeviceDataRequestMeteringDevice
ConvertToMeteringDevice(ГисПриборУчета прибор)
{
var device = new HouseManagement.importMeteringDeviceDataRequestMeteringDevice();
// ГИС будет возвращать ошибку с указанием этого идентификатора для определения элемента пакета
device.TransportGUID = FormatGuid(Guid.NewGuid());
if (прибор.ГуидВерсииПрибора != default)
{
var update = new HouseManagement.importMeteringDeviceDataRequestMeteringDeviceDeviceDataToUpdate();
update.MeteringDeviceVersionGUID = FormatGuid(прибор.ГуидВерсииПрибора);
update.Item = ConvertToFullInformationType(прибор);
device.Item = update;
}
else
{
device.Item = ConvertToFullInformationType(прибор);
}
return device;
}
private HouseManagement.MeteringDeviceFullInformationType ConvertToFullInformationType(
ГисПриборУчета прибор)
{
var basic = new HouseManagement.MeteringDeviceBasicCharacteristicsType();
basic.MeteringDeviceNumber = прибор.ЗаводскойНомер;
basic.MeteringDeviceModel = прибор.МодельПрибораУчета;
basic.MeteringDeviceStamp = прибор.МодельПрибораУчета;
basic.TemperatureSensor = false;
basic.PressureSensor = false;
basic.RemoteMeteringMode = прибор.РежимДистанционногоОпроса;
if (прибор.РежимДистанционногоОпроса)
basic.RemoteMeteringInfo = прибор.ОписаниеДистанционногоОпроса;
if (прибор.ДатаУстановки != null)
{
basic.InstallationDate = (DateTime)прибор.ДатаУстановки;
basic.InstallationDateSpecified = true;
}
if (прибор.ДатаВводаВЭксплуатацию != null)
{
basic.CommissioningDate = (DateTime)прибор.ДатаВводаВЭксплуатацию;
basic.CommissioningDateSpecified = true;
}
if (прибор.ДатаПоследнейПоверки != null)
{
basic.FirstVerificationDate = (DateTime)прибор.ДатаПоследнейПоверки;
basic.FirstVerificationDateSpecified = true;
}
if (прибор.ДатаИзготовления != null)
{
basic.FactorySealDate = (DateTime)прибор.ДатаИзготовления;
basic.FactorySealDateSpecified = true;
}
switch (прибор.ВидПрибораУчета)
{
case ГисВидПрибораУчета.ОДПУ:
if (IsArrayEmpty(прибор.ГуидыЗданийФиас))
throw new HcsException("Для ОДПУ необходимо указать ГУИД здания ФИАС");
basic.Item = new HouseManagement.MeteringDeviceBasicCharacteristicsTypeCollectiveDevice()
{
FIASHouseGuid = прибор.ГуидыЗданийФиас.Select(FormatGuid).ToArray()
};
break;
case ГисВидПрибораУчета.НежилоеПомещение:
if (IsArrayEmpty(прибор.ГуидыЛицевыхСчетов))
throw new HcsException("Для размещения ПУ нежилого помещения следует указать ГУИД лицевого счета");
if (IsArrayEmpty(прибор.ГуидыПомещений))
throw new HcsException("Для размещения ПУ нежилого помещения следует указать ГУИД помещения");
basic.Item = new HouseManagement.MeteringDeviceBasicCharacteristicsTypeNonResidentialPremiseDevice()
{
AccountGUID = прибор.ГуидыЛицевыхСчетов.Select(FormatGuid).ToArray(),
PremiseGUID = прибор.ГуидыПомещений.Select(FormatGuid).ToArray()
};
break;
default:
throw new NotImplementedException(
"Не реализовано размещение вида прибора: " + прибор.ВидПрибораУчета);
}
var electric = new HouseManagement.MunicipalResourceElectricBaseType();
electric.Unit = HouseManagement.MunicipalResourceElectricBaseTypeUnit.Item245; // Константа ОКЕИ 245=кВт*ч
electric.UnitSpecified = true;
electric.MeteringValueT1 = HcsDeviceMeteringUtil.ConvertMeterReading(прибор.ПоказаниеТ1, true);
electric.MeteringValueT2 = HcsDeviceMeteringUtil.ConvertMeterReading(прибор.ПоказаниеТ2, false);
electric.MeteringValueT3 = HcsDeviceMeteringUtil.ConvertMeterReading(прибор.ПоказаниеТ3, false);
if (прибор.КоэффициентТрансформацииУказан)
{
electric.TransformationRatio = прибор.КоэффициентТрансформации;
electric.TransformationRatioSpecified = true;
}
return new HouseManagement.MeteringDeviceFullInformationType()
{
BasicChatacteristicts = basic,
// TODO: Проверить комментарий
Item = true, // NotLinkedWithMetering (нет связей с другими приборами)
Items = [electric]
};
}
private async Task<(Guid MeteringDeviceGuid, DateTime UpdateDate)> CallImportMeteringDevice(
HouseManagement.importMeteringDeviceDataRequestMeteringDevice device,
CancellationToken token)
{
HouseManagement.importMeteringDeviceDataRequestMeteringDevice[] devices = { device };
var request = new HouseManagement.importMeteringDeviceDataRequest
{
Id = HcsConstants.SignedXmlElementId,
MeteringDevice = devices
// TODO: Проверить хардкод версии
//version = "13.1.1.1" // Версия указана в API
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.importMeteringDeviceDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
var commonResult = ParseSingleImportResult(stateResult);
switch (commonResult.ItemElementName)
{
case HouseManagement.ItemChoiceType2.importMeteringDevice:
var deviceResult = RequireType<HouseManagement.getStateResultImportResultCommonResultImportMeteringDevice>(commonResult.Item);
DateTime updateDate = commonResult.Items.OfType<DateTime>().FirstOrDefault();
if (updateDate == default) throw new HcsException("В ответе сервера не указана дата обновления прибора учета");
return (ParseGuid(deviceResult.MeteringDeviceGUID), updateDate);
default:
throw new HcsException($"Неожиданная структура в пакете результата: {commonResult.ItemElementName}");
}
}
}
}

View File

@ -0,0 +1,386 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод передачи в ГИС ЖКХ сведений о договоре РСО (новом или уже существующем)
/// </summary>
public class HcsMethodImportSupplyResourceContractData : HcsHouseManagementMethod
{
public HcsMethodImportSupplyResourceContractData(HcsClientConfig config) : base(config)
{
CanBeRestarted = false;
}
/// <summary>
/// Размещение нового договора если ГисДоговор.ГуидДоговора не заполнен,
/// размещение новой версии договора если заполнен
/// http://open-gkh.ru/HouseManagement/SupplyResourceContractType.html
/// </summary>
public async Task<DateTime> ImportContract(
ГисДоговор договор, IEnumerable<ГисАдресныйОбъект> адреса, CancellationToken token)
{
if (договор == null) throw new ArgumentNullException(nameof(договор));
if (адреса == null || !адреса.Any())
throw new ArgumentException($"Для импорта нового договора {договор.НомерДоговора}" +
" необходимо указать хотя-бы один адресный объект");
Guid? contractGuid = (договор.ГуидДоговора == default) ? null : договор.ГуидДоговора;
var contract = ConvertToSupplyResourceContract(договор, адреса);
return await CallImportContract(contractGuid, contract, token);
}
/// <summary>
/// Вызывает удаленный метод импорта договора с @contractGuid и данными операции импорта @contractItem.
/// Чтобы перевести договор из состояния "Проект" в состояние "Размещен" необходимо вызвать
/// importSupplyResourceContractProjectData/PlacingContractProject=true
/// http://open-gkh.ru/HouseManagement/importSupplyResourceContractRequest.html
/// </summary>
private async Task<DateTime> CallImportContract(
Guid? contractGuid, object contractItem, CancellationToken token)
{
var contract = new HouseManagement.importSupplyResourceContractRequestContract();
HouseManagement.importSupplyResourceContractRequestContract[] contracts = { contract };
// Передаем условие запроса - гуид версии договора.
// При создании нового договора атрибут importSupplyResourceContractRequest.Contract.ContractGUID не заполняется.
if (contractGuid != null)
{
contract.ItemElementName = HouseManagement.ItemChoiceType26.ContractRootGUID;
contract.Item = FormatGuid(contractGuid);
}
contract.TransportGUID = FormatGuid(Guid.NewGuid());
contract.Item1 = contractItem;
var request = new HouseManagement.importSupplyResourceContractRequest
{
Id = HcsConstants.SignedXmlElementId,
Contract = contracts
// TODO: Проверить комментарий
//version = "13.1.1.1" // Версия указана в API
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.importSupplyResourceContractDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
var commonResult = ParseSingleImportResult(stateResult);
switch (commonResult.ItemElementName)
{
case HouseManagement.ItemChoiceType2.ImportSupplyResourceContract:
var contractResult = RequireType<HouseManagement.getStateResultImportResultCommonResultImportSupplyResourceContract>(commonResult.Item);
var датаИмпорта = RequireSingleItem<DateTime>(commonResult.Items);
return датаИмпорта;
default:
throw new HcsException($"Неожиданная структура в пакете результата: {commonResult.ItemElementName}");
}
}
/// <summary>
/// Преобразует модель данных ГисДоговор в модель данных HouseManagement.SupplyResourceContractType
/// http://open-gkh.ru/HouseManagement/SupplyResourceContractType.html
/// </summary>
private HouseManagement.SupplyResourceContractType ConvertToSupplyResourceContract(
ГисДоговор договор, IEnumerable<ГисАдресныйОбъект> адреса)
{
var contract = new HouseManagement.SupplyResourceContractType();
if (договор.ЭтоДоговорНежилогоПомещения)
{
var isNotContract = new HouseManagement.SupplyResourceContractTypeIsNotContract();
isNotContract.ContractNumber = договор.НомерДоговора;
if (договор.ДатаЗаключения != null)
isNotContract.SigningDate = (DateTime)договор.ДатаЗаключения;
if (!IsArrayEmpty(договор.ПриложенияДоговора))
isNotContract.ContractAttachment = договор.ПриложенияДоговора.Select(ConvertToAttachment).ToArray();
contract.Item = isNotContract;
}
if (договор.ЭтоДоговорИКУ)
{
var isContract = new HouseManagement.SupplyResourceContractTypeIsContract();
isContract.ContractNumber = договор.НомерДоговора;
var нужнаДатаЗаключения = (договор.ДатаЗаключения != null) ? (DateTime)договор.ДатаЗаключения : DateTime.Now;
isContract.SigningDate = isContract.EffectiveDate = нужнаДатаЗаключения;
// Для ИКУ обязательно приложение файла договора (иначе 400 Bad request)
if (IsArrayEmpty(договор.ПриложенияДоговора))
throw new HcsException($"Для размещения договора ИКУ {договор.НомерДоговора} необходимо указать файл приложения");
isContract.ContractAttachment = договор.ПриложенияДоговора.Select(ConvertToAttachment).ToArray();
contract.Item = isContract;
}
if (договор.Контрагент == null)
throw new HcsException($"В договоре {договор.НомерДоговора} не указан Контрагент");
if (договор.ЭтоДоговорНежилогоПомещения)
{
contract.Item1 = new HouseManagement.SupplyResourceContractTypeApartmentBuildingOwner()
{
Item = ConvertToDRSOContragent(договор.Контрагент)
};
contract.ContractBase = [HcsHouseManagementNsi.ОснованиеЗаключенияДоговора.ЗаявлениеПотребителя];
}
if (договор.ЭтоДоговорИКУ)
{
if (договор.Контрагент.ГуидОрганизации == null)
throw new HcsException($"В договоре ИКУ {договор.НомерДоговора} не указан ГУИД организации");
contract.Item1 = new HouseManagement.SupplyResourceContractTypeOrganization()
{
orgRootEntityGUID = FormatGuid(договор.Контрагент.ГуидОрганизации)
};
contract.ContractBase = [HcsHouseManagementNsi.ОснованиеЗаключенияДоговора.ДоговорУправления];
}
Guid contractSubjectGuid = Guid.NewGuid();
contract.ContractSubject = [
new HouseManagement.SupplyResourceContractTypeContractSubject() {
ServiceType = HcsHouseManagementNsi.ElectricSupplyServiceType,
MunicipalResource = HcsHouseManagementNsi.ElectricSupplyMunicipalResource,
StartSupplyDate = (договор.ДатаЗаключения != null ? (DateTime)договор.ДатаЗаключения : DateTime.Now),
EndSupplyDate = DateTime.Now.AddYears(50),
TransportGUID = FormatGuid(contractSubjectGuid)
}
];
// Порядок размещения информации о начислениях за коммунальные услуги ведется.
// "D" - в разрезе договора. "O" - в разрезе объектов. Обязательно для ИКУ.
if (договор.ЭтоДоговорИКУ)
{
contract.AccrualProcedure = HouseManagement.SupplyResourceContractTypeAccrualProcedure.D;
contract.AccrualProcedureSpecified = true;
}
if (договор.ЭтоДоговорИКУ)
{
// Размещение информации о начислениях за коммунальные услуги осуществляет.
// R(SO)- РСО. P(roprietor)-Исполнитель коммунальных услуг. Обязательно для ИКУ.
contract.CountingResource =
договор.НачисленияРазмещаетРСО ?
HouseManagement.SupplyResourceContractTypeCountingResource.R :
HouseManagement.SupplyResourceContractTypeCountingResource.P;
contract.CountingResourceSpecified = true;
if (договор.НачисленияРазмещаетРСО)
{
if (договор.ПриборыРазмещаетРСО)
{
contract.MeteringDeviceInformation = true;
contract.MeteringDeviceInformationSpecified = true;
}
}
// В договоре нет планового объема потребления
contract.IsPlannedVolume = false;
}
if (договор.НачисленияРазмещаетРСО)
{
// Cрок предоставления платежных документов, не позднее
contract.BillingDate = new HouseManagement.SupplyResourceContractTypeBillingDate()
{
Date = 15,
DateType = HouseManagement.SupplyResourceContractTypeBillingDateDateType.N // Следующий месяц
};
// Срок предоставления информации о поступивших платежах, не позднее
contract.ProvidingInformationDate = new HouseManagement.SupplyResourceContractTypeProvidingInformationDate()
{
Date = 15,
DateType = HouseManagement.SupplyResourceContractTypeProvidingInformationDateDateType.N // Следующий месяц
};
}
if (договор.ПриборыРазмещаетРСО)
{
// Период передачи текущих показаний должен быть указан, если ИПУ размещает РСО
contract.Period = new HouseManagement.SupplyResourceContractTypePeriod()
{
Start = new HouseManagement.SupplyResourceContractTypePeriodStart()
{
StartDate = 1
},
End = new HouseManagement.SupplyResourceContractTypePeriodEnd()
{
EndDate = 25
}
};
}
// Срок представления (выставления) платежных документов, не позднее. Является обязательным,
// если вторая сторона договора отличается от "Управляющая организация".
if (договор.ЭтоДоговорНежилогоПомещения)
{
contract.BillingDate = new HouseManagement.SupplyResourceContractTypeBillingDate()
{
Date = -1, // последний день месяца
DateType = HouseManagement.SupplyResourceContractTypeBillingDateDateType.N // следующего месяца
};
// Объем поставки определяется на основании прибора учета (признак необходим чтобы
// ГИС разрешал размещать ПУ на лицевых счетах договора) (признак запрещен для ИКУ)
contract.VolumeDepends = true;
contract.VolumeDependsSpecified = true;
// Период передачи текущих показаний должен быть указан если указано VolumeDepends
contract.Period = new HouseManagement.SupplyResourceContractTypePeriod()
{
Start = new HouseManagement.SupplyResourceContractTypePeriodStart()
{
StartDate = 1
},
End = new HouseManagement.SupplyResourceContractTypePeriodEnd()
{
EndDate = 25
}
};
}
// Срок действия договора
contract.ItemsElementName = [HouseManagement.ItemsChoiceType25.IndefiniteTerm];
contract.Items = [true];
// Данные об объекте жилищного фонда. При импорте договора должен быть добавлен
// как минимум один адрес объекта жилищного фонда.
if (адреса != null)
{
contract.ObjectAddress = адреса.Select(
адрес => ConvertToObjectAddress(договор, адрес, contractSubjectGuid)).ToArray();
}
return contract;
}
/// <summary>
/// Сборка сведений для отправки указателя на файл приложения к договору
/// http://open-gkh.ru/Base/AttachmentType.html
/// </summary>
private HouseManagement.AttachmentType ConvertToAttachment(ГисПриложение приложение)
{
return new HouseManagement.AttachmentType()
{
Name = приложение.ИмяПриложения ?? throw new HcsException("Не указано имя файла приложения"),
Description = приложение.ОписаниеПриложения != null ? приложение.ОписаниеПриложения : приложение.ИмяПриложения,
AttachmentHASH = приложение.ХэшПриложения ?? throw new HcsException("Не указан хэш файла приложения"),
Attachment = new HouseManagement.Attachment()
{
AttachmentGUID = FormatGuid(приложение.ГуидПриложения)
}
};
}
private HouseManagement.SupplyResourceContractTypeObjectAddress ConvertToObjectAddress(
ГисДоговор договор, ГисАдресныйОбъект адрес, Guid contractSubjectGuid)
{
// Дату начала снабжения выводим из даты заключения договора
DateTime startSupplyDate = (договор.ДатаЗаключения != null) ?
(DateTime)договор.ДатаЗаключения : DateTime.Now;
// Ссылка на пару определения ресурсов предмета договора
var pair = new HouseManagement.SupplyResourceContractTypeObjectAddressPair();
pair.PairKey = FormatGuid(contractSubjectGuid);
pair.StartSupplyDate = startSupplyDate;
// TODO: Проверить комментарий
pair.EndSupplyDateSpecified = false; // Не указана дата окончания поставки ресурса
var address = new HouseManagement.SupplyResourceContractTypeObjectAddress()
{
TransportGUID = FormatGuid(Guid.NewGuid()),
FIASHouseGuid = FormatGuid(адрес.ГуидЗданияФиас),
ApartmentNumber = MakeEmptyNull(адрес.НомерПомещения),
RoomNumber = MakeEmptyNull(адрес.НомерКомнаты),
Pair = [pair]
};
if (!string.IsNullOrEmpty(адрес.ТипЗдания))
{
address.HouseTypeSpecified = true;
address.HouseType = ConvertToHouseType(адрес.ТипЗдания);
}
return address;
}
private HouseManagement.ObjectAddressTypeHouseType ConvertToHouseType(string типЗдания)
{
return типЗдания switch
{
ГисАдресныйОбъект.ИзвестныеТипыЗдания.MKD => HouseManagement.ObjectAddressTypeHouseType.MKD,
ГисАдресныйОбъект.ИзвестныеТипыЗдания.ZHD => HouseManagement.ObjectAddressTypeHouseType.ZHD,
ГисАдресныйОбъект.ИзвестныеТипыЗдания.ZHDBlockZastroyki => HouseManagement.ObjectAddressTypeHouseType.ZHDBlockZastroyki,
_ => throw new HcsException($"Указан неизвестный тип здания [{типЗдания}]")
};
}
/// <summary>
/// Преобразует реквизиты контрагента в модель данных ГИС ЖКХ
/// </summary>
private object ConvertToDRSOContragent(ГисКонтрагент контрагент)
{
if (контрагент.ГуидОрганизации != null)
{
return new HouseManagement.DRSORegOrgType()
{
orgRootEntityGUID = FormatGuid(контрагент.ГуидОрганизации)
};
}
if (контрагент.Индивид != null)
{
контрагент.Индивид.ПроверитьЗаполнениеСНИЛС();
контрагент.Индивид.ПроверитьЗаполнениеФИО();
return new HouseManagement.DRSOIndType()
{
Patronymic = MakeEmptyNull(контрагент.Индивид.Отчество),
FirstName = MakeEmptyNull(контрагент.Индивид.Имя),
Surname = MakeEmptyNull(контрагент.Индивид.Фамилия),
Item = MakeEmptyNull(контрагент.Индивид.СНИЛСТолькоЦифры) // В СНИЛС требуется только 11 цифр
};
}
return false;
}
/// <summary>
/// Выполнение операции размещения факта расторжения договора
/// http://open-gkh.ru/HouseManagement/importSupplyResourceContractRequest/Contract/TerminateContract.html
/// </summary>
public async Task<DateTime> TerminateContract(
ГисДоговор договор, DateTime датаРасторжения, CancellationToken token)
{
var terminate = new HouseManagement.importSupplyResourceContractRequestContractTerminateContract();
terminate.Terminate = датаРасторжения;
terminate.ReasonRef = HcsHouseManagementNsi.ПричинаРасторженияДоговора.ПоВзаимномуСогласиюСторон;
return await CallImportContract(договор.ГуидДоговора, terminate, token);
}
/// <summary>
/// Выполнение операции размещения факта аннулирование договора
/// http://open-gkh.ru/HouseManagement/AnnulmentType.html
/// </summary>
public async Task<DateTime> AnnulContract(ГисДоговор договор, string причина, CancellationToken token)
{
var annulment = new HouseManagement.AnnulmentType();
annulment.ReasonOfAnnulment = причина;
return await CallImportContract(договор.ГуидДоговора, annulment, token);
}
}
}

View File

@ -0,0 +1,133 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод добавления/изменения/удаления элементов списка адресных объектов
/// в договоре ресурсоснабжения
/// </summary>
public class HcsMethodImportSupplyResourceContractObjectAddress : HcsHouseManagementMethod
{
public HcsMethodImportSupplyResourceContractObjectAddress
(HcsClientConfig config) : base(config)
{
EnableMinimalResponseWaitDelay = true;
CanBeRestarted = true;
}
public async Task ImportObjectAddresses(
ГисДоговор договор,
IEnumerable<ГисАдресныйОбъект> адресаДляРазмещения,
IEnumerable<ГисАдресныйОбъект> адресаДляУдаления,
CancellationToken token)
{
if (договор == null) throw new ArgumentNullException(nameof(договор));
var list = new List<HouseManagement.importSupplyResourceContractObjectAddressRequestObjectAddress>();
if (адресаДляРазмещения != null) list.AddRange(адресаДляРазмещения.Select(AdoptForLoading));
if (адресаДляУдаления != null) list.AddRange(адресаДляУдаления.Select(AdoptForRemoval));
var request = new HouseManagement.importSupplyResourceContractObjectAddressRequest()
{
Id = HcsConstants.SignedXmlElementId,
Item = FormatGuid(договор.ГуидДоговора),
ItemElementName = HouseManagement.ItemChoiceType28.ContractRootGUID,
ObjectAddress = list.ToArray()
// TODO: Проверить комментарий
//version = "13.1.1.1" // Версия указана в API
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.importSupplyResourceContractObjectAddressDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
ParseImportResults(stateResult, list.Count(), true);
}
/// <summary>
/// Готовит структуру адресного объекта для удаления в ГИС
/// </summary>
private HouseManagement.importSupplyResourceContractObjectAddressRequestObjectAddress AdoptForRemoval(
ГисАдресныйОбъект адрес)
{
if (адрес == null) throw new ArgumentNullException(nameof(адрес));
Guid transportGuid = Guid.NewGuid();
bool deleteObject = true;
return new HouseManagement.importSupplyResourceContractObjectAddressRequestObjectAddress()
{
TransportGUID = FormatGuid(transportGuid),
ObjectGUID = FormatGuid(адрес.ГуидАдресногоОбъекта),
Item = deleteObject
};
}
/// <summary>
/// Готовит структуру адресного объекта для добавления/обновления в ГИС
/// </summary>
private HouseManagement.importSupplyResourceContractObjectAddressRequestObjectAddress AdoptForLoading(
ГисАдресныйОбъект адрес)
{
if (адрес == null) throw new ArgumentNullException(nameof(адрес));
Guid transportGuid = Guid.NewGuid();
var serviceType = new HouseManagement.ContractSubjectObjectAdressTypeServiceType()
{
Code = HcsHouseManagementNsi.ElectricSupplyServiceType.Code,
GUID = HcsHouseManagementNsi.ElectricSupplyServiceType.GUID,
Name = HcsHouseManagementNsi.ElectricSupplyServiceType.Name
};
var municipalResource = new HouseManagement.ContractSubjectObjectAdressTypeMunicipalResource()
{
Code = HcsHouseManagementNsi.ElectricSupplyMunicipalResource.Code,
GUID = HcsHouseManagementNsi.ElectricSupplyMunicipalResource.GUID,
Name = HcsHouseManagementNsi.ElectricSupplyMunicipalResource.Name
};
var pair = new HouseManagement.importSupplyResourceContractObjectAddressRequestObjectAddressLoadObjectPair()
{
// TODO: Проверить комментарий
TransportGUID = FormatGuid(Guid.NewGuid()), // Получал BadRequest пока не сделал здесь новый GUID
ServiceType = serviceType,
MunicipalResource = municipalResource,
// TODO: Проверить комментарий
StartSupplyDate = DateTime.Now // В договоре нет даты начала снабжения адреса, ставлю что-нибудь
};
var loadObject = new HouseManagement.importSupplyResourceContractObjectAddressRequestObjectAddressLoadObject()
{
FIASHouseGuid = FormatGuid(адрес.ГуидЗданияФиас),
ApartmentNumber = MakeEmptyNull(адрес.НомерПомещения),
RoomNumber = MakeEmptyNull(адрес.НомерКомнаты),
Pair = [pair]
};
var address = new HouseManagement.importSupplyResourceContractObjectAddressRequestObjectAddress()
{
TransportGUID = FormatGuid(transportGuid),
Item = loadObject
};
if (адрес.ГуидАдресногоОбъекта != default)
{
address.ObjectGUID = FormatGuid(адрес.ГуидАдресногоОбъекта);
}
return address;
}
}
}

View File

@ -0,0 +1,80 @@
using Hcs.ClientApi.DataTypes;
using Hcs.Service.Async.HouseManagement.v14_5_0_1;
using System;
using System.Threading;
using System.Threading.Tasks;
using HouseManagement = Hcs.Service.Async.HouseManagement.v14_5_0_1;
namespace Hcs.ClientApi.HouseManagementApi
{
/// <summary>
/// Метод отправки в ГИС проекта договора ресурсоснабжения, удаления
/// проекта договора РСО, перевода проекта в статус Размещенные
/// </summary>
public class HcsMethodImportSupplyResourceContractProject : HcsHouseManagementMethod
{
public HcsMethodImportSupplyResourceContractProject
(HcsClientConfig config) : base(config)
{
EnableMinimalResponseWaitDelay = true;
CanBeRestarted = false;
}
/// <summary>
/// Выполнение удаления в ГИС проекта договора
/// </summary>
public async Task DeleteContractProject(ГисДоговор договор, CancellationToken token)
{
await DoContractProjectOperation(
договор, Item1ChoiceType10.DeleteContractProject, token);
}
/// <summary>
/// Выполнение перевода проекта договора в статус Размещен
/// </summary>
public async Task PlaceContractProject(ГисДоговор договор, CancellationToken token)
{
await DoContractProjectOperation(
договор, Item1ChoiceType10.PlacingContractProject, token);
}
private async Task DoContractProjectOperation(
ГисДоговор договор, Item1ChoiceType10 operationType, CancellationToken token)
{
if (договор == null) throw new ArgumentNullException(nameof(договор));
if (договор.ГуидВерсииДоговора == default)
throw new ArgumentException("Для проекта договора не указан ГУИД версии");
var contract = new HouseManagement.importSupplyResourceContractProjectRequestContract()
{
TransportGUID = FormatGuid(Guid.NewGuid()),
ItemElementName = ItemChoiceType29.ContractRootGUID,
Item = FormatGuid(договор.ГуидДоговора),
// TODO: Проверить комментарий
// Если удалять версию проекта то остается предыдущая версия проекта
//ItemElementName = ItemChoiceType29.ContractGUID,
//Item = FormatGuid(договор.ГуидВерсииДоговора),
Item1ElementName = operationType,
Item1 = true,
};
var request = new HouseManagement.importSupplyResourceContractProjectRequest()
{
Id = HcsConstants.SignedXmlElementId,
Contract = [contract],
// TODO: Проверить комментарий
//version = "13.1.1.1" // Версия указана в API
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var ackResponse = await portClient.importSupplyResourceContractProjectDataAsync(
CreateRequestHeader(), request);
return ackResponse.AckRequest.Ack;
}, token);
ParseSingleImportResult(stateResult);
}
}
}

View File

@ -0,0 +1,10 @@
namespace Hcs.ClientApi
{
/// <summary>
/// Интерфейс для механизма вывода отладочных сообщений для обработки вызывающей системой
/// </summary>
public interface IHcsLogger
{
void WriteLine(string message);
}
}

View File

@ -0,0 +1,11 @@
namespace Hcs.ClientApi
{
/// <summary>
/// Интерфейс для механизма захвата отправляемых и принимаемых
/// SOAP сообщений в ходе коммуникации с ГИС ЖКХ
/// </summary>
public interface IHcsMessageCapture
{
void CaptureMessage(bool sentOrReceived, string messageBody);
}
}

View File

@ -0,0 +1,133 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OrgRegistryCommon = Hcs.Service.Async.OrgRegistryCommon.v14_5_0_1;
namespace Hcs.ClientApi.OrgRegistryCommonApi
{
/// <summary>
/// Операции экспорта данных из реестра организаций ГИС ЖКХ
/// </summary>
public class HcsMethodExportOrgRegistry : HcsOrgRegistryCommonMethod
{
public HcsMethodExportOrgRegistry(HcsClientConfig config) : base(config)
{
EnableMinimalResponseWaitDelay = true;
CanBeRestarted = true;
}
/// <summary>
/// Возвращает карточки организации в ГИС ЖКХ по номеру ОГРН организации.
/// При отсутствии результатов будет выброшено HcsNoResultsRemoteException.
/// </summary>
public async Task<IEnumerable<ГисОрганизация>> GetOrgByOgrn(
string ogrn, string kpp, CancellationToken token)
{
if (string.IsNullOrEmpty(ogrn)) throw new ArgumentException("Не указан ОГРН для поиска организации");
if (ogrn.Length != ГисОрганизация.ДлинаОГРН && ogrn.Length != ГисОрганизация.ДлинаОГРНИП)
{
throw new ArgumentException(
$"В строке ОГРН допускается или {ГисОрганизация.ДлинаОГРН} или {ГисОрганизация.ДлинаОГРНИП} символов: {ogrn}");
}
var criteria = new OrgRegistryCommon.exportOrgRegistryRequestSearchCriteria();
if (!string.IsNullOrEmpty(kpp))
{
criteria.ItemsElementName = [OrgRegistryCommon.ItemsChoiceType3.OGRN, OrgRegistryCommon.ItemsChoiceType3.KPP];
criteria.Items = [ogrn, kpp];
}
else
{
if (ogrn.Length == ГисОрганизация.ДлинаОГРНИП)
criteria.ItemsElementName = [OrgRegistryCommon.ItemsChoiceType3.OGRNIP];
else criteria.ItemsElementName = [OrgRegistryCommon.ItemsChoiceType3.OGRN];
criteria.Items = [ogrn];
}
var request = new OrgRegistryCommon.exportOrgRegistryRequest
{
Id = HcsConstants.SignedXmlElementId,
SearchCriteria = [criteria]
};
var stateResult = await SendAndWaitResultAsync(request, async (portClient) =>
{
var response = await portClient.exportOrgRegistryAsync(CreateRequestHeader(), request);
return response.AckRequest.Ack;
}, token);
// В возвращаемой структуре мало ценной информации, только ГУИД организации в ГИС ЖКХ
// (необходимый для размещения договоров) и ГУИД поставщика информации OrgPPAGUID.
// Для организаций с филиалами может вернуться список из нескольких ГУИД.
return stateResult.Items
.OfType<OrgRegistryCommon.exportOrgRegistryResultType>()
.Select(x => Adopt(x));
}
private ГисОрганизация Adopt(OrgRegistryCommon.exportOrgRegistryResultType orgResult)
{
if (orgResult.OrgVersion == null)
throw new HcsException("В структуре exportOrgRegistryResultType не указано поле OrgVersion");
var организация = new ГисОрганизация()
{
ГуидОрганизации = ParseGuid(orgResult.orgRootEntityGUID),
ГуидВерсииОрганизации = ParseGuid(orgResult.OrgVersion.orgVersionGUID),
Действующая = orgResult.OrgVersion.IsActual
};
switch (orgResult.OrgVersion.Item)
{
case OrgRegistryCommon.LegalType legal:
организация.ТипОрганизации = ГисТипОрганизации.ЮЛ;
организация.ИНН = legal.INN;
организация.КПП = legal.KPP;
организация.ОГРН = legal.OGRN;
организация.ОКОПФ = legal.OKOPF;
организация.КраткоеИмяОрганизации = legal.ShortName;
организация.ПолноеИмяОрганизации = legal.FullName;
организация.ЮридическийАдрес = legal.Address;
if (legal.ActivityEndDateSpecified)
организация.ДатаЛиквидации = legal.ActivityEndDate;
break;
case OrgRegistryCommon.EntpsType entps:
организация.ТипОрганизации = ГисТипОрганизации.ИП;
организация.ИНН = entps.INN;
организация.ОГРН = entps.OGRNIP;
организация.Фамилия = entps.Surname;
организация.Имя = entps.FirstName;
организация.Отчество = entps.Patronymic;
break;
case OrgRegistryCommon.SubsidiaryType sub:
организация.ТипОрганизации = ГисТипОрганизации.Филиал;
организация.ИНН = sub.INN;
организация.КПП = sub.KPP;
организация.ОГРН = sub.OGRN;
организация.ОКОПФ = sub.OKOPF;
организация.КраткоеИмяОрганизации = sub.ShortName;
организация.ПолноеИмяОрганизации = sub.FullName;
организация.ЮридическийАдрес = sub.Address;
if (sub.ActivityEndDateSpecified)
организация.ДатаЛиквидации = sub.ActivityEndDate;
break;
case OrgRegistryCommon.ForeignBranchType foreign:
организация.ТипОрганизации = ГисТипОрганизации.Иностранный;
организация.ИНН = foreign.INN;
организация.КПП = foreign.KPP;
организация.КраткоеИмяОрганизации = foreign.ShortName;
организация.ПолноеИмяОрганизации = foreign.FullName;
организация.ЮридическийАдрес = foreign.Address;
break;
}
return организация;
}
}
}

View File

@ -0,0 +1,48 @@
using Hcs.ClientApi.DataTypes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.OrgRegistryCommonApi
{
public class HcsOrgRegistryCommonApi
{
public HcsClientConfig Config { get; private set; }
public HcsOrgRegistryCommonApi(HcsClientConfig config)
{
this.Config = config;
}
/// <summary>
/// Возвращает ГУИДы действующих организаций в ГИС ЖКХ по номеру ОГРН (КПП может быть не указан).
/// Если организации не найдены, возвращается пустой список.
/// </summary>
public async Task<IEnumerable<Guid>> GetOrgRootEntityGuidByOgrn(
string ogrn, string kpp, CancellationToken token = default)
{
var orgs = await GetOrgByOgrn(ogrn, kpp, token);
return orgs.Where(x => x.Действующая).Select(x => x.ГуидОрганизации);
}
/// <summary>
/// Возвращает карточки организации в ГИС ЖКХ по номеру ОГРН (КПП может быть не указан).
/// Если организации не найдены, возвращается пустой список.
/// </summary>
public async Task<IEnumerable<ГисОрганизация>> GetOrgByOgrn(
string ogrn, string kpp, CancellationToken token = default)
{
try
{
var method = new HcsMethodExportOrgRegistry(Config);
return await method.GetOrgByOgrn(ogrn, kpp, token);
}
catch (HcsNoResultsRemoteException)
{
return [];
}
}
}
}

View File

@ -0,0 +1,112 @@
using Hcs.ClientApi.RemoteCaller;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OrgRegistryCommon = Hcs.Service.Async.OrgRegistryCommon.v14_5_0_1;
namespace Hcs.Service.Async.OrgRegistryCommon.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.OrgRegistryCommonApi
{
public class HcsOrgRegistryCommonMethod : HcsRemoteCallMethod
{
public HcsEndPoints EndPoint => HcsEndPoints.OrgRegistryCommonAsync;
public OrgRegistryCommon.ISRequestHeader CreateRequestHeader() =>
HcsRequestHelper.CreateHeader<OrgRegistryCommon.ISRequestHeader>(ClientConfig);
public HcsOrgRegistryCommonMethod(HcsClientConfig config) : base(config) { }
public System.ServiceModel.EndpointAddress RemoteAddress
=> GetEndpointAddress(HcsConstants.EndPointLocator.GetPath(EndPoint));
private OrgRegistryCommon.RegOrgPortsTypeAsyncClient NewPortClient()
{
var client = new OrgRegistryCommon.RegOrgPortsTypeAsyncClient(_binding, RemoteAddress);
ConfigureEndpointCredentials(client.Endpoint, client.ClientCredentials);
return client;
}
public async Task<IHcsGetStateResult> SendAndWaitResultAsync(
object request,
Func<OrgRegistryCommon.RegOrgPortsTypeAsyncClient, Task<IHcsAck>> sender,
CancellationToken token)
{
token.ThrowIfCancellationRequested();
while (true)
{
try
{
return await SendAndWaitResultAsyncImpl(request, sender, token);
}
catch (HcsRestartTimeoutException e)
{
if (!CanBeRestarted) throw new HcsException("Превышен лимит ожидания выполнения запроса", e);
Log($"Перезапускаем запрос типа {request.GetType().Name}...");
}
}
}
private async Task<IHcsGetStateResult> SendAndWaitResultAsyncImpl(
object request,
Func<OrgRegistryCommon.RegOrgPortsTypeAsyncClient, Task<IHcsAck>> sender,
CancellationToken token)
{
if (request == null) throw new ArgumentNullException("Null request");
string version = HcsRequestHelper.GetRequestVersionString(request);
_config.Log($"Отправляем запрос: {RemoteAddress.Uri}/{request.GetType().Name} в версии {version}...");
var stopWatch = System.Diagnostics.Stopwatch.StartNew();
IHcsAck ack;
using (var client = NewPortClient())
{
ack = await sender(client);
}
stopWatch.Stop();
_config.Log($"Запрос принят в обработку за {stopWatch.ElapsedMilliseconds}мс., подтверждение {ack.MessageGUID}");
var stateResult = await WaitForResultAsync(ack, true, token);
stateResult.Items.OfType<OrgRegistryCommon.ErrorMessageType>().ToList().ForEach(x =>
{
throw HcsRemoteException.CreateNew(x.ErrorCode, x.Description);
});
return stateResult;
}
/// <summary>
/// Выполняет однократную проверку наличия результата.
/// Возвращает null если результата еще нет.
/// </summary>
protected override async Task<IHcsGetStateResult> TryGetResultAsync(IHcsAck sourceAck, CancellationToken token)
{
using (var client = NewPortClient())
{
var requestHeader = HcsRequestHelper.CreateHeader<OrgRegistryCommon.ISRequestHeader>(_config);
var requestBody = new OrgRegistryCommon.getStateRequest { MessageGUID = sourceAck.MessageGUID };
var response = await client.getStateAsync(requestHeader, requestBody);
var resultBody = response.getStateResult;
if (resultBody.RequestState == HcsAsyncRequestStateTypes.Ready)
{
return resultBody;
}
return null;
}
}
}
}

View File

@ -0,0 +1,34 @@
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace Hcs.ClientApi.RemoteCaller
{
public class GostSigningEndpointBehavior : IEndpointBehavior
{
private HcsClientConfig clientConfig;
public GostSigningEndpointBehavior(HcsClientConfig clientConfig)
{
this.clientConfig = clientConfig;
}
public void Validate(ServiceEndpoint endpoint)
{
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(
new GostSigningMessageInspector(clientConfig));
}
}
}

View File

@ -0,0 +1,120 @@
using Hcs.GostXades;
using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Xml;
namespace Hcs.ClientApi.RemoteCaller
{
/// <summary>
/// Фильтр сообщений добавляет в XML сообщение электронную подпись XADES/GOST.
/// </summary>
internal class GostSigningMessageInspector : IClientMessageInspector
{
private HcsClientConfig clientConfig;
public GostSigningMessageInspector(HcsClientConfig clientConfig)
{
this.clientConfig = clientConfig;
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
try
{
string filterHeader = " Фильтр отправки:";
PurgeDebuggerHeaders(ref request);
var messageBody = GetMessageBodyString(ref request, Encoding.UTF8);
if (!messageBody.Contains(HcsConstants.SignedXmlElementId))
{
clientConfig.MaybeCaptureMessage(true, messageBody);
}
else
{
string certInfo = HcsX509Tools.ДатьСтрокуФИОСертификатаСДатойОкончания(clientConfig.Certificate);
clientConfig.Log($"{filterHeader} подписываю сообщение ключем [{certInfo}]...");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var service = new GostXadesBesService(clientConfig.CryptoProviderType);
var signedXml = service.Sign(messageBody,
HcsConstants.SignedXmlElementId,
clientConfig.CertificateThumbprint,
clientConfig.CertificatePassword);
stopwatch.Stop();
clientConfig.Log($"{filterHeader} сообщение подписано за {stopwatch.ElapsedMilliseconds}мс.");
clientConfig.MaybeCaptureMessage(true, signedXml);
request = Message.CreateMessage(
XmlReaderFromString(signedXml), int.MaxValue, request.Version);
}
}
catch (Exception ex)
{
string error = $"В {GetType().Name} произошло исключение";
throw new Exception(error, ex);
}
return null;
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
clientConfig.MaybeCaptureMessage(false, reply.ToString());
}
private void PurgeDebuggerHeaders(ref Message request)
{
int limit = request.Headers.Count;
for (int i = 0; i < limit; ++i)
{
if (request.Headers[i].Name.Equals("VsDebuggerCausalityData"))
{
request.Headers.RemoveAt(i);
break;
}
}
}
string GetMessageBodyString(ref Message request, Encoding encoding)
{
MessageBuffer mb = request.CreateBufferedCopy(int.MaxValue);
request = mb.CreateMessage();
Stream s = new MemoryStream();
XmlWriter xw = XmlWriter.Create(s);
mb.CreateMessage().WriteMessage(xw);
xw.Flush();
s.Position = 0;
byte[] bXML = new byte[s.Length];
s.Read(bXML, 0, (int)s.Length);
if (bXML[0] != (byte)'<')
{
return encoding.GetString(bXML, 3, bXML.Length - 3);
}
else
{
return encoding.GetString(bXML, 0, bXML.Length);
}
}
XmlReader XmlReaderFromString(String xml)
{
var stream = new MemoryStream();
var writer = new System.IO.StreamWriter(stream);
writer.Write(xml);
writer.Flush();
stream.Position = 0;
return XmlReader.Create(stream);
}
}
}

View File

@ -0,0 +1,66 @@
using System;
namespace Hcs.ClientApi.RemoteCaller
{
/// <summary>
/// Состояние многостраничной выдачи для методов HCS выдыющих длинные списки.
/// Списки выдаются порциями по 100 позиций и в каждой порции указано состояние
/// многостраничной выдачи одним значением - это либо bool со значением true что
/// означает что эта порция последняя IsLastPage, либо это строка содержащая
/// guid объекта начала следующей порции - и этот guid надо указать в запросе
/// чтобы получить следующую порцию.
/// </summary>
public class HcsPagedResultState
{
/// <summary>
/// Состояние указыввает что это последняя страница
/// </summary>
public bool IsLastPage { get; private set; }
/// <summary>
/// Состояние указывает что это не последняя страница и
/// следующая страница начинается с NextGuid
/// </summary>
public Guid NextGuid { get; private set; }
private const string me = nameof(HcsPagedResultState);
public static readonly HcsPagedResultState IsLastPageResultState = new HcsPagedResultState(true);
/// <summary>
/// Новый маркер состояния многостраничной выдачи метода HCS
/// </summary>
public HcsPagedResultState(object item)
{
if (item == null) throw new HcsException($"{me}.Item is null");
if (item is bool)
{
if ((bool)item == false) throw new HcsException($"{me}.IsLastPage is false");
IsLastPage = true;
}
else if (item is string)
{
try
{
IsLastPage = false;
NextGuid = HcsUtil.ParseGuid((string)item);
}
catch (Exception e)
{
throw new HcsException($"Failed to parse {me}.NextGuid value", e);
}
}
else
{
throw new HcsException($"{me}.Item is of unrecognized type " + item.GetType().FullName);
}
}
public override string ToString()
{
return $"{me}({nameof(IsLastPage)}={IsLastPage}" +
(IsLastPage ? "" : $",{nameof(NextGuid)}={NextGuid}") + ")";
}
}
}

View File

@ -0,0 +1,373 @@
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.RemoteCaller
{
/// <summary>
/// Базовый класс для методов HCS вызываемых удаленно
/// </summary>
public abstract class HcsRemoteCallMethod
{
public HcsClientConfig _config;
protected CustomBinding _binding;
/// <summary>
/// Для методов возвращающих мало данных можно попробовать сократить
/// начальный период ожидания подготовки ответа
/// </summary>
public bool EnableMinimalResponseWaitDelay { get; internal set; }
/// <summary>
/// Для противодействия зависанию ожидания вводится предел ожидания в минутах
/// для методов которые можно перезапустить заново с теми же параметрами.
/// С периодом в 120 минут 09.2024 не успевали за ночь получить все данные.
/// </summary>
public int RestartTimeoutMinutes = 20;
/// <summary>
/// Можно ли этот метод перезапускать в случае зависания ожидания или в случае сбоя на сервере?
/// </summary>
public bool CanBeRestarted { get; protected set; }
public HcsClientConfig ClientConfig => _config;
public HcsRemoteCallMethod(HcsClientConfig config)
{
this._config = config;
ConfigureBinding();
}
private void ConfigureBinding()
{
_binding = new CustomBinding();
// Эксперимент 19.07.2022 возникает ошибка WCF (TimeoutException 60 сек)
_binding.ReceiveTimeout = TimeSpan.FromSeconds(180);
_binding.OpenTimeout = TimeSpan.FromSeconds(180);
_binding.SendTimeout = TimeSpan.FromSeconds(180);
_binding.CloseTimeout = TimeSpan.FromSeconds(180);
_binding.Elements.Add(new TextMessageEncodingBindingElement
{
MessageVersion = MessageVersion.Soap11,
WriteEncoding = Encoding.UTF8
});
if (_config.UseTunnel)
{
if (System.Diagnostics.Process.GetProcessesByName("stunnel").Any() ? false : true)
{
throw new Exception("stunnel не запущен");
}
_binding.Elements.Add(new HttpTransportBindingElement
{
AuthenticationScheme = (_config.IsPPAK ? System.Net.AuthenticationSchemes.Digest : System.Net.AuthenticationSchemes.Basic),
MaxReceivedMessageSize = int.MaxValue,
UseDefaultWebProxy = false
});
}
else
{
_binding.Elements.Add(new HttpsTransportBindingElement
{
AuthenticationScheme = (_config.IsPPAK ? System.Net.AuthenticationSchemes.Digest : System.Net.AuthenticationSchemes.Basic),
MaxReceivedMessageSize = int.MaxValue,
UseDefaultWebProxy = false,
RequireClientCertificate = true
});
}
}
protected EndpointAddress GetEndpointAddress(string endpointName)
{
return new EndpointAddress(_config.ComposeEndpointUri(endpointName));
}
protected void ConfigureEndpointCredentials(
ServiceEndpoint serviceEndpoint, ClientCredentials clientCredentials)
{
serviceEndpoint.EndpointBehaviors.Add(new GostSigningEndpointBehavior(_config));
if (!_config.IsPPAK)
{
clientCredentials.UserName.UserName = HcsConstants.UserAuth.Name;
clientCredentials.UserName.Password = HcsConstants.UserAuth.Passwd;
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate (
object sender, X509Certificate serverCertificate, X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
return true;
};
}
else
{
bool letSystemValidateServerCertificate = false;
if (!letSystemValidateServerCertificate)
{
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate (
object sender, X509Certificate serverCertificate, X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
// 06.06.2024 возникла ошибка "Это может быть связано с тем, что сертификат сервера
// не настроен должным образом с помощью HTTP.SYS в случае HTTPS."
// ГИС ЖКХ заменил сертификат сервера HTTPS и System.Net не смогла проверить новый.
// В похожем случае необходимо включить "return true" чтобы любой сертификат
// без проверки принимался (или найти файл lk_api_dom_gosuslugi_ru.cer нового сертификата
// сервера ГИС ЖКХ API в разделе "Регламенты и инструкции" портала dom.gosuslugi.ru
// и установить этот сертификат текущему пользователю).
// Файл сертификата сервера API в разделе "Регламенты и инструкции" называется, например, так:
// "Сертификат открытого ключа для организации защищенного TLS соединения с сервисами
// легковесной интеграции (c 10.06.2024)".
return true;
};
}
}
if (!_config.UseTunnel)
{
clientCredentials.ClientCertificate.SetCertificate(
StoreLocation.CurrentUser,
StoreName.My,
X509FindType.FindByThumbprint,
_config.CertificateThumbprint);
}
}
/// <summary>
/// Выполнение одной попытки пооучить результат операции.
/// Реализуется в производных классах.
/// </summary>
protected abstract Task<IHcsGetStateResult> TryGetResultAsync(IHcsAck sourceAck, CancellationToken token);
/// <summary>
/// Основной алгоритм ожидания ответа на асинхронный запрос.
/// Из документации ГИС ЖКХ:
/// Также рекомендуем придерживаться следующего алгоритма отправки запросов на получение статуса обработки пакета в случае использования асинхронных сервисов ГИС ЖКХ (в рамках одного MessageGUID):
/// - первый запрос getState направлять не ранее чем через 10 секунд, после получения квитанции о приеме пакета с бизнес-данными от сервиса ГИС КЖХ;
/// - в случае, если на первый запрос getSate получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 60 секунд после отправки предыдущего запроса;
/// - в случае, если на второй запрос getSate получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 300 секунд после отправки предыдущего запроса;
/// - в случае, если на третий запрос getSate получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 900 секунд после отправки предыдущего запроса;
/// - в случае, если на четвертый(и все последующие запросы) getState получен результат с RequestState равным "1" или "2", то следующий запрос getState необходимо направлять не ранее чем через 1800 секунд после отправки предыдущего запроса.
/// </summary>
protected async Task<IHcsGetStateResult> WaitForResultAsync(
IHcsAck ack, bool withInitialDelay, CancellationToken token)
{
var startTime = DateTime.Now;
IHcsGetStateResult result;
for (int attempts = 1; ; attempts++)
{
token.ThrowIfCancellationRequested();
int delaySec = EnableMinimalResponseWaitDelay ? 2 : 5;
if (attempts >= 2) delaySec = 5;
if (attempts >= 3) delaySec = 10;
if (attempts >= 5) delaySec = 20;
if (attempts >= 7) delaySec = 40;
if (attempts >= 9) delaySec = 80;
if (attempts >= 12) delaySec = 300;
if (attempts > 1 || withInitialDelay)
{
var minutesElapsed = (int)(DateTime.Now - startTime).TotalMinutes;
if (CanBeRestarted && minutesElapsed > RestartTimeoutMinutes)
throw new HcsRestartTimeoutException($"Превышено ожидание в {RestartTimeoutMinutes} минут");
_config.Log($"Ожидаю {delaySec} сек. до попытки #{attempts}" +
$" получить ответ (ожидание {minutesElapsed} минут(ы))...");
await Task.Delay(delaySec * 1000, token);
}
_config.Log($"Запрашиваю ответ, попытка #{attempts} {ThreadIdText}...");
result = await TryGetResultAsync(ack, token);
if (result != null) break;
}
_config.Log($"Ответ получен, число частей: {result.Items.Count()}");
return result;
}
/// <summary>
/// Исполнение повторяемой операции некоторое дпустимое число ошибок
/// </summary>
public async Task<T> RunRepeatableTaskAsync<T>(
Func<Task<T>> taskFunc, Func<Exception, bool> canIgnoreFunc, int maxAttempts)
{
for (int attempts = 1; ; attempts++)
{
try
{
return await taskFunc();
}
catch (Exception e)
{
if (canIgnoreFunc(e))
{
if (attempts < maxAttempts)
{
Log($"Игнорирую {attempts} из {maxAttempts} допустимых ошибок");
continue;
}
throw new HcsException(
$"Более {maxAttempts} продолжений после допустимых ошибок", e);
}
throw new HcsException("Вложенная ошибка", e);
}
}
}
/// <summary>
/// Для запросов к серверу которые можно направлять несколько раз, разрешаем
/// серверу аномально отказаться. Предполагается, что здесь мы игнорируем
/// только жесткие отказы серверной инфраструктуры, которые указывают
/// что запрос даже не был принят в обработку. Также все запросы на
/// чтение можно повторять в случае их серверных системных ошибок.
/// </summary>
protected async Task<T> RunRepeatableTaskInsistentlyAsync<T>(
Func<Task<T>> func, CancellationToken token)
{
int afterErrorDelaySec = 120;
for (int attempt = 1; ; attempt++)
{
try
{
return await func();
}
catch (Exception e)
{
string marker;
if (CanIgnoreSuchException(e, out marker))
{
_config.Log($"Игнорирую ошибку #{attempt} типа [{marker}].");
_config.Log($"Ожидаю {afterErrorDelaySec} сек. до повторения после ошибки...");
await Task.Delay(afterErrorDelaySec * 1000, token);
continue;
}
if (e is HcsRestartTimeoutException)
throw new HcsRestartTimeoutException("Наступило событие рестарта", e);
// Ошибки удаленной системы, которые нельзя игнорировать, дублируем для точности перехвата
if (e is HcsRemoteException) throw HcsRemoteException.CreateNew(e as HcsRemoteException);
throw new HcsException("Ошибка, которую нельзя игнорировать", e);
}
}
}
// "[EXP001000] Произошла ошибка при передаче данных. Попробуйте осуществить передачу данных повторно",
// Видимо, эту ошибку нельзя включать здесь. Предположительно это маркер DDOS защиты и если отправлять
// точно такой же пакет повторно, то ошибка входит в бесконечный цикл - необходимо заново
// собирать пакет с новыми кодами и временем и новой подписью. Такую ошибку надо обнаруживать
// на более высоком уровне и заново отправлять запрос новым пакетом. (21.09.2022)
private static string[] ignorableSystemErrorMarkers = {
"Истекло время ожидания шлюза",
"Базовое соединение закрыто: Соединение, которое должно было работать, было разорвано сервером",
"Попробуйте осуществить передачу данных повторно", // Включено 18.10.2024, HouseManagement API сильно сбоит
"(502) Недопустимый шлюз",
"(503) Сервер не доступен"
};
private bool CanIgnoreSuchException(Exception e, out string resultMarker)
{
foreach (var marker in ignorableSystemErrorMarkers)
{
var found = HcsUtil.EnumerateInnerExceptions(e).Find(
x => x.Message != null && x.Message.Contains(marker));
if (found != null)
{
resultMarker = marker;
return true;
}
}
resultMarker = null;
return false;
}
/// <summary>
/// Проверяет массив @items на содержание строго одного элемента типа @T и этот элемент
/// </summary>
protected T RequireSingleItem<T>(object[] items)
{
if (items == null)
throw new HcsException($"Array of type {typeof(T)} must not be null");
if (items.Length == 0)
throw new HcsException($"Array of type {typeof(T)} must not be empty");
if (items.Length > 1)
throw new HcsException($"Array of type {typeof(T)} must contain 1 element, not {items.Length} of type {items[0].GetType().FullName}");
return RequireType<T>(items[0]);
}
/// <summary>
/// Проверяет @obj на соответствие типу @T и возвращает преобразованный объект
/// </summary>
protected T RequireType<T>(object obj)
{
if (obj != null)
{
if (typeof(T) == obj.GetType()) return (T)obj;
}
throw new HcsException(
$"Require object of type {typeof(T)} but got" +
(obj == null ? "null" : obj.GetType().FullName));
}
internal static HcsException NewUnexpectedObjectException(object obj)
{
if (obj == null) return new HcsException("unexpected object is null");
return new HcsException($"Unexpected object [{obj}] of type {obj.GetType().FullName}");
}
public static string FormatGuid(Guid guid) => HcsUtil.FormatGuid(guid);
public static string FormatGuid(Guid? guid) => (guid != null) ? FormatGuid((Guid)guid) : null;
public static Guid ParseGuid(string guid) => HcsUtil.ParseGuid(guid);
public static Guid ParseGuid(object obj)
{
if (obj == null) throw new HcsException("Can't parse null as Guid");
if (obj is Guid) return (Guid)obj;
return ParseGuid(obj.ToString());
}
public static Guid[] ParseGuidArray(string[] array)
{
if (array == null) return null;
return array.ToList().Select(x => ParseGuid(x)).ToArray();
}
public bool IsArrayEmpty(Array a) => (a == null || a.Length == 0);
public string MakeEmptyNull(string s)
{
return string.IsNullOrEmpty(s) ? null : s;
}
/// <summary>
/// Выполняет @action на объекте @x если объект не пустой и приводится к типу @T
/// </summary>
public void CallOnType<T>(object x, Action<T> action) where T : class
{
var t = x as T;
if (t != null) action(t);
}
/// <summary>
/// Возвращает индентификатор текущего исполняемого потока
/// </summary>
public int ThreadId => System.Environment.CurrentManagedThreadId;
public string ThreadIdText => $"(thread #{ThreadId})";
public void Log(string message) => ClientConfig.Log(message);
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.Linq;
namespace Hcs.ClientApi.RemoteCaller
{
public static class HcsRequestHelper
{
/// <summary>
/// Подготовка заголовка сообщения отправляемого в ГИС ЖКХ с обязательными атрибутами.
/// Заголовки могут быть разного типа для разных типов сообщений но имена полей одинаковые.
/// </summary>
public static THeaderType CreateHeader<THeaderType>(HcsClientConfig config) where THeaderType : class
{
try
{
var instance = Activator.CreateInstance(typeof(THeaderType));
foreach (var prop in instance.GetType().GetProperties())
{
switch (prop.Name)
{
case "Item":
prop.SetValue(instance, config.OrgPPAGUID);
break;
case "ItemElementName":
prop.SetValue(instance, Enum.Parse(prop.PropertyType, "orgPPAGUID"));
break;
case "MessageGUID":
prop.SetValue(instance, Guid.NewGuid().ToString());
break;
case "Date":
prop.SetValue(instance, DateTime.Now);
break;
case "IsOperatorSignatureSpecified":
if (config.Role == HcsOrganizationRoles.RC || config.Role == HcsOrganizationRoles.RSO)
prop.SetValue(instance, true);
break;
case "IsOperatorSignature":
if (config.Role == HcsOrganizationRoles.RC || config.Role == HcsOrganizationRoles.RSO)
prop.SetValue(instance, true);
break;
}
}
return instance as THeaderType;
}
catch (ArgumentNullException ex)
{
throw new ApplicationException($"При сборке заголовка запроса для ГИС произошла ошибка: {ex.Message}");
}
catch (SystemException exc)
{
throw new ApplicationException($"При сборке заголовка запроса для ГИС произошла не предвиденная ошибка {exc.GetBaseException().Message}");
}
}
/// <summary>
/// Для объекта запроса возвращает значение строки свойства version
/// </summary>
public static string GetRequestVersionString(object requestObject)
{
if (requestObject == null) return null;
object versionHost = requestObject;
if (versionHost != null)
{
var versionProperty = versionHost.GetType().GetProperties().FirstOrDefault(x => x.Name == "version");
if (versionProperty != null) return versionProperty.GetValue(versionHost) as string;
}
foreach (var field in requestObject.GetType().GetFields())
{
versionHost = field.GetValue(requestObject);
if (versionHost != null)
{
var versionProperty = versionHost.GetType().GetProperties().FirstOrDefault(x => x.Name == "version");
if (versionProperty != null) return versionProperty.GetValue(versionHost) as string;
}
}
return null;
}
}
}

View File

@ -0,0 +1,18 @@
namespace Hcs.ClientApi.RemoteCaller
{
/// <summary>
/// Конфигурация ServicePointManager для работы с TLS. Скорее всего класс не нужен.
/// </summary>
public static class HcsServicePointConfig
{
public static void InitConfig()
{
// TODO: Проверить комментарий
// Отключено 15.12.2023, работает и так
//ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
//ServicePointManager.CheckCertificateRevocationList = false;
//ServicePointManager.ServerCertificateValidationCallback = ((sender, certificate, chain, sslPolicyErrors) => true);
//ServicePointManager.Expect100Continue = false;
}
}
}

View File

@ -0,0 +1,8 @@
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsAck
{
string MessageGUID { get; set; }
string RequesterMessageGUID { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsFault
{
string ErrorCode { get; set; }
string ErrorMessage { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsGetStateResult
{
object[] Items { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Hcs.ClientApi.RemoteCaller
{
public interface IHcsHeaderType
{
string MessageGUID { get; set; }
DateTime Date { get; set; }
}
}