☰
基于SpringBoot2+Vue3的船舶维保管理系统全栈实战解析
2026/10/13 3:45:45 网站建设 项目流程

如果你打开各类源码分享平台,输入"船舶维保管理系统"这个关键词,大概率会看到这样一个技术组合:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,末尾还要缀上"含文档"。这几乎是当前Java Web中小型管理系统的标准套餐——后端用SpringBoot2搭接口,MyBatis-Plus做ORM层省去大量SQL,前端用Vue3全家桶撑起交互,数据落在MySQL8.0上,附带文档意味着不光是代码,还给了数据库脚本、接口说明和操作步骤,能真正跑起来。

我用这个技术栈做过不止一个船务类的信息化项目,对这个标题背后的业务逻辑和落地细节非常熟悉。很多刚接触这类源码的人,第一反应是"我能不能直接拿它当毕业设计",或者"能不能改改就接到真实业务里"。我的答案是:可以,但你必须先把这套系统要管的业务理顺。船舶维保不是简单的设备维修登记,它牵扯到船舶设备台账、保养计划排期、工单下发与验收、备件领用出库、船检证书到期提醒这一整条链。下面我从业务拆解、后端实现、前端实战、本地部署和踩坑记录这几个角度,把这类项目完整过一遍。

1. 船舶维保业务到底要管什么,系统怎么拆出来

1.1 船务维保的行业痛点和模块划分

船舶和普通设备最大的区别在于"移动的资产"和"强制性的检验"。一艘船开到海上,主机、辅机、舵机、锅炉、救生设备、通导设备任何一样出问题,不光影响运营,还会牵涉船舶检验和保险条款。所以船东公司对维保的核心诉求不是"坏了再修",而是"按计划保养、提前发现隐患"。系统里最核心的模块自然就是设备台账、维保计划、工单执行和检验提醒这四个。

我拆过好几个船务项目,发现真正上线后使用频率最高的页面反而是设备台账和计划列表。设备台账是根,所有维保计划、工单、备件消耗都挂靠在设备上。拿主机举例,台账里要记录船舶名称、设备编码、设备名称、型号、安装位置、生产厂商、出厂编号、投产日期这些字段。没有台账,后续一切计划都无从谈起。

1.2 从标题看技术选型背后的逻辑

SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套组合在中小企业项目里出现频率极高,不是没有道理。

SpringBoot2的好处是生态成熟,网上资料多,遇到问题一搜就有解决方案。相比SpringBoot3,虽然3已经出了很久,但在国内存量项目里SpringBoot2依然占据大量份额,JDK8跑得稳,主流中间件兼容性也更好。MyBatis-Plus解决的是单表CRUD的效率问题,BaseMapper直接提供增删改查方法,配合条件构造器QueryWrapper,简单查询连SQL都不用手写。MySQL8.0相对5.x增加了窗口函数、CTE等能力,对报表统计类需求更友好,而且8.0对JSON数据类型的支持也明显更强。

这套组合的本质是"用最小的开发量把传统管理系统的骨架搭起来,把节省出来的时间留给业务逻辑"。对于船舶维保这个场景来说,业务逻辑的复杂度集中在计划周期计算和工单状态流转,而不是框架本身,所以选这套组合非常合适。

1.3 数据模型设计思路

看这类源码,我建议先理清实体关系而不是急着跑代码。典型的船舶维保系统会有这样几张核心表:

  • 船档表:船名、船舶类型、总吨、净吨、主机功率、船籍港、建造日期
  • 设备分类表和设备台账表:设备属于哪艘船、属于哪个系统、编码规则、位置、状态
  • 维保计划表:计划名称、设备关联、计划周期类型(日保、周保、月度、季度、年度)、间隔参数、上次执行日期、下次执行日期
  • 维保工单表:单号、计划关联、工单状态、执行人、验收人、计划开始时间、实际完成时间
  • 备件材料表:备件编码、名称、规格、单位、库存数量、预警阈值
  • 维保记录表:工单执行后写入的文本记录、附件地址、结果标记
  • 检验证书表:证书名称、证书编号、发证日期、有效期截止日、下次检验日期

