Files
OrpaonVision/OrpaonVision.ConfigApp/Infrastructure/Services/ProductionSessionManagerService.cs
2026-04-06 22:04:05 +08:00

1051 lines
43 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrpaonVision.Core.Production;
using OrpaonVision.Core.Results;
using OrpaonVision.Core.Common;
using OrpaonVision.Model.Production;
using OrpaonVision.ConfigApp.Infrastructure.Options;
using OrpaonVision.ConfigApp.Infrastructure.Persistence.Options;
using System.Collections.Concurrent;
namespace OrpaonVision.ConfigApp.Infrastructure.Services;
/// <summary>
/// 产品会话管理服务实现。
/// </summary>
public sealed class ProductionSessionManagerService : IProductionSessionManagerService
{
private readonly ILogger<ProductionSessionManagerService> _logger;
private readonly PersistenceOptions _persistenceOptions;
private readonly ConcurrentDictionary<Guid, ProductionSessionModel> _activeSessions = new();
private readonly ConcurrentDictionary<string, List<ProductionSessionModel>> _stationSessions = new();
private readonly ConcurrentDictionary<Guid, List<ProductionSessionEvent>> _sessionEvents = new();
private readonly object _lock = new();
/// <summary>
/// 构造函数。
/// </summary>
public ProductionSessionManagerService(
ILogger<ProductionSessionManagerService> logger,
IOptions<PersistenceOptions> persistenceOptions)
{
_logger = logger;
_persistenceOptions = persistenceOptions.Value;
}
/// <inheritdoc />
public async Task<Result<ProductionSessionModel>> CreateSessionOnArrivalAsync(CreateSessionOnArrivalRequest request, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("进站建会话开始: 产品类型={ProductType}, 工位={Station}, 操作员={Operator}",
request.ProductTypeCode, request.StationId, request.OperatorId);
lock (_lock)
{
// 检查工位是否已有活动会话
var existingSession = GetCurrentActiveSessionInternal(request.StationId);
if (existingSession != null)
{
return Result<ProductionSessionModel>.Fail("ACTIVE_SESSION_EXISTS",
$"工位 {request.StationId} 已存在活动会话 {existingSession.SessionId}");
}
// 创建新会话
var session = new ProductionSessionModel
{
SessionId = Guid.NewGuid(),
ProductTypeCode = request.ProductTypeCode,
ProductTypeName = request.ProductTypeName,
StationId = request.StationId,
StationName = request.StationName,
OperatorId = request.OperatorId,
OperatorName = request.OperatorName,
ShiftId = request.ShiftId,
ShiftName = request.ShiftName,
StartedAtUtc = request.ArrivalTimeUtc,
Status = ProductionSessionStatus.InProgress,
Result = ProductionSessionResult.Pending,
CurrentLayer = 0,
TotalLayers = request.TotalLayers,
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow,
CreatedBy = request.CreatedBy,
UpdatedBy = request.CreatedBy,
Remark = request.Remark
};
// 添加到内存存储
_activeSessions.TryAdd(session.SessionId, session);
var stationSessions = _stationSessions.GetOrAdd(request.StationId, _ => new List<ProductionSessionModel>());
stationSessions.Add(session);
// 记录会话事件
RecordSessionEvent(session.SessionId, ProductionSessionEventType.SessionCreated, "进站建会话", request.CreatedBy);
_logger.LogInformation("进站建会话成功: 会话ID={SessionId}, 产品类型={ProductType}",
session.SessionId, request.ProductTypeCode);
return Result<ProductionSessionModel?>.Success(session);
}
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "进站建会话失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "CREATE_SESSION_FAILED", "进站建会话失败。", traceId);
return Result<ProductionSessionModel>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result> ArchiveSessionOnDepartureAsync(Guid sessionId, ProductionSessionResult result, string? ngReason = null, string? remark = null, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("离站归档会话开始: 会话ID={SessionId}, 结果={Result}", sessionId, result);
lock (_lock)
{
if (!_activeSessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在");
}
// 更新会话状态
session.EndedAtUtc = DateTime.UtcNow;
session.Result = result;
session.Status = result == ProductionSessionResult.Ok ? ProductionSessionStatus.CompletedOk : ProductionSessionStatus.CompletedNg;
session.NgReason = ngReason;
session.Remark = remark;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
// 从活动会话中移除
_activeSessions.TryRemove(sessionId, out _);
if (_stationSessions.TryGetValue(session.StationId, out var stationSessions))
{
stationSessions.Remove(session);
}
// 记录会话事件
RecordSessionEvent(sessionId, ProductionSessionEventType.SessionArchived,
$"离站归档: 结果={result}, NG原因={ngReason}", "System");
// 模拟持久化到数据库
PersistSessionToDatabase(session);
_logger.LogInformation("离站归档会话成功: 会话ID={SessionId}", sessionId);
return Result.Success();
}
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "离站归档会话失败。TraceId: {TraceId}", traceId);
var errorResult = Result.FromException(ex, "ARCHIVE_SESSION_FAILED", "离站归档会话失败。", traceId);
return Result.FailWithTrace(errorResult.Code, errorResult.Message, errorResult.TraceId ?? traceId, errorResult.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result> UpdateSessionProgressAsync(Guid sessionId, int currentLayer, ProductionSessionStatus layerStatus, CancellationToken cancellationToken = default)
{
try
{
_logger.LogDebug("更新会话进度: 会话ID={SessionId}, 当前层级={CurrentLayer}, 状态={Status}", sessionId, currentLayer, layerStatus);
lock (_lock)
{
if (!_activeSessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在");
}
session.CurrentLayer = currentLayer;
session.Status = layerStatus;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
// 记录会话事件
RecordSessionEvent(sessionId, ProductionSessionEventType.ProgressUpdated,
$"进度更新: 层级={currentLayer}, 状态={layerStatus}", "System");
return Result.Success();
}
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "更新会话进度失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "UPDATE_PROGRESS_FAILED", "更新会话进度失败。", traceId);
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result> PauseSessionAsync(Guid sessionId, string reason, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("暂停会话: 会话ID={SessionId}, 原因={Reason}", sessionId, reason);
lock (_lock)
{
if (!_activeSessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在");
}
session.Status = ProductionSessionStatus.Paused;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
session.Remark = $"暂停: {reason}";
// 记录会话事件
RecordSessionEvent(sessionId, ProductionSessionEventType.SessionPaused, reason, "System");
return Result.Success();
}
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "暂停会话失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "PAUSE_SESSION_FAILED", "暂停会话失败。", traceId);
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result> ResumeSessionAsync(Guid sessionId, string reason, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("恢复会话: 会话ID={SessionId}, 原因={Reason}", sessionId, reason);
lock (_lock)
{
if (!_activeSessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在");
}
session.Status = ProductionSessionStatus.InProgress;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
session.Remark = $"恢复: {reason}";
// 记录会话事件
RecordSessionEvent(sessionId, ProductionSessionEventType.SessionResumed, reason, "System");
return Result.Success();
}
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "恢复会话失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "RESUME_SESSION_FAILED", "恢复会话失败。", traceId);
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result> CancelSessionAsync(Guid sessionId, string reason, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("取消会话: 会话ID={SessionId}, 原因={Reason}", sessionId, reason);
lock (_lock)
{
if (!_activeSessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在");
}
session.Status = ProductionSessionStatus.Cancelled;
session.EndedAtUtc = DateTime.UtcNow;
session.Result = ProductionSessionResult.Ng;
session.NgReason = $"取消: {reason}";
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
// 从活动会话中移除
_activeSessions.TryRemove(sessionId, out _);
if (_stationSessions.TryGetValue(session.StationId, out var stationSessions))
{
stationSessions.Remove(session);
}
// 记录会话事件
RecordSessionEvent(sessionId, ProductionSessionEventType.SessionCancelled, reason, "System");
// 模拟持久化到数据库
PersistSessionToDatabase(session);
return Result.Success();
}
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "取消会话失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "CANCEL_SESSION_FAILED", "取消会话失败。", traceId);
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result<ProductionSessionModel?>> GetCurrentActiveSessionAsync(string stationId, CancellationToken cancellationToken = default)
{
try
{
var session = GetCurrentActiveSessionInternal(stationId);
return Result<ProductionSessionModel?>.Success(session);
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "获取当前活动会话失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "GET_CURRENT_SESSION_FAILED", "获取当前活动会话失败。", traceId);
return Result<ProductionSessionModel?>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result<ProductionSessionModel?>> GetSessionAsync(Guid sessionId, CancellationToken cancellationToken = default)
{
try
{
if (_activeSessions.TryGetValue(sessionId, out var session))
{
return Result<ProductionSessionModel?>.Success(session);
}
// 如果活动会话中没有,从数据库查询
var dbSession = await GetSessionFromDatabaseAsync(sessionId, cancellationToken);
return Result<ProductionSessionModel?>.Success(dbSession);
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "获取会话详情失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "GET_SESSION_FAILED", "获取会话详情失败。", traceId);
return Result<ProductionSessionModel?>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result<PagedResult<ProductionSessionModel>>> GetSessionHistoryAsync(GetSessionHistoryRequest request, CancellationToken cancellationToken = default)
{
try
{
// 模拟从数据库查询历史记录
var allSessions = await GetAllSessionsFromDatabaseAsync(cancellationToken);
// 应用过滤条件
var filteredSessions = allSessions.AsEnumerable();
if (!string.IsNullOrWhiteSpace(request.ProductTypeCode))
{
filteredSessions = filteredSessions.Where(s => s.ProductTypeCode == request.ProductTypeCode);
}
if (!string.IsNullOrWhiteSpace(request.StationId))
{
filteredSessions = filteredSessions.Where(s => s.StationId == request.StationId);
}
if (!string.IsNullOrWhiteSpace(request.OperatorId))
{
filteredSessions = filteredSessions.Where(s => s.OperatorId == request.OperatorId);
}
if (request.Status.HasValue)
{
filteredSessions = filteredSessions.Where(s => s.Status == request.Status.Value);
}
if (request.Result.HasValue)
{
filteredSessions = filteredSessions.Where(s => s.Result == request.Result.Value);
}
if (request.StartTimeUtc.HasValue)
{
filteredSessions = filteredSessions.Where(s => s.StartedAtUtc >= request.StartTimeUtc.Value);
}
if (request.EndTimeUtc.HasValue)
{
filteredSessions = filteredSessions.Where(s => s.EndedAtUtc <= request.EndTimeUtc.Value);
}
// 应用排序
filteredSessions = request.SortField switch
{
OrpaonVision.Core.Production.SessionSortField.StartTimeUtc => request.SortDirection == OrpaonVision.Core.Production.SortDirection.Ascending
? filteredSessions.OrderBy(s => s.StartedAtUtc)
: filteredSessions.OrderByDescending(s => s.StartedAtUtc),
OrpaonVision.Core.Production.SessionSortField.EndTimeUtc => request.SortDirection == OrpaonVision.Core.Production.SortDirection.Ascending
? filteredSessions.OrderBy(s => s.EndedAtUtc)
: filteredSessions.OrderByDescending(s => s.EndedAtUtc),
OrpaonVision.Core.Production.SessionSortField.ProductTypeCode => request.SortDirection == OrpaonVision.Core.Production.SortDirection.Ascending
? filteredSessions.OrderBy(s => s.ProductTypeCode)
: filteredSessions.OrderByDescending(s => s.ProductTypeCode),
OrpaonVision.Core.Production.SessionSortField.StationId => request.SortDirection == OrpaonVision.Core.Production.SortDirection.Ascending
? filteredSessions.OrderBy(s => s.StationId)
: filteredSessions.OrderByDescending(s => s.StationId),
OrpaonVision.Core.Production.SessionSortField.OperatorName => request.SortDirection == OrpaonVision.Core.Production.SortDirection.Ascending
? filteredSessions.OrderBy(s => s.OperatorName)
: filteredSessions.OrderByDescending(s => s.OperatorName),
OrpaonVision.Core.Production.SessionSortField.CurrentLayer => request.SortDirection == OrpaonVision.Core.Production.SortDirection.Ascending
? filteredSessions.OrderBy(s => s.CurrentLayer)
: filteredSessions.OrderByDescending(s => s.CurrentLayer),
_ => filteredSessions.OrderByDescending(s => s.StartedAtUtc)
};
// 应用分页
var totalCount = filteredSessions.Count();
var pagedSessions = filteredSessions
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
var result = new PagedResult<ProductionSessionModel>
{
Items = pagedSessions,
TotalCount = totalCount,
PageIndex = request.PageIndex,
PageSize = request.PageSize
};
return Result<PagedResult<ProductionSessionModel>>.Success(result);
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "获取会话历史记录失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "GET_SESSION_HISTORY_FAILED", "获取会话历史记录失败。", traceId);
return Result<PagedResult<ProductionSessionModel>>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public async Task<Result<ProductionSessionStatistics>> GetSessionStatisticsAsync(GetSessionStatisticsRequest request, CancellationToken cancellationToken = default)
{
try
{
// 模拟从数据库查询统计数据
var allSessions = await GetAllSessionsFromDatabaseAsync(cancellationToken);
// 应用过滤条件
var filteredSessions = allSessions.AsEnumerable();
if (!string.IsNullOrWhiteSpace(request.ProductTypeCode))
{
filteredSessions = filteredSessions.Where(s => s.ProductTypeCode == request.ProductTypeCode);
}
if (!string.IsNullOrWhiteSpace(request.StationId))
{
filteredSessions = filteredSessions.Where(s => s.StationId == request.StationId);
}
if (!string.IsNullOrWhiteSpace(request.OperatorId))
{
filteredSessions = filteredSessions.Where(s => s.OperatorId == request.OperatorId);
}
filteredSessions = filteredSessions.Where(s => s.StartedAtUtc >= request.StartTimeUtc && s.StartedAtUtc <= request.EndTimeUtc);
// 计算平均处理时间
var completedSessions = filteredSessions.Where(s => s.EndedAtUtc.HasValue).ToList();
double averageProcessingTimeSeconds = 0;
double throughputPerHour = 0;
if (completedSessions.Any())
{
averageProcessingTimeSeconds = completedSessions.Average(s => (s.EndedAtUtc!.Value - s.StartedAtUtc).TotalSeconds);
throughputPerHour = completedSessions.Count / completedSessions.Average(s => (s.EndedAtUtc!.Value - s.StartedAtUtc).TotalHours);
}
// 按维度分组统计
var byProductType = new List<ProductTypeStatistics>();
var byStation = new List<StationStatistics>();
var byOperator = new List<OperatorStatistics>();
var byDate = new List<DailyStatistics>();
if (request.Dimensions.Contains(OrpaonVision.Core.Common.StatisticsDimension.ByProductType))
{
byProductType = CalculateProductTypeStatistics(filteredSessions);
}
if (request.Dimensions.Contains(OrpaonVision.Core.Common.StatisticsDimension.ByStation))
{
byStation = CalculateStationStatistics(filteredSessions);
}
if (request.Dimensions.Contains(OrpaonVision.Core.Common.StatisticsDimension.ByOperator))
{
byOperator = CalculateOperatorStatistics(filteredSessions);
}
if (request.Dimensions.Contains(OrpaonVision.Core.Common.StatisticsDimension.ByDate))
{
byDate = CalculateDailyStatistics(filteredSessions);
}
// 计算基础统计
var successRate = filteredSessions.Count() > 0 ? (double)filteredSessions.Count(s => s.Result == ProductionSessionResult.Ok) / filteredSessions.Count() * 100 : 0;
var statistics = new ProductionSessionStatistics
{
TotalSessions = filteredSessions.Count(),
OkSessions = filteredSessions.Count(s => s.Result == ProductionSessionResult.Ok),
NgSessions = filteredSessions.Count(s => s.Result == ProductionSessionResult.Ng),
InProgressSessions = filteredSessions.Count(s => s.Status == ProductionSessionStatus.InProgress),
CancelledSessions = filteredSessions.Count(s => s.Status == ProductionSessionStatus.Cancelled),
PausedSessions = filteredSessions.Count(s => s.Status == ProductionSessionStatus.Paused),
AverageProcessingTimeSeconds = averageProcessingTimeSeconds,
ThroughputPerHour = throughputPerHour,
SuccessRate = successRate,
ByProductType = byProductType,
ByStation = byStation,
ByOperator = byOperator,
ByDate = byDate
};
return Result<ProductionSessionStatistics>.Success(statistics);
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "获取会话统计信息失败。TraceId: {TraceId}", traceId);
return Result<ProductionSessionStatistics>.FailWithTrace("GET_STATISTICS_FAILED", "获取会话统计信息失败。", traceId, ex.Message);
}
}
/// <inheritdoc />
public async Task<Result<byte[]>> ExportSessionsAsync(ExportSessionsRequest request, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("导出会话记录开始: 格式={Format}", request.Format);
// 获取要导出的会话数据
var historyRequest = new GetSessionHistoryRequest
{
ProductTypeCode = request.ProductTypeCode,
StationId = request.StationId,
OperatorId = request.OperatorId,
Status = request.Status,
Result = request.Result,
StartTimeUtc = request.StartTimeUtc,
EndTimeUtc = request.EndTimeUtc,
PageIndex = 1,
PageSize = int.MaxValue // 导出所有数据
};
var historyResult = await GetSessionHistoryAsync(historyRequest, cancellationToken);
if (!historyResult.Succeeded)
{
return Result<byte[]>.Fail(historyResult.Code, historyResult.Message, historyResult.Errors.ToArray());
}
// 根据格式导出数据
var exportData = request.Format switch
{
ExportFormat.Excel => await ExportToExcelAsync(historyResult.Data.Items, request, cancellationToken),
ExportFormat.Csv => await ExportToCsvAsync(historyResult.Data.Items, request, cancellationToken),
ExportFormat.Json => await ExportToJsonAsync(historyResult.Data.Items, request, cancellationToken),
_ => throw new NotSupportedException($"不支持的导出格式: {request.Format}")
};
_logger.LogInformation("导出会话记录成功: 记录数={Count}, 格式={Format}", historyResult.Data.Items.Count, request.Format);
return Result<byte[]>.Success(exportData);
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "导出会话记录失败。TraceId: {TraceId}", traceId);
return Result<byte[]>.FailWithTrace("EXPORT_SESSIONS_FAILED", "导出会话记录失败。", traceId, ex.Message);
}
}
/// <inheritdoc />
public async Task<Result<BatchArchiveResult>> BatchArchiveSessionsAsync(IReadOnlyList<Guid> sessionIds, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("批量归档会话开始: 会话数量={Count}", sessionIds.Count);
var successCount = 0;
var failureCount = 0;
var failedSessionIds = new List<Guid>();
var errors = new List<string>();
foreach (var sessionId in sessionIds)
{
try
{
// 检查会话是否存在且为活动状态
if (_activeSessions.TryGetValue(sessionId, out var session))
{
// 归档会话
var archiveResult = await ArchiveSessionOnDepartureAsync(sessionId, ProductionSessionResult.Ok, null, "批量归档", cancellationToken);
if (archiveResult.Succeeded)
{
successCount++;
}
else
{
failureCount++;
failedSessionIds.Add(sessionId);
errors.Add($"会话 {sessionId} 归档失败: {archiveResult.Message}");
}
}
else
{
failureCount++;
failedSessionIds.Add(sessionId);
errors.Add($"会话 {sessionId} 不存在或已归档");
}
}
catch (Exception ex)
{
failureCount++;
failedSessionIds.Add(sessionId);
errors.Add($"会话 {sessionId} 归档异常: {ex.Message}");
}
}
_logger.LogInformation("批量归档会话完成: 成功={SuccessCount}, 失败={FailureCount}", successCount, failureCount);
var result = new BatchArchiveResult
{
TotalCount = sessionIds.Count,
SuccessCount = successCount,
FailureCount = failureCount,
FailedSessionIds = failedSessionIds,
Errors = errors
};
return Result<BatchArchiveResult>.Success(result);
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "批量归档会话失败。TraceId: {TraceId}", traceId);
return Result<BatchArchiveResult>.FailWithTrace("BATCH_ARCHIVE_FAILED", "批量归档会话失败。", traceId, ex.Message);
}
}
/// <inheritdoc />
public async Task<Result<CleanupResult>> CleanupExpiredSessionsAsync(int retentionDays, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("清理过期会话开始: 保留天数={RetentionDays}", retentionDays);
var startTime = DateTime.UtcNow;
var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
// 获取所有已完成的会话
var allSessions = await GetAllSessionsFromDatabaseAsync(cancellationToken);
var expiredSessions = allSessions.Where(s => s.EndedAtUtc.HasValue && s.EndedAtUtc.Value < cutoffDate).ToList();
var cleanedCount = 0;
var freedSpace = 0L;
foreach (var session in expiredSessions)
{
try
{
// 模拟删除会话和相关数据
await DeleteSessionFromDatabaseAsync(session.SessionId, cancellationToken);
cleanedCount++;
// 模拟释放存储空间
freedSpace += 1024; // 假设每个会话占用1KB
}
catch (Exception ex)
{
_logger.LogWarning(ex, "清理过期会话失败: 会话ID={SessionId}", session.SessionId);
}
}
var elapsed = DateTime.UtcNow - startTime;
var result = new CleanupResult
{
CleanedCount = cleanedCount,
FreedSpaceBytes = freedSpace,
ElapsedMilliseconds = (long)elapsed.TotalMilliseconds
};
_logger.LogInformation("清理过期会话完成: 清理数量={CleanedCount}, 释放空间={FreedSpace}KB, 耗时={ElapsedMs}ms",
cleanedCount, freedSpace / 1024, result.ElapsedMilliseconds);
return Result<CleanupResult>.Success(result);
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "清理过期会话失败。TraceId: {TraceId}", traceId);
return Result<CleanupResult>.FailWithTrace("CLEANUP_FAILED", "清理过期会话失败。", traceId, ex.Message);
}
}
#region
/// <summary>
/// 获取当前活动会话(内部方法)。
/// </summary>
private ProductionSessionModel? GetCurrentActiveSessionInternal(string stationId)
{
if (_stationSessions.TryGetValue(stationId, out var sessions))
{
return sessions.LastOrDefault(s => s.Status == ProductionSessionStatus.InProgress || s.Status == ProductionSessionStatus.Paused);
}
return null;
}
/// <summary>
/// 记录会话事件。
/// </summary>
private void RecordSessionEvent(Guid sessionId, ProductionSessionEventType eventType, string description, string operatorName)
{
var events = _sessionEvents.GetOrAdd(sessionId, _ => new List<ProductionSessionEvent>());
var sessionEvent = new ProductionSessionEvent
{
EventId = Guid.NewGuid(),
SessionId = sessionId,
EventType = eventType,
Description = description,
OperatorName = operatorName,
EventTimeUtc = DateTime.UtcNow
};
events.Add(sessionEvent);
}
/// <summary>
/// 持久化会话到数据库(模拟)。
/// </summary>
private void PersistSessionToDatabase(ProductionSessionModel session)
{
// 模拟数据库持久化操作
_logger.LogDebug("持久化会话到数据库: 会话ID={SessionId}", session.SessionId);
}
/// <summary>
/// 从数据库获取会话(模拟)。
/// </summary>
private async Task<ProductionSessionModel?> GetSessionFromDatabaseAsync(Guid sessionId, CancellationToken cancellationToken = default)
{
// 模拟数据库查询
await Task.Delay(10, cancellationToken);
return null; // 简化实现返回null表示未找到
}
/// <summary>
/// 从数据库获取所有会话(模拟)。
/// </summary>
private async Task<List<ProductionSessionModel>> GetAllSessionsFromDatabaseAsync(CancellationToken cancellationToken = default)
{
// 模拟数据库查询,返回一些示例数据
await Task.Delay(10, cancellationToken);
return new List<ProductionSessionModel>
{
new ProductionSessionModel
{
SessionId = Guid.NewGuid(),
ProductTypeCode = "VF-A100",
ProductTypeName = "变频器A100",
StationId = "ST-001",
StationName = "装配工位1",
OperatorId = "OP-001",
OperatorName = "张三",
ShiftId = "SH-001",
ShiftName = "白班",
StartedAtUtc = DateTime.UtcNow.AddHours(-2),
EndedAtUtc = DateTime.UtcNow.AddHours(-1),
Status = ProductionSessionStatus.CompletedOk,
Result = ProductionSessionResult.Ok,
CurrentLayer = 3,
TotalLayers = 3,
CreatedAtUtc = DateTime.UtcNow.AddDays(-1),
UpdatedAtUtc = DateTime.UtcNow.AddHours(-1),
CreatedBy = "System",
UpdatedBy = "System"
}
};
}
/// <summary>
/// 从数据库删除会话(模拟)。
/// </summary>
private async Task DeleteSessionFromDatabaseAsync(Guid sessionId, CancellationToken cancellationToken = default)
{
// 模拟数据库删除操作
await Task.Delay(5, cancellationToken);
_logger.LogDebug("从数据库删除会话: 会话ID={SessionId}", sessionId);
}
/// <summary>
/// 计算产品类型统计。
/// </summary>
private List<ProductTypeStatistics> CalculateProductTypeStatistics(IEnumerable<ProductionSessionModel> sessions)
{
return sessions
.GroupBy(s => s.ProductTypeCode)
.Select(g => new ProductTypeStatistics
{
ProductTypeCode = g.Key,
ProductTypeName = g.First().ProductTypeName,
TotalSessions = g.Count(),
OkSessions = g.Count(s => s.Result == ProductionSessionResult.Ok),
NgSessions = g.Count(s => s.Result == ProductionSessionResult.Ng),
PassRate = g.Count() > 0 ? (double)g.Count(s => s.Result == ProductionSessionResult.Ok) / g.Count() * 100 : 0
})
.ToList();
}
/// <summary>
/// 计算工位统计。
/// </summary>
private List<StationStatistics> CalculateStationStatistics(IEnumerable<ProductionSessionModel> sessions)
{
return sessions
.GroupBy(s => s.StationId)
.Select(g => new StationStatistics
{
StationId = g.Key,
StationName = g.First().StationName,
TotalSessions = g.Count(),
OkSessions = g.Count(s => s.Result == ProductionSessionResult.Ok),
NgSessions = g.Count(s => s.Result == ProductionSessionResult.Ng),
PassRate = g.Count() > 0 ? (double)g.Count(s => s.Result == ProductionSessionResult.Ok) / g.Count() * 100 : 0,
ThroughputPerHour = CalculateThroughputPerHour(g)
})
.ToList();
}
/// <summary>
/// 计算操作员统计。
/// </summary>
private List<OperatorStatistics> CalculateOperatorStatistics(IEnumerable<ProductionSessionModel> sessions)
{
return sessions
.GroupBy(s => s.OperatorId)
.Select(g => new OperatorStatistics
{
OperatorId = g.Key,
OperatorName = g.First().OperatorName,
TotalSessions = g.Count(),
OkSessions = g.Count(s => s.Result == ProductionSessionResult.Ok),
NgSessions = g.Count(s => s.Result == ProductionSessionResult.Ng),
PassRate = g.Count() > 0 ? (double)g.Count(s => s.Result == ProductionSessionResult.Ok) / g.Count() * 100 : 0,
AverageProcessingTimeSeconds = CalculateAverageProcessingTime(g)
})
.ToList();
}
/// <summary>
/// 计算日期统计。
/// </summary>
private List<DailyStatistics> CalculateDailyStatistics(IEnumerable<ProductionSessionModel> sessions)
{
return sessions
.GroupBy(s => s.StartedAtUtc.Date)
.Select(g => new DailyStatistics
{
DateUtc = g.Key,
TotalSessions = g.Count(),
OkSessions = g.Count(s => s.Result == ProductionSessionResult.Ok),
NgSessions = g.Count(s => s.Result == ProductionSessionResult.Ng),
PassRate = g.Count() > 0 ? (double)g.Count(s => s.Result == ProductionSessionResult.Ok) / g.Count() * 100 : 0,
ThroughputPerHour = CalculateThroughputPerHour(g)
})
.ToList();
}
/// <summary>
/// 计算每小时处理量。
/// </summary>
private double CalculateThroughputPerHour(IEnumerable<ProductionSessionModel> sessions)
{
var completedSessions = sessions.Where(s => s.EndedAtUtc.HasValue).ToList();
if (!completedSessions.Any()) return 0;
var totalHours = completedSessions.Sum(s => (s.EndedAtUtc!.Value - s.StartedAtUtc).TotalHours);
return totalHours > 0 ? completedSessions.Count / totalHours : 0;
}
/// <summary>
/// 计算平均处理时间。
/// </summary>
private double CalculateAverageProcessingTime(IEnumerable<ProductionSessionModel> sessions)
{
var completedSessions = sessions.Where(s => s.EndedAtUtc.HasValue).ToList();
if (!completedSessions.Any()) return 0;
return completedSessions.Average(s => (s.EndedAtUtc!.Value - s.StartedAtUtc).TotalSeconds);
}
/// <summary>
/// 导出为Excel格式模拟
/// </summary>
private async Task<byte[]> ExportToExcelAsync(IReadOnlyList<ProductionSessionModel> sessions, ExportSessionsRequest request, CancellationToken cancellationToken = default)
{
await Task.Delay(100, cancellationToken);
// 模拟Excel文件内容
var csvContent = "SessionId,ProductTypeCode,StationId,OperatorName,StartedAtUtc,EndedAtUtc,Status,Result\n";
foreach (var session in sessions)
{
csvContent += $"{session.SessionId},{session.ProductTypeCode},{session.StationId},{session.OperatorName},{session.StartedAtUtc:yyyy-MM-dd HH:mm:ss},{session.EndedAtUtc:yyyy-MM-dd HH:mm:ss},{session.Status},{session.Result}\n";
}
return System.Text.Encoding.UTF8.GetBytes(csvContent);
}
/// <summary>
/// 导出为CSV格式。
/// </summary>
private async Task<byte[]> ExportToCsvAsync(IReadOnlyList<ProductionSessionModel> sessions, ExportSessionsRequest request, CancellationToken cancellationToken = default)
{
await Task.Delay(50, cancellationToken);
var csvContent = "SessionId,ProductTypeCode,StationId,OperatorName,StartedAtUtc,EndedAtUtc,Status,Result\n";
foreach (var session in sessions)
{
csvContent += $"{session.SessionId},{session.ProductTypeCode},{session.StationId},{session.OperatorName},{session.StartedAtUtc:yyyy-MM-dd HH:mm:ss},{session.EndedAtUtc:yyyy-MM-dd HH:mm:ss},{session.Status},{session.Result}\n";
}
return System.Text.Encoding.UTF8.GetBytes(csvContent);
}
/// <summary>
/// 导出为JSON格式。
/// </summary>
private async Task<byte[]> ExportToJsonAsync(IReadOnlyList<ProductionSessionModel> sessions, ExportSessionsRequest request, CancellationToken cancellationToken = default)
{
await Task.Delay(50, cancellationToken);
var json = System.Text.Json.JsonSerializer.Serialize(sessions, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
});
return System.Text.Encoding.UTF8.GetBytes(json);
}
#endregion
}
/// <summary>
/// 产品会话事件模型。
/// </summary>
public sealed class ProductionSessionEvent
{
/// <summary>
/// 事件ID。
/// </summary>
public Guid EventId { get; set; }
/// <summary>
/// 会话ID。
/// </summary>
public Guid SessionId { get; set; }
/// <summary>
/// 事件类型。
/// </summary>
public ProductionSessionEventType EventType { get; set; }
/// <summary>
/// 事件描述。
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 操作员姓名。
/// </summary>
public string OperatorName { get; set; } = string.Empty;
/// <summary>
/// 事件时间UTC
/// </summary>
public DateTime EventTimeUtc { get; set; }
/// <summary>
/// 扩展属性。
/// </summary>
public Dictionary<string, object> ExtendedProperties { get; set; } = new();
}
/// <summary>
/// 产品会话事件类型。
/// </summary>
public enum ProductionSessionEventType
{
/// <summary>
/// 会话创建。
/// </summary>
SessionCreated = 0,
/// <summary>
/// 会话归档。
/// </summary>
SessionArchived = 1,
/// <summary>
/// 进度更新。
/// </summary>
ProgressUpdated = 2,
/// <summary>
/// 会话暂停。
/// </summary>
SessionPaused = 3,
/// <summary>
/// 会话恢复。
/// </summary>
SessionResumed = 4,
/// <summary>
/// 会话取消。
/// </summary>
SessionCancelled = 5,
/// <summary>
/// 层级开始。
/// </summary>
LayerStarted = 6,
/// <summary>
/// 层级完成。
/// </summary>
LayerCompleted = 7,
/// <summary>
/// 检测到NG。
/// </summary>
NgDetected = 8,
/// <summary>
/// 人工干预。
/// </summary>
ManualIntervention = 9
}