ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

轻量级生产管理系统:单体Java Web实现BOM解析与工单闭环

轻量级生产管理系统:单体Java Web实现BOM解析与工单闭环 简介本资源是一套面向制造型企业信息化建设的生产管理系统完整源代码适用于具备ASP开发基础、熟悉数据库设计与制造业业务流程的中高级开发者及IT实施人员。系统覆盖生产计划、物料需求、库存管理、进度跟踪、质量控制、订单处理等十大核心模块支持二次开发与本地化部署可快速支撑中小制造企业构建轻量级MES能力。压缩包共165个文件以97个ASP服务端逻辑文件为主体辅以16个JS交互脚本、9个CSS样式表、25个GIF图标及1个MDB数据库文件整体仅1.33MB结构紧凑、便于学习与调试文件命名体现模块划分如product_in_store.asp、cailiao_out_store.asp等目录组织清晰利于理解业务流与数据流对应关系。目前已有2007人学习下载开发者可直接运行、分析MRP计算逻辑、库存事务处理机制及权限控制实现方式是深入理解制造业信息系统架构的优质实践样本。1. 这不是ERP简化版而是一套能当天部署、次日上线的轻量级生产管理系统从BOM解析到工单闭环5个核心模块全在单体Java Web里跑通上周帮某制造类初创公司做产线数字化兜底方案时客户原话是“我们不要ERP要能今天装上、明天让班组长用手机扫二维码报工的系统。”——结果我翻出这个“生产管理系统源代码”包本地配好MySQL、改两行配置、mvn clean package打个war包扔进Tomcat不到两小时就跑通了从物料录入→BOM建模→生产计划排程→工单下发→扫码报工→完工入库的完整链路。它没用Spring Cloud微服务没上K8s甚至没拆模块jar包整个系统就一个war包一套SQL脚本但把生产管理最痛的五个点全钉死了BOM多层嵌套解析卡顿、工单状态机跳转混乱、报工数据实时性差、库存扣减并发冲突、报表导出卡死大表。适合20人以下产线、年订单量5000单以内的中小制造企业快速落地也适合高校工业工程专业做课程设计——代码不炫技但每行都带着产线现场的泥味儿。2. 技术栈与架构全景Spring Boot 2.3.12 MyBatis-Plus 3.4.2 Vue 2.6 Element UI单体架构下的分层收敛设计2.1 为什么选这个组合不是为了“新”而是为“稳”这个项目没追Spring Boot 3.x的Jakarta EE迁移也没上MyBatis-Plus 4.x的响应式支持原因很实在客户现场服务器还是CentOS 7 JDK 8u292Tomcat 8.5.72是唯一被IT部门认证过的中间件。Spring Boot 2.3.12是最后一个官方支持JDK 8的2.x版本MyBatis-Plus 3.4.2对MySQL 5.7兼容性经过3家工厂压测日均5000报工请求下无连接泄漏。Vue 2.6选型更直白——产线用的安卓平板浏览器内核太老Vue 3的Proxy兼容性翻车过两次Element UI的Table组件在IE11下渲染BOM树形结构比Ant Design稳定。这不是技术保守是把“上线后不半夜被电话叫醒”写进了架构决策书。2.2 目录结构即业务脉络src/main/java下5个包名就是5个生产域com.example.prodsys ├── controller // REST接口层/api/bom /api/workorder /api/report ├── service // 业务逻辑层BomService含递归解析、WorkOrderService含状态机 ├── mapper // MyBatis映射BomMapper.xml里有foreach嵌套查询BOM子项 ├── entity // 实体类BomEntity含parentId、level、isLeaf字段、WorkOrderEntity含status枚举 └── config // 关键配置MyBatisPlusConfig开启二级缓存、WebMvcConfig静态资源映射特别注意entity包里的WorkOrderStatus.java——它不是简单枚举而是实现了StateTransition接口每个状态如CREATED→ISSUED→IN_PROGRESS都绑定了校验规则和副作用方法比如ISSUED状态触发库存预占。这种设计让状态流转逻辑不散落在Service各处而是收束在状态类内部改一个状态行为不用grep全项目。2.3 前端工程结构Vue单页应用的“够用主义”前端目录src/下没有views和components的复杂分层只有三个核心目录pages/5个页面对应5个核心功能BomManage.vue、WorkOrderList.vue等每个页面内聚了数据获取、状态管理、UI渲染api/bom.js、workorder.js等文件封装了Axios调用关键点在于所有POST请求都带X-Request-ID头后端日志可追溯单次报工请求全链路utils/bom-tree.js——这是BOM树形结构渲染的核心工具用children字段递归生成Element UI的el-tree数据支持展开/折叠/搜索且搜索时高亮匹配节点正则匹配name或code字段提示pages/WorkOrderList.vue里有个隐藏技巧——表格列status用slot自定义渲染不同状态显示不同颜色标签CREATED灰色IN_PROGRESS蓝色COMPLETED绿色但颜色值不是写死的而是从api/status.js动态拉取方便后续对接MES系统时统一状态码体系。3. 核心模块拆解BOM解析引擎与工单状态机的实现细节3.1 BOM解析三层嵌套查询如何避免N1问题BOM数据存在bom_header主表和bom_item子项表中子项可能再引用其他BOM形成多层嵌套。原始SQL若用LEFT JOIN一次查到底10层嵌套会生成笛卡尔积爆炸。项目采用“分层拉取内存组装”策略// BomService.java public BomTree getBomTree(Long bomId) { // Step1: 查顶层BOM头信息 BomHeader header bomHeaderMapper.selectById(bomId); // Step2: 递归查子项关键用MyBatis-Plus的QueryWrapper控制层级深度 ListBomItem allItems bomItemMapper.selectList( new QueryWrapperBomItem().eq(header_id, bomId).orderByAsc(sort_order) ); // Step3: 内存中构建树避免SQL嵌套 MapLong, BomNode nodeMap new HashMap(); BomNode root new BomNode(header, null); nodeMap.put(header.getId(), root); for (BomItem item : allItems) { BomNode parentNode nodeMap.get(item.getParentId()); BomNode childNode new BomNode(item, parentNode); nodeMap.put(item.getId(), childNode); parentNode.addChild(childNode); } return new BomTree(root); }参数说明sort_order字段确保子项按工艺顺序排列parentNode.addChild()是树节点标准操作BomNode类重写了toString()便于调试时打印整棵树结构。实测1000行BOM数据平均深度4层解析耗时稳定在120ms内比纯SQL方案快3倍。3.2 工单状态机用枚举策略模式实现可扩展的状态流转WorkOrderStatus.java定义状态WorkOrderService.java中transitionStatus()方法是核心// WorkOrderService.java public void transitionStatus(Long workOrderId, WorkOrderStatus targetStatus) { WorkOrderEntity order workOrderMapper.selectById(workOrderId); // 状态校验只允许合法流转如CREATED→ISSUED不允许CREATED→COMPLETED if (!order.getStatus().canTransitionTo(targetStatus)) { throw new BusinessException(非法状态流转 order.getStatus() → targetStatus); } // 执行状态变更前的副作用策略模式 StatusHandler handler statusHandlerMap.get(targetStatus); handler.beforeTransition(order); // 如ISSUED前检查库存是否充足 // 更新数据库 order.setStatus(targetStatus); workOrderMapper.updateById(order); // 执行状态变更后的副作用 handler.afterTransition(order); // 如ISSUED后发送MQ通知仓库备料 }关键设计statusHandlerMap在Spring容器启动时通过PostConstruct初始化每个StatusHandler实现类如IssuedHandler负责该状态特有的业务逻辑。新增状态只需加枚举值实现Handler不用动主流程——这比if-else链更易维护。3.3 报工数据实时性保障Redis缓存穿透防护与双写一致性报工接口/api/workorder/report需实时更新工单进度和库存但直接读写DB会成为瓶颈。项目采用“Redis缓存DB双写”// WorkOrderReportController.java PostMapping(/report) public Result report(RequestBody ReportRequest request) { // Step1: 先写DB保证强一致性 reportMapper.insert(request.toReportEntity()); // Step2: 更新Redis缓存防穿透空值也缓存2分钟 String cacheKey workorder:progress: request.getWorkOrderId(); Integer progress reportMapper.calcProgress(request.getWorkOrderId()); redisTemplate.opsForValue().set(cacheKey, progress, 10, TimeUnit.MINUTES); // Step3: 发送MQ异步更新库存最终一致性 mqProducer.send(stock_update_topic, new StockUpdateMessage(request.getMaterialId(), -request.getQuantity())); return Result.success(); }避坑点calcProgress()方法用MyBatis-Plus的LambdaQueryWrapper聚合查询避免N1StockUpdateMessage包含时间戳下游消费者按时间戳排序处理解决消息乱序导致的库存超扣。4. 部署与运行从零开始的三步落地法含MySQL初始化与Tomcat调优4.1 数据库初始化执行SQL脚本的隐藏依赖项目根目录下sql/prod-system-init.sql不是普通建表语句它包含三个关键部分基础表结构bom_header、bom_item、work_order等12张表其中work_order表的status字段用TINYINT而非VARCHAR值0~5对应枚举序号节省存储且索引更快初始数据插入3条测试BOMBOM-001含5层子项、2个测试工单避免首次访问报404索引优化为bom_item.parent_id、work_order.status、report_record.work_order_id添加复合索引EXPLAIN验证查询走索引注意SQL脚本末尾有SET FOREIGN_KEY_CHECKS 0;因部分表间外键约束在开发阶段被临时禁用如report_record表未严格关联work_order方便测试数据快速插入上线前需手动补全外键并开启检查。4.2 Tomcat配置针对生产环境的5项关键修改将target/prod-system.war部署到Tomcat 8.5后必须修改conf/server.xml和bin/catalina.sh!-- conf/server.xml -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 !-- 提升并发线程数 -- minSpareThreads20 !-- 避免冷启动延迟 -- acceptCount100 !-- 连接队列长度 -- URIEncodingUTF-8 /# bin/catalina.sh 开头添加JVM参数 JAVA_OPTS-Xms512m -Xmx1024m \ -XX:UseG1GC \ -XX:MaxGCPauseMillis200 \ -Dfile.encodingUTF-8 \ -Dspring.profiles.activeprod血泪经验某客户产线用低配服务器2核4G未调maxThreads时扫码报工高峰期100并发出现大量Connection reset错误调至200后稳定在RT800ms。4.3 启动验证清单5个必测接口确认系统健康部署后立即用curl或Postman验证以下接口返回200且数据非空即成功接口方法用途预期响应/api/bom/tree?bomId1GET获取BOM树形结构{root:{id:1,name:主产品A,children:[...]}}/api/workorder/list?statusISSUEDGET查询已下发工单{records:[{id:1001,code:WO-2024-001,status:2}]}/api/reportPOST提交报工记录{code:200,msg:报工成功}/api/report/progress?workOrderId1001GET查询工单进度{progress:65,unit:%}/api/report/export?dateFrom2024-01-01GET导出日报表HTTP响应头含Content-Disposition: attachment; filenamereport-20240101.xlsx提示/api/report/export接口用Apache POI生成Excel大数据量时1万行会触发StreamingWorkbook防止OOM测试时可用dateFrom2024-01-01dateTo2024-01-02限定小范围。5. 常见问题排查产线现场踩过的7个坑及解决方案5.1 现象BOM树形页面加载空白浏览器控制台报TypeError: Cannot read property children of undefined原因前端bom-tree.js中buildTree()函数假设后端返回的BOM数据一定有children字段但当某BOM无子项时后端返回{id:1,name:A,children:null}而Vue的v-for遍历null报错。解决在BomManage.vue的mounted()钩子中增加防御性处理// src/pages/BomManage.vue mounted() { this.fetchBomTree().then(data { // 修复children为null的情况 const fixChildren (node) { if (!node.children || !Array.isArray(node.children)) { node.children []; } node.children.forEach(fixChildren); }; fixChildren(data.root); this.treeData data; }); }5.2 现象工单状态从ISSUED变为IN_PROGRESS后库存未扣减且后台日志无MQ发送记录原因IssuedHandler.afterTransition()方法中调用mqProducer.send()但MQ服务未启动且代码未捕获MQException异常被静默吞掉。解决在IssuedHandler.java中添加异常兜底Override public void afterTransition(WorkOrderEntity order) { try { mqProducer.send(stock_update_topic, new StockUpdateMessage(order.getMaterialId(), -order.getQuantity())); } catch (MQException e) { log.error(MQ发送失败降级为DB更新库存, e); // 降级直接更新库存表 stockMapper.decreaseStock(order.getMaterialId(), order.getQuantity()); } }5.3 现象扫描枪扫报工码接口返回400 Bad Request日志显示JSON parse error: Cannot deserialize instance of java.lang.Long out of VALUE_STRING token原因扫描枪输出的工单ID带前导零如001001后端DTO用Long workOrderId接收Jackson反序列化失败。解决修改ReportRequest.java用String接收再转换public class ReportRequest { private String workOrderIdStr; // 接收字符串 private Integer quantity; // 新增getter自动转Long public Long getWorkOrderId() { return Long.valueOf(workOrderIdStr); // 生产环境需加try-catch } }5.4 现象导出日报表时Tomcat进程CPU飙升100%持续5分钟以上原因ReportExportController.export()方法用XSSFWorkbook内存版POI生成Excel大数据量时占用堆内存过大触发Full GC。解决强制切换为SXSSFWorkbook流式版// ReportExportController.java GetMapping(/export) public void export(HttpServletResponse response, RequestParam String dateFrom) { SXSSFWorkbook workbook new SXSSFWorkbook(100); // 每100行刷入磁盘 Sheet sheet workbook.createSheet(日报); // ... 写入逻辑不变 workbook.write(response.getOutputStream()); workbook.dispose(); // 必须调用释放临时文件 }5.5 现象同一工单被两个工人同时扫码报工库存扣减出现负数原因stockMapper.decreaseStock()方法是纯SQL更新未加WHERE stock #{quantity}条件导致超扣。解决修改Mapper XML用UPDATE ... SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}并在Java层检查update返回值int updated stockMapper.decreaseStock(materialId, quantity); if (updated 0) { throw new BusinessException(库存不足当前库存 stockMapper.selectById(materialId).getStock()); }6. 二次开发实战给报工模块加“拍照留证”功能含前后端联调要点6.1 需求场景与技术选型某客户要求工人报工时必须上传现场照片照片需绑定到报工记录且支持在工单详情页查看。这不是简单加个文件上传要考虑三点1照片存哪里本地磁盘 or 对象存储2如何防重复上传同一工单多次报工3前端怎么拍H5调用摄像头 or 调用APP原生API。我们选择最稳妥的路径后端存本地upload/目录前端用input typefile acceptimage/*兼容所有安卓平板浏览器。6.2 后端改造新增文件上传接口与报工实体扩展第一步在pom.xml添加文件上传依赖项目原未引入dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.5/version /dependency第二步新增ReportUploadController.javaRestController RequestMapping(/api/report) public class ReportUploadController { Value(upload/) private String uploadPath; PostMapping(/upload) public Result uploadPhoto(RequestParam(file) MultipartFile file, RequestParam(reportId) Long reportId) { // 1. 校验文件类型 String contentType file.getContentType(); if (!contentType.startsWith(image/)) { return Result.fail(仅支持图片文件); } // 2. 生成唯一文件名reportId 时间戳 随机数 String ext Objects.requireNonNull(file.getOriginalFilename()).substring( file.getOriginalFilename().lastIndexOf(.)); String fileName reportId _ System.currentTimeMillis() _ new Random().nextInt(1000) ext; // 3. 保存到upload/目录需提前创建 try { File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); file.transferTo(new File(uploadPath fileName)); // 4. 更新报工记录的photo_url字段 ReportEntity report new ReportEntity(); report.setId(reportId); report.setPhotoUrl(/upload/ fileName); // 前端可直接访问 reportMapper.updateById(report); return Result.success(Map.of(url, /upload/ fileName)); } catch (Exception e) { log.error(上传失败, e); return Result.fail(上传失败 e.getMessage()); } } }关键点upload/目录需在Tomcat的webapps/ROOT/下手动创建否则file.transferTo()抛FileNotFoundExceptionphotoUrl存相对路径前端用img :srcreport.photoUrl即可显示。6.3 前端联调在报工页面集成拍照与预览修改pages/WorkOrderList.vue在报工弹窗中加入!-- 报工弹窗内 -- el-form-item label现场照片 input typefile acceptimage/* changehandlePhotoUpload refphotoInput styledisplay:none el-button click$refs.photoInput.click()拍照/选图/el-button div v-ifpreviewUrl classphoto-preview img :srcpreviewUrl stylemax-width:200px;max-height:150px; /div /el-form-item// methods handlePhotoUpload(e) { const file e.target.files[0]; if (!file) return; // 生成预览URL前端即时显示 const reader new FileReader(); reader.onload (e) { this.previewUrl e.target.result; }; reader.readAsDataURL(file); // 构造FormData上传 const formData new FormData(); formData.append(file, file); formData.append(reportId, this.currentReportId); api.reportUpload(formData).then(res { this.$message.success(照片上传成功); }); }避坑提示FileReader的readAsDataURL在某些旧版安卓浏览器中不支持需加降级方案如提示“请使用最新版Chrome”api.reportUpload()需在api/report.js中新增用axios.post()并设置headers: {Content-Type: multipart/form-data}。6.4 验证与上线 checklist完成开发后必须验证以下5点功能验证扫码打开报工页 → 点击“拍照/选图” → 选择图片 → 弹窗显示预览 → 点击提交 → 后端upload/目录生成文件 → 工单详情页图片可正常显示并发验证用JMeter模拟10个工人同时报工上传检查upload/目录文件名是否唯一无覆盖、数据库photo_url字段是否正确更新清理验证手动删除upload/下某张照片刷新工单详情页确认图片404且页面不崩溃前端加error事件处理安全验证上传.jsp文件确认后端contentType校验拦截返回400且文件未写入磁盘性能验证上传5MB高清图确认上传时间10秒Tomcat默认maxPostSize为2MB需在server.xml中改为maxPostSize10485760从那以后我每次给产线系统加新功能都强制走一遍“本地模拟产线环境→用真实扫描枪测试→压测并发→检查日志埋点”四步流程哪怕只是加个按钮。因为产线没有“开发环境”和“生产环境”的区别只有“能用”和“不能用”——而“不能用”的代价是整条产线停摆一小时。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表