船舶维保系统实战:SpringBoot+Vue3前后端分离实现与部署

没想到这个项目标题能被这么多人搜到,看来船舶维保系统的需求确实不小。我之前跟几个做船舶信息化项目的朋友聊过,大家普遍的感受是:行业里的老系统大多是 C/S 架构的单机版,操作繁琐、数据孤岛严重,而市面上能直接落地的 B/S 架构维保系统源码并不算多。正好借这个机会,把我自己参与过的一个模拟项目“某船舶维保管理系统”的完整实现经验拆开揉碎了讲一讲。这篇文章没有废话,直接上干货,从需求拆解到数据库设计、后端接口、前端页面、权限控制,再到部署避坑,一条线捋下来。

如果你正准备做船舶相关的毕设,或者公司内部想搭一套设备运维管理平台,又或者只是单纯想学 SpringBoot + Vue3 前后端分离项目的完整落地套路,这篇内容都适合你。我尽量用“做过的人”的口吻,把那些文档里不写、但实际开发中一定会踩的坑和细节一次性讲清楚。

1. 项目整体设计与技术选型思路

1.1 船舶维保系统的核心业务场景

先别急着写代码,搞懂业务才是第一步。船舶维保管理系统听起来高大上,但落到实际使用场景,核心其实就四件事:管船、管养、管修、管件。

  • 管船:需要维护每艘船的基本档案,包括船名、船舶类型(散货船、集装箱船、油轮等)、吨位、建造日期、主机型号、当前状态(在航、检修、停航等)。
  • 管养:也就是计划性维护(Planned Maintenance)。船舶设备需要按照运行小时数或日历周期进行保养,比如“主机每运行 2000 小时更换滑油滤器”、“救生设备每月检查一次”。系统要能自动生成保养计划并提醒。
  • 管修:设备真正出故障了,要发起维修工单,记录故障现象、维修内容、更换配件、工时和费用,形成完整的维修履历。
  • 管件:备品备件管理。维修时需要消耗配件,完了要扣减库存;库存不够时要触发采购提醒;采购入库后更新库存。

围绕这四个场景,系统可以拆分为几个核心模块:船舶档案模块、保养计划模块、维修工单模块、备件库存模块、统计分析模块、系统管理模块(用户、角色、菜单、字典)。

1.2 技术栈选型:为什么是 SpringBoot + Vue3 + MyBatis?

这个组合在当下属于非常主流的“Java 全栈开发黄金组合”,选择它不只是因为流行,更是因为它恰好覆盖了这套系统的所有需求点。

我先说后端 SpringBoot。SpringBoot 最大的价值在于“约定优于配置”,它能让你以极快的速度搭建一个可运行的 Web 服务,相比于传统的 SSM 组合省掉了大量繁琐的 XML 配置。在船舶维保这种业务逻辑不算极其复杂、但涉及模块较多的管理系统中,SpringBoot 的自动装配特性和丰富的 Starters 生态系统能大幅缩短开发周期。而且 Java 生态稳定,适合船舶这种要求可靠性较高的工业管理场景。

再看看前端 Vue3。Vue3 的组合式 API(Composition API)和 TypeScript 支持,让组件逻辑复用变得非常优雅。维保系统的界面有大量表格、表单、弹窗、统计图表,Vue3 的响应式系统配合 Element Plus 组件库,可以快速构建出质感不错的业务界面。加上 Vite 构建速度极快,开发体验很舒服。

至于 MyBatis,虽然 JPA 也很流行,但 MyBatis 在复杂查询优化和 SQL 精细控制方面更有优势。维保系统的报表统计模块往往涉及多表联查、条件动态拼接,用 MyBatis 的 XML 映射文件可以写得很直观,方便后期 DBA 优化慢查询。而且国内企业项目对 MyBatis 的接受度极高,你几乎不用为“招来的人会不会用”发愁。

