测试过程问题修改

This commit is contained in:
zgw
2026-01-07 13:30:26 +08:00
parent c9076d2db7
commit e0330b604d
3 changed files with 618 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
package com.ruoyi.web.controller.schedule;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.schedule.ScheduleThread;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 线程管理控制器
*
* @author ruoyi
*/
@RestController
@RequestMapping("/schedule/thread")
public class ThreadManageController {
@Autowired
private ScheduleThread scheduleThread;
/**
* 获取线程列表
*/
@GetMapping("/list")
public AjaxResult list() {
if (scheduleThread.threadTypeMap.isEmpty()) {
scheduleThread.refreshThreadMap();
}
return AjaxResult.success(scheduleThread.getAllThreadStatus());
}
/**
* 启动线程
*/
@PostMapping("/start/{key}")
public AjaxResult start(@PathVariable String key) {
try {
scheduleThread.startThread(key);
return AjaxResult.success("启动成功");
} catch (Exception e) {
return AjaxResult.error("启动失败: " + e.getMessage());
}
}
/**
* 停止线程
*/
@PostMapping("/stop/{key}")
public AjaxResult stop(@PathVariable String key) {
try {
scheduleThread.stopThread(key);
return AjaxResult.success("停止指令已下发");
} catch (Exception e) {
return AjaxResult.error("停止失败: " + e.getMessage());
}
}
/**
* 启动全部线程
*/
@PostMapping("/startAll")
public AjaxResult startAll() {
try {
for (String key : scheduleThread.threadMap.keySet()) {
try {
scheduleThread.startThread(key);
} catch (Exception e) {
// 忽略单个启动失败,继续启动下一个
}
}
return AjaxResult.success("全部启动指令已下发");
} catch (Exception e) {
return AjaxResult.error("操作失败: " + e.getMessage());
}
}
/**
* 停止全部线程
*/
@PostMapping("/stopAll")
public AjaxResult stopAll() {
try {
for (String key : scheduleThread.threadMap.keySet()) {
scheduleThread.stopThread(key);
}
return AjaxResult.success("全部停止指令已下发");
} catch (Exception e) {
return AjaxResult.error("操作失败: " + e.getMessage());
}
}
/**
* 启动分组线程
*/
@PostMapping("/startGroup/{type}")
public AjaxResult startGroup(@PathVariable String type) {
try {
for (Map.Entry<String, String> entry : scheduleThread.threadTypeMap.entrySet()) {
if (type.equals(entry.getValue())) {
try {
scheduleThread.startThread(entry.getKey());
} catch (Exception e) {
}
}
}
return AjaxResult.success("分组启动指令已下发");
} catch (Exception e) {
return AjaxResult.error("操作失败: " + e.getMessage());
}
}
/**
* 停止分组线程
*/
@PostMapping("/stopGroup/{type}")
public AjaxResult stopGroup(@PathVariable String type) {
try {
for (Map.Entry<String, String> entry : scheduleThread.threadTypeMap.entrySet()) {
if (type.equals(entry.getValue())) {
scheduleThread.stopThread(entry.getKey());
}
}
return AjaxResult.success("分组停止指令已下发");
} catch (Exception e) {
return AjaxResult.error("操作失败: " + e.getMessage());
}
}
/**
* 获取线程日志
*/
@GetMapping("/log/{key}")
public AjaxResult log(@PathVariable String key) {
// 如果key是"ALL",则不进行关键词过滤,显示所有日志(除了堆栈合并逻辑外)
String target = "ALL".equals(key) ? null : key;
// 日志路径,根据logback配置及实际环境确认
File file = new File("D:/home/ruoyi/logs/sys-info.log");
if (!file.exists()) {
return AjaxResult.error("日志文件不存在");
}
LinkedList<String> lines = new LinkedList<>();
// 简单的日志行首匹配模式 (yyyy-MM-dd HH:mm:ss)
Pattern logStartPattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}");
boolean isMatchingStack = false;
// 如果是查看所有日志,默认所有行都匹配,除非遇到新的日志行且我们想做更复杂的过滤(目前全显)
// 但为了支持堆栈合并,我们还是需要判断行首
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "GBK"))) {
String line;
while ((line = br.readLine()) != null) {
boolean isLogStart = logStartPattern.matcher(line).find();
if (target == null) {
// 查看所有日志:直接添加,或者如果是异常日志查看模式可以在这里过滤
// 暂时逻辑:全部添加
lines.add(line);
} else {
// 查看特定线程日志
if (line.contains(target)) {
isMatchingStack = true;
lines.add(line);
} else if (isLogStart) {
isMatchingStack = false;
} else if (isMatchingStack) {
lines.add(line);
}
}
// 限制行数,避免内存溢出,保留最后1000行
if (lines.size() > 1000) {
lines.removeFirst();
}
}
} catch (Exception e) {
return AjaxResult.error("读取日志失败: " + e.getMessage());
}
return AjaxResult.success(lines);
}
/**
* 获取异常日志
*/
@GetMapping("/errorLog")
public AjaxResult errorLog() {
File file = new File("D:/home/ruoyi/logs/sys-error.log");
if (!file.exists()) {
// 如果没有单独的error log,尝试从info log中过滤ERROR级别?
// 假设有 sys-error.log
return AjaxResult.error("异常日志文件不存在");
}
LinkedList<String> lines = new LinkedList<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "GBK"))) {
String line;
while ((line = br.readLine()) != null) {
lines.add(line);
if (lines.size() > 1000) {
lines.removeFirst();
}
}
} catch (Exception e) {
return AjaxResult.error("读取日志失败: " + e.getMessage());
}
return AjaxResult.success(lines);
}
}

