| @ -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); | |||
| } | |||
| } | |||
| } | |||