支付功能
2024年11月5日大约 5 分钟
支付功能
这里我选择主流的支付宝完成支付功能,
支付宝|开放平台 地址:https://open.alipay.com/develop/manage
支付流程
我将分为用户下单、对接支付、处理结果三步实现支付功能,具体流程图如下:

用户下单
首先,用户在系统中创建订单(内部订单),创建过程中需要判断是否存在未支付的订单,存在则可以直接返回。另外还有一种可能,创建的订单存在,但没有支付订单(支付宝订单),也就是【掉单】。这是因为本身的业务系统和外部的支付创建(支付宝)不是一个事务,不能一起成功或失败,所以要做一些流程的校验。比如我们创建订单成功,但创建支付单失败。掉单情况用户继续创建订单,就会优先使用这笔订单创建支付单。如果用户正常下单,就先创建系统内部订单,然后调用支付宝创建支付订单。
@Slf4j
@Service
public class OrderServiceImpl implements IOrderService {
@Resource
private IOrderDao orderDao;
@Resource
private ProductRPC productRPC;
@Override
public PayOrderRes createOrder(ShopCartReq shopCartReq) throws Exception {
// 1. 查询当前用户是否存在未支付订单或掉单订单
PayOrder payOrderReq = new PayOrder();
payOrderReq.setUserId(shopCartReq.getUserId());
payOrderReq.setProductId(shopCartReq.getProductId());
PayOrder unpaidOrder = orderDao.queryUnPayOrder(payOrderReq);
if (null != unpaidOrder && Constants.OrderStatusEnum.PAY_WAIT.getCode().equals(unpaidOrder.getStatus())) {
log.info("创建订单-存在,已存在未支付订单。userId:{} productId:{} orderId:{}", shopCartReq.getUserId(), shopCartReq.getProductId(), unpaidOrder.getOrderId());
return PayOrderRes.builder()
.orderId(unpaidOrder.getOrderId())
.payUrl(unpaidOrder.getPayUrl())
.build();
} else if (null != unpaidOrder && Constants.OrderStatusEnum.CREATE.getCode().equals(unpaidOrder.getStatus())){
// todo 创建支付订单
}
// 2. 查询商品 & 创建订单
ProductVO productVO = productRPC.queryProductByProductId(shopCartReq.getProductId());
String orderId = RandomStringUtils.randomNumeric(16);
orderDao.insert(PayOrder.builder()
.userId(shopCartReq.getUserId())
.productId(shopCartReq.getProductId())
.productName(productVO.getProductName())
.orderId(orderId)
.totalAmount(productVO.getPrice())
.orderTime(new Date())
.status(Constants.OrderStatusEnum.CREATE.getCode())
.build());
// 3. 创建支付单 todo
return PayOrderRes.builder()
.orderId(orderId)
.payUrl("暂无")
.build();
}
}对接支付
参考支付宝官方文档,引入相关sdk、配置信息
生成支付宝订单接口
private PayOrder doPrepayOrder(String productId, String productName, String orderId, BigDecimal totalAmount) throws AlipayApiException {
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(notifyUrl);
request.setReturnUrl(returnUrl);
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderId);
bizContent.put("total_amount", totalAmount.toString());
bizContent.put("subject", productName);
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
String form = alipayClient.pageExecute(request).getBody();
PayOrder payOrder = new PayOrder();
payOrder.setOrderId(orderId);
payOrder.setPayUrl(form);
payOrder.setStatus(Constants.OrderStatusEnum.PAY_WAIT.getCode());
orderDao.updateOrderPayInfo(payOrder);
return payOrder;
}完整下单接口
@Service
public class OrderServiceImpl implements IOrderService {
@Value("${alipay.notify_url}")
private String notifyUrl;
@Value("${alipay.return_url}")
private String returnUrl;
@Resource
private IOrderDao orderDao;
@Resource
private ProductRPC productRPC;
@Resource
private AlipayClient alipayClient;
@Resource
private EventBus eventBus;
@Override
public PayOrderRes createOrder(ShopCartReq shopCartReq) throws Exception {
// 1. 查询当前用户是否存在掉单和未支付订单
PayOrder payOrderReq = new PayOrder();
payOrderReq.setUserId(shopCartReq.getUserId());
payOrderReq.setProductId(shopCartReq.getProductId());
PayOrder unpaidOrder = orderDao.queryUnPayOrder(payOrderReq);
if (null != unpaidOrder && Constants.OrderStatusEnum.PAY_WAIT.getCode().equals(unpaidOrder.getStatus())) {
log.info("创建订单-存在,已存在未支付订单。userId:{} productId:{} orderId:{}", shopCartReq.getUserId(), shopCartReq.getProductId(), unpaidOrder.getOrderId());
return PayOrderRes.builder()
.orderId(unpaidOrder.getOrderId())
.payUrl(unpaidOrder.getPayUrl())
.build();
} else if (null != unpaidOrder && Constants.OrderStatusEnum.CREATE.getCode().equals(unpaidOrder.getStatus())) {
log.info("创建订单-存在,存在未创建支付单订单,创建支付单开始 userId:{} productId:{} orderId:{}", shopCartReq.getUserId(), shopCartReq.getProductId(), unpaidOrder.getOrderId());
PayOrder payOrder = this.doPrepayOrder(shopCartReq.getUserId(), shopCartReq.getProductId(), unpaidOrder.getProductName(), unpaidOrder.getOrderId(), unpaidOrder.getTotalAmount());
return PayOrderRes.builder()
.orderId(payOrder.getOrderId())
.payUrl(payOrder.getPayUrl())
.build();
}
// 2. 查询商品 & 创建订单
ProductVO productVO = productRPC.queryProductByProductId(shopCartReq.getProductId());
String orderId = RandomStringUtils.randomNumeric(16);
orderDao.insert(PayOrder.builder()
.userId(shopCartReq.getUserId())
.productId(productVO.getProductId())
.productName(productVO.getProductName())
.orderId(orderId)
.orderTime(new Date())
.totalAmount(productVO.getPrice())
.status(Constants.OrderStatusEnum.CREATE.getCode())
.build());
// 3. 创建支付单
PayOrder payOrder = doPrepayOrder(shopCartReq.getUserId(), shopCartReq.getProductId(), productVO.getProductName(), orderId, productVO.getPrice());
return PayOrderRes.builder()
.orderId(orderId)
.payUrl(payOrder.getPayUrl())
.build();
}
}处理结果
最后,在用户完成支付后,支付宝会向我们指定的接口发送消息,通知我们支付结果。我们需要接收支付回调消息,更新本地的订单状态,以及推动后续流程。比如;发放商品、驱动物流、营销活动等。 值得注意的是,因为支付订单和系统内部订单不是一个事务,所以针对可能的异常需要进行任务补偿,以及超时未支付的订单也要进行关单操作。
因为已经加入redis依赖,这里我使用redis的发布订阅功能简单实现一个消息队列,就不额外添加mq依赖了。
@Configuration
public class RedisMqConfig {
@Autowired
private MessageSubscriber messageSubscriber;
@Bean
public RedisMessageListenerContainer container(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, topic());//绑定监听器到指定频道
return container;
}
// 监听器
@Bean
public MessageListenerAdapter listenerAdapter() {
return new MessageListenerAdapter(messageSubscriber);
}
// 订阅频道
@Bean
public ChannelTopic topic() {
return new ChannelTopic("order");
}
}生产者
@Component
public class MessagePublisher {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ChannelTopic topic;
public void publish(String message) {
redisTemplate.convertAndSend(topic.getTopic(), message);
}
}消费者
@Component
public class MessageSubscriber implements MessageListener {
@Override
public void onMessage(Message message, byte[] bytes) {
PayOrder payOrderReq = new PayOrder();
String orderId= (String) SerializationUtils.deserialize(message.getBody());
payOrderReq.setOrderId((orderId));
payOrderReq.setStatus(Constants.OrderStatusEnum.PAY_SUCCESS.getCode());
orderDao.changeOrderPaySuccess(payOrderReq);
log.info("收到支付成功消息,已更改订单状态,可以做接下来的事情,如;发货、充值、开户员、返利 ");
}
}支付回调接口
@RequestMapping(value = "pay_notify", method = RequestMethod.POST)
public String payNotify(HttpServletRequest request) {
try {
log.info("支付回调,消息接收 {}", request.getParameter("trade_status"));
if (request.getParameter("trade_status").equals("TRADE_SUCCESS")) {
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
params.put(name, request.getParameter(name));
}
String tradeNo = params.get("out_trade_no");
String gmtPayment = params.get("gmt_payment");
String alipayTradeNo = params.get("trade_no");
String sign = params.get("sign");
String content = AlipaySignature.getSignCheckContentV1(params);
boolean checkSignature = AlipaySignature.rsa256CheckContent(content, sign, alipayPublicKey, "UTF-8"); // 验证签名
// 支付宝验签
if (checkSignature) {
// 验签通过
log.info("支付回调,交易名称: {}", params.get("subject"));
log.info("支付回调,交易状态: {}", params.get("trade_status"));
log.info("支付回调,支付宝交易凭证号: {}", params.get("trade_no"));
log.info("支付回调,商户订单号: {}", params.get("out_trade_no"));
log.info("支付回调,交易金额: {}", params.get("total_amount"));
log.info("支付回调,买家在支付宝唯一id: {}", params.get("buyer_id"));
log.info("支付回调,买家付款时间: {}", params.get("gmt_payment"));
log.info("支付回调,买家付款金额: {}", params.get("buyer_pay_amount"));
log.info("支付回调,支付回调,更新订单 {}", tradeNo);
// 发送消息更新订单状态及其他操作
messagePublisher.publish(tradeNo);
}
}
return "success";
} catch (Exception e) {
log.error("支付回调,处理失败", e);
return "false";
}
}定时任务补偿调单
<select id="queryNoPayNotifyOrder" resultType="java.lang.String">
SELECT order_id as orderId FROM pay_order
WHERE status = 'PAY_WAIT' AND NOW() >= order_time + INTERVAL 1 MINUTE
ORDER BY id ASC
LIMIT 10
</select>
@Scheduled(cron = "0/3 * * * * ?")
public void exec() {
try {
List<String> orderIds = orderService.queryNoPayNotifyOrder();
if (null == orderIds || orderIds.isEmpty()) return;
for (String orderId : orderIds) {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel bizModel = new AlipayTradeQueryModel();
bizModel.setOutTradeNo(orderId);
request.setBizModel(bizModel);
AlipayTradeQueryResponse alipayTradeQueryResponse = alipayClient.execute(request);
String code = alipayTradeQueryResponse.getCode();
// 判断状态码
if ("10000".equals(code)) {
orderService.changeOrderPaySuccess(orderId);
}
}
} catch (Exception e) {
log.error("检测未接收到或未正确处理的支付回调通知失败", e);
}
}用户超时未支付订单,定时任务检查关闭
<select id="queryTimeoutCloseOrderList" resultType="java.lang.String">
SELECT order_id as orderId FROM openai_order
WHERE status = 'PAY_WAIT' AND NOW() >= order_time + INTERVAL 30 MINUTE
ORDER BY id ASC
LIMIT 50
</select>
@Scheduled(cron = "0/3 * * * * ?")
public void exec() {
try {
List<String> orderIds = orderService.queryNoPayNotifyOrder();
if (null == orderIds || orderIds.isEmpty()) return;
for (String orderId : orderIds) {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel bizModel = new AlipayTradeQueryModel();
bizModel.setOutTradeNo(orderId);
request.setBizModel(bizModel);
AlipayTradeQueryResponse alipayTradeQueryResponse = alipayClient.execute(request);
String code = alipayTradeQueryResponse.getCode();
// 判断状态码
if ("10000".equals(code)) {
orderService.changeOrderPaySuccess(orderId);
}
}
} catch (Exception e) {
log.error("检测未接收到或未正确处理的支付回调通知失败", e);
}
}