feat: 在线状态改为基于last_ping_time实时判断,删除is_online列;新增online_timeout配置项(默认300秒);全链路修改Repository/Service/Collector/测试

feat/windows-service-status-auto
haoliang 10 hours ago
parent 0563da73e8
commit ccdfec31bb

@ -0,0 +1,25 @@
-- ============================================================
-- 迁移脚本06: 删除is_online列 + 新增在线超时配置项
-- 幂等执行先加配置再删列IF EXISTS
-- ============================================================
-- 1. 新增系统配置项:在线超时阈值(秒)
INSERT INTO cnc_sys_config (config_key, config_value, value_type, description, updated_at)
SELECT 'online_timeout', '300', 'number', '在线超时阈值(秒)超过此时间未Ping的机床判定为离线', NOW()
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM cnc_sys_config WHERE config_key = 'online_timeout');
-- 2. 删除 is_online 列幂等IF EXISTS 在 MariaDB 10.0.2+ 支持)
-- 注意MariaDB 不支持 ALTER TABLE DROP COLUMN IF EXISTS用存储过程实现
DROP PROCEDURE IF EXISTS drop_column_if_exists;
DELIMITER //
CREATE PROCEDURE drop_column_if_exists()
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cnc_machine' AND COLUMN_NAME = 'is_online') THEN
ALTER TABLE cnc_machine DROP COLUMN is_online;
END IF;
END //
DELIMITER ;
CALL drop_column_if_exists();
DROP PROCEDURE IF EXISTS drop_column_if_exists;

@ -13,7 +13,7 @@
</el-card>
<el-card shadow="hover">
<template #header><div style="display:flex;justify-content:space-between"><span>字段映射列表</span><el-button size="small" @click="addMapping">+ </el-button></div></template>
<el-table :data="form.mappings" border stripe size="small" style="width:100%" row-class-name="mapping-row" :row-style="({row}) => row.isEnabled === 0 ? { opacity: 0.5 } : {}">
<el-table :data="form.mappings" border stripe size="small" style="width:100%" row-class-name="mapping-row" :row-style="({row}: any) => row.isEnabled === 0 ? { opacity: 0.5 } : {}">
<el-table-column label="标准字段" min-width="180"><template #default="{row}"><el-select v-model="row.standardField" style="width:100%"><el-option v-for="f in standardFields" :key="f" :label="f" :value="f" /></el-select></template></el-table-column>
<el-table-column label="字段名" min-width="140"><template #default="{row}"><el-input v-model="row.fieldName" /></template></el-table-column>
<el-table-column label="匹配方式" min-width="110"><template #default="{row}"><el-select v-model="row.matchBy" style="width:100%"><el-option label="id" value="id" /><el-option label="desc" value="desc" /></el-select></template></el-table-column>

@ -247,10 +247,10 @@ namespace CncCollector.Core
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",
conn.Execute(@"UPDATE cnc_machine SET 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",
conn.Execute(@"UPDATE cnc_machine SET last_ping_time = NOW(), updated_at = NOW() WHERE id IN @Ids",
new { Ids = offlineIds });
}
@ -309,7 +309,7 @@ namespace CncCollector.Core
// 加载此地址下的机床列表
machines = conn.Query<Machine>(
"SELECT 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, is_online 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 FROM cnc_machine WHERE collect_address_id = @AddrId AND is_enabled = 1",
"SELECT 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, 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 FROM cnc_machine WHERE collect_address_id = @AddrId AND is_enabled = 1",
new { AddrId = _address.Id }).AsList();
}

@ -31,10 +31,7 @@ namespace CncModels.Entity
/// <summary>是否启用</summary>
public int IsEnabled { get; set; }
/// <summary>是否在线</summary>
public int IsOnline { get; set; }
/// <summary>最近Ping时间</summary>
/// <summary>最近Ping时间在线状态由 last_ping_time 实时计算)</summary>
public DateTime? LastPingTime { get; set; }
/// <summary>最近采集时间</summary>

