diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java b/ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java index e484821..606cf25 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java @@ -14,6 +14,7 @@ import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.in.job.domain.MydoJob; import com.ruoyi.in.job.service.IMydoJobService; +import com.ruoyi.out.relocation.service.IRelocationJobService; import com.ruoyi.pda.service.PdaService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.BeanUtils; @@ -37,6 +38,7 @@ public class ExternalApiController extends BaseController { @Autowired private MesClient mesClient; @Autowired private WcsClient wcsClient; @Autowired private IMydoJobService mydoJobService; + @Autowired private IRelocationJobService relocationJobService; @Autowired private PdaService pdaService; // ═══════════════ ERP ═══════════════ @@ -353,11 +355,20 @@ public class ExternalApiController extends BaseController { job = mydoJobService.selectLatestOutMydoJobByTrayCodeAndWarehouseId(request.getTrayCode().trim(), request.getWarehouseId().trim()); } if (job == null) { + if (request != null && request.getJobId() != null && !request.getJobId().trim().isEmpty()) { + AjaxResult relocationResult = relocationJobService.completeFromWcs(request.getJobId().trim()); + if (relocationResult != null && relocationResult.isSuccess()) { + return wcsResult(relocationResult); + } + } return wcsError("未找到对应出库作业"); } if (!isExpectedJobType(job, false)) { return wcsError("非法出库作业"); } + if (Long.valueOf(23L).equals(job.getInOrOut())) { + return wcsResult(relocationJobService.completeFromWcs(job.getJobId())); + } return wcsResult(mydoJobService.completeOutJobFromWcs(job.getJobId())); } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/controller/RelocationJobController.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/controller/RelocationJobController.java new file mode 100644 index 0000000..24f717b --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/controller/RelocationJobController.java @@ -0,0 +1,40 @@ +package com.ruoyi.out.relocation.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.out.relocation.domain.RelocationGenerateRequest; +import com.ruoyi.out.relocation.service.IRelocationJobService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 密集库移库作业接口。 + */ +@RestController +@RequestMapping("/out/relocation") +public class RelocationJobController extends BaseController +{ + private final IRelocationJobService relocationJobService; + + public RelocationJobController(IRelocationJobService relocationJobService) + { + this.relocationJobService = relocationJobService; + } + + @PreAuthorize("@ss.hasPermi('out:relocation:preview')") + @PostMapping("/preview") + public AjaxResult preview(@RequestBody RelocationGenerateRequest request) + { + return success(relocationJobService.preview(request)); + } + + @PreAuthorize("@ss.hasPermi('out:relocation:generate')") + @PostMapping("/generate") + public AjaxResult generate(@RequestBody RelocationGenerateRequest request) + { + return success(relocationJobService.generate(request)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/domain/RelocationGenerateRequest.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/domain/RelocationGenerateRequest.java new file mode 100644 index 0000000..10b9e7c --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/domain/RelocationGenerateRequest.java @@ -0,0 +1,24 @@ +package com.ruoyi.out.relocation.domain; + +/** + * 密集库指定移库请求。 + */ +public class RelocationGenerateRequest +{ + private String warehouseId; + private Long theFloor; + private String sourceGroupCoding; + private String targetGroupCoding; + private Integer trayCount; + + public String getWarehouseId() { return warehouseId; } + public void setWarehouseId(String warehouseId) { this.warehouseId = warehouseId; } + public Long getTheFloor() { return theFloor; } + public void setTheFloor(Long theFloor) { this.theFloor = theFloor; } + public String getSourceGroupCoding() { return sourceGroupCoding; } + public void setSourceGroupCoding(String sourceGroupCoding) { this.sourceGroupCoding = sourceGroupCoding; } + public String getTargetGroupCoding() { return targetGroupCoding; } + public void setTargetGroupCoding(String targetGroupCoding) { this.targetGroupCoding = targetGroupCoding; } + public Integer getTrayCount() { return trayCount; } + public void setTrayCount(Integer trayCount) { this.trayCount = trayCount; } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/domain/RelocationPreview.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/domain/RelocationPreview.java new file mode 100644 index 0000000..541a958 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/domain/RelocationPreview.java @@ -0,0 +1,20 @@ +package com.ruoyi.out.relocation.domain; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import com.ruoyi.out.dense.domain.DenseLanePosition; + +/** + * 指定移库外侧托盘预览。 + */ +public class RelocationPreview +{ + private List trays = new ArrayList<>(); + private BigDecimal totalQuantity = BigDecimal.ZERO; + + public List getTrays() { return trays; } + public void setTrays(List trays) { this.trays = trays; } + public BigDecimal getTotalQuantity() { return totalQuantity; } + public void setTotalQuantity(BigDecimal totalQuantity) { this.totalQuantity = totalQuantity; } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/mapper/RelocationJobMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/mapper/RelocationJobMapper.java new file mode 100644 index 0000000..41d49c9 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/mapper/RelocationJobMapper.java @@ -0,0 +1,51 @@ +package com.ruoyi.out.relocation.mapper; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import com.ruoyi.in.job.domain.MydoJob; +import com.ruoyi.in.jobdetail.domain.MydoJobDetail; +import com.ruoyi.out.dense.domain.DenseLanePosition; +import com.ruoyi.out.relocation.domain.RelocationGenerateRequest; +import org.apache.ibatis.annotations.Param; + +/** + * 密集库移库作业 Mapper。 + */ +public interface RelocationJobMapper +{ + MydoJob selectJobForUpdate(@Param("jobId") String jobId); + + List selectStockDetailsForUpdate(@Param("cargoSpaceId") String cargoSpaceId, + @Param("trayCode") String trayCode); + + List selectRelocationSourcesForUpdate(RelocationGenerateRequest request); + + BigDecimal sumTrayStockQuantity(@Param("trayCodes") String[] trayCodes); + + int reserveTarget(Map params); + + int reserveSource(Map params); + + int insertRelocationJob(MydoJob job); + + int insertRelocationDetail(MydoJobDetail detail); + + int lockRelocationState(MydoJob job); + + int moveTrayStock(MydoJob job); + + int completeCargoSpaces(MydoJob job); + + int archiveRelocationDetails(@Param("jobId") String jobId); + + int archiveRelocationJob(@Param("jobId") String jobId); + + int deleteRelocationDetails(@Param("jobId") String jobId); + + int deleteRelocationJob(@Param("jobId") String jobId); + + Integer isFinishedRelocation(@Param("jobId") String jobId); + + String selectNextSourceLaneJob(@Param("cargoSpaceId") String cargoSpaceId); +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/IRelocationJobService.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/IRelocationJobService.java new file mode 100644 index 0000000..77a07f4 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/IRelocationJobService.java @@ -0,0 +1,20 @@ +package com.ruoyi.out.relocation.service; + +import java.util.List; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.out.relocation.domain.RelocationGenerateRequest; +import com.ruoyi.out.relocation.domain.RelocationPreview; + +/** + * 密集库移库作业服务。 + */ +public interface IRelocationJobService +{ + List createBlockerRelocations(String blockedJobId); + + RelocationPreview preview(RelocationGenerateRequest request); + + List generate(RelocationGenerateRequest request); + + AjaxResult completeFromWcs(String jobId); +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImpl.java new file mode 100644 index 0000000..477b942 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImpl.java @@ -0,0 +1,299 @@ +package com.ruoyi.out.relocation.service.impl; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import com.ruoyi.bas.cargospace.domain.BasCargoSpace; +import com.ruoyi.bas.id.job.service.JobIdService; +import com.ruoyi.bas.id.taskno.service.TaskNoService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.in.job.domain.MydoJob; +import com.ruoyi.in.jobdetail.domain.MydoJobDetail; +import com.ruoyi.out.dense.domain.DenseLanePosition; +import com.ruoyi.out.dense.domain.RelocationTargetQuery; +import com.ruoyi.out.dense.service.IDenseLaneService; +import com.ruoyi.out.dispatch.domain.DispatchTrigger; +import com.ruoyi.out.dispatch.service.IOutboundDispatchService; +import com.ruoyi.out.relocation.domain.RelocationGenerateRequest; +import com.ruoyi.out.relocation.domain.RelocationPreview; +import com.ruoyi.out.relocation.mapper.RelocationJobMapper; +import com.ruoyi.out.relocation.service.IRelocationJobService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * 密集库移库作业服务实现。 + */ +@Service +public class RelocationJobServiceImpl implements IRelocationJobService +{ + private final RelocationJobMapper mapper; + private final IDenseLaneService denseLaneService; + private final JobIdService jobIdService; + private final TaskNoService taskNoService; + private final IOutboundDispatchService dispatchService; + + public RelocationJobServiceImpl(RelocationJobMapper mapper, + IDenseLaneService denseLaneService, + JobIdService jobIdService, + TaskNoService taskNoService, + IOutboundDispatchService dispatchService) + { + this.mapper = mapper; + this.denseLaneService = denseLaneService; + this.jobIdService = jobIdService; + this.taskNoService = taskNoService; + this.dispatchService = dispatchService; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List createBlockerRelocations(String blockedJobId) + { + if (!StringUtils.hasText(blockedJobId)) { + throw new RuntimeException("被遮挡作业ID不能为空"); + } + MydoJob blockedJob = mapper.selectJobForUpdate(blockedJobId.trim()); + if (blockedJob == null) { + throw new RuntimeException("被遮挡出库作业不存在"); + } + List blockers = denseLaneService.selectBlockersForUpdate( + blockedJob.getCargoSpaceId()); + return createRelocations(blockers, blockedJob.getWarehouseName(), null); + } + + @Override + @Transactional(readOnly = true) + public RelocationPreview preview(RelocationGenerateRequest request) + { + validateRequest(request); + List candidates = mapper.selectRelocationSourcesForUpdate(request); + if (candidates == null || candidates.size() < request.getTrayCount()) { + throw new RuntimeException("源通道可移库托盘数量不足"); + } + List selected = denseLaneService.orderOutermostFirst(candidates) + .stream().limit(request.getTrayCount()).collect(Collectors.toList()); + RelocationPreview preview = new RelocationPreview(); + preview.setTrays(selected); + String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new); + BigDecimal total = mapper.sumTrayStockQuantity(trayCodes); + preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total); + return preview; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List generate(RelocationGenerateRequest request) + { + validateRequest(request); + List candidates = mapper.selectRelocationSourcesForUpdate(request); + if (candidates == null || candidates.size() < request.getTrayCount()) { + throw new RuntimeException("源通道可移库托盘数量不足"); + } + List selected = denseLaneService.orderOutermostFirst(candidates) + .stream().limit(request.getTrayCount()).collect(Collectors.toList()); + return createRelocations(selected, null, request.getTargetGroupCoding()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AjaxResult completeFromWcs(String jobId) + { + if (!StringUtils.hasText(jobId)) { + return AjaxResult.error("移库作业ID不能为空"); + } + MydoJob job = mapper.selectJobForUpdate(jobId.trim()); + if (job == null) { + if (Integer.valueOf(1).equals(mapper.isFinishedRelocation(jobId.trim()))) { + return AjaxResult.success("移库作业已完成"); + } + return AjaxResult.error("移库作业不存在"); + } + if (!Long.valueOf(23L).equals(job.getInOrOut())) { + return AjaxResult.error("当前作业不是移库作业"); + } + if (!"3".equals(job.getStatus()) || !"1".equals(job.getExecutionPhase())) { + return AjaxResult.error("移库作业尚未成功下发WCS"); + } + if (mapper.lockRelocationState(job) != 1) { + throw new RuntimeException("移库源货位、目标货位或托盘状态已变化"); + } + if (mapper.moveTrayStock(job) <= 0) { + throw new RuntimeException("移库托盘库存不存在"); + } + if (mapper.completeCargoSpaces(job) != 2) { + throw new RuntimeException("移库货位状态更新失败"); + } + mapper.archiveRelocationDetails(job.getJobId()); + if (mapper.archiveRelocationJob(job.getJobId()) != 1) { + throw new RuntimeException("移库作业归档失败"); + } + mapper.deleteRelocationDetails(job.getJobId()); + if (mapper.deleteRelocationJob(job.getJobId()) != 1) { + throw new RuntimeException("移库活动作业删除失败"); + } + afterCommit(() -> dispatchNextSourceLaneJob(job.getCargoSpaceId())); + return AjaxResult.success("移库作业完成"); + } + + private List createRelocations(List sources, + String warehouseName, + String targetGroupCoding) + { + if (sources == null || sources.isEmpty()) { + return new ArrayList<>(); + } + List targets = new ArrayList<>(); + for (DenseLanePosition source : sources) { + RelocationTargetQuery query = new RelocationTargetQuery(); + query.setWarehouseId(source.getWarehouseId()); + query.setTheFloor(source.getTheFloor()); + query.setSourceGroupCoding(source.getGroupCoding()); + query.setTargetGroupCoding(targetGroupCoding); + query.setTrayCode(source.getTrayCode()); + BasCargoSpace target = denseLaneService.selectRelocationTargetForUpdate(query); + if (target == null) { + throw new RuntimeException("同层可用移库货位不足"); + } + Map reserve = new HashMap<>(); + reserve.put("cargoSpaceId", target.getCargoSpaceId()); + reserve.put("trayCode", source.getTrayCode()); + if (mapper.reserveTarget(reserve) != 1) { + throw new RuntimeException("移库目标货位已被占用"); + } + targets.add(target); + } + + List jobIds = new ArrayList<>(); + for (int index = 0; index < sources.size(); index++) { + DenseLanePosition source = sources.get(index); + BasCargoSpace target = targets.get(index); + Map sourceReserve = new HashMap<>(); + sourceReserve.put("cargoSpaceId", source.getCargoSpaceId()); + sourceReserve.put("trayCode", source.getTrayCode()); + if (mapper.reserveSource(sourceReserve) != 1) { + throw new RuntimeException("移库源货位状态已变化"); + } + List stockDetails = mapper.selectStockDetailsForUpdate( + source.getCargoSpaceId(), source.getTrayCode()); + if (stockDetails == null || stockDetails.isEmpty()) { + throw new RuntimeException("移库托盘没有库存明细"); + } + String jobId = jobIdService.getId("CK"); + if (!StringUtils.hasText(jobId)) { + throw new RuntimeException("移库作业ID生成失败"); + } + MydoJob job = buildJob(jobId, taskNoService.getTaskNo("23"), source, + target, warehouseName); + if (mapper.insertRelocationJob(job) != 1) { + throw new RuntimeException("移库作业生成失败"); + } + int detailIndex = 1; + for (MydoJobDetail template : stockDetails) { + MydoJobDetail detail = copyDetail(template, jobId, detailIndex++); + if (mapper.insertRelocationDetail(detail) != 1) { + throw new RuntimeException("移库作业明细生成失败"); + } + } + jobIds.add(jobId); + } + afterCommit(() -> jobIds.forEach(id -> dispatchService.dispatch(id, DispatchTrigger.INITIAL))); + return jobIds; + } + + private MydoJob buildJob(String jobId, String taskNo, DenseLanePosition source, + BasCargoSpace target, String warehouseName) + { + MydoJob job = new MydoJob(); + job.setJobId(jobId); + job.setTaskNo(StringUtils.hasText(taskNo) ? taskNo : jobId); + job.setInOrOut(23L); + job.setStatus("1"); + job.setExecutionPhase("0"); + job.setTrayCode(source.getTrayCode()); + job.setWarehouseId(source.getWarehouseId()); + job.setWarehouseName(warehouseName); + job.setCargoSpaceId(source.getCargoSpaceId()); + job.setCargoSpaceName(source.getCargoSpaceName()); + job.setTargetCargoSpaceId(target.getCargoSpaceId()); + job.setTargetCargoSpaceName(target.getCargoSpaceName()); + job.setLineType("1"); + job.setPriority(10L); + job.setCreateTime(DateUtils.getNowDate()); + return job; + } + + private MydoJobDetail copyDetail(MydoJobDetail source, String jobId, int index) + { + MydoJobDetail detail = new MydoJobDetail(); + detail.setJobDetailId(jobId + "-" + index); + detail.setJobId(jobId); + detail.setStockId(source.getStockId()); + detail.setLotNo(source.getLotNo()); + detail.setInDate(source.getInDate()); + detail.setInOrOutId(source.getInOrOutId()); + detail.setInOrOutDId(source.getInOrOutDId()); + detail.setProductId(source.getProductId()); + detail.setProductName(source.getProductName()); + detail.setStockQuantity(source.getStockQuantity()); + detail.setStockinId(source.getStockinId()); + detail.setStockinDetailId(source.getStockinDetailId()); + detail.setCustomerId(source.getCustomerId()); + detail.setCustomerName(source.getCustomerName()); + detail.setSupplierId(source.getSupplierId()); + detail.setSupplierName(source.getSupplierName()); + detail.setProjectNo(source.getProjectNo()); + return detail; + } + + private void validateRequest(RelocationGenerateRequest request) + { + if (request == null || !"03".equals(request.getWarehouseId())) { + throw new RuntimeException("指定移库仅支持03仓密集库"); + } + if (request.getTheFloor() == null) { + throw new RuntimeException("请选择移库层"); + } + if (!StringUtils.hasText(request.getSourceGroupCoding()) + || !StringUtils.hasText(request.getTargetGroupCoding())) { + throw new RuntimeException("请选择源通道和目标通道"); + } + if (request.getSourceGroupCoding().equals(request.getTargetGroupCoding())) { + throw new RuntimeException("移库目标必须是同层不同通道"); + } + if (request.getTrayCount() == null || request.getTrayCount() <= 0) { + throw new RuntimeException("移库托盘数量必须大于0"); + } + } + + private void dispatchNextSourceLaneJob(String sourceCargoSpaceId) + { + String nextJobId = mapper.selectNextSourceLaneJob(sourceCargoSpaceId); + if (StringUtils.hasText(nextJobId)) { + dispatchService.dispatch(nextJobId, DispatchTrigger.INITIAL); + } + } + + private void afterCommit(Runnable action) + { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() + { + @Override + public void afterCommit() + { + action.run(); + } + }); + } else { + action.run(); + } + } +} diff --git a/ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml b/ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml new file mode 100644 index 0000000..66ff549 --- /dev/null +++ b/ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + update bas_cargo_space + set status = '3', tray_code = #{trayCode} + where cargo_space_id = #{cargoSpaceId} + and status = '1' + and isnull(locked, 'N') = 'N' + and tray_code is null + + + + update bas_cargo_space + set status = '4' + where cargo_space_id = #{cargoSpaceId} + and status in ('2', '3') + and tray_code = #{trayCode} + + + + insert into mydo_job + (job_id, status, task_no, in_or_out, execution_phase, tray_code, + warehouse_id, warehouse_name, cargo_space_id, cargo_space_name, + target_cargo_space_id, target_cargo_space_name, line_type, priority, create_time) + values + (#{jobId}, #{status}, #{taskNo}, #{inOrOut}, #{executionPhase}, #{trayCode}, + #{warehouseId}, #{warehouseName}, #{cargoSpaceId}, #{cargoSpaceName}, + #{targetCargoSpaceId}, #{targetCargoSpaceName}, #{lineType}, #{priority}, #{createTime}) + + + + insert into mydo_job_detail + (job_detail_id, job_id, stock_id, lot_no, in_date, in_or_out_id, + in_or_out_d_id, product_id, product_name, stock_quantity, stockin_id, + stockin_detail_id, customer_id, customer_name, supplier_id, supplier_name, project_no) + values + (#{jobDetailId}, #{jobId}, #{stockId}, #{lotNo}, #{inDate}, #{inOrOutId}, + #{inOrOutDId}, #{productId}, #{productName}, #{stockQuantity}, #{stockinId}, + #{stockinDetailId}, #{customerId}, #{customerName}, #{supplierId}, #{supplierName}, #{projectNo}) + + + + + + update bas_stock + set cargo_space_id = #{targetCargoSpaceId} + where cargo_space_id = #{cargoSpaceId} + and tray_code = #{trayCode} + + + + update bas_cargo_space + set status = case when cargo_space_id = #{cargoSpaceId} then '1' else '2' end, + tray_code = case when cargo_space_id = #{cargoSpaceId} then null else #{trayCode} end + where (cargo_space_id = #{cargoSpaceId} and status = '4' and tray_code = #{trayCode}) + or (cargo_space_id = #{targetCargoSpaceId} and status = '3' and tray_code = #{trayCode}) + + + + insert into mydo_job_detail_finish + (job_detail_id, job_id, stock_id, lot_no, in_date, in_or_out_id, + in_or_out_d_id, product_id, product_name, in_quantity, stock_quantity, + out_quantity, stockin_id, stockin_detail_id, customer_id, customer_name, + supplier_id, supplier_name, project_no) + select job_detail_id, job_id, stock_id, lot_no, in_date, in_or_out_id, + in_or_out_d_id, product_id, product_name, in_quantity, stock_quantity, + out_quantity, stockin_id, stockin_detail_id, customer_id, customer_name, + supplier_id, supplier_name, project_no + from mydo_job_detail with (UPDLOCK,HOLDLOCK) + where job_id = #{jobId} + + + + insert into mydo_job_finish + (job_id, status, task_no, in_or_out, execution_phase, tray_code, + warehouse_id, warehouse_name, roadway_id, cargo_space_id, cargo_space_name, + target_roadway_id, target_cargo_space_id, target_cargo_space_name, line_type, + priority, height, weight, create_time, start_time, finish_time, entrance_id, + exit_id, dept_id, dept_name, user_id, user_name, supplier_id, supplier_name, + customer_id, customer_name) + select job_id, '4', task_no, in_or_out, '5', tray_code, + warehouse_id, warehouse_name, roadway_id, cargo_space_id, cargo_space_name, + target_roadway_id, target_cargo_space_id, target_cargo_space_name, line_type, + priority, height, weight, create_time, start_time, getdate(), entrance_id, + exit_id, dept_id, dept_name, user_id, user_name, supplier_id, supplier_name, + customer_id, customer_name + from mydo_job with (UPDLOCK,HOLDLOCK) + where job_id = #{jobId} and in_or_out = 23 + + + + delete from mydo_job_detail where job_id = #{jobId} + + + + delete from mydo_job where job_id = #{jobId} and in_or_out = 23 + + + + + + diff --git a/ruoyi-admin/src/test/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImplTest.java new file mode 100644 index 0000000..8610523 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImplTest.java @@ -0,0 +1,212 @@ +package com.ruoyi.out.relocation.service.impl; + +import java.math.BigDecimal; +import java.util.List; +import com.ruoyi.bas.cargospace.domain.BasCargoSpace; +import com.ruoyi.bas.id.job.service.JobIdService; +import com.ruoyi.bas.id.taskno.service.TaskNoService; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.in.job.domain.MydoJob; +import com.ruoyi.in.jobdetail.domain.MydoJobDetail; +import com.ruoyi.out.dense.domain.DenseLanePosition; +import com.ruoyi.out.dense.domain.RelocationTargetQuery; +import com.ruoyi.out.dense.service.IDenseLaneService; +import com.ruoyi.out.dispatch.domain.DispatchTrigger; +import com.ruoyi.out.dispatch.service.IOutboundDispatchService; +import com.ruoyi.out.relocation.domain.RelocationGenerateRequest; +import com.ruoyi.out.relocation.mapper.RelocationJobMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class RelocationJobServiceImplTest +{ + private RelocationJobMapper mapper; + private IDenseLaneService denseLaneService; + private JobIdService jobIdService; + private TaskNoService taskNoService; + private IOutboundDispatchService dispatchService; + private RelocationJobServiceImpl service; + + @BeforeEach + void setUp() + { + mapper = mock(RelocationJobMapper.class); + denseLaneService = mock(IDenseLaneService.class); + jobIdService = mock(JobIdService.class); + taskNoService = mock(TaskNoService.class); + dispatchService = mock(IOutboundDispatchService.class); + service = new RelocationJobServiceImpl( + mapper, denseLaneService, jobIdService, taskNoService, dispatchService); + } + + @Test + void createsOneType23JobPerBlockerAndCopiesEveryStockDetail() + { + MydoJob blocked = blockedJob("OUT-1", "CS-3"); + DenseLanePosition outer = source("CS-5", "T-5"); + DenseLanePosition inner = source("CS-4", "T-4"); + when(mapper.selectJobForUpdate("OUT-1")).thenReturn(blocked); + when(denseLaneService.selectBlockersForUpdate("CS-3")).thenReturn(List.of(outer, inner)); + when(denseLaneService.selectRelocationTargetForUpdate(any(RelocationTargetQuery.class))) + .thenReturn(target("TG-1", "TARGET-1"), target("TG-2", "TARGET-2")); + when(mapper.reserveTarget(any())).thenReturn(1); + when(mapper.reserveSource(any())).thenReturn(1); + when(jobIdService.getId("CK")).thenReturn("MOVE-1", "MOVE-2"); + when(taskNoService.getTaskNo("23")).thenReturn("MOVE-TASK-1", "MOVE-TASK-2"); + when(mapper.selectStockDetailsForUpdate(anyString(), anyString())) + .thenReturn(List.of(detail("S-1", "P-1"), detail("S-2", "P-2"))); + when(mapper.insertRelocationJob(any(MydoJob.class))).thenReturn(1); + when(mapper.insertRelocationDetail(any(MydoJobDetail.class))).thenReturn(1); + when(dispatchService.dispatch(anyString(), any(DispatchTrigger.class))) + .thenReturn(AjaxResult.success()); + + List ids = service.createBlockerRelocations("OUT-1"); + + assertThat(ids).containsExactly("MOVE-1", "MOVE-2"); + ArgumentCaptor jobs = ArgumentCaptor.forClass(MydoJob.class); + verify(mapper, org.mockito.Mockito.times(2)).insertRelocationJob(jobs.capture()); + assertThat(jobs.getAllValues()).allSatisfy(job -> { + assertThat(job.getInOrOut()).isEqualTo(23L); + assertThat(job.getStatus()).isEqualTo("1"); + assertThat(job.getExecutionPhase()).isEqualTo("0"); + assertThat(job.getTargetCargoSpaceId()).isNotBlank(); + }); + verify(mapper, org.mockito.Mockito.times(4)).insertRelocationDetail(any(MydoJobDetail.class)); + verify(taskNoService, org.mockito.Mockito.times(2)).getTaskNo("23"); + verify(dispatchService).dispatch("MOVE-1", DispatchTrigger.INITIAL); + verify(dispatchService).dispatch("MOVE-2", DispatchTrigger.INITIAL); + } + + @Test + void reservesAllTargetsBeforeInsertingAnyRelocationJob() + { + MydoJob blocked = blockedJob("OUT-2", "CS-3"); + when(mapper.selectJobForUpdate("OUT-2")).thenReturn(blocked); + when(denseLaneService.selectBlockersForUpdate("CS-3")) + .thenReturn(List.of(source("CS-5", "T-5"), source("CS-4", "T-4"))); + when(denseLaneService.selectRelocationTargetForUpdate(any(RelocationTargetQuery.class))) + .thenReturn(target("TG-1", "TARGET-1"), (BasCargoSpace) null); + when(mapper.reserveTarget(any())).thenReturn(1); + + assertThatThrownBy(() -> service.createBlockerRelocations("OUT-2")) + .hasMessageContaining("同层可用移库货位不足"); + verify(mapper, never()).insertRelocationJob(any(MydoJob.class)); + } + + @Test + void rejectsManualRelocationToSameLaneOrDifferentFloor() + { + RelocationGenerateRequest sameLane = new RelocationGenerateRequest(); + sameLane.setWarehouseId("03"); + sameLane.setTheFloor(2L); + sameLane.setSourceGroupCoding("G-02"); + sameLane.setTargetGroupCoding("G-02"); + sameLane.setTrayCount(1); + + assertThatThrownBy(() -> service.generate(sameLane)) + .hasMessageContaining("不同通道"); + } + + @Test + void completionMovesInventorySwapsCargoSpacesArchivesAndWakesNextJob() + { + MydoJob move = blockedJob("MOVE-1", "SOURCE"); + move.setInOrOut(23L); + move.setStatus("3"); + move.setExecutionPhase("1"); + move.setTargetCargoSpaceId("TARGET"); + move.setTrayCode("T-1"); + when(mapper.selectJobForUpdate("MOVE-1")).thenReturn(move); + when(mapper.lockRelocationState(move)).thenReturn(1); + when(mapper.moveTrayStock(move)).thenReturn(2); + when(mapper.completeCargoSpaces(move)).thenReturn(2); + when(mapper.archiveRelocationDetails("MOVE-1")).thenReturn(2); + when(mapper.archiveRelocationJob("MOVE-1")).thenReturn(1); + when(mapper.deleteRelocationDetails("MOVE-1")).thenReturn(2); + when(mapper.deleteRelocationJob("MOVE-1")).thenReturn(1); + when(mapper.selectNextSourceLaneJob("SOURCE")).thenReturn("NEXT-1"); + when(dispatchService.dispatch("NEXT-1", DispatchTrigger.INITIAL)) + .thenReturn(AjaxResult.success()); + + AjaxResult result = service.completeFromWcs("MOVE-1"); + + assertThat(result.isSuccess()).isTrue(); + var ordered = inOrder(mapper); + ordered.verify(mapper).moveTrayStock(move); + ordered.verify(mapper).completeCargoSpaces(move); + ordered.verify(mapper).archiveRelocationDetails("MOVE-1"); + ordered.verify(mapper).archiveRelocationJob("MOVE-1"); + ordered.verify(mapper).deleteRelocationDetails("MOVE-1"); + ordered.verify(mapper).deleteRelocationJob("MOVE-1"); + verify(dispatchService).dispatch("NEXT-1", DispatchTrigger.INITIAL); + } + + @Test + void duplicateCompletionOfArchivedRelocationIsIdempotent() + { + when(mapper.selectJobForUpdate("MOVE-DONE")).thenReturn(null); + when(mapper.isFinishedRelocation("MOVE-DONE")).thenReturn(1); + + assertThat(service.completeFromWcs("MOVE-DONE").isSuccess()).isTrue(); + verify(mapper, never()).moveTrayStock(any(MydoJob.class)); + } + + private MydoJob blockedJob(String jobId, String cargoSpaceId) + { + MydoJob job = new MydoJob(); + job.setJobId(jobId); + job.setWarehouseId("03"); + job.setWarehouseName("三号仓"); + job.setCargoSpaceId(cargoSpaceId); + job.setStatus("1"); + job.setInOrOut(20L); + return job; + } + + private DenseLanePosition source(String cargoSpaceId, String trayCode) + { + DenseLanePosition position = new DenseLanePosition(); + position.setCargoSpaceId(cargoSpaceId); + position.setCargoSpaceName(cargoSpaceId); + position.setWarehouseId("03"); + position.setGroupCoding("SOURCE-GROUP"); + position.setTheFloor(1L); + position.setTrayCode(trayCode); + return position; + } + + private BasCargoSpace target(String group, String cargoSpaceId) + { + BasCargoSpace target = new BasCargoSpace(); + target.setCargoSpaceId(cargoSpaceId); + target.setCargoSpaceName(cargoSpaceId); + target.setWarehouseId("03"); + target.setGroupCoding(group); + target.setTheFloor(1L); + return target; + } + + private MydoJobDetail detail(String stockId, String productId) + { + MydoJobDetail detail = new MydoJobDetail(); + detail.setStockId(stockId); + detail.setStockinDetailId("DETAIL-" + stockId); + detail.setStockinId("IN-" + stockId); + detail.setProductId(productId); + detail.setProductName(productId); + detail.setProjectNo("PROJECT-1"); + detail.setStockQuantity(BigDecimal.TEN); + return detail; + } +}