最近做的一个项目要求实现支付宝手机网站支付和微信公众号内支付,由于是第一次接触,所以开发过程中遇到许多的问题(也有自己读开发文档不细心),为了让大家不重蹈覆辙,少走弯路,以下将介绍具体开发前准备和开发过程。首先先介绍微信公众号内支付,如果最近有时间,将会介绍支付宝手机网站支付详细开发过程。
官方文档看这里
需要准备的4个参数
1)商户ID
3) 商户平台设置的秘钥key(签名时需要)
4)appID
5) appsecret
开发配置
设置支付授权目录 (如何配置请看下文-所遇问题4)
位置:微信支付商户平台中
设置网页授权域名(目的获取openid)
位置:进入服务号 微信公众平台
公众号设置--功能设置--网页授权域名设置
上面盗用微信官方的图,我的是tomcat服务器,所以将下载的 .txt 文件放在了 tomcat安装路径--webapps--ROOT目录下。
公众号内支付时序图(再次盗用微信官方图片)
梳理后流程:
1、微信公众号内选择商品下单;
2、JS将用户的商品数据传给商户服务器,请求生成支付订单;
3、商户后台调用统一下单API向微信服务器发送请求,微信服务器生成一个预付单, 并生成一个prepay_id返回给商户后台;
4、商户后台将返回的prepay_id返回给前端;
5、前端JS内调用getBrandWCPayRequest,发起微信支付请求,进入支付流程 详情点击;
6、用户成功支付点击完成按钮后,商户的前端会收到JavaScript的返回值。商户可直接跳转到支付成功的静态页面进行展示;
7、与此同时微信会把相关支付结果和用户信息异步发送给商户,商户服务器接收处理,并返回应答详情点击;
8、完成。
前端部分(使用AUI)
订单详情 html
订单详情
-
订单详情
-
商品名称
xxxxxx
-
编号
012345678910
-
详情
abcdefghijklmnopqrstuvwxyz
-
金额
¥88.00
支付
订单详情 JS
function toPay(){
document.location.href="pay.html"
}
支付页面 html
支付
-
微信支付
推荐有微信账号的用户使用
-
支付宝支付
推荐有支付宝账号的用户使用
确认支付
支付页面 js
function pay() {
var payType = "";
var radio = document.getElementsByName("payType");
for (i = 0; i < radio.length; i++) {
if (radio[i].checked) {
payType = radio[i].value;
}
};
var orderid=""; //订单编号,为了方便测试,随机生成
for(i=0;i<11;i++){
orderid +=Math.floor(Math.random()*10); ;
}
var data = {
orderid : orderid,
body : 'abcdefghijklmnopqrstuvwxyz',
totalFee : '0.01',
payType : payType //0:微信; 1:支付宝
};
console.log(data);
$.ajax({
type : 'POST',
dataType : 'json',
url : '/wxCourse/weixinPay/pay.action',
data : data,
success : function(data) {
if (data.code == 200) {
if (data.result.payType == "alipay") {
document.write(data.result.html);
document.close();
} else if (data.result.payType == "weixin") {
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(data);
}
// onBridgeReady(data);
} else {
alert("系统异常,请稍后再试!");
}
} else {
alert(data.msg);
}
},
fail : function(data) {
console.log("请求失败!")
}
})
}
function onBridgeReady(data) {
console.log(data)
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId" : data.result.appId, //公众号名称,由商户传入
"timeStamp" : data.result.timeStamp, //时间戳,自1970年以来的秒数
"nonceStr" : data.result.nonceStr, //随机串
"package" : data.result.packageValue,
"signType" : data.result.signType, //微信签名方式:
"paySign" : data.result.paySign, //微信签名
},
function(res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
window.location.href = "http://www.baidu.com"; //可在此设置跳转到支付成功页面
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
alert("您的支付已取消!");
} else {
alert("支付失败!请稍后再试!");
}
}
);
}
后端部分
调用微信支付接口
/**
* 调用微信支付接口
*
* @param orderid
* 商户订单编号
* @param body
* 订单详情
* @param totalFee
* 订单金额
* @param payType
* 支付方式,0微信,1支付宝
* @param request
* @return
*/
@RequestMapping("pay")
@ResponseBody
public ResultVO pay(String orderid, String body, String totalFee, String payType, HttpServletRequest request) {
ResultVO res = new ResultVO();
Map map = new HashMap();
String notify_url = WeixinConfig.NOTIFY_URL; // 设置回调地址
String openid = (String) request.getSession().getAttribute("openid"); // 获取openid。获取用户授权信息后,存在session中,怎样授权自己百度吧
String spbill_create_ip = WeixinUtil.getRemortIP(request); // 获取终端ip
//这地方可添加根据订单号查询订单状态的操作,避免重复支付或返回“total_fee为空提示”,如果订单状态为未支付,则继续;
//已支付或取消,则退出
try {
// 调用统一下单接口,获取prepay_id(微信生成的预支付会话标识)
map = WeiXinPay.getPrepayId(notify_url, spbill_create_ip, body, openid, orderid, totalFee);
String result = (String) map.get("result"); // success
// 表示调用统一下单接口并返回prepay_id成功;error表示失败
if ("SUCCESS".equals(result)) {
res.setCode(ResponseCode.SUCCESS_CODE);
res.setMsg(ResponseCode.SUCCESS_MSG);
res.setResult(map.get("params"));
} else {
res.setCode(ResponseCode.FAIL_CODE);
res.setMsg(ResponseCode.FAIL_MSG);
}
} catch (Exception e) {
logger.error("[调用微信支付接口]异常:", e);
res.setCode(ResponseCode.EXCEPTION_CODE);
res.setMsg(ResponseCode.EXCEPTION_MSG);
}
return res;
}
调用统一下单接口,获取prepay_id
/**
* 调用统一下单接口,获取prepay_id(预支付会话标识)
* @param notify_url 异步接收微信支付结果通知的回调地址
* @param spbill_create_ip 终端IP
* @param orderDetail 商品描述
* @param openid 用户标识
* @param orderno 商户订单号
* @param money 标价金额
* @return
* @throws Exception
*/
public static Map getPrepayId(String notify_url,
String spbill_create_ip,String orderDetail,String openid,String orderno,String money) throws Exception{
SortedMap
支付完成回调,接收微信服务器发送的相关支付结果和用户信息,并应答。
(摘自公众号支付文档)特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额
是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
/**
* 后端回调,统一下单API notify_url参数设置的路径
*/
@RequestMapping("callBack")
@ResponseBody
public String callBack(HttpServletRequest request) {
String result = "";
try {
//获取返回参数
SortedMap map = WeixinUtil.getReqParams(request);
if (map.get("return_code").equals("SUCCESS")) {
// 验证签名
if (WeixinUtil.checkSign(map)) {
logger.info("[验证签名成功]");
// 这个地方可添加更改商户订单状态操作
result = WeixinUtil.setXML("SUCCESS", "");
return result;
} else {
logger.info("[验证签名失败]");
}
}
} catch (Exception e) {
logger.error("[微信支付后端回调异常]", e);
}
result = WeixinUtil.setXML("FAIL", "");
return result;
}
工具类
WeixinUtil工具类
package com.gusy.pay.weixin.utils;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
/**
* 微信支付工具类
* @author gusy
* @date 2017年9月11日,下午5:23:07
*/
public class WeixinUtil {
private static Logger logger=Logger.getLogger(WeixinUtil.class);
/**
* 生成随机串
* @return
*/
public static String create_nonce_str() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 生成时间戳
* @return
*/
private static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
/**
* 获取ip
* @param request
* @return
*/
public static String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
return request.getHeader("x-forwarded-for");
}
/**
* 获取请求xml
* @param parameters
* @return
*/
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 k = (String)entry.getKey();
String v = (String)entry.getValue();
if ("attach".equalsIgnoreCase(k)||"body".equalsIgnoreCase(k)||"sign".equalsIgnoreCase(k)) {
sb.append("<"+k+">"+"");
}else {
sb.append("<"+k+">"+v+"");
}
}
sb.append(" ");
return sb.toString();
}
/**
* 返回数据为xml格式的post请求
* @param url
* @param outStr
* @return
* @throws Exception
*/
public static Map doPostStrXML(String url,String outStr) throws Exception{
HttpClient client = new DefaultHttpClient();
HttpPost httpost = new HttpPost(url);
httpost.setEntity(new StringEntity(outStr,"UTF-8"));
HttpResponse response = client.execute(httpost);
String result = EntityUtils.toString(response.getEntity(),"UTF-8");
Map doXMLParse = XmlUtils.doXMLParse(result);
return doXMLParse;
}
/**
* 后端回调,应答微信xml
* @param return_code
* @param return_msg
* @return
*/
public static String setXML(String return_code, String return_msg) {
return " ";
}
/**
* 创建签名
* @param characterEncoding
* @param parameters
* @return
*/
public static String createSign(String characterEncoding,SortedMap parameters){
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + WeixinConfig.API_KEY);
String sign = DigestUtils.md5Hex(sb.toString()).toUpperCase();
return sign;
}
/**
* 签名验证
* @return
*/
public static boolean checkSign(SortedMap params){
String sign = params.get("sign").toString();//签名
String mSign = createSign(WeixinConfig.CHARSET, params);
if(sign.equals(mSign)){
return true;
}
return false;
}
/**
* 获取支付回调请求参数
* @param request
* @return
*/
public static SortedMap getReqParams(HttpServletRequest request) throws Exception {
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);
}
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(),"utf-8");//获取微信调用我们notify_url的返回信息
Map map = XmlUtils.doXMLParse(result);
SortedMap dd = new TreeMap();
for(Object keyValue : map.keySet()){
dd.put(keyValue, map.get(keyValue));
logger.info("========>>>>微信付款成功回调:"+keyValue+"="+map.get(keyValue));
}
return dd;
}
}
xmlUtil工具类
package com.gusy.pay.weixin.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
/**
* 解析xml文件
*/
public class XmlUtils {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
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();
//SortedMap m = new TreeMap();
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;
}
/**
* 获取子结点的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("");
}
}
return sb.toString();
}
}
1、遇到“支付参数签名失败”,看生成签名的参数是否多加或缺少;
2、遇到“下单账号和支付账号不一致”,那肯定是你的openID是写死的
解决方法:调用 微信网页授权接口,获取openID
3、微信H5内调起支付,返回“get_brand_wcpay_request:fail”,
原因:大概率是支付回调路径配置错误;
4、如何正确配置 支付授权目录:
保留支付路径最后一个“/”(包括)及之前内容。
例:假如我的支付路径为 http://test.gusy.com/wxCourse/static/pay.html
则添加的路径为 http://test.gusy.com/wxCourse/static/;
这个地方我还遇到了一个坑:
做项目时自己一直使用本人的iphone手机测试,可以完美支付;当换用android手机测时,总是返回“get_brand_wcpay_request:fail”,
其间花费了大量时间和精力找解决方法,包括换用 微信JSSDK中调起支付的方法,全部失败!!!
最终问题答案有些意想不到: iOS和Android 所配置支付路径不一样!!!
小结:
如果你们遇到iOS和Android中有一个可以成功支付,但另一个平台不能支付, 那你们就要看看是不是支付路径不一样了 。
本文只是本人写的的一个测试demo,没有数据库相关操作,真实场景肯定需要商户后台生成一个订单并存入数据库,支付成功后修改订单状态等。需要的自己按需添加吧。
还有用户授权部分的代码没有放上去,比较简单点击查看微信开发文档。