備管理接口-從分頁到心跳)
06-設(shè)備管理接口-從分頁到心跳系列URM Ultra 商品/設(shè)備/訂單/支付/視覺 業(yè)務(wù)實戰(zhàn)主角urm-server 后端 · device 域 · AdminDeviceController DeviceService上篇第 05 篇我們把Device實體拆透它是雙門智能柜的電子檔案deviceNo是唯一身份證狀態(tài)機三態(tài)心跳即脈搏。結(jié)尾預告過下一篇打開AdminDeviceController和DeviceService看設(shè)備增刪改查接口尤其是heartbeat和listLowStock這兩個帶坑的實現(xiàn)。這一篇就把這兩個類攤開。它們既是管理端入口也是訂單扣庫存、補貨計劃生成的地基。讀完你會明白三件事管理端對設(shè)備做了哪些接口每個接口背后對應(yīng)DeviceService的哪個方法create / heartbeat / bindProduct / deductStock / listLowStock這五個方法各自藏著什么規(guī)則與坑為什么分頁是 CRUD 的標配而心跳和低庫存卻是物聯(lián)網(wǎng)特有的靈魂。一、先上接口全景AdminDeviceController 干了啥AdminDeviceController位于com.urm.ultra.device.controller是所有管理端操作設(shè)備的 HTTP 入口統(tǒng)一掛在/admin-api/device下把請求轉(zhuǎn)交DeviceService處理結(jié)果包成統(tǒng)一Result返回。先放完整接口清單與《API 接口文檔》一致方法路徑入?yún)?yīng) Service 方法作用GET/admin-api/device/pagepageNum1, pageSize10page(pageNum, pageSize)分頁查詢設(shè)備GET/admin-api/device/list無list()設(shè)備列表不分頁GET/admin-api/device/{id}路徑 idget(id)設(shè)備詳情POST/admin-api/deviceDevice 對象create(device)新增設(shè)備PUT/admin-api/device/{id}id Device 對象update(id, device)編輯設(shè)備DELETE/admin-api/device/{id}路徑 iddelete(id)刪除設(shè)備GET/admin-api/device/{deviceId}/products路徑 deviceIdlistDeviceProducts(deviceId)貨道/商品列表POST/admin-api/device/{deviceId}/productsslotNo, productId, capacity?, price?, enabled1bindProduct(...)綁定貨道商品PUT/admin-api/device/{deviceId}/products/{slotNo}/stockstockupdateStock(...)更新貨道庫存GET/admin-api/device/low-stockthreshold5listLowStock(threshold)低庫存貨道注意路徑里{id}是自增主鍵、{deviceId}同為 id另有products子路徑管貨道。設(shè)備本體與貨道商品是兩套資源用子路徑區(qū)分是 RESTful 常規(guī)寫法。二、分頁CRUD 的開胃菜先看最基礎(chǔ)也最易被輕視的page方法【項目源碼】// 項目源碼com.urm.ultra.device.service.DeviceServicepublicPageResultDevicepage(intpageNum,intpageSize){PageRequestpageablePageRequest.of(pageNum-1,pageSize,Sort.by(Sort.Direction.DESC,id));PageDevicepagedeviceRepository.findAll(pageable);returnPageResult.of(page,d-d);}三行邏輯藏三個點pageNum - 1前端頁碼從 1 開始人類習慣第 1 頁Spring Data 分頁從 0 開始“第 0 頁”。所以這里減一換算PageResult里pageNum又加回保證前后端對齊。Sort.by(DESC, id)默認按 id 倒序最新的排最前。PageResult.of(page, d - d)把Page連同total/list/pageNum/pageSize包成統(tǒng)一分頁體返回。PageResult是common包里的通用分頁殼結(jié)構(gòu)如下字段類型含義listListT當前頁數(shù)據(jù)totallong總條數(shù)用于算總頁數(shù)pageNumint當前頁碼從 1pageSizeint每頁大小list()是不分頁全量列表給下拉框/批量面板用如補貨選設(shè)備。get(id)按主鍵查詳情查不到拋 404。查不到直接拋 404由全局異常處理器轉(zhuǎn)成前端能懂的錯誤提示而不是讓null悄悄漏到后面引發(fā)空指針。這是異常即業(yè)務(wù)信號的一貫風格。三、create新建設(shè)備默認離線新增設(shè)備是管理端常用動作看create【項目源碼】// 項目源碼com.urm.ultra.device.service.DeviceServicepublicDevicecreate(Devicedevice){if(deviceRepository.findByDeviceNo(device.getDeviceNo()).isPresent()){thrownewBusinessException(設(shè)備編號已存在);}if(device.getStatus()null){device.setStatus(DeviceStatus.OFFLINE);}returndeviceRepository.save(device);}兩個守衛(wèi)缺一不可編號唯一校驗findByDeviceNo(...).isPresent()先查一遍存在就拋設(shè)備編號已存在——業(yè)務(wù)層兜底比等數(shù)據(jù)庫拋ConstraintViolationException更友好。默認離線status null時設(shè)為OFFLINE。剛錄入系統(tǒng)的柜子還沒通電聯(lián)網(wǎng)憑什么在線必須等真正上報一次心跳才翻ONLINE。update(id, source)是取舊設(shè)備 → 覆蓋可寫字段 → 保存只改name/model/doorCount/lockCount/cameraCount/network/location/address/firmwareVersion這些硬件檔案不改deviceNo/status/lastHeartbeat——編號不能改、狀態(tài)歸心跳、時間歸心跳邏輯。delete(id)直接deleteById。四、heartbeat讓一臺柜子活過來這是物聯(lián)網(wǎng)設(shè)備的命門【項目源碼】// 項目源碼com.urm.ultra.device.service.DeviceService/** 設(shè)備心跳更新在線狀態(tài)與心跳時間。 */publicDeviceheartbeat(StringdeviceNo){DevicedevicegetByNo(deviceNo);device.setStatus(DeviceStatus.ONLINE);device.setLastHeartbeat(LocalDateTime.now());returndeviceRepository.save(device);}它接收deviceNo業(yè)務(wù)編號非 idgetByNo查設(shè)備后置ONLINE、lastHeartbeat刷當前時間并保存。調(diào)用方是VendingMachineAgentController路徑POST /device-api/vending/{deviceNo}/heartbeat柜子每約 30 秒發(fā)一次我還活著后端收到即翻ONLINE。getByNo和get的差別get(id)用主鍵、getByNo(deviceNo)用業(yè)務(wù)編號查不到都拋 404只是消息不同。心跳、識別這類設(shè)備自報場景固件只認deviceNo故用getByNo。五、bindProduct把商品塞進貨道柜子空殼沒用得往貨道放商品——bindProduct即干此活【項目源碼】// 項目源碼com.urm.ultra.device.service.DeviceServicepublicDeviceProductbindProduct(LongdeviceId,StringslotNo,LongproductId,Integercapacity,BigDecimalprice,Integerenabled){get(deviceId);ProductproductproductRepository.findById(productId).orElseThrow(()-newBusinessException(404,商品不存在));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);returndeviceProductRepository.save(dp);}逐行看規(guī)則get(deviceId)先確認設(shè)備存在不存在直接 404。商品校驗productId必須指向真實存在的商品pms_product否則 404。存在則覆蓋不存在則新建findByDeviceIdAndSlotNo(...).orElseGet(DeviceProduct::new)——同一個deviceId slotNo再次綁定會覆蓋舊綁定而不是插重復行。這避免了一個貨道出現(xiàn)兩條記錄的臟數(shù)據(jù)。庫存初始化stock capacity ! null ? capacity : 0。綁定時若給了容量capacity庫存直接填滿到容量沒給容量就先記 0。價格回落price price ! null ? price : product.getPrice()。貨道可設(shè)設(shè)備端特價覆蓋標準售價沒設(shè)就用商品標準價。啟用默認 1enabled默認 1啟用。下面第 17 篇講 Mock 視覺時你會看到視覺識別只從enabled1的貨道里挑。updateStock(deviceId, slotNo, stock)是補貨上架后手動改庫存的入口按deviceId slotNo找到貨道設(shè)新值找不到拋貨道不存在。六、deductStock訂單扣庫存的唯一正門這個方法不起眼卻是下單鏈路的守門員【項目源碼】// 項目源碼com.urm.ultra.device.service.DeviceService/** 扣減庫存識別出商品后調(diào)用。 */publicvoiddeductStock(LongdeviceId,StringslotNo,intquantity){DeviceProductdpdeviceProductRepository.findByDeviceIdAndSlotNo(deviceId,slotNo).orElseThrow(()-newBusinessException(404,貨道不存在));intremain(dp.getStock()null?0:dp.getStock())-quantity;if(remain0){thrownewBusinessException(庫存不足);}dp.setStock(remain);deviceProductRepository.save(dp);}它做了兩層保護貨道必須存在找不到deviceId slotNo組合直接 404。庫存守衛(wèi)remain 當前庫存 - 要扣數(shù)量若 0拋庫存不足。絕不允許扣成負數(shù)——這是庫存鐵律否則超賣用戶付錢卻沒貨。更重要的這才是訂單真正扣庫存的地方?;乜吹?13 篇createAndPay扣庫存并非訂單自己改而是調(diào)deviceService.deductStock(deviceId, slotNo, quantity)。庫存這個敏感資源的改法被收口在DeviceService一處訂單域只告訴它扣多少。為什么要收口庫存是下單扣減、補貨回補、人工改庫存共享的狀態(tài)。把怎么扣、扣之前校驗什么集中在一處才能避免并發(fā)下對不上賬。七、listLowStock低庫存的報警器最后一個方法也是上篇預告過的帶坑實現(xiàn)?!卷椖吭创a】// 項目源碼com.urm.ultra.device.service.DeviceService/** 低庫存貨道供補貨計劃使用。 */publicListDeviceProductViewlistLowStock(intthreshold){returndeviceProductRepository.findAll().stream().filter(dp-dp.getStock()!nulldp.getStock()threshold).map(dp-toView(deviceRepository.findById(dp.getDeviceId()).orElse(null),dp)).toList();}邏輯簡單把dms_device_product全表拉出用流過濾stock ! null 且 threshold默認 5的貨道轉(zhuǎn)成DeviceProductView返回。但坑就在這全表拉出來沒走 SQLWHERE stock ?而是findAll()全表加載內(nèi)存過濾Demo 才 10 條貨道無所謂上萬貨道就難看。它被admin-api/device/low-stock直接調(diào)用也是ReplenishService.generate的地基那篇用Integer.MAX_VALUE當閾值取全部貨道再篩。一處實現(xiàn)兩處復用。這個純內(nèi)存過濾是 Demo 的有意簡化《二次開發(fā)文檔》點明大數(shù)據(jù)量應(yīng)下推為findByStockLessThanEqual(threshold)之類——能跑和能扛是兩碼事。toView(...)把DeviceProduct拼上設(shè)備編號、商品名、條碼組裝成DeviceProductView含slotNo/productName/barcode/capacity/stock/price/enabled商品名條碼按productId從pms_product取——又一次貨道→標準庫回落第 18 篇詳講。八、十個接口一張調(diào)用地圖把本篇方法收成一張調(diào)用地圖page/list/get→ 查create編號唯一默認 OFFLINE→ 增update→ 改delete→ 刪listDeviceProducts→ 貨道視圖bindProduct覆蓋/容量/價回落/默認啟用updateStocklistLowStock內(nèi)存過濾設(shè)備端心跳 →heartbeatOFFLINE→ONLINE。更重要的是跨域調(diào)用OrderService.createAndPay調(diào)deductStockReplenishService.generate調(diào)listLowStock。DeviceService還被訂單域、補貨域反向依賴——設(shè)備域是整個系統(tǒng)的地基層。九、本篇小結(jié)AdminDeviceController掛在/admin-api/device提供設(shè)備 CRUD 貨道綁定/改庫存 低庫存查詢共11 個接口本質(zhì)是DeviceService的 HTTP 投影。五個核心方法各有門道create編號唯一校驗、默認OFFLINEheartbeat翻ONLINE并刷新lastHeartbeatbindProduct同貨道存在則覆蓋、庫存初始化為容量、售價回落標準價、默認啟用deductStock是下單扣庫存唯一正門帶不允許負庫存守衛(wèi)listLowStock內(nèi)存流過濾低庫存是補貨地基大數(shù)據(jù)量需下推 SQL。分頁是 CRUD 標配pageNum-1 倒序 PageResult心跳與低庫存才是物聯(lián)網(wǎng)/零售靈魂前者管在線、后者管補貨。設(shè)備域不止服務(wù)管理端還被訂單域、補貨域反向依賴——它是整個系統(tǒng)的地基。下一篇離開設(shè)備域跳到支付域的 Mock 實現(xiàn)不接微信憑什么也能把扣款跑通MockWechatPayScoreService如何用ConditionalOnProperty和內(nèi)存Map演示整條支付閉環(huán)第 15 篇。