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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容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转换成二维码展示给用户,可以查看我这篇文章附上链接

四:友情链接

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
存储 前端开发 Java
揭秘!如何用Spring Boot轻松打造动态二维码生成器?一键解锁无限可能,你的创意将无处不在!
【8月更文挑战第29天】在数字化时代,二维码成为信息快速传递的关键工具,广泛应用于支付、身份验证和产品追溯等场景。本文将指导你如何利用Spring Boot框架和Google的ZXing库,搭建一个动态生成二维码的Web服务。首先,通过Spring Initializr创建项目并配置相关依赖;接着,编写二维码生成逻辑和服务类;最后,在Controller中整合这些功能,提供RESTful接口供外部调用。通过访问`/generate-qrcode?text=你的内容`即可测试API并获取二维码图片。这为开发者提供了强大的工具,未来还可进一步优化存储和提升性能。
107 3
|
1月前
|
Java 容器
如何在SpringBoot项目中使用过滤器和拦截器
过滤器和拦截器是日常开发中常用技术,用于对特定请求进行增强处理,如插入自定义代码以实现特定功能。过滤器在请求到达 `servlet` 前执行,而拦截器在请求到达 `servlet` 后执行。`SpringBoot` 中的拦截器依赖于 `SpringBoot` 容器,过滤器则由 `servlet` 提供。通过实现 `Filter` 接口并重写 `doFilter()` 方法可实现过滤器;通过实现 `HandlerInterceptor` 接口并重写相应方法可实现拦截器。两者的主要区别在于执行时机的不同,需根据具体场景选择使用。
如何在SpringBoot项目中使用过滤器和拦截器
|
15天前
|
前端开发 JavaScript Java
SpringBoot项目部署打包好的React、Vue项目刷新报错404
本文讨论了在SpringBoot项目中部署React或Vue打包好的前端项目时,刷新页面导致404错误的问题,并提供了两种解决方案:一是在SpringBoot启动类中配置错误页面重定向到index.html,二是将前端路由改为hash模式以避免刷新问题。
66 1
|
1月前
|
Java 关系型数据库 MySQL
创建一个SpringBoot项目,实现简单的CRUD功能和分页查询
【9月更文挑战第6天】该内容介绍如何使用 Spring Boot 实现具备 CRUD 功能及分页查询的项目。首先通过 Spring Initializr 创建项目并选择所需依赖;其次配置数据库连接,并创建实体类与数据访问层;接着构建服务层处理业务逻辑;最后创建控制器处理 HTTP 请求。分页查询可通过添加 URL 参数实现。
|
13天前
|
JavaScript Java 关系型数据库
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
本文介绍了一个基于Spring Boot和Vue.js实现的在线考试系统。随着在线教育的发展,在线考试系统的重要性日益凸显。该系统不仅能提高教学效率,减轻教师负担,还为学生提供了灵活便捷的考试方式。技术栈包括Spring Boot、Vue.js、Element-UI等,支持多种角色登录,具备考试管理、题库管理、成绩查询等功能。系统采用前后端分离架构,具备高性能和扩展性,未来可进一步优化并引入AI技术提升智能化水平。
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
|
15天前
|
Java 关系型数据库 MySQL
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
本文介绍了一款基于Spring Boot和JSP技术的房屋租赁系统,旨在通过自动化和信息化手段提升房屋管理效率,优化租户体验。系统采用JDK 1.8、Maven 3.6、MySQL 8.0、JSP、Layui和Spring Boot 2.0等技术栈,实现了高效的房源管理和便捷的租户服务。通过该系统,房东可以轻松管理房源,租户可以快速找到合适的住所,双方都能享受数字化带来的便利。未来,系统将持续优化升级,提供更多完善的服务。
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
|
6天前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
27 10
|
5天前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
|
10天前
|
JavaScript 前端开发 Java
SpringBoot项目的html页面使用axios进行get post请求
SpringBoot项目的html页面使用axios进行get post请求
27 6
|
11天前
|
消息中间件 Java Kafka
springboot项目启动报错-案例情景介绍
springboot项目启动报错-案例情景介绍
19 2