Springboot----项目整合微信支付(获取支付二维码)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Springboot----项目整合微信支付(获取支付二维码)

前言

目前更新的是Springboot项目整合微信支付系列的文章,可以在我的主页中找到该系列其他文章,这一系列的文章将会系统介绍如何在项目中引入微信支付的下单、关单、处理回调通知等功能。由于前面创作经验不足,写的文章可能不是很好,后面我会多加努力学习怎么创作,也请各位大佬有什么建议的可以不吝赐教。因为我侧重的方面不是介绍项目开发,所以关于项目开发的具体代码可以查看文末的项目源代码(后面可能会出文章介绍该项目的开发)。喜欢的话希望大家多多点赞评论收藏,当然还可以加个关注喔,目前我的愿望是突破500粉,求各位大佬成全,我在线回。


一:问题引入

在很多项目中都会涉及到支付问题,我这里的项目是外卖项目,需要再用户点击菜品加入购物车进行结算时候使用支付功能,可以采用微信支付或者支付宝支付,由于微信支付比较普遍,在这里我选用的就是微信支付。首先要说明的是,要使用微信支付是有一定的要求的,你需要获取到你的商户证书,商户私钥,商户号等等,这些东西个人是很难获取的,只有商家才能申请,可以到B站尚硅谷下载他们的证书来使用,亲测可以直接用。微信支付总体来说开发并不难,就是繁琐,因为涉及到交易问题,处理逻辑一定要严谨,但是中间很多繁琐的工作微信已经给我们封装好了方法,直接查看接口文档调用即可,省去了中间一些验签的细节,大大提高了开发效率。这里附上微信支付开发文档入口地址。我使用的是微信的Native支付,该支付方式特点是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式,当然如果你的是小程序的话可以使用小程序支付。

二:流程图

获取支付二维码并不难,难的地方在于如何对订单进行处理,当用户点击“去支付”按钮时候,这时候订单就已经生成了,需要将其存入数据库中,订单状态为未支付。这时候有一个问题,一般我们下单付款途径有两种,一种是通过购物车点击去支付直接进行付款,另一种是在购物车中点击去支付弹出付款界面之后点击取消,然后到“我的订单”界面进行支付,这种称为“锁单”。这两者有什么区别呢?区别无非就在于订单是否已经创建,当我们在购物车点击去支付在向微信服务器发送请求之前,这时候其实订单还没生成,前端传过来的参数并不是一个完整的订单信息,而是一些订单的相关信息,比如收货地址,订单备注等等,后台需要拿着这些参数生成一个订单。要注意的是,出于安全考虑,一般不会在前端将订单金额直接传到后端进行使用,因为这样有可能会在前端对金额进行修改。我这里采用的策略是前端将用户的购物车id传过来,然后统计其购物车金额。

说了那么多,要如何区分两种支付发起方式呢?其实很简单,前面说过两者区别就是订单有没有创建,在“我的订单”界面发起的支付请求是已经生成有订单信息了的,我们就可以通过判断前端穿过来的数据是否携带订单号来判断该支付请求是在哪发起的。假如订单号为空,那么该支付请求就是在购物车界面“去支付”处发起的,假如订单号不为空,那么该支付请求就是在“我的订单”处发起的。

这时候还要考虑另外一个问题,用户点击去支付按钮弹出支付二维码之后,用户可以点击空白处取消该弹窗,这时候假如用户多次这样反复操作,那么我们是否就需要针对用户的每一次点击都向微信支付服务器申请一个新的二维码呢?显然这是不需要的,只有当该支付二维码过期了或者用户重新选择商品才需要重新申请。为了解决这个问题,我采取的策略是将第一次获得的code_url存入redis,由于code_url的有效期为2小时,不妨就设置该code_url在redis的存活时间就为2小时。这样每次用户发起新请求时候先尝试在redis获取code_url,获取成功则直接返回,不需要再向微信支付后台服务器发送请求。这时候还有另外一个问题,这个问题我将在下面代码中讲解。

三:代码实现

1.Controller层

@PostMapping("/native")
@ApiOperation("调用统一下单API,生成支付二维码")
public R<Map> nativePay(@RequestBody Orders orders, HttpSession session) throws IOException {
    log.info("发起支付请求");
    //返回支付二维码和订单号
    Map body = wxPayService.nativePay(orders, session);
    if(body != null) {
        return R.success(body);
    } else {
        return R.error("获取支付信息失败!");
    }
}

