1468 lines
62 KiB
C#
1468 lines
62 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// SQL产品会话仓储实现。
|
|
/// </summary>
|
|
public sealed class SqlProductionSessionRepository : IProductionSessionRepository
|
|
{
|
|
private readonly ILogger<SqlProductionSessionRepository> _logger;
|
|
private readonly SessionPersistenceOptions _options;
|
|
private readonly ConcurrentDictionary<string, SqlConnection> _connections = new();
|
|
|
|
/// <summary>
|
|
/// 构造函数。
|
|
/// </summary>
|
|
public SqlProductionSessionRepository(
|
|
ILogger<SqlProductionSessionRepository> logger,
|
|
IOptions<SessionPersistenceOptions> options)
|
|
{
|
|
_logger = logger;
|
|
_options = options.Value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result> 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());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result> 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());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result> 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());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<ProductionSessionModel?>> 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<ProductionSessionModel?>.Success(session);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("未找到会话: {SessionId}", sessionId);
|
|
return Result<ProductionSessionModel?>.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<ProductionSessionModel?>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<ProductionSessionModel?>> 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<ProductionSessionModel?>.Success(session);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("工位 {StationId} 无活动会话", stationId);
|
|
return Result<ProductionSessionModel?>.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<ProductionSessionModel?>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<IReadOnlyList<ProductionSessionModel>>> 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<ProductionSessionModel>();
|
|
using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
sessions.Add(MapToSession(reader));
|
|
}
|
|
|
|
_logger.LogDebug("查询到 {Count} 个会话", sessions.Count);
|
|
return Result<IReadOnlyList<ProductionSessionModel>>.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<IReadOnlyList<ProductionSessionModel>>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<PagedSessionResult>> 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<ProductionSessionModel>();
|
|
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<PagedSessionResult>.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<PagedSessionResult>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<BatchCreateResult>> BatchCreateAsync(IReadOnlyList<ProductionSessionModel> sessions, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogInformation("批量创建会话: 数量={Count}", sessions.Count);
|
|
|
|
var totalCount = sessions.Count;
|
|
var successCount = 0;
|
|
var failureCount = 0;
|
|
var failedIndexes = new List<int>();
|
|
var errors = new List<string>();
|
|
|
|
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<BatchCreateResult>.Success(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
var traceId = Guid.NewGuid().ToString("N");
|
|
_logger.LogError(ex, "批量创建会话事务失败。TraceId: {TraceId}", traceId);
|
|
return Result<BatchCreateResult>.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<BatchCreateResult>.FailWithTrace("BATCH_CREATE_FAILED", "批量创建会话失败。", traceId, ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<BatchUpdateResult>> BatchUpdateAsync(IReadOnlyList<ProductionSessionModel> sessions, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogInformation("批量更新会话: 数量={Count}", sessions.Count);
|
|
|
|
var successCount = 0;
|
|
var failureCount = 0;
|
|
var failedIndexes = new List<int>();
|
|
var errors = new List<string>();
|
|
|
|
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<BatchUpdateResult>.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<BatchUpdateResult>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<BatchDeleteResult>> BatchDeleteAsync(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>();
|
|
|
|
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<BatchDeleteResult>.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<BatchDeleteResult>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<SessionStatistics>> 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<ProductTypeStatistics>();
|
|
var byStation = new List<StationStatistics>();
|
|
var byOperator = new List<OperatorStatistics>();
|
|
var byDate = new List<DailyStatistics>();
|
|
var byShift = new List<ShiftStatistics>();
|
|
|
|
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<SessionStatistics>.Success(statistics);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var traceId = Guid.NewGuid().ToString("N");
|
|
_logger.LogError(ex, "获取会话统计信息失败。TraceId: {TraceId}", traceId);
|
|
return Result<SessionStatistics>.FailWithTrace("GET_STATISTICS_FAILED", "获取会话统计信息失败。", traceId, ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<CleanupResult>> 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<CleanupResult>.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<CleanupResult>.FailWithTrace("CLEANUP_FAILED", "清理过期会话失败。", traceId, ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<bool>> 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<bool>.Success(count > 0);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var traceId = Guid.NewGuid().ToString("N");
|
|
_logger.LogError(ex, "检查会话是否存在失败。TraceId: {TraceId}", traceId);
|
|
return Result<bool>.FailWithTrace("EXISTS_CHECK_FAILED", "检查会话是否存在失败。", traceId, ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Result<int>> 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<int>.Success(count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var traceId = Guid.NewGuid().ToString("N");
|
|
_logger.LogError(ex, "获取会话数量失败。TraceId: {TraceId}", traceId);
|
|
return Result<int>.FailWithTrace("COUNT_FAILED", "获取会话数量失败。", traceId, ex.Message);
|
|
}
|
|
}
|
|
|
|
#region 私有方法
|
|
|
|
/// <summary>
|
|
/// 获取数据库连接。
|
|
/// </summary>
|
|
private async Task<SqlConnection> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 添加参数。
|
|
/// </summary>
|
|
private static void AddParameter(SqlCommand command, string name, object value)
|
|
{
|
|
var parameter = command.CreateParameter();
|
|
parameter.ParameterName = name;
|
|
parameter.Value = value;
|
|
command.Parameters.Add(parameter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从DataReader映射到会话模型。
|
|
/// </summary>
|
|
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)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建查询SQL。
|
|
/// </summary>
|
|
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<string>();
|
|
var parameters = new Dictionary<string, object>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建计数SQL。
|
|
/// </summary>
|
|
private string BuildCountSql(SessionQuery query)
|
|
{
|
|
var querySql = BuildQuerySql(query);
|
|
return $"SELECT COUNT(*) FROM ({querySql}) AS CountQuery";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建查询参数。
|
|
/// </summary>
|
|
private Dictionary<string, object> BuildQueryParameters(SessionQuery query)
|
|
{
|
|
var parameters = new Dictionary<string, object>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建基础统计SQL。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 构建时间统计SQL。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取产品类型统计。
|
|
/// </summary>
|
|
private async Task<List<ProductTypeStatistics>> GetProductTypeStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default)
|
|
{
|
|
var statistics = new List<ProductTypeStatistics>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取工位统计。
|
|
/// </summary>
|
|
private async Task<List<StationStatistics>> GetStationStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default)
|
|
{
|
|
var statistics = new List<StationStatistics>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取操作员统计。
|
|
/// </summary>
|
|
private async Task<List<OperatorStatistics>> GetOperatorStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default)
|
|
{
|
|
var statistics = new List<OperatorStatistics>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取日期统计。
|
|
/// </summary>
|
|
private async Task<List<DailyStatistics>> GetDailyStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default)
|
|
{
|
|
var statistics = new List<DailyStatistics>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取班次统计。
|
|
/// </summary>
|
|
private async Task<List<ShiftStatistics>> GetShiftStatisticsAsync(SessionStatisticsQuery query, SqlConnection connection, CancellationToken cancellationToken = default)
|
|
{
|
|
var statistics = new List<ShiftStatistics>();
|
|
|
|
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
|
|
}
|