微信支付–JAVA开发
1.使用JSAPI支付方式.
JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。开发文档
应用场景有:
线下场所:调用接口生成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
公众号场景:用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
PC网站场景:在网站中展示二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
也即是说,jsapi支付均需在微信浏览器内部进行.
2.准备工作:
(1)微信支付支持在公众平台注册并完成微信认证的服务号,政府或媒体订阅号接入支付功能。
(2)申请商户平台并设置对应信息
(3)主要配置信息包含:公众号appid,appsecret;商户平台mchid,apikey
3.业务流程时序图:
商户系统和微信支付系统主要交互:
1、商户server调用统一下单接口请求订单,api参见公共api【统一下单API】
2、商户server接收支付通知,api参见公共api【支付结果通知API】
3、商户server查询支付结果,api参见公共api【查询订单API】
4.前期配置:
(1)设置支付目录:
支付目录填写发起jsapi支付请求的页面地址.
在微信商户平台(pay.weixin.qq.com)设置您的JSAPI支付支付目录,设置路径:商户平台–>产品中心–>开发配置,如图7.7所示。JSAPI支付在请求支付的时候会校验请求来源是否有在商户平台做了配置,所以必须确保支付目录已经正确的被配置,否则将验证失败,请求支付不成功。
(2)设置授权域名
需要在公众号里配置授权域名.
开发JSAPI支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败
5.开发步骤:
在微信浏览器中打开H5页面执行JS调起支付请求,需要获取请求必须的参数.所以,大体的开发流程如下:
A.通过网页授权,获取到用户的code;
B.通过code获取用户基本信息(主要获取openid: jsapi必传参数);
C.调用”统一下单”API获取prepay_id(预支付交易会话标识)并后台封装数据返回前端页面以备支付使用;
D.浏览器页面通过js调起支付请求,前端可根据支付结果返回展示页面(不一定可靠);
E.后台接收支付结果通知回调,确认支付结果(必须通过此回调确认是否成功支付);
F.必要时,可以再次调用”订单查询API”再次核对.
6.具体步骤: (推荐下载”微信web开发者工具”进行浏览器页面调试)
①打开支付场景页面时,请求网页授权域名:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
其中:
APPID为公众号appid,;
REDIRECT_URI为该域名请求成功后携带code返回的跳转页面(注意,此url不可携带参数);
SCOPE分两类:
snsapi_base为静默授权,不会弹出授权弹出,默认用户同意授权,官方文档讲此方法只能获取到用户openid,无法获取微信名,性别,头像等基本信息;
snsapi_userinfo方式,访问授权域名时,会弹出授权页面,用户点击”同意”才会跳转,可以获取用户基本信息
②请求成功后,微信会跳转至”REDIRECT_URI”地址,并携带一个code和state数据:
redirect_uri/?code=CODE&state=STATE state没啥用,主要获取code
code说明: code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
③拿到code后,传入后台,通过code调用”网页授权code获取基本信息URL”地址,去获取到用户的openid等信息(https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code)
APPID,SECRET是公众号的配置信息,CODE就是我们前端页面传入的code
④拿到用户openid,根据官方文档参数说明,需要封装参数并进行签名,再调用”统一下单API”去请求获取到支付所需的prepay_id.
统一下单参数主要有11个字段必传:
appid 公众账号ID String(32)
mch_id 商户号 String(32)
nonce_str 随机字符串 String(32) 后台生成,sdk有示例
sign 签名 String(32) 后台生成
body 商品描述 String(128)
out_trade_no 商户订单号 String(32)
total_fee 标价金额 Int 单位为”分”,不可带小数点
spbill_create_ip 终端IP String(64) 用户的客户端ip,后台request获取
notify_url 通知地址 String(256) 异步接收支付成功回调通知
trade_type 交易类型 String(16) JSAPI, NATIVE, APP
openid 用户标识 String(128) JSAPI方式必传
后台主要负责3个数据:nonce_str, ip, sign
nonce_str可通过sdk里的方法生成;
sign:签名,是将除sign之外的10个参数通过MD5签名算法生成,此处需要用到商户平台的apiKey;
spbill_create_ip:客户端ip,也即是用户点击支付的微信浏览器的ip:
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader(“x-forwarded-for”);
if (ip == null || ip.length() == 0 || “unknown”.equalsIgnoreCase(ip)) {
ip = request.getHeader(“Proxy-Client-IP”);
}
if (ip == null || ip.length() == 0 || “unknown”.equalsIgnoreCase(ip)) {
ip = request.getHeader(“WL-Proxy-Client-IP”);
}
if (ip == null || ip.length() == 0 || “unknown”.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.equals(“0:0:0:0:0:0:0:1”) ? “127.0.0.1” : ip;
}
⑤11个参数处理完毕后,需要将map转换为xml格式的字符串,调用post请求访问”统一下单API”: https://api.mch.weixin.qq.com/pay/unifiedorder
此处有个坑注意:sdk里有一个设置header的参数:
httpPost.addHeader(“User-Agent”, WXPayConstants.USER_AGENT);
这个不设置会出错~~~~
请求成功后,会返回xml格式的结果,主要有trade_type,prepay_id,我们就需要拿到prepay_id即可.
⑥最后一步,封装js页面调起jsapi支付所需的参数: 建议Map集合封装
appid 公众号appid
timeStamp 时间戳,秒级
nonceStr 随机字符串,sdk生成吧
package 注意:package为java关键字!!!所以推荐map集合来封装吧~~~~
signType 签名方式: MD5
paySign 上面5个参数再次签名
所有数据处理完毕,后台工作完成,将参数返回前端页面;
⑦拿到后台封装好的数据,在前端页面js调起jsapi支付请求,这里就可以使用"微信web开发者工具"来调试了哟.很简单了,代码官网有示例: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
⑧最后的最后!!!如果支付成功,自行页面逻辑处理或跳转展示页面.但是,需记住,还有一个支付成功通知!!!也就是第4步”统一下单”时填写的notify_url回调.需在此回调中,接收数据,验签->数据匹配(如金额,订单号等匹配是否一致)->业务数据处理,修改数据库数据支付状态等等操作.
注意:如果校验成功,记得即时响应微信服务器: response.getWriter().write(“
建议,前端页面支付成功跳转展示页面时,通过查询接口获取后台异步通知校验成功后的数据.
贴上部分代码:
/**
* 根据code获取openId
*
* @param code
* @return openId
*/
private String getOpenId(String code) {
String getUrl = Constants.CODE_TOKEN_URL.replace("APPID", appId)
.replace("SECRET", appSecret).replace("CODE", code);
System.out.println("url: " + getUrl);
JSONObject jsonObject = getUrlJson(getUrl);
return jsonObject.get("openid").toString();
}
/**
* httpget请求
*
* @param url
* @return JSONObject
*/
private JSONObject getUrlJson(String url) {
JSONObject jsonObject = null;
HttpClient client = HttpClientBuilder.create().build();
HttpGet httpGet = new HttpGet(url);
HttpResponse response;
try {
response = client.execute(httpGet);
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
logger.info("getUrlJson: {}", result);
jsonObject = JSONObject.parseObject(result);
httpGet.releaseConnection();
} catch (IOException e) {
e.printStackTrace();
}
return jsonObject;
}
/**
* 网页请求生成支付订单
*
* @param request
* @param map
* @return
*/
@Override
public Map createOrderOpenId(HttpServletRequest request, Map map) {
logger.info("createOrder------: {}", map);
Map orderMap = new HashMap();
String openId = getOpenId(map.get("code").toString());
if (StringUtils.isEmpty(openId)) {
return orderMap;
}
try {
String clientIp = getClientIp(request);
logger.info("createOrder获取clientIp------: {}", clientIp);
orderMap.put("appid", appId); // 微信支付分配的公众账号ID(企业号corpid即为此appId)
orderMap.put("mch_id", mchId); // 商户号
orderMap.put("nonce_str", WXPayUtil.generateNonceStr()); // 随机字符串
orderMap.put("body", map.get("body")); // 商品描述
orderMap.put("out_trade_no", map.get("outTradeNo")); // 商户订单号
orderMap.put("total_fee", map.get("totalFee")); // 订单总金额,单位为分
orderMap.put("spbill_create_ip", clientIp); // 终端IP,用户的客户端IP
orderMap.put("notify_url", backUrl + "/payNotify"); // 通知地址-异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
orderMap.put("trade_type", map.get("tradeType")); // 交易类型 JSAPI -JSAPI支付
orderMap.put("openid", openId); // 用户标识
// 获取sign
orderMap.put("sign", WXPayUtil.generateSignature(orderMap, apiKey)); // key: apiKey
// 订单数据拼接完成,转换map为xml的String字符串
String xmlData = WXPayUtil.mapToXml(orderMap);
// 统一下单->获取订单的prepay_id
HttpClient httpClient = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost("https://" + WXPayConstants.DOMAIN_API + WXPayConstants.SANDBOX_UNIFIEDORDER_URL_SUFFIX);
StringEntity postEntity = new StringEntity(xmlData, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.addHeader("User-Agent", WXPayConstants.USER_AGENT);
httpPost.setEntity(postEntity);
HttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
// 获取统一下单接口返回数据
String resXml = EntityUtils.toString(httpEntity, "UTF-8");
logger.info("获取统一下单接口返回数据--resXml----: {}", resXml);
Map orderResMap = WXPayUtil.xmlToMap(resXml);
String prepayId = orderResMap.get("prepay_id").toString();
logger.info("createOrder获取prepayId------: {}", prepayId);
// 存储订单信息
map.put("clientIp", clientIp);
map.put("openId", openId);
map.put("prepayId", prepayId);
insertOrder(map);
// 封装前端调起JSAPI支付所需参数
Map payMap = new HashMap();
payMap.put("appId", appId); // 公众号id
payMap.put("timeStamp", WXPayUtil.getCurrentTimestamp()); // 时间戳,转换成秒数
payMap.put("nonceStr", WXPayUtil.generateNonceStr()); // 随机字符串
payMap.put("package", "prepay_id=" + prepayId); // 订单详情扩展字符串
payMap.put("signType", "MD5"); // 签名方式
// 再次签名获取paySign
payMap.put("paySign", WXPayUtil.generateSignature(payMap, apiKey)); // 签名 key:apiKey
return payMap;
} catch (Exception e) {
e.printStackTrace();
return new HashMap(0);
}
}
/**
* 支付成功回调通知
*
* @param request
* @param response
* @return
*/
@Override
public void payNotify(HttpServletRequest request, HttpServletResponse response) {
logger.info("微信支付成功回调通知---------------------");
String returnStr = " ";
InputStream is = null;
try {
is = request.getInputStream();//获取请求的流信息(这里是微信发的xml格式所有只能使用流来读)
String xmlStr = convertStreamToString(is);
Map notifyMap = WXPayUtil.xmlToMap(xmlStr);//将微信发的xml转map
Map signMap = new HashMap(notifyMap);
signMap.remove("sign");
// 验签
String localSign = WXPayUtil.generateSignature(signMap, apiKey);
if (!notifyMap.get("sign").equals(localSign)) {
logger.info("验签不通过");
return;
}
//同样的通知可能会多次发送给商户系统,商户系统必须能够正确处理重复的通知。如果已处理过,直接给微信支付返回成功。
// 获取本地订单信息比对
String resultCode = notifyMap.get("result_code");
PayOrder localOrder = payMapper.singleOrder(notifyMap.get("out_trade_no"));
if (localOrder.getCallbackState() == 1 && resultCode.equals("SUCCESS")) {
//重复通知,直接响应微信服务器
response.getWriter().write(returnStr);
is.close();
return;
}
//判断金额是否一致
if (localOrder.getTotalFee() != Integer.valueOf(notifyMap.get("total_fee"))) {
return;
}
//修改订单的支付状态
if (notifyMap.get("return_code").equals("SUCCESS")) {
localOrder.setCallbackState(1);
localOrder.setTradeState(resultCode);
localOrder.setBankType(notifyMap.get("bank_type"));
localOrder.setTransactionId(notifyMap.get("transaction_id"));
payMapper.updateByPrimaryKeySelective(localOrder);
}
//响应微信服务器
response.getWriter().write(returnStr);
is.close();
} catch (Exception e) {
e.printStackTrace();
}
return;
}