1、注册小程序
拿到App_id 和 AppSecret 小程序密钥
取得商户的微信支付商户号 MCHID 和 微信支付密钥 APIKEY。
2、流程
2.1.1 小程序中用户选择商品下单。
【小程序】中,用户选中商品,数量,下单------提交-----》【后台】接收下单数据。验证数据,成功入库后,
**后台对微信支付接口发起支付请求。**后台进行第一次签名验算。
必须参数如下:
requestmap.put("**appid**" , app_id); //APP应用ID
requestmap.put("**attach**", "test"); //附加编号。
requestmap.put("**mch_id**" , mch_id); //直连商户号
requestmap.put("**body**", 订单号); //商品介绍。
requestmap.put("**nonce_str**" , nonce_str); // 随机字符串不大于32位。参考必须
requestmap.put("**notify_url**", "https://****/notify"); //通知地址 ,必须,这个是下一步客户支付后的情况,消息接收地址。 必须
requestmap.put("**openid**", open_id); //下单客户和小程序支付客户的open_id,查询获取到。必须
requestmap.put("**out_trade_no**" , order.getOrder_no()); //商户订单号,这个是平台内部的订单号。必须
requestmap.put("**spbill_create_ip**", requestIp(request));//下单ip,这个不是必须。
requestmap.put("**total_fee**", 支付金额) //必须
requestmap.put("**trade_type**" , "**JSAPI**"); //支付验证方式
这个map文件。需排序。所以用SortedMap格式存储,因为url这个串key需要按ask码排序,再进行MD5加密验算。
String url = mapToUrl(resultmap); //这里用了一个map转str的工具类。
private static String makeSign(String str){
//生成签名
return DigestUtils.md5DigestAsHex(str.getBytes());
}
//map转url。
private static String mapToUrl(SortedMap<String,String> map){
//生成url map.entrySet()
String buffer ="";
Set<Map.Entry<String,String>> map2 = map.entrySet();
for(Map.Entry<String, String> s: map2){
buffer+=s.getKey()+"="+s.getValue()+"&";
}
return buffer;
}
String signstr = url + "key="+ wxapp.getApikey(); //这个就比较关键了。这个字符串是从上面一系列参数转换成字符串后,本地加密验算的字符串。转化为字符串 “appid=*********&attach=*******............&“ + 最尾后再加上 **key=微信支付密钥APIKEY**
String sign = makeSign(signstr).toUpperCase() ; //MD5加密后,再转换成大写。这个sign。可以用微信开发平台提供的工具进行核算,验证。sign和上面signstr在平台算出来的结果是否一致。[(签名校验工具)](https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=20_1)
2.1.2 后台算签名,及把签名加入到xml中去。对统一支付接口https://api.mch.weixin.qq.com/pay/unifiedorder发起提交。xml post提交得到返回数据xml格式的。requestmap.put("sign", sign );
//好了。上面第一次签名加密串有了。在这里加入到map中去。才组合成了完整的提交到微信支付接口的参数。这个格式肯定不接受。需要转换为xml格式。
String xml = "";
**xml** = MapToXml.mapToXml(resultmap); //这里用了一个工具类,转换为了xml格式的串。后台这里就要对微信支付接口进行一个访问提交,同时把参数,用xml的格式进行提交。
CloseableHttpClient **httpClient** = HttpClientBuilder.create().build();
// 创建Post请求
HttpPost **httpPost** = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder"); //微信小程序的开发文档坑爹,v3和统一支付,初始根本分布清楚。乱套了。一直提交给的的是v3的地址。总是验证错误。
StringEntity **entity** = new StringEntity(xml, null, "UTF-8", false);
// post请求是将参数放在请求体里面传过去的;这里将entity放入post请求体中
httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36");
httpPost.setHeader("Content-Type", "application/json;charset=utf8");
httpPost.setHeader("Accept", "application/json;charset=utf8");
httpPost.setEntity(**entity**);
这样后台提交就完成了。
2.1.3接收微信接口返回的xml,取值,本地时间戳,不大于32位的随机串。再次md5加密签名。
接收返回的参数。
CloseableHttpResponse response = null; Map<String, String> remap = new HashMap<String,String>(); // 由客户端执行(发送)Post请求 response = httpClient.execute(httpPost); // 从响应模型中获取响应实体 HttpEntity responseEntity = response.getEntity(); if (responseEntity != null) { System.out.println("响应内容长度为:" + responseEntity.getContentLength()); remap = XmlToMap.getXmlBodyContext(EntityUtils.toString(responseEntity)); }
//这里就是接收到的微信返回数据。返回的格式一样是xml格式的。这里用了一个工具类。对数据进行整理到map中。这个map中有几个关键key-value。这个是需要后台返回给小程序中给用户的。
paysignMap.put(“appId”, app_id); //小程序的app_id
paysignMap.put(“nonceStr”, nonce_str); //随机字符串。第二次签名需要加入的。这个可以重新再生成一个。也可以拿之前的。
paysignMap.put(“package”, “prepay_id=” + prepay_id); //支付订单号
paysignMap.put(“signType”, “MD5”); //验算加密的方式
paysignMap.put(“timeStamp”, time2); //时间戳
把这个再次传换成url的 appid=&nonceStr=&package=… + 再加上 key=微信支付密钥APIKEY
String paysignurl = mapToUrl(paysignMap);
paysignurl += “key=”+ wxapp.getApikey();
String paysign = makeSign(paysignurl).toUpperCase();
再次md5验算。可以去微信平台工具那里验算。但有个巨坑。微信网站平台的验算。在package=parepay_id=wx**订单号。他会过滤掉后面的订单号。验算不会相等自己平台后面的MD5加密串。 这里可以把自己的urlstr 转xml 。再把xml提供到网站验算平台,进行验算。看是不是能够一样。后台还是按url方式进行加密算。但去网站上检验时,用XML的格式去复制粘贴。就能能看到一样了。这个困扰了大天半。这样第二次签名验算的加密串就拿到了。
把下面四个必要参数返回给微信用户。以及下单成功,发起支付的消息一并发给微信端。
towxmap.put(“timeStamp”, time); //时间戳
towxmap.put(“nonceStr”, nonce_str); //随机串
towxmap.put(“prepay_id”, prepay_id); //在微信支付返回来的支付订单号
towxmap.put(“paySign”, paysign); //后台二次签名
2.1.4小程序接收到必要参数,调用wx.requestPayment.发起支付。提供下面正确的参数。即可正常支付。
小程序调用wx.requestPayment 发起支付 发送必须参数如下:
timeStamp: result.data.payment.timeStamp, //后台二次签名时,用的时间戳,返回过来的。
nonceStr: result.data.payment.nonceStr, //后台二次签名时候。用到的随机串
package: 'prepay_id=' + result.data.payment.prepay_id, //注意这里和后台返回的不同。前面加了'prepay_id='支付订单号
signType: 'MD5', //加密方式
paySign: result.data.payment.paySign,
2.1.5 前面提到notify。后台接收微信支付成功失败的网址。接收的数据一样是xml格式。必须要给微信接口返回消息。不然会一直发。
再根据微信发过来的支付结果消息,进行订单更新处理,改变订单状态,和给用户发送消息。
@RequestMapping(value="wxclient/notify")
@ResponseBody
public void notify(HttpServletRequest request,HttpServletResponse response) throws Exception{
//request.setCharacterEncoding("utf-8"); // 接收格式指定
//String path = request.getServletContext().getRealPath("/WEB-INF/temple/default/");
String result = "";
BufferedReader in = null;
request.setCharacterEncoding("UTF-8");
//InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
in = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
//ObjectMapper MAPPER = new ObjectMapper();
//Map<String, Object> jsonmap = MAPPER.readValue(result, HashMap.class);
SortedMap<String,String> map2 = new TreeMap<String,String>();
map2 = XmlToMap.getXmlBodyContext(result); //获得的result来的格式为xml,这里xml转map
String signurl = mapToUrlQuSign(map2); //map再转url后,加key。再md5算sign验证码。
WxApp wxapp = yixingService.SelectWxApp(10001); //暂时直接用的wxapp_id没有查询。
signurl += "key="+ wxapp.getApikey();
map2.put("signurl", signurl);
System.out.println(signurl);
String sign2 = makeSign(signurl).toUpperCase();
map2.put("sign2", sign2);
System.out.println(map2);
YiXingConstants.map = map2;
SortedMap<String,String> map = new TreeMap<String,String>();
if(sign2!=null && sign2.equals(map2.get("sign"))){
Order order = new Order();
int update_time = Integer.parseInt(String.valueOf(System.currentTimeMillis()/1000));
//order.setUser_id(Integer.parseInt((String)redisUtil.get(token)));
order.setWxapp_id(10001);
order.setOrder_no(map2.get("out_trade_no"));
order.setUpdate_time(update_time);
order.setPay_time(StampToData.dateToStamp(map2.get("time_end")));
order.setTransaction_id(map2.get("transaction_id"));
yixingService.OrderPayUpdateByOrderId(order);
System.out.println(order);
map.put("result_code","SUCCESS");
}else{
map.put("result_code", "FAIL");
}
String xml = "";
xml = MapToXml.mapToXml(map); //返回给支付平台成功消息。map转xml。
response.setCharacterEncoding("UTF-8");
response.getWriter().println(xml);
}
XmlToMap
public class XmlToMap{
public static Document parseXmlString(String xmlStr){
try{
InputSource is = new InputSource(new StringReader(xmlStr));
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder=factory.newDocumentBuilder();
Document doc = builder.parse(is);
return doc;
}catch(Exception e){
e.printStackTrace();
}
return null;
}
public static SortedMap<String, String> getXmlBodyContext(String bodyXml){
SortedMap<String, String> dataMap = new TreeMap<String,String>();
Document doc = parseXmlString(bodyXml);
if(null != doc){
NodeList rootNode = doc.getElementsByTagName("xml");
if(rootNode != null){
Node root = rootNode.item(0);
NodeList nodes = root.getChildNodes();
for(int i = 0;i < nodes.getLength(); i++){
Node node = nodes.item(i);
dataMap.put(node.getNodeName(), node.getTextContent());
}
}
}
return dataMap;
}
}
MapToXml 两个方式,都可以用。
public class MapToXml {
public static boolean isNumeric(String str){
for(int i=str.length();--i>=0;){
int chr=str.charAt(i);
if(chr<48 || chr>57)
return false;
}
return true;
}
public static String mapToXml(SortedMap<String, String> map) throws Exception {
/* if (map.size()==0) {
return "fail";
}
String xml = "<xml>";
for(Entry<String,String> s : map.entrySet()) {
if(isNumeric(s.getValue())){
xml += "<" + s.getKey() + "><![CDATA[" + s.getValue() + "]]></" + s.getKey() + ">";
}else{
xml += "<" + s.getKey() + ">" + s.getValue() + "</" + s.getKey() + ">";
}
}
xml += "</xml>";
return xml;
}*/
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//防止XXE攻击
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: map.keySet()) {
String value = map.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString();
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
}
HttpClient 需要 httpcomponents-client-5.1-bin 支持。