Compare commits

..

No commits in common. 'main' and 'feat/windows-service-status-auto' have entirely different histories.

@ -31,15 +31,9 @@ export interface CollectCycle {
export interface CollectRaw { export interface CollectRaw {
id: number id: number
collectAddressId: number logTime: string
requestTime: string sourceAddress?: string
responseTime: string contentPreview?: string
responseDuration?: number | null
isSuccess: number
statusCode?: number | null
rawJson?: string
errorMessage?: string
createdAt: string
} }
// --- 公开的 API 封装 --- // --- 公开的 API 封装 ---
@ -97,9 +91,4 @@ export function fetchRawList(params?: {
) )
} }
// 获取原始日志详情
export function fetchRawDetail(id: number) {
return request.get<CollectRaw>(`/admin/collect-log/raw/${id}`)
}
export default {} export default {}

@ -132,25 +132,6 @@
background background
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
/> />
<!-- 采集周期详情弹窗 -->
<el-dialog v-model="cycleDetailVisible" title="采集周期详情" width="640px" destroy-on-close>
<el-descriptions :column="2" border v-if="cycleDetailRow">
<el-descriptions-item label="周期时间">{{ cycleDetailRow.cycleTime }}</el-descriptions-item>
<el-descriptions-item label="采集地址">{{ cycleDetailRow.addressName || cycleDetailRow.collectAddressId }}</el-descriptions-item>
<el-descriptions-item label="总机床数">{{ cycleDetailRow.totalMachines }}</el-descriptions-item>
<el-descriptions-item label="成功数">{{ cycleDetailRow.successCount }}</el-descriptions-item>
<el-descriptions-item label="失败数">{{ cycleDetailRow.failCount }}</el-descriptions-item>
<el-descriptions-item label="是否异常">
<el-tag :type="cycleDetailRow.hasAnomaly ? 'danger' : 'success'" size="small">{{ cycleDetailRow.hasAnomaly ? '有异常' : '无异常' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="变更分布" :span="2">{{ cycleDetailRow.changeDistribution || '无' }}</el-descriptions-item>
<el-descriptions-item label="摘要" :span="2">{{ cycleDetailRow.cycleSummary || '无' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="cycleDetailVisible = false">关闭</el-button>
</template>
</el-dialog>
</el-tab-pane> </el-tab-pane>
<!-- 3) Raw --> <!-- 3) Raw -->
@ -172,20 +153,9 @@
<el-table :data="rawList" border stripe v-loading="rawLoading" style="width: 100%"> <el-table :data="rawList" border stripe v-loading="rawLoading" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="requestTime" label="请求时间" width="170" /> <el-table-column prop="logTime" label="时间" width="170" />
<el-table-column prop="responseDuration" label="耗时(ms)" width="100" /> <el-table-column prop="sourceAddress" label="地址" />
<el-table-column label="成功" width="80" align="center"> <el-table-column prop="contentPreview" label="内容摘要" show-overflow-tooltip />
<template #default="{ row }">
<el-tag :type="row.isSuccess ? 'success' : 'danger'" size="small">{{ row.isSuccess ? '成功' : '失败' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="statusCode" label="状态码" width="90" />
<el-table-column prop="errorMessage" label="错误信息" show-overflow-tooltip />
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-button type="text" @click="viewRawDetail(row)"></el-button>
</template>
</el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
@ -196,30 +166,6 @@
background background
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
/> />
<!-- 原始数据详情弹窗 -->
<el-dialog v-model="rawDetailVisible" title="原始采集数据详情" width="860px" destroy-on-close>
<el-descriptions :column="2" border v-if="rawDetail">
<el-descriptions-item label="ID">{{ rawDetail.id }}</el-descriptions-item>
<el-descriptions-item label="采集地址ID">{{ rawDetail.collectAddressId }}</el-descriptions-item>
<el-descriptions-item label="请求时间">{{ rawDetail.requestTime }}</el-descriptions-item>
<el-descriptions-item label="响应时间">{{ rawDetail.responseTime }}</el-descriptions-item>
<el-descriptions-item label="响应耗时">{{ rawDetail.responseDuration }} ms</el-descriptions-item>
<el-descriptions-item label="状态码">{{ rawDetail.statusCode }}</el-descriptions-item>
<el-descriptions-item label="是否成功">
<el-tag :type="rawDetail.isSuccess ? 'success' : 'danger'" size="small">{{ rawDetail.isSuccess ? '成功' : '失败' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ rawDetail.createdAt }}</el-descriptions-item>
<el-descriptions-item label="错误信息" :span="2">{{ rawDetail.errorMessage || '无' }}</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 16px">
<div style="font-weight: bold; margin-bottom: 8px">原始JSON</div>
<el-input type="textarea" :rows="16" readonly :model-value="formatJson(rawDetail?.rawJson)" style="font-family: monospace" />
</div>
<template #footer>
<el-button @click="rawDetailVisible = false">关闭</el-button>
</template>
</el-dialog>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
@ -355,13 +301,10 @@ function resetCycle() {
} }
function viewCycle(row: CollectCycle) { function viewCycle(row: CollectCycle) {
cycleDetailRow.value = row //
cycleDetailVisible.value = true ElMessage.info(`周期 ${row.cycleTime} 区间分析完成,共 ${row.totalMachines} 台机床`)
} }
const cycleDetailRow = ref<CollectCycle | null>(null)
const cycleDetailVisible = ref(false)
watch(() => cyclePage.page + cyclePage.pageSize, loadCycles) watch(() => cyclePage.page + cyclePage.pageSize, loadCycles)
// //
onMounted(() => { onMounted(() => {
@ -377,28 +320,7 @@ const rawList = ref<CollectRaw[]>([])
const rawLoading = ref(false) const rawLoading = ref(false)
const rawPage = reactive({ page: 1, pageSize: 20, total: 0 }) const rawPage = reactive({ page: 1, pageSize: 20, total: 0 })
const rawQuery = reactive({ dateRange: null as string[] | null, addressId: undefined as number | undefined }) const rawQuery = reactive({ dateRange: null as string[] | null, addressId: undefined as number | undefined })
const rawDetailVisible = ref(false) const rawURLList = ref<string[]>([])
const rawDetail = ref<CollectRaw | null>(null)
async function viewRawDetail(row: CollectRaw) {
rawDetailVisible.value = true
rawDetail.value = null
try {
const res = await request.get<CollectRaw>(`/admin/collect-log/raw/${row.id}`)
rawDetail.value = res.data as CollectRaw
} catch {
rawDetail.value = row
}
}
function formatJson(json: string | undefined | null): string {
if (!json) return ''
try {
return JSON.stringify(JSON.parse(json), null, 2)
} catch {
return json
}
}
async function loadRaw() { async function loadRaw() {
rawLoading.value = true rawLoading.value = true
@ -423,8 +345,6 @@ function resetRaw() {
loadRaw() loadRaw()
} }
watch(() => [rawPage.page, rawPage.pageSize], () => loadRaw())
onMounted(() => { onMounted(() => {
loadRaw() loadRaw()
}) })
@ -456,6 +376,8 @@ async function loadMachines() {
} }
// -------------- Helpers -------------- // -------------- Helpers --------------
// tag/label
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

@ -33,7 +33,7 @@
<div class="stat-card"> <div class="stat-card">
<div class="stat-label"> <div class="stat-label">
采集服务 采集服务
<el-tooltip content="数据采集服务的运行状态。点击暂停可暂停采集引擎,需手动启动/停止Windows服务。" placement="top"> <el-tooltip content="数据采集服务的运行状态。服务每30秒向系统上报一次心跳超过90秒未上报则判定为停止。" placement="top">
<span class="info-icon"></span> <span class="info-icon"></span>
</el-tooltip> </el-tooltip>
</div> </div>
@ -42,12 +42,11 @@
{{ collectorStatusText }} {{ collectorStatusText }}
</el-tag> </el-tag>
</div> </div>
<div class="stat-sub" v-if="collectorStatus.status === 'running'"> {{ formatUptime(collectorStatus.uptimeSeconds) }}</div> <div class="stat-sub" v-if="collectorStatus.serviceStatus === 'Running' && collectorStatus.status === 'running'"> {{ formatUptime(collectorStatus.uptimeSeconds) }}</div>
</div> </div>
<div class="collector-actions"> <div class="collector-actions">
<el-button v-if="collectorStatus.status === 'stopped' || collectorStatus.status === 'not_installed'" size="small" type="success" disabled>需手动启动服务</el-button> <el-button v-if="collectorStatus.serviceStatus !== 'Running'" size="small" type="success" :loading="startLoading" @click="startCollector"></el-button>
<el-button v-if="collectorStatus.status === 'paused' || collectorStatus.status === 'timeout'" size="small" type="success" :loading="startLoading" @click="startCollector"></el-button> <el-button v-if="collectorStatus.status === 'running'" size="small" type="danger" :loading="stopLoading" @click="stopCollector"></el-button>
<el-button v-if="collectorStatus.status === 'running'" size="small" type="danger" :loading="stopLoading" @click="stopCollector"></el-button>
<el-button size="small" type="warning" :loading="refreshLoading" @click="refreshCollectorConfig"></el-button> <el-button size="small" type="warning" :loading="refreshLoading" @click="refreshCollectorConfig"></el-button>
</div> </div>
</el-card> </el-card>
@ -335,7 +334,7 @@ const workshopUnitLabel = computed(() => workshopDays.value > 1 ? '日均' : '
function getDateRange(type: DateType, customRange?: [string, string]): { startDate: string; endDate: string } { function getDateRange(type: DateType, customRange?: [string, string]): { startDate: string; endDate: string } {
const today = new Date() const today = new Date()
const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` const fmt = (d: Date) => d.toISOString().slice(0, 10)
const endDate = fmt(today) const endDate = fmt(today)
switch (type) { switch (type) {
case 'today': return { startDate: endDate, endDate } case 'today': return { startDate: endDate, endDate }
@ -403,25 +402,25 @@ function formatNumber(val: number | undefined | null): string {
return Number(val).toFixed(2) return Number(val).toFixed(2)
} }
// 5 // + Windows
const collectorTagType = computed(() => { const collectorTagType = computed(() => {
const { status } = collectorStatus.value const { serviceStatus, status } = collectorStatus.value
if (status === 'running') return 'success' if (serviceStatus === 'Running' && status === 'running') return 'success'
if (status === 'paused') return 'info' if (serviceStatus === 'Running' && status !== 'running') return 'warning' //
if (status === 'timeout') return 'warning' if (serviceStatus === 'NotInstalled') return 'danger'
if (status === 'stopped') return 'info' if (serviceStatus === 'StartFailed') return 'danger'
if (status === 'not_installed') return 'danger'
return 'warning' return 'warning'
}) })
const collectorStatusText = computed(() => { const collectorStatusText = computed(() => {
const { status } = collectorStatus.value const { serviceStatus, status } = collectorStatus.value
if (status === 'running') return '运行中' if (serviceStatus === 'Running' && status === 'running') return '运行中'
if (status === 'paused') return '已暂停' if (serviceStatus === 'Running' && status !== 'running') return '心跳超时'
if (status === 'timeout') return '心跳超时' if (serviceStatus === 'NotInstalled') return '未安装'
if (status === 'stopped') return '已停止' if (serviceStatus === 'Stopped') return '已停止'
if (status === 'not_installed') return '未安装' if (serviceStatus === 'Starting') return '启动中'
return status || '-' if (serviceStatus === 'StartFailed') return '启动失败'
return serviceStatus || '-'
}) })
function alertTypeTag(type: string): string { function alertTypeTag(type: string): string {
@ -444,7 +443,7 @@ function initWorkshopChart() {
trigger: 'axis', trigger: 'axis',
formatter: (params: any) => { formatter: (params: any) => {
const d = workshopData.value[params[0].dataIndex] const d = workshopData.value[params[0].dataIndex]
return `${d.workshopName}<br/>${unitLabel}产量: ${Number(params[0].value).toFixed(2)} ${unit}<br/>总产量: ${d.quantity} 件<br/>机床数: ${d.machineCount}` return `${d.workshopName}<br/>${unitLabel}产量: ${params[0].value} ${unit}<br/>总产量: ${d.quantity} 件<br/>机床数: ${d.machineCount}`
}, },
}, },
grid: { left: 60, right: 20, top: 20, bottom: 30 }, grid: { left: 60, right: 20, top: 20, bottom: 30 },
@ -453,7 +452,7 @@ function initWorkshopChart() {
series: [{ series: [{
type: 'bar', data: workshopData.value.map(i => i.avgQuantity), type: 'bar', data: workshopData.value.map(i => i.avgQuantity),
itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] }, barWidth: '40%', itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] }, barWidth: '40%',
label: { show: true, position: 'top', formatter: (p: any) => `${Number(p.value).toFixed(2)} ${unit}`, fontSize: 12 }, label: { show: true, position: 'top', formatter: `{c} ${unit}`, fontSize: 12 },
}], }],
}) })
} }

@ -48,7 +48,7 @@
<el-table-column prop="ipAddress" label="IP地址" /> <el-table-column prop="ipAddress" label="IP地址" />
<el-table-column label="在线状态" align="center"> <el-table-column label="在线状态" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.isOnline ? 'success' : 'info'" size="small">{{ row.isOnline ? '在线' : '离线' }}{{ row.lastPingLatency != null ? `-${row.lastPingLatency}ms` : '' }}</el-tag> <el-tag :type="row.isOnline ? 'success' : 'info'" size="small">{{ row.isOnline ? '在线' : '离线' }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" align="center"> <el-table-column label="状态" align="center">

@ -216,7 +216,6 @@ namespace CncCollector.Core
/// <summary> /// <summary>
/// Ping每台机床的IP地址更新各自的在线状态并行执行 /// Ping每台机床的IP地址更新各自的在线状态并行执行
/// 每台机床连续Ping 4次取平均值作为延迟
/// </summary> /// </summary>
private void PingAllMachines() private void PingAllMachines()
{ {
@ -233,33 +232,27 @@ namespace CncCollector.Core
if (machines.Count == 0) return; if (machines.Count == 0) return;
// 并行Ping所有机床每台4次取平均超时2秒/次 // 并行Ping所有机床超时2秒/台
var results = new ConcurrentDictionary<int, int>(); // machineId → 平均延迟(ms)-1表示离线 var results = new ConcurrentDictionary<int, bool>();
var tasks = machines.Select(m => Task.Run(() => var tasks = machines.Select(m => Task.Run(() =>
{ {
results[m.Id] = PingHostAvg(m.Ip, 4); results[m.Id] = PingHost(m.Ip);
})).ToArray(); })).ToArray();
Task.WaitAll(tasks, Math.Min(machines.Count * 12000, 120000)); Task.WaitAll(tasks, Math.Min(machines.Count * 3000, 30000));
// 按在线/离线分组 // 按在线/离线分组批量更新
var onlineIds = results.Where(kv => kv.Value >= 0).Select(kv => kv.Key).ToList(); var onlineIds = results.Where(kv => kv.Value).Select(kv => kv.Key).ToList();
var offlineIds = results.Where(kv => kv.Value < 0).Select(kv => kv.Key).ToList(); var offlineIds = results.Where(kv => !kv.Value).Select(kv => kv.Key).ToList();
using (var conn = new MySqlConnection(_businessConnStr)) using (var conn = new MySqlConnection(_businessConnStr))
{ {
// 只更新在线机床的 last_ping_time 和 last_ping_latency if (onlineIds.Count > 0)
foreach (var kv in results.Where(kv => kv.Value >= 0)) conn.Execute(@"UPDATE cnc_machine SET last_ping_time = NOW(), updated_at = NOW() WHERE id IN @Ids",
{ new { Ids = onlineIds });
conn.Execute(@"UPDATE cnc_machine SET last_ping_time = NOW(), last_ping_latency = @Latency, updated_at = NOW() WHERE id = @Id",
new { Id = kv.Key, Latency = kv.Value });
}
// 离线机床:不更新 last_ping_time保留上次延迟值
if (offlineIds.Count > 0) if (offlineIds.Count > 0)
{ conn.Execute(@"UPDATE cnc_machine SET last_ping_time = NOW(), updated_at = NOW() WHERE id IN @Ids",
conn.Execute(@"UPDATE cnc_machine SET updated_at = NOW() WHERE id IN @Ids",
new { Ids = offlineIds }); new { Ids = offlineIds });
} }
}
_log.Info($"Ping完成地址={_address.Name}):在线{onlineIds.Count}台,离线{offlineIds.Count}台"); _log.Info($"Ping完成地址={_address.Name}):在线{onlineIds.Count}台,离线{offlineIds.Count}台");
} }
@ -270,39 +263,22 @@ namespace CncCollector.Core
} }
/// <summary> /// <summary>
/// Ping指定主机地址连续ping count次取平均延迟(ms)。离线返回-1 /// Ping指定主机地址超时2秒
/// </summary> /// </summary>
private int PingHostAvg(string host, int count = 4) private bool PingHost(string host)
{ {
try try
{ {
long totalMs = 0;
int successCount = 0;
using (var ping = new Ping()) using (var ping = new Ping())
{
for (int i = 0; i < count; i++)
{
try
{ {
var reply = ping.Send(host, 2000); var reply = ping.Send(host, 2000);
if (reply.Status == IPStatus.Success) return reply.Status == IPStatus.Success;
{
totalMs += reply.RoundtripTime;
successCount++;
}
}
catch
{
// 单次失败忽略
}
}
} }
return successCount > 0 ? (int)(totalMs / successCount) : -1;
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.Debug($"Ping失败主机={host}: {ex.Message}"); _log.Debug($"Ping失败主机={host}: {ex.Message}");
return -1; return false;
} }
} }

@ -119,11 +119,8 @@ namespace CncCollector.Core
using (var conn = new MySqlConnection(_connectionString)) using (var conn = new MySqlConnection(_connectionString))
{ {
// 结账所有活跃段is_settled=0 且 end_time IS NULL // 结账所有活跃段is_settled=0 且 end_time IS NULL
// 正确计算quantity保留当前end_part_count用end-start计算产量
conn.Execute(@"UPDATE cnc_production_segment conn.Execute(@"UPDATE cnc_production_segment
SET end_time = NOW(), SET end_time = NOW(), end_part_count = start_part_count, quantity = 0,
end_part_count = COALESCE(end_part_count, start_part_count),
quantity = GREATEST(0, COALESCE(end_part_count, start_part_count) - start_part_count),
close_reason = @Reason, is_settled = 1, updated_at = NOW() close_reason = @Reason, is_settled = 1, updated_at = NOW()
WHERE is_settled = 0 AND end_time IS NULL", WHERE is_settled = 0 AND end_time IS NULL",
new { Reason = SegmentCloseReason.ServiceStop }); new { Reason = SegmentCloseReason.ServiceStop });

@ -18,8 +18,6 @@ namespace CncModels.Dto.Machine
public string IpAddress { get; set; } public string IpAddress { get; set; }
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
public bool IsOnline { get; set; } public bool IsOnline { get; set; }
/// <summary>最近Ping延迟(ms)</summary>
public int? LastPingLatency { get; set; }
public int? WorkerId { get; set; } public int? WorkerId { get; set; }
public string WorkerName { get; set; } public string WorkerName { get; set; }
public string LastProgramName { get; set; } public string LastProgramName { get; set; }

@ -18,8 +18,6 @@ namespace CncModels.Dto.Machine
public string IpAddress { get; set; } public string IpAddress { get; set; }
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
public bool IsOnline { get; set; } public bool IsOnline { get; set; }
/// <summary>最近Ping延迟(ms)</summary>
public int? LastPingLatency { get; set; }
public int? WorkerId { get; set; } public int? WorkerId { get; set; }
public string WorkerName { get; set; } public string WorkerName { get; set; }
public string LastProgramName { get; set; } public string LastProgramName { get; set; }

@ -34,9 +34,6 @@ namespace CncModels.Entity
/// <summary>最近Ping时间在线状态由 last_ping_time 实时计算)</summary> /// <summary>最近Ping时间在线状态由 last_ping_time 实时计算)</summary>
public DateTime? LastPingTime { get; set; } public DateTime? LastPingTime { get; set; }
/// <summary>最近Ping延迟(ms)4次平均值</summary>
public int? LastPingLatency { get; set; }
/// <summary>最近采集时间</summary> /// <summary>最近采集时间</summary>
public DateTime? LastCollectTime { get; set; } public DateTime? LastCollectTime { get; set; }

@ -44,11 +44,12 @@ namespace CncRepository.Impl.Dashboard
) all_days"; ) all_days";
/// <summary>汇总卡片数据</summary> /// <summary>汇总卡片数据</summary>
public DashboardSummaryResponse GetSummary() public DashboardSummaryResponse GetSummary(int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
var onlineCount = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL 20 SECOND"); var onlineCount = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND",
new { OnlineTimeout = onlineTimeout });
var totalMachines = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1"); var totalMachines = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1");
// 今日总产量:直接从产量分段实时计算(今日一定没有日终汇总) // 今日总产量:直接从产量分段实时计算(今日一定没有日终汇总)
var todayProduction = conn.ExecuteScalar<int>(@" var todayProduction = conn.ExecuteScalar<int>(@"
@ -94,7 +95,7 @@ namespace CncRepository.Impl.Dashboard
ELSE COALESCE(SUM(ad.day_quantity), 0) / (DATEDIFF(@EndDate, @StartDate) + 1) / NULLIF(COUNT(DISTINCT m.id), 0) ELSE COALESCE(SUM(ad.day_quantity), 0) / (DATEDIFF(@EndDate, @StartDate) + 1) / NULLIF(COUNT(DISTINCT m.id), 0)
END AS AvgQuantity END AS AvgQuantity
FROM cnc_workshop w FROM cnc_workshop w
LEFT JOIN cnc_machine m ON m.workshop_id = w.id AND m.is_enabled = 1 LEFT JOIN cnc_machine m ON m.workshop_id = w.id
LEFT JOIN ( LEFT JOIN (
SELECT machine_id, production_date, day_quantity FROM ( SELECT machine_id, production_date, day_quantity FROM (
SELECT dp.machine_id, dp.production_date, SUM(dp.total_quantity) AS day_quantity SELECT dp.machine_id, dp.production_date, SUM(dp.total_quantity) AS day_quantity
@ -120,7 +121,7 @@ namespace CncRepository.Impl.Dashboard
} }
/// <summary>机床排行</summary> /// <summary>机床排行</summary>
public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc") public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, int onlineTimeout = 300, string sortOrder = "desc")
{ {
// 排序方向白名单校验防止SQL注入 // 排序方向白名单校验防止SQL注入
var orderBy = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC"; var orderBy = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
@ -130,7 +131,7 @@ namespace CncRepository.Impl.Dashboard
SELECT m.id AS MachineId, SELECT m.id AS MachineId,
m.name AS MachineName, m.name AS MachineName,
COALESCE(SUM(ad.day_quantity), 0) AS Quantity, COALESCE(SUM(ad.day_quantity), 0) AS Quantity,
(CASE WHEN m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL 20 SECOND THEN 1 ELSE 0 END) AS Status, (CASE WHEN m.is_enabled = 1 AND m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND THEN 1 ELSE 0 END) AS Status,
(SELECT seg.program_name FROM cnc_production_segment seg (SELECT seg.program_name FROM cnc_production_segment seg
WHERE seg.machine_id = m.id AND seg.production_date = CURDATE() WHERE seg.machine_id = m.id AND seg.production_date = CURDATE()
ORDER BY seg.id DESC LIMIT 1) AS Program ORDER BY seg.id DESC LIMIT 1) AS Program
@ -157,7 +158,7 @@ namespace CncRepository.Impl.Dashboard
GROUP BY m.id, m.name, m.is_enabled, m.last_ping_time GROUP BY m.id, m.name, m.is_enabled, m.last_ping_time
ORDER BY Quantity {orderBy} ORDER BY Quantity {orderBy}
LIMIT @Top"; LIMIT @Top";
var rows = conn.Query<MachineRankResponse>(sql, new { StartDate = startDate, EndDate = endDate, Top = top }).ToList(); var rows = conn.Query<MachineRankResponse>(sql, new { StartDate = startDate, EndDate = endDate, Top = top, OnlineTimeout = onlineTimeout }).ToList();
// 填充排名 // 填充排名
for (int i = 0; i < rows.Count; i++) rows[i].Rank = i + 1; for (int i = 0; i < rows.Count; i++) rows[i].Rank = i + 1;
return rows; return rows;
@ -238,12 +239,14 @@ namespace CncRepository.Impl.Dashboard
} }
/// <summary>机床状态分布</summary> /// <summary>机床状态分布</summary>
public object GetMachineStatusDistribution() public object GetMachineStatusDistribution(int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
var online = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL 20 SECOND"); var online = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND",
var offline = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND (last_ping_time IS NULL OR last_ping_time < NOW() - INTERVAL 20 SECOND)"); new { OnlineTimeout = onlineTimeout });
var offline = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND (last_ping_time IS NULL OR last_ping_time < NOW() - INTERVAL @OnlineTimeout SECOND)",
new { OnlineTimeout = onlineTimeout });
var disabled = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 0"); var disabled = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 0");
return new { online, offline, disabled }; return new { online, offline, disabled };
} }

@ -50,19 +50,6 @@ namespace CncRepository.Impl.Log
} }
} }
/// <summary>分页获取所有原始记录</summary>
public PagedResult<CollectRaw> GetAll(int page, int pageSize)
{
using (var conn = CreateConnection())
{
int skip = Math.Max(0, (page - 1) * pageSize);
var items = conn.Query<CollectRaw>($"SELECT {SelectColumns} FROM log_collect_raw ORDER BY created_at DESC LIMIT @PageSize OFFSET @Skip", new { PageSize = pageSize, Skip = skip }).AsList();
int total = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM log_collect_raw");
return new PagedResult<CollectRaw> { Items = items.ToList(), Total = total, Page = page, PageSize = pageSize };
}
}
/// <summary>写入原始采集记录</summary> /// <summary>写入原始采集记录</summary>
public long Create(CollectRaw entity) public long Create(CollectRaw entity)
{ {

@ -17,22 +17,22 @@ namespace CncRepository.Impl
public MachineRepository(string connectionString) : base(connectionString) { } public MachineRepository(string connectionString) : base(connectionString) { }
/// <summary>机床SELECT列映射模板snake_case列名 → PascalCase属性名</summary> /// <summary>机床SELECT列映射模板snake_case列名 → PascalCase属性名</summary>
/// <summary>在线判断SQL片段last_ping_time在20秒内视为在线</summary> /// <summary>在线判断SQL片段已启用且最近Ping在超时阈值内视为在线。参数 @OnlineTimeout</summary>
private const string OnlineExpr = "(CASE WHEN last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL 20 SECOND THEN 1 ELSE 0 END)"; private const string OnlineExpr = "(CASE WHEN is_enabled = 1 AND last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND THEN 1 ELSE 0 END)";
private const string SelectColumns = @"id as Id, device_code as DeviceCode, name as Name, workshop_id as WorkshopId, collect_address_id as CollectAddressId, ip_address as IpAddress, brand_id as BrandId, is_enabled as IsEnabled, {0} as IsOnline, last_ping_time as LastPingTime, last_ping_latency as LastPingLatency, last_collect_time as LastCollectTime, last_device_status as LastDeviceStatus, last_run_status as LastRunStatus, last_program_name as LastProgramName, last_part_count as LastPartCount, last_operate_mode as LastOperateMode, last_machining_status as LastMachiningStatus, created_at as CreatedAt, updated_at as UpdatedAt"; private const string SelectColumns = @"id as Id, device_code as DeviceCode, name as Name, workshop_id as WorkshopId, collect_address_id as CollectAddressId, ip_address as IpAddress, brand_id as BrandId, is_enabled as IsEnabled, {0} as IsOnline, last_ping_time as LastPingTime, last_collect_time as LastCollectTime, last_device_status as LastDeviceStatus, last_run_status as LastRunStatus, last_program_name as LastProgramName, last_part_count as LastPartCount, last_operate_mode as LastOperateMode, last_machining_status as LastMachiningStatus, created_at as CreatedAt, updated_at as UpdatedAt";
public Machine GetById(int id) public Machine GetById(int id, int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
var cols = string.Format(SelectColumns, OnlineExpr); var cols = string.Format(SelectColumns, OnlineExpr);
var sql = $"SELECT {cols} FROM cnc_machine WHERE id = @Id"; var sql = $"SELECT {cols} FROM cnc_machine WHERE id = @Id";
return conn.QuerySingleOrDefault<Machine>(sql, new { Id = id }); return conn.QuerySingleOrDefault<Machine>(sql, new { Id = id, OnlineTimeout = onlineTimeout });
} }
} }
public MachineDetailResponse GetDetailById(int id) public MachineDetailResponse GetDetailById(int id, int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
@ -42,8 +42,7 @@ namespace CncRepository.Impl
m.brand_id as BrandId, b.brand_name as BrandName, m.brand_id as BrandId, b.brand_name as BrandName,
m.ip_address as IpAddress, m.ip_address as IpAddress,
m.is_enabled as IsEnabled, m.is_enabled as IsEnabled,
(CASE WHEN m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL 20 SECOND THEN 1 ELSE 0 END) as IsOnline, (CASE WHEN m.is_enabled = 1 AND m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND THEN 1 ELSE 0 END) as IsOnline,
m.last_ping_latency as LastPingLatency,
w.id as WorkerId, w.name as WorkerName, w.id as WorkerId, w.name as WorkerName,
m.last_program_name as LastProgramName, m.last_collect_time as LastCollectTime m.last_program_name as LastProgramName, m.last_collect_time as LastCollectTime
FROM cnc_machine m FROM cnc_machine m
@ -52,16 +51,17 @@ namespace CncRepository.Impl
LEFT JOIN cnc_worker_machine wm ON m.id = wm.machine_id LEFT JOIN cnc_worker_machine wm ON m.id = wm.machine_id
LEFT JOIN cnc_worker w ON wm.worker_id = w.id LEFT JOIN cnc_worker w ON wm.worker_id = w.id
WHERE m.id = @Id"; WHERE m.id = @Id";
return conn.QuerySingleOrDefault<MachineDetailResponse>(sql, new { Id = id }); return conn.QuerySingleOrDefault<MachineDetailResponse>(sql, new { Id = id, OnlineTimeout = onlineTimeout });
} }
} }
public PagedResult<MachineListItem> GetList(MachineQuery query) public PagedResult<MachineListItem> GetList(MachineQuery query, int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
var where = " WHERE 1=1"; var where = " WHERE 1=1";
var p = new DynamicParameters(); var p = new DynamicParameters();
p.Add("OnlineTimeout", onlineTimeout);
if (!string.IsNullOrWhiteSpace(query.Keyword)) if (!string.IsNullOrWhiteSpace(query.Keyword))
{ {
where += " AND (m.name LIKE @Keyword OR m.device_code LIKE @Keyword)"; where += " AND (m.name LIKE @Keyword OR m.device_code LIKE @Keyword)";
@ -75,9 +75,9 @@ namespace CncRepository.Impl
if (query.IsOnline.HasValue) if (query.IsOnline.HasValue)
{ {
if (query.IsOnline.Value == 1) if (query.IsOnline.Value == 1)
where += " AND m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL 20 SECOND"; where += " AND m.is_enabled = 1 AND m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND";
else else
where += " AND (m.last_ping_time IS NULL OR m.last_ping_time < NOW() - INTERVAL 20 SECOND)"; where += " AND (m.is_enabled = 0 OR m.last_ping_time IS NULL OR m.last_ping_time < NOW() - INTERVAL @OnlineTimeout SECOND)";
} }
if (query.BrandId.HasValue) if (query.BrandId.HasValue)
{ {
@ -87,8 +87,7 @@ namespace CncRepository.Impl
var limit = query.PageSize; var limit = query.PageSize;
var offset = query.Offset; var offset = query.Offset;
var sql = @"SELECT m.id as Id, m.device_code as DeviceCode, m.name as Name, m.workshop_id as WorkshopId, ws.name as WorkshopName, m.collect_address_id as CollectAddressId, m.brand_id as BrandId, b.brand_name as BrandName, m.ip_address as IpAddress, m.is_enabled as IsEnabled, var sql = @"SELECT m.id as Id, m.device_code as DeviceCode, m.name as Name, m.workshop_id as WorkshopId, ws.name as WorkshopName, m.collect_address_id as CollectAddressId, m.brand_id as BrandId, b.brand_name as BrandName, m.ip_address as IpAddress, m.is_enabled as IsEnabled,
(CASE WHEN m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL 20 SECOND THEN 1 ELSE 0 END) as IsOnline, (CASE WHEN m.is_enabled = 1 AND m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND THEN 1 ELSE 0 END) as IsOnline,
m.last_ping_latency as LastPingLatency,
m.last_program_name as LastProgramName, m.last_collect_time as LastCollectTime, w.id as WorkerId, w.name as WorkerName m.last_program_name as LastProgramName, m.last_collect_time as LastCollectTime, w.id as WorkerId, w.name as WorkerName
FROM cnc_machine m FROM cnc_machine m
LEFT JOIN cnc_workshop ws ON m.workshop_id = ws.id LEFT JOIN cnc_workshop ws ON m.workshop_id = ws.id
@ -158,33 +157,33 @@ namespace CncRepository.Impl
} }
} }
public Machine GetByDeviceCode(string deviceCode) public Machine GetByDeviceCode(string deviceCode, int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
var cols = string.Format(SelectColumns, OnlineExpr); var cols = string.Format(SelectColumns, OnlineExpr);
var sql = $"SELECT {cols} FROM cnc_machine WHERE device_code = @DeviceCode"; var sql = $"SELECT {cols} FROM cnc_machine WHERE device_code = @DeviceCode";
return conn.QuerySingleOrDefault<Machine>(sql, new { DeviceCode = deviceCode }); return conn.QuerySingleOrDefault<Machine>(sql, new { DeviceCode = deviceCode, OnlineTimeout = onlineTimeout });
} }
} }
public List<Machine> GetEnabledByAddressId(int collectAddressId) public List<Machine> GetEnabledByAddressId(int collectAddressId, int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
var cols = string.Format(SelectColumns, OnlineExpr); var cols = string.Format(SelectColumns, OnlineExpr);
var sql = $"SELECT {cols} FROM cnc_machine WHERE collect_address_id = @CollectAddressId AND is_enabled = 1"; var sql = $"SELECT {cols} FROM cnc_machine WHERE collect_address_id = @CollectAddressId AND is_enabled = 1";
return conn.Query<Machine>(sql, new { CollectAddressId = collectAddressId }).ToList(); return conn.Query<Machine>(sql, new { CollectAddressId = collectAddressId, OnlineTimeout = onlineTimeout }).ToList();
} }
} }
public List<Machine> GetEnabledOnline() public List<Machine> GetEnabledOnline(int onlineTimeout = 300)
{ {
using (var conn = CreateConnection()) using (var conn = CreateConnection())
{ {
var cols = string.Format(SelectColumns, OnlineExpr); var cols = string.Format(SelectColumns, OnlineExpr);
var sql = $"SELECT {cols} FROM cnc_machine WHERE is_enabled = 1 AND last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL 20 SECOND"; var sql = $"SELECT {cols} FROM cnc_machine WHERE is_enabled = 1 AND last_ping_time IS NOT NULL AND last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND";
return conn.Query<Machine>(sql).ToList(); return conn.Query<Machine>(sql, new { OnlineTimeout = onlineTimeout }).ToList();
} }
} }

@ -13,8 +13,6 @@ namespace CncRepository.Interface
PagedResult<CollectRaw> GetByAddressId(int collectAddressId, int page, int pageSize); PagedResult<CollectRaw> GetByAddressId(int collectAddressId, int page, int pageSize);
PagedResult<CollectRaw> GetAll(int page, int pageSize);
CollectRaw GetLatestByAddressId(int collectAddressId); CollectRaw GetLatestByAddressId(int collectAddressId);
long Create(CollectRaw entity); long Create(CollectRaw entity);

@ -9,17 +9,17 @@ namespace CncRepository.Interface
/// </summary> /// </summary>
public interface IDashboardRepository public interface IDashboardRepository
{ {
DashboardSummaryResponse GetSummary(); DashboardSummaryResponse GetSummary(int onlineTimeout = 300);
List<WorkshopProductionResponse> GetWorkshopProduction(DateTime startDate, DateTime endDate); List<WorkshopProductionResponse> GetWorkshopProduction(DateTime startDate, DateTime endDate);
List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc"); List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, int onlineTimeout = 300, string sortOrder = "desc");
List<WorkerRankResponse> GetWorkerRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc"); List<WorkerRankResponse> GetWorkerRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc");
List<dynamic> GetProductionTrend(int days); List<dynamic> GetProductionTrend(int days);
object GetMachineStatusDistribution(); object GetMachineStatusDistribution(int onlineTimeout = 300);
List<AlertListItem> GetRecentAlerts(int count); List<AlertListItem> GetRecentAlerts(int count);
} }

@ -10,17 +10,17 @@ namespace CncRepository.Interface
/// </summary> /// </summary>
public interface IMachineRepository public interface IMachineRepository
{ {
Machine GetById(int id); Machine GetById(int id, int onlineTimeout = 300);
MachineDetailResponse GetDetailById(int id); MachineDetailResponse GetDetailById(int id, int onlineTimeout = 300);
PagedResult<MachineListItem> GetList(MachineQuery query); PagedResult<MachineListItem> GetList(MachineQuery query, int onlineTimeout = 300);
int Create(Machine entity); int Create(Machine entity);
bool Update(Machine entity); bool Update(Machine entity);
bool Delete(int id); bool Delete(int id);
int BatchDelete(List<int> ids); int BatchDelete(List<int> ids);
bool ToggleEnabled(int id); bool ToggleEnabled(int id);
Machine GetByDeviceCode(string deviceCode); Machine GetByDeviceCode(string deviceCode, int onlineTimeout = 300);
List<Machine> GetEnabledByAddressId(int collectAddressId); List<Machine> GetEnabledByAddressId(int collectAddressId, int onlineTimeout = 300);
List<Machine> GetEnabledOnline(); List<Machine> GetEnabledOnline(int onlineTimeout = 300);
void UpdateLastCollect(int id, Machine entity); void UpdateLastCollect(int id, Machine entity);
/// <summary>设置机床所属的采集地址</summary> /// <summary>设置机床所属的采集地址</summary>
void SetCollectAddress(int machineId, int? collectAddressId); void SetCollectAddress(int machineId, int? collectAddressId);

@ -27,10 +27,18 @@ namespace CncService.Impl
_serviceChecker = serviceChecker; _serviceChecker = serviceChecker;
} }
/// <summary>从sys_config读取online_timeout默认300秒</summary>
private int GetOnlineTimeout()
{
var cfg = _sysConfigRepository.GetByKey("online_timeout");
if (cfg != null && int.TryParse(cfg.ConfigValue, out var val) && val > 0) return val;
return 300;
}
/// <inheritdoc/> /// <inheritdoc/>
public DashboardSummaryResponse GetSummary() public DashboardSummaryResponse GetSummary()
{ {
return _dashboardRepository.GetSummary(); return _dashboardRepository.GetSummary(GetOnlineTimeout());
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -46,7 +54,7 @@ namespace CncService.Impl
{ {
var s = startDate ?? DateTime.Today; var s = startDate ?? DateTime.Today;
var e = endDate ?? DateTime.Today; var e = endDate ?? DateTime.Today;
return _dashboardRepository.GetMachineRank(s, e, top, sortOrder); return _dashboardRepository.GetMachineRank(s, e, top, GetOnlineTimeout(), sortOrder);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -66,7 +74,7 @@ namespace CncService.Impl
/// <inheritdoc/> /// <inheritdoc/>
public object GetMachineStatusDistribution() public object GetMachineStatusDistribution()
{ {
return _dashboardRepository.GetMachineStatusDistribution(); return _dashboardRepository.GetMachineStatusDistribution(GetOnlineTimeout());
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -75,20 +83,12 @@ namespace CncService.Impl
return _dashboardRepository.GetRecentAlerts(count); return _dashboardRepository.GetRecentAlerts(count);
} }
/// <summary>从sys_config读取heartbeat_interval计算心跳超时阈值间隔×3</summary>
private int GetHeartbeatTimeout()
{
var cfg = _sysConfigRepository.GetByKey("heartbeat_interval");
if (cfg != null && int.TryParse(cfg.ConfigValue, out var val) && val > 0)
return val * 3;
return 30; // 默认10秒间隔 × 3 = 30秒
}
/// <inheritdoc/> /// <inheritdoc/>
public object GetCollectorStatus() public object GetCollectorStatus()
{ {
var latest = _collectorHeartbeatRepository.GetLatest("CncCollector"); var latest = _collectorHeartbeatRepository.GetLatest("collector-service");
int heartbeatTimeoutSeconds = GetHeartbeatTimeout(); // 心跳超时阈值90秒3个心跳间隔采集服务默认每30秒上报一次
const int heartbeatTimeoutSeconds = 90;
bool heartbeatRunning = false; bool heartbeatRunning = false;
long heartbeatUptime = 0; long heartbeatUptime = 0;
@ -111,30 +111,8 @@ namespace CncService.Impl
serviceStatusText = svc.ToString(); serviceStatusText = svc.ToString();
} }
// 组合状态精确区分5种情况 // 组合状态NotInstalled -> 停止,其他根据心跳决定
string status; string status = (serviceStatusText == "NotInstalled") ? "stopped" : (heartbeatRunning ? "running" : "stopped");
if (serviceStatusText == "NotInstalled")
{
status = "not_installed";
}
else if (serviceStatusText == "Stopped" || serviceStatusText == "StartFailed")
{
status = "stopped";
}
else if (heartbeatRunning)
{
status = "running";
}
else if (latest != null && latest.Status == "stopped")
{
// 引擎主动停止暂停心跳status='stopped'
status = "paused";
}
else
{
// 服务在运行但心跳超时
status = "timeout";
}
return new { return new {
status, status,

@ -75,21 +75,8 @@ namespace CncWebApi.Controllers
[ResponseType(typeof(ApiResponse<PagedResult<CollectRaw>>))] [ResponseType(typeof(ApiResponse<PagedResult<CollectRaw>>))]
public IHttpActionResult GetRawList([FromUri] int? collectAddressId = null, [FromUri] int page = 1, [FromUri] int pageSize = 20) public IHttpActionResult GetRawList([FromUri] int? collectAddressId = null, [FromUri] int page = 1, [FromUri] int pageSize = 20)
{ {
var result = collectAddressId.HasValue var result = _rawRepository.GetByAddressId(collectAddressId ?? 0, page, pageSize);
? _rawRepository.GetByAddressId(collectAddressId.Value, page, pageSize)
: _rawRepository.GetAll(page, pageSize);
return Ok(ApiResponse<PagedResult<CollectRaw>>.Success(result)); return Ok(ApiResponse<PagedResult<CollectRaw>>.Success(result));
} }
/// <summary>获取原始采集日志详情</summary>
[HttpGet]
[Route("raw/{id:long}")]
[ResponseType(typeof(ApiResponse<CollectRaw>))]
public IHttpActionResult GetRawDetail(long id)
{
var entity = _rawRepository.GetById(id);
if (entity == null) return NotFound();
return Ok(ApiResponse<CollectRaw>.Success(entity));
}
} }
} }

@ -137,21 +137,15 @@ namespace CncWebApi.Controllers
try try
{ {
dynamic statusObj = _dashboardService.GetCollectorStatus(); dynamic statusObj = _dashboardService.GetCollectorStatus();
string status = statusObj?.status as string;
string serviceStatus = statusObj?.serviceStatus as string; string serviceStatus = statusObj?.serviceStatus as string;
if (!string.IsNullOrEmpty(serviceStatus) && string.Equals(serviceStatus, "NotInstalled", StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(serviceStatus) && string.Equals(serviceStatus, "NotInstalled", StringComparison.OrdinalIgnoreCase))
{ {
return Ok(ApiResponse<object>.Fail(40001, "采集服务未安装,请先在服务器上运行 install.ps1 安装服务")); return Ok(ApiResponse<object>.Fail(40001, "采集服务未安装,请先在服务器上运行 install.ps1 安装服务"));
} }
if (status == "running") if (!string.IsNullOrEmpty(serviceStatus) && string.Equals(serviceStatus, "Running", StringComparison.OrdinalIgnoreCase))
{ {
return Ok(ApiResponse<object>.Fail(40002, "采集服务已在运行中,无需再次启动")); return Ok(ApiResponse<object>.Fail(40002, "采集服务已在运行中,无需再次启动"));
} }
if (status == "stopped")
{
return Ok(ApiResponse<object>.Fail(40003, "采集服务已停止请手动启动Windows服务"));
}
// paused 或 timeout 状态转发到CncCollector恢复引擎
} }
catch { /* ignore status fetch errors and fallback to forwarding */ } catch { /* ignore status fetch errors and fallback to forwarding */ }

@ -12,12 +12,12 @@ namespace CncService.Tests
// Fake repositories to isolate DashboardService.GetCollectorStatus tests // Fake repositories to isolate DashboardService.GetCollectorStatus tests
public class FakeDashboardRepository : IDashboardRepository public class FakeDashboardRepository : IDashboardRepository
{ {
public DashboardSummaryResponse GetSummary() => new DashboardSummaryResponse(); public DashboardSummaryResponse GetSummary(int something) => new DashboardSummaryResponse();
public List<WorkshopProductionResponse> GetWorkshopProduction(DateTime startDate, DateTime endDate) => new List<WorkshopProductionResponse>(); public List<WorkshopProductionResponse> GetWorkshopProduction(DateTime startDate, DateTime endDate) => new List<WorkshopProductionResponse>();
public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc") => new List<MachineRankResponse>(); public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, int something, string sortOrder = "desc") => new List<MachineRankResponse>();
public List<WorkerRankResponse> GetWorkerRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc") => new List<WorkerRankResponse>(); public List<WorkerRankResponse> GetWorkerRank(DateTime startDate, DateTime endDate, int top, string sortOrder = "desc") => new List<WorkerRankResponse>();
public List<dynamic> GetProductionTrend(int days) => new List<dynamic>(); public List<dynamic> GetProductionTrend(int days) => new List<dynamic>();
public object GetMachineStatusDistribution() => new object(); public object GetMachineStatusDistribution(int something) => new object();
public List<AlertListItem> GetRecentAlerts(int count) => new List<AlertListItem>(); public List<AlertListItem> GetRecentAlerts(int count) => new List<AlertListItem>();
} }

Loading…
Cancel
Save