From 4aae6393eb364a9c94ebf8d3fdd1df4021a0b414 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Wed, 11 Feb 2026 19:36:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E5=A6=82=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/channel/ChannelController.java | 91 +++++++++ .../admin/channel/vo/ChannelGoodsRespVO.java | 41 ++++ .../inventory/InventoryCheckController.java | 58 ++++++ .../vo/InventoryCheckCreateReqVO.java | 56 ++++++ .../vo/InventoryCheckDetailRespVO.java | 72 +++++++ .../inventory/vo/InventoryCheckPageReqVO.java | 33 ++++ .../inventory/vo/InventoryCheckRespVO.java | 36 ++++ .../inventory/InventoryCheckDO.java | 57 ++++++ .../inventory/InventoryCheckItemDO.java | 87 +++++++++ .../inventory/InventoryCheckItemMapper.java | 24 +++ .../mysql/inventory/InventoryCheckMapper.java | 35 ++++ .../dal/mysql/spu/ProductSpuMapper.java | 1 + .../inventory/InventoryCheckService.java | 34 ++++ .../inventory/InventoryCheckServiceImpl.java | 181 ++++++++++++++++++ .../service/spu/ProductSpuServiceImpl.java | 5 + .../order/bo/DispenseChannelTypeEnum.java | 35 ++++ .../service/order/bo/DispenseRequestBo.java | 32 +++- .../service/order/bo/DispenseResponseBo.java | 27 ++- .../order/bo/DispenseResultStatusEnum.java | 40 ++++ .../handler/TradeDeviceOrderHandler.java | 175 ++++++++++++----- 20 files changed, 1061 insertions(+), 59 deletions(-) create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelGoodsRespVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/InventoryCheckController.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckCreateReqVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckDetailRespVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckPageReqVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckRespVO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckDO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckItemDO.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckItemMapper.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckMapper.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckService.java create mode 100644 yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckServiceImpl.java create mode 100644 yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseChannelTypeEnum.java create mode 100644 yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResultStatusEnum.java diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java index 495e66f..102e1d7 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/ChannelController.java @@ -13,6 +13,7 @@ import javax.validation.*; import javax.servlet.http.*; import java.util.*; import java.io.IOException; +import java.util.stream.Collectors; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -28,6 +29,10 @@ import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*; import cn.iocoder.yudao.module.product.controller.admin.channel.vo.*; import cn.iocoder.yudao.module.product.dal.dataobject.channel.ChannelDO; import cn.iocoder.yudao.module.product.service.channel.ChannelService; +import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO; +import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO; +import cn.iocoder.yudao.module.product.service.sku.ProductSkuService; +import cn.iocoder.yudao.module.product.service.spu.ProductSpuService; import org.springframework.web.multipart.MultipartFile; @Tag(name = "管理后台 - 货道") @@ -39,6 +44,12 @@ public class ChannelController { @Resource private ChannelService channelService; + @Resource + private ProductSpuService productSpuService; + + @Resource + private ProductSkuService productSkuService; + @PostMapping("/create") @Operation(summary = "创建货道") @PreAuthorize("@ss.hasPermission('product:channel:create')") @@ -119,4 +130,84 @@ public class ChannelController { public CommonResult testChannel(@RequestParam("id") Long id) { return channelService.testChannel(id); } + + /** + * 获得某设备下「货道 + 商品」的综合信息列表 + * + * 前端最终使用字段: + * - cabinetChannel:货道号(显示成「货道X」) + * - spuName:商品名称 + * - spuStock:当前库存 + * - id:货道唯一 ID(用于测试接口 channel-test) + * 预留字段: + * - deviceId:设备 IMEI + * - status:货道状态(后续可用于控制按钮是否可点) + */ + @GetMapping("/device-goods") + @Operation(summary = "根据设备 IMEI 获取货道 + 商品信息列表") + @PreAuthorize("@ss.hasPermission('product:channel:query')") + public CommonResult> getDeviceChannelGoods(@RequestParam("deviceId") String deviceId) { + // 1. 查询设备下所有货道(按行号 + 货道号排序) + List channelList = channelService.getChannelListByDeviceid(deviceId); + if (channelList == null || channelList.isEmpty()) { + return success(Collections.emptyList()); + } + + // 2. 查询该设备下所有「已上架」且仍绑定到该设备的商品 SPU + List spuList = productSpuService.getSpuListByDeviceid(deviceId); + if (spuList == null || spuList.isEmpty()) { + // 当售货机所有商品都已下架(例如执行了一键清空后),不再返回任何货道 + 商品数据 + return success(Collections.emptyList()); + } + Map spuMap = spuList.stream() + .collect(Collectors.toMap(ProductSpuDO::getId, spu -> spu)); + + // 3. 根据 SPU 列表查询所有 SKU,并根据 cabinetChannel 建立映射 + Map skuByChannel = new HashMap<>(); + if (!spuMap.isEmpty()) { + List spuIds = spuMap.keySet().stream().collect(Collectors.toList()); + List skuList = productSkuService.getSkuListBySpuId(spuIds); + if (skuList != null && !skuList.isEmpty()) { + for (ProductSkuDO sku : skuList) { + if (sku.getCabinetChannel() == null || sku.getCabinetChannel().isEmpty()) { + continue; + } + // 一个货道理论上只绑定一个 SKU;如果有多个,保留第一个 + skuByChannel.putIfAbsent(sku.getCabinetChannel(), sku); + } + } + } + + // 4. 组装返回结果:以货道为主,补充对应的商品名称、容量和库存 + List result = new ArrayList<>(channelList.size()); + for (ChannelDO channel : channelList) { + ChannelGoodsRespVO vo = new ChannelGoodsRespVO(); + vo.setId(channel.getId()); + // 这里返回设备 IMEI,便于前端直接使用 + vo.setDeviceId(channel.getDeviceImei()); + vo.setCabinetChannel(channel.getCabinetChannel()); + vo.setMaxNum(channel.getMaxNum()); + + ProductSkuDO sku = skuByChannel.get(channel.getCabinetChannel()); + if (sku != null) { + vo.setSkuId(sku.getId()); + ProductSpuDO spu = spuMap.get(sku.getSpuId()); + if (spu != null) { + vo.setSpuName(spu.getName()); + } + vo.setSpuStock(sku.getStock()); + } else { + // 没有关联商品时,名称和库存留空/0 + vo.setSkuId(null); + vo.setSpuName(null); + vo.setSpuStock(0); + } + + // 预留的货道状态字段,目前默认置为 1(启用) + vo.setStatus(1); + result.add(vo); + } + + return success(result); + } } \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelGoodsRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelGoodsRespVO.java new file mode 100644 index 0000000..0819425 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/channel/vo/ChannelGoodsRespVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.product.controller.admin.channel.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 管理后台 - 设备货道 + 商品信息 Response VO + * + * 用于前端展示「货道 + 商品」的综合信息: + * - 货道:id、deviceId、cabinetChannel、maxNum、status + * - 商品:skuId、spuName、spuStock + */ +@Schema(description = "管理后台 - 设备货道 + 商品信息 Response VO") +@Data +public class ChannelGoodsRespVO { + + @Schema(description = "货道主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private Long id; + + @Schema(description = "设备 IMEI(字符串)", requiredMode = Schema.RequiredMode.REQUIRED, example = "869667070287061") + private String deviceId; + + @Schema(description = "货道号(cabinetChannel)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String cabinetChannel; + + @Schema(description = "货道容量(最大货物数)", example = "10") + private Integer maxNum; + + @Schema(description = "绑定的 SKU 主键 ID(用于补货提交)", example = "10086") + private Long skuId; + + @Schema(description = "商品名称", example = "可乐330ml") + private String spuName; + + @Schema(description = "对应 SKU 当前库存", example = "5") + private Integer spuStock; + + @Schema(description = "货道状态:预留字段(例如 1=启用,0=禁用)", example = "1") + private Integer status; +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/InventoryCheckController.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/InventoryCheckController.java new file mode 100644 index 0000000..b243be0 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/InventoryCheckController.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.product.controller.admin.inventory; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.product.controller.admin.inventory.vo.*; +import cn.iocoder.yudao.module.product.service.inventory.InventoryCheckService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 管理后台 - 设备盘点(盘点记录 + 盘点详情) + * + * 一次盘点: + * - 前端基于 /product/channel/device-goods 展示每个货道当前「系统库存」 + * - 操作员录入实盘数量 + * - 调本 Controller 的 create 接口提交,后端自动计算盘盈盘亏、更新库存并记录盘点明细 + */ +@Tag(name = "管理后台 - 设备盘点") +@RestController +@RequestMapping("/product/inventory-check") +@Validated +public class InventoryCheckController { + + @Resource + private InventoryCheckService inventoryCheckService; + + @PostMapping("/create") + @Operation(summary = "创建盘点并更新库存(含盘点详情)") + @PreAuthorize("@ss.hasPermission('product:inventory-check:create')") + public CommonResult createInventoryCheck(@Valid @RequestBody InventoryCheckCreateReqVO createReqVO) { + return success(inventoryCheckService.createInventoryCheck(createReqVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得盘点记录分页") + @PreAuthorize("@ss.hasPermission('product:inventory-check:query')") + public CommonResult> getInventoryCheckPage(@Valid InventoryCheckPageReqVO pageReqVO) { + return success(inventoryCheckService.getInventoryCheckPage(pageReqVO)); + } + + @GetMapping("/detail") + @Operation(summary = "获得某次盘点详情") + @PreAuthorize("@ss.hasPermission('product:inventory-check:query')") + public CommonResult getInventoryCheckDetail( + @RequestParam("id") @Parameter(description = "盘点记录 ID", required = true) Long id) { + return success(inventoryCheckService.getInventoryCheckDetail(id)); + } +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckCreateReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckCreateReqVO.java new file mode 100644 index 0000000..1cf0c9b --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckCreateReqVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.product.controller.admin.inventory.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 管理后台 - 设备盘点创建 Request VO + * + * 前端一般基于 /product/channel/device-goods 的数据,录入每个货道的实盘数量后一次性提交。 + */ +@Schema(description = "管理后台 - 设备盘点创建 Request VO") +@Data +public class InventoryCheckCreateReqVO { + + @Schema(description = "设备 IMEI", requiredMode = Schema.RequiredMode.REQUIRED, example = "869667070287061") + @NotEmpty(message = "设备 IMEI 不能为空") + private String deviceId; + + @Schema(description = "盘点名称", example = "今日晚班盘点") + private String name; + + @Schema(description = "备注", example = "交接班前全量盘点") + private String remark; + + @Schema(description = "盘点明细列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "盘点明细不能为空") + @Valid + private List items; + + @Data + @Schema(description = "设备盘点明细") + public static class Item { + + @Schema(description = "货道主键 ID(channel.id)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "货道 ID 不能为空") + private Long channelId; + + @Schema(description = "SKU 主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10086") + @NotNull(message = "SKU ID 不能为空") + private Long skuId; + + @Schema(description = "货道号(cabinetChannel)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "货道号不能为空") + private String cabinetChannel; + + @Schema(description = "实盘库存(该货道当前实际数量)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "实盘库存不能为空") + private Integer actualStock; + } +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckDetailRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckDetailRespVO.java new file mode 100644 index 0000000..628ac00 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckDetailRespVO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.product.controller.admin.inventory.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 管理后台 - 设备盘点详情 Response VO(含明细) + */ +@Schema(description = "管理后台 - 设备盘点详情 Response VO") +@Data +public class InventoryCheckDetailRespVO { + + @Schema(description = "盘点 ID", example = "1") + private Long id; + + @Schema(description = "设备 IMEI", example = "869667070287061") + private String deviceId; + + @Schema(description = "盘点名称", example = "晚班盘点") + private String name; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "总差异(实盘总和 - 系统总和)", example = "3") + private Integer totalDiff; + + @Schema(description = "明细条数", example = "30") + private Integer itemCount; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "盘点明细列表") + private List items; + + @Data + @Schema(description = "盘点明细") + public static class Item { + + @Schema(description = "货道主键 ID(channel.id)", example = "1") + private Long channelId; + + @Schema(description = "货道号(cabinetChannel)", example = "1") + private String cabinetChannel; + + @Schema(description = "SKU 主键 ID", example = "10086") + private Long skuId; + + @Schema(description = "SPU 主键 ID", example = "20001") + private Long spuId; + + @Schema(description = "商品名称", example = "可乐330ml") + private String spuName; + + @Schema(description = "条形码", example = "6901234567890") + private String barCode; + + @Schema(description = "系统库存(盘点前)", example = "5") + private Integer systemStock; + + @Schema(description = "实盘库存", example = "7") + private Integer actualStock; + + @Schema(description = "差异(实盘 - 系统)", example = "2") + private Integer diff; + } +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckPageReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckPageReqVO.java new file mode 100644 index 0000000..9bd5046 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckPageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.product.controller.admin.inventory.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 管理后台 - 设备盘点记录分页 Request VO + */ +@Schema(description = "管理后台 - 设备盘点记录分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class InventoryCheckPageReqVO extends PageParam { + + @Schema(description = "设备 IMEI", example = "869667070287061") + private String deviceId; + + @Schema(description = "盘点名称", example = "晚班盘点") + private String name; + + @Schema(description = "创建时间区间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckRespVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckRespVO.java new file mode 100644 index 0000000..5fc5404 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/inventory/vo/InventoryCheckRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.product.controller.admin.inventory.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 管理后台 - 设备盘点记录列表/详情(主表) Response VO + */ +@Schema(description = "管理后台 - 设备盘点记录 Response VO") +@Data +public class InventoryCheckRespVO { + + @Schema(description = "盘点 ID", example = "1") + private Long id; + + @Schema(description = "设备 IMEI", example = "869667070287061") + private String deviceId; + + @Schema(description = "盘点名称", example = "晚班盘点") + private String name; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "总差异(实盘总和 - 系统总和)", example = "3") + private Integer totalDiff; + + @Schema(description = "明细条数", example = "30") + private Integer itemCount; + + @Schema(description = "创建时间") + private LocalDateTime createTime; +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckDO.java new file mode 100644 index 0000000..e14db7d --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckDO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.product.dal.dataobject.inventory; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 设备盘点记录 DO(盘点主表) + * + * 一次盘点 = 一条记录,对应多个货道盘点明细。 + * + * @author 小如之家 + */ +@TableName("product_inventory_check") +@KeySequence("product_inventory_check_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InventoryCheckDO extends BaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + + /** + * 设备 IMEI + */ + private String deviceId; + + /** + * 盘点名称(可为空,默认“设备{deviceId}盘点”) + */ + private String name; + + /** + * 备注 + */ + private String remark; + + /** + * 盘点总差异(实际库存总和 - 系统库存总和) + */ + private Integer totalDiff; + + /** + * 盘点明细条数 + */ + private Integer itemCount; +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckItemDO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckItemDO.java new file mode 100644 index 0000000..f210635 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/inventory/InventoryCheckItemDO.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.product.dal.dataobject.inventory; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 设备盘点明细 DO(按货道 / SKU) + * + * 每一行对应一个货道(cabinetChannel)和其绑定的 SKU 的一次盘点结果。 + * + * @author 小如之家 + */ +@TableName("product_inventory_check_item") +@KeySequence("product_inventory_check_item_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InventoryCheckItemDO extends BaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + + /** + * 盘点记录主表 ID + */ + private Long checkId; + + /** + * 设备 IMEI + */ + private String deviceId; + + /** + * 货道主键 ID(channel.id) + */ + private Long channelId; + + /** + * 货道号(cabinetChannel) + */ + private String cabinetChannel; + + /** + * SKU 主键 ID + */ + private Long skuId; + + /** + * 商品 SPU 主键 ID + */ + private Long spuId; + + /** + * 商品名称 + */ + private String spuName; + + /** + * 条形码 + */ + private String barCode; + + /** + * 系统库存(盘点前) + */ + private Integer systemStock; + + /** + * 实盘库存(盘点时录入) + */ + private Integer actualStock; + + /** + * 差异 = 实盘 - 系统(>0 盘盈,<0 盘亏) + */ + private Integer diff; +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckItemMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckItemMapper.java new file mode 100644 index 0000000..03b1815 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckItemMapper.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.product.dal.mysql.inventory; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.product.dal.dataobject.inventory.InventoryCheckItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 设备盘点明细 Mapper + * + * @author 小如之家 + */ +@Mapper +public interface InventoryCheckItemMapper extends BaseMapperX { + + default List selectListByCheckId(Long checkId) { + return selectList(new LambdaQueryWrapperX() + .eq(InventoryCheckItemDO::getCheckId, checkId) + .orderByAsc(InventoryCheckItemDO::getId)); + } +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckMapper.java new file mode 100644 index 0000000..6f96fe9 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/inventory/InventoryCheckMapper.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.product.dal.mysql.inventory; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.product.controller.admin.inventory.vo.InventoryCheckPageReqVO; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.product.dal.dataobject.inventory.InventoryCheckDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 设备盘点记录 Mapper + * + * @author 小如之家 + */ +@Mapper +public interface InventoryCheckMapper extends BaseMapperX { + + default List selectListByDeviceId(String deviceId) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(InventoryCheckDO::getDeviceId, deviceId) + .orderByDesc(InventoryCheckDO::getId)); + } + + default PageResult selectPage(InventoryCheckPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(InventoryCheckDO::getDeviceId, reqVO.getDeviceId()) + .likeIfPresent(InventoryCheckDO::getName, reqVO.getName()) + .betweenIfPresent(InventoryCheckDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(InventoryCheckDO::getId)); + } +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java index 9162eff..3b655e7 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java @@ -188,6 +188,7 @@ public interface ProductSpuMapper extends BaseMapperX { .selectAll(ProductSpuDO.class) .selectAs(AdminUserDO::getUsername, ProductSpuCreatorDO::getUsername) .likeIfPresent(ProductSpuDO::getName, pageReqVO.getName()) + .eqIfPresent(ProductSpuDO::getDeviceId, pageReqVO.getDeviceId()) .eqIfPresent(ProductSpuDO::getCategoryId, pageReqVO.getCategoryId()) .eqIfPresent(ProductSpuDO::getTypes, pageReqVO.getTypes()) .eqIfPresent(ProductSpuDO::getCreator, pageReqVO.getCreator()) diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckService.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckService.java new file mode 100644 index 0000000..6c7e373 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckService.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.product.service.inventory; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.product.controller.admin.inventory.vo.InventoryCheckCreateReqVO; +import cn.iocoder.yudao.module.product.controller.admin.inventory.vo.InventoryCheckDetailRespVO; +import cn.iocoder.yudao.module.product.controller.admin.inventory.vo.InventoryCheckPageReqVO; +import cn.iocoder.yudao.module.product.controller.admin.inventory.vo.InventoryCheckRespVO; + +/** + * 设备盘点 Service + * + * @author 小如之家 + */ +public interface InventoryCheckService { + + /** + * 创建一次盘点:按前端提交的每个货道的实盘数量,计算盘盈盘亏并更新库存。 + * + * @param createReqVO 盘点请求 + * @return 盘点记录主键 + */ + Long createInventoryCheck(InventoryCheckCreateReqVO createReqVO); + + /** + * 盘点记录分页 + */ + PageResult getInventoryCheckPage(InventoryCheckPageReqVO pageReqVO); + + /** + * 查询某次盘点详情(含明细列表) + */ + InventoryCheckDetailRespVO getInventoryCheckDetail(Long id); +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckServiceImpl.java new file mode 100644 index 0000000..0bf01f6 --- /dev/null +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/inventory/InventoryCheckServiceImpl.java @@ -0,0 +1,181 @@ +package cn.iocoder.yudao.module.product.service.inventory; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.product.controller.admin.inventory.vo.*; +import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSkuUpdateStockReqVO; +import cn.iocoder.yudao.module.product.controller.admin.stocklog.vo.StockLogSaveReqVO; +import cn.iocoder.yudao.module.product.dal.dataobject.inventory.InventoryCheckDO; +import cn.iocoder.yudao.module.product.dal.dataobject.inventory.InventoryCheckItemDO; +import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO; +import cn.iocoder.yudao.module.product.dal.mysql.spu.ProductSpuMapper; +import cn.iocoder.yudao.module.product.dal.dataobject.sku.ProductSkuDO; +import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO; +import cn.iocoder.yudao.module.product.dal.mysql.inventory.InventoryCheckItemMapper; +import cn.iocoder.yudao.module.product.dal.mysql.inventory.InventoryCheckMapper; +import cn.iocoder.yudao.module.product.service.sku.ProductSkuService; +import cn.iocoder.yudao.module.product.service.stocklog.StockLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 设备盘点 Service 实现 + * + * 基本思路: + * - 前端基于 /product/channel/device-goods 获取「货道 + 商品」列表(含 skuId、channelId、系统库存) + * - 用户录入 actualStock 后调用 createInventoryCheck + * - 后端按 diff=actual-system 计算盘盈/盘亏,更新 SKU 库存 & SPU 库存,并记录盘点主表 + 明细 + 库存日志 + */ +@Service +@Slf4j +public class InventoryCheckServiceImpl implements InventoryCheckService { + + @Resource + private InventoryCheckMapper inventoryCheckMapper; + @Resource + private InventoryCheckItemMapper inventoryCheckItemMapper; + @Resource + private ProductSkuService productSkuService; + @Resource + private ProductSpuMapper productSpuMapper; + @Resource + private StockLogService stockLogService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createInventoryCheck(InventoryCheckCreateReqVO createReqVO) { + String deviceId = createReqVO.getDeviceId(); + List items = createReqVO.getItems(); + if (CollUtil.isEmpty(items)) { + return null; + } + + // 1. 预查询所需的 SKU、SPU、货道信息,减少单条循环查询 + Set skuIds = items.stream().map(InventoryCheckCreateReqVO.Item::getSkuId).collect(Collectors.toSet()); + Map skuMap = productSkuService.getSkuList(skuIds).stream() + .collect(Collectors.toMap(ProductSkuDO::getId, s -> s)); + // SPU 信息在循环中按需懒加载到缓存,避免重复查询 + Map spuCache = new HashMap<>(); + + // 2. 逐条计算差异并更新 SKU 库存,顺带为 SPU 统计新的总库存 + Map spuNewTotalStock = new HashMap<>(); + List detailList = new ArrayList<>(items.size()); + int totalDiff = 0; + + for (InventoryCheckCreateReqVO.Item item : items) { + ProductSkuDO sku = skuMap.get(item.getSkuId()); + if (sku == null) { + log.warn("[createInventoryCheck][deviceId={}, skuId={} 不存在,跳过]", deviceId, item.getSkuId()); + continue; + } + Integer systemStock = Optional.ofNullable(sku.getStock()).orElse(0); + Integer actualStock = Optional.ofNullable(item.getActualStock()).orElse(0); + int diff = actualStock - systemStock; + totalDiff += diff; + + // 更新 SKU 库存为实盘值(绝对值更新) + ProductSkuUpdateStockReqVO updateSkuReqVO = new ProductSkuUpdateStockReqVO(); + updateSkuReqVO.setId(sku.getId()); + updateSkuReqVO.setStock(actualStock); + int updated = productSkuService.updateSkuStockById(updateSkuReqVO); + if (updated > 0) { + // 写库存日志:记录本次盘点导致的库存变化 + StockLogSaveReqVO stockLog = new StockLogSaveReqVO(); + stockLog.setDeviceId(deviceId); + stockLog.setCabinetChannel(item.getCabinetChannel()); + // 需要设置商品名称,否则会触发 @NotEmpty 校验 + ProductSpuDO spuForLog = spuCache.computeIfAbsent(sku.getSpuId(), + id -> productSpuMapper.selectById(id)); + if (spuForLog != null) { + stockLog.setSpuName(spuForLog.getName()); + } + stockLog.setBarCode(sku.getBarCode()); + stockLog.setOldStock(systemStock); + stockLog.setNewStock(actualStock); + stockLog.setCount(diff); // 这次盘点造成的差异 + stockLogService.createStockLog(stockLog); + } + + // 为后续 SPU 汇总记录新库存(按照实盘值累加) + spuNewTotalStock.merge(sku.getSpuId(), actualStock, Integer::sum); + + // 构造盘点明细 DO + ProductSpuDO spu = spuCache.computeIfAbsent(sku.getSpuId(), + id -> productSpuMapper.selectById(id)); + InventoryCheckItemDO detail = new InventoryCheckItemDO(); + detail.setDeviceId(deviceId); + detail.setChannelId(item.getChannelId()); + detail.setCabinetChannel(item.getCabinetChannel()); + detail.setSkuId(sku.getId()); + detail.setSpuId(sku.getSpuId()); + detail.setSpuName(spu != null ? spu.getName() : null); + detail.setBarCode(sku.getBarCode()); + detail.setSystemStock(systemStock); + detail.setActualStock(actualStock); + detail.setDiff(diff); + detailList.add(detail); + } + + // 3. 按新的 SKU 实盘值,回写各 SPU 的总库存 + for (Map.Entry entry : spuNewTotalStock.entrySet()) { + ProductSkuUpdateStockReqVO updateSpuReqVO = new ProductSkuUpdateStockReqVO(); + updateSpuReqVO.setId(entry.getKey()); + updateSpuReqVO.setStock(entry.getValue()); + // 直接将 SPU 的总库存更新为所有 SKU 实盘库存之和 + productSpuMapper.updateSpuStockById(updateSpuReqVO); + } + + // 4. 保存盘点主表 + InventoryCheckDO check = new InventoryCheckDO(); + check.setDeviceId(deviceId); + check.setName(Optional.ofNullable(createReqVO.getName()) + .orElse("设备" + deviceId + "盘点")); + check.setRemark(createReqVO.getRemark()); + check.setTotalDiff(totalDiff); + check.setItemCount(detailList.size()); + inventoryCheckMapper.insert(check); + + Long checkId = check.getId(); + + // 5. 批量写入盘点明细 + for (InventoryCheckItemDO detail : detailList) { + detail.setCheckId(checkId); + } + if (!detailList.isEmpty()) { + inventoryCheckItemMapper.insertBatch(detailList); + } + + return checkId; + } + + @Override + public PageResult getInventoryCheckPage(InventoryCheckPageReqVO pageReqVO) { + // BaseMapperX 已支持 selectPage,这里直接手动拼 Wrapper + PageResult page = inventoryCheckMapper.selectPage(pageReqVO); + return BeanUtils.toBean(page, InventoryCheckRespVO.class); + } + + @Override + public InventoryCheckDetailRespVO getInventoryCheckDetail(Long id) { + InventoryCheckDO check = inventoryCheckMapper.selectById(id); + if (check == null) { + return null; + } + InventoryCheckDetailRespVO respVO = BeanUtils.toBean(check, InventoryCheckDetailRespVO.class); + List items = inventoryCheckItemMapper.selectListByCheckId(id); + if (CollUtil.isEmpty(items)) { + respVO.setItems(Collections.emptyList()); + return respVO; + } + List itemVos = BeanUtils.toBean(items, InventoryCheckDetailRespVO.Item.class); + respVO.setItems(itemVos); + return respVO; + } +} + diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java index e22d14b..48b7ef1 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java @@ -28,6 +28,7 @@ import cn.iocoder.yudao.module.product.service.sku.ProductSkuService; import cn.iocoder.yudao.module.product.service.stocklog.StockLogService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.google.common.collect.Maps; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -465,6 +466,10 @@ public class ProductSpuServiceImpl implements ProductSpuService { // 更新 SPU 库存 int ispu = productSpuMapper.updateSpuStockById(updateSpuReqVO); if (ispu>0) { + // 同时将商品状态改为「仓库中」(下架),避免后续接口继续将其视为已上架售货机商品 + productSpuMapper.update(null, new LambdaUpdateWrapper() + .eq(ProductSpuDO::getId, spu.getId()) + .set(ProductSpuDO::getStatus, ProductSpuStatusEnum.DISABLE.getStatus())); for (ProductSkuDO sku : skuListBySpuId) { // 更新 SKU 库存 ProductSkuUpdateStockReqVO updateSkuReqVO = new ProductSkuUpdateStockReqVO(); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseChannelTypeEnum.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseChannelTypeEnum.java new file mode 100644 index 0000000..5b28fbb --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseChannelTypeEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.trade.service.order.bo; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 设备出货接口 - 通道类型 T(message 首段) + * + * 对应接口文档:message 格式 T,C&N,C&N 中的 T + */ +@RequiredArgsConstructor +@Getter +public enum DispenseChannelTypeEnum { + + CHANNEL(1, "货道"), + CHANNEL_WITH_DETECT(2, "货道带调货检测"), + DOOR(0x30, "柜门"), + DOOR_WITH_FEEDBACK(0x31, "柜门带反馈"), + ; + + private final int value; + private final String desc; + + public static String getDescByValue(Integer value) { + if (value == null) { + return String.valueOf(value); + } + for (DispenseChannelTypeEnum e : values()) { + if (e.getValue() == value) { + return e.getDesc(); + } + } + return "未知(" + value + ")"; + } +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseRequestBo.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseRequestBo.java index 7bd7f5b..fcd4d37 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseRequestBo.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseRequestBo.java @@ -3,17 +3,37 @@ package cn.iocoder.yudao.module.trade.service.order.bo; import lombok.Data; /** - * 货道机发货请求 + * 设备出货/查询状态 - 请求体(入参) + * + * 接口:POST /ots/multiDispense + * Content-Type: application/json + * Authorization: Bearer {token} + * + * 入参说明: + *
    + *
  • deviceId - 设备 IMEI
  • + *
  • message - 发货时格式:T,C&N,C&N,C&N; + * 查询状态时固定为 "1"
  • + *
+ * + * message 发货格式说明: + *
    + *
  • T - 通道类型:1=货道,2=货道带调货检测,0x30=柜门,0x31=柜门带反馈
  • + *
  • C - 货道或柜门编号(1-80),多个货道同时出货逗号分隔
  • + *
  • N - 出货数量
  • + *
+ * 示例:1,1&1,2&1,3&1 表示通道类型 1(货道),货道 1 出 1 个、货道 2 出 1 个、货道 3 出 1 个 */ @Data public class DispenseRequestBo { - //设备IMEI + + /** 设备 IMEI */ private String deviceId; - //参数格式:T,C&N,C&N,C&N - // T:通道类型(1:货道,2:货道带调货检测:0x30:柜门:0x31:柜门带反馈) - // C:货道或柜门编号,多个货道同时出货逗号分隔 =(1--80)指定货道出货 - // N:出货数量 + /** + * 发货:T,C&N,C&N(T=通道类型,C=货道/柜门编号,N=数量) + * 查询状态:固定 "1" + */ private String message; public DispenseRequestBo(String deviceId, String message) { diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResponseBo.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResponseBo.java index 4ab9f14..98e5f9a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResponseBo.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResponseBo.java @@ -3,11 +3,34 @@ package cn.iocoder.yudao.module.trade.service.order.bo; import lombok.Data; /** - * 货道机发货请求 + * 设备出货/查询状态 - 响应体(出参) + * + * 出参说明: + *
    + *
  • code - 0 成功,>0 失败(接口层)
  • + *
  • msg - 提示信息
  • + *
  • data - 发货时格式 R,C&N,C&N;查询状态时为 0 表示在线
  • + *
+ * + * data 发货格式说明(R,C&N,C&N): + *
    + *
  • R - 执行结果状态(货道:0出货成功 1缺货 2电机超时 3未检测到掉货 4电机短路 5销售订单号重复 6通道号错误 7通道类型错误; + * 柜门:0开门成功 1缺货 2开门失败 5销售订单号重复)
  • + *
  • C&N - 货道编号&出货数量,多个逗号分隔
  • + *
*/ @Data public class DispenseResponseBo { - private int code; + + /** 0 成功,>0 失败 */ + private Integer code; + + /** 提示信息 */ private String msg; + + /** + * 发货:R,C&N,C&N(R=执行结果状态,C&N=货道编号&数量) + * 查询状态:0 表示在线 + */ private String data; } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResultStatusEnum.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResultStatusEnum.java new file mode 100644 index 0000000..54afe20 --- /dev/null +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/bo/DispenseResultStatusEnum.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.trade.service.order.bo; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 设备出货接口 - 执行结果状态 R(响应 data 格式 R,C&N,C&N 中的 R) + * + * 货道:0 出货成功, 1 缺货, 2 电机超时, 3 未检测到掉货, 4 电机短路, 5 销售订单号重复, 6 通道号错误, 7 通道类型错误 + * 柜门:0 开门成功, 1 缺货, 2 开门失败, 5 销售订单号重复 + */ +@RequiredArgsConstructor +@Getter +public enum DispenseResultStatusEnum { + + SUCCESS(0, "出货成功/开门成功"), + OUT_OF_STOCK(1, "缺货"), + MOTOR_TIMEOUT(2, "电机超时/开门失败"), + NO_DROP_DETECTED(3, "未检测到掉货"), + MOTOR_SHORT(4, "电机短路"), + ORDER_NO_DUPLICATE(5, "销售订单号重复"), + CHANNEL_ERROR(6, "通道号错误"), + CHANNEL_TYPE_ERROR(7, "通道类型错误"), + ; + + private final int value; + private final String desc; + + public static String getDescByValue(Integer value) { + if (value == null) { + return "未知"; + } + for (DispenseResultStatusEnum e : values()) { + if (e.getValue() == value) { + return e.getDesc(); + } + } + return "未知(" + value + ")"; + } +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java index 156b6c9..1e8d306 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.trade.service.order.handler; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.product.api.device.DeviceApi; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; @@ -13,11 +14,13 @@ import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper; import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum; +import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum; import cn.iocoder.yudao.module.trade.service.devicelog.DeviceLogService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService; import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService; import cn.iocoder.yudao.module.trade.service.order.bo.DispenseRequestBo; import cn.iocoder.yudao.module.trade.service.order.bo.DispenseResponseBo; +import cn.iocoder.yudao.module.trade.service.order.bo.DispenseResultStatusEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; @@ -27,8 +30,12 @@ import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.time.LocalDateTime; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -86,6 +93,11 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { @Override public void afterPayOrder(TradeOrderDO order, List orderItems) { + // 商城与售货机共用同一支付回调,仅售货机订单才走设备出货流程 + if (!Objects.equals(order.getType(), TradeOrderTypeEnum.MACHINE.getType())) { + return; + } + // 这里不要抛异常,否则会影响“支付成功后”的主流程 // 所有错误通过日志 + 设备出货日志记录 @@ -147,29 +159,32 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { return; } - // 3. 查询 SKU 信息,并校验是否为“单一通道类型” - List skuIds = orderItems.stream() - .map(TradeOrderItemDO::getSkuId) - .distinct() - .collect(Collectors.toList()); - if (CollUtil.isEmpty(skuIds)) { - log.warn("订单:{} 无有效 SKU 明细,跳过设备出货处理", order.getNo()); - return; - } - List skuList = productSkuApi.getSkuList(skuIds); - if (CollUtil.isEmpty(skuList)) { - log.warn("订单:{} 根据 SKU 编号未查询到有效 SKU,跳过设备出货处理", order.getNo()); + // 3. 按 SPU 查询该商品下所有货道(SKU),并校验单一通道类型 + List skuListBySpu = productSkuApi.getSkuListBySpuId(spuIds); + if (CollUtil.isEmpty(skuListBySpu)) { + log.warn("订单:{} 商品下无货道(SKU),跳过设备出货处理", order.getNo()); + deviceLogSaveReqVO.setDeviceId(deviceId); + deviceLogSaveReqVO.setDeviceStatus(500); + deviceLogSaveReqVO.setContent("[出货异常]原因:商品下无货道(SKU)"); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); return; } - String cabinetType = findUniqueCabinetType(order, skuList, deviceId, deviceLogSaveReqVO); + String cabinetType = findUniqueCabinetType(order, skuListBySpu, deviceId, deviceLogSaveReqVO); if (cabinetType == null) { // 已在方法内部记录日志 return; } - // 4. 组装出货请求 - DispenseRequestBo request = buildDispenseRequest(orderItems, skuList, deviceId, cabinetType); + // 4. 组装出货请求:每条订单行在该商品下有库存的货道中随机选一个,同货道数量合并 + DispenseRequestBo request = buildDispenseRequest(order, orderItems, deviceId, cabinetType); + if (request == null) { + deviceLogSaveReqVO.setDeviceId(deviceId); + deviceLogSaveReqVO.setDeviceStatus(500); + deviceLogSaveReqVO.setContent("[出货异常]原因:存在商品无可用货道(有库存且通道类型匹配)"); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); + return; + } // 5. 调用设备出货接口,并记录出货日志 sendDispenseRequest(order, request, deviceLogSaveReqVO); @@ -246,41 +261,50 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { /** * 构建设备出货请求体 * - * message 协议格式(示例): - * T,C1&N1,C2&N2 - * 其中: + * 逻辑:一个商品(SPU)下可能存在多个货道(SKU)。每条订单行在该商品下「有库存且通道类型匹配」的货道中随机选一个出货,同一货道多次被选中则合并数量。 + * + * message 协议格式(示例):T,C1&N1,C2&N2 * - T:通道类型(cabinetType) * - C:货道或柜门编号(cabinetChannel) - * - N:出货数量(订单行的 count) + * - N:出货数量(同货道合并后的数量) + * + * @return 组装好的请求;若某商品无可用货道则返回 null */ - private DispenseRequestBo buildDispenseRequest(List orderItems, - List skuList, + private DispenseRequestBo buildDispenseRequest(TradeOrderDO order, + List orderItems, String deviceId, String cabinetType) { + Map channelToCount = new LinkedHashMap<>(); + String lastChannel = null; + + for (TradeOrderItemDO orderItem : orderItems) { + List skus = productSkuApi.getSkuListBySpuId(Collections.singletonList(orderItem.getSpuId())); + // 有库存、货道非空、通道类型一致 + List inStock = skus.stream() + .filter(sku -> sku != null + && StrUtil.isNotBlank(sku.getCabinetChannel()) + && Objects.equals(sku.getCabinetType(), cabinetType) + && sku.getStock() != null + && sku.getStock() >= orderItem.getCount()) + .collect(Collectors.toList()); + if (inStock.isEmpty()) { + log.error("订单:{} 商品 spuId={} 无可用货道(有库存且通道类型匹配)", order.getNo(), orderItem.getSpuId()); + return null; + } + ProductSkuRespDTO chosen = inStock.get(ThreadLocalRandom.current().nextInt(inStock.size())); + channelToCount.merge(chosen.getCabinetChannel(), orderItem.getCount(), Integer::sum); + lastChannel = chosen.getCabinetChannel(); + } + + String messageStr = cabinetType + "," + channelToCount.entrySet().stream() + .map(e -> e.getKey() + "&" + e.getValue()) + .collect(Collectors.joining(",")); DispenseRequestBo request = new DispenseRequestBo(); request.setDeviceId(deviceId); - - StringBuilder message = new StringBuilder(cabinetType).append(","); - for (int i = 0; i < orderItems.size(); i++) { - TradeOrderItemDO orderItem = orderItems.get(i); - ProductSkuRespDTO sku = productSkuApi.getSku(orderItem.getSkuId()); - if (sku == null) { - // 理论上不应该为空,这里简单跳过该行,后续可以根据需要增强处理 - continue; - } - message.append(sku.getCabinetChannel()) - .append("&") - .append(orderItem.getCount()); - if (i != orderItems.size() - 1) { - message.append(","); - } - - // 记录最后一次出货的货道(用于设备记录“上一次出货货道”) - if (i == orderItems.size() - 1) { - deviceApi.updateDeviceLastChannel(deviceId, sku.getCabinetChannel()); - } + request.setMessage(messageStr); + if (lastChannel != null) { + deviceApi.updateDeviceLastChannel(deviceId, lastChannel); } - request.setMessage(message.toString()); return request; } @@ -320,18 +344,17 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { return; } + Integer respCode = body.getCode() != null ? body.getCode() : -1; + String respData = body.getData() != null ? body.getData() : ""; log.info("订单--{}:[设备出货响应]: code={}, msg={}, data={}", order.getNo(), - body.getCode(), body.getMsg(), body.getData()); + respCode, body.getMsg(), respData); deviceLogSaveReqVO.setDeviceId(request.getDeviceId()); - deviceLogSaveReqVO.setDeviceStatus(body.getCode()); - if (body.getCode() == 0) { - // 出货成功 - deviceLogSaveReqVO.setContent("出货成功!详情: " + body.getData()); - } else { - // 出货失败 - log.error("订单--{}:[出货失败]:原因: {}", order.getNo(), body.getMsg()); - deviceLogSaveReqVO.setContent("[出货失败]原因: " + body.getMsg()); + deviceLogSaveReqVO.setDeviceStatus(respCode); + String content = buildDispenseLogContent(respCode, body.getMsg(), respData); + deviceLogSaveReqVO.setContent(content); + if (respCode != 0) { + log.error("订单--{}:[出货失败]: {}", order.getNo(), content); } deviceLogService.createDeviceLog(deviceLogSaveReqVO); } catch (Exception e) { @@ -344,6 +367,56 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { } } + /** + * 根据接口出参组装设备日志内容 + * + * 出参 data 格式:R,C&N,C&N(R=执行结果状态 0-7,C&N=货道编号&数量) + * 查询状态时 data 为 "0" 表示在线 + */ + private String buildDispenseLogContent(Integer code, String msg, String data) { + StringBuilder sb = new StringBuilder(); + if (code != null && code == 0) { + sb.append("出货成功!"); + } else { + sb.append("[出货失败]"); + if (code != null) { + sb.append(" code=").append(code); + } + if (StrUtil.isNotBlank(msg)) { + sb.append(" msg=").append(msg); + } + sb.append(";"); + } + if (StrUtil.isNotBlank(data)) { + String resultDesc = parseDispenseResultStatus(data); + if (resultDesc != null) { + sb.append("执行结果: ").append(resultDesc); + } + sb.append(" 详情: ").append(data); + } + return sb.toString().trim(); + } + + /** + * 解析 data 首段 R(执行结果状态),返回可读描述 + * data 格式:R,C&N,C&N 或 仅 R + */ + private String parseDispenseResultStatus(String data) { + if (StrUtil.isBlank(data)) { + return null; + } + String first = data.split(",", 2)[0].trim(); + if (first.isEmpty()) { + return null; + } + try { + int r = Integer.parseInt(first); + return r + "(" + DispenseResultStatusEnum.getDescByValue(r) + ")"; + } catch (NumberFormatException e) { + return null; + } + } + /** * 将订单状态更新为“已完成” *