diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 00000000..fa24890c --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,59 @@ +import { createRouter, createWebHistory } from 'vue-router' +import OperationDashboard from '@/views/dashboard/OperationDashboard.vue' +import WaterSupplySpecialScreen from '@/views/dashboard/WaterSupplySpecialScreen.vue' + +const routes = [ + { + path: '/', + redirect: '/dashboard/operation' + }, + { + path: '/dashboard', + redirect: '/dashboard/operation' + }, + { + path: '/dashboard/operation', + name: 'OperationDashboard', + component: OperationDashboard, + meta: { + title: '供水运营总览', + requiresAuth: true + } + }, + { + path: '/dashboard/water-supply', + name: 'WaterSupplySpecialScreen', + component: WaterSupplySpecialScreen, + meta: { + title: '供水专题大屏', + requiresAuth: true + } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 全局路由守卫 +router.beforeEach((to, from, next) => { + // 设置页面标题 + document.title = to.meta.title ? `${to.meta.title} - 供水管理系统` : '供水管理系统' + + // 这里可以添加认证逻辑 + if (to.meta.requiresAuth) { + // 检查用户是否已登录 + const token = localStorage.getItem('token') + if (token) { + next() + } else { + // 重定向到登录页面 + next('/login') + } + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js new file mode 100644 index 00000000..e8e18f6d --- /dev/null +++ b/frontend/src/utils/websocket.js @@ -0,0 +1,246 @@ +import { ElMessage } from 'element-plus' + +/** + * WebSocket服务类 + */ +class WebSocketService { + constructor() { + this.ws = null + this.isConnected = false + this.reconnectAttempts = 0 + this.maxReconnectAttempts = 5 + this.reconnectInterval = 3000 + this.messageHandlers = new Map() + this.heartbeatInterval = null + } + + /** + * 连接WebSocket + * @param {string} url - WebSocket地址 + * @param {Object} options - 连接选项 + */ + connect(url = null, options = {}) { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = url || `${protocol}//${window.location.host}/ws/water-data` + + try { + this.ws = new WebSocket(wsUrl) + + this.ws.onopen = () => { + console.log('WebSocket连接已建立') + this.isConnected = true + this.reconnectAttempts = 0 + + // 发送连接确认 + this.send({ + type: 'connection-status', + data: { status: 'connected' } + }) + + // 设置心跳检测 + this.startHeartbeat() + + // 连接成功回调 + this.emit('connected', {}) + + ElMessage.success('实时数据连接已建立') + } + + this.ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data) + this.handleMessage(message) + } catch (error) { + console.error('WebSocket消息解析错误:', error) + } + } + + this.ws.onclose = (event) => { + console.log('WebSocket连接已关闭:', event.code, event.reason) + this.isConnected = false + this.stopHeartbeat() + + // 连接关闭回调 + this.emit('disconnected', { code: event.code, reason: event.reason }) + + // 自动重连 + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++ + console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`) + + setTimeout(() => { + this.connect(wsUrl, options) + }, this.reconnectInterval) + } else { + ElMessage.error('实时数据连接失败,请刷新页面重试') + } + } + + this.ws.onerror = (error) => { + console.error('WebSocket连接错误:', error) + this.emit('error', error) + } + + } catch (error) { + console.error('WebSocket连接初始化失败:', error) + ElMessage.error('实时数据连接失败,将使用模拟数据') + } + } + + /** + * 发送消息 + * @param {Object} message - 消息对象 + */ + send(message) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)) + } else { + console.warn('WebSocket未连接,消息发送失败') + } + } + + /** + * 处理接收到的消息 + * @param {Object} message - 消息对象 + */ + handleMessage(message) { + const { type, data } = message + + // 调用对应的消息处理器 + if (this.messageHandlers.has(type)) { + const handlers = this.messageHandlers.get(type) + handlers.forEach(handler => { + try { + handler(data) + } catch (error) { + console.error(`消息处理器执行错误 [${type}]:`, error) + } + }) + } + + // 通用消息事件 + this.emit('message', { type, data }) + + // 更新实时数据 + this.updateRealTimeData(type, data) + } + + /** + * 更新实时数据 + * @param {string} type - 数据类型 + * @param {Object} data - 数据内容 + */ + updateRealTimeData(type, data) { + // 将数据存储到全局状态 + if (window.realTimeData) { + window.realTimeData[type] = { + data, + timestamp: new Date().toISOString() + } + } + } + + /** + * 订阅数据通道 + * @param {Array} channels - 数据通道列表 + */ + subscribe(channels) { + this.send({ + type: 'subscribe', + channels + }) + } + + /** + * 获取KPI数据 + */ + getKPI() { + this.send({ + type: 'get-kpi' + }) + } + + /** + * 设置消息处理器 + * @param {string} type - 消息类型 + * @param {Function} handler - 处理函数 + */ + on(type, handler) { + if (!this.messageHandlers.has(type)) { + this.messageHandlers.set(type, []) + } + this.messageHandlers.get(type).push(handler) + } + + /** + * 移除消息处理器 + * @param {string} type - 消息类型 + * @param {Function} handler - 处理函数 + */ + off(type, handler) { + if (this.messageHandlers.has(type)) { + const handlers = this.messageHandlers.get(type) + const index = handlers.indexOf(handler) + if (index > -1) { + handlers.splice(index, 1) + } + } + } + + /** + * 触发事件 + * @param {string} event - 事件名称 + * @param {Object} data - 事件数据 + */ + emit(event, data) { + // 这里可以使用事件总线系统 + if (window.eventBus) { + window.eventBus.emit(event, data) + } + } + + /** + * 开始心跳检测 + */ + startHeartbeat() { + this.heartbeatInterval = setInterval(() => { + if (this.isConnected) { + this.send({ type: 'heartbeat', timestamp: Date.now() }) + } + }, 30000) // 30秒发送一次心跳 + } + + /** + * 停止心跳检测 + */ + stopHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + this.heartbeatInterval = null + } + } + + /** + * 断开连接 + */ + disconnect() { + if (this.ws) { + this.ws.close() + this.ws = null + } + this.isConnected = false + this.stopHeartbeat() + this.messageHandlers.clear() + } +} + +// 创建WebSocket服务实例 +const websocketService = new WebSocketService() + +// 初始化全局数据 +if (typeof window !== 'undefined') { + window.websocketService = websocketService + window.realTimeData = {} +} + +export default websocketService \ No newline at end of file diff --git a/frontend/src/views/dashboard/OperationDashboard.vue b/frontend/src/views/dashboard/OperationDashboard.vue index 1c83c141..d5990f5c 100644 --- a/frontend/src/views/dashboard/OperationDashboard.vue +++ b/frontend/src/views/dashboard/OperationDashboard.vue @@ -140,6 +140,7 @@ diff --git a/frontend/src/views/dashboard/WaterSupplySpecialScreen.vue b/frontend/src/views/dashboard/WaterSupplySpecialScreen.vue new file mode 100644 index 00000000..84c84aad --- /dev/null +++ b/frontend/src/views/dashboard/WaterSupplySpecialScreen.vue @@ -0,0 +1,1459 @@ + + + + + \ No newline at end of file diff --git a/wm-bi/pom.xml b/wm-bi/pom.xml index 2e707090..add026e1 100644 --- a/wm-bi/pom.xml +++ b/wm-bi/pom.xml @@ -27,5 +27,7 @@ org.springframework.bootspring-boot-starter-quartz com.fasterxml.jackson.corejackson-databind + + org.springframework.bootspring-boot-starter-websocket \ No newline at end of file diff --git a/wm-bi/src/main/java/com/water/bi/config/WebSocketConfig.java b/wm-bi/src/main/java/com/water/bi/config/WebSocketConfig.java new file mode 100644 index 00000000..daae36d5 --- /dev/null +++ b/wm-bi/src/main/java/com/water/bi/config/WebSocketConfig.java @@ -0,0 +1,25 @@ +package com.water.bi.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import com.water.bi.service.impl.DataVisualizationServiceImpl; + +/** + * WebSocket配置类 + */ +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Autowired + private DataVisualizationServiceImpl.WebSocketDataHandler webSocketDataHandler; + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(webSocketDataHandler, "/ws/water-data") + .setAllowedOrigins("*"); + } +} \ No newline at end of file diff --git a/wm-bi/src/main/java/com/water/bi/controller/BIRestController.java b/wm-bi/src/main/java/com/water/bi/controller/BIRestController.java new file mode 100644 index 00000000..579b28ec --- /dev/null +++ b/wm-bi/src/main/java/com/water/bi/controller/BIRestController.java @@ -0,0 +1,92 @@ +package com.water.bi.controller; + +import com.water.bi.service.impl.DataVisualizationServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Map; + +/** + * BI数据可视化REST API控制器 + */ +@RestController +@RequestMapping("/api/bi") +@CrossOrigin(origins = "*") +public class BIRestController { + + @Autowired + private DataVisualizationServiceImpl dataVisualizationService; + + /** + * 获取当前KPI指标 + */ + @GetMapping("/kpi/current") + public Map getCurrentKPI() { + return Map.of( + "success", true, + "data", dataVisualizationService.getCurrentKPIData(), + "timestamp", System.currentTimeMillis() + ); + } + + /** + * 获取历史趋势数据 + */ + @GetMapping("/trends/{type}") + public Map getTrendData(@PathVariable String type) { + Map result = Map.of( + "success", true, + "type", type, + "data", dataVisualizationService.getRealTimeData().getOrDefault(type, Map.of()), + "timestamp", System.currentTimeMillis() + ); + return result; + } + + /** + * 获取实时报警统计 + */ + @GetMapping("/alarms/statistics") + public Map getAlarmStatistics() { + return Map.of( + "success", true, + "data", dataVisualizationService.getAlarmStatistics(), + "timestamp", System.currentTimeMillis() + ); + } + + /** + * 获取供水专题大屏数据 + */ + @GetMapping("/water-supply-screen") + public Map getWaterSupplyScreenData() { + return Map.of( + "success", true, + "data", dataVisualizationService.getWaterSupplyScreenData(), + "timestamp", System.currentTimeMillis() + ); + } + + /** + * 获取BI集成状态 + */ + @GetMapping("/integration/status") + public Map getBIIntegrationStatus() { + return Map.of( + "success", true, + "data", dataVisualizationService.getBIIntegrationStatus(), + "timestamp", System.currentTimeMillis() + ); + } + + /** + * 手动刷新实时数据 + */ + @PostMapping("/data/refresh") + public Map refreshData() { + return Map.of( + "success", true, + "message", "数据已刷新", + "timestamp", System.currentTimeMillis() + ); + } +} \ No newline at end of file diff --git a/wm-bi/src/main/java/com/water/bi/service/impl/DataVisualizationServiceImpl.java b/wm-bi/src/main/java/com/water/bi/service/impl/DataVisualizationServiceImpl.java index b41750b4..fbd46a56 100644 --- a/wm-bi/src/main/java/com/water/bi/service/impl/DataVisualizationServiceImpl.java +++ b/wm-bi/src/main/java/com/water/bi/service/impl/DataVisualizationServiceImpl.java @@ -4,15 +4,44 @@ import com.water.bi.service.DataVisualizationService; import com.water.bi.entity.BIDashboard; import com.water.bi.entity.DataVisualization; import org.springframework.stereotype.Service; -import java.util.*; -import java.util.stream.Collectors; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; /** * 数据可视化服务实现 */ @Service public class DataVisualizationServiceImpl implements DataVisualizationService { + + @Autowired + private BISupersetMetabaseService biSupersetMetabaseService; + + // WebSocket相关配置 + private final Map sessions = new ConcurrentHashMap<>(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // 实时数据缓存 + private final Map realTimeData = new ConcurrentHashMap<>(); + + // 模拟实时数据生成器 + private final Thread dataGeneratorThread; + private volatile boolean running = true; + + public DataVisualizationServiceImpl() { + // 启动实时数据生成线程 + this.dataGeneratorThread = new Thread(this::generateRealTimeData); + this.dataGeneratorThread.setDaemon(true); + this.dataGeneratorThread.start(); + } @Override public Long createDashboard(BIDashboard dashboard) { @@ -58,6 +87,404 @@ public class DataVisualizationServiceImpl implements DataVisualizationService { screen.setCreateTime(new Date()); return screen.getId(); } + + /** + * WebSocket处理器 - 处理实时数据连接 + */ + @Service + public static class WebSocketDataHandler extends TextWebSocketHandler { + @Autowired + private DataVisualizationServiceImpl dataVisualizationService; + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + String sessionId = session.getId(); + dataVisualizationService.sessions.put(sessionId, session); + + // 发送当前状态数据 + Map statusData = new HashMap<>(); + statusData.put("type", "connection-status"); + statusData.put("status", "connected"); + statusData.put("timestamp", LocalDateTime.now().format(formatter)); + + session.sendMessage(new TextMessage(objectMapper.writeValueAsString(statusData))); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String payload = message.getPayload(); + Map request = objectMapper.readValue(payload, Map.class); + + String type = (String) request.get("type"); + + if ("subscribe".equals(type)) { + // 处理数据订阅 + List channels = (List) request.get("channels"); + Map response = new HashMap<>(); + response.put("type", "subscription-confirmed"); + response.put("channels", channels); + response.put("timestamp", LocalDateTime.now().format(formatter)); + + session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); + } else if ("get-kpi".equals(type)) { + // 返回KPI数据 + Map kpiData = dataVisualizationService.getCurrentKPIData(); + kpiData.put("type", "kpi-update"); + + session.sendMessage(new TextMessage(objectMapper.writeValueAsString(kpiData))); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + String sessionId = session.getId(); + dataVisualizationService.sessions.remove(sessionId); + } + } + + /** + * 获取当前KPI数据 + */ + public Map getCurrentKPIData() { + Map kpiData = new HashMap<>(); + + // 模拟实时KPI数据 + kpiData.put("supplyTotal", 12580 + (int)(Math.random() * 1000 - 500)); + kpiData.put("waterOutput", 11230 + (int)(Math.random() * 800 - 400)); + kpiData.put("productionLossRate", 10.8 + (Math.random() - 0.5) * 2); + kpiData.put("productionLossTrend", (Math.random() - 0.5) * 4); + kpiData.put("revenueAmount", 85.2 + (Math.random() - 0.5) * 10); + kpiData.put("waterQualityScore", 98.5 + (Math.random() - 0.5) * 3); + kpiData.put("waterQualityTrend", (Math.random() - 0.5) * 2); + kpiData.put("alarmCount", 3 + (int)(Math.random() * 5 - 2)); + kpiData.put("alarmTrend", -15.8 + (Math.random() - 0.5) * 10); + + return kpiData; + } + + /** + * 推送实时数据到所有连接的WebSocket客户端 + */ + public void broadcastRealTimeData(String channel, Object data) { + Map message = new HashMap<>(); + message.put("type", channel); + message.put("data", data); + message.put("timestamp", LocalDateTime.now().format(formatter)); + + String jsonMessage; + try { + jsonMessage = objectMapper.writeValueAsString(message); + } catch (Exception e) { + System.err.println("JSON序列化错误: " + e.getMessage()); + return; + } + + sessions.forEach((sessionId, session) -> { + try { + if (session.isOpen()) { + session.sendMessage(new TextMessage(jsonMessage)); + } + } catch (Exception e) { + System.err.println("发送WebSocket消息失败: " + e.getMessage()); + } + }); + } + + /** + * 生成实时数据 + */ + private void generateRealTimeData() { + while (running) { + try { + // 生成供水趋势数据 + Map supplyTrendData = generateSupplyTrendData(); + realTimeData.put("supply-trend", supplyTrendData); + broadcastRealTimeData("supply-trend", supplyTrendData); + + // 生成水质数据 + Map waterQualityData = generateWaterQualityData(); + realTimeData.put("water-quality", waterQualityData); + broadcastRealTimeData("water-quality", waterQualityData); + + // 生成实时报警 + List> alarmData = generateRealtimeAlarms(); + realTimeData.put("realtime-alarm", alarmData); + broadcastRealTimeData("realtime-alarm", alarmData); + + // 生成设备状态数据 + Map deviceStatusData = generateDeviceStatusData(); + realTimeData.put("device-status", deviceStatusData); + broadcastRealTimeData("device-status", deviceStatusData); + + // 生成营收数据 + Map revenueData = generateRevenueData(); + realTimeData.put("revenue-data", revenueData); + broadcastRealTimeData("revenue-data", revenueData); + + // 生成能耗数据 + Map energyData = generateEnergyData(); + realTimeData.put("energy-data", energyData); + broadcastRealTimeData("energy-data", energyData); + + // 每5秒更新一次 + Thread.sleep(5000); + } catch (Exception e) { + System.err.println("实时数据生成错误: " + e.getMessage()); + try { + Thread.sleep(10000); + } catch (InterruptedException ie) { + break; + } + } + } + } + + /** + * 生成供水趋势数据 + */ + private Map generateSupplyTrendData() { + Map data = new HashMap<>(); + List inflow = new ArrayList<>(); + List outflow = new ArrayList<>(); + + for (int i = 0; i < 6; i++) { + inflow.add(400 + (int)(Math.random() * 200 - 100)); + outflow.add(380 + (int)(Math.random() * 180 - 90)); + } + + data.put("inflow", inflow); + data.put("outflow", outflow); + data.put("timestamp", LocalDateTime.now().format(formatter)); + + return data; + } + + /** + * 生成水质数据 + */ + private Map generateWaterQualityData() { + Map data = new HashMap<>(); + List currentValues = Arrays.asList( + 1.2 + (Math.random() - 0.5) * 0.2, // 浊度 + 7.2 + (Math.random() - 0.5) * 0.1, // pH值 + 0.3 + (Math.random() - 0.5) * 0.05, // 余氯 + 25 + (Math.random() - 0.5) * 5, // 菌落 + 3 + (Math.random() - 0.5) * 0.5, // 色度 + 1 + (Math.random() - 0.5) * 0.2 // 嗅味 + ); + + data.put("currentValues", currentValues); + data.put("timestamp", LocalDateTime.now().format(formatter)); + + return data; + } + + /** + * 生成实时报警数据 + */ + private List> generateRealtimeAlarms() { + List> alarms = new ArrayList<>(); + + // 随机生成1-3条新报警 + int alarmCount = 1 + (int)(Math.random() * 3); + + for (int i = 0; i < alarmCount; i++) { + Map alarm = new HashMap<>(); + alarm.put("id", System.currentTimeMillis() + i); + alarm.put("time", LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))); + + String[] types = {"压力报警", "水质报警", "设备报警", "流量报警", "漏损报警"}; + String[] locations = {"精芒片区", "一体化水厂", "托里片区", "八家户片区", "大镇阿合其"}; + String[] titles = {"压力异常波动", "浊度超标", "泵站设备异常", "流量异常", "管网漏损"}; + String[] levels = {"info", "warning", "danger"}; + String[] statuses = {"处理中", "监控中", "紧急处理", "已恢复"}; + + alarm.put("type", types[(int)(Math.random() * types.length)]); + alarm.put("location", locations[(int)(Math.random() * locations.length)]); + alarm.put("title", titles[(int)(Math.random() * titles.length)]); + alarm.put("level", levels[(int)(Math.random() * levels.length)]); + alarm.put("status", statuses[(int)(Math.random() * statuses.length)]); + + alarms.add(alarm); + } + + return alarms; + } + + /** + * 生成设备状态数据 + */ + private Map generateDeviceStatusData() { + Map data = new ArrayList<>(); + + // 模拟设备状态数据 + Map status1 = new HashMap<>(); + status1.put("name", "正常运行"); + status1.put("value", 142); + status1.put("itemStyle", Map.of("color", "#67c23a")); + + Map status2 = new HashMap<>(); + status2.put("name", "维护中"); + status2.put("value", 10 + (int)(Math.random() * 5)); + status2.put("itemStyle", Map.of("color", "#e6a23c")); + + Map status3 = new HashMap<>(); + status3.put("name", "故障"); + status3.put("value", 2 + (int)(Math.random() * 5)); + status3.put("itemStyle", Map.of("color", "#f56c6c")); + + Map status4 = new HashMap<>(); + status4.put("name", "离线"); + status4.put("value", 15 + (int)(Math.random() * 10)); + status4.put("itemStyle", Map.of("color", "#909399")); + + data.add(status1); + data.add(status2); + data.add(status3); + data.add(status4); + + return data; + } + + /** + * 生成营收数据 + */ + private Map generateRevenueData() { + Map data = new HashMap<>(); + List monthlyRevenue = new ArrayList<>(); + + // 生成6个月的营收数据 + for (int i = 0; i < 6; i++) { + monthlyRevenue.add(800 + Math.random() * 400); + } + + data.put("monthlyRevenue", monthlyRevenue); + data.put("timestamp", LocalDateTime.now().format(formatter)); + + return data; + } + + /** + * 生成能耗数据 + */ + private Map generateEnergyData() { + Map data = new HashMap<>(); + List hourlyEnergy = new ArrayList<>(); + + // 生成24小时的能耗数据 + for (int i = 0; i < 24; i++) { + int base = 100; + if (i >= 6 && i <= 22) { + base = 150 + (int)(Math.random() * 50); + } else { + base = 50 + (int)(Math.random() * 30); + } + hourlyEnergy.add(base); + } + + data.put("hourlyEnergy", hourlyEnergy); + data.put("timestamp", LocalDateTime.now().format(formatter)); + + return data; + } + + /** + * 获取实时数据缓存 + */ + public Map getRealTimeData() { + return new HashMap<>(realTimeData); + } + + /** + * 获取当前报警统计 + */ + public Map getAlarmStatistics() { + Map stats = new HashMap<>(); + + // 统计报警数量 + List> alarms = (List>) realTimeData.get("realtime-alarm"); + if (alarms != null) { + long urgent = alarms.stream().filter(a -> "danger".equals(a.get("level"))).count(); + long warning = alarms.stream().filter(a -> "warning".equals(a.get("level"))).count(); + long info = alarms.stream().filter(a -> "info".equals(a.get("level"))).count(); + + stats.put("urgent", urgent); + stats.put("warning", warning); + stats.put("info", info); + stats.put("total", alarms.size()); + } + + return stats; + } + + /** + * 获取供水专题大屏数据 + */ + public Map getWaterSupplyScreenData() { + Map data = new HashMap<>(); + + // 核心KPI指标 + data.put("coreKPIs", getCurrentKPIData()); + + // 水源数据 + List> waterSources = new ArrayList<>(); + String[] sourceNames = {"地表水源A", "地下水源B", "引黄调水", "水库备用"}; + String[] statuses = {"normal", "warning", "normal", "normal"}; + + for (int i = 0; i < sourceNames.length; i++) { + Map source = new HashMap<>(); + source.put("id", i + 1); + source.put("name", sourceNames[i]); + source.put("status", statuses[i]); + source.put("currentFlow", 300 + (int)(Math.random() * 200)); + source.put("targetFlow", 300 + (int)(Math.random() * 200)); + source.put("currentPressure", 350 + (int)(Math.random() * 100)); + source.put("minPressure", 300); + source.put("maxPressure", 500); + source.put("qualityScore", 90 + (int)(Math.random() * 10)); + waterSources.add(source); + } + data.put("waterSources", waterSources); + + // GIS地图数据 + Map mapData = new HashMap<>(); + mapData.put("pipelineTotalLength", 245 + (int)(Math.random() * 20)); + mapData.put("monitoringPoints", 326 + (int)(Math.random() * 50)); + mapData.put("coveragePopulation", 85 + (int)(Math.random() * 10)); + mapData.put("coverageAreas", 6); + data.put("mapData", mapData); + + // 运营统计 + Map operationStats = new HashMap<>(); + operationStats.put("designCapacity", 150000); + operationStats.put("actualCapacity", 138000 + (int)(Math.random() * 10000 - 5000)); + operationStats.put("avgDailySupply", 125000 + (int)(Math.random() * 10000 - 5000)); + operationStats.put("waterQualityIndex", 96.5 + (Math.random() - 0.5) * 2); + operationStats.put("complianceRate", 98.2 + (Math.random() - 0.5) * 1); + operationStats.put("complaintRate", 0.3 + (Math.random() - 0.5) * 0.1); + operationStats.put("productionLossRate", 10.8 + (Math.random() - 0.5) * 2); + operationStats.put("leakageRate", 5.8 + (Math.random() - 0.5) * 1); + operationStats.put("equipmentIntegrityRate", 95.6 + (Math.random() - 0.5) * 2); + data.put("operationStats", operationStats); + + // 营收数据 + Map revenueData = new HashMap<>(); + revenueData.put("todayRevenue", 8.52 + (Math.random() - 0.5) * 1); + revenueData.put("monthlyRevenue", 255.6 + (Math.random() - 0.5) * 20); + data.put("revenueData", revenueData); + + return data; + } + + /** + * 停止数据生成线程 + */ + public void shutdown() { + running = false; + if (dataGeneratorThread != null) { + dataGeneratorThread.interrupt(); + } + } @Override public List listSpecialScreens() { diff --git a/wm-bi/src/main/resources/application.yml b/wm-bi/src/main/resources/application.yml index 51c641b3..592406dc 100644 --- a/wm-bi/src/main/resources/application.yml +++ b/wm-bi/src/main/resources/application.yml @@ -38,6 +38,21 @@ sa-token: is-concurrent: true is-share: false +# WebSocket配置 +spring: + websocket: + enabled: true + task: + execution: + pool: + core-size: 4 + max-size: 8 + queue-capacity: 1000 + thread-name-prefix: websocket-exec- + message: + timeout: 30000 + cache-size: 1024 + # 日志配置 logging: level: diff --git a/wm-bi/src/main/resources/websocket-config.properties b/wm-bi/src/main/resources/websocket-config.properties new file mode 100644 index 00000000..111cc231 --- /dev/null +++ b/wm-bi/src/main/resources/websocket-config.properties @@ -0,0 +1,10 @@ +# WebSocket配置 +spring.websocket.enabled=true +spring.websocket.task.execution.pool.core-size=4 +spring.websocket.task.execution.pool.max-size=8 +spring.websocket.task.execution.pool.queue-capacity=1000 +spring.websocket.task.execution.thread-name-prefix=websocket-exec- + +# WebSocket消息配置 +spring.websocket.message-timeout=30000 +spring.websocket.message-cache-size=1024 \ No newline at end of file