前言:
在一次项目开发中我的业务模块涉及微信扫码支付和支付宝扫码支付。在查阅现有文档后发现,大部分文章还停留在V2支付,部分文章介绍了V3支付但是并不能走通,于是我便阅读了官方文档,形成了这篇总结性文章,希望对读者有所帮助。
官方开发文档
官方SDK开发工具包
API字典
相信此刻你一定已经注册并登陆过 微信开放平台 了,在此不多赘述。需在平台中获取的开发所需信息如下:
MERCHANT_ID
:商户号
MERCHANT_SERIAL_NUMBER
:商户API证书的证书序列号
MERCHANT_PRIVATEKEY
:商户API私钥
AIV3KEY
:API v3密钥
APP_ID
:小程序的APPID
注意:想要获取以上信息,必须是商户主账号才可以(即便是管理员,部分操作也无法完成)。
下面我们来逐个获取以上参数
产品中心
→AppID账号管理
→+关联AppID
,此时即可看到商户号该步骤操作需要商户主账号才可完成。
账户中心
→API安全
,此时就进入了API安全页,此页面可设置一个证书两个密钥,API证书是根据网站提供的工具生成的(具体下载操作可参考网站中给出的指引)证书使用说明.txt
进行(提示一下:Windows用户直接双击apiclient_cert.p12
文件即可上传证书)。管理证书
查看证书序列号。在获取证书序列号的第二步操作中的apiclient_key.pem
文件即为所需的商户API私钥。
注意:此处有一个小坑,在粘贴复制密钥时,除了保留-----BEGIN PRIVATE KEY-----
和-----END PRIVATE KEY-----
的换行外,中间内容在粘贴时一定要去除复制时产生的换行符(推荐复制内容后去网上的去换行工具
中去除下换行符)
设置
来设置APIv3密钥即可。32个字符,支持数字/大小写字母
,可以在随机生成字符串的线上工具中进行生成公众号和小程序的AppID获取方式雷同,具体参考如图所示指引
对于v3支付来说,此时可谓万事俱备只欠东风,下面来配置代码中的工具类和第三方请求。
此处引入的是v3支付
<dependency>
<groupId>com.github.wechatpay-apiv3groupId>
<artifactId>wechatpay-apache-httpclientartifactId>
<version>0.3.0version>
dependency>
此工具类仅供参考,配置信息也可根据个人喜好放在yml文件中(为方便阅读此处放在类中作为常量)。
此处的工具类有以下作用:
public class WeChartUtil {
public static final String MERCHANT_ID = "商户id";
public static final String MERCHANT_SERIAL_NUMBER = "证书序列号";
public static final String MERCHANT_PRIVATEKEY = "-----BEGIN PRIVATE KEY-----\n" +
"你的密钥内容(记得去换行)" +
"\n" + "-----END PRIVATE KEY-----";
public static final String AIV3KEY = "APIv3密钥";
public static final String NOTIFY_URL = "回调地址";
public static final String APP_ID = "小程序的AppID";
/**
* @Author Spence_Dou
* @Description 生成订单号
* @Date 11:35 2021/12/23
* @return void
*/
public static String orderNo(){
return UUID.randomUUID().toString()
.replaceAll("-", "")
.substring(0, 32);
}
/**
* @Author Spence_Dou
* @Description 生成订单过期时间
* @Date 11:41 2021/12/23
* @return java.time.LocalDateTime
*/
public static String timeExpire(){
//过期时间:5分钟后
long time = 5*60*1000;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
Date now = new Date();
//30分钟后的时间
Date afterDate = new Date(now.getTime() + time);
return simpleDateFormat.format(afterDate);
}
/**
* @Author Spence_Dou
* @Description 分转元 保留小数点后两位
* @Date 11:38 2021/12/23
* @Param num 转换金额
* @return java.lang.String
*/
public static BigDecimal transition(Integer num) {
BigDecimal bigDecimal1 = new BigDecimal(num + "");
BigDecimal bigDecimal2 = new BigDecimal("100.00");
return bigDecimal1.divide(bigDecimal2).setScale(2);
}
/**
* @Author Spence_Dou
* @Description 微信支付回调签名验证
* @Date 16:30 2021/12/23
* @Param serial 请求头序列号
* @Param message 请求报文
* @Param signature 签名
* @return boolean
*/
public static boolean signVerify(String serial, String message, String signature) {
try {
PrivateKey key = PemUtil.loadPrivateKey(new ByteArrayInputStream(MERCHANT_PRIVATEKEY.getBytes(StandardCharsets.UTF_8)));
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
new WechatPay2Credentials(MERCHANT_ID, new PrivateKeySigner(MERCHANT_SERIAL_NUMBER, key)),
AIV3KEY.getBytes(StandardCharsets.UTF_8));
return verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* @Author Spence_Dou
* @Description 解密密文
* @Date 17:04 2021/12/23
* @Param body 请求数据
* @return java.lang.String
*/
public static String decryptOrder(String body){
try {
AesUtil util = new AesUtil(AIV3KEY.getBytes(StandardCharsets.UTF_8));
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = objectMapper.readTree(body);
JsonNode resource = node.get("resource");
String ciphertext = resource.get("ciphertext").textValue();
String associatedData = resource.get("associated_data").textValue();
String nonce = resource.get("nonce").textValue();
return util.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
}
return null;
}
/**
* @Author Spence_Dou
* @Description 关闭订单
* @Date 17:34 2021/12/23
* @Param outTradeNo 订单号
* @return void
*/
public static void closeOrder(String outTradeNo) {
PrivateKey key = PemUtil.loadPrivateKey(new ByteArrayInputStream(MERCHANT_PRIVATEKEY.getBytes(StandardCharsets.UTF_8)));
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
new WechatPay2Credentials(MERCHANT_ID, new PrivateKeySigner(MERCHANT_SERIAL_NUMBER, key)),
AIV3KEY.getBytes(StandardCharsets.UTF_8));
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(MERCHANT_ID, MERCHANT_SERIAL_NUMBER, key)
.withValidator(new WechatPay2Validator(verifier));
CloseableHttpClient httpClient = builder.build();
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+outTradeNo+"/close");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid",MERCHANT_ID);
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
System.out.println(bodyAsString);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Native支付
为例 PrivateKey key = PemUtil.loadPrivateKey(new ByteArrayInputStream(WeChartUtil.MERCHANT_PRIVATEKEY.getBytes(StandardCharsets.UTF_8)));
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
new WechatPay2Credentials(WeChartUtil.MERCHANT_ID, new PrivateKeySigner(WeChartUtil.MERCHANT_SERIAL_NUMBER, key)),
WeChartUtil.AIV3KEY.getBytes(StandardCharsets.UTF_8));
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(WeChartUtil.MERCHANT_ID, WeChartUtil.MERCHANT_SERIAL_NUMBER, key)
.withValidator(new WechatPay2Validator(verifier));
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
// 生成订单号
String outTradeNo = WeChartUtil.orderNo();
// 请求参数
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid",WeChartUtil.MERCHANT_ID)
.put("appid", WeChartUtil.APP_ID)
.put("notify_url", WeChartUtil.NOTIFY_URL)
.put("out_trade_no", outTradeNo)
.put("time_expire", WeChartUtil.timeExpire())
.put("description", description); // 订单描述(前端获取)
rootNode.putObject("amount")
.put("total", 1); // 此处支付金额单位为分
try {
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
// 这里就是请求支付后返回的数据了
JSONObject details = JSONObject.parseObject(bodyAsString);
// 支付二维码
String qrCode= details.getString("code_url");
/**
* 此处可进行订单数据持久化等业务操作
* ...
*/
} catch (IOException e) {
e.printStackTrace();
}
notify_url
,具体功能介绍在此不做阐述public Map wxCallback(HttpServletRequest request) {
Map result = new HashMap();
result.put("code", "FAIL");
try {
StringBuilder signStr = new StringBuilder();
signStr.append(request.getHeader("Wechatpay-Timestamp")).append("\n");
signStr.append(request.getHeader("Wechatpay-Nonce")).append("\n");
BufferedReader br = request.getReader();
String str = null;
StringBuilder builder = new StringBuilder();
while ((str = br.readLine()) != null){
builder.append(str);
}
signStr.append(builder.toString()).append("\n");
// 验证签名
if (!WeChartUtil.signVerify(request.getHeader("Wechatpay-Serial"), signStr.toString(), request.getHeader("Wechatpay-Signature"))){
result.put("message", "sign error");
return result;
}
// 解密密文
String decryptOrder = WeChartUtil.decryptOrder(builder.toString());
// 验证订单
JSONObject details = JSONObject.parseObject(decryptOrder);
// 获取订单状态
String ciphertext = details.getString("trade_state");
if ("SUCCESS".equals(ciphertext)){
/**
* 此处可进行订单状态数据持久化等业务操作
* ...
*/
result.put("code", "SUCCESS");
result.put("message", "成功");
// 关闭订单
WeChartUtil.closeOrder(details.getString("out_trade_no"));
}
} catch (IOException e) {
e.printStackTrace();
//强制手动事务回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}