微信小程序支付(Java)
相信进行过微信公众号支付的同学对于微信小程序支付的开发上手很快,如下是微信官方对三种接入方式的对比
注意坑一:发起支付必须是HTTPS
流程
然后我们整理下发起订单的思路。如下是官方给的流程图,发起支付已经做了标注。
由此可见,服务器端发起订单需要以下五小步,我们来各个击破。
第一步:获取openid
第二步:生成商户订单
第三步:调用支付统一下单API
第四步:获取预支付会话标识 prepayId
第五步:将组合数据签名并返回
第一步:获取openId(跳过)
获取openId不做赘述,自行百度。
第二步:生成商户订单
这里由我们系统内部生成。官方API如下(官方建议用当前系统时间加随机序列来生成订单号):
生成商户订单号代码如下(仅供参考)
public static String getRandomOrderId() {
Random random = new Random(System.currentTimeMillis());
int value = random.nextInt();
while (value < 0) {
value = random.nextInt();
}
return value + "";
}
第三步:调用统一下单支付API
下面进行第三步调用支付统一下单API(将必传参数整理成xml格式,发送给下单url):
注意坑二:参数名按照字典序排序、参数值为空不参与签名、区分大小写、sign参数不参与签名!
注意坑三:在结尾拼接要key、进行签名运算后的sign要转化成大写!详见官方“签名算法说明”
官方API说明:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
签名算法说明:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
整理的格式如下,然后发送到下单url。代码如下
<xml>
<appid>wx2421b1c4370ec43b</appid>
<attach>支付测试</attach>
<body>JSAPI支付测试</body>
<mch_id>10000100</mch_id>
<detail><![CDATA[{ "goods_detail":[ { "goods_id":"iphone6s_16G", "wxpay_goods_id":"1001", "goods_name":"iPhone6s 16G", "quantity":1, "price":528800, "goods_category":"123456", "body":"苹果手机" }, { "goods_id":"iphone6s_32G", "wxpay_goods_id":"1002", "goods_name":"iPhone6s 32G", "quantity":1, "price":608800, "goods_category":"123789", "body":"苹果手机" } ] }]]></detail>
<nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str>
<notify_url>http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php</notify_url>
<openid>oUpF8uMuAJO_M2pxb1Q9zNjWeS6o</openid>
<out_trade_no>1415659990</out_trade_no>
<spbill_create_ip>14.23.150.211</spbill_create_ip>
<total_fee>1</total_fee>
<trade_type>JSAPI</trade_type>
<sign>0CB01533B8C1EF103065174F50BCA001</sign>
</xml>
代码如下,仅供参考
/**
* 调用统一下单接口
* @param openId
*/
private String unifiedOrder(String openId, String clientIP, String randomNonceStr) {
try {
String url = Constant.URL_UNIFIED_ORDER;
PayInfo payInfo = createPayInfo(openId, clientIP, randomNonceStr);
String md5 = getSign(payInfo);
payInfo.setSign(md5);
log.error("md5 value: " + md5);
String xml = CommonUtil.payInfoToXML(payInfo);
xml = xml.replace("__", "_").replace("<![CDATA[1]]>", "1");
//xml = xml.replace("__", "_").replace("<![CDATA[", "").replace("]]>", "");
log.error(xml);
StringBuffer buffer = HttpUtil.httpsRequest(url, "POST", xml);
log.error("unifiedOrder request return body: \n" + buffer.toString());
Map<String, String> result = CommonUtil.parseXml(buffer.toString());
String return_code = result.get("return_code");
if(StringUtils.isNotBlank(return_code) && return_code.equals("SUCCESS")) {
String return_msg = result.get("return_msg");
if(StringUtils.isNotBlank(return_msg) && !return_msg.equals("OK")) {
//log.error("统一下单错误!");
return "";
}
String prepay_Id = result.get("prepay_id");
return prepay_Id;
} else {
return "";
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 生成订单信息
* @param openId
* @param clientIP
* @param randomNonceStr
*/
private PayInfo createPayInfo(String openId, String clientIP, String randomNonceStr) {
Date date = new Date();
String timeStart = TimeUtils.getFormatTime(date, Constant.TIME_FORMAT);
String timeExpire = TimeUtils.getFormatTime(TimeUtils.addDay(date, Constant.TIME_EXPIRE), Constant.TIME_FORMAT);
String randomOrderId = CommonUtil.getRandomOrderId();
PayInfo payInfo = new PayInfo();
payInfo.setAppid(Constant.APP_ID);
payInfo.setMch_id(Constant.MCH_ID);
payInfo.setDevice_info("WEB");
payInfo.setNonce_str(randomNonceStr);
payInfo.setSign_type("MD5"); //默认即为MD5
payInfo.setBody("JSAPI支付测试");
payInfo.setAttach("支付测试4luluteam");
payInfo.setOut_trade_no(randomOrderId);
payInfo.setTotal_fee(1);
payInfo.setSpbill_create_ip(clientIP);
payInfo.setTime_start(timeStart);
payInfo.setTime_expire(timeExpire);
payInfo.setNotify_url(Constant.URL_NOTIFY);
payInfo.setTrade_type("JSAPI");
payInfo.setLimit_pay("no_credit");
payInfo.setOpenid(openId);
return payInfo;
}
/**
* 对订单信息签名
* @param payInfo
*/
private String getSign(PayInfo payInfo) throws Exception {
StringBuffer sb = new StringBuffer();
sb.append("appid=" + payInfo.getAppid())
.append("&attach=" + payInfo.getAttach())
.append("&body=" + payInfo.getBody())
.append("&device_info=" + payInfo.getDevice_info())
.append("&limit_pay=" + payInfo.getLimit_pay())
.append("&mch_id=" + payInfo.getMch_id())
.append("&nonce_str=" + payInfo.getNonce_str())
.append("¬ify_url=" + payInfo.getNotify_url())
.append("&openid=" + payInfo.getOpenid())
.append("&out_trade_no=" + payInfo.getOut_trade_no())
.append("&sign_type=" + payInfo.getSign_type())
.append("&spbill_create_ip=" + payInfo.getSpbill_create_ip())
.append("&time_expire=" + payInfo.getTime_expire())
.append("&time_start=" + payInfo.getTime_start())
.append("&total_fee=" + payInfo.getTotal_fee())
.append("&trade_type=" + payInfo.getTrade_type())
.append("&key=" + Constant.APP_KEY);
log.error("排序后的拼接参数:" + sb.toString());
return CommonUtil.getMD5(sb.toString().trim()).toUpperCase();
}
第四步:获取prepayId
第三步如果没问题,return_code 和result_code都为SUCCESS。则返回<prepay_id>(就是我们想要的预处理会话标识)如下所示:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
<openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
<sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
<trade_type><![CDATA[JSAPI]]></trade_type>
</xml>
这里提供下思路,将xml格式转化成map。然后根据key=prepay_id 获取prepayId
工具类代码如下,仅供参考
public class CommonUtil {
public static String getRandomOrderId() {
// UUID.randomUUID().toString().replace("-","")
Random random = new Random(System.currentTimeMillis());
int value = random.nextInt();
while (value < 0) {
value = random.nextInt();
}
return value + "";
}
private static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
//增加CDATA标记
boolean cdata = true;
@SuppressWarnings("rawtypes")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});
public static String payInfoToXML(PayInfo pi) {
xstream.alias("xml", pi.getClass());
return xstream.toXML(pi);
}
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(String xml) throws Exception {
Map<String, String> map = new HashMap<String, String>();
Document document = DocumentHelper.parseText(xml);
Element root = document.getRootElement();
List<Element> elementList = root.elements();
for (Element e : elementList)
map.put(e.getName(), e.getText());
return map;
}
第五步:将组合数据签名并返回