瑞吉外卖项目优化以及问题总结

306 阅读50分钟

优化项目所做的准备工作

都做完了,那么下一步当然是优化我们的项目了,那么在优化之前,我们当然需要做一些对应的准备工作,否则我们都优化不了,我们首先要在我们的Linux服务器上安装JDK、Tomcat、Nginx、mysql等内容,其中有一些非常基础的内容我们之前已经做过了,比如连接ssh以及安装mysql等,我们这里就不再提了

Linux服务器上JDK的安装

这里我们安装的方式是二进制的上传安装,这里获取安装包路径我们部分参考这个文章blog.csdn.net/hh__chen/ar…,安装包的下载地址是www.oracle.com/java/techno…,我们这里选择的是jdk-8u341-linux-x64.tar.gz的版本,其下载链接是www.oracle.com/java/techno…

下载完毕之后我们利用Xshell直接拖动安装包上传到根目录,然后我们根据下面的命令进行操作,当然,要注意的是我们这里使用的Xshell,同时我们的jdk版本也和他不一样,我们这里看个操作模板而已

首先我们解压安装包到对应路径,然后我们使用vim命令修改对应的配置文件,这里各个不同的教程要我们写的配置都不尽相同,反正我们是这样写的

然后我们出去重新加载profile文件,发现卵用没有,我们输入java -version会提示没找到命令

这时我们根据弹幕提供的方法输入yum install java-devel命令,等待其安装之后我们再次调用java -version命令就会发现能够正确看到对应的版本提示了,此时说明我们已经安装成功了

关于服务器快照以及在Linux上部署项目的知识

为了防止自己出错,提高效率,我们建议每次安装成功完一个内容之后我们就进行一次快照,这样我们可以及时回滚我们的内容,而且就算中间我们把我们的系统给搞坏了,我们也可以利用回滚回到最初的状态继续乱搞,很爽说实话

最后是关于在Linux上部署项目的知识,我们可以将idea的项目打包成jar包上传到我们的Linux上的目录即可,然后我们调用java -jar [项目名]即可在对应的窗口中运行我们的项目(当然,JDK必须先安装好,否则会寄),当然,这里也会出现端口的问题,如果要调整端口可以一开始就在我们的对应的项目上去调整,比如我这里将端口调整为80就避免了端口冲突

最后提一下我们的mysql的密码似乎是itheima

然后我们再另一个连接上可以访问对应的网址,其会返回对应的页面信息,我们的日志也会成功打印,但是我们在windows中是无法访问到其网址的,只能在Linux的连接上访问

Linux上Tomcat的安装

我们这里的安装过程部分参照这篇文章blog.csdn.net/m0_67391907…,首先我们调用这个命令直接在Linux服务器上下载我们的安装包wget archive.apache.org/dist/tomcat…

然后我们按照下面的步骤,首先将我们的安装包解压到/usr/local文件夹中

然后我们进入该文件夹的tomcat文件夹的bin目录中,调用sh startup.sh命令,可以令我们的tomcat服务启动,其下会显示一个Tomcat started的提示句,看到该句则说明我们的Tomcat服务已经启动成功了

然后如果我们想要在windows上可以访问到我们的tomcat的提示成功的网址的话,那么我们要关闭我们的防火墙,不过我们的防火墙已经关闭了,所以我们可以直接访问

在网址中输入http://175.178.114.158:8080/,即可访问到我们的Tomcat网址,前面的内容是我们的云服务器的远程连接的网址,后面的内容则是我们的端口号,现在我们就可以将我们的云服务器挂着,然后在我们的windows上随时访问该网址了

当然我们直接关闭我们的防火墙是不安全的,我们可以使用对应的操作让我们的防火墙开启,同时我们开放部分可以供给外部连接的端口即可(我这里就嫌麻烦,我直接关闭防火墙)

Linux服务器安装lrzsz

其实我们这个玩意早就安装完了,安装了之后效果就是输入rz可以进入文件传入页面

通过Shell脚本自动部署项目

手动部署项目就是打jar包然后上传到Linux中调用对应命令启动即可,我们之前已经做过了,这里就不再提了

首先我们来看看我们的自动部署项目的过程

首先我们来安装git,使用这两个命令

然后我们使用git克隆代码,调用git clone [远程仓库地址]命令即可克隆

然后我们来安装Maven,这里我们部分内容参考这个文章blog.csdn.net/weixin_5827…,我们下载安装包的地址是dlcdn.apache.org/maven/maven…,其版本为3.8.6

安装好之后我们首先进行解压到我们的指定位置,然后修改我们的配置文件,总之我们的配置文件最后修改的样子是这样的

最后我们重新加载该文件之后调用mvn -version命令,如果可以看到maven对应的版本信息则说明我们已经安装成功了

最后我们在/usr/local下创建一个repo文件夹,然后进入安装包中,打开setting.xml,加入对应的图上的一行内容,设置我们的maven的本地仓库即可

然后我们要通过一个脚本文件来帮助我们执行一系列命令,通过这一系列命令可以实现自动部署我们的项目,当然,不同的项目应该也是对应着不同的脚本的,我们这里就不演示这个脚本了

同时我们在企业开发时一般是不是root用户的,因此要令别人给我们赋予一定的权限,这里又涉及到了提供权限的知识,我们之前也学过了,就不再提了

Linux服务器安装Redis

接着就到了重量级内容了,之前我们就安装学习过这勾八玩意,结果每次都容易被人攻击,这次我们可千万别忘了要设置我们的密码,而且我们一定要设置好我们的密码,免得我们搞好了就被人侵入了

首先我们要完成Redis的下载,我们这里的下载地址是download.redis.io/releases/,我们进入里面可以选择我们想要的版本,我们这里使用的redis版本是4.0.0,当然,这是Linux的

然后我们对其进行解压,接着我们安装Redis的依赖环境gcc,调用对应的命令等待安装即可

然后我们进入对应的目录执行mke命令,再进行redis的src目录执行make install命令即可完成安装

Linux中Redis服务的启动是./redis-server命令(要保证在src目录下,否则会失效)

连接Redis服务的命令是./redis-cli,使用另外一个窗口进行连接即可,使用Ctrl+c即可退出对应服务

然而我们现在还有一个问题,那就是如果我们的redis不设置密码的话,那么用不来多久我们就会受到腾讯云的违规警告了,设置密码我们参考这个文章blog.csdn.net/m0_53078233…,简单来说我们编辑redis.conf文件,首先输入/pass代表搜索含有pass关键字的行,然后到对应行处找个空位输入requirepass [要设置的密码],然后保存该文件即可设置密码

然后我们启动和连接的方法还是一模一样的,但是不同的是,我们连接之后必须调用auth [密码],命令,输入正确的密码才可以使用redis服务,否则只是进去了,逼用没有

现在我们还存在的一个问题就是,我们的redis目前只能让我们的本机连接到,而不能够让其他机子连接到,这是因为我们的配置文件里规定了只有我们本机的ip地址的访问才允许通过,所以我们要进行一些改动,首先我们要将我们的redis.conf中的bind 127.0.0.1注释掉