最后是前后端分离架构。维保系统这种项目,天然适合前后端分离。前端部署在 Nginx,后端打包成 Jar 独立部署,两者通过 RESTful API 通信。这样开发时前后端可以并行推进(我用的是前端 Mock 数据先行,后端接口对接的模式),后期维护和升级也更灵活。MySQL 作为数据库选型,对于这种量级的管理系统完全足够,配合 MySQL Workbench 或者 Navicat 管理数据都很方便。

1.3 功能模块划分与权限模型设计

一个合格的维保系统必须有清晰的功能权限和数据权限控制。

  • 功能权限:用经典的 RBAC(基于角色的访问控制)模型,用户 -> 角色 -> 菜单权限。比如“驾驶员”角色只能查看自己船的维保记录;“机务主管”可以创建保养计划和维修工单;“备件管理员”只能操作备件出入库;“系统管理员”则拥有全部权限。
  • 数据权限:船舶维保有个特点,不同角色看到的船舶范围不同。比如一个机务主管可能只管某几条船,超级管理员可以看全部。这里我采用了比较灵活的方式:给系统管理模块增加了“数据范围”字段(全部数据、本部门数据、自定义数据),用 AOP 拦截 SQL 实现行级数据隔离。虽然实现起来稍显复杂,但这是系统能真实落地到多部门协作场景的关键。

核心权限数据表设计如下:

表名说明核心字段
sys_user用户表user_id, username, password, dept_id, status
sys_role角色表role_id, role_name, role_key, data_scope
sys_menu菜单权限表menu_id, parent_id, menu_name, perms, path, component
sys_user_role用户角色关联表user_id, role_id
sys_role_menu角色菜单关联表role_id, menu_id
sys_dept部门表dept_id, parent_id, dept_name

这个表结构是经典的 RBAC 通用设计,处理维保系统的“用户管理 -> 船舶档案 -> 维保业务”三层权限关系非常顺手。

2. 数据库设计与关键业务规则沉淀

2.1 船舶维保业务的核心数据表结构

这张表结构是整个系统的地基,我先讲最核心的几张业务表。

船舶档案表(ship_info)

CREATE TABLE ship_info ( ship_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '船舶ID', ship_name VARCHAR(64) NOT NULL COMMENT '船名', ship_type VARCHAR(32) COMMENT '船舶类型:散货船/集装箱船/油轮/拖轮等', imo_number VARCHAR(32) COMMENT 'IMO编号', gross_tonnage DECIMAL(10,2) COMMENT '总吨位', main_engine_model VARCHAR(64) COMMENT '主机型号', build_date DATE COMMENT '建造日期', current_status VARCHAR(16) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE在航/MAINTENANCE检修/STOP停航', dept_id BIGINT COMMENT '所属部门(管理该船的部门)', create_time DATETIME, update_time DATETIME, del_flag TINYINT DEFAULT 0 ) ENGINE=InnoDB COMMENT='船舶档案表';

保养计划表(maintenance_plan)

CREATE TABLE maintenance_plan ( plan_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '计划ID', ship_id BIGINT NOT NULL COMMENT '船舶ID', equipment_name VARCHAR(100) COMMENT '设备名称', plan_type VARCHAR(16) COMMENT '计划类型:CALENDAR按日历周期/HOURS按运行小时', cycle_value INT COMMENT '周期数值,如30、90、2000', cycle_unit VARCHAR(8) COMMENT '周期单位:DAY天/HOUR小时', last_maintenance_date DATE COMMENT '上次保养日期', last_maintenance_hours DECIMAL(10,2) COMMENT '上次保养时运行小时数', next_maintenance_date DATE COMMENT '下次保养日期(自动计算)', assignee VARCHAR(32) COMMENT '负责人', status VARCHAR(16) DEFAULT 'PENDING' COMMENT '状态:PENDING待执行/COMPLETED已完成/OVERDUE已逾期', remark VARCHAR(500), create_time DATETIME, update_time DATETIME ) ENGINE=InnoDB COMMENT='保养计划表';

维修工单表(repair_order)

CREATE TABLE repair_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '工单ID', order_no VARCHAR(32) NOT NULL COMMENT '工单编号,如RO20250115001', ship_id BIGINT NOT NULL COMMENT '船舶ID', equipment_name VARCHAR(100) COMMENT '故障设备', fault_description TEXT COMMENT '故障描述', fault_level VARCHAR(8) DEFAULT 'GENERAL' COMMENT '故障级别:GENERAL/URGENT/CRITICAL', repair_content TEXT COMMENT '维修内容', repair_begin_time DATETIME, repair_end_time DATETIME, repair_cost DECIMAL(12,2) DEFAULT 0 COMMENT '维修费用', parts_cost DECIMAL(12,2) DEFAULT 0 COMMENT '备件费用', labor_hours INT DEFAULT 0 COMMENT '工时', status VARCHAR(16) DEFAULT 'PENDING' COMMENT '状态:PENDING待派工/REPAIRING维修中/COMPLETED已完成/VERIFIED已验收', handler VARCHAR(32) COMMENT '维修人员', create_time DATETIME, update_time DATETIME ) ENGINE=InnoDB COMMENT='维修工单表';

