摘要:最近的一个项目中涉及到了支付业务,其中用到了微信支付和支付宝支付,在做的过程中也遇到些问题,所以现在总结梳理一下,分享给有需要的人,也为自己以后回顾留个思路。

一、微信支付接入准备工作:

首先,微信支付,只支持企业用户,个人用户是不能接入微信支付的,所以要想接入微信支付,首先需要有微信公众号,这个的企业才能申请。有了微信公众号,就能申请微信支付的相关内容,所以在准备开始写代码之前需要先把下面的这些参数申请好:公众账号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小时,过期后扫码不能再发起支付。

三、微信支付所需Maven依赖

<dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-apache-httpclient</artifactId><version>0.3.0</version></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.6</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.12</version></dependency><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.3</version></dependency><dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.3.3</version></dependency>

四、配置文件添加微信支付所需参数

# 微信支付相关参数wxpay:# 商户号mch-id: xxxxxxx# 商户API证书序列号mch-serial-no: xxxxxxxxxx# 商户私钥文件# 注意:该文件放在项目根目录下private-key-path: ./apiclient_key.pem# APIv3密钥api-v3-key: xxxxxxxx# APPIDappid: 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层

 /** * native下单 */@ApiOperation(value = "native 微信支付下单 返回Image")@GetMapping("/native")public BaseRes<String> nativePay(@RequestParam("packageId") Integer packageId) {return wxPayService.nativePay(packageId);}/** * JSAPI下单 */@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.实现层

/** * Mavicat下单 * @return * @throws Exception */@Transactional(rollbackFor = Exception.class)@Override@SneakyThrowspublic 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) { //处理成功,无返回Bodylog.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();}}/** * JSAPI下单 * @return */@Override@SneakyThrowspublic 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) { //处理成功,无返回Bodylog.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需要传OPENIDif (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")@ClientAuthControlpublic WxRes nativeNotify() {return wxPayService.nativeNotify();}

2.Service层

WxRes nativeNotify();

3.实现层

@Resourceprivate Verifier verifier;private final ReentrantLock lock = new ReentrantLock();@Override@SneakyThrows@Transactionalpublic 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","成功");}} /** * 处理订单 * * @param bodyMap */@Transactional@SneakyThrowspublic void processOrder(Map<String, Object> bodyMap){log.info("处理订单");//解密报文String plainText = decryptFromResource(bodyMap);//将明文转换成mapGson 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();}}}/** * 对称解密 * * @param bodyMap * @return */@SneakyThrowsprivate 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);}/** * 生成二维码 * @param url */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();}}/** * 将通知参数转化为字符串 * * @param request * @return */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;// APPIDprivate String appid;// 微信服务器地址private String domain;// 接收结果通知地址private String notifyDomain;/** * 获取商户的私钥文件 * * @param filename * @return */private PrivateKey getPrivateKey(String filename) {try {return PemUtil.loadPrivateKey(new FileInputStream(filename));} catch (FileNotFoundException e) {throw new RuntimeException("私钥文件不存在", e);}}/** * 获取签名验证器 * * @return */@Beanpublic 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;}/** * 获取http请求对象 * * @param verifier * @return */@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;}/** * 获取HttpClient,无需进行应答签名验证,跳过验签的流程 */@Bean(name = "wxPayNoSignClient")public CloseableHttpClient getWxPayNoSignClient() {//获取商户私钥PrivateKey privateKey = getPrivateKey(privateKeyPath);//用于构造HttpClientWechatPayHttpClientBuilder 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下单 */NATIVE_PAY("/v3/pay/transactions/native"),/** * JSAPI下单 */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 lastString[] 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,齐活~