2.OrdersServiceImpl

@Override
@Transactional
public Orders createOrder(Orders orders, HttpSession session) {
    //获取当前用户信息
    Long userId = (Long) session.getAttribute("user");
    //查询地址数据
    Long addressBookId = orders.getAddressBookId();
    AddressBook addressBook = addressBookService.getById(addressBookId);
    if (addressBook == null) {
        throw new CustomException("用户地址信息有误,不能下单");
    }
    //获取当前用户购物车数据
    LambdaQueryWrapper < ShoppingCart > SCLqw = new LambdaQueryWrapper <> ();
    SCLqw.eq(ShoppingCart:: getUserId, userId);
    List < ShoppingCart > shoppingCartList = shoppingCartService.list(SCLqw);
    //生成订单号
    long orderId = IdWorker.getId();
    //设置订单号
    orders.setNumber(String.valueOf(orderId));
    //设置订单状态(待付款)
    orders.setStatus(1);
    //设置下单用户id
    orders.setUserId(userId);
    //设置下单时间
    orders.setOrderTime(LocalDateTime.now());
    //设置付款时间
    orders.setCheckoutTime(LocalDateTime.now());
    //设置实收金额
    AtomicInteger amount = new AtomicInteger(0);
    for (ShoppingCart shoppingCart : shoppingCartList) {
        amount.addAndGet(shoppingCart.getAmount().multiply(new BigDecimal(100)).multiply(new BigDecimal(shoppingCart.getNumber())).intValue());
    }
    orders.setAmount(BigDecimal.valueOf(amount.get()));
    //设置用户信息
    User user = userService.getById(userId);
    orders.setPhone(addressBook.getPhone());
    orders.setAddress(addressBook.getDetail());
    orders.setUserName(user.getPhone());
    orders.setConsignee(addressBook.getConsignee());
    //保存单条订单信息
    this.save(orders);
    //设置订单详细信息
    List < OrderDetail > orderDetailList = new ArrayList <> ();
    for (ShoppingCart shoppingCart : shoppingCartList) {
        OrderDetail orderDetail = new OrderDetail();
        orderDetail.setName(shoppingCart.getName());
        orderDetail.setImage(shoppingCart.getImage());
        orderDetail.setOrderId(orderId);
        orderDetail.setDishId(shoppingCart.getDishId());
        orderDetail.setSetmealId(shoppingCart.getSetmealId());
        orderDetail.setDishFlavor(shoppingCart.getDishFlavor());
        orderDetail.setNumber(shoppingCart.getNumber());
        AtomicInteger detailAmount = new AtomicInteger(0);
        detailAmount.addAndGet(shoppingCart.getAmount().multiply(new BigDecimal(shoppingCart.getNumber())).intValue());
        orderDetail.setAmount(BigDecimal.valueOf(detailAmount.get()));
        orderDetailList.add(orderDetail);
    }
    //批量保存订单详细数据
    orderDetailService.saveBatch(orderDetailList);
    //清空购物车数据
    shoppingCartService.remove(SCLqw);
    return orders;
}

3.WxPayServiceImpl

@Override
public Map nativePay(Orders orders, HttpSession session) throws IOException {
    //获取用户id
    Long userId = (Long) session.getAttribute("user");
    //构建返回体Map
    Map < String, String > map = new HashMap <> ();
    //获取当前用户购物车数据
    LambdaQueryWrapper < ShoppingCart > SCLqw = new LambdaQueryWrapper <> ();
    SCLqw.eq(ShoppingCart:: getUserId, userId);
    List < ShoppingCart > shoppingCartList = shoppingCartService.list(SCLqw);
    //尝试获取订单号,若为空则表示这是通过“去支付”按钮发起的支付请求,否则为通过“我的订单”处发起请求,此时订单号已生成
    String number = orders.getNumber();
    //创建Order实体
    Orders order = new Orders();
    /*加入购物车判断的目的是防止获取到上一次支付的支付信息,假如购物车为空说明该笔支付重新生成了支付信息
    因为只有重新生成订单才会清空购物车*/
    if (shoppingCartList.size() == 0 && number == null) {
        //获取redis缓存
        String code_url = (String) redisTemplate.opsForValue().get("codeUrl");
        //获取订单id
        String order_id = (String) redisTemplate.opsForValue().get("orderId");
        //支付链接还未过期,直接返回
        if (code_url != null && order_id != null) {
            map.put("codeUrl", code_url);
            map.put("orderId", order_id);
            return map;
        }
        if (order_id == null && code_url != null) {
            //订单超时未支付
            map.put("message", "timeout");
            return map;
        }
    }
    if (number != null) {
        //表示该请求是在“我的订单”页面发起的支付请求,此时该订单已经被创建
        String key = "orderNo_" + number + "_codeUrl";
        String codeUrl = (String) redisTemplate.opsForValue().get(key);
        if (codeUrl != null) {
            map.put("codeUrl", codeUrl);
            map.put("orderId", number);
            return map;
        }
        order = orders;
    } else {
        order = ordersService.createOrder(orders, session);
    }
    //存储订单id,存活时间为5分钟
    redisTemplate.opsForValue().set("orderId", order.getNumber(), 5, TimeUnit.MINUTES);
    map.put("orderId", order.getNumber());
    //计算金额
    log.info("计算支付金额...");
    LambdaQueryWrapper < Orders > lqw = new LambdaQueryWrapper <> ();
    lqw.eq(Orders:: getNumber, order.getNumber());
    Orders order_getAmount = ordersService.getOne(lqw);
    BigDecimal amount = order_getAmount.getAmount();
    int PayAmount = amount.intValue();
    //调用统一下单API
    HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
    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();
    rootNode.put("mchid", wxPayConfig.getMchId())
        .put("appid", wxPayConfig.getAppid())
        .put("description", "外卖订单")
        .put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()))
        .put("out_trade_no", orders.getNumber());  //订单号
    rootNode.putObject("amount")
        .put("total", PayAmount);  //金额
    objectMapper.writeValue(bos, rootNode);
    httpPost.setEntity(new StringEntity(bos.toString(StandardCharsets.UTF_8), "UTF-8"));
    CloseableHttpResponse response = wxPayClient.execute(httpPost);
    String bodyAsString = EntityUtils.toString(response.getEntity());
    JSONObject body = JSON.parseObject(bodyAsString);
    String code_url = (String) body.get("code_url");
    map.put("codeUrl", code_url);
    //将Code_url存入redis缓存,有效期为2小时
    redisTemplate.opsForValue().set("codeUrl", code_url, 2, TimeUnit.HOURS);
    redisTemplate.opsForValue().set("orderNo_" + order.getNumber() + "_codeUrl", code_url, 2, TimeUnit.HOURS);
    log.info("Code_url===>{}", code_url);
    return map;
}

可以看到,在判断订单号是否为空的 if(shoppingCartList.size() = = 0 && number = = null)语句中加入了shoppingCartList.size() == 0条件,为什么要加入这个条件呢?试想下我们将code_url存入redis中会存在什么问题,redis中code_url是有存活时间的,假如用户知道这个漏洞,买了一个1分钱的商品下单之后没付款,然后退出页面挑选了一个999大洋的商品下单,这时候加入判定条件是if(number = = null),这时候该条件肯定成立,因为用户是在购物车界面发起的支付请求,这时候压根没有订单号,那么问题就来了,这时候继续执行if里面的代码

//获取redis缓存
String code_url = (String) redisTemplate.opsForValue().get("codeUrl");
//获取订单id
String order_id = (String) redisTemplate.opsForValue().get("orderId");
//支付链接还未过期,直接返回
if(code_url != null && order_id != null){
       map.put("codeUrl",code_url);
       map.put("orderId",order_id);
       return map;
}

这时候获取到的还是上一次1分钱商品的支付码呀!这样聪明的用户不就可以支付1分钱获得价值999大洋的商品了吗,显然这样资本家是不同意的。如何解决这个问题呢?首先肯定是不能将redis取消掉的,万一取消掉之后有闲着没事干的用户要采用某些技术频繁发送支付请求就是不支付呢?这样微信那边不就禁了你的账号。这时候就要思考,当用户在购物车界面发起支付请求还携带有哪些信息可以利用到呢?没错,就是购物车数据,我们通过购物车去结算当你支付完成之后你会发现购物车原本的商品会被清空,出于严谨考虑我这也会实现这一功能。购物车是什么时候被清空的呢,看程序,只有在订单创建完成之后购物车才会清空,而订单创建的前提又是订单号为空并且能够成功在redis中获取支付URL,当用户想利用这个漏洞时候前面两个条件肯定是符合的,那么我们就正好可以利用这两个条件来修复漏洞。实现过程很简单,也就仅仅加了一个判断用户购物车数据是否为空,当用户购物车为空并且能够成功获取支付URL时候将code_url返回即可。当用户购物车不为空,说明用户更换了商品,这时候就需要向微信支付服务端发送新的请求获取新的支付URL。

