Java实现微信支付

微信支付

小黄最在工作中对接需要对接微信支付,在此记录一下微信支付开发的相关流程,望能帮助到各位

前期准备

由于小黄是小程序端需要对接微信支付,需要注册小程序等,小黄会一一列举出来

小程序注册

所需文件
  • 没有注册过微信公众平台、微信开放平台的邮箱
  • 营业执照
  • 对公账户信息(需要对公账户打款来校验)
  • 纳税识别号
  • 300大洋
注册

注册网址:https://mp.weixin.qq.com/cgi-bin/wx

按要求填写所需文件信息即可,由于是企业注册的小程序,需要对公账户汇款,只需要几毛钱并且这个钱会退回来的

填写小程序信息

注册完成后,开始填写小程序信息,必须填写小程序信息,才能获得appid,也就是小程序id,开发中会用到

开通微信认证

这里需要支付300元的费用

微信支付注册

目前只支持企业注册微信支付

所需文件
  • 营业执照照片
  • 法人身份证照片
  • 所属行业的特殊资质
  • 超级管理员证明(需要公章)
  • 结算账户(对公账户)
注册

注册网址:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal

同样也是按要求填写信息即可,也是需要对公账户汇款

申请api证书

在开发中需要读取到api证书来解析你的api是否正确,所以这一步是必不可少的,另外推荐设置APIv3密钥,功能比v2版本多

Java实现微信支付_第1张图片

绑定微信小程序

在产品中心里,可以绑定微信小程序

Java实现微信支付_第2张图片

开发微信支付

微信支付有很多支付方式,具体可以参考产品文档,因为业务需求,小黄使用的是JSAPI支付方式,接下来就重点介绍一下JSAPI支付方式。

接入前准备

需要设置支付授权目录,这里小黄设置的就是公司测试域名

Java实现微信支付_第3张图片

引入maven

<dependency>
    <groupId>com.github.wechatpay-apiv3groupId>
    <artifactId>wechatpay-javaartifactId>
    <version>0.2.11version>
dependency>

支付

刚开始对接微信支付,咱们肯定先对接支付,看一下JSAPI支付的时序图,其实需要Java后端做的就只有2-6,15-21

微信创建订单

参考文档

在创建订单之前的业务处理,小黄就不贴出来了,每个开发的业务都不一样,主要是JSAPI下单给微信接受他的返回值,也就是时序图中的2-6步骤

创建配置类

商户证书序列号,可以通过执行openssl x509 -noout -serial -in apiclient_cert.pem指令解析出来,cmd到证书所在位置

image-20231005112010001

@Configuration
@Data
@RefreshScope
public class WechatConfig {
    @Value("${wechat.mchId}")
    private String mchId; //微信支付id

    @Value("${wechat.filePath}")
    private String filePath; //微信支付证书所存放的位置

    @Value("${wechat.merchantSerialNumber}")
    private String merchantSerialNumber; //商户证书序列号

    @Value("${wechat.apiV3Key}")
    private String apiV3Key; //apiv3密钥

    @Value("${wechat.appId}")
    private String appId; //绑定的小程序id

    @Value("${wechat.notify-url}")
    private String notifyUrl; //支付回调地址(目前用不到)

    @Value("${wechat.refund-notify-url}")
    private String refundNotifyUrl; //退款回调地址(目前用不到)

    @Bean
    public RSAAutoCertificateConfig rsaAutoCertificateConfig(){
        return new RSAAutoCertificateConfig.Builder()
                .merchantId(mchId)
                .merchantSerialNumber(merchantSerialNumber)
                .apiV3Key(apiV3Key)
                .privateKeyFromPath(filePath)
                .build();
    }
}

通过api调用微信支付

之前小黄看到好多文档都是通过http请求来调用的,加了依赖后,相当于微信封装了一层,使用起来更方便了,只需要向PrepayRequest中塞数据即可

这里有两个注意点:

  • 金额的单位是分
  • 必须要填异步回调地址(回调地址的作用是当用户支付后,会访问这个地址访问支付信息,该地址必须是https,测试的时候建议使用花生壳工具走内网穿透)
