瑞吉外卖项目

项目地址: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

  1. 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);
    }
  1. 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("新增员工成功");
    }
  1. 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);
    }
  1. 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. 员工管理
  2. 分类管理
  3. 菜品管理
  4. 套餐管理
  5. 订单明细

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 这个模块解决了什么问题

员工管理模块主要负责:

  1. 员工登录
  2. 员工退出
  3. 员工分页查询
  4. 新增员工
  5. 查询单个员工信息
  6. 修改员工信息

也就是说,后台左侧看到的"员工管理",本质上不是一个按钮,而是后端提供的一组接口共同支撑出来的。

2.4 员工登录如何实现

接口:

  • POST /employee/login

实现文件:

  • EmployeeController.java

实现过程:

  1. 页面提交用户名和密码
  2. 后端把密码做一次 MD5
  3. 根据用户名查询员工表
  4. 比较密码是否一致
  5. 判断账号状态是不是禁用
  6. 登录成功后把员工 id 存进 session 的 employee

为什么要存 session?

因为后面访问分类、菜品、套餐、订单这些后台模块时,过滤器就靠 session 判断你是不是已经登录。

2.5 员工列表如何实现

接口:

  • GET /employee/page

实现逻辑:

  1. 接收 pagepageSizename
  2. Page<Employee> 做分页对象
  3. 如果传了 name,就按员工姓名模糊查询
  4. 按更新时间倒序排序
  5. 调用 employeeService.page(...)
  6. 把分页结果返回给前端

所以页面才能实现:

  • 搜索员工
  • 翻页
  • 展示每一页数据

2.6 新增员工如何实现

接口:

  • POST /employee

实现逻辑:

  1. 页面提交员工资料
  2. 后端不给页面机会直接传密码
  3. 后端统一给新员工默认密码 123456
  4. 再把这个默认密码做 MD5
  5. 默认状态设置为启用
  6. 保存到员工表

这么做的好处是:

  • 页面更简单
  • 后台新增员工流程更统一

2.7 编辑员工回显如何实现

接口:

  • GET /employee/{id}

实现逻辑:

  1. 根据员工 id 查询员工
  2. 如果查不到,返回"员工信息不存在"
  3. 如果查到了,把密码字段清空
  4. 再返回给前端做表单回显

为什么要把密码清空?

因为回显页面不需要看到数据库里的密码密文,直接返回不安全。

2.8 修改员工如何实现

接口:

  • PUT /employee

实现逻辑:

  1. 页面把修改后的员工对象提交回来
  2. 后端按 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 这个模块解决了什么问题

分类管理负责管理两种分类:

  1. 菜品分类
  2. 套餐分类

分类本身看起来很简单,但它会被:

  • 菜品模块引用
  • 套餐模块引用

所以分类管理后端最关键的地方不是"新增",而是"删除前的关联校验"。

3.4 分类分页如何实现

接口:

  • GET /category/page

实现逻辑:

  1. 创建分页对象 Page<Category>
  2. sort 排序
  3. 调用 categoryService.page(...)
  4. 返回分页结果

这个接口主要服务后台分类列表页。

3.5 分类列表查询如何实现

接口:

  • GET /category/list

实现逻辑:

  1. 如果前端传了 type
  2. 就按分类类型筛选
  3. 先按 sort
  4. 再按更新时间排

这个接口除了后台能用,前台商品展示时也会用到。

3.6 新增分类如何实现

接口:

  • POST /category

实现逻辑:

  1. 接收分类对象
  2. 手动设置创建时间和更新时间
  3. 从 session 里拿当前员工 id
  4. 写入创建人和更新人
  5. 保存到数据库

所以新增分类并不是只存一个"分类名字",还会附带记录:

  • 谁创建的
  • 什么时候创建的

3.7 修改分类如何实现

接口:

  • PUT /category

实现逻辑:

  1. 前端传分类对象
  2. 后端按 id 更新

3.8 删除分类为什么不能直接删

接口:

  • DELETE /category?id=...

真正关键的实现不在 Controller,而在:

  • CategoryServiceImpl.java

删除前会做两步检查:

  1. 检查这个分类下有没有菜品
  2. 检查这个分类下有没有套餐

只要有关联,就抛出 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 这个模块为什么比分类复杂

因为菜品管理不是单表业务,而是"两张表一起工作":

  1. dish 存菜品基本信息
  2. dish_flavor 存菜品口味信息

所以菜品模块最重要的点是:

  • 新增菜品时要同时存口味
  • 修改菜品时要同时改口味
  • 删除菜品时要同时删口味

这就是典型的"主表 + 子表"业务。

4.4 新增菜品如何实现

接口:

  • POST /dish

后端接收的不是普通 Dish,而是 DishDto

原因是:

  • 页面提交的不只是菜品本身
  • 还包括口味列表

核心逻辑在:

  • DishServiceImpl.java

实现过程:

  1. 先保存菜品主表 dish
  2. 拿到新生成的菜品 id
  3. 遍历这次提交的口味列表
  4. 给每条口味补上 dishId
  5. 批量保存到 dish_flavor

