1. 准备事项
- Stripe 账号
- 域名以及配套的网站
- Stripe 账号付款信息
- 公钥和私钥
2. 配置产品以及价格
可以通过 API 或者 Stripe 管理后台来进行配置
产品:就是商品,只需要配置一个名称和一个类型(用于计算税额)
价格:价格有定期和一次性两种收费方式,定期其实就是订阅。价格实体非常灵活,适合多种场景,一般就使用固定费率的一次性付款和定期付款。
3. 设计一下流程
4. 代码集成
4.1 依赖导入
stripe/stripe-java: Java library for the Stripe API. (github.com)
xml
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>23.3.0</version>
</dependency>
4.2 配置
properties
# 公钥
stripe.key=pk_test_51Nxxxx
# 私钥
stripe.secret=sk_test_51xxxx
# webhook 密钥签名
stripe.endpoint_secret=whsec_Tcxxxx
java
@Data
@Configuration
@ConfigurationProperties(prefix = "stripe")
public class StripeConfig {
private String key;
private String secret;
private String endpointSecret;
@Bean
public StripeClient stripeClient() {
return new StripeClient(secret);
}
}
4.3 创建收银
Stripe 中有两种方式能进行收款,Stripe-hosted page
和 Embedded form
。
Stripe-hosted page
:指的是收费的时候跳转到 Stripe 提供的一个收银台页面进行付款。
Embedded form
:则是需要高度自定义页面的产品使用,或者是客户端。
文档:Stripe Checkout | Stripe 文档
Demo: docs.stripe.com/checkout/qu...
Web 端一般使用 Stripe-hosted page
来简化开发,像 ChatGPT
也是使用这种方式。
后端创建收银台
java
public CheckoutCreateResult create(CheckoutCreateRequest request) {
// 查询或者创建客户
String customerId = queryOrCreateCustomer();
// 查询价格id
String priceId = queryPrice();
// 构建成功URL和取消URL
UriComponents successUrl = UriComponentsBuilder.fromHttpUrl(request.getSuccessUrl())
.queryParam("checkout_id", checkoutId)
.queryParam("receipt", "{CHECKOUT_SESSION_ID}") // 模板变量 https://stripe.com/docs/payments/checkout/custom-success-page#modify-success-url
.build();
UriComponents cancelUrl = UriComponentsBuilder.fromHttpUrl(request.getCancelUrl())
.queryParam("checkout_id", checkoutId)
.build();
// 创建checkout 收银台
SessionCreateParams.Builder builder = SessionCreateParams.builder()
.setSuccessUrl(successUrl.toUriString())
.setCancelUrl(cancelUrl.toUriString())
// 指定付款用户
.setCustomer(customerId)
// 自动扣税
.setAutomaticTax(
SessionCreateParams.AutomaticTax.builder()
.setEnabled(false)
.build())
// 购买项目:和订单明细类似
.addLineItem(
SessionCreateParams.LineItem.builder()
// 数量
.setQuantity(request.getCount().longValue())
// 价格
.setPrice(priceId)
.build())
// 元数据:额外附加的数据。 webhook 通知的时候可以取出来
.putAllMetadata(ImmutableMap.of(
MetaDataKey.CHECKOUT_ID, checkoutId,
MetaDataKey.APP_ID, request.getAppId()
))
// 是否允许优惠码
.setAllowPromotionCodes(Boolean.TRUE);
if (productPrice.getPriceType() == PriceTypeEnum.RECURRING) {
// 定期价格,最后会创建订阅对象。可以为付款成功后生成的订阅对象设置一些数据
builder.setMode(SessionCreateParams.Mode.SUBSCRIPTION)
.setSubscriptionData(// 试用期
SessionCreateParams.SubscriptionData.builder()
.putMetadata(MetaDataKey.APP_ID, request.getAppId())
.build());
} else {
// 一次性价格,最后会创建付款对象。可以为付款成功后生成的付款对象设置一些数据
builder.setMode(SessionCreateParams.Mode.PAYMENT)
.setPaymentIntentData(
SessionCreateParams.PaymentIntentData.builder()
.putMetadata(MetaDataKey.APP_ID, request.getAppId())
.build());
}
SessionCreateParams params = builder.build();
/*.addDiscount( // 优惠券
SessionCreateParams.Discount.builder()
.setCoupon("bBfCjIMt")
.build())*/
Session session = null;
try {
session = stripeClient.checkout().sessions().create(params);
} catch (StripeException e) {
log.error("failed to create checkout session. {}, {}, {}", request, customerId, priceId, e);
throw new RuntimeException("failed to create checkout session: "+ e.getMessage());
}
return new CheckoutCreateThirdResult()
// checkout session 的 id
.setId(session.getId())
// 可供用户进行付款的页面链接,前端直接打开即可跳转到Stripe
.setTokenThird(session.getUrl());
}
4.4 完成收银
4.4.1 前端提交
用户付款完成后,Stripe 会将页面重定向到创建 Checkout Session
时设置的 success_url
。 页面可以从URL中获取到订单id和sessionId来进一步调用后端接口完成收银。
4.4.2 接收 Webhook
用户付款完成后,Stripe 的后台还会将对应的事件通过 WebHook
的方式 POST 我们预先提供的接口。
第一步,先提供一个 Webhook 回调接口
本地测试的方式不是很友好,可以使用内网穿透工具将请求转到本地来进行调试
java
@RestController
@Slf4j
public class WebhookController {
@Resource
private StripeConfig stripeConfig;
@Resource
private List<WebhookHandler> webhookHandlers;
@PostMapping("/webhook")
public Object handle(@RequestHeader("Stripe-Signature") String sigHeader, @RequestBody String payload) {
log.info("stripe webhook payload: {}", payload);
return webhook(payload, sigHeader, stripeConfig.endpointSecret());
}
private Object webhook(String payload, String sigHeader, String endpointSecret) {
Event event;
try {
event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
StripeEventType stripeEventType = StripeEventType.convert(event.getType());
webhookHandlers.stream()
.filter(webhookHandler -> webhookHandler.supports(stripeEventType))
.findFirst()
.get().handle(event);
} catch (Exception e) {
log.error("failed to handle webhook event. {}, {}", sigHeader, payload, e);
return ResponseEntity.status(500).body(e.getMessage());
}
return ResponseEntity.ok().body("OK");
}
}
Stripe 事件枚举
java
public enum StripeEventType implements EnumBase {
// 收银完成
CHECKOUT_SESSION_COMPLETED("checkout.session.completed"),
// 退款
CHARGE_REFUNDED("charge.refunded"),
IGNORED("");
private final String message;
StripeEventType(String message) {
this.message = message;
}
public static StripeEventType convert(String message) {
for (StripeEventType value : StripeEventType.values()) {
if (StringUtils.equals(value.message(), message)) {
return value;
}
}
return IGNORED;
}
@Override
public String message() {
return this.message;
}
@Override
public Number value() {
return null;
}
}
Webhook 处理器
java
public interface WebhookHandler {
boolean supports(StripeEventType stripeEventType);
void handle(Event event) throws EventDataObjectDeserializationException;
}
public abstract class WebhookHandlerBase<T> implements WebhookHandler {
@SuppressWarnings("unchecked")
@Override
public void handle(Event event) throws EventDataObjectDeserializationException {
EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();
StripeObject stripeObject = dataObjectDeserializer.deserializeUnsafe();
handle((T) stripeObject);
}
public abstract void handle(T stripeObject);
}
@Component
@Slf4j
public class WebhookHandlerDefaultImpl implements WebhookHandler {
@Override
public boolean supports(StripeEventType stripeEventType) {
return stripeEventType.equals(StripeEventType.IGNORED);
}
@Override
public void handle(Event event) {
log.info("ignored event: {} {}", event.getType(), event.toJson());
}
}
@Component
@Slf4j
public class WebhookHandlerCheckoutSessionCompletedImpl extends WebhookHandlerBase<Session> {
@Override
public boolean supports(StripeEventType stripeEventType) {
return stripeEventType.equals(StripeEventType.CHECKOUT_SESSION_COMPLETED);
}
@Override
public void handle(Session session) {
// 完成收银
}
}
@Component
@Slf4j
public class WebhookHandlerChargeRefundImpl extends WebhookHandlerBase<Charge> {
@Override
public boolean supports(StripeEventType stripeEventType) {
return stripeEventType.equals(StripeEventType.CHARGE_REFUNDED);
}
@Override
public void handle(Charge charge) {
// 订单退款
}
}
配置 Stripe Webhook
管理平台 -- FeloTranslator -- Stripe [Test]
4.4.3 完成收银
步骤流程:
- 判断对应的订单是否存在
- 订单所有者
- 对应的Stripe checkout session 状态是否正常
- 订单完成
- 发送订单完成事件
- 事件订阅者处理后续流程
java
protected Session checkCheckoutSession(String sessionId) {
// 查询是否完成
Session session = null;
try {
session = stripeClient.checkout().sessions().retrieve(sessionId);
} catch (StripeException e) {
log.error("failed to query checkout session. {}", sessionId, e);
throw new RuntimeException("failed to query checkout session:" + sessionId);
}
// https://stripe.com/docs/api/checkout/sessions/object#checkout_session_object-payment_status
String status = session.getPaymentStatus();
if (StringUtils.notEquals(status, "paid")) {
throw new RuntimeException("Checkout has no completed: " + status);
}
return session;
}
Ref
Stripe-hosted page | Stripe 文档
stripe/stripe-java: Java library for the Stripe API. (github.com)