微信支付官網(wǎng)api:https://pay.weixin.qq.com/docs/merchant/products/jsapi-payment/introduction.html
微信支付在Java中的應(yīng)用已經(jīng)非常流行,今天讓我們一起來寫一篇實(shí)戰(zhàn)篇的微信支付;
1.開發(fā)參數(shù)的準(zhǔn)備:https://pay.weixin.qq.com/docs/merchant/development/development-preparation/download-configure-merchant-certificates.html;
2.獲取到商戶證書以后,把壓縮包里的apiclient_key 文件放在項(xiàng)目的根目錄下,方便使用和讀取,如下:

image.png

image.png
3.創(chuàng)建加載商戶私鑰、加載平臺證書、初始化httpClient的通用方法。
這里我自己寫了一個(gè)配置文件
config配置文件會(huì)在項(xiàng)目啟動(dòng)時(shí)被加載(使用簽名驗(yàn)證器在每次使用微信支付的接口時(shí)會(huì)自動(dòng)驗(yàn)證身份)
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
#微信服務(wù)器地
wxpay.domain=替換為你自己的微信服務(wù)器地
# 接收結(jié)果通知地址
wxpay.notify-domain=替換為你自己的接收結(jié)果通知地址
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節(jié)點(diǎn)
@Data //使用set方法將wxpay節(jié)點(diǎn)中的值填充到當(dāng)前類的屬性中
@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;
// 微信服務(wù)器地址
private String domain;
// 接收結(jié)果通知地址
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);
}
}
/**
* 獲取簽名驗(yàn)證器
*
* @return
*/
@Bean
public verifier getVerifier() throws HttpCodeException, GeneralSecurityException, IOException, NotFoundException {
log.info("獲取簽名驗(yàn)證器");
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私鑰簽名對象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份認(rèn)證對象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 獲取證書管理器實(shí)例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向證書管理器增加需要自動(dòng)更新平臺證書的商戶信息
certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
//若有多個(gè)商戶號,可繼續(xù)調(diào)用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設(shè)置各種參數(shù),來配置你的HttpClient
// 通過WechatPayHttpClientBuilder構(gòu)造的HttpClient,會(huì)自動(dòng)的處理簽名和驗(yàn)簽,并進(jìn)行證書自動(dòng)更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 獲取HttpClient,無需進(jìn)行應(yīng)答簽名驗(yàn)證,跳過驗(yàn)簽的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient() {
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于構(gòu)造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//設(shè)置商戶信息
.withMerchant(mchId, mchSerialNo, privateKey)
//無需進(jìn)行簽名驗(yàn)證、通過withValidator((response) -> true)實(shí)現(xiàn)
.withValidator((response) -> true);
// 通過WechatPayHttpClientBuilder構(gòu)造的HttpClient,會(huì)自動(dòng)的處理簽名和驗(yàn)簽,并進(jìn)行證書自動(dòng)更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
}
4.調(diào)用支付接口,發(fā)起支付(此時(shí)需要對數(shù)據(jù)進(jìn)行簽名,簽名工具類在支付方法下;具體業(yè)務(wù)邏輯和參數(shù)自己添加)
@PostMapping("/pay")
public HashMap<String, Object> outpatientPlaceOrderNew() throws Exception {
// 開始支付
long timestamp = new Date().getTime();
log.info("訂單開始支付,調(diào)用統(tǒng)一下單API");
//調(diào)用統(tǒng)一下單API
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
// 請求body參數(shù)
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);
//將參數(shù)轉(zhuǎn)換成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("請求參數(shù) ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成簽名并執(zhí)行請求
CloseableHttpResponse response = dcWxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//響應(yīng)體
int statusCode = response.getStatusLine().getStatusCode();//響應(yīng)狀態(tài)碼
log.info("響應(yīng)結(jié)果 = " + bodyAsString);
if (statusCode == 200) {
log.info("成功, 返回結(jié)果 = " + bodyAsString);
} else if (statusCode == 204) { //處理成功,無返回Body
log.info("成功");
} else {
log.info("失敗");
}
//響應(yīng)結(jié)果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
String prepayId = resultMap.get("prepay_id");
//組裝前端拉起支付需要的參數(shù)
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++) {
//獲取指定長度的字符串中任意一個(gè)字符的索引值int number=random1.nextInt(str.length());
//根據(jù)索引值獲取對應(yīng)的字符
char charAt = str.charAt(i);
sb.append(charAt);
}
return sb.toString();
}
}
6.微信支付通知(此處沒有做簽名驗(yàn)證)
@ApiOperation("支付通知")
@PostMapping("/notice")
public Map wxJsApiCallback (@RequestBody Map body, HttpServletRequest request){
log.info("進(jìn)入微信支付通知");
Map<String, Object> result = new HashMap();
//1:獲取微信支付回調(diào)的獲取簽名信息
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
ObjectMapper objectMapper = new ObjectMapper();
try {
// 2: 開始解析報(bào)文體
String data = objectMapper.writeValueAsString(body);
String message = timestamp + "\n" + nonce + "\n" + data + "\n";
//3:獲取應(yīng)答簽名
String sign = request.getHeader("Wechatpay-Signature");
//4:獲取平臺對應(yīng)的證書
String serialNo = request.getHeader("Wechatpay-Serial");
Map<String, String> resource = (Map) body.get("resource");
// 5:回調(diào)報(bào)文解密
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: 支付狀態(tài)的判斷 如果是success就代表支付成功
if ("SUCCESS".equals(jsonData.get("trade_state"))) {
// 8:獲取支付的交易單號,流水號,和附屬參數(shù)
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"); //實(shí)際支付金額
// 成功處理,加自己的的業(yè)務(wù)
} else {
String out_trade_no = jsonData.get("out_trade_no").toString();
// 失敗處理,加自己的的業(yè)務(wù)
}
result.put("code", "SUCCESS");
result.put("message", "成功");
} catch (Exception e) {
result.put("code", "FAIL");
result.put("message", "系統(tǒng)錯(cuò)誤");
e.printStackTrace();
}
return result;
}
7.退款
@Resource
private WxPayConfig wxPayConfig;
@PostMapping("/refund")
public AjaxResult refund1(@RequestBody Order Order) throws Exception {
AjaxResult ajaxResult = new AjaxResult();
//數(shù)據(jù)庫自行計(jì)算金額
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("調(diào)用退款A(yù)PI");
//調(diào)用統(tǒng)一下單API
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/refund/domestic/refunds");
// 請求Body參數(shù)
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);
// 將參數(shù)轉(zhuǎn)換成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("請求參數(shù) ===> {} " + jsonParams);
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成簽名并執(zhí)行請求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
//響應(yīng)體
String bodyAsString = EntityUtils.toString(response.getEntity());
// 響應(yīng)狀態(tài)碼
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功 退款返回結(jié)果 = " + bodyAsString);
} else if (statusCode == 204) {
// 處理成功,無返回body
log.info("成功");
} else {
throw new RuntimeException(" 退款異常,響應(yīng)碼 = " + statusCode + ", 退款返回結(jié)果 = " + bodyAsString);
}
ajaxResult = AjaxResult.success();
} finally {
response.close();
}
return ajaxResult;
}