微信H5支付

微信解读

  • 使用场景
    商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。

  • 步骤

    • 商户下发图文消息或者通过自定义菜单吸引用户点击进入商户网页。
    • 进入商户网页,用户选择购买,完成选购流程。
  • 简述
    微信的H5页面支付只需在商家的付款页面上调用wechat的API即可,调用时需要提供一些指定参数,所以,前端页面代码只需复制微信官方提供的页面方法即可;而后端需要做的,就是将支付所需参数生成正确并传递到页面即可。

网页端调起支付API所需参数

详见微信官方文档 wechat link
在微信浏览器里面打开H5网页中执行JS调起支付。接口输入输出数据格式为JSON。
注意:WeixinJSBridge内置对象在其他浏览器中无效。列表中参数名区分大小,大小写错误签名验证会失败。

网页端接口参数列表:

| 名称 |变量名|必填|类型|示例|描述|
| 公众号id | appId | 是 | String(16) | wx8888888888888888 | 公众号名称,由商户传入,商户注册具有支付权限的公众号成功后即可获得 |
| 时间戳 | timeStamp | 是 | String(32) | 1414561699 | 当前的时间,自1970年以来的秒数,其他详见时间戳规则 |
| 随机字符串 | nonceStr | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位。|
|订单详情扩展字符串|package|是|String(128)|prepay_id=123456789|统一下单接口返回的参数值,提交格式必须如示例所示|
|签名方式| type | 是 | String(32) | MD5 |签名算法|
| 时间戳 | timeStamp | 是 | String(64) | C380BEC2BFD727A4B6845133519F3AD6 |签名|

签名生成算法
时间戳规则
随机数生成算法

网页内支付接口err_msg返回结果值说明:

|返回值|描述|
|ok|支付成功,仅在用户成功完成支付时返回|
|cancel|支付过程中,用户取消(可以统一处理为用户遇到错误或者主动放弃,不必细化区分)|
|fail|支付失败(可以统一处理为用户遇到错误或者主动放弃,不必细化区分)|

网页端调起支付API示例代码

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
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId" : "wx2421b1c4370ec43b", //公众号名称,由商户传入
"timeStamp":" 1395712654", //时间戳,自1970年以来的秒数
"nonceStr" : "e61463f8efa94090b1f366cccfbbb444", //随机串
"package" : "prepay_id=u802345jgfjsdfgsdg888",
"signType" : "MD5", //微信签名方式:
"paySign" : "70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名
},
function(res){
if (res.err_msg == "get_brand_wcpay_request:ok") {
alert("pay success");
}
if (res.err_msg == "get_brand_wcpay_request:cancel") {
alert("pay cancel");
}
if (res.err_msg == "get_brand_wcpay_request:fail") {
alert("pay failed");
}
}
);
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}

后端代码剖析

与支付H5页面交互类

其中notify_url与设置详见下图
支付授权目录:
xzl.gxcm.com.cn/#/为iOS路径
xzl.gxcm.com.cn/#/course/为android路径
支付授权目录.png?nolink
微信支付设置:需要为测试人员设置白名单,否则无法支付
开发配置.png?nolink

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
import com.*.CommonUtil;
import com.*.MD5SignUtil;
import com.*.PayUtil;
import com.*.WeChatConnector;
import org.springframework.ui.Model;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class WechatPayService{

private static Log log= LogFactory.getLog(WechatPayService.class);
public String wechatPay(Model model, String openId, String price) throws Exception {
String paternerKey = "demo123456demo123456demo123456de"; // 密钥,需登录微信支付平台获得
String appId = WeChatConnector.getMpConfigStorage().getAppId(); // 公众号名称,由商户传入
String timestamp = System.currentTimeMillis() / 1000 + "";
String nonceStr = UUID.randomUUID().toString().substring(0, 32);
String num = (Double.parseDouble(price) * 100 + "").substring(0, (Double.parseDouble(price) * 100 + "").indexOf(".")); // 对页面传入的价格进行处理
PayUtil payUtil = new PayUtil();
// get prepayid
Map map = new HashMap();
map.put("appid", appId);
map.put("mch_id", "1235171102"); // 商户号
map.put("nonce_str", nonceStr); // 随机数
map.put("body", "高校传媒"); // 产品名
map.put("out_trade_no", payUtil.orderNum());
map.put("total_fee", price); // 总价,以“分”为单位
map.put("spbill_create_ip", "127.0.0.1"); // request.getRemoteAddr()
map.put("notify_url", "http://xzl.gxcm.com.cn/wechat/gitPay"); // 支付授权目录,设置见下图
map.put("trade_type", "JSAPI"); // 支付接口类型
map.put("attach", courseId); // 课程Id
map.put("openid", openId); // 用户的openId
String paySign = MD5SignUtil.sign(CommonUtil.FormatBizQueryParaMap(map), paternerKey);
map.put("sign", paySign); // sign
String xml = CommonUtil.ArrayToXml(map);
String prepayid = payUtil.getPrepayid(xml);

// h5 param
Map signMap = new HashMap();
model.addAttribute("appId", appId);
model.addAttribute("timeStamp", timestamp);
model.addAttribute("package", "prepay_id=" + prepayid);
model.addAttribute("nonceStr", nonceStr);
model.addAttribute("signType", "MD5");

signMap.put("appId", appId);
signMap.put("timeStamp", timestamp);
signMap.put("package", "prepay_id=" + prepayid);
signMap.put("signType", "MD5");
signMap.put("nonceStr", nonceStr);
String paySignH5 = MD5SignUtil.sign(CommonUtil.FormatBizQueryParaMap(signMap), paternerKey);

model.addAttribute("paySign", paySignH5);
String url = String.format("checkout/partials/wechatPayForm", model);

return url;
}
}