其中最容易忽略的是维保计划表和工单表之间的关系。正确设计是计划表负责"该不该做",工单表负责"做了没有、结果如何"。每次到了计划周期触发时间,系统生成一张待执行的工单,执行完回写记录。如果直接把执行记录写在计划表里,后续想做历史查询和统计就会很痛苦。我在某次改造项目里就见过把执行明细塞在计划表的文本字段里的做法,结果统计报表只能靠字符串截取,非常坑。

2. 后端SpringBoot2核心实现:从建表到接口的完整链路

2.1 基础工程配置与公共类的搭建

拿到这类源码,先从pom.xml看依赖。SpringBoot2.7 + mybatis-plus-boot-starter + mysql-connector-j + lombok + jwt是常见组合。如果项目里还要做文件上传、Excel导出,会额外引入hutool或EasyExcel。

第一件事是配置统一返回结构。我习惯定义一个Result类,包含code、message、data三个字段,Controller层所有接口统一返回。前端axios拦截器只需要判断code是否为200,是则取data,否则弹出message。这个小习惯能避免前后端联调时各写各的返回格式,到了对接阶段互相扯皮。

然后是全局异常处理。用@RestControllerAdvice包一层,业务异常、参数校验异常、系统异常分开处理。很多新手项目不写全局异常处理,Controller里每个方法都写try-catch,代码又长又乱。用全局异常处理器之后,Service层只需要在业务不满足条件时抛出业务异常,前端拿到的永远是统一的错误结构。这个习惯在维保项目中尤其重要,因为工单流转涉及审批、验收、库存扣减这类多步骤操作,任一步出错都需要清晰反馈。

2.2 MyBatis-Plus的常用配置与自动填充

MyBatis-Plus在这一类项目里几乎是标配。集成时要注意三件事。

配置MapperScan。主启动类上记得加@MapperScan("xx.mapper"),或者每个Mapper接口标注@Mapper注解,二选一即可,别重复配。

分页插件必须显式声明。MyBatis-Plus的分页不是默认开启的,需要在配置类里注入MybatisPlusInterceptor,并添加PaginationInnerInterceptor。很多初学者写完分页查询发现返回全部数据,就是少了这一步。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }

自动填充是这一类系统里提升效率的关键。create_time、update_time这两个字段,如果每个新增和更新操作都手动set,不仅重复劳动,还容易漏。MyBatis-Plus提供了MetaObjectHandler接口,在插入时自动填充创建时间和更新时间,在更新时自动填充更新时间,代码层面完全无感知。字段上记得加@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE)。

逻辑删除也一样,实体字段加@TableLogic注解,配置全局逻辑删除值和未删除值。这样所有查询会自动带上deleted=0条件,删除操作变成update,数据保留在库里方便追溯。船舶维保数据是有追溯要求的,设备维修历史不能真删,逻辑删除在这种场景下几乎是必须的。

2.3 维保计划周期计算的业务逻辑

这是整个系统最需要动脑子的地方。维保计划通常不是一次性的,而是周期性循环的,计划执行完要自动推算下一次日期。

最简单的实现方式是在计划表里存上次执行日期和周期天数,下次执行日期 = 上次执行日期 + 周期天数。但这只适用于按天计算的情况。船舶维保里常见的"每季度""每半年""每年度"如果都折算成天数,遇到闰年会漂移,而且不直观。

更好的做法是枚举计划周期类型,根据类型调用不同的日期计算逻辑。例如:

  • 日保:nextDate = currentDate.plusDays(1)
  • 周保:nextDate = currentDate.plusWeeks(1)
  • 月保:nextDate = currentDate.plusMonths(1)
  • 季保:nextDate = currentDate.plusMonths(3)
  • 年保:nextDate = currentDate.plusYears(1)

还要考虑一种情况:某个计划已经逾期但一直没生成工单,这时候系统应该自动补单还是提示人工处理?我见过两种方案,一种是把逾期计划高亮列在待办页让机务人员手动触发,另一种是定时任务自动补单。实际业务中我更倾向第一种,因为维保资源是有限的,强行自动补单会让机务人员疲于应对,反而不利于真实执行。

2.4 工单状态流转与权限控制

工单是维保流程的执行载体,状态流转一般是:待下发 -> 执行中 -> 待验收 -> 已完成,特殊情况可以取消。每一步对应不同角色:调度或者机务经理下发工单,轮机员/船员执行,机务经理验收。