备件库存表(spare_part)与出入库记录表(spare_part_stock)

CREATE TABLE spare_part ( part_id BIGINT PRIMARY KEY AUTO_INCREMENT, part_code VARCHAR(32) NOT NULL COMMENT '备件编码', part_name VARCHAR(100) NOT NULL COMMENT '备件名称', specification VARCHAR(100) COMMENT '规格型号', unit VARCHAR(8) COMMENT '单位:个/套/箱', safety_stock INT DEFAULT 0 COMMENT '安全库存', current_stock INT DEFAULT 0 COMMENT '当前库存', location VARCHAR(64) COMMENT '存放位置', update_time DATETIME ) ENGINE=InnoDB COMMENT='备件表'; CREATE TABLE spare_part_stock ( stock_id BIGINT PRIMARY KEY AUTO_INCREMENT, part_id BIGINT NOT NULL, change_type VARCHAR(8) COMMENT 'IN入库/OUT出库', quantity INT NOT NULL COMMENT '数量', related_order_no VARCHAR(32) COMMENT '关联单据号(工单号/采购单号)', operator VARCHAR(32) COMMENT '操作人', create_time DATETIME ) ENGINE=InnoDB COMMENT='备件出入库记录表';

这张表设计里有几个细节非常关键:

  1. 保养计划表中的“运行小时”逻辑:船舶主机和发电机的保养周期往往不是按日历天算,而是按运行小时算(比如主机每运行 2000 小时需换机油)。这块我不仅存了 last_maintenance_hours,还单独在船舶表关联了当前累积运行小时数,由系统自动比对。如果只做日历提醒,在真实业务中是要被机务主管吐槽的。
  2. 维修工单的金额拆分:维修成本分为维修费和备件费,背后关联备件出库记录的订单号,这样财务结算时可以追溯每笔钱花在哪里。工单流转时状态必须严格按“待派工 -> 维修中 -> 已完成 -> 已验收”的顺序,每一步的操作人、操作时间都应该记录在日志表里。
  3. 软删除字段:所有业务表都加了 del_flag 字段,做逻辑删除而不是物理删除。船舶维修记录是重要台账,物理删除了未来审计要吃大亏。

2.2 维保到期自动计算:基于日历周期与运行小时双模式

这是整个系统业务逻辑最核心的点。实现思路是:定时任务每天凌晨跑一次,遍历所有保养计划,自动判断并更新状态。

日历周期模式的计算公式很简单:

下次保养日期 = 上次保养日期 + 周期数值(天)

运行小时模式的公式稍微复杂:

预计下次保养日期 = 上次保养日期 + (周期数值 - (当前累积运行小时数 - 上次保养时运行小时数)) / 日均运行小时数

这里日均运行小时数是一个经验值,比如主机日均运行 18 小时,如果没有接入实时轮机数据,就用这个估算值算出“预计日期”,在界面上显示“预计剩余 XX 天”。如果累计运行小时数已经达到周期数值,就直接触发逾期提醒。