交互所需参数生成工具类

CommonUtil
HttpClientUtil
MD5SignUtil
MD5Util
MapKeyComparatorUtil
PayUtil

CommonUtil.java

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
 import java.util.*;

public class CommonUtil {

// 转换
public static String ArrayToXml(Map<String, String> arr) {

String xml = "<xml>";

Iterator<Map.Entry<String, String>> iter = arr.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, String> entry = iter.next();
String key = entry.getKey();
String val = entry.getValue();
xml += "<" + key + ">" + val + "</" + key + ">";
}

xml += "</xml>";
return xml;
}

// 字典排序
public static String FormatBizQueryParaMap(Map map) throws Exception {

StringBuffer sb = new StringBuffer();
if (map == null || map.isEmpty()) {
return null;
}
Map<String, String> sortMap = new TreeMap<String, String>(new MapKeyComparatorUtil());
sortMap.putAll(map);
Map<String, String> resultMap = sortMap;
for (Map.Entry<String, String> entry : resultMap.entrySet()) {
sb.append(entry.getKey() + "=" + entry.getValue() + "&");
}

return sb.toString().substring(0,sb.toString().length()-1);
}
}

HttpClientUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.apache.commons.httpclient.methods.PostMethod;

public class HttpClientUtil {

private static HttpClientUtil instance = null;

private HttpClientUtil() {

}

public static HttpClientUtil getInstance() {
if (instance == null) {
instance = new HttpClientUtil();
}
return instance;
}

// 发送 post请求
// @param httpUrl 地址
public PostMethod postMethod(String httpUrl) {
PostMethod postMethod=new PostMethod(httpUrl);// 创建httpPost
return postMethod;
}
}

MD5SignUtil.java

1
2
3
4
5
6
7
public class MD5SignUtil {

public static String sign(String content, String key) throws Exception {
String signStr = content + "&key=" + key;
return MD5Util.MD5(signStr).toUpperCase();
}
}

MD5Util.java

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
import java.security.MessageDigest;

public class MD5Util {

public final static String MD5(String s) {
char hexDigits[]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
try {
byte[] btInput = s.getBytes();
MessageDigest mdInst = MessageDigest.getInstance("MD5");
mdInst.update(btInput);
byte[] md = mdInst.digest();
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

MapKeyComparatorUtil.java

1
2
3
4
5
6
7
8
9
import java.util.Comparator;

public class MapKeyComparatorUtil implements Comparator<String> {

@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}

PayUtil.java

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
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.SimpleHttpConnectionManager;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jettison.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;

public class PayUtil {

private static Log log = LogFactory.getLog(PayUtil.class);
public static String URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";

public String getPrepayid(String xml) {
log.warn(xml);
try {
JSONObject jo = getPrepayJson(xml);
JSONObject element = jo.getJSONObject("xml");
String prepayid = (element.get("prepay_id")).toString();
return prepayid.substring(1, prepayid.length() - 1);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@SuppressWarnings("deprecation")
public JSONObject getPrepayJson(String xml) {
HttpClient httpClient = new HttpClient(new HttpClientParams(), new SimpleHttpConnectionManager(true));
InputStream is = null;
PostMethod method = null;
try {
method = HttpClientUtil.getInstance().postMethod(URL);
method.setRequestBody(xml);
method.getParams().setContentCharset("UTF-8"); // UTF-8
httpClient.executeMethod(method);
//读取响应
is = method.getResponseBodyAsStream();
JSONObject o = XmlJsonUtil.xmlJSON(is);
return o;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (method != null) {
method.releaseConnection();
}
if (is != null) {
try {
is.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
return null;
}

// @return time&randomNum
public String orderNum() {
String chars = "0123456789";
String order = System.currentTimeMillis() + "";
String res = "";
for (int i = 0; i < 19; i++) {
Random rd = new Random();
res += chars.charAt(rd.nextInt(chars.length() - 1));
}
order += res;
return order;
}
}

支付成功发送模版消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// wechat shopping receipt
// 发送模版消息
// @param openId
// @param price
public void sendShoppingReceipt(String openId, String price) {
WxMpTemplateMessage wxMpTemplateMessage = new WxMpTemplateMessage();
wxMpTemplateMessage.setToUser(openId);
wxMpTemplateMessage.setTemplateId("VTevmK1xd15sAlMPOmna1bXnh-l46Epw9DwrqHvkKVo"); // 模版ID
wxMpTemplateMessage.setUrl("bige.t.beeoz.com/#/owncourse");// user center page
wxMpTemplateMessage.setTopColor("#173177");
wxMpTemplateMessage.getDatas().add(new WxMpTemplateData("first", "购买成功", "#173177"));
wxMpTemplateMessage.getDatas().add(new WxMpTemplateData("product", "在线课程", "#173177"));
wxMpTemplateMessage.getDatas().add(new WxMpTemplateData("price", price, "#173177"));
wxMpTemplateMessage.getDatas().add(new WxMpTemplateData("time", System.currentTimeMillis() + "", "#173177"));
wxMpTemplateMessage.getDatas().add(new WxMpTemplateData("remark", "如有疑问,请咨询公众号客服", "#173177"));
try {
WeChatConnector.getMpService().templateSend(wxMpTemplateMessage);
} catch (WxErrorException e) {
e.printStackTrace();
}
}