using Microsoft.EntityFrameworkCore; using PetWash.Api.Data; using PetWash.Api.Models; namespace PetWash.Api.Services; public class OrderService { private static readonly OrderStatus[] RetryablePaymentStatuses = [ OrderStatus.Created, OrderStatus.WaitingPayment, OrderStatus.Expired, OrderStatus.PaymentInitFailed ]; private readonly PetWashDbContext _context; private readonly MqttService _mqttService; private readonly ILogger _logger; public OrderService(PetWashDbContext context, MqttService mqttService, ILogger logger) { _context = context; _mqttService = mqttService; _logger = logger; } public async Task CreateOrderAsync(int packageId) { var package = await _context.Packages.FindAsync(packageId); if (package == null) throw new ArgumentException("套餐不存在"); var order = new Order { PackageId = packageId, Package = package, CreatedAt = DateTime.Now, Status = OrderStatus.Created, IsPaid = false, OutTradeNo = string.Empty, PaymentCodeUrl = string.Empty, PaymentExpiresAt = null, PaymentInitError = string.Empty }; _context.Orders.Add(order); await _context.SaveChangesAsync(); _logger.LogInformation($"订单创建成功: OrderId={order.Id}, PackageId={packageId}"); return order; } public async Task GetLatestRetryableOrderAsync(int packageId) { var retryCutoff = DateTime.Now.AddMinutes(-30); return await _context.Orders .Include(o => o.Package) .Where(o => o.PackageId == packageId && !o.IsPaid && o.CreatedAt >= retryCutoff && RetryablePaymentStatuses.Contains(o.Status)) .OrderByDescending(o => o.Id) .FirstOrDefaultAsync(); } public async Task MarkPaymentReadyAsync( int orderId, string outTradeNo, string codeUrl, DateTimeOffset? expiresAt) { var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId); if (order == null) { return null; } order.Status = OrderStatus.WaitingPayment; order.OutTradeNo = outTradeNo; order.PaymentCodeUrl = codeUrl; order.PaymentExpiresAt = expiresAt; order.PaymentInitError = string.Empty; await _context.SaveChangesAsync(); _logger.LogInformation( "订单支付初始化成功: OrderId={OrderId}, OutTradeNo={OutTradeNo}, ExpiresAt={ExpiresAt}", order.Id, order.OutTradeNo, order.PaymentExpiresAt); return order; } public async Task MarkPaymentInitializationFailedAsync(int orderId, string errorMessage) { var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId); if (order == null) { return null; } order.Status = OrderStatus.PaymentInitFailed; order.PaymentCodeUrl = string.Empty; order.PaymentExpiresAt = null; order.PaymentInitError = errorMessage; await _context.SaveChangesAsync(); _logger.LogWarning( "订单支付初始化失败: OrderId={OrderId}, Error={Error}", order.Id, errorMessage); return order; } public async Task ConfirmPaymentAsync(int orderId) { var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId); if (order == null) return null; if (order.IsPaid) { if (order.Status == OrderStatus.Paid) { await PublishOpenDoorCommandAsync(order); _logger.LogInformation("订单已支付但尚未开门,补发开门指令: OrderId={OrderId}", orderId); } _logger.LogInformation("Order payment already confirmed: OrderId={OrderId}", orderId); return order; } order.IsPaid = true; order.PaidAt = DateTime.Now; order.Status = OrderStatus.Paid; order.PaymentInitError = string.Empty; await _context.SaveChangesAsync(); await PublishOpenDoorCommandAsync(order); _logger.LogInformation($"订单支付成功,已发送开门指令: OrderId={orderId}"); return order; } public async Task UpdateOrderStatusAsync(int orderId, OrderStatus status) { var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId); if (order == null) return null; order.Status = status; if (status == OrderStatus.DoorClosed) { order.StartedAt = DateTime.Now; // 门关闭后,发送开始清洗指令 await _mqttService.PublishAsync("device/command", new { command = "start_wash", orderId = order.Id, durationMinutes = order.Package?.DurationMinutes ?? 15, timestamp = DateTime.Now }); } else if (status == OrderStatus.Completed) { order.CompletedAt = DateTime.Now; } else if (status == OrderStatus.Cancelled || status == OrderStatus.Expired) { order.PaymentCodeUrl = string.Empty; order.PaymentExpiresAt = null; } await _context.SaveChangesAsync(); _logger.LogInformation($"订单状态更新: OrderId={orderId}, Status={status}"); return order; } public async Task GetOrderAsync(int orderId) { return await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId); } public async Task EnsureOpenDoorCommandDispatchedAsync(int orderId) { var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId); if (order == null || !order.IsPaid || order.Status != OrderStatus.Paid) { return; } await PublishOpenDoorCommandAsync(order); _logger.LogInformation("轮询支付状态时补发开门指令: OrderId={OrderId}", orderId); } private async Task PublishOpenDoorCommandAsync(Order order) { await _mqttService.PublishAsync("device/command", new { command = "open_door", orderId = order.Id, timestamp = DateTime.Now }); } }