建超市庫存管理系統(tǒng)關(guān)鍵設(shè)計(jì))
簡介這套基于Java SpringBoot與MyBatis的超市倉庫管理系統(tǒng)源碼面向課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)以及希望上手企業(yè)級后端項(xiàng)目的初學(xué)者。項(xiàng)目采用SpringBoot整合MybatisPlus并引入Shiro安全框架實(shí)現(xiàn)用戶認(rèn)證與權(quán)限控制業(yè)務(wù)模塊涵蓋商品分類、庫存管理、供應(yīng)商管理、訂單處理等核心場景同時(shí)附帶完整的MySQL數(shù)據(jù)庫初始化腳本和Maven構(gòu)建配置導(dǎo)入IDE后即可運(yùn)行調(diào)試。資源壓縮包共392個(gè)文件大小約10.3MB其中106個(gè)Java源碼文件構(gòu)成業(yè)務(wù)邏輯主體75個(gè)GIF動(dòng)圖用于界面效果演示49個(gè)HTML頁面與42個(gè)JS、CSS文件支撐前端交互14個(gè)XML與JSON文件包含配置文件及數(shù)據(jù)交換結(jié)構(gòu)另有SQL腳本、項(xiàng)目文檔、Git版本管理信息等整體目錄層次分明方便按模塊查看代碼。目前已有425人學(xué)習(xí)通過閱讀源碼和運(yùn)行項(xiàng)目能夠深入理解SpringBoot整合MyBatis與Shiro的完整流程掌握從數(shù)據(jù)庫設(shè)計(jì)到前后端聯(lián)調(diào)的實(shí)戰(zhàn)技能適合作為課設(shè)拓展或畢業(yè)設(shè)計(jì)的參考基礎(chǔ)。1. 超市倉庫管理系統(tǒng)用 SpringBoot MyBatis SQL Server先想清楚這一件事我第一次做這種系統(tǒng)時(shí)先把“商品的增刪改查”這個(gè)思維丟掉。真正讓收銀和倉管頭疼的不是商品檔案能不能存而是庫存臺(tái)賬和出入庫流水之間的關(guān)系。貨架上的庫存數(shù)字是結(jié)果入庫單、出庫單、盤點(diǎn)單才是原因。這套系統(tǒng)里SpringBoot 負(fù)責(zé)把收銀、采購、倉庫的請求變成穩(wěn)定的服務(wù)接口MyBatis 負(fù)責(zé)把業(yè)務(wù)規(guī)則寫成一段段可控 SQLSQL Server 數(shù)據(jù)庫則保證庫存數(shù)字在并發(fā)扣減時(shí)不被寫壞。它適合跑結(jié)業(yè)設(shè)計(jì)、外包交付也適合門店量不大的中小型自研項(xiàng)目不引入消息隊(duì)列也不需要微服務(wù)那套基礎(chǔ)設(shè)施一臺(tái)裝好 SQL Server 2019 的 Windows 機(jī)器就能完整跑起來。2. SQL Server 庫表建模把商品、庫存、單據(jù)流水一次定清楚2.1 臺(tái)賬與流水分離是這套系統(tǒng)最容易抄錯(cuò)的地方很多初版實(shí)現(xiàn)包括一些趕工的外包代碼習(xí)慣在商品表里直接放一個(gè) quantity 字段賣一瓶就減一。這個(gè)設(shè)計(jì)在數(shù)據(jù)量小、流程簡單的時(shí)候看不出問題一旦出現(xiàn)退貨、破損報(bào)損、盤點(diǎn)差異就沒有任何追溯依據(jù)因?yàn)閹齑鏀?shù)字被直接覆蓋了。正確做法是臺(tái)賬與流水分離product 表只管商品信息stock 表只記錄當(dāng)前可用庫存所有數(shù)量變動(dòng)都必須寫進(jìn) stock_flow 流水表。庫存臺(tái)賬可以被流水重建盤點(diǎn)差異也能逐單對賬這是我在設(shè)計(jì)庫存類系統(tǒng)時(shí)不會(huì)讓步的約定。表名職責(zé)關(guān)鍵字段與庫存的關(guān)系product商品主數(shù)據(jù)barcode, product_name, spec, unit靜態(tài)信息不存數(shù)量stock庫存臺(tái)賬warehouse_id, product_id, quantity當(dāng)前庫存只做加減stock_flow出入庫流水biz_no, flow_type, quantity, before_qty, after_qty驅(qū)動(dòng)臺(tái)賬變化的依據(jù)stocktake盤點(diǎn)單stocktake_no, book_qty, real_qty, diff_qty盤點(diǎn)后校正臺(tái)賬stock_flow 表里同時(shí)記錄 before_qty 和 after_qty這一點(diǎn)很多人會(huì)省。省掉的代價(jià)是日后想排查“為什么這次入庫后數(shù)量不對”時(shí)只能靠猜。兩個(gè)字段多占一點(diǎn)存儲(chǔ)但換來的是一對一的對賬能力在做超市管理系統(tǒng)時(shí)這條成本值得付。2.2 建庫建表 SQLSQL Server 語法下的最小可跑版本下面這段 T-SQL 能直接建出四張核心表。字段類型我按 SQL Server 的習(xí)慣寫金額用 decimal庫存用 int時(shí)間用 datetime2字符串用 nvarchar。這樣做可以避免字符集和精度上的隱藏問題。CREATE DATABASE supermarket; GO USE supermarket; GO CREATE TABLE product ( product_id INT IDENTITY(1,1) PRIMARY KEY, barcode NVARCHAR(32) NOT NULL UNIQUE, product_name NVARCHAR(64) NOT NULL, spec NVARCHAR(64) NULL, unit NVARCHAR(16) NOT NULL DEFAULT N瓶, category_id INT NULL, create_time DATETIME2 DEFAULT SYSDATETIME() ); CREATE TABLE stock ( warehouse_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0, PRIMARY KEY (warehouse_id, product_id) ); CREATE TABLE stock_flow ( flow_id BIGINT IDENTITY(1,1) PRIMARY KEY, biz_no NVARCHAR(32) NOT NULL, warehouse_id INT NOT NULL, product_id INT NOT NULL, flow_type TINYINT NOT NULL, -- 1 入庫 2 出庫 3 盤點(diǎn)調(diào)整 quantity INT NOT NULL, before_qty INT NOT NULL, after_qty INT NOT NULL, remark NVARCHAR(200) NULL, create_time DATETIME2 DEFAULT SYSDATETIME() ); CREATE TABLE stocktake ( stocktake_no NVARCHAR(32) PRIMARY KEY, product_id INT NOT NULL, book_qty INT NOT NULL, real_qty INT NOT NULL, diff_qty AS real_qty - book_qty, take_time DATETIME2 DEFAULT SYSDATETIME() );這段 SQL 在 SQL Server 2019 和 2022 上可以直接跑。IDENTITY(1,1) 是自增主鍵stocks 的主鍵用聯(lián)合主鍵確保同一個(gè)倉庫、同一個(gè)商品只會(huì)有一行庫存記錄。diff_qty 是計(jì)算列由 SQL Server 計(jì)算不需要在 SpringBoot 或者 MyBatis 里再算一遍。建表腳本建議手工維護(hù)不要依靠 SpringBoot 自動(dòng)建表。提示不要在 application.yml 里配置 ddl-auto 相關(guān)自動(dòng)建表選項(xiàng)。生產(chǎn)環(huán)境里一旦自動(dòng)執(zhí)行容易把人工加的索引和約束覆蓋掉。schema 變更應(yīng)該走 SQL 腳本和源碼一起提交。2.3 索引與約束MyBatis 查詢慢先回來看這里庫存類系統(tǒng)最常見的性能問題是條碼查詢。barcode 已經(jīng)建了唯一索引但 MyBatis XML 里如果寫成LIKE % #{keyword} %這個(gè)索引就會(huì)失效。前端掃碼通常是完整條碼最實(shí)用的查詢是條碼前綴匹配barcode LIKE CONCAT(#{keyword}, %)可以走索引商品名稱的人名模糊搜索量小走全表掃也問題不大。stock_flow 會(huì)隨時(shí)間增長得很快它上面應(yīng)該有聯(lián)合索引。常見做法是在 warehouse_id、product_id、create_time 三列上建聯(lián)合索引這樣按倉庫、按商品、按時(shí)間窗口查流水時(shí)執(zhí)行計(jì)劃不會(huì)每次都回表掃全量數(shù)據(jù)。排序規(guī)則統(tǒng)一使用 Chinese_PRC_CI_AS注意 SQL Server 里的排序規(guī)則同源問題否則在不同環(huán)境下字符串比較可能區(qū)分大小寫或在中文排序上出現(xiàn)詭異行為。2.4 初始化數(shù)據(jù)不要用 MyBatis 循環(huán) insert源碼包里常見的 init.sql 一般分三部分建表、插入分類、插入商品。手動(dòng)初始化幾千條商品數(shù)據(jù)時(shí)不應(yīng)該在 Java 里循環(huán)調(diào)用單條 insert既慢又會(huì)產(chǎn)生大量事務(wù)日志。SQL Server 里更推薦用 INSERT ... SELECT 加 UNION ALL 批量插入比如把五條商品數(shù)據(jù)一次性寫入INSERT INTO product (barcode, product_name, spec, unit, category_id) SELECT N690123456001, N純牛奶, N250ml, N盒, 1 UNION ALL SELECT N690123456002, N面包, N原味, N袋, 2 UNION ALL SELECT N690123456003, N礦泉水, N550ml, N瓶, 3;批量插入的 SQL 仍然放在 MyBatis XML 里管理保持項(xiàng)目里只有一種 SQL 書寫入口。初始化數(shù)據(jù)表時(shí)還需要順手插入每個(gè)商品的庫存初始行否則后端聯(lián)查庫存時(shí)會(huì)得到 null而不只是 0。3. SpringBoot 集成 MyBatis 連 SQL Server依賴、yml、XML 一次跑通3.1 最小依賴三個(gè) starter別引錯(cuò) JDBC 驅(qū)動(dòng)dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.microsoft.sqlserver/groupId artifactIdmssql-jdbc/artifactId version12.4.2.jre11/version /dependency這里最容易踩坑的是版本對應(yīng)關(guān)系。SpringBoot 2.x 配 mybatis-spring-boot-starter 2.3.xSpringBoot 3.x 必須換成 mybatis-spring-boot-starter 3.x否則自動(dòng)裝配直接失效。mssql-jdbc 的 jre8 和 jre11 版本按 JDK 選JDK 17 上用 jre8 驅(qū)動(dòng)會(huì)報(bào) UnsupportedClassVersionError。如果項(xiàng)目用了 SpringBoot 較高版本優(yōu)先看官方對應(yīng)的 MyBatis starter而不是盲目升最新版。3.2 application.yml 配置 SQL Server 連接參數(shù)spring: datasource: url: jdbc:sqlserver://localhost:1433;DatabaseNamesupermarket;encryptfalse;trustServerCertificatetrue username: sa password: 你的密碼 driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.supermarket.entity configuration: map-underscore-to-camel-case: trueJDBC URL 里的參數(shù)是有含義的。encryptfalse 和 trustServerCertificatetrue 是為了關(guān)掉 SQL Server JDBC 驅(qū)動(dòng)默認(rèn)的 SSL 主機(jī)名校驗(yàn)本地開發(fā)必須加生產(chǎn)環(huán)境按公司安全策略決定是否保留。mybatis 的 mapper-locations 告訴 SpringBoot 去哪里加載 XML 映射文件type-aliases-package 讓 XML 里寫 resultType 時(shí)不用寫全限定類名。map-underscore-to-camel-case 打開后SQL 查詢出來的 product_id 會(huì)自動(dòng)映射到實(shí)體類字段 productId不用手寫 resultMap。實(shí)體類里比較重要的是 stock 查詢結(jié)果不要用基本類型 int而是用 Integer。庫存數(shù)量在數(shù)據(jù)庫里是 NULL 時(shí)int 接收會(huì)報(bào)空指針I(yè)nteger 會(huì)得到 null這樣才能在業(yè)務(wù)層區(qū)分“沒有庫存記錄”和“庫存為 0”。我在設(shè)計(jì)庫存更新邏輯時(shí)通常先判斷 stock 行是否存在不存在就插入一行初始數(shù)量。3.3 Mapper 接口與 XML商品分頁查詢的寫法商品查詢是超市管理系統(tǒng)的高頻路徑。Mapper 接口定義如下package com.supermarket.mapper; import org.apache.ibatis.annotations.Param; import com.supermarket.entity.Product; import java.util.List; public interface ProductMapper { ListProduct selectPage(Param(keyword) String keyword, Param(offset) int offset, Param(rows) int rows); }對應(yīng)的 XML 映射文件select idselectPage resultTypeProduct SELECT product_id, barcode, product_name, spec, unit FROM product where if testkeyword ! null and keyword ! barcode LIKE CONCAT(#{keyword}, %) OR product_name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY product_id OFFSET #{offset} ROWS FETCH NEXT #{rows} ROWS ONLY /select這段配置里有兩個(gè)關(guān)鍵點(diǎn)。一是 SQL Server 不能用 MySQL 的 LIMIT 分頁必須用 OFFSET ... FETCH NEXT。二是 MyBatis 的where標(biāo)簽會(huì)自動(dòng)處理?xiàng)l件為空時(shí)多余的 AND 和 WHERE這比在 Java 里拼接 SQL 字符串安全得多。條碼匹配用前綴商品名匹配用雙百分號(hào)模糊查兩種命中的索引行為完全不同。3.4 減庫存的 update 必須防超賣update iddecreaseStock UPDATE stock SET quantity quantity - #{qty} WHERE warehouse_id #{warehouseId} AND product_id #{productId} AND quantity gt; #{qty} /update這是庫存并發(fā)問題上的第一道防線。SQL Server 執(zhí)行 UPDATE 時(shí)本身會(huì)加行級排他鎖兩個(gè)請求同時(shí)扣最后一個(gè)庫存時(shí)只有一個(gè)請求的 WHERE 條件能匹配到 quantity qty另一個(gè) update 返回影響行數(shù)為 0。Service 層收到 0 時(shí)拋業(yè)務(wù)異常“庫存不足”不進(jìn)入后面的支付或出庫流程。頁面顯示有貨但扣減失敗絕大多數(shù)情況都是這里沒寫好而不是前端的問題。4. 盤點(diǎn)調(diào)整與事務(wù)邊界行鎖、數(shù)據(jù)庫事務(wù)、MyBatis 緩存4.1 Transactional 放在哪一層才算對Controller 里直接調(diào)用 Mapper 是不行的事務(wù)會(huì)被拆散。我一般在 Service 層寫 StockService把入庫、出庫、盤點(diǎn)調(diào)整都收斂成獨(dú)立方法。以出庫為例完整操作包括查庫存、寫流水、更新庫存。這三個(gè)動(dòng)作必須在一個(gè)事務(wù)里否則流水寫成功、庫存沒減賬就平不上。Service public class StockService { private final StockMapper stockMapper; private final StockFlowMapper stockFlowMapper; public StockService(StockMapper stockMapper, StockFlowMapper stockFlowMapper) { this.stockMapper stockMapper; this.stockFlowMapper stockFlowMapper; } Transactional(rollbackFor Exception.class) public void outbound(StockFlow flow) { int beforeQty stockMapper.selectQuantity(flow.getWarehouseId(), flow.getProductId()); int rows stockMapper.decreaseStock(flow.getWarehouseId(), flow.getProductId(), flow.getQuantity()); if (rows 0) { throw new BusinessException(庫存不足); } flow.setBeforeQty(beforeQty); flow.setAfterQty(beforeQty - flow.getQuantity()); stockFlowMapper.insert(flow); } }rollbackFor Exception.class 這個(gè)參數(shù)必須寫。Spring 默認(rèn)只會(huì)對 RuntimeException 回滾而很多項(xiàng)目里的 BusinessException 是受檢異常不顯式配置的話庫存已經(jīng)扣了異常向上拋時(shí)事務(wù)還是提交了。另一個(gè)坑是同類內(nèi)部調(diào)用this.outbound() 調(diào)用不會(huì)經(jīng)過 Spring 代理Transactional 會(huì)失效。要保證事務(wù)生效必須通過注入的 StockService 對象跨類調(diào)用。4.2 SQL Server 的鎖寫法與 MySQL 的差異MySQL 里常用 SELECT ... FOR UPDATE 鎖行SQL Server 的語法完全不同。如果業(yè)務(wù)上需要先查詢庫存再根據(jù)查到的值決定下一步SQL Server 里最好的做法是不要用普通 SELECT而是用鎖提示SELECT quantity FROM stock WITH (UPDLOCK, ROWLOCK) WHERE warehouse_id #{warehouseId} AND product_id #{productId};UPDLOCK 指定在行上加更新鎖直到事務(wù)結(jié)束才釋放ROWLOCK 告訴優(yōu)化器使用行級鎖而不是頁鎖。這樣兩個(gè)事務(wù)并發(fā)讀同一行庫存時(shí)后一個(gè)會(huì)被阻塞在當(dāng)前事務(wù)提交之后讀到的都是上一個(gè)事務(wù)提交后的結(jié)果。日常出庫場景推薦直接用條件 update因?yàn)闂l件 update 本身鎖的行范圍更小也不用先查后改但盤點(diǎn)前鎖定庫存做數(shù)量確認(rèn)時(shí)上面的 SELECT WITH 寫法更直觀。4.3 盤點(diǎn)差異不直接改庫存盤點(diǎn)流程不能直接 UPDATE stock 把賬面數(shù)改成實(shí)盤數(shù)那樣會(huì)失去差異記錄。正確步驟是先記錄盤點(diǎn)單算出差異值然后照常走一次盤點(diǎn)調(diào)整流水flow_type 為 3quantity 為正數(shù)表示盤盈負(fù)數(shù)表示盤虧。庫存臺(tái)賬的更新仍然走 increaseStock 或 decreaseStock 方法只是業(yè)務(wù)單號(hào)填盤點(diǎn)單號(hào)。這樣盤點(diǎn)動(dòng)作可以復(fù)現(xiàn)、可以審計(jì)也可以在下個(gè)盤點(diǎn)周期做趨勢對比。場景推薦 SQL 寫法鎖行為日常出庫扣減UPDATE stock SET quantity quantity - ? WHERE quantity ?更新行時(shí)排他鎖盤點(diǎn)前讀取并鎖定SELECT ... WITH (UPDLOCK, ROWLOCK)更新鎖保持到事務(wù)結(jié)束庫存展示查詢普通 SELECT快照讀不加鎖4.4 MyBatis 緩存在庫存查詢上的教訓(xùn)MyBatis 一級緩存默認(rèn)開啟作用域是同一個(gè) SqlSession。在庫存頻繁變動(dòng)的場景里同一個(gè)事務(wù)里第一次查庫存得到 100接著另一個(gè)請求把庫存改成 90同一個(gè) SqlSession 第二次查詢可能直接命中一級緩存返回舊的 100。這個(gè)問題不容易復(fù)現(xiàn)但會(huì)導(dǎo)致頁面顯示和數(shù)據(jù)庫不一致。我的建議是庫存表和流水表徹底避開緩存查詢方法上顯式設(shè)置 flushCache 或者直接用普通查詢商品主數(shù)據(jù)相對靜態(tài)可以把數(shù)據(jù)交給 Spring 的本地緩存或 Redis 管理而不是依賴 MyBatis 的二級緩存。凡是需要實(shí)時(shí)讀數(shù)的表都不要放在 MyBatis 二級緩存里。5. SQL Server 2019 部署與首啟動(dòng)驗(yàn)證建庫賬號(hào)、排序規(guī)則、回滾測試5.1 用 sqlcmd 建庫建賬號(hào)繞開 SSMS 交互SQL Server 2019 安裝時(shí)選混合認(rèn)證模式并設(shè)置 sa 密碼。安裝完成后打開命令行工具執(zhí)行sqlcmd -S localhost -U sa -P MyPssw0rd -Q CREATE DATABASE supermarket不要用 sa 跑業(yè)務(wù)再創(chuàng)建一個(gè)最小權(quán)限的登錄名sqlcmd -S localhost -U sa -P MyPssw0rd -d master -Q CREATE LOGIN shop_user WITH PASSWORDShop123; sqlcmd -S localhost -U shop_user -P Shop123 -d supermarket -Q CREATE USER shop_user FOR LOGIN shop_user; sqlcmd -S localhost -U shop_user -P Shop123 -d supermarket -Q EXEC sp_addrolemember db_datareader, shop_user; EXEC sp_addrolemember db_datawriter, shop_user;然后修改 application.yml 里的 username 和 password使用 shop_user 連接。首啟動(dòng)前執(zhí)行 init.sql 建表并插入基礎(chǔ)數(shù)據(jù)再啟動(dòng) SpringBoot 項(xiàng)目。SpringBoot 能夠正常啟動(dòng)只是第一步真正要驗(yàn)證的是讀寫鏈路。5.2 最容易翻車的三項(xiàng)配置問題現(xiàn)象原因處理方式Caused by: SSL connection errorJDBC 驅(qū)動(dòng)默認(rèn)校驗(yàn)主機(jī)名證書URL 加 encryptfalse;trustServerCertificatetrue無法登錄 sa 用戶安裝時(shí)沒啟用混合認(rèn)證模式用 SQL Server 配置管理器打開混合認(rèn)證并重啟服務(wù)中文亂碼或比較異常實(shí)例排序規(guī)則不是 Chinese_PRC_CI_AS建庫時(shí)指定 COLLATE Chinese_PRC_CI_AS如果是 1433 端口連不上先檢查 Windows 防火墻是否放行 TCP 1433。SQL Server 的默認(rèn)實(shí)例沒有動(dòng)態(tài)端口問題命名實(shí)例才需要啟用 SQL Browser 服務(wù)這類系統(tǒng)通常不需要命名實(shí)例。5.3 用一條必失敗的出庫驗(yàn)證事務(wù)和流水項(xiàng)目跑通后先做一個(gè)回滾測試。臨時(shí)寫一個(gè)部署階段專用的 Service 方法插入流水后立刻拋異常觀察數(shù)據(jù)庫里是否殘留數(shù)據(jù)Transactional public void testRollbackOnPurpose() { StockFlow flow new StockFlow(); flow.setBizNo(TEST-ROLLBACK-001); flow.setFlowType((byte) 1); stockFlowMapper.insert(flow); throw new BusinessException(故意失敗驗(yàn)證回滾); }調(diào)用這個(gè)方法后查 stock_flow 表如果 TEST-ROLLBACK-001 這條流水不存在說明事務(wù)回滾機(jī)制正常。如果這條流水還在就去查兩個(gè)地方方法上是否寫了 rollbackFor Exception.class以及調(diào)用方有沒有在同一個(gè)類里直接調(diào)用這個(gè)方法繞過了 Spring 代理。這個(gè)驗(yàn)證通過后再討論性能優(yōu)化和鎖等待才有意義。本文還有配套的精品資源點(diǎn)擊獲取