定时任务我用的是 Spring 自带的 @Scheduled 注解,每分钟跑一次“检查并更新维护计划”的任务。判断逻辑大概是:

// 伪代码逻辑 public void checkMaintenancePlan() { List<MaintenancePlan> plans = maintenancePlanMapper.selectAllActivePlans(); Date today = new Date(); for (MaintenancePlan plan : plans) { if (plan.getPlanType().equals("CALENDAR")) { // 日历周期判断 Date nextDate = DateUtil.offsetDay(plan.getLastMaintenanceDate(), plan.getCycleValue()); if (today.after(nextDate) && plan.getStatus().equals("PENDING")) { // 更新状态为逾期 OVERDUE,同时给负责人发站内消息 plan.setStatus("OVERDUE"); // 推送到消息中心 } } else { // 运行小时判断 BigDecimal shipHours = shipMapper.selectRunHours(plan.getShipId()); BigDecimal totalNeed = plan.getLastMaintenanceHours().add(plan.getCycleValue()); if (shipHours.compareTo(totalNeed) >= 0) { plan.setStatus("OVERDUE"); } } } }

在真实项目中,定时任务不要只依赖 @Scheduled,如果系统做了集群部署,多台机器同时跑会重复处理。我当时是加了一个基于数据库的分布式锁表,或者引入 XXL-Job 这类分布式调度中间件,保证同一时刻只有一个节点在干活。

2.3 备用配件库存扣减与安全库存预警

维修工单完成时,系统要支持“关联备件出库”。这块很容易做成一锤子买卖:工单填个备件费用就完了,库存不扣。实战中我设计如下流程:

  1. 工单进入“已完成”状态前,必须经过“引用备件”步骤。前端弹出备件选择框,选择备件编号和数量。
  2. 后端收到请求后,先查询当前库存,库存充足才能扣减:当前库存减去出库数量。
  3. 扣减后判断当前库存是否小于安全库存,如果小于,自动生成一条补货预警消息推送给备件管理员。
  4. 同时向 spare_part_stock 表插入一条出库记录,关联工单号。

这里有个非常容易踩坑的点是并发扣减导致库存变负数。解决思路很标准:扣减库存的 SQL 必须使用乐观锁或条件更新,比如执行:

UPDATE spare_part SET current_stock = current_stock - #{quantity}, update_time = NOW() WHERE part_id = #{partId} AND current_stock >= #{quantity}

如果返回的影响行数为 0,说明库存不足,直接抛出业务异常提示“库存不足”。这种原子性更新比“先查询再判断再更新”安全得多,虽然看起来只是多了一个 where 条件,但这就是老手和新手写库存系统的差别。

3. 后端核心接口实现与业务逻辑落地

3.1 工程结构规划与通用返回体设计

后端工程我按经典的分层架构组织:Controller -> Service -> Mapper,清晰可维护。

com.ship.maintenance ├─ common # 通用类(返回体、异常处理、常量、工具类) ├─ config # 配置类(MyBatis、安全配置、跨域配置、定时任务) ├─ controller # 接口层 ├─ service # 业务逻辑层 ├─ mapper # 数据访问层 ├─ entity # 数据库实体类 ├─ dto # 数据传输对象(VO、BO) ├─ security # 认证授权相关 └─ task # 定时任务

通用的返回体设计是整个系统前后端沟通的“协议”。我自定义了一个 Result 类,统一返回格式为:

{ "code": 200, "message": "操作成功", "data": {} }
  • code 为 200 表示成功,401 表示未登录,403 表示无权限,500 表示服务器异常。
  • 所有 Controller 的返回值都是 Result ,成功时调用 Result.success(data),失败时抛出自定义业务异常 BusinessException,由全局异常处理器统一捕获并包装成上述结构返回。

这个设计避免了每个接口各写一套返回格式的尴尬,也方便了前端 axios 响应拦截器统一处理错误码,比如 401 时自动跳转登录页,403 时弹出“无权限”提示。

3.2 基于 JWT 的登录认证与接口拦截器

