208 lines
6.6 KiB
C#
208 lines
6.6 KiB
C#
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<OrderService> _logger;
|
|
|
|
public OrderService(PetWashDbContext context, MqttService mqttService, ILogger<OrderService> logger)
|
|
{
|
|
_context = context;
|
|
_mqttService = mqttService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<Order> 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<Order?> 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<Order?> 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<Order?> 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<Order?> 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<Order?> 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<Order?> 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
|
|
});
|
|
}
|
|
}
|