|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
using System.Linq;
|
|
|
using System.Threading.Tasks;
|
|
|
using Haoliang.Models.Production;
|
|
|
using Haoliang.Models.Device;
|
|
|
using Haoliang.Models.DataCollection;
|
|
|
using Haoliang.Data.Repositories;
|
|
|
using Haoliang.Core.Services;
|
|
|
|
|
|
namespace Haoliang.Core.Services
|
|
|
{
|
|
|
public interface IProductionCalculator
|
|
|
{
|
|
|
Task<int> CalculateProductionAsync(DeviceCurrentStatus current, DeviceCurrentStatus last);
|
|
|
Task<int> CalculateProgramSwitchProductionAsync(DeviceCurrentStatus current, DeviceCurrentStatus last);
|
|
|
Task<bool> IsNewProgramAsync(string currentProgram, string lastProgram);
|
|
|
Task HandleCrossDayResetAsync(DeviceCurrentStatus current, DeviceCurrentStatus last);
|
|
|
Task<int> ValidateProductionValueAsync(int quantity, DeviceCurrentStatus current);
|
|
|
Task<ProductionRecord> CreateProductionRecordAsync(int deviceId, DeviceCurrentStatus current, DeviceCurrentStatus last, int quantity);
|
|
|
}
|
|
|
|
|
|
public interface IProductionService
|
|
|
{
|
|
|
Task CalculateProductionAsync(int deviceId);
|
|
|
Task CalculateAllProductionAsync();
|
|
|
Task<ProductionRecord> GetTodayProductionAsync(int deviceId);
|
|
|
Task<IEnumerable<ProductionRecord>> GetProductionByDateAsync(int deviceId, DateTime date);
|
|
|
Task<ProductionStatistics> GetProductionStatisticsAsync(int deviceId, DateTime date);
|
|
|
Task<ProductionSummary> GetProductionSummaryAsync(DateTime date);
|
|
|
Task<decimal> GetQualityRateAsync(int deviceId, DateTime date);
|
|
|
Task<bool> HasProductionDataAsync(int deviceId, DateTime date);
|
|
|
Task ArchiveProductionDataAsync(int daysToKeep = 90);
|
|
|
}
|
|
|
|
|
|
public interface IProductionScheduler
|
|
|
{
|
|
|
Task StartProductionCalculationAsync();
|
|
|
Task StopProductionCalculationAsync();
|
|
|
Task ScheduleProductionCalculationAsync(int deviceId);
|
|
|
Task<DateTime> GetNextCalculationTimeAsync();
|
|
|
Task<bool> IsCalculationRunningAsync();
|
|
|
}
|
|
|
|
|
|
public class ProductionCalculator : IProductionCalculator
|
|
|
{
|
|
|
private readonly IProductionRepository _productionRepository;
|
|
|
private readonly IProgramProductionSummaryRepository _summaryRepository;
|
|
|
|
|
|
public ProductionCalculator(
|
|
|
IProductionRepository productionRepository,
|
|
|
IProgramProductionSummaryRepository summaryRepository)
|
|
|
{
|
|
|
_productionRepository = productionRepository;
|
|
|
_summaryRepository = summaryRepository;
|
|
|
}
|
|
|
|
|
|
public async Task<int> CalculateProductionAsync(DeviceCurrentStatus current, DeviceCurrentStatus last)
|
|
|
{
|
|
|
// 同一程序连续加工
|
|
|
if (current.NCProgram == last.NCProgram)
|
|
|
{
|
|
|
int diff = current.CumulativeCount - last.CumulativeCount;
|
|
|
return Math.Max(0, await ValidateProductionValueAsync(diff, current)); // 异常值保护,避免负数
|
|
|
}
|
|
|
|
|
|
// 程序切换逻辑
|
|
|
return await CalculateProgramSwitchProductionAsync(current, last);
|
|
|
}
|
|
|
|
|
|
public async Task<int> CalculateProgramSwitchProductionAsync(DeviceCurrentStatus current, DeviceCurrentStatus last)
|
|
|
{
|
|
|
// 检查是否切换到新程序
|
|
|
if (await IsNewProgramAsync(current.NCProgram, last.NCProgram))
|
|
|
{
|
|
|
// 新程序以当前累计数为起点
|
|
|
return current.CumulativeCount;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
// 切回历史程序,视为重新开始
|
|
|
return 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public async Task<bool> IsNewProgramAsync(string currentProgram, string lastProgram)
|
|
|
{
|
|
|
// 实现程序切换判断逻辑
|
|
|
return currentProgram != lastProgram && !string.IsNullOrEmpty(currentProgram);
|
|
|
}
|
|
|
|
|
|
public async Task HandleCrossDayResetAsync(DeviceCurrentStatus current, DeviceCurrentStatus last)
|
|
|
{
|
|
|
// 跨天处理:0点自动重置
|
|
|
if (current.RecordTime.Date != last.RecordTime.Date)
|
|
|
{
|
|
|
// 新日期以首次采集累计值为起点
|
|
|
// 这个逻辑需要在业务服务中实现
|
|
|
var todayProduction = await _productionRepository.GetByDeviceAndDateAsync(
|
|
|
current.DeviceId, current.RecordTime.Date);
|
|
|
|
|
|
if (!todayProduction.Any())
|
|
|
{
|
|
|
// 如果当天没有生产记录,创建一条记录记录当前累计数
|
|
|
var firstRecord = new ProductionRecord
|
|
|
{
|
|
|
DeviceId = current.DeviceId,
|
|
|
NCProgram = current.NCProgram,
|
|
|
ProductionDate = current.RecordTime.Date,
|
|
|
Quantity = 0,
|
|
|
QualityRate = 100,
|
|
|
CreatedAt = DateTime.Now
|
|
|
};
|
|
|
|
|
|
await _productionRepository.AddAsync(firstRecord);
|
|
|
await _productionRepository.SaveAsync();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public async Task<int> ValidateProductionValueAsync(int quantity, DeviceCurrentStatus current)
|
|
|
{
|
|
|
// 跳变检测:产量变化超过阈值时跳过
|
|
|
const int maxJumpThreshold = 1000; // 单次最大产量变化
|
|
|
|
|
|
if (quantity > maxJumpThreshold)
|
|
|
{
|
|
|
await LogWarningAsync($"Production jump detected: {quantity} exceeds threshold {maxJumpThreshold}");
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
// 负数保护:产量为负数时归零
|
|
|
if (quantity < 0)
|
|
|
{
|
|
|
await LogWarningAsync("Negative production quantity detected, setting to 0");
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
return quantity;
|
|
|
}
|
|
|
|
|
|
public async Task<ProductionRecord> CreateProductionRecordAsync(int deviceId, DeviceCurrentStatus current, DeviceCurrentStatus last, int quantity)
|
|
|
{
|
|
|
var productionRecord = new ProductionRecord
|
|
|
{
|
|
|
DeviceId = deviceId,
|
|
|
NCProgram = current.NCProgram,
|
|
|
ProductionDate = current.RecordTime.Date,
|
|
|
Quantity = quantity,
|
|
|
QualityRate = 100, // 默认合格率,可以根据实际情况调整
|
|
|
StartTime = last != null ? last.RecordTime : (DateTime?)null,
|
|
|
EndTime = current.RecordTime,
|
|
|
CreatedAt = DateTime.Now
|
|
|
};
|
|
|
|
|
|
await _productionRepository.AddAsync(productionRecord);
|
|
|
await _productionRepository.SaveAsync();
|
|
|
|
|
|
// 更新程序产量汇总
|
|
|
await _summaryRepository.UpdateSummaryAsync(
|
|
|
deviceId,
|
|
|
current.NCProgram,
|
|
|
current.RecordTime.Date,
|
|
|
quantity,
|
|
|
productionRecord.QualityRate);
|
|
|
|
|
|
return productionRecord;
|
|
|
}
|
|
|
|
|
|
private async Task LogWarningAsync(string message)
|
|
|
{
|
|
|
// 这里可以实现日志记录逻辑
|
|
|
Console.WriteLine($"Warning: {message}");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public class ProductionService : IProductionService
|
|
|
{
|
|
|
private readonly IProductionRepository _productionRepository;
|
|
|
private readonly IProgramProductionSummaryRepository _summaryRepository;
|
|
|
private readonly IDeviceRepository _deviceRepository;
|
|
|
private readonly IProductionCalculator _calculator;
|
|
|
private readonly IProductionScheduler _scheduler;
|
|
|
private readonly ICollectionResultRepository _collectionRepository;
|
|
|
private readonly ILoggerService _logger;
|
|
|
|
|
|
public ProductionService(
|
|
|
IProductionRepository productionRepository,
|
|
|
IProgramProductionSummaryRepository summaryRepository,
|
|
|
IDeviceRepository deviceRepository,
|
|
|
IProductionCalculator calculator,
|
|
|
IProductionScheduler scheduler,
|
|
|
ICollectionResultRepository collectionRepository,
|
|
|
ILoggerService logger)
|
|
|
{
|
|
|
_productionRepository = productionRepository;
|
|
|
_summaryRepository = summaryRepository;
|
|
|
_deviceRepository = deviceRepository;
|
|
|
_calculator = calculator;
|
|
|
_scheduler = scheduler;
|
|
|
_collectionRepository = collectionRepository;
|
|
|
_logger = logger;
|
|
|
}
|
|
|
|
|
|
public async Task CalculateProductionAsync(int deviceId)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
|
if (device == null || !device.IsAvailable)
|
|
|
return;
|
|
|
|
|
|
var collectionResults = await _collectionRepository.GetResultsByDeviceIdAsync(deviceId)
|
|
|
.OrderBy(cr => cr.CollectionTime)
|
|
|
.ToListAsync();
|
|
|
|
|
|
for (int i = 1; i < collectionResults.Count; i++)
|
|
|
{
|
|
|
var current = collectionResults[i];
|
|
|
var last = collectionResults[i - 1];
|
|
|
|
|
|
if (current.IsSuccess && last.IsSuccess)
|
|
|
{
|
|
|
var currentStatus = ParseCollectionResult(current);
|
|
|
var lastStatus = ParseCollectionResult(last);
|
|
|
|
|
|
if (currentStatus != null && lastStatus != null)
|
|
|
{
|
|
|
// 处理跨天重置
|
|
|
await _calculator.HandleCrossDayResetAsync(currentStatus, lastStatus);
|
|
|
|
|
|
// 计算产量
|
|
|
var quantity = await _calculator.CalculateProductionAsync(currentStatus, lastStatus);
|
|
|
|
|
|
if (quantity > 0)
|
|
|
{
|
|
|
// 创建生产记录
|
|
|
var productionRecord = await _calculator.CreateProductionRecordAsync(
|
|
|
deviceId, currentStatus, lastStatus, quantity);
|
|
|
|
|
|
await _logger.LogInformationAsync(
|
|
|
$"Production calculated: Device {device.DeviceCode}, " +
|
|
|
$"Program {currentStatus.NCProgram}, Quantity {quantity}");
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
await _logger.LogErrorAsync(
|
|
|
$"Failed to calculate production for device {deviceId}: {ex.Message}");
|
|
|
throw;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public async Task CalculateAllProductionAsync()
|
|
|
{
|
|
|
var devices = await _deviceRepository.GetAvailableDevicesAsync();
|
|
|
|
|
|
foreach (var device in devices)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
await CalculateProductionAsync(device.Id);
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
await _logger.LogErrorAsync(
|
|
|
$"Failed to calculate production for device {device.DeviceCode}: {ex.Message}");
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public async Task<ProductionRecord> GetTodayProductionAsync(int deviceId)
|
|
|
{
|
|
|
var today = DateTime.Today;
|
|
|
var productions = await _productionRepository.GetByDeviceAndDateAsync(deviceId, today);
|
|
|
|
|
|
return productions.OrderByDescending(p => p.CreatedAt).FirstOrDefault();
|
|
|
}
|
|
|
|
|
|
public async Task<IEnumerable<ProductionRecord>> GetProductionByDateAsync(int deviceId, DateTime date)
|
|
|
{
|
|
|
return await _productionRepository.GetByDeviceAndDateAsync(deviceId, date);
|
|
|
}
|
|
|
|
|
|
public async Task<ProductionStatistics> GetProductionStatisticsAsync(int deviceId, DateTime date)
|
|
|
{
|
|
|
var productions = await _productionRepository.GetByDeviceAndDateAsync(deviceId, date);
|
|
|
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
|
|
|
|
if (!productions.Any())
|
|
|
{
|
|
|
return new ProductionStatistics
|
|
|
{
|
|
|
Date = date,
|
|
|
DeviceId = deviceId,
|
|
|
DeviceName = device?.DeviceName ?? "",
|
|
|
TotalQuantity = 0,
|
|
|
ValidQuantity = 0,
|
|
|
InvalidQuantity = 0,
|
|
|
QualityRate = 100
|
|
|
};
|
|
|
}
|
|
|
|
|
|
var totalQuantity = productions.Sum(p => p.Quantity);
|
|
|
var qualityRate = productions.Any() ?
|
|
|
productions.Average(p => p.QualityRate) : 100;
|
|
|
|
|
|
return new ProductionStatistics
|
|
|
{
|
|
|
Date = date,
|
|
|
DeviceId = deviceId,
|
|
|
DeviceName = device?.DeviceName ?? "",
|
|
|
TotalQuantity = totalQuantity,
|
|
|
ValidQuantity = (int)(totalQuantity * qualityRate / 100),
|
|
|
InvalidQuantity = totalQuantity - (int)(totalQuantity * qualityRate / 100),
|
|
|
QualityRate = qualityRate
|
|
|
};
|
|
|
}
|
|
|
|
|
|
public async Task<ProductionSummary> GetProductionSummaryAsync(DateTime date)
|
|
|
{
|
|
|
var devices = await _deviceRepository.GetAllAsync();
|
|
|
var summaries = new List<ProductionStatistics>();
|
|
|
|
|
|
foreach (var device in devices)
|
|
|
{
|
|
|
var stats = await GetProductionStatisticsAsync(device.Id, date);
|
|
|
summaries.Add(stats);
|
|
|
}
|
|
|
|
|
|
var totalQuantity = summaries.Sum(s => s.TotalQuantity);
|
|
|
var totalValidQuantity = summaries.Sum(s => s.ValidQuantity);
|
|
|
var overallQualityRate = totalQuantity > 0 ?
|
|
|
(decimal)totalValidQuantity / totalQuantity * 100 : 100;
|
|
|
|
|
|
return new ProductionSummary
|
|
|
{
|
|
|
Date = date,
|
|
|
DeviceCount = summaries.Count,
|
|
|
TotalQuantity = totalQuantity,
|
|
|
TotalValidQuantity = totalValidQuantity,
|
|
|
TotalInvalidQuantity = totalQuantity - totalValidQuantity,
|
|
|
OverallQualityRate = overallQualityRate,
|
|
|
DeviceStatistics = summaries
|
|
|
};
|
|
|
}
|
|
|
|
|
|
public async Task<decimal> GetQualityRateAsync(int deviceId, DateTime date)
|
|
|
{
|
|
|
return await _productionRepository.GetQualityRateAsync(deviceId, date);
|
|
|
}
|
|
|
|
|
|
public async Task<bool> HasProductionDataAsync(int deviceId, DateTime date)
|
|
|
{
|
|
|
return await _productionRepository.HasProductionDataAsync(deviceId, date);
|
|
|
}
|
|
|
|
|
|
public async Task ArchiveProductionDataAsync(int daysToKeep = 90)
|
|
|
{
|
|
|
var cutoffDate = DateTime.Now.AddDays(-daysToKeep);
|
|
|
var oldProductions = await _productionRepository.FindAsync(
|
|
|
p => p.CreatedAt < cutoffDate);
|
|
|
|
|
|
if (oldProductions.Any())
|
|
|
{
|
|
|
await _productionRepository.RemoveRange(oldProductions);
|
|
|
await _productionRepository.SaveAsync();
|
|
|
|
|
|
await _logger.LogInformationAsync(
|
|
|
$"Archived {oldProductions.Count()} production records older than {daysToKeep} days");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private DeviceCurrentStatus ParseCollectionResult(CollectionResult result)
|
|
|
{
|
|
|
// 这里需要根据实际的CollectionResult结构来解析
|
|
|
// 暂时返回一个空的实现
|
|
|
return new DeviceCurrentStatus
|
|
|
{
|
|
|
DeviceId = result.DeviceId,
|
|
|
RecordTime = result.CollectionTime,
|
|
|
NCProgram = "",
|
|
|
CumulativeCount = 0
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public class ProductionScheduler : IProductionScheduler
|
|
|
{
|
|
|
private readonly IProductionService _productionService;
|
|
|
private readonly ILoggerService _logger;
|
|
|
private Timer _calculationTimer;
|
|
|
private bool _isRunning;
|
|
|
|
|
|
public ProductionScheduler(
|
|
|
IProductionService productionService,
|
|
|
ILoggerService logger)
|
|
|
{
|
|
|
_productionService = productionService;
|
|
|
_logger = logger;
|
|
|
}
|
|
|
|
|
|
public async Task StartProductionCalculationAsync()
|
|
|
{
|
|
|
if (_isRunning)
|
|
|
return;
|
|
|
|
|
|
_isRunning = true;
|
|
|
|
|
|
// 立即执行一次计算
|
|
|
await _productionService.CalculateAllProductionAsync();
|
|
|
|
|
|
// 设置定时器,每5分钟执行一次
|
|
|
_calculationTimer = new Timer(async _ =>
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
await _productionService.CalculateAllProductionAsync();
|
|
|
await _logger.LogInformationAsync("Scheduled production calculation completed");
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
await _logger.LogErrorAsync($"Scheduled production calculation failed: {ex.Message}");
|
|
|
}
|
|
|
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
|
|
|
|
|
|
await _logger.LogInformationAsync("Production calculation scheduler started");
|
|
|
}
|
|
|
|
|
|
public async Task StopProductionCalculationAsync()
|
|
|
{
|
|
|
if (!_isRunning)
|
|
|
return;
|
|
|
|
|
|
_isRunning = false;
|
|
|
_calculationTimer?.Dispose();
|
|
|
_calculationTimer = null;
|
|
|
|
|
|
await _logger.LogInformationAsync("Production calculation scheduler stopped");
|
|
|
}
|
|
|
|
|
|
public async Task ScheduleProductionCalculationAsync(int deviceId)
|
|
|
{
|
|
|
if (!_isRunning)
|
|
|
{
|
|
|
await _logger.LogWarningAsync("Production calculation scheduler is not running");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
await _productionService.CalculateProductionAsync(deviceId);
|
|
|
}
|
|
|
|
|
|
public async Task<DateTime> GetNextCalculationTimeAsync()
|
|
|
{
|
|
|
if (!_isRunning || _calculationTimer == null)
|
|
|
return DateTime.MinValue;
|
|
|
|
|
|
// 这里可以根据定时器的配置返回下一个执行时间
|
|
|
return DateTime.Now.AddMinutes(5);
|
|
|
}
|
|
|
|
|
|
public async Task<bool> IsCalculationRunningAsync()
|
|
|
{
|
|
|
return _isRunning;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public class ProductionSummary
|
|
|
{
|
|
|
public DateTime Date { get; set; }
|
|
|
public int DeviceCount { get; set; }
|
|
|
public int TotalQuantity { get; set; }
|
|
|
public int TotalValidQuantity { get; set; }
|
|
|
public int TotalInvalidQuantity { get; set; }
|
|
|
public decimal OverallQualityRate { get; set; }
|
|
|
public List<ProductionStatistics> DeviceStatistics { get; set; }
|
|
|
}
|
|
|
|
|
|
public interface ILoggerService
|
|
|
{
|
|
|
Task LogInformationAsync(string message);
|
|
|
Task LogWarningAsync(string message);
|
|
|
Task LogErrorAsync(string message);
|
|
|
Task LogDebugAsync(string message);
|
|
|
}
|
|
|
} |