rsf-admin/src/page/task/TaskList.jsx
@@ -372,6 +372,21 @@ ) } /** 拣料/盘点出库:仅 RCS 执行中(<198)可取消;199、198 不可取消 */ const canCancelPickOrCheckOut = (record) => { if (record?.taskType != 103 && record?.taskType != 107) return false; const s = record.taskStatus; return s < 198; }; /** 普通入出库、空板、移库等:创建态可取消;199 不可取消 */ const canCancelLegacy = (record) => { const t = record?.taskType; const s = record?.taskStatus; if (t != 1 && t != 101 && t != 10 && t != 11) return false; return s == 1 || s == 101; }; /** * 取消按钮 * @returns @@ -393,8 +408,9 @@ notify(msg); } } const showCancel = canCancelPickOrCheckOut(record) || canCancelLegacy(record); return ( (record.taskStatus == 1 || record.taskStatus == 101 || record.taskStatus == 199) && (record.taskType == 1 || record.taskType == 101 || record.taskType == 10 || record.taskType == 107 || record.taskType == 103 || record.taskType == 11) ? showCancel ? <ConfirmButton onConfirm={clickCancel} startIcon={<CancelIcon />} rsf-server/src/main/java/com/vincent/rsf/server/api/entity/constant/RcsConstant.java
@@ -19,4 +19,7 @@ //取消RCS任务 public static String cancelTask = "/api/open/task/cancel"; /** 任务状态反向通知 RCS(POST body: taskNo, status) */ public static String TASK_STATUS_NOTICE = "/api/open/bus/notice"; } rsf-server/src/main/java/com/vincent/rsf/server/api/service/RcsBusTaskNoticeService.java
New file @@ -0,0 +1,14 @@ package com.vincent.rsf.server.api.service; /** * 管理后台对工作档点「完成」时反向通知 RCS:POST /api/open/bus/notice。 * 「取消」仅通过 RCS 取消接口(/api/open/task/cancel);PDA、RCS 回调、定时任务等不调此处。 */ public interface RcsBusTaskNoticeService { /** * @param taskNo 工作档任务编码 * @param status {@link com.vincent.rsf.server.manager.enums.TaskStsType} 的 id(完成 98/198/200);取消走 RCS 取消接口,不用本接口 */ void notifyTaskStatus(String taskNo, Integer status); } rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOutStockServiceImpl.java
@@ -191,7 +191,7 @@ } if (first.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)) { for (Task task : tasks) { taskService.completeFullOutStock(task.getId(), loginUserId); taskService.completeFullOutStock(task.getId(), loginUserId, false); } return R.ok("确认成功,全版出库已完成"); } rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/RcsBusTaskNoticeServiceImpl.java
New file @@ -0,0 +1,75 @@ package com.vincent.rsf.server.api.service.impl; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.vincent.rsf.server.api.config.RemotesInfoProperties; import com.vincent.rsf.server.api.entity.CommonResponse; import com.vincent.rsf.server.api.entity.constant.RcsConstant; import com.vincent.rsf.server.api.service.RcsBusTaskNoticeService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; /** 管理后台工作档完成/取消时通知 RCS */ @Slf4j @Service public class RcsBusTaskNoticeServiceImpl implements RcsBusTaskNoticeService { @Autowired(required = false) private RemotesInfoProperties.RcsApi rcsApi; @Autowired(required = false) private RestTemplate restTemplate; @Override public void notifyTaskStatus(String taskNo, Integer status) { if (StringUtils.isBlank(taskNo) || status == null) { return; } if (rcsApi == null || StringUtils.isBlank(rcsApi.getHost()) || StringUtils.isBlank(rcsApi.getPort())) { log.debug("跳过 RCS 任务状态通知:未配置 platform.rcs"); return; } if (restTemplate == null) { log.warn("跳过 RCS 任务状态通知:RestTemplate 未注入"); return; } String url = rcsApi.getHost() + ":" + rcsApi.getPort() + RcsConstant.TASK_STATUS_NOTICE; Map<String, String> body = new HashMap<>(2); body.put("taskNo", taskNo); body.put("status", String.valueOf(status)); try { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json"); headers.add("api-version", "v2.0"); HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers); log.info("RCS 任务状态通知 POST {} body={}", url, JSONObject.toJSONString(body)); ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); if (exchange.getBody() == null) { log.warn("RCS 任务状态通知响应体为空 taskNo={}", taskNo); return; } ObjectMapper om = new ObjectMapper(); om.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); CommonResponse res = om.readValue(exchange.getBody(), CommonResponse.class); if (res.getCode() != null && res.getCode() == 200) { log.info("RCS 任务状态通知成功 taskNo={} status={}", taskNo, status); } else { log.warn("RCS 任务状态通知非成功 taskNo={} status={} code={} msg={}", taskNo, status, res.getCode(), res.getMsg()); } } catch (Exception e) { log.warn("RCS 任务状态通知异常 taskNo={} status={}:{}", taskNo, status, e.getMessage()); } } } rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java
@@ -118,7 +118,34 @@ private WarehouseAreasItemService warehouseAreasItemService; @Autowired private ConfigService configService; @Autowired private TaskItemService taskItemService; @Autowired private TaskService taskService; /** * 云仓改单/取消前:任务明细已关联该单据且主任务未逻辑删除则不允许 */ private void assertWkOrderNoLinkedTask(Long wkOrderId) { if (wkOrderId == null) { return; } List<TaskItem> links = taskItemService.list(new LambdaQueryWrapper<TaskItem>() .select(TaskItem::getTaskId) .and(w -> w.eq(TaskItem::getOrderId, wkOrderId) .or(o -> o.eq(TaskItem::getSourceId, wkOrderId) .eq(TaskItem::getOrderType, OrderType.ORDER_OUT.type)))); if (links.isEmpty()) { return; } Set<Long> taskIds = links.stream().map(TaskItem::getTaskId).filter(Objects::nonNull).collect(Collectors.toSet()); if (taskIds.isEmpty()) { return; } if (taskService.count(new LambdaQueryWrapper<Task>().in(Task::getId, taskIds)) > 0) { throw new CoolException("该单据已生成任务,不可修改、取消或删除!!"); } } /** * @author Ryan @@ -465,6 +492,7 @@ WkOrder order = asnOrderService.getOne(new LambdaQueryWrapper<WkOrder>() .eq(WkOrder::getPoCode, syncOrder.getOrderInternalCode())); if (!Objects.isNull(order)) { assertWkOrderNoLinkedTask(order.getId()); // 仅未执行状态可被 order/add 修改(入库未执行、出库任务初始) List<Short> editableStatus = Arrays.asList(AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val ,AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val @@ -943,6 +971,7 @@ if (Objects.isNull(wkOrder)) { throw new CoolException("请确认单据:" + order.getOrderNo() + "是否已经执行或是否同步!!"); } assertWkOrderNoLinkedTask(wkOrder.getId()); order.getOrderItems().forEach(orderItem -> { WkOrderItem wkOrderItem = asnOrderItemService.getOne(new LambdaUpdateWrapper<WkOrderItem>() .eq(WkOrderItem::getMatnrCode, orderItem.getMatnr()) @@ -1006,6 +1035,7 @@ throw new CoolException("单据不存在,无法取消!!请提供单据内码(orderInternalCode)或单号(orderNo)。"); } final WkOrder finalWkOrder = wkOrder; assertWkOrderNoLinkedTask(finalWkOrder.getId()); // 已组托不可取消 long pakinCount = waitPakinItemService.count(new LambdaQueryWrapper<WaitPakinItem>() .eq(WaitPakinItem::getAsnId, finalWkOrder.getId())); rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/TaskController.java
@@ -138,7 +138,7 @@ if (Cools.isEmpty(id)) { throw new CoolException("参数不能为空!!"); } return R.ok("全版出库完结成功").add(taskService.completeFullOutStock(id, getLoginUserId())); return R.ok("全版出库完结成功").add(taskService.completeFullOutStock(id, getLoginUserId(), true)); } /** rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/TaskStsType.java
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/MaterialAutoSchedules.java
@@ -307,7 +307,7 @@ continue; } try { taskService.completeFullOutStock(task.getId(), SYSTEM_USER_ID); taskService.completeFullOutStock(task.getId(), SYSTEM_USER_ID, false); log.info("[自动拣货完成] 任务: {}, 料箱: {}, 物料: {} 已自动确认出库并更新库存", task.getTaskCode(), task.getBarcode(), matnrCode); } catch (Exception e) { log.warn("[自动拣货完成] 任务: {} 处理失败: {}", task.getTaskCode(), e.getMessage()); rsf-server/src/main/java/com/vincent/rsf/server/manager/service/TaskService.java
@@ -28,7 +28,10 @@ Task operateComplete(Long id, Long loginUserId); Task completeFullOutStock(Long id, Long loginUserId); /** * @param notifyRcsFromAdmin true:管理后台「全版出库完结」接口调用时通知 RCS;false:定时/PDA 等同源不通知 */ Task completeFullOutStock(Long id, Long loginUserId, boolean notifyRcsFromAdmin); void moveToDeep(Long loginUserId, String curLoc) throws Exception; rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -13,6 +13,7 @@ import com.vincent.rsf.server.api.config.RemotesInfoProperties; import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; import com.vincent.rsf.server.api.controller.erp.params.TaskInParam; import com.vincent.rsf.server.api.service.RcsBusTaskNoticeService; import com.vincent.rsf.server.api.entity.CommonResponse; import com.vincent.rsf.server.api.entity.constant.RcsConstant; import com.vincent.rsf.server.api.entity.dto.InTaskMsgDto; @@ -131,6 +132,8 @@ private CloudWmsNotifyLogService cloudWmsNotifyLogService; @Autowired private WarehouseService warehouseService; @Autowired private RcsBusTaskNoticeService rcsBusTaskNoticeService; @Override @Transactional(rollbackFor = Exception.class) @@ -509,7 +512,7 @@ } /** * 手动完成任务 * 手动完成任务:入库类置 98、出库类置 198,库位/单据扣减与上报由对应定时任务执行 * * @param id * @param loginUserId @@ -550,12 +553,21 @@ modiftyTaskSort(task, loginUserId); // 如果任务状态已经是AWAIT (196),再次点击完结时,直接完成 if (task.getTaskStatus().equals(TaskStsType.AWAIT.id)) { // AWAIT状态的任务再次完结,直接设置为出库完成 // 入库:->98,出库:-> 198,由 complateOutStock 定时任务 更新库存 if (task.getTaskType() < 100) { task.setTaskStatus(TaskStsType.COMPLETE_IN.id); if (StringUtils.isNotBlank(task.getOrgSite())) { BasStation station = basStationService.getOne(new LambdaQueryWrapper<BasStation>() .eq(BasStation::getStationName, task.getOrgSite())); if (Objects.nonNull(station) && station.getType().equals(StationTypeEnum.STATION_TYPE_NORMAL.type)) { station.setUseStatus(LocStsType.LOC_STS_TYPE_O.type); if (!basStationService.updateById(station)) { throw new CoolException("入库站点状态修改失败!!"); } } } } else { task.setTaskStatus(TaskStsType.COMPLETE_OUT.id); // 更新出库站点状态(与RCS通知完结保持一致) if (task.getTaskType() >= TaskType.TASK_TYPE_OUT.type && StringUtils.isNotBlank(task.getTargSite())) { BasStation station = basStationService.getOne(new LambdaQueryWrapper<BasStation>() .eq(BasStation::getStationName, task.getTargSite())); @@ -566,28 +578,17 @@ } } } } else { // 其他情况按原有逻辑处理 // 入库任务(taskType < 100):设置为入库完成 // 出库任务(taskType >= 100):设置为等待确认 Integer newStatus = task.getTaskType() < 100 ? TaskStsType.COMPLETE_IN.id : TaskStsType.AWAIT.id; task.setTaskStatus(newStatus); // 如果是入库任务完成,更新入库站点状态(与RCS通知完结保持一致) if (newStatus.equals(TaskStsType.COMPLETE_IN.id) && StringUtils.isNotBlank(task.getOrgSite())) { BasStation station = basStationService.getOne(new LambdaQueryWrapper<BasStation>() .eq(BasStation::getStationName, task.getOrgSite())); if (Objects.nonNull(station) && station.getType().equals(StationTypeEnum.STATION_TYPE_NORMAL.type)) { station.setUseStatus(LocStsType.LOC_STS_TYPE_O.type); if (!basStationService.updateById(station)) { throw new CoolException("入库站点状态修改失败!!"); } } } } // 原:196 时再点一次才置 198;出库首次点击曾置 196(AWAIT) // if (task.getTaskStatus().equals(TaskStsType.AWAIT.id)) { task.setTaskStatus(TaskStsType.COMPLETE_OUT.id); ... } // else { Integer newStatus = task.getTaskType() < 100 ? COMPLETE_IN.id : AWAIT.id; ... } if (!this.updateById(task)) { throw new CoolException("完成任务失败"); } // 管理后台「完成任务」通知 RCS if (StringUtils.isNotBlank(task.getTaskCode())) { rcsBusTaskNoticeService.notifyTaskStatus(task.getTaskCode(), task.getTaskStatus()); } return task; } @@ -597,11 +598,12 @@ * * @param id 任务ID * @param loginUserId 登录用户ID * @param notifyRcsFromAdmin 管理后台全版出库完结接口为 true 时通知 RCS;定时/PDA 等为 false * @return 任务对象 */ @Override @Transactional(rollbackFor = Exception.class) public Task completeFullOutStock(Long id, Long loginUserId) { public Task completeFullOutStock(Long id, Long loginUserId, boolean notifyRcsFromAdmin) { // 查询任务 Task task = taskService.getOne(new LambdaQueryWrapper<Task>() .eq(Task::getId, id)); @@ -667,6 +669,9 @@ if (!this.updateById(task)) { throw new CoolException("任务状态更新失败!!"); } if (notifyRcsFromAdmin && StringUtils.isNotBlank(task.getTaskCode())) { rcsBusTaskNoticeService.notifyTaskStatus(task.getTaskCode(), TaskStsType.UPDATED_OUT.id); } return task; @@ -984,25 +989,22 @@ } } // 如果有任务已下发到RCS,先调用RCS取消接口 // 已下发 RCS 的工作档:必须先调 RCS 取消接口成功,否则不允许取消工作档 boolean rcsCancelSuccess = false; if (!rcsTaskCodes.isEmpty()) { // 检查 RCS API 配置是否有效 if (rcsApi == null || StringUtils.isBlank(rcsApi.getHost()) || StringUtils.isBlank(rcsApi.getPort())) { log.error("========== RCS任务取消失败 =========="); log.error("RCS API 配置无效!host: {}, port: {}", rcsApi != null ? rcsApi.getHost() : "null", rcsApi != null ? rcsApi.getPort() : "null"); // 即使配置无效,也继续执行任务删除操作 } else { throw new CoolException("任务已下发RCS,但未配置RCS地址,无法取消!!"); } if (restTemplate == null) { throw new CoolException("任务已下发RCS,但无法调用RCS取消接口,无法取消!!"); } try { log.info("========== 开始取消RCS任务 =========="); log.info("需要取消的RCS任务编号:{}", rcsTaskCodes); String rcsUrl = rcsApi.getHost() + ":" + rcsApi.getPort() + RcsConstant.cancelTask; log.info("RCS取消任务请求地址:{}", rcsUrl); // 如果没有批次编号,使用第一个任务编号作为批次编号 if (StringUtils.isBlank(batchNo) && !rcsTaskCodes.isEmpty()) { if (StringUtils.isBlank(batchNo)) { batchNo = rcsTaskCodes.get(0); } @@ -1045,6 +1047,8 @@ log.error("RCS取消任务失败:{}", result.getMsg()); throw new CoolException("RCS取消任务失败:" + result.getMsg()); } } catch (CoolException e) { throw e; } catch (JsonProcessingException e) { log.error("RCS取消任务响应解析失败:{}", e.getMessage(), e); throw new CoolException("RCS取消任务响应解析失败:" + e.getMessage()); @@ -1053,14 +1057,18 @@ throw new CoolException("RCS取消任务异常:" + e.getMessage()); } } } // 查询符合取消条件的任务(状态为1、101、199) List<Integer> allowedStatuses = Arrays.asList(TaskStsType.GENERATE_IN.id, TaskStsType.GENERATE_OUT.id, TaskStsType.WAVE_SEED.id); // 可取消状态:原 1/101(不含 199);拣料/盘点出库 RCS 执行中(<198);拣料/盘点再入库(53/57)不支持取消 List<Task> tasks = this.list(new LambdaQueryWrapper<Task>() .in(Task::getTaskType, list) .in(Task::getId, (Object[]) ids) .in(Task::getTaskStatus, allowedStatuses)); .and(w -> w .in(Task::getTaskStatus, Arrays.asList( TaskStsType.GENERATE_IN.id, TaskStsType.GENERATE_OUT.id)) .or(w2 -> w2 .in(Task::getTaskType, TaskType.TASK_TYPE_PICK_AGAIN_OUT.type, TaskType.TASK_TYPE_CHECK_OUT.type) .lt(Task::getTaskStatus, TaskStsType.COMPLETE_OUT.id)))); // 如果符合取消条件的任务为空,但RCS取消成功,允许继续(可能是任务状态已变更) if (tasks.isEmpty() && !rcsCancelSuccess) {