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 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 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 ToDictionary(JsonElement element) { var values = new Dictionary(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 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); }