至此,微信支付的用户下单功能已完成,至于如何在前端将Code_url转换成二维码展示给用户,可以查看我这篇文章附上链接

四:友情链接

相关文章
|
6月前
|
Java Maven Android开发
微服务——SpringBoot使用归纳——Spring Boot开发环境搭建和项目启动
本文介绍了Spring Boot开发环境的搭建和项目启动流程。主要内容包括:jdk的配置(IDEA、STS/eclipse设置方法)、Spring Boot工程的构建方式(IDEA快速构建、官方构建工具start.spring.io使用)、maven配置(本地maven路径与阿里云镜像设置)以及编码配置(IDEA和eclipse中的编码设置)。通过这些步骤,帮助开发者顺利完成Spring Boot项目的初始化和运行准备。
560 0
微服务——SpringBoot使用归纳——Spring Boot开发环境搭建和项目启动
|
5月前
|
前端开发 安全 Java
Spring Boot 便利店销售系统项目分包设计解析
本文深入解析了基于Spring Boot的便利店销售系统分包设计,通过清晰的分层架构(表现层、业务逻辑层、数据访问层等)和模块化设计,提升了代码的可维护性、复用性和扩展性。具体分包结构包括`controller`、`service`、`repository`、`entity`、`dto`、`config`和`util`等模块,职责分明,便于团队协作与功能迭代。该设计为复杂企业级应用开发提供了实践参考。
219 0
|
6月前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
92 0
|
6月前
|
Java 微服务 Spring
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——使用Logger在项目中打印日志
本文介绍了如何在项目中使用Logger打印日志。通过SLF4J和Logback,可设置不同日志级别(如DEBUG、INFO、WARN、ERROR)并支持占位符输出动态信息。示例代码展示了日志在控制器中的应用,说明了日志配置对问题排查的重要性。附课程源码下载链接供实践参考。
722 0
|
2月前
|
Java 关系型数据库 MySQL
springboot项目集成dolphinscheduler调度器 实现datax数据同步任务
springboot项目集成dolphinscheduler调度器 实现datax数据同步任务
333 2
|
2月前
|
分布式计算 Java 大数据
springboot项目集成dolphinscheduler调度器 可拖拽spark任务管理
springboot项目集成dolphinscheduler调度器 可拖拽spark任务管理
151 2
|
2月前
|
移动开发 小程序 开发工具
揭秘微信/支付宝6大支付方式:从扫码到刷脸,谁在偷偷赚你的手续费?优雅草卓伊凡
揭秘微信/支付宝6大支付方式:从扫码到刷脸,谁在偷偷赚你的手续费?优雅草卓伊凡
345 0
揭秘微信/支付宝6大支付方式:从扫码到刷脸,谁在偷偷赚你的手续费?优雅草卓伊凡
|
2月前
|
Java 测试技术 Spring
简单学Spring Boot | 博客项目的测试
本内容介绍了基于Spring Boot的博客项目测试实践,重点在于通过测试驱动开发(TDD)优化服务层代码,提升代码质量和功能可靠性。案例详细展示了如何为PostService类编写测试用例、运行测试并根据反馈优化功能代码,包括两次优化过程。通过TDD流程,确保每项功能经过严格验证,增强代码可维护性与系统稳定性。
148 0
|
2月前
|
存储 Java 数据库连接
简单学Spring Boot | 博客项目的三层架构重构
本案例通过采用三层架构(数据访问层、业务逻辑层、表现层)重构项目,解决了集中式开发导致的代码臃肿问题。各层职责清晰,结合依赖注入实现解耦,提升了系统的可维护性、可测试性和可扩展性,为后续接入真实数据库奠定基础。
248 0
|
3月前
|
Java API
wxid添加微信好友工具,免费微信wxid转换器二维码,jar实现仅供学习参考
本项目实现微信ID与wxid的转换及二维码生成功能,核心逻辑基于ZXing库完成QR编码,支持文件批量导入导出。

热门文章

最新文章