最近的项目在对接微信支付,所以抽出一些时间,将方法总结一下:
欢迎加群交流:724225958
官方开发文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=1_1;
文档中分别对支付账户(参数)、接口规则、支付业务场景,流程、API做了详细介绍,并提供了SDK以及DEMO ,大部分有一定基础的开发或研发,均可参照文档及其demo,一步一步的梳理整合。只需注意最重要的2点即可:
① js调起微信支付时,需二次加签,统一下单返回的签名无法调起微信支付
② 二次加签的加签类型SignType是‘HMAC-SHA256’,此处不可为‘MD5’
微信支付业务场景最终都可以抽象为技术上的2个点:前端发起支付请求,后端响应请求。
首先,从前端入手,整合微信JS-SDK,
官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115。
按照文档指示的步骤开发即可,此处不做详细介绍。在支付调用页面注入jssdk即可:
/** * 配置jssdk */ function jssdk(url){ if(null == url || url == ''){ url = location.href.split('#')[0]; } $.ajax({ type:'post', url:buildUrl('villa/jssdk'), data:{ url:url }, cache:false, dataType:'json', success:function(data){ if (data.code == '200') { wx.config({ debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: data.obj.appId, // 必填,公众号的唯一标识 timestamp: data.obj.timestamp, // 必填,生成签名的时间戳 nonceStr: data.obj.nonceStr, // 必填,生成签名的随机串 signature: data.obj.signature,// 必填,签名,见附录1 jsApiList: ['checkJsApi', 'chooseWXPay' ] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 }); wx.error(function(res){ // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 }); } } }); } 此函数方法可以调整,将使用的接口作为参数,传入函数,灵活使用。 发起支付的函数为: function recharge(){ //业务代码省略 //统一下单 $.ajax({ type:'post', url:buildUrl('villa/a/wxpay/unifiedOrder'), data:{ openid:’’,//用户openid必需,或后台单独获取 totalFee:totalFee }, cache:false, dataType:'json', success:function(data){ if (data.code == '200') { var obj = data.obj; var prepay_id = 'prepay_id=' + obj.prepay_id; //再次生成支付签名 $.ajax({ type:'post', url:buildUrl('villa/a/wxpay/generatePaySign'), data:{ prepay_id:prepay_id //预支付订单id,二次加签必需 }, cache:false, dataType:'json', success:function(data){ //业务代码 if (data.code == '200') { var pay_obj = data.obj; //调起微信支付 chooseWXPay(pay_obj.timeStamp, pay_obj.nonceStr, prepay_id, pay_obj.signType, pay_obj.paySign); } else { $("#errorMessage").html(data.msg); loadDialog('dialog'); } } }); } else { $("#errorMessage").html(data.msg); loadDialog('dialog'); } } }); }
如果签名正确,基本就可以看到微信支付调用效果。
下面为后端整合代码:
微信支付DEMO中给出了一个统一下单的调用示例: public class WXPayExample { public static void main(String[] args) throws Exception { MyWXPayConfig config = new MyWXPayConfig(); WXPay wxpay = new WXPay(config); Map<String, String> data = new HashMap<String, String>(); data.put("body", "腾讯充值中心-QQ会员充值"); data.put("out_trade_no", "2016090910595900000012"); data.put("device_info", ""); data.put("fee_type", "CNY"); data.put("total_fee", "1"); data.put("spbill_create_ip", "123.12.12.123"); data.put("notify_url", "http://www.masaike.com/wxpay/notify"); // data.put("trade_type", "NATIVE"); // 此处指定为扫码支付 data.put("trade_type", "JSAPI"); data.put("openid", "masaike"); data.put("product_id", "12"); try { Map<String, String> resp = wxpay.unifiedOrder(data); System.out.println(resp); } catch (Exception e) { e.printStackTrace(); } //成功返还结果 // {result_code=SUCCESS, sign=A7D34A90163C12BAFB887FC752B9EF22D8C75C05BE581917E0E2806C2E73F2F1, mch_id=1496788202, prepay_id=wx201802142113131695db46a40709589533, return_msg=OK, appid=’masaike’, nonce_str=ULtDr28SorCa5haa, return_code=SUCCESS, trade_type=JSAPI} } } 调用失败的原因可能只有一个,那就是没有找到你的支付安全证书,证书的获取方法在上一篇博文《微信服务号之公众号支付配置》:http://blog.csdn.net/soongp/article/details/79405161查看。
WxPayInfo类为自己封装的一个微信参数类,用于传参: public class WXPayInfo { /**用户唯一标识*/ private String openid; /**充值金额*/ private String totalFee; /**商品描述*/ private String body; public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getTotalFee() { return totalFee; } public void setTotalFee(String totalFee) { this.totalFee = totalFee; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } }
WxPayConfig为微信支付配置抽象类,WxPayConfig为其实现,说白了就是一些必要参数的获取,例如:appid(服务号账号id),appsecret(服务号秘钥),mchid(支付账户账号),key(支付秘钥,在微信商户平台配置),算了,还是把代码粘出来吧: public abstract class WXPayConfig { /** * 获取appid * <p>Discription:[获取微信公众号唯一标识:appid]</p> * Created on 2018年1月24日 * @return * @author:[soong] */ public abstract String getAppID(); /** * 获取 Mch ID * <p>Discription:[获取微信支付商户号:商户申请微信支付后,由微信支付分配的商户收款账号]</p> * Created on 2018年1月24日 * @return * @author:[soong] */ public abstract String getMchID(); /** * 获取 API 密钥 * <p>Discription:[交易过程生成签名的密钥,仅保留在商户系统和微信支付后台,不会在网络中传播; * 商户可根据邮件提示登录微信商户平台进行设置; * 也可按一下路径设置:微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->密钥设置]</p> * Created on 2018年1月24日 * @return * @author:[soong] */ public abstract String getKey(); /** * 获取开发者密码 * <p>Discription:[AppSecret是APPID对应的接口密码,用于获取接口调用凭证access_token时使用 * 在微信支付中,先通过OAuth2.0接口获取用户openid,此openid用于微信内网页支付模式下单接口使用]</p> * Created on 2018年1月24日 * @return * @author:[soong] */ public abstract String getAppsecret(); /** * 获取商户证书内容 * * @return 商户证书内容 */ public abstract InputStream getCertStream(); /** * HTTP(S) 连接超时时间,单位毫秒 * * @return */ public int getHttpConnectTimeoutMs() { return 6*1000; } /** * HTTP(S) 读数据超时时间,单位毫秒 * * @return */ public int getHttpReadTimeoutMs() { return 8*1000; } /** * 获取WXPayDomain, 用于多域名容灾自动切换 * @return */ public abstract IWXPayDomain getWXPayDomain(); /** * 是否自动上报。 * 若要关闭自动上报,子类中实现该函数返回 false 即可。 * * @return */ public boolean shouldAutoReport() { return true; } /** * 进行健康上报的线程的数量 * * @return */ public int getReportWorkerNum() { return 6; } /** * 健康上报缓存消息的最大数量。会有线程去独立上报 * 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受 * * @return */ public int getReportQueueMaxSize() { return 10000; } /** * 批量上报,一次最多上报多个数据 * * @return */ public int getReportBatchSize() { return 10; } } public class MyWXPayConfig extends WXPayConfig{ /**证书二级制字节流*/ private byte[] certData; private static MyWXPayConfig INSTANCE; public MyWXPayConfig() throws Exception{ String certPath = GlobalConstants.getCertPath(); File file = new File(certPath); InputStream certStream = new FileInputStream(file); this.certData = new byte[(int) file.length()]; certStream.read(this.certData); certStream.close(); } public static MyWXPayConfig getInstance() throws Exception{ if (INSTANCE == null) { synchronized (MyWXPayConfig.class) { if (INSTANCE == null) { INSTANCE = new MyWXPayConfig(); } } } return INSTANCE; } @Override public String getAppID() { return GlobalConstants.getAppid(); } @Override public String getMchID() { return GlobalConstants.getMchID(); } @Override public String getKey() { return GlobalConstants.getApiKey(); } @Override public InputStream getCertStream() { ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData); return certBis; } @Override public IWXPayDomain getWXPayDomain() { return WXPayDomainSimpleImpl.instance(); } @Override public String getAppsecret() { return GlobalConstants.getAppSecret (); } } GlobalConstants.getAppid();等方法的内部实现,其实就是获取了配置文件中的参数,只不过多封装了一层做了垂直解耦。
控制器WxPayController: @Controller @RequestMapping(value = "${adminPath}/wxpay") public class WXPayController extends BaseController{ @Autowired private WxPayService wxPayService; /** * 除被扫支付场景以外,商户系统先调用该接口在微信支付服务后台生成预支付交易单, * 返回正确的预支付交易回话标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付 * <p>Discription:[商户server调用统一下单接口请求订单,生成预支付交易单]</p> * Created on 2018年2月15日 * @param wxPayInfo * @param req * @param resp * @return * @author:[soong] */ @RequestMapping(value = "unifiedOrder", method = RequestMethod.POST) @ResponseBody public Json<Map<String, String>> unifiedOrder(WXPayInfo wxPayInfo, HttpServletRequest req, HttpServletResponse resp) { Json<Map<String, String>> json = new Json<Map<String, String>>(); if (StringUtils.isBlank(wxPayInfo.getOpenid())) { json.setFalid(Json.PARAM_EXCEPTION, "用户标识openid不能为空"); logger.info("用户标识openid不能为空"); return json; } if (StringUtils.isBlank(wxPayInfo.getTotalFee())) { json.setFalid(Json.PARAM_EXCEPTION, "充值金额totalFee不能为空"); logger.info("充值金额totalFee不能为空"); return json; } try { json = wxPayService.unifiedOrder(wxPayInfo, json); } catch (BusinessException e) { } // {result_code=SUCCESS, sign=B7C41F5D85E0F4579BC839FE9C864518B1C5F4DEF900F57AA5E4B1367159074F, mch_id=1496788202, prepay_id=wx20180214211654305951b3800266781582, return_msg=OK, appid=wxf79c25d666f76ee0, nonce_str=BDhKVLBBzukDxPAl, return_code=SUCCESS, trade_type=JSAPI} return json; } /** * * <p>Discription:[支付结果通知]</p> * Created on 2018年2月16日 * @param openid * @param request * @param response * @return * @author:[soong] */ @RequestMapping(value = "notify", method = RequestMethod.POST) @ResponseBody public Json<Map<String, String>> notify(String openid, HttpServletRequest request, HttpServletResponse response) { Json<Map<String, String>> json = new Json<Map<String, String>>(); // if (StringUtils.isBlank(openid)) {// // json.setFalid(Json.PARAM_EXCEPTION, "openid不能为空"); // return json; // } logger.info("支付结果通知"); // try { // json = wxPayService.unifiedOrder(json); // } catch (BusinessException e) { // // } // {result_code=SUCCESS, sign=B7C41F5D85E0F4579BC839FE9C864518B1C5F4DEF900F57AA5E4B1367159074F, mch_id=1496788202, prepay_id=wx20180214211654305951b3800266781582, return_msg=OK, appid=wxf79c25d666f76ee0, nonce_str=BDhKVLBBzukDxPAl, return_code=SUCCESS, trade_type=JSAPI} return json; } /** * * <p>Discription:[再次生成签名,js发起微信支付请求]</p> * Created on 2018年2月16日 * @param openid * @param request * @param response * @return * @author:[soong] */ @RequestMapping(value = "generatePaySign", method = RequestMethod.POST) @ResponseBody public Json<Map<String, String>> generatePaySign(HttpServletRequest request, HttpServletResponse response) { Json<Map<String, String>> json = new Json<Map<String, String>>(); String prepay_id = request.getParameter("prepay_id");//预支付订单id if (StringUtils.isBlank(prepay_id)) { json.setFalid(Json.PARAM_EXCEPTION, "预支付订单ID(prepay_id)不能为空"); } String appId = GlobalConstants.getAppid();//公众号id String timeStamp = GlobalConstants.getTimestamp();//时间戳 String nonceStr = GlobalConstants.getUUID();//随机字符串 String signType = SignType.HMACSHA256 + "";//加签类型 Map<String, String> map = new HashMap<String, String>(); map.put("appId", appId); map.put("timeStamp", timeStamp); map.put("nonceStr", nonceStr); map.put("package", prepay_id); map.put("signType", signType); try { String paySign = WXPayUtil.generateSignature(map, GlobalConstants.getApiKey(), SignType.HMACSHA256); map.put("paySign", paySign); json.setSuccess(Json.SUCCESS, "再次生成支付签名成功", map); } catch (Exception e) { e.printStackTrace(); } return json; } /** * * <p>Discription:[查询订单]</p> * Created on 2018年2月17日 * @param request * @param response * @return * @author:[soong] */ @RequestMapping(value = "orderQuery", method = RequestMethod.POST) @ResponseBody public Json<Map<String, String>> orderQuery(HttpServletRequest request, HttpServletResponse response) { Json<Map<String, String>> json = new Json<Map<String, String>>(); try { } catch (Exception e) { e.printStackTrace(); } return json; } }
业务实现类WxPayServcieImpl: @Service("wxPayService") public class WxPayServiceImpl implements WxPayService{ @Override public Json<Map<String, String>> unifiedOrder(WXPayInfo wxPayInfo, Json<Map<String, String>> json) { User u = UserUtils.getUser(); Map<String, String> data = new HashMap<String, String>(); String body = wxPayInfo.getBody(); if (StringUtils.isBlank(body)) { body = GlobalConstants.getWxpayBody(); } data.put("body", body);//商品描述 String out_trade_no = String.valueOf(WXPayUtil.getCurrentTimestamp()); data.put("out_trade_no", out_trade_no);//商户订单号 data.put("fee_type", WXPayConstants.FEE_TYPE);//标价币种 data.put("total_fee", "1"); // String totalFee = wxPayInfo.getTotalFee(); // totalFee = String.valueOf(Integer.valueOf(totalFee) * 100); // data.put("total_fee", totalFee);//标价金额,单价为分(乘以100转化为元) data.put("spbill_create_ip", u.getLoginIp());//终端IP data.put("notify_url", GlobalConstants.getNotifyUrl());//通知地址 data.put("trade_type", WXPayConstants.TRADE_TYPE_JSAPI);//支付类型 data.put("openid", wxPayInfo.getOpenid());//公众号支付必需:用户标识 try { MyWXPayConfig config = new MyWXPayConfig(); WXPay wxpay = new WXPay(config); Map<String, String> resp = wxpay.unifiedOrder(data); //通信成功判断 if (null != resp.get("return_code") && resp.get("return_code").toString().equals("SUCCESS")) { //交易成功判断 if (null != resp.get("result_code") && resp.get("result_code").toString().equals("SUCCESS")) { json.setSuccess(Json.SUCCESS, "统一下单成功", resp); } else { String err_code_des = "系统异常,请用相同参数重新调用"; if (null != resp.get("err_code_des")) { err_code_des = resp.get("err_code_des").toString(); } json.setFalid(Json.BUSINESS_EXCEPTION, "统一下单失败-交易失败:" + err_code_des, resp); } } else { String return_msg = "签名失败"; if (null != resp.get("return_msg")) { return_msg = resp.get("return_msg").toString(); } json.setFalid(Json.BUSINESS_EXCEPTION, "统一下单失败-通信失败:" + return_msg, resp); } } catch (Exception e) { e.printStackTrace(); json.setFalid(Json.BUSINESS_EXCEPTION, "统一下单失败:" + e.getMessage()); throw new BusinessException(Json.BUSINESS_EXCEPTION, "统一下单失败:" + e.getMessage()); } return json; } }
其实,只要肯花时间,仔细认真的阅读文档即可,整合jssdk,能看懂支付DEMO就ok,剩下的就是一些细节调试,以及在调试过程中遇到问题,并解决的一个过程;
交流QQ群:724225958