using System.Text.Json; using Senparc.Weixin; using Senparc.Weixin.Entities; using Senparc.Weixin.TenPayV3; using Senparc.Weixin.TenPayV3.Apis; using Senparc.Weixin.TenPayV3.Apis.BasePay; using Senparc.Weixin.TenPayV3.Helpers; 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) { cancellationToken.ThrowIfCancellationRequested(); _ = BuildTenPaySettings(account); var signatureValid = HasWechatPaySignatureHeaders(request.Headers); var payload = 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 WechatPaySettings BuildTenPaySettings(PaymentProviderAccount account) { return new WechatPaySettings { TenPayV3_AppId = Required(account.ConfigPublic, "appId"), TenPayV3_AppSecret = Optional(account.SecretPayload, "appSecret") ?? string.Empty, TenPayV3_MchId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid"), TenPayV3_Key = Optional(account.SecretPayload, "apiKey", "key") ?? string.Empty, TenPayV3_CertPath = Optional(account.SecretPayload, "certPath") ?? string.Empty, TenPayV3_CertSecret = Optional(account.SecretPayload, "certSecret") ?? string.Empty, TenPayV3_TenpayNotify = Optional(account.ConfigPublic, "notifyUrl") ?? string.Empty, TenPayV3_WxOpenTenpayNotify = Optional(account.ConfigPublic, "wxOpenNotifyUrl") ?? string.Empty, TenPayV3_PrivateKey = Required(account.SecretPayload, "privateKey", "merchantCertificatePrivateKey"), TenPayV3_SerialNumber = Required(account.ConfigPublic, "certificateSerialNumber", "serialNo"), TenPayV3_APIv3Key = Required(account.SecretPayload, "apiV3Key", "merchantV3Secret"), TenPayV3_TenPayPubKey = Optional(account.ConfigPublic, "tenPayPublicKey") ?? string.Empty, TenPayV3_TenPayPubKeyID = Optional(account.ConfigPublic, "tenPayPublicKeyId") ?? string.Empty, TenPayV3_TenPayPubKeyEnable = !string.IsNullOrWhiteSpace(Optional(account.ConfigPublic, "tenPayPublicKey")), EncryptionType = CertType.RSA }; } 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 tenPaySettings = BuildTenPaySettings(account); var appId = Required(account.ConfigPublic, "appId"); var mchId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid"); var payApis = new BasePayApis(tenPaySettings); var wxRequest = new TransactionsRequestData( appId, mchId, request.Subject, request.OrderNo, null, null, request.NotifyUrl, null, new TransactionsRequestData.Amount(request.AmountCents, "CNY"), new TransactionsRequestData.Payer(request.OpenId), null, null, null, false); object response = string.Equals(request.Method, "h5", StringComparison.OrdinalIgnoreCase) ? await payApis.H5Async(wxRequest).WaitAsync(cancellationToken) : await payApis.JsApiAsync(wxRequest).WaitAsync(cancellationToken); var rawPayload = JsonSerializer.SerializeToElement(response); var prepayId = GetPropertyValue(response, "prepay_id", "PrepayId"); var h5Url = GetPropertyValue(response, "h5_url", "H5Url"); if (string.IsNullOrWhiteSpace(prepayId) && string.IsNullOrWhiteSpace(h5Url)) { throw new PaymentProviderException( "WeChat Pay create transaction failed.", "wechat_pay_create_failed"); } var clientPayload = string.IsNullOrWhiteSpace(prepayId) ? JsonSerializer.SerializeToElement(new { h5Url }) : JsonSerializer.SerializeToElement(TenPaySignHelper.GetJsApiUiPackage(appId, prepayId, tenPaySettings)); var providerPayload = JsonSerializer.SerializeToElement(new { PrepayId = prepayId, H5Url = h5Url, request.OrderNo, request.AmountCents }); return new CreatePaymentProviderResult( Provider, request.Method, "pending", null, clientPayload, MergeRawPayload(rawPayload, providerPayload)); } 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 string? Optional(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(); } } } return null; } private static bool HasWechatPaySignatureHeaders(IReadOnlyDictionary headers) { return ContainsHeader(headers, "Wechatpay-Timestamp") && ContainsHeader(headers, "Wechatpay-Nonce") && ContainsHeader(headers, "Wechatpay-Signature") && ContainsHeader(headers, "Wechatpay-Serial"); } private static bool ContainsHeader(IReadOnlyDictionary headers, string name) { return headers.Keys.Any(key => string.Equals(key, name, StringComparison.OrdinalIgnoreCase)); } private static T? GetPropertyValue(object value, params string[] names) { var type = value.GetType(); foreach (var name in names) { var property = type.GetProperty(name); if (property?.GetValue(value) is T typed) { return typed; } } return default; } private static JsonElement MergeRawPayload(JsonElement response, JsonElement summary) { return JsonSerializer.SerializeToElement(new { response, summary }); } private sealed class WechatPaySettings : ISenparcWeixinSettingForTenpayV3 { public string ItemKey { get; set; } = string.Empty; public string TenPayV3_MchId { get; set; } = string.Empty; public string TenPayV3_SubMchId { get; set; } = string.Empty; public string TenPayV3_Key { get; set; } = string.Empty; public string TenPayV3_CertPath { get; set; } = string.Empty; public string TenPayV3_CertSecret { get; set; } = string.Empty; public string TenPayV3_AppId { get; set; } = string.Empty; public string TenPayV3_AppSecret { get; set; } = string.Empty; public string TenPayV3_SubAppId { get; set; } = string.Empty; public string TenPayV3_SubAppSecret { get; set; } = string.Empty; public string TenPayV3_TenpayNotify { get; set; } = string.Empty; public string TenPayV3_PrivateKey { get; set; } = string.Empty; public string TenPayV3_SerialNumber { get; set; } = string.Empty; public string TenPayV3_APIv3Key { get; set; } = string.Empty; public string TenPayV3_TenPayPubKey { get; set; } = string.Empty; public string TenPayV3_TenPayPubKeyID { get; set; } = string.Empty; public bool TenPayV3_TenPayPubKeyEnable { get; set; } public string TenPayV3_WxOpenTenpayNotify { get; set; } = string.Empty; public CertType? EncryptionType { get; set; } } 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; } }