forked from gongxuegit/tiku-backend.net
feat: add commerce order and payment checkout
This commit is contained in:
79
Tiku.Api/Contracts/CommerceDtos.cs
Normal file
79
Tiku.Api/Contracts/CommerceDtos.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
90
Tiku.Api/Controllers/CommerceController.cs
Normal file
90
Tiku.Api/Controllers/CommerceController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user