在之前记录了一下做微信公众号支付的过程,但是有些混乱,之前做的内个也不是直接接的微信官方,而是转接的别人在接的微信官方,他们赚个手续费,在这之后因为app停用了一段时间,上游公司把我们的appid给关掉了,所以打算从新接,直接接微信官方,好了这是背景。
我们做的是公众号支付,也就是在微信网页内部进行调取支付插件进行支付的一个过程
所以需要到微信官方开通公众号支付 微信官网:https://pay.weixin.qq.com
1、登录后点击产品中心, 点击公众号支付
进入后就会看到这个页面
因为我的已经开通所以就不需要了
这是官方文档 : https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3
点击开发配置
进行配置支付授权目录:也就是你的支付页面所在的目录
一定是生产环境的,微信不支持 ip +端口 形式的地址 异步通知也不支持,
所以测试都需要线上真实环境的域名+支付页面所在目录
登录公众号平台进行配置
公众号的按钮在下面
其次设置你的JS接口安全域名:也就是完整域名如:www.baidu.com
配置到这里基本就算完成了
现在我们需要获取几个必须的参数
appid,mch_id ,加密key
基本配置按钮也在下面
显示appid 点击基本配置就会看到了 如:wxf8xxxxxxxxxfca
mch_id 就是你登录微信商户后台的内个号,如:1594xxxxxxxxx98
key 获取,也是在微信商户后台
这个是自己设置的,看你自己设置了,
1. appid APPID (已有)
wxfxxxxxxxxxxxxxca
2. mch_id 商户ID (已有)
147xxxxxxxxxxx54
3. nonce_str 随机字符串 , 生成UUID就可以了;
UUID.randomUUID().toString().trim().replaceAll("-", "");
4. sign 签名 用WXPayUtil中的generateSignature(finalMap<String, String> data, String key)方法,data是将除了sign外,其他10个参数放到map中,key是四大配置参数中的API秘钥(paternerKey)(这里不要着急管它,最后处理它);
5. body 所支付的名称
6. out_trade_no 自己后台生成的订单号,只要保证唯一就好:如“pay2018062521331”
7. total_fee 支付金额 单位:分,为了测试此值给1,表示支付1分钱
8. spbill_create_ip IP地址 网上很多ip的方法,自己找,此处测试给“127.0.0.1”
9. notify_url 回调地址:这是微信支付成功后,微信那边会带着一大堆参数(XML格式)请求这个地址多次,这个地址做我们业务处理如:修改订单状态,赠送积分等。Ps:支付还没成功还想这么远干嘛,最后再说。地址要公网可以访问。
10. trade_type 支付类型 咱们是公众号支付此处给“JSAPI”
11. openid 支付人的微信公众号对应的唯一标识,每个人的openid在不同的公众号是不一样的,这11个参数里,最费劲的就是他了,其他的几乎都已经解决,现在开发得到这个参数。
获得openid的部分内容应该不属于微信支付的范畴,属于微信公众号网页授权的东西,详情请参考微信网页授权:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
获得openid步骤:
第一步:用户同意授权,获取code
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
注意:1. redirect_uri参数:授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理。
2. scope:用snsapi_base 。
通过此链接可以获取code,可以在一个空页面设置一个a标签,链接至其redirect_uri的地址。点击a标签,即可链接到redirect_uri的地址,并携带code。
- <a href="https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx15c*********&redirect_uri=http%3a%2f%2fwww.***.com%2fpay.jsp&response_type=code&cope=snsapi_base#wechat_redirect">去支付页面pay.jsp并携带code</a>
第二步:通过code换取网页授权access_token(其实微信支付就没有必要获取access_token了,咱们只要其中openid,不是要用户信息,此步结果已经就含有咱们需要的openid了)
获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
上一步的code有了,对于此链接的参数就容易了。可是在页面上如何处理是个问题,我是在pay.jsp页面加载完成后将获取code当做参数传异步到后台,在后台中用http相关类发送get请求(可以自行网上查找)。返回的JSON结果为:
- { "access_token":"ACCESS_TOKEN",
- "expires_in":7200,
- "refresh_token":"REFRESH_TOKEN",
- "openid":"OPENID",//就是它,只要这个值
- "scope":"SCOPE" }
好了都有了,我们就可以开始写拼装参数了, 参数填写修改成你自己的就可以了
HashMap<String, Object> reqMap = new HashMap<String, Object>()
reqMap.put("appid", appid);
reqMap.put("mch_id", mchid);
reqMap.put("nonce_str", getUUid());
reqMap.put("out_trade_no", "订单号");
reqMap.put("total_fee", 金额);
reqMap.put("body", "所支付的名称");
reqMap.put("spbill_create_ip", getIp);
reqMap.put("notify_url", notifyUrl);
// reqMap.put("device_info", "WEB");
reqMap.put("trade_type", "JSAPI");
reqMap.put("openid", openId);
下面是工具类
/** 生成32位编码
* @return string
*/
public static String getUUid() {
String uuid = UUID.randomUUID().toString().trim().replaceAll("-", "");
return uuid;
}
/**
* 获取IP
* @param request
* @return
*/
public String getRemortIP(HttpServletRequest request) {
String remoteAddr = request.getRemoteAddr();
String forwarded = request.getHeader("X-Forwarded-For");
String realIp = request.getHeader("X-Real-IP");
String ip = null;
if (realIp == null) {
if (forwarded == null) {
ip = remoteAddr;
} else {
ip = remoteAddr + "/" + forwarded.split(",")[0];
}
} else {
if (realIp.equals(forwarded)) {
ip = realIp;
} else {
if (forwarded != null) {
forwarded = forwarded.split(",")[0];
}
ip = realIp + "/" + forwarded;
}
}
return ip
}
下面进行签名,MD5工具类会贴出了的
def md5str = createLinkString(reqMap);//排序
System.out.println("---> md5str:" + md5str + "&key=" + wxapiKey);
def sign = MD5Tool.md5(md5str + "&key=" + apiKey, charset).toUpperCase();签名并转大写
reqMap.put("sign", sign)
/**
* SignStr 待签名字符串
* @param params
* @return
*/
public static String createLinkString(Map<String, Object> map) {
System.out.println("带签名排序params:::::" + map.toString())
List<String> keys = new ArrayList<String>(map.keySet());
Collections.sort(keys);
System.out.println("排序完Keys:::::" + keys.toString())
def sb = new StringBuffer()
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
Object value = map.get(key)
System.out.println("key::" + key + "::value::" + value)
if ("sign".equals(key) || "sign_type".equals(key) || value == null || value == "") {
continue;
}
if (i == keys.size() - 1) {
sb.append(key).append("=").append(value)
} else {
sb.append(key).append("=").append(value).append("&")
}
}
def sbStr = sb.toString()
System.out.println("===========签名串:" + sbStr)
return sbStr;
}
package eims;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
//MD5工具类
public class MD5Tool {
public static String md5(byte[] data) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new IllegalStateException("System doesn't support MD5 algorithm.");
}
md5.update(data);
byte[] encoded = md5.digest();
StringBuffer buf = new StringBuffer();
for (int i = 0; i < encoded.length; i++) {
if ((encoded[i] & 0xff) < 0x10) {
buf.append("0");
}
buf.append(Long.toString(encoded[i] & 0xff, 16));
}
return buf.toString();
}
/**
* 签名字符
*
* @param text
* 要签名的字符
* @param key
* 密钥
* 编码格式
* @return 签名结果
*/
public static String sign(String text, String key, String charset) throws Exception {
text = text + key;
return DigestUtils.md5Hex(getContentBytes(text, charset));
}
/**
* @param content
* @param charset
* @return
* @throws java.io.UnsupportedEncodingException
*/
private 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("签名过程中出现错,指定的编码集不对,您目前指定的编码集是:" + charset);
}
}
public static String md5(String str, String charset) {
if (str == null) return null;
try {
return md5( str.getBytes(charset) );
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new IllegalStateException("System doesn't support Charset '" + charset + "'");
}
}
}
到这里所有的参数都拼装好了,微信要的是XML 格式的所以我们需要进行转换
def requestXML = mapGetXML(reqMap);
System.out.println("req xml :\n" + requestXML);
String response = post(wxUrl, requestXML);
/**
* map 转 XML
* @param map
* @return
*/
public static String mapGetXML(Map<String, String> map) {
String xmlStr = null;
StringBuffer sbf = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
sbf.append("<xml>");
for (Map.Entry<String, String> s : map.entrySet()) {
sbf.append("<")
.append(s.getKey())
.append(">")
.append(s.getValue())
.append("</")
.append(s.getKey())
.append(">");
}
sbf.append("</xml>");
xmlStr = sbf.toString();
return xmlStr;
}
XML也转换好了,我们现在可以直接发送请求了 格式xml
请求地址 :https://api.mch.weixin.qq.com/pay/unifiedorder
String response = post(wxUrl, requestXML);
post方法
/**
* 发送xml数据请求到server端
*
* @param url
* xml请求数据地址
* @param xmlString
* 发送的xml数据流
* @return null发送失败,否则返回响应内容
*/
public static String post(String url, String xmlFileName) {
// 关闭
System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true");
System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.commons.httpclient", "stdout");
// 创建httpclient工具对象
HttpClient client = new HttpClient();
// 创建post请求方法
PostMethod myPost = new PostMethod(url);
// 设置请求超时时间
client.setConnectionTimeout(300 * 1000);
String responseString = null;
try {
// 设置请求头部类型
myPost.setRequestHeader("Content-Type", "text/xml");
myPost.setRequestHeader("charset", "utf-8");
// 设置请求体,即xml文本内容,注:这里写了两种方式,一种是直接获取xml内容字符串,一种是读取xml文件以流的形式
// myPost.setRequestBody(xmlString);
// InputStream
// body=this.getClass().getResourceAsStream("/"+xmlFileName);
// myPost.setRequestBody(body);
myPost.setRequestEntity(new StringRequestEntity(xmlFileName, "text/xml", "utf-8"));
int statusCode = client.executeMethod(myPost);
if (statusCode == HttpStatus.SC_OK) {
BufferedInputStream bis = new BufferedInputStream(myPost.getResponseBodyAsStream());
byte[] bytes = new byte[1024];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int count = 0;
while ((count = bis.read(bytes)) != -1) {
bos.write(bytes, 0, count);
}
byte[] strByte = bos.toByteArray();
responseString = new String(strByte, 0, strByte.length, "utf-8");
bos.close();
bis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
myPost.releaseConnection();
return responseString;
}
搞了这么多终于看到点结果了
请求过后 微信端返回的也是XML 不利于我们处理,所以继续转map
Map<String, Object> respMap = xmlString2Map(response);
xml 转map
/**
* Xml string转换成Map
* @param xmlStr
* @return
*/
public static Map<String, Object> xmlString2Map(String xmlStr) {
Map<String, Object> map = new HashMap<String, Object>();
Document doc;
try {
doc = DocumentHelper.parseText(xmlStr);
Element el = doc.getRootElement();
map = recGetXmlElementValue(el, map);
} catch (DocumentException e) {
e.printStackTrace();
}
return map;
}

转好map 后我们就开始取 prepay_id了
搞了这么久就是为了丫的, 取出来后我们还需要把参数拼装一遍,扔到页面,掉起 JS 插件进行支付
参数:
appid 也有
timeStamp 时间戳 你们new Date();即可,因为我语言是 Groovy 所以需要getTime 才是秒数
package prepay_id 已有了
nonceStr 随机数 getuuid方法就可以了
signType 固定值 MD5
sign 上面5个参数的签名结果
这里值得注意的是package 参数, 这个参数可不是简单的吧prepay_id 放进去
,要把 “prepay_id=”这个拼接上里面不能有多余的"或者'符号
之前没有拼接 ,微信支付的时候返回 缺少total_fee参数, 可是上一步给微信传的时候并没有少,微信返回的都成功了
所以还是抛页面的时候出现的错误,害我整了好久。
String retCode = respMap.get("result_code");
if ("SUCCESS".equals(retCode)) {
String prepay_id = respMap.get("prepay_id");
Map map = new HashMap();
map.put("appId", wxappid);
map.put("timeStamp", new Date().getTime());
map.put("package", "prepay_id=" + prepay_id);
map.put("nonceStr", getUUid());
map.put("signType", "MD5");
def newmd5str = createLinkString(map);
System.out.println("ShoppingCartService---> md5str:" + newmd5str + "&key=" + wxapiKey);
def newSign = MD5Tool.md5(newmd5str + "&key=" + wxapiKey, charset).toUpperCase();
map.put("paySign", newSign);
}
参数装完后直接传到页面,看自己框架了,我就不贴了
下面直接把页面贴出了 值得注意的是
package 这个参数 ,在页面是一个域 ,所以在后台传的时候重新改个名 我改的是paypackage
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<title>微信支付</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
</head>
<script>
function onBridgeReady() {
<%
System.out.println("incoming...");
System.out.println(request.getParameter("appId"));
System.out.println(request.getParameter("timeStamp"));
System.out.println(request.getParameter("nonceStr"));
System.out.println(request.getParameter("paypackage"));
System.out.println(request.getParameter("signType"));
System.out.println(request.getParameter("paySign"));
System.out.println('deviceType:::'+request.getParameter("deviceType"));
System.out.println("incoming...");
%>
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId": "<%= request.getParameter("appId") %>", //公众号名称,由商户传入
"timeStamp": "<%= request.getParameter("timeStamp") %>", //时间戳,自1970年以来的秒数
"nonceStr": "<%= request.getParameter("nonceStr") %>", //随机串
"package": "<%= request.getParameter("paypackage") %>",
"signType": "<%= request.getParameter("signType") %>", //微信签名方式:
"paySign": "<%= request.getParameter("paySign") %>" //微信签名,paySign 采用统一的微信支付 Sign 签名生成方法,注意这里 appId 也要参与签名,appId 与 config 中传入的 appId 一致,即最后参与签名的参数有appId, timeStamp, nonceStr, package, signType。
},
function (res) {
if (res.err_msg == "get_brand_wcpay_request:ok") { // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
alert('支付成功!');
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
alert('已取消微信支付!');
} else {
alert('支付失败!' + res.err_msg)
}
/*WeixinJSBridge.call('closeWindow');*/ //关闭微信端窗口
}
);
}
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();
}
</script>
</html>