| rsf-open-api/src/main/java/com/vincent/rsf/openApi/OpenApi.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsRcsController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/RcsTvCallbackService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollProperties.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollSchedule.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-open-api/src/main/resources/application-dev.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rsf-open-api/src/main/resources/application-prod.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
rsf-open-api/src/main/java/com/vincent/rsf/openApi/OpenApi.java
@@ -5,9 +5,11 @@ import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class }) @EnableFeignClients(basePackages = "com.vincent.rsf.openApi.feign") @EnableScheduling public class OpenApi { public static void main(String[] args) { SpringApplication.run(OpenApi.class, args); rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsRcsController.java
@@ -9,16 +9,24 @@ import com.vincent.rsf.openApi.entity.params.RcsPubTaskParams; import com.vincent.rsf.openApi.entity.params.SyncRcsLocsParam; import com.vincent.rsf.openApi.entity.params.TaskReportParams; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.vincent.rsf.httpaudit.support.HttpAuditSupport; import com.vincent.rsf.openApi.service.WmsRcsService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StreamUtils; 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; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.nio.charset.Charset; import java.util.Map; import java.util.Objects; @@ -28,8 +36,64 @@ @RequestMapping("/rcs") public class WmsRcsController { private static final String API_AGV_ERROR = "RCS-AGV异常上报(仅接收)"; private static final String API_PUB_TASK = "调度任务下发"; private static final String API_CANCEL_TASK = "取消调度任务"; private static final String API_CALLBACK_EVENT = "状态上报回调"; private static final String API_SYNC_LOCS = "RCS库位信息同步"; private static final String API_MODIFY_STATUS = "RCS修改库位或站点状态"; private static final String API_TASK_REPORT = "RCS回调接口"; private static final String API_ALLOCATE = "RCS-申请入库任务"; @Autowired private WmsRcsService wmsRcsService; @Resource private ObjectMapper objectMapper; /** * RCS AGV 异常上报:原样接收请求体,打日志;http-audit 需配置 URI 白名单见 version/db */ @ApiOperation(API_AGV_ERROR) @PostMapping("/api/open/agvError") public CommonResponse agvError(HttpServletRequest request) throws IOException { Charset charset = HttpAuditSupport.resolveCharset(request); String body = StreamUtils.copyToString(request.getInputStream(), charset); log.info("RCS POST /rcs/api/open/agvError | {} contentType={} charset={} bytes={}\n{}", API_AGV_ERROR, request.getContentType(), charset.name(), body.getBytes(charset).length, formatBodyForLog(body)); return CommonResponse.ok(); } private String formatBodyForLog(String body) { if (body.isEmpty()) { return "(empty body)"; } try { JsonNode n = objectMapper.readTree(body); return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(n); } catch (Exception e) { int max = 16384; return body.length() > max ? body.substring(0, max) + "...(truncated,len=" + body.length() + ")" : body; } } /** RCS 入站请求体打日志(含 @ApiOperation 中文说明) */ private void logRcsRequest(String action, String apiOperationValue, Object body) { if (body == null) { log.info("RCS {} | {} request body=null", action, apiOperationValue); return; } try { log.info("RCS {} | {} request:\n{}", action, apiOperationValue, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body)); } catch (Exception e) { log.info("RCS {} | {} request: {}", action, apiOperationValue, body); } } /** * @author Ryan @@ -37,9 +101,10 @@ * @description: 任务下发 * @version 1.0 */ @ApiOperation("调度任务下发") @ApiOperation(API_PUB_TASK) @PostMapping("/pub/task") public CommonResponse pubTasks(@RequestBody RcsPubTaskParams params) { logRcsRequest("POST /rcs/pub/task", API_PUB_TASK, params); if (Objects.isNull(params)) { throw new CoolException("参数不能为空!!"); } @@ -52,9 +117,10 @@ * @description: 取消任务 * @version 1.0 */ @ApiOperation("取消调度任务") @ApiOperation(API_CANCEL_TASK) @PostMapping("/cancel/task") public CommonResponse cancelTasks(@RequestBody Map<String, Object> params) { logRcsRequest("POST /rcs/cancel/task", API_CANCEL_TASK, params); return wmsRcsService.cancelTasks(params); } @@ -64,9 +130,10 @@ * @description: 任务回调,状态回写 * @version 1.0 */ @ApiOperation("状态上报回调") @ApiOperation(API_CALLBACK_EVENT) @PostMapping("/callback/event") public CommonResponse callBackEvent(@RequestBody ExMsgCallbackParams params) { logRcsRequest("POST /rcs/callback/event", API_CALLBACK_EVENT, params); return wmsRcsService.callBackEvent(params); } @@ -77,9 +144,10 @@ * @description: RCS库位信息同步 * @version 1.0 */ @ApiOperation("RCS库位信息同步") @ApiOperation(API_SYNC_LOCS) @PostMapping("/sync/locs") public R syncLocsToWms(@RequestBody SyncRcsLocsParam params) { logRcsRequest("POST /rcs/sync/locs", API_SYNC_LOCS, params); if (Objects.isNull(params)) { return R.error("参数不能为空!!"); } @@ -92,9 +160,10 @@ * @description: WMS 出库成功后,修改库位、站点状态 * @version 1.0 */ @ApiOperation("RCS修改库位或站点状态") @ApiOperation(API_MODIFY_STATUS) @PostMapping("/modify/status") public R modifyLocOrSite(@RequestBody LocSiteParams params) { logRcsRequest("POST /rcs/modify/status", API_MODIFY_STATUS, params); if (Objects.isNull(params)) { return R.error("参数不能为空!!"); } @@ -107,10 +176,10 @@ * @description: RCS回调接口 * @version 1.0 */ @ApiOperation("RCS回调接口") @ApiOperation(API_TASK_REPORT) @PostMapping("/api/open/task/report") public CommonResponse reportTask(@RequestBody TaskReportParams params) { log.debug("RCS回调:{}", params); logRcsRequest("POST /rcs/api/open/task/report", API_TASK_REPORT, params); if (Objects.isNull(params)) { throw new CoolException("参数不能为空!!"); } @@ -123,10 +192,10 @@ * @description: 申请入库任务 * @version 1.0 */ @ApiOperation("RCS-申请入库任务") @ApiOperation(API_ALLOCATE) @PostMapping("/api/open/location/allocate") public R allocateLocation(@RequestBody LocationAllocateParams params) { log.info("申请入库任务,请求参数:{}", params); logRcsRequest("POST /rcs/api/open/location/allocate", API_ALLOCATE, params); if (Objects.isNull(params)) { return R.error("参数不能为空!!"); } rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/RcsTvCallbackService.java
@@ -122,12 +122,7 @@ if (!StringUtils.hasText(staNo)) { throw new IllegalArgumentException("staNo 不能为空"); } if (!StringUtils.hasText(taskNo)) { tvMonitorStringRedisTemplate.opsForHash().delete(TvMonitorRedisKeys.TV_RCS_STATION_TASK_NO, staNo); } else { tvMonitorStringRedisTemplate.opsForHash() .put(TvMonitorRedisKeys.TV_RCS_STATION_TASK_NO, staNo, taskNo); } writeStationTaskNo(staNo, taskNo); log.info("RCS 任务号已写入 Redis Hash staNo={} taskNo={}", staNo, taskNo); Map<String, Object> payload = new LinkedHashMap<>(); payload.put("staNo", staNo); @@ -135,6 +130,22 @@ return rcsOk(payload); } /** * 轮询 RCS 站点任务号时写入 Redis,与 {@link #handleStationTaskNo} 中 Hash 规则一致 */ public void writeStationTaskNo(String staNo, String taskNo) { if (!StringUtils.hasText(staNo)) { return; } String t = taskNo == null ? "" : taskNo.trim(); if (!StringUtils.hasText(t) || "0".equals(t)) { tvMonitorStringRedisTemplate.opsForHash().delete(TvMonitorRedisKeys.TV_RCS_STATION_TASK_NO, staNo); } else { tvMonitorStringRedisTemplate.opsForHash() .put(TvMonitorRedisKeys.TV_RCS_STATION_TASK_NO, staNo, t); } } private Map<String, Object> rcsOk(Object data) { Map<String, Object> m = new LinkedHashMap<>(); m.put("code", 200); rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollProperties.java
New file @@ -0,0 +1,32 @@ package com.vincent.rsf.openApi.tv; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * 定时拉取 RCS 站点任务号/异常并写入电视机 Redis(与 zy-monitor-admin 键一致) */ @Data @Component @ConfigurationProperties(prefix = "tv-monitor.rcs-station-poll") public class TvRcsStationPollProperties { /** 关闭时不注册定时任务 */ private boolean enabled = false; /** Spring cron,默认每 5 秒 */ private String cron = "0/5 * * * * ?"; /** GET 全路径,如 http://10.10.10.200:8088/station/getTaskNo?stationId=1007 */ private String taskNoPollUrl = ""; /** 写入 Redis Hash 的 field,与 RCS 回调 staNo 一致 */ private String taskNoStationId = "1007"; /** GET 全路径,如 http://10.10.10.200:8088/station/getError?stationId=1010 */ private String errorPollUrl = ""; /** 解析异常列表时默认站点号(写入 [staNo] 前缀) */ private String errorStationId = "1010"; } rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollSchedule.java
New file @@ -0,0 +1,25 @@ package com.vincent.rsf.openApi.tv; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; /** * 定时拉 RCS 站点数据写入 Redis,电视机侧沿用既有 WebSocket 推送 */ @Slf4j @Component @ConditionalOnProperty(prefix = "tv-monitor.rcs-station-poll", name = "enabled", havingValue = "true") public class TvRcsStationPollSchedule { @Resource private TvRcsStationPollService tvRcsStationPollService; @Scheduled(cron = "${tv-monitor.rcs-station-poll.cron:0/5 * * * * ?}") public void pollRcsStationToTvRedis() { tvRcsStationPollService.pollOnce(); } } rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollService.java
New file @@ -0,0 +1,197 @@ package com.vincent.rsf.openApi.tv; 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.service.RcsTvCallbackService; import lombok.extern.slf4j.Slf4j; 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; /** * 调用 RCS 站点接口,结果写入电视机 Redis,由 zy-monitor-admin WebSocket 推送到电视机 */ @Slf4j @Service public class TvRcsStationPollService { @Resource private RestTemplate restTemplate; @Resource private ObjectMapper objectMapper; @Resource private RcsTvCallbackService rcsTvCallbackService; @Resource private TvRcsStationPollProperties pollProperties; public void pollOnce() { if (StringUtils.hasText(pollProperties.getTaskNoPollUrl())) { try { String raw = restTemplate.getForObject(pollProperties.getTaskNoPollUrl(), String.class); applyTaskNoResponse(raw, pollProperties.getTaskNoStationId()); } catch (RestClientException e) { log.warn("RCS 任务号轮询 HTTP 失败: {}", e.getMessage()); } catch (Exception e) { log.warn("RCS 任务号轮询处理失败", e); } } if (StringUtils.hasText(pollProperties.getErrorPollUrl())) { try { String raw = restTemplate.getForObject(pollProperties.getErrorPollUrl(), String.class); applyErrorResponse(raw, pollProperties.getErrorStationId()); } catch (RestClientException e) { log.warn("RCS 异常轮询 HTTP 失败: {}", e.getMessage()); } catch (Exception e) { log.warn("RCS 异常轮询处理失败", e); } } } private void applyTaskNoResponse(String raw, String staNo) { if (!StringUtils.hasText(staNo)) { return; } if (!StringUtils.hasText(raw)) { rcsTvCallbackService.writeStationTaskNo(staNo, null); return; } String trimmed = raw.trim(); try { JsonNode root = objectMapper.readTree(trimmed); String taskNo = extractTaskNo(root); rcsTvCallbackService.writeStationTaskNo(staNo, taskNo); } catch (Exception e) { rcsTvCallbackService.writeStationTaskNo(staNo, trimmed); } } private static String extractTaskNo(JsonNode root) { if (root == null || root.isNull()) { return null; } if (root.isValueNode()) { return textNode(root); } String[] paths = {"taskNo", "task_no", "taskCode", "seqNum"}; for (String p : paths) { JsonNode n = root.get(p); if (n != null && !n.isNull() && StringUtils.hasText(textNode(n))) { return textNode(n); } } JsonNode data = root.get("data"); if (data != null && !data.isNull()) { if (data.isObject()) { for (String p : paths) { JsonNode n = data.get(p); if (n != null && !n.isNull() && StringUtils.hasText(textNode(n))) { return textNode(n); } } } else if (data.isValueNode()) { return textNode(data); } } JsonNode result = root.get("result"); if (result != null && result.isObject()) { for (String p : paths) { JsonNode n = result.get(p); if (n != null && !n.isNull() && StringUtils.hasText(textNode(n))) { return textNode(n); } } } return null; } private static String textNode(JsonNode n) { if (n == null || n.isNull()) { return ""; } String s = n.asText(""); return s == null ? "" : s.trim(); } private void applyErrorResponse(String raw, String defaultStaNo) throws Exception { JsonNode body = buildErrorCallbackBody(raw, defaultStaNo); 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 (root.isArray()) { return fillStaNo((ArrayNode) root, defaultStaNo); } if (root.isObject()) { if (root.has("data") && root.get("data").isArray()) { ObjectNode o = objectMapper.createObjectNode(); o.set("data", fillStaNo((ArrayNode) root.get("data"), defaultStaNo)); return o; } if (root.has("errors") && root.get("errors").isArray()) { ObjectNode o = objectMapper.createObjectNode(); o.set("data", fillStaNo((ArrayNode) root.get("errors"), defaultStaNo)); return o; } String msg = firstMessageField(root); if (StringUtils.hasText(msg)) { return wrapSingleError(defaultStaNo, msg); } JsonNode data = root.get("data"); if (data != null && data.isTextual()) { return wrapSingleError(defaultStaNo, data.asText()); } } return wrapSingleError(defaultStaNo, trimmed); } private JsonNode wrapSingleError(String staNo, String msg) { ArrayNode arr = objectMapper.createArrayNode(); ObjectNode item = objectMapper.createObjectNode(); if (StringUtils.hasText(staNo)) { item.put("staNo", staNo); } item.put("error", msg); arr.add(item); ObjectNode body = objectMapper.createObjectNode(); body.set("data", arr); return body; } private static String firstMessageField(JsonNode o) { String[] keys = {"errorMsg", "message", "msg", "error", "plcDesc", "desc"}; for (String k : keys) { JsonNode n = o.get(k); if (n != null && !n.isNull() && StringUtils.hasText(n.asText("").trim())) { return n.asText("").trim(); } } return ""; } private ArrayNode fillStaNo(ArrayNode arr, String defaultStaNo) { for (int i = 0; i < arr.size(); i++) { JsonNode el = arr.get(i); if (!el.isObject()) { continue; } ObjectNode obj = (ObjectNode) el; if (!StringUtils.hasText(textNode(obj.get("staNo"))) && StringUtils.hasText(defaultStaNo)) { obj.put("staNo", defaultStaNo); } } return arr; } } rsf-open-api/src/main/resources/application-dev.yml
@@ -60,6 +60,14 @@ database: 0 # 非空时要求请求头 X-Rcs-Token 与本值一致 rcs-callback-token: "" # 定时 GET RCS 站点接口 → 写入与 zy-monitor-admin 相同 Redis → 电视机 WebSocket 沿用既有推送 rcs-station-poll: enabled: false cron: "0/5 * * * * ?" task-no-poll-url: "http://10.10.10.200:8088/station/getTaskNo?stationId=1007" task-no-station-id: "1007" error-poll-url: "http://10.10.10.200:8088/station/getError?stationId=1010" error-station-id: "1010" #平台接口信息配置(如:ERP, QMS, WCS等) platform: rsf-open-api/src/main/resources/application-prod.yml
@@ -68,6 +68,13 @@ redis: database: 0 rcs-callback-token: "" rcs-station-poll: enabled: false cron: "0/5 * * * * ?" task-no-poll-url: "" task-no-station-id: "1007" error-poll-url: "" error-station-id: "1010" stock: flagAvailable: true