目录
- 前言
- 1、POM
- 2、YAML
- 3、支付配置类
- 4、分账接口实现
- 4.1、统一收单交易结算接口
- 4.2、交易分账查询接口
- 4.3、交易分账结果通知
- 4.4、统一收单交易退款接口
前言
互联网平台直付通产品是支付宝面向电商、数娱等互联网平台专属打造的,集支付、结算、分账 等功能为一体的资金解决方案。
平台上的二级商户入驻支付宝成为支付宝的商家,买家在该平台的订单支付成功(支持多个商家的订单合并支付)后,支付宝记录对应商家待结算资金,待平台确认可结算时,支付宝将资金直接结算至商家指定的收款账号,同时支持平台按订单灵活抽取佣金。
本文主要实现了支付功能的分账/补差
接口
- 统一收单交易结算接口
- 交易分账查询接口
- 交易分账结果通知
- 统一收单交易退款接口
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:# appidappId: # 商户PID,卖家支付宝账号IDsellerId: # 私钥 pkcs8格式的,rsc中的私钥:https://openhome.alipay.com/platform/appDaily.htm?tab=infoprivateKey: # 支付宝公钥:https://openhome.alipay.com/platform/appDaily.htm?tab=infopublicKey: # 服务器异步通知页面路径 需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*/@Beanpublic AlipayClient alipayClient() {// 获得初始化的AlipayClientreturn new DefaultAlipayClient(serverUrl, appId, privateKey,AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,publicKey, AlipayConstants.SIGN_TYPE_RSA2);}
}
4、分账接口实现
4.1、统一收单交易结算接口
用于在卖家交易成功之后,基于交易订单,进行卖家与第三方(如供应商或平台商)的资金再分配。一般用于第三方从卖家抽佣场景。
- 订单确认结算后,才可发起分账。
- 同一笔订单,同步分账和异步分账不能混用。如果业务上存在一次分账请求超过5个分账收款方的情况,推荐使用异步分账。
- 建议支付成功后间隔 30s 再发起该接口请求
- 单个卖家请求频率最高 30 TPS。接口报错FREQUENCY_LIMIT 请控制请求频率
- 如果接口调用超时或者返回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 {@Resourceprivate AlipayClient alipayClient;@Resourceprivate AlipayConfig alipayConfig;/*** 统一收单交易结算接口* 用于在卖家交易成功之后,基于交易订单,进行卖家与第三方(如供应商或平台商)的资金再分配。接口调用要求:* (1)建议支付成功后间隔 30s 再发起该接口请求* (2)单个商户请求频率最高 30 qps* (3)基于同一笔交易订单,该接口多次调用请求建议间隔 3s。** @param tradeNo 支付宝订单号* @return*/@ApiOperation(value = "统一收单交易结算接口", notes = "统一收单交易结算接口")@ApiOperationSupport(order = 1)@GetMapping("/settle")@ResponseBodypublic 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 {@Resourceprivate AlipayClient alipayClient;@Resourceprivate AlipayConfig alipayConfig;/*** 交易分账查询接口* 根据分账请求号查询交易分账结果** @param settleNo 预支付订单号* @return*/@ApiOperation(value = "交易分账查询接口", notes = "交易分账查询接口")@ApiOperationSupport(order = 1)@GetMapping("/query")@ResponseBodypublic 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 {@Resourceprivate AlipayClient alipayClient;@Resourceprivate 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、统一收单交易退款接口
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
交易超过约定时间(签约时设置的可退款时间)的订单无法进行退款。
支付宝退款支持单笔交易分多次退款,多次退款需要提交原支付订单的订单号和设置不同的退款请求号。一笔退款失败后重新提交,要保证重试时退款请求号不能变更,防止该笔交易重复退款。
同一笔交易累计提交的退款金额不能超过原始交易总金额。
- 同一笔交易的退款至少间隔3s后发起
- 请严格按照接口文档中的参数进行接入。若在此接口中传入【非当前接口文档中的参数】会造成【退款失败或重复退款】。
- 该接口不可与其他退款产品混用。若商户侧同一笔退款请求已使用了当前接口退款的情况下,【再使用其他退款产品进行退款】可能会造成【重复退款】。
- 退款成功判断说明:接口返回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 {@Resourceprivate AlipayClient alipayClient;@Resourceprivate 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}")@ResponseBodypublic 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 "调用订单退款";}
}