From a15f97ac2e4991a47f647894c76585a4d142bfc8 Mon Sep 17 00:00:00 2001 From: chfei Date: Fri, 21 Aug 2026 14:15:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BB=E9=A1=B5=E5=A2=9E=E5=8A=A0=E5=8F=91?= =?UTF-8?q?=E7=A5=A8=E5=8A=A9=E6=89=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 29WxOrderWpf/29WxOrderWpf.csproj | 2 + .../ImageUrlToBitmapImageConverter.cs | 28 +- .../Ctrls/OrderCtrls/OrderListCtrl.Command.cs | 318 +++++++ .../Ctrls/OrderCtrls/OrderListCtrl.xaml | 42 +- .../Ctrls/OrderCtrls/OrderListCtrl.xaml.cs | 273 +----- .../Ctrls/PDDInvoice/PDDInvoiceView.xaml | 868 ++++++++++++++++++ .../Ctrls/PDDInvoice/PDDInvoiceView.xaml.cs | 37 + .../Ctrls/PDDInvoice/Resources/invoice.js | 134 +++ .../ViewModels/PDDInvoiceOrderModel.cs | 94 ++ .../ViewModels/PDDInvoiceViewModel.Browser.cs | 250 +++++ .../ViewModels/PDDInvoiceViewModel.cs | 633 +++++++++++++ .../Statistics/ShopRevenueStatisticsView.xaml | 27 +- .../Helpers/CookieConvertHelper.cs | 156 ++++ WxOrder.Framework/Helpers/HttpHelper.cs | 839 +++++++++++++++++ WxOrder.Framework/Helpers/ResourceHelper.cs | 145 +++ WxOrder.Framework/PDDAPI/PddApi.cs | 249 +++++ .../Resources/Fonts/FontGeometry.xaml | 4 + .../Resources/Fonts/iconfont.css | 10 +- WxOrder.Framework/Resources/Fonts/iconfont.js | 2 +- .../Resources/Fonts/iconfont.json | 7 + .../Resources/Fonts/iconfont.ttf | Bin 455132 -> 455420 bytes WxOrder.Framework/WxOrder.Framework.csproj | 16 +- 22 files changed, 3825 insertions(+), 309 deletions(-) create mode 100644 29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.Command.cs create mode 100644 29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml create mode 100644 29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml.cs create mode 100644 29WxOrderWpf/Ctrls/PDDInvoice/Resources/invoice.js create mode 100644 29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceOrderModel.cs create mode 100644 29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.Browser.cs create mode 100644 29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.cs create mode 100644 WxOrder.Framework/Helpers/CookieConvertHelper.cs create mode 100644 WxOrder.Framework/Helpers/HttpHelper.cs create mode 100644 WxOrder.Framework/Helpers/ResourceHelper.cs create mode 100644 WxOrder.Framework/PDDAPI/PddApi.cs diff --git a/29WxOrderWpf/29WxOrderWpf.csproj b/29WxOrderWpf/29WxOrderWpf.csproj index 1b34088..ef214a5 100644 --- a/29WxOrderWpf/29WxOrderWpf.csproj +++ b/29WxOrderWpf/29WxOrderWpf.csproj @@ -63,8 +63,10 @@ + + diff --git a/29WxOrderWpf/Converters/ImageUrlToBitmapImageConverter.cs b/29WxOrderWpf/Converters/ImageUrlToBitmapImageConverter.cs index dfbf7e4..2063aaf 100644 --- a/29WxOrderWpf/Converters/ImageUrlToBitmapImageConverter.cs +++ b/29WxOrderWpf/Converters/ImageUrlToBitmapImageConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -14,27 +15,38 @@ namespace _29WxOrderWpf.Converters { public class ImageUrlToBitmapImageConverter : IValueConverter { + /// 缓存上限,防止大量缩略图重复下载导致内存膨胀 + private const int MaxCacheCount = 200; + + private readonly ConcurrentDictionary _imageCache = new ConcurrentDictionary(); + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is string url && Uri.IsWellFormedUriString(url, UriKind.Absolute)) { + if (_imageCache.TryGetValue(url, out BitmapImage cached)) + { + return cached; + } + try { // 直接加载绑定如果遇到有问题的网络图片会直接崩溃。 - //var bitMap = new BitmapImage(new Uri(url)); - BitmapImage bitMap = null; - //bitMap = LoadImageFromUrl(url + "?imageView2/1/w/100/format/webp|imageMogr2/auto-orient="); - bitMap = AllBase.ImageBuild.LoadBitmipImageFromUrl(url); + BitmapImage bitMap = AllBase.ImageBuild.LoadBitmipImageFromUrl(url); + if (bitMap != null) + { + if (_imageCache.Count >= MaxCacheCount) + { + _imageCache.Clear(); + } + _imageCache[url] = bitMap; + } return bitMap; - //string errStr = ""; - //var bitmapImg = GetUrlImg(url, out errStr); - //return bitmapImg; } catch { } - //return Task.Run(() => new BitmapImage(new Uri(url))); // 使用 Task.Run 来异步加载图片。 } return null; } diff --git a/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.Command.cs b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.Command.cs new file mode 100644 index 0000000..becbbc4 --- /dev/null +++ b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.Command.cs @@ -0,0 +1,318 @@ +using _29WxOrderWpf.BaseWindows; +using _29WxOrderWpf.Commands; +using _29WxOrderWpf.Common; +using _29WxOrderWpf.Ctrls.PDDInvoice; +using _29WxOrderWpf.Ctrls.Statistics; +using _29WxOrderWpf.Services; +using AllBase.Helper; +using AllBase.Model.DBModel; +using NaiBa.Util.Extension; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using Work._29Tools.BLL; +using static _29WxOrderWpf.Services.ShipGiftService; + +namespace _29WxOrderWpf.Ctrls.OrderCtrls; + +public partial class OrderListCtrl +{ + #region 统计 + private DialogWindow _statisticsDialog; + public ICommand ShowShopRevenueStatisticsDialogViewCommand => new DelegateCommand(ShowShopRevenueStatisticsDialogView); + + private void ShowShopRevenueStatisticsDialogView() + { + if (_statisticsDialog == null) + { + _statisticsDialog = new DialogWindow() + { + Title = "统计", + Content = new ShopRevenueStatisticsView(), + + }; + _statisticsDialog.Closed += (s, e) => _statisticsDialog = null; + } + _statisticsDialog.Show(); + } + #endregion + + #region 礼物 + public ICommand MultipleShipGiftCommand => new AsyncDelegateCommand(MultipleShipGift); + /// + /// 批量发送礼品单 + /// + /// + /// + + private async Task MultipleShipGift() + { + + try + { + var list = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList(); + int count = list.Count; + if (count == 0) + { + BaseMsgWindow.ShowError("请选取要发货的订单", "Error"); + return; + } + + + GiftType giftType = GiftType._160; + if (cb_gift.SelectedIndex == 0) + { + giftType = GiftType._155; + } + else + { + giftType = GiftType._160; + } + int index = 1; + int sucessCount = 0; + int errorCount = 0; + foreach (oms_wei_order_info item in list) + { + try + { + var rst = await WxApiBLL.WxSearchAndDropshipCancelAsync(item.Shop, item); + if (rst == "success") + { + var ret = await _shipGiftService.Send(item.order_id, giftType); + if (ret) + { + sucessCount++; + item.gift_order_status = 1; + this.FilterOneOrder(item, true); + } + else + { + errorCount++; + } + } + else + { + errorCount++; + } + } + catch (Exception ex) + { + errorCount++; + } + finally + { + item.IsSelect = false; + } + + SetProgress(index, count); + BottomMsg = $"礼品单发送进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}"; + index++; + } + + BottomMsg = $"礼品单发送完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}"; + } + catch (Exception ex) + { + BaseMsgWindow.ShowError("礼品单发货失败\n" + ex.Message, "Error"); + } + finally + { + + } + } + #endregion + #region 同步订单 + /// + /// 拉取微信订单到任务列表 + /// + public ICommand PullWXOrderCommand => new AsyncDelegateCommand(PullWXOrder); + + private async Task PullWXOrder() + { + try + { + var isAfterSales = GlobalConfig.OrderTabHeaderStr.StartsWith("需处理售后单");//是否售后 + + var selectedShops = GlobalConfig.ShopLoginList.Where(a => a.IsSelect).ToList(); + if (selectedShops.Count == 0) + { + selectedShops = GlobalConfig.ShopLoginList.ToList(); + } + + var selectedOrders = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList(); + int count = selectedOrders.Count; + if (count > 0 && isAfterSales == false)//非售后订单 + { + if (BaseMsgWindow.ShowConfirm($"已选取:{count}个订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return; + BottomMsg = $"订单同步进度...."; + int index = 1; + int sucessCount = 0; + int errorCount = 0; + foreach (var item in selectedOrders) + { + var ret = await PullWXOrderService.Instance.PullOrderDetailAsync(item.shop_id, item.order_id); ; + if (ret) + { + sucessCount++; + this.GetOrder(item.order_id); + SetProgress(index, count); + item.IsSelect = false; + } + else + { + errorCount++; + } + BottomMsg = $"订单同步进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}"; + index++; + } + BottomMsg = $"订单同步进度完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}"; + } + else if (count > 0 && isAfterSales == true)//售后订单 + { + if (BaseMsgWindow.ShowConfirm($"已选取:{count}个售后订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return; + //var dictionaryResult = selectedOrders + // .Where(o => o?.refundItems != null) + // .SelectMany(o => o.refundItems + // .Where(item => item != null) + // .Select(item => new { o.shop_id, item.after_sale_order_id })) + // .GroupBy(x => x.shop_id) + // .ToDictionary( + // g => g.Key, + // g => g.Select(x => x.after_sale_order_id).ToArray() + // ); + + + //BottomMsg = $"售后订单同步进度...."; + //foreach (var item in dictionaryResult) + //{ + // var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(item.Key, item.Value); + //} + //BottomMsg = $"售后同步任务已发送,等待同步完成"; + + + + int index = 1; + int sucessCount = 0; + int errorCount = 0; + BottomMsg = $"售后订单同步进度...."; + foreach (KeyValuePair> item in selectedOrders.GroupBy(a => a.shop_id).ToDictionary(a => a.Key, a => a.ToList())) + { + long shopid = item.Key; + string shopName = item.Value.FirstOrDefault()?.shop_name ?? ""; + string[] afterSaleOrderIds = item.Value.SelectMany(a => a.refundItems).Where(a => a != null).Select(a => a.after_sale_order_id).ToArray(); + var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(shopid, afterSaleOrderIds); + if (ret) + { + sucessCount++; + SetProgress(index, count); + item.Value.ForEach(a => a.IsSelect = false); + } + else + { + errorCount++; + } + BottomMsg = $"售后订单同步进度:{index}/{count}\t\t{shopName}"; + index++; + + } + BottomMsg = $"售后同步任务已发送,等待同步完成"; + } + else + { + count = selectedShops.Count; + string prefix = $"已选取:{count}个商铺"; + if (count == GlobalConfig.ShopLoginList.Count) + { + prefix = "已选取全部商铺"; + } + + + var startTime = dateStart.SelectedDate.Value; + var endTime = dateEnd.SelectedDate.Value; + + string msg = ""; + if (isAfterSales) + { + msg = $"{prefix}"; + } + else + { + msg = $"{prefix}\n\n时间范围:{startTime.ToString("yyyy-MM-dd")}-{endTime.ToString("yyyy-MM-dd")}"; + } + + if (BaseMsgWindow.ShowConfirm(msg, $"{(isAfterSales ? "售后" : "")}同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return; + + if ((endTime - startTime).Days > 7) + { + throw new Exception("时间范围最大7天"); + } + + var ret = false; + if (isAfterSales) + { + ret = await PullWXOrderService.Instance.PullRefundOrderByShopsAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime); + } + else + { + ret = await PullWXOrderService.Instance.PullOrderByShopIdsAndTimeRangeAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime); + } + if (ret) + { + BaseMsgWindow.ShowInfo("同步任务已发送,等待同步完成\n任务详情查看任务列表"); + } + else + { + BaseMsgWindow.ShowError("同步任务失败"); + } + } + + + } + catch (Exception ex) + { + BaseMsgWindow.ShowError("同步订单失败\n" + ex.Message, "Error"); + } + //var service = PullWXOrderService.Instance; + //foreach (var item in list) + //{ + // await service.PullOrderDetailAsync(item.shop_id, item.order_id); + //} + } + #endregion + #region 显示任务列表 + private DialogWindow _taskDialog; + public ICommand ShowTaskDialogViewCommand => new DelegateCommand(ShowTaskDialogView); + + private void ShowTaskDialogView() + { + if (_taskDialog == null) + { + _taskDialog = new DialogWindow() + { + Title = "任务列表", + Content = new OrderTaskDialogView(), + + }; + _taskDialog.Closed += (s, e) => _taskDialog = null; + } + _taskDialog.Show(); + } + #endregion + + #region 拼多多发票 + public ICommand ShowPDDInvoiceDialogCommand => new DelegateCommand(ShowPDDInvoiceDialog); + + private void ShowPDDInvoiceDialog() + { + new DialogWindow + { + Title = "发票助手", + Content = new PDDInvoiceView() + }.Show(); + } + #endregion +} diff --git a/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml index c36abb5..129ea92 100644 --- a/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml +++ b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml @@ -25,7 +25,8 @@ - + + @@ -40,11 +41,8 @@ - - - - + @@ -205,7 +203,8 @@ Padding="4" Click="btnFilter_Click" Content="过滤" - Style="{StaticResource BasePageButtonStyle}" /> + Style="{StaticResource BasePageButtonStyle}" + ToolTip="过滤" /> - + _statisticsWindow = null; - } - _statisticsWindow.Show(); - - } + /// /// @@ -2268,263 +2253,7 @@ namespace _29WxOrderWpf.Ctrls.OrderCtrls // set => SetProperty(ref _IsShipGift, value); //} - public ICommand MultipleShipGiftCommand => new AsyncDelegateCommand(MultipleShipGift); - /// - /// 批量发送礼品单 - /// - /// - /// - private async Task MultipleShipGift() - { - - try - { - var list = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList(); - int count = list.Count; - if (count == 0) - { - BaseMsgWindow.ShowError("请选取要发货的订单", "Error"); - return; - } - - - GiftType giftType = GiftType._160; - if (cb_gift.SelectedIndex == 0) - { - giftType = GiftType._155; - } - else - { - giftType = GiftType._160; - } - int index = 1; - int sucessCount = 0; - int errorCount = 0; - foreach (oms_wei_order_info item in list) - { - try - { - var rst = await WxApiBLL.WxSearchAndDropshipCancelAsync(item.Shop, item); - if (rst == "success") - { - var ret = await _shipGiftService.Send(item.order_id, giftType); - if (ret) - { - sucessCount++; - item.gift_order_status = 1; - this.FilterOneOrder(item, true); - } - else - { - errorCount++; - } - } - else - { - errorCount++; - } - } - catch (Exception ex) - { - errorCount++; - } - finally - { - item.IsSelect = false; - } - - SetProgress(index, count); - BottomMsg = $"礼品单发送进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}"; - index++; - } - - BottomMsg = $"礼品单发送完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}"; - } - catch (Exception ex) - { - BaseMsgWindow.ShowError("礼品单发货失败\n" + ex.Message, "Error"); - } - finally - { - - } - } - - /// - /// 拉取微信订单到任务列表 - /// - public ICommand PullWXOrderCommand => new AsyncDelegateCommand(PullWXOrder); - - private async Task PullWXOrder() - { - try - { - var isAfterSales = GlobalConfig.OrderTabHeaderStr.StartsWith("需处理售后单");//是否售后 - - var selectedShops = GlobalConfig.ShopLoginList.Where(a => a.IsSelect).ToList(); - if (selectedShops.Count == 0) - { - selectedShops = GlobalConfig.ShopLoginList.ToList(); - } - - var selectedOrders = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList(); - int count = selectedOrders.Count; - if (count > 0 && isAfterSales == false)//非售后订单 - { - if (BaseMsgWindow.ShowConfirm($"已选取:{count}个订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return; - BottomMsg = $"订单同步进度...."; - int index = 1; - int sucessCount = 0; - int errorCount = 0; - foreach (var item in selectedOrders) - { - var ret = await PullWXOrderService.Instance.PullOrderDetailAsync(item.shop_id, item.order_id); ; - if (ret) - { - sucessCount++; - this.GetOrder(item.order_id); - SetProgress(index, count); - item.IsSelect = false; - } - else - { - errorCount++; - } - BottomMsg = $"订单同步进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}"; - index++; - } - BottomMsg = $"订单同步进度完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}"; - } - else if (count > 0 && isAfterSales == true)//售后订单 - { - if (BaseMsgWindow.ShowConfirm($"已选取:{count}个售后订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return; - //var dictionaryResult = selectedOrders - // .Where(o => o?.refundItems != null) - // .SelectMany(o => o.refundItems - // .Where(item => item != null) - // .Select(item => new { o.shop_id, item.after_sale_order_id })) - // .GroupBy(x => x.shop_id) - // .ToDictionary( - // g => g.Key, - // g => g.Select(x => x.after_sale_order_id).ToArray() - // ); - - - //BottomMsg = $"售后订单同步进度...."; - //foreach (var item in dictionaryResult) - //{ - // var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(item.Key, item.Value); - //} - //BottomMsg = $"售后同步任务已发送,等待同步完成"; - - - - int index = 1; - int sucessCount = 0; - int errorCount = 0; - BottomMsg = $"售后订单同步进度...."; - foreach (KeyValuePair> item in selectedOrders.GroupBy(a => a.shop_id).ToDictionary(a => a.Key, a => a.ToList())) - { - long shopid = item.Key; - string shopName = item.Value.FirstOrDefault()?.shop_name ?? ""; - string[] afterSaleOrderIds = item.Value.SelectMany(a => a.refundItems).Where(a => a != null).Select(a => a.after_sale_order_id).ToArray(); - var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(shopid, afterSaleOrderIds); - if (ret) - { - sucessCount++; - SetProgress(index, count); - item.Value.ForEach(a => a.IsSelect = false); - } - else - { - errorCount++; - } - BottomMsg = $"售后订单同步进度:{index}/{count}\t\t{shopName}"; - index++; - - } - BottomMsg = $"售后同步任务已发送,等待同步完成"; - } - else - { - count = selectedShops.Count; - string prefix = $"已选取:{count}个商铺"; - if (count == GlobalConfig.ShopLoginList.Count) - { - prefix = "已选取全部商铺"; - } - - - var startTime = dateStart.SelectedDate.Value; - var endTime = dateEnd.SelectedDate.Value; - - string msg = ""; - if (isAfterSales) - { - msg = $"{prefix}"; - } - else - { - msg = $"{prefix}\n\n时间范围:{startTime.ToString("yyyy-MM-dd")}-{endTime.ToString("yyyy-MM-dd")}"; - } - - if (BaseMsgWindow.ShowConfirm(msg, $"{(isAfterSales ? "售后" : "")}同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return; - - if ((endTime - startTime).Days > 7) - { - throw new Exception("时间范围最大7天"); - } - - var ret = false; - if (isAfterSales) - { - ret = await PullWXOrderService.Instance.PullRefundOrderByShopsAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime); - } - else - { - ret = await PullWXOrderService.Instance.PullOrderByShopIdsAndTimeRangeAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime); - } - if (ret) - { - BaseMsgWindow.ShowInfo("同步任务已发送,等待同步完成\n任务详情查看任务列表"); - } - else - { - BaseMsgWindow.ShowError("同步任务失败"); - } - } - - - } - catch (Exception ex) - { - BaseMsgWindow.ShowError("同步订单失败\n" + ex.Message, "Error"); - } - //var service = PullWXOrderService.Instance; - //foreach (var item in list) - //{ - // await service.PullOrderDetailAsync(item.shop_id, item.order_id); - //} - } - - private DialogWindow _taskDialog; - public ICommand ShowTaskDialogViewCommand => new DelegateCommand(ShowTaskDialogView); - - private void ShowTaskDialogView() - { - if (_taskDialog == null) - { - _taskDialog = new DialogWindow() - { - Title = "任务列表", - Content = new OrderTaskDialogView(), - - }; - _taskDialog.Closed += (s, e) => _taskDialog = null; - } - _taskDialog.Show(); - } } public class OrderFilterConfig diff --git a/29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml b/29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml new file mode 100644 index 0000000..a79a1bf --- /dev/null +++ b/29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml @@ -0,0 +1,868 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +public class CookieItem +{ + public string Name { get; set; } + public string Value { get; set; } + public string Domain { get; set; } + public string Path { get; set; } + public bool Secure { get; set; } + public bool HttpOnly { get; set; } + public DateTime? Expires { get; set; } + public DateTime? Creation { get; set; } + public DateTime? LastAccess { get; set; } + public int SameSite { get; set; } + public int Priority { get; set; } +} +/// +/// Cookie转换帮助类 +/// +public static class CookieConvertHelper +{ + /// + /// 将CookieItem列表转换为Cookie字符串 + /// + /// Cookie对象列表 + /// Cookie字符串,格式:name1=value1; name2=value2 + public static string ToCookieString(List cookieItems) + { + if (cookieItems == null || cookieItems.Count == 0) + return string.Empty; + + var builder = new StringBuilder(); + for (int i = 0; i < cookieItems.Count; i++) + { + var item = cookieItems[i]; + if (!string.IsNullOrEmpty(item.Name) && item.Value != null) + { + builder.Append($"{item.Name}={item.Value}"); + if (i < cookieItems.Count - 1) + builder.Append("; "); + } + } + return builder.ToString(); + } + + /// + /// 将单个CookieItem转换为Cookie字符串 + /// + /// Cookie对象 + /// Cookie字符串,格式:name=value + public static string ToCookieString(CookieItem cookieItem) + { + if (cookieItem == null || string.IsNullOrEmpty(cookieItem.Name)) + return string.Empty; + + return $"{cookieItem.Name}={cookieItem.Value}"; + } + + /// + /// 将CookieItem列表转换为字典 + /// + /// Cookie对象列表 + /// Cookie字典 + public static Dictionary ToDictionary(List cookieItems) + { + var dict = new Dictionary(); + if (cookieItems == null || cookieItems.Count == 0) + return dict; + + foreach (var item in cookieItems) + { + if (!string.IsNullOrEmpty(item.Name) && !dict.ContainsKey(item.Name)) + { + dict.Add(item.Name, item.Value); + } + } + return dict; + } + + /// + /// 将CookieItem列表转换为System.Net.CookieCollection + /// + /// Cookie对象列表 + /// 域名 + /// CookieCollection + public static System.Net.CookieCollection ToCookieCollection(List cookieItems, string domain = "mobile.yangkeduo.com") + { + var cookieCollection = new System.Net.CookieCollection(); + if (cookieItems == null || cookieItems.Count == 0) + return cookieCollection; + + foreach (var item in cookieItems) + { + if (string.IsNullOrEmpty(item.Name)) + continue; + + var cookie = new System.Net.Cookie + { + Name = item.Name, + Value = item.Value ?? string.Empty, + Domain = string.IsNullOrEmpty(item.Domain) ? domain : item.Domain, + Path = string.IsNullOrEmpty(item.Path) ? "/" : item.Path, + Secure = item.Secure, + HttpOnly = item.HttpOnly, + Expires = item.Expires ?? DateTime.MinValue + }; + cookieCollection.Add(cookie); + } + return cookieCollection; + } + + /// + /// 从JSON数组字符串转换为Cookie字符串 + /// + /// JSON数组字符串 + /// Cookie字符串 + public static string FromJsonToCookieString(string cookie) + { + try + { + var cookieItems = Newtonsoft.Json.JsonConvert.DeserializeObject>(cookie); + return ToCookieString(cookieItems); + } + catch + { + return string.Empty; + } + } + + /// + /// 从JSON数组字符串转换为字典 + /// + /// JSON数组字符串 + /// Cookie字典 + public static Dictionary FromJsonToDictionary(string jsonArray) + { + try + { + var cookieItems = Newtonsoft.Json.JsonConvert.DeserializeObject>(jsonArray); + return ToDictionary(cookieItems); + } + catch + { + return new Dictionary(); + } + } +} diff --git a/WxOrder.Framework/Helpers/HttpHelper.cs b/WxOrder.Framework/Helpers/HttpHelper.cs new file mode 100644 index 0000000..253ee63 --- /dev/null +++ b/WxOrder.Framework/Helpers/HttpHelper.cs @@ -0,0 +1,839 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace WxOrder.Framework.Helpers +{ + + /// + /// Http连接操作帮助类 + /// + public class HttpHelper + { + public static HttpResult Get(string Url) + { + return new HttpHelper().GetHtml(new HttpItem + { + URL = Url, + Method = MethodType.Get, + }); + } + + public static HttpResult Post(string Url, string PostData) + { + return new HttpHelper().GetHtml(new HttpItem + { + URL = Url, + Method = MethodType.Get, + Postdata = PostData, + PostDataType = PostDataType.String + }); + } + public static HttpResult Request(HttpItem item) + { + return new HttpHelper().GetHtml(item); + } + /// + /// 下载文件 + /// + /// + /// + public static byte[] DownloadFile(string address) + { + HttpItem item = new HttpItem(); + item.URL = address; + item.Timeout = 3000; + item.ResultType = ResultType.Byte; + HttpHelper http = new HttpHelper(); + HttpResult result = http.GetHtml(item); + if (result.StatusCode == HttpStatusCode.OK) + { + return result.ResultByte; + } + else + { + throw new HttpListenerException((int)result.StatusCode, result.StatusDescription); + } + } + + /// + /// 下载文件 + /// + /// + /// + public static bool DownloadFile(string address, string fileName) + { + HttpItem item = new HttpItem(); + item.URL = address; + item.Timeout = 3000; + item.ResultType = ResultType.Byte; + HttpHelper http = new HttpHelper(); + HttpResult result = http.GetHtml(item); + if (result.StatusCode == HttpStatusCode.OK) + { + FileStream fs = new FileStream(fileName, FileMode.CreateNew); + BinaryWriter bw = new BinaryWriter(fs); + bw.Write(result.ResultByte); + fs.Close(); + bw.Close(); + } + else + { + throw new HttpListenerException((int)result.StatusCode, result.StatusDescription); + } + + return true; + } + + #region 预定义方法或者变更 + //默认的编码 + private Encoding encoding = Encoding.Default; + //Post数据编码 + private Encoding postencoding = Encoding.Default; + //HttpWebRequest对象用来发起请求 + private HttpWebRequest request = null; + //获取影响流的数据对象 + private HttpWebResponse response = null; + /// + /// 根据相传入的数据,得到相应页面数据 + /// + /// 参数类对象 + /// 返回HttpResult类型 + public HttpResult GetHtml(HttpItem item) + { + //返回参数 + HttpResult result = new HttpResult(); + result.item = item; + try + { + //准备参数 + SetRequest(item); + } + catch (Exception ex) + { + result = new HttpResult(); + result.Cookie = string.Empty; + result.Header = null; + result.Html = ex.Message; + result.StatusDescription = "配置参数时出错:" + ex.Message; + return result; + } + try + { + #region 得到请求的response + using (response = (HttpWebResponse)request.GetResponse()) + { + result.StatusCode = response.StatusCode; + result.StatusDescription = response.StatusDescription; + result.Header = response.Headers; + if (response.Cookies != null) result.CookieCollection = response.Cookies; + if (response.Headers["set-cookie"] != null) result.Cookie = response.Headers["set-cookie"]; + MemoryStream _stream = new MemoryStream(); + //GZIIP处理 + if (response.ContentEncoding != null && response.ContentEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase)) + { + //开始读取流并设置编码方式 + //new GZipStream(response.GetResponseStream(), CompressionMode.Decompress).CopyTo(_stream, 10240); + //.net4.0以下写法 + _stream = GetMemoryStream(new GZipStream(response.GetResponseStream(), CompressionMode.Decompress)); + + } + else + { + //开始读取流并设置编码方式 + //response.GetResponseStream().CopyTo(_stream, 10240); + //.net4.0以下写法 + _stream = GetMemoryStream(response.GetResponseStream()); + } + //获取Byte + byte[] ResponseByte = _stream.ToArray(); + _stream.Close(); + if (ResponseByte != null & ResponseByte.Length > 0) + { + //是否返回Byte类型数据 + if (item.ResultType == ResultType.Byte) result.ResultByte = ResponseByte; + //从这里开始我们要无视编码了 + if (encoding == null) + { + Match meta = Regex.Match(Encoding.Default.GetString(ResponseByte), " 1) ? meta.Groups[2].Value.ToLower().Trim() : string.Empty; + if (c.Length > 2) + { + try + { + if (c.IndexOf(" ") > 0) c = c.Substring(0, c.IndexOf(" ")); + encoding = Encoding.GetEncoding(c.Replace("\"", "").Replace("'", "").Replace(";", "").Replace("iso-8859-1", "gbk").Trim()); + } + catch + { + if (string.IsNullOrEmpty(response.CharacterSet)) encoding = Encoding.UTF8; + else encoding = Encoding.GetEncoding(response.CharacterSet); + } + } + else + { + if (string.IsNullOrEmpty(response.CharacterSet)) encoding = Encoding.UTF8; + else encoding = Encoding.GetEncoding(response.CharacterSet); + } + } + //得到返回的HTML + result.Html = encoding.GetString(ResponseByte); + } + else + { + //得到返回的HTML + result.Html = "本次请求并未返回任何数据"; + } + } + #endregion + } + catch (WebException ex) + { + //这里是在发生异常时返回的错误信息 + response = (HttpWebResponse)ex.Response; + result.Html = ex.Message; + if (response != null) + { + result.StatusCode = response.StatusCode; + result.StatusDescription = response.StatusDescription; + } + } + catch (Exception ex) + { + result.Html = ex.Message; + } + if (item.IsToLower) result.Html = result.Html.ToLower(); + return result; + } + /// + /// 4.0以下.net版本取数据使用 + /// + /// 流 + private MemoryStream GetMemoryStream(Stream streamResponse) + { + MemoryStream _stream = new MemoryStream(); + int Length = 256; + Byte[] buffer = new Byte[Length]; + int bytesRead = streamResponse.Read(buffer, 0, Length); + while (bytesRead > 0) + { + _stream.Write(buffer, 0, bytesRead); + bytesRead = streamResponse.Read(buffer, 0, Length); + } + return _stream; + } + /// + /// 为请求准备参数 + /// + ///参数列表 + private void SetRequest(HttpItem item) + + { + // 验证证书 + SetCer(item); + //设置Header参数 + if (item.Header != null && item.Header.Count > 0) foreach (string key in item.Header.AllKeys) + { + request.Headers.Add(key, item.Header[key]); + } + // 设置代理 + SetProxy(item); + if (item.ProtocolVersion != null) request.ProtocolVersion = item.ProtocolVersion; + request.ServicePoint.Expect100Continue = item.Expect100Continue; + //请求方式Get或者Post + request.Method = item.Method.ToString(); + request.Timeout = item.Timeout; + request.KeepAlive = item.KeepAlive; + request.ReadWriteTimeout = item.ReadWriteTimeout; + //Accept + request.Accept = item.Accept; + //ContentType返回类型 + request.ContentType = item.ContentType; + //UserAgent客户端的访问类型,包括浏览器版本和操作系统信息 + request.UserAgent = item.UserAgent; + // 编码 + encoding = item.Encoding; + //设置安全凭证 + request.Credentials = item.ICredentials; + //设置Cookie + SetCookie(item); + //来源地址 + request.Referer = item.Referer; + //是否执行跳转功能 + request.AllowAutoRedirect = item.Allowautoredirect; + //设置Post数据 + SetPostData(item); + //设置最大连接 + if (item.Connectionlimit > 0) request.ServicePoint.ConnectionLimit = item.Connectionlimit; + } + /// + /// 设置证书 + /// + /// + private void SetCer(HttpItem item) + { + if (!string.IsNullOrEmpty(item.CerPath)) + { + //这一句一定要写在创建连接的前面。使用回调的方法进行证书验证。 + ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult); + //初始化对像,并设置请求的URL地址 + request = (HttpWebRequest)WebRequest.Create(item.URL); + SetCerList(item); + //将证书添加到请求里 + request.ClientCertificates.Add(new X509Certificate(item.CerPath)); + } + else + { + //初始化对像,并设置请求的URL地址 + request = (HttpWebRequest)WebRequest.Create(item.URL); + SetCerList(item); + } + } + /// + /// 设置多个证书 + /// + /// + private void SetCerList(HttpItem item) + { + if (item.ClentCertificates != null && item.ClentCertificates.Count > 0) + { + foreach (X509Certificate c in item.ClentCertificates) + { + request.ClientCertificates.Add(c); + } + } + } + /// + /// 设置Cookie + /// + /// Http参数 + private void SetCookie(HttpItem item) + { + if (!string.IsNullOrEmpty(item.Cookie)) request.Headers[HttpRequestHeader.Cookie] = item.Cookie; + //设置CookieCollection + if (item.ResultCookieType == ResultCookieType.CookieCollection) + { + request.CookieContainer = new CookieContainer(); + if (item.CookieCollection != null && item.CookieCollection.Count > 0) + request.CookieContainer.Add(item.CookieCollection); + } + } + /// + /// 设置Post数据 + /// + /// Http参数 + private void SetPostData(HttpItem item) + { + //验证在得到结果时是否有传入数据 + if (request.Method.Trim().ToLower().Contains("post")) + { + if (item.PostEncoding != null) + { + postencoding = item.PostEncoding; + } + byte[] buffer = null; + //写入Byte类型 + if (item.PostDataType == PostDataType.Byte && item.PostdataByte != null && item.PostdataByte.Length > 0) + { + //验证在得到结果时是否有传入数据 + buffer = item.PostdataByte; + }//写入文件 + else if (item.PostDataType == PostDataType.FilePath && !string.IsNullOrEmpty(item.Postdata)) + { + StreamReader r = new StreamReader(item.Postdata, postencoding); + buffer = postencoding.GetBytes(r.ReadToEnd()); + r.Close(); + } //写入字符串 + else if (!string.IsNullOrEmpty(item.Postdata)) + { + buffer = postencoding.GetBytes(item.Postdata); + } + if (buffer != null) + { + request.ContentLength = buffer.Length; + request.GetRequestStream().Write(buffer, 0, buffer.Length); + } + } + } + /// + /// 设置代理 + /// + /// 参数对象 + private void SetProxy(HttpItem item) + { + bool isIeProxy = item.ProxyIp.ToLower().Contains("ieproxy"); + if (!string.IsNullOrEmpty(item.ProxyIp) && !isIeProxy) + { + //设置代理服务器 + if (item.ProxyIp.Contains(":")) + { + string[] plist = item.ProxyIp.Split(':'); + WebProxy myProxy = new WebProxy(plist[0].Trim(),int.Parse(plist[1].Trim())); + //建议连接 + myProxy.Credentials = new NetworkCredential(item.ProxyUserName, item.ProxyPwd); + //给当前请求对象 + request.Proxy = myProxy; + } + else + { + WebProxy myProxy = new WebProxy(item.ProxyIp, false); + //建议连接 + myProxy.Credentials = new NetworkCredential(item.ProxyUserName, item.ProxyPwd); + //给当前请求对象 + request.Proxy = myProxy; + } + } + else if (isIeProxy) + { + //设置为IE代理 + } + else + { + request.Proxy = item.WebProxy; + } + } + /// + /// 回调验证证书问题 + /// + /// 流对象 + /// 证书 + /// X509Chain + /// SslPolicyErrors + /// bool + public bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { return true; } + #endregion + } + /// + /// Http请求参考类 + /// + public class HttpItem + { + string _URL = string.Empty; + /// + /// 请求URL必须填写 + /// + public string URL + { + get { return _URL; } + set { _URL = value; } + } + MethodType _Method = MethodType.Get; + /// + /// 请求方式默认为GET方式,当为POST方式时必须设置Postdata的值 + public MethodType Method + /// + { + get { return _Method; } + set + { + _Method = value; + if (_Method == MethodType.Post) + { + _ContentType = "application/x-www-form-urlencoded"; + } + } + } + int _Timeout = 100000; + /// + /// 默认请求超时时间 + /// + public int Timeout + { + get { return _Timeout; } + set { _Timeout = value; } + } + int _ReadWriteTimeout = 30000; + /// + /// 默认写入Post数据超时间 + /// + public int ReadWriteTimeout + { + get { return _ReadWriteTimeout; } + set { _ReadWriteTimeout = value; } + } + Boolean _KeepAlive = true; + /// + /// 获取或设置一个值,该值指示是否与 Internet 资源建立持久性连接默认为true。 + /// + public Boolean KeepAlive + { + get { return _KeepAlive; } + set { _KeepAlive = value; } + } + string _Accept = "text/html, application/xhtml+xml, */*"; + /// + /// 请求标头值 默认为text/html, application/xhtml+xml, */* + /// + public string Accept + { + get { return _Accept; } + set { _Accept = value; } + } + string _ContentType = "text/html"; + /// + /// 请求返回类型默认 text/html + /// + public string ContentType + { + get { return _ContentType; } + set { _ContentType = value; } + } + string _UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"; + /// + /// 客户端访问信息默认Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) + /// + public string UserAgent + { + get { return _UserAgent; } + set { _UserAgent = value; } + } + Encoding _Encoding = null; + /// + /// 返回数据编码默认为NUll,可以自动识别,一般为utf-8,gbk,gb2312 + /// + public Encoding Encoding + { + get { return _Encoding; } + set { _Encoding = value; } + } + private PostDataType _PostDataType = PostDataType.String; + /// + /// Post的数据类型 + /// + public PostDataType PostDataType + { + get { return _PostDataType; } + set { _PostDataType = value; } + } + string _Postdata = string.Empty; + /// + /// Post请求时要发送的字符串Post数据 + /// + public string Postdata + { + get { return _Postdata; } + set { _Postdata = value; } + } + private byte[] _PostdataByte = null; + /// + /// Post请求时要发送的Byte类型的Post数据 + /// + public byte[] PostdataByte + { + get { return _PostdataByte; } + set { _PostdataByte = value; } + } + private WebProxy _WebProxy; + /// + /// 设置代理对象,不想使用IE默认配置就设置为Null,而且不要设置ProxyIp + /// + public WebProxy WebProxy + { + get { return _WebProxy; } + set { _WebProxy = value; } + } + + CookieCollection cookiecollection = null; + /// + /// Cookie对象集合 + /// + public CookieCollection CookieCollection + { + get { return cookiecollection; } + set { cookiecollection = value; } + } + string _Cookie = string.Empty; + /// + /// 请求时的Cookie + /// + public string Cookie + { + get { return _Cookie; } + set { _Cookie = value; } + } + string _Referer = string.Empty; + /// + /// 来源地址,上次访问地址 + /// + public string Referer + { + get { return _Referer; } + set { _Referer = value; } + } + string _CerPath = string.Empty; + /// + /// 证书绝对路径 + /// + public string CerPath + { + get { return _CerPath; } + set { _CerPath = value; } + } + private Boolean isToLower = false; + /// + /// 是否设置为全文小写,默认为不转化 + /// + public Boolean IsToLower + { + get { return isToLower; } + set { isToLower = value; } + } + private Boolean allowautoredirect = true; + /// + /// 支持跳转页面,查询结果将是跳转后的页面 + /// + public Boolean Allowautoredirect + { + get { return allowautoredirect; } + set { allowautoredirect = value; } + } + private int connectionlimit = 1024; + /// + /// 最大连接数 + /// + public int Connectionlimit + { + get { return connectionlimit; } + set { connectionlimit = value; } + } + private string proxyusername = string.Empty; + /// + /// 代理Proxy 服务器用户名 + /// + public string ProxyUserName + { + get { return proxyusername; } + set { proxyusername = value; } + } + private string proxypwd = string.Empty; + /// + /// 代理 服务器密码 + /// + public string ProxyPwd + { + get { return proxypwd; } + set { proxypwd = value; } + } + private string proxyip = string.Empty; + /// + /// 代理 服务IP ,如果要使用IE代理就设置为ieproxy + /// + public string ProxyIp + { + get { return proxyip; } + set { proxyip = value; } + } + private ResultType resulttype = ResultType.String; + /// + /// 设置返回类型String和Byte + /// + public ResultType ResultType + { + get { return resulttype; } + set { resulttype = value; } + } + private WebHeaderCollection header = new WebHeaderCollection(); + /// + /// header对象 + /// + public WebHeaderCollection Header + { + get { return header; } + set { header = value; } + } + + private Version _ProtocolVersion; + + /// + // 获取或设置用于请求的 HTTP 版本。返回结果:用于请求的 HTTP 版本。默认为 System.Net.HttpVersion.Version11。 + /// + public Version ProtocolVersion + { + get { return _ProtocolVersion; } + set { _ProtocolVersion = value; } + } + private Boolean _expect100continue = true; + /// + /// 获取或设置一个 System.Boolean 值,该值确定是否使用 100-Continue 行为。如果 POST 请求需要 100-Continue 响应,则为 true;否则为 false。默认值为 true。 + /// + public Boolean Expect100Continue + { + get { return _expect100continue; } + set { _expect100continue = value; } + } + private X509CertificateCollection _ClentCertificates; + /// + /// 设置509证书集合 + /// + public X509CertificateCollection ClentCertificates + { + get { return _ClentCertificates; } + set { _ClentCertificates = value; } + } + private Encoding _PostEncoding; + /// + /// 设置或获取Post参数编码,默认的为Default编码 + /// + public Encoding PostEncoding + { + get { return _PostEncoding; } + set { _PostEncoding = value; } + } + private ResultCookieType _ResultCookieType = ResultCookieType.String; + /// + /// Cookie返回类型,默认的是只返回字符串类型 + /// + public ResultCookieType ResultCookieType + { + get { return _ResultCookieType; } + set { _ResultCookieType = value; } + } + + private ICredentials _ICredentials = CredentialCache.DefaultCredentials; + /// + /// 获取或设置请求的身份验证信息。 + /// + public ICredentials ICredentials + { + get { return _ICredentials; } + set { _ICredentials = value; } + } + + } + /// + /// Http返回参数类 + /// + public class HttpResult + { + private string _Cookie; + /// + /// Http请求返回的Cookie + /// + public string Cookie + { + get { return _Cookie; } + set { _Cookie = value; } + } + + private CookieCollection _CookieCollection; + /// + /// Cookie对象集合 + /// + public CookieCollection CookieCollection + { + get { return _CookieCollection; } + set { _CookieCollection = value; } + } + private string _Html; + /// + /// 返回的String类型数据 只有ResultType.String时才返回数据,其它情况为空 + /// + public string Html + { + get { return _Html; } + set { _Html = value; } + } + private byte[] _ResultByte; + /// + /// 返回的Byte数组 只有ResultType.Byte时才返回数据,其它情况为空 + /// + public byte[] ResultByte + { + get { return _ResultByte; } + set { _ResultByte = value; } + } + + private WebHeaderCollection _Header; + /// + /// header对象 + /// + public WebHeaderCollection Header + { + get { return _Header; } + set { _Header = value; } + } + + private string _StatusDescription; + /// + /// 返回状态说明 + /// + public string StatusDescription + { + get { return _StatusDescription; } + set { _StatusDescription = value; } + } + private HttpStatusCode _StatusCode; + /// + /// 返回状态码,默认为OK + /// + public HttpStatusCode StatusCode + { + get { return _StatusCode; } + set { _StatusCode = value; } + } + /// + /// HttpItem类,用于存储实际执行所用到的HttpItem对象 + /// + public HttpItem item { get; set; } + } + /// + /// 返回类型 + /// + public enum ResultType + { + /// + /// 表示只返回字符串 只有Html有数据 + /// + String, + /// + /// 表示返回字符串和字节流 ResultByte和Html都有数据返回 + /// + Byte + } + /// + /// Post的数据格式默认为string + /// + public enum PostDataType + { + /// + /// 字符串类型,这时编码Encoding可不设置 + /// + String, + /// + /// Byte类型,需要设置PostdataByte参数的值编码Encoding可设置为空 + /// + Byte, + /// + /// 传文件,Postdata必须设置为文件的绝对路径,必须设置Encoding的值 + /// + FilePath + } + /// + /// Cookie返回类型 + /// + public enum ResultCookieType + { + /// + /// 只返回字符串类型的Cookie + /// + String, + /// + /// CookieCollection格式的Cookie集合同时也返回String类型的cookie + /// + CookieCollection + } + public enum MethodType + { + Get, + Post + } +} + diff --git a/WxOrder.Framework/Helpers/ResourceHelper.cs b/WxOrder.Framework/Helpers/ResourceHelper.cs new file mode 100644 index 0000000..f00524d --- /dev/null +++ b/WxOrder.Framework/Helpers/ResourceHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Resources; + +namespace WxOrder.Framework.Helpers; + +/// +/// WPF 资源读取帮助类 +/// 支持 Resource、Embedded Resource、Content 三种方式 +/// +public static class ResourceHelper +{ + /// + /// 从 WPF 应用程序资源中读取文本内容(Build Action = Resource) + /// + /// 相对路径,如 "Ctrls/PDDInvoice/Resources/invoice.js" + /// 程序集名称,可选,默认使用当前程序集 + /// 文件内容字符串 + public static string GetResourceText(string relativePath, string assemblyName = null) + { + try + { + // 构建 pack URI + string uriString; + if (string.IsNullOrEmpty(assemblyName)) + { + // 当前程序集 + uriString = $"pack://application:,,,/{relativePath}"; + } + else + { + // 指定程序集 + uriString = $"pack://application:,,,/{assemblyName};component/{relativePath}"; + } + + var uri = new Uri(uriString, UriKind.Absolute); + StreamResourceInfo resourceInfo = Application.GetResourceStream(uri); + + if (resourceInfo == null) + { + throw new FileNotFoundException($"资源未找到: {relativePath}"); + } + + using (var reader = new StreamReader(resourceInfo.Stream)) + { + return reader.ReadToEnd(); + } + } + catch (Exception ex) + { + return ""; + } + } + + /// + /// 从嵌入资源中读取文本内容(Build Action = Embedded Resource) + /// + /// 完整资源名称,如 "YourNamespace.Ctrls.PDDInvoice.Resources.invoice.js" + /// 程序集,默认使用当前程序集 + /// 文件内容字符串 + public static string GetEmbeddedResourceText(string resourceName, Assembly assembly = null) + { + try + { + assembly ??= Assembly.GetExecutingAssembly(); + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + throw new FileNotFoundException($"嵌入资源未找到: {resourceName}"); + } + + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"读取嵌入资源失败: {resourceName}", ex); + } + } + + /// + /// 从嵌入资源中读取文本内容,自动查找资源名称(支持部分匹配) + /// + /// 文件名,如 "invoice.js" + /// 程序集,默认使用当前程序集 + /// 文件内容字符串 + public static string GetEmbeddedResourceTextByFileName(string fileName, Assembly assembly = null) + { + assembly ??= Assembly.GetExecutingAssembly(); + + // 查找匹配的资源名称 + var resourceNames = assembly.GetManifestResourceNames(); + var matchedName = Array.Find(resourceNames, name => name.EndsWith(fileName)); + + if (string.IsNullOrEmpty(matchedName)) + { + throw new FileNotFoundException($"未找到名为 {fileName} 的嵌入资源"); + } + + return GetEmbeddedResourceText(matchedName, assembly); + } + + /// + /// 从文件系统读取文本内容(Build Action = Content) + /// + /// 文件物理路径或相对路径 + /// 文件内容字符串 + public static string GetFileText(string filePath) + { + try + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"文件未找到: {filePath}"); + } + + return File.ReadAllText(filePath, System.Text.Encoding.UTF8); + } + catch (Exception ex) + { + throw new InvalidOperationException($"读取文件失败: {filePath}", ex); + } + } + + /// + /// 读取 JS 资源(专门针对 JS 文件的便捷方法) + /// + /// JS 文件相对路径 + /// JS 文件内容 + public static string GetJavaScriptResource(string relativePath) + { + return GetResourceText(relativePath); + } +} diff --git a/WxOrder.Framework/PDDAPI/PddApi.cs b/WxOrder.Framework/PDDAPI/PddApi.cs new file mode 100644 index 0000000..91190b5 --- /dev/null +++ b/WxOrder.Framework/PDDAPI/PddApi.cs @@ -0,0 +1,249 @@ +using System; +using System.Net; +using System.Text; + +namespace WxOrder.Framework.Helpers +{ + /// + /// 拼多多API请求类 + /// + public class PddApi + { + private const string BASE_URL = "https://mobile.yangkeduo.com"; + + /// + /// 获取发票详情页 + /// + /// 订单号 + /// 用户Cookie + /// 返回页面HTML内容 + public static string GetReturnCompensation(string orderSn, string cookie) + { + string url = $"{BASE_URL}/transac_return_compensation.html?_t_timestamp=transac_modify_invoice&order_sn={orderSn}"; + + var item = new HttpItem + { + URL = url, + Method = MethodType.Get, + Timeout = 30000, + ReadWriteTimeout = 30000, + KeepAlive = true, + Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + ContentType = "text/html", + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", + Cookie = cookie, + Referer = BASE_URL, + Allowautoredirect = true, + Encoding = Encoding.UTF8, + ResultType = ResultType.String + }; + + // 添加自定义Header + item.Header.Add("sec-ch-ua", "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\""); + item.Header.Add("sec-ch-ua-mobile", "?0"); + item.Header.Add("sec-ch-ua-platform", "\"Windows\""); + item.Header.Add("upgrade-insecure-requests", "1"); + item.Header.Add("sec-fetch-site", "none"); + item.Header.Add("sec-fetch-mode", "navigate"); + item.Header.Add("sec-fetch-user", "?1"); + item.Header.Add("sec-fetch-dest", "document"); + item.Header.Add("priority", "u=0, i"); + item.ProxyIp = "127.0.0.1:8080"; + var result = new HttpHelper().GetHtml(item); + + if (result.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"请求失败: {result.StatusDescription}, 状态码: {result.StatusCode}"); + } + + return result.Html; + } + + /// + /// 异步获取退货补偿页面信息 + /// + /// 订单号 + /// 用户Cookie + /// 返回页面HTML内容 + public static async System.Threading.Tasks.Task GetReturnCompensationAsync(string orderSn, string cookie) + { + return await System.Threading.Tasks.Task.Run(() => GetReturnCompensation(orderSn, cookie)); + } + + /// + /// 获取退货补偿页面信息(带完整Cookie字符串) + /// + /// 订单号 + /// PDD访问令牌 + /// 用户ID + /// 用户UIN + /// 额外Cookie键值对 + /// 返回页面HTML内容 + public static string GetReturnCompensationWithCookies( + string orderSn, + string accessToken, + string userId, + string userUin, + System.Collections.Generic.Dictionary extraCookies = null) + { + // 构建Cookie字符串 + var cookieBuilder = new StringBuilder(); + cookieBuilder.Append($"PDDAccessToken={accessToken}; "); + cookieBuilder.Append($"pdd_user_id={userId}; "); + cookieBuilder.Append($"pdd_user_uin={userUin}; "); + + // 添加额外Cookie + if (extraCookies != null) + { + foreach (var kv in extraCookies) + { + cookieBuilder.Append($"{kv.Key}={kv.Value}; "); + } + } + + // 添加一些常用的固定Cookie + cookieBuilder.Append("api_uid=CiNLVGqFh3IfyACAene3Ag==; "); + cookieBuilder.Append("webp=1; "); + cookieBuilder.Append("dilx=3EDqgJtFtp4E9OYvXLozM; "); + + string cookie = cookieBuilder.ToString().TrimEnd(' ', ';'); + + return GetReturnCompensation(orderSn, cookie); + } + + /// + /// 构建完整的拼多多请求Cookie + /// + /// Cookie键值对 + /// Cookie字符串 + public static string BuildCookieString(System.Collections.Generic.Dictionary cookies) + { + if (cookies == null || cookies.Count == 0) + return string.Empty; + + var builder = new StringBuilder(); + foreach (var kv in cookies) + { + builder.Append($"{kv.Key}={kv.Value}; "); + } + return builder.ToString().TrimEnd(' ', ';'); + } + + /// + /// 通用拼多多请求方法 + /// + /// 请求路径 + /// 查询参数 + /// Cookie字符串 + /// 请求方法 + /// POST数据 + /// 返回结果 + public static string Request( + string path, + System.Collections.Generic.Dictionary queryParams = null, + string cookie = null, + MethodType method = MethodType.Get, + string postData = null) + { + // 构建完整URL + var urlBuilder = new StringBuilder(BASE_URL); + urlBuilder.Append(path); + + if (queryParams != null && queryParams.Count > 0) + { + urlBuilder.Append('?'); + int index = 0; + foreach (var kv in queryParams) + { + if (index > 0) urlBuilder.Append('&'); + urlBuilder.Append($"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"); + index++; + } + } + + var item = new HttpItem + { + URL = urlBuilder.ToString(), + Method = method, + Timeout = 30000, + ReadWriteTimeout = 30000, + KeepAlive = true, + Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + ContentType = "application/x-www-form-urlencoded", + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", + Cookie = cookie, + Referer = BASE_URL, + Allowautoredirect = true, + Encoding = Encoding.UTF8, + ResultType = ResultType.String, + Postdata = postData + }; + + // 添加安全相关Header + item.Header.Add("sec-ch-ua", "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\""); + item.Header.Add("sec-ch-ua-mobile", "?0"); + item.Header.Add("sec-ch-ua-platform", "\"Windows\""); + item.Header.Add("upgrade-insecure-requests", "1"); + item.Header.Add("sec-fetch-site", "none"); + item.Header.Add("sec-fetch-mode", "navigate"); + item.Header.Add("sec-fetch-dest", "document"); + + var result = new HttpHelper().GetHtml(item); + + if (result.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"请求失败: {result.StatusDescription}, 状态码: {result.StatusCode}"); + } + + return result.Html; + } + } + + /// + /// 拼多多Cookie构建帮助类 + /// + public static class PddCookieHelper + { + /// + /// 从各个Cookie片段构建完整Cookie + /// + public static string BuildCookie(PddCookieInfo info) + { + var builder = new StringBuilder(); + + if (!string.IsNullOrEmpty(info.ApiUid)) + builder.Append($"api_uid={info.ApiUid}; "); + if (!string.IsNullOrEmpty(info.NanoFp)) + builder.Append($"_nano_fp={info.NanoFp}; "); + if (!string.IsNullOrEmpty(info.AccessToken)) + builder.Append($"PDDAccessToken={info.AccessToken}; "); + if (!string.IsNullOrEmpty(info.UserId)) + builder.Append($"pdd_user_id={info.UserId}; "); + if (!string.IsNullOrEmpty(info.UserUin)) + builder.Append($"pdd_user_uin={info.UserUin}; "); + if (!string.IsNullOrEmpty(info.PddVds)) + builder.Append($"pdd_vds={info.PddVds}; "); + + // 添加固定值 + builder.Append("webp=1; "); + builder.Append("dilx=3EDqgJtFtp4E9OYvXLozM; "); + + return builder.ToString().TrimEnd(' ', ';'); + } + } + + /// + /// 拼多多Cookie信息类 + /// + public class PddCookieInfo + { + public string ApiUid { get; set; } = "CiNLVGqFh3IfyACAene3Ag=="; + public string NanoFp { get; set; } = "XpmJnqgqnpCynqEYnC_vqOqYD6xToHisZXFzP~Qy"; + public string AccessToken { get; set; } + public string UserId { get; set; } + public string UserUin { get; set; } + public string PddVds { get; set; } + public string Jrpl { get; set; } = "m4Tc1seqEgg0YoYo7U4GeUxzylfpG8W7"; + public string Njrpl { get; set; } = "m4Tc1seqEgg0YoYo7U4GeUxzylfpG8W7"; + } +} \ No newline at end of file diff --git a/WxOrder.Framework/Resources/Fonts/FontGeometry.xaml b/WxOrder.Framework/Resources/Fonts/FontGeometry.xaml index 3e738dc..6a80a7f 100644 --- a/WxOrder.Framework/Resources/Fonts/FontGeometry.xaml +++ b/WxOrder.Framework/Resources/Fonts/FontGeometry.xaml @@ -16,6 +16,7 @@ > + @@ -1522,6 +1523,9 @@ + + M170.666667 868.693333l98.346666-94.72a21.333333 21.333333 0 0 1 30.72 1.173334l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 31.829334 0l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 30.72-1.173334L853.333333 868.693333V128H170.666667v740.693333z m-42.666667 49.386667V106.709333C128 94.890667 137.706667 85.333333 149.333333 85.333333h725.333334c11.776 0 21.333333 9.642667 21.333333 21.184v811.541334a21.333333 21.333333 0 0 1-36.117333 16.149333l-118.506667-114.133333-99.349333 111.445333a21.333333 21.333333 0 0 1-31.850667 0L512 821.397333l-98.176 110.122667a21.333333 21.333333 0 0 1-31.850667 0l-99.349333-111.445333-118.485333 114.133333A21.333333 21.333333 0 0 1 128 918.08zM298.666667 320c0-11.776 9.493333-21.333333 21.184-21.333333h341.632c11.690667 0 21.184 9.472 21.184 21.333333 0 11.776-9.493333 21.333333-21.184 21.333333H319.850667A21.205333 21.205333 0 0 1 298.666667 320z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 448z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 576z + M330.666667 298.666667a32 32 0 0 0-32 32v170.666666c0 17.664 14.336 32 32 32h362.666666a32 32 0 0 0 32-32v-170.666666a32 32 0 0 0-32-32h-362.666666z m32 170.666666V362.666667h298.666666V469.333333h-298.666666z M757.333333 896A138.666667 138.666667 0 0 0 896 757.333333V266.666667A138.666667 138.666667 0 0 0 757.333333 128H266.666667A138.666667 138.666667 0 0 0 128 266.666667v490.666666A138.666667 138.666667 0 0 0 266.666667 896h490.666666zM832 266.666667v490.666666a74.666667 74.666667 0 0 1-74.666667 74.666667H266.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V266.666667c0-41.216 33.450667-74.666667 74.666667-74.666667h490.666666c41.216 0 74.666667 33.450667 74.666667 74.666667z diff --git a/WxOrder.Framework/Resources/Fonts/iconfont.css b/WxOrder.Framework/Resources/Fonts/iconfont.css index e152232..60c11ea 100644 --- a/WxOrder.Framework/Resources/Fonts/iconfont.css +++ b/WxOrder.Framework/Resources/Fonts/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 5223831 */ - src: url('iconfont.woff2?t=1786935726171') format('woff2'), - url('iconfont.woff?t=1786935726171') format('woff'), - url('iconfont.ttf?t=1786935726171') format('truetype'); + src: url('iconfont.woff2?t=1787127285848') format('woff2'), + url('iconfont.woff?t=1787127285848') format('woff'), + url('iconfont.ttf?t=1787127285848') format('truetype'); } .iconfont { @@ -13,6 +13,10 @@ -moz-osx-font-smoothing: grayscale; } +.icon-fapiao:before { + content: "\ed35"; +} + .icon-calendar_work_week:before { content: "\e850"; } diff --git a/WxOrder.Framework/Resources/Fonts/iconfont.js b/WxOrder.Framework/Resources/Fonts/iconfont.js index f126599..8badfba 100644 --- a/WxOrder.Framework/Resources/Fonts/iconfont.js +++ b/WxOrder.Framework/Resources/Fonts/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_5223831='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,z,c,m,i=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5223831,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?i(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=o,c=h.document,m=!1,p(),c.onreadystatechange=function(){"complete"==c.readyState&&(c.onreadystatechange=null,t())})}function t(){m||(m=!0,z())}function p(){try{c.documentElement.doScroll("left")}catch(a){return void setTimeout(p,50)}t()}})(window); \ No newline at end of file +window._iconfont_svg_string_5223831='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,z,c,m,i=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5223831,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?i(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=o,c=h.document,m=!1,p(),c.onreadystatechange=function(){"complete"==c.readyState&&(c.onreadystatechange=null,t())})}function t(){m||(m=!0,z())}function p(){try{c.documentElement.doScroll("left")}catch(a){return void setTimeout(p,50)}t()}})(window); \ No newline at end of file diff --git a/WxOrder.Framework/Resources/Fonts/iconfont.json b/WxOrder.Framework/Resources/Fonts/iconfont.json index bb8fb64..13c8940 100644 --- a/WxOrder.Framework/Resources/Fonts/iconfont.json +++ b/WxOrder.Framework/Resources/Fonts/iconfont.json @@ -5,6 +5,13 @@ "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "5068538", + "name": "发票", + "font_class": "fapiao", + "unicode": "ed35", + "unicode_decimal": 60725 + }, { "icon_id": "44033866", "name": "calendar_work_week", diff --git a/WxOrder.Framework/Resources/Fonts/iconfont.ttf b/WxOrder.Framework/Resources/Fonts/iconfont.ttf index 941a5eb177b65d372034b0a57cc58926b1a6003d..30f6476ff50d0401a0a27c0e45a4e2448f6ba497 100644 GIT binary patch delta 24939 zcmY+t3!GJB`?!BUr`(CTP_w2pj%|A5U_hZANrkNz-W{Ak3 zp`!=qKeD#uJ&^{ZL@ru<<*3Ocw)xY47b%=4S=ASf96oqhoALSUId>~`9LWROo2w3{ zyb_v?99=MJs_g%d_9{gVnL29h(7|=a&+8{Lx)J+@qX$pQ&-&Xn`)_qGEPxqNCn|9eMj&tF&Wp$nySgZ3MsUCSiPP;gx1AmK?-$u|v2NL?%UMesPIl9lv}&~9xxPzU9iEbVa7p{7?B&mD-@NOr z2@Tsba&)|B&C#t$`d-^Ngf-dG*N`Uo z5<`x{te+ys;Y$r^hx;3HBEHO!E_i?;XJS@FkskPpjBeQJjRzW{9St%>PabT@P&~wt ztFZ2ajK#wYxegCEL--m)=3=r>k*DxzLl)vO zhP;GzARx;zd7#J}_*z5W#^VfmACDKw=te$ar@)YnSZ4w94W3|#hGwE6JMbh!cH+r~ z{En|Te5c_q#djHQAf913jjx6coQCKg!|5{L zYdBr0BE#uyXBtiie4pWT0GU~Az-c4JhEv~fxQForhMR{UG~Cm8w&50G%@=SlW32;j z1%AYEZ(^=8#jVAU8X5Nig~trH9?v!07g#$4w;4ZfxNZ0e!tNnB zytenA;dv~xV;wu-OR(M)z<-7{FTiiY9~yoO*7Lw`#~&HK9Di*1U0ClY;Qzqu4Zjb6 zY6KEthxYLq@efrfd~SrAc!Lpg@kS#YioY;IWBjENT3{U@ggpF}5!&LfjnD~yV}w)j zW+QaN-)j87rSiF0X9uAd{>}(}@m3=Yz~38T2;OFdk@yEAjKR7x5DM^*Mz|jTWQ3`B zhY>QjQ7ARS3|wY}`>-x6gxR>l2#?`PBh1G;jqn`)*$9jAFGhF;Ye*oh#J?J04gO8z z&n6K69lskvlu;zd#SDl#JTtoBXXN9t#3r`l%)-f$Q`frP$P2FDs5;)?m4Bpl8}kqJW3BU zBG-QD;YQ^8Ej_}BT!y8MjmV``+Qf)l4W$|eh-7qWQzMd;rOk{;UX?aCBDqo8!ia>o z^eE!bMv}%zV+cu?`Di2QQXOL?o$awk(gEigNjp8xNLo+h52-c&AE^W7Hby!bA8(}7 zh<|BYBb|-g8R(tLcTk)Fkx@Q_|0{-ru= zNK0^cBdx&a7)g`mTqCW)J&g1o)~f|l3Dy;Z^eH~yNMGO!jHG$f%Mg!yAAt4Grf{Jl zIarq)L}TB_5Y76F4Iyhw`x-*xm0n_q=8s;cAesXjDi950e?v4Bml?uJmJZPP58xzS zlFJRzS!rxRbYKGw(Z&ZEqQ?gtqW6FyhFpy`7eMl{9s`+xhZ~~d9$`r477AAyG97F3 zfZT(xGNc%5!yp>tYYdr(M;Y=o)?@=&gvS{2G9GJ)hE)dz@+Q7kI*$jG*E}dk^&v>#SyYcmg{Ecrg9Q{kFE(h?+P)ct!oaVtz zhC2vrm_T16mrga@;aIO0aLur;B)DVnG{d#Qg@)^hZ#7(Je4C<;o=)L*!*$2g4R=1i z!*G4@ordd=^>PF^2+uH_#`JE(jlvp2aJsVh8ctWI$Z$Hr%uF`m`r`Wxr=89+oL1EM zgVSsMf4D~|KVZ1W@q>nY2G2I!^Y|gdEy0>Q;9kXAA2Mzgg*k?M2R~}K5AkD$`xMVL z+?RNs;l9P%Ik+G26NW3pPa5tQJl}A8@Kc8S2S2UxCv&}HM@J4m8!s??4g9R(>*D7O z-vBQ({1JGO;hSTfHTYxk3x+=)zi9Xq@M6QCLi|f#GW;3%Wy7C?b;-bAfR`HnV!X`o zm*H0oKNv4J{FQix;YVX#Zt&yrYlfeUUxy4EuY*`=c+Jo^3{QfUzG--Jq;!?xHP)*Q zui<*j@Vew{46m#9w&8W=dWnPA!E2bo(|4xyU3S20Bkvhrz0U9-;r9*yIoA6F_^+_$ z2>9=CiQ#|5S_gau{@C!p;!h0!C)Rt*dgAZ@rSPc{0{+YhRk3ymp%&g?gnD?R5gOtz zjL-ysX@sNjCLT-!JCb6Cf3Y`&;xHVLT{`yg>VVpYUo>R zrQaK2DBfm-tMCs-7>ltH<(;vpg|$^qhG*cq`Y z_C`D!2P3w|(TE)|eUzmP4V_FO%ZR7pY$KkHs~GV-T-Asd;c7;_6f;024#YV|9FA)k z@fuvyh}Ys;Mx2Ng_PQwQo@eW+qi1*@LBi@he8IgN>*}+ES)?B751Mz8G z--z5H%NiJwn_StUMkaEnDr;y&ZZ>6&jL6NROqUfR*L&IFMqH1NFd~;;Sz{w|^^`R+ zB3D4!kwzqm%QPeqNyM^dMkJTYniGFE5=n@%76xIFvZIWoVQFb3U0RJNBwd|jjHEL> z)<`{!KWJOcHG5CGjLZU&BU6( zkRHG~2}qCNGmJD3cQevcT>oWfvIFTke3p@3#G3t(mSSB2NU!2^jHJ18u94oxJ&d#t z_cYQ+Sg#yNpJ81+NMGU$4v>+giPOuFfO{MK)k4ZHG^7UB%ML_iuS*Z2nSQY$nyh^d zA=%0nArh(0#h zkl}cUA$kuOYRI)%^8zF@k%FEGqVXPK$TWPVA$Q=BhTMxakwEUpR~w=+)=oek$D<5+ z29Gx6d8~;CvILLS__Gm>YrY|?u#OPq9X!sE5Ak?IKE(xwXe_TYpX@b)*-fg(i z_#VUQYG&?b15THu$Z$HVnTFF*-e)-NXqMsh++xG&wXV4UZVuLca8KX|4YvT#HrxyN zAtU3KQh3;KuVGCjaI3L42JT(_sNqWRV}|<-&o$g8JkM}juqGk6?N|o_t{gvUxLtU@ z#(zEs{$S@R!|lUQ8{Xq*46pYCodx)s_*ui};^z#1C|+pz#(0t8Tj1vnpNDk?z_-<< ze$nuq@M6QCieEB(H~g~U&&5j&-wW&Nf$xi#8GZnM#qdM$a>I|rD-1sdzX};PzJP)* zJ^1VK>xS2iUTOH-@EeAof!{Q|=Ef?+YkW0q;59^V8J-m`TVr@#skaT!WHV)J*#WO3 z*6SU-cKWX2wc>k*Ux)vP|CsXohTnicF#Ok8?-k&;;u6FEgg-KTCD!^M6Mz33g-;B> z7q2(`e^_rc5F*xOgHR2BZiL!cJBM&E-e`nI_zNQ(iN7>LOT5Vlt?*YG|F5Xr9_z>< zoP@tILRY-m2xsAMjnETsF~WuTJ0tYNI%^15;O~tv3~w{S)%XV^Wb!F&H^Kz`qYqu z`;3@{|21L`)*OL&5Z-UZ`XVgE0pj7<8L=7mMmz=wh4{xd6zD~X9dR;ZXH17mJRN5n zu{*9}#Pe}gBlf}7jMyJnH{u}7V3jxm*D&HJoTl(2L=Nj<=T+fKy#>)>j;^X)bBR+$5l^_$jEtWShB6qg(Lyh<Tu@Sjq%9|LGtS>*(h$L=#Q{vA?BFR^-@qtJJl{YscIZ)oh zNE*kZjHIDxX(V0Fqm861qOpaflRDN&I2al zq-D6fkzT{P8jx1ubB(kX_b}4?xTldm#^)V?=l`EmINwN{ur4JeO`KkaMBLkus`x@f zxN^!bGDKsqmmmn4U9MpO(FE;l2>Dfhi6NRN{WSjlI7wrDsUaFKy?#M-$u))`x@-ds z(V1Uvhz|Y=Lv#QG4be6R8KNF+$Ot^dkc{30h8i*s4>M#E)+7PZcxxq)LVTqmcjA$T z6yd83c>rrpfe_>JYYce;YkwdM@Mw)c8+n19F@`L~nuH)4*L*`(<7*9h7wcd_O7M6? zKEnluXeh5UWDA~P$aXx@;2zEOuQLVNg*D4T{=nB8vJc;2IFF|ot_r@_e~;mG3GX$Wu7buEoX%vX;dEg48BQCYWjH-vY&gBv z?>F3|Sn~qhlUR>I#yv}6w&7mH4;gM5e%NrY<3|kl7S^N!_a4@U!F_}uGu-ERuHnAI z^9=VL*82>&AMq0!f3nh5u%n{__bZ-nxIghzhWi&kZTNtnF??05*$loG)(L^Hho3Wi zL%h)NP4FVaA4UAjpEvw*_yxna!@3&aPsEE2-vz&9_%rd#hVOxw7```NYWPdAt|a)& z@hgTOikCx%jlYV*3d4`ZuNwY3{F>qQ3eYfszZtJIye8rsh9{58-!#1D!z#mTNLL$v zE`H1KtZb%y4Ljg&Ty^ab(HTIUOUojAH1Hc@dy7tUT64EuI1GPb zgr@k*jBYq^G~Q%{*7z$UbiiL5;bg3{fN&b#Y=pD%w?;S*Z!y9}_&Xz9inkhJAl4Ot zkQq*4n-Q+TKN#U!yxj;B@sCEh5&vX_X;@be!X3EO2>0SLBixV6jqnJrFv8=wQsd7i zJj0GIJ%s1+&qi2+e=)+Vc$X1Y;a`oQx$&D3G`<=(2pXc@Mqq`@_ZUG}>JKCQpx6JO z>_E^F?=^yUsxgP475_HEKlpz{M|qzSv+=)1tbzYCVqLu7hz&%T`~l(-nEDm^R!eM7 z!5i^d9E^B8jz&BICnKJMvy6BKrgJ5pgR2ICim{}`vCeAhDgSeg%xph|@Y-HjS6b>;Wx6q3E zM&wRdp-TplyIIAdM&yQ6(a?z8R4N)7k$XbLVMZ*$hZ~VgvqF~}B3D>NV~GDIv2MyQZL-zNPTe!BMrbEA;TtV zB4~m^8iBPFNY~&Kjg*g1GSYSUWFuXVI~)4PtO`v|NQGDj20^dIq0qq(%5FBfW&rHqtA&yOCbU=NL(IMOOyWJGjRIc>ezZ zg`P(G1fOT54fuQ`eTDUsf~1L~%L@)o|kRo$um1_`fc%Ij8hVqi zm}JPWSZ59LC)SJy`4`_{xPYe^t}4FKaGD1<8Ll4I6$950PgQiM35A;tcND(GaL3_k zhHHoQk^*-k*5wA*1>a`4Gx6<)>w%{mt~b8JaF^ga4X1gMxr+@rjr9z}X}s<>oG!Uu zv*2{u?lqjwyvT4mc#ShS9n5`(euJrEmf`fVV#DdBe!r1%k5PEQaPzSy3ApF5o(OI+ ze#mgI;D-&j5rDq-CD!Bw z_Z!v$g4>IqGTeXoX~ReSjN$cuu)y%Ou_iY7gRxEyd?VstvC!~G;zfpUiJv!oEBu1t z+v67ve-hS}0pAtBWcai2%ZBfXml*y+ywvdh@G{7-@mEmLWd%PBFE{+vc!lBf@vDZP zfL}AbX8r4izXfYZz-#ioVfcISn}*jMSY>#^n5kIJ4)}TaEyL?_t}(o>$lHe3S*bgUDC&>eqkg!AzhBlN-F86nf3!d4>;!rvQV1lHAnFbe-*gmHMg5hmdujc^nG z$q0pbhY{|?x{?rzaG4Pvz~vf$Hen7s6-IahR~lgf-f4svu!aG`Qv8b%Uc+0cmGM=Bc`k$XYq zVMgR~t~}g`T#l7T5PvojxvVO6=^=7yR5mdp*rbZ;mDw`RRJgIDML}Fd3 zVS}XMI?718Jk6SV~nJuKh{V(0F5~$ZR9v3saqMT9d2!;6LA|Ob-~9Q z=}fHo0;vbqI*@wf_K;zdE}_uDNSEV|M$$Yu!AMtPO*TlQuyzRPT6~g`Cg78ebOY{e zB+ZFajC3p3M1^!G@vqd;Lb?}sHBvD?%}5X7(~a~PKEp_wADZQm7GRwzr04NjMtT{a zZKUP6yOCDna}L1s|FO6kovRJe7++(^e5}a^@*Ey*$YQJm0eJEE zdj0DxKs1)u8L|~mFytpZ(a?KzElKErx4_rx~t2E;QUpST8SdU9m1bxU=x>hU#0UxM6sP;WR{d8%~$`9>eKM>E#PfXIo@A9q>%U=>Rn5;IxfdM#gEO*l>ES z-*33N_yNN`g&#EBLag}$?j@{sz%9oQ8}1GKh~eJGa}4)Be$;TE;KwxnB%IsGj&=y{ z8$8c&-{Z#(w*x<6xSjY(!~KpmQNjI%b+F*}HPTWxq< z{yzT%z|NoT2`-cA#e_;4;v7QM2 z2V7$KGW?O@f5Cb~0lx=-V)%dXdLuZjeL%>@pBbSB{#@h#IS1<9FM;;!U_0mBboC8UJVmo#0PKxE1d(!d}=yGa2A?S+yVFaDkpGMGu z?KOfn{+AK-_}@nO7i%s+40xXrt71L&FUu2aQTWe@_3(ZpHsmKy#E4C>GvZO$8}T?y z!%A$2qY+QU$%tKWmJ!dy*+%SvGga6??2Q?f5--8kjCeV&Zp5KD$B0+q8b%z8YZ~!7 z%!HJ91Fmhvn{gc@-i{A4GVyK-b&WU+=Nggw`%YFviI3rfjmT|w=OIRX4%as#cf6eq zjL1!E=b=XA#2LZf&HKa2q3a#m5`zEZi0{Y*J4Onmmv$#M%I)ez=1nB;Wu2PoLH3 zTXOobONyd@w$YE;WHW*6-6B<)d9`^WIk`Z_*C-OHSs+rYn@DY3rcFSzr9O=`n9UPn9zS%YNW=Lejo`5Le3E7<@0!zKlRhFx&Jk$}%~$gzne&e7 zCDJlSC31tDH7>NLzfna^v@T$ zj7pcy5gAY@a`{@3fo((v<%tZg0#ikX))pDY@!^|AMwEz*>1rPh=G5 zjb6z!cj}JKOY`3}yxlXLFM<7EKH z?}mHk(Z;<4M2gs%Ih)r6bNCG+PA(qAcMGPAJTOV*!KJ)B;IW5j=%HPF|CpIPLMM;x zBxyu{MHoX?jM=xFICk!4g`USDKIF5gYSuTk-J{Q7EsR*MGS;G8$^;is3Pj!?z;~PJ>%%F0W@(VfM}0&-Zb|$< zSs=20g~+Fi_(3fi-Ox#7V`q^s7|@qg+5}(a@%m&2KZr!-Z)b~aq4Ia_MYggM-z%H= zBJ_5V9~!}Ck?ke?J%hG>TFH-CY~TxP475C#H=JmvlFoNd75RA-Uxvsqvt3mBl}3Nt zDzaNd_R#SkJpM-kfAE+uvUi`z-*obiGKW8W)aOM`Cw^5akGn#?I6g1r{3u@RG!n-j zC|%Y7aoKakRh=%b+9Gk)hl$I6BWI)wpo{ktx7*mb?Q(@Mmhu~=NUdE&VC-B~n#_AYVV**~XLT#sCFJ-hL% zPSm}C@&yaT^`gVxwZ&c76R3YtzPLWLdGX>5-@M)`?h+d4*A+I4yOfjqGm!r4#a-43 zrt=Z5+2Ssz!oX&H<-4W0!8ziFRN=2SG(K#vxZ!j@VmDv)pwlboh#NUl+*O=&6^~sb z;zqTBsp3W#^GPrI7`s(m{$g?0W@vaEjgG4nS5O6E}69xSJb`yJd>FX`C~S#|jI?-AWs`<%_$W$EP!}=^Mn| zLA^U?h`XyZKX5mU`ge2UJ#cT4xS~c-B5o%8_e~WyYm>NQ_V1^G2a3f#$o@l}#66rN z?vXs;*qp`U9&IV^v0QO;sXvd$AE(nN803??#LeF=?rFTBySQg5FJyoVIsSZoaW4!a z{x2@#A2T{wyg}T{?Zqvjk);Z=Sk?~~h ziR){_T5+FNiu-J{xX(B7N2_kUKBeLp0<6lWD&oGHBkt=`UaQj3=3(N#Z7*&Md`G8S z>HPa#puKGz|6x8~f}!4z@KaaVE^Y^%m(oUAh6l^3$j2@m?@&5kIJ%vC#r-lx+%7u* zb+x$NYsKv;;7c+&Fk9T8TloXn0&)LL7q@SaxPLik{|520NxUB*KK2x!io|D45ue>1 zwu-O9V^!>Edgbh(9P7wu`R|xl6^@qtd~(#UBFo zBWxDmV4nCxDL2gLg))yHHiuv5qJzerVZHb!mAq(Xzv(XV%~ptSULd{&$67FumU*yP z{Ly{HAG1O-{@4=nc~m-XwfNSJ#J9;2fBZ1mE50p{wS)FF)}H+i#o{}*6n_GP>a3qz3@nZ?awY9~M>&0&dbP`{{NUx(Io*(!L`@~ONCw>wgOrqn-aQ$lWQyPoEk&bWT zyqlI1*9GFI(%~(fH;r;3eH6|Uf9qE9x2@pMgZ0JV!LbZa3jCe3#oskW{ER;0?=BI4 z4-_%tnKU#Dig$^BfZ5J&DgGfUK1^c|GmuAUcuv0fN7wRwqZzdWo{2S(<$`k)|Pw~%;6u)4R_-Cp2+-%~%kdt{r;Gb_S{)O)1U(A7d;urJy%kz0} zVFo{1&0sqz@o%gXzp9P+)xG#FgJw`Fe$9IEZ*LI4 zwww5O28n-{w%?=vd%O9}BIOUd68{f5SVD!5a)AS%3;-&BIz{|vtHpo5m1o+l&<2ie zr2GZPzNF(#bo$jw@!#Yf_#3AGHj3YpFaA3^`fh{xt^I&OZ{zW8ocF^5el)ESY!d%t zPw_v|W@&~-$_m7nQ>lUjybt7eP7(ievG`rX#Q#c1ztx9b;(w>}-LQuN{Lx4JpPaLI z5+9CSEB^0F@&EAnKKB2eFF_gub}J=>=@Mdh32C>4tVz5H!edpST7BMssGckO7t&Cp zkA#}DCDbaAP$x&iL4~|2vQ+}tf2cQyZ&!^H{Ul{LWVM6_{H|z&sS*zD38fMm(r_d8 zxetWH;fRqE8V>>-(V|$wQGH;wWJ1do5{@pBa10OTRgrL9 zBMGe-ZR>6l+U%0hHkY4m=nVTLbfDu7izRd%1{)-tz+;{8iR^PP2q!bx&NC#OI!8j6 zMH0HM=98MVaXM|E!2r7T1L~ZWC*kb&61sE!2VOLUbE(J^htQL9&k_me=S#R?06zsc zMM7^*?p-P2A_jFa1MDj>o1cIvkkGG>giE3Sd=zu$P2EoHL}QgrU?K zM(4vyB@ACGVZ=HKBO6P&3a-x3_|VbP~)gPzNLig+Dn)~ zqZ2uA;sObisXUp+ZkQoq3T@oTU~lS7Jm*N5%DFdpf=zsNjJBpRutGY$t&N1+H}g@^ zJ`(PnF5xcD&CKX0;cgDx!^!uq<*lHeuvfyYTnWV-yT87K2Np?~-3&PX&@c%Pb9@e! zALH0u9-q5j!n_jEKOKkp>_3$w;prkiRysw(0?v7s_Mht}Vc`G?i}vvmQ|i7jTf&RQ z#Q&wWeE)1Se^cg!Wiuqa!pN4h|5|O{Lt+))Xe8lH$~@l>t4B(Bt60JsX7Khd3GY;q z@NT|@_xed#H($d0+eJTv8a|}WkGf0vm{t0u4eXP!p5vbmlJFVcuvEgv#S*?4M*P2I zrd-eAD=K{5Q^GeZB=GNf*wTsjo(8~f3ExN9E@2z|K*jC5B>cqrJMtuy_TsavP@XTL zf_nTj9(K~s&)p>a0=t?46Z&Wd`ibeC9TzQmfvyr)$tv36I9 zb%sggQjT@^O3d9LvEE{d2Ny{^q(EYQ>SXwLJT|0}M)?vC?Ur~tmCs=0-6)^6SYmf7 zc4rpntdw}}3W+@!Y|r)*&s!?-{1S;5Z0E1*3wR@Ks>BPa(}&|17fb9*-F|e~uSjD5 zmJ%=Hu>tiZUQT;gEXqh6I9=kP0*QlHOB}*Thee>l;WRR0l*B6sNgUY=X!xp460f1+ zC>T9J;uwyPrLp`xiPy5vQ~5Zaa|=4bdWjRLGjXxRNsT2=rl0GVO1y!AP2t#$T_xU> zDIx~jB~INX@s8@glGwA4^W-vwKy)7jc&5<~h zbME8ZSsNr47fQUp4Xl^=z*311E|NIAQsP6aB|frN;-gI9G0JnNOPp6q-%o6n_+&qc z^J(m<+7h2;HqTIL!ETAqcMKtmPoxQkM;!8aKQi;TuM@d{lV_dp%8TDRi zByo8siLWw%*CHPYpx(+niEncLDh9cFg~T=V@ixcao-J`LpZ|#Oc9!_we2ME8Nqj$F z;s=u?en`250eoCv;`%{+w~&rL>nU-=Fo_!rB=XWce%VRlCXR2~Eb;6162GC|W;*(I zs>Cf!W=o|+F5S3wlf-Qd=m+X=FP8Y@dVZ^Lq{Pw<63YvSe+7+JR*|@qf&9#ff8Hzc z7x=X+Flt_w$KCAjSt{|*_CN!BXG{ExbN?1#0RPb0z6BEhog(qSIg%t-5^vKbw?LBL zASuk26njcag_5$SOUj-hsY;Qgs+sO=c1fx}0Cr2NLB$%IB-NZRsTPmbrs6?WB;|IJ zR4+%;!BZsF=Nz7frv^(UHLMNmB{ia*Mtdb4mM7`(m695BZWB5>lE=AxQ&WyLpChS7 zsidQ5vnA!DIhHwQv824tl8)n~R?vEdq~m)@YFi|!J&m+4k;Ff_spAw$CyWFd;Gf%c z(sW6kY2XyjJ(ZbunJ1|$gXXQd#6Pv^j7^fd6^p*nna<|;*&8IC(+ueN+yRn$P`4+4 z|4rvjl63w8*e9u1BT2pcNV>4Sq>J(;^;s|JVrJBrhA&wp=~Bx5*GjsKK@M0a>2f-` zf@6cIKbVGx%$78Cm!#nlICf>Oq>*hT@l-oqJxbCwQ((8G(K95Cq0ZP%eEuMx6R+jL zahz1pO%gA^(}W^P6E{nmOheZXl61pfNjEZpo9H0ZN77Upy1BljTR1+gwxq(IlDK5k zttFCfZv(3(P2Vo*PLAI-Owx?$lJ4fQdpTA#Qqs&ulJ4W!tPIOjOvU?Y^Z_1tV5_7D zVK$?Ch?5_p{P1c?j}%Lq!>AvH$GS?In+F46v7~wXBt6kn(vvGBJyiuZNqPo9Ge^<_ zI$uED1*MXnqo0M87v)QOfx0h_Vls;xOIo~I(o02>UY;UpNjKOmY3XiB%V^-0mXem2 zNLoRKR|mm%Nw3k->l|ANZ%}@dx~ry0T3smVEgD_Jxo`K9w6?RPcX;ew2J>DsN$cSK z+6O+_kv`lYsf5>m>7xQk9}knXzP_YS`$+nX#y+P3o^q#+%xdFaNng_FCOZF$O5YSq z+PqcL7S7+wthQ0-#~dh;v_mA7LK&Tu&68BVNK(amNtK+xllFdYBk33Rf2BR1QK#K} zjUw%#p*@`Z2M7LKA!#oi{MA^}-vyHXVPN~ZOZsn+WbyHgEZ0o3Jmo++Hj=CqB`d3q zWMxm6tST)ft7KBo@sTlG@= z7xpvTpugEs_n$G~lGE>LdH=3v!$(@9)h6^mt#7|}1IO%3+4-<*e;!z6-l~1rp9Zb_ z4;)lI*jD^Udktpm4<2ytseRWP_{co7flW9bGO%xTRinScMx1X!`Bvxj9e7%^2R7_9 z$r|0%!7QFpb?ya&EqpZ=*V;N89o2PBj`5OzAg?`aP{6CCLSa_F2$z{8-mXeb~X0;{r&n!vU8@eTku)J#^AGs-Gc`R8;=JHdlY-;V3Y8< z!k)!d!c%bJdBR@9=L?&OD;KZB6c>G6jqB{rIif|;pPJ~|gdJ)Kxg&ryh{qPMUc(`s9!KeHt5q#EaMDQ!W zSp*-vhZ({(ezl`S@CHYV;E&xZ!X5ZF5$?sei|`=!9D(o{_Hq!O!gq@B9KK70m+)8- zUdMMsg-v*y9ZxU_A7JkU!Y6o~2w&p+MEDlpFTz6nfC!7RCntp8un!QzU-%&rR^W$4 zEM}F36F7jlI(|gNwXkP4#PzXX9mI|CL=m^ZkBhhsenP|@@slF%f+vZ%C-#|vxDS4M zB^8SYvNKu4L-7<5kHXK0cpQFK#FMd4E5y_AR1wd{&x?2-enG^G@rxo}j;Dz@48K(I z4Gnqny)2?9&?_Q(4onx(^y&0?Li8D#A);T@8zTC^-W1W>eoMrP7nmvH zyV!F9;)nPh5kJFz4&qmMmWbcu_e8A2eh-28GyXut-|=h_|H0laq<}y2__Il?aPYB6 zO|jpAAgzNx5orVbsYsh*9~7i5@m!I%!=H<^6aGS^-SC$p?SWjY<=?v_%0cjv!Akqc+2aztpKZ;br3mtaSRqWJ?bUpT|gmg2m z7wLAqNTj=QgGdkH#Ued|e-i0Q>|ucP3|=DAi})9jUd5GP*+6;=|0a^hbg4)l7LOq$ zpV~h}^4a-QB)`ICBKaWy63ILITO=>*@rU#;UM?y*vEvUwUKRf*@|t*s$m^OhK`Y4{ zVUowmo8c(()|j@Oygkk$?~IGcyW>V8cgCv_e>Rr)=b*93hhPTcuqPXo0oXf)avt7Ol#6gDkAEjlcv9>o$`HJ_C|6<6R4CVB zA1suc@IIo9#`}tL2i{K<&yTL6Jb*pPp-jMjp-?K1vvZ&*PvdT)JckbwWg0$MlvlB@ z5GZeApCu^o;zLE5jSmy$6MVQRo;N)NuN177)m8BgFB+`Yrx%TV#U_Lz_PcXm2lY}wA+LMKO zM}35O*}lR~#U6h!zX$j+*txi$unX~N!aUrkJH+3Hvf~K@b`9;~+OfsMjv3G*1A zE$m+G2?q8c9w_WFe2%cEunz>x!&)WmC0sd=4cP1Wd|_|n3xs`u2MP07`c;5^i9LhC zzQq>{TZpTLEykA!`wd?z>@Vyy02N!o&R`J=zFdUW@f9Mhg_$WQtdEC?urc=8gRlj@ zQiN^rFcEgd!$sHyUnRnx_-c>;)l}Yx1D|>b2jUST9Ez_M;V68a2*=^;MK~Fc6v5-` zVT0fyx={q5<(owCnW_=ta=)sZMK~JYB7zUVV-CR^@ih*?eX9s};@dhDhwm4m7C#`u zPk6itOR*;^gui|L`)DB=eptjtc!G#c@FOCwjUN@U1@*9Pg)7j|2whsl!*IcpC*Xi@MIAW!&5};iJuYic>Ju0eeiQ4o{oKrAr8RLi+DbM zLBwkOqR0P53SEJxi8vhl+Jxu{_p*qdG_Q#0ai1=thwfDoefnP$(Pz+?HAJ5U4=qH$ zkT*oE_-Nh~(HneAM1O3ih_kTY6Ci$sJue{6#qWyvHTH547vT3qtjF(*xCHzC1mYif zw#T1MT+YFVB9RES-U+0}_+ydQz;i@ujz1A;L;R^ot?*|eZH0Y+klNzUMQV?~5UHcD z|1UX!v^Sn7Qdj(yNC#uTI!Hb6HzFN_=Zn-Ee=E`{_&bsM#mw0NhyQ z?s!#^kHieh$-VIEBAtFS z<|2>A>x#_%z0PL?^1XO{k-5#*wGf#*W!(lMbHl6K5GrgkcdfdOMCPVb=Tixp8%N#7 zBD41EHW8WiTeqpmEW^51BD0F>HWQiEQ0HNQOhVUfAu>r>*IH!qs#3Ql2arjQx~)Ve zzI9uRLU`&thERN(w-Lo>YFknKYPS=`2i#T^?{s@nyq?D&%8pp@{C^h?+KI9!-bs{w zaC=b>#5;>}DE8!mauoLZP>#d9isE^&n<%H^j-vF(yNfaq@8R($`O5j&JBM;H?j(xm z#9pEd#e0i#HSR3R_1KdZN)7gbL%9|2E6SaCKT+<*T}7#kXJ>y=9>JdRP@cek)leqm zZlX-Z2Z`dja#|Km9TBMfyX2>7cCs*UypJ$H-&dI515Od<_W;iYuqy25`VoJ-h@I1fdAv^-b|pST*a+;& z19l@mQ<&caykRhp@!7)0;Q_)P!k%nk6Y)91p2n4P*?@UmeN8l2VWp; zHXbC*V|k&ldDt%q>^tnq4F29tUA3^E@Fk*ROWC z8#9BB|KY4|hzOhDp(3=#K9dl(#lu8shlhLohjU_A4z3cR6TVu6eepFSbi*S=I1FDa zf+ym2A{>ve7oiUxDT0Ue1`#|g9zqD``{-{H!KbH21i!+YMeq^dB7%21N(8Ux@rU4R z{eOhJ*uPDL`|#}|JdDSP@HoB$Dr~}Jc073?JdeEr2(RF=BD{g`7U5ldj|d;)dqwyR zj}zf5?43jS9^WrQ9ezNBpMCw0=K#X*_(2i=!Jf4c1NMF;ip905BqX~co6m(gLpWeBI42b84*vw z&x+U=KPTcDc&dm4@$(*kraWGNUl8#U{Gy0{ewv7$pf8E&N%pdco+qz}=&_zIqKE5M z5q-*E6VYewbrCCm+{Fz8w z`SkxTlF#5DBKa(MXd(H9EEARFqxnlD zZ}4xC{IP#TT7j2~T(IW_1}Ear#`2CFWRZ8l zbmHVaaU+rU!K;XTAZ{%3p?Fo1kHV{od>m$gPCgko5&1OSROGWO?5rX3d3a5cFUHM8 zz8rIPP9BEW7WrDdj>tFR<|5yU*Az^&5!H9j<;u zk*DH~MCN8w-%@1m7xg}^kXi5bn~2Pkt>08+mR@};ky$LN)LMYYP2Li=&VlPpy#CwZ!4el(;NZdt~o3SS_l-sah1QdV&uYNyK#^J7_ zJc#!f-Z2+X5#LmyoXsi72lMPIPmF%@)fOuqOuC#n=mi zdAv^-HVmI3>{{Gk*iG2;2<%pTmM{;o_W^bv9w6*tTp7p)>~ZXg2R0d>E6n3sCF~XK z0|a{mpD*lPe1WhJ@gQLy$_s^kg)b8JJ@$(O{{3agGaKw@e2K8%@ukB4!Iy~;@L&;E z!9Ft(n&K-&SO+t+PS^ks5n)q2RD><@l^%bVPuPwFpH>Jv;o&0ehOZJ~FMPEK`{8Ru zI0%mr;c$Gd@VE2puM@%Z=Xw!52YhKlIKxMIg9wD7{zegeIz65cd`4T}2w!7QLI?|d{rgBD)Z+(4Sc1ok@CSZSgyr}l5hL~thUoVLzX*tH z;73Gkjvp0qL;RSCt?`lSS-- zefA(8gP#$xH-1*cQ}A;h&*v!AA5RtW9Q?e9gRoCM#7psuB6>zo6Y*;Nl87W#{mUYH zZoDF*hu6af(L?mAi2nXh{c9rnRJ|^uU+oMLeZ;=rA$q58is<#;5^*N}AMt(m-xhHW zen-SFu-_{n&d0Mn{%qop9K0uD1NH_W{)#^kaT%U1;(yq0G>|g>NTk*9$09Yu-Z`Z8 z@Fya*#Gi_^xv&4vIDpg!&lPD0{JBUSu#X(l9{5XOZ1R2VtS>UR(?vc_ zkRQhzh|HaB(S{;(b6T{K$gkj*B6ELOw6Vyn@kKtxkXfdSHWitbwy2fJteDE8%{YKe z)-T#zWHNWr79x{;i#$A#NuWhricAhH+Da5cxM*upJQQt2@!8x)6rT|fEfl|~?N-9` ze;-*}QM~c(MezbVh_VyjQIy@V=LM9#u%CmnA8s$oL3n3T4#yotIU4UG$_aQ^k3Wf3 z`e1Ju%4xWxC}-i_ML8GmA<7`Urzn?TPe>@96Fw*?!|>jsjKG~mxdC?(rE&{9`-pNo z_H2eS7W)-Jxes?0d2)Lgz&7#qe+&m;B-o;3 zg?Xa%66Uc!P8gwD>J;}C!IoxZ|O$EOJM zd%&r}&d2?PRb$T)Fpsy_0UM6b5Oy8zFRTWiDeN}vIR)k+uAI#V?0)PWf=$2!g*}1K z5jF*T5`uYLtAtI*=Lvfg`(VLl;R}R)ga--pP+ll1_BA^f30r_K7XIeVBEM3wCD^kZ z><@gYu;utN5h5NeLSuZn2%ZN%OAwl4X50xI;vpXYAsn>gV5kUN;VVUGi-(EO9{UtS z=!mZpVQ+l32wm|tA{>lIh|mLHE5b4OIuUyNXs#E*<33UZkDaem2tNHcir`atlL$Tw z9$N@p#G;!;@PXYTg10?N1V2Ao1Yhd6LWNDZn;lOK2oGRC3E>euMuaEv9U?q~?-b!h ze3uBXVoxdvZ((m2!h8505kAKEitxFw|8X2Z_y+rp2Eq^66B5EA?1O^v3mz}RpZGx$ z{>2Z8nDE0Q`u)I@8RDAQF9_ng_|cW{{C^{M9usjhJW<50@#7+HkDm~6XY8{9ad$jP z#LoCB5%n%({E~xrMf?`~jRWFB{GN!5@%tkFhP^R}f8p68uD~CP zRIuN4Agzu+7HKU!N2K+A{rgBDZHzw^X$$N8xXr3KfoHXTC@$W1j&?r{V8JIvam4(s_7+NEhQDM7kXRDAF+Ovj^!~ zTr1K|xK5;7aiyLOq`UAUk?z9{B0Y?K>LERje-ddj{#m5w@e+|1ETZ!VK+gcQ#{x+ic z3~sX$p8xwaY%7XiiN_g=52mds-r)A4_+vYW(jM<9N=MvIl)bU%2$Zha%RxC9?<`6W z+`(hjffL8zT}0`PcNN9+z!MD0>DW7gayH&wlq$T3C>P>AMY$Ar62<%RZpD7O~7#G@bqA(xuNe*!_@APD0 zUa^lbkNy8JzXzNm%kY-;Q0b}754gI*W>=eZpM`}*?`@S&l2V_ z_5=fa01puM2=>mwp2X(}dj_8?%;V|_3HB;JPuN@dd|~flA2?L(V|E4!^H_QYgMEW9 z67~bWSooVa4b{Sa!G6_Xe`3#guz&GoA|yOmgjMn7BCLt8@c3WB6U%iu@F|0^5gsDK zW_YLwTjMK5*d7lPVQ1`13c~K#ryD|Ne6o1`VU2!Y25Gqebwweya%g;M+tP zk3B~qJc_*>gh}`g5uU|&iZBh|CBkcXtOzr)=NE+cef@hU5a!@}Mfd`b6Jb8SPlO+_ z-*g}}U{6j6zhWOCgk|_a5&pvuiJ0-jBKrMc!b*7l-;A9{L|hO1)j@2D9}{tNJW<3p z_;C?;z)y(S0Y53?9@u9FVi)|BhzH=OMeL3zd;BL;=tw+8#9sIr5l_NCtq}X+=R`aU zPZhBWKQH1%_yrL?>t7V{O6(zl=*jnzh!v8j;bjp$2VN1;<2YT!arjjceL7zg(P!j! z5&f!Wi0A`*Lqu=;O%eV4TOz)L$%Tq<;%pAy7V%T;Cn3(m?~3>xo+V-}_L~UApYZ!4 zF2x^+_&4@;AsPNqq(=B7k(&7W|Cj?vYvVa0wZNZL28XZ6KPvKSEP3MbCGt% zUx?HRe<{+w*e?iDH~f`D{L^9Vd@WK>{EbM*!QYB>I`-LsGys1u()oCSNY(fU zk*>f$iZmQA6zMu#@u`GVgX=`P4cCh_7B3R%e%v6^1iV2D%=Ts(%5d}@Ce$!F&ek^Bn(6v+pZC}oZJ?ZVNPz3v&bEB5qWRiNaU_~6_F1n z{)-ujlY8J*MLq_vCUS4Qy2z*CCL;I8O+`Kja}iD+gx3`LQrt}BA$Tp3uf}Ubg-ssG z&N?FBf|&s)bFW{#uE^Z47q2Js19*LrxpOXVAu@Nr#T$ss4Q#Q`9%ODpi#HOP`^e&! zB6BaOEZ&#{$gJhXn~2PET)e5sEUU#n^^jQ^i#HRQ>|VUN$Rz0EEkq{C7Pl6eJXyS@ z$b@;ZhYgB{YwMNp{NJa%jVL~C+lb;k9Bcpoe-rulZ8rBj@;8lVKBn{4*(R`rU-wyRw(3~E zK1;M$XH7I2$xruGnXS>qY)#y(hL6p|+P%!y>1o!Spsm}V{9l*G)~Ascqj?I=eyF-{=NCtpo!-1w_CS)Jin&Q!3^vWI_?gKjx{^16)>2? z8B~uEW=Bx>NILF02o~_vyTYiB8EJMb4fU$x13LrEj;GS`W6Vw%V%B@Q*-359PHw~d z^ce=5ozleYRL=LCYj)a9voksY4fm(vGrOCe)sxTkP;S6fe&30@=QQJsz&ut(d*?BT z^J(*f4rYURtTKoJTsWLB0rcSmKQqj#X{@@|?9#?&mvuB7Os9kA@d2P7yxGsWD{81j z14F1cbh+7;)Eh>bVVoa6geUv`%vjuZ4f`V~GlDYLwc&|B=SRW~WBCF>FSDE2uNh7J zZysZI3x!AZHXA*hPYw+-yKNFL2zZQBHm1Ssj;VaRfKKjQW_A~hooROW60>`1&F*b( zHm(bwETZuJ{dvJK#ccdwvxhpEJxrSuXycK2W{)wji9OAp7{H5%Zv0viGxO8{;{P-q zO`c;mg-Xvh=PSz1_%;H5o{BHv7p9rLNCVR-^Aa7tJl*V-v1Zd-!ctykFu>O)@(t!1 zvl)Z?bMDyHJxE$jae3&QKwmhg|)j|Scf{zvsJ>n9r>-L-h8im3g4li!UiMx+sIrCEn8aH zxT}Rt23gp&xrJ5(ENn*E&8J$}f=;%ifvs9t*t(yEHZ-`+biQ9fneAG^d<$(Uw|!3w zJB+rlV~vG&wAYS-v~L2qLmLab^r)=-?nT&*209Mn^%Dj6q*5ma#QG0=(_!aX7P`!| zuFHzmFjqed{VCI*$Ie{N7r@)XU<(6kEDWTNbLLq%m(Htb_dMD> zuhv54f+iL&pu!*;xUijti+WkOn8&K==#sAHf8+_5F0gRfbPIziclk66SInT|SPMg1 zS{Pbw;Y!L}$z#K+EDWcOs|N65kH@cJVAssHFoJs5j<9fDM+?{Y;WzClcLUrwlK9`$ z!op26E!6P9&4YO{ILE>$jz`nLtu+>I<9JL53wJcOaAzCf++AG7*w*~vsF{U(sDCey zkE7H3807s8{1Iu1g$MD&UHNbs`;RifM>#*SxrN7jTX}ON034dLNi?VLEkQ?O@^cUKVCh z?~NgRGOY)2Zf182Z!?H@CRuowbF-RTcyBnbKAFAuD-`^o2~6i-)>;c6&b9E-919E9USOnO+uoNfCJ>7fZFpMqObZUzg#r#?4{0#Z@V@ zI&RY4V$&8D*T}&4pW>P`Ew0rJ=387F)|q6nIhEFJVsSm-iC$cPuEiE(EpEX6h68wM z%;PP`@OxZzuxUq_WwBMQ#mzb1yusoYQ}`Z!mBlSNwOpDu5 zr|mRefA7%3;*O0iw(A2+`997xK6?i{)7Z`&cc`(rOKXd}Hn+IjbpGbX`VvyG_$zx6pQ;&uIpTj`_Hs^Kp%eNi}MF{vUo68eK6$?8DX*eV2g)V z`dd7V6NfFa*n>hxFv267SUjqW#h#Na9zBE)@KNDd&i7(4$8$v|v;p={q~Q~J{3P6m zDev3P;wh~xp6blu_4+t|AZ(Du)4TBtj$Gs!4Sddzw$AKg@vOG6++t+_oe!L4@toln ztD0Cmue-(bJMe`dMmmUwczzHsTxRj285S?5gNy078ZMb;@v@c{2h;K8l)HQqKO!)m z2I+7p<*sCZ7@Z6oYjOBIi&sstcujL&{Bv%^a*NlF=Enu9i9b&a;>ej6Z-AQ^aSaXK z0;3u%-pbY9-rC|AD&9e3cQBAUY51-I7ROGvc=rgP+&%Lx-pld0T8sD1uz3GSpp)@! zEI!!H;zRu`K0LwV1nNCP$B$B$=L2zKOXB}{SBp*PVI-gZ|cla^H=&_+e41j_t5$l|i;{OPmS;@>>} z568>LSzOT)##yqdmf~(sl{MR8+3yOmNul}jX35$kT!-*`dQkvH}H6? zNtQO7ZfWyQmbMtl+a~m}WsRk+dcZVGTTijnW~QZWYATkt%ltq?3rpKG+8w%B+OfgX zPR%T}?+D8*@uVkpm}qI2J}}$Tt~|CI?#MCsg0u&N-E)Mcy~gkx4ihYOrh_iDu}_Vq zeHp-hJ%Kv=x3P3UJ4*-BRyW!_h6j`@$M&$)3yvFS>3AADp~ljQ&G=AfcS|Qzrf+LYr%>lqIzM%RrGAqv zoi@YL87(dKhcjvW%%$dk(@m8DbT*LDokM5m4z^UqsLrGE`K>JtYG>&}8oh{u7mc@6 zP33ADyL5!5%V=aUgT1_?r7OnJ5M_sSfH{_i($Y#5zh)z;G0b1jYN!5@N$Te^<2 z*Y~tElE-hDL;P=?&YM5oV5y~Bnpqmf$6X@WPE|wncW$CeHmL^j7@zIu^sIfF@I$t=O zYiV*9;y-1CCDwR)mIt11V(A5DVOk4IFR}kpt)*A`S(;9zSGj`M8Z6Domfjd(>CK*& z-Wq3V=6p+UGl+L+bXHeO?=eg7w}oYvKH&W9-j+VZA5XG0XQHJ~`dIq388G;{6)Jq* z&C(ZBEb;Go`l^E^{yk4$FR?V=V7?`u067=T^W4Etz2ca~J8?IhKB7f9YucYCYf5pKW0VVf%|K{fo~2q2uLT`M-4j z-*`(aCRq+mEXO&Pb8E|`i{(bJN)OA8+gM(8gyq#HSYExW8u^)cH&(7TFV`1 zW0!80cg>b}TW)!G+TVk=_E=`QQ&-D-asSVIx3Juqid~ji-iONjGV=Y{-+!Xz1F3l6 zQp??@T0Urs<%1dQA?+-8pJe&anU)WmZ~5@?mU|4gd<1ol;(X5<%STi97&<(5q~+sU zTRxu0PH1kqH|?EBA14jBd~%iX`P*rh`_@`M)qn>3(a352EuY@o@)_NMhWpR4d=?eY zh5@}S59ItgG zuA$7$l)YuPpoNjq67jQTG_YAju?*hyB&9i)e zPsTM8u>AHQzDYR2^1BS+z2=rb=xup69evo%^2dEF&#AKf3FSWRVEHr7e>T_h=j|+i zLA@{OXx?DUU$ur>%Pige%^b_$GNA9M|9y?+1+y&w*w6C9*_P|7EZ5WKqHMXL!blb~ z;>AlX{{%}q0i)(+dH#*#rIRfG(GFDlsv==e{?U6Mp#*8q?N{9p~1>(y{uGLTVkaN6`Rbl(sZ1aHF$7M zDz25S@EW}|Z)|1VYAfqghNt1B#Uv{mHi21IHlm@8mRf1q#>&Q1t!zr!R&=x(kF$Ks z=A3Ii#>$optZYS_TeIJWbK6X`vRy|jZCU?idkXR%U1`_d%1$G#>`Ws&&$Plnxn-AX zE4%gs8sMMXvioo=d(uEB%I?LL?mgB@X9mq%bA^9u%f550>{nx@>vAgxaQ=YVR=TwU zIzFhEm4m5!2#<9iWaZHDu#ER#4{u?mM-SfiY-i=j0alKhWu+%qbTkbgGr>wP_K%xx z<#+~p!VD|D>EuMtolFCLXt?iaE2lJA>1V*X)0?K?T`nAH<)XR9D<~_M^tN*8QY(WQz~!T@T+xGA(a@0QR)%u^ z$|hEZb+f{fEyHJ8xw3wL+2a!(uR1rx2@yUfac-K^X{#me{$ zbF4gs9~xujVLE@9x(_d~@(3M0%Kl>mtUONLC;D4?vL(>|BwqiQr>d>gG=q_K}^;N#I&=5SSWmRk9g zMn9wTxm5b1#>$uTtbEnn$~Roqx71nC7-m}efrfvCS~{s6Yo%_2mHJs$7Ezu*YnR1s zt^CCC658V#b@|P#ETx^Ll>Pk(PW&;&%Aa(wtfiH|s;vCY$o}bS<=@^`R`jw)*2)@% z-qt9x&(q*W*{o4%YmFKWw??b9wnmM+TccGw!f50F3t-sztF9S;Re%TyGL+}WU)5-* Zrs13CKE0*U%c}=_?w)%Zozyef{{dr!E585$ diff --git a/WxOrder.Framework/WxOrder.Framework.csproj b/WxOrder.Framework/WxOrder.Framework.csproj index 078c8ae..dc9b26b 100644 --- a/WxOrder.Framework/WxOrder.Framework.csproj +++ b/WxOrder.Framework/WxOrder.Framework.csproj @@ -6,7 +6,9 @@ true true - + + + @@ -31,4 +33,16 @@ + + + + + + + True + True + FontGeometry.tt + + +