forked from xiongyuxing/tiku-backend.net
243 lines
8.8 KiB
C#
243 lines
8.8 KiB
C#
using System.Text.Json;
|
|
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models;
|
|
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Events;
|
|
using SKIT.FlurlHttpClient.Wechat.TenpayV3;
|
|
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings;
|
|
using Tiku.Application.Commerce;
|
|
|
|
namespace Tiku.Infrastructure.Commerce;
|
|
|
|
internal sealed class WechatPayProvider : IPaymentProvider
|
|
{
|
|
public string Provider => PaymentProviders.WechatPay;
|
|
|
|
public Task<CreatePaymentProviderResult> CreatePaymentAsync(
|
|
PaymentProviderAccount account,
|
|
CreatePaymentProviderRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return CreatePaymentCoreAsync(account, request, cancellationToken);
|
|
}
|
|
|
|
public Task<PaymentNotificationResult> ParsePaymentNotificationAsync(
|
|
PaymentProviderAccount account,
|
|
PaymentNotificationRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var client = BuildClient(account);
|
|
var signatureValid = TryGetHeader(request.Headers, "Wechatpay-Timestamp", out var timestamp) &&
|
|
TryGetHeader(request.Headers, "Wechatpay-Nonce", out var nonce) &&
|
|
TryGetHeader(request.Headers, "Wechatpay-Signature", out var signature) &&
|
|
TryGetHeader(request.Headers, "Wechatpay-Serial", out var serial) &&
|
|
client.VerifyEventSignature(timestamp, nonce, request.RawBody, signature, serial);
|
|
var payload = TryDecryptTransactionResource(client, request.Body, out var decrypted)
|
|
? decrypted
|
|
: request.Body;
|
|
var eventId = GetString(request.Body, "id") ?? GetString(payload, "transaction_id") ?? Guid.NewGuid().ToString("N");
|
|
var orderNo = GetString(payload, "out_trade_no", "outTradeNo")
|
|
?? throw new PaymentProviderException("WeChat Pay notification order number is missing.", "wechat_pay_notify_order_missing");
|
|
var tradeNo = GetString(payload, "transaction_id", "transactionId");
|
|
var tradeState = GetString(payload, "trade_state", "tradeState") ?? GetString(request.Body, "event_type");
|
|
var amount = GetInt(payload, "amount", "total") ?? GetInt(payload, "amountCents") ?? 0;
|
|
var paidAt = GetDateTimeOffset(payload, "success_time", "successTime");
|
|
|
|
return Task.FromResult(new PaymentNotificationResult(
|
|
Provider,
|
|
GetString(request.Body, "event_type") ?? "TRANSACTION.SUCCESS",
|
|
eventId,
|
|
orderNo,
|
|
tradeNo,
|
|
amount,
|
|
string.Equals(tradeState, "SUCCESS", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(tradeState, "TRANSACTION.SUCCESS", StringComparison.OrdinalIgnoreCase),
|
|
signatureValid,
|
|
paidAt,
|
|
payload));
|
|
}
|
|
|
|
private static WechatTenpayClient BuildClient(PaymentProviderAccount account)
|
|
{
|
|
var options = new WechatTenpayClientOptions
|
|
{
|
|
MerchantId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid"),
|
|
MerchantV3Secret = Required(account.SecretPayload, "apiV3Key", "merchantV3Secret"),
|
|
MerchantCertificateSerialNumber = Required(account.ConfigPublic, "certificateSerialNumber", "serialNo"),
|
|
MerchantCertificatePrivateKey = Required(account.SecretPayload, "privateKey", "merchantCertificatePrivateKey"),
|
|
PlatformAuthScheme = PlatformAuthScheme.PublicKey,
|
|
PlatformPublicKeyManager = new InMemoryPublicKeyManager()
|
|
};
|
|
|
|
return WechatTenpayClientBuilder.Create(options).Build();
|
|
}
|
|
|
|
private async Task<CreatePaymentProviderResult> CreatePaymentCoreAsync(
|
|
PaymentProviderAccount account,
|
|
CreatePaymentProviderRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.OpenId))
|
|
{
|
|
throw new PaymentProviderException("WeChat Pay JSAPI requires openId.", "wechat_pay_openid_required");
|
|
}
|
|
|
|
var client = BuildClient(account);
|
|
var appId = Required(account.ConfigPublic, "appId");
|
|
var wxRequest = new CreatePayTransactionJsapiRequest
|
|
{
|
|
AppId = appId,
|
|
Description = request.Subject,
|
|
OutTradeNumber = request.OrderNo,
|
|
NotifyUrl = request.NotifyUrl,
|
|
Amount = new CreatePayTransactionJsapiRequest.Types.Amount
|
|
{
|
|
Total = request.AmountCents
|
|
},
|
|
Payer = new CreatePayTransactionJsapiRequest.Types.Payer
|
|
{
|
|
OpenId = request.OpenId
|
|
}
|
|
};
|
|
var response = await client.ExecuteCreatePayTransactionJsapiAsync(wxRequest, cancellationToken);
|
|
if (!response.IsSuccessful())
|
|
{
|
|
throw new PaymentProviderException(
|
|
$"WeChat Pay create transaction failed: {response.ErrorCode}",
|
|
"wechat_pay_create_failed");
|
|
}
|
|
|
|
var clientPayload = JsonSerializer.SerializeToElement(
|
|
client.GenerateParametersForJsapiPayRequest(appId, response.PrepayId));
|
|
var rawPayload = JsonSerializer.SerializeToElement(new
|
|
{
|
|
response.PrepayId,
|
|
request.OrderNo,
|
|
request.AmountCents
|
|
});
|
|
|
|
return new CreatePaymentProviderResult(
|
|
Provider,
|
|
request.Method,
|
|
"pending",
|
|
null,
|
|
clientPayload,
|
|
rawPayload);
|
|
}
|
|
|
|
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(
|
|
"WeChat Pay provider configuration is incomplete.",
|
|
"wechat_pay_config_incomplete");
|
|
}
|
|
|
|
private static bool TryDecryptTransactionResource(
|
|
WechatTenpayClient client,
|
|
JsonElement body,
|
|
out JsonElement payload)
|
|
{
|
|
payload = default;
|
|
try
|
|
{
|
|
if (body.ValueKind != JsonValueKind.Object ||
|
|
!body.TryGetProperty("resource", out var resource))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var webhookEvent = JsonSerializer.Deserialize<WechatTenpayEvent>(body.GetRawText());
|
|
if (webhookEvent?.Resource is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var decrypted = client.DecryptEventResource<TransactionResource>(webhookEvent);
|
|
payload = JsonSerializer.SerializeToElement(decrypted);
|
|
_ = resource;
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryGetHeader(
|
|
IReadOnlyDictionary<string, string> headers,
|
|
string name,
|
|
out string value)
|
|
{
|
|
foreach (var pair in headers)
|
|
{
|
|
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
value = pair.Value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
value = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
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 int? GetInt(JsonElement element, string parentKey, string childKey)
|
|
{
|
|
if (element.ValueKind == JsonValueKind.Object &&
|
|
element.TryGetProperty(parentKey, out var parent) &&
|
|
parent.ValueKind == JsonValueKind.Object &&
|
|
parent.TryGetProperty(childKey, out var child) &&
|
|
child.TryGetInt32(out var value))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static int? GetInt(JsonElement element, string key)
|
|
{
|
|
return element.ValueKind == JsonValueKind.Object &&
|
|
element.TryGetProperty(key, out var property) &&
|
|
property.TryGetInt32(out var value)
|
|
? value
|
|
: null;
|
|
}
|
|
|
|
private static DateTimeOffset? GetDateTimeOffset(JsonElement element, params string[] keys)
|
|
{
|
|
var value = GetString(element, keys);
|
|
return DateTimeOffset.TryParse(value, out var parsed) ? parsed : null;
|
|
}
|
|
}
|