文章目录
前言
关于微信支付很早之前做过一次,近期项目再次使用就简单的记录一下。微信公众账号开通及微信支付认证的相关流程就不在这里做介绍了、本次单从代码实现上出一个完整案例。
一、JSAPI支付场景及逻辑
支付场景的交互细节,设计商户页面的逻辑:
- 用户打开商户网页选购商品,发起支付,在网页通过JavaScript调用getBrandWCPayRequest接口,发起微信支付请求,用户进入支付流程。
- 用户成功支付点击完成按钮后,商户的前端会收到JavaScript的返回值。商户可直接跳转到支付成功的静态页面进行展示。
-
商户后台收到来自微信开放平台的支付成功回调通知,标志该笔订单支付成功。
注:2和3的触发不保证遵循严格的时序。JS API返回值作为触发商户网页跳转的标志,但商户后台应该只在收到微信后台的支付成功回调通知后,才做真正的支付成功的处理。
二、开发步骤
1.设置支付目录
支付授权目录说明:
-
商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”,例如:https://www.weixin.com/pay.php。
-
商户实际的支付目录必须和在微信支付商户平台设置的一致,否则会报错 “ 当前页面的URL未注册:”
支付授权目录设置说明:
- 登录微信支付商户平台(pay.weixin.qq.com)–>产品中心–>开发配置,设置后一般5分钟内生效。
支付授权目录校验规则说明:
- 如果支付授权目录设置为顶级域名(例如:https://www.weixin.com/ ),那么只校验顶级域名,不校验后缀;
-
如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为https://www.weixin.com/abc/123/,则实际请求页面目录不能为https://www.weixin.com/abc/,也不能为https://www.weixin.com/abc/123/pay/,必须为https://www.weixin.com/abc/123/
微信JSAPI支付-支付目录配置如图:
2.设置授权域名
开发JSAPI支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败。具体界面如图所示:
3.业务流程
商户系统和微信支付系统主要交互:
- 商户server调用统一下单接口请求订单,api参见公共api【统一下单API】
- 商户server可通过【JSAPI调起支付API】调起微信支付,发起支付请求。
- 商户server接收支付通知,api参见公共api【支付结果通知API】
- 商户server查询支付结果,api参见公共api【查询订单API】
三、代码设计
1. 支付页面
按照上述支付流程,从页面发起支付请求,支付页面代码如下:
<div class="login-button">
<div class="button" onClick="doPay()">确认支付</div>
</div>
<script type="text/javascript">
var appid,timeStamp,nonceStr,packageValue,signType,paySign;
function doPay(){
$.ajax({
url : '${ctx}/pay/unifiedorder',
type : 'POST',
data:{'orderId':${order.id}},//这里传入项目所需的订单信息例如订单金额等
success : function(obj) {
//ar obj = eval('(' + data + ')');
if(parseInt(obj.agent)<5){
alert("您的微信版本低于5.0无法使用微信支付");
return;
}
appid = obj.appId, //公众号名称,由商户传入
timeStamp = obj.timeStamp, //时间戳,自 1970 年以来的秒数
nonceStr = obj.nonceStr, //随机串
packageValue = obj.packageValue, //<span style="font-family:微软雅黑;>商品包信息</span>
signType = obj.signType, //微信签名方式:
paySign = obj.paySign//微信签名
try {
onBridgeReady();
} catch (e) {
}
}
});
}
</script>
 : : : :代码包含详细注释,此次只关注
doPay()
方法的调用 ,前端发起调用,可以看见调用的接口为
/pay/unifiedorder
。 接着我们来看后端代码的实现;
2. Controller
@RequestMapping("/pay/unifiedorder")
@ResponseBody
public SortedMap<Object, Object> payparm(HttpServletRequest request, HttpServletResponse response){
String orderId = request.getParameter("orderId");
System.out.println(orderId);
/** 用户订单信息 **/
WrUserOrderEntity wrUserOrderEntity = wrUserOrderService.findById(WrUserOrderEntity.class, Long.valueOf(orderId));
int total_fee = (int) (wrUserOrderEntity.getCountPrice() * 100);
/** 用户信息 **/
BnkUserEntity userEntity = bnkUserService.findById(BnkUserEntity.class, wrUserOrderEntity.getUserId());
SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
parameters.put("appid", ConfigUtil.APPID);
parameters.put("mch_id", ConfigUtil.MCH_ID);
parameters.put("nonce_str", PayCommonUtil.CreateNoncestr());
parameters.put("body", NAME_ORDER);
parameters.put("out_trade_no", orderId);
parameters.put("total_fee", total_fee + "");
parameters.put("spbill_create_ip",getRemoteHost(request));
parameters.put("notify_url", ConfigUtil.NOTIFY_URL);
parameters.put("trade_type", "JSAPI");
parameters.put("openid", userEntity.getOpenId());
//获取签名
String sign = PayCommonUtil.createSign("UTF-8", parameters);
parameters.put("sign", sign);
String requestXML = PayCommonUtil.getRequestXml(parameters);
log.info("UNIFIEDORDER 请求参数:"+requestXML);
/**注意:此处调用统一下单接口 **/
String result =CommonUtil.httpsRequest(ConfigUtil.UNIFIED_ORDER_URL, "POST", requestXML);
log.info("UNIFIEDORDER 请求返回:"+result);
try {
//解析微信返回的信息,以Map形式存储便于取值
Map<String, String> map = XMLUtil.doXMLParse(result);
SortedMap<Object,Object> params = new TreeMap<Object,Object>();
params.put("appId", ConfigUtil.APPID);
params.put("timeStamp",System.currentTimeMillis());
params.put("nonceStr", PayCommonUtil.CreateNoncestr());
params.put("package", "prepay_id="+map.get("prepay_id"));
params.put("signType", ConfigUtil.SIGN_TYPE);
String paySign = PayCommonUtil.createSign("UTF-8", params);
//这里用packageValue是预防package是关键字在js获取值出错
params.put("packageValue", "prepay_id="+map.get("prepay_id"));
//paySign的生成规则和Sign的生成规则一致
params.put("paySign", paySign);
//付款成功后跳转的页面
params.put("sendUrl", ConfigUtil.SUCCESS_URL);
String userAgent = request.getHeader("user-agent");
char agent = userAgent.charAt(userAgent.indexOf("MicroMessenger")+15);
//微信版本号,用于前面提到的判断用户手机微信的版本是否是5.0以上版本。
params.put("agent", new String(new char[]{agent}));
return params;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@RequestMapping("/paySuccess")
public void paySuccess(HttpServletRequest request, HttpServletResponse response) 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);
}
System.out.println("~~~~~~~~~~~~~~~~付款成功~~~~~~~~~");
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(),"utf-8");//获取微信调用我们notify_url的返回信息
Map<Object, Object> map = XMLUtil.doXMLParse(result);
for(Object keyValue : map.keySet()){
System.out.println(keyValue+"="+map.get(keyValue));
}
if (map.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
//TODO 对数据库的操作
response.getWriter().write(PayCommonUtil.setXML("SUCCESS", "")); //告诉微信服务器,我收到信息了,不要在调用回调action了
System.out.println("-------------"+PayCommonUtil.setXML("SUCCESS", ""));
}
}
/**
* 回调
* @param request
* @param response
* @throws Exception
*/
@RequestMapping("/notify")
public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("回调成功");
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);
}
System.out.println("~~~~~~~~~~~~~~~~付款成功~~~~~~~~~");
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(),"utf-8");//获取微信调用我们notify_url的返回信息
Map<Object, Object> map = XMLUtil.doXMLParse(result);
for(Object keyValue : map.keySet()){
System.out.println(keyValue+"="+map.get(keyValue));
}
if (map.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
//更新订单信息写库等操作.
String orderId = (String) map.get("out_trade_no");
//告诉微信服务器,我收到信息了,不要在调用回调action了
response.getWriter().write(PayCommonUtil.setXML("SUCCESS", ""));
System.out.println("-------------"+PayCommonUtil.setXML("SUCCESS", ""));
}
}
public String getRemoteHost(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
}
注意:此方法中涉及
openId
,这个需要根据各位的业务场景自行设计获取,我们项目中在设计之初使用了手机号绑定微信的操作,已经绑定在用户信息中。要注意的是微信
openId
的获取有两种授权方式,如果仅需要
openId
,则可以使用静默授权,需要用户头像及昵称等,需要使用 非静默授权:
type: snsapi_base
: 静默 ;
snsapi_userinfo
:非静默授权页面。
上述方法中有
notify()
为支付结果通知回调方法-异步,
paySuccess()
为支付成功后跳转页面,由于本次使用微信前端拉起支付所以忽略
paySuccess()
的页面跳转方法;参数说明详见
微信支付官方文档
;
3. JS调起支付
在微信浏览器里面打开H5网页中执行JS调起支付。接口输入输出数据格式为JSON。接着看支付页面代码根据统一下单接口调用返回相关参数 调用
onBridgeReady()
方法 ;
注意:WeixinJSBridge内置对象在其他浏览器中无效。
getBrandWCPayRequest
参数以及返回值定义
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"wx2421b1c4370ec43b", //公众号名称,由商户传入
"timeStamp":"1395712654", //时间戳,自1970年以来的秒数
"nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串
"package":"prepay_id=u802345jgfjsdfgsdg888",
"signType":"MD5", //微信签名方式:
"paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ){
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
});
}
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();
}
get_brand_wcpay_request:ok 支付成功
get_brand_wcpay_request:cancel 支付过程中用户取消
get_brand_wcpay_request:fail 支付失败
调用支付JSAPI缺少参数:total_fee
1、请检查预支付会话标识prepay_id是否已失效
2、请求的appid与下单接口的appid是否一致
注:JS API的返回结果get_brand_wcpay_request:ok仅在用户成功完成支付时返回。由于前端交互复杂,get_brand_wcpay_request:cancel或者get_brand_wcpay_request:fail可以统一处理为用户遇到错误或者主动放弃,不必细化区分。
至此微信支付流程全部完成;js判断支付成功后可继续执行业务逻辑;
4. 工具类
- 配置工具类
这里面整理了微信支付相关的所有配置属性及接口可直接使用,本案例只使用了
统一下单接口
, 当微信拉起支付成功后微信会调用
统一回调接口
及
支付成功后跳转地址
public class ConfigUtil {
/**
* 服务号相关信息
*/
public final static String APPID = "";//服务号的应用号
public final static String APP_SECRECT = "";//服务号的应用密码
//public final static String TOKEN = "weixinCourse";//服务号的配置token
public final static String MCH_ID = "";//商户号
public final static String API_KEY = "";//API密钥
public final static String SIGN_TYPE = "MD5";//签名加密方式
// public final static String CERT_PATH = "D:/apiclient_cert.p12";//微信支付证书存放路径地址,退款接口需要使用证书
public final static String CERT_PATH = "/home/cert/apiclient_cert.p12";//微信支付证书存放路径地址、退款接口需要使用证书
//微信支付统一接口的回调action
public final static String NOTIFY_URL = "http://****/pay/notify";
//微信支付成功支付后跳转的地址
public final static String SUCCESS_URL = "http://****/pay/paySuccess";
//oauth2授权时回调action
public final static String REDIRECT_URI = "http://****/pay/authcode";
/**
* 微信基础接口地址
*/
//获取token接口(GET)
public final static String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
//oauth2授权接口(GET)
public final static String OAUTH2_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
//刷新access_token接口(GET)
public final static String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN";
// 菜单创建接口(POST)
public final static String MENU_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
// 菜单查询(GET)
public final static String MENU_GET_URL = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN";
// 菜单删除(GET)
public final static String MENU_DELETE_URL = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN";
/**
* 微信支付接口地址
*/
//微信支付统一接口(POST)
public final static String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
//微信退款接口(POST)
public final static String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
//订单查询接口(POST)
public final static String CHECK_ORDER_URL = "https://api.mch.weixin.qq.com/pay/orderquery";
//关闭订单接口(POST)
public final static String CLOSE_ORDER_URL = "https://api.mch.weixin.qq.com/pay/closeorder";
//退款查询接口(POST)
public final static String CHECK_REFUND_URL = "https://api.mch.weixin.qq.com/pay/refundquery";
//对账单接口(POST)
public final static String DOWNLOAD_BILL_URL = "https://api.mch.weixin.qq.com/pay/downloadbill";
//短链接转换接口(POST)
public final static String SHORT_URL = "https://api.mch.weixin.qq.com/tools/shorturl";
//接口调用上报接口(POST)
public final static String REPORT_URL = "https://api.mch.weixin.qq.com/payitil/report";
}
上述配置文件中所需要的
API_KEY、APP_SECRECT
秘钥等,均在商户在微信公众平台或开放平台提交微信支付申请,微信支付工作人员审核资料无误后开通相应的微信支付权限。微信支付申请审核通过后,商户在申请资料填写的邮箱中收取到由微信支付小助手发送的邮件,此邮件包含开发时需要使用的支付账户信息;
- 随机字符串生成工具类
public class PayCommonUtil {
private static Logger log = LoggerFactory.getLogger(PayCommonUtil.class);
public static String CreateNoncestr(int length) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
String res = "";
for (int i = 0; i < length; i++) {
Random rd = new Random();
res += chars.indexOf(rd.nextInt(chars.length() - 1));
}
return res;
}
public static String CreateNoncestr() {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
String res = "";
for (int i = 0; i < 16; i++) {
Random rd = new Random();
res += chars.charAt(rd.nextInt(chars.length() - 1));
}
return res;
}
public static String createSign(String characterEncoding,SortedMap<Object,Object> 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=" + ConfigUtil.API_KEY);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
public static String getRequestXml(SortedMap<Object,Object> parameters){
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
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+">"+"<![CDATA["+v+"]]></"+k+">");
}else {
sb.append("<"+k+">"+v+"</"+k+">");
}
}
sb.append("</xml>");
return sb.toString();
}
public static String setXML(String return_code, String return_msg) {
return "<xml><return_code><![CDATA[" + return_code
+ "]]></return_code><return_msg><![CDATA[" + return_msg
+ "]]></return_msg></xml>";
}
-
通用工具类
public class CommonUtil { private static Logger log = LoggerFactory.getLogger(CommonUtil.class); /** * 发送https请求 * @param requestUrl 请求地址 * @param requestMethod 请求方式(GET、POST) * @param outputStr 提交的数据 * @return 返回微信服务器响应的信息 */ public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) { try { // 创建SSLContext对象,并使用我们指定的信任管理器初始化 TrustManager[] tm = { new MyX509TrustManager() }; SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); sslContext.init(null, tm, new java.security.SecureRandom()); // 从上述SSLContext对象中得到SSLSocketFactory对象 SSLSocketFactory ssf = sslContext.getSocketFactory(); URL url = new URL(requestUrl); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.setSSLSocketFactory(ssf); 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) { log.error("连接超时:{}", ce); } catch (Exception e) { log.error("https请求异常:{}", e); } return null; } /** * 获取接口访问凭证 * * @param appid 凭证 * @param appsecret 密钥 * @return */ public static Token getToken(String appid, String appsecret) { Token token = null; String requestUrl = ConfigUtil.TOKEN_URL.replace("APPID", appid).replace("APPSECRET", appsecret); // 发起GET请求获取凭证 JSONObject jsonObject = JSONObject.fromObject(httpsRequest(requestUrl, "GET", null)); if (null != jsonObject) { try { token = new Token(); token.setAccessToken(jsonObject.getString("access_token")); token.setExpiresIn(jsonObject.getInt("expires_in")); } catch (JSONException e) { token = null; // 获取token失败 log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg")); } } return token; } public static String urlEncodeUTF8(String source){ String result = source; try { result = java.net.URLEncoder.encode(source,"utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return result; }
回顾一下支付流程: 前端发起支付调用 > 后端调用微信官方统一下单 > 根据返回参数返回前端 > 微信浏览器根据参数执行JS调起支付 > 支付成功 。 建议前端JS判断支付成功后在后续业务操作中增支付订单查询接口确认支付成功。
提示:文章中很多参数详细引用了官方文档,增加了些许流程性代码,如需发现缺少相关工具类及代码错误私信我即可