From 9b0da46c0d48bbd760580fd0fab6eac887128b0e Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Tue, 4 Aug 2026 18:59:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E7=A9=BA=E8=BD=BD=E5=85=B7=E5=87=BA?= =?UTF-8?q?=E5=85=A5=E5=BA=93=E3=80=81=E5=8F=8A=E5=90=84=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=B1=BB=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execute/core/dto/TaskExecuteDTO.java | 3 + .../module/task/message/TaskEventMessage.java | 3 + .../manage/TransportTaskOperationManager.java | 19 +- .../TransportTaskCallbackServiceImpl.java | 11 +- .../module/wms/enums/ErrorCodeConstants.java | 1 + .../emptyvehicle/EmptyVehicleController.java | 25 + .../vo/EmptyVehicleTaskCreateReqVO.java | 16 + .../admin/iostorinv/IostorInvController.java | 22 +- .../admin/moveinv/vo/MoveInvPageReqVO.java | 7 + .../structAttr/EmptyVehicleRowCandidate.java | 13 + .../iostorinvdis/IostorinvDisMapper.java | 17 +- .../iostorinvdtl/IostorinvDtlMapper.java | 10 + .../wms/dal/mysql/moveinv/MoveInvMapper.java | 1 + .../mysql/moveinvdtl/MoveInvDtlMapper.java | 4 + .../dal/mysql/structAttr/StrucAttrMapper.java | 115 ++-- .../consumer/WmsTaskStatusChangeConsumer.java | 30 +- .../emptyvehicle/EmptyVehicleService.java | 9 + .../emptyvehicle/EmptyVehicleServiceImpl.java | 110 ++++ .../iostorinv/InboundIostorInvService.java | 27 + .../InboundIostorInvServiceImpl.java | 576 ++++++++++++++++++ .../service/iostorinv/IostorInvService.java | 21 +- .../iostorinv/IostorInvServiceImpl.java | 495 +++------------ .../wms/service/moveinv/MoveInvService.java | 4 + .../service/moveinv/MoveInvServiceImpl.java | 62 ++ .../nl/module/wms/task/EmptyVehicleTask.java | 22 + .../cn/code/nl/module/wms/task/InTask.java | 30 + .../cn/code/nl/module/wms/task/MoveTask.java | 30 + .../cn/code/nl/module/wms/task/OutTask.java | 32 + .../iostorinvdis/IostorinvDisMapper.xml | 45 ++ .../mapper/moveinvdtl/MoveInvDtlMapper.xml | 13 + .../mapper/structAttr/StrucAttrMapper.xml | 249 ++++++++ .../src/api/wms/iostorinv/index.ts | 4 +- .../src/views/wms/inbound/index.vue | 22 +- .../src/views/wms/inbound/modules/form.vue | 70 ++- .../src/views/wms/iostorinv/data.ts | 6 + .../src/views/wms/moveinv/data.ts | 16 + sql/mysql/wms_empty_vehicle_indexes.sql | 8 + sql/mysql/wms_empty_vehicle_lock_type.sql | 10 + sql/mysql/wms_structattr_layout_sample.sql | 74 +++ 39 files changed, 1648 insertions(+), 584 deletions(-) create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/EmptyVehicleController.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/vo/EmptyVehicleTaskCreateReqVO.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/structAttr/EmptyVehicleRowCandidate.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/emptyvehicle/EmptyVehicleService.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/emptyvehicle/EmptyVehicleServiceImpl.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvService.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvServiceImpl.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/EmptyVehicleTask.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/InTask.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/MoveTask.java create mode 100644 nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/OutTask.java create mode 100644 sql/mysql/wms_empty_vehicle_indexes.sql create mode 100644 sql/mysql/wms_empty_vehicle_lock_type.sql create mode 100644 sql/mysql/wms_structattr_layout_sample.sql diff --git a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/dto/TaskExecuteDTO.java b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/dto/TaskExecuteDTO.java index e0cff2a4..e11342d8 100644 --- a/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/dto/TaskExecuteDTO.java +++ b/nl-framework/nl-spring-boot-starter-execute/src/main/java/cn/code/nl/framework/execute/core/dto/TaskExecuteDTO.java @@ -24,6 +24,9 @@ public class TaskExecuteDTO { @NotNull(message = "任务ID不能为空") private Long taskId; + @Schema(description = "触发任务状态完成时的登录用户") + private Long operatorId; + @Schema(description = "ACS反馈扩展数据") private Map payload; } diff --git a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/message/TaskEventMessage.java b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/message/TaskEventMessage.java index 5d7752bc..ebc4d70d 100644 --- a/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/message/TaskEventMessage.java +++ b/nl-module-task/nl-module-task-api/src/main/java/cn/code/nl/module/task/message/TaskEventMessage.java @@ -42,6 +42,9 @@ public class TaskEventMessage { /** 业务回调处理器编码 */ private String handleCode; + /** 触发任务状态完成时的登录用户 */ + private Long operatorId; + /** 扩展数据 */ private Map payload; } diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/manage/TransportTaskOperationManager.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/manage/TransportTaskOperationManager.java index 5158daf8..d7cc701c 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/manage/TransportTaskOperationManager.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/manage/TransportTaskOperationManager.java @@ -1,6 +1,7 @@ package cn.code.nl.module.task.manage; import cn.code.nl.framework.common.exception.ServiceException; +import cn.code.nl.framework.security.core.util.SecurityFrameworkUtils; import cn.code.nl.module.task.dal.dataobject.transporttask.TransportTaskDO; import cn.code.nl.module.task.dal.mysql.transporttask.TransportTaskMapper; import cn.code.nl.module.task.dto.AcsFeedbackReqDTO; @@ -166,19 +167,13 @@ public class TransportTaskOperationManager { * 处理取消 */ private void handleCancelled(TransportTaskDO task, AcsFeedbackReqDTO reqDTO) { - if (!isAllowedFrom(task.getTaskStatus(), TransportTaskStatusEnum.CREATED.getCode(), - TransportTaskStatusEnum.READY.getCode(), TransportTaskStatusEnum.ISSUING.getCode(), - TransportTaskStatusEnum.ISSUED.getCode(), TransportTaskStatusEnum.EXECUTING.getCode(), - TransportTaskStatusEnum.PICKED.getCode(), - TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING.getCode(), - TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode())) { - log.warn("取消反馈状态校验不通过, taskId={}, currentStatus={}", + // 任务一旦进入待下发、已下发或执行中等后续状态,就不能再从 WMS 侧取消。 + if (!TransportTaskStatusEnum.CREATED.getCode().equals(task.getTaskStatus())) { + log.warn("仅生成状态的任务允许取消, taskId={}, currentStatus={}", task.getTaskId(), task.getTaskStatus()); - return; - } - if (!TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode().equals(task.getTaskStatus())) { - task.setTaskStatus(TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode()); + throw exception(TRANSPORT_TASK_STATUS_NOT_ALLOW); } + task.setTaskStatus(TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING.getCode()); task.setCallbackStatus(CallbackStatusEnum.PENDING.getCode()); transportTaskMapper.updateById(task); @@ -221,6 +216,8 @@ public class TransportTaskOperationManager { msg.setBizId(task.getBizId()); msg.setTaskType(task.getTaskType()); msg.setHandleCode(task.getHandleCode()); + // MQ 消费线程没有 Web 登录上下文,在生产事件时捕获最后任务完成操作人。 + msg.setOperatorId(SecurityFrameworkUtils.getLoginUserId()); msg.setPayload(payload); try { diff --git a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskCallbackServiceImpl.java b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskCallbackServiceImpl.java index b406f330..ad9bc3e9 100644 --- a/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskCallbackServiceImpl.java +++ b/nl-module-task/nl-module-task-server/src/main/java/cn/code/nl/module/task/service/transporttask/TransportTaskCallbackServiceImpl.java @@ -53,12 +53,14 @@ public class TransportTaskCallbackServiceImpl implements TransportTaskCallbackSe // 3. 根据结果处理 boolean success = "SUCCESS".equalsIgnoreCase(reqDTO.getResult()); + boolean deleteCancelledTask = false; if (success) { // 根据当前状态决定目标终态 if (statusCode(TransportTaskStatusEnum.FINISHED_CALLBACK_PENDING).equals(currentStatus)) { task.setTaskStatus(finalFinished); } else if (statusCode(TransportTaskStatusEnum.CANCEL_CALLBACK_PENDING).equals(currentStatus)) { task.setTaskStatus(finalCancelled); + deleteCancelledTask = true; } task.setCallbackStatus(CallbackStatusEnum.SUCCESS.getCode()); } else { @@ -69,7 +71,12 @@ public class TransportTaskCallbackServiceImpl implements TransportTaskCallbackSe ? task.getCallbackRetryCount() + 1 : 1); } - transportTaskMapper.updateById(task); + if (deleteCancelledTask) { + // WMS 已完成分配记录回退后再删除任务,避免提前删除导致取消回调无法消费。 + transportTaskMapper.deleteById(task.getTaskId()); + } else { + transportTaskMapper.updateById(task); + } LogRecordContext.putVariable("ownerService", task.getOwnerService()); LogRecordContext.putVariable("taskId", task.getTaskId()); @@ -79,6 +86,6 @@ public class TransportTaskCallbackServiceImpl implements TransportTaskCallbackSe } private String statusCode(TransportTaskStatusEnum statusEnum) { - return String.format("%03d", statusEnum.getCode()); + return statusEnum.getCode(); } } diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java index 13105524..5e0aae5f 100644 --- a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java @@ -26,6 +26,7 @@ public interface ErrorCodeConstants { ErrorCode INBOUND_TARGET_INVALID = new ErrorCode(6_001_3, "目标货位不可用或与所选库区、仓库不一致"); ErrorCode INBOUND_NOT_ALLOCATED = new ErrorCode(6_001_4, "入库单尚未完成货位分配"); ErrorCode INBOUND_ALLOCATION_TASK_CREATED = new ErrorCode(6_001_5, "入库任务已创建,不允许取消货位分配"); + ErrorCode INBOUND_BIZ_DATE_INVALID = new ErrorCode(6_001_6, "业务日期无效,请重新选择"); // ================ 移库单相关错误码 ================= ErrorCode MOVE_INV_NOT_EXISTS = new ErrorCode(6_002_0, "移库单不存在"); diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/EmptyVehicleController.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/EmptyVehicleController.java new file mode 100644 index 00000000..c4797450 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/EmptyVehicleController.java @@ -0,0 +1,25 @@ +package cn.code.nl.module.wms.controller.admin.emptyvehicle; + +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.module.wms.controller.admin.emptyvehicle.vo.EmptyVehicleTaskCreateReqVO; +import cn.code.nl.module.wms.service.emptyvehicle.EmptyVehicleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +import static cn.code.nl.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 空载具出入库") +@RestController +@RequestMapping("/wms/empty-vehicle") +public class EmptyVehicleController { + @Resource private EmptyVehicleService emptyVehicleService; + + @PostMapping("/create-task") + @Operation(summary = "创建空载具入库或出库任务") + public CommonResult createTask(@Valid @RequestBody EmptyVehicleTaskCreateReqVO reqVO) { + return success(emptyVehicleService.createTask(reqVO)); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/vo/EmptyVehicleTaskCreateReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/vo/EmptyVehicleTaskCreateReqVO.java new file mode 100644 index 00000000..668ec6c0 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/emptyvehicle/vo/EmptyVehicleTaskCreateReqVO.java @@ -0,0 +1,16 @@ +package cn.code.nl.module.wms.controller.admin.emptyvehicle.vo; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Data +public class EmptyVehicleTaskCreateReqVO { + @NotBlank(message = "设备编码不能为空") + private String deviceCode; + + /** 0:入库,1:出库。 */ + @NotBlank(message = "出入库类型不能为空") + @Pattern(regexp = "[01]", message = "出入库类型只能为0或1") + private String ioType; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java index 3f6def93..814579f5 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java @@ -28,6 +28,7 @@ import static cn.code.nl.framework.apilog.core.enums.OperateTypeEnum.*; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.*; import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO; import cn.code.nl.module.wms.service.iostorinv.IostorInvService; +import cn.code.nl.module.wms.service.iostorinv.InboundIostorInvService; import org.dromara.core.trans.anno.TransMethodResult; @Tag(name = "管理后台 - 出入库单主表") @@ -39,6 +40,9 @@ public class IostorInvController { @Resource private IostorInvService iostorInvService; + @Resource + private InboundIostorInvService inboundIostorInvService; + @PostMapping("/createOutbound") @Operation(summary = "创建出库单及明细") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") @@ -50,14 +54,14 @@ public class IostorInvController { @Operation(summary = "创建入库单及明细") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") public CommonResult createInbound(@Valid @RequestBody InboundSaveReqVO reqVO) { - return success(iostorInvService.createInbound(reqVO)); + return success(inboundIostorInvService.createInbound(reqVO)); } @PutMapping("/update-inbound") @Operation(summary = "修改入库单及明细") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')") public CommonResult updateInbound(@Valid @RequestBody InboundSaveReqVO reqVO) { - iostorInvService.updateInbound(reqVO); + inboundIostorInvService.updateInbound(reqVO); return success(true); } @@ -65,7 +69,7 @@ public class IostorInvController { @Operation(summary = "删除生成状态的入库单") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:delete')") public CommonResult deleteInbound(@RequestParam("iostorinvId") Long iostorinvId) { - iostorInvService.deleteInbound(iostorinvId); + inboundIostorInvService.deleteInbound(iostorinvId); return success(true); } @@ -74,14 +78,14 @@ public class IostorInvController { @PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')") public CommonResult> getInboundGroupPlates(@RequestParam("storId") String storId) { - return success(iostorInvService.getInboundGroupPlates(storId)); + return success(inboundIostorInvService.getInboundGroupPlates(storId)); } @PostMapping("/allocate-inbound") @Operation(summary = "分配入库货位并按库区类型创建任务") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')") public CommonResult allocateInbound(@Valid @RequestBody InboundAllocationReqVO reqVO) { - iostorInvService.allocateInbound(reqVO); + inboundIostorInvService.allocateInbound(reqVO); return success(true); } @@ -91,7 +95,7 @@ public class IostorInvController { public CommonResult cancelInboundAllocation( @RequestParam("iostorinvId") Long iostorinvId, @RequestParam("iostorinvdtlId") Long iostorinvdtlId) { - iostorInvService.cancelInboundAllocation(iostorinvId, iostorinvdtlId); + inboundIostorInvService.cancelInboundAllocation(iostorinvId, iostorinvdtlId); return success(true); } @@ -101,14 +105,14 @@ public class IostorInvController { public CommonResult> getInboundAvailableStructs(@RequestParam("storId") String storId, @RequestParam("sectId") String sectId) { - return success(iostorInvService.getInboundAvailableStructs(storId, sectId)); + return success(inboundIostorInvService.getInboundAvailableStructs(storId, sectId)); } @PostMapping("/set-inbound-point") @Operation(summary = "设置入库起点并创建入库任务") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')") public CommonResult setInboundPoint(@Valid @RequestBody InboundSetPointReqVO reqVO) { - iostorInvService.setInboundPoint(reqVO); + inboundIostorInvService.setInboundPoint(reqVO); return success(true); } @@ -116,7 +120,7 @@ public class IostorInvController { @Operation(summary = "完成入库并绑定木箱与仓位") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:update')") public CommonResult completeInbound(@RequestParam("iostorinvId") Long iostorinvId) { - iostorInvService.completeInbound(iostorinvId); + inboundIostorInvService.completeInbound(iostorinvId); return success(true); } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvPageReqVO.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvPageReqVO.java index 8e2a1a84..1dc59f17 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvPageReqVO.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/moveinv/vo/MoveInvPageReqVO.java @@ -2,6 +2,11 @@ package cn.code.nl.module.wms.controller.admin.moveinv.vo; import cn.code.nl.framework.common.pojo.PageParam; import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.code.nl.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; /** * 移库单分页参数。 @@ -13,4 +18,6 @@ public class MoveInvPageReqVO extends PageParam { private String billType; private String storId; private String billStatus; + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/structAttr/EmptyVehicleRowCandidate.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/structAttr/EmptyVehicleRowCandidate.java new file mode 100644 index 00000000..73328c4f --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/dataobject/structAttr/EmptyVehicleRowCandidate.java @@ -0,0 +1,13 @@ +package cn.code.nl.module.wms.dal.dataobject.structAttr; + +import lombok.Data; + +/** 空载具入库候选排,Java 按顺序逐排尝试预占空仓位。 */ +@Data +public class EmptyVehicleRowCandidate { + private String storId; + private String sectId; + private Integer blockNum; + private Integer rowNum; + private Integer layerNum; +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdis/IostorinvDisMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdis/IostorinvDisMapper.java index 19f58e21..2bc43488 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdis/IostorinvDisMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdis/IostorinvDisMapper.java @@ -7,7 +7,6 @@ import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO; import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Delete; import cn.code.nl.module.wms.controller.admin.iostorinvdis.vo.*; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.OutboundAllocationRespVO; import org.apache.ibatis.annotations.Param; @@ -25,18 +24,18 @@ public interface IostorinvDisMapper extends BaseMapperX { @Param("iostorinvdtlId") Long iostorinvdtlId, @Param("sectId") String sectId); - @Delete(""" - DELETE FROM wms_iostorinvdis - WHERE iostorinvdis_id = #{allocationId} - """) int physicallyDeleteInboundAllocation(@Param("allocationId") String allocationId); - @Delete(""" - DELETE FROM wms_iostorinvdis - WHERE iostorinv_id = #{iostorinvId} - """) int physicallyDeleteByIostorinvId(@Param("iostorinvId") Long iostorinvId); + int completeOutboundAllocations(@Param("taskId") String taskId); + + int clearGeneratedOutboundTask(@Param("taskId") String taskId); + + int clearInboundTask(@Param("taskId") String taskId); + + long countUnfinishedByDetailId(@Param("detailId") Long detailId); + default PageResult selectPage(IostorinvDisPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(IostorinvDisDO::getIostorinvId, reqVO.getIostorinvId()) diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java index 82344123..603b9a8b 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java @@ -11,6 +11,7 @@ import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Select; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryPageReqVO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvDetailRespVO; @@ -31,6 +32,15 @@ public interface IostorinvDtlMapper extends BaseMapperX { """) int physicallyDeleteByIostorinvId(@Param("iostorinvId") Long iostorinvId); + @Select(""" + SELECT COUNT(*) + FROM wms_iostorinvdtl + WHERE iostorinv_id = #{iostorinvId} + AND deleted = '0' + AND bill_status <> '99' + """) + long countUnfinishedByIostorinvId(@Param("iostorinvId") Long iostorinvId); + /** * 分页查询指定仓库的可用库存。 */ diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java index c86e05cc..017139b2 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinv/MoveInvMapper.java @@ -19,6 +19,7 @@ public interface MoveInvMapper extends BaseMapperX { .eqIfPresent(MoveInvDO::getBillType, reqVO.getBillType()) .eqIfPresent(MoveInvDO::getStorId, reqVO.getStorId()) .eqIfPresent(MoveInvDO::getBillStatus, reqVO.getBillStatus()) + .betweenIfPresent(MoveInvDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(MoveInvDO::getMoveinvId)); } } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java index 33691d73..78162165 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/moveinvdtl/MoveInvDtlMapper.java @@ -16,4 +16,8 @@ public interface MoveInvDtlMapper extends BaseMapperX { List selectByMoveinvId(@Param("moveinvId") Long moveinvId); int deleteByMoveinvId(@Param("moveinvId") Long moveinvId); + + long countUnfinishedByMoveinvId(@Param("moveinvId") Long moveinvId); + + int clearTaskId(@Param("taskId") Long taskId); } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java index c43cb081..4f926815 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/structAttr/StrucAttrMapper.java @@ -6,10 +6,10 @@ import cn.code.nl.framework.common.pojo.PageResult; import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; +import cn.code.nl.module.wms.dal.dataobject.structAttr.EmptyVehicleRowCandidate; import cn.code.nl.module.wms.controller.admin.moveinv.vo.MoveInvInventoryRespVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Update; import cn.code.nl.module.wms.controller.admin.structAttr.vo.*; /** @@ -20,6 +20,29 @@ import cn.code.nl.module.wms.controller.admin.structAttr.vo.*; @Mapper public interface StrucAttrMapper extends BaseMapperX { + List selectEmptyVehiclePreferredRows(); + + List selectCompletelyEmptyRows(); + + StrucAttrDO selectFreeStructInRow(EmptyVehicleRowCandidate row); + + List selectEmptyVehicleOutboundCandidates(); + + int lockEmptyVehicleInbound(@Param("structId") String structId, @Param("taskId") String taskId); + + int lockEmptyVehicleOutbound(@Param("structId") String structId, @Param("taskId") String taskId); + + int bindEmptyVehicleTask(@Param("structId") String structId, + @Param("reservationCode") String reservationCode, + @Param("taskId") String taskId, + @Param("lockType") String lockType); + + int completeEmptyVehicleInbound(@Param("taskId") String taskId); + + int completeEmptyVehicleOutbound(@Param("taskId") String taskId); + + int cancelEmptyVehicleTask(@Param("taskId") String taskId); + List selectMoveAvailableInventory( @Param("storId") String storId, @Param("sectId") String sectId, @@ -42,109 +65,41 @@ public interface StrucAttrMapper extends BaseMapperX { int unlockMoveInv(@Param("invId") String invId); - @Update(""" - UPDATE wms_structattr - SET lock_type = '3', - inv_type = #{invType}, - inv_id = #{invId}, - inv_code = #{invCode} - WHERE struct_id = #{structId} - AND deleted = '0' - AND (lock_type IS NULL OR lock_type IN ('0', '1') - OR (lock_type = '3' AND inv_id = #{invId})) - """) + int completeMoveOut(@Param("structCode") String structCode, + @Param("invId") String invId, + @Param("vehicleCode") String vehicleCode); + + int completeMoveIn(@Param("structCode") String structCode, + @Param("invId") String invId, + @Param("vehicleCode") String vehicleCode); + int lockForOutbound(@Param("structId") String structId, @Param("invType") String invType, @Param("invId") String invId, @Param("invCode") String invCode); - @Update(""" - UPDATE wms_structattr - SET lock_type = '1', - inv_type = NULL, - inv_id = NULL, - inv_code = NULL - WHERE storagevehicle_code = #{vehicleCode} - AND deleted = '0' - AND lock_type = '3' - AND inv_id = #{invId} - """) int unlockOutbound(@Param("vehicleCode") String vehicleCode, @Param("invId") String invId); - @Update(""" - UPDATE wms_structattr - SET lock_type = '2', - task_code = #{taskCode} - WHERE struct_id = #{structId} - AND deleted = '0' - AND (lock_type IS NULL OR lock_type IN ('0', '1', '3')) - """) int lockForTask(@Param("structId") String structId, @Param("taskCode") String taskCode); - @Update(""" - UPDATE wms_structattr - SET lock_type = '2', - inv_type = '0', - inv_id = #{invId}, - inv_code = #{invCode} - WHERE struct_id = #{structId} - AND deleted = '0' - AND is_used = 1 - AND lock_type = '1' - AND (storagevehicle_code IS NULL OR storagevehicle_code = '') - """) int lockForInbound(@Param("structId") String structId, @Param("invId") String invId, @Param("invCode") String invCode); - @Update(""" - UPDATE wms_structattr - SET storagevehicle_code = #{vehicleCode}, - storagevehicle_qty = 1, - lock_type = '1', - inv_type = NULL, - inv_id = NULL, - inv_code = NULL, - task_code = NULL - WHERE struct_id = #{structId} - AND deleted = '0' - AND lock_type = '2' - AND inv_id = #{invId} - """) int completeInbound(@Param("structId") String structId, @Param("invId") String invId, @Param("vehicleCode") String vehicleCode); - @Update(""" - UPDATE wms_structattr - SET lock_type = '1', - inv_type = NULL, - inv_id = NULL, - inv_code = NULL, - task_code = NULL - WHERE deleted = '0' - AND inv_id = #{invId} - AND lock_type = '2' - """) int unlockInbound(@Param("invId") String invId); - @Update(""" - UPDATE wms_structattr - SET lock_type = '1', - inv_type = NULL, - inv_id = NULL, - inv_code = NULL, - task_code = NULL - WHERE struct_id = #{structId} - AND deleted = '0' - AND inv_id = #{invId} - AND lock_type = '2' - """) int unlockInboundStruct(@Param("structId") String structId, @Param("invId") String invId); + int completeOutboundTask(@Param("structId") String structId, + @Param("taskId") String taskId); + default PageResult selectPage(StrucAttrPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(StrucAttrDO::getStructCode, reqVO.getStructCode()) diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/mq/consumer/WmsTaskStatusChangeConsumer.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/mq/consumer/WmsTaskStatusChangeConsumer.java index 0c41724a..1db3a013 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/mq/consumer/WmsTaskStatusChangeConsumer.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/mq/consumer/WmsTaskStatusChangeConsumer.java @@ -6,6 +6,7 @@ import cn.code.nl.framework.execute.core.TaskFactory; import cn.code.nl.framework.execute.core.dto.TaskExecuteDTO; import cn.code.nl.module.task.api.TransportTaskApi; import cn.code.nl.module.task.dto.TaskInfoDTO; +import cn.code.nl.module.task.dto.TaskCallbackResultReqDTO; import cn.code.nl.module.task.enums.TransportTaskStatusEnum; import cn.code.nl.module.task.message.TaskEventMessage; import jakarta.annotation.Resource; @@ -105,14 +106,31 @@ public class WmsTaskStatusChangeConsumer implements RocketMQListener rows, String reservationCode) { + for (EmptyVehicleRowCandidate row : rows) { + StrucAttrDO candidate = strucAttrMapper.selectFreeStructInRow(row); + if (candidate != null && strucAttrMapper.lockEmptyVehicleInbound( + candidate.getStructId(), reservationCode) == 1) { + return candidate; + } + } + return null; + } + + private StrucAttrDO reserveOutboundStruct(String reservationCode) { + for (StrucAttrDO candidate : strucAttrMapper.selectEmptyVehicleOutboundCandidates()) { + if (strucAttrMapper.lockEmptyVehicleOutbound( + candidate.getStructId(), reservationCode) == 1) { + return candidate; + } + } + return null; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeTask(Long taskId) { + String value = String.valueOf(taskId); + int updated = strucAttrMapper.completeEmptyVehicleInbound(value); + if (updated == 0) { + updated = strucAttrMapper.completeEmptyVehicleOutbound(value); + } + if (updated != 1) { + throw new IllegalStateException("未找到空载具任务锁定的仓位,taskId=" + taskId); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelTask(Long taskId) { + if (strucAttrMapper.cancelEmptyVehicleTask(String.valueOf(taskId)) != 1) { + throw new IllegalStateException("未找到空载具任务锁定的仓位,taskId=" + taskId); + } + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvService.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvService.java new file mode 100644 index 00000000..5a822168 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvService.java @@ -0,0 +1,27 @@ +package cn.code.nl.module.wms.service.iostorinv; + +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.InboundAllocationReqVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.InboundSaveReqVO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.InboundSetPointReqVO; +import cn.code.nl.module.wms.dal.dataobject.groupplate.GroupPlateDO; +import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO; +import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; +import jakarta.validation.Valid; +import java.util.List; + +/** 入库单业务接口。 */ +public interface InboundIostorInvService { + Long createInbound(@Valid InboundSaveReqVO reqVO); + void updateInbound(@Valid InboundSaveReqVO reqVO); + void deleteInbound(Long iostorinvId); + List getInboundGroupPlates(String storId); + void allocateInbound(@Valid InboundAllocationReqVO reqVO); + void cancelInboundAllocation(Long iostorinvId, Long iostorinvdtlId); + List getInboundAvailableStructs(String storId, String sectId); + void setInboundPoint(@Valid InboundSetPointReqVO reqVO); + void completeInbound(Long iostorinvId); + void completeInboundTask(Long taskId, Long confirmUserId); + void cancelInboundTask(Long taskId); + void ensureAllocationPlaceholder(IostorInvDO header, IostorinvDtlDO detail); +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvServiceImpl.java new file mode 100644 index 00000000..57214ae8 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/InboundIostorInvServiceImpl.java @@ -0,0 +1,576 @@ +package cn.code.nl.module.wms.service.iostorinv; + +import cn.code.nl.module.wms.task.InTask; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.code.nl.module.base.api.codegen.CodeGenApi; +import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; +import cn.code.nl.module.task.api.TransportTaskApi; +import cn.code.nl.module.task.dto.TransportTaskCreateReqDTO; +import cn.code.nl.module.wms.controller.admin.iostorinv.vo.*; +import cn.code.nl.module.wms.dal.dataobject.groupplate.GroupPlateDO; +import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO; +import cn.code.nl.module.wms.dal.dataobject.iostorinvdis.IostorinvDisDO; +import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; +import cn.code.nl.module.wms.dal.dataobject.sectattr.SectAttrDO; +import cn.code.nl.module.wms.dal.dataobject.structAttr.StrucAttrDO; +import cn.code.nl.module.wms.dal.mysql.groupplate.GroupPlateMapper; +import cn.code.nl.module.wms.dal.mysql.iostorinv.IostorInvMapper; +import cn.code.nl.module.wms.dal.mysql.iostorinvdis.IostorinvDisMapper; +import cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper; +import cn.code.nl.module.wms.dal.mysql.sectattr.SectAttrMapper; +import cn.code.nl.module.wms.dal.mysql.structAttr.StrucAttrMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; +import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.*; + +/** 入库单业务实现,与出库业务独立维护。 */ +@Service +@Validated +@Slf4j +public class InboundIostorInvServiceImpl implements InboundIostorInvService { + @Resource private IostorInvMapper iostorInvMapper; + @Resource private IostorinvDtlMapper iostorinvDtlMapper; + @Resource private CodeGenApi codeGenApi; + @Resource private IostorinvDisMapper iostorinvDisMapper; + @Resource private GroupPlateMapper groupPlateMapper; + @Resource private StrucAttrMapper strucAttrMapper; + @Resource private SectAttrMapper sectAttrMapper; + @Resource private TransportTaskApi transportTaskApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createInbound(InboundSaveReqVO reqVO) { + validateInboundBizDate(reqVO); + List groups = validateInboundGroups(reqVO.getGroupIds(), null); + IostorInvDO header = new IostorInvDO(); + header.setBillCode(generateIoBillCode("createInbound")); + header.setIoType("0"); + header.setBillType(reqVO.getBillType()); + header.setBizDate(reqVO.getBizDate()); + header.setStorId(reqVO.getStorId()); + header.setBillStatus("10"); + header.setCreateMode("01"); + header.setRemark(reqVO.getRemark()); + header.setDetailCount(groups.size()); + header.setTotalQty(groups.stream().map(GroupPlateDO::getQty) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + header.setTotalWeight(header.getTotalQty()); + iostorInvMapper.insert(header); + insertInboundDetails(header.getIostorinvId(), groups); + insertInboundAllocationPlaceholders(header, groups); + groups.forEach(group -> updateGroupStatus(group.getGroupId(), "01")); + return header.getIostorinvId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateInbound(InboundSaveReqVO reqVO) { + validateInboundBizDate(reqVO); + IostorInvDO header = requireInbound(reqVO.getIostorinvId(), "10"); + List oldDetails = iostorinvDtlMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId())); + Set oldGroupIds = oldDetails.stream().map(IostorinvDtlDO::getSourceBilldtlId) + .filter(StrUtil::isNotBlank).map(Long::valueOf).collect(Collectors.toSet()); + List groups = validateInboundGroups(reqVO.getGroupIds(), oldGroupIds); + oldGroupIds.forEach(id -> updateGroupStatus(id, "00")); + strucAttrMapper.unlockInbound(String.valueOf(header.getIostorinvId())); + iostorinvDisMapper.delete(new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId()))); + iostorinvDtlMapper.delete(new LambdaQueryWrapperX() + .eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId())); + IostorInvDO update = new IostorInvDO(); + update.setIostorinvId(header.getIostorinvId()); + update.setBillType(reqVO.getBillType()); + update.setBizDate(reqVO.getBizDate()); + update.setStorId(reqVO.getStorId()); + update.setRemark(reqVO.getRemark()); + update.setDetailCount(groups.size()); + update.setTotalQty(groups.stream().map(GroupPlateDO::getQty) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + update.setTotalWeight(update.getTotalQty()); + iostorInvMapper.updateById(update); + insertInboundDetails(header.getIostorinvId(), groups); + insertInboundAllocationPlaceholders(header, groups); + groups.forEach(group -> updateGroupStatus(group.getGroupId(), "01")); + } + + private void validateInboundBizDate(InboundSaveReqVO reqVO) { + if (reqVO.getBizDate() == null || reqVO.getBizDate().getYear() < 2000) { + throw exception(INBOUND_BIZ_DATE_INVALID); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteInbound(Long iostorinvId) { + IostorInvDO header = requireInbound(iostorinvId, "10"); + List details = iostorinvDtlMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDtlDO::getIostorinvId, iostorinvId)); + // 入库单删除后,原组盘数据恢复为生成状态,允许再次选择建单。 + details.stream() + .map(IostorinvDtlDO::getSourceBilldtlId) + .filter(StrUtil::isNotBlank) + .map(Long::valueOf) + .forEach(groupId -> updateGroupStatus(groupId, "00")); + // 分配表、明细表按业务要求物理删除,避免留下孤立记录。 + iostorinvDisMapper.physicallyDeleteByIostorinvId(iostorinvId); + iostorinvDtlMapper.physicallyDeleteByIostorinvId(iostorinvId); + // 主表沿用 MyBatis-Plus 逻辑删除,仅更新 deleted 字段。 + iostorInvMapper.deleteById(header.getIostorinvId()); + } + + @Override + public List getInboundGroupPlates(String storId) { + // 生成状态的组盘数据尚未进入仓库,因此此处不按 storId 过滤。 + return groupPlateMapper.selectList(new LambdaQueryWrapperX() + .eq(GroupPlateDO::getStatus, "00") + .orderByDesc(GroupPlateDO::getGroupId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void allocateInbound(InboundAllocationReqVO reqVO) { + IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId()); + if (header == null || !"0".equals(header.getIoType()) + || !Set.of("10", "20").contains(header.getBillStatus())) { + throw exception(INBOUND_STATUS_INVALID); + } + for (InboundAllocationReqVO.Item item : reqVO.getItems()) { + IostorinvDtlDO detail = iostorinvDtlMapper.selectById(item.getIostorinvdtlId()); + SectAttrDO sect = sectAttrMapper.selectById(item.getSectId()); + if (detail == null || !Objects.equals(detail.getIostorinvId(), header.getIostorinvId()) + || sect == null || !Objects.equals(header.getStorId(), sect.getStorId())) { + throw exception(INBOUND_TARGET_INVALID); + } + StrucAttrDO target; + if (Boolean.TRUE.equals(reqVO.getAutoAllocate())) { + target = getInboundAvailableStructs(header.getStorId(), item.getSectId()).stream() + .findFirst().orElseThrow(() -> exception(INBOUND_TARGET_INVALID)); + } else { + if (StrUtil.isBlank(item.getStructId())) { + throw exception(INBOUND_TARGET_INVALID); + } + target = strucAttrMapper.selectById(item.getStructId()); + } + if (target == null + || !Objects.equals(header.getStorId(), target.getStorId()) + || !Objects.equals(item.getSectId(), target.getSectId()) + || !"1".equals(target.getLockType()) + || StrUtil.isNotBlank(target.getStoragevehicleCode())) { + throw exception(INBOUND_TARGET_INVALID); + } + if (strucAttrMapper.lockForInbound(target.getStructId(), + String.valueOf(header.getIostorinvId()), header.getBillCode()) != 1) { + throw exception(INBOUND_TARGET_INVALID); + } + IostorinvDisDO allocation = iostorinvDisMapper.selectOne( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId())) + .eq(IostorinvDisDO::getIostorinvdtlId, + String.valueOf(detail.getIostorinvdtlId()))); + if (allocation == null || StrUtil.isNotBlank(allocation.getStructId())) { + throw exception(INBOUND_TARGET_INVALID); + } + allocation.setSectId(sect.getSectId()); + allocation.setSectCode(sect.getSectCode()); + allocation.setSectName(sect.getSectName()); + allocation.setStructId(target.getStructId()); + allocation.setStructCode(target.getStructCode()); + allocation.setStructName(target.getStructName()); + iostorinvDisMapper.updateById(allocation); + detail.setAssignQty(detail.getPlanQty()); + detail.setUnassignQty(BigDecimal.ZERO); + detail.setBillStatus("30"); + iostorinvDtlMapper.updateById(detail); + } + refreshInboundAllocationStatus(header.getIostorinvId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelInboundAllocation(Long iostorinvId, Long iostorinvdtlId) { + IostorInvDO header = iostorInvMapper.selectById(iostorinvId); + if (header == null || !"0".equals(header.getIoType()) + || !Set.of("20", "30").contains(header.getBillStatus())) { + throw exception(INBOUND_STATUS_INVALID); + } + IostorinvDtlDO detail = iostorinvDtlMapper.selectById(iostorinvdtlId); + IostorinvDisDO allocation = iostorinvDisMapper.selectOne( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId)) + .eq(IostorinvDisDO::getIostorinvdtlId, String.valueOf(iostorinvdtlId))); + if (detail == null || !Objects.equals(detail.getIostorinvId(), iostorinvId) + || allocation == null || StrUtil.isBlank(allocation.getStructId())) { + throw exception(INBOUND_NOT_ALLOCATED); + } + // 任务一旦创建并下发,不能只释放货位;需走后续的任务撤销流程。 + if (StrUtil.isNotBlank(allocation.getTaskId())) { + throw exception(INBOUND_ALLOCATION_TASK_CREATED); + } + if (strucAttrMapper.unlockInboundStruct(allocation.getStructId(), + String.valueOf(iostorinvId)) != 1) { + throw exception(INBOUND_TARGET_INVALID); + } + // 取消分配要求直接物理删除分配记录,不保留逻辑删除数据。 + iostorinvDisMapper.physicallyDeleteInboundAllocation(allocation.getIostorinvdisId()); + detail.setAssignQty(BigDecimal.ZERO); + detail.setUnassignQty(detail.getPlanQty()); + detail.setBillStatus("10"); + iostorinvDtlMapper.updateById(detail); + refreshInboundAllocationStatus(iostorinvId); + } + + @Override + public List getInboundAvailableStructs(String storId, String sectId) { + return strucAttrMapper.selectList(new LambdaQueryWrapperX() + .eq(StrucAttrDO::getStorId, storId) + .eq(StrucAttrDO::getSectId, sectId) + .eq(StrucAttrDO::getIsUsed, true) + .eq(StrucAttrDO::getLockType, "1") + .and(wrapper -> wrapper.isNull(StrucAttrDO::getStoragevehicleCode) + .or().eq(StrucAttrDO::getStoragevehicleCode, "")) + .orderByAsc(StrucAttrDO::getRowNum, StrucAttrDO::getColNum, + StrucAttrDO::getLayerNum, StrucAttrDO::getStructId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void setInboundPoint(InboundSetPointReqVO reqVO) { + IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId()); + if (header == null || !"0".equals(header.getIoType()) + || !Set.of("10", "20", "30").contains(header.getBillStatus())) { + throw exception(INBOUND_STATUS_INVALID); + } + IostorinvDisDO allocation = iostorinvDisMapper.selectOne( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId())) + .eq(IostorinvDisDO::getIostorinvdtlId, + String.valueOf(reqVO.getIostorinvdtlId()))); + if (allocation == null || StrUtil.isBlank(allocation.getStructId())) { + throw exception(INBOUND_NOT_ALLOCATED); + } + if (StrUtil.isNotBlank(allocation.getTaskId())) { + throw exception(INBOUND_ALLOCATION_TASK_CREATED); + } + allocation.setPointCode(reqVO.getPointCode()); + SectAttrDO sect = sectAttrMapper.selectById(allocation.getSectId()); + if (sect != null && "主存区".equals(sect.getSectTypeAttr())) { + IostorinvDtlDO detail = iostorinvDtlMapper.selectById(reqVO.getIostorinvdtlId()); + GroupPlateDO group = groupPlateMapper.selectById(Long.valueOf(detail.getSourceBilldtlId())); + Long taskId = createAndIssueInboundTask(reqVO.getPointCode(), allocation.getStructCode(), + group, header.getIostorinvId(), detail.getSeqNo(), allocation.getIostorinvdisId()); + allocation.setTaskId(String.valueOf(taskId)); + allocation.setIsIssued("1"); + allocation.setWorkStatus("20"); + } + iostorinvDisMapper.updateById(allocation); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeInbound(Long iostorinvId) { + IostorInvDO header = requireInbound(iostorinvId, "30"); + List allocations = iostorinvDisMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId))); + if (allocations.isEmpty()) { + throw exception(INBOUND_NOT_ALLOCATED); + } + for (IostorinvDisDO allocation : allocations) { + if (strucAttrMapper.completeInbound(allocation.getStructId(), + String.valueOf(iostorinvId), allocation.getStoragevehicleCode()) != 1) { + throw exception(INBOUND_TARGET_INVALID); + } + IostorinvDtlDO detail = iostorinvDtlMapper.selectById( + Long.valueOf(allocation.getIostorinvdtlId())); + updateGroupStatus(Long.valueOf(detail.getSourceBilldtlId()), "02"); + allocation.setWorkStatus("99"); + allocation.setRealQty(allocation.getPlanQty()); + iostorinvDisMapper.updateById(allocation); + detail.setBillStatus("99"); + detail.setRealQty(detail.getPlanQty()); + iostorinvDtlMapper.updateById(detail); + } + IostorInvDO update = new IostorInvDO(); + update.setIostorinvId(iostorinvId); + update.setBillStatus("99"); + update.setConfirmTime(java.time.LocalDateTime.now()); + iostorInvMapper.updateById(update); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeInboundTask(Long taskId, Long confirmUserId) { + String taskIdValue = String.valueOf(taskId); + List allocations = iostorinvDisMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getTaskId, taskIdValue)); + if (allocations.isEmpty()) { + throw new IllegalStateException("未找到入库任务对应的分配记录,taskId=" + taskId); + } + + Set detailIds = new LinkedHashSet<>(); + Set headerIds = new LinkedHashSet<>(); + for (IostorinvDisDO allocation : allocations) { + Long headerId = Long.valueOf(allocation.getIostorinvId()); + Long detailId = Long.valueOf(allocation.getIostorinvdtlId()); + IostorinvDtlDO detail = iostorinvDtlMapper.selectById(detailId); + if (detail == null || !Objects.equals(detail.getIostorinvId(), headerId)) { + throw new IllegalStateException("入库任务对应的明细不存在,taskId=" + taskId); + } + + // 任务完成后将载具正式绑定到目标仓位,并释放该仓位的入库锁。 + if (strucAttrMapper.completeInbound(allocation.getStructId(), + allocation.getIostorinvId(), allocation.getStoragevehicleCode()) != 1) { + throw exception(INBOUND_TARGET_INVALID); + } + updateGroupStatus(Long.valueOf(detail.getSourceBilldtlId()), "02"); + + allocation.setWorkStatus("99"); + allocation.setRealQty(allocation.getPlanQty()); + allocation.setIsIssued("1"); + iostorinvDisMapper.updateById(allocation); + detailIds.add(detailId); + headerIds.add(headerId); + } + + // 当前明细下的全部分配完成后,才完成明细。 + for (Long detailId : detailIds) { + if (iostorinvDisMapper.countUnfinishedByDetailId(detailId) == 0) { + IostorinvDtlDO detail = iostorinvDtlMapper.selectById(detailId); + IostorinvDtlDO detailUpdate = new IostorinvDtlDO(); + detailUpdate.setIostorinvdtlId(detailId); + detailUpdate.setBillStatus("99"); + detailUpdate.setRealQty(detail.getPlanQty()); + iostorinvDtlMapper.updateById(detailUpdate); + } + } + + // 所有明细完成后完成主表,并记录最后一个任务完成时的时间和操作人。 + for (Long headerId : headerIds) { + if (iostorinvDtlMapper.countUnfinishedByIostorinvId(headerId) == 0) { + IostorInvDO headerUpdate = new IostorInvDO(); + headerUpdate.setIostorinvId(headerId); + headerUpdate.setBillStatus("99"); + headerUpdate.setConfirmTime(java.time.LocalDateTime.now()); + if (confirmUserId != null) { + headerUpdate.setConfirmOptid(String.valueOf(confirmUserId)); + } + iostorInvMapper.updateById(headerUpdate); + } + } + log.info("入库任务完成业务处理成功, taskId={}, allocationCount={}", taskId, allocations.size()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelInboundTask(Long taskId) { + String taskIdValue = String.valueOf(taskId); + List allocations = iostorinvDisMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getTaskId, taskIdValue)); + if (allocations.isEmpty()) { + throw new IllegalStateException("未找到入库任务对应的分配记录,taskId=" + taskId); + } + int cleared = iostorinvDisMapper.clearInboundTask(taskIdValue); + if (cleared != allocations.size()) { + throw new IllegalStateException("入库任务取消时分配记录已发生变化,taskId=" + taskId); + } + log.info("入库任务取消业务处理成功, taskId={}, allocationCount={}", taskId, allocations.size()); + } + + private List validateInboundGroups(List groupIds, Set editableIds) { + Set ids = new LinkedHashSet<>(groupIds); + if (ids.size() != groupIds.size()) { + throw exception(INBOUND_GROUP_PLATE_INVALID); + } + List groups = groupPlateMapper.selectBatchIds(ids); + if (groups.size() != ids.size() || groups.stream().anyMatch(group -> + !("00".equals(group.getStatus()) + || editableIds != null && editableIds.contains(group.getGroupId())))) { + throw exception(INBOUND_GROUP_PLATE_INVALID); + } + return groups; + } + + private void insertInboundDetails(Long headerId, List groups) { + int seq = 1; + for (GroupPlateDO group : groups) { + IostorinvDtlDO detail = new IostorinvDtlDO(); + detail.setIostorinvId(headerId); + detail.setSeqNo(seq++); + detail.setMaterialId(group.getMaterialId()); + detail.setMaterialCode(group.getMaterialCode()); + detail.setPcsn(group.getPcsn()); + detail.setQtyUnitId(StrUtil.blankToDefault(group.getQtyUnitId(), "0")); + detail.setPlanQty(group.getQty()); + detail.setAssignQty(BigDecimal.ZERO); + detail.setUnassignQty(group.getQty()); + detail.setBillStatus("10"); + detail.setSourceBillCode(group.getExtCode()); + detail.setSourceBillType(group.getExtType()); + detail.setSourceBilldtlId(String.valueOf(group.getGroupId())); + // 入库明细表没有独立载具字段,使用来源装载点保存木箱号。 + detail.setSourceLoadPort(group.getVehicleCode()); + iostorinvDtlMapper.insert(detail); + } + } + + /** + * 入库单创建时即生成分配占位记录。此时没有库区、货位和起点,状态统一为生成。 + */ + private void insertInboundAllocationPlaceholders(IostorInvDO header, List groups) { + List details = iostorinvDtlMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId()) + .orderByAsc(IostorinvDtlDO::getSeqNo)); + Map groupMap = groups.stream().collect( + Collectors.toMap(group -> String.valueOf(group.getGroupId()), group -> group)); + for (IostorinvDtlDO detail : details) { + insertInboundAllocationPlaceholder(header, detail, groupMap.get(detail.getSourceBilldtlId())); + } + } + + @Override + public void ensureAllocationPlaceholder(IostorInvDO header, IostorinvDtlDO detail) { + GroupPlateDO group = StrUtil.isBlank(detail.getSourceBilldtlId()) + ? null : groupPlateMapper.selectById(Long.valueOf(detail.getSourceBilldtlId())); + insertInboundAllocationPlaceholder(header, detail, group); + } + + private void insertInboundAllocationPlaceholder(IostorInvDO header, IostorinvDtlDO detail, + GroupPlateDO group) { + IostorinvDisDO row = new IostorinvDisDO(); + row.setIostorinvdisId(IdUtil.getSnowflakeNextIdStr()); + row.setIostorinvId(String.valueOf(header.getIostorinvId())); + row.setIostorinvdtlId(String.valueOf(detail.getIostorinvdtlId())); + row.setSeqNo(String.valueOf(detail.getSeqNo())); + row.setMaterialId(detail.getMaterialId()); + row.setMaterialCode(detail.getMaterialCode()); + row.setPcsn(StrUtil.blankToDefault(detail.getPcsn(), "")); + row.setWorkStatus("10"); + row.setStoragevehicleCode(group == null ? detail.getSourceLoadPort() : group.getVehicleCode()); + row.setIsIssued("0"); + row.setQtyUnitId(detail.getQtyUnitId()); + row.setQtyUnitName(group == null ? "" : StrUtil.blankToDefault(group.getQtyUnitName(), "")); + row.setPlanQty(detail.getPlanQty()); + row.setRealQty(BigDecimal.ZERO); + row.setHandType(false); + iostorinvDisMapper.insert(row); + } + + private void refreshInboundAllocationStatus(Long iostorinvId) { + List allocations = iostorinvDisMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId))); + boolean allAllocated = !allocations.isEmpty() + && allocations.stream().allMatch(item -> StrUtil.isNotBlank(item.getStructId())); + IostorInvDO update = new IostorInvDO(); + update.setIostorinvId(iostorinvId); + update.setBillStatus(allAllocated ? "30" : "20"); + update.setDisTime(java.time.LocalDateTime.now()); + iostorInvMapper.updateById(update); + } + + private IostorInvDO requireInbound(Long id, String status) { + IostorInvDO header = iostorInvMapper.selectById(id); + if (header == null || !"0".equals(header.getIoType())) { + throw exception(IOSTOR_INV_NOT_EXISTS); + } + if (!status.equals(header.getBillStatus())) { + throw exception(INBOUND_STATUS_INVALID); + } + return header; + } + + private void updateGroupStatus(Long groupId, String status) { + GroupPlateDO update = new GroupPlateDO(); + update.setGroupId(groupId); + update.setStatus(status); + groupPlateMapper.updateById(update); + } + + private String generateIoBillCode(String scene) { + try { + CommonResult result = codeGenApi.generate( + new CodeGenerateReqDTO().setRuleCode("IO_CODE")); + if (result != null && result.isSuccess() && StrUtil.isNotBlank(result.getData())) { + return result.getData(); + } + } catch (RuntimeException ex) { + log.error("[{}][生成入库单号异常]", scene, ex); + } + throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); + } + + private IostorinvDisDO buildInboundAllocation(IostorInvDO header, IostorinvDtlDO detail, + GroupPlateDO group, SectAttrDO sect, + StrucAttrDO target, String pointCode) { + IostorinvDisDO row = new IostorinvDisDO(); + row.setIostorinvdisId(IdUtil.getSnowflakeNextIdStr()); + row.setIostorinvId(String.valueOf(header.getIostorinvId())); + row.setIostorinvdtlId(String.valueOf(detail.getIostorinvdtlId())); + row.setSeqNo(String.valueOf(detail.getSeqNo())); + row.setSectId(sect.getSectId()); + row.setSectCode(sect.getSectCode()); + row.setSectName(sect.getSectName()); + row.setStructId(target.getStructId()); + row.setStructCode(target.getStructCode()); + row.setStructName(target.getStructName()); + row.setMaterialId(detail.getMaterialId()); + row.setMaterialCode(detail.getMaterialCode()); + row.setPcsn(StrUtil.blankToDefault(detail.getPcsn(), "")); + row.setWorkStatus("10"); + row.setStoragevehicleCode(group.getVehicleCode()); + row.setIsIssued("0"); + row.setQtyUnitId(detail.getQtyUnitId()); + row.setQtyUnitName(StrUtil.blankToDefault(group.getQtyUnitName(), "")); + row.setPlanQty(detail.getPlanQty()); + row.setRealQty(BigDecimal.ZERO); + row.setPointCode(pointCode); + row.setHandType(false); + return row; + } + + private Long createAndIssueInboundTask(String sourcePoint, String targetPoint, + GroupPlateDO group, long taskGroupId, + long sortSeq, String bizId) { + TransportTaskCreateReqDTO task = new TransportTaskCreateReqDTO(); + task.setTaskName("入库任务"); + task.setOwnerService("WMS"); + task.setBizType("INBOUND"); + task.setBizId(bizId); + task.setHandleCode(InTask.class.getName()); + task.setTaskType("010501"); + task.setAcsTaskType("7"); + task.setAgvSystemType("1"); + task.setPointCode1(sourcePoint); + task.setPointCode2(targetPoint); + task.setVehicleCode(group.getVehicleCode()); + if (StrUtil.isNotBlank(group.getMaterialId())) { + task.setMaterialId(Long.valueOf(group.getMaterialId())); + } + task.setMaterialCode(group.getMaterialCode()); + task.setTaskGroupId(taskGroupId); + task.setSortSeq(sortSeq); + task.setIsAutoIssue("0"); + task.setProductArea("LK"); + task.setIsCreateFinish(true); + Long taskId = transportTaskApi.createTransportTask(task).getCheckedData(); + transportTaskApi.issueTransportTaskRollBack(taskId).getCheckedData(); + return taskId; + } + +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java index 6a359f9e..a8c1421c 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java @@ -23,24 +23,11 @@ public interface IostorInvService { */ Long createOutbound(@Valid IostorInvCreateReqVO reqVO); - Long createInbound(@Valid InboundSaveReqVO reqVO); + /** 根据搬运任务完成对应出库单业务。 */ + void completeOutboundTask(Long taskId, Long confirmUserId); - void updateInbound(@Valid InboundSaveReqVO reqVO); - - void deleteInbound(Long iostorinvId); - - List getInboundGroupPlates(String storId); - - void allocateInbound(@Valid InboundAllocationReqVO reqVO); - - void cancelInboundAllocation(Long iostorinvId, Long iostorinvdtlId); - - List - getInboundAvailableStructs(String storId, String sectId); - - void setInboundPoint(@Valid InboundSetPointReqVO reqVO); - - void completeInbound(Long iostorinvId); + /** 根据搬运任务取消对应出库分配。 */ + void cancelOutboundTask(Long taskId); /** * 获得指定仓库的可用库存分页。 diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java index 3635a1b5..1b77bd76 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java @@ -1,5 +1,7 @@ package cn.code.nl.module.wms.service.iostorinv; +import cn.code.nl.module.wms.task.MoveTask; +import cn.code.nl.module.wms.task.OutTask; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; @@ -94,440 +96,97 @@ public class IostorInvServiceImpl implements IostorInvService { @Resource private AdminUserApi adminUserApi; - @Override - @Transactional(rollbackFor = Exception.class) - public Long createInbound(InboundSaveReqVO reqVO) { - List groups = validateInboundGroups(reqVO.getGroupIds(), null); - IostorInvDO header = new IostorInvDO(); - header.setBillCode(generateIoBillCode("createInbound")); - header.setIoType("0"); - header.setBillType(reqVO.getBillType()); - header.setBizDate(reqVO.getBizDate()); - header.setStorId(reqVO.getStorId()); - header.setBillStatus("10"); - header.setCreateMode("01"); - header.setRemark(reqVO.getRemark()); - header.setDetailCount(groups.size()); - header.setTotalQty(groups.stream().map(GroupPlateDO::getQty) - .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); - header.setTotalWeight(header.getTotalQty()); - iostorInvMapper.insert(header); - insertInboundDetails(header.getIostorinvId(), groups); - insertInboundAllocationPlaceholders(header, groups); - groups.forEach(group -> updateGroupStatus(group.getGroupId(), "01")); - return header.getIostorinvId(); - } + @Resource + private InboundIostorInvService inboundIostorInvService; @Override @Transactional(rollbackFor = Exception.class) - public void updateInbound(InboundSaveReqVO reqVO) { - IostorInvDO header = requireInbound(reqVO.getIostorinvId(), "10"); - List oldDetails = iostorinvDtlMapper.selectList( - new LambdaQueryWrapperX() - .eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId())); - Set oldGroupIds = oldDetails.stream().map(IostorinvDtlDO::getSourceBilldtlId) - .filter(StrUtil::isNotBlank).map(Long::valueOf).collect(Collectors.toSet()); - List groups = validateInboundGroups(reqVO.getGroupIds(), oldGroupIds); - oldGroupIds.forEach(id -> updateGroupStatus(id, "00")); - strucAttrMapper.unlockInbound(String.valueOf(header.getIostorinvId())); - iostorinvDisMapper.delete(new LambdaQueryWrapperX() - .eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId()))); - iostorinvDtlMapper.delete(new LambdaQueryWrapperX() - .eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId())); - IostorInvDO update = new IostorInvDO(); - update.setIostorinvId(header.getIostorinvId()); - update.setBillType(reqVO.getBillType()); - update.setBizDate(reqVO.getBizDate()); - update.setStorId(reqVO.getStorId()); - update.setRemark(reqVO.getRemark()); - update.setDetailCount(groups.size()); - update.setTotalQty(groups.stream().map(GroupPlateDO::getQty) - .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); - update.setTotalWeight(update.getTotalQty()); - iostorInvMapper.updateById(update); - insertInboundDetails(header.getIostorinvId(), groups); - insertInboundAllocationPlaceholders(header, groups); - groups.forEach(group -> updateGroupStatus(group.getGroupId(), "01")); - } + public void completeOutboundTask(Long taskId, Long confirmUserId) { + String taskIdValue = String.valueOf(taskId); + List allocations = iostorinvDisMapper.selectList( + new LambdaQueryWrapperX() + .eq(IostorinvDisDO::getTaskId, taskIdValue)); + if (CollUtil.isEmpty(allocations)) { + throw new IllegalStateException("未找到出库任务对应的分配记录,taskId=" + taskId); + } - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteInbound(Long iostorinvId) { - IostorInvDO header = requireInbound(iostorinvId, "10"); - List details = iostorinvDtlMapper.selectList( - new LambdaQueryWrapperX() - .eq(IostorinvDtlDO::getIostorinvId, iostorinvId)); - // 入库单删除后,原组盘数据恢复为生成状态,允许再次选择建单。 - details.stream() - .map(IostorinvDtlDO::getSourceBilldtlId) + // 1. 同一任务可能关联同一载具下的多条分配记录,统一推进为完成。 + iostorinvDisMapper.completeOutboundAllocations(taskIdValue); + + // 2. 出库完成后释放起点仓位,同时移除仓位上的载具占用信息。 + allocations.stream() + .filter(item -> StrUtil.isNotBlank(item.getStructId())) + .map(IostorinvDisDO::getStructId) + .distinct() + .forEach(structId -> strucAttrMapper.completeOutboundTask(structId, taskIdValue)); + + // 3. 一个木箱可能对应多条组盘记录,全部更新为“出库”。 + Set vehicleCodes = allocations.stream() + .map(IostorinvDisDO::getStoragevehicleCode) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (CollUtil.isNotEmpty(vehicleCodes)) { + GroupPlateDO groupUpdate = new GroupPlateDO(); + groupUpdate.setStatus("03"); + groupPlateMapper.update(groupUpdate, new LambdaQueryWrapperX() + .in(GroupPlateDO::getVehicleCode, vehicleCodes)); + } + + // 4. 分配全部完成后完成对应明细;明细全部完成后再完成主表。 + Set detailIds = allocations.stream() + .map(IostorinvDisDO::getIostorinvdtlId) .filter(StrUtil::isNotBlank) .map(Long::valueOf) - .forEach(groupId -> updateGroupStatus(groupId, "00")); - // 分配表、明细表按业务要求物理删除,避免留下孤立记录。 - iostorinvDisMapper.physicallyDeleteByIostorinvId(iostorinvId); - iostorinvDtlMapper.physicallyDeleteByIostorinvId(iostorinvId); - // 主表沿用 MyBatis-Plus 逻辑删除,仅更新 deleted 字段。 - iostorInvMapper.deleteById(header.getIostorinvId()); - } - - @Override - public List getInboundGroupPlates(String storId) { - // 生成状态的组盘数据尚未进入仓库,因此此处不按 storId 过滤。 - return groupPlateMapper.selectList(new LambdaQueryWrapperX() - .eq(GroupPlateDO::getStatus, "00") - .orderByDesc(GroupPlateDO::getGroupId)); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void allocateInbound(InboundAllocationReqVO reqVO) { - IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId()); - if (header == null || !"0".equals(header.getIoType()) - || !Set.of("10", "20").contains(header.getBillStatus())) { - throw exception(INBOUND_STATUS_INVALID); - } - for (InboundAllocationReqVO.Item item : reqVO.getItems()) { - IostorinvDtlDO detail = iostorinvDtlMapper.selectById(item.getIostorinvdtlId()); - SectAttrDO sect = sectAttrMapper.selectById(item.getSectId()); - if (detail == null || !Objects.equals(detail.getIostorinvId(), header.getIostorinvId()) - || sect == null || !Objects.equals(header.getStorId(), sect.getStorId())) { - throw exception(INBOUND_TARGET_INVALID); + .collect(Collectors.toCollection(LinkedHashSet::new)); + Set headerIds = new LinkedHashSet<>(); + for (Long detailId : detailIds) { + IostorinvDtlDO detail = iostorinvDtlMapper.selectById(detailId); + if (detail == null) { + continue; } - StrucAttrDO target; - if (Boolean.TRUE.equals(reqVO.getAutoAllocate())) { - target = getInboundAvailableStructs(header.getStorId(), item.getSectId()).stream() - .findFirst().orElseThrow(() -> exception(INBOUND_TARGET_INVALID)); - } else { - if (StrUtil.isBlank(item.getStructId())) { - throw exception(INBOUND_TARGET_INVALID); + headerIds.add(detail.getIostorinvId()); + if (iostorinvDisMapper.countUnfinishedByDetailId(detailId) == 0) { + IostorinvDtlDO detailUpdate = new IostorinvDtlDO(); + detailUpdate.setIostorinvdtlId(detailId); + detailUpdate.setBillStatus("99"); + detailUpdate.setRealQty(Objects.requireNonNullElse(detail.getPlanQty(), BigDecimal.ZERO)); + iostorinvDtlMapper.updateById(detailUpdate); + } + } + for (Long headerId : headerIds) { + if (iostorinvDtlMapper.countUnfinishedByIostorinvId(headerId) == 0) { + IostorInvDO headerUpdate = new IostorInvDO(); + headerUpdate.setIostorinvId(headerId); + headerUpdate.setBillStatus("99"); + headerUpdate.setConfirmTime(java.time.LocalDateTime.now()); + if (confirmUserId != null) { + // 主表完成人以最后一个出库任务完成时的登录上下文用户为准。 + headerUpdate.setConfirmOptid(String.valueOf(confirmUserId)); } - target = strucAttrMapper.selectById(item.getStructId()); + iostorInvMapper.updateById(headerUpdate); } - if (target == null - || !Objects.equals(header.getStorId(), target.getStorId()) - || !Objects.equals(item.getSectId(), target.getSectId()) - || !"1".equals(target.getLockType()) - || StrUtil.isNotBlank(target.getStoragevehicleCode())) { - throw exception(INBOUND_TARGET_INVALID); - } - if (strucAttrMapper.lockForInbound(target.getStructId(), - String.valueOf(header.getIostorinvId()), header.getBillCode()) != 1) { - throw exception(INBOUND_TARGET_INVALID); - } - IostorinvDisDO allocation = iostorinvDisMapper.selectOne( - new LambdaQueryWrapperX() - .eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId())) - .eq(IostorinvDisDO::getIostorinvdtlId, - String.valueOf(detail.getIostorinvdtlId()))); - if (allocation == null || StrUtil.isNotBlank(allocation.getStructId())) { - throw exception(INBOUND_TARGET_INVALID); - } - allocation.setSectId(sect.getSectId()); - allocation.setSectCode(sect.getSectCode()); - allocation.setSectName(sect.getSectName()); - allocation.setStructId(target.getStructId()); - allocation.setStructCode(target.getStructCode()); - allocation.setStructName(target.getStructName()); - iostorinvDisMapper.updateById(allocation); - detail.setAssignQty(detail.getPlanQty()); - detail.setUnassignQty(BigDecimal.ZERO); - detail.setBillStatus("30"); - iostorinvDtlMapper.updateById(detail); } - refreshInboundAllocationStatus(header.getIostorinvId()); + log.info("出库任务完成业务处理成功, taskId={}, allocationCount={}", taskId, allocations.size()); } @Override @Transactional(rollbackFor = Exception.class) - public void cancelInboundAllocation(Long iostorinvId, Long iostorinvdtlId) { - IostorInvDO header = iostorInvMapper.selectById(iostorinvId); - if (header == null || !"0".equals(header.getIoType()) - || !Set.of("20", "30").contains(header.getBillStatus())) { - throw exception(INBOUND_STATUS_INVALID); - } - IostorinvDtlDO detail = iostorinvDtlMapper.selectById(iostorinvdtlId); - IostorinvDisDO allocation = iostorinvDisMapper.selectOne( - new LambdaQueryWrapperX() - .eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId)) - .eq(IostorinvDisDO::getIostorinvdtlId, String.valueOf(iostorinvdtlId))); - if (detail == null || !Objects.equals(detail.getIostorinvId(), iostorinvId) - || allocation == null || StrUtil.isBlank(allocation.getStructId())) { - throw exception(INBOUND_NOT_ALLOCATED); - } - // 任务一旦创建并下发,不能只释放货位;需走后续的任务撤销流程。 - if (StrUtil.isNotBlank(allocation.getTaskId())) { - throw exception(INBOUND_ALLOCATION_TASK_CREATED); - } - if (strucAttrMapper.unlockInboundStruct(allocation.getStructId(), - String.valueOf(iostorinvId)) != 1) { - throw exception(INBOUND_TARGET_INVALID); - } - // 取消分配要求直接物理删除分配记录,不保留逻辑删除数据。 - iostorinvDisMapper.physicallyDeleteInboundAllocation(allocation.getIostorinvdisId()); - detail.setAssignQty(BigDecimal.ZERO); - detail.setUnassignQty(detail.getPlanQty()); - detail.setBillStatus("10"); - iostorinvDtlMapper.updateById(detail); - refreshInboundAllocationStatus(iostorinvId); - } - - @Override - public List getInboundAvailableStructs(String storId, String sectId) { - return strucAttrMapper.selectList(new LambdaQueryWrapperX() - .eq(StrucAttrDO::getStorId, storId) - .eq(StrucAttrDO::getSectId, sectId) - .eq(StrucAttrDO::getIsUsed, true) - .eq(StrucAttrDO::getLockType, "1") - .and(wrapper -> wrapper.isNull(StrucAttrDO::getStoragevehicleCode) - .or().eq(StrucAttrDO::getStoragevehicleCode, "")) - .orderByAsc(StrucAttrDO::getRowNum, StrucAttrDO::getColNum, - StrucAttrDO::getLayerNum, StrucAttrDO::getStructId)); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void setInboundPoint(InboundSetPointReqVO reqVO) { - IostorInvDO header = iostorInvMapper.selectById(reqVO.getIostorinvId()); - if (header == null || !"0".equals(header.getIoType()) - || !Set.of("10", "20", "30").contains(header.getBillStatus())) { - throw exception(INBOUND_STATUS_INVALID); - } - IostorinvDisDO allocation = iostorinvDisMapper.selectOne( - new LambdaQueryWrapperX() - .eq(IostorinvDisDO::getIostorinvId, String.valueOf(header.getIostorinvId())) - .eq(IostorinvDisDO::getIostorinvdtlId, - String.valueOf(reqVO.getIostorinvdtlId()))); - if (allocation == null || StrUtil.isBlank(allocation.getStructId())) { - throw exception(INBOUND_NOT_ALLOCATED); - } - if (StrUtil.isNotBlank(allocation.getTaskId())) { - throw exception(INBOUND_ALLOCATION_TASK_CREATED); - } - allocation.setPointCode(reqVO.getPointCode()); - SectAttrDO sect = sectAttrMapper.selectById(allocation.getSectId()); - if (sect != null && "主存区".equals(sect.getSectTypeAttr())) { - IostorinvDtlDO detail = iostorinvDtlMapper.selectById(reqVO.getIostorinvdtlId()); - GroupPlateDO group = groupPlateMapper.selectById(Long.valueOf(detail.getSourceBilldtlId())); - Long taskId = createAndIssueInboundTask(reqVO.getPointCode(), allocation.getStructCode(), - group, header.getIostorinvId(), detail.getSeqNo(), allocation.getIostorinvdisId()); - allocation.setTaskId(String.valueOf(taskId)); - allocation.setIsIssued("1"); - allocation.setWorkStatus("20"); - } - iostorinvDisMapper.updateById(allocation); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void completeInbound(Long iostorinvId) { - IostorInvDO header = requireInbound(iostorinvId, "30"); + public void cancelOutboundTask(Long taskId) { + String taskIdValue = String.valueOf(taskId); List allocations = iostorinvDisMapper.selectList( new LambdaQueryWrapperX() - .eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId))); - if (allocations.isEmpty()) { - throw exception(INBOUND_NOT_ALLOCATED); + .eq(IostorinvDisDO::getTaskId, taskIdValue)); + if (CollUtil.isEmpty(allocations)) { + throw new IllegalStateException("未找到出库任务对应的分配记录,taskId=" + taskId); } - for (IostorinvDisDO allocation : allocations) { - if (strucAttrMapper.completeInbound(allocation.getStructId(), - String.valueOf(iostorinvId), allocation.getStoragevehicleCode()) != 1) { - throw exception(INBOUND_TARGET_INVALID); - } - IostorinvDtlDO detail = iostorinvDtlMapper.selectById( - Long.valueOf(allocation.getIostorinvdtlId())); - updateGroupStatus(Long.valueOf(detail.getSourceBilldtlId()), "02"); - allocation.setWorkStatus("99"); - allocation.setRealQty(allocation.getPlanQty()); - iostorinvDisMapper.updateById(allocation); - detail.setBillStatus("99"); - detail.setRealQty(detail.getPlanQty()); - iostorinvDtlMapper.updateById(detail); + // 任务关联的所有分配记录都必须仍为“生成”,防止已下发或执行中的任务回退业务数据。 + if (allocations.stream().anyMatch(item -> !"10".equals(item.getWorkStatus()))) { + throw new IllegalStateException("仅生成状态的出库分配允许取消,taskId=" + taskId); } - IostorInvDO update = new IostorInvDO(); - update.setIostorinvId(iostorinvId); - update.setBillStatus("99"); - update.setConfirmTime(java.time.LocalDateTime.now()); - iostorInvMapper.updateById(update); - } - - private List validateInboundGroups(List groupIds, Set editableIds) { - Set ids = new LinkedHashSet<>(groupIds); - if (ids.size() != groupIds.size()) { - throw exception(INBOUND_GROUP_PLATE_INVALID); + int cleared = iostorinvDisMapper.clearGeneratedOutboundTask(taskIdValue); + if (cleared != allocations.size()) { + throw new IllegalStateException("出库任务取消时分配记录状态已发生变化,taskId=" + taskId); } - List groups = groupPlateMapper.selectBatchIds(ids); - if (groups.size() != ids.size() || groups.stream().anyMatch(group -> - !("00".equals(group.getStatus()) - || editableIds != null && editableIds.contains(group.getGroupId())))) { - throw exception(INBOUND_GROUP_PLATE_INVALID); - } - return groups; - } - - private void insertInboundDetails(Long headerId, List groups) { - int seq = 1; - for (GroupPlateDO group : groups) { - IostorinvDtlDO detail = new IostorinvDtlDO(); - detail.setIostorinvId(headerId); - detail.setSeqNo(seq++); - detail.setMaterialId(group.getMaterialId()); - detail.setMaterialCode(group.getMaterialCode()); - detail.setPcsn(group.getPcsn()); - detail.setQtyUnitId(StrUtil.blankToDefault(group.getQtyUnitId(), "0")); - detail.setPlanQty(group.getQty()); - detail.setAssignQty(BigDecimal.ZERO); - detail.setUnassignQty(group.getQty()); - detail.setBillStatus("10"); - detail.setSourceBillCode(group.getExtCode()); - detail.setSourceBillType(group.getExtType()); - detail.setSourceBilldtlId(String.valueOf(group.getGroupId())); - // 入库明细表没有独立载具字段,使用来源装载点保存木箱号。 - detail.setSourceLoadPort(group.getVehicleCode()); - iostorinvDtlMapper.insert(detail); - } - } - - /** - * 入库单创建时即生成分配占位记录。此时没有库区、货位和起点,状态统一为生成。 - */ - private void insertInboundAllocationPlaceholders(IostorInvDO header, List groups) { - List details = iostorinvDtlMapper.selectList( - new LambdaQueryWrapperX() - .eq(IostorinvDtlDO::getIostorinvId, header.getIostorinvId()) - .orderByAsc(IostorinvDtlDO::getSeqNo)); - Map groupMap = groups.stream().collect( - Collectors.toMap(group -> String.valueOf(group.getGroupId()), group -> group)); - for (IostorinvDtlDO detail : details) { - insertInboundAllocationPlaceholder(header, detail, groupMap.get(detail.getSourceBilldtlId())); - } - } - - private void insertInboundAllocationPlaceholder(IostorInvDO header, IostorinvDtlDO detail) { - GroupPlateDO group = StrUtil.isBlank(detail.getSourceBilldtlId()) - ? null : groupPlateMapper.selectById(Long.valueOf(detail.getSourceBilldtlId())); - insertInboundAllocationPlaceholder(header, detail, group); - } - - private void insertInboundAllocationPlaceholder(IostorInvDO header, IostorinvDtlDO detail, - GroupPlateDO group) { - IostorinvDisDO row = new IostorinvDisDO(); - row.setIostorinvdisId(IdUtil.getSnowflakeNextIdStr()); - row.setIostorinvId(String.valueOf(header.getIostorinvId())); - row.setIostorinvdtlId(String.valueOf(detail.getIostorinvdtlId())); - row.setSeqNo(String.valueOf(detail.getSeqNo())); - row.setMaterialId(detail.getMaterialId()); - row.setMaterialCode(detail.getMaterialCode()); - row.setPcsn(StrUtil.blankToDefault(detail.getPcsn(), "")); - row.setWorkStatus("10"); - row.setStoragevehicleCode(group == null ? detail.getSourceLoadPort() : group.getVehicleCode()); - row.setIsIssued("0"); - row.setQtyUnitId(detail.getQtyUnitId()); - row.setQtyUnitName(group == null ? "" : StrUtil.blankToDefault(group.getQtyUnitName(), "")); - row.setPlanQty(detail.getPlanQty()); - row.setRealQty(BigDecimal.ZERO); - row.setHandType(false); - iostorinvDisMapper.insert(row); - } - - private void refreshInboundAllocationStatus(Long iostorinvId) { - List allocations = iostorinvDisMapper.selectList( - new LambdaQueryWrapperX() - .eq(IostorinvDisDO::getIostorinvId, String.valueOf(iostorinvId))); - boolean allAllocated = !allocations.isEmpty() - && allocations.stream().allMatch(item -> StrUtil.isNotBlank(item.getStructId())); - IostorInvDO update = new IostorInvDO(); - update.setIostorinvId(iostorinvId); - update.setBillStatus(allAllocated ? "30" : "20"); - update.setDisTime(java.time.LocalDateTime.now()); - iostorInvMapper.updateById(update); - } - - private IostorInvDO requireInbound(Long id, String status) { - IostorInvDO header = iostorInvMapper.selectById(id); - if (header == null || !"0".equals(header.getIoType())) { - throw exception(IOSTOR_INV_NOT_EXISTS); - } - if (!status.equals(header.getBillStatus())) { - throw exception(INBOUND_STATUS_INVALID); - } - return header; - } - - private void updateGroupStatus(Long groupId, String status) { - GroupPlateDO update = new GroupPlateDO(); - update.setGroupId(groupId); - update.setStatus(status); - groupPlateMapper.updateById(update); - } - - private String generateIoBillCode(String scene) { - try { - CommonResult result = codeGenApi.generate( - new CodeGenerateReqDTO().setRuleCode("IO_CODE")); - if (result != null && result.isSuccess() && StrUtil.isNotBlank(result.getData())) { - return result.getData(); - } - } catch (RuntimeException ex) { - log.error("[{}][生成入库单号异常]", scene, ex); - } - throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); - } - - private IostorinvDisDO buildInboundAllocation(IostorInvDO header, IostorinvDtlDO detail, - GroupPlateDO group, SectAttrDO sect, - StrucAttrDO target, String pointCode) { - IostorinvDisDO row = new IostorinvDisDO(); - row.setIostorinvdisId(IdUtil.getSnowflakeNextIdStr()); - row.setIostorinvId(String.valueOf(header.getIostorinvId())); - row.setIostorinvdtlId(String.valueOf(detail.getIostorinvdtlId())); - row.setSeqNo(String.valueOf(detail.getSeqNo())); - row.setSectId(sect.getSectId()); - row.setSectCode(sect.getSectCode()); - row.setSectName(sect.getSectName()); - row.setStructId(target.getStructId()); - row.setStructCode(target.getStructCode()); - row.setStructName(target.getStructName()); - row.setMaterialId(detail.getMaterialId()); - row.setMaterialCode(detail.getMaterialCode()); - row.setPcsn(StrUtil.blankToDefault(detail.getPcsn(), "")); - row.setWorkStatus("10"); - row.setStoragevehicleCode(group.getVehicleCode()); - row.setIsIssued("0"); - row.setQtyUnitId(detail.getQtyUnitId()); - row.setQtyUnitName(StrUtil.blankToDefault(group.getQtyUnitName(), "")); - row.setPlanQty(detail.getPlanQty()); - row.setRealQty(BigDecimal.ZERO); - row.setPointCode(pointCode); - row.setHandType(false); - return row; - } - - private Long createAndIssueInboundTask(String sourcePoint, String targetPoint, - GroupPlateDO group, long taskGroupId, - long sortSeq, String bizId) { - TransportTaskCreateReqDTO task = new TransportTaskCreateReqDTO(); - task.setTaskName("入库任务"); - task.setOwnerService("WMS"); - task.setBizType("INBOUND"); - task.setBizId(bizId); - task.setHandleCode("INTASK"); - task.setTaskType("010501"); - task.setAcsTaskType("7"); - task.setAgvSystemType("1"); - task.setPointCode1(sourcePoint); - task.setPointCode2(targetPoint); - task.setVehicleCode(group.getVehicleCode()); - if (StrUtil.isNotBlank(group.getMaterialId())) { - task.setMaterialId(Long.valueOf(group.getMaterialId())); - } - task.setMaterialCode(group.getMaterialCode()); - task.setTaskGroupId(taskGroupId); - task.setSortSeq(sortSeq); - task.setIsAutoIssue("0"); - task.setProductArea("LK"); - task.setIsCreateFinish(true); - Long taskId = transportTaskApi.createTransportTask(task).getCheckedData(); - transportTaskApi.issueTransportTaskRollBack(taskId).getCheckedData(); - return taskId; } /** @@ -764,7 +423,7 @@ public class IostorInvServiceImpl implements IostorInvService { IostorinvDtlDO detail = iostorinvDtlMapper.selectById(iostorinvdtlId); if (header != null && "0".equals(header.getIoType()) && detail != null && Objects.equals(detail.getIostorinvId(), iostorinvId)) { - insertInboundAllocationPlaceholder(header, detail); + inboundIostorInvService.ensureAllocationPlaceholder(header, detail); result = iostorinvDisMapper.selectAllocationList( iostorinvId, iostorinvdtlId, sectId); } @@ -1037,8 +696,8 @@ public class IostorInvServiceImpl implements IostorInvService { task.setOwnerService("WMS"); task.setBizType(bizType); task.setBizId(bizId); - // TODO 填写出库任务在 WMS 中对应的任务回调处理器编码。 - task.setHandleCode("OUTTASK"); + // 移库和出库任务分别交给各自处理器,避免移库任务误走出库完成逻辑。 + task.setHandleCode("MOVE".equals(bizType) ? MoveTask.class.getName() : OutTask.class.getName()); task.setTaskType(taskType); task.setAcsTaskType("7"); // TODO 填写现场使用的 AGV 系统类型。 diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java index a1e2a12c..d483fc4b 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvService.java @@ -39,4 +39,8 @@ public interface MoveInvService { * @param taskId 搬运任务标识 */ void bindTask(Long moveinvdtlId, Long taskId); + + void completeMoveTask(Long taskId, Long confirmUserId); + + void cancelMoveTask(Long taskId); } diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java index d3fccf96..42ae65b9 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/moveinv/MoveInvServiceImpl.java @@ -28,6 +28,7 @@ import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -245,6 +246,67 @@ public class MoveInvServiceImpl implements MoveInvService { moveInvDtlMapper.updateById(update); } + @Override + @Transactional(rollbackFor = Exception.class) + public void completeMoveTask(Long taskId, Long confirmUserId) { + List details = moveInvDtlMapper.selectList( + new cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(MoveInvDtlDO::getTaskId, taskId)); + if (details.isEmpty()) { + throw new IllegalStateException("未找到移库任务对应的明细,taskId=" + taskId); + } + + Set moveInvIds = new HashSet<>(); + for (MoveInvDtlDO detail : details) { + String moveInvId = String.valueOf(detail.getMoveinvId()); + // 先占用目标仓位,再清理来源仓位;任一步失败都会回滚整个事务。 + if (strucAttrMapper.completeMoveIn(detail.getTurninStructCode(), moveInvId, + detail.getStoragevehicleCode()) != 1) { + throw exception(MOVE_INV_TARGET_INVALID); + } + if (strucAttrMapper.completeMoveOut(detail.getTurnoutStructCode(), moveInvId, + detail.getStoragevehicleCode()) != 1) { + throw exception(MOVE_INV_SOURCE_INVALID); + } + + MoveInvDtlDO detailUpdate = new MoveInvDtlDO(); + detailUpdate.setMoveinvdtlId(detail.getMoveinvdtlId()); + detailUpdate.setWorkStatus("99"); + detailUpdate.setIsIssued("1"); + moveInvDtlMapper.updateById(detailUpdate); + moveInvIds.add(detail.getMoveinvId()); + } + + // 当前移库单全部明细完成后,更新主表完成状态、时间和最后任务操作人。 + for (Long moveInvId : moveInvIds) { + if (moveInvDtlMapper.countUnfinishedByMoveinvId(moveInvId) == 0) { + MoveInvDO headerUpdate = new MoveInvDO(); + headerUpdate.setMoveinvId(moveInvId); + headerUpdate.setBillStatus("99"); + headerUpdate.setConfirmTime(LocalDateTime.now()); + if (confirmUserId != null) { + headerUpdate.setConfirmor(String.valueOf(confirmUserId)); + } + moveInvMapper.updateById(headerUpdate); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelMoveTask(Long taskId) { + List details = moveInvDtlMapper.selectList( + new cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(MoveInvDtlDO::getTaskId, taskId)); + if (details.isEmpty()) { + throw new IllegalStateException("未找到移库任务对应的明细,taskId=" + taskId); + } + int cleared = moveInvDtlMapper.clearTaskId(taskId); + if (cleared != details.size()) { + throw new IllegalStateException("移库任务取消时明细已发生变化,taskId=" + taskId); + } + } + /** * 创建主表、明细并锁定来源和目标仓位。 */ diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/EmptyVehicleTask.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/EmptyVehicleTask.java new file mode 100644 index 00000000..bf5a1624 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/EmptyVehicleTask.java @@ -0,0 +1,22 @@ +package cn.code.nl.module.wms.task; + +import cn.code.nl.framework.execute.core.AbstractTask; +import cn.code.nl.framework.execute.core.dto.TaskExecuteDTO; +import cn.code.nl.module.wms.service.emptyvehicle.EmptyVehicleService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +@Component("EMPTYVEHICLETASK") +public class EmptyVehicleTask extends AbstractTask { + @Resource private EmptyVehicleService emptyVehicleService; + + @Override + public void doHandleFinish(TaskExecuteDTO dto) { + emptyVehicleService.completeTask(dto.getTaskId()); + } + + @Override + public void doHandleCancel(TaskExecuteDTO dto) { + emptyVehicleService.cancelTask(dto.getTaskId()); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/InTask.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/InTask.java new file mode 100644 index 00000000..d4fce2ee --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/InTask.java @@ -0,0 +1,30 @@ +package cn.code.nl.module.wms.task; + +import cn.code.nl.framework.execute.core.AbstractTask; +import cn.code.nl.framework.execute.core.dto.TaskExecuteDTO; +import cn.code.nl.module.wms.service.iostorinv.InboundIostorInvService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** 入库任务回调处理器,具体事务逻辑统一封装在入库 Service 中。 */ +@Slf4j +@Component +public class InTask extends AbstractTask { + + @Resource + private InboundIostorInvService inboundIostorInvService; + + @Override + public void doHandleFinish(TaskExecuteDTO taskExecuteDTO) { + inboundIostorInvService.completeInboundTask( + taskExecuteDTO.getTaskId(), taskExecuteDTO.getOperatorId()); + } + + @Override + public void doHandleCancel(TaskExecuteDTO taskExecuteDTO) { + inboundIostorInvService.cancelInboundTask(taskExecuteDTO.getTaskId()); + log.info("入库任务取消完成,已清空分配记录起点和任务ID, taskId={}", + taskExecuteDTO.getTaskId()); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/MoveTask.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/MoveTask.java new file mode 100644 index 00000000..0357997c --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/MoveTask.java @@ -0,0 +1,30 @@ +package cn.code.nl.module.wms.task; + +import cn.code.nl.framework.execute.core.AbstractTask; +import cn.code.nl.framework.execute.core.dto.TaskExecuteDTO; +import cn.code.nl.module.wms.service.moveinv.MoveInvService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** 移库任务回调处理器,业务事务统一封装在移库 Service 中。 */ +@Slf4j +@Component("MOVETASK") +public class MoveTask extends AbstractTask { + + @Resource + private MoveInvService moveInvService; + + @Override + public void doHandleFinish(TaskExecuteDTO taskExecuteDTO) { + moveInvService.completeMoveTask( + taskExecuteDTO.getTaskId(), taskExecuteDTO.getOperatorId()); + } + + @Override + public void doHandleCancel(TaskExecuteDTO taskExecuteDTO) { + moveInvService.cancelMoveTask(taskExecuteDTO.getTaskId()); + log.info("移库任务取消完成,已清空移库明细任务ID, taskId={}", + taskExecuteDTO.getTaskId()); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/OutTask.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/OutTask.java new file mode 100644 index 00000000..94fbd491 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/task/OutTask.java @@ -0,0 +1,32 @@ +package cn.code.nl.module.wms.task; + +import cn.code.nl.framework.execute.core.AbstractTask; +import cn.code.nl.framework.execute.core.dto.TaskExecuteDTO; +import cn.code.nl.module.wms.service.iostorinv.IostorInvService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 出库任务回调处理器。 + * handleCode 必须与创建任务时写入的 OUTTASK 完全一致。 + */ +@Slf4j +@Component("OUTTASK") +public class OutTask extends AbstractTask { + + @Resource + private IostorInvService iostorInvService; + + @Override + public void doHandleFinish(TaskExecuteDTO taskExecuteDTO) { + iostorInvService.completeOutboundTask( + taskExecuteDTO.getTaskId(), taskExecuteDTO.getOperatorId()); + } + + @Override + public void doHandleCancel(TaskExecuteDTO taskExecuteDTO) { + iostorInvService.cancelOutboundTask(taskExecuteDTO.getTaskId()); + log.info("出库任务取消完成,已清空分配记录起点和任务ID, taskId={}", taskExecuteDTO.getTaskId()); + } +} diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdis/IostorinvDisMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdis/IostorinvDisMapper.xml index bb630d92..2c1d0d18 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdis/IostorinvDisMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdis/IostorinvDisMapper.xml @@ -34,4 +34,49 @@ ORDER BY dis.seq_no, dis.iostorinvdis_id + + DELETE FROM wms_iostorinvdis + WHERE iostorinvdis_id = #{allocationId} + + + + DELETE FROM wms_iostorinvdis + WHERE iostorinv_id = #{iostorinvId} + + + + UPDATE wms_iostorinvdis + SET work_status = '99', + real_qty = plan_qty, + is_issued = '1' + WHERE task_id = #{taskId} + AND deleted = '0' + AND work_status <> '99' + + + + UPDATE wms_iostorinvdis + SET point_code = NULL, + task_id = NULL + WHERE task_id = #{taskId} + AND deleted = '0' + AND work_status = '10' + + + + UPDATE wms_iostorinvdis + SET point_code = NULL, + task_id = NULL + WHERE task_id = #{taskId} + AND deleted = '0' + + + + diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml index b2738995..5be12c22 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/moveinvdtl/MoveInvDtlMapper.xml @@ -14,4 +14,17 @@ DELETE FROM wms_moveinvdtl WHERE moveinv_id = #{moveinvId} + + + + UPDATE wms_moveinvdtl + SET task_id = NULL + WHERE task_id = #{taskId} + + diff --git a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml index 2713b26f..4cea95d4 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml +++ b/nl-module-wms/nl-module-wms-server/src/main/resources/mapper/structAttr/StrucAttrMapper.xml @@ -104,4 +104,253 @@ AND inv_id = #{invId} AND lock_type IN ('4', '5') + + + + + + + + + + UPDATE wms_structattr + SET lock_type = '6', task_code = #{taskId} + WHERE struct_id = #{structId} + AND deleted = '0' + AND is_used = 1 + AND lock_type = '1' + AND (storagevehicle_code IS NULL OR storagevehicle_code = '') + + + + UPDATE wms_structattr + SET lock_type = '7', task_code = #{taskId} + WHERE struct_id = #{structId} + AND deleted = '0' + AND is_used = 1 + AND lock_type = '1' + AND storagevehicle_code IS NOT NULL + AND storagevehicle_code != '' + AND NOT EXISTS (SELECT 1 FROM wms_group_plate gp + WHERE gp.deleted = '0' + AND gp.vehicle_code = wms_structattr.storagevehicle_code) + + + + UPDATE wms_structattr + SET task_code = #{taskId} + WHERE struct_id = #{structId} + AND deleted = '0' + AND lock_type = #{lockType} + AND task_code = #{reservationCode} + + + + UPDATE wms_structattr + SET storagevehicle_code = 'EMPTY_VEHICLE', storagevehicle_qty = 1, + lock_type = '1', task_code = NULL + WHERE task_code = #{taskId} AND deleted = '0' AND lock_type = '6' + + + + UPDATE wms_structattr + SET storagevehicle_code = NULL, storagevehicle_qty = 0, + lock_type = '1', task_code = NULL + WHERE task_code = #{taskId} AND deleted = '0' AND lock_type = '7' + + + + UPDATE wms_structattr + SET lock_type = '1', task_code = NULL + WHERE task_code = #{taskId} AND deleted = '0' AND lock_type IN ('6', '7') + + + + UPDATE wms_structattr + SET storagevehicle_code = NULL, + storagevehicle_qty = 0, + lock_type = '1', + inv_type = NULL, + inv_id = NULL, + inv_code = NULL, + task_code = NULL + WHERE deleted = '0' + AND struct_code = #{structCode} + AND inv_id = #{invId} + AND lock_type = '4' + AND storagevehicle_code = #{vehicleCode} + + + + UPDATE wms_structattr + SET storagevehicle_code = #{vehicleCode}, + storagevehicle_qty = 1, + lock_type = '1', + inv_type = NULL, + inv_id = NULL, + inv_code = NULL, + task_code = NULL + WHERE deleted = '0' + AND struct_code = #{structCode} + AND inv_id = #{invId} + AND lock_type = '5' + AND (storagevehicle_code IS NULL OR storagevehicle_code = '') + + + + UPDATE wms_structattr + SET lock_type = '3', + inv_type = #{invType}, + inv_id = #{invId}, + inv_code = #{invCode} + WHERE struct_id = #{structId} + AND deleted = '0' + AND (lock_type IS NULL OR lock_type IN ('0', '1') + OR (lock_type = '3' AND inv_id = #{invId})) + + + + UPDATE wms_structattr + SET lock_type = '1', + inv_type = NULL, + inv_id = NULL, + inv_code = NULL + WHERE storagevehicle_code = #{vehicleCode} + AND deleted = '0' + AND lock_type = '3' + AND inv_id = #{invId} + + + + UPDATE wms_structattr + SET lock_type = '2', + task_code = #{taskCode} + WHERE struct_id = #{structId} + AND deleted = '0' + AND (lock_type IS NULL OR lock_type IN ('0', '1', '3')) + + + + UPDATE wms_structattr + SET lock_type = '2', + inv_type = '0', + inv_id = #{invId}, + inv_code = #{invCode} + WHERE struct_id = #{structId} + AND deleted = '0' + AND is_used = 1 + AND lock_type = '1' + AND (storagevehicle_code IS NULL OR storagevehicle_code = '') + + + + UPDATE wms_structattr + SET storagevehicle_code = #{vehicleCode}, + storagevehicle_qty = 1, + lock_type = '1', + inv_type = NULL, + inv_id = NULL, + inv_code = NULL, + task_code = NULL + WHERE struct_id = #{structId} + AND deleted = '0' + AND lock_type = '2' + AND inv_id = #{invId} + + + + UPDATE wms_structattr + SET lock_type = '1', + inv_type = NULL, + inv_id = NULL, + inv_code = NULL, + task_code = NULL + WHERE deleted = '0' + AND inv_id = #{invId} + AND lock_type = '2' + + + + UPDATE wms_structattr + SET lock_type = '1', + inv_type = NULL, + inv_id = NULL, + inv_code = NULL, + task_code = NULL + WHERE struct_id = #{structId} + AND deleted = '0' + AND inv_id = #{invId} + AND lock_type = '2' + + + + UPDATE wms_structattr + SET storagevehicle_code = NULL, + storagevehicle_qty = 0, + lock_type = '1', + task_code = NULL, + inv_type = NULL, + inv_id = NULL, + inv_code = NULL + WHERE struct_id = #{structId} + AND deleted = '0' + AND task_code = #{taskId} + + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts index d22913fb..194b3841 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts @@ -183,7 +183,7 @@ export function getInboundGroupPlates(storId: string) { export function createInbound(data: { billType: string; - bizDate: string; + bizDate: number; groupIds: string[]; remark?: string; storId: string; @@ -193,7 +193,7 @@ export function createInbound(data: { export function updateInbound(data: { billType: string; - bizDate: string; + bizDate: number; groupIds: string[]; iostorinvId: string; remark?: string; diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/index.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/index.vue index 70925c33..c72ac295 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/index.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/index.vue @@ -2,6 +2,8 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; +import dayjs from 'dayjs'; + import { Page, useVbenModal } from '@vben/common-ui'; import { DICT_TYPE } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; @@ -95,6 +97,10 @@ const [Grid, gridApi] = useVbenVxeGrid({ ...getRangePickerDefaultProps(), allowClear: true, }, + defaultValue: [ + dayjs().subtract(7, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'), + dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'), + ], fieldName: 'createTime', label: '创建日期', }, @@ -157,7 +163,13 @@ const [Grid, gridApi] = useVbenVxeGrid({ diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/modules/form.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/modules/form.vue index 05b92ea3..639448ad 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/modules/form.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/inbound/modules/form.vue @@ -26,6 +26,7 @@ const pickerLoading = ref(false); const pickerSelectedKeys = ref([]); const pickerPage = ref(1); const pickerPageSize = ref(10); +const remarkValue = ref(''); const search = reactive({ extCode: '', materialCode: '', @@ -112,29 +113,12 @@ const [Form, formApi] = useVbenForm({ }, { component: 'DatePicker', - componentProps: { - format: 'YYYY-MM-DD', - valueFormat: 'YYYY-MM-DD HH:mm:ss', - }, - defaultValue: dayjs().format('YYYY-MM-DD HH:mm:ss'), + componentProps: { class: 'w-full', format: 'YYYY-MM-DD' }, + defaultValue: dayjs(), fieldName: 'bizDate', label: '业务日期', rules: 'required', }, - { - component: 'Textarea', - componentProps: { - allowClear: true, - disabled: false, - maxlength: 1000, - placeholder: '请输入备注', - rows: 3, - showCount: true, - }, - fieldName: 'remark', - formItemClass: 'col-span-1 md:col-span-2 xl:col-span-4', - label: '备注', - }, ], showDefaultActions: false, wrapperClass: 'grid grid-cols-1 gap-x-6 gap-y-1 md:grid-cols-2 xl:grid-cols-4', @@ -208,19 +192,19 @@ async function save() { const { valid } = await formApi.validate(); if (!valid) return; if (selectedGroups.value.length === 0) return message.warning('请添加箱号物料'); + const values = await formApi.getValues(); + const normalizedBizDate = dayjs(values.bizDate); + if (!normalizedBizDate.isValid() || normalizedBizDate.year() < 2000) { + return message.warning('业务日期无效,请重新选择'); + } modalApi.lock(); try { - const values = await formApi.getValues(); - const rawBizDate = values.bizDate as number | string; - // 日期组件在不同适配器下可能返回字符串、毫秒或秒级时间戳,统一转换后再提交。 - const normalizedBizDate = typeof rawBizDate === 'number' && rawBizDate < 1_000_000_000_000 - ? dayjs(rawBizDate * 1000) - : dayjs(rawBizDate); const payload = { billType: String(values.billType), - bizDate: normalizedBizDate.startOf('day').format('YYYY-MM-DD HH:mm:ss'), + // 后端 LocalDateTime 的 JSON 反序列化器按毫秒时间戳接收,需与出库保存保持一致。 + bizDate: normalizedBizDate.startOf('day').valueOf(), groupIds: selectedGroups.value.map((item) => String(item.groupId)), - remark: values.remark ? String(values.remark) : undefined, + remark: remarkValue.value || undefined, storId: String(values.storId), }; if (editData.value?.iostorinvId) { @@ -244,11 +228,16 @@ const [ModalComponent, modalApi] = useVbenModal({ allGroups.value = []; selectedGroups.value = []; pickerSelectedKeys.value = []; + remarkValue.value = ''; resetSearch(); await formApi.resetForm(); if (editData.value?.iostorinvId) { const data = await getIostorInv(editData.value.iostorinvId); - await formApi.setValues(data); + await formApi.setValues({ + ...data, + bizDate: data.bizDate ? dayjs(data.bizDate) : dayjs(), + }); + remarkValue.value = data.remark || ''; selectedGroups.value = (data.details || []).map((item) => ({ extCode: item.sourceBillCode, groupId: String(item.sourceBilldtlId), @@ -283,6 +272,17 @@ const [ModalComponent, modalApi] = useVbenModal({
+
+ 备注 + +
@@ -394,6 +394,20 @@ const [ModalComponent, modalApi] = useVbenModal({ box-shadow: 0 1px 3px rgb(15 23 42 / 4%); } +.remark-row { + display: grid; + grid-template-columns: 86px minmax(0, 1fr); + align-items: start; + padding: 0 24px 18px; +} + +.remark-label { + padding-top: 5px; + padding-right: 12px; + color: #606a78; + text-align: right; +} + .detail-card { margin-top: 16px; } diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts index c7bc02be..1e00c510 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/data.ts @@ -2,6 +2,8 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { WmsIostorInvApi } from '#/api/wms/iostorinv'; +import dayjs from 'dayjs'; + import { getRangePickerDefaultProps } from '#/utils'; import { getBsrealStorAttrSimpleList } from '#/api/wms/bsrealstorattr'; import {getDictOptions} from "@vben/hooks"; @@ -309,6 +311,10 @@ export function useGridFormSchema(): VbenFormSchema[] { fieldName: 'createTime', label: '创建时间', component: 'RangePicker', + defaultValue: [ + dayjs().subtract(7, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'), + dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'), + ], componentProps: { ...getRangePickerDefaultProps(), allowClear: true, diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/data.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/data.ts index 9942c34d..686af86c 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/data.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/moveinv/data.ts @@ -1,10 +1,13 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import dayjs from 'dayjs'; + import { DICT_TYPE } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { getBsrealStorAttrSimpleList } from '#/api/wms/bsrealstorattr'; +import { getRangePickerDefaultProps } from '#/utils'; export function useGridFormSchema(): VbenFormSchema[] { return [ @@ -34,6 +37,19 @@ export function useGridFormSchema(): VbenFormSchema[] { fieldName: 'billStatus', label: '单据状态', }, + { + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + defaultValue: [ + dayjs().subtract(7, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'), + dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'), + ], + fieldName: 'createTime', + label: '创建时间', + }, ]; } diff --git a/sql/mysql/wms_empty_vehicle_indexes.sql b/sql/mysql/wms_empty_vehicle_indexes.sql new file mode 100644 index 00000000..db2ed70a --- /dev/null +++ b/sql/mysql/wms_empty_vehicle_indexes.sql @@ -0,0 +1,8 @@ +-- 空载具按排找位及组盘存在性判断索引(执行前请确认同名索引尚未创建)。 +CREATE INDEX idx_struct_empty_vehicle_row + ON wms_structattr + (deleted, is_used, stor_id, sect_id, block_num, row_num, layer_num, + lock_type, storagevehicle_code, col_num); + +CREATE INDEX idx_group_plate_vehicle_deleted + ON wms_group_plate (vehicle_code, deleted); diff --git a/sql/mysql/wms_empty_vehicle_lock_type.sql b/sql/mysql/wms_empty_vehicle_lock_type.sql new file mode 100644 index 00000000..d7422399 --- /dev/null +++ b/sql/mysql/wms_empty_vehicle_lock_type.sql @@ -0,0 +1,10 @@ +-- 空载具出入库仓位锁类型 +INSERT INTO system_dict_data + (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) +SELECT 2080566516439883783, 6, '空载具入库锁', '6', 'wms_lock_type', 0, 'success', '', '', '1', NOW(), '1', NOW(), b'0' +WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'wms_lock_type' AND value = '6' AND deleted = b'0'); + +INSERT INTO system_dict_data + (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) +SELECT 2080566516439883784, 7, '空载具出库锁', '7', 'wms_lock_type', 0, 'warning', '', '', '1', NOW(), '1', NOW(), b'0' +WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'wms_lock_type' AND value = '7' AND deleted = b'0'); diff --git a/sql/mysql/wms_structattr_layout_sample.sql b/sql/mysql/wms_structattr_layout_sample.sql new file mode 100644 index 00000000..bd04131e --- /dev/null +++ b/sql/mysql/wms_structattr_layout_sample.sql @@ -0,0 +1,74 @@ +-- 货架布局初始化样例(仅 3 排,每排 2 个纵向位置,每个位置 3 层,共 18 个仓位)。 +-- 编码:块(2)-排(3)-列(2)-层(1),例如 B01-R002-C02-L3。 +-- placement_type:01 双通、02 左通、03 右通。 +-- 图纸映射样例:最上方排=左通,中间排=双通,最下方排=右通。 +-- 当前仅生成 SQL 文件供确认,未执行数据库插入。 + +INSERT INTO wms_structattr + (struct_id, struct_code, struct_name, simple_name, + sect_id, sect_code, sect_name, + stor_id, stor_code, stor_name, stor_type, + is_tempstruct, row_num, col_num, layer_num, block_num, placement_type, + creator, create_time, updater, update_time, deleted, is_used, + storagevehicle_code, storagevehicle_type, storagevehicle_qty, + lock_type, task_code, inv_type, inv_id, inv_code, remark) +VALUES +-- 001排:左侧有轨道,只允许从左侧存取。 +('B01-R001-C01-L1', 'B01-R001-C01-L1', '主存区-01块-001排-01列-1层', 'B01-R001-C01-L1', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 1, 1, 1, 1, '02', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-左通'), +('B01-R001-C01-L2', 'B01-R001-C01-L2', '主存区-01块-001排-01列-2层', 'B01-R001-C01-L2', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 1, 1, 2, 1, '02', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-左通'), +('B01-R001-C01-L3', 'B01-R001-C01-L3', '主存区-01块-001排-01列-3层', 'B01-R001-C01-L3', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 1, 1, 3, 1, '02', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-左通'), +('B01-R001-C02-L1', 'B01-R001-C02-L1', '主存区-01块-001排-02列-1层', 'B01-R001-C02-L1', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 1, 2, 1, 1, '02', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-左通'), +('B01-R001-C02-L2', 'B01-R001-C02-L2', '主存区-01块-001排-02列-2层', 'B01-R001-C02-L2', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 1, 2, 2, 1, '02', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-左通'), +('B01-R001-C02-L3', 'B01-R001-C02-L3', '主存区-01块-001排-02列-3层', 'B01-R001-C02-L3', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 1, 2, 3, 1, '02', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-左通'), + +-- 002排:两侧均有轨道,可以从两侧存取。 +('B01-R002-C01-L1', 'B01-R002-C01-L1', '主存区-01块-002排-01列-1层', 'B01-R002-C01-L1', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 2, 1, 1, 1, '01', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-双通'), +('B01-R002-C01-L2', 'B01-R002-C01-L2', '主存区-01块-002排-01列-2层', 'B01-R002-C01-L2', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 2, 1, 2, 1, '01', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-双通'), +('B01-R002-C01-L3', 'B01-R002-C01-L3', '主存区-01块-002排-01列-3层', 'B01-R002-C01-L3', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 2, 1, 3, 1, '01', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-双通'), +('B01-R002-C02-L1', 'B01-R002-C02-L1', '主存区-01块-002排-02列-1层', 'B01-R002-C02-L1', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 2, 2, 1, 1, '01', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-双通'), +('B01-R002-C02-L2', 'B01-R002-C02-L2', '主存区-01块-002排-02列-2层', 'B01-R002-C02-L2', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 2, 2, 2, 1, '01', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-双通'), +('B01-R002-C02-L3', 'B01-R002-C02-L3', '主存区-01块-002排-02列-3层', 'B01-R002-C02-L3', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 2, 2, 3, 1, '01', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-双通'), + +-- 003排:右侧有轨道,只允许从右侧存取。 +('B01-R003-C01-L1', 'B01-R003-C01-L1', '主存区-01块-003排-01列-1层', 'B01-R003-C01-L1', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 3, 1, 1, 1, '03', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-右通'), +('B01-R003-C01-L2', 'B01-R003-C01-L2', '主存区-01块-003排-01列-2层', 'B01-R003-C01-L2', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 3, 1, 2, 1, '03', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-右通'), +('B01-R003-C01-L3', 'B01-R003-C01-L3', '主存区-01块-003排-01列-3层', 'B01-R003-C01-L3', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 3, 1, 3, 1, '03', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-右通'), +('B01-R003-C02-L1', 'B01-R003-C02-L1', '主存区-01块-003排-02列-1层', 'B01-R003-C02-L1', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 3, 2, 1, 1, '03', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-右通'), +('B01-R003-C02-L2', 'B01-R003-C02-L2', '主存区-01块-003排-02列-2层', 'B01-R003-C02-L2', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 3, 2, 2, 1, '03', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-右通'), +('B01-R003-C02-L3', 'B01-R003-C02-L3', '主存区-01块-003排-02列-3层', 'B01-R003-C02-L3', + '2078406241102450689', 'ZC01', '主存区', '2078405279260463106', 'AC01', '遂宁成品仓', NULL, + '0', 3, 2, 3, 1, '03', '1', NOW(), '1', NOW(), '0', 1, NULL, NULL, 0, '1', NULL, NULL, NULL, NULL, '布局样例-右通');