然后我们参照这篇文章blog.csdn.net/zhizhenggua…,连接进redis中并输入config set protected-mode no命令即可

最后我们还有一个问题,那就是我们的密码虽然已经在redis.conf的配置文件中设置了,但是却没有效果,为了解决这个问题,我们参照这个文章www.cnblogs.com/simyeo/p/13…,进入redis中并输入config set requirepass "密码",并输入我们的密码feigeA.5200....,之后我们的密码就正确生效了

之后还有一个学习用SpringDataRedis在idea中连接Redis的知识,我们把它放在了我们的Redis文件夹中了,有兴趣的就自己去看吧

Reids缓存优化项目

那么到现在,我们就正式到优化我们的项目的章节了,我们之前虽然实现了我们的项目,但是我们的项目还存在很多问题,其中最明显的问题就是我们的项目所需要的数据总是往我们的数据库中查找,这样我们的用户数量一旦多起来,系统的访问量一大,我们的系统性能就会下降,最直观的感受就是要等好几秒才能加载数据

所以我们要对我们的项目进行改造,我们可以用缓存来改造的项目,这样我们的数据都先存放到缓存中,当用户请求服务器时直接返回缓存的数据即可,这样就不需要我们频繁去请求我们的数据库了,我们这里就使用Redis来进行实现我们的缓存功能

缓存短信验证码数据

那么在正式实现之前,我们需要对我们的项目本身做一些环境上的构建,首先我们要在pom文件下引入下面的坐标

然后为了防止我们的存入的缓存数据是乱码的情况,我们需要写入如下配置类代码

package com.itheima.reggie.config;
​
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
​
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
​
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
​
        RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
​
        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
​
        redisTemplate.setConnectionFactory(connectionFactory);
​
        return redisTemplate;
    }
}

首先我们来看我们的实现思路

在正式实现之前,为了便于我们的查询,我们要整一个用于便捷查询我们的Redis内的数据的软件来,这里我们参考这篇文章blog.csdn.net/asdfadafd/a…,下载了一个比较不错的软件并且连接上了我们的Redis,以后我们就使用这个来查看我们的Redis数据

首先我们在对应的UserController中注入进行Redis操作的属性对象

@Autowired
private RedisTemplate redisTemplate;

然后我们注释其中的将验证码保存到Session中的代码,转而设置为将生成的验证码缓存到我们的Redis中,并且设置时间为5分钟

//将生成的验证码缓存到Redis中,并且设置有效期为5分钟
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);

然后我们在登录的方法中,我们将原来的从共享域中获取对象改为从Redis中获取对象然后进行比对

//从Redis中获取缓存的验证码
Object codeInSession = redisTemplate.opsForValue().get(phone);

最后如果我们登录成功,我们则删除缓存中的验证码数据

//如果用户登录成功,删除Redis中缓存的验证码
redisTemplate.delete(phone);

那么到此为止,我们缓存短信验证码的工作就做完了

缓存菜品数据

接着我们来学习如何缓存我们的菜品数据,先来看看我们的实现思路

那么我们就可以修改我们的用户端展示菜品的方法,首先我们先创建所需要的对象,然后我们的目的是让我们的请求在查询数据库之前先查询我们的缓存,如果缓存有数据,我们就使用缓存的数据,如果没有,我们再请求服务器的数据,同时将服务器的数据设置到我们的缓存中,那么我们缓存中的数据肯定需要一个key,那么我们的key要如何构造呢?我们这里采取菜品字符串拼接其分类id及其状态的方法来拼接成我们的key,这样的key不但含有关键信息,重要的是使用者一看就懂这是个什么几把玩意,就很不错

所以我们这里的逻辑是先动态构造我们的key,然后我们通过这个key从redis中获取缓存数据,判断其能否获取到,若获取到则直接返回,否则就从数据库中查找数据,然后将数据设置到我们的Redis中,设置缓存的时间为一小时

/**
 * 根据条件查询菜品数据(缓存优化)
 * @param dish
 * @return
 */
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
​
    List<DishDto> dishDtoList = null;
​
    //动态构造Redis中的key,dish_1397844391040167938_1
    String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();
​
    //先从redis中获取缓存数据
    dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
​
    if(dishDtoList != null){
        //如果存在,直接返回,无需查询数据库
        return R.success(dishDtoList);
    }
​
    //需要查询数据库缓存到Redis
    
    //查找数据库中数据的代码,此处省略
    
    //如果不存在,需要查询数据库缓存到Redis
    redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
    
    return R.success(dishDtoList);

接着我们要明确,如果我们对我们的菜品做增删改,那么我们就要清除我们的缓存数据,清除缓存数据的方法有两种,一种是清除所有缓存数据,另一种是精确清除对应分类的缓存数据,我们当然选择第二种,第一种了解下得了

我们先来看看我们的清除所有缓存数据的方法,这就是利用*号拼接字符串无脑清除所有就完了,没啥好说的说实话

第二种方法则是对应拼接上具体的分类id,从而来清楚对应的分类缓存数据,也很好理解,也是我们所推荐的方法

        //清理所有菜品的缓存数据
//        Set keys = redisTemplate.keys("dish_*");
//        redisTemplate.delete(keys);
​
        //清理某个分类下面的菜品数据
        String key = "dish_" + dishDto.getCategoryId() + "_1";
        redisTemplate.delete(key);

然后我们对我们的保存方法也放上同样的代码,当然是要放到我们的保存方法完成之后

对于修改或者是批量修改菜品状态的方法,我们要注意的是,我们这里传入的id是菜品id,而我们存入缓存的数据是分类id拼接的字符串,因此我们这里要先查找出对应的菜品对象,获得其分类id然后再进行缓存数据的删除,由于是批量删除,因此我们这里要用stream流的形式来删

ids.stream().map((item) ->{
    DishDto dishDto = dishService.getByIdWithFlavor(item);
    //清理某个分类下面的菜品数据
    String key = "dish_" + dishDto.getCategoryId() + "_1";
    redisTemplate.delete(key);
    return item;
}).collect(Collectors.toList());

而删除的方法就比较麻烦了,首先我们要知道我们删除的方法也是传入的菜品id,但是我们这里不可能先把菜品删除了之后再去查找对应的菜品,因此我们这里的方法是改造我们的服务层的方法,令其返回菜品对象的集合

//删除菜品信息
public List<DishDto> deleteWithFlavor(List<Long> ids);

然后我们在其下做的处理是在stream流中先获得菜品对象,然后将对应数据拷贝到新对象中,接着返回新对象集合即可

/**
 * 删除或批量删除菜品
 * @param list
 */
