瑞吉外卖项目技术栈总结,及个人项目理解。
缓存任务队列消息队列分布式锁注意:在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。
其他知识
git
Git 是一个分布式版本控制工具,通常用来对软件开发过程中的源代码文件进行管理。通过Git 仓库来存储和管理这些文件,Git 仓库分为两种1. 本地仓库:开发人员自己电脑上的 Git 仓库2.远程仓库:远程服务器上的 Git 仓库
- commit:提交将本地文件和版本信息保存到本地仓库
- push: 推送将本地仓库文件和版本信息上传到远程仓库
- pull: 拉取,将远程仓库文件和版本信息下载到本地仓库
git的功能
- 代码回溯
- 版本切换
- 多人协作
- 远程备份
git代码托管服务
- 首先需要创建仓库
- 邀请成员
git常用命令
全局设置
当安装Git后首先要做的事情是设置用户名称和email地址。这是非常重要的,因为每次Git提交都会使用该用户信息。在Git 命令行中执行下面命令:
- 设置用户信息
git config --global user.name “duanjq”
git config --global user.email “hello@duanjq.cn"
- 查看配置信息
git config --list
注意:上面设置的user.name和user.email并不是我们在注册码云账号时使用的用户名和邮箱,此处可以任意设置
获取git仓库
两种方式:
- 在本地初始化一个git仓库(不常用)
- 在任意目录下创建一个空目录,作为我们的本地仓库
- 进入这个目录中,点击右键打开Git bash窗口
- 执行git init命令
如果在目录中看到.git文件夹,则说明git本地仓库创建成功
- 从远程仓库克隆
- 可以通过git提供的命令从远程的仓库进行克隆,将远程仓库克隆到本地
- 命令形式:git clone 【远程仓库的地址】
工作区、暂存区、版本库 概念
- 版本库:前面看到的.git隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等
- 工作区:包含.git文件夹的目录就是工作区,也称为工作目录,主要用于存放开发的代码
- 暂存区:.t文件夹中有很多文件,其中有一个index文件就是暂存区,也可以叫做stage。暂存区是一个临时保存修改文件的地方
Git工作区中文件的状态
- untracked 未跟踪(床被纳入版本控制)
- tracked 已跟踪(被纳入版本控制)
- Unmodified 未修改状态
- Modified 已修改状态
- staged 已暂存状态
注意: 这些文件的状态会随着我们执行Git的命令发生变化
本地仓库的常用命令
- git status 查看文件的状态
- git add 将文件的修改加入暂存区
- git reset 将暂存区的文件取消暂存或者切换到指定的版本
git reset --hard 版本号
- git commit 将暂存区的文件修改提交到版本库
- git log 查看日志
远程仓库的操作命令
- git remote 查看远程仓库
- git remote add 添加远程仓库
- git clone 从远程仓库克隆
- git pull 从远程仓库拉取
- git push 推送到远程仓库
分支操作
分支是Git使用过程中,非常重要的概念。使用分支意味着可以把你的工作从开发主线上分离开来,以免影响主线的开发,同一个仓库可以有多个分支,各个分支相互独立,互不干扰。
通过git init命令创建本地仓库时,会默认创建一个master分支
常用命令:
- git branch 查看分支
- git branch[name] 创建分支
- git checkout[name] 切换分支
- git push[shortName][name] 推送至远程分支
- git merge [name] 合并分支
标签操作
Git中的标签,指的是某个分支某个特定时间的状态。通过标签,可以很方便的切换到标记时的状态。
比较代表性的是人们用这个功能来标记发布节点(v1.0,v2.0)等
标记不同版本
常用命令:
- git tag 列出已有的标签
- git tag[name] 创建标签
- git push[shortName][name] 将标签推送到远程仓库
- git checkout -b [branch][name] 检出标签
在IDEA中使用Git
在IDEA中配置Git
获取Git仓库
本地仓库操作
- 将文件加入暂存区
- 将暂存区的文件提交到版本库
- 查看日志
其他知识:
- split(“ ”)以什么分割开,返回值为一个数组,可以根据数组下表获取想要的值。
一、知识点概要
- 默认可以访问的静态资源目录为static和templet,如果有其他的目录,需要设置配置类WebMvcConfig继承WebMvcConfigurationSupport类,并重写addResourceHandlers方法来设置静态资源映射。
- registry.addSourceHandler(…pathPatterns:“/backend/**”).addResourceLocations(“classpath:/backend/”)//网页访问backend包下的所有路径时会映射到项目目录下的backend下的静态页面
- MybatisPlus相关知识
- MyBatisPlus配置文件中配置map-underscore-to-camel-case:true//在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射。
- Mapper接口:需要加@Mapper注解,接口继承BaseMapper接口,需要提供一个泛型<实体类>
- Service接口:需要继承IService接口<实体类>泛型
- ServiceImpl:添加@Service注解,extend ServiceImpl<Mapper接口,实体类>implements Service接口
- Controller:添加@RestController接口(返回的json数据,不是页面)
- 导入返回结果类R(所有的controller方法都会返回一个R对象)
- 此类是一个通用结果类,服务端响应的所有结果最终都会包装成此类类型返回给前端页面 A类属性:
- code:编码:1成功,0和其他数字为失败
- msg:错误信息,业务逻辑失败时,向msg属性设值返回给前端页面
- T data:数据 可以存储实体
- Map map:动态数据
- 登录功能的实现
- public R login(HttpServeletRequest request,@RequestBody Employee employe)其中@RequestBody Employee employe把前台提交的数据封装到Employee实体属性中,request可以获取登陆成功的用户信息。
- 完善登录功能,访问后台页面首先判断是否登录
- 创建自定义过滤器LoginCheckFilter
- 在启动类上加入注解@ServletComponentScan(扫描过滤器)
- 完善过滤器的处理逻辑
- 员工信息分页查询
- 配置MybatisPlus的分页插件
- MyBatisPlusInterceptor对象的addInterceptor(newPaginationInnerInterceptor)方法
- 然后返回MyBatisPlusInterceptor对象
- 加Bean注解
- 代码逻辑实现
- 构造分页构造器Page pageInfo = new Page(page,pageSize)
- 构造条件构造器LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
- 执行查询employeeService.page(pageInfo,queryWrapper);
- 配置MybatisPlus的分页插件
- 启用、禁用功能
- js数据转换格式会有误差,长整型的数字不能够完整显示,需要转换为字符串形式
- 提供对象转换器JackSonObjectMapper,基于jackson进行java对象到json数据的转换
- 在webMvcConfig配置类中扩展Spring Mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换。
- 编辑员工信息
linux
常用命令
linux命令格式
文件目录操作命令
- ls
- 作用:显示指定目录下的内容
- 语法:ls[al][dir]
说明:
- -a显示所有文件夹及目录(.开头的隐藏文件也会显示)
- -l 除文件名称外,同时将文件型态(d表示目录,-表示文件)、权限、拥有者、文件大小等信息详细列出
注意:
由于我们使用LS命令时经常需要加入-l选项,所以Linux为ls -l命令提供了一种简写方式,即ll
- cd
- cat
4. more
5. tail
6. mkdir
7. rmdir
8. rm
拷贝移动命令
- cp
- mv
打包压缩命令
- tar
文本编辑命令
- vim
查找命令
- find
注意:-name是固定写法(按照名称查找)
- grep
软件安装
安装jdk
安装tomcat
防火墙操作:
安装MySQL
注意:如果当前系统中已经安装有MYSQL数据库,安装将失败,Centos7自带mariadb,与MYSQL数据库冲突
安装lrzsz
- 搜索lrzsz安装包,命令为 yum list lrzsz
- 使用yum命令在线安装,命令为yum install lrzsz.x86_64
项目部署
手工部署项目
- 在idea中开发springboot项目并打成jar包
- java -jar来运行项目
通过shell脚本自动部署项目
操作步骤:
- 在Linux中安装Git
使用git克隆代码:
- 在Linux中安装maven
- 编写shell脚本(拉取代码、编译、打包、启动)
- 为用户授予执行Shell脚本的权限
- 执行Shell脚本
- 设置静态ip
- 重启网络服务
Redis
redis是一个基于内存的key-value结构数据库
特点:
- 基于内存存储,读写性能高
- 适用于存储热点数据(热点商品、咨询、新闻)
Redis入门
简介
应用场景
- 缓存
- 任务队列
- 消息队列
- 分布式锁
Redis下载安装
- Linux下载安装:
- window安装
直接解压
Redis服务启动停止
- Linux:
进入src目录下./redis-server启动
停止:Ctrl+c
- windows:直接点击
Redis的数据类型
Redis存储的是key-value结构的数据,其中key是字符串数据,value有5中常用的数据类型
- 字符串 String
普通的字符串,常用
- 哈希 hash
hash适合存储对象
- 列表 list
list按照插入顺序排序,可以有重复的元素
- 集合 set
set 无序集合,没有重复元素
- 有序集合 sorted set
sorted set 有序集合,没有重复元素
Redis常用命令
字符串String操作命令
注意:set命令,当key值相同时,后面设置的值会覆盖前面的值
哈希hash 操作命令
Redis hash是一个String类型的filed和value的映射表,hash特别适合用于存储对象,常用命令:
举个例子:
列表list操作命令
Redis列表是简单的字符串列表,按照插入顺序排序
常用命令:举个例子:
集合Set 操作命令
Redis set是String类型的无序集合。集合的成员是唯一的。这就意味着集合中不能出现重复的数据。
常用命令:举个例子:
有序集合 sorted set操作命令
是String类型的元素集合,且不允许重复的成员,每个元素都会关联一个double类型的分数(score),
redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数可以重复
常用命令:举个例子:
通用命令
在JAVA中操作redis
Jedis
- 导入坐标
举个例子:
springdata redis
在springboot项目中,可以使用springdata redis来简化redis操作
- 导入坐标:
- Redis配置
select 编号 切换数据库 默认0
使用框架时自动序列化key,如果想不序列化,需要自己设置配置类
举个例子:
缓存优化
用户数量多,系统的访问量大,频繁的访问数据库,系统的性能下降,用户体验差
环境搭建
- 导入坐标
- 配置文件
- 配置类
上面已经提到
缓存短信验证码
前面实现了验证码登录,随机生成的验证码是保存在session中的。
实现思路:
- 在服务端UserController中注入RedisTemplate对象,用于操作Redis
- 在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
- 在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
redisTemplate.delete(phone);
缓存菜品数据
前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。
实现思路:
- 改造DishController的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据放入Redis。
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
List<DishDto> dishDtoList = null;
//动态构造key
String key = "dish_"+dish.getCategoryId()+"_"+dish.getStatus();
//先从redis中获取缓存数据
dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
if (dishDtoList != null){
//如果存在直接返回,无需查询数据库
return R.success(dishDtoList);
}
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//查询状态为1 起售状态的条件
queryWrapper.eq(Dish::getStatus,1);
//构造排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
dishDtoList = list.stream().map((item)->{
//因为这个对象是new出来的,只设置categoryName属性,其他的属性值为空,所以需要继续拷贝
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//获取的每个菜品的分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
//如果不存在,需要查询数据库,将查询到的菜品数据缓存到Redis
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
return R.success(dishDtoList);
}
- 改造DishController的save和update方法,加入清理缓存的逻辑
//清理所有菜品的缓存数据
// Set keys = redisTemplate.keys("dish_*");
// redisTemplate.delete(keys);
//清理某个分类下的菜品
String key = "dish_" + dishDto.getCategoryId() +"_1";
redisTemplate.delete(key);
注意事项:
在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。
SpringCache
介绍
常用注解
注意:
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
使用方式
在springboot项目中使用springcache操作步骤:
- 导入maven坐标
- 配置application.yml
- 在启动类上加入@EnableCaChing注解,开启缓存注解功能
- 在Controller的方法上加入@Cacheable、@CacheEvict等注解进行操作
举个例子:
缓存套餐数据
前面已经实现了移动端套餐查看功能,对应的服务端方法为SetmealController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长现在需要对此方法进行缓存优化,提高系统的性能。
实现思路:
- 导入Spring Cache和Redis相关maven坐标
- 在application.ym[中配置缓存数据的过期时间
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
- 在SetmealController的list方法上加入@Cacheable注解
- 在SetmealController的save和delete方法上加入CacheEvict注解
读写分离(mysql的主从复制)
mysql主从复制
介绍
MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台MySOL数据库(slave,即从库)从另一台MySL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySOL主从复制是MySOL数据库自带功能,无需借助第三方工具。
mysql复制过程分为三步:
- master将改变记录到二进制日志(binary log)
- slave将master的binary log拷贝到它的中继日志 (relay log)
- slave重做中继日志中的事件,将改变应用到自己的数据库中
图:
前置条件
提前准备好两台服务器,分别安装Mysql并且启动服务成功
配置主库master
第二步:重启mysql服务 systemctl restart mysqld
第三步:登录mysql数据库,执行下面sql第四步:
配置从库Slave
第一步:
第二步:
第三步:第四步:
读写分离案例
背景:面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
图:
sharding-JDBC
介绍:Sharding-JDBC定位为轻量级/ava框架,在Java的]DBC层提供的额外服务。它使用客户端直连数据库,以iar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。使用sharding-JDBC可以在程序中轻松的实现数据库读写分离。
- 适用于任何基于JDBC的ORM框架,如: JPA,Hibernate,Mybatis,SpringJDBCTemplate或直接使用JDBC。
- 支持任何第三方的数据库连接池,如:DBCP,C3P0,BoneCP,Druid,HikariCP等。
- 支持任意实现IDBC规范的数据库。目前支持MySOL,Oracle,SOLServer,PostgresOL以及任何遵循SOL92标准的数据库。
坐标:
使用sharding-JDBC实现读写分离的步骤:
- 导入maven坐标
- 在配置文件中配置读写分离规则
- 在配置文件中配置允许bean定义覆盖配置项
Nginx
介绍
下载安装注意:wget命令不存在的话,首先执行yum install wget 安装
Nginx目录结构
Nginx命令
查看版本
./nginx -v
检查配置文件的正确性
在启动nginx服务之前,可以先检查一下conf/nginx.conf文件的配置是否有错误,命令如下:
./nginx -t
启动和停止
- 启动:
./nginx
- 停止:
./nginx -s stop
- 启动完成后可以查看nginx进程:
ps -ef | grep nginx
重新加载配置文件
当修改配置文件后需要重新加载才能生效
./nginx -s reload
nginx 配置文件结构
Nginx具体应用
部署静态资源
Nginx可以作为静态web服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到Nainx中。将静态资源部署到Nginx非常简单,只需要将文件复制到Nginx安装目录下的html目录中即可
反向代理
正向代理:
是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。正向代理的典型用途是为在防火墙内的局域网客户端提供访问internet的途径正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器
图:
反向代理:
反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。用户不需要知道目标服务器的地址,也无须在用户端作任何设定
图:
- 配置反向代理
负载均衡
早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群进行性能的水平扩展以及避免单点故障出现。
- 应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据
- 负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理
图:
配置负载均衡
负载均衡策略
注意:权重 weight 越大,分发给他的几率越高
iphash:根据ip地址算出hash,分发到固定的服务器
前后端分离开发
接口(API接口):就是一个HTTP请求的地址,主要是去定义:请求路径、请求方式、请求参数、相应数据等内容。
图:
YApi
介绍
YApi是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 AP1,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理。
源码地址: https://github.com/YMFE/yapi
要使用YApi,需要自己进行部署
Swagger
员工管理模块
1.1后台登录模块
1.1.1 登录功能
- 将页面提交的密码进行md5加密处理 ,使用DigestUtils.md5DigestAsHex(password.getBytes())的方法,需要将密码转换为bytes进行加密
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
- 登陆成功后需要将用户的id存入session,后面会用到
request.getSession().setAttribute("employee",emp.getId())
1.1.2 完善登录功能
访问后台页面首先判断是否登录
- 首先设置一个路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER= new AntPathMatcher();
- 过滤器内获取本次请求的uri
//1.获取本次请求的uri
String requestURI = request.getRequestURI();
//定义不需要处理的请求路径 封装为数组
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
- 判断本次请求是否需要处理
boolean check = check(urls, requestURI);
- 放行的条件
//如果不需要处理,则直接放行
if (check){
filterChain.doFilter(request,response);
return;
}
//4.判断登录状态,如果已经登录,则直接放行
if (request.getSession().getAttribute("employee")!=null){
Long empId = (Long)request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request,response);
return;
}
//5.如果没有登录则返回未登录的结果,通过输出流的方式向客户端页面相应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
- check方法 路径匹配 检查本次请求是否需要放行
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match){
return true;
}
}
return false;
}
1.1.3 登出功能
- 清理Session中保存的当前用户的id
request.getSession().removeAttribute("employee");
1.2 分页查询
首先定义分页插件,并且注入到Spring容器中方法的返回值
@Configuration//配置类
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor( new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
代码实现
//Page类是MP提供的
public R<Page> page(int page,int pageSize,String name){
//构造分页构造器
Page pageInfo = new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
//只需要返回pageInfo对象,里面已经封装好数据
return R.success(pageInfo);
}
1.3 添加员工
设置初始密码并且md5加密
1.4 更新员工
根据id进行修改
在前端传递的id使用雪花算法19位,js数据转换格式会有误差,长整型的数字不能够完整显示,最后几位被取整,需要转换为字符串形式。
提供对象转换器JackSonObjectMapper,基于jackson进行java对象到json数据的转换
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
在webMvcConfig配置类中扩展Spring Mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换。
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中 0代表转换器的优先级
converters.add(0,messageConverter);
}
1.5 插入和修改时的公共处理(自动填充)
更新时间和修改人,添加时间和添加人,每次都要手写,很麻烦,提供了一个@TableFiled注解,在实体类上,在插入或者更新的时候未指定的字段赋值(被@TableFiled修饰的字段)
- @TableFiled
@TableField(fill = FieldFill.INSERT) //插入时填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)//插入更新时填充
private LocalDateTime updateTime;
- 自动填充的实现步骤:
- 在实体类的属性上加入@TableField注解,指定自动填充的策略
- 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
- 数据对象处理器中不能获取session,就不能获取登录用户的id,也就无法完成某些操作。这是提供了ThreadLocal类。
什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,
而不会影响其它线程所对应的副本ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,
只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
public void set(T value)设置当前线程的线程局部变量的值
public T get()返回当前线程所对应的线程局部变量的值
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,
并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值 (用户id),
然后在MyMetaobjectHandler的updateFill方法中调用ThreadLocal的get方法
来获得当前线程所对应的线程局部变量的值 (用户id)。
实例:
/**
* 基于ThreadLocal封装的工具类,用于保存和获取当前登录用户的id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入操作自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
/**
* 更新操作自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
1.6 全局异常处理
//选择获取那些注解
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if (ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg=split[2]+"已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
分类管理模块
2.1 新增分类
没有值得注意的点
- 后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。在后台系统中添加菜品时需要选择一个菜品分类,在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
- name字段加了唯一约束。
2.2 删除分类
删除分类值得注意的是,当该分类关联了其他的菜品或者套餐不能删除
解决方法:
在service添加方法
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count = dishService.count(dishLambdaQueryWrapper);
// 查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
if (count>0){
// 已经关联菜品,需要抛出业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
// 查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count1 = setmealService.count(setmealLambdaQueryWrapper);
if (count1>0){
// 抛出异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
// 正常删除
super.removeById(id);
}
其中如果关联了其他的菜品或者套餐会抛出自定义的异常
2.2.1 自定义的异常处理
- 自定义业务异常
public class CustomException extends RuntimeException{
/**
* 把提示信息传进去
* @param message
*/
public CustomException(String message){
super(message);
}
}
- 在全局异常处理中返回异常信息
/**
* 全局异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
2.3 修改分类
没有值得注意的
自动填充
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
文件的上传和下载
在上传图片时用到文件的上传,上传成功后会回显到页面,用的是文件的下载
3.1 上传
知识点
- 用MultipartFile类来接收参数,并且_参数的名字必须和表的的name保持一致_
- 获得原始的文件名:file.getOriginalFilename();
- _截取原始文件名的后缀 _originalFilename.substring(originalFilename.lastIndexOf(“.”));
- _使用UUID重新生成文件名,防止文件名称重复造成文件覆盖 _String fileName = UUID.randomUUID().toString() + suffix;
- 创建文件:File dir = new File(basePath); dir.mkdirs();
- file.transferTo(new File(basePath + fileName)); 移动文件并且给路径和文件名
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
//参数的名字必须和标的的name保持一致
public R<String> upload(MultipartFile file){
// file是一个临时文件。需要转存,否则本次请求结束后临时文件会删除
log.info(file.toString());
//获得原始的文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if (!dir.exists()) {
//目录不存在,需要创建
dir.mkdirs();
}
try {
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
return R.success(fileName);
}
3.2 下载
- 输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));
- 输出流,通过输出流将文件写会浏览器
ServletOutputStream outputStream = response.getOutputStream();
- 输入流读取 ,读取后放到bytes中
fileInputStream.read(bytes)
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));
//输出流,通过输出流将文件写会浏览器
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg");
byte [] bytes = new byte[1024];
int len = 0;
//输入流读取 ,读取后放到bytes中
while ((len=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
菜品管理
新增菜品
点击新增菜品,首先会查询菜品分类 到下拉列表中 然后提交时 携带菜品表的数据和口味表的数据。
这时无论是菜品的实体类还是口味的实体类都不能接收前端发来的数据
这时需要用到dto类。继承了菜品类,里面含有口味类的集合 可以封装数据
DTO,全称为Data TransferObject,即数据传输对象,一般用于展示层与服务层之间的数据传输。
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
- 新增菜品,同时保存对应的口味数据
- 保存菜品的基本信息到菜品表dish
- 保存口味数据到菜品口味表dish_flavor
- 菜品的口味是个集合,都需要获取菜品的id
/**
* 新增菜品,同时保存对应的口味数据
* @param dishDto
*/
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto ) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item)->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
菜品信息分页查询
查询的菜品分类名称菜品表存的为菜品分类的id,并没有菜品分类的名称,这时候需要再菜品分类表里面查,这时候就需要用到Dto类。
//封装了categoryName为菜品分类名称
private String categoryName;
- 分页构造器需要构造一个dishDto类型,然后吧dish里面的数据拷贝到里面,使用BeanUtils.copyProperties的方法。
// 对象拷贝
// records属性是page分页查询里面的数据
// 第一个参数是要赋值的对象,第二个是被赋值的对象,第三个是不需要拷贝的属性
//dish里面的records里面已经有值,dto继承了该类也有值,只需要拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
通过流对象的map,将每个元素拿出来
List<DishDto> list = records.stream().map((item)->{
//因为这个对象是new出来的,只设置categoryName属性,其他的属性值为空,所以需要继续拷贝
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//获取的每个菜品的分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
完整的代码:
/**
* 菜品信息的分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//构造分页构造器
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();
//构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name!=null,Dish::getName,name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
//分页查询
dishService.page(pageInfo,queryWrapper);
// 对象拷贝
// records属性是page分页查询里面的数据
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
//通过流对象的map,将每个元素拿出来
List<DishDto> list = records.stream().map((item)->{
//因为这个对象是new出来的,只设置categoryName属性,其他的属性值为空,所以需要继续拷贝
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//获取的每个菜品的分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
修改菜品
- 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
- 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
- 页面发送请求,请求服务端进行图片下载,用于页图片回显
- 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以ison形式提交到服务端
需要注意的是 根据id查询菜品信息时,还需查询口味表的口味信息。保存同理
- 更新时操作两张表,更新菜品的基本信息和口味表
- 在操作口味表时,不容易修改操作,可以先删除原有的数据,然后重新插入
- 插入口味表时要获取菜品的id存入数据库
service:
@Transactional
@Override
public void updateWithFlavor(DishDto dishDto) {
//更新菜品表基本信息
this.updateById(dishDto);
//清理当前菜品对应的口味数据,dish_flavor的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//添加当前提交过来的口味数据,dish_flavor的insert操作
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item)->{
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
知识总结
- 拷贝的工具类:BeanUtils.copyProperties
- stream流的方法:stream().map遍历取值
套餐管理
新增套餐
在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal dish表插入套餐和菜品关联数据所以在新增套餐时,涉及到两个表:
开发流程
- 页面发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
这个在前面的list分类查询中已经实现过
- 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
前面的方法已经实现
- 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
根据对应的分类id查询菜品
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,将套餐相关数据以ison形式提交到服务端
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Transactional
@Override
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的进本信息,操作setmeal,执行insert操作
this.save(setmealDto);
//获取菜品信息集合
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
//里面的套餐id没有值,需要进行赋值
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
套餐信息分页查询
页面(backend/page/combp/list.html)发送ajax请求,将分页查询参数(page、pageSize.name)提交到服务端,获取分页数据
页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发流程
- 逻辑思路:
套餐需要展示套餐的名称,需要用到dto类,然后查询到setmeal的分页数据后,拷贝到dto的类中,
根据CategoryId查询分类信息,把categoryName查询出来赋值,最后封装为集合,赋值给dto返回。
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//构建分页构造器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>();
//
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件
queryWrapper.like(name!=null,Setmeal::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//查询分类
Category category = categoryService.getById(categoryId);
if (category!=null){
//分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
删除套餐
可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
开发流程
- 查询套餐的状态,确认是否可以删除
- 如果不能删除,抛出异常
- 如果可以删除,先删除套餐表中的数据 setmeal表中的数据
- 删除关系表中的数据 setmeal_dish
代码实现:
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@Transactional
@Override
public void removeWithDish(List<Long> ids) {
//查询套餐的状态,确认是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
//如果不能删除,抛出异常
int count = this.count(queryWrapper);
if (count>0){
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据 setmeal表中的数据
this.removeByIds(ids);
//删除关系表中的数据 setmeal_dish
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(lambdaQueryWrapper);
}
知识总结
- 同时操作两张表时,需要加入@transactional注解。
手机验证码登录
知识梳理
- 在登录页面(front/page/login.html)输入手机号,点击[获取验证码] 按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信
- 在登录页面输入验证码,点击[登录]按钮,发送ajax请求,在服务端处理登录请求
因为需要调用阿里的API就不过解释,有需要可以查看阿里短信服务的官方文档
手机端菜品展示
需求分析
用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示“选择规范”按钮,否则显示“+”按钮
开发流程
- 页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
此功能在之前添加菜品时查询的list方法,显示到下拉框中已经实现
- 页面发送aiax请求,获取第一个分类下的菜品或者套餐
之前根据菜品的种类查询菜品,显示到右侧的导航栏中已经实现过方法,但是需要根据菜品有无口味表的关联数据,来显示“选择规范”按钮,还是显示“+”按钮,需要改造之前的查询菜品的list方法
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//查询状态为1 起售状态的条件
queryWrapper.eq(Dish::getStatus,1);
//构造排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item)->{
//因为这个对象是new出来的,只设置categoryName属性,其他的属性值为空,所以需要继续拷贝
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//获取的每个菜品的分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
购物车功能
需求分析
移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击“+”按钮将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量也可以清空购物车。
开发流程
添加购物车
- 点击加入购物车或者+ 按,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
- 设置用户id,指定当前是那个用户的购物车数据
- 查询当前菜品或套餐是否在购物车中
- 如果已经存在,在原来基础数量上加一
- 如果不存在,添加到购物车,默认数量为1
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}",shoppingCart);
//设置用户id,指定当前是那个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
//查询当前菜品或套餐是否在购物车中
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
if (dishId != null){
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}else {
//添加到购物车的为套餐
queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
ShoppingCart cartServiceOne = shoppingCardService.getOne(queryWrapper);
if (cartServiceOne != null){
//如果已经存在,在原来基础数量上加一
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number + 1);
shoppingCardService.updateById(cartServiceOne);
}else {
//如果不存在,添加到购物车,默认数量为1
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCardService.save(shoppingCart);
cartServiceOne = shoppingCart;
}
return R.success(cartServiceOne);
}
查询购物车
- 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
根据用户的id查询购物车数据表
删除购物车
- 点击清空购物车按钮,页面发送aiax请求,请求服务端来执行清空购物车操作
根据用户id删除
用户下单
需求分析
移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮则完成下单操作
开发流程
- 在购物车中点击按钮,页面跳转到订单确认页面
查询订单信息和用户地址信息,前面的方法已经实现过
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
前面方法实现过
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
前面,实现
- 在订单确认页面点击去支付按钮,发送aiax请求,请求服务端完成下单操作
- //获得当前用户id
- 查询当前用户的购物车数据
- 查询用户数据
- 查询地址数据
- 向订单表插入数据,一条数据
- 向订单明细表插入数据,多条数据
- 最后要清空购物车的数据
/**
* 用户下单
* @param orders
*/
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long userId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCarts = shoppingCardService.list(wrapper);
if(shoppingCarts == null || shoppingCarts.size() == 0){
throw new CustomException("购物车为空,不能下单");
}
//查询用户数据
User user = userService.getById(userId);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook == null){
throw new CustomException("用户地址信息有误,不能下单");
}
long orderId = IdWorker.getId();//订单号
AtomicInteger amount = new AtomicInteger(0);
List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserId(userId);
orders.setNumber(String.valueOf(orderId));
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
//向订单表插入数据,一条数据
this.save(orders);
//向订单明细表插入数据,多条数据
orderDetailService.saveBatch(orderDetails);
//清空购物车数据
shoppingCardService.remove(wrapper);
}
更多推荐
所有评论(0)