You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

823 lines
28 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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服务实现 ==========
/// <summary>
/// Ping服务实现 - 设备网络连通性检测
/// </summary>
public class PingService : IPingService
{
private readonly ILogger<PingService> _logger;
private const int DefaultTimeout = 5000; // 5秒超时
public PingService(ILogger<PingService> logger)
{
_logger = logger;
}
/// <summary>
/// Ping指定设备
/// </summary>
public async Task<PingResult> 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;
}
/// <summary>
/// 批量Ping设备
/// </summary>
public async Task<IEnumerable<PingResult>> 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;
}
/// <summary>
/// 检查设备是否可达
/// </summary>
public async Task<bool> 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解析 ==========
/// <summary>
/// 数据解析服务实现 - 解析发那科CNC设备返回的JSON数据
///
/// 发那科标准JSON格式
/// {
/// "device": "设备编号",
/// "desc": "设备描述",
/// "tags": [
/// { "id": "Tag5", "value": "NC程序名", ... },
/// { "id": "Tag8", "value": "12345.00000", ... },
/// { "id": "_io_status", "value": "1", ... },
/// ...
/// ]
/// }
/// </summary>
public class DataParserService : IDataParserService
{
private readonly ILogger<DataParserService> _logger;
public DataParserService(ILogger<DataParserService> logger)
{
_logger = logger;
}
/// <summary>
/// 解析设备原始数据
/// </summary>
public Task<ParsedDeviceData> 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<string, TagValue>()
};
// 解析device字段 - 设备标识
result.DeviceName = json["device"]?.GetValue<string>();
// 解析tags数组 - 关键字段
var tags = json["tags"]?.AsArray();
if (tags != null)
{
foreach (var tag in tags)
{
if (tag == null) continue;
var tagId = tag["id"]?.GetValue<string>();
if (string.IsNullOrEmpty(tagId)) continue;
var tagValue = new TagValue
{
Id = tagId,
Description = tag["desc"]?.GetValue<string>(),
Value = ParseTagValue(tag["value"]),
Quality = tag["quality"]?.GetValue<string>(),
Timestamp = DateTime.TryParse(tag["time"]?.GetValue<string>(), 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);
}
}
/// <summary>
/// 解析多设备数据(数组格式)
/// </summary>
public Task<IEnumerable<ParsedDeviceData>> ParseMultiDeviceDataAsync(string rawData, int templateId)
{
try
{
var jsonArray = JsonNode.Parse(rawData)?.AsArray();
if (jsonArray == null)
{
return Task.FromResult<IEnumerable<ParsedDeviceData>>(
Array.Empty<ParsedDeviceData>());
}
var results = new List<ParsedDeviceData>();
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<IEnumerable<ParsedDeviceData>>(results);
}
catch (Exception ex)
{
_logger.LogError(ex, "解析多设备数据异常");
return Task.FromResult<IEnumerable<ParsedDeviceData>>(
Array.Empty<ParsedDeviceData>());
}
}
/// <summary>
/// 验证数据格式
/// </summary>
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;
}
}
/// <summary>
/// 解析Tag值 - 处理数值类型的尾缀去除
/// 发那科返回的数值如 "12345.00000" 需要转换为整数/小数
/// </summary>
private object? ParseTagValue(JsonNode? valueNode)
{
if (valueNode == null)
return null;
var strValue = valueNode.GetValue<string>();
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 ========== 设备采集服务实现 ==========
/// <summary>
/// 设备采集服务实现
///
/// 负责定时轮询采集CNC设备数据支持
/// - Ping检测设备连通性
/// - HTTP请求采集数据
/// - 失败自动重试3次间隔30秒
/// - 并行采集多设备
/// </summary>
public class DeviceCollectionService : IDeviceCollectionService
{
private readonly ILogger<DeviceCollectionService> _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<DeviceCollectionService> logger,
IPingService pingService,
IDataParserService dataParserService,
IDeviceRepository deviceRepository)
{
_logger = logger;
_pingService = pingService;
_dataParserService = dataParserService;
_deviceRepository = deviceRepository;
}
/// <summary>
/// 获取所有设备
/// </summary>
public async Task<IEnumerable<CNCDevice>> GetAllDevicesAsync()
{
return await _deviceRepository.GetAllAsync();
}
/// <summary>
/// 根据ID获取设备
/// </summary>
public async Task<CNCDevice?> GetDeviceByIdAsync(int deviceId)
{
return await _deviceRepository.GetByIdAsync(deviceId);
}
/// <summary>
/// 创建设备
/// </summary>
public async Task<CNCDevice> CreateDeviceAsync(CNCDevice device)
{
device.CreatedAt = DateTime.UtcNow;
device.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.AddAsync(device);
await _deviceRepository.SaveAsync();
return device;
}
/// <summary>
/// 更新设备信息
/// </summary>
public async Task<CNCDevice?> UpdateDeviceAsync(CNCDevice device)
{
device.UpdatedAt = DateTime.UtcNow;
_deviceRepository.Update(device);
var affected = await _deviceRepository.SaveAsync();
return affected > 0 ? device : null;
}
/// <summary>
/// 删除设备
/// </summary>
public async Task<bool> DeleteDeviceAsync(int deviceId)
{
var device = await _deviceRepository.GetByIdAsync(deviceId);
if (device == null) return false;
_deviceRepository.Remove(device);
return await _deviceRepository.SaveAsync() > 0;
}
/// <summary>
/// 采集指定设备数据
/// 采集流程Ping检测 -> HTTP请求 -> 数据解析 -> 存储
/// </summary>
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);
}
/// <summary>
/// 采集所有设备数据
/// </summary>
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("采集任务完成");
}
/// <summary>
/// 获取设备状态
/// </summary>
public Task<DeviceStatus> GetDeviceStatusAsync(int deviceId)
{
return Task.FromResult(new DeviceStatus
{
DeviceId = deviceId,
Timestamp = DateTime.UtcNow
});
}
/// <summary>
/// 获取设备健康状态
/// </summary>
public Task<DeviceHealth> GetDeviceHealthAsync(int deviceId)
{
return Task.FromResult(new DeviceHealth
{
DeviceId = deviceId,
IsHealthy = true,
LastCheck = DateTime.UtcNow
});
}
/// <summary>
/// 获取设备当前状态
/// </summary>
public Task<DeviceStatus> GetDeviceCurrentStatusAsync(int deviceId)
{
return GetDeviceStatusAsync(deviceId);
}
#region 私有方法
/// <summary>
/// HTTP采集数据
/// </summary>
private async Task<string?> 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;
}
}
/// <summary>
/// 更新设备在线状态
/// </summary>
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();
}
}
/// <summary>
/// 更新最后采集时间
/// </summary>
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 ========== 产量计算服务实现 ==========
/// <summary>
/// 产量计算服务实现
///
/// 核心差分计算逻辑:
/// 1. 同一程序连续加工:产量 = MAX(0, 当前累计数 - 上次累计数)
/// 2. 程序切换(A→B)A产量锁定B以当前累计数为新起点
/// 3. 切回历史程序(B→A)视为A重新开始增量累加到当日产量
/// 4. 跨天处理0点自动重置新日期以首次采集累计值为起点
/// </summary>
public class ProductionCalculator : IProductionCalculator
{
private readonly ILogger<ProductionCalculator> _logger;
// 存储每个设备的生产状态设备ID -> (程序名, 上次累计数, 记录时间, 上次日期)
private readonly Dictionary<int, DeviceProductionState> _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<ProductionCalculator> logger)
{
_logger = logger;
}
/// <summary>
/// 计算生产增量
/// </summary>
public Task<decimal> 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);
}
/// <summary>
/// 重置设备生产状态
/// </summary>
public void ResetDeviceProductionState(int deviceId)
{
lock (_productionStates)
{
if (_productionStates.ContainsKey(deviceId))
{
_productionStates.Remove(deviceId);
_logger.LogInformation("设备{DeviceId}生产状态已重置", deviceId);
}
}
}
/// <summary>
/// 验证生产数据有效性
/// </summary>
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 ========== 数据存储服务实现 ==========
/// <summary>
/// 数据存储服务实现
///
/// 负责将解析后的数据存储到数据库:
/// - 原始JSON存入日志库(cnc_log)
/// - 解析后结构化数据存入业务库(cnc_business)
/// </summary>
public class DataStorageService : IDataStorageService
{
private readonly ILogger<DataStorageService> _logger;
private readonly IDeviceRepository _deviceRepository;
public DataStorageService(
ILogger<DataStorageService> logger,
IDeviceRepository deviceRepository)
{
_logger = logger;
_deviceRepository = deviceRepository;
}
/// <summary>
/// 存储设备数据
/// </summary>
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;
}
/// <summary>
/// 批量存储设备数据
/// </summary>
public Task StoreDeviceDataBatchAsync(IEnumerable<ParsedDeviceData> dataList)
{
foreach (var data in dataList)
{
try
{
StoreDeviceDataAsync(data).Wait();
}
catch (Exception ex)
{
_logger.LogError(ex, "批量存储中设备{DeviceId}失败", data.DeviceId);
}
}
return Task.CompletedTask;
}
/// <summary>
/// 存储生产记录
/// </summary>
public Task StoreProductionRecordAsync(ProductionRecord record)
{
// TODO: 调用ProductionRepository存储
_logger.LogDebug("存储生产记录: DeviceId={DeviceId}, Quantity={Quantity}",
record.DeviceId, record.Quantity);
return Task.CompletedTask;
}
/// <summary>
/// 更新设备状态
/// </summary>
public Task UpdateDeviceStatusAsync(int deviceId, DeviceStatus status)
{
_deviceRepository.UpdateDeviceStatusAsync(deviceId, true, true).Wait();
return Task.CompletedTask;
}
}
#endregion
}