最近在做APP调起微信支付,鉴于文档之坑爹,特此记录,希望对大家有所帮助。
1、第一步,先把相关配置弄到一个类里面,方便以后使用。
public final class WeChatConfig { public final static String TRADE_TYPE = "APP";//支付方式 public final static String CHARSET = "UTF-8";//编码格式 public final static String APP_KEK = ""; //api 秘钥 public final static String APP_ID = "";//appid public final static String MCH_ID = "";//商户号 }
2、生成待支付订单,微信支付的套路是酱紫的,先生成一笔待付款的订单,然后把待付款订单的ID和其他一堆数据抛给前台,然后APP发起付款。生成预支付订单的方式,请参见文档,这块写的还是挺清楚的,这里只是提供一下具体的代码。这里有个坑,后台发送HTTPS请求生成预支付订单的时候,必须把发送的内容重新编码为ISO-8859-1,否则APP支付的时候显示的中文都是乱码,对于微信返回的预支付订单的信息,必须用UTF-8重新编码,否则中文也是乱码。
import org.apache.commons.lang3.StringUtils; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import java.io.UnsupportedEncodingException; import java.util.*; import java.util.regex.Pattern; public class PayCore { //按ASCII码表正序,然后生成链接 public static String sortAndCreateLink(SortedMap<String, String> params) throws UnsupportedEncodingException { Set<Map.Entry<String, String>> es = params.entrySet(); return createUrlPara(es.iterator()); } private final static Pattern pattern = Pattern.compile("[\\u4e00-\\u9fa5]"); //把Map拼装成xml string public static String toXml(Map<String, String> map) throws UnsupportedEncodingException { Set<Map.Entry<String, String>> es = map.entrySet(); Iterator<Map.Entry<String, String>> iterator = es.iterator(); StringBuffer buffer = new StringBuffer(); buffer.append("<xml>"); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); if (StringUtils.isBlank(entry.getValue())) continue; buffer.append(String.format("<%s>", entry.getKey())); buffer.append(String.format("<![CDATA[%s]]>", entry.getValue())); buffer.append(String.format("</%s>", entry.getKey())); } buffer.append("</xml>"); return buffer.toString(); } //把xml解析成Map public static Map<String, String> fromXml(String xml) throws UnsupportedEncodingException { Map map = new HashMap(); try { Document document = DocumentHelper.parseText(xml); Element element = document.getRootElement(); Iterator iterator = element.elementIterator(); while (iterator.hasNext()) { Element childElement = (Element) iterator.next(); map.put(childElement.getName(), childElement.getText()); } } catch (Exception e) { e.printStackTrace(); } return map; } private static String createUrlPara(Iterator<Map.Entry<String, String>> iterator) throws UnsupportedEncodingException { StringBuffer stringBuffer = new StringBuffer(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); stringBuffer.append(entry.getKey()); stringBuffer.append("="); stringBuffer.append(entry.getValue()); stringBuffer.append("&"); } String value = stringBuffer.toString(); return value.substring(0, value.lastIndexOf("&")); } }
import org.apache.commons.lang3.StringUtils; //配置回调地址 public class Callback { private String callback; private String notify; public Callback() { this.callback = StringUtils.EMPTY; this.notify = StringUtils.EMPTY; } public String getCallback() { return callback; } public void setCallback(String callback) { this.callback = callback; } public String getNotify() { return notify; } public void setNotify(String notify) { this.notify = notify; } } import com.jiayukang.common.pay.Pay; import com.jiayukang.common.pay.dao.PayRecordDao; import com.jiayukang.common.pay.dao.model.PayMethod; import com.jiayukang.common.pay.dao.model.PayRecord; import com.jiayukang.common.pay.exception.PayFailException; import com.jiayukang.common.pay.model.Callback; import com.jiayukang.common.utils.http.Https; import com.jiayukang.common.utils.sign.MD5; import com.jiayukang.common.utils.PayCore; import com.jiayukang.common.utils.UUIDGenerator; import org.apache.commons.lang3.StringUtils; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.io.SAXReader; import org.xml.sax.InputSource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.*; public final class WeChartPay{ private Callback callback; public WeChartPay(Callback callback) { this.callback = callback; } private final static String SIGN = "sign"; private final static String REQUEST_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; private final static NumberFormat numberFormat = new DecimalFormat("#");//格式化数字为 public Map pay(String orderNumber, String productName, Float price) throws KeyManagementException, NoSuchAlgorithmException, DocumentException, IOException { try { InetAddress addr = InetAddress.getLocalHost(); Map orderInfo = new HashMap(); orderInfo.put("appid", WeChartConfig.APP_ID);// orderInfo.put("mch_id", WeChartConfig.MCH_ID); //商户号 orderInfo.put("nonce_str", UUIDGenerator.generator());//随机数,保证请求唯一性 orderInfo.put("out_trade_no", orderNumber);//商户订单号 orderInfo.put("body", productName); //商品描述 orderInfo.put("total_fee", numberFormat.format(price)); //总金额,单位为分,不能有小数部分,只能是正整数 orderInfo.put("spbill_create_ip", addr.getHostAddress().toString());//你的服务器的IP orderInfo.put("notify_url", callback.getNotify());//支付成功后异步回调的方法 orderInfo.put("trade_type", WeChartConfig.TRADE_TYPE);//支付类型,之前在WeChatConfig里配置的 orderInfo.put("sign", sign(orderInfo));//签名,╮( ̄▽ ̄")╭ ,讨厌的玩意 String xml = PayCore.toXml(orderInfo); String resultXml = Https.post(REQUEST_URL, new String(xml.getBytes(WeChartConfig.CHARSET), "ISO-8859-1"));//这里发送请求的时候,一定要 return analyzeReturnValue(new String(resultXml.getBytes("ISO-8859-1"), WeChartConfig.CHARSET)); } catch (Exception ex) { throw ex; } } private String sign(Map map) throws UnsupportedEncodingException { String pack = PayCore.sortAndCreateLink(new TreeMap<String, String>(map)); String signValue = MD5.sign(pack, "&key=" + WeChartConfig.APP_KEK, WeChartConfig.CHARSET).toUpperCase(); return signValue; } private Map analyzeReturnValue(String resultXml) throws UnsupportedEncodingException, DocumentException { SAXReader saxReader = new SAXReader(); Document doc = saxReader.read(new InputSource(new ByteArrayInputStream(resultXml.getBytes(WeChartConfig.CHARSET)))); String returnValue = doc.selectSingleNode("/xml/return_code").getText(); Map<String, String> map = new HashMap<>(); if (SUCCESS.equals(returnValue)) { String resultValue = doc.selectSingleNode("/xml/result_code").getText(); if (SUCCESS.equals(resultValue)) { map.put("appid", WeChartConfig.APP_ID); map.put("partnerid", WeChartConfig.MCH_ID); map.put("prepayid", doc.selectSingleNode("/xml/prepay_id").getText());//微信返回的预支付订单 map.put("package", "Sign=WXPay"); map.put("noncestr", UUIDGenerator.generator());//随机数,防止唯一 map.put("timestamp", String.valueOf(new Date().getTime() / 1000));//这里返回的是秒 map.put("sign", sign(map));//签名 } } return map; } //验证返回值 public String check(Map map) throws PayFailException { if (isTenpaySign(new TreeMap(map))) { String orderNumber = (String) map.get("out_trade_no");//商户订单号 String tradeNumber = (String) map.get("transaction_id");//交易流水号 if (!StringUtils.isBlank(orderNumber) && !StringUtils.isBlank(tradeNumber)) //.dosth } else { throw new PayFailException("签名验证失败"); } } //验证微信支付回调返回值签名 private Boolean isTenpaySign(SortedMap map) { StringBuffer sb = new StringBuffer(); Set es = map.entrySet(); Iterator it = es.iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); String k = (String) entry.getKey(); String v = (String) entry.getValue(); if (!SIGN.equals(k) && !StringUtils.isBlank(v)) { sb.append(k + "=" + v + "&"); } } String mySign = MD5.sign(sb.toString(), "key=" + WeChartConfig.APP_KEK, WeChartConfig.CHARSET).toLowerCase(); String tenpaySign = map.get(SIGN).toString().toLowerCase(); return tenpaySign.equals(mySign); } }
3、支付成功后,会异步回调notify_url指定的地址,微信会把文档上写的那些值用xml的格式POST过来。╮( ̄▽ ̄")╭ ,所以,我们得解析XML,方法如下。
private final static Logger logger = LoggerFactory.getLogger("PayLogger"); @RequestMapping(value = "wechart/notify.do", method = RequestMethod.POST) public void weChartPayNotify(HttpServletRequest request, HttpServletResponse response) throws IOException { BufferedReader bufferedReader = request.getReader(); StringBuffer contentBuffer = new StringBuffer(); String line = null; while (!StringUtils.isBlank((line = bufferedReader.readLine()))) { contentBuffer.append(line); } String content = contentBuffer.toString();//以上代码用来获取content. Map map = null; try { map = xmlToMap(content); } catch (DocumentException e) { logger.error(exceptionToString(e)); } logger.info(content); try { weChartPay.check(map); response.getOutputStream().println("Success"); } catch (PayFailException iex) { response.getOutputStream().println("Fail"); } catch (Exception ex) { response.getOutputStream().println("Fail"); } response.getOutputStream().flush(); response.getOutputStream().close(); } //似乎之前写过了..?忘了,再来一百年吗,XML TO OBJECT private Map xmlToMap(String xml) throws DocumentException { Map map = new HashMap(); Document document = DocumentHelper.parseText(xml); Element element = document.getRootElement(); Iterator iterator = element.elementIterator(); while (iterator.hasNext()) { Element childElement = (Element) iterator.next(); map.put(childElement.getName(), childElement.getText()); } return map; } //把堆栈信息转换成字符串 private String exceptionToString(Exception ex) { StringWriter stringWriter = new StringWriter(); PrintWriter writer = new PrintWriter(stringWriter); ex.printStackTrace(writer); StringBuffer buffer = stringWriter.getBuffer(); return buffer.toString(); }
╮( ̄▽ ̄")╭ ,如上,就是微信支付的整个流程,用到了的jar如下。
<dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.3.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency>
应该就这些了吧╮( ̄▽ ̄")╭ ,如果缺点啥,那个,大家克服下...