微信支付官网api:https://pay.weixin.qq.com/docs/merchant/products/jsapi-payment/introduction.html
微信支付在Java中的应用已经非常流行,今天让我们一起来写一篇实战篇的微信支付;
1.开发参数的准备:https://pay.weixin.qq.com/docs/merchant/development/development-preparation/download-configure-merchant-certificates.html;
2.获取到商户证书以后,把压缩包里的apiclient_key 文件放在项目的根目录下,方便使用和读取,如下:
image.png
image.png
3.创建加载商户私钥、加载平台证书、初始化httpClient的通用方法。
这里我自己写了一个配置文件
config配置文件会在项目启动时被加载(使用签名验证器在每次使用微信支付的接口时会自动验证身份)
3.1wxpay.propertie配置文件
#商户号
wxpay.mch-id=替换为你自己的商户号
#API
wxpay.mch-serial-no=替换为你自己的商户号序列号
#商户私钥文件,放在工程目录下
wxpay.private-key-path=替换为你自己的apiclient_key.pem
#APIv3密钥
wxpay.api-v3-key=替换为你自己的APIv3密钥
#APPID
wxpay.appid=替换为你自己的appid
#微信服务器地
wxpay.domain=替换为你自己的微信服务器地
# 接收结果通知地址
wxpay.notify-domain=替换为你自己的接收结果通知地址
3.2config配置文件
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.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import javax.annotation.Resource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix = "wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
@Resource
private WxPayConfig wxPayConfig;
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
/**
* 获取商户的私钥文件
*
* @param filename
* @return
*/
public PrivateKey getPrivateKey(String filename) {
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
/**
* 获取签名验证器
*
* @return
*/
@Bean
public verifier getVerifier() throws HttpCodeException, GeneralSecurityException, IOException, NotFoundException {
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
//若有多个商户号,可继续调用putMerchant添加商户信息
certificatesManager.putMerchant(wxPayConfig.getMchId(), wechatPay2CredentialsDc, wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(mchId);
return verifier ;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClientverifier verifier){
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier ));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient() {
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
}
4.调用支付接口,发起支付(此时需要对数据进行签名,签名工具类在支付方法下;具体业务逻辑和参数自己添加)
@PostMapping("/pay")
public HashMap<String, Object> outpatientPlaceOrderNew() throws Exception {
// 开始支付
long timestamp = new Date().getTime();
log.info("订单开始支付,调用统一下单API");
//调用统一下单API
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", "支付");
paramsMap.put("out_trade_no", wjOrder.getId().toString());
paramsMap.put("notify_url", "https://www.wjgzzdyy.mil.cn" + "/api/wxPay/notice");
Map amountMap = new HashMap();
amountMap.put("total", wjOrder.getAmount().multiply(new BigDecimal(100)).intValue());
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
Map payer = new HashMap();
payer.put("openid", wjOrder.getOpenId());
paramsMap.put("payer", payer);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = dcWxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
log.info("响应结果 = " + bodyAsString);
if (statusCode == 200) {
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("失败");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
String prepayId = resultMap.get("prepay_id");
//组装前端拉起支付需要的参数
String nonceStr = signUtil.makeString();
String signType = "RSA";
String packageId = "prepay_id=" + prepayId;
String source = appid + "\n";
source += timestamp + "\n";
source += nonceStr + "\n";
source += packageId + "\n";
//签名
String paySign = signUtil.signBySHA256WithRSAOrder(source, "UTF-8");
HashMap<String, Object> res = new HashMap<>();
res.put("timestamp", timestamp);
res.put("nonceStr", nonceStr);
res.put("package", packageId);
res.put("signType", signType);
res.put("paySign", paySign);
} finally {
response.close();
}
return res;
}
5.签名工具类SignUtil
import com.colorful.hospital.config.WxPayConfig;
import com.colorful.hospital.config.WxPayDcConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.security.PrivateKey;
import java.security.Signature;
@Component
public class SignUtil {
@Autowired
WxPayConfig wxPayConfig;
/**
* SHA256withRSA签名
* @author xpl
* @param content
* @param charset
* @return
*/
public String signBySHA256WithRSA(String content, String charset){
try {
PrivateKey privateKey = wxPayConfig.getPrivateKey("apiclient_key.pem");
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(content.getBytes(charset));
return org.apache.commons.codec.binary.Base64.encodeBase64String(signature.sign());
} catch (Exception e) {
//签名失败
return null;
}
}
public String makeString() {
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random1 = new Random();
//指定字符串长度,拼接字符并toString
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 16; i++) {
//获取指定长度的字符串中任意一个字符的索引值int number=random1.nextInt(str.length());
//根据索引值获取对应的字符
char charAt = str.charAt(i);
sb.append(charAt);
}
return sb.toString();
}
}
6.微信支付通知(此处没有做签名验证)
@ApiOperation("支付通知")
@PostMapping("/notice")
public Map wxJsApiCallback (@RequestBody Map body, HttpServletRequest request){
log.info("进入微信支付通知");
Map<String, Object> result = new HashMap();
//1:获取微信支付回调的获取签名信息
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
ObjectMapper objectMapper = new ObjectMapper();
try {
// 2: 开始解析报文体
String data = objectMapper.writeValueAsString(body);
String message = timestamp + "\n" + nonce + "\n" + data + "\n";
//3:获取应答签名
String sign = request.getHeader("Wechatpay-Signature");
//4:获取平台对应的证书
String serialNo = request.getHeader("Wechatpay-Serial");
Map<String, String> resource = (Map) body.get("resource");
// 5:回调报文解密
AesUtil aesUtil = new AesUtil(privateApiV3Key.getBytes());
//解密后json字符串
String decryptToString = aesUtil.decryptToString(
resource.get("associated_data").getBytes(),
resource.get("nonce").getBytes(),
resource.get("ciphertext"));
//6:获取微信支付返回的信息
com.alibaba.fastjson.JSONObject jsonData = com.alibaba.fastjson.JSONObject.parseObject(decryptToString);
log.info("wxJsApiCallback responseJson:" + jsonData.toJSONString());
//7: 支付状态的判断 如果是success就代表支付成功
if ("SUCCESS".equals(jsonData.get("trade_state"))) {
// 8:获取支付的交易单号,流水号,和附属参数
String out_trade_no = jsonData.get("out_trade_no").toString();
String transaction_id = jsonData.get("transaction_id").toString();
String success_time = jsonData.get("success_time").toString();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
Date parse = df.parse(success_time);
com.alibaba.fastjson.JSONObject amount = jsonData.getJSONObject("amount");// 订单金额信息
int payMoney = amount.getIntValue("payer_total"); //实际支付金额
// 成功处理,加自己的的业务
} else {
String out_trade_no = jsonData.get("out_trade_no").toString();
// 失败处理,加自己的的业务
}
result.put("code", "SUCCESS");
result.put("message", "成功");
} catch (Exception e) {
result.put("code", "FAIL");
result.put("message", "系统错误");
e.printStackTrace();
}
return result;
}
7.退款
@Resource
private WxPayConfig wxPayConfig;
@PostMapping("/refund")
public AjaxResult refund1(@RequestBody Order Order) throws Exception {
AjaxResult ajaxResult = new AjaxResult();
//数据库自行计算金额
BigDecimal refundAmount = new BigDecimal(0);
//商户退款单号
String outRefundNo = "R" + SnowFlake.nextId().toString();
Long orderId = order.getId();
// 退款金额
int refund = refundAmount.multiply(new BigDecimal(100)).intValue();
log.info("调用退款API");
//调用统一下单API
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/refund/domestic/refunds");
// 请求Body参数
Gson gson = new Gson();
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put("out_trade_no", orderId.toString());
paramsMap.put("out_refund_no", outRefundNo);
Map amountMap = new HashMap<>();
amountMap.put("total", order.getAmount().multiply(new BigDecimal(100)).intValue());
amountMap.put("refund", refund);
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
// 将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {} " + jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
// 响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功 退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
// 处理成功,无返回body
log.info("成功");
} else {
throw new RuntimeException(" 退款异常,响应码 = " + statusCode + ", 退款返回结果 = " + bodyAsString);
}
ajaxResult = AjaxResult.success();
} finally {
response.close();
}
return ajaxResult;
}