项目地址:https://gitee.com/meiyouname/reggie_project
运行项目
导入sql文件至mysql数据库

打开application.yml文件
修改数据库连接信息:

修改静态资源目录:


运行项目,访问项目后台管理外卖平台:
plain
http://localhost:8081/backend/page/login/login.html
http://localhost:8081/backend/index.html


访问外卖前端点外卖:
http://localhost:8081/front/page/login.html
前台移动端页面在桌面浏览器中的首屏布局问题:
注意!!!由于项目做的是手机端的适配,在电脑PC端无法直接正常显示,在浏览器按下F12按钮即可正常显示

输入手机号点击获取验证码
立刻回答idea查看打印日志,获取四位验证码

输入验证码后点确定即可进入页面

页面如下:

提交订单


然后可在后台管理系统订单管理查看

基本bug修改
(1)员工管理在任务栏不显示
一开始员工管理在左侧任务栏不显示,修改了src/main/resources/backend/index.html,把首页 menuList 改成了下面这套编号:
html
menuList: [
{
id: '1',
name: '员工管理',
url: 'page/member/list.html',
icon: 'icon-member'
},
{
id: '2',
name: '分类管理',
url: 'page/category/list.html',
icon: 'icon-category'
},
{
id: '3',
name: '菜品管理',
url: 'page/food/list.html',
icon: 'icon-food'
},
{
id: '4',
name: '套餐管理',
url: 'page/combo/list.html',
icon: 'icon-combo'
},
{
id: '5',
name: '订单明细',
url: 'page/order/list.html',
icon: 'icon-order'
}
// ],
// },
],
1 员工管理
2 分类管理
3 菜品管理
4 套餐管理
5 订单明细。
同时修改页面跳转文件统一编号:
src/main/resources/backend/page/member/list.html
src/main/resources/backend/page/member/add.html
src/main/resources/backend/page/food/list.html
src/main/resources/backend/page/food/add.html
src/main/resources/backend/page/combo/list.html
src/main/resources/backend/page/combo/add.html
修正结果
员工管理相关页面统一使用菜单 id 1
菜品管理相关页面统一使用菜单 id 3
套餐管理相关页面统一使用菜单 id 4

(2)照片添加
菜品管理本来是没有照片的,于是在application.yml里面加上
yaml
# 常量配置,存放图片的目录
reggie:
path: E:/J2EE/code/chapter10/reggie/src/main/resources/backend/images/
以及去网上找到了图片把对应图片复制黏贴到了文件夹里面

