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 }