Browse Source

基本功能

td_orm
H Vs 1 year ago
parent
commit
69264ae161
8 changed files with 396 additions and 109 deletions
  1. +184
    -0
      HealthMonitor.Common/SafeType.cs
  2. +29
    -1
      HealthMonitor.Model/Cache/GpsDevicePerson.cs
  3. +1
    -1
      HealthMonitor.Service/Biz/db/TDengineService.cs
  4. +68
    -0
      HealthMonitor.Service/Cache/BloodPressReferenceValueCacheManager.cs
  5. +37
    -42
      HealthMonitor.Service/Cache/PersonCacheManager.cs
  6. +1
    -0
      HealthMonitor.Service/HealthMonitor.Service.csproj
  7. +74
    -64
      HealthMonitor.WebApi/Controllers/HealthMonitor/HmBloodPressController.cs
  8. +2
    -1
      HealthMonitor.WebApi/Program.cs

+ 184
- 0
HealthMonitor.Common/SafeType.cs View File

@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace HealthMonitor.Common
{
public class SafeType
{

/// <summary>
/// Safes the int.
/// </summary>
/// <param name="o">The o.</param>
/// <returns></returns>
public static int SafeInt(object o)
{
int returnValue;
try
{
returnValue = Convert.ToInt32(o);
}
catch
{
returnValue = 0;
}

return returnValue;
}

/// <summary>
/// Safes the int.
/// </summary>
/// <param name="o">The o.</param>
/// <returns></returns>
public static long SafeInt64(object o)
{
long returnValue;
try
{
returnValue = Convert.ToInt64(o);
}
catch
{
returnValue = 0;
}

return returnValue;
}
/// <summary>
/// Safes the string.
/// </summary>
/// <param name="o">The o.</param>
/// <returns></returns>
public static string SafeString(object o)
{
string? returnValue;
try
{
returnValue = Convert.ToString(o);
}
catch
{
returnValue = "";
}

return returnValue!;
}

/// <summary>
/// Safes the bool.
/// </summary>
/// <param name="o">The o.</param>
/// <returns></returns>
public static bool SafeBool(object o)
{
bool returnValue;
try
{
returnValue = Convert.ToBoolean(o);
}
catch
{
returnValue = false;
}

return returnValue;
}

public static double SafeDouble(object o)
{
double returnValue;
try
{
returnValue = Convert.ToDouble(o);
}
catch
{
returnValue = 0;
}

return returnValue;
}

/// <summary>
/// Safes the decimal.
/// </summary>
/// <param name="o">The o.</param>
/// <returns></returns>
public static decimal SafeDecimal(object o)
{
decimal returnValue;
try
{
returnValue = Convert.ToDecimal(o);
}
catch
{
returnValue = 0.0M;
}

return returnValue;
}

/// <summary>
/// Safes the time.
/// </summary>
/// <param name="o">The o.</param>
/// <returns></returns>
public static DateTime SafeTime(object o)
{
var returnValue = DateTime.Parse("1900-01-01");
try
{
returnValue = Convert.ToDateTime(o);
}
catch
{
returnValue = DateTime.Parse("1900-01-01");
}

return returnValue;
}

/// <summary>
/// OBD时间转换
/// </summary>
/// <param name="o"></param>
/// <returns></returns>
public static DateTime OBDSafeTime(object o)
{
var returnValue = DateTime.Parse("1900-01-01");
try
{
returnValue = Convert.ToDateTime(o);
}
catch
{
returnValue = DateTime.UtcNow;
}

return returnValue;
}


/// <summary>
/// 是否为数字
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool IsNumber(string str)
{
int n;
if (int.TryParse(str, out n))
{
return true;
}
else
{
return false;
}
}

}
}

+ 29
- 1
HealthMonitor.Model/Cache/GpsDevicePerson.cs View File

@@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -30,11 +31,38 @@ namespace HealthMonitor.Model.Cache
[JsonProperty("gender")] [JsonProperty("gender")]
public bool Gender { get; set; } = default!; public bool Gender { get; set; } = default!;



// 用于存储真实值的字段
private bool gender;

