using _29WxOrderWpf.BaseWindows; using _29WxOrderWpf.CefHelper; using WxOrder.Framework.Commons; using WxOrder.Framework.Commons; using _29WxOrderWpf.Ctrls.Statistics; 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 NPOI.SS.Formula.Functions; 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.Data; using System.Windows.Input; using System.Windows.Threading; using Work._29Tools.CefTools; using Work.BaseForms; using WPFCommon.Converters; using WPFCommon.Helpers; using WPFCommon.Models; using WxOrder.Framework.Commons; using WxOrder.Framework.Commons.Dialogs; using WxOrder.Framework.Helpers; namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels; /// /// 拼多多发票助手 视图模型 /// public partial class PDDInvoiceViewModel : BindableBaseEx, IDialogAware, IDisposable { private const string ConfigPath = "./invoice.config"; private List OrderInvoiceInfoList = 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; private InvoiceConfig Config { get; set; } /// 订单列表 public ObservableCollection Orders { get; } public ICollectionView OrdersView { get; } public PDDInvoiceViewModel() { _cancellationTokenSource = new CancellationTokenSource(); Config = NaiBa.Util.Helper.ConfigHelper.LoadConfig(ConfigPath); Orders = new ObservableCollection(); Orders.CollectionChanged += Orders_CollectionChanged; OrdersView = CollectionViewSource.GetDefaultView(Orders); OrdersView.Filter = FilterPredicate; OrdersView.SortDescriptions.Add( new SortDescription(nameof(PddBrowserOrderInvoiceViewModel.order_time), ListSortDirection.Ascending)); BuyerLoginList = GlobalConfig.PDDBuyerLoginList; //Orders = new ObservableCollection(_allOrders); InitBrowser(); InitBrowserDetail(); if (Config == null) Config = new InvoiceConfig(); EndTime = DateTime.Now; StartTime = EndTime.AddMonths(-1); if (EnvironmentHelper.IsTest) { Config.Title = "嵩县雷神商贸有限责任公司"; Config.TaxNo = "91410325MADQERT68E"; Config.MaxAmount = 99; _ = InitSampleData(); } } private bool FilterPredicate(object obj) { if (obj is not PddBrowserOrderInvoiceViewModel order) return false; // 发票状态过滤 bool? temp = InvoiceStatus == 0 ? null : InvoiceStatus == 2; if (temp.HasValue && order.IsInvoiced != temp.Value) return false; // 关键字过滤 var keyword = SearchText?.Trim(); if (!IsKeywordMatch(order, keyword)) return false; if (order.OrderTime < StartTime || EndTime < order.OrderTime) return false; return true; } /// /// 获取筛选后的数据 /// /// private IEnumerable GetFilterOrders() { return OrdersView.Cast(); } partial void InitBrowser(); partial void InitBrowserDetail(); /// 列表增删时订阅/退订订单勾选状态,用于刷新表头全选状态 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(); private void InvoiceStatusChanged() { } 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 string SearchText { get => _searchText; set => SetProperty(ref _searchText, value, nameof(SearchText)); } public int InvoiceStatus { get => GetValue(); set => SetValue(value, InvoiceStatusChanged); } public DateTime StartTime { get => GetValue(); set => SetValue(value); } public DateTime EndTime { get => GetValue(); set => SetValue(value); } /// 开票弹窗是否打开 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 decimal AmountTotal { get => GetValue(); set => SetValue(value); } ///// ///// 最大开票金额 ///// //public decimal MaxAmount //{ // get => GetValue(); // set => SetValue(value); //} /// /// 已开票金额 /// public decimal InvoicedAmountTotal { get => GetValue(); set => SetValue(value); } /// /// 未开票金额 /// public decimal UninvoicedAmountTotal { get => GetValue(); set => SetValue(value); } public bool IsLoadImage { get => GetValue(); set => SetValue(value); } 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; } Debug.WriteLine("开始读单"); BottomMsg = $"开始读取订单"; StartProgress(); try { if (!IsReadOrder) { _orderSns.Clear(); Orders.Clear(); OrderInvoiceInfoList.Clear(); string path = string.Format(inviceDataPath, SelectedBuyer.buyer_id); if (File.Exists(NaiBa.Util.Helper.ConfigHelper.GetConfigUserPath(path))) { var ret = BaseMsgWindow.ShowConfirm("检测到已有历史数据是否加载?").DialogResult; if (ret == true) { var data = NaiBa.Util.Helper.ConfigHelper.LoadConfig>(path); AddOrders(data.Select(a => new PddBrowserOrderInvoiceViewModel(a))); BottomMsg = $"已加载历史数据"; return; } else if (ret == null) { return; } } 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 勾选 / 全选 / 反选 public int SelectedCount { get => GetValue(); set => SetValue(value); } 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 GetFilterOrders()) { if (order.IsInvoiced == false) { 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; SelectedCount = selected; } 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(Config.Title) || string.IsNullOrEmpty(Config.TaxNo)) { BaseMsgWindow.ShowError("未填写公司或税号"); return; } var targets = GetFilterOrders().Where(o => o.IsSelected && !o.IsInvoiced).ToList(); if (targets.Count == 0) { BaseMsgWindow.ShowError("请先勾选需要开票的订单"); return; } if (Config.MaxAmount <= 0) { BaseMsgWindow.ShowError("请填写开票总金额"); return; } var confirm = BaseMsgWindow.ShowConfirm( $"已勾选 {targets.Count} 个待开票订单,确定开始批量开票吗?\n请确认发票抬头与税号无误。\n开票总金额:{Config.MaxAmount}", "批量开票"); if (confirm.DialogResult != true) return; //Config.Title = InvoiceTitle; //Config.TaxNo = TaxNo; //Config.MaxAmount = MaxAmount; //NaiBa.Util.Helper.ConfigHelper.SaveConfig(ConfigPath, Config); await BatchInvoiceAsync(targets); } public ICommand StopBatchInvoiceCommand => new AsyncDelegateCommand(StopBatchInvoice); private async Task StopBatchInvoice() { IsBatchInvoicing = false; } public ICommand ShowSettingDialogCommand => new DelegateCommand(ShowSettingDialog); private void ShowSettingDialog() { var view = new PDDInvoiceDialog(Config); DialogService.ShowDialog(view, null, result => { if (result.Result == ButtonResult.OK) { Config.Assign(view.Config); NaiBa.Util.Helper.ConfigHelper.SaveConfig(ConfigPath, Config); } }); } public event EventHandler BatchInvoiceChanged; public event Action RequestClose; /// /// 后台批量开票:逐单加载开票页并自动填写提交,底部进度条实时更新,成功后更新列表状态 /// private async Task BatchInvoiceAsync(List targets) { IsBatchInvoicing = true; AmountTotal = 0; int success = 0, fail = 0; try { ProgressMaximum = targets.Count; ProgressValue = 0; BottomMsg = $"正在批量开票:0/{targets.Count}"; for (int i = 0; i < targets.Count; i++) { if (IsBatchInvoicing == false) { BottomMsg = "已取消批量开票"; return; } if (AmountTotal >= Config.MaxAmount) { return; } var order = targets[i]; BottomMsg = $"正在批量开票:{i}/{targets.Count},当前订单:{order.order_sn}"; ProgressValue = i; SelectedOrder = order; BatchInvoiceChanged?.Invoke(this, GetFilterOrders().ToList().IndexOf(order)); bool ok = false; if (EnvironmentHelper.IsTest) { ok = true; await Task.Delay(1000); } else { _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))); ok = done == _currentInvoiceTcs.Task && _currentInvoiceTcs.Task.Result; } if (ok) { order.IsInvoiced = true; order.IsSelected = false; success++; AmountTotal += order.order_amount / 100m; if (Config.IsSaveData) { var temp = OrderInvoiceInfoList?.FirstOrDefault(a => a.order_sn == order.order_sn); if (temp != null) temp.IsInvoiced = true; } } else { fail++; } ProgressValue = success + fail; BottomMsg = $"正在批量开票:{i}/{targets.Count},当前订单:{order.order_sn}。总金额:{AmountTotal.ToString("N2")}"; BottomRMsg = $"批量开票状态:成功 {success} 个,失败 {fail} 个。总金额:{AmountTotal.ToString("N2")}"; StatisticsInvoiceAmount(); await Task.Delay(200); if (Config.IsSaveData) { SaveOrderInvoiceInfo(); } } } catch (Exception ex) { fail = targets.Count - success; BaseMsgWindow.ShowError("批量开票异常:" + ex.Message); } finally { _currentInvoiceTcs = null; ProgressMaximum = targets.Count; ProgressValue = targets.Count; IsBatchInvoicing = false; BaseMsgWindow.ShowInfo(BottomMsg); } } /// /// 统计发票金额 /// private void StatisticsInvoiceAmount() { InvoicedAmountTotal = GetFilterOrders().Where(a => a.IsInvoiced).Sum(a => a.order_amount) / 100m; UninvoicedAmountTotal = GetFilterOrders().Where(a => a.IsInvoiced == false).Sum(a => a.order_amount) / 100m; } #endregion /// 按 订单号 / 商品名称 过滤订单 private void Search() { OrdersView.Refresh(); StatisticsInvoiceAmount(); //var keyword = SearchText?.Trim(); //bool? _tempIsInvoiced = InvoiceStatus == 0 ? null : InvoiceStatus == 2; //foreach (var order in Orders) //{ // if (_tempIsInvoiced.HasValue) // { // if (order.IsInvoiced != _tempIsInvoiced.Value) continue; // } // if (!IsKeywordMatch(order, keyword)) // { // continue; // } // 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 = ""; InvoiceStatus = 0; EndTime = DateTime.Now; StartTime = EndTime.AddMonths(-1); Search(); } /// /// 申请开票 /// /// private void OpenInvoice(PddBrowserOrderInvoiceViewModel order) { SelectedOrder = order; if (string.IsNullOrEmpty(Config.Title) || string.IsNullOrEmpty(Config.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) { parserData(data, out string _); } private void parserData(string data, out string lastOrderSN) { lastOrderSN = "0"; List list = JsonHelper.DeserializeJsonToList(JsonHelper.JObjectParse(data)["orders"].ParseToString()); if (list.Count == 0) { StopAutoScroll(); return; } lastOrderSN = list.Last().order_sn; var currentLastItem = list.Last(); var matched = list.Where(item => item.status == 4 && item.pay_status == 2).ToList(); Application.Current?.Dispatcher.BeginInvoke(new Action(() => { if (matched.Count > 0) { AddOrders(matched); } BottomMsg = $"正在读取订单,读取日期{currentLastItem.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; } if (item.IsInvoiced == false) { item.IsInvoiced = item.order_buttons?.Any(a => a.brief_prompt == "发票详情") ?? false; } Orders.Add(item); if (Config.IsSaveData) { OrderInvoiceInfoList.Add(new OrderInvoiceInfo { GoodsName = item.GoodsName, order_amount = item.order_amount, order_time = item.order_time, order_sn = item.order_sn, order_link_url = item.order_link_url, IsInvoiced = item.IsInvoiced, ThumbUrl = item.ThumbUrl }); } } StatisticsInvoiceAmount(); if (Config.IsSaveData) { SaveOrderInvoiceInfo(); } } #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) { } } Orders.FirstOrDefault().IsInvoiced = true; return Task.CompletedTask; } private const string inviceDataPath = "inviceData_{0}"; /// /// 保存数据 /// private void SaveOrderInvoiceInfo() { if (SelectedBuyer == null) return; string path = string.Format(inviceDataPath, SelectedBuyer.buyer_id); NaiBa.Util.Helper.ConfigHelper.SaveConfig>(path, OrderInvoiceInfoList); } private void GetOrderInvoiceInfo() { if (SelectedBuyer == null) return; string path = string.Format(inviceDataPath, SelectedBuyer.buyer_id); if (File.Exists(NaiBa.Util.Helper.ConfigHelper.GetConfigUserPath(path))) { if (BaseMsgWindow.ShowConfirm("检测到已有旧数据是否加载?").DialogResult == true) { var data = NaiBa.Util.Helper.ConfigHelper.LoadConfig>(path); AddOrders(data.Select(a => new PddBrowserOrderInvoiceViewModel(a))); } } } #region 进度条 private string _bottomMsg = ""; public string BottomMsg { get => _bottomMsg; set => SetValue(ref _bottomMsg, value); } private string _bottomRMsg = ""; public string BottomRMsg { get => _bottomRMsg; set => SetValue(ref _bottomRMsg, 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 string Title => "发票助手"; 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; } public void Dispose() { StopAutoScroll(); Browser.Dispose(); BrowserDetail.Dispose(); } public void OnDialogOpened(IDialogParameters parameters) { } public bool CanCloseDialog() { return true; } public void OnDialogClosed() { Dispose(); } #endregion #endregion } public class InvoiceConfig { public string Title { get; set; } public string TaxNo { get; set; } public decimal MaxAmount { get; set; } public bool IsSaveData { get; set; } = true; public void Assign(InvoiceConfig config) { Title = config.Title; TaxNo = config.TaxNo; MaxAmount = config.MaxAmount; IsSaveData = config.IsSaveData; } }