@Override
@Transactional
public List<DishDto> deleteWithFlavor(List<Long> list) {
​
    List<DishDto> dishDtoList= list.stream().map((ids) -> {
        DishDto dishDto = new DishDto();
​
        Dish byId = this.getById(ids);
​
        this.removeById(ids);
​
        BeanUtils.copyProperties(byId,dishDto);
​
        //清理当前菜品对应口味数据---dish_flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,ids);
​
        dishFlavorService.remove(queryWrapper);
​
        return dishDto;
    }).collect(Collectors.toList());
​
    return dishDtoList;
}

最后我们在控制层中的所做的事就是利用传过来的集合对象来删除对应缓存数据即可

List<DishDto> list = dishService.deleteWithFlavor(ids);
​
list.stream().map((item) -> {
    //清理某个分类下面的菜品数据
    String key = "dish_" + item.getCategoryId() + "_1";
    redisTemplate.delete(key);
    return item;
}).collect(Collectors.toList());

当然,实际上我们直接返回一个分类id的集合也可以完成我们的功能,但是我们还是返回一个对象比较好,因为指不定以后我们用得上是吧

最后我们提一下关于git的问题,我们的git的使用一般是我们创建一个分支,然后我们在分支上进行开发,开发完成之后我们查看分支功能是否没有问题?没问题我们就直接合并到主支master上,合并的方法就是点击右下角,从分支切换到master分支,然后我们能看到我们的代码就变成没开发时候的样子了,接着我们点击分支,到1.0,然后在点击后面出现的merge即可

Spring Cache

接着我们来学习SpringCache,利用这个框架我们可以简化我们的缓存代码,首先我们来看看Spring Cache的介绍

针对不同的缓存技术要实现不同的CacheManager,不过我们这里是使用Redis作为缓存技术的,因为我们现在就是在学这玩意不是,然后我们再来看看SpringCache给我们的提供的常用注解

然后我们来学习下SpringCache下的各种注解的使用规则,首先是CachePut注解,其作用是将方法的返回值放入缓存,其下我们需要设置value属性,该属性的作用是设置缓存的名称,而key则是我们要填入的内容,key属性里支持SpEL表达式子,我们可以结合这个表达式来往我们的缓存中存入返回对象内的各种属性,比如名字或者是id,比如下图中就通过EL表达式的方式来动态获得了我们的存入数据的id

接着我们来看看CacheEvict注解,该注解用于删除指定缓存

value的值需要指定redis中存入数据的名称,而key则是用于指定我们需要具体删除其下的什么数据,同样支持SpEL表达式

我们这里user代表直接拿到user对象,p0表示拿到第一个数据对象,我们也可以使用p1来拿到第二个对象,result表示拿到返回对象,使用root.args[0]也是表示拿到第一个数据对象,我们同样可以使用root.args[1]来拿到第二个对象

虽然说方法多种多样而且都是可行的,但是一般我们都推荐使用最直观的,也就是user的那一种形式来完成目标

接着我们来看看Cacheable注解,该注解的需要设置value和key,value指的是数据存放的名字,而key是要存放的具体数据,使用这个注解时,每次执行方法时都会先从缓存中查找,若查找到,就直接返回缓存数据,若找不到则将数据设置于缓存数据中,同时如果我们查出的数据为null,其也会将对应的数据插入,只不过值为null而已,为了避免这种情况,我们可以调用其下的condition属性,来指定只有当什么情况下我们才将缓存存入,比如我们这里指定只有当返回值不为空时才将结果存储

这里值得一提的是,我们也还有当满足什么结果时不存入的注解属性可供使用,同时key属性内要求填入的数据是字符串类型的数据,其支持字符串拼接的操作

最后我们要学习的就是在Springboot项目中使用SpringCache并且要使用Redis来作为缓存,直接看下图的步骤吧

接着我们来看看我们的具体步骤

首先我们要在对应的maven文件中导入下面的坐标,否则我们无法使用

然后我们要在yml文件中设置具体的配置

值得一提的是,弹幕上说还要加上下面这一句的配置,否则会一直报找不到的对应名字的错误,我也不知道是不是,反正我一开始就加了

cache:
  type: redis

然后是如果我们的缓存时间都设置为一样的话,那么到时候缓存一起过期是会引发缓存雪崩的

缓存雪崩

但是我们目前主要的目标不是学习这个,反正我知道会引发这么个玩意就行了,后面我们涉及到了我们会专门去学习如何解决这个问题的

然后我们来用注解来实现下我们的缓存,由于我们的套餐返回的R的对象,因此我们要先将R实现序列化接口,否则R压根就没法序列化到时候存入到Redis中是会报错的

然后我们的逻辑很简单,首先是当查询套餐数据时,如果缓存中有套餐数据我们就返回缓存的套餐数据,如果没有就从数据库中查找数据并且将数据设置到我们的Redis中,那么我们只需要在我们的显示套餐的方法中加入这行代码即可

@Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")

本行代码的意思是我们查询的数据的名字是setmealCache,其key是我们拼接的名字,内部运用了SpEL表达式,注意整个字符串都是由大括号括住,如果要使用EL表达式直接用#结合对象的方式即可,如果要加入自定义的字符串就用''来加入,同时需要引入+号,最后我们会在这个key内部存入我们的对象的所有内容,并且是序列化的形式

接着我们要实现的逻辑是,如果我们对套餐数据进行了修改或者是新增,我们都要清除我们的缓存数据,那么我们就要在新增和修改方法上加入这行注解,可以看到我们这行注解的意思很简单,就是先定位setmealCache的文件夹,然后摄氏allEntries属性为true,意为删除其下所有数据,包括其自身,我们就可以实现我们的删除所有数据的逻辑了

@CacheEvict(value = "setmealCache",allEntries = true)

本来嘛,其实我们还应该用这个注解去实现我们的套餐的其他逻辑的,但是我觉得学太多这一类的内容要是到时候派不上那就亏了,我们现在还是先放着吧,以后我们真的有需要的时候再来实现也不迟

主从复制与读写分离

那么现在我们的项目还存在什么问题呢?我们的项目存在的一个重点问题在于

那么我们要如何解决这个问题么?我们可以对我们的项目进行读写分离,也就是说,我们可以整两个数据库来,一个是主库,一个是从库,读的操作全部给从库做,而写的操作全部给主库

同时我们的主库和从库的数据要保持一致,为达成这个目的我们可以使用mysql提供的主从复制功能,让我们的主库与从库的数据总是会同步保持一致

同时由于从库复制着主库的数据,相当于是给主库数据做了备份,这样也能增加我们数据的安全性

我们首先来实现我们的主从复制,主从复制是mysql提供的功能,我们不需要借助第三方工具,其实现的原理是我们的对mysql执行的任何操作都会存在记录日志,而其令主从数据同步的方式是将主库的日志传给从库,然后从库执行完全相同的语句,然后就能实现主从库的数据完全一样了

这里我们值得一提的是,虽然我们这里只有一个从库,但实际上我们的从库可以不止只有一个,别因为我们这里只整了一个从库就产生从库只有一个的误解了

