☰
瑞吉外卖项目实战:Spring Boot+MyBatis-Plus+Redis全栈解析
2026/10/21 4:46:36 网站建设 项目流程

简介:面向Java开发学习者的瑞吉外卖项目源码及数据库资源,是一套基于SpringBoot与MyBatis-Plus的外卖平台实战案例,可在Linux环境部署运行。项目覆盖用户注册登录、下单、支付等核心流程,将短信验证优化为邮箱验证,并引入Redis缓存提升响应速度。同时采用主从数据库保存数据,兼顾读写性能与数据安全。资源包共228个文件,含69个Java源码、52张图片、22个JS、21个HTML、18个CSS,以及SQL脚本、YAML配置、字体与图标文件,整体32.01MB;前后端代码按目录归类,便于模块化学习,从界面展示到数据持久化均有完整实现。目前已有5187人学习浏览,适合正在学习上述技术栈的开发者,亦可作为课程设计与毕业设计参考。读者还可对照源码理解业务分层、数据库表设计、缓存运用及Linux部署等完整开发链路,为高并发场景积累实战经验。

1. 瑞吉外卖项目源码及数据库资源:拿到手先把这三件事做对

拿到这份瑞吉外卖项目源码及数据库资源之后,别急着看代码,先把数据库脚本导进去、把 Redis 启动起来、再确认 JDK 和 Maven 版本,顺序错了后面全是玄学报错。这是一个典型的 Spring Boot + MyBatis-Plus 单体外卖系统:后台管员工、分类、菜品、套餐和订单,C 端管用户登录、地址簿、购物车和下单,前后端静态页面都打包在同一个 jar 里。

适合两类人:刚学完 SSM、想看真实业务流程怎么串起来的新手,以及准备在 Linux 上做一次完整部署演练的开发者。系统难点不在单个类,而在登录拦截、Redis 菜品缓存和下单事务环绕起来的业务闭环,读的时候先盯这三处。

2. 技术栈与整体架构:Spring Boot + MyBatis-Plus 的分层设计

这套系统没有上微服务,也没拆前后端两个工程,而是把后台管理端和 C 端 H5 页面都放进同一个 Spring Boot 工程,最后产出一个可执行 jar。对学习者和部署者来说这是最友好的形态:依赖少、启动快、在 Linux 上出了问题也好定位。你拿到源码后,第一遍浏览不要进细节,先把技术栈、目录和数据库模型三件事对齐。

2.1 技术栈速览:这套组合解决了什么问题

层次技术承担的职责
应用框架Spring Boot内嵌 Tomcat,统一管理 Bean 和配置
持久层MyBatis-Plus单表 CRUD 免写 SQL,分页走插件
数据库MySQL员工、菜品、套餐、订单等业务数据
缓存RedisC 端菜品列表缓存,降低数据库压力
构建部署Maven + Linux打包成可执行 jar,后台常驻运行

MySQL 5.7 和 8.0 都能跑,区别主要在 JDBC 驱动类名;Redis 也不需要哨兵集群,单机默认配置足够。选 MyBatis-Plus 而不是纯手写 MyBatis XML,是因为 employee、category、dish 这些表的增删改查几乎全是单表操作,Service 继承 IService 就能拿到 save、removeById、page 方法,省掉大量重复 SQL。唯一需要手写 XML 的是订单统计这类多表聚合查询,占整个项目很小一部分。

用 MyBatis-Plus 还有个隐藏收益:配置 MetaObjectHandler 之后,createTime、updateTime、createUser、updateUser 这类公共字段会在 insert 和 update 时自动填充,Service 层不用每个方法手动 set,这个细节在代码里到处都是。常见误用是仍然按 MyBatis 的习惯给每个方法配一个 XML 文件,文件数量翻倍但逻辑没变。

Redis 在这套系统里不是用来做分布式锁、也不是存 Session,核心场景只有一个:C 端菜品列表按分类缓存。菜品是典型的读多写少数据,运营改菜品时又不能等 TTL 自然过期,所以缓存要设计成按分类维度区分 key、更新时主动删除,而不是一个全局 key 一把梭。第三章会专门拆这段。

2.2 目录结构:几百个类里先认清这几个包

