2026-02-25 15:41:00 +08:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using PetWash.Api.Models;
|
|
|
|
|
using PetWash.Api.Services;
|
|
|
|
|
|
|
|
|
|
namespace PetWash.Api.Controllers;
|
|
|
|
|
|
|
|
|
|
[ApiController]
|
|
|
|
|
[Route("api/[controller]")]
|
|
|
|
|
public class OrdersController : ControllerBase
|
|
|
|
|
{
|
|
|
|
|
private readonly OrderService _orderService;
|
2026-03-16 15:38:08 +08:00
|
|
|
private readonly WeChatPayService _weChatPayService;
|
2026-02-25 15:41:00 +08:00
|
|
|
|
2026-03-16 15:38:08 +08:00
|
|
|
public OrdersController(OrderService orderService, WeChatPayService weChatPayService)
|
2026-02-25 15:41:00 +08:00
|
|
|
{
|
|
|
|
|
_orderService = orderService;
|
2026-03-16 15:38:08 +08:00
|
|
|
_weChatPayService = weChatPayService;
|
2026-02-25 15:41:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
|
|
|
|
|
{
|
2026-03-20 16:23:56 +08:00
|
|
|
Order? order = null;
|
|
|
|
|
|
2026-02-25 15:41:00 +08:00
|
|
|
try
|
|
|
|
|
{
|
2026-03-20 16:23:56 +08:00
|
|
|
order = await _orderService.GetLatestRetryableOrderAsync(request.PackageId);
|
|
|
|
|
if (order is not null && HasActivePayment(order))
|
|
|
|
|
{
|
|
|
|
|
return Ok(ToCreateOrderResponse(order));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
order ??= await _orderService.CreateOrderAsync(request.PackageId);
|
2026-03-16 15:38:08 +08:00
|
|
|
var payment = await _weChatPayService.CreateNativePayAsync(order, HttpContext.RequestAborted);
|
2026-03-20 16:23:56 +08:00
|
|
|
order = await _orderService.MarkPaymentReadyAsync(
|
|
|
|
|
order.Id,
|
|
|
|
|
payment.OutTradeNo,
|
|
|
|
|
payment.CodeUrl,
|
|
|
|
|
payment.ExpiresAt) ?? order;
|
2026-03-16 15:38:08 +08:00
|
|
|
|
2026-03-20 16:23:56 +08:00
|
|
|
return Ok(ToCreateOrderResponse(order));
|
2026-02-25 15:41:00 +08:00
|
|
|
}
|
|
|
|
|
catch (ArgumentException ex)
|
|
|
|
|
{
|
|
|
|
|
return BadRequest(ex.Message);
|
|
|
|
|
}
|
2026-03-16 15:38:08 +08:00
|
|
|
catch (InvalidOperationException ex)
|
|
|
|
|
{
|
2026-03-20 16:23:56 +08:00
|
|
|
if (order != null)
|
|
|
|
|
{
|
|
|
|
|
await _orderService.MarkPaymentInitializationFailedAsync(order.Id, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 15:38:08 +08:00
|
|
|
return StatusCode(StatusCodes.Status502BadGateway, ex.Message);
|
|
|
|
|
}
|
2026-02-25 15:41:00 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 17:48:40 +08:00
|
|
|
[HttpPost("{id}/payment-qrcode")]
|
|
|
|
|
public async Task<IActionResult> CreatePaymentQrCode(int id)
|
|
|
|
|
{
|
|
|
|
|
var order = await _orderService.GetOrderAsync(id);
|
|
|
|
|
if (order == null)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 16:23:56 +08:00
|
|
|
if (HasActivePayment(order))
|
|
|
|
|
{
|
|
|
|
|
return Ok(ToCreateOrderResponse(order));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 17:48:40 +08:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var payment = await _weChatPayService.CreateNativePayAsync(order, HttpContext.RequestAborted);
|
2026-03-20 16:23:56 +08:00
|
|
|
order = await _orderService.MarkPaymentReadyAsync(
|
|
|
|
|
order.Id,
|
|
|
|
|
payment.OutTradeNo,
|
|
|
|
|
payment.CodeUrl,
|
|
|
|
|
payment.ExpiresAt) ?? order;
|
2026-03-18 17:48:40 +08:00
|
|
|
|
2026-03-20 16:23:56 +08:00
|
|
|
return Ok(ToCreateOrderResponse(order));
|
2026-03-18 17:48:40 +08:00
|
|
|
}
|
|
|
|
|
catch (InvalidOperationException ex)
|
|
|
|
|
{
|
2026-03-20 16:23:56 +08:00
|
|
|
await _orderService.MarkPaymentInitializationFailedAsync(order.Id, ex.Message);
|
2026-03-18 17:48:40 +08:00
|
|
|
return StatusCode(StatusCodes.Status502BadGateway, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 15:41:00 +08:00
|
|
|
[HttpPost("{id}/payment")]
|
|
|
|
|
public async Task<IActionResult> ConfirmPayment(int id)
|
|
|
|
|
{
|
|
|
|
|
var order = await _orderService.ConfirmPaymentAsync(id);
|
|
|
|
|
if (order == null)
|
|
|
|
|
return NotFound();
|
|
|
|
|
|
2026-03-04 17:51:35 +08:00
|
|
|
return Ok(order);
|
2026-02-25 15:41:00 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-16 15:38:08 +08:00
|
|
|
[HttpGet("{id}/payment-status")]
|
|
|
|
|
public async Task<IActionResult> GetPaymentStatus(int id, [FromQuery] string outTradeNo)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(outTradeNo))
|
|
|
|
|
{
|
|
|
|
|
return BadRequest("outTradeNo is required.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var order = await _orderService.GetOrderAsync(id);
|
|
|
|
|
if (order == null)
|
|
|
|
|
{
|
|
|
|
|
return NotFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (order.IsPaid)
|
|
|
|
|
{
|
2026-03-20 16:23:56 +08:00
|
|
|
if (order.Status == OrderStatus.Paid)
|
|
|
|
|
{
|
|
|
|
|
await _orderService.EnsureOpenDoorCommandDispatchedAsync(id);
|
|
|
|
|
order = await _orderService.GetOrderAsync(id) ?? order;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 15:38:08 +08:00
|
|
|
return Ok(new PaymentStatusResponse
|
|
|
|
|
{
|
|
|
|
|
Order = order,
|
|
|
|
|
IsPaid = true,
|
|
|
|
|
TradeState = "SUCCESS",
|
|
|
|
|
OutTradeNo = outTradeNo,
|
|
|
|
|
Message = "Order already confirmed."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var payment = await _weChatPayService.QueryOrderByOutTradeNoAsync(outTradeNo, HttpContext.RequestAborted);
|
|
|
|
|
if (string.Equals(payment.TradeState, "SUCCESS", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
order = await _orderService.ConfirmPaymentAsync(id) ?? order;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Ok(new PaymentStatusResponse
|
|
|
|
|
{
|
|
|
|
|
Order = order,
|
|
|
|
|
IsPaid = order.IsPaid,
|
|
|
|
|
TradeState = payment.TradeState,
|
|
|
|
|
OutTradeNo = payment.OutTradeNo,
|
|
|
|
|
TransactionId = payment.TransactionId,
|
|
|
|
|
Message = order.IsPaid ? "Payment confirmed." : "Waiting for payment."
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (InvalidOperationException ex)
|
|
|
|
|
{
|
|
|
|
|
return StatusCode(StatusCodes.Status502BadGateway, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 15:41:00 +08:00
|
|
|
[HttpGet("{id}")]
|
|
|
|
|
public async Task<IActionResult> GetOrder(int id)
|
|
|
|
|
{
|
|
|
|
|
var order = await _orderService.GetOrderAsync(id);
|
|
|
|
|
if (order == null)
|
|
|
|
|
return NotFound();
|
|
|
|
|
|
|
|
|
|
return Ok(order);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPut("{id}/status")]
|
|
|
|
|
public async Task<IActionResult> UpdateStatus(int id, [FromBody] UpdateStatusRequest request)
|
|
|
|
|
{
|
|
|
|
|
var order = await _orderService.UpdateOrderStatusAsync(id, request.Status);
|
|
|
|
|
if (order == null)
|
|
|
|
|
return NotFound();
|
|
|
|
|
|
|
|
|
|
return Ok(order);
|
|
|
|
|
}
|
2026-03-20 16:23:56 +08:00
|
|
|
|
|
|
|
|
private static bool HasActivePayment(Order? order)
|
|
|
|
|
{
|
|
|
|
|
return order is
|
|
|
|
|
{
|
|
|
|
|
IsPaid: false,
|
|
|
|
|
Status: OrderStatus.WaitingPayment
|
|
|
|
|
} &&
|
|
|
|
|
!string.IsNullOrWhiteSpace(order.PaymentCodeUrl) &&
|
|
|
|
|
!string.IsNullOrWhiteSpace(order.OutTradeNo) &&
|
|
|
|
|
order.PaymentExpiresAt is not null &&
|
|
|
|
|
order.PaymentExpiresAt > DateTimeOffset.UtcNow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static CreateOrderResponse ToCreateOrderResponse(Order order)
|
|
|
|
|
{
|
|
|
|
|
return new CreateOrderResponse
|
|
|
|
|
{
|
|
|
|
|
Order = order,
|
|
|
|
|
CodeUrl = order.PaymentCodeUrl,
|
|
|
|
|
OutTradeNo = order.OutTradeNo,
|
|
|
|
|
ExpiresAt = order.PaymentExpiresAt
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-02-25 15:41:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public record CreateOrderRequest(int PackageId);
|
|
|
|
|
public record UpdateStatusRequest(OrderStatus Status);
|