Files
OrpaonVision/OrpaonVision.Core/Services/ProductionSessionService.cs
2026-04-06 22:04:05 +08:00

649 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging;
using OrpaonVision.Core.Abstractions;
using OrpaonVision.Core.Results;
using OrpaonVision.Model.Production;
using System.Collections.Concurrent;
namespace OrpaonVision.Core.Services;
/// <summary>
/// 产品会话记录服务实现。
/// </summary>
public sealed class ProductionSessionService : IProductionSessionService
{
private readonly ILogger<ProductionSessionService> _logger;
private readonly ConcurrentDictionary<Guid, ProductionSessionModel> _sessions = new();
private readonly object _lock = new object();
/// <summary>
/// 构造函数。
/// </summary>
public ProductionSessionService(ILogger<ProductionSessionService> logger)
{
_logger = logger;
InitializeSampleData();
}
/// <inheritdoc />
public Result<ProductionSessionModel> CreateSession(CreateProductionSessionRequest request)
{
try
{
_logger.LogInformation("正在创建产品会话,产品类型: {ProductType}, 工位: {Station}, 操作员: {Operator}",
request.ProductTypeCode, request.StationId, request.OperatorId);
// 检查当前是否已有活动会话
var currentSession = GetCurrentSession(request.StationId);
if (currentSession.Succeeded && currentSession.Data != null)
{
return Result<ProductionSessionModel>.Fail("ACTIVE_SESSION_EXISTS",
$"工位 {request.StationId} 已存在活动会话。");
}
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 = DateTime.UtcNow,
Status = ProductionSessionStatus.InProgress,
Result = ProductionSessionResult.Pending,
CurrentLayer = 1,
TotalLayers = request.TotalLayers,
Remark = request.Remark,
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow,
CreatedBy = request.CreatedBy,
UpdatedBy = request.CreatedBy
};
_sessions.TryAdd(session.SessionId, session);
_logger.LogInformation("产品会话创建成功会话ID: {SessionId}", session.SessionId);
return Result<ProductionSessionModel>.Success(session, message: "产品会话创建成功。");
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "创建产品会话失败。TraceId: {TraceId}", traceId);
var errorResult = Result.FromException(ex, "CREATE_SESSION_FAILED", "创建产品会话失败。", traceId);
return Result<ProductionSessionModel>.FailWithTrace(errorResult.Code, errorResult.Message, errorResult.TraceId ?? traceId, errorResult.Errors.ToArray());
}
}
/// <inheritdoc />
public Result UpdateSessionStatus(Guid sessionId, ProductionSessionStatus status, string? reason = null)
{
try
{
if (!_sessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在。");
}
lock (_lock)
{
session.Status = status;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
if (status == ProductionSessionStatus.CompletedNg && !string.IsNullOrEmpty(reason))
{
session.NgReason = reason;
}
if (status == ProductionSessionStatus.CompletedOk || status == ProductionSessionStatus.CompletedNg || status == ProductionSessionStatus.Cancelled)
{
session.EndedAtUtc = DateTime.UtcNow;
}
}
_logger.LogInformation("会话状态更新成功会话ID: {SessionId}, 状态: {Status}", sessionId, status);
return Result.Success("会话状态更新成功。");
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "更新会话状态失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "UPDATE_SESSION_STATUS_FAILED", "更新会话状态失败。", traceId);
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public Result UpdateSessionProgress(Guid sessionId, int currentLayer)
{
try
{
if (!_sessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在。");
}
lock (_lock)
{
session.CurrentLayer = currentLayer;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
}
_logger.LogDebug("会话进度更新成功会话ID: {SessionId}, 当前层: {CurrentLayer}", sessionId, currentLayer);
return Result.Success("会话进度更新成功。");
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "更新会话进度失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "UPDATE_SESSION_PROGRESS_FAILED", "更新会话进度失败。", traceId);
return Result.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public Result CompleteSession(Guid sessionId, ProductionSessionResult result, string? ngReason = null, string? remark = null)
{
try
{
if (!_sessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在。");
}
lock (_lock)
{
session.Result = result;
session.Status = result == ProductionSessionResult.Ok ? ProductionSessionStatus.CompletedOk : ProductionSessionStatus.CompletedNg;
session.EndedAtUtc = DateTime.UtcNow;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
if (result == ProductionSessionResult.Ng && !string.IsNullOrEmpty(ngReason))
{
session.NgReason = ngReason;
}
if (!string.IsNullOrEmpty(remark))
{
session.Remark = remark;
}
}
_logger.LogInformation("会话完成会话ID: {SessionId}, 结果: {Result}", sessionId, result);
return Result.Success("会话完成。");
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "完成会话失败。TraceId: {TraceId}", traceId);
var errorResult = Result.FromException(ex, "COMPLETE_SESSION_FAILED", "完成会话失败。", traceId);
return Result.FailWithTrace(errorResult.Code, errorResult.Message, errorResult.TraceId ?? traceId, errorResult.Errors.ToArray());
}
}
/// <inheritdoc />
public Result CancelSession(Guid sessionId, string? reason = null)
{
try
{
if (!_sessions.TryGetValue(sessionId, out var session))
{
return Result.Fail("SESSION_NOT_FOUND", $"会话 {sessionId} 不存在。");
}
lock (_lock)
{
session.Status = ProductionSessionStatus.Cancelled;
session.EndedAtUtc = DateTime.UtcNow;
session.UpdatedAtUtc = DateTime.UtcNow;
session.UpdatedBy = "System";
if (!string.IsNullOrEmpty(reason))
{
session.Remark = reason;
}
}
_logger.LogInformation("会话取消会话ID: {SessionId}, 原因: {Reason}", sessionId, reason);
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 Result<ProductionSessionModel?> GetSession(Guid sessionId)
{
try
{
_sessions.TryGetValue(sessionId, out var session);
if (session == null)
{
_logger.LogWarning("会话不存在会话ID: {SessionId}", sessionId);
return Result<ProductionSessionModel?>.Success(null, message: "会话不存在。");
}
_logger.LogDebug("获取会话成功会话ID: {SessionId}", sessionId);
return Result<ProductionSessionModel?>.Success(session, message: "获取会话成功。");
}
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 Result<PagedResult<ProductionSessionModel>> GetSessions(GetProductionSessionsRequest request)
{
try
{
_logger.LogInformation("正在获取会话列表,产品类型: {ProductType}, 工位: {Station}, 页码: {PageIndex}",
request.ProductTypeCode, request.StationId, request.PageIndex);
var query = _sessions.Values.AsQueryable();
// 应用筛选条件
if (!string.IsNullOrEmpty(request.ProductTypeCode))
{
query = query.Where(s => s.ProductTypeCode.Equals(request.ProductTypeCode, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrEmpty(request.StationId))
{
query = query.Where(s => s.StationId.Equals(request.StationId, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrEmpty(request.OperatorId))
{
query = query.Where(s => s.OperatorId.Equals(request.OperatorId, StringComparison.OrdinalIgnoreCase));
}
if (request.Status.HasValue)
{
query = query.Where(s => s.Status == request.Status.Value);
}
if (request.Result.HasValue)
{
query = query.Where(s => s.Result == request.Result.Value);
}
if (request.StartTimeUtc.HasValue)
{
query = query.Where(s => s.StartedAtUtc >= request.StartTimeUtc.Value);
}
if (request.EndTimeUtc.HasValue)
{
query = query.Where(s => s.EndedAtUtc <= request.EndTimeUtc.Value);
}
// 排序
query = query.OrderByDescending(s => s.StartedAtUtc);
var totalCount = query.Count();
var items = query.Skip((request.PageIndex - 1) * request.PageSize).Take(request.PageSize).ToList();
var pagedResult = new PagedResult<ProductionSessionModel>
{
Items = items,
TotalCount = totalCount,
PageIndex = request.PageIndex,
PageSize = request.PageSize
};
_logger.LogInformation("获取会话列表成功,总数: {TotalCount}, 当前页: {Count}", totalCount, items.Count);
return Result<PagedResult<ProductionSessionModel>>.Success(pagedResult, message: "获取会话列表成功。");
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "获取会话列表失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "GET_SESSIONS_FAILED", "获取会话列表失败。", traceId);
return Result<PagedResult<ProductionSessionModel>>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public Result<ProductionSessionModel?> GetCurrentSession(string stationId)
{
try
{
var currentSession = _sessions.Values
.FirstOrDefault(s => s.StationId.Equals(stationId, StringComparison.OrdinalIgnoreCase)
&& s.Status == ProductionSessionStatus.InProgress);
_logger.LogDebug("获取当前会话完成,工位: {StationId}, 会话ID: {SessionId}",
stationId, currentSession?.SessionId);
return Result<ProductionSessionModel?>.Success(currentSession, message: "获取当前会话成功。");
}
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 Result<ProductionSessionStatistics> GetStatistics(GetProductionSessionStatisticsRequest request)
{
try
{
_logger.LogInformation("正在获取会话统计信息,时间范围: {StartTime} - {EndTime}",
request.StartTimeUtc, request.EndTimeUtc);
var query = _sessions.Values.Where(s => s.StartedAtUtc >= request.StartTimeUtc && s.StartedAtUtc <= request.EndTimeUtc);
// 应用筛选条件
if (!string.IsNullOrEmpty(request.ProductTypeCode))
{
query = query.Where(s => s.ProductTypeCode.Equals(request.ProductTypeCode, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrEmpty(request.StationId))
{
query = query.Where(s => s.StationId.Equals(request.StationId, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrEmpty(request.OperatorId))
{
query = query.Where(s => s.OperatorId.Equals(request.OperatorId, StringComparison.OrdinalIgnoreCase));
}
var sessions = query.ToList();
var statistics = CalculateStatistics(sessions, request.StartTimeUtc, request.EndTimeUtc);
_logger.LogInformation("获取会话统计信息成功,总会话数: {TotalSessions}, 合格率: {PassRate:P1}",
statistics.TotalSessions, statistics.PassRate);
return Result<ProductionSessionStatistics>.Success(statistics, message: "获取会话统计信息成功。");
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "获取会话统计信息失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "GET_STATISTICS_FAILED", "获取会话统计信息失败。", traceId);
return Result<ProductionSessionStatistics>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
/// <inheritdoc />
public Result<byte[]> ExportSessions(ExportProductionSessionsRequest request)
{
try
{
_logger.LogInformation("正在导出会话记录,格式: {Format}", request.Format);
// 获取要导出的会话
var getSessionsRequest = new GetProductionSessionsRequest
{
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 sessionsResult = GetSessions(getSessionsRequest);
if (!sessionsResult.Succeeded || sessionsResult.Data == null)
{
return Result<byte[]>.Fail("EXPORT_FAILED", "获取要导出的会话失败。");
}
var sessions = sessionsResult.Data.Items.ToList();
var exportData = GenerateExportData(sessions, request.Format);
_logger.LogInformation("导出会话记录成功,记录数: {Count}, 格式: {Format}", sessions.Count, request.Format);
return Result<byte[]>.Success(exportData, message: "导出会话记录成功。");
}
catch (Exception ex)
{
var traceId = Guid.NewGuid().ToString("N");
_logger.LogError(ex, "导出会话记录失败。TraceId: {TraceId}", traceId);
var result = Result.FromException(ex, "EXPORT_SESSIONS_FAILED", "导出会话记录失败。", traceId);
return Result<byte[]>.FailWithTrace(result.Code, result.Message, result.TraceId ?? traceId, result.Errors.ToArray());
}
}
private ProductionSessionStatistics CalculateStatistics(List<ProductionSessionModel> sessions, DateTime startTime, DateTime endTime)
{
var statistics = new ProductionSessionStatistics
{
TotalSessions = sessions.Count,
OkSessions = sessions.Count(s => s.Result == ProductionSessionResult.Ok),
NgSessions = sessions.Count(s => s.Result == ProductionSessionResult.Ng),
InProgressSessions = sessions.Count(s => s.Status == ProductionSessionStatus.InProgress),
CancelledSessions = sessions.Count(s => s.Status == ProductionSessionStatus.Cancelled),
PausedSessions = sessions.Count(s => s.Status == ProductionSessionStatus.Paused)
};
statistics.PassRate = statistics.TotalSessions > 0 ? (double)statistics.OkSessions / statistics.TotalSessions : 0;
// 计算平均处理时间
var completedSessions = sessions.Where(s => s.EndedAtUtc.HasValue).ToList();
if (completedSessions.Any())
{
statistics.AverageProcessingTimeSeconds = completedSessions
.Average(s => (s.EndedAtUtc!.Value - s.StartedAtUtc).TotalSeconds);
}
// 计算每小时处理量
var totalHours = (endTime - startTime).TotalHours;
if (totalHours > 0)
{
statistics.ThroughputPerHour = statistics.TotalSessions / totalHours;
}
// 按产品类型分组统计
statistics.ByProductType = sessions
.GroupBy(s => new { s.ProductTypeCode, s.ProductTypeName })
.Select(g => new ProductTypeStatistics
{
ProductTypeCode = g.Key.ProductTypeCode,
ProductTypeName = g.Key.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() : 0
})
.ToList();
// 按工位分组统计
statistics.ByStation = sessions
.GroupBy(s => new { s.StationId, s.StationName })
.Select(g => new StationStatistics
{
StationId = g.Key.StationId,
StationName = g.Key.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() : 0
})
.ToList();
// 按操作员分组统计
statistics.ByOperator = sessions
.GroupBy(s => new { s.OperatorId, s.OperatorName })
.Select(g => new OperatorStatistics
{
OperatorId = g.Key.OperatorId,
OperatorName = g.Key.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() : 0
})
.ToList();
// 按日期分组统计
statistics.ByDate = 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() : 0
})
.OrderBy(d => d.DateUtc)
.ToList();
return statistics;
}
private byte[] GenerateExportData(List<ProductionSessionModel> sessions, ExportFormat format)
{
return format switch
{
ExportFormat.Csv => GenerateCsvExport(sessions),
ExportFormat.Json => GenerateJsonExport(sessions),
ExportFormat.Excel => GenerateExcelExport(sessions),
_ => GenerateCsvExport(sessions)
};
}
private byte[] GenerateCsvExport(List<ProductionSessionModel> sessions)
{
using var ms = new System.IO.MemoryStream();
using var writer = new System.IO.StreamWriter(ms, System.Text.Encoding.UTF8);
// 写入CSV头部
writer.WriteLine("会话ID,产品类型,工位,操作员,开始时间,结束时间,状态,结果,当前层,总层数,NG原因,备注");
// 写入数据行
foreach (var session in sessions)
{
writer.WriteLine($"{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},{session.CurrentLayer},{session.TotalLayers}," +
$"{session.NgReason ?? ""},{session.Remark ?? ""}");
}
writer.Flush();
return ms.ToArray();
}
private byte[] GenerateJsonExport(List<ProductionSessionModel> sessions)
{
var json = System.Text.Json.JsonSerializer.Serialize(sessions, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
return System.Text.Encoding.UTF8.GetBytes(json);
}
private byte[] GenerateExcelExport(List<ProductionSessionModel> sessions)
{
// 简化的Excel导出实现实际项目中可以使用EPPlus等库
// 这里暂时返回CSV格式
return GenerateCsvExport(sessions);
}
private void InitializeSampleData()
{
_logger.LogInformation("正在初始化示例产品会话数据");
var sampleSessions = new List<ProductionSessionModel>
{
new()
{
SessionId = Guid.NewGuid(),
ProductTypeCode = "VFD-001",
ProductTypeName = "变频器V1.0",
StationId = "ST-001",
StationName = "装配工位1",
OperatorId = "OP-001",
OperatorName = "张三",
ShiftId = "SH-001",
ShiftName = "白班",
StartedAtUtc = DateTime.UtcNow.AddHours(-2),
EndedAtUtc = DateTime.UtcNow.AddHours(-1.8),
Status = ProductionSessionStatus.CompletedOk,
Result = ProductionSessionResult.Ok,
CurrentLayer = 3,
TotalLayers = 3,
CreatedAtUtc = DateTime.UtcNow.AddHours(-2),
UpdatedAtUtc = DateTime.UtcNow.AddHours(-1.8),
CreatedBy = "System",
UpdatedBy = "System"
},
new()
{
SessionId = Guid.NewGuid(),
ProductTypeCode = "VFD-001",
ProductTypeName = "变频器V1.0",
StationId = "ST-001",
StationName = "装配工位1",
OperatorId = "OP-002",
OperatorName = "李四",
ShiftId = "SH-001",
ShiftName = "白班",
StartedAtUtc = DateTime.UtcNow.AddHours(-1.5),
EndedAtUtc = DateTime.UtcNow.AddHours(-1.2),
Status = ProductionSessionStatus.CompletedNg,
Result = ProductionSessionResult.Ng,
CurrentLayer = 2,
TotalLayers = 3,
NgReason = "第2层检测到缺陷",
CreatedAtUtc = DateTime.UtcNow.AddHours(-1.5),
UpdatedAtUtc = DateTime.UtcNow.AddHours(-1.2),
CreatedBy = "System",
UpdatedBy = "System"
},
new()
{
SessionId = Guid.NewGuid(),
ProductTypeCode = "VFD-002",
ProductTypeName = "变频器V2.0",
StationId = "ST-002",
StationName = "装配工位2",
OperatorId = "OP-003",
OperatorName = "王五",
ShiftId = "SH-002",
ShiftName = "夜班",
StartedAtUtc = DateTime.UtcNow.AddMinutes(-30),
Status = ProductionSessionStatus.InProgress,
Result = ProductionSessionResult.Pending,
CurrentLayer = 1,
TotalLayers = 4,
CreatedAtUtc = DateTime.UtcNow.AddMinutes(-30),
UpdatedAtUtc = DateTime.UtcNow.AddMinutes(-30),
CreatedBy = "System",
UpdatedBy = "System"
}
};
foreach (var session in sampleSessions)
{
_sessions.TryAdd(session.SessionId, session);
}
_logger.LogInformation("示例产品会话数据初始化完成,共 {Count} 条记录", sampleSessions.Count);
}
}