更新20260316

This commit is contained in:
GukSang.Jin
2026-03-16 15:38:08 +08:00
parent 5b7238befa
commit 54b3448e31
16 changed files with 1132 additions and 30 deletions

View File

@@ -2,9 +2,14 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PetWashControl.Models;
using PetWashControl.Services;
using QRCoder;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace PetWashControl.ViewModels;
@@ -39,6 +44,27 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty]
private Order? _currentOrder;
[ObservableProperty]
private ImageSource? _paymentQrCodeImage;
[ObservableProperty]
private string _paymentCodeUrl = "";
[ObservableProperty]
private string _paymentOutTradeNo = "";
[ObservableProperty]
private DateTimeOffset? _paymentExpiresAt;
[ObservableProperty]
private string _paymentCountdownText = "";
[ObservableProperty]
private string _paymentStatusText = "等待扫码支付";
[ObservableProperty]
private bool _isPaymentExpired;
[ObservableProperty]
private string _statusMessage = "系统就绪";
@@ -146,8 +172,12 @@ public partial class MainViewModel : ObservableObject
private readonly System.Timers.Timer _carouselTimer;
private readonly System.Timers.Timer _clockTimer;
private readonly System.Timers.Timer _liquidLevelTimer;
private readonly DispatcherTimer _paymentStatusTimer;
private readonly DispatcherTimer _paymentCountdownTimer;
private readonly string[] _carouselImages = { "/Images/dog.png", "/Images/dog1.png", "/Images/dog2.png" };
private int _currentImageIndex = 0;
private bool _isPaymentPolling;
private bool _isCheckingPaymentStatus;
public MainViewModel()
{
@@ -217,6 +247,18 @@ public partial class MainViewModel : ObservableObject
}
};
_liquidLevelTimer.Start();
_paymentStatusTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(Math.Max(1, _config.PaymentCheckIntervalSeconds))
};
_paymentStatusTimer.Tick += async (s, e) => await PollPaymentStatusAsync();
_paymentCountdownTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_paymentCountdownTimer.Tick += async (s, e) => await RefreshPaymentCountdownAsync();
}
private void InitializeWashSteps()
@@ -654,16 +696,33 @@ public partial class MainViewModel : ObservableObject
try
{
// 创建订单
CurrentOrder = await _apiService.CreateOrderAsync(package.Id);
var createOrderResponse = await _apiService.CreateOrderAsync(package.Id);
CurrentOrder = createOrderResponse?.Order;
PaymentCodeUrl = createOrderResponse?.CodeUrl ?? "";
PaymentOutTradeNo = createOrderResponse?.OutTradeNo ?? "";
PaymentExpiresAt = createOrderResponse?.ExpiresAt;
IsPaymentExpired = false;
PaymentStatusText = "等待扫码支付";
RefreshPaymentCountdown();
PaymentQrCodeImage = string.IsNullOrWhiteSpace(PaymentCodeUrl)
? null
: BuildPaymentQrCodeImage(PaymentCodeUrl);
if (CurrentOrder == null || PaymentQrCodeImage == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
throw new InvalidOperationException("Payment QR code was not returned by the server.");
}
StatusMessage = $"订单创建成功,请扫码支付 ¥{package.Price}";
_logger.LogInfo($"订单创建成功订单ID: {CurrentOrder?.Id}");
// 切换到二维码支付界面
CurrentView = "QRCode";
ViewChanged?.Invoke("QRCode");
StartPaymentStatusPolling();
}
catch (Exception ex)
{
StopPaymentStatusPolling();
_logger.LogError("创建订单失败", ex);
StatusMessage = $"创建订单失败: {ex.Message}";
MessageBox.Show($"创建订单失败\n\n{ex.Message}", "错误",
@@ -672,14 +731,10 @@ public partial class MainViewModel : ObservableObject
}
[RelayCommand]
private void CancelPayment()
private async Task CancelPayment()
{
_logger.LogInfo("取消支付");
CurrentOrder = null;
SelectedPackage = null;
CurrentView = "Payment";
ViewChanged?.Invoke("Payment");
StatusMessage = "已取消支付,请重新选择套餐";
await CancelCurrentPaymentAsync("已取消支付,请重新选择套餐");
}
[RelayCommand]
@@ -694,7 +749,22 @@ public partial class MainViewModel : ObservableObject
try
{
_logger.LogInfo($"创建订单套餐ID: {SelectedPackage.Id}");
CurrentOrder = await _apiService.CreateOrderAsync(SelectedPackage.Id);
var createOrderResponse = await _apiService.CreateOrderAsync(SelectedPackage.Id);
CurrentOrder = createOrderResponse?.Order;
PaymentCodeUrl = createOrderResponse?.CodeUrl ?? "";
PaymentOutTradeNo = createOrderResponse?.OutTradeNo ?? "";
PaymentExpiresAt = createOrderResponse?.ExpiresAt;
IsPaymentExpired = false;
PaymentStatusText = "等待扫码支付";
RefreshPaymentCountdown();
PaymentQrCodeImage = string.IsNullOrWhiteSpace(PaymentCodeUrl)
? null
: BuildPaymentQrCodeImage(PaymentCodeUrl);
if (CurrentOrder == null || PaymentQrCodeImage == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
throw new InvalidOperationException("Payment QR code was not returned by the server.");
}
StatusMessage = $"订单创建成功,订单号: {CurrentOrder?.Id},请支付 ¥{SelectedPackage.Price}";
_logger.LogInfo($"订单创建成功订单ID: {CurrentOrder?.Id}");
}
@@ -719,6 +789,7 @@ public partial class MainViewModel : ObservableObject
try
{
_logger.LogInfo($"模拟支付订单ID: {CurrentOrder.Id}");
StopPaymentStatusPolling();
// 模拟支付处理延迟
StatusMessage = "正在处理支付...";
@@ -726,17 +797,7 @@ public partial class MainViewModel : ObservableObject
// 确认支付
CurrentOrder = await _apiService.ConfirmPaymentAsync(CurrentOrder.Id);
StatusMessage = "支付成功!设备门已打开,请将宠物放入";
IsDoorOpen = true;
_logger.LogInfo("支付成功,门已打开");
// 显示支付成功提示
MessageBox.Show("支付成功!\n\n设备门已自动打开\n请将宠物放入设备后关闭门开始洗护",
"支付成功", MessageBoxButton.OK, MessageBoxImage.Information);
// 返回待机界面,显示关门按钮
CurrentView = "Idle";
ViewChanged?.Invoke("Idle");
await HandlePaymentSuccessAsync();
}
catch (Exception ex)
{
@@ -747,6 +808,212 @@ public partial class MainViewModel : ObservableObject
}
}
private static BitmapImage BuildPaymentQrCodeImage(string codeUrl)
{
using var generator = new QRCodeGenerator();
using var qrCodeData = generator.CreateQrCode(codeUrl, QRCodeGenerator.ECCLevel.Q);
var qrCode = new PngByteQRCode(qrCodeData);
var pngBytes = qrCode.GetGraphic(20);
using var stream = new MemoryStream(pngBytes);
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
return image;
}
private void StartPaymentStatusPolling()
{
StopPaymentStatusPolling();
if (CurrentOrder == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
return;
}
_isPaymentPolling = true;
_paymentStatusTimer.Start();
_paymentCountdownTimer.Start();
}
private void StopPaymentStatusPolling()
{
_isPaymentPolling = false;
_paymentStatusTimer.Stop();
_paymentCountdownTimer.Stop();
_isCheckingPaymentStatus = false;
}
private void RefreshPaymentCountdown()
{
if (PaymentExpiresAt == null)
{
PaymentCountdownText = "";
return;
}
var remaining = PaymentExpiresAt.Value - DateTimeOffset.Now;
if (remaining <= TimeSpan.Zero)
{
PaymentCountdownText = "00:00";
return;
}
PaymentCountdownText = remaining.ToString(@"mm\:ss");
}
private async Task RefreshPaymentCountdownAsync()
{
RefreshPaymentCountdown();
if (IsPaymentExpired || PaymentExpiresAt == null || CurrentOrder == null || CurrentOrder.IsPaid)
{
return;
}
if (PaymentExpiresAt.Value <= DateTimeOffset.Now)
{
await HandlePaymentExpiredAsync();
}
}
private async Task PollPaymentStatusAsync()
{
if (_isCheckingPaymentStatus)
{
return;
}
if (!_isPaymentPolling || CurrentOrder == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
StopPaymentStatusPolling();
return;
}
try
{
_isCheckingPaymentStatus = true;
PaymentStatusText = "正在确认支付结果...";
var paymentStatus = await _apiService.GetPaymentStatusAsync(CurrentOrder.Id, PaymentOutTradeNo);
if (paymentStatus?.Order != null)
{
CurrentOrder = paymentStatus.Order;
}
if (paymentStatus?.IsPaid == true)
{
await HandlePaymentSuccessAsync();
}
else if (!IsPaymentExpired)
{
PaymentStatusText = "等待扫码支付";
}
}
catch (Exception ex)
{
_logger.LogError("轮询支付状态失败", ex);
if (!IsPaymentExpired)
{
PaymentStatusText = "支付状态查询异常";
}
}
finally
{
_isCheckingPaymentStatus = false;
}
}
private Task HandlePaymentSuccessAsync()
{
StopPaymentStatusPolling();
if (CurrentOrder == null)
{
return Task.CompletedTask;
}
IsPaymentExpired = false;
PaymentStatusText = "支付成功";
PaymentCountdownText = "";
PaymentExpiresAt = null;
StatusMessage = "支付成功!设备门已打开,请将宠物放入";
IsDoorOpen = true;
_logger.LogInfo("支付成功,门已打开");
MessageBox.Show("支付成功!\n\n设备门已自动打开\n请将宠物放入设备后关闭门开始洗护",
"支付成功", MessageBoxButton.OK, MessageBoxImage.Information);
CurrentView = "Idle";
ViewChanged?.Invoke("Idle");
return Task.CompletedTask;
}
private async Task HandlePaymentExpiredAsync()
{
if (IsPaymentExpired)
{
return;
}
IsPaymentExpired = true;
StopPaymentStatusPolling();
PaymentStatusText = "二维码已超时,请重新下单";
PaymentCountdownText = "00:00";
PaymentQrCodeImage = null;
StatusMessage = "支付超时,请重新选择套餐";
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
try
{
CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Cancelled);
}
catch (Exception ex)
{
_logger.LogError("支付超时后取消订单失败", ex);
}
}
}
private async Task CancelCurrentPaymentAsync(string statusMessage)
{
StopPaymentStatusPolling();
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
try
{
CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Cancelled);
}
catch (Exception ex)
{
_logger.LogError("取消支付时更新订单状态失败", ex);
}
}
ResetPaymentSession();
CurrentView = "Payment";
ViewChanged?.Invoke("Payment");
StatusMessage = statusMessage;
}
private void ResetPaymentSession()
{
CurrentOrder = null;
SelectedPackage = null;
PaymentCodeUrl = "";
PaymentOutTradeNo = "";
PaymentExpiresAt = null;
PaymentCountdownText = "";
PaymentStatusText = "等待扫码支付";
IsPaymentExpired = false;
PaymentQrCodeImage = null;
}
[RelayCommand]
private async Task CloseDoorAsync()
{