超市進銷存商城源碼:前后臺庫存打通與對賬實戰(zhàn))
簡介本資源為基于JSP與SSM框架的社區(qū)生活超市進銷存購物商城管理系統(tǒng)畢業(yè)設(shè)計全套資料面向計算機相關(guān)專業(yè)需要完成課程設(shè)計或畢業(yè)設(shè)計的學生以及希望學習SSM整合開發(fā)的初級開發(fā)者。系統(tǒng)采用MySQL數(shù)據(jù)庫、JDK1.8兼容Eclipse、MyEclipse、STS、IDEA等開發(fā)工具功能覆蓋管理員、供應(yīng)商、用戶三種角色包含個人中心、用戶管理、供應(yīng)商管理、商品類型與信息管理、商品進貨退貨、訂單管理、銷售出庫、商品盤點、超市資訊及系統(tǒng)管理等模塊前臺提供首頁、商品信息與后臺跳轉(zhuǎn)入口。資源包內(nèi)含源碼、數(shù)據(jù)庫腳本、論文、答辯PPT、環(huán)境工具包及同框架項目的安裝教程說明文檔壓縮包為rar格式整體約51.55MB。目前已有33人學習下載適合需要完整賽題方案、可運行代碼與配套文檔的讀者參考使用。1. 從一份 JSPSSM 源碼說起社區(qū)超市進銷存商城到底在解決什么問題很多同學拿到「畢業(yè)設(shè)計jspSSM的社區(qū)生活超市進銷存購物商城管理源碼含文檔含教程」這個題目時第一反應(yīng)是去搜一堆 jsp 源碼、進銷存源碼、購物商城源碼然后拼一個能跑起來的殼子交差。但真正做過社區(qū)超市這類業(yè)務(wù)的人會告訴你它和普通電商最大的區(qū)別在于前臺是購物商城后臺是進銷存兩者共用一套庫存和訂單數(shù)據(jù)。用戶下單買走兩瓶醬油后臺庫存要實時扣減采購入庫一批生鮮前臺商品的可售數(shù)量要同步變化。這個「前后臺數(shù)據(jù)打通」才是這類系統(tǒng)真正的技術(shù)難點也是答辯老師最愛追問的地方。這篇文章面向三類人正在做這個畢業(yè)設(shè)計的同學、想用 SSM 快速搭一套小型進銷存商城的開發(fā)者、以及需要給社區(qū)超市做一套輕量管理系統(tǒng)的從業(yè)者。我會把 JSPSSM 這套技術(shù)棧在進銷存商城場景下的選型理由、數(shù)據(jù)庫設(shè)計、核心模塊實現(xiàn)、以及我踩過的坑按能復現(xiàn)的順序講清楚。你跟著走完能拿到一套結(jié)構(gòu)清晰、能跑通下單扣庫存全流程的工程骨架而不是一個只能看不能用的演示頁面。2. 技術(shù)選型與工程骨架為什么是 JSPSSM 而不是別的2.1 在 2024 年還選 JSPSSM 的四個現(xiàn)實理由先說清楚JSPSSMSpring SpringMVC MyBatis在今天不是最時髦的組合前端早就是 Vue/React 的天下了。但放到畢業(yè)設(shè)計和中小型社區(qū)超市系統(tǒng)這個具體場景里它有幾個繞不開的優(yōu)勢。第一是學習成本可控。SSM 三層架構(gòu)Controller-Service-DAO的職責劃分非常清晰MyBatis 把 SQL 寫在 XML 里對于要講清楚「數(shù)據(jù)怎么從頁面流到數(shù)據(jù)庫」的答辯場景比 MyBatis-Plus 那種全自動 ORM 更容易解釋。第二是資料密度高遇到問題搜 jsp、mybatis 源碼、springframework 源碼運行這類關(guān)鍵詞能找到大量可參考的實現(xiàn)。第三是部署簡單打成一個 war 包丟進 Tomcat 就能跑不需要 Node 環(huán)境、不需要前后端分離的跨域配置。第四是文檔和教程配套完整這類題目通常要求含文檔含教程SSM 的配置流程是標準化的寫文檔時不容易卡殼。需要提醒的是如果你的目標是做一個真正上線的社區(qū)團購系統(tǒng)那 SSM 的并發(fā)能力和開發(fā)效率都不夠看應(yīng)該考慮 SpringBoot Vue。但作為畢業(yè)設(shè)計SSM 的性價比是最高的。2.2 工程目錄結(jié)構(gòu)與依賴清單一個能跑通的 SSM 進銷存商城標準目錄結(jié)構(gòu)如下。我一般會按功能模塊分包而不是按技術(shù)層分包這樣后期加功能時不用在四個包里來回跳。community-market/ ├── src/main/java/com/market/ │ ├── controller/ # 頁面請求入口 │ │ ├── GoodsController.java │ │ ├── OrderController.java │ │ └── StockController.java │ ├── service/ # 業(yè)務(wù)邏輯 │ │ ├── GoodsService.java │ │ └── OrderService.java │ ├── dao/ # MyBatis Mapper 接口 │ ├── entity/ # 實體類 │ └── util/ # 工具類 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML 映射文件 │ ├── spring-dao.xml │ ├── spring-service.xml │ ├── spring-mvc.xml │ └── jdbc.properties ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── jsp/ # 所有 JSP 頁面 │ └── static/ # css/js/圖片 └── pom.xmlMaven 依賴的核心部分注意版本要統(tǒng)一Spring 5.x 配 MyBatis 3.5.x 是經(jīng)過驗證的穩(wěn)定組合!-- pom.xml 核心依賴 -- dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency !-- MyBatis 與 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.29/version /dependency !-- JSP 相關(guān) -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies這段依賴里mybatis-spring是整合的關(guān)鍵它負責把 MyBatis 的 SqlSession 交給 Spring 管理。mysql-connector-java用 8.x 版本時JDBC URL 必須加時區(qū)參數(shù)否則啟動就報時區(qū)錯誤這是新手最常見的翻車點之一。2.3 數(shù)據(jù)庫表設(shè)計進銷存和商城如何共用一套庫存這是整個系統(tǒng)設(shè)計的核心。很多人把商城和進銷存做成兩套表結(jié)果庫存對不上。正確做法是商品表只有一張庫存字段掛在商品上所有出入庫都通過庫存流水表記錄。表名作用關(guān)鍵字段goods商品主表id, name, price, stock, category_idstock_record庫存流水id, goods_id, type(入庫/出庫), num, create_timeorders訂單主表id, user_id, total_price, statusorder_item訂單明細id, order_id, goods_id, num, pricepurchase采購單id, goods_id, num, supplier, status庫存扣減的邏輯必須放在 Service 層用事務(wù)包起來先查庫存夠不夠再扣減再寫流水三步要么全成功要么全回滾。下面這段是下單扣庫存的核心代碼// OrderServiceImpl.java 下單扣庫存 Transactional(rollbackFor Exception.class) public void createOrder(Order order, ListOrderItem items) { for (OrderItem item : items) { // 1. 查詢商品當前庫存加行鎖防止并發(fā)超賣 Goods goods goodsMapper.selectForUpdate(item.getGoodsId()); if (goods.getStock() item.getNum()) { throw new RuntimeException(庫存不足 goods.getName()); } // 2. 扣減庫存 goodsMapper.reduceStock(item.getGoodsId(), item.getNum()); // 3. 寫庫存流水 StockRecord record new StockRecord(); record.setGoodsId(item.getGoodsId()); record.setType(OUT); record.setNum(item.getNum()); stockRecordMapper.insert(record); } // 4. 寫訂單主表和明細 orderMapper.insert(order); orderItemMapper.batchInsert(items); }selectForUpdate是關(guān)鍵它在 SQL 層加了行鎖防止兩個用戶同時下單把庫存扣成負數(shù)。Transactional的rollbackFor Exception.class保證任何異常都回滾默認只回滾 RuntimeException加上這個更保險。庫存流水表的作用是后悔藥一旦發(fā)現(xiàn)庫存對不上可以順著流水一條條查是哪一步出的問題。3. 前臺購物商城模塊從商品列表到下單支付3.1 商品列表與詳情頁的 JSP 實現(xiàn)前臺頁面用 JSP JSTL 渲染數(shù)據(jù)由 Controller 塞進 Model 再轉(zhuǎn)發(fā)到 JSP。商品列表頁的核心是把分類篩選和分頁做出來這是答辯時容易被問的點。// GoodsController.java 商品列表 RequestMapping(/goods/list) public String list(RequestParam(defaultValue 1) Integer page, RequestParam(required false) Integer categoryId, Model model) { // 每頁 12 條社區(qū)超市商品不多不用太大 PageHelper.startPage(page, 12); ListGoods goodsList goodsService.listByCategory(categoryId); PageInfoGoods pageInfo new PageInfo(goodsList); model.addAttribute(pageInfo, pageInfo); model.addAttribute(categoryId, categoryId); return goods/list; }這里用了 PageHelper 插件做分頁startPage必須緊跟在查詢方法前一行中間不能插別的查詢否則分頁會串。JSP 頁面里用c:forEach遍歷pageInfo.list用c:if判斷庫存為 0 時顯示「已售罄」。商品圖片的路徑建議存相對路徑比如/static/upload/xxx.jpg不要存絕對路徑換臺機器部署就找不到圖了。3.2 購物車與訂單提交的完整鏈路購物車有兩種實現(xiàn)存 Session 或存數(shù)據(jù)庫。社區(qū)超市這種場景我建議存數(shù)據(jù)庫因為用戶可能換設(shè)備而且訂單提交時要校驗購物車數(shù)據(jù)存庫更可靠。購物車表結(jié)構(gòu)很簡單cart(id, user_id, goods_id, num, create_time)。加入購物車時先查是否已存在存在就累加數(shù)量不存在就插入。提交訂單時從購物車讀出所有條目逐條校驗庫存然后調(diào)用第 2 章寫的createOrder方法。// CartController.java 提交訂單 RequestMapping(/order/submit) public String submit(HttpSession session, Model model) { User user (User) session.getAttribute(loginUser); if (user null) { return redirect:/user/login; } ListCart cartList cartService.listByUser(user.getId()); if (cartList.isEmpty()) { model.addAttribute(msg, 購物車為空); return cart/list; } try { orderService.createOrderFromCart(user.getId(), cartList); cartService.clearByUser(user.getId()); // 清空購物車 return redirect:/order/list; } catch (RuntimeException e) { model.addAttribute(msg, e.getMessage()); return cart/list; } }注意createOrderFromCart內(nèi)部要把 Cart 轉(zhuǎn)成 OrderItem價格從商品表實時讀取不要用購物車里的價格否則商品調(diào)價后訂單金額就錯了。清空購物車要放在訂單創(chuàng)建成功之后如果放前面訂單失敗購物車就丟了用戶得重新加一遍體驗很差。3.3 訂單狀態(tài)流轉(zhuǎn)與庫存回滾訂單狀態(tài)一般有待付款、已付款、已發(fā)貨、已完成、已取消。社區(qū)超市可以簡化去掉發(fā)貨環(huán)節(jié)改成待付款、待取貨、已完成、已取消。關(guān)鍵點是取消訂單時要回滾庫存?;貪L邏輯和扣減相反加回庫存、寫一條 IN 類型的流水。這里有個坑如果訂單已經(jīng)付款了再取消涉及退款畢業(yè)設(shè)計里可以簡化成只允許待付款狀態(tài)取消。// OrderServiceImpl.java 取消訂單回滾庫存 Transactional(rollbackFor Exception.class) public void cancelOrder(Integer orderId) { Order order orderMapper.selectById(orderId); if (!UNPAID.equals(order.getStatus())) { throw new RuntimeException(只有待付款訂單可以取消); } ListOrderItem items orderItemMapper.listByOrder(orderId); for (OrderItem item : items) { goodsMapper.addStock(item.getGoodsId(), item.getNum()); StockRecord record new StockRecord(); record.setGoodsId(item.getGoodsId()); record.setType(IN); record.setNum(item.getNum()); stockRecordMapper.insert(record); } orderMapper.updateStatus(orderId, CANCELED); }狀態(tài)判斷用字符串常量建議定義成枚舉或常量類不要到處寫魔法字符串改起來容易漏。4. 后臺進銷存模塊采購入庫、庫存預警與數(shù)據(jù)看板4.1 采購入庫流程與庫存流水記錄進銷存的核心是「進」和「銷」。進貨就是采購入庫流程是創(chuàng)建采購單 → 審核 → 入庫 → 更新商品庫存 → 寫流水。采購單表purchase(id, goods_id, num, supplier, status, create_time)status 有「待審核」「已入庫」。入庫操作要在一個事務(wù)里完成三件事改采購單狀態(tài)、加商品庫存、寫庫存流水。// StockServiceImpl.java 采購入庫 Transactional(rollbackFor Exception.class) public void purchaseIn(Integer purchaseId) { Purchase purchase purchaseMapper.selectById(purchaseId); if (!PENDING.equals(purchase.getStatus())) { throw new RuntimeException(該采購單已入庫或狀態(tài)異常); } // 1. 加庫存 goodsMapper.addStock(purchase.getGoodsId(), purchase.getNum()); // 2. 寫流水 StockRecord record new StockRecord(); record.setGoodsId(purchase.getGoodsId()); record.setType(IN); record.setNum(purchase.getNum()); record.setRemark(采購入庫單號 purchaseId); stockRecordMapper.insert(record); // 3. 改采購單狀態(tài) purchaseMapper.updateStatus(purchaseId, FINISHED); }remark字段建議一定要留把采購單號寫進去對賬時能直接定位。我見過有人把流水表設(shè)計得只有 goods_id 和 num出了問題完全查不出是哪筆業(yè)務(wù)導致的。4.2 庫存預警的閾值設(shè)置與定時檢查社區(qū)超市最怕的是貨賣空了還不知道。庫存預警的做法是給商品表加一個warn_stock字段表示預警閾值。當庫存低于這個值時后臺首頁標紅提示。檢查方式有兩種一是每次扣減庫存后判斷實時性高但代碼侵入性強二是用 Spring 的定時任務(wù)每隔一段時間掃一遍。我一般用第二種配置簡單對業(yè)務(wù)代碼無侵入。// StockWarnTask.java 定時檢查庫存預警 Component public class StockWarnTask { Autowired private GoodsMapper goodsMapper; // 每天早上 8 點和下午 6 點各檢查一次 Scheduled(cron 0 0 8,18 * * ?) public void checkWarnStock() { ListGoods warnList goodsMapper.listWarnGoods(); if (!warnList.isEmpty()) { // 實際項目里這里發(fā)郵件或站內(nèi)信畢業(yè)設(shè)計打印日志即可 System.out.println(庫存預警商品數(shù)量 warnList.size()); } } }Scheduled需要在 spring 配置里開啟task:annotation-driven/cron 表達式0 0 8,18 * * ?表示每天 8 點和 18 點整執(zhí)行。閾值warn_stock不要設(shè)成固定值生鮮類可以設(shè) 20日用品設(shè) 5按品類區(qū)分更合理。4.3 銷售統(tǒng)計看板用 SQL 聚合代替 Java 循環(huán)后臺首頁一般要展示今日銷售額、熱銷商品 Top10、庫存總量這些指標。新手容易犯的錯是把所有訂單查出來在 Java 里循環(huán)累加數(shù)據(jù)一多就慢。正確做法是用 SQL 聚合。-- 今日銷售額統(tǒng)計 SELECT IFNULL(SUM(total_price), 0) AS todayAmount FROM orders WHERE status FINISHED AND DATE(create_time) CURDATE(); -- 熱銷商品 Top10 SELECT g.name, SUM(oi.num) AS saleNum FROM order_item oi JOIN goods g ON oi.goods_id g.id JOIN orders o ON oi.order_id o.id WHERE o.status FINISHED GROUP BY oi.goods_id ORDER BY saleNum DESC LIMIT 10;IFNULL是為了防止當天沒有訂單時返回 null 導致頁面報錯。DATE(create_time) CURDATE()這種寫法用不上索引數(shù)據(jù)量大時要改成范圍查詢create_time CURDATE() AND create_time CURDATE() INTERVAL 1 DAY。畢業(yè)設(shè)計數(shù)據(jù)量小無所謂但養(yǎng)成好習慣沒壞處。5. 避坑與排查這套源碼跑不起來時先看這幾條5.1 啟動報 404頁面就是出不來現(xiàn)象Tomcat 啟動沒報錯訪問首頁返回 404。原因通常是web.xml里 DispatcherServlet 的映射配錯或者spring-mvc.xml的組件掃描路徑?jīng)]覆蓋到 Controller。解決檢查web.xml中url-pattern是不是/檢查context:component-scan base-packagecom.market.controller/的包名和實際目錄是否一致。還有一個隱蔽原因是 JSP 放在WEB-INF下但視圖解析器前綴寫錯/WEB-INF/jsp/和/WEB-INF/views/差一個字母就 404。5.2 庫存扣成負數(shù)并發(fā)下單出問題現(xiàn)象壓測或多人同時下單時庫存出現(xiàn)負數(shù)。原因是沒有加鎖兩個線程同時查到庫存為 1都判斷夠都扣減。解決在查詢庫存的 SQL 上加FOR UPDATE或者用UPDATE goods SET stock stock - #{num} WHERE id #{id} AND stock #{num}這種帶條件的更新根據(jù)返回的影響行數(shù)判斷是否成功。后者性能更好推薦用。5.3 MyBatis 查詢返回 null字段名對不上現(xiàn)象數(shù)據(jù)庫明明有數(shù)據(jù)查詢返回的對象字段全是 null。原因九成是數(shù)據(jù)庫字段名和實體類屬性名不一致比如數(shù)據(jù)庫是create_time實體類是createTime而 MyBatis 沒開啟駝峰映射。解決在mybatis-config.xml里加setting namemapUnderscoreToCamelCase valuetrue/或者在 SQL 里用AS起別名。我一般兩個都做雙保險。5.4 中文亂碼從頁面到數(shù)據(jù)庫全是問號現(xiàn)象表單提交的中文存進數(shù)據(jù)庫變成???。原因分三處JSP 頁面編碼、請求編碼、數(shù)據(jù)庫連接編碼。解決JSP 頂部加% page contentTypetext/html;charsetUTF-8 %web.xml里配 CharacterEncodingFilter 設(shè)成 UTF-8JDBC URL 加useUnicodetruecharacterEncodingutf8。三處缺一處都可能亂碼按順序排查。5.5 事務(wù)不生效庫存扣了訂單沒生成現(xiàn)象下單時庫存扣了但訂單表沒數(shù)據(jù)或者反過來。原因通常是Transactional加在了 Controller 上或者同類內(nèi)部方法調(diào)用繞過了代理。解決事務(wù)注解必須加在 Service 層的 public 方法上且這個方法要被外部調(diào)用。如果 Service 內(nèi)部 A 方法調(diào) B 方法B 的事務(wù)不會生效要么把 B 抽到另一個 Service要么用 AopContext 拿代理對象。6. 進階技巧把庫存流水做成可追溯的對賬工具前面幾章把系統(tǒng)跑通了這一章講一個能讓你的畢業(yè)設(shè)計在答辯時加分、實際用起來也真正有價值的技巧用庫存流水表做一套完整的對賬機制。大部分人的庫存流水只是記一下出入庫查完就完了。但流水表真正的價值在于「任意時間點的庫存還原」。公式很簡單某時刻庫存 期初庫存 期間所有 IN 流水之和 - 期間所有 OUT 流水之和。把這個公式做成一個對賬頁面輸入起止日期系統(tǒng)自動算出理論庫存再和商品表當前庫存對比不一致就說明有 bug 或者有人直接改了數(shù)據(jù)庫。// StockServiceImpl.java 庫存對賬 public StockCheckVO checkStock(Integer goodsId, String startDate, String endDate) { // 期初庫存起始日期之前的流水匯總 Integer initStock stockRecordMapper.sumBeforeDate(goodsId, startDate); // 期間入庫總量 Integer inNum stockRecordMapper.sumByTypeAndRange(goodsId, IN, startDate, endDate); // 期間出庫總量 Integer outNum stockRecordMapper.sumByTypeAndRange(goodsId, OUT, startDate, endDate); // 理論庫存 int theoryStock (initStock null ? 0 : initStock) inNum - outNum; // 實際庫存 int realStock goodsMapper.selectById(goodsId).getStock(); StockCheckVO vo new StockCheckVO(); vo.setTheoryStock(theoryStock); vo.setRealStock(realStock); vo.setDiff(theoryStock - realStock); return vo; }對應(yīng)的 SQL 用SUM和CASE WHEN就能搞定不需要在 Java 里循環(huán)-- 期初庫存起始日期前的入庫減出庫 SELECT IFNULL(SUM(CASE WHEN type IN THEN num ELSE -num END), 0) FROM stock_record WHERE goods_id #{goodsId} AND create_time #{startDate};這個對賬功能有三個實際好處。第一答辯時老師問「你怎么保證庫存數(shù)據(jù)準確」你可以直接演示對賬頁面比空口說「我用了事務(wù)」有說服力得多。第二實際運營中如果發(fā)現(xiàn)庫存對不上能快速定位是哪天的流水出了問題。第三這套思路可以擴展到金額對賬訂單表的應(yīng)收和支付流水對不上時同樣適用。參數(shù)上要注意startDate和endDate建議用yyyy-MM-dd格式SQL 里用create_time #{startDate} AND create_time DATE_ADD(#{endDate}, INTERVAL 1 DAY)這樣能覆蓋 endDate 當天的全部數(shù)據(jù)避免漏掉當天下午的記錄。這個邊界坑我踩過當時對賬差了 3 筆查了一下午才發(fā)現(xiàn)是日期范圍沒包含當天。最后說個習慣。我做這類系統(tǒng)時任何改動庫存的操作都必須寫流水沒有例外。哪怕是手動在后臺改一個數(shù)字也要補一條備注為「手動調(diào)整」的流水。這樣流水表就是庫存的唯一真相來源商品表的 stock 字段只是流水算出來的一個緩存值。養(yǎng)成這個習慣后庫存問題從「玄學」變成了「查日志」排查時間從半天縮短到幾分鐘。希望幫到你。本文還有配套的精品資源點擊獲取