支付宝资金预授权(冻结、解冻、转支付、异步通知回调、撤销、授权操作查询)

应用场景:需要支付宝预授权功能的APP、H5和小程序服务端。编辑于2022/09/14


前言

        刚开始接触预授权功能的时候并没有找到很多最新的资料,而预授权功能随着“租借”场景增多使用的频率也越来越多。我对接的时候出现了一些疑问,所以这篇文章旨在提供较新的预授权服务端接入参考,避免走太多弯路。首先呢,先拿出官方文档啦~~支付宝预授权产品介绍 | 网页&移动应用支付宝文档中心https://opendocs.alipay.com/open/20180417160701241302/intro?ref=api


一、来看预授权是什么

        商家在需要用户提前出资担保的消费场景下(如租车、充电桩、酒店预订等)用户在开启服务时做一笔资金授权,当服务完结算时,再从授权资金中扣除消费金额,剩余返还给用户。可以浅浅的理解为交了一笔押金,从押金里消费扣钱,然后退押金。


二、写在代码之前

接入准备:

对接预授权这部分前,如没有使用过支付宝产品的话就得创建你的应用,绑定商家账号等。这部分挺简单的,官方文档有详细步骤而且有其他文章写到这方面,所以这里就不再赘述啦。

接入服务端 SDK:

pom里粘贴依赖然后install一下就行了,目前最新版本就是这个。如果反复下载爆红就检查镜像、重启项目,也可能需要github加速。

        <!--支付宝资金预授权-->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.31.84.ALL</version>
        </dependency>

三、服务端代码

1.授权冻结接口

一步引入SDK成功后就可以直接开发了。

接口部分:

大部分情况是前端直接调用冻结,然后唤醒前台支付宝的支付功能。放上较为完整的接口,删减了其他业务处理,这里只post资金冻结。

/**
     * 资金冻结
     *
     * @author FaithfulKiller
     * @since 2022/09/14
     */
    @RequestMapping("/freezeorder")
    public Object fundAuthOrderAppFreeze(@RequestBody Map<String, Object> 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;
    }

2.冻结转支付

交易结算调用转支付,因为不局限于用户主动完成结算(按产品需求而定),放在后台处理调用比较好。

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;
		}
    }

3.解冻

若出现用户主动或者其他原因需要退款,则进行解冻操作。

以下就只放上方法体吧,这样更清晰一些。(其实是懒 #^_^# )

        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;
		}

4.异步通知回调

这个很重要也比较多,芝麻返回的信息都通过这个接口。按照你的业务需求处理返回数据。

/**
	 * 资金预授权回调接口(支付宝)
	 *
	 * @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<String, String> params = new HashMap<String, String>();
		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();
			}
		}
	}

5.撤销冻结

无法确认冻结操作是否成功时调用撤销接口。

        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);

6.授权操作查询

这个入参比较灵活了,需要订单信息查什么,返回的参数有很多。

        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)一定要看官方文档,入参出参大部分都在官方文档,但是有少部分官网也没有,恰巧你有需要那只能花时间找了。

taytay

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值