259 lines
11 KiB
C#
259 lines
11 KiB
C#
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<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)
|
|
{
|
|
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<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 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));
|
|
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.",
|
|
"wechat_pay_create_failed");
|
|
|
|
var clientPayload = string.IsNullOrWhiteSpace(prepayId)
|
|
? JsonSerializer.SerializeToElement(new { h5Url })
|
|
: JsonSerializer.SerializeToElement(
|
|
TenPaySignHelper.GetJsApiUiPackage(
|
|
prepayId,
|
|
new TenPayV3Info(tenPaySettings),
|
|
JsApiAppType.WxOpen));
|
|
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<string, string> 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)
|
|
{
|
|
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 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;
|
|
}
|
|
|
|
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; }
|
|
}
|
|
}
|