一、支付流程
直连商户:单店、单账号;单店的钱打到单账号上。
服务商模式:总店、总账号、分店、分帐号;各个分店的钱打到各个分帐号上。
1.1、支付准备
1.获取商户号
微信商户平台 申请成为商户 => 提交资料 => 签署协议 => 获取商户号
2.获取 AppID
微信公众平台 注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
3.申请商户证书
登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
4.获取微信的证书
5.获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
1.2、开发平台
1.2.1、开发平台创建应用
1.2.2、开发平台获取应用ID
1.3、商户平台
1.3.1、商户平台设置API密钥
1.3.2、商户平台查看商户号
二、 V2 直连商户
微信小程序账号:要认证、获取appid、生成secret、开通支付、关联商户号
微信商户平台账号:要认证、获取商户号mch_id、获取商户API密钥mch_key、APPID授权、配置支付接口
2.1、依赖与配置
2.1.1、依赖
<!-- 微信支付 SDK -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
2.1.2、配置文件
# 微信支付配置 notifyUrl:微信支付异步回调地址
pay:
appId: #应用id
apiKey: #商户私钥key
mchId: #商户id
appSecret: #小程序密钥
notifyUrl: #支付回调地址
2.1.3、配置类
2.1.3.1、微信支付配置类
/**
* 微信支付配置
*/
@Data
@Component
@Configuration
@ConfigurationProperties(prefix = "pay")
public class WxPayConfig {
/**
* 微信公众号appid
*/
private String appId;
/**
* 公众号设置的API v2密钥
*/
private String apiKey;
/**
* 微信商户平台 商户id
*/
private String mchId;
/**
*小程序密钥
*/
private String appSecret;
/**
* 小程序支付异步回调地址
*/
private String notifyUrl;
}
2.1.3.2、微信支付预下单实体类
/**
* 微信支付预下单实体类
*/
@Data
@Accessors(chain = true)
public class WeChatPay {
/**
* 返回状态码 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断
*/
public String return_code;
/**
* 返回信息 当return_code为FAIL时返回信息为错误原因 ,例如 签名失败 参数格式校验错误
*/
private String return_msg;
/**
* 公众账号ID 调用接口提交的公众账号ID
*/
private String appid;
/**
* 商户号 调用接口提交的商户号
*/
private String mch_id;
/**
* api密钥 详见:https://pay.weixin.qq.com/index.php/extend/employee
*/
private String api_key;
/**
* 设备号 自定义参数,可以为请求支付的终端设备号等
*/
private String device_info;
/**
* 随机字符串 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 微信返回的随机字符串
*/
private String nonce_str;
/**
* 签名 微信返回的签名值,详见签名算法:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
*/
private String sign;
/**
* 签名类型
*/
private String sign_type;
/**
* 业务结果 SUCCESS SUCCESS/FAIL
*/
private String result_code;
/**
* 错误代码 当result_code为FAIL时返回错误代码,详细参见下文错误列表
*/
private String err_code;
/**
* 错误代码描述 当result_code为FAIL时返回错误描述,详细参见下文错误列表
*/
private String err_code_des;
/**
* 交易类型 JSAPI JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付 说明详见;https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String trade_type;
/**
* 预支付交易会话标识 微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时
*/
private String prepay_id;
/**
* 二维码链接 weixin://wxpay/bizpayurl/up?pr=NwY5Mz9&groupid=00 trade_type=NATIVE时有返回,此url用于生成支付二维码,然后提供给用户进行扫码支付。注意:code_url的值并非固定,使用时按照URL格式转成二维码即可
*/
private String code_url;
/**
* 商品描述 商品简单描述,该字段请按照规范传递,具体请见 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String body;
/**
* 商家订单号 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一。详见商户订单号 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String out_trade_no;
/**
* 标价金额 订单总金额,单位为分,详见支付金额 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_2
*/
private String total_fee;
/**
* 终端IP 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP
*/
private String spbill_create_ip;
/**
* 通知地址 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
*/
private String notify_url;
/**
* 子商户号 sub_mch_id 非必填(商户不需要传入,服务商模式才需要传入) 微信支付分配的子商户号
*/
private String sub_mch_id;
/**
* 附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
*/
private String attach;
/**
* 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
*/
private String out_refund_no;
/**
* 退款总金额,单位为分,只能为整数,可部分退款。详见支付金额 https://pay.weixin.qq.com/wiki/doc/api/native_sl.php?chapter=4_2
*/
private String refund_fee;
/**
* 退款原因 若商户传入,会在下发给用户的退款消息中体现退款原因 注意:若订单退款金额≤1元,且属于部分退款,则不会在退款消息中体现退款原因
*/
private String refund_desc;
/**
* 交易结束时间 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。其他详见时间规则 注意:最短失效时间间隔必须大于5分钟
*/
private String time_expire;
/**
* 用户标识 trade_type=JSAPI,此参数必传,用户在主商户appid下的唯一标识。openid和sub_openid可以选传其中之一,如果选择传sub_openid,则必须传sub_appid。下单前需要调用【网页授权获取用户信息: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 】接口获取到用户的Openid。
*/
private String openid;
/**
* 时间戳
*/
private String time_stamp;
/**
* 会员类型
*/
private String memberShipType;
}
2.1.3.3、预下单成功之后返回结果实体类
/**
* 预下单成功之后返回结果实体类
*/
@Data
public class OrderReturnInfo {
private String return_code;
private String return_msg;
private String result_code;
private String appid;
private String mch_id;
private String nonce_str;
private String sign;
private String prepay_id;
private String trade_type;
}
2.1.3.4、查询订单返回的实体类
/**
* 查询订单返回实体类
*/
@Data
public class QueryReturnInfo {
private String return_code;
private String return_msg;
private String result_code;
private String err_code;
private String err_code_des;
private String appid;
private String mch_id;
private String nonce_str;
private String sign;
private String prepay_id;
private String trade_type;
private String device_info;
private String openid;
private String is_subscribe;
private String trade_state;
private String bank_type;
private int total_fee;
private int settlement_total_fee;
private String fee_type;
private int cash_fee;
private String cash_fee_type;
private int coupon_fee;
private int coupon_count;
private String coupon_type_$n;
private String coupon_id_$n;
private String transaction_id;
private String out_trade_no;
private String time_end;
private String trade_state_desc;
}
2.1.3.5、签名实体类
/**
* 签名实体类
*/
@Data
public class SignInfo {
private String appId;//小程序ID
private String timeStamp;//时间戳
private String nonceStr;//随机串
@XStreamAlias("package")
private String repay_id;
private String signType;//签名方式
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(String timeStamp) {
this.timeStamp = timeStamp;
}
public String getNonceStr() {
return nonceStr;
}
public void setNonceStr(String nonceStr) {
this.nonceStr = nonceStr;
}
public String getRepay_id() {
return repay_id;
}
public void setRepay_id(String repay_id) {
this.repay_id = repay_id;
}
public String getSignType() {
return signType;
}
public void setSignType(String signType) {
this.signType = signType;
}
}
2.2、工具类
2.2.1、微信支付API地址
/**
* 微信支付API地址
*/
public class WeChatPayUrlConstants {
/**
* 统一下单预下单接口url
*/
public static final String Uifiedorder = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 订单状态查询接口URL
*/
public static final String Orderquery = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 订单申请退款
*/
public static final String Refund = "https://api.mch.weixin.qq.com/secapi/pay/refund";
/**
* 付款码 支付
*/
public static final String MicroPay = "https://api.mch.weixin.qq.com/pay/micropay";
/**
* 微信网页授权 获取“code”请求地址
*/
public static final String GainCodeUrl = "https://open.weixin.qq.com/connect/oauth2/authorize";
/**
* 微信网页授权 获取“code” 回调地址
*/
public static final String GainCodeRedirect_uri = "http://i5jmxe.natappfree.cc/boss/WeChatPayMobile/SkipPage.html";
}
2.2.2、Http工具类
/**
* Http工具类
*/
public class HttpRequest {
//连接超时时间,默认10秒
private static final int socketTimeout = 10000;
//传输超时时间,默认30秒
private static final int connectTimeout = 30000;
/**
* post请求
*/
public static String sendPost(String url, Object xmlObj) throws ClientProtocolException, IOException, UnrecoverableKeyException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException {
HttpPost httpPost = new HttpPost(url);
//解决XStream对出现双下划线的bug
XStream xStreamForRequestPostData = new XStream(new DomDriver("UTF-8", new XmlFriendlyNameCoder("-_", "_")));
xStreamForRequestPostData.alias("xml", xmlObj.getClass());
//将要提交给API的数据对象转换成XML格式数据Post给API
String postDataXML = xStreamForRequestPostData.toXML(xmlObj);
//得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
StringEntity postEntity = new StringEntity(postDataXML, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(postEntity);
//设置请求器的配置
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
httpPost.setConfig(requestConfig);
HttpClient httpClient = HttpClients.createDefault();
HttpResponse response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
return result;
}
/**
* 自定义证书管理器,信任所有证书
*/
public static class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] arg0, String arg1)throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] arg0, String arg1)throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}
2.2.3、微信签名
/**
* 微信签名
*/
public class SignUtils {
/**
* 签名算法
* @param o 要参与签名的数据对象
* @return 签名
*/
public static String getSign(Object o) throws IllegalAccessException {
ArrayList<String> list = new ArrayList<String>();
Class cls = o.getClass();
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
f.setAccessible(true);
if (f.get(o) != null && f.get(o) != "") {
String name = f.getName();
XStreamAlias anno = f.getAnnotation(XStreamAlias.class);
if (anno != null) {
name = anno.value();
}
list.add(name + "=" + f.get(o) + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + Configure.getKey();
System.out.println("签名数据:" + result);
result = MD5.MD5Encode(result).toUpperCase();
return result;
}
public static String getSign(Map<String, Object> map) {
ArrayList<String> list = new ArrayList<String>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() != "") {
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + Configure.getKey();
result = MD5.MD5Encode(result).toUpperCase();
return result;
}
}
2.2.4、MD5加密工具类
/**
* MD5加密工具类
*/
public class MD5 {
private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7","8", "9", "a", "b", "c", "d", "e", "f"};
/**
* 转换字节数组为16进制字串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder resultSb = new StringBuilder();
for (byte aB : b) {
resultSb.append(byteToHexString(aB));
}
return resultSb.toString();
}
/**
* 转换byte到16进制
*/
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
/**
* MD5编码
*/
public static String MD5Encode(String origin) {
String resultString = null;
try {
resultString = origin;
MessageDigest md = MessageDigest.getInstance("MD5");
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
return resultString;
}
}
2.3、业务接口
/**
* 业务接口
*/
@Slf4j
public class WxPayInfo {
@Resource
private WxPayConfig payProperties;
private static final DecimalFormat df = new DecimalFormat("#");
/**
* 插入订单记录
*/
@Transactional
public Map insertPayRecord(PayParameterVO payParameterVO) {
//接收返回的参数
Map<String, Object> map = new HashMap<>();
String title = "koko测试点数";
//金额 * 100 以分为单位
BigDecimal fee = BigDecimal.valueOf(1);
BigDecimal RMB = new BigDecimal(100);
BigDecimal totalFee = fee.multiply(RMB);
try {
WeChatPay weChatPay = new WeChatPay();
weChatPay.setAppid(payProperties.getAppId());
weChatPay.setMch_id(payProperties.getMchId());
weChatPay.setNonce_str(getRandomStringByLength(32));
weChatPay.setBody(title);
weChatPay.setOut_trade_no(getRandomStringByLength(32));
weChatPay.setTotal_fee( df.format(Double.parseDouble(String.valueOf(totalFee))));
weChatPay.setSpbill_create_ip("127.0.0.1");
weChatPay.setNotify_url(payProperties.getNotifyUrl());
weChatPay.setTrade_type("JSAPI");
//这里直接使用当前用户的openid
weChatPay.setOpenid("oOKq*******xj8o");
weChatPay.setSign_type("MD5");
//生成签名
String sign = SignUtils.getSign(weChatPay);
weChatPay.setSign(sign);
String result = HttpRequest.sendPost(WeChatPayUrlConstants.Uifiedorder, weChatPay);
System.out.println(result);
//将返回结果从xml格式转换为map格式
Map<String, String> wxResultMap = WXPayUtil.xmlToMap(result);
if (ObjectUtil.isNotEmpty(wxResultMap.get("return_code")) && wxResultMap.get("return_code").equals("SUCCESS")){
if (wxResultMap.get("result_code").equals("FAIL")){
map.put("msg", "统一下单失败");
map.put("status",500);
map.put("data", wxResultMap.get("err_code_des"));
return map;
}
}
XStream xStream = new XStream();
xStream.alias("xml", OrderReturnInfo.class);
OrderReturnInfo returnInfo = (OrderReturnInfo) xStream.fromXML(result);
// 二次签名
if ("SUCCESS".equals(returnInfo.getReturn_code()) && returnInfo.getReturn_code().equals(returnInfo.getResult_code())) {
SignInfo signInfo = new SignInfo();
signInfo.setAppId(payProperties.getAppId());
long time = System.currentTimeMillis() / 1000;
signInfo.setTimeStamp(String.valueOf(time));
signInfo.setNonceStr(WXPayUtil.generateNonceStr());
signInfo.setRepay_id("prepay_id=" + returnInfo.getPrepay_id());
signInfo.setSignType("MD5");
//生成签名
String sign1 = SignUtils.getSign(signInfo);
Map<String, String> payInfo = new HashMap<>();
payInfo.put("timeStamp", signInfo.getTimeStamp());
payInfo.put("nonceStr", signInfo.getNonceStr());
payInfo.put("package", signInfo.getRepay_id());
payInfo.put("signType", signInfo.getSignType());
payInfo.put("paySign", sign1);
map.put("status", 200);
map.put("msg", "统一下单成功!");
map.put("data", payInfo);
//预下单成功,处理业务逻辑
//****************************//
// 业务逻辑结束 回传给小程序端唤起支付
return map;
}
map.put("status", 500);
map.put("msg", "统一下单失败!");
map.put("data", null);
return map;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 查询订单
* @param out_trade_no 订单号
* @return 返回结果
*/
public Map orderQuery(String out_trade_no){
Map<String, Object> map = new HashMap<>();
try {
WeChatPay weChatPay = new WeChatPay();
weChatPay.setAppid(payProperties.getAppId());
weChatPay.setMch_id(payProperties.getMchId());
weChatPay.setNonce_str(WXPayUtil.generateNonceStr());
weChatPay.setOut_trade_no(out_trade_no);
//order.setSign_type("MD5");
//生成签名
String sign = SignUtils.getSign(weChatPay);
weChatPay.setSign(sign);
String result = HttpRequest.sendPost(WXPayConstants.ORDERQUERY_URL, weChatPay);
System.out.println(result);
XStream xStream = new XStream();
xStream.alias("xml", QueryReturnInfo.class);
QueryReturnInfo returnInfo = (QueryReturnInfo) xStream.fromXML(result);
map.put("status", 500);
map.put("msg", "统一下单失败!");
map.put("data", returnInfo);
return map;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 微信小程序支付成功回调
*/
public String callBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("接口已被调用");
ServletInputStream inputStream = request.getInputStream();
String notifyXml = StreamUtils.inputStream2String(inputStream, "utf-8");
System.out.println(notifyXml);
// 解析返回结果
Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyXml);
// 判断支付是否成功
if ("SUCCESS".equals(notifyMap.get("result_code"))) {
//支付成功时候,处理业务逻辑
System.out.println("支付成功");
System.out.println("<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ");
/**
* 注意
* 因为微信回调会有八次之多,所以当第一次回调成功了,那么我们就不再执行逻辑了
* return返回的结果一定是这种格式,当result_code返回的结果是SUCCESS时,则不进行调用了
* 如果不返回下面的格式,业务逻辑会出现回调多次的情况,我就遇到过这种情况。
*/
return "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
}
// 创建响应对象:微信接收到校验失败的结果后,会反复的调用当前回调函数
Map<String, String> returnMap = new HashMap<>();
returnMap.put("return_code", "FAIL");
returnMap.put("return_msg", "");
String returnXml = WXPayUtil.mapToXml(returnMap);
response.setContentType("text/xml");
System.out.println("校验失败");
return returnXml;
}
/**
* 获取一定长度的随机字符串
*/
public static String getRandomStringByLength(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
三、V3 直连商户
3.1、前置条件
Java 1.8+。
1.成为微信支付商户。
2.商户 API 证书:指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
3.商户 API 私钥:商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。
4.APIv3密钥:为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了 AES-256-GCM 加密。APIv3密钥是加密时使用的对称密钥。
3.2、流程
1.前端点支付按钮,调起微信自带支付页面前,向后端发请求;
2.后端调用微信的 "统一下单" 接口,把本地订单号一起发去,得到一个 prepay_id;
3.后端对 prepay_id 和一些参数做一个算法,得到相对应的签名值,返回给前端;
4.前端就可根据这些返回值调用支付;
5.支付成功,把本地的订单号一起传给去给微信,本地的订单号跟微信的订单绑定,只要支付了腾讯的订单,就完成本地订单。
商户平台-申请证书设置V3秘钥
商户平台—设置APPid账号管理
证书秘钥
3.3、依赖与配置
3.3.1、依赖
<!-- 微信支付依赖 -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
3.3.2、配置
public interface DirectConnection {
//支付成功后的回调地址
String NOTIFY_URL = "";
//商户号
String MCH_ID = "";
//商户证书序列号
String MCH_SERIAL_NO = "";
//V3密钥
String API_3KEY = "";
//小程序或者公众号的ApId
String APP_ID = "";
// 商户证书序列号对应的证书秘钥
String privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDOsZnx5Nh4qK7O" +
"vzbDOQu5UMtSdoZWyqOC+gFNVAB7aPAzQwgN7OAUt7G8synPRdovQ/l116dZ0ZiX"+
"XQX3Le8/o5szRH6LxpqcpFMaZg2N/HOydyTMaHI0wnZIc9BXR8aaXl7uVQnydF40"+
"FoWicge6vTCXOyjirTpS2PGKy9+hu0vx7GbX1NUDl2hNXkH54pdWn5eof1fnbh/V"+
"45q/OS7d9qnpYfs1ff+0nA==";
}
3.4、工具类
3.4.1、下单工具类
/**
* 下单工具类
*/
public class PayUtil {
private CloseableHttpClient httpClient;
private CertificatesManager certificatesManager;
private Verifier verifier;
private PrivateKey merchantPrivateKey;
{
try {
merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(DirectConnection.privateKey.getBytes("utf-8")));
// 获取证书管理器实例
certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(DirectConnection.MCH_ID, new WechatPay2Credentials(DirectConnection.MCH_ID,
new PrivateKeySigner(DirectConnection.MCH_SERIAL_NO, merchantPrivateKey)),
DirectConnection.API_3KEY.getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(DirectConnection.MCH_ID);
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(DirectConnection.MCH_ID, DirectConnection.MCH_SERIAL_NO, merchantPrivateKey)
.withValidator(new WechatPay2Validator(certificatesManager.getVerifier(DirectConnection.MCH_ID)))
.build();
} catch (IOException | GeneralSecurityException | HttpCodeException | NotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 统一下单,获取到 prepay_id,然后获取签名。
*/
public String requestwxChatPay(String orderSn, int total, String description,String openid) throws Exception {
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
//组合请求参数JSON格式
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid", DirectConnection.MCH_ID)
.put("appid", DirectConnection.APP_ID)
.put("notify_url", DirectConnection.NOTIFY_URL + "returnNotify")
.put("description", description)
.put("out_trade_no", orderSn);
rootNode.putObject("amount")
// total:金额,以分为单位,假如是10块钱,那就要写 1000
.put("total", total)
.put("currency", "CNY");
rootNode.putObject("payer")
// openid:用户在该小程序或者公众号下的openid
.put("openid", openid);
try {
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
//获取预支付ID
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
//微信成功响应
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
//时间戳
String timestamp = System.currentTimeMillis() / 1000 + "";
//随机字符串
String nonce = RandomUtil.randomString(32);
StringBuilder builder = new StringBuilder();
// Appid
builder.append(DirectConnection.APP_ID).append("\n");
// 时间戳
builder.append(timestamp).append("\n");
// 随机字符串
builder.append(nonce).append("\n");
JsonNode jsonNode = objectMapper.readTree(bodyAsString);
// 预支付会话ID
builder.append("prepay_id=").append(jsonNode.get("prepay_id").textValue()).append("\n");
//获取签名
String sign = this.sign(builder.toString().getBytes("utf-8"), merchantPrivateKey);
JSONObject jsonMap = new JSONObject();
jsonMap.put("noncestr", nonce);
jsonMap.put("timestamp", timestamp);
jsonMap.put("prepayid", jsonNode.get("prepay_id").textValue());
jsonMap.put("sign", sign);
jsonMap.put("appid", DirectConnection.APP_ID);
jsonMap.put("partnerid", DirectConnection.MCH_ID);
return jsonMap.toJSONString();//响应签名数据,前端拿着响应数据调起微信SDK
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 计算签名
*/
private String sign(byte[] message, PrivateKey yourPrivateKey) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(yourPrivateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
}
return "";
}
}
3.4.2、回调签名工具类
/**
* 回调签名工具类
*/
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.aesKey = key;
}
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
3.5、Controller
@RestController
@RequestMapping(value = "/pay")
public class payController {
/**
* 预支付下单
* @param orderSn 订单号
* @param total 分
* @param description 描述
*/
@GetMapping(value = "/getPay")
public String getPay(String orderSn,int total , String description) {
PayUtil payUtil = new PayUtil();
try {
return payUtil.requestwxChatPay(orderSn, total, description, "oYgFI91D00GpCwccdnKDR4KNxI4k");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 支付回调
@PostMapping(value = "/returnNotify")
public Map returnNotify(@RequestBody JSONObject jsonObject) {
// v3 私钥
String key = "xxxxx";
String json = jsonObject.toString();
String associated_data = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.associated_data");
String ciphertext = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.ciphertext");
String nonce = (String) JSONUtil.getByPath(JSONUtil.parse(json), "resource.nonce");
try {
String decryptData = new AesUtil(key.getBytes(StandardCharsets.UTF_8)).decryptToString(associated_data.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
System.out.println("decryptData = " + decryptData);
//TODO 业务校验
} catch (Exception e) {
e.printStackTrace();
}
HashMap<String, String> stringStringHashMap = new HashMap<>();
stringStringHashMap.put("code","200");
stringStringHashMap.put("message","返回成功");
// 返回这个说明应答成功
return stringStringHashMap;
}
}