SpringBoot+Vue3+MyBatis+MySQL实战:从零搭建BS美食网站系统
“踩过的坑比别人写的代码还多”——这是我和这套BS美食网站系统源码打交道最真实的感受。前后端分离这件事网上教程一抓一大把但真正能把SpringBoot、Vue3、MyBatis、MySQL这四样东西揉成一个“能跑、能看、能改、能上线”的完整项目尤其还是一套贴近现实业务的B/S美食网站系统细节决定成败。这篇文章把你从零带到能自己动手复现、二次开发的水平我尽量把背后的“为什么”也讲透不是光丢一段代码让你复制粘贴就完事。这个系统解决的是典型的“内容展示用户互动后台管理”三类问题前台给访客看美食分类、菜品详情、操作日志与评论评分后台给运营者做菜品上下架、分类排序、用户状态管理等。适合刚学完SpringBoot和Vue3基础、想找个完整项目练手的学生也适合需要一套可直接二次开发的基础框架来接外包项目的开发者。整篇我会按“架构设计→数据库→后端→前端→部署→排坑”的顺序来写每一环节都尽量给出可直接落地的方案。1. 项目定位与整体架构拆解1.1 这套美食系统到底包含什么业务先说清楚这个系统不是那种“只吐数据的管理后台”而是一个完整的B/S网站。用户打开浏览器输入网址不需要装任何客户端直接通过HTTP协议访问服务器就能看到美食网站的门面——首页轮播、菜品分类导航、热销菜品排行、菜品详情页、用户登录注册、收藏和评论、个人中心、购物车与模拟结算然后是运营侧的菜品管理、分类管理、用户管理、评论审核和数据统计。整体业务链路是访客→注册登录→浏览/搜索→收藏/评论→下单结算→后台管理审核。在这个系统设计里最核心的实体是菜品dish。所有功能几乎都围着它转分类表管菜品的归属评论表关联菜品和用户收藏表记录用户与菜品的关注关系订单表记录某个用户在什么时间买了哪些菜品。所以做表结构设计时我第一条建议就是先把菜品这个核心表想清楚其他表都往它身上挂很多初级项目表结构混乱根源就是核心实体没定好。1.2 为什么选择前后端分离B/S架构这套系统的B/S架构是因为目标使用场景是“随时打开浏览器就能访问”对访客零安装成本。前后端分离则是开发协作和后期扩展的需要前端Vue3跑在Nginx或Node环境里负责页面渲染和用户交互后端SpringBoot只提供RESTful API返回JSON数据两者通过HTTP接口通信互不干扰。前后端分离带来了三个看得见的好处第一前端可以独立迭代UI改版不动后端代码第二后端接口可以被多个端复用将来做移动端H5甚至小程序直接复用同一套API第三部署上更灵活前端静态资源可以用CDN加速后端服务可以做集群。代价也很明确——跨域问题、鉴权传递、接口文档维护成本会上升这部分我在后面会专门讲。1.3 技术栈选型分析为什么是SpringBootVue3MyBatisMySQL选SpringBoot是因为它把Spring那一堆繁琐的XML配置砍掉了内嵌Tomcat打一个Jar包就能跑尤其适合这种需要快速交付的BS项目。Vue3选它是因为组合式APIComposition API在组件复用和逻辑组织上比Vue2的Options API清晰太多配合Vite构建开发体验也好。MyBatis在持久层上是半自动ORMSQL让你自己控制做多表联查、复杂统计时比JPA更直接性能调优也更可控。MySQL则是成熟稳定、社区资料丰富这种量级的美食网站系统完全够用。这里我想多说一句选型背后的考量。很多人纠结MyBatis和JPA我的判断标准是如果项目核心是“复杂的查询、报表、多条件筛选”选MyBatis如果项目核心是“标准CRUD、实体关系简单、不需要写SQL”选JPA。美食网站系统的核心正好是菜品多条件检索、销量统计、评论分页这类查询密集场景所以MyBatis是更务实的选择。Vue3同理项目里包含大量表单、弹窗、分页、路由跳转组合式API封装这些逻辑非常顺手这也是我推荐这套组合的原因。2. 数据库设计与核心表结构2.1 实体关系梳理与设计思路开始建表前先画清楚实体关系图。我通常用“用户-菜品”作为主轴用户可以直接收藏菜品也可以对菜品发表评论还可以把菜品加入购物车生成订单。分类表是菜品的从属维度属于一对多关系。订单表需要记录下单用户和菜品快照信息注意这里我强调“快照”——因为菜品价格或名称后续可能调整订单里应该冗余存储下单那一刻的名称和价格否则历史订单数据会跟着菜品一起变这是很多新手容易忽略的点。字段设计上我建议所有表都带上id主键、create_time创建时间核心表再加update_time并用status字段做软删除或者上下架标记。软删除的好处后面排查数据问题时你会感激自己——误删除数据还能捞回来管理员也能看到“已删除”内容做审计。时间字段统一用DATETIME别用TIMESTAMP在2038年会出问题虽然那离我们还远但没必要给自己埋雷。2.2 MySQL建表SQL实战可直接复制以下是我实际使用的建表脚本字符集统一用utf8mb4因为菜品描述、评论内容里完全可能放表情字符utf8mb4才能存下。引擎用InnoDB支持事务和外键约束虽然我建表时不建议加物理外键逻辑外键即字段关联就够。CREATE DATABASE IF NOT EXISTS food_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE food_db; -- 用户表 CREATE TABLE t_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像URL, role TINYINT NOT NULL DEFAULT 0 COMMENT 角色0-普通用户1-管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-正常0-禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 菜品分类表 CREATE TABLE t_category ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT NOT NULL DEFAULT 0 COMMENT 排序权重越大越靠前, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-显示0-隐藏, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品分类表; -- 菜品表 CREATE TABLE t_dish ( id BIGINT NOT NULL AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT 所属分类ID, name VARCHAR(100) NOT NULL COMMENT 菜品名称, price DECIMAL(10,2) NOT NULL COMMENT 价格, image VARCHAR(255) DEFAULT NULL COMMENT 主图URL, description TEXT COMMENT 菜品描述, monthly_sales INT NOT NULL DEFAULT 0 COMMENT 月销量, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-上架0-下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表; -- 收藏表 CREATE TABLE t_favorite ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_dish (user_id, dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表; -- 评论表 CREATE TABLE t_comment ( id BIGINT NOT NULL AUTO_INCREMENT, dish_id BIGINT NOT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL COMMENT 评论内容, rating TINYINT NOT NULL DEFAULT 5 COMMENT 评分1-5, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-展示0-审核隐藏, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_dish (dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;这段SQL里有两个设计点我特意说明一下。一是t_favorite表加了uk_user_dish唯一索引这是为了防止同一个用户反复收藏同一个菜品产生脏数据代码里用INSERT IGNORE或者先查询再插入都可以但唯一索引才是兜底。二是评论表的status字段承担了“审核隐藏”功能这就是为什么管理员后台要做评论审核——有人恶意刷差评或者发广告一键隐藏即可不会直接删数据。2.3 关于“MyBatis-Plus根据实体类生成建表SQL”的正确姿势我看到热搜词里有很多人搜“mybatisplus根据java实体类生成创建表的sql语句”这其实是个容易踩坑的地方。MyBatis-Plus本身并不提供“实体类→建表SQL”的自动生成能力它提供的代码生成器是反过来的根据数据库表生成实体类。如果你非要根据实体类生成建表SQL常见做法有三种。第一种也是最可靠的通过IDEA的Database工具面板或者用MySQL Workbench、Navicat的数据建模功能先把实体类字段整理成ER图再通过工具导出DDL。第二种直接用Flyway这类数据库迁移工具在迁移脚本里手写SQL让程序启动时自动执行这样表结构有版本记录团队协作不会乱。第三种网上有一些开源的EntityToSql工具类能扫描实体类上的注解比如自定义TableName、TableField拼装CREATE TABLE语句但说实话这种工具适合demo演示不适合生产项目因为字段类型映射、索引设计、注释生成这些细节很容易出问题。我自己在这套系统里维护表结构的习惯是实体类与建表SQL双写以SQL为准实体类通过MyBatis-Plus的代码生成器反向生成。这样至少保证了一侧是正确的再用map-underscore-to-camel-case配置把数据库下划线字段自动映射成驼峰属性不会出现字段对不上的情况。2.4 数据初始化与测试数据填充分享表建好后下一步就是灌数据。我建议先写一个data.sql或者通过接口造数准备300条菜品、10个分类、几十个用户的测试数据这样在开发前端的时候页面不会空空如也。价格字段建议故意造一些“9.9、19.9、99.9”这种带小数的数据测试分页和排序时更真实。造数据时注意category_id必须真实存在否则联查时会出现空分类的菜品影响列表展示。3. 后端SpringBoot实现要点3.1 项目初始化与依赖配置后端工程我建议直接用Spring Initializr创建但版本选择要重点注意。热搜词里“springboot版本太高”这个反映很真实——SpringBoot 3.x要求JDK17起步并且把javax.*包换成了jakarta.*如果你的本机还是JDK8或者团队其他人还没升级就会遇到一堆莫名其妙的编译错误。便捷的方案是先用SpringBoot 2.7.x最后一个支持JDK8的版本线把业务逻辑跑通再考虑是否升级3.x。下面是这套系统的核心依赖配置我用自己的项目pom简化后给你parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这个组合里mybatis-spring-boot-starter的版本2.3.2和SpringBoot 2.7.x兼容得很好。如果你用了SpringBoot 3.x记得换成mybatis-spring-boot-starter的3.x版本否则启动时会报ClassNotFoundException之类的问题。3.2 application.yml配置细节与SQL日志打印配置文件是整个后端的“总开关”也是新手最容易出错的地方。我把这套系统的关键配置说明拆成三块讲。第一数据源配置。JDBC URL里必带useSSLfalse和serverTimezoneAsia/Shanghai前者避免本机MySQL8在SSL握手时报错后者解决时区偏移8小时导致的日期错乱。用户名密码按你自己的MySQL设置来。第二MyBatis相关配置。mapper-locations指定XML文件位置map-underscore-to-camel-case开启驼峰映射。这里单独说log-impl热点词里有人搜“mybatis配置打印”调试阶段建议把日志实现配成StdOutImpl这样控制台会打印每条SQL和参数排查问题效率翻倍。生产环境记得关掉否则日志量太大。下面是完整配置示例server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/food_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: root mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.food.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl第三关于自定义配置项比如文件上传路径、JWT密钥、接口版本号我习惯用ConfigurationProperties绑定到一个配置类里不要散落在一堆Value中后面维护起来会清晰很多。3.3 统一返回结果与全局异常处理前后端分离项目必须有统一的数据返回格式否则前端解析数据时得写一堆分支判断。我在这套系统里定义了Result类结构很简单code200成功其他失败、message提示信息、data业务数据可以是对象或列表。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }配合全局异常处理器RestControllerAdvice把业务异常比如菜品已下架、用户不存在、参数校验失败统一捕捉并转换为Result返回这样前端axios拦截器只需要判断code是否为200就能统一处理各种错误提示代码会非常整洁。我见过很多项目每个接口里都写try-catch返回不同结构联调时前端苦不堪言。3.4 登录鉴权与拦截器实现美食网站的用户登录、管理员鉴权是典型场景。我选择JWT方案因为它是无状态的适合前后端分离和将来的微服务拆分。登录接口校验用户名密码成功后生成一个包含用户ID和角色的token返回给前端前端存在localStorage里每次请求在Header中携带Authorization字段。后端用一个拦截器统一校验token这里我强调几个细节第一JWT密钥放到配置文件中不要硬编码第二拦截器要放行登录注册接口和菜品列表等公开接口第三校验通过后把解析出来的用户信息放入ThreadLocal或RequestContext后续Service层可以通过工具类直接获取当前用户ID避免在每个Controller里重复解析token。贴一段核心拦截器逻辑public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 处理跨域预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); try { Long userId JwtUtil.parseToken(token); UserContext.setUserId(userId); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\未登录或登录过期\}); return false; } } }还有一点必须说密码存储绝对不要用MD5。MD5撞库太容易了用BCryptPasswordEncoder做哈希每次生成的密文带随机盐即使两个用户密码相同存储的哈希值也不同安全性高一个量级。4. 前端Vue3实现与前后端联调4.1 Vue3项目创建与工程结构规划前端这块我用的构建工具是Vite创建命令是npm create vitelatest food-web -- --template vue。如果你需要TypeScript就把模板换成vue-ts但这里我建议普通JavaScript就行因为这套系统体量不大上TS会增加类型定义和泛型的心智负担除非你的团队成员对TS很熟。页面结构上我规划了views目录下的几个核心模块home首页、detail菜品详情、login/register登录注册、cart购物车、admin后台管理以及layout用户和管理员共用的整体布局。这样按业务模块而不是按组件类型分目录是我比较推荐的前端工程组织方式——找文件时先想“我在改哪个页面”再去找对应目录。路由设计上使用了Vue Router 4的路由守卫做登录校验。简单说就是meta里标记requiresAuth的路由进入前检查本地有没有token没有就跳登录页。后台管理的路由还要额外校验用户的role是否为管理员否则直接跳403页面。4.2 Axios封装与跨域问题处理前后端分离的痛点之一就是跨域。我在Vite开发环境里通过server.proxy配置代理把所有/api开头的请求转发到后端的http://localhost:8080这样前端请求是相对路径不产生跨域。生产环境则用Nginx做反向代理把/api转发到后端服务。这是目前最规范的做法不建议在SpringBoot里粗暴配置CrossOrigin(origins *)那等于放开所有来源安全隐患大。Axios封装上面已经给过代码。我要补充一个细节响应拦截器里对401状态码一定要做全局跳转登录的逻辑避免用户token过期后所有接口都报错但页面毫无反应。同时对500错误在后端返回错误提示后用ElMessage弹出提示这样前端每个接口调用处就不用重复写错误弹窗代码了。4.3 核心页面实现要点首页是最能体现前端水平的地方。我建议用Element Plus的Carousel组件做顶部轮播下面用Tabs或Card组件按分类展示菜品卡片每个卡片包含菜品图、名称、价格、月销量和“加入购物车”按钮。菜品列表数据通过onMounted生命周期调用getDishList接口获取。这里关键词是“分页”列表接口必须返回total和records前端配合Pagination组件每次切换页码重新请求数据。有些新手会把所有菜品一次性返回渲染数据量一大页面直接卡死教训深刻。菜品详情页更复杂一点。除了展示基本信息外还要加载评论区。评论区的关键是联表查询用户昵称和头像这在MyBatis里通过关联查询实现。我这里给一个简化版的Mapper映射示例select idselectCommentsWithUser resultTypecom.food.vo.CommentVO SELECT c.id, c.content, c.rating, c.create_time, u.nickname, u.avatar FROM t_comment c LEFT JOIN t_user u ON c.user_id u.id WHERE c.dish_id #{dishId} AND c.status 1 ORDER BY c.create_time DESC LIMIT #{offset}, #{limit} /select后台管理界面我用了经典的侧边栏布局左侧el-menu菜单菜品管理、分类管理、评论审核、用户管理、数据概览右侧内容区用router-view渲染对应页面。菜品管理页最关键的功能是表单校验和图片上传。图片上传我单独封装了一个UploadImg组件调后端/api/file/upload接口返回图片URL后回填到表单的image字段里。4.4 组合式API的封装经验Vue3的核心优势是组合式API。我在这套系统里把“菜品列表页”的逻辑抽成useDishList组合函数里面包含loading、dishList、page、total这些响应式变量以及fetchData、changePage、searchDish这些方法。然后在页面组件里只需要一行const { dishList, loading, fetchData } useDishList()就可以拿到完整的状态和操作。这样做的直接好处是首页和后台菜品管理页都用到列表逻辑但各自页面形态完全不同通过组合函数复用逻辑而不是生硬地抽公共组件代码可读性和维护性都好很多。关于热搜词里一直有人问的“vue3后台管理系统”我的建议是不要上来就套用若依之类的重型框架。先自己按照“登录→Layout→CRUD页面”这条线理一遍重点是搞懂Vue Router的路由守卫、Pinia的登录态存储和Axios拦截器这三件事怎么配合等你明白了这些底层逻辑再用若依这类框架就不会一头雾水遇到“若依vue3 ts报错”之类的问题也知道怎么定位。5. 完整部署流程与环境准备5.1 硬性环境清单JDK、MySQL、Node开发环境和部署环境我都列一个清单。开发机建议JDK 1.8配合SpringBoot 2.7、MySQL 5.7或8.0、Node 16、Maven 3.6。需要注意JDK版本与SpringBoot版本必须匹配SpringBoot 2.7支持JDK8到JDK17SpringBoot 3.x则必须JDK17。这一点在我接手过的几个项目里反复踩坑特别提醒你检查本机java -version再决定用哪个SpringBoot版本。MySQL我推荐的安装方式是直接官网下载mysql-installer-community-8.0.36.msi一路Next时注意选“Server only”模式端口默认3306字符集选utf8mb4。如果是Windows 10环境安装完成后还要配环境变量MYSQL_HOME和Path否则你只能在安装目录下敲命令。这一步经常被忽略导致mysql -u root -p在命令行里找不到命令。5.2 后端打包与启动后端启动前先确认application.yml里的数据库账号密码正确并已执行过建表SQL。然后用Maven打包命令很简单mvn clean package -DskipTests打包成功后target目录下会生成一个food-server.jar启动命令java -jar target/food-server.jar生产环境我建议用nohup java -jar food-server.jar server.log 21 方式后台运行日志重定向到文件方便排错。如果你用的是云服务器记得在安全组里放行8080端口不然外部访问不到。5.3 前端构建与Nginx部署前端打包更简单npm install npm run build生成在dist目录下的静态文件就是整个前端的全部产物。部署方式我强烈建议用Nginx配置要点是两段第一root指向dist目录让浏览器能访问到前端资源第二把/api的请求反向代理到后端服务。下面是精简版Nginx配置server { listen 80; server_name your-domain.com; root /var/www/food-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这段配置里有三个关键细节。第一是try_files $uri $uri/ /index.html;这是SPA单页应用路由模式的标配否则刷新页面时Nginx会返回404。第二是proxy_pass末尾的上游地址http://127.0.0.1:8080这里建议带协议前缀不能多加斜杠否则路径会被改写。第三是透传真实的用户IP因为前后端分离后后端记录日志时默认看到的是Nginx的IP不利于审计。6. 常见问题与排查技巧实录6.1 SpringBoot版本过高导致的兼容性坑很多人在“springboot版本太高”上栽跟头症状通常是项目启动直接报错查看依赖树时发现某个Starter版本不兼容比如spring-boot-starter-parent用了3.2.x但mybatis-spring-boot-starter还是2.x启动时ClassNotFoundException: javax.sql.DataSource或者Failed to configure a DataSource。这里的核心原因是SpringBoot3把Java EE API的包名从javax迁移到了jakarta老版本的MyBatis Starter内部还引用javax类找不到必然报错。排查思路是打开Maven依赖树mvn dependency:tree重点看冲突的jar包版本。解决方案就两条一是明确SpringBoot3必须搭配mybatis-spring-boot-starter3.x二是如果你不想折腾兼容性直接用SpringBoot2.7.x即可满足这套美食系统所有需求。不要为了“追新”而追新稳定性优先。6.2 Vue3项目常见报错路由守卫与TS类型问题Vue3项目里我碰到最多的是两类报错。第一类是路由守卫相关的死循环典型表现是“登录成功后仍然跳回登录页”。原因通常是守卫里的判断逻辑写反了或者next()方向不对。我排查这类问题的经验是在守卫里加console.log打印每次跳转的to.path和from.path很快就能发现守卫在来回跳。正确的逻辑是如果访问的是不需要登录的页面但已经登录了直接next()放行如果要访问的页面需要登录但没有token才next(/login)。第二类是TS类型的报错比如用window.xxx的时候报“Property xxx does not exist on type Window”。这是因为TS不认识你在window上挂的属性。解决方案是写一个global.d.ts声明文件里面用interface Window { xxx: any }扩展类型定义。这条如果你在用若依Vue3 TS版本时特别常见报错大概率就卡在类型定义上不是你的业务逻辑有问题。6.3 MyBatis缓存与SQL日志排查的实用技巧MyBatis的缓存分为一级缓存和二级缓存默认一级缓存是开启的作用域是同一个SqlSession。在SpringBoot结合MyBatis时因为每次请求都会创建新的SqlSession所以一级缓存基本感知不到。二级缓存需要手动开启但我在这套系统里不建议开——美食系统的菜品和评论数据频繁更新开二级缓存反而容易造成脏数据。这就是个典型的“知道功能但不用”的决策你要理解为什么才能在生产环境里不犯低级错误。排查SQL问题时开log-impl: org.apache.ibatis.logging.stdout.StdOutImpl是最快的方法。日志里会打印Preparing语句和Parameters参数你可以直接拿着SQL语句去Navicat里执行对比结果就知道是SQL写错了还是参数传错了。还有一个技巧如果查询结果里有字段为null先查数据库表里字段名和实体类属性名的映射关系常见问题是map-underscore-to-camel-case没开启导致create_time字段映射不到createTime属性上。6.4 跨域与接口鉴权问题排查表我把前后端联调时最容易遇到的三类问题整理成速查表方便你对照排查问题现象可能原因解决方案浏览器Console提示CORS错误前后端不同源且后端未允许用Nginx或Vite代理解决不要在Controller加CrossOrigin(*)请求能到达后端但返回401token未携带或已过期检查Axios请求拦截器是否设置Authorization Header登录成功后刷新页面又变成未登录登录态只存在内存里使用localStorage持久化token在路由守卫里读取并校验菜品图片加载404图片URL是绝对路径访问不到统一使用/api/file/...相对路径配合代理访问静态资源6.5 一个隐藏的坑文件上传目录不存在菜品管理页的图片上传后端接收MultipartFile后需要保存到本地目录比如/var/food-web/upload/。如果你在Linux服务器部署时没有提前创建这个目录并且程序没有做“不存在则创建”的逻辑上传图片会直接报FileNotFoundException同时前端提示上传失败但后端日志不明显排查起来很浪费时间。稳妥做法是在FileService里保存前调用FileUtil.mkdirs()确保目录存在并且把上传目录配置到application.yml里不要在Service代码里硬编码路径。写在最后的小建议这套美食系统我前前后后重构过三轮最大的体会是写前端时先想好后端接口返回结构再动手否则你会被反复改接口折磨到崩溃。建议你在正式开发前至少用接口文档工具Apifox或Swagger把核心接口约定好哪怕只是写在Markdown里也行。后端尽早把Result统一返回结构和全局异常处理搭好前端尽早把Axios封装和路由守卫搭好这两件事完成后整个项目骨架就稳了。最后再分享一个扩展方向这个BS美食系统后续可以无缝加两个模块一个是“每日推荐”算法按用户收藏和评论评分做简单协同过滤另一个是用ECharts在后台做“菜品销量趋势图”。这两块都不需要改技术栈数据结构也已经支持你在二次开发时优先往这两个方向延伸性价比最高。我当初就是靠给这套系统加了销量分析和定时上架任务被客户追加了一笔需求费用这些都很值得你去尝试。