View File

@@ -0,0 +1,73 @@
import request from '@/utils/request'
// 查询线程列表
export function listThread() {
return request({
url: '/schedule/thread/list',
method: 'get'
})
}
// 启动线程
export function startThread(key) {
return request({
url: '/schedule/thread/start/' + key,
method: 'post'
})
}
// 停止线程
export function stopThread(key) {
return request({
url: '/schedule/thread/stop/' + key,
method: 'post'
})
}
// 获取日志
export function getThreadLog(key) {
return request({
url: '/schedule/thread/log/' + key,
method: 'get'
})
}
// 启动全部线程
export function startAllThread() {
return request({
url: '/schedule/thread/startAll',
method: 'post'
})
}
// 停止全部线程
export function stopAllThread() {
return request({
url: '/schedule/thread/stopAll',
method: 'post'
})
}
// 启动分组线程
export function startGroupThread(type) {
return request({
url: '/schedule/thread/startGroup/' + type,
method: 'post'
})
}
// 停止分组线程
export function stopGroupThread(type) {
return request({
url: '/schedule/thread/stopGroup/' + type,
method: 'post'
})
}
// 获取异常日志
export function getErrorLog() {
return request({
url: '/schedule/thread/errorLog',
method: 'get'
})
}

View File

@@ -0,0 +1,331 @@
<template>
<div class="app-container">
<!-- 全局控制卡片 -->
<el-card class="box-card" shadow="hover" style="margin-bottom: 20px;">
<div slot="header" class="clearfix">
<span style="font-weight: bold; font-size: 18px;"><i class="el-icon-setting"></i> 全局控制中心</span>
</div>
<el-row :gutter="20" type="flex" align="middle">
<el-col :span="7">
<div class="control-group">
<span class="control-label">系统总控:</span>
<el-button type="success" size="small" icon="el-icon-video-play" @click="handleStartAll">全部开启</el-button>
<el-button type="danger" size="small" icon="el-icon-video-pause" @click="handleStopAll">全部关闭</el-button>
</div>
</el-col>
<el-col :span="11" style="border-left: 1px solid #EBEEF5; border-right: 1px solid #EBEEF5;">
<div class="control-group" style="justify-content: space-evenly;">
<div style="display: flex; align-items: center;">
<span class="control-label"><i class="el-icon-cpu"></i> 通讯组:</span>
<el-button-group>
<el-button type="primary" size="mini" icon="el-icon-video-play" @click="handleStartGroup('read')">开启</el-button>
<el-button type="info" size="mini" icon="el-icon-video-pause" @click="handleStopGroup('read')">停止</el-button>
</el-button-group>
</div>
<el-divider direction="vertical" style="height: 20px;"></el-divider>
<div style="display: flex; align-items: center;">
<span class="control-label"><i class="el-icon-s-operation"></i> 调度组:</span>
<el-button-group>
<el-button type="primary" size="mini" icon="el-icon-video-play" @click="handleStartGroup('schedule')">开启</el-button>
<el-button type="info" size="mini" icon="el-icon-video-pause" @click="handleStopGroup('schedule')">停止</el-button>
</el-button-group>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="control-group" style="justify-content: flex-end;">
<span class="control-label">日志:</span>
<el-button type="primary" plain size="small" icon="el-icon-document" @click="handleGlobalLog">系统日志</el-button>
<el-button type="danger" plain size="small" icon="el-icon-warning-outline" @click="handleErrorLog">异常日志</el-button>
</div>
</el-col>
</el-row>
</el-card>
<el-row :gutter="20">
<!-- 读取线程组 -->
<el-col :span="12">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span style="font-weight: bold; font-size: 16px;"><i class="el-icon-cpu"></i> PLC读取线程</span>
<el-tag size="small" type="info" style="float: right">硬件通信</el-tag>
</div>
<el-table v-loading="loading" :data="readThreads" size="small" :show-header="true">
<el-table-column label="线程名称" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.isRun ? 'success' : 'danger'" size="mini" effect="dark">
{{ scope.row.isRun ? '运行中' : '已停止' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-video-play" v-if="!scope.row.isRun" @click="handleStart(scope.row)">启动</el-button>
<el-button size="mini" type="text" icon="el-icon-video-pause" v-if="scope.row.isRun" @click="handleStop(scope.row)">停止</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<!-- 调度线程组 -->
<el-col :span="12">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span style="font-weight: bold; font-size: 16px;"><i class="el-icon-s-operation"></i> 调度控制线程</span>
<el-tag size="small" type="primary" style="float: right">业务逻辑</el-tag>
</div>
<el-table v-loading="loading" :data="scheduleThreads" size="small" :show-header="true">
<el-table-column label="线程名称" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.isRun ? 'success' : 'danger'" size="mini" effect="dark">
{{ scope.row.isRun ? '运行中' : '已停止' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-video-play" v-if="!scope.row.isRun" @click="handleStart(scope.row)">启动</el-button>
<el-button size="mini" type="text" icon="el-icon-video-pause" v-if="scope.row.isRun" @click="handleStop(scope.row)">停止</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<!-- 监控线程组 -->
<el-col :span="24">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span style="font-weight: bold; font-size: 16px;"><i class="el-icon-monitor"></i> 系统监控线程</span>
<el-tag size="small" type="warning" style="float: right">状态监控</el-tag>
</div>
<el-table v-loading="loading" :data="monitorThreads" size="small" :show-header="true">
<el-table-column label="线程名称" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="标识" prop="key" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.isRun ? 'success' : 'danger'" size="mini" effect="dark">
{{ scope.row.isRun ? '运行中' : '已停止' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-video-play" v-if="!scope.row.isRun" @click="handleStart(scope.row)">启动</el-button>
<el-button size="mini" type="text" icon="el-icon-video-pause" v-if="scope.row.isRun" @click="handleStop(scope.row)">停止</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 日志弹窗 -->
<el-dialog :title="logTitle" :visible.sync="openLog" width="800px" append-to-body>
<div class="log-container" v-loading="logLoading">
<pre v-if="logContent.length > 0">{{ logContent.join('\n') }}</pre>
<div v-else style="text-align: center; color: #909399; padding-top: 200px;">暂无日志</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="refreshLog">刷 新</el-button>
<el-button @click="openLog = false">关 闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listThread, startThread, stopThread, getThreadLog, startAllThread, stopAllThread, startGroupThread, stopGroupThread, getErrorLog } from "@/api/schedule/thread";
export default {
name: "Thread",
data() {
return {
// 遮罩层
loading: true,
// 线程表格数据
threadList: [],
// 日志弹窗
openLog: false,
logTitle: "",
logLoading: false,
logContent: [],
logType: 'system', // system, error
currentLogKey: null // for refresh
};
},
computed: {
readThreads() {
return this.threadList.filter(item => item.type === 'read' || !item.type);
},
scheduleThreads() {
return this.threadList.filter(item => item.type === 'schedule');
},
monitorThreads() {
return this.threadList.filter(item => item.type === 'monitor');
}
},
created() {
this.getList();
},
methods: {
/** 查询线程列表 */
getList() {
this.loading = true;
listThread().then(response => {
this.threadList = response.data;
this.loading = false;
});
},
/** 启动按钮操作 */
handleStart(row) {
this.$confirm('确认要启动 "' + row.name + '" 吗?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
return startThread(row.key);
}).then(response => {
this.$modal.msgSuccess("启动指令已下发");
setTimeout(() => {
this.getList();
}, 1000);
}).catch(error => {
console.error("启动失败:", error);
});
},
/** 停止按钮操作 */
handleStop(row) {
this.$confirm('确认要停止 "' + row.name + '" 吗?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
return stopThread(row.key);
}).then(response => {
this.$modal.msgSuccess("停止指令已下发");
// 稍微延迟一下刷新,等待线程退出
setTimeout(() => {
this.getList();
}, 1000);
}).catch(error => {
console.error("停止失败:", error);
});
},
/** 一键开启所有 */
handleStartAll() {
this.$confirm('确认要启动所有线程吗?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
return startAllThread();
}).then(() => {
this.$modal.msgSuccess("操作成功");
setTimeout(() => { this.getList(); }, 2000);
});
},
/** 一键关闭所有 */
handleStopAll() {
this.$confirm('确认要停止所有线程吗?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "error"
}).then(() => {
return stopAllThread();
}).then(() => {
this.$modal.msgSuccess("操作成功");
setTimeout(() => { this.getList(); }, 2000);
});
},
/** 分组开启 */
handleStartGroup(type) {
startGroupThread(type).then(() => {
this.$modal.msgSuccess("操作成功");
setTimeout(() => { this.getList(); }, 1500);
});
},
/** 分组停止 */
handleStopGroup(type) {
stopGroupThread(type).then(() => {
this.$modal.msgSuccess("操作成功");
setTimeout(() => { this.getList(); }, 1500);
});
},
/** 系统日志 */
handleGlobalLog() {
this.logTitle = "系统运行日志";
this.logType = 'system';
this.currentLogKey = 'ALL';
this.openLog = true;
this.refreshLog();
},
/** 异常日志 */
handleErrorLog() {
this.logTitle = "系统异常日志";
this.logType = 'error';
this.openLog = true;
this.refreshLog();
},
refreshLog() {
this.logLoading = true;
if (this.logType === 'system') {
getThreadLog(this.currentLogKey).then(response => {
this.logContent = response.data;
this.logLoading = false;
this.$nextTick(() => {
const container = this.$el.querySelector('.log-container');
if (container) container.scrollTop = container.scrollHeight;
});
});
} else {
getErrorLog().then(response => {
this.logContent = response.data;
this.logLoading = false;
this.$nextTick(() => {
const container = this.$el.querySelector('.log-container');
if (container) container.scrollTop = container.scrollHeight;
});
});
}
}
}
};
</script>
<style scoped>
.log-container {
height: 500px;
overflow-y: auto;
background-color: #f5f7fa;
padding: 10px;
border: 1px solid #e4e7ed;
border-radius: 4px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: Consolas, Monaco, monospace;
font-size: 12px;
margin: 0;
}
.box-card {
height: 100%;
}
.control-group {
display: flex;
align-items: center;
height: 40px;
}
.control-label {
font-weight: bold;
margin-right: 10px;
color: #606266;
}
</style>