瑞吉外卖笔记
Posted 风吟Pro
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了瑞吉外卖笔记相关的知识,希望对你有一定的参考价值。
概述
功能架构图
数据库建库建表
表说明
开发环境
Maven搭建
直接创建新工程
继承父工程的形式来做这个,这里新建父工程
pom文件
server:
port: 9001
spring:
application:
name: ccTakeOut
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruiji?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 333
redis:
host: localhost # 本地IP 或是 虚拟机IP
port: 6379
# password: root
database: 0 # 默认使用 0号db
cache:
redis:
time-to-live: 1800000 # 设置缓存数据的过期时间,30分钟
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
启动测试
创建测试类并启动
导入前端页面
导入
在默认页面和前台页面的情况下,直接把这俩拖到resource目录下直接访问是访问不到的,因为被mvc框架拦截了
所以我们要编写一个映射类放行这些资源
创建配置映射类
访问成功
后台开发
数据库实体类映射
用mybatis plus来实现逆向工程
这里是老版本的逆向工程
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--mybatis-plus 代码生成器依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
具体怎么玩看这里
MP逆向工程教程
账户操作
登陆功能
前端页面
数据库
业务逻辑
这里两个字符串的比较没法用!=来实现,只能equals再取反来判断
直接上代码,这里没有涉及service层的操作
/**
* @param request 如果登陆成功把对象放入Session中,方便后续拿取
* @param employee 利用@RequestBody注解来解析前端传来的Json,同时用对象来封装
* @return
*/
@PostMapping("/login")
public Result login(HttpServletRequest request, @RequestBody Employee employee)
String password=employee.getPassword();
String username = employee.getUsername();
log.info("登陆");
//MD5加密
MD5Util md5Util = new MD5Util();
password=MD5Util.getMD5(password);
//通过账户查这个员工对象,这里就不走Service层了
LambdaQueryWrapper<Employee> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.eq(Employee::getUsername, username);
Employee empResult=employeeService.getOne(lambdaQueryWrapper);
//判断用户是否存在
if (!empResult.getUsername().equals(username))
return Result.error("账户不存在");
//密码是否正确
else if (!empResult.getPassword().equals(password))
return Result.error("账户密码错误");
//员工账户状态是否正常,1状态正常,0封禁
else if (empResult.getStatus()!=1)
return Result.error("当前账户正在封禁");
//状态正常允许登陆
else
log.info("登陆成功,账户存入session");
//员工id存入session,
request.getSession().setAttribute("employ",empResult.getId());
return Result.success("登陆成功");
具体代码可以参考如下路径
com.cc.controller.EmployeeController
退出功能
点击退出
删除session对象
/**
* @param request 删除request作用域中的session对象,就按登陆的request.getSession().setAttribute("employ",empResult.getId());删除employee就行
* @return
*/
@PostMapping("/logout")
public Result login(HttpServletRequest request)
//尝试删除
try
request.getSession().removeAttribute("employ");
catch (Exception e)
//删除失败
return Result.error("登出失败");
return Result.success("登出成功");
完善登陆(添加过滤器)
这里的话用户直接url+资源名可以随便访问,所以要加个拦截器,没有登陆时,不给访问,自动跳转到登陆页面
过滤器配置类注解@WebFilter(filterName="拦截器类名首字母小写",urlPartten=“要拦截的路径,比如/*”)
判断用户的登陆状态这块之前因为存入session里面有一个名为employee的对象,那么只需要看看这个session还在不在就知道他是否在登陆状态
注意,想存或者想获取的话,就都得用HttpServletRequest
的对象来进行获取,别的request对象拿不到的
这里提一嘴
调用Spring核心包的字符串匹配类的对象,对路径进行匹配,并且返回比较结果
如果相等就为true
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
前端拦截器完成跳转到登陆页面,不在后端做处理
代码太多了,给个路径好啦,直接去Gitee看
request的js代码路径:resource/backend/js/request.js
拦截器的路径:com.cc.filter.LoginCheckFilter
新增员工
新增员工功能,(前端对手机号和身份证号长度做了一个校验)
请求 URL: http://localhost:9001/employee (POST请求)
改造一下Employee实体类,通用id雪花自增算法来新增id
这里用service接口继承的MybatisPlus的功能
注入一下就可以使用了,插入方法
基本上都是自动CRUD,访问路径:com.cc.controller.EmployeeController
全局异常处理
先看看这种代码的try catch
这种try catch来捕获异常固然好,但是,代码量一大起来,超级多的try catch就会很乱
所以我们要加入全局异常处理,在Common包下,和Result同级,这里只是示例,并不完整
当报错信息出现Duplicate entry时,就意味着新增员工异常了
所以,我们对异常类的方法进行一些小改动,让这个异常反馈变得更人性化
这个时候再来客户端试试,就会提供人性化的报错,非常的快乐~
这回再回到Controller,这时就不需要再来try catch这种形式了,不用管他,因为一旦出现错误就会被我们的AOP捕获。所以,不需要再用try catch来抓了
异常类位置:com.cc.common.GloableExceptionHandler
员工信息分页查询
接口分析
老生常谈分页查询了
需求
分页请求接口
查询员工及显示接口
逻辑流程
分页插件配置类
先弄个MP分页插件配置类
原因是和3.2.3版本的代码生成器冲突
分页插件爆红解决方案
直接注释掉
加入配置类
接口设计
前端注意事项
page对象内部
里面包含了查询构造器的使用
具体的细节在这个包下:com.cc.controller.EmployeeController.page
/**
* 分页展示员工列表接口、查询某个员工
* @param page 查询第几页
* @param pageSize 每页一共几条数据
* @param name 查询名字=name的数据
* @return 返回Page页
*/
@GetMapping("/page")
public Result<Page> page(int page, int pageSize,String name)
//分页构造器,Page(第几页, 查几条)
Page pageInfo = new Page(page, pageSize);
//查询构造器
LambdaQueryWrapper<Employee> lambdaQueryWrapper = new LambdaQueryWrapper();
//过滤条件.like(什么条件下启用模糊查询,模糊查询字段,被模糊插叙的名称)
lambdaQueryWrapper.like(!StringUtils.isEmpty(name), Employee::getName, name);
//添加排序
lambdaQueryWrapper.orderByDesc(Employee::getCreateTime);
//查询分页、自动更新
employeeService.page(pageInfo, lambdaQueryWrapper);
//返回查询结果
return Result.success(pageInfo);
启用、禁用员工账号
无非就是修改status,0禁用,1启用
这种根据登陆人物来进行判断的玩法,是前端
这个页面的位置resource/backend/page/member/list.html
看拿出来的对象是什么样子的,如果是admin,vue的v-if指令就会把编辑按钮显示出来
如果是普通用户,就会把编辑按钮隐藏
修复一个小Bug
前端一直不显示编辑按钮,在localStorage里没有发现admin对象
这个值不应该是登陆成功,应该是Employee的对象Json
猜测是登陆的时候往request里存对象没存好
改成对象存入就好了
这回都正常了
功能编写
复习一下
PutMapping是Resultful风格的请求方式
当前状态是1,直接带着目标状态值(状态改禁用)进行更新
Id精度丢失,js独有的bug,直接处理Long处理不了,要Long转String再返回去
利用对象转换器JacksonObjectMapper,将对象转Json
将Long型的Id转换为String类型的数据
在MVC配置类中扩展一个消息转换器
测试功能正常,正常更新员工状态
消息扩展器配置位置:com.cc.common.JacksonObjectMapper
对象映射器位置:com.cc.config.WebMvcConfig
员工状态更新位置:com.cc.controller.EmployeeController
编辑员工信息
请求API,这个是先发请求,查到用户,然后填充到页面上
可以看出来,这种请求方式是ResultFul风格的请求方式
在控制器中要用@PathVariable(“/参数名称”)注解来进行接收
完美更新
更新方法位置:com.cc.controller.EmployeeController.getEmployee
公共字段自动填充
像是一部分公共字段,反复填充起来没有意义,简化填充的操作。
把这个功能拿出来,单独拎出来做自动填充处理
为实体类属性上面加入注解@TableField(fill = 填充条件)
看一下源码。fill是填充条件,用枚举来进行处理的
加完注解和条件不算完,还要加入配置类进行处理,对填充的数据做规定
在common包下创建一个自定义类,最关键的是要实现MetaObjectHandler接口下的insertFill和updateFill
确认填充时需要的字段。还有要加入@Component注解,将这个类交给框架来管理,否则的话容易找不到,setValue的值会根据注解加入的字段名称来锁定是否需要更新
位置:com.cc.common.MyMetaObjectHandler
但是这里有个问题,如果我想去更新管理员字段是非常困难的,因为我这里拿不到Request的作用域对象,所以要想个办法来处理。
这个时候就需要ThreadLocal
来进行对象的获取,这个线程是贯穿整个运行的,可以通过他来获取
使用时
何为ThreadLocal
重点来了
这个图
我的思路就是在用户登陆的时候,把这个id存进去,等到在填充字段的时候,从ThreadLocal里把这个资源再拿出来。
直接操作不太好,把他封装成一个工具类,这个工具类里方法都是静态的,可以通过类直接调用、并且都是静态方法,来操作保存和读取
我选择在Utils下创建
第一次的Bug
具体包在utils里,有Bug,封装的类ThreadLocal获取不到数据,不太清楚为什么,暂时就把这个写死了
// 基于ThreadLocal 封装工具类,用户保存和获取当前登录的用户id
// ThreadLocal以线程为 作用域,保存每个线程中的数据副本
public class BaseContext
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
// 设置当前用户id
public static void setCurrentId(Long id)
threadLocal.set(id);
public static Long getCurrentId()
return threadLocal.get();
注意,ThreadLocal不是一个线程,只有同一个线程才能拿到,不是一个线程拿不到的
解决方案
更改setId的位置,存储的时候放在过滤器内部,就算是一个线程了,就能拿到。不过我都试过了,确实是一个线程,但是还是拿不到。
换个思路:因为我想拿Request对象里的Id嘛,所以,只要有Request的id就行,不必过于执着一定要用ThreadLocal来存,因此,我这里选择注入一下HttpServletRequest对象来解决这个问题。
菜品页面
菜品分类
涉及的表有分类表category
业务流程
新增菜品分类
请求方式是Post请求
控制器位置:com.cc.controller.CategoryController (save)
菜品分类展现
还是那几步
- 创建分页构造器 Page pageInfo = new Page(第几页,每页几条数据);
- 如果有需要条件过滤的加入条件过滤器LambaQueryWarpper
- 注入的service对象(已经继承MP的BaseMapper接口)去调用Page对象
service对象.page(分页信息,条件过滤器) - 返回结果就可以了
分页查询位置:com.cc.controller.CategoryController.page
删除菜品分类
普通版本,没有考虑分类有关联的情况
完善一下,如果当前菜品分类下有菜品的话,就不许删除
所以在删除之前要先做判断才可以删除,不符合条件的,我们要抛出异常进行提示
因为没有返回异常信息的类,我们这里要做一个自定义的专门返回异常信息的类CustomerException
这个类的位置也在common包下
因为我们之前创建了一个全局异常处理,也要用上,因为要拦截异常统一处理
还是com.cc.common.GloableExceptionHandler
对抛出异常进行处理,就可以对新增的异常提供目标的拦截和异常通知
删除菜品分类的controller接口在:com.cc.controller.CategoryController (delCategory)
因为业务特殊,且比较长,就分离出来把业务放在service包下
service接口位置:com.cc.service.impl.CategoryServiceImpl (removeCategory)
修改套餐信息
非常简单的CRUD,直接调用MP更新一下就行
API位置
com.cc.controller.CategoryController (updateCategory)
文件上传下载(重点)
上传逻辑
第一次接触上传和下载的功能
文件上传逻辑(后端)
参数名有要求的
接收的文件类型一定是 方法名(MultipartFile 前端上传的文件名称)
所以后端的接收名字也得改为file
上传逻辑实现
具体的存储路径写在配置文件里了
用@Value注入到业务里就可以了
具体位置在com.cc.controller.CommonController (upLoadFile)
下载逻辑
图片回显功能
用到了输入输出流
位置:com.cc.controller.CommonController (fileDownload)
菜品管理页面
新增菜品
需求分析
涉及表为dish和dish_flavor
开发逻辑
新增实现
由于是多表的操作,MP直接干肯定不行,所以就把service层抽离出来进行处理
还有,因为涉及两张表,这里还要加入事务进行控制,防止多表操作崩溃
多表操作只能一个一个来,MP没有办法一次性操作多张表
因为涉及到多表的问题,所以还要加入注解来处理事务
@Transactional 开启事务
@EnableTransactionManagement 在启动类加入,支持事务开启
Controller位置:com.cc.controller.DishController (addDish)
Service位置:com.cc.service.DishService
ServiceImpl位置:com.cc.service.impl.DishServiceImpl (addDishWithFlavor)
新增菜品之获取菜品种类
从前端接收一个type=1的标注,目的是在分类表中,菜品分类是1,套餐分类是2,把二者区分开,获取所有的菜品类型
位置:com.cc.controller.CategoryController (listCategory)
菜品分页
顺手把菜品分页也做了,不写太多了,位置在:com.cc.controller.CategoryController (dishPage)
记录一个知识点,如果说后端没有类和前端要的数据对应,那么自己就可以封装一个类来对前端特殊需要的数据进行封装
DTO对象
这个类可以是对一些实体类进行扩展,继承于某个父类,再添加一些内容
比如Dish和DishDto
DishDto就继承于Dish类,并在此基础上进行了扩展
更新菜品信息
就是个update
逻辑
注意,这里回显数据是要用DishDto,因为前端要显示口味等信息,这里如果用Dish是无法完美显示的,所以要用DishDto
回显填充查询
除此之外,这是个多表联查,用MP肯定不行,得自己写
Controller位置:com.cc.controller.DishController (updateDish)
Service位置:com.cc.service.DishService
ServiceImpl位置:com.cc.service.impl.DishServiceImpl
更新实现
实际上就是两个表联动更新和删除操作,所以MP直接操作是不可以的,所以要在Service层自己再封装一个删除方法,给Controller层调用删除就行
对于Dish对象可以直接进行更新,因为DishDto是Dish的子类
因此可以调用DishService的update方法传入DishDto对象,来实现Dish的更新
Controller位置:com.cc.controller.DishController (updateDish)
确实和上面那个一样,因为请求方式不一样
Service位置:com.cc.service.DishService
ServiceImpl位置:com.cc.service.impl.DishServiceImpl (updateDishWithFlavor)
其他功能
完成一些小功能的开发
停售功能
就是把数据库的status值更新一下,两个路径,一个启售,一个停售
停售请求路径
如果状态不一样了,会从停售变成启售,同时对应的请求路径也不一样
Controller位置:com.cc.controller.DishController (updateStatusStop)
停止
Controller位置:com.cc.controller.DishController (updateStatusStart)
启动
删除功能
菜品删除功能
完成逻辑删除,不是真删
位置:
Controller位置:com.cc.controller.DishController (deleteDish)
停止
套餐页面
实际上就是一组菜品的集合
新增套餐概述
涉及到的数据库
导入SetmealDto
新增套餐之菜品列表
Controller位置:com.cc.controller.DishController (listCategory)
新增套餐实现
和新增菜品差不多,这里也是多表的操作
Controller位置:com.cc.controller.SetmealController (saveSetmeal)
Service位置:com.cc.service.SetmealService
ServiceImpl位置:com.cc.service.impl.SetmealServiceImpl(saveWithDish)
套餐分页
这里的套餐分页和以往不同,设计到了多表内容
套餐分页Controller位置:com.cc.controller.SetmealController.pageList
套餐Mapper接口位置:com.cc.mapper.SetmealMapper
Mapper文件位置:resource.mapper.SetmealMapper
更新套餐
添加套餐和更新套餐是几乎完全一致的,字段巴拉巴拉的都一样
但是注意,修改套餐的话,需要先对菜品页面进行填充,这一页都是需要填充满要修改的菜品信息的。
先发请求,一看就是Restful风格请求
获取套餐Controller位置:com.cc.controller.SetmealController.getSetmal
更新销售状态
和之前一个业务逻辑很像,不想多赘述了,直接放接口位置
Controller位置:com.cc.controller.SetmealController (startSale/stopSale)
删除套餐
可以单独删,也可以批量删,接口是万金油,都能接,主要看传来的数据是几个
接口
== 多表删除,在Controller直接实现不太现实,所以要在Service把业务写好==
Controller位置:com.cc.controller.SetmealController (deleteSetmeal)
Service位置:com.cc.service.SetmealService
ServiceImpl位置:com.cc.service.impl.SetmealServiceImpl(removeWithDish)
前台开发(手机端)
账户登陆
短信发送
代码实现
官方文档地址
导入Maven
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
导入短信登陆的工具类,把ACCESSKeyID和Secret更换到位就行
验证码发送
数据模型user表,手机验证码专用的表
开发流程
修改拦截器,放行操作
controller位置:com.cc.controller.UserController (sendMsg)
发送完还需要验证,验证就是另一个login了
用户登陆
controller位置:com.cc.controller.UserController (login)
这里登陆还涉及到过滤器放行的功能,不要忘记了,把用户id存入session,过滤器会进行验证
过滤器
黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV13a411q753?spm_id_from=333.337.search-card.all.click这篇博客是记录自己学习该项目的markdown笔记;并且自己把视频中一些没实现的功能给实现了;本人技术可能不到位,笔记仅供参考学习使用;
本人自己把视频中老师没讲的一些功能给实现了,比如,后台按条件查询客户订单,用户个人查询自己的订单,菜品,套餐的启售,停售,购物车中菜品或者是套餐数量减少,后台套餐的修改。代码不一定规范,但是功能是没问题的!!!
项目中的资料下载链接:(从黑马公众号获取到的最初状态的源码,后面自己补充了一些课程没讲的功能,功能的实现代码在我博客的笔记中有)
链接:https://pan.baidu.com/s/1cdHI5cDjyHKZ4_0GmIevnQ
提取码:668a
目录
菜品信息分页查询(功能完善里面的代码要熟悉,有集合泛型的转换,对象copy)
一、项目背景介绍
技术选型:
二、软件开发整体介绍
三、开发环境的搭建
①数据库环境的搭建
1.创建数据库:
2.导入表结构,直接运行外部SQL文件;
数据表的说明:
序号 | 表名 | 说明 |
---|---|---|
1 | employee | 员工表 |
2 | category | 菜品和套餐分类表 |
3 | dish | 菜品表 |
4 | setmeal | 套餐表 |
5 | setmeal_dish | 套餐菜品关系表 |
6 | dish_flavor | 菜品口味关系表 |
7 | user | 用表(c端) |
8 | address_book | 地址薄表 |
9 | shopping_cart | 购物车表 |
10 | orders | 订单表 |
11 | orders_detail | 订单明细表 |
②maven项目搭建
1.创建一个maven项目
注意:创建maven项目后,一定要检查项目的编码,maven仓库的配置,jdk的配置等;
2.导入pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itheima</groupId>
<artifactId>reggie_take_out</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.6</version>
</plugin>
</plugins>
</build>
</project>
3.创建application.yml文件:
server:
port: 8080
spring:
application:
# 应用的名称,选择性配置
name: reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
# 把SQL的查询的过程输出到控制台
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
3.创建Boot程序入口
package com.itheima.reggie;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author LJM
* @create 2022/4/14
*/
@Slf4j
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication
public static void main(String[] args)
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功...");
4.运行Boot程序,看是否成功;
③导入前端文件
注意前端文件的位置,在Boot项目中,前台默认就只能访问 resource目录下的static和template文件夹下的文件;所以如果要使用这种方式,直接创建一个static目录就行,然后把这些前端资源放在这个static目录下就行;
如果你不想把前端文件放在这两个默认的文件夹下,那么就可以自己定义mvc的支持,这里我们使用的就是这方式;(多学习一种定义的方法,以后自定义映射的时候可以使用)
package com.itheima.reggie.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* @author LJM
* @create 2022/4/14
*/
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport
/**
* 设置资源映射
* @param registry
* 前面表示的是浏览器访问的请求
* 后面表示的是要把请求映射到哪里去
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry)
log.info("开始进行静态资源映射");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
记得在启动程序加上@ServletComponentScan这个注解,否则这个配置类不会生效;
四、后台登陆功能开发
①需求分析:
需求分析是通过产品原型来进行的,这个是项目经理负责的;
②代码开发:
前端页面访问地址:http://localhost:8080/backend/page/login/login.html
查看登陆请求信息:点击登录会发送登录请求:http://localhost:8080/employee/login
我们去后端进行代码开发相关的接口就行;
创建相关的包:
实体类和mapper的开发
在entity导入实体类employee类;
使用mybatis-plus提供的自动生成mapper:
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Employee;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee>
使用快捷键 Ctrl + f3 就可以看见mybatis-plus 帮我们定义的mapper接口:
service
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Employee;
public interface EmployeeService extends IService<Employee>
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.mapper.EmployeeMapper;
import org.springframework.stereotype.Service;
/**
* @author LJM
* @create 2022/4/15
*/
@Service //这两个泛型一个是实体类对应的mapper,一个是实体类
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper,Employee> implements EmployeeService
查看帮我们实现的方法:
封装返回的结果类
创建一个新的包,common,用来存放共同使用的类,把这个返回结果类放入这个公共包;
package com.itheima.reggie.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 通用返回结果类,服务端响应的数据最终都会封装成此对象
* @param <T>
*/
@Data
public class R<T>
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object)
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
public static <T> R<T> error(String msg)
R r = new R();
r.msg = msg;
r.code = 0;
return r;
public R<T> add(String key, Object value)
this.map.put(key, value);
return this;
controller
登陆的具体流程图:在平板上,记得传过来。
先处理业务逻辑,然后再编码!!!
1、将页面提交的密码password进行md5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码比对,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果
package com.itheima.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
/**
* @author LJM
* @create 2022/4/15
*/
@RestController
@Slf4j
@RequestMapping("/employee")
public class EmployeeController
@Autowired
private EmployeeService employeeService;
@PostMapping("/login") //使用restful风格开发
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee)//接收前端的json数据,这个json数据是在请求体中的
//这里为什么还有接收一个request对象的数据?
//登陆成功后,我们需要从请求中获取员工的id,并且把这个id存到session中,这样我们想要获取登陆对象的时候就可以随时获取
//1、将页面提交的密码password进行md5加密处理
String password = employee.getPassword();//从前端用户登录拿到的用户密码
password = DigestUtils.md5DigestAsHex(password.getBytes());//对用户密码进行加密
//2、根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
//在设计数据库的时候我们对username使用了唯一索引,所以这里可以使用getOne方法
Employee emp = employeeService.getOne(queryWrapper);//这里的切入Wrapper是什么?
//3、如果没有查询到则返回登录失败结果
if (emp == null )
return R.error("用户不存在");
//4、密码比对,如果不一致则返回登录失败结果
if (!emp.getPassword().equals(password))
//emp.getPassword()用户存在后从数据库查询到的密码(加密状态的) password是前端用户自己输入的密码(已经加密处理)
return R.error("密码不正确");
//5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
if (emp.getStatus() == 0)
return R.error("账号已禁用");
//6、登录成功,将员工id存入Session并返回登录成功结果
request.getSession().setAttribute("employee",emp.getId());
//把从数据库中查询到的用户返回出去
return R.success(emp);
③功能测试:
使用debug的形式启动项目,然后在浏览器访问:http://localhost:8080/backend/page/login/login.html
然后打开浏览器的f12,查看具体的请求情况:
在后台查看debug的状态:
运行成功后:(这个密码是123456),数据存在了浏览器中:这个代码是吧返回的数据保持在浏览器中:
localStorage.setItem('userInfo',JSON.stringify(res.data))
在浏览器我们可以看见,key为userInfo,value为我们返回的数据;
五、后台系统退出功能
点击退出按钮,发送退出的请求:http://localhost:8080/employee/logout
后端代码处理:
①在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
②清理session中的用户id
③返回结果(前端页面会进行跳转到登录页面)
前端代码,也要把浏览器中的数据给清除;
/**
* 退出功能
* ①在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
* ②清理session中的用户id
* ③返回结果(前端页面会进行跳转到登录页面)
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request)
//清理session中的用户id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
功能测试:先登陆,然后退出即可;看浏览器中的数据是否会被清除;
六、员工管理模块
完善登陆功能
问题分析:前面的登陆存在一个问题,如果用户不进行登陆,直接访问系统的首页,照样可以正常访问,这种设计是不合理的,我们希望看到的效果是只有完成了登陆后才可以访问系统中的页面,如果没有登陆则跳转到登陆页面;
那么如何实现?
答案就是使用过滤器或者是拦截器,在拦截器或者是过滤器中判断用户是否已经完成了登陆,如果没有登陆则跳转到登陆页面;
代码实现:这里使用的是过滤器;
①创建自定义过滤器LongCheckFilter
package com.itheima.reggie.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author LJM
* @create 2022/4/15
* 检查用户是否已经完成登陆
* filterName过滤器名字
* urlPatterns拦截的请求,这里是拦截所有的请求
*
*/
@WebFilter(filterName = "LongCheckFilter",urlPatterns = "/*")
@Slf4j
public class LongCheckFilter implements Filter
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("拦截到的请求:",request.getRequestURL());
//对请求进行放行
filterChain.doFilter(request,response);
②在启动类加上注解@ServletComponentScan
然后先测试一下过滤器能不能生效,具体的逻辑等下再书写;发送请求,看后台能不能打印拦截的信息:
③完善过滤器的处理逻辑
具体逻辑的代码实现:
package com.itheima.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author LJM
* @create 2022/4/15
* 检查用户是否已经完成登陆
* filterName过滤器名字
* urlPatterns拦截的请求,这里是拦截所有的请求
*
*/
@WebFilter(filterName = "LongCheckFilter",urlPatterns = "/*")
@Slf4j
public class LongCheckFilter implements Filter
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
//对请求和响应进行强转,我们需要的是带http的
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI
String requestURL = request.getRequestURI();
//定义不需要处理的请求路径 比如静态资源(静态页面我们不需要拦截,因为此时的静态页面是没有数据的)
String[] urls = new String[]
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
;
//做调试用的
//log.info("拦截到请求:",requestURL);
//2、判断本次请求是否需要处理
boolean check = check(urls, requestURL);
//3、如果不需要处理,则直接放行
if(check)
//log.info("本次请求不需要处理",requestURL);
filterChain.doFilter(request,response);
return;
//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null)
//log.info("用户已登录,用户id为:",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
//log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据,具体响应什么数据,看前端的需求,然后前端会根据登陆状态做页面跳转
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI)
for (String url : urls)
//把浏览器发过来的请求和我们定义的不拦截的url做比较,匹配则放行
boolean match = PATH_MATCHER.match(url, requestURI);
if(match)
return true;
return false;
功能测试: 发起几个请求看看后台的输出,和能不能访问到资源里面的数据,和能不能跳转,注意,上面的后台日志代码已经被注释,需要在后台看到日志的话,需要把注释去掉;
新增员工
数据模型:
新增员工,其实就是将我们的新增页面录入的员工数据插入到employee表;注意:employee表中对username字段加入了唯一的约束,因为username是员工的登陆账号,必须是唯一的!
employee表中的status字段默认设置为1,表示员工状态可以正常登陆;
代码开发:
梳理一下代码执行的流程:
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping()//因为请求就是 /employee 在类上已经写了,所以咱俩不用再写了
public R<String> save(HttpServletRequest request,@RequestBody Employee employee)
//对新增的员工设置初始化密码123456,需要进行md5加密处理,后续员工可以直接修改密码
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId); //创建人的id,就是当前用户的id(在进行添加操作的id)
employee.setUpdateUser(empId);//最后的更新人是谁
//mybatis提供的新增方法
employeeService.save(employee);
return R.success("新增员工成功");
功能测试:登陆之后,点击添加,然后确认,然后去数据库看一下新增数据成功没,新增成功,那就表示代码可以执行; 注意:但是因为我们把username设置为唯一索引,所以下次再新增用户的时候,就会出现异常,这个异常是MySQL数据库抛出来的;
解决bug:
全局异常捕获
这个全局异常捕获写在common包下;
package com.itheima.reggie.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* @author LJM
* @create 2022/4/15
* 全局异常处理
*/
@ControllerAdvice(annotations = RestController.class, Controller.class) //表示拦截哪些类型的controller注解
@ResponseBody
@Slf4j
public class GlobalExceptionHandler
/**
* 处理SQLIntegrityConstraintViolationException异常的方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandle(SQLIntegrityConstraintViolationException exception)
log.error(exception.getMessage()); //报错记得打日志
if (exception.getMessage().contains("Duplicate entry"))
//获取已经存在的用户名,这里是从报错的异常信息中获取的
String[] split = exception.getMessage().split(" ");
String msg = split[2] + "这个用户名已经存在";
return R.error(msg);
return R.error("未知错误");
功能测试:登陆后,添加一个一个已经存在账号名,看前端页面提示的是什么信息,以及看后台是否输出了报错日志;
员工信息分页查询
需求分析:系统中的员工比较多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般都系统中都会以分页的方式来展示列表数据。
流程分析:
Java代码:
//配置mybatis-plus的分页插件
package com.itheima.reggie.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author LJM
* @create 2022/4/15
* 配置mybatis-plus提供的分页插件拦截器
*/
@Configuration
public class MybatisPlusConfig
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor()
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
/**
* 员工信息分页
* @param page 当前页数
* @param pageSize 当前页最多存放数据条数,就是这一页查几条数据
* @param name 根据name查询员工的信息
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name)
//这里之所以是返回page对象(mybatis-plus的page对象),是因为前端需要这些分页的数据(比如当前页,总页数)
//在编写前先测试一下前端传过来的分页数据有没有被我们接受到
//log.info("page = ,pageSize = ,name = " ,page,pageSize,name);
//构造分页构造器 就是page对象
Page pageInfo = new Page(page,pageSize);
//构造条件构造器 就是动态的封装前端传过来的过滤条件 记得加泛型
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//根据条件查询 注意这里的条件是不为空
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加一个排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询 这里不用封装了mybatis-plus帮我们做好了
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
功能测试:分页的三个时机,①用户登录成功时,分页查询一次 ②用户使用条件查询的时候分页一次 ③跳转页面的时候分页查询一次
启用/禁用员工账号
需求分析:
在员工管理列表页面中,可以对某个员工账号进行启用或者是禁用操作。账号禁用的员工不能登陆系统,启用后的员工可以正常登陆;
需要注意的是:只有管理员(admin用户)才可以对其他普通用户进行启用操作,禁用操作,所以普通用户登录系统后启用,禁用按钮不显示;
并且如果某个员工账号的状态为正常,则按钮显示为’‘禁用’,如果员工账号状态为已禁用,则按钮显示为“启用”。
普通员工登录系统后,启用,禁用按钮不显示;
代码开发:
注意:这里修改状态码要反着来,因为正常的用户你只能把它设置为禁用;已经禁用的账号你只能把它设置为正常
流程分析:
注意:启用,禁用的员工账号,本质上就是一个更新操作,也就是对status状态字段进行修改操作;
在controller中创建update方法,此方法是一个通用的修改员工信息的方法,因为status也是employee中的一个属性而已;这里使用了动态SQL的功能,根据具体的数据修改对应的字段信息;
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee)
log.info(employee.toString());
Long empId = (Long)request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
功能测试:测试的时候我们发现出现了问题,就是我们修改员工的状态,提示信息显示修改成功,但是我们去数据库查验证的时候,发现员工的状态码压根就没有变化,这是为什么呢?
仔细观察id后,我们会发现后台的SQL语句使用的id和数据库中的id是不一样的!
原因是:mybatis-plus对id使用了雪花算法,所以存入数据库中的id是19为长度,但是前端的js只能保证数据的前16位的数据的精度,对我们id后面三位数据进行了四舍五入,所以就出现了精度丢失;就会出现前度传过来的id和数据里面的id不匹配,就没办法正确的修改到我们想要的数据;
当然另一种解决bug的方法是:关闭mybatis-plus的雪花算法来处理ID,我们使用自增ID的策略来往数据库添加id就行;
使用自定义消息转换器
代码bug修复:
思路:既然js对long型的数据会进行精度丢失,那么我们就对数据进行转型,我们可以在服务端(Java端)给页面响应json格式的数据时进行处理,将long型的数据统一转换为string字符串;
代码实现步骤:
步骤一:自定义消息转换类
package com.itheima.reggie.common;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化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 的消息转换器,在此消息转换器中使用spring提供的对象转换器进行Java对象到json数据的转换;
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters)
//log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
//转换器是有优先级顺序的,这里我们把自己定义的消息转换器设置为第一优先级,所以会优先使用我们的转换器来进行相关数据进行转换,如果我们的转换器没有匹配到相应的数据来转换,那么就会去寻找第二个优先级的转换器,以此类推
converters.add(0,messageConverter);
然后启动程序,使用f12查看服务器响应到浏览器的用户id是不是变成了字符串,和数据库中是否相对应;
Java项目瑞吉外卖保姆级学习笔记(改项目名称+改邮件验证码登录+功能补充)
基于Springboot和mybatis的外卖项目瑞吉外卖Day5