船舶维保系统的登录认证我用的是 JWT(JSON Web Token)+ Spring Security 的组合方案。JWT 的好处是无状态、便于水平扩展,适合前后端分离架构。登录成功后,后端签发一个 token 返回给前端,前端存储在 localStorage 中,每次请求通过 Header 的 Authorization 字段携带。

核心逻辑:

// 登录成功后生成token String token = JwtUtil.createToken(userId, username); // Spring Security 配置核心过滤器链 http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/login", "/api/auth/captcha").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

这里要注意几个实操细节:

  • 密码存储:使用 BCryptPasswordEncoder 加密,数据库中不要存明文。BCrypt 自带盐值,同样的密码每次加密结果不同,安全性远高于 MD5。
  • JWT 密钥管理:密钥不要硬编码在代码中,要放到 application.yml 配置里,并使用环境变量注入,防止泄露。
  • Token 过期策略:登录态一般设置 12 小时过期。如果用户长期在线,前端需要在 401 时自动调用刷新接口重新签发 token,不然做一半就被强制下线,体验很糟糕。
  • 继承权限注解:在 Controller 方法上用 @PreAuthorize("hasAuthority('maintenance:plan:add')") 做接口粒度权限控制,前端按钮通过 v-permission 指令控制显隐,前后端双重校验。

3.3 典型业务接口实现:保养计划 CRUD 与工单状态机流转

我挑两个最具代表性的接口展开讲。

保养计划创建。前端传 shipId、设备名称、周期数值、周期单位。后端除了做常规的字段校验,还要做业务校验:该船舶同一设备是否已存在活跃的保养计划,避免重复创建。

维修工单状态流转是这套系统状态设计最复杂的部分,直接用硬编码字符串去 if-else 是灾难。我采用状态机设计模式来管理:

public enum RepairOrderStatus { PENDING { @Override public RepairOrderStatus next() { return REPAIRING; } }, REPAIRING { @Override public RepairOrderStatus next() { return COMPLETED; } }, COMPLETED { @Override public RepairOrderStatus next() { return VERIFIED; } }, VERIFIED { @Override public RepairOrderStatus next() { return null; } }; public abstract RepairOrderStatus next(); }

比如“待派工”状态只能流转到“维修中”,不能直接跳到“已完成”,非法流转抛异常提示。这个设计在代码层面保证了流程不可跳步,比在 Service 层写一堆 if-else 要优雅得多,也方便后续加“驳回”“挂起”等特殊流转。

4. 前端工程搭建与页面核心交互

4.1 Vue3 + Element Plus 工程搭建与项目目录

前端使用 Vite + Vue3 + TypeScript + Element Plus,配合 Pinia 做状态管理,Axios 封装请求。

src ├─ api # 接口请求函数 ├─ assets # 静态资源 ├─ components # 通用组件 ├─ router # 路由配置 ├─ stores # Pinia状态仓库(用户信息、权限、菜单) ├─ views # 页面组件 │ ├─ login │ ├─ dashboard │ ├─ ship │ ├─ plan │ ├─ order │ ├─ spare │ └─ system ├─ directives # 自定义指令(v-permission) ├─ utils # 请求封装、token管理、工具函数 └─ types # TS类型定义

脚手架直接用npm create vue@latest生成。项目初始化的时候,有几个实用配置值得说一下:

  • vite.config.ts 配置代理:开发环境下,前端访问 /api 自动代理到 http://localhost:8080,解决跨域问题。注意:生产环境跨域问题要通过 Nginx 反向代理解决,不能在前端用 CORS 解决。
