Junjie
7 天以前 cd448f774cfd4837a969d01ebea03530608c6839
#websocket连接管理页面
1个文件已添加
2个文件已修改
320 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/TvDeviceController.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/websocket/TvWebSocketServer.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/tvWebSocket/tvWebSocket.html 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/TvDeviceController.java
@@ -11,6 +11,8 @@
import com.zy.asrs.entity.TvDevice;
import com.zy.asrs.service.ApkBuildTaskService;
import com.zy.asrs.service.TvDeviceService;
import com.zy.asrs.websocket.TvWebSocketServer;
import jakarta.websocket.Session;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -50,8 +52,8 @@
    @RequestMapping(value = "/tvDevice/list/auth")
    @ManagerAuth
    public R list(@RequestParam(defaultValue = "1") Integer curr,
            @RequestParam(defaultValue = "10") Integer limit,
            @RequestParam Map<String, Object> param) {
                  @RequestParam(defaultValue = "10") Integer limit,
                  @RequestParam Map<String, Object> param) {
        excludeTrash(param);
        QueryWrapper<TvDevice> wrapper = new QueryWrapper<>();
@@ -230,7 +232,7 @@
    @RequestMapping(value = "/tvDevice/uploadAndInstall/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R uploadAndInstall(@RequestParam("file") MultipartFile file,
            @RequestParam("deviceIds") String deviceIdsStr) {
                              @RequestParam("deviceIds") String deviceIdsStr) {
        try {
            if (file.isEmpty()) {
                return R.error("请选择APK文件");
@@ -369,4 +371,40 @@
            return R.error("重启失败: " + e.getMessage());
        }
    }
    /**
     * 查询电视机WebSocket连接状态
     */
    @RequestMapping(value = "/tvDevice/tvWebSocket/status/auth")
    @ManagerAuth
    public R tvWebSocketStatus() {
        Map<String, Session> sessions = TvWebSocketServer.getSessions();
        // 查询所有设备信息,用于匹配IP对应的设备名称
        List<TvDevice> allDevices = tvDeviceService.list();
        Map<String, String> ipToDeviceName = new HashMap<>();
        for (TvDevice device : allDevices) {
            if (device.getIp() != null) {
                ipToDeviceName.put(device.getIp(), device.getName());
            }
        }
        List<Map<String, Object>> connections = new ArrayList<>();
        for (Map.Entry<String, Session> entry : sessions.entrySet()) {
            Map<String, Object> info = new HashMap<>();
            String ip = entry.getKey();
            Session session = entry.getValue();
            info.put("ip", ip);
            info.put("deviceName", ipToDeviceName.getOrDefault(ip, "-"));
            info.put("sessionId", session.getId());
            info.put("open", session.isOpen());
            connections.add(info);
        }
        Map<String, Object> result = new HashMap<>();
        result.put("total", sessions.size());
        result.put("connections", connections);
        return R.ok(result);
    }
}
src/main/java/com/zy/asrs/websocket/TvWebSocketServer.java
@@ -179,6 +179,10 @@
        return SESSIONS.keySet();
    }
    public static Map<String, Session> getSessions() {
        return Collections.unmodifiableMap(SESSIONS);
    }
    private String getIp(Session session) {
        Object ip = session.getUserProperties().get("ip");
        return ip != null ? ip.toString() : "unknown";
src/main/webapp/views/tvWebSocket/tvWebSocket.html
New file
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>WebSocket连接监控</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/vue/element/element.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
            background: #f5f7fa;
            padding: 15px;
        }
        .app-container {
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        .stats-cards {
            display: flex;
            gap: 15px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }
        .stat-card {
            background: linear-gradient(135deg, #409eff, #66b1ff);
            border-radius: 8px;
            padding: 20px 30px;
            color: #fff;
            min-width: 200px;
            flex: 1;
            max-width: 300px;
        }
        .stat-card .label {
            font-size: 14px;
            opacity: 0.9;
            margin-bottom: 8px;
        }
        .stat-card .value {
            font-size: 36px;
            font-weight: bold;
        }
        .header-section {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }
        .header-section .title {
            font-size: 16px;
            font-weight: bold;
            color: #303133;
        }
        .status-dot {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 6px;
        }
        .status-online {
            background: #67c23a;
        }
        .pagination-section {
            margin-top: 20px;
            display: flex;
            justify-content: flex-end;
        }
        .auto-refresh-bar {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 15px;
            font-size: 13px;
            color: #909399;
        }
    </style>
</head>
<body>
<div id="app">
    <div class="app-container">
        <!-- 统计卡片 -->
        <div class="stats-cards">
            <div class="stat-card">
                <div class="label">WebSocket连接总数</div>
                <div class="value">{{ total }}</div>
            </div>
        </div>
        <!-- 自动刷新控制 -->
        <div class="auto-refresh-bar">
            <el-switch v-model="autoRefresh" active-text="自动刷新" inactive-text="关闭"></el-switch>
            <span>每5秒刷新一次</span>
            <el-button size="mini" type="primary" icon="el-icon-refresh" @click="loadData"
                       :loading="loading">手动刷新</el-button>
        </div>
        <!-- 表格区域 -->
        <div class="header-section">
            <span class="title">连接列表</span>
            <span style="color: #909399; font-size: 13px;">最后刷新: {{ lastRefreshTime || '-' }}</span>
        </div>
        <el-table :data="pagedData" border stripe v-loading="loading"
                  :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%;">
            <el-table-column type="index" label="序号" width="60" align="center"
                             :index="indexMethod"></el-table-column>
            <el-table-column prop="ip" label="客户端IP" min-width="140" align="center"></el-table-column>
            <el-table-column prop="deviceName" label="设备名称" min-width="180">
                <template slot-scope="scope">
                    {{ scope.row.deviceName || '-' }}
                </template>
            </el-table-column>
            <el-table-column prop="sessionId" label="会话ID" min-width="200" show-overflow-tooltip>
                <template slot-scope="scope">
                    {{ scope.row.sessionId || '-' }}
                </template>
            </el-table-column>
            <el-table-column prop="open" label="状态" width="100" align="center">
                <template slot-scope="scope">
                        <span>
                            <span class="status-dot" :class="scope.row.open ? 'status-online' : ''"></span>
                            {{ scope.row.open ? '活跃' : '已关闭' }}
                        </span>
                </template>
            </el-table-column>
        </el-table>
        <!-- 分页 -->
        <div class="pagination-section" v-if="connections.length > pageSize">
            <el-pagination background layout="total, prev, pager, next" :total="connections.length"
                           :page-size="pageSize" :current-page="currentPage" @current-change="handlePageChange">
            </el-pagination>
        </div>
        <!-- 空状态 -->
        <div v-if="!loading && connections.length === 0"
             style="text-align: center; padding: 60px; color: #909399;">
            <i class="el-icon-warning-outline" style="font-size: 48px; margin-bottom: 15px;"></i>
            <div>暂无WebSocket连接</div>
        </div>
    </div>
</div>
<script src="../../static/vue/js/vue.min.js"></script>
<script src="../../static/vue/element/element.js"></script>
<script src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
<script src="../../static/js/common.js"></script>
<script>
    new Vue({
        el: '#app',
        data: {
            connections: [],
            total: 0,
            loading: false,
            autoRefresh: true,
            refreshTimer: null,
            lastRefreshTime: '',
            currentPage: 1,
            pageSize: 10
        },
        computed: {
            pagedData() {
                var start = (this.currentPage - 1) * this.pageSize;
                return this.connections.slice(start, start + this.pageSize);
            }
        },
        created() {
            this.loadData();
            this.startAutoRefresh();
        },
        watch: {
            autoRefresh: function (val) {
                if (val) {
                    this.startAutoRefresh();
                } else {
                    this.stopAutoRefresh();
                }
            }
        },
        methods: {
            getHeaders() {
                return { 'token': localStorage.getItem('token') };
            },
            loadData() {
                this.loading = true;
                var self = this;
                $.ajax({
                    url: baseUrl + '/tvDevice/tvWebSocket/status/auth',
                    headers: this.getHeaders(),
                    success: function (res) {
                        self.loading = false;
                        if (res.code === 200) {
                            self.connections = res.data.connections || [];
                            self.total = res.data.total || 0;
                            self.lastRefreshTime = self.formatTime(new Date());
                        } else if (res.code === 403) {
                            top.location.href = baseUrl + '/';
                        } else {
                            self.$message.error(res.msg || '加载失败');
                        }
                    },
                    error: function () {
                        self.loading = false;
                        self.$message.error('请求失败');
                    }
                });
            },
            startAutoRefresh() {
                this.stopAutoRefresh();
                this.refreshTimer = setInterval(this.loadData, 5000);
            },
            stopAutoRefresh() {
                if (this.refreshTimer) {
                    clearInterval(this.refreshTimer);
                    this.refreshTimer = null;
                }
            },
            handlePageChange(page) {
                this.currentPage = page;
            },
            indexMethod(index) {
                return (this.currentPage - 1) * this.pageSize + index + 1;
            },
            formatTime(date) {
                var h = String(date.getHours()).padStart(2, '0');
                var m = String(date.getMinutes()).padStart(2, '0');
                var s = String(date.getSeconds()).padStart(2, '0');
                return h + ':' + m + ':' + s;
            }
        },
        beforeDestroy() {
            this.stopAutoRefresh();
        }
    });
</script>
</body>
</html>