@ -44,12 +44,13 @@ namespace CncRepository.Impl.Dashboard
) all_days";
/// <summary>汇总卡片数据</summary>
public DashboardSummaryResponse GetSummary()
public DashboardSummaryResponse GetSummary(int onlineTimeout = 300)
{
using (var conn = CreateConnection())
{
var onlineCount = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine WHERE is_online = 1");
var totalMachines = conn.ExecuteScalar<int>(@"SELECT COUNT(1) FROM cnc_machine");
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 todayProduction = conn.ExecuteScalar<int>(@"
SELECT COALESCE(SUM(CASE WHEN is_settled=1 THEN quantity
@ -130,7 +131,7 @@ namespace CncRepository.Impl.Dashboard
}
/// <summary>机床排行</summary>
public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top)
public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, int onlineTimeout = 300)
{
using (var conn = CreateConnection())
{
@ -138,7 +139,7 @@ namespace CncRepository.Impl.Dashboard
SELECT m.id AS MachineId,
m.name AS MachineName,
COALESCE(SUM(ad.day_quantity), 0) AS Quantity,
CAST(m.is_online AS SIGNED) 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
WHERE seg.machine_id = m.id AND seg.production_date = CURDATE()
ORDER BY seg.id DESC LIMIT 1) AS Program
@ -162,10 +163,10 @@ namespace CncRepository.Impl.Dashboard
)
GROUP BY seg.machine_id, seg.production_date
) ad ON ad.machine_id = m.id
GROUP BY m.id, m.name, m.is_online
GROUP BY m.id, m.name, m.is_enabled, m.last_ping_time
ORDER BY Quantity DESC
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;
return rows;
@ -244,12 +245,14 @@ namespace CncRepository.Impl.Dashboard
}
/// <summary>机床状态分布</summary>
public object GetMachineStatusDistribution()
public object GetMachineStatusDistribution(int onlineTimeout = 300)
{
using (var conn = CreateConnection())
{
var online = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND is_online = 1");
var offline = conn.ExecuteScalar<int>("SELECT COUNT(1) FROM cnc_machine WHERE is_enabled = 1 AND is_online = 0");
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",
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");
return new { online, offline, disabled };
}

@ -17,18 +17,22 @@ namespace CncRepository.Impl
public MachineRepository(string connectionString) : base(connectionString) { }
/// <summary>机床SELECT列映射模板snake_case列名 → PascalCase属性名</summary>
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, is_online 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";
/// <summary>在线判断SQL片段已启用且最近Ping在超时阈值内视为在线。参数 @OnlineTimeout</summary>
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)";
public Machine GetById(int id)
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, int onlineTimeout = 300)
{
using (var conn = CreateConnection())
{
var sql = $"SELECT {SelectColumns} FROM cnc_machine WHERE id = @Id";
return conn.QuerySingleOrDefault<Machine>(sql, new { Id = id });
var cols = string.Format(SelectColumns, OnlineExpr);
var sql = $"SELECT {cols} FROM cnc_machine WHERE 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())
{
@ -37,7 +41,8 @@ namespace CncRepository.Impl
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, m.is_online as IsOnline,
m.is_enabled as IsEnabled,
(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,
w.id as WorkerId, w.name as WorkerName,
m.last_program_name as LastProgramName, m.last_collect_time as LastCollectTime
FROM cnc_machine m
@ -46,16 +51,17 @@ namespace CncRepository.Impl
LEFT JOIN cnc_worker_machine wm ON m.id = wm.machine_id
LEFT JOIN cnc_worker w ON wm.worker_id = w.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())
{
var where = " WHERE 1=1";
var p = new DynamicParameters();
p.Add("OnlineTimeout", onlineTimeout);
if (!string.IsNullOrWhiteSpace(query.Keyword))
{
where += " AND (m.name LIKE @Keyword OR m.device_code LIKE @Keyword)";
@ -68,8 +74,10 @@ namespace CncRepository.Impl
}
if (query.IsOnline.HasValue)
{
where += " AND m.is_online = @IsOnline";
p.Add("IsOnline", query.IsOnline.Value);
if (query.IsOnline.Value == 1)
where += " AND m.is_enabled = 1 AND m.last_ping_time IS NOT NULL AND m.last_ping_time >= NOW() - INTERVAL @OnlineTimeout SECOND";
else
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)
{
@ -78,7 +86,9 @@ namespace CncRepository.Impl
}
var limit = query.PageSize;
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, m.is_online as IsOnline, m.last_program_name as LastProgramName, m.last_collect_time as LastCollectTime, w.id as WorkerId, w.name as WorkerName
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.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_program_name as LastProgramName, m.last_collect_time as LastCollectTime, w.id as WorkerId, w.name as WorkerName
FROM cnc_machine m
LEFT JOIN cnc_workshop ws ON m.workshop_id = ws.id
LEFT JOIN cnc_brand b ON m.brand_id = b.id
@ -103,8 +113,8 @@ namespace CncRepository.Impl
{
using (var conn = CreateConnection())
{
var sql = @"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES (@DeviceCode, @Name, @WorkshopId, @CollectAddressId, @IpAddress, @BrandId, @IsEnabled, @IsOnline, @CreatedAt, @UpdatedAt);
var sql = @"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES (@DeviceCode, @Name, @WorkshopId, @CollectAddressId, @IpAddress, @BrandId, @IsEnabled, @CreatedAt, @UpdatedAt);
SELECT LAST_INSERT_ID();";
return conn.QuerySingle<int>(sql, entity);
}
@ -114,7 +124,7 @@ namespace CncRepository.Impl
{
using (var conn = CreateConnection())
{
var sql = @"UPDATE cnc_machine SET device_code = @DeviceCode, name = @Name, workshop_id = @WorkshopId, collect_address_id = @CollectAddressId, ip_address = @IpAddress, brand_id = @BrandId, is_enabled = @IsEnabled, is_online = @IsOnline, updated_at = @UpdatedAt, last_program_name = @LastProgramName, last_collect_time = @LastCollectTime, last_device_status = @LastDeviceStatus, last_run_status = @LastRunStatus, last_machining_status = @LastMachiningStatus WHERE id = @Id";
var sql = @"UPDATE cnc_machine SET device_code = @DeviceCode, name = @Name, workshop_id = @WorkshopId, collect_address_id = @CollectAddressId, ip_address = @IpAddress, brand_id = @BrandId, is_enabled = @IsEnabled, updated_at = @UpdatedAt, last_program_name = @LastProgramName, last_collect_time = @LastCollectTime, last_device_status = @LastDeviceStatus, last_run_status = @LastRunStatus, last_machining_status = @LastMachiningStatus WHERE id = @Id";
return conn.Execute(sql, entity) > 0;
}
}
@ -147,39 +157,33 @@ namespace CncRepository.Impl
}
}
public Machine GetByDeviceCode(string deviceCode)
{
using (var conn = CreateConnection())
{
var sql = $"SELECT {SelectColumns} FROM cnc_machine WHERE device_code = @DeviceCode";
return conn.QuerySingleOrDefault<Machine>(sql, new { DeviceCode = deviceCode });
}
}
public List<Machine> GetEnabledByAddressId(int collectAddressId)
public Machine GetByDeviceCode(string deviceCode, int onlineTimeout = 300)
{
using (var conn = CreateConnection())
{
var sql = $"SELECT {SelectColumns} FROM cnc_machine WHERE collect_address_id = @CollectAddressId AND is_enabled = 1";
return conn.Query<Machine>(sql, new { CollectAddressId = collectAddressId }).ToList();
var cols = string.Format(SelectColumns, OnlineExpr);
var sql = $"SELECT {cols} FROM cnc_machine WHERE device_code = @DeviceCode";
return conn.QuerySingleOrDefault<Machine>(sql, new { DeviceCode = deviceCode, OnlineTimeout = onlineTimeout });
}
}
public List<Machine> GetEnabledOnline()
public List<Machine> GetEnabledByAddressId(int collectAddressId, int onlineTimeout = 300)
{
using (var conn = CreateConnection())
{
var sql = $"SELECT {SelectColumns} FROM cnc_machine WHERE is_enabled = 1 AND is_online = 1";
return conn.Query<Machine>(sql).ToList();
var cols = string.Format(SelectColumns, OnlineExpr);
var sql = $"SELECT {cols} FROM cnc_machine WHERE collect_address_id = @CollectAddressId AND is_enabled = 1";
return conn.Query<Machine>(sql, new { CollectAddressId = collectAddressId, OnlineTimeout = onlineTimeout }).ToList();
}
}
public void UpdateOnlineStatus(int id, bool isOnline)
public List<Machine> GetEnabledOnline(int onlineTimeout = 300)
{
using (var conn = CreateConnection())
{
var sql = @"UPDATE cnc_machine SET is_online = @IsOnline, updated_at = NOW() WHERE id = @Id";
conn.Execute(sql, new { Id = id, IsOnline = isOnline ? 1 : 0 });
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 @OnlineTimeout SECOND";
return conn.Query<Machine>(sql, new { OnlineTimeout = onlineTimeout }).ToList();
}
}

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

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

@ -20,18 +20,29 @@ namespace CncService.Impl
private readonly IBrandRepository _brandRepository;
private readonly IWorkshopRepository _workshopRepository;
private readonly ICollectRawRepository _collectRawRepository;
private readonly ISysConfigRepository _sysConfigRepository;
public CollectAddressService(ICollectAddressRepository collectAddressRepository,
IMachineRepository machineRepository,
IBrandRepository brandRepository,
IWorkshopRepository workshopRepository,
ICollectRawRepository collectRawRepository)
ICollectRawRepository collectRawRepository,
ISysConfigRepository sysConfigRepository)
{
_collectAddressRepository = collectAddressRepository;
_machineRepository = machineRepository;
_brandRepository = brandRepository;
_workshopRepository = workshopRepository;
_collectRawRepository = collectRawRepository;
_sysConfigRepository = sysConfigRepository;
}
/// <summary>从sys_config读取在线超时阈值</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;
}
public PagedResult<CollectAddressListItem> GetList(CollectAddressQuery query)
@ -161,7 +172,7 @@ namespace CncService.Impl
MachineName = m.Name ?? m.DeviceCode,
DeviceCode = m.DeviceCode,
WorkshopName = workshopName,
IsOnline = m.IsOnline == 1,
IsOnline = m.IsEnabled == 1 && m.LastPingTime.HasValue && (DateTime.Now - m.LastPingTime.Value).TotalSeconds <= GetOnlineTimeout(),
ProgramName = m.LastProgramName
});
}

