From c1126ffe7468cfd960e52493f0a914a86d7af61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B5=87=E6=96=87=E9=BE=99?= Date: Sat, 12 Oct 2024 18:09:28 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E6=88=90WSL=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HangfireModuleInstall.cs | 29 ++++ .../Fee/Method/FeeRecordService.cs | 2 +- .../DS.WMS.Core/HangfireJob/Dtos/WSLModel.cs | 32 +++++ .../Interface/IWSLReportJobService.cs | 17 +++ .../HangfireJob/Method/JobMiddleware.cs | 21 ++- .../HangfireJob/Method/WSLReportJobService.cs | 136 +++++++++++++++--- .../Info/Entity/InfoClientStakeholder.cs | 13 ++ .../Info/Method/ClientStakeholderService.cs | 34 ++++- ds-wms-service/DS.WMS.FeeApi/Program.cs | 2 + ds-wms-service/DS.WMS.OpApi/Program.cs | 8 +- ds-wms-service/DS.WMS.OpApi/appsettings.json | 11 +- .../DS.WMS.OpApi/wwwroot/templates/WSL.xlsx | Bin 0 -> 10307 bytes 12 files changed, 269 insertions(+), 36 deletions(-) create mode 100644 ds-wms-service/DS.WMS.Core/HangfireJob/Dtos/WSLModel.cs create mode 100644 ds-wms-service/DS.WMS.Core/HangfireJob/Interface/IWSLReportJobService.cs create mode 100644 ds-wms-service/DS.WMS.OpApi/wwwroot/templates/WSL.xlsx diff --git a/ds-wms-service/DS.Module.HangfireModule/HangfireModuleInstall.cs b/ds-wms-service/DS.Module.HangfireModule/HangfireModuleInstall.cs index bd955aeb..c0d3ac0f 100644 --- a/ds-wms-service/DS.Module.HangfireModule/HangfireModuleInstall.cs +++ b/ds-wms-service/DS.Module.HangfireModule/HangfireModuleInstall.cs @@ -76,4 +76,33 @@ public static class HangfireModuleInstall options.Queues = ["fee"]; }); } + + public static void AddHangfireOPInstall(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + services.AddHangfire(configuration => configuration + .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) //设置Hangfire 存储的数据兼容性级别为 Version_170 + .UseSimpleAssemblyNameTypeSerializer()//使用简单的程序集名称类型序列化器。这是一种用于序列化和反序列化 Hangfire 任务数据的方法 + .UseRecommendedSerializerSettings()//使用推荐的序列化器设置。这将使用推荐的序列化器选项进行配置 + .UseStorage(new MySqlStorage( + AppSetting.app(new string[] { "HangfireSettings", "DbString" }), + new MySqlStorageOptions + { + TransactionIsolationLevel = IsolationLevel.ReadCommitted,// 事务隔离级别。默认是读取已提交。 + QueuePollInterval = TimeSpan.FromSeconds(15), //- 作业队列轮询间隔。默认值为15秒。 + JobExpirationCheckInterval = TimeSpan.FromHours(1), //- 作业到期检查间隔(管理过期记录)。默认值为1小时。 + CountersAggregateInterval = TimeSpan.FromMinutes(5), //- 聚合计数器的间隔。默认为5分钟。 + PrepareSchemaIfNecessary = true, //- 如果设置为true,则创建数据库表。默认是true。 + DashboardJobListLimit = 50000,//- 仪表板作业列表限制。默认值为50000。 + TransactionTimeout = TimeSpan.FromMinutes(1), //- 交易超时。默认为1分钟。 + TablesPrefix = "Hangfire" + })).UseHangfireHttpJob()); + services.AddHangfireServer(options => + { + //options.WorkerCount = AppSetting.app(new string[] { "HangfireSettings", "WorkerCount" }).ToInt(); + //options.ServerName = AppSetting.app(new string[] { "HangfireSettings", "ServerName" }); + options.Queues = ["op"]; + }); + } } \ No newline at end of file diff --git a/ds-wms-service/DS.WMS.Core/Fee/Method/FeeRecordService.cs b/ds-wms-service/DS.WMS.Core/Fee/Method/FeeRecordService.cs index 8b0f7225..65e4053d 100644 --- a/ds-wms-service/DS.WMS.Core/Fee/Method/FeeRecordService.cs +++ b/ds-wms-service/DS.WMS.Core/Fee/Method/FeeRecordService.cs @@ -254,7 +254,7 @@ namespace DS.WMS.Core.Fee.Method { if (fe.FeeStatus != FeeStatus.Entering && fe.FeeStatus != FeeStatus.RejectSubmission) { - sb.AppendFormat(MultiLanguageConst.FeeRecordStatus, fe.FeeName); + sb.AppendFormat(MultiLanguageConst.GetDescription(MultiLanguageConst.FeeRecordStatus), fe.FeeName); continue; } } diff --git a/ds-wms-service/DS.WMS.Core/HangfireJob/Dtos/WSLModel.cs b/ds-wms-service/DS.WMS.Core/HangfireJob/Dtos/WSLModel.cs new file mode 100644 index 00000000..8547a1c2 --- /dev/null +++ b/ds-wms-service/DS.WMS.Core/HangfireJob/Dtos/WSLModel.cs @@ -0,0 +1,32 @@ +namespace DS.WMS.Core.HangfireJob.Dtos +{ + public class WSLModel + { + public string Date { get; set; } + + public string Month { get; set; } + + public IEnumerable? List { get; set; } + } + + public class WSLItem + { + public long CustomerId { get; set; } + + public string? CustomerName { get; set; } + + public int? YesterdayTeu { get; set; } + + public int? TodayTeu { get; set; } + + public int? TodayTeuCTNPickup { get; set; } + + public int? IncreasedTeu => TodayTeu - YesterdayTeu; + + public string? Date { get; set; } + + public int? NextMonthTEU { get; set; } + + public int? LastMonthTEU { get; set; } + } +} diff --git a/ds-wms-service/DS.WMS.Core/HangfireJob/Interface/IWSLReportJobService.cs b/ds-wms-service/DS.WMS.Core/HangfireJob/Interface/IWSLReportJobService.cs new file mode 100644 index 00000000..ce100133 --- /dev/null +++ b/ds-wms-service/DS.WMS.Core/HangfireJob/Interface/IWSLReportJobService.cs @@ -0,0 +1,17 @@ +using Hangfire; + +namespace DS.WMS.Core.HangfireJob.Interface +{ + /// + /// WSL报表发送 + /// + public interface IWSLReportJobService + { + /// + /// 生成报表 + /// + /// + [Queue("op")] + Task GeneratReportAsync(); + } +} diff --git a/ds-wms-service/DS.WMS.Core/HangfireJob/Method/JobMiddleware.cs b/ds-wms-service/DS.WMS.Core/HangfireJob/Method/JobMiddleware.cs index f1ad6940..4b713cd0 100644 --- a/ds-wms-service/DS.WMS.Core/HangfireJob/Method/JobMiddleware.cs +++ b/ds-wms-service/DS.WMS.Core/HangfireJob/Method/JobMiddleware.cs @@ -1,4 +1,4 @@ -using DS.WMS.Core.HangfireJob.Interface; +using System.Linq.Expressions; using Hangfire; using Hangfire.Dashboard.BasicAuthorization; using Microsoft.AspNetCore.Builder; @@ -21,7 +21,7 @@ namespace DS.WMS.Core.HangfireJob.Method // 将 Hangfire 仪表板添加到应用程序的请求处理管道中 app.UseHangfireDashboard("/hangfire", new DashboardOptions { - Authorization = new[] {new BasicAuthAuthorizationFilter(new BasicAuthAuthorizationFilterOptions + Authorization = [new BasicAuthAuthorizationFilter(new BasicAuthAuthorizationFilterOptions { RequireSsl = false, SslRedirect = false, @@ -34,13 +34,22 @@ namespace DS.WMS.Core.HangfireJob.Method PasswordClear = "ds2024" } } - })} + })] }); - RecurringJob.AddOrUpdate(nameof(IFeeCustTemplateJobService), - s => s.GenerateFeesAsync(), Cron.Daily(23, 30)); - return app; } + + /// + /// 注册定时任务 + /// + /// + /// + /// CRON表达式 + /// 任务ID + public static void RegisterJob(Expression> methodCall, string cron, string? jobId = null) + { + RecurringJob.AddOrUpdate(jobId ?? typeof(T).FullName, methodCall, cron); + } } } diff --git a/ds-wms-service/DS.WMS.Core/HangfireJob/Method/WSLReportJobService.cs b/ds-wms-service/DS.WMS.Core/HangfireJob/Method/WSLReportJobService.cs index 1e2278d7..66f33939 100644 --- a/ds-wms-service/DS.WMS.Core/HangfireJob/Method/WSLReportJobService.cs +++ b/ds-wms-service/DS.WMS.Core/HangfireJob/Method/WSLReportJobService.cs @@ -1,8 +1,10 @@ using DS.Module.Core; -using DS.WMS.Core.Sys.Entity; +using DS.WMS.Core.HangfireJob.Dtos; +using DS.WMS.Core.HangfireJob.Interface; +using DS.WMS.Core.Op.Entity; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using MiniExcelLibs; using SqlSugar; namespace DS.WMS.Core.HangfireJob.Method @@ -10,11 +12,11 @@ namespace DS.WMS.Core.HangfireJob.Method /// /// WSL报表服务 /// - public class WSLReportJobService + public class WSLReportJobService : IWSLReportJobService { static readonly ApiFox api; ISqlSugarClient? db; - IConfiguration configuration; + Microsoft.Extensions.Configuration.IConfiguration config; IWebHostEnvironment hostEnvironment; static WSLReportJobService() @@ -29,7 +31,7 @@ namespace DS.WMS.Core.HangfireJob.Method public WSLReportJobService(IServiceProvider serviceProvider) { db = serviceProvider.GetRequiredService(); - configuration = serviceProvider.GetRequiredService(); + config = serviceProvider.GetRequiredService(); hostEnvironment = serviceProvider.GetRequiredService(); } @@ -37,36 +39,106 @@ namespace DS.WMS.Core.HangfireJob.Method /// 生成报表 /// /// - public async Task GeneratReportAsync() + public async Task GeneratReportAsync() { + string path = Path.Combine(hostEnvironment.WebRootPath, "templates", "WSL.xlsx"); + FileInfo templateFile = new FileInfo(path); + if (!templateFile.Exists) + throw new ApplicationException("未能在下列路径找到模板文件:" + path); + db.QueryFilter.Clear(); var dbLinks = await db.Queryable().ToListAsync(); SqlSugarClient? tenantDb = null; + var today = DateTime.Now; + var today0 = new DateTime(today.Year, today.Month, today.Day); + var yesterday0 = today0.AddDays(-1).Date; + var yesterday = new DateTime(yesterday0.Year, yesterday0.Month, yesterday0.Day, 23, 59, 59); + var lastMonth = today.AddMonths(-1); + var nextMonth = today.AddMonths(1); + var startDate = new DateTime(lastMonth.Year, lastMonth.Month, lastMonth.Day); + var endDate = new DateTime(nextMonth.Year, nextMonth.Month, nextMonth.Day, 23, 59, 59); try { foreach (var dbLink in dbLinks) { - var adminUser = await db.Queryable() - .Where(x => x.TenantId == dbLink.TenantId && x.Status == 0 && x.UserType == 1) - .OrderByDescending(x => x.CreateTime) + if (config["TaskMail:DefaultSetting:Tenant"] != dbLink.TenantId.ToString()) + continue; + + tenantDb = new SqlSugarClient(new ConnectionConfig + { + ConfigId = dbLink.Id, + ConnectionString = dbLink.Connection, + DbType = dbLink.DbType, + IsAutoCloseConnection = true + }); + + WSLModel model = new() + { + Date = today.ToString("yyyy.MM.dd"), + Month = today.ToString("yyyy.MM") + }; + + var list = await tenantDb.Queryable().Where(x => x.SourceCode == "FOB-WSL" && SqlFunc.Between(x.ETD, startDate, endDate)) .Select(x => new { x.Id, - x.UserName, - x.Password, - x.DefaultOrgId, - x.DefaultOrgName, - x.TenantId, - x.TenantName - }).FirstAsync(); - - if (adminUser == null) + x.CustomerId, + x.CustomerName, + x.ETD, + x.TEU + }).ToListAsync(); + + var ids = list.Select(x => x.Id.ToString()); + var ctnList = await tenantDb.Queryable().Where(x => ids.Contains(x.BSNO) && + !SqlFunc.IsNullOrEmpty(x.CntrNo) && !SqlFunc.IsNullOrEmpty(x.SealNo)) + .Select(x => new + { + x.BSNO, + x.TEU + }).ToListAsync(); + + model.List = list.GroupBy(x => new { x.CustomerId, x.CustomerName }).Select(x => new WSLItem { - Console.WriteLine($"未能获取租户系统管理员,租户ID:{dbLink.TenantId}"); - continue; + CustomerId = x.Key.CustomerId, + CustomerName = x.Key.CustomerName, + Date = model.Date, + YesterdayTeu = x.Where(x => x.ETD >= yesterday0 && x.ETD <= yesterday).Sum(x => x.TEU), + TodayTeu = x.Where(x => x.ETD >= today0 && x.ETD <= today).Sum(x => x.TEU), + NextMonthTEU = x.Where(x => x.ETD.Value.Year == nextMonth.Year && x.ETD.Value.Month == nextMonth.Month).Sum(x => x.TEU), + LastMonthTEU = x.Where(x => x.ETD.Value.Year == lastMonth.Year && x.ETD.Value.Month == lastMonth.Month).Sum(x => x.TEU), + }).ToList(); + + foreach (var item in model.List) + { + var ids2 = list.Where(x => x.CustomerId == item.CustomerId).Select(x => x.Id.ToString()); + item.TodayTeuCTNPickup = ctnList.Where(x => ids2.Contains(x.BSNO)).Sum(x => x.TEU); } + MemoryStream ms = new MemoryStream(); + await MiniExcel.SaveAsByTemplateAsync(ms, path, model); + string base64Str = Convert.ToBase64String(ms.ToArray()); + var attaches = new List + { + new() { AttachName = "WSL Volume Daily Increase Report.xlsx", AttachContent = base64Str} + }; + + dynamic[] mailParams = [new + { + SendTo = config["TaskMail:DefaultSetting:Receivers"], + Title = "WSL Volume Daily Increase Report", + Body = "Dear WSL Team" + Environment.NewLine + "Pls kindly check the daily report for your member's nomination booking:", + //ShowName = "", + Account = config["TaskMail:DefaultSetting:Account"], + Password = config["TaskMail:DefaultSetting:Password"], + Server = config["TaskMail:DefaultSetting:Host"], + Port = config["TaskMail:DefaultSetting:Port"], + UseSSL = config["TaskMail:DefaultSetting:UseSSL"], + Attaches = attaches + }]; + var mailResult = await api.SendRequestAsync(HttpMethod.Post, config["TaskMail:MailApiUrl"], mailParams); + if (!mailResult.IsSuccessStatusCode) + throw new ApplicationException("发送邮件失败"); } } finally @@ -74,5 +146,29 @@ namespace DS.WMS.Core.HangfireJob.Method tenantDb?.Dispose(); } } + + class DefaultSetting + { + public long Tenant { get; set; } + + public string? Account { get; set; } + + public string? Password { get; set; } + + public string? Host { get; set; } + + public int? Port { get; set; } + + public bool UseSSL { get; set; } + + public string? Receivers { get; set; } + } + + class Attachment + { + public string? AttachName { get; set; } + + public string? AttachContent { get; set; } + } } } diff --git a/ds-wms-service/DS.WMS.Core/Info/Entity/InfoClientStakeholder.cs b/ds-wms-service/DS.WMS.Core/Info/Entity/InfoClientStakeholder.cs index 38dd6cb5..91de5966 100644 --- a/ds-wms-service/DS.WMS.Core/Info/Entity/InfoClientStakeholder.cs +++ b/ds-wms-service/DS.WMS.Core/Info/Entity/InfoClientStakeholder.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using Masuit.Tools.Systems; using SqlSugar; namespace DS.WMS.Core.Info.Entity @@ -21,6 +22,12 @@ namespace DS.WMS.Core.Info.Entity [SugarColumn(ColumnDescription = "客户ID", IsNullable = false)] public long ClientId { get; set; } + /// + /// 客户简称 + /// + [SugarColumn(IsIgnore = true)] + public string? ClientShortName { get; set; } + /// /// 干系人ID /// @@ -51,6 +58,12 @@ namespace DS.WMS.Core.Info.Entity [SugarColumn(ColumnDescription = "干系人状态", IsNullable = false)] public StakeholderStatus Status { get; set; } + /// + /// 干系人状态文本 + /// + [SugarColumn(IsIgnore = true)] + public string StatusText => Status.GetDescription(); + /// /// 备注 /// diff --git a/ds-wms-service/DS.WMS.Core/Info/Method/ClientStakeholderService.cs b/ds-wms-service/DS.WMS.Core/Info/Method/ClientStakeholderService.cs index ffafae2b..35bf0bea 100644 --- a/ds-wms-service/DS.WMS.Core/Info/Method/ClientStakeholderService.cs +++ b/ds-wms-service/DS.WMS.Core/Info/Method/ClientStakeholderService.cs @@ -43,12 +43,16 @@ namespace DS.WMS.Core.Info.Method /// public async Task SubmitAuditAsync(IdModel idModel) { - var list = await TenantDb.Queryable().Where(x => idModel.Ids.Contains(x.Id)).Select(x => new - { - x.Id, - x.Status, - x.CreateByName, - }).ToListAsync(); + var list = await TenantDb.Queryable() + .InnerJoin((x, y) => x.ClientId == y.Id) + .Where((x, y) => idModel.Ids.Contains(x.Id)).Select((x, y) => new + { + x.Id, + x.Status, + x.CreateBy, + x.CreateByName, + CustShortName = y.ShortName + }).ToListAsync(); if (list.Exists(x => x.Status == StakeholderStatus.Pending)) return DataResult.FailedWithDesc(nameof(MultiLanguageConst.ItemsAreAuditing)); @@ -59,7 +63,7 @@ namespace DS.WMS.Core.Info.Method { BusinessId = x.Id, TaskTypeName = CLIENT_STAKEHOLDER_TASK.ToString(), - TaskTitle = $"【{CLIENT_STAKEHOLDER_TASK.GetDescription()}】{x.CreateByName}" + TaskTitle = $"【{CLIENT_STAKEHOLDER_TASK.GetDescription()}】{x.CustShortName} {x.CreateByName}" }); await TenantDb.Ado.BeginTranAsync(); @@ -244,6 +248,22 @@ namespace DS.WMS.Core.Info.Method .Where(whereList) .ToQueryPageAsync(request.PageCondition); + if (result.Data?.Count > 0) + { + var ids = result.Data.Select(x => x.ClientId).Distinct(); + var clients = await TenantDb.Queryable().Where(x => ids.Contains(x.Id)) + .Select(x => new + { + x.Id, + x.ShortName + }).ToListAsync(); + + foreach (var item in result.Data) + { + item.ClientShortName = clients.Find(x => x.Id == item.ClientId)?.ShortName; + } + } + return result; } diff --git a/ds-wms-service/DS.WMS.FeeApi/Program.cs b/ds-wms-service/DS.WMS.FeeApi/Program.cs index 74e7ed76..f084b8e6 100644 --- a/ds-wms-service/DS.WMS.FeeApi/Program.cs +++ b/ds-wms-service/DS.WMS.FeeApi/Program.cs @@ -15,6 +15,7 @@ using NLog.Web; using DS.WMS.Core.HangfireJob.Method; using DS.Module.HangfireModule; using Hangfire; +using DS.WMS.Core.HangfireJob.Interface; var builder = WebApplication.CreateBuilder(args); var environment = builder.Environment.EnvironmentName; @@ -57,5 +58,6 @@ app.UsePublicMiddlewares(); app.UseHangfireServer(); app.UseJobMiddlewares(); +JobMiddleware.RegisterJob(j => j.GenerateFeesAsync(), Cron.Daily(23, 30)); app.Run(); \ No newline at end of file diff --git a/ds-wms-service/DS.WMS.OpApi/Program.cs b/ds-wms-service/DS.WMS.OpApi/Program.cs index 62ba33d4..2b194bb4 100644 --- a/ds-wms-service/DS.WMS.OpApi/Program.cs +++ b/ds-wms-service/DS.WMS.OpApi/Program.cs @@ -15,6 +15,8 @@ using DS.Module.SqlSugar; using DS.Module.Swagger; using DS.Module.UserModule; using DS.WMS.Core; +using DS.WMS.Core.HangfireJob.Interface; +using DS.WMS.Core.HangfireJob.Method; using Hangfire; using NLog.Web; using Swashbuckle.AspNetCore.SwaggerUI; @@ -48,7 +50,8 @@ builder.Services.AddMultiLanguageInstall();// builder.Services.AddDjyModuleInstall();//Djy builder.Services.AddRuleEngineModuleInstall();//DjyУ -builder.Services.AddHangfireModuleInstall();//Hangfire +//builder.Services.AddHangfireModuleInstall();//Hangfire +builder.Services.AddHangfireOPInstall();//Hangfire builder.Services.AddMQModuleInstall();//MQ // builder.Services.AddEndpointsApiExplorer(); @@ -105,4 +108,7 @@ app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); //await ServiceLocator.Instance.GetService().LoadCommonCache(); +app.UseHangfireServer(); +JobMiddleware.RegisterJob(j => j.GeneratReportAsync(), Cron.Daily(23, 59)); + app.Run(); \ No newline at end of file diff --git a/ds-wms-service/DS.WMS.OpApi/appsettings.json b/ds-wms-service/DS.WMS.OpApi/appsettings.json index cfa05c8b..d031cf89 100644 --- a/ds-wms-service/DS.WMS.OpApi/appsettings.json +++ b/ds-wms-service/DS.WMS.OpApi/appsettings.json @@ -111,7 +111,16 @@ "JsonPrint": "/printApi/OpenPrint/GetOpenJsonPrintInfoAsync", "JsonPrintByCode": "/printApi/OpenPrint/GetOpenJsonPrintInfoByTemplateCode", "SQLPrint": "/printApi/OpenPrint/GetOpenSqlPrintInfo", - "MailApiUrl": "http://47.104.73.97:8801/mail/send" + "MailApiUrl": "http://47.104.73.97:8801/mail/send", + "DefaultSetting": { + "Tenant": 1750335377144680448, + "Account": "lyle@yyts.fun", + "Password": "15275215387jZ", + "Host": "smtp.mxhichina.com", + "Port": 465, + "UseSSL": true, + "Receivers": "daisusu@dongshengsoft.com" + } }, "FeeService": { "BaseUrl": "http://118.190.144.189:3008", diff --git a/ds-wms-service/DS.WMS.OpApi/wwwroot/templates/WSL.xlsx b/ds-wms-service/DS.WMS.OpApi/wwwroot/templates/WSL.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d55087e44c5745de57568752e36444836eeed1d9 GIT binary patch literal 10307 zcmeHN1y>x~(j6Ey*x(l2C3w)_65QPq+}#}pw*&^4;O>&(?oO~^39boF2p+yk?tAa% z=HBlYyw|g)duFXuwfdaidslVUQI-S3;Q$Z-NB{tU0??42W9|k60K~!r0N4N|SUqtE zdsj1i*B9zuj%F?fES`3@@n==lE~|HUg%kvOT?%Z55|EZrua#AIFXB!X1A zY}AEG%T7v8eMIy`A&-}Md3n=3AVgW`sa=Yc-NEr?|2xifTcU(YiX8U_(_Z5)j+$u( zJA9hk%+8(H%)Rs#SOYrri+FJg1C2cS*B^l9iFUQ7ZZY2@T@s&i!J_PTPb+SVQBk&s z!C#88^uu1=zdY$GtjDw+Gb!&L$hRoz8|gbR6VLWgge5Rtbj4q&lcjCr(T$Q;yk063 zPV`lxr|0Z{St+;@k2WBHd^qtC=(i$}L~9AZC?9e~OH|j*6Bf`~bLVWYlZm(QxL+nK zr>s~-R3;K66sW8X>>I$bdP0^D)*;nAW#COj5bfq?1b>^$tGJ#aD}^V{+mw&pluk<>EV{i4kkK7Gf_ftw7MtlP zkW_k|q+8SPr3>~uulnAvus)h?wr%d&OOHMkNJHmUO4+|c1OOf$fB@ycuoc^|!weS6 z)>SAOQJ`#n;cRB>!pidF{6Cid7gO_>saHT0`VCtXDs7(?!tvoRK-SK2n)?aAc5O+&3>?8v%VFtc9JuJ?pIliG8+A) zZIZB6BIR+e{t0O$0T*DN&{f-Y8#j+awbiJ%!*C%LQ(Te6M4Zl5+@jd$^QU>5xrNl- zHU1;}5hJ&hCO}d;-TUmlW1A^eRVkX#bl{tv>jPf+w`~QIF4zGd)1-WN+1fZTinZ-o zhx0DkYXwavv?h75CRXZhpsVnA%7~S|k|F{E0HshuVL(TC+Om2$INKOIIN1CMZUq{4 z4jF8q??P4&FxLX`32`8Gj;Fbn@zX5Ut?CJs8tA;;j4#bIXf@^@Iykoqt%?|IRE$kl zuTNb(UXNclx@8e6sY?$;1J*f-3ExI|^+cHk`mab9g`<{A$H&FsbBRo<=Q-S0znIX{ ze>TAvzy-*OgON>Q^c#gZ$+}QZddi^GFgFcFf^)TX>u?gOISSRc)3z7f2S<>Zl!Moz z)!43#yN!v?nntX0i9n*9d4V1BIt-1LUcQg?75hvy(%C2F0#oZ_N_UBH2)zzoC&S~L zW{ED?91z*SbfVwYqQ%yKLY9B7x6O>Fe-_FQv@ECa!x3(H5`&>yBQ()%C2c47p03EI z$%KZV%ye$i$nD$9h3{NM2v&2trxadf5-uJeAI}QEsEJV|-X|(xu9m4{N}`~mQZ=BJ zpQ08qAUMPQq|`J7(%>u6M$jCrC&Aabf0IcEi=ccPEw7>ik_>NCCVz)^F*#B;Kw9)z z!uI3)Ra|VG33O?so}lDDp|nwCl~R>RW|3Us4E6JvVOP8r@A`=8Xvnx`dD5ExIL^3S zoKR9dk1q9K&jZF&h&Ej>7>eq1lIE}% z#Hl?w?Tw!K($I^3l)j!roNhO7mj-l*P$*s|HLiABbgsRB)E)7P%tf`-Rm~8MB!_$U zY{*segC0u5eH4uk5$m@dcDIMB#U(?xt{ch>juway#a?c0hlOO177Ryi^6~k)qXzM@ zPM)eV0^q60m z&Y1nz?D~^8QkCbZBi}B#jT6^eUM(lF4{t=WxoQb~t2zAo7SW)*R^1IF%B0J2%^TP{ z=Q8FVLRBB@)4)gIx>Q6g^`tSncfF8!A8TK4^@UAeKO#oG#auaq3T`H>AK-Is^|B>r zYlc1-cE%nC&f#hZf8zM(0KSzTtta_({Lg2wNy6u~JhbR;Ua=mN9_6=i2gH5=gS*m} zllzza&29t!kwy^2!~-~Jp{W}oBK+{F&*#GXM0y1jd$A&6- z8TvhVs>6xHPdUuVd`dOdF2g_2F_Bl^7*Wq}>)1w>f4D2ozK&hG=aMwCbveMJ@`{>_ z$O6|?<`KBU-OR)W{^SL$jCnQqS~fi=PGe5vtxx%qosat%sN!Xe zZM}^8MW+WUg}SVUq#hi1jNt24!q+;&nd%CL-P)xYnNp(C$XsIhb&I=Smq*=$F6W+P* zlFCM6EuMEwg&kYR+~O4K$0c+N4A2p#>>u(lF)+k5w79}R7IUt-?{M|lS*i+_ua0{p z7&uQD?-3MBuIwOr*GafaHCwSY{kS_bW_5~qeD{QgRfON-9ePR$2mPnUTjH8}g&2)< z#`Xv4ePkJyD{gPDS=KEdJn3E?cZi|&z*J$XY>c`TUvw+^N~!G`EXE!;uue{f;V7R~ zxo5lHi{n*|>_@$7V7>%6QMo7BCouncutB0{qf2jdDwepm9lX z@Lc{*i^1be*?+P(p!Z>@2M2>tmwHr&NVgj1M)1qb4e^gi{SzaZiKF(xY#_-1c&|Bo zu`X)rA#&XFC0C6LAc#f*9~rmE>h_Yv(bMxlTEpGabq8Gt)qF#H-aysetZ!*&*;;%9 zB3vTbt(AI9xuw9%dFBhcz@xM}Nblsg4C`QD}@5Zp$K02ir%hQyIU2rA&_A+KLv z?_@$9TB#uO=W6>jVUTr8?$*5<*(=vig;^{9_3Y*jG(%qWdQ6Dm2>z9kBG4a2QOdJT zLiOXDY4Y~DH|s}_@uMU~kE#S$Q~uELpNsr#OzsgZJ1AtmJVYokAH|6?EtUx%tQDcS z8?=PWKOWJ@L$z1se^pXzj-=OYFe@4H#$3tA`CHY2jAICMMWu*k>HrZ|c5o5pxFBzO z@!`(M~Q&%6Amgv0T_u5i>Vko_b7KhsWTk3#DV+=M@B z4L|wsYH4O?#`^R8lllibO0S8Z;&f7-l1RA}%ebEx>payv+)=`BS=RjYw&c0RK$_Dg z;cHuq#^&5_5g{S;5hz$Z=tw2Avic*%I`m-53#OBUw6O)^aJx}W#*$&0&Fl8v-OH?i z$;oEdYeTVisxf#Yt4Rm_l3P+mARdJ4^L67uJ_gH8RvbMVrWwQ0H45|H=<;!=2eRET z0o?-dTHQ7`i%c%fZqAxA(1@C)2DSLae#8?@{p_p+&)F5#8(Vj|WPbS_T>R{%YGUBh zml+1+n$C80fr1=?Br!C&L^8%gPh8>&qDK^Xc}mS5A(oTjgktW`1}+PZRj8@k_O9LT zPi>Y{tS!qydCsvF6g<<}VpSj{3u13YQiTzjjB(BScD2v*07fmR-iaDP+mstF06a_u zS7ZO|;?m&}x-R?&z_u_+3*wt@^WGlf8y+p&6|2(f(D@ww02g>+qKocmJi}mT-_iAi z38_x2^{4cMjanyfZYFXTYfp%9)F2a@?=Lic>O1xAkkdF?c0)Y$##Joxl?`*riAN4p z)#P;;=eKpiM;t=1fv~F7GM|Sf!;0wx?+&SCX)`@#OBmj850h(8!0wL8XBEZ4@5RHx;v~`fAeq?_1D|@a^VpIQkX(b8KuOT4cPHr@jZD zOpz-T1O^%&D_xb##F$7hBjW_yhkwqEd9wBnK9A)iOT!zF3}9$epw^DZ6Fm$Qp_D%M z^EuwyK*X3SgMl~bO=UYA&z@x!$NM-7@i_5d@;i@zdR~gVwEhfuM9q?PPoQVMl0%GX zokaGR99L-<9$hXRkfPw*o`G<`*qX&^hTaf=r(pIwWnrXCHd}=}7&Jda2=MQT&l- z3IhG27JEwlFAhw)z^`U{-~du3~xHU;y13F(B&;c~H*z%pr%q)MBVV$GD|B{_*Djvi^AWh5#z z6Pyi7f$=Y|&tu#gAC;PAIrlsBsGGa8bVO=1Ve$2&aXn1~7p*u=Iyf|}*)xggOlFH+ zq6;UWIzKN-c#?;y?+*{zCI0S~O0y}39v9&fZZkc+NKS#}@)}aGTfUvLD55q_vD7et z0P4}R(Z{#kw&O$x+anSx74ycKFT`T{k>ta4o$zV#u3DeYBQnWwhdYuDtk?47y7*&@ zM!n-l+wO`(YTB^cT0uqrD4Q&yxxHH2qyE0i>q&_VJ=t@vXcZL|d5+2TbO||^uWB8Z z2HRah$tVZAc_2kfjh7E@bAqkvOp}BKI&<>T8tQYd3HupLXJHx|nC~h^O7$Ai@-|wI zAYaH;-e_#Zl-**%ce7Qdk8vpIw8{2H)kmu=zjb*ABFM6=KGoBq%FOcoG=kw=T(5~6 zvE8id0K6Xzy~Y4$e1tzE=rTCUl#O3qSqavA%=>k_v75%ua_X=Xn)(0zHGCyyom>+R z0D#8?0MP!>eiv6STQip*3E!;t_^V}ZPzU~=08*O`%_3x;gBkJE8y-$N2(}g8yO3st zRxBPt(qS8?{b_Ml6zMIXy>K2NdBcSk*CrXBXgZwXc2{rt{xKdNgY!;KehC^CkDOQ{Jgp~Y zZlP*<{3gyYWUHwO#MHB{Kx_I67j?UbK@+m}_I-x)&R2bpwXIZVbsd83VRk8J<`)%O z?v!V^ny!k38pI|jI$j*!4QpZ>e87^z-OWD6_MiiuLfg#L#n*RRT=99Jv=%_?t09i` zxYz4pihFAX@8FUr>$l)vEiXs5CtB#9k;ojC6k#X~q^*_JVAbw0ictXO7zoUyMh$Ao7N^J2Ba`S9X`eK=YEh)Y#xRvN+yhGlh&oJ&Gy z*OpELA^z|$nDGSDD7@2&wRVx$E$J_fIl>GJBDUC3D{ijc>%qDk@43roEIyEEx5s&I zUVt}waKKaQ`P%wvkCLOkLN*rMe2%&t1*%`zE|R1=JMjm)HIo95I;var0>$Xdh+p|Q z5r!G9d8OC~>|v}|R|EOrgJ58FvQW~%^iL}*FfcXM64AG<{<86>&bDKp_G#MT{t& z`59v}zm{MAhOIOs0*KO%G*3hS}nX9Mh@VRZYj^nhr+3rml{v3`?+;Zm?8x!*Zfwz zecE86XwwZG<}9rlrF^Oflrj=m5#QoKI^c5Bfqn#2{)*o*KwnIp`G>w+jO8<^&%C`* zSxMpUl!T*h#Dxu3VlRJMxP8H#HL19f6N-9$M@gQoUr9GRy6Dw*cUFR^6HxQz-oj5> z?wclZ&ZP&n<@3M>F{6FfR-|^H^PPJ#VGnD|jv~|!>9UFHN_&sM@wc|yW4`1JetKMD zjTf+G$q>})w3bgDG0L}hXfpf!;dPZ-esael9JG|PLc;Se=vPD<;Z@6a;fI9obZum) zzHVN8<;XM`6!h6s3~yMa3nD?7Nh7(^nptEpCCY-LCne z`@@>UQZtep08VV%5#lPMu$FoeEL364kQughXaq~WL-{8GD$Jzp@P$f~8Z-gL`Ii7$ z8abPps<}E_*<1Y7p^8K$n;sER|8b~)xY2QorX>SJ!d6~1BA}+Wf0LU~p=43PgHAHu z|FWtSN#U@6Cv|ag@OZo}c4l>l_)5f%gT}yp=7>#%u$(% z%kLd*I3excP8HOJo#m;Wc~zHEjFPMTv$zc$Dox!&TN!^37biMMhFQ?3Gz$HK{&-p@Mz$v6mPS_g zKO%EGw#(lp^=1Vx@Dbf%cl(wV7)iL$eUqYJBvgR|L>$v|5-|Erimm(CZmD%;CO(0>fS zn!DAZFg-l`-t8*ffDe>OzY@B&jAuIyk!Td`UJ*GFZM(3;@kN<&FDH&l8RlDM-Swp= zW^uz540bEy0!mI z!$X|p)qz}>9mWI#?1gQZC?G;(KXk684gG!H(?74vTz*Gl8H(@G5~ zo!(!4E-EErQahFfjBqlf)^zz2B(9Sbt{8W}Aqx*x!W}x-_UQ(fCIU@j!Q%euMfi`o zkyygpw2shpzVKF@1Sq&v+jKOgI>^B$Z?rI1PCeRBK3mya%V%Y~Zku`ue;ftgGtYSe z?yi)FKW;ECN(v?BNDi&TgcC;XBTy$;cbs0qFd zWBEm6-CSH9?EVMme}fqSD2Shw>t(|kgf@u9huRg=)87%=4c?&i1%q9*7_3@wmkOx& zT_YY^3HiejS-pLI$+9oKj%mv0odAfr3e~2MfMnw2)LM3|9t^%qvxcZawXwDtqB8M4 z`D~6!1}|tZbQQ#w4GGpj(*=$Whi@f@PTC2GH(Rw^3e>!9E0`|OORK&#)8l48r+7k1 zzzWKa)yU`Z+gzh_@Tv)rY>3J}7;|^Y%(PGT?S7p@Y`0o;QduCWqbe_`THAx-80W)m z8Xs;FBcm9VURmyeRHVikB*LAqcYk!cVzivL?0XdM&h%EC?7%tIFlLUhJJA#7^b^`W zIqejsXbPLCMR>qch(*S-@vX^So^b`e7?G;(3MlNHzv=^DbZ?&Sb0=+K#4R2Q*K4zH zP%!-gGZ@$>(1h`y_e=e`s(<|Yhuu@ka({L3*DV~sI;eso`eVX`|eLu0DvCajQRhYdB6Mly@m6qCtKA2+{E7+I=_4Qy%7A17ih8vy_?_5 z!oNHCy_)c+153Q04*sky{BHXDd%&Nj`$T`3{{B|*yNBO%(?30=JpSq7FPZA^)_=_g z|Fi`FOh^ELzvYI%oBuUv|7v~