ruiji/ # 源码根目录:后端与前端都在这里 ├── src/main/java/com/ruiji/ │ ├── common/ # 统一返回结果 R、全局异常处理、ThreadLocal 用户上下文 │ ├── config/ # 拦截器注册、Redis 序列化、MyBatis-Plus 分页插件 │ ├── controller/ # 后台与 C 端接口,按业务模块拆分 │ ├── entity/ # 与 11 张表一一对应的实体类 │ ├── mapper/ # 继承 BaseMapper 的持久层接口 │ ├── service/ # 接口与实现,下单等核心业务都在这里 │ └── dto/ # 跨层传输对象,如 DishDto = 菜品 + 口味列表 ├── src/main/resources/ │ ├── application.yml # 数据源、Redis、上传路径等全局配置 │ ├── backend/ # 后台管理端静态页面 │ ├── front/ # C 端移动端静态页面 │ └── sql/ # 建库建表脚本与初始化数据

common 包是整个项目的骨架:Result 类统一了接口返回格式,前端页面全部依赖 code 字段判断成功失败;BaseContext 用 ThreadLocal 保存当前登录用户 id,Service 层在任意位置都能取到,避免在方法参数里层层传递;全局异常处理器兜底业务异常和未知异常,避免把堆栈直接抛给前端。config 包里的 WebMvcConfig 是理解请求链路的关键,它注册了登录拦截器,也配置了文件上传目录的资源映射。

entity 和 dto 的区别值得先弄清楚:entity 与数据库表字段一一对应,比如 Dish 只有 dish 表里的字段;但 C 端展示菜品时还要带上 flavor 口味列表,这时候就得用 DishDto 来组装。这种表结构不变、展示结构变的场合,用 dto 比往 entity 里塞冗余字段更干净,二次开发时你八成也会遇到。

2.3 数据库模型:11 张表怎么撑起两个端

表名职责关键字段
employee后台员工账号username、password、status
category菜品/套餐分类name、type(1菜品 2套餐)
dish菜品category_id、price、status
dish_flavor菜品口味dish_id、name、value
setmeal套餐category_id、price、status
setmeal_dish套餐菜品关联setmeal_id、dish_id、copies
userC 端用户phone、status
address_book用户地址簿user_id、detail
shopping_cart购物车user_id、dish_id/setmeal_id
orders订单主表user_id、amount、status、number
order_detail订单明细order_id、dish_id、amount

表之间的关系很直白:dish 和 setmeal 都通过 category_id 挂在分类下,查询列表时按分类过滤;setmeal_dish 是一张关联表,把套餐和菜品多对多地连起来;orders 和 order_detail 是一对多,一个订单对应多条明细,明细里冗余了菜品快照信息,这样菜品改了价格,历史订单依然能还原当时内容;shopping_cart 是临时表,下单成功就被清空,不会长期堆积数据。

这套模型里没有独立的库存表,菜品也没有 sku 概念,所以它定位是业务闭环演示而不是电商级实现。你拿它做二次开发,库存和优惠券是最自然的两个扩展方向,最后一章会给出一个比加表更轻的改造示例。

3. 核心模块拆解:从登录鉴权到下单落库的完整链路

这一章选四个关键节点,对应一条真实请求的完整旅程:登录时拦截器怎么放行、上传图片怎么落盘、C 端菜品列表怎么命中缓存、下单时购物车怎么变成两张表。这四段是这个项目里最值得抄的部分,每段后面我会把参数和边界条件说清楚。

3.1 登录鉴权:拦截器在请求进入 Controller 之前做了什么

// LoginCheckInterceptor:后台管理端的登录检查 @Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从 Session 里拿登录时存入的员工对象 HttpSession session = request.getSession(); Object employee = session.getAttribute("employee"); if (employee == null) { // 未登录:写回固定 JSON,前端根据 msg 跳转登录页 response.setContentType("application/json;charset=utf-8"); response.getWriter().write( "{\"code\":0,\"msg\":\"NOTLOGIN\",\"data\":null}"); return false; } return true; // 已登录放行 } }

