修复管理后台4个Bug + 改进Ping逻辑

Bug1(告警中心分页不工作): AlertPage.vue添加watch监听page.page和page.pageSize变化触发loadData

Bug2(仪表盘按钮报错): DashboardPage.vue为刷新配置/停止采集/启动采集添加ElMessage成功提示,导入ElMessage

Bug3(设备详情页数据不对): MachineService四个方法从返回空数据改为查询实际数据
- GetStatus: 从Machine entity的last_*字段获取实时状态
- GetTodayProduction: 从cnc_daily_production或cnc_production_segment查今日产量
- GetProductionTrend: 从cnc_daily_production或segment查7天趋势
- GetCollectRecords: 从cnc_collect_record查最近20条采集记录
- IMachineRepository新增4个方法声明,MachineRepository实现SQL查询

Bug4(产量报表员工筛选不正确):
- DailyProductionRepository.GetList添加WorkerId过滤(通过cnc_worker_machine关联表)
- ProductionController.GetSummary扩展参数接受startDate/endDate/machineId/workerId
- IProductionService/ProductionService.GetSummary签名同步更新

Ping逻辑改进: CollectWorker从Ping采集URL主机改为Ping每台机床IP地址
- 新增PingAllMachines()并行Ping所有机床IP,逐台更新在线状态
- 新增PingHost()执行单次ICMP Ping(超时2秒)
- 移除旧的PingAddress()(Ping URL主机)和UpdateMachineOnlineStatus()(批量更新)
main
haoliang 4 days ago
parent c2c4d15453
commit cb3a6071bd

