微信小程序+apiv3+java后台对接支付(亲测有效)

前言

很久没有对接小程序的支付了,看了下文档,多了一个apiv3支付,去看了其他博客的内容,总感觉少了点什么,幸好多方借鉴调试成功了,在此写下具体步骤,帮助各位顺利调通,减少不必要的麻烦

小程序发起请求

   pay(e: { currentTarget: { dataset: { pay: any } } }) {
        const pay = e.currentTarget.dataset.pay
        console.log("pay>>>", pay)
        const userInfo = app.globalData.userInfo
        const data = {
            openid: userInfo.openid,
            personId: userInfo.personId,
            integralId: pay.integralId,
            total: pay.spend * 100
        }
        https.postReq("createOrder", JSON.stringify(data), (res) => {
            console.log("请求回调", res)
            const data = {
                appId: res.appId,
                timeStamp: res.timeStamp,
                nonceStr: res.nonceStr,
                package: res.package,
                signType: res.signType,
                paySign: res.paySign,
            }
            console.log("查看发送的data>>>", data)
            wx.requestPayment({
                appId: res.appId,
                timeStamp: res.timeStamp,
                nonceStr: res.nonceStr,
                package: res.package,
                signType: res.signType,
                paySign: res.paySign,
                success: (res) => {
                    console.log("res>>>", res)
                },
                fail: (res) => {
                    console.log("error", res)
                }
            })
        })
    },

新建一张订单表,记录订单信息

