在航运业快速发展的今天,船舶维修保养管理系统的数字化转型已成为行业刚需。传统单体架构的维保系统普遍面临扩展性差、迭代困难、用户体验不佳等痛点。我们团队基于SpringBoot+Vue的前后端分离架构,开发了一套高性能、易扩展的船舶维保管理系统,经过半年多的实际运行验证,系统稳定性与效率提升显著。
技术选型上,后端采用SpringBoot 2.7 + MyBatis-Plus 3.5的组合,前端使用Vue 3 + Element Plus,数据库选用MySQL 8.0。这套技术栈的搭配主要基于以下考量:
提示:实际部署时建议使用Nginx作为静态资源服务器和反向代理,我们测试发现这比直接使用SpringBoot内嵌Tomcat服务静态资源的性能高出3倍左右。
维修任务流转是本系统的核心功能,我们设计了状态机驱动的工作流引擎。任务状态包括:待分配(10)→已分配(20)→进行中(30)→待验收(40)→已完成(50)→已取消(90)。关键实现代码如下:
java复制// 任务状态变更服务
@Transactional
public void changeTaskStatus(Long taskId, String newStatus) {
TaskEntity task = taskMapper.selectById(taskId);
if (!StateMachine.validTransition(task.getStatus(), newStatus)) {
throw new BusinessException("非法状态变更");
}
task.setStatus(newStatus);
task.setUpdateTime(LocalDateTime.now());
taskMapper.updateById(task);
// 记录状态变更日志
TaskLog log = new TaskLog();
log.setTaskId(taskId);
log.setOldStatus(task.getStatus());
log.setNewStatus(newStatus);
taskLogMapper.insert(log);
}
数据库表设计上,除了基础的task表外,我们还添加了task_attachment(任务附件)、task_comment(任务评论)、task_log(操作日志)三个辅助表,形成完整的数据关系。
备件管理采用"库存批次"的设计模式,每个入库批次记录独立保质期和供应商信息。核心难点在于实现实时库存预警,我们通过MySQL触发器+Redis缓存的方案解决:
sql复制CREATE TRIGGER `stock_update_trigger` AFTER UPDATE ON `part_stock`
FOR EACH ROW BEGIN
IF NEW.stock_quantity < NEW.low_level THEN
INSERT INTO stock_alert(part_id, current_qty, alert_time)
VALUES(NEW.part_id, NEW.stock_quantity, NOW());
END IF;
END
前端采用ECharts实现库存可视化看板,关键配置项包括:
船舶信息表(ship_info)采用星型模型设计,主表存储基本信息,扩展表存放技术参数:
sql复制CREATE TABLE `ship_info` (
`ship_id` int NOT NULL AUTO_INCREMENT,
`ship_code` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT 'IMO编号',
`ship_name` varchar(50) COLLATE utf8mb4_bin NOT NULL,
`ship_type` enum('CONTAINER','BULK','TANKER','OTHER') COLLATE utf8mb4_bin DEFAULT NULL,
`build_date` date DEFAULT NULL,
`last_dock_date` date DEFAULT NULL,
`status` tinyint DEFAULT '1' COMMENT '1-运营中 2-维修中 3-停运',
PRIMARY KEY (`ship_id`),
UNIQUE KEY `udx_code` (`ship_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
针对维修记录查询慢的问题(>2s),我们实施了以下优化措施:
sql复制ALTER TABLE repair_record
ADD INDEX `idx_ship_status` (`ship_code`, `repair_status`);
sql复制CREATE TABLE repair_record_detail (
record_id INT PRIMARY KEY,
repair_detail TEXT,
FOREIGN KEY (record_id) REFERENCES repair_record(record_id)
);
java复制public Page<RepairRecordVO> queryRecords(RepairQueryDTO dto) {
return repairRecordMapper.selectPage(new Page<>(dto.getPage(), dto.getSize()),
new QueryWrapper<RepairRecord>()
.eq(dto.getShipCode() != null, "ship_code", dto.getShipCode())
.between(dto.getStartDate() != null && dto.getEndDate() != null,
"repair_time", dto.getStartDate(), dto.getEndDate())
.orderByDesc("repair_time"));
}
实测优化后查询性能提升8倍以上,平均响应时间降至300ms内。
我们采用HATEOAS风格的API设计,响应示例:
json复制{
"_embedded": {
"repairTasks": [
{
"taskId": 1024,
"title": "主机润滑油更换",
"status": "IN_PROGRESS",
"_links": {
"self": { "href": "/api/tasks/1024" },
"cancel": { "href": "/api/tasks/1024/cancel" }
}
}
]
},
"_links": {
"self": { "href": "/api/tasks?page=1" },
"next": { "href": "/api/tasks?page=2" }
},
"page": {
"size": 10,
"totalElements": 45,
"totalPages": 5,
"number": 0
}
}
针对维修报告等文件上传需求,前端采用分片上传方案:
vue复制<template>
<el-upload
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-success="handleSuccess"
:data="uploadData"
:multiple="false"
:limit="1"
:file-list="fileList"
:http-request="customRequest">
<el-button type="primary">点击上传</el-button>
</el-upload>
</template>
<script setup>
const customRequest = async (options) => {
const file = options.file;
const chunkSize = 5 * 1024 * 1024; // 5MB分片
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice(start, end);
await api.uploadChunk({
chunk,
chunkNumber: i,
totalChunks: chunks,
filename: file.name
});
}
await api.mergeChunks({
filename: file.name,
totalChunks: chunks
});
};
</script>
后端使用Spring的MultipartFile接收分片,通过MD5校验保证文件完整性。
我们推荐使用Docker Compose进行一键部署,docker-compose.yml关键配置:
yaml复制version: '3.8'
services:
backend:
image: registry.example.com/ship-repair:1.0.0
container_name: repair-backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/repair_db
depends_on:
mysql:
condition: service_healthy
frontend:
image: registry.example.com/ship-repair-ui:1.0.0
container_name: repair-frontend
ports:
- "80:80"
depends_on:
backend:
condition: service_started
mysql:
image: mysql:8.0
container_name: repair-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: repair_db
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 10s
retries: 5
volumes:
mysql_data:
使用SpringBoot Actuator + Prometheus + Grafana搭建监控看板:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ship-repair
在前后端分离部署时,我们遇到CORS问题的典型报错:
code复制Access to XMLHttpRequest at 'http://api.example.com/tasks'
from origin 'http://localhost:8080' has been blocked by CORS policy
最终采用的解决方案是在SpringBoot配置类中添加:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:8080", "https://prod.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
}
注意:生产环境务必严格限制allowedOrigins,避免使用通配符"*"带来安全风险
在复杂查询场景下,我们总结出以下最佳实践:
xml复制<sql id="taskBaseColumns">
task_id, ship_code, task_title, task_status
</sql>
<select id="selectTasks" resultType="TaskVO">
SELECT <include refid="taskBaseColumns"/>
FROM repair_task
<where>
<if test="status != null">
AND task_status = #{status}
</if>
</where>
</select>
xml复制<insert id="batchInsertParts">
INSERT INTO part_inventory (part_id, warehouse, quantity) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.partId}, #{item.warehouse}, #{item.quantity})
</foreach>
</insert>
同时需要在JDBC URL添加参数:
code复制jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
对于复杂表单组件,我们采用Composition API的封装方式:
vue复制<script setup>
// useTaskForm.js
export default function useTaskForm(initialValues) {
const form = reactive({
title: '',
priority: 'MEDIUM',
...initialValues
});
const rules = {
title: [{ required: true, message: '请输入任务标题' }],
deadline: [{ validator: checkDeadline }]
};
function checkDeadline(rule, value, callback) {
if (new Date(value) < new Date()) {
callback(new Error('截止时间不能早于当前时间'));
} else {
callback();
}
}
return { form, rules };
}
</script>
<template>
<el-form :model="form" :rules="rules">
<!-- 表单字段 -->
</el-form>
</template>
这种设计带来以下优势:
现有系统可通过以下方式扩展移动支持:
javascript复制// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: '船舶维保',
short_name: '维保助手',
theme_color: '#1890ff'
}
})
]
});
建议通过以下方式增强数据分析能力:
sql复制-- 创建物化视图加速查询
CREATE MATERIALIZED VIEW repair_stats_mv
DISTRIBUTED BY HASH(ship_code)
REFRESH ASYNC
AS
SELECT
ship_code,
COUNT(*) as repair_count,
SUM(cost) as total_cost
FROM repair_records
GROUP BY ship_code;
python复制# 自定义维保指标
class RepairMetrics(BaseMetric):
metric_name = "avg_repair_time"
expression = "AVG(TIMESTAMPDIFF(HOUR, start_time, end_time))"
完整源码采用标准的Maven多模块结构:
code复制ship-repair/
├── ship-admin/ # 后台管理前端
│ ├── public/
│ ├── src/
│ │ ├── api/ # API封装
│ │ ├── assets/
│ │ ├── components/ # 通用组件
│ │ ├── router/ # 路由配置
│ │ └── views/ # 页面视图
│ └── vite.config.js
│
├── ship-app/ # 移动端前端
│
└── ship-server/ # 后端服务
├── src/main/
│ ├── java/com/example/ship/
│ │ ├── config/ # 配置类
│ │ ├── controller/ # 控制器
│ │ ├── entity/ # 数据实体
│ │ ├── mapper/ # MyBatis接口
│ │ ├── service/ # 业务逻辑
│ │ └── ShipApplication.java
│ └── resources/
│ ├── mapper/ # XML映射文件
│ ├── static/
│ └── application.yml
└── pom.xml
bash复制# JDK 17+
brew install openjdk@17
# Node.js 16+
nvm install 16
nvm use 16
# MySQL 8.0
docker run --name mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:8.0
sql复制CREATE DATABASE `ship_repair_dev` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
GRANT ALL PRIVILEGES ON ship_repair_dev.* TO 'repair_user'@'%' IDENTIFIED BY 'user123';
FLUSH PRIVILEGES;
bash复制cd ship-admin
npm install
npm run dev
bash复制cd ship-server
mvn spring-boot:run -Dspring-boot.run.profiles=dev
问题1:前端代理配置错误
code复制Proxy error: Could not proxy request /api/tasks from localhost:8080 to http://localhost:8081
解决方案:
javascript复制// vite.config.js
server: {
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
问题2:MyBatis映射文件未加载
code复制Invalid bound statement (not found): com.example.ship.mapper.TaskMapper.selectById
检查步骤:
xml复制<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
问题3:分页查询缓慢
优化方案:
java复制// 在Mapper接口添加
@Select("SELECT COUNT(1) FROM repair_task ${ew.customSqlSegment}")
Long selectCountCustom(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
yaml复制mybatis-plus:
configuration:
cache-enabled: true
问题4:前端大数据量渲染卡顿
解决方案:
vue复制<template>
<el-table-v2
:columns="columns"
:data="data"
:width="800"
:height="400"
:row-height="50"
fixed
/>
</template>
java复制// 添加Logstash编码器
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.2</version>
</dependency>
当系统规模扩大时,建议按以下步骤拆分:
按功能拆分为独立服务:
技术架构升级:
yaml复制# Spring Cloud Alibaba技术栈
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080
在项目实际开发过程中,我们发现合理的模块划分和清晰的接口定义比技术选型更重要。特别是在前后端协作中,使用Swagger或YAPI维护API文档能显著减少沟通成本。对于中小型船舶管理公司,当前单体架构已能满足需求,但建议在代码层面提前做好模块化设计,为可能的微服务化改造预留扩展点。