feat: add payment notifications and entitlement fulfillment

This commit is contained in:
xiong
2026-07-26 19:38:53 +08:00
parent 494039973d
commit 36b64dcf70
6 changed files with 570 additions and 13 deletions

View File

@@ -1,5 +1,8 @@
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;
@@ -78,6 +81,28 @@ public sealed class CommerceController(
cancellationToken));
}
[AllowAnonymous]
[HttpPost("payments/notify/wechat-pay")]
[EndpointSummary("微信支付回调")]
[ProducesResponseType<PaymentNotificationProcessResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<PaymentNotificationProcessResult>> WechatPayNotify(
[FromQuery] Guid tenantId,
CancellationToken cancellationToken)
{
return Ok(await ProcessNotificationAsync(tenantId, PaymentProviders.WechatPay, cancellationToken));
}
[AllowAnonymous]
[HttpPost("payments/notify/alipay")]
[EndpointSummary("支付宝支付回调")]
[ProducesResponseType<PaymentNotificationProcessResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<PaymentNotificationProcessResult>> AlipayNotify(
[FromQuery] Guid tenantId,
CancellationToken cancellationToken)
{
return Ok(await ProcessNotificationAsync(tenantId, PaymentProviders.Alipay, cancellationToken));
}
private CommerceActor ResolveActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
@@ -87,4 +112,64 @@ public sealed class CommerceController(
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);
}
}