接着我们就可以正式来实现我们的主从复制了,先准备两台服务器并分别安装mysql服务并启动成功

但是很不幸的是,我们没有两个服务器,因为买不起,而且我也觉得没啥必要,所以这一章节的内容我们就以了解为主,实际的项目中我们就不实现了

主从复制

首先我们先来配置我们的主库,我们需要进入到我们的当前数据库的配置文件中添加如下两个代码

第二步是重启我们的mysql服务,其命令是systemctl restart mysqld

然后我们要登录我的Mysql数据库执行下图中的sql,这个sql的意义在于创建一个用户,名字是xiaoming,当然,这个名字我们可以自己换。然后给xiaoming这个用户授予建立复制时所需要用到的用户权限,并且给其设置密码,当然,这个密码我们也可以根据自己的喜好随便换

最后我们来配置我们的主库,执行show master status命令,这里的文件名就是我们的日志文件名,Position则是我们的命令的条数,如果我们此时再执行其他命令的话,命令条数的内容就会被修改了,而我们后面是需要用到精确的命令数的,所以我们到此为止就不能再执行任何操作了

接着我们来配置从库,从库也是首先要修改其对应的配置文件

然后要重启其服务

接着我们登录器数据库,往令其执行下面的命令,这里是设置的主库,先填入主库的ip地址,然后填入要绑定主库的用户名和密码,然后在输入主库的日志文件名和具体条数,这样我们就可以正确执行主从复制了

最后异步是要登录Mysql的数据库,执行show slave status来查看从库的状态,直接在Linux上看太乱了,我们可以将其复制到文本上看,我直接看Slave_IO_Running和Slave_SQL_Running这两个状态即可,若都为yes,那一般也就没什么问题了

那么到此为止,我们的主从复制就搞定了,此时我们用navicat连接两个数据库,往主库里添加数据之后,我们往从库里一刷新也能够看到从库也更新的主库的数据

读写分离

接着我们来实现读写分离,先来看看为什么我们要实现读写分离

那么要实现读写分析,我们面临的一个重大问题就是,我们怎么判断我们的请求是读还是写然后将让他们去请求对应的数据库?这里就要使用到我们的Sharding-JDBC框架了,先来看看其介绍

我们来看看其实现读写分离的步骤

首先我们要导入其对应的maven坐标

然后我们来配置我们的Sharding-JDBC,我们整这个框架要搞的事情只有配置而已,配置搞好了剩下的工作他会自动帮我们完成。另外值得一提的是我们下面的配置并不是我们实际上的配置,这是我们课程中的配置,由于实际上我们并没有使用这项技术,因此下面的格式只是做一个参考而已

server:
  port: 8080
spring:
  application:
    #应用的名称,可选
    name: reggie_take_out
  shardingsphere:
    datasource:
      names:
        master,slave
      # 主数据源
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.138.100:3306/reggie?characterEncoding=utf-8
        username: root
        password: root
      # 从数据源
      slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.138.101:3306/reggie?characterEncoding=utf-8
        username: root
        password: root
    masterslave:
      # 读写分离配置
      load-balance-algorithm-type: round_robin #轮询
      # 最终的数据源名称
      name: dataSource
      # 主库数据源名称
      master-data-source-name: master
      # 从库数据源名称列表,多个逗号分隔
      slave-data-source-names: slave
    props:
      sql:
        show: true #开启SQL显示,默认false
  main:
    allow-bean-definition-overriding: true
  redis:
    host: 172.17.2.94
    port: 6379
    password: root@123456
    database: 0
  cache:
    redis:
      time-to-live: 1800000 #设置缓存数据的过期时间
mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID
reggie:
  path: D:\img\

首先我们在spring下配置我们的shardingsphere,我们这里首先要配置我们的数据源的名字,这里的名字可以随意指定,指定的不同的名字就代表不同的数据库,但是要注意的是,我们下面的具体配置的数据源的名字必须要和我们上面的设置的名字一样

首先我们要配置的是主数据源,其标签是master,其下的内容就是我们配置数据源的内容罢了,这个不多提

然后我们要配置的就是从数据源,也大差不差,同样不多提

接着我们利用masterslave标签来配置我们的全局配置,首先配置的是我们的负载均衡,我们这里设置的是轮询。所谓负载均衡其实指的就是当我们的请求发送过来时,我们选择由哪个数据库来执行查询的请求,由于往往我们的从库都不只是一个的,我们这里设置的轮询,其实就是按照顺序一个个发,没了

接着我们要设置的我们的最终的数据源的名称,因为我们的这个框架的数据源其实是将两个数据源封装成一个数据源对象,而这个对象当然需要名字,我们这里设置其名字就为dataSource。然后我们要我们要设置我们的主数据源的名称和从库数据源的名称,我们就将对应的主库从库的名字放上去就完了

最后我们还要设置的一个是mian标签下的allow-bean-definition-overriding为true,这个意为开启bean的自动覆盖,因为在们的这个项目中,Spring自己还会创建一个数据源对象,而我们这里我们又会创建一个数据源对象,如果我们不开启自动覆盖,那么就会导致出现两个同名的数据源对象,而这是Spring项目所不允许的,此时我们的Spring会直接报错停止

那么设置完毕之后,我们的东西就搞定了,没啥特别的问题,此时我们发送请求时可以在控制台上看到其会自动定位到我们的具体数据库并发送对应的请求

Nginx部署项目

接着我们来学习Nginx,这个内容虽然我们之前学过了,但是我们不妨再学习一遍,我们先来看看Nginx的介绍

Nginx介绍

然后我们来看看其下载和安装的过程

安装和完成之后,我们的一个问题就是Nginx有什么用呢?我们的Nginx主要有三个作用,一是部署静态资源,二是反向代理,三是负载均衡,我们接下来我们一一讲解,不过在此之前,我们先来学习下Nginx的目录结构

Nginx目录结构

第一个conf中最重点的内容在于nginx.conf,logs文件夹内部需要我们先运行了nginx之后才会有文件,其他请看图

这里我们值得一提的是,我们下面的命令都应该进入到我们的nginx中的sbin文件夹中才可以正确执行,否则其会报没有找到对应的文件

首先是查看版本的命令

然后是检查配置文件正确性的命令,我们启动Nginx服务之前都应该启动该命令

接着我们来启动Nginx,使用./nginx命令即可,但是由于Nginx默认使用的是80端口,而80端口在之前我们启动Tomcat的时候用过了,所以我们这里开启会显示端口被占用的问题

我们解决这个问题参考了这个文章blog.csdn.net/yufeng_lai/…,其中在该文章重启防火墙中又出现了Failed to start firewalld.service: Unit is masked.的报错,为了解决这个问题我们又参考了这篇文章blog.csdn.net/centose/art…,最终我们成功开启了Nginx

然后我们只要直接用我们的ip地址就可以访问到Nginx的首页了(80端口可以省略不写),注意我们要事先将我们的防火墙关闭,否则是无法访问的

