
07-貨道綁定-DeviceProduct與slotNo庫存模型系列URM Ultra 商品/設(shè)備/訂單/支付/視覺 業(yè)務(wù)實戰(zhàn)篇主角設(shè)備域的「貨道綁定」——把一臺售貨柜的每個格子跟具體商品、庫存、售價綁在一起。適合人群沒寫過自動售貨柜后端、但會一點 Java/SpringBoot 的新手。在無人零售系統(tǒng)里商品和設(shè)備是兩件獨立的事。商品表pms_product描述「可樂是什么、賣多少錢」設(shè)備表dms_device描述「這臺柜子長什么樣、在哪」。但真正做生意時你要回答的是這臺柜子的 A01 格子里放的是可樂、還剩幾瓶、按什么價賣。把「格子」和「商品」關(guān)聯(lián)起來的那張表就是本篇的主角dms_device_product我們叫它貨道綁定表。讀懂它庫存、扣減、低庫存、補(bǔ)貨全鏈路就通了一半。一、先理清三個概念設(shè)備、貨道、商品概念對應(yīng)表一句話解釋設(shè)備 Devicedms_device一臺真實的售貨柜有編號deviceNo如DEV-0001貨道/格子 Slot無獨立表柜子里的一個格子用編號slotNo表示如A01、B02商品 Productpms_product商品主數(shù)據(jù)有productId、售價、條碼注意貨道不是一個單獨的數(shù)據(jù)庫表它只是dms_device_product里的一行數(shù)據(jù)。一行「設(shè)備 貨道編號 商品」就代表「這臺柜子的這個格子里放了這種商品」。二、核心實體 DeviceProductDeviceProduct掛在dms_device_product表上字段不多但每個都有講究// 項目源碼device/entity/DeviceProduct.javaEntityTable(namedms_device_product)publicclassDeviceProductextendsBaseEntity{privateLongdeviceId;// 設(shè)備 ID對應(yīng) dms_device.idprivateLongproductId;// 商品 ID對應(yīng) pms_product.idprivateStringslotNo;// 貨道/格子編號如 A01privateIntegercapacity;// 貨道容量最多能放幾件privateIntegerstock;// 當(dāng)前庫存還剩幾件privateBigDecimalprice;// 設(shè)備端售價可覆蓋標(biāo)準(zhǔn)售價privateIntegerenabled;// 是否啟用 1啟用 0停用}幾個新手容易踩的點繼承BaseEntity自動帶id主鍵自增、createTime、updateTime不用自己寫。price是「設(shè)備端售價」商品表里也有一個標(biāo)準(zhǔn)價pms_product.price。這里允許「同一瓶可樂A 柜賣 3 元、B 柜賣 3.5 元」——地鐵口和寫字樓定價不同很常見。capacity與stock是兩回事capacity是容量上限stock是實時庫存。補(bǔ)貨就是把stock拉回capacity。enabled是軟開關(guān)不想賣這個格子了置 0 即可不用刪數(shù)據(jù)。三、關(guān)鍵方法bindProduct 的「覆蓋語義」綁定貨道的入口是DeviceService.bindProduct。它的行為不是「新增就新增」而是存在即覆蓋——這是本篇最容易誤解的地方。// 項目源碼device/service/DeviceService.javapublicDeviceProductbindProduct(LongdeviceId,StringslotNo,LongproductId,Integercapacity,BigDecimalprice,Integerenabled){get(deviceId);// 校驗設(shè)備存在否則拋 404ProductproductproductRepository.findById(productId).orElseThrow(()-newBusinessException(404,商品不存在));// 重點先按 deviceId slotNo 查查不到才 new 一個新對象DeviceProductdpdeviceProductRepository.findByDeviceIdAndSlotNo(deviceId,slotNo).orElseGet(DeviceProduct::new);dp.setDeviceId(deviceId);dp.setSlotNo(slotNo);dp.setProductId(productId);dp.setCapacity(capacity);dp.setStock(capacity!null?capacity:0);// ① 庫存初始化為容量dp.setPrice(price!null?price:product.getPrice());// ② 價格缺省取商品價dp.setEnabled(enabled!null?enabled:1);// ③ 默認(rèn)啟用returndeviceProductRepository.save(dp);}逐條拆解它的「覆蓋語義」① 庫存初始化為容量dp.setStock(capacity!null?capacity:0);新綁定一個貨道時如果傳了capacity就把stock設(shè)成等于capacity相當(dāng)于「滿格上架」。如果沒傳capacitystock就是 0——意味著這個格子暫時是空的。為什么這樣設(shè)計因為「第一次上架」通常就是「把格子裝滿」所以默認(rèn)庫存容量省得前端再單獨調(diào)一次更新庫存接口。② 價格缺省取商品價dp.setPrice(price!null?price:product.getPrice());這是兜底策略管理員在綁定時如果懶得填售價系統(tǒng)自動用商品主數(shù)據(jù)的標(biāo)準(zhǔn)價。但如果他想做差異化定價比如景區(qū)柜子貴一點傳一個price就能覆蓋。小提示這里用的是product.getPrice()所以綁定時商品價是多少就定多少。綁完之后如果改了商品標(biāo)準(zhǔn)價已綁定的貨道價不會自動變——這是有意為之避免改主數(shù)據(jù)把歷史定價全沖掉。③ 默認(rèn)啟用enabled不傳就是 1啟用。停用某個格子時再單獨傳 0。覆蓋而不是報錯findByDeviceIdAndSlotNo(...).orElseGet(DeviceProduct::new)這行是關(guān)鍵同一個deviceId slotNo再次調(diào)用bindProduct不會插入第二行而是查出舊行重新賦值。所以「改商品」「改價」「改容量」都復(fù)用同一個綁定接口前端不用區(qū)分「新增」還是「編輯」。這是一種很省心的接口設(shè)計。四、綁定接口長什么樣管理端管理端控制器AdminDeviceController暴露的綁定接口// 項目源碼device/controller/AdminDeviceController.javaPostMapping(/{deviceId}/products)publicResultDeviceProductbindProduct(PathVariableLongdeviceId,RequestParamStringslotNo,RequestParamLongproductId,RequestParam(requiredfalse)Integercapacity,RequestParam(requiredfalse)BigDecimalprice,RequestParam(requiredfalse,defaultValue1)Integerenabled){returnResult.ok(deviceService.bindProduct(deviceId,slotNo,productId,capacity,price,enabled));}調(diào)用示例基于文檔約定端口 8080POST /admin-api/device/1/products?slotNoA01productId1capacity10含義給 1 號設(shè)備如DEV-0001的A01貨道綁定 1 號商品可樂容量 10。不傳price則售價自動取可樂標(biāo)準(zhǔn)價。五、查詢時為什么要組裝 DeviceProductView綁定表dms_device_product只存了productId不存商品名和條碼。但管理端列表、小程序商品列表都需要把「可樂」「690xxx 條碼」直接展示出來。如果每行都去JOIN商品表SQL 寫法繁瑣本項目用了一個內(nèi)存組裝的視圖對象DeviceProductView// 項目源碼device/dto/DeviceProductView.javaDatapublicclassDeviceProductView{privateLongid;privateLongdeviceId;privateStringdeviceNo;// 來自 dms_deviceprivateStringslotNo;privateLongproductId;privateStringproductName;// 來自 pms_productprivateStringbarcode;// 來自 pms_productprivateIntegercapacity;privateIntegerstock;privateBigDecimalprice;privateIntegerenabled;}組裝邏輯在toView里先拿到Device取deviceNo再拿Product取productName、barcode拼成視圖。// 項目源碼device/service/DeviceService.javaprivateDeviceProductViewtoView(Devicedevice,DeviceProductdp){ProductproductproductRepository.findById(dp.getProductId()).orElse(null);returnnewDeviceProductView(dp.getId(),dp.getDeviceId(),device!null?device.getDeviceNo():null,dp.getSlotNo(),dp.getProductId(),product!null?product.getName():null,product!null?product.getBarcode():null,dp.getCapacity(),dp.getStock(),dp.getPrice(),dp.getEnabled());}而listDeviceProducts就是把某設(shè)備的全部綁定行逐行toView// 項目源碼device/service/DeviceService.javapublicListDeviceProductViewlistDeviceProducts(LongdeviceId){Devicedeviceget(deviceId);returndeviceProductRepository.findByDeviceId(deviceId).stream().map(dp-toView(device,dp)).toList();}新手視角理解綁定表是「關(guān)系」視圖對象是「給人看的成品」。底層保持干凈的多對多關(guān)系展示層才去拼名字——這就是領(lǐng)域模型與展示模型分離。六、演示數(shù)據(jù)里長什么樣項目啟動時DataInitializer會灌一批演示貨道節(jié)選deviceIdslotNo商品capacitystock說明1A01可樂88滿格1A03橙汁33滿格1B02巧克力22低庫存樣例1B01紙巾11低庫存樣例注意B02巧克力capacity2, stock2B01紙巾capacity1, stock1——它們?nèi)萘勘旧砭托∷允翘烊坏牡蛶齑鏄永竺娴?08、09 篇會反復(fù)用到。七、本篇小結(jié)貨道不是表是dms_device_product的一行設(shè)備 貨道號 商品。bindProduct是覆蓋語義存在則更新不存在才新增stock默認(rèn)容量price缺省取商品價enabled默認(rèn) 1。capacity是上限stock是實時量補(bǔ)貨就是讓stock回到capacity。DeviceProductView是展示視圖把deviceNo、productName、barcode組裝進(jìn)來避免前端二次查表。下一篇08我們順著這條線看下單后庫存怎么扣扣的時候有哪些守衛(wèi)校驗在保護(hù)數(shù)據(jù)。系列目錄03-商品設(shè)備訂單支付視覺實戰(zhàn)篇本篇 07 貨道綁定。