源WMS倉(cāng)庫(kù)管理系統(tǒng)解析:從批次追溯到庫(kù)存精度提升)
簡(jiǎn)介GreaterWMS倉(cāng)庫(kù)管理系統(tǒng)完整源碼包面向供應(yīng)鏈開(kāi)發(fā)、企業(yè)應(yīng)用系統(tǒng)研究人員及物流倉(cāng)儲(chǔ)從業(yè)者。系統(tǒng)源自福特亞太區(qū)售后物流倉(cāng)儲(chǔ)供應(yīng)鏈流程作者離開(kāi)福特后將其從內(nèi)部定制方案發(fā)展為獨(dú)立項(xiàng)目重點(diǎn)解決庫(kù)存精確性、庫(kù)位分配、出入庫(kù)與訂單處理等效率問(wèn)題。壓縮包共2000個(gè)文件約85.15MB以1529個(gè)JavaScript、179個(gè)Python、105個(gè)Vue組件為主要構(gòu)成輔以CSS、JSON及Markdown文檔覆蓋前端管理界面、后端服務(wù)、數(shù)據(jù)處理邏輯與部署說(shuō)明目錄結(jié)構(gòu)清晰便于按模塊研讀。目前已有42人學(xué)習(xí)適合通過(guò)完整實(shí)戰(zhàn)項(xiàng)目理解WMS架構(gòu)學(xué)習(xí)企業(yè)應(yīng)用前后端開(kāi)發(fā)或參考其從內(nèi)部系統(tǒng)轉(zhuǎn)向商業(yè)產(chǎn)品的設(shè)計(jì)思路。內(nèi)容包括庫(kù)存控制、條碼掃描、報(bào)告生成等可運(yùn)行實(shí)現(xiàn)是結(jié)合真實(shí)業(yè)務(wù)場(chǎng)景深入掌握現(xiàn)代倉(cāng)庫(kù)管理軟件的優(yōu)質(zhì)素材。1. GreaterWMS 是什么從汽車(chē)售后物流走出來(lái)的開(kāi)源倉(cāng)庫(kù)管理系統(tǒng)倉(cāng)庫(kù)管理系統(tǒng)做久了會(huì)發(fā)現(xiàn)一個(gè)真相最難的從來(lái)不是庫(kù)存加減而是批次追溯、逆向退貨和拆零揀貨這三件事。GreaterWMS 正是一套把這三件事按“汽車(chē)售后備件”標(biāo)準(zhǔn)做出來(lái)的開(kāi)源倉(cāng)庫(kù)管理系統(tǒng)——它最早承載的是某大型跨國(guó)整車(chē)廠(chǎng)亞太區(qū)售后物流的完整供應(yīng)鏈流程從備件預(yù)測(cè)、入倉(cāng)上架一直管到經(jīng)銷(xiāo)商訂單下發(fā)和逆向退件。項(xiàng)目作者離開(kāi)那家整車(chē)廠(chǎng)后把這套業(yè)務(wù)邏輯從內(nèi)部系統(tǒng)里抽出來(lái)重寫(xiě)做成了可以獨(dú)立部署的開(kāi)源項(xiàng)目。這套系統(tǒng)適合三類(lèi)人業(yè)務(wù)上被批次強(qiáng)制追溯逼到墻角的備件倉(cāng)和醫(yī)藥倉(cāng)想從 Excel 管理庫(kù)存升級(jí)到系統(tǒng)化管理的中小電商倉(cāng)以及需要拿一套完整 WMS 做二次開(kāi)發(fā)的團(tuán)隊(duì)。它把售后物流里那些“一個(gè)零件用錯(cuò)批次就要全部召回”的嚴(yán)謹(jǐn)性帶到了普通倉(cāng)庫(kù)的日常管理里。說(shuō)到底WMS 不是一個(gè)錄入界面的合集而是一套庫(kù)存狀態(tài)的業(yè)務(wù)規(guī)則引擎。這篇文章會(huì)從業(yè)務(wù)模型、技術(shù)架構(gòu)、部署命令到上線(xiàn)避坑把這條鏈路完整拆給你看。2. 拆解業(yè)務(wù)模型售后件、庫(kù)位、批次和波次是怎么在系統(tǒng)里運(yùn)作的2.1 售后物流和成品物流的差異為什么通用 WMS 容易在這里翻車(chē)多數(shù)通用 WMS 是從電商或產(chǎn)線(xiàn)物料場(chǎng)景寫(xiě)出來(lái)的它們的庫(kù)存模型通常是“SKU 數(shù)量 貨架”批次只是一個(gè)可選的小字段??墒酆髠浼锪鞯牡讓舆壿嬐耆煌?。汽車(chē)售后備件的 SKU 動(dòng)輒五萬(wàn)到二十萬(wàn)遠(yuǎn)比產(chǎn)線(xiàn)物料復(fù)雜單張訂單往往只有三五行的拆零需求卻要求每一件貨都能按批次追溯到出廠(chǎng)時(shí)間、供應(yīng)商、質(zhì)檢報(bào)告。更麻煩的是逆向物流——經(jīng)銷(xiāo)商退回的舊件、索賠件、維修件不是簡(jiǎn)單重新入庫(kù)它們要按狀態(tài)分流處理。在售后場(chǎng)景里通用 WMS 通常會(huì)在三個(gè)地方翻車(chē)。第一是批次在揀貨環(huán)節(jié)被抹掉出庫(kù)記錄里只剩數(shù)量沒(méi)有批號(hào)真要召回時(shí)根本無(wú)從下手第二是逆向退貨直接復(fù)用正向入庫(kù)流程導(dǎo)致同一件貨的庫(kù)存被重復(fù)累計(jì)第三是拆零后原包裝余量找不到存放規(guī)則散件到處亂塞賬實(shí)差異越滾越大。GreaterWMS 的庫(kù)存模型從一開(kāi)始就是圍繞“主數(shù)據(jù)分層”和“批次全鏈路”設(shè)計(jì)的而不是把批次當(dāng)附加字段。它管的不只是貨在哪里、有多少而是每一批貨從哪個(gè)供應(yīng)商來(lái)、在哪個(gè)庫(kù)位、當(dāng)前什么狀態(tài)、能賣(mài)給誰(shuí)。理解這套系統(tǒng)的第一步是接受一個(gè)觀(guān)念轉(zhuǎn)換WMS 的庫(kù)存不是“庫(kù)存余額”而是一組帶有狀態(tài)的庫(kù)位明細(xì)。每一次收貨、上架、揀貨、出庫(kù)、盤(pán)點(diǎn)本質(zhì)都是在做“庫(kù)位明細(xì)狀態(tài)轉(zhuǎn)移”。你在系統(tǒng)里看到的庫(kù)存總數(shù)永遠(yuǎn)是這些明細(xì)的聚合結(jié)果。我見(jiàn)過(guò)很多團(tuán)隊(duì)在實(shí)施時(shí)總想跳過(guò)明細(xì)直接改總數(shù)最后無(wú)一例外倒在賬實(shí)不符上。2.2 主數(shù)據(jù)分層貨主、倉(cāng)庫(kù)、庫(kù)區(qū)、庫(kù)位、批次的關(guān)系GreaterWMS 的主數(shù)據(jù)組織方式可以理解成一棵從上到下的樹(shù)。最頂層是貨主Owner用于區(qū)分自營(yíng)庫(kù)存和第三方代管庫(kù)存貨主下面掛倉(cāng)庫(kù)Warehouse倉(cāng)庫(kù)下面分庫(kù)區(qū)Area庫(kù)區(qū)下面再細(xì)到庫(kù)位Location。每一層都有自己的編碼規(guī)則而編碼本身承擔(dān)著定位信息。庫(kù)位編碼是這套系統(tǒng)里最值得學(xué)習(xí)的細(xì)節(jié)。常見(jiàn)做法是用分段編碼比如CH-A-01-02表示“倉(cāng)庫(kù) CH、庫(kù)區(qū) A存儲(chǔ)區(qū)、貨架 01、層 02”。這種編碼的好處是揀貨路徑能通過(guò)字符串排序直接優(yōu)化——系統(tǒng)按庫(kù)位編碼排序生成揀貨順序揀貨員沿著貨架走就是最短路徑。如果庫(kù)位編碼是隨意編的哪怕系統(tǒng)再先進(jìn)揀貨效率也提不起來(lái)。編碼規(guī)則在項(xiàng)目初始化時(shí)就要定死半路再改是巨大的災(zāi)難。批次Batch是另一個(gè)核心主數(shù)據(jù)。它記錄的是同一供應(yīng)商、同一生產(chǎn)日期、同一質(zhì)量狀態(tài)的貨物集合。系統(tǒng)在收貨時(shí)強(qiáng)制要求錄入批次號(hào)和到期日期后續(xù)所有庫(kù)存移動(dòng)都綁定批次。對(duì)于 ECU、電瓶這類(lèi)需要單品追溯的貴重件還要啟用序列號(hào)Serial Number管理每一件貨都有唯一的身份編碼。下面這張表列出了我在實(shí)施時(shí)重點(diǎn)關(guān)注的主數(shù)據(jù)表數(shù)據(jù)對(duì)象關(guān)鍵字段作用貨主 Owner編碼、名稱(chēng)、結(jié)算方式區(qū)分庫(kù)存歸屬支撐多客戶(hù)共倉(cāng)倉(cāng)庫(kù) Warehouse編碼、地址、時(shí)區(qū)物理倉(cāng)的邊界庫(kù)區(qū) Area編碼、類(lèi)型區(qū)分收貨區(qū)、存儲(chǔ)區(qū)、揀貨區(qū)、退貨區(qū)、不合格區(qū)庫(kù)位 Location編碼、庫(kù)區(qū)、容量、狀態(tài)最小存儲(chǔ)單元承載上架策略SKU編碼、名稱(chēng)、單位、尺寸、重量貨物檔案注意多單位換算批次 Batch批次號(hào)、到期日、入庫(kù)日、質(zhì)檢狀態(tài)追溯的最小邏輯單元序列號(hào) Serial序列號(hào)、SKU、批次、來(lái)源單號(hào)單件級(jí)追溯啟用后揀貨必須掃序列號(hào)主數(shù)據(jù)不干凈后面所有流程都是空中樓閣。我在給一家三方云倉(cāng)做實(shí)施時(shí)光是庫(kù)位編碼就統(tǒng)一了三輪第一輪是客戶(hù)歷史編碼里帶特殊字符第二輪是庫(kù)位狀態(tài)字段含義不一致第三輪是同一個(gè)物理庫(kù)位在系統(tǒng)里錄了兩遍。這些都要在初始化階段全部清干凈再開(kāi)始跑業(yè)務(wù)否則后面每一條庫(kù)存記錄都是錯(cuò)的。2.3 從 ASN 收貨到波次揀貨流程在數(shù)據(jù)上是如何串聯(lián)的業(yè)務(wù)流程從供應(yīng)商預(yù)約送貨開(kāi)始。倉(cāng)庫(kù)先建 ASN到貨預(yù)報(bào)單寫(xiě)明預(yù)計(jì)到達(dá)時(shí)間、SKU、數(shù)量、批次信息。收貨員收到實(shí)物后按 ASN 核對(duì)并生成正式入庫(kù)單。這里有個(gè)關(guān)鍵參數(shù)是否允許“無(wú) ASN 收貨”。售后物流強(qiáng)烈建議關(guān)閉這個(gè)選項(xiàng)因?yàn)榕涡畔⒃?ASN 里已經(jīng)預(yù)先錄好現(xiàn)場(chǎng)補(bǔ)錄非常容易出錯(cuò)。收貨完成后進(jìn)入上架環(huán)節(jié)。系統(tǒng)按上架策略推薦庫(kù)位常見(jiàn)策略有三條同 SKU 合并到已有庫(kù)位減少碎片化按 ABC 分類(lèi)把高頻揀貨 SKU 放到靠近打包區(qū)的庫(kù)位按批次先進(jìn)先出原則將早批次放在可揀貨位、晚批次放入存儲(chǔ)位。這個(gè)動(dòng)作在系統(tǒng)里會(huì)產(chǎn)生一條上架任務(wù)PDA 上掃庫(kù)位碼和箱碼完成綁定。出庫(kù)側(cè)的核心是波次Wave。常見(jiàn)做法是把一段時(shí)間內(nèi)的一批訂單合并成一個(gè)波次系統(tǒng)按訂單行生成揀貨任務(wù)再按庫(kù)位編碼排序輸出揀貨路徑。揀貨員沿路徑一次性把整個(gè)波次的貨揀完到復(fù)核區(qū)按訂單二次分揀、掃描校驗(yàn)、打包出庫(kù)。整個(gè)過(guò)程在數(shù)據(jù)庫(kù)里會(huì)留下完整的移動(dòng)記錄庫(kù)存從庫(kù)位明細(xì)扣減、生成出庫(kù)明細(xì)、批次和序列號(hào)被鎖定到訂單號(hào)上。這套“追蹤鏈路”正是售后追溯的基礎(chǔ)——任何一個(gè)出庫(kù)件都能反查它來(lái)自哪個(gè)批次、哪個(gè)庫(kù)位、哪次收貨。3. 技術(shù)架構(gòu)與數(shù)據(jù)落盤(pán)Django 后臺(tái)、API 與雙模式前端3.1 技術(shù)棧選型為什么是 Python 和 Django 而不是一堆 Node 服務(wù)倉(cāng)庫(kù)管理系統(tǒng)的本質(zhì)是“業(yè)務(wù)對(duì)象多、狀態(tài)流轉(zhuǎn)多、報(bào)表查詢(xún)多、實(shí)時(shí)并發(fā)中低”的典型企業(yè)應(yīng)用它的核心訴求是業(yè)務(wù)邏輯清晰、能快速出后臺(tái)界面、數(shù)據(jù)和權(quán)限可控。Django 在這類(lèi)場(chǎng)景里的優(yōu)勢(shì)非常明顯自帶 ORM、數(shù)據(jù)庫(kù)遷移、Admin 后臺(tái)和權(quán)限框架一個(gè)團(tuán)隊(duì)可以在很短時(shí)間內(nèi)把主數(shù)據(jù)管理和單據(jù)流程全部跑起來(lái)。相比用 Node/Go 從零搭一套微服務(wù)Django 的開(kāi)發(fā)效率在中期維護(hù)階段會(huì)拉開(kāi)明顯差距。從分工上看系統(tǒng)拆成前后端兩層。后端是一套 Django 應(yīng)用對(duì)外提供 JSON API管理后臺(tái)走 Django 自帶的 Admin 界面可以快速處理主數(shù)據(jù)維護(hù)、單據(jù)查詢(xún)這類(lèi)低頻操作一線(xiàn)操作界面則是一套面向移動(dòng)端的 Web 應(yīng)用專(zhuān)門(mén)給掃碼槍、PDA 和手機(jī)瀏覽器用。操作員在 PDA 上打開(kāi)任務(wù)列表點(diǎn)擊任務(wù)開(kāi)始掃一個(gè)條碼提交一個(gè)動(dòng)作界面簡(jiǎn)單到只剩大按鈕和輸入框。這樣設(shè)計(jì)是刻意為之的。倉(cāng)儲(chǔ)現(xiàn)場(chǎng)的硬件條件參差不齊Windows 平板、安卓 PDA、老式掃碼槍都有瀏覽器是最大公約數(shù)。移動(dòng)端頁(yè)面不能依賴(lài)復(fù)雜交互和長(zhǎng)表單每個(gè)動(dòng)作一個(gè)頁(yè)面提交后立即返回結(jié)果。管理端則可以做得信息密集因?yàn)楣芾砣藛T是在電腦前用鼠標(biāo)鍵盤(pán)操作搜索、篩選、批量修改這些能力都要齊備。3.2 核心數(shù)據(jù)模型庫(kù)存表是如何避免余額式設(shè)計(jì)的Django 的 ORM 讓建表非常直接但建模思路錯(cuò)了后面全是坑。最典型的誤區(qū)是把“庫(kù)存”建一張簡(jiǎn)單的余額表比如sku_id location_id qty每次出入庫(kù)直接qty qty n。這種模型在并發(fā)場(chǎng)景下極易出錯(cuò)且沒(méi)有任何歷史鏈路。GreaterWMS 的思路是把庫(kù)存拆成“庫(kù)存快照”和“庫(kù)存移動(dòng)記錄”兩張核心表。庫(kù)存快照表達(dá)“當(dāng)前每個(gè)庫(kù)位有什么”移動(dòng)記錄表達(dá)“每一筆變化從哪來(lái)到哪去”。我通常會(huì)在項(xiàng)目里保持下面這種核心結(jié)構(gòu)class StockBatch(models.Model): # 批次庫(kù)存SKU在一個(gè)庫(kù)位上的批次明細(xì)而不是籠統(tǒng)的庫(kù)存余額 sku models.ForeignKey(Sku, on_deletemodels.PROTECT) location models.ForeignKey(Location, on_deletemodels.PROTECT) batch_no models.CharField(max_length64, db_indexTrue) qty models.DecimalField(max_digits15, decimal_places3) locked_qty models.DecimalField(max_digits15, decimal_places3, default0) expire_date models.DateField(nullTrue, blankTrue) # 狀態(tài)待檢/合格/不合格/凍結(jié) status models.CharField(max_length20, db_indexTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: constraints [ models.UniqueConstraint( fields[sku, location, batch_no, status], nameuniq_stock_batch_sku_location_status, ) ] class StockMove(models.Model): # 移動(dòng)記錄所有庫(kù)存變化都寫(xiě)一條帶方向、單號(hào)、操作人 move_type models.CharField(max_length30) # inbound/outbound/transfer/stocktake ref_no models.CharField(max_length64) # 關(guān)聯(lián)的入庫(kù)單/出庫(kù)單號(hào) sku models.ForeignKey(Sku, on_deletemodels.PROTECT) from_location models.ForeignKey(Location, nullTrue, on_deletemodels.SET_NULL, related_name) to_location models.ForeignKey(Location, nullTrue, on_deletemodels.SET_NULL, related_name) batch_no models.CharField(max_length64) qty models.DecimalField(max_digits15, decimal_places3) operator models.CharField(max_length64) created_at models.DateTimeField(auto_now_addTrue, db_indexTrue)上面這兩張表配合起來(lái)庫(kù)存總數(shù)永遠(yuǎn)是StockBatch按 SKU 聚合出來(lái)的結(jié)果而不是某一行字段。locked_qty是關(guān)鍵參數(shù)訂單已經(jīng)分配但還沒(méi)實(shí)際揀貨的數(shù)量要記在上面可用量等于qty - locked_qty。否則銷(xiāo)售看了庫(kù)存說(shuō)有貨揀貨時(shí)才發(fā)現(xiàn)被別的訂單鎖定了這種事故在電商促銷(xiāo)期間尤其常見(jiàn)。移動(dòng)記錄表只插入不更新是財(cái)務(wù)審計(jì)和批次追溯的數(shù)據(jù)底倉(cāng)。線(xiàn)上問(wèn)題排查時(shí)只要拿著 SKU 和批次號(hào)去查StockMove從最后一次收貨到當(dāng)前庫(kù)存的全部路徑都會(huì)浮出來(lái)。這套設(shè)計(jì)不復(fù)雜但把“庫(kù)存余額思維”換成“流水思維”之后絕大多數(shù)的對(duì)賬問(wèn)題都能在幾分鐘內(nèi)定位而不是半夜去翻 Excel。3.3 API 與移動(dòng)端配合掃碼交互里的三個(gè)邊界后端 API 采用 REST 風(fēng)格接口認(rèn)證走 JWT操作員在 PDA 上登錄一次Token 到點(diǎn)失效重新登錄。接口設(shè)計(jì)上有一條鐵律前端只負(fù)責(zé)掃碼和展示結(jié)果所有業(yè)務(wù)判斷都由后端做。條碼掃進(jìn)來(lái)是一串原始字符串后端去解析它到底是 SKU 碼、庫(kù)位碼還是批次碼前端不做任何猜測(cè)。這樣換個(gè)掃碼槍型號(hào)或者條碼規(guī)則調(diào)整都只需要改后端不用升級(jí)每一臺(tái) PDA。接口需要重點(diǎn)設(shè)計(jì)冪等性。掃碼提交是最容易產(chǎn)生重復(fù)請(qǐng)求的場(chǎng)景PDA 網(wǎng)絡(luò)卡頓操作員以為自己沒(méi)點(diǎn)中又點(diǎn)了一次兩條一模一樣的收貨請(qǐng)求都到了后端。我一般會(huì)在入庫(kù)明細(xì)表上做唯一約束接收單號(hào)SKU批次號(hào)三字段聯(lián)合唯一重復(fù)提交直接返回“該明細(xì)已處理”。這個(gè)約束在數(shù)據(jù)庫(kù)層面做不依賴(lài)前端按鈕防抖。權(quán)限模型用 RBAC角色劃分為收貨員、揀貨員、上架員、倉(cāng)管員、管理員五類(lèi)。每個(gè) API 按角色做訪(fǎng)問(wèn)控制。這里有個(gè)細(xì)節(jié)不要把權(quán)限控制得太粗比如讓揀貨員也能做庫(kù)存調(diào)整。庫(kù)存調(diào)整是止血操作寧可讓倉(cāng)管員多走兩步路也不能讓一線(xiàn)操作員隨手改庫(kù)存否則月底盤(pán)點(diǎn)差異永遠(yuǎn)解釋不清。4. 本地跑通最小系統(tǒng)Docker 部署命令與關(guān)鍵參數(shù)4.1 用 Docker Compose 拉一套最小環(huán)境在本地跑通一份代碼永遠(yuǎn)比看文檔學(xué)得快。常見(jiàn)做法是把整個(gè)環(huán)境用 Docker Compose 組織成兩個(gè)核心服務(wù)web是 Django 應(yīng)用db是 PostgreSQL 數(shù)據(jù)庫(kù)。生產(chǎn)環(huán)境再外部掛一層反向代理和對(duì)象存儲(chǔ)但本地驗(yàn)證這套最小配置就足夠了。下面這個(gè) Compose 文件是多數(shù) Django WMS 項(xiàng)目都能直接對(duì)上的骨架version: 3.9 services: db: image: postgres:14-alpine environment: POSTGRES_DB: wms POSTGRES_USER: wms POSTGRES_PASSWORD: wms_pass # 參數(shù)說(shuō)明容器內(nèi)數(shù)據(jù)庫(kù)名/用戶(hù)名/密碼首次啟動(dòng)自動(dòng)建好庫(kù) volumes: - pgdata:/var/lib/postgresql/data ports: - 5432:5432 web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/app # 參數(shù)說(shuō)明掛載源碼目錄改代碼即生效不用重建鏡像 ports: - 8000:8000 environment: DJANGO_DEBUG: True DB_HOST: db DB_PORT: 5432 DB_NAME: wms DB_USER: wms DB_PASSWORD: wms_pass depends_on: - db volumes: pgdata:數(shù)據(jù)庫(kù)連接配置是本地最容易卡住的一步。DB_HOST必須寫(xiě)db不是localhost因?yàn)閼?yīng)用容器和數(shù)據(jù)庫(kù)容器是兩臺(tái)獨(dú)立的機(jī)器localhost指向的是 web 容器自己。第一次啟動(dòng)時(shí)如果 web 容器報(bào)could not connect to server通常不是密碼錯(cuò)了而是 db 容器還在初始化等十幾秒后docker compose restart web就行。我一般會(huì)在項(xiàng)目的.env文件里把數(shù)據(jù)庫(kù)配置和 Django 密鑰拆出來(lái)避免把環(huán)境配置寫(xiě)死在代碼里。調(diào)試階段DJANGO_DEBUG開(kāi) True能看到詳細(xì)的報(bào)錯(cuò)堆棧一旦要往測(cè)試環(huán)境部署第一件事就是把 Debug 關(guān)掉這也意味著要提前處理靜態(tài)文件后面第 5 章的坑就是從這里長(zhǎng)出來(lái)的。4.2 初始化數(shù)據(jù)庫(kù)與第一個(gè)管理員賬號(hào)容器起來(lái)之后進(jìn)入 web 容器執(zhí)行數(shù)據(jù)遷移和管理員創(chuàng)建這一步是通用的 Django 流程# 進(jìn)入 web 容器注意這里執(zhí)行的是容器內(nèi)的 shell docker compose exec web python manage.py migrate # 生成 Django 后臺(tái)必需指令執(zhí)行后按提示輸入用戶(hù)名、郵箱、密碼 docker compose exec web python manage.py createsuperuser # 收集靜態(tài)文件到指定目錄為后續(xù)關(guān)閉 Debug 做準(zhǔn)備 docker compose exec web python manage.py collectstatic --noinputmigrate負(fù)責(zé)把項(xiàng)目里所有的數(shù)據(jù)模型映射成數(shù)據(jù)庫(kù)表。如果migrate中間報(bào)錯(cuò)說(shuō)某張表已存在不要慌常見(jiàn)原因是之前跑過(guò)一次或者數(shù)據(jù)庫(kù)卷沒(méi)清干凈執(zhí)行docker compose down -v清掉數(shù)據(jù)卷再重新up就好。createsuperuser創(chuàng)建的是管理員賬號(hào)用于登錄 Django Admin 后臺(tái)做基礎(chǔ)配置賬號(hào)密碼只存在數(shù)據(jù)庫(kù)里重建容器也不會(huì)丟因?yàn)閿?shù)據(jù)卷是獨(dú)立的。collectstatic這一步容易被人忽略。本地開(kāi)著默認(rèn)的 App 服務(wù)器時(shí)Django 會(huì)自動(dòng)處理靜態(tài)資源你會(huì)覺(jué)得不執(zhí)行這條命令也能用但一旦切換成生產(chǎn)模式樣式和腳本會(huì)全部 404整個(gè)后臺(tái)變成裸 HTML。所以項(xiàng)目初始化階段就要把它寫(xiě)進(jìn)部署腳本別等到上線(xiàn)才想起來(lái)。4.3 首次登錄后的配置順序登錄系統(tǒng)之后配置順序是有講究的順序錯(cuò)了后面每步都別扭。正確順序是先配倉(cāng)庫(kù)和庫(kù)區(qū)再配庫(kù)位然后建貨主和 SKU 檔案最后配供應(yīng)商和客戶(hù)。步驟配置項(xiàng)注意事項(xiàng)1倉(cāng)庫(kù)檔案設(shè)置默認(rèn)時(shí)區(qū)時(shí)區(qū)錯(cuò)會(huì)導(dǎo)致批次到期日計(jì)算偏差2庫(kù)區(qū)與庫(kù)位先把編碼規(guī)則寫(xiě)進(jìn)文檔按規(guī)則批量生成庫(kù)位3貨主第三方倉(cāng)儲(chǔ)業(yè)務(wù)的貨主編碼要和客戶(hù)系統(tǒng)保持一致4SKU 檔案錄入重量和尺寸波次揀貨的路徑優(yōu)化依賴(lài)這些參數(shù)5供應(yīng)商/客戶(hù)建議提前把聯(lián)系人姓名和電話(huà)維護(hù)齊全單據(jù)上要打印有個(gè)最容易踩的細(xì)節(jié)SKU 的單位換算關(guān)系要先定死。汽車(chē)備件經(jīng)常出現(xiàn)“箱”和“個(gè)”混用的情況入庫(kù)按箱數(shù)出庫(kù)按個(gè)如果換算系數(shù)在 SKU 檔案里配錯(cuò)庫(kù)存數(shù)字會(huì)成倍失真。我在給某公司做實(shí)施時(shí)遇到過(guò)一件奇葩事一種機(jī)油濾芯的換算系數(shù)被配成了 1 箱 12 個(gè)但供應(yīng)商實(shí)際包裝是一箱 10 個(gè)結(jié)果系統(tǒng)庫(kù)存顯示 1200實(shí)物只有 1000。查到最后就是在 SKU 檔案里差了這一步。這個(gè)參數(shù)在配置時(shí)就要和倉(cāng)庫(kù)主管當(dāng)面確認(rèn)不要自己猜。5. 上線(xiàn)避坑最容易翻車(chē)的 5 個(gè)問(wèn)題5.1 批次號(hào)被 Excel 自動(dòng)截?cái)喱F(xiàn)象導(dǎo)入批次主數(shù)據(jù)后系統(tǒng)里批次號(hào)的后幾位全變成了 0比如原始批次號(hào)是BF20241118A1234導(dǎo)入后變成了BF20241118A0000。追溯查詢(xún)對(duì)不上供應(yīng)商原始記錄。原因Excel 默認(rèn)把超過(guò) 15 位的數(shù)字列當(dāng)成數(shù)值處理批次號(hào)一旦長(zhǎng)得像數(shù)字就會(huì)被轉(zhuǎn)成科學(xué)計(jì)數(shù)法再把精度丟光。這個(gè)錯(cuò)在導(dǎo)入階段根本看不出來(lái)因?yàn)榻缑骖A(yù)覽時(shí)數(shù)字差不多要到出庫(kù)追溯才對(duì)不上。解決導(dǎo)入模板里把批次號(hào)列的數(shù)據(jù)格式強(qiáng)制設(shè)為文本同時(shí)后端這塊字段必須定義為CharField而不是數(shù)字類(lèi)型。最穩(wěn)妥的做法是導(dǎo)入腳本里對(duì)批次號(hào)做一次正則校驗(yàn)只允許字母、數(shù)字和短橫線(xiàn)任何科學(xué)計(jì)數(shù)法特征直接報(bào)錯(cuò)攔截。順帶在數(shù)據(jù)庫(kù)層給batch_no字段加上唯一約束重復(fù)數(shù)據(jù)進(jìn)不來(lái)問(wèn)題就提前暴露而非事后排查。5.2 并發(fā)揀貨把庫(kù)存扣成負(fù)數(shù)現(xiàn)象兩個(gè)揀貨員在同一時(shí)間掃描同一個(gè)庫(kù)位的同一個(gè) SKU系統(tǒng)顯示揀貨成功但庫(kù)存明細(xì)變成了負(fù)值總賬對(duì)不上。原因這是典型的讀改寫(xiě)并發(fā)問(wèn)題。邏輯是“先查庫(kù)存判斷夠不夠再扣減”但兩個(gè)請(qǐng)求同時(shí)讀到同樣的庫(kù)存數(shù)字各自都以為夠扣然后分別扣減最終結(jié)果就突破了零。解決扣減庫(kù)存必須走原子操作不能用“先讀后寫(xiě)”。ORM 層用條件更新加F()表達(dá)式在數(shù)據(jù)庫(kù)層把扣減和檢查合成一條語(yǔ)句類(lèi)似UPDATE stock_batch SET qty qty - 2 WHERE sku xxx AND qty 2。這樣即使并發(fā)再高數(shù)據(jù)庫(kù)的鎖機(jī)制也會(huì)讓第二個(gè)請(qǐng)求等待等它執(zhí)行時(shí)條件已經(jīng)不滿(mǎn)足直接返回庫(kù)存不足而不是扣成負(fù)數(shù)。線(xiàn)上最好再配合流水表唯一約束做兜底一筆入庫(kù)單明細(xì)只能被出庫(kù)沖銷(xiāo)一次。5.3 批次到期日差了一天導(dǎo)致效期管理錯(cuò)亂現(xiàn)象倉(cāng)庫(kù)反饋某藥品批次的到期日比實(shí)物標(biāo)簽晚了 8 小時(shí)部分庫(kù)位顯示還有效期實(shí)物已經(jīng)過(guò)期。原因前后端時(shí)區(qū)處理不一致。前端輸入日期2024-11-30后把它當(dāng)成帶時(shí)區(qū)的完整時(shí)間2024-11-30T00:00:00傳給后端后端用 UTC 解析再存到數(shù)據(jù)庫(kù)的timestamp字段東八區(qū)環(huán)境下最后記錄就變成了實(shí)際日期的前一天或后一天。解決效期管理只關(guān)心“哪天到期”完全不關(guān)心幾點(diǎn)幾分。API 層對(duì)日期字段只接收YYYY-MM-DD純?nèi)掌谧址蠖擞肈ateField存儲(chǔ)計(jì)算效期剩余時(shí)把到期日當(dāng)天的 23:59:59 作為最終時(shí)間邊界。這個(gè)規(guī)則在接口文檔里寫(xiě)清楚前端不做任何時(shí)間轉(zhuǎn)換時(shí)區(qū)問(wèn)題就不會(huì)有機(jī)會(huì)滲透到庫(kù)存邏輯里。5.4 逆向退貨重復(fù)上架導(dǎo)致庫(kù)存虛增現(xiàn)象客戶(hù)退貨重新入庫(kù)后庫(kù)存總數(shù)翻了一倍財(cái)務(wù)對(duì)賬怎么都對(duì)不上。一查發(fā)現(xiàn)同一件貨既在“已出庫(kù)”記錄里又在“可用庫(kù)存”里。原因逆向退貨直接用正向入庫(kù)流程處理系統(tǒng)把退回來(lái)的貨當(dāng)成一筆全新入庫(kù)加進(jìn)庫(kù)存而不是沖銷(xiāo)原來(lái)那筆出庫(kù)記錄。正向和逆向沒(méi)有關(guān)聯(lián)賬面上就出現(xiàn)了同一件貨的重復(fù)累計(jì)。解決退貨必須走獨(dú)立的逆向流程而且退貨單要引用原出庫(kù)單號(hào)。正確狀態(tài)流轉(zhuǎn)是退貨收貨進(jìn)入“待檢庫(kù)位”質(zhì)檢合格后做兩步操作——先沖銷(xiāo)原出庫(kù)記錄再生成新的入庫(kù)記錄然后才上架到可用庫(kù)存。質(zhì)檢不合格的直接進(jìn)不合格品區(qū)走供應(yīng)商退貨流程。我見(jiàn)過(guò)太多項(xiàng)目為了省事把逆向流程簡(jiǎn)化掉最后全部倒在月底對(duì)賬上。5.5 關(guān)閉 Debug 后后臺(tái)全部 404現(xiàn)象把DJANGO_DEBUG從 True 改成 False 后部署到測(cè)試服務(wù)器登錄頁(yè)面能打開(kāi)但沒(méi)有任何樣式圖片全部裂開(kāi)JS 控制臺(tái)全是 404。原因Django 默認(rèn)只在 Debug 模式下自己服務(wù)靜態(tài)文件關(guān)掉 Debug 后靜態(tài)文件請(qǐng)求不再被框架處理。項(xiàng)目部署前沒(méi)有執(zhí)行collectstatic或者執(zhí)行了但沒(méi)有把靜態(tài)文件目錄暴露到反向代理的路由里。解決部署腳本里把collectstatic --noinput放在啟動(dòng)服務(wù)之前然后讓反向代理配置里加一條/static/的路徑映射直接服務(wù)靜態(tài)文件目錄。驗(yàn)證方法很簡(jiǎn)單直接訪(fǎng)問(wèn)一個(gè)靜態(tài)文件地址能返回文件內(nèi)容就說(shuō)明配通了。這個(gè)坑沒(méi)有技術(shù)難度純粹是部署流程繞不過(guò)的必選項(xiàng)。6. 進(jìn)階技巧用循環(huán)盤(pán)點(diǎn)把庫(kù)存精度拉回 99% 以上庫(kù)存系統(tǒng)跑三個(gè)月后賬實(shí)差異會(huì)像雪球一樣滾起來(lái)。差異來(lái)源不一定是系統(tǒng) bug很可能是收貨時(shí)掃碼漏掃、揀貨時(shí)串貨、或者是某個(gè)庫(kù)位的貨被挪了位置沒(méi)錄系統(tǒng)。與其等年底一次性大盤(pán)點(diǎn)不如用一個(gè)技巧按 ABC 分類(lèi)做循環(huán)盤(pán)點(diǎn)每天點(diǎn)名一小部分庫(kù)位讓庫(kù)存精度持續(xù)保持在可接受范圍內(nèi)。做法是給每個(gè) SKU 打上分類(lèi)標(biāo)簽A 類(lèi)是出貨頻率最高的頭部 SKU數(shù)量可能只占 10%但出貨行數(shù)占 70%每周盤(pán)一次B 類(lèi)每月盤(pán)一次C 類(lèi)每季度盤(pán)一次。盤(pán)點(diǎn)任務(wù)按庫(kù)位隨機(jī)抽樣生成每天盤(pán) 20 到 50 個(gè)庫(kù)位即可不會(huì)干擾正常作業(yè)。盤(pán)點(diǎn)采用盲盤(pán)模式更靠譜——盤(pán)點(diǎn)單據(jù)上不顯示系統(tǒng)賬面數(shù)量操作員只錄入實(shí)盤(pán)數(shù)量系統(tǒng)隨后對(duì)比差異這樣能避免操作員被賬面數(shù)字暗示。我把這套邏輯簡(jiǎn)化成一個(gè)獨(dú)立腳本def generate_stocktake_tasks(warehouses, today): tasks [] for wh in warehouses: for sku_class in [A, B, C]: frequency {A: 7, B: 30, C: 90}[sku_class] if today.day % frequency 0 or sku_class A: # 按庫(kù)位抽樣A類(lèi)全盤(pán)B/C類(lèi)按庫(kù)位隨機(jī)抽 locs StockBatch.objects.filter( warehousewh, sku_classsku_class ).values_list(location_id, flatTrue).distinct() sample list(locs) if sku_class A else random.sample(list(locs), kmin(len(locs), 30)) tasks.append({warehouse: wh, locations: sample}) return tasks每日盤(pán)點(diǎn)產(chǎn)生的差異不要直接改庫(kù)存。正確做法是把差異掛起讓倉(cāng)管員去查StockMove流水確認(rèn)是漏掃碼還是挪庫(kù)沒(méi)錄再?zèng)Q定是補(bǔ)單還是做調(diào)整單。這里的教訓(xùn)是盤(pán)點(diǎn)不是為了把賬面改對(duì)而是為了找到流程里那個(gè)讓庫(kù)存變錯(cuò)的環(huán)節(jié)。只有把根因修掉循環(huán)盤(pán)點(diǎn)才真正發(fā)揮作用。這套方法我實(shí)踐過(guò)多次一般兩個(gè)月內(nèi)能把庫(kù)存準(zhǔn)確率從 90% 拉到 99% 以上。希望這個(gè)思路對(duì)你手頭的倉(cāng)庫(kù)項(xiàng)目有用也能幫你少走我走過(guò)的那些彎路。本文還有配套的精品資源點(diǎn)擊獲取