chore: 保存盘点开发前改动

This commit is contained in:
zgw
2026-06-25 17:42:39 +08:00
parent 1af5b3cbd3
commit 4b8bc03343
114 changed files with 10005 additions and 1392 deletions

View File

@@ -26,6 +26,8 @@ import com.ruoyi.common.utils.ExceptionUtil;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.constant.HttpStatus;
/**
* 外部接口入站调用日志切面。
@@ -71,7 +73,8 @@ public class ApiLogAspect
logEntity.setSysCode(apiLog.sysCode());
logEntity.setInterfaceName(apiLog.interfaceName());
logEntity.setMsgType("JSON");
logEntity.setStatus(e == null ? "SUCCESS" : "FAIL");
boolean businessFailed = e == null && isBusinessFailed(jsonResult);
logEntity.setStatus(e == null && !businessFailed ? "SUCCESS" : "FAIL");
logEntity.setHttpStatus(e == null ? 200 : 500);
logEntity.setCostMs(costMs());
@@ -91,6 +94,8 @@ public class ApiLogAspect
}
if (e != null) {
logEntity.setErrorMsg(StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, PARAM_MAX_LENGTH));
} else if (businessFailed) {
logEntity.setErrorMsg(StringUtils.substring(extractBusinessErrorMessage(jsonResult), 0, PARAM_MAX_LENGTH));
}
apiLogService.record(logEntity);
} catch (Exception ex) {
@@ -172,4 +177,38 @@ public class ApiLogAspect
{
return StringUtils.isNotEmpty(first) ? first : second;
}
private boolean isBusinessFailed(Object jsonResult)
{
Object code = extractResultCode(jsonResult);
if (code == null) {
return false;
}
String codeText = String.valueOf(code);
return !String.valueOf(HttpStatus.SUCCESS).equals(codeText) && !"0".equals(codeText);
}
private Object extractResultCode(Object jsonResult)
{
if (jsonResult instanceof AjaxResult) {
return ((AjaxResult) jsonResult).get(AjaxResult.CODE_TAG);
}
if (jsonResult instanceof Map) {
return ((Map<?, ?>) jsonResult).get(AjaxResult.CODE_TAG);
}
return null;
}
private String extractBusinessErrorMessage(Object jsonResult)
{
if (jsonResult instanceof AjaxResult) {
Object msg = ((AjaxResult) jsonResult).get(AjaxResult.MSG_TAG);
return msg == null ? "" : String.valueOf(msg);
}
if (jsonResult instanceof Map) {
Object msg = ((Map<?, ?>) jsonResult).get(AjaxResult.MSG_TAG);
return msg == null ? "" : String.valueOf(msg);
}
return "";
}
}

View File

@@ -36,6 +36,9 @@ public class WcsClient extends BaseExternalClient {
public ApiResponse<String> sendPutawayTask(String stockinNo, String locationCode,
String trayNo, String priority) {
if (mockEnabled()) {
return ApiResponse.ok("MOCK_WCS_PUTAWAY_OK");
}
String body = String.join("|", "PUTAWAY", String.valueOf(nextSeq()),
stockinNo, locationCode, trayNo, priority);
return post(url("sendPutawayTask"), body, "WCS-下发入库任务", this::parseOkNg);
@@ -48,6 +51,9 @@ public class WcsClient extends BaseExternalClient {
if (job == null) {
return ApiResponse.fail("PARAM_ERROR", "入库作业不能为空");
}
if (mockEnabled()) {
return ApiResponse.ok("MOCK_WCS_PUTAWAY_OK");
}
try {
String body = objectMapper.writeValueAsString(job);
return post(url("sendPutawayTask"), body, "WCS-下发入库任务", this::parseWcsAck, MsgTypeEnum.JSON);
@@ -60,6 +66,9 @@ public class WcsClient extends BaseExternalClient {
public ApiResponse<String> sendRetrieveTask(String stockoutNo, String locationCode,
String materialCode, String qty) {
if (mockEnabled()) {
return ApiResponse.ok("MOCK_WCS_RETRIEVE_OK");
}
String body = String.join("|", "RETRIEVE", String.valueOf(nextSeq()),
stockoutNo, locationCode, materialCode, qty);
return post(url("sendRetrieveTask"), body, "WCS-下发出库任务", this::parseOkNg);
@@ -72,6 +81,9 @@ public class WcsClient extends BaseExternalClient {
if (job == null) {
return ApiResponse.fail("PARAM_ERROR", "出库作业不能为空");
}
if (mockEnabled()) {
return ApiResponse.ok("MOCK_WCS_RETRIEVE_OK");
}
try {
String body = objectMapper.writeValueAsString(job);
return post(url("sendPutawayTask"), body, "WCS-下发出库任务", this::parseWcsAck, MsgTypeEnum.JSON);
@@ -83,6 +95,14 @@ public class WcsClient extends BaseExternalClient {
// ── 接口3:查询设备状态 ───────────────────────────────────
public ApiResponse<List<Map<String, String>>> queryDeviceStatus(String deviceType) {
if (mockEnabled()) {
Map<String, String> device = new LinkedHashMap<>();
device.put("deviceId", deviceType == null || deviceType.isEmpty() ? "MOCK_DEVICE" : "MOCK_" + deviceType);
device.put("status", "MOCK_ONLINE");
device.put("running", "Y");
device.put("error", "");
return ApiResponse.ok(Collections.singletonList(device));
}
String body = "STATUS|" + nextSeq() + "|" + deviceType;
return post(url("queryDeviceStatus"), body, "WCS-查询设备状态", rawBody -> {
String[] lines = rawBody.trim().split("\\n");
@@ -107,6 +127,9 @@ public class WcsClient extends BaseExternalClient {
public ApiResponse<String> sendCraneMove(String craneNo, String fromLocation,
String toLocation, String taskType) {
if (mockEnabled()) {
return ApiResponse.ok("MOCK_WCS_CRANE_MOVE_OK");
}
String body = String.join("|", "MOVE", String.valueOf(nextSeq()),
craneNo, fromLocation, toLocation, taskType);
return post(url("sendCraneMove"), body, "WCS-堆垛机移动指令", this::parseOkNg);
@@ -115,12 +138,24 @@ public class WcsClient extends BaseExternalClient {
// ── 接口5:取消任务 ──────────────────────────────────────
public ApiResponse<String> cancelTask(String taskId) {
if (mockEnabled()) {
return ApiResponse.ok("MOCK_WCS_CANCEL_OK");
}
String body = "CANCEL|" + nextSeq() + "|" + taskId;
return post(url("cancelTask"), body, "WCS-取消任务", this::parseOkNg);
}
// ── 辅助 ─────────────────────────────────────────────────
private boolean mockEnabled() {
ExternalProp prop = externalProp();
if (prop == null) {
return true;
}
ExternalProp.SystemConfig wcsConfig = prop.getSystem(sysEnum().getCode());
return wcsConfig == null || wcsConfig.isMockEnabled();
}
private String parseOkNg(String rawBody) {
String[] parts = rawBody.trim().split("\\|", 2);
if ("OK".equals(parts[0])) {
@@ -141,11 +176,9 @@ public class WcsClient extends BaseExternalClient {
Object success = result.get("success");
Object code = result.get("code");
Object message = result.get("message");
boolean ok = Boolean.TRUE.equals(success)
|| "true".equalsIgnoreCase(String.valueOf(success))
|| "00000".equals(String.valueOf(code))
|| "200".equals(String.valueOf(code))
|| "OK".equalsIgnoreCase(String.valueOf(code));
boolean ok = "0".equals(String.valueOf(code))
|| (code == null && (Boolean.TRUE.equals(success)
|| "true".equalsIgnoreCase(String.valueOf(success))));
if (ok) {
Object data = result.get("data");
return data == null ? (message == null ? "OK" : String.valueOf(message)) : String.valueOf(data);

View File

@@ -29,6 +29,8 @@ public class ExternalProp {
private String token;
private int connectTimeout = 5000;
private int readTimeout = 10000;
/** 是否启用本地模拟返回。当前仅 WCS 客户端使用,默认开启便于本地测试。 */
private boolean mockEnabled = true;
/** 该系统的接口路径配置,key=接口名,value=路径 */
private Map<String, String> apis;
@@ -41,6 +43,8 @@ public class ExternalProp {
public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; }
public int getReadTimeout() { return readTimeout; }
public void setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; }
public boolean isMockEnabled() { return mockEnabled; }
public void setMockEnabled(boolean mockEnabled) { this.mockEnabled = mockEnabled; }
public Map<String, String> getApis() { return apis; }
public void setApis(Map<String, String> apis) { this.apis = apis; }

View File

