前段时间在公司的开发了一个微信小程序的项目,今天来说一说微信小程序的支付,有很多优秀的文章都说了小程序支付的
开发流程步骤,这里我们推荐一个博主以前就是看他的开发小程序支付:https://github.com/1913045515/weixin
现在来说说我对小程序支付的理解,首先我们在开发文档中找到小程序支付流程,
上面的图是我们支付的流程图,下面是开发要调用接口的顺序,
首先第一步我们获取openid,因为在统一下单接口中的交易类型是JSAPI,是必须要填openid
第二步就是下单了,把我们要下单参数拼接成xml的形式去发送接口,这里主要是签名很重要
签名:
1.签名算法
签名生成的通用步骤如下:
第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
特别注意以下重要规则:
◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
第二步,在stringA最后拼接上key=(API密钥的值)得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。
假设传送的参数如下:
appid: wxd930ea5d5a258f4f
mch_id: 10000100
device_info: 1000
body: test
nonce_str: ibuaiVcKdpRxkhJA
第一步:对参数按照key=value的格式,并按照参数名ASCII字典序排序如下:
stringA=”appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA”;
第二步:拼接API密钥:
stringSignTemp=”stringA&key=192006250b4c09247ec02edce69f6a2d”
sign=MD5(stringSignTemp).toUpperCase()=”9A0A8659F005D6984697E2CA0A9CF3B7”
网友的整理:
这样我们就得到了签名
我们下单成功后微信会给我们预支付交易会话标识 这个很主要,拿这个在进行一次签名加密,
因为我们调用支付的接口中需要一个签名,支付成功后就会调用我们下单时所传的回调地址,我们在回调方法中判断用户是否支付成功,进行业务处理
代码实例:
OrderController.java
/**
* 下单:
* @param openid 用户openid
* @param money 金额
* @return map
*/
@RequestMapping("/createOrder")
@ResponseBody
public Map<String,String>createOrder(String openid,int money){
logger.info("用户的openid:-------->"+openid+"下单的金额:-------------->"+money);
String mch_id= WeChatTool.mch_id; //商户号
String today = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String WXPay= WXPayUtil.createCode(8);
String out_trade_no=mch_id+today+WXPay;//生成订单号
Map<String,String> result=new HashMap<String,String>();
//去Service层中去生成签名,用户openid out_trade_no订单号 money支付的金额
String formData=orderService.getopenid(openid,out_trade_no,money);
//在servlet层中生成签名成功后,把下单所要的参数以xml的格式拼接,发送下单接口
String httpResult = HttpUtils.httpXMLPost(WeChatTool.createOrderUrl,formData);
try {
//xml转换成Map对象或者值
Map<String, String> resultMap = WXPayUtil.xmlToMap(httpResult);
result.put("package", "prepay_id=" + resultMap.get("prepay_id")); //这里是拿下单成功的微信交易号去拼接,因为在下面的接口中必须要这个样子
result.put("nonceStr",resultMap.get("nonce_str")); //随机字符串
} catch (Exception e) {
e.printStackTrace();
}
String times= WXPayUtil.getCurrentTimestamp()+""; //获取当前时间
result.put("timeStamp",times); //当前时间戳
//生成调用支付接口要的签名
Map<String, String> packageParams = new HashMap<String ,String>();
packageParams.put("appId", WeChatTool.wxspAppid);
packageParams.put("signType", WeChatTool.sign_type);
packageParams.put("nonceStr",result.get("nonceStr")+"");
packageParams.put("timeStamp",times);
packageParams.put("package", result.get("package")+"");//商户订单号
String sign="";
try {
sign= WXPayUtil.generateSignature(packageParams, WeChatTool.sercet_key); //生成签名:
} catch (Exception e) {
e.printStackTrace();
}
result.put("paySign",sign);
logger.info("签名成功----->"+result.get("paySign"));
return result; //所有的参数放进map中保存发送到小程序页面中,去调用微信支付接口
}
OrderServiceImpl.java:
/**
* 统一下单
* @param openid 用户标识
* @param out_trade_no 订单号
* @param total_fee 金额
* @return String
*/
@Override
public String getopenid(String openid,String out_trade_no,int total_fee) {
//下单的金额,因为在微信支付中默认是分所以要这样处理
Integer total_fees=total_fee*100;
微信下单的金额是String类型的所以要转换类型
String money=total_fees.toString();
String nonceStr=WXPayUtil.generateUUID(); //设置UUID作为随机字符串
Map<String ,String> map = new HashMap<String ,String>();
map.put("appid",WeChatTool.wxspAppid); //商户appid
map.put("mch_id", WeChatTool.mch_id);//商户号
map.put("nonce_str",nonceStr); //随机数
map.put("body","大米");//商户名称
map.put("out_trade_no",out_trade_no);//商户订单号
map.put("total_fee",money);//下单金额
map.put("spbill_create_ip", "127.0.0.1");//终端IP
map.put("notify_url",https://xxxx/xxxxx/notify.do);//回调地址 这里的接口必须是在线上用户支付成功才能收到微信发送的信息
map.put("trade_type","JSAPI");//交易类型
map.put("openid",openid+"");//用户openid
map.put("sign_type","MD5");//加密类型
String sign="";
try {
sign= WXPayUtil.generateSignature(map, WeChatTool.sercet_key); //生成sign签名WeChatTool.sercet_key是商户的支付秘钥
} catch (Exception e) {
e.printStackTrace();
}
//拼接成xml的格式,这里的参数必须要和上面的一致,并且每次下单的订单号不能一致
String formData="" ;
formData += "" + WeChatTool.wxspAppid+"";
formData += "" + WeChatTool.mch_id+"";
formData += "" +nonceStr+"";
formData += ""+WeChatTool.month+"";
formData += "" +out_trade_no +"";
formData += "" +money+"";
formData += "" +"127.0.0.1"+"";
formData += "" +WeChatTool.notify_url+"";
formData += "" +WeChatTool.trade_type+"";
formData += "" +openid+""; //appid
formData += "" +WeChatTool.sign_type+"";
formData += "" +sign+""; //签名算法
formData += "";
return formData;
}
工具类:
WXPayUtil.java:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.youquan.utli.WXPayConstants.SignType;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* User: qrn
*
Date: 14-1-28
*
Version: 1.0
* 描述: 工具类
*/
public class WXPayUtil {
/**
* XML格式字符串转换为Map
* @param strXML XML字符串
* @return Map XML数据转换后的Map
* @see Exception
*/
public static Map xmlToMap(String strXML) throws Exception {
try {
Map data = new HashMap();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
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) {
// do nothing
}
return data;
} catch (Exception ex) {
WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
}
}
/**
* 将Map转换为XML格式的字符串
* @param data Map类型数据
* @return XML格式的字符串
* @see Exception
*/
public static String mapToXml(Map data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
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: data.keySet()) {
String value = data.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(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
/**
* 生成6位或10位随机数 param codeLength(多少位)
* @param codeLength 参数
* @return String
*/
public static String createCode(int codeLength) {
String code = "";
for (int i = 0; i < codeLength; i++) {
code += (int) (Math.random() * 9);
}
return code;
}
/**
* 生成带有 sign 的 XML 格式字符串
* @param data Map类型数据
* @param key API密钥
* @see Exception
* @return String 含有sign字段的XML
*/
public static String generateSignedXml(final Map data, String key) throws Exception {
return generateSignedXml(data, key, SignType.MD5);
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名类型
* @see Exception
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map data, String key, SignType signType) throws Exception {
String sign = generateSignature(data, key, signType);
data.put(WXPayConstants.FIELD_SIGN, sign);
return mapToXml(data);
}
/**
* 判断签名是否正确
* @param xmlStr XML格式数据
* @param key API密钥
* @return boolean 签名是否正确
* @see Exception
*/
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
Map data = xmlToMap(xmlStr);
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key).equals(sign);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
* @param data Map类型数据
* @param key API密钥
* @return boolean 签名是否正确
* @see Exception
*/
public static boolean isSignatureValid(Map data, String key) throws Exception {
return isSignatureValid(data, key, SignType.MD5);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return boolean 签名是否正确
* @exception Exception
*/
public static boolean isSignatureValid(Map data, String key, SignType signType) throws Exception {
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key, signType).equals(sign);
}
/**
* 生成签名
* @param data 待签名数据
* @param key API密钥
* @return String
* @see Exception
*/
public static String generateSignature(final Map data, String key) throws Exception {
return generateSignature(data, key, SignType.MD5);
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return String 签名
* @see Exception
*/
public static String generateSignature(final Map data, String key, SignType signType) throws Exception {
Set keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
}
else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
*
*
* @param data 待处理数据
* @return String MD5结果
* @see Exception
*/
public static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 生成 HMACSHA256
* @param data 待处理数据
* @param key 密钥
* @return String 加密结果
* @see Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 日志
* @return Logger
*/
public static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
}
/**
* 获取当前时间戳,单位秒
* @return long
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis()/1000;
}
/**
* 当前时间的下一个月:
* @return String
*/
public static String getcuentime() {
Calendar cal = Calendar.getInstance();
cal.add(cal.MONTH, 1);
SimpleDateFormat dft = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
String preMonth = dft.format(cal.getTime());
System.out.println(preMonth);
return preMonth;
}
/**
* 给时间在加上一个月:
* @param cur 时间参数
* @return Date
*/
public Date getNewDate(Date cur) {
Calendar c = Calendar.getInstance();
c.setTime(cur); //设置时间
c.add(Calendar.MINUTE, 1); //日期分钟加1,Calendar.DATE(天),Calendar.HOUR(小时)
Date date = c.getTime(); //结果
return date;
}
/**
* 获取当前时间戳,单位毫秒
* @return long
*/
public static long getCurrentTimestampMs() {
return System.currentTimeMillis();
}
/**
* 生成 uuid, 即用来标识一笔单,也用做 nonce_str
* @return String
*/
public static String generateUUID() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* map 转换为
* @param map
* @return
*/
public static String sunx(Map map) {
String xmlResult="";
StringBuffer sb = new StringBuffer();
sb.append("" );
for (String key : map.keySet()) {
sb.append("<" + key + ">" + map.get(key) + "" + key + ">");
System.out.println();
}
sb.append("");
xmlResult = sb.toString();
return xmlResult;
}
}
HttpUtils.java:
package com.youquan.utli;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLDecoder;
/**
* User: qrn
*
Date: 14-1-28
*
Version: 1.0
* 描述: http请求的工具类
*/
public class HttpUtils {
private static Logger logger = LoggerFactory.getLogger(HttpUtils.class); //日志记录
/**
* httpPost
*
* @param url 路径
* @param jsonParam 参数
* @return JSONObject
*/
public static JSONObject httpPost(String url, String jsonParam) {
return httpPost(url, jsonParam, false);
}
/**
* post请求
*
* @param url url地址
* @param param 参数
* @param noNeedResponse 不需要返回结果
* @return JSONObject
*/
public static JSONObject httpPost(String url, String param, boolean noNeedResponse) {
//post请求返回结果
HttpClient httpClient = HttpClients.createDefault();
JSONObject jsonResult = null;
HttpPost method = new HttpPost(url);
try {
if (null != param) {
//解决中文乱码问题
StringEntity entity = new StringEntity(param, "utf-8");
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
method.setEntity(entity);
}
HttpResponse result = httpClient.execute(method);
url = URLDecoder.decode(url, "UTF-8");
/**请求发送成功,并得到响应**/
if (result.getStatusLine().getStatusCode() == 200) {
String str = "";
try {
/**读取服务器返回过来的json字符串数据**/
str = EntityUtils.toString(result.getEntity());
System.out.println(str);
if (noNeedResponse) {
return null;
}
/**把json字符串转换成json对象**/
jsonResult = JSONObject.parseObject(str);
} catch (Exception e) {
logger.error("post请求提交失败:" + url, e);
}
}
} catch (IOException e) {
logger.error("post请求提交失败:" + url, e);
}
return jsonResult;
}
/**
* post请求
*
* @param url url地址
* @param param 参数
* @return String
*/
public static String httpXMLPost(String url, String param) {
//post请求返回结果
HttpClient httpClient = HttpClients.createDefault();
String xmlResult = null;
HttpPost method = new HttpPost(url);
try {
if (null != param) {
//解决中文乱码问题
StringEntity entity = new StringEntity(param, "utf-8");
entity.setContentEncoding("utf-8");
entity.setContentType("text/xml");
method.setEntity(entity);
}
HttpResponse result = httpClient.execute(method);
url = URLDecoder.decode(url, "utf-8");
/**请求发送成功,并得到响应**/
if (result.getStatusLine().getStatusCode() == 200) {
try {
/**读取服务器返回过来的json字符串数据**/
xmlResult = EntityUtils.toString(result.getEntity(),"utf-8");
} catch (Exception e) {
logger.error("post请求提交失败:" + url, e);
}
}
} catch (IOException e) {
logger.error("post请求提交失败:" + url, e);
}
return xmlResult;
}
/**
* 发送get请求
* @param url 路径
* @return JSONObject
*/
public static JSONObject httpGet(String url){
//get请求返回结果
JSONObject jsonResult = null;
try {
HttpClient client = HttpClients.createDefault();
//发送get请求
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
/**请求发送成功,并得到响应**/
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
/**读取服务器返回过来的json字符串数据**/
String strResult = EntityUtils.toString(response.getEntity());
/**把json字符串转换成json对象**/
jsonResult = JSONObject.parseObject(strResult);
url = URLDecoder.decode(url, "UTF-8");
} else {
logger.error("get请求提交失败:" + url);
}
} catch (IOException e) {
logger.error("get请求提交失败:" + url, e);
}
return jsonResult;
}
}
好了支付的代码就到这里了,拿到参数在前端使用小程序
wx.requestPayment(
{
'timeStamp': '',
'nonceStr': '',
'package': '',
'signType': 'MD5',
'paySign': '',
'success':function(res){},
'fail':function(res){},
'complete':function(res){}
})
把上面的参数填充就可以调用支付了,如果没有问题那么支付就成功了