应用场景:需要支付宝预授权功能的APP、H5和小程序服务端。编辑于2022/09/14
前言
一、来看预授权是什么
二、写在代码之前
接入准备:
接入服务端 SDK:
三、服务端代码
1.授权冻结接口
接口部分:
随机流水号部分:
公共请求参数部分:
2.冻结转支付
3.解冻
4.异步通知回调
5.撤销冻结
6.授权操作查询
充当总结的部分
刚开始接触预授权功能的时候并没有找到很多最新的资料,而预授权功能随着“租借”场景增多使用的频率也越来越多。我对接的时候出现了一些疑问,所以这篇文章旨在提供较新的预授权服务端接入参考,避免走太多弯路。首先呢,先拿出官方文档啦~~支付宝预授权产品介绍 | 网页&移动应用支付宝文档中心https://opendocs.alipay.com/open/20180417160701241302/intro?ref=api
商家在需要用户提前出资担保的消费场景下(如租车、充电桩、酒店预订等)用户在开启服务时做一笔资金授权,当服务完结算时,再从授权资金中扣除消费金额,剩余返还给用户。可以浅浅的理解为交了一笔押金,从押金里消费扣钱,然后退押金。
对接预授权这部分前,如没有使用过支付宝产品的话就得创建你的应用,绑定商家账号等。这部分挺简单的,官方文档有详细步骤而且有其他文章写到这方面,所以这里就不再赘述啦。
pom里粘贴依赖然后install一下就行了,目前最新版本就是这个。如果反复下载爆红就检查镜像、重启项目,也可能需要github加速。
com.alipay.sdk
alipay-sdk-java
4.31.84.ALL
一步引入SDK成功后就可以直接开发了。
大部分情况是前端直接调用冻结,然后唤醒前台支付宝的支付功能。放上较为完整的接口,删减了其他业务处理,这里只post资金冻结。
/**
* 资金冻结
*
* @author FaithfulKiller
* @since 2022/09/14
*/
@RequestMapping("/freezeorder")
public Object fundAuthOrderAppFreeze(@RequestBody Map params) throws AlipayApiException {
try {
//我只处理了订单号和订单金额这两个最重要的参数,其他写成固定的。并不是只可以传这两个参数,按需求自定
String orderId = params.get("orderId")==null?"":params.get("orderId").toString();
String amount = params.get("amount")==null?"":params.get("amount").toString();
//这里抛出的是自定义的异常类,有需要的话换成你自己的异常处理
if (StringUtils.isEmpty(orderId) || StringUtils.isEmpty(amount)) {
// 捕获频繁出现的空指针异常
log.info("参数错误!");
return new ErrorResponseData(ResponseConfig.PARA_ERROR_CODE,
"系统正忙,稍后再试!");
}
//初始化client,getClientParams()为公共请求参数方法,见下文
AlipayClient alipayClient = new DefaultAlipayClient(getClientParams());
//请求
AlipayFundAuthOrderAppFreezeRequest request = new AlipayFundAuthOrderAppFreezeRequest();
//用来存放信息字段的对象
AlipayFundAuthOrderAppFreezeModel model = new AlipayFundAuthOrderAppFreezeModel();
model.setOrderTitle("预支付资金授权");//订单标题。业务订单的简单描述或商品名称等
model.setOutOrderNo(orderId);//商户授权资金订单号。在商户端不重复
model.setOutRequestNo(genOrderNo());//商户本次资金操作的请求流水号,用于标示请求流水的唯一性。genOrderNo()详见下文
model.setAmount(amount);//冻结的金额
model.setProductCode("PRE_AUTH_ONLINE");//PRE_AUTH_ONLINE为预授权产品固定值表示冻结操作,不要替换
//以上为必填参数,以下为非必填参数(基本用不到,视需求的而定)
//model.setPayeeUserId(payee_user_id);//收款账户的支付宝用户号
//model.setPayeeLogonId(payee_logon_id);//收款账户的支付宝登录号
//model.setTimeoutExpress(timeout_express);//超时时间,m-分钟,h-小时,d-天
//model.setEnablePayChannels(enable_pay_channels);//指定支付渠道,json格式
//model.setDisablePayChannels(disable_pay_channels);//该参数禁用支付渠道,json格式
//model.setIdentityParams(identity_params);//买家实名信息,json格式
//model.setExtraParam(extra_param);//用于特定业务信息的传递,json格式
//设置参数
request.setBizModel(model);
request.setNotifyUrl(AlipayConfig.preNotifyUrl);//异步通知地址(必填)该接口只通过该参数进行异步通知,定义在常量里,替换成你需要的异步地址
AlipayFundAuthOrderAppFreezeResponse response = alipayClient.sdkExecute(request);//注意这里是sdkExecute,可以获取签名参数
if (response.isSuccess()) {
System.out.println("调用成功");
System.out.println(response.getBody());//签名后的参数,直接作为 orderStr 入参到my.tradePay接口
return new SuccessResponseData(ResponseData.DEFAULT_SUCCESS_CODE, response.getBody(), null);
} else {
System.out.println("调用失败");
return new ErrorResponseData(ResponseConfig.SYSTEM_ERROR_CODE,
"调用失败");
}
} catch (Exception e) {
e.printStackTrace();
log.info("系统异常");
return new ErrorResponseData(ResponseConfig.SYSTEM_ERROR_CODE,
"系统正忙,稍后再试");
}
}
方法不唯一,只要确定每次请求接口都会有新的流水号就行。
/**
* 生成随机流水号
*
* @return
*/
public static String genOrderNo() {
long num = System.currentTimeMillis() + (long) (Math.random() * 10000000L);
return String.valueOf(num);
}
每个预授权功能都会调用此方法。参数具体信息都定义在配置类,根据自己业务需要按照技术文档编写。
/**
* 公共请求参数
*
* @author FaithfulKIller
* @since 2022/09/14
*/
private static CertAlipayRequest getClientParams() {
CertAlipayRequest certParams = new CertAlipayRequest();
certParams.setServerUrl("https://openapi.alipay.com/gateway.do");
//AppId
certParams.setAppId(AlipayConfig.app_id);
//PKCS8格式的应用私钥
certParams.setPrivateKey(AlipayConfig.privateKey);
//字符集编码,推荐采用utf-8
certParams.setCharset(AlipayConfig.input_charset);
certParams.setFormat("json");
certParams.setSignType(AlipayConfig.sign_type);
//应用公钥证书文件路径
certParams.setCertPath(AlipayConfig.app_cert_path);
//支付宝公钥证书文件路径
certParams.setAlipayPublicCertPath(AlipayConfig.alipay_cert_path);
//支付宝根证书文件路径
certParams.setRootCertPath(AlipayConfig.alipay_root_cert_path);
return certParams;
}
交易结算调用转支付,因为不局限于用户主动完成结算(按产品需求而定),放在后台处理调用比较好。
ps:传入COMPLETE时,用户剩余未消费金额会自动解冻,不需要再做退款业务处理。NOT_COMPLETE为转交易完成后不解冻剩余冻结金额。公共请求参数见上文。
/**
* 冻结转支付
*
* @auther FaithfulKiller
* @since 2022/09/14
*/
public boolean tradePay(String outTradeNo,String amount,String authNo) {
try {
log.info("=============支付宝预授权冻结转支付============");
// 捕获频繁出现的空指针异常
if (StringUtils.isEmpty(outTradeNo) && StringUtils.isEmpty(authNo) && StringUtils.isEmpty(amount)) {
//自定义的异常类,换成你自己的异常处理
log.info("冻结转支付参数错误!");
return new ErrorResponseData(ResponseConfig.PARA_ERROR_CODE,
"系统正忙,稍后再试!");
}
//初始化client
AlipayClient alipayClient = new DefaultAlipayClient(getClientParams());
//初始化请求体
AlipayTradePayRequest request = new AlipayTradePayRequest();
//用来存放信息字段的对象
AlipayTradePayModel model = new AlipayTradePayModel();
model.setOutTradeNo(outTradeNo); // 预授权转支付商户订单号,为新的商家交易流水号
model.setAuthNo(authNo); // 预授权冻结交易号(资金预授权单号)
model.setTotalAmount(amount); // 订单总金额。单位为元,精确到小数点后两位,
model.setSubject("预授权冻结转支付"); // 解冻转支付标题,用于展示在支付宝账单中
model.setAuthConfirmMode("COMPLETE");//传入COMPLETE时,用户剩余金额会自动解冻
model.setProductCode("PRE_AUTH_ONLINE"); // 固定值PRE_AUTH_ONLINE 产品码。
//以上为必填参数,以下为非必填参数
model.setBody("预授权解冻转支付测试"); // 备注信息
//model.setSellerId(sellerId); // 填写卖家支付宝账户pid
//model.setBuyerId(buyerId); // 填写预授权用户uid,通过预授权冻结接口返回的payer_user_id字段获取(支付宝userId)
//其他非必传参数可见官方文档
//设置参数
request.setBizModel(model);
//异步通知地址(必填)该接口只通过该参数进行异步通知,定义在常量里,替换成你需要的异步地址
request.setNotifyUrl(AlipayConfig.preNotifyUrl);
//注意这里是sdkExecute,可以获取签名参数
AlipayTradePayResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
System.out.println("调用成功");
return true;
} else {
System.out.println("调用失败");
return false;
}
} catch (Exception e) {
e.printStackTrace();
log.info("系统异常");
return false;
}
}
若出现用户主动或者其他原因需要退款,则进行解冻操作。
以下就只放上方法体吧,这样更清晰一些。(其实是懒 #^_^# )
log.info("=============支付宝预授权解冻============");
try {
AlipayClient alipayClient = new DefaultAlipayClient(getClientParams());
//解冻的请求体
AlipayFundAuthOrderUnfreezeRequest request = new AlipayFundAuthOrderUnfreezeRequest();
//这里json方式和上面model可以看作没区别,都试试呗,多了解,有兴趣就看看源码。
JSONObject bizContent = new JSONObject();
bizContent.put("auth_no",authNo);
bizContent.put("out_request_no",outRequestNo);
bizContent.put("amount",amount);
bizContent.put("remark","预授权资金解冻");
//设置参数
request.setBizContent(bizContent.toString());
request.setNotifyUrl(AlipayConfig.preNotifyUrl);//异步通知地址,必填,该接口只通过该参数进行异步通知
// 使用execute方法发起请求
AlipayFundAuthOrderUnfreezeResponse response = alipayClient.execute(request);
//因为我这里写的后台调用,执行成功与否就是
if (response.isSuccess()) {
System.out.println("调用成功");
return true;
} else {
System.out.println("调用失败");
return false;
}
} catch (Exception e) {
e.printStackTrace();
log.info("系统异常");
return false;
}
这个很重要也比较多,芝麻返回的信息都通过这个接口。按照你的业务需求处理返回数据。
/**
* 资金预授权回调接口(支付宝)
*
* @auther FaithfulKiller
* @since 2022/09/14
*/
@RequestMapping("/preauthnotify")
public void preAuthNotify(HttpServletRequest request,HttpServletResponse response){
try {
//调用业务处理
preAuthCallback(request,response);
} catch (IOException e) {
e.printStackTrace();
} catch (AlipayApiException e) {
e.printStackTrace();
}
}
/**
* 资金预授权回调业务处理(支付宝)
*
* @auther FaithfulKiller
* @since 2022/09/14
*/
public void preAuthCallback(final HttpServletRequest request,HttpServletResponse response) throws IOException, AlipayApiException {
Map requestParams = request.getParameterMap();
String notifyParamStr = JSONObject.toJSONString(requestParams);
log.warn("回调通知参数:", notifyParamStr);
String notifyResult = "false";
//验证入参
Map params = new HashMap();
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);
}
// 官方提供的公钥证书验签
//params – 待验签的从支付宝接收到的参数Map
//alipayPublicCertPath – 支付宝公钥证书本地路径
//charset – 参数内容编码集
//signType – 指定采用的签名方式,RSA2
boolean verify_result = AlipaySignature.rsaCertCheckV1(params, AlipayConfig.alipay_cert_path, AlipayConfig.input_charset, "RSA2");
// Boolean verify_result=true;
if(verify_result) {
log.info("预授权支付宝回调签名认证成功");
//执行业务逻辑
String notifyType = request.getParameter("notify_type")==null?"":request.getParameter("notify_type");// 通知类型
try {
//参数有很多,异步参数通知说明里有全部的解释,这里只附上我用到的部分参数
String authNo = request.getParameter("auth_no")==null?"":request.getParameter("auth_no"); // 支付宝的资金授权订单号
String outOrderNo = request.getParameter("out_order_no")==null?"":request.getParameter("out_order_no"); // 商户的授权资金订单号
String operationId = request.getParameter("operation_id")==null?"":request.getParameter("operation_id");//支付宝的资金操作流水号
String amount = request.getParameter("amount")==null?"":request.getParameter("amount");//本次操作的金额,单位为:元(人民币),精确到小数点后两位
String gmtCreate = request.getParameter("gmt_create")==null?"":request.getParameter("gmt_create");// 明细创建时间
String status = request.getParameter("status")==null?"":request.getParameter("status");//资金预授权明细的状态目前支持:INIT:初始 SUCCESS: 成功 CLOSED:关闭
String operationType = request.getParameter("operation_type")==null?"":request.getParameter("operation_type");// 资金操作类型FREEZE:冻结UNFREEZE:解冻 PAY:转交易
String payerUserId = request.getParameter("payer_user_id")==null?"":request.getParameter("payer_user_id");//付款方
String payeeUserId = request.getParameter("payee_user_id")==null?"":request.getParameter("payee_user_id");//收款方
if ("fund_auth_freeze".equals(notifyType)) {
// 预授权冻结回调
//写出你的业务代码············
}else
if("trade_status_sync".equals(notifyType)){
// 预授权转支付回调 trade_status是转支付特有的
String tradeStatus = request.getParameter("trade_status");
//交易信息 有两种,都表示交易成功
if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")){
//写出你的业务代码············
}
}else
if("fund_auth_unfreeze".equals(notifyType)){
//预授权解冻回调
//写出你的业务代码············
}
} catch (Exception e) {
log.error("预授权支付宝回调业务处理报错,params:" + notifyParamStr +e.getMessage());
}
} else {
log.info("预授权支付宝回调签名认证失败,signVerified=false, paramsJson:", notifyParamStr);
}
//向芝麻反馈处理是否成功
PrintWriter writer = null;
try {
writer = response.getWriter();
if ("success".equals(notifyResult)){
writer.write("success"); //回调成功
}else {
writer.write("false"); //回调失败
}
writer.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (writer != null) {
writer.close();
}
}
}
无法确认冻结操作是否成功时调用撤销接口。
AlipayClient alipayClient = new DefaultAlipayClient(getClientParams());
//撤销的请求体
AlipayFundAuthOperationCancelRequest request = new AlipayFundAuthOperationCancelRequest();
//用model方式也行,都一样
JSONObject bizContent = new JSONObject();
bizContent.put("auth_no",authNo);// out_order_no与auth_no选择其一传入即可
bizContent.put("out_request_no",outRequestNo);// out_request_no与operation_id选择其一传入即可
bizContent.put("remark",remark);//商户对本次撤销操作的附言描述。
// 设置整体请求参数
request.setBizContent(bizContent.toString());
request.setNotifyUrl(AlipayConfig.preNotifyUrl);//异步通知地址
// 使用execute方法发起请求
AlipayFundAuthOperationCancelResponse response = alipayClient.execute(request);
这个入参比较灵活了,需要订单信息查什么,返回的参数有很多。
AlipayClient alipayClient = new DefaultAlipayClient(getClientParams());//初始化client
//查询的请求体
AlipayFundAuthOperationDetailQueryRequest request = new AlipayFundAuthOperationDetailQueryRequest();
AlipayFundAuthOperationDetailQueryModel model = new AlipayFundAuthOperationDetailQueryModel();
model.setAuthNo(authNo); // out_order_no与auth_no选择其一传入即可
model.setOutRequestNo(outRequestNo); // out_request_no与operation_id选择其一传入即可
model.setOperationType(operationType);//可选值FREEZE/UNFREEZE/PAY,分别对应冻结、解冻、支付明细类型;
//设置整体请求参数
request.setBizModel(model);
request.setNotifyUrl(AlipayConfig.preNotifyUrl);//异步通知地址
// 使用execute方法发起请求
AlipayFundAuthOperationDetailQueryResponse response = alipayClient.execute(request);
兄弟门(没打错字 :D)一定要看官方文档,入参出参大部分都在官方文档,但是有少部分官网也没有,恰巧你有需要那只能花时间找了。