微信小程序业务接口
这里只举例接口的方法实现,具体的和框架的业务整合就不赘述
所有的 key 都需要到微信官方相关平台认证获取
一、微信支付
1. 整体流程
图片中框出来的三个地方就是我们需要用代码实现的地方
- 微信小程序把支付数据打包发送给商户后台
- 商户后台生成订单向微信支付系统请求预付单信息
- 商户后台对预付单信息签名再返回给微信小程序
- 微信小程序向用户申请支付
- 用户向微信支付系统确认支付
- 微信支付系统进行转账操作并将支付结果通知到商户后台
- 商户后台得到支付结果进行业务操作
- 商户后台可以向微信支付系统进行订单查询
- 商户系统也可以向微信支付系统进行退款申请

2. 预支付接口
URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
以下只列举必要参数,非必要参数可根据文档自行添加
字段名 |
变量名 |
必填 |
类型 |
示例值 |
描述 |
小程序ID |
appid |
是 |
String(32) |
wxd678efh567hg6787 |
微信分配的小程序ID |
商户号 |
mch_id |
是 |
String(32) |
1230000109 |
微信支付分配的商户号 |
随机字符串 |
nonce_str |
是 |
String(32) |
5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
随机字符串,长度要求在32位以内。推荐随机数生成算法 |
签名 |
sign |
是 |
String(64) |
C380BEC2BFD727A4B6845133519F3AD6 |
通过签名算法计算得出的签名值,详见签名生成算法 |
商品描述 |
body |
是 |
String(128) |
腾讯充值中心-QQ会员充值 |
商品简单描述,该字段请按照规范传递,具体请见参数规定 |
商户订单号 |
out_trade_no |
是 |
String(32) |
20150806125346 |
商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号 |
标价金额 |
total_fee |
是 |
Int |
88 |
订单总金额,单位为分,详见支付金额 |
终端IP |
spbill_create_ip |
是 |
String(64) |
123.12.12.123 |
支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP |
通知地址 |
notify_url |
是 |
String(256) |
http://www.weixin.qq.com/wxpay/pay.php |
异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 |
交易类型 |
trade_type |
是 |
String(16) |
JSAPI |
小程序取值如下:JSAPI,详细说明见参数规定 |
用户标识 |
openid |
否 |
String(128) |
oUpF8uMuAJO_M2pxb1Q9zNjWeS6o |
trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。openid如何获取,可参考【获取openid】。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
| public Map<String, String> prepay(){ Map<String,String> params = new TreeMap<String,String>(); params.put("appid","你的appid"); params.put("mch_id","你的mchid"); params.put("openid","支付用户的openid"); params.put("nonce_str",getRandomString(30)); params.put("body", "商品信息"); params.put("out_trade_no",out_trade_no); params.put("total_fee", "订单金额"); params.put("spbill_create_ip", getLocalIp()); params.put("notify_url", "你的接口回调地址"); params.put("trade_type", "JSAPI"); String xmlData = null; try { params.put("sign",createSign(params, "商户密钥")); xmlData = mapToXml(params); Map mapRes = getMapFromXML(HttpUtil.post("https://api.mch.weixin.qq.com/pay/unifiedorder", xmlData)); if("FAIL".equals(mapRes.get("result_code"))){ return null; }else { SortedMap<String,String> data = new TreeMap<>(); data.put("package","prepay_id="+mapRes.get("prepay_id").toString()); data.put("nonceStr",mapRes.get("nonce_str").toString()); data.put("signType","MD5"); data.put("timeStamp", String.valueOf(System.currentTimeMillis()/1000)); data.put("appid","你的appid"); data.put("sign", createSign(data, "商户密钥")); data.put("out_trade_no",out_trade_no); return data; } } catch (Exception e) { e.printStackTrace(); } return null; }
public static String createSign(Map<String,String> parameters,String key){ SortedMap<String,String> data = new TreeMap<>(); for (String s : parameters.keySet()) { data.put(s,parameters.get(s)); } StringBuffer sb = new StringBuffer(); Set es = data.entrySet(); Iterator<?> it = es.iterator(); while(it.hasNext()) { Map.Entry entry = (Map.Entry)it.next(); String k = (String)entry.getKey(); String v = String.valueOf(entry.getValue()); if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=").append(key); String sign = SecureUtil.md5(sb.toString()).toUpperCase(); return sign; }
public static String getRandomString(int length){ String str="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random=new Random(); StringBuffer sb=new StringBuffer(); for(int i=0;i<length;i++){ int number=random.nextInt(36); sb.append(str.charAt(number)); } return sb.toString(); }
public static String getLocalIp(){ InetAddress ia=null; String localip = null; try { ia= InetAddress.getLocalHost(); localip=ia.getHostAddress(); } catch (Exception e) { e.printStackTrace(); } return localip; }
public static String mapToXml(Map<String, String> map) throws Exception { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 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; }
public static Map<String, String> getMapFromXML(String strXML) throws Exception { try { Map<String, String> data = new HashMap<String, String>(); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setXIncludeAware(false); documentBuilderFactory.setExpandEntityReferences(false); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); org.w3c.dom.Document doc = documentBuilder.parse(stream); doc.getDocumentElement().normalize(); NodeList nodeList = doc.getDocumentElement().getChildNodes(); for (int idx = 0; idx < nodeList.getLength(); ++idx) { Node node = nodeList.item(idx); if (node.getNodeType() == Node.ELEMENT_NODE) { org.w3c.dom.Element element = (org.w3c.dom.Element) node; data.put(element.getNodeName(), element.getTextContent()); } } try { stream.close(); } catch (Exception ex) { ex.printStackTrace(); } return data; } catch (Exception ex) { throw ex; } }
|
添加Maven依赖
1 2 3 4 5 6
| <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.4.7</version> </dependency>
|
3. 回调接口
该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。
通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @PostMapping("/callback") public void NotifyUrl(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { String returnXmlMessage = null; Map<String, String> notifyMapData = null; try { String notifyXmlData = readXmlFromStream(httpServletRequest); notifyMapData = WxUtils.getMapFromXML(notifyXmlData); boolean signatureValid = WxUtils.isSignatureValid(notifyMapData, key); if (signatureValid) { returnXmlMessage = setReturnXml("SUCCESS", "OK"); } else { returnXmlMessage = setReturnXml("FAIL", "Verification sign failed!"); } BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(httpServletResponse.getOutputStream()); bufferedOutputStream.write(returnXmlMessage.getBytes()); bufferedOutputStream.flush(); bufferedOutputStream.close(); } catch (IOException e) { returnXmlMessage = setReturnXml("FAIL", "An exception occurred while reading the WeChat server returning xml data in the stream."); } catch (Exception e) { returnXmlMessage = setReturnXml("FAIL", "Payment successful, exception occurred during asynchronous notification processing."); } return notifyMapData; }
private String readXmlFromStream(HttpServletRequest httpServletRequest) throws IOException { InputStream inputStream = httpServletRequest.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); final StringBuffer sb = new StringBuffer(); String line = null; try { while ((line = bufferedReader.readLine()) != null) { sb.append(line); } } finally { bufferedReader.close(); inputStream.close(); }
return sb.toString(); }
private String setReturnXml(String returnCode, String returnMsg) { return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code><return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>"; }
|
4. 查询账单接口
https://api.mch.weixin.qq.com/pay/orderquery
请求参数
字段名 |
变量名 |
必填 |
类型 |
示例值 |
描述 |
小程序ID |
appid |
是 |
String(32) |
wxd678efh567hg6787 |
微信分配的小程序ID |
商户号 |
mch_id |
是 |
String(32) |
1230000109 |
微信支付分配的商户号 |
微信订单号 |
transaction_id |
二选一 |
String(32) |
1009660380201506130728806387 |
微信的订单号,优先使用 |
商户订单号 |
out_trade_no |
二选一 |
String(32) |
20150806125346 |
商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_- |
随机字符串 |
nonce_str |
是 |
String(32) |
C380BEC2BFD727A4B6845133519F3AD6 |
随机字符串,不长于32位。推荐随机数生成算法 |
签名 |
sign |
是 |
String(32) |
5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
通过签名算法计算得出的签名值,详见签名生成算法 |
签名类型 |
sign_type |
否 |
String(32) |
HMAC-SHA256 |
签名类型,目前支持HMAC-SHA256和MD5,默认为MD5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public Map<String, String> check(String out_trade_no){ Map<String,String> check_params = new TreeMap<>(); check_params.put("appid","你的appid"); check_params.put("mch_id","你的mchid"); check_params.put("out_trade_no", out_trade_no); check_params.put("nonce_str",getRandomString(30)); String xmlRes = null; try { check_params.put("sign",createSign("https://api.mch.weixin.qq.com/pay/orderquery", "商户密钥")); xmlRes = mapToXml(check_params); xmlRes = HttpUtil.sendPost(check_url, xmlRes); Map mapRes = getMapFromXML(xmlRes); return mapRes; } catch (Exception e) { e.printStackTrace(); } return null; }
|
5. 退款接口
接口链接:https://api.mch.weixin.qq.com/secapi/pay/refund
需要双向证书
请求参数
字段名 |
变量名 |
必填 |
类型 |
示例值 |
描述 |
小程序ID |
appid |
是 |
String(32) |
wx8888888888888888 |
微信分配的小程序ID |
商户号 |
mch_id |
是 |
String(32) |
1900000109 |
微信支付分配的商户号 |
随机字符串 |
nonce_str |
是 |
String(32) |
5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
随机字符串,不长于32位。推荐随机数生成算法 |
签名 |
sign |
是 |
String(32) |
C380BEC2BFD727A4B6845133519F3AD6 |
签名,详见签名生成算法 |
微信支付订单号 |
transaction_id |
二选一 |
String(32) |
1217752501201407033233368018 |
微信生成的订单号,在支付通知中有返回 |
商户订单号 |
out_trade_no |
二选一 |
String(32) |
1217752501201407033233368018 |
商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 transaction_id、out_trade_no二选一,如果同时存在优先级:transaction_id> out_trade_no |
商户退款单号 |
out_refund_no |
是 |
String(64) |
1217752501201407033233368018 |
商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 |
订单金额 |
total_fee |
是 |
Int |
100 |
订单总金额,单位为分,只能为整数,详见支付金额 |
退款金额 |
refund_fee |
是 |
Int |
100 |
退款总金额,订单总金额,单位为分,只能为整数,详见支付金额 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| public JSONObject refund(Pay pay){ Map<String,String> refund_params = new TreeMap<String,String>(); refund_params.put("appid","你的appid"); refund_params.put("mch_id","你的mchid"); refund_params.put("out_trade_no", out_trade_no); refund_params.put("out_refund_no", out_refund_no); refund_params.put("nonce_str",.getRandomString(30)); refund_params.put("total_fee", "订单金额"); refund_params.put("refund_fee","退款金额"); String xmlRes = null; try { refund_params.put("sign",createSign(refund_params, "商户密钥")); xmlRes = mapToXml(refund_params); BasicHttpClientConnectionManager connManager; if (useCert) { char[] password = mchid.toCharArray(); cn.hutool.core.io.file.FileReader fileReader = new FileReader("你的证书路径"); byte[] certData = fileReader.readBytes(); KeyStore ks = KeyStore.getInstance("PKCS12"); ByteArrayInputStream certBis = new ByteArrayInputStream(certData); ks.load(certBis, password); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, password); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory( sslContext, new String[]{"TLSv1"}, null, new DefaultHostnameVerifier()); connManager = new BasicHttpClientConnectionManager( RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslConnectionSocketFactory) .build(), null, null, null ); } else { connManager = new BasicHttpClientConnectionManager( RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(), null, null, null ); } HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .build(); HttpPost httpPost = new HttpPost(url); RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(8*1000).setConnectTimeout(6*1000).build(); httpPost.setConfig(requestConfig); StringEntity postEntity = new StringEntity(data, "UTF-8"); httpPost.addHeader("Content-Type", "text/xml"); httpPost.addHeader("User-Agent", USER_AGENT + " " + mchid); httpPost.setEntity(postEntity); HttpResponse httpResponse = httpClient.execute(httpPost); HttpEntity httpEntity = httpResponse.getEntity(); xmlRes = EntityUtils.toString(httpEntity, "UTF-8"); Map<String, String> mapRes = getMapFromXML(xmlRes); JSONObject jsonObject = JSONUtil.parseFromMap(mapRes); return jsonObject; } catch (Exception e) { e.printStackTrace(); log.error("退款操作异常:{}", ThrowableUtil.getStackTrace(e)); return null; } }
|
添加Maven依赖
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> <version>4.4.11</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency>
|
二、小程序码
在生成小程序码之前要先获得接口调用凭证
1. 接口调用凭证
文档地址:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
请求地址:GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
请求参数:
属性 |
类型 |
默认值 |
必填 |
说明 |
grant_type |
string |
|
是 |
填写 client_credential |
appid |
string |
|
是 |
小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态) |
secret |
string |
|
是 |
小程序唯一凭证密钥,即 AppSecret,获取方式同 appid |
1 2 3 4 5
| public static String getToken(String appid,String secret){ String api = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", "你的appid", "你的密钥"); String result = HttpRequest.get(api).execute().body(); return JSONUtil.parseObj(result).get("access_token").toString(); }
|
2. 获取小程序码
文档地址:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html
接口地址:POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
请求参数:
属性 |
类型 |
默认值 |
必填 |
说明 |
access_token |
string |
|
是 |
接口调用凭证 |
scene |
string |
|
是 |
最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~ ,其它字符请自行编码为合法字符(因不支持% ,中文无法使用 urlencode 处理,请使用其他编码方式) |
page |
string |
主页 |
否 |
必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index , 根路径前不要填加 / ,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面 |
width |
number |
430 |
否 |
二维码的宽度,单位 px,最小 280px,最大 1280px |
auto_color |
boolean |
false |
否 |
自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false |
line_color |
Object |
{“r”:0,”g”:0,”b”:0} |
否 |
auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示 |
is_hyaline |
boolean |
false |
否 |
是否需要透明底色,为 true 时,生成透明底色的小程序 |
1 2 3 4 5
| public static String getImg(String appid,String secret, String info){ String api = String.format("https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s", "获取的接口调用凭证"); JSONObject params = JSONUtil.createObj().set("scene", info).set("page", "你的页面路径"); return HttpRequest.post(api).body(params.toString()).execute().body(); }
|