using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OrpaonVision.ConfigApp.Infrastructure.Options; using OrpaonVision.Core.Results; using OrpaonVision.Core.Common; using OrpaonVision.Model.Production; using System.Collections.Concurrent; using System.Data; using System.Text.Json; namespace OrpaonVision.ConfigApp.Infrastructure.Persistence; /// /// SQL产品会话仓储实现。 /// public sealed class SqlProductionSessionRepository : IProductionSessionRepository { private readonly ILogger _logger; private readonly SessionPersistenceOptions _options; private readonly ConcurrentDictionary _connections = new(); /// /// 构造函数。 /// public SqlProductionSessionRepository( ILogger logger, IOptions options) { _logger = logger; _options = options.Value; } /// public async Task CreateAsync(ProductionSessionModel session, CancellationToken cancellationToken = default) { try { _logger.LogDebug("创建会话: {SessionId}", session.SessionId); using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = @" INSERT INTO mdl_production_session ( SessionId, ProductTypeCode, ProductTypeName, StationId, StationName, OperatorId, OperatorName, ShiftId, ShiftName, StartedAtUtc, EndedAtUtc, Status, Result, CurrentLayer, TotalLayers, NgReason, Remark, CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy ) VALUES ( @SessionId, @ProductTypeCode, @ProductTypeName, @StationId, @StationName, @OperatorId, @OperatorName, @ShiftId, @ShiftName, @StartedAtUtc, @EndedAtUtc, @Status, @Result, @CurrentLayer, @TotalLayers, @NgReason, @Remark, @CreatedAtUtc, @UpdatedAtUtc, @CreatedBy, @UpdatedBy )"; AddParameter(command, "@SessionId", session.SessionId); AddParameter(command, "@ProductTypeCode", session.ProductTypeCode); AddParameter(command, "@ProductTypeName", session.ProductTypeName); AddParameter(command, "@StationId", session.StationId); AddParameter(command, "@StationName", session.StationName); AddParameter(command, "@OperatorId", session.OperatorId); AddParameter(command, "@OperatorName", session.OperatorName); AddParameter(command, "@ShiftId", session.ShiftId); AddParameter(command, "@ShiftName", session.ShiftName); AddParameter(command, "@StartedAtUtc", session.StartedAtUtc); AddParameter(command, "@EndedAtUtc", (object?)session.EndedAtUtc ?? DBNull.Value); AddParameter(command, "@Status", (int)session.Status); AddParameter(command, "@Result", (int)session.Result); AddParameter(command, "@CurrentLayer", session.CurrentLayer); AddParameter(command, "@TotalLayers", session.TotalLayers); AddParameter(command, "@NgReason", (object?)session.NgReason ?? DBNull.Value); AddParameter(command, "@Remark", (object?)session.Remark ?? DBNull.Value); AddParameter(command, "@CreatedAtUtc", session.CreatedAtUtc); AddParameter(command, "@UpdatedAtUtc", session.UpdatedAtUtc); AddParameter(command, "@CreatedBy", session.CreatedBy); AddParameter(command, "@UpdatedBy", session.UpdatedBy); var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); if (rowsAffected > 0) { _logger.LogDebug("会话创建成功: {SessionId}", session.SessionId); return Result.Success(); } else { return Result.Fail("CREATE_FAILED", "会话创建失败,未影响任何行"); } } 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.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task UpdateAsync(ProductionSessionModel session, CancellationToken cancellationToken = default) { try { _logger.LogDebug("更新会话: {SessionId}", session.SessionId); using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = @" UPDATE mdl_production_session SET ProductTypeCode = @ProductTypeCode, ProductTypeName = @ProductTypeName, StationId = @StationId, StationName = @StationName, OperatorId = @OperatorId, OperatorName = @OperatorName, ShiftId = @ShiftId, ShiftName = @ShiftName, StartedAtUtc = @StartedAtUtc, EndedAtUtc = @EndedAtUtc, Status = @Status, Result = @Result, CurrentLayer = @CurrentLayer, TotalLayers = @TotalLayers, NgReason = @NgReason, Remark = @Remark, UpdatedAtUtc = @UpdatedAtUtc, UpdatedBy = @UpdatedBy WHERE SessionId = @SessionId"; AddParameter(command, "@SessionId", session.SessionId); AddParameter(command, "@ProductTypeCode", session.ProductTypeCode); AddParameter(command, "@ProductTypeName", session.ProductTypeName); AddParameter(command, "@StationId", session.StationId); AddParameter(command, "@StationName", session.StationName); AddParameter(command, "@OperatorId", session.OperatorId); AddParameter(command, "@OperatorName", session.OperatorName); AddParameter(command, "@ShiftId", session.ShiftId); AddParameter(command, "@ShiftName", session.ShiftName); AddParameter(command, "@StartedAtUtc", session.StartedAtUtc); AddParameter(command, "@EndedAtUtc", (object?)session.EndedAtUtc ?? DBNull.Value); AddParameter(command, "@Status", (int)session.Status); AddParameter(command, "@Result", (int)session.Result); AddParameter(command, "@CurrentLayer", session.CurrentLayer); AddParameter(command, "@TotalLayers", session.TotalLayers); AddParameter(command, "@NgReason", (object?)session.NgReason ?? DBNull.Value); AddParameter(command, "@Remark", (object?)session.Remark ?? DBNull.Value); AddParameter(command, "@UpdatedAtUtc", session.UpdatedAtUtc); AddParameter(command, "@UpdatedBy", session.UpdatedBy); var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); if (rowsAffected > 0) { _logger.LogDebug("会话更新成功: {SessionId}", session.SessionId); return Result.Success(); } else { return Result.Fail("UPDATE_FAILED", "会话更新失败,未找到匹配的记录"); } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "更新会话失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "UPDATE_SESSION_FAILED", "更新会话失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task DeleteAsync(Guid sessionId, CancellationToken cancellationToken = default) { try { _logger.LogDebug("删除会话: {SessionId}", sessionId); using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = "DELETE FROM mdl_production_session WHERE SessionId = @SessionId"; AddParameter(command, "@SessionId", sessionId); var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); if (rowsAffected > 0) { _logger.LogDebug("会话删除成功: {SessionId}", sessionId); return Result.Success(); } else { return Result.Fail("DELETE_FAILED", "会话删除失败,未找到匹配的记录"); } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "删除会话失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "DELETE_SESSION_FAILED", "删除会话失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task> GetByIdAsync(Guid sessionId, CancellationToken cancellationToken = default) { try { _logger.LogDebug("根据ID获取会话: {SessionId}", sessionId); using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = @" SELECT SessionId, ProductTypeCode, ProductTypeName, StationId, StationName, OperatorId, OperatorName, ShiftId, ShiftName, StartedAtUtc, EndedAtUtc, Status, Result, CurrentLayer, TotalLayers, NgReason, Remark, CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy FROM mdl_production_session WHERE SessionId = @SessionId"; AddParameter(command, "@SessionId", sessionId); using var reader = await command.ExecuteReaderAsync(cancellationToken); if (await reader.ReadAsync(cancellationToken)) { var session = MapToSession(reader); _logger.LogDebug("会话获取成功: {SessionId}", sessionId); return Result.Success(session); } else { _logger.LogDebug("未找到会话: {SessionId}", sessionId); return Result.Success(null); } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "根据ID获取会话失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "GET_SESSION_FAILED", "根据ID获取会话失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task> GetActiveByStationIdAsync(string stationId, CancellationToken cancellationToken = default) { try { _logger.LogDebug("根据工位ID获取活动会话: {StationId}", stationId); using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = @" SELECT TOP 1 SessionId, ProductTypeCode, ProductTypeName, StationId, StationName, OperatorId, OperatorName, ShiftId, ShiftName, StartedAtUtc, EndedAtUtc, Status, Result, CurrentLayer, TotalLayers, NgReason, Remark, CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy FROM mdl_production_session WHERE StationId = @StationId AND Status IN (0, 4) -- InProgress, Paused ORDER BY StartedAtUtc DESC"; AddParameter(command, "@StationId", stationId); using var reader = await command.ExecuteReaderAsync(cancellationToken); if (await reader.ReadAsync(cancellationToken)) { var session = MapToSession(reader); _logger.LogDebug("活动会话获取成功: {SessionId}", session.SessionId); return Result.Success(session); } else { _logger.LogDebug("工位 {StationId} 无活动会话", stationId); return Result.Success(null); } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "根据工位ID获取活动会话失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "GET_ACTIVE_SESSION_FAILED", "根据工位ID获取活动会话失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task>> GetByQueryAsync(SessionQuery query, CancellationToken cancellationToken = default) { try { _logger.LogDebug("根据查询条件获取会话列表"); var sql = BuildQuerySql(query); var parameters = BuildQueryParameters(query); using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = sql; foreach (var param in parameters) { AddParameter(command, param.Key, param.Value); } var sessions = new List(); using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { sessions.Add(MapToSession(reader)); } _logger.LogDebug("查询到 {Count} 个会话", sessions.Count); return Result>.Success(sessions); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "根据查询条件获取会话列表失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "QUERY_SESSIONS_FAILED", "根据查询条件获取会话列表失败。", traceId); return Result>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task> GetPagedByQueryAsync(SessionQuery query, int pageIndex, int pageSize, CancellationToken cancellationToken = default) { try { _logger.LogDebug("分页查询会话: 页码={PageIndex}, 页大小={PageSize}", pageIndex, pageSize); var countSql = BuildCountSql(query); var dataSql = BuildQuerySql(query, pageIndex, pageSize); var parameters = BuildQueryParameters(query); using var connection = await GetConnectionAsync(cancellationToken); // 获取总数 using var countCommand = connection.CreateCommand(); countCommand.CommandText = countSql; foreach (var param in parameters) { AddParameter(countCommand, param.Key, param.Value); } var totalCount = (int?)await countCommand.ExecuteScalarAsync(cancellationToken) ?? 0; // 获取数据 using var dataCommand = connection.CreateCommand(); dataCommand.CommandText = dataSql; foreach (var param in parameters) { AddParameter(dataCommand, param.Key, param.Value); } var sessions = new List(); using var reader = await dataCommand.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { sessions.Add(MapToSession(reader)); } var result = new PagedSessionResult { Items = sessions, TotalCount = totalCount, PageIndex = pageIndex, PageSize = pageSize }; _logger.LogDebug("分页查询完成: 总数={TotalCount}, 当前页={Count}", totalCount, sessions.Count); return Result.Success(result); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "分页查询会话失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "PAGED_QUERY_FAILED", "分页查询会话失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task> BatchCreateAsync(IReadOnlyList sessions, CancellationToken cancellationToken = default) { try { _logger.LogInformation("批量创建会话: 数量={Count}", sessions.Count); var totalCount = sessions.Count; var successCount = 0; var failureCount = 0; var failedIndexes = new List(); var errors = new List(); using var connection = await GetConnectionAsync(cancellationToken); await connection.OpenAsync(cancellationToken); using var transaction = connection.BeginTransaction(); try { for (int i = 0; i < sessions.Count; i++) { var session = sessions[i]; using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" INSERT INTO mdl_production_session ( SessionId, ProductTypeCode, ProductTypeName, StationId, StationName, OperatorId, OperatorName, ShiftId, ShiftName, StartedAtUtc, EndedAtUtc, Status, Result, CurrentLayer, TotalLayers, NgReason, Remark, CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy ) VALUES ( @SessionId, @ProductTypeCode, @ProductTypeName, @StationId, @StationName, @OperatorId, @OperatorName, @ShiftId, @ShiftName, @StartedAtUtc, @EndedAtUtc, @Status, @Result, @CurrentLayer, @TotalLayers, @NgReason, @Remark, @CreatedAtUtc, @UpdatedAtUtc, @CreatedBy, @UpdatedBy )"; AddParameter(command, "@SessionId", session.SessionId); AddParameter(command, "@ProductTypeCode", session.ProductTypeCode); AddParameter(command, "@ProductTypeName", session.ProductTypeName); AddParameter(command, "@StationId", session.StationId); AddParameter(command, "@StationName", session.StationName); AddParameter(command, "@OperatorId", session.OperatorId); AddParameter(command, "@OperatorName", session.OperatorName); AddParameter(command, "@ShiftId", session.ShiftId); AddParameter(command, "@ShiftName", session.ShiftName); AddParameter(command, "@StartedAtUtc", session.StartedAtUtc); AddParameter(command, "@EndedAtUtc", (object?)session.EndedAtUtc ?? DBNull.Value); AddParameter(command, "@Status", (int)session.Status); AddParameter(command, "@Result", (int)session.Result); AddParameter(command, "@CurrentLayer", session.CurrentLayer); AddParameter(command, "@TotalLayers", session.TotalLayers); AddParameter(command, "@NgReason", (object?)session.NgReason ?? DBNull.Value); AddParameter(command, "@Remark", (object?)session.Remark ?? DBNull.Value); AddParameter(command, "@CreatedAtUtc", session.CreatedAtUtc); AddParameter(command, "@UpdatedAtUtc", session.UpdatedAtUtc); AddParameter(command, "@CreatedBy", session.CreatedBy); AddParameter(command, "@UpdatedBy", session.UpdatedBy); try { var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); if (rowsAffected > 0) { successCount++; } else { failureCount++; failedIndexes.Add(i); errors.Add($"会话 {session.SessionId} 创建失败,未影响任何行"); } } catch (Exception ex) { failureCount++; failedIndexes.Add(i); errors.Add($"会话 {session.SessionId} 创建异常: {ex.Message}"); } } await transaction.CommitAsync(cancellationToken); _logger.LogInformation("批量创建会话完成: 成功={SuccessCount}, 失败={FailureCount}", successCount, failureCount); var result = new BatchCreateResult { TotalCount = totalCount, SuccessCount = successCount, FailureCount = failureCount, FailedIndexes = failedIndexes, Errors = errors }; return Result.Success(result); } catch (Exception ex) { await transaction.RollbackAsync(cancellationToken); var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "批量创建会话事务失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("BATCH_CREATE_TRANSACTION_FAILED", "批量创建会话事务失败。", traceId, ex.Message); } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "批量创建会话失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("BATCH_CREATE_FAILED", "批量创建会话失败。", traceId, ex.Message); } } /// public async Task> BatchUpdateAsync(IReadOnlyList sessions, CancellationToken cancellationToken = default) { try { _logger.LogInformation("批量更新会话: 数量={Count}", sessions.Count); var successCount = 0; var failureCount = 0; var failedIndexes = new List(); var errors = new List(); using var connection = await GetConnectionAsync(cancellationToken); await connection.OpenAsync(cancellationToken); using var transaction = connection.BeginTransaction(); try { for (int i = 0; i < sessions.Count; i++) { var session = sessions[i]; using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" UPDATE mdl_production_session SET ProductTypeCode = @ProductTypeCode, ProductTypeName = @ProductTypeName, StationId = @StationId, StationName = @StationName, OperatorId = @OperatorId, OperatorName = @OperatorName, ShiftId = @ShiftId, ShiftName = @ShiftName, StartedAtUtc = @StartedAtUtc, EndedAtUtc = @EndedAtUtc, Status = @Status, Result = @Result, CurrentLayer = @CurrentLayer, TotalLayers = @TotalLayers, NgReason = @NgReason, Remark = @Remark, UpdatedAtUtc = @UpdatedAtUtc, UpdatedBy = @UpdatedBy WHERE SessionId = @SessionId"; AddParameter(command, "@SessionId", session.SessionId); AddParameter(command, "@ProductTypeCode", session.ProductTypeCode); AddParameter(command, "@ProductTypeName", session.ProductTypeName); AddParameter(command, "@StationId", session.StationId); AddParameter(command, "@StationName", session.StationName); AddParameter(command, "@OperatorId", session.OperatorId); AddParameter(command, "@OperatorName", session.OperatorName); AddParameter(command, "@ShiftId", session.ShiftId); AddParameter(command, "@ShiftName", session.ShiftName); AddParameter(command, "@StartedAtUtc", session.StartedAtUtc); AddParameter(command, "@EndedAtUtc", session.EndedAtUtc); AddParameter(command, "@Status", session.Status); AddParameter(command, "@Result", session.Result); AddParameter(command, "@CurrentLayer", session.CurrentLayer); AddParameter(command, "@TotalLayers", session.TotalLayers); AddParameter(command, "@NgReason", (object?)session.NgReason ?? DBNull.Value); AddParameter(command, "@Remark", (object?)session.Remark ?? DBNull.Value); AddParameter(command, "@UpdatedAtUtc", DateTime.UtcNow); AddParameter(command, "@UpdatedBy", session.UpdatedBy); try { var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); if (rowsAffected > 0) { successCount++; } else { failureCount++; failedIndexes.Add(i); errors.Add($"会话 {session.SessionId} 更新失败,未找到匹配的记录"); } } catch (Exception ex) { failureCount++; failedIndexes.Add(i); errors.Add($"会话 {session.SessionId} 更新异常: {ex.Message}"); } } await transaction.CommitAsync(cancellationToken); _logger.LogInformation("批量更新会话完成: 成功={SuccessCount}, 失败={FailureCount}", successCount, failureCount); var result = new BatchUpdateResult { TotalCount = sessions.Count, SuccessCount = successCount, FailureCount = failureCount, FailedIndexes = failedIndexes, Errors = errors }; return Result.Success(result); } catch { await transaction.RollbackAsync(cancellationToken); throw; } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "批量更新会话失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "BATCH_UPDATE_FAILED", "批量更新会话失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task> BatchDeleteAsync(IReadOnlyList sessionIds, CancellationToken cancellationToken = default) { try { _logger.LogInformation("批量删除会话: 数量={Count}", sessionIds.Count); var successCount = 0; var failureCount = 0; var failedSessionIds = new List(); var errors = new List(); using var connection = await GetConnectionAsync(cancellationToken); await connection.OpenAsync(cancellationToken); using var transaction = connection.BeginTransaction(); try { for (int i = 0; i < sessionIds.Count; i++) { var sessionId = sessionIds[i]; using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = "DELETE FROM mdl_production_session WHERE SessionId = @SessionId"; AddParameter(command, "@SessionId", sessionId); try { var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken); if (rowsAffected > 0) { successCount++; } else { failureCount++; failedSessionIds.Add(sessionId); errors.Add($"会话 {sessionId} 删除失败,未找到匹配的记录"); } } catch (Exception ex) { failureCount++; failedSessionIds.Add(sessionId); errors.Add($"会话 {sessionId} 删除异常: {ex.Message}"); } } await transaction.CommitAsync(cancellationToken); _logger.LogInformation("批量删除会话完成: 成功={SuccessCount}, 失败={FailureCount}", successCount, failureCount); var result = new BatchDeleteResult { TotalCount = sessionIds.Count, SuccessCount = successCount, FailureCount = failureCount, FailedSessionIds = failedSessionIds, Errors = errors }; return Result.Success(result); } catch { await transaction.RollbackAsync(cancellationToken); throw; } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "批量删除会话失败。TraceId: {TraceId}", traceId); var result = Result.FromException(ex, "BATCH_DELETE_FAILED", "批量删除会话失败。", traceId); return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray()); } } /// public async Task> GetStatisticsAsync(SessionStatisticsQuery query, CancellationToken cancellationToken = default) { try { _logger.LogDebug("获取会话统计信息"); using var connection = await GetConnectionAsync(cancellationToken); // 获取基础统计数据 int totalSessions = 0, okSessions = 0, ngSessions = 0, inProgressSessions = 0, cancelledSessions = 0, pausedSessions = 0; using var basicCommand = connection.CreateCommand(); basicCommand.CommandText = BuildBasicStatisticsSql(query); AddParameter(basicCommand, "@StartTime", query.StartTime); AddParameter(basicCommand, "@EndTime", query.EndTime); using var reader = await basicCommand.ExecuteReaderAsync(cancellationToken); if (await reader.ReadAsync(cancellationToken)) { totalSessions = reader.GetInt32(0); okSessions = reader.GetInt32(1); ngSessions = reader.GetInt32(2); inProgressSessions = reader.GetInt32(3); cancelledSessions = reader.GetInt32(4); pausedSessions = reader.GetInt32(5); } // 计算合格率 var passRate = totalSessions > 0 ? (double)okSessions / totalSessions * 100 : 0; // 获取平均处理时间和处理量 double averageProcessingTimeSeconds = 0, throughputPerHour = 0; using var timeCommand = connection.CreateCommand(); timeCommand.CommandText = BuildTimeStatisticsSql(query); AddParameter(timeCommand, "@StartTime", query.StartTime); AddParameter(timeCommand, "@EndTime", query.EndTime); using var timeReader = await timeCommand.ExecuteReaderAsync(cancellationToken); if (await timeReader.ReadAsync(cancellationToken)) { if (!timeReader.IsDBNull(0)) averageProcessingTimeSeconds = timeReader.GetDouble(0); if (!timeReader.IsDBNull(1)) throughputPerHour = timeReader.GetDouble(1); } // 按维度分组统计 var byProductType = new List(); var byStation = new List(); var byOperator = new List(); var byDate = new List(); var byShift = new List(); if (query.Dimensions.Contains(StatisticsDimension.ByProductType)) { byProductType = await GetProductTypeStatisticsAsync(query, connection, cancellationToken); } if (query.Dimensions.Contains(StatisticsDimension.ByStation)) { byStation = await GetStationStatisticsAsync(query, connection, cancellationToken); } if (query.Dimensions.Contains(StatisticsDimension.ByOperator)) { byOperator = await GetOperatorStatisticsAsync(query, connection, cancellationToken); } if (query.Dimensions.Contains(StatisticsDimension.ByDate)) { byDate = await GetDailyStatisticsAsync(query, connection, cancellationToken); } if (query.Dimensions.Contains(StatisticsDimension.ByShift)) { byShift = await GetShiftStatisticsAsync(query, connection, cancellationToken); } // 创建统计对象 var statistics = new SessionStatistics { TotalSessions = totalSessions, OkSessions = okSessions, NgSessions = ngSessions, InProgressSessions = inProgressSessions, CancelledSessions = cancelledSessions, PausedSessions = pausedSessions, PassRate = passRate, AverageProcessingTimeSeconds = averageProcessingTimeSeconds, ThroughputPerHour = throughputPerHour, ByProductType = byProductType, ByStation = byStation, ByOperator = byOperator, ByDate = byDate, ByShift = byShift }; _logger.LogDebug("会话统计获取完成: 总数={TotalCount}, 合格率={PassRate:F2}%", statistics.TotalSessions, statistics.PassRate); return Result.Success(statistics); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "获取会话统计信息失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("GET_STATISTICS_FAILED", "获取会话统计信息失败。", traceId, ex.Message); } } /// public async Task> CleanupExpiredAsync(DateTime cutoffDate, CancellationToken cancellationToken = default) { try { _logger.LogInformation("清理过期会话: 截止日期={CutoffDate}", cutoffDate); var startTime = DateTime.UtcNow; using var connection = await GetConnectionAsync(cancellationToken); await connection.OpenAsync(cancellationToken); using var transaction = connection.BeginTransaction(); try { // 获取要清理的会话数量 using var countCommand = connection.CreateCommand(); countCommand.Transaction = transaction; countCommand.CommandText = "SELECT COUNT(*) FROM mdl_production_session WHERE EndedAtUtc < @CutoffDate"; AddParameter(countCommand, "@CutoffDate", cutoffDate); var cleanedCount = (int?)await countCommand.ExecuteScalarAsync(cancellationToken) ?? 0; // 删除过期会话 using var deleteCommand = connection.CreateCommand(); deleteCommand.Transaction = transaction; deleteCommand.CommandText = "DELETE FROM mdl_production_session WHERE EndedAtUtc < @CutoffDate"; AddParameter(deleteCommand, "@CutoffDate", cutoffDate); var rowsAffected = await deleteCommand.ExecuteNonQueryAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); var elapsed = DateTime.UtcNow - startTime; var result = new CleanupResult { CleanedCount = cleanedCount, FreedSpaceBytes = rowsAffected * 1024, // 假设每个会话占用1KB ElapsedMilliseconds = (long)elapsed.TotalMilliseconds, CleanedEventCount = rowsAffected // 简化实现 }; _logger.LogInformation("清理过期会话完成: 清理数量={CleanedCount}, 耗时={ElapsedMs}ms", result.CleanedCount, result.ElapsedMilliseconds); return Result.Success(result); } catch { await transaction.RollbackAsync(cancellationToken); throw; } } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "清理过期会话失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("CLEANUP_FAILED", "清理过期会话失败。", traceId, ex.Message); } } /// public async Task> ExistsAsync(Guid sessionId, CancellationToken cancellationToken = default) { try { using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = "SELECT COUNT(*) FROM mdl_production_session WHERE SessionId = @SessionId"; AddParameter(command, "@SessionId", sessionId); var count = (int?)await command.ExecuteScalarAsync(cancellationToken) ?? 0; return Result.Success(count > 0); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "检查会话是否存在失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("EXISTS_CHECK_FAILED", "检查会话是否存在失败。", traceId, ex.Message); } } /// public async Task> CountAsync(SessionQuery query, CancellationToken cancellationToken = default) { try { var sql = BuildCountSql(query); var parameters = BuildQueryParameters(query); using var connection = await GetConnectionAsync(cancellationToken); using var command = connection.CreateCommand(); command.CommandText = sql; foreach (var param in parameters) { AddParameter(command, param.Key, param.Value); } var count = (int?)await command.ExecuteScalarAsync(cancellationToken) ?? 0; return Result.Success(count); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "获取会话数量失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("COUNT_FAILED", "获取会话数量失败。", traceId, ex.Message); } } #region 私有方法 /// /// 获取数据库连接。 /// private async Task GetConnectionAsync(CancellationToken cancellationToken = default) { var connectionString = _options.ConnectionString; if (string.IsNullOrWhiteSpace(connectionString)) { throw new InvalidOperationException("数据库连接字符串未配置"); } var connection = new SqlConnection(connectionString); await connection.OpenAsync(cancellationToken); return connection; } /// /// 添加参数。 /// private static void AddParameter(SqlCommand command, string name, object value) { var parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value; command.Parameters.Add(parameter); } /// /// 从DataReader映射到会话模型。 /// private static ProductionSessionModel MapToSession(SqlDataReader reader) { return new ProductionSessionModel { SessionId = reader.GetGuid(0), ProductTypeCode = reader.GetString(1), ProductTypeName = reader.GetString(2), StationId = reader.GetString(3), StationName = reader.GetString(4), OperatorId = reader.GetString(5), OperatorName = reader.GetString(6), ShiftId = reader.GetString(7), ShiftName = reader.GetString(8), StartedAtUtc = reader.GetDateTime(9), EndedAtUtc = reader.IsDBNull(10) ? null : reader.GetDateTime(10), Status = (ProductionSessionStatus)reader.GetInt32(11), Result = (ProductionSessionResult)reader.GetInt32(12), CurrentLayer = reader.GetInt32(13), TotalLayers = reader.GetInt32(14), NgReason = reader.IsDBNull(15) ? null : reader.GetString(15), Remark = reader.IsDBNull(16) ? null : reader.GetString(16), CreatedAtUtc = reader.GetDateTime(17), UpdatedAtUtc = reader.GetDateTime(18), CreatedBy = reader.GetString(19), UpdatedBy = reader.GetString(20) }; } /// /// 构建查询SQL。 /// private string BuildQuerySql(SessionQuery query, int? pageIndex = null, int? pageSize = null) { var sql = @" SELECT SessionId, ProductTypeCode, ProductTypeName, StationId, StationName, OperatorId, OperatorName, ShiftId, ShiftName, StartedAtUtc, EndedAtUtc, Status, Result, CurrentLayer, TotalLayers, NgReason, Remark, CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy FROM mdl_production_session WHERE 1=1"; var conditions = new List(); var parameters = new Dictionary(); if (query.SessionIds?.Any() == true) { conditions.Add($"SessionId IN ({string.Join(",", query.SessionIds.Select((_, i) => $"@SessionId{i}"))})"); for (int i = 0; i < query.SessionIds.Count; i++) { parameters[$"@SessionId{i}"] = query.SessionIds[i]; } } if (!string.IsNullOrWhiteSpace(query.ProductTypeCode)) { conditions.Add("ProductTypeCode = @ProductTypeCode"); parameters["@ProductTypeCode"] = query.ProductTypeCode; } if (!string.IsNullOrWhiteSpace(query.StationId)) { conditions.Add("StationId = @StationId"); parameters["@StationId"] = query.StationId; } if (!string.IsNullOrWhiteSpace(query.OperatorId)) { conditions.Add("OperatorId = @OperatorId"); parameters["@OperatorId"] = query.OperatorId; } if (!string.IsNullOrWhiteSpace(query.ShiftId)) { conditions.Add("ShiftId = @ShiftId"); parameters["@ShiftId"] = query.ShiftId; } if (query.Statuses?.Any() == true) { conditions.Add($"Status IN ({string.Join(",", query.Statuses.Select(s => (int)s))})"); } if (query.Results?.Any() == true) { conditions.Add($"Result IN ({string.Join(",", query.Results.Select(r => (int)r))})"); } if (query.StartTimeRange != null) { if (query.StartTimeRange.IncludeStart) conditions.Add("StartedAtUtc >= @StartTimeStart"); else conditions.Add("StartedAtUtc > @StartTimeStart"); parameters["@StartTimeStart"] = query.StartTimeRange.Start; if (query.StartTimeRange.IncludeEnd) conditions.Add("StartedAtUtc <= @StartTimeEnd"); else conditions.Add("StartedAtUtc < @StartTimeEnd"); parameters["@StartTimeEnd"] = query.StartTimeRange.End; } if (query.EndTimeRange != null) { if (query.EndTimeRange.IncludeStart) conditions.Add("EndedAtUtc >= @EndTimeStart"); else conditions.Add("EndedAtUtc > @EndTimeStart"); parameters["@EndTimeStart"] = query.EndTimeRange.Start; if (query.EndTimeRange.IncludeEnd) conditions.Add("EndedAtUtc <= @EndTimeEnd"); else conditions.Add("EndedAtUtc < @EndTimeEnd"); parameters["@EndTimeEnd"] = query.EndTimeRange.End; } if (query.CreatedTimeRange != null) { if (query.CreatedTimeRange.IncludeStart) conditions.Add("CreatedAtUtc >= @CreatedTimeStart"); else conditions.Add("CreatedAtUtc > @CreatedTimeStart"); parameters["@CreatedTimeStart"] = query.CreatedTimeRange.Start; if (query.CreatedTimeRange.IncludeEnd) conditions.Add("CreatedAtUtc <= @CreatedTimeEnd"); else conditions.Add("CreatedAtUtc < @CreatedTimeEnd"); parameters["@CreatedTimeEnd"] = query.CreatedTimeRange.End; } if (!string.IsNullOrWhiteSpace(query.Keyword)) { conditions.Add("(ProductTypeCode LIKE @Keyword OR StationId LIKE @Keyword OR OperatorName LIKE @Keyword)"); parameters["@Keyword"] = $"%{query.Keyword}%"; } if (query.ActiveOnly) { conditions.Add("Status IN (0, 4)"); // InProgress, Paused } if (query.CompletedOnly) { conditions.Add("Status IN (1, 2)"); // CompletedOk, CompletedNg } if (conditions.Any()) { sql += " AND " + string.Join(" AND ", conditions); } // 添加排序 var sortField = query.SortField switch { SessionSortField.StartTimeUtc => "StartedAtUtc", SessionSortField.EndTimeUtc => "EndedAtUtc", SessionSortField.ProductTypeCode => "ProductTypeCode", SessionSortField.StationId => "StationId", SessionSortField.OperatorName => "OperatorName", SessionSortField.CurrentLayer => "CurrentLayer", _ => "StartedAtUtc" }; var sortDirection = query.SortDirection == SortDirection.Ascending ? "ASC" : "DESC"; sql += $" ORDER BY {sortField} {sortDirection}"; // 添加分页 if (pageIndex.HasValue && pageSize.HasValue) { var offset = (pageIndex.Value - 1) * pageSize.Value; sql += $" OFFSET {offset} ROWS FETCH NEXT {pageSize.Value} ROWS ONLY"; } return sql; } /// /// 构建计数SQL。 /// private string BuildCountSql(SessionQuery query) { var querySql = BuildQuerySql(query); return $"SELECT COUNT(*) FROM ({querySql}) AS CountQuery"; } /// /// 构建查询参数。 /// private Dictionary BuildQueryParameters(SessionQuery query) { var parameters = new Dictionary(); if (query.SessionIds?.Any() == true) { for (int i = 0; i < query.SessionIds.Count; i++) { parameters[$"@SessionId{i}"] = query.SessionIds[i]; } } if (!string.IsNullOrWhiteSpace(query.ProductTypeCode)) parameters["@ProductTypeCode"] = query.ProductTypeCode; if (!string.IsNullOrWhiteSpace(query.StationId)) parameters["@StationId"] = query.StationId; if (!string.IsNullOrWhiteSpace(query.OperatorId)) parameters["@OperatorId"] = query.OperatorId; if (!string.IsNullOrWhiteSpace(query.ShiftId)) parameters["@ShiftId"] = query.ShiftId; if (query.StartTimeRange != null) { parameters["@StartTimeStart"] = query.StartTimeRange.Start; parameters["@StartTimeEnd"] = query.StartTimeRange.End; } if (query.EndTimeRange != null) { parameters["@EndTimeStart"] = query.EndTimeRange.Start; parameters["@EndTimeEnd"] = query.EndTimeRange.End; } if (query.CreatedTimeRange != null) { parameters["@CreatedTimeStart"] = query.CreatedTimeRange.Start; parameters["@CreatedTimeEnd"] = query.CreatedTimeRange.End; } if (!string.IsNullOrWhiteSpace(query.Keyword)) parameters["@Keyword"] = $"%{query.Keyword}%"; return parameters; } /// /// 构建基础统计SQL。 /// private string BuildBasicStatisticsSql(SessionStatisticsQuery query) { var sql = @" SELECT COUNT(*) as TotalSessions, SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) as OkSessions, SUM(CASE WHEN Result = 2 THEN 1 ELSE 0 END) as NgSessions, SUM(CASE WHEN Status = 0 THEN 1 ELSE 0 END) as InProgressSessions, SUM(CASE WHEN Status = 3 THEN 1 ELSE 0 END) as CancelledSessions, SUM(CASE WHEN Status = 4 THEN 1 ELSE 0 END) as PausedSessions FROM mdl_production_session WHERE StartedAtUtc >= @StartTime AND StartedAtUtc <= @EndTime"; if (!string.IsNullOrWhiteSpace(query.ProductTypeCode)) sql += " AND ProductTypeCode = @ProductTypeCode"; if (!string.IsNullOrWhiteSpace(query.StationId)) sql += " AND StationId = @StationId"; if (!string.IsNullOrWhiteSpace(query.OperatorId)) sql += " AND OperatorId = @OperatorId"; if (!string.IsNullOrWhiteSpace(query.ShiftId)) sql += " AND ShiftId = @ShiftId"; return sql; } /// /// 构建时间统计SQL。 /// private string BuildTimeStatisticsSql(SessionStatisticsQuery query) { var sql = @" SELECT AVG(DATEDIFF(SECOND, StartedAtUtc, EndedAtUtc)) as AvgProcessingTime, COUNT(*) / NULLIF(SUM(DATEDIFF(HOUR, StartedAtUtc, EndedAtUtc)), 0) as ThroughputPerHour FROM mdl_production_session WHERE StartedAtUtc >= @StartTime AND StartedAtUtc <= @EndTime AND EndedAtUtc IS NOT NULL"; if (!string.IsNullOrWhiteSpace(query.ProductTypeCode)) sql += " AND ProductTypeCode = @ProductTypeCode"; if (!string.IsNullOrWhiteSpace(query.StationId)) sql += " AND StationId = @StationId"; if (!string.IsNullOrWhiteSpace(query.OperatorId)) sql += " AND OperatorId = @OperatorId"; if (!string.IsNullOrWhiteSpace(query.ShiftId)) sql += " AND ShiftId = @ShiftId"; return sql; } /// /// 获取产品类型统计。 /// private async Task> GetProductTypeStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default) { var statistics = new List(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT ProductTypeCode, ProductTypeName, COUNT(*) as TotalSessions, SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) as OkSessions, SUM(CASE WHEN Result = 2 THEN 1 ELSE 0 END) as NgSessions FROM mdl_production_session WHERE StartedAtUtc >= @StartTime AND StartedAtUtc <= @EndTime GROUP BY ProductTypeCode, ProductTypeName"; AddParameter(command, "@StartTime", query.StartTime); AddParameter(command, "@EndTime", query.EndTime); using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { statistics.Add(new ProductTypeStatistics { ProductTypeCode = reader.GetString(0), ProductTypeName = reader.GetString(1), TotalSessions = reader.GetInt32(2), OkSessions = reader.GetInt32(3), NgSessions = reader.GetInt32(4), PassRate = reader.GetInt32(2) > 0 ? (double)reader.GetInt32(3) / reader.GetInt32(2) * 100 : 0 }); } return statistics; } /// /// 获取工位统计。 /// private async Task> GetStationStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default) { var statistics = new List(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT StationId, StationName, COUNT(*) as TotalSessions, SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) as OkSessions, SUM(CASE WHEN Result = 2 THEN 1 ELSE 0 END) as NgSessions, AVG(DATEDIFF(SECOND, StartedAtUtc, EndedAtUtc)) as AvgProcessingTime FROM mdl_production_session WHERE StartedAtUtc >= @StartTime AND StartedAtUtc <= @EndTime AND EndedAtUtc IS NOT NULL GROUP BY StationId, StationName"; AddParameter(command, "@StartTime", query.StartTime); AddParameter(command, "@EndTime", query.EndTime); using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { var totalSessions = reader.GetInt32(2); var avgProcessingTime = reader.IsDBNull(5) ? 0.0 : reader.GetDouble(5); statistics.Add(new StationStatistics { StationId = reader.GetString(0), StationName = reader.GetString(1), TotalSessions = totalSessions, OkSessions = reader.GetInt32(3), NgSessions = reader.GetInt32(4), PassRate = totalSessions > 0 ? (double)reader.GetInt32(3) / totalSessions * 100 : 0, ThroughputPerHour = avgProcessingTime > 0 ? totalSessions / (avgProcessingTime / 3600) : 0 }); } return statistics; } /// /// 获取操作员统计。 /// private async Task> GetOperatorStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default) { var statistics = new List(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT OperatorId, OperatorName, COUNT(*) as TotalSessions, SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) as OkSessions, SUM(CASE WHEN Result = 2 THEN 1 ELSE 0 END) as NgSessions, AVG(DATEDIFF(SECOND, StartedAtUtc, EndedAtUtc)) as AvgProcessingTime FROM mdl_production_session WHERE StartedAtUtc >= @StartTime AND StartedAtUtc <= @EndTime AND EndedAtUtc IS NOT NULL GROUP BY OperatorId, OperatorName"; AddParameter(command, "@StartTime", query.StartTime); AddParameter(command, "@EndTime", query.EndTime); using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { var totalSessions = reader.GetInt32(2); statistics.Add(new OperatorStatistics { OperatorId = reader.GetString(0), OperatorName = reader.GetString(1), TotalSessions = totalSessions, OkSessions = reader.GetInt32(3), NgSessions = reader.GetInt32(4), PassRate = totalSessions > 0 ? (double)reader.GetInt32(3) / totalSessions * 100 : 0, AverageProcessingTimeSeconds = reader.IsDBNull(5) ? 0.0 : reader.GetDouble(5) }); } return statistics; } /// /// 获取日期统计。 /// private async Task> GetDailyStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default) { var statistics = new List(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT CAST(StartedAtUtc AS DATE) as Date, COUNT(*) as TotalSessions, SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) as OkSessions, SUM(CASE WHEN Result = 2 THEN 1 ELSE 0 END) as NgSessions, AVG(DATEDIFF(SECOND, StartedAtUtc, EndedAtUtc)) as AvgProcessingTime FROM mdl_production_session WHERE StartedAtUtc >= @StartTime AND StartedAtUtc <= @EndTime AND EndedAtUtc IS NOT NULL GROUP BY CAST(StartedAtUtc AS DATE) ORDER BY Date"; AddParameter(command, "@StartTime", query.StartTime); AddParameter(command, "@EndTime", query.EndTime); using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { var totalSessions = reader.GetInt32(1); var avgProcessingTime = reader.IsDBNull(4) ? 0.0 : reader.GetDouble(4); statistics.Add(new DailyStatistics { DateUtc = reader.GetDateTime(0), TotalSessions = totalSessions, OkSessions = reader.GetInt32(2), NgSessions = reader.GetInt32(3), PassRate = totalSessions > 0 ? (double)reader.GetInt32(2) / totalSessions * 100 : 0, ThroughputPerHour = avgProcessingTime > 0 ? totalSessions / (avgProcessingTime / 3600) : 0 }); } return statistics; } /// /// 获取班次统计。 /// private async Task> GetShiftStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default) { var statistics = new List(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT ShiftId, ShiftName, COUNT(*) as TotalSessions, SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) as OkSessions, SUM(CASE WHEN Result = 2 THEN 1 ELSE 0 END) as NgSessions, AVG(DATEDIFF(SECOND, StartedAtUtc, EndedAtUtc)) as AvgProcessingTime FROM mdl_production_session WHERE StartedAtUtc >= @StartTime AND StartedAtUtc <= @EndTime AND EndedAtUtc IS NOT NULL GROUP BY ShiftId, ShiftName"; AddParameter(command, "@StartTime", query.StartTime); AddParameter(command, "@EndTime", query.EndTime); using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { var totalSessions = reader.GetInt32(2); var avgProcessingTime = reader.IsDBNull(4) ? 0.0 : reader.GetDouble(4); statistics.Add(new ShiftStatistics { ShiftId = reader.GetString(0), ShiftName = reader.GetString(1), TotalSessions = totalSessions, OkSessions = reader.GetInt32(3), NgSessions = reader.GetInt32(4), PassRate = totalSessions > 0 ? (double)reader.GetInt32(3) / totalSessions * 100 : 0, ThroughputPerHour = avgProcessingTime > 0 ? totalSessions / (avgProcessingTime / 3600) : 0 }); } return statistics; } #endregion }