<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";
}