更新20260316

This commit is contained in:
GukSang.Jin
2026-03-16 15:38:08 +08:00
parent 5b7238befa
commit 54b3448e31
16 changed files with 1132 additions and 30 deletions

View File

@@ -9,10 +9,12 @@ namespace PetWash.Api.Controllers;
public class OrdersController : ControllerBase
{
private readonly OrderService _orderService;
private readonly WeChatPayService _weChatPayService;
public OrdersController(OrderService orderService)
public OrdersController(OrderService orderService, WeChatPayService weChatPayService)
{
_orderService = orderService;
_weChatPayService = weChatPayService;
}
[HttpPost]
@@ -21,12 +23,24 @@ public class OrdersController : ControllerBase
try
{
var order = await _orderService.CreateOrderAsync(request.PackageId);
return Ok(order);
var payment = await _weChatPayService.CreateNativePayAsync(order, HttpContext.RequestAborted);
return Ok(new CreateOrderResponse
{
Order = order,
CodeUrl = payment.CodeUrl,
OutTradeNo = payment.OutTradeNo,
ExpiresAt = payment.ExpiresAt
});
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (InvalidOperationException ex)
{
return StatusCode(StatusCodes.Status502BadGateway, ex.Message);
}
}
[HttpPost("{id}/payment")]
@@ -39,6 +53,56 @@ public class OrdersController : ControllerBase
return Ok(order);
}
[HttpGet("{id}/payment-status")]
public async Task<IActionResult> GetPaymentStatus(int id, [FromQuery] string outTradeNo)
{
if (string.IsNullOrWhiteSpace(outTradeNo))
{
return BadRequest("outTradeNo is required.");
}
var order = await _orderService.GetOrderAsync(id);
if (order == null)
{
return NotFound();
}
if (order.IsPaid)
{
return Ok(new PaymentStatusResponse
{
Order = order,
IsPaid = true,
TradeState = "SUCCESS",
OutTradeNo = outTradeNo,
Message = "Order already confirmed."
});
}
try
{
var payment = await _weChatPayService.QueryOrderByOutTradeNoAsync(outTradeNo, HttpContext.RequestAborted);
if (string.Equals(payment.TradeState, "SUCCESS", StringComparison.OrdinalIgnoreCase))
{
order = await _orderService.ConfirmPaymentAsync(id) ?? order;
}
return Ok(new PaymentStatusResponse
{
Order = order,
IsPaid = order.IsPaid,
TradeState = payment.TradeState,
OutTradeNo = payment.OutTradeNo,
TransactionId = payment.TransactionId,
Message = order.IsPaid ? "Payment confirmed." : "Waiting for payment."
});
}
catch (InvalidOperationException ex)
{
return StatusCode(StatusCodes.Status502BadGateway, ex.Message);
}
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{

View File

@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Mvc;
using PetWash.Api.Services;
namespace PetWash.Api.Controllers;
[ApiController]
[Route("api/payments/wechat")]
public class PaymentsController : ControllerBase
{
private readonly OrderService _orderService;
private readonly WeChatPayService _weChatPayService;
private readonly ILogger<PaymentsController> _logger;
public PaymentsController(
OrderService orderService,
WeChatPayService weChatPayService,
ILogger<PaymentsController> logger)
{
_orderService = orderService;
_weChatPayService = weChatPayService;
_logger = logger;
}
[HttpPost("notify")]
public async Task<IActionResult> Notify(CancellationToken cancellationToken)
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync(cancellationToken);
try
{
var notification = await _weChatPayService.ParsePaymentNotificationAsync(
body,
Request.Headers,
cancellationToken);
if (string.Equals(notification.TradeState, "SUCCESS", StringComparison.OrdinalIgnoreCase))
{
var order = await _orderService.ConfirmPaymentAsync(notification.OrderId);
if (order == null)
{
_logger.LogWarning(
"WeChat payment notification references a missing order. OrderId={OrderId}, OutTradeNo={OutTradeNo}",
notification.OrderId,
notification.OutTradeNo);
}
}
return Ok(new { code = "SUCCESS", message = "成功" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process WeChat payment notification.");
return BadRequest(new { code = "FAIL", message = ex.Message });
}
}
}

View File

@@ -0,0 +1,9 @@
namespace PetWash.Api.Models;
public sealed class CreateOrderResponse
{
public required Order Order { get; init; }
public string CodeUrl { get; init; } = string.Empty;
public string OutTradeNo { get; init; } = string.Empty;
public DateTimeOffset? ExpiresAt { get; init; }
}

View File

@@ -0,0 +1,11 @@
namespace PetWash.Api.Models;
public sealed class PaymentStatusResponse
{
public required Order Order { get; init; }
public bool IsPaid { get; init; }
public string TradeState { get; init; } = string.Empty;
public string OutTradeNo { get; init; } = string.Empty;
public string TransactionId { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
}

View File

@@ -27,6 +27,12 @@ builder.Services.AddDbContext<PetWashDbContext>(options =>
builder.Services.AddSingleton<MqttService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<MqttService>());
builder.Services.AddScoped<OrderService>();
builder.Services.Configure<WeChatPayOptions>(builder.Configuration.GetSection("WeChatPay"));
builder.Services.AddHttpClient<WeChatPayService>(client =>
{
client.BaseAddress = new Uri("https://api.mch.weixin.qq.com");
client.Timeout = TimeSpan.FromSeconds(15);
});
// 添加CORS
builder.Services.AddCors(options =>

View File

@@ -26,6 +26,7 @@ public class OrderService
var order = new Order
{
PackageId = packageId,
Package = package,
CreatedAt = DateTime.Now,
Status = OrderStatus.WaitingPayment,
IsPaid = false
@@ -43,6 +44,12 @@ public class OrderService
var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId);
if (order == null) return null;
if (order.IsPaid)
{
_logger.LogInformation("Order payment already confirmed: OrderId={OrderId}", orderId);
return order;
}
order.IsPaid = true;
order.PaidAt = DateTime.Now;
order.Status = OrderStatus.Paid;
@@ -62,7 +69,7 @@ public class OrderService
public async Task<Order?> UpdateOrderStatusAsync(int orderId, OrderStatus status)
{
var order = await _context.Orders.FindAsync(orderId);
var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId);
if (order == null) return null;
order.Status = status;

View File

@@ -0,0 +1,602 @@
using System.Globalization;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using PetWash.Api.Models;
namespace PetWash.Api.Services;
public sealed class WeChatPayOptions
{
public string AppId { get; set; } = string.Empty;
public string MerchantId { get; set; } = string.Empty;
public string CertificateSerialNumber { get; set; } = string.Empty;
public string PrivateKeyPath { get; set; } = string.Empty;
public string PrivateKeyPem { get; set; } = string.Empty;
public string NotifyUrl { get; set; } = string.Empty;
public string ApiV3Key { get; set; } = string.Empty;
public string PlatformPublicKeyPath { get; set; } = string.Empty;
public string PlatformPublicKeyPem { get; set; } = string.Empty;
public string PlatformPublicKeySerial { get; set; } = string.Empty;
public string Currency { get; set; } = "CNY";
public int OrderExpireMinutes { get; set; } = 5;
}
public sealed class WeChatNativePayResult
{
public string CodeUrl { get; init; } = string.Empty;
public string OutTradeNo { get; init; } = string.Empty;
public DateTimeOffset? ExpiresAt { get; init; }
}
public sealed class WeChatTransactionStatus
{
public string OutTradeNo { get; init; } = string.Empty;
public string TradeState { get; init; } = string.Empty;
public string TransactionId { get; init; } = string.Empty;
public string Attach { get; init; } = string.Empty;
public string SuccessTime { get; init; } = string.Empty;
}
public sealed class WeChatPaymentNotification
{
public required int OrderId { get; init; }
public string OutTradeNo { get; init; } = string.Empty;
public string TradeState { get; init; } = string.Empty;
public string TransactionId { get; init; } = string.Empty;
}
public sealed class WeChatPayService
{
private const string NativePayPath = "/v3/pay/transactions/native";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly HttpClient _httpClient;
private readonly WeChatPayOptions _options;
private readonly ILogger<WeChatPayService> _logger;
public WeChatPayService(
HttpClient httpClient,
IOptions<WeChatPayOptions> options,
ILogger<WeChatPayService> logger)
{
_httpClient = httpClient;
_options = options.Value;
_logger = logger;
}
public async Task<WeChatNativePayResult> CreateNativePayAsync(Order order, CancellationToken cancellationToken = default)
{
ValidateOptions();
if (order.Package is null)
{
throw new InvalidOperationException("Package data is required to create a WeChat payment.");
}
var expiresAt = DateTimeOffset.UtcNow.AddMinutes(_options.OrderExpireMinutes);
var outTradeNo = BuildOutTradeNo(order.Id);
var payload = new NativePayRequest
{
AppId = _options.AppId,
MerchantId = _options.MerchantId,
Description = $"{order.Package.Name} payment",
OutTradeNo = outTradeNo,
NotifyUrl = _options.NotifyUrl,
TimeExpire = expiresAt.UtcDateTime.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture),
Attach = $"orderId={order.Id}",
Amount = new NativePayAmount
{
Total = ConvertToFen(order.Package.Price),
Currency = string.IsNullOrWhiteSpace(_options.Currency) ? "CNY" : _options.Currency
}
};
var body = JsonSerializer.Serialize(payload, JsonOptions);
using var request = new HttpRequestMessage(HttpMethod.Post, NativePayPath)
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
var nonce = Guid.NewGuid().ToString("N");
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue(
"WECHATPAY2-SHA256-RSA2048",
BuildAuthorizationParameter(NativePayPath, HttpMethod.Post.Method, body, nonce, timestamp));
using var response = await _httpClient.SendAsync(request, cancellationToken);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
var error = TryDeserialize<WeChatErrorResponse>(responseBody);
var message = error is null
? responseBody
: $"{error.Code}: {error.Message}";
_logger.LogError(
"WeChat Native pay request failed. StatusCode={StatusCode}, Message={Message}",
(int)response.StatusCode,
message);
throw new InvalidOperationException($"WeChat Native pay request failed: {message}");
}
var result = TryDeserialize<NativePayResponse>(responseBody);
if (string.IsNullOrWhiteSpace(result?.CodeUrl))
{
throw new InvalidOperationException("WeChat returned an empty code_url.");
}
return new WeChatNativePayResult
{
CodeUrl = result.CodeUrl,
OutTradeNo = outTradeNo,
ExpiresAt = expiresAt
};
}
public async Task<WeChatTransactionStatus> QueryOrderByOutTradeNoAsync(
string outTradeNo,
CancellationToken cancellationToken = default)
{
ValidateOptions();
if (string.IsNullOrWhiteSpace(outTradeNo))
{
throw new ArgumentException("outTradeNo is required.", nameof(outTradeNo));
}
var canonicalUrl =
$"/v3/pay/transactions/out-trade-no/{Uri.EscapeDataString(outTradeNo)}?mchid={Uri.EscapeDataString(_options.MerchantId)}";
using var request = new HttpRequestMessage(HttpMethod.Get, canonicalUrl);
var nonce = Guid.NewGuid().ToString("N");
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue(
"WECHATPAY2-SHA256-RSA2048",
BuildAuthorizationParameter(canonicalUrl, HttpMethod.Get.Method, string.Empty, nonce, timestamp));
using var response = await _httpClient.SendAsync(request, cancellationToken);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
var error = TryDeserialize<WeChatErrorResponse>(responseBody);
var message = error is null
? responseBody
: $"{error.Code}: {error.Message}";
throw new InvalidOperationException($"WeChat order query failed: {message}");
}
var result = TryDeserialize<QueryOrderResponse>(responseBody);
if (result is null)
{
throw new InvalidOperationException("WeChat order query returned an unreadable response.");
}
return new WeChatTransactionStatus
{
OutTradeNo = result.OutTradeNo,
TradeState = result.TradeState,
TransactionId = result.TransactionId,
Attach = result.Attach,
SuccessTime = result.SuccessTime
};
}
public async Task<WeChatPaymentNotification> ParsePaymentNotificationAsync(
string body,
IHeaderDictionary headers,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(headers);
ValidateNotificationOptions();
var timestamp = headers["Wechatpay-Timestamp"].ToString();
var nonce = headers["Wechatpay-Nonce"].ToString();
var signature = headers["Wechatpay-Signature"].ToString();
var serial = headers["Wechatpay-Serial"].ToString();
if (string.IsNullOrWhiteSpace(timestamp) ||
string.IsNullOrWhiteSpace(nonce) ||
string.IsNullOrWhiteSpace(signature) ||
string.IsNullOrWhiteSpace(serial))
{
throw new InvalidOperationException("Missing WeChat notification headers.");
}
VerifyNotificationSignature(timestamp, nonce, body, signature, serial);
var notification = TryDeserialize<PaymentNotificationEnvelope>(body)
?? throw new InvalidOperationException("Invalid WeChat notification payload.");
if (notification.Resource is null)
{
throw new InvalidOperationException("WeChat notification resource is missing.");
}
var decryptedJson = DecryptNotificationResource(notification.Resource);
var transaction = TryDeserialize<PaymentTransaction>(decryptedJson)
?? throw new InvalidOperationException("Unable to decrypt WeChat notification resource.");
if (!TryExtractOrderId(transaction.Attach, out var orderId))
{
throw new InvalidOperationException("Unable to resolve orderId from WeChat notification.");
}
await Task.CompletedTask;
return new WeChatPaymentNotification
{
OrderId = orderId,
OutTradeNo = transaction.OutTradeNo,
TradeState = transaction.TradeState,
TransactionId = transaction.TransactionId
};
}
private string BuildAuthorizationParameter(string canonicalUrl, string method, string body, string nonce, string timestamp)
{
var message = $"{method}\n{canonicalUrl}\n{timestamp}\n{nonce}\n{body}\n";
var signature = Sign(message);
return
$"mchid=\"{_options.MerchantId}\"," +
$"nonce_str=\"{nonce}\"," +
$"timestamp=\"{timestamp}\"," +
$"serial_no=\"{_options.CertificateSerialNumber}\"," +
$"signature=\"{signature}\"";
}
private string Sign(string message)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(GetPrivateKeyPem());
var signatureBytes = rsa.SignData(
Encoding.UTF8.GetBytes(message),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signatureBytes);
}
private string GetPrivateKeyPem()
{
if (!string.IsNullOrWhiteSpace(_options.PrivateKeyPem))
{
return _options.PrivateKeyPem;
}
var keyPath = _options.PrivateKeyPath;
if (string.IsNullOrWhiteSpace(keyPath))
{
throw new InvalidOperationException("WeChat private key is not configured.");
}
if (!Path.IsPathRooted(keyPath))
{
keyPath = Path.Combine(AppContext.BaseDirectory, keyPath);
}
if (!File.Exists(keyPath))
{
throw new InvalidOperationException($"WeChat private key file was not found: {keyPath}");
}
return File.ReadAllText(keyPath);
}
private void ValidateOptions()
{
var missingFields = new List<string>();
if (string.IsNullOrWhiteSpace(_options.AppId))
{
missingFields.Add("WeChatPay:AppId");
}
if (string.IsNullOrWhiteSpace(_options.MerchantId))
{
missingFields.Add("WeChatPay:MerchantId");
}
if (string.IsNullOrWhiteSpace(_options.CertificateSerialNumber))
{
missingFields.Add("WeChatPay:CertificateSerialNumber");
}
if (string.IsNullOrWhiteSpace(_options.NotifyUrl))
{
missingFields.Add("WeChatPay:NotifyUrl");
}
if (string.IsNullOrWhiteSpace(_options.PrivateKeyPem) && string.IsNullOrWhiteSpace(_options.PrivateKeyPath))
{
missingFields.Add("WeChatPay:PrivateKeyPath or WeChatPay:PrivateKeyPem");
}
if (missingFields.Count > 0)
{
throw new InvalidOperationException(
$"Missing WeChat Pay configuration: {string.Join(", ", missingFields)}");
}
}
private void ValidateNotificationOptions()
{
var missingFields = new List<string>();
if (string.IsNullOrWhiteSpace(_options.ApiV3Key))
{
missingFields.Add("WeChatPay:ApiV3Key");
}
if (string.IsNullOrWhiteSpace(_options.PlatformPublicKeyPem) &&
string.IsNullOrWhiteSpace(_options.PlatformPublicKeyPath))
{
missingFields.Add("WeChatPay:PlatformPublicKeyPath or WeChatPay:PlatformPublicKeyPem");
}
if (string.IsNullOrWhiteSpace(_options.PlatformPublicKeySerial))
{
missingFields.Add("WeChatPay:PlatformPublicKeySerial");
}
if (missingFields.Count > 0)
{
throw new InvalidOperationException(
$"Missing WeChat notification configuration: {string.Join(", ", missingFields)}");
}
}
private void VerifyNotificationSignature(
string timestamp,
string nonce,
string body,
string signature,
string serial)
{
if (!string.Equals(serial, _options.PlatformPublicKeySerial, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Unexpected WeChat notification serial.");
}
var signatureMessage = $"{timestamp}\n{nonce}\n{body}\n";
var signatureBytes = Convert.FromBase64String(signature);
using var rsa = RSA.Create();
rsa.ImportFromPem(GetPlatformPublicKeyPem());
var verified = rsa.VerifyData(
Encoding.UTF8.GetBytes(signatureMessage),
signatureBytes,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
if (!verified)
{
throw new InvalidOperationException("WeChat notification signature verification failed.");
}
}
private string GetPlatformPublicKeyPem()
{
if (!string.IsNullOrWhiteSpace(_options.PlatformPublicKeyPem))
{
return _options.PlatformPublicKeyPem;
}
var keyPath = _options.PlatformPublicKeyPath;
if (string.IsNullOrWhiteSpace(keyPath))
{
throw new InvalidOperationException("WeChat platform public key is not configured.");
}
if (!Path.IsPathRooted(keyPath))
{
keyPath = Path.Combine(AppContext.BaseDirectory, keyPath);
}
if (!File.Exists(keyPath))
{
throw new InvalidOperationException($"WeChat platform public key file was not found: {keyPath}");
}
return File.ReadAllText(keyPath);
}
private string DecryptNotificationResource(PaymentNotificationResource resource)
{
if (!string.Equals(resource.Algorithm, "AEAD_AES_256_GCM", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Unsupported WeChat notification algorithm: {resource.Algorithm}");
}
var keyBytes = Encoding.UTF8.GetBytes(_options.ApiV3Key);
if (keyBytes.Length != 32)
{
throw new InvalidOperationException("WeChat ApiV3Key must be 32 bytes.");
}
var cipherBytes = Convert.FromBase64String(resource.Ciphertext);
var nonceBytes = Encoding.UTF8.GetBytes(resource.Nonce);
var associatedDataBytes = Encoding.UTF8.GetBytes(resource.AssociatedData ?? string.Empty);
if (cipherBytes.Length < 16)
{
throw new InvalidOperationException("Invalid WeChat notification ciphertext.");
}
var tagLength = 16;
var ciphertextLength = cipherBytes.Length - tagLength;
var plaintextBytes = new byte[ciphertextLength];
var tagBytes = new byte[tagLength];
Buffer.BlockCopy(cipherBytes, ciphertextLength, tagBytes, 0, tagLength);
using var aesGcm = new AesGcm(keyBytes, tagLength);
aesGcm.Decrypt(
nonceBytes,
cipherBytes.AsSpan(0, ciphertextLength),
tagBytes,
plaintextBytes,
associatedDataBytes);
return Encoding.UTF8.GetString(plaintextBytes);
}
private static bool TryExtractOrderId(string? attach, out int orderId)
{
orderId = 0;
if (string.IsNullOrWhiteSpace(attach))
{
return false;
}
const string prefix = "orderId=";
if (!attach.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return int.TryParse(attach[prefix.Length..], out orderId);
}
private static int ConvertToFen(decimal amount)
{
return decimal.ToInt32(decimal.Round(amount * 100m, 0, MidpointRounding.AwayFromZero));
}
private static string BuildOutTradeNo(int orderId)
{
return $"PW{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{orderId:D6}";
}
private static T? TryDeserialize<T>(string json)
{
try
{
return JsonSerializer.Deserialize<T>(json, JsonOptions);
}
catch
{
return default;
}
}
private sealed class NativePayRequest
{
[JsonPropertyName("appid")]
public string AppId { get; init; } = string.Empty;
[JsonPropertyName("mchid")]
public string MerchantId { get; init; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; init; } = string.Empty;
[JsonPropertyName("out_trade_no")]
public string OutTradeNo { get; init; } = string.Empty;
[JsonPropertyName("notify_url")]
public string NotifyUrl { get; init; } = string.Empty;
[JsonPropertyName("time_expire")]
public string? TimeExpire { get; init; }
[JsonPropertyName("attach")]
public string? Attach { get; init; }
[JsonPropertyName("amount")]
public NativePayAmount Amount { get; init; } = new();
}
private sealed class NativePayAmount
{
[JsonPropertyName("total")]
public int Total { get; init; }
[JsonPropertyName("currency")]
public string Currency { get; init; } = "CNY";
}
private sealed class NativePayResponse
{
[JsonPropertyName("code_url")]
public string CodeUrl { get; init; } = string.Empty;
}
private sealed class QueryOrderResponse
{
[JsonPropertyName("out_trade_no")]
public string OutTradeNo { get; init; } = string.Empty;
[JsonPropertyName("trade_state")]
public string TradeState { get; init; } = string.Empty;
[JsonPropertyName("transaction_id")]
public string TransactionId { get; init; } = string.Empty;
[JsonPropertyName("attach")]
public string Attach { get; init; } = string.Empty;
[JsonPropertyName("success_time")]
public string SuccessTime { get; init; } = string.Empty;
}
private sealed class WeChatErrorResponse
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("message")]
public string Message { get; init; } = string.Empty;
}
private sealed class PaymentNotificationEnvelope
{
[JsonPropertyName("resource")]
public PaymentNotificationResource? Resource { get; init; }
}
private sealed class PaymentNotificationResource
{
[JsonPropertyName("algorithm")]
public string Algorithm { get; init; } = string.Empty;
[JsonPropertyName("ciphertext")]
public string Ciphertext { get; init; } = string.Empty;
[JsonPropertyName("nonce")]
public string Nonce { get; init; } = string.Empty;
[JsonPropertyName("associated_data")]
public string? AssociatedData { get; init; }
}
private sealed class PaymentTransaction
{
[JsonPropertyName("out_trade_no")]
public string OutTradeNo { get; init; } = string.Empty;
[JsonPropertyName("trade_state")]
public string TradeState { get; init; } = string.Empty;
[JsonPropertyName("transaction_id")]
public string TransactionId { get; init; } = string.Empty;
[JsonPropertyName("attach")]
public string Attach { get; init; } = string.Empty;
}
}

View File

@@ -10,5 +10,19 @@
"DefaultConnection": "Data Source=petwash.db",
"MySqlConnection": "Server=101.132.182.216;Database=petwash;User=sc_root;Password=Shsc#$@2024#@!;Port=3306;CharSet=utf8mb4;"
},
"DatabaseProvider": "Sqlite"
"DatabaseProvider": "Sqlite",
"WeChatPay": {
"AppId": "",
"MerchantId": "1107066208",
"CertificateSerialNumber": "",
"PrivateKeyPath": "",
"PrivateKeyPem": "",
"NotifyUrl": "",
"ApiV3Key": "",
"PlatformPublicKeyPath": "",
"PlatformPublicKeyPem": "",
"PlatformPublicKeySerial": "",
"Currency": "CNY",
"OrderExpireMinutes": 5
}
}