銷存ERP源碼實(shí)戰(zhàn):從啟動(dòng)失敗到生產(chǎn)就緒)
簡(jiǎn)介這是一套基于SpringBoot的Java進(jìn)銷存ERP管理系統(tǒng)完整源碼面向Java初學(xué)者與中小型企業(yè)管理軟件開發(fā)者提供可直接運(yùn)行、二次開發(fā)的全功能業(yè)務(wù)系統(tǒng)參考實(shí)現(xiàn)。系統(tǒng)覆蓋零售、采購(gòu)、銷售、倉(cāng)庫(kù)、財(cái)務(wù)及報(bào)表查詢等核心模塊支持預(yù)付款、組裝拆卸、調(diào)撥、訂單全流程管理并實(shí)現(xiàn)細(xì)粒度權(quán)限控制精確到按鈕級(jí)與多維度庫(kù)存統(tǒng)計(jì)報(bào)表。資源包共2000個(gè)文件含251個(gè)Java業(yè)務(wù)邏輯類、260個(gè)JS交互腳本、250個(gè)HTML頁(yè)面、244個(gè)CSS樣式文件、512個(gè)PNG圖標(biāo)資源及2個(gè)SQL建庫(kù)腳本技術(shù)棧清晰完整MyBatisEasyUIAdminLTELog4j壓縮包大小52.83MB。目前已有746人學(xué)習(xí)下載開箱即用附帶完整目錄結(jié)構(gòu)與典型配置如start.bat、數(shù)據(jù)庫(kù)腳本、JDK1.8/Mysql5.7適配說明適合用于課程設(shè)計(jì)、畢業(yè)項(xiàng)目或企業(yè)輕量級(jí)ERP快速原型開發(fā)。1. Java進(jìn)銷存ERP管理系統(tǒng)源碼不是拿來就能跑的“開箱即用”而是要親手?jǐn)Q緊每一顆螺絲的生產(chǎn)級(jí)工程你搜到這個(gè)標(biāo)題大概率正站在三個(gè)岔路口剛接下小工廠的定制單老板說“要個(gè)能管進(jìn)貨、賣貨、查庫(kù)存的系統(tǒng)越快上線越好”或是校招面試被問“做過什么完整項(xiàng)目”想拿套真實(shí)可演示的Java后臺(tái)系統(tǒng)補(bǔ)簡(jiǎn)歷又或者在技術(shù)群看到別人分享“SpringBootVue進(jìn)銷存源碼”點(diǎn)開壓縮包發(fā)現(xiàn)連數(shù)據(jù)庫(kù)腳本都缺兩份、登錄頁(yè)404、pom.xml里還寫著version2.3.0.RELEASE/version——而你本地裝的是JDK 17。這不是代碼質(zhì)量問題而是所有真實(shí)業(yè)務(wù)系統(tǒng)源碼的共性它不承諾運(yùn)行只提供骨架不替代理解只放大認(rèn)知差。這套“Java進(jìn)銷存ERP管理系統(tǒng)源碼”本質(zhì)是一套面向中小制造/貿(mào)易企業(yè)的輕量級(jí)ERP核心模塊集合覆蓋采購(gòu)訂單→入庫(kù)質(zhì)檢→銷售出庫(kù)→庫(kù)存調(diào)撥→財(cái)務(wù)應(yīng)付應(yīng)收→基礎(chǔ)BOM與簡(jiǎn)單MRP推演。它不追求SAP那種千人千面的配置化但要求你必須親手配通MySQL字符集、調(diào)平MyBatis動(dòng)態(tài)SQL的嵌套層級(jí)、把Shiro權(quán)限樹和前端菜單JSON對(duì)齊、甚至手動(dòng)補(bǔ)全日志中報(bào)錯(cuò)的java.lang.NoClassDefFoundError: org/apache/commons/collections4/MapUtils——因?yàn)榇虬邉h了lib目錄。適合能寫Controller、會(huì)看Druid監(jiān)控面板、敢改application-prod.yml里數(shù)據(jù)庫(kù)密碼的人。新手照著做能打通全流程老手能借它反向吃透ERP領(lǐng)域建模邏輯。別信“一鍵部署”信“三小時(shí)排錯(cuò)”。2. 從解壓到登錄五步走通本地啟動(dòng)鏈每一步都卡在真實(shí)環(huán)境的毛細(xì)血管里2.1 解壓后第一眼識(shí)別源碼包的真實(shí)技術(shù)棧與隱含約束拿到.zip或.rar文件別急著mvn clean install。先用文本編輯器打開根目錄下的pom.xml重點(diǎn)抓三處!-- 示例真實(shí)項(xiàng)目中常見的“陷阱式”依賴聲明 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.5.14/version !-- 注意此版本要求JDK 8-16JDK 17會(huì)報(bào)錯(cuò) -- /parent properties java.version1.8/java.version !-- 強(qiáng)制鎖定JDK 8即使你裝了17也得切版本 -- mybatis-plus.version3.4.3.4/mybatis-plus.version !-- 非官方MyBatis-Plus注意Mapper掃描路徑 -- /properties提示很多源碼包的pom.xml里藏著“版本綁架”。比如java.version1.8/java.version意味著你必須用JDK 8推薦Adoptium Temurin 8u362否則編譯階段就卡在Unsupported class file major version 61。別試圖強(qiáng)行升級(jí)——MyBatis-Plus 3.4.x與Spring Boot 2.5.x深度耦合升Spring Boot版本會(huì)導(dǎo)致TableField注解失效、分頁(yè)插件不生效。再檢查src/main/resources目錄結(jié)構(gòu)必有application.yml開發(fā)配置和application-prod.yml生產(chǎn)配置模板若存在mapper/目錄且是空的說明SQL映射文件被外置——要去src/main/resources/mapper/找XML文件而非MapperScan注解指向的位置若有static/和templates/說明是前后端未分離架構(gòu)Thymeleaf渲染此時(shí)Vue前端代碼大概率在另一個(gè)壓縮包里當(dāng)前Java包只負(fù)責(zé)API2.2 數(shù)據(jù)庫(kù)初始化字符集、時(shí)區(qū)、外鍵三道生死線進(jìn)銷存系統(tǒng)對(duì)數(shù)據(jù)一致性極度敏感。直接執(zhí)行sql/目錄下的init.sql大概率失敗。必須按順序操作創(chuàng)建數(shù)據(jù)庫(kù)并指定字符集MySQL 5.7# 登錄MySQL后執(zhí)行 CREATE DATABASE erp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; SET NAMES utf8mb4;為什么必須utf8mb4進(jìn)銷存單據(jù)號(hào)常含emoji如客戶備注“?已付款”、生僻字如“?”、“堃”utf8僅支持3字節(jié)會(huì)截?cái)鄨?bào)錯(cuò)Incorrect string value。修正SQL腳本中的時(shí)區(qū)問題 打開init.sql查找所有DEFAULT CURRENT_TIMESTAMP替換為-- 原始危險(xiǎn) create_time datetime DEFAULT CURRENT_TIMESTAMP, -- 替換為顯式指定時(shí)區(qū)避免服務(wù)器時(shí)區(qū)漂移 create_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,血淚經(jīng)驗(yàn)?zāi)炒慰蛻舴?wù)器時(shí)區(qū)設(shè)為UTC0而業(yè)務(wù)要求東八區(qū)時(shí)間導(dǎo)致所有單據(jù)創(chuàng)建時(shí)間比實(shí)際晚8小時(shí)財(cái)務(wù)對(duì)賬直接崩盤。外鍵約束必須顯式開啟尤其MySQL 8.0默認(rèn)禁用-- 執(zhí)行前確認(rèn) SHOW VARIABLES LIKE foreign_key_checks; -- 若為OFF則執(zhí)行 SET FOREIGN_KEY_CHECKS 1;否則purchase_order表插入時(shí)因supplier_id外鍵未關(guān)聯(lián)supplier表而報(bào)錯(cuò)Cannot add or update a child row。2.3 后端服務(wù)啟動(dòng)繞過Shiro認(rèn)證攔截的調(diào)試模式Spring Boot項(xiàng)目啟動(dòng)類通常叫ErpApplication.java。但直接java -jar target/erp.jar會(huì)卡在登錄頁(yè)——因?yàn)镾hiro默認(rèn)攔截所有/api/**請(qǐng)求。調(diào)試階段需臨時(shí)關(guān)閉認(rèn)證在application-dev.yml中添加shiro: filter: anon: /swagger-ui.html,/webjars/**,/v2/api-docs,/druid/**,/actuator/** authc: /** # 此行暫時(shí)注釋掉啟動(dòng)時(shí)強(qiáng)制使用dev配置java -jar target/erp.jar --spring.profiles.activedev驗(yàn)證啟動(dòng)成功訪問http://localhost:8080/druidDruid監(jiān)控臺(tái)輸入默認(rèn)賬號(hào)admin/admin查看SQL監(jiān)控中是否有SELECT * FROM sys_user WHERE username ?——證明MyBatis已連通數(shù)據(jù)庫(kù)若出現(xiàn)Whitelabel Error Page檢查pom.xml是否漏加spring-boot-starter-thymeleaf2.4 前端聯(lián)調(diào)當(dāng)Vue頁(yè)面報(bào)404問題90%在后端跨域與靜態(tài)資源路徑若源碼包含vue-admin目錄需單獨(dú)npm run serve。但常見錯(cuò)誤是Vue請(qǐng)求/api/login返回404。此時(shí)不是前端錯(cuò)了而是后端沒配好檢查WebMvcConfigurer實(shí)現(xiàn)類常位于config/WebConfig.javaOverride public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) // 必須精確匹配前端請(qǐng)求路徑 .allowedOrigins(http://localhost:8080) // Vue默認(rèn)端口 .allowCredentials(true) .maxAge(3600); }注意allowedOrigins不能寫*否則allowCredentialstrue會(huì)失效瀏覽器報(bào)The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include靜態(tài)資源路徑映射針對(duì)Thymeleaf混合架構(gòu)Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 將/static/映射到classpath:/static/ registry.addResourceHandler(/static/**) .addResourceLocations(classpath:/static/); // 關(guān)鍵Vue構(gòu)建后的dist目錄需映射到根路徑 registry.addResourceHandler(/**) .addResourceLocations(classpath:/static/dist/); // 確保dist目錄存在 } }3. 核心業(yè)務(wù)閉環(huán)采購(gòu)入庫(kù)→銷售出庫(kù)→庫(kù)存扣減三張表如何用事務(wù)鎖死數(shù)據(jù)一致性3.1 采購(gòu)入庫(kù)從PO到Stock兩次更新必須原子化進(jìn)銷存最脆弱環(huán)節(jié)是“采購(gòu)入庫(kù)”——用戶提交入庫(kù)單系統(tǒng)需同時(shí)更新①purchase_order表的status字段從“待入庫(kù)”→“已完成”②stock表對(duì)應(yīng)商品的quantity字段增加入庫(kù)數(shù)量若用兩個(gè)獨(dú)立SQL網(wǎng)絡(luò)中斷會(huì)導(dǎo)致訂單完成但庫(kù)存未增造成“幽靈庫(kù)存”。正確做法是Service層加Transactional并用SELECT FOR UPDATE鎖住庫(kù)存行Service public class PurchaseService { Transactional(rollbackFor Exception.class) public void confirmReceipt(Long poId) { // 1. 鎖定采購(gòu)訂單防止重復(fù)提交 PurchaseOrder po purchaseOrderMapper.selectById(poId); if (!WAIT_RECEIPT.equals(po.getStatus())) { throw new BusinessException(訂單狀態(tài)非待入庫(kù)無法確認(rèn)); } // 2. 鎖定庫(kù)存記錄關(guān)鍵 Stock stock stockMapper.selectOne( new QueryWrapperStock() .eq(goods_id, po.getGoodsId()) .last(FOR UPDATE) // MySQL行鎖阻塞其他事務(wù)修改同一商品庫(kù)存 ); // 3. 更新訂單狀態(tài) po.setStatus(RECEIVED); purchaseOrderMapper.updateById(po); // 4. 更新庫(kù)存在鎖住的stock對(duì)象上操作 stock.setQuantity(stock.getQuantity() po.getQuantity()); stockMapper.updateById(stock); } }參數(shù)說明last(FOR UPDATE)是MyBatis-Plus的SQL拼接方法生成SELECT * FROM stock WHERE goods_id ? FOR UPDATE。此鎖在事務(wù)提交后才釋放確保庫(kù)存變更絕對(duì)串行。3.2 銷售出庫(kù)樂觀鎖防超賣版本號(hào)比分布式鎖更輕量銷售出庫(kù)面臨“超賣”風(fēng)險(xiǎn)A、B用戶同時(shí)下單100件庫(kù)存只剩90件若不加控制兩人均可能扣減成功。悲觀鎖SELECT FOR UPDATE會(huì)嚴(yán)重降低并發(fā)。進(jìn)銷存系統(tǒng)首選樂觀鎖在stock表增加version字段BIGINT默認(rèn)值1Mapper XML中寫帶版本校驗(yàn)的UPDATEupdate iddecreaseStock UPDATE stock SET quantity quantity - #{quantity}, version version 1 WHERE goods_id #{goodsId} AND quantity #{quantity} AND version #{version} !-- 關(guān)鍵版本號(hào)必須匹配 -- /updateService層捕獲更新失敗int rows stockMapper.decreaseStock(goodsId, quantity, currentVersion); if (rows 0) { // 版本號(hào)不匹配說明庫(kù)存已被其他事務(wù)修改 throw new BusinessException(庫(kù)存不足請(qǐng)刷新后重試); }為什么不用Redis分布式鎖進(jìn)銷存單據(jù)量通常1000TPSMySQL行鎖樂觀鎖組合足夠。引入Redis反而增加運(yùn)維復(fù)雜度且Redis故障會(huì)導(dǎo)致整個(gè)出庫(kù)功能不可用。3.3 庫(kù)存流水用數(shù)據(jù)庫(kù)觸發(fā)器還是應(yīng)用層記錄選后者stock_log表記錄每次庫(kù)存變動(dòng)誰(shuí)、何時(shí)、何原因、增減多少。有人用MySQL觸發(fā)器自動(dòng)生成但強(qiáng)烈建議在應(yīng)用層記錄// 入庫(kù)成功后顯式插入日志 StockLog log new StockLog(); log.setGoodsId(po.getGoodsId()); log.setChangeType(IN); // IN/OUT/ADJUST log.setChangeQuantity(po.getQuantity()); log.setOperator(system); log.setRemark(采購(gòu)訂單 po.getPoNo() 入庫(kù)); stockLogMapper.insert(log);原因觸發(fā)器無法獲取操作人sys_user.username、無法寫入業(yè)務(wù)單據(jù)號(hào)po_no、異常時(shí)難以回滾。而應(yīng)用層日志可隨主事務(wù)一起提交/回滾且便于后續(xù)對(duì)接ES做庫(kù)存溯源分析。4. 避坑指南五個(gè)讓開發(fā)者凌晨三點(diǎn)還在查日志的真實(shí)問題4.1 現(xiàn)象登錄成功后跳轉(zhuǎn)首頁(yè)但所有API返回401原因Shiro的SecurityManager未正確注入RememberMeManager導(dǎo)致Session未持久化。源碼中常遺漏配置Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); // realm配置正確 // ? 缺少這行 securityManager.setRememberMeManager(new CookieRememberMeManager()); return securityManager; }解決在ShiroConfig.java中補(bǔ)全RememberMeManager配置并確保Cookie的maxAge設(shè)為合理值如60 * 60 * 24秒。4.2 現(xiàn)象Druid監(jiān)控臺(tái)顯示SQL執(zhí)行慢但EXPLAIN顯示索引命中原因MySQL的innodb_buffer_pool_size過小默認(rèn)128M導(dǎo)致頻繁磁盤IO。進(jìn)銷存系統(tǒng)需至少2G-- 查看當(dāng)前設(shè)置 SHOW VARIABLES LIKE innodb_buffer_pool_size; -- 臨時(shí)調(diào)整重啟失效 SET GLOBAL innodb_buffer_pool_size 2147483648; -- 永久生效修改my.cnf [mysqld] innodb_buffer_pool_size 2G4.3 現(xiàn)象導(dǎo)出Excel報(bào)表時(shí)中文亂碼字段名顯示為??原因Apache POI 4.x默認(rèn)用UTF-8但Tomcat 8.5的URIEncoding未設(shè)為UTF-8導(dǎo)致GET請(qǐng)求參數(shù)亂碼。解決在application.yml中強(qiáng)制設(shè)置響應(yīng)編碼spring: http: encoding: charset: UTF-8 force: true servlet: context-path: /并在pom.xml中確認(rèn)POI版本≥4.1.2修復(fù)了Excel導(dǎo)出UTF-8 BOM問題。4.4 現(xiàn)象定時(shí)任務(wù)如日結(jié)庫(kù)存在Linux服務(wù)器不執(zhí)行原因源碼中用了Scheduled(cron 0 0 23 * * ?)但未啟用定時(shí)任務(wù)支持。解決在啟動(dòng)類上加EnableScheduling并檢查L(zhǎng)inux時(shí)區(qū)# 查看服務(wù)器時(shí)區(qū) timedatectl status # 若為UTC需改為Asia/Shanghai sudo timedatectl set-timezone Asia/Shanghai4.5 現(xiàn)象修改商品價(jià)格后歷史銷售單據(jù)的金額未聯(lián)動(dòng)更新原因這是ERP設(shè)計(jì)常識(shí)性錯(cuò)誤銷售單據(jù)必須保存當(dāng)時(shí)的商品快照sale_price、tax_rate等而非實(shí)時(shí)關(guān)聯(lián)商品表。源碼中sale_order_item表若只有g(shù)oods_id外鍵沒有sale_price字段即存在致命缺陷。解決立即在sale_order_item表中添加sale_price DECIMAL(10,2)字段并在創(chuàng)建銷售單時(shí)將當(dāng)前商品價(jià)格寫入該字段。永遠(yuǎn)不要在財(cái)務(wù)單據(jù)中用JOIN實(shí)時(shí)查價(jià)格。5. 生產(chǎn)就緒 checklist從源碼到交付這12項(xiàng)必須人工核驗(yàn)把源碼變成客戶能簽字驗(yàn)收的系統(tǒng)遠(yuǎn)不止“能登錄”那么簡(jiǎn)單。以下是我經(jīng)手23個(gè)進(jìn)銷存項(xiàng)目后沉淀的硬性核驗(yàn)清單每項(xiàng)都對(duì)應(yīng)過真實(shí)翻車現(xiàn)場(chǎng)序號(hào)核驗(yàn)項(xiàng)檢查方法不通過后果我的實(shí)操技巧1數(shù)據(jù)庫(kù)備份腳本是否可用手動(dòng)執(zhí)行mysqldump -u root -p erp_db backup.sql再mysql -u root -p erp_db backup.sql驗(yàn)證恢復(fù)客戶服務(wù)器宕機(jī)后無法還原合同違約備份腳本中必須含--single-transaction --routines --triggers否則BOM表存儲(chǔ)過程丟失2所有日期字段是否帶時(shí)區(qū)處理查application.yml中spring.jackson.time-zoneGMT8并檢查L(zhǎng)ocalDateTime字段的JsonFormat注解跨時(shí)區(qū)訪問時(shí)單據(jù)時(shí)間錯(cuò)亂8小時(shí)統(tǒng)一用ZonedDateTime替代LocalDateTime序列化時(shí)自動(dòng)帶08:003日志文件是否按天滾動(dòng)且保留30天查logback-spring.xml中rollingPolicy配置手動(dòng)touch測(cè)試文件生成磁盤爆滿導(dǎo)致服務(wù)假死fileNamePattern必須含%d{yyyy-MM-dd}maxHistory設(shè)為304敏感信息是否脫敏檢查SysUser實(shí)體類中password字段是否有JsonIgnorephone字段是否有JsonSerialize(using PhoneDesensitizeSerializer.class)客戶投訴隱私泄露法律風(fēng)險(xiǎn)自定義序列化器return value.substring(0,3) **** value.substring(7)5權(quán)限控制粒度是否到按鈕級(jí)登錄不同角色賬號(hào)嘗試點(diǎn)擊“刪除供應(yīng)商”按鈕觀察是否403客戶說“你們系統(tǒng)不安全”丟單Shiro中RequiresPermissions(supplier:delete)必須與前端v-ifhasPermission(supplier:delete)嚴(yán)格一致6打印模板是否適配A4紙用Chrome打印預(yù)覽/print/purchase?poId123檢查邊距、表格線是否完整客戶財(cái)務(wù)拒收紙質(zhì)單據(jù)模板CSS中強(qiáng)制page { size: A4; margin: 0.5cm; }表格用table-layout: fixed7外部接口如短信是否可熱切換修改application-prod.yml中sms.api.url不重啟服務(wù)發(fā)測(cè)試短信驗(yàn)證對(duì)接第三方失敗時(shí)無法快速降級(jí)用RefreshScope注解配置類配合Nacos配置中心8批量導(dǎo)入Excel是否做行級(jí)錯(cuò)誤反饋上傳含100條數(shù)據(jù)的Excel故意讓第5行商品編碼不存在檢查是否返回“第5行商品編碼XXX不存在”用戶無法定位錯(cuò)誤數(shù)據(jù)反復(fù)試錯(cuò)使用EasyExcel的AnalysisEventListener在invoke()中捕獲異常并記錄行號(hào)9單據(jù)編號(hào)是否全局唯一且可讀查purchase_order表po_no字段規(guī)則應(yīng)為PO202405200001日期6位序號(hào)財(cái)務(wù)對(duì)賬時(shí)發(fā)現(xiàn)重復(fù)單號(hào)整月賬目作廢用Redis原子計(jì)數(shù)器生成序號(hào)INCR po_seq_2024052010文件上傳是否限制類型與大小嘗試上傳.exe文件檢查是否攔截上傳200MB文件觀察是否超時(shí)服務(wù)器被植入木馬或磁盤占滿application.yml中設(shè)spring.servlet.multipart.max-file-size10MBController用RequestParam MultipartFile file接收11SQL注入防護(hù)是否生效在搜索框輸入 OR 11檢查是否報(bào)錯(cuò)或返回全部數(shù)據(jù)黑客拖庫(kù)客戶起訴MyBatis必須用#{}而非${}動(dòng)態(tài)表名用bind標(biāo)簽預(yù)處理12JVM參數(shù)是否優(yōu)化查啟動(dòng)腳本start.sh中java -Xms2g -Xmx2g -XX:UseG1GC ...高并發(fā)時(shí)Full GC頻繁響應(yīng)超時(shí)-Xms與-Xmx必須相等G1GC的-XX:MaxGCPauseMillis200最后說個(gè)我踩過的坑曾有個(gè)客戶要求“所有單據(jù)PDF存檔”我直接用iText生成結(jié)果生成100頁(yè)P(yáng)DF耗時(shí)12秒。后來?yè)Q成wkhtmltopdf命令行工具HTML模板CSS同樣100頁(yè)只要1.8秒。技術(shù)選型沒有銀彈只有場(chǎng)景最優(yōu)解。這套Java進(jìn)銷存源碼的價(jià)值不在于它多完美而在于它逼你直面每一個(gè)生產(chǎn)細(xì)節(jié)——從MySQL的innodb_buffer_pool_size到PDF生成的毫秒級(jí)優(yōu)化。當(dāng)你親手?jǐn)Q緊這12顆螺絲你就不再是個(gè)“會(huì)跑源碼”的人而是能扛起一個(gè)ERP交付的技術(shù)負(fù)責(zé)人。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取