微信小程序业务接口

这里只举例接口的方法实现,具体的和框架的业务整合就不赘述

所有的 key 都需要到微信官方相关平台认证获取

一、微信支付

1. 整体流程

图片中框出来的三个地方就是我们需要用代码实现的地方

  1. 微信小程序把支付数据打包发送给商户后台
  2. 商户后台生成订单向微信支付系统请求预付单信息
  3. 商户后台对预付单信息签名再返回给微信小程序
  4. 微信小程序用户申请支付
  5. 用户微信支付系统确认支付
  6. 微信支付系统进行转账操作并将支付结果通知到商户后台
  7. 商户后台得到支付结果进行业务操作
  8. 商户后台可以向微信支付系统进行订单查询
  9. 商户系统也可以向微信支付系统进行退款申请

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 集合,确保签名的唯一性
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();
//防止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;
}

public static Map<String, String> getMapFromXML(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//防止XXE攻击
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
<!-- hutools -->
<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) {
// 一切正常返回的xml数据
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) {
// mchid 你的商户id
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 = 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
<!-- http请求所需jar包 -->
<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();
}