//public int Age
//{
// get
// {
// DateTime bornDate = DateTime.Parse(BornDate);
// int age = DateTime.Today.Year - bornDate.Year;
// if (bornDate > DateTime.Today.AddYears(-age)) age--;
// return age;
// }
// set
// {
// BornDate = DateTime.Today.AddYears(-value).ToShortDateString();
// }
//}


[JsonProperty("age")] [JsonProperty("age")]
public int Age { get; set; } = default!; public int Age { get; set; } = default!;


[JsonProperty("bornDate")] [JsonProperty("bornDate")]
public string BornDate { get; set; } = default!;
public DateTime? BornDate { get; set; } = default!;

[JsonProperty("height")]
public int Height { get; set; } = default!;

[JsonProperty("weight")]
public int Weight { get; set; } = default!;


} }
public class GpsDevicePerson public class GpsDevicePerson


+ 1
- 1
HealthMonitor.Service/Biz/db/TDengineService.cs View File

@@ -216,7 +216,7 @@ namespace HealthMonitor.Service.Biz.db
var res = JsonConvert.DeserializeObject<TDengineRestResBase>(result!); var res = JsonConvert.DeserializeObject<TDengineRestResBase>(result!);
List<dynamic> data = res?.Data!; List<dynamic> data = res?.Data!;


sql = $"SELECT AVG({field}) FROM {_configTDengineService.DB}.{tbName} WHERE {condition} AND {field} < {data[0][0]} and {field} > {data[0][1]}";
sql = $"SELECT AVG({field}) FROM {_configTDengineService.DB}.{tbName} WHERE {condition} AND {field} < { (data.Count.Equals(0)? 0: data[0][0]) } and {field} > {(data.Count.Equals(0) ? 0 : data[0][1])}";
result = await GernalRestSqlResText(sql); result = await GernalRestSqlResText(sql);
res = JsonConvert.DeserializeObject<TDengineRestResBase>(result!); res = JsonConvert.DeserializeObject<TDengineRestResBase>(result!);
data = res?.Data!; data = res?.Data!;


+ 68
- 0
HealthMonitor.Service/Cache/BloodPressReferenceValueCacheManager.cs View File

@@ -0,0 +1,68 @@
using HealthMonitor.Core.Dal;
using HealthMonitor.Util.Entities.HealthMonitor;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Security.Cryptography;
using System.Text;

namespace HealthMonitor.Service.Cache
{
public class BloodPressReferenceValueCacheManager
{
private readonly ILogger<BloodPressReferenceValueCacheManager> _logger;
protected readonly IHealthMonitorDataAccessor _dataAccessor;

private static object _syncLocker = new object();
private MD5 _md5;
private const string CACHE_KEY_BP_REF_VALUE = "HM_REF_Val_";

public BloodPressReferenceValueCacheManager(
IHealthMonitorDataAccessor dataAccessor,
ILogger<BloodPressReferenceValueCacheManager> logger
)
{
_md5 = MD5.Create();
_dataAccessor = dataAccessor;
_logger = logger;
}

public async Task<HmBloodPressReferenceValue> GetBloodPressReferenceValueAsync(int age, int gender,bool isHypertension)
{
var hashModel = new
{
Index = $"{age}-{gender}-{isHypertension}"
};
var hash = ComputeHash(hashModel);
var cacheKey = CACHE_KEY_BP_REF_VALUE + $"{hash}";

var bpRef = await RedisHelper.GetAsync<HmBloodPressReferenceValue>(cacheKey).ConfigureAwait(false);
if (bpRef == null)
{
bpRef = _dataAccessor
.GetFirstOrDefault<HmBloodPressReferenceValue>(
i =>
i.Age.Equals(age) &&
i.Gender.Equals(gender) &&
i.Hypertension.Equals(isHypertension)
);
;
await RedisHelper.SetAsync(cacheKey, JsonConvert.SerializeObject(bpRef));
}
return bpRef;
}


private string ComputeHash(object data)
{
lock (_syncLocker)
{
var value = JsonConvert.SerializeObject(data);
value = Convert.ToBase64String(_md5.ComputeHash(Encoding.UTF8.GetBytes(value)));
return value;
}
}



}
}

