上个月我帮一家机械加工厂做了套车间管理系统技术栈就是标题里这套——SpringBoot后端Vue前端MySQL前后端分离拿来改改就能用。这里把整个项目的设计思路、核心代码、数据库结构和部署排错过程完整梳理一遍希望能让打算做类似信息管理系统的朋友少走弯路。这套系统能做的事很直接工单下达、工序报工、物料出入库、设备台账、人员管理、看板统计。车间的班组长在电脑或平板上操作生产主管可以实时看到每张工单走到哪个工序月底不用再翻Excel对账。适合中小型工厂的车间数字化改造也适合想快速搭一套管理类Web系统来做毕业设计、课程项目的同学参考。1. 车间管理到底缺什么这套系统要解决的几个核心问题1.1 纸质工单和Excel表格撑不起的现场管理很多中小型工厂的车间管理还停留在纸质工单阶段。业务员接单后打印一张生产工单流转到车间主任手里排产工人做完一道工序就手写记录。这种模式有两个致命问题第一单据容易丢、字迹容易糊月底对账的时候经常出现“我明明做了但记录找不到了”的扯皮第二管理者想了解当前生产进度必须跑到车间问一圈或者等班组长口头汇报效率极低。我之前接触的那家机械加工厂就是这样车间里堆着厚厚一沓工单生产主管每天下午要花一个小时统计当天的完成量。引入这套系统之后工人报工的时候在平板或电脑上点几下数据实时入库主管打开看板页面就能看到每个订单的完成百分比、剩余工序、延期风险再也没有出现过月底对账对不上的情况。所以这套系统的核心价值不是“做个网页展示一下数据”而是把车间里最关键的几个环节——工单流转、工序报工、物料出入库、人员设备状态——从纸质流程变成数字流程让数据自己说话。1.2 谁在什么设备上做了什么工序数据追踪的空白车间管理的另一个痛点是过程追踪难。当一张工单出现质量问题时管理者往往很难快速定位到是哪个班组做的、用哪台设备加工、什么时候报工的、当时的质检记录是什么。针对这个需求系统设计了完整的工序报工链路。每张工单被拆成多道工序每道工序报工的时候记录操作人、设备编号、开始时间、结束时间、合格数量、不合格数量。这样一旦出现质量问题翻一下报工记录几秒钟就能锁定责任环节。我设计这套系统的时候把“工单—工序—报工记录”设计成三级数据模型工单表只保存订单维度的信息每道工序在工序表中独立存在每次报工插入一条记录不做任何覆盖式更新。这种设计的好处是审计性强每一笔报工都留痕坏处是数据量会增长较快所以我在开发时就给报工表加了按工单号的分区索引实测数据量到几十万条的时候查询依然流畅。1.3 对账、统计、报表月底最头疼的收尾工作如果问车间主任最烦什么“月底对账”绝对排前三。物料账、工时账、完成量账三个本子如果数据对不上整晚都要加班查原因。这套系统的报表模块就是为了解决这个问题。系统按天、按周、按工单维度自动汇总产量、工时、物料消耗、设备运行时长等指标生产主管打开报表页面选一下时间范围就能看到。技术上我用的是定时任务统计表的方式每天凌晨自动跑一次汇总任务把前一天的数据写入统计表。这样看报表的时候不必实时扫描明细表即使数据量到了百万级报表打开速度依然在1秒以内。后来我又加了一个导出功能支持把报表导出成Excel很多管理人员习惯用Excel做二次分析这个功能他们反馈非常实用。2. 技术栈为什么这样选SpringBootVueMySQL的取舍思路2.1 后端框架SpringBoot为什么是当前中小系统的最优解先说为什么不用更传统的SSHStrutsSpringHibernate或者更轻量的PHP方案。SSH的问题在于配置复杂XML到处都是现在招人都不好招PHP虽然上手快但后期做复杂业务逻辑、写单元测试、做接口管理都比较别扭。SpringBoot的最大优势是约定优于配置内嵌Tomcat打成一个jar包就能跑。开发的时候不用再去折腾web.xml、applicationContext.xml这些配置文件一个application.yml就能搞定大部分配置。而且SpringBoot的生态极其成熟Spring Security做权限、MyBatis-Plus做持久层、Redis做缓存都有大量现成方案可以直接接进来遇到问题一搜就有解决方案这一点对于需要快速交付的项目非常重要。2.2 前端框架VueElement UI的组合为什么够用前端我选择Vue而不是React原因很实际Vue对后端出身的开发者友好模板语法接近HTML学习曲线平缓而Element UI组件库提供了现成的表格、表单、对话框、日期选择器做管理端页面基本就是“拼积木”。这里有一个值得说的版本选择项目开发时Vue已经出了Vue 3但很多成熟的开源后台模板比如若依这类还基于Vue 2。考虑到我需要快速交付并且希望项目中可以引用的组件资源最多最终选用了Vue 2 Element UI Vuex Vue Router这套稳定组合。如果现在重新选型我会直接上Vue 3 Element Plus但从功能实现角度Vue 2依然完全够用对于只想“跑起来看效果”的场景更稳妥。2.3 数据库MySQL在车间数据量级下的表现车间管理系统的数据量并没有想象中那么大一台中型加工厂一年产生的工单大约几千张报工记录几十万条。这个量级用MySQL完全没问题不需要动不动就上Oracle或者PostgreSQL更不需要一开始就考虑分布式数据库。MySQL的优势在于简单、稳定、生态好。Navicat、Workbench这些GUI工具让开发和维护都很方便主从复制、全文索引、JSON字段这些功能也都够用。实际项目中我把数据量最大的报工表做了按月分表处理半年下来单表数据量在几十万量级查询基本无压力。2.4 工程整体结构前后端分离后的目录组织项目分成两个独立工程springboot-backend后端和vue-frontend前端。后端按照标准的三层架构组织controller层负责接收请求和参数校验service层写业务逻辑mapper层MyBatis-Plus的mapper负责数据库操作。实体类放在entity包DTO对象放在dto包安全相关代码放在security包工具类放在util包。这个结构虽然不花哨但非常清晰团队协作时每个人负责一个模块不会冲突。前端按照页面功能划分view目录下面按照模块分子目录比如order工单、process工序、material物料、device设备、system系统管理api目录下面按模块封装接口请求方法router目录统一管理路由store目录管理全局状态。这种约定俗成的目录结构让人一看就懂接手项目的成本很低。3. 后端工程拆解鉴权、业务接口与持久层的实现细节3.1 工程初始化与核心依赖后端工程使用Spring Initializr创建核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency这里的核心选择是MyBatis-Plus。它最大价值在于内置了通用Mapper基础的增删改查不需要手写SQL用LambdaQueryWrapper就能完成条件构造LambdaQueryWrapperWorkOrder wrapper new LambdaQueryWrapper(); wrapper.eq(WorkOrder::getOrderNo, orderNo) .in(WorkOrder::getStatus, Arrays.asList(RUNNING, PENDING)) .orderByAsc(WorkOrder::getCreateTime); ListWorkOrder orders workOrderService.list(wrapper);复杂一点的统计SQL比如按班组维度汇总产量就在Mapper上写自定义注解SQL或XML文件。两者结合既保证开发效率又不失灵活性。3.2 登录鉴权我为什么选JWT而不是Session登录鉴权这块现场系统有个明显的特殊性可能同时存在一个登录用户多终端使用的场景比如班组长在车间平板登录、回到办公室又在电脑上登录。如果使用传统Session方案一个账号只允许一个会话在线用户会被频繁顶下线体验非常差。所以我采用了JWTJSON Web Token方案。登录成功后将用户ID和角色信息写入token前端把token存在localStorage每次请求在请求头携带Authorization字段。后端过滤器统一解析token获取当前用户信息并存入ThreadLocal业务代码随时可以取到“当前操作人是谁”。String token request.getHeader(Authorization).replace(Bearer , ); Long userId JwtUtil.parseToken(token).get(userId, Long.class); // 用户信息放入上下文 UserContext.set(userId);JWT方案有个经典坑token无法在服务端主动失效。为了解决这个问题我在用户表加了一个token_expire_time字段修改密码或管理员强制下线时更新这个字段每次请求时检查token生成时间是否早于该时间早于则拒绝。这个方案虽然不如Redis存储token那么严格但省去了一套Redis依赖架构更简单适合这个量级的项目。3.3 工单与工序报工接口业务闭环的核心工单模块的核心接口有四个接口路径功能说明POST /api/order/create创建工单创建时自动拆分工序、生成工单号GET /api/order/page分页查询工单支持按状态、工单号、客户名称筛选POST /api/order/start开始生产工单状态由待生产变为生产中POST /api/order/process/report工序报工记录单道工序的完成情况其中工序报工是整个系统里最重要的接口因为它直接关联生产进度和员工业绩。设计时需要考虑一个业务场景一张工单有多道工序工人不能跳工序报工。例如加工工序还没完成就不能报装配工序。这个约束在数据库层用工序表中的sort_order字段控制后端接口逻辑里校验当前上报的工序编号是否等于工单最大已完成工序编号1。代码实现如下Transactional public void reportProgress(ProcessReportDTO dto) { // 校验当前工单状态 WorkOrder order workOrderService.getById(dto.getOrderId()); if (!RUNNING.equals(order.getStatus())) { throw new BusinessException(工单不在生产状态无法报工); } // 校验工序顺序 ProcessInfo current processService.getById(dto.getProcessId()); ProcessInfo lastCompleted processService.getLastCompleted(dto.getOrderId()); if (current.getSortOrder() ! lastCompleted.getSortOrder() 1) { throw new BusinessException(工序顺序错误请先完成上一道工序); } // 更新工序状态 ProcessReport report new ProcessReport(); report.setOrderId(dto.getOrderId()); report.setProcessId(dto.getProcessId()); report.setReportUser(dto.getUserId()); report.setQualifiedCount(dto.getQualifiedCount()); report.setUnqualifiedCount(dto.getUnqualifiedCount()); report.setStartTime(dto.getStartTime()); report.setEndTime(dto.getEndTime()); processReportService.save(report); // 如果所有工序都完成工单状态改为已完成 if (processService.isAllCompleted(dto.getOrderId())) { order.setStatus(FINISHED); order.setFinishTime(LocalDateTime.now()); workOrderService.updateById(order); } else { // 更新当前进行工序值 order.setCurrentProcessId(current.getId()); workOrderService.updateById(order); } }这里我用了Transactional注解保证整个报工过程的事务性一旦报工插入或者工单状态更新出错数据库自动回滚不会出现“报了工单但状态没更新”这种脏数据。3.4 持久层设计为什么实体用物理外键要慎重数据表之间不可避免存在关联关系比如工单属于某个客户、报工记录属于某道工序。但我在建表的时候刻意没有使用数据库物理外键而是仅保留逻辑外键也就是在代码里维护关联关系。原因很实际物理外键会影响删除和更新操作的性能尤其在数据量上去之后每次父表删除记录时数据库都要检查子表这个开销非常可观。车间管理系统的数据是长期累积的工单和报工记录原则上不能物理删除只做状态上的“作废”处理。用逻辑外键加状态字段的方式既保持数据一致性又避免了物理外键的性能损耗。另外分页查询工单列表时需要连接多张表包括用户表、客户表、设备表。我在专用查询SQL里使用left join一次性把展示需要的关联字段查出来避免循环查询造成的性能问题。4. 前端Vue工程拆解页面路由、状态管理与接口联调4.1 前端目录与路由设计前端工程用Vue CLI创建。路由设计上主框架是一个侧边栏顶栏布局左侧菜单按模块分组生产管理工单、工序、物料管理、设备管理、系统管理用户、角色。路由配置使用动态路由根据用户角色加载对应的菜单项。const routes [ { path: /login, component: () import(/views/login/index.vue) }, { path: /dashboard, component: () import(/layout/index.vue), children: [ { path: workOrder, component: () import(/views/order/workOrder.vue), meta: { title: 工单管理 } }, { path: processReport, component: () import(/views/process/report.vue), meta: { title: 工序报工 } } ] } ];这里有个实际操作细节项目中使用() import()懒加载按需加载路由对应的组件首屏加载速度会快很多。实测下来全量打包后的main.js大约1MB使用懒加载后首屏只加载核心框架部分其他页面在路由跳转时才加载首屏速度提升了一倍多。4.2 axios统一封装与错误处理与后端联调时最怕的就是每个页面各自写一遍请求处理逻辑代码到处都是重复的try-catch。所以我对axios做了一次统一封装import axios from axios; import { ElMessage } from element-ui; // 创建实例 const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL || /api, timeout: 15000 }); // 请求拦截器附加token service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }, error { return Promise.reject(error); }); // 响应拦截器统一处理错误 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.data; }, error { if (error.response) { switch (error.response.status) { case 401: ElMessage.error(登录已过期请重新登录); localStorage.removeItem(token); router.push(/login); break; case 403: ElMessage.error(没有权限执行该操作); break; default: ElMessage.error(error.response.data?.message || 服务器异常); } } return Promise.reject(error); });这个封装解决了好几个实际问题token自动附加、401自动跳转到登录页、后端业务错误码统一提示、网络错误区分提示。所有页面只需调用一行代码就能完成请求。这里有个细节值得提醒经过响应拦截器之后返回的是res.data也就是后端返回的数据体本身。所以页面里写request({...}).then(res { ... res.list ... })的时候直接就能拿到业务数据不需要再res.data.list多加一层容易搞混。4.3 工单看板页面一个典型列表筛选分页的实现工单看板是系统的核心页面。它把工单列表分为“待生产”“生产中”“已完成”三个tab每个tab显示当前状态下所有工单的表格包含工单号、客户、产品、数量、计划开始结束时间、实际进度等信息。这个页面的核心实现就是一个el-table加筛选条件template div classwork-order-page el-form :inlinetrue :modelquery el-form-item label工单号 el-input v-modelquery.orderNo placeholder请输入工单号 / /el-form-item el-form-item label状态 el-select v-modelquery.status placeholder选择状态 el-option label待生产 valuePENDING / el-option label生产中 valueRUNNING / el-option label已完成 valueFINISHED / /el-select /el-form-item el-form-item el-button typeprimary clickloadData查询/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form el-table :dataorderList v-loadingloading el-table-column proporderNo label工单号 width200 / el-table-column propcustomerName label客户 / el-table-column propproductName label产品 / el-table-column propquantity label数量 / el-table-column propcurrentProcessName label当前工序 / el-table-column propprogress label进度 template slot-scopescope el-progress :percentagescope.row.progress / /template /el-table-column el-table-column label操作 width200 template slot-scopescope el-button sizemini clickviewDetail(scope.row.id)详情/el-button el-button sizemini typeprimary clickreport(scope.row.id)报工/el-button /template /el-table-column /el-table el-pagination :current-pagequery.page :page-sizequery.pageSize :totaltotal layouttotal, prev, pager, next current-changehandlePageChange / /div /template页面初始化调用loadData方法从后端请求列表数据。每次筛选条件变化、tab切换、翻页都会触发重新加载数据实时性很好。后端返回的分页结果结构中包含total字段前端用这个值初始化分页组件。4.4 权限控制在前端的落地按钮级别的处理后端已经做了接口级别的权限校验但前端也要配合做菜单和按钮的权限控制。菜单权限通过动态路由实现登录成功后调用/api/user/info获取当前用户的角色和权限点列表前端基于权限点动态生成路由。按钮权限通过自定义指令实现Vue.directive(perm, { inserted(el, binding) { const permissionList store.getters.permissions; const requiredPermission binding.value; if (!permissionList.includes(requiredPermission)) { el.parentNode?.removeChild(el); } } });使用方式就是在按钮上加上v-permorder:create。这样没有创建工单权限的用户在界面上直接看不到新建按钮避免了“有界面但点进去就报错”的尴尬体验。5. MySQL表结构设计从用户表到工单表的全解析5.1 系统核心数据表的角色划分我对表结构做了精简设计去掉了冗余字段核心一共7张表表名用途核心字段sys_user用户表id, username, password, real_name, role_id, statussys_role角色表id, role_name, permission_codeswork_order工单表id, order_no, customer, product, quantity, status, current_process_id, plan_start, plan_end, finish_timeprocess_info工序表id, order_id, process_name, sort_order, status, report_user, report_timeprocess_report报工记录表id, process_id, order_id, user_id, device_id, qualified_count, unqualified_count, start_time, end_timematerial_record物料出入库表id, order_id, material_name, in_out_type, quantity, operator, create_timedevice_info设备台账表id, device_no, device_name, status, last_maintenance_time这7张表基本上覆盖了车间管理最核心的业务域。有必要说明的是工序表和报工记录表分别承担了“工序定义”和“报工事实”两个职责区分得很清楚工序表里一道工序一行数据状态从待处理变成已完成报工记录表则每次报工追加一条记录两人重复报工也只是多一条记录不影响工序状态判断。这种设计保证数据的可追溯性。5.2 工单号为什么不直接用自增ID建表的时候很多新手喜欢把主键ID露给用户看。但实际业务中用户对“工单号”这个业务标识有明确要求一眼看出是哪个订单、哪天创建的、第几张单。所以我在生成工单号时使用了一个工具类public static String generateOrderNo() { return WO new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()) String.format(%03d, (int)(Math.random() * 1000)); }生成效果类似WO20240512143007652。这样做完全避免了并发环境下自增ID冲突的问题同时也方便一线人员在沟通时快速标识工单。数据库主键字段仍然保留自增id但不对业务暴露。5.3 索引设计与数据增长预估车间管理系统最容易出现性能瓶颈的查询场景是按时间范围查询报工记录、按工单号查询工单、按用户查报工汇总。所以索引设计也要围绕这三个场景走。第一张加索引的表是work_order工单号order_no字段加唯一索引状态status字段加普通索引。第二张是process_report核心索引是联合索引(user_id, report_time)因为报表模块经常按人按天统计order_id也需要索引用来做工单维度查询。第三张是material_record对操作时间create_time做索引满足库存汇总查询。一个很值得注意的坑是联合索引的字段顺序不能随意调整。比如(user_id, report_time)这个联合索引如果查询条件是report_time BETWEEN ? AND ?就用不上这个索引必须user_id字段作为等值条件、report_time作为范围条件才能最大程度命中索引。开发时我在报表接口的SQL上加过EXPLAIN验证确保关键查询都走索引避免后期数据量上来后接口突然变慢。5.4 初始化数据与版本管理第一次部署时要初始化管理员账号和基础角色数据。我写了一个全量的init.sql脚本包含建库建表语句、初始数据insert语句放在项目根目录的sql文件夹下。日常开发中的表结构变更则通过V1.x__migration.sql的形式按版本递增管理。使用flyway做自动化迁移是个更好的方案但这个项目考虑到维护成本和团队习惯最终选择了手动脚本管理方式。一个重要的经验是生产环境改表结构前一定备份并且先执行一遍SHOW CREATE TABLE确认当前表结构再写差值更新脚本。我遇到过两次因为脚本重复执行导致表结构错乱的问题后来养成了在每次执行迁移脚本前检查information_schema中表结构的习惯。6. 从源码到跑起来环境准备、启动步骤与排错记录6.1 环境版本匹配清单这套系统想要本地运行成功环境版本必须先对上。我整理了一份实测过匹配良好的版本清单软件推荐版本说明JDK1.8SpringBoot 2.7.x最高支持稳定Maven3.6依赖管理Node.js14.x 或 16.xVue 2项目在18上可能会有opencollective相关告警但不影响构建npm包管理器推荐用yarn或pnpm安装依赖更快MySQL5.7 或 8.0两种版本项目都兼容注意字符集用utf8mb4数据库GUINavicat / Workbench导入初始化脚本JDK版本这里特别强调很多人装了JDK 17跑SpringBoot 2.7项目问题不大但某些老版本依赖可能因为模块化限制报错。稳定起见直接用JDK 8最稳妥不用追求新。6.2 后端启动步骤与常见报错后端启动只有三步。第一步修改application.yml里的数据库账号密码和连接地址第二步先执行init.sql初始化数据库第三步运行mvn spring-boot:run或把项目打包成jar文件执行java -jar。spring: datasource: url: jdbc:mysql://localhost:3306/workshop_manage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456常见报错一数据库无法连接提示Access denied for user。原因基本都是账号密码错误或者初始化脚本没有执行导致数据库不存在。常见报错二端口被占用后端默认配置了8080端口如果本机已有服务占用把yml里的server.port改成8081即可。常见报错三启动时报java.sql.SQLException: The server time zone value Öйú±ê׼ʱ¼ä这是因为MySQL时区与时区失效需要在url上加serverTimezoneAsia/Shanghai这也是上面配置里为什么特别标注时区的原因。6.3 前端启动步骤与跨域处理前端启动命令是cd vue-frontend yarn install yarn dev。yarn install之后如果出现node-sass相关的报错多半是Node版本不对。解决方案有两种降低Node版本到14或者把sass-loader的版本降级到7.x。我建议直接用新方案把scss变量相关代码替换为原生CSS完全绕开node-sass这个极易出问题的依赖。跨域问题也是前后端联调时的热门报错点。浏览器访问localhost:8080前端时向localhost:8080后端发送请求浏览器会因同源策略拦截。解决办法是在后端配置一个全局CORS过滤器Configuration public class CorsConfig { Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsWebFilter(source); } }更推荐的方案是生产环境用Nginx做反向代理把前端/api请求转发到后端服务这样浏览器看到的是同源请求不需要额外配置CORS。开发阶段也可以在前端配置devServer的proxy代理。6.4 生产部署Nginx反代与后端jar包生产部署只需要两台服务器应用服务器上跑后端jar包Web服务器上跑Nginx托管前端静态文件。前端打包命令是yarn build生成dist目录把它放到/var/www/workshop目录下。Nginx配置核心部分如下server { listen 80; server_name your-domain; root /var/www/workshop; location / { try_files $uri $uri/ /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; } }这段配置里最关键的是try_files $uri $uri/ /index.html它解决的是Vue路由模式下的刷新404问题。如果不加这一行用户访问/dashboard页面后按F5刷新Nginx会去找磁盘上是否存在对应路径的文件找不到就返回404。加上try_files之后所有找不到的路径都会回退到index.html交给前端路由接管。后端jar包建议用nohup java -jar workshop-admin.jar /dev/null 21 方式后台启动或者配置成systemd服务。还需要在服务器防火墙中放行80端口和8080端口。7. 项目落地时踩过的坑从并发报工到部署刷新7.1 并发报工导致的重复更新问题系统上线第三天就遇到一个实际问题两个工人几乎同时提交报工请求结果返回了同一组校验结果最终工序表被覆盖成了一个不合理的状态。原因很明显报工接口存在并发场景工人操作平板时习惯连续点几次提交按钮后端同一张工序记录被两个线程同时读取和修改。解决方案是在工序表加一个版本号字段version更新时使用乐观锁UPDATE process_info SET status FINISHED, version version 1 WHERE id #{id} AND version #{oldVersion}更新影响行数为0时说明版本号不匹配返回“操作过于频繁请刷新后重试”。这样处理之后再没出现过并发覆盖的报工记录。MyBatis-Plus对乐观锁有现成的Version注解支持配置一个乐观锁插件即可非常方便。7.2 MySQL连接时区引发的8小时时间差有一次客户反馈工单的创建时间比实际时间早了8小时。排查后发现是MySQL连接串的session时区没有指定数据库默认使用的是系统时区而Java侧的LocalDateTime默认用的是JVM时区两者不一致就产生了时间差。修正方案就是在jdbc连接串上显式加上serverTimezoneAsia/Shanghai同时在MySQL端执行SET time_zone08:00。还有一个与此相关的坑开发环境一切正常部署到生产环境后时间又对不上后来排查才发现是生产服务器的系统时区是UTC。解决方案统一在jdbc连接串和JVM启动参数里显式指定时区不要依赖服务器默认配置。7.3 Nginx刷新404与前端路由模式的坑上文提到刷新404问题这里再多说一句如果你的前端用了history路由模式Nginx的try_files配置必须加上。另外还有一个容易被忽视的点如果前端项目部署在子路径下比如http://ip/workshop/那么Vue的路由base要设置为/workshop/所有前端资源路径都得加这个前缀不然打包出来的文件无法加载。我建议在没有特殊需求时前端项目直接部署在域名根路径Nginx配置简洁问题也少。如果非要子路径部署就需要在打包时配置publicPath这个参数非常隐蔽很多人死活打不开页面就是因为没设置这个。7.4 这套系统可以做的扩展方向系统基础功能稳定后我在实际使用中明显感到有几个方向值得继续完善一个是消息通知。当工单状态流转、设备报修、库存低于阈值时通过WebSocket或短信推送给相关人员能有效减少管理者的盯盘压力。另一个是移动端适配。目前系统在平板上用是没问题但手机端横竖屏体验还有优化空间可以考虑改造为响应式布局或单独开发小程序。还有一个方向是设备数据对接如果工厂的设备具备数据采集能力可以通过Modbus或OPC UA协议接入PLC数据让设备状态、运行时长自动上报减少人工录入的误差和成本。我个人的体会是做这类管理系统技术难点其实不在框架而在于把业务流程理解透把数据关系设计好。报工顺序约束、工单状态流转、权限层级控制这些业务规则的准确与否最终决定了系统能不能真正用起来。希望这份源码级拆解能帮到正在做类似项目的你。