logs文件夹内会存放各种日志文件,包括错误的日志文件和成功的日志文件。当我们的nginx启动时,其还会生成一个nginx.pid文件,内部记录了nginx的进程号

当我们修改了Nginx的配置文件后,我们需要用重新加载配置文件的命令才可以使得我们的配置文件生效

nginx.conf结构学习

接着我们来学习Nginx的配置文件nginx.conf的整体结构,其总分为三大块,分别是全局块、events块、http块,其中最重要的就是http块

http块其下又分为两块,分别是http全局块与Server块,其中Server块又分为Server全局块和location块

这里最值得一提的是http块中可以配置多个Server块,每个Server块中可以配置多个location块

更加具体的内容可以看下图,红色框住了三大块,黑色框住了http块的两小块,黄色框住了Server块下的两小块

那么接着我们就要到我们的具体应用了,终于讲到这里了,我们首先来讲解如何部署我们的静态资源,我们部署静态资源要将我们的静态资源放置到我们的html文件夹中,我们这里放入了一个hello文件,然后我们开启nginx服务,接着我们访问这个网址http://175.178.114.158/hello.html,我们会发现其能够正确访问到该网址,但是为什么这样做我们就可以访问到了呢?这就需要到我们的配置文件中去找答案了

我们看下面的内容,我们可以看到这里其默认监视的是80端口,因为这里设置了80不是,默认的地址就是我们本机的ip地址,也就是localhost,然后我们location则是用于处理我们的请求的块,其可以匹配客户端请求的url,而root,也就是根,根指向html文件夹,其用于指定我们的静态资源的根目录,也就是说,所有的我们的对我们的ip地址的请求都会从html该文件夹中去获取,这也是为什么我们可以访问到我们的静态资源

那为什么我们直接访问ip地址的时候仍然有页面呢?这是因为我们下面的index中配置了默认界面,默认界面可以配置多个,中间用空格隔开即可

最后我们来看看总结

反向代理

接着我们来讲nginx的第二个作用,反向代理。再讲反向代理之前,我们先来理解一下正向代理的概念,所谓正向代理,I其意思就是在客户端和原始服务器之间设置一个代理服务器,用户需要请求代理服务器,然后由代理服务器完成对资源的请求和返回,这个代理服务器一般是在客户端上设置的

而反向代理是位于用户和目标服务器之间的,客户端向反向代理服务器请求,然后反向代理服务器就会跟具体要请求的服务器进行交互,然后完成对用户请求的响应。这样单看起来似乎正向代理和反向代理大差不差,但其实他们是有区别的,最简单的区别是,正向代理一般而言用户是知道代理服务器的存在的,而是设置在客户端的,而反向代理则不需要设置在客户端,用户直接访问反向代理服务器即可,用户不需要知道目标服务器的地址,我们也不用需要在用户端做什么特别的设定

最简单粗暴理解可以是,正向代理是向用户服务,而反向代理是为服务器服务,前者关联在在客户端上,后者关联在服务器上

那么我们要如何在Nginx上做反向代理呢?其实很简单,我们只需要直接在对应的配置文件上设置上下面这行代码即可,最重要的标红的代码,这个代码是设置我们的反向代理的服务器的目标地址,设置好了之后我们只要访问反向代理服务器的对应请求地址,我们的反向代理服务器就会接受请求并返回对应的结果,而且在我们的真实的服务器上也会接收到对应的请求

当然,我们不要忘了我们的请求的路径应该是一样的,最多要变化的就是其端口号和ip地址

负载均衡

最后我们来学习负载均衡,这也是Nginx的最后一个内容,关于负载均衡的内容直接看图吧

那么我们要如何实现负载均衡呢?其实很简单,我们到负载均衡的服务器上,然后设置如下的内容,我们这里请求的地址直接变成了网址,而这个网址我们上面有设置,通过upstream我们可以在下面定义一组服务器,令其实现访问多个服务器来处理请求的方法

最后我们的负载均衡同时也有多个策略,具体有啥策略可以自己看

前后端分离开发

现在我们的项目还存在什么问题呢?那就是我们的代码是前端和后端都是放在一起开发的,这种开发存在许许多多的问题,具体请看图

那么我们要如何改善呢?这就需要使用到我们的前后端分离开发,前端人员负责前端代码,后端人员负责后端代码,这是目前开发的主流方式

前后端分离开发后,我们的工程结构也会发生变化,不会再混合到一个maven工程中,而是分为前端和后端工程,其中后端工程我们会打包部署到Tomcat中,而前端工程则是部署到Nginx中

那么接着我们面临的问题就是,我们前后端分离之后要怎么协调统一开发呢?这里我们可以使用如下的流程来进行,首先我们要定制接口,注意,这里的接口指的是一个http的请求地址,其下定义了请求路径、方式、参数等内容,具体可以看下图

接口的定制是最重要的,因为其定义了前后端开发的规则,如果接口定义不好,那么后面就全完了。前后端开发都有自测的数据和方法,自测没问题后就进行联调,看看前后端是否能正常工作,都没问题之后就进入到最后的提测步骤,也就是自动化测试,这一步骤都过了那就全没事了

最后我们来了解前端开发所需要的技术栈,看看就行了,毕竟我们后端开发不太用得上前端代码

YApi

我们此前讲过定义接口是最为重要的,那么我们现在就来学习定义接口的工具,YApi,请看介绍

关于YApi的知识,我们这里就不深入了解了,反正大伙们只要知道其可以定义接口并且给前后端定义一个规范就可以了,其部署需要前端的环境,而且最重要的是,这些接口的定义一般也是产品经理的事,我们只管写后端代码就完了的,所以我们不用管这个太多

Swagger

虽然说这一节的内容是Swagger,但其实我们不用Swagger,而是使用Knife4j,其实javaMVC框架继承Swagger生成的Api文档的增强解决方案,本质上使用的还是Swagger,但是外部上我们用的是knife4j,功能更加强大

我们来看看我们的执行步骤,首先我们要做的当然是导入对应的坐标

然后我们需要导入相关的配置,这里需要加入两个注解,并且还需要额外在对应的配置类里添加两个方法

@Bean
public Docket createRestApi() {
    // 文档类型
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
            .paths(PathSelectors.any())
            .build();
}

private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
            .title("瑞吉外卖")
            .version("1.0")
            .description("瑞吉外卖接口文档")
            .build();
}

然后我们要操作对应的静态资源映射,否则我们是无法访问其对应的生成页面的

具体代码如下

registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");

最后我们需要在过滤器中设置不需要处理的请求路径,其实不设置也可以,但是这样的话我们每次访问对应的文档页面就要先登录一次,我反正是嫌弃麻烦,所以我们这里还是要设置下不需要处理的请求路径

最后这一切都搞定之后我们只需要直接访问这个路径就可以看到我们的接口描述文档了,http://localhost:8080/doc.html#/home

