每年到了毕设季,找我咨询项目的人就多起来了。问得最多的就是:有没有一个SpringBoot+Vue的管理系统源码,业务别太无聊,技术栈别太旧,最好能直接跑起来改一改就交差?说实话,图书管理系统、学生管理系统这些题目,老师每年见几十遍,答辩时很难出彩。这套船舶维保管理系统是个挺不一样的答案——同样是Java+MySQL的前后端分离项目,但业务场景放在船舶设备维护上,天然带着计划流转、角色权限、库存联动这些硬逻辑,就算不改一行代码,“船舶维保”这四个字本身就赢在选题上。这篇文章我把它从里到外彻底拆开:业务模型怎么设计、数据库表怎么建、核心代码在哪、怎么在本机跑起来,再到答辩时老师常问的问题,全程实操经验,无保留分享。
1. 项目整体拆解:船舶维保到底在管什么
1.1 业务场景:为什么船舶需要一套维保系统
先想一个问题:一条船上,主机、辅机、舵机、甲板机械、消防设备、救生设备加起来可能有几十上百种,每种设备又有各自的保养周期和检修标准。以前的传统做法是纸笔记录,甚至有些小团队靠老师傅的记忆。这里面的核心痛点有两个。
第一是“到时间了没人记得”。设备保养是强周期性的,但不同设备的周期完全不一样,主机可能运行500小时就要换机油,救生筏是每12个月要做一次年检,气体灭火系统又是另一套周期。靠人工去盯这些时间节点,漏检是必然的。第二是“设备履历断层”。一台设备前任维修工给它换过什么零件、调过什么参数、修过什么故障,如果只存在于个人记忆里,一旦人员变动,这些信息就全丢了。下一任接手时只能重新排查,效率极低。
船舶维保管理系统解决的就是这两件事:用计划驱动代替人工记忆,用设备履历代替个人记忆。系统里每一台设备都有独立的档案页,从出厂信息、安装位置到历次维保记录全部串联起来,状态一目了然。这个逻辑不仅适用于船舶,也适用于所有强设备管理场景,但船舶行业的“多设备、强周期、重安全”特点,让它比普通资产管理系统更有代表性,这也是毕设选题时最能打动老师的切入点。
1.2 角色与权限矩阵设计
船舶维保系统涉及的人员不是只有管理员和用户这么简单,至少要区分出四类角色,它们的职责差异直接决定了系统的权限设计。
| 角色 | 业务定位 | 核心权限范围 |
|---|---|---|
| 系统管理员 | 平台的拥有者和配置者 | 用户管理、船舶档案、字典参数、公告发布 |
| 轮机长/船管员 | 维保计划制定与审核者 | 创建计划、审批工单、查看全船报表、分配任务 |
| 维修工 | 计划的具体执行者 | 查看自己的工单、填写维修记录、申领备件 |
| 巡检员 | 设备状态检查者 | 录入巡检记录、上报异常、查看设备档案 |
这个矩阵里最有讨论价值的是“数据权限”和“菜单权限”的区分。菜单权限好理解——不同角色登录后看到的菜单项不一样,维修工看不到报表管理,管理员不参与业务流转。但数据权限才是体现设计深度的位置:维修工登录后应该只能看到分配给自己的工单,不能看别人的;轮机长能看到全船所有设备的数据;而普通巡检员可能只能看自己负责的舱段。代码实现上,最直接的做法就是在SQL查询层加条件过滤,而不是单纯靠前端隐藏菜单,因为前端隐藏只是体验层面的控制,后端查询加权限条件是真正的安全边界。这个点答辩时一定要主动提,老师说你有安全意识。
小程序、APP不做,但如果你时间充裕,这套权限矩阵将来扩展成多租户或者移动端审批流,都是很自然的演进方向。
1.3 功能模块清单:从计划到报表的闭环
整个系统围绕“计划-执行-记录-分析”形成一个闭环,模块划分大致如下:
- 基础数据:船舶档案、设备台账、备件库、供应商信息
- 维保中心:维保计划管理、工单流转、维修记录、巡检管理
- 备件管理:备件入库、出库、库存预警、领用记录
- 统计报表:按船舶/设备/时间维度统计维保完成率、故障率、备件消耗
- 系统管理:用户、角色、菜单、字典、日志
这里建议重点关注维保中心内部的流转,因为它是整个项目的业务中枢。一个完整的流程是:轮机长在月初根据设备台账自动生成本月维保计划,系统将计划拆解成工单,工单指派给对应的维修工,维修工在手机或电脑端接单,执行完毕后填写维修内容、耗用备件、工时,轮机长审核通过后工单关闭,同时更新设备履历。这条链路上的每一步都涉及数据库表的状态变更,是一道非常完整的《业务状态机设计》实战题。
2. 技术栈选型与架构设计:为什么是SpringBoot+Vue+MySQL
2.1 前后端分离已经是标配
这套系统采用SpringBoot+Vue的前后端分离架构,而不是传统的JSP或者FreeMarker模板渲染,核心原因是:前后端分离可以让前端团队和后端团队并行开发,接口一旦约定好,两边互不阻塞。对毕设来说,还有一个实际好处——答辩时可以分别展示后端Swagger接口文档和前端页面,技术点展示面更大,老师能问到的东西更多,也更容易体现你的工作量。
项目结构上,典型的前端是一个独立的Vue工程,通过HTTP请求访问后端的RESTful API。后端只需要暴露JSON格式的接口,不关心页面渲染。本地开发时通常用Vue CLI自带的devServer做代理,把/api开头的请求转发到后端的8080端口,这样可以避免开发环境下跨域的问题。生产环境下则有两种选择:一是把前端build出来的dist目录丢到后端resources/static下面,由SpringBoot统一托管;二是用Nginx分别代理前端静态资源和后端接口。毕设阶段用第一种就够了,省配置。
2.2 版本选择:SpringBoot 2.x还是3.x
这是拿到源码后第一个要确认的事。我见过太多同学在环境搭建上卡住,不是因为代码有bug,而是因为JDK和SpringBoot版本不匹配。
目前市面上流通的这类毕设源码大多基于SpringBoot 2.5~2.7开发,对应的JDK是1.8或11。如果你电脑里装的是JDK 17甚至21,并且直接用SpringBoot 2.7以下的老项目,大概率会遇到编译报错,因为旧版Spring Boot对高版本JDK的兼容并不好。反过来,如果你拿到的是SpringBoot 3.x的源码,它要求最低JDK 17,同时包名从javax迁移到了jakarta,很多老教程里的import javax.*代码直接编译不过。
我的建议比较简单:做毕设求稳,不要刻意追求SpringBoot 3.x。2.x技术栈成熟,网上资料多,老师也熟悉。学习周期短的人,用2.x版本的源码,配合JDK 1.8或者11,是最稳妥的组合。如果你是工作党用来学习,倒是可以顺手把项目从2.x升级到3.x,中途会踩不少javax到jakarta迁移、配置项改名的坑,但这些坑都是面试能聊的素材,值。
2.3 数据访问:MyBatis-Plus的功劳
这套系统的数据访问层用的是MyBatis-Plus,而不是纯MyBatis或Spring Data JPA。MyBatis-Plus是MyBatis的增强工具,核心价值在于:它内置了通用的Mapper接口和Service实现,大多数单表CRUD你不需要写一行SQL。比如用户列表的分页查询,Page page = userMapper.selectPage(new Page<>(current, size), wrapper)就是一句搞定,实体类加注解就能映射表结构。
更省事的是代码生成器。MyBatis-Plus官方提供的AutoGenerator可以根据数据库表结构反向生成实体类、Mapper接口、Service和Controller,对于这种表很多的系统来说,能省掉一半的重复劳动。很多搜索词比如“根据Java实体类生成建表SQL”也是这个方向的需求——本质上数据库表结构和实体类是双向映射的,你可以用工具正向生成表,也可以从表逆向生成类。建议你在熟悉项目时,优先看实体类和表结构的对应关系,这是理解业务的捷径。
这里提醒一个细节:使用MyBatis-Plus时,逻辑删除建议用@TableLogic注解实现,而不是物理删除。用户记录、工单记录这些数据在业务上不允许直接消失,逻辑删除是在表里加一个deleted字段,查询时MP会自动追加deleted=0条件,这样既能保证业务数据可追溯,成本又极低。答辩时提到这个细节,属于“有工程经验”的表现。
2.4 前端方案:Vue + Element UI + ECharts
前端技术栈相对固定:Vue 2 + Element UI是最好的选择。为什么不用Vue 3?并不是Vue 3不好,而是大量毕设级别源码的组件库、教程、踩坑经验都沉淀在Vue 2 + Element UI这套组合上。Vue 3对应的Element Plus虽然也在快速成熟,但很多老插件和教程对不上,新手容易在配置上浪费大量时间。选Vue 2本质上是“求稳”。
页面结构上,Element UI的Container布局组件可以直接搭出后台管理系统的经典样式:左侧菜单栏、顶部导航条、中间内容区。菜单可以按角色动态渲染,后端返回一个菜单树,前端递归生成,这个功能实现起来有门槛但不算难,做完之后你对Vue的组件递归、路由守卫、状态管理都会有一个质的提升。
数据可视化部分用ECharts,用来画维保完成率环形图、故障类型饼图、每月维保量折线图。ECharts的图表配置本身不复杂,但要注意图表数据来自后端接口,前后端需要约定好数据结构。我习惯让后端一次性返回一个包含多个图表数据的聚合对象,前端拿到后分发给不同的图表组件,这样页面加载只需要请求一次,体验也好一些。
2.5 数据库表结构设计:理解表就是理解业务
这套系统核心表大概有七到八张,我列一个简表,做毕设的同学拿到源码后可以按这个清单去对照检查数据库设计质量:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 用户表 | id, username, password, real_name, role_id |
| ship_info | 船舶档案 | id, ship_name, ship_type, build_date, captain |
| device_info | 设备台账 | id, ship_id, device_name, model, install_position, maintenance_cycle |
| maintenance_plan | 维保计划 | id, ship_id, plan_month, plan_type, create_by, status |
| maintenance_order | 维修工单 | id, plan_id, device_id, assignee, status, start_time, end_time, result |
| spare_part | 备件表 | id, part_name, part_model, stock_num, warn_line, unit |
| spare_part_record | 备件出入库记录 | id, part_id, type, count, order_id, operator |
| inspection_record | 巡检记录 | id, device_id, inspector, inspect_time, result, remark |
关键外键关系是:ship_info 1对N device_info,maintenance_plan 1对N maintenance_order,maintenance_order N对N spare_part(通过spare_part_record关联)。设计表的时候,统一使用bigint做代理主键,业务字段用varchar(50)或(100),时间字段用datetime,金额和数量保留两位小数。状态字段建议用int小型字典值,比如工单状态:0待接单、1执行中、2待审核、3已完成、4已驳回。
特别注意:spare_part_record表是典型的多对多关联表,它不只是做关联,还承载着出入库的历史审计信息。这类表是面试官和答辩老师最爱的考点,因为它体现的是你对“业务事实”的理解,而不只是会搭表。
3. 核心功能实现拆解:五个值得吃透的模块
3.1 登录鉴权与JWT无状态认证
这套系统登录模块建议用JWT(JSON Web Token)实现无状态鉴权,而不是传统的Session。JWT的逻辑是:用户登录成功后,后端签发一个包含用户id、角色、过期时间的加密token返回给前端;前端把token存在localStorage里,每次请求在header里带上Authorization: Bearer token;后端通过拦截器解析token,如果合法就放行,不合法就返回401。
拦截器只需要在SpringBoot里注册一个HandlerInterceptor,在preHandle方法里校验token。这里有个坑要注意:前端请求OPTIONS预检请求时,后端过滤器必须直接放行,否则跨域请求永远过不去。另外JWT密钥不要写在代码里,放到application.yml的配置项里,答辩时可以说这是可配置的安全设计。
密码存储一定不要用明文。用BCrypt加密,spring-security-crypto包里可以直接引入,不需要引入整个Spring Security,避免权限配置牵连太多。因为如果用了完整的Spring Security,默认几乎会把所有接口拦截住,新手配置不当连Swagger都看不了,容易把自己劝退。轻量级拦截器+JWT+BCrypt这套方案很适合毕设系统。
3.2 维保计划生成与工单状态机
维保计划模块是整个系统的业务发动机。设计上,报表页面选择月份,后端读取该月所有设备的保养周期,自动生成计划列表,同时检查备件库存,低库存设备在计划中打标提醒。这一步可以用定时任务做自动化,也可以用按钮触发手动生成,毕设里手动触发再加上一个@Scheduled定时检查到期工单就足够了。
工单状态用int字典值管理,前面表结构里已经列了五个状态。实现时注意状态流转是有方向的,不是所有状态都能任意跳转。比如“已驳回”的工单只能重新指派,不能直接变成“已完成”。这个约束在后端Service层验证,前端按钮按状态控制显隐。状态机的实现用最简单的方式就是switch判断,但更推荐的写法是维护一个Map<状态, 允许跳转的状态集合>,代码更清晰,答辩也是一个亮点。
3.3 备件库存管理与预警
备件管理看起来是普通的CRUD,但有两个点值得认真实现。一是出入库流水,每次出库或者入库都要往spare_part_record表里插一条记录,同时更新spare_part表的stock_num字段。这里必须放在一个事务里,否则会出现库存改了流水没记、或者流水记了库存没改的脏数据。用Spring的@Transactional注解即可,注意事务加在Service层而不是Controller层。
二是库存预警。每个备件有warn_line字段,库存低于预警线时,在前端页面用红色角标提醒,同时工单填写时可领用的备件列表要置灰并提示库存不足。这个功能实现不难,但很出效果,因为“自动预警”听起来比“列表查询”高级一个档次。如果还想做得更细,可以在后端加一个每日定时任务,扫描低库存备件发送站内通知。
3.4 统计数据与ECharts可视化
报表模块我用三个维度来设计:按船舶维度统计各船维保完成率,按设备类型统计故障分布,按月份统计工单数量趋势。后端接口就是三条SQL+一个聚合对象,核心SQL是带group by的统计查询。比如按月统计工单数量:SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) FROM maintenance_order GROUP BY month。这个SQL很基础,但要注意日期格式化函数在MySQL 5.7和8.0里都一样,不会有兼容问题。
前端用ECharts时,常见错误是初始化图表的DOM还没渲染完成,导致图表不出来。解决方法是在Vue的nextTick里初始化,或者用watch监听数据变化后调用setOption。另外请务必记得在组件销毁时调用chart.dispose(),否则页面切换多了浏览器内存会越涨越高,被眼尖的老师看到就尴尬了。
4. 从源码到本地运行:完整实操流程
4.1 环境准备与版本匹配
拿到源码后不要急着点启动,先用30分钟把环境对齐。这套系统最稳妥的组合是:
- JDK 1.8或11(推荐1.8,最稳)
- Maven 3.6以上
- MySQL 5.7或8.0(8.0注意时区问题)
- Node.js 14~16(Vue 2项目缓存版本太新会出问题)
- 前端包管理器npm或yarn
如果你发现项目pom.xml里用的是SpringBoot 2.7.x,同时JDK是17,建议要么降JDK要么升级SpringBoot版本,不要硬着头皮编译。前端如果node-sass安装失败,把它换成sass(dart-sass),写法基本不变,但配置上要在vue.config.js里做一点调整。
4.2 数据库初始化配置
第一步,创建一个数据库,建议命名ship_maintenance,字符集用utf8mb4,因为utf8mb4才能完整支持中文和特殊符号。第二步,导入项目里的sql脚本。注意看sql文件里面的建库语句,如果它自带CREATE DATABASE,就不要再手动建库,直接全量执行即可。
第三步,修改后端的application.yml里的数据源配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ship_maintenance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你自己的密码大于等于8.0的MySQL必须用com.mysql.cj.jdbc.Driver,5.7用com.mysql.jdbc.Driver也行,但前者兼容后者。serverTimezone一定要配,否则会报时区错误。allowPublicKeyRetrieval=true是MySQL 8.x经常会出现的坑,不配的话连接时可能报Public Key Retrieval is not allowed。
4.3 后端启动步骤
后端启动非常简单,在项目根目录下执行:
mvn clean package -DskipTests java -jar target/ship-maintenance-0.0.1-SNAPSHOT.jar如果你用的是IDEA,更推荐直接打开项目,等待Maven自动下载依赖,然后找到主类ShipMaintenanceApplication,右键直接运行。启动后访问http://localhost:8080,如果能打开Swagger文档(一般在/swagger-ui.html或/doc.html),说明后端已经活过来了。
建议第一次启动时看控制台日志有没有报红色ERROR,大部分启动失败都集中在:端口被占、数据库账号密码不对、表不存在这三大类。
4.4 前端启动步骤
前端是独立的Vue工程,进入前端目录:
npm install npm run servenpm install的时间取决于网络环境,建议提前配好npm国内镜像源。如果安装过程中node-sass报错,先检查Node版本,Node 16以下配合node-sass 4.x比较稳,Node 17以上直接换sass。启动成功后控制台会打印一个地址,一般默认是http://localhost:8081,因为前端默认端口8081,和后端的8080区分开。
前端请求后端接口,在vue.config.js里已经写好了代理配置:
devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这个配置意味着前端请求/ajax/user/login,会被代理到后端http://localhost:8080/api/user/login。如果你改了后端端口,这里也要同步改,改完记得重启npm run serve,代理配置不是热更新的。
4.5 联调验证与数据初始化
前后端都启动后,用管理员账号登录。这里的关键是:你的数据库里必须有一批初始化数据。很多源码sql脚本里只建了表,没有测试数据,登录后一片空白。解决办法有两个:要么自己通过页面录入几条测试数据,要么找一个数据更全的sql版本。我的习惯是让sql脚本里自带3艘船、20台设备、几条未完成的工单数据,自查演示时才有东西可讲。如果你拿到的源码没带数据,建议自己补上,哪怕手动录十几条也行,付出的时间在答辩演示时回报率很高。
5. 常见问题与排查技巧实录
5.1 环境类问题速查表
| 现象 | 可能的根因 | 解决方向 |
|---|---|---|
| 后端启动报ClassNotFoundException: javax.servlet.* | JDK版本过高且SpringBoot版本过老 | 降JDK到1.8或升级SpringBoot到2.7+ |
| 启动报No suitable driver | dataSource驱动类没写对 | 检查连接url和driver-class-name |
| 前端npm install报node-sass错误 | Node版本与node-sass不兼容 | 换dart-sass或换Node 14 |
| 前端启动后页面空白 | 可能是路由base路径问题或构建失败 | 看控制台报错,检查publicPath |
| 接口返回401 | token过期或没带token | 重新登录,检查请求拦截器 |
| 接口跨域报错 | 后端未配置CORS | 加CorsFilter,注意放行OPTIONS |
5.2 业务代码里的典型坑
说几个我实际调试中遇到的高频问题。
第一个,MyBatis-Plus分页查询不生效。很多人的写法是直接调用selectPage,但如果你没有配置PaginationInnerInterceptor,分页只会查出全部数据然后再内存里切。这个拦截器本质是拦截SQL并在后面拼接LIMIT,不配置就没有真正的分页。配置位置通常在配置类里声明一个MybatisPlusInterceptor bean。
第二个,前端传日期时间格式不对。后端实体类的时间字段用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),前端传的时候必须按这个格式,否则会出现400或者解析成null。前后端联调的时候,这是一个非常常见又隐蔽的坑,建议统一封装一个时间处理工具。
第三个,逻辑删除字段没做唯一约束。比如备件表的part_name字段,如果没有把deleted字段纳入联合唯一索引,逻辑删除后再新增同名备件,会出现两条“同名不同id”的记录。解决办法是建联合唯一索引(part_name, deleted),或者不做逻辑删除改用状态字段。这种细节可能不会被老师看到,但如果你自己运行久了,一定会遇到。
5.3 我的排查习惯
遇到启动失败,先看日志再搜报错,不要盲目改代码。日志是最老实的信息来源。我通常按以下顺序排查:
先看有没有数据库连接相关的报错——这占了启动失败的一半;再看端口占用情况,Windows上用netstat -ano | findstr 8080,Linux用lsof -i:8080;如果是前端问题,优先看Node进程是否正常监听端口,再看代理配置是否指向了正确的后端地址。
拿到一个陌生源码,不要第一时间去跑,先花10分钟看README或者项目结构,找到后端主类和前端入口。没有README的源码,就去看pom.xml和package.json,这两个文件几乎能告诉你所有版本信息。
6. 答辩视角:怎么把一个“管理系统”讲出深度
6.1 表达框架:从业务到技术
答辩时间一般五到十分钟,别上来就讲代码。我建议用这样一个递进结构:
第一段讲业务。陈述船舶维保的核心矛盾是“周期遗忘”和“履历断层”,你的系统如何用计划驱动和全生命周期记录来解决。这段控制在两分钟以内,重点让老师感受到你真的理解需求。
第二段讲架构。展示前后端分离的部署图、技术栈选型理由、数据库ER图和核心表关系。这里可以快速带过,不用展开太多。
第三段讲亮点。选两到三个你觉得实现得最有细节的功能,比如工单状态机、库存预警联动、JWT鉴权流程。每个亮点按照“业务难度-技术方案-实现效果”三段式讲。这一段是决定印象分的部分,务必提前排练。
6.2 老师爱问的问题与应答思路
我整理几个高频提问,提前准备好答案能很大程度降低紧张感。
第一个,为什么用JWT而不用Session?回答要点:前后端分离架构下,后端无状态化让接口更容易扩展;JWT自包含用户信息,减少Redis或Session共享的压力;顺带提一下token过期和刷新机制的设计。
第二个,MyBatis-Plus和MyBatis有什么区别?回答要点:MP是MyBatis的增强工具,内置通用Mapper、分页插件和条件构造器,单表CRUD不用手写SQL。进一步说一下如果你遇到复杂多表联查,MP也能通过注解或XML自定义SQL,两者不冲突。
第三个,数据库有哪些索引?回答要点:除了主键索引,常用查询字段比如device_info表的ship_id、maintenance_order表的assignee和status应该建普通索引。如果老师追问联合索引,可以答“(ship_id, status)”组合索引能覆盖绝大多数维保工单查询场景,同时说明最左前缀原则。
第四个,如果用户量大了,系统瓶颈在哪,怎么优化?回答要点:先数据库层加索引和读写分离,再引入Redis缓存热门设备档案和工单状态;进一步可以按船舶维度做数据分片。用不到微服务,但你要表现出知道怎么演进。
6.3 项目扩展方向
如果时间富余,强烈建议至少做以下三个扩展中的一个,这部分相当于答辩的加分题。
一是引入Redis缓存验证码和热点数据。登录验证码存入Redis并设置两分钟过期,设备档案和备件库存等热点数据缓存起来,同时解决缓存和数据库的一致性更新问题。这个扩展能引出分布式会话、缓存穿透、缓存雪崩等面试高频题。
二是加入消息队列做工单通知。维修工被指派新工单时,通过MQ或者WebSocket推送一个站内消息,替代现在的刷新才看到新工单的体验。实现可以在本地用Spring的事件驱动先跑通,答辩时讲清楚设计思路就够了。
三是把报表模块升级成定时生成的Dashboard大屏。用定时任务每天凌晨汇总前一天的数据,前端用大屏页面展示全船维保健康度。视觉效果极好,几乎可以保证答辩现场所有人的注意力都在你的屏幕上。
最后说句题外话。很多人拿到源码后第一反应是赶紧跑起来看看效果,但我的建议正相反——先花一个小时把表结构和核心业务流程看懂,再动手启动。因为答辩时老师不一定会盯着你的系统有多炫,但他一定会问:这张表为什么要这样设计?这个状态是怎么流转的?你要是连自己的数据库都没理清楚,系统做得再漂亮也白搭。这个习惯我从带毕设到现在,至少让几十个学生避开了答辩翻车的坑,今天一起分享给你。