资讯详情

资讯详情

船舶维保管理系统实战:SpringBoot+Vue3+MyBatis+MySQL全栈开发

船舶维保管理系统在很多外人看来好像就是一个“设备维修记录软件”。真做过船舶行业的人会告诉你恰恰相反。船舶的日常管理更看重“计划保养”而不是“坏了再修”。船级社检查、公司体系审核都在逼着每一艘船把柴油机保养、滤器清洁、救生设备检查这些工作做成“到点必须做”的闭环。用Excel管几个月后就开始乱这个月该做的项目有没有做上次换备件是什么时候设备台账是不是又忘了维护谁都说不清。所以做船舶信息化时维保管理往往是第一套要上的系统。它的业务边界非常清楚又足够锻炼前后端能力是一个适合团队磨合的典型全栈项目。这里要说的技术选型是Java SpringBoot Vue3 MyBatis MySQL前后端分离后端提供REST接口前端用Vite构建数据库落MySQL。不管你是准备切入船舶行业的开发人员还是想用一个真实业务场景来学全栈的初学者这套源码和设计思路都值得从头到尾拆一遍。1. 船舶维保系统中“计划”比“维修”更核心1.1 设备台账是计划体系的“底座”如果说一艘船的维保体系是一棵树设备台账就是树根。系统里的设备不能简单做成一张“设备名称规格”的扁平列表它一定是有层级的全船 - 机舱 - 主机系统 - 柴油机 - 高压油泵……后端如果用parent_id也要准备递归查询或物化路径字段。我在这套方案里看到的做法是device表中同时保留parent_id和path_codepath_code存类似“01.02.03.04”的编码查询子树时用like 01.02.03.%效果远优于逐层递归。字段也要在开始就定义清楚避免后期返工。设备名称、设备编码、所属系统、安装位置、型号、规格参数、制造商、安装日期、质保截止、启用状态、维护周期类型、周期值这些字段会一直延伸到维保计划、工单和报表里。特别是在船舶场景中同类设备数量很大例如全船可能有几十台泵如果设备编码不规范后面做备件对应和统计时就会非常痛苦。1.2 计划生成、提醒和工单闭环计划保养也就是常说的PMS体系这里最容易做错的地方是把“计划”写死成一张静态表每个月人工去复制新任务。正确做法是维护一个“计划模板表”和一个“任务流水表”。模板表记录某个设备需要按什么周期做哪类保养任务流水表记录某个时间点实际生成的一次维保任务。为什么必须分两张表因为同一台设备的保养周期可能调整历史任务却不能被改写否则连台账审计都做不了。生成逻辑不复杂但要考虑“日历周期”和“运行小时周期”两个维度。日历周期按上次完成日期加周期天数运行小时周期要读取设备运行时长这类数据在船舶端往往需要从监控系统或人工抄表接口进入系统。定时任务扫描“在提醒窗口内的设备”然后批量生成task记录同时更新计划模板里的last_plan_date这才算一次完整闭环。很多团队做开发时容易一上来就猛写前后端代码结果业务上少了一个“计划生成”概念最后做出来一个只能手动记录维修单的工具。实际上真正值钱的是那套自动生成、自动提醒、自动统计的逻辑。后面的大数据模型和后端实现都是围绕这个核心来设计的。2. 数据模型先行拆分船舶、设备与维保工单在开始码代码之前先画清楚核心表。如果做过几个管理系统的人很容易把“设备表”看得太轻把“工单表”看得太重。实际上在船舶维保场景里一条工单背后必然关联着设备、维保计划、执行人、备件和验收记录。如果刚开始建表时只用一个device_id去关联后面所有报表都会变成噩梦。2.1 核心表的字段设计与关联最基础的表包括船舶表、设备表、计划模板表、维保任务表、工单表。每张表都建议加公共字段id、create_time、update_time、deleted、version。以下是我整理过多次的字段布局可以直接参考表名核心字段说明shipid, ship_name, ship_no, imo_no, ship_type, build_date, status船舶基础资料deviceid, ship_id, device_code, device_name, parent_id, path_code, spec, cycle_type, cycle_value, last_maintenance_date, next_maintenance_date, status设备台账带层级maintenance_planid, device_id, plan_name, cycle_type, cycle_value, lead_days, enabled, last_task_time计划模板可启停maintenance_taskid, plan_id, device_id, ship_id, task_name, planned_date, status, executor_id, finished_time, result每次生成的维保任务work_orderid, task_id, order_no, priority, order_type, assignee_id, plan_time, finish_time, status, remark工单执行与验收关键在device表和task表。task表冗余ship_id、device_id、device_name、plan_name是为了报表减少join。有些同事觉得冗余会导致数据不一致但在任务流水这个场景里我更愿意把它看成“快照”。设备改名后历史任务保留旧名称反而符合审计习惯不用纠结。唯一要注意的是后续未完成的任务如果也要展示新名称可以单独写同步逻辑只更新状态未完成的任务。2.2 状态字段与逻辑删除的取舍管理系统审计要求高物理删除设备可能会破坏历史工单因此device表建议保留deleted字段工单流水则不要物理删只用状态取消。基础资料类数据做逻辑删除流水类数据只允许状态迁移这是一个简单又稳妥的约定。这里有一个我在好几次项目里都踩过的坑逻辑删除字段和数据库唯一索引会打架。比如device表想对device_code加唯一索引逻辑删除后又重新录入同一编码第二次插入会因为已删除的记录还在表中而直接报错。解决思路是要么允许device_code重复由业务层去校验要么在唯一索引里包含deleted字段但这里仍然不适合用0和1做逻辑删除因为在MySQL里多个NULL是允许的可以用deleted_at时间戳代替布尔值删除时写入时间未删除为NULL这样(device_code, deleted_at)就能组成一个比较可靠的历史唯一索引。2.3 为什么任务表要冗余设备、船舶名称做一个设计决定时我总是问自己这个冗余是“可管理”的吗如果只是拷贝字段、不更新会造成脏数据。更好的做法是给task表加ship_name、device_name、plan_name的快照字段并在任务生成的那一刻写入因为业务流程上维保任务是一条不能回写的流水。这类字段在统计表里会大量用到比如“某条船某年完成了多少次主机保养”直接查task表即可不需要链式关联device和ship。实际系统里最常用的页面是“维保日历”按船舶、部门、月份展示任务配合筛选器和分页。如果数据量大建议在task表上加组合索引ship_id planned_date status。索引设计时不能把每个字段都加索引要靠慢SQL日志针对列表页真实查询进行优化。刚开始数据量几千条时无所谓一旦跑上三五年任务表百万级记录是常态这条组合索引能决定页面响应时间是从1秒变成100毫秒还是从1秒变成10秒。3. 后端SpringBoot分层与MyBatis动态SQL的取舍3.1 包结构controller/service/mapper 还是按业务域拆分这套源码采用了经典的按技术层分包我也比较推荐。控制器只做参数接收与响应封装不写任何业务逻辑service层负责业务规则mapper层保持“一个方法对应一段SQL”。有人喜欢按业务域分包但中小型管理系统按层分包更容易上手新人进来半天就能跟上节奏。公共代码放在common里Result类、PageResult类、全局异常处理、JWT工具、登录拦截器、统一日志切面、操作审计。操作审计在维保系统里比较重要谁在什么时间改动了工单状态责任要能追踪。可以用一张operation_log表在Controller方法上做一个Log注解通过Spring AOP把方法名、参数、操作人、操作时间记录下来。3.2 手写XML还是用MyBatis-PlusMyBatis和MyBatis-Plus并不冲突。基础单表CRUD用MyBatis-Plus的BaseMapper多表关联、复杂动态查询用XML这是目前效率较高的组合。需要注意MyBatis-Plus在遇到“逻辑删除”字段和“唯一索引”时容易产生预料不到的SQL拼接自己写的XML反而更可控。写条件查询的时候以前习惯用where 11后来发现会让SQL看起来有点脏优化器处理起来也谈不上优美。更推荐用where标签SELECT t.id, t.task_name, t.planned_date, t.status, t.device_name, t.ship_name FROM maintenance_task t INNER JOIN ship s ON t.ship_id s.id AND s.deleted 0 where if testshipId ! null AND t.ship_id #{shipId} /if if testdeviceName ! null and deviceName ! AND t.device_name LIKE CONCAT(%, #{deviceName}, %) /if if teststartDate ! null AND t.planned_date gt; #{startDate} /if if testendDate ! null AND t.planned_date lt; #{endDate} /if /where ORDER BY t.planned_date DESC LIMIT #{offset}, #{pageSize}这里有两个细节。第一个LIKE参数建议在应用层拼接好目标名称或者用CONCAT拼接再传入#{...}参数避免用户输入单引号时出现SQL注入。第二个分页直接用LIMIT如果使用了LIMIT #{pageSize} OFFSET #{offset}在数据量大时性能会随offset增大而退化可以考虑换成游标分页或者记录上一页最大值不过一般维保系统任务列表按日期排序数据增长符合业务规律普通分页问题不大。MyBatis-Plus分页插件生成的count查询在复杂SQL下可能不准确特别是带GROUP BY时必要时自己写统计SQL。比如设备维保次数统计如果直接用分页插件生成count很可能统计的是分组后的行数逻辑上没问题但需要明确自己要的是“总组数”还是“总记录数”。3.3 批量生成维保任务时如何防止重复和错乱假设每天凌晨2点系统执行一次计划任务扫描逻辑如下查询所有enabled1的计划模板对每个模板根据cycle_type计算下次到期日若next_due_date小于等于今天加提前量则生成任务更新模板的last_task_time和next_due_date这段逻辑如果直接在service方法里写一个for循环并在类上加Transactional那么整批任务都会打包在一个大事务里。一旦中间某台设备生成失败整个批次回滚虽然数据安全但用户体验很差。更好的方案是外层扫描不加事务单设备生成时单独开启事务就算某台设备数据处理失败也只影响这一台同时把失败记录写到task_gen_log。关于防止重复不能只靠代码里的if判断。如果后端做了多实例部署两个节点可能同时扫到同一条计划。小型系统用数据库行锁最简单SELECT * FROM maintenance_plan WHERE id #{id} FOR UPDATE查询后在同一事务里生成任务、更新模板时间锁在数据库层面即使多个节点同时执行也不会重复生成。比引入Redis分布式锁要少维护一套中间件适合中小系统。运行小时周期会稍微特殊一点它要读设备当前运行小时如果船舶端还没接入自动化采集就要开发人工录入运行小时的功能再把“当前小时减去上次维保小时大于等于周期小时”作为生成条件。4. Vue3端实现从登录拦截到维保看板4.1 项目初始化与技术选型前端部分Vue3 Vite Pinia Vue Router Element Plus是当前比较成熟的组合。Vite的构建速度比Webpack快很多对开发体验提升明显。源码里还用了图标库做菜单图标axios做请求封装dayjs做日期处理。项目骨架可以拆成这样src/api按模块拆分接口定义src/router路由文件与守卫src/storePinia状态用户信息、token、菜单权限src/views页面src/components业务组件src/utils请求实例、公共方法src/directives自定义指令比如按钮权限前后端联调成本大部分来自接口不规范。建议一开始就约定统一响应结构{ code: 200, msg: success, data: { } }再把axios封装成const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }); 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.msg || 请求失败); return Promise.reject(new Error(res.msg)); } return res; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } ElMessage.error(网络异常); return Promise.reject(error); } );这个封装简单但够用。登录后把用户角色信息放到Pinia前端根据角色显示或隐藏左侧菜单里的“系统管理”“审核管理”等模块。按钮级别的权限可以用自定义指令v-permission后端把权限码返给前端指令判断没有权限就直接移除按钮DOM。这里想提醒一点权限的核心一定在后端。前端隐藏按钮只是用户体验接口能不能被调用必须靠后端再次校验。尤其维保系统里有不少审核类的接口把“拒绝越权操作”的职责放在后端是基本底线。4.2 列表页、表单页与动态搜索的处理方式维保系统里全是列表设备列表、计划列表、任务列表、工单列表。如果每个页面都重新写一遍“搜索栏表格分页”工作量很大。比较常见的做法是封装一个BaseList页面模板用组件插槽来填搜索条件和操作列然后再用代码生成器从数据库表生成一套前后端CRUD页面。如果团队还没有代码生成器我建议至少把搜索和分页逻辑抽成Composable减少重复代码。页面实现有几个容易出问题的地方搜索条件字段与后端DTO字段必须保持一致命名错一个联调就得排查半天日期范围组件一般绑定为[startDate, endDate]数组提交时拆成两个参数字段表格里的状态列用tag展示后端不要返回中文状态值返回code前端自己去映射分页数据从后端PageResult取变量名统一定成total、pages、records还有一个我在实践中的偏好列表查询接口统一用POST请求body传复杂查询条件返回PageResult。虽然GET语义更符合REST但实际项目里多条件查询时GET很容易把URL拼得又长又乱遇到特殊字符还要encode。POST显得更省事。4.3 联调中最容易出问题的几个点联调阶段前后端最容易出现分歧的地方就是日期格式。后端返回时间如果是一个LocalDateTime且没配置Jackson格式前端会拿到一串带T的ISO字符串显示时还得转换。建议后端统一在配置里设置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样前端可以无脑展示字符串。但要注意如果字段类型是LocalDateJackson的date-format对LocalDate并不生效需要单独加JsonFormat或者用全局配置处理。实际开发中我对所有LocalDate和LocalDateTime字段都用JsonFormat明确标注避免不同版本Spring Boot行为不一致。第二个麻烦是文件上传接口。船舶维保工单经常要上传维修照片、验收单。上传接口一般返回文件的ID或URL建议把文件表单独抽出来包括original_name、file_path、file_size、uploader、upload_time。前端上传成功后把返回的id存到工单附件关联表这样避免一张表放太多业务字段也方便以后接对象存储。第三个是大列表性能。几万条维保任务全部加载进前端肯定卡。前后端都做分页不要用前端表格自带的分页去处理全量数据。搜索条件一旦变化就重新请求第一页。有些同事为了让操作“流畅”一次性把一年的任务拉到前端再分页季度报表还能忍时间长了内存必定爆。5. 部署上线前必须处理的几个坑5.1 MySQL连接参数、字符集与时区问题项目使用MySQL第一个坑就是时区。云上MySQL 8的默认时区经常是UTC后端连上去做日期计算时结果可能直接差8小时。JDBC URL建议这样写jdbc:mysql://localhost:3306/ship_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltruerewriteBatchedStatementstrue字符集在MySQL 8里默认utf8mb4基本够用但还是建议建库时明确指定CREATE DATABASE ship_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二个坑是rewriteBatchedStatementstrue这个参数。如果后续要批量插入任务流水没有它MyBatis的批量插入本质上是一条一条执行性能和逐条insert没有区别。不要一遇到批量插入慢就怀疑MyBatis先检查连接参数。5.2 Nginx部署与前端刷新404前后端分离项目部署时有几个注意点。前端打包成dist放到nginx的html目录后端jar单独跑在某个端口。nginx配置最关键的两段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; } location / { try_files $uri $uri/ /index.html; }try_files这段的作用是处理Vue Router在history模式下刷新404。漏掉它进入系统后按F5就可能直接白屏。另外proxy_pass后面的URI是否带结尾斜杠会影响最终请求路径的拼接这里要特别小心。例如proxy_pass http://127.0.0.1:8080;不带斜杠访问/api/ship/page时后端收到的是/api/ship/page如果带了斜杠/api前缀可能被吃掉。生产环境里这属于“配置了一次就忘”的坑最好部署后用真实请求验证一遍。开发环境下前端用Vite代理可以但不建议在生产环境开CORS去解决跨域直接用nginx反向代理最干净。如果非要用CORS后端要处理allowedOriginPatterns允许携带cookie时更要谨慎。5.3 定时任务重复执行与任务丢失计划任务用Spring的Scheduled部署在单节点没啥问题。但如果后端做了多副本部署必须防止所有节点同时生成任务。推荐用一个最简单的数据库表scheduler_lock节点执行前获取某个key的行锁执行完释放。另一种做法是把定时任务单独抽成一个服务只在那一台上启动Scheduled。对于船舶维保系统这种规模数据库行锁方案已经够用不需要引入复杂中间件。任务丢失的问题如果定时任务执行到一半进程重启已经生成的任务和更新计划模板处在同一步里很容易出现“任务生成了但下次执行日期没更新”或者相反的情况。设计上要把“生成动作”和“更新next_due_date”放到同一个事务如果进程中断未提交的回滚第二天扫描时还能重新生成。另外每次定时任务都建议写日志表记录开始时间、结束时间、生成条数、失败条数。线上查问题时比翻应用日志直观得多。5.4 权限与数据隔离的边界如果系统只是单公司单船队使用权限模型可以做得轻一些但仍然建议保留用户、角色、菜单三张表和关联表不要省。后面如果扩展多船队设备数据要按船队隔离时至少权限骨架不用重构。船舶维保系统的敏感点在于工单可能包含船舶安全检查内容一些整改项不能随便让船端人员删除。前端隐藏按钮只是交互设计后端接口必须做权限校验。接口测试时把修改、审核、启用禁用这些危险接口单独过一遍权限测试不登录直接调接口应该返回401或403而不是放过。6. 这套源码的二次开发建议6.1 哪些部分可以当模板直接复用拆开看最容易复用的是通用部分用户登录、JWT鉴权、角色菜单、字典管理、文件上传、操作日志、代码生成。这类模块和业务无关搬到哪里都能用。第二类复用是“计划任务工单闭环”的模式不只是船舶工厂设备维保、物业巡检、电梯维保、环保设备维护几乎都是同一套业务逻辑。如果是从这套源码开始做我建议第一次完全不做修改先把登录、设备台账、维保计划、任务生成、工单处理这五个流程跑通。跑通后再考虑改字段。有些人一上来就要加字段加模块结果连核心流程都没走通过最后所有问题混在一起很难定位。6.2 如何扩展审批与流程中小型系统上完整的流程引擎成本其实很高。除非业务确实需要在流程中途任意跳转、退回、会签否则在工单表加一个status字段用状态机来控制最灵活。状态可以设计成等待审核、待执行、执行中、已完成、已取消。再用一张audit_record表记录审核动作。状态机要注意状态不是随便跳的。可以把“允许状态迁移”配成一个Map比如“待审核”只能跳到“待执行”或“已取消”“待执行”只能跳到“执行中”。对船舶维保来说最重要的审批点是维保任务完成后的验收建议做两级审核工程主管初审、部门经理终审审核通过后工单才算闭环同时把设备的下次维保日期刷新。6.3 与船岸一体化或船舶监控系统对接的方向船舶信息系统大多不会只有维保这一块。后续会有备件管理、油料管理、航次计划、船员考勤等系统。对接时后端给外部系统提供接口时不要直接暴露内部Mapper而是单独写一层Open API在接口层做鉴权和限流。如果船上网络不稳定还要考虑离线优先模式船端把维保数据缓存在本地靠岸后再同步到岸端。这时候系统设计要从“前后端分离”升级成“多端离线队列”对架构的影响更大但核心的维保业务表结构可以保留下来。如果只做一个单体系统这套SpringBootVue3MyBatisMySQL的方案完全够用前后端分离的边界也让后续扩展空间更大。代码不是越复杂越好能稳定跑上三五年、业务数据不丢失不混乱才是生产系统真正该追求的东西。最后再给一个实在建议如果你只是学习全栈项目别一上来就把注意力放在“我要把船舶维保系统做得像完整工业软件”。把上面几个核心模块实现完再加上部署就已经覆盖了企业级管理系统的大部分难点。做的时候尽量按真实业务来先定义设备台账再定义计划模板再跑一次任务生成和工单闭环。所有框架都是工具真正有价值的是对业务闭环的理解。这些经验我在多个项目里反复验证过一开始容易被炫技技术点吸引但最后能留下来的往往是最朴素的那套结构。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →