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 941a5eb..30f6476 100644 Binary files a/WxOrder.Framework/Resources/Fonts/iconfont.ttf and b/WxOrder.Framework/Resources/Fonts/iconfont.ttf differ 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 + + +