forked from gongxuegit/tiku-backend.net
feat: add phase five operations foundation
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
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 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;
|
||||
@@ -24,15 +26,10 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
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;
|
||||
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");
|
||||
@@ -55,19 +52,26 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
payload));
|
||||
}
|
||||
|
||||
private static WechatTenpayClient BuildClient(PaymentProviderAccount account)
|
||||
private static WechatPaySettings BuildTenPaySettings(PaymentProviderAccount account)
|
||||
{
|
||||
var options = new WechatTenpayClientOptions
|
||||
return new WechatPaySettings
|
||||
{
|
||||
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()
|
||||
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
|
||||
};
|
||||
|
||||
return WechatTenpayClientBuilder.Create(options).Build();
|
||||
}
|
||||
|
||||
private async Task<CreatePaymentProviderResult> CreatePaymentCoreAsync(
|
||||
@@ -80,36 +84,45 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
throw new PaymentProviderException("WeChat Pay JSAPI requires openId.", "wechat_pay_openid_required");
|
||||
}
|
||||
|
||||
var client = BuildClient(account);
|
||||
var tenPaySettings = BuildTenPaySettings(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())
|
||||
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<string?>(response, "prepay_id", "PrepayId");
|
||||
var h5Url = GetPropertyValue<string?>(response, "h5_url", "H5Url");
|
||||
if (string.IsNullOrWhiteSpace(prepayId) && string.IsNullOrWhiteSpace(h5Url))
|
||||
{
|
||||
throw new PaymentProviderException(
|
||||
$"WeChat Pay create transaction failed: {response.ErrorCode}",
|
||||
"WeChat Pay create transaction failed.",
|
||||
"wechat_pay_create_failed");
|
||||
}
|
||||
|
||||
var clientPayload = JsonSerializer.SerializeToElement(
|
||||
client.GenerateParametersForJsapiPayRequest(appId, response.PrepayId));
|
||||
var rawPayload = JsonSerializer.SerializeToElement(new
|
||||
var clientPayload = string.IsNullOrWhiteSpace(prepayId)
|
||||
? JsonSerializer.SerializeToElement(new { h5Url })
|
||||
: JsonSerializer.SerializeToElement(TenPaySignHelper.GetJsApiUiPackage(appId, prepayId, tenPaySettings));
|
||||
var providerPayload = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
response.PrepayId,
|
||||
PrepayId = prepayId,
|
||||
H5Url = h5Url,
|
||||
request.OrderNo,
|
||||
request.AmountCents
|
||||
});
|
||||
@@ -120,7 +133,7 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
"pending",
|
||||
null,
|
||||
clientPayload,
|
||||
rawPayload);
|
||||
MergeRawPayload(rawPayload, providerPayload));
|
||||
}
|
||||
|
||||
private static string Required(JsonElement element, params string[] keys)
|
||||
@@ -143,53 +156,82 @@ internal sealed class WechatPayProvider : IPaymentProvider
|
||||
"wechat_pay_config_incomplete");
|
||||
}
|
||||
|
||||
private static bool TryDecryptTransactionResource(
|
||||
WechatTenpayClient client,
|
||||
JsonElement body,
|
||||
out JsonElement payload)
|
||||
private static string? Optional(JsonElement element, params string[] keys)
|
||||
{
|
||||
payload = default;
|
||||
try
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (body.ValueKind != JsonValueKind.Object ||
|
||||
!body.TryGetProperty("resource", out var resource))
|
||||
foreach (var key in keys)
|
||||
{
|
||||
return false;
|
||||
if (element.TryGetProperty(key, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(property.GetString()))
|
||||
{
|
||||
return property.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryGetHeader(
|
||||
IReadOnlyDictionary<string, string> headers,
|
||||
string name,
|
||||
out string value)
|
||||
private static bool HasWechatPaySignatureHeaders(IReadOnlyDictionary<string, string> headers)
|
||||
{
|
||||
foreach (var pair in headers)
|
||||
return ContainsHeader(headers, "Wechatpay-Timestamp") &&
|
||||
ContainsHeader(headers, "Wechatpay-Nonce") &&
|
||||
ContainsHeader(headers, "Wechatpay-Signature") &&
|
||||
ContainsHeader(headers, "Wechatpay-Serial");
|
||||
}
|
||||
|
||||
private static bool ContainsHeader(IReadOnlyDictionary<string, string> headers, string name)
|
||||
{
|
||||
return headers.Keys.Any(key => string.Equals(key, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static T? GetPropertyValue<T>(object value, params string[] names)
|
||||
{
|
||||
var type = value.GetType();
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
|
||||
var property = type.GetProperty(name);
|
||||
if (property?.GetValue(value) is T typed)
|
||||
{
|
||||
value = pair.Value;
|
||||
return true;
|
||||
return typed;
|
||||
}
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user