然后我们访问到了常用文档之后会发现这些文档虽然比较齐全,但是其说明很少,前端人员或者是其他同行看了可能会一下子不理解这是个什么几把玩意,因此我们需要使用Swagger提供给我们的注解,加到对应的我们的代码的地方,然后在我们的接口文档中就会对应生成这些说明

首先我们来看看请求在类上的注解

@Api(tags = "套餐相关接口")
public class SetmealController {

然后是在实体类上的注解

@ApiModel("套餐")
public class Setmeal implements Serializable {

属性上的注解

@ApiModelProperty("主键")
private Long id;

请求方法上的注解及其传入的参数的说明的注解

@GetMapping("/page")
@ApiOperation(value = "套餐分页查询接口")
@ApiImplicitParams({
        @ApiImplicitParam(name = "page",value = "页码",required = true),
        @ApiImplicitParam(name = "pageSize",value = "每页记录数",required = true),
        @ApiImplicitParam(name = "name",value = "套餐名称",required = false),
})
public R<Page> page(int page,int pageSize,String name) {

独立开发的功能以及问题总结

我们的这个程序还有许许多多的内容是课程中没有提到的,那些内容就要靠我们自己去完善了,我们这里主要记录我们是如何完成这些内容并遇上了什么问题,又是怎么解决的

订单信息分页查询

虽然我们已经实现了下单功能了,但是我们还没有开发我们的订单功能分页查询的功能,我们首先到前端中来查看下其请求

然后前端所需求的东西,很容易就能构造对应的分页查询的代码,返回的是Order对象,但是这样的代码出现的问题就是我们的前端页面不会显示我们商品的数量,经过分析前端代码,我们发现其会调用我们返回的对象的sunNum属性来展示数量,因此我们的解决方式是创建一个新的OrdersDto继承原来的Orders对象,加入sumNum属性,那么我们可以构造其实体类如下

@Data
public class OrdersDto extends Orders {

    //记录订单的数量
    private Integer sumNum;

}

然后我们的控制层的处理逻辑就是首先获取用户的id,构造两个分页构造器,查出第一个Orders的分页对象的所有内容并进行对应的排序,然后我们进行对象拷贝,除了原来的数据本体内容之外,其他内容全部拷贝到我们的另一个分页对象中

然后我们取出数据本体,用stream流将集合中的每一个单独的order的内容拷贝到ordersDto中,而菜品的具体数量则是我们的订单详细表中的菜品数据量,我们直接利用对应的菜品id查询出来结果并赋予就可以了,最后我们将该集合设置到dto的分页对象中,将分页对象传回即可

/**
 * 订单信息分页查询
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("/userPage")
public R<Page> page(int page,int pageSize){
    log.info("page = {},pageSize = {}",page,pageSize);

    //获取用户id
    Long currentId = BaseContext.getCurrentId();

    //构造分页构造器
    Page<Orders> pageInfo = new Page(page,pageSize);
    Page<OrdersDto> dtoPage = new Page<>();

    //构造条件构造器
    LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();

    //添加查询条件,根据用户id进行查询
    queryWrapper.eq(Orders::getUserId,currentId);

    //添加排序条件,根据更新时间进行排序
    queryWrapper.orderByAsc(Orders::getCheckoutTime);

    ordersService.page(pageInfo,queryWrapper);

    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dtoPage,"records");
    List<Orders> orders = pageInfo.getRecords();

    List<OrdersDto> list = orders.stream().map((item) -> {
        OrdersDto ordersDto = new OrdersDto();
        //对象拷贝
        BeanUtils.copyProperties(item,ordersDto);
        //构造条件构造器
        LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
        //订单id
        Long id = item.getId();
        wrapper.eq(OrderDetail::getOrderId,id);
        //根据id查询订单详细表中的数据数量
        int count = orderDetailService.count(wrapper);
        ordersDto.setSumNum(count);
        return ordersDto;
    }).collect(Collectors.toList());

    dtoPage.setRecords(list);

    return R.success(dtoPage);
}

这样构造代码在实际情况中是可行的,是没有问题的

然而不幸的是我后面去看别人做的前端页面的显示,发现其实我们这里还需要返回对应的菜品数据,这样在我们的订单页面里还会具体显示我们下单的具体菜品。说实话吧,我又看不太懂前端代码,捏麻麻的又不给我看最后的效果,我他妈怎么知道你要啥啊

总之我们可以将我们的代码改造如下,首先我要往我们的Dto对象中添加一个记录具体菜品数据的属性

package com.itheima.reggie.dto;

import com.itheima.reggie.entity.OrderDetail;
import com.itheima.reggie.entity.Orders;
import lombok.Data;

import java.util.List;

@Data
public class OrdersDto extends Orders {

    //记录订单的数量
    private Integer sumNum;

    //记录下单用户的名字
    private String consignee;

    //记录订单中具体的菜品
    private List<OrderDetail> orderDetails;
}

然后我们这里的基本逻辑基本跟前面一样,不同的是我们这里再每一个具体的订单对象中都查找一下具体的订单详细信息的集合,然后将该集合数据设置到我们的对象中即可,最后返回该集合对象

/**
 * 订单信息分页查询
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("/userPage")
public R<Page> page(int page,int pageSize){
    log.info("page = {},pageSize = {}",page,pageSize);
​
    //获取用户id
    Long currentId = BaseContext.getCurrentId();
​
    //构造分页构造器
    Page<Orders> pageInfo = new Page(page,pageSize);
    Page<OrdersDto> dtoPage = new Page<>();
​
    //构造条件构造器
    LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
​
    //添加查询条件,根据用户id进行查询
    queryWrapper.eq(Orders::getUserId,currentId);
​
    //添加排序条件,根据更新时间进行排序
    queryWrapper.orderByAsc(Orders::getCheckoutTime);
​
    ordersService.page(pageInfo,queryWrapper);
​
    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dtoPage,"records");
    List<Orders> orders = pageInfo.getRecords();
​
    List<OrdersDto> list = orders.stream().map((item) -> {
        OrdersDto ordersDto = new OrdersDto();
        //对象拷贝
        BeanUtils.copyProperties(item,ordersDto);
        //构造条件构造器
        LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
        //订单id
        Long id = item.getId();
        wrapper.eq(OrderDetail::getOrderId,id);
        //根据id查询订单详细表中的数据数量
        List<OrderDetail> orderDetailList = orderDetailService.list(wrapper);
        ordersDto.setSumNum(orderDetailList.size());
        ordersDto.setOrderDetails(orderDetailList);
        return ordersDto;
    }).collect(Collectors.toList());
​
    dtoPage.setRecords(list);
​
    return R.success(dtoPage);
}

再来一单功能开发

首先我们来分析下前端的再来一单的发送的请求

其发送的请求中一并发送一个json格式的数据,数据中只有id这个属性,我们可以用RequestBody注解+Orders对象来承接,用其他方式都是不行的,记住我们承接JSON格式的数据用RequestBody注解,并且我们要加入含有对应属性的对象

由于实现再来一单的逻辑比较复杂,因此我们先在对应的服务层接口中创建新的方法

    /**
     * 用户再来一单
     * @param orders
     */
    public void again(Orders orders);
}

然后我们具体实现这个方法,我们的基本逻辑就是模拟一次再次下单的过程,所以我们首先获得里面传入的最重要的订单id,然后我们利用该id获得原先的订单信息,然后我们查找原先的订单里的具体菜品,然后我们用steam流的形式将每一个菜品都加入到我们的购物车中

我们这里加入到购物车的逻辑是首先创建一个购物车对象,然后设置上对应的当前用户的id,接着我们往对应的菜品数据中取出菜品id,判断其是否为空,若不为空则说明其取出的菜品对象,此时我们就从数据库中取出对应的菜品对象,然后将各项数据都设置对应的购物车对象中。

这里有两点需要注意,第一点是我们菜品的口味数据,口味数据是保存到订单表中的,所以我们往购物车设置具体口味数据时要从订单中取出并设置,第二点是我们的金额设置由于在前端会给我们扩大100倍,因此我们这里加入时也需要将我们的金额给缩小一百倍

反之若为空则说明是套餐,此时我们取出订单中的套餐id并在数据库中查出对应的套餐数据,要注意的事情也是一样的,这里就不重复提了

最后我们统一给我们的购物车对象设置上新的时间,然后将原先订单中的数量也设置上去(注意这里的数量是设置上去的,这样才能动态形成用户下单的数量),然后我们再将购物车对象保存到购物车中即可

这一切搞定之后,调用服务层中的下单方法即可(当然由于我们这里涉及到了多表操作,所以别忘了要加入事务处理的注解)

/**
 * 用户再次下单
 * @param order
 */
@Override
@Transactional
public void again(Orders order) {
    Long id = order.getId();
​
    log.info("订单数据:{}",id);
​
    LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Orders::getId,id);
​
    //得到原先的订单信息
    order = ordersService.getOne(queryWrapper);
​
    //获取订单明细表中的具体菜品
    LambdaQueryWrapper<OrderDetail> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(OrderDetail::getOrderId,order.getId());
​
    List<OrderDetail> list = orderDetailService.list(wrapper);
​
    list.stream().map((item) -> {
        Long dishId = item.getDishId();
        ShoppingCart shoppingCart = new ShoppingCart();
​
        //先设置用户id于购物车对象中
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);
​
        if(dishId != null){
            //从数据库中取出对应的菜品对象,设置到对应的购物车对象中并保存
            DishDto dishDto = dishService.getByIdWithFlavor(dishId);
            String dishFlavor = item.getDishFlavor();
            shoppingCart.setImage(dishDto.getImage());
            shoppingCart.setName(dishDto.getName());
            shoppingCart.setDishId(dishDto.getId());
            shoppingCart.setDishFlavor(dishFlavor);
            BigDecimal price = dishDto.getPrice();
            shoppingCart.setAmount(price.divide(new BigDecimal(100)));
        }else {
            //没有菜品id则说明是套餐
            Long setMealId = item.getSetmealId();
​
            SetmealDto setmealDto = setmealService.getByIdWithDish(setMealId);
            BigDecimal price = setmealDto.getPrice();
            shoppingCart.setAmount(price.divide(new BigDecimal(100)));
            shoppingCart.setName(setmealDto.getName());
            shoppingCart.setSetmealId(setmealDto.getId());
            shoppingCart.setImage(setmealDto.getImage());
        }
        shoppingCart.setCreateTime(LocalDateTime.now());
        shoppingCart.setNumber(item.getNumber());
        shoppingCartService.save(shoppingCart);
        return item;
    }).collect(Collectors.toList());
​
    ordersService.submit(order);
}

