生鮮配送系統(tǒng)源碼實戰(zhàn):訂單狀態(tài)機與庫存扣減全鏈路解析)
簡介這份資源是面向高校計算機專業(yè)學(xué)生與Java Web開發(fā)學(xué)習(xí)者的社區(qū)生鮮配送系統(tǒng)完整項目源碼圍繞大學(xué)生及周邊居民在線購買生鮮的場景覆蓋用戶管理、商品管理、訂單處理、庫存控制、配送調(diào)度、支付接口、數(shù)據(jù)分析、客戶服務(wù)與移動端適配等核心業(yè)務(wù)模塊適合作為課程設(shè)計、畢業(yè)設(shè)計或SSM/Spring Boot實戰(zhàn)練手參考。壓縮包共909個文件約1.61MB以html頁面、css樣式、js腳本與png/gif圖片構(gòu)成前端展示層120個java源文件與152個class文件對應(yīng)后臺控制器與業(yè)務(wù)邏輯另有sql建表腳本、xml與yml配置文件及json數(shù)據(jù)文件目錄結(jié)構(gòu)清晰便于按模塊檢索。已有139人學(xué)習(xí)下載讀者可從中獲取一套可直接運行的三層架構(gòu)項目骨架、完整的數(shù)據(jù)庫設(shè)計與實體關(guān)系、后臺管理端增刪改查實現(xiàn)思路以及訂單狀態(tài)流轉(zhuǎn)、庫存扣減與配送調(diào)度的具體編碼參考便于對照理解并二次開發(fā)。1. 高校社區(qū)生鮮配送系統(tǒng)一份能跑通“下單—分揀—配送”全鏈路的源碼包很多做課程設(shè)計或接私活的開發(fā)者一提到“生鮮配送系統(tǒng)”就頭大訂單要按小區(qū)聚合、庫存要按批次扣減、配送要按路線分單三件事攪在一起寫出來的代碼往往只能演示不能落地。這份《高校社區(qū)生鮮配送系統(tǒng).zip》解決的正是這個痛點——它把“用戶下單、倉庫分揀、騎手配送”這條主鏈路拆成了可獨立調(diào)試的模塊適合計算機專業(yè)做畢業(yè)設(shè)計的學(xué)生、想練手完整業(yè)務(wù)閉環(huán)的初級后端以及需要快速搭一套社區(qū)團購原型的從業(yè)者。我拆包跑了一遍前后端分離、數(shù)據(jù)庫腳本齊全最值得看的是訂單狀態(tài)機和庫存扣減這兩塊坑基本都埋在那里。下面按“它是什么—怎么跑起來—哪里會翻車”的順序講透。2. 系統(tǒng)架構(gòu)與數(shù)據(jù)模型先看懂表結(jié)構(gòu)再動手拿到一個陌生源碼包我一般不會急著npm install而是先翻數(shù)據(jù)庫腳本。表結(jié)構(gòu)看不懂后面調(diào)接口全是盲人摸象。這個項目的核心表不多但字段設(shè)計有幾個地方值得單獨拎出來說。2.1 六張核心表撐起整條鏈路打開sql/目錄下的建表腳本能理出六張主表用戶表、商品表、庫存批次表、訂單主表、訂單明細(xì)表、配送任務(wù)表。真正決定系統(tǒng)能不能跑通的是后四張之間的關(guān)聯(lián)關(guān)系。表名關(guān)鍵字段作用易錯點productid、name、price、unit商品基礎(chǔ)信息價格用decimal(10,2)別用 floatstock_batchproduct_id、batch_no、quantity、expire_at按批次管理庫存生鮮必須帶保質(zhì)期扣減要先進先出order_mainorder_no、user_id、status、community_id訂單主記錄status是狀態(tài)機核心別隨意加值order_itemorder_id、product_id、batch_id、qty訂單明細(xì)必須記錄實際扣減的批次號方便追溯delivery_taskorder_id、rider_id、route_seq、status配送任務(wù)route_seq決定同一小區(qū)內(nèi)的配送順序這里有個設(shè)計取舍值得說庫存沒有直接掛在product表上而是單獨拆了stock_batch。生鮮場景下同一商品不同批次進價和保質(zhì)期都不同扣庫存必須按批次先進先出否則會出現(xiàn)“新貨先出、舊貨爛倉”的玄學(xué)問題。訂單明細(xì)里存batch_id就是為了事后能查清每一單到底扣了哪批貨出問題時有據(jù)可查。2.2 訂單狀態(tài)機五個狀態(tài)不能亂跳order_main.status的取值在代碼里是硬編碼的我數(shù)了一下共五個待支付、已支付待分揀、分揀完成待配送、配送中、已完成。狀態(tài)流轉(zhuǎn)集中在service/OrderService里用了一個簡單的switch做校驗。// OrderService.java 狀態(tài)流轉(zhuǎn)核心邏輯節(jié)選 public void transitStatus(Long orderId, OrderStatus target) { OrderMain order orderMapper.selectById(orderId); OrderStatus current order.getStatus(); // 只允許按預(yù)設(shè)路徑前進禁止回退和跳級 if (!current.canTransitTo(target)) { throw new BizException(非法狀態(tài)流轉(zhuǎn): current - target); } order.setStatus(target); orderMapper.updateById(order); // 狀態(tài)變更后觸發(fā)對應(yīng)副作用分揀、派單、通知 statusHandlerMap.get(target).handle(order); }這段代碼的邏輯說明canTransitTo是一個枚舉方法內(nèi)部維護了允許的下一狀態(tài)集合比如“待支付”只能到“已支付待分揀”“配送中”只能到“已完成”。參數(shù)target是目標(biāo)狀態(tài)傳錯直接拋業(yè)務(wù)異常。這樣寫的好處是把狀態(tài)規(guī)則收在一處壞處是新增狀態(tài)要改枚舉和 handler 兩處擴展時容易漏。我一般會在枚舉里加一個ListOrderStatus nextStates字段把規(guī)則數(shù)據(jù)化比switch好維護。2.3 庫存扣減放在哪一層一個容易吵起來的問題項目把庫存扣減放在OrderService.createOrder里下單即扣。這在高校社區(qū)場景下是合理的——下單量小、并發(fā)低先扣后付能防止超賣。但如果照搬到高并發(fā)場景就會翻車后面避坑章節(jié)細(xì)說。扣減邏輯本身用了UPDATE ... WHERE quantity ?的樂觀寫法-- 扣減指定批次庫存影響行數(shù)為 0 說明庫存不足 UPDATE stock_batch SET quantity quantity - #{qty} WHERE id #{batchId} AND quantity #{qty};參數(shù)說明batchId來自分揀時選定的批次qty是訂單明細(xì)數(shù)量??繑?shù)據(jù)庫行鎖保證原子性影響行數(shù)為 0 時上層拋“庫存不足”。這種寫法比先SELECT再UPDATE安全但前提是批次要選對——選批次的邏輯在StockService.pickBatch里按expire_at升序取第一個夠量的批次這就是先進先出的落地。3. 本地跑通全流程從建庫到下單成功看懂表結(jié)構(gòu)后就可以動手了。這個包是前后端分離的后端 Spring Boot、前端 Vue數(shù)據(jù)庫 MySQL 8。我按實際跑通的順序?qū)懨羁梢灾苯映?.1 環(huán)境準(zhǔn)備與建庫先確認(rèn)本機有 JDK 8、Maven 3.6、Node 14、MySQL 8。版本別貪新我試過 JDK 17 跑這個包有個老版本依賴會報模塊訪問異常換回 JDK 8 就沒事。# 1. 建庫并導(dǎo)入表結(jié)構(gòu)和初始數(shù)據(jù) mysql -uroot -p -e CREATE DATABASE fresh_delivery DEFAULT CHARSET utf8mb4; mysql -uroot -p fresh_delivery sql/schema.sql mysql -uroot -p fresh_delivery sql/data.sql # 2. 改后端數(shù)據(jù)庫配置 # 編輯 src/main/resources/application-dev.yml # spring.datasource.url / username / password 三處改成自己的 # 3. 啟動后端 mvn clean package -DskipTests java -jar target/fresh-delivery-1.0.jar --spring.profiles.activedev邏輯說明schema.sql只建表data.sql灌了三個小區(qū)、二十個商品和幾個測試賬號分開導(dǎo)入方便你只重置數(shù)據(jù)不動結(jié)構(gòu)。application-dev.yml里默認(rèn)連的是localhost:3306/fresh_delivery賬號密碼是占位符不改必報連接失敗。啟動參數(shù)--spring.profiles.activedev不能省否則會去讀生產(chǎn)配置。3.2 前端啟動與接口聯(lián)調(diào)后端起來后默認(rèn) 8080前端在web/目錄下cd web npm install # 開發(fā)環(huán)境接口地址在 .env.development 里默認(rèn)指向 http://localhost:8080 npm run serve前端跑在 8081登錄頁用data.sql里的測試賬號手機號加驗證碼驗證碼在開發(fā)模式下固定為 1234寫在AuthController的注釋里。登錄后進商品列表選一個小區(qū)、加購、下單能看到訂單狀態(tài)從“待支付”變成“已支付待分揀”——這一步能走通說明訂單、庫存、狀態(tài)機三條線都通了。3.3 用接口腳本驗證分揀和配送頁面點一遍只能驗證主流程分揀和配送的邊界得用接口腳本壓。項目自帶docs/api.http我用 curl 復(fù)現(xiàn)了關(guān)鍵幾步# 1. 管理員登錄拿 token TOKEN$(curl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {phone:13800000001,code:1234} | jq -r .data.token) # 2. 查詢待分揀訂單 curl -s http://localhost:8080/api/admin/orders?statusPAID_WAIT_PICK \ -H Authorization: $TOKEN | jq .data[].orderNo # 3. 對指定訂單執(zhí)行分揀會自動選批次扣庫存 curl -s -X POST http://localhost:8080/api/admin/orders/1/pick \ -H Authorization: $TOKEN # 4. 生成配送任務(wù)并分配騎手 curl -s -X POST http://localhost:8080/api/admin/delivery/dispatch \ -H Authorization: $TOKEN \ -H Content-Type: application/json \ -d {communityId:1,riderId:2}參數(shù)說明第 3 步的1是訂單主鍵分揀接口內(nèi)部會調(diào)StockService.pickBatch逐條明細(xì)選批次任何一條庫存不足整個分揀回滾。第 4 步的communityId決定按哪個小區(qū)聚合訂單riderId是騎手用戶 id派單后delivery_task.route_seq會按訂單創(chuàng)建時間排序生成。跑完這四步數(shù)據(jù)庫里訂單狀態(tài)應(yīng)該是“配送中”stock_batch對應(yīng)批次數(shù)量減少delivery_task多出一條記錄——三張表對得上鏈路才算真通。4. 避坑與排查五個我實際踩過的坑這套代碼邏輯不算復(fù)雜但生鮮業(yè)務(wù)本身的特殊性會放大一些細(xì)節(jié)問題。下面五條都是我跑的時候真實遇到的按“現(xiàn)象—原因—解決”記下來你大概率也會撞上。4.1 下單成功但庫存沒減事務(wù)沒生效現(xiàn)象調(diào)下單接口返回成功訂單表有記錄但stock_batch.quantity紋絲不動。原因createOrder方法上標(biāo)了Transactional但類內(nèi)部另一個方法通過this.調(diào)用它Spring 的代理沒攔住事務(wù)根本沒開。解決把扣庫存邏輯抽到獨立的StockService里注入調(diào)用或者用AopContext.currentProxy()拿代理對象。我一般選前者干凈。4.2 分揀時提示庫存不足但頁面顯示有貨批次選錯現(xiàn)象商品總庫存明明夠分揀卻報“庫存不足”。原因pickBatch按expire_at升序選批次如果最早過期的批次量不夠它不會自動跨批次湊直接返回失敗。解決這是設(shè)計上的簡化單批次不夠時應(yīng)拆成多條明細(xì)跨批次扣減。改法是在pickBatch里循環(huán)取批次直到湊夠數(shù)量每取一批生成一條order_item的批次分?jǐn)傆涗?。不改的話運營上就得保證單批次量大于單筆訂單量。4.3 同一小區(qū)訂單配送順序亂route_seq 沒重排現(xiàn)象派單后騎手端看到的配送順序和下單時間對不上。原因dispatch接口只在首次生成任務(wù)時寫route_seq后續(xù)追加訂單沒有重排。解決每次派單后對該小區(qū)所有未完成任務(wù)按create_time重新編號或者干脆在查詢時用ORDER BY create_time代替依賴route_seq。后者更省事但分頁時會有點別扭看你的量級。4.4 前端跨域報錯開發(fā)代理沒配現(xiàn)象前端 8081 調(diào)后端 8080瀏覽器控制臺一片 CORS 紅字。原因.env.development里接口地址寫的是絕對路徑?jīng)]走vue.config.js的 devServer 代理。解決把接口 baseURL 改成/api在vue.config.js里配proxy: { /api: { target: http://localhost:8080, changeOrigin: true } }。生產(chǎn)環(huán)境用 Nginx 反代同理。4.5 測試數(shù)據(jù)重復(fù)導(dǎo)入主鍵沖突現(xiàn)象第二次執(zhí)行data.sql報Duplicate entry。原因腳本用的是固定主鍵插入沒加INSERT IGNORE或ON DUPLICATE KEY UPDATE。解決重置數(shù)據(jù)時先TRUNCATE相關(guān)表再導(dǎo)入或者把腳本里的INSERT INTO改成INSERT IGNORE INTO。我習(xí)慣在data.sql開頭加一段SET FOREIGN_KEY_CHECKS0; TRUNCATE ...; SET FOREIGN_KEY_CHECKS1;一鍵重置不鬧心。提示改完任何一處庫存或狀態(tài)邏輯務(wù)必用 3.3 的接口腳本把“下單—分揀—派單”重跑一遍光看頁面容易漏掉表間不一致。5. 進階改造把單機扣庫存換成可驗證的并發(fā)方案這套包默認(rèn)是單機低并發(fā)場景下單即扣庫存的寫法在高校社區(qū)里夠用。但如果你要拿它當(dāng)“高并發(fā)秒殺”的課程加分項或者想驗證自己對樂觀鎖的理解就得動庫存這塊。我給一個我實際改過、能跑通的方案順帶說怎么驗證它真的防住了超賣。核心思路是把“選批次”和“扣減”合并成一次帶條件的更新避免先查后改的窗口。改造后的扣減 SQL 在原有基礎(chǔ)上加了版本號-- 帶版本號的樂觀扣減version 每次更新自增 UPDATE stock_batch SET quantity quantity - #{qty}, version version 1 WHERE id #{batchId} AND quantity #{qty} AND version #{version};參數(shù)說明version是查詢批次時讀到的版本號更新時帶上影響行數(shù)為 0 說明要么庫存不夠、要么被別人搶先改了上層重試一次重新讀批次即可。重試次數(shù)我設(shè)成 3超過就拋“庫存緊張請重試”。這個方案比純quantity qty多了一層版本校驗在批次被并發(fā)修改時能更早發(fā)現(xiàn)沖突代價是多一次重試邏輯。驗證方法我用了一個土辦法但很有效寫個 JMeter 或 Python 腳本對同一批次并發(fā)發(fā) 100 個下單請求每單買 1 件批次初始庫存設(shè) 50。跑完查stock_batch.quantity應(yīng)該正好是 0訂單成功數(shù)正好 50多一件或少一件都說明扣減有漏洞。# 并發(fā)驗證腳本核心片段requests ThreadPoolExecutor import requests from concurrent.futures import ThreadPoolExecutor def place_order(_): return requests.post(http://localhost:8080/api/order/create, json{productId: 1, qty: 1, communityId: 1}, headers{Authorization: TOKEN}).status_code with ThreadPoolExecutor(max_workers50) as pool: results list(pool.map(place_order, range(100))) print(成功數(shù):, results.count(200), 庫存余量:, query_stock(1))這段腳本的邏輯說明50 個線程并發(fā)打 100 個請求統(tǒng)計返回 200 的數(shù)量再查一次批次余量。預(yù)期是成功 50、余量 0。如果成功數(shù)超過 50說明扣減沒鎖住如果余量是負(fù)數(shù)說明quantity qty條件被繞過了。我第一次跑的時候成功數(shù)是 52查了半天發(fā)現(xiàn)是重試邏輯把“版本沖突”也當(dāng)成了可重試結(jié)果重試時又扣了一次——血淚經(jīng)驗重試前必須重新讀批次和版本號不能拿舊參數(shù)硬重試。改完這套系統(tǒng)就從“能演示”變成了“能講清楚為什么不會超賣”答辯或評審時這一塊最容易被追問提前準(zhǔn)備好并發(fā)驗證的數(shù)據(jù)比背概念管用。從那以后我每次改庫存相關(guān)代碼都強制先跑一遍這個并發(fā)腳本再提交寧可多花十分鐘也不想上線后被超賣問題追著跑。希望幫到你。本文還有配套的精品資源點擊獲取