支付宝直付通实现账单结算(分账)功能完整Demo

😊 @ 作者: 一恍过去
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: 支付宝直付通实现账单结算(分账)功能完整Demo
⏱️ @ 创作时间: 2022年09月07日

在这里插入图片描述

前言

互联网平台直付通产品是支付宝面向电商、数娱等互联网平台专属打造的,集支付、结算、分账 等功能为一体的资金解决方案。

平台上的二级商户入驻支付宝成为支付宝的商家,买家在该平台的订单支付成功(支持多个商家的订单合并支付)后,支付宝记录对应商家待结算资金,待平台确认可结算时,支付宝将资金直接结算至商家指定的收款账号,同时支持平台按订单灵活抽取佣金。

本文主要实现了支付功能的分账/补差接口

  • 统一收单交易结算接口
  • 交易分账查询接口
  • 交易分账结果通知
  • 统一收单交易退款接口

1、POM

        <!--支付宝SDK -->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.31.48.ALL</version>
        </dependency>

        <!-- 支付宝SDK依赖的日志-->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

2、YAML

alipay:
  # appid
  appId: 
  # 商户PID,卖家支付宝账号ID
  sellerId: 
  # 私钥 pkcs8格式的,rsc中的私钥:https://openhome.alipay.com/platform/appDaily.htm?tab=info
  privateKey: 
  # 支付宝公钥:https://openhome.alipay.com/platform/appDaily.htm?tab=info
  publicKey: 
  # 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
  notifyUrl: http://zhbexg.natappfree.cc/alipay/notify
  # 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
  returnUrl: https://zhbexg.natappfree.cc/alipay/return
  # 请求网关地址
  # 正式为:"https://openapi.alipay.com/gateway.do"
  serverUrl: https://openapi.alipaydev.com/gateway.do

3、支付配置类

package com.lhz.config;

import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayConstants;
import com.alipay.api.DefaultAlipayClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

/**
 * @Description:
 **/
@Component
@Data
@ConfigurationProperties(prefix = "alipay")
public class AlipayConfig {

    /**
     * 商户appid
     */
    private String appId;

    /**
     * 商户PID,卖家支付宝账号ID
     */
    private String sellerId;

    /**
     * 私钥 pkcs8格式的,rsc中的私钥
     */
    private String privateKey;

    /**
     * 支付宝公钥
     */
    private String publicKey;

    /**
     * 请求网关地址
     */
    private String serverUrl;

    /**
     * 页面跳转同步通知(可以直接返回前端页面、或者通过后端进行跳转)
     */
    private String returnUrl;

    /**
     * 服务器异步通知
     */
    private String notifyUrl;

    /**
     * 获得初始化的AlipayClient
     *
     * @return
     */
    @Bean
    public AlipayClient alipayClient() {
        // 获得初始化的AlipayClient
        return new DefaultAlipayClient(serverUrl, appId, privateKey,
                AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
                publicKey, AlipayConstants.SIGN_TYPE_RSA2);
    }
}

4、分账接口实现

4.1、统一收单交易结算接口

用于在卖家交易成功之后,基于交易订单,进行卖家与第三方(如供应商或平台商)的资金再分配。一般用于第三方从卖家抽佣场景。

    1. 订单确认结算后,才可发起分账。
    1. 同一笔订单,同步分账和异步分账不能混用。如果业务上存在一次分账请求超过5个分账收款方的情况,推荐使用异步分账。
    1. 建议支付成功后间隔 30s 再发起该接口请求
    1. 单个卖家请求频率最高 30 TPS。接口报错FREQUENCY_LIMIT 请控制请求频率
    1. 如果接口调用超时或者返回ACQ.SYSTEM_ERROR ACQ.TRADE_SETTLE_ERROR 当前请求可能成功也可能失败。请使用相同的参数再次重试调用,外部请求号和金额等均不能变更。如果前一次分账已经成功,接口会幂等返回成功;如果前一次分账请求没有成功,接口会重试执行分账操作

参考API: https://opendocs.alipay.com/open/028xqz?pathHash=d66637d9

/**
 * @Description: 合单支付相关接口
 **/
@Api(tags = "分账相关接口")
@RestController
@RequestMapping("/sharing")
@Slf4j
public class SharingPayController {

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private AlipayConfig alipayConfig;
    
