/**
* 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
}