| @ -0,0 +1,35 @@ | |||||
| package com.inspect.api.config; | |||||
| import com.inspect.api.constant.YanyuanConstants; | |||||
| import org.springframework.amqp.core.Binding; | |||||
| import org.springframework.amqp.core.BindingBuilder; | |||||
| import org.springframework.amqp.core.Queue; | |||||
| import org.springframework.amqp.core.TopicExchange; | |||||
| import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; | |||||
| import org.springframework.amqp.support.converter.MessageConverter; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.context.annotation.Configuration; | |||||
| @Configuration | |||||
| public class RabbitMQConfig { | |||||
| @Bean | |||||
| public TopicExchange alarmExchange() { | |||||
| return new TopicExchange(YanyuanConstants.EXCHANGE, true, false); | |||||
| } | |||||
| @Bean("alarmQueue") | |||||
| public Queue alarmQueue() { | |||||
| return new Queue(YanyuanConstants.QUEUE, true); | |||||
| } | |||||
| @Bean | |||||
| public Binding alarmBinding() { | |||||
| return BindingBuilder.bind(alarmQueue()).to(alarmExchange()).with(YanyuanConstants.EXCHANGE); | |||||
| } | |||||
| @Bean | |||||
| public MessageConverter messageConverter() { | |||||
| return new Jackson2JsonMessageConverter(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,10 @@ | |||||
| package com.inspect.api.constant; | |||||
| public class YanyuanConstants { | |||||
| private YanyuanConstants() {} | |||||
| public static final String EXCHANGE = "alarm.exchange"; | |||||
| public static final String QUEUE = "alarm.queue"; | |||||
| public static final String ROUTING_KEY = "alarm.routing-key"; | |||||
| } | |||||
| @ -0,0 +1,62 @@ | |||||
| package com.inspect.api.controller; | |||||
| import com.inspect.api.domain.AlarmMessage; | |||||
| import com.inspect.api.domain.ApiResult; | |||||
| import com.inspect.api.domain.AuthTokenData; | |||||
| import com.inspect.api.enums.AlarmCategory; | |||||
| import com.inspect.api.enums.MessageType; | |||||
| import com.inspect.api.enums.SystemCode; | |||||
| import com.inspect.api.service.AlarmPublisher; | |||||
| import com.inspect.api.service.ExtAuthService; | |||||
| import com.inspect.base.core.web.controller.BaseController; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import com.inspect.base.core.web.page.TableDataInfo; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.web.bind.annotation.*; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| @Slf4j | |||||
| @RestController | |||||
| @RequestMapping("/v1/api/alarm") | |||||
| public class AlarmController extends BaseController { | |||||
| private final ExtAuthService extAuthService; | |||||
| private final AlarmPublisher alarmPublisher; | |||||
| public AlarmController(ExtAuthService extAuthService, AlarmPublisher alarmPublisher) { | |||||
| this.extAuthService = extAuthService; | |||||
| this.alarmPublisher = alarmPublisher; | |||||
| } | |||||
| @PostMapping("/history") | |||||
| @ResponseBody | |||||
| public TableDataInfo list(AlarmMessage alarmMessage) { | |||||
| startPage(); | |||||
| List<AlarmMessage> list = new ArrayList<>(); | |||||
| list.add(AlarmMessage.builder() | |||||
| .messageType(MessageType.ALARM.getCode()) | |||||
| .alarmCategory(AlarmCategory.APPEARANCE_DEFECT.getCode()) | |||||
| .systemCode(SystemCode.PATROL.getCode()) | |||||
| .build()); | |||||
| // List<AlarmMessage> list = jobLogService.selectJobLogList(jobLog); | |||||
| return getDataTable(list); | |||||
| } | |||||
| @GetMapping("/getToken") | |||||
| public AjaxResult getToken(String clientId, String clientSecret) { | |||||
| ApiResult<AuthTokenData> result = extAuthService.getAuthToken(clientId, clientSecret); | |||||
| log.info("Result={}", result); | |||||
| return AjaxResult.success(result.getData()); | |||||
| } | |||||
| @PostMapping("/push") | |||||
| @ResponseBody | |||||
| public AjaxResult push(AlarmMessage msg) { | |||||
| if (msg != null) { | |||||
| alarmPublisher.publish(msg); | |||||
| return AjaxResult.success(msg); | |||||
| } | |||||
| return AjaxResult.error(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,17 @@ | |||||
| package com.inspect.api.domain; | |||||
| import com.inspect.base.core.web.domain.BaseEntity; | |||||
| import lombok.*; | |||||
| @ToString | |||||
| @Data | |||||
| @Builder | |||||
| public class AlarmMessage extends BaseEntity { | |||||
| private String messageId; | |||||
| private String messageType; | |||||
| // 告警类型: messageType = alarm_push时传入 | |||||
| private String alarmCategory; | |||||
| private String timestamp; | |||||
| private String systemCode; | |||||
| private Object data; | |||||
| } | |||||
| @ -0,0 +1,12 @@ | |||||
| package com.inspect.api.domain; | |||||
| import lombok.Data; | |||||
| @Data | |||||
| public class ApiResult<T> { | |||||
| private boolean success; | |||||
| private T data; | |||||
| private String hintMessage; | |||||
| private String type; | |||||
| private int code; | |||||
| } | |||||
| @ -0,0 +1,12 @@ | |||||
| package com.inspect.api.domain; | |||||
| import lombok.Data; | |||||
| @Data | |||||
| public class AuthTokenData { | |||||
| private String access_token; | |||||
| private String token_type; | |||||
| private String scope; | |||||
| private String userId; | |||||
| private String jti; | |||||
| } | |||||
| @ -0,0 +1,17 @@ | |||||
| package com.inspect.api.enums; | |||||
| import lombok.AllArgsConstructor; | |||||
| import lombok.Getter; | |||||
| @Getter | |||||
| @AllArgsConstructor | |||||
| public enum AlarmCategory { | |||||
| FIRE_SMOKE("fire_smoke", "烟火报警"), | |||||
| NO_HELMET("no_helmet", "未佩戴安全帽报警"), | |||||
| NO_UNIFORM("no_uniform", "未穿工装报警"), | |||||
| METER_OVERRUN("meter_overrun", "红外温度超限报警"), | |||||
| APPEARANCE_DEFECT("appearance_defect", "外观缺陷告警"); | |||||
| final private String code; | |||||
| final private String value; | |||||
| } | |||||
| @ -0,0 +1,14 @@ | |||||
| package com.inspect.api.enums; | |||||
| import lombok.AllArgsConstructor; | |||||
| import lombok.Getter; | |||||
| @Getter | |||||
| @AllArgsConstructor | |||||
| public enum MessageType { | |||||
| ALARM("alarm_push", "告警推送"), | |||||
| REPORT("report_push", "报告推送"); | |||||
| final private String code; | |||||
| final private String value; | |||||
| } | |||||
| @ -0,0 +1,15 @@ | |||||
| package com.inspect.api.enums; | |||||
| import lombok.AllArgsConstructor; | |||||
| import lombok.Getter; | |||||
| @Getter | |||||
| @AllArgsConstructor | |||||
| public enum SystemCode { | |||||
| PE("power&environment", "动环"), | |||||
| FIREFIGHTING("firefighting", "消防"), | |||||
| PATROL("patrol", "巡检"); | |||||
| final private String code; | |||||
| final private String value; | |||||
| } | |||||
| @ -0,0 +1,15 @@ | |||||
| package com.inspect.api.props; | |||||
| import lombok.Data; | |||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
| import org.springframework.stereotype.Component; | |||||
| @Data | |||||
| @Component | |||||
| @ConfigurationProperties(prefix = "yanyuan.auth") | |||||
| public class AppProperties { | |||||
| private String baseUrl; | |||||
| private String wsUrl; | |||||
| private String clientId; | |||||
| private String clientSecret; | |||||
| } | |||||
| @ -0,0 +1,37 @@ | |||||
| package com.inspect.api.service; | |||||
| import com.fasterxml.jackson.databind.ObjectMapper; | |||||
| import com.inspect.api.domain.AlarmMessage; | |||||
| import com.inspect.api.websocket.AlarmWsConnection; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.amqp.rabbit.annotation.RabbitListener; | |||||
| import org.springframework.stereotype.Component; | |||||
| @Slf4j | |||||
| @Component | |||||
| public class AlarmConsumer { | |||||
| private final AlarmWsConnection wsConnection; | |||||
| private final ObjectMapper objectMapper; | |||||
| public AlarmConsumer(AlarmWsConnection wsConnection, ObjectMapper objectMapper) { | |||||
| this.wsConnection = wsConnection; | |||||
| this.objectMapper = objectMapper; | |||||
| } | |||||
| @RabbitListener(queues = "#{alarmQueue.name}") | |||||
| public void onAlarm(AlarmMessage msg) { | |||||
| try { | |||||
| String json = objectMapper.writeValueAsString(msg); | |||||
| boolean sent = wsConnection.send(json); | |||||
| if (sent) { | |||||
| log.info("Alarm sent to WS: msg={}", msg); | |||||
| } else { | |||||
| log.warn("Alarm not sent, WS not connected: msg={}", msg); | |||||
| throw new RuntimeException("WS not connected, message will be requeued"); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| log.error("Alarm consumer error: {}", e.getMessage()); | |||||
| throw new RuntimeException("Alarm processing failed", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,26 @@ | |||||
| package com.inspect.api.service; | |||||
| import com.inspect.api.constant.YanyuanConstants; | |||||
| import com.inspect.api.domain.AlarmMessage; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.amqp.rabbit.core.RabbitTemplate; | |||||
| import org.springframework.stereotype.Service; | |||||
| @Slf4j | |||||
| @Service | |||||
| public class AlarmPublisher { | |||||
| private final RabbitTemplate rabbitTemplate; | |||||
| public AlarmPublisher(RabbitTemplate rabbitTemplate) { | |||||
| this.rabbitTemplate = rabbitTemplate; | |||||
| } | |||||
| public void publish(AlarmMessage msg) { | |||||
| try { | |||||
| rabbitTemplate.convertAndSend(YanyuanConstants.EXCHANGE, YanyuanConstants.ROUTING_KEY, msg); | |||||
| log.info("Alert published: msg={}", msg); | |||||
| } catch (Exception e) { | |||||
| log.error("Failed to publish alert: {}", e.getMessage()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,41 @@ | |||||
| package com.inspect.api.service; | |||||
| import com.fasterxml.jackson.core.type.TypeReference; | |||||
| import com.fasterxml.jackson.databind.ObjectMapper; | |||||
| import com.inspect.api.domain.ApiResult; | |||||
| import com.inspect.api.domain.AuthTokenData; | |||||
| import com.inspect.api.props.AppProperties; | |||||
| import com.inspect.base.core.utils.HttpClientUtils; | |||||
| import org.springframework.stereotype.Service; | |||||
| @Service | |||||
| public class ExtAuthService { | |||||
| private static final String TOKEN_HEADER = "joinbright-token"; | |||||
| private final ObjectMapper objectMapper; | |||||
| private final AppProperties appProperties; | |||||
| public ExtAuthService(ObjectMapper objectMapper, AppProperties appProperties) { | |||||
| this.objectMapper = objectMapper; | |||||
| this.appProperties = appProperties; | |||||
| } | |||||
| public ApiResult<AuthTokenData> getAuthToken() { | |||||
| return getAuthToken(appProperties.getClientId(), appProperties.getClientSecret()); | |||||
| } | |||||
| public ApiResult<AuthTokenData> getAuthToken(String clientId, String clientSecret) { | |||||
| String url = appProperties.getBaseUrl() + "/ext_auth/getAuthToken"; | |||||
| String param = "clientId=" + clientId + "&clientSecret=" + clientSecret; | |||||
| return executeGet(url, param, new TypeReference<ApiResult<AuthTokenData>>() {}); | |||||
| } | |||||
| private <T> ApiResult<T> executeGet(String url, String param, TypeReference<ApiResult<T>> typeRef) { | |||||
| try { | |||||
| String json = HttpClientUtils.get(url, param); | |||||
| return objectMapper.readValue(json, typeRef); | |||||
| } catch (Exception e) { | |||||
| throw new RuntimeException("HTTP GET failed: " + url, e); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,139 @@ | |||||
| package com.inspect.api.websocket; | |||||
| import com.inspect.api.domain.ApiResult; | |||||
| import com.inspect.api.domain.AuthTokenData; | |||||
| import com.inspect.api.props.AppProperties; | |||||
| import com.inspect.api.service.ExtAuthService; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.scheduling.annotation.Scheduled; | |||||
| import org.springframework.stereotype.Component; | |||||
| import org.springframework.web.socket.CloseStatus; | |||||
| import org.springframework.web.socket.TextMessage; | |||||
| import org.springframework.web.socket.WebSocketSession; | |||||
| import org.springframework.web.socket.client.standard.StandardWebSocketClient; | |||||
| import org.springframework.web.socket.handler.TextWebSocketHandler; | |||||
| import javax.annotation.PostConstruct; | |||||
| import java.util.concurrent.atomic.AtomicInteger; | |||||
| import java.util.concurrent.atomic.AtomicReference; | |||||
| @Slf4j | |||||
| @Component | |||||
| public class AlarmWsConnection { | |||||
| private final ExtAuthService extAuthService; | |||||
| private final AppProperties appProperties; | |||||
| private final StandardWebSocketClient wsClient; | |||||
| private final AtomicReference<WebSocketSession> sessionRef = new AtomicReference<>(); | |||||
| private final AtomicInteger reconnectBackoff = new AtomicInteger(5); | |||||
| private volatile boolean connecting = false; | |||||
| public AlarmWsConnection(ExtAuthService extAuthService, AppProperties appProperties) { | |||||
| this.extAuthService = extAuthService; | |||||
| this.appProperties = appProperties; | |||||
| this.wsClient = new StandardWebSocketClient(); | |||||
| } | |||||
| @PostConstruct | |||||
| public void init() { | |||||
| connect(); | |||||
| } | |||||
| public void connect() { | |||||
| if (connecting) { | |||||
| return; | |||||
| } | |||||
| connecting = true; | |||||
| try { | |||||
| ApiResult<AuthTokenData> result = extAuthService.getAuthToken(); | |||||
| if (!result.isSuccess() || result.getData() == null) { | |||||
| log.error("Alarm WS: failed to get auth token, retry in {}s", reconnectBackoff.get()); | |||||
| connecting = false; | |||||
| return; | |||||
| } | |||||
| String token = result.getData().getAccess_token(); | |||||
| String wsUrl = appProperties.getWsUrl() + "?Joinbright-Token=" + token; | |||||
| wsClient.doHandshake(new AlarmWsHandler(), wsUrl) | |||||
| .addCallback( | |||||
| session -> { | |||||
| sessionRef.set(session); | |||||
| reconnectBackoff.set(5); | |||||
| connecting = false; | |||||
| log.info("Alarm WS connected: {}", session.getId()); | |||||
| }, | |||||
| ex -> { | |||||
| connecting = false; | |||||
| log.error("Alarm WS handshake failed: {}", ex.getMessage()); | |||||
| } | |||||
| ); | |||||
| } catch (Exception e) { | |||||
| connecting = false; | |||||
| log.error("Alarm WS connect error: {}", e.getMessage()); | |||||
| } | |||||
| } | |||||
| @Scheduled(fixedDelay = 1000) | |||||
| public void reconnect() { | |||||
| WebSocketSession session = sessionRef.get(); | |||||
| if (session != null && session.isOpen()) { | |||||
| return; | |||||
| } | |||||
| if (connecting) { | |||||
| return; | |||||
| } | |||||
| int delay = reconnectBackoff.get(); | |||||
| log.info("Alarm WS reconnecting in {}s", delay); | |||||
| try { | |||||
| Thread.sleep(delay * 1000L); | |||||
| } catch (InterruptedException ignored) { | |||||
| Thread.currentThread().interrupt(); | |||||
| return; | |||||
| } | |||||
| int nextDelay = Math.min(delay * 2, 60); | |||||
| reconnectBackoff.set(nextDelay); | |||||
| connect(); | |||||
| } | |||||
| public boolean send(String message) { | |||||
| WebSocketSession session = sessionRef.get(); | |||||
| if (session == null || !session.isOpen()) { | |||||
| log.warn("Alarm WS not connected, message dropped: {}", message); | |||||
| return false; | |||||
| } | |||||
| try { | |||||
| synchronized (session) { | |||||
| session.sendMessage(new TextMessage(message)); | |||||
| } | |||||
| return true; | |||||
| } catch (Exception e) { | |||||
| log.error("Alarm WS send failed: {}", e.getMessage()); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public boolean isConnected() { | |||||
| WebSocketSession session = sessionRef.get(); | |||||
| return session != null && session.isOpen(); | |||||
| } | |||||
| private class AlarmWsHandler extends TextWebSocketHandler { | |||||
| @Override | |||||
| public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { | |||||
| log.warn("Alarm WS closed: {}", status); | |||||
| sessionRef.compareAndSet(session, null); | |||||
| } | |||||
| @Override | |||||
| protected void handleTextMessage(WebSocketSession session, TextMessage message) { | |||||
| log.info("Alarm WS received: {}", message.getPayload()); | |||||
| } | |||||
| @Override | |||||
| public void handleTransportError(WebSocketSession session, Throwable ex) { | |||||
| log.error("Alarm WS transport error: {}", ex.getMessage()); | |||||
| sessionRef.compareAndSet(session, null); | |||||
| } | |||||
| } | |||||
| } | |||||