+ 37
- 42
HealthMonitor.Service/Cache/PersonCacheManager.cs View File

@@ -15,11 +15,10 @@ using TelpoDataService.Util.QueryObjects;


namespace HealthMonitor.Service.Cache namespace HealthMonitor.Service.Cache
{ {
public abstract class MyHelper2 : RedisHelper<MyHelper2> { }
public class PersonCacheManager public class PersonCacheManager
{ {
private const int DEFAULT_DURATION_SECONDS = 1200; //20分钟
private const string CACHE_KEY_PERSON = "Person_";

//注意要改用 redis DB7 数据库 ,Prefix=TELPO //注意要改用 redis DB7 数据库 ,Prefix=TELPO
private const string CACHE_HASH_KEY_GPSDEVICEPERSON = "#GPSDEVICE_PERSON_HASH"; private const string CACHE_HASH_KEY_GPSDEVICEPERSON = "#GPSDEVICE_PERSON_HASH";
//注意要改用 redis DB7 数据库 ,Prefix=TELPO //注意要改用 redis DB7 数据库 ,Prefix=TELPO
@@ -27,47 +26,46 @@ namespace HealthMonitor.Service.Cache


private readonly GpsCardAccessorClient<GpsPerson> _personApiClient; private readonly GpsCardAccessorClient<GpsPerson> _personApiClient;
private readonly ILogger<PersonCacheManager> _logger; private readonly ILogger<PersonCacheManager> _logger;
private readonly IOptions<RedisConfig> _optConfigRedis;


public PersonCacheManager(GpsCardAccessorClient<GpsPerson> personApiClient,
ILogger<PersonCacheManager> logger, IOptions<RedisConfig> optConfigRedis)

public PersonCacheManager(
GpsCardAccessorClient<GpsPerson> personApiClient,
ILogger<PersonCacheManager> logger)
{ {
_optConfigRedis = optConfigRedis;
_personApiClient = personApiClient; _personApiClient = personApiClient;
_logger = logger; _logger = logger;
} }
public async Task<GpsPerson?> GetPersonBySerialNoAsync(string messageId, string sn)
{
if (string.IsNullOrWhiteSpace(sn)) return null;
// 切换redis DB7 数据库和前缀 "TELPO"
_optConfigRedis.Value.DefaultDatabase = 7;
_optConfigRedis.Value.Prefix = "TELPO";
//public async Task<GpsPerson?> GetPersonBySerialNoAsync(string messageId, string sn)
//{
// if (string.IsNullOrWhiteSpace(sn)) return null;
// // 切换redis DB7 数据库和前缀 "TELPO"


// 增加容错,防止Redis宕机造成业务中断
try
{
using var csRedisDb7 = new CSRedisClient(_optConfigRedis.Value.ToString());
return await csRedisDb7.HGetAsync<GpsPerson>(CACHE_HASH_KEY_GPSDEVICEPERSON, sn);
}
catch (Exception ex)
{
_logger.LogWarning($"Redis发生异常,将直接读取MySQL数据库,构造新实例:{ex.Message}, {ex.StackTrace}");
var param = new GeneralParam
{
Filters = new List<QueryFilterCondition>
{
new QueryFilterCondition
{
Key=nameof(GpsPerson.Serialno),
Value=sn,
ValueType=QueryValueTypeEnum.String,
Operator=QueryOperatorEnum.Equal
}
}
}!;
return await _personApiClient.GetFirstAsync(param, new RequestHeader { RequestId = messageId }).ConfigureAwait(false);
}
}
// // 增加容错,防止Redis宕机造成业务中断
// try
// {
// return await csRedisDb7.HGetAsync<GpsPerson>(CACHE_HASH_KEY_GPSDEVICEPERSON, sn);
// }
// catch (Exception ex)
// {
// _logger.LogWarning($"Redis发生异常,将直接读取MySQL数据库,构造新实例:{ex.Message}, {ex.StackTrace}");
// var param = new GeneralParam
// {
// Filters = new List<QueryFilterCondition>
// {
// new QueryFilterCondition
// {
// Key=nameof(GpsPerson.Serialno),
// Value=sn,
// ValueType=QueryValueTypeEnum.String,
// Operator=QueryOperatorEnum.Equal
// }
// }
// }!;
// return await _personApiClient.GetFirstAsync(param, new RequestHeader { RequestId = messageId }).ConfigureAwait(false);
// }
//}


/// <summary> /// <summary>
/// 读取个人信息(血压使用)注意要改用 redis DB7 /// 读取个人信息(血压使用)注意要改用 redis DB7
@@ -78,10 +76,7 @@ namespace HealthMonitor.Service.Cache
public async Task<GpsDevicePerson?> GetDeviceGpsPersonCacheBySerialNoAsync(string messageId, string sn) public async Task<GpsDevicePerson?> GetDeviceGpsPersonCacheBySerialNoAsync(string messageId, string sn)
{ {
if (string.IsNullOrWhiteSpace(sn)) return null; if (string.IsNullOrWhiteSpace(sn)) return null;
// 切换redis DB7 数据库和前缀 "TELPO"
_optConfigRedis.Value.DefaultDatabase = 7;
_optConfigRedis.Value.Prefix = "TELPO";


// 增加容错,防止Redis宕机造成业务中断 // 增加容错,防止Redis宕机造成业务中断
try try


+ 1
- 0
HealthMonitor.Service/HealthMonitor.Service.csproj View File

@@ -16,6 +16,7 @@


<ItemGroup> <ItemGroup>
<ProjectReference Include="..\HealthMonitor.Common\HealthMonitor.Common.csproj" /> <ProjectReference Include="..\HealthMonitor.Common\HealthMonitor.Common.csproj" />
<ProjectReference Include="..\HealthMonitor.Core\HealthMonitor.Core.csproj" />
<ProjectReference Include="..\HealthMonitor.Model\HealthMonitor.Model.csproj" /> <ProjectReference Include="..\HealthMonitor.Model\HealthMonitor.Model.csproj" />
<ProjectReference Include="..\HealthMonitor.Util\HealthMonitor.Util.csproj" /> <ProjectReference Include="..\HealthMonitor.Util\HealthMonitor.Util.csproj" />
</ItemGroup> </ItemGroup>


+ 74
- 64
HealthMonitor.WebApi/Controllers/HealthMonitor/HmBloodPressController.cs View File

@@ -1,13 +1,16 @@
using HealthMonitor.Core.Dal;
using HealthMonitor.Common;
using HealthMonitor.Core.Dal;
using HealthMonitor.Model.Cache; using HealthMonitor.Model.Cache;
using HealthMonitor.Service.Biz.db; using HealthMonitor.Service.Biz.db;
using HealthMonitor.Service.Cache; using HealthMonitor.Service.Cache;
using HealthMonitor.Util.Entities.HealthMonitor; using HealthMonitor.Util.Entities.HealthMonitor;
using HealthMonitor.WebApi.Configs; using HealthMonitor.WebApi.Configs;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -21,16 +24,22 @@ namespace HealthMonitor.WebApi.Controllers.HealthMonitor
[ApiController] [ApiController]
public class HmBloodPressController:ControllerBase public class HmBloodPressController:ControllerBase
{ {

protected readonly ILogger _logger; protected readonly ILogger _logger;
private readonly TDengineService _serviceTDengine; private readonly TDengineService _serviceTDengine;
private readonly PersonCacheManager _personCacheMgr; private readonly PersonCacheManager _personCacheMgr;
private readonly BloodPressReferenceValueCacheManager _bpRefValCacheManager;
protected readonly IHealthMonitorDataAccessor _dataAccessor; protected readonly IHealthMonitorDataAccessor _dataAccessor;



public HmBloodPressController public HmBloodPressController
( (
ILogger<HmBloodPressController> logger, ILogger<HmBloodPressController> logger,
TDengineService serviceDengine, TDengineService serviceDengine,
PersonCacheManager personCacheMgr,
PersonCacheManager personCacheMgr,
BloodPressReferenceValueCacheManager bpRefValCacheManager,
IHealthMonitorDataAccessor dataAccessor IHealthMonitorDataAccessor dataAccessor
) )
{ {
@@ -38,32 +47,70 @@ namespace HealthMonitor.WebApi.Controllers.HealthMonitor
_serviceTDengine = serviceDengine; _serviceTDengine = serviceDengine;
_personCacheMgr = personCacheMgr; _personCacheMgr = personCacheMgr;
_dataAccessor = dataAccessor; _dataAccessor = dataAccessor;
_bpRefValCacheManager = bpRefValCacheManager;
} }


[HttpPost] [HttpPost]
public async Task<IActionResult> AddAsync([FromHeader] string requestId, HisGpsBloodPress bp1)
public async Task<IActionResult> AddAsync([FromHeader] string requestId,[FromBody] HisGpsBloodPress bp)
{ {
HisGpsBloodPress bp = new()
{
BloodPressId = "261850cb-ce91-4003-8c63-a1f8f50d6495",
MessageId = "1670682284342246914",
Serialno = "861281060083627",
SystolicValue = 114,
DiastolicValue = 79,
CreateTime = DateTime.Parse("2023-06-19 14:37:53"),
LastUpdate = DateTime.Parse("2023-06-19 14:26:52"),
Method = 1,
IsDisplay = 1

};
//
//HisGpsBloodPress bp = new()
//{
// BloodPressId = "261850cb-ce91-4003-8c63-a1f8f50d6495",
// MessageId = "1670682284342246914",
// Serialno = "861281060083627",
// SystolicValue = 114,
// DiastolicValue = 79,
// CreateTime = DateTime.Parse("2023-06-19 14:37:53"),
// LastUpdate = DateTime.Parse("2023-06-19 14:26:52"),
// Method = 1,
// IsDisplay = 1
//};

//
//861281060081969
//HisGpsBloodPress bp = new()
//{
// BloodPressId = "7df62202-8d49-4f91-90da-25a4036c26fb",
// MessageId = "1670992704491900929",
// Serialno = "861281060081969",
// SystolicValue = 110,
// DiastolicValue = 72,
// CreateTime = DateTime.Parse("2023-06-20 11:11:31"),
// LastUpdate = DateTime.Parse("2023-06-20 10:20:40"),
// Method = 1,
// IsDisplay = 1
//};

// 861281060086380


//var aggregate = await _serviceTDengine.GetAvgExceptMaxMinValue("diastolic_value", "hm_bloodpress", $"ts>='{DateTime.Now.AddDays(-7):yyyy-MM-ddTHH:mm:ss.fffZ}' and ts <='{DateTime.Now:yyyy-MM-ddTHH:mm:ss.fffZ}' and serialno='861281060083627' and systolic_value < {120} and diastolic_value >{80}"); //var aggregate = await _serviceTDengine.GetAvgExceptMaxMinValue("diastolic_value", "hm_bloodpress", $"ts>='{DateTime.Now.AddDays(-7):yyyy-MM-ddTHH:mm:ss.fffZ}' and ts <='{DateTime.Now:yyyy-MM-ddTHH:mm:ss.fffZ}' and serialno='861281060083627' and systolic_value < {120} and diastolic_value >{80}");


//+++++++++++++++++++++++++++++++ //+++++++++++++++++++++++++++++++




#region 获取个人信息

var person = await _personCacheMgr.GetDeviceGpsPersonCacheBySerialNoAsync(bp.MessageId, bp.Serialno).ConfigureAwait(false);
// 验证这个信息是否存在
if (person == null || person?.Person.BornDate == null) return Ok(false);
// 验证年龄是否在范围 (2 - 120)
var age = SafeType.SafeInt(DateTime.Today.Year - person?.Person.BornDate!.Value.Year!);
if (age < 1 || age > 120) return Ok(false);

var gender = person?.Person.Gender == true ? 1 : 2;
var isHypertension = SafeType.SafeBool(person?.Person.Ishypertension!);
var height = SafeType.SafeDouble(person?.Person.Height!);
var weight = SafeType.SafeDouble(person?.Person.Weight!);
#endregion

#region 插入当次BP数据 #region 插入当次BP数据
// 保存到TDengine // 保存到TDengine
/**
var sql = $"INSERT INTO health_monitor.hm_bloodpress VALUES(" +
var bpSql = $"INSERT INTO health_monitor.hm_bloodpress VALUES(" +
$"'{bp.LastUpdate:yyyy-MM-ddTHH:mm:ss.fffZ}'," + $"'{bp.LastUpdate:yyyy-MM-ddTHH:mm:ss.fffZ}'," +
$"'{bp.BloodPressId}'," + $"'{bp.BloodPressId}'," +
$"'{bp.MessageId}'," + $"'{bp.MessageId}'," +
@@ -75,52 +122,19 @@ namespace HealthMonitor.WebApi.Controllers.HealthMonitor
$"{bp.Method}," + $"{bp.Method}," +
$"{bp.IsDisplay == 1})"; $"{bp.IsDisplay == 1})";


// var sql = "select * from health_monitor.hm_bloodpress order by last_update desc";


var res=await _serviceTDengine.GernalRestSql(sql);
*/
#endregion

#region 获取个人信息
var person = await _personCacheMgr.GetDeviceGpsPersonCacheBySerialNoAsync(bp.MessageId, bp.Serialno).ConfigureAwait(false);
await _serviceTDengine.GernalRestSql(bpSql);


//var person = new GpsDevicePerson()
//{
// Time = DateTime.Now,
// Person = new Person()
// {
// Age = 43,
// DeviceId = "111111",
// Gender = true,
// Ishypertension = false,
// SerialNo = bp.Serialno
// }
//};
#endregion #endregion


#region 计算增量值 #region 计算增量值
// 获取个人资料 redis
var age = person?.Person.Age;
var gender = person?.Person.Gender == true ? 1 : 2;
var isHypertension = person?.Person.Ishypertension;
var height = 0f;
var weight = 0f;
// 获取标定值 mysql
var bpRef = _dataAccessor
.GetFirstOrDefault<HmBloodPressReferenceValue>(
i =>
i.Age.Equals(age) &&
i.Gender.Equals(gender) &&
i.Hypertension.Equals(isHypertension)
);
var systolicRefValue = bpRef.Systolic;//?
var diastolicRefValue = bpRef.Diastolic;//?
var bpRef = await _bpRefValCacheManager.GetBloodPressReferenceValueAsync(age, gender, isHypertension);

var systolicRefValue = bpRef?.Systolic;//?
var diastolicRefValue = bpRef?.Diastolic;//?


// 获取历史数据 // 获取历史数据
DateTime now = DateTime.Now; DateTime now = DateTime.Now;
DateTime startTime = now.AddDays(-2);
DateTime startTime = now.AddDays(-30);
DateTime endTime = now; DateTime endTime = now;


// 计算去除最大值和最小值和异常值的平均值 // 计算去除最大值和最小值和异常值的平均值
@@ -131,8 +145,8 @@ namespace HealthMonitor.WebApi.Controllers.HealthMonitor
var avgOffset = 0.25M; var avgOffset = 0.25M;


// 增量值 // 增量值
var systolicInc = (int)(systolicRefValue - systolicAvg * avgOffset);
var diastolicInc = (int)(diastolicRefValue - systolicAvg * avgOffset);
var systolicInc = systolicAvg.Equals(0M) ? 0:(int)(systolicRefValue - systolicAvg * avgOffset)!;
var diastolicInc = diastolicAvg.Equals(0M) ? 0 : (int)(diastolicRefValue - diastolicAvg * avgOffset)!;


#endregion #endregion


@@ -157,11 +171,7 @@ namespace HealthMonitor.WebApi.Controllers.HealthMonitor
var res = await _serviceTDengine.GernalRestSql(sql); var res = await _serviceTDengine.GernalRestSql(sql);
#endregion #endregion



return Ok(true);
return Ok(res);
} }



} }
} }

+ 2
- 1
HealthMonitor.WebApi/Program.cs View File

@@ -142,7 +142,8 @@ namespace HealthMonitor.WebApi


#region Cache #region Cache
builder.Services builder.Services
.AddSingleton<PersonCacheManager>();
.AddSingleton<PersonCacheManager>()
.AddSingleton<BloodPressReferenceValueCacheManager>();


#endregion #endregion




Loading…
Cancel
Save