【微信小程序】Java实现微信支付(小程序支付JSAPI-V3)java-sdk工具包(包含支付出现的多次回调的问题解析,接口幂等性)

      对于一个没有写过支付的小白,打开微信支付官方文档时彻底懵逼,因为微信支付文档太过详细,导致我无从下手,所以写此文章,帮助第一次写支付的小伙伴梳理一下。

一、流程分为三个接口:(这是前言,先看一遍,保持印象,方便理解代码)

1、第一个接口:微信预支付 小程序调用后端预支付接口  =>预支付接口调用成功返回给小程序支付凭证id
(如下返回示例示例,能返回这些代表后端工作完成了一半了,在第一个接口中,我加入了保存订单信息业务,将订单状态保存为待支付状态)

{
    "timeStamp": "1414561699",
    "nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
    "package": "prepay_id=wx201410272009395522657a690389285100",
    "paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddqNjKNqZLhLw4jq\/xDg=="
  }

        这些就是第一个接口需要返回的数据,也是微信支付官方文档要求返回的数据,有了这些支付凭证,前端就可以拿去直接调用微信后台支付接口。

2、第二个接口:支付回调 小程序支付成功后,微信后台执行支付回调将支付订单信息返回。(在此接口中,我将上一个接口的待支付状态更新为订单已支付状态,并且保存充值流水日志,更新充值人的余额操作。) 

此接口可能出现的问题:①.在正常流程下,已经完成了支付,但是如果某些原因导致支付回调失败,比如由于微信的原因导致多次调用了此接口,针对此接口幂等性,我们可以在做业务更新流程之前先进行订单的状态判断,让已经回调一次的订单不再进行保存流水以及余额的更新。

②.由于服务器宕机或者延迟等问题原因回调接口失败时,这时用户有可能已经付钱,但是没有进行业务更新流程,导致充值没有余额增加,充值流水缺失等问题,就要需要第三个接口解决。

3.第三个接口:支付订单查询 此订单查询顾名思义就是查询订单,先去查询订单状态,如果为已支付说明回调没问题,如果为待支付,就去微信提供的接口查询是否支付,支付成功就重复第二步中更新订单状态保存订单流水更新用户余额操作,因为第二步回调失败所以需要再次此操作。

后续更新加锁操作,进行并发控制。。。。。。

二、开始撸代码

1、maven
<dependency>
  <groupId>com.github.wechatpay-apiv3</groupId>
  <artifactId>wechatpay-java</artifactId>
  <version>0.2.11</version>
</dependency>
2、资源文件配置:配置微信支付前必要的密钥和商户信息

# 微信小程序支付配置信息
wx:
  # 微信小程序appid
  app-id: *********
  # 商户号
  mch-id: **********
  # 证书序列号
  mch-serial-no: **********
  # 小程序密钥
  app-secret: **********
  # api密钥
  api-key: ***********
  # 回调接口地址
  notify-url: https://*********/payNotify
  # 证书地址
  key-path: /data/iot/cert/apiclient_key.pem(这是我服务器中apiclient_key.pem文件存放地址,测试改成本地的)
3、获取配置信息
@Component
@ConfigurationProperties(prefix = "wx")
@Data
@ToString
public class WxPayV3Bean {
    //小程序appid
    private String appId;
    //商户号
    private String mchId;
    //证书序列号
    private String mchSerialNo;
    //小程序秘钥
    private String appSecret;
    //api秘钥
    private String apiKey;
    //回调接口地址
    private String notifyUrl;
    //证书地址
    private String keyPath;
}
4、预支付请求类(由前端传值 根据自己业务请求添加)
@Data
@Accessors(chain = true)
public class WxPayOrderReqVO {

    @ApiModelProperty(value = "订单支付类型(商品订单;预约订单)",required = true)
    @NotBlank(message = "订单支付类型不能为空!")
    private String orderType;//附加数据,回调时可根据这个数据辨别订单类型或其他
 
    @ApiModelProperty(value = "总金额(单位:分)",required = true)
    @NotNull(message = "总金额不能为空!")
    private BigDecimal amount;

