小如重构
This commit is contained in:
1 parent
f91b8587da
commit
4aae6393eb
20 files changed
+1061
-59
No files matched your search
+91
@@ -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<String> 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<List<ChannelGoodsRespVO>> getDeviceChannelGoods(@RequestParam("deviceId") String deviceId) {
|
||||
// 1. 查询设备下所有货道(按行号 + 货道号排序)
|
||||
List<ChannelDO> channelList = channelService.getChannelListByDeviceid(deviceId);
|
||||
if (channelList == null || channelList.isEmpty()) {
|
||||
return success(Collections.emptyList());
|
||||
}
|
||||
|
||||
// 2. 查询该设备下所有「已上架」且仍绑定到该设备的商品 SPU
|
||||
List<ProductSpuDO> spuList = productSpuService.getSpuListByDeviceid(deviceId);
|
||||
if (spuList == null || spuList.isEmpty()) {
|
||||
// 当售货机所有商品都已下架(例如执行了一键清空后),不再返回任何货道 + 商品数据
|
||||
return success(Collections.emptyList());
|
||||
}
|
||||
Map<Long, ProductSpuDO> spuMap = spuList.stream()
|
||||
.collect(Collectors.toMap(ProductSpuDO::getId, spu -> spu));
|
||||
|
||||
// 3. 根据 SPU 列表查询所有 SKU,并根据 cabinetChannel 建立映射
|
||||
Map<String, ProductSkuDO> skuByChannel = new HashMap<>();
|
||||
if (!spuMap.isEmpty()) {
|
||||
List<Long> spuIds = spuMap.keySet().stream().collect(Collectors.toList());
|
||||
List<ProductSkuDO> 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<ChannelGoodsRespVO> 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);
|
||||
}
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
|
||||
+58
@@ -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<Long> createInventoryCheck(@Valid @RequestBody InventoryCheckCreateReqVO createReqVO) {
|
||||
return success(inventoryCheckService.createInventoryCheck(createReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得盘点记录分页")
|
||||
@PreAuthorize("@ss.hasPermission('product:inventory-check:query')")
|
||||
public CommonResult<PageResult<InventoryCheckRespVO>> getInventoryCheckPage(@Valid InventoryCheckPageReqVO pageReqVO) {
|
||||
return success(inventoryCheckService.getInventoryCheckPage(pageReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@Operation(summary = "获得某次盘点详情")
|
||||
@PreAuthorize("@ss.hasPermission('product:inventory-check:query')")
|
||||
public CommonResult<InventoryCheckDetailRespVO> getInventoryCheckDetail(
|
||||
@RequestParam("id") @Parameter(description = "盘点记录 ID", required = true) Long id) {
|
||||
return success(inventoryCheckService.getInventoryCheckDetail(id));
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -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<Item> 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;
|
||||
}
|
||||
}
|
||||
|
||||
+72
@@ -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<Item> 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;
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -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;
|
||||
}
|
||||
|
||||
+36
@@ -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;
|
||||
}
|
||||
|
||||
+57
@@ -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;
|
||||
}
|
||||
|
||||
+87
@@ -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;
|
||||
}
|
||||
|
||||
+24
@@ -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<InventoryCheckItemDO> {
|
||||
|
||||
default List<InventoryCheckItemDO> selectListByCheckId(Long checkId) {
|
||||
return selectList(new LambdaQueryWrapperX<InventoryCheckItemDO>()
|
||||
.eq(InventoryCheckItemDO::getCheckId, checkId)
|
||||
.orderByAsc(InventoryCheckItemDO::getId));
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -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<InventoryCheckDO> {
|
||||
|
||||
default List<InventoryCheckDO> selectListByDeviceId(String deviceId) {
|
||||
return selectList(new LambdaQueryWrapperX<InventoryCheckDO>()
|
||||
.eqIfPresent(InventoryCheckDO::getDeviceId, deviceId)
|
||||
.orderByDesc(InventoryCheckDO::getId));
|
||||
}
|
||||
|
||||
default PageResult<InventoryCheckDO> selectPage(InventoryCheckPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<InventoryCheckDO>()
|
||||
.eqIfPresent(InventoryCheckDO::getDeviceId, reqVO.getDeviceId())
|
||||
.likeIfPresent(InventoryCheckDO::getName, reqVO.getName())
|
||||
.betweenIfPresent(InventoryCheckDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(InventoryCheckDO::getId));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -188,6 +188,7 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
|
||||
.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())
|
||||
|
||||
+34
@@ -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<InventoryCheckRespVO> getInventoryCheckPage(InventoryCheckPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 查询某次盘点详情(含明细列表)
|
||||
*/
|
||||
InventoryCheckDetailRespVO getInventoryCheckDetail(Long id);
|
||||
}
|
||||
|
||||
+181
@@ -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<InventoryCheckCreateReqVO.Item> items = createReqVO.getItems();
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 预查询所需的 SKU、SPU、货道信息,减少单条循环查询
|
||||
Set<Long> skuIds = items.stream().map(InventoryCheckCreateReqVO.Item::getSkuId).collect(Collectors.toSet());
|
||||
Map<Long, ProductSkuDO> skuMap = productSkuService.getSkuList(skuIds).stream()
|
||||
.collect(Collectors.toMap(ProductSkuDO::getId, s -> s));
|
||||
// SPU 信息在循环中按需懒加载到缓存,避免重复查询
|
||||
Map<Long, ProductSpuDO> spuCache = new HashMap<>();
|
||||
|
||||
// 2. 逐条计算差异并更新 SKU 库存,顺带为 SPU 统计新的总库存
|
||||
Map<Long, Integer> spuNewTotalStock = new HashMap<>();
|
||||
List<InventoryCheckItemDO> 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<Long, Integer> 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<InventoryCheckRespVO> getInventoryCheckPage(InventoryCheckPageReqVO pageReqVO) {
|
||||
// BaseMapperX 已支持 selectPage,这里直接手动拼 Wrapper
|
||||
PageResult<InventoryCheckDO> 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<InventoryCheckItemDO> items = inventoryCheckItemMapper.selectListByCheckId(id);
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
respVO.setItems(Collections.emptyList());
|
||||
return respVO;
|
||||
}
|
||||
List<InventoryCheckDetailRespVO.Item> itemVos = BeanUtils.toBean(items, InventoryCheckDetailRespVO.Item.class);
|
||||
respVO.setItems(itemVos);
|
||||
return respVO;
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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<ProductSpuDO>()
|
||||
.eq(ProductSpuDO::getId, spu.getId())
|
||||
.set(ProductSpuDO::getStatus, ProductSpuStatusEnum.DISABLE.getStatus()));
|
||||
for (ProductSkuDO sku : skuListBySpuId) {
|
||||
// 更新 SKU 库存
|
||||
ProductSkuUpdateStockReqVO updateSkuReqVO = new ProductSkuUpdateStockReqVO();
|
||||
|
||||
+35
@@ -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 + ")";
|
||||
}
|
||||
}
|
||||
+26
-6
@@ -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}
|
||||
*
|
||||
* 入参说明:
|
||||
* <ul>
|
||||
* <li>deviceId - 设备 IMEI</li>
|
||||
* <li>message - 发货时格式:T,C&N,C&N,C&N;
|
||||
* 查询状态时固定为 "1"</li>
|
||||
* </ul>
|
||||
*
|
||||
* message 发货格式说明:
|
||||
* <ul>
|
||||
* <li>T - 通道类型:1=货道,2=货道带调货检测,0x30=柜门,0x31=柜门带反馈</li>
|
||||
* <li>C - 货道或柜门编号(1-80),多个货道同时出货逗号分隔</li>
|
||||
* <li>N - 出货数量</li>
|
||||
* </ul>
|
||||
* 示例: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) {
|
||||
|
||||
+25
-2
@@ -3,11 +3,34 @@ package cn.iocoder.yudao.module.trade.service.order.bo;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 货道机发货请求
|
||||
* 设备出货/查询状态 - 响应体(出参)
|
||||
*
|
||||
* 出参说明:
|
||||
* <ul>
|
||||
* <li>code - 0 成功,>0 失败(接口层)</li>
|
||||
* <li>msg - 提示信息</li>
|
||||
* <li>data - 发货时格式 R,C&N,C&N;查询状态时为 0 表示在线</li>
|
||||
* </ul>
|
||||
*
|
||||
* data 发货格式说明(R,C&N,C&N):
|
||||
* <ul>
|
||||
* <li>R - 执行结果状态(货道:0出货成功 1缺货 2电机超时 3未检测到掉货 4电机短路 5销售订单号重复 6通道号错误 7通道类型错误;
|
||||
* 柜门:0开门成功 1缺货 2开门失败 5销售订单号重复)</li>
|
||||
* <li>C&N - 货道编号&出货数量,多个逗号分隔</li>
|
||||
* </ul>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
+40
@@ -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 + ")";
|
||||
}
|
||||
}
|
||||
+124
-51
@@ -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<TradeOrderItemDO> orderItems) {
|
||||
// 商城与售货机共用同一支付回调,仅售货机订单才走设备出货流程
|
||||
if (!Objects.equals(order.getType(), TradeOrderTypeEnum.MACHINE.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里不要抛异常,否则会影响“支付成功后”的主流程
|
||||
// 所有错误通过日志 + 设备出货日志记录
|
||||
|
||||
@@ -147,29 +159,32 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 查询 SKU 信息,并校验是否为“单一通道类型”
|
||||
List<Long> skuIds = orderItems.stream()
|
||||
.map(TradeOrderItemDO::getSkuId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (CollUtil.isEmpty(skuIds)) {
|
||||
log.warn("订单:{} 无有效 SKU 明细,跳过设备出货处理", order.getNo());
|
||||
return;
|
||||
}
|
||||
List<ProductSkuRespDTO> skuList = productSkuApi.getSkuList(skuIds);
|
||||
if (CollUtil.isEmpty(skuList)) {
|
||||
log.warn("订单:{} 根据 SKU 编号未查询到有效 SKU,跳过设备出货处理", order.getNo());
|
||||
// 3. 按 SPU 查询该商品下所有货道(SKU),并校验单一通道类型
|
||||
List<ProductSkuRespDTO> 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<TradeOrderItemDO> orderItems,
|
||||
List<ProductSkuRespDTO> skuList,
|
||||
private DispenseRequestBo buildDispenseRequest(TradeOrderDO order,
|
||||
List<TradeOrderItemDO> orderItems,
|
||||
String deviceId,
|
||||
String cabinetType) {
|
||||
Map<String, Integer> channelToCount = new LinkedHashMap<>();
|
||||
String lastChannel = null;
|
||||
|
||||
for (TradeOrderItemDO orderItem : orderItems) {
|
||||
List<ProductSkuRespDTO> skus = productSkuApi.getSkuListBySpuId(Collections.singletonList(orderItem.getSpuId()));
|
||||
// 有库存、货道非空、通道类型一致
|
||||
List<ProductSkuRespDTO> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将订单状态更新为“已完成”
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user