背景:
由于公司业务需要想把支付宝和微信整合在一起,做一个统一的支付模块。其中经过了各种坑,因为之前也没搞过这个。所以这里记录下,同时,也让后面做相同业务的小伙伴们少走一些弯路。
支付宝
接口:alipay.trade.precreate(统一收单线下交易预创建
接口描述: 收银员通过收银台或商户后台调用支付宝接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。 这里我要讲下,我们使用这个功能的前提是你的商户账号已经开通“当面付”功能;
这里我们不在贴出参数和响应参数的信息,这些信息在 alipay.trade.precreate(统一收单线下交易预创建 中都有很详细的介绍。我传递相应的参数就可得到一个二维码链接,前端或者后端都可以根据这个链接进行生成二维码;
这里有几个小细节需要注意:
-
total_amount (订单总金额)为0,是不会返回二维码链接的;其实文档上也有写到,订单金额,单位为元,精确到小数点后两位,取值范围[0.01,100000000] ;
-
notify_url(回调地址),这里的回调地址必须是外网可以直接访问的,本地想测试的话也可以内外网穿透下;这里推荐一个好用的工具utools,下载一个内外网穿透工具就行了;
-
timeout_express 和 qr_code_timeout_express的区别:官网的详细介绍, 点击查看,这里要说下沙箱和真实商户账号的区别,沙箱中我们扫过码会在支付宝app上有一笔未支付订单信息,过期后还是可以继续支付,但是真实的商户账号,扫码后也会在app有一笔订单,但是在支付时间过期后,就不能再继续支付了;
-
沙箱环境回调通知和真实商户账号的回调通知的区别:
- 回调通知的次数不同: 沙箱只回调一次(偶尔还不回调,这里也是一个坑);真实的是会多次尝试的,所以可以很放心的使用;
- 回调通知的触发条件不同: 沙箱在用户扫码和支付完成都会触发回调通知,通知的状态码分别为:WAIT_BUYER_PAY(交易创建)、TRADE_SUCCESS(交易成功)
-
沙箱环境返回的qr_code和真实商户返回的qr_code的区别: 沙箱返回的qr_code是不带反斜杠的’\‘,但是真实的商户账号环境返回的qr_code是带反斜杠的,所以如果有小伙伴是在沙箱写完后再对接真实商户账号的话,就非常容易忽略这一点,如果没有处理掉这个反斜杠,生成的二维,用支付宝app扫码就是会是一片空白;这个时候就是一脸懵逼;
微信支付:
背景:
这里要说下,微信支付时分为v3和v2两个版本的,这里选用的v3版本,但是网上关于v3版本的demo很少,v3和v2版本的差别很大,所以路途很艰难;
接口: 普通支付(直连模式)/ Native下单API)
接口描述:除付款码支付场景以外,商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
在 普通支付(直连模式)/ Native下单API)也有很详细的字段介绍,下面我要将的就是几个注意的点:
-
微信支付设置GET/POST 请求头和请求体要设置相应的格式:
/** * 请求支付端(GET) * * @param url 请求地址 * @return 请求结果 */ public static String sendGet(String url) { HttpGet request = new HttpGet(url); // 这个header是必须设置 request.addHeader("Accept", "application/json"); request.addHeader("Content-Type", "application/json"); return builder(request); } /** * 请求支付端(POST) * * @param url 请求地址 * @param json 参数 * @return 请求结果 */ public static String sendPost(String url, JSONObject json) { String res = null; try { HttpPost request = new HttpPost(url); // 这个header和entity是必须设置 request.addHeader("Accept", "application/json"); request.addHeader("Content-Type", "application/json"); request.setEntity(new StringEntity(json.toString(), ContentType.create("application/json", "utf-8"))); res = builder(request); } catch (UnsupportedCharsetException e) { LOG.error("调用WXPay接口失败:", e); } return res; } /** * 构建微信支付请求 * * @param request HttpURL * @return HttpResponse */ private static String builder(HttpUriRequest request) { CloseableHttpClient client = null; String res = null; try { // 获取支付客户端 client = WechatPayHttpClientBuilder.create() .withMerchant(MERCHANT_ID, serialNo, privatekey) .withValidator(response -> true) .build(); // 处理请求结果 HttpResponse response = client.execute(request); if (response.getEntity() != null) { res = EntityUtils.toString(response.getEntity()); EntityUtils.consume(response.getEntity()); } } catch (Exception e) { LOG.error("调用WXPay接口失败:", e); } finally { if (client != null) { try { client.close(); } catch (IOException e) { LOG.error("调用WXPay接口失败:", e); } } } return res; }
这里必须要在request中设置相应的header和entity请求格式;如果没有设置,请求过去微信也会返回给你一个二维码链接,但是这个链接生成二维码后,使用微信app扫码就会出现不停的跳转;这也是很坑的地方;
- 注意传给微信的对象的类型;例如:总金额(total)是int类型,本人就是传了一个BigDecimal也是可以拿到二维链接,但是同样出现了使用微信app扫码不停的跳转,最终弹出系统繁忙的信息;
-
notify_url参数设置,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例: “https://pay.weixin.qq.com/wxpay/pay.action”
这个notify_url 一个商户账号只能设置一个回调地址;
-
设置完回调地址就可以正常接收到微信的回调,我们可以在request中读取到响应的信息;
private String getWxParams(HttpServletRequest request) { InputStream inStream = null; ByteArrayOutputStream outStream = null; String result = null; try { inStream = request.getInputStream(); outStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { outStream.write(buffer, 0, len); } result = new String(outStream.toByteArray(), "utf-8"); } catch (IOException e) { e.printStackTrace(); } finally { try { if (null != inStream) { inStream.close(); } if (null != outStream) { outStream.close(); } } catch (IOException e) { e.printStackTrace(); } } return result; }
这里拿到是加密的json对象,具体的信息在resource中;例如:
{ "id":"EV-2018022511223320873", "create_time":"2015-05-20T13:29:35+08:00", "resource_type":"encrypt-resource", "event_type":"TRANSACTION.SUCCESS", "resource" : { "algorithm":"AEAD_AES_256_GCM", "ciphertext": "...", "nonce": "...", "associated_data": "" }, "summary":"支付成功" }
下面我们就要在商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,拿到一个key;然后通过微信提供的工具类(AesUtil)进行解密,大家可以具体看下,这里提供一个解密demo
// PayConstants.API_V3 就是上面我们设置APIv3拿到的key AesUtil aesUtil = new AesUtil(PayConstants.API_V3.getBytes(StandardCharsets.UTF_8)); // 解密回调信息 associatedData,nonce,ciphertext 分别是上面resource中的对应值 String decryptToString = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
通过上面的解密后就可以拿到真实的回调支付信息了,例如:
{ "transaction_id": "1217752501201407033233368018", "amount": { "payer_total": 100, "total": 100, "currency": "CNY", "payer_currency": "CNY" }, "mchid": "1230000109", "trade_state": "SUCCESS", "bank_type": "CMC", "promotion_detail": [{ "amount": 100, "wechatpay_contribute": 0, "coupon_id": "109519", "scope": "GLOBALSINGLE", "merchant_contribute": 0, "name": "单品惠-6", "other_contribute": 0, "currency": "CNY", "type": "CASHNOCASH", "stock_id": "931386", "goods_detail": [{ "goods_remark": "商品备注信息", "quantity": 1, "discount_amount": 1, "goods_id": "M1006", "unit_price": 100 }, { "goods_remark": "商品备注信息", "quantity": 1, "discount_amount": 1, "goods_id": "M1006", "unit_price": 100 }] }, { "amount": 100, "wechatpay_contribute": 0, "coupon_id": "109519", "scope": "GLOBALSINGLE", "merchant_contribute": 0, "name": "单品惠-6", "other_contribute": 0, "currency": "CNY", "type": "CASHNOCASH", "stock_id": "931386", "goods_detail": [{ "goods_remark": "商品备注信息", "quantity": 1, "discount_amount": 1, "goods_id": "M1006", "unit_price": 100 }, { "goods_remark": "商品备注信息", "quantity": 1, "discount_amount": 1, "goods_id": "M1006", "unit_price": 100 }] }], "success_time": "2018-06-08T10:34:56+08:00", "payer": { "openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o" }, "out_trade_no": "1217752501201407033233368018", "appid": "wxd678efh567hg6787", "trade_state_desc": "支付失败,请重新下单支付", "trade_type": "MICROPAY", "attach": "自定义数据", "scene_info": { "device_id": "013467007045764" } }
-
由于微信通知规则,即用户在接收并处理完回调信息后,要相应的返回一个应答信息;不然微信会认为商户接收通知失败,微信会在一定时间内策略定期重发通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m);通知应答就是返回一个json字符串,例如:
/** * 微信-回调请求-成功 */ public static final String RETURN_WECHAT = "{ \n" + " \"code\": \"SUCCESS\",\n" + " \"message\": \"交易成功\",\n" + "}"; /** * <p>响应微信回调通知</p > * * @param response */ public void responseNotification(HttpServletResponse response) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/xml; charset=utf-8"); PrintWriter out = null; try { out = response.getWriter(); out.print(PayConstants.RETURN_WECHAT); out.close(); logger.info("响应微信回调通知成功, 无需重复回调"); } catch (IOException e) { logger.info("响应微信回调通知报错"); e.printStackTrace(); } }