【开发心得】SpringBoot对接Stripe支付
概述
应用出海,需要对接国外的支付,之一的选择就是Stripe。这个介绍下Java对接Stripe的实现。
官网: Stripe | Financial Infrastructure to Grow Your Revenue
API文档:
https://docs.stripe.com/apihttps://docs.stripe.com/api
Stripe支付方式和概念比较多。
- 付款方式: 一次性付款和周期订阅付款。
- 支付方式: Paylink、charge、pay intent多种概念。
- 对接方式: 前端对接,后端对接两种。
Stripe在认证开通之前,支付属于沙箱模式。支付的时候,卡号填 4242 4242 4242 4242即可(在外网资料中看到的,暂时不知道其他特殊卡号是否支持)。
国内正式环境测试可以通过一些双币卡,比如东方航空与中信银行的联名信用卡。(吐槽下,Stripe有一个最低消费额,换算成人民币,3块左右每次)
对接步骤
1. 引入maven/gradle依赖
这里以maven为例。(2024年5月份对接的时候,我这里选型26.3,网上一些资料基于更老的版本)
<!-- https://mvnrepository.com/artifact/com.stripe/stripe-java -->
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<!-- <version>24.16.0</version>-->
<version>26.3.0</version>
</dependency>
2. webhook监听
设置webhook监听地址:
(1) 页面方式
入口比较隐蔽,可以在全局搜
地址填写实现webhook的地址。事件根据需要选择。
创建完后点击对应链接,获得密钥签名webHookSecret
(2) 代码方式创建
import com.stripe.Stripe;
import com.stripe.model.Event;
import com.stripe.model.WebhookEndpoint;
import com.stripe.net.WebhookEndpointCollection;
import com.stripe.exception.*;
public class CreateWebhookEndpoint {
public static void main(String[] args) {
Stripe.apiKey = "your_stripe_api_key";
try {
WebhookEndpoint endpoint = WebhookEndpoint.create(
"https://your-webhook-handler-domain.com",
Arrays.asList("charge.succeeded", "charge.failed")
);
System.out.println(endpoint.getId());
} catch (StripeException e) {
e.printStackTrace();
}
}
}
Stripe有个回调机制,在一定时间内,不断重试知道请求成功。它依赖于http的状态码。
关于Stripe的Java服务端监听,官方文档参考: Stripe Login | Sign in to the Stripe DashboardSign in to the Stripe Dashboard to manage business payments and operations in your account. Manage payments and refunds, respond to disputes and more.https://dashboard.stripe.com/webhooks/create?endpoint_location=hosted
private static final String PAYLOAD_CHARSET = "UTF-8";
private static final String STRIPE_SIGNATURE_FIELD = "Stripe-Signature";
private static final String REMOTE_SUCCEED_FIELD = "succeeded";
stripeApiKey与webHookSecret 这里是我从配置文件中读取出来的的,@Value方式,可自行实现。
stripeApiKey 是Stripe的支付sk
webHookSecret是回调页面配置获取的key
webhook 监听的事件包含如下几个常用,其实比较多,可以通过页面设置webhook,或者代码设置webhook的时候指定。
* charge.succeed * charge.canceled * payment_intent.succeeded * payment_intent.failed * payment_intent.canceled
@ResponseBody
@PostMapping(value = "/callback")
public void postEventsWebhook(HttpServletRequest request, HttpServletResponse response) {
Stripe.apiKey = stripeApiKey;
try {
// String payload = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
InputStream inputStream = request.getInputStream();
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 4];
int n = 0;
while (-1 != (n = inputStream.read(buffer))) {
output.write(buffer, 0, n);
}
byte[] bytes = output.toByteArray();
String payload = new String(bytes, PAYLOAD_CHARSET);
if (!StringUtils.isEmpty(payload)) {
String sigHeader = request.getHeader(STRIPE_SIGNATURE_FIELD);
String endpointSecret = webHookSecret;
Event event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
Optional<StripeObject> stripeObject = event.getDataObjectDeserializer().getObject();
String remoteStatus = null;
Map<String, String> metaData = null;
if (stripeObject.isPresent()) {
PaymentIntent intent = (PaymentIntent) stripeObject.get();
remoteStatus = intent.getStatus();
metaData = intent.getMetadata();
} else {
StripeObject sobj = event.getData().getObject();
if (sobj instanceof Charge) {
Charge object = (Charge) sobj;
remoteStatus = object.getStatus();
metaData = object.getMetadata();
} else {
PaymentIntent intent = (PaymentIntent) sobj;
remoteStatus = intent.getStatus();
metaData = intent.getMetadata();
}
}
ProcessStatus processStatus = null;
StripePayResultEnum payResultEnum = StripePayResultEnum.getByName(event.getType());
switch (payResultEnum) {
case PI_CREATED: //创建订单
// 这种先不处理
break;
case PI_CANCELED: // 取消订单
processStatus = ProcessStatus.CANCELLED;
break;
case PI_SUCCEED: // 支付成功
case CH_SUCCEED: // 结算成功
if (REMOTE_SUCCEED_FIELD.equals(remoteStatus)) {
processStatus = ProcessStatus.COMPLETED;
}
break;
case PI_FAILED: // 支付失败
case CH_FAILED: // 结算失败
processStatus = ProcessStatus.ERROR;
break;
default:
break;
}
if (processStatus != null) {
//自定义传入的参数
String orderNo = metaData.get(GatewayConstant.PAY_ORDER_NO);
log.info("finished, orderNo:{}, webhook event:{}, status:{}", orderNo, payResultEnum, processStatus);
Boolean finish = orderService.finish(orderNo, processStatus);
log.info("orderNo:{} deal finished, result:{}", orderNo, finish);
}
response.setStatus(200);
}
} catch (Exception e) {
response.setStatus(500);
log.error(e.getMessage(), e);
}
}
可以通过内网穿透,或者把应用放到云服务器上测试。
也可以通过本地webhook模拟测试,参考:
Stripe Login | Sign in to the Stripe DashboardSign in to the Stripe Dashboard to manage business payments and operations in your account. Manage payments and refunds, respond to disputes and more.https://dashboard.stripe.com/webhooks/create?endpoint_location=local
3. 业务支付
(1) 创建PriceId
XxxPrice(自己命名):
private String name; // 名称
private Long amount; // 金额
private String currency; // 货币单位(如usd)
checkPrice是简单判断了一下,param参数的货币,金额,调用该代码之后,我们会得到一个price_开头的价格id。将其记录下来,与自定义的商品关联。不用每次创建,这个在商品创建与价格更新的时候处理一次即可。
public String createPrice(XxxPrice param) {
this.checkPrice(param);
Stripe.apiKey = privateKey;
try {
PriceCreateParams params =
PriceCreateParams.builder()
.setCurrency(param.getCurrency())
.setUnitAmount(param.getAmount())
.setProductData(
PriceCreateParams.ProductData.builder().setName(param.getName()).build()
)
.build();
Price price = Price.create(params);
if (price != null) {
return price.getId();
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
(2) 创建PayIntent(支付意图,这是Stripe较新的概念)
这里SuccessUrl和CancelUrl自定设置,作用是支付页面支付完成后,重定向到对应的地址。
Quantity是数量,PriceId是上一步获得的id
/**
* @param priceId
* @return
* @throws StripeException
* @description: 获取支付链接
*/
public Session createPayment(String priceId, String orderNo) throws StripeException {
String payStatusUrl = this.getPayStatusUrl(orderNo);
Stripe.apiKey = privateKey;
SessionCreateParams.Builder builder = SessionCreateParams.builder();
builder.setMode(SessionCreateParams.Mode.PAYMENT);
builder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder()
.putMetadata(GatewayConstant.PAY_ORDER_NO, orderNo)
.build());
SessionCreateParams params =
builder
// 支付成功跳转
.setSuccessUrl(payStatusUrl)
// 支付取消跳转
.setCancelUrl(payStatusUrl)
.addLineItem(
SessionCreateParams.LineItem.builder()
.setQuantity(1L)
// Provide the exact Price ID (for example, pr_1234) of the product you want to sell
// 传入价格ID
.setPrice(priceId)
.build())
.build();
Session session = Session.create(params);
return session;
}
这个会得到一个 Session,id为一个pi_开头的字符串,代表付款唯一标识,url是支付界面的url,这个需要前端/客户端展示的。页面类似如下,其中商品名称,描述,货币单位,价格等都可以代码控制。(截图是正式的样例,测试的会有提示测试环境的字样,测试环境可以使用4242 4242 4242 4242来测试)
关于签名: SDK可以自动验证签名:
String sigHeader = request.getHeader(STRIPE_SIGNATURE_FIELD);
String endpointSecret = webHookSecret;
Event event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
关于携带自定义字段:
SessionCreateParams的putMetadata方法实际是一个map,可以设置自定义的键值对来传递。比如我这里传递的orderNo。
结语:
虽然概念多,但实际Stripe的文档也足够详细了。多看文档,多看sdk的源码,对接本身还是简单的。