ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

校园食堂点评系统毕业设计:从数据库设计到匿名审核答辩要点

校园食堂点评系统毕业设计:从数据库设计到匿名审核答辩要点 做毕设最怕什么不是不会写代码而是老师验收时问一句“这个项目里最难的点在哪你改了哪些设计”你支支吾吾答不上来只能反复说“用了 Spring Boot Vue MySQL”。校园食堂点评系统就是这一类的典型题目。听上去它只是普通的管理系统无非是把“图书管理”换成“食堂窗口管理”再套一层登录。但如果你真去拆需求会发现它比图书管理多了一个非常真实的产品核心评分、匿名、审核和统计。这篇文章不会贴一个完整的整仓库源码让你直接下载而是教你拿到这类源码之后怎么把项目跑起来、怎么理解它的核心表结构、怎么看懂后端和前端的关键代码、怎么回答答辩老师的追问。如果你打算自己从零写一个这套拆解同样可以直接落地。我的核心判断是食堂点评系统的难点不在增删改查而在“一个评分从用户提交到前台展示中间经历了什么”。把这个流程讲透你的毕业设计就已经赢了一半。1. 选题分析食堂点评系统适合什么样的读者先聊选题值不值。很多计算机专业的学生会避开“点评”“论坛”“社区”类题目觉得要实现评论互动、敏感词、匿名、审核工作量比传统的单表 CRUD 大。但实际上这恰恰是这类题目比“XX管理系统”更有竞争力的原因。传统的管理系统页面是登录、列表、新增、编辑、删除。答辩时十分钟讲完老师其实很难找到有价值的问题。食堂点评系统则天然具备三个可以被追问的设计点匿名点评用户选了匿名之后前端展示时怎么隐藏用户名但管理员后台如何仍然能追溯评分设计口味、卫生、服务分别打分最终展示的综合分是怎么算出来的要不要支持不同权重内容审核新提交的点评不应该立即出现在前台管理员审核通过之后才公开展示那窗口的平均分什么时候更新如果你把这三个点想清楚了代码写明白答辩时基本是在带着老师看设计而不是被老师问倒。再说说技术栈。Spring Boot 负责提供 RESTful APIVue 负责页面交互MySQL 负责数据持久化这是目前高校毕业设计里最主流的一套组合网上资料多、排错成本低找参考时也不容易卡住。如果你拿到的是 Vue 2 的版本也没关系下面大部分页面逻辑是一样的差异主要集中在路由初始化和 Element UI 的使用方式上。本文示例偏向 Vue 3 Element Plus但思路完全通用。2. 系统模块划分不要一上来就写代码源码拿到手第一步不是看代码而是先看项目结构弄清楚这个系统被拆分成了哪些模块。一个标准的校园食堂点评系统按角色划分大致如下角色核心诉求对应功能学生查食堂、看评分、表达真实用餐体验注册、登录、浏览窗口、发布点评、匿名点评、查看个人点评食堂窗口了解自家评分、看学生反馈查看点评、查看评分统计部分系统会提供窗口管理入口系统管理员保证内容真实、平台有序用户管理、窗口审核、点评审核、公告管理、数据统计对毕设而言最简单的角色划分就是两类普通学生和管理员。窗口经营者不需要独立登录因为那会牵扯到新的角色表和权限逻辑增加的工作量大于答辩收益。核心业务闭环可以这样理解学生注册登录 - 浏览食堂窗口 - 对用餐窗口打分并提交文字点评 - 点评进入待审核状态 - 管理员审核通过 - 前台可见 - 窗口平均分与统计图表同步更新。这里面最容易被忽略的是“待审核状态”。很多新手会把点评直接插入数据库、直接展示出来这个流程虽然也能跑通但缺少业务深度。现实中任何 UGC 内容都需要审核点评系统更不例外。“审核后再展示”才能体现内容管理的设计意识。3. 数据库设计核心不是“能存下”而是“能算得对”数据库是这类系统的灵魂。你说你会 Spring Boot、会 Vue但表结构一展开老师基本就能看出你的设计水平。3.1 核心表拆解食堂点评系统至少需要四张核心表表名作用关键字段sys_user用户表包含学生和管理员username、password、role、statuscanteen_window食堂窗口表name、canteen_name、manager、avg_score、review_countreview_record点评记录表user_id、window_id、taste_score、hygiene_score、service_score、anonymous、statusmenu_item窗口菜单表用于展示窗口的菜品信息window_id、name、price、image可能有的系统里还会加一张公告表或者把角色单独抽成一张表。这里只讨论最小可用模型你先把它跑通再按源码或自己需求扩展。3.2 为什么点评表和窗口表会有关键设计陷阱重点说 review_record 表。一个点评系统最常见的错误是只给点评表留一个总分字段比如final_score。前端让用户打个分后端存个数字就完了。这个做法当场就能实现但老师一问“为什么只打一个总分这样的评价维度是不是太粗”你就很难自圆其说。我的建议是参考真实点评产品的做法把评分拆成几个小维度。食堂场景最合理的是三维度评分taste_score口味评分hygiene_score卫生评分service_score服务评分展示综合分时可以取三个维度的平均值也可以给不同维度配权重。比如口味占 40%卫生占 40%服务占 20%。毕设里用简单平均即可但如果你实现了权重配置反而是一个可以写进论文的亮点。之所以不用单总分还有一个逻辑上的好处你可以在列表页提供“按口味排序”“按卫生排序”的扩展也可以后续再加上维度字段而不用重构评分逻辑。下面给出一份可以直接执行的 MySQL 建表脚本CREATE DATABASE IF NOT EXISTS canteen_review DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE canteen_review; CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, 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 头像地址, role varchar(20) NOT NULL DEFAULT STUDENT COMMENT 角色ADMIN / STUDENT, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用, deleted tinyint(4) NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; CREATE TABLE canteen_window ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 窗口名称, canteen_name varchar(100) DEFAULT NULL COMMENT 所属食堂如一食堂、二食堂, description varchar(500) DEFAULT NULL COMMENT 窗口简介, image varchar(255) DEFAULT NULL COMMENT 窗口图片, manager varchar(50) DEFAULT NULL COMMENT 窗口负责人, avg_score decimal(3,2) NOT NULL DEFAULT 0.00 COMMENT 综合评分冗余字段, review_count int(11) NOT NULL DEFAULT 0 COMMENT 已通过点评数量, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1展示 0下架, deleted tinyint(4) NOT NULL DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT食堂窗口表; CREATE TABLE review_record ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 点评人ID, window_id bigint(20) NOT NULL COMMENT 被点评窗口ID, content varchar(1000) DEFAULT NULL COMMENT 文字点评内容, taste_score tinyint(4) NOT NULL DEFAULT 0 COMMENT 口味评分1-5, hygiene_score tinyint(4) NOT NULL DEFAULT 0 COMMENT 卫生评分1-5, service_score tinyint(4) NOT NULL DEFAULT 0 COMMENT 服务评分1-5, anonymous tinyint(4) NOT NULL DEFAULT 0 COMMENT 是否匿名0否 1是, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待审核 1已通过 2已驳回, reject_reason varchar(200) DEFAULT NULL COMMENT 驳回原因, deleted tinyint(4) NOT NULL DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_window_id (window_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT点评记录表; CREATE TABLE menu_item ( id bigint(20) NOT NULL AUTO_INCREMENT, window_id bigint(20) NOT NULL COMMENT 所属窗口ID, name varchar(100) NOT NULL COMMENT 菜品名称, price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 价格, image varchar(255) DEFAULT NULL COMMENT 菜品图片, recommended tinyint(4) NOT NULL DEFAULT 0 COMMENT 是否为推荐菜, deleted tinyint(4) NOT NULL DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT窗口菜单表;这里再解释两个容易被忽略的设计点。第一点评表没有设计总评分数字段。综合分通过taste_score hygiene_score service_score计算得到不要在数据库里冗余一个重复字段。如果你一定要在列表页快速排序那也应该是窗口表里的avg_score担当而不是点评表里的总评分。第二窗口表里的avg_score是冗余字段。常规设计里需要展示窗口平均分时直接查点评表求 AVG 也可以但每次列表页都做一次子查询会显得不够优雅。于是把平均分冗余到窗口表中点评审核通过后同步更新一次。这个字段可以让老师觉得你理解了“读多写少”的性能优化思路。但也要意识到冗余字段维护不好就会脏数据所以后台模块必须把“重算窗口平均分”封装成一个独立方法在审核通过、驳回、删除点评时都调用它。3.3 菜单表有什么作用很多同学会把菜单直接写死在窗口描述里这非常不利于后面的扩展。单独建一张 menu_item 表后食堂窗口就可以在前台或后台展示菜品窗口详情页也不至于只有一句话。对毕设答辩而言窗口详情页是给老师演示的最佳页面。你可以展示窗口图片、菜单列表、历史点评、评分分布四个区块。菜单表的存在让页面内容更饱满不显得单薄。4. 后端 Spring Boot核心代码怎么看Spring Boot 后端最忌讳的就是 Controller 里写了一大堆业务逻辑。拿到源码后先看包结构如果里面包含清晰的 controller、service、mapper、entity、config 分层这个项目的质量通常不会太差。4.1 典型包结构com.example.canteen ├── CanteenApplication.java ├── config │ ├── CorsConfig.java │ ├── JwtInterceptor.java │ └── MybatisPlusConfig.java ├── controller │ ├── AuthController.java │ ├── WindowController.java │ └── ReviewController.java ├── dto │ ├── LoginDTO.java │ └── ReviewSubmitDTO.java ├── entity │ ├── User.java │ ├── CanteenWindow.java │ └── ReviewRecord.java ├── mapper │ ├── UserMapper.java │ ├── CanteenWindowMapper.java │ └── ReviewRecordMapper.java ├── service │ ├── ReviewService.java │ └── impl │ └── ReviewServiceImpl.java └── common ├── Result.java └── ServiceException.java如果你的项目还多出 vo、utils、security 这类包也很正常。关键是看到代码时能快速说出每个类的作用。4.2 后端核心链路点评是怎么被提交的点评提交是系统的核心流程。先看 Controller// 文件路径com.example.canteen.controller.ReviewController.java RestController RequestMapping(/api/review) public class ReviewController { Resource private ReviewService reviewService; /** * 提交点评点评默认状态为待审核 */ PostMapping(/submit) public ResultVoid submit(RequestBody ReviewSubmitDTO dto) { reviewService.submitReview(dto); return Result.success(); } /** * 管理员分页查询点评 */ GetMapping(/admin/page) public ResultPageReviewRecord adminPage(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) Integer status) { return Result.success(reviewService.adminPage(page, size, status)); } /** * 管理员审核点评 */ PostMapping(/admin/audit/{id}) public ResultVoid audit(PathVariable Long id, RequestParam Integer status, RequestParam(required false) String rejectReason) { reviewService.auditReview(id, status, rejectReason); return Result.success(); } }这里的ReviewSubmitDTO是前端提交过来的对象里面包含窗口 ID、三个评分、点评内容和是否匿名。注意不要让前端直接把 user_id 传过来这是一个安全设计习惯。当前登录用户应该从 Token 里解析获取而不是信任客户端参数。再看 Service 层。真正值得细读的是这部分// 文件路径com.example.canteen.service.impl.ReviewServiceImpl.java Service public class ReviewServiceImpl implements ReviewService { Resource private ReviewRecordMapper reviewRecordMapper; Resource private CanteenWindowMapper canteenWindowMapper; Override Transactional(rollbackFor Exception.class) public void submitReview(ReviewSubmitDTO dto) { Long userId LoginUserHolder.getUserId(); // 判断用户是否已经点评过该窗口 Long count reviewRecordMapper.selectCount( new LambdaQueryWrapperReviewRecord() .eq(ReviewRecord::getUserId, userId) .eq(ReviewRecord::getWindowId, dto.getWindowId()) .in(ReviewRecord::getStatus, 0, 1) ); if (count ! null count 0) { throw new ServiceException(您已经点评过该窗口请勿重复提交); } // 校验分数范围避免前端传 0 或 100 之类的非法值 checkScore(dto.getTasteScore()); checkScore(dto.getHygieneScore()); checkScore(dto.getServiceScore()); // 创建点评记录状态默认为待审核 ReviewRecord record new ReviewRecord(); record.setUserId(userId); record.setWindowId(dto.getWindowId()); record.setContent(dto.getContent()); record.setTasteScore(dto.getTasteScore()); record.setHygieneScore(dto.getHygieneScore()); record.setServiceScore(dto.getServiceScore()); record.setAnonymous(dto.getAnonymous() ! null dto.getAnonymous() ? 1 : 0); record.setStatus(0); reviewRecordMapper.insert(record); } Override Transactional(rollbackFor Exception.class) public void auditReview(Long id, Integer status, String rejectReason) { ReviewRecord record reviewRecordMapper.selectById(id); if (record null) { throw new ServiceException(点评不存在); } record.setStatus(status); if (status 2) { record.setRejectReason(rejectReason); } reviewRecordMapper.updateById(record); // 审核通过或驳回都需要重算该窗口的平均分确保统计结果一致 recalcWindowScore(record.getWindowId()); } /** * 重算某个窗口的平均分只统计审核通过的点评 */ private void recalcWindowScore(Long windowId) { ListReviewRecord passedList reviewRecordMapper.selectList( new LambdaQueryWrapperReviewRecord() .eq(ReviewRecord::getWindowId, windowId) .eq(ReviewRecord::getStatus, 1) ); if (passedList.isEmpty()) { canteenWindowMapper.update(null, new LambdaUpdateWrapperCanteenWindow() .eq(CanteenWindow::getId, windowId) .set(CanteenWindow::getAvgScore, 0.00) .set(CanteenWindow::getReviewCount, 0)); return; } BigDecimal total BigDecimal.ZERO; for (ReviewRecord record : passedList) { BigDecimal scoreAvg BigDecimal.valueOf(record.getTasteScore()) .add(BigDecimal.valueOf(record.getHygieneScore())) .add(BigDecimal.valueOf(record.getServiceScore())) .divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP); total total.add(scoreAvg); } BigDecimal avg total.divide(BigDecimal.valueOf(passedList.size()), 2, RoundingMode.HALF_UP); canteenWindowMapper.update(null, new LambdaUpdateWrapperCanteenWindow() .eq(CanteenWindow::getId, windowId) .set(CanteenWindow::getAvgScore, avg) .set(CanteenWindow::getReviewCount, passedList.size())); } }这里的几个细节就是你答辩时能讲的东西加了Transactional(rollbackFor Exception.class)保证点评插入和窗口评分更新同时成功或同时失败。重复点评的判断放在 Service 层并且只拦截状态为待审核和已通过的记录。如果学生上次点评被管理员驳回了可以重新提交这个逻辑比简单的一刀切更合理。窗口平均分不是在前台查询时临时计算而是在审核动作之后重算保证展示分数和审核数据一致。如果你发现源码里没有recalcWindowScore只是把总评分存到一个字段里也不需要慌张。只要理解了上面这段代码想解决的问题你会自己在源码里改出一版更合理的设计这本身就已经是加分项。4.3 后端配置MySQL 连接与 MyBatis-Plus绝大多数校园食堂点评系统源码都会使用 MyBatis-Plus因为它能显著减少 Mapper 层的样板代码。如果你手里项目的pom.xml里没有 MyBatis-Plus而是使用了 Spring Data JPA代码会稍有不同但 service 的业务流程依旧一致。配置文件示例# 文件路径src/main/resources/application.yml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/canteen_review?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 # 如果使用的是 MySQL 5.x 且驱动版本较低driver-class-name 可能是 com.mysql.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 logging: level: com.example.canteen.mapper: debug连接串里的serverTimezoneAsia/Shanghai和characterEncodingutf8大概率能帮你避免中文乱码和日期差八小时的问题。allowPublicKeyRetrievaltrue是 MySQL 8 下常见的一个连接配置不加的话有时会报错。另外MyBatis-Plus 的逻辑删除字段如果不做配置实体里的deleted字段就会被当成普通字段处理。上面的配置已经把逻辑删除开关打开了这样执行deleteById时实际执行的是UPDATE ... SET deleted 1。要注意逻辑删除生效后所有查询都会自动带上deleted 0条件。5. Vue 前端页面结构与关键交互前端这部分你不需要背下每一行代码。重点理解三件事路由对应哪些页面、请求是怎么发出去的、点评表单里哪些交互会直接影响后端展示。5.1 页面与路由典型的 Vue 页面结构如下src ├── api │ ├── request.js │ └── review.js ├── router │ └── index.js ├── views │ ├── Login.vue │ ├── Register.vue │ ├── Home.vue │ ├── WindowDetail.vue │ ├── MyReview.vue │ └── admin │ ├── UserManage.vue │ ├── WindowManage.vue │ └── ReviewManage.vue └── components ├── ScoreRate.vue └── ReviewCard.vue整体逻辑是未登录用户访问/显示食堂窗口列表。点击某个窗口进入/window/:id看到窗口详情、菜单、历史点评。登录用户可以提交点评。管理员访问/admin/review对点评进行审核管理。5.2 统一请求封装前端所有接口请求建议走统一封装。这里以 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 }) // 请求拦截器把登录后拿到的 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 }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request这个封装几乎是所有 Vue 毕设项目的标准做法。一方面避免了每个页面重复写axios.get时的拦截逻辑另一方面也统一了后端返回的{ code, msg, data }结构。5.3 匿名点评前台怎么处理这里有一个很容易被忽略的细节匿名点评只是在页面上不显示用户名但数据库依然要存user_id因为管理员在后台审核时可能需要知道这条点评是谁发的。如果数据库把 user_id 也置空了管理员就无法追溯恶意或违规内容。前端组装点评卡片时判断逻辑类似下面这样!-- 文件路径src/components/ReviewCard.vue匿名展示控制片段 -- template div classreview-card div classreview-header span v-ifreview.anonymous 1 classreviewer 匿名用户 /span span v-else classreviewer {{ review.nickname || review.username }} /span span classreview-time{{ review.createTime }}/span /div div classreview-score el-rate :model-valuereview.avgScore disabled / /div p classreview-content{{ review.content }}/p /div /template后端查询点评列表时要处理好是否返回用户昵称。如果用户选择了匿名后端在返回给前台的 DTO/VO 里就不应该把username填上真实昵称而是统一填“匿名用户”。不要在实体类上直接把user字段全量序列化出去否则匿名点评对普通学生来说就形同虚设。5.4 评分组件与点评提交页面学生提交点评的页面应该用星级评分组件来做。Element Plus 里的el-rate就是一个现成的组件!-- 文件路径src/views/WindowDetail.vue 中提交点评的部分 -- template el-card classsubmit-card v-ifisLogin h3发表你的评价/h3 div classrate-item span口味/span el-rate v-modelform.tasteScore show-score / /div div classrate-item span卫生/span el-rate v-modelform.hygieneScore show-score / /div div classrate-item span服务/span el-rate v-modelform.serviceScore show-score / /div el-input v-modelform.content typetextarea :rows4 maxlength500 show-word-limit placeholder说说这家窗口的菜品、分量、出餐速度吧 / div classsubmit-footer el-checkbox v-modelform.anonymous匿名发布/el-checkbox el-button typeprimary clicksubmitReview发布点评/el-button /div /el-card /template script setup import { reactive } from vue import request from /api/request import { ElMessage } from element-plus const form reactive({ windowId: route.params.id, tasteScore: 5, hygieneScore: 5, serviceScore: 5, content: , anonymous: false }) async function submitReview() { const res await request.post(/review/submit, form) if (res.code 200) { ElMessage.success(提交成功等待审核) form.content } } /script这类页面唯一的坑是表单校验不完整。前端只做展示拦截真正的比分范围校验一定要在后端做一遍不然把tasteScore传成 5.5 或 100后台计算平均分就会出现匪夷所思的结果。6. 完整运行流程拿到源码后先跑通无论你是从网上下载的源码还是自己跟着教程敲的代码第一次运行之前都要确认顺序。6.1 前置环境如果你完全从零开始需要先安装JDK 1.8 或更高版本Maven 3.xMySQL 5.7 或 8.xNode.js 14 或更高版本Navicat、DataGrip、IDEA 等数据库或 IDE 工具具体版本以你手里源码的pom.xml和前端package.json要求为准。这里不写死版本号是因为网上流传的源码差别比较大。拿到源码后第一件事就是打开这两个文件确认版本要求比直接运行更稳妥。6.2 导入数据库在项目目录下找到.sql文件一般放在sql/或db/目录下。使用 Navicat 连接本地 MySQL新建查询执行整个 SQL 文件或者直接在命令行导入mysql -uroot -p sql/canteen_review.sql如果 SQL 文件里包含DROP TABLE IF EXISTS语句执行前务必确认当前数据库没有重要数据。开发环境无所谓但如果库里已有数据会被直接清空。导入完成后查看一下三张关键表的数据尤其是用户表里有没有admin账号。如果没有就需要先通过前端注册接口创建一个普通账号然后把数据库中对应的role改为ADMIN或者执行源码自带的初始化数据脚本。6.3 启动后端在 IDEA 中打开后端目录等待 Maven 拉取依赖。如果网络不佳导致依赖下载失败可以配置国内 Maven 镜像。也可以使用命令行启动cd canteen-review-server # 这里换成你实际的后端目录 mvn spring-boot:run启动成功后控制台会出现 Spring Boot 的启动日志默认端口通常是 8080。验证方式浏览器访问http://localhost:8080/api/window/list。如果返回 JSON 数据说明后端接口已正常启动。6.4 启动前端进入前端目录安装依赖cd canteen-review-web # 这里换成你实际的前端目录 npm install npm run dev如果前端使用的是 Vite控制台会显示Local: http://localhost:5173/。如果前端使用的是 Vue CLI则启动命令一般是npm run serve端口通常是 8080。为了避免和后端端口冲突一般建议前后端使用不同端口然后通过代理转发接口。Vite 项目中可以在vite.config.js里配置// 文件路径vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })配置完成后前端页面里请求/api/window/list时会被 Vite 自动转发到后端 8080 端口从而避免跨域问题。6.5 核心业务全流程验证项目跑通后建议你按下面这条路径演示一遍这也是答辩时最推荐的演示路径步骤操作预期结果1注册学生账号并登录成功跳转到首页2进入一个食堂窗口详情页能看到窗口信息和已有点评3提交一条点评选择匿名提示“提交成功等待审核”4退出学生账号登录管理员账号后台点评管理出现待审核记录5管理员审核通过这条点评前台窗口页能看到这条内容但用户名显示为“匿名用户”6回看窗口评分窗口平均分已按最新点评重新计算这条路径覆盖了普通用户的点评流程和管理员的审核流程也顺带验证了匿名机制和统计更新是整场演示中最能体现系统完整度的部分。7. 常见问题与排查思路拿到源码跑不通大概率不是代码本身问题而是环境或配置差异。下面是我从大量同类项目反馈中总结的高频问题和排查建议。问题现象可能原因排查方向解决方案后端启动失败报数据库连接异常MySQL 没启动、账号密码错误、连接串端口不对确认 MySQL 服务和连接串配置用 Navicat 测试连接修正 application.yml 中的账号密码中文乱码数据库字符集不是 utf8mb4或连接串缺少 characterEncoding查看表字符集查询输出是否乱码建库时指定 utf8mb4连接串改成 utf8mb4 并重启前端口请求 404 或 500后端接口路径与前端请求路径不一致打开浏览器 F12 看 Network 面板逐字核对请求 URL 和 Controller 的 RequestMapping浏览器报跨域错误前端端口和后端端口不一致又没配代理或 CORS查看 Network 响应头是否有 Access-Control-Allow-Origin在 vite / vue.config 配置代理或后端写全局 CorsConfignpm install 报错Node 版本与项目依赖不匹配执行node -v查看版本换 Node 14/16/18 等兼容版本删除 node_modules 后重装分页查询不生效MyBatis-Plus 分页插件未配置查看是否有 MybatisPlusInterceptor Bean在 config 包中添加分页拦截器头像无法显示图片路径是本机绝对路径或没有上传模块观察图片 URL用本地静态资源映射或图床地址管理员登录不了初始化数据没导入或密码加密规则不一致查 sys_user 表是否有账号检查项目使用了 BCrypt 还是 MD5按对应用户插入账号用一个比较典型的例子展开讲MyBatis-Plus 分页拦截器缺失。如果你调用 selectPage 后发现返回的总条数一直是 0或者压根不进入分页 SQL多半是在配置类里没有注册分页插件。可以这样补上// 文件路径com.example.canteen.config.MybatisPlusConfig.java Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置极其常见但也是新手最喜欢漏掉的地方。类似的还有逻辑删除插件、乐观锁插件都属于“少了不会编译报错但运行行为不对”的隐蔽问题。8. 如何把这个项目变成自己的毕业设计很多人下载源码后会陷入一种矛盾不用源码自己从零写来不及直接用源码答辩时又心虚。破解方法很简单不是大幅重构而是做“外科手术式”改进。挑一两个点把它改得比原项目更合理然后能清楚讲出为什么这样改。8.1 把“一成点评”改成“可匿名且可重评”原版系统如果只是简单判断“一个用户对应一个窗口只允许一条点评”那直接实现唯一索引就完了。但你仔细想想如果管理员把某个人的点评驳回了他还有没有机会重新点评如果用户当时评错了想改怎么办更好的方案是设计成类似“最近一条有效点评”的逻辑学生提交点评时先查询自己对该窗口最近一次有效点评。如果最近点评处于待审核或已通过状态前端提示不能重复评分。如果最近点评被驳回允许重新提交。已通过的点评可以允许用户进入自己“我的点评”页面进行修改或删除。这样既避免了刷屏也保留了用户纠错空间。代码上的改动主要是去掉数据库里强制的(user_id, window_id)唯一索引改用 Service 层查询判断原先表设计里的 IN 条件已经没有重复所以已经兼容这一点你可以在此基础上把「我的点评」编辑功能补出来。8.2 增加统计可视化页面只有列表和审核的管理后台还是太单薄。如果想让答辩内容更充实可以给管理员增加一个数据看板用 ECharts 展示各个食堂窗口的平均分排名评分分布图比如 5 分、4 分、3 分分别占多少一周内点评提交与审核通过数量的趋势每个食堂窗口收到点评的数量排行前端使用 ECharts 的方式也很成熟核心是后端提供一个统计接口返回 GroupBy 聚合
返回列表