    /**
     * 统一收单交易结算接口
     * 用于在卖家交易成功之后,基于交易订单,进行卖家与第三方(如供应商或平台商)的资金再分配。接口调用要求:
     * (1)建议支付成功后间隔 30s 再发起该接口请求
     * (2)单个商户请求频率最高 30 qps
     * (3)基于同一笔交易订单,该接口多次调用请求建议间隔 3s。
     *
     * @param tradeNo 支付宝订单号
     * @return
     */
    @ApiOperation(value = "统一收单交易结算接口", notes = "统一收单交易结算接口")
    @ApiOperationSupport(order = 1)
    @GetMapping("/settle")
    @ResponseBody
    public String tradeMerge(@RequestParam("tradeNo") String tradeNo) throws Exception {
        AlipayTradeOrderSettleRequest request = new AlipayTradeOrderSettleRequest();

        JSONObject bizContent = new JSONObject();
        // 结算请求流水号
        long outRequestNo = System.currentTimeMillis();
        bizContent.put("out_request_no", outRequestNo);
        // 支付宝订单号
        bizContent.put("trade_no", tradeNo);

        // 分账明细信息,商家分账场景下分账收入方 trans_in 只支持支付宝账户,不支持使用 cardAliasNo 卡编号。
        JSONArray royaltyParameters = new JSONArray();
        // 模拟两条分账信息
        for (int a = 1; a <= 2; a++) {
            JSONObject royaltyParameter = new JSONObject();
            royaltyParameter.put("royalty_type", "transfer");
            // 收入方账户类型。userId表示是支付宝账号对应的支付宝唯一用户号;cardAliasNo表示是卡编号;loginName表示是支付宝登录号;
            royaltyParameter.put("trans_in_type", "userId");
            // 收入方账户。如果收入方账户类型为userId,本参数为收入方的支付宝账号对应的支付宝唯一用户号,以2088开头的纯16位数字;
            // 如果收入方类型为cardAliasNo,本参数为收入方在支付宝绑定的卡编号;如果收入方类型为loginName,本参数为收入方的支付宝登录号;
            royaltyParameter.put("trans_in", "12312312");
            // 分账收款方姓名,上送则进行姓名与支付宝账号的一致性校验,校验不一致则分账失败。不上送则不进行姓名校验
            royaltyParameter.put("trans_in_name", "name");
            // 分账金额
            royaltyParameter.put("amount", 0.01);

            royaltyParameters.add(royaltyParameter);
        }
        bizContent.put("royalty_parameters", royaltyParameters);

        request.setBizContent(bizContent.toString());
        AlipayTradeOrderSettleResponse response = alipayClient.execute(request);

        // 根据response中的结果继续业务逻辑处理
        if (response.isSuccess()) {
            String resTradeNo = response.getTradeNo();
            // 支付宝分账单号,可以根据该单号查询单次分账请求执行结果
            String settleNo = response.getSettleNo();
            return "操作成功";
        } else {
            log.error("调用支付宝失败");
            log.error(response.getSubCode());
            log.error(response.getSubMsg());
            return response.getSubMsg();
        }
    }
}

4.2、交易分账查询接口

根据分账请求号查询交易分账结果,参考API:https://opendocs.alipay.com/open/02o6e0?pathHash=158daac5

/**
 * @Description: 合单支付相关接口
 **/
@Api(tags = "分账相关接口")
@RestController
@RequestMapping("/sharing")
@Slf4j
public class SharingPayController {

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private AlipayConfig alipayConfig;
    
    /**
     * 交易分账查询接口
     * 根据分账请求号查询交易分账结果
     *
     * @param settleNo 预支付订单号
     * @return
     */
    @ApiOperation(value = "交易分账查询接口", notes = "交易分账查询接口")
    @ApiOperationSupport(order = 1)
    @GetMapping("/query")
    @ResponseBody
    public String query(@RequestParam("settleNo") String settleNo) throws Exception {

        log.info("支付宝分账请求单号:" + settleNo);

        AlipayTradeOrderSettleQueryRequest request = new AlipayTradeOrderSettleQueryRequest();

        JSONObject bizContent = new JSONObject();
        // 支付宝分账请求单号
        bizContent.put("settle_no", settleNo);

        request.setBizContent(bizContent.toString());
        AlipayTradeOrderSettleQueryResponse response = alipayClient.execute(request);

        // 根据response中的结果继续业务逻辑处理
        if (response.isSuccess()) {
            // 商户分账请求单号
            String outRequestNo = response.getOutRequestNo();
            // 分账受理时间
            Date operationDt = response.getOperationDt();
            // 分账明细
            List<RoyaltyDetail> royaltyDetailList = response.getRoyaltyDetailList();
            for (RoyaltyDetail detail : royaltyDetailList) {
                // fe分账金额
                String amount = detail.getAmount();
                // 分账操作类型: replenish(补差)、replenish_refund(退补差)、transfer(分账)、transfer_refund(退分账)
                String operationType = detail.getOperationType();
                // 分账执行时间
                Date executeDt = detail.getExecuteDt();
                // 分账转入账号类型
                detail.getTransInType();
                // 分账转入账号
                detail.getTransIn();
                // 分账状态,SUCCESS成功,FAIL失败,PROCESSING处理中
                String state = detail.getState();
            }

            return "操作成功";
        } else {
            log.error("调用支付宝失败");
            log.error(response.getSubCode());
            log.error(response.getSubMsg());
            return response.getSubMsg();
        }
    }   
}