DROP TABLE IF EXISTS `pay_record`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `pay_record` (
  `person_id` bigint(20) DEFAULT NULL,
  `create_time` timestamp NULL DEFAULT NULL,
  `order_id` varchar(255) DEFAULT NULL COMMENT '支付单号',
  `payer_total` int(11) DEFAULT NULL COMMENT '交易金额',
  `trade_state` varchar(128) DEFAULT NULL COMMENT '交易状态',
  `pay_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `remark` varchar(500) DEFAULT NULL,
  `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信后台生成的订单号',
  `trade_type` varchar(16) DEFAULT NULL,
  `total` int(11) DEFAULT NULL,
  `success_time` timestamp NULL DEFAULT NULL,
  `integral_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`pay_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

maven引入包

    <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.0.M2</version>
        </dependency>
        
        <!-- 导入微信支付v3工具包 -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version>
        </dependency>

所需要的各个参数

public class WXPayConstants {

    /*appid*/
    public static final String APP_ID = "";
    /*商户号*/
    public static final String MCH_ID = "";
    /* 商户证书序列号 */
    public static final String MCH_SERIAL_NO = "";
    /*APIv3密钥*/
    public static final String API_V3KEY = "";
    /*支付成功回调地址*/
    public static final String NOTIFY_URL = "https://xxx.xxxx.xx/wxPayNotify";
    /*序列号地址*/
    public static final String PRIVATE_KEY_PATH = "/XXXX/apiclient_key.pem";
}

定义微信结果数据接收对象

@Data
public class WeChatPayResultIn {

    private String id;
    private String create_time;
    private String resource_type;
    private String event_type;
    private String summary;
    private WechatPayResourceIn resource;
}
@Data
public class WechatPayResourceIn {
    private String original_type;
    private String algorithm;
    private String ciphertext;
    private String associated_data;
    private String nonce;
}

创建订单号请求对象(openid和total是必填,后面两个参数是自己逻辑所需,可按自己业务调整)

@Data
public class PayBody {
    private String openid;
    private Integer total;
    private Long personId;
    /**
     * 所选积分规则id
     */
    private Long integralId;
}

主要controller

@RestController
@Slf4j
public class WxPayController {

    @Resource
    private WxPayService wxPayService;

    @PostMapping(value = "/wx/createOrder")
    public Object create(@RequestBody PayBody data) {
        try {
            // Integer total = data.getTotal();
            Integer total = 1;
            Map<String, Object> order = createOrder(data.getOpenid(), "支付", total);
            // 新建预支付记录,这里可以新建一张记录支付信息的表。
            return order;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    @PostMapping(value = "/wxPayNotify")
    public Map wechatPayCallBack(HttpServletRequest request,
                                 @RequestBody WeChatPayResultIn weChatPayResultIn) {
        Map result = new HashMap();
        result.put("code", wxPayService.wechatPayCallBack(weChatPayResultIn,request));
        return result;
    }
  }

工具类

import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.RandomUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.util.ResourceUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class V3WXPayUtil {


    public static Map<String, Object> createOrder(String openId, String description, Integer total) throws Exception {
        try {
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(ResourceUtils.getFile(WXPayConstants.PRIVATE_KEY_PATH)));
            log.warn("获取私钥:=========>" + merchantPrivateKey.toString());
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            log.warn("获取证书管理器实例:=========>" + certificatesManager.toString());
            // 向证书管理器增加需要自动更新平台证书的商户信息
            WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(WXPayConstants.MCH_ID,
                    new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey));
            log.warn("获取管理器增加需要自动更新平台证书的商户信息前:=========>" + wechatPay2Credentials.toString() + "\n");
            byte[] api_v3KEYBytes = WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8);
            log.warn("获取api_v3KEYBytes:=========>" + api_v3KEYBytes);
            certificatesManager.putMerchant(WXPayConstants.MCH_ID, wechatPay2Credentials, api_v3KEYBytes);
            log.warn("证书管理器实例:=========>" + certificatesManager.toString());
            // 从证书管理器中获取verifier
            Verifier verifier = certificatesManager.getVerifier(WXPayConstants.MCH_ID);
            log.warn("从证书管理器中获取verifier:=========>" + verifier);
            CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
                    .withMerchant(WXPayConstants.MCH_ID, WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)
                    .withValidator(new WechatPay2Validator(verifier))
                    .build();
            log.warn("从证书管理器中获取verifier后:=========>");
            HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
            httpPost.addHeader("Accept", "application/json");
            httpPost.addHeader("Content-type", "application/json; charset=utf-8");

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectMapper objectMapper = new ObjectMapper();

            ObjectNode rootNode = objectMapper.createObjectNode();
            String str_no = new DateTime().toString("yyyyMMddHHmmssSSS");
            rootNode.put("mchid", WXPayConstants.MCH_ID)
                    .put("appid", WXPayConstants.APP_ID)
                    .put("notify_url", WXPayConstants.NOTIFY_URL)
                    .put("description", description)
                    .put("out_trade_no", str_no);
            rootNode.putObject("amount")
                    .put("total", total) // 支付总金额,单位为分, 需前端传入
                    .put("currency", "CNY");
            rootNode.putObject("payer")
                    .put("openid", openId);

            objectMapper.writeValue(bos, rootNode);

            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            CloseableHttpResponse response = httpClient.execute(httpPost);

            String bodyAsString = EntityUtils.toString(response.getEntity());
            log.warn("成功获取微信预支付订单号{}", bodyAsString);
            System.out.println("成功获取微信预支付订单号==========>" + bodyAsString);
            // 时间戳
            String currentTimeMillis = System.currentTimeMillis() / 1000 + "";
            // 随机字符串 hutool工具类
            String randomString = RandomUtil.randomString(32);

            StringBuilder builder = new StringBuilder();
            // 应用ID
            builder.append(WXPayConstants.APP_ID).append("\n");
            // 时间戳
            builder.append(currentTimeMillis).append("\n");
            // 随机字符串
            builder.append(randomString).append("\n");

            JsonNode node = objectMapper.readTree(bodyAsString);
            log.warn("从获取node后:=========>" + node);
            // 订单详情扩展字符串
            builder.append("prepay_id=").append(node.get("prepay_id").toString().replace("\"", "")).append("\n");

            String paySign = sign(builder.toString().getBytes(), merchantPrivateKey);

            Map<String, Object> map = new HashMap();
            map.put("package", "prepay_id=" + node.get("prepay_id").toString().replace("\"", ""));
            map.put("prepayId", node.get("prepay_id").toString().replace("\"", ""));
            map.put("nonceStr", randomString);
            map.put("signType", "RSA");
            map.put("paySign", paySign);
            map.put("timeStamp", currentTimeMillis);
            map.put("appId", WXPayConstants.APP_ID);
            map.put("orderId", str_no);
            return map;
        } catch (Exception e) {
            log.warn("异常:" + getStackTraceMessage(e));
            throw e;
        }
    }

    public static String getStackTraceMessage(Throwable e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw, true);
        e.printStackTrace(pw);
        pw.flush();
        sw.flush();
        return sw.toString();
    }

    public static String sign(byte[] message, PrivateKey privateKey) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(privateKey);
        sign.update(message);

        return Base64.getEncoder().encodeToString(sign.sign());
    }
    public static boolean signVerify(String serial, String message, String signature) {
        // 从证书管理器中获取verifier
        Verifier verifier = null;
        try {
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(ResourceUtils.getFile(WXPayConstants.PRIVATE_KEY_PATH)));
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant(WXPayConstants.MCH_ID, new WechatPay2Credentials(WXPayConstants.MCH_ID,
                    new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)), WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            verifier = certificatesManager.getVerifier(WXPayConstants.MCH_ID);
            return verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        } catch (HttpCodeException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return false;
    }
}

