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