@ -14,20 +14,31 @@ namespace CncService.Impl
private readonly IDashboardRepository _dashboardRepository;
private readonly ICollectorHeartbeatRepository _collectorHeartbeatRepository;
private readonly IWindowsServiceChecker _serviceChecker;
private readonly ISysConfigRepository _sysConfigRepository;
public DashboardService(IDashboardRepository dashboardRepository,
ICollectorHeartbeatRepository collectorHeartbeatRepository,
ISysConfigRepository sysConfigRepository,
IWindowsServiceChecker serviceChecker = null)
{
_dashboardRepository = dashboardRepository ?? throw new ArgumentNullException(nameof(dashboardRepository));
_collectorHeartbeatRepository = collectorHeartbeatRepository ?? throw new ArgumentNullException(nameof(collectorHeartbeatRepository));
_sysConfigRepository = sysConfigRepository ?? throw new ArgumentNullException(nameof(sysConfigRepository));
_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/>
public DashboardSummaryResponse GetSummary()
{
return _dashboardRepository.GetSummary();
return _dashboardRepository.GetSummary(GetOnlineTimeout());
}
/// <inheritdoc/>
@ -43,7 +54,7 @@ namespace CncService.Impl
{
var s = startDate ?? DateTime.Today;
var e = endDate ?? DateTime.Today;
return _dashboardRepository.GetMachineRank(s, e, top);
return _dashboardRepository.GetMachineRank(s, e, top, GetOnlineTimeout());
}
/// <inheritdoc/>
@ -63,7 +74,7 @@ namespace CncService.Impl
/// <inheritdoc/>
public object GetMachineStatusDistribution()
{
return _dashboardRepository.GetMachineStatusDistribution();
return _dashboardRepository.GetMachineStatusDistribution(GetOnlineTimeout());
}
/// <inheritdoc/>

@ -65,7 +65,6 @@ namespace CncService.Impl
BrandId = request.BrandId,
IpAddress = request.IpAddress,
IsEnabled = 1,
IsOnline = 0,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};

@ -19,15 +19,26 @@ namespace CncService.Impl
private readonly IWorkerRepository _workerRepository;
private readonly IWorkerMachineRepository _workerMachineRepository;
private readonly IMachineRepository _machineRepository;
private readonly ISysConfigRepository _sysConfigRepository;
public WorkerService(
IWorkerRepository workerRepository,
IWorkerMachineRepository workerMachineRepository,
IMachineRepository machineRepository)
IMachineRepository machineRepository,
ISysConfigRepository sysConfigRepository)
{
_workerRepository = workerRepository ?? throw new ArgumentNullException(nameof(workerRepository));
_workerMachineRepository = workerMachineRepository ?? throw new ArgumentNullException(nameof(workerMachineRepository));
_workerMachineRepository = workerMachineRepository ?? throw new ArgumentNullException(nameof(_workerMachineRepository));
_machineRepository = machineRepository ?? throw new ArgumentNullException(nameof(machineRepository));
_sysConfigRepository = sysConfigRepository ?? throw new ArgumentNullException(nameof(sysConfigRepository));
}
/// <summary>从sys_config读取在线超时阈值</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/>
@ -193,7 +204,7 @@ namespace CncService.Impl
DeviceCode = m.DeviceCode,
WorkshopName = workshopName,
BrandName = brandName,
IsOnline = m.IsOnline == 1,
IsOnline = m.IsEnabled == 1 && m.LastPingTime.HasValue && (DateTime.Now - m.LastPingTime.Value).TotalSeconds <= GetOnlineTimeout(),
ProgramName = m.LastProgramName
});
}