为什么要写在 Service 里?

因为这已经不是简单的"存一张表",而是一段完整业务流程。

4.5 菜品分页如何实现

接口:

  • GET /dish/page

实现逻辑:

  1. 按名称模糊搜索 dish
  2. 按更新时间倒序排序
  3. 查到的是 Dish 分页数据
  4. 但页面还想显示分类名
  5. 所以后端把 Dish 转成 DishDto
  6. 再根据 categoryId 查询分类名
  7. 填充到 dishDto.categoryName

所以这一步的本质是:

  • 数据库里存的是 categoryId
  • 页面要看的却是"分类名称"
  • 后端负责补充展示所需信息

4.6 编辑菜品回显如何实现

接口:

  • GET /dish/{id}

实现逻辑:

  1. 查菜品主表
  2. 查该菜品所有口味
  3. 合并成 DishDto
  4. 返回给前端回显

4.7 修改菜品为什么不是一条 update 就结束

接口:

  • PUT /dish

核心逻辑在 updateWithFlavor(dishDto)

实现过程:

  1. 更新 dish 主表
  2. 删除原有全部口味
  3. 用前端最新提交的口味列表重新保存

为什么这样做?

因为口味列表可能:

  • 增加了
  • 删除了
  • 改名了

如果逐条判断会很麻烦,所以这里直接采用:

  • 先清空旧口味
  • 再重建新口味

这种做法更简单,也更稳定。

4.8 菜品起售和停售如何实现

接口:

  • POST /dish/status/{statusNum}

实现逻辑:

  1. 前端传一组菜品 id
  2. 路径里传目标状态
  3. 后端用 LambdaUpdateWrapper 批量更新状态

约定:

  • 1 表示起售
  • 0 表示停售

4.9 删除菜品如何实现

接口:

  • DELETE /dish?ids=...

核心逻辑在 delByIdWithFlavor(ids)

实现过程:

  1. 先检查待删除菜品里有没有正在售卖的
  2. 如果有,直接报错,不允许删
  3. 如果没有,删除 dish
  4. 再删除关联的 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 这个模块和菜品管理很像

套餐管理和菜品管理的思路非常像,也属于"主表 + 关系表"的结构:

  1. setmeal 存套餐本身
  2. setmeal_dish 存套餐和菜品的对应关系

所以套餐模块的关键点也是:

  • 新增套餐时不只存一张表
  • 修改套餐时不只改一张表
  • 删除套餐时不只删一张表

5.4 新增套餐如何实现

接口:

  • POST /setmeal

实现逻辑:

  1. 先保存套餐主表 setmeal
  2. 拿到套餐 id
  3. 遍历这次提交的套餐菜品集合
  4. 给每条关系数据补上 setmealId
  5. 批量保存到 setmeal_dish

5.5 套餐分页如何实现

接口:

  • GET /setmeal/page

实现逻辑:

  1. 按套餐名模糊查询
  2. 按更新时间倒序
  3. 查的是 Setmeal
  4. 页面还需要分类名称
  5. 所以后端把结果转成 SetmealDto
  6. 再补上 categoryName

5.6 编辑套餐回显如何实现

接口:

  • GET /setmeal/{id}

实现逻辑:

  1. 查套餐主表
  2. 查这个套餐里包含哪些菜品
  3. 合并成 SetmealDto
  4. 返回给编辑页面

5.7 修改套餐如何实现

接口:

  • PUT /setmeal

核心逻辑在 updateWithDish(setmealDto)

实现过程:

  1. 更新套餐主表
  2. 删除原有套餐和菜品的关系数据
  3. 按最新提交的结果重新建立关系

这和菜品模块"修改时重建口味"是同一个思路。

5.8 套餐起售和停售如何实现

接口:

  • POST /setmeal/status/{statusNum}

实现逻辑:

  1. 接收状态值
  2. 接收套餐 id 列表
  3. 批量更新套餐状态

5.9 删除套餐如何实现

接口:

  • DELETE /setmeal?ids=...

核心逻辑在 removeWithDish(ids)

实现过程:

  1. 先检查套餐里有没有正在售卖的
  2. 如果有,不允许删
  3. 如果没有,删除套餐主表
  4. 再删除 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 表,而是:

  1. 订单主信息
  2. 订单里包含的商品明细

所以后端实际做的是:

  • 先查 orders
  • 再查每条订单对应的 order_detail
  • 最后合并成 OrdersDto

6.4 后台订单分页如何实现

接口:

  • GET /order/page

实现逻辑:

  1. 接收页码、每页条数、订单号、时间范围
  2. 先分页查询订单主表 orders
  3. 支持按订单号模糊查询
  4. 支持按下单时间范围查询
  5. 把每条订单转成 OrdersDto
  6. 再为每条订单单独查询 OrderDetail 列表
  7. 把这个列表塞进 ordersDto.orderDetails
  8. 最后把组装好的分页结果返回给后台页面

所以后台"订单明细"页面看到的其实是:

  • 订单表数据
  • 订单下的商品数据