4.3、交易分账结果通知

接收通知消息前需要在支付宝开放平台-应用配置中配置应用网关地址,分账通知消息将发送到配置的地址,参考API:https://opendocs.alipay.com/open/02owty?pathHash=7db1774f

/**
 * @Description: 合单支付相关接口
 **/
@Api(tags = "分账相关接口")
@RestController
@RequestMapping("/sharing")
@Slf4j
public class SharingPayController {

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private AlipayConfig alipayConfig;
    	
    /**
     * 接收通知消息前需要在支付宝开放平台-应用配置中配置应用网关地址,分账通知消息将发送到配置的地址
     *
     * @param request
     * @param response
     * @throws IOException
     */
    @PostMapping("/notify")
    public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        log.info("结算异步通知");
        PrintWriter out = response.getWriter();
        // 乱码解决,这段代码在出现乱码时使用
        request.setCharacterEncoding("utf-8");

        // 获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<>(8);
        Map<String, String[]> requestParams = request.getParameterMap();
        for (Map.Entry<String, String[]> stringEntry : requestParams.entrySet()) {
            String[] values = stringEntry.getValue();
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            params.put(stringEntry.getKey(), valueStr);
        }

        // 调用SDK验证签名
        boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.getPublicKey(), AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);

        if (!signVerified) {
            log.error("验签失败");
            out.print("fail");
            return;
        }

        // ================= 获取参数 ================= //

        // 商户分账请求号
        String outRequestNo = new String(params.get("out_request_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
        // 支付宝分账受理单号
        String settleNo = new String(params.get("settle_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
        // 分账受理时间
        String operationDt = new String(params.get("operation_dt").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
        // 分账明细
        String royaltyDetailList = new String(params.get("royalty_detail_list").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
        List<Map<String, String>> detailMapList = JSON.parseObject(royaltyDetailList, new TypeReference<List<Map<String, String>>>() {
        });

        for (Map<String, String> detailMap : detailMapList) {
            // 分账操作类型: replenish(补差)、replenish_refund(退补差)、transfer(分账)、transfer_refund(退分账)
            String operationType = new String(detailMap.get("operation_type").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 分账执行时间
            String executeDt = new String(detailMap.get("execute_dt").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 分账转入账号类型
            String transInType = new String(detailMap.get("trans_in_type").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 分账转入账号
            String transIn = new String(detailMap.get("trans_in").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 分账金额
            String amount = new String(detailMap.get("amount").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 分账状态,SUCCESS成功,FAIL失败,PROCESSING处理中
            String state = new String(detailMap.get("state").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            if ("FAIL".equals(state)) {
                // 分账失败错误码,只在分账失败时返回
                String errorCode = new String(detailMap.get("error_code").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
                // 分账错误描述信息
                String errorDesc = new String(detailMap.get("error_desc").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            }
        }

        out.print("success");
    }   
}

4.4、统一收单交易退款接口

当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
交易超过约定时间(签约时设置的可退款时间)的订单无法进行退款。
支付宝退款支持单笔交易分多次退款,多次退款需要提交原支付订单的订单号和设置不同的退款请求号。一笔退款失败后重新提交,要保证重试时退款请求号不能变更,防止该笔交易重复退款。
同一笔交易累计提交的退款金额不能超过原始交易总金额。

    1. 同一笔交易的退款至少间隔3s后发起
    1. 请严格按照接口文档中的参数进行接入。若在此接口中传入【非当前接口文档中的参数】会造成【退款失败或重复退款】。
    1. 该接口不可与其他退款产品混用。若商户侧同一笔退款请求已使用了当前接口退款的情况下,【再使用其他退款产品进行退款】可能会造成【重复退款】。
    1. 退款成功判断说明:接口返回fund_change=Y为退款成功,fund_change=N或无此字段值返回时需通过退款查询接口进一步确认退款状态。注意,接口中code=10000,仅代表本次退款请求成功,不代表退款成功。

参考API:https://opendocs.alipay.com/open/028xqx?pathHash=c1ec91b4

/**
 * @Description: 合单支付相关接口
 **/
@Api(tags = "分账相关接口")
@RestController
@RequestMapping("/sharing")
@Slf4j
public class SharingPayController {

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private AlipayConfig alipayConfig;
    
     /**
     * 同一笔交易的退款至少间隔3s后发起
     *
     * <p>
     * 如果金额传入0,refund_royalty_parameters不为空,则表示"只退分账,不退款";
     * 如果金额不能0,refund_royalty_parameters为空,则表示"只退款,不退分账";
     * <p>
     * 退款成功判断说明:接口返回fund_change=Y为退款成功,fund_change=N或无此字段值返回时需通过退款查询接口进一步确认退款状态。
     *
     * @return
     * @throws Exception
     *  具体API参数说明参考:https://opendocs.alipay.com/open/02ivbx
     */
    @ApiOperation(value = "订单退款", notes = "订单退款")
    @ApiOperationSupport(order = 3)
    @GetMapping("/refund/{tradeNum}")
    @ResponseBody
    public Object refund(@PathVariable("tradeNum") String tradeNum) throws Exception {
        //创建API对应的request类
        AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
        JSONObject bizContent = new JSONObject();
        // 商户网站唯一订单号
        bizContent.put("out_trade_no", tradeNum);
        // 支付宝交易号
        //  bizContent.put("trade_no", "2021081722001419121412730660");

        // 退款金额(只退分账,不退款)
        bizContent.put("refund_amount", 0);
        // 退款原因
        bizContent.put("refund_reason", "申请退款");

        // 退款请求号,标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
        // 针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更。
        long outRequestNo = System.currentTimeMillis();
        bizContent.put("out_request_no", outRequestNo);

        // 退分账明细信息
        JSONArray refundRoyaltyParameters = new JSONArray();
        // 模拟两条分账退款记录,与申请分账时的明细保持一致
        for (int a = 1; a <= 2; a++) {
            JSONObject refundRoyaltyParameter = new JSONObject();
            refundRoyaltyParameter.put("royalty_type", "transfer");
            // 收入方账户类型。userId表示是支付宝账号对应的支付宝唯一用户号;cardAliasNo表示是卡编号;loginName表示是支付宝登录号;
            refundRoyaltyParameter.put("trans_in_type", "userId");
            // 收入方账户。如果收入方账户类型为userId,本参数为收入方的支付宝账号对应的支付宝唯一用户号,以2088开头的纯16位数字;
            // 如果收入方类型为cardAliasNo,本参数为收入方在支付宝绑定的卡编号;如果收入方类型为loginName,本参数为收入方的支付宝登录号;
            refundRoyaltyParameter.put("trans_in", "12312312");
            // 分账收款方姓名,上送则进行姓名与支付宝账号的一致性校验,校验不一致则分账失败。不上送则不进行姓名校验
            refundRoyaltyParameter.put("trans_in_name", "name");
            // 分账金额
            refundRoyaltyParameter.put("amount", 0.01);

            refundRoyaltyParameters.add(refundRoyaltyParameter);
        }

        bizContent.put("refund_royalty_parameters", refundRoyaltyParameters);

        request.setBizContent(bizContent.toString());
        AlipayTradeRefundResponse response = alipayClient.execute(request);


        // 根据response中的结果继续业务逻辑处理
        if (response.isSuccess()) {
            log.info("调用支付宝成功");
            log.info(response.getSubMsg());

            /**
             * 本次退款是否发生了资金变化
             * Y 表示退款成功
             */
            log.info("是否发生了资金变化:" + response.getFundChange());
            log.info("支付宝交易号:" + response.getTradeNo());
            log.info("商家订单号:" + response.getOutTradeNo());
            log.info("已退款的总金额:" + response.getRefundFee());
            log.info("买家支付宝账号:" + response.getBuyerLogonId());
            log.info("买家在支付宝的用户id:" + response.getBuyerUserId());
            log.info("买家在支付宝的用户id:" + response.getBuyerUserId());
        } else {
            log.error("调用支付宝失败");
            log.error(response.getSubCode());
            log.error(response.getSubMsg());
        }
        return "调用订单退款";
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一恍过去

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值