升级到 .NET 8.0,重构核心服务接口层
- 删除 Haoliang.Tests 项目(用户要求先保证项目功能实现) - 重构 Haoliang.Core 服务接口定义(IServices.cs,约2200行) - 创建服务桩实现(StubServices.cs)使核心项目可编译 - 创建自定义异常类(Exceptions.cs) - 更新 Haoliang.Models 数据模型 - ApiResponse 方法重命名(Success -> Ok, Error -> ErrorResult 等) - 修复命名空间歧义和类型冲突 - Haoliang.Api 项目暂未完全编译,待后续处理main
parent
a7881ff7d0
commit
8022fafd55
@ -1,552 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IAlarmNotificationService
|
|
||||||
{
|
|
||||||
Task SendAlarmNotificationAsync(Alarm alarm);
|
|
||||||
Task SendBulkAlarmNotificationsAsync(IEnumerable<Alarm> alarms);
|
|
||||||
Task<bool> SendSmsNotificationAsync(string phoneNumber, string message);
|
|
||||||
Task<bool> SendEmailNotificationAsync(string email, string subject, string message);
|
|
||||||
Task<bool> SendWechatNotificationAsync(string openId, string message);
|
|
||||||
Task<IEnumerable<AlarmNotification>> GetNotificationHistoryAsync(DateTime startDate, DateTime endDate);
|
|
||||||
Task<bool> ConfigureNotificationChannelAsync(NotificationChannel channel);
|
|
||||||
Task<IEnumerable<NotificationChannel>> GetAvailableChannelsAsync();
|
|
||||||
Task TestNotificationChannelAsync(NotificationChannel channel);
|
|
||||||
Task<int> GetFailedNotificationCountAsync(DateTime date);
|
|
||||||
Task<bool> ResendFailedNotificationsAsync(int maxRetries = 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AlarmNotificationService : IAlarmNotificationService
|
|
||||||
{
|
|
||||||
private readonly IAlarmNotificationRepository _notificationRepository;
|
|
||||||
private readonly ISystemConfigRepository _configRepository;
|
|
||||||
private readonly ILoggingService _loggingService;
|
|
||||||
private readonly INotificationSender _smsSender;
|
|
||||||
private readonly INotificationSender _emailSender;
|
|
||||||
private readonly INotificationSender _wechatSender;
|
|
||||||
private readonly ConcurrentDictionary<string, NotificationChannel> _channels;
|
|
||||||
|
|
||||||
public AlarmNotificationService(
|
|
||||||
IAlarmNotificationRepository notificationRepository,
|
|
||||||
ISystemConfigRepository configRepository,
|
|
||||||
ILoggingService loggingService,
|
|
||||||
INotificationSender smsSender,
|
|
||||||
INotificationSender emailSender,
|
|
||||||
INotificationSender wechatSender)
|
|
||||||
{
|
|
||||||
_notificationRepository = notificationRepository;
|
|
||||||
_configRepository = configRepository;
|
|
||||||
_loggingService = loggingService;
|
|
||||||
_smsSender = smsSender;
|
|
||||||
_emailSender = emailSender;
|
|
||||||
_wechatSender = wechatSender;
|
|
||||||
_channels = new ConcurrentDictionary<string, NotificationChannel>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendAlarmNotificationAsync(Alarm alarm)
|
|
||||||
{
|
|
||||||
if (alarm == null)
|
|
||||||
throw new ArgumentNullException(nameof(alarm));
|
|
||||||
|
|
||||||
if (!alarm.IsActive)
|
|
||||||
{
|
|
||||||
await _loggingService.LogDebugAsync($"Skipping notification for inactive alarm {alarm.AlarmId}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get notification channels for this alarm
|
|
||||||
var channels = await GetNotificationChannelsForAlarm(alarm);
|
|
||||||
|
|
||||||
foreach (var channel in channels)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await SendNotificationViaChannelAsync(alarm, channel);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"Failed to send alarm {alarm.AlarmId} via {channel.ChannelType}: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendBulkAlarmNotificationsAsync(IEnumerable<Alarm> alarms)
|
|
||||||
{
|
|
||||||
if (alarms == null)
|
|
||||||
throw new ArgumentNullException(nameof(alarms));
|
|
||||||
|
|
||||||
var alarmList = alarms.ToList();
|
|
||||||
await _loggingService.LogInformationAsync($"Processing bulk notifications for {alarmList.Count} alarms");
|
|
||||||
|
|
||||||
var notificationTasks = alarmList.Select(alarm => SendAlarmNotificationAsync(alarm));
|
|
||||||
await Task.WhenAll(notificationTasks);
|
|
||||||
|
|
||||||
await _loggingService.LogInformationAsync($"Completed bulk notifications for {alarmList.Count} alarms");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SendSmsNotificationAsync(string phoneNumber, string message)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var smsConfig = await GetSmsConfigurationAsync();
|
|
||||||
if (!smsConfig.IsEnabled)
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync("SMS notifications are disabled");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _smsSender.SendAsync(phoneNumber, message, smsConfig);
|
|
||||||
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
await _loggingService.LogInformationAsync($"SMS sent successfully to {phoneNumber}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"SMS failed to {phoneNumber}: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Success;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"SMS notification error: {ex.Message}", ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SendEmailNotificationAsync(string email, string subject, string message)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var emailConfig = await GetEmailConfigurationAsync();
|
|
||||||
if (!emailConfig.IsEnabled)
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync("Email notifications are disabled");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _emailSender.SendAsync(email, subject, message, emailConfig);
|
|
||||||
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
await _loggingService.LogInformationAsync($"Email sent successfully to {email}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"Email failed to {email}: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Success;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"Email notification error: {ex.Message}", ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SendWechatNotificationAsync(string openId, string message)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var wechatConfig = await GetWechatConfigurationAsync();
|
|
||||||
if (!wechatConfig.IsEnabled)
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync("WeChat notifications are disabled");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _wechatSender.SendAsync(openId, message, wechatConfig);
|
|
||||||
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
await _loggingService.LogInformationAsync($"WeChat message sent successfully to {openId}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"WeChat message failed to {openId}: {result.ErrorMessage}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Success;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"WeChat notification error: {ex.Message}", ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<AlarmNotification>> GetNotificationHistoryAsync(DateTime startDate, DateTime endDate)
|
|
||||||
{
|
|
||||||
return await _notificationRepository.GetNotificationsByDateRangeAsync(startDate, endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ConfigureNotificationChannelAsync(NotificationChannel channel)
|
|
||||||
{
|
|
||||||
if (channel == null)
|
|
||||||
throw new ArgumentNullException(nameof(channel));
|
|
||||||
|
|
||||||
// Validate channel configuration
|
|
||||||
if (!await ValidateChannelConfigurationAsync(channel))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Save channel configuration
|
|
||||||
await _configRepository.UpsertAsync(new SystemConfig
|
|
||||||
{
|
|
||||||
ConfigKey = $"notification_{channel.ChannelType}_enabled",
|
|
||||||
ConfigValue = channel.IsEnabled.ToString(),
|
|
||||||
Category = "Notification",
|
|
||||||
Description = $"Enable {channel.ChannelType} notifications"
|
|
||||||
});
|
|
||||||
|
|
||||||
await _configRepository.UpsertAsync(new SystemConfig
|
|
||||||
{
|
|
||||||
ConfigKey = $"notification_{channel.ChannelType}_config",
|
|
||||||
ConfigValue = System.Text.Json.JsonSerializer.Serialize(channel.Settings),
|
|
||||||
Category = "Notification",
|
|
||||||
Description = $"{channel.ChannelType} notification settings"
|
|
||||||
});
|
|
||||||
|
|
||||||
_channels[channel.ChannelType] = channel;
|
|
||||||
|
|
||||||
await _loggingService.LogInformationAsync($"Configured notification channel: {channel.ChannelType}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<NotificationChannel>> GetAvailableChannelsAsync()
|
|
||||||
{
|
|
||||||
var channels = new List<NotificationChannel>();
|
|
||||||
|
|
||||||
// SMS Channel
|
|
||||||
if (await IsChannelEnabledAsync("SMS"))
|
|
||||||
{
|
|
||||||
channels.Add(new NotificationChannel
|
|
||||||
{
|
|
||||||
ChannelType = "SMS",
|
|
||||||
IsEnabled = true,
|
|
||||||
Settings = await GetSmsConfigurationAsync()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email Channel
|
|
||||||
if (await IsChannelEnabledAsync("Email"))
|
|
||||||
{
|
|
||||||
channels.Add(new NotificationChannel
|
|
||||||
{
|
|
||||||
ChannelType = "Email",
|
|
||||||
IsEnabled = true,
|
|
||||||
Settings = await GetEmailConfigurationAsync()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// WeChat Channel
|
|
||||||
if (await IsChannelEnabledAsync("WeChat"))
|
|
||||||
{
|
|
||||||
channels.Add(new NotificationChannel
|
|
||||||
{
|
|
||||||
ChannelType = "WeChat",
|
|
||||||
IsEnabled = true,
|
|
||||||
Settings = await GetWechatConfigurationAsync()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TestNotificationChannelAsync(NotificationChannel channel)
|
|
||||||
{
|
|
||||||
if (channel == null)
|
|
||||||
throw new ArgumentNullException(nameof(channel));
|
|
||||||
|
|
||||||
var testMessage = $"This is a test notification from CNC System at {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
|
|
||||||
|
|
||||||
switch (channel.ChannelType)
|
|
||||||
{
|
|
||||||
case "SMS":
|
|
||||||
await SendSmsNotificationAsync(channel.TestPhoneNumber, testMessage);
|
|
||||||
break;
|
|
||||||
case "Email":
|
|
||||||
await SendEmailNotificationAsync(channel.TestEmail, "CNC System Test", testMessage);
|
|
||||||
break;
|
|
||||||
case "WeChat":
|
|
||||||
await SendWechatNotificationAsync(channel.TestOpenId, testMessage);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException($"Unsupported channel type: {channel.ChannelType}");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _loggingService.LogInformationAsync($"Test notification sent via {channel.ChannelType}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetFailedNotificationCountAsync(DateTime date)
|
|
||||||
{
|
|
||||||
var startOfDay = date.Date;
|
|
||||||
var endOfDay = startOfDay.AddDays(1);
|
|
||||||
|
|
||||||
return await _notificationRepository.GetFailedNotificationsCountAsync(startOfDay, endOfDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ResendFailedNotificationsAsync(int maxRetries = 3)
|
|
||||||
{
|
|
||||||
var failedNotifications = await _notificationRepository.GetFailedNotificationsAsync(maxRetries);
|
|
||||||
|
|
||||||
if (!failedNotifications.Any())
|
|
||||||
{
|
|
||||||
await _loggingService.LogInformationAsync("No failed notifications to resend");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var successCount = 0;
|
|
||||||
var retryTasks = failedNotifications.Select(async notification =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RetryFailedNotificationAsync(notification);
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"Failed to resend notification {notification.NotificationId}: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.WhenAll(retryTasks);
|
|
||||||
|
|
||||||
await _loggingService.LogInformationAsync($"Resent {successCount}/{failedNotifications.Count} failed notifications");
|
|
||||||
return successCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IEnumerable<NotificationChannel>> GetNotificationChannelsForAlarm(Alarm alarm)
|
|
||||||
{
|
|
||||||
var channels = new List<NotificationChannel>();
|
|
||||||
var alarmType = alarm.AlarmType.ToLower();
|
|
||||||
|
|
||||||
// Get global notification settings
|
|
||||||
var smsEnabled = await IsChannelEnabledAsync("SMS");
|
|
||||||
var emailEnabled = await IsChannelEnabledAsync("Email");
|
|
||||||
var wechatEnabled = await IsChannelEnabledAsync("WeChat");
|
|
||||||
|
|
||||||
// High severity alarms get all channels
|
|
||||||
if (alarm.AlarmSeverity == AlarmSeverity.Critical || alarm.AlarmSeverity == AlarmSeverity.High)
|
|
||||||
{
|
|
||||||
if (smsEnabled) channels.Add(await CreateSmsChannelAsync());
|
|
||||||
if (emailEnabled) channels.Add(await CreateEmailChannelAsync());
|
|
||||||
if (wechatEnabled) channels.Add(await CreateWechatChannelAsync());
|
|
||||||
}
|
|
||||||
// Medium severity alarms get email and SMS
|
|
||||||
else if (alarm.AlarmSeverity == AlarmSeverity.Medium)
|
|
||||||
{
|
|
||||||
if (emailEnabled) channels.Add(await CreateEmailChannelAsync());
|
|
||||||
if (smsEnabled) channels.Add(await CreateSmsChannelAsync());
|
|
||||||
}
|
|
||||||
// Low severity alarms get email only
|
|
||||||
else if (alarm.AlarmSeverity == AlarmSeverity.Low && emailEnabled)
|
|
||||||
{
|
|
||||||
channels.Add(await CreateEmailChannelAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendNotificationViaChannelAsync(Alarm alarm, NotificationChannel channel)
|
|
||||||
{
|
|
||||||
var subject = $"CNC Alarm: {alarm.AlarmType} - {alarm.DeviceName}";
|
|
||||||
var message = CreateAlarmMessage(alarm);
|
|
||||||
|
|
||||||
switch (channel.ChannelType)
|
|
||||||
{
|
|
||||||
case "SMS":
|
|
||||||
await SendSmsNotificationAsync(channel.Recipients.FirstOrDefault(), message);
|
|
||||||
break;
|
|
||||||
case "Email":
|
|
||||||
await SendEmailNotificationAsync(channel.Recipients.FirstOrDefault(), subject, message);
|
|
||||||
break;
|
|
||||||
case "WeChat":
|
|
||||||
await SendWechatNotificationAsync(channel.Recipients.FirstOrDefault(), message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CreateAlarmMessage(Alarm alarm)
|
|
||||||
{
|
|
||||||
return $@"
|
|
||||||
🚨 CNC System Alarm Alert 🚨
|
|
||||||
|
|
||||||
Device: {alarm.DeviceName} ({alarm.DeviceCode})
|
|
||||||
Type: {alarm.AlarmType}
|
|
||||||
Severity: {alarm.AlarmSeverity}
|
|
||||||
Time: {alarm.CreateTime:yyyy-MM-dd HH:mm:ss}
|
|
||||||
|
|
||||||
Description: {alarm.Title}
|
|
||||||
|
|
||||||
Please take immediate action.
|
|
||||||
".Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NotificationSettings> GetSmsConfigurationAsync()
|
|
||||||
{
|
|
||||||
// Get SMS configuration from database or use defaults
|
|
||||||
return new NotificationSettings
|
|
||||||
{
|
|
||||||
IsEnabled = true,
|
|
||||||
ApiKey = await _configRepository.GetValueAsync("sms_api_key") ?? "",
|
|
||||||
ApiSecret = await _configRepository.GetValueAsync("sms_api_secret") ?? "",
|
|
||||||
Provider = await _configRepository.GetValueAsync("sms_provider") ?? "twilio"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NotificationSettings> GetEmailConfigurationAsync()
|
|
||||||
{
|
|
||||||
return new NotificationSettings
|
|
||||||
{
|
|
||||||
IsEnabled = true,
|
|
||||||
SmtpServer = await _configRepository.GetValueAsync("smtp_server") ?? "smtp.gmail.com",
|
|
||||||
SmtpPort = int.Parse(await _configRepository.GetValueAsync("smtp_port") ?? "587"),
|
|
||||||
Username = await _configRepository.GetValueAsync("smtp_username") ?? "",
|
|
||||||
Password = await _configRepository.GetValueAsync("smtp_password") ?? "",
|
|
||||||
UseSsl = bool.Parse(await _configRepository.GetValueAsync("smtp_ssl") ?? "true")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NotificationSettings> GetWechatConfigurationAsync()
|
|
||||||
{
|
|
||||||
return new NotificationSettings
|
|
||||||
{
|
|
||||||
IsEnabled = true,
|
|
||||||
AppId = await _configRepository.GetValueAsync("wechat_app_id") ?? "",
|
|
||||||
AppSecret = await _configRepository.GetValueAsync("wechat_app_secret") ?? "",
|
|
||||||
TemplateId = await _configRepository.GetValueAsync("wechat_template_id") ?? ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> IsChannelEnabledAsync(string channelType)
|
|
||||||
{
|
|
||||||
var configKey = $"notification_{channelType}_enabled";
|
|
||||||
var configValue = await _configRepository.GetValueAsync(configKey);
|
|
||||||
return bool.TryParse(configValue, out bool enabled) && enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> ValidateChannelConfigurationAsync(NotificationChannel channel)
|
|
||||||
{
|
|
||||||
// Add validation logic based on channel type
|
|
||||||
switch (channel.ChannelType)
|
|
||||||
{
|
|
||||||
case "SMS":
|
|
||||||
return !string.IsNullOrEmpty(channel.TestPhoneNumber);
|
|
||||||
case "Email":
|
|
||||||
return !string.IsNullOrEmpty(channel.TestEmail);
|
|
||||||
case "WeChat":
|
|
||||||
return !string.IsNullOrEmpty(channel.TestOpenId);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NotificationChannel> CreateSmsChannelAsync()
|
|
||||||
{
|
|
||||||
return new NotificationChannel
|
|
||||||
{
|
|
||||||
ChannelType = "SMS",
|
|
||||||
IsEnabled = true,
|
|
||||||
Recipients = await GetSmsRecipientsAsync(),
|
|
||||||
TestPhoneNumber = await _configRepository.GetValueAsync("sms_test_phone") ?? "+1234567890",
|
|
||||||
Settings = await GetSmsConfigurationAsync()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NotificationChannel> CreateEmailChannelAsync()
|
|
||||||
{
|
|
||||||
return new NotificationChannel
|
|
||||||
{
|
|
||||||
ChannelType = "Email",
|
|
||||||
IsEnabled = true,
|
|
||||||
Recipients = await GetEmailRecipientsAsync(),
|
|
||||||
TestEmail = await _configRepository.GetValueAsync("email_test_address") ?? "test@example.com",
|
|
||||||
Settings = await GetEmailConfigurationAsync()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NotificationChannel> CreateWechatChannelAsync()
|
|
||||||
{
|
|
||||||
return new NotificationChannel
|
|
||||||
{
|
|
||||||
ChannelType = "WeChat",
|
|
||||||
IsEnabled = true,
|
|
||||||
Recipients = await GetWechatRecipientsAsync(),
|
|
||||||
TestOpenId = await _configRepository.GetValueAsync("wechat_test_openid") ?? "test_openid",
|
|
||||||
Settings = await GetWechatConfigurationAsync()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<string>> GetSmsRecipientsAsync()
|
|
||||||
{
|
|
||||||
var configValue = await _configRepository.GetValueAsync("sms_recipients");
|
|
||||||
return string.IsNullOrEmpty(configValue) ? new List<string>() : configValue.Split(',').Select(s => s.Trim()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<string>> GetEmailRecipientsAsync()
|
|
||||||
{
|
|
||||||
var configValue = await _configRepository.GetValueAsync("email_recipients");
|
|
||||||
return string.IsNullOrEmpty(configValue) ? new List<string>() : configValue.Split(',').Select(s => s.Trim()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<string>> GetWechatRecipientsAsync()
|
|
||||||
{
|
|
||||||
var configValue = await _configRepository.GetValueAsync("wechat_recipients");
|
|
||||||
return string.IsNullOrEmpty(configValue) ? new List<string>() : configValue.Split(',').Select(s => s.Trim()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RetryFailedNotificationAsync(AlarmNotification notification)
|
|
||||||
{
|
|
||||||
// Retry logic here
|
|
||||||
await _notificationRepository.MarkAsRetriedAsync(notification.NotificationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supporting interfaces and classes
|
|
||||||
public interface INotificationSender
|
|
||||||
{
|
|
||||||
Task<NotificationResult> SendAsync(string recipient, string message, NotificationSettings settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotificationResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
public string ReferenceId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotificationSettings
|
|
||||||
{
|
|
||||||
public bool IsEnabled { get; set; }
|
|
||||||
public string ApiKey { get; set; }
|
|
||||||
public string ApiSecret { get; set; }
|
|
||||||
public string Provider { get; set; }
|
|
||||||
public string SmtpServer { get; set; }
|
|
||||||
public int SmtpPort { get; set; }
|
|
||||||
public string Username { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
public bool UseSsl { get; set; }
|
|
||||||
public string AppId { get; set; }
|
|
||||||
public string AppSecret { get; set; }
|
|
||||||
public string TemplateId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional repository interface for alarm notifications
|
|
||||||
public interface IAlarmNotificationRepository : IRepository<AlarmNotification>
|
|
||||||
{
|
|
||||||
Task<IEnumerable<AlarmNotification>> GetNotificationsByDateRangeAsync(DateTime startDate, DateTime endDate);
|
|
||||||
Task<IEnumerable<AlarmNotification>> GetFailedNotificationsAsync(int maxRetries = 3);
|
|
||||||
Task<int> GetFailedNotificationsCountAsync(DateTime startDate, DateTime endDate);
|
|
||||||
Task<bool> MarkAsRetriedAsync(int notificationId);
|
|
||||||
Task<IEnumerable<AlarmNotification>> GetNotificationsByAlarmAsync(int alarmId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IAlarmRuleService
|
|
||||||
{
|
|
||||||
Task<AlarmRule> CreateAlarmRuleAsync(AlarmRule rule);
|
|
||||||
Task<AlarmRule> UpdateAlarmRuleAsync(int ruleId, AlarmRule rule);
|
|
||||||
Task<bool> DeleteAlarmRuleAsync(int ruleId);
|
|
||||||
Task<AlarmRule> GetAlarmRuleByIdAsync(int ruleId);
|
|
||||||
Task<IEnumerable<AlarmRule>> GetAllAlarmRulesAsync();
|
|
||||||
Task<IEnumerable<AlarmRule>> GetActiveAlarmRulesAsync();
|
|
||||||
Task<IEnumerable<AlarmRule>> GetRulesByDeviceAsync(int deviceId);
|
|
||||||
Task<bool> EvaluateAlarmRuleAsync(AlarmRule rule, DeviceCurrentStatus status);
|
|
||||||
Task<Alarm> GenerateAlarmFromRuleAsync(AlarmRule rule, DeviceCurrentStatus status);
|
|
||||||
Task TestAlarmRuleAsync(int ruleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AlarmRuleService : IAlarmRuleService
|
|
||||||
{
|
|
||||||
private readonly IAlarmRuleRepository _alarmRuleRepository;
|
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
|
||||||
private readonly IAlarmRepository _alarmRepository;
|
|
||||||
private readonly ILoggingService _loggingService;
|
|
||||||
|
|
||||||
public AlarmRuleService(
|
|
||||||
IAlarmRuleRepository alarmRuleRepository,
|
|
||||||
IDeviceRepository deviceRepository,
|
|
||||||
IAlarmRepository alarmRepository,
|
|
||||||
ILoggingService loggingService)
|
|
||||||
{
|
|
||||||
_alarmRuleRepository = alarmRuleRepository;
|
|
||||||
_deviceRepository = deviceRepository;
|
|
||||||
_alarmRepository = alarmRepository;
|
|
||||||
_loggingService = loggingService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AlarmRule> CreateAlarmRuleAsync(AlarmRule rule)
|
|
||||||
{
|
|
||||||
// Validate rule
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.RuleName))
|
|
||||||
throw new ArgumentException("Rule name is required");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.Condition))
|
|
||||||
throw new ArgumentException("Condition is required");
|
|
||||||
|
|
||||||
rule.RuleId = 0; // Ensure new rule
|
|
||||||
rule.CreatedAt = DateTime.Now;
|
|
||||||
rule.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
await _alarmRuleRepository.AddAsync(rule);
|
|
||||||
await _alarmRuleRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync($"Created alarm rule: {rule.RuleName}");
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AlarmRule> UpdateAlarmRuleAsync(int ruleId, AlarmRule rule)
|
|
||||||
{
|
|
||||||
var existingRule = await _alarmRuleRepository.GetByIdAsync(ruleId);
|
|
||||||
if (existingRule == null)
|
|
||||||
throw new KeyNotFoundException($"Alarm rule with ID {ruleId} not found");
|
|
||||||
|
|
||||||
existingRule.RuleName = rule.RuleName;
|
|
||||||
existingRule.Condition = rule.Condition;
|
|
||||||
existingRule.AlarmType = rule.AlarmType;
|
|
||||||
existingRule.AlarmSeverity = rule.AlarmSeverity;
|
|
||||||
existingRule.IsActive = rule.IsActive;
|
|
||||||
existingRule.DeviceId = rule.DeviceId;
|
|
||||||
existingRule.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
await _alarmRuleRepository.UpdateAsync(existingRule);
|
|
||||||
await _alarmRuleRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync($"Updated alarm rule: {existingRule.RuleName}");
|
|
||||||
return existingRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteAlarmRuleAsync(int ruleId)
|
|
||||||
{
|
|
||||||
var rule = await _alarmRuleRepository.GetByIdAsync(ruleId);
|
|
||||||
if (rule == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
await _alarmRuleRepository.DeleteAsync(rule);
|
|
||||||
await _alarmRuleRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync($"Deleted alarm rule: {rule.RuleName}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AlarmRule> GetAlarmRuleByIdAsync(int ruleId)
|
|
||||||
{
|
|
||||||
return await _alarmRuleRepository.GetByIdAsync(ruleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<AlarmRule>> GetAllAlarmRulesAsync()
|
|
||||||
{
|
|
||||||
return await _alarmRuleRepository.GetAllAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<AlarmRule>> GetActiveAlarmRulesAsync()
|
|
||||||
{
|
|
||||||
return await _alarmRuleRepository.GetActiveRulesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<AlarmRule>> GetRulesByDeviceAsync(int deviceId)
|
|
||||||
{
|
|
||||||
return await _alarmRuleRepository.GetRulesByDeviceAsync(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> EvaluateAlarmRuleAsync(AlarmRule rule, DeviceCurrentStatus status)
|
|
||||||
{
|
|
||||||
if (!rule.IsActive)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Simple condition evaluation (in real implementation, use expression parser)
|
|
||||||
var condition = rule.Condition.ToLower();
|
|
||||||
|
|
||||||
if (condition.Contains("device_offline") && !status.IsOnline)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (condition.Contains("device_error") && status.Status == "Error")
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (condition.Contains("high_temperature") &&
|
|
||||||
status.Tags?.Any(t => t.Id == "temperature" && Convert.ToDouble(t.Value) > 80) == true)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (condition.Contains("low_production") &&
|
|
||||||
status.CumulativeCount < 10)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Custom condition evaluation
|
|
||||||
if (condition.Contains("running_time") &&
|
|
||||||
status.IsRunning &&
|
|
||||||
(DateTime.Now - status.RecordTime).TotalMinutes > 120)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"Error evaluating alarm rule {rule.RuleName}: {ex.Message}", ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Alarm> GenerateAlarmFromRuleAsync(AlarmRule rule, DeviceCurrentStatus status)
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(status.DeviceId);
|
|
||||||
if (device == null)
|
|
||||||
throw new InvalidOperationException("Device not found");
|
|
||||||
|
|
||||||
var alarm = new Alarm
|
|
||||||
{
|
|
||||||
DeviceId = status.DeviceId,
|
|
||||||
DeviceCode = device.DeviceCode,
|
|
||||||
DeviceName = device.DeviceName,
|
|
||||||
AlarmType = rule.AlarmType.ToString(),
|
|
||||||
AlarmSeverity = rule.AlarmSeverity,
|
|
||||||
Title = $"Alarm triggered by rule: {rule.RuleName}",
|
|
||||||
Description = $"Condition: {rule.Condition}",
|
|
||||||
AlarmStatus = AlarmStatus.Active,
|
|
||||||
CreateTime = DateTime.Now,
|
|
||||||
IsActive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
await _alarmRepository.AddAsync(alarm);
|
|
||||||
await _alarmRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _loggingService.LogWarningAsync($"Generated alarm: {alarm.Title} for device {device.DeviceCode}");
|
|
||||||
return alarm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TestAlarmRuleAsync(int ruleId)
|
|
||||||
{
|
|
||||||
var rule = await _alarmRuleRepository.GetByIdAsync(ruleId);
|
|
||||||
if (rule == null)
|
|
||||||
throw new KeyNotFoundException($"Alarm rule with ID {ruleId} not found");
|
|
||||||
|
|
||||||
// Get a sample device status for testing
|
|
||||||
var devices = await _deviceRepository.GetAllAsync();
|
|
||||||
var sampleDevice = devices.FirstOrDefault();
|
|
||||||
|
|
||||||
if (sampleDevice == null)
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync($"No devices available to test alarm rule {rule.RuleName}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sampleStatus = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = sampleDevice.Id,
|
|
||||||
DeviceCode = sampleDevice.DeviceCode,
|
|
||||||
DeviceName = sampleDevice.DeviceName,
|
|
||||||
IsOnline = true,
|
|
||||||
IsAvailable = true,
|
|
||||||
Status = "Running",
|
|
||||||
IsRunning = true,
|
|
||||||
NCProgram = "TEST_PROGRAM",
|
|
||||||
CumulativeCount = 50,
|
|
||||||
OperatingMode = "Auto",
|
|
||||||
RecordTime = DateTime.Now,
|
|
||||||
Tags = new List<TagData>
|
|
||||||
{
|
|
||||||
new TagData { Id = "temperature", Value = 85.0, Time = DateTime.Now },
|
|
||||||
new TagData { Id = "pressure", Value = 120, Time = DateTime.Now }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var shouldTrigger = await EvaluateAlarmRuleAsync(rule, sampleStatus);
|
|
||||||
|
|
||||||
if (shouldTrigger)
|
|
||||||
{
|
|
||||||
await GenerateAlarmFromRuleAsync(rule, sampleStatus);
|
|
||||||
await _loggingService.LogInformationAsync($"Alarm rule test: {rule.RuleName} would trigger an alarm");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _loggingService.LogInformationAsync($"Alarm rule test: {rule.RuleName} would not trigger an alarm");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional repository interface for alarm rules
|
|
||||||
public interface IAlarmRuleRepository : IRepository<AlarmRule>
|
|
||||||
{
|
|
||||||
Task<IEnumerable<AlarmRule>> GetActiveRulesAsync();
|
|
||||||
Task<IEnumerable<AlarmRule>> GetRulesByDeviceAsync(int deviceId);
|
|
||||||
Task<IEnumerable<AlarmRule>> GetRulesByAlarmTypeAsync(AlarmType alarmType);
|
|
||||||
Task<bool> RuleExistsAsync(string ruleName);
|
|
||||||
Task<IEnumerable<AlarmRule>> GetEnabledRulesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IAlarmService
|
|
||||||
{
|
|
||||||
Task<Alarm> CreateAlarmAsync(Alarm alarm);
|
|
||||||
Task<Alarm> UpdateAlarmAsync(int alarmId, Alarm alarm);
|
|
||||||
Task<bool> DeleteAlarmAsync(int alarmId);
|
|
||||||
Task<Alarm> GetAlarmByIdAsync(int alarmId);
|
|
||||||
Task<IEnumerable<Alarm>> GetAllAlarmsAsync();
|
|
||||||
Task<IEnumerable<Alarm>> GetAlarmsByDeviceAsync(int deviceId);
|
|
||||||
Task<IEnumerable<Alarm>> GetAlarmsByTypeAsync(AlarmType type);
|
|
||||||
Task<IEnumerable<Alarm>> GetActiveAlarmsAsync();
|
|
||||||
Task<IEnumerable<Alarm>> GetAlarmsByDateRangeAsync(DateTime startDate, DateTime endDate);
|
|
||||||
Task<bool> ResolveAlarmAsync(int alarmId, string resolutionNote);
|
|
||||||
Task<bool> AcknowledgeAlarmAsync(int alarmId, string acknowledgeNote);
|
|
||||||
Task<AlarmStatistics> GetAlarmStatisticsAsync(DateTime date);
|
|
||||||
Task<IEnumerable<Alarm>> GetCriticalAlarmsAsync();
|
|
||||||
Task<IEnumerable<Alarm>> GetDeviceAlarmsAsync(int deviceId, int days = 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AlarmManager : IAlarmService
|
|
||||||
{
|
|
||||||
private readonly IAlarmRepository _alarmRepository;
|
|
||||||
private readonly IAlarmRuleService _alarmRuleService;
|
|
||||||
private readonly IAlarmNotificationService _notificationService;
|
|
||||||
|
|
||||||
public AlarmManager(
|
|
||||||
IAlarmRepository alarmRepository,
|
|
||||||
IAlarmRuleService alarmRuleService,
|
|
||||||
IAlarmNotificationService notificationService)
|
|
||||||
{
|
|
||||||
_alarmRepository = alarmRepository;
|
|
||||||
_alarmRuleService = alarmRuleService;
|
|
||||||
_notificationService = notificationService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Alarm> CreateAlarmAsync(Alarm alarm)
|
|
||||||
{
|
|
||||||
alarm.AlarmStatus = AlarmStatus.Active;
|
|
||||||
alarm.CreateTime = DateTime.Now;
|
|
||||||
alarm.UpdateTime = DateTime.Now;
|
|
||||||
|
|
||||||
var createdAlarm = await _alarmRepository.AddAsync(alarm);
|
|
||||||
await _notificationService.SendAlarmNotificationAsync(createdAlarm);
|
|
||||||
|
|
||||||
return createdAlarm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Alarm> UpdateAlarmAsync(int alarmId, Alarm alarm)
|
|
||||||
{
|
|
||||||
var existingAlarm = await _alarmRepository.GetByIdAsync(alarmId);
|
|
||||||
if (existingAlarm == null)
|
|
||||||
{
|
|
||||||
throw new KeyNotFoundException($"Alarm with ID {alarmId} not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
alarm.AlarmId = alarmId;
|
|
||||||
alarm.UpdateTime = DateTime.Now;
|
|
||||||
|
|
||||||
var updatedAlarm = await _alarmRepository.UpdateAsync(alarm);
|
|
||||||
return updatedAlarm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteAlarmAsync(int alarmId)
|
|
||||||
{
|
|
||||||
return await _alarmRepository.DeleteAsync(alarmId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Alarm> GetAlarmByIdAsync(int alarmId)
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetByIdAsync(alarmId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Alarm>> GetAllAlarmsAsync()
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetAllAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Alarm>> GetAlarmsByDeviceAsync(int deviceId)
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetByDeviceIdAsync(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Alarm>> GetAlarmsByTypeAsync(AlarmType type)
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetByAlarmTypeAsync(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Alarm>> GetActiveAlarmsAsync()
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetByStatusAsync(AlarmStatus.Active);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Alarm>> GetAlarmsByDateRangeAsync(DateTime startDate, DateTime endDate)
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetByDateRangeAsync(startDate, endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ResolveAlarmAsync(int alarmId, string resolutionNote)
|
|
||||||
{
|
|
||||||
var alarm = await _alarmRepository.GetByIdAsync(alarmId);
|
|
||||||
if (alarm == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
alarm.AlarmStatus = AlarmStatus.Resolved;
|
|
||||||
alarm.ResolutionNote = resolutionNote;
|
|
||||||
alarm.ResolvedTime = DateTime.Now;
|
|
||||||
alarm.UpdateTime = DateTime.Now;
|
|
||||||
|
|
||||||
return await _alarmRepository.UpdateAsync(alarm) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> AcknowledgeAlarmAsync(int alarmId, string acknowledgeNote)
|
|
||||||
{
|
|
||||||
var alarm = await _alarmRepository.GetByIdAsync(alarmId);
|
|
||||||
if (alarm == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
alarm.AlarmStatus = AlarmStatus.Acknowledged;
|
|
||||||
alarm.AcknowledgeNote = acknowledgeNote;
|
|
||||||
alarm.AcknowledgedTime = DateTime.Now;
|
|
||||||
alarm.UpdateTime = DateTime.Now;
|
|
||||||
|
|
||||||
return await _alarmRepository.UpdateAsync(alarm) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AlarmStatistics> GetAlarmStatisticsAsync(DateTime date)
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetAlarmStatisticsAsync(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Alarm>> GetCriticalAlarmsAsync()
|
|
||||||
{
|
|
||||||
return await _alarmRepository.GetBySeverityAsync(AlarmSeverity.Critical);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Alarm>> GetDeviceAlarmsAsync(int deviceId, int days = 7)
|
|
||||||
{
|
|
||||||
var startDate = DateTime.Now.AddDays(-days);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
return await _alarmRepository.GetByDeviceAndDateRangeAsync(deviceId, startDate, endDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,870 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Haoliang.Models.User;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IAuthService
|
|
||||||
{
|
|
||||||
Task<AuthResult> LoginAsync(LoginRequest request);
|
|
||||||
Task<bool> LogoutAsync(int userId);
|
|
||||||
Task<bool> ValidateTokenAsync(string token);
|
|
||||||
Task<UserClaims> GetUserClaimsAsync(int userId);
|
|
||||||
Task<string> GenerateRefreshTokenAsync(int userId);
|
|
||||||
Task<bool> ValidateRefreshTokenAsync(int userId, string refreshToken);
|
|
||||||
Task<AuthResult> RefreshTokenAsync(string refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IUserService
|
|
||||||
{
|
|
||||||
Task<UserViewModel> CreateUserAsync(User user);
|
|
||||||
Task<UserViewModel> UpdateUserAsync(int userId, User user);
|
|
||||||
Task<bool> DeleteUserAsync(int userId);
|
|
||||||
Task<UserViewModel> GetUserByIdAsync(int userId);
|
|
||||||
Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
|
|
||||||
Task<IEnumerable<UserViewModel>> GetUsersByRoleAsync(string roleName);
|
|
||||||
Task<bool> ActivateUserAsync(int userId);
|
|
||||||
Task<bool> DeactivateUserAsync(int userId);
|
|
||||||
Task<bool> ChangePasswordAsync(int userId, string oldPassword, string newPassword);
|
|
||||||
Task<bool> ResetPasswordAsync(int userId, string newPassword);
|
|
||||||
Task<bool> AssignRoleAsync(int userId, int roleId);
|
|
||||||
Task<bool> UnassignRoleAsync(int userId, int roleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPermissionService
|
|
||||||
{
|
|
||||||
Task<IEnumerable<Permission>> GetAllPermissionsAsync();
|
|
||||||
Task<Permission> GetPermissionByIdAsync(int permissionId);
|
|
||||||
Task<IEnumerable<Permission>> GetPermissionsByCategoryAsync(string category);
|
|
||||||
Task<bool> UserHasPermissionAsync(int userId, string permissionName);
|
|
||||||
Task<IEnumerable<string>> GetUserPermissionsAsync(int userId);
|
|
||||||
Task<bool> AddPermissionToRoleAsync(int roleId, int permissionId);
|
|
||||||
Task<bool> RemovePermissionFromRoleAsync(int roleId, int permissionId);
|
|
||||||
Task<bool> CreatePermissionAsync(Permission permission);
|
|
||||||
Task<bool> UpdatePermissionAsync(Permission permission);
|
|
||||||
Task<bool> DeletePermissionAsync(int permissionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ISessionService
|
|
||||||
{
|
|
||||||
Task<UserSession> CreateSessionAsync(int userId, string deviceInfo, string ipAddress);
|
|
||||||
Task<bool> ValidateSessionAsync(string sessionToken);
|
|
||||||
Task<UserSession> GetSessionByTokenAsync(string sessionToken);
|
|
||||||
Task<bool> UpdateSessionActivityAsync(string sessionToken);
|
|
||||||
Task<bool> TerminateSessionAsync(string sessionToken);
|
|
||||||
Task<bool> TerminateAllUserSessionsAsync(int userId);
|
|
||||||
Task<IEnumerable<UserSession>> GetUserSessionsAsync(int userId);
|
|
||||||
Task<bool> CleanupExpiredSessionsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JwtSettings
|
|
||||||
{
|
|
||||||
public string SecretKey { get; set; }
|
|
||||||
public string Issuer { get; set; }
|
|
||||||
public string Audience { get; set; }
|
|
||||||
public int AccessTokenExpirationMinutes { get; set; }
|
|
||||||
public int RefreshTokenExpirationDays { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AuthService : IAuthService
|
|
||||||
{
|
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
private readonly IRoleRepository _roleRepository;
|
|
||||||
private readonly IUserSessionRepository _userSessionRepository;
|
|
||||||
private readonly IPasswordResetRepository _passwordResetRepository;
|
|
||||||
private readonly IOptions<JwtSettings> _jwtSettings;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public AuthService(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IRoleRepository roleRepository,
|
|
||||||
IUserSessionRepository userSessionRepository,
|
|
||||||
IPasswordResetRepository passwordResetRepository,
|
|
||||||
IOptions<JwtSettings> jwtSettings,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_userRepository = userRepository;
|
|
||||||
_roleRepository = roleRepository;
|
|
||||||
_userSessionRepository = userSessionRepository;
|
|
||||||
_passwordResetRepository = passwordResetRepository;
|
|
||||||
_jwtSettings = jwtSettings;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AuthResult> LoginAsync(LoginRequest request)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var user = await _userRepository.AuthenticateAsync(request.Username, request.Password);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
await _logger.LogWarningAsync($"Failed login attempt for username: {request.Username}");
|
|
||||||
return new AuthResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Invalid username or password"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = GenerateJwtToken(user);
|
|
||||||
var refreshToken = await GenerateRefreshTokenAsync(user.Id);
|
|
||||||
|
|
||||||
var userClaims = await GetUserClaimsAsync(user.Id);
|
|
||||||
var permissions = await _roleRepository.GetRolePermissionsAsync(user.RoleId);
|
|
||||||
|
|
||||||
await _logger.LogInformationAsync($"User {user.Username} logged in successfully");
|
|
||||||
|
|
||||||
return new AuthResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Token = token,
|
|
||||||
User = user,
|
|
||||||
Permissions = permissions.Select(p => p.Name).ToList(),
|
|
||||||
ExpiresAt = DateTime.Now.AddMinutes(_jwtSettings.Value.AccessTokenExpirationMinutes)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Login failed: {ex.Message}");
|
|
||||||
return new AuthResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "An error occurred during login"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> LogoutAsync(int userId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sessions = await _userSessionRepository.GetUserSessionsAsync(userId);
|
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
|
||||||
await _userSessionRepository.TerminateSessionAsync(session.SessionToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _logger.LogInformationAsync($"User {userId} logged out");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Logout failed: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateTokenAsync(string token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
var key = Encoding.UTF8.GetBytes(_jwtSettings.Value.SecretKey);
|
|
||||||
|
|
||||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidIssuer = _jwtSettings.Value.Issuer,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidAudience = _jwtSettings.Value.Audience,
|
|
||||||
ValidateLifetime = true,
|
|
||||||
ClockSkew = TimeSpan.Zero
|
|
||||||
}, out SecurityToken validatedToken);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UserClaims> GetUserClaimsAsync(int userId)
|
|
||||||
{
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var role = await _roleRepository.GetByIdAsync(user.RoleId);
|
|
||||||
var permissions = await _roleRepository.GetRolePermissionsAsync(user.RoleId);
|
|
||||||
|
|
||||||
return new UserClaims
|
|
||||||
{
|
|
||||||
UserId = user.Id,
|
|
||||||
Username = user.Username,
|
|
||||||
RealName = user.RealName,
|
|
||||||
RoleId = user.RoleId,
|
|
||||||
RoleName = role?.RoleName ?? "",
|
|
||||||
Permissions = permissions.Select(p => p.Name).ToList(),
|
|
||||||
SessionTime = DateTime.Now
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> GenerateRefreshTokenAsync(int userId)
|
|
||||||
{
|
|
||||||
var refreshToken = Guid.NewGuid().ToString();
|
|
||||||
var expiresAt = DateTime.Now.AddDays(_jwtSettings.Value.RefreshTokenExpirationDays);
|
|
||||||
|
|
||||||
var session = new UserSession
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
SessionToken = refreshToken,
|
|
||||||
DeviceInfo = "",
|
|
||||||
IPAddress = "",
|
|
||||||
LoginTime = DateTime.Now,
|
|
||||||
LastActivityTime = DateTime.Now,
|
|
||||||
IsActive = true,
|
|
||||||
CreatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
await _userSessionRepository.AddAsync(session);
|
|
||||||
await _userSessionRepository.SaveAsync();
|
|
||||||
|
|
||||||
return refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateRefreshTokenAsync(int userId, string refreshToken)
|
|
||||||
{
|
|
||||||
var session = await _userSessionRepository.GetSessionByTokenAsync(refreshToken);
|
|
||||||
return session != null && session.UserId == userId && session.IsActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<AuthResult> RefreshTokenAsync(string refreshToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var session = await _userSessionRepository.GetSessionByTokenAsync(refreshToken);
|
|
||||||
if (session == null || !session.IsActive)
|
|
||||||
{
|
|
||||||
return new AuthResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Invalid refresh token"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _userRepository.GetByIdAsync(session.UserId);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return new AuthResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "User not found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var newToken = GenerateJwtToken(user);
|
|
||||||
var newRefreshToken = await GenerateRefreshTokenAsync(user.Id);
|
|
||||||
|
|
||||||
// Terminate old refresh token
|
|
||||||
await _userSessionRepository.TerminateSessionAsync(refreshToken);
|
|
||||||
|
|
||||||
var userClaims = await GetUserClaimsAsync(user.Id);
|
|
||||||
var permissions = await _roleRepository.GetRolePermissionsAsync(user.RoleId);
|
|
||||||
|
|
||||||
return new AuthResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Token = newToken,
|
|
||||||
RefreshToken = newRefreshToken,
|
|
||||||
User = user,
|
|
||||||
Permissions = permissions.Select(p => p.Name).ToList(),
|
|
||||||
ExpiresAt = DateTime.Now.AddMinutes(_jwtSettings.Value.AccessTokenExpirationMinutes)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Token refresh failed: {ex.Message}");
|
|
||||||
return new AuthResult
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "Failed to refresh token"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GenerateJwtToken(User user)
|
|
||||||
{
|
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
var key = Encoding.UTF8.GetBytes(_jwtSettings.Value.SecretKey);
|
|
||||||
|
|
||||||
var claims = new[]
|
|
||||||
{
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
|
||||||
new Claim(ClaimTypes.Name, user.Username),
|
|
||||||
new Claim(ClaimTypes.GivenName, user.RealName),
|
|
||||||
new Claim(ClaimTypes.Email, user.Email),
|
|
||||||
new Claim(ClaimTypes.Role, user.RoleId.ToString())
|
|
||||||
};
|
|
||||||
|
|
||||||
var tokenDescriptor = new SecurityTokenDescriptor
|
|
||||||
{
|
|
||||||
Subject = new ClaimsIdentity(claims),
|
|
||||||
Expires = DateTime.Now.AddMinutes(_jwtSettings.Value.AccessTokenExpirationMinutes),
|
|
||||||
Issuer = _jwtSettings.Value.Issuer,
|
|
||||||
Audience = _jwtSettings.Value.Audience,
|
|
||||||
SigningCredentials = new SigningCredentials(
|
|
||||||
new SymmetricSecurityKey(key),
|
|
||||||
SecurityAlgorithms.HmacSha256Signature)
|
|
||||||
};
|
|
||||||
|
|
||||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
|
||||||
return tokenHandler.WriteToken(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UserService : IUserService
|
|
||||||
{
|
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
private readonly IRoleRepository _roleRepository;
|
|
||||||
private readonly IEmployeeRepository _employeeRepository;
|
|
||||||
private readonly IPermissionService _permissionService;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public UserService(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IRoleRepository roleRepository,
|
|
||||||
IEmployeeRepository employeeRepository,
|
|
||||||
IPermissionService permissionService,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_userRepository = userRepository;
|
|
||||||
_roleRepository = roleRepository;
|
|
||||||
_employeeRepository = employeeRepository;
|
|
||||||
_permissionService = permissionService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UserViewModel> CreateUserAsync(User user)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (await _userRepository.UsernameExistsAsync(user.Username))
|
|
||||||
{
|
|
||||||
throw new Exception("Username already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await _userRepository.EmailExistsAsync(user.Email))
|
|
||||||
{
|
|
||||||
throw new Exception("Email already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
user.PasswordHash = HashPassword(user.PasswordHash);
|
|
||||||
user.IsActive = true;
|
|
||||||
user.CreatedAt = DateTime.Now;
|
|
||||||
user.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
await _userRepository.AddAsync(user);
|
|
||||||
await _userRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _logger.LogInformationAsync($"User created: {user.Username}");
|
|
||||||
|
|
||||||
return await GetUserByIdAsync(user.Id);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to create user: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UserViewModel> UpdateUserAsync(int userId, User user)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var existingUser = await _userRepository.GetByIdAsync(userId);
|
|
||||||
if (existingUser == null)
|
|
||||||
throw new Exception("User not found");
|
|
||||||
|
|
||||||
// Don't update username if it hasn't changed
|
|
||||||
if (existingUser.Username != user.Username)
|
|
||||||
{
|
|
||||||
if (await _userRepository.UsernameExistsAsync(user.Username))
|
|
||||||
throw new Exception("Username already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't update email if it hasn't changed
|
|
||||||
if (existingUser.Email != user.Email)
|
|
||||||
{
|
|
||||||
if (await _userRepository.EmailExistsAsync(user.Email))
|
|
||||||
throw new Exception("Email already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user properties
|
|
||||||
existingUser.RealName = user.RealName;
|
|
||||||
existingUser.Email = user.Email;
|
|
||||||
existingUser.Phone = user.Phone;
|
|
||||||
existingUser.RoleId = user.RoleId;
|
|
||||||
existingUser.IsActive = user.IsActive;
|
|
||||||
existingUser.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(user.PasswordHash))
|
|
||||||
{
|
|
||||||
existingUser.PasswordHash = HashPassword(user.PasswordHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
_userRepository.Update(existingUser);
|
|
||||||
await _userRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _logger.LogInformationAsync($"User updated: {existingUser.Username}");
|
|
||||||
|
|
||||||
return await GetUserByIdAsync(userId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to update user: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteUserAsync(int userId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if user has active sessions
|
|
||||||
// (Assuming you have a session repository)
|
|
||||||
|
|
||||||
// Check if user is assigned to devices
|
|
||||||
var employee = await _employeeRepository.GetByEmployeeCodeAsync(user.Username);
|
|
||||||
if (employee != null && employee.AssignedDevices.Any())
|
|
||||||
{
|
|
||||||
throw new Exception("Cannot delete user that is assigned to devices");
|
|
||||||
}
|
|
||||||
|
|
||||||
_userRepository.Remove(user);
|
|
||||||
await _userRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _logger.LogInformationAsync($"User deleted: {user.Username}");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to delete user: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UserViewModel> GetUserByIdAsync(int userId)
|
|
||||||
{
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var role = await _roleRepository.GetByIdAsync(user.RoleId);
|
|
||||||
var permissions = await _roleRepository.GetRolePermissionsAsync(user.RoleId);
|
|
||||||
|
|
||||||
return new UserViewModel
|
|
||||||
{
|
|
||||||
Id = user.Id,
|
|
||||||
Username = user.Username,
|
|
||||||
RealName = user.RealName,
|
|
||||||
Email = user.Email,
|
|
||||||
Phone = user.Phone,
|
|
||||||
RoleName = role?.RoleName ?? "",
|
|
||||||
IsActive = user.IsActive,
|
|
||||||
LastLoginTime = user.LastLoginTime,
|
|
||||||
CreatedAt = user.CreatedAt,
|
|
||||||
Permissions = permissions.Select(p => p.Name).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<UserViewModel>> GetAllUsersAsync()
|
|
||||||
{
|
|
||||||
var users = await _userRepository.GetAllAsync();
|
|
||||||
var userViewModels = new List<UserViewModel>();
|
|
||||||
|
|
||||||
foreach (var user in users)
|
|
||||||
{
|
|
||||||
userViewModels.Add(await GetUserByIdAsync(user.Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return userViewModels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<UserViewModel>> GetUsersByRoleAsync(string roleName)
|
|
||||||
{
|
|
||||||
var role = await _roleRepository.GetByNameAsync(roleName);
|
|
||||||
if (role == null)
|
|
||||||
return new List<UserViewModel>();
|
|
||||||
|
|
||||||
var users = await _userRepository.GetByRoleIdAsync(role.Id);
|
|
||||||
var userViewModels = new List<UserViewModel>();
|
|
||||||
|
|
||||||
foreach (var user in users)
|
|
||||||
{
|
|
||||||
userViewModels.Add(await GetUserByIdAsync(user.Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return userViewModels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ActivateUserAsync(int userId)
|
|
||||||
{
|
|
||||||
return await SetUserActiveStatusAsync(userId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeactivateUserAsync(int userId)
|
|
||||||
{
|
|
||||||
return await SetUserActiveStatusAsync(userId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ChangePasswordAsync(int userId, string oldPassword, string newPassword)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _userRepository.ChangePasswordAsync(userId, oldPassword, newPassword);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to change password: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ResetPasswordAsync(int userId, string newPassword)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _userRepository.ResetPasswordAsync(userId, newPassword);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to reset password: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> AssignRoleAsync(int userId, int roleId)
|
|
||||||
{
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
user.RoleId = roleId;
|
|
||||||
user.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
_userRepository.Update(user);
|
|
||||||
await _userRepository.SaveAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> UnassignRoleAsync(int userId, int roleId)
|
|
||||||
{
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId);
|
|
||||||
if (user == null || user.RoleId != roleId)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Assign default role
|
|
||||||
var defaultRole = await _roleRepository.GetByNameAsync("User");
|
|
||||||
if (defaultRole != null)
|
|
||||||
{
|
|
||||||
user.RoleId = defaultRole.Id;
|
|
||||||
user.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
_userRepository.Update(user);
|
|
||||||
await _userRepository.SaveAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> SetUserActiveStatusAsync(int userId, bool isActive)
|
|
||||||
{
|
|
||||||
var user = await _userRepository.GetByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
user.IsActive = isActive;
|
|
||||||
user.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
_userRepository.Update(user);
|
|
||||||
await _userRepository.SaveAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string HashPassword(string password)
|
|
||||||
{
|
|
||||||
using (var sha256 = SHA256.Create())
|
|
||||||
{
|
|
||||||
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
|
|
||||||
return Convert.ToBase64String(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PermissionService : IPermissionService
|
|
||||||
{
|
|
||||||
private readonly IRoleRepository _roleRepository;
|
|
||||||
private readonly IPermissionRepository _permissionRepository;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public PermissionService(
|
|
||||||
IRoleRepository roleRepository,
|
|
||||||
IPermissionRepository permissionRepository,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_roleRepository = roleRepository;
|
|
||||||
_permissionRepository = permissionRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Permission>> GetAllPermissionsAsync()
|
|
||||||
{
|
|
||||||
return await _permissionRepository.GetAllAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Permission> GetPermissionByIdAsync(int permissionId)
|
|
||||||
{
|
|
||||||
return await _permissionRepository.GetByIdAsync(permissionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<Permission>> GetPermissionsByCategoryAsync(string category)
|
|
||||||
{
|
|
||||||
return await _permissionRepository.FindAsync(p => p.Category == category);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> UserHasPermissionAsync(int userId, string permissionName)
|
|
||||||
{
|
|
||||||
return await _roleRepository.UserHasPermissionAsync(userId, permissionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetUserPermissionsAsync(int userId)
|
|
||||||
{
|
|
||||||
var user = await _roleRepository.GetUserById(userId); // Assuming this method exists
|
|
||||||
if (user == null)
|
|
||||||
return new List<string>();
|
|
||||||
|
|
||||||
var role = await _roleRepository.GetByIdAsync(user.RoleId);
|
|
||||||
return role?.Permissions ?? new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> AddPermissionToRoleAsync(int roleId, int permissionId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _roleRepository.AddPermissionToRoleAsync(roleId, permissionId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to add permission to role: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> RemovePermissionFromRoleAsync(int roleId, int permissionId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _roleRepository.RemovePermissionFromRoleAsync(roleId, permissionId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to remove permission from role: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CreatePermissionAsync(Permission permission)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _permissionRepository.AddPermissionAsync(permission);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to create permission: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> UpdatePermissionAsync(Permission permission)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _permissionRepository.UpdatePermissionAsync(permission);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to update permission: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeletePermissionAsync(int permissionId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _permissionRepository.DeletePermissionAsync(permissionId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to delete permission: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SessionService : ISessionService
|
|
||||||
{
|
|
||||||
private readonly IUserSessionRepository _userSessionRepository;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public SessionService(
|
|
||||||
IUserSessionRepository userSessionRepository,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_userSessionRepository = userSessionRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UserSession> CreateSessionAsync(int userId, string deviceInfo, string ipAddress)
|
|
||||||
{
|
|
||||||
var sessionToken = Guid.NewGuid().ToString();
|
|
||||||
|
|
||||||
var session = new UserSession
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
SessionToken = sessionToken,
|
|
||||||
DeviceInfo = deviceInfo,
|
|
||||||
IPAddress = ipAddress,
|
|
||||||
LoginTime = DateTime.Now,
|
|
||||||
LastActivityTime = DateTime.Now,
|
|
||||||
IsActive = true,
|
|
||||||
CreatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
await _userSessionRepository.AddAsync(session);
|
|
||||||
await _userSessionRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _logger.LogInformationAsync($"Session created for user {userId}");
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateSessionAsync(string sessionToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var session = await _userSessionRepository.GetSessionByTokenAsync(sessionToken);
|
|
||||||
return session != null && session.IsActive;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UserSession> GetSessionByTokenAsync(string sessionToken)
|
|
||||||
{
|
|
||||||
return await _userSessionRepository.GetSessionByTokenAsync(sessionToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> UpdateSessionActivityAsync(string sessionToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var session = await _userSessionRepository.GetSessionByTokenAsync(sessionToken);
|
|
||||||
if (session != null)
|
|
||||||
{
|
|
||||||
session.LastActivityTime = DateTime.Now;
|
|
||||||
await _userSessionRepository.UpdateSessionAsync(session);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to update session: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> TerminateSessionAsync(string sessionToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var session = await _userSessionRepository.GetSessionByTokenAsync(sessionToken);
|
|
||||||
if (session != null)
|
|
||||||
{
|
|
||||||
session.IsActive = false;
|
|
||||||
session.LogoutTime = DateTime.Now;
|
|
||||||
await _userSessionRepository.UpdateSessionAsync(session);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to terminate session: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> TerminateAllUserSessionsAsync(int userId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sessions = await _userSessionRepository.GetUserSessionsAsync(userId);
|
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
|
||||||
await TerminateSessionAsync(session.SessionToken);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to terminate all sessions: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<UserSession>> GetUserSessionsAsync(int userId)
|
|
||||||
{
|
|
||||||
return await _userSessionRepository.GetUserSessionsAsync(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CleanupExpiredSessionsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var expiredSessions = await _userSessionRepository.GetExpiredSessionsAsync();
|
|
||||||
if (expiredSessions.Any())
|
|
||||||
{
|
|
||||||
await _userSessionRepository.RemoveRangeAsync(expiredSessions);
|
|
||||||
await _userSessionRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _logger.LogInformationAsync($"Cleaned up {expiredSessions.Count()} expired sessions");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _logger.LogErrorAsync($"Failed to cleanup expired sessions: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,384 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IBackgroundTaskService
|
|
||||||
{
|
|
||||||
Task StartAsync(CancellationToken cancellationToken);
|
|
||||||
Task StopAsync(CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BackgroundTaskService : BackgroundService, IBackgroundTaskService
|
|
||||||
{
|
|
||||||
private readonly ILogger<BackgroundTaskService> _logger;
|
|
||||||
private readonly IDeviceCollectionService _collectionService;
|
|
||||||
private readonly IProductionService _productionService;
|
|
||||||
private readonly IAlarmService _alarmService;
|
|
||||||
private readonly IRealTimeService _realTimeService;
|
|
||||||
private readonly ISchedulerService _schedulerService;
|
|
||||||
private readonly Timer _collectionTimer;
|
|
||||||
private Timer _productionTimer;
|
|
||||||
private Timer _alarmTimer;
|
|
||||||
private Timer _realTimeTimer;
|
|
||||||
private bool _isRunning;
|
|
||||||
|
|
||||||
public BackgroundTaskService(
|
|
||||||
ILogger<BackgroundTaskService> logger,
|
|
||||||
IDeviceCollectionService collectionService,
|
|
||||||
IProductionService productionService,
|
|
||||||
IAlarmService alarmService,
|
|
||||||
IRealTimeService realTimeService,
|
|
||||||
ISchedulerService schedulerService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_collectionService = collectionService;
|
|
||||||
_productionService = productionService;
|
|
||||||
_alarmService = alarmService;
|
|
||||||
_realTimeService = realTimeService;
|
|
||||||
_schedulerService = schedulerService;
|
|
||||||
|
|
||||||
_isRunning = false;
|
|
||||||
_collectionTimer = new Timer(ExecuteCollectionTasks, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Background Task Service is starting.");
|
|
||||||
|
|
||||||
_isRunning = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await StartTimers(stoppingToken);
|
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
|
||||||
|
|
||||||
// Log service health
|
|
||||||
await LogServiceHealthAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Background Task Service encountered an error.");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await StopTimersAsync();
|
|
||||||
_logger.LogInformation("Background Task Service is stopping.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_isRunning)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogInformation("Starting background task service...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Start device collection
|
|
||||||
await _collectionService.CollectAllDevicesAsync();
|
|
||||||
_logger.LogInformation("Device collection started.");
|
|
||||||
|
|
||||||
// Start production calculation
|
|
||||||
await _productionService.CalculateAllProductionAsync();
|
|
||||||
_logger.LogInformation("Production calculation started.");
|
|
||||||
|
|
||||||
// Start alarm monitoring
|
|
||||||
var activeAlarms = await _alarmService.GetActiveAlarmsAsync();
|
|
||||||
if (activeAlarms.Any())
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Found {activeAlarms.Count()} active alarms.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_isRunning = true;
|
|
||||||
_logger.LogInformation("Background task service started successfully.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to start background task service.");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!_isRunning)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogInformation("Stopping background task service...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await StopTimersAsync();
|
|
||||||
_isRunning = false;
|
|
||||||
|
|
||||||
_logger.LogInformation("Background task service stopped successfully.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error while stopping background task service.");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ExecuteCollectionTasks(object state)
|
|
||||||
{
|
|
||||||
if (!_isRunning)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _collectionService.CollectAllDevicesAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error executing collection tasks.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartTimers(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Start production calculation timer (every 5 minutes)
|
|
||||||
_productionTimer = new Timer(async _ =>
|
|
||||||
{
|
|
||||||
if (!_isRunning) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _productionService.CalculateAllProductionAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in production calculation timer.");
|
|
||||||
}
|
|
||||||
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
// Start alarm monitoring timer (every 1 minute)
|
|
||||||
_alarmTimer = new Timer(async _ =>
|
|
||||||
{
|
|
||||||
if (!_isRunning) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activeAlarms = await _alarmService.GetActiveAlarmsAsync();
|
|
||||||
_logger.LogInformation($"Monitoring {activeAlarms.Count()} active alarms.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in alarm monitoring timer.");
|
|
||||||
}
|
|
||||||
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
|
|
||||||
|
|
||||||
// Start real-time data push timer (every 30 seconds)
|
|
||||||
_realTimeTimer = new Timer(async _ =>
|
|
||||||
{
|
|
||||||
if (!_isRunning) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _realTimeService.BroadcastDeviceStatusAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in real-time data push timer.");
|
|
||||||
}
|
|
||||||
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
|
||||||
|
|
||||||
_logger.LogInformation("Background timers started.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StopTimersAsync()
|
|
||||||
{
|
|
||||||
_collectionTimer?.Dispose();
|
|
||||||
_productionTimer?.Dispose();
|
|
||||||
_alarmTimer?.Dispose();
|
|
||||||
_realTimeTimer?.Dispose();
|
|
||||||
|
|
||||||
_logger.LogInformation("Background timers stopped.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LogServiceHealthAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var collectionHealth = await _collectionService.GetCollectionHealthAsync();
|
|
||||||
var deviceCount = await _collectionService.GetCollectionStatisticsAsync(DateTime.Today);
|
|
||||||
|
|
||||||
_logger.LogInformation($"Service Health - Devices: {collectionHealth.TotalDevices}, " +
|
|
||||||
$"Online: {collectionHealth.OnlineDevices}, " +
|
|
||||||
$"Success Rate: {collectionHealth.SuccessRate:F1}%, " +
|
|
||||||
$"Active Tasks: {collectionHealth.ActiveCollectionTasks}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error logging service health.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ISchedulerService
|
|
||||||
{
|
|
||||||
Task ScheduleDeviceCollectionAsync(int deviceId);
|
|
||||||
Task ScheduleProductionCalculationAsync(int deviceId);
|
|
||||||
Task ScheduleAlarmCheckAsync();
|
|
||||||
Task CancelScheduledTaskAsync(string taskId);
|
|
||||||
Task<IEnumerable<ScheduledTask>> GetScheduledTasksAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BackgroundTaskManager : ISchedulerService
|
|
||||||
{
|
|
||||||
private readonly ILogger<BackgroundTaskManager> _logger;
|
|
||||||
private readonly IDeviceCollectionService _collectionService;
|
|
||||||
private readonly IProductionService _productionService;
|
|
||||||
private readonly IAlarmService _alarmService;
|
|
||||||
private readonly Dictionary<string, Timer> _scheduledTasks = new Dictionary<string, Timer>();
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
|
|
||||||
public BackgroundTaskManager(
|
|
||||||
ILogger<BackgroundTaskManager> logger,
|
|
||||||
IDeviceCollectionService collectionService,
|
|
||||||
IProductionService productionService,
|
|
||||||
IAlarmService alarmService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_collectionService = collectionService;
|
|
||||||
_productionService = productionService;
|
|
||||||
_alarmService = alarmService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ScheduleDeviceCollectionAsync(int deviceId)
|
|
||||||
{
|
|
||||||
var taskId = $"collection_{deviceId}_{DateTime.Now:yyyyMMddHHmmss}";
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (_scheduledTasks.ContainsKey(taskId))
|
|
||||||
{
|
|
||||||
_scheduledTasks[taskId].Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
var timer = new Timer(async _ =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _collectionService.CollectDeviceAsync(deviceId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to execute scheduled collection for device {deviceId}");
|
|
||||||
}
|
|
||||||
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
_scheduledTasks[taskId] = timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation($"Scheduled device collection for device {deviceId} with task ID {taskId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ScheduleProductionCalculationAsync(int deviceId)
|
|
||||||
{
|
|
||||||
var taskId = $"production_{deviceId}_{DateTime.Now:yyyyMMddHHmmss}";
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (_scheduledTasks.ContainsKey(taskId))
|
|
||||||
{
|
|
||||||
_scheduledTasks[taskId].Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
var timer = new Timer(async _ =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _productionService.CalculateProductionAsync(deviceId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to execute scheduled production calculation for device {deviceId}");
|
|
||||||
}
|
|
||||||
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
|
|
||||||
|
|
||||||
_scheduledTasks[taskId] = timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation($"Scheduled production calculation for device {deviceId} with task ID {taskId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ScheduleAlarmCheckAsync()
|
|
||||||
{
|
|
||||||
var taskId = $"alarm_check_{DateTime.Now:yyyyMMddHHmmss}";
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (_scheduledTasks.ContainsKey(taskId))
|
|
||||||
{
|
|
||||||
_scheduledTasks[taskId].Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
var timer = new Timer(async _ =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activeAlarms = await _alarmService.GetActiveAlarmsAsync();
|
|
||||||
_logger.LogInformation($"Alarm check completed: {activeAlarms.Count()} active alarms");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to execute alarm check");
|
|
||||||
}
|
|
||||||
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(2));
|
|
||||||
|
|
||||||
_scheduledTasks[taskId] = timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation($"Scheduled alarm check with task ID {taskId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CancelScheduledTaskAsync(string taskId)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (_scheduledTasks.TryGetValue(taskId, out var timer))
|
|
||||||
{
|
|
||||||
timer.Dispose();
|
|
||||||
_scheduledTasks.Remove(taskId);
|
|
||||||
_logger.LogInformation($"Cancelled scheduled task {taskId}");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning($"Attempted to cancel non-existent task {taskId}");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IEnumerable<ScheduledTask>> GetScheduledTasksAsync()
|
|
||||||
{
|
|
||||||
var tasks = _scheduledTasks.Keys.Select(key => new ScheduledTask
|
|
||||||
{
|
|
||||||
TaskId = key,
|
|
||||||
ScheduledTime = DateTime.Now,
|
|
||||||
Status = "Active"
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
return Task.FromResult<IEnumerable<ScheduledTask>>(tasks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScheduledTask
|
|
||||||
{
|
|
||||||
public string TaskId { get; set; }
|
|
||||||
public DateTime ScheduledTime { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,655 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.Production;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface ICacheService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get cached value or execute factory if not exists
|
|
||||||
/// </summary>
|
|
||||||
Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, MemoryCacheEntryOptions options = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get cached value synchronously
|
|
||||||
/// </summary>
|
|
||||||
T Get<T>(string key);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set cache value
|
|
||||||
/// </summary>
|
|
||||||
void Set<T>(string key, T value, MemoryCacheEntryOptions options = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove cached value
|
|
||||||
/// </summary>
|
|
||||||
bool Remove(string key);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if key exists in cache
|
|
||||||
/// </summary>
|
|
||||||
bool Exists(string key);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clear all cache
|
|
||||||
/// </summary>
|
|
||||||
void Clear();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get cache statistics
|
|
||||||
/// </summary>
|
|
||||||
CacheStatistics GetStatistics();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get cache keys matching pattern
|
|
||||||
/// </summary>
|
|
||||||
IEnumerable<string> GetKeys(string pattern);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Refresh cached value
|
|
||||||
/// </summary>
|
|
||||||
bool Refresh<T>(string key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CacheStatistics
|
|
||||||
{
|
|
||||||
public long TotalItems { get; set; }
|
|
||||||
public long Hits { get; set; }
|
|
||||||
public long Misses { get; set; }
|
|
||||||
public double HitRate => Hits + Misses > 0 ? (double)Hits / (Hits + Misses) : 0;
|
|
||||||
public long MemoryUsageBytes { get; set; }
|
|
||||||
public DateTime LastCleared { get; set; }
|
|
||||||
public Dictionary<string, long> ItemsByType { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MemoryCacheService : ICacheService
|
|
||||||
{
|
|
||||||
private readonly IMemoryCache _memoryCache;
|
|
||||||
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
|
|
||||||
private long _hits = 0;
|
|
||||||
private long _misses = 0;
|
|
||||||
private long _memoryUsage = 0;
|
|
||||||
|
|
||||||
public MemoryCacheService(IMemoryCache memoryCache)
|
|
||||||
{
|
|
||||||
_memoryCache = memoryCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, MemoryCacheEntryOptions options = null)
|
|
||||||
{
|
|
||||||
return Task.Run(async () =>
|
|
||||||
{
|
|
||||||
// Try to get from cache first
|
|
||||||
if (_memoryCache.TryGetValue(key, out T value))
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _hits);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _misses);
|
|
||||||
|
|
||||||
// Create new value
|
|
||||||
value = await factory();
|
|
||||||
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
// Set cache options if not provided
|
|
||||||
options = options ?? new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = TimeSpan.FromMinutes(30),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2),
|
|
||||||
Size = 1024 // 1KB
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set cache with lock
|
|
||||||
_lock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_memoryCache.Set(key, value, options);
|
|
||||||
UpdateMemoryUsage(options.Size.GetValueOrDefault(1024));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public T Get<T>(string key)
|
|
||||||
{
|
|
||||||
if (_memoryCache.TryGetValue(key, out T value))
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _hits);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
Interlocked.Increment(ref _misses);
|
|
||||||
return default(T);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Set<T>(string key, T value, MemoryCacheEntryOptions options = null)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
options = options ?? new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = TimeSpan.FromMinutes(30),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2),
|
|
||||||
Size = 1024 // 1KB
|
|
||||||
};
|
|
||||||
|
|
||||||
_lock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_memoryCache.Set(key, value, options);
|
|
||||||
UpdateMemoryUsage(options.Size.GetValueOrDefault(1024));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Remove(string key)
|
|
||||||
{
|
|
||||||
_lock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_memoryCache.TryGetValue(key, out object value))
|
|
||||||
{
|
|
||||||
_memoryCache.Remove(key);
|
|
||||||
UpdateMemoryUsage(-(GetEstimatedSize(value)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Exists(string key)
|
|
||||||
{
|
|
||||||
return _memoryCache.TryGetValue(key, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
_lock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// This is a simplified implementation
|
|
||||||
// In a real scenario, you might need a more sophisticated way to clear the cache
|
|
||||||
_memoryCache.Compact(1.0); // Remove all entries
|
|
||||||
_memoryUsage = 0;
|
|
||||||
Interlocked.Exchange(ref _hits, 0);
|
|
||||||
Interlocked.Exchange(ref _misses, 0);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheStatistics GetStatistics()
|
|
||||||
{
|
|
||||||
_lock.EnterReadLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var stats = new CacheStatistics
|
|
||||||
{
|
|
||||||
TotalItems = GetCacheSize(),
|
|
||||||
Hits = _hits,
|
|
||||||
Misses = _misses,
|
|
||||||
MemoryUsageBytes = _memoryUsage,
|
|
||||||
LastCleared = DateTime.Now,
|
|
||||||
ItemsByType = new Dictionary<string, long>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Count items by type (simplified)
|
|
||||||
var deviceCacheCount = GetKeys("device:*").Count();
|
|
||||||
var productionCacheCount = GetKeys("production:*").Count();
|
|
||||||
var configCacheCount = GetKeys("config:*").Count();
|
|
||||||
var templateCacheCount = GetKeys("template:*").Count();
|
|
||||||
|
|
||||||
stats.ItemsByType["device"] = deviceCacheCount;
|
|
||||||
stats.ItemsByType["production"] = productionCacheCount;
|
|
||||||
stats.ItemsByType["config"] = configCacheCount;
|
|
||||||
stats.ItemsByType["template"] = templateCacheCount;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitReadLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> GetKeys(string pattern)
|
|
||||||
{
|
|
||||||
// This is a simplified implementation
|
|
||||||
// In a real scenario, you might need a more sophisticated key pattern matching
|
|
||||||
return _memoryCache.Keys
|
|
||||||
.Cast<string>()
|
|
||||||
.Where(key => key.StartsWith(pattern.Replace("*", "")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Refresh<T>(string key)
|
|
||||||
{
|
|
||||||
if (!_memoryCache.TryGetValue(key, out T value))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Remove and re-add to refresh
|
|
||||||
_lock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_memoryCache.Remove(key);
|
|
||||||
_memoryCache.Set(key, value, new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = TimeSpan.FromMinutes(30),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
|
|
||||||
private int GetCacheSize()
|
|
||||||
{
|
|
||||||
// This is an approximation
|
|
||||||
return _memoryCache.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateMemoryUsage(long delta)
|
|
||||||
{
|
|
||||||
Interlocked.Add(ref _memoryUsage, delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetEstimatedSize(object obj)
|
|
||||||
{
|
|
||||||
// Simplified size estimation
|
|
||||||
if (obj == null) return 0;
|
|
||||||
|
|
||||||
var type = obj.GetType();
|
|
||||||
if (type.IsValueType || type == typeof(string))
|
|
||||||
{
|
|
||||||
return System.Text.Json.JsonSerializer.Serialize(obj).Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For complex objects, estimate based on type
|
|
||||||
return 1024; // Default 1KB for complex objects
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extension methods for common caching patterns
|
|
||||||
public static class CacheServiceExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Cache device information
|
|
||||||
/// </summary>
|
|
||||||
public static Task<CNCDevice> GetOrSetDeviceAsync(this ICacheService cache, int deviceId,
|
|
||||||
Func<Task<CNCDevice>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions();
|
|
||||||
if (expiration.HasValue)
|
|
||||||
{
|
|
||||||
options.SlidingExpiration = expiration.Value;
|
|
||||||
options.AbsoluteExpirationRelativeToNow = expiration.Value + TimeSpan.FromMinutes(30);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
options.SlidingExpiration = TimeSpan.FromMinutes(30);
|
|
||||||
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync($"device:{deviceId}", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache device list
|
|
||||||
/// </summary>
|
|
||||||
public static Task<List<CNCDevice>> GetOrSetAllDevicesAsync(this ICacheService cache,
|
|
||||||
Func<Task<List<CNCDevice>>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(15),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync("devices:all", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache device status
|
|
||||||
/// </summary>
|
|
||||||
public static Task<DeviceCurrentStatus> GetOrSetDeviceStatusAsync(this ICacheService cache, int deviceId,
|
|
||||||
Func<Task<DeviceCurrentStatus>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(5),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync($"device:status:{deviceId}", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache production records
|
|
||||||
/// </summary>
|
|
||||||
public static Task<List<ProductionRecord>> GetOrSetProductionRecordsAsync(this ICacheService cache,
|
|
||||||
int deviceId, DateTime date, Func<Task<List<ProductionRecord>>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(10),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync($"production:records:{deviceId}:{date:yyyy-MM-dd}", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache production summary
|
|
||||||
/// </summary>
|
|
||||||
public static Task<ProgramProductionSummary> GetOrSetProductionSummaryAsync(this ICacheService cache,
|
|
||||||
int deviceId, string programName, Func<Task<ProgramProductionSummary>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(30),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync($"production:summary:{deviceId}:{programName}", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache system configuration
|
|
||||||
/// </summary>
|
|
||||||
public static Task<SystemConfiguration> GetOrSetSystemConfigurationAsync(this ICacheService cache,
|
|
||||||
Func<Task<SystemConfiguration>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromHours(1),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync("config:system", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache template
|
|
||||||
/// </summary>
|
|
||||||
public static Task<CNCBrandTemplate> GetOrSetTemplateAsync(this ICacheService cache,
|
|
||||||
int templateId, Func<Task<CNCBrandTemplate>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(30),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync($"template:{templateId}", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache template list
|
|
||||||
/// </summary>
|
|
||||||
public static Task<List<CNCBrandTemplate>> GetOrSetAllTemplatesAsync(this ICacheService cache,
|
|
||||||
Func<Task<List<CNCBrandTemplate>>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(30),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync("templates:all", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache alert configuration
|
|
||||||
/// </summary>
|
|
||||||
public static Task<AlertConfiguration> GetOrSetAlertConfigurationAsync(this ICacheService cache,
|
|
||||||
Func<Task<AlertConfiguration>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(30),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync("config:alerts", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache dashboard summary
|
|
||||||
/// </summary>
|
|
||||||
public static Task<DashboardSummary> GetOrSetDashboardSummaryAsync(this ICacheService cache,
|
|
||||||
DateTime date, Func<Task<DashboardSummary>> factory, TimeSpan? expiration = null)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = expiration ?? TimeSpan.FromMinutes(10),
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync($"dashboard:summary:{date:yyyy-MM-dd}", factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invalidate device-related cache
|
|
||||||
/// </summary>
|
|
||||||
public static void InvalidateDeviceCache(this ICacheService cache, int deviceId, params string[] additionalKeys)
|
|
||||||
{
|
|
||||||
var keys = new[]
|
|
||||||
{
|
|
||||||
$"device:{deviceId}",
|
|
||||||
$"device:status:{deviceId}",
|
|
||||||
$"production:records:{deviceId}",
|
|
||||||
$"production:summary:{deviceId}"
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var key in keys.Concat(additionalKeys))
|
|
||||||
{
|
|
||||||
cache.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invalidate production-related cache
|
|
||||||
/// </summary>
|
|
||||||
public static void InvalidateProductionCache(this ICacheService cache, int deviceId, string programName, DateTime date)
|
|
||||||
{
|
|
||||||
cache.Remove($"production:records:{deviceId}:{date:yyyy-MM-dd}");
|
|
||||||
cache.Remove($"production:summary:{deviceId}:{programName}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invalidate template-related cache
|
|
||||||
/// </summary>
|
|
||||||
public static void InvalidateTemplateCache(this ICacheService cache, int templateId)
|
|
||||||
{
|
|
||||||
cache.Remove($"template:{templateId}");
|
|
||||||
cache.Remove("templates:all");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invalidate system configuration cache
|
|
||||||
/// </summary>
|
|
||||||
public static void InvalidateSystemConfigCache(this ICacheService cache)
|
|
||||||
{
|
|
||||||
cache.Remove("config:system");
|
|
||||||
cache.Remove("config:alerts");
|
|
||||||
cache.Remove("dashboard:summary");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invalidate dashboard cache
|
|
||||||
/// </summary>
|
|
||||||
public static void InvalidateDashboardCache(this ICacheService cache, DateTime date)
|
|
||||||
{
|
|
||||||
cache.Remove($"dashboard:summary:{date:yyyy-MM-dd}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get or set with sliding expiration for frequently accessed data
|
|
||||||
/// </summary>
|
|
||||||
public static Task<T> GetOrSetWithSlidingExpiration<T>(this ICacheService cache, string key,
|
|
||||||
Func<Task<T>> factory, TimeSpan slidingExpiration)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
SlidingExpiration = slidingExpiration
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync(key, factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get or set with absolute expiration for time-sensitive data
|
|
||||||
/// </summary>
|
|
||||||
public static Task<T> GetOrSetWithAbsoluteExpiration<T>(this ICacheService cache, string key,
|
|
||||||
Func<Task<T>> factory, TimeSpan absoluteExpiration)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
AbsoluteExpirationRelativeToNow = absoluteExpiration
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync(key, factory, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get or set with priority for important data
|
|
||||||
/// </summary>
|
|
||||||
public static Task<T> GetOrSetWithPriority<T>(this ICacheService cache, string key,
|
|
||||||
Func<Task<T>> factory, CacheItemPriority priority)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions
|
|
||||||
{
|
|
||||||
Priority = priority
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.GetOrSetAsync(key, factory, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache service for distributed caching
|
|
||||||
/// </summary>
|
|
||||||
public interface IDistributedCacheService : ICacheService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get distributed lock
|
|
||||||
/// </summary>
|
|
||||||
Task<IDistributedLock> AcquireLockAsync(string key, TimeSpan timeout);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Refresh distributed cache
|
|
||||||
/// </summary>
|
|
||||||
Task RefreshAsync(string key);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get distributed cache statistics
|
|
||||||
/// </summary>
|
|
||||||
Task<DistributedCacheStatistics> GetDistributedStatisticsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IDistributedLock : IDisposable
|
|
||||||
{
|
|
||||||
bool IsAcquired { get; }
|
|
||||||
void Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DistributedCacheStatistics : CacheStatistics
|
|
||||||
{
|
|
||||||
public int ConnectedNodes { get; set; }
|
|
||||||
public long NetworkCalls { get; set; }
|
|
||||||
public long SynchronizationErrors { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache manager for managing multiple cache instances
|
|
||||||
/// </summary>
|
|
||||||
public interface ICacheManager
|
|
||||||
{
|
|
||||||
ICacheService LocalCache { get; }
|
|
||||||
IDistributedCacheService DistributedCache { get; }
|
|
||||||
void ConfigureCacheSettings(CacheSettings settings);
|
|
||||||
void InitializeCaches();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CacheSettings
|
|
||||||
{
|
|
||||||
public bool EnableLocalCache { get; set; } = true;
|
|
||||||
public bool EnableDistributedCache { get; set; } = false;
|
|
||||||
public TimeSpan DefaultSlidingExpiration { get; set; } = TimeSpan.FromMinutes(30);
|
|
||||||
public TimeSpan DefaultAbsoluteExpiration { get; set; } = TimeSpan.FromHours(2);
|
|
||||||
public long MaxMemorySizeBytes { get; set; } = 1024 * 1024 * 100; // 100MB
|
|
||||||
public CacheEvictionPolicy EvictionPolicy { get; set; } = CacheEvictionPolicy.LRU;
|
|
||||||
public bool EnableCacheLogging { get; set; } = false;
|
|
||||||
public TimeSpan CacheRefreshInterval { get; set; } = TimeSpan.FromMinutes(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum CacheEvictionPolicy
|
|
||||||
{
|
|
||||||
LRU, // Least Recently Used
|
|
||||||
LFU, // Least Frequently Used
|
|
||||||
FIFO, // First In First Out
|
|
||||||
Random
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CacheManager : ICacheManager
|
|
||||||
{
|
|
||||||
public ICacheService LocalCache { get; private set; }
|
|
||||||
public IDistributedCacheService DistributedCache { get; private set; }
|
|
||||||
|
|
||||||
public CacheManager(ICacheService localCache, IDistributedCacheService distributedCache)
|
|
||||||
{
|
|
||||||
LocalCache = localCache;
|
|
||||||
DistributedCache = distributedCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConfigureCacheSettings(CacheSettings settings)
|
|
||||||
{
|
|
||||||
// Configure cache settings based on provided configuration
|
|
||||||
// This would involve setting up the cache instances with the specified settings
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InitializeCaches()
|
|
||||||
{
|
|
||||||
// Initialize caches with default settings
|
|
||||||
ClearAllCaches();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearAllCaches()
|
|
||||||
{
|
|
||||||
LocalCache?.Clear();
|
|
||||||
DistributedCache?.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public class ConsoleLoggerService : ILoggerService
|
|
||||||
{
|
|
||||||
private readonly ILogger<ConsoleLoggerService> _logger;
|
|
||||||
|
|
||||||
public ConsoleLoggerService(ILogger<ConsoleLoggerService> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogInformationAsync(string message)
|
|
||||||
{
|
|
||||||
_logger.LogInformation(message);
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogWarningAsync(string message)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(message);
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogErrorAsync(string message)
|
|
||||||
{
|
|
||||||
_logger.LogError(message);
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogDebugAsync(string message)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(message);
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,707 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public class DeviceCollectionService : IDeviceCollectionService
|
|
||||||
{
|
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
|
||||||
private readonly ICollectionTaskRepository _taskRepository;
|
|
||||||
private readonly ICollectionResultRepository _resultRepository;
|
|
||||||
private readonly ICollectionLogRepository _logRepository;
|
|
||||||
private readonly IPingService _pingService;
|
|
||||||
private readonly IDataParserService _dataParserService;
|
|
||||||
private readonly IDataStorageService _dataStorageService;
|
|
||||||
private readonly IRetryService _retryService;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
public DeviceCollectionService(
|
|
||||||
IDeviceRepository deviceRepository,
|
|
||||||
ICollectionTaskRepository taskRepository,
|
|
||||||
ICollectionResultRepository resultRepository,
|
|
||||||
ICollectionLogRepository logRepository,
|
|
||||||
IPingService pingService,
|
|
||||||
IDataParserService dataParserService,
|
|
||||||
IDataStorageService dataStorageService,
|
|
||||||
IRetryService retryService)
|
|
||||||
{
|
|
||||||
_deviceRepository = deviceRepository;
|
|
||||||
_taskRepository = taskRepository;
|
|
||||||
_resultRepository = resultRepository;
|
|
||||||
_logRepository = logRepository;
|
|
||||||
_pingService = pingService;
|
|
||||||
_dataParserService = dataParserService;
|
|
||||||
_dataStorageService = dataStorageService;
|
|
||||||
_retryService = retryService;
|
|
||||||
_httpClient = new HttpClient();
|
|
||||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CollectAllDevicesAsync()
|
|
||||||
{
|
|
||||||
var devices = await _deviceRepository.GetAllAsync();
|
|
||||||
var onlineDevices = devices.Where(d => d.IsOnline && d.IsAvailable).ToList();
|
|
||||||
|
|
||||||
foreach (var device in onlineDevices)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await CollectDeviceAsync(device.Id);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await LogCollectionAsync(device.Id, LogLevel.Error,
|
|
||||||
$"Failed to collect from device {device.DeviceCode}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CollectDeviceAsync(int deviceId)
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
if (device == null)
|
|
||||||
throw new CollectionException(deviceId, "", "Device not found", CollectionErrorType.Unknown);
|
|
||||||
|
|
||||||
if (!device.IsOnline || !device.IsAvailable)
|
|
||||||
{
|
|
||||||
throw new CollectionException(deviceId, device.DeviceCode,
|
|
||||||
"Device is not online or not available", CollectionErrorType.DeviceOffline);
|
|
||||||
}
|
|
||||||
|
|
||||||
var taskId = await CreateCollectionTaskAsync(device);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _retryService.ExecuteWithRetryAsync(async () =>
|
|
||||||
{
|
|
||||||
var result = await CollectDeviceDataAsync(deviceId);
|
|
||||||
await ProcessCollectedDataAsync(result);
|
|
||||||
await MarkTaskCompletedAsync(taskId, true);
|
|
||||||
return result;
|
|
||||||
}, maxRetries: 3, delayMs: 30000);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await MarkTaskCompletedAsync(taskId, false, ex.Message);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeviceCurrentStatus> CollectDeviceDataAsync(int deviceId)
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
if (device == null)
|
|
||||||
throw new CollectionException(deviceId, "", "Device not found", CollectionErrorType.Unknown);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _pingService.PingAsync(device.IPAddress);
|
|
||||||
|
|
||||||
var rawJson = await GetDeviceDataAsync(device.HttpUrl);
|
|
||||||
var result = await _dataParserService.ParseDeviceDataAsync(rawJson, deviceId);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new CollectionException(deviceId, device.DeviceCode,
|
|
||||||
ex.Message, CollectionErrorType.NetworkError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> PingDeviceAsync(string ipAddress)
|
|
||||||
{
|
|
||||||
return await _pingService.PingAsync(ipAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> GetDeviceDataAsync(string httpUrl)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await _httpClient.GetAsync(httpUrl);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
throw new Exception($"HTTP request failed: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception($"Failed to get device data: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ProcessCollectedDataAsync(CollectionResult result)
|
|
||||||
{
|
|
||||||
if (result.IsSuccess && result.ParsedData != null)
|
|
||||||
{
|
|
||||||
await _dataStorageService.SaveDeviceStatusAsync(result.ParsedData);
|
|
||||||
|
|
||||||
if (await _dataParserService.ValidateDeviceDataAsync(result.ParsedData))
|
|
||||||
{
|
|
||||||
await UpdateDeviceStatusAsync(result.ParsedData);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await LogCollectionAsync(result.DeviceId, LogLevel.Warning,
|
|
||||||
"Device data validation failed", JsonSerializer.Serialize(result.ParsedData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _dataStorageService.LogCollectionAsync(result.DeviceId, LogLevel.Error,
|
|
||||||
"Collection failed", result.ErrorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<CollectionResult>> GetCollectionHistoryAsync(int deviceId, DateTime startDate, DateTime endDate)
|
|
||||||
{
|
|
||||||
return await _resultRepository.GetResultsByDateRangeAsync(startDate, endDate)
|
|
||||||
.Where(r => r.DeviceId == deviceId).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CollectionStatistics> GetCollectionStatisticsAsync(DateTime date)
|
|
||||||
{
|
|
||||||
var startOfDay = date.Date;
|
|
||||||
var endOfDay = startOfDay.AddDays(1);
|
|
||||||
|
|
||||||
var results = await _resultRepository.GetResultsByDateRangeAsync(startOfDay, endOfDay);
|
|
||||||
|
|
||||||
var stats = new CollectionStatistics
|
|
||||||
{
|
|
||||||
Date = date,
|
|
||||||
TotalAttempts = results.Count(),
|
|
||||||
SuccessCount = results.Count(r => r.IsSuccess),
|
|
||||||
FailedCount = results.Count(r => !r.IsSuccess),
|
|
||||||
SuccessRate = results.Any() ? (decimal)results.Count(r => r.IsSuccess) / results.Count() * 100 : 0,
|
|
||||||
DeviceCount = results.Select(r => r.DeviceId).Distinct().Count(),
|
|
||||||
OnlineDeviceCount = await _deviceRepository.CountOnlineDevicesAsync(),
|
|
||||||
TotalDataSize = results.Sum(r => r.DataSize ?? 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (stats.SuccessCount > 0)
|
|
||||||
{
|
|
||||||
var successfulResults = results.Where(r => r.IsSuccess).ToList();
|
|
||||||
stats.AverageResponseTime = TimeSpan.FromTicks(
|
|
||||||
(long)successfulResults.Average(r => r.ResponseTime ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CollectionHealth> GetCollectionHealthAsync()
|
|
||||||
{
|
|
||||||
var stats = await GetCollectionStatisticsAsync(DateTime.Now);
|
|
||||||
var onlineDeviceCount = await _deviceRepository.CountOnlineDevicesAsync();
|
|
||||||
var availableDeviceCount = await _deviceRepository.CountAvailableDevicesAsync();
|
|
||||||
|
|
||||||
var activeTasks = await _taskRepository.GetRunningTasksAsync();
|
|
||||||
var failedTasks = await _taskRepository.GetFailedTasksAsync();
|
|
||||||
|
|
||||||
var lastSuccessful = await _resultRepository.GetResultsByDateRangeAsync(
|
|
||||||
DateTime.Now.AddDays(-1), DateTime.Now)
|
|
||||||
.Where(r => r.IsSuccess).FirstOrDefault();
|
|
||||||
|
|
||||||
var lastFailed = await _resultRepository.GetResultsByDateRangeAsync(
|
|
||||||
DateTime.Now.AddDays(-1), DateTime.Now)
|
|
||||||
.Where(r => !r.IsSuccess).FirstOrDefault();
|
|
||||||
|
|
||||||
return new CollectionHealth
|
|
||||||
{
|
|
||||||
CheckTime = DateTime.Now,
|
|
||||||
TotalDevices = onlineDeviceCount,
|
|
||||||
OnlineDevices = availableDeviceCount,
|
|
||||||
ActiveCollectionTasks = activeTasks.Count(),
|
|
||||||
FailedTasks = failedTasks.Count(),
|
|
||||||
SuccessRate = stats.SuccessRate,
|
|
||||||
AverageResponseTime = stats.AverageResponseTime,
|
|
||||||
TotalCollectedData = stats.TotalDataSize,
|
|
||||||
LastSuccessfulCollection = lastSuccessful?.CollectionTime ?? DateTime.MinValue,
|
|
||||||
LastFailedCollection = lastFailed?.CollectionTime ?? DateTime.MinValue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RestartFailedCollectionsAsync()
|
|
||||||
{
|
|
||||||
var failedTasks = await _taskRepository.GetFailedTasksAsync();
|
|
||||||
|
|
||||||
foreach (var task in failedTasks)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await CollectDeviceAsync(task.DeviceId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await LogCollectionAsync(task.DeviceId, LogLevel.Error,
|
|
||||||
$"Failed to restart collection for device {task.DeviceId}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> TestConnectionAsync(int deviceId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
if (device == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var isOnline = await _pingService.PingAsync(device.IPAddress);
|
|
||||||
if (!isOnline)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var data = await GetDeviceDataAsync(device.HttpUrl);
|
|
||||||
return !string.IsNullOrEmpty(data);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CreateCollectionTaskAsync(CNCDevice device)
|
|
||||||
{
|
|
||||||
var task = new CollectionTask
|
|
||||||
{
|
|
||||||
DeviceId = device.Id,
|
|
||||||
TaskName = $"Collection_{device.DeviceCode}_{DateTime.Now:yyyyMMddHHmmss}",
|
|
||||||
Status = "Pending",
|
|
||||||
ScheduledTime = DateTime.Now,
|
|
||||||
CreatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
await _taskRepository.AddAsync(task);
|
|
||||||
await _taskRepository.SaveAsync();
|
|
||||||
return task.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task MarkTaskCompletedAsync(int taskId, bool isSuccess, string errorMessage = null)
|
|
||||||
{
|
|
||||||
await _taskRepository.MarkTaskCompletedAsync(taskId, isSuccess, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateDeviceStatusAsync(DeviceCurrentStatus status)
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(status.DeviceId);
|
|
||||||
if (device != null)
|
|
||||||
{
|
|
||||||
device.IsOnline = true;
|
|
||||||
device.LastCollectionTime = status.RecordTime;
|
|
||||||
await _deviceRepository.SaveAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LogCollectionAsync(int deviceId, LogLevel logLevel, string message, string data = null)
|
|
||||||
{
|
|
||||||
await _dataStorageService.LogCollectionAsync(deviceId, logLevel, message, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PingService : IPingService
|
|
||||||
{
|
|
||||||
public async Task<bool> PingAsync(string ipAddress)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var client = new System.Net.NetworkInformation.Ping())
|
|
||||||
{
|
|
||||||
var reply = await client.SendPingAsync(ipAddress, 3000);
|
|
||||||
return reply.Status == System.Net.NetworkInformation.IPStatus.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetPingTimeAsync(string ipAddress)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var client = new System.Net.NetworkInformation.Ping())
|
|
||||||
{
|
|
||||||
var reply = await client.SendPingAsync(ipAddress, 3000);
|
|
||||||
return reply.Status == System.Net.NetworkInformation.IPStatus.Success ?
|
|
||||||
reply.RoundtripTime : -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsDeviceOnlineAsync(string ipAddress)
|
|
||||||
{
|
|
||||||
return await PingAsync(ipAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PingResult> GetPingResultAsync(string ipAddress)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var client = new System.Net.NetworkInformation.Ping())
|
|
||||||
{
|
|
||||||
var reply = await client.SendPingAsync(ipAddress, 3000);
|
|
||||||
return new PingResult
|
|
||||||
{
|
|
||||||
IsSuccess = reply.Status == System.Net.NetworkInformation.IPStatus.Success,
|
|
||||||
PingTime = reply.RoundtripTime,
|
|
||||||
PingTime = DateTime.Now
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new PingResult
|
|
||||||
{
|
|
||||||
IsSuccess = false,
|
|
||||||
ErrorMessage = ex.Message,
|
|
||||||
PingTime = DateTime.Now
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DataParserService : IDataParserService
|
|
||||||
{
|
|
||||||
public async Task<DeviceCurrentStatus> ParseDeviceDataAsync(string rawJson, int deviceId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var document = JsonDocument.Parse(rawJson);
|
|
||||||
var root = document.RootElement;
|
|
||||||
|
|
||||||
var device = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
RecordTime = DateTime.Now,
|
|
||||||
Tags = new List<TagData>()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (root.TryGetProperty("device", out var deviceElement))
|
|
||||||
{
|
|
||||||
device.DeviceCode = deviceElement.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.TryGetProperty("desc", out var descElement))
|
|
||||||
{
|
|
||||||
device.DeviceName = descElement.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.TryGetProperty("tags", out var tagsElement))
|
|
||||||
{
|
|
||||||
foreach (var tagElement in tagsElement.EnumerateArray())
|
|
||||||
{
|
|
||||||
var tag = new TagData
|
|
||||||
{
|
|
||||||
Id = tagElement.GetProperty("id").GetString(),
|
|
||||||
Desc = tagElement.GetProperty("desc").GetString(),
|
|
||||||
Quality = tagElement.GetProperty("quality").GetString(),
|
|
||||||
Time = DateTime.Parse(tagElement.GetProperty("time").GetString())
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tagElement.TryGetProperty("value", out var valueElement))
|
|
||||||
{
|
|
||||||
tag.Value = ParseTagValue(valueElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
device.Tags.Add(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtractDeviceStatus(device);
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception($"Failed to parse device data: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExtractDeviceStatus(DeviceCurrentStatus device)
|
|
||||||
{
|
|
||||||
var ioStatus = device.Tags?.FirstOrDefault(t => t.Id == "_io_status");
|
|
||||||
var tag9 = device.Tags?.FirstOrDefault(t => t.Id == "Tag9");
|
|
||||||
var tag26 = device.Tags?.FirstOrDefault(t => t.Id == "Tag26");
|
|
||||||
|
|
||||||
device.Status = "Unknown";
|
|
||||||
device.IsRunning = false;
|
|
||||||
|
|
||||||
if (ioStatus?.Value?.ToString() == "1" || tag9?.Value?.ToString() == "1" || tag26?.Value?.ToString() == "1")
|
|
||||||
{
|
|
||||||
device.Status = "Running";
|
|
||||||
device.IsRunning = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
device.Status = "Stopped";
|
|
||||||
device.IsRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ncProgram = device.Tags?.FirstOrDefault(t => t.Id == "Tag5");
|
|
||||||
device.NCProgram = ncProgram?.Value?.ToString();
|
|
||||||
|
|
||||||
var cumulativeCount = device.Tags?.FirstOrDefault(t => t.Id == "Tag8");
|
|
||||||
if (cumulativeCount?.Value != null)
|
|
||||||
{
|
|
||||||
if (int.TryParse(cumulativeCount.Value.ToString(), out int count))
|
|
||||||
{
|
|
||||||
device.CumulativeCount = count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var operatingMode = device.Tags?.FirstOrDefault(t => t.Id == "Tag11");
|
|
||||||
device.OperatingMode = operatingMode?.Value?.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private object ParseTagValue(JsonElement valueElement)
|
|
||||||
{
|
|
||||||
if (valueElement.ValueKind == JsonValueKind.String)
|
|
||||||
{
|
|
||||||
return valueElement.GetString();
|
|
||||||
}
|
|
||||||
else if (valueElement.ValueKind == JsonValueKind.Number)
|
|
||||||
{
|
|
||||||
if (valueElement.TryGetInt32(out int intValue))
|
|
||||||
return intValue;
|
|
||||||
else if (valueElement.TryGetDecimal(out decimal decimalValue))
|
|
||||||
return decimalValue;
|
|
||||||
else
|
|
||||||
return valueElement.GetDouble();
|
|
||||||
}
|
|
||||||
else if (valueElement.ValueKind == JsonValueKind.True || valueElement.ValueKind == JsonValueKind.False)
|
|
||||||
{
|
|
||||||
return valueElement.GetBoolean();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return valueElement.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TagData> ParseTagDataAsync(object tagValue, string dataType)
|
|
||||||
{
|
|
||||||
var tag = new TagData();
|
|
||||||
|
|
||||||
switch (dataType.ToLower())
|
|
||||||
{
|
|
||||||
case "int":
|
|
||||||
if (int.TryParse(tagValue?.ToString(), out int intValue))
|
|
||||||
tag.Value = intValue;
|
|
||||||
break;
|
|
||||||
case "decimal":
|
|
||||||
if (decimal.TryParse(tagValue?.ToString(), out decimal decimalValue))
|
|
||||||
tag.Value = decimalValue;
|
|
||||||
break;
|
|
||||||
case "bool":
|
|
||||||
if (bool.TryParse(tagValue?.ToString(), out bool boolValue))
|
|
||||||
tag.Value = boolValue;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
tag.Value = tagValue?.ToString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateDeviceDataAsync(DeviceCurrentStatus data)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(data.DeviceCode))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (data.Tags == null || !data.Tags.Any())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (data.CumulativeCount < 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> ConvertDataFormatAsync(object value, string dataType)
|
|
||||||
{
|
|
||||||
switch (dataType.ToLower())
|
|
||||||
{
|
|
||||||
case "int":
|
|
||||||
if (int.TryParse(value?.ToString(), out int intValue))
|
|
||||||
return intValue.ToString();
|
|
||||||
break;
|
|
||||||
case "decimal":
|
|
||||||
if (decimal.TryParse(value?.ToString(), out decimal decimalValue))
|
|
||||||
return decimalValue.ToString("F2");
|
|
||||||
break;
|
|
||||||
case "bool":
|
|
||||||
if (bool.TryParse(value?.ToString(), out bool boolValue))
|
|
||||||
return boolValue.ToString();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return value?.ToString() ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DataStorageService : IDataStorageService
|
|
||||||
{
|
|
||||||
private readonly ICollectionResultRepository _resultRepository;
|
|
||||||
private readonly IDeviceStatusRepository _deviceStatusRepository;
|
|
||||||
private readonly ICollectionLogRepository _logRepository;
|
|
||||||
|
|
||||||
public DataStorageService(
|
|
||||||
ICollectionResultRepository resultRepository,
|
|
||||||
IDeviceStatusRepository deviceStatusRepository,
|
|
||||||
ICollectionLogRepository logRepository)
|
|
||||||
{
|
|
||||||
_resultRepository = resultRepository;
|
|
||||||
_deviceStatusRepository = deviceStatusRepository;
|
|
||||||
_logRepository = logRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveCollectionResultAsync(CollectionResult result)
|
|
||||||
{
|
|
||||||
await _resultRepository.AddAsync(result);
|
|
||||||
await _resultRepository.SaveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveDeviceStatusAsync(DeviceCurrentStatus status)
|
|
||||||
{
|
|
||||||
var deviceStatus = new DeviceStatus
|
|
||||||
{
|
|
||||||
DeviceId = status.DeviceId,
|
|
||||||
Status = status.Status,
|
|
||||||
IsRunning = status.IsRunning,
|
|
||||||
NCProgram = status.NCProgram,
|
|
||||||
CumulativeCount = status.CumulativeCount,
|
|
||||||
OperatingMode = status.OperatingMode,
|
|
||||||
RecordTime = status.RecordTime
|
|
||||||
};
|
|
||||||
|
|
||||||
await _deviceStatusRepository.AddAsync(deviceStatus);
|
|
||||||
await _deviceStatusRepository.SaveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveRawDataAsync(int deviceId, string rawJson, bool isSuccess, string errorMessage = null)
|
|
||||||
{
|
|
||||||
var result = new CollectionResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
RawJson = rawJson,
|
|
||||||
IsSuccess = isSuccess,
|
|
||||||
ErrorMessage = errorMessage,
|
|
||||||
CollectionTime = DateTime.Now,
|
|
||||||
CreatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
await _resultRepository.AddAsync(result);
|
|
||||||
await _resultRepository.SaveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogCollectionAsync(int deviceId, LogLevel logLevel, string message, string data = null)
|
|
||||||
{
|
|
||||||
var log = new CollectionLog
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
LogLevel = logLevel.ToString(),
|
|
||||||
LogCategory = "Collection",
|
|
||||||
LogMessage = message,
|
|
||||||
LogData = data,
|
|
||||||
LogTime = DateTime.Now,
|
|
||||||
CreatedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
await _logRepository.AddAsync(log);
|
|
||||||
await _logRepository.SaveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ArchiveOldDataAsync(int daysToKeep = 30)
|
|
||||||
{
|
|
||||||
await _resultRepository.DeleteOldResultsAsync(daysToKeep);
|
|
||||||
await _logRepository.DeleteOldLogsAsync(daysToKeep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RetryService : IRetryService
|
|
||||||
{
|
|
||||||
public async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3, int delayMs = 30000)
|
|
||||||
{
|
|
||||||
Exception lastException = null;
|
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await operation();
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (attempt < maxRetries)
|
|
||||||
{
|
|
||||||
lastException = ex;
|
|
||||||
if (ShouldRetry(ex, attempt))
|
|
||||||
{
|
|
||||||
await Task.Delay(delayMs);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastException ?? new Exception("Operation failed after retries");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ExecuteWithRetryAsync(Func<Task> operation, int maxRetries = 3, int delayMs = 30000)
|
|
||||||
{
|
|
||||||
await ExecuteWithRetryAsync(async () =>
|
|
||||||
{
|
|
||||||
await operation();
|
|
||||||
return true;
|
|
||||||
}, maxRetries, delayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShouldRetry(Exception ex, int attemptNumber)
|
|
||||||
{
|
|
||||||
// Retry on network-related exceptions
|
|
||||||
if (ex is HttpRequestException ||
|
|
||||||
ex is System.Net.Sockets.SocketException ||
|
|
||||||
ex is System.TimeoutException)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry on certain specific exceptions
|
|
||||||
if (ex.Message.Contains("timeout") ||
|
|
||||||
ex.Message.Contains("network") ||
|
|
||||||
ex.Message.Contains("connection"))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't retry after max attempts
|
|
||||||
if (attemptNumber >= 3)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,997 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
using Haoliang.Models.Common;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IDeviceStateMachine
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get current state of device
|
|
||||||
/// </summary>
|
|
||||||
DeviceState GetCurrentState(int deviceId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if device can transition to target state
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> CanTransitionAsync(int deviceId, DeviceState targetState);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transition device to new state
|
|
||||||
/// </summary>
|
|
||||||
Task<DeviceStateTransitionResult> TransitionToStateAsync(int deviceId, DeviceState targetState, object context = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger device event
|
|
||||||
/// </summary>
|
|
||||||
Task<DeviceStateTransitionResult> TriggerEventAsync(int deviceId, DeviceEvent deviceEvent, object context = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get device state history
|
|
||||||
/// </summary>
|
|
||||||
Task<List<DeviceStateHistory>> GetStateHistoryAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get device state statistics
|
|
||||||
/// </summary>
|
|
||||||
Task<DeviceStateStatistics> GetStateStatisticsAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Force device to specific state
|
|
||||||
/// </summary>
|
|
||||||
Task<DeviceStateTransitionResult> ForceStateAsync(int deviceId, DeviceState targetState, string reason);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate device state
|
|
||||||
/// </summary>
|
|
||||||
Task<DeviceValidationResult> ValidateStateAsync(int deviceId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Register state change handler
|
|
||||||
/// </summary>
|
|
||||||
void RegisterStateHandler(StateChangeHandler handler);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unregister state change handler
|
|
||||||
/// </summary>
|
|
||||||
void UnregisterStateHandler(StateChangeHandler handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeviceStateMachine : IDeviceStateMachine, IHostedService
|
|
||||||
{
|
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
|
||||||
private readonly IDeviceCollectionService _collectionService;
|
|
||||||
private readonly IAlarmService _alarmService;
|
|
||||||
private readonly ICacheService _cacheService;
|
|
||||||
private readonly Timer _stateCheckTimer;
|
|
||||||
private readonly ConcurrentDictionary<int, DeviceStateContext> _deviceStates = new ConcurrentDictionary<int, DeviceStateContext>();
|
|
||||||
private readonly List<StateChangeHandler> _stateHandlers = new List<StateChangeHandler>();
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
|
|
||||||
public DeviceStateMachine(
|
|
||||||
IDeviceRepository deviceRepository,
|
|
||||||
IDeviceCollectionService collectionService,
|
|
||||||
IAlarmService alarmService,
|
|
||||||
ICacheService cacheService)
|
|
||||||
{
|
|
||||||
_deviceRepository = deviceRepository;
|
|
||||||
_collectionService = collectionService;
|
|
||||||
_alarmService = alarmService;
|
|
||||||
_cacheService = cacheService;
|
|
||||||
|
|
||||||
// Start timer for periodic state checks
|
|
||||||
_stateCheckTimer = new Timer(CheckDeviceStates, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceState GetCurrentState(int deviceId)
|
|
||||||
{
|
|
||||||
if (_deviceStates.TryGetValue(deviceId, out var context))
|
|
||||||
{
|
|
||||||
return context.CurrentState;
|
|
||||||
}
|
|
||||||
return DeviceState.Unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CanTransitionAsync(int deviceId, DeviceState targetState)
|
|
||||||
{
|
|
||||||
var currentState = GetCurrentState(deviceId);
|
|
||||||
return await CanTransitionFromAsync(currentState, targetState, deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeviceStateTransitionResult> TransitionToStateAsync(int deviceId, DeviceState targetState, object context = null)
|
|
||||||
{
|
|
||||||
var result = new DeviceStateTransitionResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
FromState = GetCurrentState(deviceId),
|
|
||||||
ToState = targetState,
|
|
||||||
Success = false,
|
|
||||||
Message = "",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Validate transition
|
|
||||||
if (!await CanTransitionAsync(deviceId, targetState))
|
|
||||||
{
|
|
||||||
result.Message = $"Cannot transition from {result.FromState} to {targetState}";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get device
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
if (device == null)
|
|
||||||
{
|
|
||||||
result.Message = "Device not found";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create transition context
|
|
||||||
var transitionContext = new DeviceStateTransitionContext
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Device = device,
|
|
||||||
FromState = result.FromState,
|
|
||||||
ToState = targetState,
|
|
||||||
Context = context,
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute exit actions for current state
|
|
||||||
var exitResult = await ExecuteStateActionsAsync(deviceId, result.FromState, transitionContext, true);
|
|
||||||
if (!exitResult.Success)
|
|
||||||
{
|
|
||||||
result.Message = $"Failed to exit {result.FromState}: {exitResult.Message}";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute enter actions for target state
|
|
||||||
var enterResult = await ExecuteStateActionsAsync(deviceId, targetState, transitionContext, false);
|
|
||||||
if (!enterResult.Success)
|
|
||||||
{
|
|
||||||
result.Message = $"Failed to enter {targetState}: {enterResult.Message}";
|
|
||||||
// Attempt to revert to original state
|
|
||||||
await ExecuteStateActionsAsync(deviceId, result.FromState, transitionContext, true);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update device state
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var deviceContext = _deviceStates.GetOrAdd(deviceId, new DeviceStateContext
|
|
||||||
{
|
|
||||||
CurrentState = targetState,
|
|
||||||
PreviousState = result.FromState,
|
|
||||||
StateChangedAt = DateTime.UtcNow,
|
|
||||||
Context = context
|
|
||||||
});
|
|
||||||
|
|
||||||
deviceContext.CurrentState = targetState;
|
|
||||||
deviceContext.PreviousState = result.FromState;
|
|
||||||
deviceContext.StateChangedAt = DateTime.UtcNow;
|
|
||||||
deviceContext.Context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record state change in history
|
|
||||||
await RecordStateChangeAsync(deviceId, result.FromState, targetState, context);
|
|
||||||
|
|
||||||
// Notify handlers
|
|
||||||
await NotifyStateHandlersAsync(deviceId, result.FromState, targetState, transitionContext);
|
|
||||||
|
|
||||||
// Update device status
|
|
||||||
await UpdateDeviceStatusAsync(device, targetState);
|
|
||||||
|
|
||||||
result.Success = true;
|
|
||||||
result.Message = $"Successfully transitioned from {result.FromState} to {targetState}";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Message = $"Error during state transition: {ex.Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeviceStateTransitionResult> TriggerEventAsync(int deviceId, DeviceEvent deviceEvent, object context = null)
|
|
||||||
{
|
|
||||||
var currentState = GetCurrentState(deviceId);
|
|
||||||
var eventConfig = GetEventConfiguration(deviceEvent, currentState);
|
|
||||||
|
|
||||||
if (eventConfig == null)
|
|
||||||
{
|
|
||||||
return new DeviceStateTransitionResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
FromState = currentState,
|
|
||||||
ToState = currentState,
|
|
||||||
Success = false,
|
|
||||||
Message = $"No event handler configured for {deviceEvent} in state {currentState}",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check event conditions
|
|
||||||
if (!await EvaluateEventConditionsAsync(deviceId, deviceEvent, eventConfig.Conditions))
|
|
||||||
{
|
|
||||||
return new DeviceStateTransitionResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
FromState = currentState,
|
|
||||||
ToState = currentState,
|
|
||||||
Success = false,
|
|
||||||
Message = $"Event conditions not met for {deviceEvent}",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transition to target state
|
|
||||||
return await TransitionToStateAsync(deviceId, eventConfig.TargetState, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<DeviceStateHistory>> GetStateHistoryAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null)
|
|
||||||
{
|
|
||||||
var history = await _deviceRepository.GetDeviceStateHistoryAsync(deviceId, startDate, endDate);
|
|
||||||
|
|
||||||
// Add current state if not in history
|
|
||||||
if (!history.Any(h => h.State == GetCurrentState(deviceId)))
|
|
||||||
{
|
|
||||||
history.Add(new DeviceStateHistory
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
State = GetCurrentState(deviceId),
|
|
||||||
ChangedAt = DateTime.UtcNow,
|
|
||||||
ChangedBy = "System",
|
|
||||||
Notes = "Current state"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return history.OrderBy(h => h.ChangedAt).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeviceStateStatistics> GetStateStatisticsAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null)
|
|
||||||
{
|
|
||||||
var history = await GetStateHistoryAsync(deviceId, startDate, endDate);
|
|
||||||
var statistics = new DeviceStateStatistics
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
PeriodStart = startDate ?? DateTime.UtcNow.AddDays(-7),
|
|
||||||
PeriodEnd = endDate ?? DateTime.UtcNow,
|
|
||||||
StateTransitions = history.Count - 1, // Excluding initial state
|
|
||||||
StateDurations = new Dictionary<DeviceState, TimeSpan>(),
|
|
||||||
StateCounts = new Dictionary<DeviceState, int>(),
|
|
||||||
LastStateChange = history.LastOrDefault()?.ChangedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate state durations
|
|
||||||
for (int i = 0; i < history.Count - 1; i++)
|
|
||||||
{
|
|
||||||
var fromState = history[i].State;
|
|
||||||
var toState = history[i + 1].State;
|
|
||||||
var duration = history[i + 1].ChangedAt - history[i].ChangedAt;
|
|
||||||
|
|
||||||
if (!statistics.StateDurations.ContainsKey(fromState))
|
|
||||||
{
|
|
||||||
statistics.StateDurations[fromState] = TimeSpan.Zero;
|
|
||||||
}
|
|
||||||
statistics.StateDurations[fromState] += duration;
|
|
||||||
|
|
||||||
if (!statistics.StateCounts.ContainsKey(fromState))
|
|
||||||
{
|
|
||||||
statistics.StateCounts[fromState] = 0;
|
|
||||||
}
|
|
||||||
statistics.StateCounts[fromState]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add current state duration
|
|
||||||
if (history.Any())
|
|
||||||
{
|
|
||||||
var currentState = GetCurrentState(deviceId);
|
|
||||||
var lastState = history.Last();
|
|
||||||
var currentDuration = DateTime.UtcNow - lastState.ChangedAt;
|
|
||||||
|
|
||||||
if (!statistics.StateDurations.ContainsKey(currentState))
|
|
||||||
{
|
|
||||||
statistics.StateDurations[currentState] = TimeSpan.Zero;
|
|
||||||
}
|
|
||||||
statistics.StateDurations[currentState] += currentDuration;
|
|
||||||
|
|
||||||
if (!statistics.StateCounts.ContainsKey(currentState))
|
|
||||||
{
|
|
||||||
statistics.StateCounts[currentState] = 0;
|
|
||||||
}
|
|
||||||
statistics.StateCounts[currentState]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return statistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeviceStateTransitionResult> ForceStateAsync(int deviceId, DeviceState targetState, string reason)
|
|
||||||
{
|
|
||||||
// Create force transition context
|
|
||||||
var forceContext = new { Forced = true, Reason = reason };
|
|
||||||
|
|
||||||
var result = await TransitionToStateAsync(deviceId, targetState, forceContext);
|
|
||||||
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
// If normal transition fails, create a force entry in history
|
|
||||||
await RecordStateChangeAsync(deviceId, GetCurrentState(deviceId), targetState, forceContext, true);
|
|
||||||
|
|
||||||
// Update in-memory state
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var deviceContext = _deviceStates.GetOrAdd(deviceId, new DeviceStateContext());
|
|
||||||
deviceContext.CurrentState = targetState;
|
|
||||||
deviceContext.PreviousState = GetCurrentState(deviceId);
|
|
||||||
deviceContext.StateChangedAt = DateTime.UtcNow;
|
|
||||||
deviceContext.Context = forceContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Success = true;
|
|
||||||
result.Message = $"Forced state transition to {targetState}: {reason}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeviceValidationResult> ValidateStateAsync(int deviceId)
|
|
||||||
{
|
|
||||||
var result = new DeviceValidationResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
IsValid = true,
|
|
||||||
Issues = new List<string>(),
|
|
||||||
CurrentState = GetCurrentState(deviceId),
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
if (device == null)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.Issues.Add("Device not found");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if device state matches actual status
|
|
||||||
var actualStatus = await _collectionService.GetDeviceCurrentStatusAsync(deviceId);
|
|
||||||
var expectedState = TranslateStatusToDeviceState(actualStatus);
|
|
||||||
|
|
||||||
if (expectedState != result.CurrentState)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.Issues.Add($"State mismatch: expected {expectedState}, actual {result.CurrentState}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate state-specific rules
|
|
||||||
var stateValidation = await ValidateStateRulesAsync(deviceId, result.CurrentState);
|
|
||||||
if (!stateValidation.IsValid)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.Issues.AddRange(stateValidation.Issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for state timeouts
|
|
||||||
var stateTimeout = await CheckStateTimeoutAsync(deviceId, result.CurrentState);
|
|
||||||
if (stateTimeout.IsTimeout)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.Issues.Add($"State timeout: {result.CurrentState} exceeded maximum duration");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.Issues.Add($"Validation error: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterStateHandler(StateChangeHandler handler)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_stateHandlers.Add(handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnregisterStateHandler(StateChangeHandler handler)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_stateHandlers.Remove(handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Initialize device states
|
|
||||||
return InitializeDeviceStatesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_stateCheckTimer?.Dispose();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
|
|
||||||
private async Task InitializeDeviceStatesAsync()
|
|
||||||
{
|
|
||||||
var devices = await _deviceRepository.GetAllDevicesAsync();
|
|
||||||
|
|
||||||
foreach (var device in devices)
|
|
||||||
{
|
|
||||||
var stateContext = new DeviceStateContext
|
|
||||||
{
|
|
||||||
CurrentState = DeviceState.Unknown,
|
|
||||||
PreviousState = DeviceState.Unknown,
|
|
||||||
StateChangedAt = DateTime.UtcNow,
|
|
||||||
Context = null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get current device status to determine state
|
|
||||||
var status = await _collectionService.GetDeviceCurrentStatusAsync(device.Id);
|
|
||||||
stateContext.CurrentState = TranslateStatusToDeviceState(status);
|
|
||||||
|
|
||||||
_deviceStates.AddOrUpdate(device.Id, stateContext, (key, existing) => stateContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CheckDeviceStates(object state)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var devices = await _deviceRepository.GetAllActiveDevicesAsync();
|
|
||||||
|
|
||||||
foreach (var device in devices)
|
|
||||||
{
|
|
||||||
var validationResult = await ValidateStateAsync(device.Id);
|
|
||||||
if (!validationResult.IsValid)
|
|
||||||
{
|
|
||||||
// Handle invalid state
|
|
||||||
await HandleInvalidStateAsync(device.Id, validationResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for state timeouts
|
|
||||||
var stateTimeout = await CheckStateTimeoutAsync(device.Id, GetCurrentState(device.Id));
|
|
||||||
if (stateTimeout.IsTimeout)
|
|
||||||
{
|
|
||||||
await HandleStateTimeoutAsync(device.Id, stateTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log error
|
|
||||||
Console.WriteLine($"Error checking device states: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<DeviceStateTransitionResult> ExecuteStateActionsAsync(
|
|
||||||
int deviceId,
|
|
||||||
DeviceState state,
|
|
||||||
DeviceStateTransitionContext context,
|
|
||||||
bool isExit)
|
|
||||||
{
|
|
||||||
var actions = isExit ? GetExitActions(state) : GetEnterActions(state);
|
|
||||||
|
|
||||||
foreach (var action in actions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await action.ExecuteAsync(deviceId, context);
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
return new DeviceStateTransitionResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
FromState = context.FromState,
|
|
||||||
ToState = context.ToState,
|
|
||||||
Success = false,
|
|
||||||
Message = result.Message,
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new DeviceStateTransitionResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
FromState = context.FromState,
|
|
||||||
ToState = context.ToState,
|
|
||||||
Success = false,
|
|
||||||
Message = $"Error executing action: {ex.Message}",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DeviceStateTransitionResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
FromState = context.FromState,
|
|
||||||
ToState = context.ToState,
|
|
||||||
Success = true,
|
|
||||||
Message = "Actions executed successfully",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<StateAction> GetExitActions(DeviceState state)
|
|
||||||
{
|
|
||||||
return GetStateActions(state, "exit");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<StateAction> GetEnterActions(DeviceState state)
|
|
||||||
{
|
|
||||||
return GetStateActions(state, "enter");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<StateAction> GetStateActions(DeviceState state, string actionType)
|
|
||||||
{
|
|
||||||
// This would typically come from configuration or database
|
|
||||||
// For now, return basic actions
|
|
||||||
var actions = new List<StateAction>();
|
|
||||||
|
|
||||||
switch (state)
|
|
||||||
{
|
|
||||||
case DeviceState.Running:
|
|
||||||
if (actionType == "exit")
|
|
||||||
{
|
|
||||||
actions.Add(new StopProductionAction());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
actions.Add(new StartProductionAction());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DeviceState.Idle:
|
|
||||||
if (actionType == "enter")
|
|
||||||
{
|
|
||||||
actions.Add(new LogIdleAction());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DeviceState.Error:
|
|
||||||
if (actionType == "enter")
|
|
||||||
{
|
|
||||||
actions.Add(new NotifyErrorAction());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
actions.Add(new LogStateAction(actionType, state));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CanTransitionFromAsync(DeviceState fromState, DeviceState targetState, int deviceId)
|
|
||||||
{
|
|
||||||
var allowedTransitions = GetAllowedTransitions(fromState);
|
|
||||||
return allowedTransitions.Contains(targetState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<DeviceState> GetAllowedTransitions(DeviceState fromState)
|
|
||||||
{
|
|
||||||
// Define state transition rules
|
|
||||||
var transitions = new Dictionary<DeviceState, List<DeviceState>>
|
|
||||||
{
|
|
||||||
[DeviceState.Unknown] = new List<DeviceState> { DeviceState.Offline, DeviceState.Idle },
|
|
||||||
[DeviceState.Offline] = new List<DeviceState> { DeviceState.Online, DeviceState.Unknown },
|
|
||||||
[DeviceState.Online] = new List<DeviceState> { DeviceState.Idle, DeviceState.Running, DeviceState.Error, DeviceState.Maintenance },
|
|
||||||
[DeviceState.Idle] = new List<DeviceState> { DeviceState.Running, DeviceState.Offline, DeviceState.Maintenance },
|
|
||||||
[DeviceState.Running] = new List<DeviceState> { DeviceState.Idle, DeviceState.Error, DeviceState.Stopped, DeviceState.Maintenance },
|
|
||||||
[DeviceState.Error] = new List<DeviceState> { DeviceState.Idle, DeviceState.Maintenance, DeviceState.Unknown },
|
|
||||||
[DeviceState.Maintenance] = new List<DeviceState> { DeviceState.Idle, DeviceState.Offline, DeviceState.Unknown },
|
|
||||||
[DeviceState.Stopped] = new List<DeviceState> { DeviceState.Idle, DeviceState.Offline }
|
|
||||||
};
|
|
||||||
|
|
||||||
return transitions.GetValueOrDefault(fromState, new List<DeviceState>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private EventConfig GetEventConfiguration(DeviceEvent deviceEvent, DeviceState currentState)
|
|
||||||
{
|
|
||||||
// This would typically come from configuration
|
|
||||||
var eventConfigs = new Dictionary<(DeviceEvent, DeviceState), EventConfig>
|
|
||||||
{
|
|
||||||
[(DeviceEvent.Start, DeviceState.Idle)] = new EventConfig
|
|
||||||
{
|
|
||||||
TargetState = DeviceState.Running,
|
|
||||||
Conditions = new List<Func<int, Task<bool>>>
|
|
||||||
{
|
|
||||||
async deviceId => await IsDeviceReadyForProduction(deviceId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
[(DeviceEvent.Stop, DeviceState.Running)] = new EventConfig
|
|
||||||
{
|
|
||||||
TargetState = DeviceState.Idle,
|
|
||||||
Conditions = new List<Func<int, Task<bool>>>
|
|
||||||
{
|
|
||||||
async deviceId => await IsProductionComplete(deviceId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
[(DeviceEvent.Error, DeviceState.Running)] = new EventConfig
|
|
||||||
{
|
|
||||||
TargetState = DeviceState.Error,
|
|
||||||
Conditions = new List<Func<int, Task<bool>>>
|
|
||||||
{
|
|
||||||
async deviceId => await HasDeviceError(deviceId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
[(DeviceEvent.Resume, DeviceState.Maintenance)] = new EventConfig
|
|
||||||
{
|
|
||||||
TargetState = DeviceState.Idle,
|
|
||||||
Conditions = new List<Func<int, Task<bool>>>
|
|
||||||
{
|
|
||||||
async deviceId => await IsMaintenanceComplete(deviceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return eventConfigs.GetValueOrDefault((deviceEvent, currentState));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> EvaluateEventConditionsAsync(int deviceId, DeviceEvent deviceEvent, List<Func<int, Task<bool>>> conditions)
|
|
||||||
{
|
|
||||||
if (conditions == null || !conditions.Any())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
foreach (var condition in conditions)
|
|
||||||
{
|
|
||||||
var conditionResult = await condition(deviceId);
|
|
||||||
if (!conditionResult)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RecordStateChangeAsync(int deviceId, DeviceState fromState, DeviceState toState, object context, bool isForced = false)
|
|
||||||
{
|
|
||||||
var history = new DeviceStateHistory
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
FromState = fromState,
|
|
||||||
ToState = toState,
|
|
||||||
ChangedAt = DateTime.UtcNow,
|
|
||||||
ChangedBy = isForced ? "System (Forced)" : "System",
|
|
||||||
Notes = isForced ? $"Forced transition: {JsonSerializer(context)}" : JsonSerializer(context)
|
|
||||||
};
|
|
||||||
|
|
||||||
await _deviceRepository.AddDeviceStateHistoryAsync(history);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NotifyStateHandlersAsync(int deviceId, DeviceState fromState, DeviceState toState, DeviceStateTransitionContext context)
|
|
||||||
{
|
|
||||||
var handlersCopy = _stateHandlers.ToList(); // Copy to avoid modification during iteration
|
|
||||||
|
|
||||||
foreach (var handler in handlersCopy)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await handler(deviceId, fromState, toState, context);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log error but continue with other handlers
|
|
||||||
Console.WriteLine($"Error in state handler: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateDeviceStatusAsync(CNCDevice device, DeviceState state)
|
|
||||||
{
|
|
||||||
// Update device status in database
|
|
||||||
device.Status = TranslateStateToDeviceStatus(state);
|
|
||||||
await _deviceRepository.UpdateDeviceAsync(device);
|
|
||||||
|
|
||||||
// Update cache
|
|
||||||
_cacheService.InvalidateDeviceCache(device.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeviceState TranslateStatusToDeviceState(DeviceCurrentStatus status)
|
|
||||||
{
|
|
||||||
if (status == null || status.Status == DeviceStatus.Unknown)
|
|
||||||
return DeviceState.Unknown;
|
|
||||||
|
|
||||||
return status.Status switch
|
|
||||||
{
|
|
||||||
DeviceStatus.Offline => DeviceState.Offline,
|
|
||||||
DeviceStatus.Online => DeviceState.Online,
|
|
||||||
DeviceStatus.Idle => DeviceState.Idle,
|
|
||||||
DeviceStatus.Running => DeviceState.Running,
|
|
||||||
DeviceStatus.Error => DeviceState.Error,
|
|
||||||
DeviceStatus.Maintenance => DeviceState.Maintenance,
|
|
||||||
DeviceStatus.Stopped => DeviceState.Stopped,
|
|
||||||
_ => DeviceState.Unknown
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeviceStatus TranslateStateToDeviceStatus(DeviceState state)
|
|
||||||
{
|
|
||||||
return state switch
|
|
||||||
{
|
|
||||||
DeviceState.Offline => DeviceStatus.Offline,
|
|
||||||
DeviceState.Online => DeviceStatus.Online,
|
|
||||||
DeviceState.Idle => DeviceStatus.Idle,
|
|
||||||
DeviceState.Running => DeviceStatus.Running,
|
|
||||||
DeviceState.Error => DeviceStatus.Error,
|
|
||||||
DeviceState.Maintenance => DeviceStatus.Maintenance,
|
|
||||||
DeviceState.Stopped => DeviceStatus.Stopped,
|
|
||||||
_ => DeviceStatus.Unknown
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<DeviceValidationResult> ValidateStateRulesAsync(int deviceId, DeviceState state)
|
|
||||||
{
|
|
||||||
var result = new DeviceValidationResult { IsValid = true, Issues = new List<string>() };
|
|
||||||
|
|
||||||
switch (state)
|
|
||||||
{
|
|
||||||
case DeviceState.Running:
|
|
||||||
// Check if device should actually be running
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
if (device != null && device.EnableProduction)
|
|
||||||
{
|
|
||||||
var status = await _collectionService.GetDeviceCurrentStatusAsync(deviceId);
|
|
||||||
if (status.Status != DeviceStatus.Running)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.Issues.Add("Device is in Running state but actual status is not Running");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DeviceState.Error:
|
|
||||||
// Check if device has active errors
|
|
||||||
var activeAlarms = await _alarmService.GetActiveAlertsByDeviceAsync(deviceId);
|
|
||||||
if (!activeAlarms.Any(a => a.AlertType == "DeviceError"))
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.Issues.Add("Device is in Error state but has no active error alerts");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<StateTimeoutInfo> CheckStateTimeoutAsync(int deviceId, DeviceState state)
|
|
||||||
{
|
|
||||||
var stateContext = _deviceStates.GetValueOrDefault(deviceId);
|
|
||||||
if (stateContext == null)
|
|
||||||
return new StateTimeoutInfo { IsTimeout = false };
|
|
||||||
|
|
||||||
var stateDurations = GetStateTimeoutDurations();
|
|
||||||
var maxDuration = stateDurations.GetValueOrDefault(state, TimeSpan.FromMinutes(30));
|
|
||||||
|
|
||||||
var currentDuration = DateTime.UtcNow - stateContext.StateChangedAt;
|
|
||||||
|
|
||||||
return new StateTimeoutInfo
|
|
||||||
{
|
|
||||||
IsTimeout = currentDuration > maxDuration,
|
|
||||||
State = state,
|
|
||||||
CurrentDuration = currentDuration,
|
|
||||||
MaxDuration = maxDuration,
|
|
||||||
ExceededBy = currentDuration - maxDuration
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<DeviceState, TimeSpan> GetStateTimeoutDurations()
|
|
||||||
{
|
|
||||||
return new Dictionary<DeviceState, TimeSpan>
|
|
||||||
{
|
|
||||||
[DeviceState.Unknown] = TimeSpan.FromMinutes(5),
|
|
||||||
[DeviceState.Offline] = TimeSpan.FromHours(1),
|
|
||||||
[DeviceState.Online] = TimeSpan.FromMinutes(15),
|
|
||||||
[DeviceState.Idle] = TimeSpan.FromMinutes(10),
|
|
||||||
[DeviceState.Running] = TimeSpan.FromHours(8),
|
|
||||||
[DeviceState.Error] = TimeSpan.FromMinutes(60),
|
|
||||||
[DeviceState.Maintenance] = TimeSpan.FromHours(4),
|
|
||||||
[DeviceState.Stopped] = TimeSpan.FromMinutes(30)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleInvalidStateAsync(int deviceId, DeviceValidationResult validationResult)
|
|
||||||
{
|
|
||||||
// Log warning
|
|
||||||
Console.WriteLine($"Device {deviceId} state validation failed: {string.Join(", ", validationResult.Issues)}");
|
|
||||||
|
|
||||||
// Attempt to transition to a safe state
|
|
||||||
var safeState = DetermineSafeState(deviceId, validationResult.CurrentState);
|
|
||||||
await ForceStateAsync(deviceId, safeState, $"State validation failed: {string.Join(", ", validationResult.Issues)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleStateTimeoutAsync(int deviceId, StateTimeoutInfo timeoutInfo)
|
|
||||||
{
|
|
||||||
// Log warning
|
|
||||||
Console.WriteLine($"Device {deviceId} state {timeoutInfo.State} exceeded maximum duration by {timeoutInfo.ExceededBy}");
|
|
||||||
|
|
||||||
// Trigger timeout event
|
|
||||||
await TriggerEventAsync(deviceId, DeviceEvent.Timeout, timeoutInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeviceState DetermineSafeState(int deviceId, DeviceState currentState)
|
|
||||||
{
|
|
||||||
return DeviceState.Idle; // Default safe state
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> IsDeviceReadyForProduction(int deviceId)
|
|
||||||
{
|
|
||||||
// Check if device is ready for production
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
var status = await _collectionService.GetDeviceCurrentStatusAsync(deviceId);
|
|
||||||
|
|
||||||
return device != null && device.EnableProduction &&
|
|
||||||
status.Status == DeviceStatus.Online &&
|
|
||||||
!device.IsUnderMaintenance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> IsProductionComplete(int deviceId)
|
|
||||||
{
|
|
||||||
// Check if production is complete
|
|
||||||
var records = await _deviceRepository.GetProductionRecordsByDeviceAsync(deviceId);
|
|
||||||
return records.Any() && records.Last().IsComplete;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> HasDeviceError(int deviceId)
|
|
||||||
{
|
|
||||||
// Check if device has errors
|
|
||||||
var alerts = await _alarmService.GetActiveAlertsByDeviceAsync(deviceId);
|
|
||||||
return alerts.Any(a => a.AlertType == "DeviceError");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> IsMaintenanceComplete(int deviceId)
|
|
||||||
{
|
|
||||||
// Check if maintenance is complete
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
return device != null && !device.IsUnderMaintenance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string JsonSerializer(object obj)
|
|
||||||
{
|
|
||||||
// Simplified JSON serialization
|
|
||||||
return obj?.ToString() ?? "{}";
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Supporting Classes and Interfaces
|
|
||||||
|
|
||||||
public delegate Task StateChangeHandler(int deviceId, DeviceState fromState, DeviceState toState, DeviceStateTransitionContext context);
|
|
||||||
|
|
||||||
public interface IStateAction
|
|
||||||
{
|
|
||||||
Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StateActionResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeviceStateContext
|
|
||||||
{
|
|
||||||
public DeviceState CurrentState { get; set; }
|
|
||||||
public DeviceState PreviousState { get; set; }
|
|
||||||
public DateTime StateChangedAt { get; set; }
|
|
||||||
public object Context { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeviceStateTransitionContext
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public CNCDevice Device { get; set; }
|
|
||||||
public DeviceState FromState { get; set; }
|
|
||||||
public DeviceState ToState { get; set; }
|
|
||||||
public object Context { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EventConfig
|
|
||||||
{
|
|
||||||
public DeviceState TargetState { get; set; }
|
|
||||||
public List<Func<int, Task<bool>>> Conditions { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StateTimeoutInfo
|
|
||||||
{
|
|
||||||
public bool IsTimeout { get; set; }
|
|
||||||
public DeviceState State { get; set; }
|
|
||||||
public TimeSpan CurrentDuration { get; set; }
|
|
||||||
public TimeSpan MaxDuration { get; set; }
|
|
||||||
public TimeSpan ExceededBy { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeviceValidationResult
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public bool IsValid { get; set; }
|
|
||||||
public List<string> Issues { get; set; }
|
|
||||||
public DeviceState CurrentState { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region State Action Implementations
|
|
||||||
|
|
||||||
public class StopProductionAction : IStateAction
|
|
||||||
{
|
|
||||||
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
||||||
{
|
|
||||||
// Implementation to stop production
|
|
||||||
return new StateActionResult { Success = true, Message = "Production stopped" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StartProductionAction : IStateAction
|
|
||||||
{
|
|
||||||
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
||||||
{
|
|
||||||
// Implementation to start production
|
|
||||||
return new StateActionResult { Success = true, Message = "Production started" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LogIdleAction : IStateAction
|
|
||||||
{
|
|
||||||
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
||||||
{
|
|
||||||
// Implementation to log idle state
|
|
||||||
return new StateActionResult { Success = true, Message = "Idle state logged" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NotifyErrorAction : IStateAction
|
|
||||||
{
|
|
||||||
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
||||||
{
|
|
||||||
// Implementation to notify about error
|
|
||||||
return new StateActionResult { Success = true, Message = "Error notification sent" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LogStateAction : IStateAction
|
|
||||||
{
|
|
||||||
private readonly string _actionType;
|
|
||||||
private readonly DeviceState _state;
|
|
||||||
|
|
||||||
public LogStateAction(string actionType, DeviceState state)
|
|
||||||
{
|
|
||||||
_actionType = actionType;
|
|
||||||
_state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
||||||
{
|
|
||||||
// Implementation to log state changes
|
|
||||||
return new StateActionResult { Success = true, Message = $"{_actionType} action for {_state} logged" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IDeviceCollectionService
|
|
||||||
{
|
|
||||||
Task CollectAllDevicesAsync();
|
|
||||||
Task CollectDeviceAsync(int deviceId);
|
|
||||||
Task<DeviceCurrentStatus> CollectDeviceDataAsync(int deviceId);
|
|
||||||
Task<bool> PingDeviceAsync(string ipAddress);
|
|
||||||
Task<string> GetDeviceDataAsync(string httpUrl);
|
|
||||||
Task ProcessCollectedDataAsync(CollectionResult result);
|
|
||||||
Task<IEnumerable<CollectionResult>> GetCollectionHistoryAsync(int deviceId, DateTime startDate, DateTime endDate);
|
|
||||||
Task<CollectionStatistics> GetCollectionStatisticsAsync(DateTime date);
|
|
||||||
Task<CollectionHealth> GetCollectionHealthAsync();
|
|
||||||
Task RestartFailedCollectionsAsync();
|
|
||||||
Task<bool> TestConnectionAsync(int deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPingService
|
|
||||||
{
|
|
||||||
Task<bool> PingAsync(string ipAddress);
|
|
||||||
Task<int> GetPingTimeAsync(string ipAddress);
|
|
||||||
Task<bool> IsDeviceOnlineAsync(string ipAddress);
|
|
||||||
Task<PingResult> GetPingResultAsync(string ipAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IDataParserService
|
|
||||||
{
|
|
||||||
Task<DeviceCurrentStatus> ParseDeviceDataAsync(string rawJson, int deviceId);
|
|
||||||
Task<TagData> ParseTagDataAsync(object tagValue, string dataType);
|
|
||||||
Task<bool> ValidateDeviceDataAsync(DeviceCurrentStatus data);
|
|
||||||
Task<string> ConvertDataFormatAsync(object value, string dataType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IDataStorageService
|
|
||||||
{
|
|
||||||
Task SaveCollectionResultAsync(CollectionResult result);
|
|
||||||
Task SaveDeviceStatusAsync(DeviceCurrentStatus status);
|
|
||||||
Task SaveRawDataAsync(int deviceId, string rawJson, bool isSuccess, string errorMessage = null);
|
|
||||||
Task LogCollectionAsync(int deviceId, LogLevel logLevel, string message, string data = null);
|
|
||||||
Task ArchiveOldDataAsync(int daysToKeep = 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PingResult
|
|
||||||
{
|
|
||||||
public bool IsSuccess { get; set; }
|
|
||||||
public int PingTimeMs { get; set; }
|
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IRetryService
|
|
||||||
{
|
|
||||||
Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3, int delayMs = 30000);
|
|
||||||
Task ExecuteWithRetryAsync(Func<Task> operation, int maxRetries = 3, int delayMs = 30000);
|
|
||||||
bool ShouldRetry(Exception ex, int attemptNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CollectionException : Exception
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceCode { get; set; }
|
|
||||||
public CollectionErrorType ErrorType { get; set; }
|
|
||||||
|
|
||||||
public CollectionException(int deviceId, string deviceCode, string message, CollectionErrorType errorType)
|
|
||||||
: base(message)
|
|
||||||
{
|
|
||||||
DeviceId = deviceId;
|
|
||||||
DeviceCode = deviceCode;
|
|
||||||
ErrorType = errorType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum CollectionErrorType
|
|
||||||
{
|
|
||||||
DeviceOffline,
|
|
||||||
NetworkError,
|
|
||||||
DataParseError,
|
|
||||||
DatabaseError,
|
|
||||||
Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,402 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
using Haoliang.Models.Production;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IProductionStatisticsService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Calculate production trends for a specific device and time range
|
|
||||||
/// </summary>
|
|
||||||
Task<ProductionTrendAnalysis> CalculateProductionTrendsAsync(int deviceId, DateTime startDate, DateTime endDate);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate comprehensive production report
|
|
||||||
/// </summary>
|
|
||||||
Task<ProductionReport> GenerateProductionReportAsync(ReportFilter filter);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculate efficiency metrics for devices or programs
|
|
||||||
/// </summary>
|
|
||||||
Task<EfficiencyMetrics> CalculateEfficiencyMetricsAsync(EfficiencyFilter filter);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Perform quality analysis based on production data
|
|
||||||
/// </summary>
|
|
||||||
Task<QualityAnalysis> PerformQualityAnalysisAsync(QualityFilter filter);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get production summary for dashboard display
|
|
||||||
/// </summary>
|
|
||||||
Task<DashboardSummary> GetDashboardSummaryAsync(DashboardFilter filter);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculate OEE (Overall Equipment Effectiveness)
|
|
||||||
/// </summary>
|
|
||||||
Task<OeeMetrics> CalculateOeeAsync(int deviceId, DateTime date);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get production forecasts based on historical data
|
|
||||||
/// </summary>
|
|
||||||
Task<ProductionForecast> GenerateProductionForecastAsync(ForecastFilter filter);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analyze production anomalies and outliers
|
|
||||||
/// </summary>
|
|
||||||
Task<AnomalyAnalysis> DetectProductionAnomaliesAsync(AnomalyFilter filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supporting models for statistics
|
|
||||||
public class ProductionTrendAnalysis
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public DateTime PeriodStart { get; set; }
|
|
||||||
public DateTime PeriodEnd { get; set; }
|
|
||||||
public decimal TotalProduction { get; set; }
|
|
||||||
public decimal AverageDailyProduction { get; set; }
|
|
||||||
public decimal ProductionVariance { get; set; }
|
|
||||||
public double TrendCoefficient { get; set; }
|
|
||||||
public ProductionTrendDirection TrendDirection { get; set; }
|
|
||||||
public List<DailyProduction> DailyData { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DailyProduction
|
|
||||||
{
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
public decimal Target { get; set; }
|
|
||||||
public decimal Efficiency { get; set; }
|
|
||||||
public List<ProductionRecord> Records { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProductionReport
|
|
||||||
{
|
|
||||||
public DateTime ReportDate { get; set; }
|
|
||||||
public ReportType ReportType { get; set; }
|
|
||||||
public List<ProductionSummaryItem> SummaryItems { get; set; }
|
|
||||||
public ReportMetadata Metadata { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProductionSummaryItem
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public string ProgramName { get; set; }
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
public decimal TargetQuantity { get; set; }
|
|
||||||
public decimal Efficiency { get; set; }
|
|
||||||
public decimal QualityRate { get; set; }
|
|
||||||
public TimeSpan Runtime { get; set; }
|
|
||||||
public TimeSpan Downtime { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EfficiencyMetrics
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public DateTime PeriodStart { get; set; }
|
|
||||||
public DateTime PeriodEnd { get; set; }
|
|
||||||
public decimal Availability { get; set; }
|
|
||||||
public decimal Performance { get; set; }
|
|
||||||
public decimal Quality { get; set; }
|
|
||||||
public decimal Oee { get; set; }
|
|
||||||
public EquipmentUtilization Utilization { get; set; }
|
|
||||||
public List<HourlyEfficiency> HourlyData { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HourlyEfficiency
|
|
||||||
{
|
|
||||||
public DateTime Hour { get; set; }
|
|
||||||
public decimal Availability { get; set; }
|
|
||||||
public decimal Performance { get; set; }
|
|
||||||
public decimal Quality { get; set; }
|
|
||||||
public decimal Oee { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QualityAnalysis
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public DateTime PeriodStart { get; set; }
|
|
||||||
public DateTime PeriodEnd { get; set; }
|
|
||||||
public decimal TotalProduced { get; set; }
|
|
||||||
public decimal TotalGood { get; set; }
|
|
||||||
public decimal QualityRate { get; set; }
|
|
||||||
public decimal DefectRate { get; set; }
|
|
||||||
public List<QualityMetric> QualityMetrics { get; set; }
|
|
||||||
public List<DefectAnalysis> DefectAnalysis { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QualityMetric
|
|
||||||
{
|
|
||||||
public string MetricName { get; set; }
|
|
||||||
public decimal Value { get; set; }
|
|
||||||
public string Unit { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DefectAnalysis
|
|
||||||
{
|
|
||||||
public string DefectType { get; set; }
|
|
||||||
public int Count { get; set; }
|
|
||||||
public decimal Percentage { get; set; }
|
|
||||||
public List<DateTime> OccurrenceTimes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DashboardSummary
|
|
||||||
{
|
|
||||||
public DateTime GeneratedAt { get; set; }
|
|
||||||
public int TotalDevices { get; set; }
|
|
||||||
public int ActiveDevices { get; set; }
|
|
||||||
public int OfflineDevices { get; set; }
|
|
||||||
public decimal TotalProductionToday { get; set; }
|
|
||||||
public decimal TotalProductionThisWeek { get; set; }
|
|
||||||
public decimal TotalProductionThisMonth { get; set; }
|
|
||||||
public decimal OverallEfficiency { get; set; }
|
|
||||||
public decimal QualityRate { get; set; }
|
|
||||||
public List<DeviceSummary> DeviceSummaries { get; set; }
|
|
||||||
public List<AlertSummary> ActiveAlerts { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeviceSummary
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public DeviceStatus Status { get; set; }
|
|
||||||
public decimal TodayProduction { get; set; }
|
|
||||||
public decimal Efficiency { get; set; }
|
|
||||||
public decimal QualityRate { get; set; }
|
|
||||||
public TimeSpan Runtime { get; set; }
|
|
||||||
public string CurrentProgram { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AlertSummary
|
|
||||||
{
|
|
||||||
public int AlertId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public AlertType AlertType { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OeeMetrics
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public decimal Availability { get; set; }
|
|
||||||
public decimal Performance { get; set; }
|
|
||||||
public decimal Quality { get; set; }
|
|
||||||
public decimal Oee { get; set; }
|
|
||||||
public TimeSpan PlannedProductionTime { get; set; }
|
|
||||||
public TimeSpan ActualProductionTime { get; set; }
|
|
||||||
public TimeSpan Downtime { get; set; }
|
|
||||||
public decimal IdealCycleTime { get; set; }
|
|
||||||
public decimal TotalCycleTime { get; set; }
|
|
||||||
public decimal TotalPieces { get; set; }
|
|
||||||
public decimal GoodPieces { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProductionForecast
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public DateTime ForecastStartDate { get; set; }
|
|
||||||
public DateTime ForecastEndDate { get; set; }
|
|
||||||
public List<ForecastItem> DailyForecasts { get; set; }
|
|
||||||
public decimal ForecastAccuracy { get; set; }
|
|
||||||
public ForecastModel ModelUsed { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ForecastItem
|
|
||||||
{
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public decimal ForecastedQuantity { get; set; }
|
|
||||||
public decimal ConfidenceLower { get; set; }
|
|
||||||
public decimal ConfidenceUpper { get; set; }
|
|
||||||
public decimal ActualQuantity { get; set; }
|
|
||||||
public decimal Variance { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnomalyAnalysis
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public string DeviceName { get; set; }
|
|
||||||
public DateTime AnalysisStartDate { get; set; }
|
|
||||||
public DateTime AnalysisEndDate { get; set; }
|
|
||||||
public List<ProductionAnomaly> Anomalies { get; set; }
|
|
||||||
public AnomalySeverity OverallSeverity { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProductionAnomaly
|
|
||||||
{
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
public AnomalyType Type { get; set; }
|
|
||||||
public AnomalySeverity Severity { get; set; }
|
|
||||||
public decimal Deviation { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public AnomalyAction RecommendedAction { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter models
|
|
||||||
public class ReportFilter
|
|
||||||
{
|
|
||||||
public List<int> DeviceIds { get; set; }
|
|
||||||
public List<string> ProgramNames { get; set; }
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public ReportType ReportType { get; set; }
|
|
||||||
public GroupBy GroupBy { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EfficiencyFilter
|
|
||||||
{
|
|
||||||
public List<int> DeviceIds { get; set; }
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public EfficiencyMetric Metrics { get; set; }
|
|
||||||
public GroupBy GroupBy { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QualityFilter
|
|
||||||
{
|
|
||||||
public List<int> DeviceIds { get; set; }
|
|
||||||
public List<string> ProgramNames { get; set; }
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public QualityMetricType MetricType { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DashboardFilter
|
|
||||||
{
|
|
||||||
public List<int> DeviceIds { get; set; }
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public bool IncludeAlerts { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ForecastFilter
|
|
||||||
{
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
public int DaysToForecast { get; set; }
|
|
||||||
public ForecastModel Model { get; set; }
|
|
||||||
public DateTime HistoricalDataStart { get; set; }
|
|
||||||
public DateTime HistoricalDataEnd { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnomalyFilter
|
|
||||||
{
|
|
||||||
public List<int> DeviceIds { get; set; }
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public AnomalyType? Type { get; set; }
|
|
||||||
public AnomalySeverity? MinSeverity { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enum types
|
|
||||||
public enum ProductionTrendDirection
|
|
||||||
{
|
|
||||||
Increasing,
|
|
||||||
Decreasing,
|
|
||||||
Stable,
|
|
||||||
Volatile
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ReportType
|
|
||||||
{
|
|
||||||
Daily,
|
|
||||||
Weekly,
|
|
||||||
Monthly,
|
|
||||||
Custom
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ReportMetadata
|
|
||||||
{
|
|
||||||
GeneratedAt,
|
|
||||||
GeneratedBy,
|
|
||||||
Period,
|
|
||||||
DeviceCount,
|
|
||||||
TotalProduction,
|
|
||||||
AverageEfficiency
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum EquipmentUtilization
|
|
||||||
{
|
|
||||||
Low,
|
|
||||||
Medium,
|
|
||||||
High,
|
|
||||||
Optimal
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum AlertType
|
|
||||||
{
|
|
||||||
DeviceOffline,
|
|
||||||
ProductionAnomaly,
|
|
||||||
QualityIssue,
|
|
||||||
MaintenanceRequired,
|
|
||||||
SystemError
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum GroupBy
|
|
||||||
{
|
|
||||||
Device,
|
|
||||||
Program,
|
|
||||||
Date,
|
|
||||||
Hour
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum QualityMetricType
|
|
||||||
{
|
|
||||||
DefectRate,
|
|
||||||
FirstPassYield,
|
|
||||||
ReworkRate,
|
|
||||||
ScrapRate
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ForecastModel
|
|
||||||
{
|
|
||||||
Linear,
|
|
||||||
ExponentialSmoothing,
|
|
||||||
Seasonal,
|
|
||||||
MovingAverage,
|
|
||||||
MachineLearning
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum AnomalyType
|
|
||||||
{
|
|
||||||
ProductionDrop,
|
|
||||||
QualitySpike,
|
|
||||||
DowntimeSpike,
|
|
||||||
EfficiencyDrop,
|
|
||||||
RuntimeAnomaly
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum AnomalySeverity
|
|
||||||
{
|
|
||||||
Low,
|
|
||||||
Medium,
|
|
||||||
High,
|
|
||||||
Critical
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum AnomalyAction
|
|
||||||
{
|
|
||||||
Monitor,
|
|
||||||
Investigate,
|
|
||||||
Alert,
|
|
||||||
Shutdown
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum EfficiencyMetric
|
|
||||||
{
|
|
||||||
Availability,
|
|
||||||
Performance,
|
|
||||||
Quality,
|
|
||||||
Oee,
|
|
||||||
All
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,178 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
// 中间件和过滤器
|
|
||||||
public class ExceptionMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
|
||||||
|
|
||||||
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _next(context);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "An unhandled exception occurred");
|
|
||||||
await HandleExceptionAsync(context, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
|
|
||||||
{
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
|
|
||||||
var response = new
|
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
message = "Internal server error",
|
|
||||||
timestamp = DateTime.Now,
|
|
||||||
error = exception.Message
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Response.StatusCode = 500;
|
|
||||||
return context.Response.WriteAsJsonAsync(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoggingMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
private readonly ILogger<LoggingMiddleware> _logger;
|
|
||||||
|
|
||||||
public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
var startTime = DateTime.Now;
|
|
||||||
|
|
||||||
// 记录请求信息
|
|
||||||
_logger.LogInformation($"Request: {context.Request.Method} {context.Request.Path}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _next(context);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
var duration = DateTime.Now - startTime;
|
|
||||||
_logger.LogInformation($"Response: {context.Response.StatusCode} - Duration: {duration.TotalMilliseconds}ms");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CORSMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
|
|
||||||
public CORSMiddleware(RequestDelegate next)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
|
||||||
context.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
||||||
context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
|
|
||||||
|
|
||||||
if (context.Request.Method == "OPTIONS")
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 200;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _next(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API响应格式
|
|
||||||
public class ApiResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public object Data { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
public int Code { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static ApiResponse NotFound(string message = "Resource not found")
|
|
||||||
{
|
|
||||||
return Error(message, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ApiResponse BadRequest(string message = "Bad request")
|
|
||||||
{
|
|
||||||
return Error(message, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ApiResponse Unauthorized(string message = "Unauthorized")
|
|
||||||
{
|
|
||||||
return Error(message, 401);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统一响应包装器
|
|
||||||
public class PagedResponse<T>
|
|
||||||
{
|
|
||||||
public IEnumerable<T> Items { get; set; }
|
|
||||||
public int TotalCount { get; set; }
|
|
||||||
public int PageNumber { get; set; }
|
|
||||||
public int PageSize { get; set; }
|
|
||||||
public int TotalPages { get; set; }
|
|
||||||
public bool HasNextPage { get; set; }
|
|
||||||
public bool HasPreviousPage { get; set; }
|
|
||||||
|
|
||||||
public static PagedResponse<T> Create(IEnumerable<T> items, int totalCount, int pageNumber, int pageSize)
|
|
||||||
{
|
|
||||||
return new PagedResponse<T>
|
|
||||||
{
|
|
||||||
Items = items,
|
|
||||||
TotalCount = totalCount,
|
|
||||||
PageNumber = pageNumber,
|
|
||||||
PageSize = pageSize,
|
|
||||||
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
|
|
||||||
HasNextPage = pageNumber < (int)Math.Ceiling(totalCount / (double)pageSize),
|
|
||||||
HasPreviousPage = pageNumber > 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页参数
|
|
||||||
public class PaginationParams
|
|
||||||
{
|
|
||||||
public int PageNumber { get; set; } = 1;
|
|
||||||
public int PageSize { get; set; } = 10;
|
|
||||||
public string SortBy { get; set; }
|
|
||||||
public string SortOrder { get; set; } = "asc";
|
|
||||||
public string Search { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序参数
|
|
||||||
public class SortParams
|
|
||||||
{
|
|
||||||
public string Field { get; set; }
|
|
||||||
public string Direction { get; set; } = "asc";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,356 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
using Haoliang.Models.Production;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IRealTimeService
|
|
||||||
{
|
|
||||||
Task BroadcastDeviceStatusAsync();
|
|
||||||
Task BroadcastProductionDataAsync();
|
|
||||||
Task BroadcastAlarmDataAsync();
|
|
||||||
Task SendDeviceUpdateAsync(int deviceId, DeviceCurrentStatus status);
|
|
||||||
Task SendProductionUpdateAsync(int deviceId, ProductionRecord production);
|
|
||||||
Task SendAlarmUpdateAsync(Alarm alarm);
|
|
||||||
Task BroadcastSystemHealthAsync();
|
|
||||||
Task JoinDeviceGroupAsync(string connectionId, int deviceId);
|
|
||||||
Task LeaveDeviceGroupAsync(string connectionId, int deviceId);
|
|
||||||
Task JoinAllDevicesGroupAsync(string connectionId);
|
|
||||||
Task LeaveAllDevicesGroupAsync(string connectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RealTimeService : IRealTimeService
|
|
||||||
{
|
|
||||||
private readonly IHubContext<RealTimeHub> _hubContext;
|
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
|
||||||
private readonly IProductionRepository _productionRepository;
|
|
||||||
private readonly IAlarmRepository _alarmRepository;
|
|
||||||
private readonly ICollectionRepository _collectionRepository;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
private readonly Dictionary<string, HashSet<int>> _userDeviceGroups = new Dictionary<string, HashSet<int>>();
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
|
|
||||||
public RealTimeService(
|
|
||||||
IHubContext<RealTimeHub> hubContext,
|
|
||||||
IDeviceRepository deviceRepository,
|
|
||||||
IProductionRepository productionRepository,
|
|
||||||
IAlarmRepository alarmRepository,
|
|
||||||
ICollectionRepository collectionRepository,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_hubContext = hubContext;
|
|
||||||
_deviceRepository = deviceRepository;
|
|
||||||
_productionRepository = productionRepository;
|
|
||||||
_alarmRepository = alarmRepository;
|
|
||||||
_collectionRepository = collectionRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BroadcastDeviceStatusAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var devices = await _deviceRepository.GetAllAsync();
|
|
||||||
var deviceStatuses = new List<DeviceCurrentStatus>();
|
|
||||||
|
|
||||||
foreach (var device in devices)
|
|
||||||
{
|
|
||||||
var status = await GetDeviceCurrentStatusAsync(device.Id);
|
|
||||||
deviceStatuses.Add(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveDeviceStatusUpdate", deviceStatuses);
|
|
||||||
_logger.LogDebug($"Broadcasted device status update for {deviceStatuses.Count} devices");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to broadcast device status update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BroadcastProductionDataAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var devices = await _deviceRepository.GetAllAsync();
|
|
||||||
var productionData = new Dictionary<int, List<ProductionRecord>>();
|
|
||||||
|
|
||||||
foreach (var device in devices)
|
|
||||||
{
|
|
||||||
var todayProductions = await _productionRepository.GetByDeviceAndDateAsync(device.Id, DateTime.Today);
|
|
||||||
productionData[device.Id] = todayProductions.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveProductionUpdate", productionData);
|
|
||||||
_logger.LogDebug($"Broadcasted production data update for {devices.Count} devices");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to broadcast production data update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BroadcastAlarmDataAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activeAlarms = await _alarmRepository.GetActiveAlarmsAsync();
|
|
||||||
var alarmData = activeAlarms.Select(a => new
|
|
||||||
{
|
|
||||||
a.AlarmId,
|
|
||||||
a.DeviceCode,
|
|
||||||
a.AlarmType,
|
|
||||||
a.Severity,
|
|
||||||
a.Title,
|
|
||||||
a.Description,
|
|
||||||
a.AlarmStatus,
|
|
||||||
a.CreateTime
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveAlarmUpdate", alarmData);
|
|
||||||
_logger.LogDebug($"Broadcasted alarm data update for {alarmData.Count} alarms");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to broadcast alarm data update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendDeviceUpdateAsync(int deviceId, DeviceCurrentStatus status)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _hubContext.Clients.Group($"device_{deviceId}").SendAsync("ReceiveDeviceUpdate", status);
|
|
||||||
_logger.LogDebug($"Sent device update for device {deviceId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to send device update for device {deviceId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendProductionUpdateAsync(int deviceId, ProductionRecord production)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _hubContext.Clients.Group($"device_{deviceId}").SendAsync("ReceiveProductionUpdate", production);
|
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveGlobalProductionUpdate", production);
|
|
||||||
_logger.LogDebug($"Sent production update for device {deviceId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to send production update for device {deviceId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendAlarmUpdateAsync(Alarm alarm)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _hubContext.Clients.Group($"device_{alarm.DeviceId}").SendAsync("ReceiveAlarmUpdate", alarm);
|
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveGlobalAlarmUpdate", alarm);
|
|
||||||
_logger.LogDebug($"Sent alarm update for alarm {alarm.AlarmId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to send alarm update for alarm {alarm.AlarmId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BroadcastSystemHealthAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var onlineDevices = await _deviceRepository.CountOnlineDevicesAsync();
|
|
||||||
var activeAlarms = await _alarmRepository.CountActiveAlarmsAsync();
|
|
||||||
var totalProductions = await _productionRepository.CountTodayProductionsAsync();
|
|
||||||
|
|
||||||
var healthData = new
|
|
||||||
{
|
|
||||||
Timestamp = DateTime.Now,
|
|
||||||
OnlineDevices = onlineDevices,
|
|
||||||
ActiveAlarms = activeAlarms,
|
|
||||||
TotalProductions = totalProductions,
|
|
||||||
SystemStatus = activeAlarms > 10 ? "Warning" : "Healthy"
|
|
||||||
};
|
|
||||||
|
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveSystemHealth", healthData);
|
|
||||||
_logger.LogDebug($"Broadcasted system health update");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to broadcast system health update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task JoinDeviceGroupAsync(string connectionId, int deviceId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (!_userDeviceGroups.ContainsKey(connectionId))
|
|
||||||
{
|
|
||||||
_userDeviceGroups[connectionId] = new HashSet<int>();
|
|
||||||
}
|
|
||||||
_userDeviceGroups[connectionId].Add(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _hubContext.Groups.AddToGroupAsync(connectionId, $"device_{deviceId}");
|
|
||||||
_logger.LogDebug($"Connection {connectionId} joined device group {deviceId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to add connection {connectionId} to device group {deviceId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LeaveDeviceGroupAsync(string connectionId, int deviceId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (_userDeviceGroups.ContainsKey(connectionId))
|
|
||||||
{
|
|
||||||
_userDeviceGroups[connectionId].Remove(deviceId);
|
|
||||||
if (_userDeviceGroups[connectionId].Count == 0)
|
|
||||||
{
|
|
||||||
_userDeviceGroups.Remove(connectionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _hubContext.Groups.RemoveFromGroupAsync(connectionId, $"device_{deviceId}");
|
|
||||||
_logger.LogDebug($"Connection {connectionId} left device group {deviceId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to remove connection {connectionId} from device group {deviceId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task JoinAllDevicesGroupAsync(string connectionId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _hubContext.Groups.AddToGroupAsync(connectionId, "all_devices");
|
|
||||||
_logger.LogDebug($"Connection {connectionId} joined all devices group");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to add connection {connectionId} to all devices group");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LeaveAllDevicesGroupAsync(string connectionId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _hubContext.Groups.RemoveFromGroupAsync(connectionId, "all_devices");
|
|
||||||
_logger.LogDebug($"Connection {connectionId} left all devices group");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Failed to remove connection {connectionId} from all devices group");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<DeviceCurrentStatus> GetDeviceCurrentStatusAsync(int deviceId)
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
||||||
if (device == null)
|
|
||||||
return new DeviceCurrentStatus();
|
|
||||||
|
|
||||||
var latestCollection = await _collectionRepository.GetLatestDeviceStatusAsync(deviceId);
|
|
||||||
|
|
||||||
return new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
DeviceCode = device.DeviceCode,
|
|
||||||
DeviceName = device.DeviceName,
|
|
||||||
Status = latestCollection?.Status ?? "Unknown",
|
|
||||||
IsRunning = latestCollection?.IsRunning ?? false,
|
|
||||||
NCProgram = latestCollection?.NCProgram ?? "",
|
|
||||||
CumulativeCount = latestCollection?.CumulativeCount ?? 0,
|
|
||||||
OperatingMode = latestCollection?.OperatingMode ?? "",
|
|
||||||
RecordTime = latestCollection?.RecordTime ?? DateTime.Now
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignalR Hub for real-time communication
|
|
||||||
public class RealTimeHub : Hub
|
|
||||||
{
|
|
||||||
private readonly IRealTimeService _realTimeService;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public RealTimeHub(IRealTimeService realTimeService, ILoggerService logger)
|
|
||||||
{
|
|
||||||
_realTimeService = realTimeService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task OnConnectedAsync()
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Connection {Context.ConnectionId} connected to RealTimeHub");
|
|
||||||
|
|
||||||
// Automatically join all devices group for new connections
|
|
||||||
await _realTimeService.JoinAllDevicesGroupAsync(Context.ConnectionId);
|
|
||||||
|
|
||||||
await base.OnConnectedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task OnDisconnectedAsync(Exception exception)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Connection {Context.ConnectionId} disconnected from RealTimeHub");
|
|
||||||
|
|
||||||
// Clean up device group memberships
|
|
||||||
// Note: This is a simplified cleanup - in production you'd track which groups each user was in
|
|
||||||
await _realTimeService.LeaveAllDevicesGroupAsync(Context.ConnectionId);
|
|
||||||
|
|
||||||
await base.OnDisconnectedAsync(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client methods that can be called from frontend
|
|
||||||
public async Task JoinDeviceGroup(int deviceId)
|
|
||||||
{
|
|
||||||
await _realTimeService.JoinDeviceGroupAsync(Context.ConnectionId, deviceId);
|
|
||||||
await Clients.Caller.SendAsync("JoinedDeviceGroup", deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LeaveDeviceGroup(int deviceId)
|
|
||||||
{
|
|
||||||
await _realTimeService.LeaveDeviceGroupAsync(Context.ConnectionId, deviceId);
|
|
||||||
await Clients.Caller.SendAsync("LeftDeviceGroup", deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RequestSystemHealth()
|
|
||||||
{
|
|
||||||
await _realTimeService.BroadcastSystemHealthAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RequestDeviceStatus()
|
|
||||||
{
|
|
||||||
await _realTimeService.BroadcastDeviceStatusAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RequestProductionData()
|
|
||||||
{
|
|
||||||
await _realTimeService.BroadcastProductionDataAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RequestAlarmData()
|
|
||||||
{
|
|
||||||
await _realTimeService.BroadcastAlarmDataAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,438 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface IRulesService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get all business rules
|
|
||||||
/// </summary>
|
|
||||||
Task<List<BusinessRuleConfig>> GetAllRulesAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create or update business rule
|
|
||||||
/// </summary>
|
|
||||||
Task<BusinessRuleConfig> CreateOrUpdateRuleAsync(BusinessRuleConfig rule);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delete business rule
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> DeleteRuleAsync(int ruleId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get statistics rules
|
|
||||||
/// </summary>
|
|
||||||
Task<List<StatisticsRuleConfig>> GetStatisticsRulesAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update statistics rules
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> UpdateStatisticsRulesAsync(List<StatisticsRuleConfig> rules);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate business rule expression
|
|
||||||
/// </summary>
|
|
||||||
Task<RuleValidationResult> ValidateRuleAsync(BusinessRuleConfig rule);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluate business rule against data
|
|
||||||
/// </summary>
|
|
||||||
Task<RuleEvaluationResult> EvaluateRuleAsync(BusinessRuleConfig rule, object data);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get rule execution history
|
|
||||||
/// </summary>
|
|
||||||
Task<List<RuleExecutionHistory>> GetRuleExecutionHistoryAsync(int ruleId, DateTime? startDate = null, DateTime? endDate = null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RulesService : IRulesService
|
|
||||||
{
|
|
||||||
private readonly ISystemRepository _systemRepository;
|
|
||||||
private readonly IProductionRepository _productionRepository;
|
|
||||||
private readonly IAlarmRepository _alarmRepository;
|
|
||||||
private readonly ICacheService _cacheService;
|
|
||||||
|
|
||||||
public RulesService(
|
|
||||||
ISystemRepository systemRepository,
|
|
||||||
IProductionRepository productionRepository,
|
|
||||||
IAlarmRepository alarmRepository,
|
|
||||||
ICacheService cacheService)
|
|
||||||
{
|
|
||||||
_systemRepository = systemRepository;
|
|
||||||
_productionRepository = productionRepository;
|
|
||||||
_alarmRepository = alarmRepository;
|
|
||||||
_cacheService = cacheService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<BusinessRuleConfig>> GetAllRulesAsync()
|
|
||||||
{
|
|
||||||
return await _cacheService.GetOrSetAllRulesAsync(() =>
|
|
||||||
_systemRepository.GetAllBusinessRulesAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BusinessRuleConfig> CreateOrUpdateRuleAsync(BusinessRuleConfig rule)
|
|
||||||
{
|
|
||||||
// Validate rule before saving
|
|
||||||
var validationResult = await ValidateRuleAsync(rule);
|
|
||||||
if (!validationResult.IsValid)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid rule: {validationResult.ErrorMessage}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save rule to repository
|
|
||||||
var savedRule = await _systemRepository.SaveBusinessRuleAsync(rule);
|
|
||||||
|
|
||||||
// Clear cache
|
|
||||||
_cacheService.InvalidateRulesCache();
|
|
||||||
|
|
||||||
return savedRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteRuleAsync(int ruleId)
|
|
||||||
{
|
|
||||||
var result = await _systemRepository.DeleteBusinessRuleAsync(ruleId);
|
|
||||||
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
_cacheService.InvalidateRulesCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<StatisticsRuleConfig>> GetStatisticsRulesAsync()
|
|
||||||
{
|
|
||||||
return await _cacheService.GetOrSetAllStatisticsRulesAsync(() =>
|
|
||||||
_systemRepository.GetAllStatisticsRulesAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> UpdateStatisticsRulesAsync(List<StatisticsRuleConfig> rules)
|
|
||||||
{
|
|
||||||
// Validate all rules
|
|
||||||
foreach (var rule in rules)
|
|
||||||
{
|
|
||||||
var validationResult = await ValidateStatisticsRuleAsync(rule);
|
|
||||||
if (!validationResult.IsValid)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid statistics rule: {validationResult.ErrorMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save all rules
|
|
||||||
var result = await _systemRepository.SaveStatisticsRulesAsync(rules);
|
|
||||||
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
_cacheService.InvalidateRulesCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<RuleValidationResult> ValidateRuleAsync(BusinessRuleConfig rule)
|
|
||||||
{
|
|
||||||
var result = new RuleValidationResult { IsValid = true };
|
|
||||||
|
|
||||||
if (rule == null)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = "Rule cannot be null";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.RuleName))
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = "Rule name cannot be empty";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.RuleExpression))
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = "Rule expression cannot be empty";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate rule syntax
|
|
||||||
if (!IsValidRuleExpression(rule.RuleExpression))
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = "Invalid rule expression syntax";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test rule with sample data
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sampleData = GetSampleDataForRule(rule);
|
|
||||||
var testResult = await EvaluateRuleAsync(rule, sampleData);
|
|
||||||
|
|
||||||
if (!testResult.Success)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = $"Rule evaluation failed: {testResult.ErrorMessage}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = $"Rule validation error: {ex.Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<RuleEvaluationResult> EvaluateRuleAsync(BusinessRuleConfig rule, object data)
|
|
||||||
{
|
|
||||||
var result = new RuleEvaluationResult { Success = true };
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Parse and evaluate the rule expression
|
|
||||||
var evaluator = new RuleExpressionEvaluator();
|
|
||||||
var evaluationResult = evaluator.Evaluate(rule.RuleExpression, data);
|
|
||||||
|
|
||||||
result.Success = evaluationResult.Success;
|
|
||||||
result.Result = evaluationResult.Result;
|
|
||||||
result.EvaluationTime = evaluationResult.EvaluationTime;
|
|
||||||
result.ErrorMessage = evaluationResult.ErrorMessage;
|
|
||||||
|
|
||||||
// Log rule execution if successful
|
|
||||||
if (result.Success && rule.Enabled)
|
|
||||||
{
|
|
||||||
await LogRuleExecutionAsync(rule, data, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Success = false;
|
|
||||||
result.ErrorMessage = ex.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<RuleExecutionHistory>> GetRuleExecutionHistoryAsync(int ruleId, DateTime? startDate = null, DateTime? endDate = null)
|
|
||||||
{
|
|
||||||
return await _systemRepository.GetRuleExecutionHistoryAsync(ruleId, startDate, endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
|
|
||||||
private bool IsValidRuleExpression(string expression)
|
|
||||||
{
|
|
||||||
// Basic validation - in a real implementation, you would use a proper expression parser
|
|
||||||
return !string.IsNullOrWhiteSpace(expression) &&
|
|
||||||
!expression.Contains("DELETE") &&
|
|
||||||
!expression.Contains("DROP") &&
|
|
||||||
!expression.Contains("TRUNCATE");
|
|
||||||
}
|
|
||||||
|
|
||||||
private object GetSampleDataForRule(BusinessRuleConfig rule)
|
|
||||||
{
|
|
||||||
// Return sample data based on rule type
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
Production = new { Quantity = 100, Target = 120, Quality = 95 },
|
|
||||||
Device = new { Status = "Running", Efficiency = 85 },
|
|
||||||
Time = DateTime.Now
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ValidateStatisticsRuleAsync(StatisticsRuleConfig rule)
|
|
||||||
{
|
|
||||||
// Implementation for validating statistics rules
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.RuleName))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Statistics rule name cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.CalculationExpression))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Statistics rule calculation expression cannot be empty");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LogRuleExecutionAsync(BusinessRuleConfig rule, object data, RuleEvaluationResult result)
|
|
||||||
{
|
|
||||||
var execution = new RuleExecutionHistory
|
|
||||||
{
|
|
||||||
RuleId = rule.RuleId,
|
|
||||||
RuleName = rule.RuleName,
|
|
||||||
InputDataJson = System.Text.Json.JsonSerializer.Serialize(data),
|
|
||||||
Result = result.Result?.ToString(),
|
|
||||||
Success = result.Success,
|
|
||||||
ErrorMessage = result.ErrorMessage,
|
|
||||||
ExecutionTime = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
await _systemRepository.LogRuleExecutionAsync(execution);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Supporting Classes
|
|
||||||
|
|
||||||
public class RuleExpressionEvaluator
|
|
||||||
{
|
|
||||||
public RuleEvaluationResult Evaluate(string expression, object data)
|
|
||||||
{
|
|
||||||
var result = new RuleEvaluationResult();
|
|
||||||
var startTime = DateTime.UtcNow;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Parse the expression and evaluate against data
|
|
||||||
// This is a simplified implementation
|
|
||||||
// In a real scenario, you would use a proper expression parser or scripting engine
|
|
||||||
|
|
||||||
var parser = new ExpressionParser();
|
|
||||||
var evaluationResult = parser.ParseAndEvaluate(expression, data);
|
|
||||||
|
|
||||||
result.Success = evaluationResult.Success;
|
|
||||||
result.Result = evaluationResult.Value;
|
|
||||||
result.ErrorMessage = evaluationResult.Error;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Success = false;
|
|
||||||
result.ErrorMessage = ex.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.EvaluationTime = DateTime.UtcNow - startTime;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ExpressionParser
|
|
||||||
{
|
|
||||||
public ParseResult ParseAndEvaluate(string expression, object data)
|
|
||||||
{
|
|
||||||
var result = new ParseResult();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Simple expression evaluation
|
|
||||||
// In a real implementation, you would use a proper expression parser
|
|
||||||
// like NCalc, System.Linq.Dynamic.Core, or a custom parser
|
|
||||||
|
|
||||||
if (expression.Contains(">"))
|
|
||||||
{
|
|
||||||
var parts = expression.Split('>');
|
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
|
||||||
var left = EvaluateExpression(parts[0].Trim(), data);
|
|
||||||
var right = EvaluateExpression(parts[1].Trim(), data);
|
|
||||||
|
|
||||||
if (left != null && right != null)
|
|
||||||
{
|
|
||||||
result.Value = Convert.ToDecimal(left) > Convert.ToDecimal(right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (expression.Contains("<"))
|
|
||||||
{
|
|
||||||
var parts = expression.Split('<');
|
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
|
||||||
var left = EvaluateExpression(parts[0].Trim(), data);
|
|
||||||
var right = EvaluateExpression(parts[1].Trim(), data);
|
|
||||||
|
|
||||||
if (left != null && right != null)
|
|
||||||
{
|
|
||||||
result.Value = Convert.ToDecimal(left) < Convert.ToDecimal(right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (expression.Contains("="))
|
|
||||||
{
|
|
||||||
var parts = expression.Split('=');
|
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
|
||||||
var left = EvaluateExpression(parts[0].Trim(), data);
|
|
||||||
var right = EvaluateExpression(parts[1].Trim(), data);
|
|
||||||
|
|
||||||
if (left != null && right != null)
|
|
||||||
{
|
|
||||||
result.Value = left.ToString() == right.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Simple value evaluation
|
|
||||||
result.Value = EvaluateExpression(expression, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Success = true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Success = false;
|
|
||||||
result.Error = ex.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private object EvaluateExpression(string expression, object data)
|
|
||||||
{
|
|
||||||
// Simple property extraction from data object
|
|
||||||
// In a real implementation, this would be more sophisticated
|
|
||||||
|
|
||||||
if (data is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Handle simple property access
|
|
||||||
if (expression.Contains("."))
|
|
||||||
{
|
|
||||||
var parts = expression.Split('.');
|
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
|
||||||
var property = parts[1];
|
|
||||||
var dataDict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(
|
|
||||||
System.Text.Json.JsonSerializer.Serialize(data));
|
|
||||||
|
|
||||||
return dataDict?.TryGetValue(property, out var value) == true ? value : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle simple numeric comparison
|
|
||||||
if (decimal.TryParse(expression, out var numericValue))
|
|
||||||
{
|
|
||||||
return numericValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ParseResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public object Value { get; set; }
|
|
||||||
public string Error { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RuleEvaluationResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public object Result { get; set; }
|
|
||||||
public TimeSpan EvaluationTime { get; set; }
|
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RuleValidationResult
|
|
||||||
{
|
|
||||||
public bool IsValid { get; set; }
|
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
public List<string> Warnings { get; set; } = new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@ -0,0 +1,323 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Haoliang.Models.User;
|
||||||
|
using Haoliang.Models.Device;
|
||||||
|
using Haoliang.Models.Production;
|
||||||
|
using Haoliang.Models.System;
|
||||||
|
using Haoliang.Models.Template;
|
||||||
|
using Haoliang.Models.DataCollection;
|
||||||
|
|
||||||
|
namespace Haoliang.Core.Services
|
||||||
|
{
|
||||||
|
#region ========== 用户与认证服务 ==========
|
||||||
|
public class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
public Task<AuthResult> LoginAsync(LoginRequest request) => Task.FromResult<AuthResult>(null);
|
||||||
|
public Task<bool> LogoutAsync(int userId) => Task.FromResult(false);
|
||||||
|
public Task<AuthResult> RefreshTokenAsync(string refreshToken) => Task.FromResult<AuthResult>(null);
|
||||||
|
public Task<bool> UsernameExistsAsync(string username) => Task.FromResult(false);
|
||||||
|
public Task<bool> EmailExistsAsync(string email) => Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserService : IUserService
|
||||||
|
{
|
||||||
|
public Task<UserViewModel> CreateUserAsync(User user) => Task.FromResult<UserViewModel>(null);
|
||||||
|
public Task<UserViewModel> GetUserByIdAsync(int userId) => Task.FromResult<UserViewModel>(null);
|
||||||
|
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync() => Task.FromResult<IEnumerable<UserViewModel>>(new List<UserViewModel>());
|
||||||
|
public Task<UserViewModel> UpdateUserAsync(int userId, User user) => Task.FromResult<UserViewModel>(null);
|
||||||
|
public Task<bool> ChangePasswordAsync(int userId, string oldPassword, string newPassword) => Task.FromResult(false);
|
||||||
|
public Task<bool> ActivateUserAsync(int userId) => Task.FromResult(false);
|
||||||
|
public Task<bool> DeactivateUserAsync(int userId) => Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PermissionService : IPermissionService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<string>> GetUserPermissionsAsync(int userId) => Task.FromResult<IEnumerable<string>>(new List<string>());
|
||||||
|
public Task<bool> HasPermissionAsync(int userId, string permission) => Task.FromResult(false);
|
||||||
|
public Task AssignPermissionsToUserAsync(int userId, IEnumerable<string> permissions) => Task.CompletedTask;
|
||||||
|
public Task RemoveAllPermissionsFromUserAsync(int userId) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 日志与缓存服务 ==========
|
||||||
|
public class LoggingService : ILoggingService
|
||||||
|
{
|
||||||
|
public Task LogInformationAsync(string message) { Console.WriteLine($"[INFO] {message}"); return Task.CompletedTask; }
|
||||||
|
public Task LogWarningAsync(string message) { Console.WriteLine($"[WARN] {message}"); return Task.CompletedTask; }
|
||||||
|
public Task LogErrorAsync(string message, Exception? exception = null) { Console.WriteLine($"[ERROR] {message}: {exception?.Message}"); return Task.CompletedTask; }
|
||||||
|
public Task<IEnumerable<LogEntry>> GetLogsAsync(Haoliang.Models.System.LogLevel? logLevel, DateTime? startDate, DateTime? endDate, string? category = null) => Task.FromResult<IEnumerable<LogEntry>>(new List<LogEntry>());
|
||||||
|
public Task<IEnumerable<LogEntry>> GetErrorLogsAsync(DateTime? startDate = null, DateTime? endDate = null) => Task.FromResult<IEnumerable<LogEntry>>(new List<LogEntry>());
|
||||||
|
public Task<int> GetLogCountAsync(Haoliang.Models.System.LogLevel? logLevel = null, DateTime? startDate = null, DateTime? endDate = null) => Task.FromResult(0);
|
||||||
|
public Task ArchiveLogsAsync(int daysToKeep = 90) => Task.CompletedTask;
|
||||||
|
public Task ClearLogsAsync() => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MemoryCacheService : ICacheService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, object> _cache = new Dictionary<string, object>();
|
||||||
|
public T? Get<T>(string key) where T : class => _cache.TryGetValue(key, out var value) ? value as T : null;
|
||||||
|
public void Set<T>(string key, T value, TimeSpan? expiration = null) where T : class { _cache[key] = value; }
|
||||||
|
public bool Remove(string key) => _cache.Remove(key);
|
||||||
|
public bool Exists(string key) => _cache.ContainsKey(key);
|
||||||
|
public T GetOrSet<T>(string key, Func<T> factory, TimeSpan? expiration = null) where T : class { if (!_cache.TryGetValue(key, out var value)) { value = factory(); _cache[key] = value; } return (T)value; }
|
||||||
|
public void Clear() => _cache.Clear();
|
||||||
|
public CacheStats GetStatistics() => new CacheStats();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 设备采集服务 ==========
|
||||||
|
public class DeviceCollectionService : IDeviceCollectionService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<CNCDevice>> GetAllDevicesAsync() => Task.FromResult<IEnumerable<CNCDevice>>(new List<CNCDevice>());
|
||||||
|
public Task<CNCDevice?> GetDeviceByIdAsync(int deviceId) => Task.FromResult<CNCDevice?>(null);
|
||||||
|
public Task<CNCDevice> CreateDeviceAsync(CNCDevice device) => Task.FromResult(device);
|
||||||
|
public Task<CNCDevice?> UpdateDeviceAsync(CNCDevice device) => Task.FromResult<CNCDevice?>(null);
|
||||||
|
public Task<bool> DeleteDeviceAsync(int deviceId) => Task.FromResult(false);
|
||||||
|
public Task CollectDeviceAsync(int deviceId) => Task.CompletedTask;
|
||||||
|
public Task CollectAllDevicesAsync() => Task.CompletedTask;
|
||||||
|
public Task<DeviceStatus> GetDeviceStatusAsync(int deviceId) => Task.FromResult(new DeviceStatus());
|
||||||
|
public Task<DeviceHealth> GetDeviceHealthAsync(int deviceId) => Task.FromResult(new DeviceHealth());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceStateMachine : IDeviceStateMachine
|
||||||
|
{
|
||||||
|
public DeviceStatus GetCurrentState(int deviceId) => new DeviceStatus();
|
||||||
|
public bool TransitionTo(int deviceId, DeviceStatus newState) => false;
|
||||||
|
public void RegisterStateChangeHandler(int deviceId, Action<int, DeviceStatus, DeviceStatus> callback) { }
|
||||||
|
public IEnumerable<DeviceStatusChange> GetStateHistory(int deviceId, DateTime fromTime, DateTime toTime) => new List<DeviceStatusChange>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PingService : IPingService
|
||||||
|
{
|
||||||
|
public Task<PingResult> PingAsync(int deviceId, string ipAddress) => Task.FromResult(new PingResult { DeviceId = deviceId, IpAddress = ipAddress, Success = false });
|
||||||
|
public Task<IEnumerable<PingResult>> PingAllAsync(IEnumerable<(int DeviceId, string IpAddress)> devices) => Task.FromResult<IEnumerable<PingResult>>(new List<PingResult>());
|
||||||
|
public Task<bool> IsReachableAsync(string ipAddress, TimeSpan? timeout = null) => Task.FromResult(false);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 生产统计服务 ==========
|
||||||
|
public class ProductionService : IProductionService
|
||||||
|
{
|
||||||
|
public Task<ProductionSummary> GetProductionSummaryAsync(DateTime date) => Task.FromResult<ProductionSummary>(null);
|
||||||
|
public Task<ProductionStatistics> GetProductionStatisticsAsync(DateTime date) => Task.FromResult<ProductionStatistics>(null);
|
||||||
|
public Task<ProductionRecord> GetTodayProductionAsync(int deviceId) => Task.FromResult<ProductionRecord>(null);
|
||||||
|
public Task<ProductionStatistics> GetProductionStatisticsAsync(int deviceId, DateTime date) => Task.FromResult<ProductionStatistics>(null);
|
||||||
|
public Task<decimal> GetQualityRateAsync(int deviceId, DateTime date) => Task.FromResult(0m);
|
||||||
|
public Task CalculateAllProductionAsync() => Task.CompletedTask;
|
||||||
|
public Task CalculateProductionAsync(int deviceId) => Task.CompletedTask;
|
||||||
|
public Task<IEnumerable<string>> GetProductionProgramsAsync(DateTime date) => Task.FromResult<IEnumerable<string>>(new List<string>());
|
||||||
|
public Task<ProgramProductionSummary> GetProgramProductionAsync(string programName, DateTime date) => Task.FromResult<ProgramProductionSummary>(null);
|
||||||
|
public Task<byte[]> ExportProductionDataAsync(DateTime startDate, DateTime endDate) => Task.FromResult<byte[]>(null);
|
||||||
|
public Task ArchiveProductionDataAsync(int daysToKeep = 90) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductionCalculator : IProductionCalculator
|
||||||
|
{
|
||||||
|
public Task<decimal> CalculateProductionIncrementAsync(int deviceId, decimal currentValue, string programName, DateTime timestamp) => Task.FromResult(0m);
|
||||||
|
public void ResetDeviceProductionState(int deviceId) { }
|
||||||
|
public bool ValidateProductionValue(int deviceId, decimal value) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductionScheduler : IProductionScheduler
|
||||||
|
{
|
||||||
|
private bool _isRunning;
|
||||||
|
public Task StartAsync() { _isRunning = true; return Task.CompletedTask; }
|
||||||
|
public Task StopAsync() { _isRunning = false; return Task.CompletedTask; }
|
||||||
|
public void RegisterTask(string taskId, string schedule, Func<Task> action) { }
|
||||||
|
public void RemoveTask(string taskId) { }
|
||||||
|
public bool IsRunning => _isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductionStatisticsService : IProductionStatisticsService
|
||||||
|
{
|
||||||
|
public Task<ProductionTrendAnalysis> CalculateProductionTrendsAsync(int deviceId, DateTime startDate, DateTime endDate) => Task.FromResult<ProductionTrendAnalysis>(null);
|
||||||
|
public Task<ProductionReport> GenerateProductionReportAsync(ReportFilter filter) => Task.FromResult<ProductionReport>(null);
|
||||||
|
public Task<EfficiencyMetrics> CalculateEfficiencyMetricsAsync(EfficiencyFilter filter) => Task.FromResult<EfficiencyMetrics>(null);
|
||||||
|
public Task<QualityAnalysis> PerformQualityAnalysisAsync(QualityFilter filter) => Task.FromResult<QualityAnalysis>(null);
|
||||||
|
public Task<DashboardSummary> GetDashboardSummaryAsync(DashboardFilter filter) => Task.FromResult<DashboardSummary>(null);
|
||||||
|
public Task<OeeMetrics> CalculateOeeAsync(int deviceId, DateTime date) => Task.FromResult<OeeMetrics>(null);
|
||||||
|
public Task<ProductionForecast> GenerateProductionForecastAsync(ForecastFilter filter) => Task.FromResult<ProductionForecast>(null);
|
||||||
|
public Task<AnomalyAnalysis> DetectProductionAnomaliesAsync(AnomalyFilter filter) => Task.FromResult<AnomalyAnalysis>(null);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 告警服务 ==========
|
||||||
|
public class AlarmService : IAlarmService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<Alarm>> GetAllAlarmsAsync() => Task.FromResult<IEnumerable<Alarm>>(new List<Alarm>());
|
||||||
|
public Task<IEnumerable<Alarm>> GetAlarmsByTypeAsync(AlarmType type) => Task.FromResult<IEnumerable<Alarm>>(new List<Alarm>());
|
||||||
|
public Task<IEnumerable<Alarm>> GetActiveAlarmsAsync() => Task.FromResult<IEnumerable<Alarm>>(new List<Alarm>());
|
||||||
|
public Task<Alarm?> GetAlarmByIdAsync(int alarmId) => Task.FromResult<Alarm?>(null);
|
||||||
|
public Task<Alarm> CreateAlarmAsync(Alarm alarm) => Task.FromResult(alarm);
|
||||||
|
public Task<Alarm?> UpdateAlarmAsync(int alarmId, Alarm alarm) => Task.FromResult<Alarm?>(null);
|
||||||
|
public Task<bool> DeleteAlarmAsync(int alarmId) => Task.FromResult(false);
|
||||||
|
public Task<bool> ResolveAlarmAsync(int alarmId, string? resolutionNote) => Task.FromResult(false);
|
||||||
|
public Task<bool> AcknowledgeAlarmAsync(int alarmId, string? acknowledgeNote) => Task.FromResult(false);
|
||||||
|
public Task<IEnumerable<Alarm>> GetDeviceAlarmsAsync(int deviceId, int days = 7) => Task.FromResult<IEnumerable<Alarm>>(new List<Alarm>());
|
||||||
|
public Task<IEnumerable<Alarm>> GetCriticalAlarmsAsync() => Task.FromResult<IEnumerable<Alarm>>(new List<Alarm>());
|
||||||
|
public Task<AlarmStatistics> GetAlarmStatisticsAsync(DateTime date) => Task.FromResult<AlarmStatistics>(null);
|
||||||
|
public Task<IEnumerable<Alarm>> GetAlarmsByDateRangeAsync(DateTime startDate, DateTime endDate) => Task.FromResult<IEnumerable<Alarm>>(new List<Alarm>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlarmRuleService : IAlarmRuleService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<AlarmRule>> GetAllAlarmRulesAsync() => Task.FromResult<IEnumerable<AlarmRule>>(new List<AlarmRule>());
|
||||||
|
public Task<AlarmRule?> GetAlarmRuleByIdAsync(int ruleId) => Task.FromResult<AlarmRule?>(null);
|
||||||
|
public Task<AlarmRule> CreateAlarmRuleAsync(AlarmRule rule) => Task.FromResult(rule);
|
||||||
|
public Task<AlarmRule?> UpdateAlarmRuleAsync(int ruleId, AlarmRule rule) => Task.FromResult<AlarmRule?>(null);
|
||||||
|
public Task<bool> DeleteAlarmRuleAsync(int ruleId) => Task.FromResult(false);
|
||||||
|
public Task TestAlarmRuleAsync(int ruleId) => Task.CompletedTask;
|
||||||
|
public Task SetAlarmRuleEnabledAsync(int ruleId, bool enabled) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlarmNotificationService : IAlarmNotificationService
|
||||||
|
{
|
||||||
|
public Task SendAlarmNotificationAsync(AlarmNotification notification) => Task.CompletedTask;
|
||||||
|
public Task SendAlarmNotificationToChannelsAsync(Alarm alarm, IEnumerable<NotificationChannel> channels) => Task.CompletedTask;
|
||||||
|
public Task<NotificationStatus> GetNotificationStatusAsync(int notificationId) => Task.FromResult<NotificationStatus>(default);
|
||||||
|
public Task RetryNotificationAsync(int notificationId) => Task.CompletedTask;
|
||||||
|
public Task CancelNotificationAsync(int notificationId) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 模板服务 ==========
|
||||||
|
public class TemplateService : ITemplateService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<CNCBrandTemplate>> GetAllTemplatesAsync() => Task.FromResult<IEnumerable<CNCBrandTemplate>>(new List<CNCBrandTemplate>());
|
||||||
|
public Task<CNCBrandTemplate?> GetTemplateByIdAsync(int templateId) => Task.FromResult<CNCBrandTemplate?>(null);
|
||||||
|
public Task<CNCBrandTemplate> CreateTemplateAsync(CNCBrandTemplate template) => Task.FromResult(template);
|
||||||
|
public Task<CNCBrandTemplate?> UpdateTemplateAsync(int templateId, CNCBrandTemplate template) => Task.FromResult<CNCBrandTemplate?>(null);
|
||||||
|
public Task<bool> DeleteTemplateAsync(int templateId) => Task.FromResult(false);
|
||||||
|
public Task<bool> EnableTemplateAsync(int templateId) => Task.FromResult(false);
|
||||||
|
public Task<bool> DisableTemplateAsync(int templateId) => Task.FromResult(false);
|
||||||
|
public Task<CNCBrandTemplate> CloneTemplateAsync(int templateId, string newName) => Task.FromResult<CNCBrandTemplate>(null);
|
||||||
|
public Task TestTemplateAsync(int templateId) => Task.CompletedTask;
|
||||||
|
public Task<IEnumerable<CNCBrandTemplate>> GetTemplatesByBrandAsync(string brandName) => Task.FromResult<IEnumerable<CNCBrandTemplate>>(new List<CNCBrandTemplate>());
|
||||||
|
public Task<IEnumerable<CNCBrandTemplate>> GetActiveTemplatesAsync() => Task.FromResult<IEnumerable<CNCBrandTemplate>>(new List<CNCBrandTemplate>());
|
||||||
|
public Task<bool> ValidateTemplateAsync(CNCBrandTemplate template) => Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TagMappingService : ITagMappingService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<TagMapping>> GetMappingsByTemplateAsync(int templateId) => Task.FromResult<IEnumerable<TagMapping>>(new List<TagMapping>());
|
||||||
|
public Task<TagMapping> CreateTagMappingAsync(TagMapping mapping) => Task.FromResult(mapping);
|
||||||
|
public Task CreateTagMappingsAsync(int templateId, IEnumerable<TagMapping> mappings) => Task.CompletedTask;
|
||||||
|
public Task<TagMapping> UpdateTagMappingAsync(int mappingId, TagMapping mapping) => Task.FromResult(mapping);
|
||||||
|
public Task<bool> DeleteTagMappingAsync(int mappingId) => Task.FromResult(false);
|
||||||
|
public Task<IEnumerable<Haoliang.Models.Device.TagData>> MapDeviceTagsAsync(IEnumerable<Haoliang.Models.Device.TagData> deviceTags, int templateId) => Task.FromResult<IEnumerable<Haoliang.Models.Device.TagData>>(new List<Haoliang.Models.Device.TagData>());
|
||||||
|
public Task<TagMapping?> GetMappingBySystemFieldAsync(int templateId, string systemFieldId) => Task.FromResult<TagMapping?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TemplateValidationService : ITemplateValidationService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<string>> ValidateTemplateForDeviceAsync(int templateId, int deviceId) => Task.FromResult<IEnumerable<string>>(new List<string>());
|
||||||
|
public Task<TemplateMigrationReport> GenerateMigrationReportAsync(CNCBrandTemplate template, string targetBrand) => Task.FromResult<TemplateMigrationReport>(null);
|
||||||
|
public Task<TemplateValidationResult> ValidateTemplateCompletenessAsync(CNCBrandTemplate template) => Task.FromResult<TemplateValidationResult>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TemplateMigrationService : ITemplateMigrationService
|
||||||
|
{
|
||||||
|
public Task<CNCBrandTemplate> MigrateTemplateAsync(int sourceTemplateId, string targetBrand) => Task.FromResult<CNCBrandTemplate>(null);
|
||||||
|
public Task<bool> CanMigrateAsync(int sourceTemplateId, string targetBrand) => Task.FromResult(false);
|
||||||
|
public Task<IEnumerable<TagMappingSuggestion>> GetMigrationMappingSuggestionsAsync(int sourceTemplateId, string targetBrand) => Task.FromResult<IEnumerable<TagMappingSuggestion>>(new List<TagMappingSuggestion>());
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 系统服务 ==========
|
||||||
|
public class SystemService : ISystemService
|
||||||
|
{
|
||||||
|
public Task<SystemStatusInfo> GetSystemStatusAsync() => Task.FromResult<SystemStatusInfo>(null);
|
||||||
|
public Task<HealthCheckResult> PerformHealthCheckAsync() => Task.FromResult<HealthCheckResult>(null);
|
||||||
|
public Task<SystemMetrics> GetSystemMetricsAsync() => Task.FromResult<SystemMetrics>(null);
|
||||||
|
public Task RestartAsync() => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SystemConfigService : ISystemConfigService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<SystemConfig>> GetAllConfigsAsync() => Task.FromResult<IEnumerable<SystemConfig>>(new List<SystemConfig>());
|
||||||
|
public Task<SystemConfig?> GetConfigAsync(string key) => Task.FromResult<SystemConfig?>(null);
|
||||||
|
public Task<SystemConfig> SetConfigAsync(string key, string value) => Task.FromResult<SystemConfig>(null);
|
||||||
|
public Task<bool> DeleteConfigAsync(string key) => Task.FromResult(false);
|
||||||
|
public Task<bool> ConfigExistsAsync(string key) => Task.FromResult(false);
|
||||||
|
public Task<IEnumerable<SystemConfig>> GetConfigsByCategoryAsync(string category) => Task.FromResult<IEnumerable<SystemConfig>>(new List<SystemConfig>());
|
||||||
|
public Task RefreshConfigCacheAsync() => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SchedulerService : ISchedulerService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<ScheduledTask>> GetAllScheduledTasksAsync() => Task.FromResult<IEnumerable<ScheduledTask>>(new List<ScheduledTask>());
|
||||||
|
public Task<ScheduledTask?> GetTaskByIdAsync(string taskId) => Task.FromResult<ScheduledTask?>(null);
|
||||||
|
public Task ScheduleTaskAsync(ScheduledTask task) => Task.CompletedTask;
|
||||||
|
public Task ExecuteTaskAsync(string taskId) => Task.CompletedTask;
|
||||||
|
public Task<bool> RemoveTaskAsync(string taskId) => Task.FromResult(false);
|
||||||
|
public Task StartSchedulerAsync() => Task.CompletedTask;
|
||||||
|
public Task StopSchedulerAsync() => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 实时服务 ==========
|
||||||
|
public class RealTimeService : IRealTimeService
|
||||||
|
{
|
||||||
|
public Task<int> GetConnectedClientsCountAsync() => Task.FromResult(0);
|
||||||
|
public Task<IEnumerable<ClientInfo>> GetConnectedClientsByTypeAsync(string clientType) => Task.FromResult<IEnumerable<ClientInfo>>(new List<ClientInfo>());
|
||||||
|
public Task<DeviceMonitoringStatus> GetDeviceMonitoringStatusAsync(int deviceId) => Task.FromResult<DeviceMonitoringStatus>(null);
|
||||||
|
public Task StartDeviceStreamingAsync(int deviceId, int intervalMs = 1000) => Task.CompletedTask;
|
||||||
|
public Task StopDeviceStreamingAsync(int deviceId) => Task.CompletedTask;
|
||||||
|
public Task<IEnumerable<int>> GetActiveStreamingDevicesAsync() => Task.FromResult<IEnumerable<int>>(new List<int>());
|
||||||
|
public Task BroadcastDeviceStatusAsync(DeviceStatusUpdate statusUpdate) => Task.CompletedTask;
|
||||||
|
public Task BroadcastProductionUpdateAsync(ProductionUpdate productionUpdate) => Task.CompletedTask;
|
||||||
|
public Task BroadcastAlertAsync(AlertUpdate alertUpdate) => Task.CompletedTask;
|
||||||
|
public Task SendSystemNotificationAsync(SystemNotification notification) => Task.CompletedTask;
|
||||||
|
public Task SendDashboardUpdateAsync(DashboardUpdate dashboardUpdate) => Task.CompletedTask;
|
||||||
|
public Task SendCommandToClientAsync(string connectionId, RealTimeCommand command) => Task.CompletedTask;
|
||||||
|
public Task BroadcastCommandAsync(RealTimeCommand command) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 规则与数据服务 ==========
|
||||||
|
public class RulesService : IRulesService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<BusinessRule>> GetAllRulesAsync() => Task.FromResult<IEnumerable<BusinessRule>>(new List<BusinessRule>());
|
||||||
|
public Task<BusinessRule?> GetRuleByIdAsync(int ruleId) => Task.FromResult<BusinessRule?>(null);
|
||||||
|
public Task<BusinessRule> CreateRuleAsync(BusinessRule rule) => Task.FromResult(rule);
|
||||||
|
public Task<BusinessRule?> UpdateRuleAsync(int ruleId, BusinessRule rule) => Task.FromResult<BusinessRule?>(null);
|
||||||
|
public Task<bool> DeleteRuleAsync(int ruleId) => Task.FromResult(false);
|
||||||
|
public Task<RuleExecutionResult> ExecuteRuleAsync(int ruleId, Dictionary<string, object> context) => Task.FromResult<RuleExecutionResult>(null);
|
||||||
|
public Task<RuleTestResult> TestRuleAsync(int ruleId, Dictionary<string, object> testData) => Task.FromResult<RuleTestResult>(null);
|
||||||
|
public Task<IEnumerable<RuleExecutionHistory>> GetRuleExecutionHistoryAsync(int ruleId, DateTime fromTime, DateTime toTime) => Task.FromResult<IEnumerable<RuleExecutionHistory>>(new List<RuleExecutionHistory>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DataParserService : IDataParserService
|
||||||
|
{
|
||||||
|
public Task<ParsedDeviceData> ParseRawDataAsync(string rawData, int templateId) => Task.FromResult<ParsedDeviceData>(null);
|
||||||
|
public Task<IEnumerable<ParsedDeviceData>> ParseMultiDeviceDataAsync(string rawData, int templateId) => Task.FromResult<IEnumerable<ParsedDeviceData>>(new List<ParsedDeviceData>());
|
||||||
|
public bool ValidateDataFormat(string rawData) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DataStorageService : IDataStorageService
|
||||||
|
{
|
||||||
|
public Task StoreDeviceDataAsync(ParsedDeviceData data) => Task.CompletedTask;
|
||||||
|
public Task StoreDeviceDataBatchAsync(IEnumerable<ParsedDeviceData> dataList) => Task.CompletedTask;
|
||||||
|
public Task StoreProductionRecordAsync(ProductionRecord record) => Task.CompletedTask;
|
||||||
|
public Task UpdateDeviceStatusAsync(int deviceId, DeviceStatus status) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RetryService : IRetryService
|
||||||
|
{
|
||||||
|
public async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3, TimeSpan? delay = null) { for (int i = 0; i < maxRetries; i++) { try { return await operation(); } catch { if (i == maxRetries - 1) throw; } } return default; }
|
||||||
|
public async Task ExecuteWithRetryAsync(Func<Task> operation, int maxRetries = 3, TimeSpan? delay = null) { for (int i = 0; i < maxRetries; i++) { try { await operation(); return; } catch { if (i == maxRetries - 1) throw; } } }
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ========== 后台任务服务 ==========
|
||||||
|
public class BackgroundTaskService : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -1,257 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface ISystemConfigService
|
|
||||||
{
|
|
||||||
Task<SystemConfig> GetConfigAsync(string configKey);
|
|
||||||
Task<SystemConfig> SetConfigAsync(string configKey, string configValue);
|
|
||||||
Task<IEnumerable<SystemConfig>> GetAllConfigsAsync();
|
|
||||||
Task<bool> DeleteConfigAsync(string configKey);
|
|
||||||
Task<SystemConfig> GetOrCreateConfigAsync(string configKey, string defaultValue);
|
|
||||||
Task<bool> ValidateConfigAsync(SystemConfig config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SystemConfigService : ISystemConfigService
|
|
||||||
{
|
|
||||||
private readonly ISystemConfigRepository _configRepository;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public SystemConfigService(
|
|
||||||
ISystemConfigRepository configRepository,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_configRepository = configRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> GetConfigAsync(string configKey)
|
|
||||||
{
|
|
||||||
return await _configRepository.GetByKeyAsync(configKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> SetConfigAsync(string configKey, string configValue)
|
|
||||||
{
|
|
||||||
var existingConfig = await _configRepository.GetByKeyAsync(configKey);
|
|
||||||
|
|
||||||
if (existingConfig != null)
|
|
||||||
{
|
|
||||||
// Update existing config
|
|
||||||
existingConfig.ConfigValue = configValue;
|
|
||||||
existingConfig.UpdateTime = DateTime.Now;
|
|
||||||
|
|
||||||
var updatedConfig = await _configRepository.UpdateAsync(existingConfig);
|
|
||||||
await _logger.LogInformationAsync($"Updated config '{configKey}' with new value");
|
|
||||||
return updatedConfig;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Create new config
|
|
||||||
var newConfig = new SystemConfig
|
|
||||||
{
|
|
||||||
ConfigKey = configKey,
|
|
||||||
ConfigValue = configValue,
|
|
||||||
Description = $"Configuration for {configKey}",
|
|
||||||
CreatedAt = DateTime.Now,
|
|
||||||
UpdateTime = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
var createdConfig = await _configRepository.AddAsync(newConfig);
|
|
||||||
await _logger.LogInformationAsync($"Created new config '{configKey}'");
|
|
||||||
return createdConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<SystemConfig>> GetAllConfigsAsync()
|
|
||||||
{
|
|
||||||
return await _configRepository.GetAllAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteConfigAsync(string configKey)
|
|
||||||
{
|
|
||||||
var config = await _configRepository.GetByKeyAsync(configKey);
|
|
||||||
if (config != null)
|
|
||||||
{
|
|
||||||
var result = await _configRepository.DeleteAsync(config.ConfigId);
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
await _logger.LogInformationAsync($"Deleted config '{configKey}'");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> GetOrCreateConfigAsync(string configKey, string defaultValue)
|
|
||||||
{
|
|
||||||
var config = await GetConfigAsync(configKey);
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
return await SetConfigAsync(configKey, defaultValue);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateConfigAsync(SystemConfig config)
|
|
||||||
{
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
await _logger.LogWarningAsync("System config validation failed: config is null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(config.ConfigKey))
|
|
||||||
{
|
|
||||||
await _logger.LogWarningAsync("System config validation failed: config key is empty");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(config.ConfigValue))
|
|
||||||
{
|
|
||||||
await _logger.LogWarningAsync($"System config validation failed: config value is empty for key '{config.ConfigKey}'");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate specific config keys
|
|
||||||
if (config.ConfigKey == "DailyProductionTarget" && !int.TryParse(config.ConfigValue, out _))
|
|
||||||
{
|
|
||||||
await _logger.LogWarningAsync($"System config validation failed: invalid DailyProductionTarget value '{config.ConfigValue}'");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.ConfigKey == "CollectionInterval" && !int.TryParse(config.ConfigValue, out _))
|
|
||||||
{
|
|
||||||
await _logger.LogWarningAsync($"System config validation failed: invalid CollectionInterval value '{config.ConfigValue}'");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SystemConfigManager : ISystemConfigService
|
|
||||||
{
|
|
||||||
private readonly ISystemConfigRepository _configRepository;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public SystemConfigManager(
|
|
||||||
ISystemConfigRepository configRepository,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_configRepository = configRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> GetConfigAsync(string configKey)
|
|
||||||
{
|
|
||||||
return await _configRepository.GetByKeyAsync(configKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> SetConfigAsync(string configKey, string configValue)
|
|
||||||
{
|
|
||||||
return await new SystemConfigService(_configRepository, _logger).SetConfigAsync(configKey, configValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<SystemConfig>> GetAllConfigsAsync()
|
|
||||||
{
|
|
||||||
return await _configRepository.GetAllAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteConfigAsync(string configKey)
|
|
||||||
{
|
|
||||||
return await _configRepository.DeleteByKeyAsync(configKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> GetOrCreateConfigAsync(string configKey, string defaultValue)
|
|
||||||
{
|
|
||||||
return await new SystemConfigService(_configRepository, _logger).GetOrCreateConfigAsync(configKey, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateConfigAsync(SystemConfig config)
|
|
||||||
{
|
|
||||||
return await new SystemConfigService(_configRepository, _logger).ValidateConfigAsync(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoggingManager : ILoggingService
|
|
||||||
{
|
|
||||||
private readonly ILogRepository _logRepository;
|
|
||||||
private readonly ILoggerService _logger;
|
|
||||||
|
|
||||||
public LoggingManager(
|
|
||||||
ILogRepository logRepository,
|
|
||||||
ILoggerService logger)
|
|
||||||
{
|
|
||||||
_logRepository = logRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogInformationAsync(string message)
|
|
||||||
{
|
|
||||||
await _logRepository.LogAsync(message, "Information");
|
|
||||||
await _logger.LogInformationAsync(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogWarningAsync(string message)
|
|
||||||
{
|
|
||||||
await _logRepository.LogAsync(message, "Warning");
|
|
||||||
await _logger.LogWarningAsync(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogErrorAsync(string message)
|
|
||||||
{
|
|
||||||
await _logRepository.LogAsync(message, "Error");
|
|
||||||
await _logger.LogErrorAsync(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogDebugAsync(string message)
|
|
||||||
{
|
|
||||||
await _logRepository.LogAsync(message, "Debug");
|
|
||||||
await _logger.LogDebugAsync(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogExceptionAsync(Exception ex, string message)
|
|
||||||
{
|
|
||||||
var fullMessage = $"{message}: {ex.Message}\n{ex.StackTrace}";
|
|
||||||
await _logRepository.LogAsync(fullMessage, "Error");
|
|
||||||
await _logger.LogErrorAsync(fullMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogAsync(LogLevel level, string message)
|
|
||||||
{
|
|
||||||
await _logRepository.LogAsync(message, level.ToString());
|
|
||||||
await LogByLevel(level, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LogByLevel(LogLevel level, string message)
|
|
||||||
{
|
|
||||||
switch (level)
|
|
||||||
{
|
|
||||||
case LogLevel.Trace:
|
|
||||||
await _logger.LogDebugAsync(message);
|
|
||||||
break;
|
|
||||||
case LogLevel.Debug:
|
|
||||||
await _logger.LogDebugAsync(message);
|
|
||||||
break;
|
|
||||||
case LogLevel.Information:
|
|
||||||
await _logger.LogInformationAsync(message);
|
|
||||||
break;
|
|
||||||
case LogLevel.Warning:
|
|
||||||
await _logger.LogWarningAsync(message);
|
|
||||||
break;
|
|
||||||
case LogLevel.Error:
|
|
||||||
case LogLevel.Critical:
|
|
||||||
await _logger.LogErrorAsync(message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
await _logger.LogInformationAsync(message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,375 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface ISystemConfigService
|
|
||||||
{
|
|
||||||
Task<SystemConfig> GetConfigAsync(string configKey);
|
|
||||||
Task<Dictionary<string, string>> GetAllConfigsAsync();
|
|
||||||
Task<SystemConfig> SetConfigAsync(string configKey, string configValue);
|
|
||||||
Task<bool> DeleteConfigAsync(string configKey);
|
|
||||||
Task<bool> ConfigExistsAsync(string configKey);
|
|
||||||
Task<IEnumerable<SystemConfig>> GetConfigsByCategoryAsync(string category);
|
|
||||||
Task<bool> ValidateConfigAsync(SystemConfig config);
|
|
||||||
Task RefreshConfigCacheAsync();
|
|
||||||
Task<T> GetConfigValueAsync<T>(string configKey, T defaultValue = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ILoggingService
|
|
||||||
{
|
|
||||||
Task LogAsync(LogLevel logLevel, string message, Exception exception = null, Dictionary<string, object> properties = null);
|
|
||||||
Task LogErrorAsync(string message, Exception exception = null, Dictionary<string, object> properties = null);
|
|
||||||
Task LogWarningAsync(string message, Dictionary<string, object> properties = null);
|
|
||||||
Task LogInfoAsync(string message, Dictionary<string, object> properties = null);
|
|
||||||
Task LogDebugAsync(string message, Dictionary<string, object> properties = null);
|
|
||||||
Task LogTraceAsync(string message, Dictionary<string, object> properties = null);
|
|
||||||
Task<IEnumerable<LogEntry>> GetLogsAsync(LogLevel? logLevel = null, DateTime? startDate = null, DateTime? endDate = null, string category = null);
|
|
||||||
Task<IEnumerable<LogEntry>> GetErrorLogsAsync(DateTime? startDate = null, DateTime? endDate = null);
|
|
||||||
Task<int> GetLogCountAsync(LogLevel? logLevel = null, DateTime? startDate = null, DateTime? endDate = null);
|
|
||||||
Task ArchiveLogsAsync(int daysToKeep = 30);
|
|
||||||
Task ClearLogsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ISchedulerService
|
|
||||||
{
|
|
||||||
Task StartSchedulerAsync();
|
|
||||||
Task StopSchedulerAsync();
|
|
||||||
Task ScheduleTaskAsync(ScheduledTask task);
|
|
||||||
Task<bool> RemoveTaskAsync(string taskId);
|
|
||||||
Task<IEnumerable<ScheduledTask>> GetAllScheduledTasksAsync();
|
|
||||||
Task<ScheduledTask> GetTaskByIdAsync(string taskId);
|
|
||||||
Task ExecuteTaskAsync(string taskId);
|
|
||||||
Task<TaskExecutionResult> GetTaskExecutionResultAsync(string taskId);
|
|
||||||
Task<bool> IsTaskRunningAsync(string taskId);
|
|
||||||
Task<string> ScheduleRecurringTaskAsync(string taskName, Action taskAction, TimeSpan interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ICachingService
|
|
||||||
{
|
|
||||||
Task<T> GetAsync<T>(string key);
|
|
||||||
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
|
|
||||||
Task<bool> RemoveAsync(string key);
|
|
||||||
Task<bool> ExistsAsync(string key);
|
|
||||||
Task ClearAsync();
|
|
||||||
Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null);
|
|
||||||
Task<IEnumerable<string>> GetAllKeysAsync();
|
|
||||||
Task<bool> RefreshAsync<T>(string key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SystemConfigManager : ISystemConfigService
|
|
||||||
{
|
|
||||||
private readonly ISystemConfigRepository _configRepository;
|
|
||||||
private readonly ICachingService _cachingService;
|
|
||||||
|
|
||||||
public SystemConfigManager(
|
|
||||||
ISystemConfigRepository configRepository,
|
|
||||||
ICachingService cachingService)
|
|
||||||
{
|
|
||||||
_configRepository = configRepository;
|
|
||||||
_cachingService = cachingService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> GetConfigAsync(string configKey)
|
|
||||||
{
|
|
||||||
// 先从缓存获取
|
|
||||||
var cachedConfig = await _cachingService.GetAsync<SystemConfig>($"config_{configKey}");
|
|
||||||
if (cachedConfig != null)
|
|
||||||
{
|
|
||||||
return cachedConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存未命中,从数据库获取
|
|
||||||
var config = await _configRepository.GetByKeyAsync(configKey);
|
|
||||||
if (config != null)
|
|
||||||
{
|
|
||||||
// 存入缓存
|
|
||||||
await _cachingService.SetAsync($"config_{configKey}", config, TimeSpan.FromMinutes(30));
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, string>> GetAllConfigsAsync()
|
|
||||||
{
|
|
||||||
var configs = await _configRepository.GetAllAsync();
|
|
||||||
var configDict = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
foreach (var config in configs)
|
|
||||||
{
|
|
||||||
configDict[config.ConfigKey] = config.ConfigValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return configDict;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SystemConfig> SetConfigAsync(string configKey, string configValue)
|
|
||||||
{
|
|
||||||
// 验证配置
|
|
||||||
var config = new SystemConfig
|
|
||||||
{
|
|
||||||
ConfigKey = configKey,
|
|
||||||
ConfigValue = configValue,
|
|
||||||
Category = "General",
|
|
||||||
LastUpdated = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
var validationErrors = await ValidateConfigAsync(config);
|
|
||||||
if (validationErrors.Count > 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Config validation failed: {string.Join(", ", validationErrors)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查配置是否已存在
|
|
||||||
var existingConfig = await _configRepository.GetByKeyAsync(configKey);
|
|
||||||
if (existingConfig != null)
|
|
||||||
{
|
|
||||||
config.ConfigId = existingConfig.ConfigId;
|
|
||||||
config.CreateTime = existingConfig.CreateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.LastUpdated = DateTime.Now;
|
|
||||||
var updatedConfig = await _configRepository.UpsertAsync(config);
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
await _cachingService.SetAsync($"config_{configKey}", updatedConfig, TimeSpan.FromMinutes(30));
|
|
||||||
await _cachingService.RemoveAsync("all_configs");
|
|
||||||
|
|
||||||
return updatedConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteConfigAsync(string configKey)
|
|
||||||
{
|
|
||||||
var result = await _configRepository.DeleteByKeyAsync(configKey);
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
// 清除缓存
|
|
||||||
await _cachingService.RemoveAsync($"config_{configKey}");
|
|
||||||
await _cachingService.RemoveAsync("all_configs");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ConfigExistsAsync(string configKey)
|
|
||||||
{
|
|
||||||
// 先检查缓存
|
|
||||||
var existsInCache = await _cachingService.ExistsAsync($"config_{configKey}");
|
|
||||||
if (existsInCache)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _configRepository.KeyExistsAsync(configKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<SystemConfig>> GetConfigsByCategoryAsync(string category)
|
|
||||||
{
|
|
||||||
return await _configRepository.GetByCategoryAsync(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateConfigAsync(SystemConfig config)
|
|
||||||
{
|
|
||||||
var errors = new List<string>();
|
|
||||||
|
|
||||||
// 验证配置键
|
|
||||||
if (string.IsNullOrWhiteSpace(config.ConfigKey))
|
|
||||||
{
|
|
||||||
errors.Add("Config key cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证配置值
|
|
||||||
if (config.ConfigValue == null)
|
|
||||||
{
|
|
||||||
errors.Add("Config value cannot be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据不同的配置键进行特定验证
|
|
||||||
switch (config.ConfigKey)
|
|
||||||
{
|
|
||||||
case "Database.ConnectionString":
|
|
||||||
if (!IsValidConnectionString(config.ConfigValue))
|
|
||||||
{
|
|
||||||
errors.Add("Invalid database connection string");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Logging.Level":
|
|
||||||
if (!IsValidLogLevel(config.ConfigValue))
|
|
||||||
{
|
|
||||||
errors.Add("Invalid log level");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Collection.Interval":
|
|
||||||
if (!IsValidInterval(config.ConfigValue))
|
|
||||||
{
|
|
||||||
errors.Add("Invalid collection interval");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Security.JwtSecret":
|
|
||||||
if (string.IsNullOrWhiteSpace(config.ConfigValue) || config.ConfigValue.Length < 16)
|
|
||||||
{
|
|
||||||
errors.Add("JWT secret must be at least 16 characters long");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Count == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshConfigCacheAsync()
|
|
||||||
{
|
|
||||||
// 清除所有配置缓存
|
|
||||||
await _cachingService.RemoveAsync("all_configs");
|
|
||||||
|
|
||||||
// 重新加载常用配置
|
|
||||||
var commonKeys = new[] { "Database.ConnectionString", "Logging.Level", "Collection.Interval" };
|
|
||||||
foreach (var key in commonKeys)
|
|
||||||
{
|
|
||||||
await GetConfigAsync(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<T> GetConfigValueAsync<T>(string configKey, T defaultValue = default)
|
|
||||||
{
|
|
||||||
var config = await GetConfigAsync(configKey);
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return (T)Convert.ChangeType(config.ConfigValue, typeof(T));
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidConnectionString(string connectionString)
|
|
||||||
{
|
|
||||||
// 简单的连接字符串验证
|
|
||||||
return !string.IsNullOrWhiteSpace(connectionString) &&
|
|
||||||
(connectionString.Contains("Server=") || connectionString.Contains("Host="));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidLogLevel(string logLevel)
|
|
||||||
{
|
|
||||||
var validLevels = new[] { "Trace", "Debug", "Information", "Warning", "Error", "Critical", "None" };
|
|
||||||
return Array.Exists(validLevels, level => level.Equals(logLevel, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidInterval(string interval)
|
|
||||||
{
|
|
||||||
if (int.TryParse(interval, out int seconds))
|
|
||||||
{
|
|
||||||
return seconds >= 5 && seconds <= 3600; // 5秒到1小时
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LoggingManager : ILoggingService
|
|
||||||
{
|
|
||||||
private readonly ILogRepository _logRepository;
|
|
||||||
private readonly ICachingService _cachingService;
|
|
||||||
|
|
||||||
public LoggingManager(
|
|
||||||
ILogRepository logRepository,
|
|
||||||
ICachingService cachingService)
|
|
||||||
{
|
|
||||||
_logRepository = logRepository;
|
|
||||||
_cachingService = cachingService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogAsync(LogLevel logLevel, string message, Exception exception = null, Dictionary<string, object> properties = null)
|
|
||||||
{
|
|
||||||
var logEntry = new LogEntry
|
|
||||||
{
|
|
||||||
LogLevel = logLevel,
|
|
||||||
Message = message,
|
|
||||||
ExceptionMessage = exception?.Message,
|
|
||||||
StackTrace = exception?.StackTrace,
|
|
||||||
Properties = properties,
|
|
||||||
Timestamp = DateTime.Now,
|
|
||||||
Category = "General"
|
|
||||||
};
|
|
||||||
|
|
||||||
// 异步保存到数据库
|
|
||||||
await _logRepository.AddAsync(logEntry);
|
|
||||||
|
|
||||||
// 控制台输出(开发环境)
|
|
||||||
if (IsDevelopmentEnvironment())
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[{logLevel}] {message}");
|
|
||||||
if (exception != null)
|
|
||||||
{
|
|
||||||
Console.WriteLine(exception.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogErrorAsync(string message, Exception exception = null, Dictionary<string, object> properties = null)
|
|
||||||
{
|
|
||||||
await LogAsync(LogLevel.Error, message, exception, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogWarningAsync(string message, Dictionary<string, object> properties = null)
|
|
||||||
{
|
|
||||||
await LogAsync(LogLevel.Warning, message, null, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogInfoAsync(string message, Dictionary<string, object> properties = null)
|
|
||||||
{
|
|
||||||
await LogAsync(LogLevel.Information, message, null, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogDebugAsync(string message, Dictionary<string, object> properties = null)
|
|
||||||
{
|
|
||||||
await LogAsync(LogLevel.Debug, message, null, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogTraceAsync(string message, Dictionary<string, object> properties = null)
|
|
||||||
{
|
|
||||||
await LogAsync(LogLevel.Trace, message, null, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<LogEntry>> GetLogsAsync(LogLevel? logLevel = null, DateTime? startDate = null, DateTime? endDate = null, string category = null)
|
|
||||||
{
|
|
||||||
return await _logRepository.GetLogsAsync(logLevel, startDate, endDate, category);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<LogEntry>> GetErrorLogsAsync(DateTime? startDate = null, DateTime? endDate = null)
|
|
||||||
{
|
|
||||||
return await _logRepository.GetLogsAsync(LogLevel.Error, startDate, endDate, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetLogCountAsync(LogLevel? logLevel = null, DateTime? startDate = null, DateTime? endDate = null)
|
|
||||||
{
|
|
||||||
return await _logRepository.GetLogCountAsync(logLevel, startDate, endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ArchiveLogsAsync(int daysToKeep = 30)
|
|
||||||
{
|
|
||||||
var cutoffDate = DateTime.Now.AddDays(-daysToKeep);
|
|
||||||
await _logRepository.ArchiveLogsAsync(cutoffDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ClearLogsAsync()
|
|
||||||
{
|
|
||||||
await _logRepository.ClearLogsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDevelopmentEnvironment()
|
|
||||||
{
|
|
||||||
// 简单判断是否为开发环境
|
|
||||||
return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,406 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.Template;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface ITagMappingService
|
|
||||||
{
|
|
||||||
Task<TagMapping> CreateTagMappingAsync(TagMapping mapping);
|
|
||||||
Task<TagMapping> UpdateTagMappingAsync(int mappingId, TagMapping mapping);
|
|
||||||
Task<bool> DeleteTagMappingAsync(int mappingId);
|
|
||||||
Task<TagMapping> GetTagMappingByIdAsync(int mappingId);
|
|
||||||
Task<IEnumerable<TagMapping>> GetAllTagMappingsAsync();
|
|
||||||
Task<IEnumerable<TagMapping>> GetMappingsByTemplateAsync(int templateId);
|
|
||||||
Task<TagMapping> MapDeviceTagAsync(TagData deviceTag, int templateId);
|
|
||||||
Task<Dictionary<string, TagData>> MapDeviceTagsAsync(IEnumerable<TagData> deviceTags, int templateId);
|
|
||||||
Task ValidateTagMappingAsync(TagMapping mapping);
|
|
||||||
Task<IEnumerable<TagMapping>> GetMappingsByTagIdAsync(string tagId);
|
|
||||||
Task<bool> IsTagMappedAsync(string tagId, int templateId);
|
|
||||||
Task<TagMappingResult> ValidateAndMapTagsAsync(IEnumerable<TagData> deviceTags, int templateId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TagMappingService : ITagMappingService
|
|
||||||
{
|
|
||||||
private readonly ITagMappingRepository _tagMappingRepository;
|
|
||||||
private readonly ITemplateRepository _templateRepository;
|
|
||||||
private readonly ILoggingService _loggingService;
|
|
||||||
|
|
||||||
public TagMappingService(
|
|
||||||
ITagMappingRepository tagMappingRepository,
|
|
||||||
ITemplateRepository templateRepository,
|
|
||||||
ILoggingService loggingService)
|
|
||||||
{
|
|
||||||
_tagMappingRepository = tagMappingRepository;
|
|
||||||
_templateRepository = templateRepository;
|
|
||||||
_loggingService = loggingService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TagMapping> CreateTagMappingAsync(TagMapping mapping)
|
|
||||||
{
|
|
||||||
// Validate mapping
|
|
||||||
await ValidateTagMappingAsync(mapping);
|
|
||||||
|
|
||||||
// Check if mapping already exists
|
|
||||||
if (await IsTagMappedAsync(mapping.DeviceTagId, mapping.TemplateId))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Tag {mapping.DeviceTagId} is already mapped for template {mapping.TemplateId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
mapping.MappingId = 0; // Ensure new mapping
|
|
||||||
mapping.CreatedAt = DateTime.Now;
|
|
||||||
mapping.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
await _tagMappingRepository.AddAsync(mapping);
|
|
||||||
await _tagMappingRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync($"Created tag mapping: {mapping.DeviceTagId} -> {mapping.SystemTagId} for template {mapping.TemplateId}");
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TagMapping> UpdateTagMappingAsync(int mappingId, TagMapping mapping)
|
|
||||||
{
|
|
||||||
var existingMapping = await _tagMappingRepository.GetByIdAsync(mappingId);
|
|
||||||
if (existingMapping == null)
|
|
||||||
throw new KeyNotFoundException($"Tag mapping with ID {mappingId} not found");
|
|
||||||
|
|
||||||
// Validate updated mapping
|
|
||||||
await ValidateTagMappingAsync(mapping);
|
|
||||||
|
|
||||||
existingMapping.DeviceTagId = mapping.DeviceTagId;
|
|
||||||
existingMapping.SystemTagId = mapping.SystemTagId;
|
|
||||||
existingMapping.DataType = mapping.DataType;
|
|
||||||
existingMapping.ConversionFormula = mapping.ConversionFormula;
|
|
||||||
existingMapping.Description = mapping.Description;
|
|
||||||
existingMapping.IsActive = mapping.IsActive;
|
|
||||||
existingMapping.UpdatedAt = DateTime.Now;
|
|
||||||
|
|
||||||
await _tagMappingRepository.UpdateAsync(existingMapping);
|
|
||||||
await _tagMappingRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync($"Updated tag mapping: {existingMapping.DeviceTagId} -> {existingMapping.SystemTagId}");
|
|
||||||
return existingMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteTagMappingAsync(int mappingId)
|
|
||||||
{
|
|
||||||
var mapping = await _tagMappingRepository.GetByIdAsync(mappingId);
|
|
||||||
if (mapping == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
await _tagMappingRepository.DeleteAsync(mapping);
|
|
||||||
await _tagMappingRepository.SaveAsync();
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync($"Deleted tag mapping: {mapping.DeviceTagId} -> {mapping.SystemTagId}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TagMapping> GetTagMappingByIdAsync(int mappingId)
|
|
||||||
{
|
|
||||||
return await _tagMappingRepository.GetByIdAsync(mappingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<TagMapping>> GetAllTagMappingsAsync()
|
|
||||||
{
|
|
||||||
return await _tagMappingRepository.GetAllAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<TagMapping>> GetMappingsByTemplateAsync(int templateId)
|
|
||||||
{
|
|
||||||
return await _tagMappingRepository.GetMappingsByTemplateAsync(templateId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TagMapping> MapDeviceTagAsync(TagData deviceTag, int templateId)
|
|
||||||
{
|
|
||||||
if (deviceTag == null)
|
|
||||||
throw new ArgumentNullException(nameof(deviceTag));
|
|
||||||
|
|
||||||
var template = await _templateRepository.GetByIdAsync(templateId);
|
|
||||||
if (template == null)
|
|
||||||
throw new KeyNotFoundException($"Template with ID {templateId} not found");
|
|
||||||
|
|
||||||
// Find mapping for this device tag
|
|
||||||
var mapping = await _tagMappingRepository.GetByDeviceTagAndTemplateAsync(deviceTag.Id, templateId);
|
|
||||||
if (mapping == null)
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync($"No mapping found for tag {deviceTag.Id} in template {templateId}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply mapping and conversion
|
|
||||||
var mappedTag = ApplyTagMapping(deviceTag, mapping);
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, TagData>> MapDeviceTagsAsync(IEnumerable<TagData> deviceTags, int templateId)
|
|
||||||
{
|
|
||||||
if (deviceTags == null)
|
|
||||||
throw new ArgumentNullException(nameof(deviceTags));
|
|
||||||
|
|
||||||
var template = await _templateRepository.GetByIdAsync(templateId);
|
|
||||||
if (template == null)
|
|
||||||
throw new KeyNotFoundException($"Template with ID {templateId} not found");
|
|
||||||
|
|
||||||
var mappings = await GetMappingsByTemplateAsync(templateId);
|
|
||||||
var mappingDict = mappings.ToDictionary(m => m.DeviceTagId);
|
|
||||||
|
|
||||||
var result = new Dictionary<string, TagData>();
|
|
||||||
|
|
||||||
foreach (var deviceTag in deviceTags)
|
|
||||||
{
|
|
||||||
if (mappingDict.ContainsKey(deviceTag.Id))
|
|
||||||
{
|
|
||||||
var mapping = mappingDict[deviceTag.Id];
|
|
||||||
var mappedTag = ApplyTagMapping(deviceTag, mapping);
|
|
||||||
result[mapping.SystemTagId] = mappedTag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _loggingService.LogInformationAsync($"Mapped {result.Count} tags from {deviceTags.Count()} device tags for template {templateId}");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ValidateTagMappingAsync(TagMapping mapping)
|
|
||||||
{
|
|
||||||
if (mapping == null)
|
|
||||||
throw new ArgumentNullException(nameof(mapping));
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mapping.DeviceTagId))
|
|
||||||
throw new ArgumentException("Device tag ID is required");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mapping.SystemTagId))
|
|
||||||
throw new ArgumentException("System tag ID is required");
|
|
||||||
|
|
||||||
// Validate template exists
|
|
||||||
var template = await _templateRepository.GetByIdAsync(mapping.TemplateId);
|
|
||||||
if (template == null)
|
|
||||||
throw new KeyNotFoundException($"Template with ID {mapping.TemplateId} not found");
|
|
||||||
|
|
||||||
// Validate data type
|
|
||||||
if (!IsValidDataType(mapping.DataType))
|
|
||||||
throw new ArgumentException($"Invalid data type: {mapping.DataType}");
|
|
||||||
|
|
||||||
// Check for duplicate mappings
|
|
||||||
var existingMapping = await _tagMappingRepository.GetByDeviceTagAndTemplateAsync(mapping.DeviceTagId, mapping.TemplateId);
|
|
||||||
if (existingMapping != null && existingMapping.MappingId != mapping.MappingId)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Duplicate mapping found for tag {mapping.DeviceTagId} in template {mapping.TemplateId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<TagMapping>> GetMappingsByTagIdAsync(string tagId)
|
|
||||||
{
|
|
||||||
return await _tagMappingRepository.GetMappingsByDeviceTagAsync(tagId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsTagMappedAsync(string tagId, int templateId)
|
|
||||||
{
|
|
||||||
var mapping = await _tagMappingRepository.GetByDeviceTagAndTemplateAsync(tagId, templateId);
|
|
||||||
return mapping != null && mapping.IsActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TagMappingResult> ValidateAndMapTagsAsync(IEnumerable<TagData> deviceTags, int templateId)
|
|
||||||
{
|
|
||||||
var result = new TagMappingResult
|
|
||||||
{
|
|
||||||
TemplateId = templateId,
|
|
||||||
TotalDeviceTags = deviceTags.Count(),
|
|
||||||
MappedTags = 0,
|
|
||||||
UnmappedTags = new List<string>(),
|
|
||||||
ConversionErrors = new List<string>(),
|
|
||||||
MappedData = new Dictionary<string, TagData>()
|
|
||||||
};
|
|
||||||
|
|
||||||
var template = await _templateRepository.GetByIdAsync(templateId);
|
|
||||||
if (template == null)
|
|
||||||
{
|
|
||||||
result.ConversionErrors.Add($"Template with ID {templateId} not found");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mappings = await GetMappingsByTemplateAsync(templateId);
|
|
||||||
var mappingDict = mappings.ToDictionary(m => m.DeviceTagId);
|
|
||||||
|
|
||||||
foreach (var deviceTag in deviceTags)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (mappingDict.ContainsKey(deviceTag.Id))
|
|
||||||
{
|
|
||||||
var mapping = mappingDict[deviceTag.Id];
|
|
||||||
if (mapping.IsActive)
|
|
||||||
{
|
|
||||||
var mappedTag = ApplyTagMapping(deviceTag, mapping);
|
|
||||||
result.MappedData[mapping.SystemTagId] = mappedTag;
|
|
||||||
result.MappedTags++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.UnmappedTags.Add(deviceTag.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.UnmappedTags.Add(deviceTag.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.ConversionErrors.Add($"Failed to map tag {deviceTag.Id}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _loggingService.LogInformationAsync($"Tag mapping validation: {result.MappedTags}/{result.TotalDeviceTags} tags mapped for template {templateId}");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TagData ApplyTagMapping(TagData deviceTag, TagMapping mapping)
|
|
||||||
{
|
|
||||||
var mappedTag = new TagData
|
|
||||||
{
|
|
||||||
Id = mapping.SystemTagId,
|
|
||||||
Desc = mapping.Description ?? deviceTag.Desc,
|
|
||||||
Quality = deviceTag.Quality,
|
|
||||||
Time = deviceTag.Time
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply data type conversion
|
|
||||||
mappedTag.Value = ConvertTagValue(deviceTag.Value, mapping.DataType, mapping.ConversionFormula);
|
|
||||||
|
|
||||||
return mappedTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
private object ConvertTagValue(object value, string dataType, string conversionFormula)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Apply conversion formula if provided
|
|
||||||
if (!string.IsNullOrEmpty(conversionFormula))
|
|
||||||
{
|
|
||||||
value = ApplyConversionFormula(value, conversionFormula);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to target data type
|
|
||||||
switch (dataType.ToLower())
|
|
||||||
{
|
|
||||||
case "int":
|
|
||||||
if (int.TryParse(value.ToString(), out int intValue))
|
|
||||||
return intValue;
|
|
||||||
throw new ArgumentException($"Cannot convert {value} to int");
|
|
||||||
|
|
||||||
case "decimal":
|
|
||||||
if (decimal.TryParse(value.ToString(), out decimal decimalValue))
|
|
||||||
return decimalValue;
|
|
||||||
throw new ArgumentException($"Cannot convert {value} to decimal");
|
|
||||||
|
|
||||||
case "bool":
|
|
||||||
if (bool.TryParse(value.ToString(), out bool boolValue))
|
|
||||||
return boolValue;
|
|
||||||
return Convert.ToBoolean(value);
|
|
||||||
|
|
||||||
case "string":
|
|
||||||
return value.ToString();
|
|
||||||
|
|
||||||
case "datetime":
|
|
||||||
if (DateTime.TryParse(value.ToString(), out DateTime dateTimeValue))
|
|
||||||
return dateTimeValue;
|
|
||||||
throw new ArgumentException($"Cannot convert {value} to datetime");
|
|
||||||
|
|
||||||
default:
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Failed to convert tag value: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private object ApplyConversionFormula(object value, string formula)
|
|
||||||
{
|
|
||||||
// Simple formula evaluation (in real implementation, use expression parser)
|
|
||||||
if (value == null || string.IsNullOrEmpty(formula))
|
|
||||||
return value;
|
|
||||||
|
|
||||||
var valueStr = value.ToString();
|
|
||||||
|
|
||||||
// Handle basic arithmetic operations
|
|
||||||
if (formula.Contains("*"))
|
|
||||||
{
|
|
||||||
var parts = formula.Split('*');
|
|
||||||
if (parts.Length == 2 && double.TryParse(parts[1], out double factor))
|
|
||||||
{
|
|
||||||
if (double.TryParse(valueStr, out double numericValue))
|
|
||||||
return numericValue * factor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (formula.Contains("/"))
|
|
||||||
{
|
|
||||||
var parts = formula.Split('/');
|
|
||||||
if (parts.Length == 2 && double.TryParse(parts[1], out double divisor))
|
|
||||||
{
|
|
||||||
if (double.TryParse(valueStr, out double numericValue) && divisor != 0)
|
|
||||||
return numericValue / divisor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (formula.Contains("+"))
|
|
||||||
{
|
|
||||||
var parts = formula.Split('+');
|
|
||||||
if (parts.Length == 2 && double.TryParse(parts[1], out double offset))
|
|
||||||
{
|
|
||||||
if (double.TryParse(valueStr, out double numericValue))
|
|
||||||
return numericValue + offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (formula.Contains("-"))
|
|
||||||
{
|
|
||||||
var parts = formula.Split('-');
|
|
||||||
if (parts.Length == 2 && double.TryParse(parts[1], out double offset))
|
|
||||||
{
|
|
||||||
if (double.TryParse(valueStr, out double numericValue))
|
|
||||||
return numericValue - offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidDataType(string dataType)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(dataType))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var validTypes = new[] { "int", "decimal", "bool", "string", "datetime", "double" };
|
|
||||||
return validTypes.Contains(dataType.ToLower());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supporting classes and interfaces
|
|
||||||
public class TagMappingResult
|
|
||||||
{
|
|
||||||
public int TemplateId { get; set; }
|
|
||||||
public int TotalDeviceTags { get; set; }
|
|
||||||
public int MappedTags { get; set; }
|
|
||||||
public List<string> UnmappedTags { get; set; }
|
|
||||||
public List<string> ConversionErrors { get; set; }
|
|
||||||
public Dictionary<string, TagData> MappedData { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional repository interface for tag mappings
|
|
||||||
public interface ITagMappingRepository : IRepository<TagMapping>
|
|
||||||
{
|
|
||||||
Task<IEnumerable<TagMapping>> GetMappingsByTemplateAsync(int templateId);
|
|
||||||
Task<TagMapping> GetByDeviceTagAndTemplateAsync(string deviceTagId, int templateId);
|
|
||||||
Task<IEnumerable<TagMapping>> GetMappingsByDeviceTagAsync(string deviceTagId);
|
|
||||||
Task<IEnumerable<TagMapping>> GetActiveMappingsAsync();
|
|
||||||
Task<bool> DeviceTagExistsAsync(string deviceTagId, int templateId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,675 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Haoliang.Models.Template;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
|
|
||||||
namespace Haoliang.Core.Services
|
|
||||||
{
|
|
||||||
public interface ITemplateValidationService
|
|
||||||
{
|
|
||||||
Task<bool> ValidateTemplateStructureAsync(CNCBrandTemplate template);
|
|
||||||
Task<bool> ValidateTagMappingsAsync(CNCBrandTemplate template);
|
|
||||||
Task<bool> ValidateDataParsingRulesAsync(CNCBrandTemplate template);
|
|
||||||
Task<IEnumerable<ValidationError>> ValidateTemplateForDeviceAsync(int templateId, int deviceId);
|
|
||||||
Task<bool> TestTemplateDataParsingAsync(CNCBrandTemplate template, string sampleData);
|
|
||||||
Task<IEnumerable<string>> GetMissingRequiredTagsAsync(CNCBrandTemplate template);
|
|
||||||
Task<ValidationReport> ValidateTemplateComprehensivelyAsync(CNCBrandTemplate template);
|
|
||||||
Task<IEnumerable<TagValidationResult>> ValidateDeviceDataAsync(IEnumerable<TagData> deviceTags, int templateId);
|
|
||||||
Task<bool> ValidateTemplateCompatibilityAsync(CNCBrandTemplate template1, CNCBrandTemplate template2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TemplateValidationService : ITemplateValidationService
|
|
||||||
{
|
|
||||||
private readonly ITemplateRepository _templateRepository;
|
|
||||||
private readonly ITagMappingRepository _tagMappingRepository;
|
|
||||||
private readonly ILoggingService _loggingService;
|
|
||||||
|
|
||||||
public TemplateValidationService(
|
|
||||||
ITemplateRepository templateRepository,
|
|
||||||
ITagMappingRepository tagMappingRepository,
|
|
||||||
ILoggingService loggingService)
|
|
||||||
{
|
|
||||||
_templateRepository = templateRepository;
|
|
||||||
_tagMappingRepository = tagMappingRepository;
|
|
||||||
_loggingService = loggingService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateTemplateStructureAsync(CNCBrandTemplate template)
|
|
||||||
{
|
|
||||||
var errors = new List<ValidationError>();
|
|
||||||
|
|
||||||
// Check basic structure
|
|
||||||
if (template == null)
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = "template", Message = "Template cannot be null" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(template.TemplateName))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = "templateName", Message = "Template name is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(template.BrandName))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = "brandName", Message = "Brand name is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (template.Tags == null || !template.Tags.Any())
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = "tags", Message = "At least one tag must be defined" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate tag structure
|
|
||||||
if (template.Tags != null)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < template.Tags.Count; i++)
|
|
||||||
{
|
|
||||||
var tag = template.Tags[i];
|
|
||||||
var tagErrors = ValidateTagStructure(tag, $"tags[{i}]");
|
|
||||||
errors.AddRange(tagErrors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate data processing rules
|
|
||||||
if (template.DataProcessingRules != null)
|
|
||||||
{
|
|
||||||
foreach (var rule in template.DataProcessingRules)
|
|
||||||
{
|
|
||||||
var ruleErrors = ValidateDataProcessingRule(rule);
|
|
||||||
errors.AddRange(ruleErrors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log validation results
|
|
||||||
if (errors.Any())
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync($"Template structure validation failed with {errors.Count} errors");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _loggingService.LogInfoAsync("Template structure validation passed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return !errors.Any();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateTagMappingsAsync(CNCBrandTemplate template)
|
|
||||||
{
|
|
||||||
var errors = new List<ValidationError>();
|
|
||||||
|
|
||||||
if (template.Tags == null || !template.Tags.Any())
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = "tags", Message = "No tags defined to validate" });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mappings = await _tagMappingRepository.GetMappingsByTemplateAsync(template.TemplateId);
|
|
||||||
var mappingDict = mappings.ToDictionary(m => m.DeviceTagId);
|
|
||||||
|
|
||||||
foreach (var tag in template.Tags)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(tag.DeviceTagId))
|
|
||||||
{
|
|
||||||
// Check if mapping exists for device tag
|
|
||||||
if (!mappingDict.ContainsKey(tag.DeviceTagId))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError
|
|
||||||
{
|
|
||||||
Field = $"tags[{tag.SystemTagId}].deviceTagId",
|
|
||||||
Message = $"No mapping found for device tag '{tag.DeviceTagId}'"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var mapping = mappingDict[tag.DeviceTagId];
|
|
||||||
if (!mapping.IsActive)
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError
|
|
||||||
{
|
|
||||||
Field = $"tags[{tag.SystemTagId}].deviceTagId",
|
|
||||||
Message = $"Mapping for device tag '{tag.DeviceTagId}' is inactive"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !errors.Any();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateDataParsingRulesAsync(CNCBrandTemplate template)
|
|
||||||
{
|
|
||||||
var errors = new List<ValidationError>();
|
|
||||||
|
|
||||||
if (template.DataProcessingRules == null || !template.DataProcessingRules.Any())
|
|
||||||
{
|
|
||||||
return true; // No rules to validate
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var rule in template.DataProcessingRules)
|
|
||||||
{
|
|
||||||
var ruleErrors = ValidateDataProcessingRule(rule);
|
|
||||||
errors.AddRange(ruleErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !errors.Any();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<ValidationError>> ValidateTemplateForDeviceAsync(int templateId, int deviceId)
|
|
||||||
{
|
|
||||||
var errors = new List<ValidationError>();
|
|
||||||
|
|
||||||
var template = await _templateRepository.GetByIdAsync(templateId);
|
|
||||||
if (template == null)
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = "template", Message = "Template not found" });
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
var device = await _templateRepository.GetDeviceByIdAsync(deviceId); // Assuming this method exists
|
|
||||||
if (device == null)
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = "device", Message = "Device not found" });
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if template is compatible with device brand
|
|
||||||
if (template.BrandName != device.DeviceBrand)
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError
|
|
||||||
{
|
|
||||||
Field = "template.brand",
|
|
||||||
Message = $"Template brand '{template.BrandName}' does not match device brand '{device.DeviceBrand}'"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for missing required tags
|
|
||||||
var missingTags = await GetMissingRequiredTagsAsync(template);
|
|
||||||
if (missingTags.Any())
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError
|
|
||||||
{
|
|
||||||
Field = "tags",
|
|
||||||
Message = $"Missing required tags: {string.Join(", ", missingTags)}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> TestTemplateDataParsingAsync(CNCBrandTemplate template, string sampleData)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Parse sample data
|
|
||||||
var document = JsonDocument.Parse(sampleData);
|
|
||||||
var root = document.RootElement;
|
|
||||||
|
|
||||||
// Extract device data
|
|
||||||
var deviceTags = new List<TagData>();
|
|
||||||
|
|
||||||
if (root.TryGetProperty("tags", out var tagsElement))
|
|
||||||
{
|
|
||||||
foreach (var tagElement in tagsElement.EnumerateArray())
|
|
||||||
{
|
|
||||||
var tag = new TagData
|
|
||||||
{
|
|
||||||
Id = tagElement.GetProperty("id").GetString(),
|
|
||||||
Desc = tagElement.GetProperty("desc").GetString(),
|
|
||||||
Quality = tagElement.GetProperty("quality").GetString(),
|
|
||||||
Time = DateTime.Parse(tagElement.GetProperty("time").GetString())
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tagElement.TryGetProperty("value", out var valueElement))
|
|
||||||
{
|
|
||||||
tag.Value = ParseTagValue(valueElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceTags.Add(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test tag mapping
|
|
||||||
var mappingResult = await ValidateDeviceDataAsync(deviceTags, template.TemplateId);
|
|
||||||
if (mappingResult.Any(r => !r.IsValid))
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync($"Template data parsing test failed with {mappingResult.Count(r => !r.IsValid)} validation errors");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test data processing rules
|
|
||||||
if (template.DataProcessingRules != null)
|
|
||||||
{
|
|
||||||
foreach (var rule in template.DataProcessingRules)
|
|
||||||
{
|
|
||||||
if (!await ExecuteDataProcessingRuleAsync(rule, deviceTags))
|
|
||||||
{
|
|
||||||
await _loggingService.LogWarningAsync($"Data processing rule '{rule.RuleName}' failed during test");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync("Template data parsing test passed");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"Template data parsing test failed: {ex.Message}", ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetMissingRequiredTagsAsync(CNCBrandTemplate template)
|
|
||||||
{
|
|
||||||
var missingTags = new List<string>();
|
|
||||||
|
|
||||||
if (template == null || template.Tags == null)
|
|
||||||
return missingTags;
|
|
||||||
|
|
||||||
var requiredTags = template.Tags
|
|
||||||
.Where(t => t.IsRequired)
|
|
||||||
.Select(t => t.DeviceTagId)
|
|
||||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (!requiredTags.Any())
|
|
||||||
return missingTags;
|
|
||||||
|
|
||||||
var mappings = await _tagMappingRepository.GetMappingsByTemplateAsync(template.TemplateId);
|
|
||||||
var mappedTags = mappings
|
|
||||||
.Where(m => m.IsActive)
|
|
||||||
.Select(m => m.DeviceTagId)
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
foreach (var requiredTag in requiredTags)
|
|
||||||
{
|
|
||||||
if (!mappedTags.Contains(requiredTag))
|
|
||||||
{
|
|
||||||
missingTags.Add(requiredTag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return missingTags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ValidationReport> ValidateTemplateComprehensivelyAsync(CNCBrandTemplate template)
|
|
||||||
{
|
|
||||||
var report = new ValidationReport
|
|
||||||
{
|
|
||||||
TemplateId = template.TemplateId,
|
|
||||||
TemplateName = template.TemplateName,
|
|
||||||
ValidationTime = DateTime.Now,
|
|
||||||
Checks = new List<ValidationCheck>
|
|
||||||
{
|
|
||||||
new ValidationCheck
|
|
||||||
{
|
|
||||||
Name = "Structure Validation",
|
|
||||||
Passed = await ValidateTemplateStructureAsync(template),
|
|
||||||
Errors = new List<ValidationError>()
|
|
||||||
},
|
|
||||||
new ValidationCheck
|
|
||||||
{
|
|
||||||
Name = "Tag Mapping Validation",
|
|
||||||
Passed = await ValidateTagMappingsAsync(template),
|
|
||||||
Errors = new List<ValidationError>()
|
|
||||||
},
|
|
||||||
new ValidationCheck
|
|
||||||
{
|
|
||||||
Name = "Data Parsing Rules Validation",
|
|
||||||
Passed = await ValidateDataProcessingRulesAsync(template),
|
|
||||||
Errors = new List<ValidationError>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Aggregate all errors
|
|
||||||
report.HasErrors = report.Checks.Any(c => !c.Passed);
|
|
||||||
report.TotalChecks = report.Checks.Count;
|
|
||||||
report.PassedChecks = report.Checks.Count(c => c.Passed);
|
|
||||||
report.FailedChecks = report.Checks.Count(c => !c.Passed);
|
|
||||||
|
|
||||||
await _loggingService.LogInformationAsync($"Comprehensive template validation: {report.PassedChecks}/{report.TotalChecks} checks passed");
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<TagValidationResult>> ValidateDeviceDataAsync(IEnumerable<TagData> deviceTags, int templateId)
|
|
||||||
{
|
|
||||||
var results = new List<TagValidationResult>();
|
|
||||||
var template = await _templateRepository.GetByIdAsync(templateId);
|
|
||||||
|
|
||||||
if (template == null)
|
|
||||||
{
|
|
||||||
results.Add(new TagValidationResult
|
|
||||||
{
|
|
||||||
SystemTagId = "template",
|
|
||||||
IsValid = false,
|
|
||||||
ErrorMessage = "Template not found"
|
|
||||||
});
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
var templateTags = template.Tags ?? new List<TagTemplate>();
|
|
||||||
var mappings = await _tagMappingRepository.GetMappingsByTemplateAsync(templateId);
|
|
||||||
var mappingDict = mappings.ToDictionary(m => m.DeviceTagId);
|
|
||||||
|
|
||||||
foreach (var templateTag in templateTags)
|
|
||||||
{
|
|
||||||
var result = new TagValidationResult
|
|
||||||
{
|
|
||||||
SystemTagId = templateTag.SystemTagId,
|
|
||||||
ExpectedDataType = templateTag.DataType,
|
|
||||||
IsRequired = templateTag.IsRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(templateTag.DeviceTagId))
|
|
||||||
{
|
|
||||||
// Find corresponding device tag
|
|
||||||
var deviceTag = deviceTags.FirstOrDefault(t => t.Id == templateTag.DeviceTagId);
|
|
||||||
|
|
||||||
if (deviceTag == null)
|
|
||||||
{
|
|
||||||
if (templateTag.IsRequired)
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = $"Required device tag '{templateTag.DeviceTagId}' not found";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.IsValid = true;
|
|
||||||
result.Message = "Optional tag not present";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Validate device tag
|
|
||||||
result.DeviceTagId = templateTag.DeviceTagId;
|
|
||||||
result.DeviceValue = deviceTag.Value;
|
|
||||||
result.Quality = deviceTag.Quality;
|
|
||||||
|
|
||||||
if (!ValidateTagValue(deviceTag, templateTag))
|
|
||||||
{
|
|
||||||
result.IsValid = false;
|
|
||||||
result.ErrorMessage = $"Tag value validation failed for '{templateTag.DeviceTagId}'";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.IsValid = true;
|
|
||||||
result.MappedValue = ApplyTagMapping(deviceTag, mappingDict[templateTag.DeviceTagId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.IsValid = true;
|
|
||||||
result.Message = "No device tag mapping defined";
|
|
||||||
}
|
|
||||||
|
|
||||||
results.Add(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateTemplateCompatibilityAsync(CNCBrandTemplate template1, CNCBrandTemplate template2)
|
|
||||||
{
|
|
||||||
if (template1 == null || template2 == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (template1.TemplateId == template2.TemplateId)
|
|
||||||
return true; // Same template is always compatible
|
|
||||||
|
|
||||||
// Check if same brand
|
|
||||||
if (template1.BrandName != template2.BrandName)
|
|
||||||
{
|
|
||||||
await _loggingService.LogInfoAsync($"Templates have different brands: {template1.BrandName} vs {template2.BrandName}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check tag compatibility
|
|
||||||
var tags1 = template1.Tags ?? new List<TagTemplate>();
|
|
||||||
var tags2 = template2.Tags ?? new List<TagTemplate>();
|
|
||||||
|
|
||||||
// Count common tags
|
|
||||||
var commonTags = tags1.Intersect(tags2, new TagTemplateComparer());
|
|
||||||
var compatibilityScore = (double)commonTags.Count() / Math.Max(tags1.Count, tags2.Count);
|
|
||||||
|
|
||||||
await _loggingService.LogInfoAsync($"Template compatibility score: {compatibilityScore:P1}");
|
|
||||||
return compatibilityScore >= 0.8; // 80% compatibility threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ValidationError> ValidateTagStructure(TagTemplate tag, string fieldPath)
|
|
||||||
{
|
|
||||||
var errors = new List<ValidationError>();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(tag.SystemTagId))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"{fieldPath}.systemTagId", Message = "System tag ID is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tag.DeviceTagId))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(tag.DataType))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"{fieldPath}.dataType", Message = "Data type is required when device tag ID is specified" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tag.DataType))
|
|
||||||
{
|
|
||||||
var validTypes = new[] { "int", "decimal", "bool", "string", "datetime", "double" };
|
|
||||||
if (!validTypes.Contains(tag.DataType.ToLower()))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"{fieldPath}.dataType", Message = $"Invalid data type: {tag.DataType}" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate regex pattern if provided
|
|
||||||
if (!string.IsNullOrWhiteSpace(tag.ValidationRegex))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Regex.IsMatch("test", tag.ValidationRegex); // Test if regex is valid
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"{fieldPath}.validationRegex", Message = "Invalid regular expression pattern" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ValidationError> ValidateDataProcessingRule(DataProcessingRule rule)
|
|
||||||
{
|
|
||||||
var errors = new List<ValidationError>();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.RuleName))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"dataProcessingRules[{rule.RuleName}].ruleName", Message = "Rule name is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.Condition))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"dataProcessingRules[{rule.RuleName}].condition", Message = "Condition is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rule.Action))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"dataProcessingRules[{rule.RuleName}].action", Message = "Action is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate condition syntax (simple check)
|
|
||||||
if (!string.IsNullOrWhiteSpace(rule.Condition) && !IsValidCondition(rule.Condition))
|
|
||||||
{
|
|
||||||
errors.Add(new ValidationError { Field = $"dataProcessingRules[{rule.RuleName}].condition", Message = "Invalid condition syntax" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidCondition(string condition)
|
|
||||||
{
|
|
||||||
// Simple condition validation (in real implementation, use expression parser)
|
|
||||||
var validOperators = new[] { ">", "<", ">=", "<=", "==", "!=", "&&", "||", "(", ")", "true", "false" };
|
|
||||||
var parts = condition.Split(' ');
|
|
||||||
|
|
||||||
foreach (var part in parts)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(part) && !validOperators.Contains(part) && !double.TryParse(part, out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private object ParseTagValue(JsonElement valueElement)
|
|
||||||
{
|
|
||||||
if (valueElement.ValueKind == JsonValueKind.String)
|
|
||||||
{
|
|
||||||
return valueElement.GetString();
|
|
||||||
}
|
|
||||||
else if (valueElement.ValueKind == JsonValueKind.Number)
|
|
||||||
{
|
|
||||||
if (valueElement.TryGetInt32(out int intValue))
|
|
||||||
return intValue;
|
|
||||||
else if (valueElement.TryGetDecimal(out decimal decimalValue))
|
|
||||||
return decimalValue;
|
|
||||||
else
|
|
||||||
return valueElement.GetDouble();
|
|
||||||
}
|
|
||||||
else if (valueElement.ValueKind == JsonValueKind.True || valueElement.ValueKind == JsonValueKind.False)
|
|
||||||
{
|
|
||||||
return valueElement.GetBoolean();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return valueElement.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateTagValue(TagData deviceTag, TagTemplate templateTag)
|
|
||||||
{
|
|
||||||
if (deviceTag.Value == null)
|
|
||||||
return !templateTag.IsRequired;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Validate data type
|
|
||||||
switch (templateTag.DataType.ToLower())
|
|
||||||
{
|
|
||||||
case "int":
|
|
||||||
return int.TryParse(deviceTag.Value.ToString(), out _);
|
|
||||||
case "decimal":
|
|
||||||
return decimal.TryParse(deviceTag.Value.ToString(), out _);
|
|
||||||
case "bool":
|
|
||||||
return bool.TryParse(deviceTag.Value.ToString(), out _);
|
|
||||||
case "datetime":
|
|
||||||
return DateTime.TryParse(deviceTag.Value.ToString(), out _);
|
|
||||||
default:
|
|
||||||
return true; // String type accepts any value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TagData ApplyTagMapping(TagData deviceTag, TagMapping mapping)
|
|
||||||
{
|
|
||||||
return new TagData
|
|
||||||
{
|
|
||||||
Id = mapping.SystemTagId,
|
|
||||||
Desc = mapping.Description ?? deviceTag.Desc,
|
|
||||||
Quality = deviceTag.Quality,
|
|
||||||
Time = deviceTag.Time,
|
|
||||||
Value = ConvertTagValue(deviceTag.Value, mapping.DataType, mapping.ConversionFormula)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> ExecuteDataProcessingRuleAsync(DataProcessingRule rule, IEnumerable<TagData> deviceTags)
|
|
||||||
{
|
|
||||||
// Simple rule execution (in real implementation, use expression parser)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// This is a simplified implementation
|
|
||||||
// In a real system, you would parse and execute the condition and action
|
|
||||||
|
|
||||||
if (rule.Condition.Contains("temperature") && deviceTags.Any(t => t.Id == "temperature"))
|
|
||||||
{
|
|
||||||
var tempTag = deviceTags.First(t => t.Id == "temperature");
|
|
||||||
var tempValue = Convert.ToDouble(tempTag.Value);
|
|
||||||
|
|
||||||
if (tempValue > 100 && rule.Action.Contains("alert"))
|
|
||||||
{
|
|
||||||
return true; // Rule executed successfully
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await _loggingService.LogErrorAsync($"Failed to execute data processing rule '{rule.RuleName}': {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supporting classes and interfaces
|
|
||||||
public class ValidationReport
|
|
||||||
{
|
|
||||||
public int TemplateId { get; set; }
|
|
||||||
public string TemplateName { get; set; }
|
|
||||||
public DateTime ValidationTime { get; set; }
|
|
||||||
public List<ValidationCheck> Checks { get; set; }
|
|
||||||
public bool HasErrors { get; set; }
|
|
||||||
public int TotalChecks { get; set; }
|
|
||||||
public int PassedChecks { get; set; }
|
|
||||||
public int FailedChecks { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ValidationCheck
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public bool Passed { get; set; }
|
|
||||||
public List<ValidationError> Errors { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TagValidationResult
|
|
||||||
{
|
|
||||||
public string SystemTagId { get; set; }
|
|
||||||
public string DeviceTagId { get; set; }
|
|
||||||
public object DeviceValue { get; set; }
|
|
||||||
public object MappedValue { get; set; }
|
|
||||||
public string ExpectedDataType { get; set; }
|
|
||||||
public string Quality { get; set; }
|
|
||||||
public bool IsRequired { get; set; }
|
|
||||||
public bool IsValid { get; set; }
|
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TagTemplateComparer : IEqualityComparer<TagTemplate>
|
|
||||||
{
|
|
||||||
public bool Equals(TagTemplate x, TagTemplate y)
|
|
||||||
{
|
|
||||||
return x?.SystemTagId == y?.SystemTagId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetHashCode(TagTemplate obj)
|
|
||||||
{
|
|
||||||
return obj?.SystemTagId?.GetHashCode() ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
namespace Haoliang.Models.Device
|
||||||
|
{
|
||||||
|
public class DeviceData
|
||||||
|
{
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public string DeviceCode { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
public string NCProgram { get; set; }
|
||||||
|
public int CumulativeCount { get; set; }
|
||||||
|
public string OperatingMode { get; set; }
|
||||||
|
public List<TagData> Tags { get; set; }
|
||||||
|
public DateTime CollectionTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MappedTag
|
||||||
|
{
|
||||||
|
public string TagId { get; set; }
|
||||||
|
public string TagName { get; set; }
|
||||||
|
public object Value { get; set; }
|
||||||
|
public string Quality { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Haoliang.Models.Production
|
||||||
|
{
|
||||||
|
public class ProductionData
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public string DeviceCode { get; set; }
|
||||||
|
public string ProgramName { get; set; }
|
||||||
|
public int OutputCount { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
public string Operator { get; set; }
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
namespace Haoliang.Models.Device
|
||||||
|
{
|
||||||
|
public enum DeviceState
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Offline = 1,
|
||||||
|
Idle = 2,
|
||||||
|
Running = 3,
|
||||||
|
Alarm = 4,
|
||||||
|
Maintenance = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceStateTransitionResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public DeviceState PreviousState { get; set; }
|
||||||
|
public DeviceState CurrentState { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public DateTime TransitionTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceEvent
|
||||||
|
{
|
||||||
|
public int EventId { get; set; }
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public string EventType { get; set; }
|
||||||
|
public string EventDescription { get; set; }
|
||||||
|
public DateTime EventTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceStateHistory
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public DeviceState State { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
public string Reason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceStateStatistics
|
||||||
|
{
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public TimeSpan TotalRunningTime { get; set; }
|
||||||
|
public TimeSpan TotalIdleTime { get; set; }
|
||||||
|
public TimeSpan TotalOfflineTime { get; set; }
|
||||||
|
public TimeSpan TotalAlarmTime { get; set; }
|
||||||
|
public DateTime CalculatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StateAction
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Start = 1,
|
||||||
|
Stop = 2,
|
||||||
|
Pause = 3,
|
||||||
|
Resume = 4,
|
||||||
|
Reset = 5,
|
||||||
|
Alarm = 6
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Haoliang.Models.System
|
||||||
|
{
|
||||||
|
public class BusinessRuleConfig
|
||||||
|
{
|
||||||
|
public int RuleId { get; set; }
|
||||||
|
public string RuleName { get; set; }
|
||||||
|
public string RuleType { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public string Configuration { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StatisticsRuleConfig
|
||||||
|
{
|
||||||
|
public int ConfigId { get; set; }
|
||||||
|
public string ConfigName { get; set; }
|
||||||
|
public string ConfigType { get; set; }
|
||||||
|
public string Formula { get; set; }
|
||||||
|
public string GroupBy { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SystemConfiguration
|
||||||
|
{
|
||||||
|
public int ConfigId { get; set; }
|
||||||
|
public string ConfigKey { get; set; }
|
||||||
|
public string ConfigValue { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RuleExecutionHistory
|
||||||
|
{
|
||||||
|
public int HistoryId { get; set; }
|
||||||
|
public int RuleId { get; set; }
|
||||||
|
public DateTime ExecutionTime { get; set; }
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Result { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Haoliang.Models.System
|
||||||
|
{
|
||||||
|
public enum NotificationChannelType
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Email = 1,
|
||||||
|
Sms = 2,
|
||||||
|
WeChat = 3,
|
||||||
|
Webhook = 4,
|
||||||
|
DingTalk = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationChannel
|
||||||
|
{
|
||||||
|
public int ChannelId { get; set; }
|
||||||
|
public string ChannelType { get; set; }
|
||||||
|
public string Recipient { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public string Configuration { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Haoliang.Models.Template
|
||||||
|
{
|
||||||
|
public class DataProcessingRule
|
||||||
|
{
|
||||||
|
public int RuleId { get; set; }
|
||||||
|
public string RuleName { get; set; }
|
||||||
|
public string RuleType { get; set; }
|
||||||
|
public string Expression { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
namespace Haoliang.Models.Template
|
||||||
|
{
|
||||||
|
public class TagTemplate
|
||||||
|
{
|
||||||
|
public int TemplateId { get; set; }
|
||||||
|
public string SystemTagId { get; set; }
|
||||||
|
public string DeviceTagPattern { get; set; }
|
||||||
|
public string DataType { get; set; }
|
||||||
|
public string ConversionRule { get; set; }
|
||||||
|
public bool IsRequired { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TemplateConfiguration
|
||||||
|
{
|
||||||
|
public int ConfigurationId { get; set; }
|
||||||
|
public int TemplateId { get; set; }
|
||||||
|
public string Configuration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,460 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using Moq;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Models.Models.Device;
|
|
||||||
using Haoliang.Models.Models.Production;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests.Services
|
|
||||||
{
|
|
||||||
public class CacheServiceTests
|
|
||||||
{
|
|
||||||
private readonly Mock<IMemoryCache> _mockMemoryCache;
|
|
||||||
private readonly Mock<IProductionRepository> _mockProductionRepository;
|
|
||||||
private readonly Mock<IDeviceRepository> _mockDeviceRepository;
|
|
||||||
private readonly Mock<ICollectionRepository> _mockCollectionRepository;
|
|
||||||
private readonly CacheService _cacheService;
|
|
||||||
|
|
||||||
public CacheServiceTests()
|
|
||||||
{
|
|
||||||
_mockMemoryCache = new Mock<IMemoryCache>();
|
|
||||||
_mockProductionRepository = new Mock<IProductionRepository>();
|
|
||||||
_mockDeviceRepository = new Mock<IDeviceRepository>();
|
|
||||||
_mockCollectionRepository = new Mock<ICollectionRepository>();
|
|
||||||
|
|
||||||
_cacheService = new CacheService(_mockMemoryCache.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetOrSetAsync_ValueNotInCache_ExecutesFactoryAndCachesResult()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
var factoryResult = "test-value";
|
|
||||||
|
|
||||||
var factoryMock = new Mock<Func<Task<string>>>();
|
|
||||||
factoryMock.Setup(f => f()).ReturnsAsync(factoryResult);
|
|
||||||
|
|
||||||
// Setup cache miss
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Setup cache set
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Set(
|
|
||||||
key,
|
|
||||||
factoryResult,
|
|
||||||
It.IsAny<MemoryCacheEntryOptions>()))
|
|
||||||
.Callback<string, object, MemoryCacheEntryOptions>((k, v, o) => { });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _cacheService.GetOrSetAsync(key, factoryMock.Object);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(factoryResult, result);
|
|
||||||
factoryMock.Verify(f => f(), Times.Once);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Set(
|
|
||||||
key,
|
|
||||||
factoryResult,
|
|
||||||
It.IsAny<MemoryCacheEntryOptions>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetOrSetAsync_ValueInCache_ReturnsCachedValue()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
var cachedValue = "cached-value";
|
|
||||||
|
|
||||||
var factoryMock = new Mock<Func<Task<string>>>();
|
|
||||||
factoryMock.Setup(f => f()).ReturnsAsync("new-value");
|
|
||||||
|
|
||||||
// Setup cache hit
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
||||||
.Callback<string, out string>((k, out string v) => v = cachedValue)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _cacheService.GetOrSetAsync(key, factoryMock.Object);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(cachedValue, result);
|
|
||||||
factoryMock.Verify(f => f(), Times.Never);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Set(
|
|
||||||
key,
|
|
||||||
It.IsAny<string>(),
|
|
||||||
It.IsAny<MemoryCacheEntryOptions>()), Times.Never);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Get_ValueExists_ReturnsCachedValue()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
var cachedValue = "cached-value";
|
|
||||||
|
|
||||||
// Setup cache hit
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
||||||
.Callback<string, out string>((k, out string v) => v = cachedValue)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Get<string>(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(cachedValue, result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Get_ValueNotExists_ReturnsDefault()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
|
|
||||||
// Setup cache miss
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Get<string>(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Null(result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Set_ValidValue_CachesValue()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
var value = "test-value";
|
|
||||||
|
|
||||||
var mockEntryOptions = new Mock<MemoryCacheEntryOptions>();
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Set(key, value, mockEntryOptions.Object))
|
|
||||||
.Verifiable();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_cacheService.Set(key, value, mockEntryOptions.Object);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Set(key, value, mockEntryOptions.Object), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Set_NullValue_DoesNotCache()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_cacheService.Set<string>(key, null);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Set(
|
|
||||||
It.IsAny<string>(),
|
|
||||||
It.IsAny<string>(),
|
|
||||||
It.IsAny<MemoryCacheEntryOptions>()), Times.Never);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Remove_KeyExists_RemovesFromCache()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
|
|
||||||
// Setup cache hit for removal check
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
||||||
.Callback<string, out object>((k, out object v) => v = "some-value")
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Remove(key))
|
|
||||||
.Verifiable();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Remove(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Remove_KeyNotExists_ReturnsFalse()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
|
|
||||||
// Setup cache miss
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Remove(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Never);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Exists_KeyExists_ReturnsTrue()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
|
|
||||||
// Setup cache hit
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Exists(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Exists_KeyNotExists_ReturnsFalse()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
|
|
||||||
// Setup cache miss
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Exists(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Clear_ClearsAllCache()
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
_cacheService.Clear();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Compact(1.0), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetStatistics_ReturnsCacheStatistics()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Count)
|
|
||||||
.Returns(5);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.GetStatistics();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.True(result.TotalItems >= 0);
|
|
||||||
Assert.True(result.HitRate >= 0 && result.HitRate <= 1);
|
|
||||||
Assert.True(result.MemoryUsageBytes >= 0);
|
|
||||||
Assert.NotNull(result.ItemsByType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetKeys_MatchingPattern_ReturnsKeys()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var pattern = "device:*";
|
|
||||||
var expectedKeys = new List<string> { "device:1", "device:2", "device:3" };
|
|
||||||
|
|
||||||
// Mock cache keys
|
|
||||||
var mockKeys = new List<string> { "device:1", "template:1", "device:2", "config:1", "device:3" };
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Keys)
|
|
||||||
.Returns(mockKeys.Cast<object>().ToList());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.GetKeys(pattern).ToList();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(3, result.Count);
|
|
||||||
Assert.Contains("device:1", result);
|
|
||||||
Assert.Contains("device:2", result);
|
|
||||||
Assert.Contains("device:3", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetKeys_NoMatches_ReturnsEmptyList()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var pattern = "nonexistent:*";
|
|
||||||
|
|
||||||
// Mock cache keys
|
|
||||||
var mockKeys = new List<string> { "device:1", "template:1", "config:1" };
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Keys)
|
|
||||||
.Returns(mockKeys.Cast<object>().ToList());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.GetKeys(pattern).ToList();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Refresh_ExistingKey_RefreshesCache()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "test-key";
|
|
||||||
var value = "test-value";
|
|
||||||
var mockOptions = new MemoryCacheEntryOptions();
|
|
||||||
|
|
||||||
// Setup cache hit
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
||||||
.Callback<string, out object>((k, out object v) => v = value)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Remove(key))
|
|
||||||
.Verifiable();
|
|
||||||
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Set(key, value, mockOptions))
|
|
||||||
.Verifiable();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Refresh<object>(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Once);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Set(key, value, mockOptions), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Refresh_NonExistingKey_ReturnsFalse()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var key = "nonexistent-key";
|
|
||||||
|
|
||||||
// Setup cache miss
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _cacheService.Refresh<object>(key);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Never);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Set(
|
|
||||||
It.IsAny<string>(),
|
|
||||||
It.IsAny<object>(),
|
|
||||||
It.IsAny<MemoryCacheEntryOptions>()), Times.Never);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetOrSetDeviceAsync_ValidDevice_ReturnsDevice()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
||||||
|
|
||||||
var factoryMock = new Mock<Func<Task<CNCDevice>>>();
|
|
||||||
factoryMock.Setup(f => f()).ReturnsAsync(device);
|
|
||||||
|
|
||||||
// Setup cache miss
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<CNCDevice>.IsAny))
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Setup cache set
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Set(
|
|
||||||
It.IsAny<string>(),
|
|
||||||
device,
|
|
||||||
It.IsAny<MemoryCacheEntryOptions>()))
|
|
||||||
.Callback<string, object, MemoryCacheEntryOptions>((k, v, o) => { });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _cacheService.GetOrSetDeviceAsync(deviceId, factoryMock.Object);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(device, result);
|
|
||||||
factoryMock.Verify(f => f(), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidateDeviceCache_ValidDevice_RemovesRelatedKeys()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
|
|
||||||
// Setup cache hits for removal
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<object>.IsAny))
|
|
||||||
.Callback<string, out object>((k, out object v) => v = "some-value")
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Remove(It.IsAny<string>()))
|
|
||||||
.Verifiable();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_cacheService.InvalidateDeviceCache(deviceId, "additional:key");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove("device:1"), Times.Once);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove("device:status:1"), Times.Once);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove(It.IsAny<string>()), Times.AtLeast(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetOrSetSystemConfigurationAsync_ValidFactory_ReturnsConfiguration()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var config = new SystemConfiguration { DailyProductionTarget = 100 };
|
|
||||||
|
|
||||||
var factoryMock = new Mock<Func<Task<SystemConfiguration>>>();
|
|
||||||
factoryMock.Setup(f => f()).ReturnsAsync(config);
|
|
||||||
|
|
||||||
// Setup cache miss
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<SystemConfiguration>.IsAny))
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
// Setup cache set
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Set(
|
|
||||||
It.IsAny<string>(),
|
|
||||||
config,
|
|
||||||
It.IsAny<MemoryCacheEntryOptions>()))
|
|
||||||
.Callback<string, object, MemoryCacheEntryOptions>((k, v, o) => { });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _cacheService.GetOrSetSystemConfigurationAsync(factoryMock.Object);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(config, result);
|
|
||||||
factoryMock.Verify(f => f(), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void InvalidateSystemConfigCache_RemovesSystemConfigKeys()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// Setup cache hits for removal
|
|
||||||
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<object>.IsAny))
|
|
||||||
.Callback<string, out object>((k, out object v) => v = "some-value")
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
_mockMemoryCache.Setup(cache => cache.Remove(It.IsAny<string>()))
|
|
||||||
.Verifiable();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_cacheService.InvalidateSystemConfigCache();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove("config:system"), Times.Once);
|
|
||||||
_mockMemoryCache.Verify(cache => cache.Remove("config:alerts"), Times.Once);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,765 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using Moq;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Haoliang.Api.Controllers;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Models.Models.System;
|
|
||||||
using Haoliang.Models.Models.Production;
|
|
||||||
using Haoliang.Models.Common;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests.Controllers
|
|
||||||
{
|
|
||||||
public class StatisticsControllerTests
|
|
||||||
{
|
|
||||||
private readonly Mock<IProductionStatisticsService> _mockStatisticsService;
|
|
||||||
private readonly StatisticsController _controller;
|
|
||||||
|
|
||||||
public StatisticsControllerTests()
|
|
||||||
{
|
|
||||||
_mockStatisticsService = new Mock<IProductionStatisticsService>();
|
|
||||||
_controller = new StatisticsController(_mockStatisticsService.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProductionTrendsAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var startDate = DateTime.Now.AddDays(-7);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
|
|
||||||
var trendAnalysis = new ProductionTrendAnalysis
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
DeviceName = "Test Device",
|
|
||||||
PeriodStart = startDate,
|
|
||||||
PeriodEnd = endDate,
|
|
||||||
TotalProduction = 1000,
|
|
||||||
AverageDailyProduction = 142.86m,
|
|
||||||
TrendCoefficient = 0.5m,
|
|
||||||
TrendDirection = ProductionTrendDirection.Increasing,
|
|
||||||
DailyData = new List<DailyProduction>()
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.CalculateProductionTrendsAsync(deviceId, startDate, endDate))
|
|
||||||
.ReturnsAsync(trendAnalysis);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetProductionTrends(deviceId, startDate, endDate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<ProductionTrendAnalysis>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(trendAnalysis, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProductionTrendsAsync_InvalidDevice_ReturnsNotFound()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 999;
|
|
||||||
var startDate = DateTime.Now.AddDays(-7);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.CalculateProductionTrendsAsync(deviceId, startDate, endDate))
|
|
||||||
.ThrowsAsync(new KeyNotFoundException("Device not found"));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetProductionTrends(deviceId, startDate, endDate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var notFoundResult = Assert.IsType<NotFoundObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<ProductionTrendAnalysis>>(notFoundResult.Value);
|
|
||||||
Assert.False(response.Success);
|
|
||||||
Assert.Contains("Device not found", response.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProductionReportAsync_ValidFilter_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new ReportFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
StartDate = DateTime.Now.AddDays(-7),
|
|
||||||
EndDate = DateTime.Now,
|
|
||||||
ReportType = ReportType.Daily
|
|
||||||
};
|
|
||||||
|
|
||||||
var productionReport = new ProductionReport
|
|
||||||
{
|
|
||||||
ReportDate = DateTime.Now,
|
|
||||||
ReportType = ReportType.Daily,
|
|
||||||
SummaryItems = new List<ProductionSummaryItem>(),
|
|
||||||
Metadata = ReportMetadata.GeneratedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.GenerateProductionReportAsync(filter))
|
|
||||||
.ReturnsAsync(productionReport);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetProductionReport(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<ProductionReport>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(productionReport, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetDashboardSummaryAsync_ValidFilter_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new DashboardFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
Date = DateTime.Today,
|
|
||||||
IncludeAlerts = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var dashboardSummary = new DashboardSummary
|
|
||||||
{
|
|
||||||
GeneratedAt = DateTime.Now,
|
|
||||||
TotalDevices = 10,
|
|
||||||
ActiveDevices = 8,
|
|
||||||
OfflineDevices = 2,
|
|
||||||
TotalProductionToday = 1000,
|
|
||||||
TotalProductionThisWeek = 7000,
|
|
||||||
TotalProductionThisMonth = 30000,
|
|
||||||
OverallEfficiency = 85.5m,
|
|
||||||
QualityRate = 98.2m,
|
|
||||||
DeviceSummaries = new List<DeviceSummary>(),
|
|
||||||
ActiveAlerts = new List<AlertSummary>()
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.GetDashboardSummaryAsync(filter))
|
|
||||||
.ReturnsAsync(dashboardSummary);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetDashboardSummary(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<DashboardSummary>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(dashboardSummary, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetEfficiencyMetricsAsync_ValidFilter_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new EfficiencyFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
StartDate = DateTime.Now.AddDays(-7),
|
|
||||||
EndDate = DateTime.Now,
|
|
||||||
Metrics = EfficiencyMetric.Oee
|
|
||||||
};
|
|
||||||
|
|
||||||
var efficiencyMetrics = new EfficiencyMetrics
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceName = "Test Device",
|
|
||||||
PeriodStart = DateTime.Now.AddDays(-7),
|
|
||||||
PeriodEnd = DateTime.Now,
|
|
||||||
Availability = 95m,
|
|
||||||
Performance = 90m,
|
|
||||||
Quality = 98m,
|
|
||||||
Oee = 83.79m,
|
|
||||||
Utilization = EquipmentUtilization.Optimal,
|
|
||||||
HourlyData = new List<HourlyEfficiency>()
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.CalculateEfficiencyMetricsAsync(filter))
|
|
||||||
.ReturnsAsync(efficiencyMetrics);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetEfficiencyMetrics(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<EfficiencyMetrics>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(efficiencyMetrics, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetOeeMetricsAsync_ValidDeviceAndDate_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var date = DateTime.Today;
|
|
||||||
|
|
||||||
var oeeMetrics = new OeeMetrics
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
DeviceName = "Test Device",
|
|
||||||
Date = date,
|
|
||||||
Availability = 95m,
|
|
||||||
Performance = 90m,
|
|
||||||
Quality = 98m,
|
|
||||||
Oee = 83.79m,
|
|
||||||
PlannedProductionTime = TimeSpan.FromHours(8),
|
|
||||||
ActualProductionTime = TimeSpan.FromHours(7.6),
|
|
||||||
Downtime = TimeSpan.FromMinutes(24),
|
|
||||||
IdealCycleTime = 2,
|
|
||||||
TotalCycleTime = 500,
|
|
||||||
TotalPieces = 250,
|
|
||||||
GoodPieces = 245
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.CalculateOeeAsync(deviceId, date))
|
|
||||||
.ReturnsAsync(oeeMetrics);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetOeeMetrics(deviceId, date);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<OeeMetrics>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(oeeMetrics, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProductionForecastAsync_ValidFilter_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new ForecastFilter
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DaysToForecast = 7,
|
|
||||||
Model = ForecastModel.Linear,
|
|
||||||
HistoricalDataStart = DateTime.Now.AddDays(-30),
|
|
||||||
HistoricalDataEnd = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
var productionForecast = new ProductionForecast
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceName = "Test Device",
|
|
||||||
ForecastStartDate = DateTime.Today,
|
|
||||||
ForecastEndDate = DateTime.Today.AddDays(6),
|
|
||||||
DailyForecasts = new List<ForecastItem>(),
|
|
||||||
ForecastAccuracy = 85.5m,
|
|
||||||
ModelUsed = ForecastModel.Linear
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.GenerateProductionForecastAsync(filter))
|
|
||||||
.ReturnsAsync(productionForecast);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetProductionForecast(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<ProductionForecast>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(productionForecast, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DetectProductionAnomaliesAsync_ValidFilter_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new AnomalyFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
StartDate = DateTime.Now.AddDays(-7),
|
|
||||||
EndDate = DateTime.Now,
|
|
||||||
MinSeverity = AnomalySeverity.Medium
|
|
||||||
};
|
|
||||||
|
|
||||||
var anomalyAnalysis = new AnomalyAnalysis
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
DeviceNames = new List<string> { "Test Device" },
|
|
||||||
AnalysisStartDate = DateTime.Now.AddDays(-7),
|
|
||||||
AnalysisEndDate = DateTime.Now,
|
|
||||||
Anomalies = new List<ProductionAnomaly>(),
|
|
||||||
OverallSeverity = AnomalySeverity.Low
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.DetectProductionAnomaliesAsync(filter))
|
|
||||||
.ReturnsAsync(anomalyAnalysis);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.DetectProductionAnomalies(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<AnomalyAnalysis>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(anomalyAnalysis, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetHistoricalProductionDataAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var startDate = DateTime.Now.AddDays(-7);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
var groupBy = GroupBy.Date;
|
|
||||||
|
|
||||||
var historicalData = new HistoricalProductionData
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
PeriodStart = startDate,
|
|
||||||
PeriodEnd = endDate,
|
|
||||||
GroupBy = groupBy,
|
|
||||||
DataPoints = new List<DataPoint>()
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.GenerateProductionReportAsync(It.IsAny<ReportFilter>()))
|
|
||||||
.ReturnsAsync(new ProductionReport { SummaryItems = new List<ProductionSummaryItem>() });
|
|
||||||
|
|
||||||
// Mock the conversion in the controller
|
|
||||||
// This would normally be done by the controller logic
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetHistoricalProductionData(deviceId, startDate, endDate, groupBy);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<HistoricalProductionData>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(deviceId, response.Data.DeviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetHistoricalProductionDataAsync_InvalidDevice_ReturnsBadRequest()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 0; // Invalid device ID
|
|
||||||
var startDate = DateTime.Now.AddDays(-7);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetHistoricalProductionData(deviceId, startDate, endDate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<HistoricalProductionData>>(badRequestResult.Value);
|
|
||||||
Assert.False(response.Success);
|
|
||||||
Assert.Contains("Invalid device ID", response.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetEfficiencyTrendsAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var startDate = DateTime.Now.AddDays(-7);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
var metric = EfficiencyMetric.Oee;
|
|
||||||
|
|
||||||
var efficiencyTrendData = new EfficiencyTrendData
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Metric = metric,
|
|
||||||
PeriodStart = startDate,
|
|
||||||
PeriodEnd = endDate,
|
|
||||||
DataPoints = new List<EfficiencyDataPoint>()
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.CalculateEfficiencyMetricsAsync(It.IsAny<EfficiencyFilter>()))
|
|
||||||
.ReturnsAsync(new EfficiencyMetrics { HourlyData = new List<HourlyEfficiency>() });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetEfficiencyTrends(deviceId, startDate, endDate, metric);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<EfficiencyTrendData>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(deviceId, response.Data.DeviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetMultiDeviceSummaryAsync_ValidDevices_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceIds = new List<int> { 1, 2, 3 };
|
|
||||||
|
|
||||||
_mockStatisticsService.Setup(service => service.GetDashboardSummaryAsync(It.IsAny<DashboardFilter>()))
|
|
||||||
.ReturnsAsync(new DashboardSummary
|
|
||||||
{
|
|
||||||
TotalDevices = 3,
|
|
||||||
ActiveDevices = 2,
|
|
||||||
OfflineDevices = 1,
|
|
||||||
TotalProductionToday = 1000,
|
|
||||||
TotalProductionThisWeek = 7000,
|
|
||||||
TotalProductionThisMonth = 30000,
|
|
||||||
OverallEfficiency = 85.5m,
|
|
||||||
QualityRate = 98.2m,
|
|
||||||
DeviceSummaries = new List<DeviceSummary>()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetMultiDeviceSummary(deviceIds);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<MultiDeviceSummary>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(3, response.Data.DeviceCount);
|
|
||||||
Assert.Equal(2, response.Data.ActiveDeviceCount);
|
|
||||||
Assert.Equal(1, response.Data.OfflineDeviceCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ConfigControllerTests
|
|
||||||
{
|
|
||||||
private readonly Mock<ISystemService> _mockSystemService;
|
|
||||||
private readonly Mock<ITemplateService> _mockTemplateService;
|
|
||||||
private readonly Mock<IRulesService> _mockRulesService;
|
|
||||||
private readonly Mock<IProductionStatisticsService> _mockStatisticsService;
|
|
||||||
private readonly ConfigController _controller;
|
|
||||||
|
|
||||||
public ConfigControllerTests()
|
|
||||||
{
|
|
||||||
_mockSystemService = new Mock<ISystemService>();
|
|
||||||
_mockTemplateService = new Mock<ITemplateService>();
|
|
||||||
_mockRulesService = new Mock<IRulesService>();
|
|
||||||
_mockStatisticsService = new Mock<IProductionStatisticsService>();
|
|
||||||
_controller = new ConfigController(
|
|
||||||
_mockSystemService.Object,
|
|
||||||
_mockTemplateService.Object,
|
|
||||||
_mockRulesService.Object,
|
|
||||||
_mockStatisticsService.Object
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetSystemConfigurationAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var systemConfig = new SystemConfiguration
|
|
||||||
{
|
|
||||||
DailyProductionTarget = 100,
|
|
||||||
EnableProduction = true,
|
|
||||||
EnableAlerts = true,
|
|
||||||
AlertThresholds = new Dictionary<string, decimal>
|
|
||||||
{
|
|
||||||
{ "production_drop", 30 },
|
|
||||||
{ "quality_rate", 90 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.GetSystemConfigurationAsync())
|
|
||||||
.ReturnsAsync(systemConfig);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetSystemConfiguration();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<SystemConfiguration>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(systemConfig, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task UpdateSystemConfigurationAsync_ValidConfig_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var config = new SystemConfiguration
|
|
||||||
{
|
|
||||||
DailyProductionTarget = 150,
|
|
||||||
EnableProduction = true,
|
|
||||||
EnableAlerts = true
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.UpdateSystemConfigurationAsync(config))
|
|
||||||
.ReturnsAsync(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.UpdateSystemConfiguration(config);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<bool>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.True(response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetProductionTargetsAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var targets = new List<ProductionTargetConfig>
|
|
||||||
{
|
|
||||||
new ProductionTargetConfig { DeviceId = 1, ProgramName = "Program1", DailyTarget = 100 },
|
|
||||||
new ProductionTargetConfig { DeviceId = 2, ProgramName = "Program2", DailyTarget = 150 }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.GetProductionTargetsAsync())
|
|
||||||
.ReturnsAsync(targets);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetProductionTargets();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<List<ProductionTargetConfig>>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(2, response.Data.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetWorkingHoursConfigAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var workingHoursConfig = new WorkingHoursConfig
|
|
||||||
{
|
|
||||||
WorkingDays = new List<DayOfWeek> { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday },
|
|
||||||
WorkingHours = TimeSpan.FromHours(8),
|
|
||||||
StartHour = 9,
|
|
||||||
EndHour = 17,
|
|
||||||
BreakIntervals = new List<TimeSpan>
|
|
||||||
{
|
|
||||||
TimeSpan.FromHours(12) // Lunch break
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.GetWorkingHoursConfigAsync())
|
|
||||||
.ReturnsAsync(workingHoursConfig);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetWorkingHoursConfig();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<WorkingHoursConfig>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(5, response.Data.WorkingDays.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetAlertConfigurationAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var alertConfig = new AlertConfiguration
|
|
||||||
{
|
|
||||||
EnableAlerts = true,
|
|
||||||
AlertTypes = new List<string> { "production_drop", "quality_rate", "device_error" },
|
|
||||||
AlertThresholds = new Dictionary<string, decimal>
|
|
||||||
{
|
|
||||||
{ "production_drop", 30 },
|
|
||||||
{ "quality_rate", 90 },
|
|
||||||
{ "device_error", 5 }
|
|
||||||
},
|
|
||||||
NotificationChannels = new List<string> { "email", "sms", "webhook" }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.GetAlertConfigurationAsync())
|
|
||||||
.ReturnsAsync(alertConfig);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetAlertConfiguration();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<AlertConfiguration>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(3, response.Data.AlertTypes.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetBusinessRulesAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var rules = new List<BusinessRuleConfig>
|
|
||||||
{
|
|
||||||
new BusinessRuleConfig { RuleId = 1, RuleName = "Production Target", Enabled = true },
|
|
||||||
new BusinessRuleConfig { RuleId = 2, RuleName = "Quality Control", Enabled = false }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockRulesService.Setup(service => service.GetAllRulesAsync())
|
|
||||||
.ReturnsAsync(rules);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetBusinessRules();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<List<BusinessRuleConfig>>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(2, response.Data.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateOrUpdateBusinessRuleAsync_ValidRule_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var rule = new BusinessRuleConfig
|
|
||||||
{
|
|
||||||
RuleId = 1,
|
|
||||||
RuleName = "Production Target",
|
|
||||||
Enabled = true,
|
|
||||||
RuleExpression = "production > target * 0.9"
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockRulesService.Setup(service => service.CreateOrUpdateRuleAsync(rule))
|
|
||||||
.ReturnsAsync(rule);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.CreateOrUpdateBusinessRule(rule);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<BusinessRuleConfig>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(rule, response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DeleteBusinessRuleAsync_ValidRuleId_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var ruleId = 1;
|
|
||||||
|
|
||||||
_mockRulesService.Setup(service => service.DeleteRuleAsync(ruleId))
|
|
||||||
.ReturnsAsync(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.DeleteBusinessRule(ruleId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<bool>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.True(response.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetStatisticsRulesAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var rules = new List<StatisticsRuleConfig>
|
|
||||||
{
|
|
||||||
new StatisticsRuleConfig { RuleId = 1, RuleName = "Production Trend", Enabled = true },
|
|
||||||
new StatisticsRuleConfig { RuleId = 2, RuleName = "Efficiency Analysis", Enabled = true }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockRulesService.Setup(service => service.GetStatisticsRulesAsync())
|
|
||||||
.ReturnsAsync(rules);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetStatisticsRules();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<List<StatisticsRuleConfig>>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(2, response.Data.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetDataRetentionConfigAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var retentionConfig = new DataRetentionConfig
|
|
||||||
{
|
|
||||||
BusinessDataRetentionDays = 365,
|
|
||||||
LogDataRetentionDays = 90,
|
|
||||||
StatisticsDataRetentionDays = 180,
|
|
||||||
AutoCleanupEnabled = true,
|
|
||||||
CleanupSchedule = "0 2 * * *" // Daily at 2 AM
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.GetDataRetentionConfigAsync())
|
|
||||||
.ReturnsAsync(retentionConfig);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetDataRetentionConfig();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<DataRetentionConfig>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(365, response.Data.BusinessDataRetentionDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetDashboardConfigAsync_ValidRequest_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var dashboardConfig = new DashboardConfig
|
|
||||||
{
|
|
||||||
RefreshInterval = 30,
|
|
||||||
EnableRealTimeUpdates = true,
|
|
||||||
DefaultTimeRange = "7d",
|
|
||||||
AvailableTimeRanges = new List<string> { "1d", "7d", "30d", "90d" },
|
|
||||||
AvailableWidgets = new List<string> { "production_chart", "efficiency_chart", "device_status" }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.GetDashboardConfigAsync())
|
|
||||||
.ReturnsAsync(dashboardConfig);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.GetDashboardConfig();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<DashboardConfig>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.Equal(30, response.Data.RefreshInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ValidateConfigurationAsync_ValidConfig_ReturnsOk()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var config = new SystemConfiguration { DailyProductionTarget = 100 };
|
|
||||||
|
|
||||||
var validationResult = new ValidationResult
|
|
||||||
{
|
|
||||||
IsValid = true,
|
|
||||||
Errors = new List<string>(),
|
|
||||||
Warnings = new List<string>()
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.ValidateConfigurationAsync(config))
|
|
||||||
.ReturnsAsync(validationResult);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.ValidateConfiguration(config);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
||||||
var response = Assert.IsType<ApiResponse<ValidationResult>>(okResult.Value);
|
|
||||||
Assert.True(response.Success);
|
|
||||||
Assert.True(response.Data.IsValid);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ExportConfigurationAsync_ValidRequest_ReturnsFile()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var config = new SystemConfiguration
|
|
||||||
{
|
|
||||||
DailyProductionTarget = 100,
|
|
||||||
EnableProduction = true,
|
|
||||||
EnableAlerts = true
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockSystemService.Setup(service => service.GetSystemConfigurationAsync())
|
|
||||||
.ReturnsAsync(config);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _controller.ExportConfiguration();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var fileResult = Assert.IsType<FileContentResult>(result);
|
|
||||||
Assert.Contains("system-configuration.json", fileResult.FileDownloadName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Xunit;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
using Haoliang.Data.Entities;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests
|
|
||||||
{
|
|
||||||
public class DeviceRepositoryTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void GetAllDevices_WhenNoDevices_ReturnsEmptyList()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var devices = repository.GetAllDevices();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Empty(devices);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CreateDevice_WithValidDevice_ReturnsCreatedDevice()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
var device = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "TEST001",
|
|
||||||
DeviceName = "测试设备",
|
|
||||||
IPAddress = "192.168.1.100",
|
|
||||||
HttpUrl = "http://192.168.1.100/api/status",
|
|
||||||
CollectionInterval = 30,
|
|
||||||
TemplateId = 1,
|
|
||||||
IsAvailable = true,
|
|
||||||
IsOnline = true
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var createdDevice = repository.CreateDevice(device);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(createdDevice);
|
|
||||||
Assert.True(createdDevice.Id > 0);
|
|
||||||
Assert.Equal("TEST001", createdDevice.DeviceCode);
|
|
||||||
Assert.Equal("测试设备", createdDevice.DeviceName);
|
|
||||||
Assert.True(createdDevice.CreatedAt > DateTime.MinValue);
|
|
||||||
Assert.True(createdDevice.UpdatedAt > DateTime.MinValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetDeviceById_WithExistingDevice_ReturnsDevice()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
var device = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "TEST002",
|
|
||||||
DeviceName = "测试设备2",
|
|
||||||
IPAddress = "192.168.1.101",
|
|
||||||
HttpUrl = "http://192.168.1.101/api/status",
|
|
||||||
CollectionInterval = 60,
|
|
||||||
TemplateId = 1,
|
|
||||||
IsAvailable = true,
|
|
||||||
IsOnline = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var createdDevice = repository.CreateDevice(device);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var retrievedDevice = repository.GetDeviceById(createdDevice.Id);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(retrievedDevice);
|
|
||||||
Assert.Equal(createdDevice.Id, retrievedDevice.Id);
|
|
||||||
Assert.Equal("TEST002", retrievedDevice.DeviceCode);
|
|
||||||
Assert.Equal("测试设备2", retrievedDevice.DeviceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetDeviceById_WithNonExistingId_ReturnsNull()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var device = repository.GetDeviceById(999);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Null(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetDeviceByCode_WithExistingCode_ReturnsDevice()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
var device = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "TEST003",
|
|
||||||
DeviceName = "测试设备3",
|
|
||||||
IPAddress = "192.168.1.102",
|
|
||||||
HttpUrl = "http://192.168.1.102/api/status",
|
|
||||||
CollectionInterval = 45,
|
|
||||||
TemplateId = 1,
|
|
||||||
IsAvailable = false,
|
|
||||||
IsOnline = false
|
|
||||||
};
|
|
||||||
|
|
||||||
repository.CreateDevice(device);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var retrievedDevice = repository.GetDeviceByCode("TEST003");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(retrievedDevice);
|
|
||||||
Assert.Equal("TEST003", retrievedDevice.DeviceCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetOnlineDevices_WithMixedDevices_ReturnsOnlyOnlineDevices()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
// Create online device
|
|
||||||
var onlineDevice = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "ONLINE001",
|
|
||||||
DeviceName = "在线设备",
|
|
||||||
IsOnline = true,
|
|
||||||
IsAvailable = true
|
|
||||||
};
|
|
||||||
repository.CreateDevice(onlineDevice);
|
|
||||||
|
|
||||||
// Create offline device
|
|
||||||
var offlineDevice = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "OFFLINE001",
|
|
||||||
DeviceName = "离线设备",
|
|
||||||
IsOnline = false,
|
|
||||||
IsAvailable = true
|
|
||||||
};
|
|
||||||
repository.CreateDevice(offlineDevice);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var onlineDevices = repository.GetOnlineDevices();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Single(onlineDevices);
|
|
||||||
Assert.Equal("ONLINE001", onlineDevices.First().DeviceCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void UpdateDevice_WithValidDevice_UpdatesDevice()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
var device = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "TEST004",
|
|
||||||
DeviceName = "原始名称",
|
|
||||||
IPAddress = "192.168.1.103",
|
|
||||||
HttpUrl = "http://192.168.1.103/api/status",
|
|
||||||
CollectionInterval = 30,
|
|
||||||
TemplateId = 1,
|
|
||||||
IsAvailable = true,
|
|
||||||
IsOnline = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var createdDevice = repository.CreateDevice(device);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
createdDevice.DeviceName = "更新后的名称";
|
|
||||||
createdDevice.IsAvailable = false;
|
|
||||||
var updatedDevice = repository.UpdateDevice(createdDevice);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(updatedDevice);
|
|
||||||
Assert.Equal("更新后的名称", updatedDevice.DeviceName);
|
|
||||||
Assert.False(updatedDevice.IsAvailable);
|
|
||||||
Assert.True(updatedDevice.UpdatedAt > createdDevice.CreatedAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeleteDevice_WithExistingId_DeletesDevice()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var options = CreateNewDbContextOptions();
|
|
||||||
using var context = new CNCBusinessDbContext(options);
|
|
||||||
var repository = new DeviceRepository(context);
|
|
||||||
|
|
||||||
var device = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "DELETE001",
|
|
||||||
DeviceName = "待删除设备",
|
|
||||||
IPAddress = "192.168.1.104",
|
|
||||||
HttpUrl = "http://192.168.1.104/api/status",
|
|
||||||
CollectionInterval = 30,
|
|
||||||
TemplateId = 1,
|
|
||||||
IsAvailable = true,
|
|
||||||
IsOnline = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var createdDevice = repository.CreateDevice(device);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
repository.DeleteDevice(createdDevice.Id);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var deletedDevice = repository.GetDeviceById(createdDevice.Id);
|
|
||||||
Assert.Null(deletedDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Microsoft.EntityFrameworkCore.DbContextOptions<CNCBusinessDbContext> CreateNewDbContextOptions()
|
|
||||||
{
|
|
||||||
var optionsBuilder = new Microsoft.EntityFrameworkCore.DbContextOptionsBuilder<CNCBusinessDbContext>();
|
|
||||||
optionsBuilder.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString());
|
|
||||||
return optionsBuilder.Options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,525 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using Moq;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
using Haoliang.Models.Models.Device;
|
|
||||||
using Haoliang.Models.Models.System;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests.Services
|
|
||||||
{
|
|
||||||
public class DeviceStateMachineTests
|
|
||||||
{
|
|
||||||
private readonly Mock<IDeviceRepository> _mockDeviceRepository;
|
|
||||||
private readonly Mock<IDeviceCollectionService> _mockCollectionService;
|
|
||||||
private readonly Mock<IAlarmService> _mockAlarmService;
|
|
||||||
private readonly Mock<ICacheService> _mockCacheService;
|
|
||||||
private readonly DeviceStateMachine _deviceStateMachine;
|
|
||||||
|
|
||||||
public DeviceStateMachineTests()
|
|
||||||
{
|
|
||||||
_mockDeviceRepository = new Mock<IDeviceRepository>();
|
|
||||||
_mockCollectionService = new Mock<IDeviceCollectionService>();
|
|
||||||
_mockAlarmService = new Mock<IAlarmService>();
|
|
||||||
_mockCacheService = new Mock<ICacheService>();
|
|
||||||
|
|
||||||
_deviceStateMachine = new DeviceStateMachine(
|
|
||||||
_mockDeviceRepository.Object,
|
|
||||||
_mockCollectionService.Object,
|
|
||||||
_mockAlarmService.Object,
|
|
||||||
_mockCacheService.Object
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetCurrentState_DeviceInCache_ReturnsState()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var expectedState = DeviceState.Running;
|
|
||||||
|
|
||||||
// Setup device context in cache
|
|
||||||
var deviceContext = new DeviceStateContext
|
|
||||||
{
|
|
||||||
CurrentState = expectedState,
|
|
||||||
PreviousState = DeviceState.Idle,
|
|
||||||
StateChangedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// This would require mocking the internal dictionary
|
|
||||||
// For this test, we'll simulate the initial state
|
|
||||||
await _deviceStateMachine.StartAsync(CancellationToken.None);
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, expectedState, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = _deviceStateMachine.GetCurrentState(deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(expectedState, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CanTransitionAsync_ValidTransition_ReturnsTrue()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var fromState = DeviceState.Idle;
|
|
||||||
var targetState = DeviceState.Running;
|
|
||||||
|
|
||||||
// Initialize device state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.CanTransitionAsync(deviceId, targetState);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CanTransitionAsync_InvalidTransition_ReturnsFalse()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var fromState = DeviceState.Running;
|
|
||||||
var targetState = DeviceState.Offline;
|
|
||||||
|
|
||||||
// Initialize device state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.CanTransitionAsync(deviceId, targetState);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TransitionToStateAsync_ValidTransition_TransitionsState()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var fromState = DeviceState.Idle;
|
|
||||||
var targetState = DeviceState.Running;
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device", Status = DeviceStatus.Idle };
|
|
||||||
var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Idle, Runtime = TimeSpan.Zero };
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
||||||
.ReturnsAsync(currentStatus);
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device))
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
|
|
||||||
// Initialize device state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.TransitionToStateAsync(deviceId, targetState);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result.Success);
|
|
||||||
Assert.Equal(fromState, result.FromState);
|
|
||||||
Assert.Equal(targetState, result.ToState);
|
|
||||||
Assert.Equal($"Successfully transitioned from {fromState} to {targetState}", result.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TransitionToStateAsync_InvalidTransition_ReturnsFailure()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var fromState = DeviceState.Running;
|
|
||||||
var targetState = DeviceState.Offline;
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
|
|
||||||
// Initialize device state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, fromState, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.TransitionToStateAsync(deviceId, targetState);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result.Success);
|
|
||||||
Assert.Contains($"Cannot transition from {fromState} to {targetState}", result.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TriggerEventAsync_ValidEvent_TransitionsState()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var deviceEvent = DeviceEvent.Start;
|
|
||||||
var targetState = DeviceState.Running;
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device", EnableProduction = true };
|
|
||||||
var currentStatus = new DeviceCurrentStatus { DeviceId = deviceId, Status = DeviceStatus.Online, Runtime = TimeSpan.Zero };
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
||||||
.ReturnsAsync(currentStatus);
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device))
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
|
|
||||||
// Initialize device state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.TriggerEventAsync(deviceId, deviceEvent);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result.Success);
|
|
||||||
Assert.Equal(DeviceState.Idle, result.FromState);
|
|
||||||
Assert.Equal(targetState, result.ToState);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TriggerEventAsync_InvalidEvent_ReturnsFailure()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var deviceEvent = DeviceEvent.Stop; // Cannot stop when not running
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
|
|
||||||
// Initialize device state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.TriggerEventAsync(deviceId, deviceEvent);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result.Success);
|
|
||||||
Assert.Contains($"No event handler configured for {deviceEvent} in state DeviceState.Idle", result.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ForceStateAsync_ValidForce_TransitionsState()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var targetState = DeviceState.Running;
|
|
||||||
var reason = "Emergency override";
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.ForceStateAsync(deviceId, targetState, reason);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result.Success);
|
|
||||||
Assert.Equal($"Forced state transition to {targetState}: {reason}", result.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ValidateStateAsync_ValidState_ReturnsValid()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var device = new CNCDevice {
|
|
||||||
Id = deviceId,
|
|
||||||
Name = "Test Device",
|
|
||||||
Status = DeviceStatus.Idle,
|
|
||||||
EnableProduction = true
|
|
||||||
};
|
|
||||||
var currentStatus = new DeviceCurrentStatus {
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Status = DeviceStatus.Idle,
|
|
||||||
Runtime = TimeSpan.FromHours(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
||||||
.ReturnsAsync(currentStatus);
|
|
||||||
|
|
||||||
// Initialize device state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.ValidateStateAsync(deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result.IsValid);
|
|
||||||
Assert.Equal(DeviceState.Idle, result.CurrentState);
|
|
||||||
Assert.Empty(result.Issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ValidateStateAsync_StateMismatch_ReturnsInvalid()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var device = new CNCDevice {
|
|
||||||
Id = deviceId,
|
|
||||||
Name = "Test Device",
|
|
||||||
Status = DeviceStatus.Running, // Status doesn't match state
|
|
||||||
EnableProduction = true
|
|
||||||
};
|
|
||||||
var currentStatus = new DeviceCurrentStatus {
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Status = DeviceStatus.Running,
|
|
||||||
Runtime = TimeSpan.FromHours(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
||||||
.ReturnsAsync(currentStatus);
|
|
||||||
|
|
||||||
// Initialize device state as Idle but status is Running
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Idle, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.ValidateStateAsync(deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result.IsValid);
|
|
||||||
Assert.Contains("State mismatch", result.Issues.First());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RegisterStateHandler_ValidHandler_RegistersHandler()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var handlerMock = new Mock<Func<int, DeviceState, DeviceState, Task>>();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_deviceStateMachine.RegisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// This test would need to verify the handler was registered
|
|
||||||
// For now, we'll just ensure it doesn't throw
|
|
||||||
Assert.True(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void UnregisterStateHandler_ValidHandler_UnregistersHandler()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var handlerMock = new Mock<Func<int, DeviceState, DeviceState, Task>>();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_deviceStateMachine.RegisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState));
|
|
||||||
_deviceStateMachine.UnregisterStateHandler((deviceId, fromState, toState) => handlerMock.Object(deviceId, fromState, toState));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// This test would need to verify the handler was unregistered
|
|
||||||
// For now, we'll just ensure it doesn't throw
|
|
||||||
Assert.True(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetStateHistoryAsync_DeviceWithHistory_ReturnsHistory()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var history = new List<DeviceStateHistory>
|
|
||||||
{
|
|
||||||
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddHours(-2) },
|
|
||||||
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-1) }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, null, null))
|
|
||||||
.ReturnsAsync(history);
|
|
||||||
|
|
||||||
// Initialize current state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.GetStateHistoryAsync(deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(3, result.Count); // History entries + current state
|
|
||||||
Assert.Equal(DeviceState.Idle, result[0].State);
|
|
||||||
Assert.Equal(DeviceState.Running, result[1].State);
|
|
||||||
Assert.Equal(DeviceState.Stopped, result[2].State);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetStateStatisticsAsync_DeviceWithHistory_ReturnsStatistics()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var history = new List<DeviceStateHistory>
|
|
||||||
{
|
|
||||||
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddHours(-2) },
|
|
||||||
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-1) }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, null, null))
|
|
||||||
.ReturnsAsync(history);
|
|
||||||
|
|
||||||
// Initialize current state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.GetStateStatisticsAsync(deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(deviceId, result.DeviceId);
|
|
||||||
Assert.True(result.StateTransitions > 0);
|
|
||||||
Assert.True(result.StateDurations.ContainsKey(DeviceState.Idle));
|
|
||||||
Assert.True(result.StateDurations.ContainsKey(DeviceState.Running));
|
|
||||||
Assert.True(result.StateDurations.ContainsKey(DeviceState.Stopped));
|
|
||||||
Assert.True(result.StateCounts.ContainsKey(DeviceState.Idle));
|
|
||||||
Assert.True(result.StateCounts.ContainsKey(DeviceState.Running));
|
|
||||||
Assert.True(result.StateCounts.ContainsKey(DeviceState.Stopped));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetStateStatisticsAsync_WithDateFilter_ReturnsFilteredStatistics()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var filterStartDate = DateTime.Now.AddDays(-1);
|
|
||||||
var filterEndDate = DateTime.Now;
|
|
||||||
|
|
||||||
var history = new List<DeviceStateHistory>
|
|
||||||
{
|
|
||||||
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Idle, ChangedAt = DateTime.Now.AddDays(-2) }, // Outside filter
|
|
||||||
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Running, ChangedAt = DateTime.Now.AddHours(-12) }, // Within filter
|
|
||||||
new DeviceStateHistory { DeviceId = deviceId, State = DeviceState.Stopped, ChangedAt = DateTime.Now.AddHours(-6) } // Within filter
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetDeviceStateHistoryAsync(deviceId, filterStartDate, filterEndDate))
|
|
||||||
.ReturnsAsync(history);
|
|
||||||
|
|
||||||
// Initialize current state
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Stopped, "Test setup");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _deviceStateMachine.GetStateStatisticsAsync(deviceId, filterStartDate, filterEndDate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(deviceId, result.DeviceId);
|
|
||||||
Assert.Equal(filterStartDate, result.PeriodStart);
|
|
||||||
Assert.Equal(filterEndDate, result.PeriodEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TranslateStatusToDeviceState_ValidStatus_ReturnsState()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var currentStatus = new DeviceCurrentStatus { Status = DeviceStatus.Running };
|
|
||||||
|
|
||||||
// Act
|
|
||||||
// This method would need to be made public or tested through another method
|
|
||||||
// For now, we'll test the translation logic through a state transition
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(1))
|
|
||||||
.ReturnsAsync(currentStatus);
|
|
||||||
|
|
||||||
// Initialize device state and check if it matches the status
|
|
||||||
await _deviceStateMachine.ForceStateAsync(1, DeviceState.Unknown, "Test setup");
|
|
||||||
var result = _deviceStateMachine.GetCurrentState(1);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// The state should be Running based on the status
|
|
||||||
Assert.Equal(DeviceState.Unknown, result); // Initially unknown until transition
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TranslateStateToDeviceStatus_ValidState_ReturnsStatus()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var state = DeviceState.Running;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
// This would test the reverse translation
|
|
||||||
// We can test it by verifying the device status is updated correctly
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.UpdateDeviceAsync(device))
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
|
|
||||||
// Transition to Running state and check if device status is updated
|
|
||||||
await _deviceStateMachine.TransitionToStateAsync(1, state);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// The device status should be updated to Running
|
|
||||||
Assert.Equal(DeviceStatus.Running, device.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task HandleInvalidStateAsync_InvalidState_TransitionsToSafeState()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var validationResult = new DeviceValidationResult
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
IsValid = false,
|
|
||||||
Issues = new List<string> { "State mismatch" },
|
|
||||||
CurrentState = DeviceState.Error
|
|
||||||
};
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
// This method is private, so we test through ValidateStateAsync which calls it
|
|
||||||
await _deviceStateMachine.ForceStateAsync(deviceId, DeviceState.Error, "Test setup");
|
|
||||||
var result = await _deviceStateMachine.ValidateStateAsync(deviceId);
|
|
||||||
|
|
||||||
// The state machine should handle the invalid state
|
|
||||||
// We expect the state to remain Error since it's already a safe state for errors
|
|
||||||
Assert.Equal(DeviceState.Error, _deviceStateMachine.GetCurrentState(deviceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task StartAsync_InitializesDeviceStates()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var devices = new List<CNCDevice>
|
|
||||||
{
|
|
||||||
new CNCDevice { Id = 1, Name = "Device 1" },
|
|
||||||
new CNCDevice { Id = 2, Name = "Device 2" }
|
|
||||||
};
|
|
||||||
|
|
||||||
var device1Status = new DeviceCurrentStatus { DeviceId = 1, Status = DeviceStatus.Online, Runtime = TimeSpan.Zero };
|
|
||||||
var device2Status = new DeviceCurrentStatus { DeviceId = 2, Status = DeviceStatus.Running, Runtime = TimeSpan.FromHours(1) };
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetAllDevicesAsync())
|
|
||||||
.ReturnsAsync(devices);
|
|
||||||
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(1))
|
|
||||||
.ReturnsAsync(device1Status);
|
|
||||||
_mockCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(2))
|
|
||||||
.ReturnsAsync(device2Status);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _deviceStateMachine.StartAsync(CancellationToken.None);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// Allow time for async initialization
|
|
||||||
await Task.Delay(100);
|
|
||||||
|
|
||||||
// Check if states are initialized based on device status
|
|
||||||
Assert.Equal(DeviceState.Online, _deviceStateMachine.GetCurrentState(1));
|
|
||||||
Assert.Equal(DeviceState.Running, _deviceStateMachine.GetCurrentState(2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.2" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.6.1" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Moq" Version="4.20.69" />
|
|
||||||
<PackageReference Include="Moq.AutoMock" Version="3.5.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Haoliang.Models\Haoliang.Models.csproj" />
|
|
||||||
<ProjectReference Include="..\Haoliang.Core\Haoliang.Core.csproj" />
|
|
||||||
<ProjectReference Include="..\Haoliang.Data\Haoliang.Data.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,229 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Xunit;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.Template;
|
|
||||||
using Haoliang.Models.Production;
|
|
||||||
using Haoliang.Models.User;
|
|
||||||
using Haoliang.Models.System;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests
|
|
||||||
{
|
|
||||||
public class ModelTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void CNCDevice_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var device = new CNCDevice
|
|
||||||
{
|
|
||||||
DeviceCode = "TEST001",
|
|
||||||
DeviceName = "测试设备",
|
|
||||||
IPAddress = "192.168.1.100",
|
|
||||||
HttpUrl = "http://192.168.1.100/api/status",
|
|
||||||
CollectionInterval = 30,
|
|
||||||
TemplateId = 1,
|
|
||||||
IsAvailable = true,
|
|
||||||
IsOnline = false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("TEST001", device.DeviceCode);
|
|
||||||
Assert.Equal("测试设备", device.DeviceName);
|
|
||||||
Assert.Equal("192.168.1.100", device.IPAddress);
|
|
||||||
Assert.Equal("http://192.168.1.100/api/status", device.HttpUrl);
|
|
||||||
Assert.Equal(30, device.CollectionInterval);
|
|
||||||
Assert.Equal(1, device.TemplateId);
|
|
||||||
Assert.True(device.IsAvailable);
|
|
||||||
Assert.False(device.IsOnline);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CNCDevice_WhenCreated_HasDefaultTimestamps()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var device = new CNCDevice();
|
|
||||||
var beforeCreation = DateTime.Now;
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(device.CreatedAt >= beforeCreation || device.CreatedAt == DateTime.MinValue);
|
|
||||||
Assert.True(device.UpdatedAt >= beforeCreation || device.UpdatedAt == DateTime.MinValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TagData_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var tag = new TagData
|
|
||||||
{
|
|
||||||
Id = "_io_status",
|
|
||||||
Desc = "设备状态",
|
|
||||||
Quality = "Good",
|
|
||||||
Value = "Running",
|
|
||||||
Time = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("_io_status", tag.Id);
|
|
||||||
Assert.Equal("设备状态", tag.Desc);
|
|
||||||
Assert.Equal("Good", tag.Quality);
|
|
||||||
Assert.Equal("Running", tag.Value);
|
|
||||||
Assert.NotNull(tag.Time);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CNCBrandTemplate_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var template = new CNCBrandTemplate
|
|
||||||
{
|
|
||||||
BrandName = "发那科",
|
|
||||||
Description = "发那科CNC设备模板",
|
|
||||||
IsEnabled = true,
|
|
||||||
FieldMappings = new List<TemplateFieldMapping>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("发那科", template.BrandName);
|
|
||||||
Assert.Equal("发那科CNC设备模板", template.Description);
|
|
||||||
Assert.True(template.IsEnabled);
|
|
||||||
Assert.Empty(template.FieldMappings);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ProductionRecord_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var record = new ProductionRecord
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
NCProgram = "O0001",
|
|
||||||
ProductionDate = DateTime.Now,
|
|
||||||
Quantity = 100,
|
|
||||||
QualityRate = 95.5m,
|
|
||||||
OperatorId = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(1, record.DeviceId);
|
|
||||||
Assert.Equal("O0001", record.NCProgram);
|
|
||||||
Assert.Equal(DateTime.Now.Date, record.ProductionDate.Date);
|
|
||||||
Assert.Equal(100, record.Quantity);
|
|
||||||
Assert.Equal(95.5m, record.QualityRate);
|
|
||||||
Assert.Equal(1, record.OperatorId);
|
|
||||||
Assert.NotNull(record.CreatedAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ProductionRealtimeData_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var realtimeData = new ProductionRealtimeData
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceCode = "CNC001",
|
|
||||||
DeviceName = "车床1号",
|
|
||||||
Status = "Running",
|
|
||||||
IsRunning = true,
|
|
||||||
NCProgram = "O0001",
|
|
||||||
CurrentCount = 1500,
|
|
||||||
TodayQuantity = 50,
|
|
||||||
LastUpdateTime = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(1, realtimeData.DeviceId);
|
|
||||||
Assert.Equal("CNC001", realtimeData.DeviceCode);
|
|
||||||
Assert.Equal("车床1号", realtimeData.DeviceName);
|
|
||||||
Assert.Equal("Running", realtimeData.Status);
|
|
||||||
Assert.True(realtimeData.IsRunning);
|
|
||||||
Assert.Equal("O0001", realtimeData.NCProgram);
|
|
||||||
Assert.Equal(1500, realtimeData.CurrentCount);
|
|
||||||
Assert.Equal(50, realtimeData.TodayQuantity);
|
|
||||||
Assert.NotNull(realtimeData.LastUpdateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void User_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var user = new User
|
|
||||||
{
|
|
||||||
Username = "admin",
|
|
||||||
PasswordHash = "hashed_password",
|
|
||||||
RealName = "管理员",
|
|
||||||
Email = "admin@example.com",
|
|
||||||
Phone = "13800138000",
|
|
||||||
RoleId = 1,
|
|
||||||
IsActive = true,
|
|
||||||
LastLoginTime = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("admin", user.Username);
|
|
||||||
Assert.Equal("hashed_password", user.PasswordHash);
|
|
||||||
Assert.Equal("管理员", user.RealName);
|
|
||||||
Assert.Equal("admin@example.com", user.Email);
|
|
||||||
Assert.Equal("13800138000", user.Phone);
|
|
||||||
Assert.Equal(1, user.RoleId);
|
|
||||||
Assert.True(user.IsActive);
|
|
||||||
Assert.NotNull(user.LastLoginTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void SystemHealth_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var health = new SystemHealth
|
|
||||||
{
|
|
||||||
CheckTime = DateTime.Now,
|
|
||||||
DatabaseConnected = true,
|
|
||||||
OnlineDeviceCount = 10,
|
|
||||||
TotalDeviceCount = 15,
|
|
||||||
ActiveAlarmCount = 2,
|
|
||||||
CollectionSuccessRate = 98.5,
|
|
||||||
AverageResponseTime = 120.5,
|
|
||||||
DatabaseSize = 1024000000,
|
|
||||||
CpuUsage = 45.2,
|
|
||||||
MemoryUsage = 78.3,
|
|
||||||
DiskUsage = 65.8
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(health.DatabaseConnected);
|
|
||||||
Assert.Equal(10, health.OnlineDeviceCount);
|
|
||||||
Assert.Equal(15, health.TotalDeviceCount);
|
|
||||||
Assert.Equal(2, health.ActiveAlarmCount);
|
|
||||||
Assert.Equal(98.5, health.CollectionSuccessRate);
|
|
||||||
Assert.Equal(120.5, health.AverageResponseTime);
|
|
||||||
Assert.True(health.CpuUsage > 0);
|
|
||||||
Assert.True(health.MemoryUsage > 0);
|
|
||||||
Assert.True(health.DiskUsage > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Alarm_CreatedWithValidData_HasCorrectProperties()
|
|
||||||
{
|
|
||||||
// Arrange & Act
|
|
||||||
var alarm = new Alarm
|
|
||||||
{
|
|
||||||
AlarmType = "设备离线",
|
|
||||||
AlarmLevel = "高",
|
|
||||||
AlarmContent = "设备连接超时",
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceName = "CNC001",
|
|
||||||
IsResolved = false,
|
|
||||||
OccurrenceTime = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal("设备离线", alarm.AlarmType);
|
|
||||||
Assert.Equal("高", alarm.AlarmLevel);
|
|
||||||
Assert.Equal("设备连接超时", alarm.AlarmContent);
|
|
||||||
Assert.Equal(1, alarm.DeviceId);
|
|
||||||
Assert.Equal("CNC001", alarm.DeviceName);
|
|
||||||
Assert.False(alarm.IsResolved);
|
|
||||||
Assert.NotNull(alarm.OccurrenceTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,376 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using Moq;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Data.Repositories;
|
|
||||||
using Haoliang.Models.Models.Device;
|
|
||||||
using Haoliang.Models.Models.Production;
|
|
||||||
using Haoliang.Models.Models.System;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests.Services
|
|
||||||
{
|
|
||||||
public class ProductionStatisticsServiceTests
|
|
||||||
{
|
|
||||||
private readonly Mock<IProductionRepository> _mockProductionRepository;
|
|
||||||
private readonly Mock<IDeviceRepository> _mockDeviceRepository;
|
|
||||||
private readonly Mock<ISystemRepository> _mockSystemRepository;
|
|
||||||
private readonly Mock<IAlarmRepository> _mockAlarmRepository;
|
|
||||||
private readonly Mock<ICollectionRepository> _mockCollectionRepository;
|
|
||||||
private readonly ProductionStatisticsService _statisticsService;
|
|
||||||
|
|
||||||
public ProductionStatisticsServiceTests()
|
|
||||||
{
|
|
||||||
_mockProductionRepository = new Mock<IProductionRepository>();
|
|
||||||
_mockDeviceRepository = new Mock<IDeviceRepository>();
|
|
||||||
_mockSystemRepository = new Mock<ISystemRepository>();
|
|
||||||
_mockAlarmRepository = new Mock<IAlarmRepository>();
|
|
||||||
_mockCollectionRepository = new Mock<ICollectionRepository>();
|
|
||||||
|
|
||||||
_statisticsService = new ProductionStatisticsService(
|
|
||||||
_mockProductionRepository.Object,
|
|
||||||
_mockDeviceRepository.Object,
|
|
||||||
_mockSystemRepository.Object,
|
|
||||||
_mockAlarmRepository.Object,
|
|
||||||
_mockCollectionRepository.Object
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CalculateProductionTrendsAsync_ValidDeviceAndDates_ReturnsTrendAnalysis()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var startDate = DateTime.Now.AddDays(-7);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
||||||
var productionRecords = new List<ProductionRecord>
|
|
||||||
{
|
|
||||||
new ProductionRecord { Id = 1, DeviceId = deviceId, Created = startDate, Quantity = 100, TargetQuantity = 120 },
|
|
||||||
new ProductionRecord { Id = 2, DeviceId = deviceId, Created = startDate.AddDays(1), Quantity = 110, TargetQuantity = 120 },
|
|
||||||
new ProductionRecord { Id = 3, DeviceId = deviceId, Created = startDate.AddDays(2), Quantity = 130, TargetQuantity = 120 }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByDeviceAndDateRangeAsync(deviceId, startDate, endDate))
|
|
||||||
.ReturnsAsync(productionRecords);
|
|
||||||
_mockSystemRepository.Setup(repo => repo.GetSystemConfigurationAsync())
|
|
||||||
.ReturnsAsync(new SystemConfiguration { DailyProductionTarget = 100 });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.CalculateProductionTrendsAsync(deviceId, startDate, endDate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(deviceId, result.DeviceId);
|
|
||||||
Assert.Equal(device.Name, result.DeviceName);
|
|
||||||
Assert.Equal(startDate, result.PeriodStart);
|
|
||||||
Assert.Equal(endDate, result.PeriodEnd);
|
|
||||||
Assert.Equal(340, result.TotalProduction);
|
|
||||||
Assert.Equal(3, result.AverageDailyProduction);
|
|
||||||
Assert.Equal(3, result.DailyData.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CalculateProductionTrendsAsync_InvalidDevice_ThrowsKeyNotFoundException()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 999;
|
|
||||||
var startDate = DateTime.Now.AddDays(-7);
|
|
||||||
var endDate = DateTime.Now;
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync((CNCDevice)null);
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
await Assert.ThrowsAsync<KeyNotFoundException>(() =>
|
|
||||||
_statisticsService.CalculateProductionTrendsAsync(deviceId, startDate, endDate));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GenerateProductionReportAsync_ValidFilter_ReturnsProductionReport()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new ReportFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
StartDate = DateTime.Now.AddDays(-7),
|
|
||||||
EndDate = DateTime.Now,
|
|
||||||
ReportType = ReportType.Daily
|
|
||||||
};
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
var records = new List<ProductionRecord>
|
|
||||||
{
|
|
||||||
new ProductionRecord { Id = 1, DeviceId = 1, Created = DateTime.Now, Quantity = 100, TargetQuantity = 120, IsGood = true },
|
|
||||||
new ProductionRecord { Id = 2, DeviceId = 1, Created = DateTime.Now, Quantity = 50, TargetQuantity = 60, IsGood = true }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByFilterAsync(filter))
|
|
||||||
.ReturnsAsync(records);
|
|
||||||
_mockSystemRepository.Setup(repo => repo.GetSystemConfigurationAsync())
|
|
||||||
.ReturnsAsync(new SystemConfiguration { DailyProductionTarget = 100 });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.GenerateProductionReportAsync(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(ReportType.Daily, result.ReportType);
|
|
||||||
Assert.Equal(2, result.SummaryItems.Count);
|
|
||||||
|
|
||||||
var summary = result.SummaryItems.First();
|
|
||||||
Assert.Equal(1, summary.DeviceId);
|
|
||||||
Assert.Equal("Test Device", summary.DeviceName);
|
|
||||||
Assert.Equal(150, summary.Quantity);
|
|
||||||
Assert.Equal(180, summary.TargetQuantity);
|
|
||||||
Assert.Equal(83.33m, summary.Efficiency);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetDashboardSummaryAsync_ValidFilter_ReturnsDashboardSummary()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new DashboardFilter
|
|
||||||
{
|
|
||||||
Date = DateTime.Today,
|
|
||||||
IncludeAlerts = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
var deviceSummaries = new List<DeviceSummary>
|
|
||||||
{
|
|
||||||
new DeviceSummary { DeviceId = 1, DeviceName = "Test Device", TodayProduction = 100, Efficiency = 85, QualityRate = 98 }
|
|
||||||
};
|
|
||||||
|
|
||||||
var alertSummaries = new List<AlertSummary>
|
|
||||||
{
|
|
||||||
new AlertSummary { AlertId = 1, DeviceName = "Test Device", AlertType = AlertType.DeviceOffline, Message = "Device offline" }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetAllDevicesAsync())
|
|
||||||
.ReturnsAsync(new List<CNCDevice> { device });
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockCollectionRepository.Setup(repo => GetDeviceCurrentStatusAsync(1))
|
|
||||||
.ReturnsAsync(new DeviceCurrentStatus { Status = DeviceStatus.Online, Runtime = TimeSpan.FromHours(8) });
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByDeviceAndDateAsync(1, DateTime.Today))
|
|
||||||
.ReturnsAsync(new List<ProductionRecord>());
|
|
||||||
_mockAlarmRepository.Setup(repo => GetActiveAlertsByDeviceAsync(1))
|
|
||||||
.ReturnsAsync(new List<Alert> { new Alert { Id = 1, DeviceId = 1, AlertType = "DeviceOffline", Message = "Device offline" } });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.GetDashboardSummaryAsync(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(DateTime.Now.Date, result.GeneratedAt.Date);
|
|
||||||
Assert.Equal(1, result.TotalDevices);
|
|
||||||
Assert.Equal(1, result.ActiveDevices);
|
|
||||||
Assert.Equal(0, result.OfflineDevices);
|
|
||||||
Assert.Equal(1, result.DeviceSummaries.Count);
|
|
||||||
Assert.Equal(1, result.ActiveAlerts.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CalculateOeeAsync_ValidDeviceAndDate_ReturnsOeeMetrics()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var date = DateTime.Today;
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = deviceId, Name = "Test Device", IdealCycleTime = 2 };
|
|
||||||
var records = new List<ProductionRecord>
|
|
||||||
{
|
|
||||||
new ProductionRecord { Id = 1, DeviceId = deviceId, Created = date, Quantity = 50, TargetQuantity = 60, IsGood = true }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(deviceId))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByDeviceAndDateAsync(deviceId, date))
|
|
||||||
.ReturnsAsync(records);
|
|
||||||
_mockSystemRepository.Setup(repo => repo.GetSystemConfigurationAsync())
|
|
||||||
.ReturnsAsync(new SystemConfiguration { DailyWorkingHours = TimeSpan.FromHours(8) });
|
|
||||||
_mockCollectionRepository.Setup(repo => GetDeviceStatusHistoryAsync(deviceId, date, date))
|
|
||||||
.ReturnsAsync(new List<DeviceStatusHistory>());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.CalculateOeeAsync(deviceId, date);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(deviceId, result.DeviceId);
|
|
||||||
Assert.Equal(device.Name, result.DeviceName);
|
|
||||||
Assert.Equal(date, result.Date);
|
|
||||||
Assert.True(result.Availability >= 0 && result.Availability <= 100);
|
|
||||||
Assert.True(result.Performance >= 0 && result.Performance <= 100);
|
|
||||||
Assert.True(result.Quality >= 0 && result.Quality <= 100);
|
|
||||||
Assert.True(result.Oee >= 0 && result.Oee <= 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GenerateProductionForecastAsync_ValidFilter_ReturnsProductionForecast()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new ForecastFilter
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DaysToForecast = 7,
|
|
||||||
Model = ForecastModel.Linear,
|
|
||||||
HistoricalDataStart = DateTime.Now.AddDays(-30),
|
|
||||||
HistoricalDataEnd = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
var historicalData = new List<ProductionRecord>
|
|
||||||
{
|
|
||||||
new ProductionRecord { Id = 1, DeviceId = 1, Created = DateTime.Now.AddDays(-30), Quantity = 100 },
|
|
||||||
new ProductionRecord { Id = 2, DeviceId = 1, Created = DateTime.Now.AddDays(-29), Quantity = 110 },
|
|
||||||
new ProductionRecord { Id = 3, DeviceId = 1, Created = DateTime.Now.AddDays(-28), Quantity = 105 }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByDeviceAndDateRangeAsync(1, filter.HistoricalDataStart, filter.HistoricalDataEnd))
|
|
||||||
.ReturnsAsync(historicalData);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.GenerateProductionForecastAsync(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(filter.DeviceId, result.DeviceId);
|
|
||||||
Assert.Equal(device.Name, result.DeviceName);
|
|
||||||
Assert.Equal(filter.Model, result.ModelUsed);
|
|
||||||
Assert.Equal(7, result.DailyForecasts.Count);
|
|
||||||
|
|
||||||
// Check that forecasts have reasonable values
|
|
||||||
foreach (var forecast in result.DailyForecasts)
|
|
||||||
{
|
|
||||||
Assert.True(forecast.ForecastedQuantity >= 0);
|
|
||||||
Assert.True(forecast.ConfidenceLower <= forecast.ForecastedQuantity);
|
|
||||||
Assert.True(forecast.ConfidenceUpper >= forecast.ForecastedQuantity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DetectProductionAnomaliesAsync_ValidFilter_ReturnsAnomalyAnalysis()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new AnomalyFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
StartDate = DateTime.Now.AddDays(-7),
|
|
||||||
EndDate = DateTime.Now,
|
|
||||||
MinSeverity = AnomalySeverity.Medium
|
|
||||||
};
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
var records = new List<ProductionRecord>
|
|
||||||
{
|
|
||||||
new ProductionRecord { Id = 1, DeviceId = 1, Created = DateTime.Now.AddDays(-2), Quantity = 100 },
|
|
||||||
new ProductionRecord { Id = 2, DeviceId = 1, Created = DateTime.Now.AddDays(-1), Quantity = 30 }, // Big drop
|
|
||||||
new ProductionRecord { Id = 3, DeviceId = 1, Created = DateTime.Now, Quantity = 95 }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByDeviceAndDateRangeAsync(1, filter.StartDate, filter.EndDate))
|
|
||||||
.ReturnsAsync(records);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.DetectProductionAnomaliesAsync(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(1, result.DeviceIds.Count);
|
|
||||||
Assert.True(result.Anomalies.Count >= 1); // Should detect the production drop
|
|
||||||
|
|
||||||
var anomaly = result.Anomalies.FirstOrDefault(a => a.Type == AnomalyType.ProductionDrop);
|
|
||||||
Assert.NotNull(anomaly);
|
|
||||||
Assert.Equal(AnomalySeverity.High, anomaly.Severity);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetEfficiencyMetricsAsync_ValidFilter_ReturnsEfficiencyMetrics()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new EfficiencyFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
StartDate = DateTime.Now.AddDays(-7),
|
|
||||||
EndDate = DateTime.Now,
|
|
||||||
Metrics = EfficiencyMetric.Oee
|
|
||||||
};
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
var records = new List<ProductionRecord>
|
|
||||||
{
|
|
||||||
new ProductionRecord { Id = 1, DeviceId = 1, Created = DateTime.Now.AddDays(-1), Quantity = 100, TargetQuantity = 120, IsGood = true }
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetAllActiveDevicesAsync())
|
|
||||||
.ReturnsAsync(new List<CNCDevice> { device });
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByDeviceAndDateRangeAsync(1, filter.StartDate, filter.EndDate))
|
|
||||||
.ReturnsAsync(records);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.CalculateEfficiencyMetricsAsync(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Single(result.DeviceIds);
|
|
||||||
Assert.Equal(1, result.DeviceIds.First());
|
|
||||||
Assert.True(result.Availability >= 0 && result.Availability <= 100);
|
|
||||||
Assert.True(result.Performance >= 0 && result.Performance <= 100);
|
|
||||||
Assert.True(result.Quality >= 0 && result.Quality <= 100);
|
|
||||||
Assert.True(result.Oee >= 0 && result.Oee <= 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PerformQualityAnalysisAsync_ValidFilter_ReturnsQualityAnalysis()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var filter = new QualityFilter
|
|
||||||
{
|
|
||||||
DeviceIds = new List<int> { 1 },
|
|
||||||
StartDate = DateTime.Now.AddDays(-7),
|
|
||||||
EndDate = DateTime.Now,
|
|
||||||
MetricType = QualityMetricType.DefectRate
|
|
||||||
};
|
|
||||||
|
|
||||||
var device = new CNCDevice { Id = 1, Name = "Test Device" };
|
|
||||||
var records = new List<ProductionRecord>
|
|
||||||
{
|
|
||||||
new ProductionRecord { Id = 1, DeviceId = 1, Created = DateTime.Now, Quantity = 100, TargetQuantity = 120, IsGood = true },
|
|
||||||
new ProductionRecord { Id = 2, DeviceId = 1, Created = DateTime.Now, Quantity = 20, TargetQuantity = 30, IsGood = false } // Defects
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetByIdAsync(1))
|
|
||||||
.ReturnsAsync(device);
|
|
||||||
_mockDeviceRepository.Setup(repo => repo.GetAllActiveDevicesAsync())
|
|
||||||
.ReturnsAsync(new List<CNCDevice> { device });
|
|
||||||
_mockProductionRepository.Setup(repo => repo.GetProductionRecordsByFilterAsync(It.IsAny<ReportFilter>()))
|
|
||||||
.ReturnsAsync(records);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _statisticsService.PerformQualityAnalysisAsync(filter);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Single(result.DeviceIds);
|
|
||||||
Assert.Equal(1, result.TotalProduced);
|
|
||||||
Assert.Equal(0.8m, result.QualityRate); // 80% quality rate
|
|
||||||
Assert.Equal(0.2m, result.DefectRate); // 20% defect rate
|
|
||||||
Assert.True(result.QualityMetrics.Count > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,445 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using Moq;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
using Haoliang.Models.Models.Device;
|
|
||||||
using Haoliang.Models.Models.Production;
|
|
||||||
using Haoliang.Models.Models.System;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests.Services
|
|
||||||
{
|
|
||||||
public class RealTimeServiceTests
|
|
||||||
{
|
|
||||||
private readonly Mock<IHubContext<RealTimeHub>> _mockHubContext;
|
|
||||||
private readonly Mock<IDeviceCollectionService> _mockDeviceCollectionService;
|
|
||||||
private readonly Mock<IProductionService> _mockProductionService;
|
|
||||||
private readonly Mock<IAlarmService> _mockAlarmService;
|
|
||||||
private readonly Mock<ICacheService> _mockCacheService;
|
|
||||||
private readonly RealTimeService _realTimeService;
|
|
||||||
|
|
||||||
public RealTimeServiceTests()
|
|
||||||
{
|
|
||||||
_mockHubContext = new Mock<IHubContext<RealTimeHub>>();
|
|
||||||
_mockDeviceCollectionService = new Mock<IDeviceCollectionService>();
|
|
||||||
_mockProductionService = new Mock<IProductionService>();
|
|
||||||
_mockAlarmService = new Mock<IAlarmService>();
|
|
||||||
_mockCacheService = new Mock<ICacheService>();
|
|
||||||
|
|
||||||
_realTimeService = new RealTimeService(
|
|
||||||
_mockHubContext.Object,
|
|
||||||
_mockDeviceCollectionService.Object,
|
|
||||||
_mockProductionService.Object,
|
|
||||||
_mockAlarmService.Object,
|
|
||||||
_mockCacheService.Object
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ConnectClientAsync_ValidConnection_ConnectsClient()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var connectionId = "test-connection-id";
|
|
||||||
var userId = "test-user-id";
|
|
||||||
var clientType = "web";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.ConnectClientAsync(connectionId, userId, clientType);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Client(connectionId)
|
|
||||||
.SendAsync("ClientConnected",
|
|
||||||
It.Is<object>(o =>
|
|
||||||
dynamic obj = o &&
|
|
||||||
obj.GetType().GetProperty("ClientId").GetValue(obj) == connectionId &&
|
|
||||||
obj.GetType().GetProperty("UserId").GetValue(obj) == userId &&
|
|
||||||
obj.GetType().GetProperty("ClientType").GetValue(obj) == clientType),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DisconnectClientAsync_ValidConnection_DisconnectsClient()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var connectionId = "test-connection-id";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.DisconnectClientAsync(connectionId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.AllExcept(connectionId)
|
|
||||||
.SendAsync("ClientDisconnected",
|
|
||||||
It.IsAny<object>(),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task JoinDeviceGroupAsync_ValidConnection_JoinsGroup()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var connectionId = "test-connection-id";
|
|
||||||
var deviceId = 1;
|
|
||||||
var deviceStatus = new DeviceCurrentStatus {
|
|
||||||
DeviceId = deviceId,
|
|
||||||
Status = DeviceStatus.Online,
|
|
||||||
CurrentProgram = "Test Program"
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockDeviceCollectionService.Setup(service => service.GetDeviceCurrentStatusAsync(deviceId))
|
|
||||||
.ReturnsAsync(deviceStatus);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.JoinDeviceGroupAsync(connectionId, deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Groups.AddToGroupAsync(connectionId, $"device_{deviceId}", It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Client(connectionId)
|
|
||||||
.SendAsync("DeviceStatusUpdated",
|
|
||||||
It.Is<object>(o =>
|
|
||||||
dynamic obj = o &&
|
|
||||||
obj.GetType().GetProperty("DeviceId").GetValue(obj) == deviceId),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task LeaveDeviceGroupAsync_ValidConnection_LeavesGroup()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var connectionId = "test-connection-id";
|
|
||||||
var deviceId = 1;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.LeaveDeviceGroupAsync(connectionId, deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Groups.RemoveFromGroupAsync(connectionId, $"device_{deviceId}", It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task JoinDashboardGroupAsync_ValidConnection_JoinsGroup()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var connectionId = "test-connection-id";
|
|
||||||
var dashboardId = "dashboard-1";
|
|
||||||
var dashboardUpdate = new DashboardUpdate {
|
|
||||||
Timestamp = DateTime.UtcNow,
|
|
||||||
TotalDevices = 10,
|
|
||||||
ActiveDevices = 8,
|
|
||||||
TotalProductionToday = 1000
|
|
||||||
};
|
|
||||||
|
|
||||||
_mockCacheService.Setup(cache => cache.GetOrSetDashboardSummaryAsync(It.IsAny<DateTime>(), It.IsAny<Func<Task<DashboardSummary>>>()))
|
|
||||||
.ReturnsAsync(new DashboardSummary {
|
|
||||||
TotalDevices = 10,
|
|
||||||
ActiveDevices = 8,
|
|
||||||
TotalProductionToday = 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.JoinDashboardGroupAsync(connectionId, dashboardId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Groups.AddToGroupAsync(connectionId, $"dashboard_{dashboardId}", It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Client(connectionId)
|
|
||||||
.SendAsync("DashboardUpdated",
|
|
||||||
It.Is<object>(o =>
|
|
||||||
dynamic obj = o &&
|
|
||||||
obj.GetType().GetProperty("DashboardId").GetValue(obj)?.ToString() == dashboardId),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task LeaveDashboardGroupAsync_ValidConnection_LeavesGroup()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var connectionId = "test-connection-id";
|
|
||||||
var dashboardId = "dashboard-1";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.LeaveDashboardGroupAsync(connectionId, dashboardId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Groups.RemoveFromGroupAsync(connectionId, $"dashboard_{dashboardId}", It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task BroadcastDeviceStatusAsync_ValidStatus_BroadcastsToGroups()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var statusUpdate = new DeviceStatusUpdate
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceName = "Test Device",
|
|
||||||
Status = DeviceStatus.Running,
|
|
||||||
CurrentProgram = "Test Program",
|
|
||||||
Runtime = TimeSpan.FromHours(1),
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.BroadcastDeviceStatusAsync(statusUpdate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group($"device_1")
|
|
||||||
.SendAsync("DeviceStatusUpdated",
|
|
||||||
It.Is<DeviceStatusUpdate>(s => s.DeviceId == 1 && s.Status == DeviceStatus.Running),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group("dashboard")
|
|
||||||
.SendAsync("DeviceStatusUpdated",
|
|
||||||
It.Is<DeviceStatusUpdate>(s => s.DeviceId == 1 && s.Status == DeviceStatus.Running),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task BroadcastProductionUpdateAsync_ValidUpdate_BroadcastsToGroups()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var productionUpdate = new ProductionUpdate
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceName = "Test Device",
|
|
||||||
Quantity = 100,
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.BroadcastProductionUpdateAsync(productionUpdate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group($"device_1")
|
|
||||||
.SendAsync("ProductionUpdated",
|
|
||||||
It.Is<ProductionUpdate>(p => p.DeviceId == 1 && p.Quantity == 100),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group("dashboard")
|
|
||||||
.SendAsync("ProductionUpdated",
|
|
||||||
It.Is<ProductionUpdate>(p => p.DeviceId == 1 && p.Quantity == 100),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task BroadcastAlertAsync_ValidAlert_BroadcastsToRelevantGroups()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var alertUpdate = new AlertUpdate
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceName = "Test Device",
|
|
||||||
AlertType = "DeviceError",
|
|
||||||
Message = "Device error occurred",
|
|
||||||
Timestamp = DateTime.UtcNow,
|
|
||||||
IsResolved = false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.BroadcastAlertAsync(alertUpdate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group("dashboard")
|
|
||||||
.SendAsync("AlertUpdated",
|
|
||||||
It.Is<AlertUpdate>(a => a.DeviceId == 1 && a.AlertType == "DeviceError"),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group("alerts")
|
|
||||||
.SendAsync("AlertUpdated",
|
|
||||||
It.Is<AlertUpdate>(a => a.DeviceId == 1 && a.AlertType == "DeviceError"),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group($"device_1")
|
|
||||||
.SendAsync("AlertUpdated",
|
|
||||||
It.Is<AlertUpdate>(a => a.DeviceId == 1 && a.AlertType == "DeviceError"),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SendSystemNotificationAsync_ValidNotification_SendsToNotificationGroup()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var notification = new SystemNotification
|
|
||||||
{
|
|
||||||
NotificationType = "Info",
|
|
||||||
Title = "System Update",
|
|
||||||
Message = "System maintenance scheduled",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.SendSystemNotificationAsync(notification);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group("notifications")
|
|
||||||
.SendAsync("SystemNotification",
|
|
||||||
It.Is<SystemNotification>(n =>
|
|
||||||
n.NotificationType == "Info" &&
|
|
||||||
n.Title == "System Update"),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SendDashboardUpdateAsync_ValidUpdate_SendsToDashboardGroup()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var dashboardUpdate = new DashboardUpdate
|
|
||||||
{
|
|
||||||
Timestamp = DateTime.UtcNow,
|
|
||||||
TotalDevices = 10,
|
|
||||||
ActiveDevices = 8,
|
|
||||||
TotalProductionToday = 1000
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.SendDashboardUpdateAsync(dashboardUpdate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Group("dashboard")
|
|
||||||
.SendAsync("DashboardUpdated",
|
|
||||||
It.Is<DashboardUpdate>(d =>
|
|
||||||
d.TotalDevices == 10 &&
|
|
||||||
d.ActiveDevices == 8),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SendCommandToClientAsync_ValidCommand_SendsToClient()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var connectionId = "test-connection-id";
|
|
||||||
var command = new RealTimeCommand
|
|
||||||
{
|
|
||||||
Command = "RefreshData",
|
|
||||||
Parameters = new { Interval = 5000 },
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.SendCommandToClientAsync(connectionId, command);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.Client(connectionId)
|
|
||||||
.SendAsync("Command",
|
|
||||||
It.Is<RealTimeCommand>(c =>
|
|
||||||
c.Command == "RefreshData" &&
|
|
||||||
c.Parameters.ToString().Contains("Interval")),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task BroadcastCommandAsync_ValidCommand_BroadcastsToAllClients()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var command = new RealTimeCommand
|
|
||||||
{
|
|
||||||
Command = "SystemShutdown",
|
|
||||||
Parameters = new { DelayMinutes = 5 },
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _realTimeService.BroadcastCommandAsync(command);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockHubContext.Verify(hub => hub.Clients.All
|
|
||||||
.SendAsync("Command",
|
|
||||||
It.Is<RealTimeCommand>(c =>
|
|
||||||
c.Command == "SystemShutdown"),
|
|
||||||
It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetConnectedClientsCountAsync_ValidClients_ReturnsCount()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// This test would need to mock the internal client tracking
|
|
||||||
// For now, we'll verify the method exists and doesn't throw
|
|
||||||
var result = await _realTimeService.GetConnectedClientsCountAsync();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result >= 0); // Should return a non-negative number
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetConnectedClientsByTypeAsync_ValidType_ReturnsClients()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var clientType = "web";
|
|
||||||
|
|
||||||
// This test would need to mock the internal client tracking
|
|
||||||
// For now, we'll verify the method exists and doesn't throw
|
|
||||||
var result = await _realTimeService.GetConnectedClientsByTypeAsync(clientType);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetDeviceMonitoringStatusAsync_ValidDevice_ReturnsStatus()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var streamingInfo = new DeviceStreamingInfo
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
IntervalMs = 1000,
|
|
||||||
StartedAt = DateTime.UtcNow.AddMinutes(-5),
|
|
||||||
LastUpdate = DateTime.UtcNow.AddMinutes(-1),
|
|
||||||
IsRunning = true
|
|
||||||
};
|
|
||||||
|
|
||||||
// This test would need to mock the internal device streaming tracking
|
|
||||||
// For now, we'll verify the method exists and doesn't throw
|
|
||||||
var result = await _realTimeService.GetDeviceMonitoringStatusAsync(deviceId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(deviceId, result.DeviceId);
|
|
||||||
Assert.True(result.IsStreaming);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task StartDeviceStreamingAsync_ValidDevice_StartsStreaming()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
var intervalMs = 1000;
|
|
||||||
|
|
||||||
// This test would need to mock the internal device streaming tracking
|
|
||||||
// and verify that streaming starts
|
|
||||||
// For now, we'll verify the method exists and doesn't throw
|
|
||||||
await _realTimeService.StartDeviceStreamingAsync(deviceId, intervalMs);
|
|
||||||
|
|
||||||
// Assert - would need to verify streaming started
|
|
||||||
Assert.True(true); // Placeholder assertion
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task StopDeviceStreamingAsync_ValidDevice_StopsStreaming()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var deviceId = 1;
|
|
||||||
|
|
||||||
// This test would need to mock the internal device streaming tracking
|
|
||||||
// and verify that streaming stops
|
|
||||||
// For now, we'll verify the method exists and doesn't throw
|
|
||||||
await _realTimeService.StopDeviceStreamingAsync(deviceId);
|
|
||||||
|
|
||||||
// Assert - would need to verify streaming stopped
|
|
||||||
Assert.True(true); // Placeholder assertion
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetActiveStreamingDevicesAsync_ReturnsStreamingDevices()
|
|
||||||
{
|
|
||||||
// This test would need to mock the internal device streaming tracking
|
|
||||||
// For now, we'll verify the method exists and doesn't throw
|
|
||||||
var result = await _realTimeService.GetActiveStreamingDevicesAsync();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.IsType<List<int>>(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,367 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Xunit;
|
|
||||||
using Haoliang.Models.Device;
|
|
||||||
using Haoliang.Models.DataCollection;
|
|
||||||
using Haoliang.Core.Services;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests
|
|
||||||
{
|
|
||||||
public class DeviceCollectionServiceTests
|
|
||||||
{
|
|
||||||
private readonly IDeviceCollectionService _collectionService;
|
|
||||||
|
|
||||||
public DeviceCollectionServiceTests()
|
|
||||||
{
|
|
||||||
// 这里应该使用mock对象或测试数据库
|
|
||||||
_collectionService = new DeviceCollectionService(null, null, null, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CollectDeviceDataAsync_ShouldParseValidJson()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var sampleJson = @"{
|
|
||||||
""device"": ""FANUC_01"",
|
|
||||||
""desc"": ""CNC Machine"",
|
|
||||||
""tags"": [
|
|
||||||
{
|
|
||||||
""id"": ""_io_status"",
|
|
||||||
""desc"": ""I/O Status"",
|
|
||||||
""quality"": 0,
|
|
||||||
""value"": 1,
|
|
||||||
""time"": ""2024-01-01T10:00:00""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
""id"": ""Tag5"",
|
|
||||||
""desc"": ""NC Program"",
|
|
||||||
""quality"": 0,
|
|
||||||
""value"": ""O1234"",
|
|
||||||
""time"": ""2024-01-01T10:00:00""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
""id"": ""Tag8"",
|
|
||||||
""desc"": ""Cumulative Count"",
|
|
||||||
""quality"": 0,
|
|
||||||
""value"": 12345.00000,
|
|
||||||
""time"": ""2024-01-01T10:00:00""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _collectionService.CollectDeviceDataAsync(1);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal("FANUC_01", result.DeviceCode);
|
|
||||||
Assert.Equal("O1234", result.NCProgram);
|
|
||||||
Assert.Equal(12345, result.CumulativeCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CollectDeviceDataAsync_ShouldHandleInvalidJson()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var invalidJson = @"{
|
|
||||||
""device"": ""FANUC_01"",
|
|
||||||
""desc"": ""CNC Machine"",
|
|
||||||
""tags"": [
|
|
||||||
{
|
|
||||||
""id"": ""invalid_tag"",
|
|
||||||
""value"": ""invalid_value""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}";
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
await Assert.ThrowsAsync<JsonException>(() =>
|
|
||||||
_collectionService.CollectDeviceDataAsync(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PingDeviceAsync_ShouldReturnTrueForValidIp()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var validIp = "8.8.8.8"; // Google DNS
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _collectionService.PingDeviceAsync(validIp);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProductionServiceTests
|
|
||||||
{
|
|
||||||
private readonly IProductionService _productionService;
|
|
||||||
|
|
||||||
public ProductionServiceTests()
|
|
||||||
{
|
|
||||||
_productionService = new ProductionService(null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CalculateProductionAsync_ShouldCalculateDifference()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var current = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
NCProgram = "O1234",
|
|
||||||
CumulativeCount = 15000,
|
|
||||||
RecordTime = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
var last = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
NCProgram = "O1234",
|
|
||||||
CumulativeCount = 12000,
|
|
||||||
RecordTime = DateTime.Now.AddMinutes(-5)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _productionService.CalculateProductionAsync(1);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(3000, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CalculateProductionAsync_ShouldHandleNegativeValues()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var current = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
NCProgram = "O1234",
|
|
||||||
CumulativeCount = 10000,
|
|
||||||
RecordTime = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
var last = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
NCProgram = "O1234",
|
|
||||||
CumulativeCount = 12000, // 比当前值大,应该产生负数
|
|
||||||
RecordTime = DateTime.Now.AddMinutes(-5)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _productionService.CalculateProductionAsync(1);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(0, result); // 负数应该被保护为0
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CalculateProductionAsync_ShouldHandleProgramSwitch()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var current = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
NCProgram = "O5678", // 不同的程序
|
|
||||||
CumulativeCount = 20000,
|
|
||||||
RecordTime = DateTime.Now
|
|
||||||
};
|
|
||||||
|
|
||||||
var last = new DeviceCurrentStatus
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
NCProgram = "O1234",
|
|
||||||
CumulativeCount = 15000,
|
|
||||||
RecordTime = DateTime.Now.AddMinutes(-5)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _productionService.CalculateProductionAsync(1);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(20000, result); // 新程序以当前累计数为起点
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AlarmServiceTests
|
|
||||||
{
|
|
||||||
private readonly IAlarmService _alarmService;
|
|
||||||
|
|
||||||
public AlarmServiceTests()
|
|
||||||
{
|
|
||||||
_alarmService = new AlarmManager(null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateAlarmAsync_ShouldCreateAlarm()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var alarm = new Alarm
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceCode = "FANUC_01",
|
|
||||||
AlarmType = AlarmType.DeviceOffline,
|
|
||||||
Severity = AlarmSeverity.Critical,
|
|
||||||
Title = "Device Offline",
|
|
||||||
Description = "The device has been offline for more than 5 minutes",
|
|
||||||
AlarmStatus = AlarmStatus.Active
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _alarmService.CreateAlarmAsync(alarm);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(AlarmStatus.Active, result.AlarmStatus);
|
|
||||||
Assert.NotNull(result.CreateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ResolveAlarmAsync_ShouldMarkAsResolved()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var alarm = new Alarm
|
|
||||||
{
|
|
||||||
DeviceId = 1,
|
|
||||||
DeviceCode = "FANUC_01",
|
|
||||||
AlarmType = AlarmType.DeviceOffline,
|
|
||||||
Severity = AlarmSeverity.Warning,
|
|
||||||
Title = "Device Offline",
|
|
||||||
Description = "The device has been offline",
|
|
||||||
AlarmStatus = AlarmStatus.Active
|
|
||||||
};
|
|
||||||
|
|
||||||
var createdAlarm = await _alarmService.CreateAlarmAsync(alarm);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _alarmService.ResolveAlarmAsync(createdAlarm.AlarmId, "Device reconnected");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
|
|
||||||
// 验证状态确实改变了
|
|
||||||
var resolvedAlarm = await _alarmService.GetAlarmByIdAsync(createdAlarm.AlarmId);
|
|
||||||
Assert.Equal(AlarmStatus.Resolved, resolvedAlarm.AlarmStatus);
|
|
||||||
Assert.NotNull(resolvedAlarm.ResolvedTime);
|
|
||||||
Assert.Equal("Device reconnected", resolvedAlarm.ResolutionNote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TemplateServiceTests
|
|
||||||
{
|
|
||||||
private readonly ITemplateService _templateService;
|
|
||||||
|
|
||||||
public TemplateServiceTests()
|
|
||||||
{
|
|
||||||
_templateService = new TemplateManager(null, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CreateTemplateAsync_ShouldCreateValidTemplate()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var template = new CNCBrandTemplate
|
|
||||||
{
|
|
||||||
TemplateName = "FANUC Standard",
|
|
||||||
BrandName = "FANUC",
|
|
||||||
Description = "Standard FANUC template",
|
|
||||||
IsEnabled = true,
|
|
||||||
Version = "1.0",
|
|
||||||
TemplateJson = @"{
|
|
||||||
""device"": {
|
|
||||||
""status"": ""_io_status""
|
|
||||||
},
|
|
||||||
""production"": {
|
|
||||||
""program"": ""Tag5"",
|
|
||||||
""count"": ""Tag8""
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _templateService.CreateTemplateAsync(template);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal("FANUC Standard", result.TemplateName);
|
|
||||||
Assert.True(result.IsEnabled);
|
|
||||||
Assert.NotNull(result.CreateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ValidateTemplateAsync_ShouldRejectInvalidTemplate()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var invalidTemplate = new CNCBrandTemplate
|
|
||||||
{
|
|
||||||
TemplateName = "", // 空模板名
|
|
||||||
BrandName = "FANUC",
|
|
||||||
Description = "Invalid template",
|
|
||||||
IsEnabled = true,
|
|
||||||
Version = "1.0",
|
|
||||||
TemplateJson = "" // 空JSON
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _templateService.ValidateTemplateAsync(invalidTemplate);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SystemServiceTests
|
|
||||||
{
|
|
||||||
private readonly ISystemConfigService _configService;
|
|
||||||
private readonly ILoggingService _loggingService;
|
|
||||||
|
|
||||||
public SystemServiceTests()
|
|
||||||
{
|
|
||||||
_configService = new SystemConfigManager(null, null);
|
|
||||||
_loggingService = new LoggingManager(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetConfigAsync_ShouldReturnExistingConfig()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
await _configService.SetConfigAsync("test.config", "test.value");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _configService.GetConfigAsync("test.config");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal("test.value", result.ConfigValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SetConfigAsync_ShouldUpdateConfig()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var newConfig = await _configService.SetConfigAsync("test.config", "new.value");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _configService.GetConfigAsync("test.config");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal("new.value", result.ConfigValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task LogAsync_ShouldLogMessage()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var message = "Test log message";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _loggingService.LogAsync(LogLevel.Information, message);
|
|
||||||
|
|
||||||
// 这里可以添加数据库验证,检查日志是否正确存储
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Haoliang.Tests;
|
|
||||||
|
|
||||||
public class UnitTest1
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Test1()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,327 +0,0 @@
|
|||||||
{
|
|
||||||
"format": 1,
|
|
||||||
"restore": {
|
|
||||||
"/root/opencode/haoliang/Haoliang.Tests/Haoliang.Tests.csproj": {}
|
|
||||||
},
|
|
||||||
"projects": {
|
|
||||||
"/root/opencode/haoliang/Haoliang.Core/Haoliang.Core.csproj": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"restore": {
|
|
||||||
"projectUniqueName": "/root/opencode/haoliang/Haoliang.Core/Haoliang.Core.csproj",
|
|
||||||
"projectName": "Haoliang.Core",
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Core/Haoliang.Core.csproj",
|
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
|
||||||
"outputPath": "/root/opencode/haoliang/Haoliang.Core/obj/",
|
|
||||||
"projectStyle": "PackageReference",
|
|
||||||
"configFilePaths": [
|
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
|
||||||
],
|
|
||||||
"originalTargetFrameworks": [
|
|
||||||
"net8.0"
|
|
||||||
],
|
|
||||||
"sources": {
|
|
||||||
"https://api.nuget.org/v3/index.json": {}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"projectReferences": {
|
|
||||||
"/root/opencode/haoliang/Haoliang.Data/Haoliang.Data.csproj": {
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Data/Haoliang.Data.csproj"
|
|
||||||
},
|
|
||||||
"/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj": {
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"warningProperties": {
|
|
||||||
"warnAsError": [
|
|
||||||
"NU1605"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"dependencies": {
|
|
||||||
"BCrypt.Net-Next": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[4.0.3, )"
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.Http.Abstractions": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[2.2.0, )"
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.SignalR": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[1.1.0, )"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Caching.Memory": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.0, )"
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.0, )"
|
|
||||||
},
|
|
||||||
"Microsoft.IdentityModel.Tokens": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[7.0.3, )"
|
|
||||||
},
|
|
||||||
"System.IdentityModel.Tokens.Jwt": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[7.0.3, )"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"imports": [
|
|
||||||
"net461",
|
|
||||||
"net462",
|
|
||||||
"net47",
|
|
||||||
"net471",
|
|
||||||
"net472",
|
|
||||||
"net48",
|
|
||||||
"net481"
|
|
||||||
],
|
|
||||||
"assetTargetFallback": true,
|
|
||||||
"warn": true,
|
|
||||||
"frameworkReferences": {
|
|
||||||
"Microsoft.NETCore.App": {
|
|
||||||
"privateAssets": "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeIdentifierGraphPath": "/usr/lib/dotnet/sdk/8.0.125/PortableRuntimeIdentifierGraph.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/root/opencode/haoliang/Haoliang.Data/Haoliang.Data.csproj": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"restore": {
|
|
||||||
"projectUniqueName": "/root/opencode/haoliang/Haoliang.Data/Haoliang.Data.csproj",
|
|
||||||
"projectName": "Haoliang.Data",
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Data/Haoliang.Data.csproj",
|
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
|
||||||
"outputPath": "/root/opencode/haoliang/Haoliang.Data/obj/",
|
|
||||||
"projectStyle": "PackageReference",
|
|
||||||
"configFilePaths": [
|
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
|
||||||
],
|
|
||||||
"originalTargetFrameworks": [
|
|
||||||
"net8.0"
|
|
||||||
],
|
|
||||||
"sources": {
|
|
||||||
"https://api.nuget.org/v3/index.json": {}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"projectReferences": {
|
|
||||||
"/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj": {
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"warningProperties": {
|
|
||||||
"warnAsError": [
|
|
||||||
"NU1605"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.EntityFrameworkCore": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.2, )"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Design": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.2, )"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.Tools": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.2, )"
|
|
||||||
},
|
|
||||||
"Pomelo.EntityFrameworkCore.MySql": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.2, )"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"imports": [
|
|
||||||
"net461",
|
|
||||||
"net462",
|
|
||||||
"net47",
|
|
||||||
"net471",
|
|
||||||
"net472",
|
|
||||||
"net48",
|
|
||||||
"net481"
|
|
||||||
],
|
|
||||||
"assetTargetFallback": true,
|
|
||||||
"warn": true,
|
|
||||||
"frameworkReferences": {
|
|
||||||
"Microsoft.NETCore.App": {
|
|
||||||
"privateAssets": "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeIdentifierGraphPath": "/usr/lib/dotnet/sdk/8.0.125/PortableRuntimeIdentifierGraph.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"restore": {
|
|
||||||
"projectUniqueName": "/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj",
|
|
||||||
"projectName": "Haoliang.Models",
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj",
|
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
|
||||||
"outputPath": "/root/opencode/haoliang/Haoliang.Models/obj/",
|
|
||||||
"projectStyle": "PackageReference",
|
|
||||||
"configFilePaths": [
|
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
|
||||||
],
|
|
||||||
"originalTargetFrameworks": [
|
|
||||||
"net8.0"
|
|
||||||
],
|
|
||||||
"sources": {
|
|
||||||
"https://api.nuget.org/v3/index.json": {}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"projectReferences": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"warningProperties": {
|
|
||||||
"warnAsError": [
|
|
||||||
"NU1605"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"imports": [
|
|
||||||
"net461",
|
|
||||||
"net462",
|
|
||||||
"net47",
|
|
||||||
"net471",
|
|
||||||
"net472",
|
|
||||||
"net48",
|
|
||||||
"net481"
|
|
||||||
],
|
|
||||||
"assetTargetFallback": true,
|
|
||||||
"warn": true,
|
|
||||||
"frameworkReferences": {
|
|
||||||
"Microsoft.NETCore.App": {
|
|
||||||
"privateAssets": "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeIdentifierGraphPath": "/usr/lib/dotnet/sdk/8.0.125/PortableRuntimeIdentifierGraph.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/root/opencode/haoliang/Haoliang.Tests/Haoliang.Tests.csproj": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"restore": {
|
|
||||||
"projectUniqueName": "/root/opencode/haoliang/Haoliang.Tests/Haoliang.Tests.csproj",
|
|
||||||
"projectName": "Haoliang.Tests",
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Tests/Haoliang.Tests.csproj",
|
|
||||||
"packagesPath": "/root/.nuget/packages/",
|
|
||||||
"outputPath": "/root/opencode/haoliang/Haoliang.Tests/obj/",
|
|
||||||
"projectStyle": "PackageReference",
|
|
||||||
"configFilePaths": [
|
|
||||||
"/root/.nuget/NuGet/NuGet.Config"
|
|
||||||
],
|
|
||||||
"originalTargetFrameworks": [
|
|
||||||
"net8.0"
|
|
||||||
],
|
|
||||||
"sources": {
|
|
||||||
"https://api.nuget.org/v3/index.json": {}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"projectReferences": {
|
|
||||||
"/root/opencode/haoliang/Haoliang.Core/Haoliang.Core.csproj": {
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Core/Haoliang.Core.csproj"
|
|
||||||
},
|
|
||||||
"/root/opencode/haoliang/Haoliang.Data/Haoliang.Data.csproj": {
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Data/Haoliang.Data.csproj"
|
|
||||||
},
|
|
||||||
"/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj": {
|
|
||||||
"projectPath": "/root/opencode/haoliang/Haoliang.Models/Haoliang.Models.csproj"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"warningProperties": {
|
|
||||||
"warnAsError": [
|
|
||||||
"NU1605"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"frameworks": {
|
|
||||||
"net8.0": {
|
|
||||||
"targetAlias": "net8.0",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.Mvc.Testing": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.0, )"
|
|
||||||
},
|
|
||||||
"Microsoft.EntityFrameworkCore.InMemory": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[8.0.2, )"
|
|
||||||
},
|
|
||||||
"Microsoft.NET.Test.Sdk": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[17.8.0, )"
|
|
||||||
},
|
|
||||||
"Moq": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[4.20.69, )"
|
|
||||||
},
|
|
||||||
"Moq.AutoMock": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[3.5.0, )"
|
|
||||||
},
|
|
||||||
"coverlet.collector": {
|
|
||||||
"include": "Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive",
|
|
||||||
"suppressParent": "All",
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[6.0.0, )"
|
|
||||||
},
|
|
||||||
"xunit": {
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[2.6.1, )"
|
|
||||||
},
|
|
||||||
"xunit.runner.visualstudio": {
|
|
||||||
"include": "Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive",
|
|
||||||
"suppressParent": "All",
|
|
||||||
"target": "Package",
|
|
||||||
"version": "[2.5.3, )"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"imports": [
|
|
||||||
"net461",
|
|
||||||
"net462",
|
|
||||||
"net47",
|
|
||||||
"net471",
|
|
||||||
"net472",
|
|
||||||
"net48",
|
|
||||||
"net481"
|
|
||||||
],
|
|
||||||
"assetTargetFallback": true,
|
|
||||||
"warn": true,
|
|
||||||
"frameworkReferences": {
|
|
||||||
"Microsoft.NETCore.App": {
|
|
||||||
"privateAssets": "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeIdentifierGraphPath": "/usr/lib/dotnet/sdk/8.0.125/PortableRuntimeIdentifierGraph.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
|
||||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
|
||||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
|
||||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
|
||||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
|
||||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/root/.nuget/packages/</NuGetPackageRoot>
|
|
||||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/root/.nuget/packages/</NuGetPackageFolders>
|
|
||||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
|
||||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.8.1</NuGetToolVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
|
||||||
<SourceRoot Include="/root/.nuget/packages/" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
|
||||||
<Import Project="$(NuGetPackageRoot)xunit.runner.visualstudio/2.5.3/build/net6.0/xunit.runner.visualstudio.props" Condition="Exists('$(NuGetPackageRoot)xunit.runner.visualstudio/2.5.3/build/net6.0/xunit.runner.visualstudio.props')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)xunit.core/2.6.1/build/xunit.core.props" Condition="Exists('$(NuGetPackageRoot)xunit.core/2.6.1/build/xunit.core.props')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore/8.0.2/buildTransitive/net8.0/Microsoft.EntityFrameworkCore.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore/8.0.2/buildTransitive/net8.0/Microsoft.EntityFrameworkCore.props')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.testplatform.testhost/17.8.0/build/netcoreapp3.1/Microsoft.TestPlatform.TestHost.props" Condition="Exists('$(NuGetPackageRoot)microsoft.testplatform.testhost/17.8.0/build/netcoreapp3.1/Microsoft.TestPlatform.TestHost.props')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.codecoverage/17.8.0/build/netstandard2.0/Microsoft.CodeCoverage.props" Condition="Exists('$(NuGetPackageRoot)microsoft.codecoverage/17.8.0/build/netstandard2.0/Microsoft.CodeCoverage.props')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.net.test.sdk/17.8.0/build/netcoreapp3.1/Microsoft.NET.Test.Sdk.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.test.sdk/17.8.0/build/netcoreapp3.1/Microsoft.NET.Test.Sdk.props')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Configuration.UserSecrets.props" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Configuration.UserSecrets.props')" />
|
|
||||||
</ImportGroup>
|
|
||||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
|
||||||
<Pkgxunit_analyzers Condition=" '$(Pkgxunit_analyzers)' == '' ">/root/.nuget/packages/xunit.analyzers/1.4.0</Pkgxunit_analyzers>
|
|
||||||
<PkgMicrosoft_CodeAnalysis_Analyzers Condition=" '$(PkgMicrosoft_CodeAnalysis_Analyzers)' == '' ">/root/.nuget/packages/microsoft.codeanalysis.analyzers/3.3.3</PkgMicrosoft_CodeAnalysis_Analyzers>
|
|
||||||
<PkgMicrosoft_EntityFrameworkCore_Tools Condition=" '$(PkgMicrosoft_EntityFrameworkCore_Tools)' == '' ">/root/.nuget/packages/microsoft.entityframeworkcore.tools/8.0.2</PkgMicrosoft_EntityFrameworkCore_Tools>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
|
||||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
|
||||||
<Import Project="$(NuGetPackageRoot)xunit.core/2.6.1/build/xunit.core.targets" Condition="Exists('$(NuGetPackageRoot)xunit.core/2.6.1/build/xunit.core.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)system.text.json/8.0.0/buildTransitive/net6.0/System.Text.Json.targets" Condition="Exists('$(NuGetPackageRoot)system.text.json/8.0.0/buildTransitive/net6.0/System.Text.Json.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Logging.Abstractions.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.options/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Options.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.codecoverage/17.8.0/build/netstandard2.0/Microsoft.CodeCoverage.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.codecoverage/17.8.0/build/netstandard2.0/Microsoft.CodeCoverage.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.net.test.sdk/17.8.0/build/netcoreapp3.1/Microsoft.NET.Test.Sdk.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.test.sdk/17.8.0/build/netcoreapp3.1/Microsoft.NET.Test.Sdk.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.binder/8.0.0/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.binder/8.0.0/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Configuration.UserSecrets.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Configuration.UserSecrets.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.mvc.testing/8.0.0/buildTransitive/net8.0/Microsoft.AspNetCore.Mvc.Testing.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.mvc.testing/8.0.0/buildTransitive/net8.0/Microsoft.AspNetCore.Mvc.Testing.targets')" />
|
|
||||||
<Import Project="$(NuGetPackageRoot)coverlet.collector/6.0.0/build/netstandard1.0/coverlet.collector.targets" Condition="Exists('$(NuGetPackageRoot)coverlet.collector/6.0.0/build/netstandard1.0/coverlet.collector.targets')" />
|
|
||||||
</ImportGroup>
|
|
||||||
</Project>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,196 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 2,
|
|
||||||
"dgSpecHash": "iR4RVpWptKYCHWbbq1Rku7PTbrEP77UHqwEC6hu1ZjIwKsl5gE1wVURStZwFwkV4cRD6ceZYnuePsxkoN764cA==",
|
|
||||||
"success": true,
|
|
||||||
"projectFilePath": "/root/opencode/haoliang/Haoliang.Tests/Haoliang.Tests.csproj",
|
|
||||||
"expectedPackageFiles": [
|
|
||||||
"/root/.nuget/packages/bcrypt.net-next/4.0.3/bcrypt.net-next.4.0.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/castle.core/5.1.1/castle.core.5.1.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/coverlet.collector/6.0.0/coverlet.collector.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/humanizer.core/2.14.1/humanizer.core.2.14.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.authentication.abstractions/2.2.0/microsoft.aspnetcore.authentication.abstractions.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.authorization/2.2.0/microsoft.aspnetcore.authorization.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.authorization.policy/2.2.0/microsoft.aspnetcore.authorization.policy.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.connections.abstractions/2.2.0/microsoft.aspnetcore.connections.abstractions.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.hosting.abstractions/2.2.0/microsoft.aspnetcore.hosting.abstractions.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.hosting.server.abstractions/2.2.0/microsoft.aspnetcore.hosting.server.abstractions.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.http/2.2.0/microsoft.aspnetcore.http.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.http.abstractions/2.2.0/microsoft.aspnetcore.http.abstractions.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.http.connections/1.1.0/microsoft.aspnetcore.http.connections.1.1.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.http.connections.common/1.1.0/microsoft.aspnetcore.http.connections.common.1.1.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.http.extensions/2.2.0/microsoft.aspnetcore.http.extensions.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.http.features/2.2.0/microsoft.aspnetcore.http.features.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.mvc.testing/8.0.0/microsoft.aspnetcore.mvc.testing.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.routing/2.2.0/microsoft.aspnetcore.routing.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.routing.abstractions/2.2.0/microsoft.aspnetcore.routing.abstractions.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.signalr/1.1.0/microsoft.aspnetcore.signalr.1.1.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.signalr.common/1.1.0/microsoft.aspnetcore.signalr.common.1.1.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.signalr.core/1.1.0/microsoft.aspnetcore.signalr.core.1.1.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.signalr.protocols.json/1.1.0/microsoft.aspnetcore.signalr.protocols.json.1.1.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.testhost/8.0.0/microsoft.aspnetcore.testhost.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.websockets/2.2.0/microsoft.aspnetcore.websockets.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.aspnetcore.webutilities/2.2.0/microsoft.aspnetcore.webutilities.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.codeanalysis.analyzers/3.3.3/microsoft.codeanalysis.analyzers.3.3.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.codeanalysis.common/4.5.0/microsoft.codeanalysis.common.4.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.codeanalysis.csharp/4.5.0/microsoft.codeanalysis.csharp.4.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.codeanalysis.csharp.workspaces/4.5.0/microsoft.codeanalysis.csharp.workspaces.4.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.codeanalysis.workspaces.common/4.5.0/microsoft.codeanalysis.workspaces.common.4.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.codecoverage/17.8.0/microsoft.codecoverage.17.8.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.entityframeworkcore/8.0.2/microsoft.entityframeworkcore.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.entityframeworkcore.abstractions/8.0.2/microsoft.entityframeworkcore.abstractions.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.entityframeworkcore.analyzers/8.0.2/microsoft.entityframeworkcore.analyzers.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.entityframeworkcore.design/8.0.2/microsoft.entityframeworkcore.design.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.entityframeworkcore.inmemory/8.0.2/microsoft.entityframeworkcore.inmemory.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.entityframeworkcore.relational/8.0.2/microsoft.entityframeworkcore.relational.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.entityframeworkcore.tools/8.0.2/microsoft.entityframeworkcore.tools.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.caching.abstractions/8.0.0/microsoft.extensions.caching.abstractions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.caching.memory/8.0.0/microsoft.extensions.caching.memory.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration/8.0.0/microsoft.extensions.configuration.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration.abstractions/8.0.0/microsoft.extensions.configuration.abstractions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration.binder/8.0.0/microsoft.extensions.configuration.binder.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration.commandline/8.0.0/microsoft.extensions.configuration.commandline.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration.environmentvariables/8.0.0/microsoft.extensions.configuration.environmentvariables.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration.fileextensions/8.0.0/microsoft.extensions.configuration.fileextensions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration.json/8.0.0/microsoft.extensions.configuration.json.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.configuration.usersecrets/8.0.0/microsoft.extensions.configuration.usersecrets.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.dependencyinjection/8.0.0/microsoft.extensions.dependencyinjection.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.0/microsoft.extensions.dependencyinjection.abstractions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.dependencymodel/8.0.0/microsoft.extensions.dependencymodel.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.diagnostics/8.0.0/microsoft.extensions.diagnostics.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.diagnostics.abstractions/8.0.0/microsoft.extensions.diagnostics.abstractions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.fileproviders.abstractions/8.0.0/microsoft.extensions.fileproviders.abstractions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.fileproviders.physical/8.0.0/microsoft.extensions.fileproviders.physical.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.filesystemglobbing/8.0.0/microsoft.extensions.filesystemglobbing.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.hosting/8.0.0/microsoft.extensions.hosting.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.hosting.abstractions/8.0.0/microsoft.extensions.hosting.abstractions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging/8.0.0/microsoft.extensions.logging.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.0/microsoft.extensions.logging.abstractions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.configuration/8.0.0/microsoft.extensions.logging.configuration.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.console/8.0.0/microsoft.extensions.logging.console.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.debug/8.0.0/microsoft.extensions.logging.debug.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.eventlog/8.0.0/microsoft.extensions.logging.eventlog.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.logging.eventsource/8.0.0/microsoft.extensions.logging.eventsource.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.objectpool/2.2.0/microsoft.extensions.objectpool.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.options/8.0.0/microsoft.extensions.options.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.options.configurationextensions/8.0.0/microsoft.extensions.options.configurationextensions.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.extensions.primitives/8.0.0/microsoft.extensions.primitives.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.identitymodel.abstractions/7.0.3/microsoft.identitymodel.abstractions.7.0.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.identitymodel.jsonwebtokens/7.0.3/microsoft.identitymodel.jsonwebtokens.7.0.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.identitymodel.logging/7.0.3/microsoft.identitymodel.logging.7.0.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.identitymodel.tokens/7.0.3/microsoft.identitymodel.tokens.7.0.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.net.http.headers/2.2.0/microsoft.net.http.headers.2.2.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.net.test.sdk/17.8.0/microsoft.net.test.sdk.17.8.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.netcore.platforms/2.0.0/microsoft.netcore.platforms.2.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.netcore.targets/1.1.0/microsoft.netcore.targets.1.1.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.testplatform.objectmodel/17.8.0/microsoft.testplatform.objectmodel.17.8.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.testplatform.testhost/17.8.0/microsoft.testplatform.testhost.17.8.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/microsoft.win32.primitives/4.3.0/microsoft.win32.primitives.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/mono.texttemplating/2.2.1/mono.texttemplating.2.2.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/moq/4.20.69/moq.4.20.69.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/moq.automock/3.5.0/moq.automock.3.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/mysqlconnector/2.3.5/mysqlconnector.2.3.5.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/netstandard.library/1.6.1/netstandard.library.1.6.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/nonblocking/2.1.1/nonblocking.2.1.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/nuget.frameworks/6.5.0/nuget.frameworks.6.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/pomelo.entityframeworkcore.mysql/8.0.2/pomelo.entityframeworkcore.mysql.8.0.2.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.native.system/4.3.0/runtime.native.system.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.native.system.io.compression/4.3.0/runtime.native.system.io.compression.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.native.system.net.http/4.3.0/runtime.native.system.net.http.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.native.system.security.cryptography.apple/4.3.0/runtime.native.system.security.cryptography.apple.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.native.system.security.cryptography.openssl/4.3.0/runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.opensuse.13.2-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.opensuse.13.2-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.opensuse.42.1-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.opensuse.42.1-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.osx.10.10-x64.runtime.native.system.security.cryptography.apple/4.3.0/runtime.osx.10.10-x64.runtime.native.system.security.cryptography.apple.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.osx.10.10-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.osx.10.10-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.rhel.7-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.rhel.7-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl/4.3.0/runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.appcontext/4.3.0/system.appcontext.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.buffers/4.5.0/system.buffers.4.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.codedom/4.4.0/system.codedom.4.4.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.collections/4.3.0/system.collections.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.collections.concurrent/4.3.0/system.collections.concurrent.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.collections.immutable/6.0.0/system.collections.immutable.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.composition/6.0.0/system.composition.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.composition.attributedmodel/6.0.0/system.composition.attributedmodel.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.composition.convention/6.0.0/system.composition.convention.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.composition.hosting/6.0.0/system.composition.hosting.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.composition.runtime/6.0.0/system.composition.runtime.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.composition.typedparts/6.0.0/system.composition.typedparts.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.console/4.3.0/system.console.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.diagnostics.debug/4.3.0/system.diagnostics.debug.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.diagnostics.diagnosticsource/8.0.0/system.diagnostics.diagnosticsource.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.diagnostics.eventlog/8.0.0/system.diagnostics.eventlog.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.diagnostics.tools/4.3.0/system.diagnostics.tools.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.diagnostics.tracing/4.3.0/system.diagnostics.tracing.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.globalization/4.3.0/system.globalization.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.globalization.calendars/4.3.0/system.globalization.calendars.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.globalization.extensions/4.3.0/system.globalization.extensions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.identitymodel.tokens.jwt/7.0.3/system.identitymodel.tokens.jwt.7.0.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.io/4.3.0/system.io.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.io.compression/4.3.0/system.io.compression.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.io.compression.zipfile/4.3.0/system.io.compression.zipfile.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.io.filesystem/4.3.0/system.io.filesystem.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.io.filesystem.primitives/4.3.0/system.io.filesystem.primitives.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.io.pipelines/8.0.0/system.io.pipelines.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.linq/4.3.0/system.linq.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.linq.expressions/4.3.0/system.linq.expressions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.net.http/4.3.0/system.net.http.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.net.primitives/4.3.0/system.net.primitives.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.net.sockets/4.3.0/system.net.sockets.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.net.websockets.websocketprotocol/4.5.1/system.net.websockets.websocketprotocol.4.5.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.objectmodel/4.3.0/system.objectmodel.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection/4.3.0/system.reflection.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection.emit/4.3.0/system.reflection.emit.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection.emit.ilgeneration/4.3.0/system.reflection.emit.ilgeneration.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection.emit.lightweight/4.3.0/system.reflection.emit.lightweight.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection.extensions/4.3.0/system.reflection.extensions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection.metadata/6.0.1/system.reflection.metadata.6.0.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection.primitives/4.3.0/system.reflection.primitives.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.reflection.typeextensions/4.3.0/system.reflection.typeextensions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.resources.resourcemanager/4.3.0/system.resources.resourcemanager.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.runtime/4.3.0/system.runtime.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.runtime.compilerservices.unsafe/6.0.0/system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.runtime.extensions/4.3.0/system.runtime.extensions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.runtime.handles/4.3.0/system.runtime.handles.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.runtime.interopservices/4.3.0/system.runtime.interopservices.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.runtime.interopservices.runtimeinformation/4.3.0/system.runtime.interopservices.runtimeinformation.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.runtime.numerics/4.3.0/system.runtime.numerics.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.cryptography.algorithms/4.3.0/system.security.cryptography.algorithms.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.cryptography.cng/4.3.0/system.security.cryptography.cng.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.cryptography.csp/4.3.0/system.security.cryptography.csp.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.cryptography.encoding/4.3.0/system.security.cryptography.encoding.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.cryptography.openssl/4.3.0/system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.cryptography.primitives/4.3.0/system.security.cryptography.primitives.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.cryptography.x509certificates/4.3.0/system.security.cryptography.x509certificates.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.security.principal.windows/4.5.0/system.security.principal.windows.4.5.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.text.encoding/4.3.0/system.text.encoding.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.text.encoding.codepages/6.0.0/system.text.encoding.codepages.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.text.encoding.extensions/4.3.0/system.text.encoding.extensions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.text.encodings.web/8.0.0/system.text.encodings.web.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.text.json/8.0.0/system.text.json.8.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.text.regularexpressions/4.3.0/system.text.regularexpressions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.threading/4.3.0/system.threading.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.threading.channels/6.0.0/system.threading.channels.6.0.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.threading.tasks/4.3.0/system.threading.tasks.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.threading.tasks.extensions/4.3.0/system.threading.tasks.extensions.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.threading.timer/4.3.0/system.threading.timer.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.xml.readerwriter/4.3.0/system.xml.readerwriter.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/system.xml.xdocument/4.3.0/system.xml.xdocument.4.3.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit/2.6.1/xunit.2.6.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit.abstractions/2.0.3/xunit.abstractions.2.0.3.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit.analyzers/1.4.0/xunit.analyzers.1.4.0.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit.assert/2.6.1/xunit.assert.2.6.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit.core/2.6.1/xunit.core.2.6.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit.extensibility.core/2.6.1/xunit.extensibility.core.2.6.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit.extensibility.execution/2.6.1/xunit.extensibility.execution.2.6.1.nupkg.sha512",
|
|
||||||
"/root/.nuget/packages/xunit.runner.visualstudio/2.5.3/xunit.runner.visualstudio.2.5.3.nupkg.sha512"
|
|
||||||
],
|
|
||||||
"logs": []
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue