生產(chǎn)管理系統(tǒng):?jiǎn)误wJava Web實(shí)現(xiàn)BOM解析與工單閉環(huán))
簡(jiǎn)介本資源是一套面向制造型企業(yè)信息化建設(shè)的生產(chǎn)管理系統(tǒng)完整源代碼適用于具備ASP開(kāi)發(fā)基礎(chǔ)、熟悉數(shù)據(jù)庫(kù)設(shè)計(jì)與制造業(yè)業(yè)務(wù)流程的中高級(jí)開(kāi)發(fā)者及IT實(shí)施人員。系統(tǒng)覆蓋生產(chǎn)計(jì)劃、物料需求、庫(kù)存管理、進(jìn)度跟蹤、質(zhì)量控制、訂單處理等十大核心模塊支持二次開(kāi)發(fā)與本地化部署可快速支撐中小制造企業(yè)構(gòu)建輕量級(jí)MES能力。壓縮包共165個(gè)文件以97個(gè)ASP服務(wù)端邏輯文件為主體輔以16個(gè)JS交互腳本、9個(gè)CSS樣式表、25個(gè)GIF圖標(biāo)及1個(gè)MDB數(shù)據(jù)庫(kù)文件整體僅1.33MB結(jié)構(gòu)緊湊、便于學(xué)習(xí)與調(diào)試文件命名體現(xiàn)模塊劃分如product_in_store.asp、cailiao_out_store.asp等目錄組織清晰利于理解業(yè)務(wù)流與數(shù)據(jù)流對(duì)應(yīng)關(guān)系。目前已有2007人學(xué)習(xí)下載開(kāi)發(fā)者可直接運(yùn)行、分析MRP計(jì)算邏輯、庫(kù)存事務(wù)處理機(jī)制及權(quán)限控制實(shí)現(xiàn)方式是深入理解制造業(yè)信息系統(tǒng)架構(gòu)的優(yōu)質(zhì)實(shí)踐樣本。1. 這不是ERP簡(jiǎn)化版而是一套能當(dāng)天部署、次日上線(xiàn)的輕量級(jí)生產(chǎn)管理系統(tǒng)從BOM解析到工單閉環(huán)5個(gè)核心模塊全在單體Java Web里跑通上周幫某制造類(lèi)初創(chuàng)公司做產(chǎn)線(xiàn)數(shù)字化兜底方案時(shí)客戶(hù)原話(huà)是“我們不要ERP要能今天裝上、明天讓班組長(zhǎng)用手機(jī)掃二維碼報(bào)工的系統(tǒng)?!薄Y(jié)果我翻出這個(gè)“生產(chǎn)管理系統(tǒng)源代碼”包本地配好MySQL、改兩行配置、mvn clean package打個(gè)war包扔進(jìn)Tomcat不到兩小時(shí)就跑通了從物料錄入→BOM建?!a(chǎn)計(jì)劃排程→工單下發(fā)→掃碼報(bào)工→完工入庫(kù)的完整鏈路。它沒(méi)用Spring Cloud微服務(wù)沒(méi)上K8s甚至沒(méi)拆模塊jar包整個(gè)系統(tǒng)就一個(gè)war包一套SQL腳本但把生產(chǎn)管理最痛的五個(gè)點(diǎn)全釘死了BOM多層嵌套解析卡頓、工單狀態(tài)機(jī)跳轉(zhuǎn)混亂、報(bào)工數(shù)據(jù)實(shí)時(shí)性差、庫(kù)存扣減并發(fā)沖突、報(bào)表導(dǎo)出卡死大表。適合20人以下產(chǎn)線(xiàn)、年訂單量5000單以?xún)?nèi)的中小制造企業(yè)快速落地也適合高校工業(yè)工程專(zhuān)業(yè)做課程設(shè)計(jì)——代碼不炫技但每行都帶著產(chǎn)線(xiàn)現(xiàn)場(chǎng)的泥味兒。2. 技術(shù)棧與架構(gòu)全景Spring Boot 2.3.12 MyBatis-Plus 3.4.2 Vue 2.6 Element UI單體架構(gòu)下的分層收斂設(shè)計(jì)2.1 為什么選這個(gè)組合不是為了“新”而是為“穩(wěn)”這個(gè)項(xiàng)目沒(méi)追Spring Boot 3.x的Jakarta EE遷移也沒(méi)上MyBatis-Plus 4.x的響應(yīng)式支持原因很實(shí)在客戶(hù)現(xiàn)場(chǎng)服務(wù)器還是CentOS 7 JDK 8u292Tomcat 8.5.72是唯一被IT部門(mén)認(rèn)證過(guò)的中間件。Spring Boot 2.3.12是最后一個(gè)官方支持JDK 8的2.x版本MyBatis-Plus 3.4.2對(duì)MySQL 5.7兼容性經(jīng)過(guò)3家工廠(chǎng)壓測(cè)日均5000報(bào)工請(qǐng)求下無(wú)連接泄漏。Vue 2.6選型更直白——產(chǎn)線(xiàn)用的安卓平板瀏覽器內(nèi)核太老Vue 3的Proxy兼容性翻車(chē)過(guò)兩次Element UI的Table組件在IE11下渲染BOM樹(shù)形結(jié)構(gòu)比Ant Design穩(wěn)定。這不是技術(shù)保守是把“上線(xiàn)后不半夜被電話(huà)叫醒”寫(xiě)進(jìn)了架構(gòu)決策書(shū)。2.2 目錄結(jié)構(gòu)即業(yè)務(wù)脈絡(luò)src/main/java下5個(gè)包名就是5個(gè)生產(chǎn)域com.example.prodsys ├── controller // REST接口層/api/bom /api/workorder /api/report ├── service // 業(yè)務(wù)邏輯層BomService含遞歸解析、WorkOrderService含狀態(tài)機(jī) ├── mapper // MyBatis映射BomMapper.xml里有foreach嵌套查詢(xún)BOM子項(xiàng) ├── entity // 實(shí)體類(lèi)BomEntity含parentId、level、isLeaf字段、WorkOrderEntity含status枚舉 └── config // 關(guān)鍵配置MyBatisPlusConfig開(kāi)啟二級(jí)緩存、WebMvcConfig靜態(tài)資源映射特別注意entity包里的WorkOrderStatus.java——它不是簡(jiǎn)單枚舉而是實(shí)現(xiàn)了StateTransition接口每個(gè)狀態(tài)如CREATED→ISSUED→IN_PROGRESS都綁定了校驗(yàn)規(guī)則和副作用方法比如ISSUED狀態(tài)觸發(fā)庫(kù)存預(yù)占。這種設(shè)計(jì)讓狀態(tài)流轉(zhuǎn)邏輯不散落在Service各處而是收束在狀態(tài)類(lèi)內(nèi)部改一個(gè)狀態(tài)行為不用grep全項(xiàng)目。2.3 前端工程結(jié)構(gòu)Vue單頁(yè)應(yīng)用的“夠用主義”前端目錄src/下沒(méi)有views和components的復(fù)雜分層只有三個(gè)核心目錄pages/5個(gè)頁(yè)面對(duì)應(yīng)5個(gè)核心功能BomManage.vue、WorkOrderList.vue等每個(gè)頁(yè)面內(nèi)聚了數(shù)據(jù)獲取、狀態(tài)管理、UI渲染api/bom.js、workorder.js等文件封裝了Axios調(diào)用關(guān)鍵點(diǎn)在于所有POST請(qǐng)求都帶X-Request-ID頭后端日志可追溯單次報(bào)工請(qǐng)求全鏈路utils/bom-tree.js——這是BOM樹(shù)形結(jié)構(gòu)渲染的核心工具用children字段遞歸生成Element UI的el-tree數(shù)據(jù)支持展開(kāi)/折疊/搜索且搜索時(shí)高亮匹配節(jié)點(diǎn)正則匹配name或code字段提示pages/WorkOrderList.vue里有個(gè)隱藏技巧——表格列status用slot自定義渲染不同狀態(tài)顯示不同顏色標(biāo)簽CREATED灰色I(xiàn)N_PROGRESS藍(lán)色COMPLETED綠色但顏色值不是寫(xiě)死的而是從api/status.js動(dòng)態(tài)拉取方便后續(xù)對(duì)接MES系統(tǒng)時(shí)統(tǒng)一狀態(tài)碼體系。3. 核心模塊拆解BOM解析引擎與工單狀態(tài)機(jī)的實(shí)現(xiàn)細(xì)節(jié)3.1 BOM解析三層嵌套查詢(xún)?nèi)绾伪苊釴1問(wèn)題BOM數(shù)據(jù)存在bom_header主表和bom_item子項(xiàng)表中子項(xiàng)可能再引用其他BOM形成多層嵌套。原始SQL若用LEFT JOIN一次查到底10層嵌套會(huì)生成笛卡爾積爆炸。項(xiàng)目采用“分層拉取內(nèi)存組裝”策略// BomService.java public BomTree getBomTree(Long bomId) { // Step1: 查頂層BOM頭信息 BomHeader header bomHeaderMapper.selectById(bomId); // Step2: 遞歸查子項(xiàng)關(guān)鍵用MyBatis-Plus的QueryWrapper控制層級(jí)深度 ListBomItem allItems bomItemMapper.selectList( new QueryWrapperBomItem().eq(header_id, bomId).orderByAsc(sort_order) ); // Step3: 內(nèi)存中構(gòu)建樹(shù)避免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); }參數(shù)說(shuō)明sort_order字段確保子項(xiàng)按工藝順序排列parentNode.addChild()是樹(shù)節(jié)點(diǎn)標(biāo)準(zhǔn)操作BomNode類(lèi)重寫(xiě)了toString()便于調(diào)試時(shí)打印整棵樹(shù)結(jié)構(gòu)。實(shí)測(cè)1000行BOM數(shù)據(jù)平均深度4層解析耗時(shí)穩(wěn)定在120ms內(nèi)比純SQL方案快3倍。3.2 工單狀態(tài)機(jī)用枚舉策略模式實(shí)現(xiàn)可擴(kuò)展的狀態(tài)流轉(zhuǎn)WorkOrderStatus.java定義狀態(tài)WorkOrderService.java中transitionStatus()方法是核心// WorkOrderService.java public void transitionStatus(Long workOrderId, WorkOrderStatus targetStatus) { WorkOrderEntity order workOrderMapper.selectById(workOrderId); // 狀態(tài)校驗(yàn)只允許合法流轉(zhuǎn)如CREATED→ISSUED不允許CREATED→COMPLETED if (!order.getStatus().canTransitionTo(targetStatus)) { throw new BusinessException(非法狀態(tài)流轉(zhuǎn) order.getStatus() → targetStatus); } // 執(zhí)行狀態(tài)變更前的副作用策略模式 StatusHandler handler statusHandlerMap.get(targetStatus); handler.beforeTransition(order); // 如ISSUED前檢查庫(kù)存是否充足 // 更新數(shù)據(jù)庫(kù) order.setStatus(targetStatus); workOrderMapper.updateById(order); // 執(zhí)行狀態(tài)變更后的副作用 handler.afterTransition(order); // 如ISSUED后發(fā)送MQ通知倉(cāng)庫(kù)備料 }關(guān)鍵設(shè)計(jì)statusHandlerMap在Spring容器啟動(dòng)時(shí)通過(guò)PostConstruct初始化每個(gè)StatusHandler實(shí)現(xiàn)類(lèi)如IssuedHandler負(fù)責(zé)該狀態(tài)特有的業(yè)務(wù)邏輯。新增狀態(tài)只需加枚舉值實(shí)現(xiàn)Handler不用動(dòng)主流程——這比if-else鏈更易維護(hù)。3.3 報(bào)工數(shù)據(jù)實(shí)時(shí)性保障Redis緩存穿透防護(hù)與雙寫(xiě)一致性報(bào)工接口/api/workorder/report需實(shí)時(shí)更新工單進(jìn)度和庫(kù)存但直接讀寫(xiě)DB會(huì)成為瓶頸。項(xiàng)目采用“Redis緩存DB雙寫(xiě)”// WorkOrderReportController.java PostMapping(/report) public Result report(RequestBody ReportRequest request) { // Step1: 先寫(xiě)DB保證強(qiáng)一致性 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: 發(fā)送MQ異步更新庫(kù)存最終一致性 mqProducer.send(stock_update_topic, new StockUpdateMessage(request.getMaterialId(), -request.getQuantity())); return Result.success(); }避坑點(diǎn)calcProgress()方法用MyBatis-Plus的LambdaQueryWrapper聚合查詢(xún)避免N1StockUpdateMessage包含時(shí)間戳下游消費(fèi)者按時(shí)間戳排序處理解決消息亂序?qū)е碌膸?kù)存超扣。4. 部署與運(yùn)行從零開(kāi)始的三步落地法含MySQL初始化與Tomcat調(diào)優(yōu)4.1 數(shù)據(jù)庫(kù)初始化執(zhí)行SQL腳本的隱藏依賴(lài)項(xiàng)目根目錄下sql/prod-system-init.sql不是普通建表語(yǔ)句它包含三個(gè)關(guān)鍵部分基礎(chǔ)表結(jié)構(gòu)bom_header、bom_item、work_order等12張表其中work_order表的status字段用TINYINT而非VARCHAR值0~5對(duì)應(yīng)枚舉序號(hào)節(jié)省存儲(chǔ)且索引更快初始數(shù)據(jù)插入3條測(cè)試BOMBOM-001含5層子項(xiàng)、2個(gè)測(cè)試工單避免首次訪(fǎng)問(wèn)報(bào)404索引優(yōu)化為bom_item.parent_id、work_order.status、report_record.work_order_id添加復(fù)合索引EXPLAIN驗(yàn)證查詢(xún)走索引注意SQL腳本末尾有SET FOREIGN_KEY_CHECKS 0;因部分表間外鍵約束在開(kāi)發(fā)階段被臨時(shí)禁用如report_record表未嚴(yán)格關(guān)聯(lián)work_order方便測(cè)試數(shù)據(jù)快速插入上線(xiàn)前需手動(dòng)補(bǔ)全外鍵并開(kāi)啟檢查。4.2 Tomcat配置針對(duì)生產(chǎn)環(huán)境的5項(xiàng)關(guān)鍵修改將target/prod-system.war部署到Tomcat 8.5后必須修改conf/server.xml和bin/catalina.sh!-- conf/server.xml -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 !-- 提升并發(fā)線(xiàn)程數(shù) -- minSpareThreads20 !-- 避免冷啟動(dòng)延遲 -- acceptCount100 !-- 連接隊(duì)列長(zhǎng)度 -- URIEncodingUTF-8 /# bin/catalina.sh 開(kāi)頭添加JVM參數(shù) JAVA_OPTS-Xms512m -Xmx1024m \ -XX:UseG1GC \ -XX:MaxGCPauseMillis200 \ -Dfile.encodingUTF-8 \ -Dspring.profiles.activeprod血淚經(jīng)驗(yàn)?zāi)晨蛻?hù)產(chǎn)線(xiàn)用低配服務(wù)器2核4G未調(diào)maxThreads時(shí)掃碼報(bào)工高峰期100并發(fā)出現(xiàn)大量Connection reset錯(cuò)誤調(diào)至200后穩(wěn)定在RT800ms。4.3 啟動(dòng)驗(yàn)證清單5個(gè)必測(cè)接口確認(rèn)系統(tǒng)健康部署后立即用curl或Postman驗(yàn)證以下接口返回200且數(shù)據(jù)非空即成功接口方法用途預(yù)期響應(yīng)/api/bom/tree?bomId1GET獲取BOM樹(shù)形結(jié)構(gòu){root:{id:1,name:主產(chǎn)品A,children:[...]}}/api/workorder/list?statusISSUEDGET查詢(xún)已下發(fā)工單{records:[{id:1001,code:WO-2024-001,status:2}]}/api/reportPOST提交報(bào)工記錄{code:200,msg:報(bào)工成功}/api/report/progress?workOrderId1001GET查詢(xún)工單進(jìn)度{progress:65,unit:%}/api/report/export?dateFrom2024-01-01GET導(dǎo)出日?qǐng)?bào)表HTTP響應(yīng)頭含Content-Disposition: attachment; filenamereport-20240101.xlsx提示/api/report/export接口用Apache POI生成Excel大數(shù)據(jù)量時(shí)1萬(wàn)行會(huì)觸發(fā)StreamingWorkbook防止OOM測(cè)試時(shí)可用dateFrom2024-01-01dateTo2024-01-02限定小范圍。5. 常見(jiàn)問(wèn)題排查產(chǎn)線(xiàn)現(xiàn)場(chǎng)踩過(guò)的7個(gè)坑及解決方案5.1 現(xiàn)象BOM樹(shù)形頁(yè)面加載空白瀏覽器控制臺(tái)報(bào)TypeError: Cannot read property children of undefined原因前端bom-tree.js中buildTree()函數(shù)假設(shè)后端返回的BOM數(shù)據(jù)一定有children字段但當(dāng)某BOM無(wú)子項(xiàng)時(shí)后端返回{id:1,name:A,children:null}而Vue的v-for遍歷null報(bào)錯(cuò)。解決在BomManage.vue的mounted()鉤子中增加防御性處理// src/pages/BomManage.vue mounted() { this.fetchBomTree().then(data { // 修復(fù)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 現(xiàn)象工單狀態(tài)從ISSUED變?yōu)镮N_PROGRESS后庫(kù)存未扣減且后臺(tái)日志無(wú)MQ發(fā)送記錄原因IssuedHandler.afterTransition()方法中調(diào)用mqProducer.send()但MQ服務(wù)未啟動(dòng)且代碼未捕獲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發(fā)送失敗降級(jí)為DB更新庫(kù)存, e); // 降級(jí)直接更新庫(kù)存表 stockMapper.decreaseStock(order.getMaterialId(), order.getQuantity()); } }5.3 現(xiàn)象掃描槍掃報(bào)工碼接口返回400 Bad Request日志顯示JSON parse error: Cannot deserialize instance of java.lang.Long out of VALUE_STRING token原因掃描槍輸出的工單ID帶前導(dǎo)零如001001后端DTO用Long workOrderId接收J(rèn)ackson反序列化失敗。解決修改ReportRequest.java用String接收再轉(zhuǎn)換public class ReportRequest { private String workOrderIdStr; // 接收字符串 private Integer quantity; // 新增getter自動(dòng)轉(zhuǎn)Long public Long getWorkOrderId() { return Long.valueOf(workOrderIdStr); // 生產(chǎn)環(huán)境需加try-catch } }5.4 現(xiàn)象導(dǎo)出日?qǐng)?bào)表時(shí)Tomcat進(jìn)程CPU飆升100%持續(xù)5分鐘以上原因ReportExportController.export()方法用XSSFWorkbook內(nèi)存版POI生成Excel大數(shù)據(jù)量時(shí)占用堆內(nèi)存過(guò)大觸發(fā)Full GC。解決強(qiáng)制切換為SXSSFWorkbook流式版// ReportExportController.java GetMapping(/export) public void export(HttpServletResponse response, RequestParam String dateFrom) { SXSSFWorkbook workbook new SXSSFWorkbook(100); // 每100行刷入磁盤(pán) Sheet sheet workbook.createSheet(日?qǐng)?bào)); // ... 寫(xiě)入邏輯不變 workbook.write(response.getOutputStream()); workbook.dispose(); // 必須調(diào)用釋放臨時(shí)文件 }5.5 現(xiàn)象同一工單被兩個(gè)工人同時(shí)掃碼報(bào)工庫(kù)存扣減出現(xiàn)負(fù)數(shù)原因stockMapper.decreaseStock()方法是純SQL更新未加WHERE stock #{quantity}條件導(dǎo)致超扣。解決修改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(庫(kù)存不足當(dāng)前庫(kù)存 stockMapper.selectById(materialId).getStock()); }6. 二次開(kāi)發(fā)實(shí)戰(zhàn)給報(bào)工模塊加“拍照留證”功能含前后端聯(lián)調(diào)要點(diǎn)6.1 需求場(chǎng)景與技術(shù)選型某客戶(hù)要求工人報(bào)工時(shí)必須上傳現(xiàn)場(chǎng)照片照片需綁定到報(bào)工記錄且支持在工單詳情頁(yè)查看。這不是簡(jiǎn)單加個(gè)文件上傳要考慮三點(diǎn)1照片存哪里本地磁盤(pán) or 對(duì)象存儲(chǔ)2如何防重復(fù)上傳同一工單多次報(bào)工3前端怎么拍H5調(diào)用攝像頭 or 調(diào)用APP原生API。我們選擇最穩(wěn)妥的路徑后端存本地upload/目錄前端用input typefile acceptimage/*兼容所有安卓平板瀏覽器。6.2 后端改造新增文件上傳接口與報(bào)工實(shí)體擴(kuò)展第一步在pom.xml添加文件上傳依賴(lài)項(xiàng)目原未引入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. 校驗(yàn)文件類(lèi)型 String contentType file.getContentType(); if (!contentType.startsWith(image/)) { return Result.fail(僅支持圖片文件); } // 2. 生成唯一文件名reportId 時(shí)間戳 隨機(jī)數(shù) String ext Objects.requireNonNull(file.getOriginalFilename()).substring( file.getOriginalFilename().lastIndexOf(.)); String fileName reportId _ System.currentTimeMillis() _ new Random().nextInt(1000) ext; // 3. 保存到upload/目錄需提前創(chuàng)建 try { File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); file.transferTo(new File(uploadPath fileName)); // 4. 更新報(bào)工記錄的photo_url字段 ReportEntity report new ReportEntity(); report.setId(reportId); report.setPhotoUrl(/upload/ fileName); // 前端可直接訪(fǎng)問(wèn) reportMapper.updateById(report); return Result.success(Map.of(url, /upload/ fileName)); } catch (Exception e) { log.error(上傳失敗, e); return Result.fail(上傳失敗 e.getMessage()); } } }關(guān)鍵點(diǎn)upload/目錄需在Tomcat的webapps/ROOT/下手動(dòng)創(chuàng)建否則file.transferTo()拋FileNotFoundExceptionphotoUrl存相對(duì)路徑前端用img :srcreport.photoUrl即可顯示。6.3 前端聯(lián)調(diào)在報(bào)工頁(yè)面集成拍照與預(yù)覽修改pages/WorkOrderList.vue在報(bào)工彈窗中加入!-- 報(bào)工彈窗內(nèi) -- el-form-item label現(xiàn)場(chǎng)照片 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; // 生成預(yù)覽URL前端即時(shí)顯示 const reader new FileReader(); reader.onload (e) { this.previewUrl e.target.result; }; reader.readAsDataURL(file); // 構(gòu)造FormData上傳 const formData new FormData(); formData.append(file, file); formData.append(reportId, this.currentReportId); api.reportUpload(formData).then(res { this.$message.success(照片上傳成功); }); }避坑提示FileReader的readAsDataURL在某些舊版安卓瀏覽器中不支持需加降級(jí)方案如提示“請(qǐng)使用最新版Chrome”api.reportUpload()需在api/report.js中新增用axios.post()并設(shè)置headers: {Content-Type: multipart/form-data}。6.4 驗(yàn)證與上線(xiàn) checklist完成開(kāi)發(fā)后必須驗(yàn)證以下5點(diǎn)功能驗(yàn)證掃碼打開(kāi)報(bào)工頁(yè) → 點(diǎn)擊“拍照/選圖” → 選擇圖片 → 彈窗顯示預(yù)覽 → 點(diǎn)擊提交 → 后端upload/目錄生成文件 → 工單詳情頁(yè)圖片可正常顯示并發(fā)驗(yàn)證用JMeter模擬10個(gè)工人同時(shí)報(bào)工上傳檢查upload/目錄文件名是否唯一無(wú)覆蓋、數(shù)據(jù)庫(kù)photo_url字段是否正確更新清理驗(yàn)證手動(dòng)刪除upload/下某張照片刷新工單詳情頁(yè)確認(rèn)圖片404且頁(yè)面不崩潰前端加error事件處理安全驗(yàn)證上傳.jsp文件確認(rèn)后端contentType校驗(yàn)攔截返回400且文件未寫(xiě)入磁盤(pán)性能驗(yàn)證上傳5MB高清圖確認(rèn)上傳時(shí)間10秒Tomcat默認(rèn)maxPostSize為2MB需在server.xml中改為maxPostSize10485760從那以后我每次給產(chǎn)線(xiàn)系統(tǒng)加新功能都強(qiáng)制走一遍“本地模擬產(chǎn)線(xiàn)環(huán)境→用真實(shí)掃描槍測(cè)試→壓測(cè)并發(fā)→檢查日志埋點(diǎn)”四步流程哪怕只是加個(gè)按鈕。因?yàn)楫a(chǎn)線(xiàn)沒(méi)有“開(kāi)發(fā)環(huán)境”和“生產(chǎn)環(huán)境”的區(qū)別只有“能用”和“不能用”——而“不能用”的代價(jià)是整條產(chǎn)線(xiàn)停擺一小時(shí)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取