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 CreatePaymentAsync( PaymentProviderAccount account, CreatePaymentProviderRequest request, CancellationToken cancellationToken = default) { return CreatePaymentCoreAsync(account, request, cancellationToken); } public Task 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 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(body.GetRawText()); if (webhookEvent?.Resource is null) { return false; } var decrypted = client.DecryptEventResource(webhookEvent); payload = JsonSerializer.SerializeToElement(decrypted); _ = resource; return true; } catch { return false; } } private static bool TryGetHeader( IReadOnlyDictionary 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; } }