diff --git a/PetWashControl/App.xaml b/PetWashControl/App.xaml index 0a28f88..764e29d 100644 --- a/PetWashControl/App.xaml +++ b/PetWashControl/App.xaml @@ -2,8 +2,16 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PetWashControl" - StartupUri="MainWindow.xaml"> + xmlns:converters="clr-namespace:PetWashControl.Converters" + StartupUri="Views/MainWindow.xaml"> - + + + + + + + + diff --git a/PetWashControl/Converters/BoolToStatusConverter.cs b/PetWashControl/Converters/BoolToStatusConverter.cs new file mode 100644 index 0000000..a63d58f --- /dev/null +++ b/PetWashControl/Converters/BoolToStatusConverter.cs @@ -0,0 +1,38 @@ +using System.Globalization; +using System.Windows.Data; + +namespace PetWashControl.Converters; + +public class BoolToStatusConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isOpen) + { + return isOpen ? "已打开" : "已关闭"; + } + return "未知"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +public class BoolToWashingConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isWashing) + { + return isWashing ? "清洗中" : "空闲"; + } + return "未知"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/PetWashControl/MainWindow.xaml b/PetWashControl/MainWindow.xaml deleted file mode 100644 index 6032a7e..0000000 --- a/PetWashControl/MainWindow.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/PetWashControl/MainWindow.xaml.cs b/PetWashControl/MainWindow.xaml.cs deleted file mode 100644 index e39e4fd..0000000 --- a/PetWashControl/MainWindow.xaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text; -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 PetWashControl -{ - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : Window - { - public MainWindow() - { - InitializeComponent(); - } - } -} \ No newline at end of file diff --git a/PetWashControl/Models/Order.cs b/PetWashControl/Models/Order.cs new file mode 100644 index 0000000..827ad82 --- /dev/null +++ b/PetWashControl/Models/Order.cs @@ -0,0 +1,23 @@ +namespace PetWashControl.Models; + +public class Order +{ + public int Id { get; set; } + public int PackageId { get; set; } + public Package? Package { get; set; } + public DateTime CreatedAt { get; set; } + public OrderStatus Status { get; set; } + public bool IsPaid { get; set; } +} + +public enum OrderStatus +{ + Created, + WaitingPayment, + Paid, + DoorOpened, + DoorClosed, + Washing, + Completed, + Cancelled +} diff --git a/PetWashControl/Models/Package.cs b/PetWashControl/Models/Package.cs new file mode 100644 index 0000000..64f7b56 --- /dev/null +++ b/PetWashControl/Models/Package.cs @@ -0,0 +1,10 @@ +namespace PetWashControl.Models; + +public class Package +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public int DurationMinutes { get; set; } + public string Description { get; set; } = string.Empty; +} diff --git a/PetWashControl/PetWashControl.csproj b/PetWashControl/PetWashControl.csproj index e3e33e3..76e59f8 100644 --- a/PetWashControl/PetWashControl.csproj +++ b/PetWashControl/PetWashControl.csproj @@ -8,4 +8,12 @@ true + + + + + + + + diff --git a/PetWashControl/Resources/Styles.xaml b/PetWashControl/Resources/Styles.xaml new file mode 100644 index 0000000..8279cd0 --- /dev/null +++ b/PetWashControl/Resources/Styles.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PetWashControl/Services/ApiService.cs b/PetWashControl/Services/ApiService.cs new file mode 100644 index 0000000..d3b38ec --- /dev/null +++ b/PetWashControl/Services/ApiService.cs @@ -0,0 +1,88 @@ +using System.Net.Http; +using System.Net.Http.Json; +using PetWashControl.Models; + +namespace PetWashControl.Services; + +public class ApiService +{ + private readonly HttpClient _httpClient; + private readonly ConfigurationService _config; + + public ApiService(ConfigurationService? config = null) + { + _config = config ?? new ConfigurationService(); + _httpClient = new HttpClient + { + BaseAddress = new Uri(_config.ApiBaseUrl), + Timeout = TimeSpan.FromSeconds(30) + }; + } + + public async Task> GetPackagesAsync() + { + try + { + return await _httpClient.GetFromJsonAsync>("api/packages") + ?? new List(); + } + catch (HttpRequestException ex) + { + throw new Exception($"无法连接到服务器: {ex.Message}", ex); + } + } + + public async Task CreateOrderAsync(int packageId) + { + try + { + var response = await _httpClient.PostAsJsonAsync("api/orders", new { packageId }); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + catch (HttpRequestException ex) + { + throw new Exception($"创建订单失败: {ex.Message}", ex); + } + } + + public async Task ConfirmPaymentAsync(int orderId) + { + try + { + var response = await _httpClient.PostAsync($"api/orders/{orderId}/payment", null); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + catch (HttpRequestException ex) + { + throw new Exception($"确认支付失败: {ex.Message}", ex); + } + } + + public async Task GetOrderAsync(int orderId) + { + try + { + return await _httpClient.GetFromJsonAsync($"api/orders/{orderId}"); + } + catch (HttpRequestException ex) + { + throw new Exception($"获取订单失败: {ex.Message}", ex); + } + } + + public async Task UpdateOrderStatusAsync(int orderId, OrderStatus status) + { + try + { + var response = await _httpClient.PutAsJsonAsync($"api/orders/{orderId}/status", new { status }); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } + catch (HttpRequestException ex) + { + throw new Exception($"更新订单状态失败: {ex.Message}", ex); + } + } +} diff --git a/PetWashControl/Services/ConfigurationService.cs b/PetWashControl/Services/ConfigurationService.cs new file mode 100644 index 0000000..689e6d6 --- /dev/null +++ b/PetWashControl/Services/ConfigurationService.cs @@ -0,0 +1,12 @@ +namespace PetWashControl.Services; + +public class ConfigurationService +{ + public string ApiBaseUrl { get; set; } = "https://localhost:7001/"; + public string MqttBrokerHost { get; set; } = "localhost"; + public int MqttBrokerPort { get; set; } = 1883; + public string MqttClientId { get; set; } = "PetWashControl"; + + public int PaymentCheckIntervalSeconds { get; set; } = 2; + public int WashSimulationSeconds { get; set; } = 10; +} diff --git a/PetWashControl/Services/LogService.cs b/PetWashControl/Services/LogService.cs new file mode 100644 index 0000000..62a9f05 --- /dev/null +++ b/PetWashControl/Services/LogService.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using System.IO; + +namespace PetWashControl.Services; + +public class LogService +{ + private readonly string _logFilePath; + + public LogService() + { + var logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + Directory.CreateDirectory(logDirectory); + _logFilePath = Path.Combine(logDirectory, $"log_{DateTime.Now:yyyyMMdd}.txt"); + } + + public void LogInfo(string message) + { + Log("INFO", message); + } + + public void LogWarning(string message) + { + Log("WARN", message); + } + + public void LogError(string message, Exception? ex = null) + { + var fullMessage = ex != null ? $"{message}\n{ex}" : message; + Log("ERROR", fullMessage); + } + + private void Log(string level, string message) + { + var logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}"; + + // 输出到调试窗口 + Debug.WriteLine(logMessage); + + // 写入文件 + try + { + File.AppendAllText(_logFilePath, logMessage + Environment.NewLine); + } + catch + { + // 忽略日志写入失败 + } + } +} diff --git a/PetWashControl/Services/MqttClientService.cs b/PetWashControl/Services/MqttClientService.cs new file mode 100644 index 0000000..93f3feb --- /dev/null +++ b/PetWashControl/Services/MqttClientService.cs @@ -0,0 +1,109 @@ +using MQTTnet; +using MQTTnet.Client; +using System.Text; +using System.Text.Json; + +namespace PetWashControl.Services; + +public class MqttClientService +{ + private readonly IMqttClient _mqttClient; + private readonly MqttClientOptions _options; + private readonly ConfigurationService _config; + + public event Action? MessageReceived; + public bool IsConnected => _mqttClient.IsConnected; + + public MqttClientService(ConfigurationService? config = null) + { + _config = config ?? new ConfigurationService(); + + var factory = new MqttFactory(); + _mqttClient = factory.CreateMqttClient(); + + _options = new MqttClientOptionsBuilder() + .WithTcpServer(_config.MqttBrokerHost, _config.MqttBrokerPort) + .WithClientId(_config.MqttClientId) + .WithCleanSession() + .Build(); + + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceived; + _mqttClient.DisconnectedAsync += OnDisconnected; + } + + public async Task ConnectAsync() + { + if (_mqttClient.IsConnected) + return; + + try + { + await _mqttClient.ConnectAsync(_options, CancellationToken.None); + + // 订阅设备状态和命令主题 + var subscribeOptions = new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter("device/status") + .WithTopicFilter("device/command") + .Build(); + + await _mqttClient.SubscribeAsync(subscribeOptions, CancellationToken.None); + } + catch (Exception ex) + { + throw new Exception($"MQTT连接失败: {ex.Message}", ex); + } + } + + public async Task DisconnectAsync() + { + if (_mqttClient.IsConnected) + { + await _mqttClient.DisconnectAsync(); + } + } + + public async Task PublishAsync(string topic, object payload) + { + if (!_mqttClient.IsConnected) + { + throw new InvalidOperationException("MQTT客户端未连接"); + } + + var json = JsonSerializer.Serialize(payload); + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(json) + .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce) + .Build(); + + await _mqttClient.PublishAsync(message, CancellationToken.None); + } + + private Task OnMessageReceived(MqttApplicationMessageReceivedEventArgs args) + { + var topic = args.ApplicationMessage.Topic; + var payloadBytes = args.ApplicationMessage.PayloadSegment.ToArray(); + var payload = Encoding.UTF8.GetString(payloadBytes); + + MessageReceived?.Invoke(topic, payload); + return Task.CompletedTask; + } + + private async Task OnDisconnected(MqttClientDisconnectedEventArgs args) + { + // 自动重连 + if (!args.ClientWasConnected) + return; + + await Task.Delay(TimeSpan.FromSeconds(5)); + + try + { + await ConnectAsync(); + } + catch + { + // 重连失败,等待下次重试 + } + } +} diff --git a/PetWashControl/ViewModels/MainViewModel.cs b/PetWashControl/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..d0753b2 --- /dev/null +++ b/PetWashControl/ViewModels/MainViewModel.cs @@ -0,0 +1,262 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using PetWashControl.Models; +using PetWashControl.Services; +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Windows; + +namespace PetWashControl.ViewModels; + +public partial class MainViewModel : ObservableObject +{ + private readonly ApiService _apiService; + private readonly MqttClientService _mqttService; + private readonly ConfigurationService _config; + private readonly LogService _logger; + + [ObservableProperty] + private ObservableCollection _packages = new(); + + [ObservableProperty] + private Package? _selectedPackage; + + [ObservableProperty] + private Order? _currentOrder; + + [ObservableProperty] + private string _statusMessage = "请选择套餐"; + + [ObservableProperty] + private bool _isDoorOpen; + + [ObservableProperty] + private bool _isWashing; + + [ObservableProperty] + private bool _isConnected; + + public MainViewModel() + { + _config = new ConfigurationService(); + _logger = new LogService(); + _apiService = new ApiService(_config); + _mqttService = new MqttClientService(_config); + _mqttService.MessageReceived += OnMqttMessageReceived; + } + + public async Task InitializeAsync() + { + try + { + _logger.LogInfo("开始初始化系统..."); + + await _mqttService.ConnectAsync(); + IsConnected = _mqttService.IsConnected; + _logger.LogInfo("MQTT连接成功"); + + await LoadPackagesAsync(); + StatusMessage = "系统就绪,请选择套餐"; + _logger.LogInfo("系统初始化完成"); + } + catch (Exception ex) + { + _logger.LogError("初始化失败", ex); + StatusMessage = $"初始化失败: {ex.Message}"; + MessageBox.Show($"系统初始化失败,请检查后端服务是否启动。\n\n错误: {ex.Message}", + "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + [RelayCommand] + private async Task LoadPackagesAsync() + { + try + { + _logger.LogInfo("加载套餐列表..."); + var packages = await _apiService.GetPackagesAsync(); + Packages.Clear(); + foreach (var package in packages) + { + Packages.Add(package); + } + _logger.LogInfo($"成功加载 {packages.Count} 个套餐"); + } + catch (Exception ex) + { + _logger.LogError("加载套餐失败", ex); + StatusMessage = $"加载套餐失败: {ex.Message}"; + } + } + + [RelayCommand] + private async Task CreateOrderAsync() + { + if (SelectedPackage == null) + { + MessageBox.Show("请先选择套餐", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + try + { + _logger.LogInfo($"创建订单,套餐ID: {SelectedPackage.Id}"); + CurrentOrder = await _apiService.CreateOrderAsync(SelectedPackage.Id); + StatusMessage = $"订单创建成功,订单号: {CurrentOrder?.Id},请支付 ¥{SelectedPackage.Price}"; + _logger.LogInfo($"订单创建成功,订单ID: {CurrentOrder?.Id}"); + } + catch (Exception ex) + { + _logger.LogError("创建订单失败", ex); + StatusMessage = $"创建订单失败: {ex.Message}"; + MessageBox.Show($"创建订单失败\n\n{ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + [RelayCommand] + private async Task SimulatePaymentAsync() + { + if (CurrentOrder == null) + { + MessageBox.Show("请先创建订单", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + try + { + _logger.LogInfo($"模拟支付,订单ID: {CurrentOrder.Id}"); + CurrentOrder = await _apiService.ConfirmPaymentAsync(CurrentOrder.Id); + StatusMessage = "支付成功!设备门已打开,请将宠物放入"; + IsDoorOpen = true; + _logger.LogInfo("支付成功,门已打开"); + } + catch (Exception ex) + { + _logger.LogError("支付失败", ex); + StatusMessage = $"支付失败: {ex.Message}"; + MessageBox.Show($"支付失败\n\n{ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + [RelayCommand] + private async Task CloseDoorAsync() + { + if (CurrentOrder == null || !IsDoorOpen) + { + MessageBox.Show("门未打开", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + try + { + _logger.LogInfo($"关门,订单ID: {CurrentOrder.Id}"); + + // 通过MQTT发送关门状态 + await _mqttService.PublishAsync("device/status", new + { + status = "door_closed", + orderId = CurrentOrder.Id, + timestamp = DateTime.Now + }); + + // 更新订单状态 + CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.DoorClosed); + + IsDoorOpen = false; + StatusMessage = "门已关闭,清洗即将开始..."; + _logger.LogInfo("门已关闭,等待清洗开始"); + } + catch (Exception ex) + { + _logger.LogError("关门失败", ex); + StatusMessage = $"关门失败: {ex.Message}"; + MessageBox.Show($"关门失败\n\n{ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void OnMqttMessageReceived(string topic, string payload) + { + Application.Current.Dispatcher.Invoke(() => + { + try + { + _logger.LogInfo($"收到MQTT消息 - Topic: {topic}, Payload: {payload}"); + var message = JsonSerializer.Deserialize(payload); + + if (topic == "device/command") + { + var command = message.GetProperty("command").GetString(); + + if (command == "open_door") + { + IsDoorOpen = true; + StatusMessage = "设备门已打开,请将宠物放入后点击关门"; + _logger.LogInfo("收到开门指令"); + } + else if (command == "start_wash") + { + IsWashing = true; + var duration = message.GetProperty("durationMinutes").GetInt32(); + StatusMessage = $"清洗已开始,预计 {duration} 分钟完成"; + _logger.LogInfo($"收到开始清洗指令,时长: {duration}分钟"); + + // 模拟清洗完成 + Task.Delay(TimeSpan.FromSeconds(_config.WashSimulationSeconds)).ContinueWith(async _ => + { + await SimulateWashCompleteAsync(); + }); + } + } + else if (topic == "device/status") + { + var status = message.GetProperty("status").GetString(); + StatusMessage = $"设备状态: {status}"; + _logger.LogInfo($"设备状态更新: {status}"); + } + } + catch (Exception ex) + { + _logger.LogError("处理MQTT消息失败", ex); + StatusMessage = $"处理消息失败: {ex.Message}"; + } + }); + } + + private async Task SimulateWashCompleteAsync() + { + if (CurrentOrder == null) return; + + await Application.Current.Dispatcher.InvokeAsync(async () => + { + try + { + _logger.LogInfo($"清洗完成,订单ID: {CurrentOrder.Id}"); + + // 发送清洗完成状态 + await _mqttService.PublishAsync("device/status", new + { + status = "completed", + orderId = CurrentOrder.Id, + timestamp = DateTime.Now + }); + + CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Completed); + + IsWashing = false; + StatusMessage = "清洗完成!请取出宠物"; + + MessageBox.Show("清洗完成!感谢使用", "完成", + MessageBoxButton.OK, MessageBoxImage.Information); + _logger.LogInfo("订单流程完成"); + } + catch (Exception ex) + { + _logger.LogError("完成流程失败", ex); + StatusMessage = $"完成流程失败: {ex.Message}"; + } + }); + } +} diff --git a/PetWashControl/Views/MainWindow.xaml b/PetWashControl/Views/MainWindow.xaml new file mode 100644 index 0000000..6959cf7 --- /dev/null +++ b/PetWashControl/Views/MainWindow.xaml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +