一、要想做微信JSAPI的支付,首先得有一些准备工作:
1、有一个服务号,该服务号必须开通微信支付,公众号都有的两个参数:APPID和APPSECRET;开通微信支付后会有以下参数:MCHID(商户号),KEY(商户支付密钥)
2、配置公众号微信支付,我们需要配置微信公众号支付的地址(测试目录,就是你的微信支付js页面对应的连接,也就是所在地址)和测试白名单(比如你要测试微信支付,你的微信号得配置到支付白名单中,不然没办法测试,会出现redirect_uri参数错误),如果你的支付对应的方法或者支付js页面的地址为https://ayo.tunnel.qydev.com/anyou/customize/toCreateOrder,那么此处只需要配置成https://ayo.tunnel.qydev.com/anyou/customize/即可;
二、接下来我们就开始开发
首先上个图,让大家对微信支付的开发先有个思路,知道其中的业务逻辑:
我们要做的就是实现上图红色的部分;
微信公众号支付的总体其实很简单,大致就分为三步。第一步需要获取用户授权;第二步调用统一下单接口获取预支付id;第三步H5调起微信支付的内置的js。下面介绍具体每一步的开发流程:
一)首先要明确微信公众号支付属于网页版支付,所以相较于app的直接调取微信支付要多一步微信授权。也就是需要获取用户的openid。微信公众号使用的交易类型是JSAPI,所以统一下单接口的文档明确的写到
因此我们必须去获取openid,同时也可以处理一些我们需要的逻辑。获取用户授权有两种方式:scope=snsapi_base;scope=snsapi_userinfo。下面是授权的代码:其中的snsapi_userinfo就代表第二种授权方式,可以改成snsapi_base,自己根据情况进行选择,说一下这两种的区别:
1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
3、用户管理类接口中的“获取用户基本信息接口”,是在用户和公众号产生消息交互或关注后事件推送后,才能根据用户OpenID来获取用户基本信息。这个接口,包括其他微信接口,都是需要该用户(即openid)关注了公众号后,才能调用成功的。
微信工具类WexinUtil:
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.iambuyer.entity.WxUser;
import net.sf.json.JSONObject;
/**
*
* @ClassName: WexinUtil
* @Company: http://an.you.cn/
* @Description:微信的工具类
* @author ayo
* @date 2016年10月26日 下午9:57:35
*/
public class WexinUtil {
/**
*
* @Title: isWechat
* @Description: 判断请求是否来自于微信客户端
* @param request
* @return
* @return boolean
* @throws
*/
public static boolean isWechat(HttpServletRequest request){
String agent = request.getHeader("user-Agent").toLowerCase();
if(agent.contains("micromessenger")){
return true;
}
return false;
}
/**
*
* @Title: getBaseUrl
* @Description: TODO(判断是否是客户端请求)
* @param @return 设定文件
* @return String 返回类型
* @throws
*/
public static String getBaseUrl(HttpServletRequest request){
String path = request.getContextPath();
String url = "";
String _port = ":"+request.getServerPort();
if(_port.equals(":80")||_port.equals(":443"))_port = "";
url = request.getScheme()+"://"+request.getServerName()+_port+path;
return url;
}
/**
*
* @Title: getUrlParameter
* @Description: TODO(参数封装)
* @param @return 设定文件
* @return String 返回类型
* @throws
*/
public static String getUrlParameter(HttpServletRequest request){
Map<String, String[]> map = request.getParameterMap();
String pul = "";
if(map.size() > 0){
pul += "?";
for (String key : map.keySet()) {
String value = map.get(key)[0];
pul += key + "=" + value +"&";
}
pul = pul.substring(0, pul.length()-1);
}
return pul;
}
/**
* @throws UnsupportedEncodingException
*
* @Title: oauthByGet
* @Description: TODO(获取微信用户的信息)
* @param @param user
* @param @param code
* @param @return 设定文件
* @return WxUser 返回类型
* @throws
*/
public static WxUser oauthByGet(WxUser user,String code) throws UnsupportedEncodingException{
/**
* 获取网页授权的access_token
*/
String _url = WexinUrlConst.getWEXIN_USER_TOKEN(WxConfigure.getAPPID(),
WxConfigure.getAPPSECRET(), code);
/**
* https请求忽略证书
*/
String resp = new Con2http().httpsRequest(_url, "GET", null);
if (!resp.contains("errcode")) {
JSONObject object = JSONObject.fromObject(resp);
String openid = object.getString("openid");
user.setOpenid(openid);
String access_token = object.getString("access_token");
String userUrl = WexinUrlConst.getWEXIN_USER_USERINFO(access_token,
openid);
String userInfo = new Con2http().httpsRequest(userUrl, "GET", null);
if (!userInfo.contains("errcode")) {
JSONObject _user = JSONObject.fromObject(userInfo);
user.setCity(_user.getString("city"));
user.setCountry(_user.getString("country"));
user.setCreateTime(new Date());
user.setHeadImgUrl(_user.getString("headimgurl"));
String filterEmoji = EmojiFilter.filterEmoji(_user.getString("nickname"));
user.setNickname(filterEmoji);
user.setProvince(_user.getString("province"));
}
}
return user;
}
/**
*
* @Title: oauthResp
* @Description: TODO(微信授权)
* @param @param response
* @param @param url
* @param @throws UnsupportedEncodingException
* @param @throws IOException 设定文件
* @return void 返回类型
* @throws
*/
public static void oauthResp(HttpServletRequest request, HttpServletResponse response,String url)
throws UnsupportedEncodingException, IOException {
/**
* 授权后回调的url
*/
String reqUrl = getBaseUrl(request)+url+getUrlParameter(request);
reqUrl = URLEncoder.encode(reqUrl, "utf-8");
/**
* 获取code
*/
String _url = WexinUrlConst.getWEXIN_USER_GETCODE(
WxConfigure.getAPPID(), reqUrl, "code", "snsapi_userinfo",
"qawine");
/**
* 重定向到这个路径去获取code
*/
response.sendRedirect(_url);
}
}
使用方法:
String code = request.getParameter("code");
String state = request.getParameter("state");
if(code != null && state != null){
wxuser = new WxUser();
wxuser = WexinUtil.oauthByGet(wxuser, code);
request.getSession().setAttribute("wxuser", wxuser);
customizeService.queryAndAddOrUpdateWxUser(wxuser);
return "redirect:/customize/"+shopCode+"/toCustomizeIndex";
}
else{
WexinUtil.oauthResp(request, response, "/customize/"+shopCode+"/toCustomizeIndex");
return null;
}
至此openid已经通过授权获得!
二)我们获取了openid后,就可以进行下一步的统一下单的开发了。微信上统一下单接口的文档写的比较详细了,具体的参数含义我就不多做介绍了。下面直接贴最直观的代码。
1、微信支付统一下单
/**
* 微信支付统一下单
*
* @param o
* 订单
* @param spBillCreateIP
* 客户端ip
* @param notifyUrl
* 异步通知
* @return 下单的xml数据
*/
public static String wxpay(Order o, String spBillCreateIP) {
String body = "iambuyer";
String outTradeNo = o.getOrderCode();
double d = o.getTotalMoney();
int feeTotal = (int) (d * 100);
String tradeType = "JSAPI";
String timeStart = DateUtil.getTimes(o.getCreateTime());
String timeExpire = DateUtil.getTimes(o.getCreateTime(), 1000 * 60 * 60
* 24 * 7);// 测试失效时间为7天
String openid = o.getuId();
String attach = o.getId() + "";
String notifyUrl = NOTIFYURL;
WebPayReqData web = new WebPayReqData(body, outTradeNo, feeTotal,
spBillCreateIP, timeStart, timeExpire, notifyUrl, tradeType,
openid, attach);
String xml = getWXPAY(web);
return xml;
}
2、获取微信支付统一下单返回数据
/**
* 获取微信支付统一下单返回数据
*
* @param o
* @param spBillCreateIP
* @param notifyUrl
* @return
*/
@SuppressWarnings("static-access")
public static Map<String, String> reqWxpay(Order o, String spBillCreateIP) {
// 组合请求的数据
String data = wxpay(o, spBillCreateIP);
System.out.println("请求的数据" + data);
String url = URL_TYXD;
String resp = "";
try {
resp = new JavaConnetInternet().sendHttpPostRequestxj(url, data);
} catch (IOException e) {
e.printStackTrace();
}
TreeMap<String, String> map = parse(resp);
return map;
}
3、拼接jsapi数据
/**
* 拼接jsapi数据
*
* @param map
* @return
*/
public static String getJsapiJson(String prepay_id) {
Map<String, Object> treeMap = new TreeMap<String, Object>();
treeMap.put("appId", APPID);
treeMap.put("timeStamp", (int) (System.currentTimeMillis() / 1000) + "");
treeMap.put("nonceStr", RandomUtil.getRandom(32));
String pack = "prepay_id=" + prepay_id;
treeMap.put("package", pack);
treeMap.put("signType", "MD5");
String sign = getSign(treeMap);
treeMap.put("paySign", sign);
return JsonUtil.getJsonString4JavaPOJO(treeMap);
}
4、去往微信js所在的页面调起微信支付
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache, must-revalidate">
<meta http-equiv="expires" content="0">
<title>-支付</title>
<script src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script type="text/javascript">
//调用微信JS api 支付
function onBridgeReady() {
var jsapi = ${jsapiData};
WeixinJSBridge
.invoke(
'getBrandWCPayRequest',jsapi,
function(res) {
WeixinJSBridge.log(res.err_msg);
if (res.err_msg == "get_brand_wcpay_request:ok") {
window.location.href = "<%=path%>/rong/paySuc?type=web&id=${order.id}";
} else {
//返回跳转到订单详情页面
alert("支付失败");
window.location.href = "<%=path%>/rong/myOrder?type=web";
}
});
}
function callpay() {
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady,false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
}
</script>
</head>
<body>
<div class="vpay vorder">
<p></p>
<p>
支付金额:<span><i>¥</i>${order.totalMoney}</span>
</p>
<div class="half_line half_line_top"></div>
</div>
<div class="vpay_sub">
<a onclick="callpay()">微信支付</a>
</div>
</body>
</html>
微信支付工具类如下,里面设计到支付的所有方法:
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.TreeMap;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import com.iambuyer.entity.Order;
import com.iambuyer.utils.DateUtil;
import com.iambuyer.utils.MD5;
import com.iambuyer.utils.Tools;
import com.iambuyer.utils.consts.Const;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.XmlFriendlyReplacer;
import com.thoughtworks.xstream.io.xml.XppDriver;
@SuppressWarnings("unused")
public class WxPayUtil {
private static ResourceBundle resb1 = ResourceBundle.getBundle("properties/wxpayapi");
// APPID
private static String APPID = resb1.getString("APPID");
// MCHID
private static String MCHID = resb1.getString("MCHID");
// KEY
private static String KEY = resb1.getString("KEY");
// APPID
private static String APPSECRET = resb1.getString("APPSECRET");
// 统一下单接口
private static String URL_TYXD = resb1.getString("URL_TYXD");
// 查询订单
private static String URL_CXDD = resb1.getString("URL_CXDD");
// 关闭订单
private static String URL_GBDD = resb1.getString("URL_GBDD");
// 查询退款
private static String URL_CXTK = resb1.getString("URL_CXTK");
// 申请退款
private static String URL_SQTK = resb1.getString("URL_SQTK");
// 对账单
private static String URL_DOWNORDER = resb1.getString("URL_DOWNORDER");
// 测速
private static String URL_TEST = resb1.getString("URL_TEST");
// 融咖啡微信支付异步通知地址
private static String NOTIFYURL = resb1.getString("NOTIFYURL");
/**
* 获取数字签名
*
* @param o
* @return
* @throws IllegalAccessException
*/
public static String getSign(Map<String, Object> map) {
ArrayList<String> list = new ArrayList<String>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() != "") {
list.add(entry.getKey() + "=" + entry.getValue() + "&");
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + WxPayUtil.KEY;
result = MD5.md5(result).toUpperCase();
return result;
}
/**
* 微信支付统一下单
*
* @param o
* 订单
* @param spBillCreateIP
* 客户端ip
* @param notifyUrl
* 异步通知
* @return 下单的xml数据
*/
public static String wxpay(Order o, String spBillCreateIP) {
String body = "iambuyer";
String outTradeNo = o.getOrderCode();
double d = o.getTotalMoney();
int feeTotal = (int) (d * 100);
String tradeType = "JSAPI";
String timeStart = DateUtil.getTimes(o.getCreateTime());
String timeExpire = DateUtil.getTimes(o.getCreateTime(), 1000 * 60 * 60
* 24 * 7);// 测试失效时间为7天
String openid = o.getuId();
String attach = o.getId() + "";
String notifyUrl = NOTIFYURL;
WebPayReqData web = new WebPayReqData(body, outTradeNo, feeTotal,
spBillCreateIP, timeStart, timeExpire, notifyUrl, tradeType,
openid, attach);
String xml = getWXPAY(web);
return xml;
}
/**
* 封装微信统一下单数据成XML格式字符串
* @param web
* @return
*/
private static String getWXPAY(WebPayReqData web){
String xml = WxPayUtil.webWxpayToXml(web);
return xml;
}
/**
* 获取微信支付统一下单返回数据
*
* @param o
* @param spBillCreateIP
* @param notifyUrl
* @return
*/
@SuppressWarnings("static-access")
public static Map<String, String> reqWxpay(Order o, String spBillCreateIP) {
// 组合请求的数据
String data = wxpay(o, spBillCreateIP);
System.out.println("请求的数据" + data);
String url = URL_TYXD;
String resp = "";
try {
resp = new JavaConnetInternet().sendHttpPostRequestxj(url, data);
} catch (IOException e) {
e.printStackTrace();
}
TreeMap<String, String> map = parse(resp);
return map;
}
/**
* 通过微信支付请求数据,获取返回
* @param web
* @return
*/
public static Map<String, String> reqWxpay(WebPayReqData web) {
String data = getWXPAY(web);
String url = URL_TYXD;
String resp = "";
try {
new JavaConnetInternet();
resp = JavaConnetInternet.sendHttpPostRequestxj(url, data);
} catch (IOException e) {
e.printStackTrace();
}
TreeMap<String, String> map = parse(resp);
return map;
}
/**
* 拼接jsapi数据
*
* @param map
* @return
*/
public static String getJsapiJson(String prepay_id) {
Map<String, Object> treeMap = new TreeMap<String, Object>();
treeMap.put("appId", APPID);
treeMap.put("timeStamp", (int) (System.currentTimeMillis() / 1000) + "");
treeMap.put("nonceStr", RandomUtil.getRandom(32));
String pack = "prepay_id=" + prepay_id;
treeMap.put("package", pack);
treeMap.put("signType", "MD5");
String sign = getSign(treeMap);
treeMap.put("paySign", sign);
return JsonUtil.getJsonString4JavaPOJO(treeMap);
}
/**
* 获取查询订单的支付结果
*
* @param request
* @param orderNum
* @return
*/
public static Map<String, String> getSearchOrder(String orderNum) {
Map<String, Object> map = new HashMap<String, Object>();
String nonce_str = RandomUtil.getRandom(32);
map.put("appid", APPID);
map.put("mch_id", MCHID);
map.put("out_trade_no", orderNum);
map.put("nonce_str", nonce_str);
String sign = getSign(map);
map.put("sign", sign);
String xml = objectToXml(map);
String serverUrl = URL_CXDD;
String respoXml = null;
try {
respoXml = JavaConnetInternet.sendHttpPostRequestxj(serverUrl, xml);
} catch (IOException e) {
e.printStackTrace();
}
Map<String, String> _map = parse(respoXml);
return _map;
}
/**
* 判断签名是否正确
*
* @param map
* @return
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static String isSign(Map map) {
String sign = (String) map.get("sign");
map.remove("sign");
String _sign = getSign(map);
if (sign.equals(_sign)) {
return "OK";
}
return "签名错误";
}
/**
* 解析参数成xml格式的字符串
*/
public static String getXMLString(TreeMap<String, String> map) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
for (Map.Entry<String, String> entry : map.entrySet()) {
sb.append("\r\t<").append(entry.getKey()).append(">")
.append(entry.getValue()).append("</")
.append(entry.getKey()).append(">");
}
sb.append("\r</xml>");
return sb.toString();
}
private static XStream xstream = new XStream(new XppDriver(
new XmlFriendlyReplacer("_-", "_")));
public static String musicMessageToXml(TreeMap<String, String> map) {
xstream.alias("xml", map.getClass());
return xstream.toXML(map);
}
/**
* 同意下单数据封装成xml
*
* @param obj
* @return
*/
public static String webWxpayToXml(WebPayReqData obj) {
xstream.alias("xml", obj.getClass());
return xstream.toXML(obj);
}
/**
* java对象转换成xml
*
* @param obj
* @return
*/
public static String objectToXml(Object obj) {
xstream.alias("xml", obj.getClass());
return xstream.toXML(obj);
}
public static String generate(String return_code, String return_msg) {
String xml = "<xml><return_code><![CDATA[%1$s]]></return_code><return_msg><![CDATA[%2$s]]></return_msg></xml>";
return String.format(xml, return_code, return_msg);
}
/**
* 解析成数组
*
* @param data
* @return
*/
@SuppressWarnings("unchecked")
public static TreeMap<String, String> parse(String data) {
TreeMap<String, String> map = new TreeMap<String, String>();
try {
Document document = DocumentHelper.parseText(data);
List<Element> elements = document.getRootElement().elements();
for (Element e : elements) {
map.put(e.getName(), e.getText());
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
/**
* 解析request中的xml数据
*
* @param args
*/
public static TreeMap<String, String> parseXml(HttpServletRequest request)
throws Exception {
// 将解析结果存储在HashMap中
TreeMap<String, String> map = new TreeMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
@SuppressWarnings("unchecked")
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList)
map.put(e.getName(), e.getText());
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
/**
* jsapi 数字签名
* @param jsapi_ticket
* @param url
* @return
*/
public static Map<String, String> jsSign(String url) {
Map<String, String> ret = new HashMap<String, String>();
String jsapi_ticket = "";
String str = Tools.readTxtFile(Const.WXCONFIG);//获取配置信息
if(null != str&& !"".equals(str)){
String strIW[] = str.split(",hc360,");
if(strIW.length == 5 ){
jsapi_ticket = strIW[4];
ret = jsApiData(jsapi_ticket, url);
}
}
return ret;
}
/**
* jsapi数据拼合
* @param jsapi_ticket
* @param url
* @return
*/
public static Map<String, String> jsApiData(String jsapi_ticket, String url) {
Map<String, String> ret = new HashMap<String, String>();
String nonce_str = RandomUtil.getRandom(32);
String timestamp = create_timestamp();
String string1;
String signature = "";
// 注意这里参数名必须全部小写,且必须有序
string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str
+ "×tamp=" + timestamp + "&url=" + url;
System.out.println(string1);
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes("UTF-8"));
signature = byteToHex(crypt.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
ret.put("url", url);
ret.put("jsapi_ticket", jsapi_ticket);
ret.put("nonceStr", nonce_str);
ret.put("timestamp", timestamp);
ret.put("signature", signature);
String str = Tools.readTxtFile(Const.WXCONFIG);//获取配置信息
String strIW[] = str.split(",hc360,");
ret.put("appId", strIW[0]);
return ret;
}
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash)
{
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
/**
* 获取当前时间秒数
* @return
*/
private static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
public static void main(String[] args) {
}
}
三)至此,微信的JSAPI支付已经开发完成!