一起拼出来的结果。

6.5 修改订单状态如何实现

接口:

  • PUT /order

实现逻辑:

  1. 前端传订单 id 和新状态
  2. 后端先查这个订单是否存在
  3. 不存在就返回错误
  4. 存在就新建一个只包含 idstatus 的对象
  5. 只更新订单状态

为什么不直接拿前端传回来的整个订单对象做更新?

因为订单字段很多,前端不一定全传。

如果整对象覆盖,可能误把别的字段改掉。

所以这里故意采用"只更新状态"的安全写法。

6.6 为什么还要讲 submit 方法

虽然后台左侧菜单叫"订单明细",但后台看到的订单数据,最早是前台下单时生成的。

所以要真正明白订单模块,必须知道订单是怎么来的。

核心方法:

  • OrderServiceImpl.submit(Orders orders)

实现过程:

  1. 先查当前用户购物车
  2. 如果购物车为空,就不允许下单
  3. 用雪花算法生成订单号
  4. 计算订单总金额
  5. 从地址表中补齐收货信息
  6. 从用户表中补齐用户名
  7. 保存订单主表
  8. 把购物车数据复制成订单明细数据
  9. 批量保存订单明细
  10. 清空购物车

所以后台订单模块能查到订单,不是凭空出现的,而是前台下单流程先把订单主表和订单明细表都写好了。

6.7 用户端订单分页如何实现

接口:

  • GET /order/userPage

虽然这不是后台左侧菜单直接点的接口,但它和订单明细模块是同一套后端数据。

实现逻辑:

  1. 只查当前用户自己的订单
  2. 分页查询订单主表
  3. 再查每条订单的订单明细
  4. 组合成 OrdersDto

也就是说,后台订单页和前台"我的订单"页,底层思路是一样的,都是:

  • 订单主表 + 订单明细表一起组装

详情见文件

  1. EmployeeController.java
    先理解最基础的登录、分页、新增、修改接口写法
  2. CategoryController.java 和 CategoryServiceImpl.java
    看"Controller 简单,真正校验在 Service"的写法
  3. DishController.java 和 DishServiceImpl.java
    看 DTO、主子表、事务是怎么配合的
  4. SetmealController.java 和 SetmealServiceImpl.java
    看套餐和菜品关系表是怎么维护的
  5. 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> 是保存在当前应用进程内存里的。

这就意味着:

  1. 用户登录成功
  2. session 里暂时记住了"这个人是谁"
  3. 一旦 Spring Boot 服务重启
  4. 原来内存里的 session 全没了
  5. 过滤器再去读 <font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">session</font> 时就读不到登录信息
  6. 页面就会重新跳回登录页

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

这次增加了两类配置:

  1. session 超时时间
plain 复制代码
server:
  servlet:
    session:
      timeout: 7d
  • 只要 7 天内还在用,这个登录状态就可以继续保留
  1. 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;
    }
}
  1. <font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">@EnableRedisHttpSession(...)</font> 开启 Redis 版 HttpSession
  2. 配置浏览器里的 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 这次优化完成后的效果

现在登录状态的保存逻辑变成了这样:

  1. 用户登录成功
  2. 后端把员工 id 或用户 id 写进 session
  3. 这个 session 由 Spring Session 自动保存到 Redis
  4. 浏览器保存对应的 <font style="color:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);">JSESSIONID</font>
  5. 就算 Spring Boot 服务重启
  6. 只要 Redis 里的会话还没过期,浏览器再次请求时仍能识别出已登录用户

所以最终效果就是:

  • 服务重启后,不需要立刻重新登录
  • 可以继续直接进入系统使用
相关推荐
逍遥德7 小时前
Java编程高频的“技术点”-03:“下划线命名”参数,后端用“驼峰命名“接收
java·后端·springboot
弹简特11 小时前
【Java项目-轻聊】08-用户管理模块-实现获取用户信息+头像上传+显示头像
java·开发语言·springboot
行者-全栈开发3 天前
SpringBoot CI/CD 流水线实战|Jenkins+GitLab CI,从手动到自动化交付
ci/cd·jenkins·springboot·devops·自动化部署·gitlab ci
华大哥5 天前
前后端分离实现五级行政区划树形菜单及设备查询管理
sqlite·vue·springboot
码哥字节5 天前
升到 Spring Boot 4.1,虚拟线程开了,HikariCP 连接池却崩了
java·springboot·claude code
极光代码工作室6 天前
基于SpringBoot的校园论坛系统
java·springboot·web开发·后端开发
源码宝7 天前
MES系统源码:Java8 + SpringBoot2.7 + MySQL8 + Redis,后端源码清爽易扩展
java·后端·源码·springboot·mes系统·源码二开·mes源码
MaCa .BaKa8 天前
55-宠物爱心救助领养系统-宠物救助领养系统
java·vue.js·tomcat·maven·springboot·宠物救助领养系统
苏渡苇8 天前
Spring Cloud Gateway 网关限流
spring cloud·gateway·springboot·网关限流