前言
互联网平台直付通产品是支付宝面向电商、数娱等互联网平台专属打造的,集支付、结算、分账 等功能为一体的资金解决方案。
平台上的二级商户入驻支付宝成为支付宝的商家,买家在该平台的订单支付成功(支持多个商家的订单合并支付)后,支付宝记录对应商家待结算资金,待平台确认可结算时,支付宝将资金直接结算至商家指定的收款账号,同时支持平台按订单灵活抽取佣金。
本文主要实现了支付功能的分账/补差
接口
- 统一收单交易结算接口
- 交易分账查询接口
- 交易分账结果通知
- 统一收单交易退款接口
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、统一收单交易结算接口
用于在卖家交易成功之后,基于交易订单,进行卖家与第三方(如供应商或平台商)的资金再分配。一般用于第三方从卖家抽佣场景。
- 订单确认结算后,才可发起分账。
- 同一笔订单,同步分账和异步分账不能混用。如果业务上存在一次分账请求超过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 {
@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、统一收单交易退款接口
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
交易超过约定时间(签约时设置的可退款时间)的订单无法进行退款。
支付宝退款支持单笔交易分多次退款,多次退款需要提交原支付订单的订单号和设置不同的退款请求号。一笔退款失败后重新提交,要保证重试时退款请求号不能变更,防止该笔交易重复退款。
同一笔交易累计提交的退款金额不能超过原始交易总金额。
- 同一笔交易的退款至少间隔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 {
@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 "调用订单退款";
}
}