状态流转不要用简单的int字段在前端随便改,应该在后端用枚举或者状态机校验。最简单的做法是接口里做状态判断,只有合法流转才允许更新。比如待下发状态只能流转到执行中,不能直接跳到已完成。这个约束放在后端能避免前端误操作带来的脏数据。

权限控制方面,这类管理系统的常规做法是Spring Security + JWT,配合RBAC模型。用户表、角色表、菜单表、用户角色关联表、角色菜单关联表五张表是标配。前端根据用户角色渲染菜单,后端在接口上做角色校验。维保系统里典型的角色有管理员、机务经理、船长、轮机员、普通船员,角色不同能看到的菜单和可执行的操作差异很大。

3. 前端Vue3项目结构:从初始化到维保页面实战

3.1 工程初始化与常用依赖选择

前端工程用Vite构建是比较主流的选择,比Webpack快很多。创建Vue3 + Vite工程后,常用的依赖是vue-router、pinia、axios、Element Plus。Element Plus是这套组合里最省事的UI组件库,表格、表单、弹窗、分页都齐了。

工程目录我习惯这样组织:

src/ api/ # 按模块拆分的接口请求文件 assets/ # 静态资源 components/ # 公共组件 router/ # 路由配置 stores/ # Pinia状态管理 utils/ # axios封装、auth工具等 views/ # 页面组件

如果拿到手的源码没有按模块拆分api,建议自己重构一遍。把请求地址散落在页面组件里是最难维护的写法,改一个接口名得全局搜索。

3.2 axios封装与token处理的细节

axios封装的核心是拦截器。请求拦截器统一把token放到Authorization头里,响应拦截器统一处理业务错误码和HTTP错误码。这套逻辑几乎每个管理系统都有,但细节上有差别。

