ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot2+Vue3全套设备管理系统开发实战:架构设计到部署避坑

SpringBoot2+Vue3全套设备管理系统开发实战:架构设计到部署避坑 设备台账乱、维修记录靠Excel、盘点全靠腿和记忆力——这是我见过很多中小制造企业的真实状态。开发这套基于SpringBoot2Vue3MyBatis-PlusMySQL8.0的中小企业设备管理系统就是为了把“纸质单据人工统计”这套老流程换成一条“录入即归档、状态可追踪、报表自动出”的数字化链路。整套系统从前端页面到后端接口再到数据库表结构全部开源并附带部署文档拿到手改一改logo和字段就能直接用。这篇文章不是给你复述代码而是站在做完整个项目的角度把架构选型的理由、核心模块怎么拆、数据库怎么建模、前后端怎么对接、部署时有哪些坑完整过一遍。适合正在做毕业设计、刚进公司接手管理类系统开发、或者想把工厂设备管理从Excel里解放出来的朋友参考。1. 设备管理系统到底在解决什么问题先看清业务再谈技术1.1 中小企业的设备管理现状与核心痛点在谈SpringBoot2和Vue3之前得先把这个系统为什么存在说清楚。很多初学者一上来就建表写接口结果做出来的系统没人用本质原因是没搞明白业务痛点。中小企业设备管理通常困在四个环节台账不清设备编号、存放位置、购入日期、供应商信息分散在纸质档案和不同人的电脑里盘点时口径不一致。维修无痕设备坏了打个电话叫人来修维修记录没人录入下次再坏同一个部件维修工根本不知道历史故障。保养靠拍脑袋什么时候该换机油、什么时候该检查电路全凭老师傅记忆人一走经验就断档。统计靠手工月底想算一下设备故障率、维修费用、保养完成率得翻一堆纸质单据然后手工做Excel透视表。这套系统就是要在这四个环节上做信息化改造形成“设备档案—使用状态—维修保养—统计报表”的管理闭环。1.2 系统的功能模块划分与角色权限从实际使用场景倒推系统分成了四个核心业务模块加一个系统管理模块模块核心功能对应角色设备台账管理设备新增、编辑、查询、导出支持设备分类与状态管理设备管理员维修管理维修工单创建、指派、处理结果回填、维修历史查询维修工、设备管理员保养管理保养计划制定、执行登记、到期提醒设备管理员、保养员统计报表设备状态分布、维修费用统计、保养完成率负责人、设备管理员系统管理用户管理、角色管理、菜单权限分配超级管理员权限设计上没有做得过于复杂采用RBAC模型即“用户—角色—菜单”三层结构。后端通过Spring Security做登录认证前端根据用户角色控制菜单显示和按钮点击权限。比如普通维修工登录后只看得到“待处理工单”和“我的维修记录”设备管理员才能看到台账和报表模块。1.3 为什么选择前后端分离架构这个项目采用前后端分离而不是传统的JSPServlet模式主要有三个原因第一开发和维护的解耦。前端Vue3项目独立运行端口是5173开发环境后端SpringBoot独立运行在8080端口两边通过JSON格式的HTTP接口通信。前端团队改页面不需要重启后端服务后端加接口不影响前端页面逻辑。第二复用性。系统的后端接口设计成RESTful风格以后如果要扩展移动端APP或者小程序直接复用现有接口就行不需要重新写一套业务逻辑。第三部署灵活性。前端构建出来的静态资源可以放在Nginx里后端打包成Jar包独立跑流量大时后端可以多实例部署前面挂负载均衡。这种方式比传统单体JSP应用更适合未来的扩展需求。2. 技术栈选型为什么是SpringBoot2、Vue3、MyBatis-Plus、MySQL8.02.1 SpringBoot2约定大于配置让开发专注于业务后端框架选择SpringBoot2而不是SpringMVCXML配置的老一套核心原因是开发效率。SpringBoot通过自动配置机制把数据源、事务管理器、Web容器这些基础设施全部帮你配好你只需要在application.yml里写数据库连接信息、Redis连接信息等必要配置即可。具体到这个项目SpringBoot2.7.18版本保持了对JDK8的良好兼容同时支持JDK11和JDK17。考虑到中小企业内部服务器环境普遍还是JDK8居多我最终锁定在SpringBoot2.7.x而不是直接上SpringBoot3。这是个很实际的取舍SpringBoot3必须JDK17以上很多老机器的JDK版本根本跑不起来。SpringBoot2在这个项目里承担的核心职责包括接收HTTP请求通过Controller层接口暴露给前端管理Service层事务确保设备新增、维修工单状态变更等操作的原子性集成Spring Security做登录认证和权限校验集成MyBatis-Plus做数据库操作2.2 Vue3 Vite Element Plus前端开发的现代组合前端选择Vue3的原因非常直白Vue2官方已经停止维护新项目再学老技术不划算。搭配Vite作为构建工具开发环境下热更新速度比Webpack快非常多——改一行代码页面几乎毫秒级刷新这在调试设备台账表单时体验尤其明显。UI组件库选的是Element Plus它是Vue3生态里最成熟的中后台组件方案。表格、表单、弹窗、日期选择器、分页组件全都开箱即用对这类管理系统来说可以节省大量写UI的时间。前端项目的标准目录结构如下src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── views/ # 页面视图 │ ├── device/ # 设备台账页面 │ ├── repair/ # 维修管理页面 │ ├── maintenance/ # 保养管理页面 │ ├── report/ # 统计报表页面 │ └── system/ # 系统管理页面 ├── utils/ # 工具函数 ├── App.vue └── main.js路由守卫的逻辑值得单独说一下。在router/index.js里配置了一个全局前置守卫每次路由跳转前检查Pinia里的token和用户信息。如果没有token直接跳转到登录页如果有token但用户信息已经丢失就从localStorage里恢复角色不匹配的访问直接跳404。这样的好处是刷新页面时不会因为状态丢失而误跳登录页。2.3 MyBatis-Plus把CRUD从繁琐中解放出来MyBatis-Plus在这个项目里扮演的是数据持久层增强工具。在传统的MyBatis里每写一个实体类的增删改查都得手写对应的SQL语句和Mapper XML映射这种工作量对业务相对标准化的管理系统来说是完全不必要的。MyBatis-Plus提供了BaseMapper接口继承之后单表CRUD基本不需要写SQL。比如DeviceMapper extends BaseMapperDevice那么selectById、selectList、insert、updateById这些方法全都自带。配合ServiceImpl和IService连Service层的基础逻辑都可以复用了。具体的集成操作如下。2.4 MySQL8.0字符集、窗口函数带来的红利数据库选择MySQL8.0是在性能、可用性和学习成本之间权衡后的结果。相比MySQL5.78.0有几个对开发非常友好的改进默认字符集是utf8mb4完整支持emoji和所有Unicode字符不用担心设备名称里带特殊符号导致乱码。支持窗口函数比如计算设备维修费用的排名、按月累计统计可以直接用SQL完成不需要在Java代码里做一堆循环。原生支持WITH公共表表达式复杂查询的SQL可读性提升明显。性能优化器更智能在某些场景下查询速度优于5.7。安装方面我在调试阶段用过两种方式Windows本机直接安装以及Linux服务器上通过Docker运行。对于学习阶段更推荐Docker方式拉取镜像后一条命令就能启动不用踩本地安装的初始化坑。后面第5部分会有具体的Docker命令和参数说明。2.5 为什么要选这套组合而不是SSH框架SSH指的是Spring Struts Hibernate这套老组合。Struts2的XML配置极其繁琐Hibernate在复杂查询时又不够灵活两者的学习曲线都比SpringBoot加MyBatis-Plus陡峭。最关键的是现在招开发基本没有人写Struts了新项目用老框架就是给自己埋坑。SpringBoot2Vue3MyBatis-Plus这套组合在社区活跃度、招聘需求量和学习资源丰富度上都是现阶段的优解。3. 系统核心模块设计与数据库建模3.1 数据库整体设计与表结构拆分数据库命名为device_management核心表一共5张外加权限相关的3张表表名说明关键字段device设备台账表id, device_name, device_code, category_id, location, purchase_date, status, responsible_personrepair_order维修工单表id, device_id, fault_desc, reporter, assignee, status, repair_result, repair_cost, create_time, finish_timemaintenance_plan保养计划表id, device_id, plan_name, cycle_days, last_execute_time, next_execute_time, statusmaintenance_record保养记录表id, plan_id, device_id, executor, execute_time, content, remarkdevice_category设备分类表id, category_name, parent_idsys_user用户表id, username, password, real_name, phone, statussys_role角色表id, role_name, role_codesys_user_role用户角色关联表user_id, role_id有两点设计是值得展开说说的。第一设备表中status字段只用了三种值RUNNING运行中、FAULT故障中、MAINTENANCE保养中。这个状态不是人工手动改的而是通过维修工单和保养记录自动联动更新的——提交维修工单时设备状态自动改为FAULT维修完成回填结果时自动改回RUNNING。从业务流程上杜绝了“台账状态跟实际不符”的问题。第二保养计划表里cycle_days和next_execute_time的关系。next_execute_time在生成计划时根据last_execute_time cycle_days计算执行保养登记后再根据最新执行时间和周期重新计算下一次日期。这种设计只需要一个定时任务去扫描next_execute_time小于当前日期的记录并提醒逻辑非常清晰。3.2 设备词条的数据查询与MyBatis-Plus的配合在最频繁使用的设备台账列表页我使用了MyBatis-Plus的Page分页对象配合LambdaQueryWrapper进行条件查询。查询逻辑大概是public PageDevice queryDevicePage(int pageNum, int pageSize, DeviceQuery query) { PageDevice page new Page(pageNum, pageSize); LambdaQueryWrapperDevice wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getDeviceName()), Device::getDeviceName, query.getDeviceName()) .like(StringUtils.hasText(query.getDeviceCode()), Device::getDeviceCode, query.getDeviceCode()) .eq(query.getCategoryId() ! null, Device::getCategoryId, query.getCategoryId()) .eq(StringUtils.hasText(query.getStatus()), Device::getStatus, query.getStatus()) .orderByDesc(Device::getCreateTime); return deviceMapper.selectPage(page, wrapper); }注意wrapper里的条件判断写法StringUtils.hasText()成立时才会把对应条件拼进SQL否则跳过。这样页面上的搜索表单不需要进行空值判断直接传参就能实现动态查询。这个写法在管理类项目里非常实用建议直接记下来用。3.3 维修工单的状态流转设计维修工单是整个系统里业务流程最复杂的模块。状态机的定义如下PENDING待指派→ ASSIGNED维修中→ COMPLETED已完成 ↘ CANCELLED已取消具体流转逻辑是报修人提交工单状态为PENDING设备管理员指派给维修工状态变更为ASSIGNED维修工填写修复结果和费用状态变更为COMPLETED如果设备经检查无需维修或无法维修可取消工单状态为CANCELLED工单列表页的查询需要多表关联要关联device表查出设备名称、关联sys_user表查出指派人姓名。这里没有用MyBatis-Plus的TableField(exist false)做临时字段而是直接在Mapper XML里写了一段JOIN查询返回一个RepairOrderVO。原因很简单多表JOIN用QueryWrapper写起来绕还是XML里的SQL最直观。MyBatis-Plus并不排斥你写XML它的优势在于单表CRUD不用写复杂查询还是可以自己控制。3.4 逻辑删除字段deleted一个必须注意的细节这是网上搜索热度非常高的一个点——mybatis-plus 查询deleted在实际项目里确实容易踩坑。为了防止用户误删重要数据设备表、维修工单表、用户表都加了deleted字段默认值为0删除时执行的是更新操作把deleted改为1。配置方式是在application.yml里声明全局逻辑删除配置mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0只要配置了这项MyBatis-Plus所有内置的查询方法都会自动拼接AND deleted 0条件。但是这里有几个容易出错的地方值得记住自己手写在XML文件里的SQL不会自动带deleted条件必须手动加上WHERE deleted 0否则会查出已删除数据。selectById方法是默认过滤逻辑删除数据的但selectBatchIds按ID批量查询在旧版本中不会过滤升级要谨慎。如果要恢复数据不能直接调用updateById把deleted改为0因为MyBatis-Plus的更新方法也会强制带上deleted 0条件导致更新不到记录。需要写自定义SQL。这个细节在项目联调文件里专门写了一页注意事项可以说是实战中最容易忽略的坑。3.5 定时任务实现保养到期提醒保养提醒不需要引入Quartz这种重量级框架SpringBoot自带的Scheduled注解就够用了。在MaintenanceJob组件里配置一个每天凌晨执行的任务Component public class MaintenanceJob { Scheduled(cron 0 0 1 * * ?) public void checkMaintenanceDue() { // 查询所有 next_execute_time now 且 status NORMAL 的保养计划 // 生成待办通知写入数据库 } }需要注意的是Scheduled默认是单线程串行执行的。如果系统里将来要增加多个定时任务建议配置一个线程池TaskScheduler来避免任务互相阻塞。另外多实例部署时Scheduled会重复执行生产环境如果做了集群要考虑引入分布式锁不过这套系统面向的是中小企业单实例部署场景这个问题暂时不突出。4. 后端实战SpringBoot2与MyBatis-Plus的工程落地细节4.1 后端标准目录结构与包设计很多刚学Java Web的同学容易把包结构写得乱七八糟这里直接给出这个项目实际使用的后端目录com.company.device ├── config/ # 配置类MybatisPlusConfig、SecurityConfig、CorsConfig ├── controller/ # 控制层接收请求、参数校验、返回结果 ├── service/ # 业务层核心业务逻辑 │ └── impl/ # 业务实现类 ├── mapper/ # 数据访问层接口 ├── entity/ # 实体类与数据表对应 ├── vo/ # 视图对象供接口返回前端 ├── dto/ # 数据传输对象接收前端参数 ├── common/ # 公共类统一返回结果、异常处理、分页参数 ├── jwt/ # JWT令牌工具 └── job/ # 定时任务这里把entity和vo分开是个好习惯。entity严格对应数据库字段比如password字段就在entity里vo是按前端需要组织的数据结构。举一个实际例子维修工单列表需要显示设备名称和指派人姓名但repair_order表里只有device_id和assignee_id两个关联ID。如果在entity里加冗余字段方向就反了——应该建一个RepairOrderVO把需要的关联名称加进去。4.2 统一返回结果与全局异常处理的约定前后端分离项目的接口通信需要一套稳定的数据格式约定。项目里定义了一个Result类所有接口统一返回这样的JSON结构{ code: 200, message: 操作成功, data: {} }code为200表示成功非200表示业务异常。data是泛型可以是对象、列表或者分页数据。全局异常处理使用RestControllerAdvice注解实现。业务代码里只需要抛出BusinessException自定义异常统一异常处理器会捕捉并转换成对应的错误码返回。这样做的好处是Controller层不需要每个方法都写try-catch代码非常干净。还有一个细节文件上传的接口不要放在同一个统一返回结构中因为上传用的是multipart/form-data格式返回值需要不同处理。我在前端封装upload方法时做了单独处理避免拿不到data字段。4.3 登录认证与JWT会话管理由于前后端分离Session这种依赖Cookie的会话方式不太合适项目采用JWT做登录令牌。登录流程如下用户输入用户名密码后端校验。密码加密方式采用BCryptPasswordEncoder数据库存储的也是BCrypt加密后的密码而不是明文。校验通过后生成一个有效期为24小时的JWT令牌返回给前端。前端把Token存在localStorage里每个请求在请求拦截器中自动在Header里带上Authorization: Bearer token。后端通过Spring Security的过滤器链解析Token判断用户信息和角色权限。CorsConfig配置类需要特别提醒一下。开发环境前端跑在5173端口后端跑在8080端口两边的端口不同必定触发跨域。我一开始只配置了CrossOrigin注解结果发现预检请求偶尔会失败后来直接在CorsConfig里统一配置了允许的源路径、请求头和请求方法。生产环境前后端通常通过Nginx同域代理跨域配置反而要注释掉否则可能出现奇怪的重复请求问题。4.4 接口参数校验注解方式的实践Controller层接收参数以后参数校验不要用一堆if判断。项目里引入了spring-boot-starter-validation在DTO字段上加注解即可public class DeviceDTO { NotBlank(message 设备名称不能为空) private String deviceName; NotNull(message 设备分类不能为空) private Long categoryId; NotBlank(message 存放位置不能为空) private String location; }在Controller方法参数前加上Valid注解校验不通过时直接抛出MethodArgumentNotValidException由全局异常处理器捕获后返回具体的错误信息。这种方式比手写判断省了至少三分之一代码。业务校验建议放在Service层。比如删除设备分类时要检查下面是否还有设备这个逻辑用注解校验表达不了就正常写在Service方法里有异常直接抛BusinessException。4.5 事务控制的坑自调用不生效维修工单提交方法里有两个操作插入工单记录 修改设备状态为FAULT。这两步必须在一个事务里否则会出现工单创建成功但设备状态没变的情况。项目里直接在Service方法上加了Transactional(rollbackFor Exception.class)。这里有一个非常隐蔽的坑当同类内部方法调用带Transactional的方法时事务是不生效的因为Spring事务是基于AOP代理的内部this.xxx()调用不会经过代理对象。解决方法是把需要事务的方法独立到另一个Service类中或者通过AopContext.currentProxy()获取代理对象再调用。这个坑在排查“为什么数据插入一半没有回滚”时浪费了不少时间这里提醒大家注意。5. 数据库环境从零搭建到初始化数据5.1 本机与Docker方式安装MySQL8.0MySQL8.0的安装有两种常见途径。Windows本机安装直接去官网下载MSI安装包按向导操作注意选Server Only和Use Legacy Authentication。因为MySQL8.0默认用的caching_sha2_password加密规则部分老版本的数据库连接驱动特别是5.x版本的JDBC驱动不兼容。如果不想污染本机环境推荐用Docker方式docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEdevice_management \ -e TZAsia/Shanghai \ -v mysql-data:/var/lib/mysql \ mysql:8.0这里有几个值得注意的参数MYSQL_DATABASE会在容器首次启动时自动创建数据库省去手动建库。TZAsia/Shanghai很关键不加的话容器默认用的是UTC时区导致数据库里写入的时间比北京时间慢8个小时。数据目录挂载到mysql-data这个命名卷中容器删除数据也不会丢这是本机调试阶段很重要的保障。5.2 字符集与时区配置项目初始化SQL脚本里建表语句的默认字符集一定要显式声明CREATE TABLE device ( id bigint NOT NULL AUTO_INCREMENT, device_name varchar(100) NOT NULL, ... create_time datetime DEFAULT NULL, deleted tinyint DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;数据库连接字符串里也要加上时区和字符集参数spring: datasource: url: jdbc:mysql://localhost:3306/device_management?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver我调试时遇到过一个问题插入数据时日期时间显示正常读取出来却少了8个小时后来发现是连接字符串里没加serverTimezoneAsia/Shanghai。再加上之前说的Docker时区问题时间相关的问题一定要在数据库、连接串、JVM时区三个层面统一。5.3 初始化数据与演示账号项目提供的SQL脚本中包含基础数据设备分类、两三台示例设备、用户账号和角色。演示账号配置为超级管理员admin / admin123设备管理员manager / 123456维修工repair / 123456用户密码的初始化方式是通过Java命令生成BCrypt加密串后手动更新到数据库而不是直接在SQL里写明文。文档里我写了这个生成步骤方便二次开发时创建新用户。6. 前端实战Vue3 Element Plus的关键页面实现6.1 登录页的实现与动态背景效果登录页是系统的门面。实现了一个带点线动态背景的登录页面——背景中有很多小点在缓慢移动相邻的点之间会连接成线。这个效果看起来炫酷实际上实现原理并不复杂用Canvas画布铺满整个登录页背景。定时器每隔几十毫秒更新小点坐标位置。遍历所有点组合计算两点距离小于设定阈值时用stroke画一条半透明的线。鼠标移动时把鼠标坐标也作为一个点参与连线产生交互感。关键代码如下function draw() { ctx.clearRect(0, 0, width, height); for (let i 0; i points.length; i) { const p points[i]; p.x p.vx; p.y p.vy; // 碰撞边界反弹 if (p.x 0 || p.x width) p.vx * -1; if (p.y 0 || p.y height) p.vy * -1; // 画点 ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI * 2); ctx.fill(); // 连线 for (let j i 1; j points.length; j) { const q points[j]; const dist Math.hypot(q.x - p.x, q.y - p.y); if (dist 150) { ctx.globalAlpha 1 - dist / 150; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke(); } } } ctx.globalAlpha 1; requestAnimationFrame(draw); }这里需要注意Canvas在window.resize事件里要重新设置宽高否则浏览器缩放后画面会变形。另外登录页背景动画的帧率要控制住不要每个设备都跑满60帧低配电脑风扇会狂转。可以设置一个帧率上限比如每33毫秒重绘一次大约30帧视觉上完全够用。6.2 设备台账页面表格、搜索、分页、状态标签设备台账列表是系统使用频率最高的页面。布局是顶部搜索栏、中间表格、底部翻页器的经典结构。Element Plus的el-table组件的用法不展开说了这里说几个实际开发中容易忽略的细节表格列要设置min-width而不是固定的width这样在窄屏下表格可以横向滚动不至于把列挤得变形。状态字段的值需要转换成对应的el-tag类型。例如RUNNING显示为绿色标签“运行中”FAULT显示为红色“故障中”MAINTENANCE显示为橙色“保养中”。转换逻辑抽成一个方法在status列中用:formatter调用。操作列固定在最右侧使用fixedright属性让“编辑、删除”按钮始终可见不用横向滚动才能看到。删除操作一定要加第二次确认弹窗。ElMessageBox.confirm组件可以很方便地实现避免误点。6.3 表单校验与日期组件的类型转换设备新增和编辑共用一个弹窗表单通过判断form.id是否存在来决定是新增还是更新。需要注意Element Plus的el-date-picker组件默认返回的是Date对象提交给后端时需要用dayjs格式化form.purchaseDate dayjs(form.purchaseDate).format(YYYY-MM-DD);如果你的后端字段类型是LocalDate前端传2024-03-15这种字符串即可。但如果是LocalDateTime需要传2024-03-15 10:30:00。这两个类型混用的坑很容易在联调时出现——后端报JSON parse error: Cannot deserialize value of type java.time.LocalDate from String十有八九就是前端日期格式多了时分秒或者反了。6.4 维修工单的前后端交互逻辑维修工单页面有三个Tab页签全部、待处理、已完成。切换Tab时重新请求对应状态的接口。这里设计了一个巧妙的地方不额外写三个接口而是在同一个查询接口上增加status参数前端Tab切换时用不同的状态值刷新数据。维修工单的状态操作按钮是通过v-if根据当前状态值动态显示的PENDING状态显示“指派”按钮弹出维修工选择框。ASSIGNED状态显示“完成维修”按钮弹出结果回填表单。COMPLETED状态不显示任何操作按钮只有详情可看。这个动态渲染的点击态在权限上也要注意后端接口做了角色限制维修工只能调用回填结果接口设备管理员才能调用指派接口。前端隐藏按钮只是体验优化真正的安全边界在后端。6.5 统计报表页面的可视化方案统计报表板块用到了可视化图表库ECharts。三个核心图表分别是设备状态分布饼图直观展示当前三类状态的设备数量占比。维修费用趋势折线图按月份汇总维修工单费用。保养完成率柱状图统计每个设备类型的保养执行情况。ECharts在Vue3中的使用有一点和Vue2不同图表实例不能直接挂载在Vue响应式对象上否则会报TypeError: Cannot read properties of undefined。正确做法是用普通的JavaScript变量或shallowRef来持有图表实例在onMounted中初始化在onBeforeUnmount中调用dispose销毁。另外有一个页面尺寸的适配细节如果不处理浏览器窗口缩放时图表不会自动跟着变。项目里在容器组件上添加了window.addEventListener(resize, handleResize)监听在handleResize中调用图表实例的resize()方法。这里要记得在onBeforeUnmount中移除监听否则页面来回切换会导致监听事件重复累积。7. 联调、部署与项目文档的使用方式7.1 前后端联调的常见问题排查前后端联调是最容易出问题也最能学到东西的阶段。这段时间实际遇到的几个高频问题跨域请求失败。表现为前端控制台报Access-Control-Allow-Origin相关错误。排查时先确认后端CorsConfig是否配置正确再看请求是不是被代理拦截。Vite开发环境可以通过配置server.proxy把/api开头的请求代理到后端避免跨域// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })做了这个代理之后前端请求路径统一写成/api/device/list后端用server.servlet.context-path/api保持路径一致。200状态但code非200。这是另一个容易困惑的地方。HTTP状态码返回200但Result里的code是500或4000。排查时不要盯着网络面板的状态码要看响应体里的code字段。项目的前端axios响应拦截器里统一处理了非200的code弹出错误消息提示所以页面能正常报错。日期字段序列化格式不一致。后端把LocalDateTime序列化给前端时默认格式是2024-03-15T10:30:00中间有字母T不美观且前端处理麻烦。配置Jackson的全局格式解决spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai7.2 打包部署Jar包与静态资源分离后端打包使用Maven的package命令打成可执行的Jar包然后用java -jar运行mvn clean package java -jar device-system.jar --spring.profiles.activeprod生产环境的配置写在application-prod.yml里与本地开发使用的application-dev.yml分开。切换环境通过--spring.profiles.active参数指定。这个习惯强烈建议新项目就建立起来否则后面环境多了配置文件会变成一团乱麻。前端构建npm run build构建产物输出到dist目录包含index.html和assets静态资源。部署时用Nginx托管前端静态文件同时配置接口反向代理server { listen 80; server_name your-domain.com; root /var/www/device-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html这行特别重要。Vue Router如果采用history模式刷新非根路径时Nginx会直接按真实路径找文件找不到就返回404。加上这行配置后所有路径都会回退到index.html由前端路由接管刷新页面就不会白屏了。7.3 项目文档的使用方式与二次开发指引标题里强调“含文档”这个文档不是摆设。文档包含三个部分环境搭建文档从JDK安装到MySQL初始化再到前端依赖安装每一步都有截图和命令。数据库设计文档每张表的字段说明、关联关系、状态枚举定义更新需求时先查文档再动表结构避免字段语义冲突。接口说明文档每个接口的请求方式、路径、参数、返回示例。这部分我用SpringDoc生成了Swagger UI界面在线调试接口非常方便。对二次开发最有帮助的是文档里的“功能扩展指引”。比如要新增一个“设备报废管理”模块文档里给出清晰的路线图先在数据库建表再写实体类、Mapper接口、Service、Controller最后在前端新建页面并配置路由和菜单权限。照着这条流水线走就不需要把整个系统代码都读懂才能加功能。7.4 部署后的验证清单部署完成后不要急着交付按照下面这个清单逐项验证[ ] 登录页能正常加载后台管理页面能正常打开[ ] 新用户能注册/创建旧用户能正常登录[ ] 设备台账增删改查功能全部可用分页正常[ ] 维修工单整个流程闭环创建→指派→维修→完成[ ] 保养计划的到期提醒能正常触发[ ] 报表图表数据与数据库中的实际数据一致[ ] 跨域配置在生产环境已正确调整页面无404[ ] 数据备份策略已设置mysqldump定时备份8. 实际运行效果与踩坑复盘8.1 数据库时间字段的“8小时时差”与容器时区问题这个问题排查时间最长非常有代表性。现象是设备新增时提交的create_time在页面上显示比当前时间快了8个小时后来换成Docker部署又变成慢了8个小时。根本原因就是两端时区不一致本机MySQL8.0默认使用系统时区Docker容器里默认UTC时间。解决方式已经在第5部分写了Docker启动加TZAsia/ShanghaiJDBC连接串加serverTimezoneAsia/Shanghaispring.jackson.time-zone: Asia/Shanghai三处统一。记住这个三处统一的经验时间问题基本就根除了。8.2 前端组件状态刷新不及时的处理维修工单完成回填后维修工列表页的“待处理”数量应该同步减少。但最初实现时发现从“待处理”Tab切到“已完成”Tab再切回“待处理”列表数据已经是更新过的最新结果。然而如果停留在“待处理”Tab不切换数量不会自动变化。原因是列表数据只在onMounted时请求了一次后续操作没有触发重新查询。解决方案是在Tab切换时重新调用数据加载方法并在操作按钮的成功回调里也主动调用一次刷新。这一步不做用户就会以为操作失败实际上数据已经改了。8.3 大字段查询的性能问题设备列表页最初是直接用selectList查出所有字段包括remark备注字段结果数据量大之后列表接口响应时间增加非常明显。优化方式是用select方法指定查询字段列表页只查必要的列LambdaQueryWrapperDevice wrapper new LambdaQueryWrapper(); wrapper.select(Device::getId, Device::getDeviceName, Device::getDeviceCode, Device::getCategoryId, Device::getLocation, Device::getStatus) // ... 其他条件空白描述和备注字段只在详情接口中单独查询。这类优化改一行就能生效但很多人容易忽略。8.4 一个值得长期保留的习惯写技术笔记运行整套项目下来最大的感触是写技术笔记比写业务代码更重要。遇到过的每个坑第一时间记录现象、原因、解决方案三个月后再看就是一部完整的避坑指南。项目最终的部署手册、排错记录很大程度上就是从这些零散笔记整理而来的。9. 这套系统的局限与可以继续扩展的方向9.1 当前版本已知的局限任何系统都有边界这套系统也不例外。目前明确的局限有三个没有引入Redis做缓存设备列表在高并发场景下会频繁查询MySQL但从中小企业内部系统的并发量来看完全够用。没有做多租户隔离如果多个分公司或子公司都要独立管理设备需要引入租户ID字段做数据隔离。文件上传功能没有接入OSS或MinIO设备照片等附件直接存储在本地磁盘服务器硬盘故障时会丢文件。9.2 可以扩展的几个方向如果要把这套系统继续做下去优先级比较高的扩展方向是加入设备巡检模块配合手机端实现扫码巡检。对接企业微信/钉钉工单指派和保养提醒直接推送到负责人钉钉会话。嵌入RFID或二维码标签给每台物理设备贴上唯一标识码手机扫码即可查看设备档案和维修记录。增加设备全生命周期成本分析报表从采购、维修、保养、能耗多维度统计单台设备的综合运营成本为设备更新换代决策提供数据依据。引入消息队列处理维修通知类异步任务避免在请求线程里同步发送消息阻塞接口响应。9.3 从项目中学到什么做这个项目的过程中最大的收获不只是把SpringBoot2、Vue3、MyBatis-Plus这些技术栈跑通而是建立起一套完整的项目管理视角——拿到一个业务需求后先抽象核心流程再设计数据表结构接着定接口协议然后才动手写代码。很多初学者一上来就建表写CRUD做着做着发现字段不够用、状态流转乱套被迫推翻重来。这套系统从需求分析、数据库设计到前后端分离实现、部署文档输出走完了软件开发的标准生命周期这套方法论可以迁移到任何类型的管理系统上。项目代码和文档都整理妥当拿到手后建议按文档先把环境跑通再根据自己实际业务场景修改字段和流程。有不清楚的地方多看日志、多打断点、多翻文档系统的边界和可能性都在代码里保留着空白。
返回列表