记一次微信支付JSAPI支付功能集成。
JSAPI支付前提是必须在微信环境中(公众号、聊天界面打开链接),否则无法调起支付。小程序webview控件中也无法调起支付。
准备工作:
1.首先在微信商户号中开通JSAPI支付功能(ps:我的一打开就是开通的,可能之前的同事开通的,so开通流程无法复现)。开通方式如下图:
2.申请开通之后在开发配置中添加域名,按照提示添加即可。
3.然后登录微信公众号后台,左侧菜单栏最下方,点击接口权限,然后左侧找到网页授权,点击其右边修改,在到新页面网页授权域名栏点右边设置,添加自己的支付链接域名。步骤图示:
准备工作完成。
开发工作 使用vue写的页面
1.由于JSAPI支付时需要用户openid,所以先获取openid
1.1 在页面js中加入以下代码:
reload(){
let redirect_uri = window.location.href;
let state = window.location.href;
window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+API.APPID+"&redirect_uri="+redirect_uri+"&response_type=code&scope=snsapi_base&state="+state+"#wechat_redirect"
}
代码中的API.APPID要换成自己的公众号AppId,其他字段可继续使用。
scope参数的值有两个可选,一个是snsapi_base,另一个是snsapi_userinfo。
关于网页授权的两种scope的区别说明
1.1.1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
1.1.2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
这里我们只是获取用户openid,无需用户手动同意,所以用snsapi_base。
在页面打开时判断openid是否已获取,未获取时调用reload()方法。
initData(){
let self = this
if(self.getQueryParam("code")){
let code = self.getQueryParam("code") //getQueryParam()方法在下方
API.request({ //api.js 文件在下方。
url:"自己的后台接口",
type:'请求方式', //get post
data:{
code:code //页面通过reload()方法获取到的code 重定向后参数是拼接在页面链接上的 需要自己获取 。
},
success:res=>{
if(res.errcode === 200 && res.data != undefined){
self.$data.openID = res.data
}else{
self.reload();
}
}
})
}else{
self.reload()
}
}
getQueryParam(variable){
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i
后台通过前端传的code参数,获取到用户openid后返回给前端保存。
2.下单
用户点击页面支付功能后,前端调用后台接口生成预付单并返回数据,前端解析诗句获取各种参数然后入下:
pay(){
let self = this
API.request({
url:"自己的下单接口",
type:'POST',
data:{
//下单需要的参数
},
success:res=>{
if(res.errcode === 200){
let data = res.data
//下面代码调起微信支付
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":data .appid, //公众号id
"timeStamp":data .timeStamp, //时间戳,自1970年以来的秒数
"nonceStr":data .nonceStr, //随机串
"package":data .package,
"signType":"MD5", //微信签名方式:通用参数 可不改
"paySign":data .paySign //微信签名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ){
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
//支付成功 请求接口确认 刷新页面
}
});
}else {
//失败处理
}
}
})
}
前端代码完成。
后台代码:
1.获取openid
/**
* 网页支付 获取openid
*
* @param code
* @return
*/
@PostMapping("getOpenID")
private String getOpenID(@RequestParam("code") String code) {
JSONObject accessToken = getAccessToken(code);
return accessToken.getString("openid");
}
public static JSONObject getAccessToken(String code){
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="公众号AppID"&secret="公众号秘钥"&code="+code+"&grant_type=authorization_code";
String result = HttpUtils.httpGet(url);
JSONObject data = com.alibaba.fastjson.JSON.parseObject(result);
return data;
}
/**
* 发送get请求
* @param url 路径
* @return
*/
public static String httpGet(String url){
//get请求返回结果
String strResult = null;
try {
DefaultHttpClient client = new DefaultHttpClient();
//发送get请求
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
/**请求发送成功,并得到响应**/
if (response.getStatusLine().getStatusCode() == org.apache.http.HttpStatus.SC_OK) {
/**读取服务器返回过来的json字符串数据**/
strResult = EntityUtils.toString(response.getEntity(),"UTF-8");
} else {
}
} catch (IOException e) {
}
return strResult;
}
获取openid完成。
- 调用微信支付统一下单接口
2.1 payUtils工具类
import org.apache.commons.codec.digest.DigestUtils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.json.JSONObject;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
public class PayUtil {
public static String APP_ID = "";
public static JSONObject wxPay(String appID, String openid, String bodys, String orderNos, String price, String notify_url) {
JSONObject json = new JSONObject();
try {
//生成的随机字符串
String nonce_str = Utils.genNonceStr();
//获取本机的ip地址
String spbill_create_ip = "127.0.0.1";
Map packageParams = new HashMap();
packageParams.put("appid", appID);
packageParams.put("mch_id", WXPayUtil.MCH_ID);
packageParams.put("nonce_str", nonce_str);
packageParams.put("body", bodys);//商品名称
packageParams.put("out_trade_no", orderNos);//商户订单号
packageParams.put("total_fee", price);
packageParams.put("spbill_create_ip", spbill_create_ip);
packageParams.put("notify_url", notify_url);
packageParams.put("trade_type", "JSAPI");
packageParams.put("openid", openid);
// 除去数组中的空值和签名参数
packageParams = paraFilter(packageParams);
String prestr = createLinkString(packageParams); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
//MD5运算生成签名,这里是第一次签名,用于调用统一下单接口
String mysign = sign(prestr, WXPayUtil.API_KEY, "utf-8").toUpperCase();
// System.out.println("=======================第一次签名:" + mysign + "=====================");
//拼接统一下单接口使用的xml数据,要将上一步生成的签名一起拼接进去
String xml = "" + "" + appID + " "
+ ""
+ "" + WXPayUtil.MCH_ID + " "
+ "" + nonce_str + " "
+ "" + notify_url + " "
+ "" + openid + " "
+ "" + orderNos + " "
+ "" + spbill_create_ip + " "
+ "" + price + " "
+ "" + "JSAPI" + " "
+ "" + mysign + " "
+ " ";
// System.out.println("调试模式_统一下单接口 请求XML数据:" + xml);
//调用统一下单接口,并接受返回的结果
String result = httpRequest(pay_url, "POST", xml);
// System.out.println("调试模式_统一下单接口 返回XML数据:" + result);
// 将解析结果存储在HashMap中
Map map = doXMLParse(result);
// System.out.println("map:"+map);
String return_code = (String) map.get("return_code");//返回状态码
//返回给移动端需要的参数
Map response = new HashMap();
if (return_code == "SUCCESS" || return_code.equals(return_code)) {
// 业务结果
String prepay_id = (String) map.get("prepay_id");//返回的预付单信息
// System.out.println("prepay_id:"+prepay_id);
response.put("nonceStr", nonce_str);
response.put("package", "prepay_id=" + prepay_id);
Long timeStamp = System.currentTimeMillis() / 1000;
response.put("timeStamp", timeStamp + "");//这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
String stringSignTemp = "appId=" + appID + "&nonceStr=" + nonce_str + "&package=prepay_id=" + prepay_id + "&signType=" + "MD5" + "&timeStamp=" + timeStamp;
//再次签名,这个签名用于小程序端调用wx.requesetPayment方法
String paySign = sign(stringSignTemp, WXPayUtil.API_KEY, "utf-8").toUpperCase();
System.out.print("=======================第二次签名:" + paySign + "=====================");
response.put("paySign", paySign);
//更新订单信息
//业务逻辑代码
}
response.put("appid", appID);
json.put("errMsg", "OK");
json.put("data", response);
} catch (Exception e) {
e.printStackTrace();
json.put("errMsg", "Failed");
}
return json;
}
/**
* 签名字符串
*
* @param text 需要签名的字符串
* @param key 密钥
* @param input_charset 编码格式
* @return 签名结果
*/
public static String sign(String text, String key, String input_charset) {
text = text + "&key=" + key;
return DigestUtils.md5Hex(getContentBytes(text, input_charset));
}
/**
* 签名字符串
*
* @param text 需要签名的字符串
* @param sign 签名结果
* @param key 密钥
* @param input_charset 编码格式
* @return 签名结果
*/
public static boolean verify(String text, String sign, String key, String input_charset) {
text = text + key;
String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));
if (mysign.equals(sign)) {
return true;
} else {
return false;
}
}
/**
* @param content
* @param charset
* @return
* @throws UnsupportedEncodingException
*/
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);
}
}
/**
* 生成6位或10位随机数 param codeLength(多少位)
*
* @return
*/
public static String createCode(int codeLength) {
String code = "";
for (int i = 0; i < codeLength; i++) {
code += (int) (Math.random() * 9);
}
return code;
}
@SuppressWarnings("unused")
private static boolean isValidChar(char ch) {
if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
return true;
if ((ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f))
return true;// 简体中文汉字编码
return false;
}
/**
* 除去数组中的空值和签名参数
*
* @param sArray 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
public static Map paraFilter(Map sArray) {
Map result = new HashMap();
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;
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param params 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
public static String createLinkString(Map params) {
List keys = new ArrayList(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 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);
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
return buffer.toString();
}
public static String urlEncodeUTF8(String source) {
String result = source;
try {
result = java.net.URLEncoder.encode(source, "UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
*
* @param strxml
* @return
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws Exception {
if (null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = String2Inputstream(strxml);
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if (children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}
/**
* 获取子结点的xml
*
* @param children
* @return String
*/
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if (!children.isEmpty()) {
Iterator it = children.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if (!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("" + name + ">");
}
}
return sb.toString();
}
public static InputStream String2Inputstream(String str) {
return new ByteArrayInputStream(str.getBytes());
}
public static void wxNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader((ServletInputStream) request.getInputStream()));
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
//sb为微信返回的xml
String notityXml = sb.toString();
String resXml = "";
// System.out.println("接收到的报文:" + notityXml);
Map map = doXMLParse(notityXml);
String returnCode = (String) map.get("return_code");
if ("SUCCESS".equals(returnCode)) {
//验证签名是否正确
if (verify(createLinkString(map), (String) map.get("sign"), WXPayUtil.API_KEY, "utf-8")) {
/**此处添加自己的业务逻辑代码start**/
/**此处添加自己的业务逻辑代码end**/
//通知微信服务器已经支付成功
resXml = "" + " "
+ " " + " ";
}
} else {
resXml = "" + " "
+ " " + " ";
}
// System.out.println(resXml);
// System.out.println("微信支付回调数据结束");
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}
}
2.2 在接口中调用方式:
//微信支付 参数说明
//APP_ID 必须是公众号appid
//openid 下单用户的openid 前端传过来的
//str 商品简单描述
//out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一
//"1" 商品价格 这里的价格是*100后的价格
//NOTIFY_URL 支付回调地址
org.json.JSONObject objects = PayUtil .wxPay(APP_ID,openid, str, out_trade_no, "1", NOTIFY_URL);//
System.out.println(objects);
String errMsg = objects.getString("errMsg");
if (errMsg.equals("OK")) {
org.json.JSONObject data = objects.getJSONObject("data");
// System.out.println(data);
Map map = new HashMap<>();
map.put("timeStamp", data.getString("timeStamp"));
map.put("package", data.getString("package"));
map.put("paySign", data.getString("paySign"));
map.put("appid", data.getString("appid"));
map.put("nonceStr", data.getString("nonceStr"));
return map;
}
2.3 支付回调
/**
* jsApi H5广告微信支付回调
* @param request
* @param response
* @return
*/
@RequestMapping(value = "自定义name")
private String 自定义name(HttpServletRequest request, HttpServletResponse response) {
Map return_data = new HashMap();
try {
InputStream inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
String resultxml = new String(outSteam.toByteArray(), "utf-8");
// System.out.println("resultxml:-----------------");
// System.out.println(resultxml);
// System.out.println("---------------------------");
Map params = PayCommonUtil.doXMLParse(resultxml);
outSteam.close();
inStream.close();
if (!PayCommonUtil.isTenpaySign(params)) {
// 支付失败
System.out.println("===============信支付回调验签失败==============");
return_data.put("return_code", "FAIL");
return_data.put("return_msg", "return_code不正确");
return StringUtil.GetMapToXML(return_data);
} else {
// ------------------------------
// 处理业务开始
// ------------------------------
String total_fee = params.get("total_fee");
double v = Double.valueOf(total_fee) / 100;
String tradeNo = params.get("transaction_id");
String out_trade_no = String.valueOf(Long.parseLong(params.get("out_trade_no").split("O")[0]));
//处理业务逻辑
return_data.put("return_code", "SUCCESS");
return_data.put("return_msg", "OK");
return StringUtil.GetMapToXML(return_data);
}
}
} catch (
IOException e) {
e.printStackTrace();
} catch (
JDOMException e) {
e.printStackTrace();
}
return_data.put("return_code", "FAIL");
return_data.put("return_msg", "out_trade_no不正确");
return StringUtil.GetMapToXML(return_data);
}
PayCommonUtil工具类
public class PayCommonUtil {
//请求xml组装
public static String getRequestXml(SortedMap parameters){
StringBuffer sb = new StringBuffer();
sb.append("");
Set es = parameters.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String key = (String)entry.getKey();
String value = (String)entry.getValue();
if ("attach".equalsIgnoreCase(key)||"body".equalsIgnoreCase(key)||"sign".equalsIgnoreCase(key)) {
sb.append("<"+key+">"+""+key+">");
}else {
sb.append("<"+key+">"+value+""+key+">");
}
}
sb.append(" ");
return sb.toString();
}
/**
* 验证回调签名
* @return
*/
public static boolean isTenpaySign(Map map) {
String characterEncoding="utf-8";
String charset = "utf-8";
String signFromAPIResponse = map.get("sign");
if (signFromAPIResponse == null || signFromAPIResponse.equals("")) {
System.out.println("API返回的数据签名数据不存在,有可能被第三方篡改!!!");
return false;
}
// System.out.println("服务器回包里面的签名是:" + signFromAPIResponse);
//过滤空 设置 TreeMap
SortedMap packageParams = new TreeMap();
for (String parameter : map.keySet()) {
String parameterValue = map.get(parameter);
String v = "";
if (null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
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(!"sign".equals(k) && null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
//将API返回的数据根据用签名算法进行计算新的签名,用来跟API返回的签名进行比较
//算出签名
String resultSign = "";
String tobesign = sb.toString();
if (null == charset || "".equals(charset)) {
resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
}else{
try{
resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
}catch (Exception e) {
resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
}
}
String tenpaySign = ((String)packageParams.get("sign")).toUpperCase();
// System.out.println("返回数据-tenpaySign:"+tenpaySign);
// System.out.println("resultSign:"+resultSign);
// System.out.println(tenpaySign.equals(resultSign));
return tenpaySign.equals(resultSign);
}
//请求方法
public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
try {
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
System.out.println("连接超时:{}"+ ce);
} catch (Exception e) {
System.out.println("https请求异常:{}"+ e);
}
return null;
}
//xml解析
public static Map doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if(children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if(!children.isEmpty()) {
Iterator it = children.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if(!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("" + name + ">");
}
}
return sb.toString();
}
public static String GetMapToXML(Map param){
StringBuffer sb = new StringBuffer();
sb.append("");
for (Map.Entry entry : param.entrySet()) {
sb.append("<"+ entry.getKey() +">");
sb.append(entry.getValue());
sb.append(""+ entry.getKey() +">");
}
sb.append(" ");
return sb.toString();
}
}
结束!