    @ApiModelProperty(value = "商品描述",required = true)
    @NotBlank(message = "商品描述不能为空!")
    private String description;
5、工具类
import com.wechat.pay.java.core.util.PemUtil;
import org.springframework.util.Base64Utils;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Random;
 
/**
 * @project
 * @Classname WXPayUtil
 * @Description TODO
 * @Author: lsh
 * @CreateTime: 2023-10-24  16:00
 */
public class WXPayUtil {
 
    public static String getSign(String signatureStr,String privateKey) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException, URISyntaxException {
        //replace 根据实际情况,不一定都需要
        String replace = privateKey.replace("\\n", "\n");
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKeyFromPath(replace);
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(merchantPrivateKey);
        sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        return Base64Utils.encodeToString(sign.sign());
    }
 
}
6、开始写支付接口

conteoller层 (包含预支付接口 支付回调接口,像支付保存订单业务可以写在与预付接口里

ApiResponseBody 为我封装的统一返回接口,改成自己的!!!

/**
 * @author lsh
 * @ClassName WxPayController
 * @description: TODO
 * @date 2023年10月24日
 */
@Slf4j
@Api(value = "微信-支付", tags = {"微信-支付"})
@RestController
@RequestMapping("wechat/pay")
public class WxPayController{
 
    @Resource
    private WxPayService wxPayService;

    /**
     * 微信预支付
     * @param req
     * @param request
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "微信预支付", notes = "微信预支付")
    @PostMapping("/createOrder")
    public ApiResponseBody createOrder(@RequestBody @Validated WxPayOrderReqVO req, HttpServletRequest request) throws Exception {
        return wxPayService.createOrder(req, request);
    }

    /**
     * 微信支付回调
     * @param request
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "微信支付回调", notes = "微信支付回调")
    @PostMapping("/payNotify")
    public ApiResponseBody payNotify(HttpServletRequest request) throws Exception {
        log.info("-----------------------微信支付回调通知-----------------------");
       //注意:回调接口需要暴露到公网上,且要放开token验证
        return  wxPayService.payNotify(request);
    }

/**
     * 查询支付
     * @param
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "查询支付", notes = "查询支付")
    @GetMapping("/queryPayOrder")
    public ApiResponseBody queryPayOrder(@RequestParam("tradeNo") String tradeNo) {
        log.info("-----------------------订单号:" + tradeNo);
        if (tradeNo == null || "".equals(tradeNo)) {
            return ApiResponseBody.error(BizCodeMsgEnum.PARAM_ERROR);
        }
        return  wxPayService.queryPayOrder(tradeNo);
    }
}

Service、ServiceImpl层:

    ApiResponseBody createOrder(WxPayOrderReqVO req, HttpServletRequest request) throws Exception;

    ApiResponseBody payNotify(HttpServletRequest request) throws Exception;

    ApiResponseBody queryPayOrder(String tradeNo);

/**
 * @author lsh
 * @ClassName WxPayServiceImpl
 * @description: TODO
 * @date 2023年10月24日
 */
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private WxPayV3Bean wxPayV3Bean;
    @Resource
    private WechatUserMapper wechatUserMapper;
    @Resource
    private AccountUserMapper accountUserMapper;


    @Override
    @Transactional(rollbackFor = Exception.class)
    public ApiResponseBody createOrder(WxPayOrderReqVO req, HttpServletRequest request) throws Exception {
        //生成商户订单号
        String tradeNo = getTradeNo();
        // 使用自动更新平台证书的RSA配置,配置微信支付的自动证书管理功能
        Config config =
                new RSAAutoCertificateConfig.Builder()
                        .merchantId(wxPayV3Bean.getMchId())
                        .privateKeyFromPath(wxPayV3Bean.getKeyPath())
                        .merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
                        .apiV3Key(wxPayV3Bean.getApiKey())
                        .build();
        // 构建service,用于处理JSAPI支付相关的操作
        JsapiService service = new JsapiService.Builder().config(config).build();
        // 创建预支付订单的请求对象
        PrepayRequest prepayRequest = new PrepayRequest();
        Amount amount = new Amount();
        amount.setTotal(Integer.valueOf(req.getAmount().toString()));
        prepayRequest.setAmount(amount);
        prepayRequest.setAppid(wxPayV3Bean.getAppId());
        prepayRequest.setMchid(wxPayV3Bean.getMchId());
        prepayRequest.setNotifyUrl(wxPayV3Bean.getNotifyUrl());
        prepayRequest.setDescription(req.getDescription());
        prepayRequest.setOutTradeNo(tradeNo);
        prepayRequest.setAttach(req.getOrderType());
        //根据token拿到openid,指定该预支付订单的支付者身份
        String token = request.getHeader("token");
        Map<String, Object> openMap = wechatUserMapper.getOpenIdByToken(token);
        Payer payer = new Payer();
        payer.setOpenid(openMap.get("openId").toString());
        prepayRequest.setPayer(payer);

        // 调用下单方法,得到应答
        PrepayResponse response = service.prepay(prepayRequest);
        Map<String, Object> params = new HashMap<>();
        Long timeStamp = System.currentTimeMillis() / 1000;
        params.put("timeStamp", timeStamp);
        String substring = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
        params.put("nonceStr", substring);
        String signatureStr = Stream.of(wxPayV3Bean.getAppId(), String.valueOf(timeStamp), substring, "prepay_id=" + response.getPrepayId())
                .collect(Collectors.joining("\n", "", "\n"));
        String sign = WXPayUtil.getSign(signatureStr, wxPayV3Bean.getKeyPath());
        params.put("paySign", sign);
        params.put("package", "prepay_id=" + response.getPrepayId());
        params.put("tradeNo", tradeNo);
        log.info("-----------------------调用下单方法应答对象:" + params);

        //保存订单信息
        try {
            String accountUserId = accountUserMapper.getAccountUserId(req.getAccountNumber().toString());
            Map<String, Object> orderMap = new HashMap<>();
            orderMap.put("id", IdTool.getIdStr());
            orderMap.put("accountUserId", accountUserId);
            orderMap.put("appUserId", openMap.get("id"));
            orderMap.put("payOpenId", openMap.get("openId"));
            orderMap.put("orderType", req.getOrderType());
            orderMap.put("orderDate", DateUtil.getTime());
            orderMap.put("orderStatus", "0");
            orderMap.put("orderPrice", req.getAmount());
            orderMap.put("prepayId", response.getPrepayId());
            orderMap.put("tradeNo", tradeNo);
            log.info("-----------------------本次支付订单信息:" + orderMap);
            orderMapper.insertOrder(orderMap);
            return ApiResponseBody.defaultSuccess(params);
        } catch (Exception e) {
            throw new ApiException(e.toString());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ApiResponseBody  payNotify(HttpServletRequest request) throws Exception {
        //读取请求体的信息
        ServletInputStream inputStream = request.getInputStream();
        StringBuffer stringBuffer = new StringBuffer();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s;
        //读取回调请求体
        while ((s = bufferedReader.readLine()) != null) {
            stringBuffer.append(s);
        }
        String s1 = stringBuffer.toString();
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        String signType = request.getHeader("Wechatpay-Signature-Type");
        String serialNo = request.getHeader(WECHAT_PAY_SERIAL);
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        // 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
        // 没有的话,则构造一个
        NotificationConfig config = new RSAAutoCertificateConfig.Builder()
                .merchantId(wxPayV3Bean.getMchId())
                .privateKeyFromPath(wxPayV3Bean.getKeyPath())
                .merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
                .apiV3Key(wxPayV3Bean.getApiKey())
                .build();
        // 初始化 NotificationParser
        NotificationParser parser = new NotificationParser(config);
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(serialNo)
                .nonce(nonce)
                .signature(signature)
                .timestamp(timestamp)
                // 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
                .signType(signType)
                .body(s1)
                .build();
        Transaction parse = parser.parse(requestParam, Transaction.class);
        log.info("-----------------------parse = " + parse);

        try {
            String status = orderMapper.queryPayOrderStatus(parse.getOutTradeNo().toString());
            if (status.equals("1")) {
                log.info("-----------------------该订单:" + parse.getOutTradeNo() + "已经进行回调,不可重复回调");
                return ApiResponseBody.defaultSuccess();
            }
            updateAccountDetail(parse);
        } catch (Exception e) {
            throw new ApiException(e.toString());
        }
        log.info("-----------------------回调完成-----------------------");
        return ApiResponseBody.defaultSuccess();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ApiResponseBody queryPayOrder(String tradeNo) {
         String status = orderMapper.queryPayOrderStatus(tradeNo);
         if (status.equals("1")) {
             return ApiResponseBody.defaultSuccess("SUCCESS");
         }
        // 使用自动更新平台证书的RSA配置,配置微信支付的自动证书管理功能
        Config config =
                new RSAAutoCertificateConfig.Builder()
                        .merchantId(wxPayV3Bean.getMchId())
                        .privateKeyFromPath(wxPayV3Bean.getKeyPath())
                        .merchantSerialNumber(wxPayV3Bean.getMchSerialNo())
                        .apiV3Key(wxPayV3Bean.getApiKey())
                        .build();
        // 构建service,用于处理JSAPI支付相关的操作
        JsapiService service = new JsapiService.Builder().config(config).build();

        //根据商户订单号查询支付订单
        QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest();
        queryRequest.setMchid(wxPayV3Bean.getMchId());
        queryRequest.setOutTradeNo(tradeNo);
        Transaction transaction = service.queryOrderByOutTradeNo(queryRequest);
        log.info("-----------------------支付状态:" + transaction.getTradeState());
        updateAccountDetail(transaction);
        return ApiResponseBody.defaultSuccess(transaction.getTradeState().toString());
    }

    /**
     * 时间+id为订单号
     * @param
     * @return
     */
    public String getTradeNo() {
        String idStr = IdTool.getIdStr();
        long timestamp = DateUtil.getDate();
//        //序列号是为了保证同一毫秒内生成的订单号的唯一性
//        AtomicInteger sequence = new AtomicInteger(0);
//        int nextSequence = sequence.getAndIncrement();
//
        try {
//            MessageDigest md = MessageDigest.getInstance("MD5");
//            byte[] messageDigest = md.digest(String.valueOf(nextSequence).getBytes());
//
//            BigInteger no = new BigInteger(1, messageDigest);
//            String encrypted = no.toString(10); // 将十六进制转为十进制表示的字符串
//
//            // 如果加密结果长度超过20位,则截取前20位
//            if (encrypted.length() > 20) {
//                encrypted = encrypted.substring(0, 20);
//            }
            String tradeNo = timestamp + idStr;
            return tradeNo;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public void updateAccountDetail(Transaction parse) {
        //更新订单状态
        Map<String, Object> map = new HashMap<>();
        map.put("tradeNo", parse.getOutTradeNo().toString());
        map.put("transactionId", parse.getTransactionId().toString());
        orderMapper.updateOrderStatus(map);
        //保存充值流水账单
        String outTradeNo = parse.getOutTradeNo();
        Map<String, Object> accountMap = accountUserMapper.getAccountUser(outTradeNo);
        Map<String, Object> billMap = new HashMap<>();
        billMap.put("id", IdTool.getIdStr());
        billMap.put("amount", parse.getAmount().getTotal());
        billMap.put("accountUserId", accountMap.get("accountUserId"));
        billMap.put("payOrderId", accountMap.get("payOrderId"));
        billMap.put("agoBalance", accountMap.get("balance"));
        billMap.put("createDate", DateUtil.getTime());
        billMap.put("laterBalance", BigDecimal.valueOf(parse.getAmount().getTotal()).add(BigDecimal.valueOf(Integer.valueOf(accountMap.get("balance").toString()))));
        billMap.put("billType", "pay");
        billMap.put("tradeType", "wechat-applet");
        log.info("-----------------------本次支付流水账单信息:" + billMap);
        orderMapper.insertBillLog(billMap);

        //更新户号的余额
        Map<String, Object> accountUserMap = new HashMap<>();
        accountUserMap.put("accountUserId", accountMap.get("accountUserId"));
        accountUserMap.put("balance", Long.valueOf(parse.getAmount().getTotal()) + Long.valueOf(accountMap.get("balance").toString()));
        accountUserMapper.updateBalance(accountUserMap);
    }
}

后面具体dao层xml里的sql就不具体写了。 

注意:这里有个特别大的一个坑,大家一定要注意。JSONObject千万要引fastjson2的包

至此完成!! 欢迎评论区讨论指出不足一起进步!!

做一个挑战风车的傻子,鲁莽也比怯懦更接近勇敢!

版权声明:本文为博主作者:无Bug不java原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/weixin_61428498/article/details/134728436

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2024年4月16日
下一篇 2024年4月16日

相关推荐