@ -106,7 +106,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request' import request from '@/utils/request'
import type { ApiResponse, Alert, Machine } from '@/types' import type { ApiResponse, Alert, Machine } from '@/types'
@ -206,6 +206,11 @@ async function loadDrops() {
machineList.value = r.data?.items || [] machineList.value = r.data?.items || []
} }
//
watch(() => [page.page, page.pageSize], () => {
loadData()
})
onMounted(() => { onMounted(() => {
loadData() loadData()
loadDrops() loadDrops()

@ -265,6 +265,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request' import request from '@/utils/request'
import { useMockMode } from '@/composables/useMockMode' import { useMockMode } from '@/composables/useMockMode'
import echarts from '@/utils/echarts' import echarts from '@/utils/echarts'
@ -337,19 +338,19 @@ let statusPie: ECharts | null = null
async function startCollector() { async function startCollector() {
if (startLoading.value) return if (startLoading.value) return
startLoading.value = true startLoading.value = true
try { await request.post('/admin/collector/start'); await loadData() } catch { /* */ } finally { startLoading.value = false } try { await request.post('/admin/collector/start'); ElMessage.success('采集服务已启动'); await loadData() } catch { /* request拦截器已显示错误 */ } finally { startLoading.value = false }
} }
async function stopCollector() { async function stopCollector() {
if (stopLoading.value) return if (stopLoading.value) return
stopLoading.value = true stopLoading.value = true
try { await request.post('/admin/collector/stop'); await loadData() } catch { /* */ } finally { stopLoading.value = false } try { await request.post('/admin/collector/stop'); ElMessage.success('采集服务已停止'); await loadData() } catch { /* request拦截器已显示错误 */ } finally { stopLoading.value = false }
} }
async function refreshCollectorConfig() { async function refreshCollectorConfig() {
if (refreshLoading.value) return if (refreshLoading.value) return
refreshLoading.value = true refreshLoading.value = true
try { await request.post('/admin/collector/refresh'); await loadData() } catch { /* */ } finally { refreshLoading.value = false } try { await request.post('/admin/collector/refresh'); ElMessage.success('配置已刷新'); await loadData() } catch { /* request拦截器已显示错误 */ } finally { refreshLoading.value = false }
} }
function formatUptime(seconds: number): string { function formatUptime(seconds: number): string {

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Dapper; using Dapper;
using MySqlConnector; using MySqlConnector;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -118,22 +121,14 @@ namespace CncCollector.Core
} }
/// <summary> /// <summary>
/// 执行一次完整的采集周期:Ping → HTTP采集 → 解析 → 入库 /// 执行一次完整的采集周期:逐台Ping机床IP → HTTP采集 → 解析 → 入库
/// </summary> /// </summary>
private void DoCollectCycle() private void DoCollectCycle()
{ {
var requestTime = DateTime.Now; var requestTime = DateTime.Now;
// 1. Ping 检测 // 1. Ping 每台机床的IP地址更新各自的在线状态
if (!PingAddress()) PingAllMachines();
{
// Ping 失败,更新机床离线状态
UpdateMachineOnlineStatus(false);
return;
}
// Ping 成功,更新机床在线状态
UpdateMachineOnlineStatus(true);
// 2. HTTP 采集(含重试) // 2. HTTP 采集(含重试)
string rawJson = null; string rawJson = null;
@ -218,46 +213,70 @@ namespace CncCollector.Core
} }
/// <summary> /// <summary>
/// Ping 检测采集地址可达性 /// Ping每台机床的IP地址更新各自的在线状态并行执行
/// </summary> /// </summary>
private bool PingAddress() private void PingAllMachines()
{ {
try try
{ {
// 从 URL 提取主机名 // 加载此地址下所有启用的机床ID + IP地址
var uri = new Uri(_address.Url); List<(int Id, string Ip)> machines;
string host = uri.Host; using (var conn = new MySqlConnection(_businessConnStr))
{
machines = conn.Query<(int Id, string Ip)>(
"SELECT id, ip_address FROM cnc_machine WHERE collect_address_id = @AddrId AND is_enabled = 1",
new { AddrId = _address.Id }).AsList();
}
using (var ping = new Ping()) if (machines.Count == 0) return;
// 并行Ping所有机床超时2秒/台)
var results = new ConcurrentDictionary<int, bool>();
var tasks = machines.Select(m => Task.Run(() =>
{ {
var reply = ping.Send(host, 5000); results[m.Id] = PingHost(m.Ip);
return reply.Status == IPStatus.Success; })).ToArray();
Task.WaitAll(tasks, Math.Min(machines.Count * 3000, 30000));
// 按在线/离线分组批量更新
var onlineIds = results.Where(kv => kv.Value).Select(kv => kv.Key).ToList();
var offlineIds = results.Where(kv => !kv.Value).Select(kv => kv.Key).ToList();
using (var conn = new MySqlConnection(_businessConnStr))
{
if (onlineIds.Count > 0)
conn.Execute(@"UPDATE cnc_machine SET is_online = 1, last_ping_time = NOW(), updated_at = NOW() WHERE id IN @Ids",
new { Ids = onlineIds });
if (offlineIds.Count > 0)
conn.Execute(@"UPDATE cnc_machine SET is_online = 0, last_ping_time = NOW(), updated_at = NOW() WHERE id IN @Ids",
new { Ids = offlineIds });
} }
_log.Info($"Ping完成地址={_address.Name}):在线{onlineIds.Count}台,离线{offlineIds.Count}台");
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.Debug($"Ping失败地址={_address.Name}: {ex.Message}"); _log.Error($"Ping机床失败地址={_address.Name}", ex);
return false;
} }
} }
/// <summary> /// <summary>
/// 更新此地址下所有机床的在线状态 /// Ping指定主机地址超时2秒
/// </summary> /// </summary>
private void UpdateMachineOnlineStatus(bool isOnline) private bool PingHost(string host)
{ {
try try
{ {
using (var conn = new MySqlConnection(_businessConnStr)) using (var ping = new Ping())
{ {
conn.Execute(@"UPDATE cnc_machine SET is_online = @Online, last_ping_time = NOW(), updated_at = NOW() var reply = ping.Send(host, 2000);
WHERE collect_address_id = @AddrId AND is_enabled = 1", return reply.Status == IPStatus.Success;
new { Online = isOnline ? 1 : 0, AddrId = _address.Id });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.Error($"更新机床在线状态失败(地址={_address.Name}", ex); _log.Debug($"Ping失败主机={host}: {ex.Message}");
return false;
} }
} }

@ -100,6 +100,13 @@ namespace CncRepository.Impl
realtimeSql += " AND seg.machine_id = @MachineId"; realtimeSql += " AND seg.machine_id = @MachineId";
parameters.Add("MachineId", query.MachineId); parameters.Add("MachineId", query.MachineId);
} }
// 工人过滤通过worker_machine关联表
if (query?.WorkerId.HasValue == true)
{
baseSql += " AND EXISTS (SELECT 1 FROM cnc_worker_machine wm WHERE wm.machine_id = dp.machine_id AND wm.worker_id = @WorkerId)";
realtimeSql += " AND EXISTS (SELECT 1 FROM cnc_worker_machine wm WHERE wm.machine_id = seg.machine_id AND wm.worker_id = @WorkerId)";
parameters.Add("WorkerId", query.WorkerId);
}
realtimeSql += " GROUP BY seg.machine_id, seg.production_date, seg.program_name, m.name"; realtimeSql += " GROUP BY seg.machine_id, seg.production_date, seg.program_name, m.name";

@ -207,5 +207,87 @@ namespace CncRepository.Impl
new { Id = machineId, AddressId = collectAddressId }); new { Id = machineId, AddressId = collectAddressId });
} }
} }
/// <inheritdoc/>
public MachineCollectRecordItem GetLatestCollectRecord(int machineId)
{
using (var conn = CreateConnection())
{
string sql = @"SELECT DATE_FORMAT(cr.collect_time, '%Y-%m-%d %H:%i:%s') AS CollectTime,
cr.program_name AS ProgramName, cr.part_count AS PartCount, cr.run_status AS RunStatus
FROM cnc_collect_record cr
WHERE cr.machine_id = @MachineId
ORDER BY cr.collect_time DESC LIMIT 1";
return conn.QuerySingleOrDefault<MachineCollectRecordItem>(sql, new { MachineId = machineId });
}
}
/// <inheritdoc/>
public List<MachineTodayProdItem> GetTodayProduction(int machineId)
{
using (var conn = CreateConnection())
{
// 优先从已汇总的日产量表取
string dpSql = @"SELECT program_name AS ProgramName, CAST(total_quantity AS SIGNED) AS Quantity,
total_run_time AS RunTime, total_cutting_time AS CuttingTime
FROM cnc_daily_production
WHERE machine_id = @MachineId AND production_date = CURDATE()";
var dpItems = conn.Query<MachineTodayProdItem>(dpSql, new { MachineId = machineId }).AsList();
if (dpItems.Count > 0) return dpItems;
// 没有汇总数据则从分段表实时计算
string segSql = @"SELECT seg.program_name AS ProgramName,
CAST(SUM(CASE WHEN seg.is_settled=1 THEN seg.quantity
ELSE COALESCE(seg.end_part_count, seg.start_part_count) - seg.start_part_count END) AS SIGNED) AS Quantity,
NULL AS RunTime, NULL AS CuttingTime
FROM cnc_production_segment seg
WHERE seg.machine_id = @MachineId AND seg.production_date = CURDATE()
GROUP BY seg.program_name";
return conn.Query<MachineTodayProdItem>(segSql, new { MachineId = machineId }).AsList();
}
}
/// <inheritdoc/>
public List<MachineTrendItem> GetProductionTrend(int machineId, int days = 7)
{
using (var conn = CreateConnection())
{
// 优先从日产量表取近N天数据
string sql = @"SELECT DATE_FORMAT(dp.production_date, '%Y-%m-%d') AS Date,
CAST(SUM(dp.total_quantity) AS SIGNED) AS Quantity
FROM cnc_daily_production dp
WHERE dp.machine_id = @MachineId
AND dp.production_date >= DATE_SUB(CURDATE(), INTERVAL @Days DAY)
GROUP BY dp.production_date
ORDER BY dp.production_date";
var items = conn.Query<MachineTrendItem>(sql, new { MachineId = machineId, Days = days - 1 }).AsList();
if (items.Count > 0) return items;
// 没有汇总数据则从分段表实时计算
string segSql = @"SELECT DATE_FORMAT(seg.production_date, '%Y-%m-%d') AS Date,
CAST(SUM(CASE WHEN seg.is_settled=1 THEN seg.quantity
ELSE COALESCE(seg.end_part_count, seg.start_part_count) - seg.start_part_count END) AS SIGNED) AS Quantity
FROM cnc_production_segment seg
WHERE seg.machine_id = @MachineId
AND seg.production_date >= DATE_SUB(CURDATE(), INTERVAL @Days DAY)
GROUP BY seg.production_date
ORDER BY seg.production_date";
return conn.Query<MachineTrendItem>(segSql, new { MachineId = machineId, Days = days - 1 }).AsList();
}
}
/// <inheritdoc/>
public List<MachineCollectRecordItem> GetRecentCollectRecords(int machineId, int limit = 20)
{
using (var conn = CreateConnection())
{
string sql = @"SELECT DATE_FORMAT(cr.collect_time, '%Y-%m-%d %H:%i:%s') AS CollectTime,
cr.program_name AS ProgramName, cr.part_count AS PartCount, cr.run_status AS RunStatus
FROM cnc_collect_record cr
WHERE cr.machine_id = @MachineId
ORDER BY cr.collect_time DESC LIMIT @Limit";
return conn.Query<MachineCollectRecordItem>(sql, new { MachineId = machineId, Limit = limit }).AsList();
}
}
} }
} }

