Files
tiku-backend.net/Tiku.Api/Controllers/CommerceController.cs

267 lines
9.9 KiB
C#

using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Tiku.Api.Contracts;
using Tiku.Api.Security;
using Tiku.Application.Commerce;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Domain.Commerce;
namespace Tiku.Api.Controllers;
[ApiController]
[Tags("学生端-交易")]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[Produces("application/json")]
[Route("api/student/commerce")]
public sealed class CommerceController(
ICommerceService commerceService,
IRefundAdministrationService refundAdministrationService,
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));
}
[AllowAnonymous]
[HttpPost("refunds/notify/wechat_pay")]
[EndpointSummary("微信退款结果回调")]
[ProducesResponseType<CommerceRefundRequest>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommerceRefundRequest>> WechatRefundNotify(
[FromQuery] string? tenantCode,
RefundNotificationDto request,
CancellationToken cancellationToken)
{
return Ok(await refundAdministrationService.ProcessRefundNotificationAsync(
await ResolveNotificationTenantAsync(tenantCode, cancellationToken),
request.ToCommand(PaymentProviders.WechatPay),
cancellationToken));
}
[AllowAnonymous]
[HttpPost("refunds/notify/alipay")]
[EndpointSummary("支付宝退款结果回调")]
[ProducesResponseType<CommerceRefundRequest>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommerceRefundRequest>> AlipayRefundNotify(
[FromQuery] string? tenantCode,
RefundNotificationDto request,
CancellationToken cancellationToken)
{
return Ok(await refundAdministrationService.ProcessRefundNotificationAsync(
await ResolveNotificationTenantAsync(tenantCode, cancellationToken),
request.ToCommand(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,
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);
}
}