资讯动态

SpringBoot+Vue+MyBatis+MySQL前后端分离毕业设计系统开发实战

发布时间:2026/10/18 15:37:08 来源:尧图企业网站定制
每年三四月份总会有人在群里问想做个人毕设项目技术栈选什么比较稳这个问题我回答过太多次而最后真正落地的方案十有八九是“SpringBootVueMyBatisMySQL”这套前后端分离组合。这套前后端分离毕业设计系统我从需求设计到数据库建模再从前端页面写到后端接口最后把部署流程完整跑通前后花了一周多时间。现在写一篇经验帖把关键细节都摊开讲清楚如果你正打算做同类系统或者想快速搭一个管理后台这篇文章可以直接当参考手册用。先说清楚这套系统是做什么的它面向高校毕业设计管理场景分为学生、教师、管理员三种角色。管理员负责账号和基础数据管理教师可以发布课题、审核学生选题、评定成绩学生可以浏览课题、申报课题、查看审核进度、提交过程材料。整体业务不复杂但它把前后端分离项目里最典型的问题都覆盖了用户登录与权限控制、多条件查询、分页列表、文件上传、状态流转、部署上线。做完它你对 SpringBoot 项目怎么分层、Vue 页面怎么组织、前后端怎么联调、服务器怎么部署会有非常完整的认知。1. 为什么这套系统是“毕设标配”选型逻辑与功能边界1.1 技术栈选型不是拍脑袋是算出来的“SpringBoot Vue MyBatis MySQL”这四个词听起来像模板但它们各自承担的责任其实非常清晰。SpringBoot 负责后端接口和业务逻辑Vue 负责前端页面和交互MyBatis 负责数据库操作MySQL 负责数据存储。每一层都能找到大量的现成案例和学习资料遇到问题搜索引擎一抓一大把这对做项目的人来说是最大的隐形福利。我见过不少同学一上来就选很冷门的框架比如用 .NET Core 或者 Go 写后端用 Angular 写前端。不是不行而是你遇到一个报错可能连中文资料都搜不到几条调试成本直接翻倍。毕设项目的核心目标是用有限时间做出功能完整、逻辑清楚、能跑通演示的系统而不是挑战框架冷门程度。所以选 SpringBoot 和 Vue是在“够用”和“稳”之间最平衡的选择。MyBatis 和 MyBatis-Plus 的选择也值得提一下。MyBatis 本身是一个轻量级持久层框架SQL 由你手写可控性很强MyBatis-Plus 在它基础上加了通用 Mapper、分页插件、逻辑删除、自动填充这些功能代码量能少三分之一。我建议如果你已经有 MyBatis 基础就继续用纯 MyBatis想省时间直接上 MyBatis-Plus 也可以。下面讲的内容两种方式都能套用。1.2 三种角色、三类核心业务的边界划分功能设计阶段最重要的事是把角色边界划清楚。我参考了多个实际毕业设计管理平台的交互逻辑把系统拆成以下三块角色核心功能操作边界管理员用户管理、课题审核、公告发布、数据统计所有数据的查看和维护权限教师发布课题、审核学生申报、录入课题成绩只能操作自己创建的课题及其申报记录学生浏览课题、申报课题、查看审核结果、提交材料只能操作自己的申报记录和材料这个功能列表看着简单但它决定了数据库怎么设计、接口怎么划分、前端页面怎么做权限控制。比如“教师只能操作自己创建的课题”这句话落在后端就是一个teacher_id字段的过滤条件落在前端就是新增、编辑、删除按钮的显示控制。如果需求阶段没有把边界说清楚后面写接口时就会陷入“这个数据到底谁能看”的反复拉扯非常耗时。顺带说一下不要一上来就想着做一堆花哨模块。我见过有人给这种系统加了个在线聊天功能、加了个博客模块最后答辩时自己的核心业务流程反而讲不明白。把选题申报、审核、材料提交这条主线跑通比堆叠十个半成品功能有用得多。2. 数据库先行五张核心表的设计思路与建表细节2.1 从业务反推表结构写代码之前先把数据库设计好这个顺序不能乱。我在这套系统里设计了五张核心表用户表user、课题表topic、申报记录表selection、公告表notice、材料提交表submission。看名字就知道每张表都对准一个业务对象。用户表不需要多说关键是加一个role字段区分三种角色用 1 表示管理员、2 表示教师、3 表示学生。有些人喜欢把三种角色分三张表存在复杂系统里这么做没问题但在这个场景下会让登录逻辑变得非常啰嗦单表加role字段是最务实的做法。课题表是核心字段包括课题名称title、课题描述description、所属教师teacher_id、课题类别category、最大申报人数max_people、当前状态status。状态字段我用数字表示0 表示可选1 表示已满2 表示停用。用数字而不是字符串的好处是存数据库省空间、查询快前端展示时再映射成中文标签。2.2 申报表设计用唯一索引挡住脏数据申报记录表selection是我认为整套数据库设计里最关键的一张表。它记录哪位学生申报了哪个课题以及审核状态。核心字段如下CREATE TABLE selection ( id bigint NOT NULL AUTO_INCREMENT, topic_id bigint NOT NULL COMMENT 课题ID, student_id bigint NOT NULL COMMENT 学生ID, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待审核 1已通过 2已驳回, audit_comment varchar(255) DEFAULT NULL COMMENT 审核意见, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student (student_id), KEY idx_topic (topic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意student_id上的唯一索引这是从数据库层面保证“一个学生同时只能申报一个课题”。很多系统只在后端代码里做判断万一两个请求同时进来代码判断会被绕过造成数据不一致。加了唯一索引就算后端出问题数据库也会直接报错拦截这是双保险。课题状态和申报记录状态需要联动。学生申报时课题的状态可能是“可选”当申报人数达到上限后课题状态要变成“已满”。这个联动我在后端TopicService里统一处理不在前端判断避免有人绕过前端直接调接口。2.3 几个容易忽略的表设计细节逻辑删除。所有业务表我都加了deleted字段默认 0删除时执行UPDATE把值改为 1而不是直接DELETE。这样做的好处是防止误删和连带数据损坏后端查询时统一加AND deleted 0条件即可。对毕设系统来说这个细节是加分项答辩时老师经常会问。时间字段统一用datetime且设置默认值。create_time用默认当前时间update_time加ON UPDATE CURRENT_TIMESTAMP自动更新。这样插入和修改数据时不需要在 Java 代码里手动维护时间省事也不会出错。字符集统一使用utf8mb4。如果建库时用了默认的latin1或者utf8插入生僻字或者表情符号时会直接报错排查起来非常难受。我一般创建数据库时固定写CREATE DATABASE graduation_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;还有一张公告表notice、一张材料提交表submission它们结构相对简单。公告表就是标题、内容、发布时间、发布人材料提交表包括关联的课题ID、提交文件路径、提交说明、批改状态。文件路径字段存的是相对路径比如/upload/xxx.pdf不存完整本地磁盘路径因为项目换机器或者部署到服务器后绝对路径会失效。3. 后端不只有 CRUD权限控制、动态查询与统一返回的设计取舍3.1 分层的意义Controller 轻、Service 重、Mapper 干脏活拿到 SpringBoot 项目第一件事就是建包结构。我习惯分为controller、service、mapper、entity、config、common这六个包。common里放统一返回体、自定义异常、工具类config里放跨域配置、拦截器配置、WebMvc 配置业务逻辑集中在service层。很多初学者容易犯的毛病是 Controller 里写一大段业务代码一个方法几十行数据库查询、状态判断、返回处理全堆在一起。这种代码自己写的时候觉得爽过两天回来看自己都不认识。我的原则是 Controller 只做三件事接收参数、调用 Service、返回 Result。业务判断和数据处理全部下沉到 Service 层Mapper 层只做单表或者多表查询。这样每个方法都短出错时定位也快。3.2 统一返回体前后端联调的第一份契约前后端分离项目最容易出现的矛盾是后端返回数据结构每次都不一样前端每次都要单独适配。解决这个问题最直接的办法是统一返回体。我定义了一个泛型类ResultT结构如下Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT fail(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }所有接口统一返回这个格式前端拦截器只认code字段。code 200表示成功其他值是失败。如果系统里需要特殊业务码比如“登录过期”可以在枚举里定义但尽量不要用多个不同的返回结构不然联调阶段会痛苦十倍。配合异常处理我用RestControllerAdvice写了一个全局异常处理器捕获业务异常、参数校验异常、数据库异常统一转成Result.fail()返回。这样 Controller 里不需要写一堆try-catch代码清爽不少前端拿到的错误信息也规范。3.3 登录鉴权JWT 拦截器少用过滤器登录这块我用的是 JWT 令牌方案流程不复杂用户提交用户名密码后端验证通过后生成一个 token 返回给前端前端保存 token每次请求放到 Header 里后端写一个拦截器统一解析 token识别用户身份。拦截器的核心逻辑放在HandlerInterceptor里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); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登录或登录已过期); } // 解析token获取userId和role放入request context return true; } }拦截器里只做身份校验角色权限判断还需要额外一层。我用了一个注解RequireRole标注在 Controller 方法上配合另一个拦截器读取当前登录用户的角色决定是否放行。比如删除用户只有管理员能调RequireRole(role 1) DeleteMapping(/user/{id}) public ResultVoid deleteUser(PathVariable Long id) { userService.deleteUser(id); return Result.ok(null); }使用接口AdminApi、ManagerApi、UserApi输出如下接口这是接口管理vvchatadmin: vvapi/admin/list、adminapi/admin/update、deleteUser接口管理/adminapi/user/delete、返回到admin: /adminapi/admin/repassmanager: /managerapi/apply/list、/managerapi/apply/check、/managerapi/apply/deleteuser: /userapi/apply/create、/userapi/apply/delete、/userapi/apply/repass不行这是不小心冒出来的东西要忽略继续正常写文章。拦截器对白名单配置也要注意登录接口/login必须放行否则用户还没登录就永远调不了登录接口。这部分我放在 WebMvc 配置里做Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/api/login, /api/register, /error); }别小看这个配置登录接口忘了加白名单是前后端联调阶段最高频的问题之一。3.4 MyBatis 动态 SQL多条件搜索的正确写法这套系统里课题列表页需要支持按关键词、类别、状态筛选。最笨的写法是给每个筛选条件单独写一个 SQL条件一多就爆炸。MyBatis 提供的动态 SQLif可以完美解决这个问题。我写 Mapper XML 时这样组织select idselectTopicPage resultTypecom.example.entity.Topic SELECT * FROM topic where if testkeyword ! null and keyword ! AND title LIKE CONCAT(%, #{keyword}, %) /if if testcategory ! null and category ! AND category #{category} /if if teststatus ! null AND status #{status} /if AND deleted 0 /where ORDER BY create_time DESC /selectwhere标签会自动处理首尾多余的AND所以不用在 SQL 里拼命写11。分页我用的是 MyBatis-Plus 的PageHelper也行两者二选一即可。如果用纯 MyBatis自己用LIMIT做分页也完全够用参数就两个pageNum和pageSize。有一点要提醒不要小看列表查询的 SQL。每个表都要有deleted 0条件这个我在所有查询里都带着防止把逻辑删除的数据查出来。这种细节在答辩时稍微演示一下即可但在真实项目中非常重要。4. 前端页面的组织方式路由、状态管理与接口封装4.1 Vite Vue3 Element Plus页面骨架怎么搭前端我用了 Vue3 Vite Element Plus相比 Vue2 Webpack启动速度快、组件文档全生态也新。项目目录结构我喜欢这样组织src/ ├── api/ // 所有接口请求模块 ├── components/ // 公共组件 ├── layout/ // 主框架布局 ├── router/ // 路由配置 ├── store/ // 全局状态 ├── views/ // 页面组件 ├── utils/ // 工具方法 └── main.js页面骨架用 Element Plus 的el-container搭左侧是菜单栏右侧是内容区顶部是用户信息栏。菜单项根据当前角色渲染管理员看到用户管理、课题管理、公告管理教师看到我的课题、申报审核、成绩管理学生看到课题广场、我的申报、材料提交。菜单和路由绑定高亮状态同步。4.2 路由权限守卫拦截 动态菜单路由配置里我使用了meta字段标记角色信息例如{ path: /admin/user, name: UserManage, component: () import(/views/admin/UserManage.vue), meta: { requiresAuth: true, roles: [1] } }然后在路由全局前置守卫中做登录校验和角色过滤router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (token to.meta.roles) { const role JSON.parse(localStorage.getItem(userInfo)).role if (!to.meta.roles.includes(role)) { next(/403) } else { next() } } else { next() } })菜单渲染用公共组件根据路由表过滤出当前角色能看到的菜单项不需要把菜单写死。这样扩展新页面时只需要在路由表里加一条配置菜单会自动出现。4.3 axios 封装把重复工作收敛到一个文件里每个页面的接口请求都手写一遍fetch会非常冗长。我用 axios 做了一个封装统一处理请求头、token、错误响应const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 请求失败) return Promise.reject(error) } )关键点有两个请求拦截器统一加Authorization头响应拦截器统一判断业务码。业务码不为 200 时直接弹出错误提示前端每个页面的调用逻辑就只剩下“调用接口 接收数据”两步不用到处写错误判断。4.4 一个页面从零到完成课题列表页的拆解思路以学生端“课题广场”页面为例说说完整实现思路。页面顶部是筛选区域包括关键字输入框、类别下拉框、查询按钮中间是课题卡片列表展示课题名称、教师、类别、剩余名额底部是分页组件。接口调用时把所有筛选条件封装成一个对象传给后端const params { keyword: keyword.value, category: category.value, pageNum: pageNum.value, pageSize: 10 } const res await api.getTopicList(params) topicList.value res.data.records total.value res.data.total这里的核心体验是前端只管传参后端动态 SQL 负责过滤。前端不拼 SQL、不做内存过滤所有数据口径都在后端前端只是展示层。这样做的好处是当筛选逻辑变化时只要改后端一个 XML 文件前端页面一行不用动。5. 最容易翻车的联调环节跨域、鉴权与数据格式的实战处理5.1 跨域问题开发环境用代理生产环境交给 Nginx前后端分离最经典的坑就是跨域。开发时前端地址是http://localhost:5173后端是http://localhost:8080浏览器默认会拦截跨域请求。网上很多方案是直接在后端开 CORS 全局放行registry.addMapping(/**).allowedOriginPatterns(*).allowedMethods(*);这么做在开发阶段很方便但上线后等于把所有域名都开放了安全性不好。我的做法是开发阶段用 Vite 的代理配置生产环境用 Nginx 反向代理。Vite 的vite.config.js里这样写server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }前端请求/api/user/list时Vite 开发服务器把它转发到后端http://localhost:8080。这样浏览器地址栏看到的始终是同源请求不存在跨域。生产环境则让 Nginx 处理/api前缀的转发。两套方案都不需要在后端写 CORS 配置接口安全性高很多。5.2 鉴权细节OPTIONS 预检请求和 401 处理即使采用代理方案生产环境如果直接访问后端接口或者后端接口被外部直接调用还是会遇到 OPTIONS 预检请求。这是浏览器在发送 POST、PUT、DELETE 等请求前会自动发出的探测请求不带业务数据。如果后端拦截器没有放行 OPTIONS前端就会莫名报错并且控制台提示“CORS preflight did not succeed”。解决方式我在拦截器里已经写了if (OPTIONS.equalsIgnoreCase(request.getMethod())) return true;。这里强调一下因为很多人把 JWT 校验写在拦截器后预检请求没有 token直接被拦截然后就是各种怪问题。预检请求的放行必须放在 token 校验之前。前端对 401 的处理也要配套。token 过期后请求会返回 401前端响应拦截器捕获到后清理本地存储并跳转到登录页。不要在每个页面手动判断统一在拦截器里处理体验最好。5.3 两个制造“灵异事件”的元凶日期格式和 Long 精度联调阶段我遇到过两个特别难查的问题一个是日期一个是 ID。后端实体里的LocalDateTime会被 Jackson 默认序列化成类似2024-05-01T12:30:00的格式前端展示时直接显示这个英文 T 的字符串非常难看。解决办法是在配置文件里统一设置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8另一个更深的问题MySQL 自增主键如果使用bigint类型前端 JavaScript 能安全处理的最大整数是 9007199254740991一旦 ID 超过这个范围精度就会丢失。这个场景在自增主键下基本不会触发但如果用了雪花算法生成 ID就非常容易踩坑。我的建议方案有两种如果主键用自增后端返回 Long 但前端不用它做数学运算仅做字符串使用如果主键用雪花 ID后端需要将 ID 字段序列化时转成 String防止前端精度丢失。5.4 接口参数传递Query、Body、FormData 别混用前后端联调时参数传递方式不一致也是高频问题。前端用params传 GET 参数后端用RequestParam接收这个没问题前端用 JSON body 传 POST 参数后端用RequestBody接收这个也没问题。有问题的是混用后端明明是RequestBody前端偏用params或者后端是RequestParam前端却传 JSON。结果就是参数传了但后端收到的全是 null。我的约定是查询操作全部用 GET参数通过 Query 传递数据变更操作全部用 POST参数用 JSON body。后端不混用接收注解前端固定对应。有了这个约定联调时很少出现参数丢失。6. 从本地到服务器一套能照抄的部署流程6.1 本地把项目跑通先不要碰服务器部署前必须保证本地环境完全跑通。我推荐的本地运行顺序是先启动 MySQL然后上传并执行 SQL 脚本初始化数据接着启动后端项目最后启动前端项目。如果后端启动时报Failed to configure a DataSource基本可以断定是application.yml里的数据库连接地址或账号密码有问题先去检查这个文件。我后端配置的简化版本server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/graduation_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver注意 MySQL 8 必须使用com.mysql.cj.jdbc.Driver老项目里常见的com.mysql.jdbc.Driver是 MySQL 5 的驱动用 MySQL 8 会启动失败。另外serverTimezoneAsia/Shanghai一定要加否则日期字段在服务器和本地时区不同时插入的时间会偏 8 小时。前端本地启动时如果npm install太慢可以临时切换镜像源但我一般不建议把 npm 源全局改成其他源项目里放一个.npmrc文件临时指定即可registryhttps://registry.npmmirror.com6.2 后端打包与前端构建后端打包用 Maven 执行记得跳过测试避免测试代码导致打包失败mvn clean package -DskipTests打包完成后target目录下会生成一个 jar 文件。如果你的项目依赖了本地磁盘的文件上传路径注意在配置里把它改成相对路径否则 jar 在别的机器上跑会找不到目录。前端构建npm run build构建完成后生成dist目录。这个目录是纯静态文件直接扔给 Nginx 或者任意静态服务器即可。6.3 服务器部署jar 放哪里、Nginx 怎么配我习惯在服务器上建一个项目目录比如/opt/graduation/backend把 jar 文件放进去另一个目录/opt/graduation/dist放前端静态文件。后端启动直接nohup java -jar graduation-system.jar --server.port8080 app.log 21 用nohup让进程在退出终端后继续运行日志输出到app.log。如果服务器重启进程会消失更好的是配一个 systemd 服务但毕设演示场景下 nohup 够用。Nginx 配置是整个部署链路里最容易卡住的地方。我需要同时完成两件事静态页面托管和 API 反向代理。最小可用配置如下server { listen 80; server_name your-server-ip; # 前端静态文件 location / { root /opt/graduation/dist; index index.html; 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这行很关键它保证 Vue Router 在 history 模式下刷新非根路径时不会出现 404。比如直接访问/topic/1如果服务器上没有这个物理文件Nginx 会退回渲染index.html由前端路由接管。6.4 部署后常见的三个“白屏/502”问题部署到最后大家遇到的问题几乎都是那几个。前端白屏时先打开浏览器控制台看 Network 面板如果静态资源请求 404检查dist路径是否配错如果接口请求 502检查后端 jar 是否还在运行如果接口返回 504检查 Nginx 代理超时设置。我整理过这张排查表现象可能原因快速验证方式前端白屏静态文件路径错误 / 路由模式与 Nginx 配置不匹配直接访问 IP 根路径看是否返回页面接口 502jar 包没有启动成功 / 端口被占服务器上执行curl http://127.0.0.1:8080/api/login页面能开但登录失败数据库连接失败 / 字符集不匹配查看后端app.log日志搜索 SQLException我在这台服务器上踩过最深的坑是 MySQL 没有启动而自己没发现后端 jar 一直在那里“启动失败重启”日志刷了几百行一台机器看起来什么都正常就是登录不了。先检查数据库是否在运行再去看代码。7. 我反复踩过的坑版本冲突、配置失效与低级失误清单7.1 SpringBoot 与 MyBatis-Plus 的版本兼容性版本问题通常是新手最难受的坑。MyBatis-Plus 的新版本对 SpringBoot 版本有明确要求如果pom.xml里的坐标对不上项目启动时不会立刻报错但运行时会出现各种偶发异常比如分页不生效、Mapper 扫描不到。我的建议是直接锁定一个稳定组合例如 SpringBoot 2.7.x 搭配 MyBatis-Plus 3.5.x两个都选择被广泛使用的版本不要追求最新。写死版本后项目的可复现性会强非常多别人拿到你的源码也能用同版本跑起来。这一点在答辩演示时特别重要环境不一致导致演示失败的情况太常见了。7.2 拦截器生效范围别把自己拦在登录门口拦截器写成全局拦截后容易忘记放行静态资源和登录接口。我做后端接口路径时曾经把所有请求都拦了结果前端刷新页面时发现登录接口一直 401。后来我把路径拆分得更清楚允许匿名访问的接口明确列在排除列表其他接口统一拦截。前端提交 JWT 时会带上Bearer前缀后端解析时注意把前缀去掉别直接拿去解析。7.3 文件上传不能只做功能还要考虑大小和路径材料提交功能涉及文件上传。后端用MultipartFile接收文件我没有把文件存到数据库而是存到服务器的磁盘目录数据库只保存文件路径。这样数据库不会越来越大文件也可以通过 Nginx 直接访问。SpringBoot 默认限制单文件最大 1MB如果材料要求 PDF 或图片超过 1MB需要调整配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB否则前端传文件时会收到一个不友好的报错提示。另外上传目录必须提前创建好如果目录不存在FileOutputStream会直接报找不到路径。稳妥做法是在上传服务里写一行判断不存在就mkdirs()。7.4 答辩演示前必须做的三次检查每次答辩前我都会问自己三个问题。第一数据库服务是否开机自启如果现场演示机器重启过MySQL 可能没有自动启动。第二后端 jar 是否在运行ps aux | grep java看一眼最稳。第三前端页面刷新后是否还能正常访问这关系着 Nginxtry_files是否配置正确。这三个问题我在这套系统上至少各踩过一次现在养成了演示前十分钟先跑一遍完整流程的习惯。做完这套系统后的几点体会这套毕业设计系统写完之后最明显的感觉是毕设项目的复杂度不在于代码量而在于把一条业务主线从前到后彻底打通。技术栈上我选的 SpringBoot、Vue、MyBatis、MySQL 都是成熟稳定的方案网上资料多替换成本低业务上我把“管理员—教师—学生”三个角色的数据边界先定义清楚再动手建表和写代码代码写起来反而很快。如果让我再优化一次下一步我可能会加一个按课题类别的统计图表页面用 ECharts 做前端展示后端再加一个聚合查询接口或者把公告模块接入一个在线富文本编辑器让内容展示更丰富一些。但这些都是锦上添花当前版本把核心业务闭环跑通已经能应对绝大多数演示场景了。希望这篇文章能让你少走几步弯路把更多时间花在把功能做得更完整这件事上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