rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/MonitorController.java
@@ -1,8 +1,10 @@ package com.vincent.rsf.openApi.controller; import com.fasterxml.jackson.databind.JsonNode; import com.vincent.rsf.openApi.entity.dto.CommonResponse; import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient; import com.vincent.rsf.openApi.service.MonitorService; import com.vincent.rsf.openApi.service.WcsStationStatusService; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; @@ -11,6 +13,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.LinkedHashMap; import java.util.Map; @@ -27,6 +31,29 @@ @Autowired private WmsServerFeignClient wmsServerFeignClient; @Resource private WcsStationStatusService wcsStationStatusService; /** * 与 zy-monitor sys_config.wcsStationUrl 对应:POST stationIds 拉取;POST data 为 WCS 上报写入 Redis */ @PostMapping("/stationStatus") @ApiOperation("WCS站点状态(电视机:与 wcsStationUrl 约定一致)") public Map<String, Object> stationStatus(@RequestBody(required = false) JsonNode body) { return wcsStationStatusService.handle(body); } /** 占位:zy-monitor {@code wcsDeviceStatusUrl} POST crnNos;data 为 null 时同步逻辑将全部堆垛机置为离线 */ @PostMapping("/deviceStatus") @ApiOperation("WCS堆垛机状态占位(data 为 null)") public Map<String, Object> deviceStatus() { Map<String, Object> m = new LinkedHashMap<>(); m.put("code", 200); m.put("msg", "ok"); m.put("data", null); return m; } @GetMapping("/queryLoc") @ApiOperation("大屏:库位库存统计") public Map<String, Object> queryLoc() { rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/WcsStationStatusService.java
New file @@ -0,0 +1,302 @@ package com.vincent.rsf.openApi.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.vincent.rsf.openApi.tv.TvMonitorRedisKeys; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; /** * WCS 站点状态:与 zy-wcs-gsl {@code OpenController#getStationStatus} 请求字段一致(stationIds); * 查询结果字段与 zy-monitor {@code WcsStationTimer} 解析一致(data 数组含 superTaskNo、autoing、loading 等) */ @Slf4j @Service public class WcsStationStatusService { @Resource @Qualifier("tvMonitorStringRedisTemplate") private StringRedisTemplate tvMonitorStringRedisTemplate; @Resource private ObjectMapper objectMapper; public Map<String, Object> handle(JsonNode body) { if (body == null || body.isNull()) { return okEmptyData(); } // 与 zy-wcs-gsl OpenController#getStationStatus(GetStationStatusParam) 一致:stationIds 为 null 或空则返回全部 if (body.has("stationIds")) { JsonNode sidArr = body.get("stationIds"); if (sidArr.isNull()) { return okQueryAll(); } if (!sidArr.isArray()) { return fail("stationIds 须为数组"); } if (sidArr.size() == 0) { return okQueryAll(); } return okQuery(sidArr); } JsonNode data = body.get("data"); if (data == null && body.isArray()) { data = body; } if (data != null && data.isArray()) { savePush(data); return okMsg("已写入站点状态"); } // 未带 stationIds、data 上报时按全量查询,与 stationIds 空数组一致 return okQueryAll(); } private Map<String, Object> okEmptyData() { Map<String, Object> m = new LinkedHashMap<>(); m.put("code", 200); m.put("data", objectMapper.createArrayNode()); return m; } /** Redis 中全部站点,对应 WCS 侧 stationIds 为空时 stationThread.getStatus() 全量 */ private Map<String, Object> okQueryAll() { ArrayNode out = objectMapper.createArrayNode(); Set<Object> keys = tvMonitorStringRedisTemplate.opsForHash() .keys(TvMonitorRedisKeys.TV_WCS_STATION_STATUS); for (Object k : keys) { if (k == null) { continue; } String raw = (String) tvMonitorStringRedisTemplate.opsForHash() .get(TvMonitorRedisKeys.TV_WCS_STATION_STATUS, k.toString()); if (!StringUtils.hasText(raw)) { continue; } try { int sid = Integer.parseInt(k.toString()); JsonNode stored = objectMapper.readTree(raw); out.add(toMonitorRow(sid, stored)); } catch (Exception e) { log.warn("站点 {} Redis 解析失败: {}", k, e.getMessage()); } } Map<String, Object> m = new LinkedHashMap<>(); m.put("code", 200); m.put("data", out); return m; } private Map<String, Object> okQuery(JsonNode stationIds) { ArrayNode out = objectMapper.createArrayNode(); for (JsonNode idNode : stationIds) { if (idNode == null || !idNode.isNumber()) { continue; } int sid = idNode.asInt(); String raw = (String) tvMonitorStringRedisTemplate.opsForHash() .get(TvMonitorRedisKeys.TV_WCS_STATION_STATUS, String.valueOf(sid)); if (!StringUtils.hasText(raw)) { continue; } try { JsonNode stored = objectMapper.readTree(raw); out.add(toMonitorRow(sid, stored)); } catch (Exception e) { log.warn("站点 {} Redis JSON 解析失败: {}", sid, e.getMessage()); } } Map<String, Object> m = new LinkedHashMap<>(); m.put("code", 200); m.put("data", out); return m; } private ObjectNode toMonitorRow(int stationId, JsonNode stored) { ObjectNode row = objectMapper.createObjectNode(); row.put("stationId", stationId); String taskNo = textFirst(stored, "taskNo", "superTaskNo"); row.put("superTaskNo", taskNo != null ? taskNo : ""); row.put("autoing", truthy(stored, "auto", "autoing")); row.put("loading", truthy(stored, "loading")); row.put("barcode", text(stored, "barcode")); row.put("errorMsg", text(stored, "errorMsg")); row.put("systemWarning", text(stored, "systemWarning")); if (stored.has("wrkDetls") && !stored.get("wrkDetls").isNull()) { row.set("wrkDetls", stored.get("wrkDetls")); } if (stored.has("ioType") && !stored.get("ioType").isNull()) { row.set("ioType", stored.get("ioType")); } if (stored.has("orderNo") && !stored.get("orderNo").isNull()) { row.put("orderNo", text(stored, "orderNo")); } return row; } private void savePush(JsonNode data) { for (JsonNode item : data) { if (item == null || !item.isObject()) { continue; } JsonNode sidNode = item.get("stationId"); if (sidNode == null || !sidNode.isNumber()) { continue; } int sid = sidNode.asInt(); ObjectNode norm = normalizeForStore(item); try { tvMonitorStringRedisTemplate.opsForHash().put( TvMonitorRedisKeys.TV_WCS_STATION_STATUS, String.valueOf(sid), objectMapper.writeValueAsString(norm)); } catch (Exception e) { log.warn("写入站点 {} 失败: {}", sid, e.getMessage()); } } log.info("WCS 站点状态已写入 Redis,条数={}", data.size()); } /** 与 zy-monitor WcsStationDto 字段对齐,统一 taskNo/auto 数值 */ private ObjectNode normalizeForStore(JsonNode item) { ObjectNode n = objectMapper.createObjectNode(); if (item.has("stationId")) { n.put("stationId", item.get("stationId").asInt()); } String taskNo = textFirst(item, "taskNo", "superTaskNo"); if (StringUtils.hasText(taskNo)) { n.put("taskNo", taskNo); } n.put("auto", truthy(item, "auto", "autoing") ? 1 : 0); n.put("loading", truthy(item, "loading") ? 1 : 0); if (item.has("barcode")) { n.put("barcode", text(item, "barcode")); } if (item.has("errorMsg")) { n.put("errorMsg", text(item, "errorMsg")); } if (item.has("systemWarning")) { n.put("systemWarning", text(item, "systemWarning")); } if (item.has("wrkDetls")) { n.set("wrkDetls", item.get("wrkDetls")); } if (item.has("ioType")) { n.set("ioType", item.get("ioType")); } if (item.has("orderNo")) { n.put("orderNo", text(item, "orderNo")); } if (item.has("outboundSeq")) { n.put("outboundSeq", text(item, "outboundSeq")); } return n; } private static String text(JsonNode n, String field) { JsonNode v = n.get(field); if (v == null || v.isNull()) { return ""; } return v.asText(""); } private static String textFirst(JsonNode n, String... fields) { for (String f : fields) { if (n.hasNonNull(f)) { String s = n.get(f).asText(""); if (StringUtils.hasText(s)) { return s; } } } return null; } private static boolean truthy(JsonNode n, String... fields) { for (String f : fields) { if (!n.has(f) || n.get(f).isNull()) { continue; } JsonNode v = n.get(f); if (v.isBoolean()) { return v.booleanValue(); } if (v.isNumber()) { return v.asInt() != 0; } if (v.isTextual()) { return "1".equals(v.asText()) || "true".equalsIgnoreCase(v.asText()); } } return false; } private static Map<String, Object> okMsg(String msg) { Map<String, Object> m = new LinkedHashMap<>(); m.put("code", 200); m.put("message", msg); return m; } private static Map<String, Object> fail(String msg) { Map<String, Object> m = new LinkedHashMap<>(); m.put("code", 400); m.put("message", msg); return m; } /** * RCS 轮询得到任务号后,合并 WMS queryTask(料箱号、ioType、wrkDetls 物料明细)到电视机 Redis */ public void upsertFromRcsPoll(String stationId, String taskNo, Map<String, Object> wmsData) { if (!StringUtils.hasText(stationId)) { return; } ObjectNode n = objectMapper.createObjectNode(); try { n.put("stationId", Integer.parseInt(stationId.trim())); } catch (NumberFormatException e) { n.put("stationId", stationId); } String tn = taskNo != null ? taskNo.trim() : ""; n.put("taskNo", tn); n.put("superTaskNo", tn); n.put("auto", 1); n.put("loading", StringUtils.hasText(tn) && !"0".equals(tn) ? 1 : 0); if (wmsData != null) { if (wmsData.get("barcode") != null) { n.put("barcode", String.valueOf(wmsData.get("barcode"))); } if (wmsData.get("ioType") != null) { n.set("ioType", objectMapper.valueToTree(wmsData.get("ioType"))); } if (wmsData.get("wrkDetls") != null) { n.set("wrkDetls", objectMapper.valueToTree(wmsData.get("wrkDetls"))); } } try { tvMonitorStringRedisTemplate.opsForHash().put( TvMonitorRedisKeys.TV_WCS_STATION_STATUS, stationId, objectMapper.writeValueAsString(n)); } catch (Exception e) { log.warn("tvWcsStationStatus stationId={} 写入失败: {}", stationId, e.getMessage()); } } public void clearStationSnapshot(String stationId) { if (!StringUtils.hasText(stationId)) { return; } tvMonitorStringRedisTemplate.opsForHash().delete(TvMonitorRedisKeys.TV_WCS_STATION_STATUS, stationId); } } rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvMonitorRedisKeys.java
@@ -11,6 +11,9 @@ /** RCS 站点任务号缓存(Hash:field=staNo, value=taskNo);电视机 led 任务号仍依赖 WCS 轮询 stationMap,本键供扩展 */ public static final String TV_RCS_STATION_TASK_NO = "tvRcsStationTaskNo"; /** WCS 站点状态快照(Hash:field=stationId, value=JSON),供 /monitor/stationStatus 轮询与电视机侧 StationUtils 同源数据 */ public static final String TV_WCS_STATION_STATUS = "tvWcsStationStatus"; private TvMonitorRedisKeys() { } } rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollProperties.java
@@ -24,9 +24,15 @@ /** 写入 Redis Hash 的 field,与 RCS 回调 staNo 一致 */ private String taskNoStationId = "1007"; /** GET 全路径,如 http://10.10.10.200:8088/station/getError?stationId=1010 */ /** 全路径,如 http://10.10.10.200:8088/station/getError */ private String errorPollUrl = ""; /** true:POST JSON {@code {"staNo": errorStationId}}(RCS 文档);false:GET 原 URL */ private boolean errorPollUsePost = true; /** 解析异常列表时默认站点号(写入 [staNo] 前缀) */ private String errorStationId = "1010"; /** 取到任务号后是否调 WMS /monitor/queryTask 合并料箱、物料明细到 tvWcsStationStatus */ private boolean enrichTaskFromWms = true; } rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollService.java
@@ -4,17 +4,26 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient; import com.vincent.rsf.openApi.service.RcsTvCallbackService; import com.vincent.rsf.openApi.service.WcsStationStatusService; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.HashMap; import java.util.Map; /** * 调用 RCS 站点接口,结果写入电视机 Redis,由 zy-monitor-admin WebSocket 推送到电视机 * 定时:GET RCS /station/getTaskNo;POST RCS /station/getError;任务号存在时调 WMS queryTask 合并料箱与物料明细进 Redis */ @Slf4j @Service @@ -28,12 +37,18 @@ private RcsTvCallbackService rcsTvCallbackService; @Resource private TvRcsStationPollProperties pollProperties; @Resource private WcsStationStatusService wcsStationStatusService; @Resource private WmsServerFeignClient wmsServerFeignClient; public void pollOnce() { String stationId = pollProperties.getTaskNoStationId(); if (StringUtils.hasText(pollProperties.getTaskNoPollUrl())) { try { String raw = restTemplate.getForObject(pollProperties.getTaskNoPollUrl(), String.class); applyTaskNoResponse(raw, pollProperties.getTaskNoStationId()); String taskNo = applyTaskNoResponse(raw, stationId); enrichSnapshotFromWms(stationId, taskNo); } catch (RestClientException e) { log.warn("RCS 任务号轮询 HTTP 失败: {}", e.getMessage()); } catch (Exception e) { @@ -42,7 +57,7 @@ } if (StringUtils.hasText(pollProperties.getErrorPollUrl())) { try { String raw = restTemplate.getForObject(pollProperties.getErrorPollUrl(), String.class); String raw = pollErrorRaw(); applyErrorResponse(raw, pollProperties.getErrorStationId()); } catch (RestClientException e) { log.warn("RCS 异常轮询 HTTP 失败: {}", e.getMessage()); @@ -52,22 +67,104 @@ } } private void applyTaskNoResponse(String raw, String staNo) { if (!StringUtils.hasText(staNo)) { private void enrichSnapshotFromWms(String stationId, String taskNo) { if (!pollProperties.isEnrichTaskFromWms()) { return; } if (!StringUtils.hasText(stationId)) { return; } if (!StringUtils.hasText(taskNo) || "0".equals(taskNo.trim())) { wcsStationStatusService.clearStationSnapshot(stationId); return; } Map<String, Object> req = new HashMap<>(); req.put("taskNo", taskNo); try { Map<String, Object> res = wmsServerFeignClient.openAsrsQueryTask(req); if (res == null) { wcsStationStatusService.upsertFromRcsPoll(stationId, taskNo, null); return; } Object codeObj = res.get("code"); int code = parseCode(codeObj); if (code != 200) { log.debug("WMS queryTask 非成功 code={} msg={}", code, res.get("msg")); wcsStationStatusService.upsertFromRcsPoll(stationId, taskNo, null); return; } Object data = res.get("data"); @SuppressWarnings("unchecked") Map<String, Object> dataMap = data instanceof Map ? (Map<String, Object>) data : null; wcsStationStatusService.upsertFromRcsPoll(stationId, taskNo, dataMap); } catch (Exception e) { log.warn("WMS queryTask 调用失败: {}", e.getMessage()); wcsStationStatusService.upsertFromRcsPoll(stationId, taskNo, null); } } private static int parseCode(Object codeObj) { if (codeObj == null) { return 0; } if (codeObj instanceof Number) { return ((Number) codeObj).intValue(); } try { return Integer.parseInt(String.valueOf(codeObj)); } catch (Exception e) { return 0; } } private String pollErrorRaw() throws Exception { if (pollProperties.isErrorPollUsePost()) { ObjectNode body = objectMapper.createObjectNode(); body.put("staNo", pollProperties.getErrorStationId()); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(body), headers); ResponseEntity<String> resp = restTemplate.exchange( pollProperties.getErrorPollUrl(), HttpMethod.POST, entity, String.class); return resp.getBody(); } return restTemplate.getForObject(pollProperties.getErrorPollUrl(), String.class); } /** 写入 tvRcs 任务号 Hash;返回解析出的 taskNo(用于 WMS 合并) */ private String applyTaskNoResponse(String raw, String staNo) { if (!StringUtils.hasText(staNo)) { return null; } if (!StringUtils.hasText(raw)) { rcsTvCallbackService.writeStationTaskNo(staNo, null); return; return null; } String trimmed = raw.trim(); try { JsonNode root = objectMapper.readTree(trimmed); if (!httpSuccess(root)) { rcsTvCallbackService.writeStationTaskNo(staNo, null); return null; } String taskNo = extractTaskNo(root); rcsTvCallbackService.writeStationTaskNo(staNo, taskNo); return taskNo; } catch (Exception e) { rcsTvCallbackService.writeStationTaskNo(staNo, trimmed); return trimmed; } } private static boolean httpSuccess(JsonNode root) { if (root == null || !root.isObject()) { return true; } JsonNode c = root.get("code"); if (c == null || c.isNull()) { return true; } return c.asInt(200) == 200; } private static String extractTaskNo(JsonNode root) { @@ -122,15 +219,15 @@ rcsTvCallbackService.handleStationError(body); } /** * 归一成 {@link com.vincent.rsf.openApi.service.RcsTvCallbackService#handleStationError} 可识别的 data 数组 */ private JsonNode buildErrorCallbackBody(String raw, String defaultStaNo) throws Exception { if (!StringUtils.hasText(raw)) { return objectMapper.createArrayNode(); } String trimmed = raw.trim(); JsonNode root = objectMapper.readTree(trimmed); if (!httpSuccess(root)) { return objectMapper.createArrayNode(); } if (root.isArray()) { return fillStaNo((ArrayNode) root, defaultStaNo); } rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/OpenAsrsServiceImpl.java
@@ -189,6 +189,7 @@ Map<String, Object> map = new LinkedHashMap<>(); map.put("taskNo", param.getTaskNo()); map.put("barcode", task.getBarcode()); map.put("ioType", task.getTaskType()); map.put("wrkDetls", wrkDetls); return R.ok().add(map);