• 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版本多

    image-20230914164243283

    绑定微信小程序

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

    image-20231005110030294

    开发微信支付

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

    接入前准备

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

    image-20231005110519599

    引入maven

    <dependency>
        <groupId>com.github.wechatpay-apiv3groupId>
        <artifactId>wechatpay-javaartifactId>
        <version>0.2.11version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    支付

    刚开始对接微信支付,咱们肯定先对接支付,看一下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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    通过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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    支付通知

    参考文档

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

    注意

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

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

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

    解析请求

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    解密、处理业务

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    主动查询订单

    参考文档

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

    @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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    退款

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

    创建退款订单

    参考文档

    这里需要注意的是,创建退款订单会有一个返回对象,里面包含着请求信息,但是微信退款是有延迟的,测试的时候到账时间在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    退款通知

    参考文档

    注意

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

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

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

    @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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    总结

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

  • 相关阅读:
    WebRTC学习笔记五 SDP(Session Description Protocol)
    Modern C++ JSON nlohmann::json 使用详解
    Git常用方法
    结合《药品冷链物流运作规范》浅谈如何进行药品供应链监测
    使用Python创建音乐播放器
    GBase 8c 存储技术---内存引擎(三)
    手写Promise(一)
    计算机网络总结笔记
    描述一下Java中的RMI(远程方法调用)
    fmp4打包H265视频流
  • 原文地址:https://blog.csdn.net/Yellow_Star___/article/details/133579153