Files
tiku-backend.net/Tiku.Infrastructure/Commerce/AlipayProvider.cs
2026-07-26 19:46:39 +08:00

184 lines
6.3 KiB
C#

using System.Text.Json;
using Aop.Api;
using Aop.Api.Domain;
using Aop.Api.Request;
using Aop.Api.Util;
using Tiku.Application.Commerce;
namespace Tiku.Infrastructure.Commerce;
internal sealed class AlipayProvider : IPaymentProvider
{
public string Provider => PaymentProviders.Alipay;
public Task<CreatePaymentProviderResult> CreatePaymentAsync(
PaymentProviderAccount account,
CreatePaymentProviderRequest request,
CancellationToken cancellationToken = default)
{
_ = cancellationToken;
var client = BuildClient(account);
var alipayRequest = new AlipayTradeWapPayRequest();
alipayRequest.SetNotifyUrl(request.NotifyUrl);
if (!string.IsNullOrWhiteSpace(request.ReturnUrl))
{
alipayRequest.SetReturnUrl(request.ReturnUrl);
}
alipayRequest.SetBizModel(new AlipayTradeWapPayModel
{
OutTradeNo = request.OrderNo,
Subject = request.Subject,
TotalAmount = FormatYuan(request.AmountCents),
ProductCode = "QUICK_WAP_WAY",
QuitUrl = request.QuitUrl
});
var response = client.pageExecute(alipayRequest);
if (string.IsNullOrWhiteSpace(response.Body))
{
throw new PaymentProviderException("Alipay create payment returned an empty body.", "alipay_create_failed");
}
var clientPayload = JsonSerializer.SerializeToElement(new
{
formHtml = response.Body
});
var rawPayload = JsonSerializer.SerializeToElement(new
{
response.Body,
request.OrderNo,
request.AmountCents
});
return Task.FromResult(new CreatePaymentProviderResult(
Provider,
request.Method,
"pending",
null,
clientPayload,
rawPayload));
}
public Task<PaymentNotificationResult> ParsePaymentNotificationAsync(
PaymentProviderAccount account,
PaymentNotificationRequest request,
CancellationToken cancellationToken = default)
{
_ = BuildClient(account);
var values = ToDictionary(request.Body);
var signatureValid = AlipaySignature.RSACheckV1(
values,
Required(account.ConfigPublic, "alipayPublicKey"),
GetString(account.ConfigPublic, "charset") ?? "UTF-8",
GetString(account.ConfigPublic, "signType") ?? "RSA2",
false);
var eventId = GetValue(values, "notify_id") ?? GetValue(values, "trade_no") ?? Guid.NewGuid().ToString("N");
var orderNo = GetValue(values, "out_trade_no")
?? throw new PaymentProviderException("Alipay notification order number is missing.", "alipay_notify_order_missing");
var tradeStatus = GetValue(values, "trade_status");
var amountCents = YuanToCents(GetValue(values, "total_amount") ?? GetValue(values, "receipt_amount"));
DateTimeOffset? paidAt = DateTimeOffset.TryParse(GetValue(values, "gmt_payment"), out var parsedPaidAt)
? parsedPaidAt
: null;
return Task.FromResult(new PaymentNotificationResult(
Provider,
"payment_notify",
eventId,
orderNo,
GetValue(values, "trade_no"),
amountCents,
string.Equals(tradeStatus, "TRADE_SUCCESS", StringComparison.OrdinalIgnoreCase) ||
string.Equals(tradeStatus, "TRADE_FINISHED", StringComparison.OrdinalIgnoreCase),
signatureValid,
paidAt,
request.Body));
}
private static DefaultAopClient BuildClient(PaymentProviderAccount account)
{
return new DefaultAopClient(
Required(account.ConfigPublic, "gatewayUrl", "gateway"),
Required(account.ConfigPublic, "appId"),
Required(account.SecretPayload, "privateKey", "appPrivateKey"),
"json",
"1.0",
"RSA2",
Required(account.ConfigPublic, "alipayPublicKey"),
"UTF-8",
false);
}
private static string Required(JsonElement element, params string[] keys)
{
if (element.ValueKind == JsonValueKind.Object)
{
foreach (var key in keys)
{
if (element.TryGetProperty(key, out var property) &&
property.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(property.GetString()))
{
return property.GetString()!;
}
}
}
throw new PaymentProviderException(
"Alipay provider configuration is incomplete.",
"alipay_config_incomplete");
}
private static Dictionary<string, string> ToDictionary(JsonElement element)
{
var values = new Dictionary<string, string>(StringComparer.Ordinal);
if (element.ValueKind != JsonValueKind.Object)
{
return values;
}
foreach (var property in element.EnumerateObject())
{
values[property.Name] = property.Value.ValueKind == JsonValueKind.String
? property.Value.GetString() ?? string.Empty
: property.Value.GetRawText();
}
return values;
}
private static string? GetString(JsonElement element, params string[] keys)
{
if (element.ValueKind != JsonValueKind.Object)
{
return null;
}
foreach (var key in keys)
{
if (element.TryGetProperty(key, out var property) &&
property.ValueKind == JsonValueKind.String)
{
return property.GetString();
}
}
return null;
}
private static string? GetValue(IReadOnlyDictionary<string, string> values, string key) =>
values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)
? value
: null;
private static int YuanToCents(string? value)
{
return decimal.TryParse(value, out var amount)
? (int)Math.Round(amount * 100, MidpointRounding.AwayFromZero)
: 0;
}
private static string FormatYuan(int cents) =>
(cents / 100m).ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);
}