Detailed tutorial on implementing WeChat payment in Java

Abstract : A recent project involved payment business, which used WeChat payment and Alipay payment, and encountered some problems in the process of doing it, so now I will summarize and sort it out, share it with those in need, and review it for myself in the future Leave an idea.

1. Preparations for WeChat payment access:

First of all, WeChat Pay only supports corporate users, and individual users cannot access WeChat Pay. Therefore, if you want to access WeChat Pay, you first need to have a WeChat Official Account, and only companies with this status can apply. With a WeChat official account, you can apply for WeChat payment-related content, so you need to apply for the following parameters before you start writing code: public account ID, WeChat payment merchant number, API key, and AppSecret is the corresponding APPID Interface password, callback address (the callback must ensure that the external network can access this address), and the IP of the computer that initiated the request

2. WeChat payment process description:

With the parameters mentioned above, we can access WeChat payment. Let me take a look at the official documentation of WeChat payment (https://pay.weixin.qq.com/wiki/doc/api/index. html), visit this address, you can see that there are many payment methods to choose from, here we choose to scan the code to pay (https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter= 6_1)

Here we choose mode 2, let's look at the timing diagram of mode 2, as shown below:

Compared with mode 1, mode 2 has a simpler process and does not depend on the set callback payment URL. The merchant background system first calls the unified order interface of WeChat payment, and the WeChat background system returns the link parameter code_url, and the merchant background system generates a QR code image from the code_url value, and the user scans the code with the WeChat client to initiate payment. Note: code_url is valid for 2 hours, after the expiration date, you cannot initiate payment by scanning the code.

insert image description here

3. Maven dependency required for WeChat payment

    <!--微信支付SDK-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.3.0</version>
        </dependency>

        <!-- json处理器:引入gson依赖 -->
        <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>

4. Add parameters required by WeChat payment to the configuration file

# 微信支付相关参数
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

5. Implementation of WeChat payment order code

1. Controller layer

 /**
     * 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);
    }

Note: packageId is the package Id, which can be modified according to the situation

2. Service layer

   BaseRes<String> nativePay(Integer packageId);
   
   BaseRes<String> jsapiPay(Integer packageId, String openId);

3. Implementation layer


    /**
     * Mavicat下单
     * @return
     * @throws Exception
     */
    @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();
        }
    }

  /**
     * JSAPI下单
     * @return
     */

    @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);
    }

6. WeChat payment callback interface

1. Controller layer

   /**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     */
    @ApiOperation(value = "支付通知", notes = "支付通知")
    @PostMapping("/pay/notify")
    @ClientAuthControl
    public WxRes nativeNotify() {
    
    
        return wxPayService.nativeNotify();
    }
    

2. Service layer

    WxRes nativeNotify();

3. Implementation layer


	@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","成功");
        }
    }

   /**
     * 处理订单
     *
     * @param bodyMap
     */
    @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();
            }
        }
    }

	/**
     * 对称解密
     *
     * @param bodyMap
     * @return
     */

    @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;
    }

7. Tools and related configuration classes

1. WxPayUtil tool class

@Slf4j
public 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. WeChat payment configuration class

@Configuration
@ConfigurationProperties(prefix = "wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public 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;

    /**
     * 获取商户的私钥文件
     *
     * @param filename
     * @return
     */
    private PrivateKey getPrivateKey(String filename) {
    
    

        try {
    
    
            return PemUtil.loadPrivateKey(new FileInputStream(filename));
        } catch (FileNotFoundException e) {
    
    
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

    /**
     * 获取签名验证器
     *
     * @return
     */
    @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;
    }


    /**
     * 获取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);

        //用于构造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. WeChat payment enumeration class

@AllArgsConstructor
@Getter
public 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. Signature verification class

@Slf4j
public 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, live together~

Guess you like

Origin blog.csdn.net/weixin_45444807/article/details/131673713