《苍穹外卖》电商实战项目(java)知识点整理(P1~P65)【上】
史上���完整的《苍穹外卖》项目实操笔记,跟视频的每一P对应,全系列10万字,涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳,参考这篇,相信会带给你极大启发。
《苍穹外卖》项目实操笔记【中】:P66~P122《苍穹外卖》项目实操笔记【中】
《苍穹外卖》项目实操笔记【下】:P123~P189《苍穹外卖》项目实操笔记【下】
一、重要知识点精讲
1.1 nginx反向代理P11
1. nginx反向代理好处:
1. 提高访问速度(可以进行缓存,如果访问相同资源可以直接响应数据)
2. 可以进行负载均衡(如果没有nginx前端只能固定地访问后端某一台服务器,加入nginx则可以将请求分发给后端不同的服务器)
负载均衡:把大量的请求按照、我们指定的指定的方式均衡的分配给集群中的每台服务器。
3. 保证后端服务安全(前端不能直接请求到后端服务器,需要通过Nginx转发)
2. nginx反向代理的搭建:
location /api/ 的意思是如果请求能匹配上/api/这个字符串。
proxy_pass 该指令的作用是设定转发的目的地,其后跟的是转发的目的地址。
3. nginx负载均衡的配置:
在webservers里面定义一组服务器,用于承接访问负载:
4. nginx负载均衡的策略:
服务器不一定需要平均承接请求,可以通过更改参数赋以不同的权重:
技巧:
1. 按F12可以打开浏览器的调试工具
3. 备注写上TODO可以在IDEA下方的TODO列表看到待做的操作
重要知识点:
1.用Git进行版本控制 P7 **
2.JWT令牌 P10 *
3.nginx反向代理 P11 **
4.Swagger P15 *
5.ThreadLocal P20 **
6.分页查询 P22 *
7.AOP P32 *
8.上传文件 图片 P36 P37 *
9.批量删除 P43 *
二、搭建开发环境 P3~P14
2.1软件开发整体介绍P3
软件开发流程
1. 需求分析:需求规格说明书(word文档)、产品原型(静态网页展示功能图片)。
2. 设计:UI设计(用户界面,小到按钮,大到页面布局,人机交互)、数据库设计(表结构、字段、类型等)、接口设计。
3. 编码:项目代码、单元测试。
4. 测试:测试用例、测试报告。
5. 上线运维:软件环境安装、配置。
角色分工
软件环境
2.2 苍穹外卖项目介绍P4
项目介绍
为餐饮企业(餐厅、饭店)定制的一款软件产品。
功能架构:体现项目中的业务功能模块。
产品原型(产品经理)
产品原型:用于展示项目的业务功能(一般用静态的HTML页面+适当的说明文字进行展示),一般由产品经理进行设计。
技术选型(架构师)
技术选型:展示项目中使用到的技术框架和中间件等。
2.3 前端环境搭建P5
先确保将nginx.exe放在无中文的目录下:
将监听的端口号更改为81,因为80端口时常被占用,如果用80端口可能会因为端口占用而无法打开!!
注意:这里配置的是nginx的监听端口,nginx在81号端口上监听网页端,最后是将数据传入8080端口的服务器端。
2.4 后端环境搭建P6
1. 熟悉项目框架+2.5+2.6
common存放的是公共类:constant常量类,context项目上下文,enumeration枚举类,exception异常类,json处理json转换的类,properties是Springboot中的一些配置属性类,会把配置文件中的配置项封装成对象,result后端的返回结果,utils工具类。
注意下面对象职责的说明:
Entity就是实体类,实体类一般与数据库表对应。(数据库字段一般是下划线命名,实体类属性一般是驼峰命名)
DTO数据传输对象,DTO一般是作为方法传入的参数在使用,不局限于前端给controller层传参,也可以是controller层给service层传参。
VO是视图对象,用于前端数据的展示,所以一般是controller层把VO传给前端,然后前端展示。
server子模块存放的是配置文件、配置类、拦截器、controller、service、mapper、启动类等。
2.5 使用Git进行版本控制P7
.gitignore中存放的是git不需要管理的文件:比如编译后生成的targit文件,以及测试类、测试包还有idea自带的一些文件。
先创建Git本地仓库
VCS - Create Git Repository创建远程仓库,选中根目录即可,若右上角出现标志说明成功:
打钩是提交按钮,点击后勾选所有文件,编写版本文字,点击Commit,这步是将项目提交到本地仓库:
PS:如果已有本地仓库,若想移除重新添加,下面是移除本地仓库的方法,首先在settings中移除本地仓库,然后关闭idea,把仓库地址下的.git、.idea、.gitignore文件删除,重新启动idea打开项目即可:
然后创建Git远程仓库
在gitee上创建远程仓库:
点击复制按钮,在IDEA中点击向上的按钮:
点击下面的链接,定义远程仓库,然后将刚刚复制的链接粘贴进来点击OK,即可将本地仓库与远程仓库关联:
最后将本地文件推送到Git远程仓库
然后直接点击Push即可,然后刷新一下gitee页面,会发现同步成功:
2.6 搭建数据库P8
一共11张表如下:
将已经提供的建表语句粘贴到查询处,点击运行,左边建立成功11张表:
2.7 前后端联调P9
要先将连接数据库的密码改为自己的密码:
在右端Maven处选中compile进行编译,若显示BUILD SUCCESS则说明编译通过:
在sky-server目录下的SkyApplication类中启动项目:
输入localhost:81可以打开登录页面:
2.8 调试方法+JWT令牌P10
点击小虫(进入断点调试),打上断点,然后前端点击登录(此时前端的数据会作为参数传入):
光标放在字段上还会显示接收到的数据:
若想程序在所希望的地方停止,可以添加断点,然后点击左下角的右箭头,意思是放行;点击一折的箭头,意思是前进一步:
执行之后会在其中标明注入的数据:
jwt令牌是调用了一个工具类,JwtProperties是一个配置属性类,这里讲一个小技巧,ctri+鼠标左键点进去后,可以通过点击左上角的地球来锁定当前类所在的目录路径位置:
@ConfigurationProperties注解代表当前类是一个配置属性类,作用是:封装配置文件中的一些配置项。
在注解内的参数指示了配置类中的参数,比如sky.jwt,就去application.yml文件中找sky jwt的配置项,这些配置项就对应了相应的属性。
原理就是:通过配置属性类,将配置文件中的配置项,封装成一个类,然后通过@Autowired注解注入到要使用的地方。
如下图使用builder方式来建造对象,前提是要在EmployeeLoginVo类上面加上@Builder注解。
注意后端给前端响应的数据一律都是封装为Result:
按f12进入到开发者工具,点击登录,可以看到请求的路径:
2.9 nginx反向代理P11
但出现问题,前端请求的地址和后端接口的地址不一致是如何请求成功的呢?
下图是前端请求地址,端口为81:
后端的地址如下,应该是http://localhost:8080/admin/employee/login
原理:nginx反向代理,将前端发送的请求由nginx转发到后端服务器。
下图是nginx的配置文件,它监听的是81端口,服务器名是本地(http://localhost:81)。如果匹配到api字符串(http://localhost:81/api),就转发到proxy_pass对应的地址(http://localhost:8080/admin)。如果后面还有字符串就拼接到目标地址后面(http://localhost:8080/admin/employee/login)。
2.10 完善登录功能P12
目前登录存在的问题:密码是明文存储(如123456),安全性太低。
思路:将密码采用MD5方式加密后进行存储,提高安全性(存储入数据库的密码是加密后的数据,并且加密的过程是不可逆的,无法通过加密结果算出明文)。
比对思路是:将前端输入的密码,经过转换,看能否比对上数据库中存储的密文。
将下面字符串粘贴到数据库:e10adc3949ba59abbe56e057f20f883e记得要Ctrl+S保存更改结果。
在service类中只需调用Spring提供的DigestUtils类中的md5DigestAsHex方法对密码进行加密转换,提供给后面比对即可,代码如下:
password = DigestUtils.md5DigestAsHex(password.getBytes());
2.11 导入接口文档P13
在开发之前需要先将接口定义好,然后前后端人员并行开发。
前后端分离开发流程
操作步骤
2.12 SwaggerP14
Swagger介绍
使用Swagger只需要按照规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。Swagger可以帮助后端生成接口文档、进行在线接口测试。
Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
Swagger使用步骤
导入下面坐标:
com.github.xiaoymin knife4j-spring-starter 3.0.2
相关配置:
@Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; }
它会扫描controller里面的所有方法,然后通过反射去解析,最终生成接口文档。
设置静态资源映射:
protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }
启动后访问localhost:8080/doc.html即可:
有login和logout这些都是controller里的方法:
可以直接在页面的测试栏中对方法进行测试:
Swagger常用注解P15
@Api注解使用样例(记得要加tags=):
@ApiModel注解使用样例:
@ApiModelProperty注解使用样例:
@ApiOperation注解使用样例:
注解影响如下图:
三、模块开发
3.1 (新增员工)分析设计P16
一般是对产品原型(静态HTML页面)分析,因为比较直观。思考录入项有没有什么限制。
密码采用默认密码,登录后可以进行修改。
接口定义如下(data一般是查询是会用到,msg一般是出错时会返回消息):
数据库设计如下:
3.2 (新增员工)代码开发P17
注意:当前端提交的数据和实体类中对应的属性差别较大时(也就是实体类中会有多余的属性),建议使用DTO来封装(DTO里的数据字段和前端提交的数据字段都能对应上)。
1.在EmployeeController中新建一个方法save,传入的参数是employeeDTO
1. 首先编写如下代码,是网页端读入的字段数据,在这里传入employeeService对象。有2点注意事项:①前端传入的数据是json格式,要用@RequestBody注解转换为对象。②为了方便调试加一个log.info,花括号{}的内容在后面会被替换为employeeDTO的值。
@PostMapping//post方式请求 @ApiOperation("新增员工") public Result save(@RequestBody EmployeeDTO employeeDTO){ log.info("新增员工:{}",employeeDTO); employeeService.save(employeeDTO); return Result.success(); }
2. 在EmployeeService中编写如下代码,思路是:先创建一个emloyee实体类,然后把DTO的数据拷贝到实体类中,然后对剩下的属性进行赋值。
public void save(EmployeeDTO employeeDTO){ Employee employee = new Employee(); BeanUtils.copyProperties(employeeDTO,employee);//对象属性拷贝 employee.setStatus(StatusConstant.ENABLE); employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(10L); //TODO 后续需要改为当前登录用户的id employee.setUpdateUser(10L); employeeMapper.insert(employee); }
有2个注意事项:①可以用BeanUtils工具类中的copyProperties方法来对对象进行拷贝,前提是对象的属性有一部分是相同的。②不应该直接用数字数字,否则会是硬编码,应该使用StatusConstant常量类。
3. 在EmployeeMapper中编写SQL语句,来将数据插入数据库:
@Insert("insert into employee(name,username,password,phone,sex,id_number,status,create_time,update_time,create_user,update_user)"+ "values"+ "(#(name),#(username),#(password),#(phone),#(sex),#(idNumber),#(status),#(createTime),#(updateTime),#(createUser),#(updateUser))") void insert(Employee employee);
下面是开启驼峰命名:
3.3 (新增员工)功能测试P18
在开发阶段,前端界面可能没有开发好,所以不能进行前后端联调测试,只能用接口文档进行测试。
下面进行测试出现401,是因为有拦截器进行了拦截,原因是缺少token令牌:
所以我们先在员工登录页面获取一个令牌:
全局参数设置-输入参数名称+参数值,然后关闭页面:
然后带着参数值发送:
成功在数据库中添加记录:
下面是前后端联调成功:
3.4 (新增员工)完善P19-20
问题1:录入的用户名已存在,抛出异常后没有处理,没处理的话,控制台会抛出错误P
用全局的异常处理器sky-server/handler/GlobalExceptionHandler,创建exceptionHandler方法,在方法里添加如下代码:
@ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) { String message = ex.getMessage(); if(message.contains("Duplicate entry")){ String[] split = message.split(" "); String username = split[2]; String msg = username + MessageConstant.ALREADY_EXISTS; return Result.error(msg); }else{ return Result.error(MessageConstant.UNKNOWN_ERROR); } }
目的是输出:xxx已经存在的提示。核心思想是:提取错误那段话的第3个词,然后拼接后输出。
问题2:新增员工时,创建人id和修改人id设置为了固定值P
程序中将创建者和更新者的id写死为10:
JWT认证机制:用户发起请求发送用户名和密码,后端进行校验,如果验证通过就生成JWT Token,将Token返回给客户端,客户端会保存Token,在后续请求的请求头中都会携带JWT Token,请求会被拦截器拦截到,会检查Token,如果通过就会展示数据,如果没有通过就会返回错误信息。
在拦截请求验证的时候可以获得JWT令牌
问题是:在解析出登录员工id后如何传递给Service的save方法?
答:通过ThreadLocal,它是Thread的局部变量,为每个线程提供单独一份的存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,在线程外则不能访问。
可以通过在controller、service和拦截器中输出线程的id来看是否单次请求是同一个线程,经实验验证是同一个线程。
System.out.println("当前线程的id:"+Thread.currentThread().getId());
在sky-common/src/main/java/context/BaseContext下封装了ThreadLocal的操作。
先在拦截器JwtTokenAdminInterceptor里将ID存到存储空间里(set),因为每次请求线程不变,所以存储空间的值不会被更改,因此可以在EmployeeServiceImpl类中取到该值(get),进而输出,很妙!
小技巧:选中要计算的表达式,然后右键,选择Evaluate Expression,然后点击Evaluate即可。
3.5 (分页查询)分析设计P21
分页展示,每页展示10条数据,可以输入员工姓名进行查询。total是总的数目,records是一页的条目数。
3.6 (分页查询)代码开发P22
下面是PageResult和EmployeePageQueryDTO的实体类定义:
Result是在PageResult的基础上加上code和msg,作为返回给前端的对象。
在EmployeeController添加一个方法:
@GetMapping("/page") @ApiOperation("员工分页查询") public Result page(EmployeePageQueryDTO employeePageQueryDTO){ log.info("员工分页查询,参数为:{}",employeePageQueryDTO); PageResult pageResult = employeeService.PageQuery(employeePageQueryDTO); return Result.success(pageResult); }
在EmployeeService接口中编写方法:
public PageResult PageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在EmployeeServiceImpl中实现方法:
@Override public PageResult PageQuery(EmployeePageQueryDTO employeePageQueryDTO) { //DTO已将页码和每页记录数传入,因此可以算出 // select * from employee limit 0,10,通过Limit来控制 PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize()); //页码和每页记录数传入 //Page是固定的,Employee是每个用户的信息 Page page = employeeMapper.pageQuery(employeePageQueryDTO);// //要将page对象处理为PageResult对象 long total = page.getTotal(); List result = page.getResult(); return new PageResult(total,result); }
PageHelper的startPage方法可以通过传入的参数自动设置Limit,传入的是页码和每页的记录数,好处是:字符串的拼接不用自己做。底层实现是:它会给ThreadLocal设置上述参数,然后在执行SQL语句时会自动被取出,然后拼接成Limit。
Page是PageHelper插件定义的一个泛型类,是一个固定的返回类型。
pagehelper可以简化分页代码的编写:
在EmployeeMapper中编写方法:
Page pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在application.yml配置文件中扫描了EmployeeMapper.xml配置文件:
在EmployeeMapper.xml中编写SQL语句,limit不用我们手写,pagehelper会自动帮我们追加拼接,order by是排序条件:
select * from employee and name like concat('%',#{name},'%') order by create_time desc
如果有传入name,代表是员工姓名查询,它只会返回带有相关词(字也可以,因为是模糊查询)的员工信息。
如果没有传入name,那么name就为空,判断内容不执行,默认返回1,所以它会查询所有employee元素。
在这里是模糊查询,用concat将name与%进行拼接,%的意思是匹配任意字符串/字符。
标签的id是mapper中的对应方法名。resultType是传入的参数类型。
3.7 (分页查询)功能测试P23
返回401,说明JWT校验时出现问题:Token的有效时间大约是2小时(此时重新登录获得Token,然后设置全局)。
发现日期显示的格式有问题(数字挤在一起):
3.8 (分页查询)代码完善P24
、
方法一:在Employee实体类中的LocalDateTime属性上加上@JsonFormat注解,格式化时间。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
方法二:拓展Spring MVC的消息转换器,统一对后端返回给前端的数据进行转换处理:
在sky-server下的com/sky/config/WebMvcConfiguration下创建:
//托转Spring MVC框架的消息转换器 protected void extendMessageConverters(List insert into dish(name,category_id,price,image,description,create_time,update_time,create_user,update_user,status) values (#{name},#{categoryId},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
在sky-server的mapper创建DishFlavorMapper类,写入insert方法的代码。
@Mapper public interface DishFlavorMapper { @AutoFill(value= OperationType.INSERT) void insertBatch(List flavors); }
在sky-server的resources下的mapper下创建DishFlavorMapper.xml文件,写入如下代码。
insert into dish_flavor (dish_id,name,value) VALUES (#{df.dishId},#{df.name},#{df.value})
在DishMapper和DishFlavorMapper中的insert方法上添加@AutoFill(value= OperationType.INSERT)注解。
3.23 (新增菜品)功能测试P39
采用前后端联调的方式。在DishServiceImpl的saveWithFlavor方法中,首先看一下传入参数dishDTO是否被封装好。然后看拷贝后的dish对象的值是否完整。最后看insert语句后的2个公共字段是否被赋值好。
没啥问题之后提交代码:新增菜品业务代码开发。
3.24 (分页查询)设计分析P40
请求的参数会被封装成DTO
因为categoryName是不存在菜品表里的,现在前端页面要展示分类名称,所以要定义VO,将VO转为JSON数据然后展示。
3.25 (分页查询)代码开发P41
在sky-server的controller中已有的DishController类中添加如下代码:
@GetMapping("/page") @ApiOperation("菜品分页查询") public Result page(DishPageQueryDTO dishPageQueryDTO){ log.info("菜品分页查询:{}",dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); }
在sky-server的service中已有的DishService类中添加如下代码:
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
在sky-server的service中已有的DishServiceImpl类中添加如下代码
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO){ PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize()); Page page = dishMapper.pageQuery(dishPageQueryDTO); return new PageResult(page.getTotal(),page.getResult()); }
在sky-server的mapper中已有的DishMapper类中添加如下代码:
Page pageQuery(DishPageQueryDTO dishPageQueryDTO);
在sky-server的resources下的mapper下已有的DishMapper.xml中写入如下代码:
select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id and d.name like concat('%',#{name},'%') and d.category_id = #{categoryId} and d.status = #{status}
可以先在Navicat中编写SQL语句进行查询再写到配置文件中。
出现问题,有2列都叫name,于是给category表起别名。
select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
简单测试一下发现没有问题:
3.26 (删除菜品)设计分析P42
3.27 (删除菜品)代码实现P43
批量删除是在地址栏写入要删除菜品的集合。
在sky-server的controller中已有的DishController类中添加如下代码:
@DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List ids){ log.info("菜品批量删除:{}",ids); dishService.deleteBatch(ids); return Result.success(); }
需要加一个注解@RequestParam,可以将地址栏中多个数字参数提取出来然后变成List集合。
在sky-server的service中已有的DishService类中添加如下代码:
void deleteBatch(List ids);
在sky-server的service的Impl中已有的DishServiceImpl类中添加如下代码:
@Transactional public void deleteBatch(List ids){ //不能删除:存在起售中的菜品 for (Long id : ids) { Dish dish = dishMapper.getById(id); if(dish.getStatus()== StatusConstant.ENABLE){ //状态为1起售中 //当前菜品处于起售中,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } //不能删除:菜品被套餐关联 List setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if(setmealIds != null && setmealIds.size()>0){ //当前菜品被套餐关联了,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } //删除菜品表中的菜品数据 for (Long id : ids) { dishMapper.deleteById(id); //删除口味数据 dishFlavorMapper.deleteByDishId(id); } }
批量删除用foreach循环来遍历,删除被套餐关联的SQL语句比较复杂。
删除菜品表中的菜品数据这里,每次循环需要执行2次SQL,可能会出现性能问题。应该采用如下的SQL形式:delete from dish where id in (?,?,?)。
在sky-server的mapper中已有的DishMapper类中添加如下代码(负责删除菜品):
//根据主键删除菜品 @Delete("delete from dish where id = #{id}") void deleteById(Long id);
在sky-server的mapper中创建DishFlavorMapper类中添加如下代码(负责删除关联的口味数据):
//根据菜品id删除对应的口味数据 @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteByDishId(Long dishId);
在sky-server的mapper中创建SetmealDishMapper类中添加如下代码(负责查看是否有关联的套餐):
@Mapper public interface SetmealDishMapper { //根据菜品id查询对应的套餐id //select setmeal_id from setmeal_dish where dish_id in (1,2,3,4) List getSetmealIdsByDishIds(List dishIds); }
(上步SQL具体实现)在sky-server的resources的mapper中创建SetmealDishMapper类中添加如下代码(思路是去查询套餐表,看套餐菜品id是否和当前传入的id相同):
select setmeal_id from setmeal_dish where dish_id in #{dishId}
这里比较巧妙的是foreach循环,collection是集合,item是一个个项,separator是分割符号,open是开始符号,close是结束符号。每个元素用逗号分割,然后用大括号括起来。
3.28 (删除菜品)功能测试P44
先测试一下删除单个,再测试批量删除。要注意起售中和被套餐关联的菜品不能被删除。最后提交一下代码:删除菜品业务代码开发。
在sky-server的service的Impl中已有的DishServiceImpl类中修改代码如下(只是修改最后一部分):
@Transactional public void deleteBatch(List ids){ //不能删除:存在起售中的菜品 for (Long id : ids) { Dish dish = dishMapper.getById(id); if(dish.getStatus()== StatusConstant.ENABLE){ //状态为1起售中 //当前菜品处于起售中,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } //不能删除:菜品被套餐关联 List setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if(setmealIds != null && setmealIds.size()>0){ //当前菜品被套餐关联了,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } //删除菜品表中的菜品数据 dishMapper.deleteByIds(ids); //删除口味数据 dishFlavorMapper.deleteByDishIds(ids); }
在sky-server的mapper中已有的DishMapper类中添加如下代码:
void deleteByIds(List ids);
在sky-server的resources的mapper中已有的DishMapper.xml配置文件中添加如下代码:
delete from dish where id in #{id}
在sky-server的mapper中已有的DishFlavorMapper类中添加如下代码:
void deleteByDishIds(List dishIds);
在sky-server的resources的mapper中已有的DishFlavorMapper.xml配置文件中添加如下代码:
delete from dish_flavor where dish_id #{dishId}
3.29 (修改菜品)分析设计P45
根据id查询菜品和口味,回显返回数据。然后修改菜品(用PUT)。
3.30 (修改菜品)代码开发P46
在sky-server的controller中已有的DishController类中添加代码如下:
@GetMapping("/{id}") @ApiOperation("根据id查询菜品") public Result getById(@PathVariable Long id){ log.info("根据id查询菜品:{}",id); DishVO dishVO = dishService.getByIdWithFlavor(id); return Result.success(dishVO); }
在sky-server的service中已有的DishService类中添加代码如下:
//根据id查询菜品 DishVO getByIdWithFlavor(Long id);
在sky-server的service的Impl中已有的DishServiceImpl类中添加代码如下:
//根据id查询菜品和对应的口味数据 public DishVO getByIdWithFlavor(Long id){ //根据id查询菜品数据 Dish dish = dishMapper.getById(id); //根据菜品id查询口味数据 List dishFlavors = dishFlavorMapper.getByDishId(id); //将查询到的数据封装到VO DishVO dishVO = new DishVO() ; BeanUtils.copyProperties(dish,dishVO); dishVO.setFlavors(dishFlavors); return dishVO; }
在sky-server的mapper中已有的dishFlavorMapper类中添加代码如下:
@Select("select * from dish_flavor where dish_id=#{dishId}") List getByDishId(Long dishId);
3.31 (修改菜品)代码开发P47
在sky-server的controller中已有的DishController类中添加代码如下:
//修改菜品 @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品;{}",dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }
在sky-server的service中已有的DishService类中添加代码如下:
//根据id修改菜品基本信息和对应的口味信息 void updateWithFlavor(DishDTO dishDTO);
在sky-server的service的Impl中已有的DishServiceImpl类中添加代码如下:
//根据id修改菜品基本信息和对应的口味信息 public void updateWithFlavor(DishDTO dishDTO){ Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO,dish); //修改菜品表基本信息 dishMapper.update(dish); //删除原有的口味数据 dishFlavorMapper.deleteByDishId(dishDTO.getId()); //重新插入口味数据 List flavors = dishDTO.getFlavors(); if(flavors != null && flavors.size()>0){ flavors.forEach(dishFlavor ->{ dishFlavor.setDishId(dishDTO.getId()); }); } dishFlavorMapper.insertBatch(flavors); }
口味的修改比较麻烦,有可能是删除了再新增,有可能不删除,有可能没删除直接新增。
处理方法:直接把菜品原先关联的口味数据删掉,然后再按照当前传来的口味重新插入数据。
传入DTO不合适,因为DTO里有口味数据,而修改菜品不应该包含口味,所以应该只传入一个Dish数据。
在sky-server的mapper中已有的dishFlavorMapper类中添加代码如下:
//根据id动态修改菜品 @AutoFill(value=OperationType.UPDATE) void update(Dish dish);
在sky-server的resources下的mapper中已有的dishFlavorMapper.xml类中添加代码如下:
update dish name = #{name}, category_id = #{categoryId}, price = #{price}, image = #{image}, description = #{description}, status = #{status}, update_Time = #{updateTime}, update_User = #{updateUser}, where id = #{id}
这里用的是动态SQL。
3.32 (修改菜品)功能测试P48
前后端联调测试,尝试修改一道菜品,能成功修改即可。
四、Redis系列
本辑讲的是:Redis入门 -> Redis数据类型 -> Redis常用命令 -> 在Java中操作Redis -> 店铺营业状态设置。
4.1 Redis入门 P50
Redis是一个基于内存的key-value结构数据库。读写性能高。因为内存有限所以不能存储所有数据。Redis只存储热点数据。
下载方式:直接在苍穹外卖资料包中第5天的资料中,选择windows版本x64版的压缩包解压。
启动方式:在redis-server.exe和redis.windows.conf所在的目录,点击路径栏,输入cmd,然后输入下面的启动命令:
redis-server.exe redis.windows.conf
下面就是启动成功:
输入ctrl+c就是停止服务。
在server服务端启动的基础上,去启动客户端cli,同样进入当前路径下的cmd,然后输入:
redis-cli.exe
如果出现下面就是成功进入:
输入exit即可退出。
cli客户端可以通过-h指定连接的ip,-p制定连接的端口:
redis-cli.exe -h localhost -p 6379
redis默认没有密码,如果需要密码则修改redis.windows.conf配置文件。
此时重新登录也并不会提示输入密码,但如果输入keys *则会报错,身份验证被要求。
通过-a的方式来输入密码:
redis-cli.exe -h localhost -p 6379 -a 密码
也可以通过图形化界面来操作服务。
填入Host和Port和密码即可图形化:
4.2 Redis常用数据类型 P51
Redis存储是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型。
字符串string,哈希hash,列表list,集合set,有序集合zset(sorted set)。
哈希:在value里面又分为field和value。比较适合存储对象,包括属性和值。
列表:类似于一个队列,有顺序,按照插入顺序排序,可以有重复元素。可以存储跟顺序有关系的。
集合:无序集合,没有重复元素。可以运算交集或者并集。
有序集合:集合中每个元素关联一个分数,根据分数升序排序,没有重复元素。适用场景排行榜,或者投票排名、
4.3 Redis字符串操作P52
可以通过可视化工具来练习Redis语法:
SET key value 设置指定key的值。
GET key 获取指定key的值。
SETEX key seconds value 设置指定key的值,并将key的过期时间设为seconds秒。
(seconds是时间,时间以秒为单位,下图想说明的是如果过期了,将直接删除key和value)
SETNX key value 只有在key不存在时设置key的值。
(下图想说明的是如果已经存在了某个键将无法再重新设置,设置成功返回1,设置失败返回0)
4.4 Redis哈希操作P53
HSET key field value 将哈希表key中的字段field的值设为value。
效果展示如下图:
HGET key field 获取存储在哈希表中指定field的值。
HDEL key field 删除存储在哈希表中的指定field。
HKEYS key 获取哈希表中所有field。
HVALS key 获取哈希表中所有value。
4.5 Redis列表操作P54
列表是简单的字符串列表,跟插入顺序有关,最先插入的会排在最后。
LPUSH key value1 [value2] 将一个或多个值插入到列表头部
LRANGE key start stop 获取列表指定范围内的元素
(如果stop为-1则表示获取全部元素)
RPOP key 移除并获取列表最后一个元素
LLEN key 获取列表长度
4.6 Redis集合操作P55
集合中没有重复元素。而且没有顺序。
SADD key member1 [member2] 向集合添加一个或多个成员。
SMEMBERS key 返回集合中的所有成员。
SCARD key 获取集合的成员数
SINTER key1 [key2] 返回给定所有集合的交集
SUNION key1 [key2] 返回所有给定集合的并集
SREM key member1 [member2] 删除集合中一个或多个成员
4.7 Redis有序集合操作P56
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员。(stop为-1表示范围为全部,withscores是展示member的同时展示score)
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量increment。
ZREM key member [member ...] 移除有序集合中的一个或多个成员。
4.8 Redis 通用命令P57
KEYS pattern 查找所有符合给定模式(pattern)的key
(*代表全部,剩下的可以是半匹配)
EXISTS key 检查给定key是否存在
TYPE key 返回key所储存的值的类型
DEL key 该命令用于在key存在时删除key
Java中操作Redis
4.9 操作步骤说明 P58
Redis的Java客户端很多,包括:Jedis(所有的方法和Redis是一一对应的)、Lettuce、Spring Data Redis。
Spring Data Redis是Spring的一部分,对Redis底层开发包进行了高度封装。在Spring项目中,可以使用Spring Data Redis来简化操作。
①导入Spring Data Redis的maven坐标
②配置Redis数据源
③编写配置类,创建RedisTemplate对象
④通过RedisTemplate对象操作Redis
4.10 环境搭建P59
①导入Spring Data Redis的maven坐标
org.springframework.boot spring-boot-starter-data-redis
②配置Redis数据源
方法1:是直接在application.yml中配置,但不推荐,最好使用引用。
spring: redis: host: localhost port: 6379 database: 0
方法2:是在application-dev.yml中配置具体值
sky: redis: host: localhost port: 6379 database: 0
在application.yml中配置引用
spring: redis: host: ${sky.redis.host} port: ${sky.redis.port} database: ${sky.redis.database}
数据库默认是0号数据库(DB0),一直到15,总共16个。
③编写配置类,创建RedisTemplate对象
在sky-server下的config包下创建RedisConfiguration类,写入如下代码:
@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("开始创建redis模板对象..."); RedisTemplate redisTemplate = new RedisTemplate(); //设置redis的连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); //设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
在sky-server/src/test/java/com/sky/test下创建Java类SpringDataRedisTest写入如下代码:
@SpringBootTest public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate(){ System.out.println(redisTemplate); ValueOperations valueOperations = redisTemplate.opsForValue(); HashOperations hashOperations = redisTemplate.opsForHash(); ListOperations listOperations = redisTemplate.opsForList(); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); } }
可以先运行当前文件输出一下看看是否生成了对象。
④通过RedisTemplate对象操作Redis
4.11 操作字符串数据 P60
Redis的String和Java的String不是完全相同。Java中的任何对象都会转为Redis的字符串进行存储。
在sky-server/src/test/java/com/sky/test下已有的Java类SpringDataRedisTest中写入如下代码:
//操作字符串类型的数据 @Test public void testString(){ //set get setex setnx redisTemplate.opsForValue().set("city","北京"); String city = (String)redisTemplate.opsForValue().get("city"); System.out.println(city); redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES);//第3个参数是时间,第4个参数是时间单位 redisTemplate.opsForValue().setIfAbsent("lock","1"); redisTemplate.opsForValue().setIfAbsent("lock","2"); }
value会有乱码的现象,key不会有问题,是因为序列化器不同,所以结果不同。
4.12 操作哈希数据P61
在sky-server/src/test/java/com/sky/test下已有的Java类SpringDataRedisTest中写入如下代码:
@Test public void testHash(){ //hset hget hkeys hvals HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.put("100","name","tom"); //相当于hset hashOperations.put("100","age","20"); String name = (String)hashOperations.get("100", "name"); //相当于hget System.out.println(name); Set keys = hashOperations.keys("100"); //相当于hkeys System.out.println(keys); List values = hashOperations.values("100"); //相当于hvals System.out.println(values); hashOperations.delete("100","age");//相当于hdel }
控制台输出结果如下:
4.13 操作其它类型数据P62
列表(list)类数据:
在sky-server/src/test/java/com/sky/test下已有的Java类SpringDataRedisTest中写入如下代码:
@Test public void testList(){ //Lpush lrange rpop llen ListOperations listOperations = redisTemplate.opsForList(); listOperations.leftPushAll("mylist","a","b","c"); //lpush多个value listOperations.leftPush("mylist","d"); //lpush单个value List mylist = listOperations.range("mylist",0,-1); //lrange System.out.println(mylist); listOperations.rightPop("mylist"); //rpop Long size = listOperations.size("mylist"); //llen System.out.println(size); }
集合类数据:
在sky-server/src/test/java/com/sky/test下已有的Java类SpringDataRedisTest中写入如下代码:
@Test public void testSet(){ //sadd smembers scard sinter sunion srem SetOperations setOperations = redisTemplate.opsForSet(); setOperations.add("set1","a","b","c","d"); //sadd setOperations.add("set2","a","b","x","y"); Set members = setOperations.members("set1"); //smembers System.out.println(members); Long size = setOperations.size("set1"); //scard System.out.println(size); Set intersect = setOperations.intersect("set1","set2"); //sinter取交集 System.out.println(intersect); Set union = setOperations.union("set1","set2"); //sunion取并集 System.out.println(union); setOperations.remove("set1","a","b"); //srem }
有序集合类数据:
在sky-server/src/test/java/com/sky/test下已有的Java类SpringDataRedisTest中写入如下代码:
@Test public void testZset(){ //zadd zrange zincrby zrem ZSetOperations zSetOperations = redisTemplate.opsForZSet(); zSetOperations.add("zset1","a",10); //zadd zSetOperations.add("zset2","b",12); zSetOperations.add("zset1","c",9); Set zset1 = zSetOperations.range("zset1",0,-1); //zrange System.out.println(zset1); zSetOperations.incrementScore("zset1","c",10); //zincrby zSetOperations.remove("zset1","a","b"); //zrem }
通用命令操作:
在sky-server/src/test/java/com/sky/test下已有的Java类SpringDataRedisTest中写入如下代码:
@Test public void testCommon(){ //keys exists type del Set keys = redisTemplate.keys("*"); //keys System.out.println(keys); Boolean name = redisTemplate.hasKey("name"); //exists Boolean set1 = redisTemplate.hasKey("set1"); for(Object key : keys){ DataType type = redisTemplate.type(key); //type System.out.println(type.name()); } redisTemplate.delete("mylist"); //del }
4.14(营业状态设置)分析设计P63
设置营业状态;管理端查询营业状态,用户端查询营业状态(管理端和用户端查询路径不同)。
营业状态存储在Redis中,不用在Mysql中单独创建一张表。
4.15(营业状态设置)代码开发P64
先把test中的SpringDataRedisTest里的@SpringBootTest注释掉。
这里要注意一点:营业状态是存储在Redis中的,SHOP_STATUS这个key不能够通过redis直接修改,这里必须通过前端页面(管理端电脑)进行修改,也就是调用下面的setStatus,因为redis和SpringDataRedis的序列化器是不同的。
在controller/admin下创建ShopController这个是管理端的,写入如下代码:
@RestController("adminShopController") @RequestMapping("/admin/shop") @Api(tags="店铺相关接口") @Slf4j public class ShopController { public static final String KEY="SHOP_STATUS"; @Autowired private RedisTemplate redisTemplate; //设置店铺营业状态 @PutMapping("/{status}") @ApiOperation("设置店铺的营业状态") public Result setStatus( @PathVariable Integer status){ log.info("设置店铺的营业状态为:{}",status==1 ?"营业中":"打烊中"); redisTemplate.opsForValue().set("SHOP_STATUS",status); return Result.success(); } //获取店铺的营业状态 @GetMapping("/status") @ApiOperation("获取店铺的营业状态") public Result getStatus(){ Integer status = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态为:{}",status==1?"营业中":"打烊中"); return Result.success(status); } }
在controller下创建user包,然后把amin的ShopController复制到这个下面,然后进行简单修改,只保留获取状态的。因为有2个类类名相同,会导致Bean冲突,所以我们要在@RestController中指定Bean的名称。
@RestController("userShopController") @RequestMapping("/user/shop") @Api(tags="店铺相关接口") @Slf4j public class ShopController { public static final String KEY="SHOP_STATUS"; @Autowired private RedisTemplate redisTemplate; //获取店铺的营业状态 @GetMapping("/status") @ApiOperation("获取店铺的营业状态") public Result getStatus(){ Integer status = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态为:{}",status==1?"营业中":"打烊中"); return Result.success(status); } }
4.16(营业状态设置)功能测试P65
直接通过前后端联调的方式,去修改营业的状态,然后退出看状态是否保持,在Redis可视化界面中是否有相关的key-value记录。
因为现在接口用户端和管理端没有区分开,很不好看,所以现在在sky-server下的config的WebMvcConfiguration中写入如下代码:
@Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("管理端接口") .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin")) .paths(PathSelectors.any()) .build(); return docket; } @Bean public Docket docket1() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("用户端接口") .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user")) .paths(PathSelectors.any()) .build(); return docket; }
可以分为用户端和管理端的接口: