支付宝沙箱环境
蚂蚁沙箱环境(Beta)是协助开发者进行接口功能开发及主要功能联调的辅助环境。沙箱环境模拟了开放平台部分产品的主要功能和主要逻辑(当前沙箱支持产品请参考“沙箱支持产品列表”)。在开发者应用上线审核前,开发者可以根据自身需求,先在沙箱环境中了解、组合和调试各种开放接口,进行开发调通工作,从而帮助开发者在应用上线审核完成后,能更快速、更顺利的进行线上调试和验收工作。如何使用和配置沙箱环境请参考《沙箱环境使用说明》。
注意:
由于沙箱为模拟环境,在沙箱完成接口开发及主要功能调试后,请务必在蚂蚁正式环境进行完整的功能验收测试。所有返回码及业务逻辑以正式环境为准。
为保证沙箱稳定,沙箱环境测试数据会进行定期数据清理。Beta测试阶段每周日中午12点至每周一中午12点为维护时间。在此时间内沙箱环境部分功能可能会不可用,敬请谅解。
请勿在沙箱进行压力测试,以免触发相应的限流措施,导致无法正常使用沙箱环境。
沙箱支持的各个开放产品,沙箱使用的特别说明请参考各产品的快速接入文档或技术接入文档章节。
支付宝沙箱环境地址:支付宝沙箱环境
开发之前需要配置支付宝沙箱环境,如果不会配置,请看博客:支付宝沙箱环境配置详细步骤
支付宝支付流程
大体流程就是第三方平台发起支付请求,传入订单信息,支付宝返回一个支付的form表单,第三方平台渲染出这个表单,待客户支付完成后,支付宝会发起回调,一个是同步回调,是支付宝重定向的,主要是提高用户体验,告诉客户支付完毕,另一个是异步回调,支付宝通过http请求向第三方平台发起支付回调,这个请求是让第三方平台更改订单状态,保证数据的一致性的。支付完成异步回调支付宝需要第三方平台必须打印"success",如果没有打印"success",支付宝会有重试机制,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h),这就需要我们处理好幂等性问题,无论支付宝重试多少次,请求获得的结果都一样。如果不处理好幂等性问题,就会出现"本来支付10元换10个鱼翅,结果换了20个30个鱼翅"。常见的处理幂等性的方法是使用全局id+日志记录的方式防止这种错误。
支付宝支付流程
1、创建支付信息表CREATE TABLE `payment_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userid` int(11) DEFAULT NULL COMMENT '客户id',
`typeid` int(2) DEFAULT NULL COMMENT '支付类型',
`orderid` varchar(50) DEFAULT NULL COMMENT '订单id',
`price` decimal(10,0) DEFAULT NULL COMMENT '价格',
`source` varchar(10) DEFAULT NULL COMMENT '支付信息来源',
`state` int(2) DEFAULT NULL COMMENT '支付状态',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
`platformorderid` varchar(100) DEFAULT NULL COMMENT '支付宝第三方ID',
`payMessage` varchar(10000) DEFAULT NULL COMMENT '支付信息',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8;
2、创建支付令牌
1)创建支付请求,生成一条支付数据
2)生成支付令牌pay_token,存放在redis中,key为pay_token,value为支付id。
3)最后将pay_token返回
支付服务生成支付令牌代码如下://生成支付令牌接口
@Override
public ResponseBasecreateToken(@RequestBody PaymentInfo paymentInfo){
//1、创建支付请求信息,生成一条支付数据
Integer row =paymentInfoDao.savePaymentType(paymentInfo);
if(row<=0){
return setResultError("创建支付订单失败");
}
//2、生成对应的token
String payToken = TokenUtils.getPayToken();
//3、存放在redis中,key为:token,value为:支付id,过期时间15分钟
baseRedisService.setString(payToken,paymentInfo.getId()+"", Constants.PAY_TOKEN_TIME);
//4、返回token
JSONObject result =new JSONObject();
result.put("payToken",payToken);
return setResultSuccess(result);
}
3、使用支付令牌查找支付数据,并发起支付请求,获取支付宝返回的form表单数据
1)参数校验,判断支付令牌pay_token是否失效或者已经过期
2)使用支付令牌pay_token去redis中查询出支付id
3)使用支付id去数据库中查询出支付信息。
4)利用支付信息下单,调用支付宝api,获取网页支付的form表单
5)最后返回form表单数据
支付服务根据支付令牌获取form表单代码如下:/**
* 使用支付令牌查找支付信息生成网页支付的form表单
* @param payToken
* @return
*/
@Override
public ResponseBasealiPagePay(@RequestParam("payToken") String payToken){
//1、校验参数
if(StringUtils.isEmpty(payToken)){
return setResultError("token不能为空");
}
//2、判断token有效期
String payId = (String)baseRedisService.getString(payToken);
if(StringUtils.isEmpty(payId)){
return setResultError("支付已经超时");
}
//4、使用支付id,进行下单
//5、使用支付id查询支付信息
PaymentInfo paymentInfo =paymentInfoDao.getPaymentInfo(Long.parseLong(payId));
if(paymentInfo==null){
return setResultError("未找到支付信息");
}
//6、对接支付代码,返回提交支付的form表单元素的token
//获得初始化的AlipayClient
//获得初始化的AlipayClient
AlipayClient alipayClient =new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);
//设置请求参数(网页端登录)
AlipayTradePagePayRequest alipayRequest =new AlipayTradePagePayRequest();
//设置同步回调地址
alipayRequest.setReturnUrl(AlipayConfig.return_url);
//设置异步回调地址
alipayRequest.setNotifyUrl(AlipayConfig.notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
Stringout_trade_no= paymentInfo.getOrderId();
//付款金额,必填,企业金额,单位:分
Stringtotal_amount= paymentInfo.getPrice()+"";
//订单名称,必填
String subject ="刘德煌会员";
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+"\"total_amount\":\""+ total_amount +"\","
+"\"subject\":\""+ subject +"\","
//+ "\"body\":\""+ body +"\","
+"\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
//若想给BizContent增加其他可选请求参数,以增加自定义超时时间参数timeout_express来举例说明
//alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
// + "\"total_amount\":\""+ total_amount +"\","
// + "\"subject\":\""+ subject +"\","
// + "\"body\":\""+ body +"\","
// + "\"timeout_express\":\"10m\","
// + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
//请求参数可查阅【电脑网站支付的API文档-alipay.trade.page.pay-请求参数】章节
//请求
try{
String result = alipayClient.pageExecute(alipayRequest).getBody();
AlipayTradePagePayResponse response = alipayClient.pageExecute(alipayRequest);
log.info("body:{}",response.getBody());
JSONObject data =new JSONObject();
data.put("payHtml",result);
return setResultSuccess(data);
}catch (Exception e){
return setResultError("支付异常");
}
}
pc-web端控制层调用支付服务流程:
pc-web控制层调用支付服务流程:
1、获取前端传入的支付令牌,验证支付令牌参数的正确性
2、调用支付服务根据支付令牌获取支付form表单元素接口,获取支付form表单
3、在页面上渲染出支付form表单元素//使用payToken进行支付
@RequestMapping("/aliPay")
public void aliPay(String payToken, HttpServletResponse response)throws IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
//1、参数验证
if(StringUtils.isEmpty(payToken)){
return;
}
//2、调用支付服务接口,获取支付宝html元素
ResponseBase payTokenResult =payServiceFeign.aliPagePay(payToken);
if(!payTokenResult.getCode().equals(Constants.HTTP_RES_CODE_200)){
String message = payTokenResult.getMsg();
writer.print(message);
return;
}
//3、返回可以执行的html元素给客户端
LinkedHashMap map = (LinkedHashMap) payTokenResult.getData();
String payHtml = (String)map.get("payHtml");
log.info("payHtml:{}",payHtml);
//4、页面上渲染出form表单
writer.write(payHtml);
writer.close();
}
待客户支付完毕后,支付宝会发起两个回调,一个是同步回调,一个是异步回调。同步回调是支付宝重定向到第三方事先填好的同步回调地址,告诉客户支付完毕,异步回调则是支付宝发起http请求给第三方平台,让第三方平台更改支付状态以及订单状态。
支付宝同步回调流程
pc-web端控制层同步回调控制层synCallBack方法流程:
1、接收到支付宝同步回调的参数后,封装成map
2、调用支付服务的同步回调接口,告诉客户支付完成。
注意:支付宝同步回调时通过重定向的方式回调到第三方平台,如果数据不加以处理,同步回调的参数会显示在地址栏中,而这些参数中又有比较重要的信息如:商户订单号、支付宝交易号等。为避免这些数据在地址栏出现,我们可以把这些参数封装成form表单,浏览器模拟提交form表单请求,以post请求提交表单,隐藏同步回调的参数。
pc-web端控制层同步回调方法代码如下:/**
* 同步通知
*/
@RequestMapping("/synCallBack")
public void synCallBack(HttpServletRequest request, HttpServletResponse response)throws IOException {
response.setContentType("text/html;charset=utf-8");
Map params =new HashMap();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr ="";
for (int i =0; i < values.length; i++) {
valueStr = (i == values.length -1) ? valueStr + values[i]
: valueStr + values[i] +",";
}
//乱码解决,这段代码在出现乱码时使用
valueStr =new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
log.info("#####支付宝同步回调CallBackController#####synCallBack开始params:{}",params);
PrintWriter writer = response.getWriter();
ResponseBase synCallBackResponseBase =callBackServiceFeign.synCallBack(params);
if(!synCallBackResponseBase.getCode().equals(Constants.HTTP_RES_CODE_200)){
//保错页面
return;
}
LinkedHashMap data = (LinkedHashMap) synCallBackResponseBase.getData();
//封装成html的form表单,浏览器模拟提交(为了避免支付宝回调地址上的get请求参数显示在地址栏,因此模拟表单请求,隐藏同步回调的参数)
String htmlFrom ="
+" method='post' action='http://127.0.0.1/alibaba/callBack/synSuccessPage' >"
+""
+""
+""
+""
+"";
writer.println(htmlFrom);
writer.close();
log.info("#####支付宝同步回调CallBackController#####synCallBack结束params:{}",params);
}
pc-web端控制层以post请求提交表单后处理请求的方法如下。//以post请求隐藏参数
@RequestMapping(method = RequestMethod.POST,value ="/synSuccessPage")
public StringsynSuccessPage(HttpServletRequest request,String outTradeNo,String tradeNo,String totalAmount){
request.setAttribute("outTradeNo",outTradeNo);
request.setAttribute("tradeNo",tradeNo);
request.setAttribute("totalAmount",totalAmount);
return PAY_SUCCESS;
}
支付服务同步回调接口流程:
1、对pc-web端传入的参数进行验签。
2、验签成功返回订单相关信息的json给pc-web控制层,验签失败,返回验签失败
支付服务同步回调代码如下:@Override
public ResponseBasesynCallBack(@RequestParam Map params) {
//1、日志记录
log.info("#####支付宝同步通知synCallBack开始,params:{}",params);
//2、验签操作
try{
//调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
log.info("#####支付宝同步通知signVerified:{}",signVerified);
//——请在这里编写您的程序(以下代码仅作参考)——
if(!signVerified) {
return setResultError("验签失败");
}
//商户订单号
String outTradeNo = params.get("out_trade_no");
//支付宝交易号
String tradeNo = params.get("trade_no");
//付款金额
String totalAmount = params.get("total_amount");
JSONObject json =new JSONObject();
json.put("outTradeNo",outTradeNo);
json.put("tradeNo",tradeNo);
json.put("totalAmount",totalAmount);
return setResultSuccess(json);
}catch (Exception e){
log.info("#####支付宝同步通知出现异常,ERROR:",e);
return setResultError("系统错误");
}finally {
log.info("#####支付宝同步通知synCallBack结束,params:{}",params);
}
}
支付宝异步回调流程
支付宝异步回调需要第三方平台输出打印success才算成功,否则支付宝会不断的重试,这就需要处理号幂等性问题。
pc-web端控制层异步回调方法流程:
1、接收到支付宝发送过来的异步回调http请求,把参数封装成map
2、调用支付服务异步回调接口
3、最后返回结果。
pc-web端控制层异步回调方法代码如下:/**
* 异步通知
* @param request
* @param response
* @return
*/
@RequestMapping("/asynCallBack")
@ResponseBody
public StringasynCallBack(HttpServletRequest request, HttpServletResponse response){
Map params =new HashMap();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr ="";
for (int i =0; i < values.length; i++) {
valueStr = (i == values.length -1) ? valueStr + values[i]
: valueStr + values[i] +",";
}
/* //乱码解决,这段代码在出现乱码时使用valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");*/
params.put(name, valueStr);
}
log.info("#####支付宝同步回调CallBackController#####synCallBack开始params:{}",params);
String result =callBackServiceFeign.asynCallBack(params);
return result;
}
支付服务异步回调接口流程:
1、对pc-web传入的参数进行验签
2、根据参数中的订单号查询支付信息。
3、利用订单号作为全局id,解决幂等性问题,判断支付信息中的支付状态,如果是支付成功,则直接返回success,防止支付宝重试带来的幂等性问题。
4、修改支付信息表的支付状态。
5、调用订单服务,通知订单服务更改订单状态(注意:这里需要注意如果订单服务调用失败,注意事务回滚)
支付服务异步回调接口代码如下:/**
* 异步通知
* @param params
* @return
*/
@LcnTransaction
@Transactional
@Override
public synchronized StringasynCallBack(@RequestParam Map params) {
//1、日志记录
log.info("#####支付宝异步通知synCallBack开始,params:{}",params);
//2、验签操作
try{
//调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
log.info("#####支付宝异步通知signVerified:{}",signVerified);
//——请在这里编写您的程序(以下代码仅作参考)——
if(!signVerified) {
return Constants.PAY_FAIL;
}
//商户订单号
String outTradeNo = params.get("out_trade_no");
//修改支付数据库
//使用订单号作为全局id解决幂等性问题,如果有网络延迟,需要采用分布式锁
PaymentInfo payInfo =paymentInfoDao.getByOrderIdPayInfo(outTradeNo);
if(payInfo==null){
return Constants.PAY_FAIL;
}
//支付宝重试机制
Integer state = payInfo.getState();
if(state ==1){
return Constants.PAY_SUCCESS;
}
//支付宝交易号
String tradeNo = params.get("trade_no");
//付款金额
String totalAmount = params.get("total_amount");
//判断实际付款金额与商品金额是否一致
/* if(!totalAmount.equals(String.valueOf(payInfo.getPrice()))){
return Constants.PAY_FAIL;
}*/
//标识为该订单已支付
payInfo.setState(1);
//支付宝返回的参数
payInfo.setPayMessage(params.toString());
//设置第三方交易id
payInfo.setPlatformorderId(tradeNo);
//手动事务开启
int row =paymentInfoDao.updatePayInfo(payInfo);
if(row <=0){
return Constants.PAY_FAIL;
}
//调用订单数据库通知支付状态
ResponseBase responseBase =orderServiceFeign.updateOrder(1L, tradeNo, outTradeNo);
if(!responseBase.getCode().equals(Constants.HTTP_RES_CODE_200)){
//回滚 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Constants.PAY_FAIL;
}
//手动事务提交
return Constants.PAY_SUCCESS;
}catch (Exception e){
log.info("#####支付宝异步通知出现异常,ERROR:",e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Constants.PAY_FAIL;
}finally {
log.info("#####支付宝异步通知synCallBack结束,params:{}",params);
}
}
注意:在这个异步回调的过程中,如果不注意,假如在通知订单修改订单状态的后面出现代码异常,由于由事务的存在,本地的支付数据库回滚,当前支付状态回滚为未支付,但订单服务那边的订单状态已经修改成功,所以订单服务的支付状态为已支付,这是典型的分布式事务场景。在这里我是采用的LCN分布式事务解决框架解决的。在后面的笔记中,我会写一些关于分布式事务解决方案的文章。
支付宝关于异步回调的说明:支付宝异步回调说明