| | |
| | | if (serverHttpRequest instanceof ServletServerHttpRequest) { |
| | | HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest(); |
| | | Object appAuth = request.getAttribute("appAuth"); |
| | | if (appAuth != null) { |
| | | if (o instanceof R) { |
| | | if (appAuth != null && o instanceof R) { |
| | | String appkey = request.getHeader("appkey"); |
| | | if (Cools.isEmpty(appkey)) { |
| | | appkey = "-"; |
| | | } |
| | | Object reqCache = request.getAttribute("cache"); |
| | | if (!Cools.isEmpty(appkey)) { |
| | | boolean success = String.valueOf(((R) o).get("code")).equalsIgnoreCase("200"); |
| | | if (success){ |
| | | // 保存接口日志 |
| | | apiLogService.save( |
| | | String.valueOf(appAuth), |
| | | request.getRequestURI(), |
| | |
| | | JSON.toJSONString(o), |
| | | success); |
| | | } |
| | | } else if (o instanceof R && isInboundThirdPartyUri(request.getRequestURI())) { |
| | | String appkey = request.getHeader("appkey"); |
| | | if (Cools.isEmpty(appkey)) { |
| | | appkey = "-"; |
| | | } |
| | | Object reqCache = request.getAttribute("cache"); |
| | | boolean success = String.valueOf(((R) o).get("code")).equalsIgnoreCase("200"); |
| | | String ns = inboundNamespace(request.getRequestURI()); |
| | | if (success) { |
| | | apiLogService.save( |
| | | ns, |
| | | request.getRequestURI(), |
| | | appkey, |
| | | IpTools.gainRealIp(request), |
| | | reqCache == null ? "" : JSON.toJSONString(reqCache), |
| | | JSON.toJSONString(o), |
| | | success |
| | | ); |
| | | } else { |
| | | beforeBodyWriteCallApiLogSave( |
| | | ns, |
| | | request.getRequestURI(), |
| | | appkey, |
| | | IpTools.gainRealIp(request), |
| | | reqCache == null ? "" : JSON.toJSONString(reqCache), |
| | | JSON.toJSONString(o), |
| | | success); |
| | | } |
| | | } |
| | | } |
| | | return o; |
| | | } |
| | | |
| | | private static boolean isInboundThirdPartyUri(String uri) { |
| | | if (uri == null) { |
| | | return false; |
| | | } |
| | | return uri.contains("/open/asrs") || uri.contains("/wcs/openapi/report"); |
| | | } |
| | | |
| | | private static String inboundNamespace(String uri) { |
| | | if (uri != null && uri.contains("/wcs/openapi/report")) { |
| | | return "WCS回写"; |
| | | } |
| | | return "开放接口"; |
| | | } |
| | | |
| | | public void beforeBodyWriteCallApiLogSave(String name, String url, String appkey, String ip, String request, String response, boolean success) { |
| | | ApiLogService apiLogService = SpringUtils.getBean(ApiLogService.class); |
| | | String memo = response; |
| | |
| | | void publish(String topic, String payload) throws Exception; |
| | | |
| | | boolean isConnected(); |
| | | |
| | | /** 重新读库并断开/重建连接,用于后台改配置后生效。 */ |
| | | void reconnectFromDbConfig(); |
| | | } |
| | |
| | | package com.zy.integration.iot.handler.impl; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONException; |
| | | import com.core.common.Cools; |
| | | import com.zy.integration.iot.biz.IotInstructionService; |
| | | import com.zy.integration.iot.handler.IotInboundMessageHandler; |
| | | import com.zy.integration.iot.publish.IotPublishService; |
| | | import com.zy.iot.config.IotProperties; |
| | | import com.zy.iot.service.IotDbConfigService; |
| | | import com.zy.iot.entity.IotFeedbackMessage; |
| | | import com.zy.iot.entity.IotInstructionMessage; |
| | | import com.zy.iot.entity.IotTopicConfig; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | |
| | | public class IotInboundMessageHandlerImpl implements IotInboundMessageHandler { |
| | | |
| | | @Autowired |
| | | private IotProperties iotProperties; |
| | | private IotDbConfigService iotDbConfigService; |
| | | @Autowired |
| | | private IotInstructionService iotInstructionService; |
| | | @Autowired |
| | |
| | | */ |
| | | @Override |
| | | public void handleMessage(String topic, String payload) { |
| | | if (!iotProperties.isEnabled() || Cools.isEmpty(topic) || payload == null) { |
| | | if (!iotDbConfigService.isMqttEnabled() || Cools.isEmpty(topic) || payload == null) { |
| | | return; |
| | | } |
| | | try { |
| | | IotTopicConfig topics = iotDbConfigService.getEffectiveTopics(); |
| | | Long recordId = null; |
| | | if (topic.equals(iotProperties.getTopics().getEgressStow())) { |
| | | recordId = iotInstructionService.handleStowInstruction(JSON.parseObject(payload, IotInstructionMessage.class), topic, payload); |
| | | } else if (topic.equals(iotProperties.getTopics().getEgressPick())) { |
| | | recordId = iotInstructionService.handlePickInstruction(JSON.parseObject(payload, IotInstructionMessage.class), topic, payload); |
| | | } else if (topic.equals(iotProperties.getTopics().getEgressFeedback())) { |
| | | iotInstructionService.handleFeedbackAck(JSON.parseObject(payload, IotFeedbackMessage.class), topic, payload); |
| | | if (topic.equals(topics.getEgressStow())) { |
| | | IotInstructionMessage stow = parseJsonPayload(payload, topic, IotInstructionMessage.class); |
| | | if (stow == null) { |
| | | return; |
| | | } |
| | | recordId = iotInstructionService.handleStowInstruction(stow, topic, payload); |
| | | } else if (topic.equals(topics.getEgressPick())) { |
| | | IotInstructionMessage pick = parseJsonPayload(payload, topic, IotInstructionMessage.class); |
| | | if (pick == null) { |
| | | return; |
| | | } |
| | | recordId = iotInstructionService.handlePickInstruction(pick, topic, payload); |
| | | } else if (topic.equals(topics.getEgressFeedback())) { |
| | | IotFeedbackMessage ack = parseJsonPayload(payload, topic, IotFeedbackMessage.class); |
| | | if (ack == null) { |
| | | return; |
| | | } |
| | | iotInstructionService.handleFeedbackAck(ack, topic, payload); |
| | | } else { |
| | | log.warn("ignore unknown iot topic={}, payload={}", topic, payload); |
| | | } |
| | |
| | | log.error("handle iot inbound message failed, topic={}, payload={}", topic, payload, e); |
| | | } |
| | | } |
| | | |
| | | private <T> T parseJsonPayload(String payload, String topic, Class<T> type) { |
| | | try { |
| | | return JSON.parseObject(payload, type); |
| | | } catch (JSONException e) { |
| | | log.warn("IoT MQTT 非合法 JSON topic={} payload={}", topic, payload, e); |
| | | return null; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.integration.iot.paho; |
| | | |
| | | import org.eclipse.paho.client.mqttv3.MqttConnectOptions; |
| | | import org.eclipse.paho.client.mqttv3.MqttException; |
| | | import org.eclipse.paho.client.mqttv3.internal.NetworkModule; |
| | | import org.eclipse.paho.client.mqttv3.internal.TCPNetworkModuleFactory; |
| | | import org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory; |
| | | |
| | | import java.net.URI; |
| | | import java.net.URISyntaxException; |
| | | import java.util.Collections; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * Paho 仅内置 ssl/tcp/ws/wss,通过 SPI 增加 mqtt,与 tcp 等价建连。 |
| | | */ |
| | | public final class MqttNetworkModuleFactory implements NetworkModuleFactory { |
| | | |
| | | private static final TCPNetworkModuleFactory DELEGATE = new TCPNetworkModuleFactory(); |
| | | |
| | | @Override |
| | | public Set<String> getSupportedUriSchemes() { |
| | | return Collections.singleton("mqtt"); |
| | | } |
| | | |
| | | @Override |
| | | public void validateURI(URI uri) throws IllegalArgumentException { |
| | | DELEGATE.validateURI(toTcpUri(uri)); |
| | | } |
| | | |
| | | @Override |
| | | public NetworkModule createNetworkModule(URI uri, MqttConnectOptions options, String clientId) throws MqttException { |
| | | return DELEGATE.createNetworkModule(toTcpUri(uri), options, clientId); |
| | | } |
| | | |
| | | private static URI toTcpUri(URI uri) { |
| | | try { |
| | | return new URI( |
| | | "tcp", |
| | | uri.getRawUserInfo(), |
| | | uri.getHost(), |
| | | uri.getPort(), |
| | | uri.getRawPath(), |
| | | uri.getRawQuery(), |
| | | uri.getRawFragment()); |
| | | } catch (URISyntaxException e) { |
| | | throw new IllegalArgumentException(e); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.integration.iot.paho; |
| | | |
| | | import org.eclipse.paho.client.mqttv3.MqttConnectOptions; |
| | | import org.eclipse.paho.client.mqttv3.MqttException; |
| | | import org.eclipse.paho.client.mqttv3.internal.NetworkModule; |
| | | import org.eclipse.paho.client.mqttv3.internal.SSLNetworkModuleFactory; |
| | | import org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory; |
| | | |
| | | import java.net.URI; |
| | | import java.net.URISyntaxException; |
| | | import java.util.Collections; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * Paho 仅内置 ssl/tcp/ws/wss,通过 SPI 增加 mqtts,与 ssl 等价建连。 |
| | | */ |
| | | public final class MqttsNetworkModuleFactory implements NetworkModuleFactory { |
| | | |
| | | private static final SSLNetworkModuleFactory DELEGATE = new SSLNetworkModuleFactory(); |
| | | |
| | | @Override |
| | | public Set<String> getSupportedUriSchemes() { |
| | | return Collections.singleton("mqtts"); |
| | | } |
| | | |
| | | @Override |
| | | public void validateURI(URI uri) throws IllegalArgumentException { |
| | | DELEGATE.validateURI(toSslUri(uri)); |
| | | } |
| | | |
| | | @Override |
| | | public NetworkModule createNetworkModule(URI uri, MqttConnectOptions options, String clientId) throws MqttException { |
| | | return DELEGATE.createNetworkModule(toSslUri(uri), options, clientId); |
| | | } |
| | | |
| | | private static URI toSslUri(URI uri) { |
| | | try { |
| | | return new URI( |
| | | "ssl", |
| | | uri.getRawUserInfo(), |
| | | uri.getHost(), |
| | | uri.getPort(), |
| | | uri.getRawPath(), |
| | | uri.getRawQuery(), |
| | | uri.getRawFragment()); |
| | | } catch (URISyntaxException e) { |
| | | throw new IllegalArgumentException(e); |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.zy.integration.iot.task; |
| | | |
| | | import com.zy.integration.iot.publish.IotPublishService; |
| | | import com.zy.iot.config.IotProperties; |
| | | import com.zy.iot.service.IotDbConfigService; |
| | | import com.zy.iot.entity.IotPublishRecord; |
| | | import com.zy.iot.service.IotPublishRecordService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | |
| | | public class IotPendingPublishScheduler { |
| | | |
| | | @Autowired |
| | | private IotProperties iotProperties; |
| | | private IotDbConfigService iotDbConfigService; |
| | | @Autowired |
| | | private IotPublishRecordService iotPublishRecordService; |
| | | @Autowired |
| | |
| | | */ |
| | | @Scheduled(cron = "0/5 * * * * ? ") |
| | | private void execute() { |
| | | if (!iotProperties.isEnabled()) { |
| | | if (!iotDbConfigService.isMqttEnabled()) { |
| | | return; |
| | | } |
| | | List<IotPublishRecord> records = iotPublishRecordService.selectPendingPublishes(50); |
| | |
| | | package com.zy.iot.config; |
| | | |
| | | import com.zy.iot.entity.IotTopicConfig; |
| | | import lombok.AccessLevel; |
| | | import lombok.Data; |
| | | import lombok.Getter; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ThreadLocalRandom; |
| | | |
| | | @Data |
| | | @Component |
| | |
| | | |
| | | private Map<String, Integer> pickStationMappings = new LinkedHashMap<>(); |
| | | |
| | | @Getter(AccessLevel.NONE) |
| | | private volatile String resolvedClientIdCache; |
| | | |
| | | public String getResolvedClientId() { |
| | | if (clientId != null && clientId.trim().length() > 0) { |
| | | return clientId.trim(); |
| | | if (resolvedClientIdCache != null) { |
| | | return resolvedClientIdCache; |
| | | } |
| | | return thingName; |
| | | synchronized (this) { |
| | | if (resolvedClientIdCache != null) { |
| | | return resolvedClientIdCache; |
| | | } |
| | | String base = null; |
| | | if (clientId != null && clientId.trim().length() > 0) { |
| | | base = clientId.trim(); |
| | | } else if (thingName != null && thingName.trim().length() > 0) { |
| | | base = thingName.trim(); |
| | | } |
| | | if (base == null) { |
| | | return null; |
| | | } |
| | | resolvedClientIdCache = base + "-" + newClientIdSuffix(); |
| | | return resolvedClientIdCache; |
| | | } |
| | | } |
| | | |
| | | private static String newClientIdSuffix() { |
| | | long n = ThreadLocalRandom.current().nextLong() & 0xFFFFFFFFL; |
| | | return Long.toHexString(n); |
| | | } |
| | | |
| | | public boolean isTlsEnabled() { |
| | | String serverUri = getServerUri(); |
| | | return serverUri != null && serverUri.startsWith("ssl://"); |
| | | return isTlsMqttScheme(getServerUri()); |
| | | } |
| | | |
| | | /** 与 Paho 建连 URI:完整前缀 mqtts/mqtt/wss/ws/ssl/tcp 原样;仅主机名时默认 mqtts 或 mqtt。 */ |
| | | public String getServerUri() { |
| | | if (endpoint == null || endpoint.trim().length() == 0) { |
| | | return null; |
| | | } |
| | | String trimmed = endpoint.trim(); |
| | | if (trimmed.startsWith("ssl://") || trimmed.startsWith("tcp://")) { |
| | | if (hasExplicitBrokerScheme(trimmed)) { |
| | | return trimmed; |
| | | } |
| | | int resolvedPort = port == null ? 8883 : port; |
| | | return "ssl://" + trimmed + ":" + resolvedPort; |
| | | String scheme = resolvedPort == 1883 ? "mqtt://" : "mqtts://"; |
| | | return scheme + trimmed + ":" + resolvedPort; |
| | | } |
| | | |
| | | private static boolean hasExplicitBrokerScheme(String s) { |
| | | String lower = s.toLowerCase(Locale.ROOT); |
| | | return lower.startsWith("mqtts://") || lower.startsWith("mqtt://") |
| | | || lower.startsWith("ssl://") || lower.startsWith("tcp://") |
| | | || lower.startsWith("wss://") || lower.startsWith("ws://"); |
| | | } |
| | | |
| | | private static boolean isTlsMqttScheme(String serverUri) { |
| | | if (serverUri == null || serverUri.isEmpty()) { |
| | | return false; |
| | | } |
| | | int p = serverUri.indexOf("://"); |
| | | if (p <= 0) { |
| | | return false; |
| | | } |
| | | String scheme = serverUri.substring(0, p).toLowerCase(Locale.ROOT); |
| | | return "mqtts".equals(scheme) || "ssl".equals(scheme) || "wss".equals(scheme); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.iot.constant; |
| | | |
| | | public final class IotSysConfigCodes { |
| | | |
| | | private IotSysConfigCodes() { |
| | | } |
| | | |
| | | public static final String MQTT_ENABLED = "iot.mqtt.enabled"; |
| | | |
| | | public static final String TOPIC_EGRESS_STOW = "iot.topic.egress_stow"; |
| | | public static final String TOPIC_EGRESS_PICK = "iot.topic.egress_pick"; |
| | | public static final String TOPIC_EGRESS_FEEDBACK = "iot.topic.egress_feedback"; |
| | | public static final String TOPIC_INGRESS_STOW = "iot.topic.ingress_stow"; |
| | | public static final String TOPIC_INGRESS_PICK = "iot.topic.ingress_pick"; |
| | | public static final String TOPIC_INGRESS_FEEDBACK = "iot.topic.ingress_feedback"; |
| | | } |
| New file |
| | |
| | | package com.zy.iot.controller; |
| | | |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.core.annotations.ManagerAuth; |
| | | import com.core.common.Cools; |
| | | import com.core.common.R; |
| | | import com.zy.common.web.BaseController; |
| | | import com.zy.integration.iot.client.IotMqttClient; |
| | | import com.zy.iot.constant.IotSysConfigCodes; |
| | | import com.zy.iot.service.IotDbConfigService; |
| | | import com.zy.system.entity.Config; |
| | | import com.zy.system.service.ConfigService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestMethod; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 后台:IoT MQTT 开关与 topic 存 sys_config,重连后订阅生效。 |
| | | */ |
| | | @RestController |
| | | public class IotMqttAdminController extends BaseController { |
| | | |
| | | @Autowired |
| | | private ConfigService configService; |
| | | @Autowired |
| | | private IotDbConfigService iotDbConfigService; |
| | | @Autowired |
| | | private IotMqttClient iotMqttClient; |
| | | |
| | | @RequestMapping(value = "/iotMqttAdmin/config/auth", method = RequestMethod.GET) |
| | | @ManagerAuth(memo = "IoT MQTT 配置查询") |
| | | public R getConfig() { |
| | | Map<String, Object> data = new LinkedHashMap<>(iotDbConfigService.snapshotForAdmin()); |
| | | data.put("mqttConnected", iotMqttClient.isConnected()); |
| | | return R.ok(data); |
| | | } |
| | | |
| | | @RequestMapping(value = "/iotMqttAdmin/config/save/auth", method = RequestMethod.POST) |
| | | @ManagerAuth(memo = "IoT MQTT 配置保存") |
| | | public R saveConfig(@RequestBody JSONObject body) { |
| | | if (body == null) { |
| | | return R.error("参数为空"); |
| | | } |
| | | if (body.containsKey("enabled")) { |
| | | Boolean en = body.getBoolean("enabled"); |
| | | upsert(IotSysConfigCodes.MQTT_ENABLED, "IoT MQTT 总开关", en == null ? "false" : String.valueOf(en)); |
| | | } |
| | | JSONObject topics = body.getJSONObject("topics"); |
| | | if (topics != null) { |
| | | putTopicIfPresent(topics, "egressStow", IotSysConfigCodes.TOPIC_EGRESS_STOW, "IoT topic egress stow"); |
| | | putTopicIfPresent(topics, "egressPick", IotSysConfigCodes.TOPIC_EGRESS_PICK, "IoT topic egress pick"); |
| | | putTopicIfPresent(topics, "egressFeedback", IotSysConfigCodes.TOPIC_EGRESS_FEEDBACK, "IoT topic egress feedback"); |
| | | putTopicIfPresent(topics, "ingressStow", IotSysConfigCodes.TOPIC_INGRESS_STOW, "IoT topic ingress stow"); |
| | | putTopicIfPresent(topics, "ingressPick", IotSysConfigCodes.TOPIC_INGRESS_PICK, "IoT topic ingress pick"); |
| | | putTopicIfPresent(topics, "ingressFeedback", IotSysConfigCodes.TOPIC_INGRESS_FEEDBACK, "IoT topic ingress feedback"); |
| | | } |
| | | iotDbConfigService.refreshCache(); |
| | | return R.ok(); |
| | | } |
| | | |
| | | private void putTopicIfPresent(JSONObject topics, String key, String code, String name) { |
| | | if (!topics.containsKey(key)) { |
| | | return; |
| | | } |
| | | String v = topics.getString(key); |
| | | if (v == null) { |
| | | return; |
| | | } |
| | | upsert(code, name, v.trim()); |
| | | } |
| | | |
| | | @RequestMapping(value = "/iotMqttAdmin/reconnect/auth", method = RequestMethod.POST) |
| | | @ManagerAuth(memo = "IoT MQTT 重连") |
| | | public R reconnect() { |
| | | iotMqttClient.reconnectFromDbConfig(); |
| | | return R.ok(); |
| | | } |
| | | |
| | | private void upsert(String code, String name, String value) { |
| | | if (Cools.isEmpty(code)) { |
| | | return; |
| | | } |
| | | Config existing = configService.selectConfigByCode(code); |
| | | if (existing != null) { |
| | | existing.setValue(value); |
| | | existing.setStatus((short) 1); |
| | | configService.updateById(existing); |
| | | } else { |
| | | configService.insert(new Config(name, code, value, (short) 1, (short) 1)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.iot.controller; |
| | | |
| | | import com.baomidou.mybatisplus.mapper.EntityWrapper; |
| | | import com.baomidou.mybatisplus.plugins.Page; |
| | | import com.core.annotations.ManagerAuth; |
| | | import com.core.common.Cools; |
| | | import com.core.common.R; |
| | | import com.zy.common.web.BaseController; |
| | | import com.zy.integration.iot.publish.IotPublishService; |
| | | import com.zy.iot.service.IotDbConfigService; |
| | | import com.zy.iot.constant.IotConstants; |
| | | import com.zy.iot.entity.IotPublishRecord; |
| | | import com.zy.iot.service.IotPublishRecordService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | /** |
| | | * 后台:仅出站 MQTT(WMS 发出),支持列表与手动重发。 |
| | | */ |
| | | @RestController |
| | | public class IotMqttOutboundController extends BaseController { |
| | | |
| | | @Autowired |
| | | private IotDbConfigService iotDbConfigService; |
| | | @Autowired |
| | | private IotPublishRecordService iotPublishRecordService; |
| | | @Autowired |
| | | private IotPublishService iotPublishService; |
| | | |
| | | @RequestMapping(value = "/iotMqttOutbound/list/auth") |
| | | @ManagerAuth(memo = "IoT MQTT 发送记录") |
| | | public R list(@RequestParam(defaultValue = "1") Integer curr, |
| | | @RequestParam(defaultValue = "16") Integer limit, |
| | | @RequestParam(required = false) String instruction_id, |
| | | @RequestParam(required = false) String publish_status, |
| | | @RequestParam(required = false) String publish_topic, |
| | | @RequestParam(required = false) String container_id) { |
| | | EntityWrapper<IotPublishRecord> wrapper = new EntityWrapper<>(); |
| | | wrapper.eq("direction", IotConstants.DIRECTION_OUTBOUND); |
| | | wrapper.isNotNull("publish_topic"); |
| | | wrapper.isNotNull("publish_payload"); |
| | | if (!Cools.isEmpty(instruction_id)) { |
| | | wrapper.like("instruction_id", instruction_id); |
| | | } |
| | | if (!Cools.isEmpty(publish_status)) { |
| | | wrapper.eq("publish_status", publish_status); |
| | | } |
| | | if (!Cools.isEmpty(publish_topic)) { |
| | | wrapper.like("publish_topic", publish_topic); |
| | | } |
| | | if (!Cools.isEmpty(container_id)) { |
| | | wrapper.like("container_id", container_id); |
| | | } |
| | | wrapper.orderBy("create_time", false); |
| | | return R.ok(iotPublishRecordService.selectPage(new Page<>(curr, limit), wrapper)); |
| | | } |
| | | |
| | | @RequestMapping(value = "/iotMqttOutbound/resend/auth") |
| | | @ManagerAuth(memo = "IoT MQTT 发送重发") |
| | | public R resend(@RequestParam Long id) { |
| | | if (id == null) { |
| | | return R.error("缺少 id"); |
| | | } |
| | | if (!iotDbConfigService.isMqttEnabled()) { |
| | | return R.error("IoT 未开启"); |
| | | } |
| | | IotPublishRecord record = iotPublishRecordService.selectById(id); |
| | | if (record == null) { |
| | | return R.error("记录不存在"); |
| | | } |
| | | if (!IotConstants.DIRECTION_OUTBOUND.equals(record.getDirection())) { |
| | | return R.error("仅支持出站记录重发"); |
| | | } |
| | | if (Cools.isEmpty(record.getPublishTopic()) || Cools.isEmpty(record.getPublishPayload())) { |
| | | return R.error("无发送主题或消息体"); |
| | | } |
| | | iotPublishService.publishRecordNow(id); |
| | | record = iotPublishRecordService.selectById(id); |
| | | if (record != null && IotConstants.PUBLISH_STATUS_SUCCESS.equals(record.getPublishStatus())) { |
| | | return R.ok(); |
| | | } |
| | | String err = record != null && !Cools.isEmpty(record.getErrorMessage()) |
| | | ? record.getErrorMessage() |
| | | : "发送失败"; |
| | | return R.error(err); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.iot.service; |
| | | |
| | | import com.zy.iot.entity.IotTopicConfig; |
| | | |
| | | import java.util.Map; |
| | | |
| | | public interface IotDbConfigService { |
| | | |
| | | void refreshCache(); |
| | | |
| | | boolean isMqttEnabled(); |
| | | |
| | | IotTopicConfig getEffectiveTopics(); |
| | | |
| | | /** 当前生效开关与 topic,供后台表单展示。 */ |
| | | Map<String, Object> snapshotForAdmin(); |
| | | } |
| New file |
| | |
| | | package com.zy.iot.service.impl; |
| | | |
| | | import com.core.common.Cools; |
| | | import com.zy.iot.config.IotProperties; |
| | | import com.zy.iot.constant.IotSysConfigCodes; |
| | | import com.zy.iot.entity.IotTopicConfig; |
| | | import com.zy.iot.service.IotDbConfigService; |
| | | import com.zy.system.entity.Config; |
| | | import com.zy.system.service.ConfigService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | public class IotDbConfigServiceImpl implements IotDbConfigService { |
| | | |
| | | @Autowired |
| | | private ConfigService configService; |
| | | @Autowired |
| | | private IotProperties iotProperties; |
| | | |
| | | private volatile Snapshot snapshot; |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | refreshCache(); |
| | | } |
| | | |
| | | @Override |
| | | public synchronized void refreshCache() { |
| | | Snapshot s = new Snapshot(); |
| | | s.enabled = resolveEnabled(); |
| | | s.topics = resolveTopics(); |
| | | this.snapshot = s; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isMqttEnabled() { |
| | | Snapshot s = snapshot; |
| | | return s != null && s.enabled; |
| | | } |
| | | |
| | | @Override |
| | | public IotTopicConfig getEffectiveTopics() { |
| | | Snapshot s = snapshot; |
| | | if (s == null || s.topics == null) { |
| | | return copyTopics(iotProperties.getTopics()); |
| | | } |
| | | return copyTopics(s.topics); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> snapshotForAdmin() { |
| | | Snapshot s = snapshot; |
| | | if (s == null) { |
| | | refreshCache(); |
| | | s = snapshot; |
| | | } |
| | | Map<String, Object> root = new LinkedHashMap<>(); |
| | | root.put("enabled", s != null && s.enabled); |
| | | Map<String, String> topics = new LinkedHashMap<>(); |
| | | IotTopicConfig t = s != null && s.topics != null ? s.topics : iotProperties.getTopics(); |
| | | topics.put("egressStow", t.getEgressStow()); |
| | | topics.put("egressPick", t.getEgressPick()); |
| | | topics.put("egressFeedback", t.getEgressFeedback()); |
| | | topics.put("ingressStow", t.getIngressStow()); |
| | | topics.put("ingressPick", t.getIngressPick()); |
| | | topics.put("ingressFeedback", t.getIngressFeedback()); |
| | | root.put("topics", topics); |
| | | return root; |
| | | } |
| | | |
| | | private boolean resolveEnabled() { |
| | | Config c = configService.selectConfigByCode(IotSysConfigCodes.MQTT_ENABLED); |
| | | if (c != null && isConfigActive(c) && !Cools.isEmpty(c.getValue())) { |
| | | String v = c.getValue().trim(); |
| | | if ("1".equals(v) || "true".equalsIgnoreCase(v) || "yes".equalsIgnoreCase(v)) { |
| | | return true; |
| | | } |
| | | if ("0".equals(v) || "false".equalsIgnoreCase(v) || "no".equalsIgnoreCase(v)) { |
| | | return false; |
| | | } |
| | | } |
| | | return iotProperties.isEnabled(); |
| | | } |
| | | |
| | | private IotTopicConfig resolveTopics() { |
| | | IotTopicConfig yml = iotProperties.getTopics(); |
| | | IotTopicConfig t = new IotTopicConfig(); |
| | | t.setEgressStow(orDb(IotSysConfigCodes.TOPIC_EGRESS_STOW, yml.getEgressStow())); |
| | | t.setEgressPick(orDb(IotSysConfigCodes.TOPIC_EGRESS_PICK, yml.getEgressPick())); |
| | | t.setEgressFeedback(orDb(IotSysConfigCodes.TOPIC_EGRESS_FEEDBACK, yml.getEgressFeedback())); |
| | | t.setIngressStow(orDb(IotSysConfigCodes.TOPIC_INGRESS_STOW, yml.getIngressStow())); |
| | | t.setIngressPick(orDb(IotSysConfigCodes.TOPIC_INGRESS_PICK, yml.getIngressPick())); |
| | | t.setIngressFeedback(orDb(IotSysConfigCodes.TOPIC_INGRESS_FEEDBACK, yml.getIngressFeedback())); |
| | | return t; |
| | | } |
| | | |
| | | private String orDb(String code, String ymlDefault) { |
| | | Config c = configService.selectConfigByCode(code); |
| | | if (c != null && isConfigActive(c) && !Cools.isEmpty(c.getValue())) { |
| | | return c.getValue().trim(); |
| | | } |
| | | return ymlDefault; |
| | | } |
| | | |
| | | private static boolean isConfigActive(Config c) { |
| | | return c.getStatus() != null && c.getStatus() == 1; |
| | | } |
| | | |
| | | private static IotTopicConfig copyTopics(IotTopicConfig src) { |
| | | if (src == null) { |
| | | return new IotTopicConfig(); |
| | | } |
| | | IotTopicConfig c = new IotTopicConfig(); |
| | | c.setEgressStow(src.getEgressStow()); |
| | | c.setEgressPick(src.getEgressPick()); |
| | | c.setEgressFeedback(src.getEgressFeedback()); |
| | | c.setIngressStow(src.getIngressStow()); |
| | | c.setIngressPick(src.getIngressPick()); |
| | | c.setIngressFeedback(src.getIngressFeedback()); |
| | | return c; |
| | | } |
| | | |
| | | private static final class Snapshot { |
| | | private boolean enabled; |
| | | private IotTopicConfig topics; |
| | | } |
| | | } |
| New file |
| | |
| | | com.zy.integration.iot.paho.MqttsNetworkModuleFactory |
| | | com.zy.integration.iot.paho.MqttNetworkModuleFactory |
| | |
| | | switch: true |
| | | status-sync: |
| | | enabled: true |
| | | # 失败时是否打 WARN/ERROR(本地无 WCS 时可设 false,需排查时再开) |
| | | log-on-failure: true |
| | | initial-delay: 10000 |
| | | fixed-delay: 5000 |
| | | method: POST |
| | | # 地址 |
| | | address: |
| | | URL: https://127.0.0.1:9090/wcs |
| | | URL: http://127.0.0.1:9090/wcs |
| | | #入库任务下发地址 |
| | | createInTask : /openapi/createInTask |
| | | #出库任务下发地址 |
| | |
| | | stopOutTask: /openapi/cancelTaskBatch |
| | | #设备状态获取地址 |
| | | getDeviceStatus: /openapi/deviceStatus |
| | | #按 WMS 任务号查询 WCS 是否已接收任务 |
| | | queryTask: /openapi/queryTask |
| | | |
| | | |
| | | # AWS IoT 对接开关与 topic/证书配置。 |
| | | # 默认关闭,只有配置齐 endpoint 和证书路径后才会尝试建连。 |
| | | iot: |
| | | enabled: true |
| | | endpoint: tcp://192.168.100.170:1883 |
| | | port: 1883 |
| | | enabled: false |
| | | # endpoint: 192.168.100.170 |
| | | # port: 1883 |
| | | endpoint: a21wi8dwvkjf1d.ats.iot.cn-north-1.amazonaws.com.cn |
| | | port: 8883 |
| | | thingName: asrs-iot-client |
| | | clientId: asrs-iot-client |
| | | cleanSession: false |
| | |
| | | keepAliveSeconds: 60 |
| | | connectionTimeoutSeconds: 10 |
| | | persistenceDir: .local/iot-mqtt |
| | | caCertPath: |
| | | clientCertPath: |
| | | privateKeyPath: |
| | | caCertPath: D:/work/work-zy/gsl/zy-wms-gsl/src/main/resources/iot-certs/AmazonRootCA1.pem |
| | | clientCertPath: D:/work/work-zy/gsl/zy-wms-gsl/src/main/resources/iot-certs/device-certificate.pem.crt |
| | | privateKeyPath: D:/work/work-zy/gsl/zy-wms-gsl/src/main/resources/iot-certs/device-private.pem.key |
| | | topics: |
| | | #亚马逊发送组托给wms,在托盘上收到纸箱后,发布堆垛指令以将托盘发送到仓库。 |
| | | egressStow: glenn/instruction/icna/egress/asrs/stow |
| | | egressStow: glenn/instruction/icng/egress/asrs/stow |
| | | #亚马逊发送出库指令给wms,启动拣选请求后,发布拣选指令以引导ASRS从料箱中取货。 |
| | | egressPick: glenn/instruction/icna/egress/asrs/pick |
| | | egressPick: glenn/instruction/icng/egress/asrs/pick |
| | | #wms入库完成发送给亚马逊,ASRS实际拖拽托盘到料箱后,发布堆垛动作以同步BPS数据。 |
| | | egressFeedback: glenn/instruction/icna/egress/asrs/feedback |
| | | egressFeedback: glenn/instruction/icng/egress/asrs/feedback |
| | | #wms出库完成发送给亚马逊,在ASRS实际从料箱取货后,发布拣选动作sz XBPS数据。 |
| | | ingressStow: glenn/instruction/icna/ingress/asrs/stow |
| | | ingressStow: glenn/instruction/icng/ingress/asrs/stow |
| | | #ASRS 接收到 XBPS 指令后,向反馈XBPS 发送响应。 |
| | | ingressPick: glenn/instruction/icna/ingress/asrs/pick |
| | | ingressPick: glenn/instruction/icng/ingress/asrs/pick |
| | | #在XBPs收到ASRS操作后,向反馈ASRS发送响应 |
| | | ingressFeedback: glenn/instruction/icna/ingress/asrs/feedback |
| | | ingressFeedback: glenn/instruction/icng/ingress/asrs/feedback |
| | | pickStationMappings: |
| | | ASRSOutbound1: 101 |
| New file |
| | |
| | | -----BEGIN CERTIFICATE----- |
| | | MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF |
| | | ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 |
| | | b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL |
| | | MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv |
| | | b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj |
| | | ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM |
| | | 9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw |
| | | IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 |
| | | VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L |
| | | 93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm |
| | | jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC |
| | | AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA |
| | | A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI |
| | | U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs |
| | | N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv |
| | | o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU |
| | | 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy |
| | | rqXRfboQnoZsG4q5WTP468SQvvG5 |
| | | -----END CERTIFICATE----- |
| New file |
| | |
| | | -----BEGIN CERTIFICATE----- |
| | | MIIDWTCCAkGgAwIBAgIUadE+uh4j8msPlgfGjldjbBLP+tMwDQYJKoZIhvcNAQEL |
| | | BQAwTTFLMEkGA1UECwxCQW1hem9uIFdlYiBTZXJ2aWNlcyBPPUFtYXpvbi5jb20g |
| | | SW5jLiBMPVNlYXR0bGUgU1Q9V2FzaGluZ3RvbiBDPVVTMB4XDTI2MDMyNTIxNDIx |
| | | M1oXDTQ5MTIzMTIzNTk1OVowHjEcMBoGA1UEAwwTQVdTIElvVCBDZXJ0aWZpY2F0 |
| | | ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOdorxd56BY+uJqANr5B |
| | | N+zz+XeJFXd2ou6e30gG1URBomsMWZts0bhqfG+GyrqaZ0KFi6Z1yhbk0DfSir5e |
| | | FxPtfG5Rvo05V9RjsjGrSh3LeeZnTSgcGHimtwfz5D72j6nuxCdOiEpctLYyjxqM |
| | | whe02xOT+AuS82Q2kCurCrBlsxyzRh3HKJ0xT2/lohDgP1IDbxKTYJBOm1nvRRqa |
| | | GmFab+BRAIjT1Y4G7cs2EXmE23ykkEPZ6TSXMij20lN9VDF3fUUuBI5G7rcq9HfS |
| | | v1lQzVwF9NmbGfRNWobHwrvT7G9d74UXWmFfdrnzowaWRh2OtCRG7WlowHkDGVuL |
| | | AuMCAwEAAaNgMF4wHwYDVR0jBBgwFoAUzEvKR0Elhk6it4OonQyGA0e2EkowHQYD |
| | | VR0OBBYEFHyFYPxyk/7aYGwrS0S8Ai6uYgS/MAwGA1UdEwEB/wQCMAAwDgYDVR0P |
| | | AQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQBgPB5FZp8n6B3PNUuUV7HVbDrq |
| | | QAAYX18NQ7+vVOAnW6Yfisggqe8l3cC6ZLiGHpJ90bLmPTAiBr+tHJLLhJFi3e0w |
| | | WygplWlRdv3AbGEdQjMRAV86y0gx8LJZfLDzeG1K9HCj6p859Ugr3BbViFUz1YWh |
| | | rrFPnzsALBrem+J++2L834dFC+jFmSEWwBhRchRFEkJc7Iu0+dskOAlpIpJHw2X+ |
| | | 8xLIqhpPLbjWTv5LMWPrBXoH4dBvw6oTXLJLY0sn1OBZTobRJ+KRmCVZDC/Ircu/ |
| | | dABftIoCMQw53ilPNQCpP0Qe1ecAGZxxQXvdrdR+AVJPdDhCYfkXvj1NVS3h |
| | | -----END CERTIFICATE----- |
| New file |
| | |
| | | -----BEGIN RSA PRIVATE KEY----- |
| | | MIIEpQIBAAKCAQEA52ivF3noFj64moA2vkE37PP5d4kVd3ai7p7fSAbVREGiawxZ |
| | | m2zRuGp8b4bKuppnQoWLpnXKFuTQN9KKvl4XE+18blG+jTlX1GOyMatKHct55mdN |
| | | KBwYeKa3B/PkPvaPqe7EJ06ISly0tjKPGozCF7TbE5P4C5LzZDaQK6sKsGWzHLNG |
| | | HcconTFPb+WiEOA/UgNvEpNgkE6bWe9FGpoaYVpv4FEAiNPVjgbtyzYReYTbfKSQ |
| | | Q9npNJcyKPbSU31UMXd9RS4Ejkbutyr0d9K/WVDNXAX02ZsZ9E1ahsfCu9Psb13v |
| | | hRdaYV92ufOjBpZGHY60JEbtaWjAeQMZW4sC4wIDAQABAoIBAQCJWPbjZjW8Tknf |
| | | Wc4kKi15dG1S54hYOZAHNUCtTXDzbElsZA4jU/k+DeYBg+17x/0V3JHAoRTrda+o |
| | | EkzLJKlp6ID8MYR56dkZdHrlRBdfi8+0UwfWkKZtpfXowHdub4VhhRfjhJccG94e |
| | | be/GAFmLHIsTGbYVmIjhqAj2AjT78J/rA4Mvv9ng2qgXwb7WQgbauhpAZcG8cfYa |
| | | Z4sO4PJ8S4VSAxuoJYFY0A+3fYARfvxEO8jsrnqeRSGwjsWGPt9xzqbnJ/hcwFP9 |
| | | 4O7JYwJ8aqMEEGlx4FmcAt0qjz4E3qhNqPUMjyVpQcZObDoYFKOzHDHOsBxd+WXo |
| | | /+01U5IRAoGBAPQ4WRAmFs82IIG8B3OFX70TVd9qRwCqPi6bNysi60LX/0Bf7qTP |
| | | 6NAnD9rxmyJJ1PVmCgmcdLH8CnF7ntyXAx5QsFxJ/XHZQ5KEa7FDo0aTFm4GqduR |
| | | MHBPEy5mlKgum72soFpbGG9mH4mXXOhR0xkYuRQUg+i13cmeX5HLBczdAoGBAPKS |
| | | JHCtf1jidjch71QtRze56dOPZQOgmAJqNEf+lIVXRbhHCCTQED+UNYAhDp+jUFpR |
| | | yE+oR/xx3+11XCHFjpaXssL2OMAPjee1JKm9F18woJjc2LMPW6XCk6hHjZ6Ltued |
| | | VlSQplwjTxewR+81tj0KGagmmXhcXkcku3iTxDK/AoGBAKOIB7tUhfmCmQnGSocE |
| | | TDNjeyD7HUhItxKmRK7R1w8Pa5BDrJ0XyyF2xpspJWQ0ZDFefmIpLcrwpl2PFbVI |
| | | OYJXLYDe2qMdhK3blfFBBVgArghG1f58nh7WFFYBwpFLhGXh7g4S6a3OiFetzzyR |
| | | bfVkJKpZgmqVPUoAjqYleGDRAoGBAJZVvqHa1UcYK13l+Tb5TN8bqPBGObuyxyMQ |
| | | AVDxVckCGqKn20M9dCSDTVkYo8CKbd1cPEIqMFsjlD3N84i2sLViVRcBlJBr023c |
| | | VVmhaJ/FOnMixGbNSOaFng+4MOwm+Pe5Cm0krQYDmBw9U4fMiSJxZQ9SxODUllWP |
| | | TWTgZ8NvAoGAKmveOulE1ykt26FLfemjEuZIHCT+BBhILHHBRWKW4uT2YBdreiCZ |
| | | DMDN2suu7gGf30grmE/DoFUfu1m7Ser15pwf/AMkZgTErLF8xB0hoYWbSqIGOBUa |
| | | uiLjUKVT6O7Q+YY5wiKv7p/BQaF+jmXkW4kwhE1L7dueEkl0zFPUguk= |
| | | -----END RSA PRIVATE KEY----- |
| New file |
| | |
| | | -----BEGIN PUBLIC KEY----- |
| | | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA52ivF3noFj64moA2vkE3 |
| | | 7PP5d4kVd3ai7p7fSAbVREGiawxZm2zRuGp8b4bKuppnQoWLpnXKFuTQN9KKvl4X |
| | | E+18blG+jTlX1GOyMatKHct55mdNKBwYeKa3B/PkPvaPqe7EJ06ISly0tjKPGozC |
| | | F7TbE5P4C5LzZDaQK6sKsGWzHLNGHcconTFPb+WiEOA/UgNvEpNgkE6bWe9FGpoa |
| | | YVpv4FEAiNPVjgbtyzYReYTbfKSQQ9npNJcyKPbSU31UMXd9RS4Ejkbutyr0d9K/ |
| | | WVDNXAX02ZsZ9E1ahsfCu9Psb13vhRdaYV92ufOjBpZGHY60JEbtaWjAeQMZW4sC |
| | | 4wIDAQAB |
| | | -----END PUBLIC KEY----- |
| New file |
| | |
| | | -- IoT MQTT:sys_config 编码说明(无则走 application.yml 的 iot.enabled / topics) |
| | | -- iot.mqtt.enabled 值 true/false,status=1 生效 |
| | | -- iot.topic.egress_stow / egress_pick / egress_feedback / ingress_stow / ingress_pick / ingress_feedback |
| | | |
| | | IF NOT EXISTS (SELECT 1 FROM sys_config WHERE code = N'iot.mqtt.enabled') |
| | | BEGIN |
| | | INSERT INTO sys_config (name, code, value, type, status) |
| | | VALUES (N'IoT MQTT 总开关', N'iot.mqtt.enabled', N'false', 1, 1); |
| | | END; |
| New file |
| | |
| | | var pageCurr; |
| | | var tableIns; |
| | | |
| | | function escHtml(s) { |
| | | if (s == null || s === '') { |
| | | return ''; |
| | | } |
| | | return $('<div/>').text(String(s)).html(); |
| | | } |
| | | |
| | | function payloadPreview(s) { |
| | | var t = s == null ? '' : String(s); |
| | | if (t.length <= 100) { |
| | | return escHtml(t); |
| | | } |
| | | return escHtml(t.substring(0, 100)) + '…'; |
| | | } |
| | | |
| | | layui.use(['table', 'form'], function () { |
| | | var table = layui.table; |
| | | var $ = layui.jquery; |
| | | var layer = layui.layer; |
| | | var form = layui.form; |
| | | |
| | | function loadIotMqttConfig() { |
| | | $.ajax({ |
| | | url: baseUrl + '/iotMqttAdmin/config/auth', |
| | | headers: {token: localStorage.getItem('token')}, |
| | | dataType: 'json', |
| | | success: function (res) { |
| | | if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | return; |
| | | } |
| | | if (res.code !== 200 || !res.data) { |
| | | return; |
| | | } |
| | | var d = res.data; |
| | | var tp = d.topics || {}; |
| | | form.val('iotMqttCfgForm', { |
| | | enabled: d.enabled ? 'on' : '', |
| | | egressStow: tp.egressStow || '', |
| | | egressPick: tp.egressPick || '', |
| | | egressFeedback: tp.egressFeedback || '', |
| | | ingressStow: tp.ingressStow || '', |
| | | ingressPick: tp.ingressPick || '', |
| | | ingressFeedback: tp.ingressFeedback || '' |
| | | }); |
| | | $('#iotMqttConnState').text(d.mqttConnected ? '当前:已连接' : '当前:未连接'); |
| | | form.render(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | loadIotMqttConfig(); |
| | | $('#iotMqttReloadCfg').on('click', function () { |
| | | loadIotMqttConfig(); |
| | | }); |
| | | $('#iotMqttReconnect').on('click', function () { |
| | | $.ajax({ |
| | | url: baseUrl + '/iotMqttAdmin/reconnect/auth', |
| | | headers: {token: localStorage.getItem('token')}, |
| | | type: 'POST', |
| | | dataType: 'json', |
| | | success: function (res) { |
| | | if (res.code === 200) { |
| | | layer.msg('已执行重连'); |
| | | loadIotMqttConfig(); |
| | | } else { |
| | | layer.msg(res.msg || '重连失败', {icon: 2}); |
| | | } |
| | | }, |
| | | error: function () { |
| | | layer.msg('请求失败', {icon: 2}); |
| | | } |
| | | }); |
| | | }); |
| | | form.on('submit(iotMqttSave)', function () { |
| | | var v = form.val('iotMqttCfgForm'); |
| | | var payload = { |
| | | enabled: v.enabled === 'on', |
| | | topics: { |
| | | egressStow: v.egressStow || '', |
| | | egressPick: v.egressPick || '', |
| | | egressFeedback: v.egressFeedback || '', |
| | | ingressStow: v.ingressStow || '', |
| | | ingressPick: v.ingressPick || '', |
| | | ingressFeedback: v.ingressFeedback || '' |
| | | } |
| | | }; |
| | | $.ajax({ |
| | | url: baseUrl + '/iotMqttAdmin/config/save/auth', |
| | | headers: {token: localStorage.getItem('token'), 'Content-Type': 'application/json'}, |
| | | type: 'POST', |
| | | data: JSON.stringify(payload), |
| | | dataType: 'json', |
| | | success: function (res) { |
| | | if (res.code === 200) { |
| | | layer.msg('已保存,请点「重连 MQTT」使订阅生效'); |
| | | loadIotMqttConfig(); |
| | | } else { |
| | | layer.msg(res.msg || '保存失败', {icon: 2}); |
| | | } |
| | | }, |
| | | error: function () { |
| | | layer.msg('请求失败', {icon: 2}); |
| | | } |
| | | }); |
| | | return false; |
| | | }); |
| | | |
| | | tableIns = table.render({ |
| | | elem: '#iotMqttOutbound', |
| | | headers: {token: localStorage.getItem('token')}, |
| | | url: baseUrl + '/iotMqttOutbound/list/auth', |
| | | page: true, |
| | | limit: 16, |
| | | limits: [16, 30, 50, 100], |
| | | cellMinWidth: 50, |
| | | cols: [[ |
| | | {field: 'id', title: 'ID', width: 80, sort: true, align: 'center'} |
| | | , {field: 'instructionId', title: 'instructionId', align: 'center', width: 200} |
| | | , {field: 'messageType', title: '类型', align: 'center', width: 100} |
| | | , {field: 'publishTopic', title: '发送主题', align: 'center', minWidth: 200} |
| | | , { |
| | | field: 'publishPayload', |
| | | title: '发送消息体(摘要)', |
| | | align: 'left', |
| | | minWidth: 280, |
| | | templet: function (d) { |
| | | return payloadPreview(d.publishPayload); |
| | | } |
| | | } |
| | | , {field: 'publishStatus', title: '发布状态', align: 'center', width: 120} |
| | | , {field: 'errorMessage', title: '错误信息', align: 'center', minWidth: 160} |
| | | , {field: 'createTime', title: '创建时间', align: 'center', width: 170} |
| | | , {field: 'updateTime', title: '更新时间', align: 'center', width: 170} |
| | | , {fixed: 'right', title: '操作', align: 'center', toolbar: '#iotMqttOutboundOperate', width: 150} |
| | | ]], |
| | | request: { |
| | | pageName: 'curr', |
| | | pageSize: 'limit' |
| | | }, |
| | | parseData: function (res) { |
| | | return { |
| | | 'code': res.code, |
| | | 'msg': res.msg, |
| | | 'count': res.data.total, |
| | | 'data': res.data.records |
| | | }; |
| | | }, |
| | | response: { |
| | | statusCode: 200 |
| | | }, |
| | | done: function (res, curr) { |
| | | if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } |
| | | pageCurr = curr; |
| | | if (typeof limit === 'function') { |
| | | limit(); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | form.on('submit(search)', function () { |
| | | var searchData = {}; |
| | | $.each($('#search-box [name]').serializeArray(), function () { |
| | | searchData[this.name] = this.value; |
| | | }); |
| | | tableIns.reload({ |
| | | where: searchData, |
| | | page: {curr: 1}, |
| | | done: function (res, curr) { |
| | | if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } |
| | | pageCurr = curr; |
| | | if (typeof limit === 'function') { |
| | | limit(); |
| | | } |
| | | } |
| | | }); |
| | | return false; |
| | | }); |
| | | |
| | | form.on('submit(reset)', function () { |
| | | $('#search-box [name]').val(''); |
| | | form.render(); |
| | | tableIns.reload({ |
| | | where: {}, |
| | | page: {curr: 1} |
| | | }); |
| | | return false; |
| | | }); |
| | | |
| | | table.on('tool(iotMqttOutbound)', function (obj) { |
| | | var data = obj.data; |
| | | if (obj.event === 'payload') { |
| | | layer.open({ |
| | | type: 1, |
| | | title: '发送消息体', |
| | | area: ['720px', '520px'], |
| | | shadeClose: true, |
| | | content: '<div style="padding:12px;"><pre style="white-space:pre-wrap;word-break:break-all;margin:0;">' |
| | | + escHtml(data.publishPayload) + '</pre></div>' |
| | | }); |
| | | } else if (obj.event === 'resend') { |
| | | layer.confirm('确定重发该条 MQTT?', function (index) { |
| | | $.ajax({ |
| | | url: baseUrl + '/iotMqttOutbound/resend/auth', |
| | | headers: {token: localStorage.getItem('token')}, |
| | | type: 'POST', |
| | | data: {id: data.id}, |
| | | dataType: 'json', |
| | | success: function (res) { |
| | | layer.close(index); |
| | | if (res.code === 200) { |
| | | layer.msg('重发成功'); |
| | | tableIns.reload({page: {curr: pageCurr || 1}}); |
| | | } else { |
| | | layer.msg(res.msg || '重发失败', {icon: 2}); |
| | | } |
| | | }, |
| | | error: function () { |
| | | layer.close(index); |
| | | layer.msg('请求失败', {icon: 2}); |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="en"> |
| | | <head> |
| | | <meta charset="utf-8"> |
| | | <title>IoT MQTT 发送记录</title> |
| | | <meta name="renderer" content="webkit"> |
| | | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |
| | | <link rel="stylesheet" href="../../static/layui/css/layui.css" media="all"> |
| | | <link rel="stylesheet" href="../../static/css/cool.css" media="all"> |
| | | <link rel="stylesheet" href="../../static/css/common.css" media="all"> |
| | | </head> |
| | | <body> |
| | | |
| | | <div class="layui-card" style="margin:8px;"> |
| | | <div class="layui-card-header">MQTT 配置(sys_config,改 topic/开关后请先保存再点重连)</div> |
| | | <div class="layui-card-body layui-form" lay-filter="iotMqttCfgForm"> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-form-label">总开关</label> |
| | | <div class="layui-input-block"> |
| | | <input type="checkbox" name="enabled" lay-skin="switch" lay-text="开|关"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-form-label">egressStow</label> |
| | | <div class="layui-input-block"> |
| | | <input type="text" name="egressStow" autocomplete="off" class="layui-input" placeholder="订阅:XBPS→WMS 组托"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-form-label">egressPick</label> |
| | | <div class="layui-input-block"> |
| | | <input type="text" name="egressPick" autocomplete="off" class="layui-input" placeholder="订阅:拣选指令"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-form-label">egressFeedback</label> |
| | | <div class="layui-input-block"> |
| | | <input type="text" name="egressFeedback" autocomplete="off" class="layui-input" placeholder="订阅:feedback"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-form-label">ingressStow</label> |
| | | <div class="layui-input-block"> |
| | | <input type="text" name="ingressStow" autocomplete="off" class="layui-input" placeholder="发布:入库完成"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-form-label">ingressPick</label> |
| | | <div class="layui-input-block"> |
| | | <input type="text" name="ingressPick" autocomplete="off" class="layui-input" placeholder="发布:出库完成"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <label class="layui-form-label">ingressFeedback</label> |
| | | <div class="layui-input-block"> |
| | | <input type="text" name="ingressFeedback" autocomplete="off" class="layui-input" placeholder="发布:feedback"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-form-item"> |
| | | <div class="layui-input-block"> |
| | | <button class="layui-btn layui-btn-primary" type="button" id="iotMqttReloadCfg">重新加载</button> |
| | | <button class="layui-btn" lay-submit lay-filter="iotMqttSave">保存</button> |
| | | <button type="button" class="layui-btn layui-btn-normal" id="iotMqttReconnect">重连 MQTT</button> |
| | | <span id="iotMqttConnState" style="margin-left:12px;color:#666;"></span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div id="search-box" class="layui-form layui-card-header"> |
| | | <div class="layui-inline"> |
| | | <div class="layui-input-inline"> |
| | | <input class="layui-input" type="text" name="instruction_id" placeholder="instructionId" autocomplete="off"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-inline"> |
| | | <div class="layui-input-inline"> |
| | | <input class="layui-input" type="text" name="publish_status" placeholder="发布状态 PENDING/SUCCESS/FAILURE..." autocomplete="off"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-inline"> |
| | | <div class="layui-input-inline"> |
| | | <input class="layui-input" type="text" name="publish_topic" placeholder="发送主题" autocomplete="off"> |
| | | </div> |
| | | </div> |
| | | <div class="layui-inline"> |
| | | <div class="layui-input-inline"> |
| | | <input class="layui-input" type="text" name="container_id" placeholder="容器号" autocomplete="off"> |
| | | </div> |
| | | </div> |
| | | <div id="data-search-btn" class="layui-btn-container layui-form-item" style="display: inline-block"> |
| | | <button id="search" class="layui-btn layui-btn-primary layui-btn-radius" lay-submit lay-filter="search">搜索</button> |
| | | <button id="reset" class="layui-btn layui-btn-primary layui-btn-radius" lay-submit lay-filter="reset">重置</button> |
| | | </div> |
| | | </div> |
| | | |
| | | <table class="layui-hide" id="iotMqttOutbound" lay-filter="iotMqttOutbound"></table> |
| | | |
| | | <script type="text/html" id="iotMqttOutboundOperate"> |
| | | <a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">消息体</a> |
| | | <a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="resend">重发</a> |
| | | </script> |
| | | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/js/iotMqttOutbound/iotMqttOutbound.js" charset="utf-8"></script> |
| | | |
| | | </body> |
| | | </html> |
| | |
| | | properties.setPort(8883); |
| | | properties.setThingName("thing-a"); |
| | | |
| | | Assertions.assertEquals("ssl://example-ats.iot.cn-north-1.amazonaws.com.cn:8883", properties.getServerUri()); |
| | | Assertions.assertEquals("thing-a", properties.getResolvedClientId()); |
| | | Assertions.assertEquals("mqtts://example-ats.iot.cn-north-1.amazonaws.com.cn:8883", properties.getServerUri()); |
| | | String thingId = properties.getResolvedClientId(); |
| | | Assertions.assertNotNull(thingId); |
| | | Assertions.assertTrue(thingId.startsWith("thing-a-")); |
| | | Assertions.assertEquals(thingId, properties.getResolvedClientId()); |
| | | |
| | | properties.setClientId("client-b"); |
| | | Assertions.assertEquals("client-b", properties.getResolvedClientId()); |
| | | IotProperties withClient = new IotProperties(); |
| | | withClient.setClientId("client-b"); |
| | | String cid = withClient.getResolvedClientId(); |
| | | Assertions.assertNotNull(cid); |
| | | Assertions.assertTrue(cid.startsWith("client-b-")); |
| | | Assertions.assertEquals(cid, withClient.getResolvedClientId()); |
| | | } |
| | | |
| | | @Test |