@@ -5,6 +5,7 @@ import com.ruoyi.api.client.MesClient;
import com.ruoyi.api.client.WcsClient;
import com.ruoyi.api.domain.WcsInboundCompleteRequest;
import com.ruoyi.api.domain.WcsInboundLocationRequest;
import com.ruoyi.api.domain.WcsJobExceptionRequest;
import com.ruoyi.api.domain.WcsOutboundCompleteRequest;
import com.ruoyi.api.domain.ApiResponse;
import com.ruoyi.common.annotation.ApiLog;
@@ -91,7 +92,7 @@ public class ExternalApiController extends BaseController {
@RequestParam String trayNo,
@RequestParam(defaultValue = "NORMAL") String priority) {
ApiResponse<String> resp = wcsClient.sendPutawayTask(stockinNo, locationCode, trayNo, priority);
return resp.isSuccess() ? success(resp.getData()) : error(resp.getMessage());
return wcsResult(resp);
}
@PostMapping("/wcs/sendRetrieveTask")
@@ -100,13 +101,13 @@ public class ExternalApiController extends BaseController {
@RequestParam String materialCode,
@RequestParam String qty) {
ApiResponse<String> resp = wcsClient.sendRetrieveTask(stockoutNo, locationCode, materialCode, qty);
return resp.isSuccess() ? success(resp.getData()) : error(resp.getMessage());
return wcsResult(resp);
}
@PostMapping("/wcs/queryDeviceStatus")
public AjaxResult queryDeviceStatus(@RequestParam(defaultValue = "ALL") String deviceType) {
ApiResponse<List<Map<String, String>>> resp = wcsClient.queryDeviceStatus(deviceType);
return resp.isSuccess() ? success(resp.getData()) : error(resp.getMessage());
return wcsResult(resp);
}
@PostMapping("/wcs/sendCraneMove")
@@ -115,13 +116,35 @@ public class ExternalApiController extends BaseController {
@RequestParam String toLocation,
@RequestParam(defaultValue = "NORMAL") String taskType) {
ApiResponse<String> resp = wcsClient.sendCraneMove(craneNo, fromLocation, toLocation, taskType);
return resp.isSuccess() ? success(resp.getData()) : error(resp.getMessage());
return wcsResult(resp);
}
@PostMapping("/wcs/cancelTask")
public AjaxResult cancelWcsTask(@RequestParam String taskId) {
ApiResponse<String> resp = wcsClient.cancelTask(taskId);
return resp.isSuccess() ? success(resp.getData()) : error(resp.getMessage());
return wcsResult(resp);
}
/**
* WCS 识别现场异常后回调 WMS。
*/
@ApiLog(interfaceName = "WCS-作业异常处理", sysCode = "WCS", direction = "INBOUND")
@PostMapping("/wcs/exception")
public AjaxResult handleWcsException(@RequestBody WcsJobExceptionRequest request) {
String exceptionType = normalizeExceptionType(request);
if (exceptionType == null) {
return wcsError("异常类型不能为空,支持PICK_EMPTY/取货无货、PUT_OCCUPIED/放货有货");
}
MydoJob job = resolveExceptionJob(request, exceptionType);
if (job == null) {
return wcsError("未找到对应作业");
}
if ("PICK_EMPTY".equals(exceptionType)) {
return wcsResult(mydoJobService.handleWcsPickEmptyException(job.getJobId()));
}
return wcsResult(mydoJobService.handleWcsPutOccupiedException(job.getJobId()));
}
/**
@@ -149,14 +172,14 @@ public class ExternalApiController extends BaseController {
job = mydoJobService.selectLatestInMydoJobByTrayCode(request.getTrayCode().trim());
}
if (job == null) {
return error("未找到对应入库作业");
return wcsError("未找到对应入库作业");
}
if (job.getCargoSpaceId() == null || job.getCargoSpaceId().trim().isEmpty()) {
Map<String, Object> cargoSpace;
try {
cargoSpace = pdaService.lockInboundCargoSpace(job.getTrayCode(), job.getWarehouseId(), job.getRoadwayId());
} catch (RuntimeException e) {
return error(e.getMessage());
return wcsError(e.getMessage());
}
job.setRoadwayId(String.valueOf(cargoSpace.get("roadway_id")));
job.setCargoSpaceId(String.valueOf(cargoSpace.get("cargo_space_id")));
@@ -164,11 +187,11 @@ public class ExternalApiController extends BaseController {
int updateRows = mydoJobService.updateMydoJob(job);
if (updateRows <= 0) {
pdaService.releaseInboundCargoSpace(job.getTrayCode(), job.getWarehouseId());
return error("入库货位回写作业失败");
return wcsError("入库货位回写作业失败");
}
}
return success(job);
return wcsSuccess(job);
}
/**
@@ -196,15 +219,19 @@ public class ExternalApiController extends BaseController {
job = mydoJobService.selectLatestInMydoJobByTrayCode(request.getTrayCode().trim());
}
if (job == null) {
return error("未找到对应入库作业");
return wcsError("未找到对应入库作业");
}
return mydoJobService.completeInJobFromWcs(job.getJobId());
if (!isExpectedJobType(job, true)) {
return wcsError("非法入库作业");
}
return wcsResult(mydoJobService.completeInJobFromWcs(job.getJobId()));
}
/**
* WCS 完成出库后回调 WMS。
* <p>
* 出库完成仍然复用当前出库作业完成过程,统一完成库存扣减、作业归档和出库单完成联动。
* WCS 确认托盘已离开库位并到达出库口后,立即释放原货位为空货位;
* PDA 扫码确认只负责后续库存扣减、作业归档、出库单完成和回流入库联动。
*/
@ApiLog(interfaceName = "WCS-完成出库作业", sysCode = "WCS", direction = "OUTBOUND")
@PostMapping("/wcs/outbound/complete")
@@ -212,9 +239,6 @@ public class ExternalApiController extends BaseController {
MydoJob job = null;
if (request != null && request.getJobId() != null && !request.getJobId().trim().isEmpty()) {
job = mydoJobService.selectMydoJobByJobId(request.getJobId().trim());
if (job != null && (job.getInOrOut() == null || job.getInOrOut() < 20)) {
job = null;
}
}
if (job == null && request != null && request.getTaskNo() != null && !request.getTaskNo().trim().isEmpty()) {
job = mydoJobService.selectOutMydoJobByTaskNo(request.getTaskNo().trim());
@@ -225,8 +249,112 @@ public class ExternalApiController extends BaseController {
job = mydoJobService.selectLatestOutMydoJobByTrayCodeAndWarehouseId(request.getTrayCode().trim(), request.getWarehouseId().trim());
}
if (job == null) {
return error("未找到对应出库作业");
return wcsError("未找到对应出库作业");
}
return mydoJobService.completeOutJobFromWcs(job.getJobId());
if (!isExpectedJobType(job, false)) {
return wcsError("非法出库作业");
}
return wcsResult(mydoJobService.completeOutJobFromWcs(job.getJobId()));
}
private <T> AjaxResult wcsResult(ApiResponse<T> response) {
if (response != null && response.isSuccess()) {
return wcsSuccess(response.getData());
}
return wcsError(response == null ? "WCS接口无返回" : response.getMessage());
}
private AjaxResult wcsResult(AjaxResult result) {
if (result != null && result.isSuccess()) {
return wcsResponse(0, String.valueOf(result.getOrDefault(AjaxResult.MSG_TAG, "操作成功")),
result.get(AjaxResult.DATA_TAG));
}
String message = result == null ? "操作失败" : String.valueOf(result.getOrDefault(AjaxResult.MSG_TAG, "操作失败"));
return wcsError(message);
}
private AjaxResult wcsSuccess(Object data) {
return wcsResponse(0, "操作成功", data);
}
private AjaxResult wcsError(String message) {
return wcsResponse(1, message, null);
}
private AjaxResult wcsResponse(int code, String message, Object data) {
return new AjaxResult(code, message, data);
}
private String normalizeExceptionType(WcsJobExceptionRequest request) {
if (request == null) {
return null;
}
String type = trimToNull(request.getExceptionType());
if (type == null) {
type = trimToNull(request.getType());
}
if (type == null) {
return null;
}
if ("PICK_EMPTY".equalsIgnoreCase(type)
|| "PICK_NO_GOODS".equalsIgnoreCase(type)
|| "取货无货".equals(type)
|| "1".equals(type)) {
return "PICK_EMPTY";
}
if ("PUT_OCCUPIED".equalsIgnoreCase(type)
|| "PUT_HAS_GOODS".equalsIgnoreCase(type)
|| "放货有货".equals(type)
|| "2".equals(type)) {
return "PUT_OCCUPIED";
}
return null;
}
private MydoJob resolveExceptionJob(WcsJobExceptionRequest request, String exceptionType) {
if (request == null) {
return null;
}
boolean inbound = "PUT_OCCUPIED".equals(exceptionType);
MydoJob job = null;
String jobId = trimToNull(request.getJobId());
if (jobId != null) {
job = mydoJobService.selectMydoJobByJobId(jobId);
if (!isExpectedJobType(job, inbound)) {
job = null;
}
}
String taskNo = trimToNull(request.getTaskNo());
if (job == null && taskNo != null) {
job = inbound
? mydoJobService.selectInMydoJobByTaskNo(taskNo)
: mydoJobService.selectOutMydoJobByTaskNo(taskNo);
}
String trayCode = trimToNull(request.getTrayCode());
String warehouseId = trimToNull(request.getWarehouseId());
if (job == null && trayCode != null && warehouseId != null) {
job = inbound
? mydoJobService.selectLatestInMydoJobByTrayCodeAndWarehouseId(trayCode, warehouseId)
: mydoJobService.selectLatestOutMydoJobByTrayCodeAndWarehouseId(trayCode, warehouseId);
}
if (job == null && inbound && trayCode != null) {
job = mydoJobService.selectLatestInMydoJobByTrayCode(trayCode);
}
return isExpectedJobType(job, inbound) ? job : null;
}
private boolean isExpectedJobType(MydoJob job, boolean inbound) {
if (job == null || job.getInOrOut() == null) {
return false;
}
return inbound ? job.getInOrOut() < 20 : job.getInOrOut() >= 20;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}

View File

@@ -0,0 +1,84 @@
package com.ruoyi.api.domain;
/**
* WCS 作业异常回调参数。
*/
public class WcsJobExceptionRequest {
/** 异常类型:PICK_EMPTY/取货无货,PUT_OCCUPIED/放货有货 */
private String exceptionType;
/** 兼容字段:异常类型 */
private String type;
/** WMS 作业ID */
private String jobId;
/** WMS/WCS 协同任务号 */
private String taskNo;
/** 托盘号 */
private String trayCode;
/** 仓库编码 */
private String warehouseId;
/** 备注 */
private String remark;
public String getExceptionType() {
return exceptionType;
}
public void setExceptionType(String exceptionType) {
this.exceptionType = exceptionType;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getJobId() {
return jobId;
}
public void setJobId(String jobId) {
this.jobId = jobId;
}
public String getTaskNo() {
return taskNo;
}
public void setTaskNo(String taskNo) {
this.taskNo = taskNo;
}
public String getTrayCode() {
return trayCode;
}
public void setTrayCode(String trayCode) {
this.trayCode = trayCode;
}
public String getWarehouseId() {
return warehouseId;
}
public void setWarehouseId(String warehouseId) {
this.warehouseId = warehouseId;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.bas.cargospace.mapper;
import java.util.List;
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.kpi.domain.BasCargoSpaceStatus;
/**
@@ -78,4 +79,68 @@ public interface BasCargoSpaceMapper
* @return
*/
BasCargoSpace selectByCoordinates(BasCargoSpace basCargoSpace);
/**
* 按库存主表ID将货位标记为出库已分配。
*
* @param stockId 库存ID
* @return 结果
*/
int markOutboundAssignedByStockId(String stockId);
/**
* 按库存ID或库存明细ID将货位标记为出库已分配。
*
* @param stockRef 库存ID或库存明细ID
* @return 结果
*/
int markOutboundAssignedByStockRef(String stockRef);
/**
* 按出库子单已生成的作业明细将货位标记为出库已分配。
*
* @param stockoutDetailId 出库子单ID
* @return 结果
*/
int markOutboundAssignedByStockoutDetailId(String stockoutDetailId);
/**
* 按货位ID将无库存货位标记为出库已分配。
*
* @param cargoSpaceId 货位ID
* @return 结果
*/
int markOutboundAssignedByCargoSpaceId(String cargoSpaceId);
/**
* 将出库作业对应货位释放为空货位。
*
* @param cargoSpaceId 货位ID
* @return 结果
*/
int releaseOutboundCargoSpace(String cargoSpaceId);
/**
* 将取货无货作业货位标记为出库需盘点。
*
* @param cargoSpaceId 货位ID
* @return 结果
*/
int markOutboundNeedCheck(String cargoSpaceId);
/**
* 将放货有货作业原货位标记为入库需盘点。
*
* @param cargoSpaceId 货位ID
* @return 结果
*/
int markInboundNeedCheck(String cargoSpaceId);
/**
* 查询同仓库同巷道的可用入库空货位。
*
* @param job 当前入库作业
* @return 可用货位
*/
BasCargoSpace selectAvailableInboundCargoSpaceInRoadway(MydoJob job);
}

View File

@@ -38,8 +38,8 @@ public class BasStationPoint extends BaseEntity
@Excel(name = "点位类型", readConverterExp = "1=入库,2=出库,3=入出库")
private String pointType;
/** 点位大小:1-大点位,2-小点位 */
@Excel(name = "点位大小", readConverterExp = "1=大点位,2=小点位")
/** 点位大小:1-大点位,2-小点位,3-通用点位 */
@Excel(name = "点位大小", readConverterExp = "1=大点位,2=小点位,3=通用点位")
private String pointSize;
/** 托盘类型,多个类型使用英文逗号分隔 */

View File

@@ -115,6 +115,19 @@ public class BasStockController extends BaseController
return getDataTable(list);
}
/**
* 导出库存物料明细列表
*/
@PreAuthorize("@ss.hasPermi('bas:stock:export')")
@Log(title = "库存物料明细", businessType = BusinessType.EXPORT)
@PostMapping("/detail/export")
public void detailExport(HttpServletResponse response, BasStockDetail basStockDetail)
{
List<BasStockDetail> list = basStockService.selectBasStockDetailList(basStockDetail);
ExcelUtil<BasStockDetail> util = new ExcelUtil<BasStockDetail>(BasStockDetail.class);
util.exportExcel(response, list, "库存物料明细数据");
}
/**
* 获取库存物料明细
*/

View File

@@ -85,6 +85,10 @@ public class BasStockDetail extends BaseEntity
@Excel(name = "仓库名称")
private String warehouseName;
/** 托盘类型 */
@Excel(name = "托盘类型")
private String trayType;
/** 可用库存数量 */
@Excel(name = "可用库存数量")
private BigDecimal availableQuantity;
@@ -269,6 +273,16 @@ public class BasStockDetail extends BaseEntity
this.warehouseName = warehouseName;
}
public String getTrayType()
{
return trayType;
}
public void setTrayType(String trayType)
{
this.trayType = trayType;
}
public BigDecimal getAvailableQuantity()
{
return availableQuantity;

View File

@@ -2,6 +2,7 @@ package com.ruoyi.bas.stock.mapper;
import java.util.List;
import com.ruoyi.bas.stock.domain.BasStockDetail;
import org.apache.ibatis.annotations.Param;
/**
* 库存物料明细Mapper接口
@@ -12,10 +13,14 @@ public interface BasStockDetailMapper
public List<BasStockDetail> selectBasStockDetailList(BasStockDetail basStockDetail);
public int countBasStockDetailByStockId(@Param("stockId") String stockId);
public int insertBasStockDetail(BasStockDetail basStockDetail);
public int updateBasStockDetail(BasStockDetail basStockDetail);
public int rollbackOutboundQuantityByJobId(String jobId);
public List<String> selectStockIdsByDetailIds(String[] detailIds);
public int deleteBasStockDetailByDetailId(String detailId);

View File

@@ -51,6 +51,14 @@ public interface BasStockMapper
*/
public int refreshBasStockSummary(String stockId);
/**
* 将取货无货作业关联库存标记为异常。
*
* @param jobId 作业ID
* @return 结果
*/
public int markExceptionByJobId(String jobId);
/**
* 删除库存信息
*

View File

@@ -1,9 +1,13 @@
package com.ruoyi.bas.stock.service.impl;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import com.ruoyi.bas.stock.domain.BasStockDetail;
import com.ruoyi.bas.stock.mapper.BasStockDetailMapper;
@@ -31,6 +35,9 @@ import com.ruoyi.bas.stock.service.IBasStockService;
@Service
public class BasStockServiceImpl implements IBasStockService
{
private static final DateTimeFormatter STOCK_ID_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private static final AtomicLong LAST_STOCK_ID_MILLIS = new AtomicLong();
@Autowired
private BasStockMapper basStockMapper;
@Autowired
@@ -77,7 +84,7 @@ public class BasStockServiceImpl implements IBasStockService
{
if (basStock.getStockId() == null || basStock.getStockId().isEmpty())
{
basStock.setStockId(UUID.randomUUID().toString().replace("-", ""));
basStock.setStockId(generateStockId());
}
BasStockDetail initialDetail = buildInitialDetail(basStock);
clearMasterMaterialFields(basStock);
@@ -190,7 +197,7 @@ public class BasStockServiceImpl implements IBasStockService
{
if (basStockDetail.getDetailId() == null || basStockDetail.getDetailId().isEmpty())
{
basStockDetail.setDetailId(UUID.randomUUID().toString().replace("-", ""));
basStockDetail.setDetailId(generateStockDetailId(basStockDetail.getStockId()));
}
basStockDetail.setCreateTime(DateUtils.getNowDate());
int rows = basStockDetailMapper.insertBasStockDetail(basStockDetail);
@@ -247,7 +254,7 @@ public class BasStockServiceImpl implements IBasStockService
return null;
}
BasStockDetail detail = new BasStockDetail();
detail.setDetailId(UUID.randomUUID().toString().replace("-", ""));
detail.setDetailId(generateStockDetailId(basStock.getStockId()));
detail.setStockId(basStock.getStockId());
detail.setProductStatus(basStock.getProductStatus());
detail.setLotNo(basStock.getLotNo());
@@ -288,6 +295,23 @@ public class BasStockServiceImpl implements IBasStockService
return value != null && !value.isEmpty();
}
private String generateStockId()
{
long now = System.currentTimeMillis();
long stockMillis = LAST_STOCK_ID_MILLIS.updateAndGet(last -> Math.max(now, last + 1));
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stockMillis), ZoneId.systemDefault());
return "KC" + STOCK_ID_FORMATTER.format(dateTime);
}
private String generateStockDetailId(String stockId)
{
if (!hasText(stockId))
{
throw new RuntimeException("库存ID不能为空");
}
return stockId + "-" + (basStockDetailMapper.countBasStockDetailByStockId(stockId) + 1);
}
private void refreshStockSummary(List<String> stockIds)
{
if (stockIds == null || stockIds.isEmpty())

View File

@@ -4,8 +4,10 @@ import java.util.List;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -46,14 +48,39 @@ public class InMydoJobController extends BaseController
{
startPage();
List<MydoJob> list;
if(StringUtils.isEmpty(mydoJob.getStatus()) || Convert.toInt(mydoJob.getStatus())<4){
list = mydoJobService.selectInMydoJobList(mydoJob);
}else {
if (isHistoryQuery(mydoJob) || (StringUtils.isNotEmpty(mydoJob.getStatus()) && Convert.toInt(mydoJob.getStatus()) >= 4)){
list = mydoJobService.selectInMydoJobFinishList(mydoJob);
}else {
list = mydoJobService.selectInMydoJobList(mydoJob);
}
return getDataTable(list);
}
/**
* 导出入库作业信息列表
*/
@PreAuthorize("@ss.hasPermi('in:job:export')")
@Log(title = "入库作业信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, MydoJob mydoJob)
{
List<MydoJob> list;
if (isHistoryQuery(mydoJob) || (StringUtils.isNotEmpty(mydoJob.getStatus()) && Convert.toInt(mydoJob.getStatus()) >= 4)) {
list = mydoJobService.selectInMydoJobFinishList(mydoJob);
} else {
list = mydoJobService.selectInMydoJobList(mydoJob);
}
ExcelUtil<MydoJob> util = new ExcelUtil<MydoJob>(MydoJob.class);
util.exportExcel(response, list, "入库作业信息数据");
}
private boolean isHistoryQuery(MydoJob mydoJob)
{
return mydoJob != null
&& mydoJob.getParams() != null
&& "true".equalsIgnoreCase(Convert.toStr(mydoJob.getParams().get("history")));
}
/**
* 获取作业信息详细信息
*/

View File

@@ -78,6 +78,22 @@ public interface MydoJobMapper
*/
List<String> selectStockinIdsByJobId(String jobId);
/**
* 查询出库作业关联的库存主表ID。
*
* @param jobId 作业ID
* @return 库存ID集合
*/
List<String> selectStockIdsByJobId(String jobId);
/**
* 查询出库作业关联的出库子单ID。
*
* @param jobId 作业ID
* @return 出库子单ID集合
*/
List<String> selectStockoutDetailIdsByJobId(String jobId);
/**
* 查询作业信息列表
*

View File

@@ -141,12 +141,31 @@ public interface IMydoJobService
/**
* WCS 回调完成出库作业。
* <p>
* 标记作业已到达出库口并释放原货位;不执行出库完成存储过程,
* 库存扣减和作业归档仍由 PDA 扫码确认触发。
*
* @param jobId 作业ID
* @return 处理结果
*/
AjaxResult completeOutJobFromWcs(String jobId);
/**
* WCS 回调取货无货异常。
*
* @param jobId 出库作业ID
* @return 处理结果
*/
AjaxResult handleWcsPickEmptyException(String jobId);
/**
* WCS 回调放货有货异常。
*
* @param jobId 入库作业ID
* @return 处理结果,data 为重新分配货位后的作业对象
*/
AjaxResult handleWcsPutOccupiedException(String jobId);
/**
* 将未下发的出库作业推送到 WCS。
*

View File

@@ -5,6 +5,10 @@ import java.util.List;
import com.ruoyi.api.client.WcsClient;
import com.ruoyi.api.domain.ApiResponse;
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
import com.ruoyi.bas.stock.mapper.BasStockDetailMapper;
import com.ruoyi.bas.stock.mapper.BasStockMapper;
import com.ruoyi.bas.stationpoint.service.IBasStationPointService;
import com.ruoyi.common.annotation.WarehouseScope;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -12,12 +16,17 @@ import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.in.jobdetail.mapper.MydoJobDetailMapper;
import com.ruoyi.in.stockin.mapper.InStockinMapper;
import com.ruoyi.out.stockout.mapper.OutStockoutMapper;
import com.ruoyi.out.stockoutdetail.mapper.OutStockoutDetailMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.in.job.mapper.MydoJobMapper;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
/**
* 作业信息Service业务层处理
@@ -37,6 +46,24 @@ public class MydoJobServiceImpl implements IMydoJobService
@Autowired
private IBasStationPointService basStationPointService;
@Autowired
private BasCargoSpaceMapper basCargoSpaceMapper;
@Autowired
private BasStockMapper basStockMapper;
@Autowired
private BasStockDetailMapper basStockDetailMapper;
@Autowired
private OutStockoutDetailMapper outStockoutDetailMapper;
@Autowired
private OutStockoutMapper outStockoutMapper;
@Autowired
private MydoJobDetailMapper mydoJobDetailMapper;
@Autowired
private WcsClient wcsClient;
@@ -146,7 +173,8 @@ public class MydoJobServiceImpl implements IMydoJobService
MydoJob updateJob = new MydoJob();
updateJob.setJobId(job.getJobId());
updateJob.setStatus("2");
updateJob.setStatus("3");
updateJob.setExecutionPhase("1");
updateJob.setStartTime(DateUtils.getTime());
if (mydoJobMapper.updateMydoJob(updateJob) <= 0) {
return AjaxResult.error("出库作业状态更新失败");
@@ -227,6 +255,10 @@ public class MydoJobServiceImpl implements IMydoJobService
public AjaxResult cancelJob(String jobId,String type)
{
MydoJob job = mydoJobMapper.selectMydoJobByJobId(jobId);
AjaxResult wcsCancelResult = requestWcsCancelBeforeManualCancel(job);
if (wcsCancelResult.isError()) {
return wcsCancelResult;
}
if ("1".equals(type)){
HashMap<String, Object> map = new HashMap<>();
map.put("v_job_id", jobId);
@@ -251,6 +283,27 @@ public class MydoJobServiceImpl implements IMydoJobService
return AjaxResult.error();
}
private AjaxResult requestWcsCancelBeforeManualCancel(MydoJob job)
{
if (job == null) {
return AjaxResult.error("作业不存在");
}
if (!"2".equals(job.getStatus()) && !"3".equals(job.getStatus())) {
return AjaxResult.success();
}
String taskId = StringUtils.isEmpty(job.getTaskNo()) ? job.getJobId() : job.getTaskNo();
try {
ApiResponse<String> response = wcsClient.cancelTask(taskId);
if (response == null || !response.isSuccess()) {
String message = response == null ? "WCS接口无返回" : response.getMessage();
return AjaxResult.error("WCS拒绝作废:" + message);
}
} catch (RuntimeException e) {
return AjaxResult.error("WCS拒绝作废:" + e.getMessage());
}
return AjaxResult.success();
}
/**
* 手动完成作业
*
@@ -306,6 +359,12 @@ public class MydoJobServiceImpl implements IMydoJobService
public AjaxResult completeInJobFromWcs(String jobId)
{
MydoJob job = mydoJobMapper.selectMydoJobByJobId(jobId);
if (job == null) {
return AjaxResult.error("入库作业不存在");
}
if (!isInboundJob(job)) {
return AjaxResult.error("非法入库作业");
}
List<String> stockinIds = mydoJobMapper.selectStockinIdsByJobId(jobId);
HashMap<String, Object> map = new HashMap<>();
map.put("job_id", jobId);
@@ -325,21 +384,163 @@ public class MydoJobServiceImpl implements IMydoJobService
/**
* WCS 回调完成出库作业。
* <p>
* 此时托盘已离开库位,货位状态在 WCS 完成回调中释放;PDA 确认不再负责释放货位。
*/
@Override
public AjaxResult completeOutJobFromWcs(String jobId)
{
MydoJob job = mydoJobMapper.selectMydoJobByJobId(jobId);
HashMap<String, Object> map = new HashMap<>();
map.put("v_job_id", jobId);
map.put("v_dept_id", 0);
map.put("v_user_id", 0);
mydoJobMapper.makeSure(map);
if (isProcedureSuccess(map.get("v_result"))){
releaseJobStationPoint(job);
if (job == null) {
return AjaxResult.error("出库作业不存在");
}
if (!isOutboundJob(job)) {
return AjaxResult.error("非法出库作业");
}
MydoJob updateJob = new MydoJob();
updateJob.setJobId(job.getJobId());
updateJob.setExecutionPhase("5");
if (mydoJobMapper.updateMydoJob(updateJob) <= 0) {
return AjaxResult.error("出库作业等待扫码确认状态更新失败");
}
AjaxResult releaseResult = releaseOutboundCargoSpace(job);
if (releaseResult.isError()) {
return releaseResult;
}
return AjaxResult.success("出库作业已到达出口,货位已释放,等待PDA扫码确认");
}
private boolean isInboundJob(MydoJob job)
{
return job != null && job.getInOrOut() != null && job.getInOrOut() < 20;
}
private boolean isOutboundJob(MydoJob job)
{
return job != null && job.getInOrOut() != null && job.getInOrOut() >= 20;
}
/**
* WCS 回调取货无货异常。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AjaxResult handleWcsPickEmptyException(String jobId)
{
if (StringUtils.isEmpty(jobId)) {
return AjaxResult.error("作业ID不能为空");
}
MydoJob job = mydoJobMapper.selectMydoJobByJobId(jobId);
if (job == null) {
return AjaxResult.error("出库作业不存在");
}
if (job.getInOrOut() == null || job.getInOrOut() < 20) {
return AjaxResult.error("请选择出库作业");
}
if (StringUtils.isEmpty(job.getCargoSpaceId())) {
return AjaxResult.error("出库作业货位不能为空");
}
List<String> stockIds = mydoJobMapper.selectStockIdsByJobId(jobId);
List<String> stockoutDetailIds = mydoJobMapper.selectStockoutDetailIdsByJobId(jobId);
if (basCargoSpaceMapper.markOutboundNeedCheck(job.getCargoSpaceId()) <= 0) {
return rollbackError("出库作业货位异常状态更新失败");
}
boolean hasStockDetails = stockIds != null && !stockIds.isEmpty();
boolean hasStockoutDetails = stockoutDetailIds != null && !stockoutDetailIds.isEmpty();
if (hasStockoutDetails) {
if (outStockoutDetailMapper.rollbackAllotByJobId(jobId) <= 0) {
return rollbackError("出库单据分配数量回滚失败");
}
outStockoutMapper.reopenOutStockoutByExceptionJobId(jobId);
}
if (hasStockDetails) {
String[] stockIdArray = stockIds.toArray(new String[0]);
basStockDetailMapper.deleteBasStockDetailByStockIds(stockIdArray);
if (basStockMapper.deleteBasStockByStockIds(stockIdArray) <= 0) {
return rollbackError("库存删除失败");
}
}
mydoJobDetailMapper.deleteMydoJobDetailByJobId(jobId);
if (mydoJobMapper.deleteMydoJobByJobId(jobId) <= 0) {
return rollbackError("作业作废失败");
}
releaseJobStationPoint(job);
return AjaxResult.success("取货无货异常处理完成");
}
/**
* WCS 回调放货有货异常。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AjaxResult handleWcsPutOccupiedException(String jobId)
{
if (StringUtils.isEmpty(jobId)) {
return AjaxResult.error("作业ID不能为空");
}
MydoJob job = mydoJobMapper.selectMydoJobByJobId(jobId);
if (job == null) {
return AjaxResult.error("入库作业不存在");
}
if (job.getInOrOut() == null || job.getInOrOut() >= 20) {
return AjaxResult.error("请选择入库作业");
}
if (StringUtils.isEmpty(job.getCargoSpaceId())) {
return AjaxResult.error("入库作业货位不能为空");
}
if (StringUtils.isEmpty(job.getWarehouseId()) || StringUtils.isEmpty(job.getRoadwayId())) {
return AjaxResult.error("入库作业仓库或巷道不能为空");
}
if (basCargoSpaceMapper.markInboundNeedCheck(job.getCargoSpaceId()) <= 0) {
return rollbackError("入库作业原货位异常状态更新失败");
}
BasCargoSpace cargoSpace = basCargoSpaceMapper.selectAvailableInboundCargoSpaceInRoadway(job);
if (cargoSpace == null) {
return rollbackError("相同巷道没有可用入库货位");
}
BasCargoSpace lockCargoSpace = new BasCargoSpace();
lockCargoSpace.setCargoSpaceId(cargoSpace.getCargoSpaceId());
lockCargoSpace.setStatus("3");
lockCargoSpace.setTrayCode(job.getTrayCode());
if (basCargoSpaceMapper.updateBasCargoSpace(lockCargoSpace) <= 0) {
return rollbackError("新入库货位锁定失败");
}
MydoJob updateJob = new MydoJob();
updateJob.setJobId(job.getJobId());
updateJob.setRoadwayId(cargoSpace.getRoadwayId());
updateJob.setCargoSpaceId(cargoSpace.getCargoSpaceId());
updateJob.setCargoSpaceName(cargoSpace.getCargoSpaceName());
if (mydoJobMapper.updateMydoJob(updateJob) <= 0) {
return rollbackError("入库作业货位更新失败");
}
MydoJob updatedJob = mydoJobMapper.selectMydoJobByJobId(jobId);
if (updatedJob == null) {
updatedJob = updateJob;
}
return AjaxResult.success("放货有货异常处理完成", updatedJob);
}
private AjaxResult releaseOutboundCargoSpace(MydoJob job)
{
if (job == null || StringUtils.isEmpty(job.getCargoSpaceId())) {
return AjaxResult.success();
}
return AjaxResult.error(Convert.toStr(map.get("v_resultmsg")));
if (basCargoSpaceMapper.releaseOutboundCargoSpace(job.getCargoSpaceId()) <= 0) {
return AjaxResult.error("出库作业货位释放失败");
}
return AjaxResult.success();
}
private AjaxResult rollbackError(String message)
{
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return AjaxResult.error(message);
}
/**

View File

@@ -59,5 +59,7 @@ public interface MydoJobDetailMapper
*/
public int deleteMydoJobDetailByJobDetailIds(String[] jobDetailIds);
public int deleteMydoJobDetailByJobId(String jobId);
List<MydoJobDetail> selectMydoJobDetailFinishList(MydoJobDetail mydoJobDetail);
}

View File

@@ -50,10 +50,10 @@ public class InStockinController extends BaseController
{
startPage();
List<InStockin> list;
if (StringUtils.isEmpty(inStockin.getStatus()) || Convert.toInt(inStockin.getStatus())< 4){
list = inStockinService.selectInStockinList(inStockin);
}else {
if (isHistoryQuery(inStockin) || (StringUtils.isNotEmpty(inStockin.getStatus()) && Convert.toInt(inStockin.getStatus()) >= 4)){
list = inStockinService.selectInStockinFinishList(inStockin);
}else {
list = inStockinService.selectInStockinList(inStockin);
}
return getDataTable(list);
}
@@ -66,11 +66,23 @@ public class InStockinController extends BaseController
@PostMapping("/export")
public void export(HttpServletResponse response, InStockin inStockin)
{
List<InStockin> list = inStockinService.selectInStockinList(inStockin);
List<InStockin> list;
if (isHistoryQuery(inStockin) || (StringUtils.isNotEmpty(inStockin.getStatus()) && Convert.toInt(inStockin.getStatus()) >= 4)) {
list = inStockinService.selectInStockinFinishList(inStockin);
} else {
list = inStockinService.selectInStockinList(inStockin);
}
ExcelUtil<InStockin> util = new ExcelUtil<InStockin>(InStockin.class);
util.exportExcel(response, list, "入库单据主信息数据");
}
private boolean isHistoryQuery(InStockin inStockin)
{
return inStockin != null
&& inStockin.getParams() != null
&& "true".equalsIgnoreCase(Convert.toStr(inStockin.getParams().get("history")));
}
/**
* 获取入库单据主信息详细信息
*/

View File

@@ -12,6 +12,7 @@ import com.ruoyi.common.annotation.WarehouseScope;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -150,12 +151,26 @@ public class InStockinServiceImpl implements IInStockinService
}
// 关联主单ID
detail.setStockinId(saveDTO.getStockinId());
normalizeProductFields(detail);
inStockinDetailMapper.insertInStockinDetail(detail);
}
}
return saveDTO.getStockinId();
}
private void normalizeProductFields(InStockinDetail detail)
{
if (detail == null) {
return;
}
if (StringUtils.isNotEmpty(detail.getItemCod())) {
detail.setProductId(detail.getItemCod());
}
if (StringUtils.isNotEmpty(detail.getItemNam())) {
detail.setProductName(detail.getItemNam());
}
}
/**
* 手动作废入库单据
*

View File

@@ -20,7 +20,7 @@ import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.function.Function;
/**
* KPI Service实现
@@ -45,6 +45,8 @@ public class KpiServiceImpl implements KpiService {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String UNASSIGNED_ROADWAY = "未分配";
@Override
public Map<String, Object> selectInboundTrend(InAnalysisDataQuery query) {
query.setStartTime(query.getStartTime()+" 00:00:00");
@@ -55,40 +57,12 @@ public class KpiServiceImpl implements KpiService {
// 2. 获取时间范围内所有日期(x轴)
List<String> dateListBetween = DateUtils.getDateListBetween(query.getStartTime(), query.getEndTime());
// 3. 按 巷道ID 分组
Map<String, List<InAnalysisData>> roadwayMap = list.stream()
.collect(Collectors.groupingBy(InAnalysisData::getRoadwayId));
// 4. 组装 series
List<Map<String, Object>> seriesList = new ArrayList<>();
for (Map.Entry<String, List<InAnalysisData>> entry : roadwayMap.entrySet()) {
String roadwayId = entry.getKey(); // 巷道ID
List<InAnalysisData> dataList = entry.getValue();
// 把数据转成 Map<日期, 数量> 方便查找
Map<String, Long> dateCountMap = dataList.stream()
.collect(Collectors.toMap(InAnalysisData::getDateDay, InAnalysisData::getCount));
// 遍历所有日期,没有数据补 0
List<Long> countList = new ArrayList<>();
for (String date : dateListBetween) {
countList.add(dateCountMap.getOrDefault(date, 0L));
}
// 构造 series 项
Map<String, Object> series = new HashMap<>();
series.put("name", "巷道"+roadwayId); // 巷道1、巷道2...
series.put("data", countList);
seriesList.add(series);
}
// 5. 组装最终返回结构
Map<String, Object> result = new HashMap<>();
result.put("xAxis", dateListBetween);
result.put("series", seriesList);
return result; // ← 这里返回,不是return null
return buildRoadwayTrend(
list,
dateListBetween,
InAnalysisData::getRoadwayId,
InAnalysisData::getDateDay,
InAnalysisData::getCount);
}
@@ -103,40 +77,58 @@ public class KpiServiceImpl implements KpiService {
// 2. 获取时间范围内所有日期(x轴)
List<String> dateListBetween = DateUtils.getDateListBetween(query.getStartTime(), query.getEndTime());
// 3. 按 巷道ID 分组
Map<String, List<OutAnalysisData>> roadwayMap = list.stream()
.collect(Collectors.groupingBy(OutAnalysisData::getRoadwayId));
return buildRoadwayTrend(
list,
dateListBetween,
OutAnalysisData::getRoadwayId,
OutAnalysisData::getDateDay,
OutAnalysisData::getCount);
}
private <T> Map<String, Object> buildRoadwayTrend(List<T> list,
List<String> dateListBetween,
Function<T, String> roadwayGetter,
Function<T, String> dateGetter,
Function<T, Long> countGetter) {
Map<String, Map<String, Long>> roadwayDateCountMap = new LinkedHashMap<>();
if (list != null) {
for (T item : list) {
String dateDay = dateGetter.apply(item);
if (dateDay == null || dateDay.trim().isEmpty()) {
continue;
}
String roadwayId = normalizeRoadwayId(roadwayGetter.apply(item));
Long count = countGetter.apply(item);
roadwayDateCountMap
.computeIfAbsent(roadwayId, key -> new LinkedHashMap<>())
.merge(dateDay, count == null ? 0L : count, Long::sum);
}
}
// 4. 组装 series
List<Map<String, Object>> seriesList = new ArrayList<>();
for (Map.Entry<String, List<OutAnalysisData>> entry : roadwayMap.entrySet()) {
String roadwayId = entry.getKey(); // 巷道ID
List<OutAnalysisData> dataList = entry.getValue();
// 把数据转成 Map<日期, 数量> 方便查找
Map<String, Long> dateCountMap = dataList.stream()
.collect(Collectors.toMap(OutAnalysisData::getDateDay, OutAnalysisData::getCount));
// 遍历所有日期,没有数据补 0
for (Map.Entry<String, Map<String, Long>> entry : roadwayDateCountMap.entrySet()) {
List<Long> countList = new ArrayList<>();
for (String date : dateListBetween) {
countList.add(dateCountMap.getOrDefault(date, 0L));
countList.add(entry.getValue().getOrDefault(date, 0L));
}
// 构造 series 项
Map<String, Object> series = new HashMap<>();
series.put("name", "巷道"+roadwayId); // 巷道1、巷道2...
series.put("name", "巷道" + entry.getKey());
series.put("data", countList);
seriesList.add(series);
}
// 5. 组装最终返回结构
Map<String, Object> result = new HashMap<>();
result.put("xAxis", dateListBetween);
result.put("series", seriesList);
return result;
}
return result; // ← 这里返回,不是return null
private String normalizeRoadwayId(String roadwayId) {
if (roadwayId == null || roadwayId.trim().isEmpty()) {
return UNASSIGNED_ROADWAY;
}
return roadwayId.trim();
}
@Override

View File

@@ -4,8 +4,10 @@ import java.util.List;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -44,14 +46,39 @@ public class OutMydoJobController extends BaseController
{
startPage();
List<MydoJob> list;
if (StringUtils.isEmpty(mydoJob.getStatus()) || Convert.toInt(mydoJob.getStatus())<4){
list = mydoJobService.selectOutMydoJobList(mydoJob);
}else {
if (isHistoryQuery(mydoJob) || (StringUtils.isNotEmpty(mydoJob.getStatus()) && Convert.toInt(mydoJob.getStatus()) >= 4)){
list = mydoJobService.selectOutMydoJobFinishList(mydoJob);
}else {
list = mydoJobService.selectOutMydoJobList(mydoJob);
}
return getDataTable(list);
}
/**
* 导出出库作业信息列表
*/
@PreAuthorize("@ss.hasPermi('out:job:export')")
@Log(title = "出库作业信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, MydoJob mydoJob)
{
List<MydoJob> list;
if (isHistoryQuery(mydoJob) || (StringUtils.isNotEmpty(mydoJob.getStatus()) && Convert.toInt(mydoJob.getStatus()) >= 4)) {
list = mydoJobService.selectOutMydoJobFinishList(mydoJob);
} else {
list = mydoJobService.selectOutMydoJobList(mydoJob);
}
ExcelUtil<MydoJob> util = new ExcelUtil<MydoJob>(MydoJob.class);
util.exportExcel(response, list, "出库作业信息数据");
}
private boolean isHistoryQuery(MydoJob mydoJob)
{
return mydoJob != null
&& mydoJob.getParams() != null
&& "true".equalsIgnoreCase(Convert.toStr(mydoJob.getParams().get("history")));
}
/**
* 获取作业信息详细信息
*/

View File

@@ -0,0 +1,53 @@
package com.ruoyi.out.location.controller;
import java.util.List;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.out.location.domain.LocationOutboundGenerateRequest;
import com.ruoyi.out.location.domain.LocationOutboundStock;
import com.ruoyi.out.location.service.ILocationOutboundService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
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;
/**
* 指定货位出库Controller。
*/
@RestController
@RequestMapping("/out/locationOutbound")
public class LocationOutboundController extends BaseController
{
@Autowired
private ILocationOutboundService locationOutboundService;
/**
* 查询货位库存列表。
*/
@PreAuthorize("@ss.hasPermi('out:locationOutbound:list')")
@GetMapping("/list")
public TableDataInfo list(LocationOutboundStock query)
{
startPage();
List<LocationOutboundStock> list = locationOutboundService.selectLocationOutboundStockList(query);
return getDataTable(list);
}
/**
* 按选中货位整托生成出库作业。
*/
@PreAuthorize("@ss.hasPermi('out:locationOutbound:generate')")
@Log(title = "指定货位出库", businessType = BusinessType.INSERT)
@PostMapping("/generate")
public AjaxResult generate(@RequestBody LocationOutboundGenerateRequest request)
{
return success(locationOutboundService.generateWholeTrayOutboundJob(request));
}
}

View File

@@ -0,0 +1,35 @@
package com.ruoyi.out.location.domain;
/**
* 指定货位出库作业生成参数。
*/
public class LocationOutboundGenerateRequest
{
private String stockId;
private String cargoSpaceId;
private String exitId;
public String getStockId() {
return stockId;
}
public void setStockId(String stockId) {
this.stockId = stockId;
}
public String getCargoSpaceId() {
return cargoSpaceId;
}
public void setCargoSpaceId(String cargoSpaceId) {
this.cargoSpaceId = cargoSpaceId;
}
public String getExitId() {
return exitId;
}
public void setExitId(String exitId) {
this.exitId = exitId;
}
}

View File

@@ -0,0 +1,290 @@
package com.ruoyi.out.location.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.bas.stock.domain.BasStockDetail;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 指定货位出库库存视图。
*/
public class LocationOutboundStock extends BaseEntity
{
private static final long serialVersionUID = 1L;
private String cargoSpaceId;
private String cargoSpaceName;
private String cargoSpaceStatus;
private String locked;
private String warehouseId;
private String warehouseName;
private String roadwayId;
private Long thePlatoon;
private Long theColumn;
private Long theFloor;
private String theType;
private String theTypeName;
private String stockId;
private String stockStatus;
private String trayCode;
private String trayType;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date inDate;
private String stockinId;
private String inType;
private String supplierId;
private String supplierName;
private Integer detailCount;
private BigDecimal quantity;
private BigDecimal outQuantity;
private BigDecimal availableQuantity;
private String productId;
private String productName;
private String lotNo;
private Boolean showEmpty;
private List<BasStockDetail> details = new ArrayList<>();
public String getCargoSpaceId() {
return cargoSpaceId;
}
public void setCargoSpaceId(String cargoSpaceId) {
this.cargoSpaceId = cargoSpaceId;
}
public String getCargoSpaceName() {
return cargoSpaceName;
}
public void setCargoSpaceName(String cargoSpaceName) {
this.cargoSpaceName = cargoSpaceName;
}
public String getCargoSpaceStatus() {
return cargoSpaceStatus;
}
public void setCargoSpaceStatus(String cargoSpaceStatus) {
this.cargoSpaceStatus = cargoSpaceStatus;
}
public String getLocked() {
return locked;
}
public void setLocked(String locked) {
this.locked = locked;
}
public String getWarehouseId() {
return warehouseId;
}
public void setWarehouseId(String warehouseId) {
this.warehouseId = warehouseId;
}
public String getWarehouseName() {
return warehouseName;
}
public void setWarehouseName(String warehouseName) {
this.warehouseName = warehouseName;
}
public String getRoadwayId() {
return roadwayId;
}
public void setRoadwayId(String roadwayId) {
this.roadwayId = roadwayId;
}
public Long getThePlatoon() {
return thePlatoon;
}
public void setThePlatoon(Long thePlatoon) {
this.thePlatoon = thePlatoon;
}
public Long getTheColumn() {
return theColumn;
}
public void setTheColumn(Long theColumn) {
this.theColumn = theColumn;
}
public Long getTheFloor() {
return theFloor;
}
public void setTheFloor(Long theFloor) {
this.theFloor = theFloor;
}
public String getTheType() {
return theType;
}
public void setTheType(String theType) {
this.theType = theType;
}
public String getTheTypeName() {
return theTypeName;
}
public void setTheTypeName(String theTypeName) {
this.theTypeName = theTypeName;
}
public String getStockId() {
return stockId;
}
public void setStockId(String stockId) {
this.stockId = stockId;
}
public String getStockStatus() {
return stockStatus;
}
public void setStockStatus(String stockStatus) {
this.stockStatus = stockStatus;
}
public String getTrayCode() {
return trayCode;
}
public void setTrayCode(String trayCode) {
this.trayCode = trayCode;
}
public String getTrayType() {
return trayType;
}
public void setTrayType(String trayType) {
this.trayType = trayType;
}
public Date getInDate() {
return inDate;
}
public void setInDate(Date inDate) {
this.inDate = inDate;
}
public String getStockinId() {
return stockinId;
}
public void setStockinId(String stockinId) {
this.stockinId = stockinId;
}
public String getInType() {
return inType;
}
public void setInType(String inType) {
this.inType = inType;
}
public String getSupplierId() {
return supplierId;
}
public void setSupplierId(String supplierId) {
this.supplierId = supplierId;
}
public String getSupplierName() {
return supplierName;
}
public void setSupplierName(String supplierName) {
this.supplierName = supplierName;
}
public Integer getDetailCount() {
return detailCount;
}
public void setDetailCount(Integer detailCount) {
this.detailCount = detailCount;
}
public BigDecimal getQuantity() {
return quantity;
}
public void setQuantity(BigDecimal quantity) {
this.quantity = quantity;
}
public BigDecimal getOutQuantity() {
return outQuantity;
}
public void setOutQuantity(BigDecimal outQuantity) {
this.outQuantity = outQuantity;
}
public BigDecimal getAvailableQuantity() {
return availableQuantity;
}
public void setAvailableQuantity(BigDecimal availableQuantity) {
this.availableQuantity = availableQuantity;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getLotNo() {
return lotNo;
}
public void setLotNo(String lotNo) {
this.lotNo = lotNo;
}
public Boolean getShowEmpty() {
return showEmpty;
}
public void setShowEmpty(Boolean showEmpty) {
this.showEmpty = showEmpty;
}
public List<BasStockDetail> getDetails() {
return details;
}
public void setDetails(List<BasStockDetail> details) {
this.details = details;
}
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.out.location.mapper;
import java.util.HashMap;
import java.util.List;
import com.ruoyi.bas.stock.domain.BasStockDetail;
import com.ruoyi.out.location.domain.LocationOutboundStock;
/**
* 指定货位出库Mapper。
*/
public interface LocationOutboundMapper
{
List<LocationOutboundStock> selectLocationOutboundStockList(LocationOutboundStock query);
List<BasStockDetail> selectLocationOutboundDetailsByStockIds(String[] stockIds);
LocationOutboundStock selectLocationOutboundStockForUpdate(String stockId);
LocationOutboundStock selectLocationOutboundLocationForUpdate(String cargoSpaceId);
List<BasStockDetail> selectAvailableDetailsForUpdate(String stockId);
int reserveStockDetailForOutbound(HashMap<String, Object> map);
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.out.location.service;
import java.util.List;
import com.ruoyi.out.location.domain.LocationOutboundGenerateRequest;
import com.ruoyi.out.location.domain.LocationOutboundStock;
/**
* 指定货位出库服务。
*/
public interface ILocationOutboundService
{
List<LocationOutboundStock> selectLocationOutboundStockList(LocationOutboundStock query);
String generateWholeTrayOutboundJob(LocationOutboundGenerateRequest request);
}

View File

@@ -0,0 +1,310 @@
package com.ruoyi.out.location.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.mapper.BasCargoSpaceMapper;
import com.ruoyi.bas.id.job.service.JobIdService;
import com.ruoyi.bas.id.taskno.service.TaskNoService;
import com.ruoyi.bas.stationpoint.domain.BasStationPoint;
import com.ruoyi.bas.stationpoint.service.IBasStationPointService;
import com.ruoyi.bas.stock.domain.BasStockDetail;
import com.ruoyi.bas.stock.mapper.BasStockMapper;
import com.ruoyi.common.annotation.WarehouseScope;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
import com.ruoyi.in.jobdetail.service.IMydoJobDetailService;
import com.ruoyi.out.location.domain.LocationOutboundGenerateRequest;
import com.ruoyi.out.location.domain.LocationOutboundStock;
import com.ruoyi.out.location.mapper.LocationOutboundMapper;
import com.ruoyi.out.location.service.ILocationOutboundService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 指定货位出库服务实现。
*/
@Service
public class LocationOutboundServiceImpl implements ILocationOutboundService
{
@Autowired
private LocationOutboundMapper locationOutboundMapper;
@Autowired
private BasCargoSpaceMapper basCargoSpaceMapper;
@Autowired
private BasStockMapper basStockMapper;
@Autowired
private IBasStationPointService basStationPointService;
@Autowired
private IMydoJobService mydoJobService;
@Autowired
private IMydoJobDetailService mydoJobDetailService;
@Autowired
private JobIdService jobIdService;
@Autowired
private TaskNoService taskNoService;
@Override
@WarehouseScope(warehouseField = "bcs.warehouse_id")
public List<LocationOutboundStock> selectLocationOutboundStockList(LocationOutboundStock query)
{
List<LocationOutboundStock> rows = locationOutboundMapper.selectLocationOutboundStockList(query);
if (rows == null || rows.isEmpty()) {
return rows;
}
List<String> stockIds = rows.stream()
.map(LocationOutboundStock::getStockId)
.filter(StringUtils::isNotEmpty)
.distinct()
.collect(Collectors.toList());
if (stockIds.isEmpty()) {
return rows;
}
List<BasStockDetail> details = locationOutboundMapper.selectLocationOutboundDetailsByStockIds(stockIds.toArray(new String[0]));
Map<String, List<BasStockDetail>> detailMap = details == null ? new HashMap<>() :
details.stream().collect(Collectors.groupingBy(BasStockDetail::getStockId));
for (LocationOutboundStock row : rows) {
if (StringUtils.isNotEmpty(row.getStockId())) {
row.setDetails(detailMap.getOrDefault(row.getStockId(), new ArrayList<>()));
}
}
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String generateWholeTrayOutboundJob(LocationOutboundGenerateRequest request)
{
checkRequest(request);
LocationOutboundStock stock = selectOutboundLocationForUpdate(request);
boolean emptyLocation = StringUtils.isEmpty(stock.getStockId());
if (emptyLocation) {
checkEmptyLocation(stock);
} else {
checkStock(stock);
}
BasStationPoint exitPoint = validateExitPoint(request.getExitId(), stock);
String jobId = jobIdService.getId("CK");
String taskNo = taskNoService.getTaskNo("20");
MydoJob job = buildOutboundJob(jobId, taskNo, stock, exitPoint);
if (mydoJobService.insertMydoJob(job) <= 0) {
throw new RuntimeException("指定货位出库作业生成失败");
}
if (emptyLocation) {
if (basCargoSpaceMapper.markOutboundAssignedByCargoSpaceId(stock.getCargoSpaceId()) <= 0) {
throw new RuntimeException("当前货位已被占用,请刷新后重试");
}
} else {
List<BasStockDetail> details = locationOutboundMapper.selectAvailableDetailsForUpdate(stock.getStockId());
if (details == null || details.isEmpty()) {
throw new RuntimeException("当前托盘无可出库库存明细");
}
int index = 1;
BigDecimal totalQuantity = BigDecimal.ZERO;
for (BasStockDetail detail : details) {
BigDecimal availableQuantity = availableQuantity(detail);
if (availableQuantity.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
String jobDetailId = jobId + "-" + index++;
MydoJobDetail jobDetail = buildOutboundJobDetail(job, jobDetailId, stock, detail, availableQuantity);
if (mydoJobDetailService.insertMydoJobDetail(jobDetail) <= 0) {
throw new RuntimeException("指定货位出库作业明细生成失败");
}
HashMap<String, Object> reserveMap = new HashMap<>();
reserveMap.put("stockDetailId", detail.getDetailId());
reserveMap.put("stockId", detail.getStockId());
reserveMap.put("jobDetailId", jobDetailId);
reserveMap.put("quantity", availableQuantity);
if (locationOutboundMapper.reserveStockDetailForOutbound(reserveMap) <= 0) {
throw new RuntimeException("库存已被占用,请刷新后重试");
}
totalQuantity = totalQuantity.add(availableQuantity);
}
if (totalQuantity.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("当前托盘无可出库库存明细");
}
basStockMapper.refreshBasStockSummary(stock.getStockId());
basCargoSpaceMapper.markOutboundAssignedByStockId(stock.getStockId());
}
AjaxResult pushResult = mydoJobService.pushOutJobToWcs(jobId);
if (pushResult == null || pushResult.isError()) {
throw new RuntimeException(pushResult == null ? "WCS出库作业下发失败" : Convert.toStr(pushResult.get(AjaxResult.MSG_TAG)));
}
return "指定货位出库作业生成成功:" + jobId + ";" + Convert.toStr(pushResult.get(AjaxResult.MSG_TAG));
}
private void checkRequest(LocationOutboundGenerateRequest request)
{
if (request == null || (StringUtils.isEmpty(request.getStockId()) && StringUtils.isEmpty(request.getCargoSpaceId()))) {
throw new RuntimeException("请选择要出库的货位");
}
if (StringUtils.isEmpty(request.getExitId())) {
throw new RuntimeException("请选择出库口");
}
}
private LocationOutboundStock selectOutboundLocationForUpdate(LocationOutboundGenerateRequest request)
{
LocationOutboundStock stock = StringUtils.isNotEmpty(request.getStockId())
? locationOutboundMapper.selectLocationOutboundStockForUpdate(request.getStockId())
: locationOutboundMapper.selectLocationOutboundLocationForUpdate(request.getCargoSpaceId());
if (stock == null || StringUtils.isEmpty(stock.getCargoSpaceId())) {
throw new RuntimeException("货位不存在或已出库");
}
return stock;
}
private void checkStock(LocationOutboundStock stock)
{
if (stock == null || StringUtils.isEmpty(stock.getStockId())) {
throw new RuntimeException("库存不存在或已出库");
}
if (!"2".equals(stock.getCargoSpaceStatus())) {
if ("4".equals(stock.getCargoSpaceStatus())) {
throw new RuntimeException("当前货位已生成出库作业");
}
throw new RuntimeException("当前货位不是库内有库存状态");
}
if (stock.getAvailableQuantity() == null || stock.getAvailableQuantity().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("当前托盘无可出库库存");
}
}
private void checkEmptyLocation(LocationOutboundStock stock)
{
if (stock == null || StringUtils.isEmpty(stock.getCargoSpaceId())) {
throw new RuntimeException("货位不存在");
}
String status = stock.getCargoSpaceStatus();
if ("4".equals(status)) {
throw new RuntimeException("当前货位已生成出库作业");
}
if (!"1".equals(status) && !"6".equals(status)) {
throw new RuntimeException("当前货位不是可生成作业的空货位");
}
}
private BasStationPoint validateExitPoint(String exitId, LocationOutboundStock stock)
{
BasStationPoint point = basStationPointService.selectBasStationPointByPointId(exitId);
if (point == null) {
throw new RuntimeException("出库口不存在");
}
if (StringUtils.isEmpty(stock.getWarehouseId()) || !stock.getWarehouseId().equals(point.getWarehouseId())) {
throw new RuntimeException("出库口不属于当前仓库");
}
if (!"2".equals(point.getPointType()) && !"3".equals(point.getPointType())) {
throw new RuntimeException("请选择出库或入出库点位");
}
if (!"0".equals(point.getStatus())) {
throw new RuntimeException("出库口已禁用");
}
String pointStatus = StringUtils.isEmpty(point.getPointStatus()) ? "0" : point.getPointStatus();
if (!"0".equals(pointStatus)) {
throw new RuntimeException("出库口已被分配");
}
if (StringUtils.isNotEmpty(stock.getTrayType()) && StringUtils.isNotEmpty(point.getTrayType())
&& !supportsTrayType(point.getTrayType(), stock.getTrayType())) {
throw new RuntimeException("出库口不支持当前托盘类型");
}
return point;
}
private boolean supportsTrayType(String pointTrayTypes, String trayType)
{
String[] values = pointTrayTypes.split(",");
for (String value : values) {
if (trayType.equalsIgnoreCase(value.trim())) {
return true;
}
}
return false;
}
private MydoJob buildOutboundJob(String jobId, String taskNo, LocationOutboundStock stock, BasStationPoint exitPoint)
{
MydoJob job = new MydoJob();
job.setJobId(jobId);
job.setTaskNo(StringUtils.isNotEmpty(taskNo) ? taskNo : jobId);
job.setInOrOut(20L);
job.setStatus("1");
job.setExecutionPhase("4");
job.setTrayCode(stock.getTrayCode());
job.setWarehouseId(stock.getWarehouseId());
job.setWarehouseName(stock.getWarehouseName());
job.setRoadwayId(stock.getRoadwayId());
job.setCargoSpaceId(stock.getCargoSpaceId());
job.setCargoSpaceName(stock.getCargoSpaceName());
job.setLineType("1");
job.setPriority(10L);
job.setHeight(BigDecimal.ZERO);
job.setWeight(BigDecimal.ZERO);
job.setExitId(exitPoint.getPointId());
copyLoginUser(job);
return job;
}
private MydoJobDetail buildOutboundJobDetail(MydoJob job, String jobDetailId, LocationOutboundStock stock,
BasStockDetail detail, BigDecimal availableQuantity)
{
MydoJobDetail jobDetail = new MydoJobDetail();
jobDetail.setJobDetailId(jobDetailId);
jobDetail.setJobId(job.getJobId());
jobDetail.setStockId(detail.getStockId());
jobDetail.setLotNo(detail.getLotNo());
jobDetail.setInDate(DateUtils.getTime());
jobDetail.setProductId(detail.getProductId());
jobDetail.setProductName(detail.getProductName());
jobDetail.setStockQuantity(availableQuantity);
jobDetail.setOutQuantity(availableQuantity);
jobDetail.setStockinDetailId(detail.getDetailId());
jobDetail.setSupplierId(stock.getSupplierId());
jobDetail.setSupplierName(stock.getSupplierName());
return jobDetail;
}
private BigDecimal availableQuantity(BasStockDetail detail)
{
if (detail.getAvailableQuantity() != null) {
return detail.getAvailableQuantity();
}
BigDecimal quantity = detail.getQuantity() == null ? BigDecimal.ZERO : detail.getQuantity();
BigDecimal outQuantity = detail.getOutQuantity() == null ? BigDecimal.ZERO : detail.getOutQuantity();
return quantity.subtract(outQuantity);
}
private void copyLoginUser(MydoJob job)
{
try {
job.setUserId(SecurityUtils.getUserId());
job.setDeptId(SecurityUtils.getDeptId());
job.setUserName(SecurityUtils.getUsername());
} catch (RuntimeException ignored) {
// External callbacks and focused unit tests can run without login context.
}
}
}

View File

@@ -4,6 +4,7 @@ import java.util.List;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.out.stockout.domain.OutStockoutAssignDTO;
import com.ruoyi.out.stockout.domain.OutStockoutManualAssignDTO;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.annotation.WarehouseScope;
@@ -50,10 +52,10 @@ public class OutStockoutController extends BaseController
{
startPage();
List<OutStockout> list;
if (StringUtils.isEmpty(outStockout.getStatus()) || Convert.toInt(outStockout.getStatus()) < 4){
list = outStockoutService.selectOutStockoutList(outStockout);
}else {
if (isHistoryQuery(outStockout) || (StringUtils.isNotEmpty(outStockout.getStatus()) && Convert.toInt(outStockout.getStatus()) >= 4)){
list = outStockoutService.selectOutStockoutFinishList(outStockout);
}else {
list = outStockoutService.selectOutStockoutList(outStockout);
}
return getDataTable(list);
}
@@ -66,11 +68,23 @@ public class OutStockoutController extends BaseController
@PostMapping("/export")
public void export(HttpServletResponse response, OutStockout outStockout)
{
List<OutStockout> list = outStockoutService.selectOutStockoutList(outStockout);
List<OutStockout> list;
if (isHistoryQuery(outStockout) || (StringUtils.isNotEmpty(outStockout.getStatus()) && Convert.toInt(outStockout.getStatus()) >= 4)) {
list = outStockoutService.selectOutStockoutFinishList(outStockout);
} else {
list = outStockoutService.selectOutStockoutList(outStockout);
}
ExcelUtil<OutStockout> util = new ExcelUtil<OutStockout>(OutStockout.class);
util.exportExcel(response, list, "出库单据主信息数据");
}
private boolean isHistoryQuery(OutStockout outStockout)
{
return outStockout != null
&& outStockout.getParams() != null
&& "true".equalsIgnoreCase(Convert.toStr(outStockout.getParams().get("history")));
}
/**
* 获取出库单据主信息详细信息
*/
@@ -153,9 +167,17 @@ public class OutStockoutController extends BaseController
@PreAuthorize("@ss.hasPermi('out:stockout:autoAssign')")
@Log(title = "出库单据主信息", businessType = BusinessType.UPDATE)
@PutMapping("/autoAssign/{stockoutId}")
public AjaxResult autoAssign(@PathVariable("stockoutId") String stockoutId)
public AjaxResult autoAssign(@PathVariable("stockoutId") String stockoutId,
@RequestBody(required = false) OutStockoutAssignDTO assignDTO,
@RequestParam(value = "exitId", required = false) String exitId)
{
return outStockoutService.autoAssignOutStockout(stockoutId);
if (assignDTO == null) {
assignDTO = new OutStockoutAssignDTO();
}
if (StringUtils.isNotEmpty(exitId)) {
assignDTO.setExitId(exitId);
}
return outStockoutService.autoAssignOutStockout(stockoutId, assignDTO);
}
/**

View File

@@ -0,0 +1,24 @@
package com.ruoyi.out.stockout.domain;
import java.util.Map;
public class OutStockoutAssignDTO {
private String exitId;
private Map<String, String> exitByTrayType;
public String getExitId() {
return exitId;
}
public void setExitId(String exitId) {
this.exitId = exitId;
}
public Map<String, String> getExitByTrayType() {
return exitByTrayType;
}
public void setExitByTrayType(Map<String, String> exitByTrayType) {
this.exitByTrayType = exitByTrayType;
}
}

View File

@@ -1,9 +1,13 @@
package com.ruoyi.out.stockout.domain;
import java.util.List;
import java.util.Map;
public class OutStockoutManualAssignDTO {
private String stockoutDetailId;
private String exitId;
private Map<String, String> exitByTrayType;
private Map<String, String> exitByStockId;
private List<String> stockIdList;
public String getStockoutDetailId() {
@@ -14,6 +18,30 @@ public class OutStockoutManualAssignDTO {
this.stockoutDetailId = stockoutDetailId;
}
public String getExitId() {
return exitId;
}
public void setExitId(String exitId) {
this.exitId = exitId;
}
public Map<String, String> getExitByTrayType() {
return exitByTrayType;
}
public void setExitByTrayType(Map<String, String> exitByTrayType) {
this.exitByTrayType = exitByTrayType;
}
public Map<String, String> getExitByStockId() {
return exitByStockId;
}
public void setExitByStockId(Map<String, String> exitByStockId) {
this.exitByStockId = exitByStockId;
}
public List<String> getStockIdList() {
return stockIdList;
}

View File

@@ -82,4 +82,12 @@ public interface OutStockoutMapper
* @return
*/
List<OutStockout> selectOutStockoutFinishList(OutStockout outStockout);
/**
* 取货无货异常作废最后一个活动作业后,将出库主单退回已审核状态。
*
* @param jobId 作业ID
* @return 结果
*/
int reopenOutStockoutByExceptionJobId(String jobId);
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.out.stockout.service;
import java.util.List;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.out.stockout.domain.OutStockoutAssignDTO;
import com.ruoyi.out.stockout.domain.OutStockout;
import com.ruoyi.out.stockout.domain.OutStockoutManualAssignDTO;
import com.ruoyi.out.stockout.domain.OutStockoutSaveDTO;
@@ -90,9 +91,19 @@ public interface IOutStockoutService
* 自动分配出库单据
*
* @param stockoutId 出库单据主键
* @param exitId 出库口点位
* @return 结果
*/
public AjaxResult autoAssignOutStockout(String stockoutId);
public AjaxResult autoAssignOutStockout(String stockoutId, String exitId);
/**
* 自动分配出库单据
*
* @param stockoutId 出库单据主键
* @param assignDTO 出库口分配信息
* @return 结果
*/
public AjaxResult autoAssignOutStockout(String stockoutId, OutStockoutAssignDTO assignDTO);
/**
* 手动分配出库单据

View File

@@ -1,14 +1,18 @@
package com.ruoyi.out.stockout.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.ruoyi.common.annotation.WarehouseScope;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.out.stockout.domain.OutStockoutAssignDTO;
import com.ruoyi.out.stockout.domain.OutStockoutManualAssignDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -17,6 +21,7 @@ import com.ruoyi.bas.id.job.service.JobIdService;
import com.ruoyi.common.OutStockoutStatusEnum;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import com.ruoyi.out.stockout.mapper.OutStockoutMapper;
import com.ruoyi.out.stockout.domain.OutStockout;
@@ -49,6 +54,9 @@ public class OutStockoutServiceImpl implements IOutStockoutService
@Autowired
private IMydoJobService mydoJobService;
@Autowired
private BasCargoSpaceMapper basCargoSpaceMapper;
/**
* 查询出库单据主信息
*/
@@ -159,16 +167,47 @@ public class OutStockoutServiceImpl implements IOutStockoutService
* 自动分配出库单据
*/
@Override
public AjaxResult autoAssignOutStockout(String stockoutId)
public AjaxResult autoAssignOutStockout(String stockoutId, String exitId)
{
OutStockoutAssignDTO assignDTO = new OutStockoutAssignDTO();
assignDTO.setExitId(exitId);
return autoAssignOutStockout(stockoutId, assignDTO);
}
/**
* 自动分配出库单据
*/
@Override
public AjaxResult autoAssignOutStockout(String stockoutId, OutStockoutAssignDTO assignDTO)
{
if (assignDTO == null) {
assignDTO = new OutStockoutAssignDTO();
}
StringBuilder str = new StringBuilder();
List<OutStockoutDetail> details = outStockoutDetailMapper.selectOutStockoutDetailListByStockoutId(stockoutId);
if (details == null || details.isEmpty()) {
return AjaxResult.error("出库单据无明细");
}
List<String> allocatedDetailIds = new ArrayList<>();
for (OutStockoutDetail detail : details) {
String exitId = resolveExitId(assignDTO.getExitId(), assignDTO.getExitByTrayType(), detail.getTrayType());
if (StringUtils.isEmpty(exitId)) {
return AjaxResult.error(buildMissingExitMessage(detail.getTrayType()));
}
HashMap<String, Object> map = new HashMap<>();
map.put("v_stockout_detail_id", detail.getStockoutDetailId());
map.put("v_exit_id", exitId);
outStockoutMapper.autoAssignOutStockoutByProc(map);
str.append(detail.getStockoutDetailId()).append(Convert.toStr(map.get("v_resultString"))).append("|");
AjaxResult pushResult = pushPendingOutJobs(detail.getStockoutDetailId());
String resultString = Convert.toStr(map.get("v_resultString"));
str.append(detail.getStockoutDetailId()).append(resultString).append("|");
if (!isProcedureSuccess(map)) {
return AjaxResult.error(detail.getStockoutDetailId() + resultString);
}
basCargoSpaceMapper.markOutboundAssignedByStockoutDetailId(detail.getStockoutDetailId());
allocatedDetailIds.add(detail.getStockoutDetailId());
}
for (String detailId : allocatedDetailIds) {
AjaxResult pushResult = pushPendingOutJobs(detailId);
if (pushResult.isError()) {
return pushResult;
}
@@ -182,14 +221,35 @@ public class OutStockoutServiceImpl implements IOutStockoutService
@Override
public AjaxResult manualAssignOutStockout(OutStockoutManualAssignDTO outStockoutManualAssignDTO)
{
if (outStockoutManualAssignDTO == null) {
return AjaxResult.error("请选择出库口");
}
if (outStockoutManualAssignDTO.getStockIdList() == null || outStockoutManualAssignDTO.getStockIdList().isEmpty()) {
return AjaxResult.error("请选择要分配的库存");
}
StringBuilder str = new StringBuilder();
List<String> allocatedDetailIds = new ArrayList<>();
for (String stockId : outStockoutManualAssignDTO.getStockIdList()) {
String exitId = resolveManualExitId(outStockoutManualAssignDTO, stockId);
if (StringUtils.isEmpty(exitId)) {
return AjaxResult.error("请选择出库口");
}
HashMap<String, Object> map = new HashMap<>();
map.put("v_stockout_detail_id", outStockoutManualAssignDTO.getStockoutDetailId());
map.put("v_stock_id", stockId);
map.put("v_exit_id", exitId);
outStockoutMapper.manualAssignOutStockoutByProc(map);
str.append(outStockoutManualAssignDTO.getStockoutDetailId()).append("|").append(stockId).append(Convert.toStr(map.get("v_resultString"))).append("|");
AjaxResult pushResult = pushPendingOutJobs(outStockoutManualAssignDTO.getStockoutDetailId());
String resultString = Convert.toStr(map.get("v_resultString"));
str.append(outStockoutManualAssignDTO.getStockoutDetailId()).append("|").append(stockId).append(resultString).append("|");
if (!isProcedureSuccess(map)) {
return AjaxResult.error(outStockoutManualAssignDTO.getStockoutDetailId() + "|" + stockId + resultString);
}
basCargoSpaceMapper.markOutboundAssignedByStockRef(stockId);
basCargoSpaceMapper.markOutboundAssignedByStockoutDetailId(outStockoutManualAssignDTO.getStockoutDetailId());
allocatedDetailIds.add(outStockoutManualAssignDTO.getStockoutDetailId());
}
for (String detailId : allocatedDetailIds) {
AjaxResult pushResult = pushPendingOutJobs(detailId);
if (pushResult.isError()) {
return pushResult;
}
@@ -235,6 +295,38 @@ public class OutStockoutServiceImpl implements IOutStockoutService
return saveDTO.getStockoutId();
}
private boolean isProcedureSuccess(Map<String, Object> map)
{
return "10".equals(Convert.toStr(map.get("v_result")));
}
private String resolveManualExitId(OutStockoutManualAssignDTO assignDTO, String stockId)
{
if (assignDTO.getExitByStockId() != null && assignDTO.getExitByStockId().containsKey(stockId)) {
return assignDTO.getExitByStockId().get(stockId);
}
return resolveExitId(assignDTO.getExitId(), assignDTO.getExitByTrayType(), null);
}
private String resolveExitId(String defaultExitId, Map<String, String> exitByTrayType, String trayType)
{
if (StringUtils.isNotEmpty(trayType) && exitByTrayType != null && exitByTrayType.containsKey(trayType)) {
return exitByTrayType.get(trayType);
}
if (StringUtils.isEmpty(defaultExitId) && exitByTrayType != null && exitByTrayType.size() == 1) {
return exitByTrayType.values().iterator().next();
}
return defaultExitId;
}
private String buildMissingExitMessage(String trayType)
{
if (StringUtils.isNotEmpty(trayType)) {
return "请选择" + trayType + "类型托盘的出库口";
}
return "请选择出库口";
}
@Override
@WarehouseScope
public List<OutStockout> selectOutStockoutFinishList(OutStockout outStockout) {

View File

@@ -101,6 +101,9 @@ public class OutStockoutDetail extends BaseEntity
/** 分配库存明细ID */
private String stockDetailId;
/** 托盘类型 */
private String trayType;
public void setStatus(String status)
{
this.status = status;
@@ -131,6 +134,16 @@ public class OutStockoutDetail extends BaseEntity
this.stockDetailId = stockDetailId;
}
public String getTrayType()
{
return trayType;
}
public void setTrayType(String trayType)
{
this.trayType = trayType;
}
public void setStockoutDetailId(String stockoutDetailId)
{
this.stockoutDetailId = stockoutDetailId;
@@ -345,6 +358,7 @@ public class OutStockoutDetail extends BaseEntity
.append("remar", getRemar())
.append("stockId", getStockId())
.append("stockDetailId", getStockDetailId())
.append("trayType", getTrayType())
.append("remark", getRemark())
.toString();
}

View File

@@ -72,4 +72,12 @@ public interface OutStockoutDetailMapper
* @return
*/
List<OutStockoutDetail> selectOutStockoutDetailFinishList(OutStockoutDetail outStockoutDetail);
/**
* 按取货无货作业回滚出库子单分配数量。
*
* @param jobId 作业ID
* @return 结果
*/
int rollbackAllotByJobId(String jobId);
}

View File

@@ -12,6 +12,7 @@ import com.ruoyi.in.stockin.domain.InStockin;
import com.ruoyi.in.sotckindetail.domain.InStockinDetail;
import com.ruoyi.pda.domain.AddInJobRequest;
import com.ruoyi.pda.domain.BindTrayRequest;
import com.ruoyi.pda.domain.EmptyTrayOutboundRequest;
import com.ruoyi.pda.service.PdaService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
@@ -151,6 +152,21 @@ public class PdaController extends BaseController {
}
}
/**
* PDA 空托入库:选择仓库、扫描托盘号、选择入库点位后直接生成空托入库作业。
*/
@PostMapping("/emptyTrayInbound")
@PreAuthorize("@ss.hasPermi('in:job:add')")
@Log(title = "PDA空托入库并生成作业", businessType = BusinessType.INSERT)
public AjaxResult emptyTrayInbound(@Valid @RequestBody AddInJobRequest request) {
try {
String res = pdaService.addEmptyTrayInboundJob(request);
return AjaxResult.success(res);
} catch (RuntimeException e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* 入库绑盘-查询物料信息
* @param detailId
@@ -176,6 +192,47 @@ public class PdaController extends BaseController {
return AjaxResult.success(pdaService.outboundTaskList(query));
}
/**
* PDA 查询当前仓库库内空托按条码头分组的数量。
*/
@GetMapping("/emptyTrayOutbound/count")
@PreAuthorize("@ss.hasPermi('out:job:list')")
public AjaxResult emptyTrayOutboundCount(String warehouseId) {
try {
return AjaxResult.success(pdaService.emptyTrayOutboundCount(warehouseId));
} catch (RuntimeException e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* PDA 库存查询:扫描托盘码后查看托盘绑定的库存物料。
*/
@GetMapping("/trayStock")
@PreAuthorize("@ss.hasPermi('bas:stock:list')")
public AjaxResult trayStock(String trayCode) {
try {
return AjaxResult.success(pdaService.trayStock(trayCode));
} catch (RuntimeException e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* PDA 空托出库:按仓库、条码头类型和数量生成空托出库作业并下发 WCS。
*/
@PostMapping("/emptyTrayOutbound")
@PreAuthorize("@ss.hasPermi('out:job:list')")
@Log(title = "PDA空托出库并下发WCS", businessType = BusinessType.INSERT)
public AjaxResult emptyTrayOutbound(@Valid @RequestBody EmptyTrayOutboundRequest request) {
try {
String res = pdaService.addEmptyTrayOutboundJob(request);
return AjaxResult.success(res);
} catch (RuntimeException e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* PDA 扫码查询出库任务明细。
*/
@@ -204,19 +261,4 @@ public class PdaController extends BaseController {
}
}
/**
* PDA 出库异常提交。
*/
@PostMapping("/outboundException")
@PreAuthorize("@ss.hasPermi('out:job:complete')")
@Log(title = "PDA出库异常", businessType = BusinessType.UPDATE)
public AjaxResult outboundException(@RequestBody Map<String, Object> data) {
try {
pdaService.submitOutboundException(data);
return AjaxResult.success();
} catch (RuntimeException e) {
return AjaxResult.error(e.getMessage());
}
}
}

View File

@@ -0,0 +1,40 @@
package com.ruoyi.pda.domain;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class EmptyTrayOutboundRequest {
@NotNull
private String warehouseId;
@NotBlank
private String barcodePrefix;
@NotNull
private Integer quantity;
public String getWarehouseId() {
return warehouseId;
}
public void setWarehouseId(String warehouseId) {
this.warehouseId = warehouseId;
}
public String getBarcodePrefix() {
return barcodePrefix;
}
public void setBarcodePrefix(String barcodePrefix) {
this.barcodePrefix = barcodePrefix;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
}

View File

@@ -3,6 +3,8 @@ package com.ruoyi.pda.mapper;
import java.util.HashMap;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface PdaMapper {
void bindTray(HashMap<String, Object> map);
@@ -25,7 +27,7 @@ public interface PdaMapper {
HashMap<String, Object> selectAvailableInboundRoadway(HashMap<String, Object> map);
/**
* WCS 请求货位时,按已分配巷道从下到上、前到后锁定一个可入库空货位。
* WCS 请求货位时,优先选择已分配巷道内空货位最多的排,再锁定一个可入库空货位。
*
* @param map 仓库、货位类型、托盘号、巷道
* @return 锁定行数
@@ -57,4 +59,44 @@ public interface PdaMapper {
* PDA 扫码查询出库任务明细。
*/
HashMap<String, Object> selectOutboundDetail(HashMap<String, Object> map);
/**
* 查询出库托盘上的作业物料明细。
*/
List<HashMap<String, Object>> selectOutboundTrayMaterials(HashMap<String, Object> map);
/**
* 查询出库确认后托盘上仍有库存的物料。
*/
List<HashMap<String, Object>> selectResidualTrayMaterials(HashMap<String, Object> map);
/**
* PDA 库存查询:按托盘码查询托盘绑定的库存物料。
*/
List<HashMap<String, Object>> selectTrayStockMaterials(@Param("trayCode") String trayCode);
/**
* 按条码格式前缀查询指定仓库可出库空托数量。
*/
List<HashMap<String, Object>> selectAvailableEmptyTrayTypeCounts(@Param("warehouseId") String warehouseId);
/**
* 查询指定仓库、指定条码格式前缀可出库空托数量。
*/
int countAvailableEmptyTrays(HashMap<String, Object> map);
/**
* 锁定读取指定仓库可出库空托库存明细。
*/
List<HashMap<String, Object>> selectAvailableEmptyTrayStocks(HashMap<String, Object> map);
/**
* 预占空托库存明细,避免重复生成出库作业。
*/
int reserveEmptyTrayStockDetail(HashMap<String, Object> map);
/**
* 将空托所在货位标记为出库锁定。
*/
int lockEmptyTrayCargoSpace(HashMap<String, Object> map);
}

View File

@@ -6,7 +6,9 @@ import com.ruoyi.in.stockin.domain.InStockin;
import com.ruoyi.in.sotckindetail.domain.InStockinDetail;
import com.ruoyi.pda.domain.AddInJobRequest;
import com.ruoyi.pda.domain.BindTrayRequest;
import com.ruoyi.pda.domain.EmptyTrayOutboundRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -71,6 +73,38 @@ public interface PdaService {
*/
String addInJob(AddInJobRequest request);
/**
* PDA 生成空托入库作业。
*
* @param request 仓库、托盘号和入库点位
* @return 作业生成和 WCS 下发结果
*/
String addEmptyTrayInboundJob(AddInJobRequest request);
/**
* 查询指定仓库库内可出库空托按条码头分组的数量。
*
* @param warehouseId 仓库编码
* @return 空托类型数量列表
*/
List<HashMap<String, Object>> emptyTrayOutboundCount(String warehouseId);
/**
* PDA 库存查询:按托盘码查询托盘绑定的库存物料。
*
* @param trayCode 托盘码
* @return 库存物料明细
*/
List<HashMap<String, Object>> trayStock(String trayCode);
/**
* PDA 生成空托出库作业并下发 WCS。
*
* @param request 仓库、条码头类型和出库数量
* @return 作业生成和 WCS 下发结果
*/
String addEmptyTrayOutboundJob(EmptyTrayOutboundRequest request);
/**
* WCS 到达入库口请求货位时,按作业已分配巷道锁定一个具体货位。
*
@@ -112,10 +146,4 @@ public interface PdaService {
*/
void confirmOutbound(Map<String, Object> data);
/**
* PDA 提交出库异常。
*
* @param data 异常数据
*/
void submitOutboundException(Map<String, Object> data);
}

View File

@@ -6,6 +6,8 @@ import com.ruoyi.bas.warehouse.domain.BasWarehouse;
import com.ruoyi.bas.warehouse.service.IBasWarehouseService;
import com.ruoyi.api.client.WcsClient;
import com.ruoyi.api.domain.ApiResponse;
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.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
@@ -20,6 +22,7 @@ import com.ruoyi.in.sotckindetail.domain.InStockinDetail;
import com.ruoyi.in.sotckindetail.mapper.InStockinDetailMapper;
import com.ruoyi.pda.domain.AddInJobRequest;
import com.ruoyi.pda.domain.BindTrayRequest;
import com.ruoyi.pda.domain.EmptyTrayOutboundRequest;
import com.ruoyi.pda.mapper.PdaMapper;
import com.ruoyi.pda.service.PdaService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,6 +44,13 @@ import java.util.Map;
*/
@Service
public class PdaServiceImpl implements PdaService {
private static final String EMPTY_TRAY_PRODUCT_ID = "KTP0001";
private static final String EMPTY_TRAY_PRODUCT_NAME = "空容器";
private static final long EMPTY_TRAY_IN_JOB_TYPE = 3L;
private static final long EMPTY_TRAY_OUT_JOB_TYPE = 22L;
private static final String EMPTY_TRAY_IN_TASK_NO_TYPE = "6";
private static final String EMPTY_TRAY_OUT_TASK_NO_TYPE = "20";
private static final BigDecimal ONE_EMPTY_TRAY = BigDecimal.ONE;
@Autowired
private PdaMapper pdaMapper;
@@ -66,6 +76,12 @@ public class PdaServiceImpl implements PdaService {
@Autowired
private WcsClient wcsClient;
@Autowired
private JobIdService jobIdService;
@Autowired
private TaskNoService taskNoService;
// ─────────────────────────────────────────────
// 入库待处理任务列表
// ─────────────────────────────────────────────
@@ -193,10 +209,18 @@ public class PdaServiceImpl implements PdaService {
// 普通入库不使用 target_* 字段;生成作业阶段只写入口点位和作业巷道,不锁定具体货位。
job.setEntranceId(stationPoint.getPointId());
job.setRoadwayId(stationPoint.getRoadwayId());
BasWarehouse warehouse = basWarehouseService.selectBasWarehouseByWarehouseId(request.getWarehouseId());
if (warehouse != null) {
job.setWarehouseName(warehouse.getWarehouseName());
}
job.setStatus("1");
job.setExecutionPhase("0");
// updateMydoJob 只更新非 null 字段,这里使用空字符串覆盖存储过程可能写入的作业货位。
job.setCargoSpaceId("");
job.setCargoSpaceName("");
mydoJobService.updateMydoJob(job);
if (mydoJobService.updateMydoJob(job) <= 0) {
throw new RuntimeException("入库作业初始化状态更新失败");
}
MydoJobDetail query = new MydoJobDetail();
query.setJobId(job.getJobId());
@@ -212,9 +236,241 @@ public class PdaServiceImpl implements PdaService {
String message = wcsResponse == null ? "WCS接口无返回" : wcsResponse.getMessage();
throw new RuntimeException("WCS入库作业下发失败:" + message);
}
markInJobDispatchedToWcs(job.getJobId());
return resultMsg + ";WCS入库作业下发成功:" + Convert.toStr(wcsResponse.getData());
}
@Override
@Transactional(rollbackFor = Exception.class)
public String addEmptyTrayInboundJob(AddInJobRequest request) {
checkAddInJobRequest(request);
BasStationPoint stationPoint = validateInboundStationPoint(request.getStationPointId(), request.getWarehouseId(), request.getTrayCode());
ensureRoadwayHasEmptyCargoSpace(request.getTrayCode(), request.getWarehouseId(), stationPoint.getRoadwayId());
int occupyRows = basStationPointService.occupyBasStationPoint(stationPoint.getPointId());
if (occupyRows <= 0) {
throw new RuntimeException("入库点位已被分配,请重新选择点位");
}
String jobId = jobIdService.getId("RK");
String taskNo = taskNoService.getTaskNo(EMPTY_TRAY_IN_TASK_NO_TYPE);
MydoJob job = new MydoJob();
job.setJobId(jobId);
job.setTaskNo(StringUtils.hasText(taskNo) ? taskNo : jobId);
job.setInOrOut(EMPTY_TRAY_IN_JOB_TYPE);
job.setStatus("1");
job.setExecutionPhase("0");
job.setTrayCode(request.getTrayCode());
job.setWarehouseId(request.getWarehouseId());
BasWarehouse warehouse = basWarehouseService.selectBasWarehouseByWarehouseId(request.getWarehouseId());
if (warehouse != null) {
job.setWarehouseName(warehouse.getWarehouseName());
}
job.setEntranceId(stationPoint.getPointId());
job.setRoadwayId(stationPoint.getRoadwayId());
job.setPriority(10L);
job.setHeight(BigDecimal.ZERO);
job.setWeight(BigDecimal.ZERO);
copyLoginUser(job);
if (mydoJobService.insertMydoJob(job) <= 0) {
throw new RuntimeException("空托入库作业生成失败");
}
MydoJobDetail detail = new MydoJobDetail();
detail.setJobDetailId(jobId + "-1");
detail.setJobId(jobId);
detail.setProductId(EMPTY_TRAY_PRODUCT_ID);
detail.setProductName(EMPTY_TRAY_PRODUCT_NAME);
detail.setInQuantity(ONE_EMPTY_TRAY);
if (mydoJobDetailService.insertMydoJobDetail(detail) <= 0) {
throw new RuntimeException("空托入库作业明细生成失败");
}
ApiResponse<String> wcsResponse = wcsClient.sendPutawayTask(job);
if (wcsResponse == null || !wcsResponse.isSuccess()) {
String message = wcsResponse == null ? "WCS接口无返回" : wcsResponse.getMessage();
throw new RuntimeException("WCS入库作业下发失败:" + message);
}
markInJobDispatchedToWcs(job.getJobId());
return "空托入库作业生成成功:" + job.getJobId() + ";WCS入库作业下发成功:" + Convert.toStr(wcsResponse.getData());
}
@Override
public List<HashMap<String, Object>> emptyTrayOutboundCount(String warehouseId) {
if (!StringUtils.hasText(warehouseId)) {
throw new RuntimeException("仓库编码不能为空");
}
return pdaMapper.selectAvailableEmptyTrayTypeCounts(warehouseId);
}
@Override
public List<HashMap<String, Object>> trayStock(String trayCode) {
if (!StringUtils.hasText(trayCode)) {
throw new RuntimeException("托盘码不能为空");
}
return pdaMapper.selectTrayStockMaterials(trayCode.trim());
}
@Override
@Transactional(rollbackFor = Exception.class)
public String addEmptyTrayOutboundJob(EmptyTrayOutboundRequest request) {
checkEmptyTrayOutboundRequest(request);
int quantity = request.getQuantity();
HashMap<String, Object> countQuery = new HashMap<>();
countQuery.put("warehouseId", request.getWarehouseId());
countQuery.put("barcodePrefix", request.getBarcodePrefix());
int availableCount = pdaMapper.countAvailableEmptyTrays(countQuery);
if (availableCount < quantity) {
throw new RuntimeException("库内空托盘数量不足,当前可用:" + availableCount);
}
BasStationPoint exitPoint = selectDefaultOutboundStationPoint(request.getWarehouseId());
HashMap<String, Object> stockQuery = new HashMap<>();
stockQuery.put("warehouseId", request.getWarehouseId());
stockQuery.put("barcodePrefix", request.getBarcodePrefix());
stockQuery.put("limit", quantity);
List<HashMap<String, Object>> emptyTrayStocks = pdaMapper.selectAvailableEmptyTrayStocks(stockQuery);
if (emptyTrayStocks == null || emptyTrayStocks.size() < quantity) {
throw new RuntimeException("库内空托盘数量不足,请刷新后重试");
}
BasWarehouse warehouse = basWarehouseService.selectBasWarehouseByWarehouseId(request.getWarehouseId());
StringBuilder result = new StringBuilder("空托出库作业生成成功:");
for (int i = 0; i < quantity; i++) {
HashMap<String, Object> stock = emptyTrayStocks.get(i);
MydoJob job = buildEmptyTrayOutboundJob(request.getWarehouseId(), warehouse, exitPoint, stock);
if (mydoJobService.insertMydoJob(job) <= 0) {
throw new RuntimeException("空托出库作业生成失败");
}
MydoJobDetail detail = buildEmptyTrayOutboundDetail(job, stock);
if (mydoJobDetailService.insertMydoJobDetail(detail) <= 0) {
throw new RuntimeException("空托出库作业明细生成失败");
}
HashMap<String, Object> reserveMap = new HashMap<>();
reserveMap.put("stockDetailId", detail.getStockinDetailId());
reserveMap.put("stockId", detail.getStockId());
reserveMap.put("jobDetailId", detail.getJobDetailId());
reserveMap.put("quantity", detail.getOutQuantity());
if (pdaMapper.reserveEmptyTrayStockDetail(reserveMap) <= 0) {
throw new RuntimeException("空托库存已被占用,请刷新后重试");
}
HashMap<String, Object> cargoMap = new HashMap<>();
cargoMap.put("stockId", detail.getStockId());
cargoMap.put("cargoSpaceId", job.getCargoSpaceId());
pdaMapper.lockEmptyTrayCargoSpace(cargoMap);
AjaxResult pushResult = mydoJobService.pushOutJobToWcs(job.getJobId());
if (pushResult == null || pushResult.isError()) {
throw new RuntimeException(pushResult == null ? "WCS出库作业下发失败" : Convert.toStr(pushResult.get(AjaxResult.MSG_TAG)));
}
result.append(job.getJobId()).append("|");
}
return result.toString();
}
private void markInJobDispatchedToWcs(String jobId) {
MydoJob updateJob = new MydoJob();
updateJob.setJobId(jobId);
updateJob.setStatus("3");
updateJob.setExecutionPhase("1");
updateJob.setStartTime(DateUtils.getTime());
if (mydoJobService.updateMydoJob(updateJob) <= 0) {
throw new RuntimeException("入库作业WCS下发状态更新失败");
}
}
private void checkEmptyTrayOutboundRequest(EmptyTrayOutboundRequest request) {
if (request == null || !StringUtils.hasText(request.getWarehouseId())) {
throw new RuntimeException("仓库编码不能为空");
}
if (request.getQuantity() == null || request.getQuantity() <= 0) {
throw new RuntimeException("出库数量必须大于0");
}
if (!StringUtils.hasText(request.getBarcodePrefix())) {
throw new RuntimeException("空托盘类型不能为空");
}
}
private BasStationPoint selectDefaultOutboundStationPoint(String warehouseId) {
BasStationPoint query = new BasStationPoint();
query.setWarehouseId(warehouseId);
query.setStatus("0");
List<BasStationPoint> pointList = basStationPointService.selectBasStationPointList(query);
if (pointList != null) {
for (BasStationPoint point : pointList) {
if (point != null && ("2".equals(point.getPointType()) || "3".equals(point.getPointType()))) {
return point;
}
}
}
throw new RuntimeException("当前仓库未配置可用出库点位");
}
private MydoJob buildEmptyTrayOutboundJob(String warehouseId, BasWarehouse warehouse, BasStationPoint exitPoint, HashMap<String, Object> stock) {
String jobId = jobIdService.getId("CK");
String taskNo = taskNoService.getTaskNo(EMPTY_TRAY_OUT_TASK_NO_TYPE);
MydoJob job = new MydoJob();
job.setJobId(jobId);
job.setTaskNo(StringUtils.hasText(taskNo) ? taskNo : jobId);
job.setInOrOut(EMPTY_TRAY_OUT_JOB_TYPE);
job.setStatus("1");
job.setExecutionPhase("0");
job.setTrayCode(mapString(stock, "trayCode"));
job.setWarehouseId(warehouseId);
String warehouseName = mapString(stock, "warehouseName");
if (!StringUtils.hasText(warehouseName) && warehouse != null) {
warehouseName = warehouse.getWarehouseName();
}
job.setWarehouseName(warehouseName);
job.setRoadwayId(mapString(stock, "roadwayId"));
job.setCargoSpaceId(mapString(stock, "cargoSpaceId"));
job.setCargoSpaceName(mapString(stock, "cargoSpaceName"));
job.setLineType("1");
job.setPriority(10L);
job.setHeight(BigDecimal.ZERO);
job.setWeight(BigDecimal.ZERO);
job.setExitId(exitPoint.getPointId());
copyLoginUser(job);
return job;
}
private MydoJobDetail buildEmptyTrayOutboundDetail(MydoJob job, HashMap<String, Object> stock) {
MydoJobDetail detail = new MydoJobDetail();
detail.setJobDetailId(job.getJobId() + "-1");
detail.setJobId(job.getJobId());
detail.setStockId(mapString(stock, "stockId"));
detail.setLotNo(mapString(stock, "lotNo"));
detail.setInDate(DateUtils.getTime());
detail.setProductId(EMPTY_TRAY_PRODUCT_ID);
detail.setProductName(EMPTY_TRAY_PRODUCT_NAME);
detail.setStockQuantity(toBigDecimal(mapValue(stock, "availableQuantity")));
detail.setOutQuantity(ONE_EMPTY_TRAY);
detail.setStockinDetailId(mapString(stock, "stockDetailId"));
detail.setSupplierId(mapString(stock, "supplierId"));
detail.setSupplierName(mapString(stock, "supplierName"));
return detail;
}
private Object mapValue(Map<String, Object> map, String key) {
if (map == null || key == null) {
return null;
}
if (map.containsKey(key)) {
return map.get(key);
}
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (key.equalsIgnoreCase(entry.getKey())) {
return entry.getValue();
}
}
return null;
}
private String mapString(Map<String, Object> map, String key) {
return Convert.toStr(mapValue(map, key));
}
private void checkBindTrayRequest(BindTrayRequest request) {
if (request == null || !StringUtils.hasText(request.getTrayCode())) {
throw new RuntimeException("托盘号不能为空");
@@ -407,6 +663,10 @@ public class PdaServiceImpl implements PdaService {
if (detail == null || detail.isEmpty()) {
throw new RuntimeException("未找到对应出库任务");
}
HashMap<String, Object> materialQuery = new HashMap<>();
materialQuery.put("jobId", detail.get("jobId"));
List<HashMap<String, Object>> materials = pdaMapper.selectOutboundTrayMaterials(materialQuery);
detail.put("materials", materials == null ? new ArrayList<>() : materials);
return detail;
}
@@ -415,22 +675,84 @@ public class PdaServiceImpl implements PdaService {
public void confirmOutbound(Map<String, Object> data) {
Map<String, Object> detail = outboundDetail(data);
BigDecimal expectedQuantity = toBigDecimal(detail.get("quantity"));
BigDecimal actualQuantity = toBigDecimal(data == null ? null : data.get("quantity"));
BigDecimal actualQuantity = applyOutboundMaterialQuantities(data, detail);
if (actualQuantity.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("实际数量必须大于0");
}
if (actualQuantity.compareTo(expectedQuantity) != 0) {
throw new RuntimeException("实际数量必须等于出库任务数量");
}
AjaxResult result = mydoJobService.completeJob(Convert.toStr(detail.get("jobId")), "2");
String jobId = Convert.toStr(detail.get("jobId"));
MydoJob outboundJob = mydoJobService.selectMydoJobByJobId(jobId);
AjaxResult result = mydoJobService.completeJob(jobId, "2");
if (result == null || result.isError()) {
throw new RuntimeException(result == null ? "出库确认失败" : Convert.toStr(result.get(AjaxResult.MSG_TAG)));
}
createReturnInboundJobIfNeeded(outboundJob);
}
@Override
public void submitOutboundException(Map<String, Object> data) {
outboundDetail(data);
@SuppressWarnings("unchecked")
private BigDecimal applyOutboundMaterialQuantities(Map<String, Object> data, Map<String, Object> detail) {
Object materialPayload = data == null ? null : data.get("materials");
if (!(materialPayload instanceof List<?> requestMaterials) || requestMaterials.isEmpty()) {
return toBigDecimal(data == null ? null : data.get("quantity"));
}
List<HashMap<String, Object>> jobMaterials = detail.get("materials") instanceof List<?>
? (List<HashMap<String, Object>>) detail.get("materials")
: new ArrayList<>();
if (jobMaterials.isEmpty()) {
throw new RuntimeException("出库作业无物料明细");
}
BigDecimal totalQuantity = BigDecimal.ZERO;
for (Object item : requestMaterials) {
if (!(item instanceof Map<?, ?>)) {
throw new RuntimeException("出库物料确认明细格式错误");
}
Map<String, Object> requestMaterial = (Map<String, Object>) item;
String jobDetailId = mapString(requestMaterial, "jobDetailId");
String stockDetailId = mapString(requestMaterial, "stockDetailId");
HashMap<String, Object> jobMaterial = findOutboundJobMaterial(jobMaterials, jobDetailId, stockDetailId);
if (jobMaterial == null) {
throw new RuntimeException("出库物料确认明细不存在");
}
BigDecimal quantity = toBigDecimal(firstNotBlank(mapValue(requestMaterial, "actualQuantity"), mapValue(requestMaterial, "quantity")));
if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("物料实际数量必须大于0");
}
BigDecimal stockQuantity = toBigDecimal(mapValue(jobMaterial, "stockQuantity"));
if (stockQuantity.compareTo(BigDecimal.ZERO) > 0 && quantity.compareTo(stockQuantity) > 0) {
throw new RuntimeException("物料实际数量不能大于库存数量");
}
MydoJobDetail updateDetail = new MydoJobDetail();
updateDetail.setJobDetailId(mapString(jobMaterial, "jobDetailId"));
updateDetail.setOutQuantity(quantity);
if (mydoJobDetailService.updateMydoJobDetail(updateDetail) <= 0) {
throw new RuntimeException("出库物料确认数量更新失败");
}
totalQuantity = totalQuantity.add(quantity);
}
return totalQuantity;
}
private HashMap<String, Object> findOutboundJobMaterial(List<HashMap<String, Object>> jobMaterials, String jobDetailId, String stockDetailId) {
for (HashMap<String, Object> material : jobMaterials) {
if (StringUtils.hasText(jobDetailId) && jobDetailId.equals(mapString(material, "jobDetailId"))) {
return material;
}
if (StringUtils.hasText(stockDetailId) && stockDetailId.equals(mapString(material, "stockDetailId"))) {
return material;
}
}
return null;
}
private Object firstNotBlank(Object first, Object second) {
return StringUtils.hasText(Convert.toStr(first)) ? first : second;
}
private BigDecimal toBigDecimal(Object value) {
@@ -440,6 +762,84 @@ public class PdaServiceImpl implements PdaService {
return new BigDecimal(Convert.toStr(value));
}
private void createReturnInboundJobIfNeeded(MydoJob outboundJob) {
if (outboundJob == null || !StringUtils.hasText(outboundJob.getTrayCode()) || !StringUtils.hasText(outboundJob.getWarehouseId())) {
return;
}
HashMap<String, Object> residualQuery = new HashMap<>();
residualQuery.put("trayCode", outboundJob.getTrayCode());
residualQuery.put("warehouseId", outboundJob.getWarehouseId());
List<HashMap<String, Object>> residualMaterials = pdaMapper.selectResidualTrayMaterials(residualQuery);
if (residualMaterials == null || residualMaterials.isEmpty()) {
return;
}
String returnJobId = jobIdService.getId("HL");
String returnTaskNo = taskNoService.getTaskNo("5");
MydoJob returnJob = new MydoJob();
returnJob.setJobId(returnJobId);
returnJob.setTaskNo(StringUtils.hasText(returnTaskNo) ? returnTaskNo : returnJobId);
returnJob.setInOrOut(5L);
returnJob.setStatus("1");
returnJob.setExecutionPhase("0");
returnJob.setTrayCode(outboundJob.getTrayCode());
returnJob.setWarehouseId(outboundJob.getWarehouseId());
returnJob.setWarehouseName(outboundJob.getWarehouseName());
returnJob.setEntranceId(outboundJob.getExitId());
returnJob.setPriority(10L);
returnJob.setHeight(BigDecimal.ZERO);
returnJob.setWeight(BigDecimal.ZERO);
copyLoginUser(returnJob);
if (StringUtils.hasText(outboundJob.getExitId())) {
BasStationPoint exitPoint = basStationPointService.selectBasStationPointByPointId(outboundJob.getExitId());
if (exitPoint != null) {
returnJob.setRoadwayId(exitPoint.getRoadwayId());
}
}
mydoJobService.insertMydoJob(returnJob);
int index = 1;
for (HashMap<String, Object> material : residualMaterials) {
BigDecimal quantity = toBigDecimal(material.get("quantity"));
if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
MydoJobDetail returnDetail = new MydoJobDetail();
returnDetail.setJobDetailId(returnJobId + "-" + index++);
returnDetail.setJobId(returnJobId);
returnDetail.setStockId(Convert.toStr(material.get("stockId")));
returnDetail.setLotNo(Convert.toStr(material.get("lotNo")));
returnDetail.setInDate(DateUtils.getTime());
returnDetail.setInOrOutId(outboundJob.getJobId());
returnDetail.setInOrOutDId(Convert.toStr(material.get("stockDetailId")));
returnDetail.setProductId(Convert.toStr(material.get("productId")));
returnDetail.setProductName(Convert.toStr(material.get("productName")));
returnDetail.setInQuantity(quantity);
returnDetail.setStockQuantity(quantity);
returnDetail.setStockinDetailId(Convert.toStr(material.get("stockDetailId")));
returnDetail.setSupplierId(Convert.toStr(material.get("supplierId")));
returnDetail.setSupplierName(Convert.toStr(material.get("supplierName")));
mydoJobDetailService.insertMydoJobDetail(returnDetail);
}
ApiResponse<String> wcsResponse = wcsClient.sendPutawayTask(returnJob);
if (wcsResponse == null || !wcsResponse.isSuccess()) {
String message = wcsResponse == null ? "WCS接口无返回" : wcsResponse.getMessage();
throw new RuntimeException("WCS回流入库作业下发失败:" + message);
}
markInJobDispatchedToWcs(returnJobId);
}
private void copyLoginUser(MydoJob job) {
try {
job.setUserId(SecurityUtils.getUserId());
job.setDeptId(SecurityUtils.getDeptId());
job.setUserName(SecurityUtils.getUsername());
} catch (RuntimeException ignored) {
// Unit tests and external callbacks may run without a Spring Security context.
}
}
/**
* 作业生成后将关联入库单置为执行状态,供完成单据过程按既有规则校验 scan_num 与 assign_num。
*/

View File

@@ -168,6 +168,8 @@ external:
WCS:
base-url: http://172.16.4.63:8084
token: wcs-token
# 本地测试默认启用模拟返回;联调或生产设为 false 即可恢复真实 WCS 调用
mock-enabled: false
apis:
sendPutawayTask: /wcs/api/receiveWmsJob
sendRetrieveTask: /wcs/api/receiveWmsJob

View File

@@ -177,6 +177,100 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where cargo_space_id = #{cargoSpaceId}
</update>
<update id="markOutboundAssignedByStockId" parameterType="String">
update bcs
set bcs.status = '4',
bcs.tray_code = bs.tray_code
from bas_cargo_space bcs
inner join bas_stock bs on bcs.cargo_space_id = bs.cargo_space_id
where bs.stock_id = #{stockId}
</update>
<update id="markOutboundAssignedByStockRef" parameterType="String">
update bcs
set bcs.status = '4',
bcs.tray_code = bs.tray_code
from bas_cargo_space bcs
inner join bas_stock bs on bcs.cargo_space_id = bs.cargo_space_id
where bs.stock_id = #{stockRef}
or exists (
select 1
from bas_stock_detail bsd
where bsd.stock_id = bs.stock_id
and bsd.detail_id = #{stockRef}
)
</update>
<update id="markOutboundAssignedByStockoutDetailId" parameterType="String">
update bcs
set bcs.status = '4',
bcs.tray_code = bs.tray_code
from bas_cargo_space bcs
inner join bas_stock bs on bcs.cargo_space_id = bs.cargo_space_id
where exists (
select 1
from mydo_job_detail jd
inner join mydo_job j on j.job_id = jd.job_id
where jd.stock_id = bs.stock_id
and jd.in_or_out_d_id = #{stockoutDetailId}
and j.in_or_out &gt;= 20
and j.status in ('1', '2', '3')
)
</update>
<update id="markOutboundAssignedByCargoSpaceId" parameterType="String">
update bas_cargo_space
set status = '4'
where cargo_space_id = #{cargoSpaceId}
and status in ('1', '6')
</update>
<update id="releaseOutboundCargoSpace" parameterType="String">
update bas_cargo_space
set status = '1',
tray_code = null
where cargo_space_id = #{cargoSpaceId}
</update>
<update id="markOutboundNeedCheck" parameterType="String">
update bas_cargo_space
set status = '6'
where cargo_space_id = #{cargoSpaceId}
</update>
<update id="markInboundNeedCheck" parameterType="String">
update bas_cargo_space
set status = '5'
where cargo_space_id = #{cargoSpaceId}
</update>
<select id="selectAvailableInboundCargoSpaceInRoadway" parameterType="com.ruoyi.in.job.domain.MydoJob" resultMap="BasCargoSpaceResult">
select top 1 cargo_space_id, warehouse_id, roadway_id, cargo_area_id, the_type, the_type_name, cargo_space_name, the_platoon, the_column, the_floor, status, locked, tray_code, iss_double, group_coding_no, group_coding, cargo_space_logo, this_cargo_area, dept_id, dept_name, the_weight, the_height
from (
select bcs.cargo_space_id, bcs.warehouse_id, bcs.roadway_id, bcs.cargo_area_id, bcs.the_type, bcs.the_type_name,
bcs.cargo_space_name, bcs.the_platoon, bcs.the_column, bcs.the_floor, bcs.status, bcs.locked,
bcs.tray_code, bcs.iss_double, bcs.group_coding_no, bcs.group_coding, bcs.cargo_space_logo,
bcs.this_cargo_area, bcs.dept_id, bcs.dept_name, bcs.the_weight, bcs.the_height,
count(1) over (partition by bcs.the_platoon) as platoon_empty_count
from bas_cargo_space bcs
where bcs.warehouse_id = #{warehouseId}
and bcs.roadway_id = #{roadwayId}
and bcs.status = '1'
and ISNULL(bcs.locked, 'N') = 'N'
and (#{cargoSpaceId} is null or bcs.cargo_space_id != #{cargoSpaceId})
and bcs.the_type = (select top 1 btr.allowed_cargo_type
from bas_tray_rule btr
where btr.tray_type is not null
and btr.tray_type != ''
and #{trayCode} is not null
and #{trayCode} != ''
and left(#{trayCode}, len(btr.tray_type)) = btr.tray_type
and charindex(',' + #{warehouseId} + ',', ',' + btr.allowed_warehouses + ',') &gt; 0
order by len(btr.tray_type) desc, btr.id)
) candidate
order by platoon_empty_count desc, the_platoon asc, the_floor asc, the_column asc, cargo_space_id asc
</select>
<delete id="deleteBasCargoSpaceByCargoSpaceId" parameterType="String">
delete from bas_cargo_space where cargo_space_id = #{cargoSpaceId}
</delete>
@@ -187,4 +281,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{cargoSpaceId}
</foreach>
</delete>
</mapper>
</mapper>

View File

@@ -23,6 +23,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="trayCode" column="tray_code" />
<result property="warehouseId" column="warehouse_id" />
<result property="warehouseName" column="warehouse_name" />
<result property="trayType" column="tray_type" />
<result property="availableQuantity" column="available_quantity" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
@@ -33,17 +34,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select bsd.detail_id, bsd.stock_id, bsd.product_status, bsd.lot_no, bsd.product_id, bsd.product_name,
bsd.quantity, bsd.out_quantity, bsd.job_detail_id, bsd.stockin_detail_id,
bs.cargo_space_id, bcs.cargo_space_name, bs.stock_status, bs.in_date, bs.back_date,
bs.tray_code, bs.warehouse_id, bs.warehouse_name,
bs.tray_code, bs.warehouse_id, ISNULL(NULLIF(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name,
bbf.tray_type,
ISNULL(bsd.quantity, 0) - ISNULL(bsd.out_quantity, 0) as available_quantity,
bsd.create_time, bsd.update_time, bsd.remark
from bas_stock_detail bsd
left join bas_stock bs on bsd.stock_id = bs.stock_id
left join bas_cargo_space bcs on bs.cargo_space_id = bcs.cargo_space_id
left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id
OUTER APPLY (
select top 1 prefix as tray_type
from bas_barcode_format bbf
where bbf.prefix is not null
and bbf.prefix != ''
and bs.tray_code is not null
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
order by len(bbf.prefix) desc
) bbf
</sql>
<select id="selectBasStockDetailList" parameterType="BasStockDetail" resultMap="BasStockDetailResult">
<include refid="selectBasStockDetailVo" />
<where>
bcs.status = '2'
<if test="detailId != null and detailId != ''"> and bsd.detail_id = #{detailId}</if>
<if test="stockId != null and stockId != ''"> and bsd.stock_id = #{stockId}</if>
<if test="productStatus != null and productStatus != ''"> and bsd.product_status = #{productStatus}</if>
@@ -56,7 +69,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="stockStatus != null and stockStatus != ''"> and bs.stock_status = #{stockStatus}</if>
<if test="trayCode != null and trayCode != ''"> and bs.tray_code = #{trayCode}</if>
<if test="warehouseId != null and warehouseId != ''"> and bs.warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and bs.warehouse_name like concat('%', #{warehouseName}, '%')</if>
<if test="warehouseName != null and warehouseName != ''"> and ISNULL(NULLIF(bs.warehouse_name, ''), bw.warehouse_name) like concat('%', #{warehouseName}, '%')</if>
<if test="params.beginInDate != null and params.beginInDate != '' and params.endInDate != null and params.endInDate != ''"> and bs.in_date between #{params.beginInDate} and #{params.endInDate}</if>
<if test="params.beginBackDate != null and params.beginBackDate != '' and params.endBackDate != null and params.endBackDate != ''"> and bs.back_date between #{params.beginBackDate} and #{params.endBackDate}</if>
${params.warehouseScope}
@@ -69,6 +82,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bsd.detail_id = #{detailId}
</select>
<select id="countBasStockDetailByStockId" parameterType="String" resultType="int">
select count(1)
from bas_stock_detail with (UPDLOCK, HOLDLOCK)
where stock_id = #{stockId}
</select>
<select id="selectStockIdsByDetailIds" parameterType="String" resultType="String">
select distinct stock_id from bas_stock_detail where detail_id in
<foreach item="detailId" collection="array" open="(" separator="," close=")">
@@ -128,6 +147,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where detail_id = #{detailId}
</update>
<update id="rollbackOutboundQuantityByJobId" parameterType="String">
update bsd
set bsd.out_quantity = case
when ISNULL(bsd.out_quantity, 0) - ISNULL(jd.rollback_quantity, 0) &lt; 0 then 0
else ISNULL(bsd.out_quantity, 0) - ISNULL(jd.rollback_quantity, 0)
end,
bsd.job_detail_id = case
when exists (
select 1
from mydo_job_detail x
where x.job_id = #{jobId}
and x.job_detail_id = bsd.job_detail_id
) then null
else bsd.job_detail_id
end,
bsd.update_time = SYSDATETIME()
from bas_stock_detail bsd
inner join (
select stockin_detail_id as detail_id,
sum(ISNULL(out_quantity, 0)) as rollback_quantity
from mydo_job_detail
where job_id = #{jobId}
and stockin_detail_id is not null
and stockin_detail_id != ''
group by stockin_detail_id
) jd on jd.detail_id = bsd.detail_id
</update>
<delete id="deleteBasStockDetailByDetailId" parameterType="String">
delete from bas_stock_detail where detail_id = #{detailId}
</delete>

View File

@@ -35,7 +35,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectBasStockVo">
select stock_id, cargo_space_id, stock_status, product_status, lot_no, in_date, back_date, product_id, product_name, quantity, job_id, job_detail_id, tray_code, out_quantity, stockin_detail_id, stockin_id, in_type, warehouse_id, warehouse_name, user_id, user_name, dept_id, dept_name, supplier_id as suplier_id, supplier_name as suplier_name from bas_stock
select stock_id, cargo_space_id, stock_status, product_status, lot_no, in_date, back_date, product_id, product_name, quantity, job_id, job_detail_id, tray_code, out_quantity, stockin_detail_id, stockin_id, in_type, warehouse_id,
ISNULL(NULLIF(warehouse_name, ''), (select top 1 bw.warehouse_name from bas_warehouse bw where bw.warehouse_id = bas_stock.warehouse_id)) as warehouse_name,
user_id, user_name, dept_id, dept_name, supplier_id as suplier_id, supplier_name as suplier_name from bas_stock
</sql>
<select id="selectBasStockList" parameterType="BasStock" resultMap="BasStockResult">
@@ -46,12 +48,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
bs.job_id, null as job_detail_id,
bs.tray_code, ISNULL(bsa.out_quantity, 0) as out_quantity,
null as stockin_detail_id,
bs.stockin_id, bs.in_type, bs.warehouse_id, bs.warehouse_name,
bs.stockin_id, bs.in_type, bs.warehouse_id, ISNULL(NULLIF(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name,
bs.user_id, bs.user_name, bs.dept_id, bs.dept_name, bs.supplier_id as suplier_id, bs.supplier_name as suplier_name,
ISNULL(bsa.detail_count, 0) as detail_count,
ISNULL(bsa.quantity, 0) - ISNULL(bsa.out_quantity, 0) as available_quantity
from bas_stock bs
left join bas_cargo_space bcs on bs.cargo_space_id = bcs.cargo_space_id
left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id
left join (
select stock_id,
count(1) as detail_count,
@@ -80,7 +83,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="stockinId != null and stockinId != ''"> and bs.stockin_id = #{stockinId}</if>
<if test="inType != null and inType != ''"> and bs.in_type = #{inType}</if>
<if test="warehouseId != null and warehouseId != ''"> and bs.warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and bs.warehouse_name like concat('%', #{warehouseName}, '%')</if>
<if test="warehouseName != null and warehouseName != ''"> and ISNULL(NULLIF(bs.warehouse_name, ''), bw.warehouse_name) like concat('%', #{warehouseName}, '%')</if>
<if test="userName != null and userName != ''"> and bs.user_name like concat('%', #{userName}, '%')</if>
<if test="deptName != null and deptName != ''"> and bs.dept_name like concat('%', #{deptName}, '%')</if>
<if test="suplierName != null and suplierName != ''"> and bs.supplier_name like concat('%', #{suplierName}, '%')</if>
@@ -204,6 +207,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bs.stock_id = #{stockId}
</update>
<update id="markExceptionByJobId" parameterType="String">
update bs
set bs.stock_status = '3'
from bas_stock bs
where exists (
select 1
from mydo_job_detail jd
where jd.job_id = #{jobId}
and jd.stock_id = bs.stock_id
)
</update>
<delete id="deleteBasStockByStockId" parameterType="String">
delete from bas_stock where stock_id = #{stockId}
</delete>

View File

@@ -153,4 +153,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{jobDetailId}
</foreach>
</delete>
<delete id="deleteMydoJobDetailByJobId" parameterType="String">
delete from mydo_job_detail where job_id = #{jobId}
</delete>
</mapper>

View File

@@ -39,10 +39,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectMydoJobVo">
select 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 from mydo_job
select job_id, status, task_no, in_or_out, execution_phase, tray_code, warehouse_id,
isnull(nullif(warehouse_name, ''), (select top 1 bw.warehouse_name from bas_warehouse bw where bw.warehouse_id = mydo_job.warehouse_id)) as 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 from mydo_job
</sql>
<sql id="selectMydoJobFinishVo">
select 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 from mydo_job_finish
select job_id, status, task_no, in_or_out, execution_phase, tray_code, warehouse_id,
isnull(nullif(warehouse_name, ''), (select top 1 bw.warehouse_name from bas_warehouse bw where bw.warehouse_id = mydo_job_finish.warehouse_id)) as 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 from mydo_job_finish
</sql>
<select id="selectMydoJobList" parameterType="MydoJob" resultMap="MydoJobResult">
@@ -53,6 +57,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="inOrOut != null "> and in_or_out = #{inOrOut}</if>
<if test="executionPhase != null and executionPhase != ''"> and execution_phase = #{executionPhase}</if>
<if test="trayCode != null and trayCode != ''"> and tray_code = #{trayCode}</if>
<if test="warehouseId != null and warehouseId != ''"> and warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and warehouse_name like concat('%', #{warehouseName}, '%')</if>
<if test="roadwayId != null and roadwayId != ''"> and roadway_id = #{roadwayId}</if>
<if test="cargoSpaceId != null and cargoSpaceId != ''"> and cargo_space_id = #{cargoSpaceId}</if>
@@ -84,7 +89,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
finish_time, entrance_id, exit_id, dept_id, dept_name, user_id, user_name, supplier_id,
supplier_name, customer_id, customer_name
from mydo_job
where tray_code = #{trayCode} and in_or_out = 1
where tray_code = #{trayCode} and in_or_out &lt; 20
order by create_time desc
</select>
@@ -95,7 +100,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
finish_time, entrance_id, exit_id, dept_id, dept_name, user_id, user_name, supplier_id,
supplier_name, customer_id, customer_name
from mydo_job
where task_no = #{taskNo} and in_or_out = 1
where task_no = #{taskNo} and in_or_out &lt; 20
order by create_time desc
</select>
@@ -117,7 +122,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
finish_time, entrance_id, exit_id, dept_id, dept_name, user_id, user_name, supplier_id,
supplier_name, customer_id, customer_name
from mydo_job
where tray_code = #{trayCode} and warehouse_id = #{warehouseId} and in_or_out = 1
where tray_code = #{trayCode} and warehouse_id = #{warehouseId} and in_or_out &lt; 20
order by create_time desc
</select>
@@ -154,6 +159,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and stockin_id is not null
and stockin_id != ''
</select>
<select id="selectStockIdsByJobId" parameterType="String" resultType="String">
select distinct stock_id
from mydo_job_detail
where job_id = #{jobId}
and stock_id is not null
and stock_id != ''
</select>
<select id="selectStockoutDetailIdsByJobId" parameterType="String" resultType="String">
select distinct in_or_out_d_id
from mydo_job_detail
where job_id = #{jobId}
and in_or_out_d_id is not null
and in_or_out_d_id != ''
</select>
<select id="completeJob" parameterType="java.util.Map" statementType="CALLABLE">
{call proc_complete_in_job(
#{job_id,mode=IN,jdbcType=VARCHAR},
@@ -313,6 +334,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="inOrOut != null "> and in_or_out = #{inOrOut}</if>
<if test="executionPhase != null and executionPhase != ''"> and execution_phase = #{executionPhase}</if>
<if test="trayCode != null and trayCode != ''"> and tray_code = #{trayCode}</if>
<if test="warehouseId != null and warehouseId != ''"> and warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and warehouse_name like concat('%', #{warehouseName}, '%')</if>
<if test="roadwayId != null and roadwayId != ''"> and roadway_id = #{roadwayId}</if>
<if test="cargoSpaceId != null and cargoSpaceId != ''"> and cargo_space_id = #{cargoSpaceId}</if>
@@ -339,6 +361,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="inOrOut != null "> and in_or_out = #{inOrOut}</if>
<if test="executionPhase != null and executionPhase != ''"> and execution_phase = #{executionPhase}</if>
<if test="trayCode != null and trayCode != ''"> and tray_code = #{trayCode}</if>
<if test="warehouseId != null and warehouseId != ''"> and warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and warehouse_name like concat('%', #{warehouseName}, '%')</if>
<if test="roadwayId != null and roadwayId != ''"> and roadway_id = #{roadwayId}</if>
<if test="cargoSpaceId != null and cargoSpaceId != ''"> and cargo_space_id = #{cargoSpaceId}</if>
@@ -365,6 +388,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="inOrOut != null "> and in_or_out = #{inOrOut}</if>
<if test="executionPhase != null and executionPhase != ''"> and execution_phase = #{executionPhase}</if>
<if test="trayCode != null and trayCode != ''"> and tray_code = #{trayCode}</if>
<if test="warehouseId != null and warehouseId != ''"> and warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and warehouse_name like concat('%', #{warehouseName}, '%')</if>
<if test="roadwayId != null and roadwayId != ''"> and roadway_id = #{roadwayId}</if>
<if test="cargoSpaceId != null and cargoSpaceId != ''"> and cargo_space_id = #{cargoSpaceId}</if>
@@ -392,6 +416,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="inOrOut != null "> and in_or_out = #{inOrOut}</if>
<if test="executionPhase != null and executionPhase != ''"> and execution_phase = #{executionPhase}</if>
<if test="trayCode != null and trayCode != ''"> and tray_code = #{trayCode}</if>
<if test="warehouseId != null and warehouseId != ''"> and warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and warehouse_name like concat('%', #{warehouseName}, '%')</if>
<if test="roadwayId != null and roadwayId != ''"> and roadway_id = #{roadwayId}</if>
<if test="cargoSpaceId != null and cargoSpaceId != ''"> and cargo_space_id = #{cargoSpaceId}</if>

View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.out.location.mapper.LocationOutboundMapper">
<resultMap type="LocationOutboundStock" id="LocationOutboundStockResult">
<result property="cargoSpaceId" column="cargo_space_id" />
<result property="cargoSpaceName" column="cargo_space_name" />
<result property="cargoSpaceStatus" column="cargo_space_status" />
<result property="locked" column="locked" />
<result property="warehouseId" column="warehouse_id" />
<result property="warehouseName" column="warehouse_name" />
<result property="roadwayId" column="roadway_id" />
<result property="thePlatoon" column="the_platoon" />
<result property="theColumn" column="the_column" />
<result property="theFloor" column="the_floor" />
<result property="theType" column="the_type" />
<result property="theTypeName" column="the_type_name" />
<result property="stockId" column="stock_id" />
<result property="stockStatus" column="stock_status" />
<result property="trayCode" column="tray_code" />
<result property="trayType" column="tray_type" />
<result property="inDate" column="in_date" />
<result property="stockinId" column="stockin_id" />
<result property="inType" column="in_type" />
<result property="supplierId" column="supplier_id" />
<result property="supplierName" column="supplier_name" />
<result property="detailCount" column="detail_count" />
<result property="quantity" column="quantity" />
<result property="outQuantity" column="out_quantity" />
<result property="availableQuantity" column="available_quantity" />
</resultMap>
<resultMap type="BasStockDetail" id="LocationOutboundDetailResult">
<result property="detailId" column="detail_id" />
<result property="stockId" column="stock_id" />
<result property="productStatus" column="product_status" />
<result property="lotNo" column="lot_no" />
<result property="productId" column="product_id" />
<result property="productName" column="product_name" />
<result property="quantity" column="quantity" />
<result property="outQuantity" column="out_quantity" />
<result property="jobDetailId" column="job_detail_id" />
<result property="stockinDetailId" column="stockin_detail_id" />
<result property="cargoSpaceId" column="cargo_space_id" />
<result property="cargoSpaceName" column="cargo_space_name" />
<result property="stockStatus" column="stock_status" />
<result property="inDate" column="in_date" />
<result property="backDate" column="back_date" />
<result property="trayCode" column="tray_code" />
<result property="warehouseId" column="warehouse_id" />
<result property="warehouseName" column="warehouse_name" />
<result property="trayType" column="tray_type" />
<result property="availableQuantity" column="available_quantity" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="stockAggregate">
select stock_id,
count(1) as detail_count,
sum(isnull(quantity, 0)) as quantity,
sum(isnull(out_quantity, 0)) as out_quantity,
sum(isnull(quantity, 0) - isnull(out_quantity, 0)) as available_quantity
from bas_stock_detail
group by stock_id
</sql>
<sql id="detailSelect">
select bsd.detail_id, bsd.stock_id, bsd.product_status, bsd.lot_no, bsd.product_id, bsd.product_name,
bsd.quantity, bsd.out_quantity, bsd.job_detail_id, bsd.stockin_detail_id,
bs.cargo_space_id, bcs.cargo_space_name, bs.stock_status, bs.in_date, bs.back_date,
bs.tray_code, bs.warehouse_id, isnull(nullif(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name,
bbf.tray_type,
isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) as available_quantity,
bsd.create_time, bsd.update_time, bsd.remark
from bas_stock_detail bsd
inner join bas_stock bs on bsd.stock_id = bs.stock_id
left join bas_cargo_space bcs on bs.cargo_space_id = bcs.cargo_space_id
left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id
outer apply (
select top 1 prefix as tray_type
from bas_barcode_format bbf
where bbf.prefix is not null
and bbf.prefix != ''
and bs.tray_code is not null
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
order by len(bbf.prefix) desc
) bbf
</sql>
<select id="selectLocationOutboundStockList" parameterType="LocationOutboundStock" resultMap="LocationOutboundStockResult">
select bcs.cargo_space_id, bcs.cargo_space_name, bcs.status as cargo_space_status, bcs.locked,
bcs.warehouse_id, isnull(nullif(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name,
bcs.roadway_id, bcs.the_platoon, bcs.the_column, bcs.the_floor, bcs.the_type, bcs.the_type_name,
bs.stock_id, bs.stock_status, bs.tray_code, bbf.tray_type, bs.in_date, bs.stockin_id, bs.in_type,
bs.supplier_id, bs.supplier_name,
isnull(bsa.detail_count, 0) as detail_count,
isnull(bsa.quantity, 0) as quantity,
isnull(bsa.out_quantity, 0) as out_quantity,
isnull(bsa.available_quantity, 0) as available_quantity
from bas_cargo_space bcs
left join bas_stock bs on bs.cargo_space_id = bcs.cargo_space_id
and bcs.status in ('2', '4')
left join bas_warehouse bw on bcs.warehouse_id = bw.warehouse_id
left join (
<include refid="stockAggregate" />
) bsa on bsa.stock_id = bs.stock_id
outer apply (
select top 1 prefix as tray_type
from bas_barcode_format bbf
where bbf.prefix is not null
and bbf.prefix != ''
and bs.tray_code is not null
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
order by len(bbf.prefix) desc
) bbf
<where>
<if test="showEmpty == null or showEmpty == false">
and bcs.status = '2'
and bs.stock_id is not null
and isnull(bsa.available_quantity, 0) &gt; 0
</if>
<if test="warehouseId != null and warehouseId != ''"> and bcs.warehouse_id = #{warehouseId}</if>
<if test="warehouseName != null and warehouseName != ''"> and isnull(nullif(bs.warehouse_name, ''), bw.warehouse_name) like concat('%', #{warehouseName}, '%')</if>
<if test="roadwayId != null and roadwayId != ''"> and bcs.roadway_id = #{roadwayId}</if>
<if test="cargoSpaceId != null and cargoSpaceId != ''"> and bcs.cargo_space_id like concat('%', #{cargoSpaceId}, '%')</if>
<if test="cargoSpaceName != null and cargoSpaceName != ''"> and bcs.cargo_space_name like concat('%', #{cargoSpaceName}, '%')</if>
<if test="cargoSpaceStatus != null and cargoSpaceStatus != ''"> and bcs.status = #{cargoSpaceStatus}</if>
<if test="stockStatus != null and stockStatus != ''"> and bs.stock_status = #{stockStatus}</if>
<if test="trayCode != null and trayCode != ''"> and bs.tray_code like concat('%', #{trayCode}, '%')</if>
<if test="productId != null and productId != ''"> and exists (select 1 from bas_stock_detail x where x.stock_id = bs.stock_id and x.product_id like concat('%', #{productId}, '%'))</if>
<if test="productName != null and productName != ''"> and exists (select 1 from bas_stock_detail x where x.stock_id = bs.stock_id and x.product_name like concat('%', #{productName}, '%'))</if>
<if test="lotNo != null and lotNo != ''"> and exists (select 1 from bas_stock_detail x where x.stock_id = bs.stock_id and x.lot_no like concat('%', #{lotNo}, '%'))</if>
${params.warehouseScope}
</where>
order by bcs.warehouse_id, bcs.roadway_id, bcs.the_floor, bcs.the_platoon, bcs.the_column, bcs.cargo_space_id, bs.stock_id
</select>
<select id="selectLocationOutboundDetailsByStockIds" parameterType="String" resultMap="LocationOutboundDetailResult">
<include refid="detailSelect" />
where bsd.stock_id in
<foreach item="stockId" collection="array" open="(" separator="," close=")">
#{stockId}
</foreach>
and isnull(bsd.quantity, 0) &gt; 0
order by bsd.stock_id, bsd.product_id, bsd.detail_id
</select>
<select id="selectLocationOutboundStockForUpdate" parameterType="String" resultMap="LocationOutboundStockResult">
select top 1 bcs.cargo_space_id, bcs.cargo_space_name, bcs.status as cargo_space_status, bcs.locked,
bcs.warehouse_id, isnull(nullif(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name,
bcs.roadway_id, bcs.the_platoon, bcs.the_column, bcs.the_floor, bcs.the_type, bcs.the_type_name,
bs.stock_id, bs.stock_status, bs.tray_code, bbf.tray_type, bs.in_date, bs.stockin_id, bs.in_type,
bs.supplier_id, bs.supplier_name,
isnull(bsa.detail_count, 0) as detail_count,
isnull(bsa.quantity, 0) as quantity,
isnull(bsa.out_quantity, 0) as out_quantity,
isnull(bsa.available_quantity, 0) as available_quantity
from bas_stock bs with (UPDLOCK, HOLDLOCK)
inner join bas_cargo_space bcs with (UPDLOCK, HOLDLOCK) on bs.cargo_space_id = bcs.cargo_space_id
left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id
left join (
<include refid="stockAggregate" />
) bsa on bsa.stock_id = bs.stock_id
outer apply (
select top 1 prefix as tray_type
from bas_barcode_format bbf
where bbf.prefix is not null
and bbf.prefix != ''
and bs.tray_code is not null
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
order by len(bbf.prefix) desc
) bbf
where bs.stock_id = #{stockId}
</select>
<select id="selectLocationOutboundLocationForUpdate" parameterType="String" resultMap="LocationOutboundStockResult">
select top 1 bcs.cargo_space_id, bcs.cargo_space_name, bcs.status as cargo_space_status, bcs.locked,
bcs.warehouse_id, isnull(nullif(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name,
bcs.roadway_id, bcs.the_platoon, bcs.the_column, bcs.the_floor, bcs.the_type, bcs.the_type_name,
bs.stock_id, bs.stock_status, bs.tray_code, bbf.tray_type, bs.in_date, bs.stockin_id, bs.in_type,
bs.supplier_id, bs.supplier_name,
isnull(bsa.detail_count, 0) as detail_count,
isnull(bsa.quantity, 0) as quantity,
isnull(bsa.out_quantity, 0) as out_quantity,
isnull(bsa.available_quantity, 0) as available_quantity
from bas_cargo_space bcs with (UPDLOCK, HOLDLOCK)
left join bas_stock bs with (UPDLOCK, HOLDLOCK) on bs.cargo_space_id = bcs.cargo_space_id
and bcs.status in ('2', '4')
left join bas_warehouse bw on bcs.warehouse_id = bw.warehouse_id
left join (
<include refid="stockAggregate" />
) bsa on bsa.stock_id = bs.stock_id
outer apply (
select top 1 prefix as tray_type
from bas_barcode_format bbf
where bbf.prefix is not null
and bbf.prefix != ''
and bs.tray_code is not null
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
order by len(bbf.prefix) desc
) bbf
where bcs.cargo_space_id = #{cargoSpaceId}
order by bs.in_date desc, bs.stock_id
</select>
<select id="selectAvailableDetailsForUpdate" parameterType="String" resultMap="LocationOutboundDetailResult">
select bsd.detail_id, bsd.stock_id, bsd.product_status, bsd.lot_no, bsd.product_id, bsd.product_name,
bsd.quantity, bsd.out_quantity, bsd.job_detail_id, bsd.stockin_detail_id,
bs.cargo_space_id, bcs.cargo_space_name, bs.stock_status, bs.in_date, bs.back_date,
bs.tray_code, bs.warehouse_id, isnull(nullif(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name,
bbf.tray_type,
isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) as available_quantity,
bsd.create_time, bsd.update_time, bsd.remark
from bas_stock_detail bsd with (UPDLOCK, HOLDLOCK)
inner join bas_stock bs with (UPDLOCK, HOLDLOCK) on bsd.stock_id = bs.stock_id
left join bas_cargo_space bcs with (UPDLOCK, HOLDLOCK) on bs.cargo_space_id = bcs.cargo_space_id
left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id
outer apply (
select top 1 prefix as tray_type
from bas_barcode_format bbf
where bbf.prefix is not null
and bbf.prefix != ''
and bs.tray_code is not null
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
order by len(bbf.prefix) desc
) bbf
where bsd.stock_id = #{stockId}
and isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) &gt; 0
order by bsd.product_id, bsd.detail_id
</select>
<update id="reserveStockDetailForOutbound" parameterType="java.util.Map">
update bas_stock_detail
set out_quantity = isnull(out_quantity, 0) + #{quantity},
job_detail_id = #{jobDetailId},
update_time = sysdatetime()
where detail_id = #{stockDetailId}
and stock_id = #{stockId}
and isnull(quantity, 0) - isnull(out_quantity, 0) &gt;= #{quantity}
</update>
</mapper>

View File

@@ -28,56 +28,68 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="remar" column="remar" />
<result property="stockId" column="stock_id" />
<result property="stockDetailId" column="stock_detail_id" />
<result property="trayType" column="tray_type" />
</resultMap>
<sql id="selectOutStockoutDetailVo">
select stockout_detail_id, stockout_id, lot_no, product_id, product_name, quantity, allot_num, assign_num, remark, status,
line_n, item_cod, item_nam, spec_mode, uni, actual_qt, warehous, locatio, inventory_, source_doc_n, remar,
stock_id, stock_detail_id
from out_stockout_detail
select d.stockout_detail_id, d.stockout_id, d.lot_no, d.product_id, d.product_name, d.quantity, d.allot_num, d.assign_num, d.remark, d.status,
d.line_n, d.item_cod, d.item_nam, d.spec_mode, d.uni, d.actual_qt, d.warehous, d.locatio, d.inventory_, d.source_doc_n, d.remar,
d.stock_id, d.stock_detail_id, bbf.tray_type
from out_stockout_detail d
left join bas_stock_detail bsd on d.stock_detail_id = bsd.detail_id
left join bas_stock bs on bs.stock_id = COALESCE(NULLIF(d.stock_id, ''), bsd.stock_id)
OUTER APPLY (
select top 1 prefix as tray_type
from bas_barcode_format bbf
where bbf.prefix is not null
and bbf.prefix != ''
and bs.tray_code is not null
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
order by len(bbf.prefix) desc
) bbf
</sql>
<sql id="selectOutStockoutDetailFinishVo">
select stockout_detail_id, stockout_id, lot_no, product_id, product_name, quantity, allot_num, assign_num, remark, status,
line_n, item_cod, item_nam, spec_mode, uni, actual_qt, warehous, locatio, inventory_, source_doc_n, remar,
stock_id, stock_detail_id
from out_stockout_detail_finish
select d.stockout_detail_id, d.stockout_id, d.lot_no, d.product_id, d.product_name, d.quantity, d.allot_num, d.assign_num, d.remark, d.status,
d.line_n, d.item_cod, d.item_nam, d.spec_mode, d.uni, d.actual_qt, d.warehous, d.locatio, d.inventory_, d.source_doc_n, d.remar,
d.stock_id, d.stock_detail_id, null as tray_type
from out_stockout_detail_finish d
</sql>
<select id="selectOutStockoutDetailList" parameterType="OutStockoutDetail" resultMap="OutStockoutDetailResult">
<include refid="selectOutStockoutDetailVo"/>
<where>
<if test="stockoutId != null and stockoutId != ''"> and stockout_id = #{stockoutId}</if>
<if test="lotNo != null and lotNo != ''"> and lot_no = #{lotNo}</if>
<if test="productId != null and productId != ''"> and product_id = #{productId}</if>
<if test="productName != null and productName != ''"> and product_name like concat('%', #{productName}, '%')</if>
<if test="quantity != null "> and quantity = #{quantity}</if>
<if test="allotNum != null "> and allot_num = #{allotNum}</if>
<if test="assignNum != null "> and assign_num = #{assignNum}</if>
<if test="itemCod != null and itemCod != ''"> and item_cod like concat('%', #{itemCod}, '%')</if>
<if test="itemNam != null and itemNam != ''"> and item_nam like concat('%', #{itemNam}, '%')</if>
<if test="stockoutId != null and stockoutId != ''"> and d.stockout_id = #{stockoutId}</if>
<if test="lotNo != null and lotNo != ''"> and d.lot_no = #{lotNo}</if>
<if test="productId != null and productId != ''"> and d.product_id = #{productId}</if>
<if test="productName != null and productName != ''"> and d.product_name like concat('%', #{productName}, '%')</if>
<if test="quantity != null "> and d.quantity = #{quantity}</if>
<if test="allotNum != null "> and d.allot_num = #{allotNum}</if>
<if test="assignNum != null "> and d.assign_num = #{assignNum}</if>
<if test="itemCod != null and itemCod != ''"> and d.item_cod like concat('%', #{itemCod}, '%')</if>
<if test="itemNam != null and itemNam != ''"> and d.item_nam like concat('%', #{itemNam}, '%')</if>
</where>
</select>
<select id="selectOutStockoutDetailByStockoutDetailId" parameterType="String" resultMap="OutStockoutDetailResult">
<include refid="selectOutStockoutDetailVo"/>
where stockout_detail_id = #{stockoutDetailId}
where d.stockout_detail_id = #{stockoutDetailId}
</select>
<select id="selectOutStockoutDetailListByStockoutId" parameterType="String" resultMap="OutStockoutDetailResult">
<include refid="selectOutStockoutDetailVo"/>
where stockout_id = #{stockoutDetailId}
where d.stockout_id = #{stockoutDetailId}
</select>
<select id="selectOutStockoutDetailFinishList" parameterType="OutStockoutDetail" resultMap="OutStockoutDetailResult">
<include refid="selectOutStockoutDetailFinishVo"/>
<where>
<if test="stockoutId != null and stockoutId != ''"> and stockout_id = #{stockoutId}</if>
<if test="lotNo != null and lotNo != ''"> and lot_no = #{lotNo}</if>
<if test="productId != null and productId != ''"> and product_id = #{productId}</if>
<if test="productName != null and productName != ''"> and product_name like concat('%', #{productName}, '%')</if>
<if test="quantity != null "> and quantity = #{quantity}</if>
<if test="allotNum != null "> and allot_num = #{allotNum}</if>
<if test="assignNum != null "> and assign_num = #{assignNum}</if>
<if test="itemCod != null and itemCod != ''"> and item_cod like concat('%', #{itemCod}, '%')</if>
<if test="itemNam != null and itemNam != ''"> and item_nam like concat('%', #{itemNam}, '%')</if>
<if test="stockoutId != null and stockoutId != ''"> and d.stockout_id = #{stockoutId}</if>
<if test="lotNo != null and lotNo != ''"> and d.lot_no = #{lotNo}</if>
<if test="productId != null and productId != ''"> and d.product_id = #{productId}</if>
<if test="productName != null and productName != ''"> and d.product_name like concat('%', #{productName}, '%')</if>
<if test="quantity != null "> and d.quantity = #{quantity}</if>
<if test="allotNum != null "> and d.allot_num = #{allotNum}</if>
<if test="assignNum != null "> and d.assign_num = #{assignNum}</if>
<if test="itemCod != null and itemCod != ''"> and d.item_cod like concat('%', #{itemCod}, '%')</if>
<if test="itemNam != null and itemNam != ''"> and d.item_nam like concat('%', #{itemNam}, '%')</if>
</where>
</select>
@@ -164,6 +176,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where stockout_detail_id = #{stockoutDetailId}
</update>
<update id="rollbackAllotByJobId" parameterType="String">
update d
set d.allot_num = case
when ISNULL(d.allot_num, 0) - ISNULL(jd.rollback_quantity, 0) &lt; 0 then 0
else ISNULL(d.allot_num, 0) - ISNULL(jd.rollback_quantity, 0)
end,
d.status = case
when ISNULL(d.allot_num, 0) - ISNULL(jd.rollback_quantity, 0) &lt;= 0 then '0'
else d.status
end
from out_stockout_detail d
inner join (
select in_or_out_d_id as stockout_detail_id,
sum(ISNULL(out_quantity, 0)) as rollback_quantity
from mydo_job_detail
where job_id = #{jobId}
and in_or_out_d_id is not null
and in_or_out_d_id != ''
group by in_or_out_d_id
) jd on jd.stockout_detail_id = d.stockout_detail_id
</update>
<delete id="deleteOutStockoutDetailByStockoutDetailId" parameterType="String">
delete from out_stockout_detail where stockout_detail_id = #{stockoutDetailId}
</delete>

View File

@@ -108,6 +108,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="autoAssignOutStockoutByProc" parameterType="java.util.Map" statementType="CALLABLE">
{call proc_out_auto_allocation(
#{v_stockout_detail_id,mode=IN,jdbcType=VARCHAR},
#{v_exit_id,mode=IN,jdbcType=VARCHAR},
#{v_result,mode=OUT,jdbcType=INTEGER},
#{v_resultString,mode=OUT,jdbcType=VARCHAR}
)}
@@ -252,11 +253,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
{call proc_out_hand_allocation(
#{v_stockout_detail_id,mode=IN,jdbcType=VARCHAR},
#{v_stock_id,mode=IN,jdbcType=VARCHAR},
#{v_exit_id,mode=IN,jdbcType=VARCHAR},
#{v_result,mode=OUT,jdbcType=INTEGER},
#{v_resultString,mode=OUT,jdbcType=VARCHAR}
)}
</select>
<update id="reopenOutStockoutByExceptionJobId" parameterType="String">
update o
set o.status = '2',
o.start_date = null
from out_stockout o
where o.status = '3'
and exists (
select 1
from mydo_job_detail jd
where jd.job_id = #{jobId}
and jd.in_or_out_id = o.stockout_id
)
and not exists (
select 1
from mydo_job_detail active_jd
where active_jd.in_or_out_id = o.stockout_id
and active_jd.job_id != #{jobId}
)
</update>
<delete id="deleteOutStockoutByStockoutId" parameterType="String">
delete from out_stockout where stockout_id = #{stockoutId}
</delete>

View File

@@ -50,18 +50,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<update id="lockInboundCargoSpace" parameterType="java.util.Map">
update bas_cargo_space
set status = '3',
locked = 'Y',
tray_code = #{v_tray_code}
where cargo_space_id = (
select top 1 cargo_space_id
from bas_cargo_space with (UPDLOCK, READPAST, ROWLOCK)
where warehouse_id = #{v_warehouse_id}
and the_type = #{v_cargo_type}
and (#{v_roadway_id} is null or #{v_roadway_id} = '' or roadway_id = #{v_roadway_id})
and status = '1'
and locked = 'N'
and (tray_code is null or tray_code = '')
order by the_floor asc, the_platoon asc, the_column asc
from (
select bcs.cargo_space_id,
bcs.the_platoon,
bcs.the_column,
bcs.the_floor,
count(1) over (partition by bcs.the_platoon) as platoon_empty_count
from bas_cargo_space bcs with (UPDLOCK, READPAST, ROWLOCK)
where bcs.warehouse_id = #{v_warehouse_id}
and bcs.the_type = #{v_cargo_type}
and (#{v_roadway_id} is null or #{v_roadway_id} = '' or bcs.roadway_id = #{v_roadway_id})
and bcs.status = '1'
and bcs.locked = 'N'
and (bcs.tray_code is null or bcs.tray_code = '')
) candidate
order by platoon_empty_count desc, the_platoon asc, the_floor asc, the_column asc, cargo_space_id asc
)
</update>
@@ -72,19 +78,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and tray_code = #{v_tray_code}
and (#{v_roadway_id} is null or #{v_roadway_id} = '' or roadway_id = #{v_roadway_id})
and status = '3'
and locked = 'Y'
order by the_floor asc, the_platoon asc, the_column asc
</select>
<update id="releaseInboundCargoSpace" parameterType="java.util.Map">
update bas_cargo_space
set status = '1',
locked = 'N',
tray_code = null
where warehouse_id = #{v_warehouse_id}
and tray_code = #{v_tray_code}
and status = '3'
and locked = 'Y'
</update>
<select id="outboundTaskList" parameterType="java.util.Map" resultType="java.util.HashMap">
@@ -99,16 +102,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
isnull(j.exit_id, '') as portName,
convert(varchar(19), j.create_time, 120) as createTime,
j.status as status,
case j.status
when '1' then '未执行'
when '2' then '待执行'
when '3' then '执行中'
j.execution_phase as executionPhase,
case
when j.execution_phase = '5' then '待扫码确认'
when j.status = '1' then '未执行'
when j.status = '2' then '待执行'
when j.status = '3' then '执行中'
else '待确认'
end as statusName,
case j.status
when '1' then 'pending'
when '2' then 'pending'
when '3' then 'processing'
case
when j.execution_phase = '5' then 'pending'
when j.status = '1' then 'pending'
when j.status = '2' then 'pending'
when j.status = '3' then 'processing'
else 'pending'
end as statusClass
from mydo_job j
@@ -116,38 +122,192 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where j.in_or_out &gt;= 20
and j.status in ('1', '2', '3')
and (#{taskNo} is null or #{taskNo} = '' or j.task_no = #{taskNo})
group by j.job_id, j.task_no, j.exit_id, j.create_time, j.status
group by j.job_id, j.task_no, j.exit_id, j.create_time, j.status, j.execution_phase
order by j.create_time desc
</select>
<select id="selectOutboundDetail" parameterType="java.util.Map" resultType="java.util.HashMap">
select top 1 j.job_id as jobId,
j.task_no as taskNo,
jd.job_detail_id as jobDetailId,
jd.stock_id as stockId,
jd.stockin_detail_id as stockDetailId,
jd.product_id as materialCode,
jd.product_name as materialName,
jd.lot_no as lotNo,
jd.out_quantity as quantity,
j.tray_code as trayCode,
j.cargo_space_id as locationCode,
j.cargo_space_name as locationName,
j.warehouse_id as warehouseId,
j.warehouse_name as warehouseName,
j.execution_phase as executionPhase,
isnull(j.exit_id, '') as portName,
sum(isnull(jd.out_quantity, 0)) as quantity,
count(1) as materialCount,
'' as spec
from mydo_job j
inner join mydo_job_detail jd on j.job_id = jd.job_id
where j.in_or_out &gt;= 20
and j.status in ('1', '2', '3')
and (#{taskNo} is null or #{taskNo} = '' or j.task_no = #{taskNo})
and (
#{barcode} is null or #{barcode} = ''
or j.task_no = #{barcode}
or j.tray_code = #{barcode}
or jd.stock_id = #{barcode}
or jd.stockin_detail_id = #{barcode}
or jd.product_id = #{barcode}
)
order by j.create_time desc, jd.job_detail_id
and (#{barcode} is null or #{barcode} = '' or j.tray_code = #{barcode})
group by j.job_id, j.task_no, j.tray_code, j.cargo_space_id, j.cargo_space_name,
j.warehouse_id, j.warehouse_name, j.execution_phase, j.exit_id, j.create_time
order by j.create_time desc
</select>
<select id="selectOutboundTrayMaterials" parameterType="java.util.Map" resultType="java.util.HashMap">
select jd.job_detail_id as jobDetailId,
jd.stock_id as stockId,
jd.stockin_detail_id as stockDetailId,
jd.product_id as materialCode,
jd.product_name as materialName,
jd.lot_no as lotNo,
jd.stock_quantity as stockQuantity,
jd.out_quantity as quantity,
isnull(jd.stock_quantity, 0) - isnull(jd.out_quantity, 0) as residualQuantity
from mydo_job_detail jd
where jd.job_id = #{jobId}
order by jd.job_detail_id
</select>
<select id="selectResidualTrayMaterials" parameterType="java.util.Map" resultType="java.util.HashMap">
select bsd.detail_id as stockDetailId,
bsd.stock_id as stockId,
bsd.product_id as productId,
bsd.product_id as materialCode,
bsd.product_name as productName,
bsd.product_name as materialName,
bsd.lot_no as lotNo,
isnull(bsd.quantity, 0) as quantity,
bs.tray_code as trayCode,
bs.warehouse_id as warehouseId,
bs.warehouse_name as warehouseName,
bs.cargo_space_id as cargoSpaceId,
bcs.cargo_space_name as cargoSpaceName,
bcs.roadway_id as roadwayId,
bs.supplier_id as supplierId,
bs.supplier_name as supplierName
from bas_stock_detail bsd
inner join bas_stock bs on bsd.stock_id = bs.stock_id
left join bas_cargo_space bcs on bs.cargo_space_id = bcs.cargo_space_id
where bs.tray_code = #{trayCode}
and bs.warehouse_id = #{warehouseId}
and isnull(bsd.quantity, 0) &gt; 0
order by bsd.detail_id
</select>
<select id="selectTrayStockMaterials" parameterType="String" resultType="java.util.HashMap">
select bsd.detail_id as stockDetailId,
bsd.stock_id as stockId,
bsd.product_id as productId,
bsd.product_id as materialCode,
bsd.product_name as productName,
bsd.product_name as materialName,
bsd.lot_no as lotNo,
isnull(bsd.quantity, 0) as quantity,
isnull(bsd.out_quantity, 0) as outQuantity,
isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) as availableQuantity,
bs.tray_code as trayCode,
bs.stock_status as stockStatus,
bs.product_status as productStatus,
bs.warehouse_id as warehouseId,
isnull(nullif(bs.warehouse_name, ''), bw.warehouse_name) as warehouseName,
bs.cargo_space_id as cargoSpaceId,
bcs.cargo_space_name as cargoSpaceName,
bcs.roadway_id as roadwayId,
bs.supplier_id as supplierId,
bs.supplier_name as supplierName,
convert(varchar(19), bs.in_date, 120) as inDate
from bas_stock bs
inner join bas_stock_detail bsd on bs.stock_id = bsd.stock_id
left join bas_cargo_space bcs on bs.cargo_space_id = bcs.cargo_space_id
left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id
where bs.tray_code = #{trayCode}
and isnull(bsd.quantity, 0) &gt; 0
order by bsd.detail_id
</select>
<select id="selectAvailableEmptyTrayTypeCounts" parameterType="String" resultType="java.util.HashMap">
select bbf.id as formatId,
bbf.format_name as formatName,
bbf.prefix as barcodePrefix,
count(1) as quantity
from bas_stock_detail bsd
inner join bas_stock bs on bsd.stock_id = bs.stock_id
inner join bas_barcode_format bbf
on bbf.prefix is not null
and bbf.prefix != ''
and left(bs.tray_code, len(bbf.prefix)) = bbf.prefix
where bs.warehouse_id = #{warehouseId}
and bsd.product_id = 'KTP0001'
and isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) &gt; 0
and bs.tray_code is not null
and bs.tray_code != ''
and not exists (
select 1
from bas_barcode_format bbf2
where bbf2.prefix is not null
and bbf2.prefix != ''
and left(bs.tray_code, len(bbf2.prefix)) = bbf2.prefix
and len(bbf2.prefix) &gt; len(bbf.prefix)
)
group by bbf.id, bbf.format_name, bbf.prefix
order by bbf.prefix
</select>
<select id="countAvailableEmptyTrays" parameterType="java.util.Map" resultType="int">
select count(1)
from bas_stock_detail bsd
inner join bas_stock bs on bsd.stock_id = bs.stock_id
where bs.warehouse_id = #{warehouseId}
and left(bs.tray_code, len(#{barcodePrefix})) = #{barcodePrefix}
and bsd.product_id = 'KTP0001'
and isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) &gt; 0
and bs.tray_code is not null
and bs.tray_code != ''
</select>
<select id="selectAvailableEmptyTrayStocks" parameterType="java.util.Map" resultType="java.util.HashMap">
select top (#{limit})
bsd.detail_id as stockDetailId,
bsd.stock_id as stockId,
bsd.lot_no as lotNo,
bsd.product_id as productId,
bsd.product_name as productName,
isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) as availableQuantity,
bs.tray_code as trayCode,
bs.warehouse_id as warehouseId,
isnull(nullif(bs.warehouse_name, ''), (select top 1 bw.warehouse_name from bas_warehouse bw where bw.warehouse_id = bs.warehouse_id)) as warehouseName,
bs.cargo_space_id as cargoSpaceId,
bcs.cargo_space_name as cargoSpaceName,
bcs.roadway_id as roadwayId,
bs.supplier_id as supplierId,
bs.supplier_name as supplierName
from bas_stock_detail bsd with (UPDLOCK, HOLDLOCK)
inner join bas_stock bs with (UPDLOCK, HOLDLOCK) on bsd.stock_id = bs.stock_id
left join bas_cargo_space bcs with (UPDLOCK, HOLDLOCK) on bs.cargo_space_id = bcs.cargo_space_id
where bs.warehouse_id = #{warehouseId}
and left(bs.tray_code, len(#{barcodePrefix})) = #{barcodePrefix}
and bsd.product_id = 'KTP0001'
and isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) &gt; 0
and bs.tray_code is not null
and bs.tray_code != ''
order by bs.in_date asc, bs.cargo_space_id asc, bsd.detail_id asc
</select>
<update id="reserveEmptyTrayStockDetail" parameterType="java.util.Map">
update bas_stock_detail
set out_quantity = isnull(out_quantity, 0) + #{quantity},
job_detail_id = #{jobDetailId},
update_time = sysdatetime()
where detail_id = #{stockDetailId}
and stock_id = #{stockId}
and product_id = 'KTP0001'
and isnull(quantity, 0) - isnull(out_quantity, 0) &gt;= #{quantity}
</update>
<update id="lockEmptyTrayCargoSpace" parameterType="java.util.Map">
update bcs
set status = '4',
tray_code = bs.tray_code
from bas_cargo_space bcs
inner join bas_stock bs on bcs.cargo_space_id = bs.cargo_space_id
where bs.stock_id = #{stockId}
and bs.cargo_space_id = #{cargoSpaceId}
</update>
</mapper>

View File

@@ -0,0 +1,106 @@
package com.ruoyi.api.aspect;
import com.ruoyi.api.service.ApiLogService;
import com.ruoyi.common.core.domain.AjaxResult;
import org.aspectj.lang.JoinPoint;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.annotation.Annotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ApiLogAspectTest {
@Mock
private ApiLogService apiLogService;
@Mock
private JoinPoint joinPoint;
private ApiLogAspect aspect;
@BeforeEach
void setUp() {
aspect = new ApiLogAspect();
ReflectionTestUtils.setField(aspect, "apiLogService", apiLogService);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/wcs/exception");
request.addHeader("X-Trace-Id", "TRACE-001");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
}
@AfterEach
void tearDown() {
RequestContextHolder.resetRequestAttributes();
}
@Test
void recordsReturnedAjaxResultFailureAsFailedApiLog() {
when(joinPoint.getArgs()).thenReturn(new Object[0]);
com.ruoyi.common.annotation.ApiLog annotation = apiLogAnnotation();
aspect.doBefore(joinPoint, annotation);
aspect.doAfterReturning(joinPoint, annotation, new AjaxResult(1, "未找到对应作业"));
ArgumentCaptor<com.ruoyi.api.domain.ApiLog> logCaptor =
ArgumentCaptor.forClass(com.ruoyi.api.domain.ApiLog.class);
verify(apiLogService).record(logCaptor.capture());
com.ruoyi.api.domain.ApiLog apiLog = logCaptor.getValue();
assertThat(apiLog.getStatus()).isEqualTo("FAIL");
assertThat(apiLog.getHttpStatus()).isEqualTo(200);
assertThat(apiLog.getErrorMsg()).isEqualTo("未找到对应作业");
assertThat(apiLog.getResponseBody()).contains("未找到对应作业");
}
private com.ruoyi.common.annotation.ApiLog apiLogAnnotation() {
return new com.ruoyi.common.annotation.ApiLog() {
@Override
public String interfaceName() {
return "WCS-作业异常处理";
}
@Override
public String sysCode() {
return "WCS";
}
@Override
public String direction() {
return "INBOUND";
}
@Override
public boolean isSaveRequestData() {
return true;
}
@Override
public boolean isSaveResponseData() {
return true;
}
@Override
public String[] excludeParamNames() {
return new String[0];
}
@Override
public Class<? extends Annotation> annotationType() {
return com.ruoyi.common.annotation.ApiLog.class;
}
};
}
}

View File

@@ -3,10 +3,10 @@ package com.ruoyi.api.client;
import com.ruoyi.api.config.ExternalProp;
import com.ruoyi.api.constant.MsgTypeEnum;
import com.ruoyi.api.domain.ApiResponse;
import com.ruoyi.api.exception.BizException;
import com.ruoyi.in.job.domain.MydoJob;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -16,8 +16,8 @@ import static org.assertj.core.api.Assertions.assertThat;
class WcsClientTest {
@Test
void inboundAndOutboundJobPayloadsUseSameWcsReceiveEndpoint() {
RecordingWcsClient client = new RecordingWcsClient();
void wcsMethodsReturnMockValuesWithoutPostingToRealWcs() {
NoPostWcsClient client = new NoPostWcsClient(true);
MydoJob inboundJob = new MydoJob();
inboundJob.setJobId("RK001");
@@ -27,31 +27,108 @@ class WcsClientTest {
outboundJob.setJobId("CK001");
outboundJob.setInOrOut(20L);
client.sendPutawayTask(inboundJob);
client.sendRetrieveTask(outboundJob);
assertThat(client.urls).containsExactly(
"http://wcs/wcs/api/receiveWmsJob",
"http://wcs/wcs/api/receiveWmsJob");
assertThat(client.messageTypes).containsExactly(MsgTypeEnum.JSON, MsgTypeEnum.JSON);
assertThat(client.sendPutawayTask("RK001", "A0101", "TP001", "10").getData())
.isEqualTo("MOCK_WCS_PUTAWAY_OK");
assertThat(client.sendPutawayTask(inboundJob).getData())
.isEqualTo("MOCK_WCS_PUTAWAY_OK");
assertThat(client.sendRetrieveTask("CK001", "A0101", "M001", "1").getData())
.isEqualTo("MOCK_WCS_RETRIEVE_OK");
assertThat(client.sendRetrieveTask(outboundJob).getData())
.isEqualTo("MOCK_WCS_RETRIEVE_OK");
assertThat(client.sendCraneMove("CR01", "A0101", "A0102", "MOVE").getData())
.isEqualTo("MOCK_WCS_CRANE_MOVE_OK");
assertThat(client.cancelTask("TASK001").getData())
.isEqualTo("MOCK_WCS_CANCEL_OK");
assertThat(client.queryDeviceStatus("CRANE").getData()).hasSize(1);
assertThat(client.queryDeviceStatus("CRANE").getData().get(0).get("status"))
.isEqualTo("MOCK_ONLINE");
}
private static class RecordingWcsClient extends WcsClient {
private final ExternalProp externalProp;
private final List<String> urls = new ArrayList<>();
private final List<MsgTypeEnum> messageTypes = new ArrayList<>();
@Test
void wcsMethodsPostToConfiguredEndpointsWhenMockDisabled() {
RecordingWcsClient client = new RecordingWcsClient(false);
private RecordingWcsClient() {
externalProp = new ExternalProp();
ExternalProp.SystemConfig wcs = new ExternalProp.SystemConfig();
wcs.setBaseUrl("http://wcs");
Map<String, String> apis = new HashMap<>();
apis.put("sendPutawayTask", "/wcs/api/receiveWmsJob");
apis.put("sendRetrieveTask", "/task/dispatch");
wcs.setApis(apis);
Map<String, ExternalProp.SystemConfig> systems = new HashMap<>();
systems.put("WCS", wcs);
externalProp.setSystems(systems);
MydoJob inboundJob = new MydoJob();
inboundJob.setJobId("RK001");
inboundJob.setInOrOut(1L);
MydoJob outboundJob = new MydoJob();
outboundJob.setJobId("CK001");
outboundJob.setInOrOut(20L);
assertThat(client.sendPutawayTask("RK001", "A0101", "TP001", "10").getData())
.isEqualTo("ACCEPTED");
assertThat(client.sendPutawayTask(inboundJob).getData())
.isEqualTo("ACCEPTED");
assertThat(client.sendRetrieveTask("CK001", "A0101", "M001", "1").getData())
.isEqualTo("ACCEPTED");
assertThat(client.sendRetrieveTask(outboundJob).getData())
.isEqualTo("ACCEPTED");
assertThat(client.queryDeviceStatus("CRANE").getData().get(0).get("status"))
.isEqualTo("ONLINE");
assertThat(client.sendCraneMove("CR01", "A0101", "A0102", "MOVE").getData())
.isEqualTo("ACCEPTED");
assertThat(client.cancelTask("TASK001").getData())
.isEqualTo("ACCEPTED");
assertThat(client.calls).extracting(Call::url).containsExactly(
"http://wcs/wcs/api/receiveWmsJob",
"http://wcs/wcs/api/receiveWmsJob",
"http://wcs/task/dispatch",
"http://wcs/wcs/api/receiveWmsJob",
"http://wcs/device/status",
"http://wcs/crane/move",
"http://wcs/task/cancel");
assertThat(client.calls).extracting(Call::messageType).containsExactly(
MsgTypeEnum.TEXT,
MsgTypeEnum.JSON,
MsgTypeEnum.TEXT,
MsgTypeEnum.JSON,
MsgTypeEnum.TEXT,
MsgTypeEnum.TEXT,
MsgTypeEnum.TEXT);
}
@Test
void jsonAckTreatsCodeZeroAsSuccess() {
RecordingWcsClient client = new RecordingWcsClient(false,
"{\"code\":0,\"message\":\"ok\",\"data\":\"ACCEPTED\"}");
ApiResponse<String> response = client.sendPutawayTask(inboundJob());
assertThat(response.isSuccess()).isTrue();
assertThat(response.getData()).isEqualTo("ACCEPTED");
}
@Test
void jsonAckTreatsCodeOneAsFailure() {
RecordingWcsClient client = new RecordingWcsClient(false,
"{\"code\":1,\"message\":\"任务拒绝\"}");
ApiResponse<String> response = client.sendPutawayTask(inboundJob());
assertThat(response.isSuccess()).isFalse();
assertThat(response.getCode()).isEqualTo("BIZ_FAIL");
assertThat(response.getMessage()).isEqualTo("WCS拒绝: 任务拒绝");
}
@Test
void jsonAckNoLongerTreatsLegacyCodeTwoHundredAsSuccess() {
RecordingWcsClient client = new RecordingWcsClient(false,
"{\"code\":200,\"message\":\"旧成功码\"}");
ApiResponse<String> response = client.sendPutawayTask(inboundJob());
assertThat(response.isSuccess()).isFalse();
assertThat(response.getCode()).isEqualTo("BIZ_FAIL");
assertThat(response.getMessage()).isEqualTo("WCS拒绝: 旧成功码");
}
private static class NoPostWcsClient extends WcsClient {
private final ExternalProp externalProp;
private NoPostWcsClient(boolean mockEnabled) {
externalProp = testExternalProp(mockEnabled);
}
@Override
@@ -62,9 +139,76 @@ class WcsClientTest {
@Override
protected <T> ApiResponse<T> post(String url, String body, String interfaceName,
ResponseParser<T> parser, MsgTypeEnum messageType) {
urls.add(url);
messageTypes.add(messageType);
return ApiResponse.ok(null);
throw new AssertionError("WCS local mock must not post to real WCS");
}
}
private static class RecordingWcsClient extends WcsClient {
private final ExternalProp externalProp;
private final List<Call> calls = new java.util.ArrayList<>();
private final String jsonAckBody;
private RecordingWcsClient(boolean mockEnabled) {
this(mockEnabled, null);
}
private RecordingWcsClient(boolean mockEnabled, String jsonAckBody) {
externalProp = testExternalProp(mockEnabled);
this.jsonAckBody = jsonAckBody;
}
@Override
protected ExternalProp externalProp() {
return externalProp;
}
@Override
protected <T> ApiResponse<T> post(String url, String body, String interfaceName,
ResponseParser<T> parser, MsgTypeEnum messageType) {
calls.add(new Call(url, body, interfaceName, messageType));
try {
String rawBody;
if ("WCS-查询设备状态".equals(interfaceName)) {
rawBody = "OK|CR01,ONLINE,Y,";
} else if (jsonAckBody != null && MsgTypeEnum.JSON.equals(messageType)) {
rawBody = jsonAckBody;
} else {
rawBody = "OK|ACCEPTED";
}
return ApiResponse.ok(parser.parse(rawBody));
} catch (BizException e) {
return ApiResponse.fail("BIZ_FAIL", e.getMessage());
} catch (Exception e) {
return ApiResponse.fail("PARSE_FAIL", e.getMessage());
}
}
}
private static MydoJob inboundJob() {
MydoJob job = new MydoJob();
job.setJobId("RK001");
job.setInOrOut(1L);
return job;
}
private static ExternalProp testExternalProp(boolean mockEnabled) {
ExternalProp externalProp = new ExternalProp();
ExternalProp.SystemConfig wcs = new ExternalProp.SystemConfig();
wcs.setBaseUrl("http://wcs");
wcs.setMockEnabled(mockEnabled);
Map<String, String> apis = new HashMap<>();
apis.put("sendPutawayTask", "/wcs/api/receiveWmsJob");
apis.put("sendRetrieveTask", "/task/dispatch");
apis.put("queryDeviceStatus", "/device/status");
apis.put("sendCraneMove", "/crane/move");
apis.put("cancelTask", "/task/cancel");
wcs.setApis(apis);
Map<String, ExternalProp.SystemConfig> systems = new HashMap<>();
systems.put("WCS", wcs);
externalProp.setSystems(systems);
return externalProp;
}
private record Call(String url, String body, String interfaceName, MsgTypeEnum messageType) {
}
}

View File

@@ -0,0 +1,146 @@
package com.ruoyi.api.controller;
import com.ruoyi.api.client.WcsClient;
import com.ruoyi.api.domain.ApiResponse;
import com.ruoyi.api.domain.WcsInboundCompleteRequest;
import com.ruoyi.api.domain.WcsJobExceptionRequest;
import com.ruoyi.api.domain.WcsOutboundCompleteRequest;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.pda.service.PdaService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ExternalApiControllerWcsCodeTest {
@Mock
private WcsClient wcsClient;
@Mock
private IMydoJobService mydoJobService;
@Mock
private PdaService pdaService;
@InjectMocks
private ExternalApiController controller;
@Test
void wcsClientTriggerEndpointsReturnCodeZeroWhenWcsCallSucceeds() {
when(wcsClient.sendPutawayTask("RK001", "A0101", "TP001", "NORMAL"))
.thenReturn(ApiResponse.ok("PUTAWAY_OK"));
when(wcsClient.sendRetrieveTask("CK001", "A0101", "M001", "2"))
.thenReturn(ApiResponse.ok("RETRIEVE_OK"));
when(wcsClient.queryDeviceStatus("ALL"))
.thenReturn(ApiResponse.ok(List.of(Map.of("deviceId", "CR01", "status", "ONLINE"))));
when(wcsClient.sendCraneMove("CR01", "A0101", "A0102", "MOVE"))
.thenReturn(ApiResponse.ok("MOVE_OK"));
when(wcsClient.cancelTask("TASK001"))
.thenReturn(ApiResponse.ok("CANCEL_OK"));
assertWcsSuccess(controller.sendPutawayTask("RK001", "A0101", "TP001", "NORMAL"));
assertWcsSuccess(controller.sendRetrieveTask("CK001", "A0101", "M001", "2"));
assertWcsSuccess(controller.queryDeviceStatus("ALL"));
assertWcsSuccess(controller.sendCraneMove("CR01", "A0101", "A0102", "MOVE"));
assertWcsSuccess(controller.cancelWcsTask("TASK001"));
}
@Test
void wcsClientTriggerEndpointsReturnCodeOneWhenWcsCallFails() {
when(wcsClient.sendRetrieveTask("CK001", "A0101", "M001", "2"))
.thenReturn(ApiResponse.fail("WCS_NG", "WCS拒绝任务"));
AjaxResult result = controller.sendRetrieveTask("CK001", "A0101", "M001", "2");
assertWcsError(result, "WCS拒绝任务");
}
@Test
void wcsCallbackEndpointsReturnCodeZeroWhenBusinessHandlingSucceeds() {
MydoJob inboundJob = new MydoJob();
inboundJob.setJobId("RK001");
inboundJob.setInOrOut(1L);
when(mydoJobService.selectMydoJobByJobId("RK001")).thenReturn(inboundJob);
when(mydoJobService.completeInJobFromWcs("RK001")).thenReturn(AjaxResult.success("入库完成"));
MydoJob outboundJob = new MydoJob();
outboundJob.setJobId("CK001");
outboundJob.setInOrOut(20L);
when(mydoJobService.selectMydoJobByJobId("CK001")).thenReturn(outboundJob);
when(mydoJobService.completeOutJobFromWcs("CK001")).thenReturn(AjaxResult.success("出库完成"));
WcsInboundCompleteRequest inboundRequest = new WcsInboundCompleteRequest();
inboundRequest.setJobId("RK001");
WcsOutboundCompleteRequest outboundRequest = new WcsOutboundCompleteRequest();
outboundRequest.setJobId("CK001");
assertWcsSuccess(controller.completeInbound(inboundRequest));
assertWcsSuccess(controller.completeOutbound(outboundRequest));
}
@Test
void wcsCallbackEndpointsReturnCodeOneWhenBusinessHandlingFails() {
MydoJob outboundJob = new MydoJob();
outboundJob.setJobId("CK001");
outboundJob.setInOrOut(20L);
when(mydoJobService.selectMydoJobByJobId("CK001")).thenReturn(outboundJob);
when(mydoJobService.completeOutJobFromWcs("CK001")).thenReturn(AjaxResult.error("出库完成失败"));
WcsOutboundCompleteRequest request = new WcsOutboundCompleteRequest();
request.setJobId("CK001");
assertWcsError(controller.completeOutbound(request), "出库完成失败");
}
@Test
void wcsCallbackEndpointsRejectMismatchedJobTypes() {
MydoJob outboundJob = new MydoJob();
outboundJob.setJobId("CK001");
outboundJob.setInOrOut(20L);
when(mydoJobService.selectMydoJobByJobId("RK001")).thenReturn(outboundJob);
MydoJob inboundJob = new MydoJob();
inboundJob.setJobId("RK002");
inboundJob.setInOrOut(1L);
when(mydoJobService.selectMydoJobByJobId("RK002")).thenReturn(inboundJob);
WcsInboundCompleteRequest inboundRequest = new WcsInboundCompleteRequest();
inboundRequest.setJobId("RK001");
WcsOutboundCompleteRequest outboundRequest = new WcsOutboundCompleteRequest();
outboundRequest.setJobId("RK002");
assertWcsError(controller.completeInbound(inboundRequest), "非法入库作业");
assertWcsError(controller.completeOutbound(outboundRequest), "非法出库作业");
verify(mydoJobService, never()).completeInJobFromWcs("RK001");
verify(mydoJobService, never()).completeOutJobFromWcs("RK002");
}
@Test
void wcsCallbackValidationErrorsReturnCodeOne() {
WcsJobExceptionRequest request = new WcsJobExceptionRequest();
assertWcsError(controller.handleWcsException(request), "异常类型不能为空");
}
private void assertWcsSuccess(AjaxResult result) {
assertThat(result.get(AjaxResult.CODE_TAG)).isEqualTo(0);
}
private void assertWcsError(AjaxResult result, String messagePart) {
assertThat(result.get(AjaxResult.CODE_TAG)).isEqualTo(1);
assertThat(String.valueOf(result.get(AjaxResult.MSG_TAG))).contains(messagePart);
}
}

View File

@@ -0,0 +1,72 @@
package com.ruoyi.bas.cargospace.mapper;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class BasCargoSpaceMapperSqlTest {
@Test
void putOccupiedReallocationUsesTrayRuleCargoTypeInsteadOfOriginalCargoSpaceType() throws Exception {
String xml = Files.readString(resolveMapperXml());
String selectSql = section(xml, "<select id=\"selectAvailableInboundCargoSpaceInRoadway\"", "</select>");
assertThat(selectSql)
.contains("bas_tray_rule")
.contains("allowed_cargo_type")
.contains("left(#{trayCode}, len(btr.tray_type)) = btr.tray_type")
.doesNotContain("cs2.the_type");
}
@Test
void automaticCargoSpaceStatusUpdatesDoNotChangeManualLockedFlag() throws Exception {
String xml = Files.readString(resolveMapperXml());
for (String id : List.of(
"markOutboundAssignedByStockId",
"markOutboundAssignedByStockRef",
"markOutboundAssignedByStockoutDetailId",
"markOutboundAssignedByCargoSpaceId",
"releaseOutboundCargoSpace",
"markOutboundNeedCheck",
"markInboundNeedCheck")) {
String updateSql = section(xml, "<update id=\"" + id + "\"", "</update>");
assertThat(updateSql)
.doesNotContain("locked = 'Y'")
.doesNotContain("locked = 'N'");
}
}
@Test
void putOccupiedReallocationChoosesPlatoonWithMostAvailableCargoSpaces() throws Exception {
String xml = Files.readString(resolveMapperXml());
String selectSql = section(xml, "<select id=\"selectAvailableInboundCargoSpaceInRoadway\"", "</select>");
assertThat(selectSql)
.contains("count(1) over (partition by bcs.the_platoon) as platoon_empty_count")
.contains("order by platoon_empty_count desc");
}
private Path resolveMapperXml() {
return Stream.of(
Path.of("src/main/resources/mapper/bas/BasCargoSpaceMapper.xml"),
Path.of("ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml")
)
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private String section(String text, String startMarker, String endMarker) {
int start = text.indexOf(startMarker);
assertThat(start).isGreaterThanOrEqualTo(0);
int end = text.indexOf(endMarker, start);
assertThat(end).isGreaterThan(start);
return text.substring(start, end);
}
}

View File

@@ -0,0 +1,41 @@
package com.ruoyi.bas.stationpoint;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class BasStationPointConfigurationTest {
@Test
void pointSizeConfigurationIncludesCommonPointType() throws Exception {
String domain = Files.readString(resolvePath(
"src/main/java/com/ruoyi/bas/stationpoint/domain/BasStationPoint.java",
"ruoyi-admin/src/main/java/com/ruoyi/bas/stationpoint/domain/BasStationPoint.java"));
String vue = Files.readString(resolvePath(
"../ruoyi-ui/src/views/bas/stationpoint/index.vue",
"ruoyi-ui/src/views/bas/stationpoint/index.vue"));
String sql = Files.readString(resolvePath(
"../sql/bas_station_point.sql",
"sql/bas_station_point.sql"));
assertThat(domain)
.contains("1-大点位,2-小点位,3-通用点位")
.contains("1=大点位,2=小点位,3=通用点位");
assertThat(vue)
.contains("<el-option label=\"通用点位\" value=\"3\" />")
.contains("if (size === '3') return '通用点位'");
assertThat(sql)
.contains("1=大点位,2=小点位,3=通用点位");
}
private Path resolvePath(String modulePath, String rootPath) {
return Stream.of(Path.of(modulePath), Path.of(rootPath))
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
}

View File

@@ -0,0 +1,91 @@
package com.ruoyi.bas.stock.service.impl;
import com.ruoyi.bas.stock.domain.BasStock;
import com.ruoyi.bas.stock.domain.BasStockDetail;
import com.ruoyi.bas.stock.mapper.BasStockDetailMapper;
import com.ruoyi.bas.stock.mapper.BasStockMapper;
import com.ruoyi.bas.stockoper.mapper.BasStockOperRecordMapper;
import com.ruoyi.system.mapper.SysDeptMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class BasStockServiceImplTest {
@Mock
private BasStockMapper basStockMapper;
@Mock
private BasStockDetailMapper basStockDetailMapper;
@Mock
private BasStockOperRecordMapper basStockOperRecordMapper;
@Mock
private SysDeptMapper sysDeptMapper;
@InjectMocks
private BasStockServiceImpl service;
@Test
void insertBasStockGeneratesKcMillisecondTimestampStockIdWhenMissing() {
BasStock stock = new BasStock();
stock.setWarehouseId("WH01");
stock.setStockStatus("1");
when(basStockMapper.insertBasStock(stock)).thenReturn(1);
int rows = service.insertBasStock(stock);
assertThat(rows).isEqualTo(1);
ArgumentCaptor<BasStock> stockCaptor = ArgumentCaptor.forClass(BasStock.class);
verify(basStockMapper).insertBasStock(stockCaptor.capture());
assertThat(stockCaptor.getValue().getStockId()).matches("KC\\d{17}");
verify(basStockDetailMapper, never()).insertBasStockDetail(org.mockito.ArgumentMatchers.any());
}
@Test
void insertBasStockCreatesInitialDetailIdFromStockIdAndDetailSequence() {
BasStock stock = new BasStock();
stock.setStockId("KC20260624152234021");
stock.setProductId("MAT001");
stock.setProductName("物料1");
stock.setQuantity(new BigDecimal("6"));
when(basStockMapper.insertBasStock(stock)).thenReturn(1);
int rows = service.insertBasStock(stock);
assertThat(rows).isEqualTo(1);
ArgumentCaptor<BasStockDetail> detailCaptor = ArgumentCaptor.forClass(BasStockDetail.class);
verify(basStockDetailMapper).insertBasStockDetail(detailCaptor.capture());
assertThat(detailCaptor.getValue().getDetailId()).isEqualTo("KC20260624152234021-1");
}
@Test
void insertBasStockDetailCreatesDetailIdFromStockIdAndDetailSequence() {
BasStockDetail detail = new BasStockDetail();
detail.setStockId("KC20260624152234021");
detail.setProductId("MAT002");
detail.setProductName("物料2");
detail.setQuantity(new BigDecimal("4"));
when(basStockDetailMapper.insertBasStockDetail(any(BasStockDetail.class))).thenReturn(1);
int rows = service.insertBasStockDetail(detail);
assertThat(rows).isEqualTo(1);
ArgumentCaptor<BasStockDetail> detailCaptor = ArgumentCaptor.forClass(BasStockDetail.class);
verify(basStockDetailMapper).insertBasStockDetail(detailCaptor.capture());
assertThat(detailCaptor.getValue().getDetailId()).isEqualTo("KC20260624152234021-1");
}
}

View File

@@ -0,0 +1,125 @@
package com.ruoyi.in.job.mapper;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class MydoJobMapperSqlTest {
@Test
void inboundJobListFiltersByWarehouseIdSoWarehouseDropdownCanQueryReliably() throws Exception {
String xml = Files.readString(resolveMapperXml());
String selectInList = section(xml, "<select id=\"selectInMydoJobList\"", "</select>");
assertThat(selectInList)
.contains("<if test=\"warehouseId != null and warehouseId != ''\"> and warehouse_id = #{warehouseId}</if>");
}
@Test
void inboundCompletionProcedureUsesMillisecondTimestampStockId() throws Exception {
String script = Files.readString(resolveSqlPath()).replace("\r\n", "\n");
String proc = section(script, "CREATE PROCEDURE [dbo].[proc_complete_in_job]", "END\nGO");
assertThat(proc)
.contains("@now DATETIME2(3) = SYSDATETIME()")
.contains("FORMAT(@now, 'yyyyMMddHHmmssfff')")
.contains("SET @stock_id = 'KC' + FORMAT(@now, 'yyyyMMddHHmmssfff')");
}
@Test
void jobDictUpdateScriptAddsWaitingForPdaScanExecutionPhase() throws Exception {
String script = Files.readString(resolveJobDictScriptPath());
assertThat(script)
.contains("dict_type = N'mydo_job_execution_phase' AND dict_value = N'5'")
.contains("N'等待扫码确认'")
.contains("N'warning'")
.doesNotContain("INSERT INTO sys_dict_data (dict_code");
}
@Test
void inboundCancelProcedureAllowsReturnInboundJobs() throws Exception {
String script = Files.readString(resolveCancelInJobScriptPath()).replace("\r\n", "\n");
String proc = section(script, "CREATE PROCEDURE [dbo].[proc_hand_delete_in_job]", "END\nGO");
assertThat(proc)
.contains("AND in_or_out < 20")
.doesNotContain("AND in_or_out = 1");
}
@Test
void inboundCompletionProcedureDoesNotChangeManualLockedFlag() throws Exception {
String script = Files.readString(resolveSqlPath()).replace("\r\n", "\n");
String proc = section(script, "CREATE PROCEDURE [dbo].[proc_complete_in_job]", "END\nGO");
String cargoSpaceUpdate = section(proc, "UPDATE bas_cargo_space", "WHERE cargo_space_id = @cargo_space_id");
assertThat(cargoSpaceUpdate)
.contains("SET status = '2'")
.doesNotContain("locked =");
}
@Test
void inboundCancelProcedureDoesNotChangeManualLockedFlag() throws Exception {
String script = Files.readString(resolveCancelInJobScriptPath()).replace("\r\n", "\n");
String proc = section(script, "CREATE PROCEDURE [dbo].[proc_hand_delete_in_job]", "END\nGO");
String cargoSpaceUpdate = section(proc, "UPDATE dbo.bas_cargo_space", "DELETE FROM dbo.mydo_job_detail");
assertThat(cargoSpaceUpdate)
.contains("SET status = '1'")
.contains("AND status = '3'")
.doesNotContain("locked =")
.doesNotContain("AND locked = 'Y'");
}
private Path resolveMapperXml() {
return Stream.of(
Path.of("src/main/resources/mapper/in/MydoJobMapper.xml"),
Path.of("ruoyi-admin/src/main/resources/mapper/in/MydoJobMapper.xml")
)
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private Path resolveSqlPath() {
return Stream.of(
Path.of("../sql/bas_stock_master_detail.sql"),
Path.of("sql/bas_stock_master_detail.sql")
)
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private Path resolveJobDictScriptPath() {
return Stream.of(
Path.of("../sql/20260624_update_job_dicts.sql"),
Path.of("sql/20260624_update_job_dicts.sql")
)
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private Path resolveCancelInJobScriptPath() {
return Stream.of(
Path.of("../sql/proc_hand_delete_in_job.sql"),
Path.of("sql/proc_hand_delete_in_job.sql")
)
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private String section(String text, String startMarker, String endMarker) {
int start = text.indexOf(startMarker);
assertThat(start).isGreaterThanOrEqualTo(0);
int end = text.indexOf(endMarker, start);
assertThat(end).isGreaterThan(start);
return text.substring(start, end);
}
}

View File

@@ -2,20 +2,37 @@ package com.ruoyi.in.job.service.impl;
import com.ruoyi.api.client.WcsClient;
import com.ruoyi.api.domain.ApiResponse;
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
import com.ruoyi.bas.stock.mapper.BasStockDetailMapper;
import com.ruoyi.bas.stock.mapper.BasStockMapper;
import com.ruoyi.bas.stationpoint.service.IBasStationPointService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.mapper.MydoJobMapper;
import com.ruoyi.in.jobdetail.mapper.MydoJobDetailMapper;
import com.ruoyi.in.stockin.mapper.InStockinMapper;
import com.ruoyi.out.stockout.mapper.OutStockoutMapper;
import com.ruoyi.out.stockoutdetail.mapper.OutStockoutDetailMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -33,12 +50,44 @@ class MydoJobServiceImplTest {
@Mock
private IBasStationPointService basStationPointService;
@Mock
private BasCargoSpaceMapper basCargoSpaceMapper;
@Mock
private BasStockMapper basStockMapper;
@Mock
private BasStockDetailMapper basStockDetailMapper;
@Mock
private OutStockoutDetailMapper outStockoutDetailMapper;
@Mock
private OutStockoutMapper outStockoutMapper;
@Mock
private MydoJobDetailMapper mydoJobDetailMapper;
@Mock
private WcsClient wcsClient;
@InjectMocks
private MydoJobServiceImpl service;
@BeforeEach
void setUpSecurityContext() {
SysUser user = new SysUser();
user.setUserName("tester");
LoginUser loginUser = new LoginUser(10L, 20L, user, Collections.emptySet());
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()));
}
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void pushOutJobToWcsRejectsInboundJob() {
MydoJob job = new MydoJob();
@@ -88,7 +137,276 @@ class MydoJobServiceImplTest {
verify(mydoJobMapper).updateMydoJob(updateCaptor.capture());
MydoJob update = updateCaptor.getValue();
assertThat(update.getJobId()).isEqualTo("CK002");
assertThat(update.getStatus()).isEqualTo("2");
assertThat(update.getStatus()).isEqualTo("3");
assertThat(update.getExecutionPhase()).isEqualTo("1");
assertThat(update.getStartTime()).isNotBlank();
}
@Test
void cancelOutJobAsksWcsBeforeCancellingPushedJob() {
MydoJob job = new MydoJob();
job.setJobId("CK003");
job.setTaskNo("TASK003");
job.setStatus("2");
job.setInOrOut(20L);
when(mydoJobMapper.selectMydoJobByJobId("CK003")).thenReturn(job);
when(wcsClient.cancelTask("TASK003")).thenReturn(ApiResponse.ok("OK"));
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
java.util.HashMap<String, Object> map = invocation.getArgument(0);
map.put("v_result", "10");
return null;
}).when(mydoJobMapper).cancelOutJob(any(java.util.HashMap.class));
AjaxResult result = service.cancelJob("CK003", "2");
assertThat(result.isSuccess()).isTrue();
verify(wcsClient).cancelTask("TASK003");
verify(mydoJobMapper).cancelOutJob(any(java.util.HashMap.class));
}
@Test
void cancelOutJobStopsWhenWcsRejectsPushedJobCancellation() {
MydoJob job = new MydoJob();
job.setJobId("CK004");
job.setTaskNo("TASK004");
job.setStatus("2");
job.setInOrOut(20L);
when(mydoJobMapper.selectMydoJobByJobId("CK004")).thenReturn(job);
when(wcsClient.cancelTask("TASK004")).thenReturn(ApiResponse.fail("WCS_NG", "任务已执行不能取消"));
AjaxResult result = service.cancelJob("CK004", "2");
assertThat(result.isError()).isTrue();
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("WCS拒绝作废:任务已执行不能取消");
verify(wcsClient).cancelTask("TASK004");
verify(mydoJobMapper, never()).cancelOutJob(any(java.util.HashMap.class));
}
@Test
void completeOutJobFromWcsMarksJobWaitingForPdaScanAndReleasesCargoSpace() {
MydoJob job = new MydoJob();
job.setJobId("CK005");
job.setStatus("3");
job.setInOrOut(20L);
job.setCargoSpaceId("CS001");
job.setExitId("OUT01");
when(mydoJobMapper.selectMydoJobByJobId("CK005")).thenReturn(job);
when(mydoJobMapper.updateMydoJob(any(MydoJob.class))).thenReturn(1);
when(basCargoSpaceMapper.releaseOutboundCargoSpace("CS001")).thenReturn(1);
AjaxResult result = service.completeOutJobFromWcs("CK005");
assertThat(result.isSuccess()).isTrue();
ArgumentCaptor<MydoJob> updateCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobMapper).updateMydoJob(updateCaptor.capture());
MydoJob update = updateCaptor.getValue();
assertThat(update.getJobId()).isEqualTo("CK005");
assertThat(update.getStatus()).isNull();
assertThat(update.getExecutionPhase()).isEqualTo("5");
assertThat(update.getFinishTime()).isNull();
verify(basCargoSpaceMapper).releaseOutboundCargoSpace("CS001");
verify(mydoJobMapper, never()).makeSure(any(java.util.HashMap.class));
verifyNoInteractions(basStationPointService);
}
@Test
void completeInJobFromWcsRejectsOutboundJobType() {
MydoJob job = new MydoJob();
job.setJobId("CK-WRONG");
job.setInOrOut(20L);
when(mydoJobMapper.selectMydoJobByJobId("CK-WRONG")).thenReturn(job);
AjaxResult result = service.completeInJobFromWcs("CK-WRONG");
assertThat(result.isError()).isTrue();
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("非法入库作业");
verify(mydoJobMapper, never()).selectStockinIdsByJobId("CK-WRONG");
verify(mydoJobMapper, never()).completeJob(any(java.util.HashMap.class));
}
@Test
void completeOutJobFromWcsRejectsInboundJobType() {
MydoJob job = new MydoJob();
job.setJobId("RK-WRONG");
job.setInOrOut(1L);
when(mydoJobMapper.selectMydoJobByJobId("RK-WRONG")).thenReturn(job);
AjaxResult result = service.completeOutJobFromWcs("RK-WRONG");
assertThat(result.isError()).isTrue();
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("非法出库作业");
verify(mydoJobMapper, never()).updateMydoJob(any(MydoJob.class));
verifyNoInteractions(basCargoSpaceMapper);
}
@Test
void manualCompleteOutJobDoesNotReleaseCargoSpaceAfterProcedureCompletes() {
MydoJob job = new MydoJob();
job.setJobId("CK006");
job.setStatus("3");
job.setInOrOut(20L);
job.setCargoSpaceId("CS002");
job.setExitId("OUT02");
when(mydoJobMapper.selectMydoJobByJobId("CK006")).thenReturn(job);
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
java.util.HashMap<String, Object> map = invocation.getArgument(0);
map.put("v_result", "10");
return null;
}).when(mydoJobMapper).makeSure(any(java.util.HashMap.class));
AjaxResult result = service.completeJob("CK006", "2");
assertThat(result.isSuccess()).isTrue();
verify(mydoJobMapper).makeSure(any(java.util.HashMap.class));
verify(basCargoSpaceMapper, never()).releaseOutboundCargoSpace("CS002");
verify(basStationPointService).releaseBasStationPoint("OUT02");
}
@Test
void handleWcsPickEmptyExceptionDeletesAssociatedStockAndRollsBackDocumentAllocation() {
MydoJob job = new MydoJob();
job.setJobId("CK007");
job.setStatus("3");
job.setInOrOut(20L);
job.setCargoSpaceId("CS003");
job.setExitId("OUT03");
when(mydoJobMapper.selectMydoJobByJobId("CK007")).thenReturn(job);
when(mydoJobMapper.selectStockIdsByJobId("CK007")).thenReturn(Collections.singletonList("ST001"));
when(mydoJobMapper.selectStockoutDetailIdsByJobId("CK007")).thenReturn(Collections.singletonList("CKD001-1"));
when(basCargoSpaceMapper.markOutboundNeedCheck("CS003")).thenReturn(1);
when(outStockoutDetailMapper.rollbackAllotByJobId("CK007")).thenReturn(1);
when(basStockDetailMapper.deleteBasStockDetailByStockIds(any(String[].class))).thenReturn(1);
when(basStockMapper.deleteBasStockByStockIds(any(String[].class))).thenReturn(1);
when(outStockoutMapper.reopenOutStockoutByExceptionJobId("CK007")).thenReturn(1);
when(mydoJobDetailMapper.deleteMydoJobDetailByJobId("CK007")).thenReturn(1);
when(mydoJobMapper.deleteMydoJobByJobId("CK007")).thenReturn(1);
AjaxResult result = service.handleWcsPickEmptyException("CK007");
assertThat(result.isSuccess()).isTrue();
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("取货无货异常处理完成");
verify(basCargoSpaceMapper).markOutboundNeedCheck("CS003");
verify(outStockoutDetailMapper).rollbackAllotByJobId("CK007");
verify(basStockDetailMapper).deleteBasStockDetailByStockIds(argThat(ids ->
java.util.Arrays.equals(ids, new String[] {"ST001"})));
verify(basStockMapper).deleteBasStockByStockIds(argThat(ids ->
java.util.Arrays.equals(ids, new String[] {"ST001"})));
verify(basStockDetailMapper, never()).rollbackOutboundQuantityByJobId("CK007");
verify(basStockMapper, never()).refreshBasStockSummary("ST001");
verify(basStockMapper, never()).markExceptionByJobId("CK007");
verify(outStockoutMapper).reopenOutStockoutByExceptionJobId("CK007");
verify(mydoJobDetailMapper).deleteMydoJobDetailByJobId("CK007");
verify(mydoJobMapper).deleteMydoJobByJobId("CK007");
verify(basStationPointService).releaseBasStationPoint("OUT03");
}
@Test
void handleWcsPickEmptyExceptionWithoutStockDetailsStillCompletes() {
MydoJob job = new MydoJob();
job.setJobId("CK008");
job.setStatus("3");
job.setInOrOut(20L);
job.setCargoSpaceId("CS008");
job.setExitId("OUT08");
when(mydoJobMapper.selectMydoJobByJobId("CK008")).thenReturn(job);
when(mydoJobMapper.selectStockIdsByJobId("CK008")).thenReturn(Collections.emptyList());
when(basCargoSpaceMapper.markOutboundNeedCheck("CS008")).thenReturn(1);
when(mydoJobDetailMapper.deleteMydoJobDetailByJobId("CK008")).thenReturn(1);
when(mydoJobMapper.deleteMydoJobByJobId("CK008")).thenReturn(1);
AjaxResult result = service.handleWcsPickEmptyException("CK008");
assertThat(result.isSuccess()).isTrue();
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("取货无货异常处理完成");
verify(basCargoSpaceMapper).markOutboundNeedCheck("CS008");
verify(mydoJobDetailMapper).deleteMydoJobDetailByJobId("CK008");
verify(mydoJobMapper).deleteMydoJobByJobId("CK008");
verify(basStationPointService).releaseBasStationPoint("OUT08");
verifyNoInteractions(outStockoutDetailMapper);
verifyNoInteractions(basStockDetailMapper);
verifyNoInteractions(basStockMapper);
verifyNoInteractions(outStockoutMapper);
}
@Test
void handleWcsPickEmptyExceptionForLocationOutboundWithoutStockoutDocumentSkipsDocumentRollback() {
MydoJob job = new MydoJob();
job.setJobId("CK009");
job.setStatus("3");
job.setInOrOut(20L);
job.setCargoSpaceId("CS009");
job.setExitId("OUT09");
when(mydoJobMapper.selectMydoJobByJobId("CK009")).thenReturn(job);
when(mydoJobMapper.selectStockIdsByJobId("CK009")).thenReturn(Collections.singletonList("ST009"));
when(basCargoSpaceMapper.markOutboundNeedCheck("CS009")).thenReturn(1);
when(basStockDetailMapper.deleteBasStockDetailByStockIds(any(String[].class))).thenReturn(1);
when(basStockMapper.deleteBasStockByStockIds(any(String[].class))).thenReturn(1);
when(mydoJobDetailMapper.deleteMydoJobDetailByJobId("CK009")).thenReturn(1);
when(mydoJobMapper.deleteMydoJobByJobId("CK009")).thenReturn(1);
AjaxResult result = service.handleWcsPickEmptyException("CK009");
assertThat(result.isSuccess()).isTrue();
verify(basStockDetailMapper).deleteBasStockDetailByStockIds(argThat(ids ->
java.util.Arrays.equals(ids, new String[] {"ST009"})));
verify(basStockMapper).deleteBasStockByStockIds(argThat(ids ->
java.util.Arrays.equals(ids, new String[] {"ST009"})));
verify(basStockDetailMapper, never()).rollbackOutboundQuantityByJobId("CK009");
verify(basStockMapper, never()).refreshBasStockSummary("ST009");
verify(basStockMapper, never()).markExceptionByJobId("CK009");
verifyNoInteractions(outStockoutDetailMapper);
verify(outStockoutMapper, never()).reopenOutStockoutByExceptionJobId("CK009");
}
@Test
void handleWcsPutOccupiedExceptionMarksOldLocationAndReturnsReassignedInboundJob() {
MydoJob job = new MydoJob();
job.setJobId("RK002");
job.setStatus("3");
job.setInOrOut(10L);
job.setTrayCode("TRAY002");
job.setWarehouseId("WH01");
job.setRoadwayId("R01");
job.setCargoSpaceId("OLD-CS");
job.setCargoSpaceName("旧货位");
MydoJob updatedJob = new MydoJob();
updatedJob.setJobId("RK002");
updatedJob.setStatus("3");
updatedJob.setInOrOut(10L);
updatedJob.setTrayCode("TRAY002");
updatedJob.setWarehouseId("WH01");
updatedJob.setRoadwayId("R01");
updatedJob.setCargoSpaceId("NEW-CS");
updatedJob.setCargoSpaceName("新货位");
BasCargoSpace newCargoSpace = new BasCargoSpace();
newCargoSpace.setCargoSpaceId("NEW-CS");
newCargoSpace.setCargoSpaceName("新货位");
newCargoSpace.setRoadwayId("R01");
when(mydoJobMapper.selectMydoJobByJobId("RK002")).thenReturn(job, updatedJob);
when(basCargoSpaceMapper.selectAvailableInboundCargoSpaceInRoadway(job)).thenReturn(newCargoSpace);
when(basCargoSpaceMapper.updateBasCargoSpace(any(BasCargoSpace.class))).thenReturn(1);
when(basCargoSpaceMapper.markInboundNeedCheck("OLD-CS")).thenReturn(1);
when(mydoJobMapper.updateMydoJob(any(MydoJob.class))).thenReturn(1);
AjaxResult result = service.handleWcsPutOccupiedException("RK002");
assertThat(result.isSuccess()).isTrue();
assertThat(result.get(AjaxResult.DATA_TAG)).isSameAs(updatedJob);
verify(basCargoSpaceMapper).markInboundNeedCheck("OLD-CS");
verify(basCargoSpaceMapper).updateBasCargoSpace(argThat(cargoSpace ->
"NEW-CS".equals(cargoSpace.getCargoSpaceId())
&& "3".equals(cargoSpace.getStatus())
&& cargoSpace.getLocked() == null
&& "TRAY002".equals(cargoSpace.getTrayCode())));
verify(mydoJobMapper).updateMydoJob(argThat(update ->
"RK002".equals(update.getJobId())
&& "R01".equals(update.getRoadwayId())
&& "NEW-CS".equals(update.getCargoSpaceId())
&& "新货位".equals(update.getCargoSpaceName())));
}
}

View File

@@ -0,0 +1,97 @@
package com.ruoyi.in.stockin.service.impl;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.bas.id.job.service.JobIdService;
import com.ruoyi.in.sotckindetail.domain.InStockinDetail;
import com.ruoyi.in.sotckindetail.mapper.InStockinDetailMapper;
import com.ruoyi.in.stockin.domain.InStockinSaveDTO;
import com.ruoyi.in.stockin.mapper.InStockinMapper;
import com.ruoyi.system.mapper.SysDeptMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class InStockinServiceImplTest {
@Mock
private InStockinMapper inStockinMapper;
@Mock
private InStockinDetailMapper inStockinDetailMapper;
@Mock
private SysDeptMapper sysDeptMapper;
@Mock
private JobIdService jobIdService;
@InjectMocks
private InStockinServiceImpl service;
@BeforeEach
void setUpSecurityContext() {
SysUser user = new SysUser();
user.setUserName("tester");
LoginUser loginUser = new LoginUser(10L, 20L, user, Collections.emptySet());
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()));
}
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void saveWithDetailsUsesItemCodeAndItemNameForProductFields() {
when(jobIdService.getId("RKD")).thenReturn("RKD001");
SysDept dept = new SysDept();
dept.setDeptName("测试部门");
when(sysDeptMapper.selectDeptById(20L)).thenReturn(dept);
InStockinDetail detail = new InStockinDetail();
detail.setProductId("测试物料名称");
detail.setProductName("测试物料名称");
detail.setItemCod("MAT001");
detail.setItemNam("测试物料名称");
detail.setQuantity(new BigDecimal("6"));
InStockinSaveDTO saveDTO = new InStockinSaveDTO();
saveDTO.setWarehouseId("WH01");
saveDTO.setWarehouseName("一号仓");
saveDTO.setDetails(List.of(detail));
String stockinId = service.saveInStockinWithDetails(saveDTO);
assertThat(stockinId).isEqualTo("RKD001");
verify(inStockinMapper).insertInStockin(saveDTO);
ArgumentCaptor<InStockinDetail> detailCaptor = ArgumentCaptor.forClass(InStockinDetail.class);
verify(inStockinDetailMapper).insertInStockinDetail(detailCaptor.capture());
InStockinDetail insertedDetail = detailCaptor.getValue();
assertThat(insertedDetail.getStockinDetailId()).isEqualTo("RKD001-1");
assertThat(insertedDetail.getStockinId()).isEqualTo("RKD001");
assertThat(insertedDetail.getProductId()).isEqualTo("MAT001");
assertThat(insertedDetail.getProductName()).isEqualTo("测试物料名称");
assertThat(insertedDetail.getItemCod()).isEqualTo("MAT001");
assertThat(insertedDetail.getItemNam()).isEqualTo("测试物料名称");
}
}

View File

@@ -0,0 +1,109 @@
package com.ruoyi.kpi.service.impl;
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
import com.ruoyi.bas.stock.mapper.BasStockDetailMapper;
import com.ruoyi.in.job.mapper.MydoJobMapper;
import com.ruoyi.kpi.domain.InAnalysisData;
import com.ruoyi.kpi.domain.InAnalysisDataQuery;
import com.ruoyi.kpi.domain.OutAnalysisData;
import com.ruoyi.kpi.domain.OutAnalysisDataQuery;
import com.ruoyi.kpi.mapper.KpiMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class KpiServiceImplTest {
@Mock
private KpiMapper kpiMapper;
@Mock
private BasCargoSpaceMapper basCargoSpaceMapper;
@Mock
private BasStockDetailMapper basStockDetailMapper;
@Mock
private MydoJobMapper mydoJobMapper;
@InjectMocks
private KpiServiceImpl service;
@Test
void inboundTrendGroupsNullRoadwayAsUnassignedAndSumsDuplicateDates() {
InAnalysisData nullRoadway = new InAnalysisData();
nullRoadway.setDateDay("2026-06-24");
nullRoadway.setRoadwayId(null);
nullRoadway.setCount(2L);
InAnalysisData blankRoadway = new InAnalysisData();
blankRoadway.setDateDay("2026-06-24");
blankRoadway.setRoadwayId(" ");
blankRoadway.setCount(3L);
InAnalysisData roadwayA = new InAnalysisData();
roadwayA.setDateDay("2026-06-25");
roadwayA.setRoadwayId("A");
roadwayA.setCount(4L);
InAnalysisData roadwayADuplicate = new InAnalysisData();
roadwayADuplicate.setDateDay("2026-06-25");
roadwayADuplicate.setRoadwayId("A");
roadwayADuplicate.setCount(6L);
InAnalysisDataQuery query = new InAnalysisDataQuery();
query.setStartTime("2026-06-24");
query.setEndTime("2026-06-25");
when(mydoJobMapper.selectInMydoJobFinishListGroupByWarehouseId(query))
.thenReturn(List.of(nullRoadway, blankRoadway, roadwayA, roadwayADuplicate));
Map<String, Object> result = service.selectInboundTrend(query);
assertThat(result.get("xAxis")).isEqualTo(List.of("2026-06-24", "2026-06-25"));
assertThat(result.get("series")).asList()
.anySatisfy(series -> {
Map<?, ?> item = (Map<?, ?>) series;
assertThat(item.get("name")).isEqualTo("巷道未分配");
assertThat(item.get("data")).isEqualTo(List.of(5L, 0L));
})
.anySatisfy(series -> {
Map<?, ?> item = (Map<?, ?>) series;
assertThat(item.get("name")).isEqualTo("巷道A");
assertThat(item.get("data")).isEqualTo(List.of(0L, 10L));
});
}
@Test
void outboundTrendGroupsNullRoadwayAsUnassigned() {
OutAnalysisData data = new OutAnalysisData();
data.setDateDay("2026-06-25");
data.setRoadwayId(null);
data.setCount(7L);
OutAnalysisDataQuery query = new OutAnalysisDataQuery();
query.setStartTime("2026-06-25");
query.setEndTime("2026-06-25");
when(mydoJobMapper.selectOutMydoJobFinishListGroupByWarehouseId(query))
.thenReturn(List.of(data));
Map<String, Object> result = service.selectOutboundTrend(query);
assertThat(result.get("xAxis")).isEqualTo(List.of("2026-06-25"));
assertThat(result.get("series")).asList()
.singleElement()
.satisfies(series -> {
Map<?, ?> item = (Map<?, ?>) series;
assertThat(item.get("name")).isEqualTo("巷道未分配");
assertThat(item.get("data")).isEqualTo(List.of(7L));
});
}
}

View File

@@ -0,0 +1,199 @@
package com.ruoyi.out.location.service.impl;
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
import com.ruoyi.bas.id.job.service.JobIdService;
import com.ruoyi.bas.id.taskno.service.TaskNoService;
import com.ruoyi.bas.stationpoint.domain.BasStationPoint;
import com.ruoyi.bas.stationpoint.service.IBasStationPointService;
import com.ruoyi.bas.stock.domain.BasStockDetail;
import com.ruoyi.bas.stock.mapper.BasStockMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
import com.ruoyi.in.jobdetail.service.IMydoJobDetailService;
import com.ruoyi.out.location.domain.LocationOutboundGenerateRequest;
import com.ruoyi.out.location.domain.LocationOutboundStock;
import com.ruoyi.out.location.mapper.LocationOutboundMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LocationOutboundServiceImplTest {
@Mock
private LocationOutboundMapper locationOutboundMapper;
@Mock
private BasCargoSpaceMapper basCargoSpaceMapper;
@Mock
private BasStockMapper basStockMapper;
@Mock
private IBasStationPointService basStationPointService;
@Mock
private IMydoJobService mydoJobService;
@Mock
private IMydoJobDetailService mydoJobDetailService;
@Mock
private JobIdService jobIdService;
@Mock
private TaskNoService taskNoService;
@InjectMocks
private LocationOutboundServiceImpl service;
@Test
void generateWholeTrayOutboundJobReservesEveryAvailableStockDetailAndLocksCargoSpace() {
LocationOutboundStock stock = new LocationOutboundStock();
stock.setStockId("KC001");
stock.setCargoSpaceId("CS001");
stock.setCargoSpaceName("01排001列01层");
stock.setCargoSpaceStatus("2");
stock.setTrayCode("A001");
stock.setTrayType("A");
stock.setWarehouseId("WH01");
stock.setWarehouseName("一号仓");
stock.setRoadwayId("R01");
stock.setSupplierId("S001");
stock.setSupplierName("供应商1");
stock.setAvailableQuantity(new BigDecimal("12"));
when(locationOutboundMapper.selectLocationOutboundStockForUpdate("KC001")).thenReturn(stock);
BasStockDetail detailA = stockDetail("SD001", "MAT001", "物料1", "L001", "8");
BasStockDetail detailB = stockDetail("SD002", "MAT002", "物料2", "L002", "4");
when(locationOutboundMapper.selectAvailableDetailsForUpdate("KC001")).thenReturn(List.of(detailA, detailB));
BasStationPoint exitPoint = new BasStationPoint();
exitPoint.setPointId("OUT01");
exitPoint.setWarehouseId("WH01");
exitPoint.setPointType("2");
exitPoint.setStatus("0");
exitPoint.setPointStatus("0");
exitPoint.setTrayType("A,B");
when(basStationPointService.selectBasStationPointByPointId("OUT01")).thenReturn(exitPoint);
when(jobIdService.getId("CK")).thenReturn("CK001");
when(taskNoService.getTaskNo("20")).thenReturn("50001");
when(mydoJobService.insertMydoJob(any(MydoJob.class))).thenReturn(1);
when(mydoJobDetailService.insertMydoJobDetail(any(MydoJobDetail.class))).thenReturn(1);
when(locationOutboundMapper.reserveStockDetailForOutbound(any(HashMap.class))).thenReturn(1);
when(basStockMapper.refreshBasStockSummary("KC001")).thenReturn(1);
when(basCargoSpaceMapper.markOutboundAssignedByStockId("KC001")).thenReturn(1);
when(mydoJobService.pushOutJobToWcs("CK001")).thenReturn(AjaxResult.success("WCS出库作业下发成功:OK"));
LocationOutboundGenerateRequest request = new LocationOutboundGenerateRequest();
request.setStockId("KC001");
request.setExitId("OUT01");
String result = service.generateWholeTrayOutboundJob(request);
assertThat(result).contains("CK001").contains("WCS出库作业下发成功");
ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).insertMydoJob(jobCaptor.capture());
MydoJob job = jobCaptor.getValue();
assertThat(job.getJobId()).isEqualTo("CK001");
assertThat(job.getTaskNo()).isEqualTo("50001");
assertThat(job.getInOrOut()).isEqualTo(20L);
assertThat(job.getStatus()).isEqualTo("1");
assertThat(job.getCargoSpaceId()).isEqualTo("CS001");
assertThat(job.getExitId()).isEqualTo("OUT01");
ArgumentCaptor<MydoJobDetail> detailCaptor = ArgumentCaptor.forClass(MydoJobDetail.class);
verify(mydoJobDetailService, org.mockito.Mockito.times(2)).insertMydoJobDetail(detailCaptor.capture());
List<MydoJobDetail> jobDetails = detailCaptor.getAllValues();
assertThat(jobDetails).extracting(MydoJobDetail::getJobDetailId).containsExactly("CK001-1", "CK001-2");
assertThat(jobDetails).extracting(MydoJobDetail::getProductId).containsExactly("MAT001", "MAT002");
assertThat(jobDetails).extracting(MydoJobDetail::getOutQuantity)
.containsExactly(new BigDecimal("8"), new BigDecimal("4"));
assertThat(jobDetails).extracting(MydoJobDetail::getStockinDetailId).containsExactly("SD001", "SD002");
ArgumentCaptor<HashMap> reserveCaptor = ArgumentCaptor.forClass(HashMap.class);
verify(locationOutboundMapper, org.mockito.Mockito.times(2)).reserveStockDetailForOutbound(reserveCaptor.capture());
assertThat(reserveCaptor.getAllValues()).extracting(map -> map.get("stockDetailId"))
.containsExactly("SD001", "SD002");
verify(basStockMapper).refreshBasStockSummary("KC001");
verify(basCargoSpaceMapper).markOutboundAssignedByStockId("KC001");
verify(mydoJobService).pushOutJobToWcs("CK001");
}
@Test
void generateWholeTrayOutboundJobAllowsEmptyAbnormalLocationWithoutStockDetails() {
LocationOutboundStock location = new LocationOutboundStock();
location.setCargoSpaceId("CS-EMPTY");
location.setCargoSpaceName("02排001列01层");
location.setCargoSpaceStatus("6");
location.setWarehouseId("WH01");
location.setWarehouseName("一号仓");
location.setRoadwayId("R01");
when(locationOutboundMapper.selectLocationOutboundLocationForUpdate("CS-EMPTY")).thenReturn(location);
BasStationPoint exitPoint = new BasStationPoint();
exitPoint.setPointId("OUT01");
exitPoint.setWarehouseId("WH01");
exitPoint.setPointType("2");
exitPoint.setStatus("0");
exitPoint.setPointStatus("0");
when(basStationPointService.selectBasStationPointByPointId("OUT01")).thenReturn(exitPoint);
when(jobIdService.getId("CK")).thenReturn("CK002");
when(taskNoService.getTaskNo("20")).thenReturn("50002");
when(mydoJobService.insertMydoJob(any(MydoJob.class))).thenReturn(1);
when(basCargoSpaceMapper.markOutboundAssignedByCargoSpaceId("CS-EMPTY")).thenReturn(1);
when(mydoJobService.pushOutJobToWcs("CK002")).thenReturn(AjaxResult.success("WCS出库作业下发成功:OK"));
LocationOutboundGenerateRequest request = new LocationOutboundGenerateRequest();
request.setCargoSpaceId("CS-EMPTY");
request.setExitId("OUT01");
String result = service.generateWholeTrayOutboundJob(request);
assertThat(result).contains("CK002").contains("WCS出库作业下发成功");
ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).insertMydoJob(jobCaptor.capture());
MydoJob job = jobCaptor.getValue();
assertThat(job.getJobId()).isEqualTo("CK002");
assertThat(job.getTaskNo()).isEqualTo("50002");
assertThat(job.getInOrOut()).isEqualTo(20L);
assertThat(job.getCargoSpaceId()).isEqualTo("CS-EMPTY");
assertThat(job.getCargoSpaceName()).isEqualTo("02排001列01层");
assertThat(job.getTrayCode()).isNull();
verify(locationOutboundMapper, never()).selectAvailableDetailsForUpdate(any());
verify(mydoJobDetailService, never()).insertMydoJobDetail(any(MydoJobDetail.class));
verify(locationOutboundMapper, never()).reserveStockDetailForOutbound(any(HashMap.class));
verify(basStockMapper, never()).refreshBasStockSummary(any());
verify(basCargoSpaceMapper).markOutboundAssignedByCargoSpaceId("CS-EMPTY");
verify(mydoJobService).pushOutJobToWcs("CK002");
}
private BasStockDetail stockDetail(String detailId, String productId, String productName, String lotNo, String availableQuantity) {
BasStockDetail detail = new BasStockDetail();
detail.setDetailId(detailId);
detail.setStockId("KC001");
detail.setProductId(productId);
detail.setProductName(productName);
detail.setLotNo(lotNo);
detail.setAvailableQuantity(new BigDecimal(availableQuantity));
detail.setQuantity(new BigDecimal(availableQuantity));
detail.setOutQuantity(BigDecimal.ZERO);
return detail;
}
}

View File

@@ -0,0 +1,213 @@
package com.ruoyi.out.stockout.mapper;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class OutStockoutMapperSqlTest {
@Test
void allocationProcedureCallsPassExitPointParameter() throws Exception {
String xml = Files.readString(resolvePath(
"src/main/resources/mapper/out/OutStockoutMapper.xml",
"ruoyi-admin/src/main/resources/mapper/out/OutStockoutMapper.xml"));
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String autoCall = section(xml, "<select id=\"autoAssignOutStockoutByProc\"", "</select>");
String manualCall = section(xml, "<select id=\"manualAssignOutStockoutByProc\"", "</select>");
String autoProc = section(script, "CREATE PROCEDURE dbo.proc_out_auto_allocation", "AS");
String manualProc = section(script, "CREATE PROCEDURE dbo.proc_out_hand_allocation", "AS");
assertThat(autoCall)
.contains("proc_out_auto_allocation")
.contains("#{v_exit_id,mode=IN,jdbcType=VARCHAR}");
assertThat(manualCall)
.contains("proc_out_hand_allocation")
.contains("#{v_exit_id,mode=IN,jdbcType=VARCHAR}");
assertThat(autoProc).contains("@v_exit_id varchar(80)");
assertThat(manualProc).contains("@v_exit_id varchar(80)");
}
@Test
void manualAllocationStockDetailListOnlyShowsNormalCargoSpaceStock() throws Exception {
String xml = Files.readString(resolvePath(
"src/main/resources/mapper/bas/BasStockDetailMapper.xml",
"ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml"));
String selectList = section(xml, "<select id=\"selectBasStockDetailList\"", "</select>");
assertThat(selectList).contains("bcs.status = '2'");
}
@Test
void stockListAndStockDetailListFallbackToWarehouseTableName() throws Exception {
String stockXml = Files.readString(resolvePath(
"src/main/resources/mapper/bas/BasStockMapper.xml",
"ruoyi-admin/src/main/resources/mapper/bas/BasStockMapper.xml"));
String stockDetailXml = Files.readString(resolvePath(
"src/main/resources/mapper/bas/BasStockDetailMapper.xml",
"ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml"));
String stockSelect = section(stockXml, "<select id=\"selectBasStockList\"", "</select>");
String detailSelectVo = section(stockDetailXml, "<sql id=\"selectBasStockDetailVo\">", "</sql>");
assertThat(stockSelect)
.contains("left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id")
.contains("ISNULL(NULLIF(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name");
assertThat(detailSelectVo)
.contains("left join bas_warehouse bw on bs.warehouse_id = bw.warehouse_id")
.contains("ISNULL(NULLIF(bs.warehouse_name, ''), bw.warehouse_name) as warehouse_name");
}
@Test
void outboundAllocationProcedureChecksExitTrayType() throws Exception {
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String manualProc = section(script, "CREATE PROCEDURE dbo.proc_out_hand_allocation", "IF OBJECT_ID(N'dbo.proc_out_auto_allocation'");
assertThat(manualProc)
.contains("@tray_type nvarchar(80)")
.contains("bas_barcode_format")
.contains("charindex(',' + @tray_type + ',', ',' + sp.tray_type + ',') > 0")
.contains("出库口不支持当前托盘类型");
}
@Test
void outboundAllocationProcedureReusesExistingTrayJobForSameStockout() throws Exception {
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String manualProc = section(script, "CREATE PROCEDURE dbo.proc_out_hand_allocation", "IF OBJECT_ID(N'dbo.proc_out_auto_allocation'");
assertThat(manualProc)
.contains("@existing_job_id nvarchar(80)")
.contains("OR EXISTS (")
.contains("INNER JOIN dbo.mydo_job_detail ejd ON ej.job_id = ejd.job_id")
.contains("AND ejd.in_or_out_id = @stockout_id")
.contains("SET @job_id = @existing_job_id")
.contains("SET @job_detail_id = @job_id + '-' + CONVERT(nvarchar(10), @row_idx)");
}
@Test
void outboundAllocationMarksPickingJobWhenAllocatedQuantityIsLessThanTrayStock() throws Exception {
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String manualProc = section(script, "CREATE PROCEDURE dbo.proc_out_hand_allocation", "IF OBJECT_ID(N'dbo.proc_out_auto_allocation'");
assertThat(manualProc)
.contains("@job_type int")
.contains("SET @job_type = CASE WHEN @alloc_qty < @available_qty THEN 21 ELSE 20 END")
.contains("@job_id, '1', @task_no, @job_type")
.contains("SET in_or_out = CASE WHEN @job_type = 21 THEN 21 ELSE in_or_out END");
}
@Test
void outboundAllocationOnlyTreatsActiveOutboundJobDetailAsReserved() throws Exception {
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String manualProc = section(script, "CREATE PROCEDURE dbo.proc_out_hand_allocation", "IF OBJECT_ID(N'dbo.proc_out_auto_allocation'");
String autoProc = section(script, "CREATE PROCEDURE dbo.proc_out_auto_allocation", "IF OBJECT_ID(N'dbo.proc_out_makesure'");
assertThat(manualProc)
.contains("OR NOT EXISTS (")
.contains("INNER JOIN dbo.mydo_job lock_j ON lock_j.job_id = lock_jd.job_id")
.contains("WHERE lock_jd.job_detail_id = bsd.job_detail_id")
.contains("AND lock_j.in_or_out >= 20")
.contains("AND lock_j.status IN ('1', '2', '3')");
assertThat(autoProc)
.contains("OR NOT EXISTS (")
.contains("WHERE lock_jd.job_detail_id = bsd.job_detail_id")
.contains("AND lock_j.in_or_out >= 20")
.contains("OR EXISTS (")
.contains("AND ejd.in_or_out_id = @stockout_id");
}
@Test
void outboundConfirmationArchivesDeletedStockIntoFinishTables() throws Exception {
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String makeSureProc = section(script, "CREATE PROCEDURE dbo.proc_out_makesure", "IF OBJECT_ID(N'dbo.proc_hand_delete_out_job'");
assertThat(script)
.contains("CREATE TABLE dbo.bas_stock_finish")
.contains("CREATE TABLE dbo.bas_stock_detail_finish")
.contains("delete_time datetime2(7)");
assertThat(makeSureProc)
.contains("INSERT INTO dbo.bas_stock_detail_finish")
.contains("INSERT INTO dbo.bas_stock_finish")
.contains("SYSDATETIME() AS delete_time");
}
@Test
void outboundConfirmationDoesNotReoccupyOriginalCargoSpaceAfterWcsRelease() throws Exception {
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String makeSureProc = section(script, "CREATE PROCEDURE dbo.proc_out_makesure", "IF OBJECT_ID(N'dbo.proc_hand_delete_out_job'");
String residualBranch = section(makeSureProc,
"IF EXISTS (SELECT 1 FROM dbo.bas_stock_detail WHERE stock_id = @stock_id)",
"INSERT INTO dbo.bas_stock_finish");
assertThat(residualBranch)
.doesNotContain("UPDATE dbo.bas_cargo_space")
.doesNotContain("SET status = '2'")
.doesNotContain("tray_code = @tray_code");
}
@Test
void outboundAllocationAndCancelDoNotChangeManualLockedFlag() throws Exception {
String script = Files.readString(resolvePath(
"../sql/out_stockout_flow.sql",
"sql/out_stockout_flow.sql"));
String manualProc = section(script, "CREATE PROCEDURE dbo.proc_out_hand_allocation", "IF OBJECT_ID(N'dbo.proc_out_auto_allocation'");
String cancelProc = section(script, "CREATE PROCEDURE dbo.proc_hand_delete_out_job", "END\nGO");
assertThat(manualProc)
.contains("UPDATE dbo.bas_cargo_space")
.contains("SET status = '4'")
.doesNotContain("locked = 'Y'")
.doesNotContain("locked = 'N'");
assertThat(cancelProc)
.contains("UPDATE dbo.bas_cargo_space")
.contains("SET status = '2'")
.doesNotContain("locked = 'Y'")
.doesNotContain("locked = 'N'");
}
@Test
void locationOutboundListOnlyTreatsOccupiedCargoSpacesAsInStock() throws Exception {
String xml = Files.readString(resolvePath(
"src/main/resources/mapper/out/LocationOutboundMapper.xml",
"ruoyi-admin/src/main/resources/mapper/out/LocationOutboundMapper.xml"));
String selectList = section(xml, "<select id=\"selectLocationOutboundStockList\"", "</select>");
assertThat(selectList)
.contains("left join bas_stock bs on bs.cargo_space_id = bcs.cargo_space_id\n and bcs.status in ('2', '4')")
.contains("and bcs.status = '2'")
.contains("and bs.stock_id is not null");
}
private Path resolvePath(String modulePath, String rootPath) {
return Stream.of(Path.of(modulePath), Path.of(rootPath))
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private String section(String text, String startMarker, String endMarker) {
int start = text.indexOf(startMarker);
assertThat(start).isGreaterThanOrEqualTo(0);
int end = text.indexOf(endMarker, start);
assertThat(end).isGreaterThan(start);
return text.substring(start, end);
}
}

View File

@@ -0,0 +1,105 @@
package com.ruoyi.out.stockout.service.impl;
import com.ruoyi.bas.id.job.service.JobIdService;
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.out.stockout.mapper.OutStockoutMapper;
import com.ruoyi.out.stockoutdetail.domain.OutStockoutDetail;
import com.ruoyi.out.stockoutdetail.mapper.OutStockoutDetailMapper;
import com.ruoyi.system.mapper.SysDeptMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OutStockoutServiceImplTest {
@Mock
private OutStockoutMapper outStockoutMapper;
@Mock
private OutStockoutDetailMapper outStockoutDetailMapper;
@Mock
private SysDeptMapper sysDeptMapper;
@Mock
private JobIdService jobIdService;
@Mock
private IMydoJobService mydoJobService;
@Mock
private BasCargoSpaceMapper basCargoSpaceMapper;
@InjectMocks
private OutStockoutServiceImpl service;
@Test
void autoAssignReturnsErrorWhenProcedureReportsFailure() {
OutStockoutDetail detail = new OutStockoutDetail();
detail.setStockoutDetailId("CKD-1");
when(outStockoutDetailMapper.selectOutStockoutDetailListByStockoutId("CKD"))
.thenReturn(List.of(detail));
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
HashMap<String, Object> map = invocation.getArgument(0);
map.put("v_result", 2);
map.put("v_resultString", "没有可分配库存明细");
return null;
}).when(outStockoutMapper).autoAssignOutStockoutByProc(any());
AjaxResult result = service.autoAssignOutStockout("CKD", "02-AU-01");
assertThat(result.isError()).isTrue();
assertThat(result.get(AjaxResult.MSG_TAG).toString())
.contains("CKD-1")
.contains("没有可分配库存明细");
verify(mydoJobService, never()).selectPendingOutMydoJobsByStockoutDetailId(any());
}
@Test
void autoAssignDoesNotPushAnyJobUntilAllDetailsAreAllocated() {
OutStockoutDetail first = new OutStockoutDetail();
first.setStockoutDetailId("CKD-1");
OutStockoutDetail second = new OutStockoutDetail();
second.setStockoutDetailId("CKD-2");
when(outStockoutDetailMapper.selectOutStockoutDetailListByStockoutId("CKD"))
.thenReturn(List.of(first, second));
AtomicInteger callCount = new AtomicInteger();
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
HashMap<String, Object> map = invocation.getArgument(0);
if (callCount.incrementAndGet() == 1) {
map.put("v_result", 10);
map.put("v_resultString", "自动分配完成");
} else {
map.put("v_result", 2);
map.put("v_resultString", "没有可分配库存明细");
}
return null;
}).when(outStockoutMapper).autoAssignOutStockoutByProc(any());
AjaxResult result = service.autoAssignOutStockout("CKD", "02-AU-01");
assertThat(result.isError()).isTrue();
assertThat(result.get(AjaxResult.MSG_TAG).toString())
.contains("CKD-2")
.contains("没有可分配库存明细");
verify(mydoJobService, never()).selectPendingOutMydoJobsByStockoutDetailId(any());
}
}

View File

@@ -0,0 +1,112 @@
package com.ruoyi.pda.mapper;
import org.junit.jupiter.api.Test;
import org.apache.ibatis.annotations.Param;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class PdaMapperSqlTest {
@Test
void emptyTrayOutboundCountWarehouseParameterIsNamedForMyBatisXml() throws Exception {
Method method = PdaMapper.class.getMethod("selectAvailableEmptyTrayTypeCounts", String.class);
Param param = method.getParameters()[0].getAnnotation(Param.class);
assertThat(param).isNotNull();
assertThat(param.value()).isEqualTo("warehouseId");
}
@Test
void trayStockQueryTrayCodeParameterIsNamedForMyBatisXml() throws Exception {
Method method = PdaMapper.class.getMethod("selectTrayStockMaterials", String.class);
Param param = method.getParameters()[0].getAnnotation(Param.class);
assertThat(param).isNotNull();
assertThat(param.value()).isEqualTo("trayCode");
}
@Test
void trayStockQueryUsesTrayCodeOnlyAndReturnsMaterialRows() throws Exception {
String xml = Files.readString(resolveMapperXml());
String trayStockSql = section(xml, "<select id=\"selectTrayStockMaterials\"", "</select>");
assertThat(trayStockSql)
.contains("bs.tray_code = #{trayCode}")
.contains("bsd.product_id as materialCode")
.contains("bsd.product_name as materialName")
.contains("isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) as availableQuantity")
.doesNotContain("#{warehouseId}");
}
@Test
void emptyTrayOutboundCountsGroupByBarcodeFormatPrefix() throws Exception {
String xml = Files.readString(resolveMapperXml());
String typeCountSql = section(xml, "<select id=\"selectAvailableEmptyTrayTypeCounts\"", "</select>");
String countSql = section(xml, "<select id=\"countAvailableEmptyTrays\"", "</select>");
String stockSql = section(xml, "<select id=\"selectAvailableEmptyTrayStocks\"", "</select>");
assertThat(typeCountSql)
.contains("inner join bas_barcode_format bbf")
.contains("bbf.prefix as barcodePrefix")
.contains("left(bs.tray_code, len(bbf.prefix)) = bbf.prefix")
.contains("not exists")
.doesNotContain("bsd.job_detail_id is null");
assertThat(countSql)
.contains("isnull(bsd.quantity, 0) - isnull(bsd.out_quantity, 0) &gt; 0")
.doesNotContain("bsd.job_detail_id is null");
assertThat(stockSql)
.contains("left(bs.tray_code, len(#{barcodePrefix})) = #{barcodePrefix}")
.doesNotContain("bsd.job_detail_id is null");
}
@Test
void inboundCargoSpaceLockChoosesPlatoonWithMostEmptySpacesFirst() throws Exception {
String xml = Files.readString(resolveMapperXml());
String lockSql = section(xml, "<update id=\"lockInboundCargoSpace\"", "</update>");
assertThat(lockSql)
.contains("count(1) over (partition by bcs.the_platoon) as platoon_empty_count")
.contains("order by platoon_empty_count desc");
}
@Test
void outboundTaskListShowsExecutionPhaseFiveAsWaitingForPdaScan() throws Exception {
String xml = Files.readString(resolveMapperXml());
String taskListSql = section(xml, "<select id=\"outboundTaskList\"", "</select>");
String detailSql = section(xml, "<select id=\"selectOutboundDetail\"", "</select>");
assertThat(taskListSql)
.contains("j.execution_phase as executionPhase")
.contains("when j.execution_phase = '5' then '待扫码确认'")
.contains("and j.status in ('1', '2', '3')")
.contains("group by j.job_id, j.task_no, j.exit_id, j.create_time, j.status, j.execution_phase");
assertThat(detailSql)
.contains("j.execution_phase as executionPhase")
.contains("and j.status in ('1', '2', '3')");
}
private Path resolveMapperXml() {
return Stream.of(
Path.of("src/main/resources/mapper/pda/PdaMapper.xml"),
Path.of("ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml")
)
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private String section(String text, String startMarker, String endMarker) {
int start = text.indexOf(startMarker);
assertThat(start).isGreaterThanOrEqualTo(0);
int end = text.indexOf(endMarker, start);
assertThat(end).isGreaterThan(start);
return text.substring(start, end);
}
}

View File

@@ -0,0 +1,483 @@
package com.ruoyi.pda.service.impl;
import com.ruoyi.api.client.WcsClient;
import com.ruoyi.api.domain.ApiResponse;
import com.ruoyi.bas.id.job.service.JobIdService;
import com.ruoyi.bas.id.taskno.service.TaskNoService;
import com.ruoyi.bas.stationpoint.domain.BasStationPoint;
import com.ruoyi.bas.stationpoint.service.IBasStationPointService;
import com.ruoyi.bas.warehouse.domain.BasWarehouse;
import com.ruoyi.bas.warehouse.service.IBasWarehouseService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
import com.ruoyi.in.jobdetail.service.IMydoJobDetailService;
import com.ruoyi.in.stockin.mapper.InStockinMapper;
import com.ruoyi.in.sotckindetail.mapper.InStockinDetailMapper;
import com.ruoyi.pda.domain.AddInJobRequest;
import com.ruoyi.pda.domain.EmptyTrayOutboundRequest;
import com.ruoyi.pda.mapper.PdaMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
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.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PdaServiceImplTest {
@Mock
private PdaMapper pdaMapper;
@Mock
private InStockinDetailMapper inStockinDetailMapper;
@Mock
private InStockinMapper inStockinMapper;
@Mock
private IBasWarehouseService basWarehouseService;
@Mock
private IBasStationPointService basStationPointService;
@Mock
private IMydoJobService mydoJobService;
@Mock
private IMydoJobDetailService mydoJobDetailService;
@Mock
private WcsClient wcsClient;
@Mock
private JobIdService jobIdService;
@Mock
private TaskNoService taskNoService;
@InjectMocks
private PdaServiceImpl service;
@BeforeEach
void setUpSecurityContext() {
SysUser user = new SysUser();
user.setUserName("tester");
LoginUser loginUser = new LoginUser(10L, 20L, user, Collections.emptySet());
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()));
}
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void addInJobMarksJobDispatchedAfterWcsAcceptsIt() {
AddInJobRequest request = new AddInJobRequest();
request.setTrayCode("A001");
request.setWarehouseId("WH01");
request.setStationPointId("IN01");
MydoJob job = stubSuccessfulInboundJobSetup();
when(wcsClient.sendPutawayTask(job)).thenReturn(ApiResponse.ok("OK"));
String result = service.addInJob(request);
assertThat(result).contains("WCS入库作业下发成功");
verify(wcsClient).sendPutawayTask(job);
ArgumentCaptor<MydoJob> updateCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService, org.mockito.Mockito.times(2)).updateMydoJob(updateCaptor.capture());
List<MydoJob> updates = updateCaptor.getAllValues();
assertThat(updates.get(0).getStatus()).isEqualTo("1");
assertThat(updates.get(0).getExecutionPhase()).isEqualTo("0");
assertThat(updates.get(1).getJobId()).isEqualTo("RK001");
assertThat(updates.get(1).getStatus()).isEqualTo("3");
assertThat(updates.get(1).getExecutionPhase()).isEqualTo("1");
assertThat(updates.get(1).getStartTime()).isNotBlank();
}
@Test
void addInJobDoesNotMarkDispatchedWhenWcsRejectsIt() {
AddInJobRequest request = new AddInJobRequest();
request.setTrayCode("A001");
request.setWarehouseId("WH01");
request.setStationPointId("IN01");
MydoJob job = stubSuccessfulInboundJobSetup();
when(wcsClient.sendPutawayTask(job)).thenReturn(ApiResponse.fail("WCS_NG", "下发失败"));
assertThatThrownBy(() -> service.addInJob(request))
.isInstanceOf(RuntimeException.class)
.hasMessage("WCS入库作业下发失败:下发失败");
verify(wcsClient).sendPutawayTask(job);
ArgumentCaptor<MydoJob> updateCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).updateMydoJob(updateCaptor.capture());
assertThat(updateCaptor.getValue().getStatus()).isEqualTo("1");
verify(mydoJobService, never()).updateMydoJob(
org.mockito.ArgumentMatchers.argThat(update -> "3".equals(update.getStatus())));
}
@Test
void addEmptyTrayInboundJobCreatesEmptyContainerDetailAndDispatchesToWcs() {
AddInJobRequest request = new AddInJobRequest();
request.setTrayCode("A001");
request.setWarehouseId("WH01");
request.setStationPointId("IN01");
stubInboundStationAndRoadway();
when(jobIdService.getId("RK")).thenReturn("RK_EMPTY_1");
when(taskNoService.getTaskNo("6")).thenReturn("1008");
BasWarehouse warehouse = new BasWarehouse();
warehouse.setWarehouseName("一号仓");
when(basWarehouseService.selectBasWarehouseByWarehouseId("WH01")).thenReturn(warehouse);
when(mydoJobService.insertMydoJob(any(MydoJob.class))).thenReturn(1);
when(mydoJobDetailService.insertMydoJobDetail(any(MydoJobDetail.class))).thenReturn(1);
when(mydoJobService.updateMydoJob(any(MydoJob.class))).thenReturn(1);
when(wcsClient.sendPutawayTask(any(MydoJob.class))).thenReturn(ApiResponse.ok("OK"));
String result = service.addEmptyTrayInboundJob(request);
assertThat(result).contains("空托入库作业生成成功").contains("WCS入库作业下发成功");
verify(pdaMapper, never()).addInJob(any(HashMap.class));
ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).insertMydoJob(jobCaptor.capture());
MydoJob insertedJob = jobCaptor.getValue();
assertThat(insertedJob.getJobId()).isEqualTo("RK_EMPTY_1");
assertThat(insertedJob.getTaskNo()).isEqualTo("1008");
assertThat(insertedJob.getInOrOut()).isEqualTo(3L);
assertThat(insertedJob.getStatus()).isEqualTo("1");
assertThat(insertedJob.getExecutionPhase()).isEqualTo("0");
assertThat(insertedJob.getTrayCode()).isEqualTo("A001");
assertThat(insertedJob.getWarehouseId()).isEqualTo("WH01");
assertThat(insertedJob.getWarehouseName()).isEqualTo("一号仓");
assertThat(insertedJob.getEntranceId()).isEqualTo("IN01");
assertThat(insertedJob.getRoadwayId()).isEqualTo("R01");
ArgumentCaptor<MydoJobDetail> detailCaptor = ArgumentCaptor.forClass(MydoJobDetail.class);
verify(mydoJobDetailService).insertMydoJobDetail(detailCaptor.capture());
MydoJobDetail detail = detailCaptor.getValue();
assertThat(detail.getJobDetailId()).isEqualTo("RK_EMPTY_1-1");
assertThat(detail.getJobId()).isEqualTo("RK_EMPTY_1");
assertThat(detail.getProductId()).isEqualTo("KTP0001");
assertThat(detail.getProductName()).isEqualTo("空容器");
assertThat(detail.getInQuantity()).isEqualByComparingTo("1");
verify(wcsClient).sendPutawayTask(insertedJob);
ArgumentCaptor<MydoJob> updateCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).updateMydoJob(updateCaptor.capture());
assertThat(updateCaptor.getValue().getStatus()).isEqualTo("3");
assertThat(updateCaptor.getValue().getExecutionPhase()).isEqualTo("1");
}
@Test
void emptyTrayOutboundCountRequiresWarehouseAndReturnsTrayTypeCounts() {
HashMap<String, Object> typeA = new HashMap<>();
typeA.put("formatName", "A型托盘");
typeA.put("barcodePrefix", "A-");
typeA.put("quantity", 3);
HashMap<String, Object> typeB = new HashMap<>();
typeB.put("formatName", "B型托盘");
typeB.put("barcodePrefix", "B-");
typeB.put("quantity", 2);
when(pdaMapper.selectAvailableEmptyTrayTypeCounts("WH01")).thenReturn(List.of(typeA, typeB));
assertThat(service.emptyTrayOutboundCount("WH01"))
.containsExactly(typeA, typeB);
assertThatThrownBy(() -> service.emptyTrayOutboundCount(""))
.isInstanceOf(RuntimeException.class)
.hasMessage("仓库编码不能为空");
}
@Test
void trayStockRequiresTrayCodeAndReturnsMaterialsForTrimmedTrayCode() {
HashMap<String, Object> material = new HashMap<>();
material.put("stockId", "KC001");
material.put("stockDetailId", "SD001");
material.put("trayCode", "A001");
material.put("materialCode", "MAT001");
material.put("materialName", "物料1");
material.put("quantity", "6");
when(pdaMapper.selectTrayStockMaterials("A001")).thenReturn(List.of(material));
List<HashMap<String, Object>> result = service.trayStock(" A001 ");
assertThat(result).containsExactly(material);
verify(pdaMapper).selectTrayStockMaterials("A001");
assertThatThrownBy(() -> service.trayStock(" "))
.isInstanceOf(RuntimeException.class)
.hasMessage("托盘码不能为空");
}
@Test
void addEmptyTrayOutboundJobCreatesOneJobPerRequestedTrayAndPushesWcs() {
EmptyTrayOutboundRequest request = new EmptyTrayOutboundRequest();
request.setWarehouseId("WH01");
request.setBarcodePrefix("A-");
request.setQuantity(2);
BasWarehouse warehouse = new BasWarehouse();
warehouse.setWarehouseName("一号仓");
when(basWarehouseService.selectBasWarehouseByWarehouseId("WH01")).thenReturn(warehouse);
when(pdaMapper.countAvailableEmptyTrays(any(HashMap.class))).thenReturn(2);
BasStationPoint exitPoint = new BasStationPoint();
exitPoint.setPointId("OUT01");
exitPoint.setPointType("2");
exitPoint.setStatus("0");
exitPoint.setPointStatus("0");
when(basStationPointService.selectBasStationPointList(any(BasStationPoint.class))).thenReturn(List.of(exitPoint));
when(pdaMapper.selectAvailableEmptyTrayStocks(any(HashMap.class))).thenReturn(List.of(
emptyTrayStock("SD001", "KC001", "A001", "CS001", "货位1"),
emptyTrayStock("SD002", "KC002", "A002", "CS002", "货位2")
));
when(jobIdService.getId("CK")).thenReturn("CK_EMPTY_1", "CK_EMPTY_2");
when(taskNoService.getTaskNo("20")).thenReturn("50001", "50002");
when(mydoJobService.insertMydoJob(any(MydoJob.class))).thenReturn(1);
when(mydoJobDetailService.insertMydoJobDetail(any(MydoJobDetail.class))).thenReturn(1);
when(pdaMapper.reserveEmptyTrayStockDetail(any(HashMap.class))).thenReturn(1);
when(mydoJobService.pushOutJobToWcs(anyString())).thenReturn(AjaxResult.success("WCS出库作业下发成功:OK"));
String result = service.addEmptyTrayOutboundJob(request);
assertThat(result).contains("空托出库作业生成成功").contains("CK_EMPTY_1").contains("CK_EMPTY_2");
ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService, times(2)).insertMydoJob(jobCaptor.capture());
assertThat(jobCaptor.getAllValues()).extracting(MydoJob::getInOrOut).containsExactly(22L, 22L);
assertThat(jobCaptor.getAllValues()).extracting(MydoJob::getStatus).containsExactly("1", "1");
assertThat(jobCaptor.getAllValues()).extracting(MydoJob::getExecutionPhase).containsExactly("0", "0");
assertThat(jobCaptor.getAllValues()).extracting(MydoJob::getExitId).containsExactly("OUT01", "OUT01");
ArgumentCaptor<MydoJobDetail> detailCaptor = ArgumentCaptor.forClass(MydoJobDetail.class);
verify(mydoJobDetailService, times(2)).insertMydoJobDetail(detailCaptor.capture());
assertThat(detailCaptor.getAllValues()).extracting(MydoJobDetail::getProductId).containsExactly("KTP0001", "KTP0001");
assertThat(detailCaptor.getAllValues()).extracting(MydoJobDetail::getOutQuantity)
.containsExactly(new BigDecimal("1"), new BigDecimal("1"));
assertThat(detailCaptor.getAllValues()).extracting(MydoJobDetail::getStockinDetailId).containsExactly("SD001", "SD002");
verify(mydoJobService).pushOutJobToWcs("CK_EMPTY_1");
verify(mydoJobService).pushOutJobToWcs("CK_EMPTY_2");
ArgumentCaptor<HashMap> stockQueryCaptor = ArgumentCaptor.forClass(HashMap.class);
verify(pdaMapper).selectAvailableEmptyTrayStocks(stockQueryCaptor.capture());
assertThat(stockQueryCaptor.getValue())
.containsEntry("warehouseId", "WH01")
.containsEntry("barcodePrefix", "A-")
.containsEntry("limit", 2);
}
@Test
void confirmOutboundCreatesReturnInboundJobWithOutboundExitAsEntranceWhenTrayHasResidualMaterial() {
HashMap<String, Object> outboundDetail = new HashMap<>();
outboundDetail.put("jobId", "CK001");
outboundDetail.put("taskNo", "TASK001");
outboundDetail.put("trayCode", "A001");
outboundDetail.put("quantity", "5");
when(pdaMapper.selectOutboundDetail(any(HashMap.class))).thenReturn(outboundDetail);
when(pdaMapper.selectOutboundTrayMaterials(any(HashMap.class))).thenReturn(Collections.emptyList());
MydoJob outboundJob = new MydoJob();
outboundJob.setJobId("CK001");
outboundJob.setTrayCode("A001");
outboundJob.setWarehouseId("WH01");
outboundJob.setWarehouseName("一号仓");
outboundJob.setCargoSpaceId("CS01");
outboundJob.setCargoSpaceName("一层一列");
outboundJob.setExitId("EXIT01");
when(mydoJobService.selectMydoJobByJobId("CK001")).thenReturn(outboundJob);
when(mydoJobService.completeJob("CK001", "2")).thenReturn(AjaxResult.success());
HashMap<String, Object> residualMaterial = new HashMap<>();
residualMaterial.put("stockId", "KC001");
residualMaterial.put("stockDetailId", "SD001");
residualMaterial.put("productId", "MAT001");
residualMaterial.put("productName", "物料1");
residualMaterial.put("lotNo", "L001");
residualMaterial.put("quantity", "3");
when(pdaMapper.selectResidualTrayMaterials(any(HashMap.class))).thenReturn(List.of(residualMaterial));
when(jobIdService.getId("HL")).thenReturn("HL001");
when(taskNoService.getTaskNo("5")).thenReturn("80001");
BasStationPoint exitPoint = new BasStationPoint();
exitPoint.setRoadwayId("R02");
when(basStationPointService.selectBasStationPointByPointId("EXIT01")).thenReturn(exitPoint);
when(wcsClient.sendPutawayTask(any(MydoJob.class))).thenReturn(ApiResponse.ok("OK"));
when(mydoJobService.updateMydoJob(any(MydoJob.class))).thenReturn(1);
HashMap<String, Object> data = new HashMap<>();
data.put("taskNo", "TASK001");
data.put("barcode", "A001");
data.put("quantity", "5");
service.confirmOutbound(data);
ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).insertMydoJob(jobCaptor.capture());
MydoJob returnJob = jobCaptor.getValue();
assertThat(returnJob.getJobId()).isEqualTo("HL001");
assertThat(returnJob.getTaskNo()).isEqualTo("80001");
assertThat(returnJob.getInOrOut()).isEqualTo(5L);
assertThat(returnJob.getEntranceId()).isEqualTo("EXIT01");
assertThat(returnJob.getRoadwayId()).isEqualTo("R02");
assertThat(returnJob.getCargoSpaceId()).isNull();
assertThat(returnJob.getCargoSpaceName()).isNull();
verify(wcsClient).sendPutawayTask(returnJob);
ArgumentCaptor<MydoJobDetail> detailCaptor = ArgumentCaptor.forClass(MydoJobDetail.class);
verify(mydoJobDetailService).insertMydoJobDetail(detailCaptor.capture());
assertThat(detailCaptor.getValue().getJobId()).isEqualTo("HL001");
assertThat(detailCaptor.getValue().getStockQuantity()).isEqualByComparingTo("3");
ArgumentCaptor<MydoJob> updateCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).updateMydoJob(updateCaptor.capture());
assertThat(updateCaptor.getValue().getJobId()).isEqualTo("HL001");
assertThat(updateCaptor.getValue().getStatus()).isEqualTo("3");
assertThat(updateCaptor.getValue().getExecutionPhase()).isEqualTo("1");
assertThat(updateCaptor.getValue().getStartTime()).isNotBlank();
}
@Test
void confirmOutboundUpdatesEachJobDetailWithActualMaterialQuantityBeforeCompletion() {
HashMap<String, Object> outboundDetail = new HashMap<>();
outboundDetail.put("jobId", "CK002");
outboundDetail.put("taskNo", "TASK002");
outboundDetail.put("trayCode", "A002");
outboundDetail.put("quantity", "7");
when(pdaMapper.selectOutboundDetail(any(HashMap.class))).thenReturn(outboundDetail);
HashMap<String, Object> materialA = new HashMap<>();
materialA.put("jobDetailId", "CK002-1");
materialA.put("stockDetailId", "SD001");
materialA.put("stockQuantity", "6");
materialA.put("quantity", "6");
HashMap<String, Object> materialB = new HashMap<>();
materialB.put("jobDetailId", "CK002-2");
materialB.put("stockDetailId", "SD002");
materialB.put("stockQuantity", "2");
materialB.put("quantity", "1");
when(pdaMapper.selectOutboundTrayMaterials(any(HashMap.class))).thenReturn(List.of(materialA, materialB));
MydoJob outboundJob = new MydoJob();
outboundJob.setJobId("CK002");
outboundJob.setTrayCode("A002");
outboundJob.setWarehouseId("WH01");
when(mydoJobService.selectMydoJobByJobId("CK002")).thenReturn(outboundJob);
when(mydoJobService.completeJob("CK002", "2")).thenReturn(AjaxResult.success());
when(pdaMapper.selectResidualTrayMaterials(any(HashMap.class))).thenReturn(Collections.emptyList());
when(mydoJobDetailService.updateMydoJobDetail(any(MydoJobDetail.class))).thenReturn(1);
HashMap<String, Object> requestMaterialA = new HashMap<>();
requestMaterialA.put("jobDetailId", "CK002-1");
requestMaterialA.put("stockDetailId", "SD001");
requestMaterialA.put("quantity", "5");
HashMap<String, Object> requestMaterialB = new HashMap<>();
requestMaterialB.put("jobDetailId", "CK002-2");
requestMaterialB.put("stockDetailId", "SD002");
requestMaterialB.put("quantity", "2");
HashMap<String, Object> data = new HashMap<>();
data.put("taskNo", "TASK002");
data.put("barcode", "A002");
data.put("quantity", "7");
data.put("materials", List.of(requestMaterialA, requestMaterialB));
service.confirmOutbound(data);
ArgumentCaptor<MydoJobDetail> detailCaptor = ArgumentCaptor.forClass(MydoJobDetail.class);
verify(mydoJobDetailService, times(2)).updateMydoJobDetail(detailCaptor.capture());
assertThat(detailCaptor.getAllValues()).extracting(MydoJobDetail::getJobDetailId)
.containsExactly("CK002-1", "CK002-2");
assertThat(detailCaptor.getAllValues()).extracting(MydoJobDetail::getOutQuantity)
.containsExactly(new BigDecimal("5"), new BigDecimal("2"));
verify(mydoJobService).completeJob("CK002", "2");
}
private void stubInboundStationAndRoadway() {
BasStationPoint point = new BasStationPoint();
point.setPointId("IN01");
point.setWarehouseId("WH01");
point.setPointType("1");
point.setStatus("0");
point.setPointStatus("0");
point.setTrayType("A");
point.setRoadwayId("R01");
when(basStationPointService.selectBasStationPointByPointId("IN01")).thenReturn(point);
when(basStationPointService.occupyBasStationPoint("IN01")).thenReturn(1);
HashMap<String, Object> trayRule = new HashMap<>();
trayRule.put("allowed_cargo_type", "A");
when(pdaMapper.selectMatchedTrayRule(any(HashMap.class))).thenReturn(trayRule);
HashMap<String, Object> roadway = new HashMap<>();
roadway.put("roadway_id", "R01");
when(pdaMapper.selectAvailableInboundRoadway(any(HashMap.class))).thenReturn(roadway);
}
private HashMap<String, Object> emptyTrayStock(String detailId, String stockId, String trayCode, String cargoSpaceId, String cargoSpaceName) {
HashMap<String, Object> stock = new HashMap<>();
stock.put("stockDetailId", detailId);
stock.put("stockId", stockId);
stock.put("trayCode", trayCode);
stock.put("warehouseId", "WH01");
stock.put("warehouseName", "一号仓");
stock.put("cargoSpaceId", cargoSpaceId);
stock.put("cargoSpaceName", cargoSpaceName);
stock.put("roadwayId", "R01");
stock.put("availableQuantity", "1");
return stock;
}
private MydoJob stubSuccessfulInboundJobSetup() {
BasStationPoint point = new BasStationPoint();
point.setPointId("IN01");
point.setWarehouseId("WH01");
point.setPointType("1");
point.setStatus("0");
point.setPointStatus("0");
point.setTrayType("A");
point.setRoadwayId("R01");
when(basStationPointService.selectBasStationPointByPointId("IN01")).thenReturn(point);
when(basStationPointService.occupyBasStationPoint("IN01")).thenReturn(1);
HashMap<String, Object> trayRule = new HashMap<>();
trayRule.put("allowed_cargo_type", "A");
when(pdaMapper.selectMatchedTrayRule(any(HashMap.class))).thenReturn(trayRule);
HashMap<String, Object> roadway = new HashMap<>();
roadway.put("roadway_id", "R01");
when(pdaMapper.selectAvailableInboundRoadway(any(HashMap.class))).thenReturn(roadway);
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
HashMap<String, Object> map = invocation.getArgument(0);
map.put("v_result", "1");
map.put("v_resultmsg", "生成作业成功");
return null;
}).when(pdaMapper).addInJob(any(HashMap.class));
MydoJob job = new MydoJob();
job.setJobId("RK001");
job.setTaskNo("TASK001");
job.setTrayCode("A001");
job.setWarehouseId("WH01");
when(mydoJobService.selectLatestInMydoJobByTrayCodeAndWarehouseId("A001", "WH01")).thenReturn(job);
BasWarehouse warehouse = new BasWarehouse();
warehouse.setWarehouseName("一号仓");
when(basWarehouseService.selectBasWarehouseByWarehouseId("WH01")).thenReturn(warehouse);
when(mydoJobService.updateMydoJob(any(MydoJob.class))).thenReturn(1);
when(mydoJobDetailService.selectMydoJobDetailList(any())).thenReturn(Collections.emptyList());
return job;
}
}