支付宝沙箱功能网址:https://open.alipay.com/develop/sandbox/app
基本介绍:沙箱支付通常指的是在受控环境中模拟真实支付流程的一种测试工具。它并非一个实际的支付服务,而是为开发者、商家和支付服务提供商提供的一种安全的测试环境。通过沙箱支付,用户可以模拟完整的支付过程,包括发起支付请求、处理支付网关交互、以及接收支付结果,而无需使用真实的资金。
查看自己沙箱的基本环境
https://open.alipay.com/develop/sandbox/app
发起订单
手机网站支付快速集成教程链接:https://opendocs.alipay.com/open/203/105285?pathHash=ada1de5b
package com.java.sdk.demo;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.AlipayConfig;
import com.alipay.api.domain.ExtUserInfo;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.domain.ExtendParams;
import com.alipay.api.domain.GoodsDetail;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.FileItem;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;
public class AlipayTradeWapPay {
public static void main(String[] args) throws AlipayApiException {
// 初始化SDK
AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
// 构造请求参数以调用接口
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
// 设置商户订单号
model.setOutTradeNo("70501111111S001111119");
// 设置订单总金额
model.setTotalAmount("9.00");
// 设置订单标题
model.setSubject("大乐透");
// 设置产品码
model.setProductCode("QUICK_WAP_WAY");
// 设置针对用户授权接口
model.setAuthToken("appopenBb64d181d0146481ab6a762c00714cC27");
// 设置用户付款中途退出返回商户网站的地址
model.setQuitUrl("http://www.taobao.com/product/113714.html");
// 设置订单包含的商品列表信息
List<GoodsDetail> goodsDetail = new ArrayList<GoodsDetail>();
GoodsDetail goodsDetail0 = new GoodsDetail();
goodsDetail0.setGoodsName("ipad");
goodsDetail0.setAlipayGoodsId("20010001");
goodsDetail0.setQuantity(1L);
goodsDetail0.setPrice("2000");
goodsDetail0.setGoodsId("apple-01");
goodsDetail0.setGoodsCategory("34543238");
goodsDetail0.setCategoriesTree("124868003|126232002|126252004");
goodsDetail0.setBody("特价手机");
goodsDetail0.setShowUrl("http://www.alipay.com/xxx.jpg");
goodsDetail.add(goodsDetail0);
model.setGoodsDetail(goodsDetail);
// 设置订单绝对超时时间
model.setTimeExpire("2016-12-31 10:05:00");
// 设置业务扩展参数
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId("2088511833207846");
extendParams.setHbFqSellerPercent("100");
extendParams.setHbFqNum("3");
extendParams.setIndustryRefluxInfo("{\"scene_code\":\"metro_tradeorder\",\"channel\":\"xxxx\",\"scene_data\":{\"asset_name\":\"ALIPAY\"}}");
extendParams.setRoyaltyFreeze("true");
extendParams.setCardType("S0JP0000");
model.setExtendParams(extendParams);
// 设置商户传入业务信息
model.setBusinessParams("{\"mc_create_trade_ip\":\"127.0.0.1\"}");
// 设置公用回传参数
model.setPassbackParams("merchantBizType%3d3C%26merchantBizNo%3d2016010101111");
// 设置商户的原始订单号
model.setMerchantOrderNo("20161008001");
// 设置外部指定买家
ExtUserInfo extUserInfo = new ExtUserInfo();
extUserInfo.setCertType("IDENTITY_CARD");
extUserInfo.setCertNo("362334768769238881");
extUserInfo.setName("李明");
extUserInfo.setMobile("16587658765");
extUserInfo.setFixBuyer("F");
extUserInfo.setMinAge("18");
extUserInfo.setNeedCheckInfo("F");
extUserInfo.setIdentityHash("27bfcd1dee4f22c8fe8a2374af9b660419d1361b1c207e9b41a754a113f38fcc");
model.setExtUserInfo(extUserInfo);
request.setBizModel(model);
// 第三方代调用模式下请设置app_auth_token
// request.putOtherTextParam("app_auth_token", "<-- 请填写应用授权令牌 -->");
AlipayTradeWapPayResponse response = alipayClient.pageExecute(request, "POST");
// 如果需要返回GET请求,请使用
// AlipayTradeWapPayResponse response = alipayClient.pageExecute(request, "GET");
String pageRedirectionData = response.getBody();
System.out.println(pageRedirectionData);
if (response.isSuccess()) {
System.out.println("调用成功");
} else {
System.out.println("调用失败");
// sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接
// String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
// System.out.println(diagnosisUrl);
}
}
private static AlipayConfig getAlipayConfig() {
String privateKey = "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->";
String alipayPublicKey = "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->";
AlipayConfig alipayConfig = new AlipayConfig();
alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
alipayConfig.setAppId("<-- 请填写您的AppId,例如:2019091767145019 -->");
alipayConfig.setPrivateKey(privateKey);
alipayConfig.setFormat("json");
alipayConfig.setAlipayPublicKey(alipayPublicKey);
alipayConfig.setCharset("UTF-8");
alipayConfig.setSignType("RSA2");
return alipayConfig;
}
}
支付存在问题
若用户在订单过期前几十秒开始付款,付款成功之后,回调还没有执行完成,此时延时任务执行,订单到时间没有完成支付,订单被取消了,导致库存被还原,但实际用户已经付款了,这种情况怎么处理?
方案一
支付完成,回调时验证一下订单的状态,如果发现已经超时取消了,自动退款给用户
优点:
简单直接:在支付回调中处理订单状态,逻辑清晰,易于实现。
用户友好:如果订单已取消,自动退款给用户,避免用户因订单取消而损失资金。
缺点:
时间窗口问题:如果支付回调在订单取消后执行,可能会导致订单已取消但支付成功的状态不一致问题。
退款延迟:自动退款可能需要一定时间,用户可能会感到困惑,尤其是在支付成功后立即退款。
方案二
用户调用支付的时候,先锁定订单。如果订单到达过期时间,判断订单处于锁定状态,就不执行订单取消逻辑,发送一个延时消息,如果过几分钟检查简单还不是已支付状态,说明用户支付未成功,再取消订单
优点:
防止订单误取消:通过锁定订单,避免在支付过程中订单被取消,减少订单状态不一致的风险。
灵活性:通过延时消息,可以在支付完成后再次检查订单状态,确保订单不会被误取消。
缺点:
复杂性增加:需要实现订单锁定机制和延时消息处理,增加了系统的复杂性。
资源占用:锁定订单可能会占用系统资源,尤其是在高并发场景下,可能会影响系统性能。
延时消息的可靠性:延时消息的可靠性依赖于消息队列的稳定性,如果消息队列出现问题,可能会导致订单状态处理不及时。
解决方案
经过权衡,最终使用方案二来解决,代码实现参考发起支付和支付回调小节
支付回调
内网穿透
简介
内网穿透是指通过特定的技术手段,使得位于私有网络(如家庭或企业内部的局域网)中的设备能够被互联网上的其他设备访问。因为我们使用的开发机没有公网ip,支付宝无法访问我们的回调接口,因此我们需要使用内网穿透技术让回调接口可以被外网访问。
natapp实现内网穿透
natapp官网:https://natapp.cn/article/natapp_newbie
首先需要注册,登录,实名验证
然后购买隧道
购买一个免费的隧道即可
购买隧道之后,安装客户端,根据自己的系统来下载对应的版本
下载config.ini文件(下载地址:https://natapp.cn/article/config_ini),将其放到安装包同一目录
然后修改config.ini里面的authtoken,设置为隧道对应的authtoken(寻找方式如下图所示)
之后双击exe文件启动即可,注意,后面就是使用下图的内网穿透地址来让支付宝回调我们的接口。每次重新启动这个软件,穿透地址会不一样,注意及时替换地址
实体
{
"gmt_create": "2024-12-31 16:09:22",
"charset": "UTF-8",
"seller_email": "omldhw2845@sandbox.com",
"subject": "光明体育馆_篮球A区:2025-01-01 09:00到10:00",
"sign": "raBvcInPKPrL8Dl335wBxMpt8m2Kz8h8jjfTHR0x27p6zC5lrTTZBFi3yZSKOTHImI1ZLODvyaWgtP9B04zwJEuKtIoMMp130gdDHG+g/RZgeFYbyHXSMh8mMHCcONwT2w5a/UBatTcQuW19YB5h8aEbHLqYsiq5DDz6XJKWl5VDDhNmzqU+aNV/xmgN3OH3mkVY/QCe10PJHcb1RYlUUCxTU/iuKkIQxdkCEskuwFOC90dKGzAejvN5rOD9sZ9TofH8UWwBfx8N7o2e998QbUoTnTzOyzrlLP6gCPIochSWAWewZ2wJ451UuHWkAFL6XZJ7fQrewgK1fu4yy3aT4w==",
"buyer_id": "2088722053898514",
"invoice_amount": "50.00",
"notify_id": "2024123101222160924098510504508245",
"fund_bill_list": "[{\"amount\":\"50.00\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
"notify_type": "trade_status_sync",
"trade_status": "TRADE_SUCCESS",
"receipt_amount": "50.00",
"buyer_pay_amount": "50.00",
"app_id": "2021000143601715",
"sign_type": "RSA2",
"seller_id": "2088721053898502",
"gmt_payment": "2024-12-31 16:09:23",
"notify_time": "2024-12-31 16:09:24",
"version": "1.0",
"out_trade_no": "1874004712053325824850432",
"total_amount": "50.00",
"trade_no": "2024123122001498510504462937",
"auth_app_id": "2021000143601715",
"buyer_logon_id": "eseujj2902@sandbox.com",
"point_amount": "0.00"
}
支付服务
提供一个回调接口给支付宝调用
package com.vrs.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.vrs.common.dto.PayCallbackDTO;
import com.vrs.service.AlipayService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 支付结果回调
*
* @Author dam
* @create 2024/12/31 15:41
*/
@RestController
@RequiredArgsConstructor
@Tag(name = "支付相关")
public class PayCallbackController {
private final AlipayService alipayService;
/**
* 支付宝回调
* 调用支付宝支付后,支付宝会调用此接口发送支付结果
*/
// todo 如何校验回调接口的调用是否为支付宝调用
@PostMapping("/api/pay-service/callback/alipay")
public void callbackAlipay(@RequestParam Map<String, Object> requestParam) {
PayCallbackDTO payCallbackDTO = BeanUtil.mapToBean(requestParam, PayCallbackDTO.class, true, CopyOptions.create());
alipayService.callback(payCallbackDTO);
}
}
注意,网关登录验证不要拦截回调接口,否则支付成功之后回调不会成功。当然更合理的做法是,校验是否为支付宝进行回调,否则可能会被恶意利用。试想一下,没有身份验证,别人也能直接调用这个接口呀,那他直接调用回调接口就行了,不用付钱也能修改订单状态。
这里留个坑,后面优化一下
如果支付成功,修改支付状态和订单状态。
/**
* 支付之后的回调方法
*
* @param payCallbackDTO
*/
@Override
public void callback(PayCallbackDTO payCallbackDTO) {
if (payCallbackDTO.getTradeStatus().equals("TRADE_SUCCESS")) {
// --if-- 支付成功
QueryWrapper<PayDO> payDOQueryWrapper = new QueryWrapper<>();
payDOQueryWrapper.eq("order_sn", payCallbackDTO.getOutTradeNo());
payService.update(PayDO.builder()
.payAmount(payCallbackDTO.getBuyerPayAmount())
.payTime(payCallbackDTO.getGmtPayment())
.transactionId(payCallbackDTO.getTradeNo())
.build(), payDOQueryWrapper);
// 发送消息,通知订单服务,修改订单状态为已支付状态
orderPayProducer.sendMessage(OrderPayMqDTO.builder()
.orderSn(payCallbackDTO.getOutTradeNo())
.build());
}
}
订单服务:
package com.vrs.rocketMq.listener;
import com.vrs.annotation.Idempotent;
import com.vrs.constant.RocketMqConstant;
import com.vrs.domain.dto.mq.OrderPayMqDTO;
import com.vrs.enums.IdempotentSceneEnum;
import com.vrs.service.OrderService;
import com.vrs.templateMethod.MessageWrapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.SelectorType;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* @Author dam
* @create 2024/9/20 21:30
*/
@Slf4j(topic = RocketMqConstant.ORDER_TOPIC)
@Component
@RocketMQMessageListener(topic = RocketMqConstant.ORDER_TOPIC,
consumerGroup = RocketMqConstant.ORDER_CONSUMER_GROUP + "-" + RocketMqConstant.ORDER_PAY_TAG,
messageModel = MessageModel.CLUSTERING,
// 监听tag
selectorType = SelectorType.TAG,
selectorExpression = RocketMqConstant.ORDER_PAY_TAG
)
@RequiredArgsConstructor
public class OrderPayListener implements RocketMQListener<MessageWrapper<OrderPayMqDTO>> {
private final OrderService orderService;
/**
* 消费消息的方法
* 方法报错就会拒收消息
*
* @param messageWrapper 消息内容,类型和上面的泛型一致。如果泛型指定了固定的类型,消息体就是我们的参数
*/
@Idempotent(
uniqueKeyPrefix = "order_pay:",
key = "#messageWrapper.getMessage().getOrderSn()",
scene = IdempotentSceneEnum.MQ,
keyTimeout = 3600L
)
@SneakyThrows
@Override
public void onMessage(MessageWrapper<OrderPayMqDTO> messageWrapper) {
// 开头打印日志,平常可 Debug 看任务参数,线上可报平安(比如消息是否消费,重新投递时获取参数等)
log.info("[消费者] 修改订单为已支付状态:{}", messageWrapper.getMessage().getOrderSn());
String orderSn = messageWrapper.getMessage().getOrderSn();
orderService.payOrder(orderSn);
}
}