<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>24.0.0</version>
</dependency>
#stripe配置 stripe: publicKey: pk_test_51OFTJTIkpJUW1eOYejtK7ZM3YNnSNc1ELk8D4X2wkXYJoKDtU0Ny7uICL2M4zZerGnNOipTSwyVPmxoKhZE0nmS700zFLJUIAi apiKey: sk_test_51OFTJTIkpJUW1eOYDq5k2I6jT74Xn1jAymML11dfFbyeTmoVIdCpgSV8QBRnrdMYDBJ4j7m3O00R3a52VpdmsypF00VVSLNYmC cancelUrl: http://stripe.fojia21jia.com/#/pages/index/payFail successUrl: http://stripe.foji21ajia.com/#/pages/index/paySuccess webhookSecret: whsec_fXgFfbSnxd5x7MGwYrpTIU1euO1Od1yc
package com.ruoyi.web.controller.member; import com.ruoyi.religious.model.CreateOrderEntity; import com.ruoyi.religious.model.CreateRefundEntity; import com.ruoyi.religious.service.StripeOrderService; import com.stripe.Stripe; import com.stripe.model.*; import com.stripe.model.checkout.Session; import com.stripe.param.*; import com.stripe.param.checkout.SessionCreateParams; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; import java.util.*; @Slf4j @RestController @AllArgsConstructor @RequestMapping("/stripe") public class OrderController { @Autowired private StripeOrderService orderService; /** * 创建支付会话 * 1、创建产品 * 2、设置价格 * 3、创建支付信息 得到url * @return */ @PostMapping("/order/pay") public String pay(@Validated @RequestBody CreateOrderEntity createOrderEntity) { return orderService.pay(createOrderEntity); } // /** // * 发放退款 // * @return // */ // @PostMapping("/order/refund") // public void refund(@Validated @RequestBody CreateRefundEntity createRefund) { // orderService.refund(createRefund); // } }
package com.ruoyi.religious.service; import com.ruoyi.religious.model.CreateOrderEntity; import com.ruoyi.religious.model.CreateRefundEntity; public interface StripeOrderService { String pay(CreateOrderEntity createOrderEntity); void refund(CreateRefundEntity createRefund); }
package com.ruoyi.religious.service.impl; import com.ruoyi.common.enums.OrderStatusEnum; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.religious.domain.ReligiousCultureOrder; import com.ruoyi.religious.model.CreateOrderEntity; import com.ruoyi.religious.model.CreateRefundEntity; import com.ruoyi.religious.result.ApiResult; import com.ruoyi.religious.service.IReligiousCultureOrderService; import com.ruoyi.religious.service.StripeOrderService; import com.ruoyi.system.service.ISysConfigService; import com.stripe.Stripe; import com.stripe.model.Price; import com.stripe.model.Product; import com.stripe.model.Refund; import com.stripe.model.checkout.Session; import com.stripe.param.RefundCreateParams; import com.stripe.param.checkout.SessionCreateParams; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; @Slf4j @Service public class StripeOrderServiceImpl implements StripeOrderService { @Value("${stripe.apiKey}") private String apiKey; @Value("${stripe.cancelUrl}") private String cancelUrl; @Value("${stripe.successUrl}") private String successUrl; @Autowired private ISysConfigService sysConfigService; @Autowired private IReligiousCultureOrderService religiousCultureOrderService; @Override public String pay(CreateOrderEntity createOrderEntity) { Stripe.apiKey = apiKey; try { //创建产品 Map<String, Object> params = new HashMap<>(); params.put("name", createOrderEntity.getProductName()); Product product = Product.create(params); ReligiousCultureOrder religiousCultureOrder = religiousCultureOrderService.selectReligiousCultureOrderBySerialNumber(createOrderEntity.getOrderId()); if (religiousCultureOrder == null || !religiousCultureOrder.getStatus().equals(OrderStatusEnum.ADD_GONGDE_WAIT.getCode())) { throw new RuntimeException("订单状态错误!"); } String s = sysConfigService.selectConfigByKey("sys.app.virtues.ratio"); BigDecimal divide = religiousCultureOrder.getPrice().multiply(new BigDecimal(s)).divide(new BigDecimal(1), 2, RoundingMode.UP); // BigDecimal price1 = religiousCultureOrder.getPrice(); //创建价格 Map<String, Object> priceParams = new HashMap<>(); BigDecimal actualAmount = divide.multiply(BigDecimal.valueOf(100)); //stripe的默认单位是分,即传入的amount实际上小数点会被左移两位 //给price绑定元数据并更新price用于检索 Map<String, Object> metadata = new HashMap<>(); metadata.put("orderId", createOrderEntity.getOrderId()); priceParams.put("metadata", metadata); //通过订单号关联用于检索price信息(可选) priceParams.put("unit_amount", actualAmount.intValue()); priceParams.put("currency", createOrderEntity.getCurrency()); priceParams.put("product", product.getId()); Price price = Price.create(priceParams); //创建支付信息得到url SessionCreateParams params3 = SessionCreateParams.builder() .setMode(SessionCreateParams.Mode.PAYMENT) .setSuccessUrl(successUrl) //可自定义成功页面 .setCancelUrl(cancelUrl) .addLineItem( SessionCreateParams.LineItem.builder() .setQuantity(createOrderEntity.getQuantity()) .setPrice(price.getId()) .build()).putMetadata("orderId",createOrderEntity.getOrderId()) //通过订单号关联用于检索支付信息(可选) .build(); Session session = Session.create(params3); System.out.println("sessionId:" +session.getId()); String sessionId = session.getId(); //退款方式1:拿到sessionId入库,退款的时候根据这个id找到PaymentIntent的id然后发起退款 return session.getUrl(); }catch (Exception e){ log.error("创建支付会话出现异常:",e); } return ""; } @Override public void refund(CreateRefundEntity createRefund) { try{ Stripe.apiKey = apiKey; if (StringUtils.isNotEmpty(createRefund.getSessionId())){ //根据会话编号退款 Session session = Session.retrieve( createRefund.getSessionId() ); RefundCreateParams params; if (createRefund.getAmount() != null && createRefund.getAmount().compareTo(BigDecimal.ZERO) != 0){ //指定退款金额 BigDecimal actualAmount = createRefund.getAmount().multiply(BigDecimal.valueOf(100)); //api默认单位分 params = RefundCreateParams.builder() .setPaymentIntent(session.getPaymentIntent()) .setAmount(actualAmount.longValue()) .build(); }else { //全额退款 params = RefundCreateParams.builder() .setPaymentIntent(session.getPaymentIntent()) .build(); } Refund.create(params); log.info("根据会话编号退款成功"); return; } if (StringUtils.isNotEmpty(createRefund.getChargeId())){ //根据退款编号退款 Map<String, Object> params = new HashMap<>(); params.put("charge", createRefund.getChargeId()); if (createRefund.getAmount() != null && createRefund.getAmount().compareTo(BigDecimal.ZERO) != 0){ //指定退款金额,否则全额退款 BigDecimal actualAmount = createRefund.getAmount().multiply(BigDecimal.valueOf(100)); //api默认单位分 params.put("amount", actualAmount.longValue()); } Refund.create(params); log.info("根据退款编号退款成功"); return; } }catch (Exception e){ //e.getMessage.contain("charge_already_refunded") 已退款 //e.getMessage.contain("resource_missing") 退款编号错误 //e.getMessage.contain("amount on charge ($n)") 金额应小于n log.error("退款异常:",e); } } }
package com.ruoyi.web.controller.member; import com.ruoyi.religious.service.CallbackService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j @RestController @AllArgsConstructor @RequestMapping("/stripe") public class CallbackController { @Autowired private CallbackService callbackService; @GetMapping("/callback/success") public void success(String orderId) { System.out.println("调用success成功:" + orderId); } @GetMapping("/callback/cancel") public void cancel(String orderId) { System.out.println("调用cancel成功:" + orderId); } /** * webhook回调 * @return 返回处理状态(验签不通过拒绝) */ @PostMapping(value = "/callback/webhook") public Object webhook(HttpServletRequest request, HttpServletResponse response){ return callbackService.webhook(request,response); } }
package com.ruoyi.religious.service; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public interface CallbackService { Object webhook(HttpServletRequest request, HttpServletResponse response); }
package com.ruoyi.religious.service.impl; import com.alibaba.fastjson.JSONObject; import com.google.gson.JsonSyntaxException; import com.ruoyi.common.enums.OrderStatusEnum; import com.ruoyi.common.enums.OrderTypeEnum; import com.ruoyi.religious.domain.ReligiousCultureOrder; import com.ruoyi.religious.service.CallbackService; import com.ruoyi.religious.service.ClientMemberService; import com.ruoyi.religious.service.IReligiousCultureOrderService; import com.stripe.exception.SignatureVerificationException; import com.stripe.model.*; import com.stripe.net.ApiResource; import com.stripe.net.Webhook; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.stream.Collectors; @Slf4j @Service public class CallbackServiceImpl implements CallbackService { @Value("${stripe.webhookSecret}") private String webhookSecret; //webhookSecret配置在Webhook秘钥签名中 @Autowired private IReligiousCultureOrderService orderService; @Autowired private ClientMemberService clientMemberService; @Override public Object webhook(HttpServletRequest request, HttpServletResponse response) { System.out.println(request); InputStream inputStream = null; ByteArrayOutputStream output = null; try { //获取请求体的参数 inputStream = request.getInputStream(); output = new ByteArrayOutputStream(); byte[] buffer = new byte[1024 * 4]; int n; while (-1 != (n = inputStream.read(buffer))) { output.write(buffer, 0, n); } byte[] bytes = output.toByteArray(); String eventPayload = new String(bytes, "UTF-8"); System.out.println("获取请求体的参数:" + eventPayload); //获取请求头签名 String sigHeader = request.getHeader("Stripe-Signature"); System.out.println("获取请求头的签名:" + sigHeader); Event event; try { event = ApiResource.GSON.fromJson(eventPayload, Event.class); } catch (JsonSyntaxException e) { // Invalid payload response.setStatus(400); return ""; } // Deserialize the nested object inside the event EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); StripeObject stripeObject = null; if (dataObjectDeserializer.getObject().isPresent()) { stripeObject = dataObjectDeserializer.getObject().get(); } else { // Deserialization failed, probably due to an API version mismatch. // Refer to the Javadoc documentation on `EventDataObjectDeserializer` for // instructions on how to handle this case, or return an error here. } // try { // System.out.println("webhookSecret"+webhookSecret); // event = Webhook.constructEvent(eventPayload, sigHeader, webhookSecret,5000); //调用webhook进行验签 // System.out.println(event); // } catch (JsonSyntaxException e) { // log.error("参数格式有误,解析失败:", e); // System.out.println(-1); // response.setStatus(400); // return ""; // } catch (SignatureVerificationException e) { // log.error("参数被篡改,验签失败:", e); // response.setStatus(400); // System.out.println(0); // return ""; // } // Handle the event switch (event.getType()) { case "checkout.session.completed"://支付完成 System.out.println("---------------success---------------"); String s = event.getDataObjectDeserializer().getObject().get().toJson(); JSONObject jsonObject = JSONObject.parseObject(s); JSONObject jsonObject2 = (JSONObject)jsonObject.get("metadata"); String orderId = jsonObject2.getString("orderId"); System.out.println("支付完成,订单号为:"+orderId); ReligiousCultureOrder religiousCultureOrder = orderService.selectReligiousCultureOrderBySerialNumber(orderId); if (religiousCultureOrder.getStatus().equals(OrderStatusEnum.ADD_GONGDE_WAIT.getCode())) { if (religiousCultureOrder.getOrderType().equals(OrderTypeEnum.ADD_GONGDE.getCode())){ clientMemberService.addSubscriber(religiousCultureOrder); }else { orderService.alipay(religiousCultureOrder); } } break; case "checkout.session.expired": { System.out.println("回话过期"); // Then define and call a function to handle the event checkout.session.expired break; } case "payment_intent.canceled": { System.out.println("订单取消"); // Then define and call a function to handle the event payment_intent.canceled break; } case "charge.succeeded": { System.out.println("charge.succeeded"); break; } case "payment_intent.created": { System.out.println("创建支付了"); break; } case "payment_intent.payment_failed": { System.out.println("支付失败"); // Then define and call a function to handle the event payment_intent.payment_failed break; } case "payment_intent.succeeded": { //支付成功 PaymentIntent paymentIntent = (PaymentIntent) stripeObject; System.out.println("支付成功,订单chargeId:" + paymentIntent.getLatestCharge()); String chargeId = paymentIntent.getLatestCharge(); //退款方式2:回调保存该退款编号用于退款, System.out.println(chargeId); break; } // ... handle other event types default: System.out.println("Unhandled event type: " + event.getType()); } response.setStatus(200); //处理无异常,返回 } catch (Exception e) { System.out.println(2); log.error("webhook异步通知解析异常:", e); response.setStatus(400); return ""; } finally { try { if (inputStream != null) { inputStream.close(); } if (output != null) { output.close(); } } catch (Exception e) { log.error("流关闭异常"); } } return ""; } }
package com.ruoyi.religious.model; import lombok.Data; import javax.validation.constraints.NotEmpty; import java.math.BigDecimal; @Data public class CreateOrderEntity { /** 本地订单号 */ // @NotEmpty(message = "订单编号不允许为空") private String orderId; /** 商品名称 */ // @NotEmpty(message = "商品名称不允许为空") private String productName; /** 货币类型 */ private String currency; /** 购买数量 */ private long quantity; }
package com.ruoyi.religious.model; import lombok.Data; import java.math.BigDecimal; @Data public class CreateRefundEntity { /** 退款金额,支持多次退款,sessionId或者chargeId不会更改 */ private BigDecimal amount; /** 会话编号 */ private String sessionId; /** 退款编号 */ private String chargeId; }
package com.ruoyi.religious.model; public class StripeConstant { /** * pending 刚刚准备好订单; * chargeable Source对象变成chargeable客户已授权和验证的付款后。 * failed Source对象未能成为收费为你的客户拒绝授权支付。 * canceled Source对象过期,并且不能用于创建电荷。 * succeeded 收费成功,付款完成。 * created 发送客户投诉,从Stripe余额中扣除争议金额。 */ public static final String PAY_STRIPE_STATUS_PENDING = "pending"; public static final String PAY_STRIPE_STATUS_CHARGEABLE = "chargeable"; public static final String PAY_STRIPE_STATUS_FAILED = "failed"; public static final String PAY_STRIPE_STATUS_CANCELED = "canceled"; public static final String PAY_STRIPE_STATUS_SUCCEEDED = "succeeded"; public static final String PAY_STRIPE_STATUS_CREATED = "created"; //授权完成,但用户余额不足 public static final String PAY_STRIPE_STATUS_INSUFFICIENT_FUNDS = "insufficient_funds"; //授权完成,订单收费金额大于支付宝支持的费用 public static final String PAY_STRIPE_STATUS_INVALID_AMOUNT = "invalid_amount"; //授权完成,创建收费订单失败 public static final String PAY_STRIPE_STATUS_CHARGE_ERROR = "charge_error"; //支付回调状态,支付成功的事件类型 public static final String PAY_STRIPE_STATUS_CALL_BACK_SUCCESSED = "payment_intent.succeeded"; //支付回调状态,支付方式附加事件(当前的支付模式不会返回该支付状态) public static final String PAY_STRIPE_STATUS_CALL_BACK_ATTACHED = "payment_method.attached"; //支付回调状态,取消订单 public static final String PAY_STRIPE_STATUS_CALL_BACK_CANCELED = "payment_method.canceled"; //支付回调状态,支付失败 public static final String PAY_STRIPE_STATUS_CALL_BACK_FAILED = "payment_method.payment_failed"; }