// vite.config.ts 核心配置 server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }
  • Axios 请求封装:统一携带 token,统一处理错误码。响应拦截器里当 code 为 401 时清除本地登录态并跳转 /login。
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.message) return Promise.reject(new Error(res.message)) } return res }, (error) => { if (error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

4.2 船舶维保大屏与工单操作台界面实现

前端界面是用户直接感知的部分。维保系统主要的界面包括:数据看板、船舶档案管理页、保养计划页、维修工单页、备件库存页和系统管理页。

数据看板是整个系统的门面,我用 ECharts 做了四个核心指标:待执行保养计划数、逾期未处理工单数、当前库存不足备件数、本月维修总成本。下方拆成“近 12 个月维修成本趋势折线图”和“各船舶维修次数占比饼图”,这类图表在真实项目中非常常见,是管理层的核心关注点。

维修工单操作台是高频交互页面,设计原则是“能在一个页面完成的工作绝不跳页”。我采用左侧工单列表 + 右侧详情抽屉(Drawer)的结构,点击左侧工单时右侧展示设备故障描述、时间线、备件引用记录和费用明细。工单状态变更通过顶部状态按钮操作,操作完成后自动刷新列表,不必来回跳转。

保养计划页面则采用了日历视图和列表视图切换的设计。日历视图按月展示计划到期日,用不同颜色标识待执行(蓝色)、已完成(绿色)、逾期(红色),很像我们平时看共享单车的可用车辆地图,一眼扫过去就能发现哪些船需要重点关注。

4.3 前端权限按钮级控制与动态路由加载

动态路由是后台管理系统中比较有挑战的一部分,不同角色登录应该看到不同的菜单。我的方案是:用户登录成功后,通过接口getUserInfo获取该用户角色和权限标识集合;后端再返回该用户可访问的路由表(由后端根据 getRouters 接口动态路由生成);前端拿到路由表后,用 router.addRoute 动态注册。这套方案保持了前后端的权限唯一的真实性。

按钮级的权限控制通过自定义指令实现,在按钮上加v-permission="'plan:add'",在指令的 mounted 钩子中判断当前用户是否有该权限,没有则移除该按钮 DOM 元素。只有按钮隐藏还不够,后端的 @PreAuthorize 注解才是真正的防线。

还有一个容易遗漏的问题是侧边菜单刷新后高亮状态丢失。解决方案是通过读取当前路由的 meta.activeMenu 来指定高亮菜单,并在 Pinia 中缓存菜单列表,刷新页面时从 store 中恢复。

5. 环境部署与常见问题排查实录

5.1 从开发到生产:Nginx + Jar 包部署实践

开发完成后,部署方案直接决定了系统能不能被真实的业务部门用起来。这套系统的生产部署方案我分三层:

  • 数据库层:MySQL 单独部署在一台服务器,配置好远程访问账号,开启 binlog 保证数据可回溯。
  • 后端层:使用mvn package打包成 Jar 文件,然后用nohup java -jar ship-maintenance.jar --spring.profiles.active=prod后台运行。生产环境配置一定要单独放一个 application-prod.yml,数据库连接信息、Redis 地址等都和本地环境区分开。
  • 前端层:npm run build生成 dist 目录,把静态文件上传到 Nginx 的 html 目录下。关键配置是 location /api 反代到后端服务:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } 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; } }

这里有一个 Vue 路由刷新后 404 的坑:hsitory 模式路由直接刷新后 Nginx 找不到对应的静态文件,所以必须配置try_files $uri $uri/ /index.html,把所有前端路由都指向 index.html 由 Vue Router 接管。另外一个坑是 proxy_pass 最后面的斜杠,没有斜杠和带斜杠的路径拼接规则完全不同,忘了这个细节调试跨域会调试到怀疑人生。

5.2 常见问题速查表与避坑指南

开发这套系统的过程中,我整理了一些高频问题,直接做成速查表分享给你。

问题现象可能原因解决方案
前端请求 /api 接口 404Nginx 代理未配置 /api 或 Vite proxy 配置错误确认代理路径是否匹配,检查 rewrite 规则是否误删路径
登录后菜单不显示后端返回的路由表为空或前端 addRoute 失败排查 getRouters 接口返回数据,确认角色已关联菜单权限
本地可以登录,部署到服务器后提示 401系统时间不准导致 JWT 验签失败用 NTP 同步服务器时间,JWT 签发和校验时间一致性是基本前提
库存扣减出现负数没有用条件更新改用原子 UPDATE SQL:WHERE current_stock >= quantity
定时任务重复执行集群部署下每台机器都执行 @Scheduled引入分布式锁或使用 XXL-Job 统一调度
导出 Excel 中文乱码未设置响应编码设置 response.setCharacterEncoding("utf-8"),并添加 UTF-8 BOM 头
页面刷新菜单高亮丢失Vuex/Pinia 中菜单状态未持久化使用持久化插件保存 store,或刷新时重新拉取菜单数据