@ -25,5 +25,13 @@ namespace CncRepository.Interface
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);
/// <summary>获取机床最新一条采集记录(实时状态)</summary>
MachineCollectRecordItem GetLatestCollectRecord(int machineId);
/// <summary>获取机床今日产量按程序名分组</summary>
List<MachineTodayProdItem> GetTodayProduction(int machineId);
/// <summary>获取机床近N天产量趋势</summary>
List<MachineTrendItem> GetProductionTrend(int machineId, int days = 7);
/// <summary>获取机床最近N条采集记录</summary>
List<MachineCollectRecordItem> GetRecentCollectRecords(int machineId, int limit = 20);
} }
} }

@ -123,8 +123,19 @@ namespace CncService.Impl
if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID"); if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID");
var machine = _machineRepository.GetById(id); var machine = _machineRepository.GetById(id);
if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到"); if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到");
// 采集服务尚未运行,返回空状态 // 从最新采集记录获取实时状态
return new MachineStatusResponse(); var latest = _machineRepository.GetLatestCollectRecord(id);
return new MachineStatusResponse
{
ProgramName = machine.LastProgramName ?? latest?.ProgramName,
PartCount = machine.LastPartCount,
RunStatus = machine.LastRunStatus ?? latest?.RunStatus,
OperationMode = machine.LastOperateMode,
SpindleSpeedSet = null,
FeedSpeedSet = null,
SpindleSpeedActual = null,
SpindleLoad = null
};
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -133,8 +144,7 @@ namespace CncService.Impl
if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID"); if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID");
var machine = _machineRepository.GetById(id); var machine = _machineRepository.GetById(id);
if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到"); if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到");
// 采集服务尚未运行,暂无产量数据 return _machineRepository.GetTodayProduction(id);
return new List<MachineTodayProdItem>();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -143,8 +153,7 @@ namespace CncService.Impl
if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID"); if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID");
var machine = _machineRepository.GetById(id); var machine = _machineRepository.GetById(id);
if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到"); if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到");
// 采集服务尚未运行,暂无趋势数据 return _machineRepository.GetProductionTrend(id);
return new List<MachineTrendItem>();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -153,8 +162,7 @@ namespace CncService.Impl
if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID"); if (id <= 0) throw new BusinessException(ErrorCode.BadRequest, "无效的机床ID");
var machine = _machineRepository.GetById(id); var machine = _machineRepository.GetById(id);
if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到"); if (machine == null) throw new BusinessException(ErrorCode.NotFound, "机床未找到");
// 采集服务尚未运行,暂无采集记录 return _machineRepository.GetRecentCollectRecords(id);
return new List<MachineCollectRecordItem>();
} }
} }
} }

