摘要:最近的一个项目中涉及到了支付业务,其中用到了微信支付和支付宝支付,在做的过程中也遇到些问题,所以现在总结梳理一下,分享给有需要的人,也为自己以后回顾留个思路。
一、微信支付接入准备工作:
首先,微信支付,只支持企业用户,个人用户是不能接入微信支付的,所以要想接入微信支付,首先需要有微信公众号,这个的企业才能申请。有了微信公众号,就能申请微信支付的相关内容,所以在准备开始写代码之前需要先把下面的这些参数申请好:公众账号ID、微信支付商户号、API密钥、AppSecret是APPID对应的接口密码、回调地址(回调必须保证外网能访问到此地址)、发起请求的电脑IP
有了上面提到的这些参数,那我们就可以接入微信支付了,下面我来看下微信支付的官方文档(https://pay.weixin.qq.com/wiki/doc/api/index.html)、访问该地址可以看到有多种支付方式可以选择,我们这里选择扫码支付的方式(https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1)
这里我们选择模式二,下面看下模式二的时序图,如下图:
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时,过期后扫码不能再发起支付。
<dependency> <groupId>com.github.wechatpay-apiv3groupId> <artifactId>wechatpay-apache-httpclientartifactId> <version>0.3.0version> dependency> <dependency> <groupId>com.google.code.gsongroupId> <artifactId>gsonartifactId> <version>2.8.6version> dependency> <dependency> <groupId>org.apache.httpcomponentsgroupId> <artifactId>httpclientartifactId> <version>4.5.12version> dependency> <dependency> <groupId>com.google.zxinggroupId> <artifactId>coreartifactId> <version>3.3.3version> dependency> <dependency> <groupId>com.google.zxinggroupId> <artifactId>javaseartifactId> <version>3.3.3version> dependency>
# 微信支付相关参数wxpay: # 商户号 mch-id: xxxxxxx # 商户API证书序列号 mch-serial-no: xxxxxxxxxx # 商户私钥文件 # 注意:该文件放在项目根目录下 private-key-path: ./apiclient_key.pem # APIv3密钥 api-v3-key: xxxxxxxx # APPID appid: xxxxxxc27e0e7cxxx # 微信服务器地址 domain: https://api.mch.weixin.qq.com # 接收结果通知地址 # 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置 notify-domain: https://c7c1-240e-3b5-3015-be0-1bc-9bed-fca4-d09b.ngrok.io
1.Controller层
@ApiOperation(value = "native 微信支付下单 返回Image") @GetMapping("/native") public BaseRes<String> nativePay(@RequestParam("packageId") Integer packageId) { return wxPayService.nativePay(packageId); } @ApiOperation(value = "JSAPI微信支付下单") @GetMapping("/jsapi") public BaseRes<String> jsapiPay(@RequestParam("packageId") Integer packageId,@RequestParam("openId") String openId) { return wxPayService.jsapiPay(packageId,openId); }
注意:packageId是套餐Id,可根据情况修改
2.Service层
BaseRes<String> nativePay(Integer packageId); BaseRes<String> jsapiPay(Integer packageId, String openId);
3.实现层
@Transactional(rollbackFor = Exception.class) @Override @SneakyThrows public BaseRes<String> nativePay(Integer packageId){ log.info("发起Navicat支付请求"); HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); CloseableHttpResponse response = wxPayExecute(packageId, null, httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功"); } else { log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString); throw new IOException("request failed"); } Gson gson = new Gson(); //响应结果 Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); //二维码 String codeUrl = resultMap.get("code_url"); return new BaseRes<>(codeUrl,ServiceCode.SUCCESS); //生成二维码// WxPayUtil.makeQRCode(codeUrl); } finally { response.close(); } } @Override @SneakyThrows public BaseRes<String> jsapiPay(Integer packageId, String openId) { log.info("发起Navicat支付请求"); HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType())); CloseableHttpResponse response = wxPayExecute(packageId, openId, httpPost); try { String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 int statusCode = response.getStatusLine().getStatusCode();//响应状态码 if (statusCode == 200) { //处理成功 log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204) { //处理成功,无返回Body log.info("成功"); } else { log.info("JSAPI下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString); throw new IOException("request failed"); } Gson gson = new Gson(); //响应结果 Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); String prepayId = resultMap.get("prepay_id"); return new BaseRes<>(prepayId,ServiceCode.SUCCESS); } finally { response.close(); } } // 封装统一下单方法 private CloseableHttpResponse wxPayExecute(Integer packageId,String openId,HttpPost httpPost) throws IOException { // 获取套餐金额 还有相关信息 ChatPackage chatPackage = chatPackageMapper.selectById(packageId); if (null == chatPackage) { throw new NingException(ServiceCode.FAILED); } BigDecimal amount = chatPackage.getAmount(); if (null == amount || amount.equals(BigDecimal.ZERO)) { throw new NingException(ServiceCode.SUCCESS); }// 从登录信息中获取用户信息 TokenUser loginUserInfo = CommUtils.getLoginUserInfo(); Integer userId = loginUserInfo.getUserId(); // 请求body参数 Gson gson = new Gson(); Map<String,Object> paramsMap = new HashMap<>(); paramsMap.put("appid", wxPayConfig.getAppid()); paramsMap.put("mchid", wxPayConfig.getMchId()); paramsMap.put("description", chatPackage.getName()); paramsMap.put("out_trade_no", WxPayUtil.generateOrderNumber(userId,packageId)); //订单号 paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxApiType.NATIVE_NOTIFY.getType())); Map<String,Object> amountMap = new HashMap<>(); //由单位:元 转换为单位:分,并由Bigdecimal转换为整型 BigDecimal total = amount.multiply(new BigDecimal(100)); amountMap.put("total", total.intValue()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap);// 判断是Navicat下单还是JSAPI下单 JSAPI需要传OPENID if (StringUtils.isNotBlank(openId)) { Map<String,Object> payerMap = new HashMap<>(); payerMap.put("openid",openId); paramsMap.put("payer",payerMap); } JSONObject attachJson = new JSONObject(); attachJson.put("packageId",packageId); attachJson.put("userId",userId); attachJson.put("total",total); paramsMap.put("attach",attachJson.toJSONString()); //将参数转换成json字符串 String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" , jsonParams); StringEntity entity = new StringEntity(jsonParams, "utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); httpPost.setHeader("Accept", "application/json"); //完成签名并执行请求 return wxPayClient.execute(httpPost); }
1.Controller层
@ApiOperation(value = "支付通知", notes = "支付通知") @PostMapping("/pay/notify") @ClientAuthControl public WxRes nativeNotify() { return wxPayService.nativeNotify(); }
2.Service层
WxRes nativeNotify();
3.实现层
@Resource private Verifier verifier; private final ReentrantLock lock = new ReentrantLock(); @Override @SneakyThrows @Transactional public WxRes nativeNotify() { HttpServletRequest request = CommUtils.getRequest(); HttpServletResponse response = CommUtils.getResponse(); Gson gson = new Gson(); try { //处理通知参数 String body = WxPayUtil.readData(request); Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id"); //签名的验证 WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body); if (wechatPay2ValidatorForRequest.validate(request)) { throw new RuntimeException(); } log.info("通知验签成功"); //处理订单 processOrder(bodyMap); return new WxRes("SUCCESS","成功"); } catch (Exception e) { e.printStackTrace(); response.setStatus(500); return new WxRes("FAIL","成功"); } } @Transactional @SneakyThrows public void processOrder(Map<String, Object> bodyMap){ log.info("处理订单"); //解密报文 String plainText = decryptFromResource(bodyMap); //将明文转换成map Gson gson = new Gson(); HashMap plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no"); String attach = (String) plainTextMap.get("attach"); JSONObject attachJson = JSONObject.parseObject(attach); Integer packageId = attachJson.getInteger("packageId"); Integer userId = attachJson.getInteger("userId"); Integer total = attachJson.getInteger("total"); //尝试获取锁: // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放 if (lock.tryLock()) { try { log.info("plainText={}",plainText); //处理重复的通知 //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。 String orderStatus = orderService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { return; }// TODO 修改订单状态、添加支付记录等 // 通知前端用户 已完成支付 messageSocketHandle.sendMessageByUserID(userId,new TextMessage("PaySuccess")); } finally { //要主动释放锁 lock.unlock(); } } } @SneakyThrows private String decryptFromResource(Map<String, Object> bodyMap) { log.info("密文解密"); //通知数据 Map<String, String> resourceMap = (Map) bodyMap.get("resource"); //数据密文 String ciphertext = resourceMap.get("ciphertext"); //随机串 String nonce = resourceMap.get("nonce"); //附加数据 String associatedData = resourceMap.get("associated_data"); AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); //数据明文 String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); return plainText; }
1.WxPayUtil工具类
@Slf4jpublic class WxPayUtil { private static final Random random = new Random();// 生成订单号 public static String generateOrderNumber(int userId, int packageId) { // 获取当前时间戳 long timestamp = System.currentTimeMillis(); // 生成6位随机数 int randomNum = random.nextInt(900000) + 100000; // 组装订单号 return String.format("%d%d%d%d", timestamp, randomNum, userId, packageId); } public static void makeQRCode(String url){ HttpServletResponse response = CommUtils.getResponse(); //通过支付链接生成二维码 HashMap<EncodeHintType, Object> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 2); try { BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, 200, 200, hints); MatrixToImageWriter.writeToStream(bitMatrix, "PNG", response.getOutputStream()); System.out.println("创建二维码完成"); } catch (Exception e) { e.printStackTrace(); } } public static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }}
2.微信支付配置类
@Configuration@ConfigurationProperties(prefix = "wxpay") //读取wxpay节点@Data //使用set方法将wxpay节点中的值填充到当前类的属性中@Slf4jpublic class WxPayConfig { // 商户号 private String mchId; // 商户API证书序列号 private String mchSerialNo; // 商户私钥文件 private String privateKeyPath; // APIv3密钥 private String apiV3Key; // APPID private String appid; // 微信服务器地址 private String domain; // 接收结果通知地址 private String notifyDomain; private PrivateKey getPrivateKey(String filename) { try { return PemUtil.loadPrivateKey(new FileInputStream(filename)); } catch (FileNotFoundException e) { throw new RuntimeException("私钥文件不存在", e); } } @Bean public ScheduledUpdateCertificatesVerifier getVerifier() { log.info("获取签名验证器"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //私钥签名对象 PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey); //身份认证对象 WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); // 使用定时更新的签名验证器,不需要传入证书 ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier( wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); return verifier; } @Bean(name = "wxPayClient") public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) { log.info("获取httpClient"); //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator(verifier)); // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 CloseableHttpClient httpClient = builder.build(); return httpClient; } @Bean(name = "wxPayNoSignClient") public CloseableHttpClient getWxPayNoSignClient() { //获取商户私钥 PrivateKey privateKey = getPrivateKey(privateKeyPath); //用于构造HttpClient WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() //设置商户信息 .withMerchant(mchId, mchSerialNo, privateKey) //无需进行签名验证、通过withValidator((response) -> true)实现 .withValidator((response) -> true); // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 CloseableHttpClient httpClient = builder.build(); log.info("== getWxPayNoSignClient END =="); return httpClient; }}
3.微信支付枚举类
@AllArgsConstructor@Getterpublic enum WxApiType { NATIVE_PAY("/v3/pay/transactions/native"), JSAPI_PAY("/v3/pay/transactions/jsapi"), ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"), CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"), NATIVE_NOTIFY("/client/order/pay/notify"); private final String type;}
4.签名验证类
@Slf4jpublic class WechatPay2ValidatorForRequest { protected static final long RESPONSE_EXPIRED_MINUTES = 5; protected final Verifier verifier; protected final String requestId; protected final String body; public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) { this.verifier = verifier; this.requestId = requestId; this.body = body; } protected static IllegalArgumentException parameterError(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("parameter error: " + message); } protected static IllegalArgumentException verifyFail(String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException("signature verify fail: " + message); } public final boolean validate(HttpServletRequest request) throws IOException { try { //处理请求参数 validateParameters(request); //构造验签名串 String message = buildMessage(request); String serial = request.getHeader(WECHAT_PAY_SERIAL); String signature = request.getHeader(WECHAT_PAY_SIGNATURE); //验签 if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", serial, message, signature, requestId); } } catch (IllegalArgumentException e) { log.error(e.getMessage()); return false; } return true; } protected final void validateParameters(HttpServletRequest request) { // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; String header = null; for (String headerName : headers) { header = request.getHeader(headerName); if (header == null) { throw parameterError("empty [%s], request-id=[%s]", headerName, requestId); } } //判断请求是否过期 String timestampStr = header; try { Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); // 拒绝过期请求 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId); } } catch (DateTimeException | NumberFormatException e) { throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId); } } protected final String buildMessage(HttpServletRequest request) throws IOException { String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); String nonce = request.getHeader(WECHAT_PAY_NONCE); return timestamp + "\n" + nonce + "\n" + body + "\n"; } protected final String getResponseBody(CloseableHttpResponse response) throws IOException { HttpEntity entity = response.getEntity(); return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : ""; }}
OK,齐活~
来源地址:https://blog.csdn.net/weixin_45444807/article/details/131673713