@ -114,6 +114,7 @@ namespace CncWebApi.Infrastructure
return new CncService.Impl.DashboardService(
new CncRepository.Impl.Dashboard.DashboardRepository(_businessConn),
new CncRepository.Impl.Log.CollectorHeartbeatRepository(_logConn),
new CncRepository.Impl.SysConfigRepository(_businessConn),
serviceChecker);
}
@ -141,7 +142,8 @@ namespace CncWebApi.Infrastructure
new CncRepository.Impl.MachineRepository(_businessConn),
new CncRepository.Impl.BrandRepository(_businessConn),
new CncRepository.Impl.WorkshopRepository(_businessConn),
new CncRepository.Impl.Log.CollectRawRepository(_logConn));
new CncRepository.Impl.Log.CollectRawRepository(_logConn),
new CncRepository.Impl.SysConfigRepository(_businessConn));
}
private IWorkerService ResolveWorkerService()
@ -149,7 +151,8 @@ namespace CncWebApi.Infrastructure
return new CncService.Impl.WorkerService(
new CncRepository.Impl.WorkerRepository(_businessConn),
new CncRepository.Impl.WorkerMachineRepository(_businessConn),
new CncRepository.Impl.MachineRepository(_businessConn));
new CncRepository.Impl.MachineRepository(_businessConn),
new CncRepository.Impl.SysConfigRepository(_businessConn));
}
private IProductionService ResolveProductionService()

@ -665,7 +665,6 @@ namespace CncModels.Tests
Assert.Null(m0.IpAddress);
Assert.Equal(0, m0.BrandId);
Assert.Equal(0, m0.IsEnabled);
Assert.Equal(0, m0.IsOnline);
Assert.Null(m0.LastPingTime);
Assert.Null(m0.LastCollectTime);
Assert.Null(m0.LastDeviceStatus);
@ -687,7 +686,6 @@ namespace CncModels.Tests
IpAddress = "192.168.0.10",
BrandId = 3,
IsEnabled = 1,
IsOnline = 1,
LastPingTime = new DateTime(2026, 4, 28, 12, 0, 0),
LastCollectTime = new DateTime(2026, 4, 28, 12, 5, 0),
LastDeviceStatus = "OK",
@ -707,7 +705,6 @@ namespace CncModels.Tests
Assert.Equal("192.168.0.10", m.IpAddress);
Assert.Equal(3, m.BrandId);
Assert.Equal(1, m.IsEnabled);
Assert.Equal(1, m.IsOnline);
Assert.Equal(new DateTime(2026, 4, 28, 12, 0, 0), m.LastPingTime);
Assert.Equal(new DateTime(2026, 4, 28, 12, 5, 0), m.LastCollectTime);
Assert.Equal("OK", m.LastDeviceStatus);