注册与放行配置一般在 WebMvcConfig:拦截路径写成 addInterceptor(loginCheckInterceptor).addPathPatterns("/").excludePathPatterns("/employee/login", "/backend/", "/front/**")。这里有个容易看漏的点:/backend 和 /front 是两套静态资源目录,必须放行,否则页面还没打开就被拦截掉,样式全丢。前后端同源的场景下,Session 机制比 JWT 简单得多,登录成功把员工对象放进 session,后续每次请求由拦截器统一校验即可。

参数说明:code 为 0 表示业务失败,NOTLOGIN 是约定的未登录标记,前端 axios 响应拦截器只要发现 msg 是 NOTLOGIN 就跳登录页;handler 参数一般用不到,但接口签名必须保留。需要注意这里检查的是后台员工登录,C 端用户走的是另一套 user 会话,两套校验放在不同拦截器里更清晰。

3.2 文件上传与菜品新增:图片落盘与口味的两次落库

// 文件上传:把图片保存到本地磁盘,返回给前端一个可访问的 URL @PostMapping("/upload") public Result<String> upload(MultipartFile file) throws IOException { // 1. 拿到原始文件名,截取扩展名 String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring( originalFilename.lastIndexOf(".")); // 2. 用 UUID 重命名,避免中文文件名和重名覆盖 String fileName = UUID.randomUUID() + suffix; // 3. 目录不存在就创建,Windows 和 Linux 路径要分开配置 String basePath = uploadProperties.getBasePath(); File dir = new File(basePath); if (!dir.exists()) { dir.mkdirs(); } // 4. 落盘并返回相对路径,前端拼上域名访问 file.transferTo(new File(basePath, fileName)); return Result.success("/upload/" + fileName); }

