/** * CoreServices.cs - 核心业务服务实现 * * 实现CNC机床数据采集分析系统的核心业务服务。 * * 修订历史: * - 2026-04-13: 初始实现 */ using System; using System.Collections.Generic; using System.Linq; using System.Net.NetworkInformation; using System.Threading; using System.Threading.Tasks; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using Haoliang.Models.Device; using Haoliang.Models.Production; using Haoliang.Models.DataCollection; using Haoliang.Data.Repositories; namespace Haoliang.Core.Services { #region ========== Ping服务实现 ========== /// /// Ping服务实现 - 设备网络连通性检测 /// public class PingService : IPingService { private readonly ILogger _logger; private const int DefaultTimeout = 5000; // 5秒超时 public PingService(ILogger logger) { _logger = logger; } /// /// Ping指定设备 /// public async Task PingAsync(int deviceId, string ipAddress) { var result = new PingResult { DeviceId = deviceId, IpAddress = ipAddress, Timestamp = DateTime.UtcNow }; try { using var ping = new Ping(); var reply = await ping.SendPingAsync(ipAddress, DefaultTimeout); result.Success = reply.Status == IPStatus.Success; result.RoundtripTime = reply.RoundtripTime; if (!result.Success) { result.ErrorMessage = $"Ping失败: {reply.Status}"; _logger.LogWarning("设备{DeviceId} Ping {IpAddress} 失败: {Status}", deviceId, ipAddress, reply.Status); } } catch (PingException ex) { result.Success = false; result.ErrorMessage = ex.Message; _logger.LogError(ex, "设备{DeviceId} Ping {IpAddress} 异常", deviceId, ipAddress); } catch (Exception ex) { result.Success = false; result.ErrorMessage = ex.Message; _logger.LogError(ex, "设备{DeviceId} Ping {IpAddress} 未知错误", deviceId, ipAddress); } return result; } /// /// 批量Ping设备 /// public async Task> PingAllAsync(IEnumerable<(int DeviceId, string IpAddress)> devices) { var tasks = devices.Select(d => PingAsync(d.DeviceId, d.IpAddress)); var results = await Task.WhenAll(tasks); return results; } /// /// 检查设备是否可达 /// public async Task IsReachableAsync(string ipAddress, TimeSpan? timeout = null) { try { using var ping = new Ping(); var timeoutMs = timeout.HasValue ? (int)timeout.Value.TotalMilliseconds : DefaultTimeout; var reply = await ping.SendPingAsync(ipAddress, timeoutMs); return reply.Status == IPStatus.Success; } catch { return false; } } } #endregion #region ========== 数据解析服务实现(发那科JSON解析) ========== /// /// 数据解析服务实现 - 解析发那科CNC设备返回的JSON数据 /// /// 发那科标准JSON格式: /// { /// "device": "设备编号", /// "desc": "设备描述", /// "tags": [ /// { "id": "Tag5", "value": "NC程序名", ... }, /// { "id": "Tag8", "value": "12345.00000", ... }, /// { "id": "_io_status", "value": "1", ... }, /// ... /// ] /// } /// public class DataParserService : IDataParserService { private readonly ILogger _logger; public DataParserService(ILogger logger) { _logger = logger; } /// /// 解析设备原始数据 /// public Task ParseRawDataAsync(string rawData, int templateId) { try { var json = JsonNode.Parse(rawData); if (json == null) { throw new InvalidOperationException("JSON解析失败:返回空对象"); } var result = new ParsedDeviceData { Timestamp = DateTime.UtcNow, RawJson = rawData, Tags = new Dictionary() }; // 解析device字段 - 设备标识 result.DeviceName = json["device"]?.GetValue(); // 解析tags数组 - 关键字段 var tags = json["tags"]?.AsArray(); if (tags != null) { foreach (var tag in tags) { if (tag == null) continue; var tagId = tag["id"]?.GetValue(); if (string.IsNullOrEmpty(tagId)) continue; var tagValue = new TagValue { Id = tagId, Description = tag["desc"]?.GetValue(), Value = ParseTagValue(tag["value"]), Quality = tag["quality"]?.GetValue(), Timestamp = DateTime.TryParse(tag["time"]?.GetValue(), out var ts) ? ts : DateTime.UtcNow }; result.Tags[tagId] = tagValue; } } _logger.LogDebug("解析设备数据成功: Device={Device}, TagCount={Count}", result.DeviceName, result.Tags?.Count ?? 0); return Task.FromResult(result); } catch (JsonException ex) { _logger.LogError(ex, "JSON解析异常: {RawData}", rawData); throw new InvalidOperationException($"JSON格式错误: {ex.Message}", ex); } } /// /// 解析多设备数据(数组格式) /// public Task> ParseMultiDeviceDataAsync(string rawData, int templateId) { try { var jsonArray = JsonNode.Parse(rawData)?.AsArray(); if (jsonArray == null) { return Task.FromResult>( Array.Empty()); } var results = new List(); foreach (var item in jsonArray) { if (item == null) continue; var jsonStr = item.ToJsonString(); try { var parsed = ParseRawDataAsync(jsonStr, templateId).Result; results.Add(parsed); } catch (Exception ex) { _logger.LogWarning(ex, "解析单个设备数据失败: {Json}", jsonStr); } } return Task.FromResult>(results); } catch (Exception ex) { _logger.LogError(ex, "解析多设备数据异常"); return Task.FromResult>( Array.Empty()); } } /// /// 验证数据格式 /// public bool ValidateDataFormat(string rawData) { if (string.IsNullOrWhiteSpace(rawData)) return false; try { var json = JsonNode.Parse(rawData); if (json == null) return false; // 检查必要字段:发那科格式应该有 device, desc, tags var hasDevice = json["device"] != null; var hasTags = json["tags"] != null; return hasDevice && hasTags; } catch { return false; } } /// /// 解析Tag值 - 处理数值类型的尾缀去除 /// 发那科返回的数值如 "12345.00000" 需要转换为整数/小数 /// private object? ParseTagValue(JsonNode? valueNode) { if (valueNode == null) return null; var strValue = valueNode.GetValue(); if (string.IsNullOrEmpty(strValue)) return strValue; // 尝试解析为数值 if (decimal.TryParse(strValue, out var decValue)) { // 如果是小数且尾缀为.00000,去除尾缀转为整数 if (decValue == Math.Floor(decValue) && strValue.Contains(".00000")) { return (long)decValue; } // 如果是整数 if (decValue == Math.Floor(decValue)) { return (long)decValue; } // 否则返回小数 return decValue; } return strValue; } } #endregion #region ========== 设备采集服务实现 ========== /// /// 设备采集服务实现 /// /// 负责定时轮询采集CNC设备数据,支持: /// - Ping检测设备连通性 /// - HTTP请求采集数据 /// - 失败自动重试(3次,间隔30秒) /// - 并行采集多设备 /// public class DeviceCollectionService : IDeviceCollectionService { private readonly ILogger _logger; private readonly IPingService _pingService; private readonly IDataParserService _dataParserService; private readonly IDeviceRepository _deviceRepository; private const int MaxRetryCount = 3; private const int RetryIntervalSeconds = 30; public DeviceCollectionService( ILogger logger, IPingService pingService, IDataParserService dataParserService, IDeviceRepository deviceRepository) { _logger = logger; _pingService = pingService; _dataParserService = dataParserService; _deviceRepository = deviceRepository; } /// /// 获取所有设备 /// public async Task> GetAllDevicesAsync() { return await _deviceRepository.GetAllAsync(); } /// /// 根据ID获取设备 /// public async Task GetDeviceByIdAsync(int deviceId) { return await _deviceRepository.GetByIdAsync(deviceId); } /// /// 创建设备 /// public async Task CreateDeviceAsync(CNCDevice device) { device.CreatedAt = DateTime.UtcNow; device.UpdatedAt = DateTime.UtcNow; await _deviceRepository.AddAsync(device); await _deviceRepository.SaveAsync(); return device; } /// /// 更新设备信息 /// public async Task UpdateDeviceAsync(CNCDevice device) { device.UpdatedAt = DateTime.UtcNow; _deviceRepository.Update(device); var affected = await _deviceRepository.SaveAsync(); return affected > 0 ? device : null; } /// /// 删除设备 /// public async Task DeleteDeviceAsync(int deviceId) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) return false; _deviceRepository.Remove(device); return await _deviceRepository.SaveAsync() > 0; } /// /// 采集指定设备数据 /// 采集流程:Ping检测 -> HTTP请求 -> 数据解析 -> 存储 /// public async Task CollectDeviceAsync(int deviceId) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device == null) { _logger.LogWarning("采集失败:设备{DeviceId}不存在", deviceId); return; } // 检查设备是否可用 if (!device.IsAvailable) { _logger.LogDebug("设备{DeviceId}标记为不可用,跳过采集", deviceId); return; } // Ping检测 var pingResult = await _pingService.PingAsync(deviceId, device.IPAddress); if (!pingResult.Success) { _logger.LogWarning("设备{DeviceId} Ping失败: {Error}", deviceId, pingResult.ErrorMessage); await UpdateDeviceOnlineStatus(deviceId, false); return; } // HTTP请求采集数据,带重试 var retryCount = 0; while (retryCount < MaxRetryCount) { try { var rawData = await HttpCollectAsync(device); if (string.IsNullOrEmpty(rawData)) { throw new InvalidOperationException("HTTP返回空数据"); } // 解析数据 var parsedData = await _dataParserService.ParseRawDataAsync(rawData, device.TemplateId); parsedData.DeviceId = deviceId; _logger.LogInformation("设备{DeviceId}采集成功", deviceId); // 更新设备在线状态 await UpdateDeviceOnlineStatus(deviceId, true); await UpdateLastCollectionTime(deviceId); return; } catch (Exception ex) { retryCount++; _logger.LogWarning(ex, "设备{DeviceId}采集失败 (重试 {Retry}/{Max})", deviceId, retryCount, MaxRetryCount); if (retryCount < MaxRetryCount) { await Task.Delay(TimeSpan.FromSeconds(RetryIntervalSeconds)); } } } // 连续MaxRetryCount次失败 _logger.LogError("设备{DeviceId}连续{Attempts}次采集失败", deviceId, MaxRetryCount); } /// /// 采集所有设备数据 /// public async Task CollectAllDevicesAsync() { var devices = await _deviceRepository.GetAllAsync(); var availableDevices = devices.Where(d => d.IsAvailable).ToList(); _logger.LogInformation("开始采集 {Count} 台设备", availableDevices.Count); foreach (var device in availableDevices) { try { await CollectDeviceAsync(device.Id); } catch (Exception ex) { _logger.LogError(ex, "设备{DeviceId}采集异常", device.Id); } } _logger.LogInformation("采集任务完成"); } /// /// 获取设备状态 /// public Task GetDeviceStatusAsync(int deviceId) { return Task.FromResult(new DeviceStatus { DeviceId = deviceId, Timestamp = DateTime.UtcNow }); } /// /// 获取设备健康状态 /// public Task GetDeviceHealthAsync(int deviceId) { return Task.FromResult(new DeviceHealth { DeviceId = deviceId, IsHealthy = true, LastCheck = DateTime.UtcNow }); } /// /// 获取设备当前状态 /// public Task GetDeviceCurrentStatusAsync(int deviceId) { return GetDeviceStatusAsync(deviceId); } #region 私有方法 /// /// HTTP采集数据 /// private async Task HttpCollectAsync(CNCDevice device) { try { using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); var response = await httpClient.GetAsync(device.HttpUrl); if (response.IsSuccessStatusCode) { return await response.Content.ReadAsStringAsync(); } _logger.LogWarning("HTTP请求失败: {StatusCode}", response.StatusCode); return null; } catch (Exception ex) { _logger.LogError(ex, "HTTP请求异常: {Url}", device.HttpUrl); return null; } } /// /// 更新设备在线状态 /// private async Task UpdateDeviceOnlineStatus(int deviceId, bool isOnline) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device != null && device.IsOnline != isOnline) { device.IsOnline = isOnline; device.UpdatedAt = DateTime.UtcNow; _deviceRepository.Update(device); await _deviceRepository.SaveAsync(); } } /// /// 更新最后采集时间 /// private async Task UpdateLastCollectionTime(int deviceId) { var device = await _deviceRepository.GetByIdAsync(deviceId); if (device != null) { device.LastCollectionTime = DateTime.UtcNow; device.UpdatedAt = DateTime.UtcNow; _deviceRepository.Update(device); await _deviceRepository.SaveAsync(); } } #endregion } #endregion #region ========== 产量计算服务实现 ========== /// /// 产量计算服务实现 /// /// 核心差分计算逻辑: /// 1. 同一程序连续加工:产量 = MAX(0, 当前累计数 - 上次累计数) /// 2. 程序切换(A→B):A产量锁定,B以当前累计数为新起点 /// 3. 切回历史程序(B→A):视为A重新开始,增量累加到当日产量 /// 4. 跨天处理:0点自动重置,新日期以首次采集累计值为起点 /// public class ProductionCalculator : IProductionCalculator { private readonly ILogger _logger; // 存储每个设备的生产状态:设备ID -> (程序名, 上次累计数, 记录时间, 上次日期) private readonly Dictionary _productionStates = new(); // 异常值检测阈值:产量变化超过此值视为异常 private const decimal AbnormalJumpThreshold = 1000; private class DeviceProductionState { public string? ProgramName { get; set; } public decimal LastValue { get; set; } public DateTime LastTime { get; set; } public DateTime LastDate { get; set; } } public ProductionCalculator(ILogger logger) { _logger = logger; } /// /// 计算生产增量 /// public Task CalculateProductionIncrementAsync(int deviceId, decimal currentValue, string programName, DateTime timestamp) { decimal increment = 0; lock (_productionStates) { // 首次采集或跨天:初始化状态 if (!_productionStates.TryGetValue(deviceId, out var state)) { state = new DeviceProductionState { ProgramName = programName, LastValue = currentValue, LastTime = timestamp, LastDate = timestamp.Date }; _productionStates[deviceId] = state; _logger.LogDebug("设备{DeviceId}首次采集,程序={Program},初始值={Value}", deviceId, programName, currentValue); return Task.FromResult(0m); } // 跨天处理:0点自动重置 if (timestamp.Date != state.LastDate) { _logger.LogInformation("设备{DeviceId}跨天重置,昨日程序={Program},新日期={Date}", deviceId, state.ProgramName, timestamp.Date); state.ProgramName = programName; state.LastValue = currentValue; state.LastTime = timestamp; state.LastDate = timestamp.Date; return Task.FromResult(0m); } // 同一程序连续加工 if (state.ProgramName == programName) { var diff = currentValue - state.LastValue; // 异常值保护:负数视为0 if (diff < 0) { _logger.LogWarning("设备{DeviceId}产量为负,忽略: {Diff}", deviceId, diff); diff = 0; } // 异常值保护:跳变过大视为异常 else if (diff > AbnormalJumpThreshold) { _logger.LogWarning("设备{DeviceId}产量跳变过大,忽略: {Diff}", deviceId, diff); diff = 0; } increment = diff; state.LastValue = currentValue; state.LastTime = timestamp; _logger.LogDebug("设备{DeviceId}连续加工,程序={Program},增量={Increment}", deviceId, programName, increment); } // 程序切换 else { // 切回历史程序:增量累加 // 新程序:以当前累计数为新起点 _logger.LogInformation("设备{DeviceId}程序切换 {OldProgram}→{NewProgram},新起点={Value}", deviceId, state.ProgramName, programName, currentValue); state.ProgramName = programName; state.LastValue = currentValue; state.LastTime = timestamp; increment = 0; // 程序切换不计入产量 } } return Task.FromResult(increment); } /// /// 重置设备生产状态 /// public void ResetDeviceProductionState(int deviceId) { lock (_productionStates) { if (_productionStates.ContainsKey(deviceId)) { _productionStates.Remove(deviceId); _logger.LogInformation("设备{DeviceId}生产状态已重置", deviceId); } } } /// /// 验证生产数据有效性 /// public bool ValidateProductionValue(int deviceId, decimal value) { // 负数无效 if (value < 0) { _logger.LogWarning("设备{DeviceId}产量验证失败:负数 {Value}", deviceId, value); return false; } lock (_productionStates) { if (_productionStates.TryGetValue(deviceId, out var state)) { var diff = Math.Abs(value - state.LastValue); // 跳变过大无效 if (diff > AbnormalJumpThreshold) { _logger.LogWarning("设备{DeviceId}产量验证失败:跳变过大 {Diff}", deviceId, diff); return false; } } } return true; } } #endregion #region ========== 数据存储服务实现 ========== /// /// 数据存储服务实现 /// /// 负责将解析后的数据存储到数据库: /// - 原始JSON存入日志库(cnc_log) /// - 解析后结构化数据存入业务库(cnc_business) /// public class DataStorageService : IDataStorageService { private readonly ILogger _logger; private readonly IDeviceRepository _deviceRepository; public DataStorageService( ILogger logger, IDeviceRepository deviceRepository) { _logger = logger; _deviceRepository = deviceRepository; } /// /// 存储设备数据 /// public Task StoreDeviceDataAsync(ParsedDeviceData data) { try { // 更新设备状态 _deviceRepository.UpdateDeviceStatusAsync(data.DeviceId, true, true).Wait(); _logger.LogDebug("设备{DeviceId}数据存储成功", data.DeviceId); } catch (Exception ex) { _logger.LogError(ex, "设备{DeviceId}数据存储失败", data.DeviceId); } return Task.CompletedTask; } /// /// 批量存储设备数据 /// public Task StoreDeviceDataBatchAsync(IEnumerable dataList) { foreach (var data in dataList) { try { StoreDeviceDataAsync(data).Wait(); } catch (Exception ex) { _logger.LogError(ex, "批量存储中设备{DeviceId}失败", data.DeviceId); } } return Task.CompletedTask; } /// /// 存储生产记录 /// public Task StoreProductionRecordAsync(ProductionRecord record) { // TODO: 调用ProductionRepository存储 _logger.LogDebug("存储生产记录: DeviceId={DeviceId}, Quantity={Quantity}", record.DeviceId, record.Quantity); return Task.CompletedTask; } /// /// 更新设备状态 /// public Task UpdateDeviceStatusAsync(int deviceId, DeviceStatus status) { _deviceRepository.UpdateDeviceStatusAsync(deviceId, true, true).Wait(); return Task.CompletedTask; } } #endregion }