瑞吉外卖项目
项目地址https://gitee.com/meiyouname/reggie_project我优化加redis记得打开详情见最后运行项目导入sql文件至mysql数据库打开application.yml文件修改数据库连接信息修改静态资源目录运行项目访问项目后台管理外卖平台: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 改成了下面这套编号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.htmlsrc/main/resources/backend/page/member/add.htmlsrc/main/resources/backend/page/food/list.htmlsrc/main/resources/backend/page/food/add.htmlsrc/main/resources/backend/page/combo/list.htmlsrc/main/resources/backend/page/combo/add.html修正结果员工管理相关页面统一使用菜单 id 1菜品管理相关页面统一使用菜单 id 3套餐管理相关页面统一使用菜单 id 42照片添加菜品管理本来是没有照片的于是在application.yml里面加上# 常量配置存放图片的目录reggie:path:E:/J2EE/code/chapter10/reggie/src/main/resources/backend/images/以及去网上找到了图片把对应图片复制黏贴到了文件夹里面3修正了员工管理接口前端员工管理页面已经写好了,但后端 EmployeeController 里原本只有登录和退出接口后端只实现了POST /employee/loginPOST /employee/logout但前端员工管理页面实际还会调用GET /employee/pagePOST /employeeGET /employee/{id}PUT /employee所以如果只恢复菜单不补接口 点击员工管理之后仍然会继续报错主要修改文件src/main/java/com/itheima/reggie/controller/EmployeeController.javaGET /employee/page用途给后台“员工管理”列表页做分页查询功能说明支持员工列表分页支持按姓名模糊搜索结果按更新时间倒序显示这就能满足员工列表页的基本展示需求。GetMapping(/page)publicRPageEmployeepage(intpage,intpageSize,Stringname){PageEmployeepageInfonewPage(page,pageSize);LambdaQueryWrapperEmployeequeryWrappernewLambdaQueryWrapper();queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);queryWrapper.orderByDesc(Employee::getUpdateTime);employeeService.page(pageInfo,queryWrapper);returnR.success(pageInfo);}POST /employee用途新增员工最终保留的逻辑包括给新员工设置默认密码 123456用 MD5 对默认密码加密默认状态设为启用调用 employeeService.save(…) 保存/** * 新增员工。 * 前端没有单独输入密码所以这里给新员工一个统一默认密码 123456。 */PostMappingpublicRStringsave(RequestBodyEmployeeemployee){employee.setPassword(DigestUtils.md5DigestAsHex(123456.getBytes()));employee.setStatus(1);employeeService.save(employee);returnR.success(新增员工成功);}GET /employee/{id}用途给“编辑员工”页面做数据回显最终保留的逻辑包括根据员工 id 查询数据如果查不到返回“员工信息不存在”如果查到了在返回前把密码字段清空GetMapping(/{id})publicREmployeegetById(PathVariableLongid){EmployeeemployeeemployeeService.getById(id);if(employeenull){returnR.error(员工信息不存在);}// 回显时不把密码直接返回给前端更安全一些。employee.setPassword(null);returnR.success(employee);}PUT /employee用途修改员工资料启用员工禁用员工最终保留的逻辑是统一使用 employeeService.updateById(employee)/** * 修改员工信息。 * 员工资料修改、启用、禁用都统一走这个接口。 */PutMappingpublicRStringupdate(RequestBodyEmployeeemployee){employeeService.updateById(employee);returnR.success(员工信息修改成功);}还修改了src/main/resources/backend/index.htmlsrc/main/resources/backend/page/member/list.htmlsrc/main/resources/backend/page/member/add.html项目不同模块如何实现的讲后台左侧这五个模块是怎么实现的员工管理分类管理菜品管理套餐管理订单明细1. 先理解这五个模块共同的后端套路这五个模块虽然功能不同但后端实现套路基本一致都是下面这条链路后台页面 → 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 的PageT。1.4 DTO 的作用有些页面展示的数据不只来自一张表所以项目里会用 DTO。本次五个模块里最重要的 DTO 是DishDto.javaSetmealDto.javaOrdersDto.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.js2.2 后端核心文件EmployeeController.javaEmployeeService.javaEmployeeServiceImpl.javaEmployeeMapper.javaEmployee.java2.3 这个模块解决了什么问题员工管理模块主要负责员工登录员工退出员工分页查询新增员工查询单个员工信息修改员工信息也就是说后台左侧看到的“员工管理”本质上不是一个按钮而是后端提供的一组接口共同支撑出来的。2.4 员工登录如何实现接口POST /employee/login实现文件EmployeeController.java实现过程页面提交用户名和密码后端把密码做一次MD5根据用户名查询员工表比较密码是否一致判断账号状态是不是禁用登录成功后把员工 id 存进 session 的employee为什么要存 session因为后面访问分类、菜品、套餐、订单这些后台模块时过滤器就靠 session 判断你是不是已经登录。2.5 员工列表如何实现接口GET /employee/page实现逻辑接收page、pageSize、name用PageEmployee做分页对象如果传了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.js3.2 后端核心文件CategoryController.javaCategoryService.javaCategoryServiceImpl.javaCategoryMapper.javaCategory.java3.3 这个模块解决了什么问题分类管理负责管理两种分类菜品分类套餐分类分类本身看起来很简单但它会被菜品模块引用套餐模块引用所以分类管理后端最关键的地方不是“新增”而是“删除前的关联校验”。3.4 分类分页如何实现接口GET /category/page实现逻辑创建分页对象PageCategory按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.htmlsrc/main/resources/backend/page/food/add.html接口封装文件src/main/resources/backend/api/food.js4.2 后端核心文件DishController.javaDishService.javaDishServiceImpl.javaDishFlavorService.javaDishFlavorServiceImpl.javaDish.javaDishFlavor.javaDishDto.java4.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.htmlsrc/main/resources/backend/page/combo/add.html接口封装文件src/main/resources/backend/api/combo.js5.2 后端核心文件SetmealController.javaSetmealService.javaSetmealServiceImpl.javaSetmealDishService.javaSetmealDish.javaSetmeal.javaSetmealDto.java5.3 这个模块和菜品管理很像套餐管理和菜品管理的思路非常像也属于“主表 关系表”的结构setmeal存套餐本身setmeal_dish存套餐和菜品的对应关系所以套餐模块的关键点也是新增套餐时不只存一张表修改套餐时不只改一张表删除套餐时不只删一张表5.4 新增套餐如何实现接口POST /setmeal实现逻辑先保存套餐主表setmeal拿到套餐 id遍历这次提交的套餐菜品集合给每条关系数据补上setmealId批量保存到setmeal_dish5.5 套餐分页如何实现接口GET /setmeal/page实现逻辑按套餐名模糊查询按更新时间倒序查的是Setmeal页面还需要分类名称所以后端把结果转成SetmealDto再补上categoryName5.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.js6.2 后端核心文件OrderController.javaOrderService.javaOrderServiceImpl.javaOrderDetailService.javaOrders.javaOrderDetail.javaOrdersDto.java6.3 为什么这里叫“订单明细”后端却不只查明细表因为后台订单页想展示的不是单独一张order_detail表而是订单主信息订单里包含的商品明细所以后端实际做的是先查orders再查每条订单对应的order_detail最后合并成OrdersDto6.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 stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);session/fontfont stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);request.getSession().setAttribute(employee, emp.getId())/fontfont stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);session.setAttribute(user, user.getId())/font但是默认情况下这个font stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);session/font是保存在当前应用进程内存里的。这就意味着用户登录成功session 里暂时记住了“这个人是谁”一旦 Spring Boot 服务重启原来内存里的 session 全没了过滤器再去读font stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);session/font时就读不到登录信息页面就会重新跳回登录页9.2 这次优化的核心思路这次没有推翻原来的登录逻辑而是只替换了 session 的存储位置以前session 默认保存在 Tomcat 内存现在session 改成保存在 Redis9.3 具体改了哪些地方1. 增加 Spring Session Redis 依赖修改文件pom.xml新增依赖font stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);spring-session-data-redis/fontdependencygroupIdorg.springframework.session/groupIdartifactIdspring-session-data-redis/artifactId/dependency2. 增加 session 配置修改文件src/main/resources/application.yml这次增加了两类配置session 超时时间server: servlet: session: timeout: 7d只要 7 天内还在用这个登录状态就可以继续保留session 存储方式和 Redis 命名空间spring: session: store-type: redis redis: namespace: reggie:session明确告诉 Springsession 不要再只存在本机内存要存到 Redis在 Redis 里用font stylecolor: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.javapackagecom.itheima.reggie.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;importorg.springframework.session.web.http.CookieSerializer;importorg.springframework.session.web.http.DefaultCookieSerializer;/** * Redis Session 配置。 * 作用是把原来默认保存在 Tomcat 内存里的 session改成保存在 Redis 中。 * 这样服务重启后只要 Redis 里的会话数据还没过期浏览器就还能继续保持登录状态。 */ConfigurationEnableRedisHttpSession(maxInactiveIntervalInSeconds7*24*60*60,redisNamespacereggie:session)publicclassRedisSessionConfig{/** * 把 session 对应的浏览器 cookie 也设置成可持久化避免浏览器一关就丢。 * 这里和 Redis 中 session 的过期时间统一设置为 7 天。 */BeanpublicCookieSerializercookieSerializer(){DefaultCookieSerializerserializernewDefaultCookieSerializer();serializer.setCookieName(JSESSIONID);serializer.setCookieMaxAge(7*24*60*60);serializer.setUseHttpOnlyCookie(true);serializer.setSameSite(Lax);returnserializer;}}用font stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);EnableRedisHttpSession(...)/font开启 Redis 版 HttpSession配置浏览器里的 session cookie 也持久化保存一段时间9.4 为什么现有登录代码几乎不用改因为这次优化保留了原来的接口习惯。比如员工登录时还是EmployeeController.java里面这句核心逻辑没变request.getSession().setAttribute(employee, emp.getId());前台用户登录时还是UserController.java里面这句核心逻辑没变session.setAttribute(user, user.getId());变化只在于以前这两句把数据放进本地内存 session现在这两句背后会自动把数据存进 Redis Session所以你可以把这次优化理解成表面上代码看起来差不多实际上底层存储已经换成了更稳的 Redis9.5 登录过滤器为什么也自动受益过滤器文件src/main/java/com/itheima/reggie/filter/LoginCheckFilter.java它原来就是这样判断登录的先读font stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);request.getSession().getAttribute(employee)/font或者读font stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);request.getSession().getAttribute(user)/font现在因为 session 已经变成 Redis 托管所以过滤器虽然代码几乎没变但读取到的数据来源已经不一样了以前读的是本机内存现在读的是 Redis 中保存的会话信息if(request.getSession().getAttribute(employee)!null){// 这里读取到的 session 数据现在来自 Redis不再依赖单机内存。LongempId(Long)request.getSession().getAttribute(employee);BaseContext.setCurrentId(empId);filterChain.doFilter(request,response);return;}//4-2、判断登录状态如果已登录则直接放行if(request.getSession().getAttribute(user)!null){// 前台用户登录状态也走同一套 Redis Session 机制。LonguserId(Long)request.getSession().getAttribute(user);BaseContext.setCurrentId(userId);filterChain.doFilter(request,response);return;}9.6 这次优化完成后的效果现在登录状态的保存逻辑变成了这样用户登录成功后端把员工 id 或用户 id 写进 session这个 session 由 Spring Session 自动保存到 Redis浏览器保存对应的font stylecolor:rgb(8, 8, 8);background-color:rgba(212, 222, 231, 0.247);JSESSIONID/font就算 Spring Boot 服务重启只要 Redis 里的会话还没过期浏览器再次请求时仍能识别出已登录用户所以最终效果就是服务重启后不需要立刻重新登录可以继续直接进入系统使用