feat: add commerce order and payment checkout

This commit is contained in:
xiong
2026-07-26 19:34:00 +08:00
parent 5503d7a644
commit 494039973d
8 changed files with 1045 additions and 1 deletions

View File

@@ -0,0 +1,79 @@
using System.ComponentModel.DataAnnotations;
using Tiku.Application.Commerce;
namespace Tiku.Api.Contracts;
public sealed class CreateCommerceOrderDto
{
[Required]
public Guid PlanId { get; set; }
[Range(1, 99)]
public int Quantity { get; set; } = 1;
[StringLength(50)]
public string? PayMethod { get; set; }
[StringLength(50)]
public string? PayProvider { get; set; }
public Guid? RegionId { get; set; }
[StringLength(100)]
public string? CouponCode { get; set; }
public CreateCommerceOrderCommand ToCommand()
{
return new CreateCommerceOrderCommand(
PlanId,
Quantity,
PayMethod,
PayProvider,
RegionId,
CouponCode);
}
}
public sealed class CommerceOrderQueryDto
{
[Range(1, 100)]
public int? Limit { get; set; }
[StringLength(32)]
public string? Status { get; set; }
}
public sealed class CreateCommercePaymentDto
{
[Required]
[StringLength(100)]
public string OrderNo { get; set; } = string.Empty;
[Required]
[StringLength(50)]
public string Provider { get; set; } = string.Empty;
[Required]
[StringLength(50)]
public string Method { get; set; } = string.Empty;
[StringLength(255)]
public string? OpenId { get; set; }
[StringLength(2048)]
public string? ReturnUrl { get; set; }
[StringLength(2048)]
public string? QuitUrl { get; set; }
public CreateCommercePaymentCommand ToCommand()
{
return new CreateCommercePaymentCommand(
OrderNo,
Provider,
Method,
OpenId,
ReturnUrl,
QuitUrl);
}
}

View File

@@ -0,0 +1,90 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Commerce;
using Tiku.Application.Security;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Produces("application/json")]
[Route("api/commerce")]
public sealed class CommerceController(
ICommerceService commerceService,
ICurrentUser currentUser,
ICurrentTenant currentTenant) : ControllerBase
{
[HttpPost("orders")]
[EndpointSummary("创建学生端订单")]
[ProducesResponseType<CommerceOrderItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommerceOrderItem>> CreateOrder(
CreateCommerceOrderDto request,
CancellationToken cancellationToken)
{
return Ok(await commerceService.CreateOrderAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpGet("orders")]
[EndpointSummary("查询当前用户订单")]
[ProducesResponseType<CommerceOrderList>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommerceOrderList>> Orders(
[FromQuery] CommerceOrderQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await commerceService.GetOrdersAsync(
ResolveActor(),
new CommerceOrderQuery(query.Limit, query.Status),
cancellationToken));
}
[HttpGet("orders/{orderNo}")]
[EndpointSummary("查询当前用户订单详情")]
[ProducesResponseType<CommerceOrderItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommerceOrderItem>> Order(
string orderNo,
CancellationToken cancellationToken)
{
return Ok(await commerceService.GetOrderAsync(
ResolveActor(),
orderNo,
cancellationToken));
}
[HttpPost("payments")]
[EndpointSummary("创建订单支付")]
[ProducesResponseType<CommercePaymentItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommercePaymentItem>> CreatePayment(
CreateCommercePaymentDto request,
CancellationToken cancellationToken)
{
return Ok(await commerceService.CreatePaymentAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpGet("entitlements/current")]
[EndpointSummary("查询当前用户权益")]
[ProducesResponseType<CurrentEntitlementItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<CurrentEntitlementItem>> CurrentEntitlement(
CancellationToken cancellationToken)
{
return Ok(await commerceService.GetCurrentEntitlementAsync(
ResolveActor(),
cancellationToken));
}
private CommerceActor ResolveActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
{
throw new CommerceException("Current commerce actor was not resolved.", "commerce_access_denied");
}
return new CommerceActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
}
}

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Controllers;
using Tiku.Application.Assets;
using Tiku.Application.Auth;
using Tiku.Application.Commerce;
using Tiku.Application.Content;
using Tiku.Application.Storage;
using Tiku.Infrastructure.Content;
@@ -172,6 +173,26 @@ public sealed class ExceptionHandlingMiddleware(
return;
}
if (exception is CommerceException commerceException)
{
await WriteProblemAsync(
context,
commerceException.Message,
CommerceStatusCode(commerceException.Code),
commerceException.Code);
return;
}
if (exception is PaymentProviderException paymentProviderException)
{
await WriteProblemAsync(
context,
paymentProviderException.Message,
CommerceStatusCode(paymentProviderException.Code),
paymentProviderException.Code);
return;
}
if (exception is ObjectStorageException storageException)
{
await WriteProblemAsync(
@@ -308,4 +329,16 @@ public sealed class ExceptionHandlingMiddleware(
_ => StatusCodes.Status400BadRequest
};
}
private static int CommerceStatusCode(string code)
{
return code switch
{
"commerce_access_denied" or "tenant_access_denied" => StatusCodes.Status403Forbidden,
"order_not_found" or "svip_plan_not_found" or "region_not_found" => StatusCodes.Status404NotFound,
"payment_provider_not_configured" or "payment_secret_not_configured" => StatusCodes.Status503ServiceUnavailable,
"order_status_invalid" => StatusCodes.Status409Conflict,
_ => StatusCodes.Status400BadRequest
};
}
}