写校园食堂点评系统这类毕业设计时,最容易出现的问题不是某个功能不会写,而是“数据库表之间是什么关系、前后端如何约定接口、评论状态该由谁控制”这些整体性问题没有梳理清楚。一个看起来只是“给食堂打分、留言”的项目,实际上要同时处理用户登录、食堂和菜品展示、评价数据落库、管理端审核、统计分数等多条业务链路。
这篇文章围绕一个可复现的 Spring Boot + Vue + MySQL 校园食堂点评系统,按毕业设计最常见的路线来做:先拆需求,再设计表结构,然后完成后端接口,再写 Vue 页面,最后做联调和验收。项目会覆盖学生端和管理员端,包含 JWT 登录、点评提交、管理员审核、评分展示等核心模块,代码示例可以直接作为你本地项目的起点。
1. 先想清楚“校园食堂点评系统”要管什么、谁来用
这一节不是写需求文档,而是决定后面的数据库表和接口怎么拆。很多毕业设计做不下去,是因为一上来就写代码,做完之后发现功能之间互相矛盾,例如学生端能看到未审核的脏评论,或者管理员无法修改菜品价格。
1.1 角色只有两类,但权限要拆分清楚
校园食堂点评系统可以设置两类角色,一个是普通学生用户,一个是管理员。如果论文需要再加一层“食堂窗口管理员”,也可以承接这个模型,只多维护一个role字段即可。
- 学生:注册、登录、查看食堂窗口、查看菜品、提交评分和文字点评、查看自己的历史点评。
- 管理员:维护食堂窗口信息、维护菜品、管理学生点评状态、查看整体评分统计。
这两类角色会直接影响 API 设计。学生在调接口时只能操作canteen、dish、review中的部分资源,管理员需要额外的管理接口。即使不做复杂权限框架,也要在后端 Controller 上做基本的角色判断。
1.2 点评系统的核心实体只有四个
一个食堂点评系统的业务闭环可以拆成四张核心表:用户表、食堂窗口表、菜品表、点评表。
用户和点评是一对多关系,一个学生可以发表多条点评。食堂窗口和菜品是一对多关系,一个窗口有多个菜品。菜品表和点评表的关系需要注意:如果点评必须绑定菜品,会让“对某个窗口整体评价”变得很别扭;如果点评只能绑定窗口,又无法体现“哪个菜不好吃”。推荐做法是点评表同时允许窗口级和菜品级评价,用dish_id可空来区分。
1.3 功能清单可以先列成验收表
在设计阶段,把功能点做成表格,后续开发完一条勾一条。
| 功能模块 | 角色 | 操作说明 | 是否必须 |
|---|---|---|---|
| 注册登录 | 学生 | 用户名密码注册,登录后获取 token | 是 |
| 窗口列表 | 学生 | 分页查看食堂窗口及平均评分 | 是 |
| 菜品列表 | 学生 | 查看指定窗口下的菜品 | 是 |
| 提交点评 | 学生 | 选择窗口、菜名、评分、文字内容 | 是 |
| 我的点评 | 学生 | 查看自己提交的点评及审核状态 | 是 |
| 窗口维护 | 管理员 | 新增、编辑、下架食堂窗口 | 是 |
| 菜品维护 | 管理员 | 维护窗口下的菜品和价格 | 是 |
| 点评审核 | 管理员 | 通过或屏蔽学生点评 | 是 |
| 评分统计 | 管理员 | 查看窗口平均分和点评量 | 可选 |
从这张表可以看出来,数据库字段必须包含“审核状态”和“平均分展示”这两个不容易一眼看到的需求。
2. 数据库设计:用 MySQL 把用户、窗口、菜品、点评串起来
数据库是整个项目的基石。把表结构提前确定好,后端 Mapper 层会省很多事,Vue 页面需要的字段也能提前对齐。
2.1 建库和表结构
新建数据库campus_canteen,字符集使用utf8mb4。不要在 Linux 或测试环境里把中文直接放进latin1库,否则保存评论时会出现乱码。
CREATE DATABASE campus_canteen DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE campus_canteen;用户表保存登录凭据和昵称。密码字段建议至少使用 BCrypt 加密;如果是课程设计为了演示,可以在项目里先存明文,但论文和答辩要说明这是简化处理,后续需要升级。
CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT '登录密码', nickname VARCHAR(50) DEFAULT '' COMMENT '昵称', role VARCHAR(20) DEFAULT 'STUDENT' COMMENT '角色:ADMIN/STUDENT', avatar VARCHAR(255) DEFAULT '' COMMENT '头像地址', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='用户表';窗口表保存食堂名称、位置、介绍和状态。avg_score字段是先冗余存储平均分,还是每次都通过AVG函数现场计算,这里可以保留冗余字段,简化主列表查询。
CREATE TABLE t_canteen ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', name VARCHAR(100) NOT NULL COMMENT '食堂窗口名称', location VARCHAR(100) DEFAULT '' COMMENT '所在校区/楼层', description VARCHAR(500) DEFAULT '' COMMENT '介绍', cover_url VARCHAR(255) DEFAULT '' COMMENT '图片地址', avg_score DECIMAL(3,2) DEFAULT 0 COMMENT '平均评分', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='食堂窗口表';菜品表需要关联canteen_id,并保留价格、图片和简介。价格使用DECIMAL(10,2),不使用FLOAT,避免精度问题。
CREATE TABLE t_dish ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', canteen_id BIGINT NOT NULL COMMENT '所属食堂窗口', name VARCHAR(100) NOT NULL COMMENT '菜品名称', price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '价格', cover_url VARCHAR(255) DEFAULT '' COMMENT '图片地址', description VARCHAR(500) DEFAULT '' COMMENT '简介', status TINYINT DEFAULT 1 COMMENT '1在售 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='菜品表';点评表是本系统的核心。user_id、canteen_id关联用户和窗口,dish_id可空,表示这条点评是对具体菜品的评价,还是对窗口整体环境的评价。score取 1 到 5,status控制审核流程,0 表示待审核,1 表示已通过,2 表示已屏蔽。
CREATE TABLE t_review ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', user_id BIGINT NOT NULL COMMENT '用户ID', canteen_id BIGINT NOT NULL COMMENT '窗口ID', dish_id BIGINT DEFAULT NULL COMMENT '菜品ID,可空', score INT NOT NULL COMMENT '评分1-5', content VARCHAR(1000) DEFAULT '' COMMENT '点评内容', status TINYINT DEFAULT 0 COMMENT '0待审核 1已通过 2已屏蔽', reply_content VARCHAR(500) DEFAULT '' COMMENT '管理员回复', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='学生点评表';2.2 平均分字段如何维护
如果窗口列表每次都去t_review表计算AVG(score),当表数据量上来后,查询会变慢。因此t_canteen表里保留了avg_score字段。学生提交一条通过审核的点评后,后端可以在一个事务内重新计算该窗口平均分,并更新到窗口表:
UPDATE t_canteen AS c SET avg_score = ( SELECT ROUND(AVG(r.score), 2) FROM t_review AS r WHERE r.canteen_id = c.id AND r.status = 1 ) WHERE c.id = #{canteenId};这里有两个细节:平均分只计算status = 1的已审核点评;更新平均分要放在事务里,不能只修改t_review表,否则学生端看到的分数和列表不一致。
2.3 为什么不建外键
毕业设计阶段不用在 MySQL 物理层加太多外键约束。把user_id、canteen_id、dish_id设计成普通索引字段,通过应用层保证逻辑关系,数据库压力小,删除数据也更灵活。
实际项目里可以考虑为三张关联表分别建立索引:
ALTER TABLE t_dish ADD INDEX idx_dish_canteen(canteen_id); ALTER TABLE t_review ADD INDEX idx_review_user(user_id); ALTER TABLE t_review ADD INDEX idx_review_canteen_status(canteen_id, status);如果t_review的查询条件固定在某个食堂窗口下按状态过滤,联合索引(canteen_id, status)能明显减少回表。
3. Spring Boot 后端搭建:目录、依赖和配置文件
后端项目用 Maven 管理,最核心的依赖是 Spring Web、MySQL 驱动、MyBatis-Plus 和 JWT 工具类。很多人会在这一步踩到版本坑,下面按一个可用组合来写。
3.1 项目目录结构
采用分层结构,包名建议用com.campus.canteen。一个标准后端工程目录如下:
backend ├── src/main/java/com/campus/canteen │ ├── CampusCanteenApplication.java │ ├── common │ │ ├── RestResult.java │ │ └── BusinessException.java │ ├── config │ │ ├── CorsConfig.java │ │ └── WebInterceptorConfig.java │ ├── controller │ │ ├── AuthController.java │ │ ├── CanteenController.java │ │ ├── DishController.java │ │ └── ReviewController.java │ ├── interceptor │ │ └── TokenInterceptor.java │ ├── mapper │ │ ├── UserMapper.java │ │ ├── CanteenMapper.java │ │ ├── DishMapper.java │ │ └── ReviewMapper.java │ ├── entity │ │ ├── User.java │ │ ├── Canteen.java │ │ ├── Dish.java │ │ └── Review.java │ ├── dto │ │ ├── LoginDTO.java │ │ └── ReviewDTO.java │ ├── service │ │ ├── UserService.java │ │ └── ReviewService.java │ └── util │ └── JwtUtil.java └── src/main/resources ├── application.yml └── sql/campus_canteen.sql这个结构不复杂,但是能保证后面写 Controller、Service、Mapper 时不会乱。
3.2 pom.xml 依赖版本
Spring Boot 2.7.x 和 Spring Boot 3.x 的兼容方式不同。如果本机 JDK 是 1.8,建议使用 Spring Boot 2.7 系列;如果本机已经安装 JDK 17 并希望使用较新的版本,再选择 Spring Boot 3.2.x。下面的依赖以 Spring Boot 3.2.4 为例:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.4</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> </dependencies>如果使用 Spring Boot 2.7.x,则依赖需要把mybatis-plus-spring-boot3-starter改成mybatis-plus-boot-starter,同时注意 JWT 库不需要变化。常见的一个坑是直接把 Spring Boot 3 的代码复制到 JDK 1.8 环境中编译,结果出现类似org/apache/ibatis/binding/MapperRegistry或java: 不支持发行版本 17的错误。版本匹配问题要先在创建项目时检查清楚。
3.3 application.yml 关键配置
配置文件里最容易出问题的是 MySQL URL 参数。MySQL 8 默认时区是 UTC,如果不在 URL 中指定serverTimezone,后端驱动会报异常。
server: port: 8080 spring: application: name: campus-canteen-backend datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_canteen?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 servlet: multipart: max-file-size: 5MB max-request-size: 10MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case必须开启,这样数据库里的create_time才能自动映射到实体的createTime字段。log-impl用StdOutImpl是为了本地调试方便,生产环境建议去掉或改成日志框架存储。
3.4 统一返回结果和后端遇到的常见异常
前后端沟通时不能一个接口返回Map,另一个返回字符串,要统一格式。一般约定如下:
public class RestResult<T> { private Integer code; private String message; private T data; public static <T> RestResult<T> success(T data) { RestResult<T> result = new RestResult<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> RestResult<T> error(Integer code, String message) { RestResult<T> result = new RestResult<>(); result.code = code; result.message = message; return result; } }代码中尽量不要直接把Exception.getMessage()返回给前端,因为数据库驱动信息和内部堆栈会暴露太多实现细节。Controller 层做统一的业务异常处理即可。
4. 后端核心模块:JWT 登录、点评提交和审核流程
毕业设计里用户登录最常见的实现是 JWT 方式。用户登录成功后,后端签发一个 token,Vue 把它存在浏览器,并在后续请求头中携带。下面是一段可用于教学的最小实现。
4.1 用 JwtUtil 生成校验收到的 token
登录鉴权先编写工具类。使用 JJWT 时需要准备密钥,密钥长度要满足 HS256 算法要求。
public class JwtUtil { private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor("campus-canteen-secret-key-2024-change-me".getBytes(StandardCharsets.UTF_8)); public static String generateToken(Long userId, String role) { long expireMillis = 7L * 24 * 3600 * 1000; return Jwts.builder() .setSubject(userId.toString()) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expireMillis)) .signWith(SECRET_KEY, SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(SECRET_KEY) .build() .parseClaimsJws(token) .getBody(); } }这个类有两个注意点:密钥不要硬编码写进代码里,毕业设计本地跑没问题,但论文里要说明正式环境密钥应放到环境变量或配置中心;setClaims中的role只是给前端做展示,真正的权限校验依然要以后端查到的数据库角色为准。
4.2 登录接口
用户通过username和password登录,校验成功后返回用户基本信息和 token。密码校验这里直接使用数据库字段匹配,生产项目需要改成 BCrypt 校验。
@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private UserMapper userMapper; @PostMapping("/login") public RestResult<Map<String, Object>> login(@RequestBody LoginDTO dto) { User user = userMapper.selectOne( new LambdaQueryWrapper<User>() .eq(User::getUsername, dto.getUsername()) ); if (user == null || !user.getPassword().equals(dto.getPassword())) { return RestResult.error(401, "用户名或密码错误"); } String token = JwtUtil.generateToken(user.getId(), user.getRole()); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("nickname", user.getNickname()); data.put("role", user.getRole()); return RestResult.success(data); } }为了管理员能登录,表数据里至少要准备一个管理员账号:
INSERT INTO t_user (username, password, nickname, role) VALUES ('admin', '123456', '系统管理员', 'ADMIN');这条 SQL 只是演示,答辩时最好说明密码是 BCrypt 加密后的结果,演示环境为了方便可以保留明文账号,但生产系统万万不能这样处理。
4.3 点评提交接口
学生提交点评时要验证 token,判断当前用户的身份,同时校验分数范围。这个接口需要接收窗口、菜品、分数和文字内容。
@RestController @RequestMapping("/api/review") public class ReviewController { @Resource private ReviewMapper reviewMapper; @PostMapping("/submit") public RestResult<String> submit(@RequestBody ReviewDTO dto, HttpServletRequest request) { Long userId = CurrentUserHolder.getUserId(request); if (userId == null) { return RestResult.error(401, "请先登录"); } if (dto.getScore() == null || dto.getScore() < 1 || dto.getScore() > 5) { return RestResult.error(400, "评分必须在1到5之间"); } Review review = new Review(); review.setUserId(userId); review.setCanteenId(dto.getCanteenId()); review.setDishId(dto.getDishId()); review.setScore(dto.getScore()); review.setContent(dto.getContent()); review.setStatus(0); reviewMapper.insert(review); return RestResult.success("点评已提交,等待管理员审核"); } }注意CurrentUserHolder是拦截器往 ThreadLocal 里写入的当前用户信息,这样才能从HttpServletRequest去验证已经解析出来的 userId。使用独立的拦截器而不是在业务代码里频繁调用parseToken,可以避免大量重复代码。
4.4 管理员审核接口
管理员审核点评本质上是一个状态更新接口。需要区分角色,否则学生也能把自己的点评直接改成通过状态。
@PutMapping("/admin/review/{id}") public RestResult<String> audit(HttpServletRequest request, @PathVariable Long id, @RequestParam Integer status) { User currentUser = (User) request.getAttribute("currentUser"); if (!"ADMIN".equals(currentUser.getRole())) { return RestResult.error(403, "没有管理权限"); } Review review = reviewMapper.selectById(id); if (review == null) { return RestResult.error(404, "点评不存在"); } review.setStatus(status); reviewMapper.updateById(review); // 同步更新窗口平均分 updateCanteenAverageScore(review.getCanteenId()); return RestResult.success("操作成功"); }审核动作只会用status = 1或status = 2。审核为通过后,窗口平均分才被影响。这样设计的好处是未审核点评不会污染展示数据,但坏处是最近提交的点评在管理员审核之前,学生端看不到。
4.5 拦截器和 CORS 配置
后端项目如果直接让 Vue 页面请求http://localhost:8080/api/...,而前端页面跑在http://localhost:5173,会产生跨域问题。最稳妥的方法是后端配置允许跨域,同时前端也做好开发代理,双保险。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }如果你使用了 token 拦截器,还要留意OPTIONS请求。浏览器发来的预检请求没有实战的 Authorization 头,因此拦截器对/api/auth/login和请求方法为OPTIONS的请求要直接放行。
5. Vue 3 前端实现:环境准备、路由、页面组件
前端选择 Vue 3 + Vite + Element Plus。相比 Vue 2,Vue 3 的组合式 API 更适合管理食堂点评这种包含表单提交、数据列表和权限判断的中小型管理页面。
5.1 初始化项目并安装依赖
本机建议先检查 Node.js 版本。Vite 5 要求 Node.js 18 或更高,如果安装后出现报错,第一步不是重装项目,而是执行node -v确认版本。
npm create vite@latest campus-canteen-web -- --template vue cd campus-canteen-web npm install npm install axios vue-router element-plus @element-plus/icons-vue启动开发服务器:
npm run dev如果项目目录名里包含中文或特殊字符,某些版本的老打包工具会出现路径异常,建议所有前端项目目录都用英文字母。
5.2 封装 axios 请求
前后端联调时,统一封装 axios 能减少重复代码。示例放到src/api/request.js:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use((config) => { const token = localStorage.getItem('campus_token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( (response) => { const res = response.data if (res.code === 200) { return res.data } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) }, (error) => { if (error.response && error.response.status === 401) { localStorage.removeItem('campus_token') router.push('/login') } ElMessage.error('网络异常') return Promise.reject(error) } ) export default request这里还有一个小细节:baseURL写/api,然后让 Vite 的本地代理转发到 Spring Boot。开发环境代理配置在vite.config.js中:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { host: '0.0.0.0', port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })使用代理而不是直接写http://localhost:8080,能避免大量 CORS 报错,也让前端代码更整洁。
5.3 配置路由和登录守卫
食堂点评系统至少需要登录页、首页、窗口详情、我的点评、管理员布局五个路由。Vue Router 配置示例:
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('../views/Login.vue') }, { path: '/', component: () => import('../views/Home.vue') }, { path: '/canteen/:id', component: () => import('../views/CanteenDetail.vue') }, { path: '/admin', component: () => import('../views/admin/AdminLayout.vue'), meta: { role: 'ADMIN' }, children: [ { path: '', component: () => import('../views/admin/AdminCanteen.vue') }, { path: 'review', component: () => import('../views/admin/AdminReview.vue') } ] } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('campus_token') const role = localStorage.getItem('campus_role') if (to.path === '/login') { next() return } if (!token) { next('/login') return } if (to.meta.role && to.meta.role !== role) { next('/') return } next() }) export default router路由守卫是解决“学生直接访问管理员页面”的关键地方。不要只在前端隐藏菜单,路由守卫和后端接口角色校验要同时存在。
5.4 点评表单页面
食堂窗口详情页应同时展示窗口信息、菜品列表和点评提交区。点击某个菜品后,可以在弹窗里填写评分和文字。下面是一个简化后的 Vue 组件关键片段:
<template> <div> <el-form :model="form" label-width="80px"> <el-form-item label="评分"> <el-rate v-model="form.score" /> </el-form-item> <el-form-item label="点评内容"> <el-input v-model="form.content" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="这道菜分量如何?口味怎样?" /> </el-form-item> <el-button type="primary" @click="submitReview">提交点评</el-button> </el-form> </div> </template> <script setup> import { reactive } from 'vue' import { useRoute } from 'vue-router' import { ElMessage } from 'element-plus' import request from '../api/request' const route = useRoute() const form = reactive({ score: 5, content: '', dishId: null }) async function submitReview() { const data = { canteenId: Number(route.params.id), dishId: form.dishId, score: form.score, content: form.content } await request.post('/review/submit', data) ElMessage.success('提交成功,等待管理员审核') form.content = '' } </script>组件里没有直接调用 axios 原生对象,而是用request实例管理,这样 token 失效、错误提示等都集中在拦截器里,不用每个页面都重复写。
5.5 Element Plus 表格管理页面
管理员页面中,用el-table展示点评数据比较方便。审核按钮只需要传递状态值:
<template> <el-table :data="reviewList"> <el-table-column prop="user.nickname" label="用户" /> <el-table-column prop="content" label="内容" /> <el-table-column label="状态"> <template #default="{ row }"> <el-tag :type="statusMap[row.status].type"> {{ statusMap[row.status].text }} </el-tag> </template> </el-table-column> <el-table-column label="操作"> <template #default="{ row }"> <el-button type="success" @click="audit(row, 1)">通过</el-button> <el-button type="danger" @click="audit(row, 2)">屏蔽</el-button> </template> </el-table-column> </el-table> </template>Element Plus 里的el-table-column自带前端分页,不代表后端就少做了分页。毕业设计如果想要展示“大数据量处理能力”,可以在后端接口借助 MyBatis-Plus 的分页插件完成分页,前端只接收records和total。
6. 前后端联调:启动顺序、验收用例和问题排查
本地运行这个项目,需要先启动 MySQL,再启动 Spring Boot,最后启动 Vue 开发服务器。启动顺序弄错时,前端页面能打开但接口全部失败,多数情况不是代码逻辑问题,而是后端数据库连接失败导致。
6.1 启动和初始化数据
- 在 MySQL 执行第二章的建表 SQL 和管理员账号 SQL。
- 启动 Spring Boot 主类,观察控制台端口是不是
8080,以及 MyBatis-Plus 是否打印 SQL 日志。 - 在
campus-canteen-web目录执行npm run dev,浏览器访问http://localhost:5173。
如果启动后端时报Access denied for user 'root'@'localhost',先检查application.yml里的用户名密码与本地 MySQL 是否一致。如果报Unknown database 'campus_canteen',说明 SQL 脚本没有执行,需要先用 MySQL 客户端连接并建库。
6.2 推荐验收用例
不要只看页面能打开就认为项目已经完成,要逐条走一遍核心业务。下面这张表可以直接对应论文里的功能测试用例。
| 编号 | 操作入口 | 操作内容 | 预期结果 |
|---|---|---|---|
| 1 | 登录页 | 用admin/123456登录 | 跳转到管理页面,本地存储 token |
| 2 | 学生注册 | 注册新账号并登录 | 登录成功后能进入首页 |
| 3 | 首页列表 | 查看食堂窗口列表 | 窗口名称、位置、平均分均能看到 |
| 4 | 窗口详情 | 点击窗口查看菜品 | 显示该窗口下的菜品和价格 |
| 5 | 点评提交 | 给窗口/菜品打分并写内容 | 提示提交成功 |
| 6 | 管理员审核 | 在评审列表看到待审核内容 | 点击通过后点评状态变为“已通过” |
| 7 | 学生首页 | 刷新首页窗口平均分 | 平均分已按新审核点评更新 |
| 8 | 未登录访问 | 清理 localStorage 刷新页面 | 路由跳转到登录页 |
| 9 | 学生访问管理页 | 手动输入/admin | 被路由守卫拦截 |
| 10 | 后端点“通过” | 管理员角色调用审核接口 | 返回成功,数据库状态字段变化 |
其中第 7 条最容易遗漏。很多项目做了审核,但忘了在审核的同时更新窗口平均分。验收时一定要前后对比分数。
6.3 联调阶段的排错清单
最常见的报错如下。
| 现象 | 可能原因 | 检查方法 | 解决方案 |
|---|---|---|---|
| Vue 页面显示跨域错误 | 没有配置 CORS 或代理 | 查看浏览器 Network 中请求的 URL | 使用 Vite proxy 指向后端 |
| 登录接口 500 | MySQL URL 没有设置时区 | 查看后端异常日志 | 在serverTimezone=Asia/Shanghai |
| 接口返回 401 | token 没有放入请求头 | 查看 Network 请求头 | 检查 axios 请求拦截器 |
| 中文评论在数据库乱码 | 数据库字符集不是 utf8mb4 | 执行SHOW CREATE TABLE t_review | 建库时使用 utf8mb4,连接参数加入编码 |
启动报Invalid bound statement | Mapper XML 路径不一致 | 查看 target/classes 是否存在 XML | 调整 MyBatis mapper-locations 配置 |
| Vue Router 匹配不到页面 | 路由写错或缺少视图文件 | 命令行或控制台警告提示 | 确认动态路由导入路径正确 |
后端返回的数据前端读不到data | 返回体包装层数不一致 | 手动调用后端接口看 JSON | 统一使用 RestResult 并在 axios 拦截器处理 |
| Spring Boot 3 和 MyBatis-Plus 冲突 | 引入了mybatis-plus-boot-starter | 查看 NoClassDefFoundError | Spring Boot 3 使用mybatis-plus-spring-boot3-starter |
7. 写论文和答辩前还需要补充什么
校园食堂点评系统作为毕业设计,技术栈准确、功能闭环、能成功演示,已经是比较合格的选题。但如果想让项目更有竞争力,答辩时可以从这几个角度继续往下做。
7.1 数据权限和接口安全
当前系统使用 JWT 做登录,只实现了最基础的身份验证。可以在此基础上增加密码 BCrypt 加密、登录失败次数限制、接口限流、管理员操作日志。答辩时被问到“系统安全怎么保证”,不要只说有 token,而要从数据加密、权限校验、审核机制三个层面展开。
7.2 评论内容的敏感词处理
校园场景中的评论内容需要做基础过滤。后端可以在点评提交接口里维护一个简单的敏感词列表,包含词库后,命中就把该条点评打上“待人工审核”标记。再加入一层基于正则的过滤或接入第三方云内容审核服务,扩展性会更好。
7.3 部署上线方式
开发环境是npm run dev和 Spring Boot 的本地启动。毕业答辩前可以准备一套更接近生产环境的部署方案:前端执行npm run build,把dist放到 Nginx 目录;后端打成 jar 包用java -jar启动;Nginx 通过location /api把接口请求反向代理到 Spring Boot 应用;MySQL 单独跑在服务器上。
下面的本地部署可以先跑通:
npm run buildmvn clean package java -jar target/campus-canteen-0.0.1-SNAPSHOT.jar部署方案不需要太复杂,但是要在论文答辩 PPT 里画清楚三个服务之间的请求链路。面试官或导师一旦看到“开发环境能跑,且知道如何部署到服务器”,会认为项目完整度较高。
7.4 一个可以直接沿用的答题逻辑
如果被问到“这个系统里最难的模块是什么”,不用硬说登录或 CRUD 很难。你可以回答:
“核心难点是点评审核状态和窗口平均分之间的一致性。我通过在后端审核接口中同步更新平均分,并让学生端只展示 status=1 的评论,让业务规则落到了数据库字段和代码逻辑上。这个模型可以回滚,也能通过状态标记扩展为隐藏、举报等更多场景。”
这个回答既说明你理解数据库一致性,又能体现业务设计思维,回答结构比“我完成了增删改查”更清晰。
整个项目在动手前最值得花时间的不是装饰页面,而是把“用户、食堂、菜品、点评”四张表和角色权限的关系画清楚。把本文前面几节作为基础版本跑通,再按自己的学科方向加入图片上传、通知公告、Excel 导出或者订单预约等模块,就能形成一个完整且独特的毕业设计项目。