244 lines
8.5 KiB
C#
244 lines
8.5 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.WebUtilities;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Tiku.Api.Contracts;
|
|
using Tiku.Application.Commerce;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Application.Tenancy;
|
|
|
|
namespace Tiku.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
|
[Produces("application/json")]
|
|
[Route("api/commerce")]
|
|
public sealed class CommerceController(
|
|
ICommerceService commerceService,
|
|
ICurrentUser currentUser,
|
|
ITenantContext currentTenant,
|
|
ITenantContextInitializer tenantInitializer,
|
|
ITenantDirectory tenantDirectory) : 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));
|
|
}
|
|
|
|
[HttpPost("coupons/claim")]
|
|
[EndpointSummary("领取优惠券")]
|
|
[ProducesResponseType<CommerceCouponItem>(StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<CommerceCouponItem>> ClaimCoupon(
|
|
ClaimCommerceCouponDto request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Ok(await commerceService.ClaimCouponAsync(
|
|
ResolveActor(),
|
|
request.ToCommand(),
|
|
cancellationToken));
|
|
}
|
|
|
|
[HttpGet("coupons")]
|
|
[EndpointSummary("查询当前用户优惠券")]
|
|
[ProducesResponseType<CommerceCouponList>(StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<CommerceCouponList>> Coupons(
|
|
[FromQuery] CommerceCouponQueryDto query,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Ok(await commerceService.GetCouponsAsync(
|
|
ResolveActor(),
|
|
query.ToQuery(),
|
|
cancellationToken));
|
|
}
|
|
|
|
[HttpPost("coupons/check")]
|
|
[EndpointSummary("校验优惠券并预览订单金额")]
|
|
[ProducesResponseType<CommerceCouponCheckResult>(StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<CommerceCouponCheckResult>> CheckCoupon(
|
|
CheckCommerceCouponDto request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Ok(await commerceService.CheckCouponAsync(
|
|
ResolveActor(),
|
|
request.ToCommand(),
|
|
cancellationToken));
|
|
}
|
|
|
|
[AllowAnonymous]
|
|
[HttpPost("payments/notify/wechat-pay")]
|
|
[EndpointSummary("微信支付回调")]
|
|
[ProducesResponseType<PaymentNotificationProcessResult>(StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<PaymentNotificationProcessResult>> WechatPayNotify(
|
|
[FromQuery] string? tenantCode,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Ok(await ProcessNotificationAsync(
|
|
await ResolveNotificationTenantAsync(tenantCode, cancellationToken),
|
|
PaymentProviders.WechatPay,
|
|
cancellationToken));
|
|
}
|
|
|
|
[AllowAnonymous]
|
|
[HttpPost("payments/notify/alipay")]
|
|
[EndpointSummary("支付宝支付回调")]
|
|
[ProducesResponseType<PaymentNotificationProcessResult>(StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<PaymentNotificationProcessResult>> AlipayNotify(
|
|
[FromQuery] string? tenantCode,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Ok(await ProcessNotificationAsync(
|
|
await ResolveNotificationTenantAsync(tenantCode, cancellationToken),
|
|
PaymentProviders.Alipay,
|
|
cancellationToken));
|
|
}
|
|
|
|
private async Task<Guid> ResolveNotificationTenantAsync(
|
|
string? tenantCode,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (currentTenant.TenantId.HasValue)
|
|
{
|
|
return currentTenant.TenantId.Value;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(tenantCode))
|
|
{
|
|
throw new CommerceException("Tenant code is required for payment notification.", "tenant_required");
|
|
}
|
|
|
|
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
|
?? throw new CommerceException("Tenant was not found.", "tenant_not_found");
|
|
tenantInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode);
|
|
return tenant.TenantId;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private async Task<PaymentNotificationProcessResult> ProcessNotificationAsync(
|
|
Guid tenantId,
|
|
string provider,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (tenantId == Guid.Empty)
|
|
{
|
|
throw new CommerceException("Tenant id is required for payment notification.", "tenant_required");
|
|
}
|
|
|
|
var rawBody = await ReadRawBodyAsync(Request, cancellationToken);
|
|
using var body = ParseNotificationBody(Request, rawBody, cancellationToken);
|
|
var headers = Request.Headers.ToDictionary(
|
|
pair => pair.Key,
|
|
pair => pair.Value.ToString(),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
|
|
return await commerceService.ProcessPaymentNotificationAsync(
|
|
tenantId,
|
|
provider,
|
|
headers,
|
|
rawBody,
|
|
body.RootElement.Clone(),
|
|
cancellationToken);
|
|
}
|
|
|
|
private static async Task<string> ReadRawBodyAsync(HttpRequest request, CancellationToken cancellationToken)
|
|
{
|
|
using var reader = new StreamReader(
|
|
request.Body,
|
|
Encoding.UTF8,
|
|
detectEncodingFromByteOrderMarks: false,
|
|
leaveOpen: false);
|
|
return await reader.ReadToEndAsync(cancellationToken);
|
|
}
|
|
|
|
private static JsonDocument ParseNotificationBody(
|
|
HttpRequest request,
|
|
string rawBody,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (request.HasFormContentType)
|
|
{
|
|
_ = cancellationToken;
|
|
var form = QueryHelpers.ParseQuery(rawBody);
|
|
var values = form.ToDictionary(
|
|
pair => pair.Key,
|
|
pair => pair.Value.ToString(),
|
|
StringComparer.Ordinal);
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(values));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(rawBody))
|
|
{
|
|
return JsonDocument.Parse("{}");
|
|
}
|
|
|
|
return JsonDocument.Parse(rawBody);
|
|
}
|
|
}
|