@ -36,17 +36,18 @@ namespace CncService.Impl
} }
/// <inheritdoc/> /// <inheritdoc/>
public DailySummaryResponse GetSummary(DateTime? date, int? workshopId) public DailySummaryResponse GetSummary(DateTime? startDate, DateTime? endDate, int? workshopId, int? machineId, int? workerId)
{ {
var targetDate = date ?? DateTime.Today; var s = startDate ?? DateTime.Today;
var total = _dailyProductionRepository.GetTotalByDateRange(targetDate, targetDate, workshopId); var e = endDate ?? DateTime.Today;
var total = _dailyProductionRepository.GetTotalByDateRange(s, e, workshopId);
// 如果汇总表无数据,从产量分段实时计算 // 如果汇总表无数据,从产量分段实时计算
if (total == 0) if (total == 0)
{ {
total = _productionSegmentRepository.GetTotalByDateRange(targetDate, targetDate, workshopId); total = _productionSegmentRepository.GetTotalByDateRange(s, e, workshopId);
} }
int machineCount = _productionSegmentRepository.GetActiveMachineCount(targetDate, workshopId); int machineCount = _productionSegmentRepository.GetActiveMachineCount(s, workshopId);
decimal cuttingTime = _productionSegmentRepository.GetTotalCuttingTimeByDate(targetDate); decimal cuttingTime = _productionSegmentRepository.GetTotalCuttingTimeByDate(s);
return new DailySummaryResponse return new DailySummaryResponse
{ {
TotalQuantity = (int)total, TotalQuantity = (int)total,

@ -14,7 +14,7 @@ namespace CncService.Interface
PagedResult<DailyProductionListItem> GetList(ProductionQuery query); PagedResult<DailyProductionListItem> GetList(ProductionQuery query);
/// <summary>获取日汇总统计</summary> /// <summary>获取日汇总统计</summary>
DailySummaryResponse GetSummary(DateTime? date, int? workshopId); DailySummaryResponse GetSummary(DateTime? startDate, DateTime? endDate, int? workshopId, int? machineId, int? workerId);
/// <summary>获取日期范围总产量</summary> /// <summary>获取日期范围总产量</summary>
decimal GetTotalByDateRange(DateTime startDate, DateTime endDate, int? workshopId); decimal GetTotalByDateRange(DateTime startDate, DateTime endDate, int? workshopId);

@ -43,9 +43,9 @@ namespace CncWebApi.Controllers
/// </summary> /// </summary>
[HttpGet] [HttpGet]
[Route("daily-summary")] [Route("daily-summary")]
public IHttpActionResult GetSummary(DateTime? date = null, int? workshopId = null) public IHttpActionResult GetSummary(DateTime? startDate = null, DateTime? endDate = null, int? workshopId = null, int? machineId = null, int? workerId = null)
{ {
var result = _productionService.GetSummary(date, workshopId); var result = _productionService.GetSummary(startDate, endDate, workshopId, machineId, workerId);
return Ok(ApiResponse<DailySummaryResponse>.Success(result)); return Ok(ApiResponse<DailySummaryResponse>.Success(result));
} }

@ -65,7 +65,7 @@ namespace CncService.Tests
[Fact] [Fact]
public void GetSummary__0() public void GetSummary__0()
{ {
var summary = _service.GetSummary(null, null); var summary = _service.GetSummary(null, null, null, null, null);
Assert.NotNull(summary); Assert.NotNull(summary);
Assert.Equal(0, summary.TotalQuantity); Assert.Equal(0, summary.TotalQuantity);
} }
@ -80,7 +80,7 @@ namespace CncService.Tests
TestDb.Execute(@"INSERT INTO cnc_daily_production (machine_id, production_date, program_name, total_quantity, created_at, updated_at) TestDb.Execute(@"INSERT INTO cnc_daily_production (machine_id, production_date, program_name, total_quantity, created_at, updated_at)
VALUES (1, CURDATE(), 'O0001', 150, NOW(), NOW())"); VALUES (1, CURDATE(), 'O0001', 150, NOW(), NOW())");
var summary = _service.GetSummary(DateTime.Today, null); var summary = _service.GetSummary(DateTime.Today, null, null, null, null);
Assert.Equal(150, summary.TotalQuantity); Assert.Equal(150, summary.TotalQuantity);
} }

Loading…
Cancel
Save