2017-9月微信公众号支付-Java详解
在此之前,先C麻瓜藤N遍,MD官方文档一半正确一半错误。言归正传,
微信支付整体流程:微信授权登录商户的公众号——微信支付的公众号配置——统一下单——微信js调起支付页面——输入密码支付——支付成功,异步回调URL处理商户的相应业务
一、业务场景:
先看一下支付的业务场景:用户使用微信登录商户页面,点击支付按钮,调起微信支付,选择付款卡号,输入密码,完成支付,如图:
场景十分简单,不过步骤比较多,稍不注意就掉坑里了。
二、微信公众号支付的配置准备:
1)调用公众号支付,首先你得有个公众号,并且是服务号,如果是订阅号,请联系上司改为服务号。有了服务号,你需要去申请微信支付接口,具体可参考怎么申请微信支付接口(这个过程中会要求填写一个网页授权域名,下面的配置中会用到)。然后登录微信公众平台https://mp.weixin.qq.com,配置JS接口安全域名和网页授权域名:公众号设置——功能设置——JS接口安全域名,网页授权域名(注:会下载MP_verify_8QXHzPy7cuvgQiqU.txt文件,如图,将其放到项目根目录下)
两个域名配置成项目的根目录即可,
如图(注:mozhu.iok.la替换成你的网站域名,wap37lion替换成你的项目名称;开头没有http/https,结尾没有斜杠“/”),
2)登录后,修改支付授权目录:微信支付——开发配置——支付授权目录更改。
注:支付授权目录指的是你的支付页面所在目录。如图,
假设你的项目名为wTest2,你是在pay目录下,NewFile.html中调起的微信支付,外网访问NewFile.html的全路径为"http://你的网站域名/wTest2/pay/",那么你的目录是:"http://你的网站域名/wTest2/pay/";或者,如果你的网站域名就是你的项目根目录,外网访问NewFile.html的全路径为"http://你的网站域名/pay/",那目录就是"http://你的网站域名/pay/"。如图
三、微信网页授权登录公众号
授权可参考官方文档https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
1)用户同意授权,获取code
在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(服务号获得高级接口后,默认拥有scope参数中的snsapi_base和snsapi_userinfo),引导关注者打开如下页面:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 公众号的唯一标识 |
redirect_uri | 是 | 授权后重定向的回调链接地址,请使用urlEncode对链接进行处理 |
response_type | 是 | 返回类型,请填写code |
scope | 是 | 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息) |
state | 否 | 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节 |
#wechat_redirect | 是 | 无论直接打开还是做页面302重定向时候,必须带此参数 |
如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE,也就是说授权成功后,微信会自动把获取到的code当作参数传到redirect_uri路径下。我们只需要定义好redirect_uri,在redirect_uri相应的控制器中获取code参数即可
示例代码(在控制器中):
package com.common;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("test/TestResource")
public class TestResource {
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Path("wxLogin")//redirect_uri
public Response wxLogin(@QueryParam("code") String code){
System.out.println(code);
return null;
}
}
2)通过code获取access_token
请求方法
获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 公众号的唯一标识 |
secret | 是 | 公众号的appsecret |
code | 是 | 填写第一步获取的code参数 |
grant_type | 是 | 填写为authorization_code |
示例代码:
public static Map<String, Object> getAccessToken(String code) {
Map<String, Object> data = new HashMap();
try {
String appid="";//商户的APPID
String appsecret="";//商户的APPSECRET
String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
appid, appsecret, code);
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
HttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity httpEntity = httpResponse.getEntity();
String tokens = EntityUtils.toString(httpEntity, "utf-8");
data=JSON.parse(tokens,Map.class);
String openId = data.get("openid")+"";
String accessToken=data.get("access_token")+"";
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
返回说明
正确时返回的JSON数据包如下:
{ "access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE" }
参数 | 描述 |
---|---|
access_token | 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 |
expires_in | access_token接口调用凭证超时时间,单位(秒) |
refresh_token | 用户刷新access_token |
openid | 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID |
scope | 用户授权的作用域,使用逗号(,)分隔 |
3)通过access_token获取用户信息
请求方法
http:GET(请使用https协议) https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
参数说明
参数 | 描述 |
---|---|
access_token | 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 |
openid | 用户的唯一标识 |
lang | 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语 |
示例代码:
/**
* 获取用户信息
*
* @param accessToken
* @param openId
* @return
*/
public static Map<String, Object> getUserInfo(String accessToken, String openId) {
Map<String, Object> data = new HashMap();
String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN";
try {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
HttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity httpEntity = httpResponse.getEntity();
String responseStr = EntityUtils.toString(httpEntity, "utf-8");
data=JSON.parse(responseStr,Map.class);
} catch (Exception ex) {
}
return data;
}
返回说明
正确时返回的JSON数据包如下:
{ "openid":" OPENID",
" nickname": NICKNAME,
"sex":"1",
"province":"PROVINCE"
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ
4eMsv84eavHiaiceqxibJxCfHe/46",
"privilege":[ "PRIVILEGE1" "PRIVILEGE2" ],
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
参数 | 描述 |
---|---|
openid | 用户的唯一标识 |
nickname | 用户昵称 |
sex | 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 |
province | 用户个人资料填写的省份 |
city | 普通用户个人资料填写的城市 |
country | 国家,如中国为CN |
headimgurl | 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 |
privilege | 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom) |
unionid | 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 |
四、统一下单(请求参数较多,仔细看)
调用微信支付必须在后台生成统一下单接口,获取prepay_id。需要的参数,可参考官方文档统一下单。看不懂,不要急,往下看。
接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数说明:
appid :应用ID,登陆微信公众号后台——开发——基本配置获取
mch_id : 微信支付商户号,登陆微信支付后台,即可看到
device_info:设备号,终端设备号(门店号或收银设备ID),注意:PC网页或公众号内支付请传"WEB"
body:商品描述,商品或 支付单简要描述(先随便传个字符串)
trade_type:交易类型,取值如下:JSAPI,NATIVE,APP。我们这里使用的JSAPI。他们的区别,请参考https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_2
nonce_str:随机字符串,不长于32位(参考算法https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3)
notify_url:通知地址,接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数,不能是相对地址。
out_trade_no:商户订单号,商户系统内部的订单号,32个字符内、可包含字母(参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_2)
total_fee:总金额,订单总金额,单位为分
openid:用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。(要是不知道这个从哪里来的话,没关系。微信不是给咱写文档了吗https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_4)
还有最最重要的角色总要在最后登场。
sign:签名,官方给的签名算法https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3。
key:获取方法:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
示例代码:
/**
* 微信支付 获得PrePayId
*
* @param payURL
* 调用支付的完整URL request.getRequestURL().toString();
* @param nonce_str
* 随机字符串 用WXPayUtil生成
* @param body
* 商品或支付单简要描述
* @param out_trade_no
* 商户系统内部的订单号,32个字符内、可包含字母 。用WXPayUtil生成
* @param total_fee
* 订单总金额,单位为分
* @param IP
* APP和网页支付提交用户端ip
* @param notify_url
* 接收微信支付异步通知回调地址
* @param openid
* 用户openId request.getSession().getAttribute("openid")
* @throws IOException
*/
public Map<String, Object> wxPay(String payURL,String nonce_str, String body, String out_trade_no, String total_fee, String ip,
String notify_url, String openId) throws Exception {
WxPaySendDataPO result = new WxPaySendDataPO();
Map<String,Object>map=new HashMap<String, Object>();
//统一下单
Map resultMap=WXPayUtil.unifiedorder(nonce_str, body, out_trade_no, total_fee, ip, notify_url, openId);
String timeStamp = WXPayUtil.getTimeStamp();//时间戳
String result_code=resultMap.get("result_code")+"";
String return_code=resultMap.get("return_code")+"";
if("SUCCESS".equals(return_code)&&"SUCCESS".equals(result_code)){
//result.setResult_code(result_code);
result.setAppid(WXUtil.APPID);
result.setTimeStamp(timeStamp);
result.setNonce_str(nonce_str);
result.setPackageStr("prepay_id="+resultMap.get("prepay_id"));
result.setSignType("MD5");
//
// // WeChatUtil.unifiedorder(.....) 下单操作中,也有签名操作,那个只针对统一下单,要区别于下面的paySign
// //第二次签名,将微信返回的数据再进行签名
SortedMap<Object,Object> signMap = new TreeMap<Object,Object>();
signMap.put("appId", WXUtil.APPID);
signMap.put("timeStamp", timeStamp);
signMap.put("nonceStr", nonce_str);
signMap.put("package", "prepay_id="+resultMap.get("prepay_id")); //注:看清楚,值为:prepay_id=xxx,别直接放成了wxReturnData.getPrepay_id()
signMap.put("signType", "MD5");
String paySign = WXSignUtil.createSign(signMap, WXUtil.KEY);//支付签名
result.setSign(paySign);
String signature=WXUtil.getSHA1Sign( payURL, timeStamp, nonce_str);
map.put("signature",signature);
map.put("code", 1);
map.put("msg", "success");
map.put("data", result);
}else{
map.put("code", 2);
map.put("msg", "获取prepay_id失败");
map.put("data", "");
}
return map;
}
WxPaySendDataPO:
package com.util; public class WxPaySendDataPO { //公众账号ID private String appid; //附加数据 private String attach; //商品描述 private String body; //商户号 private String mch_id; //随机字符串 private String nonce_str; //通知地址 private String notify_url; //商户订单号 private String out_trade_no; //标价金额 private String total_fee; //交易类型 private String trade_type; //终端IP private String spbill_create_ip; //用户标识 private String openid; //签名 private String sign; //预支付id private String prepay_id; //签名类型:MD5 private String signType; //时间戳 private String timeStamp; //微信支付时用到的prepay_id private String packageStr; private String return_code; private String return_msg; private String result_code; private String bank_type; private Integer cash_fee; private String fee_type; private String is_subscribe; private String time_end; //微信支付订单号 private String transaction_id; private String ip; private Integer coupon_count; private Integer coupon_fee; private Integer coupon_fee_0; private String coupon_type_0; private String coupon_id_0; public String getCoupon_type_0() { return coupon_type_0; } public void setCoupon_type_0(String coupon_type_0) { this.coupon_type_0 = coupon_type_0; } public String getCoupon_id_0() { return coupon_id_0; } public void setCoupon_id_0(String coupon_id_0) { this.coupon_id_0 = coupon_id_0; } public Integer getCoupon_fee_0() { return coupon_fee_0; } public void setCoupon_fee_0(Integer coupon_fee_0) { this.coupon_fee_0 = coupon_fee_0; } public Integer getCoupon_fee() { return coupon_fee; } public void setCoupon_fee(Integer coupon_fee) { this.coupon_fee = coupon_fee; } public Integer getCoupon_count() { return coupon_count; } public void setCoupon_count(Integer coupon_count) { this.coupon_count = coupon_count; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public String getBank_type() { return bank_type; } public void setBank_type(String bank_type) { this.bank_type = bank_type; } public Integer getCash_fee() { return cash_fee; } public void setCash_fee(Integer cash_fee) { this.cash_fee = cash_fee; } public String getFee_type() { return fee_type; } public void setFee_type(String fee_type) { this.fee_type = fee_type; } public String getIs_subscribe() { return is_subscribe; } public void setIs_subscribe(String is_subscribe) { this.is_subscribe = is_subscribe; } public String getTime_end() { return time_end; } public void setTime_end(String time_end) { this.time_end = time_end; } public String getTransaction_id() { return transaction_id; } public void setTransaction_id(String transaction_id) { this.transaction_id = transaction_id; } public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getAttach() { return attach; } public void setAttach(String attach) { this.attach = attach; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getMch_id() { return mch_id; } public void setMch_id(String mch_id) { this.mch_id = mch_id; } public String getNonce_str() { return nonce_str; } public void setNonce_str(String nonce_str) { this.nonce_str = nonce_str; } public String getNotify_url() { return notify_url; } public void setNotify_url(String notify_url) { this.notify_url = notify_url; } public String getOut_trade_no() { return out_trade_no; } public void setOut_trade_no(String out_trade_no) { this.out_trade_no = out_trade_no; } public String getTotal_fee() { return total_fee; } public void setTotal_fee(String total_fee) { this.total_fee = total_fee; } public String getTrade_type() { return trade_type; } public void setTrade_type(String trade_type) { this.trade_type = trade_type; } public String getSpbill_create_ip() { return spbill_create_ip; } public void setSpbill_create_ip(String spbill_create_ip) { this.spbill_create_ip = spbill_create_ip; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getReturn_code() { return return_code; } public void setReturn_code(String return_code) { this.return_code = return_code; } public String getReturn_msg() { return return_msg; } public void setReturn_msg(String return_msg) { this.return_msg = return_msg; } public String getResult_code() { return result_code; } public void setResult_code(String result_code) { this.result_code = result_code; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public String getPrepay_id() { return prepay_id; } public void setPrepay_id(String prepay_id) { this.prepay_id = prepay_id; } public String getSignType() { return signType; } public void setSignType(String signType) { this.signType = signType; } public String getTimeStamp() { return timeStamp; } public void setTimeStamp(String timeStamp) { this.timeStamp = timeStamp; } public String getPackageStr() { return packageStr; } public void setPackageStr(String packageStr) { this.packageStr = packageStr; } @Override public String toString() { return "WxPaySendData [appid=" + appid + ", attach=" + attach + ", body=" + body + ", mch_id=" + mch_id + ", nonce_str=" + nonce_str + ", notify_url=" + notify_url + ", out_trade_no=" + out_trade_no + ", total_fee=" + total_fee + ", trade_type=" + trade_type + ", spbill_create_ip=" + spbill_create_ip + ", openid=" + openid + ", sign=" + sign + ", prepay_id=" + prepay_id + ", signType=" + signType + ", timeStamp=" + timeStamp + ", packageStr=" + packageStr + ", return_code=" + return_code + ", return_msg=" + return_msg + ", result_code=" + result_code + ", bank_type=" + bank_type + ", cash_fee=" + cash_fee + ", fee_type=" + fee_type + ", is_subscribe=" + is_subscribe + ", time_end=" + time_end + ", transaction_id=" + transaction_id + ", ip=" + ip + ", coupon_count=" + coupon_count + ", coupon_fee=" + coupon_fee + ", coupon_fee_0=" + coupon_fee_0 + ", coupon_type_0=" + coupon_type_0 + ", coupon_id_0=" + coupon_id_0 + "]"; } }
MD5Util:
package com.util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * md5加密算法实现 */ public class MD5Util { private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5","6", "7", "8", "9", "a", "b", "c", "d", "e", "f" }; /** * md5加密 * * @param text 需要加密的文本 * @return */ public static String encode(String text) { try {// aes rsa MessageDigest md = MessageDigest.getInstance("MD5"); byte[] result = md.digest(text.getBytes()); // 对文本进行加密 // b // 000000..0000011111111 StringBuilder sb = new StringBuilder(); for (byte b : result) { int i = b & 0xff ; // 取字节的后八位有效值 String s = Integer.toHexString(i); if (s.length() < 2) { s = "0" + s; } sb.append(s); } // 加密的结果 return sb.toString(); } catch (NoSuchAlgorithmException e) { // 找不到该算法,一般不会进入这里 e.printStackTrace(); } return ""; } public static String MD5Encode(String origin, String charsetname) { String resultString = null; try { resultString = new String(origin); MessageDigest md = MessageDigest.getInstance("MD5"); if (charsetname == null || "".equals(charsetname)) resultString = byteArrayToHexString(md.digest(resultString.getBytes())); else resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname))); } catch (Exception exception) { } return resultString; } private static String byteArrayToHexString(byte b[]) { StringBuffer resultSb = new StringBuffer(); for (int i = 0; i < b.length; i++) resultSb.append(byteToHexString(b[i])); return resultSb.toString(); } private static String byteToHexString(byte b) { int n = b; if (n < 0) n += 256; int d1 = n / 16; int d2 = n % 16; return hexDigits[d1] + hexDigits[d2]; } }
WXPayUtil:
package com.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; import java.util.Random; import java.util.SortedMap; import java.util.TreeMap; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import org.dom4j.DocumentException; public class WXPayUtil { /** * 统一下单 * 获得PrePayId * @param body 商品或支付单简要描述 * @param out_trade_no 商户系统内部的订单号,32个字符内、可包含字母 * @param total_fee 订单总金额,单位为分 * @param IP APP和网页支付提交用户端ip * @param notify_url 接收微信支付异步通知回调地址 * @param openid 用户openId * @throws IOException * @throws DocumentException */ public static Map unifiedorder(String nonce_str,String body,String out_trade_no,String total_fee,String ip,String notify_url,String openId)throws IOException, DocumentException { /** * 设置访问路径 */ HttpPost httppost = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder"); //String nonce_str = getNonceStr();//随机数据 SortedMap<Object,Object> parameters = new TreeMap<Object,Object>(); /** * 组装请求参数 * 按照ASCII排序 */ parameters.put("appid",WXUtil.APPID); parameters.put("body", body); parameters.put("mch_id", WXUtil.MCH_ID ); parameters.put("nonce_str", nonce_str); parameters.put("out_trade_no", out_trade_no); parameters.put("notify_url", notify_url); parameters.put("spbill_create_ip", ip); parameters.put("total_fee",total_fee ); parameters.put("trade_type",WXUtil.TRADE_TYPE ); parameters.put("openid", openId); String sign = WXSignUtil.createSign(parameters, WXUtil.KEY); /** * 组装XML */ StringBuilder sb = new StringBuilder(""); sb.append("<xml>"); setXmlKV(sb,"appid",WXUtil.APPID); setXmlKV(sb,"body",body); setXmlKV(sb,"mch_id",WXUtil.MCH_ID); setXmlKV(sb,"nonce_str",nonce_str); setXmlKV(sb,"notify_url",notify_url); setXmlKV(sb,"out_trade_no",out_trade_no); setXmlKV(sb,"spbill_create_ip",ip); setXmlKV(sb,"total_fee",total_fee); setXmlKV(sb,"trade_type",WXUtil.TRADE_TYPE); setXmlKV(sb,"sign",sign); setXmlKV(sb,"openid",openId); sb.append("</xml>"); StringEntity reqEntity = new StringEntity(new String (sb.toString().getBytes("UTF-8"),"ISO8859-1"));//这个处理是为了防止传中文的时候出现签名错误 httppost.setEntity(reqEntity); DefaultHttpClient httpclient = new DefaultHttpClient(); HttpResponse response = httpclient.execute(httppost); String strResult = EntityUtils.toString(response.getEntity(), Charset.forName("utf-8")); return XMLUtil.doXMLParse(strResult); } //获得随机字符串 public static String getNonceStr(){ Random random = new Random(); return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8"); } //插入XML标签 public static StringBuilder setXmlKV(StringBuilder sb,String Key,String value){ sb.append("<"); sb.append(Key); sb.append(">"); sb.append(value); sb.append("</"); sb.append(Key); sb.append(">"); return sb; } //解析XML 获得 PrePayId public static String getPrePayId(String xml){ int start = xml.indexOf("<prepay_id>"); int end = xml.indexOf("</prepay_id>"); if(start < 0 && end < 0){ return null; } return xml.substring(start + "<prepay_id>".length(),end).replace("<![CDATA[","").replace("]]>",""); } //商户订单号 public static String getOut_trade_no(String start,String end){ DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS"); String str=start+df.format(new Date())+end; String regex="\\w{14,32}"; if(!str.matches(regex)){ throw new RuntimeException("订单号格式错误"); } return str; } //时间戳 public static String getTimeStamp() { return String.valueOf(System.currentTimeMillis() / 1000); } //随机4位数字 public static int buildRandom(int length) { int num = 1; double random = Math.random(); if (random < 0.1) { random = random + 0.1; } for (int i = 0; i < length; i++) { num = num * 10; } return (int) (random * num); } public static String inputStream2String(InputStream inStream, String encoding){ String result = null; try { if(inStream != null){ ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte[] tempBytes = new byte[1024]; int count = -1; while((count = inStream.read(tempBytes, 0, 1024)) != -1){ outStream.write(tempBytes, 0, count); } tempBytes = null; outStream.flush(); result = new String(outStream.toByteArray(), encoding); } } catch (Exception e) { result = null; } return result; } /** * 生成length为随机数 * @param length * @return */ public static String getRandomStr(int length) { String val = ""; Random random = new Random(); for (int i = 0; i < length; i++) { // 输出字母还是数字 String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; // 字符串 if ("char".equalsIgnoreCase(charOrNum)) { // 取得大写字母还是小写字母 int choice = random.nextInt(2) % 2 == 0 ? 65 : 97; val += (char) (choice + random.nextInt(26)); } else if ("num".equalsIgnoreCase(charOrNum)) { // 数字 val += String.valueOf(random.nextInt(10)); } } return val; } public static void main(String[] args) throws IOException, DocumentException { // System.out.println(getOut_trade_no("","")); // //openID String openId=""; // // //统一下单 String nonce_str=getNonceStr(); String str="|"; String out_trade_no = WXPayUtil.getOut_trade_no("testpay",WXPayUtil.getRandomStr(6)); Map strResult = WXPayUtil.unifiedorder(nonce_str,"test", out_trade_no, "1", "127.0.0.1", "http://www.baidu.com",openId); System.out.println(strResult); // //Map map=XMLUtil.doXMLParse(strResult); // SortedMap<Object,Object> resultMap=(TreeMap)XMLUtil.doXMLParse(strResult); // System.out.println(resultMap); // String regex="[0-9a-zA-Z\\_\\-\\*\\|@]{1,32}"; // System.out.println(nonce_str.length()); System.out.println(out_trade_no.length()); } }
WXSignUtil:
package com.util; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.SortedMap; import com.job37.jser.core.util.MD5Util; public class WXSignUtil { /** * 创建签名 * @param parameters * @param key * @return */ @SuppressWarnings("rawtypes") public static String createSign(SortedMap<Object,Object> parameters,String key){ StringBuffer sb = new StringBuffer(); Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序) Iterator it = es.iterator(); while(it.hasNext()) { Map.Entry entry = (Map.Entry)it.next(); String k = (String)entry.getKey(); Object v = entry.getValue(); if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + key); String sign = MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase(); return sign; } }
WXUtil:
package com.util; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.json.JSONObject; import com.alibaba.dubbo.common.json.JSON; import com.job37.jser.core.constants.GlobalConstants; import com.job37.jser.core.exceptions.CustomGlobalException; import com.job37.jser.core.util.RedisUtil; import redis.clients.jedis.Jedis; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; public class WXUtil { //服务号ID public static final String APPID=""; //服务号密码 public static final String APPSECRET=""; //上齐猎微信支付商户号 public static final String MCH_ID=""; public static final String TRADE_TYPE="JSAPI"; //api密钥 public static final String KEY=""; //检查微信登录的URL public static final String WXLOGIN_URL=""; //微信支付,送贺礼成功,异步回调的URL public static final String GIFTAGENT_NOTIFY_URL = ""; /*******************************************************************************/ /** * 获取请求用户信息的access_token * * @param code * @return */ public static Map<String, Object> getUserInfoAccessToken(String code) { Map<String, Object> data = new HashMap(); try { String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", APPID, APPSECRET, code); DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet(url); HttpResponse httpResponse = httpClient.execute(httpGet); HttpEntity httpEntity = httpResponse.getEntity(); String tokens = EntityUtils.toString(httpEntity, "utf-8"); data=JSON.parse(tokens,Map.class); } catch (Exception e) { e.printStackTrace(); } return data; } public static Map<String, Object> getAccessToken(String code) { Map<String, Object> data = new HashMap(); try { String appid="";//商户的APPID String appsecret="";//商户的APPSECRET String url = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", appid, appsecret, code); DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet(url); HttpResponse httpResponse = httpClient.execute(httpGet); HttpEntity httpEntity = httpResponse.getEntity(); String tokens = EntityUtils.toString(httpEntity, "utf-8"); data=JSON.parse(tokens,Map.class); } catch (Exception e) { e.printStackTrace(); } return data; } /** * 获取用户信息 * * @param accessToken * @param openId * @return */ public static Map<String, Object> getUserInfo(String accessToken, String openId) { Map<String, Object> data = new HashMap(); String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN"; try { DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet(url); HttpResponse httpResponse = httpClient.execute(httpGet); HttpEntity httpEntity = httpResponse.getEntity(); String responseStr = EntityUtils.toString(httpEntity, "utf-8"); data=JSON.parse(responseStr,Map.class); } catch (Exception ex) { } return data; } /** * 生成用于获取access_token的Code的Url * * @param redirectUrl * @return */ public static String getRequestCodeUrl(String redirectUrl) { return String.format("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect", APPID, redirectUrl, "snsapi_userinfo", "xxxx_state"); } public static Map<String, Object> testGetUserInfo(String accessToken, String openId) { Map<String, Object> data = new HashMap(); String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN"; String url2="https://api.weixin.qq.com/cgi-bin/user/info?access_token="+ accessToken + "&openid=" + openId + "&lang=zh_CN";; try { DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet(url); HttpResponse httpResponse = httpClient.execute(httpGet); HttpEntity httpEntity = httpResponse.getEntity(); String responseStr = EntityUtils.toString(httpEntity, "utf-8"); data=JSON.parse(responseStr,Map.class); } catch (Exception e) { e.printStackTrace(); } return data; } public static String getTicket(String accessToken){ String ticket = null; String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+ accessToken +"&type=jsapi";//这个url链接和参数不能变 try { DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet(url); HttpResponse httpResponse = httpClient.execute(httpGet); HttpEntity httpEntity = httpResponse.getEntity(); String responseStr = EntityUtils.toString(httpEntity, "utf-8"); // System.out.println(responseStr); Map data=JSON.parse(responseStr,Map.class); ticket=data.get("ticket")+""; } catch (Exception e) { e.printStackTrace(); } return ticket; } public static String getAccessToken(){ String token=""; RedisUtil ut=new RedisUtil(); Jedis jedis=ut.getJedis(); try { jedis.select(6); token=jedis.get("ACCESS_TOKEN"); if(token==null||token.trim().equals("")||"null".equals(token)){ token=refreshAccessToken(); jedis.set("ACCESS_TOKEN", token); jedis.expire("ACCESS_TOKEN", 7000); } } catch (Exception e) { throw e; }finally{ ut.returnResource(jedis); } return token; } public static String refreshAccessToken() { String access_token = ""; String grant_type = "client_credential";//获取access_token填写client_credential //这个url链接地址和参数皆不能变 String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type="+grant_type+"&appid="+APPID+"&secret="+APPSECRET; try { DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet(url); HttpResponse httpResponse = httpClient.execute(httpGet); HttpEntity httpEntity = httpResponse.getEntity(); String responseStr = EntityUtils.toString(httpEntity, "utf-8"); //System.out.println(responseStr); Map data=JSON.parse(responseStr,Map.class); access_token=data.get("access_token")+""; } catch (Exception e) { e.printStackTrace(); } return access_token; } /** * 获取微信配置的签名 * @param jsapi_ticket * @param url * @param timeStamp * @param nonce_str * @return */ public static String getSHA1Sign(String jsapi_ticket, String url,String timeStamp,String nonce_str){ String decript = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "×tamp=" + timeStamp + "&url=" + url; return SHA1(decript); } /** * 获取微信配置的签名 * @param url * @param timeStamp * @param nonce_str * @return */ public static String getSHA1Sign( String url,String timeStamp,String nonce_str){ String token=getAccessToken(); String ticket=getTicket(token); String decript = "jsapi_ticket=" + ticket + "&noncestr=" + nonce_str + "×tamp=" + timeStamp + "&url=" + url; return SHA1(decript); } public static String SHA1(String decript) { try { MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1"); digest.update(decript.getBytes()); byte messageDigest[] = digest.digest(); // Create Hex String StringBuffer hexString = new StringBuffer(); // 字节数组转换为 十六进制 数 for (int i = 0; i < messageDigest.length; i++) { String shaHex = Integer.toHexString(messageDigest[i] & 0xFF); if (shaHex.length() < 2) { hexString.append(0); } hexString.append(shaHex); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return ""; } } //获取微信授权登录的URL public static String getLoginRedirectUrl(String redirectUrl){ // String redirect_uri=TEST_WXLOGIN_URL+redirectUrl; String redirect_uri=WXLOGIN_URL+redirectUrl; try { redirect_uri=java.net.URLEncoder.encode(redirect_uri,"utf-8"); } catch (UnsupportedEncodingException e) { throw new CustomGlobalException("参数错误"); } return "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+APPID+"&redirect_uri="+redirect_uri+"&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect"; } /** * 获取完整的URL * @param request * @param midfix 链接的中间字符串 * @return */ public static String getWholeURL(HttpServletRequest request,String midfix){ return GlobalConstants.SHARE_URL+midfix+"?" + request.getQueryString(); } }
XMLUtil:
package com.util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; public class XMLUtil { /** * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 * @param strxml * @return * @throws JDOMException * @throws IOException * @throws DocumentException */ public static Map doXMLParse(String strxml) throws IOException, DocumentException { strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\""); if(null == strxml || "".equals(strxml)) { return null; } Map m = new TreeMap(); InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8")); SAXReader builder = new SAXReader(); Document doc = builder.read(in); Element root = doc.getRootElement(); Iterator it = root.elementIterator(); while(it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = e.getText(); // List children = e.getChildren(); // if(children.isEmpty()) { // v = e.getTextNormalize(); // } else { // v = XMLUtil.getChildrenText(children); // } m.put(k, v); } //关闭流 in.close(); return m; } /** * 获取子结点的xml * @param children * @return String * @throws DocumentException * @throws IOException */ //public static String getChildrenText(List children) { // StringBuffer sb = new StringBuffer(); // if(!children.isEmpty()) { // Iterator it = children.iterator(); // while(it.hasNext()) { // Element e = (Element) it.next(); // String name = e.getName(); // String value = e.getTextNormalize(); // List list = e.getChildren(); // sb.append("<" + name + ">"); // if(!list.isEmpty()) { // sb.append(XMLUtil.getChildrenText(list)); // } // sb.append(value); // sb.append("</" + name + ">"); // } // } // // return sb.toString(); //} public static void main(String[] args) throws IOException, DocumentException { String xml="<xml><id>555</id>"+ "<name>hello</name></xml>"; Map<String,Object>m=XMLUtil.doXMLParse(xml); System.out.println(m); } }
五、H5页面调起微信支付的js
1)要调用微信的支付js,必须在页面引入<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>,并且配置微信支付
微信js配置(wx.js)如下:
var paySign ;
var appId;
var timeStamp ;
var nonceStr ;
var packageStr ;
var signType ;
var signature;
function wxPay(){
//console.log(signature);
//通过config接口注入权限验证配置
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: appId, // 必填,公众号的唯一标识
timestamp:timeStamp , // 必填,生成签名的时间戳
nonceStr: nonceStr, // 必填,生成签名的随机串
signature: signature,// 必填,签名,见附录1
jsApiList: [‘chooseWXPay‘] // 必填,需要使用的JS接口列表,这里只写支付的
});
//通过ready接口处理成功验证
wx.ready(function(){
//wx.hideOptionMenu();//隐藏右边的一些菜单
wx.chooseWXPay({
timestamp: timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: nonceStr, // 支付签名随机串,不长于 32 位
package: packageStr, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
signType: signType, // 签名方式,默认为‘SHA1‘,使用新版支付需传入‘MD5‘
paySign: paySign, // 支付签名
success: function (res) {
// 支付成功后的回调函数
layer.closeAll();
alert("支付成功");
},
//如果你按照正常的jQuery逻辑,下面如果发送错误,一定是error,那你就太天真了,当然,jssdk文档中也有提到
fail: function(res) {
//接口调用失败时执行的回调函数。
layer.closeAll();
alert("支付失败,请稍后再试");
},
complete: function(res) {
//接口调用完成时执行的回调函数,无论成功或失败都会执行。
//alert("complete");
},
cancel: function(res) {
//用户点击取消时的回调函数,仅部分有用户取消操作的api才会用到。
//alert("cancel");
},
trigger: function(res) {
//监听Menu中的按钮点击时触发的方法,该方法仅支持Menu中的相关接口。
//alert("trigger");
}
});
});
}
function testPay(){
//调用微信支付
//callpay();
var url=‘../../../guest/WXResource/getPrepayid‘;
var giftMoney=$("#giftMoney").val();
var param={giftMoney:giftMoney,identityId:identityId};
$.post(url,param,function(res){
var code=res.code;
var data=res.data;
if(code==1){
paySign=data.sign ;
appId=data.appid;
timeStamp=data.timeStamp ;
nonceStr=data.nonce_str ;
packageStr=data.packageStr ;
signType=data.signType ;
signature=res.signature;
wxPay();
//callpay();
}else{
layer.open({
content:res.msg
});
}
});
}
配置好后,在页面写个点击事件,调用testPay函数,就会初始化微信支付的配置,随后调起js支付。
六、异步回调
支付成功后,要在异步回调的URL(这个在统一下单中就定义了notify_url)中处理相应的业务
示例回调代码:
/**
* 支付回调接口
* @param request
* @return
*/
@Path("payCallback")
@POST
public void callBack(){
// System.out.println("gift_payCallBack...");
response.setContentType("text/xml;charset=UTF-8");
try {
InputStream is = request.getInputStream();
String resultStr = IOUtils.toString(is, "UTF-8");
if("".equals(resultStr)){
System.out.println("fail:result is null");
response.getWriter().write("<xm><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[参数错误!]]></return_msg></xml>");
return ;
}
//解析xml
SortedMap<Object,Object> resultMap=(TreeMap)XMLUtil.doXMLParse(resultStr);
// System.out.println(resultMap);
String sign = resultMap.get("sign")+"";
String sign2 = WXSignUtil.createSign(resultMap, WXUtil.KEY);
String result_code=resultMap.get("result_code")+"";
String return_code=resultMap.get("return_code")+"";
if(sign.equals(sign2)){//校验签名,两者需要一致,防止别人绕过支付操作,不付钱直接调用你的业务,不然,哈哈,你老板会很开心的 233333.。。。
if(return_code.equals("SUCCESS") && result_code.equals("SUCCESS")){
//业务逻辑(先判断数据库中订单号是否存在,并且订单状态为未支付状态)
try {
//resultMap.put("openid", request.getSession().getAttribute("openid"));
} catch (Exception e) {
e.printStackTrace();
}
//request.setAttribute("out_trade_no", out_trade_no);
//通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
}else{
response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[交易失败]]></return_msg></xml>");
}
}else{
//通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名校验失败]]></return_msg></xml>");
}
response.getWriter().flush();
response.getWriter().close();
return;
} catch (Exception e) {
e.printStackTrace();
}
}