-
启动采集
+
+ 启动采集
停止采集
刷新配置
@@ -275,7 +275,7 @@ import type { ApiResponse, DashboardSummary, CollectorStatus, MachineRankRow, Wo
const { isMock } = useMockMode()
const summary = ref
({ onlineCount: 0, totalMachines: 0, todayProduction: 0, activeAlerts: 0, collectSuccessRate: 0, todayCuttingTime: 0, runningMachines: 0, dataMissingMachines: 0 })
-const collectorStatus = ref({ status: 'stopped', uptimeSeconds: 0 })
+const collectorStatus = ref({ status: 'stopped', uptimeSeconds: 0, serviceStatus: 'NotInstalled', serviceName: '' })
const machineRank = ref([])
const workerRank = ref([])
const trendData = ref([])
@@ -338,7 +338,20 @@ let statusPie: ECharts | null = null
async function startCollector() {
if (startLoading.value) return
startLoading.value = true
- try { await request.post('/admin/collector/start'); ElMessage.success('采集服务已启动'); await loadData() } catch { /* request拦截器已显示错误 */ } finally { startLoading.value = false }
+ try {
+ // 在发起启动前,若已安装且正在运行,前端应给出友好提示
+ if (collectorStatus.value.serviceStatus && collectorStatus.value.serviceStatus === 'Running') {
+ ElMessage.info('采集服务已在运行中');
+ return;
+ }
+ if (collectorStatus.value.serviceStatus && collectorStatus.value.serviceStatus === 'NotInstalled') {
+ ElMessage.warning('采集服务未安装,请运行 install.ps1 安装脚本');
+ return;
+ }
+ await request.post('/admin/collector/start');
+ ElMessage.success('采集服务已启动');
+ await loadData();
+ } catch { /* request拦截器已显示错误 */ } finally { startLoading.value = false }
}
async function stopCollector() {
@@ -615,3 +628,14 @@ onUnmounted(() => {
}
}
+// 映射 Windows 服务状态为可读文本
+function serviceStatusLabel(status: string | undefined): string {
+ switch ((status || '').toString()) {
+ case 'NotInstalled': return '未安装';
+ case 'Running': return '运行中';
+ case 'Starting': return '启动中';
+ case 'StartFailed': return '启动失败';
+ case 'Stopped': return '已停止';
+ default: return status || '-';
+ }
+}
diff --git a/simulator.json b/simulator.json
new file mode 100644
index 0000000..914d815
--- /dev/null
+++ b/simulator.json
@@ -0,0 +1,28 @@
+{
+ "gatewayPort": 9000,
+ "addresses": [
+ {
+ "name": "FANUC-1号模拟",
+ "port": 9001,
+ "brand": "fanuc",
+ "dataChangeInterval": 10,
+ "scenarioMode": "auto",
+ "devices": [
+ { "deviceCode": "CNC-A001", "desc": "西栋1号", "initialProgram": "O0001", "initialPartCount": 50 },
+ { "deviceCode": "CNC-006", "desc": "6号机床", "initialProgram": "O0002", "initialPartCount": 120 },
+ { "deviceCode": "CNC-008", "desc": "8号机床", "initialProgram": "O0003", "initialPartCount": 0 }
+ ]
+ },
+ {
+ "name": "FANUC-2号模拟",
+ "port": 9002,
+ "brand": "fanuc",
+ "dataChangeInterval": 15,
+ "scenarioMode": "auto",
+ "devices": [
+ { "deviceCode": "CNC-B002", "desc": "B栋2号", "initialProgram": "1566.NC", "initialPartCount": 80 },
+ { "deviceCode": "CNC-005", "desc": "验证机床", "initialProgram": "TEST001", "initialPartCount": 10 }
+ ]
+ }
+ ]
+}
diff --git a/src/CncCollector/scripts/playwright.config.ts b/src/CncCollector/scripts/playwright.config.ts
index 0b4def3..5e340fa 100644
--- a/src/CncCollector/scripts/playwright.config.ts
+++ b/src/CncCollector/scripts/playwright.config.ts
@@ -7,7 +7,7 @@ import { defineConfig } from '@playwright/test';
*/
export default defineConfig({
testDir: '.',
- testMatch: 'e2e-collector.spec.ts',
+ testMatch: '*.spec.ts',
timeout: 120000,
retries: 0,
reporter: [['list'], ['html', { open: 'never' }]],
diff --git a/src/CncService/CncService.csproj b/src/CncService/CncService.csproj
index 2423637..cef2d3d 100644
--- a/src/CncService/CncService.csproj
+++ b/src/CncService/CncService.csproj
@@ -23,4 +23,9 @@
+
+
+
+
+
diff --git a/src/CncService/Impl/DashboardService.cs b/src/CncService/Impl/DashboardService.cs
index 0402ff5..fd492f2 100644
--- a/src/CncService/Impl/DashboardService.cs
+++ b/src/CncService/Impl/DashboardService.cs
@@ -13,12 +13,15 @@ namespace CncService.Impl
{
private readonly IDashboardRepository _dashboardRepository;
private readonly ICollectorHeartbeatRepository _collectorHeartbeatRepository;
+ private readonly IWindowsServiceChecker _serviceChecker;
public DashboardService(IDashboardRepository dashboardRepository,
- ICollectorHeartbeatRepository collectorHeartbeatRepository)
+ ICollectorHeartbeatRepository collectorHeartbeatRepository,
+ IWindowsServiceChecker serviceChecker = null)
{
_dashboardRepository = dashboardRepository ?? throw new ArgumentNullException(nameof(dashboardRepository));
_collectorHeartbeatRepository = collectorHeartbeatRepository ?? throw new ArgumentNullException(nameof(collectorHeartbeatRepository));
+ _serviceChecker = serviceChecker;
}
///
@@ -73,27 +76,41 @@ namespace CncService.Impl
public object GetCollectorStatus()
{
var latest = _collectorHeartbeatRepository.GetLatest("collector-service");
-
// 心跳超时阈值:90秒(3个心跳间隔,采集服务默认每30秒上报一次)
const int heartbeatTimeoutSeconds = 90;
- bool isRunning = false;
- long uptimeSeconds = 0;
+ bool heartbeatRunning = false;
+ long heartbeatUptime = 0;
+ DateTime? lastCollectTime = latest?.LastCollectTime;
if (latest != null && latest.Status == "running")
{
- // 检查最后心跳时间是否在阈值内,超时则判定为已停止
var lastHeartbeat = latest.CreatedAt;
var elapsed = (DateTime.Now - lastHeartbeat).TotalSeconds;
- isRunning = elapsed <= heartbeatTimeoutSeconds;
+ heartbeatRunning = elapsed <= heartbeatTimeoutSeconds;
+ if (heartbeatRunning)
+ heartbeatUptime = latest.UptimeSeconds ?? 0;
+ }
- if (isRunning)
- {
- uptimeSeconds = latest.UptimeSeconds ?? 0;
- }
+ // 额外的 Windows 服务状态
+ string serviceStatusText = "NotInstalled";
+ if (_serviceChecker != null)
+ {
+ var svc = _serviceChecker.GetServiceStatus("collector-service");
+ serviceStatusText = svc.ToString();
}
- return new { status = isRunning ? "running" : "stopped", uptimeSeconds, lastCollectTime = latest?.LastCollectTime };
+ // 组合状态:NotInstalled -> 停止,其他根据心跳决定
+ string status = (serviceStatusText == "NotInstalled") ? "stopped" : (heartbeatRunning ? "running" : "stopped");
+
+ return new {
+ status,
+ uptimeSeconds = heartbeatRunning ? heartbeatUptime : 0,
+ lastCollectTime,
+ serviceStatus = serviceStatusText,
+ serviceName = "collector-service",
+ serviceMessage = (string)null
+ };
}
}
}
diff --git a/src/CncService/Impl/WindowsServiceChecker.cs b/src/CncService/Impl/WindowsServiceChecker.cs
new file mode 100644
index 0000000..8bec113
--- /dev/null
+++ b/src/CncService/Impl/WindowsServiceChecker.cs
@@ -0,0 +1,110 @@
+using System;
+using System.ServiceProcess;
+using System.Threading;
+using CncService.Interface;
+
+namespace CncService.Impl
+{
+ ///
+ /// Windows 服务检测实现(基于 ServiceController)
+ ///
+ public class WindowsServiceChecker : IWindowsServiceChecker
+ {
+ public ServiceStatusEnum GetServiceStatus(string serviceName)
+ {
+ try
+ {
+ using (var sc = new ServiceController(serviceName))
+ {
+ sc.Refresh();
+ switch (sc.Status)
+ {
+ case ServiceControllerStatus.Running:
+ return ServiceStatusEnum.Running;
+ case ServiceControllerStatus.StartPending:
+ case ServiceControllerStatus.ContinuePending:
+ default:
+ // 启动中的状态或未知状态视作 Starting
+ return ServiceStatusEnum.Starting;
+ }
+ }
+ }
+ catch (InvalidOperationException)
+ {
+ // 服务未安装
+ return ServiceStatusEnum.NotInstalled;
+ }
+ catch
+ {
+ // 其他异常视为不可用,保守处理
+ return ServiceStatusEnum.NotInstalled;
+ }
+ }
+
+ public (bool Success, string Message) TryStartService(string serviceName, int timeoutSeconds)
+ {
+ try
+ {
+ using (var sc = new ServiceController(serviceName))
+ {
+ sc.Refresh();
+ if (sc.Status == ServiceControllerStatus.Running)
+ return (true, "已运行");
+
+ sc.Start();
+ var timeout = TimeSpan.FromSeconds(timeoutSeconds);
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ while (sw.Elapsed < timeout)
+ {
+ sc.Refresh();
+ if (sc.Status == ServiceControllerStatus.Running)
+ return (true, "启动成功");
+ Thread.Sleep(500);
+ }
+ return (false, $"启动超时,当前状态={sc.Status}");
+ }
+ }
+ catch (InvalidOperationException)
+ {
+ return (false, "NotInstalled");
+ }
+ catch (Exception ex)
+ {
+ return (false, $"启动失败: {ex.Message}");
+ }
+ }
+
+ public (bool Success, string Message) TryStopService(string serviceName, int timeoutSeconds)
+ {
+ try
+ {
+ using (var sc = new ServiceController(serviceName))
+ {
+ sc.Refresh();
+ if (sc.Status == ServiceControllerStatus.Stopped)
+ return (true, "已停止");
+
+ sc.Stop();
+ var timeout = TimeSpan.FromSeconds(timeoutSeconds);
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ while (sw.Elapsed < timeout)
+ {
+ sc.Refresh();
+ if (sc.Status == ServiceControllerStatus.Stopped)
+ return (true, "停止成功");
+ Thread.Sleep(500);
+ }
+ return (false, $"停止超时,当前状态={sc.Status}");
+ }
+ }
+ catch (InvalidOperationException)
+ {
+ return (false, "NotInstalled");
+ }
+ catch (Exception ex)
+ {
+ return (false, $"停止失败: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/src/CncService/Interface/IWindowsServiceChecker.cs b/src/CncService/Interface/IWindowsServiceChecker.cs
new file mode 100644
index 0000000..69da050
--- /dev/null
+++ b/src/CncService/Interface/IWindowsServiceChecker.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace CncService.Interface
+{
+ // Windows 服务状态枚举,用于和心跳状态区分不同场景
+ public enum ServiceStatusEnum
+ {
+ NotInstalled,
+ Stopped,
+ Running,
+ Starting,
+ StartFailed
+ }
+
+ ///
+ /// Windows 服务检测接口(用于管理后台对采集服务的状态检测与控制)
+ ///
+ public interface IWindowsServiceChecker
+ {
+ ///
+ /// 获取指定服务的当前状态
+ ///
+ /// 服务名
+ /// 服务状态枚举
+ ServiceStatusEnum GetServiceStatus(string serviceName);
+
+ ///
+ /// 尝试启动指定服务,并在给定超时内等待就绪
+ ///
+ /// 服务名
+ /// 超时(秒)
+ /// (是否成功, 详细信息)
+ (bool Success, string Message) TryStartService(string serviceName, int timeoutSeconds);
+
+ ///
+ /// 尝试停止指定服务,并在给定超时内等待停止
+ ///
+ /// 服务名
+ /// 超时(秒)
+ /// (是否成功, 详细信息)
+ (bool Success, string Message) TryStopService(string serviceName, int timeoutSeconds);
+ }
+}
diff --git a/src/CncWebApi/Controllers/DashboardController.cs b/src/CncWebApi/Controllers/DashboardController.cs
index 9457b85..c149058 100644
--- a/src/CncWebApi/Controllers/DashboardController.cs
+++ b/src/CncWebApi/Controllers/DashboardController.cs
@@ -133,6 +133,22 @@ namespace CncWebApi.Controllers
[Route("~/api/admin/collector/start")]
public IHttpActionResult StartCollector()
{
+ // 先查询服务状态,决定下一步动作
+ try
+ {
+ dynamic statusObj = _dashboardService.GetCollectorStatus();
+ string serviceStatus = statusObj?.serviceStatus as string;
+ if (!string.IsNullOrEmpty(serviceStatus) && string.Equals(serviceStatus, "NotInstalled", StringComparison.OrdinalIgnoreCase))
+ {
+ return Ok(ApiResponse