@ -41,7 +41,6 @@ namespace CncRepository.Tests
BrandId = 1,
IpAddress = "10.1.1.8",
IsEnabled = 1,
IsOnline = 0,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};

@ -38,8 +38,8 @@ namespace CncService.Tests
{
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
VALUES ('', 'http://test', 1, 30, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, NOW(), NOW())");
}
TestDb.Execute(@"INSERT INTO cnc_alert (alert_type, machine_id, title, is_resolved, created_at)
VALUES (@alertType, 1, '', @isResolved, NOW())",

@ -180,8 +180,8 @@ namespace CncService.Tests
{
var addressId = InsertTestAddress();
// 关联机床
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES ('M001', '1', 1, @addressId, '0.0.0.0', 1, 1, 0, NOW(), NOW())",
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES ('M001', '1', 1, @addressId, '0.0.0.0', 1, 1, NOW(), NOW())",
new { addressId });
var result = _service.Delete(addressId);

@ -12,12 +12,12 @@ namespace CncService.Tests
// Fake repositories to isolate DashboardService.GetCollectorStatus tests
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<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top) => new List<MachineRankResponse>();
public List<MachineRankResponse> GetMachineRank(DateTime startDate, DateTime endDate, int top, int something) => new List<MachineRankResponse>();
public List<WorkerRankResponse> GetWorkerRank(DateTime startDate, DateTime endDate, int top) => new List<WorkerRankResponse>();
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>();
}
@ -42,6 +42,13 @@ namespace CncService.Tests
true, "Stopped");
}
public class FakeSysConfigRepository : ISysConfigRepository
{
public SysConfig GetByKey(string configKey) => new SysConfig { ConfigKey = configKey, ConfigValue = "300" };
public List<SysConfig> GetAll() => new List<SysConfig>();
public bool UpdateValue(int id, string value) => true;
}
public class DashboardServiceTests
{
[Fact]
@ -53,7 +60,7 @@ namespace CncService.Tests
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.NotInstalled);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, checker);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, new FakeSysConfigRepository(), checker);
// Act
var resultObj = svc.GetCollectorStatus();
@ -77,7 +84,7 @@ namespace CncService.Tests
var dashboardRepo = new FakeDashboardRepository();
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.Running);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, checker);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, new FakeSysConfigRepository(), checker);
var resultObj = svc.GetCollectorStatus();
var t = resultObj.GetType();
@ -96,7 +103,7 @@ namespace CncService.Tests
var dashboardRepo = new FakeDashboardRepository();
var heartbeatRepo = new FakeCollectorHeartbeatRepository(latest);
var checker = new FakeWindowsServiceChecker(CncService.Interface.ServiceStatusEnum.Starting);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, checker);
var svc = new DashboardService(dashboardRepo, heartbeatRepo, new FakeSysConfigRepository(), checker);
var resultObj = svc.GetCollectorStatus();
var t = resultObj.GetType();