transferTo 接收的文件对象来自前端 multipart 请求,方法名和参数对象是 Spring MVC 自动绑定的,前端表单字段名要写 file 才能对上。返回值 /upload/xxx 是相对路径,img 标签的 src 直接拼域名就能访问;WebMvcConfig 的 addResourceHandlers 必须把 /upload/** 映射到本地磁盘目录,否则 Spring 会去 classpath 找,必然 404。

菜品保存方法 dishService.saveWithFlavor 常见做法是:在 @Transactional 方法内先插入 dish 主表拿到自增 id,再循环插入 dish_flavor 口味表,flavor 的 name 和 value 是一对多的。两次 insert 在同一事务里,第二次失败则整单回滚。接口层接收前端传来的 JSON,字段名和 DishDto 对齐,Jackson 自动完成反序列化。

3.3 Redis 菜品缓存:读多写少场景的 key 设计与失效时机

// C 端菜品列表:先读 Redis,命中直接返回,未命中查库后回填 public List<DishDto> listByCategory(Long categoryId, Integer status) { // 1. key 按分类和上下架状态做区分,方便按分类精准失效 String key = "dish_" + categoryId + "_" + status; String cached = redisTemplate.opsForValue().get(key); if (cached != null) { // 2. 命中缓存:字符串反序列化成对象列表 return JSON.parseArray(cached, DishDto.class); } // 3. 未命中:走数据库查询,并组装菜品口味数据 List<DishDto> dishList = dishService.listWithFlavor(categoryId, status); // 4. 写回缓存,过期时间 60 分钟 redisTemplate.opsForValue().set( key, JSON.toJSONString(dishList), 60, TimeUnit.MINUTES); return dishList; }

key 里带上分类 id 和上下架状态,是为了让按分类查询这条链路能精确失效;value 是 JSON 字符串,所以 redisTemplate 建议用 StringRedisTemplate,避免 JDK 默认序列化器存出二进制乱码。TTL 设 60 分钟只是兜底,真正保证一致性的是主动删除:后台新增、修改、删除菜品或套餐之后,调用 redisTemplate.delete(key) 清理对应分类的缓存,而不是等过期时间自然到。缓存穿透在这个系统里风险不大,因为分类数量有限;如果要接高并发,可以再加一条查询不到也写空值的短路逻辑。

另外要注意:缓存的是 C 端展示结果,后台管理端页面不要走缓存,否则运营改完看不到效果,会以为系统坏了。缓存和数据库的一致性,在这个项目里用更新后立即删 key、下次请求重建这种经典模式就足够,不需要引入额外中间件。

3.4 下单事务:从购物车到订单两张表的一次性写入

// 提交订单:事务内完成两张核心表的写入,并清空购物车 @Transactional public Orders submit(OrderDto orderDto) { // 1. 从 ThreadLocal 取当前登录用户 id,避免参数层层传递 Long userId = BaseContext.getCurrentId(); LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ShoppingCart::getUserId, userId); List<ShoppingCart> carts = shoppingCartMapper.selectList(wrapper); if (carts == null || carts.isEmpty()) { throw new BusinessException("购物车为空,不能下单"); } // 2. 汇总金额,生成唯一的订单号 BigDecimal amount = BigDecimal.ZERO; for (ShoppingCart cart : carts) { amount = amount.add(cart.getAmount()); } Orders order = buildOrder(userId, amount, orderDto); // 构造订单主表 ordersMapper.insert(order); // 3. 购物车每条记录转成一条订单明细 for (ShoppingCart cart : carts) { OrderDetail detail = buildDetail(order.getId(), cart); orderDetailMapper.insert(detail); } // 4. 下单成功后清空购物车 shoppingCartMapper.delete(wrapper); return order; }

@Transactional 的核心价值是,明细插入中途抛异常时,订单主表一起回滚,不会留下孤儿数据;事务默认对 RuntimeException 回滚,业务异常要确保继承 RuntimeException。BaseContext.getCurrentId() 来自 ThreadLocal,登录成功后由过滤器和拦截器写入,Service 层不用把 userId 传来传去。金额计算用 BigDecimal 而不是 double,避免精度丢失;订单号一般用时间戳加随机数生成,页面展示和流水号要求不严格的情况下够用。

购物车在订单写入成功后整表删除,这是这套模型里把临时数据和正式数据分开的关键:shopping_cart 永远只保留未下单的记录。这套设计没有库存扣减,菜品数量也不做超卖控制,不要在生产级高并发场景直接照搬。想扩展的话,在订单提交方法的第 1 步和第 2 步之间加一次校验并扣减库存的调用即可。

4. 运行与部署:从数据库脚本导入到 Linux 后台启动

整个运行流程其实很短:导数据库脚本 → 改 application.yml 两份配置 → Maven 打包 → Java 后台启动。这个项目没有独立的前端工程,不需要 Node 环境,静态页面都打在 jar 里,所以部署环节只需要守护一个后端进程。下面按顺序走。

4.1 环境清单与版本建议

软件版本建议说明
JDK8 或 11基于 Spring Boot 单体开发,两个版本都稳定
Maven3.6+本地打包和依赖下载
MySQL5.7 / 8.0驱动类名不同,见 4.2 注意
Redis5.0+单机即可,无需集群
LinuxCentOS / Ubuntu主要用于部署演示,开发期可在 Windows 上跑

如果你本机没有 Maven,用 IDE 自带的 Maven 或直接安装一个都行;Redis 在 Windows 上需要手动下载解压版,在 Linux 上一条包管理器命令就能装好。建议开发期先在 Windows 上把项目跑通,再上 Linux 做部署,两边的问题分开排查。

注意:MySQL 8.0 的驱动类名是 com.mysql.cj.jdbc.Driver,MySQL 5.7 对应 com.mysql.jdbc.Driver,换库版本时记得连 driver-class-name 一起改。

4.2 数据库脚本导入与数据源配置

-- 1. 创建数据库,字符集用 utf8mb4 防止中文和特殊字符乱码 CREATE DATABASE IF NOT EXISTS ruiji_db DEFAULT CHARACTER SET utf8mb4; -- 2. 切换到目标库,再执行 resources/sql 下的建表脚本 USE ruiji_db; -- 3. 导入完成后检查表,正常应有 11 张业务表 SHOW TABLES;

导入脚本之前先确认 MySQL 服务在运行,再用客户端连接执行。脚本里通常包含建表语句和一批初始化数据,包括管理员员工账号、分类、示例菜品。导入完成后表数量比预期少,优先检查是不是选错了库,常见翻车是把脚本执行到了别的数据库。

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/ruiji_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 换成你自己的密码 redis: host: 127.0.0.1 port: 6379 # 如果 Redis 设置了密码,这里补 password 字段

配置说明:url 里的 useUnicode=true 和 characterEncoding=utf8 保证中文不乱码,serverTimezone=Asia/Shanghai 解决时区差 8 小时问题;username 和 password 换成你自己数据库的账号;Redis 设置了密码就在 redis 节点下补 password 字段。MyBatis-Plus 默认开启驼峰转下划线映射,entity 里的 createTime 能自动匹配表的 create_time 字段,这部分不用额外配置。上传路径建议配成绝对路径,比如 Linux 下 /usr/local/img,避免相对路径踩坑。

4.3 Linux 打包启动:jar 后台运行与日志排查

# 1. 打包,跳过测试,避免测试类导致构建失败 mvn clean package -DskipTests # 2. 把 target 下的 jar 上传到服务器 /opt/ruiji 目录 # 3. 后台启动,日志写到 app.log,关闭终端不影响进程 cd /opt/ruiji nohup java -jar ruiji.jar > app.log 2>&1 & # 4. 确认 8080 端口已经在监听 ss -lntp | grep 8080

命令说明:mvn clean package 会先跑测试,如果测试类依赖本地环境,加 -DskipTests 跳过可以避免构建中断;nohup ... & 表示进程脱离终端后台运行,即使关闭 ssh 窗口也不死;> app.log 2>&1 把标准输出和错误输出都写进同一个日志文件,排查问题直接 tail -f app.log。ss -lntp | grep 8080 确认端口被监听后,再用浏览器访问才不会一脸懵。停进程用 pgrep -f ruiji.jar 拿到 pid 再 kill,别用 pkill java 这种把所有 java 进程一起干掉的危险操作。

首次启动失败很正常,重点看日志里第一段异常:连接 MySQL 失败就去查账号密码和驱动类名;连接 Redis 失败就去查服务和端口;端口被占用就查 8080 是否被别的进程占着。把这三类错误和配置信息对一遍,能排除大部分启动问题。

4.4 联调验证:页面、接口、缓存三层检查

# 登录接口返回 code 为 1 且能拿到 session,说明登录链路通 curl -X POST http://127.0.0.1:8080/employee/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 菜品列表查询后,Redis 里会出现 dish_xxx 的 key redis-cli keys "dish_*"

登录接口返回 code 为 1 并带有 session 凭证,说明数据源和登录链路正常;再用浏览器访问 http://服务器IP:8080/backend/index.html,能打开后台首页说明静态资源路径正常。最后用 redis-cli keys "dish_*" 看菜品缓存是否生成,这一步验证 Redis 序列化和缓存回写逻辑有没有跑通。

因为前后端同源,浏览器不会有跨域问题,也不需要额外配置 CORS。C 端页面在 /front 目录下,后台在 /backend 目录下,两个入口互不干扰。验证顺序建议固定为:命令行登录 → 页面登录 → 操作一个菜品 → 查缓存 key,每一步都有明确的失败信号。

5. 避坑记录:本地跑通后上 Linux 最容易翻车的四件事

这一章是几轮部署踩坑攒下来的记录,按现象、原因、解决的顺序写。你在本地跑的时候就能提前避开这些点,不用等上了 Linux 再从头排查,省下的时间够你多看两遍核心代码。

5.1 后台登录成功,点两下就被弹回登录页

现象:账号密码都对,登录成功跳进首页,但菜单点几下就回到登录页,浏览器 F12 里能看到接口返回 {"code":0,"msg":"NOTLOGIN"}。

原因:多数是拦截器路径放行不完整。拦截器拦了 /,但没有放行 /backend/和 /front/** 两个静态目录,页面资源请求全被当成未登录接口拦掉,前端拿不到资源,自然表现为页面反复横跳;另外,如果项目配了 context-path,前端请求和放行路径也要同步改,否则静态资源会 404。还有一个隐蔽点:Session 默认基于 Cookie,浏览器禁用 Cookie 后每次请求都是新会话,登录状态自然存不住。

解决:在 WebMvcConfig 的 excludePathPatterns 里补上 /employee/login、/backend/、/front/三类路径。后台员工和 C 端用户是两套会话,不要共用同一个拦截器,各自维护放行列表,改动时互不牵连。

5.2 分页查询永远只有第一页,total 一直是 0

现象:分类管理页一共 15 条数据,翻到第 2 页内容没变化,接口返回的 total 字段永远是 0,页面分页组件失效。

原因:MyBatis-Plus 的分页插件没注册。没有 PaginationInnerInterceptor 时,Page 对象只是把查询结果在内存里截取,SQL 不会加 LIMIT,也不会执行 count 查询,所以 records 和 total 都对不上。排查方法:打开 MyBatis-Plus 的 SQL 日志,如果 SQL 末尾没有 LIMIT,说明插件确实没生效。

解决:在 config 包下注册分页插件,DbType.MYSQL 要和实际数据库类型一致,换了数据库类型这里不换,分页语法会错。注册完之后,Service 层凡是接收 Page 参数的查询,返回的 page.getRecords() 才是真分页结果。

// MyBatis-Plus 分页插件:只要一个 Bean,全局生效 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

5.3 Linux 上图片上传失败,或者上传成功但图片 404

现象:Windows 本地一切正常,部署到 Linux 后上传菜品图片要么直接 FileNotFoundException,要么上传提示成功但图片地址打开是 404。

原因:两类原因叠加。一是上传路径写死了 Windows 盘符,比如 D:\img,Linux 上这个目录根本不存在;二是静态资源配置里没有把 /upload/** 映射到真实磁盘路径,Spring 默认去 classpath 下找图片,当然找不到。就算目录存在,Linux 进程没有写权限也会报 IO 异常。

解决:把 base-path 抽到 application.yml 配置里,用绝对路径并通过 mkdirs() 自动创建;WebMvcConfig 的 addResourceHandlers 加上 file: 前缀指向同一个目录;最后给目录授权,把进程运行用户和目录属主对齐。

提示:addResourceLocations("file:" + basePath) 的结尾一定要带 /,否则 /upload/xxx.jpg 匹配不到资源。

5.4 项目启动失败:Unable to connect to Redis

现象:Spring Boot 启动阶段直接抛 RedisConnectionFailureException,整个服务起不来,控制台日志显示连接 127.0.0.1:6379 失败。

原因:最常见是机器上 Redis 没安装或者没启动;其次是 Redis 设置了密码而 application.yml 里没写,或者端口被改成了非 6379。Windows 下 Redis 如果没注册成服务,重启系统后还要手动 redis-server.exe 启动。由于项目启动时会初始化 RedisTemplate 连接,缓存模块不可用会把整个应用拖垮。

解决:先执行 redis-cli ping 确认返回 PONG,再看进程和端口是否正常,密码配置对齐后再启动应用。更稳妥的做法是在缓存读取方法里做降级:用 try/catch 包裹 Redis 操作,失败时直接回源数据库,不让一个缓存把整个服务拖死。这个降级逻辑在二次开发时几乎一定会用到。

6. 二次开发实战:给后台管理端加一个轻量角色权限

这个系统目前只有登录和未登录两种状态,实际交付时客户大概率会要求区分管理员和普通员工。最轻的改造方案是给 employee 表加一个 role 字段,登录后放一份到 Session,拦截器里对敏感路径做一次角色判断。

-- 1. 员工表加角色字段,默认普通员工 ALTER TABLE employee ADD COLUMN role VARCHAR(20) DEFAULT 'employee'; -- 2. 把已有管理员账号标记成 admin UPDATE employee SET role = 'admin' WHERE username = 'admin';

登录代码放行前把角色塞进 Session,拦截器里追加一段判断:

// 只有管理员能访问员工管理相关接口 String role = (String) request.getSession().getAttribute("role"); if (!"admin".equals(role) && request.getRequestURI().contains("/employee")) { response.getWriter().write("{\"code\":0,\"msg\":\"FORBIDDEN\"}"); return false; }

验证方法:准备两个账号,一个 admin、一个普通员工,重启应用后分别登录,普通员工访问员工管理必然收到 FORBIDDEN,而菜品、订单功能不受影响。注意前端也要把员工管理入口按角色隐藏,只做接口拦截的话按钮还挂在页面上,体验不完整。从那以后我每次动权限、改缓存、加字段,都强制走一遍改脚本、清 Redis、重启、双账号验证的流程,能少掉一半线上问题。希望这一篇能帮到你跑通瑞吉外卖,也帮你在二次开发时少走一点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询