SpringBoot+Vue+MySQL船舶维保管理系统设计与实现
在工业信息化项目里摸爬滚打这几年我最大的感受是越是不起眼的传统行业越需要一套真正能用、能落地的管理系统。船舶维保就是典型场景主机、辅机、舵机、锚机、消防设备一堆每一台都关系到航行安全维护记录又长期靠纸质台账和Excel。近期我正好把之前在某船队信息化项目里做过的一套船舶维保管理系统源码整理成了可直接运行版本技术栈明确SpringBoot后端、Vue前端、MySQL数据库打通了从保养计划、工单派发到备件出库、统计报表的完整闭环。这篇文章不铺垫太多就把我从数据库建模、后端接口设计、前端页面开发到本地部署的完整经验拆开给正在做同类型系统的朋友一些可照抄的参考。1. 项目定位与整体设计思路1.1 船舶维保管理到底在管什么船舶维保和普通工厂设备维护有很大区别。普通工厂设备是固定的出了问题工人可以马上到现场船舶不一样船是移动的海上航行期间网络时有时无维修人员和备件都跟着船走一旦某个关键设备出故障影响的不只是经济损失还有安全风险。所以船舶维保系统的第一个核心诉求是“计划性”——什么时候该保养、上次保养做了什么、下次保养什么时候到期必须提前算清楚。从业务流程看一条船上的维保工作大致分四层第一层是基础档案包括船舶信息、设备清单第二层是保养计划按周期设置月度、季度、年度任务第三层是工单执行计划到期自动生成工单或者突发故障临时开维修单第四层是备件和统计每次维修用了什么备件、库存还剩多少、各船维保完成率如何都要有据可查。这个系统就是把这些线下流程搬到线上让人不用再翻纸质记录。我见过很多同类项目最大的问题不是功能不够多而是“流程太散”。每个模块单独做一个页面工单系统和备件系统互不相通做完一个工单还要手动去减库存。这套源码在设计时最关注的就是模块之间的闭环关系工单和备件、工单和计划必须串起来而不是各管各的。1.2 技术选型为什么是SpringBoot、Vue、MySQL这套组合这套组合在当前企业级应用里其实是非常务实的选择没有盲目追新。后端选SpringBoot是因为它在Java生态里已经是事实标准内置Tomcat自动配置完善开发效率相比传统SSH要高很多配合MyBatis-Plus操作数据库单表CRUD几乎不需要写SQL复杂查询再手写XML兼顾速度和可控性。前端选Vue的原因是工程化成熟、生态丰富。Vue3搭配Element Plus拿来做管理后台非常顺手表格、表单、弹窗、日期选择器这些组件都能直接复用开发周期能压缩到很短。而且Vue的响应式机制在处理工单状态流转、数据联动这类场景时非常自然不用像操作DOM那样繁琐。数据库选MySQL就不用多解释了对于项目初期几千万级别的数据量完全足够运维成本低公司内部招人也容易。很多团队一开始就上Oracle或者SqlServer实际对这类中小规模管理系统来说有点小题大做。MySQL 5.7以上版本性能已经很好配合索引优化完全够用。这套组合还有一个优势前后端分离后端只提供JSON接口前端负责页面渲染。这意味着将来做移动端或者对接其他系统时后端接口可以继续复用不会绑死在Web页面上。我见过不少老系统是Thymeleaf模板直接在服务端渲染想加一个小程序端都得重构前后端分离能避免这种尴尬。1.3 功能模块与角色权限拆解从使用角色来看船舶维保系统通常涉及四类人公司层面的机务管理员、船上的轮机长和维修工、备件管理员偶尔还有只看报表的领导层。每种角色关心的数据不同权限设计必须从一开始就考虑不能等做完了再补。模块划分上这套系统我拆成了六个核心部分基础数据管理船舶档案、设备台账、备件分类维保计划管理周期设置、到期提醒、计划模板工单管理计划生成工单、故障报修、派单、完工审核备件管理入库、出库、库存预警、流水记录系统管理用户、角色、菜单权限统计看板各船维保完成率、故障分布、备件消耗排行权限模型采用经典的RBAC用户挂角色角色挂菜单和操作权限。菜单权限控制“能看哪些页面”操作权限控制“能点哪些按钮”比如普通维修工只能看到派给自己的工单、只能提交完工没有权限把工单作废或重新指派。前端所有按钮的隐藏只是用户体验层面后端接口必须再做一次权限校验不能相信前端传过来的角色字段。2. 数据库模型设计复盘船、设备、计划、工单怎么串起来2.1 核心表结构与关系梳理数据库是整个系统的地基我整理源码时第一个核对的就是表结构。这套系统的表设计围绕一条主线一艘船有多台设备一台设备对应多条保养计划一条计划到期生成一张工单一张工单领用多种备件。把这条主线画清楚表的关系就自然出来了。核心表可以这样规划源码里基本就是这套结构表名用途关键字段ship船舶档案ship_code, ship_name, ship_type, tonnagedevice设备台账ship_id, device_code, device_name, device_typemaintain_plan维保计划ship_id, device_id, cycle_type, next_maintain_timework_order工单表order_no, plan_id, ship_id, device_id, statuswork_order_item工单备件明细work_order_id, part_id, quantityspare_part备件档案part_code, part_name, spec, stock, warning_stockstock_log出入库流水part_id, change_type, quantity, work_order_idsys_user用户表username, password, role_id, ship_idsys_role / sys_menu角色与菜单role_id, menu_id, perm_codeship和device是一对多device和maintain_plan是一对多maintain_plan和work_order是一对多。work_order和spare_part之间是多对多所以需要work_order_item这个中间表同时在stock_log里记录出库流水保证备件库存的变动都有据可查。不建议在业务表里大量设置数据库外键约束。外键会降低写入性能和迁移灵活性而且很多团队删除数据时会因为外键约束莫名其妙失败。表关系靠代码逻辑维护就够了关键查询走索引比物理外键更可控。2.2 关键字段与状态枚举设计数据库设计中有几个字段特别值得讲究。第一个是工单号order_no业务上它是给船上的机务人员看的最好一眼能看出是什么时候、哪条船、哪种类型的工单。比如ORD-20250612-SH001-03含义是2025年6月12日船编号SH001的第三张工单。自增ID不适合直接暴露给业务使用因为人工核对、电话报工时说“工单24601”远没有“ORD-20250612-SH001-03”直观。第二个是状态字段。很多新手喜欢用中文直接存环境比如“待派单”看着是清楚了但程序判断时很容易出问题万一哪天想改成“待分配”所有数据都得刷一遍。正确做法是存数字枚举比如工单状态0待派单、1维修中、2待验收、3已完成、4已作废展示层再翻译成中文。源码里已经把所有状态的数字定义写在枚举类里改起来也方便。第三个是时间字段。系统里要有create_time、update_time而且建议都由数据库自动维护MySQL里用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP后端代码就不用每次手动set时间也不容易出现时区不一致。2.3 设计时容易忽略的三个细节第一逻辑删除字段。企业系统几乎不做物理删除一张工单备注或者船舶信息误删了很难恢复。每张业务表都建议加deleted字段默认0删除时改成1查询条件统一带上deleted0。MyBatis-Plus的TableLogic注解可以直接处理这个逻辑不用每条SQL都手动加条件。第二冗余字段不要怕。工单表里冗余了ship_name、device_name这些展示字段。按理说这些信息可以从船舶表关联查出来但维保工单在后续统计、导出、二次转发时如果都要join一次ship表SQL会越来越复杂。冗余字段会带来字段不同步的风险所以要评估场景不是所有地方都冗余。工单这类经常独立展示和导出的数据冗余是划算的。第三预留一个remark字段给所有核心表。现实业务里总会有特殊情况需要记录比如“这次保养发现主机密封圈老化下次重点检查”。没有remark会让业务人员觉得系统死板只能靠Excel补充记录系统价值就打了折扣。3. 后端核心实现SpringBoot的业务闭环怎么落地3.1 分层设计Controller只做参数接收后端代码如果全部堆在Controller里改起来会非常痛苦。我整理源码时坚持了三层结构Controller只负责接收参数、调用Service、返回结果Service写业务逻辑、事务控制Mapper对应数据操作。Controller层不直接出现SQLService内部不直接写JSON返回结构。有人觉得这样多写了很多代码实际项目规模一大就知道分层的重要性。一个工单创建动作先要校验设备是否存在、计划是否过期、是否有重复工单、库存是否足够这些逻辑写在Service里可以方便复用。比如备件出库这个方法工单完工审核时要调用月底盘点时可能也要调用如果散落在某个Controller里换一个场景就无法复用了。事务控制是另一个关键点。创建工单并扣减备件库存时如果工单写入成功但库存扣减失败数据就错了。所以这类操作必须在Service方法上标注Transactional让两个数据操作处于同一个事务中要么都成功要么都失败。源码里所有涉及多表写入的方法都加了事务注解排查问题时不需要再怀疑数据不一致。3.2 维保工单闭环从计划生成到完工审核整条业务链路中最核心的是工单闭环。我不想只设计一个“手工创建工单”的接口因为这样计划模块就形同虚设了。这条闭环我拆成了四步。第一步计划生成工单。系统每天通过定时任务扫描maintain_plan表找出next_maintain_time小于当前时间且状态正常的计划自动生成对应工单。这一步必须做幂等控制否则定时任务跑两次就会生成重复工单。我的做法是在工单表里增加唯一约束由plan_id和order_type唯一确定一张计划保养工单定时任务插入时捕获重复插入异常跳过即可。第二步派单与接单。计划生成的是“待派单”状态的工单机务管理员在界面上把工单指派给具体的维修工。这个动作会更新assignee字段和assign_time同时把状态改为“维修中”。第三步完工与验收。维修工填写实际开始时间、结束时间、工作内容、更换备件明细提交完工状态变成“待验收”。机务管理员检查没问题后点击验收通过状态变成“已完成”。验收不通过则退回“维修中”附上退回原因。第四步备件出库与计划更新。工单验收通过后程序根据工单备件明细扣减库存写入stock_log流水同时反推该设备下一次保养时间更新maintain_plan的next_maintain_time。这个闭环千万不能少不然做一次保养下次系统还会提醒你保养会被人怀疑系统有Bug。3.3 接口设计路径、参数和返回结构接口设计要有统一规范。源码里所有接口路径都遵循/api/模块名/资源名方法语义通过HTTP Method表达。比如POST /api/work-order表示新建工单PUT /api/work-order/{id}/status表示修改工单状态GET /api/work-order/page表示分页查询工单。返回结构也是统一的约定一个Result对象包含code、message、data三个字段。code为200表示成功400表示参数错误401表示未认证403表示无权限500表示服务端异常。前端axios统一拦截这个结构不需要每个页面单独处理错误。这里有个坑要提醒后端千万不要把异常堆栈直接拼在message里返回既暴露内网信息又不好看懂只返回一句“系统繁忙请稍后重试”详细日志留在后端文件里排查。除登录接口和几个基础字典接口外所有接口都要求请求头带Authorization的JWT。写接口时有一个实用技巧列表查询统一支持分页参数pageNum、pageSize条件参数比如shipId、status都走GET query参数这样前端做筛选时逻辑非常一致几乎一个通用查询组件就能覆盖全部列表页。3.4 验证码与安全别在安全细节上偷懒虽然这是内部系统登录接口也不能裸奔。源码里登录逻辑做了几件容易被忽略的事第一是密码存储用BCrypt加密后的哈希值绝不能明文存储密码第二是登录接口加了图形验证码防止脚本暴力尝试第三是JWT设置合理的过期时间一般24小时左右第四是JWT密钥不写死在代码里而是放在配置文件中部署时手动修改。很多管理系统的数据泄露不是因为黑客技术多高而是因为开发阶段把测试账号、万能密码带到了生产环境。源码配置文件里默认账号是admin/初始密码但部署必须要求改掉。另外在权限拦截器里一定要把登录接口、验证码接口放进白名单其他接口全部拦截否则前端还没登录就开始请求业务接口会得到一屏的401报错。4. 前端工程化实现Vue项目架构与关键交互4.1 页面结构按业务模块拆分路由前端工程采用Vue3 Vite Element Plus目录结构按照页面角色拆分看代码时能很快找到对应模块。views目录下分为ship、device、plan、work-order、spare-part、system、dashboard几个子目录对应后端的不同业务模块。每个目录下还有一个components子目录存放这个模块特有的组件。路由方面登录后默认跳转到dashboard看板页。左侧菜单根据用户权限动态生成管理员能看到所有菜单普通维修工只看得到工单和基础信息页。这里用了路由守卫实现每次路由跳转前先检查本地存储是否有token没有就跳回/login。有token但不能访问目标路由时走404页面而不是白屏这个细节对用户体验影响很大。列表页的开发用了不少重复操作所以我把表格页拆成了通用组件。查询条件区、表格区、分页区三个区域查询参数都放在一个对象里点击搜索或重置时改变对象并重新请求接口。这样后面新增任何一个管理模块只需要套用模板再补充具体列和表单字段两三天就能出一个完整页面。4.2 axios封装统一请求、统一鉴权、统一错误前端所有请求都经过封装的request模块这个模块不封装的话每个页面各自处理401、处理超时、处理错误提示代码会爆炸。axios拦截器里做了三件事。请求拦截从本地存储取token放到Authorization头保证所有业务接口自动带上凭证。响应拦截统一检查后端返回的codecode为200时直接返回data给页面其他code统一弹错误提示并中断PromiseHTTP状态码为401时清掉本地失效token跳转登录页。超时时间设置10秒长时间没有响应的接口直接提示网络异常避免页面一直转圈。这部分还有一个经验大文件或者图片上传的接口Content-Type不能设置成application/json要让axios自动识别FormData。项目里上传船舶照片、维修附件时我在request模块里单独封装了一个uploadFile方法不走同一个拦截器避免混淆。4.3 工单状态流转的交互设计要点工单模块是整个前端交互最重要的部分。列表页默认只查询待办工单提供“我的工单”“全部工单”页签切换。状态栏用不同颜色的Tag展示0待派单用灰色1维修中用蓝色2待验收用橙色3已完成用绿色4已作废用红色机务人员扫一眼就能知道当前重点关注哪个。分状态操作按钮也是动态渲染的。待派单状态只有管理员能看到“指派”按钮维修中状态显示“提交完工”待验收状态显示“验收通过”和“退回”已完成和已作废状态不显示任何操作按钮。实现上很简单v-if结合当前登录用户的按钮权限判断但业务上很关键防止误操作把已完工厂单又流转回去。工单详情页采用抽屉Drawer组件而不是新开页面因为工单列表和详情的切换非常频繁。抽屉里除了基本信息还嵌了一个备件明细表格工单关联了哪些备件、数量多少、单价多少一次性展示。维修工在提交完工前可以在抽屉内直接添加备件行提交时工单主表和明细一起传给后端后端再在同一个事务里落库。4.4 统计看板和报表怎么做不卡dashboard看板要展示维保完成率、工单数量趋势、备件库存预警列表这些数据如果都实时查询大表慢查询是必然的。这里的做法是单独建一张统计汇总表每天凌晨定时计算前一天的各船、各设备维保指标看板页面只查这张汇总表。前端看板用图表库展示趋势数据接口只返回日期和数值两个数组图表渲染非常轻量。另外我建议看板页面不要放太多指标工业场景的看板要的是“一屏看清重点”放6到8个核心卡片就够了。做数据大屏也一样堆太多图表反而没人看。还有一个小技巧列表导出Excel功能很多人用前端做但前端导出几十万行会卡死浏览器。工单、备件流水这类数据量可能很大的列表导出应交给后端。后端生成文件后返回下载链接前端只负责打开链接并提示“导出任务已开始”大列表导出用异步任务加消息提示避免页面假死。5. 本地部署全流程把源码从零跑起来5.1 环境准备清单“可直接运行”这四个字意味着别人拿到源码后能在最短时间内跑起来。我整理源码时没少花时间在环境兼容性上。后端是SpringBoot 2.7版本开发环境用JDK8和Maven3.6前端用Node14以上版本数据库默认配置MySQL8.05.7版本也兼容。如果本机环境有冲突建议直接用Docker装MySQL避免卸载重装。安装完MySQL需要确认两件事root账号能本地登录、字符集是utf8mb4。遇到过很多次装完数据库默认字符集是latin1中文数据写入后变成乱码启动项目前就要把字符集设置好远比事后刷数据省事。5.2 数据库初始化和后端配置第一步创建数据库。连接上MySQL后用CREATE DATABASE ship_maint DEFAULT CHARACTER SET utf8mb4再导入源码目录下sql目录里的初始化脚本。脚本里同时包含了表结构和初始数据第一版跑起来后不用再去手动建表。第二步改后端配置文件。重点是application.yml里的数据库连接信息把url、username、password改成自己本机的。连接串里一定要带serverTimezoneAsia/Shanghai不然JDBC驱动连接MySQL8.0时会报时区错误。还有一个上传文件路径的配置项默认是当前目录下的upload文件夹启动前确认这个路径有写入权限否则上传功能会报FileNotFoundException。第三步启动后端。在项目根目录执行mvn spring-boot:run或者先执行mvn package打成jar再java -jar运行。看到“Started Application”日志说明启动成功本地端口默认8080。5.3 前端启动与代理配置前端启动相对简单进入frontend目录执行npm install安装依赖。这步最容易踩坑的就是网络问题和依赖版本冲突Node版本过高或过低都会导致某个包编译失败。Vite3项目建议Node14或Node16如果你本机是Node18以上个别依赖可能报错升级Vite版本即可解决。依赖安装完成后检查vite.config.js里的代理配置把/api开头的请求代理到http://127.0.0.1:8080。前端页面里请求统一用/api这个前缀开发环境通过代理转发生产环境则让Nginx把/api反向代理到后端服务。这个约定保证前端代码不用区分环境。然后执行npm run dev启动默认在5173端口。浏览器访问应该能看到登录页输入管理员账号就能进入系统。如果登录请求一直转圈优先检查浏览器控制台Network面板里接口返回是否是404如果是404说明后端没启动或者代理没生效。5.4 生产部署建议开发环境跑通只是第一步真正上线部署还需要注意几件事。后端打包后建议用java -jar配合外部配置文件启动把application-prod.yml放到jar包同级目录这样修改数据库密码或上传路径不用重新打包。前端执行npm run build后生成dist目录交给Nginx托管同时配置一个/api的location转发到后端接口地址。生产环境的数据库账号不建议直接给root单独创建一个业务账号只授权ship_maint库的增删改查权限把安全风险收敛到最小。日志配置也很关键SpringBoot默认日志会在连续运行后无限膨胀而SpringBoot内置logback建议在application.yml里限定每个日志文件大小保留最近30天即可。6. 常见问题排查与避坑实录6.1 高频启动报错速查表项目运行期最容易碰到的问题基本集中在启动阶段我整理了张速查表报错现象可能原因解决办法后端启动报Access denied for user数据库账号密码错误检查application.yml里的用户名密码后端启动报Unknown database数据库名不正确确认创建的是ship_maint且连接串一致后端启动报时区异常MySQL连接串缺时区参数加上serverTimezoneAsia/Shanghai前端npm install报node-gyp失败Node版本与依赖不兼容切换Node14或16删除node_modules重装前端页面显示404菜单权限没有配置完整登录管理员账号检查角色菜单勾选接口返回401token缺失或过期退出重新登录检查前端拦截器中文内容乱码数据库字符集不对检查库和表的字符集改为utf8mb4这些坑看着简单但新手多半都会遇到一两个。遇到Any one的报错我建议先看控制台完整错误栈不要只看最下面一行结论很多问题是多个原因叠加的。6.2 数据层面的典型坑工单列表查询性能在数据量上来后明显变慢这是典型的索引缺失问题。工单表经常按status和ship_id组合查询需要在status、ship_id、create_time上建联合索引。而索引不是越多越好写频繁的表索引太多会拖慢插入速度按核心查询路径建两三个就够了。备件库存这种频繁更新数值的表扣减库存时一定要用UPDATE stock stock - #{quantity} WHERE stock #{quantity}这样的原子操作而不是先SELECT查出来再UPDATE并发时不会超卖。库存预警也是一个常见需求在spare_part表里用warning_stock字段做“低于库存预警值”的判断把预警数量比较在SQL里完成而不是查全表后在内存里循环否则数据量一大就慢。6.3 联调阶段的网络问题前后端联调最典型的是跨域问题。前端在5173端口访问8080端口的接口浏览器会拦截非简单请求。我给的示例中前端配了代理所以基本能规避但如果你的前端不走代理而是直接请求后端地址就必须在后端配置跨域过滤器否则预检请求直接失败。另一个容易被忽略的是时间格式问题。后端返回的时间默认是时间戳前端展示时直接变成一串数字。解决方式是后端全局设置Jackson的日期格式统一为yyyy-MM-dd HH:mm:ss前端再配合Element Plus的日期组件指定value-format两边对齐避免出现“提交时间显示NaN-NaN-NaN”的怪现象。7. 后续可以继续扩展的方向这套系统已经能跑通核心业务但船舶维保场景还有不少可以深入做的地方。最常见的是增加消息提醒计划到期、工单超时未完工、备件库存低于预警线时通过企业微信或者短信推送通知相关责任人。再进一步是做移动端适配船上的维修人员不一定随时抱着电脑微信小程序或App端查看工单和处理报修能显著提高执行效率。巡检扫码也是一个很实用的方向。给每个设备生成二维码贴在设备旁维修人员现场扫码就能看到设备历史和最近工单完工后直接拍照上传维修结果。如果有多家船东公司接入还可以考虑做SaaS多租户模式在现有表结构上增加租户隔离字段一套代码服务多个船队这又是另一个量级的项目了。最后分享一个我在整理这套源码过程中最深的体会管理系统能不能用起来关键不在技术多复杂而在流程逻辑是否和真实业务对得上。开始动手写代码前一定要和一线机务人员多聊几次把“工单状态怎么流转”“备件什么时候出库”“保养周期怎么计算”这些业务规则彻底确认清楚代码写起来才顺畅。这套源码我按照真实船队维保场景调整过多次业务逻辑基本是稳的你拿到的第一件事建议多看SQL脚本和枚举类看懂业务约定再去看页面功能会事半功倍。