本文目的为了让更多像我一样的小白了解微信支付的整个流程,希望能够给你们提供一点帮忙,有不足的地方也希望各位大神能够指正,第一次发博勿喷。
微信支付整体流程
- 1.统一下单(为了得到预支付id)
- 2.再次签名(将得到的预支付id再次和相关参数进行签名)
- 3.返回给前端xml(封装前端所需相关字段数据)
- 4.支付成功后的回调(进行相关逻辑处理)
总概:
调用微信提供的统一下单接口,得到预支付id - prepay_id
此处注意部分浏览器可能打开显示不完整,推荐使用谷歌浏览器。
ASCII码升序排序加密签名类也在最后补充贴出。
-此处需要注意:签名为本步骤中较为麻烦的一点,微信要求比较严格,严格遵守文档要求进行传参,与参数处理。
-以下指出几点这个操作中可能会遇到的情况:
1.body 字段,参数如果为中文可能会导致签名错误。
2.total-fee 字段,进行处理以分为单位传递,微信是以分为单位。
3.传递参数和签名加密等相关数据与测试工具中一致表面代码正确。
4.若代码正确,还是返回签名错误的情况可尝试修改API密钥(32位key)。
微信接口调试工具使用示例
具体怎么调试我想不需要介绍,总的来说就是自己的参数和生成的东西需要与这里自动生成的一致
参数填写的截图
填写完成后点击下方生成签名………签名生成截图:
服务端完整代码
贴出自己微信支付代码供参考,代码处理不好的地方请勿喷:
//商户号
String mchId = "";
//支付密钥
String key = "&key=自己的商户密钥";
//交易类型
String tradeType = "JSAPI";
//随机字符串
String nonceStr = WxPayUtil.getNonceStr();
//微信支付完成后给该链接发送消息,判断订单是否完成
String notifyUrl = "外网能够访问的url,支付成功后的回调";
//微信用户唯一id
if(param.getOpenid()==null){
return result(500 ,"支付失败,openid is null");
}
String openId = param.getOpenid();
//小程序id
if(param.getAppid()==null){
return result(500 ,"支付失败,appid is null");
}
String appid = param.getAppid();
//商品订单号(保持唯一性)
String outTradeNo = mchId+WxPayUtil.getNonceStr();
//支付金额
if(param.getTotalFee()==null){
return result(500 ,"支付失败,totalfee is null");
}
String fee = param.getTotalFee();
String totalFee = WxPayUtil.getMoney(fee);
//发起支付设备ip
String spbillCreateIp = param.getSpbillCreateIp();
//商品描述
if(param.getBody()==null){
return result(500 ,"支付失败,body is null");
}
String body = param.getBody();
//附加数据,商户携带的订单的自定义数据 (原样返回到通知中,这类我们需要系统中订单的id 方便对订单进行处理)
String attach = param.getAttach();
//我们后面需要键值对的形式,所以先装入map
Map<String, String> sParaTemp = new HashMap<String, String>();
sParaTemp.put("appid", appid);
sParaTemp.put("attach", attach);
sParaTemp.put("body", body);
sParaTemp.put("mch_id", mchId);
sParaTemp.put("nonce_str", nonceStr);
sParaTemp.put("notify_url",notifyUrl);
sParaTemp.put("openid", openId);
sParaTemp.put("out_trade_no", outTradeNo);
sParaTemp.put("spbill_create_ip", spbillCreateIp);
sParaTemp.put("total_fee",totalFee);
sParaTemp.put("trade_type", tradeType);
//去掉空值 跟 签名参数(空值不参与签名,所以需要去掉)
Map<String, String> map = WxPayUtil.paraFilter(sParaTemp);
/**
按照 参数=参数值&参数2=参数值2 这样的形式拼接(拼接需要按照ASCII码升序排列)
/
String mapStr = WxPayUtil.createLinkString(map);
//MD5运算生成签名
String sign =
WxPayUtil.sign(mapStr, key, "utf-8").toUpperCase();
sParaTemp.put("sign", sign);
/**
组装成xml参数,此处偷懒使用手动组装,严格代码可封装一个方法,XML标排序需要注意,ASCII码升序排列
*/
String xml = "<xml>" + "<appid>" + appid + "</appid>"
+ "<attach>" + attach + "</attach>"
+ "<body>" + body + "</body>"
+ "<mch_id>" + mchId + "</mch_id>"
+ "<nonce_str>" + nonceStr + "</nonce_str>"
+ "<notify_url>" + notifyUrl + "</notify_url>"
+ "<openid>" + openId + "</openid>"
+ "<out_trade_no>" + outTradeNo + "</out_trade_no>"
+ "<spbill_create_ip>" + spbillCreateIp + "</spbill_create_ip>"
+ "<total_fee>" + totalFee + "</total_fee>"
+ "<trade_type>" + tradeType + "</trade_type>"
+ "<sign>" + sign + "</sign>"
+ "</xml>";
//统一下单url,生成预付id
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
String result =WxPayUtil.httpRequest(url, "POST", xml);
Map<String, String> paramMap = new HashMap<String, String>();
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
//得到预支付id
String prepay_id = "";
try {
prepay_id = WxPayUtil.getPayNo(result);
} catch (DocumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String packages = "prepay_id="+prepay_id;
String nonceStr1 = WxPayUtil.getNonceStr();
//开始第二次签名
String mapStr1 = "appId="+appid+"&nonceStr=" + nonceStr1 + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp;
String paySign = WxPayUtil.sign(mapStr1, key, "utf-8").toUpperCase();
//前端所需各项参数拼接
String finaPackage = "\"appId\":\"" + appid + "\",\"timeStamp\":\"" + timeStamp
+ "\",\"nonceStr\":\"" + nonceStr1 + "\",\"package\":\""
+ packages + "\",\"signType\" : \"MD5" + "\",\"paySign\":\""
+ paySign + "\"";
return result(200,finaPackage);
}
...
'''
代码中使用到的工具类
public class WxPayUtil{
/**
* 获取随机字符串 (采用截取8位当前日期数 + 4位随机整数)
* @return
*/
public static String getNonceStr() {
//获得当前日期
Date now = new Date();
SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String currTime = outFormat.format(now);
//截取8位
String strTime = currTime.substring(8, currTime.length());
//得到4位随机整数
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < 4; i++) {
num = num * 10;
}
num = (int)random * num;
return strTime + num;
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
* @param params 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params) {
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
/**
* 除去数组中的空值和签名参数
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
public static Map<String, String> paraFilter(Map<String, String> sArray) {
Map<String, String> result = new HashMap<String, String>();
if (sArray == null || sArray.size() <= 0) {
return result;
}
for (String key : sArray.keySet()) {
String value = sArray.get(key);
if (value == null || value.equals("") || key.equalsIgnoreCase("sign")
|| key.equalsIgnoreCase("sign_type")) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* MD5 加密,转为指定类型
* @param text
* @param key
* @param input_charset
* @return
*/
public static String sign(String text, String key, String input_charset) {
text = text + key;
return DigestUtils.md5Hex(getContentBytes(text, input_charset));
}
public static byte[] getContentBytes(String content, String charset) {
if (charset == null || "".equals(charset)) {
return content.getBytes();
}
try {
return content.getBytes(charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
}
}
/**
* 元转换成分
* @param money
* @return
*/
public static String getMoney(String amount) {
if(amount==null){
return "";
}
// 金额转化为分为单位
String currency = amount.replaceAll("\\$|\\¥|\\,", ""); //处理包含, ¥ 或者$的金额
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if(index == -1){
amLong = Long.valueOf(currency+"00");
}else if(length - index >= 3){
amLong = Long.valueOf((currency.substring(0, index+3)).replace(".", ""));
}else if(length - index == 2){
amLong = Long.valueOf((currency.substring(0, index+2)).replace(".", "")+0);
}else{
amLong = Long.valueOf((currency.substring(0, index+1)).replace(".", "")+"00");
}
return amLong.toString();
}
/**
* 解析xml得到 prepay_id 预支付id
* @param result
* @return
* @throws DocumentException
*/
public static String getPayNo(String result) throws DocumentException{
Map<String, String> map = new HashMap<String, String>();
InputStream in = new ByteArrayInputStream(result.getBytes());
SAXReader read = new SAXReader();
Document doc = read.read(in);
//得到xml根元素
Element root = doc.getRootElement();
//遍历 得到根元素的所有子节点
@SuppressWarnings("unchecked")
List<Element> list =root.elements();
for(Element element:list){
//装进map
map.put(element.getName(), element.getText());
}
//返回码
String return_code = map.get("return_code");
//返回信息
String result_code = map.get("result_code");
//预支付id
String prepay_id = "";
//return_code 和result_code 都为SUCCESS 的时候返回 预支付id
if(return_code.equals("SUCCESS")&&result_code.equals("SUCCESS")){
prepay_id = map.get("prepay_id");
}
return prepay_id;
}
/**
* 解析 回调时的xml装进map 返回
* @param result
* @return
* @throws DocumentException
*/
public static Map<String, String> getNotifyUrl(String result) throws DocumentException{
Map<String, String> map = new HashMap<String, String>();
InputStream in = new ByteArrayInputStream(result.getBytes());
SAXReader read = new SAXReader();
Document doc = read.read(in);
//得到xml根元素
Element root = doc.getRootElement();
//遍历 得到根元素的所有子节点
@SuppressWarnings("unchecked")
List<Element> list =root.elements();
for(Element element:list){
//装进map
map.put(element.getName().toString(), element.getText().toString());
}
return map;
}
/**
* 验证签名,判断是否是从微信发过来
* 验证方法:接收微信服务器回调我们url的时候传递的xml中的参数 然后再次加密,看是否与传递过来的sign签名相同
* @param map
* @return
*/
public static boolean verifyWeixinNotify(Map<String, String> map,String key) {
//根据微信服务端传来的各项参数 进行再一次加密后 与传过来的 sign 签名对比
String mapStr = createLinkString(map);
String signOwn = WxPayUtil.sign(mapStr, key, "utf-8").toUpperCase(); //根据微信端参数进行加密的签名
String signWx = map.get("sign"); //微信端传过来的签名
if(signOwn.equals(signWx)){
//如果两个签名一致,验证成功
return true;
}
return false;
}
/**
*
* @param requestUrl请求地址
* @param requestMethod请求方法
* @param outputStr参数
*/
public static String httpRequest(String requestUrl,String requestMethod,String outputStr){
// 创建SSLContext
StringBuffer buffer=null;
try{
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(requestMethod);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.connect();
//往服务器端写内容
if(null !=outputStr){
OutputStream os=conn.getOutputStream();
os.write(outputStr.getBytes("utf-8"));
os.close();
}
// 读取服务器端返回的内容
InputStream is = conn.getInputStream();
InputStreamReader isr = new InputStreamReader(is, "utf-8");
BufferedReader br = new BufferedReader(isr);
buffer = new StringBuffer();
String line = null;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
}catch(Exception e){
e.printStackTrace();
}
return buffer.toString();
}
ASCII码升序排列加密签名
**看许多朋友再找这个工具方法,贴出来供使用**
/**
* 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*/
public String createSign(SortedMap<String, String> packageParams) {
StringBuffer sb = new StringBuffer();
Set es = packageParams.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k)
&& !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + this.getKey());
String sign = DigestUtils.md5Hex(getContentBytes(text, "utf-8"))
.toUpperCase();
return sign;
}
支付通知(回调url)
最后一步就是支付完成后的通知了(回调同意下单时候填写的notify_url)
在此步环节中重要的是 验证(介绍在下面) ,验证通知是否是从微信端发送过来的,为了防止资金出现问题。
验证过程:
就是把微信端发送过来的报文解析,得到相关参数(具体可以查看自己第一步签名时使用了哪些参数)进行再次签名加密,然后与报文发送过来的sign签名进行对比。
注意:微信会循环回调该url,直到确认支付完成才回停止,所以我们需要在该url中返指定 xml让它停止调用。
支付通知代码片段
//用于处理结束后返回的xml
String resXml = "";
String key = "&key=自己的密钥";
try {
InputStream in = request.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
int len = 0;
byte[] b = new byte[1024];
while((len = in.read(b)) != -1){
out.write(b, 0, len);
}
out.close();
in.close();
//将流 转为字符串
String result = new String(out.toByteArray(), "utf-8");
Map<String, String> map = WxPayUtil.getNotifyUrl(result);
String return_code = map.get("return_code").toString().toUpperCase();
if(return_code.equals("SUCCESS")){
//进行签名验证,看是否是从微信发送过来的,防止资金被盗
if(WxPayUtil.verifyWeixinNotify(map, key)){
//签名验证成功后按照微信要求返回的xml
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
return resXml;
}
}else{
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[sign check error]]></return_msg>" + "</xml> ";
return resXml;
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (DocumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[xml error]]></return_msg>" + "</xml> ";
return resXml;
服务端的支付接口相关以及处理完毕了,现在需要前端上场了
-
前端操作
没错,在我们的处理之下,前端只需要这一个东西就搞定!
wx.requestPayment(OBJECT)
到这里小程序微信支付所有操作已经完成了,当自己做完了之后是不是觉得很简单了。