cl
5 天以前 9e9d5d9aed6a29b8b6b1c38cfbb7fb94b21c478b
报警回调
3个文件已添加
5个文件已修改
381 ■■■■■ 已修改文件
rsf-open-api/src/main/java/com/vincent/rsf/openApi/OpenApi.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/WmsRcsController.java 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/RcsTvCallbackService.java 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollProperties.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollSchedule.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollService.java 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-dev.yml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-prod.yml 7 ●●●●● 补丁 | 查看 | 原始文档 | 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