最后我们在控制层中调用我们新创建的服务层的方法即可

/**
 * 再来一单
 * @param order
 * @return
 */
@PostMapping("/again")
public R<String> again(@RequestBody Orders order){
​
    ordersService.again(order);
​
    return R.success("下单成功");
}

移动端用户登出功能

首先我们来看看该功能请求的地址和方式

然后我们登出的方法本质上很简单,直接从共享域中移除信息即可,所以我们容易写入其代码如下

/**
 * 移动端用户退出
 * @param request
 * @return
 */
@PostMapping("/loginout")
public R<String> logout(HttpServletRequest request){
    //清理Session保存的当前用户登录的id
    request.getSession().removeAttribute("user");
    return R.success("退出成功");
}

员工端用户登录则无法正常拦截移动端用户直接访问页面的问题解决

这个问题注意到了,但是我不知道该怎么解决他,准确来说是有些思路,但是试着去实现了下却总是出问题,看了别人的代码发现别人也没有去解决这个问题,我也就这样放着得了

订单明细分页查询

我们通过分析前端代码易知其发送请求的方式跟我们之前的分页请求差不多,但是这里值得一提的是我们这里可以选择使用订单号查询或者是用日期来查询,所以我们这里要构造对应的利用这些条件查询的方法

本质代码也不难,自己看吧

/**
 * 订单明细分页查询
 * @param page
 * @param pageSize
 * @param number
 * @param beginTime
 * @param endTime
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize,Long number,String beginTime,String endTime){
    log.info("page = {},pageSize = {},number = {},beginTime = {},endTime = {}",page,pageSize,number,beginTime,endTime);

    //构造分页构造器
    Page<Orders> pageInfo = new Page(page,pageSize);

    //构造条件构造器
    LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();

    //添加过滤条件
    queryWrapper.like(number!=null,Orders::getNumber,number);
    queryWrapper.gt(!StringUtils.isEmpty(beginTime),Orders::getOrderTime,beginTime);
    queryWrapper.lt(!StringUtils.isEmpty(endTime),Orders::getOrderTime,endTime);

    //添加排序条件
    queryWrapper.orderByDesc(Orders::getOrderTime);

    //执行查询
    ordersService.page(pageInfo,queryWrapper);

    return R.success(pageInfo);
}

不过这里要提一下的是,虽然我们页面上需要的数据我们这里全都有了,但是不知道为啥收货人那一个信息一直不显示,就很傻逼,不过无所谓,就先这样吧,反正我们的数据都在里面的,没展示出来是前端的问题,不关我们的事

现在我们发现的确是前端的问题,稍微修改一下前端的代码就可以正确展示我们的数据了

修改订单状态

最后是修改订单状态的方法,这个也不难,我也懒得说,直接看代码吧

/**
 * 修改订单状态
 * @param orders
 * @return
 */
@PutMapping
public R<String> modifyStatus(@RequestBody Orders orders){
    log.info("orders:{}",orders);
    Orders order = ordersService.getById(orders.getId());
    order.setStatus(orders.getStatus());
    ordersService.updateById(order);
    return R.success("状态修改成功");
}

点击套餐图片回显信息

