更新20260316
This commit is contained in:
9
PetWashControl/Models/CreateOrderResponse.cs
Normal file
9
PetWashControl/Models/CreateOrderResponse.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace PetWashControl.Models;
|
||||
|
||||
public sealed class CreateOrderResponse
|
||||
{
|
||||
public Order? Order { get; set; }
|
||||
public string CodeUrl { get; set; } = string.Empty;
|
||||
public string OutTradeNo { get; set; } = string.Empty;
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
}
|
||||
11
PetWashControl/Models/PaymentStatusResponse.cs
Normal file
11
PetWashControl/Models/PaymentStatusResponse.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace PetWashControl.Models;
|
||||
|
||||
public sealed class PaymentStatusResponse
|
||||
{
|
||||
public Order? Order { get; set; }
|
||||
public bool IsPaid { get; set; }
|
||||
public string TradeState { get; set; } = string.Empty;
|
||||
public string OutTradeNo { get; set; } = string.Empty;
|
||||
public string TransactionId { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="MQTTnet" Version="4.3.7.1207" />
|
||||
<PackageReference Include="NModbus" Version="3.0.81" />
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="8.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -32,13 +32,13 @@ public class ApiService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Order?> CreateOrderAsync(int packageId)
|
||||
public async Task<CreateOrderResponse?> CreateOrderAsync(int packageId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("api/orders", new { packageId });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<Order>();
|
||||
return await response.Content.ReadFromJsonAsync<CreateOrderResponse>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
@@ -60,6 +60,19 @@ public class ApiService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PaymentStatusResponse?> GetPaymentStatusAsync(int orderId, string outTradeNo)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<PaymentStatusResponse>(
|
||||
$"api/orders/{orderId}/payment-status?outTradeNo={Uri.EscapeDataString(outTradeNo)}");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new Exception($"获取支付状态失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Order?> GetOrderAsync(int orderId)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -813,20 +813,38 @@
|
||||
|
||||
<!-- 二维码 -->
|
||||
<Border Background="#F5F5F5"
|
||||
Width="220"
|
||||
Height="220"
|
||||
Width="260"
|
||||
Height="260"
|
||||
CornerRadius="15"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,18"
|
||||
BorderBrush="#E0E0E0"
|
||||
BorderThickness="2">
|
||||
<Image Source="/Images/qrcode.png"
|
||||
<Image Source="{Binding PaymentQrCodeImage}"
|
||||
Stretch="Uniform"
|
||||
Margin="10"/>
|
||||
</Border>
|
||||
|
||||
<Border Background="#F5F5F5"
|
||||
CornerRadius="12"
|
||||
Padding="18,12"
|
||||
Margin="0,0,0,18">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding PaymentStatusText}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#2E7D32"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,6"/>
|
||||
<TextBlock Text="{Binding PaymentCountdownText, StringFormat=二维码有效期剩余 {0}}"
|
||||
FontSize="14"
|
||||
Foreground="#757575"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 支付提示 -->
|
||||
<TextBlock Text="请使用微信或支付宝扫描二维码"
|
||||
<TextBlock Text="请使用微信扫码完成支付"
|
||||
FontSize="15"
|
||||
Foreground="#757575"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -892,7 +910,7 @@
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<Border Grid.Row="2" Padding="30,20">
|
||||
<TextBlock Text="等待支付中..."
|
||||
<TextBlock Text="{Binding PaymentStatusText}"
|
||||
FontSize="20"
|
||||
Foreground="#558B2F"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
Reference in New Issue
Block a user