目录
要开启分账,需要在支付时设置
settle_info
中的profit_sharing
属性为true
,在下面第7点(分账相关接口)
中的SharingController
模拟进行一次Native支付
;
主要实现以下接口:
- 请求分账
- 查询分账结果
- 请求分账回退
- 查询分账回退结果
- 解冻剩余资金(完结分账)
- 查询剩余待分金额
- 申请分账账单及下载
1、引入POM
<!--微信支付SDK-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
2、配置Yaml
wxpay:
#应用编号
appId: xxxx
#商户号
mchId: xxx
# APIv2密钥
apiKey: xxxx
# APIv3密钥
apiV3Key: xxx
# 微信支付V3-url前缀
baseUrl: https://api.mch.weixin.qq.com/v3
# 支付通知回调, pjm6m9.natappfree.cc 为内网穿透地址
notifyUrl: http://pjm6m9.natappfree.cc/pay/payNotify
# 退款通知回调, pjm6m9.natappfree.cc 为内网穿透地址
refundNotifyUrl: http://pjm6m9.natappfree.cc/pay/refundNotify
# 密钥路径,resources根目录下
keyPemPath: apiclient_key.pem
#商户证书序列号
serialNo: xxxxx
3、配置密钥文件
在商户/服务商平台的”账户中心" => “API安全” 进行API证书、密钥的设置,API证书主要用于获取“商户证书序列号”以及“p12”、“key.pem”、”cert.pem“证书文件,j将获取的apiclient_key.pem
文件放在项目的resources
目录下。
4、配置PayConfig
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
/**
* @Description:
**/
@Component
@Data
@Slf4j
@ConfigurationProperties(prefix = "wxpay")
public class WechatPayConfig {
/**
* 应用编号
*/
private String appId;
/**
* 商户号
*/
private String mchId;
/**
* 服务商商户号
*/
private String slMchId;
/**
* APIv2密钥
*/
private String apiKey;
/**
* APIv3密钥
*/
private String apiV3Key;
/**
* 支付通知回调地址
*/
private String notifyUrl;
/**
* 退款回调地址
*/
private String refundNotifyUrl;
/**
* API 证书中的 key.pem
*/
private String keyPemPath;
/**
* 商户序列号
*/
private String serialNo;
/**
* 微信支付V3-url前缀
*/
private String baseUrl;
/**
* 获取商户的私钥文件
* @param keyPemPath
* @return
*/
public PrivateKey getPrivateKey(String keyPemPath){
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
if(inputStream==null){
throw new RuntimeException("私钥文件不存在");
}
return PemUtil.loadPrivateKey(inputStream);
}
/**
* 获取证书管理器实例
* @return
*/
@Bean
public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {
log.info("获取证书管理器实例");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(keyPemPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
CertificatesManager certificatesManager = CertificatesManager.getInstance();
certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));
return certificatesManager.getVerifier(mchId);
}
/**
* 获取支付http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(Verifier verifier) {
//获取商户私钥
PrivateKey privateKey = getPrivateKey(keyPemPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, serialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
return builder.build();
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(keyPemPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, serialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
return builder.build();
}
}
5、定义统一枚举
SharingUrlEnum:
@AllArgsConstructor
@Getter
public enum SharingUrlEnum {
/**
* 请求分账(分批次分账)
*/
ORDERS_SHARING("/profitsharing/orders"),
/**
* 请求分账回退
*/
RETURN_SHARING("/profitsharing/return-orders"),
/**
* 查询订单剩余待分金额
*/
SHARING_AMOUNT_BALANCE("/profitsharing/transactions/%s/amounts"),
/**
* 完结分账
*/
FINISH_SHARING("/profitsharing/orders/unfreeze"),
/**
* 申请分账账单
*/
BILLS("/profitsharing/bills");
/**
* 类型
*/
private final String type;
}
6、封装统一请求处理
WechatPayRequest:
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
/**
* @Description:
**/
@Component
@Slf4j
public class WechatPayRequest {
@Resource
private CloseableHttpClient wxPayClient;
public String wechatHttpGet(String url) {
try {
// 拼接请求参数
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpGet);
return getResponseBody(response);
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}
public String wechatHttpPost(String url,String paramsStr) {
try {
HttpPost httpPost = new HttpPost(url);
StringEntity entity = new StringEntity(paramsStr, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
CloseableHttpResponse response = wxPayClient.execute(httpPost);
return getResponseBody(response);
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}
private String getResponseBody(CloseableHttpResponse response) throws IOException {
//响应体
HttpEntity entity = response.getEntity();
String body = entity==null?"":EntityUtils.toString(entity);
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
//处理成功,204是,关闭订单时微信返回的正常状态码
if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
log.info("成功, 返回结果 = " + body);
} else {
String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body;
log.error(msg);
throw new RuntimeException(msg);
}
return body;
}
}
7、分账相关接口
要开启分账,需要在支付时设置
settle_info
中的profit_sharing
属性为true
,在SharingController
中模拟进行一次Native支付
;
7.1、模拟支付
为了更好的演示分账流程,通过NATIVE方式进行真实的下单支付,并且支付时通过'profit_sharing'参数,开启分账
SharingController:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.lhz.demo.model.enums.SharingUrlEnum;
import com.lhz.demo.pay.WechatPayConfig;
import com.lhz.demo.pay.WechatPayRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
/**
* @Author: LiHuaZhi
* @Date: 2022/8/21 21:23
* @Description:
**/
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* TODO 模拟任意类型下单,比如:NATIVE下单
*
* @return
*/
@ApiOperation(value = "NATIVE下单", notes = "NATIVE下单")
@ApiOperationSupport(order = 10)
@GetMapping("/native")
public String nativePay() {
// 统一参数封装
Map<String, Object> params = new HashMap<>(8);
params.put("appid", wechatPayConfig.getAppId());
params.put("mchid", wechatPayConfig.getMchId());
params.put("description", "测试商品");
int outTradeNo = new Random().nextInt(999999999);
params.put("out_trade_no", outTradeNo + "");
params.put("notify_url", wechatPayConfig.getNotifyUrl());
Map<String, Object> amountMap = new HashMap<>(4);
// 金额单位为分
amountMap.put("total", 1);
amountMap.put("currency", "CNY");
params.put("amount", amountMap);
// 结算信息
Map<String, Object> settleInfoMap = new HashMap<>(4);
settleInfoMap.put("profit_sharing", true);
params.put("settle_info", settleInfoMap);
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat("/pay/transactions/native"), paramsStr);
Map<String, String> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
return resMap.get("code_url");
}
}
7.2、单次分账请求
微信订单支付成功后,商户发起分账请求,将结算后的资金分到分账接收方
- 对同一笔订单最多能发起50次分账请求,每次请求最多分给50个接收方
- 此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取
- 商户需确保向微信支付传输用户身份信息和账号标识信息做一致性校验已合法征得用户授权
参考API:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_1.shtml
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* 微信订单支付成功后,由电商平台发起分账请求,将结算后的资金分给分账接收方
*
* @param transactionId 微信支付订单号
* @return
*/
@ApiOperation(value = "单次分账请求", notes = "单次分账请求")
@ApiOperationSupport(order = 10)
@PostMapping("/ordersSharing")
public String ordersSharing(@RequestBody String transactionId) {
// TODO 为了更严谨,在发起分账前可以先“查询订单剩余待分金额”,如果没有待分账的金额则不发起分账
// 统一参数封装
Map<String, Object> params = new HashMap<>(8);
params.put("appid", wechatPayConfig.getAppId());
// 微信支付订单号,成功支付后获取,实际项目中:支付回调时需要更新到order表中
params.put("transaction_id", transactionId);
// 商户系统内部的分账单号,在商户系统内部唯一
int outOrderNo = new Random().nextInt(999999999);
params.put("out_order_no", outOrderNo + "");
/**
* 是否完成分账
* 1、如果为true,该笔订单剩余未分账的金额会解冻回电商平台二级商户;
* 2、如果为false,该笔订单剩余未分账的金额不会解冻回电商平台二级商户,可以对该笔订单再次进行分账。
*/
params.put("unfreeze_unsplit", false);
List<Map<String, Object>> receivers = new ArrayList<>();
// TODO 设置分账接收方列表,可以分给多个商户或者个人,此处模拟一个
Map<String, Object> receiversMap = new HashMap<>(4);
/**
* 分账接收方类型,枚举值:
* MERCHANT_ID:商户
* PERSONAL_OPENID:个人
*/
receiversMap.put("type", "MERCHANT_ID");
/**
* 分账接收方账号:
* 类型是MERCHANT_ID时,是商户号(mch_id或者sub_mch_id)
* 类型是PERSONAL_OPENID时,是个人openid
*/
receiversMap.put("account", wechatPayConfig.getMchId());
// 分账金额,单位为分,只能为整数
receiversMap.put("amount", 1);
receiversMap.put("description", "测试分账");
receivers.add(receiversMap);
params.put("receivers", receivers);
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat(SharingUrlEnum.ORDERS_SHARING.getType()), paramsStr);
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
// 执行结果
log.info("响应参数:{}", JSON.toJSONString(resMap));
/**
* 枚举值:
* 1、PROCESSING:处理中
* 2、FINISHED:分账完成
*/
String state = resMap.get("state").toString();
// 分账接收方列表
String receiversStr = resMap.get("receivers").toString();
List<Map<String, Object>> receiversList = JSONObject.parseObject(receiversStr, new TypeReference<List<Map<String, Object>>>() {
});
for (Map<String, Object> receiver : receiversList) {
// 微信分账明细单号,每笔分账业务执行的明细单号,可与资金账单对账使用
String detailId = receiver.get("detail_id").toString();
/**
* 枚举值:
* MERCHANT_ID:商户号(mch_id或者sub_mch_id)
* PERSONAL_OPENID:个人openid(由服务商的APPID转换得到)
* PERSONAL_SUB_OPENID:个人sub_openid(由品牌主的APPID转换得到)
*/
String type = receiver.get("type").toString();
/**
* 枚举值:
* PENDING:待分账
* SUCCESS:分账成功
* CLOSED:已关闭
*/
String result = receiver.get("result").toString();
// 分账接收方账号
String account = receiver.get("account").toString();
// 分账完成时间
String finishTime = receiver.get("finish_time").toString();
/**
* 分账失败原因,当分账结果result为CLOSED(已关闭)时,返回该字段
* 枚举值:
* 1、ACCOUNT_ABNORMAL:分账接收账户异常
* 2、NO_RELATION:分账关系已解除
* 3、RECEIVER_HIGH_RISK:高风险接收方
* 4、RECEIVER_REAL_NAME_NOT_VERIFIED:接收方未实名
* 5、NO_AUTH:分账权限已解除
* 6、RECEIVER_RECEIPT_LIMIT:接收方已达收款限额
* 7、PAYER_ACCOUNT_ABNORMAL:分出方账户异常
*/
Object failReason = receiver.get("fail_reason");
log.info("type:" + type);
log.info("result:" + result);
log.info("account:" + account);
log.info("finishTime:" + finishTime);
log.info("failReason:" + failReason);
}
return "SUCCESS";
}
}
7.3、查询单次(完结)分账请求结果
发起分账请求后,可调用此接口查询分账结果
- 发起解冻剩余资金请求后,可调用此接口查询解冻剩余资金的结果
参考API:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_2.shtml
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* 发起分账请求后,可调用此接口查询分账结果 ;发起分账完结请求后,可调用此接口查询分账完结的结果
*
* @param transactionId 微信支付订单号
* @return
*/
@ApiOperation(value = "查询单次(完结)分账请求结果", notes = "查询单次(完结)分账请求结果")
@ApiOperationSupport(order = 10)
@GetMapping("/ordersSharing")
public String queryOrdersSharing(@RequestParam("transactionId") String transactionId, @RequestParam("outOrderNo") String outOrderNo) {
String url = wechatPayConfig.getBaseUrl().concat(SharingUrlEnum.ORDERS_SHARING.getType())
.concat("/" + outOrderNo)
.concat("?transaction_id=" + transactionId);
String resStr = wechatPayRequest.wechatHttpGet(url);
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
// 执行结果
log.info("响应参数:{}", JSON.toJSONString(resMap));
/**
* 枚举值:
* 1、PROCESSING:处理中
* 2、FINISHED:分账完成
*/
String state = resMap.get("state").toString();
// 分账接收方列表
String receiversStr = resMap.get("receivers").toString();
List<Map<String, Object>> receiversList = JSONObject.parseObject(receiversStr, new TypeReference<List<Map<String, Object>>>() {
});
for (Map<String, Object> receiver : receiversList) {
// 微信分账明细单号,每笔分账业务执行的明细单号,可与资金账单对账使用
String detailId = receiver.get("detail_id").toString();
/**
* 枚举值:
* MERCHANT_ID:商户号(mch_id或者sub_mch_id)
* PERSONAL_OPENID:个人openid(由服务商的APPID转换得到)
* PERSONAL_SUB_OPENID:个人sub_openid(由品牌主的APPID转换得到)
*/
String type = receiver.get("type").toString();
/**
* 枚举值:
* PENDING:待分账
* SUCCESS:分账成功
* CLOSED:已关闭
*/
String result = receiver.get("result").toString();
// 分账接收方账号
String account = receiver.get("account").toString();
// 分账完成时间
String finishTime = receiver.get("finish_time").toString();
/**
* 分账失败原因,当分账结果result为CLOSED(已关闭)时,返回该字段
* 枚举值:
* 1、ACCOUNT_ABNORMAL:分账接收账户异常
* 2、NO_RELATION:分账关系已解除
* 3、RECEIVER_HIGH_RISK:高风险接收方
* 4、RECEIVER_REAL_NAME_NOT_VERIFIED:接收方未实名
* 5、NO_AUTH:分账权限已解除
* 6、RECEIVER_RECEIPT_LIMIT:接收方已达收款限额
* 7、PAYER_ACCOUNT_ABNORMAL:分出方账户异常
*/
Object failReason = receiver.get("fail_reason");
log.info("type:" + type);
log.info("result:" + result);
log.info("account:" + account);
log.info("finishTime:" + finishTime);
log.info("failReason:" + failReason);
}
return "SUCCESS";
}
}
7.4、请求分账回退
如果订单已经分账,在退款时,可以先调此接口,将已分账的资金从分账接收方的账户回退给分账方,再发起退款。
- 分账回退以原分账单为依据,支持多次回退,申请回退总金额不能超过原分账单分给该接收方的金额。
- 此接口采用同步处理模式,即在接收到商户请求后,会实时返回处理结果。
- 对同一个分账接收方最多能发起20次分账回退请求。
- 退款和分账回退没有耦合,分账回退可以先于退款请求,也可以后于退款请求。
- 此功能需要接收方访问商户平台-交易中心-分账-分账接收设置,开启同意分账回退后,才能使用。
- 不支持针对“分账到零钱”的分账单发起分账回退。
- 分账回退的时限是180天。
参考API:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_3.shtml
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* 订单已经分账,在退款时,可以先调此接口,将已分账的资金从分账接收方的账户回退给分账方,再发起退款。
*
* @param orderId 原发起分账请求时使用的商户系统内部的分账单号
* @return
*/
@ApiOperation(value = "请求分账回退", notes = "请求分账回退")
@ApiOperationSupport(order = 10)
@PostMapping("/returnSharing")
public String returnSharing(@RequestBody String orderId) {
//TODO 注意:实际项目中商户回退单号、回退商户号都需要通过orderId在系统的数据库获取,所以分账成功需要记录并且与支付的订单进行关联
// 统一参数封装
Map<String, Object> params = new HashMap<>(8);
// 微信分账单号,微信支付系统返回的唯一标识。
params.put("order_id", orderId);
// 商户系统内部的退单号,在商户系统内部唯一
int outReturnNo = new Random().nextInt(999999999);
params.put("out_return_no", outReturnNo + "");
// 回退商户号只能填写原分账请求中接收分账的商户号
params.put("return_mchid", "86693852");
// 需要从分账接收方回退的金额,单位为分,只能为整数,不能超过原始分账单分出给该接收方的金额。
params.put("amount", 1);
params.put("description", "测试回退");
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat(SharingUrlEnum.RETURN_SHARING.getType()), paramsStr);
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
// 执行结果
log.info("响应参数:{}", JSON.toJSONString(resMap));
// 微信分账回退单号,微信支付系统返回的唯一标识
String returnId = resMap.get("return_id").toString();
/**
* 枚举值:
* PROCESSING:处理中
* 如果返回为处理中,请勿变更商户回退单号,使用相同的参数再次发起分账回退,否则会出现资金风险。
* 在处理中状态的回退单如果5天没有成功,会因为超时被设置为已失败。
* SUCCESS:已成功
* FAILED:已失败
*/
String result = resMap.get("result").toString();
/**
* 此字段仅回退结果为FAIL时存在,枚举值:
* ACCOUNT_ABNORMAL:原分账接收方账户异常
* TIME_OUT_CLOSED::超时关单
* PAYER_ACCOUNT_ABNORMAL:原分账分出方账户异常
*/
String failReason = resMap.get("fail_reason").toString();
String finishTime = resMap.get("finish_time").toString();
log.info("result:" + result);
log.info("failReason:" + failReason);
log.info("finishTime:" + finishTime);
return "SUCCESS";
}
}
7.5、查询分账回退结果
商户需要核实回退结果,可调用此接口查询回退结果
- 如果分账回退接口返回状态为
处理中
,可调用此接口查询回退结果
参考API:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_4.shtml
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* 订单已经分账,在退款时,可以先调此接口,将已分账的资金从分账接收方的账户回退给分账方,再发起退款。
*
* @return
*/
@ApiOperation(value = "查询分账回退结果", notes = "查询分账回退结果")
@ApiOperationSupport(order = 10)
@GetMapping("/returnSharing")
public String queryReturnSharing(@RequestParam String outOrderNo, @RequestParam("outReturnNo") String outReturnNo) {
//TODO 注意:实际项目中商户回退单号、回退商户号都需要通过orderId在系统的数据库获取,所以分账成功需要记录并且与支付的订单进行关联
// 统一参数封装
Map<String, Object> params = new HashMap<>(8);
// 原发起分账请求时使用的商户系统内部的分账单号
params.put("out_order_no", outOrderNo);
// 调用回退接口提供的商户系统内部的回退单号
params.put("out_return_no", outReturnNo);
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
String url = wechatPayConfig.getBaseUrl().concat(SharingUrlEnum.RETURN_SHARING.getType())
.concat("/" + outReturnNo)
.concat("?out_order_no=" + outOrderNo);
String resStr = wechatPayRequest.wechatHttpGet(url);
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
// 执行结果
log.info("响应参数:{}", JSON.toJSONString(resMap));
// 微信分账回退单号,微信支付系统返回的唯一标识
String returnId = resMap.get("return_id").toString();
/**
* 枚举值:
* PROCESSING:处理中
* 如果返回为处理中,请勿变更商户回退单号,使用相同的参数再次发起分账回退,否则会出现资金风险。
* 在处理中状态的回退单如果5天没有成功,会因为超时被设置为已失败。
* SUCCESS:已成功
* FAILED:已失败
*/
String result = resMap.get("result").toString();
/**
* 此字段仅回退结果为FAIL时存在,枚举值:
* ACCOUNT_ABNORMAL:原分账接收方账户异常
* TIME_OUT_CLOSED::超时关单
* PAYER_ACCOUNT_ABNORMAL:原分账分出方账户异常
*/
String failReason = resMap.get("fail_reason").toString();
String finishTime = resMap.get("finish_time").toString();
log.info("result:" + result);
log.info("failReason:" + failReason);
log.info("finishTime:" + finishTime);
return "SUCCESS";
}
}
7.6、完结分账
不需要进行分账的订单或者解冻已经发起分账的订单,可直接调用本接口将订单的金额全部解冻给本商户
- 调用分账接口后,需要解冻剩余资金时,调用本接口将剩余的分账金额全部解冻给本商户
- 此接口采用异步处理模式,即在接收到商户请求后,优先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取
参考API:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_5.shtml
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* 不需要进行分账的订单,可直接调用本接口将订单的金额全部解冻给二级商户。
*
* @param transactionId 微信支付订单号。
* @return
*/
@ApiOperation(value = "完结分账", notes = "完结分账")
@ApiOperationSupport(order = 10)
@PostMapping("/finishSharing")
public String finishSharing(@RequestBody String transactionId) {
// 统一参数封装
Map<String, Object> params = new HashMap<>(8);
// 微信支付订单号
params.put("transaction_id", transactionId);
// 商户系统内部的分账单号,在商户系统内部唯一(单次分账、多次分账、完结分账应使用不同的商户分账单号),同一分账单号多次请求等同一次
int outOrderNo = new Random().nextInt(999999999);
params.put("out_order_no", outOrderNo + "");
params.put("description", "测试完结分账");
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat(SharingUrlEnum.FINISH_SHARING.getType()), paramsStr);
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
// 执行结果
log.info("响应参数:{}", JSON.toJSONString(resMap));
return "SUCCESS";
}
}
7.7、查询订单剩余待分金额
可调用此接口查询订单剩余待分金额
参考API:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_6.shtml
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* 查询订单剩余待分金额,查询订单剩余待分金额
*
* @param transactionId
* @return
*/
@ApiOperation(value = "查询订单剩余待分金额", notes = "查询订单剩余待分金额")
@ApiOperationSupport(order = 10)
@GetMapping("/sharingAmountBalance")
public String sharingAmountBalance(@RequestParam String transactionId) {
// 请求url
String url = String.format(wechatPayConfig.getBaseUrl().concat(SharingUrlEnum.SHARING_AMOUNT_BALANCE.getType()), transactionId);
String resStr = wechatPayRequest.wechatHttpGet(url);
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
// 执行结果
log.info("响应参数:{}", JSON.toJSONString(resMap));
String unsplitAmount = resMap.get("unsplit_amount").toString();
log.info("unsplitAmount:" + unsplitAmount);
return "SUCCESS";
}
}
7.8、申请分账账单及下载
微信支付按天提供分账账单文件,商户可以通过该接口获取账单文件的下载地址。
- 微信侧未成功的分账单不会出现在对账单中。
- 对账单中涉及金额的字段单位为“元”。
- 对账单接口只能下载三个月以内的账单。
- 账单文件的下载地址的有效时间为30s。
- 微信在次日9点启动生成前一天的对账单,建议在10点后再获取账单进行下载。
参考API:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_11.shtml
@Api(tags = "分账接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class SharingController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
/**
* @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
* @return
*/
@ApiOperation(value = "申请分账账单", notes = "申请分账账单")
@ApiOperationSupport(order = 10)
@GetMapping("/bill")
public String bill(@RequestParam("billDate") String billDate) {
String url = wechatPayConfig.getBaseUrl().concat(SharingUrlEnum.BILLS.getType())
.concat("?bill_date=").concat(billDate);
String res = wechatPayRequest.wechatHttpGet(url);
log.info("查询退款订单结果:{}", res);
Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
});
return resMap.get("download_url").toString();
}
@ApiOperation(value = "下载分账账单", notes = "下载分账账单")
@ApiOperationSupport(order = 45)
@GetMapping("/downloadBill")
public void downloadBill(String downloadUrl) {
log.info("下载账单,下载地址:{}", downloadUrl);
HttpGet httpGet = new HttpGet(downloadUrl);
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response = null;
try {
//使用wxPayClient发送请求得到响应
response = wxPayNoSignClient.execute(httpGet);
String body = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 || statusCode == 204) {
log.info("下载账单,返回结果 = " + body);
} else {
throw new RuntimeException("下载账单异常, 响应码 = " + statusCode + ", 下载账单返回结果 = " + body);
}
// TODO 将body内容转为excel存入本地或者输出到浏览器,演示存入本地
writeStringToFile(body);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void writeStringToFile(String body) {
FileWriter fw = null;
try {
String filePath = "C:\\Users\\lhz12\\Desktop\\wxPay.txt";
fw = new FileWriter(filePath, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(body);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}