如何解决处理 Stripes payment_intent.succeeded Webhook 如果它与来自客户端的回发竞争以在数据库中创建实体
在使用 Stripe 向信用卡收费时,我需要一些有关我的应用程序工作流程的建议。
场景 1 - 我没有为 payment_intent.succeeded
使用任何网络钩子,所以当我在客户端用 Javascript 调用 stripe.confirmCardPayment 时
并收到paymentIntent,然后将其发布到我的服务器并使用称为“SavePayment()”的某种方法在“付款”表中创建一个条目,其中将存储所有详细信息(卡ID、exp 月份、金额等)。保存到数据库后,我可以将详细信息返回给客户(赚取的积分、付款成功消息等)。然后我们就完成了!
场景 2 客户端(用户)在调用 Stripe 对卡收费后关闭浏览器,但在它可以回发到我的服务器以添加“支付”实体之前。所以现在我对 payment_intent.succeeded
使用网络钩子,因为其他人建议这样做以实现冗余。
问题 -
因为 webhook 是立即触发的,在 Stripe 向卡收费后,我的服务器可能会收到两个不同的入口点(客户端回发到服务器以保存付款和 Stripes webhook 触发事件),以创建“付款”我数据库中的实体。
现在这不是什么大问题,因为两个入口点都可以根据“Payment”实体的唯一标识符 (PaymentIntentId) 查询它是否存在于数据库中。
但是假设两个入口点都查询并返回空值,所以现在两个入口点继续创建一个新的“支付”实体并尝试将其保存在数据库中。一个会成功,一个现在会失败,经常创建 sql Server 抛出的唯一标识符约束异常。
解决方案? - 在我的数据库中创建实体时,这似乎不是理想的工作流程/场景,其中可能会频繁抛出多个异常。是否有更好的工作流程,还是我坚持以这种方式实施?
public class Payment : BaseEntity
{
public string PaymentIntentId { get; set; }
public int Amount { get; set; }
public string Currency { get; set; }
public string CardBrand { get; set; }
public string CardExpMonth { get; set; }
public string CardExpYear { get; set; }
public int CardFingerPrint { get; set; }
public string CardLastFour { get; set; }
public PaymentStatus Status { get; set; }
public int StripeFee { get; set; }
public int PointsAwarded { get; set; }
public int PointsBefore { get; set; }
public int PointsAfter { get; set; }
public string StripeCustomer { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
// submit button is pressed
// do some work here then call Stripe
from(this.stripe.confirmCardPayment(this.paymentIntent.clientSecret,data)).subscribe((result: any) => {
if (result.paymentIntent) {
let payment = {
paymentIntentId: result.paymentIntent.id,amount: result.paymentIntent.amount,currency: result.paymentIntent.currency,// fill in other fields
};
this.accountService.savePayment(payment).subscribe(response => {
if (response.status === 'Success') {
// do some stuff here
this.alertService.success("You're purchase was successful");
this.router.navigateByUrl('/somepage');
}
if (response.status === 'Failed') {
this.alertService.danger("Failed to process card");
}
},error => {
console.log(error);
this.alertService.danger("Oh no! Something happened,please contact the help desk.");
}).add(() => {
this.loadingPayment = false;
});
} else {
this.loadingPayment = false;
this.alertService.danger(result.error.message);
}
});
这里是保存“支付”实体的服务器控制器
[HttpPost("savepayment")]
public async Task<ActionResult> SavePayment(StripePaymentDto paymentDto)
{
var userFromrepo = await _userManager.FindByEmailFromClaimsPrinciple(HttpContext.User);
if (userFromrepo == null)
return Unauthorized(new ApiResponse(401));
// this calls the Stripe API to get the PaymentIntent (just incase the client changed it)
var paymentIntent = await _paymentService.RetrievePaymentIntent(paymentDto.PaymentIntentId);
if (paymentIntent == null) return BadRequest(new ApiResponse(400,"Problem Retrieving Payment Intent"));
var payment = _mapper.Map<StripePaymentDto,StripePayment>(paymentDto);
payment.UserId = userFromrepo.Id;
if (paymentIntent.Status == "succeeded") {
// fill in all the necessary fields
// left out for brevity
} else if (paymentIntent.Status == "requires_payment_method") {
payment.Status = PaymentStatus.Failed;
_logger.Loginformation("Payment Intent is not successful. Status: " + paymentIntent.Status + " PaymentIntentId: " + paymentIntent.PaymentIntentId);
// send payment failure email
} else {
// don't kNow if this will be needed
payment.Status = PaymentStatus.Pending;
}
_unitOfWork.Repository<StripePayment>().Add(payment);
var success = await _unitOfWork.Complete();
if (success > 0) {
if (payment.Status == PaymentStatus.Success) {
// send email
}
return Ok(_mapper.Map<StripePayment,StripePaymentDto>(payment));
}
return BadRequest(new ApiResponse(400,"Failed to save payment"));
}
这是 Stripe 网络钩子
[HttpPost("webhook")]
public async Task<ActionResult> StripeWebhook()
{
var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
// if this doesn't match we get an exception (sig with whSec)
var stripeEvent = EventUtility.ConstructEvent(json,Request.Headers["Stripe-Signature"],_whSecret);
PaymentIntent intent;
switch (stripeEvent.Type)
{
case "payment_intent.succeeded":
intent = (PaymentIntent)stripeEvent.Data.Object;
_logger.Loginformation("Payment Succeeded: ",intent.Id);
this.ProcessSuccess(intent);
// order = await _paymentService.UpdateOrderPaymentSucceeded(intent.Id);
// _logger.Loginformation("Order updated to payment received: ",order.Id);
break;
case "payment_intent.payment_Failed":
intent = (PaymentIntent)stripeEvent.Data.Object;
_logger.Loginformation("Payment Failed: ",intent.Id);
// _logger.Loginformation("Payment Failed: ",order.Id);
break;
}
return new EmptyResult();
}
private async void ProcessSuccess(PaymentIntent paymentIntent) {
var spec = new PaymentsWithtypespecification(paymentIntent.Id);
var paymentFromrepo = await _unitOfWork.Repository<StripePayment>().GetEntityWithSpec(spec);
if (paymentFromrepo == null) {
// create one and add it
var payment = _mapper.Map<PaymentIntent,StripePayment>(paymentIntent);
payment.UserId = Convert.ToInt32(paymentIntent.Metadata["userid"]);
}
// finish work here and then save to DB
}
解决方法
下面的要点。我很欣赏你的目标。经过一番思考,我的最终分析是:为了防止数据库中多个来源的重复记录,应该使用唯一索引。 (您正在使用)
现在通过使用唯一索引,数据库将抛出异常,代码必须妥善处理。因此,答案是您正在按照我和其他人多年来的方式进行操作。不幸的是,一旦您进入数据库层,我不知道有任何其他方法可以避免异常。
很好的问题,即使答案不是您希望的。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。