public PrepayWithRequestPaymentResponse prepay(JsPrepayDto jsPrepayDto, MemberUserEntity memberUser) {
    //下单并生成调起支付的参数
    JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();

    PrepayRequest request = new PrepayRequest();
    request.setAppid(wechatConfig.getAppId());
    request.setMchid(wechatConfig.getMchId());
    request.setOutTradeNo(jsPrepayDto.getTradeNo()); //传入自己平台的订单id
    request.setDescription(jsPrepayDto.getDescription());
    request.setNotifyUrl(wechatConfig.getNotifyUrl());
    Amount amount = new Amount();
    //todo:测试只支付一分
    amount.setTotal(1);
    request.setAmount(amount);
    Payer payer = new Payer();
    payer.setOpenid(memberUser.getWxOpenId());
    request.setPayer(payer);
    PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request);

    return response;
}
支付通知

参考文档

创建订单完成后,微信会主动调用设置的回调地址,将加密的数据返回给我们

注意

对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功

  • 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
  • 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。

特别提醒: 商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。

解析请求

private String getRequestBody(HttpServletRequest request) {
    StringBuffer sb = new StringBuffer();
    try (ServletInputStream inputStream = request.getInputStream();
         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    ) {
        String line;

        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
    } catch (IOException e) {
        log.error("读取数据流异常:{}", e);
    }
    return sb.toString();
}

解密、处理业务

Transaction类有很多,注意是com.wechat.pay.java.service.payments.model包下的

@Transactional(rollbackFor = Exception.class)
public Map wechatNotify(HttpServletRequest request, HttpServletResponse response) {
    HashMap<String, String> map = new HashMap<>();
    //获取报文
    String body = getRequestBody(request);
    //随机串
    String nonceStr = request.getHeader("Wechatpay-Nonce");

    //微信传递过来的签名
    String signature = request.getHeader("Wechatpay-Signature");

    //证书序列号(微信平台)
    String serialNo = request.getHeader("Wechatpay-Serial");

    //时间戳
    String timestamp = request.getHeader("Wechatpay-Timestamp");
    // 构造 RequestParam
    RequestParam requestParam = new RequestParam.Builder()
            .serialNumber(serialNo)
            .nonce(nonceStr)
            .signature(signature)
            .timestamp(timestamp)
            .body(body)
            .build();
    // 初始化 NotificationParser
    NotificationParser parser = new NotificationParser(config);
    Transaction transaction = null;
    try {
        // 验签、解密并转换成 Transaction
        transaction = parser.parse(requestParam, Transaction.class);
        log.info("=============解密:Transaction ============= {}", transaction);
    } catch (Exception e) {
        log.error("签名验证失败");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        map.put("code", "FAIL");
        map.put("message", "失败");
        return map;
    }

    String lockName = "wechat_notice_" + transaction.getOutTradeNo();
    RLock lock = redissonClient.getLock(lockName);
    try {
        //微信可能会重复发送数据,如果已经处理过,应该直接返回成功
        //加锁,避免并发重复添加
        lock.lock(5,TimeUnit.SECONDS);
        log.info("=============接收到微信支付回调通知=============");
        
        //-----处理业务逻辑-----
       
        //通知微信回调成功
        map.put("code", "SUCCESS");
        return map;
    } catch (Exception e) {
        log.error("业务处理失败");
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        map.put("code", "FAIL");
        map.put("message", "失败");
        return map;
    } finally {
        lock.unlock();
    }
}
主动查询订单

参考文档

这个请求可以查询订单的支付状态

@Transactional(rollbackFor = Exception.class)
public void findPaymentOrder(String payOrderId) {
    JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
    QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
    request.setMchid(wechatConfig.getMchId());
    request.setOutTradeNo(payOrderId);
    Transaction transaction = service.queryOrderByOutTradeNo(request);
    log.info("============= Transaction ============= {}", transaction);
    String lockName = "wechat_notice_" + transaction.getOutTradeNo();
    RLock lock = redissonClient.getLock(lockName);
    try {
        //加锁,避免并发重复添加
        lock.lock(5,TimeUnit.SECONDS);
       	//处理业务逻辑
    } finally {
        lock.unlock();
    }
}

