利用微信官方提供的SDK wechatpay-apache-httpclient 实现。以微信小程序支付为例,其他支付也是一样的,就是参数和接口地址不同。
首先要在微信商户平台设置好APIv3密钥和微信商户证书,查看商户号和商户证书的序列号,并准备好创建商户证书时生成的apiclient_key.pem文件,这个文件就是商户证书私钥。
创建微信配置文件(也可以配置在yml中通过@Value获取):
public class WechatConstants {
//微信支付商户号
public static final String WECHAT_MCH_ID = "";
//微信商户平台v2密钥
public static final String WECHAT_MCH_SECRET_V2 = "";
//微信商户平台v3密钥
public static final String WECHAT_MCH_SECRET_V3 = "";
//微信商户平台商户API证书序列号
public static final String WECHAT_MCH_SERIAL_NUM = "";
//微信商户平台证书私钥 即证书中的apiclient_key.pem文件中的内容 可以直接写在这里 也可以用流读取文件
public static final String WECHAT_MCH_PRIVATE_KEY = "";
//微信小程序appid
public static final String WECHAT_MP_APPID = "";
//微信小程序密钥
public static final String WECHAT_MP_SECRET = "";
}
创建微信地址配置文件:
/**
* 微信相关接口请求地址
*/
public class WechatUrlConstants {
//小程序code获取openid
public final static String CODE_2_SESSION = "https://api.weixin.qq.com/sns/jscode2session";
//微信支付v3 jsapi下单
public final static String PAY_V3_JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
//微信支付v3 申请退款
public final static String PAY_V3_REFUND = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
//微信支付v3 通过商户订单号查询订单
public final static String PAY_V3_QUERY_OUT = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s?mchid=%s";
//微信支付v3 查询单笔退款
public final static String PAY_V3_QUERY_REFUND = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/%s";
//微信支付v2 支付通知接口地址
public final static String PAY_V2_NOTIFY = "https://xxx.com/api/wechatPay/wechatPayNotify";
//微信支付v2 退款通知接口地址
public final static String PAY_V2_REFUND_NOTIFY = "https://xxx.com/api/wechatPay/wechatRefundNotify";
//微信支付v3 支付通知接口地址
public final static String PAY_V3_NOTIFY = "https://xxx.com/api/wechatPay/v3/wechatPayNotify";
//微信支付v3 退款通知接口地址
public final static String PAY_V3_REFUND_NOTIFY = "https://xxx.com/api/wechatPay/v3/wechatRefundNotify";
}
其中最后四个通知接口地址,是自己服务器的接口地址,必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http。
创建微信支付v3工具类:
import com.alibaba.fastjson.JSONObject;
import com.lzn.wechatpaydemo.common.constant.WechatConstants;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.*;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.commons.lang3.StringUtils;
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.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.Map;
/**
* 微信V3支付工具类
*/
@Component
public class WechatPayV3Utils {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
//商户证书私钥
private PrivateKey merchantPrivateKey;
//证书
private Verifier verifier;
//请求客户端
private CloseableHttpClient httpClient;
/**
* 获取商户证书私钥
*/
private void setMerchantPrivateKey() throws Exception {
ClassPathResource classPathResource = new ClassPathResource("cer/apiclient_key.pem");
InputStream certStream = classPathResource.getInputStream();
//读取文件形式加载商户私钥
merchantPrivateKey = PemUtil.loadPrivateKey(certStream);
//直接把商户私钥以字符串形式配置
//merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(WechatConstants.WECHAT_MCH_PRIVATE_KEY.getBytes(StandardCharsets.UTF_8)));
}
/**
* 获取微信证书
*
* @throws Exception
*/
private void setVerifier() throws Exception {
if (merchantPrivateKey == null) {
setMerchantPrivateKey();
}
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(WechatConstants.WECHAT_MCH_ID,
new WechatPay2Credentials(WechatConstants.WECHAT_MCH_ID, new PrivateKeySigner(WechatConstants.WECHAT_MCH_SERIAL_NUM, merchantPrivateKey)),
WechatConstants.WECHAT_MCH_SECRET_V3.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(WechatConstants.WECHAT_MCH_ID);
}
/**
* 创建请求客户端
*
* @throws Exception
*/
private void setHttpClient() throws Exception {
if (verifier == null) {
setVerifier();
}
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(WechatConstants.WECHAT_MCH_ID, WechatConstants.WECHAT_MCH_SERIAL_NUM, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
httpClient = builder.build();
}
/**
* 发送POST请求
*
* @param url 请求地址
* @param params json参数
* @return
*/
public JSONObject sendPost(String url, JSONObject params) {
try {
if (httpClient == null) {
setHttpClient();
}
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
httpPost.setEntity(new StringEntity(params.toJSONString(), StandardCharsets.UTF_8));
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
logger.info("微信返回的内容:" + bodyAsString);
if (StringUtils.isEmpty(bodyAsString)) {
return null;
}
return JSONObject.parseObject(bodyAsString);
} catch (Exception e) {
logger.error("微信支付V3请求失败");
e.printStackTrace();
return null;
}
}
/**
* 发送get请求
*
* @param url 请求地址 参数直接在地址上拼接
* @return
*/
public JSONObject sendGet(String url) {
try {
if (httpClient == null) {
setHttpClient();
}
URIBuilder uriBuilder = new URIBuilder(url);
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response = httpClient.execute(httpGet);
String bodyAsString = EntityUtils.toString(response.getEntity());
logger.info("微信返回的内容:" + bodyAsString);
if (StringUtils.isEmpty(bodyAsString)) {
return null;
}
return JSONObject.parseObject(bodyAsString);
} catch (Exception e) {
logger.error("微信支付V3请求失败");
e.printStackTrace();
return null;
}
}
/**
* 回调通知验签与解密
*
* @param request
* @return
*/
public JSONObject getCallbackData(HttpServletRequest request) {
try {
if (verifier == null) {
setVerifier();
}
String wechatPaySerial = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_SERIAL);
String nonce = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_NONCE);
String timestamp = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_TIMESTAMP);
String signature = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE);
String body;
BufferedReader reader = request.getReader();
String line ;
StringBuilder inputString = new StringBuilder();
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
body = inputString.toString();
reader.close();
// 构建request,传入必要参数
NotificationRequest res = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial)
.withNonce(nonce)
.withTimestamp(timestamp)
.withSignature(signature)
.withBody(body)
.build();
NotificationHandler handler = new NotificationHandler(verifier, WechatConstants.WECHAT_MCH_SECRET_V3.getBytes(StandardCharsets.UTF_8));
// 验签和解析请求体
Notification notification = handler.parse(res);
logger.info("回调通知数据:" + notification.toString());
// 解析开数据
String decryptData = notification.getDecryptData();
logger.info("回调解析数据:" + decryptData);
if (StringUtils.isEmpty(decryptData)) {
return null;
}
return JSONObject.parseObject(decryptData);
} catch (Exception e) {
logger.error("微信支付V3回调失败");
e.printStackTrace();
return null;
}
}
/**
* 微信支付v3签名 RSA签名
*
* @param message 要签名的字符串
* @return
*/
public String signRSA(String message) {
try {
if (merchantPrivateKey == null) {
setMerchantPrivateKey();
}
Signer signer = new PrivateKeySigner(WechatConstants.WECHAT_MCH_SERIAL_NUM, merchantPrivateKey);
Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));
return signature.getSign();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
}
这里我是通过文件流读取的商户证书私钥,因此需要把apiclient_key.pem文件放到resource目录下的cer目录中(其他目录也可以,需要自行对应修改路径)
controller接口调用方法:
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.lzn.wechatpaydemo.common.constant.WechatConstants;
import com.lzn.wechatpaydemo.common.constant.WechatUrlConstants;
import com.lzn.wechatpaydemo.common.util.WechatPayV3Utils;
import com.lzn.wechatpaydemo.project.web.BaseController;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 微信支付相关接口demo v3版本
* 文档地址 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
*/
@RestController
@RequestMapping("/api/wechatPay/v3")
public class WechatPayV3ApiController extends BaseController {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
public WechatPayV3Utils wechatPayV3Utils;
/**
* 发起微信小程序支付
*
* @param code 用于换取openid 正式使用时openid可以直接从用户信息中获取 不需要在此接口中获取
* @return 小程序支付所需参数
*/
@PostMapping("/wechatPay")
public Map<String, Object> wechatPay() {
//@TODO demo中先写死的一些参数
Long userId = 1L; //先写死一个用户id
String openid = "xxx"; //先写死一个openid
BigDecimal amount = new BigDecimal("0.01"); //先写死一个金额 单位:元
String content = "支付demo-买牛订金"; //先写死一个商品描述
String attach = "我是附加数据"; //先写死一个附加数据 这是可选的 可以用来判断支付内容做支付成功后的处理
//@TODO 在自己的数据库中创建订单数据 待支付状态
String out_trade_no = createOrderNo("DJ", userId); //创建商户订单号
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE) + 15);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
JSONObject params = new JSONObject();
params.put("appid", WechatConstants.WECHAT_MP_APPID); //小程序appid
params.put("mchid", WechatConstants.WECHAT_MCH_ID); //商户号
params.put("description", content); //商品描述
params.put("out_trade_no", out_trade_no); //商户订单号
params.put("time_expire", sdf.format(calendar.getTime())); //交易结束时间 选填 时间到了之后将不能再支付 遵循rfc3339标准格式
params.put("attach", attach); //附加数据 选填 在查询API和支付通知中原样返回 可作为自定义参数使用
params.put("notify_url", WechatUrlConstants.PAY_V3_NOTIFY); //支付结果异步通知接口
JSONObject amount_json = new JSONObject();
amount_json.put("total", Integer.parseInt(amount_fee(amount))); //支付金额 单位:分
params.put("amount", amount_json); //订单金额信息
JSONObject payer = new JSONObject();
payer.put("openid", openid); //用户在小程序侧的openid
params.put("payer", payer); //支付者信息
JSONObject res = wechatPayV3Utils.sendPost(WechatUrlConstants.PAY_V3_JSAPI, params); //发起请求
if (res == null || StringUtils.isEmpty(res.getString("prepay_id"))) {
//@TODO 支付发起失败可以将订单数据回滚
return error("支付发起失败");
}
StringBuilder sb = new StringBuilder();
//返回给小程序拉起微信支付的参数
Map<String, String> result = new HashMap<>();
result.put("appId", WechatConstants.WECHAT_MP_APPID); //小程序appid
sb.append(result.get("appId")).append("\n");
result.put("timeStamp", (new Date().getTime() / 1000) + ""); //时间戳
sb.append(result.get("timeStamp")).append("\n");
result.put("nonceStr", RandomStringUtils.randomAlphanumeric(32)); //32位随机字符串
sb.append(result.get("nonceStr")).append("\n");
result.put("package", "prepay_id=" + res.getString("prepay_id")); //预支付id 格式为 prepay_id=xxx
sb.append(result.get("package")).append("\n");
result.put("paySign", wechatPayV3Utils.signRSA(sb.toString())); //签名
result.put("signType", "RSA"); //加密方式 固定RSA
result.put("out_trade_no", out_trade_no); //商户订单号 此参数不是小程序拉起支付所需的参数 因此不参与签名
return success(result);
}
/**
* 支付成功后查询订单状态
*
* @param out_trade_no 商户订单号
* @return null代表查询失败 SUCCESS-成功 USERPAYING(用户支付中)和ACCEPT(已接收,等待扣款)为中间态 需要重新查询 其他为支付失败
*/
@PostMapping("/checkPay")
public Map<String, Object> checkPay(String out_trade_no) {
//@TODO 先查询自己数据库中的订单状态是否支付成功 若成功 则直接返回SUCCESS 若未成功 则需调用查询支付接口
String status = orderQueryByOutTradeNo(out_trade_no);
return success("请求成功", status);
}
/**
* 申请微信退款
* 交易时间超过一年的订单无法提交退款
* 微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。
* 申请退款总金额不能超过订单金额。
* 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
* 每个支付订单的部分退款次数不能超过50次
* 同一笔订单多次退款的请求需相隔1分钟
*
* @param transaction_id 微信支付订单号
* @return
*/
@PostMapping("/wechatRefund")
public Map<String, Object> wechatRefund(String transaction_id) {
//@TODO demo中先写死的一些参数
Long userId = 1L; //先写死一个用户id
BigDecimal amount = new BigDecimal("0.01"); //先写死一个退款金额 单位:元
String reason = "支付demo-退还订金"; //先写死一个退款原因 这是可选的
//@TODO 先查询订单是否可退款 将订单修改为退款中等业务处理
String out_refund_no = createOrderNo("TK", userId); //创建商户退款单号
JSONObject params = new JSONObject();
params.put("transaction_id", transaction_id); //微信支付订单号 也可以传out_trade_no 即发起支付时创建的商户订单号 二选一 transaction_id>out_trade_no
params.put("out_refund_no", out_refund_no); //商户退款单号
params.put("reason", reason); //退款原因 选填 若填写 会在退款消息中显示给用户
params.put("notify_url", WechatUrlConstants.PAY_V3_REFUND_NOTIFY); //退款结果异步通知接口
JSONObject amountJson = new JSONObject();
amountJson.put("refund", Integer.parseInt(amount_fee(amount))); //退款金额 单位:分
amountJson.put("total", Integer.parseInt(amount_fee(amount))); //原订单金额 单位:分
amountJson.put("currency", "CNY"); //退款币种
params.put("amount", amountJson); //订单金额信息
JSONObject res = wechatPayV3Utils.sendPost(WechatUrlConstants.PAY_V3_REFUND, params); //发起请求
if (res == null) {
//@TODO 退款失败时回滚订单状态
return error("退款申请失败");
}
logger.info("微信退款单号:" + res.getString("refund_id"));
//@TODO 可以在此更新订单微信退款单号等信息
return success();
}
/**
* 微信支付异步通知
*
* @param request
* @return
*/
@PostMapping("/wechatPayNotify")
public Map<String, String> wechatPayNotify(HttpServletRequest request) {
Map<String, String> result = new HashMap<>(2);
JSONObject res = wechatPayV3Utils.getCallbackData(request);
if (res == null) {
result.put("code", "FAIL");
result.put("message", "失败");
return result;
}
logger.info("最终拿到的微信支付通知数据:" + res);
//@TODO 处理支付成功后的业务 例如 将订单状态修改为已支付 具体参数键值可参考文档 注意!!! 微信可能会多次发送重复的通知 因此要判断业务是否已经处理过了 避免重复处理
result.put("code", "SUCCESS");
result.put("message", "OK");
return result;
}
/**
* 微信退款异步通知
*
* @param request
* @return
*/
@PostMapping("/wechatRefundNotify")
public Map<String, String> wechatRefundNotify(HttpServletRequest request) {
Map<String, String> result = new HashMap<>(2);
JSONObject res = wechatPayV3Utils.getCallbackData(request);
if (res == null) {
result.put("code", "FAIL");
result.put("message", "失败");
return result;
}
logger.info("最终拿到的微信退款通知数据:" + res);
//@TODO 处理退款成功后的业务 例如 将订单状态修改位已退款 具体参数键值可参考文档 注意!!! 微信可能会多次发送重复的通知 因此要判断业务是否已经处理过了 避免重复处理
result.put("code", "SUCCESS");
result.put("message", "OK");
return result;
}
/**
* 通过商户订单号查询订单在微信侧支付状态
*
* @param out_trade_no 发起支付时创建的商户订单号
* @return null代表查询失败 SUCCESS-成功 USERPAYING和ACCEPT为中间态 其他为支付失败
*/
public String orderQueryByOutTradeNo(String out_trade_no) {
JSONObject res = wechatPayV3Utils.sendGet(String.format(WechatUrlConstants.PAY_V3_QUERY_OUT, out_trade_no, WechatConstants.WECHAT_MCH_ID));
return res == null ? null : res.getString("trade_state");
}
/**
* 查询单笔退款
*
* @param out_refund_no 申请退款时创建的商户退款单号
* @return
*/
public JSONObject refundQuery(String out_refund_no) {
return wechatPayV3Utils.sendGet(String.format(WechatUrlConstants.PAY_V3_QUERY_REFUND, out_refund_no));
}
/**
* 创建商户订单号
* 要求 32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一
* 组成 两位前缀 + 17位时间戳 + 9位id补零 + 4位随机数 合计32位
*
* @param head 例如 商品-SP 订金-DJ 退款-TK 等等
* @param id 用户id
* @return
*/
public String createOrderNo(String head, Long id) {
StringBuilder uid = new StringBuilder(id.toString());
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
int length = uid.length();
for (int i = 0; i < 9 - length; i++) {
uid.insert(0, "0");
}
return head + sdf.format(date) + uid + (int) ((Math.random() * 9 + 1) * 1000);
}
/**
* 金额元转分字符串
*
* @param cny 元
* @return
*/
public String amount_fee(BigDecimal cny) {
BigDecimal b2 = new BigDecimal(100);
return cny.multiply(b2).setScale(0, RoundingMode.DOWN).toString();
}
}
demo中很多参数都是写死的,具体使用请根据自己的业务情况调整。