Stripe是一家美国科技公司,成立于2010年,由爱尔兰兄弟Patrick Collison和John Collison共同创立。该公司致力于提供高效、简洁的互联网支付收款服务,为开发者或商家提供支付API接口或代码,使商家的网站、移动APP支持信用卡付款。Stripe被誉为“移动时代的PayPal”,因其简便的支付方式而受到广泛欢迎。
Stripe 总共有三种支付方式:
1、Stripe Checkout,pay links 创建支付链接,
2、payment intent,后端预下单,返回秘钥,前端确定订单
3、前端创建支付token ,后端创建Charge,返回支付结果链接
Stripe接口调用时序图
0、初始化客户端
StripeClient client = StripeClient.builder()
.setConnectTimeout(30 * 1000)
.setReadTimeout(80 * 1000)
.setApiKey("sk_test_51PtO7DC6XhwanSnNvGezNPc4hsL2F****")
.build();
1、产品
查询或创建新产品。
每次交易传入产品名称或描述,自动查询是否已经存在,如果存在则直接使用,如果不存在则新建产品。
private Product getProduct(StripeOrder stripeOrder) throws StripeException {
ProductSearchParams searchParams =
ProductSearchParams.builder()
.setQuery("active:'true' AND name:'" + stripeOrder.getSubject()
+ "' AND description:'" + stripeOrder.getBody() + "'")
.setLimit(1L)
.build();
StripeSearchResult<Product> result = client.products().search(searchParams);
Product product;
if (result != null && !result.getData().isEmpty()) {
product = result.getData().stream().findFirst().get();
} else {
//创建产品 https://stripe.com/docs/api/products/create
ProductCreateParams params = ProductCreateParams.builder()
.setDescription(stripeOrder.getBody())
.setName(stripeOrder.getSubject()).build();
product = client.products().create(params);
}
return product;
}
2、价格
根据产品ID创建对应币种的价格,指定 lookupKey = 价格+币种+产品ID,作为价格关键字,用于查询是否已经存在。如果价格存在则直接使用,如果不存在则新增新的价格。
private Price getPrice(StripeOrder stripeOrder, String productId) throws StripeException {
Long unitAmount = Util.conversionCentAmount(stripeOrder.getPrice());
String lookupKey = unitAmount + stripeOrder.getCurrencyCode() + productId;
PriceSearchParams params =
PriceSearchParams.builder()
.setQuery("active:'true' AND product:'" + productId
+ "' AND currency:'" + stripeOrder.getCurrencyCode()
+ "' AND lookup_key:'" + lookupKey + "'")
.build();
StripeSearchResult<Price> result = client.prices().search(params);
Price price;
if (result != null && !result.getData().isEmpty()) {
price = result.getData().stream().findFirst().get();
} else {
//创建价格 https://stripe.com/docs/api/prices/create
// PriceCreateParams.Recurring recurring = PriceCreateParams.Recurring.builder()
// .setInterval(PriceCreateParams.Recurring.Interval.MONTH).build();
PriceCreateParams priceCreateParams = PriceCreateParams.builder()
.setCurrency(stripeOrder.getCurrencyCode())
.setProduct(productId)
.setLookupKey(lookupKey)
.setUnitAmount(unitAmount)
// .setRecurring(recurring)
.build();
price = client.prices().create(priceCreateParams);
}
return price;
}
3、创建支付
根据上次返回的价格信息,创建新的支付对象,这里指定银行卡支付,也可以指定别的支付方式;Stripe支持几十种支付方式,可以根据不同国家选择,具体可以在这里查看
下面通过 client.checkout().sessions().create() 创建支付,取得支付链接,在浏览器直接打开即可支付。
public Map<String, Object> orderInfo(PayOrder order) {
StripeOrder stripeOrder = (StripeOrder) order;
try {
Product product = getProduct(stripeOrder);
Price price = getPrice(stripeOrder, product.getId());
//创建支付信息 得到url
SessionCreateParams sessionCreateParams = SessionCreateParams.builder()
.setMode(SessionCreateParams.Mode.PAYMENT)
.addPaymentMethodType(SessionCreateParams.PaymentMethodType.CARD)
// .addPaymentMethodType(SessionCreateParams.PaymentMethodType.ALIPAY)
.setSuccessUrl(payConfigStorage.getReturnUrl())
.setCancelUrl(payConfigStorage.getCancelUrl())
.setCustomer(stripeOrder.getCustomer())
.setClientReferenceId(stripeOrder.getClientReferenceId())
.setCustomerEmail(stripeOrder.getCustomerEmail())
.addLineItem(
SessionCreateParams.LineItem.builder()
.setQuantity(1L)
.setPrice(price.getId())
.build())
.putMetadata("outTradeNo", stripeOrder.getOutTradeNo())
.build();
Session session = client.checkout().sessions().create(sessionCreateParams);
LOG.info("session:{}", JSON.toJSONString(session));
return preOrderHandler(Collections.singletonMap("paymentLink", session.getUrl()), order);
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
4、打开支付链接,完成支付
输入Stripe平台提供的测试卡信息,完成支付
test@example.com
4242 4242 4242 4242
12/34
567
Zhang San
United States
12345
5、支付订单查询
传入回调信息中得到的chargeId,查询订单状态。在返回的json数据了包含支付平台receiptUrl
public Map<String, Object> query(AssistOrder assistOrder) {
try {
Charge charge = client.charges().retrieve(assistOrder.getTradeNo());
// 使用Hutool的BeanUtil将User对象转换为Map
return BeanUtil.beanToMap(charge);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
支付凭证
6、退款
传入回调信息中得到的chargeId,提交退款请求;返回退款订单信息,包含退款订单Id(可用于查询退款订单)
public RefundResult refund(RefundOrder refundOrder) {
RefundCreateParams params = RefundCreateParams.builder()
.setCharge(refundOrder.getTradeNo())
.build();
try {
Refund refund = client.refunds().create(params);
LOG.info("refund:{}", JSON.toJSONString(refund));
StripeRefundResult refundResult = new StripeRefundResult(refund, refundOrder.getTradeNo());
refundOrder.setRefundNo(refundResult.getRefundNo());
return refundResult;
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
7、退款查询
传入回调信息中得到的chargeId,查询退款订单;也可以通过退款订单Id查询。
public Map<String, Object> refundquery(RefundOrder refundOrder) {
RefundListParams params = RefundListParams.builder()
.setCharge(refundOrder.getTradeNo())
.build();
try {
StripeCollection<Refund> result = client.refunds().list(params);
if (!result.getData().isEmpty()) {
Refund refund = result.getData().stream().findFirst().get();
// 使用Hutool的BeanUtil将User对象转换为Map
return BeanUtil.beanToMap(refund);
}
} catch (StripeException e) {
throw new RuntimeException(e);
}
return null;
}
8、回调通知
登录Stripe平台,在https://dashboard.stripe.com/workbench/webhooks中配置Webhook;菜单地址“开发人员-Webhook”。
Webhook回调代码示例
public class StripePayMessageHandler implements PayMessageHandler<PayMessage, StripePayService> {
private final Logger LOG = LoggerFactory.getLogger(getClass());
private final String endpointSecret = "whsec_HlzC2omyh4V4X3BCgMx5PScTCmwZpvAC";//webhook秘钥签名
@Override
public PayOutMessage handle(PayMessage payMessage, Map<String, Object> context, StripePayService payService) throws PayErrorException {
Map<String, Object> message = payMessage.getPayMessage();
NoticeParams noticeParams = (NoticeParams) context.get("noticeParams");
try {
String sigHeader = noticeParams.getHeader("Stripe-Signature");
Event event = Webhook.constructEvent(noticeParams.getBodyStr(), sigHeader, endpointSecret);//验签,并获取事件
StripeObject eventObj = event
.getDataObjectDeserializer()
.getObject()
.get();
LOG.info("EventType:{}, Event:{}", event.getType(), JSON.toJSONString(eventObj));
PaymentIntent intent;
Charge charge;
String outTradeNo;
String chargeId;
String receiptUrl;
switch (event.getType()) {
case "charge.succeeded"://支付成功
//TODO 支付成功,处理业务逻辑
charge = (Charge) eventObj;
// 取得 chargeId ,在退款时使用
chargeId = charge.getId();
receiptUrl = charge.getReceiptUrl();
message.put("trade_no", chargeId);
LOG.info("支付成功 Charge, chargeId:{}, receiptUrl:{}", chargeId, receiptUrl);
break;
case "checkout.session.completed":// 通过支付链接 支付完成
//TODO 支付完成,处理业务逻辑
Session session = (Session) eventObj;
outTradeNo = session.getMetadata().get("outTradeNo");//自定义订单号
LOG.info("支付完成 Session, 订单号为:{}", outTradeNo);
message.put("out_trade_no", outTradeNo);
break;
case "charge.refunded"://退款成功
charge = (Charge) eventObj;
if (charge.getStatus().equals("succeeded")) {
//TODO 退款成功,处理业务逻辑
chargeId = charge.getId();
receiptUrl = charge.getReceiptUrl();
message.put("trade_no", chargeId);
LOG.info("退款成功, chargeId:{}, receiptUrl:{}", chargeId, receiptUrl);
}
break;
case "checkout.session.expired"://过期
break;
case "payment_intent.created"://创建订单 这里事件就是图二选着的事件
break;
case "payment_intent.canceled"://取消订单
break;
case "payment_intent.succeeded"://支付成功
intent = (PaymentIntent) eventObj;
Map<String, String> metaData = intent.getMetadata();//自定义传入的参数
outTradeNo = metaData.get("outTradeNo");//自定义订单号
message.put("out_trade_no", outTradeNo);
LOG.info("支付成功 payment_intent, 订单号为:{}", outTradeNo);
//*********** 根据订单号从数据库中找到订单,并将状态置为成功 *********//*
break;
case "payment_intent.payment_failed"://支付失败
intent = (PaymentIntent) eventObj;
LOG.info("Failed: " + intent.getId());
break;
default:
break;
}
} catch (Exception e) {
LOG.error("stripe异步通知(webhook事件)", e);
}
// TODO 支付确认逻辑处理
return payService.successPayOutMessage(payMessage);
}
}
参考
- https://docs.stripe.com/api
- https://docs.stripe.com/js
- https://docs.stripe.com/webhooks
- https://docs.stripe.com/search#query-fields-for-products