支付回调的参数处理及解密

package cn.lasons.eps.wxpay;

import cn.lasons.eps.mapper.PayRecordMapper;
import cn.lasons.eps.model.Integral;
import cn.lasons.eps.model.IntegralAdmin;
import cn.lasons.eps.model.PayRecord;
import cn.lasons.eps.model.auto.PersonInformation;
import cn.lasons.eps.service.IIntegralAdminService;
import cn.lasons.eps.service.IIntegralService;
import cn.lasons.eps.service.IPersonInformationService;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WxPayService {

    @Resource
    private PayRecordMapper payRecordMapper;

    @Resource
    private IIntegralAdminService iIntegralAdminService;

    @Resource
    private IIntegralService iIntegralService;

    @Resource
    private IPersonInformationService iPersonInformationService;

    @Transactional
    public Map wechatPayCallBack(WeChatPayResultIn weChatPayResultIn, HttpServletRequest request) {
        log.error("微信结果通知字符串:{}" + JSON.toJSONString(weChatPayResultIn));
        String nonce = weChatPayResultIn.getResource().getNonce();
        String ciphertext = weChatPayResultIn.getResource().getCiphertext();
        String associated_data = weChatPayResultIn.getResource().getAssociated_data();
        AesUtil aesUtil = new AesUtil(WXPayConstants.API_V3KEY.getBytes());
        Map result = new HashMap();
        result.put("code", "FAIL");
        BufferedReader reader = null;
        try {
            //验签
            StringBuilder signStr = new StringBuilder();
            // 请求时间戳\n
            signStr.append(request.getHeader("Wechatpay-Timestamp")).append("\n");
            // 请求随机串\n
            signStr.append(request.getHeader("Wechatpay-Nonce")).append("\n");
            reader = request.getReader();
            log.error("reader:=========>" + reader.toString());
            String str = null;
            StringBuilder builder = new StringBuilder();
            while ((str = reader.readLine()) != null) {
                builder.append(str);
            }
            log.error("请求报文主体: {}", builder);
            signStr.append(builder.toString()).append("\n");
            // 1.验签
            if (!V3WXPayUtil.signVerify(request.getHeader("Wechatpay-Serial"), signStr.toString(), request.getHeader("Wechatpay-Signature"))) {
                result.put("message", "sign error");
                log.error("验签失败-sign error");
                return result;
            }
            // 解密请求体
            String s = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
            log.error("解密结果");
            log.error(s);
            JSONObject jsonObject = JSONObject.parseObject(s);
            log.error("json格式化的结果");
            log.error(jsonObject.toJSONString());
            //订单号
            String orderId = jsonObject.getString("out_trade_no");
            //微信支付订单号
            String transactionId = jsonObject.getString("transaction_id");

            String tradeState = jsonObject.getString("trade_state");
            //幂等性
            if (StringUtils.equals(tradeState, "USERPAYING")) {
                //还在支付中,不做处理
                return null;
            }
            if (StringUtils.equals(tradeState, "SUCCESS")) {
                QueryWrapper<PayRecord> payRecordQueryWrapper = new QueryWrapper<>();
                payRecordQueryWrapper.eq("order_id", orderId);
                PayRecord yhjOrderInfo = payRecordMapper.selectOne(payRecordQueryWrapper);
                // 支付成功,将订单状态改为1
                if (yhjOrderInfo != null && yhjOrderInfo.getPayId() > 0) {
                    JSONObject amount = jsonObject.getJSONObject("amount");
                    //总金额
                    Integer total = amount.getInteger("total");
                    //用户实际支付金额
                    Integer payerTotal = amount.getInteger("payer_total");
                    yhjOrderInfo.setTotal(total);
                    yhjOrderInfo.setPayerTotal(payerTotal);
                    Timestamp success_time = jsonObject.getTimestamp("success_time");
                    yhjOrderInfo.setSuccessTime(success_time == null ? new Timestamp(System.currentTimeMillis()) : success_time);
                    yhjOrderInfo.setTransactionId(transactionId);
                    yhjOrderInfo.setTradeState(tradeState);
                    payRecordMapper.updateById(yhjOrderInfo);
                    PersonInformation byId = iPersonInformationService.getById(yhjOrderInfo.getPersonId());
                    IntegralAdmin integralByTotal = iIntegralAdminService.getById(yhjOrderInfo.getIntegralId());
                    Integral integral = new Integral();
                    integral.setPersonId(yhjOrderInfo.getPersonId());
                    integral.setWay("用户充值" + integralByTotal.getSpend() + ";获得积分+" + integralByTotal.getIntegral());
                    byId.setIntegral(byId.getIntegral() == null ? integralByTotal.getIntegral() : byId.getIntegral() + integralByTotal.getIntegral());
                    iPersonInformationService.updateById(byId);
                    iIntegralService.save(integral);
                }
                result.put("code", "OK");
                return result;
            }
        } catch (GeneralSecurityException e) {
            log.error("微信支付回调数据处理及自己项目逻辑异常", e);
            result.put("message", e.getMessage());
        } catch (IOException e) {
            log.error("微信支付回调获取body流异常", e);
            result.put("message", e.getMessage());
        }
        return result;
    }
}