5.3 定时任务与消息通知的业务联动

维保系统非常依赖“主动提醒”的能力,船在海上跑,机务主管不可能天天盯着系统看。我的方案是独立的消息中心模块:保养计划逾期时生成站内信通知负责人;备件库存低于安全库存时通知备件管理员;工单验收超时提醒上一级主管。

定时任务只是负责写消息记录,真正的触达涉及邮件和短信通道。短信在远洋场景下经常信号不稳,所以我会优先做站内信 + 邮件结合的方式,邮件可以通过卫星网络异步接收。考虑到实际场景的通信限制,消息中心在设计上要允许批量已读、按船舶筛选、按级别筛选,方便高频使用者快速处理积压提醒。

这块业务如果做得好,系统粘性会高很多——因为用户不是来找数据的,而是来“收通知、办事”的。

6. 实测总结与老开发的一些心里话

如果让我总结这套系统的开发心得,最核心的一句话是:不要只做一个 CRUD 系统,要做一个懂业务的维保工具。CRUD 谁都会写,但“保养计划按运行小时自动计算”“库存扣减防超卖”“工单状态机防跳步”这类的细节,才是系统真正能用于生产的关键。

关于框架选型,我个人的体会是 SpringBoot + Vue3 + MyBatis 这套组合在船舶相关信息管理系统这个细分领域属于非常稳妥的选项。市场上招人容易,出了问题社区资料多,后期扩展(比如接入电子海图数据、船舶定位信息)也不至于推倒重来。如果项目已经过了原型阶段,建议优先考虑引入工作流引擎(比如 Flowable)来处理更复杂的审批流,因为船舶维修的审批链条往往比普通设备运维更长。

最后再分享一个小技巧:做这类行业管理系统,多跟实际业务人员聊聊会比看任何技术文档都有用。我第一次做船舶维保时,以为保养计划就是“每三个月做一次保养”,后来跟老轨(轮机长)聊过才知道,船上设备有“主机、辅机、锅炉、舵机、压载系统”之分,很多保养是按运行小时而非日历天计算,而且不同的船级社对保养记录的要求也完全不同。系统做得对不对,不是看你代码有多漂亮,而是看你的数据模型能不能准确还原这些真实业务场景。

希望这篇实战笔记能帮你把这个系统更快地落地。如果你正在做类似的后台管理系统,不管是船舶维保还是别的设备运维,把上面这些数据表、状态机、定时任务的思路拿去调整复用,应该能省不少弯路。有问题欢迎在评论区给我留言,一起交流。

相关新闻

基于SpringBoot+Vue3的船舶维保管理系统设计与实现

基于SpringBoot+Vue3的船舶维保管理系统设计与实现

做管理系统这些年&#xff0c;我接触过仓储、设备、物业好几个行业&#xff0c;船舶维保是其中最考验人的一个。原因很简单&#xff1a;船是移动资产&#xff0c;一条船几个月在海上漂&#xff0c;维保工作必须按计划刚性执行&#xff0c;可传统纸质记录和散落的Excel表格又实在…

2026/10/20 6:39:39 阅读更多 →
open-code-review:代码评审的工程化规则引擎与实践指南

open-code-review:代码评审的工程化规则引擎与实践指南

代码评审这件事&#xff0c;我见过太多团队把它做成一种仪式感极强、实际效果为零的流程。PR/MR 一发出来&#xff0c;评审人扫一眼绿叉就点 Approve&#xff0c;连 diff 都没完整看过&#xff1b;稍微认真的团队&#xff0c;评审时也只盯着变量命名是不是少了个空格、注释有没…