退款

支付功能完成后,咱们开始着手做退款功能,其实跟支付差不多,先是去创建退款订单,再来接受微信发起的退款回调

创建退款订单

参考文档

这里需要注意的是,创建退款订单会有一个返回对象,里面包含着请求信息,但是微信退款是有延迟的,测试的时候到账时间在1-10秒内,如果要在到账后在做操作,建议通过回调来实现

public Boolean refundOrder(WechatRefundReq data) {
    //查询支付订单信息
    PayOrderDetailsEntity order = payOrderDetailsDao.selectOne(new LambdaQueryWrapper<PayOrderDetailsEntity>().eq(PayOrderDetailsEntity::getPayOrderId, data.getPayOrderId()));
    if (data.getRefundAmount().compareTo(order.getPayAmount()) > 0) {
        log.error("退款金额超出订单金额:data : {} , order : {}", data, order);
        return false;
    }
    //退款接口
    RefundService service = new RefundService.Builder().config(config).build();
    CreateRequest request = new CreateRequest();
    request.setTransactionId(order.getTradeNo());
    request.setOutRefundNo(data.getRefundOrderId());
    //用户退款原因,会在用户端微信提示
//        request.setReason(data.getReason());
    request.setNotifyUrl(wechatConfig.getRefundNotifyUrl());
    AmountReq amount = new AmountReq();
    amount.setRefund(data.getRefundAmount().multiply(BigDecimal.valueOf(100)).longValue());
    amount.setTotal(order.getPayAmount().multiply(BigDecimal.valueOf(100)).longValue());
    amount.setCurrency("CNY");
    request.setAmount(amount);
    log.info("======开始处理微信退款======");
    Refund refund = service.create(request);
    log.info("refund : {}" ,refund);

    return true;
}
退款通知

参考文档

注意

对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功

  • 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
  • 如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。

特别提醒: 商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。

@Transactional(rollbackFor = Exception.class)
public Map wechatNotify(HttpServletRequest request, HttpServletResponse response) {
    HashMap<String, String> map = new HashMap<>();
    //获取报文
    String body = getRequestBody(request);
    //随机串
    String nonceStr = request.getHeader("Wechatpay-Nonce");

    //微信传递过来的签名
    String signature = request.getHeader("Wechatpay-Signature");

    //证书序列号(微信平台)
    String serialNo = request.getHeader("Wechatpay-Serial");

    //时间戳
    String timestamp = request.getHeader("Wechatpay-Timestamp");
    // 构造 RequestParam
    RequestParam requestParam = new RequestParam.Builder()
            .serialNumber(serialNo)
            .nonce(nonceStr)
            .signature(signature)
            .timestamp(timestamp)
            .body(body)
            .build();
    // 初始化 NotificationParser
    NotificationParser parser = new NotificationParser(config);
    Transaction transaction = null;
    try {
        // 验签、解密并转换成 Transaction
        transaction = parser.parse(requestParam, Transaction.class);
        log.info("=============解密:Transaction ============= {}", transaction);
    } catch (Exception e) {
        log.error("签名验证失败");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        map.put("code", "FAIL");
        map.put("message", "失败");
        return map;
    }

    String lockName = "wechat_notice_" + transaction.getOutTradeNo();
    RLock lock = redissonClient.getLock(lockName);
    try {
        //微信可能会重复发送数据,如果已经处理过,应该直接返回成功
        //加锁,避免并发重复添加
        lock.lock(5,TimeUnit.SECONDS);
        log.info("=============接收到微信支付回调通知=============");
        
        //-----处理业务逻辑-----
        
        //通知微信回调成功
        map.put("code", "SUCCESS");
        return map;
    } catch (Exception e) {
        log.error("业务处理失败");
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        map.put("code", "FAIL");
        map.put("message", "失败");
        return map;
    } finally {
        lock.unlock();
    }
}

总结

至此,微信支付的主要流程就已经走完了,其他的api可以参考官方文档,对照着SDK来做,应该是轻而易举的

你可能感兴趣的:(小黄工作中,java,微信,微信小程序)