做前后端分离项目最怕两件事:一是环境跑不起来,二是代码能跑但看不懂结构。这次要拆的是一个 SpringBoot + Vue3 美食网站管理系统,定位很明确——前后端分离、完整的增删改查、能本地跑通、适合当毕业设计和简历项目。它不追求复杂的推荐算法,也不追求微服务拆分,核心价值在于把一套「后端 REST 接口 + 前端管理后台 + MySQL 数据持久化」的流程走通,让你能在半小时左右把服务启动起来,看到登录页、分类管理、菜品管理、图片上传、分页搜索这些功能真正跑起来。
这个项目的几个关键特征先说清楚:后端是 SpringBoot 提供 RESTful 接口,前端是 Vue3 + Vite 构建的管理后台,数据落在 MySQL,图片走本地静态资源目录或上传接口;功能上覆盖分类管理、菜品管理、用户管理、订单/评价等常见模块(具体以你拿到的源码为准),增删改查、条件查询、分页、图片上传、登录鉴权是基本盘。硬件门槛极低,一台 8G 内存的普通笔记本就能跑,不需要独立显卡,也不需要安装任何 AI 推理环境,装好 JDK、Node、MySQL 三个东西就能开工。
本文会按「先看能力 → 再准备环境 → 建库导数据 → 启动后端 → 启动前端 → 逐项验证功能 → 调接口和批量处理 → 排错」的顺序走一遍。适合三类读者:正在找毕设题目的同学、需要一套可运行 CRUD 模板快速起项目的开发者、以及想把前后端分离流程完整过一遍的初学者。所有命令和配置都给出可复制的模板,具体路径、端口、包名请按你本地的实际项目替换。
1. 项目核心能力速览
先用一张表把关键信息压缩出来,方便你 30 秒内判断要不要继续往下看。表中所有参数都以「通用实践」为准,实际项目如果和表格不一致,以源码里的 pom.xml、package.json、application.yml 为准。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 前后端分离的 Web 管理系统(美食/餐饮主题) |
| 后端技术 | SpringBoot + MyBatis-Plus(或 MyBatis)+ MySQL + Maven |
| 前端技术 | Vue3 + Vite + Element Plus + Axios + Vue Router + Pinia(以源码为准) |
| 通信方式 | HTTP RESTful 接口,JSON 传输,前端通过代理转发到后端 |
| 核心功能 | 登录鉴权、分类管理、菜品管理、图片上传、分页查询、条件搜索、增删改查 |
| 硬件要求 | 普通笔记本即可,8G 内存够用,不需要独立显卡 |
| 运行环境 | JDK 17(或 8/11,按项目 pom.xml 的 <java.version> 决定)、Node 18+、MySQL 8.0 |
| 启动方式 | 后端 mvn spring-boot:run 或 IDE 直接运行主类;前端 npm run dev |
| 默认端口 | 后端常见 8080,前端 Vite 常见 5173,MySQL 3306(均需按实际项目确认) |
| 是否支持接口 API | 支持,后端本身就是 REST 接口服务,可用 curl / Postman / Python 直接调 |
| 是否支持批量任务 | 支持,可通过接口循环调用或 SQL 批量导入实现批量新增/导入菜品数据 |
| 数据持久化 | MySQL,配套 .sql 初始化脚本 |
| 适合场景 | 毕业设计、课程设计、简历项目、CRUD 模板二次开发、教学演示 |
| 不适合场景 | 高并发生产环境、需要分布式/微服务架构、需要支付对账等强一致业务 |
这里要强调一个容易被忽略的点:这类项目真正值钱的不是「代码有多少行」,而是分层结构是否清晰。一个合格的 SpringBoot 后端应该有 controller、service、mapper、entity、config、common 这几层,一个合格的 Vue3 前端应该有 api、views、components、router、stores、utils 这几块。看源码的时候先看目录树,比一行行读代码效率高得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 适用场景与使用边界
先说适合谁。第一类是正在准备毕业设计的同学,美食网站管理系统这个题目好处是业务语义直观——菜品、分类、价格、图片、库存、评价,这些概念不需要向老师解释太多就能讲清楚,答辩时演示「新增一个菜品 → 前台列表出现 → 编辑价格 → 删除」这条链路,比讲一堆抽象概念更有说服力。第二类是需要一套可运行 CRUD 骨架的开发者,把自己的业务字段替换进去(比如把「菜品」换成「设备」「图书」「课程」),半小时能拿到一个能跑的管理后台,比自己从零搭脚手架快得多。
再说能解决什么问题。它把「前后端分离」这件事的完整链路铺开了:前端怎么发请求、跨域怎么处理、后端怎么接收参数、怎么校验、怎么返回统一响应体、异常怎么捕获、分页怎么做、图片怎么存、登录态怎么维持。这些是实际工作中每天都要碰的东西,把这一套跑通一遍,比看十篇文章都管用。
不适合的场景也要说清楚。这类项目是单体的、面向管理的、并发量很低的应用,不要指望它扛住秒杀流量,也不要指望它有完整的权限体系(很多毕设项目的权限只做到「有没有登录」,没做到「角色-菜单-按钮」三级)。如果你要做的是对外营业的真实系统,至少还要补上:密码加盐存储与登录限流、接口鉴权与越权校验、订单金额服务端二次校验、文件上传类型与大小限制、SQL 注入与 XSS 防护、操作日志审计、数据备份策略。这些不是本篇模板能覆盖的,得按真实业务补齐。
合规边界必须单独提醒。美食网站会涉及菜品图片、店铺照片、用户头像,这些素材不要直接从网上抓取商用图,测试阶段用自己拍的、平台免费授权图库或者纯色占位图都可以;如果项目里有用户评论、手机号、地址等字段,测试数据必须是伪造的,不要导入真实用户信息。项目用于毕设演示没问题,但如果打算上线运营,涉及食品经营的还要考虑相关的经营资质与展示合规要求,这不是技术能解决的问题。源码资料可以学习和二次开发,但二次分发和商用请遵守原作者的授权说明。
3. 技术栈拆解与环境准备
3.1 先确认三个基础环境
这个项目踩坑最多的不是代码,而是版本错配。开工前先把三个东西的版本确认一遍,并且以项目自带的配置文件为准,不要凭印象装最新版。
bash复制# 检查 JDK 版本,注意看输出是否是项目要求的版本
java -version
javac -version
# 检查 Maven
mvn -v
# 检查 Node 与 npm
node -v
npm -v
# 检查 MySQL 是否在运行
mysql --version
判断标准很简单:打开后端的 pom.xml,找 <java.version> 或 <maven.compiler.source>,把本机 JDK 对齐;打开前端的 package.json,看 engines 字段和 vite 版本,Node 版本太低会在 npm install 阶段直接报错。如果 pom.xml 里写的是 JDK 17,你本机是 JDK 8,那不是「凑合能跑」,而是编译期就会挂。
3.2 环境准备检查清单
| 检查项 | 要求 | 常见问题 |
|---|---|---|
| JDK | 与 pom.xml 声明一致 |
多版本共存时 JAVA_HOME 指向错误 |
| Maven | 3.6 以上,建议配置国内镜像 | 依赖下载卡住、settings.xml 未配置 |
| Node | 18 及以上(Vite 5 要求) | 版本过低导致 npm install 失败 |
| npm 源 | 建议配置国内镜像加速 | 安装依赖超时 |
| MySQL | 8.0,字符集 utf8mb4 |
中文乱码、时区报错 |
| 端口 | 8080 / 5173 / 3306 未被占用 | 端口冲突导致启动失败 |
| IDE | IDEA(后端)+ VSCode(前端) | Lombok 插件未安装导致实体类报错 |
Lombok 这个坑值得单独说。后端实体类如果用了 @Data、@Getter 注解,而 IDEA 没装 Lombok 插件、或者没开启注解处理,IDE 里会满屏红线,但 mvn 编译其实是能过的。遇到「IDE 报错但能跑」的情况,先去 Settings → Build → Compiler → Annotation Processors 勾上 Enable annotation processing。
3.3 目录结构先看一遍
拿到源码后别急着启动,先花两分钟看目录。典型结构长这样:
text复制food-admin/
├── backend/ # SpringBoot 后端
│ ├── src/main/java/com/xxx/
│ │ ├── controller/ # 接口层
│ │ ├── service/ # 业务层
│ │ ├── mapper/ # 数据访问层
│ │ ├── entity/ # 实体类
│ │ ├── config/ # 跨域、拦截器、MyBatis 配置
│ │ └── common/ # 统一响应体、异常处理
│ ├── src/main/resources/
│ │ ├── application.yml # 数据源、端口、上传路径
│ │ └── mapper/ # XML 映射文件(如有)
│ └── pom.xml
├── frontend/ # Vue3 前端
│ ├── src/
│ │ ├── api/ # 接口封装
│ │ ├── views/ # 页面
│ │ ├── components/ # 公共组件
│ │ ├── router/ # 路由
│ │ └── stores/ # 状态管理
│ ├── vite.config.js # 代理配置
│ └── package.json
└── sql/
└── food_admin.sql # 数据库初始化脚本
看目录的意义在于:当功能出问题时,你能快速定位是哪一层的问题。前端页面空白 → 看 views 和 router;接口 404 → 看 controller 的 @RequestMapping;数据查不出来 → 看 mapper 和 SQL。
4. 数据库设计与初始化
4.1 建库与导入脚本
大部分这类项目会自带 .sql 文件,标准流程是三步:建库、导入、改配置。
sql复制-- 创建数据库,字符集必须是 utf8mb4,否则中文和 emoji 会出问题
CREATE DATABASE IF NOT EXISTS food_admin
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
USE food_admin;
然后用命令行导入脚本,注意路径替换成你自己的:
bash复制# Windows 下在 cmd 或 PowerShell 中执行,注意路径分隔符
mysql -u root -p food_admin < D:\projects\food-admin\sql\food_admin.sql
# macOS / Linux
mysql -u root -p food_admin < ~/projects/food-admin/sql/food_admin.sql
如果脚本里已经包含 CREATE DATABASE 语句,就不要先手动建库,直接 mysql -u root -p < xxx.sql 执行整个文件即可。导入完成后必须验证,不能只看「没报错」:
sql复制USE food_admin;
SHOW TABLES;
SELECT COUNT(*) FROM dish;
SELECT id, name, price FROM dish LIMIT 5;
如果 SHOW TABLES 是空的,说明脚本没执行成功或者执行到了别的库,重新确认当前连接的库名。
4.2 典型表结构参考
项目主题是美食网站,核心表通常围绕「分类—菜品」这条主线展开。下面给一份通用结构参考,字段名以你拿到的脚本为准,不要直接照抄覆盖:
sql复制-- 菜品分类表
CREATE TABLE category (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
name VARCHAR(50) NOT NULL COMMENT '分类名称',
sort INT DEFAULT 0 COMMENT '排序值',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '菜品分类';
-- 菜品表
CREATE TABLE dish (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
category_id BIGINT NOT NULL COMMENT '所属分类',
name VARCHAR(100) NOT NULL COMMENT '菜品名称',
price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格',
image VARCHAR(255) DEFAULT NULL COMMENT '图片地址',
description VARCHAR(500) DEFAULT NULL COMMENT '简介',
status TINYINT DEFAULT 1 COMMENT '状态 1上架 0下架',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
KEY idx_category (category_id),
KEY idx_name (name)
) COMMENT '菜品信息';
建表时有两个细节值得注意:价格字段一定用 DECIMAL 而不是 DOUBLE,浮点误差在金额场景是硬伤;category_id、name 这类经常出现在 WHERE 里的字段加上索引,数据量小的时候感觉不出来,数据量一到几千条差别就明显了。
4.3 修改后端数据源配置
数据库连不上,90% 是 application.yml 里的账号密码没改。打开配置文件,重点看这四行:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/food_admin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 你的数据库密码
servlet:
multipart:
max-file-size: 10MB # 图片上传大小限制,按需调整
max-request-size: 20MB
server:
port: 8080 # 后端端口,冲突时改这里
serverTimezone=Asia/Shanghai 这一项不要省,MySQL 8 在某些环境下不配时区会直接抛 The server time zone value is unrecognized。如果你的密码里有 @、#、& 这类特殊字符,注意在 YAML 里用引号包起来,否则会被解析成注释或分隔符。
5. 后端启动与接口验证
5.1 启动方式
三种方式选一种就行。IDE 里直接右键主类 XxxApplication 运行最省事,适合调试;命令行适合验证「换台机器能不能跑」。
bash复制# 进入后端目录
cd backend
# 方式一:Maven 插件启动
mvn spring-boot:run
# 方式二:先打包再运行,适合验证完整构建
mvn clean package -DskipTests
java -jar target/food-admin-0.0.1-SNAPSHOT.jar
# 方式三:指定端口启动,端口冲突时用
java -jar target/food-admin-0.0.1-SNAPSHOT.jar --server.port=8081
启动成功的标志是控制台出现 Started XxxApplication in x.xxx seconds,并且没有 APPLICATION FAILED TO START。如果紧接着刷出一堆红色异常,往下翻找第一行 Caused by,那才是真正的根因,后面的都是连锁反应。
5.2 验证接口是否真的通了
不要只看进程起来了就认为后端好了,接口能不能返回数据是两回事。先用最轻量的方式探一下:
bash复制# 探活:先看接口是否存在,返回 404 说明路径不对,返回 500 说明进了代码但报错
curl -i "http://127.0.0.1:8080/dish/list?pageNum=1&pageSize=5"
# 带查询条件的列表接口(参数名以实际 Controller 为准)
curl -i "http://127.0.0.1:8080/dish/list?pageNum=1&pageSize=5&name=鱼"
# 详情接口
curl -i "http://127.0.0.1:8080/dish/1"
返回结构通常是统一响应体,形如:
json复制{
"code": 200,
"msg": "success",
"data": {
"total": 12,
"records": [
{ "id": 1, "name": "清蒸鲈鱼", "price": 68.00, "categoryId": 2 }
]
}
}
判断标准有三条:code 是 200、data 不为 null、records 长度大于 0。三条都满足,说明「MySQL → MyBatis → Service → Controller」这条链路是通的。如果 code 是 200 但 records 是空数组,问题在数据库没数据或者查询条件把数据过滤掉了;如果 code 是 500,去看后端控制台的异常堆栈。
5.3 一个典型的增删改查接口长什么样
看源码时,Controller 层基本是这个形态,理解了这个模子,其他模块都是照抄:
java复制@RestController
@RequestMapping("/dish")
public class DishController {
@Resource
private DishService dishService;
// 分页 + 条件查询
@GetMapping("/list")
public Result<Page<Dish>> list(@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name) {
Page<Dish> page = dishService.pageQuery(pageNum, pageSize, name);
return Result.success(page);
}
// 新增
@PostMapping("/add")
public Result<Void> add(@RequestBody Dish dish) {
dishService.save(dish);
return Result.success();
}
// 修改
@PutMapping("/update")
public Result<Void> update(@RequestBody Dish dish) {
dishService.updateById(dish);
return Result.success();
}
// 删除
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
dishService.removeById(id);
return Result.success();
}
}
注意 @RequestBody 和 @RequestParam 的区别:前者接 JSON 请求体,前端用 POST 传对象;后者接 URL 参数,前端拼在地址后面。这两个用混了,最常见的现象就是后端收到 null,前端觉得「我明明传了」。
6. 前端启动与跨域配置
6.1 安装依赖与启动
bash复制cd frontend
# 安装依赖,换国内镜像会快很多
npm config set registry https://registry.npmmirror.com
npm install
# 启动开发服务器
npm run dev
启动成功后控制台会打印本地地址,通常是 http://localhost:5173,浏览器打开就能看到登录页。npm install 阶段如果卡在某个包上,先看是不是网络问题,再看是不是 Node 版本不满足 package.json 的要求——Vite 5 要求 Node 18+,Node 16 会直接报 crypto.getRandomValues is not a function 这类看不懂的错。
6.2 代理配置是前后端分离的关键
开发阶段前端跑在 5173,后端跑在 8080,浏览器会拦跨域请求。两种解法:后端加 @CrossOrigin 或全局 CORS 配置,或者前端配代理。推荐后者,因为不污染后端代码,部署时也不受影响。
javascript复制// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
// 所有以 /api 开头的请求转发到后端
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
// 如果后端接口本身没有 /api 前缀,这里要重写掉
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
配完代理,前端请求写成 /api/dish/list 就能打到后端 /dish/list。这里有个高频坑:rewrite 有没有写,取决于后端 Controller 上有没有 @RequestMapping("/api")。如果后端本身带 /api 前缀,你又 rewrite 掉了,就会稳定 404。改完 vite.config.js 必须重启 dev server,热更新不会让代理配置生效。
6.3 Axios 封装与统一错误处理
前端调接口不要在每个页面里直接写 axios.get,正常项目都会封一层:
javascript复制// src/api/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/api',
timeout: 15000
})
// 请求拦截:统一带 token
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = token
}
return config
})
// 响应拦截:统一处理业务码与错误提示
request.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.msg || '请求失败')
return Promise.reject(new Error(res.msg))
}
return res.data
},
error => {
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default request
这层封装的价值在于:登录过期、后端报错、网络超时这三类问题只会集中在一处处理,页面里只管拿数据。很多毕设项目把 try/catch 散落在每个页面,改一次提示文案要改二十个文件,这是典型的坏味道。
7. 功能测试与效果验证
服务都起来之后,别急着看代码,先按下面的顺序把功能过一遍。每一步都给出「怎么判断成功」,出了问题直接对照第 10 节排查。
7.1 登录流程测试
测试目的:确认鉴权链路完整,token 能正常下发和携带。
操作步骤:打开前端页面 → 输入测试账号(一般在 .sql 脚本的 user 表里,密码通常是明文或简单加密后存储)→ 点击登录 → 观察是否跳转到首页。
预期结果:登录成功后跳转 /dashboard 或 /index,浏览器 localStorage 或 sessionStorage 里能看到 token 或用户信息。
判断成功:刷新页面后仍然保持登录态,不跳回登录页。如果一刷新就踢回登录页,说明登录态只存在了内存里(Pinia 没做持久化)或者路由守卫没读 token。
常见失败原因:账号密码错误、后端登录接口返回的结构和前端解析的字段对不上、localStorage 的 key 前后不一致。
7.2 分类管理增删改查
这是最简单的模块,先拿它验证基础链路,跑通了再测菜品。按顺序做四步:
- 新增:填分类名称和排序值 → 提交 → 列表里应该立刻出现新行。
- 编辑:点修改 → 改名称 → 保存 → 列表对应行同步更新。
- 查询:输入分类名关键词搜索 → 结果只包含匹配项。
- 删除:点删除 → 确认弹窗 → 行消失,刷新页面后仍然是消失状态。
判断成功的关键在最后一条:刷新后数据还在,说明真的落库了;如果只在页面上消失、刷新又回来,说明前端只是删了数组元素,请求根本没发出去或者后端没执行 DELETE。
一个常见的业务细节:如果菜品表有外键或逻辑关联到这个分类,删除分类时应该给出「该分类下还有菜品,无法删除」的提示。这是答辩时很容易被问到的点,值得在源码里确认一下有没有做这个校验。
7.3 菜品管理完整链路
菜品模块是项目的主体,测的维度也多:
- 新增菜品:填名称、选择分类(下拉框数据来自分类接口,能正常回显说明两个模块联动正常)、价格、上传图片、填简介 → 保存。
- 分页测试:默认每页 10 条,翻到第二页,检查数据是否正确切换、总数是否准确。可以往库里插 25 条测试数据,看分页能不能翻出 3 页。
- 条件查询:按名称模糊搜索、按分类筛选、按状态(上架/下架)过滤,三个条件组合时结果是否正确。
- 图片上传:上传一张 200KB 以内的 JPG,保存后列表和详情页都应该能正常显示缩略图。
- 编辑与删除:同上,重点验证刷新后状态保持。
图片上传是最容易出问题的环节,判断成功的标准是:数据库里 image 字段存的是可访问的路径(相对路径 /upload/xxx.jpg 或完整 URL),浏览器直接访问这个路径能看到图片。如果数据库存了路径但浏览器打开是 404,问题在静态资源映射没配或上传目录不对,去 application.yml 里找上传路径配置,再检查有没有 WebMvcConfigurer 配置 addResourceHandlers。
7.4 边界与异常测试
正常流程跑通只能说明「happy path」没问题,真正体现质量的是边界情况。建议至少补这几个用例:
| 测试项 | 输入 | 期望行为 |
|---|---|---|
| 空名称提交 | 名称留空 | 前端表单校验拦截,或后端返回参数错误提示 |
| 价格负数 | 价格填 -10 | 应被拦截,不允许入库 |
| 超长文本 | 名称填 200 个字符 | 返回字段长度超限提示,而不是 500 |
| 不存在的 ID | 请求 /dish/99999 |
返回「数据不存在」,而不是空指针异常 |
| 重复提交 | 快速连点两次保存 | 只生成一条记录,或后端有幂等/唯一约束 |
| 大图片上传 | 上传 20MB 图片 | 返回明确的大小超限提示 |
这一轮测完,你会发现很多模板项目的校验其实很薄——大多只做了前端校验,后端完全裸奔。如果这是你的毕设,建议至少把后端参数校验补上(@NotBlank、@Min、@Valid),答辩时能明显加分。
8. 接口 API 调用与批量数据处理
8.1 用 curl 快速回归接口
后端是标准 REST 服务,除了前端调用,也可以直接用命令行验证。这套命令很适合改完代码后做快速回归:
bash复制# 新增一条菜品
curl -X POST "http://127.0.0.1:8080/dish/add" \
-H "Content-Type: application/json" \
-H "Authorization: 你的token" \
-d '{"name":"测试菜品","categoryId":1,"price":38.50,"status":1,"description":"接口测试数据"}'
# 修改
curl -X PUT "http://127.0.0.1:8080/dish/update" \
-H "Content-Type: application/json" \
-d '{"id":1,"name":"修改后的名称","price":45.00}'
# 删除
curl -X DELETE "http://127.0.0.1:8080/dish/1"
注意 Authorization 头是否必须,取决于项目有没有加拦截器。如果加了 JWT 拦截器,不带 token 会直接返回 401,这时候要么先调登录接口拿 token,要么在拦截器配置里放行测试路径。
8.2 用 Python 做批量导入
项目本身可能没有「Excel 批量导入」功能,但批量造测试数据是做演示和压测的刚需。写个脚本循环调接口最直接:
python复制import time
import requests
BASE_URL = "http://127.0.0.1:8080"
TOKEN = "登录后拿到的token"
headers = {
"Content-Type": "application/json",
"Authorization": TOKEN
}
# 批量新增菜品,名称带序号方便识别
success, failed = 0, []
for i in range(1, 51):
payload = {
"name": f"测试菜品{i:03d}",
"categoryId": (i % 5) + 1,
"price": round(10 + i * 1.5, 2),
"status": 1,
"description": f"批量导入的第 {i} 条数据"
}
try:
resp = requests.post(f"{BASE_URL}/dish/add",
json=payload, headers=headers, timeout=10)
result = resp.json()
if result.get("code") == 200:
success += 1
else:
failed.append((i, result.get("msg")))
except Exception as e:
failed.append((i, str(e)))
time.sleep(0.1) # 控制频率,避免打满连接池
print(f"成功 {success} 条,失败 {len(failed)} 条")
for item in failed:
print("失败明细:", item)
这个脚本有两个工程化细节值得保留:一是 time.sleep 控制频率,本地环境无所谓,但接口有限流时这是必需的;二是失败明细单独收集,批量任务最忌讳「跑完了但不知道哪几条失败」。
如果接口量大,比如要导入几千条,不建议逐条调接口,直接写 SQL 批量插入更快:
sql复制-- 批量插入测试数据,用存储过程或脚本生成
INSERT INTO dish (category_id, name, price, status, description)
SELECT (n % 5) + 1,
CONCAT('批量菜品', n),
ROUND(10 + n * 1.5, 2),
1,
CONCAT('第', n, '条测试数据')
FROM (
SELECT @row := @row + 1 AS n
FROM information_schema.columns, (SELECT @row := 0) t
LIMIT 500
) AS seq;
批量任务的三条原则:先小批量验证、跑之前备份、跑完核对总数。500 条数据跑完,用 SELECT COUNT(*) 对一下是不是真的多了 500 条,别相信「没报错就是成功」。
8.3 接口文档与联调建议
如果项目集成了 Swagger 或 Knife4j,启动后访问 /doc.html 或 /swagger-ui.html 就能看到接口列表,这是最省事的联调方式。如果没有,建议自己加一个依赖,成本很低,收益是把「接口有哪些、参数什么格式」这件事从口头沟通变成可查文档。前后端分离项目最大的时间浪费就是参数对不上,有了文档能省一大半。
9. 资源占用与性能观察
这类项目不需要看显存,但内存、CPU 和接口响应时间值得观察,尤其是答辩演示时卡顿会很尴尬。
后端启动后,用任务管理器或 jps + jconsole 看内存占用,一个典型的 SpringBoot 单体应用空跑占 300MB~600MB 堆内存属于正常范围,接口调用后会略微上涨。如果内存持续上涨不回落,可能是某处查询把全表数据一次性加载了,重点看有没有 selectList(null) 这种没有条件的查询。前端 Vite dev server 占 200MB 左右,属于正常。
真正影响体验的是接口响应时间。观察方法很简单,浏览器 F12 → Network 面板,看每个请求的 Time。列表接口在几百条数据下应该几十毫秒内返回,如果超过 500ms,按这个顺序查:
第一个看分页有没有真的在数据库层做。如果项目是「查出全表再在 Java 里 subList」,数据量一大立刻卡死。正确的做法是 LIMIT 下推到 SQL,MyBatis-Plus 的分页插件就是干这个的,检查有没有配 PaginationInnerInterceptor。
第二个看有没有 N+1 查询。菜品列表每条记录都要显示分类名称,如果实现方式是「查一次菜品列表,再循环里对每条菜品查一次分类」,100 条菜品就是 101 次查询。正确做法是关联查询或批量查分类再在内存里拼。
第三个看索引。WHERE name LIKE '%鱼%' 这种前置通配符用不上索引,这是 SQL 本身的性质,不是配置问题。数据量小的项目感知不明显,但要知道原因。
第四个看图片。列表页如果直接加载原图,20 张 2MB 的图就是 40MB 流量,页面会明显卡顿。建议上传时就压缩到合理尺寸(列表缩略图 200px 宽足够),或者后端返回缩略图路径。
降低资源占用的实用手段:调小数据库连接池上限(spring.datasource.hikari.maximum-pool-size 设成 10 左右够用)、关闭不必要的启动加载、前端生产构建时开启代码分割。收益最大的一条是图片压缩,比任何代码优化都立竿见影。
10. 常见问题与排查方法
把下面这张表存下来,遇到问题先对照,能省大量瞎试的时间。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
后端启动报 Port 8080 was already in use |
端口被占用 | netstat -ano | findstr 8080(Win) |
换端口或结束占用进程 |
启动报 Communications link failure |
MySQL 未启动或地址错误 | 用客户端直连测试 | 启动 MySQL,核对 url 中主机端口 |
报 Access denied for user |
账号密码错误或无权限 | 命令行 mysql -u root -p 验证 |
修改 application.yml 账号密码 |
报 Unknown database |
库没建或名字不一致 | SHOW DATABASES; |
建库或改配置中的库名 |
| 中文显示乱码 | 字符集不是 utf8mb4 | 查库、表、连接三处字符集 | 统一改成 utf8mb4 并重连 |
npm install 卡住或超时 |
网络或源问题 | 换镜像源重试 | 配置国内 registry |
npm run dev 报 Node 版本错误 |
Node 版本过低 | node -v |
升级到 18+ |
| 前端页面空白 | 路由或组件报错 | F12 Console 看红字 | 按报错定位具体组件 |
| 接口 404 | 路径不匹配或代理 rewrite 错误 | 看 Network 里的真实请求地址 | 核对 @RequestMapping 与代理配置 |
| 接口 401 | 未带 token 或 token 过期 | 看请求头 | 重新登录或放行测试路径 |
跨域报错 CORS policy |
代理没生效或后端没配 CORS | 看请求是否走了 5173 本地 | 重启 dev server,检查代理配置 |
| 图片上传成功但显示 404 | 静态资源映射缺失 | 直接访问图片 URL 测试 | 配置 addResourceHandlers |
| 列表查不到数据 | 条件过严或库里没数据 | 直接跑 SQL 验证 | 放宽条件或补测试数据 |
| 删除后刷新又出现 | 前端未真正发请求 | Network 看有没有 DELETE | 修正调用逻辑 |
| 打包后前端接口全 404 | 生产环境没配代理 | 看 Nginx 配置 | 配置反向代理到后端 |
| 实体类满屏红线但能编译 | Lombok 未启用 | 看 IDE 是否装插件 | 开启注解处理 |
单独说两个最容易卡住新手的。
跨域问题。很多人改了 vite.config.js 但没重启,代理不生效,然后开始怀疑后端。判断方法很简单:F12 Network 里看请求的 URL,如果是 http://localhost:5173/api/xxx,说明走的是代理;如果是 http://127.0.0.1:8080/xxx,说明代理没起作用,请求是直连的,那必然跨域。
静态资源 404。上传成功的判断不能只看「前端提示成功」,要去服务器上看文件到底落在哪个目录。常见配置是 file.upload-path: D:/upload/,那么访问路径 /upload/xxx.jpg 必须映射到这个物理目录。映射没配,文件在磁盘上躺着,浏览器就是打不开。
11. 最佳实践与交付建议
把项目跑通只是第一步,如果这是要交的毕设或者要写进简历的项目,还有几件事值得做。
第一,先固守一套最小可运行配置。环境变量、数据库账号、端口,这些用一份 application-dev.yml 管好,改坏了能立刻回滚。很多人折腾半天把环境搞乱,最后连原来的状态都回不去。
第二,目录分治。数据库脚本放 sql/,上传文件放独立目录,日志单独输出到 logs/,不要把上传的图片混在项目源码里。提交代码前把 target/、node_modules/、dist/ 加进 .gitignore,这几个目录动辄几百 MB,仓库会变得很难用。
第三,补充后端校验。前面测边界时你应该已经发现模板项目校验很薄,至少加上 @Valid + @NotBlank + @Min,配合全局异常处理器返回统一格式的错误信息。这块工作量不大,但对项目质量提升很明显。
第四,接口层加统一日志。用一个拦截器或 AOP 记录「谁在什么时候调了哪个接口、耗时多少、返回什么状态」,排查问题时这东西比任何注释都有用。
第五,交付物清单要齐。一份毕设项目通常需要:可运行源码、.sql 初始化脚本、环境说明文档(写清 JDK/Node/MySQL 版本)、启动步骤、功能截图、常见问题说明。这些东西在答辩前一周准备好,别等到演示当天才想起来环境在另一台电脑上跑不起来。
第六,合规自查。图片素材来源要能说清楚,测试数据不能包含真实个人信息,如果项目里开放了注册或评论功能,要说明数据仅用于本地演示。这些东西写进 README,比藏在代码里更稳妥。
12. 继续往下走的方向
如果你已经把这个项目管理后台跑通,接下来有几个方向可以继续挖,难度从低到高。
最低成本的是换皮:把美食主题换成图书管理、设备管理、课程管理,只改字段名和页面文案,一天就能出一套新的,适合快速积累简历项目数量。中等成本的是补权限:现在的登录大多只判断「登没登录」,可以往上加角色(管理员/普通用户)和菜单级权限,用拦截器 + 数据库权限表实现,这是面试常问的点。再往上可以加统计看板:菜品分类占比、月度上架趋势,用 ECharts 画几张图,答辩演示效果会好很多。
工程化方向可以做的有:把图片存储从本地目录换成对象存储、给接口加 Redis 缓存热点数据、用 Docker Compose 把 MySQL + 后端 + 前端一键拉起、用 Nginx 做生产环境部署和反向代理。这几个点随便挑一个做扎实,都能在简历上写清楚「做了什么、解决了什么问题」。
最后提醒一句最容易踩的坑:这类项目网上流传的版本很多,配置和接口路径差异不小,不要照着某篇教程硬套参数,永远以你手上源码的 pom.xml、application.yml、vite.config.js 为准。启动顺序也别搞反——先确保 MySQL 能连、库表有数据,再启后端验证接口,最后起前端联调页面,这个顺序能把 80% 的「页面打不开」挡在门外。