@ -51,8 +51,8 @@ namespace CncService.Tests
// 插入机床+日产量数据
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
VALUES ('', 'http://test', 1, 30, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_daily_production (machine_id, production_date, program_name, total_quantity, created_at, updated_at)
VALUES (1, CURDATE(), 'O0001', 100, NOW(), NOW())");
@ -75,8 +75,8 @@ namespace CncService.Tests
{
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
VALUES ('', 'http://test', 1, 30, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, NOW(), NOW())");
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())");

@ -65,13 +65,13 @@ namespace CncService.Tests
/// <summary>创建CollectAddressService</summary>
public static CollectAddressService CreateCollectAddressService()
{
return new CollectAddressService(NewCollectAddressRepo(), NewMachineRepo(), NewBrandRepo(), NewWorkshopRepo(), NewCollectRawRepo());
return new CollectAddressService(NewCollectAddressRepo(), NewMachineRepo(), NewBrandRepo(), NewWorkshopRepo(), NewCollectRawRepo(), NewSysConfigRepo());
}
/// <summary>创建WorkerService</summary>
public static WorkerService CreateWorkerService()
{
return new WorkerService(NewWorkerRepo(), NewWorkerMachineRepo(), NewMachineRepo());
return new WorkerService(NewWorkerRepo(), NewWorkerMachineRepo(), NewMachineRepo(), NewSysConfigRepo());
}
/// <summary>创建ProductionService</summary>
@ -101,7 +101,7 @@ namespace CncService.Tests
/// <summary>创建DashboardService</summary>
public static DashboardService CreateDashboardService()
{
return new DashboardService(NewDashboardRepo(), NewCollectorHeartbeatRepo());
return new DashboardService(NewDashboardRepo(), NewCollectorHeartbeatRepo(), NewSysConfigRepo());
}
/// <summary>创建CollectDataService</summary>

@ -44,8 +44,8 @@ namespace CncService.Tests
{
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
VALUES ('', 'http://test', 1, 30, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES (@code, '', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())",
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES (@code, '', 1, 1, '0.0.0.0', 1, 1, NOW(), NOW())",
new { code = deviceCode });
return TestDb.QuerySingle<int>("SELECT MAX(id) FROM cnc_machine");
}

@ -242,8 +242,8 @@ namespace CncService.Tests
// 先插入有效的采集地址
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
VALUES ('', 'http://test', 1, 30, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, NOW(), NOW())");
var ex = Assert.Throws<BusinessException>(() => _service.Delete(1));
Assert.Equal(ErrorCode.DataReferenced, ex.Code);
@ -292,9 +292,9 @@ namespace CncService.Tests
// 先插入一个有效的采集地址满足cnc_machine的外键约束
TestDb.Execute(@"INSERT INTO cnc_collect_address (name, url, brand_id, collect_interval, is_enabled, created_at, updated_at)
VALUES ('', 'http://test', 1, 30, 1, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, is_online, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW()),
('M002', '2', 1, 1, '0.0.0.0', 1, 1, 0, NOW(), NOW())");
TestDb.Execute(@"INSERT INTO cnc_machine (device_code, name, workshop_id, collect_address_id, ip_address, brand_id, is_enabled, created_at, updated_at)
VALUES ('M001', '1', 1, 1, '0.0.0.0', 1, 1, NOW(), NOW()),
('M002', '2', 1, 1, '0.0.0.0', 1, 1, NOW(), NOW())");
var count = _service.GetMachineCount(1);
Assert.Equal(2, count);

@ -48,11 +48,11 @@ namespace CncWebApi.Tests
#region Service 创建
private static IAuthService CreateAuthService() => new AuthService(SysConfigRepo(), _jwtSecret);
private static IDashboardService CreateDashboardService() => new DashboardService(DashboardRepo(), HeartbeatRepo());
private static IDashboardService CreateDashboardService() => new DashboardService(DashboardRepo(), HeartbeatRepo(), SysConfigRepo());
private static IMachineService CreateMachineService() => new MachineService(MachineRepo(), CollectAddressRepo(), WorkerMachineRepo(), BrandRepo());
private static IBrandService CreateBrandService() => new BrandService(BrandRepo(), BrandFieldMappingRepo(), CollectAddressRepo());
private static ICollectAddressService CreateCollectAddressService() => new CollectAddressService(CollectAddressRepo(), MachineRepo(), BrandRepo(), WorkshopRepo(), CollectRawRepo());
private static IWorkerService CreateWorkerService() => new WorkerService(WorkerRepo(), WorkerMachineRepo(), MachineRepo());
private static ICollectAddressService CreateCollectAddressService() => new CollectAddressService(CollectAddressRepo(), MachineRepo(), BrandRepo(), WorkshopRepo(), CollectRawRepo(), SysConfigRepo());
private static IWorkerService CreateWorkerService() => new WorkerService(WorkerRepo(), WorkerMachineRepo(), MachineRepo(), SysConfigRepo());
private static IProductionService CreateProductionService() => new ProductionService(DailyProductionRepo(), ProductionSegmentRepo(), ProductionAdjustmentRepo());
private static IAlertService CreateAlertService() => new AlertService(AlertRepo());
private static IWorkshopService CreateWorkshopService() => new WorkshopService(WorkshopRepo());

Loading…
Cancel
Save