Files
jinko.yikatong/Src/Dlhis.Basic/Organization/Services/Implements/GainfoUpdateService.cs
2026-05-13 15:05:02 +08:00

687 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Dlhis.Basic.Common.Message.Requests;
using Dlhis.Basic.Common.Message.Services;
using Dlhis.Basic.Organization.Comparers;
using Dlhis.Entity.Basic.Entity;
using Dlhis.Entity.Basic.Enums;
using Dlhis.Entity.Device.Enums;
using Dlhis.Entity.Sys.Entities;
using Dlhis.Entity.Sys.Enums;
using Dlhis.Jip.Api.Dto;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
using Kurisu.AspNetCore.Utils.Extensions;
namespace Dlhis.Basic.Organization.Services.Implements;
public class GainfoUpdateService : IGainfoUpdateService
{
private readonly IDbContext _dbContext;
private readonly ILogger<GainfoUpdateService> _logger;
private readonly IJinkoSapService _jinkoSapService;
private readonly IGainfoPermissionService _gainfoPermissionService;
private readonly IJinkoApiService _dingTalkApi;
private readonly IIdentityUserSyncService _identityUserSyncService;
public GainfoUpdateService(IDbContext dbContext,
ILogger<GainfoUpdateService> logger,
IJinkoSapService jinkoSapService,
IGainfoPermissionService gainfoPermissionService,
IJinkoApiService dingTalkApi,
IIdentityUserSyncService identityUserSyncService)
{
_dbContext = dbContext;
_logger = logger;
_jinkoSapService = jinkoSapService;
_gainfoPermissionService = gainfoPermissionService;
_dingTalkApi = dingTalkApi;
_identityUserSyncService = identityUserSyncService;
}
public async Task TodoByWerkAsync(string werk)
{
var werkInfo = await _dbContext.Queryable<WerkEntity>().Where(x => x.Werk == werk).SingleAsync();
werkInfo.LastSyncStartTime = DateTime.Now;
werkInfo.HandleMessage = "任务执行中";
await _dbContext.UpdateAsync(werkInfo);
try
{
var personResp = await _jinkoSapService.GetPersonsByWerkAsync(werk);
var persons = personResp.Persons;
werkInfo.HandleMessage = "SAP人员数量" + persons.Count;
werkInfo.DiffNumber = 0;
werkInfo.Total = persons.Count;
await _dbContext.UpdateAsync(werkInfo);
if (persons.Count > 0)
{
var modifiedPersons = await GetModifiedPersonAsync(persons.OrderByDescending(x => x.ZSTATUS).ThenBy(x => x.ZRZRQ).ToList());
werkInfo.HandleMessage = "SAP人员差异数量" + modifiedPersons.Count;
werkInfo.DiffNumber = modifiedPersons.Count;
await _dbContext.UpdateAsync(werkInfo);
if (modifiedPersons.Count > 0)
{
await SavePersonAsync(modifiedPersons);
}
}
werkInfo.HandleMessage = "同步结束";
werkInfo.LastSyncEndTime = DateTime.Now;
werkInfo.BackgroundJobKey = string.Empty;
await _dbContext.UpdateAsync(werkInfo);
}
catch (Exception ex)
{
_logger.LogError(ex, "{werk}同步处理异常:{message}", werk, ex.Message);
werkInfo.HandleMessage = ex.Message;
werkInfo.LastSyncEndTime = DateTime.Now;
werkInfo.BackgroundJobKey = string.Empty;
await _dbContext.UpdateAsync(werkInfo);
}
}
public async Task TodoByCodeAsync(string code)
{
_logger.LogInformation("SAP根据学工号同步开始:{code}", code);
try
{
var personResp = await _jinkoSapService.GetPersonsByCodeAsync(code);
var persons = personResp.Persons;
_logger.LogInformation("获取SAP人员数量为:{count}", persons.Count);
if (persons.Count > 0)
{
var modifiedPersons = await GetModifiedPersonAsync(persons.OrderByDescending(x => x.ZSTATUS).ThenBy(x => x.ZRZRQ).ToList());
_logger.LogInformation("SAP人员差异数量为:{count}", modifiedPersons.Count);
if (modifiedPersons.Count > 0)
{
await SavePersonAsync(modifiedPersons);
}
}
_logger.LogInformation("SAP根据学工号同步结束:{code}", code);
}
catch (Exception e)
{
_logger.LogError(e, "同步人员异常:{msg}", e.Message);
}
}
private async Task SavePersonAsync(List<SapPersonItem> persons)
{
var validPersons = persons.Where(x => !string.IsNullOrWhiteSpace(x.ZWORKCODE)).ToList();
if (validPersons.Count <= 0)
{
return;
}
var workCodes = validPersons.Select(x => x.ZWORKCODE).Distinct().ToList();
var chunks = workCodes.Chunk(4000).ToList();
var comparer = new SapPersonComparer();
foreach (var searchCodes in chunks)
{
_logger.LogWarning("SAP分批处理Chunk数量:{count}", searchCodes.Length);
var chunkPersons = validPersons.Where(x => searchCodes.Contains(x.ZWORKCODE)).ToList();
var currentPersons = GetCurrentPersons(chunkPersons);
var histories = await _dbContext.Queryable<GainfoSapEntity>()
.Where(x => searchCodes.Contains(x.ename))
.ToListAsync();
var historyPersons = histories.Select(ToSapPersonItem).ToList();
foreach (var person in chunkPersons)
{
try
{
if (historyPersons.Any(x => comparer.Equals(person, x)))
{
continue;
}
var item = ToGainfoSap(person);
await InsertGainfoSapHistoryAsync(item);
historyPersons.Add(ToSapPersonItem(item));
}
catch (Exception ex)
{
_logger.LogError(ex, "执行历史同步失败:{message}", ex.Message);
}
}
foreach (var person in currentPersons)
{
try
{
var item = ToGainfoSap(person);
var no = await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == item.ename).Select(x => x.No).SingleAsync();
if (no.IsEmpty())
{
await InsertCurrentPersonAsync(item);
}
else
{
await UpdatePersonAsync(item);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "执行当前人员同步失败:{message}", ex.Message);
}
}
}
}
private async Task InsertGainfoSapHistoryAsync(GainfoSapEntity item)
{
await _dbContext.InsertAsync(item);
}
private async Task InsertCurrentPersonAsync(GainfoSapEntity item)
{
var info = GainfoSapToGainfo(item);
//只有身份证号存在才进行新增
if (info.IdNo.IsPresent())
{
await _dbContext.UseTransactionAsync(async () =>
{
if (item.isowner == "T")
{
if (!await _dbContext.Queryable<GainfoEntity>().AnyAsync(x => x.Code == item.ename))
{
await _dbContext.InsertAsync(info);
}
await _identityUserSyncService.SyncAccountAsync(info.Code, info.Name, info.IdNo, info.Factory);
}
else
{
if (!await _dbContext.Queryable<GainfoLeaveEntity>().AnyAsync(x => x.Code == item.ename))
{
await _dbContext.Client.Insertable(info).AS("GainfoLeave").ExecuteCommandAsync();
}
}
});
}
else
{
_logger.LogWarning("SAP新增人员失败,身份证号不存在:{userCode},{userName}", info.Code, info.Name);
}
}
private static GainfoEntity GainfoSapToGainfo(GainfoSapEntity item)
{
var info = new GainfoEntity
{
No = Guid.NewGuid().ToString(),
InternalNo = item.PERNR,
Type = GainfoUserTypeEnumType.Normal,
Name = item.gname,
NameEn = item.gname2,
Code = item.ename,
CountryNo = item.country,
IdClass = item.idcls,
IdNo = item.idno.ToUpperInvariant(),
Native = item.native,
Gender = item.sex == "F" ? GenderEnumType.Female : GenderEnumType.Male,
Birthday = item.birthday,
Address = item.address,
DepartmentNo = item.appellation,
Nation = item.nation,
Phone = item.phone,
Degree = item.email,
Grade = item.ZZ_ZD,
Title = item.Z_ZJ,
PositionNo = item.STELL,
Remark = item.remark,
Werks = item.WERKS,
CostId = item.KOSTL,
EntryDate = item.createdate,
ResignationDate = item.ZLZRQ,
Workplace = item.ZZ_GZDJD,
Factory = item.ZZ_GZDCQ
};
return info;
}
private async Task UpdatePersonAsync(GainfoSapEntity item)
{
await _dbContext.Client.Ado.BeginTranAsync();
try
{
//获取当前在职人员
var gainfo = await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == item.ename).SingleAsync();
//默认人员存在
if (gainfo != null)
{
var workplace = item.ZZ_GZDJD;
var factory = item.ZZ_GZDCQ;
if (item.isowner == "T")
{
var change = string.Empty;
var hasFactoryChange = gainfo.Factory != factory || gainfo.Workplace != workplace;
var hasDepartmentChange = gainfo.DepartmentNo != item.appellation;
if (hasFactoryChange)
{
await _identityUserSyncService.SyncAccountAsync(gainfo.Code, gainfo.Name, gainfo.IdNo, factory);
change = $"{gainfo.Factory}->{factory}";
}
if (hasDepartmentChange)
{
change += $"{gainfo.DepartmentNo}->{item.appellation}";
}
if (change.IsPresent())
{
await _gainfoPermissionService.ResetPermissionsAsync(gainfo.No, DeviceTaskEnumType.Delete, null, null, $"异动生成({change})");
}
await _dbContext.Updateable<GainfoEntity>().SetColumns(g => new GainfoEntity
{
InternalNo = item.PERNR,
Name = item.gname,
NameEn = item.gname2,
IdNo = item.idno.ToUpperInvariant(),
Birthday = item.birthday,
PositionNo = item.STELL,
Grade = item.ZZ_ZD,
Title = item.Z_ZJ,
Phone = item.phone,
DepartmentNo = item.appellation,
Remark = item.remark,
Degree = item.email,
Werks = item.WERKS,
CostId = item.KOSTL,
EntryDate = item.createdate,
ResignationDate = item.ZLZRQ,
Workplace = workplace,
Factory = factory,
ModifiedTime = DateTime.Now
}).Where(x => x.Code == item.ename).ExecuteCommandAsync();
//有变更就重新下发
if (change.IsPresent())
{
await _gainfoPermissionService.ResetPermissionsAsync(gainfo.No, DeviceTaskEnumType.Add, null, null, $"异动生成({change})");
//只有厂区变更才需要处理工作网格
if (hasFactoryChange)
{
var workGridUser = await _dbContext.Queryable<WorkGridUserEntity>().Where(x => x.UserCode == gainfo.Code).SingleAsync();
if (workGridUser != null)
{
try
{
var resignationDate = DateOnly.FromDateTime(DateTime.Now);
var workGridSchedules = await _dbContext.Queryable<WorkGridScheduleEntity>()
.Where(x => x.UserCode == gainfo.Code)
.Where(x => x.Date >= resignationDate)
.ToListAsync();
if (workGridSchedules.Any())
{
var dates = workGridSchedules.Select(x => $"{x.Date.ToString("yyyy年-MM月-dd日")}的{x.PeriodType.GetDescription()}").Join(",");
var workGrid = await _dbContext.Queryable<WorkGridEntity>()
.Where(x => x.Code == workGridUser.WorkGridCode)
.Select(x => new
{
x.LeaderUserCode,
x.Name
}).SingleAsync();
var content = $"{workGrid.Name}的网格长,你好,检测到你网格中 {gainfo.Name}({gainfo.Code}),因发生离职或者异动,现已经删除该员工{dates}的排班信息,请前往系统处理!";
await _dingTalkApi.SendWorkMessageByWorkGridAsync(new DingTalkWorkMessageRequest
{
Content = content,
WorkCodes = new List<string> { workGrid.LeaderUserCode }
});
}
await _dbContext.DeleteAsync(workGridUser);
await _dbContext.DeleteAsync(workGridSchedules);
}
catch (Exception e)
{
_logger.LogError(e, "网格异动处理异常:{error}", e.Message);
}
}
}
}
}
else
{
await _gainfoPermissionService.ResetPermissionsAsync(gainfo.No, DeviceTaskEnumType.Delete, null, null, "人员离职生成");
//先写入离职表
gainfo.IdentityUserId = (int)gainfo.Id; //记录原始id
gainfo.Id = 0;
gainfo.ResignationDate = item.ZLZRQ;
if (!await _dbContext.Queryable<GainfoLeaveEntity>().AnyAsync(x => x.Code == item.ename))
{
await _dbContext.Client.Insertable(gainfo).AS("GainfoLeave").ExecuteCommandAsync();
}
await _dbContext.Deleteable<GainfoGroupRelationshipEntity>()
.Where(x => x.UserNo == gainfo.No)
.ExecuteCommandAsync();
await _dbContext.Deleteable<DevicePermissionEntity>()
.Where(x => x.AssignType == AssignTypeEnumType.User && x.AssignValue == gainfo.No)
.ExecuteCommandAsync();
var workGridUser = await _dbContext.Queryable<WorkGridUserEntity>()
.Where(x => x.UserCode == gainfo.Code)
.SingleAsync();
if (workGridUser != null)
{
try
{
var resignationDate = DateOnly.FromDateTime(gainfo.ResignationDate!.Value);
var workGridSchedules = await _dbContext.Queryable<WorkGridScheduleEntity>()
.Where(x => x.UserCode == gainfo.Code)
.Where(x => x.Date >= resignationDate)
.ToListAsync();
if (workGridSchedules.Any())
{
var dates = workGridSchedules.Select(x => $"{x.Date.ToString("yyyy年-MM月-dd日")}的{x.PeriodType.GetDescription()}").Join(",");
var workGrid = await _dbContext.Queryable<WorkGridEntity>()
.Where(x => x.Code == workGridUser.WorkGridCode)
.Select(x => new
{
x.LeaderUserCode,
x.Name
}).SingleAsync();
var content = $"{workGrid.Name}的网格长,你好,检测到你网格中 {gainfo.Name}({gainfo.Code}),因发生离职或者异动,现已经删除该员工{dates}的排班信息,请前往系统处理!";
await _dingTalkApi.SendWorkMessageByWorkGridAsync(new DingTalkWorkMessageRequest
{
Content = content,
WorkCodes = new List<string> { workGrid.LeaderUserCode }
});
}
await _dbContext.DeleteAsync(workGridUser);
await _dbContext.DeleteAsync(workGridSchedules);
}
catch (Exception e)
{
_logger.LogError(e, "网格异动处理异常:{error}", e.Message);
}
}
gainfo.Id = gainfo.IdentityUserId;
await _dbContext.DeleteReallyAsync(gainfo);
await _identityUserSyncService.LockAccountAsync(gainfo.Code);
}
}
await _dbContext.Client.Ado.CommitTranAsync();
}
catch (Exception ex)
{
await _dbContext.Client.Ado.RollbackTranAsync();
_logger.LogError(ex, "更新人员失败:{message}", ex.Message);
}
}
private async Task<List<SapPersonItem>> GetModifiedPersonAsync(List<SapPersonItem> sapList)
{
if (sapList.Count <= 0)
{
return new List<SapPersonItem>();
}
var comparer = new SapPersonComparer();
var workCodes = sapList
.Select(x => x.ZWORKCODE)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList();
if (workCodes.Count <= 0)
{
return sapList;
}
var last = await LoadDataAsync(workCodes);
if (last.Count <= 0)
{
return sapList;
}
var lastMap = last
.Where(x => !string.IsNullOrWhiteSpace(x.ZWORKCODE))
.GroupBy(x => x.ZWORKCODE)
.ToDictionary(x => x.Key, x => x.ToList());
var modifiedPersons = new List<SapPersonItem>();
foreach (var person in sapList)
{
if (string.IsNullOrWhiteSpace(person.ZWORKCODE))
{
modifiedPersons.Add(person);
continue;
}
if (!lastMap.TryGetValue(person.ZWORKCODE, out var historyPersons) || historyPersons.Count <= 0)
{
modifiedPersons.Add(person);
continue;
}
var exactHistory = historyPersons.FirstOrDefault(x => comparer.Equals(person, x));
if (exactHistory != null)
{
historyPersons.Remove(exactHistory);
continue;
}
modifiedPersons.Add(person);
}
if (modifiedPersons.Count > 0)
{
var first = modifiedPersons[0];
var historyCandidates = !string.IsNullOrWhiteSpace(first.ZWORKCODE)
&& lastMap.TryGetValue(first.ZWORKCODE, out var currentHistoryPersons)
? currentHistoryPersons
: new List<SapPersonItem>();
var hasHistory = historyCandidates.Count > 0;
var diffType = hasHistory ? "Update" : "Insert";
var diffMessage = hasHistory
? string.Join("; ", GetFirstDiffFields(first, historyCandidates[0]))
: "历史记录不存在";
_logger.LogWarning("SAP首条差异 DiffType:{diffType}, ZWORKCODE:{workCode}, HasHistory:{hasHistory}, Diffs:{diffs}",
diffType, first.ZWORKCODE, hasHistory, diffMessage);
}
return modifiedPersons;
}
private static List<SapPersonItem> GetCurrentPersons(List<SapPersonItem> sapList)
{
return sapList
.OrderByDescending(x => x.ZSTATUS)
.ThenBy(x => x.ZRZRQ)
.GroupBy(x => x.ZWORKCODE)
.Select(x => x.First())
.ToList();
}
private async Task<List<SapPersonItem>> LoadDataAsync(List<string> workCodes)
{
var gainfos = new List<GainfoSapEntity>();
var chunks = workCodes.Chunk(2000);
foreach (var chunk in chunks)
{
var items = await _dbContext.Queryable<GainfoSapEntity>()
.Where(x => chunk.Contains(x.ename))
.ToListAsync();
gainfos.AddRange(items);
}
return gainfos.Select(ToSapPersonItem).ToList();
}
private static List<GainfoSapEntity> ToGainfoSap(List<SapPersonItem> data)
{
return data.Select(ToGainfoSap).ToList();
}
private static GainfoSapEntity ToGainfoSap(SapPersonItem person)
{
var entryDate = person.ZRZRQ;
var resignDate = person.ZLZRQ;
var rmk = $"入职时间:{entryDate.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture)} ";
if (resignDate != null && resignDate.Value.ToString("yyyy-MM-dd") != "1900-01-01")
rmk += $"离职时间:{resignDate.Value.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture)} ";
if (!string.IsNullOrWhiteSpace(person.MGTXT))
rmk += $"变更原因:{person.MGTXT}";
var native = GetNative(person.ZZ_SFZH);
var gender = person.GESCH == "2" ? "F" : "M";
person.ZZ_SFZH = person.ZZ_SFZH?.TrimStart();
return new GainfoSapEntity
{
gname = person.NAME,
gname2 = person.NAME2,
ename = person.ZWORKCODE,
idno = person.ZZ_SFZH,
birthday = person.GBDAT,
email = person.ZXUELI,
appellation = person.ORGEH,
phone = person.ZMOBILE,
ZSTATUS = person.ZSTATUS,
createdate = person.ZRZRQ,
PERNR = person.PERNR,
WERKS = person.WERKS,
KOSTL = person.KOSTL_XJ,
ZZ_ZD = person.ZZ_ZD,
Z_ZJ = person.Z_ZJ,
STELL = person.PLANS,//person.STELL, 20251129变更
STELL_NM = person.STELL_NM ?? string.Empty,
ZLZRQ = person.ZLZRQ,
MASSN = person.MASSN ?? string.Empty,
MNTXT = person.MNTXT ?? string.Empty,
remark = rmk,
native = native,
sex = gender,
logdate = DateTime.Now,
isowner = int.Parse(person.ZSTATUS) < 5 ? "T" : "F",
country = person.ZZ_COUNNM,
address = "",
occupation = "99",
nation = "01",
idcls = "ID",
ZZ_COUNNM = person.ZZ_COUNNM,
ZZ_GZDJD = person.ZZ_GZDJD,
ZZ_GZDCQ = person.ZZ_GZDCQ
};
}
private static SapPersonItem ToSapPersonItem(GainfoSapEntity x)
{
return new SapPersonItem
{
NAME = x.gname,
NAME2 = x.gname2,
ZWORKCODE = x.ename,
ZZ_SFZH = x.idno,
GBDAT = x.birthday,
ZXUELI = x.email,
ORGEH = x.appellation,
ZMOBILE = x.phone,
ZSTATUS = x.ZSTATUS,
ZRZRQ = x.createdate,
PERNR = x.PERNR,
WERKS = x.WERKS,
KOSTL_XJ = x.KOSTL,
ZZ_ZD = x.ZZ_ZD,
Z_ZJ = x.Z_ZJ,
PLANS = x.STELL, //x.STELL, 20251129变更
STELL = x.STELL,
STELL_NM = x.STELL_NM,
ZLZRQ = x.ZLZRQ,
MASSN = x.MASSN,
MNTXT = x.MNTXT,
ZZ_COUNNM = x.ZZ_COUNNM,
ZZ_GZDJD = x.ZZ_GZDJD,
ZZ_GZDCQ = x.ZZ_GZDCQ
};
}
private static string GetNative(string idNo)
{
if (string.IsNullOrWhiteSpace(idNo) || idNo.Length < 6) return null;
idNo = idNo.TrimStart();
return idNo[..6];
}
private static List<string> GetFirstDiffFields(SapPersonItem current, SapPersonItem history)
{
var diffs = new List<string>();
AddDiff(diffs, "PERNR", current.PERNR, history.PERNR);
AddDiff(diffs, "WERKS", current.WERKS, history.WERKS);
AddDiff(diffs, "ORGEH", current.ORGEH, history.ORGEH);
AddDiff(diffs, "NAME", current.NAME, history.NAME);
AddDiff(diffs, "GBDAT", FormatDate(current.GBDAT), FormatDate(history.GBDAT));
AddDiff(diffs, "ZZ_SFZH", NormalizeIdNo(current.ZZ_SFZH), NormalizeIdNo(history.ZZ_SFZH));
AddDiff(diffs, "ZWORKCODE", current.ZWORKCODE, history.ZWORKCODE);
AddDiff(diffs, "ZMOBILE", Normalize(current.ZMOBILE), Normalize(history.ZMOBILE));
AddDiff(diffs, "ZSTATUS", Normalize(current.ZSTATUS), Normalize(history.ZSTATUS));
AddDiff(diffs, "KOSTL_XJ", Normalize(current.KOSTL_XJ), Normalize(history.KOSTL_XJ));
AddDiff(diffs, "NAME2", Normalize(current.NAME2), Normalize(history.NAME2));
AddDiff(diffs, "ZZ_ZD", Normalize(current.ZZ_ZD), Normalize(history.ZZ_ZD));
AddDiff(diffs, "Z_ZJ", Normalize(current.Z_ZJ), Normalize(history.Z_ZJ));
AddDiff(diffs, "ZRZRQ", FormatDate(current.ZRZRQ), FormatDate(history.ZRZRQ));
AddDiff(diffs, "ZLZRQ", FormatDate(current.ZLZRQ), FormatDate(history.ZLZRQ));
AddDiff(diffs, "PLANS", Normalize(current.PLANS), Normalize(history.PLANS));
AddDiff(diffs, "STELL_NM", Normalize(current.STELL_NM), Normalize(history.STELL_NM));
AddDiff(diffs, "MASSN", Normalize(current.MASSN), Normalize(history.MASSN));
AddDiff(diffs, "MNTXT", Normalize(current.MNTXT), Normalize(history.MNTXT));
AddDiff(diffs, "ZZ_GZDJD", Normalize(current.ZZ_GZDJD), Normalize(history.ZZ_GZDJD));
AddDiff(diffs, "ZZ_GZDCQ", Normalize(current.ZZ_GZDCQ), Normalize(history.ZZ_GZDCQ));
return diffs;
}
private static void AddDiff(List<string> diffs, string field, string current, string history)
{
if (!string.Equals(current ?? string.Empty, history ?? string.Empty, StringComparison.Ordinal))
{
diffs.Add($"{field}: [{history}] -> [{current}]");
}
}
private static string Normalize(string value) => value?.Trim() ?? string.Empty;
private static string NormalizeIdNo(string value) => value?.TrimStart() ?? string.Empty;
private static string FormatDate(DateTime? value) => value?.ToString("yyyy-MM-dd HH:mm:ss") ?? string.Empty;
private static string FormatDate(DateTime value) => value.ToString("yyyy-MM-dd HH:mm:ss");
}