微信支付作为三大支付之一,越来越多的客户要求产品中添加微信支付 但是网上能找到可用的demo很少
所以写一篇自己写微信支付的过程,希望能给有需要的开发者一点帮助。
下面让我们来进入正题
1准备工作
首先得去微信开放平台申请app支付权限 当申请成功后会收到一封邮件 这里面有个商店号 MCH_ID 和PARTNER_ID 注意这俩 其实是一样的,都是商店号,但是
通过上图我们可以看到 不同接口 对应的字段并不一样 (有意思吗 就不能统一一下)但其实都是一个商店号
然后我们还需要拿到一些key值 下面代码有介绍 就不一一说来源了
拿到这些值之后我们就可以进行下一步了
2写支付逻辑
我写的代码如下 关键点都加上了注解
@Api(tags = {"支付接口"})
@RestController@RequestMapping(value = "weixinMobile")public class WeixinMobilePayController { private static final Logger logger = LoggerFactory.getLogger(WeixinMobilePayController.class);@RequestMapping(value="dop",method=RequestMethod.POST) public JsonBack dopay(HttpServletRequest request, JsonBack jsonBack,HttpServletResponse response,String membertoken,int num,String id) throws Exception { Member mem= MemberServiceImp.loginMemberMap.get("token"); if(mem.getMemberToken().equals(membertoken)){ MemberLevel level = memberLevelService.getMemberLevelById(id); int x = Integer.parseInt(level.getLevelMoney()); int cc=x * num*100;//微信以分为单位,如果数据库里面的价格没扩大100的话这里要乘以100 String total_fee =cc+""; String body=ConstantUtil.BODY; String mch_id=ConstantUtil.MCH_ID; String currTime = PayCommonUtil.getCurrTime(); String strTime = currTime.substring(8, currTime.length()); String strRandom = PayCommonUtil.buildRandom(4) + ""; String nonce_str = strTime + strRandom; String notify_url=ConstantUtil.NOTIFY_URL;//回调地址 必须能直接访问 不是二级域名也可以 String out_trade_no=String.valueOf(UUID.next()); // 订单号 String timestamp=WXUtil.getTimeStamp(); SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); packageParams.put("appid",ConstantUtil.APP_ID); packageParams.put("mch_id",mch_id); packageParams.put("nonce_str",nonce_str); packageParams.put("body",body);// 商品描述 packageParams.put("out_trade_no", out_trade_no);// 商户订单号 packageParams.put("total_fee", total_fee);// 总金额 String addr = AddressUtils.getIpAddr(request); packageParams.put("spbill_create_ip", addr);// 发起人IP地址 packageParams.put("notify_url", notify_url);// 回调地址 packageParams.put("trade_type", "APP");// 交易类型 packageParams.put("time_start",timestamp); String sign = PayCommonUtil.createSign("UTF-8", packageParams,ConstantUtil.APP_KEY); packageParams.put("sign", sign);// 签名 String requestXML = PayCommonUtil.getRequestXml(packageParams); String resXml = HttpUtil.postData(ConstantUtil.NOTIFY_URL, requestXML); Map map = XMLUtil.doXMLParse(resXml); String returnCode = (String) map.get("return_code"); String returnMsg = (String) map.get("return_msg"); logger.info("result:"+returnMsg); if("SUCCESS".equals(returnCode)){ String resultCode = (String) map.get("result_code"); String prepay_id = (String) map.get("prepay_id"); String noncestr=(String) map.get("nonce_str"); if("SUCCESS".equals(resultCode)){ System.out.println("获取prepay_id成功"+prepay_id);//必须获取到这个prepay_id才算微信认可了你的第一次签名//这里写预下单业务逻辑 SortedMap<Object, Object> packageParam = new TreeMap<Object, Object>();// ConfigUtil.commonParams(packageParams); packageParam.put("appid",ConstantUtil.APP_ID); packageParam.put("partnerid",mch_id); packageParam.put("noncestr",noncestr); packageParam.put("prepayid",prepay_id);// 商品描述 packageParam.put("package", "Sign=WXPay");// 商户订单号 packageParam.put("timestamp",timestamp); String sign1 = PayCommonUtil.createSign("UTF-8", packageParam,ConstantUtil.APP_KEY);//这里是二次签名 前台要拿到去调起微信支付,如果这个错了的话会在前台报签名错误 map.put("partnerid", mch_id); map.put("timestamp", timestamp); map.put("package","Sign=WXPay"); map.put("retcode", "0"); map.put("sign", sign1); jsonBack = new JsonBack(true, "success", map); }} }else{ jsonBack = new JsonBack(false, "token不一致", null); } return jsonBack; }
3 写回调逻辑
微信会根据你的回调地址去请求回调的action 并且地址必须是网络地址 能直接访问 而且不能携带参数
代码如下
@RequestMapping(value="WXP",method=RequestMethod.POST)
public void WXPayBack(HttpServletRequest request, HttpServletResponse response){ String resXml = ""; Member mem= MemberServiceImp.loginMemberMap.get("token"); try { //解析XML Map<String, String> map = MobileUtil.parseXml(request); String return_code = map.get("return_code");//状态 String out_trade_no = map.get("out_trade_no");//订单号 if (return_code.equals("SUCCESS")) { if (out_trade_no != null) { //处理订单逻辑这里面可以根据订单号来查询到付款买的什么
然后在预订单里加一个是否付款的状态来限制只能执行一次业务逻辑
否则就需要返回给微信一个success
resXml = "<xml><return_code>SUCCESS</return_code><return_msg>OK</return_msg></xml>"; } catch (Exception e1) { // TODO Auto-generated catch block logger.warn("新增服务失败"); } } else{ logger.warn("该用户已经买过金卡"); } }else{ logger.info("已经购买会员完成"); } }else{ //logger.warn("[payPage]根据id查询等级没有查询到记录,levelId="+levelId); } }else{ logger.info("callback fail:{}",out_trade_no); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> "; } } catch (Exception e) { logger.error("callback fail",e); resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> "; } }4 工具类
上面用到的工具类如下
package com.zhjt.health.testpay;public class ConstantUtil { /** * 微信开发平台应用ID 下面参数都不完整 需要写入自己的参数 */ public static final String APP_ID="wx60e8c9b5b7"; /** * 应用对应的凭证 */ public static final String APP_SECRET="85cae8f9"; /** * 应用对应的密钥 */ public static final String APP_KEY="wffgg"; /** * 微信支付商户号 1500906271 */ public static final String MCH_ID="271"; /** * 商品描述 */ public static final String BODY="yunyi"; /** * 商户号对应的密钥 */ public static final String PARTNER_key="2121."; /** * 商户id */ public static final String PARTNER_ID="1"; /** * 常量固定值 */ public static final String GRANT_TYPE="client_credential"; /** * 获取预支付id的接口url */ public static String GATEURL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; /** * 微信服务器回调通知url */ public static String NOTIFY_URL="http://106.14.15.78:8090/weck"; public static String NOTIFY_URL2="http://106.14.15.78:8090/wec"; public static String NOTIFY_URL3="http://106.14.15.78:8090/wei;}
package com.zhjt.health.util.pay;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.net.URL;import java.net.URLConnection;/** * http请求(这里用户获取订单url生成二维码) * 创建者 科帮网 * 创建时间 2017年7月31日 * */public class HttpUtil { private final static int CONNECT_TIMEOUT = 5000; // in milliseconds private final static String DEFAULT_ENCODING = "UTF-8"; public static String postData(String urlStr, String data) { String postData = postData2(urlStr, data, null); System.out.println(postData+"测试postDate"); return postData; } public static String postData(String urlStr, String data, String contentType) { BufferedReader reader = null; System.out.println("跳进来了"); try { URL url = new URL(urlStr); System.out.println(url+"测试URL"); URLConnection conn = url.openConnection(); conn.setDoOutput(true); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(CONNECT_TIMEOUT); if (contentType != null) conn.setRequestProperty("content-type", contentType); OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING); if (data == null) data = ""; writer.write(data); writer.flush(); writer.close(); reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING)); StringBuilder sb = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line); sb.append("\r\n"); } return sb.toString(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (reader != null) reader.close(); } catch (IOException e) { } } return null; } public static String postData2(String urlStr, String data, String contentType) { BufferedReader reader = null; try { URL url = new URL(urlStr); URLConnection conn = url.openConnection(); conn.setDoOutput(true); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(CONNECT_TIMEOUT); if(contentType != null) conn.setRequestProperty("content-type", contentType); OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING); if(data == null) data = ""; writer.write(data); writer.flush(); writer.close(); reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING)); StringBuilder sb = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line); sb.append("\r\n"); } return sb.toString(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (reader != null) reader.close(); } catch (IOException e) { } } return null; } }
package com.zhjt.health.util.pay;import java.text.SimpleDateFormat;import java.util.Date;import java.util.Iterator;import java.util.Map;import java.util.Set;import java.util.SortedMap;public class PayCommonUtil { /** * 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。 * @Author 科帮网 * @param characterEncoding * @param packageParams * @param API_KEY * @return boolean * @Date 2017年7月31日 * 更新日志 * 2017年7月31日 科帮网 首次创建 * */ @SuppressWarnings({ "rawtypes"}) public static boolean isTenpaySign(String characterEncoding, SortedMap
package com.zhjt.health.util;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStream;import java.util.HashMap;import java.util.Iterator;import java.util.List;import java.util.Map;import org.jdom.Document;import org.jdom.Element;import org.jdom.JDOMException;import org.jdom.input.SAXBuilder;/** * XML解析 * 创建者 科帮网 * 创建时间 2017年7月31日 * */public class XMLUtil { /** * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。 * * @param strxml * @return * @throws JDOMException * @throws IOException */ @SuppressWarnings({ "rawtypes", "unchecked" }) public static Map doXMLParse(String strxml) throws JDOMException, IOException { strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\""); if (null == strxml || "".equals(strxml)) { return null; } Map m = new HashMap(); InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8")); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); Element root = doc.getRootElement(); List list = root.getChildren(); Iterator it = list.iterator(); while (it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = ""; 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 */ @SuppressWarnings({ "rawtypes" }) 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(" "); } } return sb.toString(); }}
package com.zhjt.health.util.pay;import java.io.InputStream;import java.net.URL;import java.util.HashMap;import java.util.List;import java.util.Map;import javax.servlet.http.HttpServletRequest;import org.dom4j.Document;import org.dom4j.Element;import org.dom4j.io.SAXReader;import com.google.gson.Gson;/** * 微信H5支付工具类 * 创建者 科帮网 * 创建时间 2017年7月31日 */public class MobileUtil { /** * 获取用户openID * @Author 科帮网 * @param code * @return String * @Date 2017年7月31日 * 更新日志 * 2017年7月31日 科帮网 首次创建 * */ public static String getOpenId(String code){ if (code != null) { String url = "https://api.weixin.qq.com/sns/oauth2/access_token?" + "appid="+ ConfigUtil.APP_ID + "&secret="+ ConfigUtil.APP_SECRET + "&code=" +code + "&grant_type=authorization_code"; String returnData = getReturnData(url); Gson gson = new Gson(); OpenIdClass openIdClass = gson.fromJson(returnData, OpenIdClass.class); if (openIdClass.getOpenid() != null) { return openIdClass.getOpenid(); } } return "**************"; } public static String getReturnData(String urlString) { String res = ""; try { URL url = new URL(urlString); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url .openConnection(); conn.connect(); java.io.BufferedReader in = new java.io.BufferedReader( new java.io.InputStreamReader(conn.getInputStream(), "UTF-8")); String line; while ((line = in.readLine()) != null) { res += line; } in.close(); } catch (Exception e) { e.printStackTrace(); } return res; } /** * 回调request 参数解析为map格式 * @Author 科帮网 * @param request * @return * @throws Exception Map* @Date 2017年7月31日 * 更新日志 * 2017年7月31日 科帮网 首次创建 * */ @SuppressWarnings("unchecked") public static Map parseXml(HttpServletRequest request) throws Exception { // 解析结果存储在HashMap Map map = new HashMap (); InputStream inputStream = request.getInputStream(); // 读取输入流 SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List elementList = root.elements(); // 遍历所有子节点 for (Element e : elementList) map.put(e.getName(), e.getText()); // 释放资源 inputStream.close(); inputStream = null; return map; }}
package com.zhjt.health.entity;import java.util.Map;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;@ApiModelpublic class JsonBack { @ApiModelProperty(value = "是否成功", required = true) private boolean success; @ApiModelProperty(value = "返回消息", required = true) private String message; @ApiModelProperty(value = "其他", required = false) private Mapmap; public JsonBack() { super(); } public JsonBack(boolean success, String message, Map map) { super(); this.success = success; this.message = message; this.map = map; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Map getMap() { return map; } public void setMap(Map map) { this.map = map; } }
package com.zhjt.health.util.pay;import java.io.BufferedReader;import java.io.DataOutputStream;import java.io.IOException;import java.io.InputStreamReader;import java.io.UnsupportedEncodingException;import java.net.HttpURLConnection;import java.net.URL;import javax.servlet.http.HttpServletRequest;import org.apache.commons.lang3.StringUtils;/** * 根据IP地址获取详细的地域信息 * 创建者 科帮网 * 创建时间 2017年7月31日 * */public class AddressUtils { /** * * @param content * 请求的参数 格式为:name=xxx&pwd=xxx * @param encoding * 服务器端请求编码。如GBK,UTF-8等 * @return * @throws UnsupportedEncodingException */ public static String getAddresses(String ip) throws UnsupportedEncodingException { String urlStr ="http://ip.taobao.com/service/getIpInfo.php"; String returnStr = getResult(urlStr, ip); if (returnStr != null) { // 处理返回的省市区信息 String[] temp = returnStr.split(","); if (temp.length < 3) { return "0";// 无效IP,局域网测试 } String region = (temp[5].split(":"))[1].replaceAll("\"", ""); region = decodeUnicode(region);// 省份 String country = ""; String area = ""; // String region = ""; String city = ""; String county = ""; String isp = ""; for (int i = 0; i < temp.length; i++) { switch (i) { case 1: country = (temp[i].split(":"))[2].replaceAll("\"", ""); country = decodeUnicode(country);// 国家 break; case 3: area = (temp[i].split(":"))[1].replaceAll("\"", ""); area = decodeUnicode(area);// 地区 break; case 5: region = (temp[i].split(":"))[1].replaceAll("\"", ""); region = decodeUnicode(region);// 省份 break; case 7: city = (temp[i].split(":"))[1].replaceAll("\"", ""); city = decodeUnicode(city);// 市区 break; case 9: county = (temp[i].split(":"))[1].replaceAll("\"", ""); county = decodeUnicode(county);// 地区 break; case 11: isp = (temp[i].split(":"))[1].replaceAll("\"", ""); isp = decodeUnicode(isp); // ISP公司 break; } } String address = region+city; if(StringUtils.isBlank(address)){ address = "地球村"; } return address; } return null; } /** * @param urlStr * 请求的地址 * @param content * 请求的参数 格式为:name=xxx&pwd=xxx * @param encoding * 服务器端请求编码。如GBK,UTF-8等 * @return */ private static String getResult(String urlStr, String ip) { URL url = null; HttpURLConnection connection = null; try { url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection();// 新建连接实例 /** * 超时错误 由 2s改为5s */ connection.setConnectTimeout(5000);// 设置连接超时时间,单位毫秒 connection.setReadTimeout(5000);// 设置读取数据超时时间,单位毫秒 connection.setDoOutput(true);// 是否打开输出流 true|false connection.setDoInput(true);// 是否打开输入流true|false connection.setRequestMethod("POST");// 提交方法POST|GET connection.setUseCaches(false);// 是否缓存true|false connection.connect();// 打开连接端口 DataOutputStream out = new DataOutputStream(connection.getOutputStream());// 打开输出流往对端服务器写数据 out.writeBytes("ip="+ip);// 写数据,也就是提交你的表单 name=xxx&pwd=xxx out.flush();// 刷新 out.close();// 关闭输出流 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));// 往对端写完数据对端服务器返回数据 // ,以BufferedReader流来读取 StringBuffer buffer = new StringBuffer(); String line = ""; while ((line = reader.readLine()) != null) { buffer.append(line); } reader.close(); return buffer.toString(); } catch (IOException e) { e.printStackTrace(); } finally { if (connection != null) { connection.disconnect();// 关闭连接 } } return null; } /** * unicode 转换成 中文 * @param theString * @return */ public static String decodeUnicode(String theString) { char aChar; int len = theString.length(); StringBuffer outBuffer = new StringBuffer(len); for (int x = 0; x < len;) { aChar = theString.charAt(x++); if (aChar == '\\') { aChar = theString.charAt(x++); if (aChar == 'u') { int value = 0; for (int i = 0; i < 4; i++) { aChar = theString.charAt(x++); switch (aChar) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': value = (value << 4) + aChar - '0'; break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': value = (value << 4) + 10 + aChar - 'a'; break; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': value = (value << 4) + 10 + aChar - 'A'; break; default: throw new IllegalArgumentException("Malformed encoding."); } } outBuffer.append((char) value); } else { if (aChar == 't') { aChar = '\t'; } else if (aChar == 'r') { aChar = '\r'; } else if (aChar == 'n') { aChar = '\n'; } else if (aChar == 'f') { aChar = '\f'; } outBuffer.append(aChar); } } else { outBuffer.append(aChar); } } return outBuffer.toString(); } /** * 获取IP地址 * @Author 科帮网 * @param request * @return String * @Date 2017年7月31日 * 更新日志 * 2017年7月31日 科帮网 首次创建 * */ public static String getIpAddr(HttpServletRequest request) { String ip = request.getHeader("X-Real-IP"); if(!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip)) return ip; ip = request.getHeader("X-Forwarded-For"); if(!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip)) { int index = ip.indexOf(','); if(index != -1) return ip.substring(0, index); else return ip; } 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.getHeader("HTTP_CLIENT_IP"); if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("HTTP_X_FORWARDED_FOR"); if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) ip = request.getRemoteAddr(); if(ip==null||ip!=null&&ip.indexOf("0:0:0:0:0:0:0")!=-1){ return "127.0.0.1"; } return ip; }}