注意

在使用reader = request.getReader();的时候会报错getInputStream() has already been called for this request,报这个错,说明在进入Controller这个过程前数据已经被读取了一次。为了解决这个问题,需要在拦截器中进行处理。

解决办法

由于采用采用application/json传输参数时,HttpServletRequest只能读取一次 body 中的内容。因为是读的字节流,读完就没了,因此需要需要做特殊处理。为实现述多次读取 Request 中的 Body 内容,需继承 HttpServletRequestWrapper 类,读取 Body 的内容,然后缓存到 byte[] 中;这样就可以实现多次读取 Body 的内容了。

具体如下

package cn.lasons.eps.config;


import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class RequestBodyReaderFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        try {
            ServletRequest requestWrapper = null;
            if (request instanceof HttpServletRequest) {
                requestWrapper = new RequestWrapper((HttpServletRequest) request);
            }
            if (requestWrapper == null) {
                chain.doFilter(request, response);
            } else {
                chain.doFilter(requestWrapper, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ServletException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void destroy() {

    }
}

package cn.lasons.eps.config;

import org.apache.commons.io.IOUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

public class RequestWrapper extends HttpServletRequestWrapper {

    //参数字节数组
    private byte[] requestBody;

    //Http请求对象
    private HttpServletRequest request;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.request = request;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        /**
         * 每次调用此方法时将数据流中的数据读取出来,然后再回填到InputStream之中
         * 解决通过@RequestBody和@RequestParam(POST方式)读取一次后控制器拿不到参数问题
         */
        if (null == this.requestBody) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            IOUtils.copy(request.getInputStream(), baos);
            this.requestBody = baos.toByteArray();
        }

        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
        return new ServletInputStream() {

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() {
                return bais.read();
            }
        };
    }

    public byte[] getRequestBody() {
        return requestBody;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

package cn.lasons.eps.config;


import java.util.ArrayList;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean<RequestBodyReaderFilter> fequestBodyReaderFilter() {
        FilterRegistrationBean<RequestBodyReaderFilter> registrationBean = new FilterRegistrationBean<>();
        RequestBodyReaderFilter filter = new RequestBodyReaderFilter();
        registrationBean.setFilter(filter);
        ArrayList<String> urls = new ArrayList<>();
        urls.add("/*");//配置过滤规则
        registrationBean.setUrlPatterns(urls);
        registrationBean.setOrder(3);
        return registrationBean;
    }

}

至此,一次成功的小程序apiv3支付对接完成,已经把主要的支付逻辑写清楚。其他的操作就按自己的项目来调整。有问题可私信讨论。

借鉴

主要借鉴
读取支付成功回调
解决多次读取body

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值