利用微信支付须要先开通做事号,然后还要开通微信支付,末了还要配置一些开拓参数,过程比较多。
申请做事号(企业)开通微信支付开拓配置 详细准备事情请参考Spring Boot入门教程(三十九):微信支付集成-申请做事号和微信支付二:开拓文档扫码支付有两种模式,分为模式一、模式二,模式二与模式一比较,流程更为大略,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数codeurl,商户后台系统将codeurl值天生二维码图片,用户利用微信客户端扫码后发起支付。把稳:code_url有效期为2小时,过期后扫码不能再发起支付。
业务流程解释:
(1)商户后台系统根据用户选购的商品天生订单。
(2)用户确认支付后调用微信支付【统一下单API】天生预支付交易;
(3)微信支付系统收到要求后天生预支付交易单,并返复活意营业会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url天生二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端要求,验证链接有效性后发起用户支付,哀求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返复活意营业结果,并将交易结果通过短信、微信提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步关照商户后台系统支付结果。商户后台系统需回答吸收情形,关照微信后台系统不再发送该单的支付关照。
(11)未收到支付关照的情形,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
扫码支付文档
三:集成步骤1. 引入依赖<dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.2.1</version></dependency><dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>0.0.3</version></dependency>
2. application.yml
pay: wxpay: appID: xxx mchID: xxx key: xxx sandboxKey: xxx certPath: /var/local/cert/apiclient_cert.p12 useSandbox: false
3. MyWXPayConfig
/ 微信支付的参数配置 @author mengday zhang /@Data@Slf4j@ConfigurationProperties(prefix = 34;pay.wxpay")public class MyWXPayConfig implements WXPayConfig{ / "大众年夜众账号ID / private String appID; / 商户号 / private String mchID; / API 密钥 / private String key; / API 沙箱环境密钥 / private String sandboxKey; / API证书绝对路径 / private String certPath; / 退款异步关照地址 / private String notifyUrl; private Boolean useSandbox; / HTTP(S) 连接超时时间,单位毫秒 / private int httpConnectTimeoutMs = 8000; / HTTP(S) 读数据超时时间,单位毫秒 / private int httpReadTimeoutMs = 10000; / 获取商户证书内容 @return 商户证书内容 / @Override public InputStream getCertStream() { File certFile = new File(certPath); InputStream inputStream = null; try { inputStream = new FileInputStream(certFile); } catch (FileNotFoundException e) { log.error("cert file not found, path={}, exception is:{}", certPath, e); } return inputStream; } @Override public String getKey(){ if (useSandbox) { return sandboxKey; } return key; }}
4. WXPayClient
/ WXPayClient <p> 对WXPay的大略封装,处理支付密切干系的逻辑. @author Mengday Zhang @version 1.0 @since 2018/6/16 /@Slf4jpublic class WXPayClient extends WXPay { / 密钥算法 / private static final String ALGORITHM = "AES"; / 加解密算法/事情模式/添补办法 / private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding"; / 用户支付中,须要输入密码 / private static final String ERR_CODE_USERPAYING = "USERPAYING"; private static final String ERR_CODE_AUTHCODEEXPIRE = "AUTHCODEEXPIRE"; / 交易状态: 未支付 / private static final String TRADE_STATE_NOTPAY = "NOTPAY"; / 用户输入密码,考试测验30秒内去查询支付结果 / private static Integer remainingTimeMs = 10000; private WXPayConfig config; public WXPayClient(WXPayConfig config, WXPayConstants.SignType signType, boolean useSandbox) { super(config, signType, useSandbox); this.config = config; } / 刷卡支付 对WXPay#microPay(Map)增加了当支付结果为USERPAYING时去轮询查询支付结果的逻辑处理 把稳:该方法没有处理return_code=FAIL的情形,暂时不考虑网络问题,这种情形直接返回缺点 @param reqData @return @throws Exception / public Map<String, String> microPayWithPOS(Map<String, String> reqData) throws Exception { // 开始韶光(毫秒) long startTimestampMs = System.currentTimeMillis(); Map<String, String> responseMapForPay = super.microPay(reqData); log.info(responseMapForPay.toString()); // // 先判断 协议字段返回(return_code),再判断 业务返回,末了判断 交易状态(trade_state) // 通信标识,非交易标识 String returnCode = responseMapForPay.get("return_code"); if (WXPayConstants.SUCCESS.equals(returnCode)) { String errCode = responseMapForPay.get("err_code"); // 余额不敷,信用卡失落效 if (ERR_CODE_USERPAYING.equals(errCode) || "SYSTEMERROR".equals(errCode) || "BANKERROR".equals(errCode)) { Map<String, String> orderQueryMap = null; Map<String, String> requestData = new HashMap<>(); requestData.put("out_trade_no", reqData.get("out_trade_no")); // 用户支付中,须要输入密码或系统缺点则去重新查询订单API err_code, result_code, err_code_des // 每次循环时确当前系统韶光 - 开始时记录的韶光 > 设定的30秒韶光就退出 while (System.currentTimeMillis() - startTimestampMs < remainingTimeMs) { // 商户收银台得到USERPAYING状态后,经由商户后台系统调用【查询订单API】查询实际支付结果。 orderQueryMap = super.orderQuery(requestData); String returnCodeForQuery = orderQueryMap.get("return_code"); if (WXPayConstants.SUCCESS.equals(returnCodeForQuery)) { // 通讯成功 String tradeState = orderQueryMap.get("trade_state"); if (WXPayConstants.SUCCESS.equals(tradeState)) { // 如果成功了直接将查询结果返回 return orderQueryMap; } // 如果支付结果仍为USERPAYING,则每隔5秒循环调用【查询订单API】判断实际支付结果 Thread.sleep(1000); } } // 如果用户取消支付或累计30秒用户都未支付,商户收银台退出查询流程后连续调用【撤销订单API】撤销支付交易。 String tradeState = orderQueryMap.get("trade_state"); if (TRADE_STATE_NOTPAY.equals(tradeState) || ERR_CODE_USERPAYING.equals(tradeState) || ERR_CODE_AUTHCODEEXPIRE.equals(tradeState)) { Map<String, String> reverseMap = this.reverse(requestData); String returnCodeForReverse = reverseMap.get("return_code"); String resultCode = reverseMap.get("result_code"); if (WXPayConstants.SUCCESS.equals(returnCodeForReverse) && WXPayConstants.SUCCESS.equals(resultCode)) { // 如果撤销成功,须要见告客户端已经撤销订单了 responseMapForPay.put("err_code_des", "用户取消支付或尚未支付,后台已经撤销该订单,请重新支付!
5. WXPayConfiguration
"); } } } } return responseMapForPay; } / 从request的inputStream中获取参数 @param request @return @throws Exception / public Map<String, String> getNotifyParameter(HttpServletRequest request) throws Exception { InputStream inputStream = request.getInputStream(); ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length = 0; while ((length = inputStream.read(buffer)) != -1) { outSteam.write(buffer, 0, length); } outSteam.close(); inputStream.close(); // 获取微信调用我们notify_url的返复书息 String resultXml = new String(outSteam.toByteArray(), "utf-8"); Map<String, String> notifyMap = WXPayUtil.xmlToMap(resultXml); return notifyMap; } / 解密退款关照 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_16&index=11>退款结果关照文档</a> @return @throws Exception / public Map<String, String> decodeRefundNotify(HttpServletRequest request) throws Exception { // 从request的流中获取参数 Map<String, String> notifyMap = this.getNotifyParameter(request); log.info(notifyMap.toString()); String reqInfo = notifyMap.get("req_info"); //(1)对加密串A做base64解码,得到加密串B byte[] bytes = new BASE64Decoder().decodeBuffer(reqInfo); //(2)对商户key做md5,得到32位小写key ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 ) Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); SecretKeySpec key = new SecretKeySpec(WXPayUtil.MD5(config.getKey()).toLowerCase().getBytes(), ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, key); //(3)用key对加密串B做AES-256-ECB解密(PKCS7Padding) // java.security.InvalidKeyException: Illegal key size or default parameters // https://www.cnblogs.com/yaks/p/5608358.html String responseXml = new String(cipher.doFinal(bytes),"UTF-8"); Map<String, String> responseMap = WXPayUtil.xmlToMap(responseXml); return responseMap; } / 获取沙箱环境验签秘钥API <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=23_1">获取验签秘钥API文档</a> @return @throws Exception / public Map<String, String> getSignKey() throws Exception { Map<String, String> reqData = new HashMap<>(); reqData.put("mch_id", config.getMchID()); reqData.put("nonce_str", WXPayUtil.generateNonceStr()); String sign = WXPayUtil.generateSignature(reqData, config.getKey(), WXPayConstants.SignType.MD5); reqData.put("sign", sign); String responseXml = this.requestWithoutCert("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", reqData, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs()); Map<String, String> responseMap = WXPayUtil.xmlToMap(responseXml); return responseMap; }}
@Configuration@EnableConfigurationProperties(MyWXPayConfig.class)public class WXPayConfiguration { @Autowired private MyWXPayConfig wxPayConfig; / useSandbox 沙盒环境 @return / @Bean public WXPay wxPay() { return new WXPay(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox() ); } @Bean public WXPayClient wxPayClient() { return new WXPayClient(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox()); }}
6. PayUtil
public class PayUtil { / 根据url天生二位图片工具 @param codeUrl @return @throws WriterException / public static BufferedImage getQRCodeImge(String codeUrl) throws WriterException { Map<EncodeHintType, Object> hints = new Hashtable(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.CHARACTER_SET, "UTF8"); int width = 256; BitMatrix bitMatrix = (new MultiFormatWriter()).encode(codeUrl, BarcodeFormat.QR_CODE, width, width, hints); BufferedImage image = new BufferedImage(width, width, 1); for(int x = 0; x < width; ++x) { for(int y = 0; y < width; ++y) { image.setRGB(x, y, bitMatrix.get(x, y) ? -16777216 : -1); } } return image; }}
7. WXPayPrecreateController
/ 微信支付-扫码支付. <p> detailed description @author Mengday Zhang @version 1.0 @since 2018/6/18 /@Slf4j@RestController@RequestMapping("/wxpay/precreate")public class WXPayPrecreateController { @Autowired private WXPay wxPay; @Autowired private WXPayClient wxPayClient; / 扫码支付 - 统一下单 相称于支付不的电脑网站支付 <a href="https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1">扫码支付API</a> / @PostMapping("") public void precreate(HttpServletRequest request, HttpServletResponse response) throws Exception { Map<String, String> reqData = new HashMap<>(); reqData.put("out_trade_no", String.valueOf(System.nanoTime())); reqData.put("trade_type", "NATIVE"); reqData.put("product_id", "1"); reqData.put("body", "商户下单"); // 订单总金额,单位为分 reqData.put("total_fee", "2"); // APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 reqData.put("spbill_create_ip", "14.23.150.211"); // 异步吸收微信支付结果关照的回调地址,关照url必须为外网可访问的url,不能携带参数。 reqData.put("notify_url", "http://3sbqi7.natappfree.cc/wxpay/precreate/notify"); // 自定义参数, 可以为终端设备号(门店号或收银设备ID),PC网页或公众年夜众号内支付可以传"WEB" reqData.put("device_info", ""); // 附加数据,在查询API和支付关照中原样返回,可作为自定义参数利用。 reqData.put("attach", ""); / { code_url=weixin://wxpay/bizpayurl?pr=vvz4xwC, trade_type=NATIVE, return_msg=OK, result_code=SUCCESS, return_code=SUCCESS, prepay_id=wx18111952823301d9fa53ab8e1414642725 } / Map<String, String> responseMap = wxPay.unifiedOrder(reqData); log.info(responseMap.toString()); String returnCode = responseMap.get("return_code"); String resultCode = responseMap.get("result_code"); if (WXPayConstants.SUCCESS.equals(returnCode) && WXPayConstants.SUCCESS.equals(resultCode)) { String prepayId = responseMap.get("prepay_id"); String codeUrl = responseMap.get("code_url"); BufferedImage image = PayUtil.getQRCodeImge(codeUrl); response.setContentType("image/jpeg"); response.setHeader("Pragma","no-cache"); response.setHeader("Cache-Control","no-cache"); response.setIntHeader("Expires",-1); ImageIO.write(image, "JPEG", response.getOutputStream()); } } / @param request @return @throws Exception / @RequestMapping("/notify") public void precreateNotify(HttpServletRequest request, HttpServletResponse response) throws Exception { Map<String, String> reqData = wxPayClient.getNotifyParameter(request); / { transaction_id=4200000138201806180751222945, nonce_str=aaaf3fe4d3aa44d8b245bc6c97bda7a8, bank_type=CFT, openid=xxx, sign=821A5F42F5E180ED9EF3743499FBCF13, fee_type=CNY, mch_id=xxx, cash_fee=1, out_trade_no=186873223426017, appid=xxx, total_fee=1, trade_type=NATIVE, result_code=SUCCESS, time_end=20180618131247, is_subscribe=N, return_code=SUCCESS } / log.info(reqData.toString()); // 特殊提醒:商户系统对付支付结果关照的内容一定要做署名验证,并校验返回的订单金额是否与商户侧的订单金额同等,防止数据泄露导致涌现“假关照”,造成资金丢失。 boolean signatureValid = wxPay.isPayResultNotifySignatureValid(reqData); if (signatureValid) { / 把稳:同样的关照可能会多次发送给商户系统。商户系统必须能够精确处理重复的关照。 推举的做法是,当收到关照进行处理时,首先检讨对应业务数据的状态, 判断该关照是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。 在对业务数据进行状态检讨和处理之前,要采取数据锁进行并发掌握,以避免函数重入造成的数据混乱。 / Map<String, String> responseMap = new HashMap<>(2); responseMap.put("return_code", "SUCCESS"); responseMap.put("return_msg", "OK"); String responseXml = WXPayUtil.mapToXml(responseMap); response.setContentType("text/xml"); response.getWriter().write(responseXml); response.flushBuffer(); } }}
8. WXPayController
/ 微信支付 - 通用API. <p> 类似支付宝中的条码支付. @author Mengday Zhang @version 1.0 @since 2018/6/15 /@Slf4j@RestController@RequestMapping("/wxpay")public class WXPayController { @Autowired private WXPay wxPay; @Autowired private WXPayClient wxPayClient; @Autowired private MyWXPayConfig wxPayConfig; / 订单查询 @param orderNo @return @throws Exception / @GetMapping("/orderQuery") public Object orderQuery(String orderNo) throws Exception { Map<String, String> data = new HashMap<>(); data.put("out_trade_no", orderNo); Map<String, String> result = wxPay.orderQuery(data); return result; } / 退款 把稳:调用申请退款、撤销订单接口须要商户证书 把稳:沙箱环境相应结果可能会是"沙箱支付金额禁绝确,请确认验收case",但是正式环境不会报这个缺点 微信支付的最小金额是0.1元,以是在测试支付时金额必须大于0.1元,否则会提示微信支付配置缺点,可以将microPay的total_fee大于1再退款 / @PostMapping("/refund") public Object refund(String orderNo) throws Exception { Map<String, String> reqData = new HashMap<>(); // 商户订单号 reqData.put("out_trade_no", orderNo); // 授权码 reqData.put("out_refund_no", orderNo); // 订单总金额,单位为分,只能为整数 reqData.put("total_fee", "2"); //退款金额 reqData.put("refund_fee", "2"); // 退款异步关照地址 reqData.put("notify_url", wxPayConfig.getNotifyUrl()); reqData.put("refund_fee_type", "CNY"); reqData.put("op_user_id", wxPayConfig.getMchID()); Map<String, String> resultMap = wxPay.refund(reqData); log.info(resultMap.toString()); return resultMap; } / 退款结果关照 特殊解释:退款结果对主要的数据进行了加密,商户须要用商户秘钥进行解密后才能得到结果关照的内容 @param request @throws Exception / @RequestMapping("/refund/notify") public String refundNotify(HttpServletRequest request) throws Exception {// Map<String, String> notifyMap = new HashMap<>();// notifyMap.put("nonce_str", "9b4e428ae262d5dca96178027e849fa9");// notifyMap.put("req_info", "VKGj8c81RwQ45LcyWEVBE9/HsNfADGbgmbIAQZ2ydcbIFhIIcJFKFQwGfcSGgFGtQlWvg6KDNsRjmCjN+PvipJ3roynJ7cME0LOFG50VGtk4EYHqdjFzUVANI7GpT+i6Ok+ZWivH0MwoGK2fsz3WG+bYs2XJBwav/K89tKjFhZGitCKKBeGqcP99fa/gAL0swNXXNQHmL806Zi+QcACzL3E89BtP9FlXM2Gi+wPQafvPr+/iE+LrPdMlNUa5LiZnenZXUF24kMdhaTafczcKL4sZjRXQHEfEyc/pIZPbIjcNIETvHsskyzKuHVr/SAFkxaM6RR1Kl9pyWZGUjkH5SOeqsT8uL7YQmTlDXrnXmno3AvZdnepTGL5w69yIOmQNPeMqsd01ES9WX36GZYOigfi2+BJ9RRXjIffmpB/MFF+zryyvLTaJE2obCwFSHrmOD8YbaJqrZXOUvWZQrn7wIQgaCypo8V57cD3w5d2RSgIHNrdnEDYlbRcLNYgKuL+T9+1HPhU/frowZgwPN9IB53OahZV3p1Yvos23kvhqPCLn3BYgUihRbey6QhEtL2QyifiQ9e8WVLzWpRZ+DOa8VrhYvSuTfjRdjoNanqHFvXGP6uEsEa+DETqnexpB7xOS9m/CdmlNCwbdUplPEVzNQQdzYT4kybi00Y8A+EdairxfVyK9A7MAYAMtAO9yxV2ht0bn3SofFyZe/YSzdJgxdtcxBf1CVYN6x+yHcSueCSgq4cM/2VCwh4J1+pUVmNpEm0OVcdKbV5USkaxJR0h7Yd+n5FTz5Q2S/qvyDo202cUzLFPI5UqQm5X+FOrWDAkmmr5yVcDQIm3dAdb31jkz0X2TPYt5g7ciQ1h9heyVxJ67FexKvEM4pKCCubtWx6nyxcOUghHMrh8DSoBtewtNjbnwGVIbLsSb6X9MIYAkWIDbqNVP1f63GiZU+cJlhBmvcb8aeQUdTTj7EX5pOTIVSVv5D6SkKmpGU4FGvV+WjufuGX4ZRZo+01p6xl0sfZVmucG1UtxhX6bMCJb06yDwxpv7tGwkwS4TCK4SQp40Xe0=");// notifyMap.put("appid", "xxx");// notifyMap.put("mch_id", "xxx");// notifyMap.put("return_code", "SUCCESS"); // 把稳:同样的关照可能会多次发送给商户系统。商户系统必须能够精确处理重复的关照。 // 推举的做法是,当收到关照进行处理时,首先检讨对应业务数据的状态,判断该关照是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。 // 在对业务数据进行状态检讨和处理之前,要采取数据锁进行并发掌握,以避免函数重入造成的数据混乱。 // TODO 处理业务 Map<String, String> requstInfoMap = wxPayClient.decodeRefundNotify(request); // 商户处理退款关照参数后同步返回给微信参数 Map<String, String> responseMap = new HashMap<>(); responseMap.put("return_code", "SUCCESS"); responseMap.put("return_msg", "OK"); String responseXml = WXPayUtil.mapToXml(responseMap); return responseXml; } / 退款查询 @param orderNo @return @throws Exception / @GetMapping("/refundQuery") public Object refundQuery(String orderNo) throws Exception { Map<String, String> reqData = new HashMap<>(); reqData.put("out_trade_no", orderNo); Map<String, String> result = wxPay.refundQuery(reqData); return result; } / 下载对账单 把稳: 微信在越日9点启动天生前一天的对账单,建议商户10点后再获取; 对账单接口只能下载三个月以内的账单。 @throws Exception / @PostMapping("/downloadBill") public Object downloadBill(String billDate) throws Exception { Map<String, String> reqData = new HashMap<>(); reqData.put("bill_date", billDate); reqData.put("bill_type", "ALL"); Map<String, String> resultMap = wxPay.downloadBill(reqData); return resultMap; } / 获取沙箱环境API秘钥, 这里只是为了获取,可以放在main方法下运行,这里作为api来运行的,实际情形下不应该暴露出来 @return @throws Exception / @GetMapping("/sandbox/getSignKey") public Object getSignKey() throws Exception { Map<String, String> signKey = wxPayClient.getSignKey(); log.info(signKey.toString()); return signKey; }}
关注并私信“微信扫码支付”获取源码。