(3)修正了员工管理接口
前端员工管理页面已经写好了,但后端 EmployeeController 里原本只有登录和退出接口
后端只实现了:
- POST /employee/login
- POST /employee/logout
但前端员工管理页面实际还会调用:
- GET /employee/page
- POST /employee
- GET /employee/{id}
- PUT /employee
所以如果只恢复菜单,不补接口, 点击员工管理之后仍然会继续报错
主要修改文件:
src/main/java/com/itheima/reggie/controller/EmployeeController.java
- GET /employee/page
用途:
给后台"员工管理"列表页做分页查询
功能说明:
- 支持员工列表分页
- 支持按姓名模糊搜索
- 结果按更新时间倒序显示
- 这就能满足员工列表页的基本展示需求。
java
@GetMapping("/page")
public R<Page<Employee>> page(int page, int pageSize, String name) {
Page<Employee> pageInfo = new Page<>(page, pageSize);
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
queryWrapper.orderByDesc(Employee::getUpdateTime);
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
- POST /employee
用途:
新增员工
最终保留的逻辑包括:
给新员工设置默认密码 123456
用 MD5 对默认密码加密
默认状态设为启用
调用 employeeService.save(...) 保存
java
/**
* 新增员工。
* 前端没有单独输入密码,所以这里给新员工一个统一默认密码 123456。
*/
@PostMapping
public R<String> save(@RequestBody Employee employee) {
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setStatus(1);
employeeService.save(employee);
return R.success("新增员工成功");
}
- GET /employee/{id}
用途:
给"编辑员工"页面做数据回显
最终保留的逻辑包括:
根据员工 id 查询数据
如果查不到,返回"员工信息不存在"
如果查到了,在返回前把密码字段清空
java
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id) {
Employee employee = employeeService.getById(id);
if (employee == null) {
return R.error("员工信息不存在");
}
// 回显时不把密码直接返回给前端,更安全一些。
employee.setPassword(null);
return R.success(employee);
}
- PUT /employee
用途:
修改员工资料
启用员工
禁用员工
最终保留的逻辑是:
统一使用 employeeService.updateById(employee)
java
/**
* 修改员工信息。
* 员工资料修改、启用、禁用都统一走这个接口。
*/
@PutMapping
public R<String> update(@RequestBody Employee employee) {
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
还修改了:
- src/main/resources/backend/index.html
- src/main/resources/backend/page/member/list.html
- src/main/resources/backend/page/member/add.html
项目不同模块如何实现的
讲后台左侧这五个模块是怎么实现的:
- 员工管理
- 分类管理
- 菜品管理
- 套餐管理
- 订单明细
1. 先理解这五个模块共同的后端套路
这五个模块虽然功能不同,但后端实现套路基本一致,都是下面这条链路:
latex
后台页面
→ backend/api/*.js 发请求
→ Controller 接口接收请求
→ Service 处理业务逻辑
→ Mapper 调用数据库
→ 把结果包装成 R 返回给页面
可以把它理解成:
- 页面负责"提需求"
- Controller 负责"接单"
- Service 负责"真正处理事情"
- Mapper 负责"去数据库拿数据或改数据"
这五个模块还共用了几个基础能力:
1.1 统一返回结果 R
文件:src/main/java/com/itheima/reggie/common/R.java
作用:
- 所有接口尽量都返回同一种格式
- 成功时返回
R.success(...) - 失败时返回
R.error(...)
这样后台页面更容易统一处理结果。
1.2 登录校验过滤器 LoginCheckFilter
文件:src/main/java/com/itheima/reggie/filter/LoginCheckFilter.java
作用:
- 后台模块不是谁都能访问
- 用户先登录
- 过滤器检查 session 里有没有
employee - 有就放行,没有就返回
NOTLOGIN
所以这五个后台模块能正常用的前提是:
- 先通过
/employee/login登录
1.3 MyBatis-Plus 分页能力
文件:src/main/java/com/itheima/reggie/config/MybatisPlusConfig.java
作用:
- 员工列表
- 分类列表
- 菜品列表
- 套餐列表
- 订单列表
这些列表页都用了分页查询,底层依赖的是 MyBatis-Plus 的 Page<T>。
1.4 DTO 的作用
有些页面展示的数据不只来自一张表,所以项目里会用 DTO。
本次五个模块里最重要的 DTO 是:
- DishDto.java
- SetmealDto.java
- OrdersDto.java
简单说:
Dish只是一张菜品表的数据- 但页面还想看到分类名、口味列表
- 所以后端要用
DishDto重新打包
2. 员工管理模块如何实现
2.1 页面入口
后台左侧菜单里的"员工管理"入口在:
- src/main/resources/backend/index.html
点击后进入:
- src/main/resources/backend/page/member/list.html
新增/编辑页面在:
- src/main/resources/backend/page/member/add.html
前端请求封装在:
- src/main/resources/backend/api/member.js
2.2 后端核心文件
- EmployeeController.java
- EmployeeService.java
- EmployeeServiceImpl.java
- EmployeeMapper.java
- Employee.java
2.3 这个模块解决了什么问题
员工管理模块主要负责:
- 员工登录
- 员工退出
- 员工分页查询
- 新增员工
- 查询单个员工信息
- 修改员工信息
也就是说,后台左侧看到的"员工管理",本质上不是一个按钮,而是后端提供的一组接口共同支撑出来的。
2.4 员工登录如何实现
接口:
POST /employee/login
实现文件:
- EmployeeController.java
实现过程:
- 页面提交用户名和密码
- 后端把密码做一次
MD5 - 根据用户名查询员工表
- 比较密码是否一致
- 判断账号状态是不是禁用
- 登录成功后把员工 id 存进 session 的
employee
为什么要存 session?
因为后面访问分类、菜品、套餐、订单这些后台模块时,过滤器就靠 session 判断你是不是已经登录。
2.5 员工列表如何实现
接口:
GET /employee/page
实现逻辑:
- 接收
page、pageSize、name - 用
Page<Employee>做分页对象 - 如果传了
name,就按员工姓名模糊查询 - 按更新时间倒序排序
- 调用
employeeService.page(...) - 把分页结果返回给前端
所以页面才能实现:
- 搜索员工
- 翻页
- 展示每一页数据
2.6 新增员工如何实现
接口:
POST /employee
实现逻辑:
- 页面提交员工资料
- 后端不给页面机会直接传密码
- 后端统一给新员工默认密码
123456 - 再把这个默认密码做
MD5 - 默认状态设置为启用
- 保存到员工表
这么做的好处是:
- 页面更简单
- 后台新增员工流程更统一
2.7 编辑员工回显如何实现
接口:
GET /employee/{id}
实现逻辑:
- 根据员工 id 查询员工
- 如果查不到,返回"员工信息不存在"
- 如果查到了,把密码字段清空
- 再返回给前端做表单回显
为什么要把密码清空?
因为回显页面不需要看到数据库里的密码密文,直接返回不安全。
2.8 修改员工如何实现
接口:
PUT /employee
实现逻辑:
- 页面把修改后的员工对象提交回来
- 后端按 id 直接更新
这个接口同时承担三种用途:
- 修改基本资料
- 启用员工
- 禁用员工
所以员工管理模块看起来功能多,后端其实是通过复用一个更新接口来完成的。
3. 分类管理模块如何实现
3.1 页面入口
页面文件:
- src/main/resources/backend/page/category/list.html
接口封装文件:
- src/main/resources/backend/api/category.js
3.2 后端核心文件
- CategoryController.java
- CategoryService.java
- CategoryServiceImpl.java
- CategoryMapper.java
- Category.java
3.3 这个模块解决了什么问题
分类管理负责管理两种分类:
- 菜品分类
- 套餐分类
分类本身看起来很简单,但它会被:
- 菜品模块引用
- 套餐模块引用
所以分类管理后端最关键的地方不是"新增",而是"删除前的关联校验"。
3.4 分类分页如何实现
接口:
GET /category/page
实现逻辑:
- 创建分页对象
Page<Category> - 按
sort排序 - 调用
categoryService.page(...) - 返回分页结果
这个接口主要服务后台分类列表页。
3.5 分类列表查询如何实现
接口:
GET /category/list
实现逻辑:
- 如果前端传了
type - 就按分类类型筛选
- 先按
sort排 - 再按更新时间排
这个接口除了后台能用,前台商品展示时也会用到。
3.6 新增分类如何实现
接口:
POST /category
实现逻辑:
- 接收分类对象
- 手动设置创建时间和更新时间
- 从 session 里拿当前员工 id
- 写入创建人和更新人
- 保存到数据库
所以新增分类并不是只存一个"分类名字",还会附带记录:
- 谁创建的
- 什么时候创建的
3.7 修改分类如何实现
接口:
PUT /category
实现逻辑:
- 前端传分类对象
- 后端按 id 更新
3.8 删除分类为什么不能直接删
接口:
DELETE /category?id=...
真正关键的实现不在 Controller,而在:
- CategoryServiceImpl.java
删除前会做两步检查:
- 检查这个分类下有没有菜品
- 检查这个分类下有没有套餐
只要有关联,就抛出 CustomException,不给删。
为什么要这样做?
因为如果分类删掉了,但菜品或套餐还在引用这个分类,就会出现脏数据。
你可以把它理解成:
- 货架分类还在被商品使用
- 就不能先把货架标签撕掉
4. 菜品管理模块如何实现
4.1 页面入口
页面文件:
- src/main/resources/backend/page/food/list.html
- src/main/resources/backend/page/food/add.html
接口封装文件:
- src/main/resources/backend/api/food.js
4.2 后端核心文件
- DishController.java
- DishService.java
- DishServiceImpl.java
- DishFlavorService.java
- DishFlavorServiceImpl.java
- Dish.java
- DishFlavor.java
- DishDto.java
4.3 这个模块为什么比分类复杂
因为菜品管理不是单表业务,而是"两张表一起工作":
dish存菜品基本信息dish_flavor存菜品口味信息
所以菜品模块最重要的点是:
- 新增菜品时要同时存口味
- 修改菜品时要同时改口味
- 删除菜品时要同时删口味
这就是典型的"主表 + 子表"业务。
4.4 新增菜品如何实现
接口:
POST /dish
后端接收的不是普通 Dish,而是 DishDto。
原因是:
- 页面提交的不只是菜品本身
- 还包括口味列表
核心逻辑在:
- DishServiceImpl.java
实现过程:
- 先保存菜品主表
dish - 拿到新生成的菜品 id
- 遍历这次提交的口味列表
- 给每条口味补上
dishId - 批量保存到
dish_flavor
为什么要写在 Service 里?
因为这已经不是简单的"存一张表",而是一段完整业务流程。
4.5 菜品分页如何实现
接口:
GET /dish/page
实现逻辑:
- 按名称模糊搜索
dish - 按更新时间倒序排序
- 查到的是
Dish分页数据 - 但页面还想显示分类名
- 所以后端把
Dish转成DishDto - 再根据
categoryId查询分类名 - 填充到
dishDto.categoryName
所以这一步的本质是:
- 数据库里存的是
categoryId - 页面要看的却是"分类名称"
- 后端负责补充展示所需信息
4.6 编辑菜品回显如何实现
接口:
GET /dish/{id}
实现逻辑:
- 查菜品主表
- 查该菜品所有口味
- 合并成
DishDto - 返回给前端回显
4.7 修改菜品为什么不是一条 update 就结束
接口:
PUT /dish
核心逻辑在 updateWithFlavor(dishDto)。
实现过程:
- 更新
dish主表 - 删除原有全部口味
- 用前端最新提交的口味列表重新保存
为什么这样做?
因为口味列表可能:
- 增加了
- 删除了
- 改名了
如果逐条判断会很麻烦,所以这里直接采用:
- 先清空旧口味
- 再重建新口味
这种做法更简单,也更稳定。
4.8 菜品起售和停售如何实现
接口:
POST /dish/status/{statusNum}
实现逻辑:
- 前端传一组菜品 id
- 路径里传目标状态
- 后端用
LambdaUpdateWrapper批量更新状态
约定:
1表示起售0表示停售
4.9 删除菜品如何实现
接口:
DELETE /dish?ids=...
核心逻辑在 delByIdWithFlavor(ids)。
实现过程:
- 先检查待删除菜品里有没有正在售卖的
- 如果有,直接报错,不允许删
- 如果没有,删除
dish - 再删除关联的
dish_flavor
所以删除菜品的关键不是"删得掉",而是:
- 不能删正在售卖的
- 删除时要把口味一并清掉
5. 套餐管理模块如何实现
5.1 页面入口
页面文件:
- src/main/resources/backend/page/combo/list.html
- src/main/resources/backend/page/combo/add.html
接口封装文件:
- src/main/resources/backend/api/combo.js
5.2 后端核心文件
- SetmealController.java
- SetmealService.java
- SetmealServiceImpl.java
- SetmealDishService.java
- SetmealDish.java
- Setmeal.java
- SetmealDto.java
5.3 这个模块和菜品管理很像
套餐管理和菜品管理的思路非常像,也属于"主表 + 关系表"的结构:
setmeal存套餐本身setmeal_dish存套餐和菜品的对应关系
所以套餐模块的关键点也是:
- 新增套餐时不只存一张表
- 修改套餐时不只改一张表
- 删除套餐时不只删一张表
5.4 新增套餐如何实现
接口:
POST /setmeal
实现逻辑:
- 先保存套餐主表
setmeal - 拿到套餐 id
- 遍历这次提交的套餐菜品集合
- 给每条关系数据补上
setmealId - 批量保存到
setmeal_dish
5.5 套餐分页如何实现
接口:
GET /setmeal/page
实现逻辑:
- 按套餐名模糊查询
- 按更新时间倒序
- 查的是
Setmeal - 页面还需要分类名称
- 所以后端把结果转成
SetmealDto - 再补上
categoryName
5.6 编辑套餐回显如何实现
接口:
GET /setmeal/{id}
实现逻辑:
- 查套餐主表
- 查这个套餐里包含哪些菜品
- 合并成
SetmealDto - 返回给编辑页面
5.7 修改套餐如何实现
接口:
PUT /setmeal
核心逻辑在 updateWithDish(setmealDto)。
实现过程:
- 更新套餐主表
- 删除原有套餐和菜品的关系数据
- 按最新提交的结果重新建立关系
这和菜品模块"修改时重建口味"是同一个思路。
5.8 套餐起售和停售如何实现
接口:
POST /setmeal/status/{statusNum}
实现逻辑:
- 接收状态值
- 接收套餐 id 列表
- 批量更新套餐状态
5.9 删除套餐如何实现
接口:
DELETE /setmeal?ids=...
核心逻辑在 removeWithDish(ids)。
实现过程:
- 先检查套餐里有没有正在售卖的
- 如果有,不允许删
- 如果没有,删除套餐主表
- 再删除
setmeal_dish关系表
所以套餐模块的真正难点不是"写一个删除接口",而是:
- 先做状态校验
- 再维护关系表数据一致性
6. 订单明细模块如何实现
6.1 页面入口
后台左侧叫"订单明细",对应页面在:
- src/main/resources/backend/page/order/list.html
接口封装文件在:
- src/main/resources/backend/api/order.js
6.2 后端核心文件
- OrderController.java
- OrderService.java
- OrderServiceImpl.java
- OrderDetailService.java
- Orders.java
- OrderDetail.java
- OrdersDto.java
6.3 为什么这里叫"订单明细",后端却不只查明细表
因为后台订单页想展示的不是单独一张 order_detail 表,而是:
- 订单主信息
- 订单里包含的商品明细
所以后端实际做的是:
- 先查
orders - 再查每条订单对应的
order_detail - 最后合并成
OrdersDto
6.4 后台订单分页如何实现
接口:
GET /order/page
实现逻辑:
- 接收页码、每页条数、订单号、时间范围
- 先分页查询订单主表
orders - 支持按订单号模糊查询
- 支持按下单时间范围查询
- 把每条订单转成
OrdersDto - 再为每条订单单独查询
OrderDetail列表 - 把这个列表塞进
ordersDto.orderDetails - 最后把组装好的分页结果返回给后台页面
所以后台"订单明细"页面看到的其实是:
- 订单表数据
- 订单下的商品数据
一起拼出来的结果。
6.5 修改订单状态如何实现
接口:
PUT /order
实现逻辑:
- 前端传订单 id 和新状态
- 后端先查这个订单是否存在
- 不存在就返回错误
- 存在就新建一个只包含
id和status的对象 - 只更新订单状态
为什么不直接拿前端传回来的整个订单对象做更新?
因为订单字段很多,前端不一定全传。
如果整对象覆盖,可能误把别的字段改掉。
所以这里故意采用"只更新状态"的安全写法。
6.6 为什么还要讲 submit 方法
虽然后台左侧菜单叫"订单明细",但后台看到的订单数据,最早是前台下单时生成的。
所以要真正明白订单模块,必须知道订单是怎么来的。
核心方法:
OrderServiceImpl.submit(Orders orders)
实现过程:
- 先查当前用户购物车
- 如果购物车为空,就不允许下单
- 用雪花算法生成订单号
- 计算订单总金额
- 从地址表中补齐收货信息
- 从用户表中补齐用户名
- 保存订单主表
- 把购物车数据复制成订单明细数据
- 批量保存订单明细
- 清空购物车
所以后台订单模块能查到订单,不是凭空出现的,而是前台下单流程先把订单主表和订单明细表都写好了。
6.7 用户端订单分页如何实现
接口:
GET /order/userPage
虽然这不是后台左侧菜单直接点的接口,但它和订单明细模块是同一套后端数据。
实现逻辑:
- 只查当前用户自己的订单
- 分页查询订单主表
- 再查每条订单的订单明细
- 组合成
OrdersDto
也就是说,后台订单页和前台"我的订单"页,底层思路是一样的,都是:
- 订单主表 + 订单明细表一起组装
详情见文件
- EmployeeController.java
先理解最基础的登录、分页、新增、修改接口写法 - CategoryController.java 和 CategoryServiceImpl.java
看"Controller 简单,真正校验在 Service"的写法 - DishController.java 和 DishServiceImpl.java
看 DTO、主子表、事务是怎么配合的 - SetmealController.java 和 SetmealServiceImpl.java
看套餐和菜品关系表是怎么维护的 - OrderController.java 和 OrderServiceImpl.java
看订单主表、订单明细表、购物车、地址、用户信息是怎么串起来的
做的优化
redis保存登录状态,即系统重启后输入系统网址登录状态仍然保留不用重新登录
原来为什么服务一重启就掉登录
原来的登录代码虽然已经把员工 id 和用户 id 放进了 <font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">session</font>:
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">request.getSession().setAttribute("employee", emp.getId())</font><font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">session.setAttribute("user", user.getId())</font>
但是默认情况下,这个 <font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">session</font> 是保存在当前应用进程内存里的。
这就意味着:
- 用户登录成功
- session 里暂时记住了"这个人是谁"
- 一旦 Spring Boot 服务重启
- 原来内存里的 session 全没了
- 过滤器再去读
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">session</font>时就读不到登录信息 - 页面就会重新跳回登录页
9.2 这次优化的核心思路
这次没有推翻原来的登录逻辑,而是只替换了 session 的存储位置:
- 以前:session 默认保存在 Tomcat 内存
- 现在:session 改成保存在 Redis
9.3 具体改了哪些地方
1. 增加 Spring Session Redis 依赖
修改文件:
- pom.xml
新增依赖:
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">spring-session-data-redis</font>
xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2. 增加 session 配置
修改文件:
- src/main/resources/application.yml
这次增加了两类配置:
- session 超时时间
plain
server:
servlet:
session:
timeout: 7d
- 只要 7 天内还在用,这个登录状态就可以继续保留
- session 存储方式和 Redis 命名空间
plain
spring:
session:
store-type: redis
redis:
namespace: reggie:session
- 明确告诉 Spring:session 不要再只存在本机内存,要存到 Redis
- 在 Redis 里用
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">reggie:session</font>作为会话数据前缀,方便区分
3. 新增 Redis Session 配置类
新增文件:
- src/main/java/com/itheima/reggie/config/RedisSessionConfig.java
java
package com.itheima.reggie.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* Redis Session 配置。
* 作用是把原来默认保存在 Tomcat 内存里的 session,改成保存在 Redis 中。
* 这样服务重启后,只要 Redis 里的会话数据还没过期,浏览器就还能继续保持登录状态。
*/
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 7 * 24 * 60 * 60, redisNamespace = "reggie:session")
public class RedisSessionConfig {
/**
* 把 session 对应的浏览器 cookie 也设置成可持久化,避免浏览器一关就丢。
* 这里和 Redis 中 session 的过期时间统一设置为 7 天。
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookieMaxAge(7 * 24 * 60 * 60);
serializer.setUseHttpOnlyCookie(true);
serializer.setSameSite("Lax");
return serializer;
}
}
- 用
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">@EnableRedisHttpSession(...)</font>开启 Redis 版 HttpSession - 配置浏览器里的 session cookie 也持久化保存一段时间
9.4 为什么现有登录代码几乎不用改
因为这次优化保留了原来的接口习惯。
比如员工登录时还是:
- EmployeeController.java
里面这句核心逻辑没变:
plain
request.getSession().setAttribute("employee", emp.getId());
前台用户登录时还是:
- UserController.java
里面这句核心逻辑没变:
plain
session.setAttribute("user", user.getId());
变化只在于:
- 以前这两句把数据放进本地内存 session
- 现在这两句背后会自动把数据存进 Redis Session
所以你可以把这次优化理解成:
- 表面上代码看起来差不多
- 实际上底层存储已经换成了更稳的 Redis
9.5 登录过滤器为什么也自动受益
过滤器文件:
- src/main/java/com/itheima/reggie/filter/LoginCheckFilter.java
它原来就是这样判断登录的:
- 先读
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">request.getSession().getAttribute("employee")</font> - 或者读
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">request.getSession().getAttribute("user")</font>
现在因为 session 已经变成 Redis 托管,所以过滤器虽然代码几乎没变,但读取到的数据来源已经不一样了:
- 以前读的是本机内存
- 现在读的是 Redis 中保存的会话信息
java
if (request.getSession().getAttribute("employee") != null) {
// 这里读取到的 session 数据现在来自 Redis,不再依赖单机内存。
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
return;
}
//4-2、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("user") != null){
// 前台用户登录状态也走同一套 Redis Session 机制。
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
9.6 这次优化完成后的效果
现在登录状态的保存逻辑变成了这样:
- 用户登录成功
- 后端把员工 id 或用户 id 写进 session
- 这个 session 由 Spring Session 自动保存到 Redis
- 浏览器保存对应的
<font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">JSESSIONID</font> - 就算 Spring Boot 服务重启
- 只要 Redis 里的会话还没过期,浏览器再次请求时仍能识别出已登录用户
所以最终效果就是:
- 服务重启后,不需要立刻重新登录
- 可以继续直接进入系统使用