2026/10/20 6:39:39 阅读更多 →
Python基础教程第三版资源实战:PDF阅读与源码运行全指南

Python基础教程第三版资源实战:PDF阅读与源码运行全指南

简介&#xff1a;这是《Python基础教程&#xff08;第3版&#xff09;》高清带目录PDF及配套源码的珍藏整合包&#xff0c;专为Python零基础入门者、自学者和高校学生准备&#xff0c;旨在解决初学者“只有教材、缺少可运行示例”的痛点。压缩包总计108个文件、约9.22MB&#x…

2026/10/20 6:39:39 阅读更多 →

最新新闻

teledart 机器人鸿蒙化适配:从云端到边缘节点的迁移实践

teledart 机器人鸿蒙化适配:从云端到边缘节点的迁移实践

前阵子我们团队把一套基于 teledart 的 Telegram 机器人自动化系统&#xff0c;从云端服务器整体迁移到了鸿蒙设备上。这套系统原本承担告警推送、定时巡检、远程指令执行这些运维工作&#xff0c;迁移的原因也很直接&#xff1a;希望让机器人能力真正落到边缘设备&#xff0c;…

2026/10/20 12:19:11 阅读更多 →
Smarty与Twig的SSTI利用原理及Cookie会话稳定性攻防解析

Smarty与Twig的SSTI利用原理及Cookie会话稳定性攻防解析

1. 项目概述&#xff1a;一次真实的Web安全实战复盘&#xff0c;从模板引擎漏洞到会话稳定性的底层逻辑你有没有遇到过这样的情况&#xff1a;明明已经拿到目标网站的源码片段&#xff0c;也确认了后端用了Smarty或Twig这类主流PHP模板引擎&#xff0c;但就是卡在“怎么让代码执…

2026/10/20 12:19:11 阅读更多 →
AI检测率太高?从原理到工具实测的降AI率完整指南

AI检测率太高?从原理到工具实测的降AI率完整指南

AI检测率这件事&#xff0c;最近把本科生折腾得不轻。交上去的论文或报告&#xff0c;明明是自己一个字一个字敲的&#xff0c;查一遍AI率却提示偏高&#xff1b;反过来&#xff0c;有些段落确实借助了AI辅助&#xff0c;又怕被严格一点的分辨器一眼识别出来。这篇指南写给正在…

2026/10/20 12:19:11 阅读更多 →
SM202 PROFIBUS-DP主站通讯配置与现场故障排查实战

SM202 PROFIBUS-DP主站通讯配置与现场故障排查实战

1. 为什么聊SM202&#xff1a;一次现场通讯故障的复盘但凡做过DCS与现场智能设备联调的人&#xff0c;大概率都遇到过这种场景&#xff1a;现场仪表、变频器或者阀门定位器死活通讯不上&#xff0c;手操器排查了半天&#xff0c;最后发现不是仪表问题&#xff0c;而是主站侧压根…

2026/10/20 12:19:11 阅读更多 →
DeepSeek API入门:Python调用大模型实现文本生成与情感分析

DeepSeek API入门:Python调用大模型实现文本生成与情感分析

简介&#xff1a;面向自然语言处理领域的开发者&#xff0c;这份围绕DeepSeek API从入门到精通的PDF技术资料&#xff0c;从账号注册、获取API Key到依赖库安装、请求参数配置、请求构建与返回数据处理&#xff0c;覆盖API调用的完整链路&#xff0c;给出明确可循的操作指引。文…

2026/10/20 12:19:11 阅读更多 →
基于卷积神经网络的人体动作跟踪研究:从数据到时序建模的实战路径

基于卷积神经网络的人体动作跟踪研究:从数据到时序建模的实战路径

1. 先搞清楚研究对象&#xff1a;动作跟踪比动作识别多了什么如果有人拿着"基于卷积神经网络的人体动作跟踪研究"这个题目来找我聊&#xff0c;我通常第一句话不是问用了哪个网络&#xff0c;而是先反问一句&#xff1a;你这里的"跟踪"&#xff0c;到底想跟…

2026/10/20 12:18:10 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →