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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml.cs b/29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml.cs
new file mode 100644
index 0000000..0e640ac
--- /dev/null
+++ b/29WxOrderWpf/Ctrls/PDDInvoice/PDDInvoiceView.xaml.cs
@@ -0,0 +1,37 @@
+using _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace _29WxOrderWpf.Ctrls.PDDInvoice;
+
+///
+/// PDDInvoiceView.xaml 的交互逻辑
+///
+public partial class PDDInvoiceView : UserControl
+{
+ public PDDInvoiceView()
+ {
+ InitializeComponent();
+ DataContext = new PDDInvoiceViewModel();
+ }
+
+ private void txtSearch_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter && DataContext is PDDInvoiceViewModel viewModel)
+ {
+ viewModel.SearchCommand.Execute(null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/29WxOrderWpf/Ctrls/PDDInvoice/Resources/invoice.js b/29WxOrderWpf/Ctrls/PDDInvoice/Resources/invoice.js
new file mode 100644
index 0000000..67939f4
--- /dev/null
+++ b/29WxOrderWpf/Ctrls/PDDInvoice/Resources/invoice.js
@@ -0,0 +1,134 @@
+// 填写信息
+function changeReactInputValue(inputDom, newText) {
+ let lastValue = inputDom.value;
+ inputDom.value = newText;
+ let event = new Event('input', { bubbles: true });
+ event.simulated = true;
+ let tracker = inputDom._valueTracker;
+ if (tracker) {
+ tracker.setValue(lastValue);
+ }
+ inputDom.dispatchEvent(event);
+}
+
+// 异步 sleep
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+// 查找元素(支持选择器或XPath)
+function findElement(selector, isXPath = true) {
+ if (isXPath) {
+ return document.evaluate(
+ selector,
+ document,
+ null,
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
+ null
+ ).singleNodeValue;
+ } else {
+ return document.querySelector(selector);
+ }
+}
+
+// 完整流程
+async function fillInvoiceAndApply(data = {}) {
+ const {
+ title = '某某科技有限公司',
+ taxNumber = '91110000123456789X'
+ } = data;
+
+ console.log('📝 开始填充发票信息...');
+
+ // 1. 点击"企业"按钮
+ const invoiceType = findElement('//*[contains(text(), "企业")]');
+ if (!invoiceType) {
+ console.warn('⚠️ 找不到"企业"按钮');
+ return false;
+ }
+
+ invoiceType.click();
+ console.log('✅ 已点击"企业"按钮');
+ await sleep(500);
+
+ // 2. 查找并填充发票抬头
+ const invoiceTitle = findElement('//label[contains(text(), "发票抬头")]/following-sibling::textarea[1]') ||
+ findElement('//*[contains(text(), "发票抬头")]/following-sibling::textarea[1]');
+ if (invoiceTitle) {
+ changeReactInputValue(invoiceTitle, title);
+ console.log('✅ 发票抬头已填充');
+ } else {
+ console.warn('⚠️ 未找到发票抬头控件');
+ }
+
+ // 3. 查找并填充税号
+ const taxNumberInput = findElement('//label[contains(text(), "税号")]/following-sibling::input[1]') ||
+ findElement('//*[contains(text(), "税号")]/following-sibling::input[1]');
+ if (taxNumberInput) {
+ changeReactInputValue(taxNumberInput, taxNumber);
+ console.log('✅ 税号已填充');
+ } else {
+ console.warn('⚠️ 未找到税号控件');
+ }
+
+ await sleep(300);
+
+ // 4. 查找并点击"申请发票"按钮
+ const applyBtn = findElement('//*[contains(text(), "申请发票")]');
+ if (!applyBtn) {
+ console.error('❌ 未找到"申请发票"按钮');
+ return false;
+ }
+
+ applyBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ await sleep(300);
+ applyBtn.click();
+ console.log('✅ 已点击"申请发票"按钮');
+
+ // 5. 等待弹窗/状态出现
+ await sleep(300);
+
+ // 6. 判断状态
+ // 检查是否有"已经申请过"的提示
+ const alreadyApplied = findElement('//*[contains(text(), "您已经提交过发票申请了")]')
+
+
+ if (alreadyApplied) {
+ console.log('✅ 已经申请过,无需重复操作');
+ return true;
+ }
+ const successElement = findElement('//*[text()="确认您的发票信息无误后再提交"]');
+ if (successElement == null) {
+ return false;
+ }
+ // 检查是否有"确认开具"按钮或提示
+ const confirmBtn = findElement('//*[text()= "确认"]');
+
+ if (confirmBtn) {
+ console.log('✅ 找到"确认"按钮,准备点击...');
+ await sleep(200);
+ confirmBtn.click();
+ console.log('✅ 已点击"确认"按钮');
+ return true;
+ }
+
+ // 如果都没有找到
+ console.warn('⚠️ 未找到"已经申请过"或"确认"按钮,请手动检查');
+ return false;
+}
+
+// ========== 调用 ==========
+(async function () {
+ try {
+ const result = await fillInvoiceAndApply({
+ title: '{公司}',
+ taxNumber: '{税号}'
+ });
+ console.log('执行结果:', result);
+ // 批量开票时,C# 端轮询该变量获取执行结果
+ window.__invoiceApplyResult = result;
+ } catch (ex) {
+ console.error('执行异常:', ex);
+ window.__invoiceApplyResult = false;
+ }
+})();
\ No newline at end of file
diff --git a/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceOrderModel.cs b/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceOrderModel.cs
new file mode 100644
index 0000000..0d86986
--- /dev/null
+++ b/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceOrderModel.cs
@@ -0,0 +1,94 @@
+using AllBase.Model.Result;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using WPFCommon.Helpers;
+using WPFCommon.Models;
+
+namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
+
+///
+/// 订单发票Model
+///
+public class PddBrowserOrderInvoiceViewModel : BindableBaseEx
+{
+ private bool _isSelected;
+
+ ///
+ /// 是否被勾选(用于批量开票)
+ ///
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set => SetProperty(ref _isSelected, value, nameof(IsSelected));
+ }
+
+ public string buyerId { get; set; } // 买手Id
+ public string buyerName { get; set; } // 买手名称
+ ///
+ /// 订单价
+ ///
+ public int order_amount { get; set; }
+ ///
+ /// 实付款
+ ///
+ public string display_amount { get; set; }
+ ///
+ /// 下单时间
+ ///
+ public long order_time { get; set; }
+ ///
+ /// 订单号
+ ///
+ public string order_sn { get; set; }
+
+ public int status { get; set; }
+ public int pay_status { get; set; }
+ public string order_status_prompt { get; set; }
+ ///
+ /// 订单详情
+ ///
+ public string order_link_url { get; set; }
+ public List order_Goods { get; set; }
+
+
+ public List order_buttons { get; set; }
+
+
+ public string GoodsName { get => order_Goods?.FirstOrDefault()?.goods_name ?? ""; }
+ ///
+ /// 是否已开票
+ ///
+ public bool IsInvoiced
+ {
+ get => GetValue();
+ set
+ {
+ SetValue(value);
+ base.RaisePropertyChanged(nameof(InvoiceStatusText));
+ }
+ }
+
+ public int Quantity => order_Goods?.Sum(a => a.goods_number.Value) ?? 0;
+ /// 发票状态文案:已开票 / 待开票
+ public string InvoiceStatusText => IsInvoiced ? "已开票" : "待开票";
+
+
+ public DateTime OrderTime
+ {
+ get => DateTimeHelper.ConvertFromUnixTimestampInSeconds(order_time);
+ }
+ public string ThumbUrl => order_Goods?.FirstOrDefault()?.thumb_url ?? "";
+ public string DisplayAmount => (order_amount / 100.0).ToString("F2");
+}
+
+public class OrderButtonItem
+{
+ public int button_code { get; set; }
+ public string brief_prompt { get; set; }
+ public ButtonTypeValue type_value { get; set; }
+}
+public class ButtonTypeValue
+{
+ public string Url { get; set; }
+}
\ No newline at end of file
diff --git a/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.Browser.cs b/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.Browser.cs
new file mode 100644
index 0000000..81f6311
--- /dev/null
+++ b/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.Browser.cs
@@ -0,0 +1,250 @@
+using _29WxOrderWpf.CefHelper;
+using _29WxOrderWpf.Commands;
+using AllBase;
+using CefSharp;
+using CefSharp.Handler;
+using CefSharp.Wpf;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using Work._29Tools.CefTools;
+using WxOrder.Framework.Helpers;
+
+namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
+
+public partial class PDDInvoiceViewModel
+{
+ ///
+ /// 订单列表浏览器
+ ///
+ public ChromiumWebBrowser Browser { get; private set; }
+ ///
+ /// 内容浏览器
+ /// 订单详情
+ /// 发票详情
+ /// 申请发票
+ ///
+ public ChromiumWebBrowser BrowserDetail { get; private set; }
+
+ partial void InitBrowser()
+ {
+ BrowserZIndex = -1;
+ BrowserDetailZIndex = -1;
+
+ Browser = WPFCEFBrowser.CreateChromiumWebBrowser("about:blank", false);
+
+ LocalResourceRequestHandler resourceRequestHandler = new LocalResourceRequestHandler();
+ //resourceRequestHandler.BeforeResourceLoadEvent += RequestHandler_BeforeResourceLoad;
+ resourceRequestHandler.ResourceLoadCompleteEvent += RequestHandler_ResourceLoadComplete;
+ Browser.RequestHandler = new LocalRequestHandler(resourceRequestHandler);
+ Browser.LifeSpanHandler = new LocalLifeSpanHandler(PopWindowType.SelfWindow);
+ Browser.KeyboardHandler = new KeyboardHandler();
+ Browser.LoadingStateChanged += Browser_LoadingStateChanged;
+
+ BrowserDetail = WPFCEFBrowser.CreateChromiumWebBrowser("about:blank", false);
+ LocalResourceRequestHandler detailResourceRequestHandler = new LocalResourceRequestHandler();
+ detailResourceRequestHandler.ResourceLoadCompleteEvent += DetailRequestHandler_ResourceLoadComplete;
+ BrowserDetail.RequestHandler = new LocalRequestHandler(resourceRequestHandler);
+ BrowserDetail.LifeSpanHandler = new LocalLifeSpanHandler(PopWindowType.SelfWindow);
+ BrowserDetail.KeyboardHandler = new KeyboardHandler();
+ BrowserDetail.LoadingStateChanged += BrowserDetail_LoadingStateChanged;
+ }
+
+ private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
+ {
+ if (e.IsLoading == false)
+ {
+ var b = (ChromiumWebBrowser)sender;
+ b.Dispatcher.InvokeAsync(async () =>
+ {
+ if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/orders.html"))
+ {
+ StartAutoScroll();
+ }
+ });
+ }
+ }
+
+ private void BrowserDetail_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
+ {
+ if (e.IsLoading == false)
+ {
+ var b = (ChromiumWebBrowser)sender;
+ b.Dispatcher.InvokeAsync(async () =>
+ {
+ if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
+ {
+ string html = await b.GetBrowser().GetSourceAsync();
+ if (html.Contains("window.rawData"))
+ {
+ await ExecuteInvoiceApplyScriptAsync(b);
+ }
+ else
+ {
+ // 页面异常(未渲染出发票表单)时直接判定失败,避免批量开票长时间等待
+ _currentInvoiceTcs?.TrySetResult(false);
+ }
+ }
+ });
+ }
+ }
+
+ ///
+ /// 注入发票填写脚本并执行。
+ /// 单张申请模式保持原有行为;批量开票模式下轮询脚本写入的
+ /// window.__invoiceApplyResult,完成后通过 回传结果。
+ ///
+ private async Task ExecuteInvoiceApplyScriptAsync(ChromiumWebBrowser b)
+ {
+ string content = ResourceHelper.GetResourceText("/Ctrls/PDDInvoice/Resources/invoice.js", "29WxFindOrder");
+ content = content.Replace("{公司}", InvoiceTitle);
+ content = content.Replace("{税号}", TaxNo);
+ if (string.IsNullOrEmpty(content)) return;
+
+ await b.EvaluateScriptAsync(content);
+
+ if (_currentInvoiceTcs == null) return; // 单张申请模式:保持原有行为
+
+ // 批量模式:等待异步脚本执行完成
+ // 捕获当前 TCS,避免批量循环超时进入下一个订单后,迟到的轮询误触发新订单的结果
+ var tcs = _currentInvoiceTcs;
+ for (int i = 0; i < 120; i++)
+ {
+ await Task.Delay(500);
+ try
+ {
+ var res = await b.EvaluateScriptAsync("window.__invoiceApplyResult");
+ if (res.Success && res.Result != null)
+ {
+ tcs.TrySetResult(Convert.ToBoolean(res.Result));
+ return;
+ }
+ }
+ catch
+ {
+ }
+ }
+ tcs.TrySetResult(false);
+ }
+
+ public ICommand CloseInvoiceDetailCommand => new DelegateCommand(HidenOrderBrowserDetail);
+
+ public int BrowserZIndex
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+ public int BrowserDetailZIndex
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+ ///
+ /// 显示主浏览器
+ ///
+ public void ShowOrderBrowser()
+ {
+ BrowserZIndex = 10;
+ BrowserDetailZIndex = -1;
+ }
+ ///
+ /// 显示子浏览器
+ ///
+ public void ShowOrderBrowserDetail()
+ {
+ BrowserZIndex = -1;
+ BrowserDetailZIndex = 10;
+ }
+ public void HidenOrderBrowser()
+ {
+ BrowserZIndex = -1;
+ }
+ public void HidenOrderBrowserDetail()
+ {
+ BrowserDetailZIndex = -1;
+ }
+ public bool IsReadOrder
+ {
+ get => GetValue();
+ set => SetValue(value);
+ }
+ private int top = 0;
+ private DateTime lastGetOrderDataTime = DateTime.Now;
+ private void RequestHandler_ResourceLoadComplete(object sender, string url, string postData, string responseData, IRequest request)
+ {
+ if (url.StartsWith("https://mobile.yangkeduo.com/orders.html"))
+ {
+ //if (isAutoScroll == false)
+ //{
+ // StartAutoScroll();
+ //}
+
+ StartAutoScroll();
+ }
+ if (url.StartsWith("https://mobile.yangkeduo.com/proxy/api/api/aristotle/order_list_v"))
+ {
+ lastGetOrderDataTime = DateTime.Now;
+ parserData(responseData);
+
+ //File.WriteAllText(@$"E:\{Guid.NewGuid().ToString()}.txt", responseData);
+ }
+ }
+ public void StartAutoScroll()
+ {
+ if (IsReadOrder) return;
+
+ IsReadOrder = true;
+ _ = autoScroll(_cancellationTokenSource.Token);
+ }
+
+ // 停止滚动
+ public void StopAutoScroll()
+ {
+ IsReadOrder = false;
+ _cancellationTokenSource?.Cancel();
+ _cancellationTokenSource?.Dispose();
+ _cancellationTokenSource = null;
+ }
+
+ private async Task autoScroll(CancellationToken token)
+ {
+ while (IsReadOrder && !token.IsCancellationRequested)
+ {
+ if ((DateTime.Now - lastGetOrderDataTime).Seconds > 30) StopAutoScroll();
+
+ top += Randomx.GetId(450, 600);
+ var jsStr = @"window.scroll({
+ top: " + top + @",
+ left: 0,
+ behavior: 'smooth' // 可选,使滚动平滑
+ });";
+
+ var client = Browser.GetDevToolsClient();
+ if (client != null)
+ {
+ var result = client.Runtime.EvaluateAsync(jsStr, null, true, null, null, true, null, true);
+ result.Wait(5000);
+ }
+ await Task.Delay(Randomx.GetId(100, 300));
+ //Thread.Sleep();
+ }
+ }
+
+
+ private void DetailRequestHandler_ResourceLoadComplete(object sender, string url, string postData, string responseData, IRequest request)
+ {
+ if (url.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
+ {
+
+ }
+ }
+
+}
diff --git a/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.cs b/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.cs
new file mode 100644
index 0000000..d09d0ae
--- /dev/null
+++ b/29WxOrderWpf/Ctrls/PDDInvoice/ViewModels/PDDInvoiceViewModel.cs
@@ -0,0 +1,633 @@
+using _29WxOrderWpf.BaseWindows;
+using _29WxOrderWpf.CefHelper;
+using _29WxOrderWpf.Commands;
+using _29WxOrderWpf.Common;
+using AllBase;
+using AllBase.Helper;
+using AllBase.Model.DBModel;
+using AllBase.Model.Result;
+using AllBase.Util;
+using CefSharp;
+using CefSharp.Handler;
+using CefSharp.Wpf;
+using NaiBa.Util.Extension;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Threading;
+using Work._29Tools.CefTools;
+using WPFCommon.Converters;
+using WPFCommon.Models;
+using WxOrder.Framework.Helpers;
+
+namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
+
+///
+/// 拼多多发票助手 视图模型
+///
+public partial class PDDInvoiceViewModel : BindableBaseEx
+{
+ private readonly ObservableCollection _allOrders = new();
+ private readonly HashSet _orderSns = new();
+ private string _searchText = "";
+ private PddBrowserOrderInvoiceViewModel _selectedOrder;
+ private bool _isInvoiceDialogOpen;
+ private bool _isInvoiceDetailOpen;
+ private string _invoiceTitle = "";
+ private string _taxNo = "";
+ private CancellationTokenSource _cancellationTokenSource; public PDDInvoiceViewModel()
+ {
+ Orders = new ObservableCollection();
+ Orders.CollectionChanged += Orders_CollectionChanged;
+
+ BuyerLoginList = GlobalConfig.PDDBuyerLoginList;
+ //Orders = new ObservableCollection(_allOrders);
+
+ //_ = InitSampleData();
+
+ InitBrowser();
+ }
+ partial void InitBrowser();
+
+ /// 列表增删时订阅/退订订单勾选状态,用于刷新表头全选状态
+ private void Orders_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.NewItems != null)
+ {
+ foreach (PddBrowserOrderInvoiceViewModel item in e.NewItems)
+ {
+ item.PropertyChanged += Order_PropertyChanged;
+ }
+ }
+ if (e.OldItems != null)
+ {
+ foreach (PddBrowserOrderInvoiceViewModel item in e.OldItems)
+ {
+ item.PropertyChanged -= Order_PropertyChanged;
+ }
+ }
+ RefreshSelectAllState();
+ }
+
+ private void Order_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(PddBrowserOrderInvoiceViewModel.IsSelected))
+ {
+ RefreshSelectAllState();
+ }
+ }
+
+
+
+
+
+
+ public ObservableCollectionThreadSafe BuyerLoginList { get; set; } = new ObservableCollectionThreadSafe();
+ public BuyerLoginInfo SelectedBuyer
+ {
+ get => GetProperty(() => SelectedBuyer);
+ set => SetProperty(() => SelectedBuyer, value, SelectedBuyerChanged);
+ }
+
+ private void SelectedBuyerChanged()
+ {
+ WPFCEFBrowser.SetCookieAsync(SelectedBuyer.cookie, Browser.GetCookieManager()).GetAwaiter();
+ WPFCEFBrowser.SetCookieAsync(SelectedBuyer.cookie, BrowserDetail.GetCookieManager()).GetAwaiter();
+
+ }
+
+ /// 订单列表
+ public ObservableCollection Orders { get; }
+
+ /// 搜索关键字:订单号 / 商品名称 / 买家
+ public string SearchText
+ {
+ get => _searchText;
+ set => SetProperty(ref _searchText, value, nameof(SearchText));
+ }
+
+ /// 开票弹窗是否打开
+ public bool IsOrderListDialogOpen
+ {
+ get => GetValue();
+ set
+ {
+ SetValue(value);
+ if (value)
+ {
+ ShowOrderBrowser();
+ }
+ else
+ {
+ HidenOrderBrowser();
+ }
+ }
+ }
+
+ /// 发票详情弹窗是否打开
+ public bool IsInvoiceDetailOpen
+ {
+ get => _isInvoiceDetailOpen;
+ set => SetProperty(ref _isInvoiceDetailOpen, value, nameof(IsInvoiceDetailOpen));
+ }
+
+ /// 当前操作的订单
+ public PddBrowserOrderInvoiceViewModel SelectedOrder
+ {
+ get => _selectedOrder;
+ set => SetProperty(ref _selectedOrder, value, nameof(SelectedOrder));
+ }
+
+ /// 发票抬头
+ public string InvoiceTitle
+ {
+ get => _invoiceTitle;
+ set => SetProperty(ref _invoiceTitle, value, nameof(InvoiceTitle));
+ }
+
+ /// 税号
+ public string TaxNo
+ {
+ get => _taxNo;
+ set => SetProperty(ref _taxNo, value, nameof(TaxNo));
+ }
+
+ public ICommand SearchCommand => new DelegateCommand(Search);
+
+ public ICommand ResetCommand => new DelegateCommand(ResetSearch);
+
+ public ICommand OpenInvoiceCommand => new DelegateCommand(OpenInvoice);
+
+ public ICommand ShowInvoiceCommand => new DelegateCommand(ShowInvoice);
+
+ public ICommand ViewOrderCommand => new DelegateCommand(ViewOrder);
+
+
+
+ public ICommand ReadOrderCommand => new AsyncDelegateCommand(ReadOrder);
+
+ private async Task ReadOrder()
+ {
+ if (SelectedBuyer == null)
+ {
+ BaseMsgWindow.ShowError("未填选择买手号");
+ return;
+ }
+ _cancellationTokenSource = new CancellationTokenSource();
+ Debug.WriteLine("开始读单");
+ BottomMsg = $"开始读取订单";
+ StartProgress();
+ try
+ {
+ if (!IsReadOrder)
+ {
+ _orderSns.Clear();
+ _allOrders.Clear();
+ Orders.Clear();
+ Browser.Load("https://mobile.yangkeduo.com/orders.html");
+ }
+
+ await Task.Delay(Timeout.Infinite, _cancellationTokenSource.Token);
+
+ }
+ catch (TaskCanceledException)
+ {
+ Debug.WriteLine("停止读单");
+ }
+ //ShowOrderBrowser();
+ }
+
+ public ICommand StopReadOrderCommand => new AsyncDelegateCommand(StopReadOrder);
+ private async Task StopReadOrder()
+ {
+ StopAutoScroll();
+ CompleteProgress();
+ }
+
+
+ #region 勾选 / 全选 / 反选
+
+ private bool? _isSelectAll;
+ private bool _isUpdatingSelectAll;
+
+ /// 表头全选状态(三态:null 表示部分选中)
+ public bool? IsSelectAll
+ {
+ get => _isSelectAll;
+ set
+ {
+ if (_isSelectAll == value) return;
+ _isSelectAll = value;
+ RaisePropertyChanged(nameof(IsSelectAll));
+ if (_isUpdatingSelectAll) return;
+ if (value == true)
+ {
+ foreach (var order in Orders) order.IsSelected = true;
+ }
+ else if (value == false)
+ {
+ foreach (var order in Orders) order.IsSelected = false;
+ }
+ RefreshSelectAllState();
+ }
+ }
+
+ public ICommand InvertSelectionCommand => new DelegateCommand(InvertSelection);
+
+ /// 反选当前可见列表
+ private void InvertSelection()
+ {
+ foreach (var order in Orders) order.IsSelected = !order.IsSelected;
+ RefreshSelectAllState();
+ }
+
+ /// 根据当前可见列表的勾选情况刷新表头全选状态
+ private void RefreshSelectAllState()
+ {
+ _isUpdatingSelectAll = true;
+ try
+ {
+ if (Orders.Count == 0)
+ {
+ IsSelectAll = false;
+ return;
+ }
+ int selected = Orders.Count(o => o.IsSelected);
+ IsSelectAll = selected == 0 ? false : selected == Orders.Count ? true : (bool?)null;
+ }
+ finally
+ {
+ _isUpdatingSelectAll = false;
+ }
+ }
+
+ #endregion
+
+ #region 批量开票
+
+ private bool _isBatchInvoicing;
+ /// 是否正在批量开票
+ public bool IsBatchInvoicing
+ {
+ get => _isBatchInvoicing;
+ set => SetProperty(ref _isBatchInvoicing, value, nameof(IsBatchInvoicing));
+ }
+
+ /// 当前正在批量处理的订单,页面脚本执行完成后通过它回传结果
+ private TaskCompletionSource _currentInvoiceTcs;
+
+ public ICommand BatchInvoiceCommand => new AsyncDelegateCommand(BatchInvoice);
+
+ ///
+ /// 批量开票:统计勾选的待开票订单 → 确认 → 后台逐单自动开票
+ ///
+ private async Task BatchInvoice()
+ {
+ if (IsBatchInvoicing) return;
+
+ if (string.IsNullOrEmpty(InvoiceTitle) || string.IsNullOrEmpty(TaxNo))
+ {
+ BaseMsgWindow.ShowError("未填写公司或税号");
+ return;
+ }
+
+ var targets = Orders.Where(o => o.IsSelected && !o.IsInvoiced).ToList();
+ if (targets.Count == 0)
+ {
+ BaseMsgWindow.ShowError("请先勾选需要开票的订单");
+ return;
+ }
+
+ var confirm = BaseMsgWindow.ShowConfirm(
+ $"已勾选 {targets.Count} 个待开票订单,确定开始批量开票吗?\n请确认发票抬头与税号无误。",
+ "批量开票");
+ if (confirm.DialogResult != true) return;
+
+ await BatchInvoiceAsync(targets);
+ }
+
+ ///
+ /// 后台批量开票:逐单加载开票页并自动填写提交,底部进度条实时更新,成功后更新列表状态
+ ///
+ private async Task BatchInvoiceAsync(List targets)
+ {
+ IsBatchInvoicing = true;
+ int success = 0, fail = 0;
+ try
+ {
+ ProgressMaximum = targets.Count;
+ ProgressValue = 0;
+ BottomMsg = $"正在批量开票:0/{targets.Count}";
+
+ for (int i = 0; i < targets.Count; i++)
+ {
+ var order = targets[i];
+ BottomMsg = $"正在批量开票:{i}/{targets.Count},当前订单:{order.order_sn}";
+ ProgressValue = i;
+
+ _currentInvoiceTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ BrowserDetail.Load($"https://mobile.yangkeduo.com/transac_return_compensation.html?_t_timestamp=transac_modify_invoice&order_sn={order.order_sn}");
+
+ // 等待页面脚本执行完成(最长 60 秒)
+ var done = await Task.WhenAny(_currentInvoiceTcs.Task, Task.Delay(TimeSpan.FromSeconds(60)));
+ bool ok = done == _currentInvoiceTcs.Task && _currentInvoiceTcs.Task.Result;
+ if (ok)
+ {
+ order.IsInvoiced = true;
+ success++;
+ }
+ else
+ {
+ fail++;
+ }
+ ProgressValue = success + fail;
+ }
+ }
+ catch (Exception ex)
+ {
+ fail = targets.Count - success;
+ BaseMsgWindow.ShowError("批量开票异常:" + ex.Message);
+ }
+ finally
+ {
+ _currentInvoiceTcs = null;
+ ProgressMaximum = targets.Count;
+ ProgressValue = targets.Count;
+ BottomMsg = $"批量开票完成:成功 {success} 个,失败 {fail} 个";
+ IsBatchInvoicing = false;
+ }
+ }
+
+ #endregion
+
+
+ /// 按 订单号 / 商品名称 过滤订单
+ private void Search()
+ {
+ var keyword = SearchText?.Trim();
+ Orders.Clear();
+ foreach (var order in _allOrders.Where(o => IsKeywordMatch(o, keyword)))
+ {
+ Orders.Add(order);
+ }
+ }
+
+ private static bool IsKeywordMatch(PddBrowserOrderInvoiceViewModel order, string keyword)
+ {
+ return string.IsNullOrEmpty(keyword)
+ || (!string.IsNullOrEmpty(order.order_sn) && order.order_sn.Contains(keyword))
+ || (!string.IsNullOrEmpty(order.GoodsName) && order.GoodsName.Contains(keyword));
+ }
+
+ private void ResetSearch()
+ {
+ SearchText = "";
+ Search();
+ }
+ ///
+ /// 申请开票
+ ///
+ ///
+ private void OpenInvoice(PddBrowserOrderInvoiceViewModel order)
+ {
+ SelectedOrder = order;
+ if (string.IsNullOrEmpty(InvoiceTitle) || string.IsNullOrEmpty(TaxNo))
+ {
+ BaseMsgWindow.ShowError("未填写公司或税号");
+ return;
+ }
+ BrowserDetail.Load($"https://mobile.yangkeduo.com/transac_return_compensation.html?_t_timestamp=transac_modify_invoice&order_sn={order.order_sn}");
+ ShowOrderBrowserDetail();
+ }
+
+ private void ShowInvoice(PddBrowserOrderInvoiceViewModel order)
+ {
+ SelectedOrder = order;
+ BrowserDetail.Load($"https://mobile.yangkeduo.com/transac_return_compensation.html?_t_timestamp=transac_invoice&order_sn={order.order_sn}");
+ ShowOrderBrowserDetail();
+ }
+
+ private void ViewOrder(PddBrowserOrderInvoiceViewModel order)
+ {
+ SelectedOrder = order;
+ BrowserDetail.Load($"https://mobile.yangkeduo.com/{order.order_link_url}");
+ ShowOrderBrowserDetail();
+ //MessageBox.Show($"查看订单:{order.OrderNo}");
+ }
+
+
+
+
+
+
+
+
+
+
+
+ #region 数据解析
+
+ private void parserData(string data)
+ {
+ List list = JsonHelper.DeserializeJsonToList(JsonHelper.JObjectParse(data)["orders"].ParseToString());
+ if (list.Count == 0)
+ {
+ StopAutoScroll();
+ return;
+ }
+
+ var matched = list.Where(item => item.status == 4 && item.pay_status == 2).ToList();
+ if (matched.Count == 0)
+ {
+ return;
+ }
+
+ // CEF 回调运行在后台线程,ObservableCollection 的更新需要切回 UI 线程
+ var dispatcher = Application.Current?.Dispatcher;
+ if (dispatcher == null || dispatcher.CheckAccess())
+ {
+ AddOrders(matched);
+ }
+ else
+ {
+ dispatcher.BeginInvoke(new Action(() =>
+ {
+ AddOrders(matched);
+ BottomMsg = $"正在读取订单,读取日期{matched.Last().OrderTime.ToString("yyyy-MM-dd")}";
+ }));
+ }
+
+ }
+
+ /// 将新订单写入总表,并按当前搜索关键字决定是否加入可见列表(自动去重)
+ private void AddOrders(IEnumerable items)
+ {
+ var keyword = SearchText?.Trim();
+ foreach (var item in items)
+ {
+ if (!string.IsNullOrEmpty(item.order_sn) && !_orderSns.Add(item.order_sn))
+ {
+ continue;
+ }
+ _allOrders.Add(item);
+ if (IsKeywordMatch(item, keyword))
+ {
+ item.IsInvoiced = item.order_buttons?.Any(a => a.brief_prompt == "发票详情") ?? false;
+ Orders.Add(item);
+ }
+ }
+ }
+ #endregion
+
+
+
+
+
+ ///
+ /// 原型演示数据(后续可替换为真实接口数据)
+ ///
+ private Task InitSampleData()
+ {
+ string directoryPath = @"E:\820";
+
+ // 3. 获取所有 txt 文件
+ var txtFiles = Directory.GetFiles(directoryPath, "*.txt");
+
+ if (txtFiles.Length == 0)
+ {
+ Console.WriteLine("未找到任何 .txt 文件");
+ return Task.CompletedTask;
+ }
+ foreach (var filePath in txtFiles)
+ {
+ try
+ {
+ string jsonContent = File.ReadAllText(filePath);
+ parserData(jsonContent);
+
+
+ }
+ catch (Exception ex)
+ {
+
+ }
+ }
+ return Task.CompletedTask;
+ }
+
+
+
+
+
+ #region 进度条
+
+ private string _bottomMsg = "";
+ public string BottomMsg
+ {
+ get => _bottomMsg;
+ set => SetValue(ref _bottomMsg, value);
+ }
+
+ private double _progressValue;
+ public double ProgressValue
+ {
+ get => _progressValue;
+ set => SetValue(ref _progressValue, value);
+ }
+
+ private double _progressMaximum = 100;
+ public double ProgressMaximum
+ {
+ get => _progressMaximum;
+ set => SetValue(ref _progressMaximum, value);
+ }
+ public void SetProgress(int value, int maxValue)
+ {
+ StopUnknownTotalProgressTimer();
+ ProgressValue = value;
+ ProgressMaximum = maxValue;
+ }
+
+ #region 未知总数百分比模拟进度
+ private System.Threading.Timer _unknownTotalProgressTimer;
+ private DateTime _unknownTotalProgressStartTime;
+
+ ///
+ /// 开始未知总数的百分比模拟进度:80% 之前走得快,越到最后走得越慢,
+ /// 理论上永远走不到 95% 以上,直到调用 才会变成 100%。
+ ///
+ public void StartProgress()
+ {
+ StopUnknownTotalProgressTimer();
+ ProgressMaximum = 100;
+ ProgressValue = 0;
+ _unknownTotalProgressStartTime = DateTime.Now;
+ _unknownTotalProgressTimer = new System.Threading.Timer(_ => UpdateUnknownTotalProgress(), null, 0, 200);
+ }
+
+ ///
+ /// 结束进度:停止模拟,进度条立即变为 100%。
+ ///
+ public void CompleteProgress()
+ {
+ StopUnknownTotalProgressTimer();
+ ProgressMaximum = 100;
+ ProgressValue = 100;
+ }
+
+ private void UpdateUnknownTotalProgress()
+ {
+ try
+ {
+ System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ var elapsed = (DateTime.Now - _unknownTotalProgressStartTime).TotalSeconds;
+ double progress;
+ if (elapsed <= 10)
+ {
+ // 前 10 秒内快速推进到 80%
+ progress = Math.Max(0, Math.Min(80, elapsed * 8));
+ }
+ else
+ {
+ // 超过 80% 后指数衰减越来越慢,渐近线为 95%,理论上永远走不到 95% 以上
+ progress = 95.0 - 15.0 * Math.Exp(-(elapsed - 10.0) / 20.0);
+ }
+ if (progress > 95) progress = 95;
+ ProgressValue = progress;
+ }));
+ }
+ catch
+ {
+ }
+ }
+
+ private void StopUnknownTotalProgressTimer()
+ {
+ if (_unknownTotalProgressTimer == null) return;
+ try
+ {
+ _unknownTotalProgressTimer.Dispose();
+ }
+ catch
+ {
+ }
+ _unknownTotalProgressTimer = null;
+ }
+ #endregion
+ #endregion
+}
+
diff --git a/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml b/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml
index 008a6df..a01248e 100644
--- a/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml
+++ b/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml
@@ -122,7 +122,7 @@
-
+
-
+
+
+
+
@@ -307,11 +314,13 @@
Margin="0,-10,-10,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
- Background="Transparent" Click="close"
+ Background="Transparent"
+ Click="close"
Content="x" />
-
+/// Cookie对象模型
+///
+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
+
+
+