166 lines
6.1 KiB
C#
166 lines
6.1 KiB
C#
using System.Globalization;
|
|
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)
|
|
{
|
|
return 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)
|
|
{
|
|
return (cents / 100m).ToString("0.00", CultureInfo.InvariantCulture);
|
|
}
|
|
} |