资讯详情

资讯详情

设备管理接口-从分页到心跳

06-设备管理接口-从分页到心跳系列URM Ultra 商品/设备/订单/支付/视觉 业务实战主角urm-server 后端 · device 域 · AdminDeviceController DeviceService上篇第 05 篇我们把Device实体拆透它是双门智能柜的电子档案deviceNo是唯一身份证状态机三态心跳即脉搏。结尾预告过下一篇打开AdminDeviceController和DeviceService看设备增删改查接口尤其是heartbeat和listLowStock这两个带坑的实现。这一篇就把这两个类摊开。它们既是管理端入口也是订单扣库存、补货计划生成的地基。读完你会明白三件事管理端对设备做了哪些接口每个接口背后对应DeviceService的哪个方法create / heartbeat / bindProduct / deductStock / listLowStock这五个方法各自藏着什么规则与坑为什么分页是 CRUD 的标配而心跳和低库存却是物联网特有的灵魂。一、先上接口全景AdminDeviceController 干了啥AdminDeviceController位于com.urm.ultra.device.controller是所有管理端操作设备的 HTTP 入口统一挂在/admin-api/device下把请求转交DeviceService处理结果包成统一Result返回。先放完整接口清单与《API 接口文档》一致方法路径入参对应 Service 方法作用GET/admin-api/device/pagepageNum1, pageSize10page(pageNum, pageSize)分页查询设备GET/admin-api/device/list无list()设备列表不分页GET/admin-api/device/{id}路径 idget(id)设备详情POST/admin-api/deviceDevice 对象create(device)新增设备PUT/admin-api/device/{id}id Device 对象update(id, device)编辑设备DELETE/admin-api/device/{id}路径 iddelete(id)删除设备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子路径管货道。设备本体与货道商品是两套资源用子路径区分是 RESTful 常规写法。二、分页CRUD 的开胃菜先看最基础也最易被轻视的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包成统一分页体返回。PageResult是common包里的通用分页壳结构如下字段类型含义listListT当前页数据totallong总条数用于算总页数pageNumint当前页码从 1pageSizeint每页大小list()是不分页全量列表给下拉框/批量面板用如补货选设备。get(id)按主键查详情查不到抛 404。查不到直接抛 404由全局异常处理器转成前端能懂的错误提示而不是让null悄悄漏到后面引发空指针。这是异常即业务信号的一贯风格。三、create新建设备默认离线新增设备是管理端常用动作看create【项目源码】// 项目源码com.urm.ultra.device.service.DeviceServicepublicDevicecreate(Devicedevice){if(deviceRepository.findByDeviceNo(device.getDeviceNo()).isPresent()){thrownewBusinessException(设备编号已存在);}if(device.getStatus()null){device.setStatus(DeviceStatus.OFFLINE);}returndeviceRepository.save(device);}两个守卫缺一不可编号唯一校验findByDeviceNo(...).isPresent()先查一遍存在就抛设备编号已存在——业务层兜底比等数据库抛ConstraintViolationException更友好。默认离线status null时设为OFFLINE。刚录入系统的柜子还没通电联网凭什么在线必须等真正上报一次心跳才翻ONLINE。update(id, source)是取旧设备 → 覆盖可写字段 → 保存只改name/model/doorCount/lockCount/cameraCount/network/location/address/firmwareVersion这些硬件档案不改deviceNo/status/lastHeartbeat——编号不能改、状态归心跳、时间归心跳逻辑。delete(id)直接deleteById。四、heartbeat让一台柜子活过来这是物联网设备的命门【项目源码】// 项目源码com.urm.ultra.device.service.DeviceService/** 设备心跳更新在线状态与心跳时间。 */publicDeviceheartbeat(StringdeviceNo){DevicedevicegetByNo(deviceNo);device.setStatus(DeviceStatus.ONLINE);device.setLastHeartbeat(LocalDateTime.now());returndeviceRepository.save(device);}它接收deviceNo业务编号非 idgetByNo查设备后置ONLINE、lastHeartbeat刷当前时间并保存。调用方是VendingMachineAgentController路径POST /device-api/vending/{deviceNo}/heartbeat柜子每约 30 秒发一次我还活着后端收到即翻ONLINE。getByNo和get的差别get(id)用主键、getByNo(deviceNo)用业务编号查不到都抛 404只是消息不同。心跳、识别这类设备自报场景固件只认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);}逐行看规则get(deviceId)先确认设备存在不存在直接 404。商品校验productId必须指向真实存在的商品pms_product否则 404。存在则覆盖不存在则新建findByDeviceIdAndSlotNo(...).orElseGet(DeviceProduct::new)——同一个deviceId slotNo再次绑定会覆盖旧绑定而不是插重复行。这避免了一个货道出现两条记录的脏数据。库存初始化stock capacity ! null ? capacity : 0。绑定时若给了容量capacity库存直接填满到容量没给容量就先记 0。价格回落price price ! null ? price : product.getPrice()。货道可设设备端特价覆盖标准售价没设就用商品标准价。启用默认 1enabled默认 1启用。下面第 17 篇讲 Mock 视觉时你会看到视觉识别只从enabled1的货道里挑。updateStock(deviceId, slotNo, stock)是补货上架后手动改库存的入口按deviceId slotNo找到货道设新值找不到抛货道不存在。六、deductStock订单扣库存的唯一正门这个方法不起眼却是下单链路的守门员【项目源码】// 项目源码com.urm.ultra.device.service.DeviceService/** 扣减库存识别出商品后调用。 */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。库存守卫remain 当前库存 - 要扣数量若 0抛库存不足。绝不允许扣成负数——这是库存铁律否则超卖用户付钱却没货。更重要的这才是订单真正扣库存的地方。回看第 13 篇createAndPay扣库存并非订单自己改而是调deviceService.deductStock(deviceId, slotNo, quantity)。库存这个敏感资源的改法被收口在DeviceService一处订单域只告诉它扣多少。为什么要收口库存是下单扣减、补货回补、人工改库存共享的状态。把怎么扣、扣之前校验什么集中在一处才能避免并发下对不上账。七、listLowStock低库存的报警器最后一个方法也是上篇预告过的带坑实现。【项目源码】// 项目源码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的货道转成DeviceProductView返回。但坑就在这全表拉出来没走 SQLWHERE stock ?而是findAll()全表加载内存过滤Demo 才 10 条货道无所谓上万货道就难看。它被admin-api/device/low-stock直接调用也是ReplenishService.generate的地基那篇用Integer.MAX_VALUE当阈值取全部货道再筛。一处实现两处复用。这个纯内存过滤是 Demo 的有意简化《二次开发文档》点明大数据量应下推为findByStockLessThanEqual(threshold)之类——能跑和能扛是两码事。toView(...)把DeviceProduct拼上设备编号、商品名、条码组装成DeviceProductView含slotNo/productName/barcode/capacity/stock/price/enabled商品名条码按productId从pms_product取——又一次货道→标准库回落第 18 篇详讲。八、十个接口一张调用地图把本篇方法收成一张调用地图page/list/get→ 查create编号唯一默认 OFFLINE→ 增update→ 改delete→ 删listDeviceProducts→ 货道视图bindProduct覆盖/容量/价回落/默认启用updateStocklistLowStock内存过滤设备端心跳 →heartbeatOFFLINE→ONLINE。更重要的是跨域调用OrderService.createAndPay调deductStockReplenishService.generate调listLowStock。DeviceService还被订单域、补货域反向依赖——设备域是整个系统的地基层。九、本篇小结AdminDeviceController挂在/admin-api/device提供设备 CRUD 货道绑定/改库存 低库存查询共11 个接口本质是DeviceService的 HTTP 投影。五个核心方法各有门道create编号唯一校验、默认OFFLINEheartbeat翻ONLINE并刷新lastHeartbeatbindProduct同货道存在则覆盖、库存初始化为容量、售价回落标准价、默认启用deductStock是下单扣库存唯一正门带不允许负库存守卫listLowStock内存流过滤低库存是补货地基大数据量需下推 SQL。分页是 CRUD 标配pageNum-1 倒序 PageResult心跳与低库存才是物联网/零售灵魂前者管在线、后者管补货。设备域不止服务管理端还被订单域、补货域反向依赖——它是整个系统的地基。下一篇离开设备域跳到支付域的 Mock 实现不接微信凭什么也能把扣款跑通MockWechatPayScoreService如何用ConditionalOnProperty和内存Map演示整条支付闭环第 15 篇。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →