最近做了个微信公众号的项目,有涉及到微信公众号支付相关的模块,但是微信上的DEMO都是PHP的,看得不太懂,摸索了几天终于了解了。。总得来说还算挺容易的。
前期工作:
注意:测试目录必须要精确到调用微信支付JS页面的包,且测试目录不能与正式目录相同。
一、页面请求调用微信的统一下单接口,获得prepay_id返回页面。
/** * 支付方法 * @param fee 订单金额 * @param request * @return */
@RequestMapping("wxPay")
@ResponseBody
public String wxPay(double fee,HttpServletRequest request){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
Date date = new Date();
if(fee < 0){
return "-3";
}
CgUser payUser = (CgUser) request.getSession().getAttribute("userinfo");
if(payUser == null ){
return "-2";//未登录
}
try {
//生成订单号 :年月日+6位随机数
String order_no = sdf.format(date)+""+((int)(Math.random()*99999+100000));
//用户ip
String user_ip = getIpAddr(request);
//创建随机数
String nonceStr = WxSign.getNonceStr();
//统一订单发送参数
WxPaySendData data = new WxPaySendData();
data.setAppid(Constants.APPID);
data.setAttach("上海灵柚网络科技有限公司");
data.setBody("微信公众号充值");
data.setMch_id(Constants.MCH_ID);
data.setNonce_str(nonceStr);
data.setNotify_url(Constants.PAY_NOTIFY_URL);
data.setOut_trade_no(order_no);
data.setTotal_fee((int)(fee*100));//单位:分
data.setTrade_type("JSAPI");
data.setSpbill_create_ip(user_ip);
data.setOpenid(payUser.getWechatId());
data.setTime_start(sdf.format(date));
String result = unifiedorder(data, Constants.WX_PAY_KEY);
XStream xstream = new XStream(new DomDriver());
xstream.alias("xml", WxPayResultData.class);
WxPayResultData resultData = (WxPayResultData) xstream.fromXML(result);
log.info("获取统一订单返回结果 result_code:"+resultData.getResult_code()+"---return_code:"+resultData.getReturn_code()+"-----return_msg:"+resultData.getReturn_msg());
if(resultData.getResult_code().equals("SUCCESS") && resultData.getReturn_code().equals("SUCCESS")){
//TODO 保存在充值记录表中
//生成sign
SortedMap<Object,Object> signMap = new TreeMap<Object,Object>();
signMap.put("appId", resultData.getAppid());
signMap.put("timeStamp", WxSign.getTimeStamp());
signMap.put("nonceStr", resultData.getNonce_str());
signMap.put("packageValue", "prepay_id="+resultData.getPrepay_id());
signMap.put("signType", "MD5");
String paySin = WxSign.createSign(signMap,Constants.WX_PAY_KEY);
log.info("生成的签名PaySIGN:"+paySin);
signMap.put("paySign", paySin);
log.info("返回页面参数json:"+new Gson().toJson(signMap));
return new Gson().toJson(signMap);
}else{
return "-1";
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
log.error("微信支付错误",e);
return "-1";
}
}
/** * 调用统一下单 * @param data * @param key * @return */
public String unifiedorder(WxPaySendData data,String key){
//统一下单支付
String returnXml = null;
try {
//生成sign签名
SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
parameters.put("appid", data.getAppid());
parameters.put("attach", data.getAttach());
parameters.put("body", data.getBody());
parameters.put("mch_id", data.getMch_id());
parameters.put("nonce_str", data.getNonce_str());
parameters.put("notify_url", data.getNotify_url());
parameters.put("out_trade_no", data.getOut_trade_no());
parameters.put("total_fee", data.getTotal_fee());
parameters.put("trade_type", data.getTrade_type());
parameters.put("spbill_create_ip", data.getSpbill_create_ip());
parameters.put("openid", data.getOpenid());
parameters.put("device_info", data.getDevice_info());
parameters.put("time_start", data.getTime_start());
log.info("SIGN:"+WxSign.createSign(parameters,key));
data.setSign(WxSign.createSign(parameters,key));
XStream xs = new XStream(new DomDriver("UTF-8",new XmlFriendlyNameCoder("-_", "_")));
xs.alias("xml", WxPaySendData.class);
String xml = xs.toXML(data);
log.info("统一下单xml为:\n" + xml);
// HttpClientUtil util = HttpClientUtil.getInstance();
returnXml = WechatUtil.httpRequest("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xml);
// returnXml = util.doPostForString("https://api.mch.weixin.qq.com/pay/unifiedorder", null, xml);
log.info("统一下单返回结果:" + returnXml);
} catch (Exception e) {
e.printStackTrace();
log.error("调用统一下单方法错误",e);
}
return returnXml;
}
二、通过页面JS请求微信支付接口(注:只能在微信自带的浏览器使用)
function wxpay(){
$.ajax({
type:"post",
url: "wp/wxPay",
data:{fee:$(".recmoney").val()},
dataType:"json",
success:function(obj){
if(obj != "-1"){
WeixinJSBridge.invoke('getBrandWCPayRequest',{
"appId" : obj.appId, //公众号名称,由商户传入
"timeStamp":obj.timeStamp, //时间戳,自 1970 年以来的秒数
"nonceStr" : obj.nonceStr, //随机串
"package" : obj.packageValue, //商品包信息
"signType" : obj.signType, //微信签名方式:
"paySign" : obj.paySign //微信签名
},function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
alert("充值成功");
location.href="XXX";
}else{
alert("支付失败");
rechargeBtn.attr("disable",false);
rechargeBtn.css("background-color","#00a895");
}
});
}
}
});
}
这里我是做了个按钮,点击请求调用统一下单接口,返回页面后直接调用支付JS。这样就无需用户再次点击充值了。当然也可以分成两步来做。
三、接收微信支付结果回调
/** * 微信支付异步通知回调 * @return */
@RequestMapping("wxPayNotify")
@ResponseBody
public String wxPayNotify(HttpSession session,HttpServletRequest request){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
try {
//解析xml参数
Map<String, String> requestMap = MessageUtil.parseXml(request);
SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
log.info("request参数:");
for (Entry<String, String> iterable_element : requestMap.entrySet()) {
if(!iterable_element.getKey().equals("sign")){
parameters.put(iterable_element.getKey(), iterable_element.getValue());
}
log.info("key:"+iterable_element.getKey()+"---value:"+iterable_element.getValue());
}
if(!requestMap.get("return_code").equals("SUCCESS")){
log.info("微信支付通知返回错误信息:"+requestMap.get("return_msg"));
return notifyReturnXML("SUCCESS", "");
}
//生成签名
String my_sign = WxSign.createSign(parameters, Constants.WX_PAY_KEY);
//获得微信的签名
String wx_sign = requestMap.get("sign");
//验证签名
if(!my_sign.equalsIgnoreCase(wx_sign)){
log.info("签名验证失败");
return "FAIL";
}
//找到充值订单
String hql = "from CgRechargeRecord crr where crr.orderNo = ?";
CgRechargeRecord rechargeRecord = rechargeRecordService.findEntity(hql, new Object[]{requestMap.get("out_trade_no")});
log.info("获取订单中userid:"+rechargeRecord.getUserId());
//获取用户
CgUser user = userService.findEntity("from CgUser cu where cu.id = ?", new Object[]{rechargeRecord.getUserId()});
log.info("获得充值用户名称:"+user.getName());
//判断订单是否成功
if(requestMap.get("result_code").equals("SUCCESS")){
//TODO 业务逻辑
return notifyReturnXML("SUCCESS", "");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
log.error("接收微信支付回调出现错误",e);
return "FAIL";
}
}
到此微信公众号支付流程完成。
下面提供一些用到的工具类:
/**
* 微信支付回调接口返回参数
* @param return_code
* @param return_msg
* @return
*/
public static String notifyReturnXML(String return_code, String return_msg) {
return "<xml><return_code><![CDATA[" + return_code + "]]></return_code><return_msg><![CDATA[" + return_msg + "]]></return_msg></xml>";
}
https请求类:
/** * 发起https请求并获取结果 * * @param requestUrl 请求地址 * @param requestMethod 请求方式(GET、POST) * @param outputStr 提交的数据 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) */
public static String httpRequest(String requestUrl , String requestMethod , String outputStr) {
StringBuffer buffer = new StringBuffer();
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL" , "SunJSSE");
sslContext.init(null , tm , new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
// 设置请求方式(GET/POST)
httpUrlConn.setRequestMethod(requestMethod);
if ("GET".equalsIgnoreCase(requestMethod))httpUrlConn.connect();
// 当有数据需要提交时
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
// 注意编码格式,防止中文乱码
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 将返回的输入流转换成字符串
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream , "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
// 释放资源
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
// jsonObject = JSONObject.fromObject(buffer.toString());
return buffer.toString();
} catch (ConnectException ce) {
log.info("请求超时",ce);
System.out.println("请求超时");
} catch (Exception e) {
System.out.println("请求错误");
log.info("请求错误",e);
}
// return jsonObject;
return null;
}
微信支付参数类:
package com.wx.common.entity.weixin;
/** * 微信统一订单发送参数 * @author Administrator * */
public class WxPaySendData {
private String appid;//微信分配的公众账号ID(企业号corpid即为此appId)
private String mch_id;//微信支付分配的商户号
private String device_info;//终端设备号(门店号或收银设备ID),注意:PC网页或公众号内支付请传"WEB"
private String nonce_str;//随机字符串,不长于32位
private String sign;//签名
private String body;//商品或支付单简要描述
private String detail;//商品名称明细列表
private String attach;//附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
private String out_trade_no;//商户系统内部的订单号,32个字符内、可包含字母
private String fee_type;//符合ISO 4217标准的三位字母代码,默认人民币:CNY
private Integer total_fee;//订单总金额
private String spbill_create_ip;//APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。
private String time_start;//订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010
private String time_expire;//订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010
private String goods_tag;//商品标记,代金券或立减优惠功能的参数
private String notify_url;//接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。
private String trade_type;//交易类型,取值如下:JSAPI,NATIVE,APP
private String product_id;//trade_type=NATIVE,此参数必传。此id为二维码中包含的商品ID,商户自行定义。
private String limit_pay;//no_credit--指定不能使用信用卡支付
private String openid;//trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getMch_id() {
return mch_id;
}
public void setMch_id(String mch_id) {
this.mch_id = mch_id;
}
public String getDevice_info() {
return device_info;
}
public void setDevice_info(String device_info) {
this.device_info = device_info;
}
public String getNonce_str() {
return nonce_str;
}
public void setNonce_str(String nonce_str) {
this.nonce_str = nonce_str;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
public String getAttach() {
return attach;
}
public void setAttach(String attach) {
this.attach = attach;
}
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 getFee_type() {
return fee_type;
}
public void setFee_type(String fee_type) {
this.fee_type = fee_type;
}
public Integer getTotal_fee() {
return total_fee;
}
public void setTotal_fee(Integer total_fee) {
this.total_fee = total_fee;
}
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 getTime_start() {
return time_start;
}
public void setTime_start(String time_start) {
this.time_start = time_start;
}
public String getTime_expire() {
return time_expire;
}
public void setTime_expire(String time_expire) {
this.time_expire = time_expire;
}
public String getGoods_tag() {
return goods_tag;
}
public void setGoods_tag(String goods_tag) {
this.goods_tag = goods_tag;
}
public String getNotify_url() {
return notify_url;
}
public void setNotify_url(String notify_url) {
this.notify_url = notify_url;
}
public String getTrade_type() {
return trade_type;
}
public void setTrade_type(String trade_type) {
this.trade_type = trade_type;
}
public String getProduct_id() {
return product_id;
}
public void setProduct_id(String product_id) {
this.product_id = product_id;
}
public String getLimit_pay() {
return limit_pay;
}
public void setLimit_pay(String limit_pay) {
this.limit_pay = limit_pay;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
}
解析微信请求XML参数类:
/** * 解析微信发来的请求(XML) * * @param request * @return * @throws Exception */
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList)
map.put(e.getName(), e.getText());
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
签名生成方法:
public class WxSign {
private static String characterEncoding = "UTF-8";
@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();
System.out.println(sb.toString());
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + key);
System.out.println(sb.toString());
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
public static String getNonceStr() {
Random random = new Random();
return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8");
}
public static String getTimeStamp() {
return String.valueOf(System.currentTimeMillis() / 1000);
}
}
还有一些人在纠结微信输入支付密码的页面从哪里来,在此说明一下:调用了微信支付的JS之后,微信会自动生成密码输入页面