cl
昨天 9ca146e7afa543d2c516a91c95929d34aa48a52c
电视机提醒
1个文件已添加
5个文件已修改
456 ■■■■■ 已修改文件
rsf-open-api/src/main/java/com/vincent/rsf/openApi/controller/MonitorController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/WcsStationStatusService.java 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvMonitorRedisKeys.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollProperties.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/tv/TvRcsStationPollService.java 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/OpenAsrsServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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);