前言
最近为一个公众号h5商城接入了微信支付功能,查找资料过程中踩了很多坑,以此文章记录一下和大家分享
前期准备
公众号认证
微信支付功能需要开通企业号并进行资质认证,费用一年300,且需企业营业执照等信息,对公账户打款验证
登录微信公众平台https://mp.weixin.qq.com/,创建服务号
如果已有服务号扫码登录后点击公众号头像选择认证详情菜单
商户开通
点击公众号左侧微信支付菜单,选择右侧关联商户按钮,如果没有商户按指引申请
参数获取
公众号参数
点击左侧基本配置菜单,记录右侧的应用ID(appid)
商户参数
点击公众号左侧微信支付菜单,滑动到已关联商户号,点击查看按钮
进入商户后,选择产品中心,左侧开发配置,记录商户号(mchId)
进入商户后,选择账户中心,左侧API安全,按照指引获取APIV3密钥(apiV3Key),API证书的序列号(merchantSerialNumber)和私钥文件apiclient_key.pem
参数配置
外网映射
在微信支付本地调试时需要用到外网映射工具,这里推荐NATAPP:https://natapp.cn/(非广)
一个月带备案域名的映射隧道12元,我们需要两个,一个映射公众号菜单页面,一个映射后端接口
公众号参数
进入公众点击左侧自定义菜单,右侧点击添加菜单,输入外网映射后的菜单地址
如果你是新手,需要进行网页授权认证获取用户openid,那你还需要进行网页授权域名的设置
点左侧接口权限菜单,修改右侧的网页授权用户信息获取
进入后设置JS接口安全域名,会需要将一个txt认证文件放置到你的静态页面目录,参照指引即可
商户参数
进入商户后,选择产品中心,左侧我的产品,进入JSAPI支付
点击产品设置,在支付配置模块,添加支付授权目录(后端接口和前端网页都添加)
支付对接
参数声明
wechartpay: | |
appId: xxx | |
mchId: xxxx | |
apiV3Key: xxxx | |
merchantSerialNumber: xxxx | |
v3PayNotifyUrl: http://xxxxxx/wechatpay/pay_notify | |
v3BackNotifyUrl: http://xxxxx/wechatpay/back_notify | |
@Value("${wechartpay.appId}") | |
private String appId; | |
@Value("${wechartpay.mchId}") | |
private String mchId; | |
@Value("${wechartpay.apiV3Key}") | |
private String apiV3Key; | |
@Value("${wechartpay.merchantSerialNumber}") | |
private String merchantSerialNumber; | |
@Value("${wechartpay.v3PayNotifyUrl}") | |
private String v3PayNotifyUrl; | |
@Value("${wechartpay.v3BackNotifyUrl}") | |
private String v3BackNotifyUrl; | |
public static RSAAutoCertificateConfig config = null; | |
public static JsapiServiceExtension service = null; | |
public static RefundService backService = null; | |
private void initPayConfig() { | |
initConfig(); | |
// 构建service | |
if (service == null) { | |
service = new JsapiServiceExtension.Builder().config(config).build(); | |
} | |
} | |
private void initBackConfig() { | |
initConfig(); | |
// 构建service | |
if (backService == null) { | |
backService = new RefundService.Builder().config(config).build(); | |
} | |
} | |
private void initConfig() { | |
String filePath = getFilePath("apiclient_key.pem"); | |
if (config == null) { | |
config = new RSAAutoCertificateConfig.Builder() | |
.merchantId(mchId) | |
.privateKeyFromPath(filePath) | |
.merchantSerialNumber(merchantSerialNumber) | |
.apiV3Key(apiV3Key) | |
.build(); | |
} | |
} | |
public RSAAutoCertificateConfig getConfig() { | |
initConfig(); | |
return config; | |
} | |
public static String getFilePath(String classFilePath) { | |
String filePath = ""; | |
try { | |
String templateFilePath = "tempfiles/classpathfile/"; | |
File tempDir = new File(templateFilePath); | |
if (!tempDir.exists()) { | |
tempDir.mkdirs(); | |
} | |
String[] filePathList = classFilePath.split("/"); | |
String checkFilePath = "tempfiles/classpathfile"; | |
for (String item : filePathList) { | |
checkFilePath += "/" + item; | |
} | |
File tempFile = new File(checkFilePath); | |
if (tempFile.exists()) { | |
filePath = checkFilePath; | |
} else { | |
//解析 | |
ClassPathResource classPathResource = new ClassPathResource(classFilePath); | |
InputStream inputStream = classPathResource.getInputStream(); | |
checkFilePath = "tempfiles/classpathfile"; | |
for (int i = 0; i < filePathList.length; i++) { | |
checkFilePath += "/" + filePathList[i]; | |
if (i == filePathList.length - 1) { | |
//文件 | |
File file = new File(checkFilePath); | |
if (!file.exists()) { | |
FileUtils.copyInputStreamToFile(inputStream, file); | |
} | |
} else { | |
//目录 | |
tempDir = new File(checkFilePath); | |
if (!tempDir.exists()) { | |
tempDir.mkdirs(); | |
} | |
} | |
} | |
inputStream.close(); | |
filePath = checkFilePath; | |
} | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return filePath; | |
} |
将apiclient_key.pem私钥文件拷贝到resources文件夹根目录
Maven引用
<dependency> | |
<groupId>com.github.wechatpay-apiv3</groupId> | |
<artifactId>wechatpay-java</artifactId> | |
<version>0.2.11</version> | |
</dependency> | |
<dependency> | |
<groupId>commons-io</groupId> | |
<artifactId>commons-io</artifactId> | |
<version>2.8.0</version> | |
</dependency> |
用户授权
为了测试流程的完整性,这里简单描述下如何通过网页授权获取用户的openid,几个参数定义如下
var appid = "xxxx"; | |
var appsecret = "xxxx"; | |
redirect_uri = encodeURIComponent("http://xxxx/xxx.html"); | |
response_type = "code"; | |
scope = "snsapi_userinfo"; |
发起授权请求
function getCodeUrl() { | |
var url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + appid + "&redirect_uri=" + redirect_uri + "&response_type=" + response_type + "&scope=" + scope + "#wechat_redirect"; | |
return url | |
} |
跳转到开始授权页面,会跳转到redirect_uri这个页面,url参数携带授权code
用户同意授权
function getAuthUrl(code) { | |
var url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appid + "&secret=" + appsecret + "&code=" + code + "&grant_type=authorization_code"; | |
return url | |
} |
根据code生成正式授权url,需要用户手动点击同意,使用get方式请求该url成功后会返回openid
var url = getAuthUrl(code); | |
$.get(url, function (data, status) { | |
var result = JSON.parse(data) | |
if (!result.errcode) { | |
var openid = result.openid; | |
} | |
}); |
不使用用户授权流程也能简单的获取到用户openid进行测试,如果该用户关注了公众号,选择公众号左侧的用户管理菜单,点击用户跳转到与该用户的聊天界面,url参数中的tofakeid就是用户的openid
支付准备
根据用户的openid,订单号,订单金额,订单说明四个参数进行支付前的参数准备,会返回如下参数
公众号ID(appId)
时间戳(timeStamp)
随机串(nonceStr)
打包值(packageVal)
微信签名方式(signType)
微信签名(paySign)
这里的orderID指业务中生成的订单号,最大32位,由数字和字母组成,支付金额最终要转转换成已分为单位
public Object prepay( Map<String, Object> params)throws Exception { | |
String openId = "xxxx"; | |
String orderID = String.valueOf(params.get("orderID")); | |
BigDecimal payAmount = new BigDecimal(String.valueOf(params.get("payAmount"))); | |
String payDes = "支付测试"; | |
return paySDK.getPreparePayInfo(openId,orderID,payAmount,payDes); | |
} | |
//支付前的准备参数,供前端调用 | |
public PrepayWithRequestPaymentResponse getPreparePayInfo(String openid, String orderID, BigDecimal payAmount, String payDes) { | |
initPayConfig(); | |
//元转换为分 | |
Integer amountInteger = (payAmount.multiply(new BigDecimal(100))).intValue(); | |
//组装预约支付的实体 | |
// request.setXxx(val)设置所需参数,具体参数可见Request定义 | |
PrepayRequest request = new PrepayRequest(); | |
//计算金额 | |
Amount amount = new Amount(); | |
amount.setTotal(amountInteger); | |
amount.setCurrency("CNY"); | |
request.setAmount(amount); | |
//公众号appId | |
request.setAppid(appId); | |
//商户号 | |
request.setMchid(mchId); | |
//支付者信息 | |
Payer payer = new Payer(); | |
payer.setOpenid(openid); | |
request.setPayer(payer); | |
//描述 | |
request.setDescription(payDes); | |
//微信回调地址,需要是https://开头的,必须外网可以正常访问 | |
//本地测试可以使用内网穿透工具,网上很多的 | |
request.setNotifyUrl(v3PayNotifyUrl); | |
//订单号 | |
request.setOutTradeNo(orderID); | |
// 加密 | |
PrepayWithRequestPaymentResponse payment = service.prepayWithRequestPayment(request); | |
//默认加密类型为RSA | |
payment.setSignType("MD5"); | |
payment.setAppId(appId); | |
return payment; | |
} |
支付拉起
在微信环境调用支付准备接口获取参数后,使用WeixinJSBridge.invoke方法发起微信支付
function submitWeChatPay(orderID,payAmount,callback) { | |
if (typeof WeixinJSBridge != "undefined") { | |
var param = { | |
orderID:orderID, | |
payAmount:payAmount | |
} | |
httpPost(JSON.stringify(param),"http://xxxx/wechatpay/prepay",function (data, status) { | |
var param = { | |
"appId": data.appId, | |
"timeStamp": data.timeStamp, | |
"nonceStr": data.nonceStr, | |
"package": data.packageVal, | |
"signType": data.signType, | |
"paySign": data.paySign | |
} | |
WeixinJSBridge.invoke( | |
'getBrandWCPayRequest', param,callback); | |
}) | |
} else { | |
alert("非微信环境") | |
} | |
} |
支付回调
支付回调地址是在支付准备阶段传递的,在用户付款完成后会自动调用该接口,传递支付订单的相关信息
public void pay_notify(HttpServletRequest request, HttpServletResponse response) throws Exception { | |
//获取报文 | |
String body = getRequestBody(request); | |
//随机串 | |
String nonceStr = request.getHeader("Wechatpay-Nonce"); | |
//微信传递过来的签名 | |
String signature = request.getHeader("Wechatpay-Signature"); | |
//证书序列号(微信平台) | |
String serialNo = request.getHeader("Wechatpay-Serial"); | |
//时间戳 | |
String timestamp = request.getHeader("Wechatpay-Timestamp"); | |
InputStream is = null; | |
try { | |
is = request.getInputStream(); | |
// 构造 RequestParam | |
com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder() | |
.serialNumber(serialNo) | |
.nonce(nonceStr) | |
.signature(signature) | |
.timestamp(timestamp) | |
.body(body) | |
.build(); | |
// 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用 config | |
// 初始化 NotificationParser | |
NotificationParser parser = new NotificationParser(paySDK.getConfig()); | |
// 验签、解密并转换成 Transaction | |
Transaction transaction = parser.parse(requestParam, Transaction.class); | |
//记录日志信息 | |
Transaction.TradeStateEnum state = transaction.getTradeState(); | |
String orderNo = transaction.getOutTradeNo(); | |
System.out.println("订单号:" + orderNo); | |
if (state == Transaction.TradeStateEnum.SUCCESS) { | |
System.out.println("支付成功"); | |
//TODO------ | |
//根据自己的需求处理相应的业务逻辑,异步 | |
//通知微信回调成功 | |
response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"); | |
} else { | |
System.out.println("微信回调失败,JsapiPayController.payNotify.transaction:" + transaction.toString()); | |
//通知微信回调失败 | |
response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>"); | |
} | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
is.close(); | |
} | |
} |
订单查询
除了支付回调的异步通知,我们还需要通过定时任务主动去查询支付信息来保证业务订单支付状态的正确
"/pay_check") | (|
public Object pay_check( Map<String, Object> params) throws Exception { | |
String orderID = String.valueOf(params.get("orderID")); | |
com.wechat.pay.java.service.payments.model.Transaction transaction = paySDK.getPayOrderInfo(orderID); | |
com.wechat.pay.java.service.payments.model.Transaction.TradeStateEnum state = transaction.getTradeState(); | |
if (state == com.wechat.pay.java.service.payments.model.Transaction.TradeStateEnum.SUCCESS) { | |
return Result.okResult().add("obj", transaction); | |
} else { | |
return Result.errorResult().add("obj", transaction); | |
} | |
} | |
//获取订单支付结果信息 | |
public com.wechat.pay.java.service.payments.model.Transaction getPayOrderInfo(String orderID) { | |
initPayConfig(); | |
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); | |
request.setMchid(mchId); | |
request.setOutTradeNo(orderID); | |
com.wechat.pay.java.service.payments.model.Transaction transaction = service.queryOrderByOutTradeNo(request); | |
return transaction; | |
} |
退款申请
退款申请需要业务订单号和微信支付号,所以我们这里先通过查询订单信息得到transactionId,你也可以冗余记录在表中
退款支持全款退款和部分退款,部分退款对应的场景就是同一个订单买了多个商品,只退款了其中一个
这里只发起退款申请,具体的退款处理进度通知由退款回调完成
"/back") | (|
public Object back( Map<String, Object> params) throws Exception { | |
String orderID = String.valueOf(params.get("orderID")); | |
String backID = String.valueOf(params.get("backID")); | |
BigDecimal backAmount = new BigDecimal(String.valueOf(params.get("backAmount"))); | |
paySDK.applyRefund(orderID,backID,backAmount); | |
return Result.okResult(); | |
} | |
//申请退款 | |
public void applyRefund(String orderID, String backID,BigDecimal backAmount) { | |
initPayConfig(); | |
initBackConfig(); | |
QueryOrderByOutTradeNoRequest payRequest = new QueryOrderByOutTradeNoRequest(); | |
payRequest.setMchid(mchId); | |
payRequest.setOutTradeNo(orderID); | |
com.wechat.pay.java.service.payments.model.Transaction transaction = service.queryOrderByOutTradeNo(payRequest); | |
CreateRequest request = new CreateRequest(); | |
request.setTransactionId(transaction.getTransactionId()); | |
request.setNotifyUrl(v3BackNotifyUrl); | |
request.setOutTradeNo(transaction.getOutTradeNo()); | |
request.setOutRefundNo(backID); | |
request.setReason("测试退款"); | |
AmountReq amountReq = new AmountReq(); | |
amountReq.setCurrency(transaction.getAmount().getCurrency()); | |
amountReq.setTotal(Long.parseLong((transaction.getAmount().getTotal().toString()))); | |
amountReq.setRefund( (backAmount.multiply(new BigDecimal(100))).longValue()); | |
request.setAmount(amountReq); | |
backService.create(request); | |
} |
退款回调
退款回调在申请退款后自动调用该接口,由于退款需要一定的处理时间,所以回调通知一般显示的状态为处理中(PROCESSING)可以在此回调更新订单退款的处理状态
public void back_notify(HttpServletRequest request, HttpServletResponse response) throws Exception { | |
//获取报文 | |
String body = getRequestBody(request); | |
//随机串 | |
String nonceStr = request.getHeader("Wechatpay-Nonce"); | |
//微信传递过来的签名 | |
String signature = request.getHeader("Wechatpay-Signature"); | |
//证书序列号(微信平台) | |
String serialNo = request.getHeader("Wechatpay-Serial"); | |
//时间戳 | |
String timestamp = request.getHeader("Wechatpay-Timestamp"); | |
InputStream is = null; | |
try { | |
is = request.getInputStream(); | |
// 构造 RequestParam | |
com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder() | |
.serialNumber(serialNo) | |
.nonce(nonceStr) | |
.signature(signature) | |
.timestamp(timestamp) | |
.body(body) | |
.build(); | |
// 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用 config | |
// 初始化 NotificationParser | |
NotificationParser parser = new NotificationParser(paySDK.getConfig()); | |
// 验签、解密并转换成 Transaction | |
Refund refund = parser.parse(requestParam, Refund.class); | |
//记录日志信息 | |
Status state = refund.getStatus(); | |
String orderID = refund.getOutTradeNo(); | |
String backID = refund.getOutRefundNo(); | |
System.out.println("订单ID:" + orderID); | |
System.out.println("退款ID:" + backID); | |
if (state == Status.PROCESSING) { | |
//TODO------ | |
//根据自己的需求处理相应的业务逻辑,异步 | |
//通知微信回调成功 | |
response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"); | |
System.out.println("退款处理中"); | |
} else if (state == Status.SUCCESS) { | |
//TODO------ | |
//根据自己的需求处理相应的业务逻辑,异步 | |
//通知微信回调成功 | |
response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"); | |
System.out.println("退款完成"); | |
} else { | |
System.out.println("微信回调失败,JsapiPayController.Refund:" + state.toString()); | |
//通知微信回调失败 | |
response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>"); | |
} | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
is.close(); | |
} | |
} |
退款查询
除了退款回调的异步通知,我们还需要通过定时任务主动去查询退款信息来保证业务订单退款状态的正确
"/back_check") | (|
public Object back_check( Map<String, Object> params) throws Exception { | |
String backID = String.valueOf(params.get("backID")); | |
Refund refund = paySDK.getRefundOrderInfo(backID); | |
if (refund.getStatus() == Status.SUCCESS) { | |
return Result.okResult().add("obj", refund); | |
}if (refund.getStatus() == Status.PROCESSING) { | |
return Result.okResult().setCode(2).setMsg("退款处理中").add("obj", refund); | |
} else { | |
return Result.errorResult().add("obj", refund); | |
} | |
} | |
//获取订单退款结果信息 | |
public Refund getRefundOrderInfo(String backID){ | |
initBackConfig(); | |
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest(); | |
request.setOutRefundNo(backID); | |
return backService.queryByOutRefundNo(request); | |
} |