之前我们没有实现这个功能是因为我们不知道前端需要我们返回什么数据,所以我们根本无从下手,那玩毛啊是吧,但是看了别人的代码之后我们可算整明白我们需要返回什么数据了,我们需要返回的是一个承载着套餐内具体菜品的集合对象,每一个菜品对象里都应该要有我们订单所下的菜品的所有数据,因此我们的逻辑很简单,首先查出对应的套餐,然后用该套餐查出其下所有的套餐关联的菜品对象,接着用stream流的形式取出其中的每一个套餐关联菜品对象,查出其具体的菜品对象,然后将数据拷贝到我们的创建的DishDto中,这里我们拷贝两个内容,一个是我们原先的套餐菜品对象的数据,第二个是我们查出的具体的套餐对象的数据,前者是提供数据令其回显,后者也是同样的,不过提供的是一些描述和图片的数据,当然,也可以手动设置,就是比较麻烦,我们这里就直接用工具类拷贝了

/**
 * 页面回显套餐数据的方法
 * @param id
 * @return
 */
@GetMapping("/dish/{id}")
public R<List<DishDto>> echo(@PathVariable Long id){

    LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(SetmealDish::getSetmealId,id);

    List<SetmealDish> list = setmealDishService.list(lambdaQueryWrapper);

    List<DishDto> dishDtoList = list.stream().map((item) ->{
        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(item,dishDto);
        Dish byId = dishService.getById(item.getDishId());
        BeanUtils.copyProperties(byId,dishDto);
        return dishDto;
    }).collect(Collectors.toList());

    return R.success(dishDtoList);
}

用户地址的删除的修改功能

不是去看了别人的代码我都不知道还有这玩意.......总之代码本身也不难,我们迅速过一过吧

首先是删除的代码,直接根据id删除即可

/**
 * 删除地址的方法
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(Long ids){
    log.info("ids = {}",ids);
    LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(AddressBook::getId,ids);
    addressBookService.remove(lambdaQueryWrapper);
    return R.success("地址删除成功");
}

然后是修改的代码,修改的基本逻辑就是先删除再新增

/**
 * 修改地址的方法
 * @param addressBook
 * @return
 */
@PutMapping
public R<String> update(@RequestBody AddressBook addressBook){
    log.info("addressBook={}",addressBook);
    LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(AddressBook::getId,addressBook.getId());
    addressBookService.remove(lambdaQueryWrapper);
    addressBookService.save(addressBook);
    return R.success("地址修改成功");
}

菜品以及套餐的勾选框点击移动问题

菜品以及套餐的勾选框点击之后就会移动且不可恢复,除非刷新,解决方式是将对应前端代码的25px改为50px

遇到的问题及其总结

如果是难题会用*号做对应的标注,普通问题则不会

maven的idea集成

由于我自己的idea重装过,导致maven在idea中的集成需要再做一次,这里我分别按照我之前做过的笔记,以及通过CSDN里的这篇文章blog.csdn.net/Z2424858916…,解决了这个问题,成功将本地仓库的位置设定好

idea自动导包问题

maven中即使已经事先加载了对应的依赖,但是在实际的类里却无法使用。手动导入对应的包的地址之后这个问题就解决了

  • 启动项目时报错org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputExcept

这个问题是由于字符集输入格式错误导致的,在idea中修改字符集即可,具体的解决过程参考这个地址blog.csdn.net/JiaMing11_2…

数据库连接地址错误

产生这个问题的原因是课程里使用的数据库是本地数据库,而我们使用的数据库是远程的数据库,因此会报错,显示连接不上对应的数据库,下面是我们原来的数据库的连接地址

url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true

我们将其改为如下形式即可,其实就是将对应的localhost改为我们的远程连接的ip地址,同时由于我们的数据库设置了密码,因此我们这里同时要设置对应的密码

driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://175.178.114.158:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: itheima

为了查询密码顺便还重新复习了下在操作台上远程登录mysql的命令,主要是通过这个网址得知的blog.csdn.net/weixin_4239…

  • sqlyog数据库修改问题

修改数据库中的数据在右下角显示数据已修改但没有保存,解决方式就是进行了修改之后点击左上角的保存即可

  • 控制台上没有正确删除共享域中的内容

产生这个问题是因为没有刷新,点一下别的再点回来就可以看到结果了

  • 配置全局异常处理器没有生效

配置了全局的异常处理器,但是测试的时候发现没有启动,问题原因是全局异常处理类的方法没有加上ExceptionHandler注解,加上就可以了

  • 添加功能抛出500异常

报错信息如下2022-07-09 14:06:17.989 ERROR 5936 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException:

发生的异常是在我们进行公共字段自动填充的功能完善进行的测试时所发生的

问题产生的原因是我们没有给对应要填充的属性加入指定的TableField注解导致我们的公共字段没有进行填充,插入时的数据由于为空抛出异常导致的

  • 已经有了公共处理异常类的情况下,前端仍然返回500异常

异常信息如下2022-07-09 16:09:23.105 ERROR 58928 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DuplicateKeyException:

这个异常信息说实话也看不出来什么,经过Debug我们发现问题发生在我们的统一异常处理类中的这行代码里,这里我们的对字符串进行分割,但是字符串压根没有2个,就会抛出数组下标越界异常,将这行代码删除,直接令该异常处理返回固定的提示信息就行了

if(ex.getMessage().contains("#23000")){
    String[] split = ex.getMessage().split(" ");
    String msg = split[2] + "已存在";
    return R.error("该名称已存在");
}
  • 分类页面分页查询抛出异常

异常信息为2022-07-09 16:53:42.174 ERROR 57232 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.jdbc.BadSqlGrammarException:

产生这个异常的原因在于我们的实体类中多了一个字段,而数据库中没有这个字段,因此抛出了该异常,将实体类中数据库里不存在的字段删去即可

分类页面点击删除提示删除成功但数据库没有对应删除

产生这个问题的原因是,前端向后端发送数据是使用的变量名是ids而不是id,而我们后端写的代码里承接数据的变量名为id,这样id没有得到值,自然就无法删除,但仍然会执行后面的删除代码,因此会显示删除成功。

解决的方法是将后端的删除代码的变量名从id改为ids

项目无法正常启动

启动项目时报错,异常信息如下org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'commonController': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'reggie.path' in value "${reggie.path}"

其代表的意义是,我们对应的注解中获取reggie.path的值,但在对应的yml文件中没有该值,因此报错。产生这个错误的原因是我们在yml文件中指定对应的值的时候没有在:后加上一个空格再指定值导致的,把空格加上就能解决这个问题了

  • 项目无法正常启动2

启动项目时报错,异常信息如下

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.

2022-07-12 16:28:45.095 ERROR 1608 --- [ main] o.s.boot.SpringApplication : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [com/itheima/reggie/config/WebMvcConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'dishController' method

com.itheima.reggie.controller.DishController#save(DishDto)

to {POST [/dish]}: There is already 'dishController' bean method

原因是我们我们所创建的方法注解里的PostMapping没有改成PutMapping,因此产生了方法上的创建冲突,改过来就没事了