支付宝预授权 & 预授权转支付
简介
支付宝预授权是支付宝小程序针对商家在需要用户提前出资担保的消费场景下(如租车、充电桩、酒店预订等),推出的功能。用户在开启服务时需要做一笔资金授权,当服务完结算时,再从预授权资金中扣除消费金额,剩余返还给用户。
支付宝预授权 支持余额,余额宝,信用卡,借记卡,花呗以及芝麻信用等渠道,其中:
- 余额,余额宝做资金冻结。
- 信用卡,借记卡扣款至支付宝内部账户做资金锁定。
- 花呗锁定额度(推荐),不产生账期,用户不需要马上还款(暂时免费开放,需发送邮件申请)。
- 芝麻信用以用户信用为担保并授权。
授权流程
用户线上提交订单,触发商户的预授权功能,并打开授权确认界面,用户仅需在支付宝收银台中输入支付密码,确认授权后,即可完成授权。业务流程如下图所示:
以充电桩场景下的支付宝预授权为例:
用户搜索访问小程序或扫码访问小程序以后,点击充电桩首页 立即充电;
商户后台生成订单,并且在小程序里自动跳转至支付宝资金预授权界面;
用户在支付宝收银台中输入支付密码,确认授权;
支付宝将信用授权结果同步返回给商户;
消费完成后实际结算时,商家根据实际消费情况直接从授权资金中发起授权转支付,用户无需参与,仅在实际扣款成功后收到推送通知。
预授权调用说明
支付宝预授权根据资金冻结方式分为资金授权和信用授权。
-
资金授权
在充电桩场景,用户扫码后在充电应用中设置充电模式后,选择预授权,进入支付宝预授权,资金授权后开启充电;充电完毕后,商户调用支付宝预授权转支付直接扣款。
-
信用授权
在充电桩场景,场景开启信用渠道后,用户扫码后在充电应用中设置充电模式后,选择信用充,进入支付宝预授权,信用授权/资金授权后开启充电;充电完毕后,商户调用支付宝预授权转支付直接扣款。
代码(此处为资金授权,非信用授权)
请求参数封装类
@Data
@ApiModel(description = "授权冻结资金请求参数")
public class AlipayFundAuthOrderAppFreezeReq {
@ApiModelProperty(value = "商户授权资金订单号",required = true)
private String outOrderNo;
@ApiModelProperty(value = "商户本次资金操作的请求流水号",required = true)
private String outRequestNo;
@ApiModelProperty(value = "业务订单的简单描述,如商品名称等",required = true)
private String orderTitle;
@ApiModelProperty(value = "需要冻结的金额,精确到小数点后两位",required = true)
private String amount;
@ApiModelProperty(value = "销售产品码,支付宝预授权产品取值PRE_AUTH_ONLINE ",required = true)
private String productCode;
@ApiModelProperty(value = "收款方支付宝账号")
private String payeeLogonId;
@ApiModelProperty(value = "收款方的支付宝唯一用户号",required = true)
private String payeeUserId;
@ApiModelProperty(value = "该笔订单允许的最晚付款时间")
private String payTimeout;
@ApiModelProperty(value = "业务扩展参数 RENT_SHARABLE_CHARGERS")
private String extraParam;
@ApiModelProperty(value = "场景码,用于区分预授权不同业务场景 ONLINE_AUTH_COMMON_SCENE ")
private String sceneCode;
@ApiModelProperty(value = "标价币种")
private String transCurrency;
@ApiModelProperty(value = "商户指定的结算币种")
private String settleCurrency;
@ApiModelProperty(value = "商户可用该参数指定用户可使用的支付渠道")
private String enablePayChannels;
@ApiModelProperty(value = "商户可用该参数指定禁止使用的支付渠道")
private String disablePayChannels;
@ApiModelProperty(value = "用户实名信息参数")
private String identityParams;
}
预授权代码
@Override
@Transactional(rollbackFor = Exception.class)
public AlipayFundAuthOrderAppFreezeResponse getFundAuthOrderAppFreeze(AlipayFundAuthOrderAppFreezeReq req) {
AlipayFundAuthOrderAppFreezeRequest alipayRequest = new AlipayFundAuthOrderAppFreezeRequest();
alipayRequest.setBizContent(setValue(req));
// 回调地址(在配置文件中配置)
alipayRequest.setNotifyUrl(alipayConfig.getNotifyUrl());
try {
AlipayFundAuthOrderAppFreezeResponse response = alipayClient.sdkExecute(alipayRequest);
if (response.isSuccess()) {
System.out.println("调用成功:"+response.getBody());
} else {
System.out.println("调用失败,原因:" + response.getMsg() + "," + response.getSubMsg());
}
return response;
} catch (Exception e) {
System.out.println("调用遭遇异常,原因:" + e.getMessage());
throw new RuntimeException(e.getMessage(), e);
}
}
参数赋值方法
/**
* 将请求参数赋值
*
* @param obj
* @return String
*/
private static String setValue(Object obj) {
Map<String, String> params = new HashMap<>();
Field[] field = obj.getClass().getDeclaredFields();
if (null == field) {
return null;
}
for (Field s : field) {
String name = s.getName();
try {
Method m = obj.getClass().getMethod("get" + name.substring(0, 1).toUpperCase() + name.substring(1));
String value = (String) m.invoke(obj);
String mapName = StringUtils.humpToLine(name);
if (null != value) {
params.put(mapName, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
Object tojson = JSONObject.toJSON(params);
return tojson.toString();
}
支付宝回调类
@Slf4j
@RestController
@RequestMapping("/alipayNotify")
public class AlipayNotifyController {
private static String TRADE_SUCCESS = "TRADE_SUCCESS";
private static String TRADE_FINISHED = "TRADE_FINISHED";
@Autowired
AlipayConfig alipayConfig; // 支付宝配置类
@RequestMapping("/notify")
public void alipayNotify(HttpServletRequest request,HttpServletResponse response){
try {
notify(request,response);
} catch (IOException e) {
e.printStackTrace();
} catch (AlipayApiException e) {
e.printStackTrace();
}
}
/**
* 预授权回调
* @param request
* @param response
* @throws IOException
* @throws AlipayApiException
*/
public void notify(final HttpServletRequest request,HttpServletResponse response) throws IOException, AlipayApiException {
Map<String, String> params = convertRequestParamsToMap(request);
String paramsJson = JSON.toJSONString(params);
log.info("支付宝回调,{}", paramsJson);
//1.验签
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.getAliPublicKey(), alipayConfig.getCharset(), alipayConfig.getSignType());
if (signVerified) {
log.info("预授权支付宝回调签名认证成功");
//2.执行业务逻辑
String notifyType = request.getParameter("notify_type");
try {
if ("fund_auth_freeze".equals(notifyType)) {
// 预授权回调 业务代码...
}else if("trade_status_sync".equals(notifyType)){
// 预授权转支付回调
String tradeStatus = params.get("trade_status");
String outTradeNo = params.get("out_trade_no");
if (tradeStatus.equals(TRADE_SUCCESS) || tradeStatus.equals(TRADE_FINISHED)){
// 业务代码
}
}
} catch (Exception e) {
log.error("预授权支付宝回调业务处理报错,params:" + paramsJson, e);
}
} else {
log.info("预授权支付宝回调签名认证失败,signVerified=false, paramsJson:{}", paramsJson);
}
//3.向芝麻反馈处理是否成功
PrintWriter writer = null;
try {
writer = response.getWriter();
//一定要打印success
writer.write("success");
writer.flush();
} finally {
if (writer != null) {
writer.close();
}
}
}
开发者通过在小程序内调用 my.tradePay 接口,唤起预授权冻结流程(入参即接入流程中获取的 orderStr)。
my.tradePay({
orderStr: 'myOrderStr', // 完整的支付参数拼接成的字符串,从服务端获取
success: (res) => {
my.alert({
content: JSON.stringify(res),
});
},
fail: (res) => {
my.alert({
content: JSON.stringify(res),
});
}
});
注意:预授权是将用户的一笔资金冻结,在租借充电宝等场景下,用户扫码,发起预授权,用户归还发起预授权转支付(alipay.trade.pay)。
BUG
问题1:订单参数异常,请重新下单后再发起付款
排除问题思路:
1.用户手机问题(IOS、安卓)系统。
2.支付宝app版本问题。
3.支付宝接口变更。
4.产品app里的sdk版本太低,支付宝不支持了。
5.后端服务接口有问题。
6.支付接口参数遗漏
7.订单号重复。
8.认真检查传递的参数,例如passback_params进行二次urlencode也会提示订单参数异常
9.支付金额没有处理好,例如超出2位小数
最终原因:商家支付宝userid没传对
问题2:返回的response如何转成json格式
使用execute 不要用sdkExecute
ps:遇到bug可以去找 开发文档 - 支付宝技术支持 询问
普通支付
回调和前端唤起支付,基本同上。
唤起支付参数不同:
// .js
my.tradePay({
// 调用统一收单交易创建接口(alipay.trade.create),获得返回字段支付宝交易号trade_no
tradeNO: '201711152100110410533667792',
success: (res) => {
my.alert({
content: JSON.stringify(res),
});
},
fail: (res) => {
my.alert({
content: JSON.stringify(res),
});
}
});
关于回调
注意看文档是否有 触发类型通知,如果没有,手动调用查询接口查询状态,进行操作。