在工业信息化项目里摸爬滚打这几年,我最大的感受是:越是不起眼的传统行业,越需要一套真正能用、能落地的管理系统。船舶维保就是典型场景,主机、辅机、舵机、锚机、消防设备一堆,每一台都关系到航行安全,维护记录又长期靠纸质台账和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,查询条件统一带上deleted=0。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层不直接出现SQL,Service内部不直接写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头,保证所有业务接口自动带上凭证。响应拦截:统一检查后端返回的code,code为200时直接返回data给页面,其他code统一弹错误提示并中断Promise;HTTP状态码为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.0,5.7版本也兼容。如果本机环境有冲突,建议直接用Docker装MySQL,避免卸载重装。安装完MySQL需要确认两件事:root账号能本地登录、字符集是utf8mb4。遇到过很多次装完数据库默认字符集是latin1,中文数据写入后变成乱码,启动项目前就要把字符集设置好,远比事后刷数据省事。5.2 数据库初始化和后端配置第一步创建数据库。连接上MySQL后用CREATE DATABASE ship_maint DEFAULT CHARACTER SET utf8mb4,再导入源码目录下sql目录里的初始化脚本。脚本里同时包含了表结构和初始数据,第一版跑起来后不用再去手动建表。第二步改后端配置文件。重点是application.yml里的数据库连接信息,把url、username、password改成自己本机的。连接串里一定要带serverTimezone=Asia/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连接串缺时区参数加上serverTimezone=Asia/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脚本和枚举类,看懂业务约定,再去看页面功能会事半功倍。
在工业信息化项目里摸爬滚打这几年,我最大的感受是:越是不起眼的传统行业,越需要一套真正能用、能落地的管理系统。船舶维保就是典型场景,主机、辅机、舵机、锚机、消防设备一堆,每一台都关系到航行安全,维护记录又长期靠纸质台账和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,查询条件统一带上deleted=0。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层不直接出现SQL,Service内部不直接写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头,保证所有业务接口自动带上凭证。响应拦截:统一检查后端返回的code,code为200时直接返回data给页面,其他code统一弹错误提示并中断Promise;HTTP状态码为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.0,5.7版本也兼容。如果本机环境有冲突,建议直接用Docker装MySQL,避免卸载重装。安装完MySQL需要确认两件事:root账号能本地登录、字符集是utf8mb4。遇到过很多次装完数据库默认字符集是latin1,中文数据写入后变成乱码,启动项目前就要把字符集设置好,远比事后刷数据省事。5.2 数据库初始化和后端配置第一步创建数据库。连接上MySQL后用CREATE DATABASE ship_maint DEFAULT CHARACTER SET utf8mb4,再导入源码目录下sql目录里的初始化脚本。脚本里同时包含了表结构和初始数据,第一版跑起来后不用再去手动建表。第二步改后端配置文件。重点是application.yml里的数据库连接信息,把url、username、password改成自己本机的。连接串里一定要带serverTimezone=Asia/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连接串缺时区参数加上serverTimezone=Asia/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脚本和枚举类,看懂业务约定,再去看页面功能会事半功倍。