import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/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) { // 业务错误统一提示 return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default service

有一个细节容易被忽略:响应拦截器返回的是res.data而不是response,这样页面里调用接口的时候直接拿业务数据,少一层结构访问。但如果后端返回的data里还有分页对象,代码里就要注意是page.records还是page.list。

3.3 维保工单页面:状态驱动的交互设计

维保工单列表页是这类系统里最典型的页面,包含搜索条件、表格、分页、新增和编辑按钮。搜索条件常见的有工单号、船舶名称、计划类型、状态、时间范围。

表格列建议展示工单号、关联设备、计划类型、执行人、状态、计划开始时间、实际完成时间、操作按钮。操作按钮要根据状态动态显示:待下发状态显示"下发"和"取消",执行中状态显示"提交验收",待验收状态显示"验收通过"和"驳回"。

这个动态按钮的逻辑用Vue3的computed或者直接在模板里用v-if都行。我更倾向把按钮权限和状态权限分开判断,状态权限在前端做,按钮权限结合用户角色做,两套一起判断。

工单新增和编辑建议用弹窗表单而不是单独的页面,因为字段控制在十个以内,弹窗操作路径短,用户体验更好。表单里日期选择器用Element Plus的el-date-picker,提交时把value-format设置为"YYYY-MM-DD HH:mm:ss",后端直接用String接收也不会出错。

3.4 Pinia状态管理和路由守卫

登录状态存Pinia还是localStorage?我的习惯是Pinia存内存中的用户信息,token持久化到localStorage。刷新页面后,通过路由守卫里调用获取用户信息接口重新拉取。用户体验和安全性都能兼顾。

路由守卫的逻辑不复杂:判断是否存在token,没有则跳转登录页;登录后根据用户角色动态生成可访问的菜单路由。这里有一个常见坑:动态路由如果是在登录后才addRoute的,刷新页面会因为路由还没注册而过早跳转404。解决方案是在全局前置守卫里判断store是否已加载路由,未加载则先await动态挂载路由再接续放行。

4. 从拿到源码到跑起来:本地环境搭建与部署实录

4.1 版本匹配清单

拿到这类源码,先别急着点运行。先按照版本矩阵检查本地环境,版本不匹配是跑不起来的第一大原因。

组件推荐版本说明
JDK1.8 或 11SpringBoot2.7官方支持JDK8/11/17,但源码多半按JDK8语法编写
Maven3.6.3 以上太低可能无法解析部分依赖
Node.js16.18.x 或 18.xVite4要求Node14.18+,Vite5要求Node18+,先看package.json
npm/yarnnpm 8+ 或 yarn 1.22常规选择
MySQL8.0.x注意字符集utf8mb4,连接驱动用8.0.33
Redis不必须若源码里引入了Redis做缓存,需要本地启动一个实例

如果源码里用的是Vite5而本地Node是16,启动报错几乎是必然的。遇到这种问题优先匹配Node版本,而不是去改Vite版本,因为项目依赖的插件可能也不兼容。

4.2 数据库脚本导入的三个关键步骤

MySQL8.0导入脚本的坑集中在编码和时区。脚本文件先确认是utf8还是utf8mb4编码,导入时如果用了错误字符集,中文乱码很难排查。

导入步骤建议:

  1. 用数据库管理工具新建数据库,字符集选择utf8mb4,排序规则选择utf8mb4_general_ci或utf8mb4_unicode_ci
  2. 打开sql脚本,检查建库语句里是否指定了数据库名。如果脚本里包含CREATE DATABASE,建议注释掉,只保留表结构和数据部分,避免库名冲突
  3. 执行脚本后,重点检查几张核心表的数据量,特别是用户表和菜单表。如果登录后发现页面空白,多半是菜单表和角色表关联数据没导全

4.3 后端数据源配置和常见报错

后端application.yml里的数据源配置是第一个要改的地方。MySQL8.0的驱动类名是com.mysql.cj.jdbc.Driver,URL要带serverTimezone参数和服务端的public key检索参数。

spring: datasource: url: jdbc:mysql://localhost:3306/ship_maintenance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver

allowPublicKeyRetrieval=true这个参数很多人会忽略。MySQL8.0默认使用caching_sha2_password认证,客户端首次连接时如果没有这个参数,会报"Public Key Retrieval is not allowed"的错误。加上之后,开发环境用没问题,生产环境如果安全要求高,建议改用mysql_native_password或者配置SSL。

4.4 前端联调与跨域代理

前后端分离项目联调最怕跨域。开发阶段不要靠后端加@CrossOrigin解决,应该用Vite的proxy代理转发。在vite.config.js里配置proxy,前端请求/api/xxx,开发服务器把请求转发到后端8080端口,浏览器感知不到跨域。

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

注意前端axios的baseURL配置成/api,和后端接口路径拼接。如果前端请求路径本来就带了/api前缀,而后端Controller的@RequestMapping是"/api/xxx",proxy的路径重写rewrite功能可能需要关闭,否则会出现双/api的问题。这块是前后端联调最典型的报错。

启动顺序上,先启动MySQL和Redis(如果用到),再启动后端SpringBoot,最后启动前端。后端启动后先访问后端端口下任意一个接口确认数据源配置没问题,再启动前端。如果一上来就前后端同时启动,出问题还要两头排查,浪费时间。

5. 我把这个项目跑通过程中踩过的坑

5.1 时间字段差8小时的陷阱

维保工单的计划时间在数据库里是对的,前端页面展示却少了8小时,而且数据库导入后date字段没问题,datetime字段却漂移。这个问题的根因是MySQL连接URL里的serverTimezone和系统时区不一致。

解决办法是把url里的serverTimezone显式设置为Asia/Shanghai,同时MySQL服务端时区也检查一下。8.0版本可以执行SET GLOBAL time_zone = '+08:00',或者修改my.cnf的default-time-zone。这类问题在初学时很容易漏掉,因为它不报错,只是数据看起来不对。

5.2 MyBatis-Plus逻辑删除与唯一索引冲突

我在一个备件表上设计了备件编码唯一索引,又加了逻辑删除字段。结果删除一条备件记录后,再插入同编码的新备件,唯一索引冲突导致插入失败。原因是逻辑删除没有真正删记录,旧数据还在表里占着唯一索引。

解决办法主要有两种:一是唯一索引改成联合索引,把deleted字段加进去;二是插入前先物理清除同编码且已删除的旧记录。考虑到维保系统的备件历史需要保留,我推荐前者。不过要注意联合索引也会让同编码的已删除记录最多保留一条,因为deleted字段在一般实现里并不是每条记录不同的值,如果不做特殊处理还是会冲突。

5.3 Excel导出字段顺序混乱与日期格式

维保计划导出是一个高频功能。用EasyExcel导出时,字段顺序由实体类属性顺序决定,而数据库查询返回的Map集合顺序不可控。如果你用的是@ExcelProperty注解可以指定index,否则默认顺序容易和表头错位。

日期格式也是高频问题。数据库的datetime字段导出到Excel里经常变成一串数字或者UTC格式,需要在导出字段上加@ExcelProperty(value = "计划时间", format = "yyyy-MM-dd HH:mm:ss")明确格式化。我踩过一次导出后日期显示成"2024-03-01T08:00:00",就是忽略了格式声明。

5.4 前端路由在history模式下的404

本地开发用BrowserRouter路由模式没问题,一旦部署到Nginx,刷新子页面就会出现404。这是因为前端是单页应用,路由跳转由前端处理,刷新时浏览器直接向服务器请求当前路径,服务器没有对应文件就返回404。

解决办法是Nginx配置try_files来兜底,让所有前端路由都回到index.html:

location / { try_files $uri $uri/ /index.html; }

或者更省事的做法是把vue-router切换到hash模式,URL上多一个#号,但部署时不需要额外配Nginx。实际项目中我一般看部署环境决定,内网管理类系统用hash模式更省心。

5.5 问题速查表

现象可能原因解决办法
项目启动报数据库连接失败数据源配置错误、MySQL未启动检查username/password/url,确认MySQL服务已启动
报Public Key Retrieval is not allowedMySQL8.0认证方式url加allowPublicKeyRetrieval=true
插入记录时create_time为空未配置自动填充处理器实现MetaObjectHandler并启动类扫描
分页查询返回全表数据未配置分页插件注入MybatisPlusInterceptor + PaginationInnerInterceptor
前端启动报Node版本不兼容Node与Vite版本不匹配按package.json要求切换Node版本
页面401循环跳登录token过期未处理响应拦截器对401做跳转,登录页排除在拦截外
导出Excel中文乱码响应头未设置编码设置Content-Type为application/vnd.ms-excel;charset=utf-8
请求后端接口跨域前后端端口不同Vite proxy代理或者后端加CORS配置

6. 这类系统还可以往哪些方向扩展

6.1 移动端扫码巡检

维保系统只坐在电脑前操作是不够的,机舱里的实际巡检场景更适合手机端操作。可以扩展扫码巡检功能:每台设备生成二维码,手机扫一扫进入设备详情,填写巡检结果,拍照上传。这个需求在船务场景里非常真实,船员不可能带着电脑去机舱。

实现方案可以是独立的移动端H5,后端复用现有接口,新增扫码登录和图片上传接口。二维码可以用设备的唯一编码生成,不需要额外开发复杂的二维码引擎。

6.2 备件库存与维保工单的联动

现有系统里备件领用往往和工单是两张皮,工单执行归工单,备件出库归出库,月底对账靠Excel。扩展方向是以工单号为关联字段,执行人领用备件时选择关联工单,备件出库自动记录在工单成本里。这样每个月可以统计出每条船的维保成本,这一块是船东公司非常关注的数据。

6.3 维保大屏与统计报表

管理部门关心的数据不是某一条工单是否按时完成,而是整体维保完成率、逾期率、故障类型分布、备件消耗趋势。可以扩展一个数据大屏页面或者把统计接口提供给BI系统。用MySQL8.0的窗口函数算月度环比增长率,比传统的临时表写法效率高很多。前端大屏用ECharts配合Vue3的组件化开发,一两天就能搭出可用的原型。

这类系统里我认为最有价值的扩展还是移动扫码和成本联动,它们直接把业务数据从"记录"变成了"分析",价值完全不同。

最后说一点个人体会。我每次拿到这类带文档的源码项目,从来不会直接扔进IDE运行,而是先花半小时把数据库脚本执行一遍,把表结构关系画出来,再根据ER图反推业务流转。这个习惯能让我在动手改代码之前就对系统的边界有清晰判断。还有一个非常实用的小技巧:跑通之后先点一遍菜单,把每一个页面涉及的接口在浏览器控制台里记录下来,标注好用途,后面无论是排查问题还是加功能,这份接口清单都能省很多时间。按这套思路把这个维保系统源码吃透,你收获的不仅是一个能跑的Demo,更是一套可以直接面对真实船务业务的技术框架。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询