1、前置准备
使用微信合单支付需要先开通
电商收付通
功能。文档API:
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml
开通步骤: 进入「微信支付服务商平台->产品中心->合作工具箱」,开通「电商收付通」
2、引入POM
<!--微信支付SDK-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
3、配置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 为内网穿透地址
combineNotifyUrl: http://pjm6m9.natappfree.cc/pay/combinePayNotify
# 退款通知回调, pjm6m9.natappfree.cc 为内网穿透地址
refundNotifyUrl: http://pjm6m9.natappfree.cc/pay/refundNotify
# 密钥路径,resources根目录下
keyPemPath: apiclient_key.pem
#商户证书序列号
serialNo: xxxxx
4、配置密钥文件
在商户/服务商平台的”账户中心" => “API安全” 进行API证书、密钥的设置,API证书主要用于获取“商户证书序列号”以及“p12”、“key.pem”、”cert.pem“证书文件,j将获取的apiclient_key.pem
文件放在项目的resources
目录下。
5、配置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;
/**
* @Author: LiHuaZhi
* @Date: 2022/7/3 22:08
* @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 combineNotifyUrl;
/**
* 退款回调地址
*/
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();
}
}
6、定义统一枚举
CombinePayUrlEnum :
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author
*/
@AllArgsConstructor
@Getter
public enum CombinePayUrlEnum {
/**
* native
*/
NATIVE("native"),
/**
* app
*/
APP("app"),
/**
* h5
*/
H5("h5"),
/**
* jsapi
*/
JSAPI("jsapi"),
/**
* 小程序jsapi
*/
SUB_JSAPI("sub_jsapi"),
/**
* 查询订单
*/
ORDER_QUERY_BY_NO("/combine-transactions/out-trade-no/"),
/**
* 关闭订单
*/
CLOSE_ORDER_BY_NO("/combine-transactions/out-trade-no/%s/close"),
/**
* 申请退款
*/
DOMESTIC_REFUNDS("/refund/domestic/refunds"),
/**
* 查询单笔退款
*/
DOMESTIC_REFUNDS_QUERY("/refund/domestic/refunds/"),
/**
* 申请交易账单
*/
TRADE_BILLS("/bill/tradebill"),
/**
* 申请资金账单
*/
FUND_FLOW_BILLS("/bill/fundflowbill"),
/**
* 申请单个子商户资金账单
*/
SUB_MERCHANT_FUND_FLOW_BILLS("/bill/sub-merchant-fundflowbill"),
/**
* 合单支付APIV3
*/
COMBINE_TRANSACTIONS("/combine-transactions/");
/**
* 类型
*/
private final String type;
}
7、封装统一请求处理
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;
}
}
8、回调校验器
WechatPayValidator:
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
* @Description:
**/
@Slf4j
public class WechatPayValidator {
/**
* 应答超时时间,单位为分钟
*/
private static final long RESPONSE_EXPIRED_MINUTES = 5;
private final Verifier verifier;
private final String requestId;
private final String body;
public WechatPayValidator(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) {
try {
//处理请求参数
validateParameters(request);
//构造验签名串
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//验签
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
private void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判断请求是否过期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期请求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
private String buildMessage(HttpServletRequest request) {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
private String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
/**
* 对称解密,异步通知的加密数据
* @param resource 加密数据
* @param apiV3Key apiV3密钥
* @param type 1-支付,2-退款
* @return
*/
public static Map<String, Object> decryptFromResource(String resource,String apiV3Key,Integer type) {
String msg = type==1?"支付成功":"退款成功";
log.info(msg+",回调通知,密文解密");
try {
//通知数据
Map<String, String> resourceMap = JSONObject.parseObject(resource, new TypeReference<Map<String, Object>>() {
});
//数据密文
String ciphertext = resourceMap.get("ciphertext");
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
String associatedData = resourceMap.get("associated_data");
log.info("密文: {}", ciphertext);
AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info(msg+",回调通知,解密结果 : {}", resourceStr);
return JSONObject.parseObject(resourceStr, new TypeReference<Map<String, Object>>(){});
}catch (Exception e){
throw new RuntimeException("回调参数,解密失败!");
}
}
}
9、回调Body内容处理工具
HttpUtils:
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
/**
* @Description:
**/
public class HttpUtils {
/**
* 将通知参数转化为字符串
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
10、封装统一代码
10.1、统一合单下单处理接口
CombinePayController:
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.CombinePayUrlEnum;
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 javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @Description:
*
**/
@Api(tags = "合单支付接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class PayController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
// 公众号APPID
private static final String WX_JSAPI_SUB_APPID = "xxxxxxxx";
// 小程序APPID
private static final String WX_JSAPI_MINI_SUB_APPID = "xxxxxxxx";
// 模拟特约商户子商户号,实际情况:应该写在表中,与商家表进行映射,根据订单动态获取
private static final String subMchId ="xxx";
/**
* type:h5、jsapi、app、native、sub_jsapi
* H5支付时,需要在商户平台--"产品中心"--"开发配置"自行配置支付提交的域名
*
* @param type
* @return
*/
@ApiOperation(value = "统一下单-统一接口", notes = "统一下单-统一接口")
@ApiOperationSupport(order = 10)
@GetMapping("/transactions")
public Map<String, Object> transactions(String type) {
int combineTradeNo = new Random().nextInt(999999999);
log.info("统一下单API,支付方式:{},合单订单号:{}", type,combineTradeNo);
// 统一参数封装
Map<String, Object> params = new HashMap<>(8);
params.put("combine_appid", wechatPayConfig.getAppId());
params.put("combine_mchid", wechatPayConfig.getMchId());
// 合单商户订单号
params.put("combine_out_trade_no", combineTradeNo+"");
params.put("notify_url", wechatPayConfig.getCombineNotifyUrl());
// 场景信息
Map<String, Object> sceneInfoMap = new HashMap<>(4);
// 客户端IP
sceneInfoMap.put("payer_client_ip", "127.0.0.1");
// 商户端设备号(门店号或收银设备ID)
sceneInfoMap.put("device_id", "127.0.0.1");
// 除H5与JSAPI有特殊参数外,其他的支付方式都一样
if (type.equals(CombinePayUrlEnum.H5.getType())) {
Map<String, Object> h5InfoMap = new HashMap<>(4);
// 场景类型:iOS, Android, Wap
h5InfoMap.put("type", "IOS");
sceneInfoMap.put("h5_info", h5InfoMap);
} else if (type.equals(CombinePayUrlEnum.JSAPI.getType()) || type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
Map<String, Object> payerMap = new HashMap<>(4);
// 用户在服务商appid下的唯一标识,测试使用默认值 o6wzFwzBMdZDr-2VgR_ZsB1d0mYk(公众号应用的)
// 小程序下的openId暂无,所以无法测试
// 实际情况,应该是由前端获取openId值
payerMap.put("sub_openid", "o6wzFwzBMdZDr-2VgR_ZsB1d0mYk");
params.put("combine_payer_info", payerMap);
}
params.put("scene_info", sceneInfoMap);
// TODO 模拟两个订单
List<Map<String, Object>> subOrders = new ArrayList<>();
for (int a=1;a<=2;a++) {
// 子单信息
Map<String, Object> subOrderMap = new HashMap<>(4);
if (type.equals(CombinePayUrlEnum.JSAPI.getType()) || type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
// TODO 小程序或者公众号支付时,需要传入子应用的appId,并且在服务商平台的特约商户管理中进行授权
// 判断是否小程序还是服务号支付
String subAppId = WX_JSAPI_SUB_APPID;
if (type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
// 发起JSAPI的小程序
subAppId = WX_JSAPI_MINI_SUB_APPID;
}
// 子商户申请的应用ID,若combine_payer_info参数中sub_openid有传的情况下,sub_appid必填
subOrderMap.put("sub_appid", subAppId);
}
int outTradeNo = new Random().nextInt(999999999);
// 子单商户订单号
subOrderMap.put("out_trade_no", outTradeNo + "");
// 发起商户号
subOrderMap.put("mchid",wechatPayConfig.getMchId());
// 二级商户商户号(特约商户),由微信支付生成并下发。
subOrderMap.put("sub_mchid",subMchId);
// 附加信息
subOrderMap.put("attach", "附加信息");
// 商品描述
subOrderMap.put("description", "测试商品");
// 金额信息
Map<String, Object> amountMap = new HashMap<>(4);
// 金额单位为分
amountMap.put("total_amount", 1);
amountMap.put("currency", "CNY");
subOrderMap.put("amount", amountMap);
// 结算信息
Map<String, Object> settleInfoMap = new HashMap<>(4);
settleInfoMap.put("profit_sharing",false);
// 单笔订单最高补差金额
settleInfoMap.put("subsidy_amount",5000);
subOrderMap.put("settle_info", settleInfoMap);
// 子订单列表
subOrders.add(subOrderMap);
}
params.put("sub_orders",subOrders);
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
// 重写type值,因为小程序会多一个下划线(sub_type)
String[] split = type.split("_");
String newType = split[split.length - 1];
String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.COMBINE_TRANSACTIONS.getType().concat(newType)), paramsStr);
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
});
Map<String, Object> signMap = paySignMsg(resMap, type);
resMap.put("type", type);
resMap.put("signMap", signMap);
return resMap;
}
private Map<String, Object> paySignMsg(Map<String, Object> map, String type) {
// 设置签名信息,Native与H5不需要
if (type.equals(CombinePayUrlEnum.H5.getType()) || type.equals(CombinePayUrlEnum.NATIVE.getType())) {
return null;
}
long timeMillis = System.currentTimeMillis();
String appId = wechatPayConfig.getAppId();
String timeStamp = timeMillis / 1000 + "";
String nonceStr = timeMillis + "";
String prepayId = map.get("prepay_id").toString();
String packageStr = "prepay_id=" + prepayId;
// 公共参数
Map<String, Object> resMap = new HashMap<>();
resMap.put("nonceStr", nonceStr);
resMap.put("timeStamp", timeStamp);
// JSAPI、SUB_JSAPI(小程序)
if (type.equals(CombinePayUrlEnum.JSAPI.getType()) || type.equals(CombinePayUrlEnum.SUB_JSAPI.getType())) {
resMap.put("appId", appId);
resMap.put("package", packageStr);
// 使用字段appId、timeStamp、nonceStr、package进行签名
String paySign = createSign(resMap);
resMap.put("paySign", paySign);
resMap.put("signType", "HMAC-SHA256");
}
// APP
if (type.equals(CombinePayUrlEnum.APP.getType())) {
resMap.put("appid", appId);
resMap.put("prepayid", prepayId);
// 使用字段appId、timeStamp、nonceStr、prepayId进行签名
String sign = createSign(resMap);
resMap.put("package", "Sign=WXPay");
resMap.put("partnerid", wechatPayConfig.getMchId());
resMap.put("sign", sign);
resMap.put("signType", "HMAC-SHA256");
}
return resMap;
}
/**
* 获取加密数据
*/
private String createSign(Map<String, Object> params) {
try {
Map<String, Object> treeMap = new TreeMap<>(params);
List<String> signList = new ArrayList<>(5);
for (Map.Entry<String, Object> entry : treeMap.entrySet()) {
signList.add(entry.getKey() + "=" + entry.getValue());
}
String signStr = String.join("&", signList);
signStr = signStr + "&key=" + wechatPayConfig.getApiV3Key();
System.out.println(signStr);
Mac sha = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(wechatPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha.init(secretKey);
byte[] array = sha.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
}
signStr = sb.toString().toUpperCase();
System.out.println(signStr);
return signStr;
} catch (Exception e) {
throw new RuntimeException("加密失败!");
}
}
}
10.2 、其他处理(退款、查询、取消订单等)接口
包含接口:
根据订单号查询订单
、关闭(取消)订单
、申请退款
、查询单笔退款信息
、申请交易账单
、申请资金账单
、申请单个子商户资金账单
、下载账单
CombinePayController:
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.CombinePayUrlEnum;
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 javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @Description:
**/
@Api(tags = "合单支付接口(API3)")
@RestController
@RequestMapping("/combine")
@Slf4j
public class PayController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private WechatPayRequest wechatPayRequest;
/**
* 无需应答签名
*/
@Resource
private CloseableHttpClient wxPayNoSignClient;
// 模拟特约商户子商户号,实际情况:应该写在表中,与商家表进行映射,根据订单动态获取
private static final String subMchId ="xxx";
/**
* "根据合单商户订单号,查询订单接口",如果要查询单个订单可以调用,普通的订单查询接口(transactions)
* @param combineOutTradeNo
* @return
*/
@ApiOperation(value = "根据合单订单号-统一接口", notes = "根据合单订单号-统一接口")
@ApiOperationSupport(order = 15)
@GetMapping("/combineTransactions/{combineOutTradeNo}")
public Map<String, Object> transactionsByOrderNo(@PathVariable("combineOutTradeNo") String combineOutTradeNo) {
// TODO 如果是扫码支付时,该接口就很有必要,应该前端通过轮询的方式请求该接口查询订单是否支付成功
log.info("根据合单订单号查询订单,合单号: {}", combineOutTradeNo);
String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.ORDER_QUERY_BY_NO.getType().concat(combineOutTradeNo));
String res = wechatPayRequest.wechatHttpGet(url);
log.info("查询订单结果:{}", res);
Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
});
String outTradeNo = resMap.get("combine_out_trade_no").toString();
String appId = resMap.get("combine_appid").toString();
String mchId = resMap.get("combine_mchid").toString();
// 支付后才返回参数
Object payer = resMap.get("combine_payer_info");
log.info("outTradeNo:" + outTradeNo);
log.info("appId:" + appId);
log.info("mchId:" + mchId);
log.info("payer:" + payer);
// 子订单数据
String subOrders = resMap.get("sub_orders").toString();
log.info("subOrders:" + subOrders);
if(subOrders!=null){
List<Map<String, Object>> subOrderList = JSONObject.parseObject(subOrders, new TypeReference<List<Map<String, Object>>>() {
});
log.info("============== 子订单信息: ==============");
for(Map<String, Object> subOrderMap:subOrderList) {
String subOrderNo = subOrderMap.get("out_trade_no").toString();
String subOrderTransactionId = subOrderMap.get("transaction_id").toString();
String subOrderMchId = subOrderMap.get("mchid").toString();
String subOrderSubMchId = subOrderMap.get("sub_mchid").toString();
// 支付后才返回参数
Object subOrderAttach = subOrderMap.get("attach");
/**
* 交易状态,枚举值:
* SUCCESS:支付成功
* REFUND:转入退款
* NOTPAY:未支付
* CLOSED:已关闭
* REVOKED:已撤销(仅付款码支付会返回)
* USERPAYING:用户支付中(仅付款码支付会返回)
* PAYERROR:支付失败(仅付款码支付会返回)
*/
String subOrderTradeState = subOrderMap.get("trade_state").toString();
log.info("subOrderNo:" + subOrderNo);
log.info("subOrderTransactionId:" + subOrderTransactionId);
log.info("subOrderMchId:" + subOrderMchId);
log.info("subOrderTradeState:" + subOrderTradeState);
log.info("subOrderAttach:" + subOrderAttach);
log.info("subMchId:" + subOrderSubMchId);
}
}
return resMap;
}
/**
* 关闭(取消)订单
* @param combineOutTradeNo
* @return
*/
@ApiOperation(value = "合单关闭(取消)订单-统一接口", notes = "合单关闭(取消)订单-统一接口")
@ApiOperationSupport(order = 20)
@PostMapping("/combineCloseOrder/{combineOutTradeNo}")
public void closeOrder(@PathVariable("combineOutTradeNo") String combineOutTradeNo) {
// TODO 用于在客户下单后,不进行支付,取消订单的场景
log.info("根据合单订单号查询订单,合单号: {}", combineOutTradeNo);
String url = String.format(CombinePayUrlEnum.CLOSE_ORDER_BY_NO.getType(), combineOutTradeNo);
url = wechatPayConfig.getBaseUrl().concat(url);
// 设置参数
Map<String, Object> params = new HashMap<>(2);
params.put("combine_appid", wechatPayConfig.getAppId());
// TODO 模拟两个订单
List<Map<String, Object>> subOrders = new ArrayList<>();
for (int a=1;a<=2;a++) {
// 子单信息
Map<String, Object> subOrderMap = new HashMap<>(4);
// 此处进行模拟生成,实际情况下,应该由前端进行传递,而`combineOutTradeNo`则是根据`outTradeNo`从数据库中获取
int outTradeNo = new Random().nextInt(999999999);
// 子单商户订单号
subOrderMap.put("out_trade_no", outTradeNo + "");
// 发起商户号
subOrderMap.put("mchid",wechatPayConfig.getMchId());
// 二级商户商户号(特约商户),由微信支付生成并下发。
subOrderMap.put("sub_mchid",subMchId);
subOrders.add(subOrderMap);
}
params.put("sub_orders",subOrders);
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
String res = wechatPayRequest.wechatHttpPost(url,paramsStr);
}
/**
* 申请退款
* @param orderNo
*/
@ApiOperation(value = "申请退款-统一接口", notes = "申请退款-统一接口")
@ApiOperationSupport(order = 25)
@PostMapping("/refundOrder/{orderNo}")
public void refundOrder(@PathVariable("orderNo") String orderNo) {
log.info("根据订单号申请退款,订单号: {}", orderNo);
String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.DOMESTIC_REFUNDS.getType());
// 设置参数
Map<String, Object> params = new HashMap<>(2);
// 订单编号
params.put("out_trade_no", orderNo);
params.put("sub_mchid", subMchId);
// 退款单编号 - 自定义
int outRefundNo = new Random().nextInt(999999999);
log.info("退款申请号:{}",outRefundNo);
params.put("out_refund_no",outRefundNo+"");
// 退款原因
params.put("reason","申请退款");
// 退款通知回调地址
params.put("notify_url", wechatPayConfig.getRefundNotifyUrl());
Map<String, Object> amountMap =new HashMap<>();
//退款金额,单位:分
amountMap.put("refund", 1);
//原订单金额,单位:分
amountMap.put("total", 1);
//退款币种
amountMap.put("currency", "CNY");
params.put("amount", amountMap);
String paramsStr = JSON.toJSONString(params);
log.info("请求参数 ===> {}" + paramsStr);
String res = wechatPayRequest.wechatHttpPost(url,paramsStr);
log.info("退款结果:{}",res);
}
/**
* 查询单笔退款信息
* @param refundNo
* @return
*/
@ApiOperation(value = "查询单笔退款信息-统一接口", notes = "查询单笔退款信息-统一接口")
@ApiOperationSupport(order = 30)
@GetMapping("/queryRefundOrder/{refundNo}")
public Map<String, Object> queryRefundOrder(@PathVariable("refundNo") String refundNo) {
log.info("根据订单号查询退款订单,订单号: {}", refundNo);
String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.DOMESTIC_REFUNDS_QUERY.getType().concat(refundNo))
.concat("?sub_mchid=").concat(subMchId);
String res = wechatPayRequest.wechatHttpGet(url);
log.info("查询退款订单结果:{}",res);
Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
String successTime = resMap.get("success_time").toString();
String refundId = resMap.get("refund_id").toString();
/**
* 款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台-交易中心,手动处理此笔退款。
* 枚举值:
* SUCCESS:退款成功
* CLOSED:退款关闭
* PROCESSING:退款处理中
* ABNORMAL:退款异常
*/
String status = resMap.get("status").toString();
/**
* 枚举值:
* ORIGINAL:原路退款
* BALANCE:退回到余额
* OTHER_BALANCE:原账户异常退到其他余额账户
* OTHER_BANKCARD:原银行卡异常退到其他银行卡
*/
String channel = resMap.get("channel").toString();
String userReceivedAccount = resMap.get("user_received_account").toString();
log.info("successTime:"+successTime);
log.info("channel:"+channel);
log.info("refundId:"+refundId);
log.info("status:"+status);
log.info("userReceivedAccount:"+userReceivedAccount);
// TODO 在查询单笔退款信息时,可以再去查询一次订单的状态,保证该订单已经退款完毕了
return resMap;
}
/**
* 申请交易账单
* @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请 ,如果传入日期未为当天则会出错
* @param billType 分为:ALL、SUCCESS、REFUND
* ALL:返回当日所有订单信息(不含充值退款订单)
* SUCCESS:返回当日成功支付的订单(不含充值退款订单)
* REFUND:返回当日退款订单(不含充值退款订单)
* @return
*/
@ApiOperation(value = "申请交易账单-统一接口", notes = "申请交易账单-统一接口")
@ApiOperationSupport(order = 35)
@GetMapping("/tradeBill")
public String tradeBill(@RequestParam("billDate") String billDate, @RequestParam("billType") String billType) {
log.info("申请交易账单,billDate:{},billType:{}", billDate,billType);
String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.TRADE_BILLS.getType())
.concat("?bill_date=").concat(billDate).concat("&bill_type=").concat(billType);
// 填则默认返回服务商下的交易或退款数据,下载某个子商户下的交易或退款数据,则该字段必填
url = url.concat("&sub_mchid=").concat(subMchId);
String res = wechatPayRequest.wechatHttpGet(url);
log.info("查询退款订单结果:{}",res);
Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
String downloadUrl = resMap.get("download_url").toString();
return downloadUrl;
}
/**
*
* @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
* @param accountType 分为:BASIC、OPERATION、FEES
* BASIC:基本账户
* OPERATION:运营账户
* FEES:手续费账户
* @return
*/
@ApiOperation(value = "申请资金账单-统一接口", notes = "申请资金账单-统一接口")
@ApiOperationSupport(order = 40)
@GetMapping("/fundFlowBill")
public String fundFlowBill(@RequestParam("billDate") String billDate, @RequestParam("accountType") String accountType) {
log.info("申请交易账单,billDate:{},accountType:{}", billDate,accountType);
String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.FUND_FLOW_BILLS.getType())
.concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType);
String res = wechatPayRequest.wechatHttpGet(url);
log.info("查询退款订单结果:{}",res);
Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
String downloadUrl = resMap.get("download_url").toString();
return downloadUrl;
}
/**
*
* @param billDate 格式yyyy-MM-dd 仅支持三个月内的账单下载申请,如果传入日期未为当天则会出错
* @param accountType 分为:BASIC、OPERATION、FEES
* BASIC:基本账户
* OPERATION:运营账户
* FEES:手续费账户
* @return
*/
@ApiOperation(value = "申请单个子商户资金账单-统一接口", notes = "申请单个子商户资金账单-统一接口")
@ApiOperationSupport(order = 40)
@GetMapping("/subFundFlowBill")
public String subMerchantFundFlowBill(@RequestParam("billDate") String billDate, @RequestParam("accountType") String accountType) {
log.info("申请单个子商户资金账单,billDate:{},accountType:{}", billDate,accountType);
String url = wechatPayConfig.getBaseUrl().concat(CombinePayUrlEnum.FUND_FLOW_BILLS.getType())
.concat("?bill_date=").concat(billDate).concat("&account_type=").concat(accountType)
.concat("&sub_mchid=").concat(billDate).concat("&algorithm=").concat("AEAD_AES_256_GCM");
String res = wechatPayRequest.wechatHttpGet(url);
log.info("查询退款订单结果:{}",res);
Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>(){});
String downloadBillCount = resMap.get("download_bill_count").toString();
String downloadBillList = resMap.get("download_bill_list").toString();
List<Map<String, Object>> billListMap = JSONObject.parseObject(downloadBillList, new TypeReference<List<Map<String, Object>>>(){});
String downloadUrl = billListMap.get(0).get("download_url").toString();
log.info("downloadBillCount="+downloadBillCount);
log.info("downloadUrl="+downloadUrl);
return downloadUrl;
}
@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();
}
}
}
}
11、支付/退款回调通知
NotifyController:文章来源:https://www.toymoban.com/news/detail-490371.html
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.pay.WechatPayConfig;
import com.lhz.demo.pay.WechatPayValidator;
import com.lhz.demo.utils.HttpUtils;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author:
* @Description:
**/
@Api(tags = "回调接口(API3)")
@RestController
@Slf4j
public class NotifyController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private Verifier verifier;
private final ReentrantLock lock = new ReentrantLock();
@ApiOperation(value = "合单支付回调", notes = "合单支付回调")
@ApiOperationSupport(order = 5)
@PostMapping("/combinePayNotify")
public Map<String, String> combinePayNotify(HttpServletRequest request, HttpServletResponse response) {
log.info("合单支付回调");
if(lock.tryLock()) {
// 处理通知参数
Map<String,Object> bodyMap = getNotifyBody(request);
if(bodyMap==null){
return falseMsg(response);
}
log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
try {
// 解密resource中的通知数据
String resource = bodyMap.get("resource").toString();
Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),1);
log.info("通知参数:{}", JSON.toJSONString(resourceMap));
// 合单号s
String combineOutTradeNo = resourceMap.get("combine_out_trade_no").toString();
// 合单商户appid
String combineAppid = resourceMap.get("combine_appid").toString();
// 合单商户号
String combineMchid = resourceMap.get("combine_mchid").toString();
// 子订单信息
// 子订单数据
String subOrders = resourceMap.get("sub_orders").toString();
log.info("subOrders:" + subOrders);
if(subOrders!=null){
List<Map<String, Object>> subOrderList = JSONObject.parseObject(subOrders, new TypeReference<List<Map<String, Object>>>() {
});
log.info("============== 子订单信息: ==============");
for(Map<String, Object> subOrderMap:subOrderList) {
String subOrderNo = subOrderMap.get("out_trade_no").toString();
String subOrderTransactionId = subOrderMap.get("transaction_id").toString();
String subOrderMchId = subOrderMap.get("mchid").toString();
String subOrderSubMchId = subOrderMap.get("sub_mchid").toString();
String subOrderSuccessTime = subOrderMap.get("success_time").toString();
// 支付后才返回参数
Object subOrderAttach = subOrderMap.get("attach");
/**
* 交易状态,枚举值:
* SUCCESS:支付成功
* REFUND:转入退款
* NOTPAY:未支付
* CLOSED:已关闭
* REVOKED:已撤销(仅付款码支付会返回)
* USERPAYING:用户支付中(仅付款码支付会返回)
* PAYERROR:支付失败(仅付款码支付会返回)
*/
String subOrderTradeState = subOrderMap.get("trade_state").toString();
log.info("subOrderNo:" + subOrderNo);
log.info("subOrderTransactionId:" + subOrderTransactionId);
log.info("subOrderMchId:" + subOrderMchId);
log.info("subOrderTradeState:" + subOrderTradeState);
log.info("subOrderAttach:" + subOrderAttach);
log.info("subOrderSubMchId:" + subOrderSubMchId);
log.info("subOrderSuccessTime:" + subOrderSuccessTime);
}
}
// TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
log.warn("=========== 根据订单号,做幂等处理 ===========");
} finally {
//要主动释放锁
lock.unlock();
}
}
//成功应答
return trueMsg(response);
}
@ApiOperation(value = "退款回调", notes = "退款回调")
@ApiOperationSupport(order = 5)
@PostMapping("/refundNotify")
public Map<String, String> refundNotify(HttpServletRequest request, HttpServletResponse response) {
log.info("退款回调");
if(lock.tryLock()) {
// 处理通知参数
Map<String,Object> bodyMap = getNotifyBody(request);
if(bodyMap==null){
return falseMsg(response);
}
log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
try {
// 解密resource中的通知数据
String resource = bodyMap.get("resource").toString();
Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),2);
log.info("通知参数:{}", JSON.toJSONString(resourceMap));
String orderNo = resourceMap.get("out_trade_no").toString();
String transactionId = resourceMap.get("transaction_id").toString();
// TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
log.warn("=========== 根据订单号,做幂等处理 ===========");
} finally {
//要主动释放锁
lock.unlock();
}
}
//成功应答
return trueMsg(response);
}
private Map<String,Object> getNotifyBody(HttpServletRequest request){
//处理通知参数
String body = HttpUtils.readData(request);
log.info("退款回调参数:{}",body);
// 转换为Map
Map<String, Object> bodyMap = JSONObject.parseObject(body, new TypeReference<Map<String, Object>>(){});
// 微信的通知ID(通知的唯一ID)
String notifyId = bodyMap.get("id").toString();
// 验证签名信息
WechatPayValidator wechatPayValidator
= new WechatPayValidator(verifier, notifyId, body);
if(!wechatPayValidator.validate(request)){
log.error("通知验签失败");
return null;
}
log.info("通知验签成功");
return bodyMap;
}
private Map<String, String> falseMsg(HttpServletResponse response){
Map<String, String> resMap = new HashMap<>(8);
//失败应答
response.setStatus(500);
resMap.put("code", "ERROR");
resMap.put("message", "通知验签失败");
return resMap;
}
private Map<String, String> trueMsg(HttpServletResponse response){
Map<String, String> resMap = new HashMap<>(8);
//成功应答
response.setStatus(200);
resMap.put("code", "SUCCESS");
resMap.put("message", "成功");
return resMap;
}
}
文章来源地址https://www.toymoban.com/news/detail-490371.html
到了这里,关于微信服务商模式(电商收付通)合单支付APIV3完整Demo,可直接使用,适用于(H5、JSAPI、H5、App、小程序)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!